745 lines
29 KiB
Java
745 lines
29 KiB
Java
/*
|
|
* Copyright (C) 2013 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package android.media;
|
|
|
|
import android.annotation.IntDef;
|
|
import android.annotation.NonNull;
|
|
import android.compat.annotation.UnsupportedAppUsage;
|
|
import android.media.MediaCodec.BufferInfo;
|
|
import android.os.Build;
|
|
|
|
import dalvik.system.CloseGuard;
|
|
|
|
import java.io.FileDescriptor;
|
|
import java.io.IOException;
|
|
import java.io.RandomAccessFile;
|
|
import java.lang.annotation.Retention;
|
|
import java.lang.annotation.RetentionPolicy;
|
|
import java.nio.ByteBuffer;
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* MediaMuxer facilitates muxing elementary streams. Currently MediaMuxer supports MP4, Webm
|
|
* and 3GP file as the output. It also supports muxing B-frames in MP4 since Android Nougat.
|
|
* <p>
|
|
* It is generally used like this:
|
|
*
|
|
* <pre>
|
|
* MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);
|
|
* // More often, the MediaFormat will be retrieved from MediaCodec.getOutputFormat()
|
|
* // or MediaExtractor.getTrackFormat().
|
|
* MediaFormat audioFormat = new MediaFormat(...);
|
|
* MediaFormat videoFormat = new MediaFormat(...);
|
|
* int audioTrackIndex = muxer.addTrack(audioFormat);
|
|
* int videoTrackIndex = muxer.addTrack(videoFormat);
|
|
* ByteBuffer inputBuffer = ByteBuffer.allocate(bufferSize);
|
|
* boolean finished = false;
|
|
* BufferInfo bufferInfo = new BufferInfo();
|
|
*
|
|
* muxer.start();
|
|
* while(!finished) {
|
|
* // getInputBuffer() will fill the inputBuffer with one frame of encoded
|
|
* // sample from either MediaCodec or MediaExtractor, set isAudioSample to
|
|
* // true when the sample is audio data, set up all the fields of bufferInfo,
|
|
* // and return true if there are no more samples.
|
|
* finished = getInputBuffer(inputBuffer, isAudioSample, bufferInfo);
|
|
* if (!finished) {
|
|
* int currentTrackIndex = isAudioSample ? audioTrackIndex : videoTrackIndex;
|
|
* muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo);
|
|
* }
|
|
* };
|
|
* muxer.stop();
|
|
* muxer.release();
|
|
* </pre>
|
|
*
|
|
|
|
<h4>Metadata Track</h4>
|
|
<p>
|
|
Per-frame metadata carries information that correlates with video or audio to facilitate offline
|
|
processing. For example, gyro signals from the sensor can help video stabilization when doing
|
|
offline processing. Metadata tracks are only supported when multiplexing to the MP4 container
|
|
format. When adding a new metadata track, the MIME type format must start with prefix
|
|
"application/" (for example, "application/gyro"). The format of the metadata is
|
|
application-defined. Metadata timestamps must be in the same time base as video and audio
|
|
timestamps. The generated MP4 file uses TextMetaDataSampleEntry (defined in section 12.3.3.2 of
|
|
the ISOBMFF specification) to signal the metadata's MIME type.
|
|
|
|
<pre class=prettyprint>
|
|
MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);
|
|
// SetUp Video/Audio Tracks.
|
|
MediaFormat audioFormat = new MediaFormat(...);
|
|
MediaFormat videoFormat = new MediaFormat(...);
|
|
int audioTrackIndex = muxer.addTrack(audioFormat);
|
|
int videoTrackIndex = muxer.addTrack(videoFormat);
|
|
|
|
// Setup Metadata Track
|
|
MediaFormat metadataFormat = new MediaFormat(...);
|
|
metadataFormat.setString(KEY_MIME, "application/gyro");
|
|
int metadataTrackIndex = muxer.addTrack(metadataFormat);
|
|
|
|
muxer.start();
|
|
while(..) {
|
|
// Allocate bytebuffer and write gyro data(x,y,z) into it.
|
|
ByteBuffer metaData = ByteBuffer.allocate(bufferSize);
|
|
metaData.putFloat(x);
|
|
metaData.putFloat(y);
|
|
metaData.putFloat(z);
|
|
BufferInfo metaInfo = new BufferInfo();
|
|
// Associate this metadata with the video frame by setting
|
|
// the same timestamp as the video frame.
|
|
metaInfo.presentationTimeUs = currentVideoTrackTimeUs;
|
|
metaInfo.offset = 0;
|
|
metaInfo.flags = 0;
|
|
metaInfo.size = bufferSize;
|
|
muxer.writeSampleData(metadataTrackIndex, metaData, metaInfo);
|
|
};
|
|
muxer.stop();
|
|
muxer.release();
|
|
}</pre>
|
|
|
|
<h2 id=History><a name="History"></a>Features and API History</h2>
|
|
<p>
|
|
The following table summarizes the feature support in different API version and containers.
|
|
For API version numbers, see {@link android.os.Build.VERSION_CODES}.
|
|
|
|
<style>
|
|
.api > tr > th, .api > tr > td { text-align: center; padding: 4px 4px; }
|
|
.api > tr > th { vertical-align: bottom; }
|
|
.api > tr > td { vertical-align: middle; }
|
|
.sml > tr > th, .sml > tr > td { text-align: center; padding: 2px 4px; }
|
|
.fn { text-align: center; }
|
|
</style>
|
|
|
|
<table align="right" style="width: 0%">
|
|
<thead>
|
|
<tbody class=api>
|
|
<tr><th>Symbol</th>
|
|
<th>Meaning</th></tr>
|
|
</tbody>
|
|
</thead>
|
|
<tbody class=sml>
|
|
<tr><td>●</td><td>Supported</td></tr>
|
|
<tr><td>○</td><td>Not supported</td></tr>
|
|
<tr><td>▧</td><td>Supported in MP4/WebM/3GP</td></tr>
|
|
<tr><td>⁕</td><td>Only Supported in MP4</td></tr>
|
|
</tbody>
|
|
</table>
|
|
<table align="center" style="width: 100%;">
|
|
<thead class=api>
|
|
<tr>
|
|
<th rowspan=2>Feature</th>
|
|
<th colspan="24">SDK Version</th>
|
|
</tr>
|
|
<tr>
|
|
<th>18</th>
|
|
<th>19</th>
|
|
<th>20</th>
|
|
<th>21</th>
|
|
<th>22</th>
|
|
<th>23</th>
|
|
<th>24</th>
|
|
<th>25</th>
|
|
<th>26+</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class=api>
|
|
<tr>
|
|
<td align="center">MP4 container</td>
|
|
<td>●</td>
|
|
<td>●</td>
|
|
<td>●</td>
|
|
<td>●</td>
|
|
<td>●</td>
|
|
<td>●</td>
|
|
<td>●</td>
|
|
<td>●</td>
|
|
<td>●</td>
|
|
</tr>
|
|
<td align="center">WebM container</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>●</td>
|
|
<td>●</td>
|
|
<td>●</td>
|
|
<td>●</td>
|
|
<td>●</td>
|
|
<td>●</td>
|
|
</tr>
|
|
<td align="center">3GP container</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>●</td>
|
|
</tr>
|
|
<td align="center">Muxing B-Frames(bi-directional predicted frames)</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>⁕</td>
|
|
<td>⁕</td>
|
|
<td>⁕</td>
|
|
</tr>
|
|
</tr>
|
|
<td align="center">Muxing Single Video/Audio Track</td>
|
|
<td>▧</td>
|
|
<td>▧</td>
|
|
<td>▧</td>
|
|
<td>▧</td>
|
|
<td>▧</td>
|
|
<td>▧</td>
|
|
<td>▧</td>
|
|
<td>▧</td>
|
|
<td>▧</td>
|
|
</tr>
|
|
</tr>
|
|
<td align="center">Muxing Multiple Video/Audio Tracks</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>⁕</td>
|
|
</tr>
|
|
</tr>
|
|
<td align="center">Muxing Metadata Tracks</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>○</td>
|
|
<td>⁕</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
*/
|
|
|
|
final public class MediaMuxer {
|
|
|
|
static {
|
|
System.loadLibrary("media_jni");
|
|
}
|
|
|
|
/**
|
|
* Defines the output format. These constants are used with constructor.
|
|
*/
|
|
public static final class OutputFormat {
|
|
/* Do not change these values without updating their counterparts
|
|
* in include/media/stagefright/MediaMuxer.h!
|
|
*/
|
|
private OutputFormat() {}
|
|
/** @hide */
|
|
public static final int MUXER_OUTPUT_FIRST = 0;
|
|
/** MPEG4 media file format*/
|
|
public static final int MUXER_OUTPUT_MPEG_4 = MUXER_OUTPUT_FIRST;
|
|
/** WEBM media file format*/
|
|
public static final int MUXER_OUTPUT_WEBM = MUXER_OUTPUT_FIRST + 1;
|
|
/** 3GPP media file format*/
|
|
public static final int MUXER_OUTPUT_3GPP = MUXER_OUTPUT_FIRST + 2;
|
|
/** HEIF media file format*/
|
|
public static final int MUXER_OUTPUT_HEIF = MUXER_OUTPUT_FIRST + 3;
|
|
/** Ogg media file format*/
|
|
public static final int MUXER_OUTPUT_OGG = MUXER_OUTPUT_FIRST + 4;
|
|
/** @hide */
|
|
public static final int MUXER_OUTPUT_LAST = MUXER_OUTPUT_OGG;
|
|
};
|
|
|
|
/** @hide */
|
|
@IntDef({
|
|
OutputFormat.MUXER_OUTPUT_MPEG_4,
|
|
OutputFormat.MUXER_OUTPUT_WEBM,
|
|
OutputFormat.MUXER_OUTPUT_3GPP,
|
|
OutputFormat.MUXER_OUTPUT_HEIF,
|
|
OutputFormat.MUXER_OUTPUT_OGG,
|
|
})
|
|
@Retention(RetentionPolicy.SOURCE)
|
|
public @interface Format {}
|
|
|
|
// All the native functions are listed here.
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private static native long nativeSetup(@NonNull FileDescriptor fd, int format)
|
|
throws IllegalArgumentException, IOException;
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private static native void nativeRelease(long nativeObject);
|
|
private static native void nativeStart(long nativeObject);
|
|
private static native void nativeStop(long nativeObject);
|
|
private static native int nativeAddTrack(
|
|
long nativeObject, @NonNull String[] keys, @NonNull Object[] values);
|
|
private static native void nativeSetOrientationHint(
|
|
long nativeObject, int degrees);
|
|
private static native void nativeSetLocation(long nativeObject, int latitude, int longitude);
|
|
private static native void nativeWriteSampleData(
|
|
long nativeObject, int trackIndex, @NonNull ByteBuffer byteBuf,
|
|
int offset, int size, long presentationTimeUs, @MediaCodec.BufferFlag int flags);
|
|
|
|
// Muxer internal states.
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private static final int MUXER_STATE_UNINITIALIZED = -1;
|
|
private static final int MUXER_STATE_INITIALIZED = 0;
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private static final int MUXER_STATE_STARTED = 1;
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private static final int MUXER_STATE_STOPPED = 2;
|
|
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private int mState = MUXER_STATE_UNINITIALIZED;
|
|
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private final CloseGuard mCloseGuard = CloseGuard.get();
|
|
private int mLastTrackIndex = -1;
|
|
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private long mNativeObject;
|
|
|
|
private String convertMuxerStateCodeToString(int aState) {
|
|
switch (aState) {
|
|
case MUXER_STATE_UNINITIALIZED:
|
|
return "UNINITIALIZED";
|
|
case MUXER_STATE_INITIALIZED:
|
|
return "INITIALIZED";
|
|
case MUXER_STATE_STARTED:
|
|
return "STARTED";
|
|
case MUXER_STATE_STOPPED:
|
|
return "STOPPED";
|
|
default:
|
|
return "UNKNOWN";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a media muxer that writes to the specified path.
|
|
* <p>The caller must not use the file {@code path} before calling {@link #stop}.
|
|
* @param path The path of the output media file.
|
|
* @param format The format of the output media file.
|
|
* @see android.media.MediaMuxer.OutputFormat
|
|
* @throws IllegalArgumentException if path is invalid or format is not supported.
|
|
* @throws IOException if an error occurs while opening or creating the output file.
|
|
*/
|
|
public MediaMuxer(@NonNull String path, @Format int format) throws IOException {
|
|
if (path == null) {
|
|
throw new IllegalArgumentException("path must not be null");
|
|
}
|
|
// Use RandomAccessFile so we can open the file with RW access;
|
|
// RW access allows the native writer to memory map the output file.
|
|
RandomAccessFile file = null;
|
|
try {
|
|
file = new RandomAccessFile(path, "rws");
|
|
file.setLength(0);
|
|
FileDescriptor fd = file.getFD();
|
|
setUpMediaMuxer(fd, format);
|
|
} finally {
|
|
if (file != null) {
|
|
file.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a media muxer that writes to the specified FileDescriptor.
|
|
* <p>The caller must not use the file referenced by the specified {@code fd} before calling
|
|
* {@link #stop}.
|
|
* <p>It is the caller's responsibility to close the file descriptor, which is safe to do so
|
|
* as soon as this call returns.
|
|
* @param fd The FileDescriptor of the output media file. If {@code format} is
|
|
* {@link OutputFormat#MUXER_OUTPUT_WEBM}, {@code fd} must be open in read-write mode.
|
|
* Otherwise, write mode is sufficient, but read-write is also accepted.
|
|
* @param format The format of the output media file.
|
|
* @see android.media.MediaMuxer.OutputFormat
|
|
* @throws IllegalArgumentException if {@code format} is not supported, or if {@code fd} is
|
|
* not open in the expected mode.
|
|
* @throws IOException if an error occurs while performing an IO operation.
|
|
*/
|
|
public MediaMuxer(@NonNull FileDescriptor fd, @Format int format) throws IOException {
|
|
setUpMediaMuxer(fd, format);
|
|
}
|
|
|
|
private void setUpMediaMuxer(@NonNull FileDescriptor fd, @Format int format) throws IOException {
|
|
if (format < OutputFormat.MUXER_OUTPUT_FIRST || format > OutputFormat.MUXER_OUTPUT_LAST) {
|
|
throw new IllegalArgumentException("format: " + format + " is invalid");
|
|
}
|
|
mNativeObject = nativeSetup(fd, format);
|
|
mState = MUXER_STATE_INITIALIZED;
|
|
mCloseGuard.open("release");
|
|
}
|
|
|
|
/**
|
|
* Sets the orientation hint for output video playback.
|
|
* <p>This method should be called before {@link #start}. Calling this
|
|
* method will not rotate the video frame when muxer is generating the file,
|
|
* but add a composition matrix containing the rotation angle in the output
|
|
* video if the output format is
|
|
* {@link OutputFormat#MUXER_OUTPUT_MPEG_4} so that a video player can
|
|
* choose the proper orientation for playback. Note that some video players
|
|
* may choose to ignore the composition matrix in a video during playback.
|
|
* By default, the rotation degree is 0.</p>
|
|
* @param degrees the angle to be rotated clockwise in degrees.
|
|
* The supported angles are 0, 90, 180, and 270 degrees.
|
|
* @throws IllegalArgumentException if degree is not supported.
|
|
* @throws IllegalStateException If this method is called after {@link #start}.
|
|
*/
|
|
public void setOrientationHint(int degrees) {
|
|
if (degrees != 0 && degrees != 90 && degrees != 180 && degrees != 270) {
|
|
throw new IllegalArgumentException("Unsupported angle: " + degrees);
|
|
}
|
|
if (mState == MUXER_STATE_INITIALIZED) {
|
|
nativeSetOrientationHint(mNativeObject, degrees);
|
|
} else {
|
|
throw new IllegalStateException("Can't set rotation degrees due" +
|
|
" to wrong state(" + convertMuxerStateCodeToString(mState) + ")");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set and store the geodata (latitude and longitude) in the output file.
|
|
* This method should be called before {@link #start}. The geodata is stored
|
|
* in udta box if the output format is
|
|
* {@link OutputFormat#MUXER_OUTPUT_MPEG_4}, and is ignored for other output
|
|
* formats. The geodata is stored according to ISO-6709 standard.
|
|
*
|
|
* @param latitude Latitude in degrees. Its value must be in the range [-90,
|
|
* 90].
|
|
* @param longitude Longitude in degrees. Its value must be in the range
|
|
* [-180, 180].
|
|
* @throws IllegalArgumentException If the given latitude or longitude is out
|
|
* of range.
|
|
* @throws IllegalStateException If this method is called after {@link #start}.
|
|
*/
|
|
public void setLocation(float latitude, float longitude) {
|
|
int latitudex10000 = Math.round(latitude * 10000);
|
|
int longitudex10000 = Math.round(longitude * 10000);
|
|
|
|
if (latitudex10000 > 900000 || latitudex10000 < -900000) {
|
|
String msg = "Latitude: " + latitude + " out of range.";
|
|
throw new IllegalArgumentException(msg);
|
|
}
|
|
if (longitudex10000 > 1800000 || longitudex10000 < -1800000) {
|
|
String msg = "Longitude: " + longitude + " out of range";
|
|
throw new IllegalArgumentException(msg);
|
|
}
|
|
|
|
if (mState == MUXER_STATE_INITIALIZED && mNativeObject != 0) {
|
|
nativeSetLocation(mNativeObject, latitudex10000, longitudex10000);
|
|
} else {
|
|
throw new IllegalStateException("Can't set location due to wrong state("
|
|
+ convertMuxerStateCodeToString(mState) + ")");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts the muxer.
|
|
* <p>Make sure this is called after {@link #addTrack} and before
|
|
* {@link #writeSampleData}.</p>
|
|
* @throws IllegalStateException If this method is called after {@link #start}
|
|
* or Muxer is released
|
|
*/
|
|
public void start() {
|
|
if (mNativeObject == 0) {
|
|
throw new IllegalStateException("Muxer has been released!");
|
|
}
|
|
if (mState == MUXER_STATE_INITIALIZED) {
|
|
nativeStart(mNativeObject);
|
|
mState = MUXER_STATE_STARTED;
|
|
} else {
|
|
throw new IllegalStateException("Can't start due to wrong state("
|
|
+ convertMuxerStateCodeToString(mState) + ")");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stops the muxer.
|
|
* <p>Once the muxer stops, it can not be restarted.</p>
|
|
* @throws IllegalStateException if muxer is in the wrong state.
|
|
*/
|
|
public void stop() {
|
|
if (mState == MUXER_STATE_STARTED) {
|
|
try {
|
|
nativeStop(mNativeObject);
|
|
} catch (Exception e) {
|
|
throw e;
|
|
} finally {
|
|
mState = MUXER_STATE_STOPPED;
|
|
}
|
|
} else {
|
|
throw new IllegalStateException("Can't stop due to wrong state("
|
|
+ convertMuxerStateCodeToString(mState) + ")");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void finalize() throws Throwable {
|
|
try {
|
|
if (mCloseGuard != null) {
|
|
mCloseGuard.warnIfOpen();
|
|
}
|
|
if (mNativeObject != 0) {
|
|
nativeRelease(mNativeObject);
|
|
mNativeObject = 0;
|
|
}
|
|
} finally {
|
|
super.finalize();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds a track with the specified format.
|
|
* <p>
|
|
* The following table summarizes support for specific format keys across android releases.
|
|
* Keys marked with '+:' are required.
|
|
*
|
|
* <table>
|
|
* <thead>
|
|
* <tr>
|
|
* <th rowspan=2>OS Version(s)</th>
|
|
* <td colspan=3>{@code MediaFormat} keys used for</th>
|
|
* </tr><tr>
|
|
* <th>All Tracks</th>
|
|
* <th>Audio Tracks</th>
|
|
* <th>Video Tracks</th>
|
|
* </tr>
|
|
* </thead>
|
|
* <tbody>
|
|
* <tr>
|
|
* <td>{@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}</td>
|
|
* <td rowspan=7>+: {@link MediaFormat#KEY_MIME}</td>
|
|
* <td rowspan=3>+: {@link MediaFormat#KEY_SAMPLE_RATE},<br>
|
|
* +: {@link MediaFormat#KEY_CHANNEL_COUNT},<br>
|
|
* +: <strong>codec-specific data<sup>AAC</sup></strong></td>
|
|
* <td rowspan=5>+: {@link MediaFormat#KEY_WIDTH},<br>
|
|
* +: {@link MediaFormat#KEY_HEIGHT},<br>
|
|
* no {@code KEY_ROTATION},
|
|
* use {@link #setOrientationHint setOrientationHint()}<sup>.mp4</sup>,<br>
|
|
* +: <strong>codec-specific data<sup>AVC, MPEG4</sup></strong></td>
|
|
* </tr><tr>
|
|
* <td>{@link android.os.Build.VERSION_CODES#KITKAT}</td>
|
|
* </tr><tr>
|
|
* <td>{@link android.os.Build.VERSION_CODES#KITKAT_WATCH}</td>
|
|
* </tr><tr>
|
|
* <td>{@link android.os.Build.VERSION_CODES#LOLLIPOP}</td>
|
|
* <td rowspan=4>as above, plus<br>
|
|
* +: <strong>codec-specific data<sup>Vorbis & .webm</sup></strong></td>
|
|
* </tr><tr>
|
|
* <td>{@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}</td>
|
|
* </tr><tr>
|
|
* <td>{@link android.os.Build.VERSION_CODES#M}</td>
|
|
* <td>as above, plus<br>
|
|
* {@link MediaFormat#KEY_BIT_RATE}<sup>AAC</sup></td>
|
|
* </tr><tr>
|
|
* <td>{@link android.os.Build.VERSION_CODES#N}</td>
|
|
* <td>as above, plus<br>
|
|
* <!-- {link MediaFormat#KEY_MAX_BIT_RATE}<sup>AAC, MPEG4</sup>,<br> -->
|
|
* {@link MediaFormat#KEY_BIT_RATE}<sup>MPEG4</sup>,<br>
|
|
* {@link MediaFormat#KEY_HDR_STATIC_INFO}<sup>#, .webm</sup>,<br>
|
|
* {@link MediaFormat#KEY_COLOR_STANDARD}<sup>#</sup>,<br>
|
|
* {@link MediaFormat#KEY_COLOR_TRANSFER}<sup>#</sup>,<br>
|
|
* {@link MediaFormat#KEY_COLOR_RANGE}<sup>#</sup>,<br>
|
|
* +: <strong>codec-specific data<sup>HEVC</sup></strong>,<br>
|
|
* codec-specific data<sup>VP9</sup></td>
|
|
* </tr>
|
|
* <tr>
|
|
* <td colspan=4>
|
|
* <p class=note><strong>Notes:</strong><br>
|
|
* #: storing into container metadata.<br>
|
|
* .mp4, .webm…: for listed containers<br>
|
|
* MPEG4, AAC…: for listed codecs
|
|
* </td>
|
|
* </tr><tr>
|
|
* <td colspan=4>
|
|
* <p class=note>Note that the codec-specific data for the track must be specified using
|
|
* this method. Furthermore, codec-specific data must not be passed/specified via the
|
|
* {@link #writeSampleData writeSampleData()} call.
|
|
* </td>
|
|
* </tr>
|
|
* </tbody>
|
|
* </table>
|
|
*
|
|
* <p>
|
|
* The following table summarizes codec support for containers across android releases:
|
|
*
|
|
* <table>
|
|
* <thead>
|
|
* <tr>
|
|
* <th rowspan=2>OS Version(s)</th>
|
|
* <td colspan=3>Codec support</th>
|
|
* </tr><tr>
|
|
* <th>{@linkplain OutputFormat#MUXER_OUTPUT_MPEG_4 MP4}</th>
|
|
* <th>{@linkplain OutputFormat#MUXER_OUTPUT_WEBM WEBM}</th>
|
|
* </tr>
|
|
* </thead>
|
|
* <tbody>
|
|
* <tr>
|
|
* <td>{@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}</td>
|
|
* <td rowspan=6>{@link MediaFormat#MIMETYPE_AUDIO_AAC AAC},<br>
|
|
* {@link MediaFormat#MIMETYPE_AUDIO_AMR_NB NB-AMR},<br>
|
|
* {@link MediaFormat#MIMETYPE_AUDIO_AMR_WB WB-AMR},<br>
|
|
* {@link MediaFormat#MIMETYPE_VIDEO_H263 H.263},<br>
|
|
* {@link MediaFormat#MIMETYPE_VIDEO_MPEG4 MPEG-4},<br>
|
|
* {@link MediaFormat#MIMETYPE_VIDEO_AVC AVC} (H.264)</td>
|
|
* <td rowspan=3>Not supported</td>
|
|
* </tr><tr>
|
|
* <td>{@link android.os.Build.VERSION_CODES#KITKAT}</td>
|
|
* </tr><tr>
|
|
* <td>{@link android.os.Build.VERSION_CODES#KITKAT_WATCH}</td>
|
|
* </tr><tr>
|
|
* <td>{@link android.os.Build.VERSION_CODES#LOLLIPOP}</td>
|
|
* <td rowspan=3>{@link MediaFormat#MIMETYPE_AUDIO_VORBIS Vorbis},<br>
|
|
* {@link MediaFormat#MIMETYPE_VIDEO_VP8 VP8}</td>
|
|
* </tr><tr>
|
|
* <td>{@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}</td>
|
|
* </tr><tr>
|
|
* <td>{@link android.os.Build.VERSION_CODES#M}</td>
|
|
* </tr><tr>
|
|
* <td>{@link android.os.Build.VERSION_CODES#N}</td>
|
|
* <td>as above, plus<br>
|
|
* {@link MediaFormat#MIMETYPE_VIDEO_HEVC HEVC} (H.265)</td>
|
|
* <td>as above, plus<br>
|
|
* {@link MediaFormat#MIMETYPE_VIDEO_VP9 VP9}</td>
|
|
* </tr>
|
|
* </tbody>
|
|
* </table>
|
|
*
|
|
* @param format The media format for the track. This must not be an empty
|
|
* MediaFormat.
|
|
* @return The track index for this newly added track, and it should be used
|
|
* in the {@link #writeSampleData}.
|
|
* @throws IllegalArgumentException if format is invalid.
|
|
* @throws IllegalStateException if muxer is in the wrong state.
|
|
*/
|
|
public int addTrack(@NonNull MediaFormat format) {
|
|
if (format == null) {
|
|
throw new IllegalArgumentException("format must not be null.");
|
|
}
|
|
if (mState != MUXER_STATE_INITIALIZED) {
|
|
throw new IllegalStateException("Muxer is not initialized.");
|
|
}
|
|
if (mNativeObject == 0) {
|
|
throw new IllegalStateException("Muxer has been released!");
|
|
}
|
|
int trackIndex = -1;
|
|
// Convert the MediaFormat into key-value pairs and send to the native.
|
|
Map<String, Object> formatMap = format.getMap();
|
|
|
|
String[] keys = null;
|
|
Object[] values = null;
|
|
int mapSize = formatMap.size();
|
|
if (mapSize > 0) {
|
|
keys = new String[mapSize];
|
|
values = new Object[mapSize];
|
|
int i = 0;
|
|
for (Map.Entry<String, Object> entry : formatMap.entrySet()) {
|
|
keys[i] = entry.getKey();
|
|
values[i] = entry.getValue();
|
|
++i;
|
|
}
|
|
trackIndex = nativeAddTrack(mNativeObject, keys, values);
|
|
} else {
|
|
throw new IllegalArgumentException("format must not be empty.");
|
|
}
|
|
|
|
// Track index number is expected to incremented as addTrack succeed.
|
|
// However, if format is invalid, it will get a negative trackIndex.
|
|
if (mLastTrackIndex >= trackIndex) {
|
|
throw new IllegalArgumentException("Invalid format.");
|
|
}
|
|
mLastTrackIndex = trackIndex;
|
|
return trackIndex;
|
|
}
|
|
|
|
/**
|
|
* Writes an encoded sample into the muxer.
|
|
* <p>The application needs to make sure that the samples are written into
|
|
* the right tracks. Also, it needs to make sure the samples for each track
|
|
* are written in chronological order (e.g. in the order they are provided
|
|
* by the encoder.)</p>
|
|
* <p> For MPEG4 media format, the duration of the last sample in a track can be set by passing
|
|
* an additional empty buffer(bufferInfo.size = 0) with MediaCodec.BUFFER_FLAG_END_OF_STREAM
|
|
* flag and a suitable presentation timestamp set in bufferInfo parameter as the last sample of
|
|
* that track. This last sample's presentation timestamp shall be a sum of the presentation
|
|
* timestamp and the duration preferred for the original last sample. If no explicit
|
|
* END_OF_STREAM sample was passed, then the duration of the last sample would be the same as
|
|
* that of the sample before that.</p>
|
|
* @param byteBuf The encoded sample.
|
|
* @param trackIndex The track index for this sample.
|
|
* @param bufferInfo The buffer information related to this sample.
|
|
* @throws IllegalArgumentException if trackIndex, byteBuf or bufferInfo is invalid.
|
|
* @throws IllegalStateException if muxer is in wrong state.
|
|
* MediaMuxer uses the flags provided in {@link MediaCodec.BufferInfo},
|
|
* to signal sync frames.
|
|
*/
|
|
public void writeSampleData(int trackIndex, @NonNull ByteBuffer byteBuf,
|
|
@NonNull BufferInfo bufferInfo) {
|
|
if (trackIndex < 0 || trackIndex > mLastTrackIndex) {
|
|
throw new IllegalArgumentException("trackIndex is invalid");
|
|
}
|
|
|
|
if (byteBuf == null) {
|
|
throw new IllegalArgumentException("byteBuffer must not be null");
|
|
}
|
|
|
|
if (bufferInfo == null) {
|
|
throw new IllegalArgumentException("bufferInfo must not be null");
|
|
}
|
|
if (bufferInfo.size < 0 || bufferInfo.offset < 0
|
|
|| (bufferInfo.offset + bufferInfo.size) > byteBuf.capacity()) {
|
|
throw new IllegalArgumentException("bufferInfo must specify a" +
|
|
" valid buffer offset and size");
|
|
}
|
|
|
|
if (mNativeObject == 0) {
|
|
throw new IllegalStateException("Muxer has been released!");
|
|
}
|
|
|
|
if (mState != MUXER_STATE_STARTED) {
|
|
throw new IllegalStateException("Can't write, muxer is not started");
|
|
}
|
|
|
|
nativeWriteSampleData(mNativeObject, trackIndex, byteBuf,
|
|
bufferInfo.offset, bufferInfo.size,
|
|
bufferInfo.presentationTimeUs, bufferInfo.flags);
|
|
}
|
|
|
|
/**
|
|
* Make sure you call this when you're done to free up any resources
|
|
* instead of relying on the garbage collector to do this for you at
|
|
* some point in the future.
|
|
*/
|
|
public void release() {
|
|
if (mState == MUXER_STATE_STARTED) {
|
|
stop();
|
|
}
|
|
if (mNativeObject != 0) {
|
|
nativeRelease(mNativeObject);
|
|
mNativeObject = 0;
|
|
mCloseGuard.close();
|
|
}
|
|
mState = MUXER_STATE_UNINITIALIZED;
|
|
}
|
|
}
|