fix(video): 扩展原生播放器到 Android 微信环境
- 修复 Android 微信用户无法观看视频的问题 - Video.js 在微信 X5 内核下存在兼容性问题 - 移动端微信环境统一使用原生 <video> 播放器 - 权衡:移动端微信用户失去清晰度切换功能 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Showing
1 changed file
with
274 additions
and
272 deletions
| 1 | -import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'; | 1 | +import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue' |
| 2 | -import { wxInfo } from "@/utils/tools"; | 2 | +import { wxInfo } from '@/utils/tools' |
| 3 | -import { buildVideoSources, canPlayHlsNatively } from "./videoPlayerSource"; | 3 | +import { buildVideoSources, canPlayHlsNatively } from './videoPlayerSource' |
| 4 | -import { useVideoProbe } from "./useVideoProbe"; | 4 | +import { useVideoProbe } from './useVideoProbe' |
| 5 | -import { useVideoPlaybackOverlays } from "./useVideoPlaybackOverlays"; | 5 | +import { useVideoPlaybackOverlays } from './useVideoPlaybackOverlays' |
| 6 | 6 | ||
| 7 | const is_safari_browser = () => { | 7 | const is_safari_browser = () => { |
| 8 | - if (typeof navigator === 'undefined') return false; | 8 | + if (typeof navigator === 'undefined') return false |
| 9 | - const ua = navigator.userAgent || ''; | 9 | + const ua = navigator.userAgent || '' |
| 10 | - const is_safari = /safari/i.test(ua) && !/chrome|crios|android|fxios|edg/i.test(ua); | 10 | + const is_safari = /safari/i.test(ua) && !/chrome|crios|android|fxios|edg/i.test(ua) |
| 11 | - return is_safari; | 11 | + return is_safari |
| 12 | -}; | 12 | +} |
| 13 | 13 | ||
| 14 | /** | 14 | /** |
| 15 | * - 使用方法 :您无需修改业务代码。只要传入的视频 URL 是七牛云生成的多码率 .m3u8 地址,播放器控制条右下角会自动出现“齿轮”图标,用户点击即可切换清晰度(或选择 Auto 自动切换)。 | 15 | * - 使用方法 :您无需修改业务代码。只要传入的视频 URL 是七牛云生成的多码率 .m3u8 地址,播放器控制条右下角会自动出现“齿轮”图标,用户点击即可切换清晰度(或选择 Auto 自动切换)。 |
| ... | @@ -44,54 +44,56 @@ const is_safari_browser = () => { | ... | @@ -44,54 +44,56 @@ const is_safari_browser = () => { |
| 44 | */ | 44 | */ |
| 45 | export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { | 45 | export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { |
| 46 | // 播放器实例 | 46 | // 播放器实例 |
| 47 | - const player = ref(null); | 47 | + const player = ref(null) |
| 48 | - const state = ref(null); | 48 | + const state = ref(null) |
| 49 | 49 | ||
| 50 | // 错误处理相关 | 50 | // 错误处理相关 |
| 51 | - const showErrorOverlay = ref(false); | 51 | + const showErrorOverlay = ref(false) |
| 52 | - const errorMessage = ref(''); | 52 | + const errorMessage = ref('') |
| 53 | - const retryCount = ref(0); | 53 | + const retryCount = ref(0) |
| 54 | - const maxRetries = 3; | 54 | + const maxRetries = 3 |
| 55 | - const canRetry = computed(() => retryCount.value < maxRetries); | 55 | + const canRetry = computed(() => retryCount.value < maxRetries) |
| 56 | 56 | ||
| 57 | - const hasEverPlayed = ref(false); | 57 | + const hasEverPlayed = ref(false) |
| 58 | - const hasStartedPlayback = ref(false); | 58 | + const hasStartedPlayback = ref(false) |
| 59 | 59 | ||
| 60 | // 原生播放器状态 | 60 | // 原生播放器状态 |
| 61 | - const nativeReady = ref(false); | 61 | + const nativeReady = ref(false) |
| 62 | - let nativeListeners = null; | 62 | + let nativeListeners = null |
| 63 | - let retry_error_check_timer = null; | 63 | + let retry_error_check_timer = null |
| 64 | 64 | ||
| 65 | // 1. 环境判断与播放器选择 | 65 | // 1. 环境判断与播放器选择 |
| 66 | const useNativePlayer = computed(() => { | 66 | const useNativePlayer = computed(() => { |
| 67 | // 如果 props 强制关闭原生播放器,则返回 false (使用 Video.js) | 67 | // 如果 props 强制关闭原生播放器,则返回 false (使用 Video.js) |
| 68 | if (props.useNativeOnIos === false) { | 68 | if (props.useNativeOnIos === false) { |
| 69 | - return false; | 69 | + return false |
| 70 | } | 70 | } |
| 71 | - // 默认逻辑:iOS 微信环境下使用原生播放器 | 71 | + // 扩展逻辑:iOS 微信 + Android 微信都使用原生播放器 |
| 72 | - return wxInfo().isIOSWeChat; | 72 | + // 理由:微信 X5 内核对原生 <video> 支持最好,避开 Video.js 在 X5 下的兼容性问题 |
| 73 | - }); | 73 | + const info = wxInfo() |
| 74 | + return (info.isIOS && info.isWeiXin) || (info.isAndroid && info.isWeiXin) | ||
| 75 | + }) | ||
| 74 | 76 | ||
| 75 | // 2. 视频源处理 | 77 | // 2. 视频源处理 |
| 76 | - const videoUrlValue = computed(() => { | 78 | + const videoUrlValue = computed(() => (props.videoUrl || '').trim()) |
| 77 | - return (props.videoUrl || "").trim(); | ||
| 78 | - }); | ||
| 79 | 79 | ||
| 80 | // 3. HLS 支持判断 | 80 | // 3. HLS 支持判断 |
| 81 | const isM3U8 = computed(() => { | 81 | const isM3U8 = computed(() => { |
| 82 | - const url = videoUrlValue.value.toLowerCase(); | 82 | + const url = videoUrlValue.value.toLowerCase() |
| 83 | - return url.includes('.m3u8'); | 83 | + return url.includes('.m3u8') |
| 84 | - }); | 84 | + }) |
| 85 | 85 | ||
| 86 | // 资源探测:只在“同源可探测”时执行,避免跨域 CORS 报错影响体验 | 86 | // 资源探测:只在“同源可探测”时执行,避免跨域 CORS 报错影响体验 |
| 87 | - const { probeInfo, probeVideo } = useVideoProbe(videoUrlValue); | 87 | + const { probeInfo, probeVideo } = useVideoProbe(videoUrlValue) |
| 88 | 88 | ||
| 89 | // 视频源构造:尽可能带上 type,老设备/部分内核对 blob/部分后缀会更稳定 | 89 | // 视频源构造:尽可能带上 type,老设备/部分内核对 blob/部分后缀会更稳定 |
| 90 | - const videoSources = computed(() => buildVideoSources({ | 90 | + const videoSources = computed(() => |
| 91 | - url: videoUrlValue.value, | 91 | + buildVideoSources({ |
| 92 | - video_id: props?.videoId, | 92 | + url: videoUrlValue.value, |
| 93 | - probe_content_type: probeInfo.value.content_type, | 93 | + video_id: props?.videoId, |
| 94 | - })); | 94 | + probe_content_type: probeInfo.value.content_type, |
| 95 | + }) | ||
| 96 | + ) | ||
| 95 | 97 | ||
| 96 | // 播放叠层:弱网提示 + HLS 速度展示(仅 video.js + m3u8) | 98 | // 播放叠层:弱网提示 + HLS 速度展示(仅 video.js + m3u8) |
| 97 | const { | 99 | const { |
| ... | @@ -112,122 +114,123 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { | ... | @@ -112,122 +114,123 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { |
| 112 | use_native_player: useNativePlayer, | 114 | use_native_player: useNativePlayer, |
| 113 | show_error_overlay: showErrorOverlay, | 115 | show_error_overlay: showErrorOverlay, |
| 114 | has_started_playback: hasStartedPlayback, | 116 | has_started_playback: hasStartedPlayback, |
| 115 | - }); | 117 | + }) |
| 116 | 118 | ||
| 117 | // 6. 错误处理逻辑 | 119 | // 6. 错误处理逻辑 |
| 118 | - const formatBytes = (bytes) => { | 120 | + const formatBytes = bytes => { |
| 119 | - const size = Number(bytes) || 0; | 121 | + const size = Number(bytes) || 0 |
| 120 | - if (!size) return ""; | 122 | + if (!size) return '' |
| 121 | - const kb = 1024; | 123 | + const kb = 1024 |
| 122 | - const mb = kb * 1024; | 124 | + const mb = kb * 1024 |
| 123 | - const gb = mb * 1024; | 125 | + const gb = mb * 1024 |
| 124 | - if (size >= gb) return (size / gb).toFixed(2) + "GB"; | 126 | + if (size >= gb) return `${(size / gb).toFixed(2)}GB` |
| 125 | - if (size >= mb) return (size / mb).toFixed(2) + "MB"; | 127 | + if (size >= mb) return `${(size / mb).toFixed(2)}MB` |
| 126 | - if (size >= kb) return (size / kb).toFixed(2) + "KB"; | 128 | + if (size >= kb) return `${(size / kb).toFixed(2)}KB` |
| 127 | - return String(size) + "B"; | 129 | + return `${String(size)}B` |
| 128 | - }; | 130 | + } |
| 129 | 131 | ||
| 130 | const getErrorHint = () => { | 132 | const getErrorHint = () => { |
| 131 | - if (probeInfo.value.status === 403) return "(403:无权限或已过期)"; | 133 | + if (probeInfo.value.status === 403) return '(403:无权限或已过期)' |
| 132 | - if (probeInfo.value.status === 404) return "(404:资源不存在)"; | 134 | + if (probeInfo.value.status === 404) return '(404:资源不存在)' |
| 133 | - if (probeInfo.value.status && probeInfo.value.status >= 500) return `(${probeInfo.value.status}:服务器异常)`; | 135 | + if (probeInfo.value.status && probeInfo.value.status >= 500) |
| 136 | + return `(${probeInfo.value.status}:服务器异常)` | ||
| 134 | 137 | ||
| 135 | - const len = probeInfo.value.content_length; | 138 | + const len = probeInfo.value.content_length |
| 136 | if (len && len >= 1024 * 1024 * 1024) { | 139 | if (len && len >= 1024 * 1024 * 1024) { |
| 137 | - const text = formatBytes(len); | 140 | + const text = formatBytes(len) |
| 138 | - return text ? `(文件约${text},建议 WiFi)` : "(文件较大,建议 WiFi)"; | 141 | + return text ? `(文件约${text},建议 WiFi)` : '(文件较大,建议 WiFi)' |
| 139 | } | 142 | } |
| 140 | - return ""; | 143 | + return '' |
| 141 | - }; | 144 | + } |
| 142 | 145 | ||
| 143 | // 7. 错误处理逻辑 | 146 | // 7. 错误处理逻辑 |
| 144 | const handleError = (code, message = '') => { | 147 | const handleError = (code, message = '') => { |
| 145 | - showErrorOverlay.value = true; | 148 | + showErrorOverlay.value = true |
| 146 | - hideNetworkSpeed(); | 149 | + hideNetworkSpeed() |
| 147 | switch (code) { | 150 | switch (code) { |
| 148 | case 4: // MEDIA_ERR_SRC_NOT_SUPPORTED | 151 | case 4: // MEDIA_ERR_SRC_NOT_SUPPORTED |
| 149 | - errorMessage.value = '视频格式不支持或无法加载,请检查网络连接' + getErrorHint(); | 152 | + errorMessage.value = `视频格式不支持或无法加载,请检查网络连接${getErrorHint()}` |
| 150 | // 旧机型/弱网下可能出现短暂的“无法加载”,这里做有限次数重试 | 153 | // 旧机型/弱网下可能出现短暂的“无法加载”,这里做有限次数重试 |
| 151 | // if (retryCount.value < maxRetries) { | 154 | // if (retryCount.value < maxRetries) { |
| 152 | // setTimeout(retryLoad, 1000); | 155 | // setTimeout(retryLoad, 1000); |
| 153 | // } | 156 | // } |
| 154 | - break; | 157 | + break |
| 155 | case 3: // MEDIA_ERR_DECODE | 158 | case 3: // MEDIA_ERR_DECODE |
| 156 | - errorMessage.value = '视频解码失败,可能是文件损坏'; | 159 | + errorMessage.value = '视频解码失败,可能是文件损坏' |
| 157 | - break; | 160 | + break |
| 158 | case 2: // MEDIA_ERR_NETWORK | 161 | case 2: // MEDIA_ERR_NETWORK |
| 159 | - errorMessage.value = '网络连接错误,请检查网络后重试' + getErrorHint(); | 162 | + errorMessage.value = `网络连接错误,请检查网络后重试${getErrorHint()}` |
| 160 | if (retryCount.value < maxRetries) { | 163 | if (retryCount.value < maxRetries) { |
| 161 | - setTimeout(retryLoad, 2000); | 164 | + setTimeout(retryLoad, 2000) |
| 162 | } | 165 | } |
| 163 | - break; | 166 | + break |
| 164 | case 1: // MEDIA_ERR_ABORTED | 167 | case 1: // MEDIA_ERR_ABORTED |
| 165 | - errorMessage.value = '视频加载被中止'; | 168 | + errorMessage.value = '视频加载被中止' |
| 166 | - break; | 169 | + break |
| 167 | default: | 170 | default: |
| 168 | - errorMessage.value = message || '视频播放出现未知错误'; | 171 | + errorMessage.value = message || '视频播放出现未知错误' |
| 169 | } | 172 | } |
| 170 | - }; | 173 | + } |
| 171 | 174 | ||
| 172 | // 4. 原生播放器逻辑 (iOS微信) | 175 | // 4. 原生播放器逻辑 (iOS微信) |
| 173 | const initNativePlayer = () => { | 176 | const initNativePlayer = () => { |
| 174 | - const videoEl = nativeVideoRef.value; | 177 | + const videoEl = nativeVideoRef.value |
| 175 | - if (!videoEl) return; | 178 | + if (!videoEl) return |
| 176 | 179 | ||
| 177 | - setHlsDebug('native:init'); | 180 | + setHlsDebug('native:init') |
| 178 | 181 | ||
| 179 | // 原生播放器走系统内核:事件主要用于控制弱网提示与错误覆盖层 | 182 | // 原生播放器走系统内核:事件主要用于控制弱网提示与错误覆盖层 |
| 180 | const onLoadStart = () => { | 183 | const onLoadStart = () => { |
| 181 | - showErrorOverlay.value = false; | 184 | + showErrorOverlay.value = false |
| 182 | - nativeReady.value = false; | 185 | + nativeReady.value = false |
| 183 | - }; | 186 | + } |
| 184 | 187 | ||
| 185 | const onCanPlay = () => { | 188 | const onCanPlay = () => { |
| 186 | - showErrorOverlay.value = false; | 189 | + showErrorOverlay.value = false |
| 187 | - retryCount.value = 0; | 190 | + retryCount.value = 0 |
| 188 | - nativeReady.value = true; | 191 | + nativeReady.value = true |
| 189 | - }; | 192 | + } |
| 190 | 193 | ||
| 191 | const onError = () => { | 194 | const onError = () => { |
| 192 | - handleError(videoEl.error?.code); | 195 | + handleError(videoEl.error?.code) |
| 193 | - }; | 196 | + } |
| 194 | 197 | ||
| 195 | const onPlay = () => { | 198 | const onPlay = () => { |
| 196 | - hideNetworkSpeed(); | 199 | + hideNetworkSpeed() |
| 197 | - setHlsDebug('native:play'); | 200 | + setHlsDebug('native:play') |
| 198 | - }; | 201 | + } |
| 199 | 202 | ||
| 200 | const onPause = () => { | 203 | const onPause = () => { |
| 201 | - hideNetworkSpeed(); | 204 | + hideNetworkSpeed() |
| 202 | - }; | 205 | + } |
| 203 | 206 | ||
| 204 | const onWaiting = () => { | 207 | const onWaiting = () => { |
| 205 | - if (videoEl.paused) return; | 208 | + if (videoEl.paused) return |
| 206 | - showNetworkSpeed(); | 209 | + showNetworkSpeed() |
| 207 | - setHlsDebug('native:waiting'); | 210 | + setHlsDebug('native:waiting') |
| 208 | - }; | 211 | + } |
| 209 | 212 | ||
| 210 | const onStalled = () => { | 213 | const onStalled = () => { |
| 211 | - if (videoEl.paused) return; | 214 | + if (videoEl.paused) return |
| 212 | - showNetworkSpeed(); | 215 | + showNetworkSpeed() |
| 213 | - setHlsDebug('native:stalled'); | 216 | + setHlsDebug('native:stalled') |
| 214 | - }; | 217 | + } |
| 215 | 218 | ||
| 216 | const onPlaying = () => { | 219 | const onPlaying = () => { |
| 217 | - hasEverPlayed.value = true; | 220 | + hasEverPlayed.value = true |
| 218 | - hasStartedPlayback.value = true; | 221 | + hasStartedPlayback.value = true |
| 219 | - hideNetworkSpeed(); | 222 | + hideNetworkSpeed() |
| 220 | - setHlsDebug('native:playing'); | 223 | + setHlsDebug('native:playing') |
| 221 | - }; | 224 | + } |
| 222 | - | 225 | + |
| 223 | - videoEl.addEventListener("loadstart", onLoadStart); | 226 | + videoEl.addEventListener('loadstart', onLoadStart) |
| 224 | - videoEl.addEventListener("canplay", onCanPlay); | 227 | + videoEl.addEventListener('canplay', onCanPlay) |
| 225 | - videoEl.addEventListener("error", onError); | 228 | + videoEl.addEventListener('error', onError) |
| 226 | - videoEl.addEventListener("play", onPlay); | 229 | + videoEl.addEventListener('play', onPlay) |
| 227 | - videoEl.addEventListener("pause", onPause); | 230 | + videoEl.addEventListener('pause', onPause) |
| 228 | - videoEl.addEventListener("waiting", onWaiting); | 231 | + videoEl.addEventListener('waiting', onWaiting) |
| 229 | - videoEl.addEventListener("stalled", onStalled); | 232 | + videoEl.addEventListener('stalled', onStalled) |
| 230 | - videoEl.addEventListener("playing", onPlaying); | 233 | + videoEl.addEventListener('playing', onPlaying) |
| 231 | 234 | ||
| 232 | nativeListeners = { | 235 | nativeListeners = { |
| 233 | videoEl, | 236 | videoEl, |
| ... | @@ -239,49 +242,45 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { | ... | @@ -239,49 +242,45 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { |
| 239 | onWaiting, | 242 | onWaiting, |
| 240 | onStalled, | 243 | onStalled, |
| 241 | onPlaying, | 244 | onPlaying, |
| 242 | - }; | 245 | + } |
| 243 | 246 | ||
| 244 | if (props.autoplay) { | 247 | if (props.autoplay) { |
| 245 | // iOS 微信 autoplay 需要用户手势/桥接事件配合,先尝试一次,再在 WeixinJSBridgeReady 时再试 | 248 | // iOS 微信 autoplay 需要用户手势/桥接事件配合,先尝试一次,再在 WeixinJSBridgeReady 时再试 |
| 246 | - tryNativePlay(); | 249 | + tryNativePlay() |
| 247 | - if (typeof document !== "undefined") { | 250 | + if (typeof document !== 'undefined') { |
| 248 | - document.addEventListener( | 251 | + document.addEventListener('WeixinJSBridgeReady', () => tryNativePlay(), { once: true }) |
| 249 | - "WeixinJSBridgeReady", | ||
| 250 | - () => tryNativePlay(), | ||
| 251 | - { once: true } | ||
| 252 | - ); | ||
| 253 | } | 252 | } |
| 254 | } | 253 | } |
| 255 | - }; | 254 | + } |
| 256 | 255 | ||
| 257 | const tryNativePlay = () => { | 256 | const tryNativePlay = () => { |
| 258 | - const videoEl = nativeVideoRef.value; | 257 | + const videoEl = nativeVideoRef.value |
| 259 | - if (!videoEl) return; | 258 | + if (!videoEl) return |
| 260 | 259 | ||
| 261 | - const playPromise = videoEl.play(); | 260 | + const playPromise = videoEl.play() |
| 262 | - if (playPromise && typeof playPromise.catch === "function") { | 261 | + if (playPromise && typeof playPromise.catch === 'function') { |
| 263 | playPromise.catch(() => { | 262 | playPromise.catch(() => { |
| 264 | - if (typeof window !== "undefined" && window.WeixinJSBridge) { | 263 | + if (typeof window !== 'undefined' && window.WeixinJSBridge) { |
| 265 | - window.WeixinJSBridge.invoke("getNetworkType", {}, () => { | 264 | + window.WeixinJSBridge.invoke('getNetworkType', {}, () => { |
| 266 | - videoEl.play().catch(() => {}); | 265 | + videoEl.play().catch(() => {}) |
| 267 | - }); | 266 | + }) |
| 268 | } | 267 | } |
| 269 | - }); | 268 | + }) |
| 270 | } | 269 | } |
| 271 | - }; | 270 | + } |
| 272 | 271 | ||
| 273 | // 5. Video.js 播放器逻辑 (PC/Android) | 272 | // 5. Video.js 播放器逻辑 (PC/Android) |
| 274 | const shouldOverrideNativeHls = computed(() => { | 273 | const shouldOverrideNativeHls = computed(() => { |
| 275 | - if (!isM3U8.value) return false; | 274 | + if (!isM3U8.value) return false |
| 276 | - if (is_safari_browser()) return false; | 275 | + if (is_safari_browser()) return false |
| 277 | // 非 Safari 且不具备原生 HLS 时,强制 video.js 的 VHS 来解 m3u8 | 276 | // 非 Safari 且不具备原生 HLS 时,强制 video.js 的 VHS 来解 m3u8 |
| 278 | - return !canPlayHlsNatively(); | 277 | + return !canPlayHlsNatively() |
| 279 | - }); | 278 | + }) |
| 280 | 279 | ||
| 281 | const videoOptions = computed(() => { | 280 | const videoOptions = computed(() => { |
| 282 | const base = { | 281 | const base = { |
| 283 | controls: true, | 282 | controls: true, |
| 284 | - preload: "metadata", | 283 | + preload: 'metadata', |
| 285 | responsive: true, | 284 | responsive: true, |
| 286 | autoplay: props.autoplay, | 285 | autoplay: props.autoplay, |
| 287 | playsinline: true, | 286 | playsinline: true, |
| ... | @@ -295,8 +294,8 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { | ... | @@ -295,8 +294,8 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { |
| 295 | nativeAudioTracks: false, | 294 | nativeAudioTracks: false, |
| 296 | nativeTextTracks: false, | 295 | nativeTextTracks: false, |
| 297 | hls: { | 296 | hls: { |
| 298 | - withCredentials: false | 297 | + withCredentials: false, |
| 299 | - } | 298 | + }, |
| 300 | }, | 299 | }, |
| 301 | techOrder: ['html5'], | 300 | techOrder: ['html5'], |
| 302 | userActions: { | 301 | userActions: { |
| ... | @@ -314,246 +313,249 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { | ... | @@ -314,246 +313,249 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { |
| 314 | }, | 313 | }, |
| 315 | ...props.options, | 314 | ...props.options, |
| 316 | errorDisplay: false, | 315 | errorDisplay: false, |
| 317 | - }; | 316 | + } |
| 318 | if (!base.poster) { | 317 | if (!base.poster) { |
| 319 | - delete base.poster; | 318 | + delete base.poster |
| 320 | } | 319 | } |
| 321 | - return base; | 320 | + return base |
| 322 | - }); | 321 | + }) |
| 323 | 322 | ||
| 324 | // 8. Video.js 挂载处理 | 323 | // 8. Video.js 挂载处理 |
| 325 | - const handleVideoJsMounted = (payload) => { | 324 | + const handleVideoJsMounted = payload => { |
| 326 | - state.value = payload.state; | 325 | + state.value = payload.state |
| 327 | - player.value = payload.player; | 326 | + player.value = payload.player |
| 328 | 327 | ||
| 329 | if (player.value) { | 328 | if (player.value) { |
| 330 | - setHlsDebug('mounted'); | 329 | + setHlsDebug('mounted') |
| 331 | 330 | ||
| 332 | - const quality_selector_inited = { value: false }; | 331 | + const quality_selector_inited = { value: false } |
| 333 | const setupQualitySelector = () => { | 332 | const setupQualitySelector = () => { |
| 334 | - if (quality_selector_inited.value) return; | 333 | + if (quality_selector_inited.value) return |
| 335 | - if (!isM3U8.value) return; | 334 | + if (!isM3U8.value) return |
| 336 | - const p = player.value; | 335 | + const p = player.value |
| 337 | - if (!p || (typeof p.isDisposed === "function" && p.isDisposed())) return; | 336 | + if (!p || (typeof p.isDisposed === 'function' && p.isDisposed())) return |
| 338 | - if (typeof p.hlsQualitySelector !== "function") return; | 337 | + if (typeof p.hlsQualitySelector !== 'function') return |
| 339 | - if (typeof p.qualityLevels !== "function") return; | 338 | + if (typeof p.qualityLevels !== 'function') return |
| 340 | - | 339 | + |
| 341 | - let tech = null; | 340 | + let tech = null |
| 342 | try { | 341 | try { |
| 343 | - tech = typeof p.tech === "function" ? p.tech({ IWillNotUseThisInPlugins: true }) : null; | 342 | + tech = typeof p.tech === 'function' ? p.tech({ IWillNotUseThisInPlugins: true }) : null |
| 344 | } catch (e) { | 343 | } catch (e) { |
| 345 | - tech = null; | 344 | + tech = null |
| 346 | } | 345 | } |
| 347 | - if (!tech) return; | 346 | + if (!tech) return |
| 348 | // videojs-hls-quality-selector 旧版本依赖 tech.hls,而 video.js 7 默认是 tech.vhs,这里做兼容别名 | 347 | // videojs-hls-quality-selector 旧版本依赖 tech.hls,而 video.js 7 默认是 tech.vhs,这里做兼容别名 |
| 349 | if (!tech.hls && tech.vhs) { | 348 | if (!tech.hls && tech.vhs) { |
| 350 | try { | 349 | try { |
| 351 | - tech.hls = tech.vhs; | 350 | + tech.hls = tech.vhs |
| 352 | } catch (e) { | 351 | } catch (e) { |
| 353 | - void e; | 352 | + void e |
| 354 | } | 353 | } |
| 355 | } | 354 | } |
| 356 | - if (!tech.hls) return; | 355 | + if (!tech.hls) return |
| 357 | 356 | ||
| 358 | try { | 357 | try { |
| 359 | p.hlsQualitySelector({ | 358 | p.hlsQualitySelector({ |
| 360 | displayCurrentQuality: true, | 359 | displayCurrentQuality: true, |
| 361 | - }); | 360 | + }) |
| 362 | - quality_selector_inited.value = true; | 361 | + quality_selector_inited.value = true |
| 363 | } catch (e) { | 362 | } catch (e) { |
| 364 | - void e; | 363 | + void e |
| 365 | } | 364 | } |
| 366 | - }; | 365 | + } |
| 367 | 366 | ||
| 368 | - setupQualitySelector(); | 367 | + setupQualitySelector() |
| 369 | 368 | ||
| 370 | player.value.on('error', () => { | 369 | player.value.on('error', () => { |
| 371 | - const err = player.value.error(); | 370 | + const err = player.value.error() |
| 372 | - handleError(err?.code, err?.message); | 371 | + handleError(err?.code, err?.message) |
| 373 | - }); | 372 | + }) |
| 374 | 373 | ||
| 375 | player.value.on('loadstart', () => { | 374 | player.value.on('loadstart', () => { |
| 376 | - showErrorOverlay.value = false; | 375 | + showErrorOverlay.value = false |
| 377 | - setupQualitySelector(); | 376 | + setupQualitySelector() |
| 378 | - }); | 377 | + }) |
| 379 | 378 | ||
| 380 | player.value.on('canplay', () => { | 379 | player.value.on('canplay', () => { |
| 381 | - showErrorOverlay.value = false; | 380 | + showErrorOverlay.value = false |
| 382 | - retryCount.value = 0; | 381 | + retryCount.value = 0 |
| 383 | - setupQualitySelector(); | 382 | + setupQualitySelector() |
| 384 | - }); | 383 | + }) |
| 385 | 384 | ||
| 386 | player.value.on('play', () => { | 385 | player.value.on('play', () => { |
| 387 | - hideNetworkSpeed(); | 386 | + hideNetworkSpeed() |
| 388 | - startHlsDownloadSpeed(); | 387 | + startHlsDownloadSpeed() |
| 389 | - setHlsDebug('play'); | 388 | + setHlsDebug('play') |
| 390 | - }); | 389 | + }) |
| 391 | 390 | ||
| 392 | player.value.on('pause', () => { | 391 | player.value.on('pause', () => { |
| 393 | - hideNetworkSpeed(); | 392 | + hideNetworkSpeed() |
| 394 | - stopHlsDownloadSpeed('pause'); | 393 | + stopHlsDownloadSpeed('pause') |
| 395 | - setHlsDebug('pause'); | 394 | + setHlsDebug('pause') |
| 396 | - }); | 395 | + }) |
| 397 | 396 | ||
| 398 | player.value.on('waiting', () => { | 397 | player.value.on('waiting', () => { |
| 399 | - if (!hasEverPlayed.value) return; | 398 | + if (!hasEverPlayed.value) return |
| 400 | - if (player.value?.paused?.()) return; | 399 | + if (player.value?.paused?.()) return |
| 401 | // 已经播放过且当前未暂停,才认为是“卡顿等待”,显示弱网提示 | 400 | // 已经播放过且当前未暂停,才认为是“卡顿等待”,显示弱网提示 |
| 402 | - showNetworkSpeed(); | 401 | + showNetworkSpeed() |
| 403 | - startHlsDownloadSpeed(); | 402 | + startHlsDownloadSpeed() |
| 404 | - setHlsDebug('waiting'); | 403 | + setHlsDebug('waiting') |
| 405 | - }); | 404 | + }) |
| 406 | 405 | ||
| 407 | player.value.on('stalled', () => { | 406 | player.value.on('stalled', () => { |
| 408 | - if (!hasEverPlayed.value) return; | 407 | + if (!hasEverPlayed.value) return |
| 409 | - if (player.value?.paused?.()) return; | 408 | + if (player.value?.paused?.()) return |
| 410 | - showNetworkSpeed(); | 409 | + showNetworkSpeed() |
| 411 | - startHlsDownloadSpeed(); | 410 | + startHlsDownloadSpeed() |
| 412 | - setHlsDebug('stalled'); | 411 | + setHlsDebug('stalled') |
| 413 | - }); | 412 | + }) |
| 414 | 413 | ||
| 415 | player.value.on('playing', () => { | 414 | player.value.on('playing', () => { |
| 416 | - hasEverPlayed.value = true; | 415 | + hasEverPlayed.value = true |
| 417 | - hasStartedPlayback.value = true; | 416 | + hasStartedPlayback.value = true |
| 418 | - hideNetworkSpeed(); | 417 | + hideNetworkSpeed() |
| 419 | - setHlsDebug('playing'); | 418 | + setHlsDebug('playing') |
| 420 | - }); | 419 | + }) |
| 421 | 420 | ||
| 422 | player.value.on('ended', () => { | 421 | player.value.on('ended', () => { |
| 423 | - stopHlsDownloadSpeed('ended'); | 422 | + stopHlsDownloadSpeed('ended') |
| 424 | - setHlsDebug('ended'); | 423 | + setHlsDebug('ended') |
| 425 | - }); | 424 | + }) |
| 426 | 425 | ||
| 427 | if (props.autoplay) { | 426 | if (props.autoplay) { |
| 428 | - player.value.play().catch(() => {}); | 427 | + player.value.play().catch(() => {}) |
| 429 | } | 428 | } |
| 430 | } | 429 | } |
| 431 | - }; | 430 | + } |
| 432 | 431 | ||
| 433 | // 6. 重试逻辑 | 432 | // 6. 重试逻辑 |
| 434 | const retryLoad = () => { | 433 | const retryLoad = () => { |
| 435 | if (!canRetry.value) { | 434 | if (!canRetry.value) { |
| 436 | - showErrorOverlay.value = true; | 435 | + showErrorOverlay.value = true |
| 437 | - return; | 436 | + return |
| 438 | } | 437 | } |
| 439 | 438 | ||
| 440 | - retryCount.value++; | 439 | + retryCount.value++ |
| 441 | - showErrorOverlay.value = false; | 440 | + showErrorOverlay.value = false |
| 442 | - hideNetworkSpeed(); | 441 | + hideNetworkSpeed() |
| 443 | - stopHlsDownloadSpeed('retry'); | 442 | + stopHlsDownloadSpeed('retry') |
| 444 | 443 | ||
| 445 | if (retry_error_check_timer) { | 444 | if (retry_error_check_timer) { |
| 446 | - clearTimeout(retry_error_check_timer); | 445 | + clearTimeout(retry_error_check_timer) |
| 447 | - retry_error_check_timer = null; | 446 | + retry_error_check_timer = null |
| 448 | } | 447 | } |
| 449 | 448 | ||
| 450 | if (useNativePlayer.value) { | 449 | if (useNativePlayer.value) { |
| 451 | // 原生 video 需要手动重置 src/load | 450 | // 原生 video 需要手动重置 src/load |
| 452 | - const videoEl = nativeVideoRef.value; | 451 | + const videoEl = nativeVideoRef.value |
| 453 | if (videoEl) { | 452 | if (videoEl) { |
| 454 | - nativeReady.value = false; | 453 | + nativeReady.value = false |
| 455 | - const currentSrc = videoEl.currentSrc || videoEl.src; | 454 | + const currentSrc = videoEl.currentSrc || videoEl.src |
| 456 | - videoEl.pause(); | 455 | + videoEl.pause() |
| 457 | - videoEl.removeAttribute("src"); | 456 | + videoEl.removeAttribute('src') |
| 458 | - videoEl.load(); | 457 | + videoEl.load() |
| 459 | - videoEl.src = currentSrc || videoUrlValue.value; | 458 | + videoEl.src = currentSrc || videoUrlValue.value |
| 460 | - videoEl.load(); | 459 | + videoEl.load() |
| 461 | - tryNativePlay(); | 460 | + tryNativePlay() |
| 462 | 461 | ||
| 463 | retry_error_check_timer = setTimeout(() => { | 462 | retry_error_check_timer = setTimeout(() => { |
| 464 | - const err_code = videoEl?.error?.code; | 463 | + const err_code = videoEl?.error?.code |
| 465 | if (err_code) { | 464 | if (err_code) { |
| 466 | - handleError(err_code); | 465 | + handleError(err_code) |
| 467 | } | 466 | } |
| 468 | - }, 800); | 467 | + }, 800) |
| 469 | } | 468 | } |
| 470 | } else { | 469 | } else { |
| 471 | // video.js 走自身 load 刷新 | 470 | // video.js 走自身 load 刷新 |
| 472 | if (player.value && !player.value.isDisposed()) { | 471 | if (player.value && !player.value.isDisposed()) { |
| 473 | - const p = player.value; | 472 | + const p = player.value |
| 474 | try { | 473 | try { |
| 475 | - p.pause?.(); | 474 | + p.pause?.() |
| 476 | } catch (e) { | 475 | } catch (e) { |
| 477 | - void e; | 476 | + void e |
| 478 | } | 477 | } |
| 479 | try { | 478 | try { |
| 480 | - p.error?.(null); | 479 | + p.error?.(null) |
| 481 | } catch (e) { | 480 | } catch (e) { |
| 482 | - void e; | 481 | + void e |
| 483 | } | 482 | } |
| 484 | try { | 483 | try { |
| 485 | - p.src?.(videoSources.value); | 484 | + p.src?.(videoSources.value) |
| 486 | } catch (e) { | 485 | } catch (e) { |
| 487 | - void e; | 486 | + void e |
| 488 | } | 487 | } |
| 489 | try { | 488 | try { |
| 490 | - p.load?.(); | 489 | + p.load?.() |
| 491 | } catch (e) { | 490 | } catch (e) { |
| 492 | - void e; | 491 | + void e |
| 493 | } | 492 | } |
| 494 | try { | 493 | try { |
| 495 | - p.play?.()?.catch?.(() => {}); | 494 | + p.play?.()?.catch?.(() => {}) |
| 496 | } catch (e) { | 495 | } catch (e) { |
| 497 | - void e; | 496 | + void e |
| 498 | } | 497 | } |
| 499 | 498 | ||
| 500 | retry_error_check_timer = setTimeout(() => { | 499 | retry_error_check_timer = setTimeout(() => { |
| 501 | - const err = p?.error?.(); | 500 | + const err = p?.error?.() |
| 502 | if (err?.code) { | 501 | if (err?.code) { |
| 503 | - handleError(err.code, err.message); | 502 | + handleError(err.code, err.message) |
| 504 | } | 503 | } |
| 505 | - }, 800); | 504 | + }, 800) |
| 506 | } | 505 | } |
| 507 | } | 506 | } |
| 508 | - }; | 507 | + } |
| 509 | 508 | ||
| 510 | // 7. 生命周期与监听 | 509 | // 7. 生命周期与监听 |
| 511 | - watch(() => videoUrlValue.value, () => { | 510 | + watch( |
| 512 | - retryCount.value = 0; | 511 | + () => videoUrlValue.value, |
| 513 | - showErrorOverlay.value = false; | 512 | + () => { |
| 514 | - hideNetworkSpeed(); | 513 | + retryCount.value = 0 |
| 515 | - stopHlsDownloadSpeed('url_change'); | 514 | + showErrorOverlay.value = false |
| 516 | - hasEverPlayed.value = false; | 515 | + hideNetworkSpeed() |
| 517 | - hasStartedPlayback.value = false; | 516 | + stopHlsDownloadSpeed('url_change') |
| 518 | - // 地址变更后刷新探测信息,错误提示会基于 probeInfo 补充更准确的原因 | 517 | + hasEverPlayed.value = false |
| 519 | - void probeVideo(); | 518 | + hasStartedPlayback.value = false |
| 520 | - | 519 | + // 地址变更后刷新探测信息,错误提示会基于 probeInfo 补充更准确的原因 |
| 521 | - // 如果是原生播放器且 URL 变化,需要手动处理 HLS (如果是非 iOS Safari 环境) | 520 | + void probeVideo() |
| 522 | - if (useNativePlayer.value && isM3U8.value) { | 521 | + |
| 523 | - // iOS 原生支持,不需要额外操作 | 522 | + // 如果是原生播放器且 URL 变化,需要手动处理 HLS (如果是非 iOS Safari 环境) |
| 524 | - // 如果未来支持 Android 原生播放器且不支持 HLS,需在此处初始化 hls.js | 523 | + if (useNativePlayer.value && isM3U8.value) { |
| 524 | + // iOS 原生支持,不需要额外操作 | ||
| 525 | + // 如果未来支持 Android 原生播放器且不支持 HLS,需在此处初始化 hls.js | ||
| 526 | + } | ||
| 525 | } | 527 | } |
| 526 | - }); | 528 | + ) |
| 527 | 529 | ||
| 528 | onMounted(() => { | 530 | onMounted(() => { |
| 529 | - void probeVideo(); | 531 | + void probeVideo() |
| 530 | if (useNativePlayer.value) { | 532 | if (useNativePlayer.value) { |
| 531 | - initNativePlayer(); | 533 | + initNativePlayer() |
| 532 | } | 534 | } |
| 533 | - }); | 535 | + }) |
| 534 | 536 | ||
| 535 | onBeforeUnmount(() => { | 537 | onBeforeUnmount(() => { |
| 536 | if (retry_error_check_timer) { | 538 | if (retry_error_check_timer) { |
| 537 | - clearTimeout(retry_error_check_timer); | 539 | + clearTimeout(retry_error_check_timer) |
| 538 | - retry_error_check_timer = null; | 540 | + retry_error_check_timer = null |
| 539 | } | 541 | } |
| 540 | 542 | ||
| 541 | if (nativeListeners?.videoEl) { | 543 | if (nativeListeners?.videoEl) { |
| 542 | - nativeListeners.videoEl.removeEventListener("loadstart", nativeListeners.onLoadStart); | 544 | + nativeListeners.videoEl.removeEventListener('loadstart', nativeListeners.onLoadStart) |
| 543 | - nativeListeners.videoEl.removeEventListener("canplay", nativeListeners.onCanPlay); | 545 | + nativeListeners.videoEl.removeEventListener('canplay', nativeListeners.onCanPlay) |
| 544 | - nativeListeners.videoEl.removeEventListener("error", nativeListeners.onError); | 546 | + nativeListeners.videoEl.removeEventListener('error', nativeListeners.onError) |
| 545 | - nativeListeners.videoEl.removeEventListener("play", nativeListeners.onPlay); | 547 | + nativeListeners.videoEl.removeEventListener('play', nativeListeners.onPlay) |
| 546 | - nativeListeners.videoEl.removeEventListener("pause", nativeListeners.onPause); | 548 | + nativeListeners.videoEl.removeEventListener('pause', nativeListeners.onPause) |
| 547 | - nativeListeners.videoEl.removeEventListener("waiting", nativeListeners.onWaiting); | 549 | + nativeListeners.videoEl.removeEventListener('waiting', nativeListeners.onWaiting) |
| 548 | - nativeListeners.videoEl.removeEventListener("stalled", nativeListeners.onStalled); | 550 | + nativeListeners.videoEl.removeEventListener('stalled', nativeListeners.onStalled) |
| 549 | - nativeListeners.videoEl.removeEventListener("playing", nativeListeners.onPlaying); | 551 | + nativeListeners.videoEl.removeEventListener('playing', nativeListeners.onPlaying) |
| 550 | } | 552 | } |
| 551 | 553 | ||
| 552 | - disposeOverlays(); | 554 | + disposeOverlays() |
| 553 | if (videoRef.value?.$player) { | 555 | if (videoRef.value?.$player) { |
| 554 | - videoRef.value.$player.dispose(); | 556 | + videoRef.value.$player.dispose() |
| 555 | } | 557 | } |
| 556 | - }); | 558 | + }) |
| 557 | 559 | ||
| 558 | return { | 560 | return { |
| 559 | player, | 561 | player, |
| ... | @@ -572,6 +574,6 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { | ... | @@ -572,6 +574,6 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { |
| 572 | hlsSpeedDebugText, | 574 | hlsSpeedDebugText, |
| 573 | retryLoad, | 575 | retryLoad, |
| 574 | handleVideoJsMounted, | 576 | handleVideoJsMounted, |
| 575 | - tryNativePlay | 577 | + tryNativePlay, |
| 576 | - }; | 578 | + } |
| 577 | } | 579 | } | ... | ... |
-
Please register or login to post a comment