jessibuca.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  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. // #ifdef H5
  142. // H5 环境使用 CDN 加载 decoder
  143. const decoderUrl = '/jessibuca/decoder.js'
  144. // #endif
  145. // #ifdef MP-WEIXIN
  146. // 微信小程序不支持 jessibuca,这里不应该被调用
  147. console.warn('Jessibuca is not supported in WeChat Mini Program')
  148. return
  149. // #endif
  150. const options = {
  151. container: container.value,
  152. autoWasm: true,
  153. background: '',
  154. controlAutoHide: false,
  155. debug: false,
  156. // #ifdef H5
  157. decoder: decoderUrl,
  158. // #endif
  159. // #ifndef H5
  160. decoder: '',
  161. // #endif
  162. forceNoOffscreen: false,
  163. hasAudio: typeof props.hasAudio === 'undefined' ? true : props.hasAudio,
  164. heartTimeout: 5,
  165. heartTimeoutReplay: true,
  166. heartTimeoutReplayTimes: 3,
  167. hiddenAutoPause: false,
  168. hotKey: true,
  169. isFlv: false,
  170. isFullResize: false,
  171. isNotMute: isNotMute.value,
  172. isResize: true,
  173. keepScreenOn: true,
  174. loadingText: '请稍等, 视频加载中......',
  175. loadingTimeout: 10,
  176. loadingTimeoutReplay: true,
  177. loadingTimeoutReplayTimes: 3,
  178. openWebglAlignment: false,
  179. operateBtns: {
  180. fullscreen: false,
  181. screenshot: false,
  182. play: false,
  183. audio: false,
  184. record: false
  185. },
  186. recordType: 'mp4',
  187. rotate: 0,
  188. showBandwidth: false,
  189. supportDblclickFullscreen: false,
  190. timeout: 10,
  191. useMSE: true,
  192. useWCS: false,
  193. useWebFullScreen: true,
  194. videoBuffer: 0.1,
  195. wasmDecodeErrorReplay: true,
  196. wcsUseVideoRender: true
  197. }
  198. console.log('Jessibuca -> options: ', options)
  199. // #ifdef H5
  200. jessibucaPlayer[uid] = new window.Jessibuca({ ...options })
  201. const jessibuca = jessibucaPlayer[uid]
  202. jessibuca.on('pause', () => {
  203. playing.value = false
  204. })
  205. jessibuca.on('play', () => {
  206. playing.value = true
  207. })
  208. jessibuca.on('fullscreen', (msg) => {
  209. fullscreen.value = msg
  210. })
  211. jessibuca.on('mute', (msg) => {
  212. isNotMute.value = !msg
  213. })
  214. jessibuca.on('performance', (perf) => {
  215. let show = '卡顿'
  216. if (perf === 2) {
  217. show = '非常流畅'
  218. } else if (perf === 1) {
  219. show = '流畅'
  220. }
  221. performance.value = show
  222. })
  223. jessibuca.on('kBps', (kbps) => {
  224. kBps.value = Math.round(kbps)
  225. })
  226. jessibuca.on('videoInfo', (msg) => {
  227. console.log('Jessibuca -> videoInfo: ', msg)
  228. })
  229. jessibuca.on('audioInfo', (msg) => {
  230. console.log('Jessibuca -> audioInfo: ', msg)
  231. })
  232. jessibuca.on('error', (msg) => {
  233. console.log('Jessibuca -> error: ', msg)
  234. })
  235. jessibuca.on('timeout', (msg) => {
  236. console.log('Jessibuca -> timeout: ', msg)
  237. })
  238. jessibuca.on('loadingTimeout', (msg) => {
  239. console.log('Jessibuca -> timeout: ', msg)
  240. })
  241. jessibuca.on('delayTimeout', (msg) => {
  242. console.log('Jessibuca -> timeout: ', msg)
  243. })
  244. jessibuca.on('playToRenderTimes', (msg) => {
  245. console.log('Jessibuca -> playToRenderTimes: ', msg)
  246. })
  247. // #endif
  248. }
  249. // 播放按钮点击
  250. const playBtnClick = () => {
  251. play(props.videoUrl)
  252. }
  253. // 播放
  254. const play = (url) => {
  255. console.log('Jessibuca -> url: ', url)
  256. if (jessibucaPlayer[uid]) {
  257. destroy()
  258. }
  259. create()
  260. // #ifdef H5
  261. jessibucaPlayer[uid].on('play', () => {
  262. playing.value = true
  263. loaded.value = true
  264. quieting.value = jessibucaPlayer[uid].quieting
  265. })
  266. if (jessibucaPlayer[uid].hasLoaded()) {
  267. jessibucaPlayer[uid].play(url)
  268. } else {
  269. jessibucaPlayer[uid].on('load', () => {
  270. jessibucaPlayer[uid].play(url)
  271. })
  272. }
  273. // #endif
  274. }
  275. // 暂停
  276. const pause = () => {
  277. // #ifdef H5
  278. if (jessibucaPlayer[uid]) {
  279. jessibucaPlayer[uid].pause()
  280. }
  281. // #endif
  282. playing.value = false
  283. performance.value = ''
  284. }
  285. // 截图
  286. const screenshot = () => {
  287. // #ifdef H5
  288. if (jessibucaPlayer[uid]) {
  289. jessibucaPlayer[uid].screenshot()
  290. }
  291. // #endif
  292. }
  293. // 静音
  294. const mute = () => {
  295. // #ifdef H5
  296. if (jessibucaPlayer[uid]) {
  297. jessibucaPlayer[uid].mute()
  298. }
  299. // #endif
  300. }
  301. // 取消静音
  302. const cancelMute = () => {
  303. // #ifdef H5
  304. if (jessibucaPlayer[uid]) {
  305. jessibucaPlayer[uid].cancelMute()
  306. }
  307. // #endif
  308. }
  309. // 销毁播放器
  310. const destroy = () => {
  311. // #ifdef H5
  312. if (jessibucaPlayer[uid]) {
  313. jessibucaPlayer[uid].destroy()
  314. }
  315. if (document.getElementById('buttonsBox') == null && btnDom.value) {
  316. container.value.appendChild(btnDom.value)
  317. }
  318. jessibucaPlayer[uid] = null
  319. // #endif
  320. playing.value = false
  321. performance.value = ''
  322. }
  323. // 全屏切换
  324. const fullscreenSwich = () => {
  325. const isFull = isFullscreen()
  326. if (!isFull) {
  327. // 进入全屏
  328. try {
  329. const containerEl = container.value
  330. // #ifdef H5
  331. // 尝试使用HTML5全屏API
  332. if (containerEl.requestFullscreen) {
  333. containerEl.requestFullscreen()
  334. } else if (containerEl.webkitRequestFullscreen) {
  335. containerEl.webkitRequestFullscreen()
  336. } else if (containerEl.msRequestFullscreen) {
  337. containerEl.msRequestFullscreen()
  338. } else if (containerEl.mozRequestFullScreen) {
  339. containerEl.mozRequestFullScreen()
  340. } else {
  341. // 如果原生API不可用,使用Jessibuca的全屏API
  342. if (jessibucaPlayer[uid]) {
  343. jessibucaPlayer[uid].setFullscreen(true)
  344. }
  345. }
  346. // #endif
  347. // 设置全屏标志
  348. fullscreen.value = true
  349. } catch (e) {
  350. console.error('全屏切换失败:', e)
  351. }
  352. } else {
  353. // 退出全屏
  354. try {
  355. // #ifdef H5
  356. if (document.exitFullscreen) {
  357. document.exitFullscreen()
  358. } else if (document.webkitExitFullscreen) {
  359. document.webkitExitFullscreen()
  360. } else if (document.msExitFullscreen) {
  361. document.msExitFullscreen()
  362. } else if (document.mozCancelFullScreen) {
  363. document.mozCancelFullScreen()
  364. } else {
  365. // 如果原生API不可用,使用Jessibuca的全屏API
  366. if (jessibucaPlayer[uid]) {
  367. jessibucaPlayer[uid].setFullscreen(false)
  368. }
  369. }
  370. // #endif
  371. // 设置全屏标志
  372. fullscreen.value = false
  373. } catch (e) {
  374. console.error('退出全屏失败:', e)
  375. }
  376. }
  377. // 重新计算尺寸
  378. setTimeout(() => {
  379. updatePlayerDomSize()
  380. }, 300)
  381. }
  382. // 监听 videoUrl 变化
  383. watch(() => props.videoUrl, (val) => {
  384. nextTick(() => {
  385. play(val)
  386. })
  387. }, { immediate: true })
  388. // 组件挂载时
  389. onMounted(() => {
  390. // #ifdef H5
  391. const paramUrl = decodeURIComponent(route.params.url || '')
  392. nextTick(() => {
  393. updatePlayerDomSize()
  394. window.onresize = updatePlayerDomSize
  395. if (typeof props.videoUrl === 'undefined' && paramUrl) {
  396. play(paramUrl)
  397. }
  398. btnDom.value = document.getElementById('buttonsBox')
  399. })
  400. // #endif
  401. })
  402. // 组件卸载前
  403. onBeforeUnmount(() => {
  404. // #ifdef H5
  405. if (jessibucaPlayer[uid]) {
  406. jessibucaPlayer[uid].destroy()
  407. }
  408. if (parentNodeResizeObserver.value) {
  409. parentNodeResizeObserver.value.disconnect()
  410. }
  411. // #endif
  412. playing.value = false
  413. loaded.value = false
  414. performance.value = ''
  415. })
  416. // 暴露方法供父组件调用
  417. defineExpose({
  418. resize,
  419. play,
  420. pause,
  421. destroy,
  422. screenshot
  423. })
  424. </script>
  425. <!-- #endif -->
  426. <style>
  427. .jessibuca-container {
  428. width: 100%;
  429. height: 100%;
  430. background-color: #000000;
  431. margin: 0 auto;
  432. position: relative;
  433. overflow: hidden;
  434. }
  435. .jessibuca-container.jessibuca-fullscreen {
  436. position: fixed;
  437. top: 0;
  438. left: 0;
  439. width: 100vw !important;
  440. height: 100vh !important;
  441. z-index: 9999;
  442. }
  443. .jessibuca-player-wrapper {
  444. width: 100%;
  445. padding-top: 56.25%;
  446. position: relative;
  447. }
  448. .buttons-box {
  449. width: 100%;
  450. height: 28px;
  451. background-color: rgba(43, 51, 63, 0.7);
  452. position: absolute;
  453. display: -webkit-box;
  454. display: -ms-flexbox;
  455. display: flex;
  456. left: 0;
  457. bottom: 0;
  458. user-select: none;
  459. z-index: 10;
  460. }
  461. .jessibuca-btn {
  462. width: 20px;
  463. color: rgb(255, 255, 255);
  464. line-height: 27px;
  465. margin: 0px 10px;
  466. padding: 0px 2px;
  467. cursor: pointer;
  468. text-align: center;
  469. font-size: 0.8rem !important;
  470. }
  471. .buttons-box-right {
  472. position: absolute;
  473. right: 0;
  474. }
  475. </style>