| 159 | |
| 160 | /** Point both recordings at session-time `time`; play/pause to match. */ |
| 161 | const apply = async (time: number, play: boolean) => { |
| 162 | const act = acts[actAt(time)]; |
| 163 | if (!act) return; |
| 164 | const video = videoRef.current; |
| 165 | const cast = castPlayer.current; |
| 166 | if (act.window === "browser") { |
| 167 | cast?.pause(); |
| 168 | if (video) { |
| 169 | const target = Math.min(mediaTime("browser", time), (videoDuration ?? Infinity) - 0.05); |
| 170 | if (Math.abs(video.currentTime - target) > 0.25) video.currentTime = target; |
| 171 | if (play) await video.play().catch(() => {}); |
| 172 | else video.pause(); |
| 173 | } |
| 174 | } else { |
| 175 | videoRef.current?.pause(); |
| 176 | if (cast) { |
| 177 | const target = Math.min(mediaTime("terminal", time), (castDuration ?? Infinity) - 0.05); |
| 178 | const current = cast.getCurrentTime(); |
| 179 | const now = typeof current === "number" ? current : await current; |
| 180 | if (Math.abs(now - target) > 0.25) await cast.seek(target); |
| 181 | if (play) cast.play(); |
| 182 | else cast.pause(); |
| 183 | } |
| 184 | } |
| 185 | }; |
| 186 | |
| 187 | // The tick: session time is read FROM the active recording (it is the |
| 188 | // clock); crossing an act boundary swaps recordings. |