Add timestamp to Metadata

`MetadataRenderer` is updated to output `Metadata` with its presentation time, in microseconds.

PiperOrigin-RevId: 457444718
This commit is contained in:
rohks 2022-06-27 11:31:10 +00:00 committed by Marc Baechinger
parent 5c2752b4a9
commit b2831d8559
9 changed files with 73 additions and 15 deletions

View File

@ -20,6 +20,7 @@ import android.os.Parcelable;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import com.google.common.primitives.Longs;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -61,11 +62,28 @@ public final class Metadata implements Parcelable {
} }
private final Entry[] entries; private final Entry[] entries;
/**
* The presentation time of the metadata, in microseconds.
*
* <p>This time is an offset from the start of the current {@link Timeline.Period}.
*
* <p>This time is {@link C#TIME_UNSET} when not known or undefined.
*/
public final long presentationTimeUs;
/** /**
* @param entries The metadata entries. * @param entries The metadata entries.
*/ */
public Metadata(Entry... entries) { public Metadata(Entry... entries) {
this(/* presentationTimeUs= */ C.TIME_UNSET, entries);
}
/**
* @param presentationTimeUs The presentation time for the metadata entries.
* @param entries The metadata entries.
*/
public Metadata(long presentationTimeUs, Entry... entries) {
this.presentationTimeUs = presentationTimeUs;
this.entries = entries; this.entries = entries;
} }
@ -73,7 +91,15 @@ public final class Metadata implements Parcelable {
* @param entries The metadata entries. * @param entries The metadata entries.
*/ */
public Metadata(List<? extends Entry> entries) { public Metadata(List<? extends Entry> entries) {
this.entries = entries.toArray(new Entry[0]); this(entries.toArray(new Entry[0]));
}
/**
* @param presentationTimeUs The presentation time for the metadata entries.
* @param entries The metadata entries.
*/
public Metadata(long presentationTimeUs, List<? extends Entry> entries) {
this(presentationTimeUs, entries.toArray(new Entry[0]));
} }
/* package */ Metadata(Parcel in) { /* package */ Metadata(Parcel in) {
@ -81,6 +107,7 @@ public final class Metadata implements Parcelable {
for (int i = 0; i < entries.length; i++) { for (int i = 0; i < entries.length; i++) {
entries[i] = in.readParcelable(Entry.class.getClassLoader()); entries[i] = in.readParcelable(Entry.class.getClassLoader());
} }
presentationTimeUs = in.readLong();
} }
/** Returns the number of metadata entries. */ /** Returns the number of metadata entries. */
@ -123,7 +150,8 @@ public final class Metadata implements Parcelable {
if (entriesToAppend.length == 0) { if (entriesToAppend.length == 0) {
return this; return this;
} }
return new Metadata(Util.nullSafeArrayConcatenation(entries, entriesToAppend)); return new Metadata(
presentationTimeUs, Util.nullSafeArrayConcatenation(entries, entriesToAppend));
} }
@Override @Override
@ -135,17 +163,21 @@ public final class Metadata implements Parcelable {
return false; return false;
} }
Metadata other = (Metadata) obj; Metadata other = (Metadata) obj;
return Arrays.equals(entries, other.entries); return Arrays.equals(entries, other.entries) && presentationTimeUs == other.presentationTimeUs;
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Arrays.hashCode(entries); int result = Arrays.hashCode(entries);
result = 31 * result + Longs.hashCode(presentationTimeUs);
return result;
} }
@Override @Override
public String toString() { public String toString() {
return "entries=" + Arrays.toString(entries); return "entries="
+ Arrays.toString(entries)
+ (presentationTimeUs == C.TIME_UNSET ? "" : ", presentationTimeUs=" + presentationTimeUs);
} }
// Parcelable implementation. // Parcelable implementation.
@ -161,6 +193,7 @@ public final class Metadata implements Parcelable {
for (Entry entry : entries) { for (Entry entry : entries) {
dest.writeParcelable(entry, 0); dest.writeParcelable(entry, 0);
} }
dest.writeLong(presentationTimeUs);
} }
public static final Parcelable.Creator<Metadata> CREATOR = public static final Parcelable.Creator<Metadata> CREATOR =

View File

@ -47,7 +47,7 @@ public final class CueGroup implements Bundleable {
/** /**
* The presentation time of the {@link #cues}, in microseconds. * The presentation time of the {@link #cues}, in microseconds.
* *
* <p>This time is an offset from the start of the current {@link Timeline.Period} * <p>This time is an offset from the start of the current {@link Timeline.Period}.
*/ */
@UnstableApi public final long presentationTimeUs; @UnstableApi public final long presentationTimeUs;

View File

@ -30,7 +30,10 @@ public class MetadataTest {
@Test @Test
public void parcelable() { public void parcelable() {
Metadata metadataToParcel = Metadata metadataToParcel =
new Metadata(new FakeMetadataEntry("id1"), new FakeMetadataEntry("id2")); new Metadata(
/* presentationTimeUs= */ 1_230_000,
new FakeMetadataEntry("id1"),
new FakeMetadataEntry("id2"));
Parcel parcel = Parcel.obtain(); Parcel parcel = Parcel.obtain();
metadataToParcel.writeToParcel(parcel, 0); metadataToParcel.writeToParcel(parcel, 0);

View File

@ -15,6 +15,7 @@
*/ */
package androidx.media3.exoplayer.metadata; package androidx.media3.exoplayer.metadata;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.castNonNull; import static androidx.media3.common.util.Util.castNonNull;
import android.os.Handler; import android.os.Handler;
@ -36,6 +37,7 @@ import androidx.media3.extractor.metadata.MetadataDecoder;
import androidx.media3.extractor.metadata.MetadataInputBuffer; import androidx.media3.extractor.metadata.MetadataInputBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.checkerframework.dataflow.qual.SideEffectFree;
/** A renderer for metadata. */ /** A renderer for metadata. */
@UnstableApi @UnstableApi
@ -53,8 +55,8 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
private boolean inputStreamEnded; private boolean inputStreamEnded;
private boolean outputStreamEnded; private boolean outputStreamEnded;
private long subsampleOffsetUs; private long subsampleOffsetUs;
private long pendingMetadataTimestampUs;
@Nullable private Metadata pendingMetadata; @Nullable private Metadata pendingMetadata;
private long outputStreamOffsetUs;
/** /**
* @param output The output. * @param output The output.
@ -85,7 +87,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
outputLooper == null ? null : Util.createHandler(outputLooper, /* callback= */ this); outputLooper == null ? null : Util.createHandler(outputLooper, /* callback= */ this);
this.decoderFactory = Assertions.checkNotNull(decoderFactory); this.decoderFactory = Assertions.checkNotNull(decoderFactory);
buffer = new MetadataInputBuffer(); buffer = new MetadataInputBuffer();
pendingMetadataTimestampUs = C.TIME_UNSET; outputStreamOffsetUs = C.TIME_UNSET;
} }
@Override @Override
@ -106,12 +108,12 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
@Override @Override
protected void onStreamChanged(Format[] formats, long startPositionUs, long offsetUs) { protected void onStreamChanged(Format[] formats, long startPositionUs, long offsetUs) {
decoder = decoderFactory.createDecoder(formats[0]); decoder = decoderFactory.createDecoder(formats[0]);
outputStreamOffsetUs = offsetUs;
} }
@Override @Override
protected void onPositionReset(long positionUs, boolean joining) { protected void onPositionReset(long positionUs, boolean joining) {
pendingMetadata = null; pendingMetadata = null;
pendingMetadataTimestampUs = C.TIME_UNSET;
inputStreamEnded = false; inputStreamEnded = false;
outputStreamEnded = false; outputStreamEnded = false;
} }
@ -158,8 +160,8 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
@Override @Override
protected void onDisabled() { protected void onDisabled() {
pendingMetadata = null; pendingMetadata = null;
pendingMetadataTimestampUs = C.TIME_UNSET;
decoder = null; decoder = null;
outputStreamOffsetUs = C.TIME_UNSET;
} }
@Override @Override
@ -200,9 +202,9 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
List<Metadata.Entry> entries = new ArrayList<>(metadata.length()); List<Metadata.Entry> entries = new ArrayList<>(metadata.length());
decodeWrappedMetadata(metadata, entries); decodeWrappedMetadata(metadata, entries);
if (!entries.isEmpty()) { if (!entries.isEmpty()) {
Metadata expandedMetadata = new Metadata(entries); Metadata expandedMetadata =
new Metadata(getPresentationTimeUs(buffer.timeUs), entries);
pendingMetadata = expandedMetadata; pendingMetadata = expandedMetadata;
pendingMetadataTimestampUs = buffer.timeUs;
} }
} }
} }
@ -214,10 +216,10 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
private boolean outputMetadata(long positionUs) { private boolean outputMetadata(long positionUs) {
boolean didOutput = false; boolean didOutput = false;
if (pendingMetadata != null && pendingMetadataTimestampUs <= positionUs) { if (pendingMetadata != null
&& pendingMetadata.presentationTimeUs <= getPresentationTimeUs(positionUs)) {
invokeRenderer(pendingMetadata); invokeRenderer(pendingMetadata);
pendingMetadata = null; pendingMetadata = null;
pendingMetadataTimestampUs = C.TIME_UNSET;
didOutput = true; didOutput = true;
} }
if (inputStreamEnded && pendingMetadata == null) { if (inputStreamEnded && pendingMetadata == null) {
@ -237,4 +239,12 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
private void invokeRendererInternal(Metadata metadata) { private void invokeRendererInternal(Metadata metadata) {
output.onMetadata(metadata); output.onMetadata(metadata);
} }
@SideEffectFree
private long getPresentationTimeUs(long positionUs) {
checkState(positionUs != C.TIME_UNSET);
checkState(outputStreamOffsetUs != C.TIME_UNSET);
return positionUs - outputStreamOffsetUs;
}
} }

View File

@ -94,8 +94,11 @@ MediaCodecAdapter (exotest.audio.aac):
buffers[91] = length 0, hash 1 buffers[91] = length 0, hash 1
MetadataOutput: MetadataOutput:
Metadata[0]: Metadata[0]:
presentationTimeUs = 100000
entry[0] = EMSG: scheme=urn:mpeg:dash:event:callback:2015, id=0, durationMs=1000, value=1 entry[0] = EMSG: scheme=urn:mpeg:dash:event:callback:2015, id=0, durationMs=1000, value=1
Metadata[1]: Metadata[1]:
presentationTimeUs = 100000
entry[0] = EMSG: scheme=urn:mpeg:dash:event:callback:2015, id=1, durationMs=1000, value=1 entry[0] = EMSG: scheme=urn:mpeg:dash:event:callback:2015, id=1, durationMs=1000, value=1
Metadata[2]: Metadata[2]:
presentationTimeUs = 1000000
entry[0] = EMSG: scheme=urn:mpeg:dash:event:callback:2015, id=2, durationMs=1000, value=1 entry[0] = EMSG: scheme=urn:mpeg:dash:event:callback:2015, id=2, durationMs=1000, value=1

View File

@ -22,11 +22,14 @@ MediaCodecAdapter (exotest.audio.eac3):
buffers[19] = length 0, hash 1 buffers[19] = length 0, hash 1
MetadataOutput: MetadataOutput:
Metadata[0]: Metadata[0]:
presentationTimeUs = 0
entry[0] = Ait(controlCode=1,url=http://static-cdn.arte.tv/redbutton/index_fr.html) entry[0] = Ait(controlCode=1,url=http://static-cdn.arte.tv/redbutton/index_fr.html)
entry[1] = Ait(controlCode=2,url=http://www.arte.tv/hbbtvv2/index.html?lang=fr_FR&page=PLUS7) entry[1] = Ait(controlCode=2,url=http://www.arte.tv/hbbtvv2/index.html?lang=fr_FR&page=PLUS7)
Metadata[1]: Metadata[1]:
presentationTimeUs = 192000
entry[0] = Ait(controlCode=1,url=http://static-cdn.arte.tv/redbutton/index_fr.html) entry[0] = Ait(controlCode=1,url=http://static-cdn.arte.tv/redbutton/index_fr.html)
entry[1] = Ait(controlCode=2,url=http://www.arte.tv/hbbtvv2/index.html?lang=fr_FR&page=PLUS7) entry[1] = Ait(controlCode=2,url=http://www.arte.tv/hbbtvv2/index.html?lang=fr_FR&page=PLUS7)
Metadata[2]: Metadata[2]:
presentationTimeUs = 384000
entry[0] = Ait(controlCode=1,url=http://static-cdn.arte.tv/redbutton/index_fr.html) entry[0] = Ait(controlCode=1,url=http://static-cdn.arte.tv/redbutton/index_fr.html)
entry[1] = Ait(controlCode=2,url=http://www.arte.tv/hbbtvv2/index.html?lang=fr_FR&page=PLUS7) entry[1] = Ait(controlCode=2,url=http://www.arte.tv/hbbtvv2/index.html?lang=fr_FR&page=PLUS7)

View File

@ -12,8 +12,11 @@ MediaCodecAdapter (exotest.video.mpeg2):
buffers[2] = length 0, hash 1 buffers[2] = length 0, hash 1
MetadataOutput: MetadataOutput:
Metadata[0]: Metadata[0]:
presentationTimeUs = 33366
entry[0] = SCTE-35 splice command: type=SpliceInsertCommand entry[0] = SCTE-35 splice command: type=SpliceInsertCommand
Metadata[1]: Metadata[1]:
presentationTimeUs = 33366
entry[0] = SCTE-35 splice command: type=SpliceInsertCommand entry[0] = SCTE-35 splice command: type=SpliceInsertCommand
Metadata[2]: Metadata[2]:
presentationTimeUs = 33366
entry[0] = SCTE-35 splice command: type=SpliceInsertCommand entry[0] = SCTE-35 splice command: type=SpliceInsertCommand

View File

@ -147,7 +147,9 @@ MediaCodecAdapter (exotest.audio.aac):
buffers[144] = length 0, hash 1 buffers[144] = length 0, hash 1
MetadataOutput: MetadataOutput:
Metadata[0]: Metadata[0]:
presentationTimeUs = 0
entry[0] = APIC: mimeType=image/jpeg, description=Hello World entry[0] = APIC: mimeType=image/jpeg, description=Hello World
Metadata[1]: Metadata[1]:
presentationTimeUs = 23219
entry[0] = COMM: language=eng, description=description entry[0] = COMM: language=eng, description=description
entry[1] = APIC: mimeType=image/jpeg, description=Hello World entry[1] = APIC: mimeType=image/jpeg, description=Hello World

View File

@ -122,6 +122,7 @@ public final class PlaybackOutput implements Dumper.Dumpable {
for (int i = 0; i < metadatas.size(); i++) { for (int i = 0; i < metadatas.size(); i++) {
dumper.startBlock("Metadata[" + i + "]"); dumper.startBlock("Metadata[" + i + "]");
Metadata metadata = metadatas.get(i); Metadata metadata = metadatas.get(i);
dumper.add("presentationTimeUs", metadata.presentationTimeUs);
for (int j = 0; j < metadata.length(); j++) { for (int j = 0; j < metadata.length(); j++) {
dumper.add("entry[" + j + "]", getEntryAsString(metadata.get(j))); dumper.add("entry[" + j + "]", getEntryAsString(metadata.get(j)));
} }