diff options
Diffstat (limited to 'libs/android-transcoder/src/main/java/net/ypresto/androidtranscoder/engine/MediaTranscoderEngine.java')
-rw-r--r-- | libs/android-transcoder/src/main/java/net/ypresto/androidtranscoder/engine/MediaTranscoderEngine.java | 219 |
1 files changed, 219 insertions, 0 deletions
diff --git a/libs/android-transcoder/src/main/java/net/ypresto/androidtranscoder/engine/MediaTranscoderEngine.java b/libs/android-transcoder/src/main/java/net/ypresto/androidtranscoder/engine/MediaTranscoderEngine.java new file mode 100644 index 000000000..64a1edef4 --- /dev/null +++ b/libs/android-transcoder/src/main/java/net/ypresto/androidtranscoder/engine/MediaTranscoderEngine.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2014 Yuya Tanaka + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.ypresto.androidtranscoder.engine; + +import android.media.MediaExtractor; +import android.media.MediaFormat; +import android.media.MediaMetadataRetriever; +import android.media.MediaMuxer; +import android.util.Log; + +import net.ypresto.androidtranscoder.format.MediaFormatStrategy; +import net.ypresto.androidtranscoder.utils.MediaExtractorUtils; + +import java.io.FileDescriptor; +import java.io.IOException; + +/** + * Internal engine, do not use this directly. + */ +// TODO: treat encrypted data +public class MediaTranscoderEngine { + private static final String TAG = "MediaTranscoderEngine"; + private static final double PROGRESS_UNKNOWN = -1.0; + private static final long SLEEP_TO_WAIT_TRACK_TRANSCODERS = 10; + private static final long PROGRESS_INTERVAL_STEPS = 10; + private FileDescriptor mInputFileDescriptor; + private TrackTranscoder mVideoTrackTranscoder; + private TrackTranscoder mAudioTrackTranscoder; + private MediaExtractor mExtractor; + private MediaMuxer mMuxer; + private volatile double mProgress; + private ProgressCallback mProgressCallback; + private long mDurationUs; + + /** + * Do not use this constructor unless you know what you are doing. + */ + public MediaTranscoderEngine() { + } + + public void setDataSource(FileDescriptor fileDescriptor) { + mInputFileDescriptor = fileDescriptor; + } + + public ProgressCallback getProgressCallback() { + return mProgressCallback; + } + + public void setProgressCallback(ProgressCallback progressCallback) { + mProgressCallback = progressCallback; + } + + /** + * NOTE: This method is thread safe. + */ + public double getProgress() { + return mProgress; + } + + /** + * Run video transcoding. Blocks current thread. + * Audio data will not be transcoded; original stream will be wrote to output file. + * + * @param outputPath File path to output transcoded video file. + * @param formatStrategy Output format strategy. + * @throws IOException when input or output file could not be opened. + * @throws InvalidOutputFormatException when output format is not supported. + * @throws InterruptedException when cancel to transcode. + */ + public void transcodeVideo(String outputPath, MediaFormatStrategy formatStrategy) throws IOException, InterruptedException { + if (outputPath == null) { + throw new NullPointerException("Output path cannot be null."); + } + if (mInputFileDescriptor == null) { + throw new IllegalStateException("Data source is not set."); + } + try { + // NOTE: use single extractor to keep from running out audio track fast. + mExtractor = new MediaExtractor(); + mExtractor.setDataSource(mInputFileDescriptor); + mMuxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); + setupMetadata(); + setupTrackTranscoders(formatStrategy); + runPipelines(); + mMuxer.stop(); + } finally { + try { + if (mVideoTrackTranscoder != null) { + mVideoTrackTranscoder.release(); + mVideoTrackTranscoder = null; + } + if (mAudioTrackTranscoder != null) { + mAudioTrackTranscoder.release(); + mAudioTrackTranscoder = null; + } + if (mExtractor != null) { + mExtractor.release(); + mExtractor = null; + } + } catch (RuntimeException e) { + // Too fatal to make alive the app, because it may leak native resources. + //noinspection ThrowFromFinallyBlock + throw new Error("Could not shutdown extractor, codecs and muxer pipeline.", e); + } + try { + if (mMuxer != null) { + mMuxer.release(); + mMuxer = null; + } + } catch (RuntimeException e) { + Log.e(TAG, "Failed to release muxer.", e); + } + } + } + + private void setupMetadata() throws IOException { + MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); + mediaMetadataRetriever.setDataSource(mInputFileDescriptor); + + String rotationString = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); + try { + mMuxer.setOrientationHint(Integer.parseInt(rotationString)); + } catch (NumberFormatException e) { + // skip + } + + // TODO: parse ISO 6709 + // String locationString = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION); + // mMuxer.setLocation(Integer.getInteger(rotationString, 0)); + + try { + mDurationUs = Long.parseLong(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)) * 1000; + } catch (NumberFormatException e) { + mDurationUs = -1; + } + Log.d(TAG, "Duration (us): " + mDurationUs); + } + + private void setupTrackTranscoders(MediaFormatStrategy formatStrategy) { + MediaExtractorUtils.TrackResult trackResult = MediaExtractorUtils.getFirstVideoAndAudioTrack(mExtractor); + MediaFormat videoOutputFormat = formatStrategy.createVideoOutputFormat(trackResult.mVideoTrackFormat); + MediaFormat audioOutputFormat = formatStrategy.createAudioOutputFormat(trackResult.mAudioTrackFormat); + if (videoOutputFormat == null && audioOutputFormat == null) { + throw new InvalidOutputFormatException("MediaFormatStrategy returned pass-through for both video and audio. No transcoding is necessary."); + } + QueuedMuxer queuedMuxer = new QueuedMuxer(mMuxer, new QueuedMuxer.Listener() { + @Override + public void onDetermineOutputFormat() { + MediaFormatValidator.validateVideoOutputFormat(mVideoTrackTranscoder.getDeterminedFormat()); + MediaFormatValidator.validateAudioOutputFormat(mAudioTrackTranscoder.getDeterminedFormat()); + } + }); + + if (videoOutputFormat == null) { + mVideoTrackTranscoder = new PassThroughTrackTranscoder(mExtractor, trackResult.mVideoTrackIndex, queuedMuxer, QueuedMuxer.SampleType.VIDEO); + } else { + mVideoTrackTranscoder = new VideoTrackTranscoder(mExtractor, trackResult.mVideoTrackIndex, videoOutputFormat, queuedMuxer); + } + mVideoTrackTranscoder.setup(); + if (audioOutputFormat == null) { + mAudioTrackTranscoder = new PassThroughTrackTranscoder(mExtractor, trackResult.mAudioTrackIndex, queuedMuxer, QueuedMuxer.SampleType.AUDIO); + } else { + mAudioTrackTranscoder = new AudioTrackTranscoder(mExtractor, trackResult.mAudioTrackIndex, audioOutputFormat, queuedMuxer); + } + mAudioTrackTranscoder.setup(); + mExtractor.selectTrack(trackResult.mVideoTrackIndex); + mExtractor.selectTrack(trackResult.mAudioTrackIndex); + } + + private void runPipelines() { + long loopCount = 0; + if (mDurationUs <= 0) { + double progress = PROGRESS_UNKNOWN; + mProgress = progress; + if (mProgressCallback != null) mProgressCallback.onProgress(progress); // unknown + } + while (!(mVideoTrackTranscoder.isFinished() && mAudioTrackTranscoder.isFinished())) { + boolean stepped = mVideoTrackTranscoder.stepPipeline() + || mAudioTrackTranscoder.stepPipeline(); + loopCount++; + if (mDurationUs > 0 && loopCount % PROGRESS_INTERVAL_STEPS == 0) { + double videoProgress = mVideoTrackTranscoder.isFinished() ? 1.0 : Math.min(1.0, (double) mVideoTrackTranscoder.getWrittenPresentationTimeUs() / mDurationUs); + double audioProgress = mAudioTrackTranscoder.isFinished() ? 1.0 : Math.min(1.0, (double) mAudioTrackTranscoder.getWrittenPresentationTimeUs() / mDurationUs); + double progress = (videoProgress + audioProgress) / 2.0; + mProgress = progress; + if (mProgressCallback != null) mProgressCallback.onProgress(progress); + } + if (!stepped) { + try { + Thread.sleep(SLEEP_TO_WAIT_TRACK_TRANSCODERS); + } catch (InterruptedException e) { + // nothing to do + } + } + } + } + + public interface ProgressCallback { + /** + * Called to notify progress. Same thread which initiated transcode is used. + * + * @param progress Progress in [0.0, 1.0] range, or negative value if progress is unknown. + */ + void onProgress(double progress); + } +} |