Custom Player with Node.js and HTML5 Video Streaming

1. HTML5 video playback

Usually, the video we play on the page is through the videotag, adding a video address we need to display in the src attribute, the browser will set the attributes according to it, such as width, height, whether to automatically play, loop and other attributes, Play the video through the browser's default video controls.

<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. Common Events and Properties

properties, methods, events

Common implementation:

Attributes describe
autoplay After specifying this property, it will not wait for the data to be loaded, and will play directly
buffered It is possible to read the time range for which media is cached
controls Allows the user to control the playback of the video, such as volume, pause, resume, etc.
controls list When controls are specified, controlslist can help the browser to select the display controls on the media element (receive parameters: nodownload, nofullscreen .... )
currentTime After setting the value, the video will set it to the start time of the current playback
volume Set the relative value of the audio volume between 0.0 and 1.0, or query the relative value of the current volume
muted Whether to mute, mute or unmute the file
startTime Generally 0, if it is streaming media or a resource that does not start from 0, it is not 0
duration The total length of the only-read media file. Some media (such as unknown live streams, webcasts, media from WebRTC) do not have this value and return NAN
paused whether only-read is paused
ended only-read whether the audio/video playback has ended
height / width The height/width of the video in css pixels
loop When specified, when the video reaches the end, it will automatically return to the place where the video started
poster Video cover, no image displayed while playing
preload none: do not cache video in advance, metadata: fetch source data reasonably. auto: This video needs to be loaded first
src URL to embed video

Common events:

event Trigger timing
loadstart start loading
durationchange Triggered when the duration property value is modified
ratechange Fired when the playback rate changes
seeking 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 Manually click the progress bar to fast forward (video jump)

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. Video segment loading

The above information about the HTML5 video player, then next, we need to get the video from the server to the client.

Streaming video is recommended when the video file is large, it supports any size. By exploiting fs.createReadStream(), the server can read the file in the stream instead of reading the entire file into memory at once. Then send the video to the client through a range request. And the client does not have to wait for the page to download the entire video from the server, and can request the server a few seconds before the video starts, and it can play the video while requesting.

  • fs.statSync(): This method is used to get the statistical information of the file. We can get the file size when the currently loaded chunk reaches the end of the file. fileSize = fs.statSync(filePath).size
  • fs.createReadStream(): Create a stream for the specified file fs.createReadStream(filePath, { start, end })
  • Returns the size of the entire chunk: endChunk - startChunk.
  • HTTP 206: Used to serve blocks of data to the front end without interruption. The following information is required when re-requesting:
    1. 'Content-Range': 'bytes chunkStart-chunkEnd/chunkSize'
    2. 'Accept-Ranges': 'bytes'
    3. 'Content-Length': chunkSize
    4. 'Content-Type': 'video/webm'

Here, I use the egg framework to implement the function of range request video.

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;
    }
  }
复制代码

参考:Video Stream With Node.js and HTML5

Guess you like

Origin juejin.im/post/7005113621415985183