앱 기본사항

Jetpack Media3 API로 미디어 재생을 최적화하고 도달범위를 극대화하세요

스피커

정보

강연 소개

미디어 소비는 사용자가 모바일 기기에서 보내는 시간의 상당 부분을 차지하므로 재생 환경은 완벽하고 효율적이며 배터리 수명을 고려해야 합니다. 또한 사용자는 휴대전화에서 거실 TV에 이르기까지 다양한 기기에서 이러한 환경이 원활하게 전환되기를 기대합니다.

Media3 및 ExoPlayer의 아키텍처 업데이트가 다양한 폼 팩터에서 원활한 재생을 지원하는 방법을 살펴보세요. Media3 Compose UI 툴킷을 사용하여 반응형 인터페이스를 만들고, 새로운 PreloadManager를 사용하여 피드의 버퍼링을 제거하고, CastPlayer와 서라운드 오디오를 사용하여 앱을 거실 화면과 Android XR로 확장하는 방법을 중점적으로 다룹니다.

요약

  • Media3 UI Compose는 사전 제작된 플레이어 컴포저블과 맞춤설정 가능한 구성 요소를 제공합니다. 라이브러리가 플레이어 상태와 전환을 관리하도록 하면서 특정 컨트롤 슬롯을 재정의할 수 있습니다.
  • 새로 작성된 CastPlayer가 독립형 Cast SDK를 대체합니다. 표준 플레이어 인터페이스를 구현하고 로컬 ExoPlayer 인스턴스와 원격 Cast 기기 간에 재생을 자동으로 위임합니다.
  • 재구성된 편집 파이프라인은 OpenGL에서 분리되어 하드웨어 버퍼를 직접 관리합니다. 이 제로 카피 접근 방식은 메모리 오버헤드를 줄이고, 분리를 통해 개발자는 WebGPU와 같은 최신 그래픽 프레임워크를 통합할 수 있습니다.
  • Media3는 Android 17 이상에서 Eclipsa 동영상 및 Eclipsa 오디오를 즉시 지원합니다. 규범적 HDR 메타데이터는 주변광에 맞게 하이라이트를 조정하고, 서라운드 오디오는 여러 혼합 프레젠테이션을 처리합니다.
  • PCM 오디오 오프로드와 같은 기본 제공 최적화 기능은 화면 꺼짐 전력 소비를 최대 20% 줄입니다. 출시 예정인 CodecDB는 벤치마크 측정항목을 사용하여 기기에 최적화된 인코더 설정을 선택합니다.

리소스

Gemini Notebook

주제

  • jetpack-media3
  • exoplayer
  • jetpack-compose
  • google-cast
  • video-editing
  • 오디오
  • performance

스크립트

Jetpack Media3 소개

  • 좋습니다.

  • 환영합니다, 여러분.

  • 제 이름은 토니입니다.

  • 저는 Android 미디어팀의 엔지니어입니다.

  • 크리스티나, 도노반과 함께 Jetpack Media3가 Android의 모든 미디어 요구사항을 어떻게 지원할 수 있는지 이야기해 보겠습니다.

  • Media3는 ExoPlayer를 사용한 재생, 미디어 세션 및 Cast를 사용한 미디어 공유, Transformer를 사용한 미디어 편집, 프레임 또는 메타데이터 추출과 같은 미디어 검사 등 미디어와 관련된 모든 작업을 지원하는 Android의 지원 라이브러리입니다.

  • Media3는 이 전체 공간을 단일 라이브러리 그룹으로 처리합니다.

  • 지원 라이브러리로서 앱에 번들로 포함됩니다.

  • 즉, 일관성을 보장할 수 있으므로 모든 OS 버전과 모든 기기에서 동일한 코드가 실행됩니다.

  • 업데이트 가능성에도 도움이 됩니다.

  • 모든 버그 수정은 API 24까지 적용되며 OS 출시 주기와는 관련이 없습니다.

  • 지원 라이브러리로서 Android 프레임워크에서는 불가능한 매우 풍부한 맞춤설정 옵션을 허용할 수 있습니다.

  • GitHub의 라이브러리로서 오픈소스 우선입니다.

  • 즉, 변경사항이 적용되는 즉시 GitHub에서 사용할 수 있습니다.

  • 활성 Issue Tracker가 있으며 pull 요청을 받고 있습니다.

  • 여기서 강조하고 싶은 핵심은 이 모든 사용 사례에서 Google은 높은 수준의 UI 요소부터 간단한 기본 제공 API에 이르기까지 모든 수준의 세부정보를 중요하게 생각한다는 점입니다. 또한 심층적인 맞춤설정 옵션, 다양한 형식과 표준에 대한 광범위한 지원, 이 모든 것을 뒷받침하는 성능과 효율성도 중요하게 생각합니다.

  • Media3는 이 전체 영역을 포괄합니다.

  • 오늘은 이러한 주제를 좀 더 자세히 살펴보겠습니다.

  • 새로운 Compose UI 모듈, 미디어 세션 공유를 더욱 쉽게 만들어 주는 새로운 CastPlayer, 새로운 고급 효과가 적용된 개선된 편집 파이프라인, 메타데이터와 프레임을 가져오는 새로운 도구, 새로운 스트리밍 기술(특히 Eclipsa 동영상 및 오디오), 미리 로드, 스크러빙, 배터리 절약을 위한 다양한 환경 품질 유틸리티에 대해 이야기해 보겠습니다.

Jetpack Compose를 사용한 미디어 UI

  • 이제 첫 번째 주제인 Media3를 사용하여 미디어용 UI 요소를 빌드하는 방법을 살펴보겠습니다.

  • 최근에 Media3 UI Compose와 Media3 UI Compose Material 3라는 두 가지 새로운 모듈을 게시했습니다.

  • 여기에서는 Media3 1.10에서 출시된 이후 추가 수정이 이루어지지 않은 상위 수준의 플레이어 컴포저블을 확인할 수 있습니다.

    @Composable
    fun videoScreen() {
      Player(viewModel.exoPlayer)
    }
  • 즉시 사용할 수 있습니다.

  • Media3 플레이어만 있으면 됩니다.

  • 이 예에서는 ExoPlayer이며 일반적으로 뷰 모델에 있습니다.

  • 하지만 CastPlayer, 미디어 컨트롤러 또는 앱에 있는 다른 플레이어일 수도 있습니다.

  • 여기서 ExoPlayer가 정보 소스라는 점을 강조할 필요가 있습니다.

  • 여기에는 모든 정보가 포함되어 있으며 플레이어 컴포저블이 UI를 렌더링하고 모든 것을 연결합니다.

  • 하지만 이 UI, 새 버튼, 다른 위치, 다른 스타일, 텍스트 오버레이를 맞춤설정하려면 어떻게 해야 할까요?

  • 플레이어 컴포저블은 구성요소로 구성됩니다.

  • 교체할 수 있습니다.

  • 각 개별 블록을 맞춤설정하거나 앱의 다른 위치에서 독립형으로 사용할 수 있습니다.

  • 예를 들어 상단, 중앙, bottomControls를 별도로 맞춤설정할 수 있습니다.

  • 동영상이 재생되지 않을 때 표시되는 셔터의 모양이나 오류 오버레이를 맞춤설정할 수도 있습니다.

  • 하지만 이는 첫 번째 레이어일 뿐입니다.

  • 기본 센터와 bottomControls에는 맞춤설정할 수 있는 더 자세한 슬롯도 있습니다.

  • 예를 들어 다른 모든 요소는 그대로 두고 중앙 버튼을 바꾸거나 슬라이더 위나 아래에 요소를 추가할 수 있습니다.

  • Media3 Compose는 이를 위해 필요한 모든 구성 요소를 제공합니다.

  • 예를 들어 재생/일시중지 버튼, 반복 모드, 셔플 모드, 음소거 버튼 등 다양한 버튼이 있습니다.

  • 그런 다음 화면에 표시되는 슬라이더와 위치 텍스트와 같은 진행 상태 표시기가 있습니다.

  • 컨테이너와 오버레이도 여러 개 있습니다.

  • 앞서 보여드린 상위 수준 플레이어 컴포저블은 한 가지 예일 뿐이며, 오류 오버레이나 자막 등 더 많은 예가 있습니다.

  • 또한 이 미니 컨트롤러와 같은 다른 유용한 구성요소도 있습니다.

  • 사용자가 앱을 탐색하는 동안 하단 시트에 배치할 수 있습니다.

  • 아직 끝이 아닙니다.

  • Google은 이 라이브러리를 적극적으로 확장하고 있으며, 예를 들어 곧 출시될 클리핑 슬라이더와 같은 새로운 편집 컨트롤을 추가할 예정입니다.

  • 이제 이러한 맞춤설정이 실제로 어떻게 작동하는지 살펴보고 이러한 구성요소가 어떻게 사용되는지 알아보겠습니다.

    Player(viewModel.exoPlayer)
  • 현재 슬라이더 옆에 있는 위치 및 재생 시간 텍스트를 슬라이더 위로 이동한다고 가정해 보겠습니다.

    Player(
      player = viewModel.exoPlayer,
      bottomControls = {
        PlayerDefaults.BottomControls(
          player,
          left = {}, right = {},
          above = {
            PositionAndDurationText(
              player, separator = " • ")
          },
        )
      },
    )
  • 따라서 이 bottomControls를 재정의할 수 있습니다.

  • 기본 구성요소를 계속 사용할 수 있지만 왼쪽 및 오른쪽 슬롯을 비워두면 사라집니다.

  • 대신 위치 및 재생 시간 텍스트를 맞춤 구분 기호와 함께 위 슬롯에 배치하여 더 흥미롭게 보이도록 합니다.

  • 이제 centerControls 오버레이를 완전히 삭제하고 대신 속도 제어 및 음소거 버튼이 있는 슬라이더 바로 위에 재생 버튼을 배치한다고 가정해 보겠습니다.

    Player(
      player = viewModel.exoPlayer,
      centerControls = {},
      bottomControls = { ...
          above = {
            Row(Modifier.fillMaxWidth()) {
              PositionAndDurationText(...)
              Spacer(Modifier.weight(1f))
              PlaybackSpeedToggleButton(player)
              MuteButton(player)
              PlayPauseButton(player)
            }
          },
        )
      }
  • 이것도 완전히 같은 아이디어입니다.

  • centerControls를 비워두면 사라지게 할 수 있으며, 슬라이더 위의 슬롯을 확장하여 추가 공간과 표시하려는 세 개의 버튼을 넣을 수 있습니다.

  • 이 버튼은 라이브러리 자체에서 제공되므로 추가 코드를 넣을 필요가 없습니다.

  • 완료되었습니다.

  • 이러한 요소를 스타일링하는 데도 이 방법을 계속 사용할 수 있습니다.

  • 음소거 버튼 아이콘을 변경하되 기능은 유지하려는 경우에도 동일한 방식으로 작동합니다.

    Player(
      ...
      bottomControls = { ...
          above = {
              ...
              MuteButton(player,
                painter = { painterResource(
                  if (showMuted)
                    R.drawable.muted
                  else
                    R.drawable.unmuted )})
          }
      },
    )
  • 여기에서 볼 수 있듯이 버튼의 맞춤 페인터를 정의하고 아이콘을 직접 선택할 수 있습니다.

  • 이 구성요소는 일부 메타데이터를 제공하므로 이 예시의 showMuted와 같은 상태를 확인하여 올바른 아이콘을 선택하기만 하면 됩니다.

  • 이것이 바로 Media3 Compose의 주요 이점입니다.

  • 개발자는 앱의 디자인과 분위기에 집중하고 나머지는 Media3가 처리합니다.

  • 플레이어에서 상태를 읽고 플레이어와 상호작용하며 그 사이의 모든 전환과 애니메이션을 처리합니다.

MediaSession으로 재생 공유

  • 이제 UI에서 미디어 공유로 넘어가겠습니다.

  • 동일한 기기의 다른 앱과 재생 상태를 공유하거나 원격 기기에 연결할 수 있습니다.

  • 진행 중인 재생을 게시하는 주요 방법은 MediaSession를 사용하는 것입니다.

  • 이를 통해 재생 상태와 메타데이터를 다른 사용자가 사용할 수 있고 재생을 제어할 수 있습니다.

  • 이것이 Gemini, 시스템 UI 미디어 알림, Cast, Wear OS 또는 Android Auto 호환 앱이 앱과 통신하는 방식입니다.

  • MediaSession는 Media3 플레이어를 래핑하며, 플레이어가 있으면 매우 쉽게 만들 수 있습니다.

    MediaSession.Builder(context, player).build()
  • Compose UI와 마찬가지로 ExoPlayer 또는 앱에 있는 다른 플레이어가 될 수 있습니다.

  • 백그라운드에서 재생하는 경우 일반적으로 MediaSessionService 또는 MediaLibraryService 내에서 호스팅하며, 이 경우 모든 수명 주기와 알림과 같은 항목이 자동으로 처리됩니다.

    class PlaybackService : MediaSessionService() {
    
      private lateinit var mediaSession:
            MediaSession
    
      override fun onGetSession(
            info: ControllerInfo) : MediaSession?
        = mediaSession
    }
  • 미디어 앱을 보유하고 있다면 이미 이 모든 것을 알고 계실 것입니다.

  • 새롭게 선보이는 흥미로운 기능은 새로 작성된 CastPlayer입니다.

  • Google Cast를 사용하면 로컬 재생을 Android TV, Chromecast, 스마트 스피커와 같은 원격 기기로 전송할 수 있습니다.

  • 이전에는 별도의 Cast SDK를 게시했지만 현재는 유지보수 모드입니다.

  • 대신 Media3 Cast는 훨씬 더 간단하고 간소화된 통합을 제공하므로 이를 완전히 대체합니다.

  • 여기서 가장 중요한 요소는 ExoPlayer와 같은 표준 플레이어 인터페이스를 구현하는 CastPlayer입니다.

  • ExoPlayer는 로컬 재생용이지만 CastPlayer는 ExoPlayer를 통해 로컬로 재생하거나 Cast 프로토콜을 통해 원격으로 재생할 수 있는 위임 인스턴스입니다.

  • 그러면 모든 것이 처리됩니다.

  • 출력 장치 변경사항을 수신 대기하고, 연결을 설정하고, 상태를 동기화하고, 필요할 때 재생을 전송합니다.

  • 따라서 CastPlayer를 앱에 통합하는 것은 매우 간단합니다.

    val player = ExoPlayer.Builder(context).build()
    
    mediaSession = MediaSession.Builder(context,player)
    .build()
  • ExoPlayer를 만들고 다음과 같이 MediaSession와 공유하는 대신 CastPlayer를 사용하면 됩니다.

    Cast.getSingletonInstance(context).initialize()
    
    val player = CastPlayer.Builder(context).build()
    
    mediaSession = MediaSession.Builder(context,player)
    .build()
  • 그 외에 해야 할 일은 앱에서 사용하기 전에 Cast 시스템을 초기화하는 것입니다.

  • 이 CastPlayer는 로컬 재생을 위해 내부적으로 ExoPlayer를 빌드하므로 로컬 플레이어를 추가로 맞춤설정하려면 자체 ExoPlayer를 빌드하여 이 CastPlayer에 로컬 플레이어로 전달할 수도 있습니다.

    Cast.getSingletonInstance(context).initialize()
    val exoPlayer = ExoPlayer.Builder(context).build()
    val player = CastPlayer.Builder(context)
                    .setLocalPlayer(exoPlayer).build()
    mediaSession = MediaSession.Builder(context,player)
    .build()
  • 이것 역시 Media3의 원칙입니다.

  • Google은 간단한 기본 제공 API 구성요소뿐만 아니라 추가 맞춤설정에 필요한 모든 맞춤설정 옵션도 제공합니다.

  • 이 설정에서 사용자가 실제로 로컬 재생과 원격 재생 간에 어떻게 전환하는지 궁금할 수 있습니다.

  • 미디어 알림의 시스템 출력 전환기 대화상자를 통해 즉시 사용할 수 있습니다.

  • 모서리에 있는 작은 버튼입니다.

  • 하지만 재생 화면에 캐스팅 버튼을 표시하여 발견 가능성을 높이려면 어떻게 해야 할까요?

  • 몇 분 전에 빌드한 맞춤설정된 Compose UI를 기억하시나요?

    Player(
      exoPlayer,
      centerControls = {}
      bottomControls = { /* customized */ },
    )
  • Media3 Cast는 여기에 배치할 수 있는 기본 버튼도 제공합니다.

  • 예를 들어 준비된 MediaRouteButton를 topControls에 추가하는 것만큼 간단하며, 설정할 추가 매개변수가 필요하지 않습니다. 정말 간단합니다.

    Player(
      castPlayer,
      centerControls = {}
      bottomControls = { /* customized */ },
      topControls = {
        PlayerDefaults.TopControls(castPlayer) {
          MediaRouteButton(
            Modifier.align(Alignment.TopEnd))
        }
      },
    )
  • 이 버튼을 클릭하면 동일한 시스템 출력 전환기 대화상자가 열리고 CastPlayer가 재생을 전송합니다.

  • 또한 원격으로 발생하는 모든 작업이 로컬 UI에 반영되도록 CastPlayer를 UI에 연결했습니다.

  • 이제 크리스티나에게 마이크를 넘겨 수정 사용 사례에 대해 알아보겠습니다.

Media3 편집 API

  • 감사합니다.

  • 안녕하세요. 이제 편집 및 고급 효과로 넘어가겠습니다.

  • Media3 API는 여러 구성요소로 이루어집니다.

  • 먼저 Transformer는 편집 API의 기본 진입점입니다.

  • Transformer는 미디어를 처리, 트랜스코딩하고 파일로 내보냅니다.

  • 다음은 미디어 컴포지션의 실시간 미리보기를 렌더링하는 CompositionPlayer입니다.

  • 이렇게 하면 최종 내보내기 전에 수정사항을 미리 보고 미세 조정할 수 있습니다.

  • 다음은 효과입니다.

  • 간단한 시각 효과를 적용하거나 그래픽 오버레이를 추가할 때마다 프레임별 처리를 완전히 제어할 수 있습니다.

  • 마지막으로 Muxer가 있습니다.

  • 파이프라인의 최종 출력 단계로, 처리된 오디오, 동영상, 메타데이터 트랙을 가져와 표준화된 MP4 컨테이너에 작성합니다.

  • 지난 1년 동안 동영상 편집 파이프라인을 처음부터 다시 구상하고 재구축했으며, 오늘 주요 개선사항을 공유하게 되어 기쁩니다.

  • 파이프라인을 다시 빌드한 이유는 무엇인가요?

  • 따라서 기존 아키텍처에는 몇 가지 문제가 있었습니다.

  • 첫째, 모든 처리 단계에서 메타데이터를 전파할 수 없었습니다.

  • OpenGL과 긴밀하게 결합되어 있어 최신 그래픽 도구의 유연성이 제한되었습니다.

  • 또한 복잡한 API로 인해 개발자가 맞춤설정 및 유지보수를 수행하는 데 큰 어려움을 겪었습니다.

  • 이러한 문제를 해결하기 위해 새로운 처리 파이프라인을 구축했습니다.

  • 첫째, 복잡한 멀티 애셋 조합을 조립하는 것이 훨씬 간단해졌습니다.

  • 둘째, OpenGL에서 분리하면 WebGPU와 같은 최신 그래픽 프레임워크를 자유롭게 사용할 수 있습니다.

  • 마지막으로, 이제 Media3가 디코더와 인코더를 직접 관리하므로 모든 기기에서 일관성과 효율성이 향상됩니다.

  • 이 파이프라인이 어떻게 작동하는지 자세히 살펴보겠습니다.

  • 디코더는 Media3에서 관리하는 하드웨어 버퍼에 프레임을 직접 출력합니다.

  • 프레임 애그리게이터는 여러 시퀀스에서 프레임을 조정하여 커스텀 프레임 프로세서가 인코더로 전송하는 버퍼를 직접 읽고 처리할 수 있도록 합니다.

  • FrameProcessor 인터페이스는 앱 수준 처리를 위한 기본 확장 지점 역할을 합니다.

    public interface FrameProcessor
        extends AutoCloseable {
    
      interface Listener {
        void onWakeup();
        void onError(
             VideoFrameProcessingException exception);
        void onFrameProcessed(
             Frame frame,
             @Nullable SyncFenceWrapper onCompleteFence);
      }
    
      interface Factory {
        FrameProcessor create(
             FrameWriter output,
             Executor listenerExecutor,
             Listener listener);
      }
    
      boolean queue(List<AsyncFrame> frames);
    
      void signalEndOfStream();
    }
  • 애플리케이션은 리스너 인터페이스를 통해 프레임이 처리될 때마다 Media3에 알리고 동기화 펜스를 전달하므로 Media3는 버퍼를 안전하게 재사용할 수 있는 시점을 정확히 알 수 있습니다.

  • 통합 메모리에 관한 참고사항: Media3가 하드웨어 버퍼의 소유권을 갖게 되면 앱은 데이터를 바로 읽고 수정할 수 있습니다.

  • 이 제로 카피 접근 방식은 메모리 오버헤드를 줄이고 처리 속도를 매우 빠르게 유지합니다.

  • 새로운 편집 API를 활용하는 개발자 중 하나는 Adobe입니다.

  • Adobe Premiere팀은 Media3 편집 API를 미디어 편집 인프라의 기반으로 사용하여 Android 생태계 전반에서 호환성을 높이는 동시에 자체 커스텀 파이프라인과 WebGPU를 사용하는 데 필요한 유연성을 유지했습니다.

  • 다음으로, Media3 1.12 출시의 일환으로 새로운 효과를 출시한다는 소식을 전해드립니다.

  • 동영상 자동 개선 기능부터 살펴보겠습니다.

  • 내부적으로 아키텍처는 모듈식 설정에 의존합니다.

  • 애플리케이션이 Play 서비스 개선 모듈과 인터페이스하는 Media3 효과 Play 서비스와 통합됩니다.

  • 동영상 보정 모듈에는 톤 매핑, 노이즈 제거, 선명하게 하기, 초해상도 기능이 포함되어 있습니다.

  • 코드 몇 줄만으로 동영상 보정 기능을 파이프라인에 통합할 수 있습니다.

    val frameProcessorFactory = EnhancementSessionDecorator.Builder(/* context = */ this, baseGlFactory)
        .setTonemappingEnabled(true)
        .setDeblurAndDenoiseEnabled(true)
        .setUpscaleVideoEnabled(true)
        .build()
    
    val transformerBuilder = Transformer.Builder(/* context = */ this)
        .setFrameProcessorFactory(frameProcessorFactory)
  • EnhancementSessionDecorator.Builder를 사용하면 톤 매핑, 선명하게 하기, 업스케일링과 같은 기능을 선택적으로 사용 설정하고 결과 frameProcessorFactory 및 하드웨어 버퍼 도우미를 Transformer.Builder에 전달할 수 있습니다.

  • Media3 1.12에 추가되는 또 다른 흥미로운 효과는 스튜디오 사운드입니다.

  • 스튜디오 사운드는 동영상 오디오 트랙을 자동으로 분석하여 음성, 주변 소음, 음악과 같은 개별 스템으로 분리합니다.

  • 크리에이터는 바람 소리나 군중 소음을 쉽게 줄이면서 대화와 음악을 선명하게 강조할 수 있습니다.

  • 스튜디오 사운드를 통합하려면 효과 객체 내에 스튜디오 사운드 오디오 프로세서를 전달합니다.

    val editedMediaItem =
         EditedMediaItem.Builder(MediaItem.fromUri(media.uri))
           .setEffects(
             Effects(
               listOf(
                 soundSeparationAudioProcessor
               ),
               emptyList(), // video effects
             )
           )
           .build()
  • 이렇게 하면 오디오 프로세서가 미디어 항목에 연결됩니다.

  • 스튜디오 사운드 오디오 프로세서를 만들 때 리스너 콜백을 연결하여 onResult를 통해 분석된 오디오 채널 매핑을 수신합니다.

    val soundSeparationAudioProcessor =
          SoundSeparationAudioProcessor(
            getApplication(),
            mainExecutor,
            object : SoundSeparationAudioProcessor.Listener {
              override fun onResult(channelToAudioType: SparseIntArray) {
                for (i in 0 until channelToAudioType.size()) {
                  val channelIndex = channelToAudioType.keyAt(i)
                  val audioType = channelToAudioType.valueAt(i)
                  val label = toAudioTypeName(audioType)
                  detectedChannelVolumes.add(ChannelVolume(channelIndex, audioType, label))
                }
              }
            },
          )
  • 각 채널을 반복하여 작업 결과를 사용할 수 있을 때 채널 색인, 식별된 오디오 유형, 읽을 수 있는 라벨을 추출합니다.

  • 그럼 스튜디오 음질이 실제로 어떻게 작동하는지 들어보겠습니다.

  • 첫 번째 효과입니다. 울지 마, 자기야.

  • 저녁 식사를 위해 자르고 있습니다.

  • 배경에서 아기 울음소리가 들리는데, 이제 효과가 적용되었습니다.

  • 들으신 것처럼 스튜디오 사운드 효과 덕분에 배경에 아기 울음소리가 들리지 않았습니다.

  • 이제 메타데이터 검색에 대해 자세히 알아보겠습니다.

  • Media3를 사용하여 컨테이너 메타데이터를 읽고 프레임을 추출하는 방법을 살펴보겠습니다.

  • MetadataRetriever부터는 Kotlin 코루틴과 원활하게 통합되어 깔끔한 비차단 코드를 작성할 수 있습니다.

  • 미디어 항목당 하나의 변경 불가능한 검색기를 사용하여 더 깔끔하고 오류가 발생할 가능성이 적은 디자인을 제공합니다.

  • 모든 API 버전과 기기에서 일관되게 작동합니다.

  • 또한 다양한 미디어 형식을 기본적으로 지원합니다.

  • 다음은 MetadataRetriever를 사용하는 실제 예시입니다.

    MetadataRetriever.Builder(context, mediaItem).build().use { retriever ->
        // 1. Duration (in microseconds)
        val durationUs = retriever.retrieveDurationUs().await()
        // 2. Track formats & container metadata (reuses the same preparation pass)
        val trackGroups = retriever.retrieveTrackGroups().await()
        for (i in 0 until trackGroups.length) {
            val format = trackGroups[i].getFormat(0)
            // Video/Audio properties: format.width, format.height, format.colorInfo …
            // Rich metadata: SlowMotionData, MotionPhotoMetadata, Mp4TimestampData …
            val metadata = format.metadata
        }
        // 3. Stream structure & manifest info
        val timeline = retriever.retrieveTimeline().await()
    }
  • Kotlin 코루틴을 사용하면 단일의 깔끔한 블록 내에서 비동기식으로 재생 시간을 가져오고 트랙 형식과 메타데이터를 검사할 수 있습니다.

  • 이제 FrameExtractor에 대해 이야기해 보겠습니다.

  • 미디어 항목에서 디코딩된 동영상 프레임을 빠르고 효율적으로 가져올 수 있습니다.

  • 디코더와 같은 기본 리소스를 자동으로 관리하고, 시각 효과나 HDR 톤 매핑을 적용할 수 있으며, 세밀한 탐색 제어 기능을 제공하여 필요한 정확한 프레임을 가져올 수 있습니다.

  • 프레임 추출은 코드에서 다음과 같이 표시됩니다.

    FrameExtractor.Builder(context, mediaItem)
        .setSeekParameters(SeekParameters.CLOSEST_SYNC) // Fast keyframe seeking
        .setEffects(listOf(Presentation.createForHeight(360))) // GPU downscaling
        .setExtractHdrFrames(true) // Optional: HLG HDR or UltraHDR
        .build()
        .use { extractor ->
            // Representative thumbnail
            val thumbnail = extractor.getThumbnail().await()
            // Frame at specific timestamp (bitmap + actual presentationTimeMs)
            val frame = extractor.getFrame(positionMs = 5_000L).await()
        }
  • 탐색 매개변수, GPU 다운스케일링, 효과, HDR 지원과 같은 옵션으로 인스턴스를 구성한 다음, 깔끔한 정지 호출을 사용하여 썸네일과 특정 프레임을 비동기식으로 기다립니다.

  • 이제 도노반에게 넘기겠습니다.

스트리밍 표준 및 새로운 미디어 형식

  • 감사합니다. Kristina

  • 이제 Media3에 추가된 기본 스트리밍 기술 지원에 대해 이야기해 보겠습니다. 여기에는 Eclipsa 동영상 및 Eclipsa 오디오와 같은 형식에 대한 지원이 포함됩니다.

  • 따라서 Media3의 핵심 노력은 스트리밍 표준보다 앞서 나가는 것입니다. 그래야 새로운 사양이 출시될 때마다 미디어 파이프라인을 다시 작성할 필요가 없습니다.

  • 최근에 출시된 몇 가지 예를 살펴보겠습니다.

  • 콘텐츠 조정 기능이 있어 스트리밍 중에 백엔드 CDN 경로를 동적으로 전환할 수 있습니다.

  • 원활한 개인 맞춤 광고 삽입을 위해 서버 가이드 광고 삽입을 지원합니다.

  • 또한 전체 AV1 HDR 지원을 포함하여 여러 진입점에서 AV1 지원을 도입했습니다.

  • 고품질 캡처 및 편집을 위한 Android 전문가용 동영상 지원이 제공됩니다.

  • JPEG 및 HEIC 형식의 모션 사진 지원도 추가됩니다.

  • 물론 Eclipsa 동영상과 Eclipsa 오디오에 대한 지원도 추가하고 있습니다. 이에 대해서는 곧 자세히 설명해 드리겠습니다.

  • 다행히 기기 지원이 출시됨에 따라 이 모든 기능이 자동으로 지원됩니다.

  • 이제 HDR의 다음 진화 단계인 Eclipsa 동영상과 이 기술이 해결하려는 핵심 문제를 살펴보겠습니다.

  • 이전 HDR 형식은 설명 메타데이터를 제공하여 장면이 어떻게 보여야 하는지 결정했지만 디스플레이에 렌더링 방법을 알려주지는 않았습니다.

  • 먼저 이 점을 명확히 해 두겠습니다.

  • 환경에 따라 콘텐츠를 선명하고 편안하게 볼 수 있도록 평균 화면 밝기에 대한 권장사항이 다릅니다.

  • 따라서 어두운 방에서는 이 범위가 100~300니트 사이가 됩니다.

  • 그리고 물론 더 밝은 환경으로 이동하면 화면도 그에 맞춰 더 밝아져야 콘텐츠를 선명하게 볼 수 있습니다. 직사광선 아래에서 휴대전화를 볼 때는 콘텐츠를 볼 수 있도록 화면이 1,000, 2,000니트 또는 그 이상으로 밝아져야 할 수도 있습니다.

  • 기존 SDR은 100~300 범위에서 작동합니다.

  • 물론 HDR은 화면을 전체적으로 밝게 만드는 것이 아닙니다.

  • 다이내믹 레인지를 확장하여 불, 햇빛과 같은 요소에 현지화된 하이라이트를 적용하려고 합니다.

  • 하지만 스마트폰은 어두운 방이나 밝은 해변 등 어디든 가지고 다니기 때문에 상황이 조금 복잡해집니다.

  • 여기에 더해 많은 앱이 혼합 피드에서 SDR UI 요소와 SDR 콘텐츠, HDR 콘텐츠를 혼합하고 있으며, 이로 인해 특히 밤늦게 스크롤할 때 눈부심 효과가 발생할 수 있습니다.

  • HDR이 왜 가치 있는지 알 수 있죠?

  • 더 많은 환경에서 콘텐츠를 멋지게 보여줄 수 있습니다.

  • HDR을 사용 설정하면 사용자 참여도가 증가하는 것으로 나타났습니다.

  • 하지만 기존 HDR 형식은 이러한 환경을 고려하지 않는다는 문제가 있습니다.

  • 이러한 점에서 Eclipsa Video가 진가를 발휘합니다.

  • 웃음으로 인해 잠시 멈춤

  • 감사합니다.

  • 감사합니다.

  • 따라서 Eclipsa Video는 설명 메타데이터 대신 규범적 메타데이터를 제공합니다.

  • 이러한 노력은 세 가지 주요 요소를 기반으로 합니다.

  • 첫 번째 핵심 요소는 기준 흰색 앵커입니다.

  • 이렇게 하면 표준 밝기가 디스플레이의 주변 조정 SDR 화이트 포인트로 고정되어 갑작스러운 밝기 급증이 방지됩니다.

  • 두 번째 핵심 요소는 크리에이터 SDR 기준입니다.

  • 이렇게 하면 크리에이터의 정확한 톤 매핑 곡선이 SDR로 다시 보존됩니다.

  • 세 번째는 헤드룸 적응형 게인 곡선입니다. 이 곡선은 디스플레이의 실시간 HDR 헤드룸에 맞게 하이라이트를 결정론적으로 조정하여 콘텐츠가 표시되는 환경과 일치하도록 합니다.

  • 이 기능은 Android 17 이상을 실행하는 기기의 Media3 1.11에서 ExoPlayer를 통해 기본적으로 제공됩니다.

  • Eclipsa Video는 업계 간 호환성을 위해 개방형 표준을 기반으로 빌드되었습니다.

  • 따라서 지금이 HDR을 지원하기에 가장 좋은 시기입니다.

  • 오디오 측면에서는 오픈소스, 로열티 없는 3D 서라운드 오디오 형식인 Eclipsa 오디오가 있습니다.

  • 이는 Alliance for Open Media의 IAMF 사양을 기반으로 합니다.

  • 장면 기반 앰비소닉스와 채널 기반 오디오를 여러 혼합 프레젠테이션이 포함된 단일 스트림으로 결합합니다.

  • 따라서 대화 향상과 같은 객체와 유사한 조작이 가능하며 스테레오 헤드폰부터 서라운드 사운드 설정까지 모든 환경에서 작동합니다.

  • Media3에는 실제로 두 가지 재생 경로가 있습니다.

  • Android 17 이상에서는 ExoPlayer와 Android 오디오 프레임워크가 Eclipsa 오디오를 자동으로 렌더링합니다.

  • Android 16 이전 버전에서도 옵션이 있습니다.

  • 선택적 Media3 부가기능 라이브러리를 통해 IAMF 확장 프로그램을 사용 설정할 수 있습니다.

  • 제작 측면에서는 Media3 멀티플렉서를 사용하여 IAMF 오디오 트랙을 작성할 수 있습니다.

성능 및 배터리 최적화

  • 하지만 고품질 형식을 지원하는 것만으로는 충분하지 않습니다.

  • 이러한 품질을 효율적으로 제공해야 합니다.

  • 이제 Media3가 파이프라인을 더 빠르고 효율적으로 만드는 방법에 대해 이야기해 보겠습니다.

  • 오늘 말씀드린 많은 내용과 마찬가지로, 이러한 성능 개선사항 대부분은 Media3 버전을 최신 상태로 유지하기만 하면 바로 사용할 수 있습니다.

  • 예를 들어 PCM 오디오 오프로드 기능은 오디오 재생을 저전력 처리 장치로 라우팅할 수 있습니다.

  • 초기 분석 결과, 화면이 꺼진 상태에서 오디오를 재생할 때 전력이 최대 20% 절약되는 것으로 나타났습니다.

  • PCM 오디오 오프로드 외에도 ExoPlayer의 내부 렌더링 루프 절전 모드 해제를 VSYNC 및 디코더 버퍼 가용성과 일치시키는 동적 예약이 있습니다.

  • 또한 렌더러 사전 준비 기능도 제공합니다. 이 기능을 사용하면 보조 동영상 렌더러를 사전 준비하여 재생목록을 즉시 전환하고 광고를 더 쉽게 삽입할 수 있습니다.

  • 또한 정지된 플레이어 감지 및 사전 캐싱 기능이 있어 정지된 버퍼와 렌더러를 자동으로 감지하고 사전 캐시된 메모리를 깔끔하게 바인딩합니다.

  • 또 다른 최적 모드는 스크러빙 모드입니다.

  • 이는 사용자가 콘텐츠를 자주 스크러빙하는 앱에 적합합니다.

  • 따라서 스크러빙 모드를 사용 설정하면 여기에서 볼 수 있듯이 탐색 메뉴를 드래그할 때 훨씬 더 반응이 빠르고 부드러워집니다.

  • 참고로 스크러빙 모드가 사용 중지된 상태입니다.

  • 오른쪽에는 스크러빙 모드가 사용 설정되어 있습니다.

  • 감사합니다.

  • 감사합니다.

  • 좋습니다.

  • 이제 Android 시스템 상태에 대해 알아보겠습니다.

  • 공유 RAM, CPU, 배터리 리소스를 존중해야 하므로 Android에서 오디오 강화 및 포그라운드 서비스에 관한 규칙을 강화함에 따라 자체 원시 서비스와 비트맵을 관리하는 앱에서 오류가 발생하는 경우가 많습니다.

  • 포그라운드 서비스 시작이 허용되지 않는 예외나 바인더 메모리 부족 예외가 발생할 수 있습니다.

  • 따라서 오래된 포그라운드 서비스 인텐트를 방지하고 아트워크 메모리를 바인딩하는 등 다양한 방식으로 미디어 서비스 세션과 미디어 알림 관리자, 비트맵 파이프라인을 강화했습니다.

  • 하지만 Media3를 사용하고 표준 가이드를 따르는 경우 이러한 시스템 상태 제한사항은 모두 영향을 미치지 않고 처리됩니다.

  • 마지막으로 Media3에 곧 출시될 기능에 관해 이야기하겠습니다.

  • 동영상을 인코딩하거나 내보낼 때 하드웨어 다양성이 가장 큰 과제 중 하나가 될 수 있습니다.

  • 많은 앱이 일률적인 코덱 구성을 사용하므로 품질과 성능이 최적화되지 않습니다.

  • 이것이 바로 CodecDB를 만든 이유입니다.

  • CodecDB는 Android 기기 전반의 표준화된 품질 측정항목과 하드웨어 벤치마킹을 사용하여 광범위한 실제 동영상 측정을 기반으로 빌드됩니다.

  • 실제로 어떻게 작동하는지 살펴보겠습니다.

  • 왼쪽에는 휴대전화로 촬영한 동영상의 프레임이 있습니다. 이 동영상은 여러 기기에서 사용할 수 있는 매우 일반적인 동영상 구성을 사용했습니다.

  • 오른쪽에는 CodecDB에서 가져온 기기 수준의 최적 구성이 사용되었습니다.

  • 그렇다면 차이점은 무엇일까요?

  • 세부정보가 많은 영역을 보면 품질 차이가 가장 크게 나타납니다.

  • 나무와 관목의 가지가 얼마나 선명한지, 건물의 가장자리가 얼마나 선명한지 살펴보세요.

  • 따라서 CodecDB를 활용하여 특정 기기의 기능을 활용하는 훨씬 더 나은 시청 환경을 얻을 수 있습니다.

  • 코드에서 CodecDB를 사용하는 방법은 매우 간단합니다.

    val config = CodecDb.onDevice(context)
       .forEncodingFallingBackTo {
               constraints -> createFallbackConfig(constraints)
       }
       .findOptimalSettings()
       .forEditing()
       .withResolution(1920, 1080, /* frameRate= */ 30.0)
       .withAnyCodec()
       .targetingQuality(VideoEncoder.QUALITY_LEVEL_VERY_GOOD)
       .get()
  • 모든 필수 매개변수를 안내하는 유연한 스테이지 빌더 패턴을 사용합니다.

  • 따라서 사용 사례를 지정합니다.

  • 여기서는 수정에 사용합니다.

  • 이렇게 하면 Media3 내보내기 또는 트랜스코딩 파이프라인을 사용 중임을 CodecDB에 알릴 수 있습니다.

  • 설정할 수 있는 다른 사용 사례로는 카메라 캡처가 있습니다.

  • 타겟 해상도와 프레임 속도도 전달합니다.

  • 그런 다음 1080p 동영상에 초당 8메가비트가 필요한지 아니면 초당 15메가비트가 필요한지 추측하는 대신 CodecDB에 사용하려는 코덱을 알려주고 지각 품질 목표를 요청합니다.

  • 이 경우 품질 수준을 '매우 좋음'으로 사용합니다.

  • 내부적으로 CodecDB는 해당 SOC의 경험적 벤치마크 곡선을 조회하고 이상적인 코덱 및 인코더 매개변수를 반환합니다.

  • CodecDB는 곧 Media3에서 출시될 예정이며, 출시되면 네이티브 Media3 유형을 사용하게 됩니다.

  • 따라서 기존 Media3 코드와 쉽게 통합할 수 있습니다.

요약 및 결론

  • 좋습니다.

  • 오늘 Media3에서 살펴본 놀라운 기능이 많았으니 요약해 보겠습니다.

  • 오늘 우리는 새로운 Compose UI 모듈, 새로운 CastPlayer, 개선된 편집 파이프라인, Media3에서 사용할 수 있는 다양한 새로운 효과에 대해 알아봤습니다.

  • 메타데이터와 프레임을 검색하는 새로운 도구도 확인했습니다.

  • 새로운 스트리밍 기술 지원과 성능 향상, 유틸리티도 살펴봤습니다.

  • Media3는 오픈소스 라이브러리이므로 GitHub 저장소에서 이러한 모든 기능을 구현한 방법을 실제로 확인할 수 있습니다.

  • 현재 Media3의 최신 안정화 버전은 1.11.1입니다.

  • 현재 알파 버전인 1.12.0도 확인할 수 있습니다.

  • 이 짧은 링크를 따라가면 Media3에 관한 개발자 문서로 이동합니다.

  • 감사합니다. 남은 ADS도 즐겁게 시청하시기 바랍니다.

코드

코드 스니펫

'Jetpack Compose를 사용한 미디어 UI'의 2분 31초 지점에 있는 Kotlin 스니펫
@Composable
fun videoScreen() {
  Player(viewModel.exoPlayer)
}
'Jetpack Compose를 사용한 미디어 UI'의 04:44에 나오는 Kotlin 스니펫
Player(viewModel.exoPlayer)
'Jetpack Compose를 사용한 미디어 UI'의 04:48에 나오는 Kotlin 스니펫
Player(
  player = viewModel.exoPlayer,
  bottomControls = {
    PlayerDefaults.BottomControls(
      player,
      left = {}, right = {},
      above = {
        PositionAndDurationText(
          player, separator = " • ")
      },
    )
  },
)
'Jetpack Compose를 사용한 미디어 UI'의 5분 13초 지점에 있는 Kotlin 스니펫
Player(
  player = viewModel.exoPlayer,
  centerControls = {},
  bottomControls = { ...
      above = {
        Row(Modifier.fillMaxWidth()) {
          PositionAndDurationText(...)
          Spacer(Modifier.weight(1f))
          PlaybackSpeedToggleButton(player)
          MuteButton(player)
          PlayPauseButton(player)
        }
      },
    )
  }
'Jetpack Compose를 사용한 미디어 UI'의 05:51에 나오는 Kotlin 스니펫
Player(
  ...
  bottomControls = { ...
      above = {
          ...
          MuteButton(player,
            painter = { painterResource(
              if (showMuted)
                R.drawable.muted
              else
                R.drawable.unmuted )})
      }
  },
)
'MediaSession으로 재생 공유'의 07:01에 나오는 Kotlin 스니펫
MediaSession.Builder(context, player).build()
'MediaSession으로 재생 공유'의 7분 17초 지점에 있는 Kotlin 스니펫
class PlaybackService : MediaSessionService() {

  private lateinit var mediaSession:
        MediaSession

  override fun onGetSession(
        info: ControllerInfo) : MediaSession?
    = mediaSession
}
'MediaSession으로 재생 공유'의 8분 25초 지점에 있는 Kotlin 스니펫
val player = ExoPlayer.Builder(context).build()

mediaSession = MediaSession.Builder(context,player)
.build()
'MediaSession으로 재생 공유'의 8분 29초 지점에 있는 Kotlin 스니펫
Cast.getSingletonInstance(context).initialize()

val player = CastPlayer.Builder(context).build()

mediaSession = MediaSession.Builder(context,player)
.build()
'MediaSession으로 재생 공유'의 8분 45초 지점에 있는 Kotlin 스니펫
Cast.getSingletonInstance(context).initialize()
val exoPlayer = ExoPlayer.Builder(context).build()
val player = CastPlayer.Builder(context)
                .setLocalPlayer(exoPlayer).build()
mediaSession = MediaSession.Builder(context,player)
.build()
'MediaSession으로 재생 공유'의 09:32에 나오는 Kotlin 스니펫
Player(
  exoPlayer,
  centerControls = {}
  bottomControls = { /* customized */ },
)
'MediaSession으로 재생 공유'의 09:40에 나오는 Kotlin 스니펫
Player(
  castPlayer,
  centerControls = {}
  bottomControls = { /* customized */ },
  topControls = {
    PlayerDefaults.TopControls(castPlayer) {
      MediaRouteButton(
        Modifier.align(Alignment.TopEnd))
    }
  },
)
'Media3 편집 API'의 13:02에 나오는 Java 스니펫
public interface FrameProcessor
    extends AutoCloseable {

  interface Listener {
    void onWakeup();
    void onError(
         VideoFrameProcessingException exception);
    void onFrameProcessed(
         Frame frame,
         @Nullable SyncFenceWrapper onCompleteFence);
  }

  interface Factory {
    FrameProcessor create(
         FrameWriter output,
         Executor listenerExecutor,
         Listener listener);
  }

  boolean queue(List<AsyncFrame> frames);

  void signalEndOfStream();
}
'Media3 editing APIs'의 14:45에 나오는 Kotlin 스니펫
val frameProcessorFactory = EnhancementSessionDecorator.Builder(/* context = */ this, baseGlFactory)
    .setTonemappingEnabled(true)
    .setDeblurAndDenoiseEnabled(true)
    .setUpscaleVideoEnabled(true)
    .build()

val transformerBuilder = Transformer.Builder(/* context = */ this)
    .setFrameProcessorFactory(frameProcessorFactory)
'Media3 편집 API'의 15:36에 나오는 Kotlin 스니펫
val editedMediaItem =
     EditedMediaItem.Builder(MediaItem.fromUri(media.uri))
       .setEffects(
         Effects(
           listOf(
             soundSeparationAudioProcessor
           ),
           emptyList(), // video effects
         )
       )
       .build()
'Media3 편집 API'의 15:50에 있는 Kotlin 스니펫
val soundSeparationAudioProcessor =
      SoundSeparationAudioProcessor(
        getApplication(),
        mainExecutor,
        object : SoundSeparationAudioProcessor.Listener {
          override fun onResult(channelToAudioType: SparseIntArray) {
            for (i in 0 until channelToAudioType.size()) {
              val channelIndex = channelToAudioType.keyAt(i)
              val audioType = channelToAudioType.valueAt(i)
              val label = toAudioTypeName(audioType)
              detectedChannelVolumes.add(ChannelVolume(channelIndex, audioType, label))
            }
          }
        },
      )
'Media3 편집 API'의 17:29에 있는 Kotlin 스니펫
MetadataRetriever.Builder(context, mediaItem).build().use { retriever ->
    // 1. Duration (in microseconds)
    val durationUs = retriever.retrieveDurationUs().await()
    // 2. Track formats & container metadata (reuses the same preparation pass)
    val trackGroups = retriever.retrieveTrackGroups().await()
    for (i in 0 until trackGroups.length) {
        val format = trackGroups[i].getFormat(0)
        // Video/Audio properties: format.width, format.height, format.colorInfo …
        // Rich metadata: SlowMotionData, MotionPhotoMetadata, Mp4TimestampData …
        val metadata = format.metadata
    }
    // 3. Stream structure & manifest info
    val timeline = retriever.retrieveTimeline().await()
}
'Media3 editing APIs'의 18:06에 나오는 Kotlin 스니펫
FrameExtractor.Builder(context, mediaItem)
    .setSeekParameters(SeekParameters.CLOSEST_SYNC) // Fast keyframe seeking
    .setEffects(listOf(Presentation.createForHeight(360))) // GPU downscaling
    .setExtractHdrFrames(true) // Optional: HLG HDR or UltraHDR
    .build()
    .use { extractor ->
        // Representative thumbnail
        val thumbnail = extractor.getThumbnail().await()
        // Frame at specific timestamp (bitmap + actual presentationTimeMs)
        val frame = extractor.getFrame(positionMs = 5_000L).await()
    }
'성능 및 배터리 최적화'의 28:26에 있는 Kotlin 스니펫
val config = CodecDb.onDevice(context)
   .forEncodingFallingBackTo {
           constraints -> createFallbackConfig(constraints)
   }
   .findOptimalSettings()
   .forEditing()
   .withResolution(1920, 1080, /* frameRate= */ 30.0)
   .withAnyCodec()
   .targetingQuality(VideoEncoder.QUALITY_LEVEL_VERY_GOOD)
   .get()