From dca875bd0bc713c809e319ddb0adc3bab2d080a6 Mon Sep 17 00:00:00 2001 From: ztenghui Date: Mon, 8 Apr 2013 14:37:53 -0700 Subject: Use MediaMuxer for the video trim and mute. bug:8548085 Change-Id: I55955285ee141ebab6437950a154280cc5fefcc0 --- .../com/android/gallery3d/common/ApiHelper.java | 4 + src/com/android/gallery3d/app/TrimVideo.java | 2 +- src/com/android/gallery3d/app/VideoUtils.java | 208 ++++++++++++++++++--- 3 files changed, 183 insertions(+), 31 deletions(-) diff --git a/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java b/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java index b0b1fe070..864e1305f 100644 --- a/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java +++ b/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java @@ -37,6 +37,7 @@ public class ApiHelper { public static final int ICE_CREAM_SANDWICH_MR1 = 15; public static final int JELLY_BEAN = 16; public static final int JELLY_BEAN_MR1 = 17; + public static final int JELLY_BEAN_MR2 = 18; } public static final boolean AT_LEAST_16 = Build.VERSION.SDK_INT >= 16; @@ -188,6 +189,9 @@ public class ApiHelper { public static final boolean HAS_CANCELLATION_SIGNAL = Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; + public static final boolean HAS_MEDIA_MUXER = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2; + public static int getIntFieldIfExists(Class klass, String fieldName, Class obj, int defaultVal) { try { diff --git a/src/com/android/gallery3d/app/TrimVideo.java b/src/com/android/gallery3d/app/TrimVideo.java index baab88990..1e7728162 100644 --- a/src/com/android/gallery3d/app/TrimVideo.java +++ b/src/com/android/gallery3d/app/TrimVideo.java @@ -237,7 +237,7 @@ public class TrimVideo extends Activity implements public void run() { try { VideoUtils.startTrim(mSrcFile, mDstFileInfo.mFile, - mTrimStartTime, mTrimEndTime, mVideoView.getDuration()); + mTrimStartTime, mTrimEndTime); // Update the database for adding a new video file. SaveVideoFileUtils.insertContent(mDstFileInfo, getContentResolver(), mUri); diff --git a/src/com/android/gallery3d/app/VideoUtils.java b/src/com/android/gallery3d/app/VideoUtils.java index 8ffc3d5eb..a3c3ef273 100644 --- a/src/com/android/gallery3d/app/VideoUtils.java +++ b/src/com/android/gallery3d/app/VideoUtils.java @@ -19,6 +19,14 @@ package com.android.gallery3d.app; +import android.media.MediaCodec.BufferInfo; +import android.media.MediaExtractor; +import android.media.MediaFormat; +import android.media.MediaMetadataRetriever; +import android.media.MediaMuxer; +import android.util.Log; + +import com.android.gallery3d.common.ApiHelper; import com.android.gallery3d.util.SaveVideoFileInfo; import com.coremedia.iso.IsoFile; import com.coremedia.iso.boxes.TimeToSampleBox; @@ -29,17 +37,49 @@ import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator; import com.googlecode.mp4parser.authoring.tracks.CroppedTrack; import java.io.File; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; +import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.Arrays; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; public class VideoUtils { + private static final String LOGTAG = "VideoUtils"; + private static final int DEFAULT_BUFFER_SIZE = 1 * 1024 * 1024; + + /** + * Remove the sound track. + */ + public static void startMute(String filePath, SaveVideoFileInfo dstFileInfo) + throws IOException { + if (ApiHelper.HAS_MEDIA_MUXER) { + genVideoUsingMuxer(filePath, dstFileInfo.mFile.getPath(), -1, -1, + false, true); + } else { + startMuteUsingMp4Parser(filePath, dstFileInfo); + } + } - public static void startMute(String filePath, SaveVideoFileInfo dstFileInfo) throws IOException { + /** + * Shortens/Crops tracks + */ + public static void startTrim(File src, File dst, int startMs, int endMs) + throws IOException { + if (ApiHelper.HAS_MEDIA_MUXER) { + genVideoUsingMuxer(src.getPath(), dst.getPath(), startMs, endMs, + true, true); + } else { + trimUsingMp4Parser(src, dst, startMs, endMs); + } + } + + private static void startMuteUsingMp4Parser(String filePath, + SaveVideoFileInfo dstFileInfo) throws FileNotFoundException, IOException { File dst = dstFileInfo.mFile; File src = new File(filePath); RandomAccessFile randomAccessFile = new RandomAccessFile(src, "r"); @@ -67,16 +107,123 @@ public class VideoUtils { IsoFile out = new DefaultMp4Builder().build(movie); FileOutputStream fos = new FileOutputStream(dst); FileChannel fc = fos.getChannel(); - out.getBox(fc); // This one build up the memory. + out.getBox(fc); // This one build up the memory. fc.close(); fos.close(); } /** - * Shortens/Crops a track + * @param srcPath the path of source video file. + * @param dstPath the path of destination video file. + * @param startMs starting time in milliseconds for trimming. Set to + * negative if starting from beginning. + * @param endMs end time for trimming in milliseconds. Set to negative if + * no trimming at the end. + * @param useAudio true if keep the audio track from the source. + * @param useVideo true if keep the video track from the source. + * @throws IOException */ - public static void startTrim(File src, File dst, int startMs, int endMs, int totalMs) throws IOException { + private static void genVideoUsingMuxer(String srcPath, String dstPath, + int startMs, int endMs, boolean useAudio, boolean useVideo) + throws IOException { + // Set up MediaExtractor to read from the source. + MediaExtractor extractor = new MediaExtractor(); + extractor.setDataSource(srcPath); + + int trackCount = extractor.getTrackCount(); + + // Set up MediaMuxer for the destination. + MediaMuxer muxer; + muxer = new MediaMuxer(dstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); + + // Set up the tracks and retrieve the max buffer size for selected + // tracks. + HashMap indexMap = new HashMap(trackCount); + int bufferSize = -1; + for (int i = 0; i < trackCount; i++) { + MediaFormat format = extractor.getTrackFormat(i); + String mime = format.getString(MediaFormat.KEY_MIME); + + boolean selectCurrentTrack = false; + + if (mime.startsWith("audio/") && useAudio) { + selectCurrentTrack = true; + } else if (mime.startsWith("video/") && useVideo) { + selectCurrentTrack = true; + } + + if (selectCurrentTrack) { + extractor.selectTrack(i); + int dstIndex = muxer.addTrack(format); + indexMap.put(i, dstIndex); + if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) { + int newSize = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE); + bufferSize = newSize > bufferSize ? newSize : bufferSize; + } + } + } + + if (bufferSize < 0) { + bufferSize = DEFAULT_BUFFER_SIZE; + } + + // Set up the orientation and starting time for extractor. + MediaMetadataRetriever retrieverSrc = new MediaMetadataRetriever(); + retrieverSrc.setDataSource(srcPath); + String degreesString = retrieverSrc.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); + if (degreesString != null) { + int degrees = Integer.parseInt(degreesString); + if (degrees >= 0) { + muxer.setOrientationHint(degrees); + } + } + + if (startMs > 0) { + extractor.seekTo(startMs * 1000, MediaExtractor.SEEK_TO_CLOSEST_SYNC); + } + + // Copy the samples from MediaExtractor to MediaMuxer. We will loop + // for copying each sample and stop when we get to the end of the source + // file or exceed the end time of the trimming. + int offset = 0; + int trackIndex = -1; + ByteBuffer dstBuf = ByteBuffer.allocate(bufferSize); + BufferInfo bufferInfo = new BufferInfo(); + + muxer.start(); + while (true) { + bufferInfo.offset = offset; + bufferInfo.size = extractor.readSampleData(dstBuf, offset); + if (bufferInfo.size < 0) { + Log.d(LOGTAG, "Saw input EOS."); + bufferInfo.size = 0; + break; + } else { + bufferInfo.presentationTimeUs = extractor.getSampleTime(); + if (endMs > 0 && bufferInfo.presentationTimeUs > (endMs * 1000)) { + Log.d(LOGTAG, "The current sample is over the trim end time."); + break; + } else { + bufferInfo.flags = extractor.getSampleFlags(); + trackIndex = extractor.getSampleTrackIndex(); + + muxer.writeSampleData(indexMap.get(trackIndex), dstBuf, + bufferInfo); + extractor.advance(); + } + } + } + + muxer.stop(); + muxer.release(); + return; + } + + private static void trimUsingMp4Parser(File src, File dst, int startMs, int endMs) + throws FileNotFoundException, IOException { RandomAccessFile randomAccessFile = new RandomAccessFile(src, "r"); Movie movie = MovieCreator.build(randomAccessFile.getChannel()); @@ -84,22 +231,25 @@ public class VideoUtils { List tracks = movie.getTracks(); movie.setTracks(new LinkedList()); - double startTime = startMs/1000; - double endTime = endMs/1000; + double startTime = startMs / 1000; + double endTime = endMs / 1000; boolean timeCorrected = false; - // Here we try to find a track that has sync samples. Since we can only start decoding - // at such a sample we SHOULD make sure that the start of the new fragment is exactly - // such a frame + // Here we try to find a track that has sync samples. Since we can only + // start decoding at such a sample we SHOULD make sure that the start of + // the new fragment is exactly such a frame. for (Track track : tracks) { if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) { if (timeCorrected) { - // This exception here could be a false positive in case we have multiple tracks - // with sync samples at exactly the same positions. E.g. a single movie containing - // multiple qualities of the same video (Microsoft Smooth Streaming file) - - throw new RuntimeException("The startTime has already been corrected by another track with SyncSample. Not Supported."); + // This exception here could be a false positive in case we + // have multiple tracks with sync samples at exactly the + // same positions. E.g. a single movie containing multiple + // qualities of the same video (Microsoft Smooth Streaming + // file) + throw new RuntimeException( + "The startTime has already been corrected by" + + " another track with SyncSample. Not Supported."); } startTime = correctTimeToSyncSample(track, startTime, false); endTime = correctTimeToSyncSample(track, endTime, true); @@ -116,20 +266,23 @@ public class VideoUtils { for (int i = 0; i < track.getDecodingTimeEntries().size(); i++) { TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i); for (int j = 0; j < entry.getCount(); j++) { - // entry.getDelta() is the amount of time the current sample covers. + // entry.getDelta() is the amount of time the current sample + // covers. if (currentTime <= startTime) { // current sample is still before the new starttime startSample = currentSample; } if (currentTime <= endTime) { - // current sample is after the new start time and still before the new endtime + // current sample is after the new start time and still + // before the new endtime endSample = currentSample; } else { // current sample is after the end of the cropped video break; } - currentTime += (double) entry.getDelta() / (double) track.getTrackMetaData().getTimescale(); + currentTime += (double) entry.getDelta() + / (double) track.getTrackMetaData().getTimescale(); currentSample++; } } @@ -139,15 +292,8 @@ public class VideoUtils { randomAccessFile.close(); } - protected static long getDuration(Track track) { - long duration = 0; - for (TimeToSampleBox.Entry entry : track.getDecodingTimeEntries()) { - duration += entry.getCount() * entry.getDelta(); - } - return duration; - } - - private static double correctTimeToSyncSample(Track track, double cutHere, boolean next) { + private static double correctTimeToSyncSample(Track track, double cutHere, + boolean next) { double[] timeOfSyncSamples = new double[track.getSyncSamples().length]; long currentSample = 0; double currentTime = 0; @@ -155,10 +301,13 @@ public class VideoUtils { TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i); for (int j = 0; j < entry.getCount(); j++) { if (Arrays.binarySearch(track.getSyncSamples(), currentSample + 1) >= 0) { - // samples always start with 1 but we start with zero therefore +1 - timeOfSyncSamples[Arrays.binarySearch(track.getSyncSamples(), currentSample + 1)] = currentTime; + // samples always start with 1 but we start with zero + // therefore +1 + timeOfSyncSamples[Arrays.binarySearch( + track.getSyncSamples(), currentSample + 1)] = currentTime; } - currentTime += (double) entry.getDelta() / (double) track.getTrackMetaData().getTimescale(); + currentTime += (double) entry.getDelta() + / (double) track.getTrackMetaData().getTimescale(); currentSample++; } } @@ -176,5 +325,4 @@ public class VideoUtils { return timeOfSyncSamples[timeOfSyncSamples.length - 1]; } - } -- cgit v1.2.3