동영상 컨트롤이 계속 작동하도록 Node.js를 사용하여 동영상 파일을 html5 동영상 플레이어로 스트리밍 하시겠습니까?


97

Tl; Dr-질문 :

비디오 컨트롤이 계속 작동하도록 Node.js를 사용하여 비디오 파일을 html5 비디오 플레이어로 스트리밍하는 올바른 방법은 무엇입니까 ?

나는 생각 이 헤더를 처리하는 방식과 관련이있다. 어쨌든, 여기에 배경 정보가 있습니다. 코드는 약간 길지만 매우 간단합니다.

Node를 사용하여 작은 비디오 파일을 HTML5 비디오로 스트리밍하는 것은 쉽습니다.

작은 비디오 파일을 HTML5 비디오 플레이어로 매우 쉽게 스트리밍하는 방법을 배웠습니다. 이 설정을 사용하면 컨트롤이 내 작업없이 작동하며 비디오는 완벽하게 스트리밍됩니다. Google 문서에서 다운로드 할 수 있는 샘플 비디오와 함께 완전히 작동하는 코드의 작업 사본이 여기에 있습니다 .

고객:

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

3
req.headers.range는 어디에서 왔습니까? replace 메서드를 시도 할 때 계속 정의되지 않습니다. 감사.
Chad Watkins

답변:


118

Accept Ranges헤더 (의 비트 writeHead()) 작업에 HTML5 비디오 컨트롤이 필요합니다.

맹목적으로 전체 파일을 보내는 대신 먼저 Accept RangesREQUEST 에서 헤더를 확인한 다음 해당 비트를 읽고 보내야한다고 생각합니다. fs.createReadStream지원 startend 옵션.

그래서 나는 예제를 시도했고 작동합니다. 코드는 예쁘지는 않지만 이해하기 쉽습니다. 먼저 시작 / 종료 위치를 얻기 위해 범위 헤더를 처리합니다. 그런 다음 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
내 질문은 신경 쓰지 마. 나는 내가 요청한 것을 달성하는 방법을 찾기 위해 마법의 단어를 찾았다 : pseudo-streaming .
pembeci

어떤 이유로 movie.mp4가 암호화 된 형식이고 브라우저로 스트리밍하기 전에 암호를 해독해야하는 경우 어떻게 작동 할 수 있습니까?
saraf

@saraf : 암호화에 사용되는 알고리즘에 따라 다릅니다. 스트림과 함께 작동합니까 아니면 전체 파일 암호화로만 작동합니까? 비디오를 임시 위치로 해독하고 평소와 같이 제공 할 수 있습니까? 일반적으로 나는 가능하지만 까다로울 수 있습니다. 여기에는 일반적인 해결책이 없습니다.
tungd

안녕하세요, tungd, 응답 해 주셔서 감사합니다! 사용 사례는 교육용 콘텐츠 개발자를위한 미디어 배포 플랫폼 역할을 할 라즈베리 파이 기반 장치입니다. 우리는 암호화 알고리즘을 자유롭게 선택할 수 있습니다. 키는 펌웨어에 있습니다. 그러나 메모리는 1GB RAM으로 제한되고 콘텐츠 크기는 약 200GB입니다 (이동식 미디어에 있음-USB 연결됨). EME는 괜찮을 것입니다-크롬이 ARM에 내장 된 EME가 없다는 문제를 제외하고는. 이동식 미디어만으로는 재생 / 복사를 할 수 없습니다.
saraf 2015 년

25

이 질문에 대한 대답 은 훌륭하며 받아 들여진 대답으로 남아 있어야합니다. 그러나 읽기 스트림이 항상 종료 / 닫히지 않는 코드에 문제가 발생했습니다. 해결책의 일부는 두 번째 인수 autoClose: true와 함께 보내는 것이 었습니다 .start:start, end:endcreateReadStream

해결책의 다른 부분은 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 2017-06-12

2
자세히 살펴보면 Safari는 2 바이트 응답에서 "/ $ {total}"을보고 다음과 같이 말합니다. "이봐, 파일 전체를 보내면 어때?". 그런 다음 "아니요, 첫 번째 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.