更新记录

1.0.0(2026-08-21)

  • 跨端首个版本,iOS deployment target 为 13.0,Android minSdk 为 21(Android 5.0)。
  • iOS 使用 AVPlayer,Android 使用 AndroidX Media3 ExoPlayer 1.4.1 与 TextureView。
  • 支持 uni-app Vue 2/3 NVue 与 uni-app x。
  • 提供三槽视频池;下一槽位提前完成引擎准备,并保持暂停和静音。
  • 统一提供组件、控制器和 play、prepared、playbackstarted、firstframe、pause、timeupdate、buffering、ended、error 事件契约。
  • iOS NVue 事件载荷兼容标准基座,可正确读取 currentTime、duration 等字段。
  • 公开示例完整处理页面显示、隐藏和销毁生命周期,避免后台播放与控制器任务残留。
  • iOS 首播 ready 后异常停在 paused 时执行有界退避恢复,并保护同步事件回调中的暂停、切源和释放不被旧播放命令覆盖。
  • Android 补齐 Media3 Player.Listener 完整回调接口,避免生成代码缺少默认回调时触发 AbstractMethodError。
  • Android 播放器命令统一进入主线程并使用生命周期代次隔离陈旧命令和回调,提升快速切换与退出释放的稳定性。
  • 完成 iOS 云端 simulator 自定义基座和 Android 云端独立测试 APK 回归,覆盖三条视频切换、首帧、seek、生命周期与单音轨行为。
  • 补充 Android 标准基座、Android Studio、首个视频首帧、预加载音频和 16 KB page size 基座限制说明。

平台兼容性

uni-app(5.24)

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

uni-app x(5.24)

Chrome Safari Android iOS 鸿蒙 微信小程序
× × √ √ × ×

iOS/Android 原生视频池组件

xjh-video-pool 提供统一的 <video-pool-player> 原生视频槽位,以及独立的 createVideoPoolController() 三槽控制器,适合短视频、短剧和课程视频。业务或第三方列表只需按稳定视频 id 提交 previous/current/next。

iOS 使用 AVPlayer,deployment target 为 iOS 13.0;Android 使用 AndroidX Media3 ExoPlayer 1.4.1,minSdk 为 API 21(Android 5.0),通过 TextureView 输出画面。下一槽位会完成原生引擎准备,但保持暂停和静音,直到成为 current。

插件导入后无需手动注册,可在 App NVue 或 App UVue 页面中直接使用。普通 App Vue 页面不能嵌入该原生组件。

支持环境

项目格式 页面 iOS(配置下限 13.0) Android(minSdk 21)
uni-app Vue 3 App NVue 支持 支持
uni-app Vue 2 App NVue 支持 支持
uni-app x App UVue,VDOM 或 Vapor 支持 支持
uni-app Vue 2/3 普通 App Vue 页面 不支持 不支持
Web、Harmony、小程序 - 不支持 不支持

构建与调试

  • 要求 HBuilderX 5.24 或更高版本。
  • 插件当前已从 DCloud 插件市场下架,市场下载、试用和升级暂不可用;维护者使用私有源码验收,重新上架后再恢复市场安装。
  • iOS 使用系统 AVFoundation;HBuilderX 5.24 标准基座已经验证可用于本地调试。生成可交付 App 时仍需重新打包,原生代码不能通过 WGT 热更新生效。
  • Android 的 Media3 依赖已在插件中声明,正常接入不需要手工修改 Gradle,也不要求安装 Android Studio。Android Studio 只用于原生调试、Profiler 或设备兼容问题排查。
  • Android 标准基座不包含 Media3,运行时会出现原生类缺失。请使用导入当前插件版本后重新制作的 Android 自定义基座或云端安装包。
  • 本地 UTS compile 可以验证 Android 源码与依赖能否解析,但不会把 Media3 注入已经存在的标准基座。

已验证环境

截至 2026-08-20,当前源码已在 HBuilderX 5.24 完成 iOS 与 Android 原生编译及云端打包验证:

平台 验证环境 已覆盖结果
iOS iPhone 16 Plus / iOS 18.6 模拟器、云端 simulator 自定义基座 三条 MP4 播放与切换、首帧、进度、seek、冷启动
Android Android 17 / API 37、ARM64、16 KB page size 模拟器、云端独立测试 APK 三条 MP4 正反切换、首帧、进度、seek、单音轨、前后台、20 次完整退出后的冷启动

本次原生运行回归使用 Vue 3 App NVue 主示例;Vue 2 App NVue 与 uni-app x VDOM/Vapor 入口已经纳入源码和自动检查,但仍需在重新上架前分别完成宿主运行验收。iOS 13.0 与 Android API 21 是构建配置下限,尚未完成对应最低版本真机验收。Android 验证所用 APK 仅是示例工程测试包,不是可上架的发行包。该记录也不替代弱网、HLS、签名 URL、厂商硬解码器和正式发行包验收。

最小示例

下面是可以直接放进 Vue 3 App NVue 页面的 <script setup> 基础用法,包含播放、获取时间、设置时间和切换视频:

<template>
    <view class="page">
        <video-pool-player
            ref="player"
            :src="videos[currentIndex]"
            role="current"
            class="player"
            @timeupdate=""
        ></video-pool-player>

        <text class="time">{{ currentTime }} / {{ duration }}</text>
        <button @click="seekToTen">跳到 10 秒</button>
        <button @click="playNext">切换视频</button>
    </view>
</template>

<script setup>
    import { nextTick, ref } from 'vue'
    import { onHide, onReady, onShow, onUnload } from '@dcloudio/uni-app'

    const player = ref(null)
    const currentIndex = ref(0)
    const currentTime = ref(0)
    const duration = ref(0)
    const videos = [
        'https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/2minute-demo.mp4',
        'https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/uni-app-video-courses.mp4'
    ]

    onReady(() => nextTick(() => player.value.play()))
    onShow(() => nextTick(() => player.value?.play()))
    onHide(() => player.value?.pause())
    onUnload(() => player.value?.release())

    function (event) {
        const time = event.detail || event
        currentTime.value = time.currentTime
        duration.value = time.duration
    }

    function seekToTen() {
        player.value.seek(10)
    }

    function playNext() {
        player.value.pause()
        currentIndex.value = (currentIndex.value + 1) % videos.length
        nextTick(() => player.value.play())
    }
</script>

<style>
    .player {
        width: 750rpx;
        height: 500px;
    }
    .time {
        color: #333333;
    }
</style>
  • 获取时间:监听 @timeupdate,读取 event.detail.currentTime 和 event.detail.duration。
  • 设置时间:调用 player.value.seek(10)。
  • 切换视频:暂停、修改 src,再调用 play()。

不要使用 uni.createVideoContext,它只适用于 UniApp 内置的 <video>。

多组件竖滑完整示例

下面代码可以直接复制到 Vue 3 App NVue 页面运行。videos 每一项的 id 就是控制器使用的视频 id;函数 ref 先保存 id -> 播放器实例,switchTo() 再传入同一个 id,resolvePlayer(id) 用它取回对应实例。例如滑到第二条时,提交的是 demo、course、native。

<template>
    <view class="page">
        <swiper class="feed" :current="currentIndex" vertical @change="onChange">
            <swiper-item v-for="item in videos" :key="item.id">
                <view class="slide">
                    <video-pool-player
                        :ref="player => setPlayerRef(item.id, player)"
                        :src="item.src"
                        managed
                        object-fit="cover"
                        class="player"
                        @timeupdate=""
                    ></video-pool-player>
                    <text class="title">{{ item.title }}</text>
                </view>
            </swiper-item>
        </swiper>

        <view class="controls">
            <text class="time">{{ currentTime }} / {{ duration }}</text>
            <button class="seek-button" @click="seekToTen">跳到 10 秒</button>
        </view>
    </view>
</template>

<script setup>
    import { ref } from 'vue'
    import { onHide, onReady, onShow, onUnload } from '@dcloudio/uni-app'
    import { createVideoPoolController } from '@/uni_modules/xjh-video-pool/js_sdk/pool-controller.js'

    const currentIndex = ref(0)
    const currentTime = ref(0)
    const duration = ref(0)
    const videos = [
        {
            id: 'demo',
            title: '2 Minute Demo',
            src: 'https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/2minute-demo.mp4'
        },
        {
            id: 'course',
            title: 'uni-app Video Course',
            src: 'https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/uni-app-video-courses.mp4'
        },
        {
            id: 'native',
            title: 'Wap2App vs Native',
            src: 'https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/wap2appvsnative.mp4'
        }
    ]
    const playerRefs = new Map()
    const pool = createVideoPoolController({
        // id 就是 switchVideo() 传入的 previous/current/next 值
        resolvePlayer: id => playerRefs.get(id)
    })

    onReady(() => switchVideo(0))
    onShow(() => pool.setActive(true))
    onHide(() => pool.setActive(false))
    onUnload(() => {
        pool.dispose()
        playerRefs.clear()
    })

    function setPlayerRef(id, player) {
        playerRefs.set(id, player)
    }

    function switchVideo(index) {
        const previous = videos[index - 1]
        const current = videos[index]
        const next = videos[index + 1]
        currentIndex.value = index
        pool.switchTo({
            previous: previous ? previous.id : null,
            current: current.id,
            next: next ? next.id : null
        })
    }

    function onChange(event) {
        switchVideo(event.detail.current)
    }

    function (event) {
        const time = event.detail || event
        currentTime.value = Math.floor(time.currentTime)
        duration.value = Math.floor(time.duration)
    }

    function seekToTen() {
        pool.seek(10)
    }
</script>

<style>
    .page {
        flex: 1;
        background-color: #000000;
    }
    .feed {
        flex: 1;
        width: 750rpx;
        background-color: #000000;
    }
    .slide,
    .player {
        flex: 1;
        width: 750rpx;
        background-color: #000000;
    }
    .title {
        position: absolute;
        left: 24px;
        bottom: 110px;
        z-index: 2;
        color: #ffffff;
        font-size: 18px;
    }
    .controls {
        position: absolute;
        left: 24px;
        right: 24px;
        bottom: 40px;
        z-index: 2;
        flex-direction: row;
        align-items: center;
        justify-content: space-between;
    }
    .time {
        width: 160px;
        color: #ffffff;
        font-size: 14px;
    }
    .seek-button {
        width: 120px;
        height: 40px;
        font-size: 14px;
    }
</style>

同一份页面源码见 examples/uni-app-nvue-feed.nvue。

控制器方法

方法 说明
switchTo({ previous, current, next }) 传入 videos 中上一条、当前条、下一条的 id。
play() / pause() 播放或暂停当前视频。
seek(seconds) 设置当前视频时间。
setActive(value) 页面显示时传 true,隐藏或进入后台时传 false。
releaseAll() 释放已跟踪播放器,控制器仍可继续使用。
dispose() 释放播放器和待处理操作,并永久停用控制器。

Props

属性 类型 默认值 说明
src String '' 视频地址。
role String 'next' current、previous 或 next。
managed Boolean false 使用多组件切换控制器时设为 true。
objectFit String 'contain' cover 或 contain。

组件 Ref 方法

方法 说明
play() 播放。
pause() 暂停。
seek(seconds) 跳到指定秒数。
retain() 声明当前组件仍被业务持有;通常由三槽控制器管理。
release() 释放当前组件的原生播放器和监听;通常由三槽控制器或页面销毁逻辑调用。

Events

事件 触发时机
play 播放意图从未激活变为激活,不代表画面已经开始。
prepared 当前 src 第一次到达原生引擎 ready 状态,每个资源最多一次。
playbackstarted 原生播放器真正开始或恢复播放,缓冲恢复后可再次触发。
firstframe 当前 src 的第一帧真实画面已经渲染,每个资源最多一次。
pause 播放意图从激活变为未激活。
timeupdate 仅 current 槽位约每 250ms 返回 currentTime 和 duration。
buffering current 播放器进入或离开等待数据状态,返回 buffering 和时间。
ended 视频自然播放到结尾。
error 归一化致命播放错误,返回 code、message、fatal 和 nativeCode。

prepared 不等于首帧可见,也不保证零延迟播放。业务封面应在 firstframe 后隐藏,真实播放统计应监听 playbackstarted。

iOS 从资源持续处于 ready 但仍 paused 时开始有界重试;约 3.5 秒仍无法启动才会发送 code: "PLAYBACK_START_FAILED" 的致命 error。waiting 不消耗恢复预算,重复布局或配置也不会重置预算。显式暂停、切源、角色降级或释放都会取消恢复,不会覆盖业务的最新操作。

常见问题

Android 标准基座为什么不能运行

Android 实现依赖 Media3 ExoPlayer 1.4.1,标准基座没有打入这些原生依赖,可能出现 NoClassDefFoundError,例如缺少 androidx.media3.ui.AspectRatioFrameLayout。这不是页面 API 差异;重新制作 Android 自定义基座或云端安装包后再测试即可。

接入 Android 是否必须安装 Android Studio

不需要。HBuilderX 会根据插件的 app-android/config.json 解析 Media3 依赖。只有需要查看 Android 原生日志、使用 Profiler 或排查特定厂商设备时,才需要 Android Studio。

首个视频为什么一直像黑屏

组件挂载或收到 prepared 只表示原生视图或播放引擎已经就绪,不表示画面已经渲染。首个 current 槽位应在 onReady 后通过 nextTick 显式调用 play();业务封面应保留到 firstframe,并监听 error 提供重试入口。不要在 prepared 时提前移除封面。

预加载是否会同时播放多路声音

不会。三槽控制器只让 current 播放和发声;previous 保留但暂停,next 完成 prepare() 后保持暂停和静音。不要绕过控制器同时对 current 和 next 调用 play()。模拟器无声时还应先确认宿主系统媒体音量和模拟器音频输出,最终音频验收以真机为准。

Android 16 KB page size 兼容提示来自哪里

插件自身不携带 .so。HBuilderX 5.24 的 DCloud Android 基座在 16 KB page size 设备上仍可能因基座原生库显示兼容提示;这不能通过修改本插件 UTS 源码消除。面向相关设备发行时,应升级到提供完整 16 KB 兼容原生库的 DCloud/HBuilderX 基座,重新打包并复测。

隐私、权限声明

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

无

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

插件不采集数据,仅使用传入的视频 URL 发起播放请求

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

无

暂无用户评论。