@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.
- package/README.md +6 -6
- package/package.json +51 -51
- package/src/assets/picture-in-picture.svg +5 -5
- package/src/components/chrome.tsx +95 -95
- package/src/components/control-bar.tsx +333 -333
- package/src/components/overlay.tsx +91 -91
- package/src/components/playback-slider.tsx +174 -174
- package/src/components/progress-bar.tsx +251 -251
- package/src/components/settings.tsx +321 -321
- package/src/components/sound.tsx +100 -100
- package/src/components/tooltip-display.tsx +83 -83
- package/src/components/volume-slider.tsx +156 -156
- package/src/index.tsx +195 -195
- package/src/main.tsx +169 -169
- package/src/state-machines/data-source.ts +84 -84
- package/src/state-machines/index.ts +5 -5
- package/src/state-machines/media-properties.ts +82 -82
- package/src/state-machines/media-source.ts +129 -129
- package/src/state-machines/media.ts +255 -255
- package/src/state-machines/subtitles.ts +285 -285
- package/src/state-machines/thumbnails.ts +123 -123
- package/src/state-machines/utils.ts +94 -94
- package/src/utils/actor-utils.ts +19 -19
- package/src/utils/colors.ts +8 -8
- package/src/utils/context.ts +23 -23
- package/src/utils/fonts.ts +159 -159
- package/src/utils/index.ts +229 -229
- package/src/utils/languages.ts +262 -262
- package/src/utils/mp4box.ts +74 -74
- package/src/utils/time.ts +15 -15
- package/src/utils/use-local-storage.ts +30 -30
- package/src/utils/use-scrub.ts +40 -40
- package/src/utils/volume-utils.ts +39 -39
- package/src/utils/window-height.ts +19 -19
- package/src/vite-env.d.ts +1 -1
- package/tsconfig.json +28 -28
- 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
|
+
})
|