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