เหตุการณ์ของผู้เล่น

การเปลี่ยนแปลงสถานะของเพลเยอร์ (เช่น การเริ่มเล่น การบัฟเฟอร์ หรือข้อผิดพลาด) จะทริกเกอร์เหตุการณ์ที่ส่งไปยังอินสแตนซ์ Player.Listener ที่ลงทะเบียนไว้ เหตุการณ์เหล่านี้แสดงด้วยค่าคงที่จำนวนเต็มและกำหนดโดย Player.Eventและ Player.Events

ลงทะเบียน Player.Listener

ระบบจะรายงานเหตุการณ์ของเพลเยอร์ไปยังอินสแตนซ์ Player.Listener ที่ลงทะเบียนไว้ วิธีลงทะเบียน Listener เพื่อรับเหตุการณ์ดังกล่าว

Kotlin

// Add a listener to receive events from the player.
player.addListener(listener)

Java

// Add a listener to receive events from the player.
player.addListener(listener);

หากใช้ Kotlin คุณยังใช้ฟังก์ชันส่วนขยายแบบระงับที่โมดูล media3-common-ktx มีให้เพื่อรอรับเหตุการณ์โดยใช้โครูทีนได้ด้วย ในกรณีนี้ คุณไม่จำเป็นต้องลงทะเบียนหรือยกเลิกการลงทะเบียน Player.Listener อย่างชัดแจ้ง

รอรับเหตุการณ์การเล่นโดยใช้ Player.Listener

Player.Listener มีเมธอดเริ่มต้นที่ว่างเปล่า คุณจึงต้องใช้เฉพาะเมธอดที่คุณสนใจเท่านั้น ดูคำอธิบายเมธอดแบบเต็มและเวลาที่เรียกใช้ได้จาก Javadoc สำหรับคำอธิบายแบบเต็มของ เมธอดและเวลาที่เรียกใช้ เราจะอธิบายเมธอดที่สำคัญที่สุดบางเมธอดโดยละเอียดเพิ่มเติมด้านล่าง

Listener สามารถเลือกระหว่างการใช้การเรียกกลับของเหตุการณ์แต่ละรายการหรือการเรียกกลับ onEvents ทั่วไปที่จะเรียกใช้หลังจากเกิดเหตุการณ์อย่างน้อย 1 รายการพร้อมกัน ดู Individual callbacks vs onEvents เพื่อดูคำอธิบายว่าควรเลือกใช้การเรียกกลับใดสำหรับกรณีการใช้งานต่างๆ

การเปลี่ยนแปลงสถานะการเล่น

คุณรับการเปลี่ยนแปลงสถานะของเพลเยอร์ได้โดยใช้ onPlaybackStateChanged(@State int state) ใน Player.Listener ที่ลงทะเบียนไว้ เพลเยอร์อาจอยู่ในสถานะการเล่น 1 ใน 4 สถานะต่อไปนี้

  • Player.STATE_IDLE: นี่คือสถานะเริ่มต้น สถานะเมื่อเพลเยอร์หยุดทำงาน และเมื่อการเล่นล้มเหลว เพลเยอร์จะใช้ทรัพยากรเพียงเล็กน้อยในสถานะนี้
  • Player.STATE_BUFFERING: เพลเยอร์ไม่สามารถเล่นต่อจากตำแหน่งปัจจุบันได้ทันที ซึ่งส่วนใหญ่เกิดขึ้นเนื่องจากต้องโหลดข้อมูลเพิ่มเติม
  • Player.STATE_READY: เพลเยอร์สามารถเล่นต่อจากตำแหน่งปัจจุบันได้ทันที
  • Player.STATE_ENDED: เพลเยอร์เล่นสื่อทั้งหมดจบแล้ว

นอกจากสถานะเหล่านี้แล้ว เพลเยอร์ยังมีแฟล็ก playWhenReady เพื่อระบุความตั้งใจของผู้ใช้ที่จะเล่น คุณรับการเปลี่ยนแปลงแฟล็กนี้ได้โดยใช้ onPlayWhenReadyChanged(playWhenReady, @PlayWhenReadyChangeReason int reason)

เพลเยอร์กำลังเล่น (นั่นคือ ตำแหน่งกำลังเลื่อนไปข้างหน้าและระบบกำลังแสดงสื่อต่อผู้ใช้) เมื่อเป็นไปตามเงื่อนไข 3 ข้อต่อไปนี้

  • เพลเยอร์อยู่ในสถานะ Player.STATE_READY
  • playWhenReady เป็น true
  • ระบบไม่ได้ระงับการเล่นด้วยเหตุผลที่แสดงผลโดย Player.getPlaybackSuppressionReason

คุณสามารถเรียกใช้ Player.isPlaying แทนที่จะต้องตรวจสอบพร็อพเพอร์ตี้เหล่านี้ทีละรายการ คุณรับการเปลี่ยนแปลงสถานะนี้ได้โดยใช้ onIsPlayingChanged(boolean isPlaying)

Kotlin

player.addListener(
  object : Player.Listener {
    override fun onIsPlayingChanged(isPlaying: Boolean) {
      if (isPlaying) {
        // Active playback.
      } else {
        // Not playing because playback is paused, ended, suppressed, or the player
        // is buffering, stopped or failed. Check player.playWhenReady,
        // player.playbackState, player.playbackSuppressionReason and
        // player.playerError for details.
      }
    }
  }
)

Java

player.addListener(
    new Player.Listener() {
      @Override
      public void onIsPlayingChanged(boolean isPlaying) {
        if (isPlaying) {
          // Active playback.
        } else {
          // Not playing because playback is paused, ended, suppressed, or the player
          // is buffering, stopped or failed. Check player.getPlayWhenReady,
          // player.getPlaybackState, player.getPlaybackSuppressionReason and
          // player.getPlaybackError for details.
        }
      }
    });

ข้อผิดพลาดในการเล่น

คุณรับข้อผิดพลาดที่ทำให้การเล่นล้มเหลวได้โดยใช้ onPlayerError(PlaybackException error) ใน Player.Listener ที่ลงทะเบียนไว้ เมื่อเกิดความล้มเหลว ระบบจะเรียกใช้เมธอดนี้ทันทีก่อนที่สถานะการเล่นจะเปลี่ยนไปเป็น Player.STATE_IDLE คุณลองเล่นซ้ำได้โดยเรียกใช้ ExoPlayer.prepare

โปรดทราบว่าการติดตั้งใช้งาน Player บางรายการจะส่งอินสแตนซ์ของคลาสย่อยของ PlaybackException เพื่อให้ข้อมูลเพิ่มเติมเกี่ยวกับความล้มเหลว เช่น ExoPlayer จะส่ง ExoPlaybackException ซึ่งมี type, rendererIndex และฟิลด์อื่นๆ ที่เฉพาะเจาะจงของ ExoPlayer

ตัวอย่างต่อไปนี้แสดงวิธีตรวจหาเมื่อการเล่นล้มเหลวเนื่องจากปัญหาเครือข่าย HTTP

Kotlin

player.addListener(
  object : Player.Listener {
    override fun onPlayerError(error: PlaybackException) {
      val cause = error.cause
      if (cause is HttpDataSourceException) {
        // An HTTP error occurred.
        val httpError = cause
        // It's possible to find out more about the error both by casting and by querying
        // the cause.
        if (httpError is InvalidResponseCodeException) {
          // Cast to InvalidResponseCodeException and retrieve the response code, message
          // and headers.
        } else {
          // Try calling httpError.getCause() to retrieve the underlying cause, although
          // note that it may be null.
        }
      }
    }
  }
)

Java

player.addListener(
    new Player.Listener() {
      @Override
      public void onPlayerError(PlaybackException error) {
        @Nullable Throwable cause = error.getCause();
        if (cause instanceof HttpDataSourceException) {
          // An HTTP error occurred.
          HttpDataSourceException httpError = (HttpDataSourceException) cause;
          // It's possible to find out more about the error both by casting and by querying
          // the cause.
          if (httpError instanceof HttpDataSource.InvalidResponseCodeException) {
            // Cast to InvalidResponseCodeException and retrieve the response code, message
            // and headers.
          } else {
            // Try calling httpError.getCause() to retrieve the underlying cause, although
            // note that it may be null.
          }
        }
      }
    });

การเปลี่ยนเพลย์ลิสต์

เมื่อใดก็ตามที่เพลเยอร์เปลี่ยนไปใช้รายการสื่อใหม่ในเพลย์ลิสต์ onMediaItemTransition(MediaItem mediaItem, @MediaItemTransitionReason int reason) จะถูกเรียกใช้ในออบเจ็กต์ Player.Listener ที่ลงทะเบียนไว้ เหตุผลจะระบุว่าเป็นการเปลี่ยนอัตโนมัติ การค้นหา (เช่น หลังจากเรียกใช้ player.next()) การเล่นรายการเดิมซ้ำ หรือเกิดจากการเปลี่ยนแปลงเพลย์ลิสต์ (เช่น หากมีการนำรายการที่กำลังเล่นอยู่ออก)

ข้อมูลเมตา

ข้อมูลเมตาที่แสดงผลจาก player.getCurrentMediaMetadata() อาจเปลี่ยนแปลงได้เนื่องจากหลายสาเหตุ เช่น การเปลี่ยนเพลย์ลิสต์ การอัปเดตข้อมูลเมตาในสตรีม หรือการอัปเดต MediaItem ปัจจุบันระหว่างการเล่น

หากคุณสนใจการเปลี่ยนแปลงข้อมูลเมตา เช่น เพื่ออัปเดต UI ที่แสดงชื่อปัจจุบัน คุณสามารถรอรับ onMediaMetadataChanged ได้

กำลังค้นหา

การเรียกใช้เมธอด Player.seekTo จะทำให้เกิดการเรียกกลับหลายรายการไปยังอินสแตนซ์ Player.Listener ที่ลงทะเบียนไว้ ดังนี้

  1. onPositionDiscontinuity โดยมี reason=DISCONTINUITY_REASON_SEEK ซึ่งเป็นผลโดยตรงจากการเรียกใช้ Player.seekTo การเรียกกลับมีฟิลด์ PositionInfo สำหรับตำแหน่งก่อนและหลังการค้นหา
  2. onPlaybackStateChanged โดยมีการเปลี่ยนแปลงสถานะทันทีที่เกี่ยวข้องกับการค้นหา โปรดทราบว่าอาจไม่มีการเปลี่ยนแปลงดังกล่าว

การเรียกกลับแต่ละรายการเทียบกับ onEvents

Listener สามารถเลือกระหว่างการใช้การเรียกกลับแต่ละรายการ เช่น onIsPlayingChanged(boolean isPlaying) กับการเรียกกลับ onEvents(Player player, Events events) ทั่วไป การเรียกกลับทั่วไปจะให้สิทธิ์เข้าถึงออบเจ็กต์ Player และระบุชุด events ที่เกิดขึ้นพร้อมกัน ระบบจะเรียกใช้การเรียกกลับนี้เสมอหลังจากเรียกใช้การเรียกกลับที่สอดคล้องกับเหตุการณ์แต่ละรายการ

Kotlin

override fun onEvents(player: Player, events: Player.Events) {
  if (
    events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED) ||
      events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)
  ) {
    uiModule.updateUi(player)
  }
}

Java

@Override
public void onEvents(Player player, Events events) {
  if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)
      || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
    uiModule.updateUi(player);
  }
}

ควรเลือกใช้เหตุการณ์แต่ละรายการในกรณีต่อไปนี้

  • Listener สนใจเหตุผลของการเปลี่ยนแปลง เช่น เหตุผลที่ระบุไว้สำหรับ onPlayWhenReadyChanged หรือ onMediaItemTransition
  • Listener จะดำเนินการกับค่าใหม่ที่ระบุผ่านพารามิเตอร์การเรียกกลับเท่านั้น หรือทริกเกอร์สิ่งอื่นที่ไม่ขึ้นอยู่กับพารามิเตอร์การเรียกกลับ
  • การติดตั้งใช้งาน Listener ต้องการการระบุที่ชัดเจนและอ่านง่ายว่าอะไรเป็นตัวทริกเกอร์เหตุการณ์ในชื่อเมธอด
  • Listener จะรายงานไปยังระบบวิเคราะห์ที่ต้องทราบเกี่ยวกับเหตุการณ์แต่ละรายการและการเปลี่ยนแปลงสถานะทั้งหมด

ควรเลือกใช้ onEvents(Player player, Events events) ทั่วไปในกรณีต่อไปนี้

  • Listener ต้องการทริกเกอร์ตรรกะเดียวกันสำหรับเหตุการณ์หลายรายการ เช่น การอัปเดต UI สำหรับทั้ง onPlaybackStateChanged และ onPlayWhenReadyChanged
  • Listener ต้องเข้าถึงออบเจ็กต์ Player เพื่อทริกเกอร์เหตุการณ์เพิ่มเติม เช่น การค้นหาหลังจากเปลี่ยนรายการสื่อ
  • Listener ต้องการใช้ค่าสถานะหลายค่าที่รายงานผ่านการเรียกกลับแยกกัน หรือใช้ร่วมกับเมธอด Getter ของ Player เช่น การใช้ Player.getCurrentWindowIndex() กับ Timeline ที่ระบุไว้ใน onTimelineChanged จะปลอดภัยเฉพาะจากการเรียกกลับ onEvents เท่านั้น
  • Listener สนใจว่าเหตุการณ์เกิดขึ้นพร้อมกันตามตรรกะหรือไม่ เช่น onPlaybackStateChanged เป็น STATE_BUFFERING เนื่องจากการเปลี่ยนรายการสื่อ

ในบางกรณี Listener อาจต้องรวมการเรียกกลับแต่ละรายการกับการเรียกกลับ onEvents ทั่วไป เช่น เพื่อบันทึกเหตุผลการเปลี่ยนแปลงรายการสื่อด้วย onMediaItemTransition แต่จะดำเนินการก็ต่อเมื่อใช้การเปลี่ยนแปลงสถานะทั้งหมดร่วมกันใน onEvents ได้

รอรับเหตุการณ์การเล่นโดยใช้โครูทีน

อีกวิธีหนึ่งคือ คุณสามารถเปิดใช้โครูทีน Kotlin โดยใช้ Player.listenTo และ ระบุ Player.Event ที่เกี่ยวข้องได้ดังนี้

โปรดทราบว่าคุณเรียกใช้ Player.listen และ Player.listenTo ได้จากเธรดใดก็ได้ แต่ระบบจะเรียกใช้แลมบ์ดาการเรียกกลับในเธรดที่เชื่อมโยงกับ Player.getApplicationLooper เสมอ ดังนั้น คุณจึงเข้าถึงเมธอด Player และพร็อพเพอร์ตี้สถานะภายในแลมบ์ดาการเรียกกลับได้อย่างปลอดภัย แม้ว่าจะเปิดใช้โครูทีนในเธรดอื่นก็ตาม

การเปลี่ยนแปลงสถานะการเล่น

coroutineScope.launch {
  player.listenTo(Player.EVENT_IS_PLAYING_CHANGED) {
    // `Player` is a receiver scope for this trailing lambda
    if (isPlaying) {
      // Active playback.
    } else {
      // Not playing.
    }
  }
}

ข้อผิดพลาดในการเล่น

coroutineScope.launch {
  player.listenTo(Player.EVENT_PLAYER_ERROR) {
    val error = playerError ?: return@listenTo
    val cause = error.cause
    if (cause is HttpDataSourceException) {
      // An HTTP error occurred.
      if (cause is InvalidResponseCodeException) {
        // Retrieve the response code, message and headers
      } else {
        // Try calling cause.cause to retrieve the underlying cause
      }
    }
  }
}

การเรียกกลับแต่ละรายการเทียบกับ onEvents

เมื่อรอรับเหตุการณ์ของเพลเยอร์ภายในโครูทีน คุณจะต้องระบุ การติดตั้งใช้งานสำหรับการเรียกกลับ onEvents เสมอ ไม่ใช่การเรียกกลับ แต่ละรายการ คุณสามารถเลือกระหว่าง Player.listen กับ Player.listenTo ได้ ทั้งนี้ขึ้นอยู่กับว่าเหตุการณ์ใดควรทริกเกอร์การเรียกใช้แลมบ์ดา แต่ฟังก์ชันอื่นๆ จะเทียบเท่ากัน

ฟัง

coroutineScope.launch {
  player.listen { events ->
    // `Player` is a receiver scope for this trailing lambda
    if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
      // Access the player state directly from the receiver
      updateUi(playbackState)
    }

    if (events.contains(Player.EVENT_PLAYER_ERROR)) {
      // Access the error directly from the player
      handleError(playerError)
    }
  }
}

ฟัง

coroutineScope.launch {
  player.listenTo(Player.EVENT_PLAYBACK_STATE_CHANGED, Player.EVENT_PLAYER_ERROR) { events ->
    // `Player` is a receiver scope for this trailing lambda
    if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
      // Access the player state directly from the receiver
      updateUi(playbackState)
    }

    if (events.contains(Player.EVENT_PLAYER_ERROR)) {
      // Access the error directly from the player
      handleError(playerError)
    }
  }
}

หากสนใจเหตุการณ์หลายประเภท คุณสามารถส่งรายการเหตุการณ์ไปยัง Player.listenTo ได้ ระบบจะเรียกใช้แลมบ์ดาเมื่อใดก็ตามที่เกิดเหตุการณ์เหล่านั้น และคุณสามารถตรวจสอบพารามิเตอร์ Events เพื่อดูว่าเหตุการณ์ใดเกิดขึ้นจริง

coroutineScope.launch {
  player.listenTo(Player.EVENT_PLAYBACK_STATE_CHANGED, Player.EVENT_PLAYER_ERROR) { events ->
    // Unclear which event got triggered without querying `events` parameter
    // The following function will fire whenever either one is caught
    updateUiAndHandleError(playbackState, playerError)
  }
}

เนื่องจากฟังก์ชันเหล่านี้ทำงานใน onEvents จึงไม่มีสิทธิ์เข้าถึงอาร์กิวเมนต์ชั่วคราวที่ส่งไปยังการเรียกกลับแต่ละรายการ เช่น เหตุผลใน onMediaItemTransition(..., int reason) หรือ oldPosition ใน onPositionDiscontinuity(...) หากตรรกะของคุณขึ้นอยู่กับอาร์กิวเมนต์ที่เฉพาะเจาะจงเหล่านี้ (และอาร์กิวเมนต์เหล่านี้ไม่พร้อมใช้งานเป็น พร็อพเพอร์ตี้สถานะ ใน Player) คุณควร ใช้อินเทอร์เฟซ Player.Listener มาตรฐานแทน

ใช้ AnalyticsListener

เมื่อใช้ ExoPlayer คุณสามารถลงทะเบียน AnalyticsListener กับเพลเยอร์ ได้โดยเรียกใช้ addAnalyticsListener การติดตั้งใช้งาน AnalyticsListener สามารถรอรับเหตุการณ์โดยละเอียดซึ่งอาจมีประโยชน์สำหรับการวิเคราะห์และการบันทึก โปรดดูรายละเอียดเพิ่มเติมในหน้าการวิเคราะห์

ใช้ EventLogger

EventLogger เป็น AnalyticsListener ที่ไลบรารีมีให้โดยตรงเพื่อวัตถุประสงค์ในการบันทึก เพิ่ม EventLogger ลงใน ExoPlayer เพื่อเปิดใช้การบันทึกเพิ่มเติมที่เป็นประโยชน์ด้วยบรรทัดเดียว ดังนี้

Kotlin

player.addAnalyticsListener(EventLogger())

Java

player.addAnalyticsListener(new EventLogger());

ดูรายละเอียดเพิ่มเติมได้ในหน้าการบันทึกการแก้ไขข้อบกพร่อง

ทริกเกอร์เหตุการณ์ที่ตำแหน่งการเล่นที่ระบุ

กรณีการใช้งานบางกรณีต้องทริกเกอร์เหตุการณ์ที่ตำแหน่งการเล่นที่ระบุ ซึ่งระบบรองรับการดำเนินการนี้โดยใช้ PlayerMessage คุณสร้าง PlayerMessage ได้โดยใช้ ExoPlayer.createMessage และตั้งค่าตำแหน่งการเล่นที่จะให้ดำเนินการได้โดยใช้ PlayerMessage.setPosition โดยค่าเริ่มต้น ระบบจะดำเนินการข้อความในเธรดการเล่น แต่คุณปรับแต่งได้โดยใช้ PlayerMessage.setLooper คุณใช้ PlayerMessage.setDeleteAfterDelivery เพื่อควบคุมว่าจะให้ดำเนินการข้อความทุกครั้งที่พบตำแหน่งการเล่นที่ระบุ (ซึ่งอาจเกิดขึ้นหลายครั้งเนื่องจากโหมดการค้นหาและโหมดเล่นซ้ำ) หรือเฉพาะครั้งแรก เมื่อกำหนดค่า PlayerMessage แล้ว คุณสามารถกำหนดเวลาได้โดยใช้ PlayerMessage.send

Kotlin

player
  .createMessage { messageType: Int, payload: Any? -> }
  .setLooper(Looper.getMainLooper())
  .setPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 120000)
  .setPayload(customPayloadData)
  .setDeleteAfterDelivery(false)
  .send()

Java

player
    .createMessage(
        (messageType, payload) -> {
          // Do something at the specified playback position.
        })
    .setLooper(Looper.getMainLooper())
    .setPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 120_000)
    .setPayload(customPayloadData)
    .setDeleteAfterDelivery(false)
    .send();