Skip to content

Commit

Permalink
Add support for passing creation time via InAppMuxer
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 538175466
  • Loading branch information
SheenaChhabra authored and tof-tof committed Jun 6, 2023
1 parent 9ca6e5d commit 7e14811
Show file tree
Hide file tree
Showing 118 changed files with 711 additions and 109 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.container;

import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import androidx.media3.common.Metadata;
import androidx.media3.common.util.UnstableApi;
import com.google.common.primitives.Longs;

/** Stores creation time. */
@UnstableApi
public final class CreationTime implements Metadata.Entry {
public final long timestampMs;

/**
* Creates an instance.
*
* @param timestampMs The creation time UTC in milliseconds since the Unix epoch.
*/
public CreationTime(long timestampMs) {
this.timestampMs = timestampMs;
}

private CreationTime(Parcel in) {
this.timestampMs = in.readLong();
}

@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof CreationTime)) {
return false;
}

return timestampMs == ((CreationTime) obj).timestampMs;
}

@Override
public int hashCode() {
return Longs.hashCode(timestampMs);
}

@Override
public String toString() {
long unsetCreationTime = -2_082_844_800_000L;
return "Creation time: " + (timestampMs == unsetCreationTime ? "unset" : timestampMs);
}

// Parcelable implementation.

@Override
public int describeContents() {
return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(timestampMs);
}

public static final Parcelable.Creator<CreationTime> CREATOR =
new Parcelable.Creator<CreationTime>() {

@Override
public CreationTime createFromParcel(Parcel in) {
return new CreationTime(in);
}

@Override
public CreationTime[] newArray(int size) {
return new CreationTime[size];
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.container.CreationTime;
import androidx.media3.container.MdtaMetadataEntry;
import androidx.media3.exoplayer.source.TrackGroupArray;
import androidx.media3.extractor.metadata.mp4.MotionPhotoMetadata;
Expand Down Expand Up @@ -162,6 +163,7 @@ public void retrieveMetadata_sefSlowMotion_outputsExpectedMetadata() throws Exce
new SlowMotionData.Segment(
/* startTimeMs= */ 1255, /* endTimeMs= */ 1970, /* speedDivisor= */ 8));
SlowMotionData expectedSlowMotionData = new SlowMotionData(segments);
CreationTime expectedCreationTime = new CreationTime(/* timestampMs= */ 1604060090000L);
MdtaMetadataEntry expectedMdtaEntry =
new MdtaMetadataEntry(
KEY_ANDROID_CAPTURE_FPS,
Expand All @@ -176,14 +178,17 @@ public void retrieveMetadata_sefSlowMotion_outputsExpectedMetadata() throws Exce

assertThat(trackGroups.length).isEqualTo(2); // Video and audio
// Audio
assertThat(trackGroups.get(0).getFormat(0).metadata.length()).isEqualTo(2);
assertThat(trackGroups.get(0).getFormat(0).metadata.length()).isEqualTo(3);
assertThat(trackGroups.get(0).getFormat(0).metadata.get(0)).isEqualTo(expectedSmtaEntry);
assertThat(trackGroups.get(0).getFormat(0).metadata.get(1)).isEqualTo(expectedSlowMotionData);
assertThat(trackGroups.get(0).getFormat(0).metadata.get(2)).isEqualTo(expectedCreationTime);

// Video
assertThat(trackGroups.get(1).getFormat(0).metadata.length()).isEqualTo(3);
assertThat(trackGroups.get(1).getFormat(0).metadata.length()).isEqualTo(4);
assertThat(trackGroups.get(1).getFormat(0).metadata.get(0)).isEqualTo(expectedMdtaEntry);
assertThat(trackGroups.get(1).getFormat(0).metadata.get(1)).isEqualTo(expectedSmtaEntry);
assertThat(trackGroups.get(1).getFormat(0).metadata.get(2)).isEqualTo(expectedSlowMotionData);
assertThat(trackGroups.get(1).getFormat(0).metadata.get(3)).isEqualTo(expectedCreationTime);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import androidx.media3.common.util.Log;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.Util;
import androidx.media3.container.CreationTime;
import androidx.media3.container.Mp4LocationData;
import androidx.media3.extractor.AacUtil;
import androidx.media3.extractor.Ac3Util;
Expand Down Expand Up @@ -79,6 +80,19 @@ public UdtaInfo(
}
}

/** Stores data retrieved from the mvhd atom. */
public static final class MvhdInfo {
/** The metadata. */
public final Metadata metadata;
/** The movie timescale. */
public final long timescale;

public MvhdInfo(Metadata metadata, long timescale) {
this.metadata = metadata;
this.timescale = timescale;
}
}

private static final String TAG = "AtomParsers";

@SuppressWarnings("ConstantCaseForConstants")
Expand Down Expand Up @@ -205,6 +219,35 @@ public static UdtaInfo parseUdta(Atom.LeafAtom udtaAtom) {
return new UdtaInfo(metaMetadata, smtaMetadata, xyzMetadata);
}

/**
* Parses a mvhd atom (defined in ISO/IEC 14496-12), returning the timescale for the movie.
*
* @param mvhd Contents of the mvhd atom to be parsed.
* @return An object containing the parsed data.
*/
public static MvhdInfo parseMvhd(ParsableByteArray mvhd) {
mvhd.setPosition(Atom.HEADER_SIZE);
int fullAtom = mvhd.readInt();
int version = Atom.parseFullAtomVersion(fullAtom);
long creationTimestampSeconds;
if (version == 0) {
creationTimestampSeconds = mvhd.readUnsignedInt();
mvhd.skipBytes(4); // modification_time
} else {
creationTimestampSeconds = mvhd.readLong();
mvhd.skipBytes(8); // modification_time
}

// Convert creation time from MP4 format to Unix epoch timestamp in Ms.
// Time delta between January 1, 1904 (MP4 format) and January 1, 1970 (Unix epoch).
// Includes leap year.
int timeDeltaSeconds = (66 * 365 + 17) * (24 * 60 * 60);
long unixTimestampMs = (creationTimestampSeconds - timeDeltaSeconds) * 1000;

long timescale = mvhd.readUnsignedInt();
return new MvhdInfo(new Metadata(new CreationTime(unixTimestampMs)), timescale);
}

/**
* Parses a metadata meta atom if it contains metadata with handler 'mdta'.
*
Expand Down Expand Up @@ -318,7 +361,7 @@ private static Track parseTrak(
if (duration == C.TIME_UNSET) {
duration = tkhdData.duration;
}
long movieTimescale = parseMvhd(mvhd.data);
long movieTimescale = parseMvhd(mvhd.data).timescale;
long durationUs;
if (duration == C.TIME_UNSET) {
durationUs = C.TIME_UNSET;
Expand Down Expand Up @@ -835,23 +878,10 @@ private static Metadata parseSmta(ParsableByteArray smta, int limit) {
return null;
}

/**
* Parses a mvhd atom (defined in ISO/IEC 14496-12), returning the timescale for the movie.
*
* @param mvhd Contents of the mvhd atom to be parsed.
* @return Timescale for the movie.
*/
private static long parseMvhd(ParsableByteArray mvhd) {
mvhd.setPosition(Atom.HEADER_SIZE);
int fullAtom = mvhd.readInt();
int version = Atom.parseFullAtomVersion(fullAtom);
mvhd.skipBytes(version == 0 ? 8 : 16);
return mvhd.readUnsignedInt();
}

/**
* Parses a tkhd atom (defined in ISO/IEC 14496-12).
*
* @param tkhd Contents of the tkhd atom to be parsed.
* @return An object containing the parsed data.
*/
private static TkhdData parseTkhd(ParsableByteArray tkhd) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package androidx.media3.extractor.mp4;

import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.castNonNull;
import static androidx.media3.extractor.mp4.AtomParsers.parseTraks;
import static androidx.media3.extractor.mp4.Sniffer.BRAND_HEIC;
Expand Down Expand Up @@ -511,6 +512,9 @@ private void processMoovAtom(ContainerAtom moov) throws ParserException {
mdtaMetadata = AtomParsers.parseMdtaFromMeta(meta);
}

Metadata mvhdMetadata =
AtomParsers.parseMvhd(checkNotNull(moov.getLeafAtomOfType(Atom.TYPE_mvhd)).data).metadata;

boolean ignoreEditLists = (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0;
List<TrackSampleTable> trackSampleTables =
parseTraks(
Expand Down Expand Up @@ -562,7 +566,8 @@ private void processMoovAtom(ContainerAtom moov) throws ParserException {
formatBuilder,
smtaMetadata,
slowMotionMetadataEntries.isEmpty() ? null : new Metadata(slowMotionMetadataEntries),
xyzMetadata);
xyzMetadata,
mvhdMetadata);
mp4Track.trackOutput.format(formatBuilder.build());

if (track.type == C.TRACK_TYPE_VIDEO && firstVideoTrackIndex == C.INDEX_UNSET) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public void createMp4File_fromInputFileSampleData_matchesExpected() throws IOExc

try {
mp4Muxer = new Mp4Muxer.Builder(outputStream).build();
mp4Muxer.setModificationTime(/* timestampMs= */ 500_000_000L);
feedInputDataToMuxer(mp4Muxer, inputFile);
} finally {
if (mp4Muxer != null) {
Expand All @@ -97,6 +98,7 @@ public void createMp4File_fromInputFileSampleData_matchesExpected() throws IOExc
@Test
public void createMp4File_muxerNotClosed_createsPartiallyWrittenValidFile() throws IOException {
Mp4Muxer mp4Muxer = new Mp4Muxer.Builder(outputStream).build();
mp4Muxer.setModificationTime(/* timestampMs= */ 500_000_000L);
feedInputDataToMuxer(mp4Muxer, H265_HDR10_MP4);

// Muxer not closed.
Expand Down
11 changes: 7 additions & 4 deletions libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java
Original file line number Diff line number Diff line change
Expand Up @@ -1101,11 +1101,14 @@ private static ByteBuffer audioEsdsBox(Format format) {
return BoxUtils.wrapIntoBox("esds", contents);
}

/** Convert UNIX timestamps to the format used by MP4 files. */
/** Convert Unix epoch timestamps to the format used by MP4 files. */
private static int toMp4Time(long unixTimeMs) {
// Jan 1, 1904, including leap years.
long delta = (66 * 365 + 17) * (24 * 60 * 60);
return (int) (unixTimeMs / 1000L + delta);
// Time delta between January 1, 1904 (MP4 format) and January 1, 1970 (Unix epoch).
// Includes leap year.
long timeDeltaSeconds = (66 * 365 + 17) * (24 * 60 * 60);

// The returned value is a positive (when read as unsigned) integer.
return (int) (unixTimeMs / 1000L + timeDeltaSeconds);
}

/** Packs a three-letter language code into a short, packing 3x5 bits. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,10 @@ public void setCaptureFps(float captureFps) {
/**
* Sets the file modification time.
*
* @param modificationDateUnixMs The modification time, in milliseconds since epoch.
* @param timestampMs The modification time UTC in milliseconds since the Unix epoch.
*/
public void setModificationTime(long modificationDateUnixMs) {
metadataCollector.setModificationTime(modificationDateUnixMs);
public void setModificationTime(long timestampMs) {
metadataCollector.setModificationTime(timestampMs);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ public void createMp4File_addTrackAndMetadataButNoSamples_createsEmptyFile() thr
public void createMp4File_withSameTracksOffset_matchesExpected() throws IOException {
Context context = ApplicationProvider.getApplicationContext();
Mp4Muxer mp4Muxer = new Mp4Muxer.Builder(outputFileStream).build();
mp4Muxer.setModificationTime(/* timestampMs= */ 500_000_000L);

Pair<ByteBuffer, BufferInfo> track1Sample1 =
MuxerTestUtil.getFakeSampleAndSampleInfo(/* presentationTimeUs= */ 100L);
Expand Down Expand Up @@ -115,6 +116,7 @@ public void createMp4File_withSameTracksOffset_matchesExpected() throws IOExcept
public void createMp4File_withDifferentTracksOffset_matchesExpected() throws IOException {
Context context = ApplicationProvider.getApplicationContext();
Mp4Muxer mp4Muxer = new Mp4Muxer.Builder(outputFileStream).build();
mp4Muxer.setModificationTime(/* timestampMs= */ 500_000_000L);

Pair<ByteBuffer, BufferInfo> track1Sample1 =
MuxerTestUtil.getFakeSampleAndSampleInfo(/* presentationTimeUs= */ 0L);
Expand Down
Loading

0 comments on commit 7e14811

Please sign in to comment.