图像

ExoPlayer 支持以下图像格式。有关如何与可能支持不同格式集的外部库集成的信息,请参阅图像加载库

图像格式 支持 备注
BMP
GIF 没有提取器支持
JPEG
JPEG Motion Photo 支持静止图像和视频
JPEG 超高清 HDR 在 Android 14 之前或非 HDR 显示屏上回退到 SDR
PNG
WebP
HEIF/HEIC
HEIC Motion Photo 部分 仅支持静止图像*
AVIF(基线) 仅在 Android 14 及更高版本上解码

* HEIC动态照片的视频部分可以使用MetadataRetriever获取,并作为独立文件播放。

使用 MediaItem

要将图像作为播放列表的一部分播放,请使用图像 URI 创建一个MediaItem,并将其传递给播放器。该MediaItem必须具有imageDurationMs来指定应显示图像的时间长度。

Kotlin

// Create a player instance.
val player = ExoPlayer.Builder(context).build()
// Set the media item to be played with the desired duration.
player.setMediaItem(
    MediaItem.Builder().setUri(imageUri).setImageDurationMs(2000).build())
// Prepare the player.
player.prepare()

Java

// Create a player instance.
ExoPlayer player = new ExoPlayer.Builder(context).build();
// Set the media item to be played with the desired duration.
player.setMediaItem(
    new MediaItem.Builder().setUri(imageUri).setImageDurationMs(2000).build());
// Prepare the player.
player.prepare();

动态照片

动态照片是将静态图像与短视频组合的文件。

  • 如果使用setImageDuration定义图像时长,则动态照片将作为静态图像显示声明的时长。
  • 如果未定义图像时长,则动态照片将作为视频播放。

使用 ProgressiveMediaSource

为了获得更多自定义选项,您可以创建一个ProgressiveMediaSource,并将其直接传递给播放器,而不是MediaItem

Kotlin

// Create a data source factory.
val dataSourceFactory = DefaultHttpDataSource.Factory()
// Create a media item with the image URI and the desired duration.
val mediaItem =
    MediaItem.Builder().setUri(imageUri).setImageDurationMs(2000).build()
// Create a progressive media source for this media item.
val mediaSource =
    ProgressiveMediaSource.Factory(dataSourceFactory)
        .createMediaSource(mediaItem)
// Create a player instance.
val player = ExoPlayer.Builder(context).build()
// Set the media source to be played.
player.setMediaSource(mediaSource)
// Prepare the player.
player.prepare()

Java

// Create a data source factory.
DataSource.Factory dataSourceFactory = new DefaultHttpDataSource.Factory();
// Create a media item with the image URI and the desired duration.
MediaItem mediaItem =
    new MediaItem.Builder().setUri(imageUri).setImageDurationMs(2000).build();
// Create a progressive media source for this media item.
MediaSource mediaSource =
    new ProgressiveMediaSource.Factory(dataSourceFactory)
        .createMediaSource(mediaItem);
// Create a player instance.
ExoPlayer player = new ExoPlayer.Builder(context).build();
// Set the media source to be played.
player.setMediaSource(mediaSource);
// Prepare the player.
player.prepare();

自定义播放

ExoPlayer 提供多种方法让您根据应用需求定制播放体验。请参阅自定义页面以获取示例。

图像加载库

图像通常由外部图像加载库管理,例如GlideCoil

将这些库集成到播放管道中需要 3 个步骤

  1. 使用APPLICATION_EXTERNALLY_LOADED_IMAGE MIME 类型定义一个MediaItem
  2. 提供一个图像解码器,从图像加载库中检索Bitmap
  3. 提供一个外部加载器来触发缓存和预加载。

带有外部加载图像 MIME 类型的 MediaItem

添加到PlayerMediaItem必须显式定义APPLICATION_EXTERNALLY_LOADED_IMAGE MIME 类型才能使用图像加载库代码路径

Kotlin

val mediaItem =
  MediaItem.Builder()
    .setUri(imageUri)
    .setMimeType(MimeTypes.APPLICATION_EXTERNALLY_LOADED_IMAGE)
    .build()

Java

MediaItem mediaItem =
    new MediaItem.Builder()
        .setUri(imageUri)
        .setMimeType(MimeTypes.APPLICATION_EXTERNALLY_LOADED_IMAGE)
        .build();

使用图像加载库的图像解码器

ImageRenderer使用ImageDecoder实例来检索图像 URI 的Bitmap。此解码器可以编写为使用外部图像加载库,如下面的 Glide 示例所示

Kotlin

val glideImageDecoder: ImageDecoder =
  object : ImageDecoder {
    private val inputBuffer =
      DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL)
    private val outputBuffer: ImageOutputBuffer =
      object : ImageOutputBuffer() {
        override fun release() {
          clear()
          bitmap = null
        }
      }
    private var pendingDecode: AtomicBoolean? = null
    private var decodeError: ImageDecoderException? = null

    override fun dequeueInputBuffer(): DecoderInputBuffer? {
      return if (pendingDecode == null) inputBuffer else null
    }

    override fun queueInputBuffer(inputBuffer: DecoderInputBuffer) {
      if (inputBuffer.isEndOfStream) {
        outputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM)
        inputBuffer.clear()
        return
      }
      val currentDecode = AtomicBoolean(true)
      pendingDecode = currentDecode
      val imageUri =
        Uri.parse(
          String(
            inputBuffer.data!!.array(),
            inputBuffer.data!!.position(),
            inputBuffer.data!!.limit() - inputBuffer.data!!.position(),
            Charsets.UTF_8,
          )
        )
      val imageTimeUs = inputBuffer.timeUs
      Glide.with(context)
        .asBitmap()
        .load(imageUri)
        .into(
          object : CustomTarget<Bitmap?>() {
            override fun onResourceReady(
              resource: Bitmap,
              transition: Transition<in Bitmap?>?,
            ) {
              if (currentDecode.get()) {
                outputBuffer.timeUs = imageTimeUs
                outputBuffer.bitmap = resource
                pendingDecode = null
              }
            }

            override fun onLoadFailed(errorDrawable: Drawable?) {
              if (currentDecode.get()) {
                decodeError = ImageDecoderException("Glide load failed")
              }
            }

            override fun onLoadCleared(placeholder: Drawable?) {}
          }
        )
      inputBuffer.clear()
    }

    @Throws(ImageDecoderException::class)
    override fun dequeueOutputBuffer(): ImageOutputBuffer? {
      if (decodeError != null) {
        throw decodeError as ImageDecoderException
      }
      val hasOutput =
        (pendingDecode == null
          && (outputBuffer.bitmap != null || outputBuffer.isEndOfStream))
      return if (hasOutput) outputBuffer else null
    }

    override fun getName(): String {
      return "glideDecoder"
    }

    override fun setOutputStartTimeUs(outputStartTimeUs: Long) {}

    override fun flush() {
      if (pendingDecode != null) {
        pendingDecode!!.set(false)
        pendingDecode = null
      }
      decodeError = null
      inputBuffer.clear()
      outputBuffer.release()
    }

    override fun release() {
      flush()
    }
}
val glideImageDecoderFactory: ImageDecoder.Factory =
  object : ImageDecoder.Factory {
    override fun supportsFormat(format: Format):
        @RendererCapabilities.Capabilities Int {
      val isExternalImageUrl =
        format.sampleMimeType != null &&
          format.sampleMimeType == MimeTypes.APPLICATION_EXTERNALLY_LOADED_IMAGE
      return RendererCapabilities.create(
        if (isExternalImageUrl) C.FORMAT_HANDLED else C.FORMAT_UNSUPPORTED_TYPE
      )
    }

    override fun createImageDecoder(): ImageDecoder {
      return glideImageDecoder
    }
  }
val player: Player =
  ExoPlayer.Builder(context)
    .setRenderersFactory(
      object : DefaultRenderersFactory(context) {
        override fun buildImageRenderers(out: ArrayList<Renderer>) {
          out.add(
            ImageRenderer(glideImageDecoderFactory, /* imageOutput= */ null))
        }
      }
    )
  .build()

Java

ImageDecoder glideImageDecoder =
    new ImageDecoder() {
      private final DecoderInputBuffer inputBuffer =
          new DecoderInputBuffer(BUFFER_REPLACEMENT_MODE_NORMAL);
      private final ImageOutputBuffer outputBuffer =
          new ImageOutputBuffer() {
            @Override
            public void release() {
              clear();
              bitmap = null;
            }
          };
      @Nullable private AtomicBoolean pendingDecode;
      @Nullable private ImageDecoderException decodeError;

      @Nullable
      @Override
      public DecoderInputBuffer dequeueInputBuffer() {
        return pendingDecode == null ? inputBuffer : null;
      }

      @Override
      public void queueInputBuffer(DecoderInputBuffer inputBuffer) {
        if (inputBuffer.isEndOfStream()) {
          outputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
          inputBuffer.clear();
          return;
        }
        AtomicBoolean currentDecode = new AtomicBoolean(true);
        pendingDecode = currentDecode;
        Uri imageUri =
            Uri.parse(
                new String(
                    inputBuffer.data.array(),
                    inputBuffer.data.position(),
                    inputBuffer.data.limit() - inputBuffer.data.position(),
                    Charsets.UTF_8));
        long imageTimeUs = inputBuffer.timeUs;
        Glide.with(context)
            .asBitmap()
            .load(imageUri)
            .into(
                new CustomTarget<Bitmap>() {
                  @Override
                  public void onResourceReady(
                      Bitmap resource,
                      @Nullable Transition<? super Bitmap> transition) {
                    if (currentDecode.get()) {
                      outputBuffer.timeUs = imageTimeUs;
                      outputBuffer.bitmap = resource;
                      pendingDecode = null;
                    }
                  }

                  @Override
                  public void onLoadFailed(@Nullable Drawable errorDrawable) {
                    if (currentDecode.get()) {
                      decodeError =
                          new ImageDecoderException("Glide load failed");
                    }
                  }

                  @Override
                  public void onLoadCleared(@Nullable Drawable placeholder) {}
                });
        inputBuffer.clear();
      }

      @Nullable
      @Override
      public ImageOutputBuffer dequeueOutputBuffer()
            throws ImageDecoderException {
        if (decodeError != null) {
          throw decodeError;
        }
        boolean hasOutput =
            pendingDecode == null
              && (outputBuffer.bitmap != null || outputBuffer.isEndOfStream());
        return hasOutput ? outputBuffer : null;
      }

      @Override
      public String getName() {
        return "glideDecoder";
      }

      @Override
      public void setOutputStartTimeUs(long outputStartTimeUs) {}

      @Override
      public void flush() {
        if (pendingDecode != null) {
          pendingDecode.set(false);
          pendingDecode = null;
        }
        decodeError = null;
        inputBuffer.clear();
        outputBuffer.release();
      }

      @Override
      public void release() {
        flush();
      }
    };
ImageDecoder.Factory glideImageDecoderFactory =
    new ImageDecoder.Factory() {
      @Override
      public @RendererCapabilities.Capabilities int supportsFormat(
          Format format) {
        boolean isExternalImageUrl =
            format.sampleMimeType != null
                && format.sampleMimeType.equals(
                    MimeTypes.APPLICATION_EXTERNALLY_LOADED_IMAGE);
        return RendererCapabilities.create(
            isExternalImageUrl ? C.FORMAT_HANDLED : C.FORMAT_UNSUPPORTED_TYPE);
      }

      @Override
      public ImageDecoder createImageDecoder() {
        return glideImageDecoder;
      }
    };
Player player =
    new ExoPlayer.Builder(context)
        .setRenderersFactory(
            new DefaultRenderersFactory(context) {
              @Override
              protected void buildImageRenderers(ArrayList<Renderer> out) {
                out.add(
                    new ImageRenderer(
                        glideImageDecoderFactory, /* imageOutput= */ null));
              }
            })
        .build();

使用图像加载库的图像预加载

在播放过程中,播放器会在播放列表中前一个项目完全加载后请求预加载下一个图像。当使用外部图像加载库时,您必须指定一个ExternalLoader来触发此预加载。如果无法或不需要预加载,则仍然需要提供此加载器,但可以不做任何操作。

以下示例使用 Glide 确保请求的图像被预加载到磁盘

Kotlin

val glidePreloader = ExternalLoader { request: LoadRequest ->
  GlideFutures.submit(
    Glide.with(context)
      .asFile()
      .apply(
        RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.DATA)
          .priority(Priority.HIGH)
          .skipMemoryCache(true)
      )
      .load(request.uri)
  )
}

Java

ExternalLoader glidePreloader =
    request ->
        GlideFutures.submit(
            Glide.with(context)
                .asFile()
                .apply(
                    diskCacheStrategyOf(DiskCacheStrategy.DATA)
                        .priority(Priority.HIGH)
                        .skipMemoryCache(true))
                .load(request.uri));