ITPub博客

首页 > IT职业 > IT职场 > 从零开发弹幕视频播放器1

从零开发弹幕视频播放器1

IT职场 作者:sherrywong 时间:2021-09-09 08:53:23 0 删除 编辑

介绍

以前想在网站放播放视频,就需要安装 flash 插件,但是 flash 占用系统资源高。而且它是 Adobe 一项封闭的商业应用,内置 flash 有可能引入相关的安全漏洞,苹果更是大力反对 falsh。

现在视频网站几乎都用 html 5 播放视频,它占用资源小更省电、省流量,是一项完全免费并且开放的新标准。无需安装任何插件直接使用 video 标签就行,而且它的兼容性也非常好,所有主流浏览器都支持。

图片描述

video 标签

video controls poster="/poster.jpg">
    source
        src="https://interactive-examples.mdn.mozilla.net/media/examples/friday.mp4"
        type="video/mp4"
    >
    source src="/other.webm" type="video/webm">
    track 
        src="https://interactive-examples.mdn.mozilla.net/media/examples/friday.vtt"
        kind="captions" 
        srclang="en"
        label="显示在这"
        default
    >
    浏览器不支持 video 标签
video>
复制代码

video

video 除了上面展示的两个还有很多属性。

图片描述
payload 3 个值如下:

    none      不进行预加载
    metadata  预加载视频元数据
    auto      预加载整个视频
复制代码

source

上面 video 标签下的 source 是用来指定视频的地址,如果浏览器不支持这个格式它就会查看下一个 source,也可以简单的使用 video 的 src 属性。

使用 Media Fragments API 可以为视频添加开始和结束时间。

视频将在 2 秒播放,5 秒结束。它的格式为 #t=[start_time][,end_time],需要确保服务器支持 Range Requests。

track

track 元素使用 WebVTT 格式来显示字幕。一个媒体元素的任意两个 track 子元素不能有相同的 kind, srclang, 和 label属性。

  • default 指定 track 默认启用
  • label 给浏览器使用的 text track 的标题,这种标题是用户可读的

图片描述

  • src 字幕地址

  • srclang 文本数据的语言,中文是 zh,如果 kind 属性被设为 subtitles, 那么必须定义此属性。

  • kind 定义 text track 应该如何使用。如果省略,默认就是 subtitles,它有以下属性值:

      subtitles     字幕给观影者看不懂的内容提供解释
      captions      隐藏式字幕提供了音频的转录甚至是翻译
      descriptions  视频内容的文本描述
      chapters      章节标题用于用户浏览媒体资源的时候
      metadata      脚本使用的 track 用户不可见
    复制代码
    

JS 中的 video

在 js 中,通过 document.querySelector('video') 等方式获取 video 元素,就可以操作视频行为了,下面介绍 video 常用的事件、属性和方法。

事件

加载相关

图片描述

错误或特殊情况

图片描述

播放时

图片描述

属性

通过 video 元素,我们可以获取上面提到的属性,也可以改变它来操作视频,比如设置 video.muted=true 设置静音。

图片描述

video 元素上还有 readyState 属性,表示视频当前的状态,它的值 0 到 4 的数字,video 上有相关描述的常量属性。

图片描述

方法

图片描述

它的参数可能是:

  • video/ogg
  • video/mp4
  • video/webm
  • video/ogg; codecs=“theora, vorbis”
  • video/mp4; codecs=“avc1.4D401E, mp4a.40.2”
  • video/webm; codecs=“vp8.0, vorbis”

视频播放器

div class="player player-loading">
    video
      playsinline
      x5-playsinline
      src="https://interactive-examples.mdn.mozilla.net/media/examples/flower.webm"
    />
    div class="loading">div>
    div class="controls">
      div class="bar">
        div class="bar_buffered">div>
        div class="bar_played">div>
      div>
      div class="control_items">
        button class="play_btn">button>
        div class="time">0 / 0div>
        button class="fullscreen">button>
      div>
    div>
div>
复制代码

其中 video 设置了 playsinline 属性,是为 IOS 视频播放时不自动进入全屏。x5-playsinline 是让腾讯 x5浏览器内核不自动进入全屏。X5 是腾讯基于 Webkit 开发的浏览器内核,应用于 Android 端的微信、QQ 等应用。更多关于 x5 video 属性参考这里。

.player-loading .loading { opacity: 1; }
.player-playing .play_btn::after { content: '暂停'; }
.player-controls-hide { cursor: none; }
.player-controls-hide .controls { opacity: 0; }
.player-fullscreen .fullscreen:after { content: '退出全屏'; }
复制代码

加点简单的 CSS,这里主要关注使用 JS 实现功能的核心代码,样式部分就省略了。

const player = document.querySelector('.player')
const video = document.querySelector('video')
复制代码

控制器的显示和隐藏

关于控制器显示/隐藏需要注意几点:

  1. 当视频没有播放时控制器要显示出来
  2. 当视频播放时需要等一会儿再将控制器隐藏
  3. 当视频播放时点击鼠标或移动鼠标需要将控制器显示
  4. 当视频播放结束时控制器显示出来
let controlsTimer = null
function showControls() {
    clearTimeout(controlsTimer)
    player.classList.remove('player-controls-hide')
    updatePlayedBarAndTime() // 更新进度条,查看下一小节
}
function delayHideControls() {
    showControls();
    controlsTimer = setTimeout(() => {
        if (video.played.length && !video.paused) {
            player.classList.add('player-controls-hide')
        }
    }, 3000)
}
player.addEventListener('click', delayHideControls)
player.addEventListener('mousemove', delayHideControls)
video.addEventListener('play', delayHideControls)
video.addEventListener('pause', showControls)
复制代码

这里主要是通过判断 video.played.length && !video.paused 来判断是否隐藏控制器,也就是视频播放过并且视频正在播放,这里没有监听 ended 事件,因为播放完毕也会触发 pause 事件。

进度条和时间显示

const playedBar = document.querySelector('.bar_played')
const bufferedBar = document.querySelector('.bar_played')
const time = document.querySelector('.time')

function updatePlayedBarAndTime(currentTime, percentage) {
    currentTime = currentTime == null ? Math.round(video.currentTime) : currentTime
    percentage = percentage == null ? Math.min(video.currentTime / video.duration, 1) : percentage
    time.textContent = currentTime + ' / ' + Math.round(video.duration)
    playedBar.style.transform = 'scaleX('+ percentage +')'
}

video.addEventListener('durationchange', updatePlayedBarAndTime)
video.addEventListener('timeupdate', () => {
  if (!player.classList.contains('player-controls-hide')) {
    updatePlayedBarAndTime()
  }
})
video.addEventListener('progress', () => {
  const percentage = video.buffered.length ? video.buffered.end(video.buffered.length - 1) / video.duration : 0;
  bufferedBar.style.transform = 'scaleX('+ Math.min(percentage, 1) +')'
})
复制代码

通过监听 timeupdate 事件在控制器显示的情况下更新 DOM,progress 事件更新视频缓存进度条 video.buffered.end(video.buffered.length - 1) 可以获取最后一段 TimeRange 的结束时间。

播放控制

const playPauseBtn = document.querySelector('.play_btn')

playPauseButton.addEventListener('click', evt => {
  evt.stopPropagation() // 为了防止父级处理事件,比如视频控件
  if (video.paused) {
    video.play()
  } else {
    video.pause()
  }
})
video.addEventListener('play', () => {
  player.classList.add('player-playing')
})
video.addEventListener('pause', () => {
  player.classList.remove('player-playing')
})
video.addEventListener('ended', () => {
  player.classList.remove('player-playing')
  video.currentTime = 0
})
复制代码

这里没有在按钮点击事件中处理视频播放暂停 UI 变化而是在 video 事件中处理,是为了让 UI 更精准,不止有这个按钮会控制视频播放和暂停。这里没有展示控制视频播放速率,控制播放速率直接设置 video.playbackRate 就行。

控制进度条

const bar = document.querySelector('.bar')
const { width: barWidth, x: barLeft } = bar.getBoundingClientRect()
let barPending = false
let barLastX = 0
let videoSeekTime = -1

function onBarPointerStart(evt) {
    evt.preventDefault()
    bar.setPointerCapture(ev.pointerId)
    bar.addEventListener('pointermove', this.onPointerMove, true)
    video.pause()
    barLastX = evt.pageX
    const [percentage, seekTime] = calcPercentageAndSeekTime(evt.pageX)
    updatePlayedBarAndTime(seekTime, percentage)
}
function onBarPointerMove(evt) {
    evt.preventDefault()
    barLastX = evt.pageX
    if (barPending) return
    barPending = true
    requestAnimationFrame(handleBarMove)
}
function onBarPointerEnd(evt) {
    evt.preventDefault()
    bar.releasePointerCapture(evt.pointerId)
    bar.removeEventListener('pointermove', onBarPointerMove, true)
    if (videoSeekTime > 0) video.curentTime = videoSeekTime
    video.play()
}
function calcPercentageAndSeekTime() {
    const percentage = Math.max(Math.min((barLastX - barLeft) / barWidth, 1), 0)
    videoSeekTime = percentage * video.duration
    return [percentage, videoSeekTime]
}
function handleBarMove() {
    if (!barPending) return
    const [percentage, seekTime] = calcPercentageAndSeekTime()
    updatePlayedBarAndTime(seekTime, percentage)
    barPending = false
}

bar.addEventListener('pointerdown', onBarPointerStart, true)
bar.addEventListener('pointerup', onBarPointerEnd, true)
bar.addEventListener('pointercancel', onBarPointerEnd, true)
复制代码

这里使用 PointerEvent 来实现监听控制条的拖拽,它的好处是兼容 PC 的鼠标拖拽和移动的手势拖拽,结束时通过设置 video.curentTime 来跳到指定时间点。控制音量与这个相似。

全屏

const isIos = /(iPad|iPhone|iPod)/gi.test(navigator.platform)
const fullscreenBtn = document.querySelector('.fullscreen')
fullscreenBtn.addEventListener('click', evt => {
    evt.stopPropagation()
    if (document.fullscreenElement) {
        if (isIos) {
            video.webkitExitFullscreen()
        } else {
            document.exitFullscreen()
        }
    } else {
        if (isIos) {
            video.webkitEnterFullscreen()
        } else {
            player.requestFullscreen()
        }
    }
})
document.addEventListener('fullscreenchange', () => {
  player.classList.toggle('player-fullscreen', document.fullscreenElement)
})
复制代码

这里需要注意对 IOS 的兼容。对于老浏览器请求、退出和全局全屏元素都需要添加浏览器前缀。想要跨浏览器兼容的全屏 API 可以使用 screenfull.js。

Loading 处理

video.addEventListener('canplay', () => {
    player.classList.remove('player-loading')
})
video.addEventListener('waiting', () => {
    player.classList.add('player-loading')
    
    const startWaitingTime = video.currentTime
    const checkCanPlay = () => {
      if (startWaitingTime !== video.currentTime) {
        player.classList.remove('player-loading')
        video.removeEventListener('timeupdate', checkCanPlay)
      }
    }
    video.addEventListener('timeupdate', checkCanPlay)
})
复制代码

并不是所有浏览器在 waiting 事件触发后,当可播放时还会触发 canplay 事件。所以这里通过 timeupdate 事件来比对时间,确认已经可以播放视频了。

不过并不是所有浏览器能正确触发 waiting 事件,所以我们需要自己检测是否停住等待加载视频。

let prevCurrentTime = 0
let playerLoading = false
let loadingTimer = setInterval(() => {
    const currentTime = video.currentTime
    if (!playerLoading && prevCurrentTime === currentTime && !video.paused) {
        player.classList.add('player-loading')
        playerLoading = true
    } else if (playerLoading && currentTime !== prevCurrentTime) {
        player.classList.remove('player-loading')
        playerLoading = false
    }
    prevCurrentTime = currentTime
}, 100)
复制代码

这里实现比较简单主要是通过定时器去不断获取视频 currentTime 通过比对它来确定视频是否卡住等待播放。还可以将上面监听 progress 事件获取到的 buffered 时间,比对 currentTime 来决定是否去除 player-loading。

字幕

if (video.textTracks && video.textTracks.length) {
    const defaultLang = navigator.language?.split('-')[0] || 'zh'
    let saw = false
    video.textTracks.forEach(track => {
        if (track.language === defaultLang) {
            track.mode = 'showing'
            saw = true
        } else {
            track.mode = 'hidden'
        }
    })
    if (!saw) video.textTracks[0].mode = 'showing'
    // 这里只是默认显示一个 textTrack
    // 应该有个菜单可以让用户选择字幕,这里就省略了
}
复制代码
::cue {
  color:#fff;
  background: transparent;
  text-shadow: 2px 3px 5px rgba(0, 0, 0, .5);
}
复制代码

上面我们通过控制 textTrack 的 mode 属性控制显示哪个 textTrack,通过 ::cue 伪类设置字幕样式,但是如果要更精准的控制字幕,我们就需要自己使用 DOM 元素来显示字幕。

if (video.textTracks && video.textTracks.length) {
    const container = document.querySelector('.player_subtitle') // 字幕容器
    const track = video.textTracks[0]
    track.mode = 'hidden'
    track.oncuechange = () => {
        const cue = track.activeCues[0] // cue 代表一条字幕
        container.innerHTML = ''
        if (cue) {
            const subtitle = document
                                .createElement('div')
                                .appendChild(cue.getCueAsHTML())
                                .innerHTML
            // 因为 getCueAsHTML 返回的是 document-fragment
            
            container.innerHTML = subtitle.split(/rn|n|r/).map(t => `

${t}`
).join('') } } } 复制代码

通过监听 oncuechange 获取当前 cue,然后获取它的内容,然后加入到自定义字幕容器中。 cue 对象长下面这样。

图片描述

画中画

Picture-in-Picture(画中画)可以让视频弹出来小屏播放,就算最小化浏览器或者切换其他 tab 页也可以播放。

图片描述

if (document.pictureInPictureEnabled) { // 是否启用画中画
    const pip = document.querySelector('.pip')
    pip.style.display = 'block'
    pip.addEventListener('click', () => {
        if (document.pictureInPictureElement) { // 当前画中画的元素
            video.exitPictureInPicture()
        } else {
            video.requestPictureInPicture() // 返回 Promise,里面是 pipWindow
        }
    })

    video.addEventListener('enterpictureinpicture', pipWindow => {
        console.log(pipWindow.width, pipWindow.height)
        player.classList.add('player-pip')
    })

    video.addEventListener('leavepictureinpicture', () => {
       player.classList.remove('player-pip')
    })
}
复制代码

截图

视频截图是通过在 canvas 渲染视频实现的。

const screenshotBtn = document.querySelector('.screenshot_btn')

screenshotBtn.addEventListener('click', () => {
    const canvas = document.createElement('canvas')
    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight
    canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height)

    const fileName = video.currentTime + '.png'
    canvas.toBlob(blob => {
        const url = URL.createObjectURL(blob)
        const a = document.createElement('a')
        a.href = url
        a.download = fileName
        a.style.display = 'none'
        document.body.appendChild(a)
        a.click()
        document.body.removeChild(a)
        URL.revokeObjectURL(url)
    }, 'image/png')
})
复制代码

源码

github.com/woopen/npla…(欢迎点赞????)

图片描述

总结

此系列文章,将教大家做个弹幕视频播放器,这篇文章主要介绍了实现功能的关键代码,但对代码组织和细节方面欠缺考虑,感兴趣的同学可以点击上方源码链接,查看源码。

参考:

作者:wopen
链接:https://juejin.cn/post/6953429334937829384
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/46/viewspace-2807218/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2019-04-12

  • 博文量
    83
  • 访问量
    594