更新记录
1.0.0(2026-09-01)
- 新版发布支持iOS、Android、HarmonyOS
平台兼容性
uni-app
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| × | × | × | × | × | × | × | × | × |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| × | × | × | × | × | × | × | × | × | × | × | × |
uni-app x(5.06)
| Chrome | Safari | Android | iOS | 鸿蒙 | 微信小程序 |
|---|---|---|---|---|---|
| × | × | √ | √ | √ | × |
其他
| 多语言 | 暗黑模式 | 宽屏模式 |
|---|---|---|
| × | × | √ |
特别提醒
- 购买本插件前,请先试用,请先试用,请先试用,确认满足需求之后再行购买。虚拟物品一旦购买之后无法退款。
- 如有使用上的疑问、bug,可以进交流群联系作者;
- 请在合法范围内使用,若使用本插件做非法开发,本方概不负责;
- 插件需先引入再打自定义基座后运行测试
- 本插件为标准组件只能用于uniapp-x项目
- 可下载插件提供的示例项目测试、试用。
支持的主要功能:
- 播放 HTTP/HTTPS MP4、HLS(m3u8)、RTSP、RTMP 和本地视频文件。
- 播放、暂停、继续、停止和播放/暂停切换。
- 点播进度拖动、相对跳转、快进和快退。
- 1x、2x、4x、8x、16x 倍速。
- 0~100 音量调节。
- auto、4:3、16:9 画面比例。
- 当前画面截图并取得本地文件路径。
- 当前媒体源录制并取得本地 MP4 路径。
- 播放录制完成的本地视频。
- 播放状态、时长、进度和录制状态查询。
- 页面层横屏全屏示例。
Android 插件支持 armeabi-v7a 和 arm64-v8a。应用工程也需要在 manifest.json 中配置对应 ABI:
{
"app-android": {
"minSdkVersion": 24,
"distribute": {
"abiFilters": [
"armeabi-v7a",
"arm64-v8a"
]
}
}
}
使用前准备
这是包含原生代码、AAR、Pod 和 HAR 的 UTS 插件。修改插件或首次集成后,需要分别重新制作对应平台的自定义调试基座,标准基座不能加载这些原生依赖。
页面通过 easycom 直接使用组件,不需要手动注册:
<yt-ijkplay ref="ijkVideo" class="player"></yt-ijkplay>
播放器必须有明确宽高:
.player {
width: 100%;
height: 220px;
}
页面取得组件实例时,类型名由 <yt-ijkplay> 标签生成,正确写法是:
player() : YtIjkplayComponentPublicInstance | null {
return this.$refs['ijkVideo'] as YtIjkplayComponentPublicInstance | null
}
注意大小写必须是 YtIjkplayComponentPublicInstance,不要写成 YtIJKPlayComponentPublicInstance。
组件与事件绑定
<yt-ijkplay
ref="ijkVideo"
class="player"
@ready="onReady"
@opening="onOpening"
@buffering="onBuffering"
@playing=""
@paused="onPaused"
@stopped="onStopped"
@ended=""
@error="onError"
@timeupdate=""
@seekablechange="ableChange"
@snapshot="onSnapshot"
@recordchange="onRecordChange"
@state="onState"
/>
方法
| 方法 | 参数 | 返回值 | 说明 | |
|---|---|---|---|---|
play(url) |
string |
void |
播放网络地址、本地绝对路径或支持的 content URI。 | |
pause() |
无 | void |
暂停并保留当前位置。 | |
resume() |
无 | void |
从暂停位置继续播放。 | |
stop() |
无 | void |
停止并清空当前媒体,之后需要重新调用 play。 | |
togglePlayPause() |
无 | void |
在播放和暂停之间切换。 | |
seekTo(position) |
number,0~1 |
void |
跳到归一化进度,仅可 seek 的点播有效。 | |
seekBySeconds(seconds) |
number |
void |
相对当前位置跳转,正数快进、负数快退。 | |
fastForward(seconds) |
number |
void |
快进指定秒数。 | |
rewind(seconds) |
number |
void |
快退指定秒数。 | |
setSpeed(rate) |
number |
void |
设置 1、2、4、8、16 倍速,直播通常无效。 | |
setVolume(volume) |
number,0~100 |
void |
设置播放音量。 | |
getVolume() |
无 | number |
获取当前音量,未就绪时返回默认值。 | |
setAspect(mode) |
auto、4:3、16:9 |
void |
设置画面显示比例。 | |
makeSnapshot() |
无 | void |
异步截图,结果通过 snapshot 事件返回。 | |
startRecording() |
无 | boolean |
请求开始录制,最终状态以 recordchange 为准。 | |
stopRecording() |
无 | boolean |
请求停止录制,文件路径通过 recordchange 返回。 | |
toggleRecording() |
无 | boolean |
在开始和停止录制之间切换。 | |
isPlaying() |
无 | boolean |
是否正在播放。 | |
isRecording() |
无 | boolean |
是否正在录制。 | |
isSeekable() |
无 | boolean |
当前媒体是否可拖动进度。 | |
getLength() |
无 | number |
媒体总时长,单位毫秒;直播通常为 0。 | |
getTime() |
无 | number |
当前时间,单位毫秒。 | |
getPosition() |
无 | number |
当前归一化进度 0~1。 | |
formatTime(ms) |
毫秒 number |
string |
格式化为 mm:ss 或 hh:mm:ss。 | |
queryState() |
无 | void |
异步查询完整状态,结果通过 state 事件返回。 | |
setPlayerConfig(options) |
string[] |
null | void |
设置下次播放使用的 IJK/FFmpeg 参数,null 表示恢复默认。 |
resetPlayerConfig() |
无 | void |
恢复默认播放器参数。 |
事件
事件载荷中的布尔字段统一使用数字 0 或 1,避免三端布尔桥接差异。
| 事件 | 载荷字段 | 说明 |
|---|---|---|
ready |
无 | 原生播放器视图已初始化。 |
opening |
无 | 正在连接或解析媒体。 |
buffering |
percent |
缓冲百分比 0~100。 |
playing |
无 | 已进入播放状态。 |
paused |
无 | 已暂停。 |
stopped |
无 | 已停止。 |
ended |
无 | 点播播放完成。 |
error |
message |
播放、截图或录制错误信息。 |
timeupdate |
currentMs、totalMs、position |
点播进度刷新。 |
seekablechange |
seekable |
1 表示可拖动,0 表示不可拖动。 |
snapshot |
success、filePath |
截图结果和本地路径。 |
recordchange |
recording、filePath |
录制开始/停止状态;停止成功时返回 MP4 路径。 |
state |
playing、recording、seekable、volume、currentMs、totalMs、position |
queryState 的状态快照。 |
不同平台可能把事件载荷直接传为 Map,或包装在 detail 中。完整 Demo 的 eventMap、readNum、readStr 和 readFlag 已统一处理这些形式。
播放调用示例
播放 HTTPS MP4
const p = this.player()
if (p != null) {
p.resetPlayerConfig()
p.play('https://example.com/video.mp4')
}
播放 HLS(m3u8)
const p = this.player()
if (p != null) {
p.resetPlayerConfig()
p.play('https://example.com/live/index.m3u8')
}
播放 RTSP 或 RTMP
建议在 play 之前配置 RTSP TCP 和低延迟参数:
const p = this.player()
if (p != null) {
const options : string[] = [
'--rtsp-tcp',
'--network-caching=300',
'--packet-buffering=0',
'--framedrop=1',
'--analyzeduration=1000000',
'--probesize=32768',
]
p.setPlayerConfig(options)
p.play('rtsp://user:password@192.168.1.10:554/Streaming/Channels/102')
}
需要录制声音时不要传 --no-audio。如需 UDP,可根据网络环境使用 --rtsp-udp。
播放本地文件或录制文件
const localPath = '/data/user/0/应用包名/files/IJKPlayerExports/video.mp4'
this.player()?.resetPlayerConfig()
this.player()?.play(localPath)
事件路径可能带 file://,传给 image 或播放器前可先去掉:
normalizeLocalPath(path : string) : string {
if (path.indexOf('file://') == 0) {
return path.substring(7)
}
return path
}
播放控制示例
const p = this.player()
if (p != null) {
p.pause()
p.resume()
p.togglePlayPause()
p.stop()
}
stop() 会清空媒体,停止后继续播放需要重新调用 play(url)。
进度、快进和快退
const p = this.player()
if (p != null && p.isSeekable()) {
p.seekTo(0.5) // 跳到 50%
p.seekBySeconds(10) // 快进 10 秒
p.seekBySeconds(-10) // 快退 10 秒
p.fastForward(5)
p.rewind(5)
}
RTSP/RTMP 直播通常不可 seek。页面应根据 seekablechange 或 isSeekable() 禁用进度条、倍速和快进快退。
倍速、音量和比例
const p = this.player()
if (p != null) {
p.setSpeed(2)
p.setVolume(80)
const volume = p.getVolume()
p.setAspect('auto')
p.setAspect('4:3')
p.setAspect('16:9')
}
截图和预览
onSnapshotClick() {
this.player()?.makeSnapshot()
},
onSnapshot(e : any) {
const data = this.eventMap(e)
if (this.readFlag(data, 'success')) {
const path = this.readStr(data, 'filePath')
this.snapshotSrc = this.normalizeLocalPath(path)
this.showSnapshot = true
}
},
截图是异步操作,不要使用 makeSnapshot() 的返回值判断成功,应监听 snapshot 事件。
录制、停止录制和回放
onRecordClick() {
const p = this.player()
if (p == null) return
if (this.recording || p.isRecording()) {
p.stopRecording()
} else {
p.startRecording()
}
},
onRecordChange(e : any) {
const data = this.eventMap(e)
this.recording = this.readFlag(data, 'recording')
if (!this.recording) {
const path = this.normalizeLocalPath(this.readStr(data, 'filePath'))
if (path.length > 0) {
this.recordPath = path
}
}
},
playRecordedVideo() {
if (this.recordPath.length == 0) return
this.player()?.resetPlayerConfig()
this.player()?.play(this.recordPath)
},
startRecording() 和 stopRecording() 的 boolean 只表示请求是否被原生层接受。最终录制状态和文件路径必须以 recordchange 事件为准。
状态查询
同步查询:
const p = this.player()
if (p != null) {
const playing = p.isPlaying()
const recording = p.isRecording()
const seekable = p.isSeekable()
const totalMs = p.getLength()
const currentMs = p.getTime()
const position = p.getPosition()
const text = p.formatTime(currentMs)
}
异步查询完整状态:
onQueryState() {
this.player()?.queryState()
},
onState(e : any) {
const data = this.eventMap(e)
const playing = this.readFlag(data, 'playing')
const recording = this.readFlag(data, 'recording')
const volume = this.readNum(data, 'volume', 100)
},
播放器配置参数
setPlayerConfig() 建议在下一次 play() 前调用。当前跨端桥接识别的常用参数包括:
--rtsp-tcp、--rtsp-udp--network-caching=300--packet-buffering=0--framedrop=1--analyzeduration=1000000--probesize=32768--no-audio--no-video
恢复默认配置:
this.player()?.resetPlayerConfig()
// 或
this.player()?.setPlayerConfig(null)
各平台 IJK 版本和 HAR 能力不同,未识别的参数可能被忽略。
横屏全屏
全屏是页面层能力,不是 yt-ijkplay 插件方法。实现方式:
- 在
pages.json中关闭系统导航并设定默认竖屏。 - 进入全屏时调用当前页面的
setPageStyle,把pageOrientation改为landscape。 - 通过 class 让播放器容器铺满页面。
- 只切换样式,不使用 v-if 销毁
<yt-ijkplay>。 - 返回键和页面卸载时恢复
portrait。
页面配置:
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "yt-ijkplay",
"navigationStyle": "custom",
"pageOrientation": "portrait"
}
}
关键方法:
applyPageOrientation(orientation : string) {
const pages = getCurrentPages()
if (pages.length == 0) return
const page = pages[pages.length - 1]
page.setPageStyle({
pageOrientation: orientation,
} as UTSJSONObject)
},
enterFullscreen() {
if (this.isFullscreen) return
this.isFullscreen = true
this.applyPageOrientation('landscape')
},
exitFullscreen() {
if (!this.isFullscreen) return
this.isFullscreen = false
this.applyPageOrientation('portrait')
},
平台注意事项
Android
- 最低 API 24,支持
armeabi-v7a和arm64-v8a。 - 开始录制前媒体必须完成 prepare。
- 截图和录制位于 App 外部沙盒的 IJKPlayerExports 目录,不需要公共存储权限。
iOS
- 最低 iOS 12.0,实际 Pod 配置为
ijkplayerssl 1.1.4。 - 局域网 RTSP 需要本地网络权限。
- ReplayKit 采集 App 播放音频,不采集麦克风。
- 截图和录制文件位于
Documents/IJKPlayerExports。 - ReplayKit、局域网摄像头、录制声音和画面方向必须使用真机验证。
鸿蒙
- 依赖随插件提供的
utssdk/app-harmony/libs/ytVlcLibrary.har。 - 底层依赖
@ohos/ijkplayer 2.0.9。 - 同步时长和音量查询受 HAR 能力限制,业务优先监听 timeupdate、seekablechange、recordchange 和 state。
更多好用插件推荐
- 高德定位连续定位后台定位保活定位
- 百度汽车摩托车导航插件
- 百度鹰眼轨迹插件支持后台采集、保活
- 百度定位插件、连续定位、保活、坐标系转换、支持双端
- 计步器插件,支持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
- 自定义相机
- VLC视频播放器
完整 Demo
下面代码与项目中的 pages/index/index.uvue 保持一致,包含全部播放控制、事件处理、截图、录制、录制回放和横屏全屏逻辑。
<!--
=============================================================================
yt-ijkplay 完整调用示例(pages/index/index.uvue)
=============================================================================
本页展示标准组件 <yt-ijkplay> 的常用能力,可直接对照学习:
- 播放网络 MP4、HLS(m3u8)、RTSP/RTMP 和本地绝对路径
- 暂停/继续/停止、状态查询
- 进度条拖动、快进快退、倍速、音量、画面比例
- 截图预览、录制与回放
- 录制完成后取得本地 MP4 路径并直接回放
- 横屏全屏:切换 pageOrientation 并让播放器铺满页面,不销毁播放器
pages.json 需要为本页配置 navigationStyle: "custom"、pageOrientation: "portrait"。
使用前请先「制作自定义调试基座」,再用自定义基座运行。
=============================================================================
-->
<template>
<view class="root" :class="{ 'root-fullscreen': isFullscreen }">
<!-- 使用自定义导航后,非全屏状态自行补齐状态栏和标题栏。 -->
<view v-if="!isFullscreen" class="nav-bar">
<view class="status-pad"></view>
<view class="nav-content">
<text class="nav-title">yt-ijkplay 标准组件演示</text>
</view>
</view>
<!--
播放器必须设置明确宽高;全屏仅切换容器样式,不使用 v-if 销毁播放器。
-->
<view class="player-wrap" :class="{ 'player-wrap-full': isFullscreen }">
<yt-ijkplay ref="ijkVideo" 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" @state="onState"></yt-ijkplay>
<!-- 全屏时保留一个悬浮退出按钮;其它控制面板隐藏。 -->
<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="onQueryState">状态</button>
<button class="btn-sm" size="mini" @click="toggleFullscreen">全屏</button>
</view>
<!-- 分别调用 pause / resume / stop,便于查看三个独立 API 的写法。 -->
<view class="row">
<button class="btn-flex" size="mini" @click="onPause">暂停</button>
<button class="btn-flex" size="mini" @click="">继续</button>
<button class="btn-flex" size="mini" @click="onStop">停止</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" :disabled="!seekable" @click="onSpeed(8)">8x</button>
<button class="btn-xs" size="mini" :disabled="!seekable" @click="onSpeed(16)">16x</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>
<!-- 输入框同时支持网络地址和 App 沙盒内的本地绝对路径 -->
<input class="url-input" v-model="inputUrl" placeholder="输入 MP4/HLS/RTSP/RTMP/本地路径" />
<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="playSampleHls">播放示例 HLS(m3u8)</button>
<button class="btn-block" @click="playSampleRtsp">播放示例 RTSP 监控流</button>
</scroll-view>
</view>
</template>
<script>
/**
* 演示页逻辑说明:
* 1. player() 获取组件实例(类型名由 easycom 按标签生成)
* 2. eventMap / readNum / readFlag 统一解析事件 Map(布尔用 0|1)
* 3. @seekablechange 控制进度条、倍速、快进是否可点
* 4. @recordchange 维护 recording / recordLabel / recordPath
* 5. @state 展示 queryState() 的异步状态快照
* 6. isFullscreen + setPageStyle 实现横屏全屏,不销毁播放器
*/
export default {
data() {
return {
/** 是否处于横屏全屏状态。 */
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',
/** Apple 官方 HLS 点播示例,用于验证 m3u8 播放和拖动。 */
sampleHls: 'https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8',
/** 示例 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-ijkplay 标准组件公开实例。
* 通过公开实例调用 play、seekTo、makeSnapshot、startRecording 等方法。
*/
player() : YtIjkplayComponentPublicInstance | null {
return this.$refs['ijkVideo'] as YtIjkplayComponentPublicInstance | null
},
/**
* 动态设置当前页面方向。
* @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', 'playing', 'volume']
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()) ? '暂停' : '播放'
},
/** MP4、HLS 和本地文件使用插件默认 IJK 参数。 */
applyDefaultPlayerConfig() {
this.player()?.resetPlayerConfig()
},
/**
* RTSP/RTMP 低延迟常用参数。
* iOS 会把这些字符串映射为 IJK/FFmpeg option;鸿蒙以 HAR 支持能力为准。
* @param muteAudio true 时禁用音轨;需要录制声音时必须保持 false。
*/
applyRtspPlayerConfig(muteAudio : boolean = false) {
const opts : string[] = [
'--rtsp-tcp',
'--network-caching=300',
'--packet-buffering=0',
'--framedrop=1',
'--analyzeduration=1000000',
'--probesize=32768',
]
if (muteAudio) opts.push('--no-audio')
this.player()?.setPlayerConfig(opts)
},
/** 判断地址是否属于通常不可拖动的 RTSP/RTMP 直播协议。 */
isLiveAddress(url : string) : boolean {
const value = url.toLowerCase()
return value.indexOf('rtsp://') == 0 || value.indexOf('rtmp://') == 0
},
/** 开始播某个 URL:重置 seekable/进度后 play */
playUrl(url : string) {
this.setStatus('正在打开:' + url)
this.seekable = false
this.progress = 0
this.player()?.play(url)
},
/**
* 播放输入框地址。
* RTSP/RTMP 自动应用低延迟参数,其余协议恢复默认参数。
*/
onPlayUrl() {
const url = this.inputUrl.trim()
if (url.length == 0) {
this.toast('请输入 URL')
return
}
if (this.isLiveAddress(url)) {
this.applyRtspPlayerConfig(false)
} else {
this.applyDefaultPlayerConfig()
}
this.playUrl(url)
},
/** 停止并清空当前媒体;之后需重新调用 play。 */
onStop() {
this.player()?.stop()
this.progress = 0
this.setStatus('已停止')
this.updatePlayPauseLabel()
},
/** 暂停当前媒体并保留播放位置。 */
onPause() {
this.player()?.pause()
},
/** 从暂停位置恢复播放。 */
() {
this.player()?.resume()
},
/** 在播放和暂停之间切换。 */
Play() {
this.player()?.togglePlayPause()
this.updatePlayPauseLabel()
},
/** 截图:结果走 @snapshot(方法名必须是 makeSnapshot) */
onSnapshotClick() {
this.player()?.makeSnapshot()
},
/** 异步查询完整状态,结果由页面的 @state 回调接收。 */
onQueryState() {
this.player()?.queryState()
},
/**
* 录制按钮:优先看页面 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)
},
/** 播放网络 MP4 点播示例。 */
playSampleMp4() {
this.inputUrl = this.sampleMp4
this.applyDefaultPlayerConfig()
this.playUrl(this.sampleMp4)
},
/** 播放 Apple 官方 HLS(m3u8) 点播示例。 */
playSampleHls() {
this.inputUrl = this.sampleHls
this.applyDefaultPlayerConfig()
this.playUrl(this.sampleHls)
},
/** 播放局域网 RTSP 监控流,并启用 TCP/低延迟参数。 */
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('录制中…')
}
},
/**
* `queryState()` 的异步结果。
* 事件包含 playing、recording、seekable、volume、currentMs、totalMs、position。
*/
onState(e : any) {
const data = this.eventMap(e)
const playing = this.readFlag(data, 'playing')
const recording = this.readFlag(data, 'recording')
const seekable = this.readFlag(data, 'seekable')
const volume = Math.floor(this.readNum(data, 'volume', this.volume))
const currentMs = this.readNum(data, 'currentMs', 0)
const totalMs = this.readNum(data, 'totalMs', 0)
const p = this.player()
const time = p != null
? p.formatTime(currentMs) + ' / ' + p.formatTime(totalMs)
: '00:00 / 00:00'
this.toast('播放=' + (playing ? '是' : '否')
+ ' 录制=' + (recording ? '是' : '否')
+ ' 可拖动=' + (seekable ? '是' : '否')
+ ' 音量=' + volume
+ ' 时间=' + time)
},
},
}
</script>
<style>
/* 页面根布局:纵向铺满 */
.root {
flex: 1;
flex-direction: column;
background-color: #F5F5F5;
}
.root-fullscreen {
background-color: #000000;
}
/* pages.json 使用 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: 16px;
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 导入示例项目
赞赏(0)
下载 593
赞赏 15
下载 12551500
赞赏 1948
赞赏
京公网安备:11010802035340号