Streaming file video ke pemutar video html5 dengan Node.js sehingga kontrol video terus berfungsi?

97

Tl; Dr - Pertanyaan:

Apa cara yang tepat untuk menangani streaming file video ke pemutar video html5 dengan Node.js sehingga kontrol video terus berfungsi?

Saya pikir itu ada hubungannya dengan cara penanganan header. Berikut informasi latar belakangnya. Kode ini agak panjang, namun cukup mudah.

Streaming file video kecil ke video HTML5 dengan Node itu mudah

Saya belajar cara melakukan streaming file video kecil ke pemutar video HTML5 dengan sangat mudah. Dengan pengaturan ini, kontrol bekerja tanpa ada pekerjaan di pihak saya, dan video mengalir dengan sempurna. Salinan yang berfungsi dari kode yang berfungsi sepenuhnya dengan video contoh ada di sini, untuk diunduh di Google Documents .

Klien:

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

Server:

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

Tetapi metode ini terbatas pada file berukuran <1GB.

Streaming file video (ukuran apa saja) dengan fs.createReadStream

Dengan memanfaatkan fs.createReadStream(), server dapat membaca file dalam aliran daripada membaca semuanya ke dalam memori sekaligus. Ini terdengar seperti cara yang tepat untuk melakukan sesuatu, dan sintaksnya sangat sederhana:

Cuplikan Server:

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

Ini mengalirkan video dengan baik! Tetapi kontrol video tidak lagi berfungsi.

WebDeveloper 404
sumber
1
Saya meninggalkan writeHead()kode itu berkomentar, tetapi di sana jika itu membantu. Haruskah saya menghapusnya agar cuplikan kode lebih mudah dibaca?
WebDeveloper404
3
dari mana req.headers.range berasal? Saya terus tidak terdefinisi ketika saya mencoba melakukan metode penggantian. Terima kasih.
Chad Watkins

Jawaban:

118

The Accept Rangesheader (bit di writeHead()) diperlukan untuk kontrol HTML5 video ke pekerjaan.

Saya pikir daripada hanya mengirim file lengkap secara membabi buta, Anda harus terlebih dahulu memeriksa Accept Rangesheader di REQUEST, kemudian membaca dan mengirim sedikit itu. fs.createReadStreamdukungan start, dan endopsi untuk itu.

Jadi saya mencoba contoh dan berhasil. Kodenya tidak bagus tapi mudah dimengerti. Pertama kami memproses header rentang untuk mendapatkan posisi awal / akhir. Kemudian kami gunakan fs.statuntuk mendapatkan ukuran file tanpa membaca seluruh file ke dalam memori. Terakhir, gunakan fs.createReadStreamuntuk mengirim bagian yang diminta ke klien.

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);
tungd
sumber
3
Bisakah kita menggunakan strategi ini untuk mengirim hanya sebagian dari film, yaitu antara detik ke-5 dan ke-7? Apakah ada cara untuk menemukan apa interval itu sesuai dengan berapa byte interval oleh ffmpeg seperti perpustakaan? Terima kasih.
pembeci
8
Sudahlah pertanyaan saya. Saya menemukan kata-kata ajaib untuk menemukan cara mencapai apa yang saya minta: streaming semu .
pembeci
Bagaimana ini dapat dilakukan jika karena alasan tertentu movie.mp4 dalam format terenkripsi dan kita perlu mendekripsi sebelum streaming ke browser?
saraf
@saraf: Itu tergantung pada algoritma apa yang digunakan untuk enkripsi. Apakah ini berfungsi dengan aliran atau hanya berfungsi sebagai enkripsi file secara keseluruhan? Apakah mungkin Anda baru saja mendekripsi video ke lokasi sementara dan menyajikannya seperti biasa? Secara umum saya mungkin, tetapi itu bisa rumit. Tidak ada solusi umum di sini.
tungd
Hai, tungd, terima kasih telah menanggapi! use case adalah perangkat berbasis raspberry pi yang akan bertindak sebagai platform distribusi media untuk pengembang konten pendidikan. Kami bebas memilih algoritma enkripsi, kuncinya ada di firmware - tetapi memori terbatas pada 1GB RAM dan ukuran konten sekitar 200GB (yang akan di media yang dapat dilepas - terpasang USB.) Bahkan sesuatu seperti algoritma Clear Key dengan EME akan baik-baik saja - kecuali untuk masalah dimana chromium tidak memiliki EME yang terpasang di ARM. Hanya saja media yang dapat dilepas dengan sendirinya tidak cukup untuk mengaktifkan pemutaran / penyalinan.
saraf
25

Jawaban yang diterima untuk pertanyaan ini luar biasa dan harus tetap menjadi jawaban yang diterima. Namun saya mengalami masalah dengan kode di mana aliran baca tidak selalu diakhiri / ditutup. Bagian dari solusi adalah untuk mengirim autoClose: truebersama dengan start:start, end:enddi kedua createReadStreamarg.

Bagian lain dari solusi itu adalah membatasi maksimal chunksizeyang dikirim dalam respons. Jawaban lainnya diatur endseperti ini:

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

... yang memiliki efek mengirim sisa file dari posisi awal yang diminta hingga byte terakhirnya, tidak peduli berapa banyak byte yang mungkin. Namun browser klien memiliki opsi untuk hanya membaca sebagian dari aliran itu, dan akan, jika belum membutuhkan semua byte. Ini akan menyebabkan pembacaan aliran diblokir hingga browser memutuskan waktunya untuk mendapatkan lebih banyak data (misalnya tindakan pengguna seperti seek / scrub, atau hanya dengan memutar aliran).

Saya membutuhkan aliran ini ditutup karena saya menampilkan <video>elemen pada halaman yang memungkinkan pengguna untuk menghapus file video. Namun file tersebut tidak dihapus dari sistem file sampai klien (atau server) menutup koneksi, karena itulah satu-satunya cara streaming diakhiri / ditutup.

Solusi saya hanya mengatur maxChunkvariabel konfigurasi, mengaturnya ke 1MB, dan tidak pernah menyalurkan aliran baca lebih dari 1MB pada satu waktu ke respons.

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

Ini memiliki efek memastikan bahwa aliran baca diakhiri / ditutup setelah setiap permintaan, dan tidak tetap hidup oleh browser.

Saya juga menulis pertanyaan dan jawaban StackOverflow terpisah yang mencakup masalah ini.

danludwig
sumber
Ini berfungsi dengan baik untuk Chrome, tetapi tampaknya tidak berfungsi di Safari. Di safari tampaknya hanya berfungsi jika dapat meminta seluruh jajaran. Apakah Anda melakukan sesuatu yang berbeda untuk Safari?
f1lt3r
2
Setelah menggali lebih lanjut: Safari melihat "/ $ {total}" dalam respons 2-byte, dan kemudian berkata ... "Hai, bagaimana kalau Anda kirimkan saja seluruh file kepada saya?". Kemudian saat diberi tahu, "Tidak, Anda hanya mendapatkan 1 MB pertama!", Safari menjadi marah "Terjadi kesalahan saat mencoba mengarahkan sumber daya".
f1lt3r
0

Pertama buat app.jsfile di direktori yang ingin Anda terbitkan.

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

Cukup jalankan node app.jsdan server Anda akan berjalan pada port 8080. Selain video, ini dapat mengalirkan semua jenis file.

Ibrahim Shah
sumber