• 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         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 
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         muxer.release();
222         return;
223     }
224 
trimUsingMp4Parser(File src, File dst, int startMs, int endMs)225     private static void trimUsingMp4Parser(File src, File dst, int startMs, int endMs)
226             throws FileNotFoundException, IOException {
227         RandomAccessFile randomAccessFile = new RandomAccessFile(src, "r");
228         Movie movie = MovieCreator.build(randomAccessFile.getChannel());
229 
230         // remove all tracks we will create new tracks from the old
231         List<Track> tracks = movie.getTracks();
232         movie.setTracks(new LinkedList<Track>());
233 
234         double startTime = startMs / 1000;
235         double endTime = endMs / 1000;
236 
237         boolean timeCorrected = false;
238 
239         // Here we try to find a track that has sync samples. Since we can only
240         // start decoding at such a sample we SHOULD make sure that the start of
241         // the new fragment is exactly such a frame.
242         for (Track track : tracks) {
243             if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) {
244                 if (timeCorrected) {
245                     // This exception here could be a false positive in case we
246                     // have multiple tracks with sync samples at exactly the
247                     // same positions. E.g. a single movie containing multiple
248                     // qualities of the same video (Microsoft Smooth Streaming
249                     // file)
250                     throw new RuntimeException(
251                             "The startTime has already been corrected by" +
252                             " another track with SyncSample. Not Supported.");
253                 }
254                 startTime = correctTimeToSyncSample(track, startTime, false);
255                 endTime = correctTimeToSyncSample(track, endTime, true);
256                 timeCorrected = true;
257             }
258         }
259 
260         for (Track track : tracks) {
261             long currentSample = 0;
262             double currentTime = 0;
263             long startSample = -1;
264             long endSample = -1;
265 
266             for (int i = 0; i < track.getDecodingTimeEntries().size(); i++) {
267                 TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i);
268                 for (int j = 0; j < entry.getCount(); j++) {
269                     // entry.getDelta() is the amount of time the current sample
270                     // covers.
271 
272                     if (currentTime <= startTime) {
273                         // current sample is still before the new starttime
274                         startSample = currentSample;
275                     }
276                     if (currentTime <= endTime) {
277                         // current sample is after the new start time and still
278                         // before the new endtime
279                         endSample = currentSample;
280                     } else {
281                         // current sample is after the end of the cropped video
282                         break;
283                     }
284                     currentTime += (double) entry.getDelta()
285                             / (double) track.getTrackMetaData().getTimescale();
286                     currentSample++;
287                 }
288             }
289             movie.addTrack(new CroppedTrack(track, startSample, endSample));
290         }
291         writeMovieIntoFile(dst, movie);
292         randomAccessFile.close();
293     }
294 
correctTimeToSyncSample(Track track, double cutHere, boolean next)295     private static double correctTimeToSyncSample(Track track, double cutHere,
296             boolean next) {
297         double[] timeOfSyncSamples = new double[track.getSyncSamples().length];
298         long currentSample = 0;
299         double currentTime = 0;
300         for (int i = 0; i < track.getDecodingTimeEntries().size(); i++) {
301             TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i);
302             for (int j = 0; j < entry.getCount(); j++) {
303                 if (Arrays.binarySearch(track.getSyncSamples(), currentSample + 1) >= 0) {
304                     // samples always start with 1 but we start with zero
305                     // therefore +1
306                     timeOfSyncSamples[Arrays.binarySearch(
307                             track.getSyncSamples(), currentSample + 1)] = currentTime;
308                 }
309                 currentTime += (double) entry.getDelta()
310                         / (double) track.getTrackMetaData().getTimescale();
311                 currentSample++;
312             }
313         }
314         double previous = 0;
315         for (double timeOfSyncSample : timeOfSyncSamples) {
316             if (timeOfSyncSample > cutHere) {
317                 if (next) {
318                     return timeOfSyncSample;
319                 } else {
320                     return previous;
321                 }
322             }
323             previous = timeOfSyncSample;
324         }
325         return timeOfSyncSamples[timeOfSyncSamples.length - 1];
326     }
327 
328 }
329