liujiaxin 2 weeks ago
parent
commit
55f898af0c

+ 4 - 3
pages/home/living.vue

@@ -62,7 +62,7 @@
 						<view class="videolist" style="margin: auto 0">
 							<view class="video" style="height:100vh">
 								<!-- 视频组件 -->
-								<video v-if="liveItem.livingUrl" :id="'myVideo_' + liveItem.liveId"
+								<video v-if="currentSwiperIndex === index && liveItem.livingUrl" :id="'myVideo_' + liveItem.liveId"
 									:class="liveItem.showType == 1 ? 'video_row' : 'videotop'" :src="liveItem.livingUrl"
 									:autoplay="currentSwiperIndex === index" :controls='false' object-fit='contain'
 									:custom-cache="false" :enable-progress-gesture="false"
@@ -70,7 +70,7 @@
 									:http-cache="false" @error="videoError">
 								</video>
 
-								<video v-if="liveItem.videoUrl"
+								<video v-if="currentSwiperIndex === index && liveItem.videoUrl"
 									:class="liveItem.showType == 1 ? 'video_row' : 'videotop'" :src="liveItem.videoUrl"
 									:autoplay="currentSwiperIndex === index" :controls='false' object-fit='contain'
 									:custom-cache="false" :enable-progress-gesture="false"
@@ -768,6 +768,7 @@
 				if (oldLive) {
 					this.pauseVideo(oldLive);
 					this.clearTimeTimer(oldLive); // 清除旧直播间的时间定时器
+					console.log("oldLive",oldLive)
 					// 强制关闭并删除实例,确保连接被释放
 					if (this.socketInstances[oldLive.liveId]) {
 						try {
@@ -2309,7 +2310,7 @@
 	}
 
 	.icon-bg {
-		background-color: rgba(157, 157, 157, 0.8);
+		background-color: rgba(57, 57, 57, 0.8);
 		border-radius: 50%;
 		width: 72rpx;
 		height: 72rpx;

+ 277 - 23
pages/list/index.vue

@@ -4,7 +4,22 @@
 			:down="downOption" :up="upOption">
 			<view class="list">
 				<view class="list-item" @click="goLive(item)" v-for="(item,index) in list" :key="index">
-					<image :src="item.liveImgUrl"></image>
+
+					<!-- <image :src="item.liveImgUrl"></image> -->
+
+
+					<video v-if="item.flvHlsUrl && item.liveId" :id="'myVideo_' + item.liveId" :src="item.flvHlsUrl"
+						:autoplay="false" :controls='false' object-fit='contain' :custom-cache="false"
+						:enable-progress-gesture="false" vslide-gesture-in-fullscreen='true'
+						:show-center-play-btn="false" :http-cache="false" @error="videoError">
+					</video>
+
+					<video v-if="item.videoUrl" :src="item.videoUrl" :autoplay="false" :controls='false'
+						object-fit='contain' :custom-cache="false" :enable-progress-gesture="false"
+						vslide-gesture-in-fullscreen='true' :show-center-play-btn="false" :http-cache="false" loop
+						@error="videoError">
+					</video>
+
 					<view class="info">
 						<text>{{item.liveName}}</text>
 					</view>
@@ -16,20 +31,31 @@
 
 </template>
 <script>
+	import Hls from 'hls.js';
 	import {
 		liveList
 	} from '@/api/list'
 	import MescrollMixin from "@/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js";
-	// import { LiveWS } from '@/utils/liveWS'
-	// import { login,loginByWeChat,getUserInfo,loginByApple } from '@/api/user'
 	export default {
 		mixins: [MescrollMixin], // 使用mixin
 		data() {
 			return {
+
+				hlsPlayer: null, // HLS播放器实例,
+				// 统一直播/录播类型标识
+				LIVE_TYPE: {
+					LIVE: 1, // 直播
+					RECORD: 2 // 录播
+				},
+
+
+
+
+
 				list: [],
 				liveId: null, // mescroll配置
 				downOption: {
-					offset: 80, 
+					offset: 80,
 					use: true,
 					auto: false // 是否在初始化后自动执行下拉回调
 				},
@@ -40,36 +66,244 @@
 						num: 0, // 当前页码
 						size: 10 // 每页数据条数
 					}
-					
+
 				},
 				mescroll: null // mescroll实例
 			}
 		},
 		onLoad() {
-			if(!uni.getStorageSync("AppToken")){
+			if (!uni.getStorageSync("AppToken")) {
 				uni.navigateTo({
 					url: '/pages/auth/login'
 				});
-				
+
+			} // 销毁HLS播放器(如果使用了hls.js)
+			if (this.hlsPlayer) {
+				this.hlsPlayer.destroy();
+				this.hlsPlayer = null;
 			}
 		},
-		methods: {
-			goLive(item) {
-				this.liveId = item.liveId
-				console.log("要传的liveId", this.liveId)
-				uni.navigateTo({
-					url: `/pages/home/living?liveId=${item.liveId}&immediate=true`
+		onUnload() {
+			this.list.forEach(item => {
+				this.$set(item, 'isDestroyed', true); // 标记已销毁,阻止播放
+				this.pauseAndCleanVideo(item); // 统一清理
+			});
+		},
+		methods: { // 处理单个视频项,适配m3u8流
+			initVideoPlayer(item) {
+				// 基础校验:无地址/无ID,不初始化
+				const isLive = item.liveType === 1;
+				const hasValidUrl = isLive ? (item.flvHlsUrl?.includes('.m3u8')) : !!item.videoUrl;
+				if (!hasValidUrl || !item.liveId) return;
+
+				const videoId = `myVideo_${item.liveId}`;
+				console.log(`[视口监听] 初始化:${videoId}`);
+
+				// 1. 创建交叉观察器(监听是否进入视口)
+				const observer = uni.createIntersectionObserver(this, {
+					thresholds: [0.2], // 20% 进入视口触发
+					observeAll: false
+				});
+
+				// 2. 监听视口状态,触发按需播放/暂停
+				observer.relativeTo('.list').observe(`#${videoId}`, (res) => {
+					const isInView = res.intersectionRatio > 0; // 是否在视口内
+
+					if (isInView) {
+						console.log(`[视口监听] 进入视口,触发按需播放:${videoId}`);
+						// 标记为在视口,避免重复调用
+						this.$set(item, 'isInView', true);
+						// 调用统一播放入口,实现按需播放
+						this.setupVideoPlayback(item);
+					} else {
+						console.log(`[视口监听] 离开视口,暂停播放:${videoId}`);
+						// 标记为离开视口
+						this.$set(item, 'isInView', false);
+						// 暂停播放并清理资源
+						this.pauseAndCleanVideo(item);
+					}
+				});
+
+				// 3. 存储观察器到 item,便于后续销毁
+				this.$set(item, 'observer', observer);
+				// 标记未销毁,避免 setupVideoPlayback 跳过
+				this.$set(item, 'isDestroyed', false);
+			},
+			pauseAndCleanVideo(item) {
+				// 1. 暂停录播/小程序直播(通过 videoContext)
+				if (item.videoContext) {
+					item.videoContext.pause();
+					this.$set(item, 'videoContext', null); // 清空上下文,避免重复调用
+				}
+
+				// 2. 销毁 HLS 实例(直播 H5 场景)
+				if (item.hlsInstance) {
+					item.hlsInstance.destroy();
+					this.$set(item, 'hlsInstance', null);
+				}
+
+				// 3. 销毁视口监听(避免列表刷新后仍监听)
+				if (item.observer) {
+					item.observer.disconnect();
+					this.$set(item, 'observer', null);
+				}
+
+				// 4. 标记为已销毁(防止重复初始化)
+				this.$set(item, 'isDestroyed', true);
+			},
+
+			setupVideoPlayback(item) {
+				// 1. 基础校验:先判断类型,再校验 URL(避免无效执行)
+				const isLive = item.liveType === this.LIVE_TYPE.LIVE;
+				const videoUrl = isLive ? item.flvHlsUrl : item.videoUrl;
+
+				// 无效场景直接返回(录播无URL、直播无m3u8、已销毁)
+				if (!videoUrl || item.isDestroyed || (isLive && !videoUrl.includes('.m3u8'))) {
+					console.warn(`[播放校验] 跳过无效播放:`, item.liveId, '类型:', isLive ? '直播' : '录播');
+					return;
+				}
+
+				const videoId = `myVideo_${item.liveId}`;
+				console.log(`[按需播放] 触发:${isLive ? '直播' : '录播'},ID:${videoId}`);
+
+				// 2. 先清理旧资源(避免重复初始化)
+				this.pauseAndCleanVideo(item);
+
+				// 3. 直播逻辑:仅 H5 平台用 HLS,其他平台(小程序/App)用原生 video
+				if (isLive) {
+					// #ifdef H5
+					// 直播 + H5 + m3u8:初始化 HLS
+					this.initHlsPlayer(item);
+					// #endif
+
+					// #ifndef H5
+					// 小程序/App 直播:直接用原生 videoContext(无需 HLS,平台自带 m3u8 支持)
+					this.initNativeVideo(item, videoId, isLive);
+					// #endif
+				} else {
+					// 4. 录播逻辑:所有平台统一用原生 videoContext(无需 HLS)
+					this.initNativeVideo(item, videoId, isLive);
+				}
+			},
+			/**
+			 * 初始化原生视频(录播 + 小程序/App 直播)
+			 * @param {Object} item - 列表项数据
+			 * @param {String} videoId - 视频DOM ID
+			 * @param {Boolean} isLive - 是否为直播
+			 */
+			initNativeVideo(item, videoId, isLive) {
+				uni.createSelectorQuery().in(this)
+					.select(`#${videoId}`)
+					.fields({
+						context: true
+					}) // 仅获取视频上下文
+					.exec((res) => {
+						if (!res || !res[0]?.context) {
+							console.warn(`[原生视频] 未找到上下文:${videoId}`);
+							return;
+						}
+
+						const videoContext = res[0].context;
+						this.$set(item, 'videoContext', videoContext); // 存储上下文,用于后续暂停
+
+						// 直播:直接播放(需用户交互,失败时提示)
+						if (isLive) {
+							videoContext.play().catch(err => {
+								console.log(`[直播播放] 自动播放失败(需点击):${item.liveId}`, err);
+								// 可选:添加「播放按钮」,点击后调用 videoContext.play()
+							});
+						}
+						// 录播:支持断点续播(从上次进度开始)
+						else {
+							const startDuration = item.nowDuration || 0; // 从接口获取的上次播放进度
+							videoContext.seek(startDuration); // 跳转到指定进度
+							videoContext.play().catch(err => {
+								console.log(`[录播播放] 失败:${item.liveId}`, err);
+							});
+						}
+					});
+			},
+
+			// 初始化HLS播放器
+			initHlsPlayer(item) {
+				// 兜底校验:确保是直播且有 m3u8 地址
+				if (item.liveType !== this.LIVE_TYPE.LIVE || !item.flvHlsUrl?.includes('.m3u8')) {
+					console.error(`[HLS 错误] 非直播场景调用:${item.liveId}`);
+					return;
+				}
+
+				const videoUrl = item.flvHlsUrl;
+				if (!Hls.isSupported()) {
+					console.error(`[HLS 错误] 浏览器不支持 HLS:${item.liveId}`);
+					return;
+				}
+
+				// 获取 H5 视频 DOM 节点
+				const video = document.getElementById(`myVideo_${item.liveId}`);
+				if (!video) {
+					console.warn(`[HLS 错误] 未找到视频节点:myVideo_${item.liveId}`);
+					return;
+				}
+
+				// 初始化 HLS(低延迟配置,适合直播)
+				const hls = new Hls({
+					enableWorker: true,
+					lowLatencyMode: true, // 直播低延迟关键配置
+					debug: false,
+					maxBufferLength: 3, // 减少缓冲,降低延迟(根据需求调整)
+					maxMaxBufferLength: 10
+				});
+
+				// 绑定视频节点 + 加载流
+				hls.attachMedia(video);
+				hls.loadSource(videoUrl);
+
+				// 解析成功后播放
+				hls.on(Hls.Events.MANIFEST_PARSED, () => {
+					console.log(`[HLS 直播] 解析成功:${item.liveId}`);
+					video.play().catch(err => {
+						console.log(`[HLS 播放] 自动播放失败(需点击):${item.liveId}`, err);
+					});
 				});
+
+				// 错误处理(自动恢复或销毁)
+				hls.on(Hls.Events.ERROR, (event, data) => {
+					console.error(`[HLS 错误] ${item.liveId}:`, data);
+					if (data.fatal) {
+						switch (data.type) {
+							case Hls.ErrorTypes.NETWORK_ERROR:
+								hls.startLoad(); // 网络错误:重试加载
+								break;
+							default:
+								hls.destroy(); // 其他致命错误:销毁实例
+								this.$set(item, 'hlsInstance', null);
+								break;
+						}
+					}
+				});
+
+				// 存储 HLS 实例到 item(便于后续销毁)
+				this.$set(item, 'hlsInstance', hls);
 			},
+
+
 			// mescroll初始化
 			mescrollInit(mescroll) {
 				this.mescroll = mescroll;
 			},
 			// 下拉刷新回调
 			downCallback(mescroll) {
-				// 重置列表数据
+				// 1. 先清理所有旧列表的视频资源(直播 HLS + 录播上下文)
+				this.list.forEach(item => {
+					this.pauseAndCleanVideo(item);
+				});
+
+				// 2. 重置列表和分页(必须在清理后执行)
 				this.list = [];
-				mescroll.resetUpScroll();
+				mescroll.resetUpScroll(); // 重置上拉加载的页码
+
+				// 3. 可选:下拉后自动加载第一页(符合用户预期)
+				this.mescroll.triggerUpScroll();
 			},
 			// 上拉加载回调
 			upCallback(mescroll) {
@@ -80,25 +314,28 @@
 					pageSize: pageSize,
 					page: pageNum,
 				}
+
 				liveList(data).then(res => {
 					if (res.code == 200) {
-						// 请求成功,处理数据
 						let curPageData = res.rows || [];
-						let curPageLen = curPageData.length;
 						let totalSize = res.total || 0;
 
-						// 如果是第一页,直接赋值
 						if (pageNum === 1) {
 							this.list = [];
 						}
 
 						// 追加新数据
-						this.list = this.list.concat(curPageData);
+					 this.list = this.list.concat(curPageData);
 
-						mescroll.endBySize(curPageLen, totalSize);
+      // 关键修改:用 $nextTick 等待DOM渲染完成
+      this.$nextTick(() => {
+        curPageData.forEach(item => {
+          this.initVideoPlayer(item); // 此时视频节点已存在
+        });
+      });
 
-					} else {
-						// 请求失败
+      mescroll.endBySize(curPageData.length, totalSize);
+    } else {
 						mescroll.endErr();
 						uni.showToast({
 							title: res.msg,
@@ -106,11 +343,22 @@
 						});
 					}
 				}).catch(err => {
-					// 请求异常
 					mescroll.endErr();
 					console.log("请求异常:" + JSON.stringify(err));
 				});
 			},
+
+
+
+
+			goLive(item) {
+				this.liveId = item.liveId
+				console.log("要传的liveId", this.liveId)
+				uni.navigateTo({
+					url: `/pages/home/living?liveId=${item.liveId}&immediate=true`
+				});
+			},
+
 			// getList() {
 			// 	const data = {
 			// 		page: 1,
@@ -163,6 +411,12 @@
 				overflow: hidden;
 				position: relative;
 
+				video {
+					width: 100%;
+					height: 100%;
+					/* 视频填满列表项,确保可视区域判断准确 */
+				}
+
 				.info {
 					position: absolute;
 					left: 20rpx;

File diff suppressed because it is too large
+ 0 - 0
unpackage/dist/dev/mp-weixin/pages/home/living.js


File diff suppressed because it is too large
+ 0 - 0
unpackage/dist/dev/mp-weixin/pages/home/living.wxss


File diff suppressed because it is too large
+ 0 - 1
unpackage/dist/dev/mp-weixin/pages/list/index.js


+ 1 - 1
unpackage/dist/dev/mp-weixin/pages/list/index.wxml

@@ -1 +1 @@
-<view class="content data-v-7bd28468"><mescroll-body wx:if="{{f}}" class="r data-v-7bd28468" u-s="{{['d']}}" u-r="mescrollRef" bindinit="{{c}}" binddown="{{d}}" bindup="{{e}}" u-i="7bd28468-0" bind:__l="__l" u-p="{{f}}"><view class="list data-v-7bd28468"><view wx:for="{{a}}" wx:for-item="item" wx:key="d" class="list-item data-v-7bd28468" bindtap="{{item.c}}"><image class="data-v-7bd28468" src="{{item.a}}"></image><view class="info data-v-7bd28468"><text class="data-v-7bd28468">{{item.b}}</text></view></view></view></mescroll-body></view>
+<view class="content data-v-7bd28468"><mescroll-body wx:if="{{f}}" class="r data-v-7bd28468" u-s="{{['d']}}" u-r="mescrollRef" bindinit="{{c}}" binddown="{{d}}" bindup="{{e}}" u-i="7bd28468-0" bind:__l="__l" u-p="{{f}}"><view class="list data-v-7bd28468"><view wx:for="{{a}}" wx:for-item="item" wx:key="j" class="list-item data-v-7bd28468" bindtap="{{item.i}}"><video wx:if="{{item.a}}" class="data-v-7bd28468" id="{{item.b}}" src="{{item.c}}" autoplay="{{false}}" controls="{{false}}" object-fit="contain" custom-cache="{{false}}" enable-progress-gesture="{{false}}" vslide-gesture-in-fullscreen="true" show-center-play-btn="{{false}}" http-cache="{{false}}" binderror="{{item.d}}"></video><video wx:if="{{item.e}}" class="data-v-7bd28468" src="{{item.f}}" autoplay="{{false}}" controls="{{false}}" object-fit="contain" custom-cache="{{false}}" enable-progress-gesture="{{false}}" vslide-gesture-in-fullscreen="true" show-center-play-btn="{{false}}" http-cache="{{false}}" loop binderror="{{item.g}}"></video><view class="info data-v-7bd28468"><text class="data-v-7bd28468">{{item.h}}</text></view></view></view></mescroll-body></view>

+ 1 - 1
unpackage/dist/dev/mp-weixin/pages/list/index.wxss

@@ -1 +1 @@
-.content.data-v-7bd28468{background-color:#111;min-height:100vh;padding:24rpx}.content .list.data-v-7bd28468{display:flex;justify-content:space-between;flex-wrap:wrap}.content .list .list-item.data-v-7bd28468{border-radius:16rpx;width:340rpx;height:600rpx;background-color:#0d0d0d;margin-right:10rpx;margin-bottom:24rpx;overflow:hidden;position:relative}.content .list .list-item .info.data-v-7bd28468{position:absolute;left:20rpx;bottom:14rpx;color:#fff}.content .list .list-item image.data-v-7bd28468{width:100%;height:100%}
+.content.data-v-7bd28468{background-color:#111;min-height:100vh;padding:24rpx}.content .list.data-v-7bd28468{display:flex;justify-content:space-between;flex-wrap:wrap}.content .list .list-item.data-v-7bd28468{border-radius:16rpx;width:340rpx;height:600rpx;background-color:#0d0d0d;margin-right:10rpx;margin-bottom:24rpx;overflow:hidden;position:relative}.content .list .list-item video.data-v-7bd28468{width:100%;height:100%}.content .list .list-item .info.data-v-7bd28468{position:absolute;left:20rpx;bottom:14rpx;color:#fff}.content .list .list-item image.data-v-7bd28468{width:100%;height:100%}

Some files were not shown because too many files changed in this diff