Home | History | Annotate | Download | only in media
      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.annotation.IntDef;
     20 import android.annotation.NonNull;
     21 import android.annotation.Nullable;
     22 import android.media.MediaCodec;
     23 import android.media.MediaCodec.BufferInfo;
     24 import dalvik.system.CloseGuard;
     25 
     26 import java.io.FileDescriptor;
     27 import java.io.IOException;
     28 import java.io.RandomAccessFile;
     29 import java.lang.annotation.Retention;
     30 import java.lang.annotation.RetentionPolicy;
     31 import java.nio.ByteBuffer;
     32 import java.util.Map;
     33 
     34 /**
     35  * MediaMuxer facilitates muxing elementary streams. Currently only supports an
     36  * mp4 file as the output and at most one audio and/or one video elementary
     37  * stream.
     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 
     70 final 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 IOException if failed to open the file for write
    131      */
    132     public MediaMuxer(@NonNull String path, @Format int format) throws IOException {
    133         if (path == null) {
    134             throw new IllegalArgumentException("path must not be null");
    135         }
    136         if (format != OutputFormat.MUXER_OUTPUT_MPEG_4 &&
    137                 format != OutputFormat.MUXER_OUTPUT_WEBM) {
    138             throw new IllegalArgumentException("format is invalid");
    139         }
    140         // Use RandomAccessFile so we can open the file with RW access;
    141         // RW access allows the native writer to memory map the output file.
    142         RandomAccessFile file = null;
    143         try {
    144             file = new RandomAccessFile(path, "rws");
    145             FileDescriptor fd = file.getFD();
    146             mNativeObject = nativeSetup(fd, format);
    147             mState = MUXER_STATE_INITIALIZED;
    148             mCloseGuard.open("release");
    149         } finally {
    150             if (file != null) {
    151                 file.close();
    152             }
    153         }
    154     }
    155 
    156     /**
    157      * Sets the orientation hint for output video playback.
    158      * <p>This method should be called before {@link #start}. Calling this
    159      * method will not rotate the video frame when muxer is generating the file,
    160      * but add a composition matrix containing the rotation angle in the output
    161      * video if the output format is
    162      * {@link OutputFormat#MUXER_OUTPUT_MPEG_4} so that a video player can
    163      * choose the proper orientation for playback. Note that some video players
    164      * may choose to ignore the composition matrix in a video during playback.
    165      * By default, the rotation degree is 0.</p>
    166      * @param degrees the angle to be rotated clockwise in degrees.
    167      * The supported angles are 0, 90, 180, and 270 degrees.
    168      */
    169     public void setOrientationHint(int degrees) {
    170         if (degrees != 0 && degrees != 90  && degrees != 180 && degrees != 270) {
    171             throw new IllegalArgumentException("Unsupported angle: " + degrees);
    172         }
    173         if (mState == MUXER_STATE_INITIALIZED) {
    174             nativeSetOrientationHint(mNativeObject, degrees);
    175         } else {
    176             throw new IllegalStateException("Can't set rotation degrees due" +
    177                     " to wrong state.");
    178         }
    179     }
    180 
    181     /**
    182      * Set and store the geodata (latitude and longitude) in the output file.
    183      * This method should be called before {@link #start}. The geodata is stored
    184      * in udta box if the output format is
    185      * {@link OutputFormat#MUXER_OUTPUT_MPEG_4}, and is ignored for other output
    186      * formats. The geodata is stored according to ISO-6709 standard.
    187      *
    188      * @param latitude Latitude in degrees. Its value must be in the range [-90,
    189      * 90].
    190      * @param longitude Longitude in degrees. Its value must be in the range
    191      * [-180, 180].
    192      * @throws IllegalArgumentException If the given latitude or longitude is out
    193      * of range.
    194      * @throws IllegalStateException If this method is called after {@link #start}.
    195      */
    196     public void setLocation(float latitude, float longitude) {
    197         int latitudex10000  = (int) (latitude * 10000 + 0.5);
    198         int longitudex10000 = (int) (longitude * 10000 + 0.5);
    199 
    200         if (latitudex10000 > 900000 || latitudex10000 < -900000) {
    201             String msg = "Latitude: " + latitude + " out of range.";
    202             throw new IllegalArgumentException(msg);
    203         }
    204         if (longitudex10000 > 1800000 || longitudex10000 < -1800000) {
    205             String msg = "Longitude: " + longitude + " out of range";
    206             throw new IllegalArgumentException(msg);
    207         }
    208 
    209         if (mState == MUXER_STATE_INITIALIZED && mNativeObject != 0) {
    210             nativeSetLocation(mNativeObject, latitudex10000, longitudex10000);
    211         } else {
    212             throw new IllegalStateException("Can't set location due to wrong state.");
    213         }
    214     }
    215 
    216     /**
    217      * Starts the muxer.
    218      * <p>Make sure this is called after {@link #addTrack} and before
    219      * {@link #writeSampleData}.</p>
    220      */
    221     public void start() {
    222         if (mNativeObject == 0) {
    223             throw new IllegalStateException("Muxer has been released!");
    224         }
    225         if (mState == MUXER_STATE_INITIALIZED) {
    226             nativeStart(mNativeObject);
    227             mState = MUXER_STATE_STARTED;
    228         } else {
    229             throw new IllegalStateException("Can't start due to wrong state.");
    230         }
    231     }
    232 
    233     /**
    234      * Stops the muxer.
    235      * <p>Once the muxer stops, it can not be restarted.</p>
    236      */
    237     public void stop() {
    238         if (mState == MUXER_STATE_STARTED) {
    239             nativeStop(mNativeObject);
    240             mState = MUXER_STATE_STOPPED;
    241         } else {
    242             throw new IllegalStateException("Can't stop due to wrong state.");
    243         }
    244     }
    245 
    246     @Override
    247     protected void finalize() throws Throwable {
    248         try {
    249             if (mCloseGuard != null) {
    250                 mCloseGuard.warnIfOpen();
    251             }
    252             if (mNativeObject != 0) {
    253                 nativeRelease(mNativeObject);
    254                 mNativeObject = 0;
    255             }
    256         } finally {
    257             super.finalize();
    258         }
    259     }
    260 
    261     /**
    262      * Adds a track with the specified format.
    263      * @param format The media format for the track.  This must not be an empty
    264      *               MediaFormat.
    265      * @return The track index for this newly added track, and it should be used
    266      * in the {@link #writeSampleData}.
    267      */
    268     public int addTrack(@NonNull MediaFormat format) {
    269         if (format == null) {
    270             throw new IllegalArgumentException("format must not be null.");
    271         }
    272         if (mState != MUXER_STATE_INITIALIZED) {
    273             throw new IllegalStateException("Muxer is not initialized.");
    274         }
    275         if (mNativeObject == 0) {
    276             throw new IllegalStateException("Muxer has been released!");
    277         }
    278         int trackIndex = -1;
    279         // Convert the MediaFormat into key-value pairs and send to the native.
    280         Map<String, Object> formatMap = format.getMap();
    281 
    282         String[] keys = null;
    283         Object[] values = null;
    284         int mapSize = formatMap.size();
    285         if (mapSize > 0) {
    286             keys = new String[mapSize];
    287             values = new Object[mapSize];
    288             int i = 0;
    289             for (Map.Entry<String, Object> entry : formatMap.entrySet()) {
    290                 keys[i] = entry.getKey();
    291                 values[i] = entry.getValue();
    292                 ++i;
    293             }
    294             trackIndex = nativeAddTrack(mNativeObject, keys, values);
    295         } else {
    296             throw new IllegalArgumentException("format must not be empty.");
    297         }
    298 
    299         // Track index number is expected to incremented as addTrack succeed.
    300         // However, if format is invalid, it will get a negative trackIndex.
    301         if (mLastTrackIndex >= trackIndex) {
    302             throw new IllegalArgumentException("Invalid format.");
    303         }
    304         mLastTrackIndex = trackIndex;
    305         return trackIndex;
    306     }
    307 
    308     /**
    309      * Writes an encoded sample into the muxer.
    310      * <p>The application needs to make sure that the samples are written into
    311      * the right tracks. Also, it needs to make sure the samples for each track
    312      * are written in chronological order (e.g. in the order they are provided
    313      * by the encoder.)</p>
    314      * @param byteBuf The encoded sample.
    315      * @param trackIndex The track index for this sample.
    316      * @param bufferInfo The buffer information related to this sample.
    317      * MediaMuxer uses the flags provided in {@link MediaCodec.BufferInfo},
    318      * to signal sync frames.
    319      */
    320     public void writeSampleData(int trackIndex, @NonNull ByteBuffer byteBuf,
    321             @NonNull BufferInfo bufferInfo) {
    322         if (trackIndex < 0 || trackIndex > mLastTrackIndex) {
    323             throw new IllegalArgumentException("trackIndex is invalid");
    324         }
    325 
    326         if (byteBuf == null) {
    327             throw new IllegalArgumentException("byteBuffer must not be null");
    328         }
    329 
    330         if (bufferInfo == null) {
    331             throw new IllegalArgumentException("bufferInfo must not be null");
    332         }
    333         if (bufferInfo.size < 0 || bufferInfo.offset < 0
    334                 || (bufferInfo.offset + bufferInfo.size) > byteBuf.capacity()
    335                 || bufferInfo.presentationTimeUs < 0) {
    336             throw new IllegalArgumentException("bufferInfo must specify a" +
    337                     " valid buffer offset, size and presentation time");
    338         }
    339 
    340         if (mNativeObject == 0) {
    341             throw new IllegalStateException("Muxer has been released!");
    342         }
    343 
    344         if (mState != MUXER_STATE_STARTED) {
    345             throw new IllegalStateException("Can't write, muxer is not started");
    346         }
    347 
    348         nativeWriteSampleData(mNativeObject, trackIndex, byteBuf,
    349                 bufferInfo.offset, bufferInfo.size,
    350                 bufferInfo.presentationTimeUs, bufferInfo.flags);
    351     }
    352 
    353     /**
    354      * Make sure you call this when you're done to free up any resources
    355      * instead of relying on the garbage collector to do this for you at
    356      * some point in the future.
    357      */
    358     public void release() {
    359         if (mState == MUXER_STATE_STARTED) {
    360             stop();
    361         }
    362         if (mNativeObject != 0) {
    363             nativeRelease(mNativeObject);
    364             mNativeObject = 0;
    365             mCloseGuard.close();
    366         }
    367         mState = MUXER_STATE_UNINITIALIZED;
    368     }
    369 }
    370