멀티미디어 터널링

멀티미디어 터널링은 앱 코드 또는 Android 프레임워크 코드로 처리하지 않고도 압축된 동영상 데이터를 하드웨어 동영상 디코더를 통해 디스플레이로 직접 터널링할 수 있는 기술입니다. Android 스택 아래의 기기별 코드가 동영상 프레임 프레젠테이션 타임스탬프를 다음 유형의 내부 클럭 중 하나와 비교하여 디스플레이에 전송할 동영상 프레임과 그 전송 시점을 결정합니다.

  • Android 5 이상에서 주문형 동영상을 재생하기 위해, 앱에 의해 전달된 오디오 프레젠테이션 타임스탬프에 동기화된 AudioTrack 클럭

  • Android 11 이상에서 실시간 방송을 재생하기 위해 튜너에 의해 구동되는 PCR(Program Clock Reference) 또는 STC(System Time Clock)

배경

Android의 기존 동영상 재생에서는 압축된 동영상 프레임이 디코딩되면 앱에 알림을 보냅니다. 그러면 앱은 디코딩된 동영상 프레임을 그에 상응하는 오디오 프레임과 같은 시스템 클럭 시간에 렌더링되도록 디스플레이에 보내고 이전 AudioTimestamps 인스턴스를 가져와 올바른 타이밍을 계산합니다.

터널링된 동영상 재생은 앱 코드를 우회하고 동영상에 작용하는 프로세스 수를 줄이기 때문에, OEM 구현에 따라 보다 효율적인 동영상 렌더링을 제공할 수 있습니다. 또한 이 재생에서는 동영상 렌더링을 위한 Android 요청 타이밍과 실제 하드웨어 vsync 타이밍 간의 차이로 인해 발생할 수 있는 타이밍 문제를 방지하기 때문에, 선택한 클럭(PRC, STC, 오디오)에 보다 정확한 동영상 주기와 동기화를 제공할 수도 있습니다. 그러나 터널링은 PIP 모드 창에서 흐리게 처리 또는 둥근 모서리 같은 GPU 효과 지원을 줄일 수도 있습니다. 이는 버퍼가 Android 그래픽 스택을 우회하기 때문입니다.

다음 다이어그램은 터널링이 동영상 재생 프로세스를 어떻게 간소화하는지 보여줍니다.

기존 모드와 터널 모드 비교

그림 1. 기존 동영상 재생 프로세스와 터널링된 동영상 재생 프로세스 비교

앱 개발자

대부분의 앱 개발자는 재생 구현 시 라이브러리와 통합합니다. 따라서 대부분의 경우 구현 시에는 터널링된 재생을 위해 그 라이브러리를 재구성하기만 하면 됩니다. 터널링된 동영상 플레이어의 하위 수준 구현에서는 다음 안내를 따릅니다.

Android 5 이상에서 주문형 동영상 재생:

  1. SurfaceView 인스턴스를 만듭니다.

  2. audioSessionId 인스턴스를 만듭니다.

  3. 2단계에서 만든 audioSessionId 인스턴스로 AudioTrackMediaCodec 인스턴스를 만듭니다.

  4. 오디오 데이터의 첫 번째 오디오 프레임을 위한 프레젠테이션 타임스탬프로 오디오 데이터를 AudioTrack의 큐에 추가합니다.

Android 11 이상에서 실시간 방송 재생:

  1. SurfaceView 인스턴스를 만듭니다.

  2. Tuner에서 avSyncHwId 인스턴스를 가져옵니다.

  3. 2단계에서 만든 avSyncHwId 인스턴스로 AudioTrackMediaCodec 인스턴스를 만듭니다.

다음 코드 스니펫에 API 호출 흐름이 나와 있습니다.

aab.setContentType(AudioAttributes.CONTENT_TYPE_MOVIE);

// configure for audio clock sync
aab.setFlag(AudioAttributes.FLAG_HW_AV_SYNC);
// or, for tuner clock sync (Android 11 or higher)
new tunerConfig = TunerConfiguration(0, avSyncId);
aab.setTunerConfiguration(tunerConfig);
if (codecName == null) {
  return FAILURE;
}

// configure for audio clock sync
mf.setInteger(MediaFormat.KEY_AUDIO_SESSION_ID, audioSessionId);
// or, for tuner clock sync (Android 11 or higher)
mf.setInteger(MediaFormat.KEY_HARDWARE_AV_SYNC_ID, avSyncId);

주문형 동영상 재생 동작

터널링된 주문형 동영상 재생은 암시적으로 AudioTrack 재생과 결부되므로 터널링된 동영상 재생 동작은 오디오 재생 동작에 따라 달라질 수 있습니다.

  • 대부분의 기기에서는 기본적으로 오디오 재생이 시작될 때까지 동영상 프레임이 렌더링되지 않습니다. 그러나 앱은 오디오 재생을 시작하기 전에 동영상 프레임을 렌더링해야 할 수 있습니다. 예를 들어 탐색 중에 사용자에게 현재 동영상 위치를 표시하기 위해 동영상 프레임을 렌더링할 수 있습니다.

    • 큐에 추가된 첫 번째 동영상 프레임이 디코딩되는 즉시 렌더링되어야 함을 알리려면 PARAMETER_KEY_TUNNEL_PEEK 매개변수를 1로 설정합니다. 이는 압축된 동영상 프레임이 큐에서 재정렬되는 경우(예: B-프레임이 있는 경우) 가장 먼저 표시되는 동영상 프레임은 항상 I-프레임이어야 함을 의미합니다.

    • 오디오 재생이 시작될 때까지 큐에 추가된 첫 번째 동영상 프레임이 렌더링되지 않도록 하려면 이 매개변수를 0으로 설정합니다.

    • 이 매개변수가 설정되지 않으면 OEM이 기기 동작을 결정합니다.

  • 오디오 데이터가 AudioTrack에 제공되지 않고 버퍼가 비어 있는 경우(오디오 언더런) 오디오 클럭이 더 이상 앞당겨지지 않으므로 더 많은 오디오 데이터가 기록될 때까지 동영상 재생은 중단됩니다.

  • 재생 중에는 앱이 수정할 수 없는 불연속성이 오디오 프레젠테이션 타임스탬프에 표시될 수 있습니다. 이 경우 OEM은 (OEM 구현에 따라) 현재 동영상 프레임을 중단하여 음의 격차를 수정하고, 동영상 프레임을 삭제하거나 무음 오디오 프레임을 삽입하여 양의 격차를 수정합니다. 삽입된 무음 오디오 프레임의 AudioTimestamp 프레임 위치는 증가하지 않습니다.

기기 제조업체

구성

OEM은 별도의 동영상 디코더를 생성하여 터널링된 동영상 재생을 지원해야 합니다. 디코더는 media_codecs.xml에서 터널링된 재생이 가능하다는 점을 알려야 합니다.

<Feature name="tunneled-playback" required="true"/>

터널링된 MediaCodec 인스턴스가 오디오 세션 ID로 구성된 경우 이 인스턴스는 다음과 같이 AudioFlingerHW_AV_SYNC ID를 쿼리합니다.

if (entry.getKey().equals(MediaFormat.KEY_AUDIO_SESSION_ID)) {
    int sessionId = 0;
    try {
        sessionId = (Integer)entry.getValue();
    }
    catch (Exception e) {
        throw new IllegalArgumentException("Wrong Session ID Parameter!");
    }
    keys[i] = "audio-hw-sync";
    values[i] = AudioSystem.getAudioHwSyncForSession(sessionId);
}

이 쿼리 중에 AudioFlinger는 기본 오디오 기기에서 HW_AV_SYNC ID를 가져와서 내부적으로 오디오 세션 ID와 연결합니다.

audio_hw_device_t *dev = mPrimaryHardwareDev->hwDevice();
char *reply = dev->get_parameters(dev, AUDIO_PARAMETER_HW_AV_SYNC);
AudioParameter param = AudioParameter(String8(reply));
int hwAVSyncId;
param.getInt(String8(AUDIO_PARAMETER_HW_AV_SYNC), hwAVSyncId);

AudioTrack 인스턴스가 이미 생성된 경우 동일한 오디오 세션 ID와 함께 HW_AV_SYNC ID가 출력 스트림에 전달됩니다. 인스턴스가 아직 생성되지 않은 경우 AudioTrack 생성 중에 출력 스트림에 HW_AV_SYNC ID가 전달됩니다. 이 작업은 다음과 같이 재생 스레드에 의해 이루어집니다.

mOutput->stream->common.set_parameters(&mOutput->stream->common, AUDIO_PARAMETER_STREAM_HW_AV_SYNC, hwAVSyncId);

HW_AV_SYNC ID는 오디오 출력 스트림 또는 Tuner 구성에 해당하는지에 상관없이 OMX 또는 Codec2 구성요소에 전달됩니다. 그러면 OEM 코드가 코덱을 그에 상응하는 오디오 출력 스트림이나 튜너 스트림에 연결할 수 있기 때문입니다.

구성요소 구성 중에 OMX 또는 Codec2 구성요소는 코덱을 하드웨어 컴포저(HWC) 레이어와 연결하는 데 사용할 수 있는 측파대 핸들을 반환해야 합니다. 앱에서 노출 영역을 MediaCodec에 연결하면 이 측파대 핸들은 레이어를 측파대 레이어로 구성하는 SurfaceFlinger를 통해 HWC로 전달됩니다.

err = native_window_set_sideband_stream(nativeWindow.get(), sidebandHandle);
if (err != OK) {
  ALOGE("native_window_set_sideband_stream(%p) failed! (err %d).", sidebandHandle, err);
  return err;
}

HWC는 적절한 시점(예: 연결된 오디오 출력 스트림 또는 튜너 프로그램 참조 클럭에 동기화되는 시점)에 코덱 출력의 새 이미지 버퍼를 수신하여 다른 레이어의 현재 콘텐츠와 합성하고 그 결과로 생성된 이미지를 표시하는 역할을 합니다. 이는 정상적인 준비 및 설정 주기와 관계없이 발생합니다. 준비 호출과 설정 호출은 다른 레이어가 변경되거나 측파대 레이어의 속성(예: 위치 또는 크기)이 변경될 때만 발생합니다.

OMX

터널링된 디코더 구성요소는 다음을 지원해야 합니다.

  • OMX.google.android.index.configureVideoTunnelMode 확장 매개변수를 설정합니다. 이 매개변수는 ConfigureVideoTunnelModeParams 구조를 사용하여 오디오 출력 기기에 연결된 HW_AV_SYNC ID를 전달하는 역할을 합니다.

  • OMX_IndexConfigAndroidTunnelPeek 매개변수를 구성합니다. 이 매개변수는 오디오 재생의 시작 여부와 관계없이 코덱에 처음 디코딩된 동영상 프레임을 렌더링하거나 렌더링하지 않도록 지시하는 역할을 합니다.

  • 터널링된 첫 번째 동영상 프레임이 디코딩되어 렌더링될 준비가 되면 OMX_EventOnFirstTunnelFrameReady 이벤트를 전송합니다.

AOSP 구현은 다음 코드 스니펫과 같이 OMXNodeInstance를 통해 ACodec에 터널 모드를 구성합니다.

OMX_INDEXTYPE index;
OMX_STRING name = const_cast<OMX_STRING>(
        "OMX.google.android.index.configureVideoTunnelMode");

OMX_ERRORTYPE err = OMX_GetExtensionIndex(mHandle, name, &index);

ConfigureVideoTunnelModeParams tunnelParams;
InitOMXParams(&tunnelParams);
tunnelParams.nPortIndex = portIndex;
tunnelParams.bTunneled = tunneled;
tunnelParams.nAudioHwSync = audioHwSync;
err = OMX_SetParameter(mHandle, index, &tunnelParams);
err = OMX_GetParameter(mHandle, index, &tunnelParams);
sidebandHandle = (native_handle_t*)tunnelParams.pSidebandWindow;

구성요소가 이 구성을 지원하는 경우 측파대 핸들을 이 코덱에 할당하고 이를 pSidebandWindow 멤버를 통해 다시 전달해야 합니다. 그래야 HWC가 연결된 코덱을 식별할 수 있습니다. 구성요소가 이 구성을 지원하지 않는 경우 bTunneledOMX_FALSE로 설정해야 합니다.

Codec2

Android 11 이상에서 Codec2는 터널링된 재생을 지원합니다. 디코더 구성요소는 다음을 지원해야 합니다.

  • C2PortTunneledModeTuning을 구성합니다. 이는 터널 모드를 구성하고 오디오 출력 기기 또는 튜너 구성에서 가져온 HW_AV_SYNC를 전달하는 역할을 합니다.

  • C2_PARAMKEY_OUTPUT_TUNNEL_HANDLE을 쿼리하여 HWC에 측파대 핸들을 할당하고 가져옵니다.

  • C2Work에 연결된 경우 C2_PARAMKEY_TUNNEL_HOLD_RENDER를 처리합니다. 그러면 코덱에 디코딩을 시작하고 작업 완료를 알리도록 지시됩니다. 하지만 1) 코덱이 렌더링하도록 이후에 지시될 때까지 또는 2) 오디오 재생이 시작될 때까지 출력 버퍼를 렌더링하도록 지시되지는 않습니다.

  • C2_PARAMKEY_TUNNEL_START_RENDER를 처리합니다. 그러면 오디오 재생이 시작되지 않은 경우에도 C2_PARAMKEY_TUNNEL_HOLD_RENDER로 표시된 프레임을 즉시 렌더링하도록 코덱에 지시됩니다.

  • debug.stagefright.ccodec_delayed_params를 구성되지 않은 상태로 둡니다(권장됨). 개발자가 이를 구성하는 경우 false로 설정합니다.

AOSP 구현은 다음 코드 스니펫과 같이 C2PortTunnelModeTuning을 통해 CCodec에 터널 모드를 구성합니다.

if (msg->findInt32("audio-hw-sync", &tunneledPlayback->m.syncId[0])) {
    tunneledPlayback->m.syncType =
            C2PortTunneledModeTuning::Struct::sync_type_t::AUDIO_HW_SYNC;
} else if (msg->findInt32("hw-av-sync-id", &tunneledPlayback->m.syncId[0])) {
    tunneledPlayback->m.syncType =
            C2PortTunneledModeTuning::Struct::sync_type_t::HW_AV_SYNC;
} else {
    tunneledPlayback->m.syncType =
            C2PortTunneledModeTuning::Struct::sync_type_t::REALTIME;
    tunneledPlayback->setFlexCount(0);
}
c2_status_t c2err = comp->config({ tunneledPlayback.get() }, C2_MAY_BLOCK,
        failures);
std::vector<std::unique_ptr<C2Param>> params;
c2err = comp->query({}, {C2PortTunnelHandleTuning::output::PARAM_TYPE},
        C2_DONT_BLOCK, &params);
if (c2err == C2_OK && params.size() == 1u) {
    C2PortTunnelHandleTuning::output *videoTunnelSideband =
            C2PortTunnelHandleTuning::output::From(params[0].get());
    return OK;
}

구성요소가 이 구성을 지원한다면 측파대 핸들을 이 코덱에 할당한 후 C2PortTunnelHandlingTuning 멤버를 통해 다시 전달해야 합니다. 그래야 HWC가 연결된 코덱을 식별할 수 있습니다.

오디오 HAL

주문형 동영상 재생에서 오디오 HAL은 big-endian 형식의 오디오 데이터를 사용하여 오디오 프레젠테이션 타임스탬프를 인라인으로 수신합니다. big-endian 형식의 오디오 데이터는 앱이 작성한 각 오디오 데이터 블록의 시작 부분에 있는 헤더 내부에 있습니다.

struct TunnelModeSyncHeader {
  // The 32-bit data to identify the sync header (0x55550002)
  int32 syncWord;
  // The size of the audio data following the sync header before the next sync
  // header might be found.
  int32 sizeInBytes;
  // The presentation timestamp of the first audio sample following the sync
  // header.
  int64 presentationTimestamp;
  // The number of bytes to skip after the beginning of the sync header to find the
  // first audio sample (20 bytes for compressed audio, or larger for PCM, aligned
  // to the channel count and sample size).
  int32 offset;
}

HWC가 상응하는 오디오 프레임과 동기화된 동영상 프레임을 렌더링하도록 오디오 HAL은 동기화 헤더를 파싱하고 프레젠테이션 타임스탬프를 사용하여 재생 클럭을 오디오 렌더링과 다시 동기화해야 합니다. 압축된 오디오가 재생될 때 재동기화하려면 오디오 HAL이 압축된 오디오 데이터 내의 메타데이터를 파싱하여 재생 시간을 확인해야 할 수 있습니다.

일시중지 지원

Android 5 이하에서는 일시중지가 지원되지 않습니다. A/V 스타베이션을 통해서만 터널링된 재생을 일시중지할 수 있지만, 동영상의 내부 버퍼가 크다면(예: OMX 구성요소에 1초의 데이터가 있는 경우) 일시중지가 응답하지 않는 것처럼 보입니다.

Android 5.1 이상에서는 AudioFlinger가 직접(터널링된) 오디오 출력의 일시중지 및 다시 시작을 지원합니다. HAL이 일시중지와 다시 시작을 구현하면 트랙 일시중지와 다시 시작이 HAL로 전달됩니다.

일시중지, 플러시, 다시 시작 호출 시퀀스는 재생 스레드에서 HAL 호출을 실행하여 준수됩니다(오프로드와 동일).

구현 제안사항

오디오 HAL

Android 11에서는 PCR 또는 STC의 HW 동기화 ID를 A/V 동기화에 사용할 수 있으므로 동영상 전용 스트림이 지원됩니다.

Android 10 이하의 경우 터널링된 동영상 재생을 지원하는 기기에는 audio_policy.conf 파일에 FLAG_HW_AV_SYNCAUDIO_OUTPUT_FLAG_DIRECT 플래그가 있는 오디오 출력 스트림 프로필이 하나 이상 있어야 합니다. 이러한 플래그는 오디오 클럭에서 시스템 클럭을 설정하는 데 사용됩니다.

OMX

기기 제조업체는 터널링된 동영상 재생을 위한 별도의 OMX 구성요소를 보유해야 합니다. 제조업체는 보안 재생처럼 다른 오디오 및 동영상 재생 유형을 위한 추가적인 OMX 구성요소를 보유할 수 있습니다. 터널링된 구성요소는 다음을 충족해야 합니다.

  • 출력 포트에 0 버퍼(nBufferCountMin, nBufferCountActual)를 지정해야 합니다.

  • OMX.google.android.index.prepareForAdaptivePlayback setParameter 확장 프로그램을 구현해야 합니다.

  • media_codecs.xml 파일에 기능을 지정하고 터널링된 재생 기능을 선언해야 합니다. 또한 프레임 크기, 정렬 또는 비트 전송률에 관한 제한을 명확히 해야 합니다. 아래 예를 참고하세요.

    <MediaCodec name="OMX.OEM_NAME.VIDEO.DECODER.AVC.tunneled"
    type="video/avc" >
        <Feature name="adaptive-playback" />
        <Feature name="tunneled-playback" required=”true” />
        <Limit name="size" min="32x32" max="3840x2160" />
        <Limit name="alignment" value="2x2" />
        <Limit name="bitrate" range="1-20000000" />
            ...
    </MediaCodec>
    

동일한 OMX 구성요소를 사용하여 터널링된 디코딩 및 터널링되지 않은 디코딩을 지원한다면 터널링된 재생 기능을 선택사항으로 두어야 합니다. 그러면 터널링된 디코더와 터널링되지 않은 디코더 모두에 동일한 기능 제한이 설정됩니다. 아래 예를 참고하세요.

<MediaCodec name="OMX._OEM\_NAME_.VIDEO.DECODER.AVC" type="video/avc" >
    <Feature name="adaptive-playback" />
    <Feature name="tunneled-playback" />
    <Limit name="size" min="32x32" max="3840x2160" />
    <Limit name="alignment" value="2x2" />
    <Limit name="bitrate" range="1-20000000" />
        ...
</MediaCodec>

하드웨어 컴포저(HWC)

터널링된 레이어(HWC_SIDEBAND compositionType이 있는 레이어)가 디스플레이에 있을 때 레이어의 sidebandStream은 OMX 동영상 구성요소에 의해 할당된 측파대 핸들입니다.

HWC는 터널링된 OMX 구성요소의 디코딩된 동영상 프레임을 관련 오디오 트랙에 동기화합니다(audio-hw-sync ID 사용). 새 동영상 프레임이 현재 상태가 되면 HWC는 이를 마지막 준비 호출 또는 설정 호출 도중에 수신된 모든 레이어의 현재 콘텐츠와 합성하고 그 결과로 생성된 이미지를 표시합니다. 준비 호출 또는 설정 호출은 다른 레이어가 변경되거나 측파대 레이어의 속성(예: 위치 또는 크기)이 변경될 때만 발생합니다.

다음 그림에서는 하드웨어(또는 커널이나 드라이버) 싱크로나이저와 함께 작동하여 동영상 프레임(7b)을 최신 구성(7a)과 결합하고 오디오(7c)를 기반으로 정확한 시간에 표시하는 HWC를 보여줍니다.

오디오 기반으로 동영상 프레임을 결합하는 HWC

그림 2. HWC 하드웨어(또는 커널이나 드라이버) 싱크로나이저