add player controller

This commit is contained in:
Yanzhen Yu
2026-04-01 12:00:00 +08:00
parent b4e983af89
commit edadb57eb4
5 changed files with 206 additions and 59 deletions

View File

@@ -1,6 +1,3 @@
<h1>
rrweb player playground
</h1>
<Player {events} />
<style>

108
src/Controller.html Normal file
View File

@@ -0,0 +1,108 @@
<div class="rr-controller">
<div class="rr-timeline">
<span class="rr-timeline__time">{formatTime(currentTime)}</span>
<div class="rr-progress">
<div class="rr-progress__step" ref:step style="width: {percentage}"></div>
<div class="rr-progress__handler" ref:handler style="left: {percentage}"></div>
</div>
<span class="rr-timeline__time">{formatTime(meta.totalTime)}</span>
</div>
<button>pause</button>
</div>
<script>
import { formatTime } from './utils.js';
export default {
data() {
return {
currentTime: 0,
};
},
computed: {
meta({ replayer }) {
return replayer.getMetaData();
},
percentage({ currentTime, meta }) {
return `${100 * currentTime / meta.totalTime}%`;
},
},
helpers: {
formatTime,
},
methods: {
loopTimer() {
const now = performance.now();
const self = this;
function update(step) {
let { currentTime, meta } = self.get();
currentTime = Math.floor(step - now);
self.set({ currentTime });
if (currentTime < meta.totalTime) {
requestAnimationFrame(update);
}
}
requestAnimationFrame(update);
},
},
onupdate({ changed, current }) {
if (changed.replayer) {
this.loopTimer();
current.replayer.play();
}
},
};
</script>
<style>
.rr-controller {
width: 100%;
height: 80px;
background: rgba(0, 0, 0, .5);
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
padding: 10px;
}
.rr-timeline {
width: 80%;
display: flex;
align-items: center
}
.rr-timeline__time {
padding: 0 20px;
color: white;
}
.rr-progress {
width: 100%;
height: 4px;
background: white;
position: relative;
border-radius: 3px;
}
.rr-progress__step {
height: 100%;
position: absolute;
left: 0;
top: 0;
background: orange
}
.rr-progress__handler {
width: 20px;
height: 20px;
border-radius: 10px;
position: absolute;
top: 2px;
transform: translate(-50%, -50%);
background: orange;
}
</style>

View File

@@ -1,10 +1,65 @@
<div class="rr-player">
<div class="rr-player__frame" ref:frame { style }></div>
{#if replayer}
<Controller { replayer } />
{/if}
</div>
<script>
import { Replayer } from 'rrweb';
import { inlineCss } from './utils.js';
export default {
components: {
Controller: './Controller.html',
},
data() {
return {
width: 1024,
height: 576,
events: [],
replayer: null,
};
},
computed: {
style({ width, height }) {
return inlineCss({
width: `${width}px`,
height: `${height}px`,
});
},
},
methods: {
updateScale(el, frameDimension) {
const { width, height } = this.get();
const widthScale = (width - 20) / frameDimension.width;
const heightScale = (height - 20) / frameDimension.height;
el.style.transform =
`scale(${Math.min(widthScale, heightScale, 1)})` +
'translate(-50%, -50%)';
},
},
oncreate(p) {
const { events } = this.get();
const replayer = new Replayer(events, {
speed: 1,
root: this.refs.frame,
});
replayer.on('resize', (dimension) =>
this.updateScale(replayer.wrapper, dimension)
);
this.set({
replayer,
});
},
};
</script>
<style>
.rr-player {
border: 3px solid indianred;
background: white;
float: left;
clear: both;
}
@@ -17,9 +72,14 @@
float: left;
clear: both;
transform-origin: top left;
left: 50%;
top: 50%;
border: 2px solid grey;
left: calc(50% - 10px);
top: calc(50% - 10px);
margin: 10px;
box-shadow: 0 3px 28px rgba(0, 0, 0, 0.25), 0 1px 10px rgba(0, 0, 0, 0.22);
}
:global(.replayer-wrapper > iframe) {
border: none;
}
/* get from rrweb */
@@ -43,48 +103,4 @@
transform: translate(-10px, -10px);
opacity: 0.5;
}
</style>
<script>
import { Replayer } from 'rrweb';
import { inlineCss } from './util.js';
export default {
data() {
return {
width: 1024,
height: 576,
events: [],
};
},
computed: {
style({ width, height }) {
return inlineCss({
width: `${width}px`,
height: `${height}px`,
});
},
},
methods: {
updateScale(el, frameDimension) {
const { width, height } = this.get();
const widthScale = width / frameDimension.width;
const heightScale = height / frameDimension.height;
el.style.transform =
`scale(${Math.min(widthScale, heightScale, 1)})` +
'translate(-50%, -50%)';
},
},
oncreate(p) {
const { events } = this.get();
const replayer = new Replayer(events, {
speed: 1,
root: this.refs.frame,
onResize: (dimension) => this.updateScale(replayer.wrapper, dimension),
});
replayer.play();
},
};
</script>
</style>

View File

@@ -1,7 +0,0 @@
export function inlineCss(cssObj) {
let style = '';
Object.keys(cssObj).forEach(key => {
style += `${key}: ${cssObj[key]};`;
});
return style;
}

33
src/utils.js Normal file
View File

@@ -0,0 +1,33 @@
export function inlineCss(cssObj) {
let style = '';
Object.keys(cssObj).forEach(key => {
style += `${key}: ${cssObj[key]};`;
});
return style;
}
function padZero(num, len = 2) {
const threshold = Math.pow(10, len - 1);
if (num < threshold) {
num = String(num);
while (String(threshold).length > num.length) {
num = '0' + num;
}
}
return num;
}
const SECOND = 1000;
const MINUTE = 60 * SECOND;
const HOUR = 60 * MINUTE;
export function formatTime(ms) {
const hour = Math.floor(ms / HOUR);
ms = ms % HOUR;
const minute = Math.floor(ms / MINUTE);
ms = ms % MINUTE;
const second = Math.floor(ms / SECOND);
if (hour) {
return `${padZero(hour)}:${padZero(minute)}:${padZero(minute)}`;
}
return `${padZero(hour)}:${padZero(second)}`;
}