-
Notifications
You must be signed in to change notification settings - Fork 2
/
ass.ts
240 lines (209 loc) · 6.87 KB
/
ass.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
import { compile, type CompiledASS } from 'ass-compiler';
import { Renderer } from './renderer';
import type { OnInitSizes } from './types';
export type ASSOptions = {
/**
* The ass text
*/
assText: string;
/**
* The video to display the subtile on.
* Can be either an `HTMLVideoElement` or `string` (html query selector)
*/
video: HTMLVideoElement | string;
/**
* List of fonts to load. This ensures that all the fonts
* needed for the rendering are present and loaded into the document
*/
fonts?: Font[];
/**
* Corresponds to the `z-index` to placed the Canvas renderer
* > The renderer will always be added right after the `video` element
*/
zIndex?: number;
/**
* A Callback that is invoked when the preprocess of the ass text by render is done
*/
onReady?: () => void;
/**
* Type of logging
* - `DEBUG` only debug type log will be displayed
* - `DISABLE` no logging will be emitted (default)
* - `VERBOSE` every log will be shown
* - `WARN` only warning will be shown
*/
logging?: LOGTYPE;
};
export type LOGTYPE = 'DISABLE' | 'VERBOSE' | 'DEBUG' | 'WARN';
export type FontStyle = {
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/FontFace/ascentOverride) */
ascentOverride: string;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/FontFace/descentOverride) */
descentOverride: string;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/FontFace/display) */
display: FontDisplay;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/FontFace/family) */
family: string;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/FontFace/featureSettings) */
featureSettings: string;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/FontFace/lineGapOverride) */
lineGapOverride: string;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/FontFace/stretch) */
stretch: string;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/FontFace/style) */
style: string;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/FontFace/unicodeRange) */
unicodeRange: string;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/FontFace/variant) */
variant: string;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/FontFace/weight) */
weight: string;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/FontFace/load) */
};
export type Font = {
family: string;
url: string;
descriptors?: Partial<FontStyle>;
};
/**
* @class ASS
*
* ASS is an ass/ssa subtitle renderer.
*
* It uses a `canvas` that is placed on top of
* the targeted video element
* */
export default class ASS {
assText: string;
private video: HTMLVideoElement | string;
videoElement: HTMLVideoElement | null = null;
private renderer: Renderer | null = null;
private ro: ResizeObserver | null = null;
private fonts?: Font[];
private zIndex?: number;
private onReady?: () => void;
private logging: LOGTYPE = 'DISABLE';
private compiledAss: CompiledASS;
constructor(options: ASSOptions) {
this.assText = options.assText;
this.compiledAss = compile(this.assText, {});
this.video = options.video;
this.fonts = options.fonts;
this.zIndex = options.zIndex;
this.onReady = options.onReady;
if (options.logging) this.logging = options.logging;
}
/**
* Start the ass rendering process
*/
async render() {
if (typeof this.video == 'string') {
this.videoElement = document.querySelector(this.video);
if (this.videoElement === null) {
throw new Error('Unable to find the video element');
}
} else {
this.videoElement = this.video;
}
const sizes = this.setCanvasSize();
if (typeof this.fonts !== 'undefined') {
await this.loadFonts(this.fonts);
}
if (this.videoElement) {
this.renderer = new Renderer(
this.compiledAss,
sizes,
this.videoElement,
this.logging,
this.zIndex
);
this.setCanvasSize();
this.videoElement.addEventListener('loadedmetadata', this.setCanvasSize.bind(this));
this.ro = new ResizeObserver(this.setCanvasSize.bind(this));
this.ro.observe(this.videoElement);
await this.renderer.warmup();
if (this.onReady) {
this.onReady();
}
await this.renderer.startRendering();
}
}
/**
* Stop the rendering
*/
destroy() {
this.videoElement?.removeEventListener('loadedmetadata', this.setCanvasSize.bind(this));
this.ro?.disconnect();
this.renderer?.destroy();
this.renderer = null;
}
private setCanvasSize() {
const { videoWidth, videoHeight, offsetTop, offsetLeft } = this
.videoElement as HTMLVideoElement;
const aspectRatio = videoWidth / videoHeight;
const maxWidth = this.videoElement?.clientWidth || 0;
const maxHeight = this.videoElement?.clientHeight || 0;
let width = maxWidth;
let height = maxHeight;
let x = offsetLeft;
let y = offsetTop;
if (maxHeight * aspectRatio > maxWidth) {
width = maxWidth;
height = width / aspectRatio;
y += (maxHeight - height) / 2;
} else {
height = maxHeight;
width = height * aspectRatio;
x += (maxWidth - width) / 2;
}
const sizes = {
width,
height,
x,
y
} as OnInitSizes;
if (this.renderer?.renderDiv) {
this.renderer.renderDiv.style.width = width + 'px';
this.renderer.renderDiv.style.height = height + 'px';
this.renderer.renderDiv.style.top = y + 'px';
this.renderer.renderDiv.style.left = x + 'px';
}
this.renderer?.layers.forEach((layer) => {
layer.canvas.width = width;
layer.canvas.height = height;
});
return sizes;
}
private async loadFonts(fonts: Font[]) {
for (const font of fonts) {
try {
const loaded = await this.loadFont(font);
if (loaded) {
if (this.logging == 'VERBOSE')
console.info(`Font ${font.family} loaded from ${font.url}`);
} else {
if (this.logging == 'VERBOSE' || this.logging == 'WARN')
console.warn(`Unable to load font ${font.family} from ${font.url}`);
}
} catch (e) {
if (this.logging == 'VERBOSE' || this.logging == 'WARN') {
console.warn(`Unable to load font ${font.family} from ${font.url}`);
console.warn(e);
}
}
}
}
private async getFontUrl(fontUrl: string) {
const response = await fetch(fontUrl);
const blob = await response.blob();
return URL.createObjectURL(blob);
}
private async loadFont(font: Font) {
const url = await this.getFontUrl(font.url);
const fontFace = new FontFace(font.family, `url(${url})`, font.descriptors || {});
const loadedFace = await fontFace.load();
// @ts-ignore
document.fonts.add(loadedFace);
return fontFace.status === 'loaded';
}
}