「WebRTCシリーズ」実戦ウェブエンド対応h265ハードソリューション

1. 背景

Web での H.265 のリアルタイム プレビューの需要は以前から存在していましたが、Chrome 自体は以前は H.265 ハード デコードをサポートしておらず、ソフト デコードのパフォーマンスが多く消費されたため、1 回の再生しかサポートできませんでした。需要は見送られました。

昨年 9 月、Chrome はデフォルトで H.265 ハード デコードを有効にする M106 バージョンをリリースし、リアルタイム プレビューで H.265 ハード デコードをサポートできるようにしました。

ただし、WebRTC自体がサポートしている動画のエンコード形式は、H.265ではなく、VP8、VP9、H.264、AV1のみです。w3cが公開している2023年のWebRTC Next Version Use Casesによると、近い将来にH.265に対応する気配はなく、WebRTCのH.265対応を自分で実装することにしました。

2、データチャンネル

背景 クロムといえば h265 ハード ソリューションをサポートしていますが、WebRTC は h265 ビデオ ストリームの直接伝送をサポートしていません。ただし、この制限は datachannel を介してバイパスできます

WebRTC のデータ チャネル DataChannel は、オーディオおよびビデオ データ以外のデータを送信するために特別に使用されます (ただし、オーディオおよびビデオ データを送信できないという意味ではありません。本質的にはソケット チャネルです)。たとえば、ショート メッセージ、リアルタイム テキスト チャットなどです。 、ファイル転送、リモート デスクトップ、ゲーム コントロール、P2P アクセラレーションなど。

1) SCTP プロトコル

DataChannel で使用されるプロトコルは SCTP (Stream Control Transport Protocol) (TCP および UDP と同じレベルのトランスポート プロトコル) であり、IP プロトコル上で直接実行できます。

しかし、WebRTC の場合、SCTP は、フロー制御、輻輳制御、メッセージごとの転送、構成可能な転送モードなどの機能をサポートしながら、それ自体が UDP の上で実行される安全な DTLS トンネルを介してトンネリングされます。送信される 1 つのメッセージのサイズが maxMessageSize (読み取り専用、デフォルト 65535 バイト) を超えることはできないことに注意してください。

2) 設定可能な送信モード

DataChannel はさまざまなモードで構成できます。1 つは再送信メカニズムを使用する信頼できる送信モード (デフォルト モード) で、データがピアに正常に送信されることを保証できます。もう 1 つは信頼できない送信モードで、設定によって設定できます。 maxRetransmits 最大送信回数を指定するか、maxPacketLife で送信間隔を設定します。

これら 2 つの構成項目は相互に排他的であり、同時に設定することはできません. 両方が null の場合、信頼できる送信モードが使用され、一方の値が null でない場合、信頼できない送信モードが有効になります.

3) データ型のサポート

データ チャネルは、文字列型または ArrayBuffer 型、つまりバイナリ ストリームまたは文字列データをサポートします。

次の 2 つのソリューションは、 datachannel に基づいています

3. 解決策 1 WebCodecs

公式ドキュメント: github.com/w3c/webcode…

アイデア: DataChannel は、H.265 ネイキッド ストリーム + Webcodecs デコード + Canvas レンダリングを送信します。つまり、WebRTC のオーディオとビデオの伝送チャネル (PeerConnection) は H.265 エンコード形式をサポートしていませんが、そのデータチャネル (DataChannel) を使用して H.265 データを伝送でき、フロントエンドは Wecodecs デコードと Canvas を使用します。それを受け取った後のレンダリング。

アドバンテージ:

  • H.265 ベア コード ストリームを追加のカプセル化なしで直接伝送、シンプルで便利、冗長データなし、伝送効率が高い

  • Wecodec はデコード遅延が少なく、リアルタイム性が高い

欠点:

  • オーディオは個別に送信、デコード、再生する必要があり、オーディオとビデオの同期の問題に対処する必要があります。

  • 既存の sdk はビデオ パッケージに基づいており、webcodes ソリューションは canvas に依存しており、既存のビデオ関連の操作 (スクリーンショット、ビデオ録画など) を書き換える必要があります。

  • さまざまなオンライン プロジェクトなどの歴史的な理由により、既存の SDK は大幅に変更されており、時間が許されません

4. スキーム 2 MSE

公式の例: github.com/bitmovin/ms…

アイデア: Fmp4 カプセル化 + DataChannel 送信 + MSE デコードと再生。つまり、H.265 ビデオ データは最初に Fmp4 形式にカプセル化され、次に WebRTC DataChannel チャネルを介して送信され、フロント エンドはそれを受信した後、MSE を使用してビデオをデコードおよび再生します。

アドバンテージ:

  • 動画タグの再生を再利用します。レンダリングを個別に実装する必要はありません

  • オーディオとビデオは Fmp4 にカプセル化されており、Web 側でオーディオとビデオの同期を考慮する必要はありません。

  • Wecodecよりも全体の負荷が小さく、すぐに起動できる

欠点:

  • デバイス側での Fmp4 カプセル化はパフォーマンスに問題がある可能性があるため、リアルタイムのクラウド フォワーディングまたはフロントエンドのカプセル化解除によってカプセル化を解除する必要があります。

  • MSE デコードのリアルタイム パフォーマンスが良くない (クラウドでの最初のスライスには 1 ~ 2 秒の遅延があります)

5. プログラムの選択

最初のバージョンは、MSE として最初に起動されます。クラウドはフロントエンドの開発量が比較的少なく、ROIが高い。

遅延が少ないだけでなく、クラウドのトラフィック消費の問題を回避し、コストを節約できる、wecodec の 2 番目のバージョンが計画されています。第 2 版で WebRTC が公式に H.265 をサポートすると仮定すると、公式のソリューションと直接互換性があります。

5.1 Mse と SDK 変換の最初のバージョンについて詳しく説明する

メディア ソース拡張、メディア ソース拡張。公式ドキュメント: developer.mozilla.org/en-US/docs/…

MSE の導入により、HTML5 をサポートする Web ブラウザーは、ストリーミング プロトコルを解析できるプレーヤーになります。

別の見方をすれば、MSEを導入することで、HTML5タグはデフォルトでサポートされているmp4、m3u8、webm、oggなどのフォーマットを直接再生できるだけでなく、JSで処理できるビデオストリームフォーマットにも対応(MSE機能あり)。このように、元々サポートされていなかった一部のビデオ ストリーム フォーマットを、JS (MSE 機能を使用) を介して、サポートされているフォーマット (H.264 mp4、H.265 fmp4 など) に変換できます。

たとえば、ステーション B のオープン ソース flv.js は、典型的なアプリケーション シナリオです。Bilibili の HTML5 プレーヤーは、MSE テクノロジを使用して、JS (flv.js) を使用して FLV ソースを HTML5 対応のビデオ ストリーム エンコーディング形式にリアルタイムでトランスコードし、それを HTML5 プレーヤーに提供して再生します。

// 此 demo 来自下面链接的官方示例, 可以直接跑起来,比较直观
// https://github.com/bitmovin/mse-demo/blob/main/index.html
​
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>MSE Demo</title>
</head>
<body>
  <h1>MSE Demo</h1>
  <div>
    <video controls width="80%"></video>
  </div>
​
  <script type="text/javascript">
    (function() {
      var baseUrl = 'https://bitdash-a.akamaihd.net/content/MI201109210084_1/video/720_2400000/dash/';
      var initUrl = baseUrl + 'init.mp4';
      var templateUrl = baseUrl + 'segment_$Number$.m4s';
      var sourceBuffer;
      var index = 0;
      var numberOfChunks = 52;
      var video = document.querySelector('video');
​
      if (!window.MediaSource) {
        console.error('No Media Source API available');
        return;
      }
        
      // 初始化 mse
      var ms = new MediaSource();
      video.src = window.URL.createObjectURL(ms);
      ms.addEventListener('sourceopen', onMediaSourceOpen);
​
      function onMediaSourceOpen() {
        // codecs,初始化 sourceBuffer
        sourceBuffer = ms.addSourceBuffer('video/mp4; codecs="avc1.4d401f"');
        sourceBuffer.addEventListener('updateend', nextSegment);
​
        GET(initUrl, appendToBuffer);
        
        // 播放
        video.play();
      }
​
      function nextSegment() {
        var url = templateUrl.replace('$Number$', index);
        GET(url, appendToBuffer);
        index++;
        if (index > numberOfChunks) {
          sourceBuffer.removeEventListener('updateend', nextSegment);
        }
      }
​
      function appendToBuffer(videoChunk) {
        if (videoChunk) {
          // 二进制流转换为 Uint8Array,sourceBuffer 进行消费
          sourceBuffer.appendBuffer(new Uint8Array(videoChunk));
        }
      }
​
      function GET(url, callback) {
        var xhr = new XMLHttpRequest();
        xhr.open('GET', url);
        xhr.responseType = 'arraybuffer';
​
        xhr.onload = function(e) {
          if (xhr.status != 200) {
            console.warn('Unexpected status code ' + xhr.status + ' for ' + url);
            return false;
          }
          // 获取 mp4 二进制流
          callback(xhr.response);
        };
​
        xhr.send();
      }
    })();
  </script>
</body>
</html>

上記のデモとテスト (dmeo の fmp4 セグメントを独自の IPC デバイス (カメラ)、H.265 タイプに置き換える) により、chrome は H.265 タイプの fmp4 セグメントをハードデコードできます。それで、物事は明らかになりました一般的な方向性では、H.265 ネイキッド ストリーム、fmp4 クリップへの変換、およびハード ソリューションの基盤となるクロムにすぎません。

5.2 fmp4 フロントエンドのリアルタイムカプセル化解除

H.265 ネイキッド ストリームは fmp4 のカプセル化を解除します。調査の結果、純粋な js をカプセル化に使用すると、ワークロードがかなり重くなります。wasm を使用して C++ ライブラリを調整しようとしたところ、カプセル化解除のパフォーマンスもあまり良くないことがわかりました。そのため、フロントエンドで渡されました。

【学習アドレス】:FFmpeg/WebRTC/RTMP/NDK/Androidの音声・動画ストリーミングメディアの高度な開発

[記事の特典]: より多くのオーディオおよびビデオ学習パッケージ、Dachang インタビューの質問、テクニカル ビデオ、学習ロードマップを無料で受け取ることができます (C/C++、Linux、FFmpeg webRTC rtmp hls rtsp ffplay srs など) 1079654574 をクリックして参加ます受け取るグループ〜

5.3 fmp4 クラウド リアルタイム デカプセル化

良好なパフォーマンス、フロントエンドへの侵入ゼロ。クラウドのカプセル化解除を確認したら、この期間の開発で遭遇したコア リンクの進化と、最終的なプロセス プランについて説明します。

6.ステージ1

クラウドはリア​​ルタイムで Fmp4 のカプセル化を解除し、コーデック (オーディオおよびビデオのエンコーディング タイプ) をハードコードします -> フロントエンド MSE がデコードして再生します -> 数秒の再生後に失敗します。MSE は例外をスローします。これはおそらくデータが間違っていることを意味します、前後は接続できません。

調査の結果、MSE が更新中に消費が実行されず、データが直接失われ、その後のデータの接続に失敗したことが判明しました。失われないため、キャッシュします。詳細については、以下のコード コメントを参照してください。

詳細については、コード コメントを参照してください。

const updating = this.sourceBuffer?.updating === true;
const bufferQueueEmpty = this.bufferQueue.length === 0;
​
  if (!updating) {
    if (bufferQueueEmpty) {
      // 缓存队列为空: 仅消费本次 buffer
      this.appendBuffer(curBuffer);
    } else {
      // 缓存队列不为空: 消费队列 + 本次 buffer
      this.bufferQueue.push(curBuffer);
​
      // 队列中多个 buffer 的合并
      const totalBufferByteLen = this.bufferQueue.reduce(
        (pre, cur) => pre + cur.byteLength,
        0
      );
      const combinedBuffer = new Uint8Array(totalBufferByteLen);
      let offset = 0;
      this.bufferQueue.forEach((array, index) => {
        offset += index > 0 ? this.bufferQueue[index - 1].length : 0;
        combinedBuffer.set(array, offset);
      });
​
      this.appendBuffer(combinedBuffer);
      this.bufferQueue = [];
    }
  } else {
    // mse 还在消费上一次 buffer(处于 updating 中), 缓存本次 buffer, 否则会有丢帧问题
    this.bufferQueue.push(curBuffer);
  }

Fmp4 データのすべてのフレームが失われないことを考慮して、データチャネルは信頼性の高い伝送を使用します。

しかし、テスト後、新たな問題が発見されました。レイテンシは、時間の経過とともに累積的に増加します。パケットが失われた後、ネットワーク層が再試行し、再試行時間が遅延に蓄積されるためです。ネットワークの状態が悪い場合、遅延が 30 秒以上になることをテストしました。理論的には、ストリームを十分に長くプルすると、遅延は増加し続けます

7. フェーズ 2

別の考え方として、フレーム損失なし + 信頼性の高い伝送によって引き起こされる遅延の問題はまったく受け入れられないため、代わりに信頼性の低い伝送を使用するとどうなるでしょうか?

信頼性の低い送信は、フレームがドロップされることを意味します。調査の結果、Fmp4 はスライス全体 (スライスには複数のフレームが含まれます) を破棄することができます. この場合、一連のフレーム損失アルゴリズムを設計できます. スライスが不完全であると判断される限り、スライス全体を破棄します.

この場合、理論的に言えば、遅延は最大で 1 スライス、約 2 秒であり、ビジネス レイヤーで許容されます。

フレーム損失アルゴリズムの設計思想: 各フレームのデータ ヘッダーに 4 バイトのデータを追加して、各フレームの特定の情報を識別します。

  • segNum: 2 バイト、ビッグエンディアン モード、Fmp4 セグメント シーケンス番号、1 から始まり、毎回 1 ずつ増加

  • fragCount: 1 バイト、Fmp4 フラグメントの総数、最小値は 1

  • fragSeq: 1 バイト、1 から始まる Fmp4 フラグメント フラグメント シーケンス番号

フロントエンドは、データの各フレームを取得した後、最初の 4 バイトを解析して、データの各フレームの詳細情報を取得します。たとえば、現在のフレームが最後のフレームかどうかを判断したい場合、fragCount が fragSeq と等しいかどうかを判断するだけで済みます。

アルゴリズムの一般的なフローチャート:

詳しく説明してください:

  • 各フレームのデータをキャッシュするために使用され、次のフレームのデータと比較して完全なフレームであるかどうかを判断するために使用される frameQueue

  • bufferQueue、このキュー内のデータはデータの完全なスライスであり、MSE が消費するときに欠落したデータがないことを保証します

  /**
   * fmp4 切片队列 frameQueue,处理丢帧,生产 bufferQueue 内容
   *
   * @param frameObj 每一帧的相关数据
   *      每来一帧进行判断
   *      buffer中加上当前帧是否为连续帧(从第一帧开始的连续帧)
   *        是
   *          当前帧是否为最后一帧
   *            是 拼接buffer帧以及当前帧,组成完整帧,放入另外一个待消费 buffer
   *            否 当前帧入 buffer
   *        否 清空 buffer,当前帧入 buffer
   */
​
const frameQueueLen = this.frameQueue.length;
const frameQueueEmpty = frameQueueLen === 0;
​
  // 单一完整分片帧单独处理,直接进行消费
  if (frameObj.fragCount === 1) {
    if (!frameQueueEmpty) {
      this.frameQueue = [];
    }
    this.bufferQueue.push(frameObj.value);
    return;
  }
​
  if (frameQueueEmpty) {
    this.frameQueue.push(frameObj);
    return;
  }
​
  // 是否为首帧
  let isFirstFragSeq = this.frameQueue[0].fragSeq === 1;
  // 当前帧加上queue帧是否为连续帧
  let isContinuousFragSeq = true;
  for (let i = 0; i < frameQueueLen; i++) {
    const isLast = i === frameQueueLen - 1;
​
    const curFragSeq = this.frameQueue[i].fragSeq;
    const nextFragSeq = isLast
      ? frameObj.fragSeq
      : this.frameQueue[i + 1].fragSeq;
​
    const curSegNum = this.frameQueue[i].segNum;
    const nextSeqNum = isLast
      ? frameObj.segNum
      : this.frameQueue[i + 1].segNum;
​
    if (curFragSeq + 1 !== nextFragSeq || curSegNum !== nextSeqNum) {
      isContinuousFragSeq = false;
      break;
    }
  }
​
  if (isFirstFragSeq && isContinuousFragSeq) {
    // 是否为最后一帧
    const isLastFrame = frameObj.fragCount === frameObj.fragSeq;
    if (isLastFrame) {
      this.frameQueue.forEach((item) => {
        this.bufferQueue.push(item.value);
      });
      this.frameQueue = [];
      this.bufferQueue.push(frameObj.value);
    } else {
      this.frameQueue.push(frameObj);
    }
  } else {
    // 丢帧则清空 frameQueue,则代表直接丢弃整个 segment 切片
    this.emit(EVENTS_ERROR.frameDropError);
    this.frameQueue = [];
    this.frameQueue.push(frameObj);
  }

終わったと思ったが、思わぬことが起きた。

フレームロスが発生すると、上記のアルゴリズムは実際にスライス全体のデータを破棄しますが、この時点で再び MSE が異常になります。つまり、データ シーケンスが正しくないため、解析に失敗します。

しかし、ffplay を使用してローカルでテストすると (スライス全体を失った後も引き続き再生できます)、デッドロック状態になり、調査を続けました。

8. ステージ 3

最近chatgptが流行っていないと言われていましたが、使ってみたところその理由がわかりました。MSE が fmp4 データを消費する場合、内部シリアル番号に従ってインデックス識別を実行する必要があるため、スライス データ全体が失われた場合でも、再生は失敗します。何をすべきか?信頼性の低い輸送に戻るのでしょうか?

ある程度の検討の結果、フレーム ロスが発生すると、フロントエンドがクラウドに再スライスを通知し、この時点でフロントエンドが MSE を再初期化することが最終的に決定されました。

変換後、効果は悪くないことがわかりました.信頼性の低い送信とデータチャネルの再送信回数を5に設定しました.

フレーム ロスの可能性が大幅に減少します. フレーム ロスが発生した場合でも、読み込みに 2 秒未満しかかからず、その後は画面を表示し続けることができます。

最後に、上記の 3 つの段階を変換すると、リンク ダイアグラム全体が得られます。もちろん、mp4box を使用してコーデックを取得したり、フロントエンドがデータ チャネルの状態を定期的にチェックしたりするなど、まだ詳細については触れていないので、ここでは詳しく説明しません。興味のある方はメッセージを残して議論してください

完全なリンク図は単純に描かれています。

9. まとめ

現在、データチャネル + MSE ソリューションが開始されており、テストの結果、オンラインで 16 チャネルを同時にハードコーディングしてもパフォーマンスの問題はありません。

将来的には、Web コードを使用して H.265 を分析し、オーディオとビデオの同期などの問題に対処する予定です。遅延の問題を完全に解決します。

次の記事では、 WebRTC の問題に関する日常的なトラブルシューティングのアイデアをいくつか書く予定です. また、コメント欄で日常生活で遭遇するいくつかの問題について話し、次の記事でそれらをまとめてください.

元リンク:「WebRTCシリーズ」実戦Web側はh265ハードソリューションに対応 - Nuggets

おすすめ

転載: blog.csdn.net/irainsa/article/details/130020347