impl media interactions recording

close #159
close #72
listen to HTMLMediaElement's play/pause events, and replay them
by programmatically play and pause the target element.
This commit is contained in:
Yanzhen Yu
2026-04-01 12:00:00 +08:00
parent d0c31bb4cf
commit 5bfc2c704a
5 changed files with 89 additions and 4 deletions

View File

@@ -58,7 +58,7 @@
"dependencies": {
"@types/smoothscroll-polyfill": "^0.3.0",
"mitt": "^1.1.3",
"rrweb-snapshot": "^0.7.23",
"rrweb-snapshot": "file:../snapshot",
"smoothscroll-polyfill": "^0.4.3"
}
}

View File

@@ -35,7 +35,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
inlineStylesheet = true,
maskAllInputs = false,
hooks,
mousemoveWait = 50
mousemoveWait = 50,
} = options;
// runtime checks for user options
if (!emit) {
@@ -178,11 +178,21 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
},
}),
),
mediaInteractionCb: p =>
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.MediaInteraction,
...p,
},
}),
),
blockClass,
ignoreClass,
maskAllInputs,
inlineStylesheet,
mousemoveWait
mousemoveWait,
},
hooks,
),

View File

@@ -31,6 +31,8 @@ import {
IncrementalSource,
hooksParam,
Arguments,
mediaInteractionCallback,
MediaInteractions,
} from '../types';
import { deepDelete, isParentRemoved, isAncestorInSet } from './collection';
@@ -517,6 +519,26 @@ function initInputObserver(
};
}
function initMediaInteractionObserver(
mediaInteractionCb: mediaInteractionCallback,
blockClass: blockClass,
): listenerHandler {
const handler = (type: 'play' | 'pause') => (event: Event) => {
const { target } = event;
if (!target || isBlocked(target as Node, blockClass)) {
return;
}
mediaInteractionCb({
type: type === 'play' ? MediaInteractions.Play : MediaInteractions.Pause,
id: mirror.getId(target as INode),
});
};
const handlers = [on('play', handler('play')), on('pause', handler('pause'))];
return () => {
handlers.forEach(h => h());
};
}
function mergeHooks(o: observerParam, hooks: hooksParam) {
const {
mutationCb,
@@ -525,6 +547,7 @@ function mergeHooks(o: observerParam, hooks: hooksParam) {
scrollCb,
viewportResizeCb,
inputCb,
mediaInteractionCb,
} = o;
o.mutationCb = (...p: Arguments<mutationCallBack>) => {
if (hooks.mutation) {
@@ -562,6 +585,12 @@ function mergeHooks(o: observerParam, hooks: hooksParam) {
}
inputCb(...p);
};
o.mediaInteractionCb = (...p: Arguments<mediaInteractionCallback>) => {
if (hooks.mediaInteaction) {
hooks.mediaInteaction(...p);
}
mediaInteractionCb(...p);
};
}
export default function initObservers(
@@ -588,6 +617,10 @@ export default function initObservers(
o.ignoreClass,
o.maskAllInputs,
);
const mediaInteractionHandler = initMediaInteractionObserver(
o.mediaInteractionCb,
o.blockClass,
);
return () => {
mutationObserver.disconnect();
mousemoveHandler();
@@ -595,5 +628,6 @@ export default function initObservers(
scrollHandler();
viewportResizeHandler();
inputHandler();
mediaInteractionHandler();
};
}

View File

@@ -20,6 +20,7 @@ import {
ReplayerEvents,
Handler,
Emitter,
MediaInteractions,
} from '../types';
import { mirror, polyfill } from '../utils';
import getInjectStyleRules from './styles/inject-style';
@@ -587,6 +588,26 @@ export class Replayer {
}
break;
}
case IncrementalSource.MediaInteraction: {
const target = mirror.getNode(d.id);
if (!target) {
return this.debugNodeNotFound(d, d.id);
}
const mediaEl = (target as Node) as HTMLMediaElement;
if (d.type === MediaInteractions.Pause) {
mediaEl.pause();
}
if (d.type === MediaInteractions.Play) {
if (mediaEl.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
mediaEl.play();
} else {
mediaEl.addEventListener('canplay', () => {
mediaEl.play();
});
}
}
break;
}
default:
}
}

View File

@@ -60,6 +60,7 @@ export enum IncrementalSource {
ViewportResize,
Input,
TouchMove,
MediaInteraction,
}
export type mutationData = {
@@ -88,13 +89,18 @@ export type inputData = {
id: number;
} & inputValue;
export type mediaInteractionData = {
source: IncrementalSource.MediaInteraction;
} & mediaInteractionParam;
export type incrementalData =
| mutationData
| mousemoveData
| mouseInteractionData
| scrollData
| viewportResizeData
| inputData;
| inputData
| mediaInteractionData;
export type event =
| domContentLoadedEvent
@@ -130,6 +136,7 @@ export type observerParam = {
scrollCb: scrollCallback;
viewportResizeCb: viewportResizeCallback;
inputCb: inputCallback;
mediaInteractionCb: mediaInteractionCallback;
blockClass: blockClass;
ignoreClass: string;
maskAllInputs: boolean;
@@ -144,6 +151,7 @@ export type hooksParam = {
scroll?: scrollCallback;
viewportResize?: viewportResizeCallback;
input?: inputCallback;
mediaInteaction?: mediaInteractionCallback;
};
export type textCursor = {
@@ -245,6 +253,18 @@ export type inputValue = {
export type inputCallback = (v: inputValue & { id: number }) => void;
export const enum MediaInteractions {
Play,
Pause,
}
export type mediaInteractionParam = {
type: MediaInteractions;
id: number;
};
export type mediaInteractionCallback = (p: mediaInteractionParam) => void;
export type Mirror = {
map: idNodeMap;
getId: (n: INode) => number;