Files
taimed-international-app/components/video-player/index.vue

467 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view>
<yb-video
ref="video" :title="videoTitle" height="400rpx"
autoplay
render-type="renderjs"
:crossOrigin="crossOrigin"
:src="src"
:initialTime="initialTime"
:custom="custom"
:poster="currentVideo.type == 2 ? poster : ''"
:alwaysShowControls="currentVideo.type == 2"
header
controls
open-direction="landscape-primary"
exitDirection="portrait"
object-fit="contain"
@loadeddata="handleLoaded"
@timeupdate="handleTimeUpdate"
@ended="handleEnded"
@loadstart="handleLoadStart"
@canplay="handleCanPlay"
@play="handlePlay"
@fullscreenchange="handleFullscreenChange"
@error="handleError"
>
<view v-if="!canPlay && !videoError" class="video-mask">{{ loadingText }}</view>
<view v-if="canNext && !videoError" class="video-mask">
<view>播放完成倒计时 <text style="font-size: 40rpx; font-weight: bold;">{{ countdown }}</text> 后播放下一条</view>
<view class="video-control-btn-group">
<view class="video-control-btn" @click="handleReplay">重播</view>
<view class="video-control-btn" @click="handleNext">立即播放</view>
</view>
</view>
<view v-if="videoError && videoErrorMsg !== ''" class="video-mask">
<view>{{ videoErrorMsg }}</view>
</view>
</yb-video>
</view>
</template>
<script>
import { BEFORE_NAVIGATE_BACK_EVENT } from "@/components/nav-bar/back-events"
export default {
props: {
videoTitle: {
type: String,
default: '',
},
poster: {
type: String,
default: '',
},
currentVideoIndex: {
type: Number,
default: 0,
},
currentVideoList: {
type: Array,
default: () => [],
},
http: {
type: Object,
default: () => {},
},
},
computed: {
canNext() {
return this.isEnded && this.currentVideoIndex < this.currentVideoList.length - 1
},
currentVideo() {
return this.currentVideoList[this.currentVideoIndex] || {}
}
},
watch: {
currentVideo: {
handler(newVal, oldVal) {
if (newVal && newVal.id && newVal.id !== (oldVal && oldVal.id)) {
if (oldVal && oldVal.id) {
this._savePositionSnapshot(oldVal.id, parseInt(this.currentTime))
}
this._resetState();
this.updateCustomConfig();
}
},
immediate: true
}
},
data () {
return {
src: '',
isDestroyed: false,
beforeBackHandler: null,
canPlay: false,
videoError: false,
videoErrorMsg: '',
loadingText: '媒体文件加载中...',
currentTime: 0,
initialTime: 0,
isEnded: false,
countdown: 10,
countdownTimer: null,
endedTimer: null,
_requestVersion: 0,
three: '',
crossOrigin: '',
danmu: [],
quality: [],
works: [],
custom: {
slots: [{
innerHTML: '<div class="fast-progress-btn-box" style="left:15%;"><view class="yb-player-btn">-15s</view></div>',
followControls: true,
click: () => {
console.log('快退 基准时间 ' + this.currentTime)
this.$refs.video.seek(this.currentTime - 15)
}
},{
innerHTML: '<div class="fast-progress-btn-box" style="right:15%;"><view class="yb-player-btn">+15s</view></div>',
followControls: true,
click: () => {
console.log('快进 基准时间 ' + this.currentTime)
this.$refs.video.seek(this.currentTime + 15)
}
}],
header: {
disableMore: true,
disableBack: true,
},
controls: {
disableDanmuSend: true,
disableDanmuVisible: true,
disableFullscreen: false,
},
progress: {
rightSlots: [],
disableFullscreen: false,
},
},
text: ''
}
},
beforeUnmount() {
this._unbindBeforeBack()
this._cleanup()
},
beforeDestroy() {
this._unbindBeforeBack()
this._cleanup()
},
created() {
this._bindBeforeBack()
},
methods: {
_resetState() {
this.canPlay = false;
this.isEnded = false;
this.videoError = false;
this.videoErrorMsg = '';
this.loadingText = '媒体文件加载中...';
this.currentTime = 0;
this.initialTime = 0;
this.isDestroyed = false;
this._requestVersion++;
if (this.countdownTimer) {
clearInterval(this.countdownTimer);
this.countdownTimer = null;
}
if (this.endedTimer) {
clearTimeout(this.endedTimer);
this.endedTimer = null;
}
},
_bindBeforeBack() {
if (this.beforeBackHandler || !uni.$on) return
this.beforeBackHandler = () => {
this._cleanup()
}
uni.$on(BEFORE_NAVIGATE_BACK_EVENT, this.beforeBackHandler)
},
_unbindBeforeBack() {
if (!this.beforeBackHandler || !uni.$off) return
uni.$off(BEFORE_NAVIGATE_BACK_EVENT, this.beforeBackHandler)
this.beforeBackHandler = null
},
unload() {
this._cleanup()
},
_cleanup() {
if (this.isDestroyed) return
this.isDestroyed = true
const videoId = this.currentVideo ? this.currentVideo.id : null
const position = parseInt(this.currentTime)
if (this.$refs.video && this.$refs.video.unload) {
this.$refs.video.unload()
}
if (this.countdownTimer) {
clearInterval(this.countdownTimer);
this.countdownTimer = null;
}
if (this.endedTimer) {
clearTimeout(this.endedTimer);
this.endedTimer = null;
}
this._savePositionSnapshot(videoId, position)
},
_savePositionSnapshot(videoId, position) {
if (!videoId) return
this._saveLocalVideoPosition(videoId, position)
this._saveRemoteVideoPosition(videoId, position)
},
_saveLocalVideoPosition(videoId, position) {
const videoPosition = JSON.parse(uni.getStorageSync('videoPosition') || '{"order":[], "position":{}}')
if (!videoPosition.position) videoPosition.position = {}
if (!videoPosition.order) videoPosition.order = []
const videoKeys = new Set(videoPosition.order)
if (videoKeys.size > 20) {
const firstId = videoKeys.values().next().value
delete videoPosition.position[firstId]
videoKeys.delete(firstId)
videoPosition.order = Array.from(videoKeys)
}
videoPosition.position[videoId] = position
videoKeys.add(videoId)
uni.setStorageSync('videoPosition', JSON.stringify(videoPosition))
},
_saveRemoteVideoPosition(videoId, position) {
const http = this.http
if (!http || !http.request) return
http.request({
url: "sociology/course/saveCoursePosition",
method: "POST",
data: { videoId, position },
header: { 'Content-Type': 'application/json' },
}).then(() => {
console.log('保存视频播放位置成功:', videoId, position)
}).catch(() => {
console.log('保存视频播放位置失败')
})
},
updateCustomConfig() {
if (this.custom && this.custom.controls) {
this.custom.controls.disableFullscreen = this.currentVideo.type == 2;
this.custom.progress.disableFullscreen = this.currentVideo.type == 2;
}
this.changeSrc()
},
handleTimeUpdate (time) {
if (this.isDestroyed) return
if (!this.canPlay) return
if (this.isEnded && time > 0) {
this.isEnded = false
if (this.countdownTimer) {
clearInterval(this.countdownTimer)
this.countdownTimer = null
}
}
this.currentTime = time
this._saveLocalVideoPosition(this.currentVideo.id, parseInt(time))
},
handleEnded () {
if (this.isDestroyed) return
this.endedTimer = setTimeout(() => {
if (this.isDestroyed) return
this.isEnded = true;
this.startCountdown();
this.currentTime = 0
}, 300);
this._savePositionSnapshot(this.currentVideo.id, parseInt(this.currentTime))
if (!this.isDestroyed && this.$refs.video && this.$refs.video.exitFullscreen) {
this.$refs.video.exitFullscreen()
}
},
startCountdown() {
this.countdown = 10;
if (this.countdownTimer) {
clearInterval(this.countdownTimer);
}
this.countdownTimer = setInterval(() => {
this.countdown--;
if (this.countdown <= 0) {
clearInterval(this.countdownTimer);
this.handleNext();
}
}, 1000);
},
handleLoadStart () {
this.loadingText = '媒体文件加载完成,准备播放'
},
handleCanPlay () {
if (this.isDestroyed) return
this.canPlay = true
this.videoError = false
},
handlePlay () {
if (this.isDestroyed) return
if (this.isEnded) {
this.isEnded = false
}
if (this.countdownTimer) {
clearInterval(this.countdownTimer)
this.countdownTimer = null
}
},
handleError () {
this.videoError = true
this.canPlay = false
},
handleNext () {
if (this.canNext) {
this.$emit('changeVideo', this.currentVideoIndex, 'next');
}
},
handleReplay () {
if (this.countdownTimer) {
clearInterval(this.countdownTimer);
this.countdownTimer = null;
}
this.$refs.video.seek(0)
this.isEnded = false
this.$refs.video.play()
},
changeSrc () {
this.getPlayAuth()
},
handleFullscreenChange (data) {
console.log('全屏状态改变:' + JSON.stringify(data))
this.$emit('fullScreenChange', data.fullscreen)
},
handleLoaded (e) {
console.log('视频加载状态:'+JSON.stringify(e))
if (e.type == 'init') {
this.loadingText = '媒体文件加载完成,立即播放'
}
},
async getPlayAuth () {
const version = ++this._requestVersion
const videoId = this.currentVideo.id
this.loadingText = '获取播放凭证...'
const http = this.http
if (!http || !http.request) return
let res_check
try {
res_check = await http.request({
url: `sociology/course/checkVideo`,
method: "Post",
data: { id: videoId },
header: { "Content-Type": "application/json" },
})
} catch (e) {
if (version !== this._requestVersion) return
this.videoError = true
this.videoErrorMsg = '获取播放凭证失败'
return
}
if (version !== this._requestVersion) return
this.loadingText = '验证媒体文件格式...'
console.log("checkVideo接口返回视频信息" + JSON.stringify(res_check.video));
if (this.$platform == 'ios') {
if (res_check.video.type == 1 && (res_check.video.m3u8Url == null || res_check.video.m3u8Url == '')) {
await this.recordErrorVideo(res_check.video, http)
this.videoErrorMsg = '抱歉,苹果手机不支持此加密视频格式,您可以在安卓端观看本视频'
return
}
}
if (version !== this._requestVersion) return
this.loadingText = '媒体文件加载中...'
const options = {
...res_check.video,
videoId: res_check.video.video,
}
this.handlePlayAuth(options)
},
handlePlayAuth (options) {
if (this.currentVideo.type == 1) {
this.src = options.m3u8Url
} else {
this.src = options.videoUrl
}
const localPosition = JSON.parse(uni.getStorageSync('videoPosition') || '{"order":[], "position":{}}')
this.initialTime = localPosition.position[this.currentVideo.id] !== undefined
? localPosition.position[this.currentVideo.id]
: (options.userCourseVideoPositionEntity ? options.userCourseVideoPositionEntity.position || 0 : 0)
},
recordErrorVideo (video, http) {
if (!http || !http.request) return
http.request({
url: "medical/course/addErrorCourse",
method: "POST",
data: {
"chapterId": video.chapterId,
"videoId": video.id,
"sort": video.sort
},
header: { 'Content-Type': 'application/json' },
}).then(() => {
console.log('记录IOS不能看的视频成功 ' + JSON.stringify(video))
}).catch(() => { console.log('数据报错') })
},
}
}
</script>
<style lang="scss">
::v-deep .fast-progress-btn-box {
position: absolute;
top: 50%;
transform: translateY(-50%);
background-color: rgba(0, 0, 0, 0.3);
border-radius: 14rpx;
padding: 2px;
display: flex;
justify-content: center;
align-items: center;
.yb-player-btn {
color: #fff;
font-size: 28rpx;
line-height: 1;
padding: 10rpx 20rpx;
border-radius: 10rpx;
border: 1px solid #fff;
}
}
::v-deep .video-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 1);
color: #fff;
z-index: 2;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 20rpx;
.video-control-btn-group {
display: flex;
justify-content: center;
align-items: center;
gap: 20rpx;
}
.video-control-btn {
line-height: 1;
padding: 5px 10px;
border-radius: 5px;
border: 1px solid #fff;
}
}
</style>