Androidアプリにおけるオーディオとビデオの同期


Androidアプリにおけるオーディオとビデオの同期

オーディオとビデオの正確な同期は、メディア再生における重要なパフォーマンス指標の1つです。通常、同じデバイスに同時に記録されたオーディオとビデオは、テレビやモニターなどで再生されるときも同時に再生される必要があります。Android APIレベル19以上を実行するデバイスでオーディオとビデオを正しく同期させるには、以降のガイドラインに従ってください。

オーディオとビデオの同期の理論

一度に処理されるデータの最小単位を、フレームと呼びます。 オーディオとビデオのストリームはいずれもフレームに分割され、すべてのフレームは特定のタイムスタンプの時点で提示されるようにマークされます。オーディオとビデオは別々にダウンロードおよびデコードできますが、一致するタイムスタンプを持つオーディオフレームとビデオフレームは一緒に提示される必要があります。理論的には、オーディオとビデオの処理を一致させる必要がある場合、オーディオとビデオを同期するためのソリューションは3つあります。

  • オーディオフレームを継続的に再生する: オーディオの再生位置を時間のマスターリファレンスとして使用して、ビデオの再生位置をそれに合わせます。
  • システム時刻をリファレンスとして使用する: オーディオとビデオの両方の再生をシステム時刻に合わせます。
  • ビデオの再生をリファレンスとして使用する: オーディオをビデオに合わせます。

1つ目は、オーディオフレームの提示時刻、再生速度、持続時間を調整せずにオーディオデータを継続的に再生する唯一の方法です。これらのパラメーターを調整すると人の耳に感知されやすく、オーディオの再サンプリングを行わないと不快なノイズが発生する原因になります。一方で、再サンプリングを行うと音程が変わってしまいます。したがって、一般的なマルチメディアアプリでは、オーディオの再生位置を時間のマスターリファレンスとして使用する必要があります。以下の段落では、このソリューションについて説明します(ほかの2つの方法は、このドキュメントでは扱いません)。

アプリにおけるオーディオとビデオの同期の維持

オーディオパイプラインとビデオパイプラインは、一致するタイムスタンプを持つフレームを同時にレンダリングする必要があります。オーディオの再生位置が時間のマスターリファレンスとして使用され、ビデオパイプラインは単純に、最後にレンダリングされたオーディオフレームと一致するビデオフレームを出力します。どのような実装においても、最後にレンダリングされたオーディオのタイムスタンプを正確に計算することが不可欠です。Androidでは、オーディオパイプラインのさまざまな段階でオーディオのタイムスタンプとレイテンシを問い合わせるためのAPIがいくつか用意されています。以下のガイダンスで、ベストプラクティスについて説明します。

1.カスタム仕様のメディアプレーヤーを使用する場合

カスタム仕様のメディアプレーヤーでは、アプリがオーディオとビデオのデータフローを完全に制御し、オーディオとビデオのパケットのデコードにかかる時間を把握します。また、継続的な再生を維持するために、バッファーされるビデオデータの量を自在に増減できます。ビデオパイプラインを、オーディオパイプラインでレンダリングされたタイムスタンプに合わせる必要があります。次の2つのAPIを使用します。

1.1 AudioTrack.getTimestamp()(APIレベル19以上)

このオーディオパイプラインで、最後にレンダリングされたタイムスタンプの問い合わせがサポートされている場合は、getTimestamp()メソッドを使用すると目的の値を簡単に取得できます。タイムスタンプが利用可能な場合、AudioTimestampインスタンスにはフレーム単位での位置と、そのフレームが提示された推定時刻が入ります。この情報を使用してビデオパイプラインを制御し、ビデオフレームをオーディオフレームと一致させることができます。

以下に注意してください。

  • タイムスタンプを問い合わせる頻度は、10秒~1分に1回とすることを推奨します。少しのずれが生じる可能性はありますが突然の変更はないと考えられるため、それより頻繁にタイムスタンプを問い合わせる必要はありません。
  • タイムスタンプが正しく返される場合、アプリはハードコーディングされたオフセット値を追加で使用せずに、返された値を信頼する必要があります。実験値の追加はしないことを強くお勧めします。実験値はプラットフォームに依存し、(Bluetoothシンクが接続されたときなどに)いつでもパイプラインが更新される可能性があるため、以前は正しかった値が不正確になる場合があります。

使用可能なパラメーターや戻り値などの詳細については、AndroidドキュメントのgetTimestamp()メソッドを参照してください。

1.2 getPlaybackHeadPosition()(APIレベル3以上)

前のセクションで説明した、最後にレンダリングされたオーディオのタイムスタンプの問い合わせがオーディオパイプラインでサポートされていない場合は、別のアプローチが必要です。

このソリューションは、AudioTrackクラスの2つの関数を使用する2つの部分で構成されます。最初の部分では、メソッドgetPlaybackHeadPosition()から返されたフレーム単位での現在のヘッド位置に基づいて、最新のオーディオのタイムスタンプを計算します。

private long framesToDurationUs(long frameCount) {
    return (frameCount * C.MICROS_PER_SECOND) / sampleRate;
}

long timestamp = framesToDurationUs(audioTrack.getPlaybackHeadPosition());

上で計算されたtimestamp値には下位レイヤーで発生したレイテンシは含まれないため、いくつかの調整が必要になります。

このソリューションの2つ目の部分では、関数getLatency()を使用して、含まれなかったレイテンシ値を求めます。getLatency()メソッドはAudioTrackクラスの非表示のメンバーである(公式SDKには含まれない)ため、アクセスするにはリフレクションが必要です。

Method getLatencyMethod;
if (Util.SDK_INT >= 18) {
  try {
    getLatencyMethod =
     android.media.AudioTrack.class.getMethod("getLatency", (Class < ? > []) null);
   } catch (NoSuchMethodException e) {
      // このメソッドが存在するという保証はありません。何もしません。
   }
}`

返される値には、ミキサー、オーディオハードウェアドライバーのレイテンシと、AudioTrackバッファーによって発生したレイテンシが含まれます。AudioTrackの下のレイヤーのレイテンシのみを取得するには、バッファーによって発生したレイテンシ(bufferSizeUs)を減算する必要があります。

long bufferSizeUs = isOutputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET;
int audioLatencyUs = (Integer) getLatencyMethod.invoke(audioTrack, (Object[]) null) * 1000L - bufferSizeUs;

2つの部分を組み合わせた、オーディオパイプラインでレンダリングされた最新のタイムスタンプの最も近い概算値を計算するためのソリューションの全体は次のとおりです。

int latestAudioFrameTimestamp = framesToDurationUs(audioTrack.getPlaybackHeadPosition() - audioLatencyUs;

実装の例は、ExoPlayerのAmazon版AudioTrackPositionTrackerクラスでご確認いただけます。

2.ExoPlayerの使用

Fire OSでのメディア再生にはExoPlayerを使用することを強くお勧めします。ExoPlayerのAmazon版は全世代のFire TVデバイスと互換性があり、追加の修正プログラムも数多く提供されています。また、Amazon以外のプラットフォームでのExoPlayerの元の動作を変更することも避けられます。

オーディオとビデオの同期に関しては、ExoPlayerのAmazon版は前のセクションで説明した方法を使用して、「APIレベル21」より前のAmazonデバイスに対してもオーディオレイテンシの正しい計算を維持します。これをメディアプレイヤーとして使用する場合、同期は自動的に実行されます。レイテンシに合わせてタイムスタンプを手動で調整する必要はありません。

3.標準のAndroid MediaPlayerの使用

Fire OSでは、オーディオとビデオの再生を処理する標準のAndroid MediaPlayerクラスがサポートされています。これらのメディアクラスは、オーディオとビデオの同期の要件に従って基本的なメディア再生を処理できます。ただし、その機能は多くの点で制限されています。代わりに、ExoPlayerのAmazon版(または有料のメディアプレーヤーオプション)を使用することを強くお勧めします。

4.Android NDKのOpenSL ESフレームワークの使用

OpenSL ESが標準のAPIを使用してオーディオレイテンシを問い合わせると、AudioFlingerによって報告されたハードウェアのオーディオレイテンシのみが返されます。(主にオーディオトラックバッファーによって)ソフトウェアで発生したレイテンシは含まれません。ハードウェアとソフトウェアの両方の遅延を含む正確なオーディオレイテンシ値を取得するために、Fire OS 6ではOpenSL ES API android_audioPlayer_getConfig()がアップデートされ、完全なオーディオレイテンシを報告できるようになりました。

以下のコードサンプルでは、これらの関数を使用して、ソフトウェアとハードウェアの両方のレイヤーで発生したレイテンシ値を計算する方法を示します。

4.1 オーディオプレーヤーオブジェクトがCAudioPlayerタイプの場合

// OpenSLフレームワークを使用して、Fire OSのソフトウェア+ハードウェアのオーディオレイテンシを取得するためのコード例
SLuint32 audioLatency = 0;
SLuint32 valueSize = 0;

// 変数apはCAudioPlayerタイプのオーディオプレーヤーオブジェクトです。
if (android_audioPlayer_getConfig((CAudioPlayer * ) & ap, (const SLchar * )
  "androidGetAudioLatency",
  (SLuint32 * ) &valueSize, (void *) &audioLatency) == SL_RESULT_SUCCESS) {
    // ハードウェア+ソフトウェアのオーディオレイテンシは`SLuint32`タイプの変数audioLatencyに入ります。
} else {
    // 現在のget_audio_latency APIを呼び出します。ハードウェアのオーディオレイテンシ値のみを問い合わせます。
}

4.2 オーディオプレーヤーオブジェクトがSLEngineItfインターフェイスAPI CreateAudioPlayer()で作成されている場合

オーディオプレーヤーが次の方法で作成された場合、

result = (*engine)->CreateAudioPlayer(engine, &playerObject, &audioSrc, &audioSink, NUM_INTERFACES, ids, req);

以下のサンプルコードを使用すると、作成されたオーディオプレーヤーの全体的なレイテンシを取得できます。変数playerObjectは、メソッドCreateAudioPlayer()が呼び出されたときと同じインスタンスを指している必要があります。

// OpenSLフレームワークを使用して、Fire OSのソフトウェア+ハードウェアのオーディオレイテンシを取得するためのコード例

// レイテンシの問い合わせインターフェイスがサポートされたplayerObjectを作成します
// CreateAudioPlayerでSL_IID_ANDROIDCONFIGURATIONを含めるようにリクエストします
const SLInterfaceID ids[] = { SL_IID_ANDROIDCONFIGURATION };
const SLboolean req[] = { SL_BOOLEAN_TRUE };
SLint32 result = 0;

result = (*engine)->CreateAudioPlayer(engine, &playerObject,
              &audioSrc, &audioSink, 1 /* size of ids & req array */, ids, req);

if (result != SL_RESULT_SUCCESS) {
    ALOGE("CreateAudioPlayer failed with result %d", result);
    return;
}

SLAndroidConfigurationItf playerConfig;
SLuint32 audioLatency = 0;
SLuint32 paramSize = sizeof(`SLuint32`);

// レイテンシの問い合わせの前にplayerObjectのリアライズが必要です
result = (*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE);

if (result != SL_RESULT_SUCCESS) {
    ALOGE("playerObject realize failed with result %d", result);
    return;
}

// オーディオプレーヤーのインターフェイスを取得
result = (*playerObject)->GetInterface(playerObject,
                                       SL_IID_ANDROIDCONFIGURATION,
                                       &playerConfig);
if (result != SL_RESULT_SUCCESS) {
    ALOGE("config GetInterface failed with result %d", result);
    return;
}

// オーディオプレーヤーのレイテンシを取得
 result = (*playerConfig)->GetConfiguration(playerConfig,
           (const SLchar * )"androidGetAudioLatency", &paramSize, &audioLatency);

if (result == SL_RESULT_SUCCESS) {
    // ハードウェア+ソフトウェアのオーディオレイテンシは変数audioLatencyのSLuint32タイプに入ります。
} else {
    // 現在のget_audio_latency APIを呼び出します。ハードウェアのオーディオレイテンシ値のみ取得できます。
}

そのほかのリソース

オーディオとビデオの同期についてさらに詳しく学習するには、以下の外部リソースが役立ちます。