1 /* 2 * Copyright (C) 2013 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 package android.media; 18 19 import android.annotation.IntDef; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.media.MediaCodec; 23 import android.media.MediaCodec.BufferInfo; 24 import dalvik.system.CloseGuard; 25 26 import java.io.FileDescriptor; 27 import java.io.IOException; 28 import java.io.RandomAccessFile; 29 import java.lang.annotation.Retention; 30 import java.lang.annotation.RetentionPolicy; 31 import java.nio.ByteBuffer; 32 import java.util.Map; 33 34 /** 35 * MediaMuxer facilitates muxing elementary streams. Currently only supports an 36 * mp4 file as the output and at most one audio and/or one video elementary 37 * stream. 38 * <p> 39 * It is generally used like this: 40 * 41 * <pre> 42 * MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4); 43 * // More often, the MediaFormat will be retrieved from MediaCodec.getOutputFormat() 44 * // or MediaExtractor.getTrackFormat(). 45 * MediaFormat audioFormat = new MediaFormat(...); 46 * MediaFormat videoFormat = new MediaFormat(...); 47 * int audioTrackIndex = muxer.addTrack(audioFormat); 48 * int videoTrackIndex = muxer.addTrack(videoFormat); 49 * ByteBuffer inputBuffer = ByteBuffer.allocate(bufferSize); 50 * boolean finished = false; 51 * BufferInfo bufferInfo = new BufferInfo(); 52 * 53 * muxer.start(); 54 * while(!finished) { 55 * // getInputBuffer() will fill the inputBuffer with one frame of encoded 56 * // sample from either MediaCodec or MediaExtractor, set isAudioSample to 57 * // true when the sample is audio data, set up all the fields of bufferInfo, 58 * // and return true if there are no more samples. 59 * finished = getInputBuffer(inputBuffer, isAudioSample, bufferInfo); 60 * if (!finished) { 61 * int currentTrackIndex = isAudioSample ? audioTrackIndex : videoTrackIndex; 62 * muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo); 63 * } 64 * }; 65 * muxer.stop(); 66 * muxer.release(); 67 * </pre> 68 */ 69 70 final public class MediaMuxer { 71 72 static { 73 System.loadLibrary("media_jni"); 74 } 75 76 /** 77 * Defines the output format. These constants are used with constructor. 78 */ 79 public static final class OutputFormat { 80 /* Do not change these values without updating their counterparts 81 * in include/media/stagefright/MediaMuxer.h! 82 */ 83 private OutputFormat() {} 84 /** MPEG4 media file format*/ 85 public static final int MUXER_OUTPUT_MPEG_4 = 0; 86 public static final int MUXER_OUTPUT_WEBM = 1; 87 }; 88 89 /** @hide */ 90 @IntDef({ 91 OutputFormat.MUXER_OUTPUT_MPEG_4, 92 OutputFormat.MUXER_OUTPUT_WEBM, 93 }) 94 @Retention(RetentionPolicy.SOURCE) 95 public @interface Format {} 96 97 // All the native functions are listed here. 98 private static native long nativeSetup(@NonNull FileDescriptor fd, int format); 99 private static native void nativeRelease(long nativeObject); 100 private static native void nativeStart(long nativeObject); 101 private static native void nativeStop(long nativeObject); 102 private static native int nativeAddTrack( 103 long nativeObject, @NonNull String[] keys, @NonNull Object[] values); 104 private static native void nativeSetOrientationHint( 105 long nativeObject, int degrees); 106 private static native void nativeSetLocation(long nativeObject, int latitude, int longitude); 107 private static native void nativeWriteSampleData( 108 long nativeObject, int trackIndex, @NonNull ByteBuffer byteBuf, 109 int offset, int size, long presentationTimeUs, @MediaCodec.BufferFlag int flags); 110 111 // Muxer internal states. 112 private static final int MUXER_STATE_UNINITIALIZED = -1; 113 private static final int MUXER_STATE_INITIALIZED = 0; 114 private static final int MUXER_STATE_STARTED = 1; 115 private static final int MUXER_STATE_STOPPED = 2; 116 117 private int mState = MUXER_STATE_UNINITIALIZED; 118 119 private final CloseGuard mCloseGuard = CloseGuard.get(); 120 private int mLastTrackIndex = -1; 121 122 private long mNativeObject; 123 124 /** 125 * Constructor. 126 * Creates a media muxer that writes to the specified path. 127 * @param path The path of the output media file. 128 * @param format The format of the output media file. 129 * @see android.media.MediaMuxer.OutputFormat 130 * @throws IOException if failed to open the file for write 131 */ 132 public MediaMuxer(@NonNull String path, @Format int format) throws IOException { 133 if (path == null) { 134 throw new IllegalArgumentException("path must not be null"); 135 } 136 if (format != OutputFormat.MUXER_OUTPUT_MPEG_4 && 137 format != OutputFormat.MUXER_OUTPUT_WEBM) { 138 throw new IllegalArgumentException("format is invalid"); 139 } 140 // Use RandomAccessFile so we can open the file with RW access; 141 // RW access allows the native writer to memory map the output file. 142 RandomAccessFile file = null; 143 try { 144 file = new RandomAccessFile(path, "rws"); 145 FileDescriptor fd = file.getFD(); 146 mNativeObject = nativeSetup(fd, format); 147 mState = MUXER_STATE_INITIALIZED; 148 mCloseGuard.open("release"); 149 } finally { 150 if (file != null) { 151 file.close(); 152 } 153 } 154 } 155 156 /** 157 * Sets the orientation hint for output video playback. 158 * <p>This method should be called before {@link #start}. Calling this 159 * method will not rotate the video frame when muxer is generating the file, 160 * but add a composition matrix containing the rotation angle in the output 161 * video if the output format is 162 * {@link OutputFormat#MUXER_OUTPUT_MPEG_4} so that a video player can 163 * choose the proper orientation for playback. Note that some video players 164 * may choose to ignore the composition matrix in a video during playback. 165 * By default, the rotation degree is 0.</p> 166 * @param degrees the angle to be rotated clockwise in degrees. 167 * The supported angles are 0, 90, 180, and 270 degrees. 168 */ 169 public void setOrientationHint(int degrees) { 170 if (degrees != 0 && degrees != 90 && degrees != 180 && degrees != 270) { 171 throw new IllegalArgumentException("Unsupported angle: " + degrees); 172 } 173 if (mState == MUXER_STATE_INITIALIZED) { 174 nativeSetOrientationHint(mNativeObject, degrees); 175 } else { 176 throw new IllegalStateException("Can't set rotation degrees due" + 177 " to wrong state."); 178 } 179 } 180 181 /** 182 * Set and store the geodata (latitude and longitude) in the output file. 183 * This method should be called before {@link #start}. The geodata is stored 184 * in udta box if the output format is 185 * {@link OutputFormat#MUXER_OUTPUT_MPEG_4}, and is ignored for other output 186 * formats. The geodata is stored according to ISO-6709 standard. 187 * 188 * @param latitude Latitude in degrees. Its value must be in the range [-90, 189 * 90]. 190 * @param longitude Longitude in degrees. Its value must be in the range 191 * [-180, 180]. 192 * @throws IllegalArgumentException If the given latitude or longitude is out 193 * of range. 194 * @throws IllegalStateException If this method is called after {@link #start}. 195 */ 196 public void setLocation(float latitude, float longitude) { 197 int latitudex10000 = (int) (latitude * 10000 + 0.5); 198 int longitudex10000 = (int) (longitude * 10000 + 0.5); 199 200 if (latitudex10000 > 900000 || latitudex10000 < -900000) { 201 String msg = "Latitude: " + latitude + " out of range."; 202 throw new IllegalArgumentException(msg); 203 } 204 if (longitudex10000 > 1800000 || longitudex10000 < -1800000) { 205 String msg = "Longitude: " + longitude + " out of range"; 206 throw new IllegalArgumentException(msg); 207 } 208 209 if (mState == MUXER_STATE_INITIALIZED && mNativeObject != 0) { 210 nativeSetLocation(mNativeObject, latitudex10000, longitudex10000); 211 } else { 212 throw new IllegalStateException("Can't set location due to wrong state."); 213 } 214 } 215 216 /** 217 * Starts the muxer. 218 * <p>Make sure this is called after {@link #addTrack} and before 219 * {@link #writeSampleData}.</p> 220 */ 221 public void start() { 222 if (mNativeObject == 0) { 223 throw new IllegalStateException("Muxer has been released!"); 224 } 225 if (mState == MUXER_STATE_INITIALIZED) { 226 nativeStart(mNativeObject); 227 mState = MUXER_STATE_STARTED; 228 } else { 229 throw new IllegalStateException("Can't start due to wrong state."); 230 } 231 } 232 233 /** 234 * Stops the muxer. 235 * <p>Once the muxer stops, it can not be restarted.</p> 236 */ 237 public void stop() { 238 if (mState == MUXER_STATE_STARTED) { 239 nativeStop(mNativeObject); 240 mState = MUXER_STATE_STOPPED; 241 } else { 242 throw new IllegalStateException("Can't stop due to wrong state."); 243 } 244 } 245 246 @Override 247 protected void finalize() throws Throwable { 248 try { 249 if (mCloseGuard != null) { 250 mCloseGuard.warnIfOpen(); 251 } 252 if (mNativeObject != 0) { 253 nativeRelease(mNativeObject); 254 mNativeObject = 0; 255 } 256 } finally { 257 super.finalize(); 258 } 259 } 260 261 /** 262 * Adds a track with the specified format. 263 * @param format The media format for the track. This must not be an empty 264 * MediaFormat. 265 * @return The track index for this newly added track, and it should be used 266 * in the {@link #writeSampleData}. 267 */ 268 public int addTrack(@NonNull MediaFormat format) { 269 if (format == null) { 270 throw new IllegalArgumentException("format must not be null."); 271 } 272 if (mState != MUXER_STATE_INITIALIZED) { 273 throw new IllegalStateException("Muxer is not initialized."); 274 } 275 if (mNativeObject == 0) { 276 throw new IllegalStateException("Muxer has been released!"); 277 } 278 int trackIndex = -1; 279 // Convert the MediaFormat into key-value pairs and send to the native. 280 Map<String, Object> formatMap = format.getMap(); 281 282 String[] keys = null; 283 Object[] values = null; 284 int mapSize = formatMap.size(); 285 if (mapSize > 0) { 286 keys = new String[mapSize]; 287 values = new Object[mapSize]; 288 int i = 0; 289 for (Map.Entry<String, Object> entry : formatMap.entrySet()) { 290 keys[i] = entry.getKey(); 291 values[i] = entry.getValue(); 292 ++i; 293 } 294 trackIndex = nativeAddTrack(mNativeObject, keys, values); 295 } else { 296 throw new IllegalArgumentException("format must not be empty."); 297 } 298 299 // Track index number is expected to incremented as addTrack succeed. 300 // However, if format is invalid, it will get a negative trackIndex. 301 if (mLastTrackIndex >= trackIndex) { 302 throw new IllegalArgumentException("Invalid format."); 303 } 304 mLastTrackIndex = trackIndex; 305 return trackIndex; 306 } 307 308 /** 309 * Writes an encoded sample into the muxer. 310 * <p>The application needs to make sure that the samples are written into 311 * the right tracks. Also, it needs to make sure the samples for each track 312 * are written in chronological order (e.g. in the order they are provided 313 * by the encoder.)</p> 314 * @param byteBuf The encoded sample. 315 * @param trackIndex The track index for this sample. 316 * @param bufferInfo The buffer information related to this sample. 317 * MediaMuxer uses the flags provided in {@link MediaCodec.BufferInfo}, 318 * to signal sync frames. 319 */ 320 public void writeSampleData(int trackIndex, @NonNull ByteBuffer byteBuf, 321 @NonNull BufferInfo bufferInfo) { 322 if (trackIndex < 0 || trackIndex > mLastTrackIndex) { 323 throw new IllegalArgumentException("trackIndex is invalid"); 324 } 325 326 if (byteBuf == null) { 327 throw new IllegalArgumentException("byteBuffer must not be null"); 328 } 329 330 if (bufferInfo == null) { 331 throw new IllegalArgumentException("bufferInfo must not be null"); 332 } 333 if (bufferInfo.size < 0 || bufferInfo.offset < 0 334 || (bufferInfo.offset + bufferInfo.size) > byteBuf.capacity() 335 || bufferInfo.presentationTimeUs < 0) { 336 throw new IllegalArgumentException("bufferInfo must specify a" + 337 " valid buffer offset, size and presentation time"); 338 } 339 340 if (mNativeObject == 0) { 341 throw new IllegalStateException("Muxer has been released!"); 342 } 343 344 if (mState != MUXER_STATE_STARTED) { 345 throw new IllegalStateException("Can't write, muxer is not started"); 346 } 347 348 nativeWriteSampleData(mNativeObject, trackIndex, byteBuf, 349 bufferInfo.offset, bufferInfo.size, 350 bufferInfo.presentationTimeUs, bufferInfo.flags); 351 } 352 353 /** 354 * Make sure you call this when you're done to free up any resources 355 * instead of relying on the garbage collector to do this for you at 356 * some point in the future. 357 */ 358 public void release() { 359 if (mState == MUXER_STATE_STARTED) { 360 stop(); 361 } 362 if (mNativeObject != 0) { 363 nativeRelease(mNativeObject); 364 mNativeObject = 0; 365 mCloseGuard.close(); 366 } 367 mState = MUXER_STATE_UNINITIALIZED; 368 } 369 } 370