Video kontrollerinin çalışmaya devam etmesi için Node.js ile bir html5 video oynatıcıya bir video dosyası mı aktarıyorsunuz?


97

Tl; Dr - Soru:

Video kontrollerinin çalışmaya devam etmesi için Node.js ile html5 video oynatıcıya bir video dosyası akışını işlemenin doğru yolu nedir ?

Bunun başlıkların işlenme şekliyle ilgisi olduğunu düşünüyorum . Her neyse, işte arka plan bilgileri. Kod biraz uzun, ancak oldukça basit.

Node ile küçük video dosyalarını HTML5 videoya aktarmak kolaydır

Küçük video dosyalarının bir HTML5 video oynatıcıya nasıl aktarılacağını çok kolay bir şekilde öğrendim. Bu kurulumla, kontroller benim tarafımdan herhangi bir işlem yapmadan çalışıyor ve video kusursuz bir şekilde yayınlanıyor. Google Dokümanlar'dan indirilmek üzere , tam olarak çalışan kodun örnek video ile çalışan bir kopyası buradadır .

Müşteri:

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

Sunucu:

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

Ancak bu yöntem boyutu 1 GB'tan küçük dosyalarla sınırlıdır.

Video dosyalarını (her boyutta) akışla fs.createReadStream

Bunu kullanarak fs.createReadStream(), sunucu dosyayı bir seferde belleğe okumak yerine bir akışta okuyabilir. Bu, işleri yapmanın doğru yolu gibi görünüyor ve sözdizimi son derece basit:

Sunucu Snippet'i:

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

Bu, videoyu gayet iyi yayınlar! Ancak video kontrolleri artık çalışmıyor.


1
Bu writeHead()kodu yorumda bıraktım , ancak yardımcı olur diye düşündüm . Kod parçacığını daha okunaklı hale getirmek için bunu kaldırmalı mıyım?
WebDeveloper404

3
req.headers.range nereden geliyor? Değiştirme yöntemini yapmaya çalıştığımda tanımsız kalmaya devam ediyorum. Teşekkürler.
Chad Watkins

Yanıtlar:


118

Accept RangesBaşlığı (bit writeHead()) çalışması için HTML5 video kontrolleri için gereklidir.

Bence tam dosyayı körü körüne göndermek yerine, önce REQUEST'teki Accept Rangesbaşlığı kontrol etmeli , sonra okuyup sadece o kısmı göndermelisin. fs.createReadStreamdestek startve endbunun için seçenek.

Bu yüzden bir örnek denedim ve işe yarıyor. Kod güzel değil ama anlaşılması kolay. İlk olarak, başlangıç ​​/ bitiş konumunu elde etmek için aralık başlığını işleriz. Daha sonra fs.statdosyanın tamamını belleğe okumadan dosyanın boyutunu elde etmek için kullanırız . Son olarak, fs.createReadStreamistenen parçayı müşteriye göndermek için kullanın .

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
Bu stratejiyi filmin sadece bir kısmını, yani 5. saniye ile 7. saniye arasında göndermek için kullanabilir miyiz? Kitaplıklar gibi ffmpeg tarafından bu aralığın hangi bayt aralığına karşılık geldiğini bulmanın bir yolu var mı? Teşekkürler.
pembeci

8
Soruma aldırma. İstediğimi nasıl başaracağımı bulmak için sihirli kelimeleri buldum: sözde akış .
pembeci

Herhangi bir nedenle movie.mp4 şifrelenmiş bir formattaysa ve tarayıcıya akış yapmadan önce şifresini çözmemiz gerekiyorsa, bu nasıl çalıştırılabilir?
saraf

@saraf: Şifreleme için hangi algoritmanın kullanıldığına bağlıdır. Akışla mı çalışıyor yoksa yalnızca tam dosya şifrelemesi olarak mı çalışıyor? Videonun şifresini geçici olarak çözüp her zamanki gibi sunmanız mümkün mü? Genel olarak konuşuyorum, mümkün, ancak yanıltıcı olabilir. Burada genel bir çözüm yok.
58'de tungd

Merhaba tungd, cevap verdiğin için teşekkürler! kullanım durumu, eğitim içeriği geliştiricileri için bir medya dağıtım platformu görevi görecek ahududu pi tabanlı bir cihazdır. Şifreleme algoritmasını seçmekte özgürüz, anahtar bellenimde olacaktır - ancak bellek 1GB RAM ile sınırlıdır ve içerik boyutu 200GB civarındadır (çıkarılabilir medyada olacaktır - USB takılıdır.) EME, Chromium'un ARM'de yerleşik EME'ye sahip olmaması sorunu dışında iyi olurdu. Sadece çıkarılabilir medyanın tek başına oynatma / kopyalamayı etkinleştirmek için yeterli olmaması gerekir.
saraf

25

Bu sorunun kabul edilen cevabı harika ve kabul edilen cevap olarak kalmalıdır. Ancak, okuma akışının her zaman bitmediği / kapatılmadığı kodla ilgili bir sorunla karşılaştım. Çözümün bir kısmı , ikinci argümanda autoClose: truebirlikte göndermekti .start:start, end:endcreateReadStream

Çözümün diğer kısmı chunksize, yanıtta gönderilen maksimum miktarı sınırlamaktı . Diğer cevap şöyle belirlendi end:

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

... dosyanın geri kalanını istenen başlangıç ​​konumundan son baytına kadar gönderme etkisine sahip, kaç bayt olursa olsun. Ancak, istemci tarayıcısı bu akışın yalnızca bir bölümünü okuma seçeneğine sahiptir ve henüz tüm baytlara ihtiyaç duymuyorsa, okuyacaktır. Bu, tarayıcı daha fazla veri alma zamanının geldiğine karar verene kadar okunan akışın engellenmesine neden olur (örneğin, arama / silme gibi bir kullanıcı eylemi veya yalnızca akışı oynatarak).

<video>Öğeyi kullanıcının video dosyasını silmesine izin veren bir sayfada görüntülediğim için bu akışın kapatılmasına ihtiyacım vardı . Ancak dosya, istemci (veya sunucu) bağlantıyı kapatana kadar dosya sisteminden kaldırılmıyordu, çünkü bu, akışın sonlandırılmasının / kapatılmasının tek yoluydu.

Benim çözümüm sadece bir maxChunkkonfigürasyon değişkeni ayarlamak, 1MB'ye ayarlamak ve yanıta her seferinde 1MB'den fazla bir okuma akışını asla iletmekti.

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

Bu, okuma akışının her istekten sonra sonlandırıldığından / kapatıldığından ve tarayıcı tarafından canlı tutulmadığından emin olma etkisine sahiptir.

Ayrıca bu sorunu kapsayan ayrı bir StackOverflow sorusu ve cevabı yazdım .


Bu, Chrome için harika çalışıyor, ancak Safari'de çalışmıyor gibi görünüyor. Safari'de sadece tüm menzili talep edebiliyorsa işe yarıyor gibi görünüyor. Safari için farklı bir şey yapıyor musunuz?
f1lt3r

2
Daha fazla araştırma yaptıktan sonra: Safari, 2 baytlık yanıtta "/ $ {toplam}" ifadesini görür ve sonra "Hey, bana tüm dosyayı göndermeye ne dersin?" Der. Daha sonra, "Hayır, sadece ilk 1Mb'yi alıyorsunuz!" Dendiğinde, Safari üzülür "Kaynağı yönlendirmeye çalışırken bir hata oluştu".
f1lt3r

0

Öncelikle app.jsyayınlamak istediğiniz dizinde dosya oluşturun .

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

Basitçe çalıştırın node app.jsve sunucunuz 8080 portunda çalışacaktır . Videonun yanı sıra her türlü dosyayı yayınlayabilir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.