Home | History | Annotate | Download | only in app
      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      */
     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