1515 * and call injectDurations() to complete the compilation.
1616 */
1717
18+ import {
19+ parseStrictFiniteTimingNumber ,
20+ readElementPlaybackRate ,
21+ readMediaStart ,
22+ } from "../runtime/playbackRate.js" ;
23+
1824// ── Types ────────────────────────────────────────────────────────────────
1925
2026export interface UnresolvedElement {
@@ -25,6 +31,7 @@ export interface UnresolvedElement {
2531 end ?: number ;
2632 duration ?: number ;
2733 mediaStart : number ;
34+ playbackRate : number ;
2835 compositionSrc ?: string ;
2936}
3037
@@ -40,6 +47,7 @@ export interface ResolvedMediaElement {
4047 start : number ;
4148 duration : number ;
4249 mediaStart : number ;
50+ playbackRate : number ;
4351 loop : boolean ;
4452}
4553
@@ -92,6 +100,11 @@ function injectAttr(tag: string, attr: string, value: string): string {
92100 return tag . replace ( / > $ / , ` ${ attr } ="${ value } ">` ) ;
93101}
94102
103+ function setAttr ( tag : string , attr : string , value : string ) : string {
104+ if ( ! hasAttr ( tag , attr ) ) return injectAttr ( tag , attr , value ) ;
105+ return tag . replace ( new RegExp ( `(${ attr } =["'])[^"']*(["'])` ) , `$1${ value } $2` ) ;
106+ }
107+
95108// Real media/timing elements never live inside comments, <script>, or <style>.
96109// The tag regexes below aren't comment-aware, so a comment that merely mentions
97110// `<video>`/`<audio>` gets rewritten as if it were a real element (issue #1938).
@@ -137,14 +150,16 @@ function compileTag(
137150 startStr = "0" ;
138151 }
139152 const start = parseFloat ( startStr ) ;
140- const mediaStartStr = getAttr ( result , "data-media-start" ) ;
141- const mediaStart = mediaStartStr ? parseFloat ( mediaStartStr ) : 0 ;
153+ const attrReader = { getAttribute : ( name : string ) => getAttr ( result , name ) } ;
154+ const mediaStart = readMediaStart ( attrReader ) ;
155+ const playbackRate = readElementPlaybackRate ( attrReader ) ;
142156
143157 // 1. Compute data-end from data-start + data-duration
144158 if ( ! hasAttr ( result , "data-end" ) ) {
145159 const durationStr = getAttr ( result , "data-duration" ) ;
146- if ( durationStr !== null ) {
147- const end = start + parseFloat ( durationStr ) ;
160+ const duration = parseStrictFiniteTimingNumber ( durationStr ) ;
161+ if ( duration != null ) {
162+ const end = start + duration ;
148163 result = injectAttr ( result , "data-end" , String ( end ) ) ;
149164 } else if ( id ) {
150165 // No data-duration: mark as unresolved so caller can provide it
@@ -154,6 +169,7 @@ function compileTag(
154169 src : getAttr ( result , "src" ) ?? undefined ,
155170 start,
156171 mediaStart,
172+ playbackRate,
157173 } ;
158174 }
159175 }
@@ -215,6 +231,7 @@ export function compileTimingAttrs(html: string): CompilationResult {
215231 tagName : "div" ,
216232 start : startStr ? parseFloat ( startStr ) : 0 ,
217233 mediaStart : 0 ,
234+ playbackRate : 1 ,
218235 compositionSrc : compositionSrc ?? undefined ,
219236 } ) ;
220237 }
@@ -241,8 +258,8 @@ export function injectDurations(html: string, resolutions: ResolvedDuration[]):
241258 let result = tag ;
242259
243260 // Add data-duration if missing
244- if ( ! hasAttr ( result , "data-duration" ) ) {
245- result = injectAttr ( result , "data-duration" , String ( duration ) ) ;
261+ if ( parseStrictFiniteTimingNumber ( getAttr ( result , "data-duration" ) ) == null ) {
262+ result = setAttr ( result , "data-duration" , String ( duration ) ) ;
246263 }
247264
248265 // Add data-end if missing
@@ -279,20 +296,21 @@ export function extractResolvedMedia(html: string): ResolvedMediaElement[] {
279296 const durationStr = getAttr ( tag , "data-duration" ) ;
280297 if ( ! id || durationStr === null ) continue ;
281298
282- const duration = parseFloat ( durationStr ) ;
283- if ( ! Number . isFinite ( duration ) || duration <= 0 ) continue ;
299+ const duration = parseStrictFiniteTimingNumber ( durationStr ) ;
300+ if ( duration == null || duration <= 0 ) continue ;
284301
285302 const isVideo = / ^ < v i d e o / i. test ( tag ) ;
286303 const startStr = getAttr ( tag , "data-start" ) ;
287- const mediaStartStr = getAttr ( tag , "data-media-start" ) ;
304+ const attrReader = { getAttribute : ( name : string ) => getAttr ( tag , name ) } ;
288305
289306 resolved . push ( {
290307 id,
291308 tagName : isVideo ? "video" : "audio" ,
292309 src : getAttr ( tag , "src" ) ?? undefined ,
293310 start : startStr !== null ? parseFloat ( startStr ) : 0 ,
294311 duration,
295- mediaStart : mediaStartStr ? parseFloat ( mediaStartStr ) : 0 ,
312+ mediaStart : readMediaStart ( attrReader ) ,
313+ playbackRate : readElementPlaybackRate ( attrReader ) ,
296314 loop : hasAttr ( tag , "loop" ) ,
297315 } ) ;
298316 }
0 commit comments