明日からちょっと自慢できる画質AUTOの仕組みについて

サムネイル

初めに

この記事は DMMグループ Advent Calendar 2025 の12日目の記事です。

こんにちは。メディア基盤開発部でフロントエンドエンジニアをしている今西(@nisshii0313)です。主に動画再生プレイヤーまわりのコンポーネントの開発および保守・運用をしています。

さて、弊社に限らず、多くの動画配信サービスでは画質設定に"AUTO"や"自動"という項目があります。
選択すれば自動でHDや4K等の画質を設定してくれる便利さから、使用されている方も多いのではないでしょうか。
本記事では、この画質AUTO機能がどんな技術の上で成り立っているのか、主にフロントエンドの目線から解説します。

画質AUTOの仕組み

画質AUTO機能は、あらかじめ様々なビットレートや解像度の動画ファイルを用意し、ユーザーの通信状況に応じて随時最適なものを配信するABR(Adaptive BitRate)という技術によって実現されています。

動画ストリーミング配信サービスにおいて、再生する動画は、大元の動画を数秒ごとに分割したセグメントファイルという形で配信されている場合が多いです。
ユーザーの通信状況は、このセグメントファイルのダウンロード速度で計測されます。

計測された通信状況を元に、次の区間に最適なセグメントファイルをダウンロードして再生する、ということを繰り返しています。

スムーズな画質変更への工夫

前章で、大雑把なABRの流れを紹介しました。
このまま通信状況に応じて取得したセグメントファイルを再生すれば、ひとまずABR機能は提供できたと言えます。
しかしながら、それだけでは安定した再生を提供できません。

ユーザーの安定した動画視聴を支えるには、以下の2点が重要です。

  • 通信状況の悪化スピードに負けない速さ(反応速度)で画質を低く調整すること
  • 通信状況の好転スピードを超えない速さで画質を高く調整すること (画質が高すぎてバッファリング状態や再生が止まるのは好ましくない)

これを実現するのが、EWMA(Exponentially Weighted Moving Average)という手法です。
時系列データを平滑化する際に、個々のデータに付く重みを指数関数的に減らしていきます。
直近のデータを重視しつつ、古いデータも切り捨てないという特徴を持ちます。
shaka-playerや、hls.jsといったメジャーな動画再生ライブラリのABRロジックに組み込まれる形で、皆さんの動画視聴を陰ながら支えています。

ABRでは、このEWMAを用いて、重みの半減期が比較的短い処理である"fast"と、重みの半減期が長い処理である"slow"を用意します。
その際、重みの計算にはセグメントファイルの読み込み時間を用います。
下の簡略化したグラフから、大雑把にイメージを掴んでいただければと思います。

fastとslowによる違いを視覚的に示したグラフ

"fast"は重みが直近のデータに偏っているため、データの変化に対してドラスティックに反応します。
一方で"slow"は重みの偏りが比較的無いため、緩やかに反応します。

最終的には、この"fast"と"slow"で得られた計算結果のうち、小さい方を仮想の帯域値として採用しています。
というのも、通信状況が悪化した場合には"fast"の方が直近のデータに急激に反応し、得られる値が小さくなります。
"fast"側が採用されることで、スピーディに画質が下がり、速やかに安定再生されるようになります。

一方、通信状況が好転した場合には"slow"の方が緩やかに反応し、得られる値が小さくなります。
"slow"側が採用されることで、安定再生が保たれたまま緩やかに画質が上がっていきます。

このようにして先ほどの2点が満たされ、ユーザーの安定した動画視聴が支えられています。

実際のコードを見ながら処理を追ってみる

それではこれまで解説したABRの流れを、実際のshaka-playerのコードを見ながら追っていきましょう。

まずはshaka-playerのインスタンス作成時に、前章で説明した"fast"と"slow"が用意されます。
引数に半減期の秒数が渡されています。

(lib/abr/ewma_bandwidth_estimator.js)

this.fast_ = new shaka.abr.Ewma(2);
this.slow_ = new shaka.abr.Ewma(5);

この半減期の秒数ですが、固定値ではなくそれぞれ設定可能です。
初期化のタイミングではそれぞれ2秒と5秒という固定値が渡されていますが、Configの読み込み以降は設定した秒数が使用されます。

各インスタンスの初期化処理が終わったうえで、動画の再生中にHTMLMediaElementのprogressイベントが発火することで、ABRの一連の処理が始まります。
イベント発火に呼応して、segmentDownloadedメソッドが呼ばれます。
その中で、セグメントファイルの読み込みにかかった秒数とファイルサイズからbandwidth(帯域)の計算が開始されます。

(lib/abr/simple_abr_manager.js)

this.bandwidthEstimator_.sample(realTimeMs, numBytes);

そして、先ほど用意した"fast"、"slow"それぞれに、計算されたbandwidthが渡されます。
前章で説明したように、EWMAの重みの計算に使うセグメントファイルの読み込み時間も渡されます。

(lib/abr/ewma_bandwidth_estimator.js)

const bandwidth = 8000 * numBytes / durationMs;
const weight = durationMs / 1000;
this.fast_.sample(weight, bandwidth);
this.slow_.sample(weight, bandwidth);

sampleメソッド内では、重みの減り方を0から1までで表した係数alpha_を用いて、現在のbandwidthに付く重みがどのくらい減っているのかを計算し、adjAlphaとします。
減った分の重みは前回のEWMAの計算結果に付く重み、残った分の重みが今回のbandwidthに付く重み、になるので、重み付き平均を取ることで今回のEWMAの計算結果が求められます。

(lib/abr/ewma.js)

sample(weight, value) {
  const adjAlpha = Math.pow(this.alpha_, weight);
  const newEstimate = value * (1 - adjAlpha) + adjAlpha * this.estimate_;

  if (!isNaN(newEstimate)) {
    this.estimate_ = newEstimate;
    this.totalWeight_ += weight;
  }
}

少し戻りますが、先ほどのsegmentDownloadedメソッド内でストリームの切り替え判定処理も呼ばれています。
その中で"fast"と"slow"で求められたEWMAの計算結果のうち、小さい方の値が採用されています。

(lib/abr/ewma_bandwidth_estimator.js)

getBandwidthEstimate(defaultEstimate) {
  if (this.bytesSampled_ < this.minTotalBytes_) {
    return defaultEstimate;
  }

  // Take the minimum of these two estimates.  This should have the effect
  // of adapting down quickly, but up more slowly.
  return Math.min(this.fast_.getEstimate(), this.slow_.getEstimate());
  }

最後にここで呼ばれているgetEstimateの中身を覗いてみます。

(lib/abr/ewma.js)

getEstimate() {
  const zeroFactor = 1 - Math.pow(this.alpha_, this.totalWeight_);
  return this.estimate_ / zeroFactor;
}

sampleメソッド内での計算で、estimate_として基本的なEWMAによる平均値は求められています。
しかしながら、計測初期においては初期値の影響を受け過ぎてしまうという注意点があります。
対策として、先ほどのgetBandwidthEstimateメソッド内で、ダウンロードしたセグメントファイルサイズが一定量に達するまでは、事前に設定した値を返しています。
その上で、計測値にかかっている重みの累積値(zeroFactor)で割ることで、初期値によるバイアスを補正しています。
このzeroFactorですが、計測期間が長くなると段々1に近づいていくため、割ることによる影響は薄れていきます。

このようにして、ABRの一連の処理が動いています。

まとめ・宣伝

時系列データに重み付けをするEWMAというアルゴリズムによって、ABRの技術、ひいては画質AUTO設定時の動画視聴体験は支えられています。
さらに、重みの半減期を調節した2種類の処理("fast"と"slow")を用意することで、画質が通信状況に応じて「急速に落ちて緩やかに上がる」安定性を重視した動画再生が実現されています。

最後に宣伝ですが、動画再生のフロントエンド処理を体系的に解説した記事を書きました。
Webブラウザが動画を再生する流れをコードを追いながら解説しているので、ぜひ本記事と併せてご覧ください。
codezine.jp