更新记录
1.0.0(2026-07-29)
- 新版发布
平台兼容性
uni-app(5.06)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| × | × | × | × | × | × | × | × | × |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| × | × | × | × | × | × | × | × | × | × | × | × |
uni-app x(5.06)
| Chrome | Safari | Android | Android插件版本 | iOS | iOS插件版本 | 鸿蒙 | 鸿蒙插件版本 | 微信小程序 |
|---|---|---|---|---|---|---|---|---|
| × | × | √ | 1.0.0 | √ | 1.0.0 | √ | 1.0.0 | × |
yt-native-vlc 使用文档
特别提醒
- 购买本插件前,请先试用,请先试用,请先试用,确认满足需求之后再行购买。虚拟物品一旦购买之后无法退款。
- 如有使用上的疑问、bug,可以进交流群联系作者;
- 请在合法范围内使用,若使用本插件做非法开发,本方概不负责;
- 插件需先引入再打自定义基座后运行测试
- 本插件为标准组件只能用于uniapp-x项目的.uvue页面,如uniapp项目需要可使用uniapp项目可用
- 可先下载Android端demo体验插件功能体验包下载。
- 可下载插件提供的示例项目测试、试用。
- iOS 播放 RTSP 流首帧很慢:从开始播放到画面出现,常见需要约 30~40 秒(与 MobileVLCKit / 网络 / 设备有关)。开播前可按文档配置低延迟参数(如
--rtsp-tcp、适当 caching、需要时--no-audio),但无法保证缩短到秒级;若业务强依赖秒开,请先试用确认是否可接受,再决定是否购买。
1. 插件是做什么的?
把手机上的原生播放器嵌进 uni-app x 页面(.uvue),用来播视频、截图、录制等。
| 你想做的事 | 能不能做 |
|---|---|
| 播放网络 MP4 / HLS(m3u8) | ✅ |
| 播放 RTSP / RTMP 监控流 | ✅(视网络与设备) |
| 播放本地文件路径 | ✅ |
| 暂停 / 继续 / 停止 | ✅ |
| 拖进度、快进快退(点播) | ✅ |
| 倍速 1x / 2x / 4x / 8x / 16x(点播) | ✅ |
| 调节音量 0~100 | ✅ |
| 画面比例 auto / 4:3 / 16:9 | ✅ |
| 截图保存到本地 | ✅ |
| 录制并拿到文件路径 | ✅ |
| 横屏全屏播放 | ✅(页面实现,见 6.8 横屏全屏) |
只支持 uni-app x(页面用 .uvue)。
不支持 普通 uni-app 的 .vue / .nvue 工程。
页面里统一写标签:
<yt-native-vlc></yt-native-vlc>
三端(Android / iOS / 鸿蒙)写法一样,底层引擎不同:
| 平台 | 底层引擎 | 说明 |
|---|---|---|
| Android | LibVLC 4.x | native-view + 原生 View |
| iOS | MobileVLCKit 3.6.x | native-view + 原生 View |
| 鸿蒙 | ijkplayer(HAR @yt/vlc-player) |
native-view + Harmony Builder |
2. 使用前必读(很重要)
2.1 需要「自定义调试基座」
本插件含 原生代码(Kotlin / Swift / HAR)。
用 HBuilderX「运行到手机」时,标准基座跑不了,纯血鸿蒙可以直接运行到鸿蒙,必须:
- 菜单:运行 → 运行到手机或模拟器 → 制作自定义调试基座
- 勾选 Android / iOS / 鸿蒙(按你要测的端)
- 制作完成后,再用自定义基座运行项目
2.2 环境要求
- HBuilderX:5.0+(建议用较新正式版)
- 工程类型:uni-app x
- Android:
minSdk建议 ≥ 24(见插件utssdk/app-android/config.json) - iOS:依赖 CocoaPods
MobileVLCKit,包体积较大,云打包时间较长 - 鸿蒙:确认插件内存在
utssdk/app-harmony相关 HAR / 依赖配置
2.3 插件放哪里?
把整个文件夹放到项目:
你的项目/
└─ uni_modules/
└─ yt-native-vlc/ ← 本插件
easycom 会自动识别,页面不用手动 import 组件。
3. 五分钟上手
3.1 页面模板
给播放器一个明确宽高(很重要,否则可能看不到画面):
<template>
<view class="page">
<yt-native-vlc
ref="vlc"
class="player"
@ready="onReady"
@opening="onOpening"
@buffering="onBuffering"
@playing=""
@paused="onPaused"
@stopped="onStopped"
@ended=""
@error="onError"
@timeupdate=""
@seekablechange="ableChange"
@snapshot="onSnapshot"
@recordchange="onRecordChange"
></yt-native-vlc>
<button @click="onPlay">播放示例</button>
<button @click="">播放/暂停</button>
<button @click="onSnap">截图</button>
</view>
</template>
<style>
.player {
width: 100%;
height: 220px;
background-color: #333333;
}
</style>
3.2 脚本(选项式 + UTS)
export default {
data() {
return {
seekable: false,
playTimeText: '00:00 / 00:00',
sampleMp4: 'https://vd3.bdstatic.com/mda-jggr72s70c52d06f/sc/mda-jggr72s70c52d06f.mp4',
}
},
methods: {
/** 拿到组件实例(easycom 用官方生成的类型名) */
player() : YtNativeVlcComponentPublicInstance | null {
return this.$refs['vlc'] as YtNativeVlcComponentPublicInstance | null
},
onReady() {
// 引擎就绪后再 play 更稳妥
this.player()?.resetPlayerConfig()
},
onPlay() {
this.player()?.play(this.sampleMp4)
},
() {
this.player()?.togglePlayPause()
},
onSnap() {
this.player()?.makeSnapshot()
},
onOpening() {},
onBuffering(e : any) {
// e 可能是 Map,字段 percent
},
() {},
onPaused() {},
onStopped() {},
() {},
onError(e : any) {
uni.showToast({ title: '播放出错', icon: 'none' })
},
ableChange(e : any) {
// 点播一般 seekable=1,直播多为 0
},
(e : any) {
// currentMs / totalMs / position
},
onSnapshot(e : any) {
// success=1 时 filePath 为截图路径
},
onRecordChange(e : any) {
// recording=1 录制中;=0 已停止,可能带 filePath
},
}
}
3.3 调用方法时注意
// ✅ 正确:用组件生成的 PublicInstance
const p = this.$refs['vlc'] as YtNativeVlcComponentPublicInstance | null
p?.play(url)
// ❌ 错误:不要自己写一个 type 再强转,Android 会 ClassCastException
海康安防取流说明
本插件播放的是 通用 RTSP / http(s) 等地址字符串,基于 VLC,不内置海康视频 SDK。业务侧需先拿到可播 URL,再 play(url)。
重要:可能播不了海康综合安防流
以下情况用本插件(VLC)可能无法出画或极不稳定,属引擎 / 取流协议差异,不是业务页少调了一个 API:
- 综合安防平台签发的临时流(如
openUrl、部分加密 / 私有封装)与 MobileVLCKit / LibVLC 不兼容 - 取流规则、鉴权、协议类型与平台要求不符(例如必须走海康 SDK 会话,而不是裸 RTSP URL)
- 设备直连 RTSP 在部分机型 / 网络下仍失败(UDP、防火墙、密码编码、子码流等)
若你的场景是 海康威视综合安防平台(预览、回放、对讲、云台等),请改用专用插件:
- 插件名:海康威视综合安防平台视频播放插件(
yt-hikvideo) - 市场地址:https://ext.dcloud.net.cn/plugin?id=26041
- 基于海康视频 SDK,支持 uni-app / uni-app x;能力含实时预览、录像回放、语音对讲、鱼眼矫正、云台控制等
本插件更适合:标准 RTSP / HLS / 点播文件 等通用媒体地址;海康平台深度能力请用上面的 yt-hikvideo。
1. 设备 / NVR 直连 RTSP(局域网,可尝试本插件)
格式:
rtsp://用户名:密码@设备IP:554/Streaming/Channels/{通道编码}
| 通道编码 | 含义 | 建议 |
|---|---|---|
101 |
通道 1 主码流 | 画质高、带宽大,预览偏卡时可改用子码流 |
102 |
通道 1 子码流 | 预览推荐,码率低、开播更快 |
201 / 202 |
通道 2 主 / 子码流 | 多路同理:通道号×100 + 1/2 |
示例:
rtsp://admin:password@192.168.1.64:554/Streaming/Channels/102
注意:
- 账号密码、IP、端口以设备实际配置为准(默认 RTSP 端口多为
554)。 - 密码含特殊字符时需做 URL 编码(如
@→%40)。 - 手机与摄像头须在同一可达网络;iOS 需本地网络权限说明(见上文 Info.plist)。
- 务必使用 TCP:
play前setPlayerConfig包含--rtsp-tcp(默认配置已含);海康等设备常不支持 UDP,否则可能空等数十秒才出画。 - 纯预览可加
--no-audio,一般开播更快(无声音)。 - 直连仍失败时,优先排查网络 / TCP / 子码流;若实际接入的是综合安防平台,请改用 yt-hikvideo。
2. 海康综合安防 / 视频平台临时流(openUrl)
平台通过开放接口(如「获取预览取流 URL」)返回的临时地址,形态多为:
rtsp://平台域名或IP:端口/openUrl/xxxxxxxx
示例形态:
rtsp://xxx.xxx.xxx.xxx:554/openUrl/vsigxxxxxxxxxxxx
注意:
- URL 由 平台 API 签发,带有效期;过期后需重新取流再
play。 - 不要手写拼接
openUrl;按海康综合安防 / 视频平台开放文档,用相机编号、协议类型等参数请求预览地址。 - 拿到地址后同样建议
--rtsp-tcp;Android 端对含/openUrl/的地址会额外加大缓存(约 2s),便于跨网临时流更稳。 - 若平台还返回 HLS(
.m3u8)等,也可直接play该 http(s) 地址(非 RTSP 时按点播/直播能力使用即可)。 openUrl/ 平台流用 VLC 播失败或黑屏时:不要继续在本插件上硬调参数,请改用专用插件 海康威视综合安防平台视频播放插件(id=26041)。
4. 功能清单(方法)
通过 ref 调用,例如:this.player()?.play(url)。
4.1 播放控制
| 方法 | 说明 |
|---|---|
play(url) |
播放网络或本地地址 |
pause() |
暂停 |
resume() |
从暂停恢复 |
stop() |
停止并清空当前媒体 |
togglePlayPause() |
播放 ↔ 暂停 |
4.2 进度与倍速(点播)
直播(seekable=0)时拖进度 / 倍速 / 快进快退通常无效或应禁用。
| 方法 | 说明 |
|---|---|
seekTo(position) |
position 为 0~1(0 开头,1 结尾) |
seekBySeconds(seconds) |
正数快进、负数快退,单位秒 |
fastForward(seconds) |
快进,秒数 > 0 |
rewind(seconds) |
快退,秒数 > 0 |
setSpeed(rate) |
倍速:建议 1 / 2 / 4 / 8 / 16 |
4.3 音量与画面
| 方法 | 说明 |
|---|---|
setVolume(volume) |
音量 0~100,0=静音 |
getVolume() |
读当前音量(鸿蒙可能为占位值) |
setAspect(mode) |
'auto' 还原、'4:3'、'16:9' |
4.4 截图与录制
| 方法 | 说明 |
|---|---|
makeSnapshot() |
截图(不要叫 takeSnapshot) |
startRecording() |
开始录制,返回是否发起成功 |
stopRecording() |
停止录制 |
toggleRecording() |
未录制则开始,录制中则停止 |
结果请听事件:
- 截图 →
@snapshot - 录制 →
@recordchange
4.5 状态查询
| 方法 | 说明 |
|---|---|
isPlaying() |
是否在播 |
isRecording() |
是否在录 |
isSeekable() |
是否可拖进度 |
getLength() |
总时长毫秒(鸿蒙建议用事件里的 totalMs) |
getTime() |
当前时间毫秒 |
getPosition() |
进度 0~1 |
formatTime(ms) |
毫秒转 mm:ss 字符串 |
queryState() |
异步查状态,结果走 @state |
4.6 播放器参数(进阶)
| 方法 | 说明 |
|---|---|
setPlayerConfig(options) |
传入字符串数组,如 RTSP 参数;null 表示恢复默认 |
resetPlayerConfig() |
恢复默认参数 |
RTSP 示例(Android / iOS 更完整;鸿蒙以实现为准):
this.player()?.setPlayerConfig([
'--rtsp-tcp',
'--network-caching=300',
'--live-caching=300',
])
this.player()?.play('rtsp://user:pass@192.168.1.100:554/Streaming/Channels/101')
建议在 @ready 之后、play 之前设置。
5. 事件清单
页面用 @事件名="处理函数" 监听。
带数据的事件,回调参数里一般是 Map(或包一层后需自己取出 Map)。
布尔类字段一律用数字 0 / 1,不要当成 JS boolean 乱比。
| 事件 | 何时触发 | 载荷(Map 字段) |
|---|---|---|
ready |
引擎就绪,可以 play | 无 |
opening |
正在打开媒体 | 无 |
buffering |
缓冲中(含快进后缓冲) | percent:0~100 |
playing |
正在播放 | 无 |
paused |
已暂停 | 无 |
stopped |
已停止 | 无 |
ended |
点播播完 | 无 |
error |
出错 | message:错误信息 |
timeupdate |
进度刷新(点播) | currentMs、totalMs、position(0~1) |
seekablechange |
可否拖进度变化 | seekable:1 点播,0 直播 |
snapshot |
截图完成 | success:0|1,filePath:本地路径 |
recordchange |
录制状态变化 | recording:0|1,filePath:停录后可能有 |
state |
queryState() 的结果 |
playing/recording/seekable/volume/currentMs/totalMs/position |
5.1 读 Map 的小例子
readNum(data : Map<string, any>, key : string, def : number = 0) : number {
if (!data.has(key)) return def
return parseFloat(`${data.get(key)}`)
},
readFlag(data : Map<string, any>, key : string) : boolean {
return this.readNum(data, key, 0) == 1
},
onSnapshot(e : any) {
const data = e as Map<string, any> // 若实际外层还有 detail,请先取出 Map
if (this.readFlag(data, 'success')) {
const path = `${data.get('filePath')}`
uni.showToast({ title: '截图成功', icon: 'none' })
} else {
uni.showToast({ title: '截图失败', icon: 'none' })
}
}
项目里 pages/index/index.uvue 有完整演示(进度条、音量、快进快退、录制回放等),可直接对照。
6. 常见场景怎么写?
6.1 播一个 MP4
onReady() {
this.player()?.resetPlayerConfig()
},
playMp4() {
this.player()?.play('https://example.com/demo.mp4')
}
6.2 进度条 + 时间文字
- 听
@seekablechange,seekable==1才允许拖动 - 听
@timeupdate更新滑块和时间 - 用户拖动结束调用
seekTo(value / 1000)(若滑块是 0~1000)
6.3 快进 10 秒 / 快退 10 秒
this.player()?.seekBySeconds(10) // 快进
this.player()?.seekBySeconds(-10) // 快退
// 或
this.player()?.fastForward(10)
this.player()?.rewind(10)
快进后可能先收到 @buffering(0%→100%),再 @playing。
6.4 音量
this.player()?.setVolume(50) // 一半音量
this.player()?.setVolume(0) // 静音
6.5 画面 4:3 / 16:9
this.player()?.setAspect('4:3')
this.player()?.setAspect('16:9')
this.player()?.setAspect('auto') // 还原
6.6 截图
// 点击
this.player()?.makeSnapshot()
// @snapshot
onSnapshot(e) {
// success==1 时用 filePath 显示图片
}
6.7 录制
// 开始
this.player()?.startRecording()
// @recordchange → recording==1,按钮改成「停止录制」
// 停止
this.player()?.stopRecording()
// @recordchange → recording==0,filePath 可能是 mp4 路径,可再 play(path) 回放
不要只靠 isRecording() 判断(个别端可能有延迟),以 @recordchange 为准更稳。
6.8 横屏全屏播放怎么设置
重要: 插件没有
requestFullscreen()之类的方法。全屏是页面层能力:隐藏导航 + 播放器铺满 + 切横屏。完整示例见文末「完整演示页源码」。
步骤一:pages.json 配置播放页
播放页必须关掉原生导航栏,并声明默认竖屏(便于退出全屏后恢复):
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "播放器",
"navigationStyle": "custom",
"pageOrientation": "portrait"
}
}
| 配置项 | 作用 |
|---|---|
navigationStyle: "custom" |
去掉原生导航栏;全屏时用自定义导航 v-if 隐藏,画面才能顶到边缘 |
pageOrientation: "portrait" |
平时竖屏;进入全屏再用代码切成 landscape |
步骤二:页面布局(伪全屏)
- 用自定义导航栏(含
height: var(--status-bar-height)),v-if="!isFullscreen"控制显隐 - 播放器外层
view:非全屏固定高度(如220px);全屏时position: absolute铺满 - 不要用
v-if销毁<yt-native-vlc>,否则会断流重启 - 全屏时隐藏进度条、列表等控件;可留一个「退出全屏」悬浮按钮
步骤三:代码切换横竖屏
通过当前页的 setPageStyle 动态改 pageOrientation:
/** 动态设置页面方向:portrait | landscape */
applyPageOrientation(orientation : string) {
const pages = getCurrentPages()
if (pages.length == 0) return
const page = pages[pages.length - 1]
const style = {
pageOrientation: orientation,
} as UTSJSONObject
page.setPageStyle(style)
},
/** 进入横屏全屏 */
enterFullscreen() {
if (this.isFullscreen) return
this.isFullscreen = true
this.applyPageOrientation('landscape') // 切横屏
},
/** 退出全屏并恢复竖屏 */
exitFullscreen() {
if (!this.isFullscreen) return
this.isFullscreen = false
this.applyPageOrientation('portrait') // 恢复竖屏
},
建议同时处理:
- Android 返回键:
onBackPress里若正在全屏,先exitFullscreen()并return true - 离开页面:
onUnload里若仍全屏,恢复portrait,避免影响其它页
常见踩坑
| 现象 | 原因 / 处理 |
|---|---|
| 全屏仍是竖屏,上下大黑边 | 只放大了容器,没调 setPageStyle({ pageOrientation: 'landscape' }) |
| 全屏后导航栏还在 | 未设 navigationStyle: "custom",或自定义导航未在全屏时隐藏 |
| 点全屏后黑屏 / 重播 | 用 v-if 重建了播放器;应只改 class / 布局,保持组件挂载 |
| 退出后其它页也是横屏 | 退出或 onUnload 时未改回 portrait |
官方说明:pages.json pageOrientation、getCurrentPages / setPageStyle。
7. 点播 vs 直播(新手最容易懵)
| 点播(如 MP4) | 直播(如 RTSP) | |
|---|---|---|
seekable |
多为 1 |
多为 0 |
| 进度条 / 快进 / 倍速 | 可用 | 应禁用或无效 |
timeupdate |
有 current/total | 可能无总时长 |
| 录制 / 截图 | 一般可用 | 一般可用(视设备) |
UI 上请根据 @seekablechange 控制按钮的 disabled。
8. 目录结构(了解即可)
yt-native-vlc/
├─ components/yt-native-vlc/yt-native-vlc.uvue # 页面用的组件壳
├─ utssdk/
│ ├─ interface.uts # 跨端类型声明(给 uvue 看的)
│ ├─ app-android/ # Android:index.uts + Kotlin
│ ├─ app-ios/ # iOS:index.uts + Swift
│ └─ app-harmony/ # 鸿蒙:index.uts + HAR
├─ package.json
└─ readme.md # 本说明
9. 常见问题 FAQ
Q1:运行到手机白屏 / 报找不到原生类?
没有用自定义调试基座,或改完原生没重新制作基座。
Q2:调用 play 没画面?
- 是否等
@ready之后再 play - 组件是否设置了宽高(如
height: 220px) - URL 是否可访问(手机网络、HTTPS 证书、RTSP 账号)
Q3:进度条不动、时间一直 00:00?
- 是否点播?直播通常没有可拖进度
- 是否收到
@timeupdate、@seekablechange - iOS 若日志出现「回调函数已释放」,需确认插件里
onEvent已加 keepAlive,并重做基座
Q4:截图提示失败,但日志说成功?
看 @snapshot 里 success 是否为数字 1(不要用严格等于 true)。
Q5:录制按钮点「停止」却又开始录?
用 @recordchange 维护页面上的 recording 状态,不要只信同步的 isRecording()。
Q6:Android 强转 ref 崩溃 ClassCastException?
必须使用:
this.$refs['vlc'] as YtNativeVlcComponentPublicInstance
不要自定义一个接口类型再 as。
Q7:鸿蒙和 Android 行为不完全一样?
正常。鸿蒙是 ijk + HAR,部分同步查询(时长/音量)可能是占位,以事件回调为准。
Q8:改了插件代码,运行还是旧行为?
原生相关改动 → 重新制作自定义基座;仅改 .uvue 页面一般重新运行即可。
Q9:怎么做横屏全屏?插件有全屏方法吗?
没有。 全屏在业务页实现,不要等插件 API。最少三步:
pages.json:navigationStyle: "custom"+pageOrientation: "portrait"- 全屏时隐藏导航/控件,播放器容器铺满(组件保持挂载)
page.setPageStyle({ pageOrientation: 'landscape' })切横屏;退出改回portrait
详见 6.8 横屏全屏播放怎么设置 与文末完整演示页。
10. 推荐学习路径
- 把插件放进
uni_modules - 制作自定义基座并运行
- 复制本文「五分钟上手」能播 MP4
- 对照
pages/index/index.uvue演示页加进度条、音量、截图、录制、横屏全屏 - 再试 RTSP(先
setPlayerConfig再play)
11. 参考链接
- UTS 标准模式组件
- native-view 组件
- pages.json(含 pageOrientation / navigationStyle)
- getCurrentPages / setPageStyle
- UTS 回调 keepAlive
如有问题,请带上:平台(Android/iOS/鸿蒙)+ HBuilderX 版本 + 完整报错日志/关键截图,便于排查。
12. 更多好用插件推荐
- 高德定位连续定位后台定位保活定位
- 百度汽车摩托车导航插件
- 百度鹰眼轨迹插件支持后台采集、保活
- 百度定位插件、连续定位、保活、坐标系转换、支持双端
- 计步器插件,支持Android、iOS双端
- uts经典蓝牙插件、蓝牙电子秤
- 获取唯一标识、ServiceID、卸载更新不变iOS+Android
- Android经典蓝牙
- 华为ScanKit统一扫码插件支持iOS+Android原生插件
- 【华为扫码】统一扫码插件支持多码连续扫码支持半屏扫码uts插件iOS+Android+HarmonyOS
- 截屏、录屏、防截屏、录屏iOS、Android
- 人脸采集插件 最新百度SDK 离线人脸采集、活体检测
- 页面截长图、截取WebView内容,生成长截图Android+iOS
- Android无预览拍照、录制、静默拍照、静默录制、抓拍插件支持
- uni高德地图功能拓展地图截图
- 科大讯飞离线合成插件支持iOS、android
- iOS保活Android保活鸿蒙保活定位插件系统定位
- 高德定位、猎鹰轨迹插件
- 海康威视综合安防平台视频播放插件
- 支持NFC读写功能检测支持Android iOS HarmonyOS
- 自定义相机
13. 完整演示页源码(可直接复制)
下面是项目 pages/index/index.uvue 的完整源码(含详细注释),覆盖播放、进度、倍速、音量、比例、快进快退、截图、录制与回放、横屏全屏。
使用方法:
- 把
yt-native-vlc放进项目uni_modules/ - 新建页面
pages/index/index.uvue,整份粘贴下文 - 在
pages.json里按下面注册该页面(全屏必须配置navigationStyle/pageOrientation) - 制作自定义调试基座后运行
pages.json 播放页配置示例:
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "yt-native-vlc",
"navigationStyle": "custom",
"pageOrientation": "portrait"
}
}
请把示例里的
sampleRtsp改成你自己的摄像头地址。横屏全屏说明见 6.8。
<!--
=============================================================================
yt-native-vlc 完整演示页(pages/index/index.uvue)
=============================================================================
本页展示标准组件 <yt-native-vlc> 的常用能力,可直接对照学习:
- 播放 MP4 / RTSP、暂停继续、停止
- 进度条拖动、快进快退、倍速、音量、画面比例
- 截图预览、录制与回放
- 横屏全屏:setPageStyle(pageOrientation=landscape) + 播放器铺满(不销毁组件)
pages.json 需配置 navigationStyle: "custom"、pageOrientation: "portrait"。
使用前请先「制作自定义调试基座」,再用自定义基座运行。
=============================================================================
-->
<template>
<view class="root" :class="{ 'root-fullscreen': isFullscreen }">
<!-- 自定义导航:全屏时隐藏(原生导航已在 pages.json 关掉) -->
<view v-if="!isFullscreen" class="nav-bar">
<view class="status-pad"></view>
<view class="nav-content">
<text class="nav-title">yt-native-vlc 标准组件演示</text>
</view>
</view>
<!--
播放器区域:非全屏固定高度;横屏全屏后 absolute 铺满宽屏。
切勿用 v-if 销毁 yt-native-vlc,否则会断流重启。
-->
<view class="player-wrap" :class="{ 'player-wrap-full': isFullscreen }">
<yt-native-vlc ref="vlcVideo" class="player" :class="{ 'player-full': isFullscreen }" @ready="onReady"
@opening="onOpening" @buffering="onBuffering" @playing="" @paused="onPaused"
@stopped="onStopped" @ended="" @error="onError" @timeupdate=""
@seekablechange="ableChange" @snapshot="onSnapshot"
@recordchange="onRecordChange"></yt-native-vlc>
<!-- 全屏时悬浮「退出全屏」;非全屏用面板里的全屏按钮 -->
<button v-if="isFullscreen" class="fs-exit-btn" size="mini" type="primary"
@click="exitFullscreen">退出全屏</button>
</view>
<!-- 控制面板:全屏时隐藏 -->
<view v-if="!isFullscreen" class="panel">
<!-- 进度条:0~1000 对应 position 0~1;直播 seekable=false 时禁用 -->
<slider class="seek-bar" :value="progress" :min="0" :max="1000" :disabled="!seekable" activeColor="#4CAF50"
backgroundColor="#666666" :block-size="18" @changing="Changing" @change="Change" />
<view class="row">
<text class="time-text">{{ playTimeText }}</text>
<button class="btn-sm" type="primary" size="mini" @click="Play">{{ playPauseLabel }}</button>
<button class="btn-sm" size="mini" @click="onSnapshotClick">截图</button>
<button class="btn-sm" size="mini" @click="onRecordClick">{{ recordLabel }}</button>
<button class="btn-sm" size="mini" @click="toggleFullscreen">全屏</button>
</view>
<!-- 倍速仅点播;比例三端均可用 -->
<view class="row">
<button class="btn-xs" size="mini" :disabled="!seekable" @click="onSpeed(1)">1x</button>
<button class="btn-xs" size="mini" :disabled="!seekable" @click="onSpeed(2)">2x</button>
<button class="btn-xs" size="mini" :disabled="!seekable" @click="onSpeed(4)">4x</button>
<button class="btn-xs" size="mini" @click="onAspect('auto')">还原</button>
<button class="btn-xs" size="mini" @click="onAspect('4:3')">4:3</button>
<button class="btn-xs" size="mini" @click="onAspect('16:9')">16:9</button>
</view>
<!-- 快进快退:内部 seekBySeconds;直播应禁用 -->
<view class="row">
<button class="btn-xs" size="mini" :disabled="!seekable" @click="BySeconds(-10)">快退10s</button>
<button class="btn-xs" size="mini" :disabled="!seekable" @click="BySeconds(-5)">快退5s</button>
<button class="btn-xs" size="mini" :disabled="!seekable" @click="BySeconds(5)">快进5s</button>
<button class="btn-xs" size="mini" :disabled="!seekable" @click="BySeconds(10)">快进10s</button>
</view>
<!-- 音量 0~100;iOS 建议在 @changing 里也 setVolume -->
<view class="row vol-row">
<text class="vol-label">音量 {{ volume }}</text>
<slider class="vol-bar" :value="volume" :min="0" :max="100" activeColor="#4CAF50"
backgroundColor="#666666" :block-size="16" @changing="onVolumeChanging" @change="" />
<button class="btn-xs" size="mini" @click="onVolume(0)">静音</button>
<button class="btn-xs" size="mini" @click="onVolume(100)">最大</button>
</view>
</view>
<scroll-view v-if="!isFullscreen" class="scroll" direction="vertical">
<text class="status">状态:{{ statusText }}</text>
<!-- 录制完成后展示路径,可一键回放本地文件 -->
<view v-if="recordPath.length > 0" class="record-box">
<text class="record-title">最近录制文件</text>
<text class="record-path">{{ recordPath }}</text>
<button class="btn-block record-play" type="primary" @click="playRecordedVideo">播放录制视频</button>
</view>
<!-- 截图成功后预览(src 用本地绝对路径,不要带 file://) -->
<image v-show="showSnapshot" class="snapshot" :src="snapshotSrc" mode="aspectFit"></image>
<input class="url-input" v-model="inputUrl" placeholder="输入播放地址" />
<view class="row">
<button class="btn-flex" type="primary" @click="onPlayUrl">播放</button>
<button class="btn-flex" @click="onStop">停止</button>
</view>
<button class="btn-block" @click="playSampleMp4">示例 MP4</button>
<button class="btn-block" @click="playSampleRtsp">示例 RTSP</button>
</scroll-view>
</view>
</template>
<script>
/**
* 演示页逻辑说明:
* 1. player() 获取组件实例(必须 as YtNativeVlcComponentPublicInstance)
* 2. eventMap / readNum / readFlag 统一解析事件 Map(布尔用 0|1)
* 3. @seekablechange 控制进度条、倍速、快进是否可点
* 4. @recordchange 维护 recording / recordLabel / recordPath
* 5. isFullscreen:横屏全屏(pageOrientation + 布局),不销毁播放器
*/
export default {
data() {
return {
/** 是否横屏全屏(隐藏导航与控件,强制 landscape,播放器铺满) */
isFullscreen: false,
/** 顶部状态文案:缓冲中 / 播放中 / 错误等 */
statusText: '就绪',
/** 进度旁时间:mm:ss / mm:ss */
playTimeText: '00:00 / 00:00',
/** 播放/暂停按钮文案 */
playPauseLabel: '播放',
/** 录制按钮文案:录制 ↔ 停止录制 */
recordLabel: '录制',
/** 是否正在录制(以 @recordchange 为准,比 isRecording() 更稳) */
recording: false,
/** 最近一次录制文件本地路径 */
recordPath: '',
/** 进度条 0~1000,对应 position * 1000 */
progress: 0,
/** 音量 0~100 */
volume: 100,
/** 是否可拖进度(点播 true,直播 false) */
seekable: false,
/** 用户正在拖进度条时为 true,避免 timeupdate 抢进度 */
userSeeking: false,
/** 输入框里的播放地址 */
inputUrl: '',
/** 截图预览路径 */
snapshotSrc: '',
showSnapshot: false,
/** 示例点播地址(可换成自己的) */
sampleMp4: 'https://vd3.bdstatic.com/mda-jggr72s70c52d06f/sc/mda-jggr72s70c52d06f.mp4',
/** 示例 RTSP(请改成你局域网摄像头地址) */
sampleRtsp: 'rtsp://admin:lyzh2019@192.168.8.135:554/Streaming/Channels/102',
}
},
onBackPress() : boolean | null {
// Android 返回键:先退出横屏全屏,再退出页面
if (this.isFullscreen) {
this.exitFullscreen()
return true
}
return null
},
onUnload() {
// 离开页面时恢复竖屏,避免影响其它页
if (this.isFullscreen) {
this.applyPageOrientation('portrait')
this.isFullscreen = false
}
},
methods: {
/**
* 获取 yt-native-vlc 组件实例。
* 注意:不要自定义 type 再强转,Android 会 ClassCastException。
*/
player() : YtNativeVlcComponentPublicInstance | null {
return this.$refs['vlcVideo'] as YtNativeVlcComponentPublicInstance | null
},
/**
* 动态设置页面方向(uni-app x:page.setPageStyle)。
* @param orientation portrait | landscape
*/
applyPageOrientation(orientation : string) {
const pages = getCurrentPages()
if (pages.length == 0) return
const page = pages[pages.length - 1]
const style = {
pageOrientation: orientation,
} as UTSJSONObject
page.setPageStyle(style)
},
/** 进入横屏全屏(不销毁播放器) */
enterFullscreen() {
if (this.isFullscreen) return
this.isFullscreen = true
this.applyPageOrientation('landscape')
},
/** 退出全屏并恢复竖屏 */
exitFullscreen() {
if (!this.isFullscreen) return
this.isFullscreen = false
this.applyPageOrientation('portrait')
},
/** 切换横屏全屏 */
toggleFullscreen() {
if (this.isFullscreen) {
this.exitFullscreen()
} else {
this.enterFullscreen()
}
},
setStatus(t : string) {
this.statusText = t
},
toast(msg : string) {
uni.showToast({ title: msg, icon: 'none' })
},
/**
* 把事件参数统一成 Map。
* 不同端可能直接给 Map,或包在 detail 里,或是 UTSJSONObject。
*/
eventMap(e : any | null) : Map<string, any> {
if (e == null) return new Map<string, any>()
if (e instanceof Map) {
const m = e as Map<string, any>
if (m.has('detail')) return this.eventMap(m.get('detail'))
return m
}
const obj = e as UTSJSONObject
if (obj['detail'] != null) return this.eventMap(obj['detail'])
const out = new Map<string, any>()
const keys = ['percent', 'message', 'currentMs', 'totalMs', 'position', 'seekable', 'success', 'filePath', 'recording']
for (let i = 0; i < keys.length; i++) {
const k = keys[i]
const v = obj[k]
if (v != null) {
out.set(k, v)
}
}
return out
},
/** 读数字字段 */
readNum(data : Map<string, any>, key : string, def : number = 0) : number {
if (!data.has(key)) return def
return parseFloat(`${data.get(key)}`)
},
/** 读字符串字段 */
readStr(data : Map<string, any>, key : string) : string {
if (!data.has(key)) return ''
return `${data.get(key)}`
},
/**
* 读 0|1 标志位(也兼容字符串 "true")。
* 插件约定:seekable / success / recording 用数字 0、1。
*/
readFlag(data : Map<string, any>, key : string) : boolean {
return this.readNum(data, key, 0) == 1 || this.readStr(data, key) == 'true'
},
/** 根据 isPlaying() 刷新按钮文案(部分端会延迟,事件里也会直接改文案) */
updatePlayPauseLabel() {
const p = this.player()
this.playPauseLabel = (p != null && p.isPlaying()) ? '暂停' : '播放'
},
/** 点播 / 普通 URL:恢复默认引擎参数 */
applyDefaultPlayerConfig() {
this.player()?.resetPlayerConfig()
},
/**
* RTSP 低延迟常用参数(Android/iOS 更完整;鸿蒙以实现为准)。
* @param muteAudio true 时加 --no-audio,加快开播
*/
applyRtspPlayerConfig(muteAudio : boolean = false) {
const opts : string[] = [
'--rtsp-tcp',
'--network-caching=300',
'--live-caching=300',
'--file-caching=300',
'--drop-late-frames',
'--skip-frames',
'--http-reconnect',
]
if (muteAudio) opts.push('--no-audio')
this.player()?.setPlayerConfig(opts)
},
/** 开始播某个 URL:重置 seekable/进度后 play */
playUrl(url : string) {
this.setStatus('正在打开:' + url)
this.seekable = false
this.progress = 0
this.player()?.play(url)
},
/** 播放输入框地址 */
onPlayUrl() {
const url = this.inputUrl.trim()
if (url.length == 0) {
this.toast('请输入 URL')
return
}
this.applyDefaultPlayerConfig()
this.playUrl(url)
},
onStop() {
this.player()?.stop()
this.progress = 0
this.setStatus('已停止')
this.updatePlayPauseLabel()
},
Play() {
this.player()?.togglePlayPause()
this.updatePlayPauseLabel()
},
/** 截图:结果走 @snapshot(方法名必须是 makeSnapshot) */
onSnapshotClick() {
this.player()?.makeSnapshot()
},
/**
* 录制按钮:优先看页面 recording(来自 @recordchange)。
* 鸿蒙上同步 isRecording() 可能瞬时不准。
*/
onRecordClick() {
const p = this.player()
if (p == null) return
if (this.recording || p.isRecording()) {
p.stopRecording()
} else {
p.startRecording()
}
},
/** 倍速:仅 seekable 时允许 */
onSpeed(rate : number) {
if (!this.seekable) {
this.toast('直播不可倍速')
return
}
this.player()?.setSpeed(rate)
this.toast('倍速 ' + rate + 'x')
},
/** 画面比例:auto / 4:3 / 16:9 */
onAspect(mode : string) {
this.player()?.setAspect(mode)
this.toast('画面比例:' + mode)
},
/**
* 快进/快退。
* @param seconds 正数快进、负数快退;内部调用 seekBySeconds
*/
BySeconds(seconds : number) {
if (!this.seekable) {
this.toast('直播不可快进快退')
return
}
this.player()?.seekBySeconds(seconds)
this.toast(seconds >= 0 ? ('快进 ' + seconds + ' 秒') : ('快退 ' + (-seconds) + ' 秒'))
},
/** 设置音量 0~100 */
onVolume(vol : number) {
let v = Math.floor(vol)
if (v < 0) v = 0
if (v > 100) v = 100
this.volume = v
this.player()?.setVolume(v)
},
/** 拖动音量中:iOS 上 @change 偶发不触发,故 changing 也 setVolume */
onVolumeChanging(e : UniSliderChangeEvent) {
const v = Math.floor(e.detail.value)
this.volume = v
this.player()?.setVolume(v)
},
(e : UniSliderChangeEvent) {
const v = Math.floor(e.detail.value)
this.onVolume(v)
this.toast('音量 ' + v)
},
/** 拖进度中:只改 UI,不 seek */
Changing(e : UniSliderChangeEvent) {
if (!this.seekable) return
this.userSeeking = true
this.progress = e.detail.value
},
/** 松手:value/1000 → seekTo(0~1) */
Change(e : UniSliderChangeEvent) {
this.userSeeking = false
if (!this.seekable) return
const pos = e.detail.value / 1000.0
this.progress = e.detail.value
this.player()?.seekTo(pos)
},
playSampleMp4() {
this.inputUrl = this.sampleMp4
this.applyDefaultPlayerConfig()
this.playUrl(this.sampleMp4)
},
playSampleRtsp() {
this.inputUrl = this.sampleRtsp
this.applyRtspPlayerConfig(false)
this.playUrl(this.sampleRtsp)
},
/** 去掉 file:// 前缀,方便 image / play 使用 */
normalizeLocalPath(path : string) : string {
if (path.indexOf('file://') == 0) {
return path.substring(7)
}
return path
},
/** 播放最近录制的本地文件 */
playRecordedVideo() {
if (this.recordPath.length == 0) {
this.toast('暂无录制文件')
return
}
const path = this.normalizeLocalPath(this.recordPath)
this.inputUrl = path
this.applyDefaultPlayerConfig()
this.playUrl(path)
this.toast('正在播放录制视频')
},
// -------------------------------------------------------------------------
// 插件事件回调
// -------------------------------------------------------------------------
/** 引擎就绪:可安全 play;此处先恢复默认配置 */
onReady() {
this.setStatus('引擎已就绪')
this.applyDefaultPlayerConfig()
},
onOpening() {
this.setStatus('正在连接…')
},
/**
* 缓冲进度(开播、快进后常见)。
* percent 到 100 后通常会再来 @playing。
*/
onBuffering(e : any) {
const percent = this.readNum(this.eventMap(e), 'percent', 0)
if (percent >= 100) {
this.setStatus('缓冲中 100%')
return
}
this.setStatus('缓冲中 ' + Math.floor(percent) + '%')
},
() {
this.playPauseLabel = '暂停'
this.setStatus(this.seekable ? '播放中(点播)' : '播放中(直播)')
},
onPaused() {
this.playPauseLabel = '播放'
this.setStatus('已暂停')
},
onStopped() {
this.playPauseLabel = '播放'
this.setStatus('已停止')
},
() {
this.playPauseLabel = '播放'
this.setStatus('播放结束')
},
onError(e : any) {
const message = this.readStr(this.eventMap(e), 'message')
this.setStatus('错误:' + message)
this.toast(message.length > 0 ? message : '播放出错')
},
/** 是否可拖进度:1 点播,0 直播 */
ableChange(e : any) {
this.seekable = this.readFlag(this.eventMap(e), 'seekable')
},
/**
* 进度刷新:更新进度条与时间文字。
* 用户拖动中或非 seekable 时忽略,避免跳动。
*/
(e : any) {
if (!this.seekable || this.userSeeking) return
const data = this.eventMap(e)
const currentMs = this.readNum(data, 'currentMs', 0)
const totalMs = this.readNum(data, 'totalMs', 0)
const position = this.readNum(data, 'position', 0)
this.progress = Math.floor(position * 1000)
const p = this.player()
if (p != null) {
this.playTimeText = p.formatTime(currentMs) + ' / ' + p.formatTime(totalMs)
}
},
/** 截图结果:success==1 时展示图片 */
onSnapshot(e : any) {
const data = this.eventMap(e)
if (this.readFlag(data, 'success')) {
const path = this.readStr(data, 'filePath')
this.snapshotSrc = path.indexOf('file://') == 0 ? path.substring(7) : path
this.showSnapshot = true
this.toast('截图成功')
} else {
this.toast('截图失败')
}
},
/**
* 录制状态:
* recording=1 →「停止录制」;
* recording=0 →「录制」,并保存 filePath 供回放。
*/
onRecordChange(e : any) {
const data = this.eventMap(e)
const recording = this.readFlag(data, 'recording')
this.recording = recording
this.recordLabel = recording ? '停止录制' : '录制'
if (!recording) {
const path = this.normalizeLocalPath(this.readStr(data, 'filePath'))
if (path.length > 0) {
this.recordPath = path
this.setStatus('录制完成:' + path)
this.toast('录制已保存')
} else {
this.setStatus('录制已停止')
}
} else {
this.setStatus('录制中…')
}
},
},
}
</script>
<style>
/* 页面根布局:纵向铺满 */
.root {
flex: 1;
flex-direction: column;
background-color: #F5F5F5;
}
.root-fullscreen {
background-color: #000000;
}
/* 自定义导航(pages.json navigationStyle=custom) */
.nav-bar {
background-color: #FFFFFF;
}
.status-pad {
height: var(--status-bar-height);
}
.nav-content {
height: 44px;
justify-content: center;
align-items: center;
}
.nav-title {
font-size: 16px;
font-weight: bold;
color: #333333;
}
/* 播放器外框:非全屏固定高度 */
.player-wrap {
margin: 8px 12px 0 12px;
height: 220px;
background-color: #333333;
}
.player {
width: 100%;
height: 220px;
}
/* 伪全屏:铺满整页,盖住下方控件占位 */
.player-wrap-full {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: 0;
height: 100%;
z-index: 100;
}
.player-full {
width: 100%;
height: 100%;
}
.fs-exit-btn {
position: absolute;
right: 16px;
top: 48px;
z-index: 101;
}
.panel {
margin: 4px 12px 0 12px;
background-color: #333333;
padding: 8px;
}
.seek-bar {
width: 100%;
}
.vol-row {
align-items: center;
}
.vol-label {
width: 72px;
color: #FFFFFF;
font-size: 12px;
}
.vol-bar {
flex: 1;
margin-left: 4px;
margin-right: 4px;
}
.row {
flex-direction: row;
align-items: center;
margin-top: 6px;
flex-wrap: wrap;
}
.time-text {
flex: 1;
color: #FFFFFF;
font-size: 12px;
}
.btn-sm {
margin-left: 4px;
}
.btn-xs {
margin-left: 4px;
}
.btn-flex {
flex: 1;
margin: 4px;
}
.btn-block {
margin: 8px 12px;
}
.scroll {
flex: 1;
padding: 12px;
}
.status {
font-size: 13px;
color: #333333;
}
.record-box {
margin-top: 10px;
padding: 10px;
background-color: #FFFFFF;
border-radius: 6px;
}
.record-title {
font-size: 13px;
font-weight: bold;
color: #333333;
}
.record-path {
margin-top: 6px;
font-size: 11px;
color: #666666;
lines: 3;
text-overflow: ellipsis;
}
.record-play {
margin: 10px 0 0 0;
}
.snapshot {
margin-top: 8px;
width: 100%;
height: 160px;
background-color: #EEEEEE;
}
.url-input {
margin-top: 12px;
padding: 8px;
border-width: 1px;
border-color: #DDDDDD;
border-style: solid;
font-size: 13px;
background-color: #FAFAFA;
}
</style>

收藏人数:
购买源码授权版(
试用
使用 HBuilderX 导入示例项目
赞赏(1)
下载 532
赞赏 13
下载 12466078
赞赏 1936
赞赏
京公网安备:11010802035340号