Node.js 및 HTML5 비디오 스트리밍이 있는 사용자 지정 플레이어

1. HTML5 동영상 재생

일반적으로 페이지에서 재생하는 비디오는 video태그를 통해 src 속성에 표시해야 하는 비디오 주소를 추가하고 브라우저는 너비, 높이, 자동 재생 여부, 루프 및 다른 속성, 브라우저의 기본 비디오 컨트롤을 통해 비디오를 재생합니다.

<video controls width="250">
    <source src="/media/cc0-videos/flower.webm" type="video/webm">
    <source src="/media/cc0-videos/flower.mp4" type="video/mp4">
    Sorry, your browser doesn't support embedded videos.
</video>
复制代码

2. 공통 이벤트 및 속성

속성, 메서드, 이벤트

일반적인 구현:

속성 설명하다
자동 재생 이 속성을 지정하면 데이터가 로드될 때까지 기다리지 않고 직접 재생됩니다.
버퍼링된 미디어가 캐시된 시간 범위를 읽을 수 있습니다.
통제 수단 사용자가 볼륨, 일시 중지, 다시 시작 등과 같은 비디오 재생을 제어할 수 있습니다.
제어 목록 컨트롤이 지정되면 controlslist는 브라우저가 미디어 요소의 디스플레이 컨트롤을 선택하는 데 도움이 될 수 있습니다(수신 매개변수: nodownload, nofullscreen ....).
현재 시간 값을 설정한 후 비디오는 현재 재생의 시작 시간으로 설정합니다.
용량 오디오 볼륨의 상대 값을 0.0에서 1.0 사이로 설정하거나 현재 볼륨의 상대 값을 쿼리합니다.
음소거 파일 음소거, 음소거 또는 음소거 해제 여부
시작 시간 일반적으로 0, 스트리밍 미디어나 0부터 시작하지 않는 리소스라면 0이 아니다.
지속 읽기 전용 미디어 파일의 전체 길이입니다. 일부 미디어(예: 알 수 없는 라이브 스트림, 웹캐스트, WebRTC의 미디어)에는 이 값이 없고 NAN을 반환합니다.
일시 중지 읽기 전용이 일시 중지되었는지 여부
끝났다 오디오/비디오 재생이 종료되었는지 여부만 읽기
높이 너비 비디오의 높이/너비(css 픽셀)
고리 지정하면 비디오가 끝까지 도달하면 비디오가 시작된 위치로 자동으로 돌아갑니다.
포스터 비디오 커버, 재생 중 이미지가 표시되지 않음
예압 없음: 비디오를 미리 캐시하지 않음, 메타데이터: 합리적으로 소스 데이터를 가져옵니다. 자동: 이 비디오를 먼저 로드해야 합니다.
src 동영상을 삽입할 URL

일반적인 이벤트:

이벤트 트리거 타이밍
로드스타트 로딩 시작
지속 시간 변경 duration 속성 값이 수정될 때 트리거됩니다.
환율변동 재생 속도가 변경될 때 발생
추구 seeking 寻找中 点击一个为(缓存)下载的区域
seeked seeked 寻找完成时触发
play 开始播放时触发
waiting 播放由于下一帧数据未获取到导致播放停止,但是播放器没有主动预期其停止,仍然在努力的获取数据,简单的说就是在等待下一帧视频数据,暂时还无法播放。
playing 我们能看到视频时触发,也就是真正处于播放状态
canplay 浏览器可以播放媒体文件,但是没有足够的数据支撑到播放结束,需要不停缓存更多内容
pause 暂停播放时触发
ended 视频停止,media已经播放到终点时触发, loop 的情况下不会触发
volumechange 音量改变时触发
loadedmetadata 获取视频meta信息完毕,这个时候播放器已经获取到了视频时长和视频资源的文件大小。
loadeddata media中的首帧已经加载时触发, 视频播放器第一次完成了当前播放位置的视频渲染。
abort 客户端主动终止下载(不是因为错误引起)
error video.error.code: 1.用户终止 2.网络错误 3.解码错误 4.URL无效
canplaythrough 浏览器可以播放文件,不需要停止缓存更多内容
progress 客户端请求数据
timeupdate 当video.currentTime发生改变时触发该事件
stalled 网速失速
suspend 延迟下载

方法:

方法 描述
play() 播放视频
pause() 暂停视频
canPlayType() 测试video元素是否支持给定MIME类型的文件
requestFullscreen() / mozRequestFullScreen() / webkitRequestFullScreen() 全屏

3. 自定义视频播放器

首先需要去掉video身上的属性controls属性,将所有播放的动作交由我们自己控制。

3.1 自定义播放或暂停

const playBtn = document.getElementById('playBtnId');
playBtn.addEventListener('click', function() {
  if (video.paused) {
    video.play();
    playBtn.textContent = '||'; // 切换样式
  } else {
    video.pause();
    playBtn.textContent = '>'; // 切换样式
  }
});
复制代码

3.2 音量控制

// 音量增加
const volIncBtn = document.getElementById('volIncId');
volIncBtn.addEventListener('click', function() {
  video.volume > 0.9 ? (video.volume = 1) : (video.volume += 0.1);
});

// 音量减小
const volDecBtn = document.getElementById('volDecId');
 volDecBtn.addEventListener('click', function() {
    video.volume < 0.1 ? (video.volume = 0) : (video.volume -= 0.1);
  });
复制代码

3.3 静音

const mutedBtn = document.getElementById('mutedId');
 mutedBtn.addEventListener('click', function() {
    video.muted = !video.muted;
    mutedBtn.textContent = video.muted ? '恢复' : '静音';
  });
复制代码

3.4 播放快进/快退

  • 快进
const speedUpBtn = document.getElementById(speedUpId);
let _speed = 1;
speedUpBtn.addEventListener('click', function() {
  _speed = _speed * 2;
  if (_speed > 4) {
    _speed = 1;
  }

  video.playbackRate = _speed;
  speedUpBtn.textContent = _speed === 1 ? '快进' : '快进x' + _speed;
});
复制代码
  • 快退
  const backBtn = document.getElementById(backBtnId);
  let back_speed = 1;
  let _t;
  backBtn.addEventListener('click', function() {
    back_speed = back_speed * 2;
    if (back_speed > 4) {
      video.playbackRate = 1;
      back_speed = 1;
      clearInterval(_t);
    } else {
      video.playbackRate = 0;
      clearInterval(_t);
      _t = setInterval(function() {
        video.currentTime -= back_speed * 0.1;
      }, 100);
    }
    backBtn.textContent = back_speed === 1 ? '快退' : '快退x' + back_speed;
  });
复制代码

3.5 全屏

const fullScreenBtn = document.getElementById(fullScreenId);
const fullScreen = function() {
  fullScreenBtn.addEventListener('click', function() {
    if (video.requestFullscreen) {
      video.requestFullscreen();
    } else if (video.mozRequestFullScreen) {
      video.mozRequestFullScreen();
    } else if (video.webkitRequestFullScreen) {
      video.webkitRequestFullScreen();
    }
  });
};
复制代码

3.6 进度条和时间显示

 const getTime = function() {
   // 当前播放时间
    nowTime.textContent = 0;
    // 总时长
    duration.textContent = 0;

    video.addEventListener('timeupdate', function() {
       // 当前播放时间, parseTime: 格式化时间
      nowTime.textContent = parseTime(video.currentTime); 

      // 计算进度条
      const percent = video.currentTime / video.duration;
      playProgress.style.width = percent * progressWrap.offsetWidth + 'px';
    });


    video.addEventListener('loadedmetadata', function() {
      // 更新视频总时长
      duration.textContent = parseTime(video.duration);
    });
  };
复制代码

3.7 진행률 표시줄을 수동으로 클릭하여 빨리 감기(비디오 점프)

progressWrap.addEventListener('click', function(e) {
  if (video.paused || video.ended) {
    video.play();
  }
  const length = e.pageX - progressWrap.offsetLeft;
  const percent = length / progressWrap.offsetWidth;
  playProgress.style.width = percent * progressWrap.offsetWidth + 'px';
  video.currentTime = percent * video.duration;
});
复制代码

4. 비디오 세그먼트 로딩

HTML5 비디오 플레이어에 대한 위의 정보를 확인한 다음 서버에서 클라이언트로 비디오를 가져와야 합니다.

스트리밍 비디오는 비디오 파일이 클 때 권장되며 모든 크기를 지원합니다. 를 활용 fs.createReadStream()하여 서버는 전체 파일을 한 번에 메모리로 읽는 대신 스트림에서 파일을 읽을 수 있습니다. 그런 다음 범위 요청을 통해 클라이언트에 비디오를 보냅니다. 그리고 클라이언트는 페이지가 서버에서 전체 비디오를 다운로드할 때까지 기다릴 필요가 없으며 비디오가 시작되기 몇 초 전에 서버를 요청할 수 있으며 요청하는 동안 비디오를 재생할 수 있습니다.

  • fs.statSync(): 파일의 통계 정보를 얻기 위한 메소드로, 현재 로드된 청크가 파일의 끝에 도달하면 파일 크기를 알 수 있다. fileSize = fs.statSync(filePath).size
  • fs.createReadStream(): 지정된 파일에 대한 스트림 생성 fs.createReadStream(filePath, { start, end })
  • 전체 청크의 크기를 반환합니다. endChunk - startChunk.
  • HTTP 206: 중단 없이 프런트 엔드에 데이터 블록을 제공하는 데 사용됩니다. 재요청 시 다음 정보가 필요합니다.
    1. '콘텐츠 범위': '바이트 chunkStart-chunkEnd/chunkSize'
    2. '허용 범위': '바이트'
    3. '콘텐츠 길이': chunkSize
    4. '콘텐츠 유형': '동영상/webm'

여기서는 에그 프레임워크를 사용하여 범위 요청 비디오 기능을 구현합니다.

async getVideo() {
    const { ctx } = this;
    const req = ctx.request;
    try {
      const homedir = `${process.env.HOME || process.env.USERPROFILE}/`;
      const filePath = path.resolve(`${process.env.NODE_ENV === 'development' ? '' : homedir}${req.query.filePath}`);
      const range = req.headers.range;
      const fileSize = fs.statSync(filePath).size;

      if (range) {
        const positions = range.replace(/bytes=/, '').split('-');
        const start = parseInt(positions[0], 10);

        const end = positions[1] ? parseInt(positions[1], 10) : fileSize - 1;
        const chunksize = end - start + 1;

        if (start >= fileSize) {
          ctx.status = 416;
          ctx.body =
            'Requested range not satisfiable\n' + start + ' >= ' + fileSize;
          return;
        }

        ctx.status = 206;
        const header = {
          'Accept-Ranges': 'bytes',
          'Content-Type': 'video/webm',
          'Content-Length': chunksize,
          'Content-Range': `bytes ${start}-${end}/${fileSize}`,
          'cache-control': 'public,max-age=31536000',
        };
        ctx.set(header);

        ctx.body = fs
          .createReadStream(filePath, {
            start,
            end,
            autoClose: true,
          })
          .on('err', err => {
            console.log(`[Video Play]: ${req.url}, 'pip stream error`);
            ctx.body = err;
            ctx.status = 500;
          });
      } else {
        this.ctx.set('Content-Length', fileSize);
        this.ctx.set('Content-Type', 'video/webm');
        this.ctx.status = 200;
        this.ctx.body = fs.createReadStream(filePath);
      }
    } catch (err) {
      console.log(err);
      ctx.body = err;
      ctx.status = 500;
    }
  }
复制代码

参考: Node.js 및 HTML5를 사용한 비디오 스트림

추천

출처juejin.im/post/7005113621415985183