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