Skip to content

Commit

Permalink
Make MetadataRenderer configurable to output metadata early.
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 457974611
  • Loading branch information
rohitjoins committed Jul 7, 2022
1 parent c74cf1f commit 14f75c1
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 8 deletions.
Expand Up @@ -35,7 +35,12 @@
import java.util.List;
import org.checkerframework.dataflow.qual.SideEffectFree;

/** A renderer for metadata. */
/**
* A renderer for metadata.
*
* <p>The renderer can be configured to render metadata as soon as they are available using {@link
* #MetadataRenderer(MetadataOutput, Looper, MetadataDecoderFactory, boolean)}.
*/
public final class MetadataRenderer extends BaseRenderer implements Callback {

private static final String TAG = "MetadataRenderer";
Expand All @@ -45,6 +50,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
private final MetadataOutput output;
@Nullable private final Handler outputHandler;
private final MetadataInputBuffer buffer;
private final boolean outputMetadataEarly;

@Nullable private MetadataDecoder decoder;
private boolean inputStreamEnded;
Expand All @@ -54,6 +60,9 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
private long outputStreamOffsetUs;

/**
* Creates an instance that uses {@link MetadataDecoderFactory#DEFAULT} to create {@link
* MetadataDecoder} instances.
*
* @param output The output.
* @param outputLooper The looper associated with the thread on which the output should be called.
* If the output makes use of standard Android UI components, then this should normally be the
Expand All @@ -66,6 +75,8 @@ public MetadataRenderer(MetadataOutput output, @Nullable Looper outputLooper) {
}

/**
* Creates an instance.
*
* @param output The output.
* @param outputLooper The looper associated with the thread on which the output should be called.
* If the output makes use of standard Android UI components, then this should normally be the
Expand All @@ -76,11 +87,34 @@ public MetadataRenderer(MetadataOutput output, @Nullable Looper outputLooper) {
*/
public MetadataRenderer(
MetadataOutput output, @Nullable Looper outputLooper, MetadataDecoderFactory decoderFactory) {
this(output, outputLooper, decoderFactory, /* outputMetadataEarly= */ false);
}

/**
* Creates an instance.
*
* @param output The output.
* @param outputLooper The looper associated with the thread on which the output should be called.
* If the output makes use of standard Android UI components, then this should normally be the
* looper associated with the application's main thread, which can be obtained using {@link
* android.app.Activity#getMainLooper()}. Null may be passed if the output should be called
* directly on the player's internal rendering thread.
* @param decoderFactory A factory from which to obtain {@link MetadataDecoder} instances.
* @param outputMetadataEarly Whether the renderer outputs metadata early. When {@code true},
* {@link #render} will output metadata as soon as they are available to the renderer,
* otherwise {@link #render} will output metadata in sync with the rendering position.
*/
public MetadataRenderer(
MetadataOutput output,
@Nullable Looper outputLooper,
MetadataDecoderFactory decoderFactory,
boolean outputMetadataEarly) {
super(C.TRACK_TYPE_METADATA);
this.output = Assertions.checkNotNull(output);
this.outputHandler =
outputLooper == null ? null : Util.createHandler(outputLooper, /* callback= */ this);
this.decoderFactory = Assertions.checkNotNull(decoderFactory);
this.outputMetadataEarly = outputMetadataEarly;
buffer = new MetadataInputBuffer();
outputStreamOffsetUs = C.TIME_UNSET;
}
Expand Down Expand Up @@ -212,7 +246,8 @@ private void readMetadata() {
private boolean outputMetadata(long positionUs) {
boolean didOutput = false;
if (pendingMetadata != null
&& pendingMetadata.presentationTimeUs <= getPresentationTimeUs(positionUs)) {
&& (outputMetadataEarly
|| pendingMetadata.presentationTimeUs <= getPresentationTimeUs(positionUs))) {
invokeRenderer(pendingMetadata);
pendingMetadata = null;
didOutput = true;
Expand Down
Expand Up @@ -144,16 +144,87 @@ public void decodeMetadata_skipsMalformedWrappedMetadata() throws Exception {
assertThat(metadata).isEmpty();
}

@Test
public void renderMetadata_withTimelyOutput() throws Exception {
EventMessage emsg =
new EventMessage(
"urn:test-scheme-id",
/* value= */ "",
/* durationMs= */ 1,
/* id= */ 0,
"Test data".getBytes(UTF_8));
byte[] encodedEmsg = eventMessageEncoder.encode(emsg);
List<Metadata> metadata = new ArrayList<>();
MetadataRenderer renderer =
new MetadataRenderer(/* output= */ metadata::add, /* outputLooper= */ null);
FakeSampleStream fakeSampleStream =
createFakeSampleStream(
ImmutableList.of(
sample(/* timeUs= */ 100_000, C.BUFFER_FLAG_KEY_FRAME, encodedEmsg),
sample(/* timeUs= */ 1_000_000, C.BUFFER_FLAG_KEY_FRAME, encodedEmsg),
END_OF_STREAM_ITEM));
fakeSampleStream.writeData(/* startPositionUs= */ 0);
renderer.replaceStream(
new Format[] {EMSG_FORMAT},
fakeSampleStream,
/* startPositionUs= */ 0L,
/* offsetUs= */ 0L);

// Call render() twice, the first call is to read the format and the second call will read the
// metadata.
renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0);
renderer.render(/* positionUs= */ 500_000, /* elapsedRealtimeUs= */ 0);

assertThat(metadata).hasSize(1);
assertThat(metadata.get(0).presentationTimeUs).isEqualTo(100_000);
}

@Test
public void renderMetadata_withEarlyOutput() throws Exception {
EventMessage emsg =
new EventMessage(
"urn:test-scheme-id",
/* value= */ "",
/* durationMs= */ 1,
/* id= */ 0,
"Test data".getBytes(UTF_8));
byte[] encodedEmsg = eventMessageEncoder.encode(emsg);
List<Metadata> metadata = new ArrayList<>();
MetadataRenderer renderer =
new MetadataRenderer(
/* output= */ metadata::add,
/* outputLooper= */ null,
MetadataDecoderFactory.DEFAULT,
/* outputMetadataEarly= */ true);
FakeSampleStream fakeSampleStream =
createFakeSampleStream(
ImmutableList.of(
sample(/* timeUs= */ 100_000, C.BUFFER_FLAG_KEY_FRAME, encodedEmsg),
sample(/* timeUs= */ 1_000_000, C.BUFFER_FLAG_KEY_FRAME, encodedEmsg),
END_OF_STREAM_ITEM));
fakeSampleStream.writeData(/* startPositionUs= */ 0);
renderer.replaceStream(
new Format[] {EMSG_FORMAT},
fakeSampleStream,
/* startPositionUs= */ 0L,
/* offsetUs= */ 0L);

// Call render() twice, the first call is to read the format and the second call will read the
// metadata.
renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0);
renderer.render(/* positionUs= */ 500_000, /* elapsedRealtimeUs= */ 0);

// The renderer outputs metadata early.
assertThat(metadata).hasSize(2);
assertThat(metadata.get(0).presentationTimeUs).isEqualTo(100_000);
assertThat(metadata.get(1).presentationTimeUs).isEqualTo(1_000_000);
}

private static List<Metadata> runRenderer(byte[] input) throws ExoPlaybackException {
List<Metadata> metadata = new ArrayList<>();
MetadataRenderer renderer = new MetadataRenderer(metadata::add, /* outputLooper= */ null);
FakeSampleStream fakeSampleStream =
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
EMSG_FORMAT,
createFakeSampleStream(
ImmutableList.of(
sample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME, input), END_OF_STREAM_ITEM));
fakeSampleStream.writeData(/* startPositionUs= */ 0);
Expand All @@ -168,6 +239,17 @@ private static List<Metadata> runRenderer(byte[] input) throws ExoPlaybackExcept
return Collections.unmodifiableList(metadata);
}

private static FakeSampleStream createFakeSampleStream(
ImmutableList<FakeSampleStream.FakeSampleStreamItem> samples) {
return new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
EMSG_FORMAT,
samples);
}

/**
* Builds an ID3v2 tag containing a single 'user defined text information frame' (id='TXXX') with
* {@code description} and {@code value}.
Expand Down
Expand Up @@ -25,6 +25,10 @@
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.metadata.MetadataDecoderFactory;
import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.robolectric.PlaybackOutput;
import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig;
import com.google.android.exoplayer2.robolectric.TestPlayerRunHelper;
Expand Down Expand Up @@ -96,4 +100,70 @@ public void emsgNearToPeriodBoundary() throws Exception {
DumpFileAsserts.assertOutput(
applicationContext, playbackOutput, "playbackdumps/dash/emsg.dump");
}

@Test
public void renderMetadata_withTimelyOutput() throws Exception {
Context applicationContext = ApplicationProvider.getApplicationContext();
RenderersFactory renderersFactory =
(eventHandler,
videoRendererEventListener,
audioRendererEventListener,
textRendererOutput,
metadataRendererOutput) ->
new Renderer[] {new MetadataRenderer(metadataRendererOutput, eventHandler.getLooper())};
ExoPlayer player =
new ExoPlayer.Builder(applicationContext, renderersFactory)
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build();
player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1)));
CapturingRenderersFactory capturingRenderersFactory =
new CapturingRenderersFactory(applicationContext);
PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory);

player.setMediaItem(MediaItem.fromUri("asset:///media/dash/emsg/sample.mpd"));
player.prepare();
player.play();
TestPlayerRunHelper.playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ 500);
player.release();

// Ensure output contains metadata up to the playback position.
DumpFileAsserts.assertOutput(
applicationContext, playbackOutput, "playbackdumps/dash/metadata_from_timely_output.dump");
}

@Test
public void renderMetadata_withEarlyOutput() throws Exception {
Context applicationContext = ApplicationProvider.getApplicationContext();
RenderersFactory renderersFactory =
(eventHandler,
videoRendererEventListener,
audioRendererEventListener,
textRendererOutput,
metadataRendererOutput) ->
new Renderer[] {
new MetadataRenderer(
metadataRendererOutput,
eventHandler.getLooper(),
MetadataDecoderFactory.DEFAULT,
/* outputMetadataEarly= */ true)
};
ExoPlayer player =
new ExoPlayer.Builder(applicationContext, renderersFactory)
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build();
player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1)));
CapturingRenderersFactory capturingRenderersFactory =
new CapturingRenderersFactory(applicationContext);
PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory);

player.setMediaItem(MediaItem.fromUri("asset:///media/dash/emsg/sample.mpd"));
player.prepare();
player.play();
TestPlayerRunHelper.playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ 500);
player.release();

// Ensure output contains all metadata irrespective of the playback position.
DumpFileAsserts.assertOutput(
applicationContext, playbackOutput, "playbackdumps/dash/metadata_from_early_output.dump");
}
}
@@ -0,0 +1,10 @@
MetadataOutput:
Metadata[0]:
presentationTimeUs = 100000
entry[0] = EMSG: scheme=urn:mpeg:dash:event:callback:2015, id=0, durationMs=1000, value=1
Metadata[1]:
presentationTimeUs = 100000
entry[0] = EMSG: scheme=urn:mpeg:dash:event:callback:2015, id=1, durationMs=1000, value=1
Metadata[2]:
presentationTimeUs = 1000000
entry[0] = EMSG: scheme=urn:mpeg:dash:event:callback:2015, id=2, durationMs=1000, value=1
@@ -0,0 +1,7 @@
MetadataOutput:
Metadata[0]:
presentationTimeUs = 100000
entry[0] = EMSG: scheme=urn:mpeg:dash:event:callback:2015, id=0, durationMs=1000, value=1
Metadata[1]:
presentationTimeUs = 100000
entry[0] = EMSG: scheme=urn:mpeg:dash:event:callback:2015, id=1, durationMs=1000, value=1

0 comments on commit 14f75c1

Please sign in to comment.