この記事では、Adobe Flash Player などのサードパーティのブラウザ プラグインを使用せずに、WebSocket 経由でビデオをストリーミングする試みを共有したいと思います。 それから何が起こったのかを知るために読んでください。
Adobe Flash (旧名 Macromedia Flash) は、Web ブラウザで実行されるアプリケーションを作成するためのプラットフォームです。 Media Stream API が導入される前は、Web カメラからビデオや音声をストリーミングしたり、ブラウザでさまざまな種類の会議やチャットを作成したりするための事実上唯一のプラットフォームでした。 メディア情報を送信するためのプロトコル RTMP (Real Time Messaging Protocol) は、実際には長い間閉鎖されていました。これは、ストリーミング サービスを強化したい場合は、Adobe 自体のソフトウェアである Adobe Media Server (AMS) を使用することを意味します。
2012 年になってしばらくして、Adobe は「あきらめて、それを一般に吐き出しました」。
Adobe Flash プラットフォームは 20 年以上前から存在しており、その間に多くの重大な脆弱性が発見され、サポートされています。
私のプロジェクトでは、ブラウザでの Flash の使用を完全に放棄することにすぐに決めました。 上で主な理由を示しましたが、Flash はモバイル プラットフォームでもまったくサポートされておらず、Windows (ワイン エミュレータ) での開発のために Adobe Flash を導入したくありませんでした。 そこで、JavaScript でクライアントを作成することにしました。 これは単なるプロトタイプになります。後で、ストリーミングは p2p に基づいてはるかに効率的に実行できることを知りました。私にとってはピア - サーバー - ピアになるだけですが、まだ準備ができていないため、これについては別の機会に説明します。
始めるには、実際の WebSocket サーバーが必要です。 私は、melody go パッケージに基づいて最も単純なものを作成しました。
サーバーコード
package main
import (
"errors"
"github.com/go-chi/chi"
"gopkg.in/olahol/melody.v1"
"log"
"net/http"
"time"
)
func main() {
r := chi.NewRouter()
m := melody.New()
m.Config.MaxMessageSize = 204800
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "public/index.html")
})
r.Get("/ws", func(w http.ResponseWriter, r *http.Request) {
m.HandleRequest(w, r)
})
// Бродкастим видео поток
m.HandleMessageBinary(func(s *melody.Session, msg []byte) {
m.BroadcastBinary(msg)
})
log.Println("Starting server...")
http.ListenAndServe(":3000", r)
}
クライアント (ストリーミング側) では、まずカメラにアクセスする必要があります。 これは次の方法で行われます
私たちは、以下を通じてカメラ/マイクへのアクセス(許可)を取得します。
次に、getUserMedia() は Promise を返し、それに MediaStream オブジェクト (ビデオ/オーディオ データのストリーム) を渡します。 このオブジェクトを video 要素の src プロパティに割り当てます。 コード:
放送側
<style>
#videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; }
</style>
</head>
<body>
<!-- Здесь в этом "окошечке" клиент будет видеть себя -->
<video autoplay id="videoObjectHtml5ApiServer"></video>
<script type="application/javascript">
var
video = document.getElementById('videoObjectHtml5ApiServer');
// если доступен MediaDevices API, пытаемся получить доступ к камере (можно еще и к микрофону)
// getUserMedia вернет обещание, на которое подписываемся и полученный видеопоток в колбеке направляем в video объект на странице
if (navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({video: true}).then(function (stream) {
// видео поток привязываем к video тегу, чтобы клиент мог видеть себя и контролировать
video.srcObject = stream;
});
}
</script>
ソケット経由でビデオ ストリームをブロードキャストするには、ビデオ ストリームをどこかでエンコードし、バッファリングして、部分的に送信する必要があります。 生のビデオ ストリームは WebSocket 経由で送信できません。 ここが私たちの助けになります
ビデオストリームをエンコードし、部分に分割します
<style>
#videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; }
</style>
</head>
<body>
<!-- Здесь в этом "окошечке" клиент будет видеть себя -->
<video autoplay id="videoObjectHtml5ApiServer"></video>
<script type="application/javascript">
var
video = document.getElementById('videoObjectHtml5ApiServer');
// если доступен MediaDevices API, пытаемся получить доступ к камере (можно еще и к микрофону)
// getUserMedia вернет обещание, на которое подписываемся и полученный видеопоток в колбеке направляем в video объект на странице
if (navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({video: true}).then(function (stream) {
// видео поток привязываем к video тегу, чтобы клиент мог видеть себя и контролировать
video.srcObject = s;
var
recorderOptions = {
mimeType: 'video/webm; codecs=vp8' // будем кодировать видеопоток в формат webm кодеком vp8
},
mediaRecorder = new MediaRecorder(s, recorderOptions ); // объект MediaRecorder
mediaRecorder.ondataavailable = function(e) {
if (e.data && e.data.size > 0) {
// получаем кусочек видеопотока в e.data
}
}
mediaRecorder.start(100); // делит поток на кусочки по 100 мс каждый
});
}
</script>
次に、WebSocket 経由の送信を追加しましょう。 驚くべきことに、これに必要なのはオブジェクトだけです
ビデオストリームをサーバーに送信します
<style>
#videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; }
</style>
</head>
<body>
<!-- Здесь в этом "окошечке" клиент будет видеть себя -->
<video autoplay id="videoObjectHtml5ApiServer"></video>
<script type="application/javascript">
var
video = document.getElementById('videoObjectHtml5ApiServer');
// если доступен MediaDevices API, пытаемся получить доступ к камере (можно еще и к микрофону)
// getUserMedia вернет обещание, на которое подписываемся и полученный видеопоток в колбеке направляем в video объект на странице
if (navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({video: true}).then(function (stream) {
// видео поток привязываем к video тегу, чтобы клиент мог видеть себя и контролировать
video.srcObject = s;
var
recorderOptions = {
mimeType: 'video/webm; codecs=vp8' // будем кодировать видеопоток в формат webm кодеком vp8
},
mediaRecorder = new MediaRecorder(s, recorderOptions ), // объект MediaRecorder
socket = new WebSocket('ws://127.0.0.1:3000/ws');
mediaRecorder.ondataavailable = function(e) {
if (e.data && e.data.size > 0) {
// получаем кусочек видеопотока в e.data
socket.send(e.data);
}
}
mediaRecorder.start(100); // делит поток на кусочки по 100 мс каждый
}).catch(function (err) { console.log(err); });
}
</script>
放送側の準備は完了です! 次に、ビデオ ストリームを受信してクライアントに表示してみましょう。 そのためには何が必要なのでしょうか? まずはもちろんソケット接続です。 「リスナー」を WebSocket オブジェクトにアタッチし、「メッセージ」イベントをサブスクライブします。 バイナリ データを受信したサーバーは、それをサブスクライバー、つまりクライアントにブロードキャストします。 この場合、「メッセージ」イベントの「リスナー」に関連付けられたコールバック関数がクライアントでトリガーされ、オブジェクト自体が関数の引数、つまり vp8 によってエンコードされたビデオ ストリームの一部に渡されます。
ビデオストリームを受け入れます
<style>
#videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; }
</style>
</head>
<body>
<!-- Здесь в этом "окошечке" клиент будет видеть тебя -->
<video autoplay id="videoObjectHtml5ApiServer"></video>
<script type="application/javascript">
var
video = document.getElementById('videoObjectHtml5ApiServer'),
socket = new WebSocket('ws://127.0.0.1:3000/ws'),
arrayOfBlobs = [];
socket.addEventListener('message', function (event) {
// "кладем" полученный кусочек в массив
arrayOfBlobs.push(event.data);
// здесь будем читать кусочки
readChunk();
});
</script>
長い間、私は受信した部分を再生のためにビデオ要素にすぐに送信することがなぜ不可能なのかを理解しようとしましたが、これは不可能であることがわかりました。もちろん、最初にその部分をバインドされた特別なバッファーに配置する必要があります。 video 要素を追加すると、ビデオ ストリームの再生が開始されます。 このために必要となるのは、
MediaSource は、メディア再生オブジェクトとこのメディア ストリームのソースの間の一種の仲介者として機能します。 MediaSource オブジェクトには、ビデオ/オーディオ ストリームのソース用のプラグ可能バッファが含まれています。 特徴の 8 つは、バッファーが保持できるのは UintXNUMX データのみであるため、そのようなバッファーを作成するには FileReader が必要になることです。 コードを見ると、より明確になります。
ビデオストリームの再生
<style>
#videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; }
</style>
</head>
<body>
<!-- Здесь в этом "окошечке" клиент будет видеть тебя -->
<video autoplay id="videoObjectHtml5ApiServer"></video>
<script type="application/javascript">
var
video = document.getElementById('videoObjectHtml5ApiServer'),
socket = new WebSocket('ws://127.0.0.1:3000/ws'),
mediaSource = new MediaSource(), // объект MediaSource
vid2url = URL.createObjectURL(mediaSource), // создаем объект URL для связывания видеопотока с проигрывателем
arrayOfBlobs = [],
sourceBuffer = null; // буфер, пока нуль-объект
socket.addEventListener('message', function (event) {
// "кладем" полученный кусочек в массив
arrayOfBlobs.push(event.data);
// здесь будем читать кусочки
readChunk();
});
// как только MediaSource будет оповещен , что источник готов отдавать кусочки
// видео/аудио потока
// создаем буфер , следует обратить внимание, что буфер должен знать в каком формате
// каким кодеком был закодирован поток, чтобы тем же способом прочитать видеопоток
mediaSource.addEventListener('sourceopen', function() {
var mediaSource = this;
sourceBuffer = mediaSource.addSourceBuffer("video/webm; codecs="vp8"");
});
function readChunk() {
var reader = new FileReader();
reader.onload = function(e) {
// как только FileReader будет готов, и загрузит себе кусочек видеопотока
// мы "прицепляем" перекодированный в Uint8Array (был Blob) кусочек в буфер, связанный
// с проигрывателем, и проигрыватель начинает воспроизводить полученный кусочек видео/аудио
sourceBuffer.appendBuffer(new Uint8Array(e.target.result));
reader.onload = null;
}
reader.readAsArrayBuffer(arrayOfBlobs.shift());
}
</script>
ストリーミングサービスのプロトタイプが完成しました。 主な欠点は、ビデオの再生が送信側より 100 ミリ秒遅れることです。サーバーに送信する前にビデオ ストリームを分割するときに、これを自分で設定します。 さらに、ノートパソコンで確認してみると、送信側と受信側のラグが徐々に蓄積されていくのがはっきりと分かりました。 このデメリットを克服する方法を探し始めたところ、見つけました。
出所: habr.com