Videojs 使用 Hls 对视频加密解码
摘要:下载Hls.js ˃ npm install hls.js video 播放组件 import { onMounted, onBeforeUnmount, watch, ref, nextTick } from 'vue' import { getVideoUrl } from './index' import { getToken } from '@/utils/auth' import vide<!--autointro-->...
下载Hls.js
npm install hls.js
video 播放组件
<template>
<div
v-bind="$attrs"
class="videoPlayer"
ref="videoPlayerRef"
v-loading="loading"
element-loading-text="视频加载中"
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(0, 0, 0, 0.8)"
>
<video class="video-js" ref="videoRef" :id="id" style="width: 100%; height: 100%;object-fit: cover;position: relative;" :poster="poster" controls></video>
</div>
</template>
<script setup>
import { onMounted, onBeforeUnmount, watch, ref, nextTick } from 'vue'
import { getVideoUrl } from './index'
import { getToken } from '@/utils/auth'
import videojs from 'video.js'
import 'video.js/dist/video-js.css'
import 'videojs-flvjs-es6'
import 'videojs-flash'
import Hls from 'hls.js'
const overrideNative = ref(false)
const props = defineProps({
videoId: { type: String, default: '' },
})
const loading = ref(false)
const videoRef = ref(null)
const poster = ref('')
const videoID = ref(props.videoId)
// ============================================================
let player
const initPlayer = () => {
if (player) {
player.dispose()
player.value = null
}
try {
player = videojs(videoRef.value, options(), playerReady)
} catch (error) {}
}
const playerReady = () => {
emit('playerMounted', player)
// 强制显示大播放按钮 及其样式自定义
player.bigPlayButton.show()
player.bigPlayButton.el().style.display = 'block'
player.bigPlayButton.el().style.opacity = '1'
player.bigPlayButton.el().style.top = '50%'
player.bigPlayButton.el().style.left = '50%'
player.bigPlayButton.el().style.transform = 'translate(-50%, -50%)'
player.bigPlayButton.el().style.borderRadius = '50%'
// 添加自定义播放控制
player.ready(() => {
console.log('Video.js播放器准备就绪')
loading.value = false
// 监听视频可播放事件
player.on('loadeddata', () => {
console.log('视频已加载,可以播放')
emit('videoCanplaythrough')
})
// 处理播放错误
player.on('error', (e) => {
console.error('播放错误:', player.error())
})
})
// 大按钮点击事件(强制绑定)
player.bigPlayButton.on('click', async () => {
videoRef.value.play()
player.bigPlayButton.hide()
emit('videoPlay')
})
player.on('canplaythrough', function () {
videojs.log('视频可以播放')
emit('videoCanplaythrough')
})
player.on('play', function () {
videojs.log('视频准备播放')
// 此代码导致无法拉动长度,换用挂在完毕之后外侧处理
player.currentTime(props.videoProgress)
emit('videoPlay')
})
player.on('playing', function () {
player.bigPlayButton.hide()
videojs.log('视频已开始播放')
emit('videoPlaying')
})
player.on('pause', function (event) {
player.bigPlayButton.show()
videojs.log('视频已暂停播放')
emit('videoPause', event.target.player.cache_?.currentTime)
})
player.on('seeked', function (event) {
emit('videoSeeked', event.target.player.cache_?.currentTime)
})
// 监听视频播放结束事件
player.on('ended', function () {
emit('videoEnded')
})
player.on('timeupdate', function () {
var playedSeconds = player.currentTime() // 获取当前播放的秒数
var duration = player.duration() // 获取视频总长度
// 你可以在这里编写代码来处理视频播放进度
emit('videoTimeupdate', playedSeconds)
})
}
// =========================== 方法一 ====================================
// 获取加密地址与密钥
const getVideoInfo = async () => {
loading.value = true
getVideoUrl(videoID.value).then((res) => {
poster.value = res.data.posterUrl ? res.data.posterUrl : ''
if (Hls.isSupported()) {
const hls = new Hls({
debug: false,
xhrSetup: function(xhr, url) {
// 自定义密钥请求
if (url.includes('/key/')) {
xhr.setRequestHeader('Authorization', 'Bearer ' + getToken());
}
}
});
hls.loadSource(res.data.m3u8Url);
hls.attachMedia(videoRef.value);
nextTick(() => {
initPlayer() // 初始化播放器
loading.value = false
})
} else if (videoRef.value.canPlayType('application/vnd.apple.mpegurl')) {
// Safari原生支持
videoRef.value.src = res.data.m3u8Url;
player = videojs(videoRef.value, options());
loading.value = false
} else {
console.log('您的浏览器不支持 HLS 播放')
}
})
}
// ======================================== 方法二 ==================================================
// const getVideoInfo = async () => {
// loading.value = true
// getVideoUrl(videoID.value).then((res) => {
// poster.value = res.data.posterUrl ? res.data.posterUrl : ''
// // 销毁现有播放器
// if (player) {
// player.dispose()
// player = null
// }
// // 初始化Video.js播放器
// player = videojs(videoRef.value, {
// ...options(),
// html5: {
// hls: {
// withCredentials: false,
// // Video.js使用beforeRequest而不是xhrSetup
// beforeRequest: function(options) {
// // 判断是否是密钥请求
// if (options.uri && options.uri.includes('/key/')) {
// const token = getToken()
// if (!token) {
// console.error('Token为空!')
// // 可以跳转到登录页或刷新token
// }
// // 添加Authorization头
// options.headers = options.headers || {}
// options.headers['Authorization'] = 'Bearer ' + token
// }
// return options
// }
// }
// },
// sources: [{
// src: res.data.m3u8Url,
// type: 'application/x-mpegURL'
// }],
// autoplay: false
// })
// // 监听播放器准备事件
// playerReady()
// player.ready(() => {
// // console.log('Video.js播放器准备就绪')
// // loading.value = false
// // // 监听错误事件
// // player.on('error', (e) => {
// // console.error('播放错误:', player.error())
// // // 如果是401错误,说明token有问题
// // const error = player.error()
// // if (error && error.code === 4) {
// // console.log('检测到权限错误(401),可能是token过期')
// // }
// // })
// // 监听网络请求
// setupRequestInterceptor()
// })
// }).catch(error => {
// console.error('获取视频信息失败:', error)
// loading.value = false
// })
// }
// const setupRequestInterceptor = () => {
// // 方法1:拦截所有XMLHttpRequest
// const originalXHROpen = XMLHttpRequest.prototype.open
// const originalXHRSend = XMLHttpRequest.prototype.send
// XMLHttpRequest.prototype.open = function(method, url) {
// this._requestMethod = method
// this._requestUrl = url
// return originalXHROpen.apply(this, arguments)
// }
// XMLHttpRequest.prototype.send = function(body) {
// // 拦截HLS相关请求
// if (this._requestUrl &&
// (this._requestUrl.includes('.m3u8') ||
// this._requestUrl.includes('.ts') ||
// this._requestUrl.includes('/key/'))) {
// // console.log('拦截HLS请求:', this._requestUrl)
// // 如果是密钥请求,添加token
// if (this._requestUrl.includes('/key/')) {
// const token = getToken()
// // console.log('为密钥请求手动添加token')
// this.setRequestHeader('Authorization', 'Bearer ' + token)
// }
// // 监听请求状态
// this.addEventListener('readystatechange', function() {
// if (this.readyState === 4) {
// // console.log('HLS请求完成:', {
// // url: this._requestUrl,
// // status: this.status,
// // statusText: this.statusText
// // })
// if (this.status === 401) {
// console.error('权限验证失败(401),token可能无效')
// }
// }
// })
// }
// return originalXHRSend.apply(this, arguments)
// }
// }
watch(() => props.videoId, (newVal) => {
if(!newVal) return
getVideoInfo()
}, { immediate: true, deep: true })
const emit = defineEmits([
// 播放器挂载完毕
'playerMounted',
'videoCanplaythrough',
'videoPlay',
'videoPlaying',
'videoPause',
'videoSeeked',
'videoEnded',
'videoTimeupdate'
])
// VideoJs更多选项配置可以参考中文文档:
function options() {
return {
html5: {
hls: {
overrideNative: overrideNative
},
nativeVideoTracks: !overrideNative,
nativeAudioTracks: !overrideNative,
nativeTextTracks: !overrideNative
},
autoplay: false, // true,浏览器准备好时开始播放。
muted: true, // 默认情况下将会消除音频。
loop: false, // 导致视频一结束就重新开始。
controls: true,
preload: 'auto', // auto浏览器选择最佳行为,立即开始加载视频(如果浏览器支持)
fluid: true, // 当true时,将按比例缩放以适应其容器。
type: 'application/x-mpegURl',
notSupportedMessage: '此视频暂无法播放,请稍后再试', // 无法播放媒体源时显示的默认信息。
textTrackDisplay: false,
playbackRates: [0.5, 1, 1.5, 2], //倍速
}
}
onBeforeUnmount(() => {
if (player) {
player.dispose()
player = null
}
})
</script>
<!-- scoped -->
<style lang="scss">
.videoPlayer {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.video-js {
padding-top: 0 !important;
}
.video-js .vjs-time-control {
display: block;
}
.video-js .vjs-remaining-time {
display: none;
}
.vjs-playback-rate .vjs-playback-rate-value {
font-size: 1em;
line-height: 3em;
}
.vjs-poster {
background-color: #00000000;
}
.video-js:hover .vjs-big-play-button{
background-color: transparent;
opacity: 1;
}
.video-js .vjs-big-play-button{
background: transparent;
width: 2em;
height: 2em;
font-size: 10em;
background: rgba(0,0,0,0.1) url(./play.png) center / 100% 100% no-repeat !important;
border: 0;
border-radius: 50%;
.vjs-icon-placeholder{
display: none;
}
}
</style>
方法一中 点击视频元素无法进行播放,只能点击播放按钮;方法二中所有元素都可点击控制播放
// decode.vue
<template>
<div class="content_box">
<div class="wrapper">
<div class="news-detail-title">
{{ title }}
</div>
<div class="news-detail-content">
<VideoPlayer
v-if="videoID"
:videoId="videoID"
style="width: 100%; height: 500px"
/>
</div>
</div>
</div>
</template>
<script setup>
import { getVideoInfo } from './index.js'
import VideoPlayer from '@/components/DecodeVideo/index.vue'
const route = useRoute()
const title = ref('')
const videoID = ref('')
const getVideoData = (id) => {
getVideoInfo(id).then((res) => {
title.value = res.data.title
videoID.value = res.data.videoId
})
}
onMounted(() => {
// 新版解码
getVideoData(route.query.id)
})
</script>
<style lang="scss" scoped>
.content_box {
width: 100%;
height: 100%;
background: #f1f4f9;
padding: 20px 0px;
box-sizing: border-box;
.wrapper {
background-color: #fff;
padding: 20px;
box-sizing: border-box;
.news-detail-title {
text-align: center;
color: #0043b2;
font-size: 30px;
font-weight: bold;
margin: 10px 85px;
}
.news-detail-post {
text-align: center;
margin: 0 120px;
color: #999;
height: 60px;
line-height: 60px;
border-bottom: 1px solid #999;
font-size: 12px;
span {
margin-right: 10px;
}
}
.news-detail-content {
padding: 10px 50px;
min-height: calc(100vh - 122px - 48px - 330px);
margin: 0 80px;
line-height: 36px;
}
}
}
</style>
接口文件
import request from '@/utils/request'
export function getVideoKey(videoId) {
return request({
url: `/client/encrypted/video/key/${videoId}`,
method: 'get',
headers: {
'Content-Type': 'application/octet-stream'
}
})
}
export function getVideoUrl(videoId) {
return request({
url: `/client/encrypted/video/playUrl/${videoId}`,
method: 'get',
})
}
本文链接:https://blog.smallhao.fun/?id=44 转载需授权!
Chen’Blog版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!