绿满眶商城微信小程序-uniapp
xiangpei
7 小时以前 a0070a397253470e2adde8f6800ce67814fc4da2
首页视频性能优化:视频上下文释放
2个文件已修改
319 ■■■■■ 已修改文件
api/video.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/tabbar/index/home.vue 315 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
api/video.js
@@ -26,7 +26,7 @@
 * 
 * @param params
 */
 export function getRecommendVideos(params) {
 export async function getRecommendVideos(params) {
  return http.request({
    url: "/lmk/video/recommend",
    method: Method.GET,
@@ -235,4 +235,4 @@
    method: Method.POST,
    needToken: true
  });
}
}
pages/tabbar/index/home.vue
@@ -1,20 +1,26 @@
<template>
  <view class="video-container">
    <!-- 视频列表 -->
    <swiper
      class="video-swiper"
      vertical
      circular
    <swiper
      class="video-swiper"
      vertical
      :current="currentIndex"
      @change="onSwiperChange"
      easing-function="linear"
    >
      <swiper-item v-for="(item, index) in videoList" :key="item.id">
      <swiper-item
        v-for="(item, index) in videoList"
        :key="item.id"
        @touchstart="handleSwiperStart"
        @touchmove="handleSwiperMove"
        @touchend="handleSwiperEnd(item)"
         >
        <view style="width: 100%;height: 100%;" v-if="item.videoContentType === 'video'">
              <!-- 播放按钮(仅当视频暂停时显示) -->
              <view
                class="play-icon"
              <view
                class="play-icon"
                @click="togglePlay(index)"
                v-if="!currentVideoIsPlaying"
                v-show="!currentVideoIsPlaying"
              >
                <image src="/static/video/play.png" style="width: 45px;height: 45px" mode="aspectFit"></image>
              </view>
@@ -22,7 +28,7 @@
                :id="'video'+index"
                :ref="'video'+index"
                :src="item.videoUrl"
                :autoplay="currentIndex === index"
                :autoplay="false"
                :controls="false"
                :loop="true"
                :object-fit="item.objectFit"
@@ -34,10 +40,9 @@
                @click="togglePlay(index)"
                @timeupdate="onTimeUpdate($event)"
                @loadedmetadata="onLoadedMetadata($event)"
              ></video>
              <!-- 自定义控制条 -->
              <view
              <view
                @touchstart="handleTouchStart"
                @touchmove="handleTouchMove"
                @touchend="handleTouchEnd"
@@ -46,11 +51,11 @@
                <view class="process-warp" :style="{ opacity: showProcess ? 1 : 0 }">
                  <!-- 显示当前进度 -->
                  <view class="progress-text">{{ hasPlayTime }}/{{formartDuration}}</view>
                  <view
                    class="progress-bar"
                  <view
                    class="progress-bar"
                    id="progressBar"
                  >
                    <!-- 已填充部分 -->
                    <view class="progress-fill" :style="{ width: progress + '%' }"></view>
                  </view>
@@ -58,10 +63,10 @@
              </view>
        </view>
        <view style="width: 100%; height: 100%;" v-else-if="item.videoContentType === 'img'">
          <uni-swiper-dot
            :info="item.imgs"
            :current="currentImgIndex"
            mode="round"
          <uni-swiper-dot
            :info="item.imgs"
            :current="currentImgIndex"
            mode="round"
            style="width: 100%;height: 100%;display: flex;justify-content: center;align-items: center;"
            :dots-styles="{width: 24, bottom: 24,selectedBackgroundColor: 'green', backgroundColor: 'gray'}"
            >
@@ -69,9 +74,9 @@
              <swiper-item v-for="img in item.imgs" :key="img">
                <view class="swiper-item">
                  <!-- 调整 image 样式,使其居中且按比例缩放 -->
                  <image
                    :src="img"
                    mode="aspectFit"
                  <image
                    :src="img"
                    mode="aspectFit"
                    style="width: 100%; height: 100%; display: block; margin: 0 auto;"
                  ></image>
                </view>
@@ -79,8 +84,8 @@
            </swiper>
          </uni-swiper-dot>
        </view>
        <!-- 悬挂商品链接层 -->
        <view class="goods-link-warp" v-if="item.goodsList.length > 0">
            <view class="goods-link">
@@ -89,7 +94,7 @@
                  <view class="goods-container" @click="jumpToPay(item.id)">
                    <!-- 商品图片 -->
                    <image class="goods-image" :src="goods.thumbnail" mode="aspectFill"></image>
                    <!-- 商品信息 -->
                    <view class="goods-info">
                      <text class="goods-name">{{goods.goodsName}}</text>
@@ -103,8 +108,8 @@
              </swiper>
            </view>
        </view>
        <!-- 视频信息层 -->
        <view class="video-info">
          <view>
@@ -115,7 +120,7 @@
              <text class="video-tag" v-for="(tag, index) in item.tagList" :key="tag.id">#{{tag.tagName}}</text>
          </view>
        </view>
        <!-- 右侧互动按钮 -->
       <view class="action-buttons">
           <view class="avatar-container">
@@ -138,13 +143,13 @@
              <button open-type="share" class="custom-share-btn" :data-obj="item">
                    <text class="iconfont">&#xe602;</text>
                  </button>
          </view>
        </view>
      </swiper-item>
    </swiper>
    <!-- 评论弹窗 -->
    <uni-popup ref="commentPopup" type="bottom" :is-mask-click="true" @maskClick="closeCommentPopup">
      <view class="comment-popup">
@@ -156,23 +161,23 @@
          </view>
          <text class="iconfont close-icon" @click="closeCommentPopup">&#xe675;</text>
        </view>
        <scroll-view class="comment-list" scroll-y :show-scrollbar="false" @scrolltolower="getCommentPage">
          <view v-if="commentLoading" class="loading">
            <uni-load-more status="loading"></uni-load-more>
          </view>
          <view v-else-if="comments.length === 0" class="empty">
            暂无评论,快来发表第一条评论吧~
          </view>
          <view v-else class="comment-item" v-for="(comment, index) in comments" :key="comment.id">
            <view style="display: flex;">
                <image class="comment-avatar" :src="comment.userAvatar || '/static/default-avatar.png'"></image>
                <view class="comment-content">
                  <text class="nickname">{{comment.userNickname}}</text>
                  <text class="content">{{comment.commentContent}}</text>
                  <view style="position: relative;">
                  <view style="position: relative;">
                    <text class="time">{{formatTime(comment.createTime)}}</text>
                    <text @click="openReply(comment)" class="reply-btu time">回复</text>
                    <text v-if="!comment.hasThumbsUp" class="thumbs-up time iconfont" @click="thubmsUp(comment.id, index, null)">&#xe614;<text v-show="comment.thumbsUpNum > 0" class="thumbs-num">{{comment.thumbsUpNum}}</text></text>
@@ -212,19 +217,19 @@
          </view>
        </scroll-view>
        <view class="comment-input-area">
          <input
          <input
            ref="commentInput"
            class="comment-input"
            v-model="commentForm.commentContent"
            :placeholder="commentForm.replyId ? `回复 @${commentForm.replyUserNickname}` : '写下你的评论...'"
            class="comment-input"
            v-model="commentForm.commentContent"
            :placeholder="commentForm.replyId ? `回复 @${commentForm.replyUserNickname}` : '写下你的评论...'"
            placeholder-class="placeholder"
          />
          <button class="submit-btn" @click="submitComment" :disabled="!commentForm.commentContent.trim()">发送</button>
        </view>
      </view>
    </uni-popup>
    <custom-tabbar bgColor="#333333" selected="index" selectedTextColor="#ffffff"></custom-tabbar>
  </view>
</template>
@@ -298,14 +303,20 @@
        isFullScreen: false,
        windowHeight: 0,
        currentIndex: 0, // 当前播放的视频索引
        videoList: [
        ],   // 视频列表数据
        videoList: [],   // 视频列表数据
        videoContexts: [], // 视频上下文对象集合
        videoBufferOffset: 0.1 ,// 视频预加载参数
        videoLiveOffset: 5, // 保留当前视频前后各多少个视频上下文
        touchXY: {  // 监听左滑右滑
            startX: 0,
            endX: 0,
            startY: 0,
            endY: 0
        },
        loading: false,  // 是否正在加载
        videoQuery: {
            pageNumber: 1,
            pageSize: 6,
            pageSize: 10,
            videoFrom: 'recommend'
        }
    }
@@ -316,7 +327,7 @@
         //  this.wxSilentLogin(() => {
            //   this.loadVideos();
         //  })
      // } else {
      // } else {
      //       this.loadVideos();
      // }
      // 如果视频按下暂停后切换页面再回到页面时,只算暂停时间(因为暂停时间和离开页面时间是重复的,只算一个)
@@ -339,13 +350,9 @@
                  saveShareClickRecord({refId: option.videoId, shareUserId: option.userId})
              }
          })
      } else {
      } else {
        this.loadVideos();
      }
  },
  onReady() {
    // 初始化视频上下文
    this.initVideoContexts();
  },
  onShareAppMessage(e) {
    const userInfo = storage.getUserInfo();
@@ -525,11 +532,11 @@
          const date = new Date(time);
          const now = new Date();
          const diff = Math.floor((now - date) / 1000); // 秒
          if (diff < 60) return '刚刚';
          if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`;
          if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`;
          return `${date.getMonth() + 1}月${date.getDate()}日`;
        },
        // 提交评论
@@ -545,7 +552,7 @@
          addVideoComment(this.commentForm).then(res => {
              if(res.data.code === 200) {
                  this.resetCommentForm()
                  // 如果是评论别人的回复,那么就将这个发布到replies里面
                  if(res.data.data.replyId) {
                      for (const [index, item] of this.comments.entries()) {
@@ -657,17 +664,60 @@
      },
    // 初始化视频上下文
    initVideoContexts() {
      this.videoContexts = this.videoList.map((_, index) => {
          let videoContent = uni.createVideoContext(`video${index}`, this);
          return videoContent;
      });
      const start = Math.max(0, this.currentIndex - this.videoLiveOffset);
      const end = Math.min(this.currentIndex + this.videoLiveOffset, this.videoList.length - 1);
      let contextsLength = this.videoContexts.length;
      if (contextsLength === 0) {
          // 第一次初始化
          for (let i = 0; i < this.videoList.length; i++) {
            if (i < start || i > end) {
                this.videoContexts.push(null)
            } else {
                let videoContent = uni.createVideoContext(`video${i}`, this);
                videoContent.seek(this.videoBufferOffset);
                videoContent.pause();
                this.videoContexts.push(videoContent);
            }
          }
      } else {
         for (let i = 0; i < this.videoList.length; i++) {
             contextsLength = this.videoContexts.length
            if (contextsLength - 1 >= i) {
                // 如果已经是null了就不用管,因为视频加载只会在后面push,前面已经设置为null则无需处理
                if (this.videoContexts[i] == null) {
                    continue
                }
                // 超出可视化范围的视频直接释放资源,并置为null
                if (i < start || i > end) {
                    if (this.videoContexts[i]) {
                        this.videoContexts[i].stop();
                        this.videoContexts[i] = null
                    }
                }
            } else {
                if (i < start || i > end) {
                    this.videoContexts.push(null);
                } else {
                    let videoContent = uni.createVideoContext(`video${i}`, this);
                    videoContent.seek(this.videoBufferOffset);
                    videoContent.pause();
                    this.videoContexts.push(videoContent);
                }
            }
         }
      }
      // 将当前视频设置为播放
      if (this.videoContexts[this.currentIndex]) {
          this.videoContexts[this.currentIndex].play()
      }
    },
    // 加载视频数据
    async loadVideos() {
      if (this.loading || this.videoNoMore) return;
      this.loading = true;
      getRecommendVideos(this.videoQuery).then(res => {
          console.log(res, "视频数据");
          if (this.videoQuery.pageNumber === 1) {
@@ -689,10 +739,10 @@
              return;
          }
          this.videoQuery.pageNumber++;
      })
    },
    // 滑动切换视频
    onSwiperChange(e) {
        // 如果视频处于暂停状态往下刷视频,那么需要再计算一次暂停时间
@@ -711,17 +761,82 @@
        if (this.videoContexts[oldIndex]) {
            this.videoContexts[oldIndex].pause();
        }
        this.startPauseTime = 0;
        // 设置当前播放视频的总时长
        this.duration = this.videoList[this.currentIndex].videoDuration;
        this.formartDuration = this.sliderFormatTime(this.duration);
        // 播放当前视频
        if (this.videoContexts[this.currentIndex]) {
            this.videoContexts[this.currentIndex].play();
        }
        // 设置当前播放视频的总时长
        this.duration = this.videoList[this.currentIndex].videoDuration;
        this.formartDuration = this.sliderFormatTime(this.duration);
        this.clearVideoContext()
    },
    // 清除超出视频可视化区域的视频上下文
    async clearVideoContext() {
        // 对超出可视化区域的视频上下文做销毁处理
        const start = Math.max(0, this.currentIndex - this.videoLiveOffset);
        const end = Math.min(this.currentIndex + this.videoLiveOffset, this.videoList.length - 1);
        for (let i = 0; i < this.videoContexts.length; i++) {
            if (i < start || i > end) {
                if (this.videoContexts[i]) {
                    this.videoContexts[i].stop();
                    this.videoContexts[i] = null
                }
            } else {
                if (this.videoContexts[i] == null) {
                    let videoContent = uni.createVideoContext(`video${i}`, this);
                    videoContent.seek(this.videoBufferOffset);
                    videoContent.pause();
                    this.videoContexts[i] = videoContent;
                }
            }
        }
        // 如果剩余视频不足,触发请求获取更多视频
        if (this.videoList.length - 1 < this.currentIndex + this.videoLiveOffset) {
            this.loadVideos()
        }
    },
    // 开始触摸
    handleSwiperStart(e) {
        console.log("开始触摸", e);
        this.touchXY.startX = e.touches[0].pageX
        this.touchXY.startY = e.touches[0].pageY
    },
    // 触摸中
    handleSwiperMove(e) {
        console.log("触摸中", e);
        this.touchXY.endX = e.touches[0].pageX
        this.touchXY.endY = e.touches[0].pageY
    },
    // 结束触摸
    handleSwiperEnd(item) {
        const diffX = this.touchXY.endX - this.touchXY.startX
        const diffY = this.touchXY.endY - this.touchXY.startY
        // 判断是否是横向滑动(X轴变化大于Y轴变化)
        if (Math.abs(diffX) > Math.abs(diffY)) {
          if (diffX > 0) {
            console.log('右滑')
            if (item.goodsList && item.goodsList.length > 0) {
                this.jumpToPay(item.id)
            }
          } else {
            console.log('左滑')
          }
        }
        // 重置坐标
        this.touchXY = {
            startX: 0,
            endX: 0,
            startY: 0,
            endY: 0
        }
    },
    // 收藏/取消收藏
    toggleCollect(item, index) {
      let data = {
@@ -778,9 +893,9 @@
            const duration = Date.now() - this.startPauseTime
            this.totalPauseTime += duration
        }
    },
    // 视频暂停事件
    onPause(index) {
        console.log(index, "触发暂停");
@@ -796,11 +911,11 @@
    onEnded(index) {
      // this.currentVideoIsPlaying = false;
    },
    // 记录播放时长
    onTimeUpdate(e) {
        this.playRecord.playAt = e.detail.currentTime;
        this.currentTime = e.detail.currentTime;
        this.progress = (e.detail.currentTime / this.duration) * 100
    },
@@ -814,7 +929,7 @@
      this.videoContexts[this.currentIndex].pause()
      // this.updateProgress(e);
    },
    // 触摸移动
    handleTouchMove(e) {
      if (!this.isDragging || !this.barWidth) return;
@@ -822,7 +937,7 @@
      this.videoContexts[this.currentIndex].pause()
      this.updateProgress(e);
    },
    // 触摸结束
    handleTouchEnd() {
      this.isDragging = false;
@@ -833,24 +948,24 @@
          this.showProcess = false;
        }, 1000);
    },
    // 更新进度
    updateProgress(e) {
        // 获取当前触摸点X坐标
        const currentX = e.touches[0].pageX;
        // 计算滑动距离(像素)
        const deltaX = currentX - this.startX;
        // 将像素距离转换为进度增量
        const deltaProgress = (deltaX / this.barWidth) * 100;
        console.log("进度增量", deltaProgress);
        // 计算新进度 = 开始时的进度 + 滑动增量
        let newProgress = this.startProgress + deltaProgress;
        // 限制范围在0-100之间
        newProgress = Math.max(0, Math.min(100, newProgress));
        this.progress = newProgress;
    },
    // 获取视频总时长
@@ -862,7 +977,7 @@
    // 保存播放记录
    async savePlayRecord() {
        console.log(Date.now(), this.playRecord.startPlayTime, this.totalHidenTime);
        const data = {
            videoId: this.playRecord.videoId,
            viewDuration: Date.now() - this.playRecord.startPlayTime - this.totalHidenTime - this.totalPauseTime,
@@ -891,12 +1006,12 @@
      height: 100vh;
      background-color: #000;
    }
    .video-swiper {
      width: 100%;
      height: calc(100% - 50px);
    }
    .video-item {
      width: 100%;
      height: 100%;
@@ -912,7 +1027,7 @@
      z-index: 10;
      opacity: 0.6;
    }
    .video-info {
      width: 70%;
      position: absolute;
@@ -922,7 +1037,7 @@
      z-index: 10;
      letter-spacing: 1px;
    }
    .action-buttons {
      position: absolute;
      right: 20px;
@@ -932,7 +1047,7 @@
      align-items: center;
      z-index: 10;
    }
    .action-item {
      margin-bottom: 18px;
      display: flex;
@@ -962,7 +1077,7 @@
      bottom: 0;  /* 定位到底部 */
      left: 50%;  /* 水平居中开始位置 */
      transform: translate(-50%, 50%); /* 水平居中并向下移动50% */
      width: 18px;  /* 图标大小 */
      height: 18px;
      background-color: #FF5A5F; /* 图标背景色 */
@@ -997,27 +1112,27 @@
      border-radius: 12rpx;
      box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
    }
    .goods-container {
      width: 100%;
      display: flex;
      align-items: center;
    }
    .goods-image {
      width: 120rpx;
      height: 120rpx;
      border-radius: 8rpx;
      margin-right: 20rpx;
    }
    .goods-info {
      flex: 1;
      display: flex;
      flex-direction: column;
      justify-content: center;
    }
    .goods-name {
      font-size: 28rpx;
      color: #333;
@@ -1030,31 +1145,31 @@
      white-space: nowrap;
      text-overflow: ellipsis;
    }
    .price-section {
      display: flex;
      align-items: center;
      margin-bottom: 6rpx;
    }
    .current-price {
      font-size: 32rpx;
      color: #ff2e4d;
      font-weight: bold;
      margin-right: 12rpx;
    }
    .original-price {
      font-size: 28rpx;
      color: #999;
      text-decoration: line-through;
    }
    .sales-count {
      font-size: 22rpx;
      color: #999;
    }
    .buy-button {
      background: linear-gradient(to right, #ff5a5f, #ff2e4d);
      color: white;
@@ -1192,16 +1307,16 @@
        align-items: center;
        height: 40rpx;
    }
    .reply-item {
      display: flex;
      margin-bottom: 20rpx;
    }
    .reply-content {
      flex: 1;
    }
    .reply-to {
      color: #576b95;
      margin: 0 10rpx;
@@ -1213,7 +1328,7 @@
      font-size: 28rpx;
      color: #333;
    }
    .cancel-reply {
      margin-left: 20rpx;
      color: #576b95;
@@ -1262,7 +1377,7 @@
      bottom: 0;
      width: 100%;
    }
    .progress-bar {
      position: relative;
      width: 100%;
@@ -1270,7 +1385,7 @@
      background-color: #eee;
      overflow: hidden;
    }
    .progress-fill {
      position: absolute;
      left: 0;
@@ -1312,4 +1427,4 @@
    .custom-share-btn::after {
      border: none;
    }
</style>
</style>