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
20package com.android.gallery3d.app;
21
22import android.media.MediaCodec.BufferInfo;
23import android.media.MediaExtractor;
24import android.media.MediaFormat;
25import android.media.MediaMetadataRetriever;
26import android.media.MediaMuxer;
27import android.util.Log;
28
29import com.android.gallery3d.common.ApiHelper;
30import com.android.gallery3d.util.SaveVideoFileInfo;
31import com.coremedia.iso.IsoFile;
32import com.coremedia.iso.boxes.TimeToSampleBox;
33import com.googlecode.mp4parser.authoring.Movie;
34import com.googlecode.mp4parser.authoring.Track;
35import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder;
36import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator;
37import com.googlecode.mp4parser.authoring.tracks.CroppedTrack;
38
39import java.io.File;
40import java.io.FileNotFoundException;
41import java.io.FileOutputStream;
42import java.io.IOException;
43import java.io.RandomAccessFile;
44import java.nio.ByteBuffer;
45import java.nio.channels.FileChannel;
46import java.util.Arrays;
47import java.util.HashMap;
48import java.util.LinkedList;
49import java.util.List;
50
51public 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     */
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     */
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
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
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     */
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
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
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