@banou/media-player 0.5.1 → 0.6.1

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 (43) hide show
  1. package/README.md +6 -6
  2. package/build/components/playback-slider.d.ts +2 -0
  3. package/build/index.js +1159 -1012
  4. package/build/state-machines/index.d.ts +2503 -1838
  5. package/build/state-machines/media.d.ts +218 -51
  6. package/build/state-machines/subtitles.d.ts +3 -0
  7. package/build/utils/use-local-storage.d.ts +1 -1
  8. package/package.json +51 -51
  9. package/src/assets/picture-in-picture.svg +5 -5
  10. package/src/components/chrome.tsx +95 -95
  11. package/src/components/control-bar.tsx +333 -327
  12. package/src/components/overlay.tsx +91 -91
  13. package/src/components/playback-slider.tsx +174 -0
  14. package/src/components/progress-bar.tsx +251 -251
  15. package/src/components/settings.tsx +321 -268
  16. package/src/components/sound.tsx +100 -100
  17. package/src/components/tooltip-display.tsx +83 -83
  18. package/src/components/volume-slider.tsx +156 -156
  19. package/src/index.tsx +195 -195
  20. package/src/main.tsx +169 -169
  21. package/src/state-machines/data-source.ts +84 -84
  22. package/src/state-machines/index.ts +5 -5
  23. package/src/state-machines/media-properties.ts +82 -82
  24. package/src/state-machines/media-source.ts +129 -129
  25. package/src/state-machines/media.ts +255 -252
  26. package/src/state-machines/subtitles.ts +285 -269
  27. package/src/state-machines/thumbnails.ts +123 -123
  28. package/src/state-machines/utils.ts +94 -94
  29. package/src/utils/actor-utils.ts +19 -19
  30. package/src/utils/colors.ts +8 -8
  31. package/src/utils/context.ts +23 -23
  32. package/src/utils/fonts.ts +159 -159
  33. package/src/utils/index.ts +229 -229
  34. package/src/utils/languages.ts +262 -262
  35. package/src/utils/mp4box.ts +74 -74
  36. package/src/utils/time.ts +15 -15
  37. package/src/utils/use-local-storage.ts +30 -30
  38. package/src/utils/use-scrub.ts +40 -40
  39. package/src/utils/volume-utils.ts +39 -39
  40. package/src/utils/window-height.ts +19 -19
  41. package/src/vite-env.d.ts +1 -1
  42. package/tsconfig.json +28 -28
  43. package/tsconfig.node.json +9 -9
@@ -1,269 +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
-
15
- type SubtitlesEmittedEvents =
16
- | { type: 'WAITING' }
17
- | { type: 'SUBTITLE_STREAMS_UPDATED', subtitlesStreams: SubtitleStream[] }
18
- | { type: 'SELECTED_SUBTITLE_STREAM_UPDATED', streamIndex: number }
19
-
20
- type SubtitlesInput = {
21
- publicPath: string
22
- videoElement: HTMLVideoElement
23
- canvasElement: HTMLCanvasElement
24
- subtitlesRendererOptions: Omit<JassubOptions, 'video' | 'canvas'>
25
- }
26
-
27
- export type SubtitlePart =
28
- | { type: 'header', streamIndex: number, content: string, eventsContent: string, dialogueFormatContent: string, parsed: ParsedASS }
29
- | { type: 'dialogue', streamIndex: number, index: number, content: string, parsed: ParsedASS, assEvent: ASS_Event }
30
-
31
- export type SubtitleStream = {
32
- header: SubtitlePart & { type: 'header' }
33
- dialogues: (SubtitlePart & { type: 'dialogue' })[]
34
- }
35
-
36
- const convertTimestamp = (ms: number) =>
37
- new Date(ms)
38
- .toISOString()
39
- .slice(11, 22)
40
-
41
- const appendParsedStyle = (jassubInstance: JASSUB, style: ParsedASSStyles['style'][number]) => {
42
- jassubInstance.createStyle({
43
- ...style,
44
- SecondaryColour: Number(style.SecondaryColour),
45
- treat_fontname_as_pattern: 0,
46
- Blur: 0,
47
- Justify: 0,
48
- FontName: style.Fontname,
49
- FontSize: Number(style.Fontsize),
50
- PrimaryColour: Number(style.PrimaryColour),
51
- BackColour: Number(style.BackColour),
52
- OutlineColour: Number(style.OutlineColour),
53
- Bold: Number(style.Bold),
54
- Italic: Number(style.Italic),
55
- Underline: Number(style.Underline),
56
- StrikeOut: Number(style.StrikeOut),
57
- ScaleX: Number(style.ScaleX),
58
- ScaleY: Number(style.ScaleY),
59
- Spacing: Number(style.Spacing),
60
- Angle: Number(style.Angle),
61
- BorderStyle: Number(style.BorderStyle),
62
- Outline: Number(style.Outline),
63
- Shadow: Number(style.Shadow),
64
- Alignment: Number(style.Alignment),
65
- MarginL: Number(style.MarginL),
66
- MarginR: Number(style.MarginR),
67
- MarginV: Number(style.MarginV),
68
- Encoding: Number(style.Encoding)
69
- })
70
- }
71
-
72
- const subtitleHeaderFragmentToSubtitleHeaderPart = (subtitleHeaderFragment: SubtitleFragment) => {
73
- const eventsContent =
74
- subtitleHeaderFragment
75
- .content
76
- .match(/\r\n\[Events\]\r\nFormat: (.*)/)
77
- ?.[0]
78
- const dialogueFormatContent =
79
- eventsContent
80
- ?.trim()
81
- .split('\n')
82
- [1]
83
- if (!eventsContent || !dialogueFormatContent) {
84
- throw new Error('dialogueFormatContent is undefined')
85
- }
86
- return {
87
- type: 'header',
88
- streamIndex: subtitleHeaderFragment.streamIndex,
89
- content: subtitleHeaderFragment.content,
90
- eventsContent,
91
- dialogueFormatContent,
92
- parsed: parse(subtitleHeaderFragment.content)
93
- } as SubtitlePart & { type: 'header' }
94
- }
95
-
96
- const subtitleDialogueFragmentToSubtitleDialoguePart = (
97
- header: SubtitlePart & { type: 'header' },
98
- subtitleFragment: SubtitleFragment & { type: 'dialogue' }
99
- ) => {
100
- const [dialogueIndexString, layer] = subtitleFragment.content.split(',')
101
- const dialogueIndex = Number(dialogueIndexString)
102
- const startTimestamp = convertTimestamp(subtitleFragment.start)
103
- const endTimestamp = convertTimestamp(subtitleFragment.end)
104
- const dialogueSourceContent = subtitleFragment.content.replace(`${dialogueIndex},${layer},`, '')
105
- const dialogueContent = `Dialogue: ${layer},${startTimestamp},${endTimestamp},${dialogueSourceContent}`
106
- const parsedEvent = parse(`${header.eventsContent}\r\n${dialogueContent}`)
107
- const dialogueEvent = parsedEvent.events.dialogue[0]
108
- if (!dialogueEvent) {
109
- throw new Error('dialogueEvent is undefined')
110
- }
111
- return {
112
- type: 'dialogue',
113
- streamIndex: subtitleFragment.streamIndex,
114
- index: dialogueIndex,
115
- content: dialogueContent,
116
- parsed: parse(`${header.eventsContent}\r\n${dialogueContent}`),
117
- assEvent: {
118
- ...dialogueEvent,
119
- Effect: dialogueEvent.Effect ?? '',
120
- Text: dialogueEvent.Text.raw,
121
- Duration: (dialogueEvent.End - dialogueEvent.Start) * 1000,
122
- Start: dialogueEvent.Start * 1000,
123
- End: dialogueEvent.End * 1000,
124
- ReadOrder: dialogueIndex,
125
- _index: dialogueIndex
126
- } as ASS_Event
127
- } as SubtitlePart & { type: 'dialogue' }
128
- }
129
-
130
- export default fromAsyncCallback<SubtitlesEvents, SubtitlesInput, SubtitlesEmittedEvents>(async ({ sendBack, receive, input, self, emit }) => {
131
- const { publicPath, subtitlesRendererOptions, videoElement: video, canvasElement: canvas } = input
132
-
133
- let attachments: [string, Uint8Array][] = []
134
- let jassubInstance: JASSUB | undefined
135
-
136
- const subtitlesStreams = new Map<number, SubtitleStream>()
137
- let appendedSubtitleParts: SubtitlePart[] = []
138
- let selectedStreamIndex: number | undefined
139
-
140
- receive((event) => {
141
- if (event.type === 'SELECT_SUBTITLE_STREAM') {
142
- selectedStreamIndex = event.streamIndex
143
- sendBack({ type: 'SELECTED_SUBTITLE_STREAM_UPDATED', streamIndex: event.streamIndex })
144
- if (!jassubInstance) return
145
- jassubInstance.freeTrack()
146
- appendedSubtitleParts = []
147
- const newSubtitleStreams = subtitlesStreams.get(selectedStreamIndex)
148
- if (!newSubtitleStreams) {
149
- throw new Error('newSubtitleStreams is undefined')
150
- }
151
- const parsedHeader = parse(newSubtitleStreams.header.content)
152
- const modifiedHeader = {
153
- ...parsedHeader,
154
- info: {
155
- ...parsedHeader.info,
156
- ScaledBorderAndShadow: 'no' as const,
157
- LayoutResX: '',
158
- LayoutResY: ''
159
- }
160
- }
161
- const header = stringify(modifiedHeader)
162
- jassubInstance.setTrack(header)
163
- jassubInstance.setCurrentTime(video.paused, video.currentTime, video.playbackRate)
164
- for (const styleIndex in newSubtitleStreams.header.parsed.styles) {
165
- const style = newSubtitleStreams.header.parsed.styles.style[Number(styleIndex)]
166
- if (!style) continue
167
- appendParsedStyle(jassubInstance, style)
168
- }
169
- for (const dialogue of newSubtitleStreams?.dialogues ?? []) {
170
- const alreadyAppended =
171
- appendedSubtitleParts
172
- .filter(appendedSubtitlePart => appendedSubtitlePart.type === 'dialogue')
173
- .find(appendedSubtitlePart =>
174
- appendedSubtitlePart.streamIndex === dialogue.streamIndex
175
- && appendedSubtitlePart.index === dialogue.index
176
- )
177
- if (alreadyAppended || selectedStreamIndex !== dialogue.streamIndex) {
178
- continue
179
- }
180
- jassubInstance?.createEvent(dialogue.assEvent)
181
- appendedSubtitleParts.push(dialogue)
182
- }
183
- }
184
- if (event.type === 'NEW_SUBTITLE_FRAGMENTS') {
185
- const subtitleParts = event.subtitles.map((subtitleFragment) => {
186
- if (subtitleFragment.type === 'header') {
187
- const header = subtitleHeaderFragmentToSubtitleHeaderPart(subtitleFragment)
188
- subtitlesStreams.set(subtitleFragment.streamIndex, { header, dialogues: [] })
189
- if (selectedStreamIndex === undefined) {
190
- selectedStreamIndex = subtitleFragment.streamIndex
191
- sendBack({ type: 'SELECTED_SUBTITLE_STREAM_UPDATED', streamIndex: subtitleFragment.streamIndex })
192
- }
193
- sendBack({ type: 'SUBTITLE_STREAMS_UPDATED', subtitlesStreams: [...subtitlesStreams.values()] })
194
- return header
195
- } else {
196
- const header = subtitlesStreams.get(subtitleFragment.streamIndex)?.header
197
- if (!header) {
198
- throw new Error('SubtitleStream or its header is undefined')
199
- }
200
- const dialogue = subtitleDialogueFragmentToSubtitleDialoguePart(header, subtitleFragment)
201
- subtitlesStreams.get(subtitleFragment.streamIndex)?.dialogues.push(dialogue)
202
- sendBack({ type: 'SUBTITLE_STREAMS_UPDATED', subtitlesStreams: [...subtitlesStreams.values()] })
203
- return dialogue
204
- }
205
- })
206
-
207
- for (const subtitlePart of subtitleParts) {
208
- if (subtitlePart.type === 'dialogue') {
209
- const alreadyAppended =
210
- appendedSubtitleParts
211
- .filter(appendedSubtitlePart => appendedSubtitlePart.type === 'dialogue')
212
- .find(appendedSubtitlePart =>
213
- appendedSubtitlePart.streamIndex === subtitlePart.streamIndex
214
- && appendedSubtitlePart.index === subtitlePart.index
215
- )
216
- if (alreadyAppended || selectedStreamIndex !== subtitlePart.streamIndex) {
217
- continue
218
- }
219
- jassubInstance?.createEvent(subtitlePart.assEvent)
220
- appendedSubtitleParts.push(subtitlePart)
221
- } else if (subtitlePart.type === 'header') {
222
- if (!jassubInstance) {
223
- const parsedHeader = parse(subtitlePart.content)
224
- const modifiedHeader = {
225
- ...parsedHeader,
226
- info: {
227
- ...parsedHeader.info,
228
- ScaledBorderAndShadow: 'no' as const,
229
- LayoutResX: '',
230
- LayoutResY: ''
231
- }
232
- }
233
- const header = stringify(modifiedHeader)
234
- jassubInstance = new JASSUB({
235
- video,
236
- canvas,
237
- subContent: header,
238
- workerUrl: subtitlesRendererOptions.workerUrl,
239
- modernWasmUrl: subtitlesRendererOptions.wasmUrl,
240
- fonts: attachments.filter(Boolean).map(([filename, data]) => data),
241
- availableFonts: { ...Object.fromEntries(attachments), 'liberation sans': `${publicPath}default.woff2` },
242
- // fallbackFont: 'liberation sans',
243
- })
244
- }
245
- for (const styleIndex in subtitlePart.parsed.styles) {
246
- const style = subtitlePart.parsed.styles.style[Number(styleIndex)]
247
- if (!style) continue
248
- appendParsedStyle(jassubInstance, style)
249
- }
250
- } else {
251
- throw new Error('Unknown subtitlePart type')
252
- }
253
- }
254
- }
255
- if (event.type === 'NEW_ATTACHMENTS') {
256
- attachments =
257
- event
258
- .attachments
259
- .map(attachment => [
260
- attachment.filename,
261
- new Uint8Array(attachment.data)
262
- ])
263
- }
264
- })
265
-
266
- return () => {
267
- jassubInstance?.destroy()
268
- }
269
- })
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
+ })