Androidアプリにおけるオーディオとビデオの同期
オーディオとビデオの正確な同期は、メディア再生における重要なパフォーマンス指標の1つです。通常、同じデバイスに同時に記録されたオーディオとビデオは、TVやモニターなどで再生されるときも同時に再生される必要があります。Android APIレベル19以上を実行するデバイスでオーディオとビデオを正しく同期させるには、以降のガイドラインに従ってください。
- オーディオとビデオの同期の理論
- アプリにおけるオーディオとビデオの同期の維持
- 1.ExoPlayerの使用
- 2.カスタム仕様のメディアプレーヤーを使用する場合
- 3.標準のAndroid MediaPlayerの使用
- 4.Android NDKのOpenSL ESフレームワークの使用
- その他のリソース
オーディオとビデオの同期の理論
一度に処理されるデータの最小単位を、フレームと呼びます。 オーディオとビデオのストリームはいずれもフレームに分割され、すべてのフレームは特定のタイムスタンプの時点で出力されるようにマークされます。オーディオとビデオは別々にダウンロードおよびデコードできますが、一致するタイムスタンプを持つオーディオフレームとビデオフレームは一緒に出力される必要があります。理論的には、オーディオとビデオの処理を一致させる必要がある場合、オーディオとビデオを同期するためのソリューションは3つあります。
- オーディオフレームを継続的に再生する: オーディオの再生位置を時間のプライマリリファレンスとして使用して、ビデオの再生位置をそれに合わせます。
- システム時刻をリファレンスとして使用する: オーディオとビデオの両方の再生をシステム時刻に合わせます。
- ビデオの再生をリファレンスとして使用する: オーディオをビデオに合わせます。
1つ目は、オーディオフレームの出力時刻、再生速度、持続時間を調整せずにオーディオデータを継続的に再生する唯一の方法です。これらのパラメーターを調整すると人の耳に感知されやすく、オーディオの再サンプリングを行わないと不快なノイズが発生する場合があります。したがって、一般的なマルチメディアアプリでは、オーディオの再生位置を時間のプライマリリファレンスとして使用する必要があります。以下の段落では、このソリューションについて説明します(ほかの2つの方法は、このドキュメントでは扱いません)。
アプリにおけるオーディオとビデオの同期の維持
オーディオパイプラインとビデオパイプラインは、一致するタイムスタンプを持つフレームを同時にレンダリングする必要があります。オーディオの再生位置が時間のプライマリリファレンスとして使用され、ビデオパイプラインは単純に、最後にレンダリングされたオーディオフレームと一致するビデオフレームを出力します。どのような実装においても、最後にレンダリングされたオーディオタイムスタンプを正確に計算することが不可欠です。Androidでは、オーディオパイプラインのさまざまな段階でオーディオタイムスタンプとレイテンシを問い合わせるためのAPIがいくつか用意されています。以下のガイダンスで、ベストプラクティスについて説明します。
1.ExoPlayerの使用
Fire OSでのメディア再生にはExoPlayerを使用することを強くお勧めします。ExoPlayerのAmazon版は全世代のFire TVデバイスと互換性があり、追加の修正プログラムも数多く提供されています。また、Amazon以外のプラットフォームでのExoPlayerの元の動作を変更することも避けられます。
オーディオとビデオの同期に関しては、ExoPlayerのAmazon版は次のセクションで説明されている方法を使用して、「APIレベル21」より前のAmazonデバイスに対してもオーディオレイテンシの正しい計算を維持します。これをメディアプレーヤーとして使用する場合、同期は自動的に実行されます。レイテンシに合わせてタイムスタンプを手動で調整する必要はありません。
2.カスタム仕様のメディアプレーヤーを使用する場合
カスタム仕様のメディアプレーヤーでは、アプリがオーディオとビデオのデータフローを完全に制御し、オーディオとビデオのパケットのデコードにかかる時間を把握します。また、連続再生を維持するために、バッファーされるビデオデータの量を自在に増減できます。ビデオパイプラインを、オーディオパイプラインでレンダリングされたタイムスタンプに合わせる必要があります。次の2つのAPIを使用します。
2.1 AudioTrack.getTimestamp()(APIレベル19以上)
このオーディオパイプラインで、最後にレンダリングされたタイムスタンプの問い合わせがサポートされている場合は、getTimestamp()
メソッドを使用すると目的の値を簡単に取得できます。タイムスタンプが取得可能な場合、AudioTimestamp
インスタンスにはフレーム単位での位置と、そのフレームが出力された時刻の推定値が入ります。この情報を使用してビデオパイプラインを制御し、ビデオフレームをオーディオフレームと一致させることができます。
以下に注意してください。
- タイムスタンプを問い合わせる頻度は、10秒に1回とすることを推奨します。少しのずれが生じる可能性はありますが突然の変化はないと考えられるため、それより頻繁にタイムスタンプを問い合わせる必要はありません。頻繁に問い合わせると、CPUやバッテリーの使用量が増加し、バッテリー駆動デバイスにとって問題となることがあります。
- タイムスタンプが正しく返される場合、アプリはハードコーディングされたオフセット値を追加で使用せずに、返された値を信頼する必要があります。経験的な値は追加しないことを強くお勧めします。経験的な値はプラットフォームに依存し、(Bluetoothシンクが接続されたときなどに)いつでもパイプラインが更新される可能性があるため、以前は正しかった値が不正確になる場合があります。
- オーディオパイプラインの初期ウォームアップ期間中、AudioTrack.getTimestamp() APIが0を返し、タイムスタンプ値がしばらく更新されない場合があります。この状態は一時的なものですが、数秒に及ぶことがあります。再生開始時にオーディオとビデオの同期に関する問題を回避するには、次のセクションに記載のAudioTrack.getPlaybackHeadPosition() APIにフォールバックする必要があります。
使用可能なパラメーターや戻り値などの詳細については、AndroidドキュメントのgetTimestamp()
メソッドを参照してください。
2.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クラスを参照してください。
3.標準のAndroid MediaPlayerの使用
Fire OSでは、オーディオとビデオの再生を処理する標準のAndroid MediaPlayer
クラスがサポートされています。これらのメディアクラスは、オーディオとビデオの同期の要件に従って基本的なメディア再生を処理できます。ただし、その機能は多くの点で制限されています。代わりに、ExoPlayerのAmazon版(または有料のメディアプレーヤーオプション)を使用することを強くお勧めします。
MediaPlayer
クラスを使用する場合、アプリはAndroid MediaPlayerとFire OSの関数と戻り値を信頼する必要があります。ハードコーディングされた値はオーディオパイプラインが更新されたときに不正確になる可能性があるため、タイムスタンプ、オーディオレイテンシ、バッファーなどの手動での調整は行わないことを強くお勧めします。4.Android NDKのOpenSL ESフレームワークの使用
OpenSL ESが標準のAPIを使用してオーディオレイテンシを問い合わせると、AudioFlingerによって報告されたハードウェアのオーディオレイテンシのみが返されます。(主にオーディオトラックバッファーによって)ソフトウェアで発生したレイテンシは含まれません。ハードウェアとソフトウェアの両方の遅延を含む正確なオーディオレイテンシ値を取得するために、Fire OS 6および7では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 /* idsおよびreq配列のサイズ */, ids, req);
if (result != SL_RESULT_SUCCESS) {
ALOGE("CreateAudioPlayerでエラーが発生しました(結果:%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のリアライズでエラーが発生しました(結果:%d)", result);
return;
}
// オーディオプレーヤーのインターフェイスを取得
result = (*playerObject)->GetInterface(playerObject,
SL_IID_ANDROIDCONFIGURATION,
&playerConfig);
if (result != SL_RESULT_SUCCESS) {
ALOGE("構成のGetInterfaceでエラーが発生しました(結果:%d)", result);
return;
}
// オーディオプレーヤーのレイテンシを取得
result = (*playerConfig)->GetConfiguration(playerConfig,
(const SLchar * )"androidGetAudioLatency", ¶mSize, &audioLatency);
if (result == SL_RESULT_SUCCESS) {
// ハードウェアおよびソフトウェアのオーディオレイテンシはSLuint32型の変数audioLatencyに格納されます。
} else {
// 現在のget_audio_latency APIを呼び出します。ハードウェアのオーディオレイテンシ値のみ取得できます。
}
その他のリソース
オーディオとビデオの同期についてさらに詳しく学習するには、以下の外部リソースが役立ちます。