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.annotation.IntDef;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.media.MediaCodec;
23import android.media.MediaCodec.BufferInfo;
24import dalvik.system.CloseGuard;
25
26import java.io.FileDescriptor;
27import java.io.IOException;
28import java.io.RandomAccessFile;
29import java.lang.annotation.Retention;
30import java.lang.annotation.RetentionPolicy;
31import java.nio.ByteBuffer;
32import java.util.Map;
33
34/**
35 * MediaMuxer facilitates muxing elementary streams. Currently supports mp4 or
36 * webm file as the output and at most one audio and/or one video elementary
37 * stream. MediaMuxer does not support muxing B-frames.
38 * <p>
39 * It is generally used like this:
40 *
41 * <pre>
42 * MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);
43 * // More often, the MediaFormat will be retrieved from MediaCodec.getOutputFormat()
44 * // or MediaExtractor.getTrackFormat().
45 * MediaFormat audioFormat = new MediaFormat(...);
46 * MediaFormat videoFormat = new MediaFormat(...);
47 * int audioTrackIndex = muxer.addTrack(audioFormat);
48 * int videoTrackIndex = muxer.addTrack(videoFormat);
49 * ByteBuffer inputBuffer = ByteBuffer.allocate(bufferSize);
50 * boolean finished = false;
51 * BufferInfo bufferInfo = new BufferInfo();
52 *
53 * muxer.start();
54 * while(!finished) {
55 *   // getInputBuffer() will fill the inputBuffer with one frame of encoded
56 *   // sample from either MediaCodec or MediaExtractor, set isAudioSample to
57 *   // true when the sample is audio data, set up all the fields of bufferInfo,
58 *   // and return true if there are no more samples.
59 *   finished = getInputBuffer(inputBuffer, isAudioSample, bufferInfo);
60 *   if (!finished) {
61 *     int currentTrackIndex = isAudioSample ? audioTrackIndex : videoTrackIndex;
62 *     muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo);
63 *   }
64 * };
65 * muxer.stop();
66 * muxer.release();
67 * </pre>
68 */
69
70final public class MediaMuxer {
71
72    static {
73        System.loadLibrary("media_jni");
74    }
75
76    /**
77     * Defines the output format. These constants are used with constructor.
78     */
79    public static final class OutputFormat {
80        /* Do not change these values without updating their counterparts
81         * in include/media/stagefright/MediaMuxer.h!
82         */
83        private OutputFormat() {}
84        /** MPEG4 media file format*/
85        public static final int MUXER_OUTPUT_MPEG_4 = 0;
86        public static final int MUXER_OUTPUT_WEBM   = 1;
87    };
88
89    /** @hide */
90    @IntDef({
91        OutputFormat.MUXER_OUTPUT_MPEG_4,
92        OutputFormat.MUXER_OUTPUT_WEBM,
93    })
94    @Retention(RetentionPolicy.SOURCE)
95    public @interface Format {}
96
97    // All the native functions are listed here.
98    private static native long nativeSetup(@NonNull FileDescriptor fd, int format);
99    private static native void nativeRelease(long nativeObject);
100    private static native void nativeStart(long nativeObject);
101    private static native void nativeStop(long nativeObject);
102    private static native int nativeAddTrack(
103            long nativeObject, @NonNull String[] keys, @NonNull Object[] values);
104    private static native void nativeSetOrientationHint(
105            long nativeObject, int degrees);
106    private static native void nativeSetLocation(long nativeObject, int latitude, int longitude);
107    private static native void nativeWriteSampleData(
108            long nativeObject, int trackIndex, @NonNull ByteBuffer byteBuf,
109            int offset, int size, long presentationTimeUs, @MediaCodec.BufferFlag int flags);
110
111    // Muxer internal states.
112    private static final int MUXER_STATE_UNINITIALIZED  = -1;
113    private static final int MUXER_STATE_INITIALIZED    = 0;
114    private static final int MUXER_STATE_STARTED        = 1;
115    private static final int MUXER_STATE_STOPPED        = 2;
116
117    private int mState = MUXER_STATE_UNINITIALIZED;
118
119    private final CloseGuard mCloseGuard = CloseGuard.get();
120    private int mLastTrackIndex = -1;
121
122    private long mNativeObject;
123
124    /**
125     * Constructor.
126     * Creates a media muxer that writes to the specified path.
127     * @param path The path of the output media file.
128     * @param format The format of the output media file.
129     * @see android.media.MediaMuxer.OutputFormat
130     * @throws IllegalArgumentException if path is invalid or format is not supported.
131     * @throws IOException if failed to open the file for write.
132     */
133    public MediaMuxer(@NonNull String path, @Format int format) throws IOException {
134        if (path == null) {
135            throw new IllegalArgumentException("path must not be null");
136        }
137        if (format != OutputFormat.MUXER_OUTPUT_MPEG_4 &&
138                format != OutputFormat.MUXER_OUTPUT_WEBM) {
139            throw new IllegalArgumentException("format is invalid");
140        }
141        // Use RandomAccessFile so we can open the file with RW access;
142        // RW access allows the native writer to memory map the output file.
143        RandomAccessFile file = null;
144        try {
145            file = new RandomAccessFile(path, "rws");
146            FileDescriptor fd = file.getFD();
147            mNativeObject = nativeSetup(fd, format);
148            mState = MUXER_STATE_INITIALIZED;
149            mCloseGuard.open("release");
150        } finally {
151            if (file != null) {
152                file.close();
153            }
154        }
155    }
156
157    /**
158     * Sets the orientation hint for output video playback.
159     * <p>This method should be called before {@link #start}. Calling this
160     * method will not rotate the video frame when muxer is generating the file,
161     * but add a composition matrix containing the rotation angle in the output
162     * video if the output format is
163     * {@link OutputFormat#MUXER_OUTPUT_MPEG_4} so that a video player can
164     * choose the proper orientation for playback. Note that some video players
165     * may choose to ignore the composition matrix in a video during playback.
166     * By default, the rotation degree is 0.</p>
167     * @param degrees the angle to be rotated clockwise in degrees.
168     * The supported angles are 0, 90, 180, and 270 degrees.
169     * @throws IllegalArgumentException if degree is not supported.
170     * @throws IllegalStateException If this method is called after {@link #start}.
171     */
172    public void setOrientationHint(int degrees) {
173        if (degrees != 0 && degrees != 90  && degrees != 180 && degrees != 270) {
174            throw new IllegalArgumentException("Unsupported angle: " + degrees);
175        }
176        if (mState == MUXER_STATE_INITIALIZED) {
177            nativeSetOrientationHint(mNativeObject, degrees);
178        } else {
179            throw new IllegalStateException("Can't set rotation degrees due" +
180                    " to wrong state.");
181        }
182    }
183
184    /**
185     * Set and store the geodata (latitude and longitude) in the output file.
186     * This method should be called before {@link #start}. The geodata is stored
187     * in udta box if the output format is
188     * {@link OutputFormat#MUXER_OUTPUT_MPEG_4}, and is ignored for other output
189     * formats. The geodata is stored according to ISO-6709 standard.
190     *
191     * @param latitude Latitude in degrees. Its value must be in the range [-90,
192     * 90].
193     * @param longitude Longitude in degrees. Its value must be in the range
194     * [-180, 180].
195     * @throws IllegalArgumentException If the given latitude or longitude is out
196     * of range.
197     * @throws IllegalStateException If this method is called after {@link #start}.
198     */
199    public void setLocation(float latitude, float longitude) {
200        int latitudex10000  = (int) (latitude * 10000 + 0.5);
201        int longitudex10000 = (int) (longitude * 10000 + 0.5);
202
203        if (latitudex10000 > 900000 || latitudex10000 < -900000) {
204            String msg = "Latitude: " + latitude + " out of range.";
205            throw new IllegalArgumentException(msg);
206        }
207        if (longitudex10000 > 1800000 || longitudex10000 < -1800000) {
208            String msg = "Longitude: " + longitude + " out of range";
209            throw new IllegalArgumentException(msg);
210        }
211
212        if (mState == MUXER_STATE_INITIALIZED && mNativeObject != 0) {
213            nativeSetLocation(mNativeObject, latitudex10000, longitudex10000);
214        } else {
215            throw new IllegalStateException("Can't set location due to wrong state.");
216        }
217    }
218
219    /**
220     * Starts the muxer.
221     * <p>Make sure this is called after {@link #addTrack} and before
222     * {@link #writeSampleData}.</p>
223     * @throws IllegalStateException If this method is called after {@link #start}
224     * or Muxer is released
225     */
226    public void start() {
227        if (mNativeObject == 0) {
228            throw new IllegalStateException("Muxer has been released!");
229        }
230        if (mState == MUXER_STATE_INITIALIZED) {
231            nativeStart(mNativeObject);
232            mState = MUXER_STATE_STARTED;
233        } else {
234            throw new IllegalStateException("Can't start due to wrong state.");
235        }
236    }
237
238    /**
239     * Stops the muxer.
240     * <p>Once the muxer stops, it can not be restarted.</p>
241     * @throws IllegalStateException if muxer is in the wrong state.
242     */
243    public void stop() {
244        if (mState == MUXER_STATE_STARTED) {
245            nativeStop(mNativeObject);
246            mState = MUXER_STATE_STOPPED;
247        } else {
248            throw new IllegalStateException("Can't stop due to wrong state.");
249        }
250    }
251
252    @Override
253    protected void finalize() throws Throwable {
254        try {
255            if (mCloseGuard != null) {
256                mCloseGuard.warnIfOpen();
257            }
258            if (mNativeObject != 0) {
259                nativeRelease(mNativeObject);
260                mNativeObject = 0;
261            }
262        } finally {
263            super.finalize();
264        }
265    }
266
267    /**
268     * Adds a track with the specified format.
269     * <p>
270     * The following table summarizes support for specific format keys across android releases.
271     * Keys marked with '+:' are required.
272     *
273     * <table style="width: 0%">
274     *  <thead>
275     *   <tr>
276     *    <th rowspan=2>OS Version(s)</th>
277     *    <td colspan=3>{@code MediaFormat} keys used for</th>
278     *   </tr><tr>
279     *    <th>All Tracks</th>
280     *    <th>Audio Tracks</th>
281     *    <th>Video Tracks</th>
282     *   </tr>
283     *  </thead>
284     *  <tbody>
285     *   <tr>
286     *    <td>{@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}</td>
287     *    <td rowspan=7>+: {@link MediaFormat#KEY_MIME}</td>
288     *    <td rowspan=3>+: {@link MediaFormat#KEY_SAMPLE_RATE},<br>
289     *        +: {@link MediaFormat#KEY_CHANNEL_COUNT},<br>
290     *        +: <strong>codec-specific data<sup>AAC</sup></strong></td>
291     *    <td rowspan=5>+: {@link MediaFormat#KEY_WIDTH},<br>
292     *        +: {@link MediaFormat#KEY_HEIGHT},<br>
293     *        no {@code KEY_ROTATION},
294     *        use {@link #setOrientationHint setOrientationHint()}<sup>.mp4</sup>,<br>
295     *        +: <strong>codec-specific data<sup>AVC, MPEG4</sup></strong></td>
296     *   </tr><tr>
297     *    <td>{@link android.os.Build.VERSION_CODES#KITKAT}</td>
298     *   </tr><tr>
299     *    <td>{@link android.os.Build.VERSION_CODES#KITKAT_WATCH}</td>
300     *   </tr><tr>
301     *    <td>{@link android.os.Build.VERSION_CODES#LOLLIPOP}</td>
302     *    <td rowspan=4>as above, plus<br>
303     *        +: <strong>codec-specific data<sup>Vorbis & .webm</sup></strong></td>
304     *   </tr><tr>
305     *    <td>{@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}</td>
306     *   </tr><tr>
307     *    <td>{@link android.os.Build.VERSION_CODES#M}</td>
308     *    <td>as above, plus<br>
309     *        {@link MediaFormat#KEY_BIT_RATE}<sup>AAC</sup></td>
310     *   </tr><tr>
311     *    <td>{@link android.os.Build.VERSION_CODES#N}</td>
312     *    <td>as above, plus<br>
313     *        <!-- {link MediaFormat#KEY_MAX_BIT_RATE}<sup>AAC, MPEG4</sup>,<br> -->
314     *        {@link MediaFormat#KEY_BIT_RATE}<sup>MPEG4</sup>,<br>
315     *        {@link MediaFormat#KEY_HDR_STATIC_INFO}<sup>#, .webm</sup>,<br>
316     *        {@link MediaFormat#KEY_COLOR_STANDARD}<sup>#</sup>,<br>
317     *        {@link MediaFormat#KEY_COLOR_TRANSFER}<sup>#</sup>,<br>
318     *        {@link MediaFormat#KEY_COLOR_RANGE}<sup>#</sup>,<br>
319     *        +: <strong>codec-specific data<sup>HEVC</sup></strong>,<br>
320     *        codec-specific data<sup>VP9</sup></td>
321     *   </tr>
322     *   <tr>
323     *    <td colspan=4>
324     *     <p class=note><strong>Notes:</strong><br>
325     *      #: storing into container metadata.<br>
326     *      .mp4, .webm&hellip;: for listed containers<br>
327     *      MPEG4, AAC&hellip;: for listed codecs
328     *    </td>
329     *   </tr><tr>
330     *    <td colspan=4>
331     *     <p class=note>Note that the codec-specific data for the track must be specified using
332     *     this method. Furthermore, codec-specific data must not be passed/specified via the
333     *     {@link #writeSampleData writeSampleData()} call.
334     *    </td>
335     *   </tr>
336     *  </tbody>
337     * </table>
338     *
339     * <p>
340     * The following table summarizes codec support for containers across android releases:
341     *
342     * <table style="width: 0%">
343     *  <thead>
344     *   <tr>
345     *    <th rowspan=2>OS Version(s)</th>
346     *    <td colspan=3>Codec support</th>
347     *   </tr><tr>
348     *    <th>{@linkplain OutputFormat#MUXER_OUTPUT_MPEG_4 MP4}</th>
349     *    <th>{@linkplain OutputFormat#MUXER_OUTPUT_WEBM WEBM}</th>
350     *   </tr>
351     *  </thead>
352     *  <tbody>
353     *   <tr>
354     *    <td>{@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}</td>
355     *    <td rowspan=6>{@link MediaFormat#MIMETYPE_AUDIO_AAC AAC},<br>
356     *        {@link MediaFormat#MIMETYPE_AUDIO_AMR_NB NB-AMR},<br>
357     *        {@link MediaFormat#MIMETYPE_AUDIO_AMR_WB WB-AMR},<br>
358     *        {@link MediaFormat#MIMETYPE_VIDEO_H263 H.263},<br>
359     *        {@link MediaFormat#MIMETYPE_VIDEO_MPEG4 MPEG-4},<br>
360     *        {@link MediaFormat#MIMETYPE_VIDEO_AVC AVC} (H.264)</td>
361     *    <td rowspan=3>Not supported</td>
362     *   </tr><tr>
363     *    <td>{@link android.os.Build.VERSION_CODES#KITKAT}</td>
364     *   </tr><tr>
365     *    <td>{@link android.os.Build.VERSION_CODES#KITKAT_WATCH}</td>
366     *   </tr><tr>
367     *    <td>{@link android.os.Build.VERSION_CODES#LOLLIPOP}</td>
368     *    <td rowspan=3>{@link MediaFormat#MIMETYPE_AUDIO_VORBIS Vorbis},<br>
369     *        {@link MediaFormat#MIMETYPE_VIDEO_VP8 VP8}</td>
370     *   </tr><tr>
371     *    <td>{@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}</td>
372     *   </tr><tr>
373     *    <td>{@link android.os.Build.VERSION_CODES#M}</td>
374     *   </tr><tr>
375     *    <td>{@link android.os.Build.VERSION_CODES#N}</td>
376     *    <td>as above, plus<br>
377     *        {@link MediaFormat#MIMETYPE_VIDEO_HEVC HEVC} (H.265)</td>
378     *    <td>as above, plus<br>
379     *        {@link MediaFormat#MIMETYPE_VIDEO_VP9 VP9}</td>
380     *   </tr>
381     *  </tbody>
382     * </table>
383     *
384     * @param format The media format for the track.  This must not be an empty
385     *               MediaFormat.
386     * @return The track index for this newly added track, and it should be used
387     * in the {@link #writeSampleData}.
388     * @throws IllegalArgumentException if format is invalid.
389     * @throws IllegalStateException if muxer is in the wrong state.
390     */
391    public int addTrack(@NonNull MediaFormat format) {
392        if (format == null) {
393            throw new IllegalArgumentException("format must not be null.");
394        }
395        if (mState != MUXER_STATE_INITIALIZED) {
396            throw new IllegalStateException("Muxer is not initialized.");
397        }
398        if (mNativeObject == 0) {
399            throw new IllegalStateException("Muxer has been released!");
400        }
401        int trackIndex = -1;
402        // Convert the MediaFormat into key-value pairs and send to the native.
403        Map<String, Object> formatMap = format.getMap();
404
405        String[] keys = null;
406        Object[] values = null;
407        int mapSize = formatMap.size();
408        if (mapSize > 0) {
409            keys = new String[mapSize];
410            values = new Object[mapSize];
411            int i = 0;
412            for (Map.Entry<String, Object> entry : formatMap.entrySet()) {
413                keys[i] = entry.getKey();
414                values[i] = entry.getValue();
415                ++i;
416            }
417            trackIndex = nativeAddTrack(mNativeObject, keys, values);
418        } else {
419            throw new IllegalArgumentException("format must not be empty.");
420        }
421
422        // Track index number is expected to incremented as addTrack succeed.
423        // However, if format is invalid, it will get a negative trackIndex.
424        if (mLastTrackIndex >= trackIndex) {
425            throw new IllegalArgumentException("Invalid format.");
426        }
427        mLastTrackIndex = trackIndex;
428        return trackIndex;
429    }
430
431    /**
432     * Writes an encoded sample into the muxer.
433     * <p>The application needs to make sure that the samples are written into
434     * the right tracks. Also, it needs to make sure the samples for each track
435     * are written in chronological order (e.g. in the order they are provided
436     * by the encoder.)</p>
437     * @param byteBuf The encoded sample.
438     * @param trackIndex The track index for this sample.
439     * @param bufferInfo The buffer information related to this sample.
440     * @throws IllegalArgumentException if trackIndex, byteBuf or bufferInfo is  invalid.
441     * @throws IllegalStateException if muxer is in wrong state.
442     * MediaMuxer uses the flags provided in {@link MediaCodec.BufferInfo},
443     * to signal sync frames.
444     */
445    public void writeSampleData(int trackIndex, @NonNull ByteBuffer byteBuf,
446            @NonNull BufferInfo bufferInfo) {
447        if (trackIndex < 0 || trackIndex > mLastTrackIndex) {
448            throw new IllegalArgumentException("trackIndex is invalid");
449        }
450
451        if (byteBuf == null) {
452            throw new IllegalArgumentException("byteBuffer must not be null");
453        }
454
455        if (bufferInfo == null) {
456            throw new IllegalArgumentException("bufferInfo must not be null");
457        }
458        if (bufferInfo.size < 0 || bufferInfo.offset < 0
459                || (bufferInfo.offset + bufferInfo.size) > byteBuf.capacity()
460                || bufferInfo.presentationTimeUs < 0) {
461            throw new IllegalArgumentException("bufferInfo must specify a" +
462                    " valid buffer offset, size and presentation time");
463        }
464
465        if (mNativeObject == 0) {
466            throw new IllegalStateException("Muxer has been released!");
467        }
468
469        if (mState != MUXER_STATE_STARTED) {
470            throw new IllegalStateException("Can't write, muxer is not started");
471        }
472
473        nativeWriteSampleData(mNativeObject, trackIndex, byteBuf,
474                bufferInfo.offset, bufferInfo.size,
475                bufferInfo.presentationTimeUs, bufferInfo.flags);
476    }
477
478    /**
479     * Make sure you call this when you're done to free up any resources
480     * instead of relying on the garbage collector to do this for you at
481     * some point in the future.
482     */
483    public void release() {
484        if (mState == MUXER_STATE_STARTED) {
485            stop();
486        }
487        if (mNativeObject != 0) {
488            nativeRelease(mNativeObject);
489            mNativeObject = 0;
490            mCloseGuard.close();
491        }
492        mState = MUXER_STATE_UNINITIALIZED;
493    }
494}
495