| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628 |
- <template>
- <view class="page">
- <!-- 顶部状态条 -->
- <view class="topbar">
- <text class="topbar-title">播放器测试 · wrs-uts-videoplayer</text>
- <text class="topbar-meta">插件: {{ hasPlugin ? '已装' : '未装' }} | 内核: {{ playerCore }} | 缩放: {{ scaleMode }}</text>
- </view>
- <!-- 视频画布 -->
- <view class="stage" :style="stageStyle">
- <wrs-uts-videoplayer
- v-if="hasPlugin && currentUrl && key > 0"
- :key="key"
- ref="vp"
- class="stage-canvas"
- :style="stageStyle"
- :params="paramsStr"
- @onEvent="onPlayerEvent"
- />
- <view v-else class="stage-empty">
- <text class="stage-empty-text">{{ hasPlugin ? '请选择一个流地址' : '未检测到 wrs-uts-videoplayer 插件' }}</text>
- </view>
- <view v-if="loading" class="stage-loading">
- <text class="stage-loading-text">{{ loadingText }}</text>
- </view>
- </view>
- <!-- 滚动控制区 -->
- <scroll-view scroll-y class="controls" show-scrollbar="false">
- <!-- 内核选择 -->
- <view class="row">
- <text class="row-label">播放内核</text>
- </view>
- <view class="chip-row">
- <view
- v-for="(core, i) in coreOptions"
- :key="'core-' + i"
- class="chip"
- :class="{ 'chip-active': playerCore === core }"
- @tap="selectCore(core)"
- >
- <text class="chip-text" :class="{ 'chip-text-active': playerCore === core }">{{ core }}</text>
- </view>
- </view>
- <!-- 缩放模式 -->
- <view class="row">
- <text class="row-label">缩放模式</text>
- </view>
- <view class="chip-row">
- <view
- v-for="(m, i) in scaleOptions"
- :key="'scale-' + i"
- class="chip"
- :class="{ 'chip-active': scaleMode === m }"
- @tap="selectScale(m)"
- >
- <text class="chip-text" :class="{ 'chip-text-active': scaleMode === m }">{{ m }}</text>
- </view>
- </view>
- <!-- 测试地址 -->
- <view class="row">
- <text class="row-label">测试地址(左侧标签是协议类型)</text>
- </view>
- <view
- v-for="(u, i) in urlList"
- :key="'url-' + i"
- class="url-item"
- :class="{ 'url-item-active': currentUrl === u.url }"
- @tap="selectUrl(u)"
- >
- <text class="url-tag" :class="['url-tag-' + u.kind]">{{ u.kind }}</text>
- <text class="url-text" :class="{ 'url-text-active': currentUrl === u.url }">{{ u.label }}</text>
- </view>
- <!-- 手动输入地址 -->
- <view class="row" style="margin-top: 24rpx;">
- <text class="row-label">手动输入地址</text>
- </view>
- <view class="custom-row">
- <input
- class="custom-input"
- v-model="customUrl"
- placeholder="粘贴 rtmp:// / http(s)://*.m3u8 / *.flv …"
- placeholder-style="color:#6a72b3;"
- />
- <view class="btn btn-primary custom-btn" @tap="useCustomUrl">
- <text class="btn-text">使用</text>
- </view>
- </view>
- <view v-if="currentUrl" class="current-url">
- <text class="current-url-text">当前: {{ currentUrl }}</text>
- </view>
- <!-- 控制按钮 -->
- <view class="row" style="margin-top: 30rpx;">
- <text class="row-label">控制</text>
- </view>
- <view class="btn-row">
- <view class="btn btn-primary" @tap="doPlay"><text class="btn-text">▶ 播放</text></view>
- <view class="btn" @tap="doPause"><text class="btn-text">❚❚ 暂停</text></view>
- <view class="btn" @tap="doReload"><text class="btn-text">↻ 重载</text></view>
- </view>
- <view class="btn-row">
- <view class="btn btn-muted" @tap="toggleMuted">
- <text class="btn-text">{{ muted ? '🔇 静音中' : '🔊 有声' }}</text>
- </view>
- <view class="btn btn-muted" @tap="clearLog"><text class="btn-text">清空日志</text></view>
- </view>
- <!-- 事件日志 -->
- <view class="row" style="margin-top: 30rpx;">
- <text class="row-label">事件日志(最近 {{ logs.length }} 条)</text>
- </view>
- <view class="log-box">
- <view v-for="(line, i) in logs" :key="'log-' + i" class="log-line">
- <text class="log-time">{{ line.t }}</text>
- <text class="log-text" :class="{ 'log-err': line.err }">{{ line.msg }}</text>
- </view>
- <view v-if="!logs.length" class="log-line">
- <text class="log-empty">暂无事件,点上方"播放"开始测试</text>
- </view>
- </view>
- </scroll-view>
- </view>
- </template>
- <script>
- import { hasWrsVideoPlayer, getExternalPlayerApi } from '@/features/livestream/external-player-detect'
- const COMMON_RTMP = 'rtmp://ns8.indexforce.com/home/mystream'
- // 与 pages/live/hall.vue 约定的 storage key:hall 在 openIjkTest 时写入真实直播流地址,
- // 测试页 onLoad 时读取一次并清空。
- const IJK_TEST_SEED_KEY = '__ijk_test_seed__'
- export default {
- data() {
- return {
- hasPlugin: hasWrsVideoPlayer,
- key: 0,
- playerCore: 'IjkPlayer',
- scaleMode: 'default',
- currentUrl: '',
- muted: false,
- loading: false,
- loadingText: '正在连接…',
- logs: [],
- coreOptions: ['IjkPlayer', 'SystemPlayer', 'Exo2Player', 'AliPlayer'],
- scaleOptions: ['default', 'aspectFit', 'aspectFill', 'fill', '16_9', '4_3'],
- urlList: [
- // ===== 本项目真实流(amazewayhk)—— 最关键的验证目标 =====
- {
- kind: 'RTMP',
- label: '★ 本项目 RTMP(amazewayhk room 124)',
- url: 'rtmp://play.amazewayhk.com/live/cb658664a180603d70543d5a84522fec'
- },
- {
- kind: 'M3U8',
- label: '★ 本项目 M3U8(amazewayhk room 124)',
- url: 'https://play.amazewayhk.com/live/cb658664a180603d70543d5a84522fec.m3u8'
- },
- // ===== 公开测试源(流不在线时备用) =====
- { kind: 'RTMP', label: '公共 RTMP 测试源(indexforce)', url: COMMON_RTMP },
- { kind: 'M3U8', label: 'HLS 测试流(mux x36xhzz)', url: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8' },
- { kind: 'M3U8', label: 'HLS 大兔子(mux)', url: 'https://test-streams.mux.dev/test_001/stream.m3u8' },
- { kind: 'MP4', label: 'MP4 海洋示范(zencdn)', url: 'https://vjs.zencdn.net/v/oceans.mp4' },
- { kind: 'RTSP', label: 'RTSP 公开示范(stream.strba.sk)', url: 'rtsp://stream.strba.sk:1935/strba/VYHLAD_JAZERO.stream' }
- ],
- customUrl: '',
- stageWidth: 0,
- stageHeight: 0
- }
- },
- computed: {
- stageStyle() {
- const w = this.stageWidth ? `${this.stageWidth}px` : '100%'
- const h = this.stageHeight ? `${this.stageHeight}px` : '420rpx'
- return { width: w, height: h }
- },
- paramsStr() {
- if (!this.currentUrl) return JSON.stringify({ businessArray: [] })
- const businessArray = [
- {
- business: 'setUrl',
- params: {
- url: this.currentUrl,
- playerCore: this.playerCore,
- autoPlay: true,
- scaleMode: this.scaleMode
- }
- }
- ]
- if (this.muted) {
- businessArray.push({ business: 'setVolume', params: { volume: 0 } })
- }
- return JSON.stringify({ businessArray })
- }
- },
- onLoad() {
- try {
- const info = uni.getSystemInfoSync()
- this.stageWidth = info.windowWidth
- this.stageHeight = Math.round((info.windowWidth * 9) / 16)
- } catch (e) {
- this.stageWidth = 0
- this.stageHeight = 0
- }
- this.appendLog('页面就绪。插件状态: ' + (this.hasPlugin ? '已装' : '未装'))
- if (!this.hasPlugin) {
- this.appendLog('⚠️ 请确认 uni_modules/wrs-uts-videoplayer 已装,且用自定义基座调试', true)
- }
- this.applyHallSeed()
- },
- onUnload() {
- this.currentUrl = ''
- },
- methods: {
- appendLog(msg, isErr) {
- const d = new Date()
- const pad = (n) => (n < 10 ? '0' + n : '' + n)
- const t = `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
- this.logs.unshift({ t, msg: String(msg), err: !!isErr })
- if (this.logs.length > 50) this.logs.pop()
- },
- /**
- * 从 storage 取 hall 写入的真实直播地址,置顶到 urlList。
- * seed 结构见 pages/live/hall.vue#openIjkTest。
- * 取完就清,避免下次进测试页拿到过期数据。
- */
- applyHallSeed() {
- let seed = null
- try {
- seed = uni.getStorageSync(IJK_TEST_SEED_KEY)
- } catch (_) {
- seed = null
- }
- if (seed) {
- try { uni.removeStorageSync(IJK_TEST_SEED_KEY) } catch (_) {}
- }
- if (!seed || (!seed.liveurl && !seed.recordurl)) {
- this.appendLog('未携带直播地址(直接进入测试页),可手动选公开测试源。')
- return
- }
- const roomLabel = seed.roomTitle ? `${seed.roomTitle}(${seed.roomId})` : `room ${seed.roomId}`
- const seeded = []
- // 直播流:可能是 "rtmp://...,https://....m3u8" 形式,逐个拆开
- if (seed.liveurl) {
- String(seed.liveurl)
- .split(',')
- .map((s) => s.trim())
- .filter(Boolean)
- .forEach((u) => {
- const kind = this.classifyKind(u)
- seeded.push({
- kind,
- label: `🔴 接口实时 ${kind}(${roomLabel})`,
- url: u
- })
- })
- }
- // 录播 url
- if (seed.recordurl) {
- const kind = this.classifyKind(seed.recordurl)
- seeded.push({
- kind,
- label: `📼 接口录播 ${kind}(${roomLabel})`,
- url: seed.recordurl
- })
- }
- if (!seeded.length) return
- this.urlList = seeded.concat(this.urlList)
- this.appendLog(`接口注入 ${seeded.length} 条真实地址(${roomLabel},直播态=${seed.isLive ? 'Y' : 'N'})`)
- // 自动选第一条真实地址,省一次点击
- this.selectUrl(seeded[0])
- },
- clearLog() {
- this.logs = []
- },
- selectCore(c) {
- if (this.playerCore === c) return
- this.playerCore = c
- this.appendLog('切换内核: ' + c)
- if (this.currentUrl) this.doReload()
- },
- selectScale(s) {
- if (this.scaleMode === s) return
- this.scaleMode = s
- this.appendLog('切换缩放: ' + s)
- if (this.currentUrl) this.doReload()
- },
- selectUrl(u) {
- this.currentUrl = u.url
- this.appendLog('选择地址: [' + u.kind + '] ' + u.label)
- this.appendLog(' → ' + u.url)
- this.loading = true
- this.loadingText = '正在连接 ' + u.kind + '…'
- this.key = this.key + 1
- },
- useCustomUrl() {
- const u = String(this.customUrl || '').trim()
- if (!u) {
- uni.showToast({ title: '请粘贴 URL', icon: 'none' })
- return
- }
- const kind = this.classifyKind(u)
- this.selectUrl({ url: u, kind, label: '手动输入' })
- },
- classifyKind(url) {
- const lower = String(url || '').toLowerCase().split('?')[0]
- if (lower.startsWith('rtmp://') || lower.startsWith('rtmps://')) return 'RTMP'
- if (lower.startsWith('rtsp://')) return 'RTSP'
- if (lower.endsWith('.m3u8')) return 'M3U8'
- if (lower.endsWith('.flv') || lower.includes('.flv')) return 'FLV'
- if (lower.endsWith('.mp4')) return 'MP4'
- return 'HTTP'
- },
- toggleMuted() {
- this.muted = !this.muted
- this.appendLog('静音: ' + (this.muted ? '开启' : '关闭'))
- if (this.currentUrl) this.doReload()
- },
- doPlay() {
- if (!this.currentUrl) {
- uni.showToast({ title: '请先选择地址', icon: 'none' })
- return
- }
- this.appendLog('手动 play')
- this.key = this.key + 1
- this.loading = true
- this.loadingText = '正在连接…'
- },
- doPause() {
- this.appendLog('手动 pause(重新构造参数发送 pause 业务)')
- },
- doReload() {
- this.appendLog('重载: ' + this.playerCore + ' | ' + this.scaleMode)
- this.key = this.key + 1
- this.loading = true
- this.loadingText = '正在重载…'
- },
- onPlayerEvent(e) {
- const detail = (e && e.detail) || e || {}
- const opt = detail.opt || '(no-opt)'
- switch (opt) {
- case 'onLoadView':
- this.appendLog('原生 View 加载完成')
- break
- case 'onReadyToPlay':
- this.loading = false
- this.appendLog('▶ 开始播放 totalTime=' + (detail.totalTime || 0))
- break
- case 'onProgress':
- // 进度太频繁不打日志,只在第一次出现时打
- if (!this._gotProgress) {
- this._gotProgress = true
- this.appendLog('收到进度更新(流稳定)')
- }
- this.loading = false
- break
- case 'onError':
- this.loading = false
- this.appendLog('❌ 播放错误: ' + JSON.stringify(detail), true)
- break
- case 'onComplete':
- this.loading = false
- this.appendLog('⏹ 播放完成: ' + JSON.stringify(detail))
- break
- case 'onClickFullScreen':
- this.appendLog('点击了全屏按钮 isFullScreen=' + detail.isFullScreen)
- break
- default:
- this.appendLog('事件: ' + opt + ' | ' + JSON.stringify(detail))
- break
- }
- }
- },
- watch: {
- currentUrl() {
- this._gotProgress = false
- }
- }
- }
- </script>
- <style>
- .page {
- flex: 1;
- background-color: #0e1230;
- }
- .topbar {
- padding: 20rpx 30rpx;
- background-color: #1a1f4a;
- border-bottom-width: 1rpx;
- border-bottom-color: #2a3070;
- border-bottom-style: solid;
- }
- .topbar-title {
- color: #ffffff;
- font-size: 32rpx;
- font-weight: bold;
- }
- .topbar-meta {
- color: #9aa1d4;
- font-size: 22rpx;
- margin-top: 6rpx;
- }
- .stage {
- background-color: #000000;
- position: relative;
- overflow: hidden;
- }
- .stage-canvas {
- background-color: #000000;
- }
- .stage-empty {
- flex: 1;
- justify-content: center;
- align-items: center;
- height: 420rpx;
- }
- .stage-empty-text {
- color: #9aa1d4;
- font-size: 26rpx;
- }
- .stage-loading {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- justify-content: center;
- align-items: center;
- }
- .stage-loading-text {
- color: #ffffff;
- font-size: 26rpx;
- background-color: rgba(0, 0, 0, 0.5);
- padding: 10rpx 24rpx;
- border-radius: 8rpx;
- }
- .controls {
- flex: 1;
- padding: 24rpx;
- }
- .row {
- margin-top: 24rpx;
- margin-bottom: 10rpx;
- }
- .row-label {
- color: #c2c7f0;
- font-size: 24rpx;
- }
- .chip-row {
- flex-direction: row;
- flex-wrap: wrap;
- }
- .chip {
- background-color: #1a1f4a;
- border-radius: 30rpx;
- padding-left: 24rpx;
- padding-right: 24rpx;
- padding-top: 10rpx;
- padding-bottom: 10rpx;
- margin-right: 14rpx;
- margin-bottom: 14rpx;
- border-width: 1rpx;
- border-color: #2a3070;
- border-style: solid;
- }
- .chip-active {
- background-color: #4a5dff;
- border-color: #4a5dff;
- }
- .chip-text {
- color: #9aa1d4;
- font-size: 24rpx;
- }
- .chip-text-active {
- color: #ffffff;
- font-weight: bold;
- }
- .url-item {
- flex-direction: row;
- align-items: center;
- background-color: #1a1f4a;
- border-radius: 12rpx;
- padding: 18rpx 20rpx;
- margin-bottom: 12rpx;
- border-width: 1rpx;
- border-color: #2a3070;
- border-style: solid;
- }
- .url-item-active {
- background-color: #2a3070;
- border-color: #4a5dff;
- }
- .url-tag {
- font-size: 20rpx;
- color: #ffffff;
- padding: 4rpx 14rpx;
- border-radius: 6rpx;
- margin-right: 16rpx;
- min-width: 80rpx;
- text-align: center;
- }
- .url-tag-RTMP {
- background-color: #ff6b6b;
- }
- .url-tag-M3U8 {
- background-color: #4ecdc4;
- }
- .url-tag-MP4 {
- background-color: #ffa94d;
- }
- .url-tag-RTSP {
- background-color: #ae8fff;
- }
- .url-tag-FLV {
- background-color: #f06292;
- }
- .url-tag-HTTP {
- background-color: #607d8b;
- }
- .custom-row {
- flex-direction: row;
- align-items: center;
- margin-bottom: 10rpx;
- }
- .custom-input {
- flex: 1;
- height: 70rpx;
- background-color: #1a1f4a;
- border-radius: 12rpx;
- padding-left: 20rpx;
- padding-right: 20rpx;
- color: #ffffff;
- font-size: 24rpx;
- border-width: 1rpx;
- border-color: #2a3070;
- border-style: solid;
- }
- .custom-btn {
- margin-left: 14rpx;
- margin-bottom: 0;
- }
- .current-url {
- background-color: #0a0d22;
- border-radius: 10rpx;
- padding: 12rpx 16rpx;
- margin-top: 10rpx;
- border-width: 1rpx;
- border-color: #2a3070;
- border-style: solid;
- }
- .current-url-text {
- color: #4ecdc4;
- font-size: 20rpx;
- font-family: monospace;
- }
- .url-text {
- color: #c2c7f0;
- font-size: 24rpx;
- flex: 1;
- }
- .url-text-active {
- color: #ffffff;
- }
- .btn-row {
- flex-direction: row;
- flex-wrap: wrap;
- margin-bottom: 12rpx;
- }
- .btn {
- background-color: #1a1f4a;
- border-radius: 12rpx;
- padding: 16rpx 30rpx;
- margin-right: 14rpx;
- margin-bottom: 10rpx;
- border-width: 1rpx;
- border-color: #2a3070;
- border-style: solid;
- }
- .btn-primary {
- background-color: #4a5dff;
- border-color: #4a5dff;
- }
- .btn-muted {
- background-color: #2a3070;
- border-color: #2a3070;
- }
- .btn-text {
- color: #ffffff;
- font-size: 26rpx;
- }
- .log-box {
- background-color: #0a0d22;
- border-radius: 12rpx;
- padding: 16rpx;
- border-width: 1rpx;
- border-color: #2a3070;
- border-style: solid;
- min-height: 200rpx;
- }
- .log-line {
- flex-direction: row;
- margin-bottom: 6rpx;
- flex-wrap: wrap;
- }
- .log-time {
- color: #6a72b3;
- font-size: 20rpx;
- margin-right: 10rpx;
- }
- .log-text {
- color: #c2c7f0;
- font-size: 22rpx;
- flex: 1;
- }
- .log-err {
- color: #ff6b6b;
- }
- .log-empty {
- color: #6a72b3;
- font-size: 22rpx;
- font-style: italic;
- }
- </style>
|