Skip to content

Commit

Permalink
create VideoPlayerStatsForNerds.vue
Browse files Browse the repository at this point in the history
  • Loading branch information
Williangalvani committed Oct 12, 2024
1 parent e0d1a40 commit 4fcbc6c
Show file tree
Hide file tree
Showing 2 changed files with 198 additions and 0 deletions.
188 changes: 188 additions & 0 deletions src/components/VideoPlayerStatsForNerds.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<template>
<div class="canvas-container">
<canvas ref="canvasRef" :width="width" :height="height"></canvas>
</div>
</template>

<script lang="ts" setup>
import { WebRTCStats } from '@peermetrics/webrtc-stats'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { useVideoStore } from '@/stores/video'
import { WebRTCStatsEvent } from '@/types/video'
const videoStore = useVideoStore()
const props = defineProps({
width: {
type: Number,
default: 110,
},
height: {
type: Number,
default: 160,
},
updateInterval: {
type: Number,
default: 20,
},
streamName: {
type: String,
default: '',
},
})
const canvasRef = ref(null)
const framerateData = ref([])
const bitrateData = ref([])
let animationFrameId = null
let intervalId = null
let bitrate = 0
let packetsLost = 0
let jitterBufferLatency = 0
let freezes = 0
let frozenTime = 0
let framedrops = 0
let framerate = 0
let videoHeight = 0
const maxDataPoints = 100
let maxBitrateReceived = 1000 // Start with a reasonable default
let maxFramerateReceived = 30 // Start with a reasonable default
let absoluteMaxFrameRate = 120 // Adjust this value to change the max framerate
const plotHeight = 60 // Adjust this value to change the height of the plot area
/**
* Normalize the value to fit the plot area
* @param {number} value - The current value
* @param {number} max - The maximum value currently in the plot
* @returns {number} The normalized value
*/
function normalizeValue(value: number, max: number): number {
return (value / max) * plotHeight
}
/**
* Draw the line plots and stats
*/
function draw(): void {
const canvas = canvasRef.value
const ctx = canvas.getContext('2d')
const { width, height } = props
ctx.clearRect(0, 0, width, height)
// Draw bitrate plot
ctx.strokeStyle = 'rgb(255, 165, 0)' // Orange
ctx.lineWidth = 1
ctx.beginPath()
for (let i = 0; i < bitrateData.value.length; i++) {
const x = (i / (maxDataPoints - 1)) * width
const y = height - normalizeValue(bitrateData.value[i], maxBitrateReceived)
if (i === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
}
ctx.stroke()
// Draw framerate plot
ctx.strokeStyle = 'rgb(0, 255, 0)' // Green
ctx.beginPath()
for (let i = 0; i < framerateData.value.length; i++) {
const x = (i / (maxDataPoints - 1)) * width
const y = height - normalizeValue(framerateData.value[i], maxFramerateReceived)
if (i === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
}
ctx.stroke()
// Display stats
const stats = [
{ label: 'Stream', value: props.streamName, color: 'white' },
{ label: 'Size', value: `${videoHeight}p`, color: 'white' },
{ label: 'Packets Lost', value: packetsLost, color: 'white' },
{ label: 'Framedrops', value: framedrops, color: 'white' },
{ label: 'Jitter(ms)', value: jitterBufferLatency.toFixed(0), color: 'white' },
{ label: 'Freezes', value: `${freezes}(${frozenTime.toFixed(1)}s)`, color: 'white' },
{ label: 'Bitrate', value: `${bitrate.toFixed(0)}kbps`, color: 'rgb(255, 165, 0)' },
{ label: 'FPS', value: framerate.toFixed(2), color: 'rgb(0, 255, 0)' },
]
ctx.font = '10px Arial'
stats.forEach((stat, index) => {
ctx.fillStyle = stat.color
ctx.fillText(`${stat.label}: ${stat.value}`, 5, 12 + index * 12)
})
animationFrameId = requestAnimationFrame(draw)
}
const webrtcStats = new WebRTCStats({ getStatsInterval: 100 })
/**
* Draws the lines and updates the stats
*/
function update(): void {
framerateData.value.push(framerate)
bitrateData.value.push(bitrate)
if (framerateData.value.length > maxDataPoints) framerateData.value.shift()
if (bitrateData.value.length > maxDataPoints) bitrateData.value.shift()
// Update max values
maxBitrateReceived = Math.max(maxBitrateReceived, ...bitrateData.value)
maxFramerateReceived = Math.max(maxFramerateReceived, ...framerateData.value)
if (maxFramerateReceived > absoluteMaxFrameRate) maxFramerateReceived = absoluteMaxFrameRate
}
watch(videoStore.activeStreams, (streams): void => {
Object.keys(streams).forEach((streamName) => {
if (streamName !== props.streamName) return
const session = streams[streamName]?.webRtcManager.session
if (!session || !session.peerConnection) return
if (webrtcStats.peersToMonitor[session.consumerId]) return
webrtcStats.addConnection({
pc: session.peerConnection,
peerId: session.consumerId,
connectionId: session.id,
remote: false,
})
})
})
onMounted(() => {
intervalId = setInterval(update, props.updateInterval)
draw()
webrtcStats.on('stats', (ev: WebRTCStatsEvent) => {
try {
const videoData = ev.data.video.inbound[0]
if (videoData === undefined) return
if (videoData.bitrate) {
const newBitrate = videoData.bitrate / 1000
bitrate = bitrate * 0.8 + newBitrate * 0.2
jitterBufferLatency = (1000 * videoData.jitterBufferDelay) / videoData.jitterBufferEmittedCount
packetsLost = videoData.packetsLost
freezes = videoData.freezeCount
frozenTime = videoData.totalFreezesDuration
framedrops = videoData.framesDropped
framerate = videoData.framesPerSecond ?? 0
videoHeight = videoData.frameHeight
}
} catch (e) {
console.error(e)
}
})
})
onUnmounted(() => {
clearInterval(intervalId)
cancelAnimationFrame(animationFrameId)
})
</script>

<style scoped>
.canvas-container {
position: absolute;
top: 50px;
left: 10px;
background-color: rgba(0, 0, 0, 0.5);
z-index: 2;
}
</style>
10 changes: 10 additions & 0 deletions src/components/widgets/VideoPlayer.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<template>
<div ref="videoWidget" class="video-widget">
<statsForNerds v-if="widget.options.statsForNerds" :stream-name="externalStreamId" />
<div v-if="nameSelectedStream === undefined" class="no-video-alert">
<span>No video stream selected.</span>
</div>
Expand Down Expand Up @@ -88,6 +89,13 @@
:color="widget.options.flipVertically ? 'white' : undefined"
hide-details
/>
<v-switch
v-model="widget.options.statsForNerds"
class="my-1"
label="Stats for nerds"
:color="widget.options.statsForNerds ? 'white' : undefined"
hide-details
/>
<div class="flex-wrap justify-center d-flex ga-5">
<v-btn prepend-icon="mdi-file-rotate-left" variant="outlined" @click="rotateVideo(-90)"> Rotate Left</v-btn>
<v-btn prepend-icon="mdi-file-rotate-right" variant="outlined" @click="rotateVideo(+90)"> Rotate Right</v-btn>
Expand All @@ -101,6 +109,7 @@
import { storeToRefs } from 'pinia'
import { computed, onBeforeMount, onBeforeUnmount, ref, toRefs, watch } from 'vue'
import StatsForNerds from '@/components/VideoPlayerStatsForNerds.vue'
import { isEqual } from '@/libs/utils'
import { useAppInterfaceStore } from '@/stores/appInterface'
import { useVideoStore } from '@/stores/video'
Expand Down Expand Up @@ -134,6 +143,7 @@ onBeforeMount(() => {
flipHorizontally: false,
flipVertically: false,
rotationAngle: 0,
statsForNerds: false,
internalStreamName: undefined as string | undefined,
}
widget.value.options = Object.assign({}, defaultOptions, widget.value.options)
Expand Down

0 comments on commit 4fcbc6c

Please sign in to comment.