圖片

ExoPlayer 支援下列圖片格式。詳情請見 圖片載入程式庫 ,瞭解如何整合外部程式庫 (可能為 組合不同格式

圖片格式 支援 附註
BMP
GIF 不支援擷取器
JPEG
JPEG 動態相片 支援靜態圖片和影片
JPEG Ultra HDR 在 Android 14 或以下版本之前改回使用 SDR 非 HDR 螢幕
PNG
WebP
HEIF/HEIC
HEIC 動態相片 不盡然 僅支援靜態圖片*
AVIF (基準) 只能在 Android 14 以上版本中進行解碼

* 若是 HEIC 動態相片,可透過 中繼資料擷取者 並以獨立檔案的形式播放

使用 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 提供多種功能,您可以配合個人 對應用程式的需求而言如需示例,請參閱「自訂」頁面

圖片載入程式庫

舉例來說,圖片通常是由外部圖片載入程式庫管理 滑行Coil

如要將這些程式庫整合至播放管道,需要 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 例項擷取 Bitmap 映像檔 URI您可以編寫這個解碼器,使用載入外部圖片 程式庫,如下方 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));