@@ -2,6 +2,35 @@ import type { RuntimePlayer, RuntimeTimelineLike } from "./types";
22import { quantizeTimeToFrame } from "../inline-scripts/parityContract" ;
33import { swallow } from "./diagnostics" ;
44
5+ /**
6+ * Safely read a numeric value from a timeline property that may be either a
7+ * function (conformant GSAP) or a bare number (user-authored timeline-like).
8+ */
9+ function safeNum ( obj : unknown , prop : string , fallback : number ) : number {
10+ const val = ( obj as Record < string , unknown > ) ?. [ prop ] ;
11+ if ( typeof val === "function" ) return Number ( val . call ( obj ) ) || fallback ;
12+ if ( typeof val === "number" && Number . isFinite ( val ) ) return val ;
13+ if ( val !== undefined && val !== null ) {
14+ swallow ( "runtime.player.nonConformantNum" , { prop, actual : typeof val } ) ;
15+ }
16+ return fallback ;
17+ }
18+
19+ /**
20+ * Safely invoke a void method on a timeline. If the method is not a function
21+ * (missing or overwritten with a non-callable value), silently skip.
22+ */
23+ function safeVoid ( obj : unknown , method : string ) : void {
24+ const fn = ( obj as Record < string , unknown > ) ?. [ method ] ;
25+ if ( typeof fn === "function" ) {
26+ fn . call ( obj ) ;
27+ return ;
28+ }
29+ if ( fn !== undefined ) {
30+ swallow ( "runtime.player.nonConformantVoid" , { method, actual : typeof fn } ) ;
31+ }
32+ }
33+
534type PlayerDeps = {
635 getTimeline : ( ) => RuntimeTimelineLike | null ;
736 setTimeline : ( timeline : RuntimeTimelineLike | null ) => void ;
@@ -52,11 +81,11 @@ function seekTimelineDeterministically(
5281 canonicalFps : number ,
5382) : number {
5483 const quantized = quantizeTimeToFrame ( timeSeconds , canonicalFps ) ;
55- timeline . pause ( ) ;
84+ safeVoid ( timeline , " pause" ) ;
5685 if ( typeof timeline . totalTime === "function" ) {
5786 timeline . totalTime ( quantized , false ) ;
5887 } else {
59- timeline . seek ( quantized , false ) ;
88+ if ( typeof timeline . seek === "function" ) timeline . seek ( quantized , false ) ;
6089 }
6190 return quantized ;
6291}
@@ -69,15 +98,15 @@ function seekMasterAndSiblingTimelinesDeterministically(
6998) : number {
7099 const rearmedSiblings : RuntimeTimelineLike [ ] = [ ] ;
71100 forEachSiblingTimeline ( registry , master , ( tl ) => {
72- tl . play ( ) ;
101+ safeVoid ( tl , " play" ) ;
73102 rearmedSiblings . push ( tl ) ;
74103 } ) ;
75104 try {
76105 return seekTimelineDeterministically ( master , timeSeconds , canonicalFps ) ;
77106 } finally {
78107 for ( const tl of rearmedSiblings ) {
79108 try {
80- tl . pause ( ) ;
109+ safeVoid ( tl , " pause" ) ;
81110 } catch ( err ) {
82111 // ignore sibling failures — one broken timeline shouldn't poison seek
83112 swallow ( "runtime.player.site2" , err ) ;
@@ -91,7 +120,7 @@ function activateSiblingTimelines(
91120 master : RuntimeTimelineLike ,
92121) : void {
93122 forEachSiblingTimeline ( registry , master , ( tl ) => {
94- tl . play ( ) ;
123+ safeVoid ( tl , " play" ) ;
95124 } ) ;
96125}
97126
@@ -103,13 +132,13 @@ export function createRuntimePlayer(deps: PlayerDeps): RuntimePlayer {
103132 if ( ! timeline || deps . getIsPlaying ( ) ) return ;
104133 const safeDuration = Math . max (
105134 0 ,
106- Number ( deps . getSafeDuration ?.( ) ?? timeline . duration ( ) ?? 0 ) || 0 ,
135+ Number ( deps . getSafeDuration ?.( ) ?? safeNum ( timeline , " duration" , 0 ) ) || 0 ,
107136 ) ;
108137 if ( safeDuration > 0 ) {
109- const currentTime = Math . max ( 0 , Number ( timeline . time ( ) ) || 0 ) ;
138+ const currentTime = Math . max ( 0 , safeNum ( timeline , " time" , 0 ) ) ;
110139 if ( currentTime >= safeDuration ) {
111- timeline . pause ( ) ;
112- timeline . seek ( 0 , false ) ;
140+ safeVoid ( timeline , " pause" ) ;
141+ if ( typeof timeline . seek === "function" ) timeline . seek ( 0 , false ) ;
113142 deps . onDeterministicSeek ( 0 ) ;
114143 deps . setIsPlaying ( false ) ;
115144 deps . onSyncMedia ( 0 , false ) ;
@@ -119,10 +148,10 @@ export function createRuntimePlayer(deps: PlayerDeps): RuntimePlayer {
119148 if ( typeof timeline . timeScale === "function" ) {
120149 timeline . timeScale ( deps . getPlaybackRate ( ) ) ;
121150 }
122- timeline . play ( ) ;
151+ safeVoid ( timeline , " play" ) ;
123152 forEachSiblingTimeline ( deps . getTimelineRegistry ?.( ) , timeline , ( tl ) => {
124153 if ( typeof tl . timeScale === "function" ) tl . timeScale ( deps . getPlaybackRate ( ) ) ;
125- tl . play ( ) ;
154+ safeVoid ( tl , " play" ) ;
126155 } ) ;
127156 deps . onDeterministicPlay ( ) ;
128157 deps . setIsPlaying ( true ) ;
@@ -132,11 +161,11 @@ export function createRuntimePlayer(deps: PlayerDeps): RuntimePlayer {
132161 pause : ( ) => {
133162 const timeline = deps . getTimeline ( ) ;
134163 if ( ! timeline ) return ;
135- timeline . pause ( ) ;
164+ safeVoid ( timeline , " pause" ) ;
136165 forEachSiblingTimeline ( deps . getTimelineRegistry ?.( ) , timeline , ( tl ) => {
137- tl . pause ( ) ;
166+ safeVoid ( tl , " pause" ) ;
138167 } ) ;
139- const time = Math . max ( 0 , Number ( timeline . time ( ) ) || 0 ) ;
168+ const time = Math . max ( 0 , safeNum ( timeline , " time" , 0 ) ) ;
140169 deps . onDeterministicSeek ( time ) ;
141170 deps . onDeterministicPause ( ) ;
142171 deps . setIsPlaying ( false ) ;
@@ -162,10 +191,10 @@ export function createRuntimePlayer(deps: PlayerDeps): RuntimePlayer {
162191 if ( typeof timeline . timeScale === "function" ) {
163192 timeline . timeScale ( deps . getPlaybackRate ( ) ) ;
164193 }
165- timeline . play ( ) ;
194+ safeVoid ( timeline , " play" ) ;
166195 forEachSiblingTimeline ( deps . getTimelineRegistry ?.( ) , timeline , ( tl ) => {
167196 if ( typeof tl . timeScale === "function" ) tl . timeScale ( deps . getPlaybackRate ( ) ) ;
168- tl . play ( ) ;
197+ safeVoid ( tl , " play" ) ;
169198 } ) ;
170199 deps . onDeterministicPlay ( ) ;
171200 deps . onShowNativeVideos ( ) ;
@@ -199,8 +228,8 @@ export function createRuntimePlayer(deps: PlayerDeps): RuntimePlayer {
199228 deps . onRenderFrameSeek ( quantized ) ;
200229 deps . onStatePost ( true ) ;
201230 } ,
202- getTime : ( ) => Number ( deps . getTimeline ( ) ?. time ( ) ?? 0 ) ,
203- getDuration : ( ) => Number ( deps . getTimeline ( ) ?. duration ( ) ?? 0 ) ,
231+ getTime : ( ) => safeNum ( deps . getTimeline ( ) , "time" , 0 ) ,
232+ getDuration : ( ) => safeNum ( deps . getTimeline ( ) , "duration" , 0 ) ,
204233 isPlaying : ( ) => deps . getIsPlaying ( ) ,
205234 setPlaybackRate : ( rate : number ) => deps . setPlaybackRate ( rate ) ,
206235 getPlaybackRate : ( ) => deps . getPlaybackRate ( ) ,
0 commit comments