2022-03-16 17:54:34 -07:00
/* eslint-disable react/no-unescaped-entities */
// import { BulbOutlined, LaptopOutlined, SaveOutlined } from '@ant-design/icons';
import { Row , Col , Typography , Space , Statistic , Card , Alert } from 'antd' ;
import React , { ReactNode , useEffect , useState } from 'react' ;
import { ClockCircleOutlined , WarningOutlined , WifiOutlined } from '@ant-design/icons' ;
import { fetchData , FETCH_INTERVAL , API_STREAM_HEALTH_METRICS } from '../utils/apis' ;
import Chart from '../components/chart' ;
interface TimedValue {
time : Date ;
value : Number ;
}
interface DescriptionBoxProps {
title : String ;
description : ReactNode ;
}
function DescriptionBox ( { title , description } : DescriptionBoxProps ) {
return (
< div className = "description-box" >
< Typography.Title > { title } < / Typography.Title >
< Typography.Paragraph > { description } < / Typography.Paragraph >
< / div >
) ;
}
export default function StreamHealth() {
const [ errors , setErrors ] = useState < TimedValue [ ] > ( [ ] ) ;
const [ qualityVariantChanges , setQualityVariantChanges ] = useState < TimedValue [ ] > ( [ ] ) ;
2022-03-16 22:49:38 -07:00
const [ lowestLatency , setLowestLatency ] = useState < TimedValue [ ] > ( ) ;
const [ highestLatency , setHighestLatency ] = useState < TimedValue [ ] > ( ) ;
const [ medianLatency , setMedianLatency ] = useState < TimedValue [ ] > ( [ ] ) ;
const [ medianSegmentDownloadDurations , setMedianSegmentDownloadDurations ] = useState <
TimedValue [ ]
> ( [ ] ) ;
const [ maximumSegmentDownloadDurations , setMaximumSegmentDownloadDurations ] = useState <
TimedValue [ ]
> ( [ ] ) ;
const [ minimumSegmentDownloadDurations , setMinimumSegmentDownloadDurations ] = useState <
TimedValue [ ]
> ( [ ] ) ;
2022-03-16 17:54:34 -07:00
const [ minimumPlayerBitrate , setMinimumPlayerBitrate ] = useState < TimedValue [ ] > ( [ ] ) ;
2022-03-16 22:49:38 -07:00
const [ medianPlayerBitrate , setMedianPlayerBitrate ] = useState < TimedValue [ ] > ( [ ] ) ;
const [ maximumPlayerBitrate , setMaximumPlayerBitrate ] = useState < TimedValue [ ] > ( [ ] ) ;
2022-03-16 17:54:34 -07:00
const [ availableBitrates , setAvailableBitrates ] = useState < Number [ ] > ( [ ] ) ;
const [ segmentLength , setSegmentLength ] = useState ( 0 ) ;
const getMetrics = async ( ) = > {
try {
const result = await fetchData ( API_STREAM_HEALTH_METRICS ) ;
setErrors ( result . errors ) ;
setQualityVariantChanges ( result . qualityVariantChanges ) ;
2022-03-16 22:49:38 -07:00
setHighestLatency ( result . highestLatency ) ;
setLowestLatency ( result . lowestLatency ) ;
setMedianLatency ( result . medianLatency ) ;
setMedianSegmentDownloadDurations ( result . medianSegmentDownloadDuration ) ;
setMaximumSegmentDownloadDurations ( result . maximumSegmentDownloadDuration ) ;
setMinimumSegmentDownloadDurations ( result . minimumSegmentDownloadDuration ) ;
2022-03-16 17:54:34 -07:00
setMinimumPlayerBitrate ( result . minPlayerBitrate ) ;
2022-03-16 22:49:38 -07:00
setMedianPlayerBitrate ( result . medianPlayerBitrate ) ;
setMaximumPlayerBitrate ( result . maxPlayerBitrate ) ;
2022-03-16 17:54:34 -07:00
setAvailableBitrates ( result . availableBitrates ) ;
setSegmentLength ( result . segmentLength - 0.3 ) ;
} catch ( error ) {
console . error ( error ) ;
}
} ;
useEffect ( ( ) = > {
let getStatusIntervalId = null ;
getMetrics ( ) ;
getStatusIntervalId = setInterval ( getMetrics , FETCH_INTERVAL ) ; // runs every 1 min.
// returned function will be called on component unmount
return ( ) = > {
clearInterval ( getStatusIntervalId ) ;
} ;
} , [ ] ) ;
const noData = (
< div >
< Typography.Title > Stream Performance < / Typography.Title >
< Typography.Paragraph >
Data has not yet been collected . Once a stream has begun and viewers are watching this page
will be available .
< / Typography.Paragraph >
< / div >
) ;
if ( ! errors ? . length ) {
return noData ;
}
2022-03-16 22:49:38 -07:00
if ( ! medianLatency ? . length ) {
2022-03-16 17:54:34 -07:00
return noData ;
}
2022-03-16 22:49:38 -07:00
if ( ! medianSegmentDownloadDurations ? . length ) {
2022-03-16 17:54:34 -07:00
return noData ;
}
const errorChart = [
{
name : 'Errors' ,
color : '#B63FFF' ,
options : { radius : 3 } ,
data : errors ,
} ,
{
name : 'Quality changes' ,
color : '#2087E2' ,
options : { radius : 2 } ,
data : qualityVariantChanges ,
} ,
] ;
const latencyChart = [
{
2022-03-16 22:49:38 -07:00
name : 'Median stream latency' ,
color : '#00FFFF' ,
options : { radius : 2 } ,
data : medianLatency ,
} ,
{
name : 'Lowest stream latency' ,
color : '#02FD0D' ,
options : { radius : 2 } ,
data : lowestLatency ,
} ,
{
name : 'Highest stream latency' ,
2022-03-16 17:54:34 -07:00
color : '#B63FFF' ,
options : { radius : 2 } ,
2022-03-16 22:49:38 -07:00
data : highestLatency ,
2022-03-16 17:54:34 -07:00
} ,
] ;
const segmentDownloadDurationChart = [
{
2022-03-16 22:49:38 -07:00
name : 'Median download duration' ,
color : '#00FFFF' ,
options : { radius : 2 } ,
data : medianSegmentDownloadDurations ,
} ,
{
name : 'Max download duration' ,
2022-03-16 17:54:34 -07:00
color : '#B63FFF' ,
options : { radius : 2 } ,
2022-03-16 22:49:38 -07:00
data : maximumSegmentDownloadDurations ,
} ,
{
name : 'Min download duration' ,
color : '#02FD0D' ,
options : { radius : 2 } ,
data : minimumSegmentDownloadDurations ,
2022-03-16 17:54:34 -07:00
} ,
{
name : ` Approximate limit ` ,
color : '#003FFF' ,
2022-03-16 22:49:38 -07:00
data : medianSegmentDownloadDurations.map ( item = > ( {
2022-03-16 17:54:34 -07:00
time : item.time ,
value : segmentLength ,
} ) ) ,
options : { radius : 0 } ,
} ,
] ;
const bitrateChart = [
{
name : 'Lowest viewer bitrate' ,
color : '#B63FFF' ,
data : minimumPlayerBitrate ,
options : { radius : 2 } ,
} ,
2022-03-16 22:49:38 -07:00
{
name : 'Median viewer bitrate' ,
color : '#00FFFF' ,
data : medianPlayerBitrate ,
options : { radius : 2 } ,
} ,
{
name : 'Maximum viewer bitrate' ,
color : '#02FD0D' ,
data : maximumPlayerBitrate ,
options : { radius : 2 } ,
} ,
2022-03-16 17:54:34 -07:00
] ;
availableBitrates . forEach ( bitrate = > {
bitrateChart . push ( {
name : ` ${ bitrate } kbps stream ` ,
color : '#003FFF' ,
data : minimumPlayerBitrate.map ( item = > ( {
time : item.time ,
value : 1200 ,
} ) ) ,
options : { radius : 0 } ,
} ) ;
} ) ;
const currentSpeed = bitrateChart [ 0 ] ? . data [ bitrateChart [ 0 ] . data . length - 1 ] ? . value ;
const currentDownloadSeconds =
2022-03-16 22:49:38 -07:00
medianSegmentDownloadDurations [ medianSegmentDownloadDurations . length - 1 ] ? . value ;
2022-03-16 17:54:34 -07:00
const lowestVariant = availableBitrates [ 0 ] ; // TODO: get lowest bitrate from available bitrates
2022-03-16 22:49:38 -07:00
const latencyMedian = medianLatency [ medianLatency . length - 1 ] ? . value || 0 ;
const latencyMax = highestLatency [ highestLatency . length - 1 ] ? . value || 0 ;
const latencyMin = lowestLatency [ lowestLatency . length - 1 ] ? . value || 0 ;
const latencyStat = ( Number ( latencyMax ) + Number ( latencyMin ) + Number ( latencyMedian ) ) / 3 ;
2022-03-16 17:54:34 -07:00
let recentErrorCount = 0 ;
const errorValueCount = errorChart [ 0 ] ? . data . length || 0 ;
if ( errorValueCount > 5 ) {
const values = errorChart [ 0 ] . data . slice ( - 5 ) ;
recentErrorCount = values . reduce ( ( acc , curr ) = > acc + Number ( curr . value ) , 0 ) ;
} else {
recentErrorCount = errorChart [ 0 ] . data . reduce ( ( acc , curr ) = > acc + Number ( curr . value ) , 0 ) ;
}
const showStats = currentSpeed > 0 || currentDownloadSeconds > 0 || recentErrorCount > 0 ;
let bitrateError = null ;
let speedError = null ;
if ( currentSpeed !== 0 && currentSpeed < lowestVariant ) {
bitrateError = ` At least one of your viewers is playing your stream at ${ currentSpeed } kbps, slower than ${ lowestVariant } kbps, the lowest quality you made available, experiencing buffering. Consider adding a lower quality with a lower bitrate if the errors over time warrant this. ` ;
}
if ( currentDownloadSeconds > segmentLength ) {
speedError =
'Your viewers may be consuming your video slower than required. This may be due to slow networks or your latency configuration. Consider adding a lower quality with a lower bitrate or experiment with increasing the latency buffer setting.' ;
}
const errorStatColor = recentErrorCount > 0 ? '#B63FFF' : '#FFFFFF' ;
const statStyle = {
display : 'flex' ,
alignItems : 'center' ,
justifyContent : 'center' ,
height : '80px' ,
} ;
return (
< >
< Typography.Title > Stream Performance < / Typography.Title >
< Typography.Paragraph >
This tool hopes to help you identify and troubleshoot problems you may be experiencing with
your stream . It aims to aggregate experiences across your viewers , meaning one viewer with
an exceptionally bad experience may throw off numbers for the whole , especially with a low
number of viewers .
< / Typography.Paragraph >
< Typography.Paragraph >
The data is only collected by those using the Owncast web interface and is unable to gain
insight into external players people may be using such as VLC , MPV , QuickTime , etc .
< / Typography.Paragraph >
< Space direction = "vertical" size = "middle" >
< Row
gutter = { [ 16 , 16 ] }
justify = "space-around"
style = { { display : showStats ? 'flex' : 'none' } }
>
< Col >
< Card type = "inner" >
< div style = { statStyle } >
< Statistic
2022-03-16 22:49:38 -07:00
title = "Viewer Playback Speed"
2022-03-16 17:54:34 -07:00
value = { ` ${ currentSpeed } ` }
prefix = { < WifiOutlined style = { { marginRight : '5px' } } / > }
precision = { 0 }
suffix = "kbps"
/ >
< / div >
< / Card >
< / Col >
< Col >
< Card type = "inner" >
< div style = { statStyle } >
< Statistic
2022-03-16 22:49:38 -07:00
title = "Viewer Latency"
2022-03-16 17:54:34 -07:00
value = { ` ${ latencyStat } ` }
prefix = { < ClockCircleOutlined style = { { marginRight : '5px' } } / > }
precision = { 0 }
suffix = "seconds"
/ >
< / div >
< / Card >
< / Col >
< Col >
< Card type = "inner" >
< div style = { statStyle } >
< Statistic
title = "Recent Playback Errors"
value = { ` ${ recentErrorCount || 0 } ` }
valueStyle = { { color : errorStatColor } }
prefix = { < WarningOutlined style = { { marginRight : '5px' } } / > }
suffix = ""
/ > { ' ' }
< / div >
< / Card >
< / Col >
< / Row >
< Card >
< DescriptionBox
title = "Video Segment Download"
description = {
< >
< Typography.Paragraph >
Once a video segment takes too long to download a viewer will experience
buffering . If you see slow downloads you should offer a lower quality for your
viewers , or find other ways , possibly an external storage provider , a CDN or a
faster network , to improve your stream quality . Increasing your latency buffer can
also help for some viewers .
< / Typography.Paragraph >
< Typography.Paragraph >
In short , once the pink line consistently gets near the blue line , your stream is
likely experiencing problems for viewers .
< / Typography.Paragraph >
< / >
}
/ >
{ speedError && (
< Alert message = "Slow downloads" description = { speedError } type = "error" showIcon / >
) }
< Chart
title = "Seconds"
dataCollections = { segmentDownloadDurationChart }
color = "#FF7700"
unit = "s"
yLogarithmic
/ >
< / Card >
< Card >
< DescriptionBox
title = "Player Network Speed"
description = {
< >
< Typography.Paragraph >
2022-03-16 22:49:38 -07:00
The playback bitrate of your viewers . Once somebody ' s bitrate drops below the
lowest video variant bitrate they will experience buffering . If you see viewers
with slow connections trying to play your video you should consider offering an
additional , lower quality .
2022-03-16 17:54:34 -07:00
< / Typography.Paragraph >
< Typography.Paragraph >
In short , once the pink line gets near the lowest blue line , your stream is likely
experiencing problems for at least one of your viewers .
< / Typography.Paragraph >
< / >
}
/ >
{ bitrateError && (
< Alert
message = "Low bandwidth viewers"
description = { bitrateError }
type = "error"
showIcon
/ >
) }
< Chart
title = "Lowest Player Bitrate"
dataCollections = { bitrateChart }
color = "#FF7700"
unit = "kbps"
yLogarithmic
/ >
< / Card >
< Card >
< DescriptionBox
title = "Errors and Quality Changes"
description = {
< >
< Typography.Paragraph >
Recent number of errors , including buffering , and quality changes from across all
your viewers . Errors can occur for many reasons , including browser issues ,
plugins , wifi problems , and they don ' t all represent fatal issues or something you
have control over .
< / Typography.Paragraph >
A quality change is not necessarily a negative thing , but if it ' s excessive and
coinciding with errors you should consider adding another quality variant .
< Typography.Paragraph / >
< / >
}
/ >
< Chart title = "#" dataCollections = { errorChart } color = "#FF7700" unit = "" / >
< / Card >
< Card >
< DescriptionBox
2022-03-16 22:49:38 -07:00
title = "Viewer Latency"
description = "An approximate number of seconds that your viewers are behind your live video. The more people buffer the further behind they will be. High latency itself is not a problem, but some people care about this more than others."
2022-03-16 17:54:34 -07:00
/ >
< Chart title = "Seconds" dataCollections = { latencyChart } color = "#FF7700" unit = "s" / >
< / Card >
< / Space >
< / >
) ;
}