Skip to content
Merged
18 changes: 14 additions & 4 deletions static/app/components/replays/replayContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
ReplayPrefs,
} from 'sentry/components/replays/preferences/replayPreferences';
import useReplayHighlighting from 'sentry/components/replays/useReplayHighlighting';
import {VideoReplayerWithInteractions} from 'sentry/components/replays/videoReplayerWithInteractions';
import {trackAnalytics} from 'sentry/utils/analytics';
import clamp from 'sentry/utils/number/clamp';
import type useInitialOffsetMs from 'sentry/utils/replays/hooks/useInitialTimeOffsetMs';
Expand All @@ -26,7 +27,6 @@ import useProjectFromId from 'sentry/utils/useProjectFromId';
import {useUser} from 'sentry/utils/useUser';

import {CanvasReplayerPlugin} from './canvasReplayerPlugin';
import {VideoReplayer} from './videoReplayer';

type Dimensions = {height: number; width: number};
type RootElem = null | HTMLDivElement;
Expand Down Expand Up @@ -474,16 +474,19 @@ function ProviderNonMemo({
return null;
}

// check if this is a video replay and if we can use the video replayer
// check if this is a video replay and if we can use the video (wrapper) replayer
if (!isVideoReplay || !videoEvents || !startTimestampMs) {
return null;
}

const inst = new VideoReplayer(videoEvents, {
// This is a wrapper class that wraps both the VideoReplayer
// and the rrweb Replayer
const inst = new VideoReplayerWithInteractions({
// video specific
videoEvents,
videoApiPrefix: `/api/0/projects/${
organization.slug
}/${projectSlug}/replays/${replay?.getReplay().id}/videos/`,
root,
start: startTimestampMs,
onFinished: setReplayFinished,
onLoaded: event => {
Expand All @@ -501,6 +504,11 @@ function ProviderNonMemo({
},
clipWindow,
durationMs,
// rrweb specific
theme,
events: events ?? [],
// common to both
root,
});
// `.current` is marked as readonly, but it's safe to set the value from
// inside a `useEffect` hook.
Expand All @@ -520,6 +528,7 @@ function ProviderNonMemo({
isFetching,
isVideoReplay,
videoEvents,
events,
organization.slug,
projectSlug,
replay,
Expand All @@ -528,6 +537,7 @@ function ProviderNonMemo({
startTimeOffsetMs,
clipWindow,
durationMs,
theme,
]
);

Expand Down
14 changes: 14 additions & 0 deletions static/app/components/replays/replayPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,20 @@ const SentryPlayerRoot = styled(PlayerRoot)`
height: 10px;
}
}

/* Correctly positions the canvas for video replays and shows the purple "mousetails" */
&.video-replayer {
.replayer-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.replayer-wrapper > iframe {
opacity: 0;
}
}
`;

const Overlay = styled('div')`
Expand Down
2 changes: 1 addition & 1 deletion static/app/components/replays/videoReplayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ interface VideoReplayerOptions {
clipWindow?: ClipWindow;
}

interface VideoReplayerConfig {
export interface VideoReplayerConfig {
/**
* Not supported, only here to maintain compat w/ rrweb player
*/
Expand Down
152 changes: 152 additions & 0 deletions static/app/components/replays/videoReplayerWithInteractions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import type {Theme} from '@emotion/react';
import {type eventWithTime, Replayer} from '@sentry-internal/rrweb';

import {
VideoReplayer,
type VideoReplayerConfig,
} from 'sentry/components/replays/videoReplayer';
import type {ClipWindow, VideoEvent} from 'sentry/utils/replays/types';

type RootElem = HTMLDivElement | null;

interface VideoReplayerWithInteractionsOptions {
durationMs: number;
events: eventWithTime[];
onBuffer: (isBuffering: boolean) => void;
onFinished: () => void;
onLoaded: (event: any) => void;
root: RootElem;
start: number;
theme: Theme;
videoApiPrefix: string;
videoEvents: VideoEvent[];
clipWindow?: ClipWindow;
}

/**
* A wrapper replayer that wraps both VideoReplayer and the rrweb Replayer.
* We need both instances in order to render the video playback alongside gestures.
*/
export class VideoReplayerWithInteractions {
public config: VideoReplayerConfig = {
skipInactive: false,
speed: 1.0,
};
private videoReplayer: VideoReplayer;
private replayer: Replayer;

constructor({
videoEvents,
events,
root,
start,
videoApiPrefix,
onBuffer,
onFinished,
onLoaded,
clipWindow,
durationMs,
theme,
}: VideoReplayerWithInteractionsOptions) {
this.videoReplayer = new VideoReplayer(videoEvents, {
videoApiPrefix,
root,
start,
onFinished,
onLoaded,
onBuffer,
clipWindow,
durationMs,
});

root?.classList.add('video-replayer');

const eventsWithSnapshots: eventWithTime[] = [];
events.forEach(e => {
eventsWithSnapshots.push(e);
if (e.type === 4) {
// Create a mock full snapshot event, in order to render rrweb gestures properly
// Need to add one for every meta event we see
// The hardcoded data.node.id here should match the ID of the data being sent
// in the `positions` arrays
const fullSnapshotEvent = {
type: 2,
data: {
node: {
type: 0,
childNodes: [
{
type: 1,
name: 'html',
publicId: '',
systemId: '',
},
{
type: 2,
tagName: 'html',
attributes: {
lang: 'en',
},
childNodes: [],
},
],
id: 0,
},
},
timestamp: e.timestamp,
};
eventsWithSnapshots.push(fullSnapshotEvent);
}
});

this.replayer = new Replayer(eventsWithSnapshots, {
root: root as Element,
blockClass: 'sentry-block',
mouseTail: {
duration: 0.75 * 1000,
lineCap: 'round',
lineWidth: 2,
strokeStyle: theme.purple200,
},
plugins: [],
skipInactive: false,
speed: this.config.speed,
});
}

public destroy() {
this.videoReplayer.destroy();
this.replayer.destroy();
}

/**
* Returns the current video time, using the video's external timer.
*/
public getCurrentTime() {
return this.videoReplayer.getCurrentTime();
}

/**
* Play both the rrweb and video player.
*/
public play(videoOffsetMs: number) {
this.videoReplayer.play(videoOffsetMs);
this.replayer.play(videoOffsetMs);
}

/**
* Pause both the rrweb and video player.
*/
public pause(videoOffsetMs: number) {
this.videoReplayer.pause(videoOffsetMs);
this.replayer.pause(videoOffsetMs);
}

/**
* Equivalent to rrweb's `setConfig()`, but here we only support the `speed` configuration.
*/
public setConfig(config: Partial<VideoReplayerConfig>): void {
this.videoReplayer.setConfig(config);
this.replayer.setConfig(config);
}
}