제품 소식

미디어 재생 개선: Media3의 PreloadManager 자세히 알아보기 - 2부

전문 길이: 9분
Mayuri Khinvasara Khabya
개발자 관계팀 엔지니어

Media3을 사용한 미디어 미리 로드에 관한 3부작 시리즈의 두 번째 편에 오신 것을 환영합니다. 이 시리즈는 Android 앱에서 응답성이 높고 지연 시간이 짧은 미디어 환경을 빌드하는 과정을 안내하도록 설계되었습니다.

  • 1부: Media3를 사용한 미리 로드 소개에서는 기본사항을 다뤘습니다. 간단한 재생목록을 위한 PreloadConfiguration과 동적 사용자 인터페이스를 위한 더 강력한 DefaultPreloadManager의 차이점을 살펴봤습니다. 기본 API 수명 주기를 구현하는 방법을 배웠습니다. add()로 미디어를 추가하고, getMediaSource()로 준비된 MediaSource를 검색하고, setCurrentPlayingIndex() 및 invalidate()로 우선순위를 관리하고, remove() 및 release()로 리소스를 해제합니다.
  • 2부 (이 게시물): 이 블로그에서는 DefaultPreloadManager의 고급 기능을 살펴봅니다. PreloadManagerListener로 통계를 얻고, ExoPlayer와 핵심 구성요소를 공유하는 등 프로덕션 준비가 완료된 권장사항을 구현하고, 슬라이딩 윈도우 패턴을 마스터하여 메모리를 효과적으로 관리하는 방법을 알아봅니다.
  • 3부: 이 시리즈의 마지막 부분에서는 PreloadManager를 영구 디스크 캐시와 통합하여 리소스 관리로 데이터 소비를 줄이고 원활한 환경을 제공하는 방법을 자세히 알아봅니다.

Media3의 미리 로드를 처음 사용하는 경우 계속 진행하기 전에 1부를 읽어보는 것이 좋습니다. 기본 사항을 넘어설 준비가 되었다면 미디어 재생 구현을 개선하는 방법을 살펴보겠습니다.

수신: PreloadManagerListener로 분석 가져오기

앱 개발자는 프로덕션에서 기능을 출시할 때 기능의 분석을 이해하고 캡처해야 합니다. 실제 환경에서 프리로딩 전략이 효과적인지 어떻게 확인할 수 있나요? 이 질문에 답하려면 성공률, 실패, 실적에 관한 데이터가 필요합니다. PreloadManagerListener 인터페이스는 이 데이터를 수집하는 기본 메커니즘입니다.

PreloadManagerListener는 미리 로드 프로세스 및 상태에 관한 중요한 통계를 제공하는 두 가지 필수 콜백을 제공합니다.

  • onCompleted(MediaItem mediaItem): 이 콜백은 TargetPreloadStatusControl에 정의된 대로 프리로드 요청이 성공적으로 완료되면 호출됩니다.
  • onError(PreloadException error): 이 콜백은 디버깅 및 모니터링에 유용할 수 있습니다. 프리로드에 실패하면 연결된 예외를 제공하면서 호출됩니다.

다음 예시 코드와 같이 단일 메서드 호출로 리스너를 등록할 수 있습니다.

  val preloadManagerListener = object : PreloadManagerListener {
    override fun onCompleted(mediaItem: MediaItem) {
        // Log success for analytics. 
        Log.d("PreloadAnalytics", "Preload completed for $mediaItem")
    }

    override fun onError( preloadError: PreloadException) {
        // Log the specific error for debugging and monitoring.
        Log.e("PreloadAnalytics", "Preload error ", preloadError)
    }
}

preloadManager.addListener(preloadManagerListener)

리스너에서 유용한 정보 추출 

이러한 리스너 콜백은 분석 파이프라인에 연결할 수 있습니다. 이러한 이벤트를 분석 엔진으로 전달하면 다음과 같은 주요 질문에 답할 수 있습니다.

  • 미리 로드 성공률은 얼마인가요? (onCompleted 이벤트 수와 총 사전 로드 시도 수의 비율)
  • 오류율이 가장 높은 CDN 또는 동영상 형식은 무엇인가요? (onError에서 예외를 파싱하여)
  • 프리로드 오류율은 얼마인가요? (onError 이벤트 수와 총 사전 로드 시도 수의 비율)

이 데이터를 통해 사전 로드 전략에 관한 정량적 피드백을 얻을 수 있으므로 A/B 테스트를 실행하고 데이터 기반으로 사용자 환경을 개선할 수 있습니다. 이 데이터는 지능적으로 미리 로드 기간, 미리 로드할 동영상 수, 할당할 버퍼를 미세 조정하는 데도 도움이 됩니다.

디버깅을 넘어: onError를 사용하여 원활한 UI 대체

실패한 미리 로드는 사용자에게 곧 버퍼링 이벤트가 발생할 가능성이 높음을 나타냅니다. onError 콜백을 사용하면 사후 대응할 수 있습니다. 오류를 로깅하는 대신 UI를 조정할 수 있습니다. 예를 들어 다음 동영상이 미리 로드되지 않으면 애플리케이션에서 다음 스와이프의 자동 재생을 사용 중지하여 사용자가 탭해야 재생이 시작되도록 할 수 있습니다.

또한 PreloadException 유형을 검사하여 더 지능적인 재시도 전략을 정의할 수 있습니다. 앱은 오류 메시지 또는 HTTP 상태 코드에 따라 실패한 소스를 관리자에서 즉시 삭제할 수 있습니다. 로드 문제가 사용자 환경에 누출되지 않도록 UI 스트림에서 항목을 적절히 삭제해야 합니다. HttpDataSourceException과 같은 PreloadException에서 더 세부적인 데이터를 가져와 오류를 자세히 조사할 수도 있습니다. ExoPlayer 문제 해결에 대해 자세히 알아보세요.

버디 시스템: ExoPlayer와 구성요소를 공유해야 하는 이유는 무엇인가요?

DefaultPreloadManager와 ExoPlayer는 함께 작동하도록 설계되었습니다. 안정성과 효율성을 보장하려면 여러 핵심 구성요소를 공유해야 합니다. 별도의 조정되지 않은 구성요소로 작동하는 경우 미리 로드된 트랙이 올바른 플레이어에서 재생되도록 해야 하므로 플레이어에서 미리 로드된 트랙의 스레드 안전성과 유용성에 영향을 미칠 수 있습니다. 별도의 구성요소가 네트워크 대역폭 및 메모리와 같은 제한된 리소스를 놓고 경쟁하여 성능 저하가 발생할 수도 있습니다. 수명 주기의 중요한 부분은 적절한 처리를 처리하는 것입니다. 권장되는 처리 순서는 먼저 PreloadManager를 출시한 다음 ExoPlayer를 출시하는 것입니다.

DefaultPreloadManager.Builder는 이러한 공유를 용이하게 하도록 설계되었으며 PreloadManager와 연결된 플레이어 인스턴스를 모두 인스턴스화하는 API가 있습니다. BandwidthMeter, LoadControl, TrackSelector, Looper와 같은 구성요소를 공유해야 하는 이유를 살펴보겠습니다. 이러한 구성요소가 ExoPlayer 재생과 상호작용하는 방식의 시각적 표현을 확인하세요.

preloadManager2.png

공유 BandwidthMeter로 대역폭 충돌 방지

BandwidthMeter는 이전 전송 속도를 기반으로 사용 가능한 네트워크 대역폭을 추정합니다. PreloadManager와 플레이어가 별도의 인스턴스를 사용하는 경우 서로의 네트워크 활동을 알지 못하므로 실패 시나리오가 발생할 수 있습니다. 예를 들어 사용자가 동영상을 시청하는 동안 네트워크 연결이 저하되고 미리 로드된 MediaSource가 동시에 향후 동영상의 적극적인 다운로드를 시작하는 시나리오를 생각해 보세요. 미디어 소스의 활동을 미리 로드하면 활성 플레이어에 필요한 대역폭이 소비되어 현재 동영상이 멈춥니다. 재생 중 멈춤은 심각한 사용자 환경 실패입니다.

단일 BandwidthMeter를 공유함으로써 TrackSelector는 프리로드 또는 재생 중에 현재 네트워크 상태와 버퍼 상태를 고려하여 최고 품질의 트랙을 선택할 수 있습니다. 그런 다음 활성 재생 세션을 보호하고 원활한 환경을 보장하기 위해 지능적인 결정을 내릴 수 있습니다.

preloadManagerBuilder.setBandwidthMeter(customBandwidthMeter)

ExoPlayer의 공유 LoadControl, TrackSelector, Renderer 구성요소와의 일관성 유지

  • LoadControl: 이 구성요소는 재생을 시작하기 전에 버퍼링할 데이터의 양, 추가 데이터 로드를 시작하거나 중지할 시점과 같은 버퍼링 정책을 결정합니다. LoadControl을 공유하면 미리 로드된 미디어와 활성 상태로 재생 중인 미디어 모두에서 단일의 조정된 버퍼링 전략에 따라 플레이어와 PreloadManager의 메모리 소비가 안내되므로 리소스 경합이 방지됩니다. 일관성을 유지하려면 미리 로드하는 항목 수와 기간에 맞춰 버퍼 크기를 적절하게 할당해야 합니다. 경합이 발생하면 플레이어는 화면에 표시된 현재 항목의 재생을 우선시합니다. 공유 LoadControl을 사용하면 프리로드 관리자는 프리로드에 할당된 타겟 버퍼 바이트가 상한에 도달하지 않는 한 계속 프리로드하며 재생을 위한 로딩이 완료될 때까지 기다리지 않습니다.

참고: 최신 버전의 Media3 (1.8)에서 LoadControl을 공유하면 할당자를 PreloadManager 및 플레이어와 올바르게 공유할 수 있습니다. LoadControl을 사용하여 효과적으로 미리 로드를 제어하는 기능은 곧 출시될 Media3 1.9에서 제공될 예정입니다.

preloadManagerBuilder.setLoadControl(customLoadControl)

  • TrackSelector: 이 구성요소는 로드하고 재생할 트랙 (예: 특정 해상도의 동영상, 특정 언어의 오디오)을 선택합니다. 공유를 통해 사전 로드 중에 선택한 트랙이 플레이어에서 사용할 트랙과 동일하게 됩니다. 이렇게 하면 480p 동영상 트랙이 미리 로드된 후 플레이어가 재생 시 이를 즉시 삭제하고 720p 트랙을 가져오는 낭비적인 시나리오를 방지할 수 있습니다.< br /> 미리 로드 관리자는 플레이어와 동일한 인스턴스의 TrackSelector를 공유하면 안 됩니다. 대신 구현은 동일하지만 다른 TrackSelector 인스턴스를 사용해야 합니다. 따라서 DefaultPreloadManager.Builder에서 TrackSelector가 아닌 TrackSelectorFactory를 설정합니다.

preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)

  • 렌더러: 이 구성요소는 전체 렌더러를 생성하지 않고 플레이어의 기능을 이해하는 역할을 합니다. 이 청사진을 확인하여 최종 플레이어에서 지원할 동영상, 오디오, 텍스트 형식을 확인합니다. 이를 통해 호환되는 미디어 트랙만 지능적으로 선택하고 다운로드할 수 있으며 플레이어가 실제로 재생할 수 없는 콘텐츠에 대역폭이 낭비되지 않습니다.

preloadManagerBuilder.setRenderersFactory(customRenderersFactory)

ExoPlayer 구성요소에 대해 자세히 알아보세요.

핵심 원칙: 모든 것을 제어하는 공통 재생 루퍼

ExoPlayer 인스턴스에 액세스할 수 있는 스레드는 플레이어를 만들 때 Looper를 전달하여 명시적으로 지정할 수 있습니다. 플레이어에 액세스해야 하는 스레드의 루퍼는 Player.getApplicationLooper를 사용하여 쿼리할 수 있습니다. 플레이어와 PreloadManager 간에 공유 Looper를 유지하면 이러한 공유 미디어 객체에 대한 모든 작업이 단일 스레드의 메시지 대기열로 직렬화됩니다. 이렇게 하면 동시 실행 버그를 줄일 수 있습니다.

PreloadManager와 로드되거나 미리 로드될 미디어 소스가 있는 플레이어 간의 모든 상호작용은 동일한 재생 스레드에서 발생해야 합니다. Looper는 스레드 안전을 위해 공유해야 하므로 PreloadManager와 플레이어 간에 PlaybackLooper를 공유해야 합니다.

PreloadManager는 백그라운드에서 상태 저장 MediaSource 객체를 준비합니다. UI 코드에서 player.setMediaSource(mediaSource)를 호출하면 미리 로드된 MediaSource에서 플레이어로 이 복잡한 상태 저장 객체를 핸드오프합니다. 이 시나리오에서는 전체 PreloadMediaSource가 관리자에서 플레이어로 이동합니다. 이러한 모든 상호작용과 핸드오프는 동일한 PlaybackLooper에서 발생해야 합니다.

PreloadManager와 ExoPlayer가 서로 다른 스레드에서 작동하는 경우 경합 상태가 발생할 수 있습니다. 플레이어의 스레드가 MediaSource에서 읽으려고 시도하는 바로 그 순간에 PreloadManager의 스레드가 MediaSource의 내부 상태를 수정할 수 있습니다 (예: 버퍼에 새 데이터 쓰기). 이로 인해 예측할 수 없는 동작과 디버깅하기 어려운 IllegalStateException이 발생합니다.

preloadManagerBuilder.setPreloadLooper(playbackLooper)

설정 자체에서 ExoPlayer와 DefaultPreloadManager 간에 위의 구성요소를 모두 공유하는 방법을 살펴보겠습니다.

  val preloadManagerBuilder =
DefaultPreloadManager.Builder(context, targetPreloadStatusControl)

// Optional - Share components between ExoPlayer and DefaultPreloadManager
preloadManagerBuilder
     .setBandwidthMeter(customBandwidthMeter)
     .setLoadControl(customLoadControl)
     .setMediaSourceFactory(customMediaSourceFactory)
     .setTrackSelectorFactory(customTrackSelectorFactory)
     .setRenderersFactory(customRenderersFactory)
     .setPreloadLooper(playbackLooper)

val preloadManager = val preloadManagerBuilder.build()

팁: ExoPlayer에서 DefaultLoadControl과 같은 기본 구성요소를 사용하는 경우 DefaultPreloadManager와 명시적으로 공유하지 않아도 됩니다. DefaultPreloadManager.Builder의 buildExoPlayer를 통해 ExoPlayer 인스턴스를 빌드할 때 기본 구성으로 기본 구현을 사용하면 이러한 구성요소가 서로 자동으로 참조됩니다. 하지만 맞춤 구성요소나 맞춤 구성을 사용하는 경우 위의 API를 통해 DefaultPreloadManager에 이를 명시적으로 알려야 합니다.

프로덕션에 즉시 사용 가능한 사전 로드: 슬라이딩 구간 패턴

동적 피드에서는 사용자가 사실상 무한한 양의 콘텐츠를 스크롤할 수 있습니다. 해당 삭제 전략 없이 DefaultPreloadManager에 동영상을 계속 추가하면 OutOfMemoryError가 발생할 수밖에 없습니다. 각 미리 로드된 MediaSource는 메모리 버퍼를 할당하는 SampleQueue를 보유합니다. 이러한 항목이 누적되면 애플리케이션의 힙 공간이 소진될 수 있습니다. 이 문제의 해결책은 슬라이딩 윈도우라는 알고리즘입니다. 슬라이딩 윈도우 패턴은 피드에서 사용자의 현재 위치와 논리적으로 인접한 작고 관리 가능한 항목 집합을 메모리에 유지합니다. 사용자가 스크롤하면 관리 항목의 이 '창'이 함께 슬라이드되어 표시되는 새 항목이 추가되고 이제 멀리 떨어진 항목이 삭제됩니다.

slidingwindow.png

슬라이딩 구간 패턴 구현

PreloadManager는 기본 제공 setWindowSize() 메서드를 제공하지 않는다는 점을 이해해야 합니다. 슬라이딩 윈도우는 개발자가 기본 add() 및 remove() 메서드를 사용하여 구현해야 하는 디자인 패턴입니다. 애플리케이션 로직은 스크롤 또는 페이지 변경과 같은 UI 이벤트를 이러한 API 호출에 연결해야 합니다. 이와 관련된 코드 참조가 필요한 경우 슬라이딩 윈도우를 모방하는 PreloadManagerWrapper도 포함된 socialite 샘플에 구현된 슬라이딩 윈도우 패턴을 참고하세요.

사용자의 시청에서 항목이 곧 표시되지 않을 가능성이 있는 경우 구현에 preloadManager.remove(mediaItem)를 추가해야 합니다. 더 이상 사용자에게 근접하지 않은 항목을 삭제하지 않으면 미리 로드 구현에서 메모리 문제가 발생할 수 있습니다. remove() 호출은 앱의 메모리 사용량을 제한하고 안정적으로 유지하는 데 도움이 되는 리소스가 해제되도록 합니다.

TargetPreloadStatusControl을 사용하여 분류된 사전 로드 전략 미세 조정

이제 미리 로드할 항목 (창의 항목)을 정의했으므로 각 항목에 대해 미리 로드할 양에 대한 잘 정의된 전략을 적용할 수 있습니다. 파트 1TargetPreloadStatusControl 설정을 통해 이 세부사항을 달성하는 방법을 이미 확인했습니다.

위치를 +/- 1로 지정한 항목은 위치를 +/- 4로 지정한 항목보다 재생될 가능성이 더 높습니다. 사용자가 다음에 볼 가능성이 가장 높은 항목에 더 많은 리소스 (네트워크, CPU, 메모리)를 할당할 수 있습니다. 이렇게 하면 근접성을 기반으로 '미리 로드' 전략이 생성되며, 이는 즉각적인 재생과 효율적인 리소스 사용 간의 균형을 맞추는 데 핵심입니다.

이전 섹션에서 설명한 대로 PreloadManagerListener를 통해 분석 데이터를 사용하여 미리 로드 기간 전략을 결정할 수 있습니다.

결론 및 다음 단계

이제 Media3의 DefaultPreloadManager를 사용하여 빠르고 안정적이며 리소스 효율적인 미디어 피드를 빌드할 수 있는 고급 지식을 갖추게 되었습니다.

핵심 내용을 다시 정리해 보겠습니다.

  • PreloadManagerListener를 사용하여 분석 통계를 수집하고 강력한 오류 처리를 구현합니다.
  • 중요한 구성요소가 공유되도록 항상 단일 DefaultPreloadManager.Builder를 사용하여 관리자와 플레이어 인스턴스를 모두 만드세요.
  • OutOfMemoryError를 방지하기 위해 add() 및 remove() 호출을 적극적으로 관리하여 슬라이딩 윈도우 패턴을 구현합니다.
  • TargetPreloadStatusControl을 사용하여 성능과 리소스 소비의 균형을 맞추는 스마트 계층화된 사전 로드 전략을 만드세요.

3부의 다음 단계: 미리 로드된 미디어로 캐싱

데이터를 메모리에 미리 로드하면 즉각적인 성능 이점을 얻을 수 있지만, 트레이드오프가 발생할 수 있습니다. 애플리케이션이 닫히거나 미리 로드된 미디어가 관리자에서 삭제되면 데이터가 사라집니다. 더 지속적인 수준의 최적화를 달성하기 위해 미리 로드와 디스크 캐싱을 결합할 수 있습니다. 이 기능은 현재 개발 중이며 몇 개월 내에 출시될 예정입니다.

공유하고 싶은 의견이 있으신가요? 여러분의 의견을 기다리고 있습니다.

앞으로도 많은 관심 부탁드립니다. 동영상 재생 속도를 높여 보세요. 🚀

작성자:

계속 읽기