@havue/solutions 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +0 -0
  2. package/bc-connect/README.md +3 -0
  3. package/bc-connect/dist/bc-connect.mjs +342 -0
  4. package/bc-connect/dist/bc-connect.umd.js +346 -0
  5. package/bc-connect/dist/types/src/index.d.ts +1 -0
  6. package/bc-connect/dist/types/src/manager.d.ts +135 -0
  7. package/bc-connect/package.json +38 -0
  8. package/bc-connect/src/index.ts +1 -0
  9. package/bc-connect/src/manager.ts +388 -0
  10. package/bc-connect/vite.config.ts +4 -0
  11. package/dist/solutions.full.js +8504 -0
  12. package/dist/solutions.full.min.js +18 -0
  13. package/dist/solutions.full.min.js.map +1 -0
  14. package/dist/solutions.mjs +2 -0
  15. package/dist/solutions.umd.js +18 -0
  16. package/dist/types/bc-connect/src/index.d.ts +1 -0
  17. package/dist/types/bc-connect/src/manager.d.ts +135 -0
  18. package/dist/types/src/index.d.ts +2 -0
  19. package/dist/types/ws-video-manager/src/hooks/useVideoPlay.d.ts +73 -0
  20. package/dist/types/ws-video-manager/src/index.d.ts +5 -0
  21. package/dist/types/ws-video-manager/src/loader/index.d.ts +2 -0
  22. package/dist/types/ws-video-manager/src/loader/websocket-loader.d.ts +62 -0
  23. package/dist/types/ws-video-manager/src/manager/index.d.ts +144 -0
  24. package/dist/types/ws-video-manager/src/render/drawer.d.ts +44 -0
  25. package/dist/types/ws-video-manager/src/render/index.d.ts +105 -0
  26. package/package.json +44 -0
  27. package/src/index.ts +2 -0
  28. package/vite.config.ts +4 -0
  29. package/ws-video-manager/README.md +3 -0
  30. package/ws-video-manager/dist/types/src/hooks/useVideoPlay.d.ts +73 -0
  31. package/ws-video-manager/dist/types/src/index.d.ts +5 -0
  32. package/ws-video-manager/dist/types/src/loader/index.d.ts +2 -0
  33. package/ws-video-manager/dist/types/src/loader/websocket-loader.d.ts +62 -0
  34. package/ws-video-manager/dist/types/src/manager/index.d.ts +144 -0
  35. package/ws-video-manager/dist/types/src/render/drawer.d.ts +44 -0
  36. package/ws-video-manager/dist/types/src/render/index.d.ts +105 -0
  37. package/ws-video-manager/dist/ws-video-manager.mjs +8132 -0
  38. package/ws-video-manager/dist/ws-video-manager.umd.js +8135 -0
  39. package/ws-video-manager/node_modules/.bin/tsc +17 -0
  40. package/ws-video-manager/node_modules/.bin/tsc.CMD +12 -0
  41. package/ws-video-manager/node_modules/.bin/tsc.ps1 +41 -0
  42. package/ws-video-manager/node_modules/.bin/tsserver +17 -0
  43. package/ws-video-manager/node_modules/.bin/tsserver.CMD +12 -0
  44. package/ws-video-manager/node_modules/.bin/tsserver.ps1 +41 -0
  45. package/ws-video-manager/package.json +41 -0
  46. package/ws-video-manager/src/hooks/useVideoPlay.ts +357 -0
  47. package/ws-video-manager/src/index.ts +6 -0
  48. package/ws-video-manager/src/loader/index.ts +3 -0
  49. package/ws-video-manager/src/loader/websocket-loader.ts +278 -0
  50. package/ws-video-manager/src/manager/index.ts +429 -0
  51. package/ws-video-manager/src/render/drawer.ts +255 -0
  52. package/ws-video-manager/src/render/index.ts +475 -0
  53. package/ws-video-manager/vite.config.ts +4 -0
@@ -0,0 +1,475 @@
1
+ import MP4Box from 'mp4box'
2
+ import { EventBus } from '@havue/shared'
3
+
4
+ // #region typedefine
5
+ export type RenderConstructorOptionType = {
6
+ /** 当前播放currentTime和最新视频时长最多相差 秒数,默认0.3s */
7
+ liveMaxLatency: number
8
+ /** 最多缓存ws传输的未处理的buffer数据大小, 默认200kb */
9
+ maxCacheBufByte: number
10
+ /** 最多存储的时间,用于清除在currentTime之前x秒时间节点前的buffer数据, 默认10s */
11
+ maxCache: number
12
+ }
13
+
14
+ export const WS_VIDEO_RENDER_DEFAULT_OPTIONS = Object.freeze({
15
+ liveMaxLatency: 0.3,
16
+ maxCacheBufByte: 200 * 1024,
17
+ maxCache: 10
18
+ })
19
+
20
+ export enum AudioState {
21
+ NOTMUTED = 'notmuted',
22
+ MUTED = 'muted'
23
+ }
24
+
25
+ export enum VideoState {
26
+ PLAY = 'play',
27
+ PAUSE = 'pause'
28
+ }
29
+
30
+ export type VideoInfo = {
31
+ width: number
32
+ height: number
33
+ }
34
+
35
+ export enum RenderEventsEnum {
36
+ AUDIO_STATE_CHANGE = 'audioStateChange',
37
+ VIDEO_STATE_CHANGE = 'videoStateChange',
38
+ VIDEO_INFO_UPDATE = 'videoInfoUpdate'
39
+ }
40
+
41
+ export type RenderEvents = {
42
+ [RenderEventsEnum.AUDIO_STATE_CHANGE]: (s: AudioState) => void
43
+ [RenderEventsEnum.VIDEO_STATE_CHANGE]: (s: VideoState) => void
44
+ [RenderEventsEnum.VIDEO_INFO_UPDATE]: (info: VideoInfo) => void
45
+ }
46
+
47
+ // #endregion typedefine
48
+
49
+ // 调试代码
50
+ // let id = 0
51
+ // let curPosX = 0
52
+ // let curPosY = 0
53
+
54
+ export class Render extends EventBus<RenderEvents> {
55
+ /** video元素 */
56
+ private _videoEl: HTMLVideoElement | undefined = undefined
57
+ /** pixi.js 实例 */
58
+ // private _pixiApp: Application | null = null
59
+ /** mp4box 实例 */
60
+ private _mp4box: MP4Box = MP4Box.createFile()
61
+ /** 接收到的socket消息 视频数据buffer数组 */
62
+ private _bufsQueue: ArrayBuffer[] = []
63
+ /** MediaSource 实例 */
64
+ private _mediaSource: MediaSource | undefined
65
+ /** SourceBuffer 实例 */
66
+ private _sourceBuffer: SourceBuffer | undefined
67
+ /** 用于MediaSource的mimeType */
68
+ private _mimeType: string = ''
69
+ /** 是否暂停播放 */
70
+ private _paused: boolean = false
71
+ private _options: RenderConstructorOptionType
72
+
73
+ private _cacheAnimationID: number | undefined = undefined
74
+
75
+ // 调试代码
76
+ // private divID = ''
77
+
78
+ constructor(options: Partial<RenderConstructorOptionType> = {}) {
79
+ super()
80
+ this._options = options
81
+ ? Object.assign({}, WS_VIDEO_RENDER_DEFAULT_OPTIONS, options)
82
+ : WS_VIDEO_RENDER_DEFAULT_OPTIONS
83
+ this._mp4box.onReady = this._onMp4boxReady.bind(this)
84
+ this._setupVideo()
85
+ }
86
+
87
+ get muted(): boolean {
88
+ return this._videoEl?.muted || false
89
+ }
90
+
91
+ set muted(val: boolean) {
92
+ if (this._videoEl) {
93
+ this._videoEl.muted = val
94
+ }
95
+ }
96
+
97
+ set paused(paused: boolean) {
98
+ this._paused = paused
99
+ if (paused) {
100
+ this._videoEl?.pause()
101
+ } else {
102
+ this._videoEl?.play()
103
+ }
104
+ }
105
+
106
+ get paused(): boolean {
107
+ return this._paused
108
+ }
109
+
110
+ get videoEl(): HTMLVideoElement | undefined {
111
+ return this._videoEl
112
+ }
113
+
114
+ /** 更新实例配置 */
115
+ public updateOptions(option: Partial<RenderConstructorOptionType> = {}) {
116
+ Object.assign(this._options, {
117
+ ...option
118
+ })
119
+ }
120
+
121
+ /**
122
+ * 添加视频流buffer数据
123
+ * @param buf
124
+ */
125
+ public appendMediaBuffer(bufs: Array<ArrayBuffer & { fileStart: number }>) {
126
+ if (this._paused) {
127
+ return
128
+ }
129
+ if (!this._sourceBuffer) {
130
+ const buf = bufs[0]
131
+ buf.fileStart = 0
132
+ this._mp4box.appendBuffer(buf)
133
+ }
134
+ this._bufsQueue.push(...bufs)
135
+
136
+ if (this._sourceBuffer && !this._videoEl?.paused && this._bufsQueue.length > 2) {
137
+ const len = this._bufsQueue.length
138
+ const maxTotal = this._options.maxCacheBufByte
139
+ let lastIndex = len - 1
140
+ let total = 0
141
+ for (let i = len - 1; i > 0; i--) {
142
+ total += this._bufsQueue[i].byteLength
143
+ lastIndex = i
144
+ if (total >= maxTotal) {
145
+ this._bufsQueue = this._bufsQueue.slice(lastIndex)
146
+ break
147
+ }
148
+ }
149
+ }
150
+ this._cacheAnimationID && cancelAnimationFrame(this._cacheAnimationID)
151
+ this._cacheAnimationID = undefined
152
+ this._cache()
153
+ }
154
+
155
+ /**
156
+ * mp4box解析完成
157
+ * @param info mp4box解析信息
158
+ */
159
+ private _onMp4boxReady(info: any) {
160
+ console.log('onMp4boxReady', info)
161
+ this._mp4box.flush()
162
+ if (!info.isFragmented) {
163
+ console.error('not fragmented mp4')
164
+ return
165
+ }
166
+ this._mimeType = info.mime
167
+
168
+ try {
169
+ const { width, height } = info.videoTracks[0].video
170
+ this.emit(RenderEventsEnum.VIDEO_INFO_UPDATE, {
171
+ width,
172
+ height
173
+ })
174
+ } catch (error) {
175
+ console.error(error)
176
+ }
177
+ // this._setupVideo()
178
+ // this.setupPixi()
179
+ this._setupMSE()
180
+ }
181
+
182
+ /**
183
+ * 初始化视频元素
184
+ */
185
+ private _setupVideo() {
186
+ this._videoEl = document.createElement('video')
187
+ this._videoEl.preload = 'auto'
188
+ this._videoEl.controls = false
189
+ this._videoEl.muted = true
190
+ this._videoEl.autoplay = true
191
+ this._videoEl.loop = false
192
+ this._videoEl.crossOrigin = 'anonymous'
193
+ this._videoEl.playsInline = true
194
+ this._videoEl['webkit-playsinline'] = true
195
+
196
+ this._videoEl.addEventListener('canplay', () => {
197
+ if (!this._paused) {
198
+ this._videoEl?.play()
199
+ }
200
+ })
201
+
202
+ this._videoEl.addEventListener('play', () => {
203
+ this.emit(RenderEventsEnum.VIDEO_STATE_CHANGE, VideoState.PLAY)
204
+ })
205
+
206
+ this._videoEl.addEventListener('pause', () => {
207
+ this.emit(RenderEventsEnum.VIDEO_STATE_CHANGE, VideoState.PAUSE)
208
+ })
209
+
210
+ this._videoEl.addEventListener('volumechange', () => {
211
+ this.emit(RenderEventsEnum.AUDIO_STATE_CHANGE, this._videoEl?.muted ? AudioState.MUTED : AudioState.NOTMUTED)
212
+ })
213
+
214
+ this._videoEl.addEventListener('error', (error) => {
215
+ console.error('video error', error)
216
+ // setTimeout(() => {
217
+ // this._mediaSource && this._setupMSE()
218
+ // }, 500)
219
+ })
220
+
221
+ /**
222
+ * bug: 因预监画面播放一段时间后,
223
+ * 虽然视频时间在走,但video的画面会暂停,
224
+ * 发现将视频元素添加到视口中,画面就不会暂停,
225
+ * 可能与浏览器的资源优化策略有关,先将video标签添加到视口中,
226
+ * 后面有时间寻找一下是否有其他解决方案
227
+ */
228
+ // this._videoEl.controls = true
229
+ document.body.appendChild(this._videoEl)
230
+ this._videoEl.style.position = 'fixed'
231
+ this._videoEl.style.left = `0px`
232
+ this._videoEl.style.top = `0px`
233
+ this._videoEl.style.zIndex = '-10000'
234
+ this._videoEl.style.width = '1px'
235
+ this._videoEl.style.height = '1px'
236
+ this._videoEl.style.opacity = '0.01'
237
+ this._videoEl.style.pointerEvents = 'none'
238
+ this._videoEl.style.touchAction = 'none'
239
+
240
+ // 调试代码
241
+ // this._videoEl.style.left = `${curPosX}px`
242
+ // this._videoEl.style.top = `${curPosY}px`
243
+ // this._videoEl.style.zIndex = '99999'
244
+ // this._videoEl.style.width = '200px'
245
+ // this._videoEl.style.height = '100px'
246
+ // this._videoEl.controls = true
247
+ // this._videoEl.style.opacity = '1'
248
+ // const div = document.createElement('div')
249
+ // div.id = `divID${id}`
250
+ // this.divID = `divID${id}`
251
+ // id = id + 1
252
+
253
+ // div.style.position = 'fixed'
254
+ // div.style.left = `${curPosX}px`
255
+ // div.style.top = `${curPosY}px`
256
+ // curPosY += 100
257
+ // if (curPosY >= 400) {
258
+ // curPosY = 0
259
+ // curPosX += 200
260
+ // }
261
+ // div.style.zIndex = '99999'
262
+ // document.body.appendChild(div)
263
+ }
264
+
265
+ /**
266
+ * 是否支持Media Source Extention
267
+ * @returns boolean
268
+ */
269
+ public isSupportMSE() {
270
+ return 'MediaSource' in window
271
+ }
272
+
273
+ /**
274
+ * 初始化MSE
275
+ * @returns
276
+ */
277
+ private _setupMSE(): void {
278
+ if (!this.isSupportMSE()) {
279
+ console.error('your borwser do not support MediaSource')
280
+ return
281
+ }
282
+ if (!MediaSource.isTypeSupported(this._mimeType)) {
283
+ console.error('Unsupported MIME type or codec: ', this._mimeType)
284
+ return
285
+ }
286
+ if (!this._videoEl) {
287
+ return
288
+ }
289
+ this.destroyMediaSource()
290
+ this._mediaSource = new MediaSource()
291
+ URL.revokeObjectURL(this._videoEl.src)
292
+ this._videoEl.src = URL.createObjectURL(this._mediaSource)
293
+
294
+ this._mediaSource.addEventListener('sourceopen', () => {
295
+ const sourceBuffer = (this._sourceBuffer = this._mediaSource!.addSourceBuffer(this._mimeType))
296
+ sourceBuffer.mode = 'sequence'
297
+ sourceBuffer.onupdateend = () => {
298
+ if (
299
+ !this._videoEl ||
300
+ !sourceBuffer ||
301
+ this._mediaSource?.readyState !== 'open' ||
302
+ ![...this._mediaSource.sourceBuffers].includes(sourceBuffer)
303
+ ) {
304
+ return
305
+ }
306
+ const currentTime = this._videoEl.currentTime
307
+ // 调试代码
308
+ // const div = document.getElementById(this.divID)
309
+ // let innerHTML = `len:${sourceBuffer.buffered.length}`
310
+
311
+ if (sourceBuffer.buffered.length > 0) {
312
+ let bufferedLen = sourceBuffer.buffered.length
313
+ /** 是否需要删除sourceBuffer中的buffer段 */
314
+ const needDelBuf = bufferedLen > 1
315
+ /**
316
+ * sourceBuffer中有多个buffered,时间不连续
317
+ * 导致视频播放到其中一个buffer最后就暂停了
318
+ * 如果出现多个buffered,删除之前有的buffer
319
+ * 使用最新的视频buffer进行播放
320
+ */
321
+ if (needDelBuf && currentTime) {
322
+ const lastIndex = bufferedLen - 1
323
+ if (currentTime < sourceBuffer.buffered.start(lastIndex)) {
324
+ this._videoEl.currentTime = sourceBuffer.buffered.start(lastIndex)
325
+ }
326
+
327
+ const delBufEnd = sourceBuffer.buffered.end(lastIndex - 1)
328
+ if (!this._sourceBuffer!.updating && currentTime > delBufEnd) {
329
+ this._sourceBuffer?.remove(0, delBufEnd)
330
+ }
331
+ }
332
+
333
+ // 调试代码
334
+ // innerHTML += ` - start:${sourceBuffer.buffered.start(
335
+ // sourceBuffer.buffered.length - 1
336
+ // )} - end:${sourceBuffer.buffered.end(sourceBuffer.buffered.length - 1)} <br/>`
337
+ // innerHTML += ` - currentTime: ${currentTime}`
338
+ // div && (div.innerHTML = innerHTML)
339
+
340
+ bufferedLen = sourceBuffer.buffered.length
341
+ const start = sourceBuffer.buffered.start(bufferedLen - 1)
342
+ const end = sourceBuffer.buffered.end(bufferedLen - 1)
343
+
344
+ // 设置开始时间
345
+ if (!currentTime && start) {
346
+ this._videoEl.currentTime = start
347
+ }
348
+
349
+ // 限制最低延迟时间
350
+ if (this._options.liveMaxLatency) {
351
+ const offsetMaxLatency = this._options.liveMaxLatency
352
+
353
+ if (end - currentTime > offsetMaxLatency) {
354
+ this._videoEl.currentTime = end - 0.1
355
+ }
356
+ }
357
+ // 移除当前时间之前的buffer
358
+ if (!this._sourceBuffer!.updating && currentTime - start > this._options.maxCache) {
359
+ this._sourceBuffer?.remove(0, currentTime - this._options.maxCache / 2)
360
+ }
361
+ }
362
+ }
363
+ })
364
+ }
365
+
366
+ /**
367
+ * 将_bufsQueue中的数据添加到SourceBuffer中
368
+ * @returns
369
+ */
370
+ private _cache() {
371
+ if (!this._videoEl) {
372
+ return
373
+ }
374
+ if (
375
+ !this._mediaSource ||
376
+ !this._sourceBuffer ||
377
+ this._sourceBuffer.updating ||
378
+ !this._bufsQueue.length ||
379
+ this._mediaSource.readyState !== 'open'
380
+ ) {
381
+ this._cacheAnimationID === undefined && (this._cacheAnimationID = requestAnimationFrame(() => this._cache()))
382
+ return
383
+ }
384
+ if (this._videoEl.error) {
385
+ this._setupMSE()
386
+ return (
387
+ this._cacheAnimationID === undefined && (this._cacheAnimationID = requestAnimationFrame(() => this._cache()))
388
+ )
389
+ }
390
+ this._cacheAnimationID = undefined
391
+ let frame: Uint8Array
392
+ if (this._bufsQueue.length > 1) {
393
+ const freeBuffer = this._bufsQueue.splice(0, this._bufsQueue.length)
394
+ const length = freeBuffer.map((e) => e.byteLength).reduce((a, b) => a + b, 0)
395
+ const buffer = new Uint8Array(length)
396
+ let offset = 0
397
+ for (const data of freeBuffer) {
398
+ const frame = new Uint8Array(data)
399
+ buffer.set(frame, offset)
400
+ offset += data.byteLength
401
+ }
402
+ frame = buffer
403
+ } else {
404
+ frame = new Uint8Array(this._bufsQueue.shift() || [])
405
+ }
406
+
407
+ if (frame) {
408
+ this._sourceBuffer.appendBuffer(frame)
409
+ }
410
+ }
411
+
412
+ /**
413
+ * 刷新播放时间为最新
414
+ */
415
+ public refresh() {
416
+ if (this._videoEl && this._videoEl.buffered.length) {
417
+ const end = this._videoEl.buffered.end(this._videoEl.buffered.length - 1)
418
+ this._videoEl.currentTime = end
419
+ }
420
+ }
421
+
422
+ /** 重置解析的视频mime type */
423
+ public resetMimeType() {
424
+ this.destroyMediaSource()
425
+ if (this._videoEl) {
426
+ this._videoEl.src = ''
427
+ }
428
+ this._mp4box = MP4Box.createFile()
429
+ this._mp4box.onReady = this._onMp4boxReady.bind(this)
430
+ this._bufsQueue.length = 0
431
+ }
432
+
433
+ private destroyMediaSource() {
434
+ if (this._mediaSource) {
435
+ if (this._videoEl) {
436
+ URL.revokeObjectURL(this._videoEl?.src || '')
437
+ this._videoEl.src = ''
438
+ }
439
+ if (this._mediaSource.readyState === 'open') {
440
+ if (this._sourceBuffer) {
441
+ this._sourceBuffer.abort()
442
+ this._mediaSource.removeSourceBuffer(this._sourceBuffer)
443
+ }
444
+ this._mediaSource.endOfStream()
445
+ }
446
+
447
+ this._mediaSource = undefined
448
+ this._sourceBuffer = undefined
449
+ }
450
+ }
451
+
452
+ /**
453
+ * 销毁
454
+ */
455
+ public destroy() {
456
+ this._bufsQueue = []
457
+ if (this._videoEl) {
458
+ this._videoEl.pause()
459
+ this._videoEl.currentTime = 0
460
+ if (this._videoEl.parentElement === document.body) {
461
+ document.body.removeChild(this._videoEl)
462
+ }
463
+ URL.revokeObjectURL(this._videoEl.src)
464
+ this._videoEl.src = ''
465
+ this._videoEl = undefined
466
+ }
467
+
468
+ this._mimeType = ''
469
+
470
+ this._cacheAnimationID && cancelAnimationFrame(this._cacheAnimationID)
471
+ this._cacheAnimationID = undefined
472
+
473
+ this.destroyMediaSource()
474
+ }
475
+ }
@@ -0,0 +1,4 @@
1
+ import { defineConfig } from 'vite'
2
+ import { getConfig } from '../../../internal/build/src'
3
+
4
+ export default defineConfig(({ mode }) => getConfig(mode))