Skip to content
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

feat(replay): Add support for playing mobile replays #66111

Merged
merged 31 commits into from
Mar 12, 2024

Conversation

billyvg
Copy link
Member

@billyvg billyvg commented Feb 29, 2024

This adds some initial support for playing mobile replays.

Internal e2e release

Future improvements

This adds some initial support for playing mobile replays.
@github-actions github-actions bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Feb 29, 2024
@billyvg billyvg self-assigned this Mar 1, 2024
@billyvg billyvg changed the title feat(replay): Add support for playing mobile replays [WIP] feat(replay): Add support for playing mobile replays Mar 6, 2024
@billyvg billyvg marked this pull request as ready for review March 6, 2024 17:10
@billyvg billyvg requested a review from a team as a code owner March 6, 2024 17:10
Comment on lines +232 to +234
const projectSlug = useProjectFromId({
project_id: replay?.getReplay().project_id,
})?.slug;
Copy link
Member Author

Choose a reason for hiding this comment

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

We need project slug here to fetch the video file

Comment on lines 397 to 421
// check if this is a video replay and if we can use the video replayer
if (isVideoReplay && videoAttachments && startTimestampMs) {
const inst = new VideoReplayer(videoAttachments, {
videoApiPrefix: `/api/0/projects/${
organization.slug
}/${projectSlug}/replays/${replay?.getReplay().id}/videos/`,
root,
start: startTimestampMs,
onFinished: setReplayFinished,
onLoaded: event => {
forceDimensions({
height: event.target.videoHeight,
width: event.target.videoWidth,
});
},
});
// `.current` is marked as readonly, but it's safe to set the value from
// inside a `useEffect` hook.
// See: https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables
// @ts-expect-error
replayerRef.current = inst;
applyInitialOffset();
return;
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Where the magic happens... VideoReplayer should have the same interface as Replayer

Comment on lines 266 to 271
const forceDimensions = useCallback(
(dimension: Dimensions) => {
setDimensions(dimension);
},
[setDimensions]
);
Copy link
Member

@ryan953 ryan953 Mar 8, 2024

Choose a reason for hiding this comment

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

This is sus because setDimensions is already the return from setState

the before code was no better

Comment on lines 423 to 433
getVideoAttachments = memoize(() =>
(
this._sortedRRWebEvents.filter(
event => event.type === EventType.Custom && event.data.tag === 'video'
) as VideoFrameEvent[]
).map(event => ({
duration: event.data.payload.duration,
id: event.data.payload.segmentId,
timestamp: event.timestamp,
}))
);
Copy link
Member

Choose a reason for hiding this comment

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

NIT: i'd put this kind of stuff into hydrateFrames.tsx and then endup passing in something like videoFrames next to the existing rrwebFrames array.

followup kind of thing, but it should make isVideoReplay slightly faster for web-based but long replays.

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated ddfea34

Comment on lines 22 to 28
for (let i = 0; i < this._callbacks.length; i++) {
if (this._time >= this._callbacks[i][0]) {
this._callbacks[i][1]();
// Remove from _callbacks
this._callbacks.splice(i, 1);
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Do we skip callbacks in the case where two in a row should run?

ie: the array is something like: [t=1, t=2, t=3] and the current _time=2
In this case we start with array.length==3 iterate to i=0 and the callback for t=1 will run because t=1 is less than _time=2
splice reduces the array to length==2, and i is set to 1, which points to t=3. t=2 is skipped :(
Then the array

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah yeah, I'll clean this up and add a test.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed here 4a1bb53

Co-authored-by: Scott Cooper <scttcper@gmail.com>
Co-authored-by: Ryan Albrecht <ryan.albrecht@sentry.io>
Copy link
Member

@ryan953 ryan953 left a comment

Choose a reason for hiding this comment

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

very cool stuff.

I got into some nits about types, because it seems easier to fix now than later when/if things get more complex. but they're just ideas.

);

it('returns first segment if target timestamp is before the first segment when there is only a single attachment', () => {
const segments2 = [
Copy link
Member

Choose a reason for hiding this comment

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

NIT: segments2

Copy link
Member Author

Choose a reason for hiding this comment

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

There's a segments in the outer block :/

attachments: VideoEvent[],
{root, start, videoApiPrefix, onFinished, onLoaded}: VideoReplayerOptions
) {
this._attachments = attachments.filter(attachment => attachment.duration > 0);
Copy link
Member

Choose a reason for hiding this comment

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

The filter could go into hydrateFrames too

Comment on lines 159 to 170
protected hideVideo(index: number | undefined): void {
const video = this.getVideo(index);

if (!video) {
return;
}

video.style.display = 'none';
}

protected showVideo(video: HTMLVideoElement | null): void {
if (!video) {
Copy link
Member

Choose a reason for hiding this comment

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

mix of undefined and null types in the return statements, and return types above. ... could cause trouble later.

return null;
}

return this._videos[index];
Copy link
Member

Choose a reason for hiding this comment

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

This could be .at(index), and/or suffixed with ?? null explicitly because types nulls and undefined are all over the place.


protected playVideo(video: HTMLVideoElement | null): Promise<void> | undefined {
if (!video) {
return undefined;
Copy link
Member

Choose a reason for hiding this comment

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

still on the types thing... I'd consider returning Promise.resolve(undefined) to cleanup the method return signature here, and in all the downstream places. the idea being that everything could say Promise<undefined>, for consistentcy, and to remove the | from return types.

@billyvg billyvg merged commit a12615b into master Mar 12, 2024
39 checks passed
@billyvg billyvg deleted the feat-replay-add-player-for-mobile-replays branch March 12, 2024 22:02
@github-actions github-actions bot locked and limited conversation to collaborators Mar 28, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Scope: Frontend Automatically applied to PRs that change frontend components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants