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