Подтвердить что ты не робот

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

Tl; Dr - Вопрос:

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

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

Потоковая передача небольших видеофайлов в видео HTML5 с помощью Node проста

Я научился очень легко передавать небольшие видеофайлы на видеоплеер 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 ГБ.

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

Это потоковое видео просто отлично! Но управление видео перестает работать.

4b9b3361

Ответ 1

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

Я думаю, вместо того, чтобы просто вслепую отправить полный файл, вы должны сначала проверить заголовок 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

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

Другая часть решения заключалась в том, чтобы ограничить 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 и ответ, посвященный этой проблеме.