Skip to content

fix(replays): sync replay timer with shown duration #69074

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions static/app/components/replays/replayContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ function ProviderNonMemo({
setVideoBuffering(buffering);
},
clipWindow,
duration: durationMs,
});
// `.current` is marked as readonly, but it's safe to set the value from
// inside a `useEffect` hook.
Expand All @@ -521,6 +522,7 @@ function ProviderNonMemo({
startTimestampMs,
startTimeOffsetMs,
clipWindow,
durationMs,
]
);

Expand Down
9 changes: 9 additions & 0 deletions static/app/components/replays/videoReplayer.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ describe('VideoReplayer - no starting gap', () => {
onFinished: jest.fn(),
onLoaded: jest.fn(),
onBuffer: jest.fn(),
duration: 40,
});
// @ts-expect-error private
expect(inst._currentIndex).toEqual(0);
Expand All @@ -112,6 +113,7 @@ describe('VideoReplayer - no starting gap', () => {
onFinished: jest.fn(),
onLoaded: jest.fn(),
onBuffer: jest.fn(),
duration: 40,
});
const playPromise = inst.play(18100);
// @ts-expect-error private
Expand Down Expand Up @@ -140,6 +142,7 @@ describe('VideoReplayer - no starting gap', () => {
onFinished: jest.fn(),
onLoaded: jest.fn(),
onBuffer: jest.fn(),
duration: 40,
});
const playPromise = inst.play(50000);
// 15000 -> 20000 is a gap, so player should start playing @ index 3, from
Expand All @@ -162,6 +165,7 @@ describe('VideoReplayer - no starting gap', () => {
onFinished: jest.fn(),
onLoaded: jest.fn(),
onBuffer: jest.fn(),
duration: 40,
});
const playPromise = inst.play(0);
jest.advanceTimersByTime(2500);
Expand All @@ -181,6 +185,7 @@ describe('VideoReplayer - no starting gap', () => {
onFinished: jest.fn(),
onLoaded: jest.fn(),
onBuffer: jest.fn(),
duration: 50,
});
// play at segment 7
const playPromise = inst.play(45_003);
Expand Down Expand Up @@ -215,6 +220,7 @@ describe('VideoReplayer - no starting gap', () => {
onFinished: jest.fn(),
onLoaded: jest.fn(),
onBuffer: jest.fn(),
duration: 60,
});
// play at segment 7
const playPromise = inst.play(45_003);
Expand Down Expand Up @@ -288,6 +294,7 @@ describe('VideoReplayer - with starting gap', () => {
onFinished: jest.fn(),
onLoaded: jest.fn(),
onBuffer: jest.fn(),
duration: 40,
});
// @ts-expect-error private
expect(inst._currentIndex).toEqual(0);
Expand All @@ -311,6 +318,7 @@ describe('VideoReplayer - with starting gap', () => {
onFinished: jest.fn(),
onLoaded: jest.fn(),
onBuffer: jest.fn(),
duration: 40,
});
const playPromise = inst.play(18100);
// @ts-expect-error private
Expand Down Expand Up @@ -339,6 +347,7 @@ describe('VideoReplayer - with starting gap', () => {
onFinished: jest.fn(),
onLoaded: jest.fn(),
onBuffer: jest.fn(),
duration: 40,
});
const playPromise = inst.play(50000);
// 15000 -> 20000 is a gap, so player should start playing @ index 3, from
Expand Down
20 changes: 17 additions & 3 deletions static/app/components/replays/videoReplayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface OffsetOptions {
}

interface VideoReplayerOptions {
duration: number;
onBuffer: (isBuffering: boolean) => void;
onFinished: () => void;
onLoaded: (event: any) => void;
Expand Down Expand Up @@ -56,6 +57,7 @@ export class VideoReplayer {
private _videos: Map<any, HTMLVideoElement>;
private _videoApiPrefix: string;
private _clipDuration: number | undefined;
private _duration: number;
public config: VideoReplayerConfig = {
skipInactive: false,
speed: 1.0,
Expand All @@ -73,6 +75,7 @@ export class VideoReplayer {
onFinished,
onLoaded,
clipWindow,
duration,
}: VideoReplayerOptions
) {
this._attachments = attachments;
Expand All @@ -86,6 +89,7 @@ export class VideoReplayer {
};
this._videos = new Map<any, HTMLVideoElement>();
this._clipDuration = undefined;
this._duration = duration;

this.wrapper = document.createElement('div');
if (root) {
Expand Down Expand Up @@ -297,7 +301,12 @@ export class VideoReplayer {

// No more segments
if (nextIndex >= this._attachments.length) {
this.stopReplay();
// If we're at the end of a segment, but there's a gap
// at the end, force the replay to play until the end duration
// rather than stopping right away.
this._timer.addNotificationAtTime(this._duration, () => {
this.stopReplay();
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a chance of a race condition here where by the time this notification is added, the timer is already passed duration and will continue on?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one relates to the case where the replay stops early because we've hit a gap/the end of the segment -- the old code here was just this.stopReplay(). i think we might be okay here because we don't want to stop early, but rather keep playing until the end, which means we haven't hit duration yet

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking about the case where we do not have a gap, and we want to stop the replay:

  • segment ends -> handleSegmentEnd is called
  • no next segment so this code block is called
  • add notification for duration (e.g. 20)
  • due to external factors (data, hardware, etc), the current time is > 20
  • ?? does the callback get fired in this case?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ohhh i see what you're saying! let me add another check that stops the replay if the time is over

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something like

    // No more segments
    if (nextIndex >= this._attachments.length) {
      if (this.getCurrentTime() < this._durationMs) {
        // If we're at the end of a segment, but there's a gap
        // at the end, force the replay to play until the end duration
        // rather than stopping right away.
        this._timer.addNotificationAtTime(this._durationMs, () => {
          this.stopReplay();
        });
        return;
      }
      this.stopReplay();
    }

return;
}

Expand Down Expand Up @@ -598,8 +607,13 @@ export class VideoReplayer {
const loadedSegmentIndex = await this.loadSegmentAtTime(videoOffsetMs);

if (loadedSegmentIndex === undefined) {
// TODO: this shouldn't happen, loadSegment should load the previous
// segment until it's time to start the next segment
// If we end up here, we seeked into a gap
// at the end of the replay.
// This tells the timer to stop at the specified duration
// and prevents the timer from running infinitely.
this._timer.addNotificationAtTime(this._duration, () => {
this.stopReplay();
});
return Promise.resolve();
}

Expand Down
Loading