更新记录

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 分钟)
  2. 宿主工程必须做的配置
  3. 完整示例页面(nvue)
  4. API 参考
  5. 必须遵守的 8 条约定
  6. 双端能力边界
  7. 常见问题排查
  8. 目录结构与版本

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)

两类参数别混:
播放参数(rate volume fit repeat quality boost)→ 独立通道,改了不重载片源;
片源参数(url format subtitle cache)→ 参与装载,改了会重载(画面闪一下、进度 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 条约定

  1. 只能在 .nvue 页面用。App-vue 下原生组件不跟随 swiper 滚动(这是选 nvue 的唯一原因)。
  2. 属性区里不要写注释(会导致整个节点失效,见 3.1)。
  3. active 与 playing 分开:active 表示「谁是当页」,playing 表示「要不要播」,一次性动作用 cmd 补一刀。
  4. cmd / seek / frame 都用「自包含字符串 + seq 递增」,别用两个 prop 组合(更新顺序不保证,会把旧命令当新命令下发)。
  5. 消费 time.dur 必须判 > 0,否则装载未就绪时会把已拿到的时长抹回 0,进度条永远禁用。
  6. 失败提示只认确定性 error,且必须 && !everPlayed:error 载荷不带 url,「下一集」装载失败也会发到同一个 handler,没有仲裁就会让正在播的这集顶着失败提示。换集必须复位 loadFail / everPlayed。
  7. 改 `utssdk/后必须重制自定义基座 / 云打包**;只改pages/**` 才能热更。
  8. 不要重复声明权限(尤其 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

隐私、权限声明

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

无

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

无

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

无

暂无用户评论。