Make MetadataRenderer configurable to output metadata early.

PiperOrigin-RevId: 457974611
This commit is contained in:
rohks 2022-06-29 14:47:12 +00:00 committed by Marc Baechinger
parent 87beb273e4
commit 621617f981
6 changed files with 218 additions and 8 deletions

View File

@ -12,6 +12,12 @@
([#2092](https://github.com/google/ExoPlayer/issues/2092)). ([#2092](https://github.com/google/ExoPlayer/issues/2092)).
* Fix parsing of H265 short term reference picture sets * Fix parsing of H265 short term reference picture sets
([#10316](https://github.com/google/ExoPlayer/issues/10316)). ([#10316](https://github.com/google/ExoPlayer/issues/10316)).
* Metadata:
* `MetadataRenderer` can now be configured to render metadata as soon as
they are available. Create an instance with
`MetadataRenderer(MetadataOutput, Looper,
MetadataDecoderFactory, boolean)` to specify whether the renderer will
output metadata early or in sync with the player position.
* RTSP: * RTSP:
* Add RTP reader for H263 * Add RTP reader for H263
([#63](https://github.com/androidx/media/pull/63)). ([#63](https://github.com/androidx/media/pull/63)).

View File

@ -39,7 +39,12 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.checkerframework.dataflow.qual.SideEffectFree; 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)}.
*/
@UnstableApi @UnstableApi
public final class MetadataRenderer extends BaseRenderer implements Callback { public final class MetadataRenderer extends BaseRenderer implements Callback {
@ -50,6 +55,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
private final MetadataOutput output; private final MetadataOutput output;
@Nullable private final Handler outputHandler; @Nullable private final Handler outputHandler;
private final MetadataInputBuffer buffer; private final MetadataInputBuffer buffer;
private final boolean outputMetadataEarly;
@Nullable private MetadataDecoder decoder; @Nullable private MetadataDecoder decoder;
private boolean inputStreamEnded; private boolean inputStreamEnded;
@ -59,6 +65,9 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
private long outputStreamOffsetUs; private long outputStreamOffsetUs;
/** /**
* Creates an instance that uses {@link MetadataDecoderFactory#DEFAULT} to create {@link
* MetadataDecoder} instances.
*
* @param output The output. * @param output The output.
* @param outputLooper The looper associated with the thread on which the output should be called. * @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 * If the output makes use of standard Android UI components, then this should normally be the
@ -71,6 +80,8 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
} }
/** /**
* Creates an instance.
*
* @param output The output. * @param output The output.
* @param outputLooper The looper associated with the thread on which the output should be called. * @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 * If the output makes use of standard Android UI components, then this should normally be the
@ -81,11 +92,34 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
*/ */
public MetadataRenderer( public MetadataRenderer(
MetadataOutput output, @Nullable Looper outputLooper, MetadataDecoderFactory decoderFactory) { 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); super(C.TRACK_TYPE_METADATA);
this.output = Assertions.checkNotNull(output); this.output = Assertions.checkNotNull(output);
this.outputHandler = this.outputHandler =
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);
this.outputMetadataEarly = outputMetadataEarly;
buffer = new MetadataInputBuffer(); buffer = new MetadataInputBuffer();
outputStreamOffsetUs = C.TIME_UNSET; outputStreamOffsetUs = C.TIME_UNSET;
} }
@ -217,7 +251,8 @@ 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 if (pendingMetadata != null
&& pendingMetadata.presentationTimeUs <= getPresentationTimeUs(positionUs)) { && (outputMetadataEarly
|| pendingMetadata.presentationTimeUs <= getPresentationTimeUs(positionUs))) {
invokeRenderer(pendingMetadata); invokeRenderer(pendingMetadata);
pendingMetadata = null; pendingMetadata = null;
didOutput = true; didOutput = true;

View File

@ -145,16 +145,87 @@ public class MetadataRendererTest {
assertThat(metadata).isEmpty(); 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 { private static List<Metadata> runRenderer(byte[] input) throws ExoPlaybackException {
List<Metadata> metadata = new ArrayList<>(); List<Metadata> metadata = new ArrayList<>();
MetadataRenderer renderer = new MetadataRenderer(metadata::add, /* outputLooper= */ null); MetadataRenderer renderer = new MetadataRenderer(metadata::add, /* outputLooper= */ null);
FakeSampleStream fakeSampleStream = FakeSampleStream fakeSampleStream =
new FakeSampleStream( createFakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
EMSG_FORMAT,
ImmutableList.of( ImmutableList.of(
sample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME, input), END_OF_STREAM_ITEM)); sample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME, input), END_OF_STREAM_ITEM));
fakeSampleStream.writeData(/* startPositionUs= */ 0); fakeSampleStream.writeData(/* startPositionUs= */ 0);
@ -169,6 +240,17 @@ public class MetadataRendererTest {
return Collections.unmodifiableList(metadata); 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 * Builds an ID3v2 tag containing a single 'user defined text information frame' (id='TXXX') with
* {@code description} and {@code value}. * {@code description} and {@code value}.

View File

@ -23,6 +23,10 @@ import android.view.Surface;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.Renderer;
import androidx.media3.exoplayer.RenderersFactory;
import androidx.media3.exoplayer.metadata.MetadataDecoderFactory;
import androidx.media3.exoplayer.metadata.MetadataRenderer;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
import androidx.media3.test.utils.CapturingRenderersFactory; import androidx.media3.test.utils.CapturingRenderersFactory;
import androidx.media3.test.utils.DumpFileAsserts; import androidx.media3.test.utils.DumpFileAsserts;
@ -96,4 +100,70 @@ public final class DashPlaybackTest {
DumpFileAsserts.assertOutput( DumpFileAsserts.assertOutput(
applicationContext, playbackOutput, "playbackdumps/dash/emsg.dump"); 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");
}
} }

View File

@ -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

View File

@ -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