ijk-test.nvue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. <template>
  2. <view class="page">
  3. <!-- 顶部状态条 -->
  4. <view class="topbar">
  5. <text class="topbar-title">播放器测试 · wrs-uts-videoplayer</text>
  6. <text class="topbar-meta">插件: {{ hasPlugin ? '已装' : '未装' }} | 内核: {{ playerCore }} | 缩放: {{ scaleMode }}</text>
  7. </view>
  8. <!-- 视频画布 -->
  9. <view class="stage" :style="stageStyle">
  10. <wrs-uts-videoplayer
  11. v-if="hasPlugin && currentUrl && key > 0"
  12. :key="key"
  13. ref="vp"
  14. class="stage-canvas"
  15. :style="stageStyle"
  16. :params="paramsStr"
  17. @onEvent="onPlayerEvent"
  18. />
  19. <view v-else class="stage-empty">
  20. <text class="stage-empty-text">{{ hasPlugin ? '请选择一个流地址' : '未检测到 wrs-uts-videoplayer 插件' }}</text>
  21. </view>
  22. <view v-if="loading" class="stage-loading">
  23. <text class="stage-loading-text">{{ loadingText }}</text>
  24. </view>
  25. </view>
  26. <!-- 滚动控制区 -->
  27. <scroll-view scroll-y class="controls" show-scrollbar="false">
  28. <!-- 内核选择 -->
  29. <view class="row">
  30. <text class="row-label">播放内核</text>
  31. </view>
  32. <view class="chip-row">
  33. <view
  34. v-for="(core, i) in coreOptions"
  35. :key="'core-' + i"
  36. class="chip"
  37. :class="{ 'chip-active': playerCore === core }"
  38. @tap="selectCore(core)"
  39. >
  40. <text class="chip-text" :class="{ 'chip-text-active': playerCore === core }">{{ core }}</text>
  41. </view>
  42. </view>
  43. <!-- 缩放模式 -->
  44. <view class="row">
  45. <text class="row-label">缩放模式</text>
  46. </view>
  47. <view class="chip-row">
  48. <view
  49. v-for="(m, i) in scaleOptions"
  50. :key="'scale-' + i"
  51. class="chip"
  52. :class="{ 'chip-active': scaleMode === m }"
  53. @tap="selectScale(m)"
  54. >
  55. <text class="chip-text" :class="{ 'chip-text-active': scaleMode === m }">{{ m }}</text>
  56. </view>
  57. </view>
  58. <!-- 测试地址 -->
  59. <view class="row">
  60. <text class="row-label">测试地址(左侧标签是协议类型)</text>
  61. </view>
  62. <view
  63. v-for="(u, i) in urlList"
  64. :key="'url-' + i"
  65. class="url-item"
  66. :class="{ 'url-item-active': currentUrl === u.url }"
  67. @tap="selectUrl(u)"
  68. >
  69. <text class="url-tag" :class="['url-tag-' + u.kind]">{{ u.kind }}</text>
  70. <text class="url-text" :class="{ 'url-text-active': currentUrl === u.url }">{{ u.label }}</text>
  71. </view>
  72. <!-- 手动输入地址 -->
  73. <view class="row" style="margin-top: 24rpx;">
  74. <text class="row-label">手动输入地址</text>
  75. </view>
  76. <view class="custom-row">
  77. <input
  78. class="custom-input"
  79. v-model="customUrl"
  80. placeholder="粘贴 rtmp:// / http(s)://*.m3u8 / *.flv …"
  81. placeholder-style="color:#6a72b3;"
  82. />
  83. <view class="btn btn-primary custom-btn" @tap="useCustomUrl">
  84. <text class="btn-text">使用</text>
  85. </view>
  86. </view>
  87. <view v-if="currentUrl" class="current-url">
  88. <text class="current-url-text">当前: {{ currentUrl }}</text>
  89. </view>
  90. <!-- 控制按钮 -->
  91. <view class="row" style="margin-top: 30rpx;">
  92. <text class="row-label">控制</text>
  93. </view>
  94. <view class="btn-row">
  95. <view class="btn btn-primary" @tap="doPlay"><text class="btn-text">▶ 播放</text></view>
  96. <view class="btn" @tap="doPause"><text class="btn-text">❚❚ 暂停</text></view>
  97. <view class="btn" @tap="doReload"><text class="btn-text">↻ 重载</text></view>
  98. </view>
  99. <view class="btn-row">
  100. <view class="btn btn-muted" @tap="toggleMuted">
  101. <text class="btn-text">{{ muted ? '🔇 静音中' : '🔊 有声' }}</text>
  102. </view>
  103. <view class="btn btn-muted" @tap="clearLog"><text class="btn-text">清空日志</text></view>
  104. </view>
  105. <!-- 事件日志 -->
  106. <view class="row" style="margin-top: 30rpx;">
  107. <text class="row-label">事件日志(最近 {{ logs.length }} 条)</text>
  108. </view>
  109. <view class="log-box">
  110. <view v-for="(line, i) in logs" :key="'log-' + i" class="log-line">
  111. <text class="log-time">{{ line.t }}</text>
  112. <text class="log-text" :class="{ 'log-err': line.err }">{{ line.msg }}</text>
  113. </view>
  114. <view v-if="!logs.length" class="log-line">
  115. <text class="log-empty">暂无事件,点上方"播放"开始测试</text>
  116. </view>
  117. </view>
  118. </scroll-view>
  119. </view>
  120. </template>
  121. <script>
  122. import { hasWrsVideoPlayer, getExternalPlayerApi } from '@/features/livestream/external-player-detect'
  123. const COMMON_RTMP = 'rtmp://ns8.indexforce.com/home/mystream'
  124. // 与 pages/live/hall.vue 约定的 storage key:hall 在 openIjkTest 时写入真实直播流地址,
  125. // 测试页 onLoad 时读取一次并清空。
  126. const IJK_TEST_SEED_KEY = '__ijk_test_seed__'
  127. export default {
  128. data() {
  129. return {
  130. hasPlugin: hasWrsVideoPlayer,
  131. key: 0,
  132. playerCore: 'IjkPlayer',
  133. scaleMode: 'default',
  134. currentUrl: '',
  135. muted: false,
  136. loading: false,
  137. loadingText: '正在连接…',
  138. logs: [],
  139. coreOptions: ['IjkPlayer', 'SystemPlayer', 'Exo2Player', 'AliPlayer'],
  140. scaleOptions: ['default', 'aspectFit', 'aspectFill', 'fill', '16_9', '4_3'],
  141. urlList: [
  142. // ===== 本项目真实流(amazewayhk)—— 最关键的验证目标 =====
  143. {
  144. kind: 'RTMP',
  145. label: '★ 本项目 RTMP(amazewayhk room 124)',
  146. url: 'rtmp://play.amazewayhk.com/live/cb658664a180603d70543d5a84522fec'
  147. },
  148. {
  149. kind: 'M3U8',
  150. label: '★ 本项目 M3U8(amazewayhk room 124)',
  151. url: 'https://play.amazewayhk.com/live/cb658664a180603d70543d5a84522fec.m3u8'
  152. },
  153. // ===== 公开测试源(流不在线时备用) =====
  154. { kind: 'RTMP', label: '公共 RTMP 测试源(indexforce)', url: COMMON_RTMP },
  155. { kind: 'M3U8', label: 'HLS 测试流(mux x36xhzz)', url: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8' },
  156. { kind: 'M3U8', label: 'HLS 大兔子(mux)', url: 'https://test-streams.mux.dev/test_001/stream.m3u8' },
  157. { kind: 'MP4', label: 'MP4 海洋示范(zencdn)', url: 'https://vjs.zencdn.net/v/oceans.mp4' },
  158. { kind: 'RTSP', label: 'RTSP 公开示范(stream.strba.sk)', url: 'rtsp://stream.strba.sk:1935/strba/VYHLAD_JAZERO.stream' }
  159. ],
  160. customUrl: '',
  161. stageWidth: 0,
  162. stageHeight: 0
  163. }
  164. },
  165. computed: {
  166. stageStyle() {
  167. const w = this.stageWidth ? `${this.stageWidth}px` : '100%'
  168. const h = this.stageHeight ? `${this.stageHeight}px` : '420rpx'
  169. return { width: w, height: h }
  170. },
  171. paramsStr() {
  172. if (!this.currentUrl) return JSON.stringify({ businessArray: [] })
  173. const businessArray = [
  174. {
  175. business: 'setUrl',
  176. params: {
  177. url: this.currentUrl,
  178. playerCore: this.playerCore,
  179. autoPlay: true,
  180. scaleMode: this.scaleMode
  181. }
  182. }
  183. ]
  184. if (this.muted) {
  185. businessArray.push({ business: 'setVolume', params: { volume: 0 } })
  186. }
  187. return JSON.stringify({ businessArray })
  188. }
  189. },
  190. onLoad() {
  191. try {
  192. const info = uni.getSystemInfoSync()
  193. this.stageWidth = info.windowWidth
  194. this.stageHeight = Math.round((info.windowWidth * 9) / 16)
  195. } catch (e) {
  196. this.stageWidth = 0
  197. this.stageHeight = 0
  198. }
  199. this.appendLog('页面就绪。插件状态: ' + (this.hasPlugin ? '已装' : '未装'))
  200. if (!this.hasPlugin) {
  201. this.appendLog('⚠️ 请确认 uni_modules/wrs-uts-videoplayer 已装,且用自定义基座调试', true)
  202. }
  203. this.applyHallSeed()
  204. },
  205. onUnload() {
  206. this.currentUrl = ''
  207. },
  208. methods: {
  209. appendLog(msg, isErr) {
  210. const d = new Date()
  211. const pad = (n) => (n < 10 ? '0' + n : '' + n)
  212. const t = `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
  213. this.logs.unshift({ t, msg: String(msg), err: !!isErr })
  214. if (this.logs.length > 50) this.logs.pop()
  215. },
  216. /**
  217. * 从 storage 取 hall 写入的真实直播地址,置顶到 urlList。
  218. * seed 结构见 pages/live/hall.vue#openIjkTest。
  219. * 取完就清,避免下次进测试页拿到过期数据。
  220. */
  221. applyHallSeed() {
  222. let seed = null
  223. try {
  224. seed = uni.getStorageSync(IJK_TEST_SEED_KEY)
  225. } catch (_) {
  226. seed = null
  227. }
  228. if (seed) {
  229. try { uni.removeStorageSync(IJK_TEST_SEED_KEY) } catch (_) {}
  230. }
  231. if (!seed || (!seed.liveurl && !seed.recordurl)) {
  232. this.appendLog('未携带直播地址(直接进入测试页),可手动选公开测试源。')
  233. return
  234. }
  235. const roomLabel = seed.roomTitle ? `${seed.roomTitle}(${seed.roomId})` : `room ${seed.roomId}`
  236. const seeded = []
  237. // 直播流:可能是 "rtmp://...,https://....m3u8" 形式,逐个拆开
  238. if (seed.liveurl) {
  239. String(seed.liveurl)
  240. .split(',')
  241. .map((s) => s.trim())
  242. .filter(Boolean)
  243. .forEach((u) => {
  244. const kind = this.classifyKind(u)
  245. seeded.push({
  246. kind,
  247. label: `🔴 接口实时 ${kind}(${roomLabel})`,
  248. url: u
  249. })
  250. })
  251. }
  252. // 录播 url
  253. if (seed.recordurl) {
  254. const kind = this.classifyKind(seed.recordurl)
  255. seeded.push({
  256. kind,
  257. label: `📼 接口录播 ${kind}(${roomLabel})`,
  258. url: seed.recordurl
  259. })
  260. }
  261. if (!seeded.length) return
  262. this.urlList = seeded.concat(this.urlList)
  263. this.appendLog(`接口注入 ${seeded.length} 条真实地址(${roomLabel},直播态=${seed.isLive ? 'Y' : 'N'})`)
  264. // 自动选第一条真实地址,省一次点击
  265. this.selectUrl(seeded[0])
  266. },
  267. clearLog() {
  268. this.logs = []
  269. },
  270. selectCore(c) {
  271. if (this.playerCore === c) return
  272. this.playerCore = c
  273. this.appendLog('切换内核: ' + c)
  274. if (this.currentUrl) this.doReload()
  275. },
  276. selectScale(s) {
  277. if (this.scaleMode === s) return
  278. this.scaleMode = s
  279. this.appendLog('切换缩放: ' + s)
  280. if (this.currentUrl) this.doReload()
  281. },
  282. selectUrl(u) {
  283. this.currentUrl = u.url
  284. this.appendLog('选择地址: [' + u.kind + '] ' + u.label)
  285. this.appendLog(' → ' + u.url)
  286. this.loading = true
  287. this.loadingText = '正在连接 ' + u.kind + '…'
  288. this.key = this.key + 1
  289. },
  290. useCustomUrl() {
  291. const u = String(this.customUrl || '').trim()
  292. if (!u) {
  293. uni.showToast({ title: '请粘贴 URL', icon: 'none' })
  294. return
  295. }
  296. const kind = this.classifyKind(u)
  297. this.selectUrl({ url: u, kind, label: '手动输入' })
  298. },
  299. classifyKind(url) {
  300. const lower = String(url || '').toLowerCase().split('?')[0]
  301. if (lower.startsWith('rtmp://') || lower.startsWith('rtmps://')) return 'RTMP'
  302. if (lower.startsWith('rtsp://')) return 'RTSP'
  303. if (lower.endsWith('.m3u8')) return 'M3U8'
  304. if (lower.endsWith('.flv') || lower.includes('.flv')) return 'FLV'
  305. if (lower.endsWith('.mp4')) return 'MP4'
  306. return 'HTTP'
  307. },
  308. toggleMuted() {
  309. this.muted = !this.muted
  310. this.appendLog('静音: ' + (this.muted ? '开启' : '关闭'))
  311. if (this.currentUrl) this.doReload()
  312. },
  313. doPlay() {
  314. if (!this.currentUrl) {
  315. uni.showToast({ title: '请先选择地址', icon: 'none' })
  316. return
  317. }
  318. this.appendLog('手动 play')
  319. this.key = this.key + 1
  320. this.loading = true
  321. this.loadingText = '正在连接…'
  322. },
  323. doPause() {
  324. this.appendLog('手动 pause(重新构造参数发送 pause 业务)')
  325. },
  326. doReload() {
  327. this.appendLog('重载: ' + this.playerCore + ' | ' + this.scaleMode)
  328. this.key = this.key + 1
  329. this.loading = true
  330. this.loadingText = '正在重载…'
  331. },
  332. onPlayerEvent(e) {
  333. const detail = (e && e.detail) || e || {}
  334. const opt = detail.opt || '(no-opt)'
  335. switch (opt) {
  336. case 'onLoadView':
  337. this.appendLog('原生 View 加载完成')
  338. break
  339. case 'onReadyToPlay':
  340. this.loading = false
  341. this.appendLog('▶ 开始播放 totalTime=' + (detail.totalTime || 0))
  342. break
  343. case 'onProgress':
  344. // 进度太频繁不打日志,只在第一次出现时打
  345. if (!this._gotProgress) {
  346. this._gotProgress = true
  347. this.appendLog('收到进度更新(流稳定)')
  348. }
  349. this.loading = false
  350. break
  351. case 'onError':
  352. this.loading = false
  353. this.appendLog('❌ 播放错误: ' + JSON.stringify(detail), true)
  354. break
  355. case 'onComplete':
  356. this.loading = false
  357. this.appendLog('⏹ 播放完成: ' + JSON.stringify(detail))
  358. break
  359. case 'onClickFullScreen':
  360. this.appendLog('点击了全屏按钮 isFullScreen=' + detail.isFullScreen)
  361. break
  362. default:
  363. this.appendLog('事件: ' + opt + ' | ' + JSON.stringify(detail))
  364. break
  365. }
  366. }
  367. },
  368. watch: {
  369. currentUrl() {
  370. this._gotProgress = false
  371. }
  372. }
  373. }
  374. </script>
  375. <style>
  376. .page {
  377. flex: 1;
  378. background-color: #0e1230;
  379. }
  380. .topbar {
  381. padding: 20rpx 30rpx;
  382. background-color: #1a1f4a;
  383. border-bottom-width: 1rpx;
  384. border-bottom-color: #2a3070;
  385. border-bottom-style: solid;
  386. }
  387. .topbar-title {
  388. color: #ffffff;
  389. font-size: 32rpx;
  390. font-weight: bold;
  391. }
  392. .topbar-meta {
  393. color: #9aa1d4;
  394. font-size: 22rpx;
  395. margin-top: 6rpx;
  396. }
  397. .stage {
  398. background-color: #000000;
  399. position: relative;
  400. overflow: hidden;
  401. }
  402. .stage-canvas {
  403. background-color: #000000;
  404. }
  405. .stage-empty {
  406. flex: 1;
  407. justify-content: center;
  408. align-items: center;
  409. height: 420rpx;
  410. }
  411. .stage-empty-text {
  412. color: #9aa1d4;
  413. font-size: 26rpx;
  414. }
  415. .stage-loading {
  416. position: absolute;
  417. top: 0;
  418. left: 0;
  419. right: 0;
  420. bottom: 0;
  421. justify-content: center;
  422. align-items: center;
  423. }
  424. .stage-loading-text {
  425. color: #ffffff;
  426. font-size: 26rpx;
  427. background-color: rgba(0, 0, 0, 0.5);
  428. padding: 10rpx 24rpx;
  429. border-radius: 8rpx;
  430. }
  431. .controls {
  432. flex: 1;
  433. padding: 24rpx;
  434. }
  435. .row {
  436. margin-top: 24rpx;
  437. margin-bottom: 10rpx;
  438. }
  439. .row-label {
  440. color: #c2c7f0;
  441. font-size: 24rpx;
  442. }
  443. .chip-row {
  444. flex-direction: row;
  445. flex-wrap: wrap;
  446. }
  447. .chip {
  448. background-color: #1a1f4a;
  449. border-radius: 30rpx;
  450. padding-left: 24rpx;
  451. padding-right: 24rpx;
  452. padding-top: 10rpx;
  453. padding-bottom: 10rpx;
  454. margin-right: 14rpx;
  455. margin-bottom: 14rpx;
  456. border-width: 1rpx;
  457. border-color: #2a3070;
  458. border-style: solid;
  459. }
  460. .chip-active {
  461. background-color: #4a5dff;
  462. border-color: #4a5dff;
  463. }
  464. .chip-text {
  465. color: #9aa1d4;
  466. font-size: 24rpx;
  467. }
  468. .chip-text-active {
  469. color: #ffffff;
  470. font-weight: bold;
  471. }
  472. .url-item {
  473. flex-direction: row;
  474. align-items: center;
  475. background-color: #1a1f4a;
  476. border-radius: 12rpx;
  477. padding: 18rpx 20rpx;
  478. margin-bottom: 12rpx;
  479. border-width: 1rpx;
  480. border-color: #2a3070;
  481. border-style: solid;
  482. }
  483. .url-item-active {
  484. background-color: #2a3070;
  485. border-color: #4a5dff;
  486. }
  487. .url-tag {
  488. font-size: 20rpx;
  489. color: #ffffff;
  490. padding: 4rpx 14rpx;
  491. border-radius: 6rpx;
  492. margin-right: 16rpx;
  493. min-width: 80rpx;
  494. text-align: center;
  495. }
  496. .url-tag-RTMP {
  497. background-color: #ff6b6b;
  498. }
  499. .url-tag-M3U8 {
  500. background-color: #4ecdc4;
  501. }
  502. .url-tag-MP4 {
  503. background-color: #ffa94d;
  504. }
  505. .url-tag-RTSP {
  506. background-color: #ae8fff;
  507. }
  508. .url-tag-FLV {
  509. background-color: #f06292;
  510. }
  511. .url-tag-HTTP {
  512. background-color: #607d8b;
  513. }
  514. .custom-row {
  515. flex-direction: row;
  516. align-items: center;
  517. margin-bottom: 10rpx;
  518. }
  519. .custom-input {
  520. flex: 1;
  521. height: 70rpx;
  522. background-color: #1a1f4a;
  523. border-radius: 12rpx;
  524. padding-left: 20rpx;
  525. padding-right: 20rpx;
  526. color: #ffffff;
  527. font-size: 24rpx;
  528. border-width: 1rpx;
  529. border-color: #2a3070;
  530. border-style: solid;
  531. }
  532. .custom-btn {
  533. margin-left: 14rpx;
  534. margin-bottom: 0;
  535. }
  536. .current-url {
  537. background-color: #0a0d22;
  538. border-radius: 10rpx;
  539. padding: 12rpx 16rpx;
  540. margin-top: 10rpx;
  541. border-width: 1rpx;
  542. border-color: #2a3070;
  543. border-style: solid;
  544. }
  545. .current-url-text {
  546. color: #4ecdc4;
  547. font-size: 20rpx;
  548. font-family: monospace;
  549. }
  550. .url-text {
  551. color: #c2c7f0;
  552. font-size: 24rpx;
  553. flex: 1;
  554. }
  555. .url-text-active {
  556. color: #ffffff;
  557. }
  558. .btn-row {
  559. flex-direction: row;
  560. flex-wrap: wrap;
  561. margin-bottom: 12rpx;
  562. }
  563. .btn {
  564. background-color: #1a1f4a;
  565. border-radius: 12rpx;
  566. padding: 16rpx 30rpx;
  567. margin-right: 14rpx;
  568. margin-bottom: 10rpx;
  569. border-width: 1rpx;
  570. border-color: #2a3070;
  571. border-style: solid;
  572. }
  573. .btn-primary {
  574. background-color: #4a5dff;
  575. border-color: #4a5dff;
  576. }
  577. .btn-muted {
  578. background-color: #2a3070;
  579. border-color: #2a3070;
  580. }
  581. .btn-text {
  582. color: #ffffff;
  583. font-size: 26rpx;
  584. }
  585. .log-box {
  586. background-color: #0a0d22;
  587. border-radius: 12rpx;
  588. padding: 16rpx;
  589. border-width: 1rpx;
  590. border-color: #2a3070;
  591. border-style: solid;
  592. min-height: 200rpx;
  593. }
  594. .log-line {
  595. flex-direction: row;
  596. margin-bottom: 6rpx;
  597. flex-wrap: wrap;
  598. }
  599. .log-time {
  600. color: #6a72b3;
  601. font-size: 20rpx;
  602. margin-right: 10rpx;
  603. }
  604. .log-text {
  605. color: #c2c7f0;
  606. font-size: 22rpx;
  607. flex: 1;
  608. }
  609. .log-err {
  610. color: #ff6b6b;
  611. }
  612. .log-empty {
  613. color: #6a72b3;
  614. font-size: 22rpx;
  615. font-style: italic;
  616. }
  617. </style>