Skip to content

Commit f1d285e

Browse files
SheenaChhabratof-tof
authored andcommitted
Add support for passing creation time via InAppMuxer
PiperOrigin-RevId: 538175466 (cherry picked from commit 7e14811)
1 parent d4e0a8d commit f1d285e

File tree

118 files changed

+711
-109
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

118 files changed

+711
-109
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright 2023 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package androidx.media3.container;
17+
18+
import android.os.Parcel;
19+
import android.os.Parcelable;
20+
import androidx.annotation.Nullable;
21+
import androidx.media3.common.Metadata;
22+
import androidx.media3.common.util.UnstableApi;
23+
import com.google.common.primitives.Longs;
24+
25+
/** Stores creation time. */
26+
@UnstableApi
27+
public final class CreationTime implements Metadata.Entry {
28+
public final long timestampMs;
29+
30+
/**
31+
* Creates an instance.
32+
*
33+
* @param timestampMs The creation time UTC in milliseconds since the Unix epoch.
34+
*/
35+
public CreationTime(long timestampMs) {
36+
this.timestampMs = timestampMs;
37+
}
38+
39+
private CreationTime(Parcel in) {
40+
this.timestampMs = in.readLong();
41+
}
42+
43+
@Override
44+
public boolean equals(@Nullable Object obj) {
45+
if (this == obj) {
46+
return true;
47+
}
48+
if (!(obj instanceof CreationTime)) {
49+
return false;
50+
}
51+
52+
return timestampMs == ((CreationTime) obj).timestampMs;
53+
}
54+
55+
@Override
56+
public int hashCode() {
57+
return Longs.hashCode(timestampMs);
58+
}
59+
60+
@Override
61+
public String toString() {
62+
long unsetCreationTime = -2_082_844_800_000L;
63+
return "Creation time: " + (timestampMs == unsetCreationTime ? "unset" : timestampMs);
64+
}
65+
66+
// Parcelable implementation.
67+
68+
@Override
69+
public int describeContents() {
70+
return 0;
71+
}
72+
73+
@Override
74+
public void writeToParcel(Parcel dest, int flags) {
75+
dest.writeLong(timestampMs);
76+
}
77+
78+
public static final Parcelable.Creator<CreationTime> CREATOR =
79+
new Parcelable.Creator<CreationTime>() {
80+
81+
@Override
82+
public CreationTime createFromParcel(Parcel in) {
83+
return new CreationTime(in);
84+
}
85+
86+
@Override
87+
public CreationTime[] newArray(int size) {
88+
return new CreationTime[size];
89+
}
90+
};
91+
}

libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MetadataRetrieverTest.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import androidx.media3.common.C;
2727
import androidx.media3.common.MediaItem;
2828
import androidx.media3.common.MimeTypes;
29+
import androidx.media3.container.CreationTime;
2930
import androidx.media3.container.MdtaMetadataEntry;
3031
import androidx.media3.exoplayer.source.TrackGroupArray;
3132
import androidx.media3.extractor.metadata.mp4.MotionPhotoMetadata;
@@ -162,6 +163,7 @@ public void retrieveMetadata_sefSlowMotion_outputsExpectedMetadata() throws Exce
162163
new SlowMotionData.Segment(
163164
/* startTimeMs= */ 1255, /* endTimeMs= */ 1970, /* speedDivisor= */ 8));
164165
SlowMotionData expectedSlowMotionData = new SlowMotionData(segments);
166+
CreationTime expectedCreationTime = new CreationTime(/* timestampMs= */ 1604060090000L);
165167
MdtaMetadataEntry expectedMdtaEntry =
166168
new MdtaMetadataEntry(
167169
KEY_ANDROID_CAPTURE_FPS,
@@ -176,14 +178,17 @@ public void retrieveMetadata_sefSlowMotion_outputsExpectedMetadata() throws Exce
176178

177179
assertThat(trackGroups.length).isEqualTo(2); // Video and audio
178180
// Audio
179-
assertThat(trackGroups.get(0).getFormat(0).metadata.length()).isEqualTo(2);
181+
assertThat(trackGroups.get(0).getFormat(0).metadata.length()).isEqualTo(3);
180182
assertThat(trackGroups.get(0).getFormat(0).metadata.get(0)).isEqualTo(expectedSmtaEntry);
181183
assertThat(trackGroups.get(0).getFormat(0).metadata.get(1)).isEqualTo(expectedSlowMotionData);
184+
assertThat(trackGroups.get(0).getFormat(0).metadata.get(2)).isEqualTo(expectedCreationTime);
185+
182186
// Video
183-
assertThat(trackGroups.get(1).getFormat(0).metadata.length()).isEqualTo(3);
187+
assertThat(trackGroups.get(1).getFormat(0).metadata.length()).isEqualTo(4);
184188
assertThat(trackGroups.get(1).getFormat(0).metadata.get(0)).isEqualTo(expectedMdtaEntry);
185189
assertThat(trackGroups.get(1).getFormat(0).metadata.get(1)).isEqualTo(expectedSmtaEntry);
186190
assertThat(trackGroups.get(1).getFormat(0).metadata.get(2)).isEqualTo(expectedSlowMotionData);
191+
assertThat(trackGroups.get(1).getFormat(0).metadata.get(3)).isEqualTo(expectedCreationTime);
187192
}
188193

189194
@Test

libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import androidx.media3.common.util.Log;
3434
import androidx.media3.common.util.ParsableByteArray;
3535
import androidx.media3.common.util.Util;
36+
import androidx.media3.container.CreationTime;
3637
import androidx.media3.container.Mp4LocationData;
3738
import androidx.media3.extractor.AacUtil;
3839
import androidx.media3.extractor.Ac3Util;
@@ -79,6 +80,19 @@ public UdtaInfo(
7980
}
8081
}
8182

83+
/** Stores data retrieved from the mvhd atom. */
84+
public static final class MvhdInfo {
85+
/** The metadata. */
86+
public final Metadata metadata;
87+
/** The movie timescale. */
88+
public final long timescale;
89+
90+
public MvhdInfo(Metadata metadata, long timescale) {
91+
this.metadata = metadata;
92+
this.timescale = timescale;
93+
}
94+
}
95+
8296
private static final String TAG = "AtomParsers";
8397

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

222+
/**
223+
* Parses a mvhd atom (defined in ISO/IEC 14496-12), returning the timescale for the movie.
224+
*
225+
* @param mvhd Contents of the mvhd atom to be parsed.
226+
* @return An object containing the parsed data.
227+
*/
228+
public static MvhdInfo parseMvhd(ParsableByteArray mvhd) {
229+
mvhd.setPosition(Atom.HEADER_SIZE);
230+
int fullAtom = mvhd.readInt();
231+
int version = Atom.parseFullAtomVersion(fullAtom);
232+
long creationTimestampSeconds;
233+
if (version == 0) {
234+
creationTimestampSeconds = mvhd.readUnsignedInt();
235+
mvhd.skipBytes(4); // modification_time
236+
} else {
237+
creationTimestampSeconds = mvhd.readLong();
238+
mvhd.skipBytes(8); // modification_time
239+
}
240+
241+
// Convert creation time from MP4 format to Unix epoch timestamp in Ms.
242+
// Time delta between January 1, 1904 (MP4 format) and January 1, 1970 (Unix epoch).
243+
// Includes leap year.
244+
int timeDeltaSeconds = (66 * 365 + 17) * (24 * 60 * 60);
245+
long unixTimestampMs = (creationTimestampSeconds - timeDeltaSeconds) * 1000;
246+
247+
long timescale = mvhd.readUnsignedInt();
248+
return new MvhdInfo(new Metadata(new CreationTime(unixTimestampMs)), timescale);
249+
}
250+
208251
/**
209252
* Parses a metadata meta atom if it contains metadata with handler 'mdta'.
210253
*
@@ -318,7 +361,7 @@ private static Track parseTrak(
318361
if (duration == C.TIME_UNSET) {
319362
duration = tkhdData.duration;
320363
}
321-
long movieTimescale = parseMvhd(mvhd.data);
364+
long movieTimescale = parseMvhd(mvhd.data).timescale;
322365
long durationUs;
323366
if (duration == C.TIME_UNSET) {
324367
durationUs = C.TIME_UNSET;
@@ -835,23 +878,10 @@ private static Metadata parseSmta(ParsableByteArray smta, int limit) {
835878
return null;
836879
}
837880

838-
/**
839-
* Parses a mvhd atom (defined in ISO/IEC 14496-12), returning the timescale for the movie.
840-
*
841-
* @param mvhd Contents of the mvhd atom to be parsed.
842-
* @return Timescale for the movie.
843-
*/
844-
private static long parseMvhd(ParsableByteArray mvhd) {
845-
mvhd.setPosition(Atom.HEADER_SIZE);
846-
int fullAtom = mvhd.readInt();
847-
int version = Atom.parseFullAtomVersion(fullAtom);
848-
mvhd.skipBytes(version == 0 ? 8 : 16);
849-
return mvhd.readUnsignedInt();
850-
}
851-
852881
/**
853882
* Parses a tkhd atom (defined in ISO/IEC 14496-12).
854883
*
884+
* @param tkhd Contents of the tkhd atom to be parsed.
855885
* @return An object containing the parsed data.
856886
*/
857887
private static TkhdData parseTkhd(ParsableByteArray tkhd) {

libraries/extractor/src/main/java/androidx/media3/extractor/mp4/Mp4Extractor.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package androidx.media3.extractor.mp4;
1717

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

515+
Metadata mvhdMetadata =
516+
AtomParsers.parseMvhd(checkNotNull(moov.getLeafAtomOfType(Atom.TYPE_mvhd)).data).metadata;
517+
514518
boolean ignoreEditLists = (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0;
515519
List<TrackSampleTable> trackSampleTables =
516520
parseTraks(
@@ -562,7 +566,8 @@ private void processMoovAtom(ContainerAtom moov) throws ParserException {
562566
formatBuilder,
563567
smtaMetadata,
564568
slowMotionMetadataEntries.isEmpty() ? null : new Metadata(slowMotionMetadataEntries),
565-
xyzMetadata);
569+
xyzMetadata,
570+
mvhdMetadata);
566571
mp4Track.trackOutput.format(formatBuilder.build());
567572

568573
if (track.type == C.TRACK_TYPE_VIDEO && firstVideoTrackIndex == C.INDEX_UNSET) {

libraries/muxer/src/androidTest/java/androidx/media3/muxer/Mp4MuxerEndToEndTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public void createMp4File_fromInputFileSampleData_matchesExpected() throws IOExc
8181

8282
try {
8383
mp4Muxer = new Mp4Muxer.Builder(outputStream).build();
84+
mp4Muxer.setModificationTime(/* timestampMs= */ 500_000_000L);
8485
feedInputDataToMuxer(mp4Muxer, inputFile);
8586
} finally {
8687
if (mp4Muxer != null) {
@@ -97,6 +98,7 @@ public void createMp4File_fromInputFileSampleData_matchesExpected() throws IOExc
9798
@Test
9899
public void createMp4File_muxerNotClosed_createsPartiallyWrittenValidFile() throws IOException {
99100
Mp4Muxer mp4Muxer = new Mp4Muxer.Builder(outputStream).build();
101+
mp4Muxer.setModificationTime(/* timestampMs= */ 500_000_000L);
100102
feedInputDataToMuxer(mp4Muxer, H265_HDR10_MP4);
101103

102104
// Muxer not closed.

libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,11 +1101,14 @@ private static ByteBuffer audioEsdsBox(Format format) {
11011101
return BoxUtils.wrapIntoBox("esds", contents);
11021102
}
11031103

1104-
/** Convert UNIX timestamps to the format used by MP4 files. */
1104+
/** Convert Unix epoch timestamps to the format used by MP4 files. */
11051105
private static int toMp4Time(long unixTimeMs) {
1106-
// Jan 1, 1904, including leap years.
1107-
long delta = (66 * 365 + 17) * (24 * 60 * 60);
1108-
return (int) (unixTimeMs / 1000L + delta);
1106+
// Time delta between January 1, 1904 (MP4 format) and January 1, 1970 (Unix epoch).
1107+
// Includes leap year.
1108+
long timeDeltaSeconds = (66 * 365 + 17) * (24 * 60 * 60);
1109+
1110+
// The returned value is a positive (when read as unsigned) integer.
1111+
return (int) (unixTimeMs / 1000L + timeDeltaSeconds);
11091112
}
11101113

11111114
/** Packs a three-letter language code into a short, packing 3x5 bits. */

libraries/muxer/src/main/java/androidx/media3/muxer/Mp4Muxer.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,10 +191,10 @@ public void setCaptureFps(float captureFps) {
191191
/**
192192
* Sets the file modification time.
193193
*
194-
* @param modificationDateUnixMs The modification time, in milliseconds since epoch.
194+
* @param timestampMs The modification time UTC in milliseconds since the Unix epoch.
195195
*/
196-
public void setModificationTime(long modificationDateUnixMs) {
197-
metadataCollector.setModificationTime(modificationDateUnixMs);
196+
public void setModificationTime(long timestampMs) {
197+
metadataCollector.setModificationTime(timestampMs);
198198
}
199199

200200
/**

libraries/muxer/src/test/java/androidx/media3/muxer/Mp4MuxerEndToEndTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public void createMp4File_addTrackAndMetadataButNoSamples_createsEmptyFile() thr
7474
public void createMp4File_withSameTracksOffset_matchesExpected() throws IOException {
7575
Context context = ApplicationProvider.getApplicationContext();
7676
Mp4Muxer mp4Muxer = new Mp4Muxer.Builder(outputFileStream).build();
77+
mp4Muxer.setModificationTime(/* timestampMs= */ 500_000_000L);
7778

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

119121
Pair<ByteBuffer, BufferInfo> track1Sample1 =
120122
MuxerTestUtil.getFakeSampleAndSampleInfo(/* presentationTimeUs= */ 0L);

0 commit comments

Comments
 (0)