Skip to content

Commit

Permalink
Implement: Inactive activity indicator on progress bar (#1039)
Browse files Browse the repository at this point in the history
* update utils.ts: add a tool function to detect inactive periods

* update Controller.svelte: add a fixed div element as an indicator

* update Controller.svelte: add one blank space at the end

* update Controller.svelte: add a variable inactivePeriods and use util function to get inactive periods

* update Controller.svelte: add width property for inactive activity indicators

* update Controller.svelte: combine calculation value with indicator UI

* update utils.ts: fix error HurricaHjz/rrweb_2120_ga_3#5 (comment) and add comments

update Controller.svelte: apply Zihan's suggestion HurricaHjz/rrweb_2120_ga_3#5 (comment)

* update Controller.svelte: make the color of indicator customizable

update index.d.ts: add type definition for the color option

Co-authored-by: u7149141 <[email protected]>
Co-authored-by: Jerry Zhang <[email protected]>
Co-authored-by: fengyun5264 <[email protected]>
Co-authored-by: Zihan Meng <[email protected]>
Co-authored-by: HurricaHjz <[email protected]>
Co-authored-by: u6924169 <[email protected]>
Co-authored-by: Majia0712 <[email protected]>
  • Loading branch information
8 people authored and Vadman97 committed Nov 4, 2022
1 parent 4a5e4cf commit 35d49f0
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 34 deletions.
128 changes: 94 additions & 34 deletions packages/rrweb-player/src/Controller.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
createEventDispatcher,
afterUpdate,
} from 'svelte';
import { formatTime } from './utils';
import { formatTime, getInactivePeriods } from './utils';
import Switch from './components/Switch.svelte';
const dispatch = createEventDispatcher();
Expand All @@ -24,6 +24,7 @@
export let speedOption: number[];
export let speed = speedOption.length ? speedOption[0] : 1;
export let tags: Record<string, string> = {};
export let inactiveColor: string;
let currentTime = 0;
$: {
Expand Down Expand Up @@ -59,6 +60,21 @@
background: string;
position: string;
};
/**
* Calculate the tag position (percent) to be displayed on the progress bar.
* @param startTime - The start time of the session.
* @param endTime - The end time of the session.
* @param tagTime - The time of the tag.
* @returns The position of the tag. unit: percentage
*/
function position(startTime: number, endTime: number, tagTime: number) {
const sessionDuration = endTime - startTime;
const eventDuration = endTime - tagTime;
const eventPosition = 100 - (eventDuration / sessionDuration) * 100;
return eventPosition.toFixed(2);
}
let customEvents: CustomEvent[];
$: customEvents = (() => {
const { context } = replayer.service.state;
Expand All @@ -67,15 +83,6 @@
const end = context.events[totalEvents - 1].timestamp;
const customEvents: CustomEvent[] = [];
// calculate tag position.
const position = (startTime: number, endTime: number, tagTime: number) => {
const sessionDuration = endTime - startTime;
const eventDuration = endTime - tagTime;
const eventPosition = 100 - (eventDuration / sessionDuration) * 100;
return eventPosition.toFixed(2);
};
// loop through all the events and find out custom event.
context.events.forEach((event) => {
/**
Expand All @@ -95,6 +102,43 @@
return customEvents;
})();
let inactivePeriods: {
name: string;
background: string;
position: string;
width: string;
}[];
$: inactivePeriods = (() => {
try {
const { context } = replayer.service.state;
const totalEvents = context.events.length;
const start = context.events[0].timestamp;
const end = context.events[totalEvents - 1].timestamp;
const periods = getInactivePeriods(context.events);
// calculate the indicator width.
const getWidth = (
startTime: number,
endTime: number,
tagStart: number,
tagEnd: number,
) => {
const sessionDuration = endTime - startTime;
const eventDuration = tagEnd - tagStart;
const width = (eventDuration / sessionDuration) * 100;
return width.toFixed(2);
};
return periods.map((period) => ({
name: 'inactive period',
background: inactiveColor,
position: `${position(start, end, period[0])}%`,
width: `${getWidth(start, end, period[0], period[1])}%`,
}));
} catch (e) {
// For safety concern, if there is any error, the main function won't be affected.
return [];
}
})();
const loopTimer = () => {
stopTimer();
Expand Down Expand Up @@ -174,11 +218,11 @@
};
export const playRange = (
timeOffset: number,
endTimeOffset: number,
startLooping: boolean = false,
afterHook: undefined | (() => void) = undefined,
) => {
timeOffset: number,
endTimeOffset: number,
startLooping: boolean = false,
afterHook: undefined | (() => void) = undefined,
) => {
if (startLooping) {
loop = {
start: timeOffset,
Expand All @@ -193,7 +237,6 @@
replayer.play(timeOffset);
};
const handleProgressClick = (event: MouseEvent) => {
if (speedState === 'skipping') {
return;
Expand All @@ -207,7 +250,7 @@
percent = 1;
}
const timeOffset = meta.totalTime * percent;
finished = false
finished = false;
goto(timeOffset);
};
Expand All @@ -230,18 +273,18 @@
export const triggerUpdateMeta = () => {
return Promise.resolve().then(() => {
meta = replayer.getMetaData();
})
}
});
};
onMount(() => {
playerState = replayer.service.state.value;
speedState = replayer.speedService.state.value ;
speedState = replayer.speedService.state.value;
replayer.on(
'state-change',
(states: { player?: PlayerMachineState; speed?: SpeedMachineState }) => {
const { player, speed } = states;
if (player?.value && playerState !== player.value) {
playerState = player.value ;
playerState = player.value;
switch (playerState) {
case 'playing':
loopTimer();
Expand All @@ -254,7 +297,7 @@
}
}
if (speed?.value && speedState !== speed.value) {
speedState = speed.value ;
speedState = speed.value;
}
},
);
Expand Down Expand Up @@ -384,17 +427,27 @@
class="rr-progress"
class:disabled={speedState === 'skipping'}
bind:this={progress}
on:click={handleProgressClick}>
on:click={handleProgressClick}
>
<div
class="rr-progress__step"
bind:this={step}
style="width: {percentage}" />
style="width: {percentage}"
/>
{#each inactivePeriods as period}
<div
title={period.name}
style="width: {period.width};height: 4px;position: absolute;background: {period.background};left:
{period.position};"
/>
{/each}
{#each customEvents as event}
<div
title={event.name}
style="width: 10px;height: 5px;position: absolute;top:
2px;transform: translate(-50%, -50%);background: {event.background};left:
{event.position};" />
{event.position};"
/>
{/each}

<div class="rr-progress__handler" style="left: {percentage}" />
Expand All @@ -411,7 +464,8 @@
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="16"
height="16">
height="16"
>
<path
d="M682.65984 128q53.00224 0 90.50112 37.49888t37.49888 90.50112l0
512q0 53.00224-37.49888 90.50112t-90.50112
Expand All @@ -426,7 +480,8 @@
12.4928-30.16704l0-512q0-17.67424-12.4928-30.16704t-30.16704-12.4928zM682.65984
213.34016q-17.67424 0-30.16704 12.4928t-12.4928 30.16704l0 512q0
17.67424 12.4928 30.16704t30.16704 12.4928 30.16704-12.4928
12.4928-30.16704l0-512q0-17.67424-12.4928-30.16704t-30.16704-12.4928z" />
12.4928-30.16704l0-512q0-17.67424-12.4928-30.16704t-30.16704-12.4928z"
/>
</svg>
{:else}
<svg
Expand All @@ -436,26 +491,30 @@
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="16"
height="16">
height="16"
>
<path
d="M170.65984 896l0-768 640 384zM644.66944
512l-388.66944-233.32864 0 466.65728z" />
512l-388.66944-233.32864 0 466.65728z"
/>
</svg>
{/if}
</button>
{#each speedOption as s}
<button
class:active={s === speed && speedState !== 'skipping'}
on:click={() => setSpeed(s)}
disabled={speedState === 'skipping'}>
disabled={speedState === 'skipping'}
>
{s}x
</button>
{/each}
<Switch
id="skip"
bind:checked={skipInactive}
disabled={speedState === 'skipping'}
label="skip inactive" />
label="skip inactive"
/>
<button on:click={() => dispatch('fullscreen')}>
<svg
class="icon"
Expand All @@ -464,10 +523,10 @@
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="16"
height="16">
height="16"
>
<defs>
<style type="text/css">
</style>
</defs>
<path
Expand All @@ -478,7 +537,8 @@
48s-21.6 48-48 48l-224 0c-26.4 0-48-21.6-48-48l0-224c0-26.4 21.6-48
48-48 26.4 0 48 21.6 48 48L164 792l253.6-253.6c18.4-18.4 48.8-18.4
68 0 18.4 18.4 18.4 48.8 0 68L231.2 860z"
p-id="1286" />
p-id="1286"
/>
</svg>
</button>
</div>
Expand Down
3 changes: 3 additions & 0 deletions packages/rrweb-player/src/Player.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
export let speed = 1;
export let showController = true;
export let tags: Record<string, string> = {};
// color of inactive periods indicator
export let inactiveColor = '#D4D4D4';
let replayer: Replayer;
Expand Down Expand Up @@ -229,6 +231,7 @@
{speedOption}
{skipInactive}
{tags}
{inactiveColor}
on:fullscreen={() => toggleFullscreen()}
/>
{/if}
Expand Down
40 changes: 40 additions & 0 deletions packages/rrweb-player/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ declare global {
}
}

import { EventType, IncrementalSource } from 'rrweb';
import type { eventWithTime } from 'rrweb/typings/types';

export function inlineCss(cssObj: Record<string, string>): string {
let style = '';
Object.keys(cssObj).forEach((key) => {
Expand Down Expand Up @@ -141,3 +144,40 @@ export function typeOf(
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
return map[toString.call(obj)];
}

/**
* Forked from 'rrweb' replay/index.ts. The original function is not exported.
* Determine whether the event is a user interaction event
* @param event - event to be determined
* @returns true if the event is a user interaction event
*/
function isUserInteraction(event: eventWithTime): boolean {
if (event.type !== EventType.IncrementalSnapshot) {
return false;
}
return (
event.data.source > IncrementalSource.Mutation &&
event.data.source <= IncrementalSource.Input
);
}

// Forked from 'rrweb' replay/index.ts. A const threshold of inactive time.
const SKIP_TIME_THRESHOLD = 10 * 1000;

/**
* Get periods of time when no user interaction happened from a list of events.
* @param events - all events
* @returns periods of time consist with [start time, end time]
*/
export function getInactivePeriods(events: eventWithTime[]) {
const inactivePeriods: [number, number][] = [];
let lastActiveTime = events[0].timestamp;
for (const event of events) {
if (!isUserInteraction(event)) continue;
if (event.timestamp - lastActiveTime > SKIP_TIME_THRESHOLD) {
inactivePeriods.push([lastActiveTime, event.timestamp]);
}
lastActiveTime = event.timestamp;
}
return inactivePeriods;
}
5 changes: 5 additions & 0 deletions packages/rrweb-player/typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ export type RRwebPlayerOptions = {
* @defaultValue `{}`
*/
tags?: Record<string, string>;
/**
* Customize the color of inactive periods indicator in the progress bar with a valid CSS color string.
* @defaultValue `#D4D4D4`
*/
inactiveColor?: string;
} & Partial<playerConfig>;
};

Expand Down

0 comments on commit 35d49f0

Please sign in to comment.