更新记录

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.vueutssdk/app-ios/index.vue。不需要导入、注册组件,也不使用旧版标准式组件的 .uvuecomponents/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-v7aarm64-v8a。应用工程也需要在 manifest.json 中配置对应 ABI:

{
  "app-android": {
    "minSdkVersion": 24,
    "distribute": {
      "abiFilters": [
        "armeabi-v7a",
        "arm64-v8a"
      ]
    }
  }
}

使用前准备

  1. 本插件是 UTS 原生组件。首次集成,以及修改 utssdk/app-androidutssdk/app-ios 后,必须重新制作对应平台的自定义调试基座或重新云打包;标准基座不能加载 IJK 原生依赖。
  2. 本插件仅用于 nvue 页面,例如 pages/index/index.nvue;HarmonyOS 不支持 uni-app 兼容式组件。
  3. 组件必须直接指定明确的逻辑像素宽高。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 不重复维护另一份示例页面,避免文档和项目代码脱节。

组件绑定与调用

组件不需要 importcomponents 注册或实例类型声明。在 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) 0100 设置音量。
setAspect(mode) auto4:316:9 设置显示比例。
makeSnapshot() 异步截图,结果由 snapshot 返回。
startRecording() / stopRecording() / toggleRecording() 录制操作;最终状态和路径以 recordchange 为准。
queryState() 异步查询状态,由 state 返回。
setPlayerConfig(options) string[] | null 设置下次播放使用的 IJK/FFmpeg 参数;null 为恢复默认。
resetPlayerConfig() 恢复默认播放器参数。

nvue 不支持可靠的兼容式组件同步返回值。不要依赖 isPlaying()getTime()getLength()isSeekable() 等同步返回值驱动页面;请使用 timeupdateseekablechangerecordchangestate 事件。

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 currentMstotalMsposition
seekablechange seekable10
snapshot success10)、filePath
recordchange recording10)、filePath
state playingrecordingseekablevolumecurrentMstotalMsposition

Android 与 iOS 的事件回调格式不同:

  • Android 通常直接传 Map,通过 event.get('字段名') 读取。
  • iOS 会以 detail 格式发送,自定义字段在 event.detail 内,例如 event.detail.currentMsevent.detail.filePath

请统一解析事件;完整示例的 eventDatafieldnumberFieldflag 已包含兼容处理。

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-v7aarm64-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 播放、截图或录制能力。

更多好用插件推荐

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>

隐私、权限声明

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

Android 需要网络状态、网络访问和唤醒锁权限;iOS 播放局域网 RTSP 时需要本地网络访问权限。

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

插件不采集任何数据

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

暂无用户评论。