MediaMuxer.java revision ecca7f60a69d99a9569c06bdf9c122f853e67d47
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(...);
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(...);
44 * muxer.start();
45 * while(inputBuffer has new data) {
46 *   if (new data is audio sample) {
47 *     muxer.writeSampleData(audioTrackIndex, inputBuffer, ...);
48 *   } else if (new data is video sample) {
49 *     muxer.writeSampleData(videoTrackIndex, inputBuffer, ...);
50 *   }
51 * }
52 * muxer.stop();
53 * muxer.release();
54 * </pre>
55 */
56
57final public class MediaMuxer {
58
59    private int mNativeContext;
60
61    static {
62        System.loadLibrary("media_jni");
63    }
64
65    /**
66     * Defines the output format. These constants are used with constructor.
67     */
68    public static final class OutputFormat {
69        /* Do not change these values without updating their counterparts
70         * in include/media/stagefright/MediaMuxer.h!
71         */
72        private OutputFormat() {}
73        /** MPEG4 media file format*/
74        public static final int MUXER_OUTPUT_MPEG_4 = 0;
75    };
76
77    // All the native functions are listed here.
78    private static native int nativeSetup(FileDescriptor fd, int format);
79    private static native void nativeRelease(int nativeObject);
80    private static native void nativeStart(int nativeObject);
81    private static native void nativeStop(int nativeObject);
82    private static native int nativeAddTrack(int nativeObject, String[] keys,
83            Object[] values);
84    private static native void nativeSetOrientationHint(int nativeObject,
85            int degrees);
86    private static native void nativeWriteSampleData(int nativeObject,
87            int trackIndex, ByteBuffer byteBuf,
88            int offset, int size, long presentationTimeUs, int flags);
89
90    // Muxer internal states.
91    private static final int MUXER_STATE_UNINITIALIZED  = -1;
92    private static final int MUXER_STATE_INITIALIZED    = 0;
93    private static final int MUXER_STATE_STARTED        = 1;
94    private static final int MUXER_STATE_STOPPED        = 2;
95
96    private int mState = MUXER_STATE_UNINITIALIZED;
97
98    private final CloseGuard mCloseGuard = CloseGuard.get();
99    private int mLastTrackIndex = -1;
100
101    private int mNativeObject;
102
103    /**
104     * Constructor.
105     * Creates a media muxer that writes to the specified path.
106     * @param path The path of the output media file.
107     * @param format The format of the output media file.
108     * @see android.media.MediaMuxer.OutputFormat
109     * @throws IOException if failed to open the file for write
110     */
111    public MediaMuxer(String path, int format) throws IOException {
112        if (path == null) {
113            throw new IllegalArgumentException("path must not be null");
114        }
115        if (format != OutputFormat.MUXER_OUTPUT_MPEG_4) {
116            throw new IllegalArgumentException("format is invalid");
117        }
118        FileOutputStream fos = null;
119        try {
120            File file = new File(path);
121            fos = new FileOutputStream(file);
122            FileDescriptor fd = fos.getFD();
123            mNativeObject = nativeSetup(fd, format);
124            mState = MUXER_STATE_INITIALIZED;
125            mCloseGuard.open("release");
126        } finally {
127            if (fos != null) {
128                fos.close();
129            }
130        }
131    }
132
133    /**
134     * Sets the orientation hint for output video playback.
135     * <p>This method should be called before {@link #start}. Calling this
136     * method will not rotate the video frame when muxer is generating the file,
137     * but add a composition matrix containing the rotation angle in the output
138     * video if the output format is
139     * {@link OutputFormat#MUXER_OUTPUT_MPEG_4} so that a video player can
140     * choose the proper orientation for playback. Note that some video players
141     * may choose to ignore the composition matrix in a video during playback.
142     * By default, the rotation degree is 0.</p>
143     * @param degrees the angle to be rotated clockwise in degrees.
144     * The supported angles are 0, 90, 180, and 270 degrees.
145     */
146    public void setOrientationHint(int degrees) {
147        if (degrees != 0 && degrees != 90  && degrees != 180 && degrees != 270) {
148            throw new IllegalArgumentException("Unsupported angle: " + degrees);
149        }
150        if (mState == MUXER_STATE_INITIALIZED) {
151            nativeSetOrientationHint(mNativeObject, degrees);
152        } else {
153            throw new IllegalStateException("Can't set rotation degrees due" +
154                    " to wrong state.");
155        }
156    }
157
158    /**
159     * Starts the muxer.
160     * <p>Make sure this is called after {@link #addTrack} and before
161     * {@link #writeSampleData}.</p>
162     */
163    public void start() {
164        if (mNativeObject == 0) {
165            throw new IllegalStateException("Muxer has been released!");
166        }
167        if (mState == MUXER_STATE_INITIALIZED) {
168            nativeStart(mNativeObject);
169            mState = MUXER_STATE_STARTED;
170        } else {
171            throw new IllegalStateException("Can't start due to wrong state.");
172        }
173    }
174
175    /**
176     * Stops the muxer.
177     * <p>Once the muxer stops, it can not be restarted.</p>
178     */
179    public void stop() {
180        if (mState == MUXER_STATE_STARTED) {
181            nativeStop(mNativeObject);
182            mState = MUXER_STATE_STOPPED;
183        } else {
184            throw new IllegalStateException("Can't stop due to wrong state.");
185        }
186    }
187
188    @Override
189    protected void finalize() throws Throwable {
190        try {
191            if (mCloseGuard != null) {
192                mCloseGuard.warnIfOpen();
193            }
194            if (mNativeObject != 0) {
195                nativeRelease(mNativeObject);
196                mNativeObject = 0;
197            }
198        } finally {
199            super.finalize();
200        }
201    }
202
203    /**
204     * Adds a track with the specified format.
205     * @param format The media format for the track.
206     * @return The track index for this newly added track, and it should be used
207     * in the {@link #writeSampleData}.
208     */
209    public int addTrack(MediaFormat format) {
210        if (format == null) {
211            throw new IllegalArgumentException("format must not be null.");
212        }
213        if (mState != MUXER_STATE_INITIALIZED) {
214            throw new IllegalStateException("Muxer is not initialized.");
215        }
216        if (mNativeObject == 0) {
217            throw new IllegalStateException("Muxer has been released!");
218        }
219        int trackIndex = -1;
220        // Convert the MediaFormat into key-value pairs and send to the native.
221        Map<String, Object> formatMap = format.getMap();
222
223        String[] keys = null;
224        Object[] values = null;
225        int mapSize = formatMap.size();
226        if (mapSize > 0) {
227            keys = new String[mapSize];
228            values = new Object[mapSize];
229            int i = 0;
230            for (Map.Entry<String, Object> entry : formatMap.entrySet()) {
231                keys[i] = entry.getKey();
232                values[i] = entry.getValue();
233                ++i;
234            }
235            trackIndex = nativeAddTrack(mNativeObject, keys, values);
236        } else {
237            throw new IllegalArgumentException("format must not be empty.");
238        }
239
240        // Track index number is expected to incremented as addTrack succeed.
241        // However, if format is invalid, it will get a negative trackIndex.
242        if (mLastTrackIndex >= trackIndex) {
243            throw new IllegalArgumentException("Invalid format.");
244        }
245        mLastTrackIndex = trackIndex;
246        return trackIndex;
247    }
248
249    /**
250     * Writes an encoded sample into the muxer.
251     * <p>The application needs to make sure that the samples are written into
252     * the right tracks. Also, it needs to make sure the samples for each track
253     * are written in chronological order (e.g. in the order they are provided
254     * by the encoder.)</p>
255     * @param byteBuf The encoded sample.
256     * @param trackIndex The track index for this sample.
257     * @param bufferInfo The buffer information related to this sample.
258     * MediaMuxer uses the flags provided in {@link MediaCodec.BufferInfo},
259     * to signal sync frames.
260     */
261    public void writeSampleData(int trackIndex, ByteBuffer byteBuf,
262            BufferInfo bufferInfo) {
263        if (trackIndex < 0 || trackIndex > mLastTrackIndex) {
264            throw new IllegalArgumentException("trackIndex is invalid");
265        }
266
267        if (byteBuf == null) {
268            throw new IllegalArgumentException("byteBuffer must not be null");
269        }
270
271        if (bufferInfo == null) {
272            throw new IllegalArgumentException("bufferInfo must not be null");
273        }
274        if (bufferInfo.size < 0 || bufferInfo.offset < 0
275                || (bufferInfo.offset + bufferInfo.size) > byteBuf.capacity()
276                || bufferInfo.presentationTimeUs < 0) {
277            throw new IllegalArgumentException("bufferInfo must specify a" +
278                    " valid buffer offset, size and presentation time");
279        }
280
281        if (mNativeObject == 0) {
282            throw new IllegalStateException("Muxer has been released!");
283        }
284
285        if (mState != MUXER_STATE_STARTED) {
286            throw new IllegalStateException("Can't write, muxer is not started");
287        }
288
289        nativeWriteSampleData(mNativeObject, trackIndex, byteBuf,
290                bufferInfo.offset, bufferInfo.size,
291                bufferInfo.presentationTimeUs, bufferInfo.flags);
292    }
293
294    /**
295     * Make sure you call this when you're done to free up any resources
296     * instead of relying on the garbage collector to do this for you at
297     * some point in the future.
298     */
299    public void release() {
300        if (mState == MUXER_STATE_STARTED) {
301            stop();
302        }
303        if (mNativeObject != 0) {
304            nativeRelease(mNativeObject);
305            mNativeObject = 0;
306            mCloseGuard.close();
307        }
308        mState = MUXER_STATE_UNINITIALIZED;
309    }
310}
311