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.media.MediaCodec.BufferInfo; 20 import dalvik.system.CloseGuard; 21 22 import java.io.FileDescriptor; 23 import java.io.IOException; 24 import java.io.RandomAccessFile; 25 import java.nio.ByteBuffer; 26 import 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 64 final 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 */ OutputFormat()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. nativeSetup(FileDescriptor fd, int format)84 private static native long nativeSetup(FileDescriptor fd, int format); nativeRelease(long nativeObject)85 private static native void nativeRelease(long nativeObject); nativeStart(long nativeObject)86 private static native void nativeStart(long nativeObject); nativeStop(long nativeObject)87 private static native void nativeStop(long nativeObject); nativeAddTrack(long nativeObject, String[] keys, Object[] values)88 private static native int nativeAddTrack(long nativeObject, String[] keys, 89 Object[] values); nativeSetOrientationHint(long nativeObject, int degrees)90 private static native void nativeSetOrientationHint(long nativeObject, 91 int degrees); nativeSetLocation(long nativeObject, int latitude, int longitude)92 private static native void nativeSetLocation(long nativeObject, int latitude, int longitude); nativeWriteSampleData(long nativeObject, int trackIndex, ByteBuffer byteBuf, int offset, int size, long presentationTimeUs, int flags)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 */ MediaMuxer(String path, int format)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 */ setOrientationHint(int degrees)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 */ setLocation(float latitude, float longitude)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 */ start()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 */ stop()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 finalize()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 */ addTrack(MediaFormat format)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 */ writeSampleData(int trackIndex, ByteBuffer byteBuf, BufferInfo bufferInfo)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 */ release()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