(params: {
clips: RuntimeMediaClip[];
timeSeconds: number;
playing: boolean;
playbackRate: number;
/** Force-mute every element (parent-frame proxy owns all audio). Asserted per
* tick so sub-composition media added mid-playback inherits the silence. */
outputMuted?: boolean;
/**
* User's explicit mute preference (set via `onSetMuted`). Symmetric to
* `outputMuted` — also asserted per tick — so a sub-composition that
* activates after the user mutes doesn't briefly play at author volume
* before the next bridge message lands.
*/
userMuted?: boolean;
/**
* User's volume preference (0–1, set via `onSetVolume`). Multiplied with the
* per-clip author volume so `data-volume="0.5"` at user volume 0.8 yields 0.4.
*/
userVolume?: number;
/**
* Invoked at most once when a media element's `play()` promise rejects with
* `NotAllowedError`. The caller is expected to latch and post a single
* outbound message; further invocations are suppressed by the caller.
*/
onAutoplayBlocked?: () => void;
onElementVolume?: (el: HTMLMediaElement, volume: number) => void;
/** Is THIS element owned by the Web Audio transport? Owned → mute it (transport
* plays it); not owned → leave audible (HTMLMedia fallback). Per-element, not a
* global flag, so a not-yet-claimed track isn't muted by other tracks. */
isWebAudioOwned?: (el: HTMLMediaElement) => boolean;
forceSync?: boolean;
})
| 130 | |
| 131 | // fallow-ignore-next-line complexity |
| 132 | export function syncRuntimeMedia(params: { |
| 133 | clips: RuntimeMediaClip[]; |
| 134 | timeSeconds: number; |
| 135 | playing: boolean; |
| 136 | playbackRate: number; |
| 137 | /** Force-mute every element (parent-frame proxy owns all audio). Asserted per |
| 138 | * tick so sub-composition media added mid-playback inherits the silence. */ |
| 139 | outputMuted?: boolean; |
| 140 | /** |
| 141 | * User's explicit mute preference (set via `onSetMuted`). Symmetric to |
| 142 | * `outputMuted` — also asserted per tick — so a sub-composition that |
| 143 | * activates after the user mutes doesn't briefly play at author volume |
| 144 | * before the next bridge message lands. |
| 145 | */ |
| 146 | userMuted?: boolean; |
| 147 | /** |
| 148 | * User's volume preference (0–1, set via `onSetVolume`). Multiplied with the |
| 149 | * per-clip author volume so `data-volume="0.5"` at user volume 0.8 yields 0.4. |
| 150 | */ |
| 151 | userVolume?: number; |
| 152 | /** |
| 153 | * Invoked at most once when a media element's `play()` promise rejects with |
| 154 | * `NotAllowedError`. The caller is expected to latch and post a single |
| 155 | * outbound message; further invocations are suppressed by the caller. |
| 156 | */ |
| 157 | onAutoplayBlocked?: () => void; |
| 158 | onElementVolume?: (el: HTMLMediaElement, volume: number) => void; |
| 159 | /** Is THIS element owned by the Web Audio transport? Owned → mute it (transport |
| 160 | * plays it); not owned → leave audible (HTMLMedia fallback). Per-element, not a |
| 161 | * global flag, so a not-yet-claimed track isn't muted by other tracks. */ |
| 162 | isWebAudioOwned?: (el: HTMLMediaElement) => boolean; |
| 163 | forceSync?: boolean; |
| 164 | }): void { |
| 165 | const forceMuteAll = !!(params.outputMuted || params.userMuted); |
| 166 | for (const clip of params.clips) { |
| 167 | const { el } = clip; |
| 168 | if (!el.isConnected) continue; |
| 169 | let relTime = (params.timeSeconds - clip.start) * clip.playbackRate + clip.mediaStart; |
| 170 | // An ended non-loop element has played its file to natural completion. |
| 171 | // Don't restart it — if the authored duration extends past the file's |
| 172 | // actual length, the element sits silently until the composition ends. |
| 173 | // (el.ended resets to false when the user scrubs back, so seeks work.) |
| 174 | const isActive = |
| 175 | params.timeSeconds >= clip.start && |
| 176 | params.timeSeconds <= clip.end && |
| 177 | relTime >= 0 && |
| 178 | (!el.ended || clip.loop); |
| 179 | if (isActive) { |
| 180 | // Loop wrapping: when media reaches end, restart from mediaStart |
| 181 | if (clip.loop && clip.sourceDuration != null && clip.sourceDuration > 0) { |
| 182 | const loopLength = clip.sourceDuration - clip.mediaStart; |
| 183 | if (loopLength > 0 && relTime >= clip.sourceDuration) { |
| 184 | relTime = clip.mediaStart + ((relTime - clip.mediaStart) % loopLength); |
| 185 | } |
| 186 | } |
| 187 | const userVol = clampVolume(params.userVolume ?? 1); |
| 188 | const fallbackAuthorVolume = clampVolume(clip.volume ?? 1); |
| 189 | const previousRuntimeVolume = lastRuntimeAppliedVolume.get(el); |
no test coverage detected