Truyền trực tuyến tệp video tới trình phát video html5 với Node.js để các điều khiển video tiếp tục hoạt động?


96

Tl; Tiến sĩ - Câu hỏi:

Đâu là cách phù hợp để xử lý việc truyền tệp video sang trình phát video html5 với Node.js để các điều khiển video tiếp tục hoạt động là gì?

Tôi nghĩ nó liên quan đến cách xử lý các tiêu đề. Dù sao, đây là thông tin cơ bản. Mã là mộtTuy nhiên, hơi dài dòng.

Dễ dàng truyền trực tuyến các tệp video nhỏ sang video HTML5 với Node

Tôi đã học cách truyền các tệp video nhỏ sang trình phát video HTML5 rất dễ dàng. Với thiết lập này, các điều khiển hoạt động mà không cần bất kỳ công việc nào của tôi và video phát trực tuyến hoàn hảo. Tại đây có một bản sao hoạt động của mã làm việc đầy đủ với video mẫu để tải xuống trên Google Tài liệu .

Khách hàng:

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

Người phục vụ:

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

Nhưng phương pháp này được giới hạn cho các tệp có kích thước <1GB.

Truyền trực tuyến các tệp video (kích thước bất kỳ) với fs.createReadStream

Bằng cách sử dụng fs.createReadStream(), máy chủ có thể đọc tệp trong một luồng thay vì đọc tất cả vào bộ nhớ cùng một lúc. Điều này nghe có vẻ như là một cách đúng đắn để thực hiện mọi việc và cú pháp cực kỳ đơn giản:

Đoạn mã máy chủ:

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

Điều này phát trực tuyến video tốt! Nhưng các điều khiển video không còn hoạt động.


1
Tôi đã để lại writeHead()mã đó bình luận, nhưng ở đó trong trường hợp nó hữu ích. Tôi có nên xóa điều đó để làm cho đoạn mã dễ đọc hơn không?
WebDeveloper404

3
req.headers.range đến từ đâu? Tôi tiếp tục nhận được không xác định khi tôi cố gắng thực hiện phương pháp thay thế. Cảm ơn.
Chad Watkins

Câu trả lời:


118

Các Accept Rangestiêu đề (các bit trongwriteHead() ) là bắt buộc đối với các điều khiển video HTML5 để làm việc.

Tôi nghĩ rằng thay vì chỉ gửi một cách mù quáng toàn bộ tệp, trước tiên bạn nên kiểm tra Accept Rangestiêu đề trong YÊU CẦU, sau đó đọc và chỉ gửi bit đó. fs.createReadStreamhỗ trợ start, vàend tùy chọn cho điều đó.

Vì vậy, tôi đã thử một ví dụ và nó hoạt động. Mã không đẹp nhưng nó rất dễ hiểu. Đầu tiên, chúng tôi xử lý tiêu đề phạm vi để có được vị trí bắt đầu / kết thúc. Sau đó, chúng tôi sử dụng fs.statđể lấy kích thước của tệp mà không cần đọc toàn bộ tệp vào bộ nhớ. Cuối cùng, sử dụng fs.createReadStreamđể gửi phần được yêu cầu cho khách hàng.

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
Chúng ta có thể sử dụng chiến lược này để chỉ gửi một số phần của bộ phim, tức là từ giây thứ 5 đến giây thứ 7 không? Có cách nào để tìm khoảng đó tương ứng với khoảng byte nào của ffmpeg như các thư viện không? Cảm ơn.
pembeci

8
Đừng bận tâm đến câu hỏi của tôi. Tôi tìm thấy những từ kỳ diệu để tìm cách đạt được những gì tôi yêu cầu: phát trực tuyến giả .
pembeci

Làm thế nào để điều này có thể hoạt động nếu vì một lý do nào đó movie.mp4 ở định dạng được mã hóa và chúng tôi cần giải mã nó trước khi phát trực tuyến đến trình duyệt?
saraf

@saraf: Nó phụ thuộc vào thuật toán được sử dụng để mã hóa. Nó hoạt động với luồng hay chỉ hoạt động dưới dạng mã hóa toàn bộ tệp? Có thể bạn chỉ giải mã video vào vị trí tạm thời và phục vụ nó như bình thường không? Nói chung, tôi có thể, nhưng nó có thể trở nên khó khăn. Không có giải pháp chung nào ở đây.
tungd

Chào tungd, cảm ơn bạn đã phản hồi! trường hợp sử dụng là một thiết bị dựa trên raspberry pi sẽ hoạt động như một nền tảng phân phối phương tiện cho các nhà phát triển nội dung giáo dục. Chúng tôi có thể tự do lựa chọn thuật toán mã hóa, khóa sẽ nằm trong phần sụn - nhưng bộ nhớ bị giới hạn ở RAM 1GB và kích thước nội dung khoảng 200GB (sẽ có trên phương tiện di động - được gắn USB.) Thậm chí một cái gì đó như thuật toán Clear Key với EME sẽ không sao - ngoại trừ vấn đề là chromium không tích hợp EME trên ARM. Chỉ rằng bản thân phương tiện di động không đủ để cho phép phát lại / sao chép.
saraf

24

Câu trả lời được chấp nhận cho câu hỏi này thật tuyệt vời và nên vẫn là câu trả lời được chấp nhận. Tuy nhiên, tôi đã gặp sự cố với mã trong đó luồng đọc không phải lúc nào cũng được kết thúc / đóng. Một phần của giải pháp là để gửi autoClose: truecùng với start:start, end:endtrong lần thứ hai createReadStreamarg.

Một phần khác của giải pháp là giới hạn mức tối đa chunksizeđược gửi trong phản hồi. Câu trả lời khác đặt endnhư vậy:

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

... có tác dụng gửi phần còn lại của tệp từ vị trí bắt đầu được yêu cầu qua byte cuối cùng của nó, bất kể số byte đó có thể là bao nhiêu. Tuy nhiên, trình duyệt máy khách có tùy chọn chỉ đọc một phần của luồng đó và sẽ đọc nếu nó chưa cần tất cả các byte. Điều này sẽ khiến quá trình đọc luồng bị chặn cho đến khi trình duyệt quyết định đã đến lúc nhận thêm dữ liệu (ví dụ: một hành động của người dùng như tìm kiếm / chà hoặc chỉ bằng cách phát luồng).

Tôi cần đóng luồng này vì tôi đang hiển thị <video> phần tử trên trang cho phép người dùng xóa tệp video. Tuy nhiên, tệp không bị xóa khỏi hệ thống tệp cho đến khi máy khách (hoặc máy chủ) đóng kết nối, vì đó là cách duy nhất để luồng kết thúc / đóng.

Giải pháp của tôi chỉ là đặt một maxChunkbiến cấu hình, đặt nó thành 1MB và không bao giờ chuyển một luồng đọc hơn 1MB cùng một lúc tới phản hồi.

// 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;
}

Điều này có tác dụng đảm bảo rằng luồng đọc được kết thúc / đóng sau mỗi yêu cầu và không bị trình duyệt giữ lại.

Tôi cũng đã viết một câu hỏicâu trả lời StackOverflow riêng về vấn đề này.


Điều này hoạt động tốt cho Chrome, nhưng dường như không hoạt động trong Safari. Trong safari, nó dường như chỉ hoạt động nếu nó có thể yêu cầu toàn bộ phạm vi. Bạn có đang làm gì khác biệt cho Safari không?
f1lt3r

2
Sau khi đào sâu hơn: Safari thấy "/ $ {total}" trong phản hồi 2 byte và sau đó nói ... "Này, thế còn bạn cứ gửi cho tôi toàn bộ tệp?". Sau đó, khi nó được thông báo, "Không, bạn chỉ nhận được 1Mb đầu tiên!", Safari bực bội "Đã xảy ra lỗi khi cố gắng dẫn tài nguyên".
f1lt3r

0

Đầu tiên tạo app.jstệp trong thư mục bạn muốn xuất bản.

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

Chỉ cần chạy node app.jsvà máy chủ của bạn sẽ chạy trên cổng 8080. Ngoài video, nó có thể phát trực tuyến tất cả các loại tệp.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.