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