index.vue 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. <template>
  2. <view class="content">
  3. <mescroll-body bottom="0" ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback"
  4. :down="downOption" :up="upOption">
  5. <view class="list">
  6. <view class="list-item" @click="goLive(item)" v-for="(item,index) in list" :key="index">
  7. <image v-if="item.liveImgUrl" :src="item.liveImgUrl"></image>
  8. <!-- 直播流 -->
  9. <!-- <video v-if=" item.liveType == 1 && item.flvHlsUrl" :id="'myVideo_' + item.liveId"
  10. :src="item.flvHlsUrl" :autoplay="false" muted :controls="false" object-fit="contain"
  11. :custom-cache="false" :enable-progress-gesture="false" :show-center-play-btn="false"
  12. :http-cache="false" :muted="true" @error="onVideoError(item, $event)"></video> -->
  13. <!-- 录播视频 -->
  14. <!-- <video v-if=" item.liveType == 2 && item.videoUrl " :id="'myVideo_' + item.liveId"
  15. :src="item.videoUrl" :autoplay="false" muted :controls="false" object-fit="contain"
  16. :custom-cache="false" :enable-progress-gesture="false" :show-center-play-btn="false"
  17. :http-cache="false" :loop="true" :muted="true" @error="onVideoError(item, $event)"
  18. @loadedmetadata="onVideoLoaded(item)"></video> -->
  19. <view class="info">
  20. <text>{{item.liveName}}</text>
  21. </view>
  22. </view>
  23. </view>
  24. </mescroll-body>
  25. </view>
  26. </template>
  27. <script>
  28. import Hls from 'hls.js';
  29. import {
  30. liveList
  31. } from '@/api/list'
  32. import MescrollMixin from "@/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js";
  33. export default {
  34. mixins: [MescrollMixin],
  35. data() {
  36. return {
  37. list: [],
  38. downOption: {
  39. offset: 80,
  40. use: true,
  41. auto: false
  42. },
  43. upOption: {
  44. use: true,
  45. auto: true,
  46. page: {
  47. num: 0,
  48. size: 10
  49. }
  50. },
  51. mescroll: null,
  52. observer: null // 存储IntersectionObserver实例
  53. }
  54. },
  55. onLoad() {
  56. if (!uni.getStorageSync("AppToken")) {
  57. uni.navigateTo({
  58. url: '/pages/auth/login'
  59. });
  60. }
  61. },
  62. onUnload() {
  63. // 清理所有观察器和视频资源
  64. this.cleanupAllVideos();
  65. },
  66. methods: {
  67. // 视频加载错误处理
  68. onVideoError(item, e) {
  69. // console.error('视频加载错误:', e.detail, item);
  70. this.$set(item, '_error', true);
  71. this.pauseVideo(item);
  72. },
  73. // 视频元数据加载完成
  74. onVideoLoaded(item) {
  75. console.log('视频元数据加载完成:', item.liveId);
  76. this.$set(item, '_error', false);
  77. },
  78. // 初始化所有视频观察器
  79. initAllVideoObservers() {
  80. // 先清理旧的
  81. this.cleanupAllVideos();
  82. // 使用setTimeout确保DOM已渲染
  83. setTimeout(() => {
  84. this.list.forEach(item => {
  85. if (item.liveId) {
  86. this.initVideoObserver(item);
  87. }
  88. });
  89. }, 300);
  90. },
  91. // 初始化单个视频的观察器
  92. initVideoObserver(item) {
  93. const videoId = `myVideo_${item.liveId}`;
  94. // 创建交叉观察器(监听视频项是否进入视口)
  95. const observer = uni.createIntersectionObserver(this);
  96. observer.relativeToViewport({
  97. top: 100, // 顶部提前100px触发(优化体验,避免刚进入就播放)
  98. bottom: 100 // 底部延迟100px触发(避免快速滑动时频繁切换)
  99. }).observe(`#${videoId}`, (res) => {
  100. const isInView = res.intersectionRatio > 0; // 是否进入视口(交叉比例>0)
  101. // 2. 联动播放/暂停:进入视口播放,离开暂停
  102. if (isInView) {
  103. this.playVideo(item); // 进入视口:播放视频
  104. } else {
  105. this.pauseVideo(item); // 离开视口:暂停视频
  106. }
  107. });
  108. // 存储观察器引用,便于后续清理
  109. this.$set(item, '_observer', observer);
  110. },
  111. // 播放视频
  112. playVideo(item) {
  113. if ( item._isPlaying || item._error) return;
  114. const videoId = `myVideo_${item.liveId}`;
  115. const isLive = item.liveType == 1;
  116. // 获取video上下文
  117. uni.createSelectorQuery().in(this)
  118. .select(`#${videoId}`)
  119. .fields({
  120. context: true
  121. })
  122. .exec((res) => {
  123. if (res && res[0] && res[0].context) {
  124. const videoContext = res[0].context;
  125. try {
  126. if (isLive) {
  127. // 直播流处理
  128. // #ifdef H5
  129. if (item.flvHlsUrl && item.flvHlsUrl.includes('.m3u8') && Hls.isSupported()) {
  130. this.setupHlsPlayback(item, videoContext);
  131. } else {
  132. videoContext.play();
  133. }
  134. //#else
  135. videoContext.play();
  136. //#endif
  137. } else {
  138. // 录播视频处理 - 添加重试机制
  139. const playAttempt = () => {
  140. videoContext.play().then(() => {
  141. console.log('录播视频播放成功:', item.liveId);
  142. this.$set(item, '_isPlaying', true);
  143. this.$set(item, '_videoContext', videoContext);
  144. }).catch(err => {
  145. console.error('录播视频播放失败:', err);
  146. this.$set(item, '_error', true);
  147. });
  148. };
  149. // 如果视频已加载,直接播放;否则监听加载事件
  150. if (videoContext.duration > 0) {
  151. playAttempt();
  152. } else {
  153. videoContext.onloadedmetadata = playAttempt;
  154. }
  155. }
  156. // 播放成功后标记 _isPlaying=true
  157. this.$set(item, '_isPlaying', true);
  158. this.$set(item, '_videoContext', videoContext);
  159. } catch (err) {
  160. console.error(`播放失败 ${videoId}:`, err);
  161. this.$set(item, '_error', true);
  162. }
  163. }
  164. });
  165. },
  166. // 暂停视频
  167. pauseVideo(item) {
  168. if (!item._isPlaying) return;
  169. if (item._videoContext) {
  170. item._videoContext.pause();
  171. }
  172. // 清理HLS实例(如果是直播且使用了HLS)
  173. // #ifdef H5
  174. if (item.liveType == 1 && item._hlsInstance) {
  175. item._hlsInstance.destroy();
  176. this.$set(item, '_hlsInstance', null);
  177. }
  178. //#endif
  179. this.$set(item, '_isPlaying', false);
  180. },
  181. // H5平台的HLS播放设置
  182. // #ifdef H5
  183. setupHlsPlayback(item, videoElement) {
  184. if (item._hlsInstance) {
  185. item._hlsInstance.destroy();
  186. }
  187. const hls = new Hls({
  188. enableWorker: true,
  189. lowLatencyMode: true,
  190. debug: false
  191. });
  192. hls.attachMedia(videoElement);
  193. hls.loadSource(item.flvHlsUrl);
  194. hls.on(Hls.Events.MANIFEST_PARSED, () => {
  195. videoElement.play();
  196. });
  197. this.$set(item, '_hlsInstance', hls);
  198. },
  199. // #endif
  200. // 清理所有视频资源
  201. cleanupAllVideos() {
  202. this.list.forEach(item => {
  203. this.pauseVideo(item);
  204. // 销毁观察器
  205. if (item._observer) {
  206. item._observer.disconnect();
  207. this.$set(item, '_observer', null);
  208. }
  209. });
  210. },
  211. // mescroll初始化
  212. mescrollInit(mescroll) {
  213. this.mescroll = mescroll;
  214. },
  215. // 下拉刷新回调
  216. downCallback(mescroll) {
  217. this.cleanupAllVideos();
  218. this.list = [];
  219. mescroll.resetUpScroll();
  220. },
  221. // 上拉加载回调
  222. upCallback(mescroll) {
  223. const pageNum = mescroll.num;
  224. const pageSize = mescroll.size;
  225. let data = {
  226. pageSize: pageSize,
  227. pageNum: pageNum,
  228. }
  229. liveList(data).then(res => {
  230. if (res.code == 200) {
  231. let curPageData = res.rows || [];
  232. // console.log("curPageData在这里>>>>", res)
  233. let totalSize = res.total || 0;
  234. // 预处理数据,添加状态字段
  235. curPageData = curPageData.map(item => {
  236. return {
  237. ...item,
  238. _error: false,
  239. _isPlaying: false,
  240. };
  241. });
  242. if (pageNum === 1) {
  243. this.list = [];
  244. }
  245. this.list = this.list.concat(curPageData);
  246. // DOM更新后初始化视频观察器
  247. this.$nextTick(() => {
  248. this.initAllVideoObservers();
  249. });
  250. mescroll.endBySize(curPageData.length, totalSize);
  251. } else {
  252. mescroll.endErr();
  253. uni.showToast({
  254. title: res.msg,
  255. icon: 'none'
  256. });
  257. }
  258. }).catch(err => {
  259. mescroll.endErr();
  260. });
  261. },
  262. goLive(item) {
  263. uni.navigateTo({
  264. url: `/pages/home/living?liveId=${item.liveId}&immediate=true`
  265. });
  266. }
  267. }
  268. }
  269. </script>
  270. <style lang="scss" scoped>
  271. .content {
  272. background-color: #111;
  273. min-height: 100vh;
  274. padding: 24rpx;
  275. .list {
  276. display: flex;
  277. justify-content: space-between;
  278. flex-wrap: wrap;
  279. .list-item {
  280. border-radius: 16rpx;
  281. width: 340rpx;
  282. height: 600rpx;
  283. background-color: #0d0d0d;
  284. margin-bottom: 24rpx;
  285. overflow: hidden;
  286. position: relative;
  287. image {
  288. width: 100%;
  289. height: 100%;
  290. }
  291. video {
  292. width: 100%;
  293. height: 100%;
  294. object-fit: cover;
  295. }
  296. .info {
  297. position: absolute;
  298. left: 20rpx;
  299. bottom: 14rpx;
  300. right: 20rpx;
  301. color: #ffffff;
  302. display: flex;
  303. align-items: center;
  304. .live-badge {
  305. background-color: #e74c3c;
  306. padding: 4rpx 12rpx;
  307. border-radius: 8rpx;
  308. font-size: 20rpx;
  309. margin-right: 12rpx;
  310. }
  311. .record-badge {
  312. background-color: #3498db;
  313. padding: 4rpx 12rpx;
  314. border-radius: 8rpx;
  315. font-size: 20rpx;
  316. margin-right: 12rpx;
  317. }
  318. }
  319. .error-tip {
  320. position: absolute;
  321. top: 50%;
  322. left: 50%;
  323. transform: translate(-50%, -50%);
  324. color: #fff;
  325. background-color: rgba(0, 0, 0, 0.7);
  326. padding: 16rpx 24rpx;
  327. border-radius: 8rpx;
  328. font-size: 24rpx;
  329. }
  330. }
  331. // 使列表项均匀分布
  332. .list-item:nth-child(2n) {
  333. margin-right: 0;
  334. }
  335. }
  336. }
  337. </style>