@banou/media-player 0.6.2 → 0.7.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 (119) hide show
  1. package/README.md +148 -3
  2. package/dist/engine/index.d.ts +10 -0
  3. package/dist/engine/index.js +2 -0
  4. package/dist/engine/picture-in-picture.d.ts +27 -0
  5. package/dist/engine/playback.d.ts +45 -0
  6. package/dist/engine/source-buffer.d.ts +12 -0
  7. package/dist/engine/subtitles.d.ts +36 -0
  8. package/dist/engine/thumbnails.d.ts +22 -0
  9. package/dist/engine-Dmc7JSd9.js +470 -0
  10. package/dist/index.d.ts +12 -0
  11. package/dist/index.js +1566 -0
  12. package/dist/react/components/chrome.d.ts +9 -0
  13. package/dist/react/components/control-bar.d.ts +2 -0
  14. package/dist/react/components/overlay.d.ts +4 -0
  15. package/dist/react/components/playback-slider.d.ts +2 -0
  16. package/dist/react/components/progress-bar.d.ts +2 -0
  17. package/dist/react/components/settings.d.ts +2 -0
  18. package/dist/react/components/sound.d.ts +7 -0
  19. package/{build → dist/react}/components/tooltip-display.d.ts +8 -8
  20. package/dist/react/components/volume-slider.d.ts +6 -0
  21. package/dist/react/hooks/use-drag-value.d.ts +23 -0
  22. package/dist/react/hooks/use-picture-in-picture.d.ts +2 -0
  23. package/dist/react/hooks/use-playback.d.ts +6 -0
  24. package/dist/react/hooks/use-thumbnails.d.ts +11 -0
  25. package/dist/react/player.d.ts +25 -0
  26. package/dist/react/source-feature.d.ts +84 -0
  27. package/dist/react/video-player.d.ts +43 -0
  28. package/dist/utils/source.d.ts +30 -0
  29. package/dist/utils/track-label.d.ts +19 -0
  30. package/dist/utils/volume-utils.d.ts +6 -0
  31. package/package.json +49 -37
  32. package/src/lib/engine/index.ts +14 -0
  33. package/src/lib/engine/picture-in-picture.ts +250 -0
  34. package/src/lib/engine/playback.ts +352 -0
  35. package/src/lib/engine/source-buffer.ts +61 -0
  36. package/src/lib/engine/subtitles.ts +210 -0
  37. package/src/lib/engine/thumbnails.ts +137 -0
  38. package/src/lib/globals.d.ts +8 -0
  39. package/src/lib/index.tsx +36 -0
  40. package/src/lib/react/components/chrome.tsx +126 -0
  41. package/src/{components → lib/react/components}/control-bar.tsx +74 -96
  42. package/src/lib/react/components/overlay.tsx +121 -0
  43. package/src/{components → lib/react/components}/playback-slider.tsx +30 -52
  44. package/src/{components → lib/react/components}/progress-bar.tsx +128 -41
  45. package/src/lib/react/components/settings.tsx +391 -0
  46. package/src/lib/react/components/sound.tsx +133 -0
  47. package/src/{components → lib/react/components}/tooltip-display.tsx +11 -8
  48. package/src/{components → lib/react/components}/volume-slider.tsx +32 -46
  49. package/src/lib/react/hooks/use-drag-value.ts +73 -0
  50. package/src/lib/react/hooks/use-picture-in-picture.ts +36 -0
  51. package/src/lib/react/hooks/use-playback.ts +134 -0
  52. package/src/lib/react/hooks/use-thumbnails.ts +77 -0
  53. package/src/lib/react/player.ts +20 -0
  54. package/src/lib/react/source-feature.ts +90 -0
  55. package/src/lib/react/video-player.tsx +119 -0
  56. package/src/lib/utils/source.ts +60 -0
  57. package/src/lib/utils/track-label.ts +36 -0
  58. package/src/lib/utils/volume-utils.ts +13 -0
  59. package/build/components/chrome.d.ts +0 -7
  60. package/build/components/control-bar.d.ts +0 -6
  61. package/build/components/overlay.d.ts +0 -4
  62. package/build/components/playback-slider.d.ts +0 -2
  63. package/build/components/progress-bar.d.ts +0 -1
  64. package/build/components/settings.d.ts +0 -2
  65. package/build/components/sound.d.ts +0 -4
  66. package/build/components/volume-slider.d.ts +0 -6
  67. package/build/index.d.ts +0 -26
  68. package/build/index.js +0 -3416
  69. package/build/main.d.ts +0 -1
  70. package/build/state-machines/data-source.d.ts +0 -27
  71. package/build/state-machines/index.d.ts +0 -34038
  72. package/build/state-machines/media-properties.d.ts +0 -45
  73. package/build/state-machines/media-source.d.ts +0 -34
  74. package/build/state-machines/media.d.ts +0 -8558
  75. package/build/state-machines/subtitles.d.ts +0 -56
  76. package/build/state-machines/thumbnails.d.ts +0 -24
  77. package/build/state-machines/utils.d.ts +0 -26
  78. package/build/utils/actor-utils.d.ts +0 -2
  79. package/build/utils/context.d.ts +0 -14
  80. package/build/utils/index.d.ts +0 -4
  81. package/build/utils/languages.d.ts +0 -260
  82. package/build/utils/mp4box.d.ts +0 -61
  83. package/build/utils/use-local-storage.d.ts +0 -3
  84. package/build/utils/use-scrub.d.ts +0 -11
  85. package/build/utils/volume-utils.d.ts +0 -17
  86. package/build/utils/window-height.d.ts +0 -2
  87. package/src/components/chrome.tsx +0 -95
  88. package/src/components/overlay.tsx +0 -91
  89. package/src/components/settings.tsx +0 -322
  90. package/src/components/sound.tsx +0 -101
  91. package/src/index.tsx +0 -195
  92. package/src/main.tsx +0 -169
  93. package/src/state-machines/data-source.ts +0 -84
  94. package/src/state-machines/index.ts +0 -5
  95. package/src/state-machines/media-properties.ts +0 -82
  96. package/src/state-machines/media-source.ts +0 -129
  97. package/src/state-machines/media.ts +0 -255
  98. package/src/state-machines/subtitles.ts +0 -285
  99. package/src/state-machines/thumbnails.ts +0 -123
  100. package/src/state-machines/utils.ts +0 -94
  101. package/src/utils/actor-utils.ts +0 -19
  102. package/src/utils/context.ts +0 -23
  103. package/src/utils/index.ts +0 -229
  104. package/src/utils/languages.ts +0 -262
  105. package/src/utils/mp4box.ts +0 -74
  106. package/src/utils/use-local-storage.ts +0 -30
  107. package/src/utils/use-scrub.ts +0 -40
  108. package/src/utils/volume-utils.ts +0 -39
  109. package/src/utils/window-height.ts +0 -19
  110. package/src/vite-env.d.ts +0 -1
  111. package/tsconfig.json +0 -28
  112. package/tsconfig.node.json +0 -9
  113. /package/{build → dist}/utils/colors.d.ts +0 -0
  114. /package/{build → dist}/utils/fonts.d.ts +0 -0
  115. /package/{build → dist}/utils/time.d.ts +0 -0
  116. /package/src/{assets → lib/assets}/picture-in-picture.svg +0 -0
  117. /package/src/{utils → lib/utils}/colors.ts +0 -0
  118. /package/src/{utils → lib/utils}/fonts.ts +0 -0
  119. /package/src/{utils → lib/utils}/time.ts +0 -0
@@ -1,255 +0,0 @@
1
- import type { Attachment, Index, SubtitleFragment } from 'libav-wasm/build/worker'
2
-
3
- import { makeRemuxer } from 'libav-wasm'
4
- import { assign, setup, sendTo, enqueueActions } from 'xstate'
5
-
6
- import mediaPropertiesLogic from './media-properties'
7
- import mediaSourceLogic, { MediaSourceOptions } from './media-source'
8
- import dataSourceLogic from './data-source'
9
- import subtitlesLogic, { SubtitleStream } from './subtitles'
10
- import thumbnailsLogic, { Thumbnail } from './thumbnails'
11
- import { JassubOptions } from 'jassub'
12
- import { DownloadedRange } from '../utils/context'
13
-
14
- export default setup({
15
- types: {} as {
16
- context: {
17
- mediaSourceOptions: MediaSourceOptions | undefined
18
- subtitlesRendererOptions: Omit<JassubOptions, 'video' | 'canvas'> | undefined
19
- remuxerOptions: Parameters<typeof makeRemuxer>[0] | undefined
20
- videoElement: HTMLVideoElement | undefined
21
- canvasElement: HTMLCanvasElement | undefined
22
- media: {
23
- duration?: number
24
- paused: boolean
25
- currentTime: number
26
- volume: number
27
- muted: boolean
28
- playbackRate: number
29
- },
30
- attachments: Attachment[]
31
- subtitleFragments: SubtitleFragment[]
32
- subtitleStreams: SubtitleStream[]
33
- selectedSubtitleStreamIndex: number | undefined
34
- indexes: Index[]
35
- thumbnails: Thumbnail[]
36
- isReady: boolean
37
- },
38
- events:
39
- | { type: 'MEDIA_SOURCE_OPTIONS', mediaSourceOptions: MediaSourceOptions }
40
- | { type: 'SUBTITLES_RENDERER_OPTIONS', subtitlesRendererOptions: Omit<JassubOptions, 'video' | 'canvas'> }
41
- | { type: 'REMUXER_OPTIONS', remuxerOptions: Parameters<typeof makeRemuxer>[0] }
42
- | { type: 'SET_VIDEO_ELEMENT', videoElement: HTMLVideoElement }
43
- | { type: 'SET_CANVAS_ELEMENT', canvasElement: HTMLCanvasElement }
44
- | { type: 'IS_READY' }
45
- | { type: 'PLAY' }
46
- | { type: 'PAUSE' }
47
- | { type: 'SET_TIME', value: number }
48
- | { type: 'PLAYING' }
49
- | { type: 'PAUSED' }
50
- | { type: 'ENDED' }
51
- | { type: 'TIME_UPDATE', currentTime: number }
52
- | { type: 'VOLUME_UPDATE', muted: boolean, volume: number }
53
- | { type: 'PLAYBACK_RATE_UPDATE', playbackRate: number }
54
- | { type: 'DURATION_UPDATE', duration: number }
55
- | { type: 'SET_VOLUME', muted: boolean, volume: number }
56
- | { type: 'SET_PLAYBACK_RATE', playbackRate: number }
57
- | { type: 'SEEKING', currentTime: number }
58
- | { type: 'NEED_DATA' }
59
- | { type: 'METADATA', mimeType: string, duration: number, data: ArrayBuffer }
60
- | { type: 'DATA', data: ArrayBuffer }
61
- | { type: 'TIMESTAMP_OFFSET', timestampOffset: number }
62
- | { type: 'NEW_ATTACHMENTS', attachments: Attachment[] }
63
- | { type: 'NEW_SUBTITLE_FRAGMENTS', subtitles: SubtitleFragment[] }
64
- | { type: 'SUBTITLE_STREAMS_UPDATED', subtitlesStreams: SubtitleStream[] }
65
- | { type: 'SELECT_SUBTITLE_STREAM', streamIndex: number | undefined }
66
- | { type: 'NEW_THUMBNAIL', thumbnail: Thumbnail }
67
- | { type: 'DOWNLOADED_RANGES_UPDATED', downloadedRanges: DownloadedRange[] }
68
- | { type: 'DESTROY' }
69
- },
70
- actions: {
71
- isReady: enqueueActions(({ context, enqueue }) => {
72
- if (context.videoElement && context.canvasElement && context.remuxerOptions && context.subtitlesRendererOptions && context.mediaSourceOptions) {
73
- enqueue.raise({ type: 'IS_READY' })
74
- }
75
- })
76
- },
77
- actors: {
78
- mediaLogic: mediaPropertiesLogic,
79
- mediaSourceLogic: mediaSourceLogic,
80
- dataSourceLogic: dataSourceLogic,
81
- thumbnailsLogic: thumbnailsLogic,
82
- subtitlesLogic: subtitlesLogic,
83
- },
84
- }).createMachine({
85
- context: {
86
- mediaSourceOptions: undefined,
87
- subtitlesRendererOptions: undefined,
88
- remuxerOptions: undefined,
89
- videoElement: undefined,
90
- canvasElement: undefined,
91
- media: {
92
- paused: true,
93
- currentTime: 0,
94
- volume: 1,
95
- muted: false,
96
- playbackRate: 1,
97
- duration: undefined
98
- },
99
- attachments: [],
100
- subtitleFragments: [],
101
- subtitleStreams: [],
102
- selectedSubtitleStreamIndex: undefined,
103
- indexes: [],
104
- thumbnails: [],
105
- isReady: false
106
- },
107
- initial: 'WAITING',
108
- on: {
109
- 'SET_VIDEO_ELEMENT': {
110
- actions: [
111
- assign({
112
- videoElement: ({ event }) => event.videoElement,
113
- }),
114
- { type: 'isReady' }
115
- ]
116
- },
117
- 'SET_CANVAS_ELEMENT': {
118
- actions: [
119
- assign({
120
- canvasElement: ({ event }) => event.canvasElement,
121
- }),
122
- { type: 'isReady' }
123
- ]
124
- },
125
- 'IS_READY': {
126
- target: '.OK',
127
- actions: assign({ isReady: () => true })
128
- },
129
- },
130
- states: {
131
- WAITING: {
132
- on: {
133
- 'MEDIA_SOURCE_OPTIONS': {
134
- actions: [
135
- assign({
136
- mediaSourceOptions: ({ event }) => event.mediaSourceOptions
137
- }),
138
- { type: 'isReady' }
139
- ]
140
- },
141
- 'REMUXER_OPTIONS': {
142
- actions: [
143
- assign({
144
- remuxerOptions: ({ event }) => event.remuxerOptions
145
- }),
146
- { type: 'isReady' }
147
- ]
148
- },
149
- 'SUBTITLES_RENDERER_OPTIONS': {
150
- actions: [
151
- assign({
152
- subtitlesRendererOptions: ({ event }) => event.subtitlesRendererOptions
153
- }),
154
- { type: 'isReady' }
155
- ]
156
- },
157
- }
158
- },
159
- OK: {
160
- type: 'parallel',
161
- invoke: [
162
- {
163
- id: 'media',
164
- src: 'mediaLogic',
165
- input: ({ context }) => ({ videoElement: context.videoElement!, remuxerOptions: context.remuxerOptions! }),
166
- },
167
- {
168
- id: 'mediaSource',
169
- src: 'mediaSourceLogic',
170
- input: ({ context }) => ({ videoElement: context.videoElement!, remuxerOptions: context.remuxerOptions! }),
171
- },
172
- {
173
- id: 'dataSource',
174
- src: 'dataSourceLogic',
175
- input: ({ context }) => ({ remuxerOptions: context.remuxerOptions! }),
176
- },
177
- {
178
- id: 'thumbnails',
179
- src: 'thumbnailsLogic',
180
- input: ({ context }) => ({ remuxerOptions: context.remuxerOptions! }),
181
- },
182
- {
183
- id: 'subtitles',
184
- src: 'subtitlesLogic',
185
- input: ({ context }) => ({
186
- publicPath: context.remuxerOptions!.publicPath,
187
- videoElement: context.videoElement!,
188
- canvasElement: context.canvasElement!,
189
- subtitlesRendererOptions: context.subtitlesRendererOptions!
190
- }),
191
- }
192
- ],
193
- on: {
194
- 'PLAY': { actions: sendTo('media', ({ event }) => event) },
195
- 'PAUSE': { actions: sendTo('media', ({ event }) => event) },
196
- 'SET_TIME': { actions: sendTo('media', ({ event }) => event) },
197
- 'SET_PLAYBACK_RATE': { actions: sendTo('media', ({ event }) => event) },
198
- 'PLAYING': { actions: assign({ media: ({ context }) => ({ ...context.media, paused: false }) }) },
199
- 'PAUSED': { actions: assign({ media: ({ context }) => ({ ...context.media, paused: true }) }) },
200
- 'ENDED': { actions: assign({ media: ({ context }) => ({ ...context.media, paused: true }) }) },
201
- 'TIME_UPDATE': { actions: [
202
- assign({ media: ({ context, event }) => ({ ...context.media, currentTime: event.currentTime }) }),
203
- sendTo('subtitles', ({ event }) => event)
204
- ] },
205
- 'VOLUME_UPDATE': { actions: assign({ media: ({ context, event }) => ({ ...context.media, muted: event.muted, volume: event.volume }) }) },
206
- 'SET_VOLUME': { actions: sendTo('media', ({ event }) => event) },
207
- 'PLAYBACK_RATE_UPDATE': { actions: assign({ media: ({ context, event }) => ({ ...context.media, playbackRate: event.playbackRate }) }) },
208
- 'DURATION_UPDATE': { actions: assign({ media: ({ context, event }) => ({ ...context.media, duration: event.duration }) }) },
209
- 'NEED_DATA': { actions: sendTo('dataSource', ({ event }) => event) },
210
- 'METADATA': { actions: sendTo('mediaSource', ({ event }) => event) },
211
- 'SEEKING': { actions: sendTo('dataSource', ({ event }) => event) },
212
- 'DATA': { actions: sendTo('mediaSource', ({ event }) => event) },
213
- 'TIMESTAMP_OFFSET': { actions: sendTo('mediaSource', ({ event }) => event) },
214
- 'NEW_THUMBNAIL': { actions: [assign({ thumbnails: ({ context, event }) => [...context.thumbnails, event.thumbnail] })] },
215
- 'DOWNLOADED_RANGES_UPDATED': { actions: sendTo('thumbnails', ({ event }) => event) },
216
- 'INDEXES': {
217
- actions: [
218
- assign({ indexes: ({ event }) => event.indexes })
219
- ]
220
- },
221
- 'NEW_ATTACHMENTS': {
222
- actions: [
223
- assign({ attachments: ({ context, event }) => [...context.attachments, ...event.attachments] }),
224
- sendTo('subtitles', ({ event }) => event)
225
- ]
226
- },
227
- 'NEW_SUBTITLE_FRAGMENTS': {
228
- actions: [
229
- assign({ subtitleFragments: ({ context, event }) => [...context.subtitleFragments, ...event.subtitles] }),
230
- sendTo('subtitles', ({ event }) => event)
231
- // emit(({ event }) => ({ type: 'NEW_SUBTITLE_FRAGMENTS', subtitles: event.subtitles }))
232
- ]
233
- },
234
- 'SUBTITLE_STREAMS_UPDATED': {
235
- actions: [
236
- assign({ subtitleStreams: ({ event }) => event.subtitlesStreams }),
237
- sendTo('subtitles', ({ event }) => event)
238
- ]
239
- },
240
- 'SELECT_SUBTITLE_STREAM': {
241
- actions: [
242
- sendTo('subtitles', ({ event }) => event)
243
- ]
244
- },
245
- 'SELECTED_SUBTITLE_STREAM_UPDATED': {
246
- actions: [
247
- assign({ selectedSubtitleStreamIndex: ({ event }) => event.streamIndex })
248
- ]
249
- },
250
- 'DESTROY': { target: 'WAITING' }
251
- }
252
- },
253
- DESTROYED: {}
254
- }
255
- })
@@ -1,285 +0,0 @@
1
- import type { ParsedASS, ParsedASSStyles } from 'ass-compiler'
2
- import type { ASS_Event, JassubOptions } from 'jassub'
3
-
4
- import JASSUB from 'jassub'
5
- import { Attachment, SubtitleFragment } from 'libav-wasm/build/worker'
6
-
7
- import { parse, stringify } from 'ass-compiler'
8
- import { fromAsyncCallback } from './utils'
9
-
10
- type SubtitlesEvents =
11
- | { type: 'NEW_SUBTITLE_FRAGMENTS', subtitles: SubtitleFragment[] }
12
- | { type: 'NEW_ATTACHMENTS', attachments: Attachment[] }
13
- | { type: 'SELECT_SUBTITLE_STREAM', streamIndex: number }
14
- | { type: 'TIME_UPDATE', currentTime: number }
15
-
16
- type SubtitlesEmittedEvents =
17
- | { type: 'WAITING' }
18
- | { type: 'SUBTITLE_STREAMS_UPDATED', subtitlesStreams: SubtitleStream[] }
19
- | { type: 'SELECTED_SUBTITLE_STREAM_UPDATED', streamIndex: number }
20
-
21
- type SubtitlesInput = {
22
- publicPath: string
23
- videoElement: HTMLVideoElement
24
- canvasElement: HTMLCanvasElement
25
- subtitlesRendererOptions: Omit<JassubOptions, 'video' | 'canvas'>
26
- }
27
-
28
- export type SubtitlePart =
29
- | { type: 'header', streamIndex: number, content: string, eventsContent: string, dialogueFormatContent: string, parsed: ParsedASS }
30
- | { type: 'dialogue', streamIndex: number, index: number, content: string, parsed: ParsedASS, assEvent: ASS_Event }
31
-
32
- export type SubtitleStream = {
33
- header: SubtitlePart & { type: 'header' }
34
- dialogues: (SubtitlePart & { type: 'dialogue' })[]
35
- }
36
-
37
- const convertTimestamp = (ms: number) =>
38
- new Date(ms)
39
- .toISOString()
40
- .slice(11, 22)
41
-
42
- const appendParsedStyle = (jassubInstance: JASSUB, style: ParsedASSStyles['style'][number]) => {
43
- jassubInstance.createStyle({
44
- ...style,
45
- SecondaryColour: Number(style.SecondaryColour),
46
- treat_fontname_as_pattern: 0,
47
- Blur: 0,
48
- Justify: 0,
49
- FontName: style.Fontname,
50
- FontSize: Number(style.Fontsize),
51
- PrimaryColour: Number(style.PrimaryColour),
52
- BackColour: Number(style.BackColour),
53
- OutlineColour: Number(style.OutlineColour),
54
- Bold: Number(style.Bold),
55
- Italic: Number(style.Italic),
56
- Underline: Number(style.Underline),
57
- StrikeOut: Number(style.StrikeOut),
58
- ScaleX: Number(style.ScaleX),
59
- ScaleY: Number(style.ScaleY),
60
- Spacing: Number(style.Spacing),
61
- Angle: Number(style.Angle),
62
- BorderStyle: Number(style.BorderStyle),
63
- Outline: Number(style.Outline),
64
- Shadow: Number(style.Shadow),
65
- Alignment: Number(style.Alignment),
66
- MarginL: Number(style.MarginL),
67
- MarginR: Number(style.MarginR),
68
- MarginV: Number(style.MarginV),
69
- Encoding: Number(style.Encoding)
70
- })
71
- }
72
-
73
- const subtitleHeaderFragmentToSubtitleHeaderPart = (subtitleHeaderFragment: SubtitleFragment) => {
74
- const eventsContent =
75
- subtitleHeaderFragment
76
- .content
77
- .match(/\r\n\[Events\]\r\nFormat: (.*)/)
78
- ?.[0]
79
- const dialogueFormatContent =
80
- eventsContent
81
- ?.trim()
82
- .split('\n')
83
- [1]
84
- if (!eventsContent || !dialogueFormatContent) {
85
- throw new Error('dialogueFormatContent is undefined')
86
- }
87
- return {
88
- type: 'header',
89
- streamIndex: subtitleHeaderFragment.streamIndex,
90
- content: subtitleHeaderFragment.content,
91
- eventsContent,
92
- dialogueFormatContent,
93
- parsed: parse(subtitleHeaderFragment.content)
94
- } as SubtitlePart & { type: 'header' }
95
- }
96
-
97
- const subtitleDialogueFragmentToSubtitleDialoguePart = (
98
- header: SubtitlePart & { type: 'header' },
99
- subtitleFragment: SubtitleFragment & { type: 'dialogue' }
100
- ) => {
101
- const [dialogueIndexString, layer] = subtitleFragment.content.split(',')
102
- const dialogueIndex = Number(dialogueIndexString)
103
- const startTimestamp = convertTimestamp(subtitleFragment.start)
104
- const endTimestamp = convertTimestamp(subtitleFragment.end)
105
- const dialogueSourceContent = subtitleFragment.content.replace(`${dialogueIndex},${layer},`, '')
106
- const dialogueContent = `Dialogue: ${layer},${startTimestamp},${endTimestamp},${dialogueSourceContent}`
107
- const parsedEvent = parse(`${header.eventsContent}\r\n${dialogueContent}`)
108
- const dialogueEvent = parsedEvent.events.dialogue[0]
109
- if (!dialogueEvent) {
110
- throw new Error('dialogueEvent is undefined')
111
- }
112
- return {
113
- type: 'dialogue',
114
- streamIndex: subtitleFragment.streamIndex,
115
- index: dialogueIndex,
116
- content: dialogueContent,
117
- parsed: parse(`${header.eventsContent}\r\n${dialogueContent}`),
118
- assEvent: {
119
- ...dialogueEvent,
120
- Effect: dialogueEvent.Effect ?? '',
121
- Text: dialogueEvent.Text.raw,
122
- Duration: (dialogueEvent.End - dialogueEvent.Start) * 1000,
123
- Start: dialogueEvent.Start * 1000,
124
- End: dialogueEvent.End * 1000,
125
- ReadOrder: dialogueIndex,
126
- _index: dialogueIndex
127
- } as ASS_Event
128
- } as SubtitlePart & { type: 'dialogue' }
129
- }
130
-
131
- export default fromAsyncCallback<SubtitlesEvents, SubtitlesInput, SubtitlesEmittedEvents>(async ({ sendBack, receive, input, self, emit }) => {
132
- const { publicPath, subtitlesRendererOptions, videoElement: video, canvasElement: canvas } = input
133
-
134
- let attachments: [string, Uint8Array][] = []
135
- let jassubInstance: JASSUB | undefined
136
-
137
- const subtitlesStreams = new Map<number, SubtitleStream>()
138
- let appendedSubtitleParts: SubtitlePart[] = []
139
- let selectedStreamIndex: number | undefined
140
-
141
- const currentTimeInterval = setInterval(() => {
142
- if (jassubInstance) {
143
- jassubInstance.setCurrentTime(video.paused, video.currentTime, video.playbackRate)
144
- }
145
- }, 100)
146
-
147
- receive((event) => {
148
- if (event.type === 'TIME_UPDATE' && jassubInstance) {
149
- jassubInstance.setCurrentTime(video.paused, event.currentTime, video.playbackRate)
150
- }
151
- if (event.type === 'SELECT_SUBTITLE_STREAM') {
152
- selectedStreamIndex = event.streamIndex
153
- sendBack({ type: 'SELECTED_SUBTITLE_STREAM_UPDATED', streamIndex: event.streamIndex })
154
- if (!jassubInstance) return
155
- jassubInstance.freeTrack()
156
- jassubInstance.setCurrentTime(video.paused, video.currentTime, video.playbackRate)
157
- if (selectedStreamIndex === undefined) {
158
- return
159
- }
160
- appendedSubtitleParts = []
161
- const newSubtitleStreams = subtitlesStreams.get(selectedStreamIndex)
162
- if (!newSubtitleStreams) {
163
- throw new Error('newSubtitleStreams is undefined')
164
- }
165
- const parsedHeader = parse(newSubtitleStreams.header.content)
166
- const modifiedHeader = {
167
- ...parsedHeader,
168
- info: {
169
- ...parsedHeader.info,
170
- ScaledBorderAndShadow: 'no' as const,
171
- LayoutResX: '',
172
- LayoutResY: ''
173
- }
174
- }
175
- const header = stringify(modifiedHeader)
176
- jassubInstance.setTrack(header)
177
- jassubInstance.setCurrentTime(video.paused, video.currentTime, video.playbackRate)
178
- for (const styleIndex in newSubtitleStreams.header.parsed.styles) {
179
- const style = newSubtitleStreams.header.parsed.styles.style[Number(styleIndex)]
180
- if (!style) continue
181
- appendParsedStyle(jassubInstance, style)
182
- }
183
- for (const dialogue of newSubtitleStreams?.dialogues ?? []) {
184
- const alreadyAppended =
185
- appendedSubtitleParts
186
- .filter(appendedSubtitlePart => appendedSubtitlePart.type === 'dialogue')
187
- .find(appendedSubtitlePart =>
188
- appendedSubtitlePart.streamIndex === dialogue.streamIndex
189
- && appendedSubtitlePart.index === dialogue.index
190
- )
191
- if (alreadyAppended || selectedStreamIndex !== dialogue.streamIndex) {
192
- continue
193
- }
194
- jassubInstance?.createEvent(dialogue.assEvent)
195
- appendedSubtitleParts.push(dialogue)
196
- }
197
- }
198
- if (event.type === 'NEW_SUBTITLE_FRAGMENTS') {
199
- const subtitleParts = event.subtitles.map((subtitleFragment) => {
200
- if (subtitleFragment.type === 'header') {
201
- const header = subtitleHeaderFragmentToSubtitleHeaderPart(subtitleFragment)
202
- subtitlesStreams.set(subtitleFragment.streamIndex, { header, dialogues: [] })
203
- if (selectedStreamIndex === undefined) {
204
- selectedStreamIndex = subtitleFragment.streamIndex
205
- sendBack({ type: 'SELECTED_SUBTITLE_STREAM_UPDATED', streamIndex: subtitleFragment.streamIndex })
206
- }
207
- sendBack({ type: 'SUBTITLE_STREAMS_UPDATED', subtitlesStreams: [...subtitlesStreams.values()] })
208
- return header
209
- } else {
210
- const header = subtitlesStreams.get(subtitleFragment.streamIndex)?.header
211
- if (!header) {
212
- throw new Error('SubtitleStream or its header is undefined')
213
- }
214
- const dialogue = subtitleDialogueFragmentToSubtitleDialoguePart(header, subtitleFragment)
215
- subtitlesStreams.get(subtitleFragment.streamIndex)?.dialogues.push(dialogue)
216
- sendBack({ type: 'SUBTITLE_STREAMS_UPDATED', subtitlesStreams: [...subtitlesStreams.values()] })
217
- return dialogue
218
- }
219
- })
220
-
221
- for (const subtitlePart of subtitleParts) {
222
- if (subtitlePart.type === 'dialogue') {
223
- const alreadyAppended =
224
- appendedSubtitleParts
225
- .filter(appendedSubtitlePart => appendedSubtitlePart.type === 'dialogue')
226
- .find(appendedSubtitlePart =>
227
- appendedSubtitlePart.streamIndex === subtitlePart.streamIndex
228
- && appendedSubtitlePart.index === subtitlePart.index
229
- )
230
- if (alreadyAppended || selectedStreamIndex !== subtitlePart.streamIndex) {
231
- continue
232
- }
233
- jassubInstance?.createEvent(subtitlePart.assEvent)
234
- appendedSubtitleParts.push(subtitlePart)
235
- } else if (subtitlePart.type === 'header') {
236
- if (!jassubInstance) {
237
- const parsedHeader = parse(subtitlePart.content)
238
- const modifiedHeader = {
239
- ...parsedHeader,
240
- info: {
241
- ...parsedHeader.info,
242
- ScaledBorderAndShadow: 'no' as const,
243
- LayoutResX: '',
244
- LayoutResY: ''
245
- }
246
- }
247
- const header = stringify(modifiedHeader)
248
- jassubInstance = new JASSUB({
249
- onDemandRender: false,
250
- video,
251
- canvas,
252
- subContent: header,
253
- workerUrl: subtitlesRendererOptions.workerUrl,
254
- modernWasmUrl: subtitlesRendererOptions.wasmUrl,
255
- fonts: attachments.filter(Boolean).map(([filename, data]) => data),
256
- availableFonts: { ...Object.fromEntries(attachments), 'liberation sans': `${publicPath}default.woff2` },
257
- // fallbackFont: 'liberation sans',
258
- })
259
- }
260
- for (const styleIndex in subtitlePart.parsed.styles) {
261
- const style = subtitlePart.parsed.styles.style[Number(styleIndex)]
262
- if (!style) continue
263
- appendParsedStyle(jassubInstance, style)
264
- }
265
- } else {
266
- throw new Error('Unknown subtitlePart type')
267
- }
268
- }
269
- }
270
- if (event.type === 'NEW_ATTACHMENTS') {
271
- attachments =
272
- event
273
- .attachments
274
- .map(attachment => [
275
- attachment.filename,
276
- new Uint8Array(attachment.data)
277
- ])
278
- }
279
- })
280
-
281
- return () => {
282
- clearInterval(currentTimeInterval)
283
- jassubInstance?.destroy()
284
- }
285
- })
@@ -1,123 +0,0 @@
1
- import type { Index } from 'libav-wasm/build/worker'
2
-
3
- import { makeRemuxer } from 'libav-wasm'
4
- import PQueue from 'p-queue'
5
-
6
- import { fromAsyncCallback } from './utils'
7
- import { toStreamChunkSize } from '../utils'
8
- import { DownloadedRange } from '../utils/context'
9
-
10
- type ExtendedIndex = Index & { duration?: number }
11
-
12
- export type Thumbnail = {
13
- url: string
14
- blob: Blob
15
- timestamp: number
16
- duration: number
17
- index: ExtendedIndex
18
- }
19
-
20
- type DataSourceEvents =
21
- | { type: 'DOWNLOADED_RANGES_UPDATED', downloadedRanges: DownloadedRange[] }
22
-
23
- type DataSourceEmittedEvents =
24
- | { type: 'NEW_THUMBNAIL', thumbnail: Thumbnail }
25
-
26
- type DataSourceInput = {
27
- remuxerOptions: Parameters<typeof makeRemuxer>[0]
28
- }
29
-
30
- export default fromAsyncCallback<DataSourceEvents, DataSourceInput, DataSourceEmittedEvents>(async ({ sendBack, receive, input, self, emit }) => {
31
- const { remuxerOptions } = input
32
- const { publicPath, workerUrl, bufferSize, length, read } = remuxerOptions
33
-
34
- let resolve: (value: void) => void
35
- const readyPromise = new Promise<void>(_resolve => {
36
- resolve = _resolve
37
- })
38
-
39
- receive(async (event) => {
40
- await readyPromise
41
- if (event.type === 'DOWNLOADED_RANGES_UPDATED') {
42
- // take 1 index every 5seconds
43
- const selectedIndexes =
44
- metadata
45
- .indexes
46
- .reduce((acc, index) => {
47
- const lastIndex = acc.at(-1)
48
- if (lastIndex && index.timestamp - lastIndex.timestamp < 5) {
49
- return acc
50
- } else {
51
- const nextValidIndex =
52
- metadata
53
- .indexes
54
- .slice(index.index + 1)
55
- .find(nextIndex => nextIndex.timestamp > index.timestamp + 5)
56
- if (!nextValidIndex) return [...acc, index]
57
- return [...acc, { ...index, duration: nextValidIndex.timestamp - index.timestamp }]
58
- }
59
- }, [] as Index[])
60
-
61
- const readyIndexes =
62
- selectedIndexes
63
- .filter((index) => {
64
- const nextIndex = metadata.indexes.at(index.index + 1)
65
- const endByte = nextIndex ? nextIndex.pos : remuxerOptions.length
66
- const startByte = index.pos
67
- const isWithinDownloadedRange =
68
- event
69
- .downloadedRanges
70
- .some(({ startByteOffset, endByteOffset }) =>
71
- startByteOffset <= startByte && endByte <=endByteOffset
72
- )
73
- return isWithinDownloadedRange
74
- })
75
- .sort((a, b) => a.timestamp - b.timestamp)
76
-
77
- readyIndexes.forEach(index => loadMore(index))
78
- }
79
- })
80
-
81
- const remuxer = await makeRemuxer({
82
- publicPath,
83
- workerUrl,
84
- bufferSize,
85
- length,
86
- read
87
- })
88
-
89
- const metadata = await remuxer.init()
90
-
91
- const queue = new PQueue({ concurrency: 1 })
92
-
93
- let thumbnails: Thumbnail[] = []
94
- const loadMore = (index: ExtendedIndex) =>
95
- queue.add(async () => {
96
- if (thumbnails.find(thumbnail => thumbnail.index.index === index.index)) return
97
- const buffer = await remuxer.readKeyframe(index.timestamp)
98
-
99
- const blob = new Blob([buffer], { type: 'image/png' })
100
- const url = URL.createObjectURL(blob)
101
- const nextIndex = metadata.indexes.at(index.index + 1)
102
- const duration =
103
- nextIndex
104
- ? nextIndex.timestamp - index.timestamp
105
- : metadata.info.input.duration - index.timestamp
106
- const thumbnail = {
107
- blob,
108
- url,
109
- timestamp: index.timestamp,
110
- duration: index.duration ?? duration,
111
- index
112
- } satisfies Thumbnail
113
- thumbnails.push(thumbnail)
114
- sendBack({ type: 'NEW_THUMBNAIL', thumbnail })
115
- })
116
-
117
- // @ts-expect-error
118
- resolve()
119
-
120
- return () => {
121
- remuxer.destroy()
122
- }
123
- })