首页前端工具函数Videojs 使用 Hls 对视频加密解码

Videojs 使用 Hls 对视频加密解码

分类前端工具函数时间2025-12-18 15:55:26发布RustStream浏览380
摘要:下载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版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

Vue3 + VueOffice 全家桶进行 多格式文件预览 本地部署 AI 大模型

游客 回复需填写必要信息
召唤伊斯特瓦尔