• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 // Modified example based on mp4parser google code open source project.
18 // http://code.google.com/p/mp4parser/source/browse/trunk/examples/src/main/java/com/googlecode/mp4parser/ShortenExample.java
19 
20 package com.android.gallery3d.app;
21 
22 import android.media.MediaCodec.BufferInfo;
23 import android.media.MediaExtractor;
24 import android.media.MediaFormat;
25 import android.media.MediaMetadataRetriever;
26 import android.media.MediaMuxer;
27 import android.util.Log;
28 
29 import com.android.gallery3d.common.ApiHelper;
30 import com.android.gallery3d.util.SaveVideoFileInfo;
31 import com.coremedia.iso.IsoFile;
32 import com.coremedia.iso.boxes.TimeToSampleBox;
33 import com.googlecode.mp4parser.authoring.Movie;
34 import com.googlecode.mp4parser.authoring.Track;
35 import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder;
36 import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator;
37 import com.googlecode.mp4parser.authoring.tracks.CroppedTrack;
38 
39 import java.io.File;
40 import java.io.FileNotFoundException;
41 import java.io.FileOutputStream;
42 import java.io.IOException;
43 import java.io.RandomAccessFile;
44 import java.nio.ByteBuffer;
45 import java.nio.channels.FileChannel;
46 import java.util.Arrays;
47 import java.util.HashMap;
48 import java.util.LinkedList;
49 import java.util.List;
50 
51 public class VideoUtils {
52     private static final String LOGTAG = "VideoUtils";
53     private static final int DEFAULT_BUFFER_SIZE = 1 * 1024 * 1024;
54 
55     /**
56      * Remove the sound track.
57      */
startMute(String filePath, SaveVideoFileInfo dstFileInfo)58     public static void startMute(String filePath, SaveVideoFileInfo dstFileInfo)
59             throws IOException {
60         if (ApiHelper.HAS_MEDIA_MUXER) {
61             genVideoUsingMuxer(filePath, dstFileInfo.mFile.getPath(), -1, -1,
62                     false, true);
63         } else {
64             startMuteUsingMp4Parser(filePath, dstFileInfo);
65         }
66     }
67 
68     /**
69      * Shortens/Crops tracks
70      */
startTrim(File src, File dst, int startMs, int endMs)71     public static void startTrim(File src, File dst, int startMs, int endMs)
72             throws IOException {
73         if (ApiHelper.HAS_MEDIA_MUXER) {
74             genVideoUsingMuxer(src.getPath(), dst.getPath(), startMs, endMs,
75                     true, true);
76         } else {
77             trimUsingMp4Parser(src, dst, startMs, endMs);
78         }
79     }
80 
startMuteUsingMp4Parser(String filePath, SaveVideoFileInfo dstFileInfo)81     private static void startMuteUsingMp4Parser(String filePath,
82             SaveVideoFileInfo dstFileInfo) throws FileNotFoundException, IOException {
83         File dst = dstFileInfo.mFile;
84         File src = new File(filePath);
85         RandomAccessFile randomAccessFile = new RandomAccessFile(src, "r");
86         Movie movie = MovieCreator.build(randomAccessFile.getChannel());
87 
88         // remove all tracks we will create new tracks from the old
89         List<Track> tracks = movie.getTracks();
90         movie.setTracks(new LinkedList<Track>());
91 
92         for (Track track : tracks) {
93             if (track.getHandler().equals("vide")) {
94                 movie.addTrack(track);
95             }
96         }
97         writeMovieIntoFile(dst, movie);
98         randomAccessFile.close();
99     }
100 
writeMovieIntoFile(File dst, Movie movie)101     private static void writeMovieIntoFile(File dst, Movie movie)
102             throws IOException {
103         if (!dst.exists()) {
104             dst.createNewFile();
105         }
106 
107         IsoFile out = new DefaultMp4Builder().build(movie);
108         FileOutputStream fos = new FileOutputStream(dst);
109         FileChannel fc = fos.getChannel();
110         out.getBox(fc); // This one build up the memory.
111 
112         fc.close();
113         fos.close();
114     }
115 
116     /**
117      * @param srcPath the path of source video file.
118      * @param dstPath the path of destination video file.
119      * @param startMs starting time in milliseconds for trimming. Set to
120      *            negative if starting from beginning.
121      * @param endMs end time for trimming in milliseconds. Set to negative if
122      *            no trimming at the end.
123      * @param useAudio true if keep the audio track from the source.
124      * @param useVideo true if keep the video track from the source.
125      * @throws IOException
126      */
genVideoUsingMuxer(String srcPath, String dstPath, int startMs, int endMs, boolean useAudio, boolean useVideo)127     private static void genVideoUsingMuxer(String srcPath, String dstPath,
128             int startMs, int endMs, boolean useAudio, boolean useVideo)
129             throws IOException {
130         // Set up MediaExtractor to read from the source.
131         MediaExtractor extractor = new MediaExtractor();
132         extractor.setDataSource(srcPath);
133 
134         int trackCount = extractor.getTrackCount();
135 
136         // Set up MediaMuxer for the destination.
137         MediaMuxer muxer;
138         muxer = new MediaMuxer(dstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
139 
140         // Set up the tracks and retrieve the max buffer size for selected
141         // tracks.
142         HashMap<Integer, Integer> indexMap = new HashMap<Integer,
143                 Integer>(trackCount);
144         int bufferSize = -1;
145         for (int i = 0; i < trackCount; i++) {
146             MediaFormat format = extractor.getTrackFormat(i);
147             String mime = format.getString(MediaFormat.KEY_MIME);
148 
149             boolean selectCurrentTrack = false;
150 
151             if (mime.startsWith("audio/") && useAudio) {
152                 selectCurrentTrack = true;
153             } else if (mime.startsWith("video/") && useVideo) {
154                 selectCurrentTrack = true;
155             }
156 
157             if (selectCurrentTrack) {
158                 extractor.selectTrack(i);
159                 int dstIndex = muxer.addTrack(format);
160                 indexMap.put(i, dstIndex);
161                 if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) {
162                     int newSize = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);
163                     bufferSize = newSize > bufferSize ? newSize : bufferSize;
164                 }
165             }
166         }
167 
168         if (bufferSize < 0) {
169             bufferSize = DEFAULT_BUFFER_SIZE;
170         }
171 
172         // Set up the orientation and starting time for extractor.
173         MediaMetadataRetriever retrieverSrc = new MediaMetadataRetriever();
174         retrieverSrc.setDataSource(srcPath);
175         String degreesString = retrieverSrc.extractMetadata(
176                 MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
177         try {
178             retrieverSrc.release();
179         } catch (IOException e) {
180             // Ignore errors occurred while releasing the MediaMetadataRetriever.
181         }
182         if (degreesString != null) {
183             int degrees = Integer.parseInt(degreesString);
184             if (degrees >= 0) {
185                 muxer.setOrientationHint(degrees);
186             }
187         }
188 
189         if (startMs > 0) {
190             extractor.seekTo(startMs * 1000, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
191         }
192 
193         // Copy the samples from MediaExtractor to MediaMuxer. We will loop
194         // for copying each sample and stop when we get to the end of the source
195         // file or exceed the end time of the trimming.
196         int offset = 0;
197         int trackIndex = -1;
198         ByteBuffer dstBuf = ByteBuffer.allocate(bufferSize);
199         BufferInfo bufferInfo = new BufferInfo();
200         try {
201             muxer.start();
202             while (true) {
203                 bufferInfo.offset = offset;
204                 bufferInfo.size = extractor.readSampleData(dstBuf, offset);
205                 if (bufferInfo.size < 0) {
206                     Log.d(LOGTAG, "Saw input EOS.");
207                     bufferInfo.size = 0;
208                     break;
209                 } else {
210                     bufferInfo.presentationTimeUs = extractor.getSampleTime();
211                     if (endMs > 0 && bufferInfo.presentationTimeUs > (endMs * 1000)) {
212                         Log.d(LOGTAG, "The current sample is over the trim end time.");
213                         break;
214                     } else {
215                         bufferInfo.flags = extractor.getSampleFlags();
216                         trackIndex = extractor.getSampleTrackIndex();
217 
218                         muxer.writeSampleData(indexMap.get(trackIndex), dstBuf,
219                                 bufferInfo);
220                         extractor.advance();
221                     }
222                 }
223             }
224 
225             muxer.stop();
226         } catch (IllegalStateException e) {
227             // Swallow the exception due to malformed source.
228             Log.w(LOGTAG, "The source video file is malformed");
229         } finally {
230             muxer.release();
231         }
232         return;
233     }
234 
trimUsingMp4Parser(File src, File dst, int startMs, int endMs)235     private static void trimUsingMp4Parser(File src, File dst, int startMs, int endMs)
236             throws FileNotFoundException, IOException {
237         RandomAccessFile randomAccessFile = new RandomAccessFile(src, "r");
238         Movie movie = MovieCreator.build(randomAccessFile.getChannel());
239 
240         // remove all tracks we will create new tracks from the old
241         List<Track> tracks = movie.getTracks();
242         movie.setTracks(new LinkedList<Track>());
243 
244         double startTime = startMs / 1000;
245         double endTime = endMs / 1000;
246 
247         boolean timeCorrected = false;
248 
249         // Here we try to find a track that has sync samples. Since we can only
250         // start decoding at such a sample we SHOULD make sure that the start of
251         // the new fragment is exactly such a frame.
252         for (Track track : tracks) {
253             if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) {
254                 if (timeCorrected) {
255                     // This exception here could be a false positive in case we
256                     // have multiple tracks with sync samples at exactly the
257                     // same positions. E.g. a single movie containing multiple
258                     // qualities of the same video (Microsoft Smooth Streaming
259                     // file)
260                     throw new RuntimeException(
261                             "The startTime has already been corrected by" +
262                             " another track with SyncSample. Not Supported.");
263                 }
264                 startTime = correctTimeToSyncSample(track, startTime, false);
265                 endTime = correctTimeToSyncSample(track, endTime, true);
266                 timeCorrected = true;
267             }
268         }
269 
270         for (Track track : tracks) {
271             long currentSample = 0;
272             double currentTime = 0;
273             long startSample = -1;
274             long endSample = -1;
275 
276             for (int i = 0; i < track.getDecodingTimeEntries().size(); i++) {
277                 TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i);
278                 for (int j = 0; j < entry.getCount(); j++) {
279                     // entry.getDelta() is the amount of time the current sample
280                     // covers.
281 
282                     if (currentTime <= startTime) {
283                         // current sample is still before the new starttime
284                         startSample = currentSample;
285                     }
286                     if (currentTime <= endTime) {
287                         // current sample is after the new start time and still
288                         // before the new endtime
289                         endSample = currentSample;
290                     } else {
291                         // current sample is after the end of the cropped video
292                         break;
293                     }
294                     currentTime += (double) entry.getDelta()
295                             / (double) track.getTrackMetaData().getTimescale();
296                     currentSample++;
297                 }
298             }
299             movie.addTrack(new CroppedTrack(track, startSample, endSample));
300         }
301         writeMovieIntoFile(dst, movie);
302         randomAccessFile.close();
303     }
304 
correctTimeToSyncSample(Track track, double cutHere, boolean next)305     private static double correctTimeToSyncSample(Track track, double cutHere,
306             boolean next) {
307         double[] timeOfSyncSamples = new double[track.getSyncSamples().length];
308         long currentSample = 0;
309         double currentTime = 0;
310         for (int i = 0; i < track.getDecodingTimeEntries().size(); i++) {
311             TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i);
312             for (int j = 0; j < entry.getCount(); j++) {
313                 if (Arrays.binarySearch(track.getSyncSamples(), currentSample + 1) >= 0) {
314                     // samples always start with 1 but we start with zero
315                     // therefore +1
316                     timeOfSyncSamples[Arrays.binarySearch(
317                             track.getSyncSamples(), currentSample + 1)] = currentTime;
318                 }
319                 currentTime += (double) entry.getDelta()
320                         / (double) track.getTrackMetaData().getTimescale();
321                 currentSample++;
322             }
323         }
324         double previous = 0;
325         for (double timeOfSyncSample : timeOfSyncSamples) {
326             if (timeOfSyncSample > cutHere) {
327                 if (next) {
328                     return timeOfSyncSample;
329                 } else {
330                     return previous;
331                 }
332             }
333             previous = timeOfSyncSample;
334         }
335         return timeOfSyncSamples[timeOfSyncSamples.length - 1];
336     }
337 
338 }
339