media-playback-footguns について
Kotlin と Compose Multiplatform のメディア再生向けトラブルシューティングスキルです。本番アプリで見つかった落とし穴をテーマ別にまとめ、実装パターンや原因、検証方法を示します。
主な機能
- 01再生に関する33の落とし穴
- 02テーマ別の短いリファレンス
- 03具体的な障害例と原因
- 04コード検証用コマンド
活用シーン
- 012台のプレーヤーによるクロスフェードの実装
- 02キュー遷移やバックグラウンド再生の不具合調査
- 03オーディオ処理やフェードのデバッグ
- 04複数プラットフォームでの再生動作の確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ maxrave-dev/ kotlin-footguns --skill media-playback-footgunsインストール後、Claude Code で/media-playback-footgunsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/audio-fade-separate-gain-line.mdaudio-focus-multiplayer.mdbeatmatched-automix.mdcrossfade-dual-player.mdcrossfade-exclusion-heuristics.mdcustom-shuffle-order.mdderive-seek-from-progress-flow.mddual-source-queue-sync.mdendless-queue-management.mdfgs-state-ended-trap.mdfilter-chain-two-owners-one-writer.mdfollower-item-built-from-shared-payload.mdfollower-transport-stays-local.mdforwarding-player-hot-swap.mdguard-on-every-trigger-path.mdintent-flag-not-observed-state.mdjoiner-catches-up-by-asking.mdmedia3-custom-audio-processor.mdone-setting-two-backends.mdoptional-engine-feature-degrade-in-tiers.mdper-track-loudness-normalization.mdplay-intent-decided-before-load.mdplayback-position-persist-restore.mdpolymorphic-load-media-entry-point.mdposition-based-group-sync.mdqueue-index-vs-shuffle-space.mdqueue-rebuild-state-machine.mdreadiness-barrier-needs-every-answer.mdrealtime-biquad-dsp.mdruntime-override-not-preference-write.md詳細
メディア再生で陥りやすい落とし穴
本番環境の Kotlin および Compose Multiplatform アプリから抽出した 33 件の落とし穴を、references/ 配下の短いファイルに 1 件ずつまとめています。各ファイルには、動作するパターン、落とし穴(実際に失敗する具体的なケースとその理由)、およびユーザー自身のツリーで実行するコマンドを記載した「確認方法」セクションがあります。
このインデックスの使い方。 これから記述しようとしているコード、または調査中の症状を以下の項目と照らし合わせてください。各項目にはトピックと、参照するべき症状が記載されています。コードや修正を提案する前に、該当する可能性のあるファイルをすべて Read ツールで読んでください。各ファイルは 150 行未満です。「確認方法」のコマンドにソースプロジェクトのパスが記載されている場合は、ユーザーのツリー内の対応するパスに置き換えてください。
相互参照。 ここに記載されていないファイル内のバッククォートで囲まれた落とし穴の名前は、隣接する領域のスキルにあります。../*/references/<name>.md を glob して開いてください。
プレーヤー、クロスフェード、エンジンのライフサイクル
crossfade-dual-player— メディア項目ごとに1つのプレーヤーインスタンスを使って2つの項目間のクロスフェードを作成し、フェード中は2つ目のライブインスタンスを使用します。その後、2つのプレーヤーから音声が出ている間も、トランスポートコマンドと再生設定が正しく保たれるようにします。プレーヤーにフェードを追加するときや、フェード中に一時停止すると古いトラックが背後で再生され続ける、シークしても何も起きないように見える、スキップ後に再生速度が1.0xに戻る、または音量スライダーが音量ランプと競合するときに使います。guard-on-every-trigger-path— 通常はポーリングループと項目終了時のコールバックという2つの入口から起動する機能について、どちらの経路でも開始条件を同一に保ちます。片方にしか追加されていない条件は、エラーもログもクラッシュも発生させないデッドコードになるためです。新しく追加したガード、除外条件、または機能フラグがまったく効果を示さないときや、ほとんどの場合は正しく動作するのに、特定のタイミングだけ誤動作するときに使います。crossfade-exclusion-heuristics— 動画として再生される項目、フェードには短すぎる項目、連続する2つの項目が同じアルバムに属する場合など、クロスフェードを実行してはならない条件を判断し、シャッフル、自動長のフェード、増え続けるキューにも耐えられるように各ルールを実装します。フェードで曲が途中で切れる、前のトラックの再生中に動画が最初のフレームへ飛ぶ、25秒の間奏の半分がフェードに費やされる、または連続再生するように並べたアルバムでトラックごとに再生が中断されるときに使います。beatmatched-automix— DJのように、2つのトラックの差に応じて自動クロスフェードの長さとテンポ/キーの一致具合を導き出します。テンポを比較する前に倍速・半速を正規化し、ビートに合わせて長さを決め、前半に重心を置いたランプと量子化されたゲイン/速度のステップを使います。自動トランジションの長さが恣意的に感じられる、テンポが1オクターブ離れたトラックが大きな差として扱われる、アウトゴーイングトラックが聞こえなくなってからでないとテンポが合わない、または速度ランプでクリック音が発生するときに使います。forwarding-player-hot-swap— メディアセッションとUIでは同じ安定した参照を維持したまま、実行時にForwardingPlayerの下にあるプレーヤーを入れ替えます。リスナーと動画出力を再アタッチし、1項目のタイムラインでは答えられないプレイリストに関する問い合わせにも対応します。システム通知から次へ/前へボタンが消える、最初の入れ替え後に何も更新されない、トラック変更後に動画が表示されなくなる、またはプレイリストのインデックスを報告するとアプリが停止するときに使います。fgs-state-ended-trap— 転送プレーヤーの境界で「終了」再生状態を抑制し、一方のプレーヤーが終了してから次のプレーヤーが開始するまでの間にメディアサービスが破棄されないようにします。開発用のスマートフォンでは起きないのに、一部のデバイスではバックグラウンド再生がキューの途中で停止する、トラック間で再生通知が消える、またはキューの途中でシステムによってアプリがフリーズするときに使います。polymorphic-load-media-entry-point— 「この項目を読み込んで再生を開始する」汎用関数で複数の項目タイプを単一の内部形式に正規化し、識別子に応じてキューの初期化方法を振り分け、キューへの追加前に行うすべてのポリシーをその関数で適用します。複数の画面から再生を開始でき、あるルール(コンテンツフィルター、重複排除、分析、自動再生せずに再開)が経路によって適用され忘れるときに使います。play-intent-decided-before-load— 新たに読み込む項目を再生するか、どこから再生するかを、項目の構築前に決めて読み込み呼び出しに渡します。「再生を開始する」というハードコードされた指定で読み込んで、少し後に修正するのではありません。同期ルームのフォロワーが、ほかの全員が一時停止しているセッションで突然音を出すときや、新たに読み込んだ項目が聞こえる状態で再生を始めてから停止するときに使います。intent-flag-not-observed-state— 実行することが決まっていても、まだ実行されていないコンポーネントは「実行していない」と報告します。そのため、意図を示す情報が必要な場合は、観測された状態ではなく意図フラグを参照します。また遷移時には、意図フラグをその場で読み取るのではなく、タイムアウト付きで待機する必要があります。1つのクライアントのバッファリングの一時的な問題で同期グループ全体が停止する、バックグラウンドでキューに追加すると再生開始直前のトラックが無音になる、毎回のティックで再開コマンドが発行される、または状態の読み取りがまさに必要な遷移時にだけ誤っているときに使います。derive-seek-from-progress-flow— すべてのプラットフォームがすでに出力している進捗ストリームから、再生ヘッドが経過時間で説明できる以上に移動した場合にスクラブを検出します。プラットフォーム固有の不連続コールバックは1つのバックエンドにしかなく、スクラバーをドラッグしても項目ストリームもトランスポートストリームも発火しないためです。共有セッションのメンバーがシークしても送信されないときや、クロスプラットフォーム層で1つのプラットフォームにしかないイベントが必要なときに使います。optional-engine-feature-degrade-in-tiers— メディアエンジンのビルド時オプション依存関係が別のプラットフォームのバンドルに含まれない場合があります。また、エンジンはチェーン内のステージを1つでも認識できないとチェーン文字列全体を拒否します。そのため、必須のステージまで失わないようオプションのステージを除いて再試行し、受け入れられた段階を返して、呼び出し側が存在しないステージを実行しないようにします。あるプラットフォームのバンドルでは機能するのに、別のプラットフォームでは何も起きないときに使います。
オーディオ処理:フォーカス、フェード、ラウドネス、DSP
media3-custom-audio-processor— Media3/ExoPlayer のオーディオパイプライン向けに、実行時に切り替えられ、複数の同時再生プレーヤーで共有されるカスタムオーディオプロセッサー(フィルター、フェード、ゲイン段など)を作成します。追加したプロセッサーが次のトラックまで何もしない場合、エラーなしで再生が停止する場合、独自の出力バッファーに対するput(ByteBuffer)が例外を投げる場合、または同じパラメーターを複数の同時再生プレーヤーに反映する必要がある場合に使用します。realtime-biquad-dsp— audio-EQ-cookbook の数式を使い、純粋な Kotlin で小規模なリアルタイム IIR フィルターを構築します。ローパス、ハイパス、複数のピーキングセクションを並べたバンクに対応し、急峻な傾斜を実現するカスケード段、チャンネルごとの独立した状態、状態サイズを固定するニュートラル段、遅延係数再計算を備えます。オーディオコールバック内で周波数をスイープできるフィルターが必要な場合、ステレオフィルターでステレオイメージが崩れる場合、フィルター出力が無音または NaN になる場合、カットオフのスイープやバンドのドラッグでノイズが発生する場合に使用します。audio-fade-separate-gain-line— スリープタイマー、アラームのフェード、ダッキングなどのプログラムによるオーディオフェードを、ユーザーの音量とは別のゲインラインに実装し、プレーヤー独自の完了処理からそのゲインを復元します。フェードによって UI の音量スライダーが下がる場合、スライダーが最大なのにアプリが無音のまま復帰する場合、フェード終了時にクリック音が残る場合、フェード完了後に音量が一瞬戻る場合に使用します。audio-focus-multiplayer— デュアルプレーヤーのクロスフェードやプリキャッシュ済みプレーヤーなど、複数のプレーヤーインスタンスが同時に動作しているときに、アプリレベルで Android のオーディオフォーカスを一度だけ保持し、フォーカスによるダッキングがフェード専用のゲインラインに影響しないようにします。トラック間でバックグラウンド再生が止まる場合、自動再生が最初の項目の後で停止する場合、ダッキングが適用されない、または解除されない場合、トランジション中に音量が突然最大に戻る場合に使用します。per-track-loudness-normalization— 各トラックに個別のプレーヤーを使う場合に、プラットフォームのラウドネスエンハンサーでトラックごとのラウドネスノーマライズを適用します。エフェクトは一つのオーディオセッションに紐づくため、トラックごとに再作成し、リモート再生セッションがアクティブな間はスキップし、メタデータ由来のゲインをクランプします。ノーマライズが最初のトラックでは機能するものの、その後は何も通知されずに停止する場合、再生中のトラックがないのに有効化すると例外が発生する場合、キューのトラックが一つおきに大きな音量で再生される場合に使用します。filter-chain-two-owners-one-writer— エンジンの一つのプロパティがチェーン全体を保持していて、書き込むとすべてが置き換わるため、二つの独立した機能がそこに項目を追加しようとする状況に対応します。各機能の項目をそれぞれのフィールドに保持し、一つの書き込み処理で合成して、「クリア」では自身の段だけを削除します。既存のエフェクトに二つ目のエフェクトを追加する場合や、二つのエフェクトのうち一方だけが機能し、トランジション後にもう一方が不定期に停止する場合に使用します。sampled-supplier-vs-per-handle-reapply— 同じ設定に対し、二つの再生バックエンドで異なる配線方法が必要な場合に対応します。各ストリームのコンシューマーが共有フィールドを読み取るバックエンドでは値のプッシュは不要ですが、新しく作成するハンドルが毎回初期状態になるバックエンドでは、各作成箇所で明示的に再適用する必要があります。設定が現在のトラックには反映されるものの次のトラックには反映されない場合、あるプラットフォームでは維持されるのに別のプラットフォームでは維持されない場合、複数のハンドルに設定したレベルが元に戻る場合に使用します。one-setting-two-backends— バンドの中心周波数、幅、範囲を一度定義し、耳で判断するのではなく参照実装と照合して検証することで、互いに異なる二つのオーディオバックエンドで一つの保存値が同じ意味を持つようにします。デバイスごとにパラメーターが異なるプラットフォーム組み込みエフェクトは使用しません。複数のプラットフォームにトーンまたはゲイン設定を追加する場合や、同じ保存設定でもプラットフォームごとに音が異なる場合に使用します。
キュー、シャッフル、位置
queue-index-vs-shuffle-space— プレーヤーの「現在のインデックス」は UI に公開されますが、エンジンのタイムライン順とシャッフル再生順が異なるインデックス空間であるため、シャッフルが有効になるとトラックの切り替え時に更新されなくなったり、誤った行が強調表示されたりします。キューリストの再生中マーカーが動かない、1 行ずれて表示される、または同じトラックの複製すべてが点灯する場合に使用します。custom-shuffle-order— メディアエンジンのデフォルトのシャッフル順を置き換え、再生中に追加されたトラックがキューの残りに散らばるのではなく、現在のトラックの直後にまとまって配置されるようにします。シャッフルが有効なときに「次に再生」するトラックや追加された続きのページがランダムな位置に入ってしまう場合、または挿入/削除/複製の契約を一貫させながらカスタムのシャッフル順を作成する場合に使用します。endless-queue-management— 1 つの StateFlow が増え続ける再生キューを保持しますが、意図的に 2 つの書き込み経路を持ちます。1 つは派生スナップショットをリセットする完全なセッター、もう 1 つはスナップショットを維持できるよう、バッキングフィールドに直接書き込む続きの追加処理です。「キューの出どころ」をキーとする機能がキューの自動拡張後に動作しなくなる場合や、2 つのキュー書き込み経路を 1 つにリファクタリングする前に使用します。queue-rebuild-state-machine— キューの再構築状態フラグは、「再構築中」と「安定」の状態を示します(列挙型で宣言された値にかかわらず、動作上は 2 つの値です)。これにより、再入した読み込みリクエストは早期に返され、キューの構築途中にスナップショットが取得されることはありません。1 回のスクロールでページネーションが 2 回実行される場合、復元したキューに再生中だったトラックが含まれない場合、またはエラー後に読み込み状態が解除されない場合に使用します。dual-source-queue-sync— UI に表示するトラックリストと再生エンジンのタイムラインの両方がキューを保持するため、エンジン側で変更があるたびに、メディア ID を使ってエンジンのタイムラインから UI リストを再生成します。サイズが一致しない場合は変更を拒否し、ユーザーが並べ替えると両側を変更します。画面上のキューと異なる順序で再生される場合、シャッフルによってリストだけが並べ替わり再生順は変わらない場合、またはキューを書き込む箇所を 2 つ目に追加する前に使用します。playback-position-persist-restore— 再生位置を効率よく保存・復元します。ライフサイクルの節目ではキュー全体を保存し、既存のループで 5 秒ごとに軽量な位置情報を更新します。キューの再構築中は保存を省略し、値を変更するプレーヤーコマンドを実行する前に値をスナップショットします。プロセス終了後、バックグラウンド再生がトラックの先頭から再開される場合や、復元後に誤ったトラックまたは位置になる場合に使用します。
共有リスニングセッション
sync-room-bridge-echo-guard— 1つのブリッジでローカルコンポーネントを共有ルームに接続し、役割によって通信方向を決めます。ソースは自身の操作を公開し、フォロワーは受信した内容を適用するだけで何も公開しません。さらに、適用中にフラグを保持することで、リモートコマンドが引き起こしたローカルで観測される副作用が、そのまま送り返されないようにします。同期セッションで2つのクライアントが互いにコマンドを送り合う場合、リモートの一時停止を適用するとすぐに一時停止が再公開される場合、またはフォロワー自身のリアクションが自ら引き起こしていない状態に対して発火する場合に使用します。server-default-overrides-client-intent— 共有状態を管理するリレーは、コマンドで指定されていないフィールドに独自のデフォルト値を設定します。特に、すべてのアイテム変更で「再生していない」を強制するため、メッセージにそのまま従うフォロワーは読み込んだばかりのアイテムを停止してしまいます。変更前の意図を引き継ぎ、不足しているフィールドを2つ目のコマンドとして公開します。ソースが再生を続ける一方で、同期ルームのフォロワーが次/前/アイテム終了のたびに無音になる場合に使用します。position-based-group-sync— すべてのコマンドに再生位置を含めて公開し、コマンドの送信にかかった時間分を補正し、ローカルとのずれが許容範囲を超えた場合にのみシークすることで、グループ内のクライアントを同期させます。最も遅いメンバーを全員で待つ方法は使いません。同期セッションで音が聞こえるほどずれていく場合、フォロワーがソースに追いつこうとして絶えず途切れる場合、または各デバイスがそれぞれ独自にストリームを解決するため準備完了までの時間に差がある場合に使用します。joiner-catches-up-by-asking— リレーが新しいメンバーに送る状態は、ソースの最後のコマンドを再生したものです。そのため、位置はそのコマンドがどれだけ古いかによって決まり、指示に従うと、他のメンバーが途中まで進んでいるコンテンツの最初から参加してしまいます。参加した時点のライブ位置を要求し、このクライアントが共有タイムラインに再参加するたびに、再び要求します。セッション途中で参加したメンバーが最初から再生を始める場合や、そのデバイスで最後に再生していた位置から再開する場合に使用します。follower-transport-stays-local— 共有セッションのフォロワーは、自分自身の停止ボタンを管理します。一時停止はローカルで行い、通知されません。再生ボタンを押すと、このデバイスが停止した位置に戻るのではなく、現在のセッション位置を問い合わせます。次の状態更新ですぐに再開されるためフォロワーが一時停止できない場合、フォロワーの一時停止によって全員が停止する場合、または再開するとグループ全体より何分も遅れた位置になる場合に使用します。runtime-override-not-preference-write— コンポーネントのランタイムオーバーライドを使って、モード中に限りユーザー向けの機能を無効にします。保存済みの設定を書き換えてはいけません。モード中にプロセスが終了すると、ユーザーの本来の設定が永続的に変更されたままになるおそれがあります。モードに入る際に既存の機能を無効にする必要がある場合、設定がいつの間にかオフになり、そのまま戻らない場合、またはモード終了時のクリーンアップだけがユーザーの設定消失を防いでいる場合に使用します。follower-item-built-from-shared-payload— ローカルカタログからアイテムを再解決するのではなく、共有セッションが保持するペイロードからフォロワーの再生可能なアイテムを構築します。アートワークの形状から形式を推測するローカルリゾルバーは誤った形式を選ぶ可能性があり、適用コレクター内でアイテムごとにネットワーク往復を行うと、後続のすべてのコマンドがその処理待ちになります。同期ルームのフォロワーで、ソースには音声があるのに無音の動画が再生される場合や、1つの遅い検索でクライアントのコマンドストリーム全体が停止する場合に使用します。readiness-barrier-needs-every-answer— 全メンバーが準備完了を報告するまでグループを待機させるバリアです。再生中ではなくバッファリング状態を基準に応答し、自分が実際に指定された場合にのみ応答し、UIで誰の応答を待っているかを明示します。また、1人でも応答しないメンバーがいると全員が停止することを理解しておきます。遅いデバイスが1台参加した後、共有セッションがすべての参加者で停止する場合、再生が何の説明もなく始まらない場合、または「読み込み中」の状態に説明が表示されない場合に使用します。
コメント
Markdown 対応