Full overhawl of video & audio playback to make it more complete (#1432)
* Add support for capturing media attributes in rrweb-snapshot * Add loop to mediaInteractionParam * Add support for loop in RRMediaElement * Add support for recording loop attribute on media elements * Update video playback and fix bugs * Update cross-origin iframe media attributes and player state
This commit is contained in:
5
.changeset/cool-grapes-hug.md
Normal file
5
.changeset/cool-grapes-hug.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'rrdom': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Support `loop` in `RRMediaElement`
|
||||||
5
.changeset/dirty-rules-dress.md
Normal file
5
.changeset/dirty-rules-dress.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'rrweb-snapshot': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Video and Audio elements now also capture `playbackRate`, `muted`, `loop`, `volume`.
|
||||||
5
.changeset/mighty-ads-worry.md
Normal file
5
.changeset/mighty-ads-worry.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'rrweb': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Full overhawl of `video` and `audio` element playback. More robust and fixes lots of bugs related to pausing/playing/skipping/muting/playbackRate etc.
|
||||||
5
.changeset/silver-pots-sit.md
Normal file
5
.changeset/silver-pots-sit.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@rrweb/types': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add `loop` to `mediaInteractionParam`
|
||||||
5
.changeset/smart-geckos-cover.md
Normal file
5
.changeset/smart-geckos-cover.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'rrweb': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Record `loop` on `<audio>` & `<video>` elements.
|
||||||
@@ -250,6 +250,8 @@ function diffAfterUpdatingChildren(
|
|||||||
oldMediaElement.currentTime = newMediaRRElement.currentTime;
|
oldMediaElement.currentTime = newMediaRRElement.currentTime;
|
||||||
if (newMediaRRElement.playbackRate !== undefined)
|
if (newMediaRRElement.playbackRate !== undefined)
|
||||||
oldMediaElement.playbackRate = newMediaRRElement.playbackRate;
|
oldMediaElement.playbackRate = newMediaRRElement.playbackRate;
|
||||||
|
if (newMediaRRElement.loop !== undefined)
|
||||||
|
oldMediaElement.loop = newMediaRRElement.loop;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'CANVAS': {
|
case 'CANVAS': {
|
||||||
|
|||||||
@@ -563,6 +563,7 @@ export function BaseRRMediaElementImpl<
|
|||||||
public paused?: boolean;
|
public paused?: boolean;
|
||||||
public muted?: boolean;
|
public muted?: boolean;
|
||||||
public playbackRate?: number;
|
public playbackRate?: number;
|
||||||
|
public loop?: boolean;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
attachShadow(_init: ShadowRootInit): IRRElement {
|
attachShadow(_init: ShadowRootInit): IRRElement {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -280,6 +280,7 @@ describe('diff algorithm for rrdom', () => {
|
|||||||
rrMedia.muted = true;
|
rrMedia.muted = true;
|
||||||
rrMedia.paused = false;
|
rrMedia.paused = false;
|
||||||
rrMedia.playbackRate = 0.5;
|
rrMedia.playbackRate = 0.5;
|
||||||
|
rrMedia.loop = false;
|
||||||
|
|
||||||
diff(element, rrMedia, replayer);
|
diff(element, rrMedia, replayer);
|
||||||
expect(element.volume).toEqual(0.5);
|
expect(element.volume).toEqual(0.5);
|
||||||
@@ -287,6 +288,7 @@ describe('diff algorithm for rrdom', () => {
|
|||||||
expect(element.muted).toEqual(true);
|
expect(element.muted).toEqual(true);
|
||||||
expect(element.paused).toEqual(false);
|
expect(element.paused).toEqual(false);
|
||||||
expect(element.playbackRate).toEqual(0.5);
|
expect(element.playbackRate).toEqual(0.5);
|
||||||
|
expect(element.loop).toEqual(false);
|
||||||
|
|
||||||
rrMedia.paused = true;
|
rrMedia.paused = true;
|
||||||
diff(element, rrMedia, replayer);
|
diff(element, rrMedia, replayer);
|
||||||
|
|||||||
@@ -1079,6 +1079,7 @@ describe('Basic RRDocument implementation', () => {
|
|||||||
expect(node.paused).toBeUndefined();
|
expect(node.paused).toBeUndefined();
|
||||||
expect(node.muted).toBeUndefined();
|
expect(node.muted).toBeUndefined();
|
||||||
expect(node.playbackRate).toBeUndefined();
|
expect(node.playbackRate).toBeUndefined();
|
||||||
|
expect(node.loop).toBeUndefined();
|
||||||
expect(node.play).toBeDefined();
|
expect(node.play).toBeDefined();
|
||||||
expect(node.pause).toBeDefined();
|
expect(node.pause).toBeDefined();
|
||||||
expect(node.toString()).toEqual('VIDEO ');
|
expect(node.toString()).toEqual('VIDEO ');
|
||||||
|
|||||||
@@ -342,6 +342,17 @@ function buildNode(
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
} else if (
|
||||||
|
name === 'rr_mediaPlaybackRate' &&
|
||||||
|
typeof value === 'number'
|
||||||
|
) {
|
||||||
|
(node as HTMLMediaElement).playbackRate = value;
|
||||||
|
} else if (name === 'rr_mediaMuted' && typeof value === 'boolean') {
|
||||||
|
(node as HTMLMediaElement).muted = value;
|
||||||
|
} else if (name === 'rr_mediaLoop' && typeof value === 'boolean') {
|
||||||
|
(node as HTMLMediaElement).loop = value;
|
||||||
|
} else if (name === 'rr_mediaVolume' && typeof value === 'number') {
|
||||||
|
(node as HTMLMediaElement).volume = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
ICanvas,
|
ICanvas,
|
||||||
elementNode,
|
elementNode,
|
||||||
serializedElementNodeWithId,
|
serializedElementNodeWithId,
|
||||||
|
type mediaAttributes,
|
||||||
} from './types';
|
} from './types';
|
||||||
import {
|
import {
|
||||||
Mirror,
|
Mirror,
|
||||||
@@ -761,10 +762,15 @@ function serializeElementNode(
|
|||||||
}
|
}
|
||||||
// media elements
|
// media elements
|
||||||
if (tagName === 'audio' || tagName === 'video') {
|
if (tagName === 'audio' || tagName === 'video') {
|
||||||
attributes.rr_mediaState = (n as HTMLMediaElement).paused
|
const mediaAttributes = attributes as mediaAttributes;
|
||||||
|
mediaAttributes.rr_mediaState = (n as HTMLMediaElement).paused
|
||||||
? 'paused'
|
? 'paused'
|
||||||
: 'played';
|
: 'played';
|
||||||
attributes.rr_mediaCurrentTime = (n as HTMLMediaElement).currentTime;
|
mediaAttributes.rr_mediaCurrentTime = (n as HTMLMediaElement).currentTime;
|
||||||
|
mediaAttributes.rr_mediaPlaybackRate = (n as HTMLMediaElement).playbackRate;
|
||||||
|
mediaAttributes.rr_mediaMuted = (n as HTMLMediaElement).muted;
|
||||||
|
mediaAttributes.rr_mediaLoop = (n as HTMLMediaElement).loop;
|
||||||
|
mediaAttributes.rr_mediaVolume = (n as HTMLMediaElement).volume;
|
||||||
}
|
}
|
||||||
// Scroll
|
// Scroll
|
||||||
if (!newlyAddedElement) {
|
if (!newlyAddedElement) {
|
||||||
|
|||||||
@@ -82,6 +82,27 @@ export type tagMap = {
|
|||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type mediaAttributes = {
|
||||||
|
rr_mediaState: 'played' | 'paused';
|
||||||
|
rr_mediaCurrentTime: number;
|
||||||
|
/**
|
||||||
|
* for backwards compatibility this is optional but should always be set
|
||||||
|
*/
|
||||||
|
rr_mediaPlaybackRate?: number;
|
||||||
|
/**
|
||||||
|
* for backwards compatibility this is optional but should always be set
|
||||||
|
*/
|
||||||
|
rr_mediaMuted?: boolean;
|
||||||
|
/**
|
||||||
|
* for backwards compatibility this is optional but should always be set
|
||||||
|
*/
|
||||||
|
rr_mediaLoop?: boolean;
|
||||||
|
/**
|
||||||
|
* for backwards compatibility this is optional but should always be set
|
||||||
|
*/
|
||||||
|
rr_mediaVolume?: number;
|
||||||
|
};
|
||||||
|
|
||||||
// @deprecated
|
// @deprecated
|
||||||
export interface INode extends Node {
|
export interface INode extends Node {
|
||||||
__sn: serializedNodeWithId;
|
__sn: serializedNodeWithId;
|
||||||
|
|||||||
@@ -1042,7 +1042,7 @@ function initMediaInteractionObserver({
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { currentTime, volume, muted, playbackRate } =
|
const { currentTime, volume, muted, playbackRate, loop } =
|
||||||
target as HTMLMediaElement;
|
target as HTMLMediaElement;
|
||||||
mediaInteractionCb({
|
mediaInteractionCb({
|
||||||
type,
|
type,
|
||||||
@@ -1051,6 +1051,7 @@ function initMediaInteractionObserver({
|
|||||||
volume,
|
volume,
|
||||||
muted,
|
muted,
|
||||||
playbackRate,
|
playbackRate,
|
||||||
|
loop,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
sampling.media || 500,
|
sampling.media || 500,
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ import {
|
|||||||
ReplayerEvents,
|
ReplayerEvents,
|
||||||
Handler,
|
Handler,
|
||||||
Emitter,
|
Emitter,
|
||||||
MediaInteractions,
|
|
||||||
metaEvent,
|
metaEvent,
|
||||||
mutationData,
|
mutationData,
|
||||||
scrollData,
|
scrollData,
|
||||||
@@ -81,6 +80,7 @@ import getInjectStyleRules from './styles/inject-style';
|
|||||||
import './styles/style.css';
|
import './styles/style.css';
|
||||||
import canvasMutation from './canvas';
|
import canvasMutation from './canvas';
|
||||||
import { deserializeArg } from './canvas/deserialize-args';
|
import { deserializeArg } from './canvas/deserialize-args';
|
||||||
|
import { MediaManager } from './media';
|
||||||
|
|
||||||
const SKIP_TIME_INTERVAL = 5 * 1000;
|
const SKIP_TIME_INTERVAL = 5 * 1000;
|
||||||
|
|
||||||
@@ -142,6 +142,9 @@ export class Replayer {
|
|||||||
// Used to track StyleSheetObjects adopted on multiple document hosts.
|
// Used to track StyleSheetObjects adopted on multiple document hosts.
|
||||||
private styleMirror: StyleSheetMirror = new StyleSheetMirror();
|
private styleMirror: StyleSheetMirror = new StyleSheetMirror();
|
||||||
|
|
||||||
|
// Used to track video & audio elements, and keep them in sync with general playback.
|
||||||
|
private mediaManager: MediaManager;
|
||||||
|
|
||||||
private firstFullSnapshot: eventWithTime | true | null = null;
|
private firstFullSnapshot: eventWithTime | true | null = null;
|
||||||
|
|
||||||
private newDocumentQueue: addedNodeMutation[] = [];
|
private newDocumentQueue: addedNodeMutation[] = [];
|
||||||
@@ -324,6 +327,7 @@ export class Replayer {
|
|||||||
this.firstFullSnapshot = null;
|
this.firstFullSnapshot = null;
|
||||||
this.mirror.reset();
|
this.mirror.reset();
|
||||||
this.styleMirror.reset();
|
this.styleMirror.reset();
|
||||||
|
this.mediaManager.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
const timer = new Timer([], {
|
const timer = new Timer([], {
|
||||||
@@ -366,6 +370,13 @@ export class Replayer {
|
|||||||
speed: state,
|
speed: state,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
this.mediaManager = new MediaManager({
|
||||||
|
warn: this.warn.bind(this),
|
||||||
|
service: this.service,
|
||||||
|
speedService: this.speedService,
|
||||||
|
emitter: this.emitter,
|
||||||
|
getCurrentTime: this.getCurrentTime.bind(this),
|
||||||
|
});
|
||||||
|
|
||||||
// rebuild first full snapshot as the poster of the player
|
// rebuild first full snapshot as the poster of the player
|
||||||
// maybe we can cache it for performance optimization
|
// maybe we can cache it for performance optimization
|
||||||
@@ -464,10 +475,16 @@ export class Replayer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the actual time offset the player is at now compared to the first event.
|
||||||
|
*/
|
||||||
public getCurrentTime(): number {
|
public getCurrentTime(): number {
|
||||||
return this.timer.timeOffset + this.getTimeOffset();
|
return this.timer.timeOffset + this.getTimeOffset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the time offset the player is at now compared to the first event, but without regard for the timer.
|
||||||
|
*/
|
||||||
public getTimeOffset(): number {
|
public getTimeOffset(): number {
|
||||||
const { baselineTime, events } = this.service.state.context;
|
const { baselineTime, events } = this.service.state.context;
|
||||||
return baselineTime - events[0].timestamp;
|
return baselineTime - events[0].timestamp;
|
||||||
@@ -527,6 +544,9 @@ export class Replayer {
|
|||||||
*/
|
*/
|
||||||
public destroy() {
|
public destroy() {
|
||||||
this.pause();
|
this.pause();
|
||||||
|
this.mirror.reset();
|
||||||
|
this.styleMirror.reset();
|
||||||
|
this.mediaManager.reset();
|
||||||
this.config.root.removeChild(this.wrapper);
|
this.config.root.removeChild(this.wrapper);
|
||||||
this.emitter.emit(ReplayerEvents.Destroy);
|
this.emitter.emit(ReplayerEvents.Destroy);
|
||||||
}
|
}
|
||||||
@@ -667,9 +687,10 @@ export class Replayer {
|
|||||||
// Timer (requestAnimationFrame) can be faster than setTimeout(..., 1)
|
// Timer (requestAnimationFrame) can be faster than setTimeout(..., 1)
|
||||||
this.firstFullSnapshot = true;
|
this.firstFullSnapshot = true;
|
||||||
}
|
}
|
||||||
|
this.mediaManager.reset();
|
||||||
|
this.styleMirror.reset();
|
||||||
this.rebuildFullSnapshot(event, isSync);
|
this.rebuildFullSnapshot(event, isSync);
|
||||||
this.iframe.contentWindow?.scrollTo(event.data.initialOffset);
|
this.iframe.contentWindow?.scrollTo(event.data.initialOffset);
|
||||||
this.styleMirror.reset();
|
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case EventType.IncrementalSnapshot:
|
case EventType.IncrementalSnapshot:
|
||||||
@@ -778,6 +799,14 @@ export class Replayer {
|
|||||||
const collected: AppendedIframe[] = [];
|
const collected: AppendedIframe[] = [];
|
||||||
const afterAppend = (builtNode: Node, id: number) => {
|
const afterAppend = (builtNode: Node, id: number) => {
|
||||||
this.collectIframeAndAttachDocument(collected, builtNode);
|
this.collectIframeAndAttachDocument(collected, builtNode);
|
||||||
|
if (this.mediaManager.isSupportedMediaElement(builtNode)) {
|
||||||
|
const { events } = this.service.state.context;
|
||||||
|
this.mediaManager.addMediaElements(
|
||||||
|
builtNode,
|
||||||
|
event.timestamp - events[0].timestamp,
|
||||||
|
this.mirror,
|
||||||
|
);
|
||||||
|
}
|
||||||
for (const plugin of this.config.plugins || []) {
|
for (const plugin of this.config.plugins || []) {
|
||||||
if (plugin.onBuild)
|
if (plugin.onBuild)
|
||||||
plugin.onBuild(builtNode, {
|
plugin.onBuild(builtNode, {
|
||||||
@@ -1261,35 +1290,14 @@ export class Replayer {
|
|||||||
return this.debugNodeNotFound(d, d.id);
|
return this.debugNodeNotFound(d, d.id);
|
||||||
}
|
}
|
||||||
const mediaEl = target as HTMLMediaElement | RRMediaElement;
|
const mediaEl = target as HTMLMediaElement | RRMediaElement;
|
||||||
try {
|
const { events } = this.service.state.context;
|
||||||
if (d.currentTime !== undefined) {
|
|
||||||
mediaEl.currentTime = d.currentTime;
|
this.mediaManager.mediaMutation({
|
||||||
}
|
target: mediaEl,
|
||||||
if (d.volume !== undefined) {
|
timeOffset: e.timestamp - events[0].timestamp,
|
||||||
mediaEl.volume = d.volume;
|
mutation: d,
|
||||||
}
|
});
|
||||||
if (d.muted !== undefined) {
|
|
||||||
mediaEl.muted = d.muted;
|
|
||||||
}
|
|
||||||
if (d.type === MediaInteractions.Pause) {
|
|
||||||
mediaEl.pause();
|
|
||||||
}
|
|
||||||
if (d.type === MediaInteractions.Play) {
|
|
||||||
// remove listener for 'canplay' event because play() is async and returns a promise
|
|
||||||
// i.e. media will evntualy start to play when data is loaded
|
|
||||||
// 'canplay' event fires even when currentTime attribute changes which may lead to
|
|
||||||
// unexpeted behavior
|
|
||||||
void mediaEl.play();
|
|
||||||
}
|
|
||||||
if (d.type === MediaInteractions.RateChange) {
|
|
||||||
mediaEl.playbackRate = d.playbackRate;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.warn(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/restrict-template-expressions
|
|
||||||
`Failed to replay media interactions: ${error.message || error}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case IncrementalSource.StyleSheetRule:
|
case IncrementalSource.StyleSheetRule:
|
||||||
@@ -1366,6 +1374,11 @@ export class Replayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the mutation to the virtual dom or the real dom.
|
||||||
|
* @param d - The mutation data.
|
||||||
|
* @param isSync - Whether the mutation should be applied synchronously (while fast-forwarding).
|
||||||
|
*/
|
||||||
private applyMutation(d: mutationData, isSync: boolean) {
|
private applyMutation(d: mutationData, isSync: boolean) {
|
||||||
// Only apply virtual dom optimization if the fast-forward process has node mutation. Because the cost of creating a virtual dom tree and executing the diff algorithm is usually higher than directly applying other kind of events.
|
// Only apply virtual dom optimization if the fast-forward process has node mutation. Because the cost of creating a virtual dom tree and executing the diff algorithm is usually higher than directly applying other kind of events.
|
||||||
if (this.config.useVirtualDom && !this.usingVirtualDom && isSync) {
|
if (this.config.useVirtualDom && !this.usingVirtualDom && isSync) {
|
||||||
|
|||||||
294
packages/rrweb/src/replay/media/index.ts
Normal file
294
packages/rrweb/src/replay/media/index.ts
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
import { Emitter, MediaInteractions, ReplayerEvents } from '@rrweb/types';
|
||||||
|
import type { RRMediaElement } from 'rrdom/es';
|
||||||
|
import type { createPlayerService, createSpeedService } from '../machine';
|
||||||
|
import type { Mirror, mediaAttributes } from 'rrweb-snapshot';
|
||||||
|
import type { mediaInteractionData } from '@rrweb/types';
|
||||||
|
|
||||||
|
type MediaState = {
|
||||||
|
isPlaying: boolean;
|
||||||
|
currentTimeAtLastInteraction: number;
|
||||||
|
lastInteractionTimeOffset: number;
|
||||||
|
playbackRate: number;
|
||||||
|
loop: boolean;
|
||||||
|
volume: number;
|
||||||
|
muted: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class MediaManager {
|
||||||
|
private mediaMap: Map<HTMLMediaElement | RRMediaElement, MediaState> =
|
||||||
|
new Map();
|
||||||
|
private warn: (...args: Parameters<typeof console.warn>) => void;
|
||||||
|
private service: ReturnType<typeof createPlayerService>;
|
||||||
|
private speedService: ReturnType<typeof createSpeedService>;
|
||||||
|
private emitter: Emitter;
|
||||||
|
private getCurrentTime: () => number;
|
||||||
|
private metadataCallbackMap: WeakMap<
|
||||||
|
HTMLMediaElement | RRMediaElement,
|
||||||
|
() => void
|
||||||
|
> = new Map();
|
||||||
|
|
||||||
|
constructor(options: {
|
||||||
|
warn: (...args: Parameters<typeof console.warn>) => void;
|
||||||
|
service: ReturnType<typeof createPlayerService>;
|
||||||
|
speedService: ReturnType<typeof createSpeedService>;
|
||||||
|
getCurrentTime: () => number;
|
||||||
|
emitter: Emitter;
|
||||||
|
}) {
|
||||||
|
this.warn = options.warn;
|
||||||
|
this.service = options.service;
|
||||||
|
this.speedService = options.speedService;
|
||||||
|
this.emitter = options.emitter;
|
||||||
|
this.getCurrentTime = options.getCurrentTime;
|
||||||
|
|
||||||
|
this.emitter.on(ReplayerEvents.Start, this.start.bind(this));
|
||||||
|
this.emitter.on(ReplayerEvents.SkipStart, this.start.bind(this));
|
||||||
|
this.emitter.on(ReplayerEvents.Pause, this.pause.bind(this));
|
||||||
|
this.emitter.on(ReplayerEvents.Finish, this.pause.bind(this));
|
||||||
|
this.speedService.subscribe(() => {
|
||||||
|
this.syncAllMediaElements();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private syncAllMediaElements(options = { pause: false }) {
|
||||||
|
this.mediaMap.forEach((mediaState, target) => {
|
||||||
|
this.syncTargetWithState(target);
|
||||||
|
if (options.pause) {
|
||||||
|
target.pause();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private start() {
|
||||||
|
this.syncAllMediaElements();
|
||||||
|
}
|
||||||
|
|
||||||
|
private pause() {
|
||||||
|
this.syncAllMediaElements({ pause: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
private seekTo({
|
||||||
|
time,
|
||||||
|
target,
|
||||||
|
mediaState,
|
||||||
|
}: {
|
||||||
|
time: number;
|
||||||
|
target: HTMLMediaElement | RRMediaElement;
|
||||||
|
mediaState: MediaState;
|
||||||
|
}) {
|
||||||
|
if (mediaState.isPlaying) {
|
||||||
|
const differenceBetweenCurrentTimeAndMediaMutationTimestamp =
|
||||||
|
time - mediaState.lastInteractionTimeOffset;
|
||||||
|
const mediaPlaybackOffset =
|
||||||
|
(differenceBetweenCurrentTimeAndMediaMutationTimestamp / 1000) *
|
||||||
|
mediaState.playbackRate;
|
||||||
|
|
||||||
|
const duration = 'duration' in target && target.duration;
|
||||||
|
|
||||||
|
// Video hasn't loaded yet, wait for metadata
|
||||||
|
if (Number.isNaN(duration)) {
|
||||||
|
this.waitForMetadata(target);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let seekToTime =
|
||||||
|
mediaState.currentTimeAtLastInteraction + mediaPlaybackOffset;
|
||||||
|
|
||||||
|
if (
|
||||||
|
target.loop &&
|
||||||
|
// RRMediaElement doesn't have a duration property
|
||||||
|
duration !== false
|
||||||
|
) {
|
||||||
|
seekToTime = seekToTime % duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.currentTime = seekToTime;
|
||||||
|
} else {
|
||||||
|
target.pause();
|
||||||
|
target.currentTime = mediaState.currentTimeAtLastInteraction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private waitForMetadata(target: HTMLMediaElement | RRMediaElement) {
|
||||||
|
if (this.metadataCallbackMap.has(target)) return;
|
||||||
|
if (!('addEventListener' in target)) return;
|
||||||
|
|
||||||
|
const onLoadedMetadata = () => {
|
||||||
|
this.metadataCallbackMap.delete(target);
|
||||||
|
const mediaState = this.mediaMap.get(target);
|
||||||
|
if (!mediaState) return;
|
||||||
|
this.seekTo({
|
||||||
|
time: this.getCurrentTime(),
|
||||||
|
target,
|
||||||
|
mediaState,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.metadataCallbackMap.set(target, onLoadedMetadata);
|
||||||
|
target.addEventListener('loadedmetadata', onLoadedMetadata, {
|
||||||
|
once: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMediaStateFromMutation({
|
||||||
|
target,
|
||||||
|
timeOffset,
|
||||||
|
mutation,
|
||||||
|
}: {
|
||||||
|
target: HTMLMediaElement | RRMediaElement;
|
||||||
|
timeOffset: number;
|
||||||
|
mutation: mediaInteractionData;
|
||||||
|
}): MediaState {
|
||||||
|
const lastState = this.mediaMap.get(target);
|
||||||
|
const { type, playbackRate, currentTime, muted, volume, loop } = mutation;
|
||||||
|
|
||||||
|
const isPlaying =
|
||||||
|
type === MediaInteractions.Play ||
|
||||||
|
(type !== MediaInteractions.Pause &&
|
||||||
|
(lastState?.isPlaying || target.getAttribute('autoplay') !== null));
|
||||||
|
|
||||||
|
const mediaState: MediaState = {
|
||||||
|
isPlaying,
|
||||||
|
currentTimeAtLastInteraction:
|
||||||
|
currentTime ?? lastState?.currentTimeAtLastInteraction ?? 0,
|
||||||
|
lastInteractionTimeOffset: timeOffset,
|
||||||
|
playbackRate: playbackRate ?? lastState?.playbackRate ?? 1,
|
||||||
|
volume: volume ?? lastState?.volume ?? 1,
|
||||||
|
muted: muted ?? lastState?.muted ?? target.getAttribute('muted') === null,
|
||||||
|
loop: loop ?? lastState?.loop ?? target.getAttribute('loop') === null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return mediaState;
|
||||||
|
}
|
||||||
|
|
||||||
|
private syncTargetWithState(target: HTMLMediaElement | RRMediaElement) {
|
||||||
|
const mediaState = this.mediaMap.get(target);
|
||||||
|
if (!mediaState) return;
|
||||||
|
const { muted, loop, volume, isPlaying } = mediaState;
|
||||||
|
const playerIsPaused = this.service.state.matches('paused');
|
||||||
|
const playbackRate =
|
||||||
|
mediaState.playbackRate * this.speedService.state.context.timer.speed;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.seekTo({
|
||||||
|
time: this.getCurrentTime(),
|
||||||
|
target,
|
||||||
|
mediaState,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (target.volume !== volume) {
|
||||||
|
target.volume = volume;
|
||||||
|
}
|
||||||
|
target.muted = muted;
|
||||||
|
target.loop = loop;
|
||||||
|
|
||||||
|
if (target.playbackRate !== playbackRate) {
|
||||||
|
// Avoid setting playbackRate when it's already the same
|
||||||
|
// Safari drops frames whenever playbackRate is set, even if it's the same
|
||||||
|
target.playbackRate = playbackRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlaying && !playerIsPaused) {
|
||||||
|
// remove listener for 'canplay' event because play() is async and returns a promise
|
||||||
|
// i.e. media will eventually start to play when data is loaded
|
||||||
|
// 'canplay' event fires even when currentTime attribute changes which may lead to
|
||||||
|
// unexpected behavior
|
||||||
|
void target.play();
|
||||||
|
} else {
|
||||||
|
target.pause();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.warn(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/restrict-template-expressions
|
||||||
|
`Failed to replay media interactions: ${error.message || error}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public addMediaElements(node: Node, timeOffset: number, mirror: Mirror) {
|
||||||
|
if (!['AUDIO', 'VIDEO'].includes(node.nodeName)) return;
|
||||||
|
const target = node as HTMLMediaElement;
|
||||||
|
const serializedNode = mirror.getMeta(target);
|
||||||
|
if (!serializedNode || !('attributes' in serializedNode)) return;
|
||||||
|
const playerIsPaused = this.service.state.matches('paused');
|
||||||
|
const mediaAttributes = serializedNode.attributes as
|
||||||
|
| mediaAttributes
|
||||||
|
| Record<string, never>;
|
||||||
|
|
||||||
|
let isPlaying = false;
|
||||||
|
if (mediaAttributes.rr_mediaState) {
|
||||||
|
isPlaying = mediaAttributes.rr_mediaState === 'played';
|
||||||
|
} else {
|
||||||
|
isPlaying = target.getAttribute('autoplay') !== null;
|
||||||
|
}
|
||||||
|
if (isPlaying && playerIsPaused) target.pause();
|
||||||
|
|
||||||
|
let playbackRate = 1;
|
||||||
|
if (typeof mediaAttributes.rr_mediaPlaybackRate === 'number') {
|
||||||
|
playbackRate = mediaAttributes.rr_mediaPlaybackRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
let muted = false;
|
||||||
|
if (typeof mediaAttributes.rr_mediaMuted === 'boolean') {
|
||||||
|
muted = mediaAttributes.rr_mediaMuted;
|
||||||
|
} else {
|
||||||
|
muted = target.getAttribute('muted') !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let loop = false;
|
||||||
|
if (typeof mediaAttributes.rr_mediaLoop === 'boolean') {
|
||||||
|
loop = mediaAttributes.rr_mediaLoop;
|
||||||
|
} else {
|
||||||
|
loop = target.getAttribute('loop') !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let volume = 1;
|
||||||
|
if (typeof mediaAttributes.rr_mediaVolume === 'number') {
|
||||||
|
volume = mediaAttributes.rr_mediaVolume;
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentTimeAtLastInteraction = 0;
|
||||||
|
if (typeof mediaAttributes.rr_mediaCurrentTime === 'number') {
|
||||||
|
currentTimeAtLastInteraction = mediaAttributes.rr_mediaCurrentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mediaMap.set(target, {
|
||||||
|
isPlaying,
|
||||||
|
currentTimeAtLastInteraction,
|
||||||
|
lastInteractionTimeOffset: timeOffset,
|
||||||
|
playbackRate,
|
||||||
|
volume,
|
||||||
|
muted,
|
||||||
|
loop,
|
||||||
|
});
|
||||||
|
this.syncTargetWithState(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
public mediaMutation({
|
||||||
|
target,
|
||||||
|
timeOffset,
|
||||||
|
mutation,
|
||||||
|
}: {
|
||||||
|
target: HTMLMediaElement | RRMediaElement;
|
||||||
|
timeOffset: number;
|
||||||
|
mutation: mediaInteractionData;
|
||||||
|
}) {
|
||||||
|
this.mediaMap.set(
|
||||||
|
target,
|
||||||
|
this.getMediaStateFromMutation({
|
||||||
|
target,
|
||||||
|
timeOffset,
|
||||||
|
mutation,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.syncTargetWithState(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isSupportedMediaElement(node: Node): node is HTMLMediaElement {
|
||||||
|
return ['AUDIO', 'VIDEO'].includes(node.nodeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public reset() {
|
||||||
|
this.mediaMap.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
waitForRAF,
|
waitForRAF,
|
||||||
generateRecordSnippet,
|
generateRecordSnippet,
|
||||||
ISuite,
|
ISuite,
|
||||||
|
hideMouseAnimation,
|
||||||
|
fakeGoto,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import type { recordOptions } from '../../src/types';
|
import type { recordOptions } from '../../src/types';
|
||||||
import type { eventWithTime } from '@rrweb/types';
|
import type { eventWithTime } from '@rrweb/types';
|
||||||
@@ -59,29 +61,6 @@ describe('e2e webgl', () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fakeGoto = async (p: puppeteer.Page, url: string) => {
|
|
||||||
const intercept = async (request: puppeteer.HTTPRequest) => {
|
|
||||||
await request.respond({
|
|
||||||
status: 200,
|
|
||||||
contentType: 'text/html',
|
|
||||||
body: ' ', // non-empty string or page will load indefinitely
|
|
||||||
});
|
|
||||||
};
|
|
||||||
await p.setRequestInterception(true);
|
|
||||||
p.on('request', intercept);
|
|
||||||
await p.goto(url);
|
|
||||||
p.off('request', intercept);
|
|
||||||
await p.setRequestInterception(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hideMouseAnimation = async (p: puppeteer.Page) => {
|
|
||||||
await p.addStyleTag({
|
|
||||||
content: `.replayer-mouse-tail{display: none !important;}
|
|
||||||
html, body { margin: 0; padding: 0; }
|
|
||||||
iframe { border: none; }`,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
it('will record and replay a webgl square', async () => {
|
it('will record and replay a webgl square', async () => {
|
||||||
page = await browser.newPage();
|
page = await browser.newPage();
|
||||||
await fakeGoto(page, `${serverURL}/html/canvas-webgl-square.html`);
|
await fakeGoto(page, `${serverURL}/html/canvas-webgl-square.html`);
|
||||||
|
|||||||
550
packages/rrweb/test/events/video-playback-on-full-snapshot.ts
Normal file
550
packages/rrweb/test/events/video-playback-on-full-snapshot.ts
Normal file
@@ -0,0 +1,550 @@
|
|||||||
|
import type { eventWithTime } from '@rrweb/types';
|
||||||
|
|
||||||
|
const events: eventWithTime[] = [
|
||||||
|
{ type: 0, data: {}, timestamp: 1900000001 },
|
||||||
|
{ type: 1, data: {}, timestamp: 1900000132 },
|
||||||
|
{
|
||||||
|
type: 4,
|
||||||
|
data: {
|
||||||
|
href: 'http://127.0.0.1:5500/test/html/video.html',
|
||||||
|
width: 1600,
|
||||||
|
height: 900,
|
||||||
|
},
|
||||||
|
timestamp: 1900000132,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
data: {
|
||||||
|
node: {
|
||||||
|
type: 0,
|
||||||
|
childNodes: [
|
||||||
|
{ type: 1, name: 'html', publicId: '', systemId: '', id: 2 },
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'html',
|
||||||
|
attributes: { lang: 'en' },
|
||||||
|
childNodes: [
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'head',
|
||||||
|
attributes: {},
|
||||||
|
childNodes: [
|
||||||
|
{ type: 3, textContent: '\n ', id: 5 },
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'meta',
|
||||||
|
attributes: { charset: 'UTF-8' },
|
||||||
|
childNodes: [],
|
||||||
|
id: 6,
|
||||||
|
},
|
||||||
|
{ type: 3, textContent: '\n ', id: 7 },
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'meta',
|
||||||
|
attributes: {
|
||||||
|
'http-equiv': 'X-UA-Compatible',
|
||||||
|
content: 'IE=edge',
|
||||||
|
},
|
||||||
|
childNodes: [],
|
||||||
|
id: 8,
|
||||||
|
},
|
||||||
|
{ type: 3, textContent: '\n ', id: 9 },
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'meta',
|
||||||
|
attributes: {
|
||||||
|
name: 'viewport',
|
||||||
|
content: 'width=device-width, initial-scale=1.0',
|
||||||
|
},
|
||||||
|
childNodes: [],
|
||||||
|
id: 10,
|
||||||
|
},
|
||||||
|
{ type: 3, textContent: '\n ', id: 11 },
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'title',
|
||||||
|
attributes: {},
|
||||||
|
childNodes: [{ type: 3, textContent: 'Video', id: 13 }],
|
||||||
|
id: 12,
|
||||||
|
},
|
||||||
|
{ type: 3, textContent: '\n ', id: 14 },
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'script',
|
||||||
|
attributes: { type: 'text/javascript' },
|
||||||
|
childNodes: [
|
||||||
|
{ type: 3, textContent: 'SCRIPT_PLACEHOLDER', id: 16 },
|
||||||
|
{ type: 3, textContent: 'SCRIPT_PLACEHOLDER', id: 17 },
|
||||||
|
{ type: 3, textContent: 'SCRIPT_PLACEHOLDER', id: 18 },
|
||||||
|
{ type: 3, textContent: 'SCRIPT_PLACEHOLDER', id: 19 },
|
||||||
|
{ type: 3, textContent: 'SCRIPT_PLACEHOLDER', id: 20 },
|
||||||
|
],
|
||||||
|
id: 15,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 4,
|
||||||
|
},
|
||||||
|
{ type: 3, textContent: '\n ', id: 21 },
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'body',
|
||||||
|
attributes: {},
|
||||||
|
childNodes: [
|
||||||
|
{ type: 3, textContent: '\n ', id: 23 },
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'h1',
|
||||||
|
attributes: {},
|
||||||
|
childNodes: [
|
||||||
|
{ type: 3, textContent: 'Big Buck Bunny', id: 25 },
|
||||||
|
],
|
||||||
|
id: 24,
|
||||||
|
},
|
||||||
|
{ type: 3, textContent: '\n ', id: 26 },
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'video',
|
||||||
|
attributes: {
|
||||||
|
muted: '',
|
||||||
|
controls: '',
|
||||||
|
loop: '',
|
||||||
|
rr_mediaState: 'played',
|
||||||
|
rr_mediaCurrentTime: 0,
|
||||||
|
rr_mediaPlaybackRate: 1,
|
||||||
|
rr_mediaMuted: true,
|
||||||
|
rr_mediaVolume: 1,
|
||||||
|
rr_mediaLoop: true,
|
||||||
|
},
|
||||||
|
childNodes: [
|
||||||
|
{ type: 3, textContent: '\n ', id: 28 },
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'source',
|
||||||
|
attributes: {
|
||||||
|
src: '/html/assets/bunny-video.webm',
|
||||||
|
type: 'video/webm',
|
||||||
|
},
|
||||||
|
childNodes: [],
|
||||||
|
id: 29,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
textContent:
|
||||||
|
'\n Your browser does not support the video element.\n ',
|
||||||
|
id: 30,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 27,
|
||||||
|
},
|
||||||
|
{ type: 3, textContent: '\n ', id: 31 },
|
||||||
|
],
|
||||||
|
id: 22,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
initialOffset: { left: 0, top: 0 },
|
||||||
|
},
|
||||||
|
timestamp: 1900000136,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: { source: 1, positions: [{ x: 955, y: 869, id: 3, timeOffset: 0 }] },
|
||||||
|
timestamp: 1900006165,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 902, y: 823, id: 3, timeOffset: -451 },
|
||||||
|
{ x: 840, y: 765, id: 3, timeOffset: -400 },
|
||||||
|
{ x: 796, y: 724, id: 3, timeOffset: -350 },
|
||||||
|
{ x: 716, y: 673, id: 3, timeOffset: -267 },
|
||||||
|
{ x: 660, y: 645, id: 3, timeOffset: -217 },
|
||||||
|
{ x: 554, y: 593, id: 3, timeOffset: -167 },
|
||||||
|
{ x: 466, y: 518, id: 3, timeOffset: -101 },
|
||||||
|
{ x: 433, y: 413, id: 27, timeOffset: -51 },
|
||||||
|
{ x: 432, y: 316, id: 27, timeOffset: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900006665,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 444, y: 229, id: 27, timeOffset: -435 },
|
||||||
|
{ x: 444, y: 210, id: 27, timeOffset: -385 },
|
||||||
|
{ x: 444, y: 209, id: 27, timeOffset: -335 },
|
||||||
|
{ x: 445, y: 214, id: 27, timeOffset: -235 },
|
||||||
|
{ x: 460, y: 246, id: 27, timeOffset: -185 },
|
||||||
|
{ x: 476, y: 273, id: 27, timeOffset: -134 },
|
||||||
|
{ x: 482, y: 281, id: 27, timeOffset: -84 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900007166,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: { source: 1, positions: [{ x: 483, y: 281, id: 27, timeOffset: 0 }] },
|
||||||
|
timestamp: 1900007814,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 483, y: 281, id: 27, timeOffset: -417 },
|
||||||
|
{ x: 484, y: 282, id: 27, timeOffset: -218 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900008315,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: { source: 1, positions: [{ x: 484, y: 281, id: 27, timeOffset: 0 }] },
|
||||||
|
timestamp: 1900010165,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 484, y: 281, id: 27, timeOffset: -322 },
|
||||||
|
{ x: 485, y: 281, id: 27, timeOffset: -256 },
|
||||||
|
{ x: 491, y: 282, id: 27, timeOffset: -205 },
|
||||||
|
{ x: 515, y: 283, id: 27, timeOffset: -156 },
|
||||||
|
{ x: 534, y: 285, id: 27, timeOffset: -106 },
|
||||||
|
{ x: 562, y: 291, id: 27, timeOffset: -56 },
|
||||||
|
{ x: 575, y: 295, id: 27, timeOffset: -6 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900010670,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: { source: 1, positions: [{ x: 576, y: 296, id: 27, timeOffset: 0 }] },
|
||||||
|
timestamp: 1900012714,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 576, y: 298, id: 27, timeOffset: -451 },
|
||||||
|
{ x: 577, y: 301, id: 27, timeOffset: -400 },
|
||||||
|
{ x: 578, y: 305, id: 27, timeOffset: -334 },
|
||||||
|
{ x: 580, y: 313, id: 27, timeOffset: -284 },
|
||||||
|
{ x: 582, y: 332, id: 27, timeOffset: -234 },
|
||||||
|
{ x: 585, y: 346, id: 27, timeOffset: -184 },
|
||||||
|
{ x: 587, y: 353, id: 27, timeOffset: -133 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900013215,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [{ x: 587, y: 354, id: 27, timeOffset: -253 }],
|
||||||
|
},
|
||||||
|
timestamp: 1900013717,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: { source: 1, positions: [{ x: 587, y: 355, id: 27, timeOffset: 0 }] },
|
||||||
|
timestamp: 1900014364,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 588, y: 358, id: 27, timeOffset: -451 },
|
||||||
|
{ x: 589, y: 368, id: 27, timeOffset: -400 },
|
||||||
|
{ x: 592, y: 386, id: 27, timeOffset: -334 },
|
||||||
|
{ x: 596, y: 402, id: 27, timeOffset: -266 },
|
||||||
|
{ x: 597, y: 410, id: 27, timeOffset: -202 },
|
||||||
|
{ x: 595, y: 415, id: 27, timeOffset: -152 },
|
||||||
|
{ x: 591, y: 417, id: 27, timeOffset: -101 },
|
||||||
|
{ x: 585, y: 418, id: 27, timeOffset: -51 },
|
||||||
|
{ x: 579, y: 418, id: 27, timeOffset: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900014865,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 562, y: 420, id: 27, timeOffset: -433 },
|
||||||
|
{ x: 547, y: 422, id: 27, timeOffset: -367 },
|
||||||
|
{ x: 538, y: 423, id: 27, timeOffset: -317 },
|
||||||
|
{ x: 525, y: 423, id: 27, timeOffset: -251 },
|
||||||
|
{ x: 509, y: 423, id: 27, timeOffset: -201 },
|
||||||
|
{ x: 488, y: 422, id: 27, timeOffset: -151 },
|
||||||
|
{ x: 440, y: 421, id: 27, timeOffset: -101 },
|
||||||
|
{ x: 373, y: 419, id: 27, timeOffset: -51 },
|
||||||
|
{ x: 296, y: 415, id: 27, timeOffset: -1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900015365,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 225, y: 409, id: 27, timeOffset: -439 },
|
||||||
|
{ x: 183, y: 408, id: 27, timeOffset: -389 },
|
||||||
|
{ x: 138, y: 407, id: 27, timeOffset: -340 },
|
||||||
|
{ x: 98, y: 406, id: 27, timeOffset: -290 },
|
||||||
|
{ x: 79, y: 406, id: 27, timeOffset: -238 },
|
||||||
|
{ x: 60, y: 406, id: 27, timeOffset: -173 },
|
||||||
|
{ x: 53, y: 406, id: 27, timeOffset: -122 },
|
||||||
|
{ x: 39, y: 405, id: 27, timeOffset: -55 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900015870,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 34, y: 403, id: 27, timeOffset: -493 },
|
||||||
|
{ x: 31, y: 401, id: 27, timeOffset: -442 },
|
||||||
|
{ x: 29, y: 399, id: 27, timeOffset: -375 },
|
||||||
|
{ x: 28, y: 399, id: 27, timeOffset: -325 },
|
||||||
|
{ x: 28, y: 397, id: 27, timeOffset: -259 },
|
||||||
|
{ x: 28, y: 394, id: 27, timeOffset: -209 },
|
||||||
|
{ x: 28, y: 394, id: 27, timeOffset: -159 },
|
||||||
|
{ x: 28, y: 393, id: 27, timeOffset: -109 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900016373,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: { source: 1, positions: [{ x: 29, y: 393, id: 27, timeOffset: 0 }] },
|
||||||
|
timestamp: 1900018598,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 30, y: 391, id: 27, timeOffset: -433 },
|
||||||
|
{ x: 31, y: 392, id: 27, timeOffset: -251 },
|
||||||
|
{ x: 30, y: 393, id: 27, timeOffset: -201 },
|
||||||
|
{ x: 30, y: 394, id: 27, timeOffset: -151 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900019098,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 30, y: 394, id: 27, timeOffset: -457 },
|
||||||
|
{ x: 30, y: 394, id: 27, timeOffset: -391 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900019605,
|
||||||
|
},
|
||||||
|
{ type: 3, data: { source: 2, type: 5, id: 27 }, timestamp: 1900020571 },
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: { source: 1, positions: [{ x: 32, y: 394, id: 27, timeOffset: 0 }] },
|
||||||
|
timestamp: 1900021531,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 48, y: 394, id: 27, timeOffset: -449 },
|
||||||
|
{ x: 286, y: 413, id: 27, timeOffset: -384 },
|
||||||
|
{ x: 418, y: 419, id: 27, timeOffset: -334 },
|
||||||
|
{ x: 474, y: 419, id: 27, timeOffset: -284 },
|
||||||
|
{ x: 482, y: 418, id: 27, timeOffset: -233 },
|
||||||
|
{ x: 482, y: 417, id: 27, timeOffset: -167 },
|
||||||
|
{ x: 477, y: 416, id: 27, timeOffset: -116 },
|
||||||
|
{ x: 439, y: 414, id: 27, timeOffset: -51 },
|
||||||
|
{ x: 402, y: 412, id: 27, timeOffset: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900022031,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 376, y: 413, id: 27, timeOffset: -450 },
|
||||||
|
{ x: 366, y: 414, id: 27, timeOffset: -400 },
|
||||||
|
{ x: 353, y: 416, id: 27, timeOffset: -334 },
|
||||||
|
{ x: 346, y: 417, id: 27, timeOffset: -283 },
|
||||||
|
{ x: 339, y: 419, id: 27, timeOffset: -233 },
|
||||||
|
{ x: 322, y: 422, id: 27, timeOffset: -167 },
|
||||||
|
{ x: 311, y: 422, id: 27, timeOffset: -117 },
|
||||||
|
{ x: 308, y: 422, id: 27, timeOffset: -51 },
|
||||||
|
{ x: 311, y: 420, id: 27, timeOffset: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900022531,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 315, y: 419, id: 27, timeOffset: -448 },
|
||||||
|
{ x: 316, y: 418, id: 27, timeOffset: -397 },
|
||||||
|
{ x: 317, y: 417, id: 27, timeOffset: -347 },
|
||||||
|
{ x: 317, y: 417, id: 27, timeOffset: -281 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900023045,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 318, y: 418, id: 27, timeOffset: -466 },
|
||||||
|
{ x: 326, y: 439, id: 27, timeOffset: -416 },
|
||||||
|
{ x: 333, y: 473, id: 3, timeOffset: -365 },
|
||||||
|
{ x: 334, y: 484, id: 3, timeOffset: -300 },
|
||||||
|
{ x: 334, y: 485, id: 3, timeOffset: -50 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900023547,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 333, y: 485, id: 3, timeOffset: -483 },
|
||||||
|
{ x: 321, y: 481, id: 3, timeOffset: -433 },
|
||||||
|
{ x: 265, y: 460, id: 3, timeOffset: -383 },
|
||||||
|
{ x: 203, y: 433, id: 27, timeOffset: -332 },
|
||||||
|
{ x: 135, y: 402, id: 27, timeOffset: -283 },
|
||||||
|
{ x: 86, y: 387, id: 27, timeOffset: -216 },
|
||||||
|
{ x: 70, y: 384, id: 27, timeOffset: -166 },
|
||||||
|
{ x: 58, y: 381, id: 27, timeOffset: -100 },
|
||||||
|
{ x: 53, y: 381, id: 27, timeOffset: -33 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900024047,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 49, y: 383, id: 27, timeOffset: -468 },
|
||||||
|
{ x: 39, y: 387, id: 27, timeOffset: -418 },
|
||||||
|
{ x: 31, y: 389, id: 27, timeOffset: -367 },
|
||||||
|
{ x: 28, y: 390, id: 27, timeOffset: -301 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900024548,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: { source: 1, positions: [{ x: 28, y: 390, id: 27, timeOffset: 0 }] },
|
||||||
|
timestamp: 1900034631,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 29, y: 393, id: 27, timeOffset: -459 },
|
||||||
|
{ x: 262, y: 474, id: 3, timeOffset: -376 },
|
||||||
|
{ x: 562, y: 573, id: 3, timeOffset: -326 },
|
||||||
|
{ x: 702, y: 603, id: 3, timeOffset: -260 },
|
||||||
|
{ x: 714, y: 603, id: 3, timeOffset: -209 },
|
||||||
|
{ x: 716, y: 600, id: 3, timeOffset: -159 },
|
||||||
|
{ x: 717, y: 597, id: 3, timeOffset: -109 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900035140,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: { source: 1, positions: [{ x: 718, y: 596, id: 3, timeOffset: 0 }] },
|
||||||
|
timestamp: 1900035963,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [{ x: 719, y: 594, id: 3, timeOffset: -451 }],
|
||||||
|
},
|
||||||
|
timestamp: 1900036464,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: { source: 1, positions: [{ x: 722, y: 594, id: 3, timeOffset: 0 }] },
|
||||||
|
timestamp: 1900037931,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 772, y: 588, id: 3, timeOffset: -438 },
|
||||||
|
{ x: 850, y: 577, id: 3, timeOffset: -371 },
|
||||||
|
{ x: 879, y: 576, id: 3, timeOffset: -321 },
|
||||||
|
{ x: 914, y: 576, id: 3, timeOffset: -255 },
|
||||||
|
{ x: 926, y: 577, id: 3, timeOffset: -205 },
|
||||||
|
{ x: 932, y: 579, id: 3, timeOffset: -154 },
|
||||||
|
{ x: 935, y: 582, id: 3, timeOffset: -88 },
|
||||||
|
{ x: 945, y: 587, id: 3, timeOffset: -22 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900038435,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 966, y: 593, id: 3, timeOffset: -483 },
|
||||||
|
{ x: 1006, y: 601, id: 3, timeOffset: -433 },
|
||||||
|
{ x: 1075, y: 608, id: 3, timeOffset: -383 },
|
||||||
|
{ x: 1098, y: 610, id: 3, timeOffset: -333 },
|
||||||
|
{ x: 1102, y: 611, id: 3, timeOffset: -283 },
|
||||||
|
{ x: 1102, y: 611, id: 3, timeOffset: -217 },
|
||||||
|
{ x: 1103, y: 612, id: 3, timeOffset: -166 },
|
||||||
|
{ x: 1103, y: 616, id: 3, timeOffset: -100 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900038947,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 1103, y: 616, id: 3, timeOffset: -151 },
|
||||||
|
{ x: 1103, y: 616, id: 3, timeOffset: -52 },
|
||||||
|
{ x: 1103, y: 614, id: 3, timeOffset: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900039448,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default events;
|
||||||
628
packages/rrweb/test/events/video-playback.ts
Normal file
628
packages/rrweb/test/events/video-playback.ts
Normal file
@@ -0,0 +1,628 @@
|
|||||||
|
import type { eventWithTime } from '@rrweb/types';
|
||||||
|
|
||||||
|
const events: eventWithTime[] = [
|
||||||
|
{ type: 0, data: {}, timestamp: 1900000001 },
|
||||||
|
{ type: 1, data: {}, timestamp: 1900000132 },
|
||||||
|
{
|
||||||
|
type: 4,
|
||||||
|
data: {
|
||||||
|
href: 'http://127.0.0.1:5500/test/html/video.html',
|
||||||
|
width: 1600,
|
||||||
|
height: 900,
|
||||||
|
},
|
||||||
|
timestamp: 1900000132,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
data: {
|
||||||
|
node: {
|
||||||
|
type: 0,
|
||||||
|
childNodes: [
|
||||||
|
{ type: 1, name: 'html', publicId: '', systemId: '', id: 2 },
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'html',
|
||||||
|
attributes: { lang: 'en' },
|
||||||
|
childNodes: [
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'head',
|
||||||
|
attributes: {},
|
||||||
|
childNodes: [
|
||||||
|
{ type: 3, textContent: '\n ', id: 5 },
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'meta',
|
||||||
|
attributes: { charset: 'UTF-8' },
|
||||||
|
childNodes: [],
|
||||||
|
id: 6,
|
||||||
|
},
|
||||||
|
{ type: 3, textContent: '\n ', id: 7 },
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'meta',
|
||||||
|
attributes: {
|
||||||
|
'http-equiv': 'X-UA-Compatible',
|
||||||
|
content: 'IE=edge',
|
||||||
|
},
|
||||||
|
childNodes: [],
|
||||||
|
id: 8,
|
||||||
|
},
|
||||||
|
{ type: 3, textContent: '\n ', id: 9 },
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'meta',
|
||||||
|
attributes: {
|
||||||
|
name: 'viewport',
|
||||||
|
content: 'width=device-width, initial-scale=1.0',
|
||||||
|
},
|
||||||
|
childNodes: [],
|
||||||
|
id: 10,
|
||||||
|
},
|
||||||
|
{ type: 3, textContent: '\n ', id: 11 },
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'title',
|
||||||
|
attributes: {},
|
||||||
|
childNodes: [{ type: 3, textContent: 'Video', id: 13 }],
|
||||||
|
id: 12,
|
||||||
|
},
|
||||||
|
{ type: 3, textContent: '\n ', id: 14 },
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'script',
|
||||||
|
attributes: { type: 'text/javascript' },
|
||||||
|
childNodes: [
|
||||||
|
{ type: 3, textContent: 'SCRIPT_PLACEHOLDER', id: 16 },
|
||||||
|
{ type: 3, textContent: 'SCRIPT_PLACEHOLDER', id: 17 },
|
||||||
|
{ type: 3, textContent: 'SCRIPT_PLACEHOLDER', id: 18 },
|
||||||
|
{ type: 3, textContent: 'SCRIPT_PLACEHOLDER', id: 19 },
|
||||||
|
{ type: 3, textContent: 'SCRIPT_PLACEHOLDER', id: 20 },
|
||||||
|
],
|
||||||
|
id: 15,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 4,
|
||||||
|
},
|
||||||
|
{ type: 3, textContent: '\n ', id: 21 },
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'body',
|
||||||
|
attributes: {},
|
||||||
|
childNodes: [
|
||||||
|
{ type: 3, textContent: '\n ', id: 23 },
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'h1',
|
||||||
|
attributes: {},
|
||||||
|
childNodes: [
|
||||||
|
{ type: 3, textContent: 'Big Buck Bunny', id: 25 },
|
||||||
|
],
|
||||||
|
id: 24,
|
||||||
|
},
|
||||||
|
{ type: 3, textContent: '\n ', id: 26 },
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'video',
|
||||||
|
attributes: {
|
||||||
|
muted: '',
|
||||||
|
controls: '',
|
||||||
|
rr_mediaState: 'played',
|
||||||
|
rr_mediaCurrentTime: 0,
|
||||||
|
rr_mediaPlaybackRate: 1,
|
||||||
|
rr_mediaMuted: true,
|
||||||
|
rr_mediaVolume: 1,
|
||||||
|
},
|
||||||
|
childNodes: [
|
||||||
|
{ type: 3, textContent: '\n ', id: 28 },
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'source',
|
||||||
|
attributes: {
|
||||||
|
src: '/html/assets/bunny-video.webm',
|
||||||
|
type: 'video/webm',
|
||||||
|
},
|
||||||
|
childNodes: [],
|
||||||
|
id: 29,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
textContent:
|
||||||
|
'\n Your browser does not support the video element.\n ',
|
||||||
|
id: 30,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 27,
|
||||||
|
},
|
||||||
|
{ type: 3, textContent: '\n ', id: 31 },
|
||||||
|
{
|
||||||
|
type: 5,
|
||||||
|
textContent: ' Code injected by live-server ',
|
||||||
|
id: 32,
|
||||||
|
},
|
||||||
|
{ type: 3, textContent: '\n', id: 33 },
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'script',
|
||||||
|
attributes: {},
|
||||||
|
childNodes: [
|
||||||
|
{ type: 3, textContent: 'SCRIPT_PLACEHOLDER', id: 35 },
|
||||||
|
],
|
||||||
|
id: 34,
|
||||||
|
},
|
||||||
|
{ type: 3, textContent: '\n\n\n', id: 36 },
|
||||||
|
],
|
||||||
|
id: 22,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
initialOffset: { left: 0, top: 0 },
|
||||||
|
},
|
||||||
|
timestamp: 1900000136,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 7,
|
||||||
|
type: 0,
|
||||||
|
id: 27,
|
||||||
|
currentTime: 0.000322,
|
||||||
|
volume: 1,
|
||||||
|
muted: true,
|
||||||
|
playbackRate: 1,
|
||||||
|
},
|
||||||
|
timestamp: 1900001500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: { source: 1, positions: [{ x: 955, y: 869, id: 3, timeOffset: 0 }] },
|
||||||
|
timestamp: 1900006165,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 902, y: 823, id: 3, timeOffset: -451 },
|
||||||
|
{ x: 840, y: 765, id: 3, timeOffset: -400 },
|
||||||
|
{ x: 796, y: 724, id: 3, timeOffset: -350 },
|
||||||
|
{ x: 716, y: 673, id: 3, timeOffset: -267 },
|
||||||
|
{ x: 660, y: 645, id: 3, timeOffset: -217 },
|
||||||
|
{ x: 554, y: 593, id: 3, timeOffset: -167 },
|
||||||
|
{ x: 466, y: 518, id: 3, timeOffset: -101 },
|
||||||
|
{ x: 433, y: 413, id: 27, timeOffset: -51 },
|
||||||
|
{ x: 432, y: 316, id: 27, timeOffset: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900006665,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 444, y: 229, id: 27, timeOffset: -435 },
|
||||||
|
{ x: 444, y: 210, id: 27, timeOffset: -385 },
|
||||||
|
{ x: 444, y: 209, id: 27, timeOffset: -335 },
|
||||||
|
{ x: 445, y: 214, id: 27, timeOffset: -235 },
|
||||||
|
{ x: 460, y: 246, id: 27, timeOffset: -185 },
|
||||||
|
{ x: 476, y: 273, id: 27, timeOffset: -134 },
|
||||||
|
{ x: 482, y: 281, id: 27, timeOffset: -84 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900007166,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: { source: 1, positions: [{ x: 483, y: 281, id: 27, timeOffset: 0 }] },
|
||||||
|
timestamp: 1900007814,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 483, y: 281, id: 27, timeOffset: -417 },
|
||||||
|
{ x: 484, y: 282, id: 27, timeOffset: -218 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900008315,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: { source: 1, positions: [{ x: 484, y: 281, id: 27, timeOffset: 0 }] },
|
||||||
|
timestamp: 1900010165,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 484, y: 281, id: 27, timeOffset: -322 },
|
||||||
|
{ x: 485, y: 281, id: 27, timeOffset: -256 },
|
||||||
|
{ x: 491, y: 282, id: 27, timeOffset: -205 },
|
||||||
|
{ x: 515, y: 283, id: 27, timeOffset: -156 },
|
||||||
|
{ x: 534, y: 285, id: 27, timeOffset: -106 },
|
||||||
|
{ x: 562, y: 291, id: 27, timeOffset: -56 },
|
||||||
|
{ x: 575, y: 295, id: 27, timeOffset: -6 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900010670,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: { source: 1, positions: [{ x: 576, y: 296, id: 27, timeOffset: 0 }] },
|
||||||
|
timestamp: 1900012714,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 576, y: 298, id: 27, timeOffset: -451 },
|
||||||
|
{ x: 577, y: 301, id: 27, timeOffset: -400 },
|
||||||
|
{ x: 578, y: 305, id: 27, timeOffset: -334 },
|
||||||
|
{ x: 580, y: 313, id: 27, timeOffset: -284 },
|
||||||
|
{ x: 582, y: 332, id: 27, timeOffset: -234 },
|
||||||
|
{ x: 585, y: 346, id: 27, timeOffset: -184 },
|
||||||
|
{ x: 587, y: 353, id: 27, timeOffset: -133 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900013215,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [{ x: 587, y: 354, id: 27, timeOffset: -253 }],
|
||||||
|
},
|
||||||
|
timestamp: 1900013717,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: { source: 1, positions: [{ x: 587, y: 355, id: 27, timeOffset: 0 }] },
|
||||||
|
timestamp: 1900014364,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 588, y: 358, id: 27, timeOffset: -451 },
|
||||||
|
{ x: 589, y: 368, id: 27, timeOffset: -400 },
|
||||||
|
{ x: 592, y: 386, id: 27, timeOffset: -334 },
|
||||||
|
{ x: 596, y: 402, id: 27, timeOffset: -266 },
|
||||||
|
{ x: 597, y: 410, id: 27, timeOffset: -202 },
|
||||||
|
{ x: 595, y: 415, id: 27, timeOffset: -152 },
|
||||||
|
{ x: 591, y: 417, id: 27, timeOffset: -101 },
|
||||||
|
{ x: 585, y: 418, id: 27, timeOffset: -51 },
|
||||||
|
{ x: 579, y: 418, id: 27, timeOffset: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900014865,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 562, y: 420, id: 27, timeOffset: -433 },
|
||||||
|
{ x: 547, y: 422, id: 27, timeOffset: -367 },
|
||||||
|
{ x: 538, y: 423, id: 27, timeOffset: -317 },
|
||||||
|
{ x: 525, y: 423, id: 27, timeOffset: -251 },
|
||||||
|
{ x: 509, y: 423, id: 27, timeOffset: -201 },
|
||||||
|
{ x: 488, y: 422, id: 27, timeOffset: -151 },
|
||||||
|
{ x: 440, y: 421, id: 27, timeOffset: -101 },
|
||||||
|
{ x: 373, y: 419, id: 27, timeOffset: -51 },
|
||||||
|
{ x: 296, y: 415, id: 27, timeOffset: -1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900015365,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 225, y: 409, id: 27, timeOffset: -439 },
|
||||||
|
{ x: 183, y: 408, id: 27, timeOffset: -389 },
|
||||||
|
{ x: 138, y: 407, id: 27, timeOffset: -340 },
|
||||||
|
{ x: 98, y: 406, id: 27, timeOffset: -290 },
|
||||||
|
{ x: 79, y: 406, id: 27, timeOffset: -238 },
|
||||||
|
{ x: 60, y: 406, id: 27, timeOffset: -173 },
|
||||||
|
{ x: 53, y: 406, id: 27, timeOffset: -122 },
|
||||||
|
{ x: 39, y: 405, id: 27, timeOffset: -55 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900015870,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 34, y: 403, id: 27, timeOffset: -493 },
|
||||||
|
{ x: 31, y: 401, id: 27, timeOffset: -442 },
|
||||||
|
{ x: 29, y: 399, id: 27, timeOffset: -375 },
|
||||||
|
{ x: 28, y: 399, id: 27, timeOffset: -325 },
|
||||||
|
{ x: 28, y: 397, id: 27, timeOffset: -259 },
|
||||||
|
{ x: 28, y: 394, id: 27, timeOffset: -209 },
|
||||||
|
{ x: 28, y: 394, id: 27, timeOffset: -159 },
|
||||||
|
{ x: 28, y: 393, id: 27, timeOffset: -109 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900016373,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: { source: 1, positions: [{ x: 29, y: 393, id: 27, timeOffset: 0 }] },
|
||||||
|
timestamp: 1900018598,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 30, y: 391, id: 27, timeOffset: -433 },
|
||||||
|
{ x: 31, y: 392, id: 27, timeOffset: -251 },
|
||||||
|
{ x: 30, y: 393, id: 27, timeOffset: -201 },
|
||||||
|
{ x: 30, y: 394, id: 27, timeOffset: -151 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900019098,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 30, y: 394, id: 27, timeOffset: -457 },
|
||||||
|
{ x: 30, y: 394, id: 27, timeOffset: -391 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900019605,
|
||||||
|
},
|
||||||
|
{ type: 3, data: { source: 2, type: 5, id: 27 }, timestamp: 1900020571 },
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 7,
|
||||||
|
type: 1,
|
||||||
|
id: 27,
|
||||||
|
currentTime: 20.088367,
|
||||||
|
volume: 1,
|
||||||
|
muted: true,
|
||||||
|
playbackRate: 1,
|
||||||
|
},
|
||||||
|
timestamp: 1900020661,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: { source: 1, positions: [{ x: 32, y: 394, id: 27, timeOffset: 0 }] },
|
||||||
|
timestamp: 1900021531,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 48, y: 394, id: 27, timeOffset: -449 },
|
||||||
|
{ x: 286, y: 413, id: 27, timeOffset: -384 },
|
||||||
|
{ x: 418, y: 419, id: 27, timeOffset: -334 },
|
||||||
|
{ x: 474, y: 419, id: 27, timeOffset: -284 },
|
||||||
|
{ x: 482, y: 418, id: 27, timeOffset: -233 },
|
||||||
|
{ x: 482, y: 417, id: 27, timeOffset: -167 },
|
||||||
|
{ x: 477, y: 416, id: 27, timeOffset: -116 },
|
||||||
|
{ x: 439, y: 414, id: 27, timeOffset: -51 },
|
||||||
|
{ x: 402, y: 412, id: 27, timeOffset: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900022031,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 376, y: 413, id: 27, timeOffset: -450 },
|
||||||
|
{ x: 366, y: 414, id: 27, timeOffset: -400 },
|
||||||
|
{ x: 353, y: 416, id: 27, timeOffset: -334 },
|
||||||
|
{ x: 346, y: 417, id: 27, timeOffset: -283 },
|
||||||
|
{ x: 339, y: 419, id: 27, timeOffset: -233 },
|
||||||
|
{ x: 322, y: 422, id: 27, timeOffset: -167 },
|
||||||
|
{ x: 311, y: 422, id: 27, timeOffset: -117 },
|
||||||
|
{ x: 308, y: 422, id: 27, timeOffset: -51 },
|
||||||
|
{ x: 311, y: 420, id: 27, timeOffset: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900022531,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 7,
|
||||||
|
type: 2,
|
||||||
|
id: 27,
|
||||||
|
currentTime: 9.744288,
|
||||||
|
volume: 1,
|
||||||
|
muted: true,
|
||||||
|
playbackRate: 1,
|
||||||
|
},
|
||||||
|
timestamp: 19000602896,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 315, y: 419, id: 27, timeOffset: -448 },
|
||||||
|
{ x: 316, y: 418, id: 27, timeOffset: -397 },
|
||||||
|
{ x: 317, y: 417, id: 27, timeOffset: -347 },
|
||||||
|
{ x: 317, y: 417, id: 27, timeOffset: -281 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900023045,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 318, y: 418, id: 27, timeOffset: -466 },
|
||||||
|
{ x: 326, y: 439, id: 27, timeOffset: -416 },
|
||||||
|
{ x: 333, y: 473, id: 3, timeOffset: -365 },
|
||||||
|
{ x: 334, y: 484, id: 3, timeOffset: -300 },
|
||||||
|
{ x: 334, y: 485, id: 3, timeOffset: -50 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900023547,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 333, y: 485, id: 3, timeOffset: -483 },
|
||||||
|
{ x: 321, y: 481, id: 3, timeOffset: -433 },
|
||||||
|
{ x: 265, y: 460, id: 3, timeOffset: -383 },
|
||||||
|
{ x: 203, y: 433, id: 27, timeOffset: -332 },
|
||||||
|
{ x: 135, y: 402, id: 27, timeOffset: -283 },
|
||||||
|
{ x: 86, y: 387, id: 27, timeOffset: -216 },
|
||||||
|
{ x: 70, y: 384, id: 27, timeOffset: -166 },
|
||||||
|
{ x: 58, y: 381, id: 27, timeOffset: -100 },
|
||||||
|
{ x: 53, y: 381, id: 27, timeOffset: -33 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900024047,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 7,
|
||||||
|
type: 0,
|
||||||
|
id: 27,
|
||||||
|
currentTime: 9.744632,
|
||||||
|
volume: 1,
|
||||||
|
muted: true,
|
||||||
|
playbackRate: 1,
|
||||||
|
},
|
||||||
|
timestamp: 1900024475,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 49, y: 383, id: 27, timeOffset: -468 },
|
||||||
|
{ x: 39, y: 387, id: 27, timeOffset: -418 },
|
||||||
|
{ x: 31, y: 389, id: 27, timeOffset: -367 },
|
||||||
|
{ x: 28, y: 390, id: 27, timeOffset: -301 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900024548,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 7,
|
||||||
|
type: 1,
|
||||||
|
id: 27,
|
||||||
|
currentTime: 19.704312,
|
||||||
|
volume: 1,
|
||||||
|
muted: true,
|
||||||
|
playbackRate: 1,
|
||||||
|
},
|
||||||
|
timestamp: 1900034476,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: { source: 1, positions: [{ x: 28, y: 390, id: 27, timeOffset: 0 }] },
|
||||||
|
timestamp: 1900034631,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 29, y: 393, id: 27, timeOffset: -459 },
|
||||||
|
{ x: 262, y: 474, id: 3, timeOffset: -376 },
|
||||||
|
{ x: 562, y: 573, id: 3, timeOffset: -326 },
|
||||||
|
{ x: 702, y: 603, id: 3, timeOffset: -260 },
|
||||||
|
{ x: 714, y: 603, id: 3, timeOffset: -209 },
|
||||||
|
{ x: 716, y: 600, id: 3, timeOffset: -159 },
|
||||||
|
{ x: 717, y: 597, id: 3, timeOffset: -109 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900035140,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: { source: 1, positions: [{ x: 718, y: 596, id: 3, timeOffset: 0 }] },
|
||||||
|
timestamp: 1900035963,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [{ x: 719, y: 594, id: 3, timeOffset: -451 }],
|
||||||
|
},
|
||||||
|
timestamp: 1900036464,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: { source: 1, positions: [{ x: 722, y: 594, id: 3, timeOffset: 0 }] },
|
||||||
|
timestamp: 1900037931,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 772, y: 588, id: 3, timeOffset: -438 },
|
||||||
|
{ x: 850, y: 577, id: 3, timeOffset: -371 },
|
||||||
|
{ x: 879, y: 576, id: 3, timeOffset: -321 },
|
||||||
|
{ x: 914, y: 576, id: 3, timeOffset: -255 },
|
||||||
|
{ x: 926, y: 577, id: 3, timeOffset: -205 },
|
||||||
|
{ x: 932, y: 579, id: 3, timeOffset: -154 },
|
||||||
|
{ x: 935, y: 582, id: 3, timeOffset: -88 },
|
||||||
|
{ x: 945, y: 587, id: 3, timeOffset: -22 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900038435,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 966, y: 593, id: 3, timeOffset: -483 },
|
||||||
|
{ x: 1006, y: 601, id: 3, timeOffset: -433 },
|
||||||
|
{ x: 1075, y: 608, id: 3, timeOffset: -383 },
|
||||||
|
{ x: 1098, y: 610, id: 3, timeOffset: -333 },
|
||||||
|
{ x: 1102, y: 611, id: 3, timeOffset: -283 },
|
||||||
|
{ x: 1102, y: 611, id: 3, timeOffset: -217 },
|
||||||
|
{ x: 1103, y: 612, id: 3, timeOffset: -166 },
|
||||||
|
{ x: 1103, y: 616, id: 3, timeOffset: -100 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900038947,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
data: {
|
||||||
|
source: 1,
|
||||||
|
positions: [
|
||||||
|
{ x: 1103, y: 616, id: 3, timeOffset: -151 },
|
||||||
|
{ x: 1103, y: 616, id: 3, timeOffset: -52 },
|
||||||
|
{ x: 1103, y: 614, id: 3, timeOffset: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timestamp: 1900039448,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default events;
|
||||||
BIN
packages/rrweb/test/html/assets/bunny-video.webm
Normal file
BIN
packages/rrweb/test/html/assets/bunny-video.webm
Normal file
Binary file not shown.
19
packages/rrweb/test/html/video.html
Normal file
19
packages/rrweb/test/html/video.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Video</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Big Buck Bunny</h1>
|
||||||
|
<video muted controls>
|
||||||
|
<source src="assets/bunny-video.webm" type="video/webm" />
|
||||||
|
Your browser does not support the video element.
|
||||||
|
</video>
|
||||||
|
</body>
|
||||||
|
<script>
|
||||||
|
document.querySelector('video').currentTime = 5;
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
@@ -263,7 +263,11 @@ exports[`cross origin iframes audio.html should emit contents of iframe once 1`]
|
|||||||
\\"attributes\\": {
|
\\"attributes\\": {
|
||||||
\\"controls\\": \\"\\",
|
\\"controls\\": \\"\\",
|
||||||
\\"rr_mediaState\\": \\"paused\\",
|
\\"rr_mediaState\\": \\"paused\\",
|
||||||
\\"rr_mediaCurrentTime\\": 0
|
\\"rr_mediaCurrentTime\\": 0,
|
||||||
|
\\"rr_mediaPlaybackRate\\": 1,
|
||||||
|
\\"rr_mediaMuted\\": false,
|
||||||
|
\\"rr_mediaLoop\\": false,
|
||||||
|
\\"rr_mediaVolume\\": 1
|
||||||
},
|
},
|
||||||
\\"childNodes\\": [
|
\\"childNodes\\": [
|
||||||
{
|
{
|
||||||
@@ -327,7 +331,8 @@ exports[`cross origin iframes audio.html should emit contents of iframe once 1`]
|
|||||||
\\"currentTime\\": 0,
|
\\"currentTime\\": 0,
|
||||||
\\"volume\\": 1,
|
\\"volume\\": 1,
|
||||||
\\"muted\\": false,
|
\\"muted\\": false,
|
||||||
\\"playbackRate\\": 1
|
\\"playbackRate\\": 1,
|
||||||
|
\\"loop\\": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]"
|
]"
|
||||||
|
|||||||
240
packages/rrweb/test/replay/ video.test.ts
Normal file
240
packages/rrweb/test/replay/ video.test.ts
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import type * as puppeteer from 'puppeteer';
|
||||||
|
import {
|
||||||
|
startServer,
|
||||||
|
launchPuppeteer,
|
||||||
|
getServerURL,
|
||||||
|
waitForRAF,
|
||||||
|
ISuite,
|
||||||
|
hideMouseAnimation,
|
||||||
|
fakeGoto,
|
||||||
|
} from '../utils';
|
||||||
|
import { toMatchImageSnapshot } from 'jest-image-snapshot';
|
||||||
|
import videoPlaybackEvents from '../events/video-playback';
|
||||||
|
import videoPlaybackOnFullSnapshotEvents from '../events/video-playback-on-full-snapshot';
|
||||||
|
expect.extend({ toMatchImageSnapshot });
|
||||||
|
|
||||||
|
describe('video', () => {
|
||||||
|
jest.setTimeout(100_000);
|
||||||
|
let code: ISuite['code'];
|
||||||
|
let page: ISuite['page'];
|
||||||
|
let browser: ISuite['browser'];
|
||||||
|
let server: ISuite['server'];
|
||||||
|
let serverURL: ISuite['serverURL'];
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
server = await startServer();
|
||||||
|
serverURL = getServerURL(server);
|
||||||
|
browser = await launchPuppeteer();
|
||||||
|
|
||||||
|
const bundlePath = path.resolve(__dirname, '../../dist/rrweb.js');
|
||||||
|
code = fs.readFileSync(bundlePath, 'utf8');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await server.close();
|
||||||
|
await browser.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
page = await browser.newPage();
|
||||||
|
|
||||||
|
await fakeGoto(page, `${serverURL}/html/video.html`);
|
||||||
|
await page.evaluate(code);
|
||||||
|
await waitForRAF(page);
|
||||||
|
await hideMouseAnimation(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('will seek to the correct moment', async () => {
|
||||||
|
await page.evaluate(`let events = ${JSON.stringify(videoPlaybackEvents)}`);
|
||||||
|
await page.evaluate(`
|
||||||
|
const { Replayer } = rrweb;
|
||||||
|
window.replayer = new Replayer(events);
|
||||||
|
`);
|
||||||
|
await waitForRAF(page);
|
||||||
|
await page.evaluate(`
|
||||||
|
window.replayer.pause(6500);
|
||||||
|
`);
|
||||||
|
await page.waitForNetworkIdle();
|
||||||
|
await waitForRAF(page);
|
||||||
|
|
||||||
|
const frameImage = await page!.screenshot();
|
||||||
|
await waitForRAF(page);
|
||||||
|
expect(frameImage).toMatchImageSnapshot({
|
||||||
|
failureThreshold: 0.05,
|
||||||
|
failureThresholdType: 'percent',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('will seek to the correct moment without media interaction events', async () => {
|
||||||
|
await page.evaluate(`
|
||||||
|
let events = ${JSON.stringify(videoPlaybackOnFullSnapshotEvents)};
|
||||||
|
const { Replayer } = rrweb;
|
||||||
|
window.replayer = new Replayer(events);
|
||||||
|
window.replayer.pause(6500);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await page.waitForNetworkIdle();
|
||||||
|
await waitForRAF(page);
|
||||||
|
|
||||||
|
const frameImage = await page!.screenshot();
|
||||||
|
await waitForRAF(page);
|
||||||
|
expect(frameImage).toMatchImageSnapshot({
|
||||||
|
failureThreshold: 0.05,
|
||||||
|
failureThresholdType: 'percent',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("will be paused when the player wasn't started yet", async () => {
|
||||||
|
await page.evaluate(`
|
||||||
|
let events = ${JSON.stringify(videoPlaybackEvents)};
|
||||||
|
const { Replayer } = rrweb;
|
||||||
|
window.replayer = new Replayer(events);
|
||||||
|
`);
|
||||||
|
await page.waitForNetworkIdle();
|
||||||
|
await waitForRAF(page);
|
||||||
|
|
||||||
|
const frameImage = await page!.screenshot();
|
||||||
|
|
||||||
|
await waitForRAF(page);
|
||||||
|
expect(frameImage).toMatchImageSnapshot({
|
||||||
|
failureThreshold: 0.05,
|
||||||
|
failureThresholdType: 'percent',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('will play from the correct moment', async () => {
|
||||||
|
await page.evaluate(`let events = ${JSON.stringify(videoPlaybackEvents)}`);
|
||||||
|
await page.evaluate(`
|
||||||
|
const { Replayer } = rrweb;
|
||||||
|
window.replayer = new Replayer(events, {
|
||||||
|
UNSAFE_replayCanvas: true,
|
||||||
|
});
|
||||||
|
`);
|
||||||
|
await waitForRAF(page);
|
||||||
|
await page.evaluate(`
|
||||||
|
window.replayer.play(6500);
|
||||||
|
`);
|
||||||
|
await page.waitForNetworkIdle();
|
||||||
|
await waitForRAF(page);
|
||||||
|
|
||||||
|
const frameImage = await page!.screenshot();
|
||||||
|
await waitForRAF(page);
|
||||||
|
expect(frameImage).toMatchImageSnapshot({
|
||||||
|
failureThreshold: 0.05,
|
||||||
|
failureThresholdType: 'percent',
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: check to see if video is same as basic replay
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should play from the start', async () => {
|
||||||
|
await page.evaluate(`let events = ${JSON.stringify(videoPlaybackEvents)}`);
|
||||||
|
await page.evaluate(`
|
||||||
|
const { Replayer } = rrweb;
|
||||||
|
window.replayer = new Replayer(events);
|
||||||
|
window.replayer.play();
|
||||||
|
`);
|
||||||
|
await waitForRAF(page);
|
||||||
|
await page.waitForNetworkIdle();
|
||||||
|
await waitForRAF(page);
|
||||||
|
|
||||||
|
const isPlaying = await page.evaluate(`
|
||||||
|
!document.querySelector('iframe').contentDocument.querySelector('video').paused &&
|
||||||
|
document.querySelector('iframe').contentDocument.querySelector('video').currentTime !== 0 &&
|
||||||
|
!document.querySelector('iframe').contentDocument.querySelector('video').ended;
|
||||||
|
`);
|
||||||
|
expect(isPlaying).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should play from the start without media events', async () => {
|
||||||
|
await page.evaluate(
|
||||||
|
`let events = ${JSON.stringify(videoPlaybackOnFullSnapshotEvents)}`,
|
||||||
|
);
|
||||||
|
await page.evaluate(`
|
||||||
|
const { Replayer } = rrweb;
|
||||||
|
window.replayer = new Replayer(events);
|
||||||
|
window.replayer.play();
|
||||||
|
`);
|
||||||
|
await waitForRAF(page);
|
||||||
|
await page.waitForNetworkIdle();
|
||||||
|
await waitForRAF(page);
|
||||||
|
|
||||||
|
const isPlaying = await page.evaluate(`
|
||||||
|
!document.querySelector('iframe').contentDocument.querySelector('video').paused &&
|
||||||
|
document.querySelector('iframe').contentDocument.querySelector('video').currentTime !== 0 &&
|
||||||
|
!document.querySelector('iframe').contentDocument.querySelector('video').ended;
|
||||||
|
`);
|
||||||
|
expect(isPlaying).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should report the correct time for looping videos that have passed their total time', async () => {
|
||||||
|
await page.evaluate(
|
||||||
|
`let events = ${JSON.stringify(videoPlaybackOnFullSnapshotEvents)}`,
|
||||||
|
);
|
||||||
|
await page.evaluate(`
|
||||||
|
const { Replayer } = rrweb;
|
||||||
|
window.replayer = new Replayer(events);
|
||||||
|
`);
|
||||||
|
await waitForRAF(page);
|
||||||
|
await page.waitForNetworkIdle();
|
||||||
|
await waitForRAF(page);
|
||||||
|
await page.evaluate(`
|
||||||
|
window.replayer.pause(25000); // 5 seconds after the video started a new loop
|
||||||
|
`);
|
||||||
|
await waitForRAF(page);
|
||||||
|
|
||||||
|
const time = await page.evaluate(`
|
||||||
|
document.querySelector('iframe').contentDocument.querySelector('video').currentTime;
|
||||||
|
`);
|
||||||
|
expect(time).toBeCloseTo(5, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the correct time on loading videos', async () => {
|
||||||
|
await page.evaluate(
|
||||||
|
`let events = ${JSON.stringify(videoPlaybackOnFullSnapshotEvents)}`,
|
||||||
|
);
|
||||||
|
await page.evaluate(`
|
||||||
|
const { Replayer } = rrweb;
|
||||||
|
window.replayer = new Replayer(events);
|
||||||
|
window.replayer.pause(25000); // 5 seconds after the video started a new loop
|
||||||
|
`);
|
||||||
|
await waitForRAF(page);
|
||||||
|
await page.waitForNetworkIdle();
|
||||||
|
await waitForRAF(page);
|
||||||
|
|
||||||
|
const time = await page.evaluate(`
|
||||||
|
document.querySelector('iframe').contentDocument.querySelector('video').currentTime;
|
||||||
|
`);
|
||||||
|
expect(time).toBeCloseTo(5, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the correct playbackRate on faster playback', async () => {
|
||||||
|
page.on('console', (msg) => {
|
||||||
|
console.log(msg.text());
|
||||||
|
});
|
||||||
|
await page.evaluate(
|
||||||
|
`let events = ${JSON.stringify(videoPlaybackOnFullSnapshotEvents)}`,
|
||||||
|
);
|
||||||
|
await page.evaluate(`
|
||||||
|
const { Replayer } = rrweb;
|
||||||
|
window.replayer = new Replayer(events, {
|
||||||
|
speed: 8,
|
||||||
|
});
|
||||||
|
window.replayer.play();
|
||||||
|
`);
|
||||||
|
await waitForRAF(page);
|
||||||
|
await page.waitForNetworkIdle();
|
||||||
|
await waitForRAF(page);
|
||||||
|
|
||||||
|
const time = await page.evaluate(`
|
||||||
|
document.querySelector('iframe').contentDocument.querySelector('video').playbackRate;
|
||||||
|
`);
|
||||||
|
expect(time).toBe(8);
|
||||||
|
});
|
||||||
|
});
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 151 KiB |
@@ -50,6 +50,7 @@ export const startServer = (defaultPort: number = 3030) =>
|
|||||||
'.html': 'text/html',
|
'.html': 'text/html',
|
||||||
'.js': 'text/javascript',
|
'.js': 'text/javascript',
|
||||||
'.css': 'text/css',
|
'.css': 'text/css',
|
||||||
|
'.webm': 'video/webm',
|
||||||
};
|
};
|
||||||
const s = http.createServer((req, res) => {
|
const s = http.createServer((req, res) => {
|
||||||
const parsedUrl = url.parse(req.url!);
|
const parsedUrl = url.parse(req.url!);
|
||||||
@@ -69,6 +70,7 @@ export const startServer = (defaultPort: number = 3030) =>
|
|||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
res.setHeader('Access-Control-Allow-Methods', 'GET');
|
res.setHeader('Access-Control-Allow-Methods', 'GET');
|
||||||
res.setHeader('Access-Control-Allow-Headers', 'Content-type');
|
res.setHeader('Access-Control-Allow-Headers', 'Content-type');
|
||||||
|
if (ext === '.webm') res.setHeader('Accept-Ranges', 'bytes');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
res.end(data);
|
res.end(data);
|
||||||
// mock delay
|
// mock delay
|
||||||
@@ -703,3 +705,26 @@ export function generateRecordSnippet(options: recordOptions<eventWithTime>) {
|
|||||||
});
|
});
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function hideMouseAnimation(p: puppeteer.Page): Promise<void> {
|
||||||
|
await p.addStyleTag({
|
||||||
|
content: `.replayer-mouse-tail{display: none !important;}
|
||||||
|
html, body { margin: 0; padding: 0; }
|
||||||
|
iframe { border: none; }`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fakeGoto = async (p: puppeteer.Page, url: string) => {
|
||||||
|
const intercept = async (request: puppeteer.HTTPRequest) => {
|
||||||
|
await request.respond({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'text/html',
|
||||||
|
body: ' ', // non-empty string or page will load indefinitely
|
||||||
|
});
|
||||||
|
};
|
||||||
|
await p.setRequestInterception(true);
|
||||||
|
p.on('request', intercept);
|
||||||
|
await p.goto(url);
|
||||||
|
p.off('request', intercept);
|
||||||
|
await p.setRequestInterception(false);
|
||||||
|
};
|
||||||
|
|||||||
@@ -573,6 +573,7 @@ export type mediaInteractionParam = {
|
|||||||
currentTime?: number;
|
currentTime?: number;
|
||||||
volume?: number;
|
volume?: number;
|
||||||
muted?: boolean;
|
muted?: boolean;
|
||||||
|
loop?: boolean;
|
||||||
playbackRate?: number;
|
playbackRate?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -653,6 +654,9 @@ export type Arguments<T> = T extends (...payload: infer U) => unknown
|
|||||||
export enum ReplayerEvents {
|
export enum ReplayerEvents {
|
||||||
Start = 'start',
|
Start = 'start',
|
||||||
Pause = 'pause',
|
Pause = 'pause',
|
||||||
|
/**
|
||||||
|
* @deprecated use Play instead
|
||||||
|
*/
|
||||||
Resume = 'resume',
|
Resume = 'resume',
|
||||||
Resize = 'resize',
|
Resize = 'resize',
|
||||||
Finish = 'finish',
|
Finish = 'finish',
|
||||||
|
|||||||
Reference in New Issue
Block a user