Node.jsを使用してhtml5ビデオプレーヤーへのビデオファイルのストリーミングを処理する正しい方法は何ですか、ビデオコントロールが引き続き機能するようにしますか?
Ithinkこれは、ヘッダーの処理方法に関係しています。とにかく、ここに背景情報があります。コードはlittle長くなりますが、非常に簡単です。
小さなビデオファイルをHTML5ビデオプレーヤーに非常に簡単にストリーミングする方法を学びました。 このセットアップでは、コントロールは自分の作業なしで機能し、ビデオは問題なくストリーミングされます。サンプルビデオを含む完全に機能するコードの作業コピーは、 ここでは、Googleドキュメントでのダウンロード用 です。
クライアント:
<html>
<title>Welcome</title>
<body>
<video controls>
<source src="movie.mp4" type="video/mp4"/>
<source src="movie.webm" type="video/webm"/>
<source src="movie.ogg" type="video/ogg"/>
<!-- fallback -->
Your browser does not support the <code>video</code> element.
</video>
</body>
</html>
サーバー:
// Declare Vars & Read Files
var fs = require('fs'),
http = require('http'),
url = require('url'),
path = require('path');
var movie_webm, movie_mp4, movie_ogg;
// ... [snip] ... (Read index page)
fs.readFile(path.resolve(__dirname,"movie.mp4"), function (err, data) {
if (err) {
throw err;
}
movie_mp4 = data;
});
// ... [snip] ... (Read two other formats for the video)
// Serve & Stream Video
http.createServer(function (req, res) {
// ... [snip] ... (Serve client files)
var total;
if (reqResource == "/movie.mp4") {
total = movie_mp4.length;
}
// ... [snip] ... handle two other formats for the video
var range = req.headers.range;
var positions = range.replace(/bytes=/, "").split("-");
var start = parseInt(positions[0], 10);
var end = positions[1] ? parseInt(positions[1], 10) : total - 1;
var chunksize = (end - start) + 1;
if (reqResource == "/movie.mp4") {
res.writeHead(206, {
"Content-Range": "bytes " + start + "-" + end + "/" + total,
"Accept-Ranges": "bytes",
"Content-Length": chunksize,
"Content-Type": "video/mp4"
});
res.end(movie_mp4.slice(start, end + 1), "binary");
}
// ... [snip] ... handle two other formats for the video
}).listen(8888);
ただし、この方法は1 GB未満のファイルに制限されています。
fs.createReadStream
を使用したストリーミング(任意のサイズ)ビデオファイルfs.createReadStream()
を利用することで、サーバーは一度にすべてのファイルをメモリに読み込むのではなく、ストリーム内のファイルを読み込むことができます。これは物事を行う正しい方法のように聞こえます、そして構文は非常に簡単です:
サーバースニペット:
movieStream = fs.createReadStream(pathToFile);
movieStream.on('open', function () {
res.writeHead(206, {
"Content-Range": "bytes " + start + "-" + end + "/" + total,
"Accept-Ranges": "bytes",
"Content-Length": chunksize,
"Content-Type": "video/mp4"
});
// This just pipes the read stream to the response object (which goes
//to the client)
movieStream.pipe(res);
});
movieStream.on('error', function (err) {
res.end(err);
});
これにより、ビデオが正常にストリーミングされます!しかし、ビデオコントロールは機能しなくなりました。
HTML5ビデオコントロールを機能させるには、Accept Ranges
ヘッダー(writeHead()
のビット)が必要です。
盲目的にファイル全体を送信するのではなく、最初にREQUESTのAccept Ranges
ヘッダーを確認してから、そのビットを読み取って送信する必要があると思います。 fs.createReadStream
はstart
をサポートし、そのためのend
オプションをサポートしています。
だから私は例を試してみましたが、それは動作します。コードはきれいではありませんが、理解するのは簡単です。最初に、範囲ヘッダーを処理して開始/終了位置を取得します。次に、fs.stat
を使用して、ファイル全体をメモリに読み込まずにファイルのサイズを取得します。最後に、fs.createReadStream
を使用して、要求された部分をクライアントに送信します。
var fs = require("fs"),
http = require("http"),
url = require("url"),
path = require("path");
http.createServer(function (req, res) {
if (req.url != "/movie.mp4") {
res.writeHead(200, { "Content-Type": "text/html" });
res.end('<video src="http://localhost:8888/movie.mp4" controls></video>');
} else {
var file = path.resolve(__dirname,"movie.mp4");
fs.stat(file, function(err, stats) {
if (err) {
if (err.code === 'ENOENT') {
// 404 Error if file not found
return res.sendStatus(404);
}
res.end(err);
}
var range = req.headers.range;
if (!range) {
// 416 Wrong range
return res.sendStatus(416);
}
var positions = range.replace(/bytes=/, "").split("-");
var start = parseInt(positions[0], 10);
var total = stats.size;
var end = positions[1] ? parseInt(positions[1], 10) : total - 1;
var chunksize = (end - start) + 1;
res.writeHead(206, {
"Content-Range": "bytes " + start + "-" + end + "/" + total,
"Accept-Ranges": "bytes",
"Content-Length": chunksize,
"Content-Type": "video/mp4"
});
var stream = fs.createReadStream(file, { start: start, end: end })
.on("open", function() {
stream.pipe(res);
}).on("error", function(err) {
res.end(err);
});
});
}
}).listen(8888);
この質問に対する受け入れられた答え は素晴らしく、受け入れられた答えのままでなければなりません。しかし、読み取りストリームが常に終了/終了されないコードの問題に遭遇しました。ソリューションの一部は、2番目のcreateReadStream
引数でautoClose: true
をstart:start, end:end
とともに送信することでした。
ソリューションの他の部分は、応答で送信される最大chunksize
を制限することでした。他の答えはend
を次のように設定します。
var end = positions[1] ? parseInt(positions[1], 10) : total - 1;
...要求された開始位置から最後のバイトまで、残りのファイルを送信する効果があります。ただし、クライアントブラウザーには、そのストリームの一部のみを読み取るオプションがあり、まだすべてのバイトが必要でない場合はそうします。これにより、ブラウザーがさらにデータを取得する時間を決定するまで、ストリームの読み取りがブロックされます(たとえば、シーク/スクラブなどのユーザーアクション、またはストリームの再生のみ)。
ユーザーがビデオファイルを削除できるページに<video>
要素を表示していたため、このストリームを閉じる必要がありました。しかし、クライアント(またはサーバー)が接続を閉じるまでファイルはファイルシステムから削除されませんでした。これがストリームが終了/閉じられる唯一の方法だからです。
私の解決策は、単にmaxChunk
構成変数を設定し、1MBに設定し、一度に1MBを超える読み取りストリームを応答にパイプしないことでした。
// same code as accepted answer
var end = positions[1] ? parseInt(positions[1], 10) : total - 1;
var chunksize = (end - start) + 1;
// poor hack to send smaller chunks to the browser
var maxChunk = 1024 * 1024; // 1MB at a time
if (chunksize > maxChunk) {
end = start + maxChunk - 1;
chunksize = (end - start) + 1;
}
これには、読み取りストリームが各リクエストの後に終了/クローズされ、ブラウザによって維持されないようにする効果があります。