@@ -16,7 +16,20 @@ type TextEngine = Awaited<ReturnType<typeof createTextEngine>>;
1616
1717export class RichTextPlayer extends Player {
1818 private static readonly PREVIEW_FPS = 60 ;
19+ /** CSS font-weight string → numeric value. Extends WEIGHT_MODIFIERS (@core/fonts/font-config.ts) with CSS aliases. */
20+ private static readonly NAMED_WEIGHTS : Record < string , number > = {
21+ thin : 100 , hairline : 100 ,
22+ extralight : 200 , ultralight : 200 ,
23+ light : 300 ,
24+ normal : 400 , regular : 400 ,
25+ medium : 500 ,
26+ semibold : 600 , demibold : 600 ,
27+ bold : 700 ,
28+ extrabold : 800 , ultrabold : 800 ,
29+ black : 900 , heavy : 900 ,
30+ } ;
1931 private static readonly fontCapabilityCache = new Map < string , Promise < boolean > > ( ) ;
32+ private static readonly fontBytesCache = new Map < string , Promise < ArrayBuffer > > ( ) ;
2033 private textEngine : TextEngine | null = null ;
2134 private renderer : ReturnType < TextEngine [ "createRenderer" ] > | null = null ;
2235 private canvas : HTMLCanvasElement | null = null ;
@@ -36,6 +49,22 @@ export class RichTextPlayer extends Player {
3649 return withoutHash . split ( "?" , 1 ) [ 0 ] ;
3750 }
3851
52+ private static fetchFontBytes ( url : string ) : Promise < ArrayBuffer > {
53+ const cacheKey = RichTextPlayer . getFontSourceCacheKey ( url ) ;
54+ const cached = RichTextPlayer . fontBytesCache . get ( cacheKey ) ;
55+ if ( cached ) return cached ;
56+
57+ const fetchPromise = fetch ( url ) . then ( res => {
58+ if ( ! res . ok ) throw new Error ( `Failed to fetch font: ${ res . status } ` ) ;
59+ return res . arrayBuffer ( ) ;
60+ } ) . catch ( err => {
61+ RichTextPlayer . fontBytesCache . delete ( cacheKey ) ;
62+ throw err ;
63+ } ) ;
64+ RichTextPlayer . fontBytesCache . set ( cacheKey , fetchPromise ) ;
65+ return fetchPromise ;
66+ }
67+
3968 constructor ( edit : Edit , clipConfiguration : ResolvedClip ) {
4069 // Remove fit property for rich-text assets
4170 // This aligns with @shotstack /schemas v1.5.6 which filters fit at track validation
@@ -45,13 +74,14 @@ export class RichTextPlayer extends Player {
4574
4675 private resolveFontWeight ( richTextAsset : RichTextAsset , fallbackWeight : number ) : number {
4776 const explicitWeight = richTextAsset . font ?. weight ;
77+ if ( typeof explicitWeight === "number" ) return explicitWeight ;
4878 if ( typeof explicitWeight === "string" ) {
49- return parseInt ( explicitWeight , 10 ) || fallbackWeight ;
50- }
51- if ( typeof explicitWeight === "number" ) {
52- return explicitWeight ;
79+ const named = RichTextPlayer . NAMED_WEIGHTS [ explicitWeight . toLowerCase ( ) . trim ( ) ] ;
80+ if ( named !== undefined ) return named ;
81+ const parsed = parseInt ( explicitWeight , 10 ) ;
82+ if ( ! Number . isNaN ( parsed ) ) return parsed ;
83+ console . warn ( `Unrecognized font weight "${ explicitWeight } ", defaulting to ${ fallbackWeight } ` ) ;
5384 }
54-
5585 return fallbackWeight ;
5686 }
5787
@@ -108,7 +138,7 @@ export class RichTextPlayer extends Player {
108138
109139 // Determine the font family for the canvas payload:
110140 // Use matched custom font name, or built-in font, or fall back to Roboto
111- const hasFontMatch = customFonts || ( requestedFamily && resolveFontPath ( requestedFamily ) ) ;
141+ const hasFontMatch = customFonts || ( requestedFamily && resolveFontPath ( requestedFamily , fontWeight ) ) ;
112142 const resolvedFamily = hasFontMatch ? baseFontFamily || requestedFamily : undefined ;
113143
114144 return {
@@ -136,7 +166,8 @@ export class RichTextPlayer extends Player {
136166 try {
137167 const fontDesc = { family, weight : weight . toString ( ) } ;
138168 if ( source . type === "url" ) {
139- await this . textEngine ! . registerFontFromUrl ( source . path , fontDesc ) ;
169+ const bytes = await RichTextPlayer . fetchFontBytes ( source . path ) ;
170+ await this . textEngine ! . registerFontFromFile ( new Blob ( [ bytes ] ) , fontDesc ) ;
140171 } else {
141172 await this . textEngine ! . registerFontFromFile ( source . path , fontDesc ) ;
142173 }
@@ -152,11 +183,7 @@ export class RichTextPlayer extends Player {
152183 private createFontCapabilityCheckPromise ( fontUrl : string ) : Promise < boolean > {
153184 return ( async ( ) : Promise < boolean > => {
154185 try {
155- const response = await fetch ( fontUrl ) ;
156- if ( ! response . ok ) {
157- throw new Error ( `Failed to fetch font: ${ response . status } ` ) ;
158- }
159- const buffer = await response . arrayBuffer ( ) ;
186+ const buffer = await RichTextPlayer . fetchFontBytes ( fontUrl ) ;
160187 const font = opentype . parse ( buffer ) ;
161188
162189 // Check for fvar table (variable font) with weight axis
@@ -193,14 +220,13 @@ export class RichTextPlayer extends Player {
193220 return this . fontSupportsBold ;
194221 }
195222
196- private resolveFont ( family : string ) : { url : string ; baseFontFamily : string ; fontWeight : number } | null {
197- const { baseFontFamily, fontWeight } = parseFontFamily ( family ) ;
223+ private resolveFont ( family : string , weight : number ) : { url : string ; baseFontFamily : string ; fontWeight : number } | null {
224+ const { baseFontFamily } = parseFontFamily ( family ) ;
198225
199226 // Check stored font metadata first (for template fonts with UUID-based URLs)
200- // Uses normalized base family + weight to match the correct font file
201- const metadataUrl = this . edit . getFontUrlByFamilyAndWeight ( baseFontFamily , fontWeight ) ;
227+ const metadataUrl = this . edit . getFontUrlByFamilyAndWeight ( baseFontFamily , weight ) ;
202228 if ( metadataUrl ) {
203- return { url : metadataUrl , baseFontFamily, fontWeight } ;
229+ return { url : metadataUrl , baseFontFamily, fontWeight : weight } ;
204230 }
205231
206232 // Check timeline fonts by filename matching (legacy fallback)
@@ -213,13 +239,13 @@ export class RichTextPlayer extends Player {
213239 } ) ;
214240
215241 if ( matchingFont ) {
216- return { url : matchingFont . src , baseFontFamily, fontWeight } ;
242+ return { url : matchingFont . src , baseFontFamily, fontWeight : weight } ;
217243 }
218244
219- // Fall back to built-in fonts from FONT_PATHS
220- const builtInPath = resolveFontPath ( family ) ;
221- if ( builtInPath ) {
222- return { url : builtInPath , baseFontFamily, fontWeight } ;
245+ // Fall back to built-in/Google fonts — single function, single priority chain
246+ const resolvedPath = resolveFontPath ( family , weight ) ;
247+ if ( resolvedPath ) {
248+ return { url : resolvedPath , baseFontFamily, fontWeight : weight } ;
223249 }
224250
225251 return null ;
@@ -261,10 +287,10 @@ export class RichTextPlayer extends Player {
261287 const family = richTextAsset . font ?. family ;
262288 if ( ! family ) return null ;
263289
264- const resolved = this . resolveFont ( family ) ;
290+ const fontWeight = this . resolveFontWeight ( richTextAsset , parseFontFamily ( family ) . fontWeight ) ;
291+ const resolved = this . resolveFont ( family , fontWeight ) ;
265292 if ( ! resolved ) return null ;
266293
267- const fontWeight = this . resolveFontWeight ( richTextAsset , resolved . fontWeight ) ;
268294 await this . registerFont ( resolved . baseFontFamily , fontWeight , { type : "url" , path : resolved . url } ) ;
269295 return resolved . url ;
270296 }
0 commit comments