更新:课程视频播放改成原生video组件
This commit is contained in:
73
components/video-player/composables/useVideoAPI.ts
Normal file
73
components/video-player/composables/useVideoAPI.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
// components/video-player/composables/useVideoAPI.ts
|
||||
import { ref } from 'vue'
|
||||
import { videoApi } from '@/api/modules/video'
|
||||
import type { IVideoInfo, VideoErrorType } from '@/types/video'
|
||||
|
||||
/**
|
||||
* 视频 API 调用管理
|
||||
*/
|
||||
export function useVideoAPI() {
|
||||
const isLoading = ref(false)
|
||||
const error = ref<{ type: VideoErrorType; message: string } | null>(null)
|
||||
|
||||
/**
|
||||
* 获取视频播放信息
|
||||
*/
|
||||
const fetchVideoInfo = async (params: {
|
||||
id: number
|
||||
}): Promise<IVideoInfo | null> => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await videoApi.checkVideo(params)
|
||||
|
||||
if (response.code === 0 && response.video) {
|
||||
console.log('Video info fetched:', response.video)
|
||||
return response.video
|
||||
} else {
|
||||
throw new Error(response.msg || '获取视频信息失败')
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Failed to fetch video info:', err)
|
||||
error.value = {
|
||||
type: 'API_ERROR',
|
||||
message: err.message || '获取视频信息失败,请稍后重试'
|
||||
}
|
||||
return null
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 报告错误视频
|
||||
*/
|
||||
const reportErrorVideo = async (params: {
|
||||
chapterId: number
|
||||
videoId: number
|
||||
sort: number
|
||||
}): Promise<void> => {
|
||||
try {
|
||||
await videoApi.addErrorCourse(params)
|
||||
console.log('Error video reported:', params)
|
||||
} catch (err) {
|
||||
console.error('Failed to report error video:', err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除错误
|
||||
*/
|
||||
const clearError = () => {
|
||||
error.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
fetchVideoInfo,
|
||||
reportErrorVideo,
|
||||
clearError
|
||||
}
|
||||
}
|
||||
189
components/video-player/composables/useVideoPlayer.ts
Normal file
189
components/video-player/composables/useVideoPlayer.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
// components/video-player/composables/useVideoPlayer.ts
|
||||
import { ref, computed } from 'vue'
|
||||
import { useVideoAPI } from './useVideoAPI'
|
||||
import { useVideoProgress } from './useVideoProgress'
|
||||
import type { IVideoInfo, IVideoPlayerProps, VideoErrorType } from '@/types/video'
|
||||
|
||||
/**
|
||||
* 视频播放器核心逻辑
|
||||
*/
|
||||
export function useVideoPlayer(props: IVideoPlayerProps) {
|
||||
const videoAPI = useVideoAPI()
|
||||
const videoProgress = useVideoProgress()
|
||||
|
||||
// 状态
|
||||
const currentVideoData = ref<IVideoInfo | null>(null)
|
||||
const currentIndex = ref(props.currentIndex)
|
||||
const isSetFirstTime = ref(false)
|
||||
const isChanging = ref(false)
|
||||
const showError = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const canRetry = ref(false)
|
||||
const platform = ref('')
|
||||
|
||||
// 获取平台信息
|
||||
// #ifdef APP-PLUS
|
||||
platform.value = uni.getSystemInfoSync().platform
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
platform.value = 'h5'
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* 计算视频 URL
|
||||
*/
|
||||
const videoUrl = computed(() => {
|
||||
if (!currentVideoData.value) return ''
|
||||
|
||||
if (currentVideoData.value.type === 0) {
|
||||
// MP4 视频
|
||||
return currentVideoData.value.mp4Url || currentVideoData.value.videoUrl || ''
|
||||
} else {
|
||||
// M3U8 视频
|
||||
return currentVideoData.value.m3u8Url || ''
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 计算封面图 URL
|
||||
*/
|
||||
const posterUrl = computed(() => {
|
||||
if (!currentVideoData.value) return ''
|
||||
|
||||
// 如果是 MP4 视频,可以使用 OSS 视频截图功能
|
||||
if (currentVideoData.value.type === 0 && currentVideoData.value.mp4Url) {
|
||||
return `${currentVideoData.value.mp4Url}?x-oss-process=video/snapshot,t_1,f_jpg`
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
|
||||
/**
|
||||
* 检查视频是否可以播放
|
||||
*/
|
||||
const canPlayVideo = (videoInfo: IVideoInfo): boolean => {
|
||||
// iOS 平台检查
|
||||
if (platform.value === 'ios') {
|
||||
if (videoInfo.type === 1 && !videoInfo.m3u8Url) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载视频
|
||||
*/
|
||||
const loadVideo = async (index: number) => {
|
||||
if (index < 0 || index >= props.videoList.length) {
|
||||
showError.value = true
|
||||
errorMessage.value = '视频索引超出范围'
|
||||
canRetry.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const videoItem = props.videoList[index]
|
||||
currentIndex.value = index
|
||||
|
||||
// 重置状态
|
||||
isSetFirstTime.value = false
|
||||
showError.value = false
|
||||
errorMessage.value = ''
|
||||
canRetry.value = false
|
||||
|
||||
// 调用 API 获取视频信息
|
||||
const videoInfo = await videoAPI.fetchVideoInfo({
|
||||
id: videoItem.id
|
||||
})
|
||||
|
||||
if (!videoInfo) {
|
||||
showError.value = true
|
||||
errorMessage.value = videoAPI.error.value?.message || '获取视频信息失败'
|
||||
canRetry.value = true
|
||||
return
|
||||
}
|
||||
|
||||
// 检查视频类型和 URL
|
||||
if (videoInfo.type === 1 && !videoInfo.m3u8Url) {
|
||||
// M3U8 视频但没有 URL,报告错误
|
||||
await videoAPI.reportErrorVideo({
|
||||
chapterId: videoInfo.chapterId,
|
||||
videoId: videoInfo.id,
|
||||
sort: videoInfo.sort
|
||||
})
|
||||
|
||||
showError.value = true
|
||||
errorMessage.value = '抱歉,本视频加密信息错误,已经提交错误信息,请过一段时间再来观看或联系客服'
|
||||
canRetry.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 设置当前视频数据
|
||||
currentVideoData.value = videoInfo
|
||||
|
||||
// 获取初始播放位置
|
||||
const initialPosition = videoProgress.getInitialPosition(videoInfo)
|
||||
videoProgress.updateCurrentTime(initialPosition)
|
||||
|
||||
return videoInfo
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换视频
|
||||
*/
|
||||
const changeVideo = async (newIndex: number) => {
|
||||
if (isChanging.value) return
|
||||
|
||||
isChanging.value = true
|
||||
|
||||
try {
|
||||
// 保存当前视频进度
|
||||
if (currentVideoData.value) {
|
||||
await videoProgress.saveNow(currentVideoData.value)
|
||||
}
|
||||
|
||||
// 停止进度保存
|
||||
videoProgress.stopSaving()
|
||||
|
||||
// 加载新视频
|
||||
await loadVideo(newIndex)
|
||||
} finally {
|
||||
isChanging.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试加载视频
|
||||
*/
|
||||
const retry = () => {
|
||||
loadVideo(currentIndex.value)
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
currentVideoData,
|
||||
currentIndex,
|
||||
isSetFirstTime,
|
||||
isChanging,
|
||||
showError,
|
||||
errorMessage,
|
||||
canRetry,
|
||||
platform,
|
||||
isLoading: videoAPI.isLoading,
|
||||
|
||||
// 计算属性
|
||||
videoUrl,
|
||||
posterUrl,
|
||||
|
||||
// 方法
|
||||
loadVideo,
|
||||
changeVideo,
|
||||
retry,
|
||||
canPlayVideo,
|
||||
|
||||
// 进度管理
|
||||
videoProgress
|
||||
}
|
||||
}
|
||||
131
components/video-player/composables/useVideoProgress.ts
Normal file
131
components/video-player/composables/useVideoProgress.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
// components/video-player/composables/useVideoProgress.ts
|
||||
import { ref } from 'vue'
|
||||
import { videoStorage } from '@/utils/videoStorage'
|
||||
import { videoApi } from '@/api/modules/video'
|
||||
import type { IVideoInfo } from '@/types/video'
|
||||
|
||||
/**
|
||||
* 视频播放进度管理
|
||||
*/
|
||||
export function useVideoProgress() {
|
||||
const currentTime = ref(0)
|
||||
const isSaving = ref(false)
|
||||
let saveTimer: any = null
|
||||
|
||||
/**
|
||||
* 获取初始播放位置
|
||||
* 合并本地和服务器的播放位置,取较大值
|
||||
* 如果视频已经看完(播放位置 >= 总时长 - 5秒),则从头开始
|
||||
*/
|
||||
const getInitialPosition = (videoInfo: IVideoInfo): number => {
|
||||
// 从服务器获取播放位置
|
||||
const serverPosition = videoInfo.userCourseVideoPositionEntity?.position || 0
|
||||
|
||||
// 从本地存储获取播放位置
|
||||
const localPosition = videoStorage.getVideoPosition(videoInfo.id) || 0
|
||||
|
||||
// 返回较大的值
|
||||
let position = Math.max(serverPosition, localPosition)
|
||||
|
||||
// 如果播放位置接近视频结尾(最后5秒内),则从头开始
|
||||
const videoDuration = videoInfo.duration || 0
|
||||
if (videoDuration > 0 && position >= videoDuration - 5) {
|
||||
position = 0
|
||||
console.log('Video already finished, reset to start')
|
||||
}
|
||||
|
||||
console.log('Initial position:', {
|
||||
server: serverPosition,
|
||||
local: localPosition,
|
||||
duration: videoDuration,
|
||||
final: position
|
||||
})
|
||||
|
||||
return position
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始保存进度
|
||||
* 每秒保存到本地
|
||||
*/
|
||||
const startSaving = (videoInfo: IVideoInfo) => {
|
||||
// 清除之前的定时器
|
||||
stopSaving()
|
||||
|
||||
// 每秒保存到本地
|
||||
saveTimer = setInterval(() => {
|
||||
if (currentTime.value > 0) {
|
||||
videoStorage.saveVideoPosition(
|
||||
videoInfo.id,
|
||||
Math.floor(currentTime.value),
|
||||
videoInfo
|
||||
)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止保存进度
|
||||
*/
|
||||
const stopSaving = () => {
|
||||
if (saveTimer) {
|
||||
clearInterval(saveTimer)
|
||||
saveTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存进度到服务器
|
||||
*/
|
||||
const saveToServer = async (videoId: number, position: number) => {
|
||||
if (isSaving.value) return
|
||||
|
||||
try {
|
||||
isSaving.value = true
|
||||
await videoApi.saveCoursePosition({
|
||||
videoId,
|
||||
position
|
||||
})
|
||||
console.log('Progress saved to server:', { videoId, position })
|
||||
} catch (error) {
|
||||
console.error('Failed to save progress to server:', error)
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即保存当前进度
|
||||
*/
|
||||
const saveNow = async (videoInfo: IVideoInfo) => {
|
||||
if (currentTime.value > 0) {
|
||||
// 保存到本地
|
||||
videoStorage.saveVideoPosition(
|
||||
videoInfo.id,
|
||||
Math.floor(currentTime.value),
|
||||
videoInfo
|
||||
)
|
||||
|
||||
// 保存到服务器
|
||||
await saveToServer(videoInfo.id, Math.floor(currentTime.value))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前播放时间
|
||||
*/
|
||||
const updateCurrentTime = (time: number) => {
|
||||
currentTime.value = time
|
||||
}
|
||||
|
||||
return {
|
||||
currentTime,
|
||||
isSaving,
|
||||
getInitialPosition,
|
||||
startSaving,
|
||||
stopSaving,
|
||||
saveToServer,
|
||||
saveNow,
|
||||
updateCurrentTime
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user