()
| 1655 | }; |
| 1656 | |
| 1657 | const syncMediaForCurrentState = () => { |
| 1658 | const resolveMediaCompositionContext = (element: HTMLVideoElement | HTMLAudioElement) => { |
| 1659 | const compositionRoot = element.closest("[data-composition-id]"); |
| 1660 | const inheritedStart = compositionRoot ? resolveStartForElement(compositionRoot, 0) : null; |
| 1661 | // Media sync follows the authored host window, matching visibility for |
| 1662 | // authored composition hosts. Live child timeline duration only fills in |
| 1663 | // when no authored timing exists, so seeks clamp against host clip timing. |
| 1664 | const inheritedDuration = compositionRoot |
| 1665 | ? resolveDurationForElement(compositionRoot, { includeAuthoredTimingAttrs: true }) |
| 1666 | : null; |
| 1667 | return { compositionRoot, inheritedStart, inheritedDuration }; |
| 1668 | }; |
| 1669 | const cache = refreshRuntimeMediaCache({ |
| 1670 | shouldIncludeElement: (element) => |
| 1671 | element.hasAttribute("data-start") || |
| 1672 | Boolean(resolveMediaCompositionContext(element).compositionRoot), |
| 1673 | resolveStartSeconds: (element) => { |
| 1674 | const context = resolveMediaCompositionContext( |
| 1675 | element as HTMLVideoElement | HTMLAudioElement, |
| 1676 | ); |
| 1677 | return resolveMediaStartSeconds(element, context.inheritedStart ?? 0); |
| 1678 | }, |
| 1679 | resolveDurationSeconds: (element) => { |
| 1680 | const context = resolveMediaCompositionContext(element); |
| 1681 | const start = resolveMediaStartSeconds(element, context.inheritedStart ?? 0); |
| 1682 | const mediaStart = |
| 1683 | Number.parseFloat(element.dataset.playbackStart ?? element.dataset.mediaStart ?? "0") || |
| 1684 | 0; |
| 1685 | const hostRemaining = |
| 1686 | context.inheritedStart != null && |
| 1687 | context.inheritedDuration != null && |
| 1688 | context.inheritedDuration > 0 |
| 1689 | ? Math.max(0, context.inheritedStart + context.inheritedDuration - start) |
| 1690 | : null; |
| 1691 | const sourceDuration = |
| 1692 | Number.isFinite(element.duration) && element.duration > mediaStart |
| 1693 | ? Math.max(0, element.duration - mediaStart) |
| 1694 | : null; |
| 1695 | // The element's own data-duration is an explicit clip-length trim |
| 1696 | // (the studio writes it when you drag the clip edge). It must bound |
| 1697 | // playback so a trimmed track stops at its edge instead of running on |
| 1698 | // to the source-file or host-composition end. Absent → no cap (an |
| 1699 | // untrimmed clip plays its natural source length). |
| 1700 | const ownDuration = Number.parseFloat(element.dataset.duration ?? ""); |
| 1701 | const explicitDuration = |
| 1702 | Number.isFinite(ownDuration) && ownDuration > 0 ? ownDuration : null; |
| 1703 | const candidates = [sourceDuration, hostRemaining, explicitDuration].filter( |
| 1704 | (value): value is number => value != null, |
| 1705 | ); |
| 1706 | return candidates.length > 0 ? Math.min(...candidates) : null; |
| 1707 | }, |
| 1708 | }); |
| 1709 | // Attach probed volume keyframes to clips so syncRuntimeMedia can use the |
| 1710 | // same envelope the renderer uses instead of tracking GSAP-change diffs. |
| 1711 | for (const clip of cache.mediaClips) { |
| 1712 | const kf = volumeKeyframeCache.get(clip.el as HTMLMediaElement); |
| 1713 | if (kf) clip.volumeKeyframes = kf; |
| 1714 | } |
no test coverage detected