Reproductor personalizado con Node.js y transmisión de video HTML5

1. Reproducción de vídeo HTML5

Por lo general, el video que reproducimos en la página es a través de la videoetiqueta, agregando una dirección de video que necesitamos mostrar en el atributo src, el navegador establecerá los atributos de acuerdo con él, como el ancho, la altura, si reproducir automáticamente, bucle y otros atributos, reproduzca el video a través de los controles de video predeterminados del navegador.

<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. Eventos y propiedades comunes

propiedades, métodos, eventos

Implementación común:

Atributos describir
auto-reproducción Después de especificar esta propiedad, no esperará a que se carguen los datos y se reproducirá directamente
amortiguado Es posible leer el intervalo de tiempo durante el cual se almacenan en caché los medios
control S Permite al usuario controlar la reproducción del video, como volumen, pausa, reanudar, etc.
controlslist Cuando se especifican los controles, la lista de controles puede ayudar al navegador a seleccionar los controles de visualización en el elemento multimedia (parámetros de recepción: sin descarga, sin pantalla completa ...)
tiempo actual Después de establecer el valor, el video lo establecerá en la hora de inicio de la reproducción actual
volumen Establezca el valor relativo del volumen de audio entre 0,0 y 1,0, o consulte el valor relativo del volumen actual
apagado Si silenciar, silenciar o reactivar el archivo
hora de inicio Generalmente 0, si se trata de transmisión de medios o un recurso que no comienza desde 0, no es 0
duración La longitud total del archivo multimedia de solo lectura. Algunos medios (como transmisiones en vivo desconocidas, webcasts, medios de WebRTC) no tienen este valor y devuelven NAN
en pausa si solo lectura está en pausa
terminado solo leer si la reproducción de audio/video ha terminado
anchura altura El alto/ancho del video en píxeles css
círculo Cuando se especifica, cuando el video llega al final, volverá automáticamente al lugar donde comenzó el video
póster Portada del video, no se muestra ninguna imagen durante la reproducción
precarga ninguno: no almacene en caché el video por adelantado, metadatos: obtenga datos de origen de manera razonable. automático: este video debe cargarse primero
origen URL para incrustar video

Eventos comunes:

evento Tiempo de disparo
inicio de carga empezar a cargar
duracióncambiar Se activa cuando se modifica el valor de la propiedad de duración
cambio de tasa Se activa cuando cambia la velocidad de reproducción
buscando 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 Haga clic manualmente en la barra de progreso para avanzar rápidamente (salto de video)

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. Carga de segmento de video

La información anterior sobre el reproductor de video HTML5, luego, necesitamos llevar el video del servidor al cliente.

Se recomienda la transmisión de video cuando el archivo de video es grande, admite cualquier tamaño. Al aprovechar fs.createReadStream(), el servidor puede leer el archivo en la transmisión en lugar de leer todo el archivo en la memoria a la vez. Luego envíe el video al cliente a través de una solicitud de rango. Y el cliente no tiene que esperar a que la página descargue el video completo del servidor, y puede solicitar al servidor unos segundos antes de que comience el video, y puede reproducir el video mientras lo solicita.

  • fs.statSync(): este método se usa para obtener la información estadística del archivo.Podemos obtener el tamaño del archivo cuando el fragmento cargado actualmente llega al final del archivo. fileSize = fs.statSync(filePath).size
  • fs.createReadStream(): Crea una secuencia para el archivo especificado fs.createReadStream(filePath, { inicio, fin })
  • Devuelve el tamaño de todo el fragmento: endChunk - startChunk.
  • HTTP 206: se utiliza para enviar bloques de datos al front-end sin interrupción. Se requiere la siguiente información al volver a solicitar:
    1. 'Rango de contenido': 'bytes chunkStart-chunkEnd/chunkSize'
    2. 'Aceptar-Rangos': 'bytes'
    3. 'Contenido-Longitud': tamaño de fragmento
    4. 'Tipo de contenido': 'video/webm'

Aquí, uso el marco de huevo para implementar la función de video de solicitud de rango.

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

参考: Transmisión de video con Node.js y HTML5

Supongo que te gusta

Origin juejin.im/post/7005113621415985183
Recomendado
Clasificación