Browse Source

feat(replay): Let users load a replay at a specific time offset (#34170)

Users, or links, can add a time-offset to the query string: `?t=<number>` and the video replay will start at the specified time instead of at the start of the video. So `?t=30` would have the video start at the 30second mark.

If someone puts a time-offset that's longer than the video, say `?t=9999999`, then the video will start from `t=0`. This is how youtube behaves.

Fixes #33886
Ryan Albrecht 2 years ago
parent
commit
bb0c2eadaf

+ 21 - 5
static/app/components/replays/replayContext.tsx

@@ -115,6 +115,15 @@ const ReplayPlayerContext = React.createContext<ReplayPlayerContextProps>({
 type Props = {
   children: React.ReactNode;
   events: eventWithTime[];
+
+  /**
+   * Time, in seconds, when the video should start
+   */
+  initialTimeOffset?: number;
+
+  /**
+   * Override return fields for testing
+   */
   value?: Partial<ReplayPlayerContextProps>;
 };
 
@@ -124,7 +133,7 @@ function useCurrentTime(callback: () => number) {
   return currentTime;
 }
 
-export function Provider({children, events, value = {}}: Props) {
+export function Provider({children, events, initialTimeOffset = 0, value = {}}: Props) {
   const theme = useTheme();
   const oldEvents = usePrevious(events);
   const replayerRef = useRef<Replayer>(null);
@@ -216,12 +225,15 @@ export function Provider({children, events, value = {}}: Props) {
   );
 
   const setCurrentTime = useCallback(
-    (time: number) => {
+    (requestedTimeMs: number) => {
       const replayer = replayerRef.current;
       if (!replayer) {
         return;
       }
 
+      const maxTimeMs = replayerRef.current?.getMetaData().totalTime;
+      const time = requestedTimeMs > maxTimeMs ? 0 : requestedTimeMs;
+
       // Sometimes rrweb doesn't get to the exact target time, as long as it has
       // changed away from the previous time then we can hide then buffering message.
       setBufferTime({target: time, previous: getCurrentTime()});
@@ -232,9 +244,6 @@ export function Provider({children, events, value = {}}: Props) {
         window.clearTimeout(playTimer.current);
       }
 
-      // TODO: it might be nice to always just pause() here
-      // Why? People can drag the scrobber, or click 'back 10s' and then be in a
-      // paused state to inspect things.
       if (isPlaying) {
         playTimer.current = window.setTimeout(() => replayer.play(time), 0);
         setIsPlaying(true);
@@ -292,6 +301,13 @@ export function Provider({children, events, value = {}}: Props) {
     setIsSkippingInactive(skip);
   }, []);
 
+  // Only on pageload: set the initial playback timestamp
+  useEffect(() => {
+    if (initialTimeOffset && events && replayerRef.current) {
+      setCurrentTime(initialTimeOffset * 1000);
+    }
+  }, [events, replayerRef.current]); // eslint-disable-line react-hooks/exhaustive-deps
+
   const currentPlayerTime = useCurrentTime(getCurrentTime);
 
   const [isBuffering, currentTime] =

+ 5 - 1
static/app/views/replays/details.tsx

@@ -26,6 +26,10 @@ function ReplayDetails() {
     params: {eventSlug, orgId},
   } = useRouteContext();
 
+  const {
+    t: initialTimeOffset, // Time, in seconds, where the video should start
+  } = location.query;
+
   const {
     breadcrumbEntry,
     event,
@@ -84,7 +88,7 @@ function ReplayDetails() {
   }
 
   return (
-    <ReplayContextProvider events={rrwebEvents}>
+    <ReplayContextProvider events={rrwebEvents} initialTimeOffset={initialTimeOffset}>
       <DetailLayout
         event={event}
         orgId={orgId}