()
| 2452 | }; |
| 2453 | |
| 2454 | const transportTick = () => { |
| 2455 | if (state.tornDown || inTransportTick) return; |
| 2456 | inTransportTick = true; |
| 2457 | try { |
| 2458 | state.transportRafId = window.requestAnimationFrame(transportTick); |
| 2459 | transportTickCount += 1; |
| 2460 | |
| 2461 | // Slower operations: timeline binding (~every 60 frames / ~1s at 60fps) |
| 2462 | if (transportTickCount % 60 === 0) { |
| 2463 | const shouldHoldRebind = |
| 2464 | clock.isPlaying() && |
| 2465 | state.capturedTimeline != null && |
| 2466 | clock.now() < PLAY_REBIND_HOLD_SECONDS; |
| 2467 | if (!shouldHoldRebind) { |
| 2468 | const prevTimeline = state.capturedTimeline; |
| 2469 | if (bindRootTimelineIfAvailable()) { |
| 2470 | if (state.capturedTimeline && !player._timeline) { |
| 2471 | player._timeline = state.capturedTimeline; |
| 2472 | } |
| 2473 | if (state.capturedTimeline && state.capturedTimeline !== prevTimeline) { |
| 2474 | state.capturedTimeline.pause(); |
| 2475 | } |
| 2476 | const dur = getSafeTimelineDurationSeconds(state.capturedTimeline, 0); |
| 2477 | if (dur > 0) clock.setDuration(dur); |
| 2478 | postTimeline(); |
| 2479 | } |
| 2480 | } |
| 2481 | } |
| 2482 | if (transportTickCount % 20 === 0) { |
| 2483 | postTimeline(); |
| 2484 | } |
| 2485 | if (transportTickCount % 30 === 0) { |
| 2486 | bindMediaMetadataListeners(); |
| 2487 | } |
| 2488 | |
| 2489 | // Sync clock duration with the resolved timeline each tick (catches async |
| 2490 | // rebinds, live data-duration edits). Never shrink while playing — transient |
| 2491 | // short reads cause reachedEnd() → playhead jumps to end (#1636). |
| 2492 | if (state.capturedTimeline) { |
| 2493 | const dur = getSafeTimelineDurationSeconds(state.capturedTimeline, 0); |
| 2494 | if (dur > 0 && (!clock.isPlaying() || dur >= clock.getDuration())) { |
| 2495 | clock.setDuration(dur); |
| 2496 | } |
| 2497 | } |
| 2498 | |
| 2499 | // Audio-master clock: three tiers of timing precision. |
| 2500 | // 1. WebAudio (AudioContext.currentTime): ~21µs, sample-accurate |
| 2501 | // 2. HTMLMediaElement (audio.currentTime): ~33ms, frame-accurate |
| 2502 | // 3. Monotonic (performance.now()): ~1ms, no audio coupling |
| 2503 | if (clock.isPlaying() && !state.mediaOutputMuted) { |
| 2504 | if ( |
| 2505 | !state.nativeMediaSyncDisabled && |
| 2506 | !state.webAudioMediaDisabled && |
| 2507 | webAudio.isActive() && |
| 2508 | webAudio.context |
| 2509 | ) { |
| 2510 | const webAudioTime = webAudio.getTime(); |
| 2511 | if (webAudioTime >= 0) { |
nothing calls this directly
no test coverage detected