이미지

ExoPlayer는 다음 이미지 형식을 지원합니다. 자세한 내용은 이미지 로드 라이브러리 API에 대한 지원을 제공할 수 있는 외부 라이브러리와 통합하는 방법에 대해 사용할 수 있습니다.

이미지 형식 지원됨 참고
BMP
GIF 아니요 추출기 지원 안 됨
JPEG
JPEG 모션 사진 정지 이미지 및 동영상 지원됨
PNG
WebP
HEIF/HEIC
HEIC 모션 포토 일부만 정지 이미지만 지원됨*
AVIF (기준) Android 14 이상에서만 디코딩됨

* HEIC 모션 사진의 동영상 부분은 MetadataRetriever 독립형 파일로 재생됩니다.

MediaItem 사용

이미지를 재생목록의 일부로 재생하려면 이미지 URI를 사용하여 MediaItem를 만듭니다. 플레이어에 전달합니다. MediaItemimageDurationMs가 있어야 합니다. 이미지를 표시할 시간을 지정할 수 있습니다.

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()

자바

// 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()

자바

// 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는 사용자의 기기에 맞게 재생 환경을 맞춤설정하는 여러 가지 방법을 파악할 수 있습니다. 예시는 맞춤설정 페이지를 참고하세요.

이미지 로드 라이브러리

이미지는 종종 외부 이미지 로드 라이브러리에서 관리됩니다. 예를 들면 다음과 같습니다. Glide 또는 코일.

이러한 라이브러리를 재생 파이프라인에 통합하려면 다음 세 단계를 수행해야 합니다.

  1. APPLICATION_EXTERNALLY_LOADED_IMAGE MIME 유형으로 MediaItem를 정의합니다.
  2. 이미지 디코더를 작성하여 이미지 로드 라이브러리에서 Bitmap를 가져옵니다.
  3. 캐싱 및 미리 로드를 트리거하는 외부 로더를 제공합니다.

외부에서 로드된 이미지 MIME 유형이 있는 MediaItem

Player에 추가된 MediaItem는 이미지를 사용하기 위한 명시적으로 APPLICATION_EXTERNALLY_LOADED_IMAGE MIME 유형 라이브러리 코드 경로 로드 중:

Kotlin

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

자바

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

이미지 로드 라이브러리를 사용하는 이미지 디코더

ImageRendererImageDecoder 인스턴스를 사용하여 Bitmap 이미지 URI입니다 이 디코더는 Transformer 블록의 외부 이미지 로딩을 라이브러리에 코드를 바인딩합니다.

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()

자바

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 ->
  val imagePreloadFuture = SettableFuture.create<Void?>()
  Glide.with(context)
    .load(request.uri)
    .addListener(
      object : RequestListener<Drawable?> {
        override fun onLoadFailed(
          e: GlideException?,
          model: Any?,
          target: Target<Drawable?>?,
          isFirstResource: Boolean,
        ): Boolean {
          imagePreloadFuture.setException(e)
          return false
        }

        override fun onResourceReady(
          resource: Drawable?,
          model: Any?,
          target: Target<Drawable?>?,
          dataSource: DataSource?,
          isFirstResource: Boolean,
        ): Boolean {
          imagePreloadFuture.set(null)
          return false
        }
      }
    )
    .preload()
  imagePreloadFuture
}
val player: Player =
  ExoPlayer.Builder(context)
    .setMediaSourceFactory(
      DefaultMediaSourceFactory(context).setExternalImageLoader(glidePreloader)
    )
    .build()

Java

ExternalLoader glidePreloader =
    request -> {
      SettableFuture<Void> imagePreloadFuture = SettableFuture.create();
      Glide.with(context)
          .load(request.uri)
          .addListener(
              new RequestListener<Drawable>() {
                @Override
                public boolean onLoadFailed(
                    @Nullable GlideException e,
                    Object model,
                    Target<Drawable> target,
                    boolean isFirstResource) {
                  imagePreloadFuture.setException(e);
                  return false;
                }

                @Override
                public boolean onResourceReady(
                    Drawable resource,
                    Object model,
                    Target<Drawable> target,
                    DataSource dataSource,
                    boolean isFirstResource) {
                  imagePreloadFuture.set(null);
                  return false;
                }
              })
          .preload();
      return imagePreloadFuture;
    };
Player player =
    new ExoPlayer.Builder(context)
        .setMediaSourceFactory(
            new DefaultMediaSourceFactory(context).setExternalImageLoader(glidePreloader))
        .build();