Node.jsを使用してビデオファイルをhtml5ビデオプレーヤーにストリーミングして、ビデオコントロールが引き続き機能するようにしますか?


96

Tl; Dr-質問:

Node.jsを使用してhtml5ビデオプレーヤーへのビデオファイルのストリーミングを処理し、ビデオコントロールが引き続き機能するようにする正しい方法は何ですか?

私は考えてそれはヘッダが処理される方法に関係しています。とにかく、ここに背景情報があります。コードは少し長いですが、それはかなり簡単です。

Nodeを使用して小さなビデオファイルをHTML5ビデオにストリーミングするのは簡単です

小さなビデオファイルを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);

ただし、この方法は1GB未満のサイズのファイルに制限されています。

ストリーミング(任意のサイズ)ビデオファイル 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);
});

これはビデオをうまくストリーミングします!しかし、ビデオコントロールは機能しなくなりました。


1
私はそのwriteHead()コードをコメントのままにしましたが、それが役立つ場合に備えてそこにあります。コードスニペットを読みやすくするために、これを削除する必要がありますか?
WebDeveloper404 2014

3
req.headers.rangeはどこから来たのですか?replaceメソッドを実行しようとすると、未定義になり続けます。ありがとう。
チャドワトキンス2015年

回答:


118

Accept Rangesヘッダは(のビットwriteHead())ワークにHTML5ビデオコントロールのために必要とされます。

やみくもにファイル全体を送信するのではなく、最初Accept RangesにREQUESTのヘッダーを確認してから、そのビットだけを読み込んで送信する必要があると思います。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);

3
この戦略を使用して、映画の一部、つまり5秒から7秒の間だけを送信できますか?ライブラリのようなffmpegによって、その間隔がどのバイト間隔に対応するかを見つける方法はありますか?ありがとう。
pembeci 2015

8
私の質問を気にしないでください。私が求めたことを達成する方法を見つけるための魔法の言葉を見つけました:疑似ストリーミング
pembeci 2015

何らかの理由でmovie.mp4が暗号化された形式であり、ブラウザにストリーミングする前に復号化する必要がある場合、これをどのように機能させることができますか?
サラフ2015

@saraf:暗号化に使用されるアルゴリズムによって異なります。ストリームで機能しますか、それともファイル全体の暗号化としてのみ機能しますか?ビデオを一時的な場所に復号化して、通常どおりに提供することは可能ですか?一般的に言えば、それは可能ですが、注意が必要です。ここには一般的な解決策はありません。
2015

こんにちは、tungd、応答してくれてありがとう!ユースケースは、教育コンテンツ開発者向けのメディア配信プラットフォームとして機能するラズベリーパイベースのデバイスです。暗号化アルゴリズムは自由に選択できます。キーはファームウェアにありますが、メモリは1GBのRAMに制限されており、コンテンツサイズは約200GBです(リムーバブルメディアにあります-USB接続されています)。 EMEは大丈夫です-クロムがARMにEMEを組み込んでいないという問題を除いて。リムーバブルメディアだけでは、再生/コピーを有効にするのに十分ではないはずです。
サラフ2015

24

この質問に対する受け入れられた答えは素晴らしいものであり、受け入れられた答えのままである必要があります。ただし、読み取りストリームが常に終了/閉じられるとは限らないコードで問題が発生しました。解決策の一部は、2番目の引数でautoClose: true一緒に送信することでした。start:start, end:endcreateReadStream

解決策の他の部分は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;
}

これには、各リクエストの後に読み取りストリームが終了/閉じられ、ブラウザによって存続されないようにする効果があります。

また、この問題をカバーする別のStackOverflowの質問回答を作成しました。


これはChromeには最適ですが、Safariでは機能しないようです。サファリでは、範囲全体を要求できる場合にのみ機能するようです。Safariで何か違うことをしていますか?
f1lt3r 2017年

2
さらに掘り下げると、Safariは2バイトの応答で「/ $ {total}」を確認し、「ファイル全体を送ってくれませんか?」と言います。次に、「いいえ、最初の1Mbしか取得できません!」と言われると、Safariは「リソースをリードしようとしてエラーが発生しました」と動揺します。
f1lt3r 2017年

0

まずapp.js、公開するディレクトリにファイルを作成します。

var http = require('http');
var fs = require('fs');
var mime = require('mime');
http.createServer(function(req,res){
    if (req.url != '/app.js') {
    var url = __dirname + req.url;
        fs.stat(url,function(err,stat){
            if (err) {
            res.writeHead(404,{'Content-Type':'text/html'});
            res.end('Your requested URI('+req.url+') wasn\'t found on our server');
            } else {
            var type = mime.getType(url);
            var fileSize = stat.size;
            var range = req.headers.range;
                if (range) {
                    var parts = range.replace(/bytes=/, "").split("-");
                var start = parseInt(parts[0], 10);
                    var end = parts[1] ? parseInt(parts[1], 10) : fileSize-1;
                    var chunksize = (end-start)+1;
                    var file = fs.createReadStream(url, {start, end});
                    var head = {
                'Content-Range': `bytes ${start}-${end}/${fileSize}`,
                'Accept-Ranges': 'bytes',
                'Content-Length': chunksize,
                'Content-Type': type
                }
                    res.writeHead(206, head);
                    file.pipe(res);
                    } else {    
                    var head = {
                'Content-Length': fileSize,
                'Content-Type': type
                    }
                res.writeHead(200, head);
                fs.createReadStream(url).pipe(res);
                    }
            }
        });
    } else {
    res.writeHead(403,{'Content-Type':'text/html'});
    res.end('Sorry, access to that file is Forbidden');
    }
}).listen(8080);

実行するだけnode app.jsで、サーバーはポート8080で実行されます。ビデオの他に、あらゆる種類のファイルをストリーミングできます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.