Skip to content

Commit f7ab9ee

Browse files
Merge pull request #2817 from yashranaway/fix/share-timestamp-seek
fix(web): keep timestamp seeks while the video is still loading
2 parents fc3f7c0 + a361b3c commit f7ab9ee

2 files changed

Lines changed: 51 additions & 10 deletions

File tree

‎apps/web/__tests__/unit/share-stage-identity.test.ts‎

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ vi.mock("next/image", async () => {
6565
});
6666

6767
vi.mock("next/navigation", () => ({
68-
useSearchParams: () => new URLSearchParams(),
68+
useSearchParams: () => new URLSearchParams(window.location.search),
6969
useRouter: () => ({ refresh: () => undefined }),
7070
}));
7171

@@ -163,12 +163,13 @@ const findButton = (container: HTMLElement, label: string) =>
163163
const stageOf = (video: Element | null) =>
164164
video?.parentElement?.parentElement?.parentElement ?? null;
165165

166-
describe("Share view toggle", () => {
166+
describe("Share", () => {
167167
beforeAll(() => {
168168
actEnvironment.IS_REACT_ACT_ENVIRONMENT = true;
169169
});
170170

171171
afterEach(() => {
172+
vi.useRealTimers();
172173
document.body.replaceChildren();
173174
window.localStorage.clear();
174175
window.history.replaceState(window.history.state, "", "/s/video-id");
@@ -178,6 +179,41 @@ describe("Share view toggle", () => {
178179
delete actEnvironment.IS_REACT_ACT_ENVIRONMENT;
179180
});
180181

182+
it.each(["loadedmetadata", "canplay"])(
183+
"seeks a timestamp link when %s arrives after five seconds",
184+
async (readyEvent) => {
185+
vi.useFakeTimers();
186+
window.history.replaceState(null, "", "/s/video-id?t=42");
187+
const container = document.createElement("div");
188+
document.body.append(container);
189+
const root = createRoot(container);
190+
191+
try {
192+
await act(async () => {
193+
root.render(createElement(Share, createProps()));
194+
});
195+
196+
const video = container.querySelector("video");
197+
if (!video) throw new Error("Share video did not mount");
198+
expect(video.currentTime).toBe(0);
199+
200+
await act(async () => {
201+
vi.advanceTimersByTime(5000);
202+
Object.defineProperty(video, "duration", { value: 60 });
203+
video.dispatchEvent(new Event(readyEvent));
204+
});
205+
206+
expect(video.currentTime).toBe(42);
207+
video.currentTime = 50;
208+
video.dispatchEvent(new Event("loadedmetadata"));
209+
video.dispatchEvent(new Event("canplay"));
210+
expect(video.currentTime).toBe(50);
211+
} finally {
212+
await act(async () => root.unmount());
213+
}
214+
},
215+
);
216+
181217
it("keeps the sidebar open after mounting with an old collapsed preference", async () => {
182218
window.localStorage.setItem("cap_share_rail_collapsed", "true");
183219
const container = document.createElement("div");

‎apps/web/app/s/[videoId]/Share.tsx‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,10 @@ export const Share = ({
482482
);
483483
}, [data.source.type, searchParams]);
484484

485+
const cancelPendingSeek = useRef<(() => void) | null>(null);
486+
485487
const handleSeek = useCallback((time: number) => {
488+
cancelPendingSeek.current?.();
486489
const v =
487490
playerRef.current ??
488491
(document.querySelector("video") as HTMLVideoElement | null);
@@ -504,19 +507,21 @@ export const Share = ({
504507
seekOnce(time);
505508
return;
506509
}
507-
let timeoutId: ReturnType<typeof setTimeout> | null = null;
508-
const handleReady = () => {
509-
seekOnce(time);
510+
// A timeline seek before metadata moves currentTime; don't undo it.
511+
const requestedFrom = v.currentTime;
512+
const cancel = () => {
510513
v.removeEventListener("canplay", handleReady);
511514
v.removeEventListener("loadedmetadata", handleReady);
512-
if (timeoutId) clearTimeout(timeoutId);
515+
if (cancelPendingSeek.current === cancel)
516+
cancelPendingSeek.current = null;
517+
};
518+
const handleReady = () => {
519+
cancel();
520+
if (v.currentTime === requestedFrom) seekOnce(time);
513521
};
514522
v.addEventListener("canplay", handleReady, { once: true });
515523
v.addEventListener("loadedmetadata", handleReady, { once: true });
516-
timeoutId = setTimeout(() => {
517-
v.removeEventListener("canplay", handleReady);
518-
v.removeEventListener("loadedmetadata", handleReady);
519-
}, 3000);
524+
cancelPendingSeek.current = cancel;
520525
}, []);
521526

522527
useEffect(() => {

0 commit comments

Comments
 (0)