useVideoManager.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. /**
  2. * useVideoManager —— 单活视频管理器
  3. * ---------------------------------------------------------------------------
  4. * 移动端 / 平板的 H.264 硬件解码器实例有限(1080p 约 4~8 路,4K 只有 1~2 路)。
  5. * 同一时刻只允许一个 <video> 持有 src,释放其他所有(pause + removeAttribute + load)。
  6. *
  7. * 职责:只管"释放"(归还解码器)和"注册/注销",不管 src 的挂载。
  8. * src 的设置由调用方(VideoPlayer / useMSE)自己决定。
  9. * ---------------------------------------------------------------------------
  10. */
  11. interface VMMeta {
  12. duration: number
  13. currentTime: number
  14. played: boolean
  15. }
  16. function getMeta(el: HTMLVideoElement): VMMeta {
  17. if (!(el as any)._vmMeta) {
  18. (el as any)._vmMeta = { duration: 0, currentTime: 0, played: false }
  19. }
  20. return (el as any)._vmMeta as VMMeta
  21. }
  22. // 单例
  23. class VideoManager {
  24. current: HTMLVideoElement | null = null
  25. private _els: HTMLVideoElement[] = []
  26. /** 注册一个 video 元素 */
  27. register(el: HTMLVideoElement) {
  28. if (!el || (el as any)._vmRegistered) return
  29. ;(el as any)._vmRegistered = true
  30. const m = getMeta(el)
  31. el.addEventListener('loadedmetadata', () => {
  32. if (isFinite(el.duration) && el.duration > 0) m.duration = el.duration
  33. })
  34. el.addEventListener('durationchange', () => {
  35. if (isFinite(el.duration) && el.duration > 0) m.duration = el.duration
  36. })
  37. el.addEventListener('timeupdate', () => {
  38. if (el.currentTime > 0) m.currentTime = el.currentTime
  39. })
  40. el.addEventListener('play', () => {
  41. m.played = true
  42. })
  43. this._els.push(el)
  44. }
  45. /**
  46. * 释放除 keepEl 之外的所有 video:pause → removeAttribute('src') → load
  47. * 必须用 load(),只删 src 不调 load 解码管线不会释放
  48. */
  49. releaseAllOthers(keepEl: HTMLVideoElement) {
  50. this._els.forEach(other => {
  51. if (other !== keepEl && other.src) {
  52. this.release(other)
  53. }
  54. })
  55. this.current = keepEl
  56. }
  57. /** 释放单个元素:归还解码器 + 释放内存 */
  58. release(el: HTMLVideoElement) {
  59. if (!el) return
  60. const m = getMeta(el)
  61. if (el.currentTime > 0) m.currentTime = el.currentTime
  62. try {
  63. el.pause()
  64. }
  65. catch {}
  66. el.removeAttribute('src')
  67. try {
  68. el.load()
  69. }
  70. catch {}
  71. // 关键:手动派发 pause 事件
  72. // 某些浏览器(尤其 iOS Safari)在 pause → remove src → load 连续调用时,
  73. // 原生 @pause 事件可能被"吞掉",导致外部状态(Vue 的 paused ref)没同步。
  74. // 这里用 dispatchEvent 兜底,确保所有监听方都能收到。
  75. try {
  76. el.dispatchEvent(new Event('pause'))
  77. }
  78. catch {}
  79. if (this.current === el) this.current = null
  80. }
  81. /** 释放全部 */
  82. releaseAll() {
  83. this._els.forEach(el => {
  84. if (el.src) this.release(el)
  85. })
  86. this.current = null
  87. }
  88. /** 取缓存的时长/进度,释放后依然可用 */
  89. getMeta(el: HTMLVideoElement) {
  90. const m = getMeta(el)
  91. return {
  92. duration: m.duration || (isFinite(el.duration) ? el.duration : 0),
  93. currentTime: m.currentTime || el.currentTime || 0,
  94. played: m.played,
  95. }
  96. }
  97. unregister(el: HTMLVideoElement) {
  98. this.release(el)
  99. this._els = this._els.filter(e => e !== el)
  100. if (this.current === el) this.current = null
  101. }
  102. }
  103. const _manager = new VideoManager()
  104. export function useVideoManager() {
  105. return _manager
  106. }
  107. export { VideoManager }
  108. export default _manager