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


97

Tl; Dr - Вопрос:

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

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

Транслировать небольшие видеофайлы в видео HTML5 с помощью Node очень просто

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

Но этот метод ограничен файлами размером <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);
});

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


1
Я оставил этот writeHead()код прокомментированным, но на тот случай, если это поможет. Следует ли мне удалить это, чтобы сделать фрагмент кода более читабельным?
WebDeveloper404

3
откуда берется req.headers.range? Я все время получаю undefined, когда пытаюсь выполнить замену. Спасибо.
Чад Уоткинс,

Ответы:


118

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

3
Можем ли мы использовать эту стратегию для отправки только некоторой части фильма, то есть между 5-й и 7-й секундами? Есть ли способ найти, какой этот интервал соответствует какому байтовому интервалу с помощью ffmpeg, как библиотеки? Спасибо.
pembeci

8
Забудь о моем вопросе. Я нашел волшебные слова, чтобы узнать, как достичь того, о чем я просил: псевдостриминг .
pembeci

Как это можно сделать, если по какой-то причине movie.mp4 находится в зашифрованном формате и нам нужно его расшифровать перед потоковой передачей в браузер?
Сараф,

@saraf: Это зависит от того, какой алгоритм используется для шифрования. Это работает с потоком или работает только с шифрованием всего файла? Возможно ли, что вы просто расшифровываете видео во временное хранилище и обслуживаете его как обычно? Вообще говоря, это возможно, но это может быть сложно. Здесь нет общего решения.
tungd

Привет, tungd, спасибо за ответ! Пример использования - устройство на основе Raspberry Pi, которое будет выступать в качестве платформы распространения мультимедиа для разработчиков образовательного контента. Мы можем выбрать алгоритм шифрования, ключ будет во встроенном ПО, но объем памяти ограничен 1 ГБ ОЗУ, а размер содержимого составляет около 200 ГБ (который будет на съемном носителе - с подключением USB). Даже что-то вроде алгоритма Clear Key с EME было бы хорошо - за исключением проблемы, что хром не имеет встроенного EME на ARM. Одного только съемного носителя не должно хватить для воспроизведения / копирования.
Сараф

25

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

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


Это отлично работает для Chrome, но, похоже, не работает в Safari. В сафари это работает, только если он может запрашивать весь диапазон. Вы делаете что-нибудь по-другому для Safari?
f1lt3r

2
При дальнейшем копании: Safari видит "/ $ {total}" в 2-байтовом ответе, а затем говорит ... «Эй, как насчет того, чтобы вы просто отправили мне весь файл?». Затем, когда ему говорят: «Нет, вы получаете только первый 1Мб!», Safari расстраивается: «Произошла ошибка при попытке вести ресурс».
f1lt3r

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. Помимо видео, он может передавать все виды файлов.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.