feat: 更新视频播放器组件,添加章节视频功能并优化返回逻辑

This commit is contained in:
2026-04-09 16:48:57 +08:00
parent 943cc862fc
commit 17022d498a
19 changed files with 1192 additions and 1599 deletions

View File

@@ -1,523 +1,466 @@
<template>
<view class="video-player-container">
<!-- 视频播放器 -->
<view class="video-wrapper">
<video
v-if="!player.showError.value && player.videoUrl.value"
:id="videoId"
:src="player.videoUrl.value"
:poster="player.posterUrl.value"
:controls="true"
:page-gesture="true"
:vslide-gesture="true"
:vslide-gesture-in-fullscreen="true"
:show-center-play-btn="true"
:show-fullscreen-btn="true"
:show-play-btn="true"
:show-progress="true"
:enable-play-gesture="true"
:enable-progress-gesture="true"
:object-fit="objectFit"
:autoplay="true"
:initial-time="initialTime"
class="video-element"
@play="play"
@pause="pause"
@ended="ended"
@timeupdate="timeupdate"
@error="error"
@fullscreenchange="fullscreenChange"
@waiting="waiting"
@loadedmetadata="loadedmetadata"
/>
<!-- 倒计时遮罩 - 使用 cover-view APP 中显示 -->
<cover-view v-if="showCountdown" class="countdown-overlay">
<cover-view class="countdown-content">
<cover-view class="countdown-timer">
<cover-view class="countdown-number">{{ countdownRemaining }}</cover-view>
<cover-view class="countdown-text">秒后播放下一个视频</cover-view>
</cover-view>
<cover-view class="countdown-actions">
<cover-view class="btn-cancel" @click="cancelCountdown">取消</cover-view>
<cover-view class="btn-replay" @click="replayVideo">重新播放</cover-view>
<cover-view class="btn-next" @click="playNext">立即播放</cover-view>
</cover-view>
</cover-view>
</cover-view>
<!-- 错误显示 -->
<view v-if="player.showError.value" class="error-display">
<view class="error-content">
<view class="error-message">{{ player.errorMessage.value }}</view>
<button v-if="player.canRetry.value" class="btn-retry" @click="retry">重试</button>
</view>
</view>
<!-- 加载中 -->
<view v-if="player.isLoading.value" class="loading-display">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
</view>
</view>
<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 setup lang="ts">
import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
import { useVideoPlayer } from './composables/useVideoPlayer'
import { videoStorage } from '@/utils/videoStorage'
import type { IVideoPlayerProps, IVideoInfo } from '@/types/video'
<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 = []
// 页面生命周期
// @ts-ignore
const onHide = typeof uni !== 'undefined' ? uni.onHide || (() => {}) : () => {}
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)
}
// Props
const props = withDefaults(defineProps<IVideoPlayerProps>(), {
countdownSeconds: 5,
showWatermark: false,
watermarkText: '本课程版权归天津众妙之门科技有限公司所有,翻版必究!'
})
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);
// Emits
const emit = defineEmits<{
play: [videoInfo: IVideoInfo]
pause: [position: number]
fullscreen: [isFullscreen: boolean]
error: [error: { type: string; message: string }]
ended: [videoInfo: IVideoInfo]
timeupdate: [data: { currentTime: number; duration: number }]
'video-change': [newIndex: number]
'update:current-index': [newIndex: number]
'countdown-start': []
'countdown-cancel': []
}>()
this._savePositionSnapshot(this.currentVideo.id, parseInt(this.currentTime))
// 使用 composables
const player = useVideoPlayer(props)
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 videoId = 'video-player-' + Date.now()
const videoContext = ref<any>(null)
const duration = ref(0)
const isPlaying = ref(false)
const isFullScreen = ref(false)
const showCountdown = ref(false)
const countdownRemaining = ref(props.countdownSeconds)
const objectFit = ref<'contain' | 'cover'>('contain')
const initialTime = ref(0)
let countdownTimer: any = null
const http = this.http
if (!http || !http.request) return
// 生命周期
onMounted(async () => {
// 初始化视频上下文
videoContext.value = uni.createVideoContext(videoId)
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
}
// 加载视频信息
const videoInfo = await player.loadVideo(props.currentIndex)
if (version !== this._requestVersion) return
if (videoInfo) {
// 设置初始播放位置loadVideo 内部已经调用过 getInitialPosition
// 这里再次获取是为了确保 initialTime 和 video 组件同步
const position = player.videoProgress.getInitialPosition(videoInfo)
initialTime.value = position
console.log('Set initial time:', position, 'for video:', videoInfo.id)
// 如果有历史进度,使用 seek 跳转(因为 initial-time 可能不生效)
if (position > 0) {
setTimeout(() => {
videoContext.value?.seek(position)
console.log('Seek to position:', position)
}, 500) // 延迟 500ms 确保视频已加载
}
// 开始保存进度
// player.videoProgress.startSaving(videoInfo) // 注释掉 不再保存到本地
}
})
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
}
}
// 页面隐藏时保存进度
onHide(() => {
console.log('Page hidden, saving progress...')
if (player.currentVideoData.value && player.videoProgress.currentTime.value > 0) {
// 立即保存到本地 注释掉: 不再保存到本地
// videoStorage.saveVideoPosition(
// player.currentVideoData.value.id,
// Math.floor(player.videoProgress.currentTime.value),
// player.currentVideoData.value
// )
// 保存到服务器
player.videoProgress.saveToServer(
player.currentVideoData.value.id,
Math.floor(player.videoProgress.currentTime.value)
)
}
})
if (version !== this._requestVersion) return
onUnmounted(() => {
console.log('Component unmounting, saving progress...')
// 停止进度保存
player.videoProgress.stopSaving()
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
}
// 立即保存当前播放进度(同步保存到本地,异步保存到服务器)
if (player.currentVideoData.value && player.videoProgress.currentTime.value > 0) {
// 立即保存到本地 注释掉: 不再保存到本地
// videoStorage.saveVideoPosition(
// player.currentVideoData.value.id,
// Math.floor(player.videoProgress.currentTime.value),
// player.currentVideoData.value
// )
// 保存到服务器(不等待完成)
player.videoProgress.saveToServer(
player.currentVideoData.value.id,
Math.floor(player.videoProgress.currentTime.value)
)
}
// 清除倒计时
if (countdownTimer) {
clearInterval(countdownTimer)
}
// 恢复竖屏
// #ifdef APP-PLUS
plus.screen.lockOrientation('portrait-primary')
// #endif
})
// 监听 currentIndex 变化(仅当外部主动修改时才触发)
watch(
() => props.currentIndex,
async (newIndex, oldIndex) => {
// 只有当新值与组件内部维护的索引不同时才处理
// 这样可以避免组件内部切换视频时触发 watch
if (newIndex !== player.currentIndex.value) {
await player.changeVideo(newIndex)
}
}
)
// 事件处理
const play = () => {
isPlaying.value = true
if (player.currentVideoData.value) {
emit('play', player.currentVideoData.value)
}
}
const pause = () => {
isPlaying.value = false
emit('pause', player.videoProgress.currentTime.value)
// 暂停时保存进度
if (player.currentVideoData.value && player.videoProgress.currentTime.value > 0) {
// 保存进度到本地 注释掉: 不再保存到本地
// videoStorage.saveVideoPosition(
// player.currentVideoData.value.id,
// Math.floor(player.videoProgress.currentTime.value),
// player.currentVideoData.value
// )
// 保存进度到服务器
player.videoProgress.saveToServer(
player.currentVideoData.value.id,
Math.floor(player.videoProgress.currentTime.value)
)
}
}
const ended = () => {
if (player.currentVideoData.value) {
emit('ended', player.currentVideoData.value)
// 检查是否有下一个视频
if (player.currentIndex.value < props.videoList.length - 1) {
startCountdown()
}
}
}
const timeupdate = (e: any) => {
const currentTime = e.detail.currentTime
const videoDuration = e.detail.duration
player.videoProgress.updateCurrentTime(currentTime)
duration.value = videoDuration
emit('timeupdate', {
currentTime,
duration: videoDuration
})
}
const error = (e: any) => {
console.error('Video error:', e)
player.showError.value = true
player.errorMessage.value = '视频播放出错,请稍后重试'
player.canRetry.value = true
emit('error', {
type: 'VIDEO_LOAD_ERROR',
message: '视频播放出错'
})
}
const fullscreenChange = (e: any) => {
isFullScreen.value = e.detail.fullScreen
// #ifdef APP-PLUS
if (e.detail.fullScreen) {
// 进入全屏,锁定横屏
setTimeout(() => {
plus.screen.lockOrientation('landscape-primary')
}, 100)
} else {
// 退出全屏,恢复竖屏
setTimeout(() => {
plus.screen.lockOrientation('portrait-primary')
}, 100)
}
// #endif
emit('fullscreen', isFullScreen.value)
}
const waiting = () => {
// 视频缓冲中
console.log('Video waiting...')
}
const loadedmetadata = (e: any) => {
duration.value = e.detail.duration
console.log('Video metadata loaded, duration:', duration.value)
}
// 倒计时相关
const startCountdown = () => {
showCountdown.value = true
countdownRemaining.value = props.countdownSeconds
emit('countdown-start')
countdownTimer = setInterval(() => {
countdownRemaining.value--
if (countdownRemaining.value <= 0) {
clearInterval(countdownTimer)
playNext()
}
}, 1000)
}
const cancelCountdown = () => {
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
showCountdown.value = false
emit('countdown-cancel')
}
const replayVideo = () => {
cancelCountdown()
// 重置播放位置并播放
initialTime.value = 0
player.videoProgress.updateCurrentTime(0)
// 重新加载当前视频
player.loadVideo(player.currentIndex.value)
}
const playNext = async () => {
cancelCountdown()
const nextIndex = player.currentIndex.value + 1
if (nextIndex < props.videoList.length) {
await player.changeVideo(nextIndex)
// 通知父组件更新 currentIndex
emit('update:current-index', nextIndex)
emit('video-change', nextIndex)
// 获取新视频的初始播放位置
if (player.currentVideoData.value) {
initialTime.value = player.videoProgress.getInitialPosition(player.currentVideoData.value)
// player.videoProgress.startSaving(player.currentVideoData.value) // 注释掉 不再保存到本地
}
}
}
const retry = () => {
player.retry()
}
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" scoped>
.video-player-container {
width: 100%;
height: 100%;
position: relative;
background-color: #000;
}
<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;
.video-wrapper {
width: 100%;
height: 400rpx;
position: relative;
}
.video-element {
width: 100%;
height: 100%;
}
.error-display {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.8);
}
.error-content {
text-align: center;
padding: 40rpx;
}
.error-icon {
font-size: 80rpx;
margin-bottom: 20rpx;
}
.error-message {
color: #fff;
font-size: 28rpx;
line-height: 1.6;
margin-bottom: 30rpx;
}
.btn-retry {
display: inline-block;
background-color: #1989fa;
color: #fff;
border: none;
padding: 20rpx 40rpx;
line-height: 1;
border-radius: 8rpx;
font-size: 28rpx;
}
.loading-display {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.5);
}
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid rgba(255, 255, 255, 0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
.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;
.loading-text {
color: #fff;
font-size: 28rpx;
margin-top: 20rpx;
}
.video-control-btn-group {
display: flex;
justify-content: center;
align-items: center;
gap: 20rpx;
}
.countdown-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
z-index: 9999;
}
.countdown-content {
position: absolute;
top: 50%;
left: 50%;
width: 600rpx;
margin-left: -300rpx;
margin-top: -150rpx;
text-align: center;
}
.countdown-timer {
margin-bottom: 60rpx;
}
.countdown-number {
color: #fff;
font-size: 32px;
font-weight: bold;
line-height: 1.2;
text-align: center;
}
.countdown-text {
color: #fff;
font-size: 16px;
line-height: 50rpx;
text-align: center;
margin-top: 20rpx;
}
.countdown-actions {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 100%;
}
.btn-cancel,
.btn-replay,
.btn-next {
padding: 0 20px;
text-align: center;
border-radius: 4px;
font-size: 14px;
height: 26px;
line-height: 26px;
color: #fff;
margin: 0 5px;
background-color: #666;
}
// .btn-cancel {
// background-color: #666;
// }
// .btn-replay {
// background-color: #ff9800;
// }
// .btn-next {
// background-color: #1989fa;
// }
.video-control-btn {
line-height: 1;
padding: 5px 10px;
border-radius: 5px;
border: 1px solid #fff;
}
}
</style>