今天,又解决了一个单子“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,
// 存储广告的 URL
adUrl: '',
// 存储当前视频在视频列表中的索引
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_url
return 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);
// return
this.$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("已经是第一个视频了");
}
},
// 更新视频信息,包括视频源和广告 URL
updateVideo() {
// 获取当前索引对应的下一个视频
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,修改错误点 3
if (this.isAdVideo) {
this.adUrl = nextVideo.ad.urll;
} else {
this.adUrl = '';
}
// 当视频源更新时,更新播放器的 src
if (this.player) {
this.player.src({ src: this.videoSrc });}
},
// 处理视频点击事件
handleVideoClick() {
// 如果是广告视频且有广告 URL
if (this.isAdVideo && this.adUrl) {
// 根据不同的平台,使用不同的跳转方式打开广告 URL
if (process.env.VUE_APP_PLATFORM === 'h5') {
// 在 H5 平台使用 window.open 打开广告 URL
window.open(this.adUrl, '_blank');
} else {
// 在小程序或其他平台使用 uni.navigateTo 进行跳转
uni.navigateTo({
url: `/pages/webview/webview?url=${encodeURIComponent(this.adUrl)}`
});
}
}
}
}
};
</script>
三、可能的原因
-
视频源路径格式问题:
-
播放器未正确销毁和重新初始化:
-
视频加载超时或失败:
-
用户交互限制:
-
播放器初始化问题:
-
广告视频逻辑问题:
四、问题分析
1. 视频路径格式问题
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>
-
总结
通过修复视频路径格式、确保播放器正确销毁和重新初始化、处理视频加载超时、确保用户交互后播放、修正播放器初始化逻辑以及修正广告视频逻辑,可以有效解决视频切换时一直加载的问题。如果问题仍然存在,建议进一步检查视频源的有效性和网络状态,并使用浏览器的开发者工具查看网络请求和错误日志。