123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436 |
- <template>
- <view class=" content ">
- <mescroll-body bottom="0" ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback"
- :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> -->
- <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>
- </view>
- </view>
- </mescroll-body>
- </view>
- </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";
- export default {
- mixins: [MescrollMixin], // 使用mixin
- data() {
- return {
- hlsPlayer: null, // HLS播放器实例,
- // 统一直播/录播类型标识
- LIVE_TYPE: {
- LIVE: 1, // 直播
- RECORD: 2 // 录播
- },
- list: [],
- liveId: null, // mescroll配置
- downOption: {
- offset: 80,
- use: true,
- auto: false // 是否在初始化后自动执行下拉回调
- },
- upOption: {
- use: true,
- auto: true, // 是否在初始化时自动执行上拉回调
- page: {
- num: 0, // 当前页码
- size: 10 // 每页数据条数
- }
- },
- mescroll: null // mescroll实例
- }
- },
- onLoad() {
- if (!uni.getStorageSync("AppToken")) {
- uni.navigateTo({
- url: '/pages/auth/login'
- });
- } // 销毁HLS播放器(如果使用了hls.js)
- if (this.hlsPlayer) {
- this.hlsPlayer.destroy();
- this.hlsPlayer = null;
- }
- },
- 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(); // 重置上拉加载的页码
- // 3. 可选:下拉后自动加载第一页(符合用户预期)
- this.mescroll.triggerUpScroll();
- },
- // 上拉加载回调
- upCallback(mescroll) {
- const pageNum = mescroll.num;
- const pageSize = mescroll.size;
- let data = {
- pageSize: pageSize,
- page: pageNum,
- }
- liveList(data).then(res => {
- if (res.code == 200) {
- let curPageData = res.rows || [];
- let totalSize = res.total || 0;
- if (pageNum === 1) {
- this.list = [];
- }
- // 追加新数据
- this.list = this.list.concat(curPageData);
- // 关键修改:用 $nextTick 等待DOM渲染完成
- this.$nextTick(() => {
- curPageData.forEach(item => {
- this.initVideoPlayer(item); // 此时视频节点已存在
- });
- });
- mescroll.endBySize(curPageData.length, totalSize);
- } else {
- mescroll.endErr();
- uni.showToast({
- title: res.msg,
- icon: 'none'
- });
- }
- }).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,
- // page_size: 10,
- // };
- // uni.showLoading({
- // title: "处理中..."
- // });
- // liveList(data)
- // .then(res => {
- // if (res.code == 200) {
- // this.list = res.rows; // 直接赋值给 this.list
- // console.log("list>>", this.list); // ✅ 打印已定义的 this.list
- // } else {
- // uni.showToast({
- // title: res.msg,
- // icon: 'none'
- // });
- // }
- // })
- // .catch(rej => {
- // console.log("请求失败:", JSON.stringify(rej));
- // })
- // .finally(() => {
- // uni.hideLoading();
- // });
- // }
- }
- }
- </script>
- <style lang="scss" scoped>
- .content {
- background-color: #111;
- min-height: 100vh;
- padding: 24rpx;
- .list {
- display: flex;
- justify-content: space-between;
- flex-wrap: wrap;
- .list-item {
- border-radius: 16rpx;
- width: 340rpx;
- height: 600rpx;
- background-color: #0d0d0d;
- margin-right: 10rpx;
- margin-bottom: 24rpx;
- overflow: hidden;
- position: relative;
- video {
- width: 100%;
- height: 100%;
- /* 视频填满列表项,确保可视区域判断准确 */
- }
- .info {
- position: absolute;
- left: 20rpx;
- bottom: 14rpx;
- color: #ffffff;
- }
- image {
- width: 100%;
- height: 100%;
- }
- }
- }
- }
- </style>
|