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 try { 178 retrieverSrc.release(); 179 } catch (IOException e) { 180 // Ignore errors occurred while releasing the MediaMetadataRetriever. 181 } 182 if (degreesString != null) { 183 int degrees = Integer.parseInt(degreesString); 184 if (degrees >= 0) { 185 muxer.setOrientationHint(degrees); 186 } 187 } 188 189 if (startMs > 0) { 190 extractor.seekTo(startMs * 1000, MediaExtractor.SEEK_TO_CLOSEST_SYNC); 191 } 192 193 // Copy the samples from MediaExtractor to MediaMuxer. We will loop 194 // for copying each sample and stop when we get to the end of the source 195 // file or exceed the end time of the trimming. 196 int offset = 0; 197 int trackIndex = -1; 198 ByteBuffer dstBuf = ByteBuffer.allocate(bufferSize); 199 BufferInfo bufferInfo = new BufferInfo(); 200 try { 201 muxer.start(); 202 while (true) { 203 bufferInfo.offset = offset; 204 bufferInfo.size = extractor.readSampleData(dstBuf, offset); 205 if (bufferInfo.size < 0) { 206 Log.d(LOGTAG, "Saw input EOS."); 207 bufferInfo.size = 0; 208 break; 209 } else { 210 bufferInfo.presentationTimeUs = extractor.getSampleTime(); 211 if (endMs > 0 && bufferInfo.presentationTimeUs > (endMs * 1000)) { 212 Log.d(LOGTAG, "The current sample is over the trim end time."); 213 break; 214 } else { 215 bufferInfo.flags = extractor.getSampleFlags(); 216 trackIndex = extractor.getSampleTrackIndex(); 217 218 muxer.writeSampleData(indexMap.get(trackIndex), dstBuf, 219 bufferInfo); 220 extractor.advance(); 221 } 222 } 223 } 224 225 muxer.stop(); 226 } catch (IllegalStateException e) { 227 // Swallow the exception due to malformed source. 228 Log.w(LOGTAG, "The source video file is malformed"); 229 } finally { 230 muxer.release(); 231 } 232 return; 233 } 234 trimUsingMp4Parser(File src, File dst, int startMs, int endMs)235 private static void trimUsingMp4Parser(File src, File dst, int startMs, int endMs) 236 throws FileNotFoundException, IOException { 237 RandomAccessFile randomAccessFile = new RandomAccessFile(src, "r"); 238 Movie movie = MovieCreator.build(randomAccessFile.getChannel()); 239 240 // remove all tracks we will create new tracks from the old 241 List<Track> tracks = movie.getTracks(); 242 movie.setTracks(new LinkedList<Track>()); 243 244 double startTime = startMs / 1000; 245 double endTime = endMs / 1000; 246 247 boolean timeCorrected = false; 248 249 // Here we try to find a track that has sync samples. Since we can only 250 // start decoding at such a sample we SHOULD make sure that the start of 251 // the new fragment is exactly such a frame. 252 for (Track track : tracks) { 253 if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) { 254 if (timeCorrected) { 255 // This exception here could be a false positive in case we 256 // have multiple tracks with sync samples at exactly the 257 // same positions. E.g. a single movie containing multiple 258 // qualities of the same video (Microsoft Smooth Streaming 259 // file) 260 throw new RuntimeException( 261 "The startTime has already been corrected by" + 262 " another track with SyncSample. Not Supported."); 263 } 264 startTime = correctTimeToSyncSample(track, startTime, false); 265 endTime = correctTimeToSyncSample(track, endTime, true); 266 timeCorrected = true; 267 } 268 } 269 270 for (Track track : tracks) { 271 long currentSample = 0; 272 double currentTime = 0; 273 long startSample = -1; 274 long endSample = -1; 275 276 for (int i = 0; i < track.getDecodingTimeEntries().size(); i++) { 277 TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i); 278 for (int j = 0; j < entry.getCount(); j++) { 279 // entry.getDelta() is the amount of time the current sample 280 // covers. 281 282 if (currentTime <= startTime) { 283 // current sample is still before the new starttime 284 startSample = currentSample; 285 } 286 if (currentTime <= endTime) { 287 // current sample is after the new start time and still 288 // before the new endtime 289 endSample = currentSample; 290 } else { 291 // current sample is after the end of the cropped video 292 break; 293 } 294 currentTime += (double) entry.getDelta() 295 / (double) track.getTrackMetaData().getTimescale(); 296 currentSample++; 297 } 298 } 299 movie.addTrack(new CroppedTrack(track, startSample, endSample)); 300 } 301 writeMovieIntoFile(dst, movie); 302 randomAccessFile.close(); 303 } 304 correctTimeToSyncSample(Track track, double cutHere, boolean next)305 private static double correctTimeToSyncSample(Track track, double cutHere, 306 boolean next) { 307 double[] timeOfSyncSamples = new double[track.getSyncSamples().length]; 308 long currentSample = 0; 309 double currentTime = 0; 310 for (int i = 0; i < track.getDecodingTimeEntries().size(); i++) { 311 TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i); 312 for (int j = 0; j < entry.getCount(); j++) { 313 if (Arrays.binarySearch(track.getSyncSamples(), currentSample + 1) >= 0) { 314 // samples always start with 1 but we start with zero 315 // therefore +1 316 timeOfSyncSamples[Arrays.binarySearch( 317 track.getSyncSamples(), currentSample + 1)] = currentTime; 318 } 319 currentTime += (double) entry.getDelta() 320 / (double) track.getTrackMetaData().getTimescale(); 321 currentSample++; 322 } 323 } 324 double previous = 0; 325 for (double timeOfSyncSample : timeOfSyncSamples) { 326 if (timeOfSyncSample > cutHere) { 327 if (next) { 328 return timeOfSyncSample; 329 } else { 330 return previous; 331 } 332 } 333 previous = timeOfSyncSample; 334 } 335 return timeOfSyncSamples[timeOfSyncSamples.length - 1]; 336 } 337 338 } 339