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