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         if (degreesString != null) {
178             int degrees = Integer.parseInt(degreesString);
179             if (degrees >= 0) {
180                 muxer.setOrientationHint(degrees);
181             }
182         }
183 
184         if (startMs > 0) {
185             extractor.seekTo(startMs * 1000, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
186         }
187 
188         // Copy the samples from MediaExtractor to MediaMuxer. We will loop
189         // for copying each sample and stop when we get to the end of the source
190         // file or exceed the end time of the trimming.
191         int offset = 0;
192         int trackIndex = -1;
193         ByteBuffer dstBuf = ByteBuffer.allocate(bufferSize);
194         BufferInfo bufferInfo = new BufferInfo();
195         try {
196             muxer.start();
197             while (true) {
198                 bufferInfo.offset = offset;
199                 bufferInfo.size = extractor.readSampleData(dstBuf, offset);
200                 if (bufferInfo.size < 0) {
201                     Log.d(LOGTAG, "Saw input EOS.");
202                     bufferInfo.size = 0;
203                     break;
204                 } else {
205                     bufferInfo.presentationTimeUs = extractor.getSampleTime();
206                     if (endMs > 0 && bufferInfo.presentationTimeUs > (endMs * 1000)) {
207                         Log.d(LOGTAG, "The current sample is over the trim end time.");
208                         break;
209                     } else {
210                         bufferInfo.flags = extractor.getSampleFlags();
211                         trackIndex = extractor.getSampleTrackIndex();
212 
213                         muxer.writeSampleData(indexMap.get(trackIndex), dstBuf,
214                                 bufferInfo);
215                         extractor.advance();
216                     }
217                 }
218             }
219 
220             muxer.stop();
221         } catch (IllegalStateException e) {
222             // Swallow the exception due to malformed source.
223             Log.w(LOGTAG, "The source video file is malformed");
224         } finally {
225             muxer.release();
226         }
227         return;
228     }
229 
trimUsingMp4Parser(File src, File dst, int startMs, int endMs)230     private static void trimUsingMp4Parser(File src, File dst, int startMs, int endMs)
231             throws FileNotFoundException, IOException {
232         RandomAccessFile randomAccessFile = new RandomAccessFile(src, "r");
233         Movie movie = MovieCreator.build(randomAccessFile.getChannel());
234 
235         // remove all tracks we will create new tracks from the old
236         List<Track> tracks = movie.getTracks();
237         movie.setTracks(new LinkedList<Track>());
238 
239         double startTime = startMs / 1000;
240         double endTime = endMs / 1000;
241 
242         boolean timeCorrected = false;
243 
244         // Here we try to find a track that has sync samples. Since we can only
245         // start decoding at such a sample we SHOULD make sure that the start of
246         // the new fragment is exactly such a frame.
247         for (Track track : tracks) {
248             if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) {
249                 if (timeCorrected) {
250                     // This exception here could be a false positive in case we
251                     // have multiple tracks with sync samples at exactly the
252                     // same positions. E.g. a single movie containing multiple
253                     // qualities of the same video (Microsoft Smooth Streaming
254                     // file)
255                     throw new RuntimeException(
256                             "The startTime has already been corrected by" +
257                             " another track with SyncSample. Not Supported.");
258                 }
259                 startTime = correctTimeToSyncSample(track, startTime, false);
260                 endTime = correctTimeToSyncSample(track, endTime, true);
261                 timeCorrected = true;
262             }
263         }
264 
265         for (Track track : tracks) {
266             long currentSample = 0;
267             double currentTime = 0;
268             long startSample = -1;
269             long endSample = -1;
270 
271             for (int i = 0; i < track.getDecodingTimeEntries().size(); i++) {
272                 TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i);
273                 for (int j = 0; j < entry.getCount(); j++) {
274                     // entry.getDelta() is the amount of time the current sample
275                     // covers.
276 
277                     if (currentTime <= startTime) {
278                         // current sample is still before the new starttime
279                         startSample = currentSample;
280                     }
281                     if (currentTime <= endTime) {
282                         // current sample is after the new start time and still
283                         // before the new endtime
284                         endSample = currentSample;
285                     } else {
286                         // current sample is after the end of the cropped video
287                         break;
288                     }
289                     currentTime += (double) entry.getDelta()
290                             / (double) track.getTrackMetaData().getTimescale();
291                     currentSample++;
292                 }
293             }
294             movie.addTrack(new CroppedTrack(track, startSample, endSample));
295         }
296         writeMovieIntoFile(dst, movie);
297         randomAccessFile.close();
298     }
299 
correctTimeToSyncSample(Track track, double cutHere, boolean next)300     private static double correctTimeToSyncSample(Track track, double cutHere,
301             boolean next) {
302         double[] timeOfSyncSamples = new double[track.getSyncSamples().length];
303         long currentSample = 0;
304         double currentTime = 0;
305         for (int i = 0; i < track.getDecodingTimeEntries().size(); i++) {
306             TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i);
307             for (int j = 0; j < entry.getCount(); j++) {
308                 if (Arrays.binarySearch(track.getSyncSamples(), currentSample + 1) >= 0) {
309                     // samples always start with 1 but we start with zero
310                     // therefore +1
311                     timeOfSyncSamples[Arrays.binarySearch(
312                             track.getSyncSamples(), currentSample + 1)] = currentTime;
313                 }
314                 currentTime += (double) entry.getDelta()
315                         / (double) track.getTrackMetaData().getTimescale();
316                 currentSample++;
317             }
318         }
319         double previous = 0;
320         for (double timeOfSyncSample : timeOfSyncSamples) {
321             if (timeOfSyncSample > cutHere) {
322                 if (next) {
323                     return timeOfSyncSample;
324                 } else {
325                     return previous;
326                 }
327             }
328             previous = timeOfSyncSample;
329         }
330         return timeOfSyncSamples[timeOfSyncSamples.length - 1];
331     }
332 
333 }
334