@banou/media-player 0.6.0 → 0.6.2

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 (37) hide show
  1. package/README.md +6 -6
  2. package/package.json +51 -51
  3. package/src/assets/picture-in-picture.svg +5 -5
  4. package/src/components/chrome.tsx +95 -95
  5. package/src/components/control-bar.tsx +333 -333
  6. package/src/components/overlay.tsx +91 -91
  7. package/src/components/playback-slider.tsx +174 -174
  8. package/src/components/progress-bar.tsx +251 -251
  9. package/src/components/settings.tsx +321 -321
  10. package/src/components/sound.tsx +100 -100
  11. package/src/components/tooltip-display.tsx +83 -83
  12. package/src/components/volume-slider.tsx +156 -156
  13. package/src/index.tsx +195 -195
  14. package/src/main.tsx +169 -169
  15. package/src/state-machines/data-source.ts +84 -84
  16. package/src/state-machines/index.ts +5 -5
  17. package/src/state-machines/media-properties.ts +82 -82
  18. package/src/state-machines/media-source.ts +129 -129
  19. package/src/state-machines/media.ts +255 -255
  20. package/src/state-machines/subtitles.ts +285 -285
  21. package/src/state-machines/thumbnails.ts +123 -123
  22. package/src/state-machines/utils.ts +94 -94
  23. package/src/utils/actor-utils.ts +19 -19
  24. package/src/utils/colors.ts +8 -8
  25. package/src/utils/context.ts +23 -23
  26. package/src/utils/fonts.ts +159 -159
  27. package/src/utils/index.ts +229 -229
  28. package/src/utils/languages.ts +262 -262
  29. package/src/utils/mp4box.ts +74 -74
  30. package/src/utils/time.ts +15 -15
  31. package/src/utils/use-local-storage.ts +30 -30
  32. package/src/utils/use-scrub.ts +40 -40
  33. package/src/utils/volume-utils.ts +39 -39
  34. package/src/utils/window-height.ts +19 -19
  35. package/src/vite-env.d.ts +1 -1
  36. package/tsconfig.json +28 -28
  37. package/tsconfig.node.json +9 -9
@@ -1,285 +1,285 @@
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
+ 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
+ })