Add support for file format for depth/editing in Mp4Muxer
PiperOrigin-RevId: 648747038
This commit is contained in:
parent
9277a34253
commit
3793a06bdd
3
api.txt
3
api.txt
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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;
|
||||
|
||||
/***
|
||||
|
@ -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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user