From 5bfc2c704a9115dfae0c443d3d58def2937de426 Mon Sep 17 00:00:00 2001 From: Yanzhen Yu Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] 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. --- package.json | 2 +- src/record/index.ts | 14 ++++++++++++-- src/record/observer.ts | 34 ++++++++++++++++++++++++++++++++++ src/replay/index.ts | 21 +++++++++++++++++++++ src/types.ts | 22 +++++++++++++++++++++- 5 files changed, 89 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 6e5f83bd..6bb44040 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/record/index.ts b/src/record/index.ts index 027af83e..7a2d50b1 100644 --- a/src/record/index.ts +++ b/src/record/index.ts @@ -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, ), diff --git a/src/record/observer.ts b/src/record/observer.ts index 4a426519..875d2d9a 100644 --- a/src/record/observer.ts +++ b/src/record/observer.ts @@ -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) => { if (hooks.mutation) { @@ -562,6 +585,12 @@ function mergeHooks(o: observerParam, hooks: hooksParam) { } inputCb(...p); }; + o.mediaInteractionCb = (...p: Arguments) => { + 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(); }; } diff --git a/src/replay/index.ts b/src/replay/index.ts index b67c2f4c..adb91333 100644 --- a/src/replay/index.ts +++ b/src/replay/index.ts @@ -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: } } diff --git a/src/types.ts b/src/types.ts index e1efafa2..f4305b1e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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;