467 lines
13 KiB
Vue
467 lines
13 KiB
Vue
<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>
|