제품 소식

미디어 재생 개선: 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가 동시에 향후 동영상의 적극적인 다운로드를 시작하는 시나리오를 생각해 보세요. 미리 로드 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 인스턴스에 액세스할 수 있는 스레드는 플레이어를 만들 때 루퍼를 전달하여 명시적으로 지정할 수 있습니다. 플레이어에 액세스해야 하는 스레드의 루퍼는 Player.getApplicationLooper를 사용하여 쿼리할 수 있습니다. 플레이어와 PreloadManager 간에 공유 루퍼를 유지하면 이러한 공유 미디어 객체에 관한 모든 작업이 단일 스레드의 메시지 큐로 직렬화됩니다. 이렇게 하면 동시 실행 버그를 줄일 수 있습니다.

로드하거나 미리 로드할 미디어 소스가 있는 PreloadManager와 플레이어 간의 모든 상호작용은 동일한 재생 스레드에서 발생해야 합니다. 루퍼를 공유하는 것은 스레드 안전을 위해 필수적이므로 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로 분류된 미리 로드 전략 미세 조정

이제 미리 로드할 항목 (윈도우의 항목)을 정의했으므로 각 항목에 미리 로드할 양에 관한 잘 정의된 전략을 적용할 수 있습니다. 1부에서 TargetPreloadStatusControl 설정을 사용하여 이 세분성을 달성하는 방법을 이미 살펴보았습니다.

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

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

결론 및 다음 단계

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

핵심 내용을 요약해 보겠습니다.

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

3부의 다음 내용: 미리 로드된 미디어를 사용한 캐싱

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

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

계속 지켜봐 주시고 동영상 재생 속도를 높여 보세요. 🚀

작성자:

계속 읽기