jessibuca.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. <template>
  2. <div
  3. ref="container"
  4. class="jessibuca-container"
  5. :class="{'jessibuca-fullscreen': fullscreen}"
  6. @dblclick="fullscreenSwich"
  7. >
  8. <div class="jessibuca-player-wrapper"></div>
  9. <div id="buttonsBox" class="buttons-box">
  10. <div class="buttons-box-left">
  11. <i v-if="!playing" class="iconfont icon-play jessibuca-btn" @click="playBtnClick" />
  12. <i v-if="playing" class="iconfont icon-pause jessibuca-btn" @click="pause" />
  13. <i class="iconfont icon-stop jessibuca-btn" @click="destroy" />
  14. <i v-if="isNotMute" class="iconfont icon-audio-high jessibuca-btn" @click="mute()" />
  15. <i v-if="!isNotMute" class="iconfont icon-audio-mute jessibuca-btn" @click="cancelMute()" />
  16. </div>
  17. <div class="buttons-box-right">
  18. <span class="jessibuca-btn">{{ kBps }} kb/s</span>
  19. <!-- <i class="iconfont icon-file-record1 jessibuca-btn"></i>-->
  20. <!-- <i class="iconfont icon-xiangqing2 jessibuca-btn" ></i>-->
  21. <i
  22. class="iconfont icon-camera1196054easyiconnet jessibuca-btn"
  23. style="font-size: 1rem !important"
  24. @click="screenshot"
  25. />
  26. <i class="iconfont icon-shuaxin11 jessibuca-btn" @click="playBtnClick" />
  27. <i v-if="!fullscreen" class="iconfont icon-weibiaoti10 jessibuca-btn" @click="fullscreenSwich" />
  28. <i v-if="fullscreen" class="iconfont icon-weibiaoti11 jessibuca-btn" @click="fullscreenSwich" />
  29. </div>
  30. </div>
  31. </div>
  32. </template>
  33. <!-- #ifdef H5 -->
  34. <script setup>
  35. import { ref, watch, nextTick, onMounted, onBeforeUnmount, getCurrentInstance } from 'vue'
  36. import { useRoute } from 'vue-router'
  37. // Props
  38. const props = defineProps({
  39. videoUrl: String,
  40. error: String,
  41. hasAudio: Boolean,
  42. height: [String, Number]
  43. })
  44. // 获取当前实例 UID (用于管理多个播放器实例)
  45. const instance = getCurrentInstance()
  46. const uid = instance.uid
  47. // 获取路由
  48. const route = useRoute()
  49. // 模板引用
  50. const container = ref(null)
  51. // 响应式数据
  52. const playing = ref(false)
  53. const isNotMute = ref(false)
  54. const quieting = ref(false)
  55. const fullscreen = ref(false)
  56. const loaded = ref(false)
  57. const speed = ref(0)
  58. const performance = ref('')
  59. const kBps = ref(0)
  60. const btnDom = ref(null)
  61. const videoInfo = ref(null)
  62. const volume = ref(1)
  63. const rotate = ref(0)
  64. const vod = ref(true)
  65. const forceNoOffscreen = ref(false)
  66. const playerWidth = ref(0)
  67. const playerHeight = ref(0)
  68. const parentNodeResizeObserver = ref(null)
  69. // 全局播放器实例存储
  70. const jessibucaPlayer = {}
  71. // 判断是否处于全屏状态
  72. const isFullscreen = () => {
  73. // #ifdef H5
  74. return document.fullscreenElement ||
  75. document.msFullscreenElement ||
  76. document.mozFullScreenElement ||
  77. document.webkitFullscreenElement || false
  78. // #endif
  79. // #ifndef H5
  80. return false
  81. // #endif
  82. }
  83. // 更新播放器 DOM 尺寸
  84. const updatePlayerDomSize = () => {
  85. const dom = container.value
  86. if (!dom) return
  87. if (!parentNodeResizeObserver.value) {
  88. // #ifdef H5
  89. parentNodeResizeObserver.value = new ResizeObserver(() => {
  90. updatePlayerDomSize()
  91. })
  92. parentNodeResizeObserver.value.observe(dom.parentNode)
  93. // #endif
  94. }
  95. // 获取父容器尺寸
  96. const boxWidth = dom.parentNode.clientWidth
  97. const boxHeight = dom.parentNode.clientHeight
  98. // 检查是否处于全屏状态
  99. const isFullscreenState = isFullscreen()
  100. let width, height
  101. if (isFullscreenState) {
  102. // 全屏模式,使用窗口尺寸
  103. // #ifdef H5
  104. width = window.innerWidth
  105. height = window.innerHeight
  106. // #endif
  107. } else {
  108. // 非全屏模式,使用16:9比例
  109. width = boxWidth
  110. height = (9 / 16) * width
  111. // 如果计算出的高度超过容器高度,则以容器高度为基准重新计算宽度
  112. if (boxHeight > 0 && height > boxHeight) {
  113. height = boxHeight
  114. width = height * 16 / 9
  115. }
  116. }
  117. // 限制尺寸不超过视口
  118. // #ifdef H5
  119. const clientHeight = Math.min(document.body.clientHeight, document.documentElement.clientHeight)
  120. if (!isFullscreenState && height > clientHeight) {
  121. height = clientHeight
  122. width = (16 / 9) * height
  123. }
  124. // #endif
  125. playerWidth.value = width
  126. playerHeight.value = height
  127. // 应用尺寸到容器
  128. dom.style.width = `${width}px`
  129. dom.style.height = `${height}px`
  130. // 如果播放器存在,更新播放器尺寸
  131. if (playing.value && jessibucaPlayer[uid]) {
  132. jessibucaPlayer[uid].resize(width, height)
  133. }
  134. }
  135. // 公共方法:调整大小
  136. const resize = () => {
  137. updatePlayerDomSize()
  138. }
  139. // 创建播放器
  140. const create = () => {
  141. const options = {
  142. container: container.value,
  143. autoWasm: true,
  144. background: '',
  145. controlAutoHide: false,
  146. debug: false,
  147. decoder: 'static/js/jessibuca/decoder.js',
  148. forceNoOffscreen: false,
  149. hasAudio: typeof props.hasAudio === 'undefined' ? true : props.hasAudio,
  150. heartTimeout: 5,
  151. heartTimeoutReplay: true,
  152. heartTimeoutReplayTimes: 3,
  153. hiddenAutoPause: false,
  154. hotKey: true,
  155. isFlv: false,
  156. isFullResize: false,
  157. isNotMute: isNotMute.value,
  158. isResize: true,
  159. keepScreenOn: true,
  160. loadingText: '请稍等, 视频加载中......',
  161. loadingTimeout: 10,
  162. loadingTimeoutReplay: true,
  163. loadingTimeoutReplayTimes: 3,
  164. openWebglAlignment: false,
  165. operateBtns: {
  166. fullscreen: false,
  167. screenshot: false,
  168. play: false,
  169. audio: false,
  170. record: false
  171. },
  172. recordType: 'mp4',
  173. rotate: 0,
  174. showBandwidth: false,
  175. supportDblclickFullscreen: false,
  176. timeout: 10,
  177. useMSE: true,
  178. useWCS: false,
  179. useWebFullScreen: true,
  180. videoBuffer: 0.1,
  181. wasmDecodeErrorReplay: true,
  182. wcsUseVideoRender: true
  183. }
  184. console.log('Jessibuca -> options: ', options)
  185. // #ifdef H5
  186. jessibucaPlayer[uid] = new window.Jessibuca({ ...options })
  187. const jessibuca = jessibucaPlayer[uid]
  188. jessibuca.on('pause', () => {
  189. playing.value = false
  190. })
  191. jessibuca.on('play', () => {
  192. playing.value = true
  193. })
  194. jessibuca.on('fullscreen', (msg) => {
  195. fullscreen.value = msg
  196. })
  197. jessibuca.on('mute', (msg) => {
  198. isNotMute.value = !msg
  199. })
  200. jessibuca.on('performance', (perf) => {
  201. let show = '卡顿'
  202. if (perf === 2) {
  203. show = '非常流畅'
  204. } else if (perf === 1) {
  205. show = '流畅'
  206. }
  207. performance.value = show
  208. })
  209. jessibuca.on('kBps', (kbps) => {
  210. kBps.value = Math.round(kbps)
  211. })
  212. jessibuca.on('videoInfo', (msg) => {
  213. console.log('Jessibuca -> videoInfo: ', msg)
  214. })
  215. jessibuca.on('audioInfo', (msg) => {
  216. console.log('Jessibuca -> audioInfo: ', msg)
  217. })
  218. jessibuca.on('error', (msg) => {
  219. console.log('Jessibuca -> error: ', msg)
  220. })
  221. jessibuca.on('timeout', (msg) => {
  222. console.log('Jessibuca -> timeout: ', msg)
  223. })
  224. jessibuca.on('loadingTimeout', (msg) => {
  225. console.log('Jessibuca -> timeout: ', msg)
  226. })
  227. jessibuca.on('delayTimeout', (msg) => {
  228. console.log('Jessibuca -> timeout: ', msg)
  229. })
  230. jessibuca.on('playToRenderTimes', (msg) => {
  231. console.log('Jessibuca -> playToRenderTimes: ', msg)
  232. })
  233. // #endif
  234. }
  235. // 播放按钮点击
  236. const playBtnClick = () => {
  237. play(props.videoUrl)
  238. }
  239. // 播放
  240. const play = (url) => {
  241. console.log('Jessibuca -> url: ', url)
  242. if (jessibucaPlayer[uid]) {
  243. destroy()
  244. }
  245. create()
  246. // #ifdef H5
  247. jessibucaPlayer[uid].on('play', () => {
  248. playing.value = true
  249. loaded.value = true
  250. quieting.value = jessibucaPlayer[uid].quieting
  251. })
  252. if (jessibucaPlayer[uid].hasLoaded()) {
  253. jessibucaPlayer[uid].play(url)
  254. } else {
  255. jessibucaPlayer[uid].on('load', () => {
  256. jessibucaPlayer[uid].play(url)
  257. })
  258. }
  259. // #endif
  260. }
  261. // 暂停
  262. const pause = () => {
  263. // #ifdef H5
  264. if (jessibucaPlayer[uid]) {
  265. jessibucaPlayer[uid].pause()
  266. }
  267. // #endif
  268. playing.value = false
  269. performance.value = ''
  270. }
  271. // 截图
  272. const screenshot = () => {
  273. // #ifdef H5
  274. if (jessibucaPlayer[uid]) {
  275. jessibucaPlayer[uid].screenshot()
  276. }
  277. // #endif
  278. }
  279. // 静音
  280. const mute = () => {
  281. // #ifdef H5
  282. if (jessibucaPlayer[uid]) {
  283. jessibucaPlayer[uid].mute()
  284. }
  285. // #endif
  286. }
  287. // 取消静音
  288. const cancelMute = () => {
  289. // #ifdef H5
  290. if (jessibucaPlayer[uid]) {
  291. jessibucaPlayer[uid].cancelMute()
  292. }
  293. // #endif
  294. }
  295. // 销毁播放器
  296. const destroy = () => {
  297. // #ifdef H5
  298. if (jessibucaPlayer[uid]) {
  299. jessibucaPlayer[uid].destroy()
  300. }
  301. if (document.getElementById('buttonsBox') == null && btnDom.value) {
  302. container.value.appendChild(btnDom.value)
  303. }
  304. jessibucaPlayer[uid] = null
  305. // #endif
  306. playing.value = false
  307. performance.value = ''
  308. }
  309. // 全屏切换
  310. const fullscreenSwich = () => {
  311. const isFull = isFullscreen()
  312. if (!isFull) {
  313. // 进入全屏
  314. try {
  315. const containerEl = container.value
  316. // #ifdef H5
  317. // 尝试使用HTML5全屏API
  318. if (containerEl.requestFullscreen) {
  319. containerEl.requestFullscreen()
  320. } else if (containerEl.webkitRequestFullscreen) {
  321. containerEl.webkitRequestFullscreen()
  322. } else if (containerEl.msRequestFullscreen) {
  323. containerEl.msRequestFullscreen()
  324. } else if (containerEl.mozRequestFullScreen) {
  325. containerEl.mozRequestFullScreen()
  326. } else {
  327. // 如果原生API不可用,使用Jessibuca的全屏API
  328. if (jessibucaPlayer[uid]) {
  329. jessibucaPlayer[uid].setFullscreen(true)
  330. }
  331. }
  332. // #endif
  333. // 设置全屏标志
  334. fullscreen.value = true
  335. } catch (e) {
  336. console.error('全屏切换失败:', e)
  337. }
  338. } else {
  339. // 退出全屏
  340. try {
  341. // #ifdef H5
  342. if (document.exitFullscreen) {
  343. document.exitFullscreen()
  344. } else if (document.webkitExitFullscreen) {
  345. document.webkitExitFullscreen()
  346. } else if (document.msExitFullscreen) {
  347. document.msExitFullscreen()
  348. } else if (document.mozCancelFullScreen) {
  349. document.mozCancelFullScreen()
  350. } else {
  351. // 如果原生API不可用,使用Jessibuca的全屏API
  352. if (jessibucaPlayer[uid]) {
  353. jessibucaPlayer[uid].setFullscreen(false)
  354. }
  355. }
  356. // #endif
  357. // 设置全屏标志
  358. fullscreen.value = false
  359. } catch (e) {
  360. console.error('退出全屏失败:', e)
  361. }
  362. }
  363. // 重新计算尺寸
  364. setTimeout(() => {
  365. updatePlayerDomSize()
  366. }, 300)
  367. }
  368. // 监听 videoUrl 变化
  369. watch(() => props.videoUrl, (val) => {
  370. nextTick(() => {
  371. play(val)
  372. })
  373. }, { immediate: true })
  374. // 组件挂载时
  375. onMounted(() => {
  376. // #ifdef H5
  377. const paramUrl = decodeURIComponent(route.params.url || '')
  378. nextTick(() => {
  379. updatePlayerDomSize()
  380. window.onresize = updatePlayerDomSize
  381. if (typeof props.videoUrl === 'undefined' && paramUrl) {
  382. play(paramUrl)
  383. }
  384. btnDom.value = document.getElementById('buttonsBox')
  385. })
  386. // #endif
  387. })
  388. // 组件卸载前
  389. onBeforeUnmount(() => {
  390. // #ifdef H5
  391. if (jessibucaPlayer[uid]) {
  392. jessibucaPlayer[uid].destroy()
  393. }
  394. if (parentNodeResizeObserver.value) {
  395. parentNodeResizeObserver.value.disconnect()
  396. }
  397. // #endif
  398. playing.value = false
  399. loaded.value = false
  400. performance.value = ''
  401. })
  402. // 暴露方法供父组件调用
  403. defineExpose({
  404. resize,
  405. play,
  406. pause,
  407. destroy,
  408. screenshot
  409. })
  410. </script>
  411. <!-- #endif -->
  412. <!-- #ifndef H5 -->
  413. <script setup>
  414. // 非 H5 平台不支持 Jessibuca
  415. console.warn('Jessibuca component is only supported on H5 platform')
  416. </script>
  417. <!-- #endif -->
  418. <style>
  419. .jessibuca-container {
  420. width: 100%;
  421. height: 100%;
  422. background-color: #000000;
  423. margin: 0 auto;
  424. position: relative;
  425. overflow: hidden;
  426. }
  427. .jessibuca-container.jessibuca-fullscreen {
  428. position: fixed;
  429. top: 0;
  430. left: 0;
  431. width: 100vw !important;
  432. height: 100vh !important;
  433. z-index: 9999;
  434. }
  435. .jessibuca-player-wrapper {
  436. width: 100%;
  437. padding-top: 56.25%;
  438. position: relative;
  439. }
  440. .buttons-box {
  441. width: 100%;
  442. height: 28px;
  443. background-color: rgba(43, 51, 63, 0.7);
  444. position: absolute;
  445. display: -webkit-box;
  446. display: -ms-flexbox;
  447. display: flex;
  448. left: 0;
  449. bottom: 0;
  450. user-select: none;
  451. z-index: 10;
  452. }
  453. .jessibuca-btn {
  454. width: 20px;
  455. color: rgb(255, 255, 255);
  456. line-height: 27px;
  457. margin: 0px 10px;
  458. padding: 0px 2px;
  459. cursor: pointer;
  460. text-align: center;
  461. font-size: 0.8rem !important;
  462. }
  463. .buttons-box-right {
  464. position: absolute;
  465. right: 0;
  466. }
  467. </style>