public final class

MetadataRenderer

extends BaseRenderer

 java.lang.Object

androidx.media3.exoplayer.BaseRenderer

↳androidx.media3.exoplayer.metadata.MetadataRenderer

Gradle dependencies

compile group: 'androidx.media3', name: 'media3-exoplayer', version: '1.5.0-alpha01'

  • groupId: androidx.media3
  • artifactId: media3-exoplayer
  • version: 1.5.0-alpha01

Artifact androidx.media3:media3-exoplayer:1.5.0-alpha01 it located at Google repository (https://maven.google.com/)

Overview

A renderer for metadata.

The renderer can be configured to render metadata as soon as they are available using MetadataRenderer.MetadataRenderer(MetadataOutput, Looper, MetadataDecoderFactory, boolean).

Summary

Constructors
publicMetadataRenderer(MetadataOutput output, Looper outputLooper)

Creates an instance that uses MetadataDecoderFactory.DEFAULT to create MetadataDecoder instances.

publicMetadataRenderer(MetadataOutput output, Looper outputLooper, MetadataDecoderFactory decoderFactory)

Creates an instance.

publicMetadataRenderer(MetadataOutput output, Looper outputLooper, MetadataDecoderFactory decoderFactory, boolean outputMetadataEarly)

Creates an instance.

Methods
public java.lang.StringgetName()

public booleanhandleMessage(Message msg)

public booleanisEnded()

public booleanisReady()

protected voidonDisabled()

Called when the renderer is disabled.

protected voidonPositionReset(long positionUs, boolean joining)

Called when the position is reset.

protected voidonStreamChanged(Format formats[], long startPositionUs, long offsetUs, MediaSource.MediaPeriodId mediaPeriodId)

Called when the renderer's stream has changed.

public voidrender(long positionUs, long elapsedRealtimeUs)

public intsupportsFormat(Format format)

from BaseRendererclearListener, createRendererException, createRendererException, disable, enable, getCapabilities, getClock, getConfiguration, getFormatHolder, getIndex, getLastResetPositionUs, getMediaClock, getPlayerId, getReadingPositionUs, getState, getStream, getStreamFormats, getStreamOffsetUs, getTimeline, getTrackType, handleMessage, hasReadStreamToEnd, init, isCurrentStreamFinal, isSourceReady, maybeThrowStreamError, onEnabled, onInit, onRelease, onRendererCapabilitiesChanged, onReset, onStarted, onStopped, onTimelineChanged, readSource, release, replaceStream, reset, resetPosition, setCurrentStreamFinal, setListener, setTimeline, skipSource, start, stop, supportsMixedMimeTypeAdaptation
from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public MetadataRenderer(MetadataOutput output, Looper outputLooper)

Creates an instance that uses MetadataDecoderFactory.DEFAULT to create MetadataDecoder instances.

Parameters:

output: The output.
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 . Null may be passed if the output should be called directly on the player's internal rendering thread.

public MetadataRenderer(MetadataOutput output, Looper outputLooper, MetadataDecoderFactory decoderFactory)

Creates an instance.

Parameters:

output: The output.
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 . Null may be passed if the output should be called directly on the player's internal rendering thread.
decoderFactory: A factory from which to obtain MetadataDecoder instances.

public MetadataRenderer(MetadataOutput output, Looper outputLooper, MetadataDecoderFactory decoderFactory, boolean outputMetadataEarly)

Creates an instance.

Parameters:

output: The output.
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 . Null may be passed if the output should be called directly on the player's internal rendering thread.
decoderFactory: A factory from which to obtain MetadataDecoder instances.
outputMetadataEarly: Whether the renderer outputs metadata early. When true, MetadataRenderer.render(long, long) will output metadata as soon as they are available to the renderer, otherwise MetadataRenderer.render(long, long) will output metadata in sync with the rendering position.

Methods

public java.lang.String getName()

public int supportsFormat(Format format)

protected void onStreamChanged(Format formats[], long startPositionUs, long offsetUs, MediaSource.MediaPeriodId mediaPeriodId)

Called when the renderer's stream has changed. This occurs when the renderer is enabled after BaseRenderer.onEnabled(boolean, boolean) has been called, and also when the stream has been replaced whilst the renderer is enabled or started.

The default implementation is a no-op.

Parameters:

formats: The enabled formats.
startPositionUs: The start position of the new stream in renderer time (microseconds).
offsetUs: The offset that will be added to the timestamps of buffers read via BaseRenderer.readSource(FormatHolder, DecoderInputBuffer, int) so that decoder input buffers have monotonically increasing timestamps.
mediaPeriodId: The of the MediaPeriod that produces the stream.

protected void onPositionReset(long positionUs, boolean joining)

Called when the position is reset. This occurs when the renderer is enabled after BaseRenderer.onStreamChanged(Format[], long, long, MediaSource.MediaPeriodId) has been called, and also when a position discontinuity is encountered.

After a position reset, the renderer's SampleStream is guaranteed to provide samples starting from a key frame.

The default implementation is a no-op.

Parameters:

positionUs: The new playback position in microseconds.
joining: Whether this renderer is being enabled to join an ongoing playback.

public void render(long positionUs, long elapsedRealtimeUs)

protected void onDisabled()

Called when the renderer is disabled.

The default implementation is a no-op.

public boolean isEnded()

public boolean isReady()

public boolean handleMessage(Message msg)

Source

/*
 * Copyright (C) 2016 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.exoplayer.metadata;

import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.castNonNull;

import android.os.Handler;
import android.os.Handler.Callback;
import android.os.Looper;
import android.os.Message;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.Metadata;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.BaseRenderer;
import androidx.media3.exoplayer.FormatHolder;
import androidx.media3.exoplayer.RendererCapabilities;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.SampleStream.ReadDataResult;
import androidx.media3.extractor.metadata.MetadataDecoder;
import androidx.media3.extractor.metadata.MetadataInputBuffer;
import java.util.ArrayList;
import java.util.List;
import org.checkerframework.dataflow.qual.SideEffectFree;

/**
 * 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
public final class MetadataRenderer extends BaseRenderer implements Callback {

  private static final String TAG = "MetadataRenderer";
  private static final int MSG_INVOKE_RENDERER = 1;

  private final MetadataDecoderFactory decoderFactory;
  private final MetadataOutput output;
  @Nullable private final Handler outputHandler;
  private final MetadataInputBuffer buffer;
  private final boolean outputMetadataEarly;

  @Nullable private MetadataDecoder decoder;
  private boolean inputStreamEnded;
  private boolean outputStreamEnded;
  private long subsampleOffsetUs;
  @Nullable private Metadata pendingMetadata;
  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
   *     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.
   */
  public MetadataRenderer(MetadataOutput output, @Nullable Looper outputLooper) {
    this(output, outputLooper, MetadataDecoderFactory.DEFAULT);
  }

  /**
   * 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.
   */
  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;
  }

  @Override
  public String getName() {
    return TAG;
  }

  @Override
  public @Capabilities int supportsFormat(Format format) {
    if (decoderFactory.supportsFormat(format)) {
      return RendererCapabilities.create(
          format.cryptoType == C.CRYPTO_TYPE_NONE ? C.FORMAT_HANDLED : C.FORMAT_UNSUPPORTED_DRM);
    } else {
      return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE);
    }
  }

  @Override
  protected void onStreamChanged(
      Format[] formats,
      long startPositionUs,
      long offsetUs,
      MediaSource.MediaPeriodId mediaPeriodId) {
    decoder = decoderFactory.createDecoder(formats[0]);
    if (pendingMetadata != null) {
      pendingMetadata =
          pendingMetadata.copyWithPresentationTimeUs(
              pendingMetadata.presentationTimeUs + outputStreamOffsetUs - offsetUs);
    }
    outputStreamOffsetUs = offsetUs;
  }

  @Override
  protected void onPositionReset(long positionUs, boolean joining) {
    pendingMetadata = null;
    inputStreamEnded = false;
    outputStreamEnded = false;
  }

  @Override
  public void render(long positionUs, long elapsedRealtimeUs) {
    boolean working = true;
    while (working) {
      readMetadata();
      working = outputMetadata(positionUs);
    }
  }

  /**
   * Iterates through {@code metadata.entries} and checks each one to see if contains wrapped
   * metadata. If it does, then we recursively decode the wrapped metadata. If it doesn't (recursion
   * base-case), we add the {@link Metadata.Entry} to {@code decodedEntries} (output parameter).
   */
  private void decodeWrappedMetadata(Metadata metadata, List<Metadata.Entry> decodedEntries) {
    for (int i = 0; i < metadata.length(); i++) {
      @Nullable Format wrappedMetadataFormat = metadata.get(i).getWrappedMetadataFormat();
      if (wrappedMetadataFormat != null && decoderFactory.supportsFormat(wrappedMetadataFormat)) {
        MetadataDecoder wrappedMetadataDecoder =
            decoderFactory.createDecoder(wrappedMetadataFormat);
        // wrappedMetadataFormat != null so wrappedMetadataBytes must be non-null too.
        byte[] wrappedMetadataBytes =
            Assertions.checkNotNull(metadata.get(i).getWrappedMetadataBytes());
        buffer.clear();
        buffer.ensureSpaceForWrite(wrappedMetadataBytes.length);
        castNonNull(buffer.data).put(wrappedMetadataBytes);
        buffer.flip();
        @Nullable Metadata innerMetadata = wrappedMetadataDecoder.decode(buffer);
        if (innerMetadata != null) {
          // The decoding succeeded, so we'll try another level of unwrapping.
          decodeWrappedMetadata(innerMetadata, decodedEntries);
        }
      } else {
        // Entry doesn't contain any wrapped metadata, so output it directly.
        decodedEntries.add(metadata.get(i));
      }
    }
  }

  @Override
  protected void onDisabled() {
    pendingMetadata = null;
    decoder = null;
    outputStreamOffsetUs = C.TIME_UNSET;
  }

  @Override
  public boolean isEnded() {
    return outputStreamEnded;
  }

  @Override
  public boolean isReady() {
    return true;
  }

  @Override
  public boolean handleMessage(Message msg) {
    switch (msg.what) {
      case MSG_INVOKE_RENDERER:
        invokeRendererInternal((Metadata) msg.obj);
        return true;
      default:
        // Should never happen.
        throw new IllegalStateException();
    }
  }

  private void readMetadata() {
    if (!inputStreamEnded && pendingMetadata == null) {
      buffer.clear();
      FormatHolder formatHolder = getFormatHolder();
      @ReadDataResult int result = readSource(formatHolder, buffer, /* readFlags= */ 0);
      if (result == C.RESULT_BUFFER_READ) {
        if (buffer.isEndOfStream()) {
          inputStreamEnded = true;
        } else if (buffer.timeUs >= getLastResetPositionUs()) {
          // Ignore metadata before start position.
          buffer.subsampleOffsetUs = subsampleOffsetUs;
          buffer.flip();
          @Nullable Metadata metadata = castNonNull(decoder).decode(buffer);
          if (metadata != null) {
            List<Metadata.Entry> entries = new ArrayList<>(metadata.length());
            decodeWrappedMetadata(metadata, entries);
            if (!entries.isEmpty()) {
              Metadata expandedMetadata =
                  new Metadata(getPresentationTimeUs(buffer.timeUs), entries);
              pendingMetadata = expandedMetadata;
            }
          }
        }
      } else if (result == C.RESULT_FORMAT_READ) {
        subsampleOffsetUs = Assertions.checkNotNull(formatHolder.format).subsampleOffsetUs;
      }
    }
  }

  private boolean outputMetadata(long positionUs) {
    boolean didOutput = false;
    if (pendingMetadata != null
        && (outputMetadataEarly
            || pendingMetadata.presentationTimeUs <= getPresentationTimeUs(positionUs))) {
      invokeRenderer(pendingMetadata);
      pendingMetadata = null;
      didOutput = true;
    }
    if (inputStreamEnded && pendingMetadata == null) {
      outputStreamEnded = true;
    }
    return didOutput;
  }

  private void invokeRenderer(Metadata metadata) {
    if (outputHandler != null) {
      outputHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget();
    } else {
      invokeRendererInternal(metadata);
    }
  }

  private void invokeRendererInternal(Metadata metadata) {
    output.onMetadata(metadata);
  }

  @SideEffectFree
  private long getPresentationTimeUs(long positionUs) {
    checkState(positionUs != C.TIME_UNSET);
    checkState(outputStreamOffsetUs != C.TIME_UNSET);

    return positionUs - outputStreamOffsetUs;
  }
}