sample-videos.vue 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. <template>
  2. <div class="page-container">
  3. <div class="page-header">
  4. <span class="title">测试视频</span>
  5. </div>
  6. <!-- 配置区域 -->
  7. <div class="config-section">
  8. <el-form label-width="120px">
  9. <el-form-item label="选择测试源">
  10. <el-select v-model="sampleConfig.selected" style="width: 500px" placeholder="请选择测试视频">
  11. <el-option-group v-for="group in sampleVideoGroups" :key="group.label" :label="group.label">
  12. <el-option v-for="item in group.options" :key="item.url" :label="item.name" :value="item.url" />
  13. </el-option-group>
  14. </el-select>
  15. </el-form-item>
  16. <el-form-item>
  17. <el-button type="primary" @click="playSample">播放测试视频</el-button>
  18. </el-form-item>
  19. </el-form>
  20. </div>
  21. <!-- 播放器区域 -->
  22. <div class="player-section">
  23. <div v-if="!currentSrc" class="player-placeholder">
  24. <el-icon :size="60" color="#ddd"><VideoPlay /></el-icon>
  25. <p>请选择测试视频并点击播放</p>
  26. </div>
  27. <VideoPlayer
  28. v-else
  29. ref="playerRef"
  30. player-type="hls"
  31. :src="currentSrc"
  32. :autoplay="playConfig.autoplay"
  33. :muted="playConfig.muted"
  34. :controls="true"
  35. @play="onPlay"
  36. @pause="onPause"
  37. @error="onError"
  38. @loadedmetadata="onLoaded"
  39. />
  40. </div>
  41. <!-- 播放控制 -->
  42. <div class="control-section">
  43. <el-space wrap>
  44. <el-button type="primary" @click="handlePlay">播放</el-button>
  45. <el-button @click="handlePause">暂停</el-button>
  46. <el-button type="danger" @click="handleStop">停止</el-button>
  47. <el-button @click="handleScreenshot">截图</el-button>
  48. <el-button @click="handleFullscreen">全屏</el-button>
  49. <el-divider direction="vertical" />
  50. <el-switch v-model="playConfig.muted" active-text="静音" inactive-text="有声" />
  51. <el-switch v-model="playConfig.autoplay" active-text="自动播放" inactive-text="手动" />
  52. </el-space>
  53. </div>
  54. <!-- 当前状态 -->
  55. <div class="status-section">
  56. <el-descriptions title="当前状态" :column="2" border>
  57. <el-descriptions-item label="当前视频">{{ currentVideoName || '-' }}</el-descriptions-item>
  58. <el-descriptions-item label="视频地址">
  59. <el-text truncated style="max-width: 600px">{{ currentSrc || '-' }}</el-text>
  60. </el-descriptions-item>
  61. </el-descriptions>
  62. </div>
  63. <!-- 日志区域 -->
  64. <div class="log-section">
  65. <div class="log-header">
  66. <h4>事件日志</h4>
  67. <el-button size="small" @click="logs = []">清空</el-button>
  68. </div>
  69. <div class="log-content">
  70. <div v-for="(log, index) in logs" :key="index" class="log-item" :class="log.type">
  71. <span class="time">{{ log.time }}</span>
  72. <span class="message">{{ log.message }}</span>
  73. </div>
  74. <div v-if="logs.length === 0" class="log-empty">暂无日志</div>
  75. </div>
  76. </div>
  77. </div>
  78. </template>
  79. <script setup lang="ts">
  80. import { ref, reactive, computed } from 'vue'
  81. import { ElMessage } from 'element-plus'
  82. import { VideoPlay } from '@element-plus/icons-vue'
  83. import VideoPlayer from '@/components/VideoPlayer.vue'
  84. const playerRef = ref<InstanceType<typeof VideoPlayer>>()
  85. // 测试视频配置
  86. const sampleConfig = reactive({
  87. selected: ''
  88. })
  89. // 播放配置
  90. const playConfig = reactive({
  91. autoplay: false,
  92. muted: true
  93. })
  94. // 当前播放状态
  95. const currentSrc = ref('')
  96. // 测试视频列表(分组)
  97. const sampleVideoGroups = [
  98. {
  99. label: '我的直播流 (Cloudflare Stream)',
  100. options: [
  101. {
  102. name: '我的摄像头直播',
  103. url: 'https://customer-pj89kn2ke2tcuh19.cloudflarestream.com/3c1ae1949e76f200feef94b8f7d093ca/manifest/video.m3u8'
  104. }
  105. ]
  106. },
  107. {
  108. label: '公共测试源',
  109. options: [
  110. { name: 'Big Buck Bunny (HLS)', url: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8' },
  111. { name: 'Sintel (HLS)', url: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8' },
  112. {
  113. name: 'Tears of Steel (HLS)',
  114. url: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8'
  115. },
  116. {
  117. name: 'Apple HLS 测试流',
  118. url: 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8'
  119. }
  120. ]
  121. }
  122. ]
  123. // 所有视频列表(用于查找名称)
  124. const allVideos = sampleVideoGroups.flatMap((group) => group.options)
  125. // 当前视频名称
  126. const currentVideoName = computed(() => {
  127. const video = allVideos.find((v) => v.url === currentSrc.value)
  128. return video?.name || ''
  129. })
  130. // 日志
  131. interface LogItem {
  132. time: string
  133. type: 'info' | 'success' | 'error'
  134. message: string
  135. }
  136. const logs = ref<LogItem[]>([])
  137. function addLog(message: string, type: LogItem['type'] = 'info') {
  138. const time = new Date().toLocaleTimeString()
  139. logs.value.unshift({ time, type, message })
  140. if (logs.value.length > 100) {
  141. logs.value.pop()
  142. }
  143. }
  144. // 播放测试视频
  145. function playSample() {
  146. if (!sampleConfig.selected) {
  147. ElMessage.warning('请选择测试视频')
  148. return
  149. }
  150. currentSrc.value = sampleConfig.selected
  151. const video = allVideos.find((v) => v.url === sampleConfig.selected)
  152. addLog(`播放测试视频: ${video?.name}`, 'success')
  153. }
  154. // 播放控制
  155. function handlePlay() {
  156. playerRef.value?.play()
  157. addLog('播放', 'info')
  158. }
  159. function handlePause() {
  160. playerRef.value?.pause()
  161. addLog('暂停', 'info')
  162. }
  163. function handleStop() {
  164. playerRef.value?.stop()
  165. addLog('停止', 'info')
  166. }
  167. function handleScreenshot() {
  168. playerRef.value?.screenshot()
  169. addLog('截图', 'info')
  170. }
  171. function handleFullscreen() {
  172. playerRef.value?.fullscreen()
  173. addLog('全屏', 'info')
  174. }
  175. // 事件处理
  176. function onPlay() {
  177. addLog('视频开始播放', 'success')
  178. }
  179. function onPause() {
  180. addLog('视频已暂停', 'info')
  181. }
  182. function onLoaded() {
  183. addLog('视频加载完成', 'success')
  184. }
  185. function onError(error: any) {
  186. addLog(`播放错误: ${JSON.stringify(error)}`, 'error')
  187. }
  188. </script>
  189. <style lang="scss" scoped>
  190. .page-container {
  191. padding: 1rem;
  192. min-height: 100vh;
  193. background-color: var(--bg-page);
  194. }
  195. .page-header {
  196. display: flex;
  197. align-items: center;
  198. margin-bottom: 20px;
  199. padding: 15px 20px;
  200. background-color: var(--bg-container);
  201. border-radius: var(--radius-base);
  202. color: var(--text-primary);
  203. .title {
  204. font-size: 18px;
  205. font-weight: 600;
  206. }
  207. }
  208. .config-section {
  209. margin-bottom: 20px;
  210. padding: 20px;
  211. background-color: var(--bg-container);
  212. border-radius: var(--radius-base);
  213. }
  214. .player-section {
  215. height: 500px;
  216. margin-bottom: 20px;
  217. border-radius: var(--radius-base);
  218. overflow: hidden;
  219. background-color: #000;
  220. .player-placeholder {
  221. height: 100%;
  222. display: flex;
  223. flex-direction: column;
  224. align-items: center;
  225. justify-content: center;
  226. color: var(--text-secondary);
  227. p {
  228. margin-top: 15px;
  229. font-size: 14px;
  230. }
  231. }
  232. }
  233. .control-section {
  234. padding: 15px 20px;
  235. background-color: var(--bg-container);
  236. border-radius: var(--radius-base);
  237. margin-bottom: 20px;
  238. }
  239. .status-section {
  240. margin-bottom: 20px;
  241. padding: 15px 20px;
  242. background-color: var(--bg-container);
  243. border-radius: var(--radius-base);
  244. }
  245. .log-section {
  246. padding: 15px 20px;
  247. background-color: var(--bg-container);
  248. border-radius: var(--radius-base);
  249. .log-header {
  250. display: flex;
  251. justify-content: space-between;
  252. align-items: center;
  253. margin-bottom: 10px;
  254. h4 {
  255. font-size: 14px;
  256. margin: 0;
  257. color: var(--text-primary);
  258. }
  259. }
  260. .log-content {
  261. max-height: 200px;
  262. overflow-y: auto;
  263. background-color: var(--bg-hover);
  264. border-radius: var(--radius-sm);
  265. padding: 10px;
  266. }
  267. .log-item {
  268. font-size: 12px;
  269. padding: 4px 0;
  270. border-bottom: 1px solid var(--border-color-light);
  271. color: var(--text-regular);
  272. &:last-child {
  273. border-bottom: none;
  274. }
  275. .time {
  276. color: var(--text-secondary);
  277. margin-right: 10px;
  278. }
  279. &.success .message {
  280. color: var(--color-success);
  281. }
  282. &.error .message {
  283. color: var(--color-danger);
  284. }
  285. }
  286. .log-empty {
  287. text-align: center;
  288. color: var(--text-secondary);
  289. font-size: 12px;
  290. padding: 20px 0;
  291. }
  292. }
  293. </style>