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 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 muxer.release(); 222 return; 223 } 224 trimUsingMp4Parser(File src, File dst, int startMs, int endMs)225 private static void trimUsingMp4Parser(File src, File dst, int startMs, int endMs) 226 throws FileNotFoundException, IOException { 227 RandomAccessFile randomAccessFile = new RandomAccessFile(src, "r"); 228 Movie movie = MovieCreator.build(randomAccessFile.getChannel()); 229 230 // remove all tracks we will create new tracks from the old 231 List<Track> tracks = movie.getTracks(); 232 movie.setTracks(new LinkedList<Track>()); 233 234 double startTime = startMs / 1000; 235 double endTime = endMs / 1000; 236 237 boolean timeCorrected = false; 238 239 // Here we try to find a track that has sync samples. Since we can only 240 // start decoding at such a sample we SHOULD make sure that the start of 241 // the new fragment is exactly such a frame. 242 for (Track track : tracks) { 243 if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) { 244 if (timeCorrected) { 245 // This exception here could be a false positive in case we 246 // have multiple tracks with sync samples at exactly the 247 // same positions. E.g. a single movie containing multiple 248 // qualities of the same video (Microsoft Smooth Streaming 249 // file) 250 throw new RuntimeException( 251 "The startTime has already been corrected by" + 252 " another track with SyncSample. Not Supported."); 253 } 254 startTime = correctTimeToSyncSample(track, startTime, false); 255 endTime = correctTimeToSyncSample(track, endTime, true); 256 timeCorrected = true; 257 } 258 } 259 260 for (Track track : tracks) { 261 long currentSample = 0; 262 double currentTime = 0; 263 long startSample = -1; 264 long endSample = -1; 265 266 for (int i = 0; i < track.getDecodingTimeEntries().size(); i++) { 267 TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i); 268 for (int j = 0; j < entry.getCount(); j++) { 269 // entry.getDelta() is the amount of time the current sample 270 // covers. 271 272 if (currentTime <= startTime) { 273 // current sample is still before the new starttime 274 startSample = currentSample; 275 } 276 if (currentTime <= endTime) { 277 // current sample is after the new start time and still 278 // before the new endtime 279 endSample = currentSample; 280 } else { 281 // current sample is after the end of the cropped video 282 break; 283 } 284 currentTime += (double) entry.getDelta() 285 / (double) track.getTrackMetaData().getTimescale(); 286 currentSample++; 287 } 288 } 289 movie.addTrack(new CroppedTrack(track, startSample, endSample)); 290 } 291 writeMovieIntoFile(dst, movie); 292 randomAccessFile.close(); 293 } 294 correctTimeToSyncSample(Track track, double cutHere, boolean next)295 private static double correctTimeToSyncSample(Track track, double cutHere, 296 boolean next) { 297 double[] timeOfSyncSamples = new double[track.getSyncSamples().length]; 298 long currentSample = 0; 299 double currentTime = 0; 300 for (int i = 0; i < track.getDecodingTimeEntries().size(); i++) { 301 TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i); 302 for (int j = 0; j < entry.getCount(); j++) { 303 if (Arrays.binarySearch(track.getSyncSamples(), currentSample + 1) >= 0) { 304 // samples always start with 1 but we start with zero 305 // therefore +1 306 timeOfSyncSamples[Arrays.binarySearch( 307 track.getSyncSamples(), currentSample + 1)] = currentTime; 308 } 309 currentTime += (double) entry.getDelta() 310 / (double) track.getTrackMetaData().getTimescale(); 311 currentSample++; 312 } 313 } 314 double previous = 0; 315 for (double timeOfSyncSample : timeOfSyncSamples) { 316 if (timeOfSyncSample > cutHere) { 317 if (next) { 318 return timeOfSyncSample; 319 } else { 320 return previous; 321 } 322 } 323 previous = timeOfSyncSample; 324 } 325 return timeOfSyncSamples[timeOfSyncSamples.length - 1]; 326 } 327 328 } 329