Add support for file format for depth/editing in Mp4Muxer

PiperOrigin-RevId: 648747038
This commit is contained in:
sheenachhabra 2024-07-02 09:39:30 -07:00 committed by Copybara-Service
parent 9277a34253
commit 3793a06bdd
16 changed files with 1083 additions and 87 deletions

View File

@ -79,6 +79,7 @@ package androidx.media3.common {
field public static final java.util.UUID PLAYREADY_UUID;
field public static final float RATE_UNSET = -3.4028235E38f;
field public static final int ROLE_FLAG_ALTERNATE = 2; // 0x2
field public static final int ROLE_FLAG_AUXILIARY = 32768; // 0x8000
field public static final int ROLE_FLAG_CAPTION = 64; // 0x40
field public static final int ROLE_FLAG_COMMENTARY = 8; // 0x8
field public static final int ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND = 1024; // 0x400
@ -156,7 +157,7 @@ package androidx.media3.common {
@IntDef(open=true, value={androidx.media3.common.C.CRYPTO_TYPE_UNSUPPORTED, androidx.media3.common.C.CRYPTO_TYPE_NONE, androidx.media3.common.C.CRYPTO_TYPE_FRAMEWORK}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface C.CryptoType {
}
@IntDef(flag=true, value={androidx.media3.common.C.ROLE_FLAG_MAIN, androidx.media3.common.C.ROLE_FLAG_ALTERNATE, androidx.media3.common.C.ROLE_FLAG_SUPPLEMENTARY, androidx.media3.common.C.ROLE_FLAG_COMMENTARY, androidx.media3.common.C.ROLE_FLAG_DUB, androidx.media3.common.C.ROLE_FLAG_EMERGENCY, androidx.media3.common.C.ROLE_FLAG_CAPTION, androidx.media3.common.C.ROLE_FLAG_SUBTITLE, androidx.media3.common.C.ROLE_FLAG_SIGN, androidx.media3.common.C.ROLE_FLAG_DESCRIBES_VIDEO, androidx.media3.common.C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND, androidx.media3.common.C.ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY, androidx.media3.common.C.ROLE_FLAG_TRANSCRIBES_DIALOG, androidx.media3.common.C.ROLE_FLAG_EASY_TO_READ, androidx.media3.common.C.ROLE_FLAG_TRICK_PLAY}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface C.RoleFlags {
@IntDef(flag=true, value={androidx.media3.common.C.ROLE_FLAG_MAIN, androidx.media3.common.C.ROLE_FLAG_ALTERNATE, androidx.media3.common.C.ROLE_FLAG_SUPPLEMENTARY, androidx.media3.common.C.ROLE_FLAG_COMMENTARY, androidx.media3.common.C.ROLE_FLAG_DUB, androidx.media3.common.C.ROLE_FLAG_EMERGENCY, androidx.media3.common.C.ROLE_FLAG_CAPTION, androidx.media3.common.C.ROLE_FLAG_SUBTITLE, androidx.media3.common.C.ROLE_FLAG_SIGN, androidx.media3.common.C.ROLE_FLAG_DESCRIBES_VIDEO, androidx.media3.common.C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND, androidx.media3.common.C.ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY, androidx.media3.common.C.ROLE_FLAG_TRANSCRIBES_DIALOG, androidx.media3.common.C.ROLE_FLAG_EASY_TO_READ, androidx.media3.common.C.ROLE_FLAG_TRICK_PLAY, androidx.media3.common.C.ROLE_FLAG_AUXILIARY}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface C.RoleFlags {
}
@IntDef(flag=true, value={androidx.media3.common.C.SELECTION_FLAG_DEFAULT, androidx.media3.common.C.SELECTION_FLAG_FORCED, androidx.media3.common.C.SELECTION_FLAG_AUTOSELECT}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface C.SelectionFlags {

View File

@ -1428,7 +1428,8 @@ public final class C {
ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY,
ROLE_FLAG_TRANSCRIBES_DIALOG,
ROLE_FLAG_EASY_TO_READ,
ROLE_FLAG_TRICK_PLAY
ROLE_FLAG_TRICK_PLAY,
ROLE_FLAG_AUXILIARY
})
public @interface RoleFlags {}
@ -1493,6 +1494,57 @@ public final class C {
/** Indicates the track is intended for trick play. */
public static final int ROLE_FLAG_TRICK_PLAY = 1 << 14;
/**
* Indicates an auxiliary track. An auxiliary track provides additional information about other
* tracks and is generally not meant for stand-alone playback, but rather for further processing
* in conjunction with other tracks (for example, a track with depth information).
*/
public static final int ROLE_FLAG_AUXILIARY = 1 << 15;
/**
* {@linkplain #ROLE_FLAG_AUXILIARY Auxiliary track types}. One of {@link
* #AUXILIARY_TRACK_TYPE_UNDEFINED}, {@link #AUXILIARY_TRACK_TYPE_ORIGINAL}, {@link
* #AUXILIARY_TRACK_TYPE_DEPTH_LINEAR}, {@link #AUXILIARY_TRACK_TYPE_DEPTH_INVERSE}, {@link
* #AUXILIARY_TRACK_TYPE_DEPTH_METADATA}.
*/
@UnstableApi
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
@IntDef({
AUXILIARY_TRACK_TYPE_UNDEFINED,
AUXILIARY_TRACK_TYPE_ORIGINAL,
AUXILIARY_TRACK_TYPE_DEPTH_LINEAR,
AUXILIARY_TRACK_TYPE_DEPTH_INVERSE,
AUXILIARY_TRACK_TYPE_DEPTH_METADATA
})
public @interface AuxiliaryTrackType {}
/** Not an auxiliary track or an auxiliary track with an undefined type. */
@UnstableApi public static final int AUXILIARY_TRACK_TYPE_UNDEFINED = 0;
/** The original video track without any depth based effects applied. */
@UnstableApi public static final int AUXILIARY_TRACK_TYPE_ORIGINAL = 1;
/**
* A linear encoded depth video track.
*
* <p>See https://developer.android.com/static/media/camera/camera2/Dynamic-depth-v1.0.pdf for
* linear depth encoding.
*/
@UnstableApi public static final int AUXILIARY_TRACK_TYPE_DEPTH_LINEAR = 2;
/**
* An inverse encoded depth video track.
*
* <p>See https://developer.android.com/static/media/camera/camera2/Dynamic-depth-v1.0.pdf for
* inverse depth encoding.
*/
@UnstableApi public static final int AUXILIARY_TRACK_TYPE_DEPTH_INVERSE = 3;
/** A timed metadata of depth video track. */
@UnstableApi public static final int AUXILIARY_TRACK_TYPE_DEPTH_METADATA = 4;
/**
* Level of support for a format. One of {@link #FORMAT_HANDLED}, {@link
* #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, {@link

View File

@ -146,6 +146,7 @@ public final class Format {
@Nullable private String language;
private @C.SelectionFlags int selectionFlags;
private @C.RoleFlags int roleFlags;
private @C.AuxiliaryTrackType int auxiliaryTrackType;
private int averageBitrate;
private int peakBitrate;
@Nullable private String codecs;
@ -225,6 +226,7 @@ public final class Format {
tileCountVertical = NO_VALUE;
// Provided by the source.
cryptoType = C.CRYPTO_TYPE_NONE;
auxiliaryTrackType = C.AUXILIARY_TRACK_TYPE_UNDEFINED;
}
/**
@ -360,6 +362,9 @@ public final class Format {
/**
* Sets {@link Format#roleFlags}. The default value is 0.
*
* <p>When {@code roleFlags} includes {@link C#ROLE_FLAG_AUXILIARY}, then the specific {@link
* C.AuxiliaryTrackType} can also be {@linkplain #setAuxiliaryTrackType(int) set}.
*
* @param roleFlags The {@link Format#roleFlags}.
* @return The builder.
*/
@ -369,6 +374,22 @@ public final class Format {
return this;
}
/**
* Sets {@link Format#auxiliaryTrackType}. The default value is {@link
* C#AUXILIARY_TRACK_TYPE_UNDEFINED}.
*
* <p>This must be set to a value other than {@link C#AUXILIARY_TRACK_TYPE_UNDEFINED} only when
* {@linkplain #setRoleFlags(int) role flags} contains {@link C#ROLE_FLAG_AUXILIARY}.
*
* @param auxiliaryTrackType The {@link Format#auxiliaryTrackType}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setAuxiliaryTrackType(@C.AuxiliaryTrackType int auxiliaryTrackType) {
this.auxiliaryTrackType = auxiliaryTrackType;
return this;
}
/**
* Sets {@link Format#averageBitrate}. The default value is {@link #NO_VALUE}.
*
@ -824,6 +845,9 @@ public final class Format {
/** Track role flags. */
public final @C.RoleFlags int roleFlags;
/** The auxiliary track type. */
@UnstableApi public final @C.AuxiliaryTrackType int auxiliaryTrackType;
/**
* The average bitrate in bits per second, or {@link #NO_VALUE} if unknown or not applicable. The
* way in which this field is populated depends on the type of media to which the format
@ -1043,7 +1067,14 @@ public final class Format {
label = builder.label;
}
selectionFlags = builder.selectionFlags;
checkState(
builder.auxiliaryTrackType == C.AUXILIARY_TRACK_TYPE_UNDEFINED
|| (builder.roleFlags & C.ROLE_FLAG_AUXILIARY) != 0,
"Auxiliary track type must only be set to a value other than AUXILIARY_TRACK_TYPE_UNDEFINED"
+ " only when ROLE_FLAG_AUXILIARY is set");
roleFlags = builder.roleFlags;
auxiliaryTrackType = builder.auxiliaryTrackType;
averageBitrate = builder.averageBitrate;
peakBitrate = builder.peakBitrate;
bitrate = peakBitrate != NO_VALUE ? peakBitrate : averageBitrate;
@ -1229,6 +1260,7 @@ public final class Format {
result = 31 * result + (language == null ? 0 : language.hashCode());
result = 31 * result + selectionFlags;
result = 31 * result + roleFlags;
result = 31 * result + auxiliaryTrackType;
result = 31 * result + averageBitrate;
result = 31 * result + peakBitrate;
result = 31 * result + (codecs == null ? 0 : codecs.hashCode());
@ -1284,6 +1316,7 @@ public final class Format {
// Field equality checks ordered by type, with the cheapest checks first.
return selectionFlags == other.selectionFlags
&& roleFlags == other.roleFlags
&& auxiliaryTrackType == other.auxiliaryTrackType
&& averageBitrate == other.averageBitrate
&& peakBitrate == other.peakBitrate
&& maxInputSize == other.maxInputSize
@ -1416,6 +1449,9 @@ public final class Format {
if (format.customData != null) {
builder.append(", customData=").append(format.customData);
}
if ((format.roleFlags & C.ROLE_FLAG_AUXILIARY) != 0) {
builder.append(", auxiliaryTrackType=").append(format.auxiliaryTrackType);
}
return builder.toString();
}
@ -1452,6 +1488,7 @@ public final class Format {
private static final String FIELD_TILE_COUNT_HORIZONTAL = Util.intToStringMaxRadix(30);
private static final String FIELD_TILE_COUNT_VERTICAL = Util.intToStringMaxRadix(31);
private static final String FIELD_LABELS = Util.intToStringMaxRadix(32);
private static final String FIELD_AUXILIARY_TRACK_TYPE = Util.intToStringMaxRadix(33);
/**
* @deprecated Use {@link #toBundle(boolean)} instead.
@ -1476,6 +1513,9 @@ public final class Format {
bundle.putString(FIELD_LANGUAGE, language);
bundle.putInt(FIELD_SELECTION_FLAGS, selectionFlags);
bundle.putInt(FIELD_ROLE_FLAGS, roleFlags);
if (auxiliaryTrackType != DEFAULT.auxiliaryTrackType) {
bundle.putInt(FIELD_AUXILIARY_TRACK_TYPE, auxiliaryTrackType);
}
bundle.putInt(FIELD_AVERAGE_BITRATE, averageBitrate);
bundle.putInt(FIELD_PEAK_BITRATE, peakBitrate);
bundle.putString(FIELD_CODECS, codecs);
@ -1540,6 +1580,8 @@ public final class Format {
.setLanguage(defaultIfNull(bundle.getString(FIELD_LANGUAGE), DEFAULT.language))
.setSelectionFlags(bundle.getInt(FIELD_SELECTION_FLAGS, DEFAULT.selectionFlags))
.setRoleFlags(bundle.getInt(FIELD_ROLE_FLAGS, DEFAULT.roleFlags))
.setAuxiliaryTrackType(
bundle.getInt(FIELD_AUXILIARY_TRACK_TYPE, DEFAULT.auxiliaryTrackType))
.setAverageBitrate(bundle.getInt(FIELD_AVERAGE_BITRATE, DEFAULT.averageBitrate))
.setPeakBitrate(bundle.getInt(FIELD_PEAK_BITRATE, DEFAULT.peakBitrate))
.setCodecs(defaultIfNull(bundle.getString(FIELD_CODECS), DEFAULT.codecs))

View File

@ -144,6 +144,10 @@ public final class MimeTypes {
@UnstableApi
public static final String APPLICATION_CAMERA_MOTION = BASE_TYPE_APPLICATION + "/x-camera-motion";
@UnstableApi
public static final String APPLICATION_DEPTH_METADATA =
BASE_TYPE_APPLICATION + "/x-depth-metadata";
@UnstableApi public static final String APPLICATION_EMSG = BASE_TYPE_APPLICATION + "/x-emsg";
public static final String APPLICATION_DVBSUBS = BASE_TYPE_APPLICATION + "/dvbsubs";
@UnstableApi public static final String APPLICATION_EXIF = BASE_TYPE_APPLICATION + "/x-exif";

View File

@ -3294,6 +3294,9 @@ public final class Util {
if ((roleFlags & C.ROLE_FLAG_TRICK_PLAY) != 0) {
result.add("trick-play");
}
if ((roleFlags & C.ROLE_FLAG_AUXILIARY) != 0) {
result.add("auxiliary");
}
return result;
}

View File

@ -19,6 +19,7 @@ import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import androidx.media3.common.Metadata;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.primitives.Ints;
@ -34,9 +35,21 @@ public final class MdtaMetadataEntry implements Metadata.Entry {
/** Key for the capture frame rate (in frames per second). */
public static final String KEY_ANDROID_CAPTURE_FPS = "com.android.capture.fps";
/** Key for editable tracks box (edvd) offset. */
public static final String KEY_EDITABLE_TRACKS_OFFSET = "editable.tracks.offset";
/** Key for editable tracks box (edvd) length. */
public static final String KEY_EDITABLE_TRACKS_LENGTH = "editable.tracks.length";
/** Key for editable tracks map. */
public static final String KEY_EDITABLE_TRACKS_MAP = "editable.tracks.map";
/** The default locale indicator which implies all speakers in all countries. */
public static final int DEFAULT_LOCALE_INDICATOR = 0;
/** The type indicator to use when no type needs to be indicated. */
public static final int TYPE_INDICATOR_RESERVED = 0;
/** The type indicator for UTF-8 string. */
public static final int TYPE_INDICATOR_STRING = 1;
@ -46,6 +59,9 @@ public final class MdtaMetadataEntry implements Metadata.Entry {
/** The type indicator for 32-bit signed integer. */
public static final int TYPE_INDICATOR_INT32 = 67;
/** The type indicator for 64-bit unsigned integer. */
public static final int TYPE_INDICATOR_UNSIGNED_INT64 = 78;
/** The metadata key name. */
public final String key;
@ -119,6 +135,15 @@ public final class MdtaMetadataEntry implements Metadata.Entry {
case TYPE_INDICATOR_INT32:
formattedValue = String.valueOf(Ints.fromByteArray(value));
break;
case TYPE_INDICATOR_UNSIGNED_INT64:
formattedValue = String.valueOf(new ParsableByteArray(value).readUnsignedLongToLong());
break;
case TYPE_INDICATOR_RESERVED:
if (key.equals(KEY_EDITABLE_TRACKS_MAP)) {
formattedValue = getFormattedValueForEditableTracksMap(value);
break;
}
// fall through
default:
formattedValue = Util.toHexString(value);
}
@ -154,4 +179,18 @@ public final class MdtaMetadataEntry implements Metadata.Entry {
return new MdtaMetadataEntry[size];
}
};
private static String getFormattedValueForEditableTracksMap(byte[] value) {
// Value has 1 byte version, 1 byte track count, n bytes track types.
int numberOfTracks = value[1];
StringBuilder sb = new StringBuilder();
sb.append("track types = ");
for (int i = 0; i < numberOfTracks; i++) {
sb.append(value[i + 2]);
if (i < numberOfTracks - 1) {
sb.append(", ");
}
}
return sb.toString();
}
}

View File

@ -62,4 +62,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
throw new IllegalArgumentException("Unsupported metadata");
}
}
/** Removes a previously added {@link MdtaMetadataEntry}. */
public void removeMdtaMetadataEntry(MdtaMetadataEntry mdtaMetadataEntry) {
metadataEntries.remove(mdtaMetadataEntry);
}
}

View File

@ -17,21 +17,28 @@ package androidx.media3.muxer;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.media.MediaCodec.BufferInfo;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.Metadata;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.container.MdtaMetadataEntry;
import androidx.media3.container.Mp4LocationData;
import androidx.media3.container.Mp4OrientationData;
import androidx.media3.container.Mp4TimestampData;
import androidx.media3.container.XmpData;
import com.google.common.io.ByteStreams;
import com.google.common.primitives.Longs;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.annotation.Documented;
@ -39,6 +46,10 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
/**
* A muxer for creating an MP4 container file.
@ -69,6 +80,19 @@ import java.nio.ByteBuffer;
*/
@UnstableApi
public final class Mp4Muxer implements Muxer {
/** Provides temporary cache files to be used by the muxer. */
public interface CacheFileProvider {
/**
* Returns a cache file path.
*
* <p>Every call to this method should return a new cache file.
*
* <p>The app is responsible for deleting the cache file after {@linkplain Mp4Muxer#close()
* closing} the muxer.
*/
String getCacheFilePath();
}
/** Behavior for the last sample duration. */
@Documented
@ -89,25 +113,48 @@ public final class Mp4Muxer implements Muxer {
*/
public static final int LAST_FRAME_DURATION_BEHAVIOR_DUPLICATE_PREV_DURATION = 1;
/** The specific MP4 file format. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({FILE_FORMAT_DEFAULT, FILE_FORMAT_EDITABLE_VIDEO})
public @interface FileFormat {}
/** The default MP4 format. */
public static final int FILE_FORMAT_DEFAULT = 0;
// TODO: b/345219017 - Add spec details.
/**
* The editable video file format. In this file format all the tracks with {@linkplain
* Format#auxiliaryTrackType} set to {@link C#AUXILIARY_TRACK_TYPE_ORIGINAL}, {@link
* C#AUXILIARY_TRACK_TYPE_DEPTH_LINEAR}, {@link C#AUXILIARY_TRACK_TYPE_DEPTH_INVERSE}, or {@link
* C#AUXILIARY_TRACK_TYPE_DEPTH_METADATA} are written in the MP4 edit data (edvd box). The rest of
* the tracks are written as usual.
*/
public static final int FILE_FORMAT_EDITABLE_VIDEO = 1;
/** A builder for {@link Mp4Muxer} instances. */
public static final class Builder {
private final FileOutputStream fileOutputStream;
private final FileOutputStream outputStream;
private @LastFrameDurationBehavior int lastFrameDurationBehavior;
@Nullable private AnnexBToAvccConverter annexBToAvccConverter;
private boolean sampleCopyEnabled;
private boolean attemptStreamableOutputEnabled;
private @FileFormat int outputFileFormat;
@Nullable private CacheFileProvider cacheFileProvider;
/**
* Creates a {@link Builder} instance with default values.
*
* @param fileOutputStream The {@link FileOutputStream} to write the media data to.
* @param outputStream The {@link FileOutputStream} to write the media data to.
*/
public Builder(FileOutputStream fileOutputStream) {
this.fileOutputStream = checkNotNull(fileOutputStream);
public Builder(FileOutputStream outputStream) {
this.outputStream = outputStream;
lastFrameDurationBehavior = LAST_FRAME_DURATION_BEHAVIOR_INSERT_SHORT_FRAME;
sampleCopyEnabled = true;
attemptStreamableOutputEnabled = true;
outputFileFormat = FILE_FORMAT_DEFAULT;
}
/**
@ -166,40 +213,99 @@ public final class Mp4Muxer implements Muxer {
return this;
}
/**
* Sets the specific MP4 file format.
*
* <p>The default value is {@link #FILE_FORMAT_DEFAULT}.
*
* <p>For {@link #FILE_FORMAT_EDITABLE_VIDEO}, a {@link CacheFileProvider} must also be
* {@linkplain #setCacheFileProvider(CacheFileProvider) set}.
*/
@CanIgnoreReturnValue
public Mp4Muxer.Builder setOutputFileFormat(@FileFormat int fileFormat) {
this.outputFileFormat = fileFormat;
return this;
}
/** Sets the {@link CacheFileProvider}. */
@CanIgnoreReturnValue
public Mp4Muxer.Builder setCacheFileProvider(CacheFileProvider cacheFileProvider) {
this.cacheFileProvider = cacheFileProvider;
return this;
}
/** Builds an {@link Mp4Muxer} instance. */
public Mp4Muxer build() {
checkArgument(
outputFileFormat != FILE_FORMAT_EDITABLE_VIDEO || cacheFileProvider != null,
"A CacheFileProvider must be set for FILE_FORMAT_EDITABLE_VIDEO");
return new Mp4Muxer(
fileOutputStream,
outputStream,
lastFrameDurationBehavior,
annexBToAvccConverter == null ? AnnexBToAvccConverter.DEFAULT : annexBToAvccConverter,
sampleCopyEnabled,
attemptStreamableOutputEnabled);
attemptStreamableOutputEnabled,
outputFileFormat,
cacheFileProvider);
}
}
private static final String TAG = "Mp4Muxer";
private final FileOutputStream fileOutputStream;
// TODO: b/295339654 - Move these constants to container module to share with the extractor.
private static final int EDITABLE_TRACK_TYPE_SHARP = 0;
private static final int EDITABLE_TRACK_TYPE_DEPTH_LINEAR = 1;
private static final int EDITABLE_TRACK_TYPE_DEPTH_INVERSE = 2;
private static final int EDITABLE_TRACK_TYPE_DEPTH_METADATA = 3;
// 4 bytes (indicating a 64-bit length field) + 4 byte (box type) + 8 bytes (actual length)
private static final int EDVD_BOX_HEADER_SIZE_BYTE = 16;
private final FileOutputStream outputStream;
private final FileChannel outputChannel;
private final @LastFrameDurationBehavior int lastFrameDurationBehavior;
private final AnnexBToAvccConverter annexBToAvccConverter;
private final boolean sampleCopyEnabled;
private final boolean attemptStreamableOutputEnabled;
private final @FileFormat int outputFileFormat;
@Nullable private final CacheFileProvider cacheFileProvider;
private final MetadataCollector metadataCollector;
private final Mp4Writer mp4Writer;
private final List<TrackToken> editableVideoTracks;
@Nullable private String cacheFilePath;
@Nullable private FileOutputStream cacheFileOutputStream;
@Nullable private MetadataCollector editableVideoMetadataCollector;
@Nullable private Mp4Writer editableVideoMp4Writer;
@Nullable private Mp4TimestampData timestampData;
private Mp4Muxer(
FileOutputStream fileOutputStream,
FileOutputStream outputStream,
@LastFrameDurationBehavior int lastFrameDurationBehavior,
AnnexBToAvccConverter annexBToAvccConverter,
boolean sampleCopyEnabled,
boolean attemptStreamableOutputEnabled) {
this.fileOutputStream = fileOutputStream;
boolean attemptStreamableOutputEnabled,
@FileFormat int outputFileFormat,
@Nullable CacheFileProvider cacheFileProvider) {
this.outputStream = outputStream;
outputChannel = outputStream.getChannel();
this.lastFrameDurationBehavior = lastFrameDurationBehavior;
this.annexBToAvccConverter = annexBToAvccConverter;
this.sampleCopyEnabled = sampleCopyEnabled;
this.attemptStreamableOutputEnabled = attemptStreamableOutputEnabled;
this.outputFileFormat = outputFileFormat;
this.cacheFileProvider = cacheFileProvider;
metadataCollector = new MetadataCollector();
Mp4MoovStructure moovStructure =
new Mp4MoovStructure(metadataCollector, lastFrameDurationBehavior);
mp4Writer =
new Mp4Writer(
fileOutputStream.getChannel(),
outputChannel,
moovStructure,
annexBToAvccConverter,
sampleCopyEnabled,
attemptStreamableOutputEnabled);
editableVideoTracks = new ArrayList<>();
}
/**
@ -212,9 +318,10 @@ public final class Mp4Muxer implements Muxer {
*
* @param format The {@link Format} for the track.
* @return A unique {@link TrackToken}. It should be used in {@link #writeSampleData}.
* @throws MuxerException If an error occurs while adding track.
*/
@Override
public TrackToken addTrack(Format format) {
public TrackToken addTrack(Format format) throws MuxerException {
return addTrack(/* sortKey= */ 1, format);
}
@ -231,15 +338,26 @@ public final class Mp4Muxer implements Muxer {
* @param sortKey The key used for sorting the track list.
* @param format The {@link Format} for the track.
* @return A unique {@link TrackToken}. It should be used in {@link #writeSampleData}.
* @throws MuxerException If an error occurs while adding track.
*/
public TrackToken addTrack(int sortKey, Format format) {
public TrackToken addTrack(int sortKey, Format format) throws MuxerException {
if (outputFileFormat == FILE_FORMAT_EDITABLE_VIDEO && isEditableVideoTrack(format)) {
try {
ensureSetupForEditableVideoTracks();
} catch (FileNotFoundException e) {
throw new MuxerException("Cache file not found", e);
}
TrackToken trackToken = editableVideoMp4Writer.addTrack(sortKey, format);
editableVideoTracks.add(trackToken);
return trackToken;
}
return mp4Writer.addTrack(sortKey, format);
}
/**
* {@inheritDoc}
*
* <p>Samples are written to the disk in batches. If {@link Builder#setSampleCopyEnabled(boolean)
* <p>Samples are written to the file in batches. If {@link Builder#setSampleCopyEnabled(boolean)
* sample copying} is disabled, the {@code byteBuffer} and the {@code bufferInfo} must not be
* modified after calling this method. Otherwise, they are copied and it is safe to modify them
* after this method returns.
@ -249,13 +367,17 @@ public final class Mp4Muxer implements Muxer {
* Builder#setSampleCopyEnabled(boolean) sample copying} is disabled. Otherwise, the position
* of the buffer is updated but the caller retains ownership.
* @param bufferInfo The {@link BufferInfo} related to this sample.
* @throws MuxerException If there is any error while writing data to the disk.
* @throws MuxerException If an error occurs while writing data to the output file.
*/
@Override
public void writeSampleData(TrackToken trackToken, ByteBuffer byteBuffer, BufferInfo bufferInfo)
throws MuxerException {
try {
mp4Writer.writeSampleData(trackToken, byteBuffer, bufferInfo);
if (editableVideoTracks.contains(trackToken)) {
checkNotNull(editableVideoMp4Writer).writeSampleData(trackToken, byteBuffer, bufferInfo);
} else {
mp4Writer.writeSampleData(trackToken, byteBuffer, bufferInfo);
}
} catch (IOException e) {
throw new MuxerException(
"Failed to write sample for presentationTimeUs="
@ -288,6 +410,9 @@ public final class Mp4Muxer implements Muxer {
@Override
public void addMetadataEntry(Metadata.Entry metadataEntry) {
checkArgument(Mp4Utils.isMetadataSupported(metadataEntry), "Unsupported metadata");
if (metadataEntry instanceof Mp4TimestampData) {
timestampData = (Mp4TimestampData) metadataEntry;
}
metadataCollector.addMetadata(metadataEntry);
}
@ -295,12 +420,14 @@ public final class Mp4Muxer implements Muxer {
public void close() throws MuxerException {
@Nullable MuxerException exception = null;
try {
mp4Writer.finishWritingSamplesAndFinalizeMoovBox();
finishWritingEditableVideoTracks();
finishWritingPrimaryVideoTracks();
appendEditableVideoTracksDataToTheOutputFile();
} catch (IOException e) {
exception = new MuxerException("Failed to finish writing samples", e);
exception = new MuxerException("Failed to finish writing data", e);
}
try {
fileOutputStream.close();
outputStream.close();
} catch (IOException e) {
if (exception == null) {
exception = new MuxerException("Failed to close output stream", e);
@ -308,8 +435,146 @@ public final class Mp4Muxer implements Muxer {
Log.e(TAG, "Failed to close output stream", e);
}
}
if (cacheFileOutputStream != null) {
try {
cacheFileOutputStream.close();
} catch (IOException e) {
if (exception == null) {
exception = new MuxerException("Failed to close the cache file output stream", e);
} else {
Log.e(TAG, "Failed to close cache file output stream", e);
}
}
}
if (exception != null) {
throw exception;
}
}
private static boolean isEditableVideoTrack(Format format) {
return (format.roleFlags & C.ROLE_FLAG_AUXILIARY) > 0
&& (format.auxiliaryTrackType == C.AUXILIARY_TRACK_TYPE_ORIGINAL
|| format.auxiliaryTrackType == C.AUXILIARY_TRACK_TYPE_DEPTH_LINEAR
|| format.auxiliaryTrackType == C.AUXILIARY_TRACK_TYPE_DEPTH_INVERSE
|| format.auxiliaryTrackType == C.AUXILIARY_TRACK_TYPE_DEPTH_METADATA);
}
@EnsuresNonNull({"editableVideoMp4Writer"})
private void ensureSetupForEditableVideoTracks() throws FileNotFoundException {
if (editableVideoMp4Writer == null) {
cacheFilePath = checkNotNull(cacheFileProvider).getCacheFilePath();
cacheFileOutputStream = new FileOutputStream(cacheFilePath);
editableVideoMetadataCollector = new MetadataCollector();
Mp4MoovStructure mp4MoovStructure =
new Mp4MoovStructure(editableVideoMetadataCollector, lastFrameDurationBehavior);
editableVideoMp4Writer =
new Mp4Writer(
cacheFileOutputStream.getChannel(),
mp4MoovStructure,
annexBToAvccConverter,
sampleCopyEnabled,
attemptStreamableOutputEnabled);
}
}
private void finishWritingEditableVideoTracks() throws IOException {
if (editableVideoMp4Writer == null) {
// Editable video tracks were not added.
return;
}
// Write editable tracks map.
// 1 byte version + 1 byte track count (n) + n bytes track types.
int totalTracks = editableVideoTracks.size();
int dataSize = 2 + totalTracks;
byte[] data = new byte[dataSize];
data[0] = 1; // version
data[1] = (byte) totalTracks; // track count
for (int i = 0; i < totalTracks; i++) {
checkState(editableVideoTracks.get(i) instanceof Track);
Track track = (Track) editableVideoTracks.get(i);
int trackType;
switch (track.format.auxiliaryTrackType) {
case C.AUXILIARY_TRACK_TYPE_ORIGINAL:
trackType = EDITABLE_TRACK_TYPE_SHARP;
break;
case C.AUXILIARY_TRACK_TYPE_DEPTH_LINEAR:
trackType = EDITABLE_TRACK_TYPE_DEPTH_LINEAR;
break;
case C.AUXILIARY_TRACK_TYPE_DEPTH_INVERSE:
trackType = EDITABLE_TRACK_TYPE_DEPTH_INVERSE;
break;
case C.AUXILIARY_TRACK_TYPE_DEPTH_METADATA:
trackType = EDITABLE_TRACK_TYPE_DEPTH_METADATA;
break;
default:
throw new IllegalArgumentException(
"Unsupported editable track type " + track.format.auxiliaryTrackType);
}
data[i + 2] = (byte) trackType;
}
checkNotNull(editableVideoMetadataCollector);
editableVideoMetadataCollector.addMetadata(
new MdtaMetadataEntry(
MdtaMetadataEntry.KEY_EDITABLE_TRACKS_MAP,
data,
MdtaMetadataEntry.TYPE_INDICATOR_RESERVED));
if (timestampData != null) {
editableVideoMetadataCollector.addMetadata(timestampData);
}
checkNotNull(editableVideoMp4Writer).finishWritingSamplesAndFinalizeMoovBox();
}
private void finishWritingPrimaryVideoTracks() throws IOException {
// The exact offset is known after writing all the data in mp4Writer.
@Nullable
MdtaMetadataEntry placeholderEditableTrackOffset =
new MdtaMetadataEntry(
MdtaMetadataEntry.KEY_EDITABLE_TRACKS_OFFSET,
new byte[8],
MdtaMetadataEntry.TYPE_INDICATOR_UNSIGNED_INT64);
if (editableVideoMp4Writer != null) {
long editableVideoDataSize = checkNotNull(cacheFileOutputStream).getChannel().size();
long edvdBoxSize = EDVD_BOX_HEADER_SIZE_BYTE + editableVideoDataSize;
metadataCollector.addMetadata(
new MdtaMetadataEntry(
MdtaMetadataEntry.KEY_EDITABLE_TRACKS_LENGTH,
Longs.toByteArray(edvdBoxSize),
MdtaMetadataEntry.TYPE_INDICATOR_UNSIGNED_INT64));
metadataCollector.addMetadata(placeholderEditableTrackOffset);
}
mp4Writer.finishWritingSamplesAndFinalizeMoovBox();
if (editableVideoMp4Writer != null) {
long primaryVideoDataSize = outputChannel.size();
metadataCollector.removeMdtaMetadataEntry(placeholderEditableTrackOffset);
metadataCollector.addMetadata(
new MdtaMetadataEntry(
MdtaMetadataEntry.KEY_EDITABLE_TRACKS_OFFSET,
Longs.toByteArray(primaryVideoDataSize),
MdtaMetadataEntry.TYPE_INDICATOR_UNSIGNED_INT64));
mp4Writer.finalizeMoovBox();
checkState(
outputChannel.size() == primaryVideoDataSize,
"The editable tracks offset should remain the same");
}
}
private void appendEditableVideoTracksDataToTheOutputFile() throws IOException {
if (editableVideoMp4Writer == null) {
// Editable video tracks were not added.
return;
}
outputChannel.position(outputChannel.size());
FileInputStream inputStream = new FileInputStream(checkNotNull(cacheFilePath));
ByteBuffer edvdBoxHeader = ByteBuffer.allocate(EDVD_BOX_HEADER_SIZE_BYTE);
edvdBoxHeader.putInt(1); // indicating a 64-bit length field
edvdBoxHeader.put(Util.getUtf8Bytes("edvd"));
edvdBoxHeader.putLong(
EDVD_BOX_HEADER_SIZE_BYTE + inputStream.getChannel().size()); // the actual length
edvdBoxHeader.flip();
outputChannel.write(edvdBoxHeader);
ByteStreams.copy(inputStream, outputStream);
inputStream.close();
}
}

View File

@ -136,6 +136,69 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
}
/**
* Writes the updated moov box to the output {@link FileChannel}.
*
* <p>It also trims any extra spaces from the file.
*
* @throws IOException If there is any error while writing data to the output {@link FileChannel}.
*/
public void finalizeMoovBox() throws IOException {
if (canWriteMoovAtStart) {
maybeWriteMoovAtStart();
return;
}
// The current state is:
// | ftyp | mdat .. .. .. (00 00 00) | moov |
// To keep the trimming safe, first write the final moov box into the gap at the end of the mdat
// box, and only then trim the extra space.
ByteBuffer currentMoovData = assembleCurrentMoovData();
int moovBytesNeeded = currentMoovData.remaining();
// Write a temporary free box wrapping the new moov box.
int moovAndFreeBytesNeeded = moovBytesNeeded + 8;
if (mdatEnd - mdatDataEnd < moovAndFreeBytesNeeded) {
// If the gap is not big enough for the moov box, then extend the mdat box once again. This
// involves writing moov box farther away one more time.
safelyReplaceMoovAtEnd(
lastMoovWritten.upperEndpoint() + moovAndFreeBytesNeeded, currentMoovData);
checkState(mdatEnd - mdatDataEnd >= moovAndFreeBytesNeeded);
}
// Write out the new moov box into the gap.
long newMoovLocation = mdatDataEnd;
outputFileChannel.position(mdatDataEnd);
outputFileChannel.write(currentMoovData);
// Add a free box to account for the actual remaining length of the file.
long remainingLength = lastMoovWritten.upperEndpoint() - (newMoovLocation + moovBytesNeeded);
// Moov boxes shouldn't be too long; they can fit into a free box with a 32-bit length field.
checkState(remainingLength < Integer.MAX_VALUE);
ByteBuffer freeHeader = ByteBuffer.allocate(4 + 4);
freeHeader.putInt((int) remainingLength);
freeHeader.put(Util.getUtf8Bytes(FREE_BOX_TYPE));
freeHeader.flip();
outputFileChannel.write(freeHeader);
// The moov box is actually written inside mdat box so the current state is:
// | ftyp | mdat .. .. .. (new moov) (free header ) (00 00 00) | old moov |
// Now change this to:
// | ftyp | mdat .. .. .. | new moov | free (00 00 00) (old moov) |
mdatEnd = newMoovLocation;
updateMdatSize(mdatEnd - mdatStart);
lastMoovWritten = Range.closed(newMoovLocation, newMoovLocation + currentMoovData.limit());
// Remove the free box.
outputFileChannel.truncate(newMoovLocation + moovBytesNeeded);
}
private void writeHeader() throws IOException {
outputFileChannel.position(0L);
outputFileChannel.write(Boxes.ftyp());
@ -249,69 +312,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
updateMdatSize(mdatDataEnd - mdatStart);
}
/**
* Writes the updated moov box to the output {@link FileChannel}.
*
* <p>It also trims any extra spaces from the file.
*
* @throws IOException If there is any error while writing data to the output {@link FileChannel}.
*/
private void finalizeMoovBox() throws IOException {
if (canWriteMoovAtStart) {
maybeWriteMoovAtStart();
return;
}
// The current state is:
// | ftyp | mdat .. .. .. (00 00 00) | moov |
// To keep the trimming safe, first write the final moov box into the gap at the end of the mdat
// box, and only then trim the extra space.
ByteBuffer currentMoovData = assembleCurrentMoovData();
int moovBytesNeeded = currentMoovData.remaining();
// Write a temporary free box wrapping the new moov box.
int moovAndFreeBytesNeeded = moovBytesNeeded + 8;
if (mdatEnd - mdatDataEnd < moovAndFreeBytesNeeded) {
// If the gap is not big enough for the moov box, then extend the mdat box once again. This
// involves writing moov box farther away one more time.
safelyReplaceMoovAtEnd(
lastMoovWritten.upperEndpoint() + moovAndFreeBytesNeeded, currentMoovData);
checkState(mdatEnd - mdatDataEnd >= moovAndFreeBytesNeeded);
}
// Write out the new moov box into the gap.
long newMoovLocation = mdatDataEnd;
outputFileChannel.position(mdatDataEnd);
outputFileChannel.write(currentMoovData);
// Add a free box to account for the actual remaining length of the file.
long remainingLength = lastMoovWritten.upperEndpoint() - (newMoovLocation + moovBytesNeeded);
// Moov boxes shouldn't be too long; they can fit into a free box with a 32-bit length field.
checkState(remainingLength < Integer.MAX_VALUE);
ByteBuffer freeHeader = ByteBuffer.allocate(4 + 4);
freeHeader.putInt((int) remainingLength);
freeHeader.put(Util.getUtf8Bytes(FREE_BOX_TYPE));
freeHeader.flip();
outputFileChannel.write(freeHeader);
// The moov box is actually written inside mdat box so the current state is:
// | ftyp | mdat .. .. .. (new moov) (free header ) (00 00 00) | old moov |
// Now change this to:
// | ftyp | mdat .. .. .. | new moov | free (00 00 00) (old moov) |
mdatEnd = newMoovLocation;
updateMdatSize(mdatEnd - mdatStart);
lastMoovWritten = Range.closed(newMoovLocation, newMoovLocation + currentMoovData.limit());
// Remove the free box.
outputFileChannel.truncate(newMoovLocation + moovBytesNeeded);
}
/**
* Rewrites the moov box after accommodating extra bytes needed for the mdat box.
*

View File

@ -19,10 +19,14 @@ import static androidx.media3.muxer.MuxerTestUtil.FAKE_VIDEO_FORMAT;
import static androidx.media3.muxer.MuxerTestUtil.XMP_SAMPLE_DATA;
import static androidx.media3.muxer.MuxerTestUtil.getFakeSampleAndSampleInfo;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.content.Context;
import android.media.MediaCodec.BufferInfo;
import android.util.Pair;
import androidx.media3.common.C;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.Util;
import androidx.media3.container.MdtaMetadataEntry;
import androidx.media3.container.Mp4LocationData;
@ -39,6 +43,7 @@ import androidx.media3.test.utils.TestUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.junit.Rule;
import org.junit.Test;
@ -360,4 +365,242 @@ public class Mp4MuxerEndToEndTest {
dumpableBox,
MuxerTestUtil.getExpectedDumpFilePath("mp4_with_moov_at_the_end_and_no_free_box.mp4"));
}
@Test
public void createMp4Muxer_withFileFormatEditableVideoButWithoutCacheFileProvider_throws()
throws Exception {
String outputFilePath = temporaryFolder.newFile().getPath();
assertThrows(
IllegalArgumentException.class,
() ->
new Mp4Muxer.Builder(new FileOutputStream(outputFilePath))
.setOutputFileFormat(Mp4Muxer.FILE_FORMAT_EDITABLE_VIDEO)
.build());
}
@Test
public void writeMp4File_withFileFormatEditableVideoAndEditableVideoTracks_writesEdvdBox()
throws Exception {
String outputFilePath = temporaryFolder.newFile().getPath();
String cacheFilePath = temporaryFolder.newFile().getPath();
Mp4Muxer muxer =
new Mp4Muxer.Builder(new FileOutputStream(outputFilePath))
.setOutputFileFormat(Mp4Muxer.FILE_FORMAT_EDITABLE_VIDEO)
.setCacheFileProvider(() -> cacheFilePath)
.build();
try {
muxer.addMetadataEntry(
new Mp4TimestampData(
/* creationTimestampSeconds= */ 1_000_000L,
/* modificationTimestampSeconds= */ 5_000_000L));
TrackToken primaryVideoTrackToken = muxer.addTrack(FAKE_VIDEO_FORMAT);
TrackToken sharpVideoTrackToken =
muxer.addTrack(
FAKE_VIDEO_FORMAT
.buildUpon()
.setRoleFlags(C.ROLE_FLAG_AUXILIARY)
.setAuxiliaryTrackType(C.AUXILIARY_TRACK_TYPE_ORIGINAL)
.build());
TrackToken depthLinearVideoTrackToken =
muxer.addTrack(
FAKE_VIDEO_FORMAT
.buildUpon()
.setRoleFlags(C.ROLE_FLAG_AUXILIARY)
.setAuxiliaryTrackType(C.AUXILIARY_TRACK_TYPE_DEPTH_LINEAR)
.build());
writeFakeSamples(muxer, primaryVideoTrackToken, /* sampleCount= */ 5);
writeFakeSamples(muxer, sharpVideoTrackToken, /* sampleCount= */ 5);
writeFakeSamples(muxer, depthLinearVideoTrackToken, /* sampleCount= */ 5);
} finally {
muxer.close();
}
DumpableMp4Box outputFileDumpableBox =
new DumpableMp4Box(ByteBuffer.wrap(TestUtil.getByteArrayFromFilePath(outputFilePath)));
// 1 track is written in the outer moov box and 2 tracks are writtin in the edvd.moov box.
DumpFileAsserts.assertOutput(
context,
outputFileDumpableBox,
MuxerTestUtil.getExpectedDumpFilePath("mp4_with_editable_video_tracks_in_edvd.box"));
}
@Test
public void writeMp4File_withFileFormatDefaultAndEditableVideoTracks_doesNotWriteEdvdBox()
throws Exception {
String outputFilePath = temporaryFolder.newFile().getPath();
String cacheFilePath = temporaryFolder.newFile().getPath();
Mp4Muxer muxer =
new Mp4Muxer.Builder(new FileOutputStream(outputFilePath))
.setCacheFileProvider(() -> cacheFilePath)
.build();
try {
muxer.addMetadataEntry(
new Mp4TimestampData(
/* creationTimestampSeconds= */ 1_000_000L,
/* modificationTimestampSeconds= */ 5_000_000L));
TrackToken primaryVideoTrackToken = muxer.addTrack(FAKE_VIDEO_FORMAT);
TrackToken sharpVideoTrackToken =
muxer.addTrack(
FAKE_VIDEO_FORMAT
.buildUpon()
.setRoleFlags(C.ROLE_FLAG_AUXILIARY)
.setAuxiliaryTrackType(C.AUXILIARY_TRACK_TYPE_ORIGINAL)
.build());
TrackToken depthLinearVideoTrackToken =
muxer.addTrack(
FAKE_VIDEO_FORMAT
.buildUpon()
.setRoleFlags(C.ROLE_FLAG_AUXILIARY)
.setAuxiliaryTrackType(C.AUXILIARY_TRACK_TYPE_DEPTH_LINEAR)
.build());
writeFakeSamples(muxer, primaryVideoTrackToken, /* sampleCount= */ 5);
writeFakeSamples(muxer, sharpVideoTrackToken, /* sampleCount= */ 5);
writeFakeSamples(muxer, depthLinearVideoTrackToken, /* sampleCount= */ 5);
} finally {
muxer.close();
}
DumpableMp4Box outputFileDumpableBox =
new DumpableMp4Box(ByteBuffer.wrap(TestUtil.getByteArrayFromFilePath(outputFilePath)));
// All 3 tracks are written in the outer moov box and no edvd box.
DumpFileAsserts.assertOutput(
context,
outputFileDumpableBox,
MuxerTestUtil.getExpectedDumpFilePath("mp4_with_editable_video_tracks_without_edvd.box"));
}
@Test
public void
writeMp4File_withFileFormatEditableVideoAndEditableVideoTracks_primaryVideoTracksMatchesExpected()
throws Exception {
String outputFilePath = temporaryFolder.newFile().getPath();
String cacheFilePath = temporaryFolder.newFile().getPath();
Mp4Muxer muxer =
new Mp4Muxer.Builder(new FileOutputStream(outputFilePath))
.setOutputFileFormat(Mp4Muxer.FILE_FORMAT_EDITABLE_VIDEO)
.setCacheFileProvider(() -> cacheFilePath)
.build();
try {
muxer.addMetadataEntry(
new Mp4TimestampData(
/* creationTimestampSeconds= */ 1_000_000L,
/* modificationTimestampSeconds= */ 5_000_000L));
TrackToken primaryVideoTrackToken = muxer.addTrack(FAKE_VIDEO_FORMAT);
TrackToken sharpVideoTrackToken =
muxer.addTrack(
FAKE_VIDEO_FORMAT
.buildUpon()
.setRoleFlags(C.ROLE_FLAG_AUXILIARY)
.setAuxiliaryTrackType(C.AUXILIARY_TRACK_TYPE_ORIGINAL)
.build());
TrackToken depthLinearVideoTrackToken =
muxer.addTrack(
FAKE_VIDEO_FORMAT
.buildUpon()
.setRoleFlags(C.ROLE_FLAG_AUXILIARY)
.setAuxiliaryTrackType(C.AUXILIARY_TRACK_TYPE_DEPTH_LINEAR)
.build());
writeFakeSamples(muxer, primaryVideoTrackToken, /* sampleCount= */ 5);
writeFakeSamples(muxer, sharpVideoTrackToken, /* sampleCount= */ 5);
writeFakeSamples(muxer, depthLinearVideoTrackToken, /* sampleCount= */ 5);
} finally {
muxer.close();
}
FakeExtractorOutput primaryTracksOutput =
TestUtil.extractAllSamplesFromFilePath(
new Mp4Extractor(new DefaultSubtitleParserFactory()), outputFilePath);
// The Mp4Extractor can not read edvd box and can only parse primary tracks.
DumpFileAsserts.assertOutput(
context,
primaryTracksOutput,
MuxerTestUtil.getExpectedDumpFilePath("mp4_with_primary_tracks.mp4"));
}
@Test
public void
writeMp4File_withFileFormatEditableVideoAndEditableVideoTracks_editableVideoTracksMatchesExpected()
throws Exception {
String outputFilePath = temporaryFolder.newFile().getPath();
String cacheFilePath = temporaryFolder.newFile().getPath();
Mp4Muxer muxer =
new Mp4Muxer.Builder(new FileOutputStream(outputFilePath))
.setOutputFileFormat(Mp4Muxer.FILE_FORMAT_EDITABLE_VIDEO)
.setCacheFileProvider(() -> cacheFilePath)
.build();
try {
muxer.addMetadataEntry(
new Mp4TimestampData(
/* creationTimestampSeconds= */ 1_000_000L,
/* modificationTimestampSeconds= */ 5_000_000L));
TrackToken primaryVideoTrackToken = muxer.addTrack(FAKE_VIDEO_FORMAT);
TrackToken sharpVideoTrackToken =
muxer.addTrack(
FAKE_VIDEO_FORMAT
.buildUpon()
.setRoleFlags(C.ROLE_FLAG_AUXILIARY)
.setAuxiliaryTrackType(C.AUXILIARY_TRACK_TYPE_ORIGINAL)
.build());
TrackToken depthLinearVideoTrackToken =
muxer.addTrack(
FAKE_VIDEO_FORMAT
.buildUpon()
.setRoleFlags(C.ROLE_FLAG_AUXILIARY)
.setAuxiliaryTrackType(C.AUXILIARY_TRACK_TYPE_DEPTH_LINEAR)
.build());
writeFakeSamples(muxer, primaryVideoTrackToken, /* sampleCount= */ 5);
writeFakeSamples(muxer, sharpVideoTrackToken, /* sampleCount= */ 5);
writeFakeSamples(muxer, depthLinearVideoTrackToken, /* sampleCount= */ 5);
} finally {
muxer.close();
}
byte[] edvdBoxPayload = getEdvdBoxPayload(outputFilePath);
FakeExtractorOutput editableTracksOutput =
TestUtil.extractAllSamplesFromByteArray(
new Mp4Extractor(new DefaultSubtitleParserFactory()), edvdBoxPayload);
// The Mp4Extractor can parse the MP4 embedded in the edvd box.
DumpFileAsserts.assertOutput(
context,
editableTracksOutput,
MuxerTestUtil.getExpectedDumpFilePath("mp4_with_editable_video_tracks.mp4"));
}
private static void writeFakeSamples(Mp4Muxer muxer, TrackToken trackToken, int sampleCount)
throws Muxer.MuxerException {
for (int i = 0; i < sampleCount; i++) {
Pair<ByteBuffer, BufferInfo> sampleAndSampleInfo =
getFakeSampleAndSampleInfo(/* presentationTimeUs= */ i);
muxer.writeSampleData(trackToken, sampleAndSampleInfo.first, sampleAndSampleInfo.second);
}
}
private static byte[] getEdvdBoxPayload(String filePath) throws IOException {
ParsableByteArray data = new ParsableByteArray(TestUtil.getByteArrayFromFilePath(filePath));
while (data.bytesLeft() > 0) {
long size = data.readInt();
String name = data.readString(/* length= */ 4);
long payloadSize = size - 8;
if (size == 1) {
size = data.readUnsignedLongToLong();
// Parsing is not supported for box having size > Integer.MAX_VALUE.
Assertions.checkState(size <= Integer.MAX_VALUE);
// Subtract 4 bytes (32-bit box size) + 4 bytes (box name) + 8 bytes (64-bit box size).
payloadSize = size - 16;
}
if (name.equals("edvd")) {
byte[] payloadData = new byte[(int) payloadSize];
data.readBytes(payloadData, /* offset= */ 0, (int) payloadSize);
return payloadData;
} else {
data.skipBytes((int) payloadSize);
}
}
return new byte[0];
}
}

View File

@ -0,0 +1,87 @@
seekMap:
isSeekable = true
duration = 0
getPosition(0) = [[timeUs=0, position=400052]]
getPosition(1) = [[timeUs=0, position=400276]]
getPosition(0) = [[timeUs=0, position=400052]]
getPosition(0) = [[timeUs=0, position=400052]]
numberOfTracks = 2
track 0:
total output bytes = 280
sample count = 5
format 0:
id = 1
sampleMimeType = video/avc
codecs = avc1.F4000A
maxInputSize = 86
maxNumReorderSamples = 2
width = 12
height = 10
colorInfo:
colorRange = 1
lumaBitdepth = 8
chromaBitdepth = 8
metadata = entries=[mdta: key=editable.tracks.map, value=track types = 0, 1, Mp4Timestamp: creation time=1000000, modification time=5000000, timescale=10000]
initializationData:
data = length 28, hash 410B510
data = length 9, hash FBADD682
sample 0:
time = 0
flags = 1
data = length 56, hash C4551A2E
sample 1:
time = 0
flags = 1
data = length 56, hash C4551A2E
sample 2:
time = 0
flags = 1
data = length 56, hash C4551A2E
sample 3:
time = 0
flags = 1
data = length 56, hash C4551A2E
sample 4:
time = 0
flags = 536870913
data = length 56, hash C4551A2E
track 1:
total output bytes = 280
sample count = 5
format 0:
id = 2
sampleMimeType = video/avc
codecs = avc1.F4000A
maxInputSize = 86
maxNumReorderSamples = 2
width = 12
height = 10
colorInfo:
colorRange = 1
lumaBitdepth = 8
chromaBitdepth = 8
metadata = entries=[mdta: key=editable.tracks.map, value=track types = 0, 1, Mp4Timestamp: creation time=1000000, modification time=5000000, timescale=10000]
initializationData:
data = length 28, hash 410B510
data = length 9, hash FBADD682
sample 0:
time = 0
flags = 1
data = length 56, hash C4551A2E
sample 1:
time = 0
flags = 1
data = length 56, hash C4551A2E
sample 2:
time = 0
flags = 1
data = length 56, hash C4551A2E
sample 3:
time = 0
flags = 1
data = length 56, hash C4551A2E
sample 4:
time = 0
flags = 536870913
data = length 56, hash C4551A2E
tracksEnded = true

View File

@ -0,0 +1,111 @@
ftyp (28 bytes):
Data = length 20, hash EF896440
moov (879 bytes):
mvhd (108 bytes):
Data = length 100, hash 2613A5C
meta (189 bytes):
hdlr (33 bytes):
Data = length 25, hash C39D0F5B
keys (76 bytes):
Data = length 68, hash AEF33438
ilst (72 bytes):
Data = length 64, hash 545348D9
trak (574 bytes):
tkhd (92 bytes):
Data = length 84, hash 3D79758F
mdia (474 bytes):
mdhd (32 bytes):
Data = length 24, hash 41542D81
hdlr (44 bytes):
Data = length 36, hash A0852FF2
minf (390 bytes):
vmhd (20 bytes):
Data = length 12, hash EE830681
dinf (36 bytes):
Data = length 28, hash D535436B
stbl (326 bytes):
stsd (166 bytes):
Data = length 158, hash 11532063
stts (24 bytes):
Data = length 16, hash E534C287
stsz (40 bytes):
Data = length 32, hash B3F09E
stsc (28 bytes):
Data = length 20, hash 8FA6E089
co64 (24 bytes):
Data = length 16, hash E4EE6662
stss (36 bytes):
Data = length 28, hash 53024615
free (399129 bytes):
Data = length 399121, hash 4626C21F
mdat (296 bytes):
Data = length 280, hash 8DCFD2E3
edvd (400628 bytes):
ftyp (28 bytes):
Data = length 20, hash EF896440
moov (1384 bytes):
mvhd (108 bytes):
Data = length 100, hash 2613A5D
meta (120 bytes):
hdlr (33 bytes):
Data = length 25, hash C39D0F5B
keys (43 bytes):
Data = length 35, hash 1CD3D3F
ilst (36 bytes):
Data = length 28, hash D9B9DABE
trak (574 bytes):
tkhd (92 bytes):
Data = length 84, hash 3D79758F
mdia (474 bytes):
mdhd (32 bytes):
Data = length 24, hash 41542D81
hdlr (44 bytes):
Data = length 36, hash A0852FF2
minf (390 bytes):
vmhd (20 bytes):
Data = length 12, hash EE830681
dinf (36 bytes):
Data = length 28, hash D535436B
stbl (326 bytes):
stsd (166 bytes):
Data = length 158, hash 11532063
stts (24 bytes):
Data = length 16, hash E534C287
stsz (40 bytes):
Data = length 32, hash B3F09E
stsc (28 bytes):
Data = length 20, hash 8FA6E089
co64 (24 bytes):
Data = length 16, hash E4EE6662
stss (36 bytes):
Data = length 28, hash 53024615
trak (574 bytes):
tkhd (92 bytes):
Data = length 84, hash 2ECB0510
mdia (474 bytes):
mdhd (32 bytes):
Data = length 24, hash 41542D81
hdlr (44 bytes):
Data = length 36, hash A0852FF2
minf (390 bytes):
vmhd (20 bytes):
Data = length 12, hash EE830681
dinf (36 bytes):
Data = length 28, hash D535436B
stbl (326 bytes):
stsd (166 bytes):
Data = length 158, hash 11532063
stts (24 bytes):
Data = length 16, hash E534C287
stsz (40 bytes):
Data = length 32, hash B3F09E
stsc (28 bytes):
Data = length 20, hash 8FA6E089
co64 (24 bytes):
Data = length 16, hash E4EE6699
stss (36 bytes):
Data = length 28, hash 53024615
free (398624 bytes):
Data = length 398616, hash 25A3AD01
mdat (576 bytes):
Data = length 560, hash 9E0D5FC5

View File

@ -0,0 +1,87 @@
ftyp (28 bytes):
Data = length 20, hash EF896440
moov (1838 bytes):
mvhd (108 bytes):
Data = length 100, hash 2613A5E
trak (574 bytes):
tkhd (92 bytes):
Data = length 84, hash 3D79758F
mdia (474 bytes):
mdhd (32 bytes):
Data = length 24, hash 41542D81
hdlr (44 bytes):
Data = length 36, hash A0852FF2
minf (390 bytes):
vmhd (20 bytes):
Data = length 12, hash EE830681
dinf (36 bytes):
Data = length 28, hash D535436B
stbl (326 bytes):
stsd (166 bytes):
Data = length 158, hash 11532063
stts (24 bytes):
Data = length 16, hash E534C287
stsz (40 bytes):
Data = length 32, hash B3F09E
stsc (28 bytes):
Data = length 20, hash 8FA6E089
co64 (24 bytes):
Data = length 16, hash E4EE6662
stss (36 bytes):
Data = length 28, hash 53024615
trak (574 bytes):
tkhd (92 bytes):
Data = length 84, hash 2ECB0510
mdia (474 bytes):
mdhd (32 bytes):
Data = length 24, hash 41542D81
hdlr (44 bytes):
Data = length 36, hash A0852FF2
minf (390 bytes):
vmhd (20 bytes):
Data = length 12, hash EE830681
dinf (36 bytes):
Data = length 28, hash D535436B
stbl (326 bytes):
stsd (166 bytes):
Data = length 158, hash 11532063
stts (24 bytes):
Data = length 16, hash E534C287
stsz (40 bytes):
Data = length 32, hash B3F09E
stsc (28 bytes):
Data = length 20, hash 8FA6E089
co64 (24 bytes):
Data = length 16, hash E4EE6699
stss (36 bytes):
Data = length 28, hash 53024615
trak (574 bytes):
tkhd (92 bytes):
Data = length 84, hash 201C9491
mdia (474 bytes):
mdhd (32 bytes):
Data = length 24, hash 41542D81
hdlr (44 bytes):
Data = length 36, hash A0852FF2
minf (390 bytes):
vmhd (20 bytes):
Data = length 12, hash EE830681
dinf (36 bytes):
Data = length 28, hash D535436B
stbl (326 bytes):
stsd (166 bytes):
Data = length 158, hash 11532063
stts (24 bytes):
Data = length 16, hash E534C287
stsz (40 bytes):
Data = length 32, hash B3F09E
stsc (28 bytes):
Data = length 20, hash 8FA6E089
co64 (24 bytes):
Data = length 16, hash E4EE66D0
stss (36 bytes):
Data = length 28, hash 53024615
free (398170 bytes):
Data = length 398162, hash E06B79C1
mdat (856 bytes):
Data = length 840, hash A66AA6A7

View File

@ -0,0 +1,48 @@
seekMap:
isSeekable = true
duration = 0
getPosition(0) = [[timeUs=0, position=400052]]
getPosition(1) = [[timeUs=0, position=400276]]
getPosition(0) = [[timeUs=0, position=400052]]
getPosition(0) = [[timeUs=0, position=400052]]
numberOfTracks = 1
track 0:
total output bytes = 280
sample count = 5
format 0:
id = 1
sampleMimeType = video/avc
codecs = avc1.F4000A
maxInputSize = 86
maxNumReorderSamples = 2
width = 12
height = 10
colorInfo:
colorRange = 1
lumaBitdepth = 8
chromaBitdepth = 8
metadata = entries=[mdta: key=editable.tracks.offset, value=400332, mdta: key=editable.tracks.length, value=400628, Mp4Timestamp: creation time=1000000, modification time=5000000, timescale=10000]
initializationData:
data = length 28, hash 410B510
data = length 9, hash FBADD682
sample 0:
time = 0
flags = 1
data = length 56, hash C4551A2E
sample 1:
time = 0
flags = 1
data = length 56, hash C4551A2E
sample 2:
time = 0
flags = 1
data = length 56, hash C4551A2E
sample 3:
time = 0
flags = 1
data = length 56, hash C4551A2E
sample 4:
time = 0
flags = 536870913
data = length 56, hash C4551A2E
tracksEnded = true

View File

@ -26,7 +26,7 @@ import java.nio.ByteBuffer;
public final class DumpableMp4Box implements Dumper.Dumpable {
private static final ImmutableSet<String> CONTAINER_BOXES =
ImmutableSet.of(
"moov", "trak", "mdia", "minf", "stbl", "edts", "meta", "mvex", "moof", "traf");
"moov", "trak", "mdia", "minf", "stbl", "edts", "meta", "mvex", "moof", "traf", "edvd");
private final ParsableByteArray box;
/***

View File

@ -475,8 +475,17 @@ public class TestUtil {
return extractAllSamplesFromByteArray(extractor, data);
}
private static FakeExtractorOutput extractAllSamplesFromByteArray(
Extractor extractor, byte[] data) throws IOException {
/**
* Extracts all samples from the given byte array into a {@link FakeTrackOutput}.
*
* @param extractor The {@link Extractor} to be used.
* @param data The byte array data.
* @return The {@link FakeTrackOutput} containing the extracted samples.
* @throws IOException If an error occurred reading from the input, or if the extractor finishes
* reading from input without extracting any {@link SeekMap}.
*/
public static FakeExtractorOutput extractAllSamplesFromByteArray(Extractor extractor, byte[] data)
throws IOException {
FakeExtractorOutput expectedOutput = new FakeExtractorOutput();
extractor.init(expectedOutput);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();