본문 바로가기

Android/지식저장소

[Android] ExoPlayer2 현재 재생위치 가져오기

ExoPlayer2 에서 currentPosition(현재 재생위치) 가져오는 방법에 대한 포스팅이다.

 

본문 요약

- ExoPlayer 에서 재생위치를 실시간으로 가져오려면 폴링 방식으로 플레이어에 직접 요청해서 체크해야한다. (주기적으로 상태 체크해서 exoPlayer.getCurrentPosition())

 

회사 프로젝트 내 기능중 동영상플레이어 구현을 위해 ExoPlayer2 라이브러리를 사용하고있는데, 현재 재생위치 콜백을 받아야하는 니즈가 생겼다. 당연히 관련 리스너를 등록하면 바로 가져올 수 있을 줄 알았는데 player 에서는 제공하고있는 리스너는 재생상태 뿐이었다.

더보기
더보기
interface EventListener {

    /**
     * Called when the timeline and/or manifest has been refreshed.
     *
     * <p>Note that if the timeline has changed then a position discontinuity may also have
     * occurred. For example, the current period index may have changed as a result of periods being
     * added or removed from the timeline. This will <em>not</em> be reported via a separate call to
     * {@link #onPositionDiscontinuity(int)}.
     *
     * @param timeline The latest timeline. Never null, but may be empty.
     * @param manifest The latest manifest. May be null.
     * @param reason The {@link TimelineChangeReason} responsible for this timeline change.
     */
    default void onTimelineChanged(
        Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {}

    /**
     * Called when the available or selected tracks change.
     *
     * @param trackGroups The available tracks. Never null, but may be of length zero.
     * @param trackSelections The track selections for each renderer. Never null and always of
     *     length {@link #getRendererCount()}, but may contain null elements.
     */
    default void onTracksChanged(
        TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {}

    /**
     * Called when the player starts or stops loading the source.
     *
     * @param isLoading Whether the source is currently being loaded.
     */
    default void onLoadingChanged(boolean isLoading) {}

    /**
     * Called when the value returned from either {@link #getPlayWhenReady()} or {@link
     * #getPlaybackState()} changes.
     *
     * @param playWhenReady Whether playback will proceed when ready.
     * @param playbackState One of the {@code STATE} constants.
     */
    default void onPlayerStateChanged(boolean playWhenReady, int playbackState) {}

    /**
     * Called when the value of {@link #isPlaying()} changes.
     *
     * @param isPlaying Whether the player is playing.
     */
    default void onIsPlayingChanged(boolean isPlaying) {}

    /**
     * Called when the value of {@link #getRepeatMode()} changes.
     *
     * @param repeatMode The {@link RepeatMode} used for playback.
     */
    default void onRepeatModeChanged(@RepeatMode int repeatMode) {}

    /**
     * Called when the value of {@link #getShuffleModeEnabled()} changes.
     *
     * @param shuffleModeEnabled Whether shuffling of windows is enabled.
     */
    default void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {}

    /**
     * Called when an error occurs. The playback state will transition to {@link #STATE_IDLE}
     * immediately after this method is called. The player instance can still be used, and {@link
     * #release()} must still be called on the player should it no longer be required.
     *
     * @param error The error.
     */
    default void onPlayerError(ExoPlaybackException error) {}

    /**
     * Called when a position discontinuity occurs without a change to the timeline. A position
     * discontinuity occurs when the current window or period index changes (as a result of playback
     * transitioning from one period in the timeline to the next), or when the playback position
     * jumps within the period currently being played (as a result of a seek being performed, or
     * when the source introduces a discontinuity internally).
     *
     * <p>When a position discontinuity occurs as a result of a change to the timeline this method
     * is <em>not</em> called. {@link #onTimelineChanged(Timeline, Object, int)} is called in this
     * case.
     *
     * @param reason The {@link DiscontinuityReason} responsible for the discontinuity.
     */
    default void onPositionDiscontinuity(@DiscontinuityReason int reason) {}

    /**
     * Called when the current playback parameters change. The playback parameters may change due to
     * a call to {@link #setPlaybackParameters(PlaybackParameters)}, or the player itself may change
     * them (for example, if audio playback switches to passthrough mode, where speed adjustment is
     * no longer possible).
     *
     * @param playbackParameters The playback parameters.
     */
    default void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {}

    /**
     * Called when all pending seek requests have been processed by the player. This is guaranteed
     * to happen after any necessary changes to the player state were reported to {@link
     * #onPlayerStateChanged(boolean, int)}.
     */
    default void onSeekProcessed() {}
  }

 

ExoPlayer에서 제공하는 기본 컨트롤러쪽을 보면 주기적으로 타임바, 현재 재생시간을 갱신하는걸 볼 수 있는데 이쪽 부분을 커스텀하면 쉽게 가져올 수 있을 줄 알았다. 내부적으로 PlayerControllView 에서 아래와 같은 progressUpdateListener 가 있어서 등록도 해봤는데 안됐고, TimeBar 를 커스텀해서 리스너를 직접 달아주는 방법까지 별짓을 다해봤는데 결론적으로는 다 안됐다.

private void updateProgress() {
    //...
    if (timeBar != null) {
      timeBar.setPosition(position);
      timeBar.setBufferedPosition(bufferedPosition);
    }
    if (progressUpdateListener != null) {
      progressUpdateListener.onProgressUpdate(position, bufferedPosition);
    }

    // ...
  }

 

안되는 이유를 stackOverflow 답변이나 ExoPlayer 레포의 이슈탭을 종합적으로 판단해보면 아래와 같이 정리할 수 있다.

- 너무 잦은 콜백이 호출되므로 CPU 사용량이 많아질 것. 참고
- updateProgress() 는 컨트롤러의 재생 타임라인이 보여질 때만 호출되므로 progressChangeListener 는 무의미함

 

 

여기 참고해보면 정답이 있는데, 폴링 방식으로 직접 currentPosition 을 취하라고 말하고있다. 그래서 나는 아래와 같이 구현했다.

폴링은 운영체제 수업에서 들었던 기억이 있는데, 하나의 장치(또는 프로그램)가 충돌 회피 또는 동기화 처리 등을 목적으로 다른 장치(또는 프로그램)의 상태를 주기적으로 검사하여 일정한 조건을 만족할 때 송수신 등의 자료처리를 하는 방식을 말한다. (위키백과)

 

private val runnableCurrentPosition = this::pollCurrentPosition

private fun pollCurrentPosition() {
    if (exoPlayer == null || !isPlaying()) return

    removeCallbacks(runnableCurrentPosition)

    listener?.onPositionChanged(exoPlayer!!.currentPosition)

    postDelayed(runnableCurrentPosition, PLAYER_TIME_INTERVAL_MS)
}

 

PlayerControlView 에서 TimeBar 를 그리는 메서드(updateProgress())를 참고해서 작성했다. 핸들러 포스트를 저런식으로 하는걸 처음 봐서 읽었을 때 이해하기 힘들었는데 별건 없었다. 재귀호출 형태로 핸들러에 작업을 넘기고, 탈출 조건에 따라 재귀호출을 멈추게 구성하면 된다.

 

참고로 exoPlayer.getCurrentPosition() 은 메인스레드에서만 해야함을 잊으면 안된다. 스택오버플로우나 여러 참고해보면 Rx나 코루틴으로도 구현 가능한데 Dispatcher 설정에 유의하자. (난 테스트 안해봤지만 익셉션 던진다고 getCurrentPosition() 메서드의 주석에 설명되어있으니 확인해볼 것!)