From 049012485e4fb048c6d638a02702cbc9070e34a5 Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Sun, 25 Oct 2020 18:57:23 -0700 Subject: [PATCH] Add server overview home page --- web/package.json | 1 + web/pages/index.tsx | 177 ++++++++++++++++++++++++++++++-------- web/pages/utils/apis.ts | 9 +- web/pages/utils/format.ts | 14 +++ 4 files changed, 162 insertions(+), 39 deletions(-) create mode 100644 web/pages/utils/format.ts diff --git a/web/package.json b/web/package.json index f9b8e8a85..07c83547d 100644 --- a/web/package.json +++ b/web/package.json @@ -13,6 +13,7 @@ "classnames": "^2.2.6", "d3-scale": "^3.2.3", "d3-time-format": "^3.0.0", + "date-fns": "^2.16.1", "next": "9.5.4", "prop-types": "^15.7.2", "react": "16.13.1", diff --git a/web/pages/index.tsx b/web/pages/index.tsx index 58cbdaaf9..6e5e18476 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -1,43 +1,152 @@ -import React from 'react'; -import { Card, Alert, Statistic, Row, Col } from "antd"; -import { LikeOutlined } from "@ant-design/icons"; +/* +Will display an overview with the following datasources: +1. Current broadcaster. +2. Viewer count. +3. Video settings. -const { Meta } = Card; +TODO: Link each overview value to the sub-page that focuses on it. +*/ + +import React, { useState, useEffect, useContext } from "react"; +import { Statistic, Card, Row, Col, Skeleton, Empty, Typography } from "antd"; +import { UserOutlined, ClockCircleOutlined } from "@ant-design/icons"; +import { formatDistanceToNow, formatRelative } from "date-fns"; +import { BroadcastStatusContext } from "./utils/broadcast-status-context"; +import { + STREAM_STATUS, + SERVER_CONFIG, + fetchData, + FETCH_INTERVAL, +} from "./utils/apis"; +import { formatIPAddress } from "./utils/format"; + +const { Title} = Typography; + +function Item(title: string, value: string, prefix: Jsx.Element) { + const valueStyle = { color: "#334", fontSize: "1.8rem" }; + + return ( + + + + + + ); +} + +export default function Stats() { + const context = useContext(BroadcastStatusContext); + const { broadcaster } = context || {}; + const { remoteAddr, streamDetails } = broadcaster || {}; + + // Pull in the server status so we can show server overview. + const [stats, setStats] = useState(null); + const getStats = async () => { + try { + const result = await fetchData(STREAM_STATUS); + setStats(result); + } catch (error) { + console.log(error); + } + }; + + + // Pull in the server config so we can show config overview. + const [videoSettings, setVideoSettings] = useState([]); + const getConfig = async () => { + try { + const result = await fetchData(SERVER_CONFIG); + setVideoSettings(result.videoSettings.videoQualityVariants); + } catch (error) { + console.log(error); + } + }; + + useEffect(() => { + setInterval(getStats, FETCH_INTERVAL); + getStats(); + getConfig(); + }, []); + + if (!stats) { + return ( +
+ + + +
+ ); + } + + if (!broadcaster) { + return Offline(); + } + + const videoQualitySettings = videoSettings.map(function (setting, index) { + const audioSetting = + setting.audioPassthrough || setting.audioBitrate === 0 + ? `${streamDetails.audioBitrate} kpbs (passthrough)` + : `${setting.audioBitrate} kbps`; + + return ( + + {Item("Output", `Video variant ${index}`, "")} + {Item( + "Outbound Video Stream", + `${setting.videoBitrate} kbps ${setting.framerate} fps`, + "" + )} + {Item("Outbound Audio Stream", audioSetting, "")} + + ); + }); + + const { viewerCount, sessionMaxViewerCount, lastConnectTime } = stats; + const streamVideoDetailString = `${streamDetails.width}x${streamDetails.height} ${streamDetails.videoBitrate} kbps ${streamDetails.framerate} fps `; + const streamAudioDetailString = `${streamDetails.audioCodec} ${streamDetails.audioBitrate} kpbs`; -export default function AdminHome() { return (
-
- < pick something
- Home view. pretty pictures. Rainbows. Kittens. -
+ Server Overview + + {Item( + `Stream started ${formatRelative( + new Date(lastConnectTime), + new Date() + )}`, + formatDistanceToNow(new Date(lastConnectTime)), + + )} -

- - - - - } /> - - - - + {Item("Viewers", viewerCount, )} + {Item("Peak viewer count", sessionMaxViewerCount, )} - - } - > - - + + + {Item("Input", formatIPAddress(remoteAddr), "")} + {Item("Inbound Video Stream", streamVideoDetailString, "")} + {Item("Inbound Audio Stream", streamAudioDetailString, "")} + + + {videoQualitySettings} +
+ ); +} + +function Offline() { + return ( +
+ There is no stream currently active. Start one. + } + />
); } diff --git a/web/pages/utils/apis.ts b/web/pages/utils/apis.ts index 898242ddc..a6b1eade1 100644 --- a/web/pages/utils/apis.ts +++ b/web/pages/utils/apis.ts @@ -29,11 +29,10 @@ export const CONNECTED_CLIENTS = `${API_LOCATION}clients`; // Get hardware stats export const HARDWARE_STATS = `${API_LOCATION}hardwarestats`; - - -// Current Stream status (no auth) -// use `admin/broadcaster` instead -// export const STREAM_STATUS = '/api/status'; +// Current Stream status. +// This is literally the same as /api/status except it supports +// auth. +export const STREAM_STATUS = `${API_LOCATION}status`; export async function fetchData(url) { const encoded = btoa(`${ADMIN_USERNAME}:${ADMIN_STREAMKEY}`); diff --git a/web/pages/utils/format.ts b/web/pages/utils/format.ts new file mode 100644 index 000000000..026746ebd --- /dev/null +++ b/web/pages/utils/format.ts @@ -0,0 +1,14 @@ +export function formatIPAddress(ipAddress: string): string { + const ipAddressComponents = ipAddress.split(':') + + // Wipe out the port component + ipAddressComponents[ipAddressComponents.length - 1] = ''; + + let ip = ipAddressComponents.join(':') + ip = ip.slice(0, ip.length - 1) + if (ip === '[::1]') { + return "Localhost" + } + + return ip; +} \ No newline at end of file