الصور

يتوافق ExoPlayer مع تنسيقات الصور التالية. عرض مكتبات تحميل الصور حول كيفية الدمج مع المكتبات الخارجية التي قد تقدّم دعمًا مجموعة مختلفة من التنسيقات.

تنسيق الصورة معلومات معتمَدة ملاحظات
BMP نعم
ملف GIF لا لا يمكن استخدام أداة الاستخراج
JPEG نعم
صورة متحركة بتنسيق JPEG نعم الصور الثابتة والفيديوهات متاحة
PNG نعم
تنسيق WebP نعم
HEIF/HEIC نعم
صورة متحركة HEIC جزئيًا إمكانية استخدام الصور الثابتة فقط*
AVIF (المرجع) نعم تم فك ترميزها على Android 14 أو الإصدارات الأحدث فقط

* يمكن الحصول على جزء الفيديو الخاص بصور حركة HEIC باستخدام MetadataRetriever وتشغيله كملف مستقل.

استخدام MediaItem

لتشغيل صورة كجزء من قائمة تشغيل، أنشِئ MediaItem باستخدام معرّف الموارد المنتظم (URI) للصورة. وتمريره إلى اللاعب. يجب أن يحتوي 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 العديد من الطرق لتخصيص تجربة التشغيل بما يتناسب مع واحتياجاته. راجِع صفحة التخصيص للحصول على أمثلة.

مكتبات تحميل الصور

وغالبًا ما تتم إدارة الصور بواسطة مكتبات خارجية لتحميل الصور، مثل: التمرير أو الملف.

يتطلب دمج هذه المكتبات في مسار التشغيل 3 خطوات:

  1. حدِّد MediaItem من نوع MIME APPLICATION_EXTERNALLY_LOADED_IMAGE.
  2. اكتب برنامج فك ترميز الصور لاسترداد Bitmap من مكتبة تحميل الصور.
  3. توفير برنامج تحميل خارجي لتشغيل التخزين المؤقت والتحميل المُسبق

MediaItem نوع MIME للصورة التي يتم تحميلها خارجيًا

يجب أن تحدّد السمة MediaItem المُضافة إلى Player 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) للصورة. يمكن كتابة برنامج فك الترميز هذا لاستخدام طريقة تحميل الصور الخارجية. كما هو موضح في المثال التالي باستخدام "التمرير السريع":

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 لتشغيل هذا التحميل المُسبق. إذا كانت الإجابة "لا" تكون التحميل المُسبق ممكنًا أو مطلوبًا، وما زال من الضروري إجراء ولكن لا يمكنه فعل أي شيء.

يستخدم المثال التالي ميزة "التمرير السريع" لتحميل الصور مسبقًا إلى الذاكرة.

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