更新记录

1.7.6(2026-10-13)

性能优化

  • 解决多路并发争抢硬件解码器导致的滑动卡顿
  • isDragging 只在开始/结束拖动时变化,避免 scroll 高频回调反复触发模板更新

预加载重构

  • 预加载串行执行,一次只预热当前项前后各 1 条,按距离由近到远排队
  • 新增加载失败记录与超时兜底,避免个别视频反复重试拖慢列表

1.7.5(2026-08-16)

  • 细节优化

1.7.4(2025-06-18)

优化第一个视频的预览速度

查看更多

平台兼容性

uni-app(5.08)

Vue2 Vue3 Vue3插件版本 Chrome Safari app-vue app-nvue Android Android插件版本 iOS iOS插件版本 鸿蒙
- √ 1.0.0 - - - - 5.0 1.3 12 1.3 -
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
- - - - - - - - - - - -

仿抖音快手刷视频插件组件文档

注意:只能用于nvue页面

注意:由于uniapp项目nvue不能加密运行,所以普通授权版运行不了,想看预览效果的只能扫下面二维码体验,觉得适合再下单哈

注意:ios端只能联系作者打包体验(需提供测试机型打包证书)

注意:源码运行前需要在项目manifest.json文件勾选video模块,否则运行后显示黑屏

使用说明

  • 将插件下载到项目uni_modules目录下
  • 新建一个nvue页面引入组件
  • 参考下面页面使用示例代码即可

扫码体验Android安装包效果

Android体验二维码

已测试机型

系统 机型 系统版本 测试情况
Android 红米K60 android14 运行丝滑
ios IphoneXR ios15 运行丝滑

组件参数说明

参数 类型 说明
videoList Array 数据源
closeRefresh Boolean 关闭刷新
enbleRefresh Boolean 开启下拉刷新效果
closeRefresh Boolean 关闭刷新
deleHeight number 要减去的高度,例如页面有tabbar,需要减去相应的高度
isAutoplay Boolean 开启自动播放 ,播完当前视频会自动播放下一条视频
toolsBottom String、Number 右侧工具栏距离底部的距离,默认55px
showFrameImage Boolean 显示帧画面,配合frameImageExpRule使用,用于拖动进度显示帧图片效果
frameImageExpRule String 获取帧画面的规则,此参数会拼接在视频链接后面,例如xxx.mp4?+传入的规则
posterRule String 当 poster、cover、coverUrl 都为空时,拼接到 playurl 后生成封面地址,默认 ?vframe/jpg/offset/1/

组件默认只渲染当前项前后各 2 条(共 5 个)原生 video 实例,避免快速滑动时反复创建销毁播放器;但同一时刻只有当前项真正在解码,其余全部处于暂停状态,因此同时解码的始终只有 1 路。预加载只预热当前项前后各 1 条,串行执行,不会出现多路并发争抢硬件解码器。建议接口优先返回 poster,也支持使用 cover 或 coverUrl 作为封面字段。

性能说明(1.8.0 起)

优化点 说明
单路解码 窗口内只保留原生实例,非当前项一律暂停,同时解码数恒为 1
串行预加载 一次只预热 1 条,按距离由近到远排队,收到 canplay 立即暂停并保留缓冲
不再清空缓冲 预加载结束不再 seek(0),用户切过去即可续播,切换更快
立即切换 滑动停止时立即确定当前项并起播,去掉了原来 300ms 的等待
触底加载 改用 list 的 loadmoreoffset + @loadmore 触发翻页,更可靠
拖动减负 拖动进度条时只更新进度与时间文本;帧图在手指停住约 130ms 后才取一次,不再每个时间点都发图片请求
无定时器泄漏 所有定时器与在途预加载都在页面隐藏/组件卸载时统一清理
时长预取 预加载期间顺带抓取视频时长,进度条总时长不必等播放后才显示
内存与解码友好 非当前项只保留实例不解码;iOS 更保守(滑动不扩窗、默认不预加载);反复缓冲时自动降载

可调参数(源码内常量)

常量 默认 说明
VIDEO_WINDOW 2 静止时当前项前后各保留几个原生 video 实例。调大更平滑、更占内存;低端机可设为 1
SCROLL_WINDOW 3 Android 滑动期间临时扩大的窗口半径,让前方几屏提前建好实例以压缩黑屏
SCROLL_WINDOW_IOS 2 iOS 滑动期间窗口半径。iOS 硬解与内存更紧张,默认不扩窗
WINDOW_SHRINK_DELAY 500 滚动停止后多久把窗口收回稳态大小(ms)
PRELOAD_AHEAD 1 预加载预热范围(前后各几条)
PRELOAD_TIMEOUT 2500 预加载连接超时(ms),超时后释放调度槽
WAITING_LIMIT / WAITING_WINDOW 3 / 8000 自适应降载阈值:窗口期内缓冲达到 3 次即暂停预加载
DEGRADE_RECOVER_DELAY 15000 降载后多久尝试恢复预加载(ms)
ROTATE_STEP 50 唱片旋转定时器间隔(ms)。nvue 不支持 CSS animation(官方样式表未收录 animation),只能用定时器驱动 transform,故降频处理
DRAG_THROTTLE 33 拖动进度条的重绘节流(ms),约 30fps
FRAME_DEBOUNCE 130 拖动时取帧图的防抖时间(ms)
LOAD_MORE_OFFSET 900 触底加载提前量(px)

页面使用示例

  • VUE2(需要联系作者提供) 使用方式
<template>
    <view class="page" style="height:100%;background-color:#000;">
        <xwqNvVideo v-if="isShow" ref="nvVideo" :closeRefresh.sync="closeRefresh" :videoList="videoList" :deleHeight="50" :isAutoplay="false" @loadNext="loadNext"
            @toolsFn="toolsFn" @onRefresh="onRefresh">
            //自定义右边内容
            <!-- <template #default="slotProps">
                <view class="v-user-avart-wrap">
                    slotProps.item即是数组的item数据
                    <image class="v-user-avart" :src="`${slotProps.item.avatar}`" />
                </view>
            </template> -->
            <!-- <template #slotLeft>
                <view class="left-wrap">
                    <text style="color: #fff;">自定义左边内容区</text>
                </view>
            </template> -->
        </xwqNvVideo>
    </view>
</template>

<script>
    import {
        onLoad,
        onShow,
        onHide,
        onReady
    } from "@dcloudio/uni-app";
    import xwqNvVideo from '@/uni_modules/xwq-app-pl-video/components/xwq-app-pl-video/xwq-app-pl-video.vue';
    export default {
        components: {
            xwqNvVideo
        },
        data() {
            return {
                videoList: [],
                isShow: true,
                finished: false,
                closeRefresh: false,
                params: {
                    pagesize: 10,
                    page: 1
                }
            }
        },
        onLoad() {
            this.isShow = true;
            this.get();
        },
        onHide() {
            //暂停视频
            this.$refs['nvVideo'].stop();
        },
        onShow() {
            //播放视频
            if(this.$refs['nvVideo']){
                this.$refs['nvVideo'].play();
            }
        },
        methods: {
            //请求下一页数据
            loadNext() {
                if (this.finished) return;
                this.params.page += 1;
                this.get();
            },
            //请求数据
            get() {
                // let cacheList = uni.getStorageSync('list');

                // if (cacheList) {
                //  setTimeout(() => {
                //      this.videoList = cacheList;
                //      console.log('缓存数据=====', this.videoList);
                //  }, 1000)

                //  return
                // }
                console.log('请求参数=====', this.params);
                const requestDb = uniCloud.importObject('videoList', { customUI: false });
                requestDb.getVideo(this.params).then(res => {
                    console.log(res, '----------')
                    if (res.code == 0) {

                        if (res.detail.length < this.params.pagesize) {
                            this.finished = true;
                        }
                        if (res.detail.length == 0) return;
                        if (this.videoList.length === 0) {
                            this.videoList = res.detail;

                        } else {
                            this.videoList = [...this.videoList, ...res.detail];
                        }

                        // uni.setStorageSync('list', res.detail);
                        // console.log('视频请求结果====', this.videoList)
                    }
                }).catch(err => {
                    console.error(err)
                    uni.showToast({
                        title: '接口异常,请稍后再试!',
                        icon: 'none'
                    })
                })

            },
            //右侧菜单点击事件回调
            toolsFn(item) {
                console.log('点击的参数=====', item);
            },

            //下拉刷新回调
            onRefresh() {
                console.log('正在下拉刷新===');
                setTimeout(() => {
                    this.closeRefresh = true;
                }, 1500)
            }
        }
    }
</script>

<style>
    .v-user-avart {
        width: 100rpx;
        height: 100rpx;
        border-radius: 100rpx;
    }

    .v-user-avart-wrap {
        width: 110rpx;
        height: 110rpx;
        border: 2px solid #fff;
        border-radius: 110rpx;
        justify-content: center;
        align-items: center;
        background-color: #fff;
        margin-bottom: 20px;
        position: relative;
        /* overflow: visible; */
    }

    .v-user-follow {
        width: 20px;
        height: 20px;
        position: absolute;
        left: 30%;
        top: 50px;
        transform: translateX(-50%);
    }

    /* 以下为光盘的样式 */
    .video-disk {
        /* // position: relative; */
        /* // background-color: #ff0000; */
    }

    .music-cover {
        position: relative;
        width: 80rpx;
        height: 80rpx;
        box-shadow: 0 2rpx 4rpx 0 rgba(0, 0, 0, 0.2);
        z-index: 2;
        /* box-sizing: border-box; */
        transform: rotate(0deg);
        transition-property: transform;
        transition-duration: 5000s;
        transition-timing-function: linear;
    }

    .music-disk {
        width: 80rpx;
        height: 80rpx;
        border-radius: 80rpx;
    }

    .music-disk-user-avart {
        position: absolute;
        width: 40rpx;
        height: 40rpx;
        border-radius: 40rpx;
        margin-left: 20rpx;
        margin-top: 20rpx;
    }

    .music-logo {
        width: 20px;
        height: 20px;
        position: absolute;
        opacity: 0.8;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
    }

    .control-item {
        /* border: none; */
        border-color: transparent;
        padding: 0;
        margin: 0;
        justify-content: center;
        align-items: center;
        margin-bottom: 20px;
    }

    .control-text {
        font-size: 32rpx;
        color: #fff;
        font-weight: 700;
    }

    .icon-style {
        width: 80rpx;
        height: 80rpx;
    }
</style>
  • VUE3 使用方式
<template>
<view class="page" style="height:100%;background-color:#000;">
    <xwqAppPlVideo v-if="isShow" :videoList="videoList" :deleHeight="0" :isAutoplay="false"
        v-model:closeRefresh="closeRefresh" @loadNext="loadNext" @toolsFn="toolsFn" @onRefresh="onRefresh">
        //可以自定义右侧图标样式,slotProps.item即是组件抛出的对应视频数据
        // <!-- <template #default="slotProps">
        //  <view class="v-user-avart-wrap">
        //      slotProps.item即是数组的item数据
        //      <image class="v-user-avart" :src="`${slotProps.item.avatar}`" />
        //  </view>
        // </template> -->
        <!-- <template #slotLeft>
            <view class="left-wrap">
                <text style="color: #fff;">自定义左边内容区</text>
            </view>
        </template> -->
    </xwqAppPlVideo>
</view>
</template>

<script setup lang="ts">
    import { ref, ComponentInternalInstance, getCurrentInstance, watch } from 'vue';
    import {
        onLoad,
        onShow,
        onHide,
        onReady
    } from "@dcloudio/uni-app";
    import xwqAppPlVideo from '@/uni_modules/xwq-app-pl-video/components/xwq-app-pl-video/xwq-app-pl-video.vue';

    const isShow = ref(false);
    let params = {
        pagesize: 10,
        page: 1
    };
    let finished = false;
    const closeRefresh = ref(false);
    const videoList = ref([]);

    onLoad(() => {
        isShow.value = true;
        get();
    })

    //请求数据
    const get = () => {
        console.log('请求参数====', params);
        const requestDb = uniCloud.importObject('videoList', { customUI: false });
        requestDb.getVideo(params).then(res => {
            console.log(res, '----------')
            if (res.code == 0) {

                if (res.detail.length < params.pagesize) {
                    finished = true;
                }
                if (res.detail.length == 0) return;
                if (videoList.value.length === 0) {
                    videoList.value = res.detail;

                } else {
                    videoList.value = [...videoList.value, ...res.detail];
                }
                console.log('视频请求结果====', videoList.value)
            }
        }).catch(err => {
            console.error(err)
            uni.showToast({
                title: '接口异常,请稍后再试!',
                icon: 'none'
            })
        })

    };
    //请求下一页
    const loadNext = () => {
        if (finished) return;
        ++params.page;

        get();
    };

    //右侧工具栏
    const toolsFn = (item) => {
        console.log('点击的参数=====', item);
    };

    //下拉刷新回调
    const onRefresh = () => {
        console.log('正在下拉刷新===');
        setTimeout(() => {
            closeRefresh.value = true;
        }, 1500)
    }
</script>

<style>
    .v-user-avart {
        width: 100rpx;
        height: 100rpx;
        border-radius: 100rpx;
    }

    .v-user-avart-wrap {
        width: 110rpx;
        height: 110rpx;
        border: 2px solid #fff;
        border-radius: 110rpx;
        justify-content: center;
        align-items: center;
        background-color: #fff;
        margin-bottom: 20px;
        position: relative;
        /* overflow: visible; */
    }

    .v-user-follow {
        width: 20px;
        height: 20px;
        position: absolute;
        left: 30%;
        top: 50px;
        transform: translateX(-50%);
    }

    /* 以下为光盘的样式 */
    .video-disk {
        /* // position: relative; */
        /* // background-color: #ff0000; */
    }

    .music-cover {
        position: relative;
        width: 80rpx;
        height: 80rpx;
        box-shadow: 0 2rpx 4rpx 0 rgba(0, 0, 0, 0.2);
        z-index: 2;
        /* box-sizing: border-box; */
        transform: rotate(0deg);
        transition-property: transform;
        transition-duration: 5000s;
        transition-timing-function: linear;
    }

    .music-disk {
        width: 80rpx;
        height: 80rpx;
        border-radius: 80rpx;
    }

    .music-disk-user-avart {
        position: absolute;
        width: 40rpx;
        height: 40rpx;
        border-radius: 40rpx;
        margin-left: 20rpx;
        margin-top: 20rpx;
    }

    .music-logo {
        width: 20px;
        height: 20px;
        position: absolute;
        opacity: 0.8;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
    }

    .control-item {
        /* border: none; */
        border-color: transparent;
        padding: 0;
        margin: 0;
        justify-content: center;
        align-items: center;
        margin-bottom: 20px;
    }

    .control-text {
        font-size: 32rpx;
        color: #fff;
        font-weight: 700;
    }

    .icon-style {
        width: 80rpx;
        height: 80rpx;
    }
</style>

接口数据格式


[
    {
        "userId": "666dd802f366f40a8b9a4dd4",//用户ID
        "avatar": "https://env-00jxgwsep3px.normal.cloudstatic.cn/snsPro/image/1728312053918.jpg",//头像
        "nickname": "追风少女心💓( ˘ ³˘)💋",//昵称
        "isfollow": false,
        "videoId": "672b6be8e4ca03e4581a0063",//视频ID
        "playurl": "xxxxxxxxxx/mu38_video/1730898885724/compress_video_1125321996.m3u8",//视频链接
        "poster": "xxxxxxxxxx/mu38_video/1730898885724/compress_video_1125321996.m3u8?vframe/jpg/offset/1/&e=1732097696&token=IAM-C1JVXCM70fL6bT80pA5w1YLPCXa7xisCoyR3eVhr:FZRg1qngVIo8BPLTvFObPNUtgAA=",
        "vduser": "追风少女心💓( ˘ ³˘)💋",//视频用户
        "vdtitle": "仿抖音视频仿抖音视频仿抖音视频仿抖音视频仿抖音视频仿抖音视频",//视频标题
        "loop": true,//循环播放
        "duration": 29970,//视频时长
        "playIng": false,//播放状态
        "muted": false,//是否静音
        "likeSum": 0,//视频被点赞数量
        "isLiked": false,//当前视频是否已点赞
        "commemtSum": 0,//评论的数量
        "shareSum": 0,//被分享的数量
        "imgList":[],//图片列表,当类型为图片类型2时展示图片内容
        "type": "1" //数据类型,1-视频,2-图片
    },
    {
        "userId": "666dd802f366f40a8b9a4dd4",
        "avatar": "https://env-00jxgwsep3px.normal.cloudstatic.cn/snsPro/image/1728312053918.jpg",
        "nickname": "追风少女心💓( ˘ ³˘)💋",
        "isfollow": false,
        "videoId": "672c903442a21686318cd4ad",
        "playurl": "xxxxxxxxxx/mu38_video/1730973720501/compress_video_1200159408.m3u8",
        "poster": "xxxxxxxxxx/mu38_video/1730973720501/compress_video_1200159408.m3u8?vframe/jpg/offset/1/&e=1732097696&token=IAM-C1JVXCM70fL6bT80pA5w1YLPCXa7xisCoyR3eVhr:inBQHVoZ_y-v_QO2-npeVpSvzvY=",
        "vduser": "追风少女心💓( ˘ ³˘)💋",
        "vdtitle": "仿抖音视频仿抖音视频仿抖音视频仿抖音视频仿抖音视频仿抖音视频",
        "loop": true,
        "duration": 9100,
        "playIng": false,
        "muted": false,
        "likeSum": 0,
        "isLiked": false,
        "commemtSum": 0,
        "shareSum": 0,
        "imgList":[
            "https://mp-f8c14aa5-9ea5-40cf-b7a4-c7319b387cde.cdn.bspapp.com/003.jpg",
            "https://mp-f8c14aa5-9ea5-40cf-b7a4-c7319b387cde.cdn.bspapp.com/004.jpg",
            "https://mp-f8c14aa5-9ea5-40cf-b7a4-c7319b387cde.cdn.bspapp.com/005.jpg"
        ],
        "type": "2"
    },
    {
        "userId": "666dd802f366f40a8b9a4dd4",
        "avatar": "https://env-00jxgwsep3px.normal.cloudstatic.cn/snsPro/image/1728312053918.jpg",
        "nickname": "追风少女心💓( ˘ ³˘)💋",
        "isfollow": false,
        "videoId": "672c911ebaea7915f45e1b09",
        "playurl": "xxxxxxxxxx/mu38_video/1730973966319/compress_video_1200405117.m3u8",
        "poster": "xxxxxxxxxx/mu38_video/1730973966319/compress_video_1200405117.m3u8?vframe/jpg/offset/1/&e=1732097696&token=IAM-C1JVXCM70fL6bT80pA5w1YLPCXa7xisCoyR3eVhr:RJvmNrC5LKkD_eWUT1o3Tid1WWI=",
        "vduser": "追风少女心💓( ˘ ³˘)💋",
        "vdtitle": "仿抖音视频仿抖音视频仿抖音视频仿抖音视频仿抖音视频仿抖音视频",
        "loop": true,
        "duration": 14280,
        "playIng": false,
        "muted": false,
        "likeSum": 0,
        "isLiked": false,
        "commemtSum": 0,
        "shareSum": 0,
        "imgList":[],
        "type": "1"
    },
    {
        "userId": "666dd802f366f40a8b9a4dd4",
        "avatar": "https://env-00jxgwsep3px.normal.cloudstatic.cn/snsPro/image/1728312053918.jpg",
        "nickname": "追风少女心💓( ˘ ³˘)💋",
        "isfollow": false,
        "videoId": "672ca13b29d183956332a5b2",
        "playurl": "xxxxxxxxxx/mu38_video/1730978068419/compress_video_1204507162.m3u8",
        "poster": "xxxxxxxxxx/mu38_video/1730978068419/compress_video_1204507162.m3u8?vframe/jpg/offset/1/&e=1732097696&token=IAM-C1JVXCM70fL6bT80pA5w1YLPCXa7xisCoyR3eVhr:9ZeeMpwqwo7SLJhoBeV9fCsnwSY=",
        "vduser": "追风少女心💓( ˘ ³˘)💋",
        "vdtitle": "仿抖音视频仿抖音视频仿抖音视频仿抖音视频仿抖音视频仿抖音视频",
        "loop": true,
        "duration": 10940,
        "playIng": false,
        "muted": false,
        "likeSum": 0,
        "isLiked": false,
        "commemtSum": 0,
        "shareSum": 0,
        "imgList":[],
        "type": "1"
    },
    {
        "userId": "666dd802f366f40a8b9a4dd4",
        "avatar": "https://env-00jxgwsep3px.normal.cloudstatic.cn/snsPro/image/1728312053918.jpg",
        "nickname": "追风少女心💓( ˘ ³˘)💋",
        "isfollow": false,
        "videoId": "672ca15529d183956332a64b",
        "playurl": "xxxxxxxxxx/mu38_video/1730978123397/compress_video_1204562476.m3u8",
        "poster": "xxxxxxxxxx/mu38_video/1730978123397/compress_video_1204562476.m3u8?vframe/jpg/offset/1/&e=1732097696&token=IAM-C1JVXCM70fL6bT80pA5w1YLPCXa7xisCoyR3eVhr:iVxxdgS871qGS2JgoFXN03nNjfQ=",
        "vduser": "追风少女心💓( ˘ ³˘)💋",
        "vdtitle": "仿抖音视频仿抖音视频仿抖音视频仿抖音视频仿抖音视频仿抖音视频",
        "loop": true,
        "duration": 9450,
        "playIng": false,
        "muted": false,
        "likeSum": 0,
        "isLiked": false,
        "commemtSum": 0,
        "shareSum": 0,
        "imgList":[],
        "type": "1"
    },
    {
        "userId": "666dd802f366f40a8b9a4dd4",
        "avatar": "https://env-00jxgwsep3px.normal.cloudstatic.cn/snsPro/image/1728312053918.jpg",
        "nickname": "追风少女心💓( ˘ ³˘)💋",
        "isfollow": false,
        "videoId": "672ca16b521a64050d97c506",
        "playurl": "xxxxxxxxxx/mu38_video/1730978147033/compress_video_1204586286.m3u8",
        "poster": "xxxxxxxxxx/mu38_video/1730978147033/compress_video_1204586286.m3u8?vframe/jpg/offset/1/&e=1732097696&token=IAM-C1JVXCM70fL6bT80pA5w1YLPCXa7xisCoyR3eVhr:TGPsE7W3EmCEdaETm3e8S3sQWxA=",
        "vduser": "追风少女心💓( ˘ ³˘)💋",
        "vdtitle": "仿抖音视频仿抖音视频仿抖音视频仿抖音视频仿抖音视频仿抖音视频",
        "loop": true,
        "duration": 7410,
        "playIng": false,
        "muted": false,
        "likeSum": 0,
        "isLiked": false,
        "commemtSum": 0,
        "shareSum": 0,
        "imgList":[],
        "type": "1"
    },
    {
        "userId": "666dd802f366f40a8b9a4dd4",
        "avatar": "https://env-00jxgwsep3px.normal.cloudstatic.cn/snsPro/image/1728312053918.jpg",
        "nickname": "追风少女心💓( ˘ ³˘)💋",
        "isfollow": false,
        "videoId": "672ca1c529d183956332a975",
        "playurl": "xxxxxxxxxx/mu38_video/1730978178134/compress_video_1204605619.m3u8",
        "poster": "xxxxxxxxxx/mu38_video/1730978178134/compress_video_1204605619.m3u8?vframe/jpg/offset/1/&e=1732097696&token=IAM-C1JVXCM70fL6bT80pA5w1YLPCXa7xisCoyR3eVhr:wFO341M63ax-f-ZHxi2p88Z5fSY=",
        "vduser": "追风少女心💓( ˘ ³˘)💋",
        "vdtitle": "仿抖音视频仿抖音视频仿抖音视频仿抖音视频仿抖音视频仿抖音视频",
        "loop": true,
        "duration": 124200,
        "playIng": false,
        "muted": false,
        "likeSum": 0,
        "isLiked": false,
        "commemtSum": 0,
        "shareSum": 0,
        "imgList":[],
        "type": "1"
    }
]

隐私、权限声明

1. 本插件需要申请的系统权限列表:

无

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

无

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

无