更新记录
1.0.0(2026-10-15)
出版提交
平台兼容性
uni-app(5.26)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| √ | √ | - | - | × | √ | - | - | - |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| - | - | - | - | - | - | - | - | - | - | - | - |
short-video-item · 短剧 / 短视频 Feed 原生列表项(uni-app)
抖音 / 红果 式「上下滑动刷视频」的 App 原生列表项组件(兼容模式 UTS 组件,Android ExoPlayer / iOS AVPlayer)。
- ✅ 画面跟着 swiper 真实滚动(原生视图在 View 层级内,不像 webview 的
<video>那样脱离文档流) - ✅ 每个列表项自带一个播放器:非当前项已预装载停在首帧,滑过去即播
- ✅ 进度 / 时长 / 缓冲二级条 / 首帧耗时 / 播放真实态 / 错误分类 全套事件
- ✅ 倍速、清晰度、画面缩放、音量增强、外挂字幕、磁盘缓存、预加载、截图存相册
- ✅ 全屏横屏、后台小窗(Android 自绘悬浮窗 / iOS 系统画中画)
- ✅ 500ms 看门狗自愈:切页不播、输出面丢失、缓冲卡死都能自己爬起来
只支持 App 端的 nvue 页面(
.nvue)。Web / 小程序 / HarmonyOS 不支持。
内部实现、双端差异、排障手册见同目录docs/DEV.md(工程内文档,不必随插件发布)。
目录
1. 快速开始(5 分钟)
1.1 安装
把 short-video-item 目录拷进项目的 uni_modules/(或从插件市场导入):
uni_modules/
└── short-video-item/
├── package.json
└── utssdk/
├── interface.uts
├── app-android/ (config.json / index.vue / index.uts / ShortVideoItemBridge.kt)
└── app-ios/ (config.json / Info.plist / index.vue / index.uts / ShortVideoItemBridge.swift)
组件标签名是 sv-video-item(由 name 字段决定),编译器自动注册,不需要写进 pages.json 的 usingComponents,也不需要 import 组件本身。
1.2 最小可用页面(pages/index/index.nvue)
<template>
<view class="page">
<swiper class="pager" vertical="true" :current="current" @change="onChange">
<swiper-item v-for="(item, i) in list" :key="i">
<view class="cell">
<view class="video-box">
<sv-video-item
class="video"
:url="item.url"
:format="item.format"
:active="i === current"
@event="onPlayerEvent"
/>
</view>
</view>
</swiper-item>
</swiper>
</view>
</template>
<script>
export default {
data() {
return {
current: 0,
curMs: 0,
duration: 0,
list: [
{ url: 'https://media.w3.org/2010/05/bunny/trailer.mp4', format: 'mp4' },
{ url: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8', format: 'hls' }
]
};
},
methods: {
onChange(e) { this.current = e.detail.current; },
onPlayerEvent(detail) {
const raw = (detail && detail.json) ? detail.json : detail;
if (!raw || typeof raw !== 'string') return;
const e = JSON.parse(raw);
if (e.type === 'time') {
if (e.dur > 0) this.duration = e.dur; // ⚠️ 必须判 > 0,否则会把已知时长抹回 0
this.curMs = e.cur;
}
}
}
};
</script>
<style>
.page { flex: 1; background-color: #000000; }
.pager { flex: 1; }
.cell { flex: 1; }
.video-box { flex: 1; } /* 容器给 flex:1 */
.video { position: absolute; left: 0; top: 0; right: 0; bottom: 0; } /* 组件铺满容器 */
</style>
⚠️ 必须是
.nvue文件。App-vue 页面里原生组件无法在 swiper / scroll-view 中使用,画面不会跟着滚。
⚠️ 改完原生代码(utssdk/**)后必须重新制作自定义调试基座或云打包,「运行到手机」只同步 JS/UTS,不会更新原生壳。
2. 宿主工程必须做的配置
2.1 manifest.json(源码视图)
"app-plus": {
"nvueStyleCompiler": "uni-app", // 必须是 uni-app 编译模式
"statusbar": { "immersed": "supportedDevice" }, // 沉浸式:视频才能顶到屏幕最上沿
"distribute": {
// iOS 屏幕方向必须是数组,单字符串会被忽略
"orientation": ["portrait-primary", "landscape-primary", "landscape-secondary"],
"android": {
"screenOrientation": ["portrait-primary", "landscape-primary", "landscape-secondary"],
"minSdkVersion": 26,
"targetSdkVersion": 33,
"permissions": [
"<uses-permission android:name=\"android.permission.SYSTEM_ALERT_WINDOW\"/>", // 小窗(自绘悬浮窗)必需
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>" // 音量增强 boost 必需
]
},
"ios": {
"UIBackgroundModes": ["audio"] // 后台继续播 / 画中画
}
}
}
⚠️ 不要再声明 WRITE_EXTERNAL_STORAGE(DCloud 基座和项目根 AndroidManifest.xml 已声明过)。
重复声明(尤其带 android:maxSdkVersion 属性的)会让云端 manifest 合并失败,连带把 uni_modules 的 Gradle 子工程注册搞挂,报一串误导性的 Project with path ':uni_modules:xxx' could not be found。
2.2 pages.json:页面去导航栏
{
"path": "pages/index/index",
"style": {
"navigationStyle": "custom",
"backgroundColor": "#000000",
"app-plus": { "titleNView": false }
}
}
2.3 iOS 截图存相册
插件 utssdk/app-ios/Info.plist 已带 NSPhotoLibraryAddUsageDescription,云打包会自动合并;若你的工程自定义了 Info.plist,请确认包含该键。
3. 完整示例页面(nvue)
完整可运行页面在 pages/demo/demo.nvue(本项目内),覆盖:上下滑切集、单击播放/暂停、双击点赞、长按 2 倍速、进度条 + 缓冲二级条、倍速、清晰度、字幕、全屏横屏、小窗、截图、清缓存、选集、播完自动下一集、加载失败重试。
下面按功能分段讲,每段都是可直接抄的代码。
3.1 模板:组件怎么摆、属性怎么给
<swiper class="pager" vertical="true" :current="current" :duration="0" @change="onChange">
<swiper-item v-for="(item, i) in list" :key="i">
<view class="cell" @touchstart="" @touchend="">
<view class="video-box">
<sv-video-item
ref="vid"
class="video"
:url="item.url"
:format="item.format"
:active="i === current && playing"
:repeat="autoNext ? 'off' : 'one'"
:rate="i === current ? rate : 1"
:fit="fit"
:quality="quality"
:subtitle="i === current && subtitleOn ? item.subtitle : ''"
:cache="cacheOn"
:floating="i === current && floatingOn"
:seek-seq="seekSeq"
:seek-ms="seekMs"
:cmd="i === current ? cmd : ''"
@event="onPlayerEvent"
/>
</view>
</view>
</swiper-item>
</swiper>
要点:
| 写法 | 为什么 |
|---|---|
只给「当前项 ±1」传 url |
滑走再滑回来时组件复用,片源仍在;全量传会让所有项都装载,起播变慢 |
:active="i === current && playing" |
active 是「归属」(谁是当页),playing 是「意图」(要不要播)。两者分开,暂停/连播才不会互相打架 |
:cmd、:floating 只给当前项 |
命令与开关是逐容器的,给邻居下发会串台 |
手势挂 .cell / .ui,*组件的 `@touch` 只给 iOS** |
iOS 原生视频层层级高会吞 touch,必须在组件上再挂一处;安卓千万别给组件绑 @touchstart/@touchmove/@touchend —— UTS 运行时会在原生 View 上挂 listener 吃掉滑动,swiper 收不到 MOVE → 上下滑切集失效。用条件编译分开写两个标签 |
🚨 属性区里绝对不要写 HTML 注释。weex 编译器遇到注释会终止该标签的属性解析,整个
<sv-video-item>退化成文本节点:画面全黑、@event收不到、所有 prop 失效,而且编译不报错。注释一律写到标签外面。
手势绑定要按平台分开(安卓的滑动体验全靠这一条):
<!-- iOS:原生视频层层级高、会吞 touch,必须在组件上再挂一处 -->
<!-- #ifdef APP-IOS -->
<sv-video-item ...
@event="onPlayerEvent"
@touchstart="onTapStart" @touchmove="" @touchend="onTapEnd" @touchcancel="onTapCancel" />
<!-- #endif -->
<!-- 安卓:绝不能给组件绑 @touch*,否则 swiper 收不到 MOVE、上下滑切集失效 -->
<!-- #ifndef APP-IOS -->
<sv-video-item ... @event="onPlayerEvent" />
<!-- #endif -->
条件编译注释必须写在标签外面(属性区里连注释都不能有,见上)。安卓的手势由 .cell / .ui 这两个普通 view 承担,它们不会阻断 swiper。
3.2 事件分发:一个 handler 收全部状态
onPlayerEvent(detail) {
const raw = (detail && detail.json) ? detail.json : detail; // 兼容两种形态
if (!raw || typeof raw !== 'string') return;
const e = JSON.parse(raw);
if (e.type === 'time') { // 500ms 一次:进度 / 时长 / 已缓冲
if (e.dur > 0) this.duration = e.dur; // ⚠️ 必须判 > 0
if (e.buf !== undefined) this.bufMs = e.buf;
if (!this.dragging) this.curMs = e.cur;
if (e.cur > 0) { this.everPlayed = true; this.loadFail = false; }
} else if (e.type === 'ended') { // 播完(loop 也照发)→ 连播交给页面
this.onEnded();
} else if (e.type === 'cue') { // 字幕文本:原生只推文本,页面负责画
this.cueText = e.text || '';
} else if (e.type === 'error') { // 装载/播放失败
if (!this.everPlayed) this.loadFail = true;
}
}
3.3 命令通道:页面 → 原生
所有一次性动作用自包含字符串 verb#seq:seq 每次递增 → 字符串必变 → 组件 watcher 必触发 → 原生按 seq 去重执行。
sendCmd(c) {
this.cmdSeq = this.cmdSeq + 1;
this.cmd = c + '#' + this.cmdSeq; // 例如 "pause#7" / "reload#8" / "fwd-15000#9"
},
togglePlay() {
this.playing = !this.playing;
this.sendCmd(this.playing ? 'play' : 'pause');
},
retryLoad() {
this.sendCmd('reload'); // 强制重新装载(绕开"url 没变就不重载"的去重)
}
3.4 进度条 + 缓冲二级条
nvue 的 <slider> 没有「已缓冲」轨道,自己叠一条:
<view class="track">
<slider class="slider" :value="sliderValue" :min="0" :max="sliderMax" :step="1"
:disabled="duration <= 0" @changing="Changing" @change="Change" />
<view v-if="duration > 0" class="buf" :style="bufStyle"></view>
</view>
onSeekChanging(e) {
if (!this.dragging) { this.dragging = true; this.sendCmd('pause'); } // 拖动期间先停
this.dragValue = e.detail.value;
},
Change(e) {
this.curMs = this.dragValue = e.detail.value;
this.dragging = false;
this.seekMs = Math.round(this.dragValue);
this.seekSeq = this.seekSeq + 1; // seq 递增 → 触发一次精确跳转
if (this.playing) this.sendCmd('play');
}
3.5 全屏横屏
import { setScreenLandscape, onHostWindowSizeChanged } from '@/uni_modules/short-video-item';
toggleFullscreen() {
this.landscape = !this.landscape;
const r = setScreenLandscape(this.landscape);
// r: landscape / portrait / landscape_unsupported_plist(iOS 工程没声明横屏)/ no_activity
if (r === 'landscape_unsupported_plist') {
uni.showToast({ title: '工程未声明横屏,请配 manifest 后重制基座', icon: 'none' });
}
setTimeout(() => this.refreshSize(), 400);
}
横屏后必须把新窗口尺寸告诉原生(否则安卓画面仍是竖屏尺寸):
onReady() {
uni.onWindowResize((res) => {
this.refreshSize(res.size.windowWidth, res.size.windowHeight);
onHostWindowSizeChanged(); // 安卓必需:重算画面缩放 / 重挂全屏容器
});
}
🚨
uni.getSystemInfoSync()在 App 内是缓存值,旋转那一刻读到的可能还是旋转前的尺寸。尺寸只认uni.onWindowResize推来的真值。
全屏的两条硬规则(已内置,页面不用管)
| 规则 | 说明 |
|---|---|
| 竖屏片横屏看 = 完整显示 + 左右黑边 | 9:16 短剧被转到横屏时若"填满"要裁掉约 73%(人物头顶、底部字幕全没)。与苹果 AVPlayerLayer 的默认行为一致 |
| 横屏片横屏看 = 铺满 | 16:9 在 19.5:9 屏上裁掉约 14%,观感就是铺满(红果横屏就是这个效果) |
| 视频层永远在页面 UI 之下 | 原生不把视频搬到 DecorView,容器留在 nvue 树里的原位置 → 全屏后控制层、选集、暂停按钮照常可点 |
想要别的观感用 fit 覆盖:fit="contain"(强制完整显示)、fit="cover"(强制填满)。
3.6 小窗(后台悬浮播放)
import { canUseFloatingWindow, openFloatingWindowSetting } from '@/uni_modules/short-video-item';
toggleFloating() {
if (this.floatingOn) { this.floatingOn = false; this.sendCmd('float-off'); return; }
if (!canUseFloatingWindow()) { // Android: 无「显示在其他应用上层」权限 / iOS: 不支持画中画
uni.showModal({
title: '小窗',
content: '小窗需要「显示在其他应用上层」权限,去开启?',
success: (res) => { if (res.confirm) openFloatingWindowSetting(); }
});
return;
}
this.floatingOn = true;
this.sendCmd('float-on'); // 只开开关:切到后台才显示小窗,回 App 自动收回
}
小窗的实际行为(两端不同,调用方式相同):
| 行为 | Android(自绘悬浮窗) | iOS(系统画中画) |
|---|---|---|
| 何时弹出 | 开开关后切到后台才弹;回 App 自动收回,再切后台会再次弹出 | 同左(系统 PiP 接管) |
| 小窗尺寸 | 我们算:横屏片宽 = 屏宽 88%(按真实比例定高,最高不超过屏高 55%);竖屏片高 = 屏高 32% | 系统决定,App 无法指定(横屏片通常为一宽幅窗) |
| 小窗控件 | 自绘:左上 ✕ + 正中 ⏸/▶;单击小窗切换控件显隐(3 秒后自动隐藏),拖动可移动 | 系统控件条(✕ / ⏸ / 快进快退) |
| 小窗字幕 | ✅ 小窗内自带一条字幕(与页面同源) | ✅ 由系统渲染画面上的文字层 |
| 单击小窗 | 不会跳回 App(切换控件显隐);回 App 请点桌面/最近任务图标 | 由系统处理 |
| 权限 | 需要「显示在其他应用上层」(canUseFloatingWindow() 为 false 时引导授权) |
无需授权 |
3.7 组件方法(通过 ref 调)
v-for 里 ref 拿到的是数组,按索引取当前项:
curVideo() {
const r = this.$refs.vid;
if (!r) return null;
return (r.length !== undefined) ? r[this.current] : r;
},
snapshot() { this.curVideo() && this.curVideo().capture(-1); }, // -1 = 当前帧,存相册
clearCache() { this.curVideo() && this.curVideo().clearCache(); },
preloadNextEp() {
const v = this.curVideo(), next = this.list[this.current + 1];
if (v && next) v.preloadNext(next.url, next.format); // 切集那一刻是最佳时机
}
3.8 完整文件
展开 pages/demo/demo.nvue 全部代码
完整代码有两份(内容一致,改一份记得同步另一份):
- `pages/demo/demo.nvue` —— 可直接运行的示例页(本仓库)
- `uni_modules/short-video-item/example/demo.nvue` —— **插件包内自带的副本**(从插件市场导入后也能看到)
把其中任意一份拷进你的工程 `pages/` 下、在 `pages.json` 里注册即可运行;记得把 `list` 里的片源换成你自己的地址。
4. API 参考
4.1 属性(props)
两类参数别混:
播放参数(ratevolumefitrepeatqualityboost)→ 独立通道,改了不重载片源;
片源参数(urlformatsubtitlecache)→ 参与装载,改了会重载(画面闪一下、进度 seek 回原位)。
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
url |
String | '' |
片源地址,变化时自动重新装载 |
format |
String | 'mp4' |
mp4 / hls / dash / flv。⚠️ format 优先于 URL 后缀,短剧地址常是 /play?id=123 这种无后缀形态,必须显式给 |
active |
Boolean | false |
是否为当前页:true 播放,false 暂停(不销毁解码器,回来秒播) |
loop |
Boolean | true |
循环(下发过 repeat 后以 repeat 为准) |
muted |
Boolean | false |
静音 |
volume |
Number | 1 |
音量 0~1(与 boost 相乘) |
rate |
Number | 1 |
倍速 0.25~4.0 |
fit |
String | 'auto' |
画面缩放:auto / contain(完整显示)/ cover(填满裁切) |
repeat |
String | 'one' |
off / one / all(单媒体项下 all ≡ one,连播由页面做) |
quality |
String | 'auto' |
auto / 480 / 720 / 1080。⚠️ 只对多码率 HLS 有效 |
floating |
Boolean | false |
开小窗:退后台自动悬浮播放,回 App 自动收回 |
seekSeq |
Number | 0 |
跳转触发序号:递增一次即跳转一次(配合 seekMs) |
seekMs |
Number | 0 |
跳转目标位置(ms) |
cmd |
String | '' |
命令通道,自包含字符串 verb#seq,见 4.4 |
frame |
String | '' |
拖动取帧请求 ms#seq(如 "59000#12"),空串忽略 |
subtitle |
String | '' |
外挂字幕 .vtt / .srt,空串 = 关闭 |
subtitleLang |
String | 'zh' |
字幕语言标签 |
boost |
Number | 1 |
音量增强 1~4(1 = 关)。Android LoudnessEnhancer / iOS AVAudioMix |
cache |
Boolean | true |
磁盘缓存 / 预热。Android 边下边存真省流;iOS 只预热 |
debug |
Boolean | false |
插件内诊断日志总开关,默认关(time 事件 500ms 一条,开着会刷爆控制台) |
4.2 事件(@event)
统一一个 event 事件,载荷是 JSON 字符串:e.detail.json → JSON.parse → 按 type 分发。
| type | 字段 | 说明 |
|---|---|---|
time |
cur / dur / buf(ms) |
每 500ms 上报进度;buf = 已缓冲到哪(画二级条) |
meta |
dur / url |
时长主动上报(与播不播无关,就绪即报一次,带 url 供页面按 url 缓存) |
loading |
on |
缓冲/加载态(起播缓冲、seek 后重缓冲、弱网卡顿) |
playing |
on |
播放器真实推进态(与页面期望区分,缓冲中这里是 0) |
firstframe |
ms |
首帧已上屏,ms = 装载到首帧的耗时(真「秒开」指标) |
ended |
url |
播到结尾,即使循环也照发,去留由页面决定 |
vsize |
w / h / rot |
片源真实尺寸(已按 rot 校正,w > h 即横屏片) |
interrupt |
on / reason |
音频被系统中断 / 恢复(来电、抢焦点、拔耳机) |
error |
msg / code / http / kind / proto |
异常。kind:offline / timeout / dns / network / decode / av / unsupported |
cue |
text / url |
当前字幕文本(原生只推文本,渲染由页面做)。带 url 防邻居串台 |
subtitle |
ok / count / msg |
仅 iOS:字幕下载+解析结果(Android 由 media3 内部处理) |
float |
ok / msg / on / playing |
小窗状态;ok=0 时 msg 为 need_permission / window_failed。iOS 另有 pip_user_pause / pip_user_play:用户在系统小窗里按了暂停/播放(原生已双向同步,页面只跟着改 UI,不要回发 play/pause) |
pip |
ok / msg / tag |
系统画中画每一步结果(定位 iOS 小窗主要看它) |
frame |
ok / w / h / data |
拖动取帧结果,data 为 JPEG base64 |
capture |
seq / ok / ms / uri / w / h / data |
截图回执;data 是 240px 小图(做「已保存」提示用),不是存相册的大图 |
preload |
ok / msg / url / p / bytes / ms |
预加载进度:ok 2=进行中 / 1=完成 / 0=失败 |
state |
tag / active / hasPlayer / ready / rate / dur |
原生状态快照(诊断用) |
tick |
active / playing / want / reason |
500ms 心跳,页面可用它做「期望 vs 现实」纠偏 |
4.3 组件方法(ref 调用)
| 方法 | 返回 | 说明 |
|---|---|---|
getStats() |
UTSJSONObject \| null |
同步读此刻状态:cur dur buf vw vh rate quality fit repeat playing loading status surf sub cue boost cache cacheBytes |
capture(ms) |
void | 截图存相册,ms < 0 = 当前帧;结果走 capture 事件 |
preloadNext(url, format) |
void | 预加载下一集(Android 真落盘 / iOS 预热),进度走 preload 事件 |
clearCache() |
string | 清空缓存并重新装载当前项(不黑屏),返回释放结果 |
4.4 命令(cmd 的 verb)
| 命令 | 说明 |
|---|---|
play / pause |
播放 / 暂停(直达原生,比只改 active 可靠) |
reload |
强制重新装载(「点击重试」用,绕开 url 去重的空操作) |
fwd-N / back-N |
相对快进 / 快退 N 毫秒,如 fwd-15000。由原生按当前位置算,页面不必知道精确进度 |
float-on / float-off |
开 / 关小窗开关 |
4.5 宿主环境能力 API(从插件 import)
import {
setScreenLandscape, // (enabled: boolean) => string
canUseFloatingWindow, // () => boolean
openFloatingWindowSetting,// () => void
onHostWindowSizeChanged, // () => void 窗口尺寸变了通知原生重算缩放(安卓必需)
enterPictureInPicture, // () => string 立刻进系统画中画(高级用法)
exitPictureInPicture, // () => string 主动退出系统画中画
pipState // () => string 诊断串 active,supported,preWidth,preHeight
} from '@/uni_modules/short-video-item';
| 方法 | 说明 |
|---|---|
setScreenLandscape(true/false) |
强制横屏 / 竖屏。返回 landscape / portrait / no_activity;iOS 未声明横屏时返回 landscape_unsupported_plist |
canUseFloatingWindow() |
Android = Settings.canDrawOverlays(自绘悬浮窗必须有权限);iOS = 设备是否支持画中画 |
openFloatingWindowSetting() |
跳系统悬浮窗授权页(iOS 空实现:画中画不需要授权) |
onHostWindowSizeChanged() |
横竖屏切换、进出小窗后调用,安卓重算缩放并重挂全屏容器 |
5. 必须遵守的 8 条约定
- 只能在
.nvue页面用。App-vue 下原生组件不跟随 swiper 滚动(这是选 nvue 的唯一原因)。 - 属性区里不要写注释(会导致整个节点失效,见 3.1)。
active与playing分开:active表示「谁是当页」,playing表示「要不要播」,一次性动作用cmd补一刀。cmd/seek/frame都用「自包含字符串 + seq 递增」,别用两个 prop 组合(更新顺序不保证,会把旧命令当新命令下发)。- 消费
time.dur必须判> 0,否则装载未就绪时会把已拿到的时长抹回 0,进度条永远禁用。 - 失败提示只认确定性
error,且必须&& !everPlayed:error载荷不带 url,「下一集」装载失败也会发到同一个 handler,没有仲裁就会让正在播的这集顶着失败提示。换集必须复位loadFail/everPlayed。 - 改 `utssdk/
后必须重制自定义基座 / 云打包**;只改pages/**` 才能热更。 - 不要重复声明权限(尤其
WRITE_EXTERNAL_STORAGE),见第 2 节。
6. 双端能力边界
| 能力 | Android | iOS | 说明 |
|---|---|---|---|
| 磁盘缓存 / 省流 | ✅ SimpleCache 边下边存 |
⚠️ 仅预热,不省流 | iOS 宣传语只能写「起播更快」 |
| 预加载下一集 | ✅ 真落盘 | ⚠️ 仅预热 | 同上 |
外挂字幕 .vtt / .srt |
✅ 改字幕必重载(闪一下) | ✅ 自解析,不重载 | 两端交互观感不同 |
| 字幕渲染 | 页面画 | 页面画 | 契约一致(cue 事件),只写一套 |
| DASH | ✅ | ❌ 硬边界 | iOS 需准备 HLS 备用线路 |
| FLV | ✅ | ❌ 硬边界 | 同上;iOS 会发 error kind:unsupported |
| 清晰度切换 | ✅ 多码率 HLS | ✅ 多码率 HLS | 单码率源无效果(平台边界,非 bug) |
| 音量增强 >100% | ✅ LoudnessEnhancer(上限 +15dB) |
✅ AVAudioMix |
boost 1~4,与 volume 相乘 |
| 截图存相册 | ✅ API 29+ 免权限,<29 退 file:// |
✅ 需 NSPhotoLibraryAddUsageDescription |
按 capture 事件的 uri 区分提示文案 |
| 小窗 | 自绘悬浮窗(需 SYSTEM_ALERT_WINDOW) |
系统画中画(无需授权) | 两端调用方式一致 |
| 后台播放 | ✅ | ✅ 需 UIBackgroundModes: audio |
7. 常见问题排查
| 现象 | 判断 / 处理 |
|---|---|
| 页面白屏、视频区没内容 | 页面不是 .nvue;或属性区里写了 HTML 注释(组件节点被退化) |
| 有声音没画面(Android) | 输出面丢了。用 getStats().surf 看是否为 0;组件内有 500ms 自愈,持续黑屏多为容器尺寸为 0 |
| 有声音没画面(iOS) | 视频层尺寸为 0:检查 .video-box 是否给了 flex:1 或明确宽高 |
| 点了暂停没反应 / 命令不生效 | cmd 的 seq 没递增(字符串没变 → watcher 不触发) |
| 滑动时画面不跟随 | 用的是 App-vue 页面,或用的是 window 级 overlay 方案(本组件不会) |
时长一直是 --:-- |
① 消费 time.dur 没判 >0;② HLS 在 readyToPlay 前时长无效,等 meta 事件;③ 换集时没按 url 缓存时长 |
| 竖屏视频上下有黑边 | fit="auto" 下横屏容器/小窗是完整显示。要满屏用 fit="cover";另确认 manifest 开了沉浸式状态栏 |
| 横屏后视频只占左半边 / 缩在顶部一条 | 用了 getSystemInfoSync() 的缓存尺寸。尺寸只认 uni.onWindowResize,并调用 onHostWindowSizeChanged() |
| 小窗里点了暂停,之后再点播放没反应 | 已修(1.0.0+):旧版在小窗里仍跑补播自愈,把系统控件的暂停/播放顶回去。现在的规则是系统小窗可见时原生不下发任何 play/pause,只跟随 |
| 上下滑动切集不灵敏 / 有时滑不动(Android) | ① 别给组件绑 @touchstart/@touchmove/@touchend(只 iOS 需要);② 插件已把容器与 TextureView 设为"不抢触摸"(关 clickable/focusable + 触摸穿透 + 允许 swiper 拦截) |
| 全屏后选集 / 暂停 / 控制层点不到 | 已修(1.0.0+):旧版把视频容器搬到 DecorView,它盖在页面 UI 之上。现在容器留在 nvue 树内,层级天然在 UI 之下 |
| 竖屏短剧横屏全屏被裁掉上下(没黑边) | 已修(1.0.0+):横屏不再一律填满,改用裁切比例判据 —— 竖屏片完整显示 + 左右黑边(与苹果一致),横屏片仍铺满 |
| 点了全屏没反应 | iOS:manifest.distribute.orientation 不是数组;Android:未声明 landscape-*;两者都要重制基座 |
| 开了小窗切后台没反应 | Android 没有「显示在其他应用上层」权限(canUseFloatingWindow() 为 false);先引导授权 |
| 切回 App 后再切后台,小窗不弹了 | 已修(1.0.0+):旧版用「窗此刻在不在」判断该不该弹,回 App 收回后那个标记为 null → 再也不弹。现在按 h.floating(用户开关)判断,可反复收回/弹出 |
| 横屏剧的小窗太窄(像一条缩略图) | 已修(1.0.0+):横屏片小窗宽度从「屏宽 42%」提到 88%,高度按视频真实比例(上限屏高 55%)。竖屏片仍是「屏高 32%」的竖条 |
| 点一下小窗,App 就被拉到前台 | 已修(1.0.0+):单击小窗改为切换控件显隐,不再 startActivity。要回 App 请点桌面/最近任务图标 |
| 从小窗回到 App 后,顶栏/底部选集条不见了 | 已修(1.0.0+):旧版 pip_exited 事件因取不到 holder 而发不出去 → 页面 pipMode 卡在 true。现在事件正常发送,页面 UI 会自动恢复 |
| 切了清晰度画面没变化 | 片源不是多码率 HLS(单码率源只有一个视频轨),属平台边界 |
| 字幕不显示 | ① 页面没在 cue 事件里画文本层(原生不渲染);② 字幕 URL 带 query 导致 mime 误判;③ 国内 .srt 多为 GBK,iOS 已做编码兜底,仍不显示请换成 UTF-8 .vtt 验证 |
云打包报 Project with path ':uni_modules:xxx' could not be found |
先往同一次日志里找 uses-permission ... duplicated。重复权限会让 manifest 合并失败并连带搞挂子工程注册,报错顺序具有误导性 |
报 ClassNotFoundException: androidx.media3... |
基座没重建,media3 依赖没打进包 |
8. 目录结构与版本
uni_modules/short-video-item/
├── package.json # dcloudext.type = component-uts(组件入口 + API 入口并存)
└── utssdk/
├── interface.uts # API 入口的类型声明
├── app-android/
│ ├── config.json # media3 依赖 + minSdkVersion
│ ├── index.vue # 【组件入口】标签名 sv-video-item
│ ├── index.uts # 【API 入口】横屏 / 小窗相关
│ └── ShortVideoItemBridge.kt # ExoPlayer + TextureView(勿删)
└── app-ios/
├── config.json # frameworks: AVFoundation / AVKit / Photos
├── Info.plist # NSPhotoLibraryAddUsageDescription
├── index.vue
├── index.uts
└── ShortVideoItemBridge.swift # AVPlayer + AVPlayerLayer(勿删)
平台门槛
| 平台 | 支持 | 最低版本 | 依赖 |
|---|---|---|---|
| Android | ✅ | minSdk 21(推荐 26+) | androidx.media3 exoplayer / exoplayer-hls / exoplayer-dash 1.4.1 |
| iOS | ✅ | iOS 12(推荐 14+) | AVFoundation / AVKit / Photos |
| Web / 小程序 / HarmonyOS | ❌ | — | 兼容模式组件仅支持 App 端 |
当前版本:1.0.0 | 示例页面:pages/demo/demo.nvue | 工程内开发文档:docs/DEV.md

收藏人数:
购买普通授权版(
试用
使用 HBuilderX 导入示例项目
赞赏(0)
下载 9
赞赏 0
下载 12680575
赞赏 1956
赞赏
京公网安备:11010802035340号