更新记录
1.0.0(2026-09-17)
- iOS、Android万能视频播放器
平台兼容性
uni-app(5.01)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| √ | √ | × | × | × | √ | √ | √ | × |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| × | × | × | × | × | × | × | × | × | × | × | × |
其他
| 多语言 | 暗黑模式 | 宽屏模式 |
|---|---|---|
| × | × | √ |
ijkPlayer 视频播放器(uni-app 兼容式组件)
yt-uniijkplay 是供 uni-app nvue 页面使用的 UTS 兼容式原生播放器组件,当前支持 Android、iOS;不支持 HarmonyOS。
组件标签为 <yt-uniijkplay>,入口位于 utssdk/app-android/index.vue 和 utssdk/app-ios/index.vue。不需要导入、注册组件,也不使用旧版标准式组件的 .uvue、components/ 或 UniNativeViewElement API。
特别提醒
- 购买本插件前,请先试用,请先试用,请先试用,确认满足需求之后再行购买。虚拟物品一旦购买之后无法退款。
- 如有使用上的疑问、bug,可以进交流群联系作者;
- 请在合法范围内使用,若使用本插件做非法开发,本方概不负责;
- 插件需先引入再打自定义基座后运行测试
- 本插件当前为兼容式组件,仅支持 Android / iOS 的 nvue 页面
- uni-app x 项目请使用 ijkPlayer视频播放器(uni-app x,Android/iOS/HarmonyOS),功能与本插件一致,并支持鸿蒙。
- 示例代码的全屏切换仅提供一个思路(全屏切换全部在uni端实现),自己项目需要做全屏切换可自定义导航(便于全屏播放时不显示导航)
- 可下载插件提供的示例项目测试、试用。
支持的主要功能:
- 播放 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"
]
}
}
}
使用前准备
- 本插件是 UTS 原生组件。首次集成,以及修改
utssdk/app-android或utssdk/app-ios后,必须重新制作对应平台的自定义调试基座或重新云打包;标准基座不能加载 IJK 原生依赖。 - 本插件仅用于
nvue页面,例如pages/index/index.nvue;HarmonyOS 不支持 uni-app 兼容式组件。 - 组件必须直接指定明确的逻辑像素宽高。nvue 中不要只依赖 class 的
width:100%,否则 Android 原生 View 可能只得到默认宽度。
<!-- nvue 页面:宽高直接传给组件 -->
<yt-uniijkplay ref="ijkVideo" :style="playerStyle"></yt-uniijkplay>
computed: {
playerStyle() {
const screen = uni.getSystemInfoSync()
return {
width: (screen.windowWidth - 24) + 'px',
height: '200px'
}
}
}
完整 uni-app 示例
pages/index/index.nvue 是本插件的完整、可运行示例代码,应作为使用和调用方法的唯一基准直接复制或改造。它包含组件布局、播放控制、MP4/HLS/RTSP 示例地址、低延迟配置、事件兼容、截图预览、录制回放、状态查询和横屏全屏。README 不重复维护另一份示例页面,避免文档和项目代码脱节。
组件绑定与调用
组件不需要 import、components 注册或实例类型声明。在 nvue 的 template 中直接使用:
<yt-uniijkplay
ref="ijkVideo"
:style="playerStyle"
@ready="onReady"
@opening="onOpening"
@buffering="onBuffering"
@playing=""
@paused="onPaused"
@stopped="onStopped"
@ended=""
@error="onError"
@timeupdate=""
@seekablechange="ableChange"
@snapshot="onSnapshot"
@recordchange="onRecordChange"
@state="onState">
</yt-uniijkplay>
在页面 methods 中使用 ref 调用。必须先收到 ready 事件,再调用 play(url)。
methods: {
player() {
return this.$refs.ijkVideo
},
onReady() {
this.player().play('https://example.com/video.mp4')
},
onPause() {
this.player().pause()
}
}
方法
| 方法 | 参数 | 说明 |
|---|---|---|
play(url) |
string |
播放 MP4、HLS、RTSP、RTMP 或本地绝对路径。 |
pause() / resume() / stop() |
无 | 暂停、继续、停止;停止后需重新 play(url)。 |
togglePlayPause() |
无 | 切换播放或暂停。 |
seekTo(position) |
number,0~1 |
跳转到归一化进度,仅点播有效。 |
seekBySeconds(seconds) |
number |
相对跳转;正数快进、负数快退。 |
fastForward(seconds) / rewind(seconds) |
number |
快进或快退指定秒数。 |
setSpeed(rate) |
1/2/4/8/16 |
设置点播倍速。 |
setVolume(volume) |
0~100 |
设置音量。 |
setAspect(mode) |
auto、4:3、16:9 |
设置显示比例。 |
makeSnapshot() |
无 | 异步截图,结果由 snapshot 返回。 |
startRecording() / stopRecording() / toggleRecording() |
无 | 录制操作;最终状态和路径以 recordchange 为准。 |
queryState() |
无 | 异步查询状态,由 state 返回。 |
setPlayerConfig(options) |
string[] | null |
设置下次播放使用的 IJK/FFmpeg 参数;null 为恢复默认。 |
resetPlayerConfig() |
无 | 恢复默认播放器参数。 |
nvue 不支持可靠的兼容式组件同步返回值。不要依赖 isPlaying()、getTime()、getLength()、isSeekable() 等同步返回值驱动页面;请使用 timeupdate、seekablechange、recordchange、state 事件。
RTSP / RTMP 低延迟配置
MP4、HLS 和本地文件先调用 resetPlayerConfig()。RTSP/RTMP 建议在播放前配置:
const player = this.player()
player.setPlayerConfig([
'--rtsp-tcp',
'--network-caching=300',
'--packet-buffering=0',
'--framedrop=1',
'--analyzeduration=1000000',
'--probesize=32768'
])
player.play('rtsp://user:password@192.168.1.10:554/Streaming/Channels/102')
RTSP/RTMP 通常是直播流:总时长为 0,进度条、快进快退和倍速应保持禁用。
事件与 Android / iOS 差异
| 事件 | 主要字段 |
|---|---|
buffering |
percent |
error |
message |
timeupdate |
currentMs、totalMs、position |
seekablechange |
seekable(1 或 0) |
snapshot |
success(1 或 0)、filePath |
recordchange |
recording(1 或 0)、filePath |
state |
playing、recording、seekable、volume、currentMs、totalMs、position |
Android 与 iOS 的事件回调格式不同:
- Android 通常直接传
Map,通过event.get('字段名')读取。 - iOS 会以
detail格式发送,自定义字段在event.detail内,例如event.detail.currentMs、event.detail.filePath。
请统一解析事件;完整示例的 eventData、field、numberField、flag 已包含兼容处理。
eventData(event) {
if (!event) return {}
if (typeof event.get === 'function') {
const detail = event.get('detail')
return detail || event // Android Map,或 Android 包装的 detail
}
return event.detail || event // iOS:字段在 detail 内
},
field(data, key, fallback) {
if (!data) return fallback
const value = typeof data.get === 'function' ? data.get(key) : data[key]
return value == null ? fallback : value
}
截图、预览与录制
截图成功时 snapshot.filePath 是 App 沙盒内的绝对路径。
- Android:可直接将绝对路径用于 nvue
<image>预览。 - iOS:nvue
<image>预览必须在绝对路径前拼接file://。 原始路径为/var/mobile/.../snapshot.jpg时,图片src必须为file:///var/mobile/.../snapshot.jpg。不添加此前缀会出现截图文件已保存、但预览空白的问题。 - 播放本地视频和录制文件时,使用不带
file://的原生绝对路径。
normalizePath(path) {
return path.indexOf('file://') === 0 ? path.substring(7) : path
},
snapshotPreviewUrl(path) {
const localPath = this.normalizePath(path)
if (uni.getSystemInfoSync().platform === 'ios' && localPath.indexOf('/') === 0) {
return 'file://' + localPath
}
return localPath
},
onSnapshot(event) {
const data = this.eventData(event)
const path = this.field(data, 'filePath', '')
if (Number(this.field(data, 'success', 0)) === 1 && path) {
this.snapshotSrc = this.snapshotPreviewUrl(path)
}
}
录制完成后 recordchange.filePath 为本地 MP4 绝对路径;去掉可能存在的 file:// 前缀后传给 play(path) 即可回放。
平台注意事项
Android
- 最低 Android 7.0(API 24),支持
armeabi-v7a、arm64-v8a。 - 原生播放器操作和事件已切回主线程,请使用当前版本的
app-android/index.vue。
iOS
- 最低 iOS 12.0,使用
ijkplayerssl 1.1.4。 - 播放局域网 RTSP 需要本地网络权限。
- iOS 回调与 Android 不同:读取事件字段时使用
event.detail。 - iOS 截图保存成功但预览空白时,确认
<image>的src已拼接file://。
HarmonyOS
兼容式组件不支持 HarmonyOS。本插件不包含 utssdk/app-harmony,也不提供 HarmonyOS 播放、截图或录制能力。
更多好用插件推荐
- 高德定位连续定位后台定位保活定位
- 百度汽车摩托车导航插件
- 百度鹰眼轨迹插件支持后台采集、保活
- 百度定位插件、连续定位、保活、坐标系转换、支持双端
- 计步器插件,支持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视频播放器
uniapp完整示例-试用调试可直接复制
<!--
yt-uniijkplay 兼容式组件(Android / iOS)nvue 调用示例。
先制作包含 ijkPlayer 原生依赖的自定义基座,再在真机上运行。
nvue 不支持可靠的 UTS 组件同步返回值:状态通过 @state 等事件获取。
-->
<template>
<view class="root" :class="{ 'root-full': isFullscreen }">
<!-- 全屏只改变布局,不用 v-if 销毁/重建播放器。nvue 原生组件必须直接指定实际宽高,不能依赖 width:100%。 -->
<view class="player-wrap" :class="{ 'player-wrap-full': isFullscreen }">
<yt-uniijkplay ref="ijkVideo" :style="playerStyle" @ready="onReady"
@opening="onOpening" @buffering="onBuffering" @playing="" @paused="onPaused"
@stopped="onStopped" @ended="" @error="onError" @timeupdate=""
@seekablechange="ableChange" @snapshot="onSnapshot" @recordchange="onRecordChange"
@state="onState"></yt-uniijkplay>
<button v-if="isFullscreen" class="exit-full" size="mini" @click="exitFullscreen">退出全屏</button>
</view>
<view v-if="!isFullscreen" class="controls">
<!-- progress 0~1000 映射到 seekTo(0~1);直播不可拖动。 -->
<slider :value="progress" :min="0" :max="1000" :disabled="!seekable" activeColor="#4CAF50"
@changing="Changing" @change="Change" />
<text class="time">{{ playTimeText }}</text>
<!-- nvue 的 flex 行不会自动换行;每行最多三个等宽按钮,避免横向裁剪。 -->
<view class="row">
<button class="button" size="mini" type="primary" @click="Play">{{ playPauseLabel }}</button>
<button class="button" size="mini" @click="onSnapshotClick">截图</button>
<button class="button" size="mini" @click="onRecordClick">{{ recordLabel }}</button>
</view>
<view class="row">
<button class="button" size="mini" @click="onQueryState">状态</button>
<button class="button" size="mini" @click="enterFullscreen">全屏</button>
<view class="button-placeholder"></view>
</view>
<view class="row">
<button class="button" size="mini" @click="onPause">暂停</button>
<button class="button" size="mini" @click="">继续</button>
<button class="button" size="mini" @click="onStop">停止</button>
</view>
<view class="row">
<button class="button" size="mini" :disabled="!seekable" @click="onFastForward">快进10s</button>
<button class="button" size="mini" :disabled="!seekable" @click="onRewind">快退10s</button>
<view class="button-placeholder"></view>
</view>
<view class="row">
<button class="button" size="mini" :disabled="!seekable" @click="BySeconds(-5)">相对后退5s</button>
<button class="button" size="mini" :disabled="!seekable" @click="BySeconds(5)">相对前进5s</button>
</view>
<!-- 倍速和 seek 只对可拖动的点播开放。 -->
<view class="row">
<button class="button" size="mini" :disabled="!seekable" @click="onSpeed(1)">1x</button>
<button class="button" size="mini" :disabled="!seekable" @click="onSpeed(2)">2x</button>
<button class="button" size="mini" :disabled="!seekable" @click="onSpeed(4)">4x</button>
</view>
<view class="row">
<button class="button" size="mini" :disabled="!seekable" @click="onSpeed(8)">8x</button>
<button class="button" size="mini" :disabled="!seekable" @click="onSpeed(16)">16x</button>
<view class="button-placeholder"></view>
</view>
<view class="row">
<button class="button" size="mini" @click="onAspect('auto')">原比例</button>
<button class="button" size="mini" @click="onAspect('4:3')">4:3</button>
<button class="button" size="mini" @click="onAspect('16:9')">16:9</button>
</view>
<view class="row">
<button class="button" size="mini" @click="onVolume(0)">静音</button>
<button class="button" size="mini" @click="onVolume(100)">最大音量</button>
<view class="button-placeholder"></view>
</view>
<view class="volume-row">
<text class="volume-label">音量 {{ volume }}</text>
<slider class="volume-slider" :value="volume" :min="0" :max="100" activeColor="#4CAF50"
@changing="onVolumeChanging" @change="" />
</view>
</view>
<scroll-view v-if="!isFullscreen" class="details" scroll-y="true">
<text class="status">状态:{{ statusText }}</text>
<text class="status">查询结果:{{ stateText }}</text>
<!-- filePath 是 App 沙盒本地路径;不要把示例内的 RTSP 地址当公共测试流。 -->
<view v-if="recordPath" class="result-box">
<text class="path">录制文件:{{ recordPath }}</text>
<button size="mini" @click="playRecordedVideo">播放录制视频</button>
</view>
<!-- iOS nvue 预览 App 沙盒图片需使用 file:/// 形式,不能直接绑定 Swift 返回的 /var/mobile/... 路径。 -->
<image v-if="snapshotSrc" class="snapshot" :src="snapshotSrc" mode="aspectFit"
@error="onSnapshotImageError"></image>
<input class="url-input" v-model="inputUrl" placeholder="MP4 / HLS / RTSP / RTMP / 本地绝对路径" />
<view class="row">
<button class="button" size="mini" type="primary" @click="onPlayUrl">播放输入地址</button>
<button class="button" size="mini" @click="playSampleMp4">示例 MP4</button>
<view class="button-placeholder"></view>
</view>
<view class="row">
<button class="button" size="mini" @click="playSampleHls">示例 HLS</button>
<button class="button" size="mini" @click="playSampleRtsp">示例 RTSP</button>
<view class="button-placeholder"></view>
</view>
<text class="hint">RTSP/RTMP 播放前会自动应用低延迟参数;示例 RTSP 需要设备能访问该局域网地址。</text>
</scroll-view>
</view>
</template>
<script>
export default {
data() {
const screen = uni.getSystemInfoSync()
return {
ready: false,
isFullscreen: false,
screenWidth: screen.windowWidth,
screenHeight: screen.windowHeight,
statusText: '等待引擎就绪',
stateText: '尚未查询',
playTimeText: '00:00 / 00:00',
playPauseLabel: '播放',
recordLabel: '录制',
recording: false,
recordPath: '',
snapshotSrc: '',
seekable: false,
userSeeking: false,
progress: 0,
volume: 100,
inputUrl: '',
// 与 IJKPlayDemo 示例页保持一致的 MP4、HLS 与局域网 RTSP 测试地址。
sampleMp4: 'https://vd3.bdstatic.com/mda-jggr72s70c52d06f/sc/mda-jggr72s70c52d06f.mp4',
sampleHls: 'https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8',
//例如:rtsp://admin:测试@192.168.11.xx:554/Streaming/Channels/102
sampleRtsp: 'rtsp://admin:<自己的用户名>@<ip>:554/Streaming/Channels/102'
}
},
computed: {
playerStyle() {
// 页面 CSS 的 px 是逻辑像素;两侧各 12px 外边距。
// 必须将尺寸直接传给兼容式组件,否则 width:100% 在 nvue 上可能只得到 100px。
return {
width: Math.max(1, this.screenWidth - (this.isFullscreen ? 0 : 24)) + 'px',
height: (this.isFullscreen ? this.screenHeight : 200) + 'px'
}
}
},
(event) {
// 横竖屏切换后重算原生 View 尺寸,保持同一个播放器实例和当前播放进度。
const size = event && event.size ? event.size : uni.getSystemInfoSync()
this.screenWidth = size.windowWidth
this.screenHeight = size.windowHeight
},
onBackPress() {
// 横屏时先退出全屏,而不是直接离开页面。
if (this.isFullscreen) {
this.exitFullscreen()
return true
}
return false
},
onUnload() {
// 屏幕方向锁定跨页面生效,退出页面必须解除。
if (this.isFullscreen) this.exitFullscreen()
},
methods: {
/** 兼容式组件在 nvue 中用 ref 调方法,不导入 .uvue 实例类型。 */
player() {
return this.$refs.ijkVideo
},
toast(message) {
uni.showToast({
title: message,
icon: 'none'
})
},
/** $emit(name, Map) 经 nvue 桥接后可能成为 Map、普通对象或 event.detail。 */
eventData(event) {
if (!event) return {}
// iOS 兼容式组件可能把自定义字段直接放在事件对象上,
// 此时 event.detail 即使存在,也不一定是播放器数据。
const keys = ['currentMs', 'totalMs', 'seekable', 'success', 'filePath',
'recording', 'playing', 'volume', 'percent', 'message']
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const value = typeof event.get === 'function' ? event.get(key) : event[key]
if (value !== undefined && value !== null) return event
}
if (typeof event.get === 'function') {
const detail = event.get('detail')
return detail ? this.eventData(detail) : event
}
return event.detail ? this.eventData(event.detail) : event
},
field(data, key, fallback) {
if (!data) return fallback
const value = typeof data.get === 'function' ? data.get(key) : data[key]
return value == null ? fallback : value
},
numberField(data, key, fallback) {
const value = Number(this.field(data, key, fallback))
return isNaN(value) ? fallback : value
},
flag(data, key) {
const value = this.field(data, key, 0)
return value === 1 || value === '1' || value === true
},
formatTime(ms) {
// nvue 不依赖组件 formatTime() 的同步返回值,直接格式化事件中的毫秒数。
const seconds = Math.floor(Math.max(0, ms) / 1000)
const min = Math.floor(seconds / 60)
const sec = seconds % 60
return (min < 10 ? '0' : '') + min + ':' + (sec < 10 ? '0' : '') + sec
},
normalizePath(path) {
return path.indexOf('file://') === 0 ? path.substring(7) : path
},
snapshotPreviewUrl(path) {
const localPath = this.normalizePath(path)
// iOS 的 nvue <image> 使用 file URL 加载应用沙盒绝对路径;
// 播放录制文件仍使用未加前缀的原生绝对路径。
if (uni.getSystemInfoSync().platform === 'ios' && localPath.indexOf('/') === 0) {
return 'file://' + localPath
}
return localPath
},
isLiveUrl(url) {
return /^(rtsp|rtmp):\/\//i.test(url)
},
/** 每次切换播放源前设置参数;低延迟参数仅针对 RTSP/RTMP。 */
configureFor(url) {
const player = this.player()
if (!player) return
if (this.isLiveUrl(url)) {
player.setPlayerConfig([
'--rtsp-tcp', '--network-caching=300', '--packet-buffering=0',
'--framedrop=1', '--analyzeduration=1000000', '--probesize=32768'
])
} else {
player.resetPlayerConfig()
}
},
playUrl(url) {
if (!this.ready || !this.player()) {
this.toast('播放器尚未就绪');
return
}
if (!url) {
this.toast('请输入播放地址');
return
}
this.configureFor(url)
this.seekable = false
this.progress = 0
this.playTimeText = '00:00 / 00:00'
this.statusText = '正在打开:' + url
this.player().play(url)
},
onPlayUrl() {
this.playUrl(this.inputUrl.trim())
},
playSampleMp4() {
this.inputUrl = this.sampleMp4;
this.playUrl(this.inputUrl)
},
playSampleHls() {
this.inputUrl = this.sampleHls;
this.playUrl(this.inputUrl)
},
playSampleRtsp() {
this.inputUrl = this.sampleRtsp;
this.playUrl(this.inputUrl)
},
playRecordedVideo() {
if (!this.recordPath) return
this.inputUrl = this.normalizePath(this.recordPath)
this.playUrl(this.inputUrl)
},
/** 播放控制:togglePlayPause 只能切换当前媒体,第一次播放请点“播放输入地址”。 */
Play() {
const player = this.player();
if (player) player.togglePlayPause()
},
onPause() {
const player = this.player();
if (player) player.pause()
},
() {
const player = this.player();
if (player) player.resume()
},
onStop() {
const player = this.player()
if (player) player.stop()
this.seekable = false
this.progress = 0
this.playTimeText = '00:00 / 00:00'
},
onSpeed(rate) {
const player = this.player();
if (player && this.seekable) player.setSpeed(rate)
},
onAspect(mode) {
const player = this.player();
if (player) player.setAspect(mode)
},
onFastForward() {
const player = this.player();
if (player && this.seekable) player.fastForward(10)
},
onRewind() {
const player = this.player();
if (player && this.seekable) player.rewind(10)
},
BySeconds(seconds) {
const player = this.player()
if (player && this.seekable) player.seekBySeconds(seconds)
},
Changing(event) {
if (!this.seekable) return
this.userSeeking = true
this.progress = event.detail.value
},
Change(event) {
this.userSeeking = false
if (!this.seekable) return
this.progress = event.detail.value
const player = this.player()
if (player) player.seekTo(this.progress / 1000)
},
onVolume(value) {
this.volume = Math.max(0, Math.min(100, Math.floor(value)))
const player = this.player()
if (player) player.setVolume(this.volume)
},
onVolumeChanging(event) {
this.onVolume(event.detail.value)
},
(event) {
this.onVolume(event.detail.value)
},
onSnapshotClick() {
const player = this.player();
if (player) player.makeSnapshot()
},
onRecordClick() {
const player = this.player()
if (!player) return
// 最终录制状态和输出路径以 @recordchange 为准,不读取同步 isRecording()。
if (this.recording) player.stopRecording()
else player.startRecording()
},
onQueryState() {
// queryState() 通过 @state 返回,避免 nvue 的同步返回值限制。
const player = this.player()
if (player) player.queryState()
},
/** 横屏是页面能力,不属于插件 API。切换样式时保留同一个播放器实例。 */
enterFullscreen() {
if (this.isFullscreen) return
this.isFullscreen = true
plus.screen.lockOrientation('landscape-primary')
},
exitFullscreen() {
if (!this.isFullscreen) return
this.isFullscreen = false
plus.screen.unlockOrientation()
},
// 以下事件来自 app-android / app-ios 的 index.vue emits。
onReady() {
this.ready = true;
this.statusText = '引擎已就绪'
},
onOpening() {
this.statusText = '正在连接…'
},
onBuffering(event) {
const percent = this.numberField(this.eventData(event), 'percent', 0)
this.statusText = '缓冲中 ' + Math.floor(percent) + '%'
},
() {
this.playPauseLabel = '暂停';
this.statusText = '播放中'
},
onPaused() {
this.playPauseLabel = '播放';
this.statusText = '已暂停'
},
onStopped() {
this.playPauseLabel = '播放';
this.statusText = '已停止'
},
() {
this.playPauseLabel = '播放';
this.statusText = '播放结束'
},
onError(event) {
const message = this.field(this.eventData(event), 'message', '播放出错')
this.statusText = '错误:' + message
this.toast(message || '播放出错')
},
ableChange(event) {
// 点播为 1,直播为 0;以插件实际回调为准,不按 URL 推断。
this.seekable = this.flag(this.eventData(event), 'seekable')
},
(event) {
const data = this.eventData(event)
const current = this.numberField(data, 'currentMs', 0)
const total = this.numberField(data, 'totalMs', 0)
// 某些 iOS 播放器先发进度再发 seekablechange;有有限时长的点播可先启用控制。
if (!this.seekable && total > 500 && !this.isLiveUrl(this.inputUrl)) this.seekable = true
this.playTimeText = this.formatTime(current) + ' / ' + this.formatTime(total)
if (this.seekable && !this.userSeeking) {
this.progress = Math.floor(this.numberField(data, 'position', 0) * 1000)
}
},
onSnapshot(event) {
const data = this.eventData(event)
const path = this.field(data, 'filePath', '')
if (this.flag(data, 'success') && path) {
this.snapshotSrc = this.snapshotPreviewUrl(path)
this.toast('截图成功')
} else this.toast('截图失败')
},
onSnapshotImageError() {
// 区分播放器成功保存文件与预览组件加载文件失败。
this.statusText = '截图已保存,但图片预览加载失败'
},
onRecordChange(event) {
const data = this.eventData(event)
this.recording = this.flag(data, 'recording')
this.recordLabel = this.recording ? '停止录制' : '录制'
const path = this.field(data, 'filePath', '')
if (path) this.recordPath = this.normalizePath(path)
this.statusText = this.recording ? '录制中' : (path ? '录制完成' : '已停止录制')
},
onState(event) {
const data = this.eventData(event)
const playing = this.flag(data, 'playing')
const recording = this.flag(data, 'recording')
const seekable = this.flag(data, 'seekable')
this.seekable = seekable
const current = this.numberField(data, 'currentMs', 0)
const total = this.numberField(data, 'totalMs', 0)
this.stateText = '播放=' + (playing ? '是' : '否') + ' 录制=' + (recording ? '是' : '否') +
' 可拖动=' + (seekable ? '是' : '否') +
' 音量=' + this.numberField(data, 'volume', 100) +
' 时间=' + this.formatTime(current) + '/' + this.formatTime(total)
}
}
}
</script>
<style>
.root {
flex: 1;
flex-direction: column;
background-color: #f5f5f5;
}
.root-full {
background-color: #000;
}
.player-wrap {
margin: 8px 12px 0 12px;
height: 200px;
background-color: #000;
}
.player-wrap-full {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: 0;
height: 100%;
z-index: 10;
}
.exit-full {
position: absolute;
right: 16px;
top: 16px;
z-index: 11;
}
.controls {
padding: 8px 12px;
background-color: #333;
}
.time {
color: #fff;
font-size: 13px;
}
.row {
flex-direction: row;
align-items: center;
margin-top: 6px;
}
.button {
flex: 1;
margin-left: 3px;
margin-right: 3px;
}
.button-placeholder {
flex: 1;
margin-left: 3px;
margin-right: 3px;
}
.volume-row {
flex-direction: row;
align-items: center;
}
.volume-label {
width: 80px;
color: #fff;
font-size: 13px;
}
.volume-slider {
flex: 1;
}
.details {
flex: 1;
padding: 12px;
}
.status {
font-size: 13px;
color: #333;
margin-bottom: 8px;
}
.result-box {
padding: 10px;
background-color: #fff;
margin-top: 8px;
}
.path {
font-size: 12px;
color: #555;
}
.snapshot {
width: 100%;
height: 160px;
margin-top: 8px;
background-color: #eee;
}
.url-input {
height: 42px;
padding-left: 8px;
margin-top: 12px;
background-color: #fff;
border-width: 1px;
border-color: #ddd;
}
.hint {
margin-top: 10px;
font-size: 12px;
color: #666;
}
</style>

收藏人数:
购买源码授权版(
试用
赞赏(0)
下载 619
赞赏 15
下载 12613033
赞赏 1949
赞赏
京公网安备:11010802035340号