vue3 中播放 m3u8 格式视频

6,113 阅读9分钟

视频文件格式

视频文件格式是指视频文件的编码和压缩方式,

1.容器和编解码器是任何视频文件的两个组成部分。

  • 视频格式是存储音频,视频,字幕和任何其他源数据的容器。
  • 编解码器是对音频和视频之类的多媒体数据进行编码和解码。

2.创建视频时,视频编解码器对视频进行编码和压缩,而音频编解码器对声音也是如此。 之后,编码的视频和音频将同步并存储在媒体容器(文件格式)中。


常见在线流媒体格式:mp4、flv、f4v、webm

移动设备格式:m4v、mov、3gp、3g2

RealPlayer :rm、rmvb

微软格式 :wmv、avi、asf

MPEG 视频 :mpg、mpeg、mpe、ts

DV格式 :div、dv、divx

其他格式 :vob、dat、mkv、lavf、cpk、dirac、ram、qt、fli、flc、mod

选择视频格式之前,请考虑以下情况:

1.对于在线视频,请选择大多数网络浏览器支持的文件格式。 这样,您的视频将在浏览器上本地播放。 MP4和WEBM是与浏览器兼容的视频格式。

2.对于家庭视频录像,请选择高质量的视频格式,以备将来使用。 开源文件格式比企业控制的专有格式更具面向未来性。 MP4或AVI格式非常适合该类别。

3.对于Windows应用程序,请选择与Windows兼容的格式。 在这种情况下,WMV是一个不错的选择。

m3u8

m3u8是一种视频流媒体播放列表文件格式,常用于将视频内容分割成多个小文件并进行流式传输。

现在的视频网站采用的是流媒体传输协议,将一段视频切成无数个小段,这几个小段就是ts格式的视频文件,一段一段的网站上播放。

  1. 服务器采集编码传输视频到切片器
  1. 切片器对视频创建索引文件,并切割成 n 个 ts 文件
  1. 这两个文件传输到 http 服务器上
  1. 网站/客户端根据索引文件查找 http 服务器上的 ts 文件,连续播放这 n 个 ts 文件,就可以了。

索引文件里面存储着ts文件的网络url链接,网站需要拿到索引文件,去按照url链接下载在http服务器中的ts文件。拿到了ts文件之后,本身这些ts文件就是原视频中的一小段视频,所有ts文件下载、顺序播放,就完成了整个视频的播放。而索引文件就是 m3u8 文件。

m3u8是文件记录了一个索引纯文本文件,打开它时播放软件并不是播放它,而是根据它的索引找到对应的音视频文件的网络地址进行在线播放。这种格式通常用于在网络上实现实时视频直播和点播服务,可以通过多种设备和平台进行播放。

Hls

HLS是苹果公司开发的一种流媒体传输协议,全称为HTTP Live Streaming,用于实时流传输的协议。

它通过将整个视频分割成多个小的ts文件,每个文件都有自己的URL地址,然后通过HTTP协议进行传输,实现了在网络上的流式传输。

HLS协议支持多种码率和分辨率的视频,可以自适应地根据用户的网络环境和设备性能进行调整,提供更好的观看体验。

hls.js是一个JavaScript库,可实现HTTP Live Streaming客戶端。 它依靠HTML5视频和MediaSource扩展进行播放。

它通过将MPEG-2传输流和AAC / MP3流转换为ISO BMFF(MP4)片段来工作。 如果在浏览器中可用,可以使用Web Worker异步执行此转换。hls.js还支持HLS + fmp4,不需要任何播放器,它可以直接在标准HTML 元素上运行。

video.js 播放 m3u8

在项目中可以使用 video.js 播放m3u8格式的视频文件文件, 同时video.js也对vue3提供支持。

顺便感慨一句 video.js 的文档我真的看不懂😭

首先在项目中安装video.js

yarn add video.js videojs-contrib-hls

初始化video.js

<template>
 <div>
    <div>
       <video id="my-video" class="video-js vjs-default-skin" preload="auto">
           <source :src="vodeoUrl" type="application/x-mpegURL">
       </video>

    <div>
 </div>
</template>

<script lang="ts" setup>
// 定义 videojs 实例
const myPlayer = ref()
const playVideo = reactive({
 videoUrl: 'xxxx.m3u8',
})
onMounted(()=>{
   const videoUrl ="xxx.example.m3u8"
   initVideoSource(videoUrl)
})
function initVideoSource () {
 myPlayer.value = videojs('my-video', {
   controls: true, // 是否显示控制条
   preload: 'auto',
   autoplay: false,
   loop: false, // 循环播放
   // fluid: true, // 自适应宽高
   language: 'zh-CN', // 设置语言
   muted: false, // 是否静音
   inactivityTimeout: false,
   // 设置控制条组件
   controlBar: { // 设置控制条组件
     //  设置控制条里面组件的相关属性及显示与否
     currentTimeDisplay: true,
     timeDivider: true,
     durationDisplay: true,
     remainingTimeDisplay: false,
     volumePanel: {
       inline: false,
     },
     pictureInPictureToggle: false,
     // 或者使用 children 定义
     /* 使用children的形式可以控制每一个控件的位置,以及显示与否 */
     // children: [
     //   { name: 'playToggle' }, // 播放按钮
     //   { name: 'currentTimeDisplay' }, // 当前已播放时间
     //   { name: 'progressControl' }, // 播放进度条
     //   { name: 'durationDisplay' }, // 总时间
     //   {
     //     name: 'volumePanel', // 音量控制
     //     inline: false, // 不使用水平方式
     //   },
     //   { name: 'FullscreenToggle' }, // 全屏
     // ],
   },
   sources: [
     {
       src: playVideo.videoUrl,
       type: 'application/x-mpegURL',
     },
   ],
   userActions: {
     seek: false,
   },
 })

}
</script>

video.js 语言设置

引入 zh-CN.json 语言包后在配置项中设置中文即可,亲测有效

import videojs from "video.js"
import "video.js/dist/video-js.css"
import video_zhCN from 'video.js/dist/lang/zh-CN.json'
videojs.addLanguage('zh-CN', video_zhCN)
在设置里配置成中文:language:“zh-CN
<template>
  <div>
    <video
      id="my-video" class="video-js vjs-default-skin video" preload="auto">
  	</video>
  </div>
</template>

<script lang="ts" setup>

import videojs from 'video.js/dist/video.min'
import 'video.js/dist/video-js.min.css'
import videoLanguage from 'video.js/dist/lang/zh-CN.json'
 // videojs 语言设置
videojs.addLanguage('zh-CN', videoLanguage)
const myPlayer = ref()
const playVideo = reactive<videoPlayType>({
  videoUrl: 'xxxx.m3u8',
})
onMounted(() => {
  initVideoSource()
})
function initVideoSource () {
  videojs.addLanguage('zh-CN', videoLanguage)
  const myPlayer = videojs('my-video', {
    controls: true, // 是否显示控制条
    preload: 'auto',
    autoplay: false,
    fluid: true, // 自适应宽高
    language: 'zh-CN', // 设置语言
    muted: false, // 是否静音
    inactivityTimeout: false,
    controlBar: { // 设置控制条组件
       children: [
        { name: 'playToggle' }, // 播放按钮
        { name: 'currentTimeDisplay' }, // 当前已播放时间
        { name: 'progressControl' }, // 播放进度条
        { name: 'durationDisplay' }, // 总时间
        {
          name: 'volumePanel', // 音量控制
          inline: false, // 不使用水平方式
        },
        { name: 'FullscreenToggle' }, // 全屏
      ],
    },
    sources: [   // 视频播放资源
      {
         src: playVideo.videoUrl,
         type: 'application/x-mpegURL',
      },
    ],
  })
  //  自动播放
  myPlayer.play()
}

</script>

video.js 触发事件汇总

在 video 上的监听事件

事件描述
loadstart浏览器开始在网上寻找媒体数据
progress浏览器正在获取媒体数据
loadedmetadata浏览器获取完毕媒体的时间长和字节数
play即将开始播放,当执行了play方法时触发,或数据下载后元素被设为autoplay属性
pause播放暂停,当执行了pause方式时触发
waiting播放过程由于得不到下一帧而暂停播放(例如下一帧尚未加载完毕),但很快就能够得到下一帧
timeupdate由于播放位置被改变,可能是播放过程中的自然改变,也可能是被人为的改变,或由于播放不能连续而发生的跳变
ended播放结束后停止播放
canplay浏览器能够播放媒体,但估计以当前的播放速率不能直接播放完毕,播放期间需要缓冲
canplaythrough浏览器能够播放媒体,而且以当前播放速率能够将媒体播放完毕,不再需要进行缓冲
seekingseeking属性变为true,浏览器正在请求数据
seekedseeking属性变为false,浏览器停止请求数据
suspend浏览器暂停获取媒体数据,但是下载过程并滑正常结束
ratechangedefaultplaybackRate属性(默认播放速率)或playbackRate属性(当前播放速率)被改变
druationchange播放时长被改变
stalled浏览器尝试获取媒体数据失败
abort浏览器在下载完全部媒体数据之前中止获取媒体数据,但是并不是由错误引起的
error获取媒体数据过程中出错
emptiedvideo元素或audio元素所在网络突然变为未初始化状态可能原因有两个:1.载入媒体过程中突然发生一个致命错误
volumechangevolume属性(音量)被改变或muted属性(静音状态)被改变
fullscreenchange监听全屏事件

设置监听的方式一:在video 的dom元素上绑定事件监听

<template>
  <div>
    <video
      id="my-video" class="video-js vjs-default-skin video" preload="auto"
      @play="onPlayerPlay($event)"
      @pause="onPlayerPause($event)"
      @ended="onPlayerEnded($event)"
      @loadeddata="onPlayerLoadeddata($event)"
      @waiting="onPlayerWaiting($event)"
      @playing="onPlayerPlaying($event)"
      @timeupdate="onPlayerTimeupdate($event)"
      @seeking="onSeek($event)">
  	</video>
  </div>
</template>
<script>
function onPlayerPlay(player:any){
console.log('即将开始播放', player.target)
}
function onPlayerLoadeddata (player:any) {
  // 获取视频总时长,只能在视频加载完成之后获取
  const totalTime = player.target.duration
  console.log('playVideo play!', playVideo)
}

</script>

监听方式二:


function initVideoSource () {
  
  // videojs 语言设置
  videojs.addLanguage('zh-CN', videoLanguage)

  myPlayer.value = videojs('my-video', {
    controls: true, // 是否显示控制条
    preload: 'auto',
    autoplay: false,
    loop: false, // 循环播放
    // fluid: true, // 自适应宽高
    language: 'zh-CN', // 设置语言
    muted: false, // 是否静音
    inactivityTimeout: false,
    // 设置控制条组件
    controlBar: {
      children: [
        { name: 'playToggle' }, // 播放按钮
        { name: 'currentTimeDisplay' }, // 当前已播放时间
        { name: 'progressControl' }, // 播放进度条
        { name: 'durationDisplay' }, // 总时间
        { name: 'remainingTimeDisplay' }, // 剩余时间
        {
          name: 'volumePanel', // 音量控制
          inline: false, // 不使用水平方式
        },
        { name: 'FullscreenToggle' }, // 全屏
      ],
    },
    sources: [
      {
        src: url,
        type: 'application/x-mpegURL',
      },
    ],
    userActions: {
      seek: false,
    },
  })
  myPlayer.value.on('loadedmetadata', function () {
    // 视频播放定位到上次播放时间
    myPlayer.value.currentTime(playVideo.currentSecond)
  })

  // 视频播放结束
  myPlayer.value.on('ended', async () => {
    // 提交视频记录  更新视频信息
    setCurrentVideo()
    getCurrentCourse()
  })

  // 暂停时提交
  myPlayer.value.on('pause', () => {
    setCurrentVideo()
  })

  // 监听全屏按钮事件
  myPlayer.value.on('fullscreenchange', () => {
    console.log('全屏')
  })

 

暂停播放时展示按钮

通过样式控制,vjs-paused 下的按钮设置为 display:block

// 播放暂停时 展示暂停按钮
::v-deep .vjs-paused > .vjs-big-play-button {
  display: block;
}

获取视频总时长

只能在视频加载完之后获取

function onPlayerLoadeddata (player:any) {
  // 获取视频总时长,只能在视频加载完成之后获取
  const totalTime = player.target.duration
  console.log('playVideo play!', playVideo)
}

即时记录播放时间

监听播放进度,获取当前播放事件,通过取余的方式判断当前经过的时间,每过 n 秒向服务器提交一次时间记录用户当前播放的进度

//播放时长(秒)
var totalTime = 0;
myPlayer.on('timeupdate', function() {
  //当前播放时长(秒)
  var currentTime = Math.floor(myPlayer.currentTime());

  if (currentTime > 0 && currentTime > totalTime && (currentTime % 5 == 0)) {
    //每隔5秒,向服务器提交播放时间(秒)
  }
  totalTime = currentTime;
});

禁止视频拖拽、阻止用户快进

只允许用户在当前已经播放的时间内调整观看进度,不允许用户快进。

通过 timeupdate 获取当前视频播放的时间差,当时间小于一秒的属于视频的正常播放,否则为人为的拖拽快进

const myPlayer = ref()
function initVideoSource () {
 // 在video实例初始化时监听视频播放时长更新
  myPlayer.value.on('timeupdate', async () => {
    //  videojs 播放的当前位置
    const currentTime = myPlayer.value.currentTime()
    if (currentTime > 0 && Math.ceil(currentTime) - 1 === playVideo.currentSecond && (Math.ceil(currentTime) % 15 === 0)) {
      // 每隔15秒,向服务器提交播放时间(秒)
      await setCurrentVideo()
    }
    
    // 禁止用户拖拽
    if (currentTime > playVideo.maxSecond) {
      const num = Math.ceil(currentTime) - Math.ceil(playVideo.maxSecond)
      if (num <= 1) {
        // 正常播放时,记录当前播放时间
        playVideo.maxSecond = Math.ceil(currentTime)
        playVideo.currentSecond = Math.ceil(currentTime)
      } else {
        // 差值大于1s时,视为拖动,将上一次记录的播放时间赋值给当前播放器的播放时间
        myPlayer.value.currentTime(playVideo.currentSecond)
      }
    }
  })
}
}

播放视频添加水印

通过获取视频播放的 dom 元素,在 video 组件中手动插入水印图层的 div 方式实现添加水印。

局部添加水印

// 设置局部水印
export function setSingleWaterMark (documentRef:any, str: any) {
  // 获取 video 外层 dom 元素 在其中设置水印
  const videoElement = documentRef.value
  const videoChild = videoElement.firstChild as HTMLElement
  const waterMark = document.createElement('div')
  waterMark.id = 'userName'
  waterMark.setAttribute('class', 'resize-drag')
  // 从父组件传过来的水印内容
  waterMark.innerText = str
  waterMark.style.cssText = 'position:absolute;top:24px;right:24px; color:#c4c6cb; font-size:20px; cursor: default;'
  videoChild.appendChild(waterMark)
}

全局添加水印

设置全局水印的方式与局部水印相似,基于 canvas 实现

  1. 通过 canvas 创建画布,绘制出一张文字填充的图片
  2. 设置好样式后将图片导出作为水印层的背景图平铺
  3. 最后将背景图插入准备好的dom元素中
// 设置全局水印
export const setFullWaterMark = (documentRef:any, str: any) => {
  const id = 'waterMark'
  const videoElement = documentRef.value
  const videoChild = videoElement.firstChild as HTMLElement

  const can = document.createElement('canvas')

  // 设置画布的长宽
  can.width = 300
  can.height = 300
  const cans = can.getContext('2d')!

  // 旋转角度
  cans.rotate((-20 * Math.PI) / 180)
  cans.font = '16px Vedana'

  // 设置填充绘画的颜色、渐变或者模式
  cans.fillStyle = 'rgba(196, 198, 203,.4)'

  // 设置文本内容的当前对齐方式
  cans.textAlign = 'left'

  // 在画布上绘制填色的文本(输出的文本,开始绘制文本的X坐标位置,开始绘制文本的Y坐标位置)
  cans.fillText(str, can.width / 8, can.height / 2)

  const div = document.createElement('div')
  div.id = id
  div.style.pointerEvents = 'none'
  div.style.top = '0px'
  div.style.left = '0px'
  div.style.position = 'absolute'
  div.style.zIndex = '100000'
  div.style.width = document.documentElement.clientWidth + 'px'
  div.style.height = screen.height + 'px' // 全屏时的屏幕高度
  div.style.background = 'url(' + can.toDataURL('image/png') + ') left top repeat'
  videoChild.appendChild(div)
}

由于视频当前水印的大小获取的是浏览器的宽高,能够适用于播放器全屏的情况下,

在浏览器未全屏的情况下可以用 overflow: hidden 控制水印的范围

在初始化 video 组件时添加水印


function initVideoSource () {
   ....
  // 设置视频全屏水印
  setFullWaterMark('水印名称xxx')
  
  //设置局部水印
  setWaterMark()
}

视频不能自动播放

在使用videojs播放视频的过程可能会存在的问题。

报错 DOMException: play() failedbecause the user didn’t interact with the document first. (用户还没有交互,不能调用play)

在最新版的Chrome浏览器(以及所有以Chromium为内核的浏览器)中,已不再允许自动播放音频和视频。就算你为video或audio标签设置了autoplay属性也一样不能自动播放, 所以浏览器在页面没有产生用户交互时时不能进行自动播放

解决方案 : 设置muted属性为true

<video id="videoPlayer" class="video-js" muted></video>

关于muted属性:

  • muted 属性,设置或返回音频是否应该被静音(关闭声音);属性的值是truefalse;
  • muted="false" 表示视频不用静音(视频播放便有声音),但设置 muted="fasle"  的情况下,视频无法实现自动播放
  • video 标签中 muted  的作用: 允许视频自动播放;(Chrome66版本开始,禁止视频和音频的自动播放)

参考

  1. vue3 项目添加水印的实现方法
  2. videojs中文文档详解_谢泽的网络日志-CSDN博客_video.js