การสตรีมไฟล์วิดีโอไปยังเครื่องเล่นวิดีโอ 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);

แต่วิธีนี้ จำกัด เฉพาะไฟล์ที่มีขนาด <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

3
req.headers.range มาจากไหน? ฉันไม่ได้กำหนดไว้เสมอเมื่อพยายามใช้วิธีการแทนที่ ขอบคุณ.
Chad Watkins

คำตอบ:


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

@saraf: ขึ้นอยู่กับอัลกอริทึมที่ใช้ในการเข้ารหัส ใช้งานได้กับสตรีมหรือใช้การเข้ารหัสไฟล์ทั้งหมดเท่านั้น? เป็นไปได้ไหมว่าคุณเพียงแค่ถอดรหัสวิดีโอไปยังตำแหน่งชั่วคราวและให้บริการตามปกติ การพูดทั่วไปฉันเป็นไปได้ แต่อาจเป็นเรื่องยุ่งยาก ไม่มีวิธีแก้ปัญหาทั่วไปที่นี่
tungd

สวัสดี tungd ขอบคุณสำหรับการตอบกลับ! กรณีการใช้งานเป็นอุปกรณ์ที่ใช้ราสเบอร์รี่ pi ซึ่งจะทำหน้าที่เป็นแพลตฟอร์มการกระจายสื่อสำหรับนักพัฒนาเนื้อหาเพื่อการศึกษา เรามีอิสระที่จะเลือกอัลกอริธึมการเข้ารหัสคีย์จะอยู่ในเฟิร์มแวร์ - แต่หน่วยความจำ จำกัด ไว้ที่ 1GB RAM และขนาดเนื้อหาอยู่ที่ประมาณ 200GB (ซึ่งจะอยู่บนสื่อแบบถอดได้ - ต่อ USB) แม้แต่บางอย่างเช่นอัลกอริทึม Clear Key ด้วย EME จะโอเค - ยกเว้นปัญหาที่โครเมียมไม่มี EME ใน ARM เพียงแค่ว่าสื่อแบบถอดได้ด้วยตัวเองไม่ควรเพียงพอที่จะเปิดใช้งานการเล่น / คัดลอก
saraf

25

คำตอบที่ยอมรับสำหรับคำถามนี้นั้นยอดเยี่ยมและควรเป็นคำตอบที่ยอมรับได้ อย่างไรก็ตามฉันพบปัญหาเกี่ยวกับรหัสที่สตรีมการอ่านไม่ได้สิ้นสุด / ปิดเสมอไป ส่วนหนึ่งของการแก้ปัญหาคือการส่งautoClose: trueไปพร้อมกับอาร์กิวเมนต์start:start, end:endที่สองcreateReadStream

ส่วนอื่น ๆ ของการแก้ปัญหาคือการ จำกัด จำนวนสูงสุด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

2
เมื่อขุดเพิ่มเติม: Safari เห็น "/ $ {total}" ในการตอบสนอง 2 ไบต์จากนั้นจึงพูดว่า ... "เฮ้คุณส่งไฟล์ทั้งหมดให้ฉันได้อย่างไร" จากนั้นเมื่อมีการแจ้งว่า "ไม่คุณได้รับ 1Mb แรกเท่านั้น!" 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 นอกจากวิดีโอแล้วยังสามารถสตรีมไฟล์ได้ทุกประเภท

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.