Потоковая передача видеофайла в видеоплеер html5 с узлом.js так, чтобы элементы управления видео продолжали работать?


Tl; Dr-Вопрос:

Как правильно обрабатывать потоковую передачу видеофайла на видеоплеер html5 с узлом.js так что элементы управления видео продолжают работать?

Я думаю это связано с тем, что заголовки обрабатываются. В любом случае, вот справочная информация. Код мало длительный, однако, это довольно просто.

потоковая передача небольших видеофайлов HTML5 Видео с узлом легко

я очень легко научился передавать небольшие видеофайлы на видеоплеер HTML5. С этой настройкой элементы управления работают без какой-либо работы с моей стороны, и видеопотоки безупречно. Рабочая копия полностью рабочего кода с образцом видео является здесь, для загрузки на Google Docs.

клиент:

<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);

но этот метод ограничивается файлами размером

потоковое (любого размера) видео файлы с 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);
});

это потоки видео просто отлично! Но видео-элементы управления больше не работают.

2 75

2 ответа:

The Accept Ranges заголовок (бит в writeHead()) требуется для работы элементов управления видео HTML5.

Я думаю, что вместо того, чтобы просто слепо отправить полный файл, вы должны сначала проверить Accept Ranges заголовок в запросе, затем прочитайте и отправьте только этот бит. fs.createReadStream поддержка start и для этого.

поэтому я попробовал пример, и он работает. Код не очень красивый, но его легко понять. Сначала мы обрабатываем заголовок диапазона, чтобы получить начальную/конечную позицию. Тогда мы используем 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);

принятый ответ на этот вопрос является удивительным и должно быть принято отвечать. Однако я столкнулся с проблемой с кодом, где поток чтения не всегда заканчивался / закрывался. Частью решения было отправить autoClose: true вместе с start:start, end:end во втором createReadStream arg.

другая часть решения заключалась в ограничении max chunksize отправляется в ответ. Другой набор ответов end вот так:

var end = positions[1] ? parseInt(positions[1], 10) : total - 1;

...который имеет эффект отправки остальной части файла из запрошенной начальной позиции через его последний байт, независимо от того, сколько байтов это может быть. Однако клиентский браузер имеет возможность читать только часть этого потока и будет, если ему еще не нужны все байты. Это приведет к тому, что поток чтения будет заблокирован, пока браузер не решит, что пришло время получить больше данных (например, действие пользователя, такое как поиск/скраб, или просто воспроизведение потока).

мне нужно было, чтобы этот поток был закрыт потому что я показывал <video> элемент на странице, который позволил пользователю удалить видеофайл. Однако файл не удалялся из файловой системы до тех пор, пока клиент (или сервер) не закрыл соединение, потому что это единственный способ завершить/закрыть поток.

мое решение было просто поставил maxChunk переменная конфигурации, установите ее в 1 МБ и никогда не передавайте поток чтения более 1 МБ за раз в ответ.

// 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 вопрос и ответ по этому вопросу.