aboutsummaryrefslogtreecommitdiffstats
path: root/libs/android-transcoder/src/main/java/net/ypresto/androidtranscoder/engine/MediaTranscoderEngine.java
diff options
context:
space:
mode:
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.java219
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);
+ }
+}