今天,又解决了一个单子“UniApp的音频播放——点击视频进入空白+解决视频播放器切换视频时一直加载的问题”
一、问题描述
在开发一个基于 video.js
的视频播放器时,用户通过上下滑动切换视频时,视频一直处于加载状态,无法正常播放。通过日志可以看到,视频源地址和索引更新是正确的,但视频无法播放。具体表现为:
-
视频加载卡住:切换视频时,播放器一直显示加载动画,无法播放视频。
-
日志显示正常:日志中显示的视频源地址和索引更新是正确的,例如:
即将更新视频源为: http://127.0.0.1:8000/media/m3u8/30bd5d2225919b1724ca69d07633beb1/index.m3u8 currentIndex: 1 videos长度: 4
-
播放器未正确响应:尽管视频源地址更新了,但播放器未能正确加载和播放新视频。
二、问题复现步骤
-
初始化播放器:加载第一个视频,播放器正常工作。
-
滑动切换视频:用户通过上下滑动切换到下一个视频。
-
视频加载卡住:播放器显示加载动画,但视频无法播放。
-
日志输出:日志显示视频源地址和索引更新正确,但播放器未响应。
三、来请看代码,各位客官
<template><!-- <view@click="handleVideoClick"@touchstart="handleTouchStart"@touchmove="handleTouchMove"@touchend="handleTouchEnd"> --><!-- 根据 isAdVideo 的值决定显示广告视频还是常规视频 --><!-- 对于广告视频,不显示控制条,自动播放 --><!-- <video v-if="isAdVideo" :src="videoSrc" :controls="false" autoplay></video> --><!-- 对于常规视频,显示控制条,自动播放 --><!-- <video v-else :src="videoSrc" controls autoplay></video> --><!-- </view> -->
<view><div id="app"><div class="video-js" ref="videos"></div></div>
</view>
</template><script>import { baseUrl } from '@/common/api.js'
export default {data() {return {// 存储当前视频文件的路径videoSrc: '',// 标记当前视频是否为广告视频isAdVideo: false,// 存储广告的 URLadUrl: '',// 存储当前视频在视频列表中的索引currentIndex: 0,// 存储所有视频的数组videos: [],// 存储触摸开始时的 Y 坐标touchStartY: 0,// 存储触摸结束时的 Y 坐标touchEndY: 0,// video.js 的播放器实例player: null,};},onLoad(options) {// 从传入的参数中获取视频文件路径const videoFile = options.videoFile;// 判断是否为广告视频,将字符串 'true' 转换为布尔值const isAd = options.isAd === 'true';// 从传入的参数中获取广告 URL,并进行解码const adUrl = options.adUrl? decodeURIComponent(options.adUrl) : '';// 从传入的参数中获取视频列表,并将其从 JSON 字符串转换为数组const videos = options.videos? JSON.parse(decodeURIComponent(options.videos)) : [];// 将视频文件路径存储到 data 中,修改错误点 1// this.videoFile = videoFile;// 将是否为广告视频的状态存储到 data 中this.isAdVideo = isAd;// 将广告 URL 存储到 data 中this.adUrl = adUrl;// 将视频列表存储到 data 中this.videos = videos;// 根据是否为广告视频来确定视频源的路径if (isAd) {// 假设广告视频的文件名直接作为参数传递,提取文件名const adVideoPath = `${videoFile}`;console.log('1111',adVideoPath);// 拼接完整的广告视频源路径this.videoSrc = `${baseUrl}${adVideoPath}`;} else {// 对于常规视频,在视频列表中查找匹配的视频文件const video = videos.find(v => {console.log('当前视频的 m3u8_url:', v.m3u8_url); // 打印每个视频的 m3u8_urlreturn v.m3u8_url === videoFile;});console.log('222',videoFile);// const video = videos.find(v => v.m3u8_url === videoFile);// console.log('222',v =>v.m3u8_url,videoFile);if (video) {// 拼接完整的常规视频源路径this.videoSrc = `${baseUrl}${video.m3u8_url}`;} else {// 如果未找到对应的视频文件,打印错误信息并退出方法console.error('未找到对应的视频文件路径');return;}}// 查找当前视频在视频列表中的索引this.currentIndex = this.videos.findIndex(v => {if (this.isAdVideo) {const asa =v.ad && v.ad.m3u8_url === videoFile;// 对于广告视频,通过广告视频文件查找索引console.log('当前视频的 vad:', v.ad.m3u8_url,asa);console.log('videoFile',videoFile)return asa ;}// 对于常规视频,通过常规视频文件查找索引return v.m3u8_url === videoFile;});console.log('this.videoFile',videos.find(v => v.m3u8_url === videoFile).m3u8_url);// 打印初始的视频文件路径console.log('Initial video file:', this.videoSrc);},
// beforeDestroy() {
// var playerElement = document.getElementById('video');
// var player = videojs.getInstance(playerElement);
// if (player) {
// player.dispose();
// } // },mounted() {this.initplayer();},beforeDestroy() {// 使用 $refs 来查找 video 元素const videoElement = this.$refs.videos.querySelector('video');if (videoElement) {const player = videojs.getPlayer(videoElement);if (player) {console.log('播放器正在销毁');player.dispose();} else {console.log('未找到播放器实例,可能未初始化');}} else {console.log('未找到 video 元素');}},methods: {initplayer(){// const videoElement = this.$refs.videos.querySelector('video');// const player = videojs.getPlayer(videoElement);// player.dispose();// if (this.player) {// // 如果播放器已经初始化,直接设置新的视频源// this.player.src({ src: this.videoSrc, type: 'application/x-mpegURL' });// this.player.play();// return;// }let video = document.createElement('video');video.id = 'video';// video.style = 'width: 100%; height: 100%;';// video.controls = true;video.preload = "auto"video.setAttribute('playsinline', true) //IOS微信浏览器支持小窗内播放video.setAttribute('webkit-playsinline', true) //这个bai属性是ios 10中设置可以让视频在小du窗内播放,也就是不是全zhi屏播放的video标签的一个属性video.setAttribute('x5-video-player-type', 'h5') //安卓 声明启用同层H5播放器 可以在video上面加东西// const ada='http://127.0.0.1:8000/media\\m3u8\\caba10d1b61e5f2aa1e068bebeb55663\\index.m3u8';let source = document.createElement('source');// source.src = ada;source.src = this.videoSrc;video.appendChild(source);// returnthis.$refs.videos.appendChild(video);let that = this;let player = this.$video('video', {autoDisable: true,preload: 'none', //auto - 当页面加载后载入整个视频 meta - 当页面加载后只载入元数据 none - 当页面加载后不载入视频language: 'zh-CN',fluid: true, // 自适应宽高muted: false, // 是否静音aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")controls: true, //是否拥有控制条 【默认true】,如果设为false ,那么只能通过api进行控制了。也就是说界面上不会出现任何控制按钮autoplay: false, //如果true,浏览器准备好时开始回放。 autoplay: "muted", // //自动播放属性,muted:静音播放loop: true, // 导致视频一结束就重新开始。 视频播放结束后,是否循环播放controlBar: {volumePanel: { //声音样式inline: true // 不使用水平方式},timeDivider: true, // 时间分割线durationDisplay: true, // 总时间progressControl: true, // 进度条remainingTimeDisplay: true, //当前以播放时间fullscreenToggle: true, //全屏按钮pictureInPictureToggle: false, //画中画}}, function() {this.on('error', function(err) { //请求数据时遇到错误console.log("请求数据时遇到错误", err)});this.on('stalled', function(stalled) { //网速失速console.log("网速失速", stalled)});});},// 处理触摸开始事件,记录触摸开始时的 Y 坐标handleTouchStart(event) {this.touchStartY = event.touches[0].clientY;},// 处理触摸移动事件,目前不做任何处理,可添加优化逻辑handleTouchMove(event) {// 例如,可以添加代码防止快速滑动时的抖动效果},// 处理触摸结束事件,记录触摸结束时的 Y 坐标,并调用 handleSwipe 方法handleTouchEnd(event) {this.touchEndY = event.changedTouches[0].clientY;this.handleSwipe();},// 处理滑动操作handleSwipe() {// 计算触摸的垂直距离const distance = this.touchEndY - this.touchStartY;// 如果滑动距离小于 30 像素,不进行任何操作if (Math.abs(distance) < 30) {return;}// 如果滑动距离大于 0,表示向下滑动,调用 handleSwipeDown 方法if (distance > 0) {this.handleSwipeDown();} else {// 否则表示向上滑动,调用 handleSwipeUp 方法this.handleSwipeUp();}},// 处理向上滑动,切换到下一个视频handleSwipeUp() {// 如果不是最后一个视频if (this.currentIndex < this.videos.length - 1) {// 增加当前视频索引this.currentIndex++;// 更新视频信息this.updateVideo();// 打印下一个视频的文件路径console.log('Swipe Up: Next video file:', this.videoSrc);// 打印是否为广告视频console.log('是否广告', this.isAdVideo);} else {// 已到达最后一个视频,打印提示信息console.log("已经是最后一个视频了");}},// 处理向下滑动,切换到上一个视频handleSwipeDown() {// 如果不是第一个视频if (this.currentIndex > 0) {// 减小当前视频索引this.currentIndex--;// 更新视频信息this.updateVideo();// 打印上一个视频的文件路径console.log('Swipe Down: Previous video file:', this.videoSrc);} else {// 已到达第一个视频,打印提示信息console.log("已经是第一个视频了");}},// 更新视频信息,包括视频源和广告 URLupdateVideo() {// 获取当前索引对应的下一个视频const nextVideo = this.videos[this.currentIndex];console.log('11111', this.videos, nextVideo, this.currentIndex);// 判断下一个视频是否为广告视频this.isAdVideo =!!nextVideo.ad;if (this.isAdVideo) {// 如果是广告视频,更新视频源为广告视频源并打印console.log('1111:', nextVideo.ad.m3u8_url);this.videoSrc = `${baseUrl}${nextVideo.ad.m3u8_url}`;} else {// 如果是常规视频,更新视频源为常规视频源并打印this.videoSrc = `${baseUrl}${nextVideo.m3u8_url}`;console.log('2222:', nextVideo.m3u8_url);}// 根据是否为广告视频更新广告 URL,修改错误点 3if (this.isAdVideo) {this.adUrl = nextVideo.ad.urll;} else {this.adUrl = '';}// 当视频源更新时,更新播放器的 srcif (this.player) {this.player.src({ src: this.videoSrc });}},// 处理视频点击事件handleVideoClick() {// 如果是广告视频且有广告 URLif (this.isAdVideo && this.adUrl) {// 根据不同的平台,使用不同的跳转方式打开广告 URLif (process.env.VUE_APP_PLATFORM === 'h5') {// 在 H5 平台使用 window.open 打开广告 URLwindow.open(this.adUrl, '_blank');} else {// 在小程序或其他平台使用 uni.navigateTo 进行跳转uni.navigateTo({url: `/pages/webview/webview?url=${encodeURIComponent(this.adUrl)}`});}}}}
};
</script>
三、可能的原因
-
视频源路径格式问题:
-
视频路径中使用了反斜杠
\
,例如:http://127.0.0.1:8000\media\m3u8\30bd5d2225919b1724ca69d07633beb1\index.m3u8
。 -
反斜杠在某些环境下可能导致路径解析错误。
-
-
播放器未正确销毁和重新初始化:
-
在切换视频时,旧的播放器实例可能未正确销毁,导致新的播放器实例无法正常初始化。
-
-
视频加载超时或失败:
-
视频文件可能无法加载,或者加载时间过长,导致播放器一直处于加载状态。
-
-
用户交互限制:
-
某些浏览器要求视频播放必须在用户交互后触发,如果未正确处理用户交互,可能导致视频无法播放。
-
-
播放器初始化问题:
-
播放器初始化逻辑中,
this.$video
未定义,可能导致播放器无法正确初始化。
-
-
广告视频逻辑问题:
-
广告视频的逻辑中,
nextVideo.ad.urll
拼写错误,导致广告 URL 无法正确更新。
-
四、问题分析
1. 视频路径格式问题
-
问题:视频路径中使用了反斜杠
\
,例如:http://127.0.0.1:8000\media\m3u8\30bd5d2225919b1724ca69d07633beb1\index.m3u8
。 -
影响:在某些环境下,反斜杠可能导致路径解析错误,视频无法加载。
-
解决方案:将反斜杠替换为正斜杠
/
。
this.videoSrc = `${baseUrl}${videoFile}`.replace(/\\/g, '/');
2. 播放器未正确销毁和重新初始化
-
问题:在切换视频时,旧的播放器实例可能未正确销毁,导致新的播放器实例无法正常初始化。
-
影响:切换视频时,播放器可能卡在加载状态或无法播放。
-
解决方案:在切换视频时,销毁旧的播放器实例并重新初始化新的播放器实例。
updateVideo() {const nextVideo = this.videos[this.currentIndex];this.isAdVideo = !!nextVideo.ad;if (this.isAdVideo) {this.videoSrc = `${baseUrl}${nextVideo.ad.m3u8_url}`.replace(/\\/g, '/');} else {this.videoSrc = `${baseUrl}${nextVideo.m3u8_url}`.replace(/\\/g, '/');}this.adUrl = this.isAdVideo ? nextVideo.ad.url : '';console.log('即将更新视频源为:', this.videoSrc);this.destroyPlayer(); // 销毁旧的播放器实例this.$nextTick(() => {this.initplayer(); // 重新初始化播放器});
}
3. 视频加载超时或失败
-
问题:视频文件可能无法加载,或者加载时间过长,导致播放器一直处于加载状态。
-
影响:用户可能会看到视频一直加载,无法播放。
-
解决方案:设置超时机制,防止长时间停留在加载状态。
async updateVideoSource() {if (!this.player) return;console.log('正在更新视频源:', this.videoSrc);try {this.player.pause(); // 暂停当前播放this.player.src({ src: this.videoSrc, type: 'application/x-mpegURL' });this.player.load();// 监听 loadeddata 事件,确保视频数据加载完成后再尝试播放this.player.one('loadeddata', () => {console.log('视频数据加载完成');this.isPlaying = false;this.player.play().then(() => {this.isPlaying = true;}).catch(error => {console.error('播放失败:', error);});});// 设置一个超时机制,防止长时间停留在加载状态const timeoutId = setTimeout(() => {console.warn('视频加载超时');// 尝试重新加载视频this.player.src({ src: this.videoSrc, type: 'application/x-mpegURL' });this.player.load();}, 10000); // 10秒超时// 当视频加载完成时清除超时this.player.on('loadeddata', () => clearTimeout(timeoutId));} catch (error) {console.error('更新视频源并准备播放失败:', error);} }
4. 用户交互限制
-
问题:某些浏览器要求视频播放必须在用户交互后触发,如果未正确处理用户交互,可能导致视频无法播放。
-
影响:视频无法自动播放,用户需要手动点击播放按钮。
-
解决方案:在用户交互后触发视频播放。
handleTouchEnd(event) {this.touchEndY = event.changedTouches[0].clientY;this.handleSwipe();this.isUserInteracted = true; // 标记用户交互if (this.player && this.isUserInteracted) {this.player.play().catch(error => {console.error('播放失败:', error);});}
}
5. 播放器初始化问题
-
问题:播放器初始化逻辑中,
this.$video
未定义,可能导致播放器无法正确初始化。 -
影响:播放器无法正常工作。
-
解决方案:使用
videojs
直接初始化播放器。initplayer() {let video = document.createElement('video');video.id = 'video';video.preload = "auto";video.setAttribute('playsinline', true);video.setAttribute('webkit-playsinline', true);video.setAttribute('x5-video-player-type', 'h5');let source = document.createElement('source');source.src = this.videoSrc;video.appendChild(source);this.$refs.videos.appendChild(video);this.player = videojs(video, {autoplay: false,controls: !this.isAdVideo,sources: [{ src: this.videoSrc, type: 'application/x-mpegURL' }]});this.player.on('error', (error) => {console.error('视频加载错误:', error);}); }
6. 播放器销毁问题
-
问题:在
beforeDestroy
钩子中,播放器销毁逻辑可能无法正确执行。 -
影响:播放器实例可能未正确销毁,导致内存泄漏。
-
解决方案:确保播放器实例被正确销毁。
beforeDestroy() {if (this.player) {console.log('播放器正在销毁');this.player.dispose();this.player = null;} else {console.log('未找到播放器实例,可能未初始化');} }
7. 日志输出不足
-
问题:日志输出较少,难以定位问题。
-
影响:调试困难。
-
解决方案:在关键步骤添加日志输出。
console.log('即将更新视频源为:', this.videoSrc); console.log('currentIndex:', this.currentIndex); console.log('videos长度:', this.videos.length);
8. 广告视频逻辑问题
-
问题:广告视频的逻辑中,
nextVideo.ad.urll
拼写错误。 -
影响:广告 URL 无法正确更新。
-
解决方案:修正拼写错误。
if (this.isAdVideo) {this.adUrl = nextVideo.ad.url; // 修正拼写错误 } else {this.adUrl = ''; }
-
五、完整代码
-
<template><view @touchstart="handleTouchStart" @touchmove="handleTouchMove" @touchend="handleTouchEnd"><!-- 视频容器 --><div id="app"><div class="video-js" ref="videos"></div></div></view> </template><script> import { baseUrl } from '@/common/api.js'; import videojs from 'video.js'; import 'video.js/dist/video-js.css'; // 引入默认样式export default {name: 'VideoPlayer',data() {return {videoSrc: '',isAdVideo: false,adUrl: '',currentIndex: 0,videos: [],touchStartY: 0,touchEndY: 0,player: null, // video.js 的播放器实例uniqueKey: Date.now(), // 用于强制刷新组件isPlaying: false, // 跟踪播放状态isUserInteracted: false, // 跟踪用户交互状态};},onLoad(options) {this.initFromOptions(options);},activated() {// 当组件被激活时(从缓存中恢复),重新初始化播放器this.$nextTick(() => this.initplayer());},deactivated() {// 当组件被停用时(进入缓存),销毁播放器this.destroyPlayer();},mounted() {// 确保在挂载时初始化播放器this.$nextTick(() => this.initplayer());},methods: {async initFromOptions(options) {const videoFile = options.videoFile;const isAd = options.isAd === 'true';const adUrl = options.adUrl ? decodeURIComponent(options.adUrl) : '';const videos = options.videos ? JSON.parse(decodeURIComponent(options.videos)) : [];this.isAdVideo = isAd;this.adUrl = adUrl;this.videos = videos;if (isAd) {this.videoSrc = `${baseUrl}${videoFile}`.replace(/\\/g, '/');} else {const video = videos.find(v => v.m3u8_url === videoFile);if (video) {this.videoSrc = `${baseUrl}${video.m3u8_url}`.replace(/\\/g, '/');} else {console.error('未找到对应的视频文件路径');return;}}this.currentIndex = this.videos.findIndex(v => {if (this.isAdVideo) {return v.ad && v.ad.m3u8_url === videoFile;}return v.m3u8_url === videoFile;});console.log('Initial video file:', this.videoSrc);await this.initplayer(); // 确保播放器初始化完成},async initplayer() {// 如果播放器已经存在,则更新源而不是重新创建if (this.player) {await this.updateVideoSource();return;}let videoElement = document.createElement('video');videoElement.id = 'video';videoElement.preload = "auto";videoElement.setAttribute('playsinline', true);videoElement.setAttribute('webkit-playsinline', true);videoElement.setAttribute('x5-video-player-type', 'h5');let source = document.createElement('source');source.src = this.videoSrc;videoElement.appendChild(source);this.$refs.videos.appendChild(videoElement);// 使用 Vue 的 nextTick 方法确保 DOM 更新完成后才初始化 Video.js 播放器this.$nextTick(() => {this.player = videojs(videoElement,{autoplay: false,controls: !this.isAdVideo,sources: [{ src: this.videoSrc, type: 'application/x-mpegURL' }]},async function onPlayerReady() {console.log("播放器已准备好");try {if (this.isUserInteracted) {await this.play(); // 使用 async/await 确保 play() 完成}} catch (error) {console.error('播放失败:', error);}}.bind(this));// 监听错误事件this.player.on('error', (error) => {console.error('视频加载错误:', error);console.error('错误详情:', this.player.error()); // 获取详细的错误信息});});},destroyPlayer() {if (this.player) {console.log('播放器正在销毁');this.player.dispose();this.player = null; // 清除 player 实例引用} else {console.log('未找到播放器实例,可能未初始化');}},async updateVideoSource() {if (!this.player) return;console.log('正在更新视频源:', this.videoSrc);try {this.player.pause(); // 暂停当前播放this.player.src({ src: this.videoSrc, type: 'application/x-mpegURL' });this.player.load();// 监听 loadeddata 事件,确保视频数据加载完成后再尝试播放this.player.one('loadeddata', () => {console.log('视频数据加载完成');this.isPlaying = false;this.player.play().then(() => {this.isPlaying = true;}).catch(error => {console.error('播放失败:', error);// 可以在这里添加重试逻辑或提示用户});});// 设置一个超时机制,防止长时间停留在加载状态const timeoutId = setTimeout(() => {console.warn('视频加载超时');// 尝试重新加载视频this.player.src({ src: this.videoSrc, type: 'application/x-mpegURL' });this.player.load();}, 10000); // 10秒超时// 当视频加载完成时清除超时this.player.on('loadeddata', () => clearTimeout(timeoutId));} catch (error) {console.error('更新视频源并准备播放失败:', error);// 可以在这里添加重试逻辑或提示用户}},handleTouchStart(event) {this.touchStartY = event.touches[0].clientY;},handleTouchMove(event) {// 这里可以添加优化逻辑,但目前保持原样},handleTouchEnd(event) {this.touchEndY = event.changedTouches[0].clientY;this.handleSwipe();this.isUserInteracted = true; // 标记用户交互if (this.player && this.isUserInteracted) {this.player.play().catch(error => {console.error('播放失败:', error);});}},handleSwipe() {const distance = this.touchEndY - this.touchStartY;if (Math.abs(distance) < 30) return;if (distance > 0) {this.handleSwipeDown();} else {this.handleSwipeUp();}},handleSwipeUp() {if (this.currentIndex < this.videos.length - 1) {this.currentIndex++;this.updateVideo();console.log('Swipe Up: Next video file:', this.videoSrc);console.log('是否广告', this.isAdVideo);} else {console.log("已经是最后一个视频了");}},handleSwipeDown() {if (this.currentIndex > 0) {this.currentIndex--;this.updateVideo();console.log('Swipe Down: Previous video file:', this.videoSrc);} else {console.log("已经是第一个视频了");}},updateVideo() {const nextVideo = this.videos[this.currentIndex];this.isAdVideo = !!nextVideo.ad;if (this.isAdVideo) {this.videoSrc = `${baseUrl}${nextVideo.ad.m3u8_url}`.replace(/\\/g, '/');} else {this.videoSrc = `${baseUrl}${nextVideo.m3u8_url}`.replace(/\\/g, '/');}this.adUrl = this.isAdVideo ? nextVideo.ad.url : '';console.log('即将更新视频源为:', this.videoSrc); // 添加日志输出console.log('currentIndex:', this.currentIndex); // 添加日志输出console.log('videos长度:', this.videos.length); // 添加日志输出this.destroyPlayer(); // 销毁旧的播放器实例this.$nextTick(() => {this.initplayer(); // 重新初始化播放器});},handleVideoClick() {if (this.isAdVideo && this.adUrl) {if (process.env.VUE_APP_PLATFORM === 'h5') {window.open(this.adUrl, '_blank');} else {uni.navigateTo({url: `/pages/webview/webview?url=${encodeURIComponent(this.adUrl)}`});}}}},watch: {// 监听路由变化并强制刷新组件$route(to, from) {this.uniqueKey = Date.now(); // 改变 key 来强制刷新组件this.$nextTick(() => this.initplayer()); // 确保播放器在路由变化后重新初始化}} }; </script><style scoped> /* 添加样式 */ #app {width: 100%;height: 100%; } .video-js {width: 100%;height: 100%; } </style>
-
总结
通过修复视频路径格式、确保播放器正确销毁和重新初始化、处理视频加载超时、确保用户交互后播放、修正播放器初始化逻辑以及修正广告视频逻辑,可以有效解决视频切换时一直加载的问题。如果问题仍然存在,建议进一步检查视频源的有效性和网络状态,并使用浏览器的开发者工具查看网络请求和错误日志。