# ナレーションが途切れず、音声解説が入りきらない動画

Source: https://easeweb.dev/learn/extended-audio-description
Topics: 画像とメディア > 説明する間がない動画は一時停止して説明する (WCAG 1.2.7)
The fix: 解説の間は映像をカードの場面で止めておく拡張版を公開し、音声解説のボタンから選べるようにします。または、解説をテキストでプレーヤーに渡し、読み上げる間は動画を一時停止させます。後者は、視聴者のブラウザーにその言語の音声がある場合に限って働きます。
Test with: screen off, keyboard, screen reader
References: https://www.w3.org/WAI/WCAG22/Understanding/extended-audio-description-prerecorded.html https://www.w3.org/WAI/WCAG22/Techniques/general/G8 https://www.w3.org/WAI/WCAG22/Techniques/html/H96 https://www.w3.org/WAI/media/av/description/ https://github.com/ableplayer/ableplayer https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis https://developer.mozilla.org/en-US/docs/Web/API/TextTrack/cuechange_event

よくある問題を再現した学習用のサンプルです。実在のクライアントの診断結果ではありません。コードは出発点となる実装例のため、実際の製品でも確認してください。ここでは支援技術でのテスト結果は報告していません。

## 場面

コーヒー焙煎店の淹れ方動画には音声解説版ができ、プレーヤーの下の「音声解説」ボタンで選べるようになりました。その後、店主が話しながら進める版が加わりました。最初のカード「豆15 g・お湯250 ml」が出ている間、店主は「このくらい量ります」と言って間を空け、その間に解説が分量を読み上げます。ところが次の2枚では、店主は話し続けます。「お湯は少し置きます。沸かしたてだと、粉が焦げてしまうんです」、続けて「ゆっくり注いで、ここで止めます。細く注いで、粉がふくらんで落ち着くのを待ちます」。カードには「沸騰後、30秒冷ます」「3回に分けて注ぐ・3:00で止める」と出ていますが、店主が一息つく前に消えてしまいます。解説を入れる間がないため、音声解説版はこの2枚について何も言いません。映像なしで聞く人には豆の量は伝わりますが、どれだけ待つのか、いつ止めるのかは伝わりません。

## 確認できる問題

レベルAAAの達成基準1.2.7は、動画の話し声の間が短すぎて、音声解説では動画の内容を伝えきれない場合を扱います。そのときに求めるのが拡張音声解説です。解説の間は動画を一時停止するか映像を止めておき、解説が終わったら再生を続けます。この動画がまさにその場合です。最初のカードの後、ナレーションの間は1秒に満たず、カードの解説にはおよそ2秒から4秒かかります。解説を早口にしたり「ケトルがある」まで削ったりすれば、カードが伝えるはずの時間と分量が失われます。音声解説版は、間に入れるだけの解説としてはできる限りのことをしていますが、そこで尽きています。ページにも、時間を作る仕組みはありません。動画は止まらず、ほかの版もありません。ブラウザーにもその機能はありません。`video` 要素は一つの音声を動画と同じ速さで流すだけなので、映像を待たせるには、解説をファイルに書き込んでおくか、スクリプトで動画を一時停止して流す必要があります。

## 利用者への影響

耳で動画を追う人は、誰でも同じ問題にぶつかります。ケトルから目を離せない人もそうです。話しながら進めるナレーションは聞きやすく、役立つ話も多いのですが、その分、解説が入る間がなくなります。

## 自動チェックとAIの限界

axeには音声解説のチェックがありません。EaseWebの診断は、すべての動画を人が確認する項目として挙げ、そばに音声解説の操作やdescriptionsトラックがあるかも記録します。そのため、この例の音声解説版は見つかり、合格に見えます。解説が収まっているか、何が抜けているかは、聞いてみないと分かりません。自動チェックは映像と音声を比べることも、話し声の間とまだ言うべき内容を照らし合わせることもできません。AIツールは音声の無音部分を見つけたり、フレームから解説の下書きを作ったりできます。それでも、視聴者が何を聞き逃すか、どこで映像を待たせるかは、内容を知る人が決める必要があります。

## 修正前：間が足りなくなる音声解説版

```html
<video id="brew-video" controls preload="metadata"
  src="brew-talk.webm" poster="brew-talk.jpg"
  data-described="brew-talk.described.webm">
  <track kind="captions" srclang="ja" label="日本語"
    src="brew-talk.ja.vtt" data-described="brew-talk.described.ja.vtt">
</video>
<button type="button" id="describe" aria-pressed="false">
  音声解説
</button>

<script>
  const video = document.getElementById('brew-video');
  const track = video.querySelector('track');
  const plain = { video: video.getAttribute('src'), captions: track.getAttribute('src') };
  document.getElementById('describe').addEventListener('click', (event) => {
    const button = event.currentTarget;
    const on = button.getAttribute('aria-pressed') !== 'true';
    const { currentTime, paused, playbackRate } = video;
    const captions = track.track.mode;
    video.src = on ? video.dataset.described : plain.video;
    track.src = on ? track.dataset.described : plain.captions;
    // 同じ位置、同じ速度で、字幕も元の表示のまま続ける。
    video.addEventListener('loadedmetadata', () => {
      video.currentTime = currentTime;
      video.playbackRate = playbackRate;
      track.track.mode = captions;
      if (!paused) video.play();
    }, { once: true });
    button.setAttribute('aria-pressed', String(on));
  });
</script>
```

```vtt brew-talk.described.ja.vtt
WEBVTT

00:04.600 --> 00:06.370
ナレーション: このくらい量ります。

00:06.420 --> 00:10.750
解説: コーヒー豆15グラム、お湯250ミリリットル。

00:11.000 --> 00:15.270
ナレーション: お湯は少し置きます。
沸かしたてだと、粉が焦げてしまうんです。

00:15.700 --> 00:21.110
ナレーション: ゆっくり注いで、ここで止めます。
細く注いで、粉がふくらんで落ち着くのを待ちます。

00:21.400 --> 00:23.400
［キッチンタイマーの音］
```

ボタンと切り替えは正しく、最初の解説も正しく入っています。どこで解説が尽きるかは、音声解説版の字幕ファイルに表れています。

- 最初のカードの後は、ナレーションが終わって0.5秒もしないうちに次のナレーションが始まり、解説を入れる間がありません。
- そのため音声解説版は、「沸騰後、30秒冷ます」も「3回に分けて注ぐ・3:00で止める」も言いません。
- 時間を作るために動画が止まることはなく、拡張版も用意されていません。

## 修正後：映像を止めておく拡張版

```html
<video id="brew-video" controls preload="metadata"
  src="brew-talk.webm" poster="brew-talk.jpg"
  data-described="brew-talk.extended.webm"
  data-holds="[[15.22, 2.54], [21.06, 3.82]]">
  <track kind="captions" srclang="ja" label="日本語"
    src="brew-talk.ja.vtt" data-described="brew-talk.extended.ja.vtt">
</video>
<button type="button" id="describe" aria-pressed="false">
  音声解説
</button>

<script>
  const video = document.getElementById('brew-video');
  const track = video.querySelector('track');
  const plain = { video: video.getAttribute('src'), captions: track.getAttribute('src') };
  // 拡張版が映像を止める位置：元の動画での時刻と、止める秒数。
  const holds = JSON.parse(video.dataset.holds);
  const toExtended = (time) => time + holds
    .filter(([at]) => at < time)
    .reduce((sum, [, held]) => sum + held, 0);
  const toOriginal = (time) => {
    let shift = 0;
    for (const [at, held] of holds) {
      if (time < at + shift) break;
      // 止めている途中なら、止め始めた位置に戻す。
      shift += Math.min(held, time - at - shift);
    }
    return time - shift;
  };
  document.getElementById('describe').addEventListener('click', (event) => {
    const button = event.currentTarget;
    const on = button.getAttribute('aria-pressed') !== 'true';
    const { currentTime, paused, playbackRate } = video;
    const captions = track.track.mode;
    video.src = on ? video.dataset.described : plain.video;
    track.src = on ? track.dataset.described : plain.captions;
    // 同じ位置、同じ速度で、字幕も元の表示のまま続ける。
    video.addEventListener('loadedmetadata', () => {
      video.currentTime = on ? toExtended(currentTime) : toOriginal(currentTime);
      video.playbackRate = playbackRate;
      track.track.mode = captions;
      if (!paused) video.play();
    }, { once: true });
    button.setAttribute('aria-pressed', String(on));
  });
</script>
```

```vtt brew-talk.extended.ja.vtt
WEBVTT

00:04.600 --> 00:06.370
ナレーション: このくらい量ります。

00:06.420 --> 00:10.750
解説: コーヒー豆15グラム、お湯250ミリリットル。

00:11.000 --> 00:15.170
ナレーション: お湯は少し置きます。
沸かしたてだと、粉が焦げてしまうんです。

00:15.570 --> 00:17.610
解説: 沸騰後、30秒冷ます。

00:18.240 --> 00:23.550
ナレーション: ゆっくり注いで、ここで止めます。
細く注いで、粉がふくらんで落ち着くのを待ちます。

00:23.950 --> 00:27.270
解説: 3回に分けて注ぎ、3分で止める。

00:27.760 --> 00:29.760
［キッチンタイマーの音］
```

1. 音声解説版を編集し直して、拡張版を作ります（W3Cの達成方法G8）。解説を入れる間がない所では、解説する内容がまだ映っているフレームで映像を止め、映像を止めたまま解説を流し、その後に動画を再開させます。この例では、2枚目と3枚目のカードで店主が話し終えた直後に、それぞれ2.54秒と3.82秒、映像を止めています。
2. 専用の字幕ファイルを用意し、各解説を「解説」と示して、聞こえる時刻に入れます。
3. 「音声解説」ボタンが切り替える先を、音声解説版から拡張版のファイルに替えます。
4. 映像を止める位置を `video` の `data-holds` に書いておきます。拡張版は長いため、同じ場面でも時刻が後ろにずれます。スクリプトはこの一覧で、一方のファイルの再生位置をもう一方の同じ場面に換算します。

これで修正前の例は直り、1.2.7を満たします。解説は録音なので、どのブラウザーでも同じように聞こえます。ファイルはどのプレーヤーでも再生でき、音声解説の機能を持たない動画サービスにもそのまま上げられます。

## 修正後：解説が間に収まらないときに一時停止するプレーヤー

```html
<video id="brew-video" controls preload="metadata"
  src="brew-talk.webm" poster="brew-talk.jpg">
  <track kind="captions" srclang="ja" label="日本語"
    src="brew-talk.ja.vtt">
  <track kind="descriptions" srclang="ja" label="日本語"
    src="brew-talk.descriptions.ja.vtt">
</video>
<button type="button" id="describe" aria-pressed="false">
  音声解説
</button>

<script>
  const video = document.getElementById('brew-video');
  const descriptions = video.querySelector('track[kind="descriptions"]').track;
  const button = document.getElementById('describe');
  // hiddenにすると、キューは届くが、ブラウザーは表示も読み上げもしない。
  descriptions.mode = 'hidden';
  let current = null;
  let holding = false;
  descriptions.addEventListener('cuechange', () => {
    if (button.getAttribute('aria-pressed') !== 'true') return;
    const cue = descriptions.activeCues[0];
    if (!cue) {
      // キューの間が終わった。解説をまだ読んでいれば、映像を止める。
      if (current && !video.paused) {
        holding = true;
        video.pause();
      }
      return;
    }
    // 動画は間の分だけ再生を続け、その間に解説を読む。
    const words = new SpeechSynthesisUtterance(cue.text);
    words.lang = descriptions.language;
    words.onend = words.onerror = () => {
      if (current !== words) return;
      current = null;
      if (holding) video.play();
      holding = false;
    };
    current = words;
    speechSynthesis.cancel();
    speechSynthesis.speak(words);
  });
  // 映像を止めている間に再生を押したら、解説の残りを飛ばす。
  video.addEventListener('play', () => {
    if (!holding) return;
    holding = false;
    current = null;
    speechSynthesis.cancel();
  });
  button.addEventListener('click', () => {
    const on = button.getAttribute('aria-pressed') !== 'true';
    button.setAttribute('aria-pressed', String(on));
    if (!on) speechSynthesis.cancel();
  });
</script>
```

```vtt brew-talk.descriptions.ja.vtt
WEBVTT

00:06.420 --> 00:10.750
コーヒー豆15グラム、お湯250ミリリットル。

00:15.220 --> 00:15.450
沸騰後、30秒冷ます。

00:21.060 --> 00:21.350
3回に分けて注ぎ、3分で止める。
```

1. 各解説を `<track kind="descriptions">` のファイルにキューとして書きます。キューはナレーションの後の間に始め、その間が終わる所か、解説する内容が画面から消える所で終えます。この例では、最初のキューには4秒ほどあり、残りの二つは1秒もありません。
2. トラックのモードを `hidden` にします。ブラウザーはキューの表示も読み上げもしませんが、キューが始まるときと終わるときに `cuechange` を発生させます。
3. キューが始まったとき「音声解説」ボタンが押されていれば、トラックの言語を指定して、Web Speech APIの `speechSynthesis` でキューの文を読み上げます。動画は再生を続けます。
4. キューが終わってもまだ読み上げていれば、動画を一時停止します。映像は読み上げが終わるまで止まり、その後に再生を再開します。間に収まる解説で動画が止まることはありません。
5. 映像を止めている間に視聴者が再生を押したら、読み上げを止めて先へ進ませます。

これで修正前の例は直り、その言語の音声があるブラウザーで1.2.7を満たします。ファイルもスクリプトも一つずつで済み、解説はテキストなので直すのも簡単です。アクセシブルなプレーヤーにも同じ仕組みがあります。ただし、間に収まらない所だけでなく、解説のたびに動画を止めます。Able Playerはテキストの解説を、一時停止の設定がオンなら動画を止めて読み、KalturaにはWebVTTから拡張音声解説を読み上げるプラグインがあります。その言語の音声がない端末では、ブラウザーが解説を読めずにそのまま再生を続けるか、別の言語の音声で読むことがあります。解説はどの字幕ファイルにも入らないため、字幕を読む人には届かず、声も端末の合成音声になります。

## どの方法を選ぶか

どちらも1.2.7を満たします。選ぶなら拡張版です。どのブラウザーでも、ファイルを上げられるどの動画サービスでも働き、解説は解説者の録音された声で、字幕にも入ります。一時停止するプレーヤーは、解説が読まれるかどうかが視聴者の端末次第なので、それだけでは足りません。解説をよく書き換える場合や、拡張版のそばに置く軽い選択肢として加え、これだけに頼らないでください。

## 実装の判断

動画を再生する場所によって、拡張版の届け方が変わります。

- 自分の `<video>`：上の例のように、拡張版のファイルとボタンを用意するか、テキストの解説を読み上げ、収まらないときに一時停止するボタンを用意する。ボタンは字幕の操作の隣に置き、音量と同じように押しやすくする。[操作バーに字幕のボタンがない動画プレーヤー](/ja/learn/caption-controls)の修正と同じ。
- YouTubeやVimeoなどの動画サービス：サービスが音声解説の音声トラックを受け付ける場合も、そのトラックは動画と同じ速さで流れ、映像を止められない。拡張版を別の動画として上げ、埋め込みのすぐそばに、何かが分かる名前（「拡張音声解説付き」）でリンクする。
- アクセシブルなプレーヤー：Able Playerは `kind="descriptions"` のトラックを読み上げ、その間は動画を一時停止でき（`data-desc-pause-default="on"`、または視聴者自身の設定）、音声合成が使えないときは文をライブリージョンに入れる。Kalturaのプレーヤーにも、WebVTTのファイルのキューごとに一時停止し、解説を音声、画面上の文、またはその両方で示すプラグインがある。使うプレーヤーが実際に何をするかを確かめてから頼る。

映像を止めるのは、ナレーションに間がない所だけにします。この例の最初のカードのように、間に収まる解説は間に入れ、動画は止めません。止めるたびに、視聴者はその分だけ待たされます。1.2.7の解説書も、拡張音声解説は、それがなければ動画の内容が伝わらない場合にだけ使うとしています。止めるのは、解説する内容がまだ映っているフレームにし、解説は止め始めた直後から流して、次のナレーションに重ならないようにします。

拡張版は元の動画より長いため、途中で切り替えた視聴者は、同じ秒数ではなく同じ場面に戻る必要があります。上のスクリプトは、止めた時間の分を換算しています。ほかの編集も加えた場合は、すべての時刻の対応表を持たせるか、最初から再生します。字幕ファイルにも、解説と新しい時刻が要ります。最初に止める位置より後は、すべての時刻がずれるからです。

一時停止するプレーヤーでは、音声合成の声とスクリーンリーダーは別々の声です。解説の途中で視聴者がページ内を移動すると、両方が同時に話します。プレーヤーの側には、スクリーンリーダーが話しているかを知る手段がありません。

最初のカードのように間が十分あるなら、通常の音声解説版で足ります。[音声解説がなく、画面の情報が伝わらない動画](/ja/learn/audio-description)で扱っています。

## 修正の確認

1. 画面を覆い、拡張版を最後まで再生するか、解説が収まらないときに一時停止するプレーヤーで解説をオンにする。聞こえた分量、時間、指示をすべて書き出し、カードと比べる。どの解説も省略されずに読まれる。
2. 映像を止める箇所を一つずつ見る。解説するカードがまだ映っている間に止まり、解説が終わるまで止まったままで、その後は動画が自動で再生を続ける。解説の途中で再生を押し、解説の直前で一時停止を押して、プレーヤーが指示どおりに動くことを確認する。
3. キーボードだけで、次に画面を覆ってスクリーンリーダーで、プレーヤーのそばの「音声解説」ボタンを見つけ、途中でオンにする。スクリーンリーダーが押されていると伝え、動画が同じ場面から続くことを確認する。
4. 拡張版で字幕をオンにする。字幕は聞こえる内容と一致し、各解説は「解説」と示され、切り替えた後も表示されたままになる。

OS、ブラウザーと支援技術のバージョン、ビルド、実施日、各手順の実際の結果を記録してください。動画、ナレーション、解説を差し替えた際は、再度確認します。

## 制限

これは出発点となる実装例であり、すべてのアプリケーションでの適合を保証するものではありません。動画に拡張音声解説が必要か、各解説で何を言うべきかは動画によって決まり、このガイドが代わりに判断することはできません。拡張音声解説はレベルAAAのため、WCAGのレベルAAを基準とする多くの法令や方針では求められていません。それでも、映像なしで内容が伝わる方法がこれしかない動画もあります。実際に聞く人と一緒に、動画が実際に再生されるプレーヤーやサービスで、一連の流れ全体を試してください。

---

このガイドをコーディングエージェントで使う: MCP https://easeweb.dev/mcp（設定は https://easeweb.dev/ja/services/developers）
