You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
245 lines
8.6 KiB
245 lines
8.6 KiB
// Copyright 2023 Simon Boyé
|
|
|
|
|
|
const STAT_URL = "http://109.18.229.148:8080/stat"
|
|
const RTMP_URL = "rtmp://109.18.229.148"
|
|
const APP_NAME = "nfortv"
|
|
|
|
const KNOWN_STREAMS = [
|
|
"alia",
|
|
"draklaw",
|
|
"draklia",
|
|
"drfred",
|
|
]
|
|
|
|
|
|
const mount_point = document.getElementById("stat")
|
|
render_stat(mount_point)
|
|
|
|
|
|
async function render_stat(mount_point) {
|
|
const g = create_element
|
|
|
|
const stat = parse_stat(await fetch_stat())
|
|
console.log(stat)
|
|
|
|
const streams = stat.applications[APP_NAME].streams
|
|
console.log(streams)
|
|
|
|
for (let stream_name of KNOWN_STREAMS) {
|
|
if (streams[stream_name] === undefined) {
|
|
streams[stream_name] = {
|
|
name: stream_name,
|
|
time: 0,
|
|
bw_in: 0,
|
|
bytes_in: 0,
|
|
bw_out: 0,
|
|
bytes_out: 0,
|
|
bw_audio: 0,
|
|
bw_video: 0,
|
|
audio: null,
|
|
video: null,
|
|
publisher: false,
|
|
viewers: [],
|
|
}
|
|
}
|
|
}
|
|
|
|
const elem = Object.keys(streams).length?
|
|
g("ul", {"class": "streams"},
|
|
...Object.entries(stat.applications[APP_NAME].streams).sort().map(([name, app]) =>
|
|
g("li", {},
|
|
g("div",
|
|
{
|
|
"class": app.publisher? "stream_status online": "stream_status offline",
|
|
title: app.publisher? "Online": "Offline",
|
|
},
|
|
),
|
|
g("span", {},
|
|
g("span", {"class": "stream_name"},
|
|
g("a", {href: `${RTMP_URL}/${APP_NAME}/${name}`}, name),
|
|
),
|
|
" ",
|
|
g("span", {"class": "stream_info"},
|
|
`[${app.viewers.length} viewers]`,
|
|
),
|
|
),
|
|
// g("video", {
|
|
// src: `${RTMP_URL}/${APP_NAME}/${name}`,
|
|
// controls: true,
|
|
// })
|
|
),
|
|
),
|
|
):
|
|
g("p", {"class": "placeholder"}, "No streams are playing currently.")
|
|
|
|
if (mount_point.childNodes.length == 0) {
|
|
mount_point.appendChild(elem)
|
|
}
|
|
else {
|
|
mount_point.replaceChild(elem, mount_point.lastChild)
|
|
}
|
|
}
|
|
|
|
|
|
function parse_stat(xml) {
|
|
const stat = {
|
|
// applications: Object.fromEntries(Array.prototype.map.call(get_xml_items(xml, "rtmp>server>application"), parse_stat_application)),
|
|
applications: Object.fromEntries(get_xml_items(xml, "rtmp>server>application", parse_stat_application)),
|
|
}
|
|
|
|
set_item_if_present(stat, "nginx_version", xml, "rtmp>nginx_version", get_xml_string)
|
|
set_item_if_present(stat, "nginx_rtmp_version", xml, "rtmp>nginx_rtmp_version", get_xml_string)
|
|
set_item_if_present(stat, "built", xml, "rtmp>built", get_xml_string)
|
|
set_item_if_present(stat, "pid", xml, "rtmp>pid", get_xml_number)
|
|
set_item_if_present(stat, "uptime", xml, "rtmp>uptime", get_xml_number)
|
|
set_item_if_present(stat, "naccepted", xml, "rtmp>naccepted", get_xml_number)
|
|
set_item_if_present(stat, "bw_in", xml, "rtmp>bw_in", get_xml_number)
|
|
set_item_if_present(stat, "bytes_in", xml, "rtmp>bytes_in", get_xml_number)
|
|
set_item_if_present(stat, "bw_out", xml, "rtmp>bw_out", get_xml_number)
|
|
set_item_if_present(stat, "bytes_out", xml, "rtmp>bytes_out", get_xml_number)
|
|
|
|
return stat
|
|
}
|
|
|
|
function parse_stat_application(xml) {
|
|
return [
|
|
xml.querySelector("application>name").firstChild.data,
|
|
{
|
|
// nclients: +xml.querySelector("application>live>nclients").firstChild.data,
|
|
// streams: Object.fromEntries(Array.prototype.map.call(xml.querySelectorAll("application>live>stream") || [], parse_stat_stream))
|
|
streams: Object.fromEntries(get_xml_items(xml, "application>live>stream", parse_stat_stream)),
|
|
}
|
|
]
|
|
}
|
|
|
|
function parse_stat_stream(xml) {
|
|
// const clients = Array.prototype.map.call(get_xml_items(xml, "stream>client"), parse_stat_client)
|
|
const clients = get_xml_items(xml, "stream>client", parse_stat_client)
|
|
const publishers = clients.filter(client => client.is_publishing)
|
|
const stream = {
|
|
name: get_xml_item(xml, "stream>name", get_xml_string),
|
|
publisher: publishers.length? publishers[0]: null,
|
|
viewers: clients.filter(client => !client.is_publishing),
|
|
}
|
|
|
|
// const audio = xml.querySelector("stream>meta>audio")?
|
|
// {
|
|
// codec: xml.querySelector("stream>meta>audio>codec").firstChild.data,
|
|
// profile: xml.querySelector("stream>meta>audio>profile").firstChild.data,
|
|
// channels: +xml.querySelector("stream>meta>audio>channels").firstChild.data,
|
|
// sample_rate: +xml.querySelector("stream>meta>audio>sample_rate").firstChild.data,
|
|
// }:
|
|
// null
|
|
// const video = xml.querySelector("stream>meta>video")?
|
|
// {
|
|
// width: +xml.querySelector("stream>meta>video>width").firstChild.data,
|
|
// height: +xml.querySelector("stream>meta>video>height").firstChild.data,
|
|
// frame_rate: +xml.querySelector("stream>meta>video>frame_rate").firstChild.data,
|
|
// codec: xml.querySelector("stream>meta>video>codec").firstChild.data,
|
|
// profile: xml.querySelector("stream>meta>video>profile").firstChild.data,
|
|
// compat: xml.querySelector("stream>meta>video>compat").firstChild.data,
|
|
// level: xml.querySelector("stream>meta>video>level").firstChild.data,
|
|
// }:
|
|
// null
|
|
|
|
set_item_if_present(stream, "time", xml, "stream>time", get_xml_number)
|
|
set_item_if_present(stream, "bw_in", xml, "stream>bw_in", get_xml_number)
|
|
set_item_if_present(stream, "bytes_in", xml, "stream>bytes_in", get_xml_number)
|
|
set_item_if_present(stream, "bw_out", xml, "stream>bw_out", get_xml_number)
|
|
set_item_if_present(stream, "bytes_out", xml, "stream>bytes_out", get_xml_number)
|
|
set_item_if_present(stream, "bw_audio", xml, "stream>bw_audio", get_xml_number)
|
|
set_item_if_present(stream, "bw_video", xml, "stream>bw_video", get_xml_number)
|
|
|
|
return [stream.name, stream]
|
|
}
|
|
|
|
function parse_stat_client(xml) {
|
|
const client = {
|
|
name: get_xml_item(xml, "client>id", get_xml_string),
|
|
is_publishing: get_xml_item(xml, "client>publishing", v=>v, null),
|
|
}
|
|
|
|
set_item_if_present(client, "address", xml, "client>address", get_xml_string)
|
|
set_item_if_present(client, "time", xml, "client>time", get_xml_number)
|
|
set_item_if_present(client, "flashver", xml, "client>flashver", get_xml_string)
|
|
// set_item_if_present(client, "swfurl", xml, "client>swfurl", get_xml_string)
|
|
set_item_if_present(client, "dropped", xml, "client>dropped", get_xml_number)
|
|
set_item_if_present(client, "avsync", xml, "client>avsync", get_xml_number)
|
|
set_item_if_present(client, "timestamp", xml, "client>timestamp", get_xml_number)
|
|
|
|
return client
|
|
}
|
|
|
|
|
|
function get_xml_item(xml, path, factory=v=>v, default_value=undefined) {
|
|
const item = xml.querySelector(path)
|
|
if (!item && default_value === undefined) {
|
|
console.error(`${path} is not defined`, xml)
|
|
throw new Error(`${path} is not defined`)
|
|
}
|
|
return item? factory(item): default_value;
|
|
}
|
|
|
|
|
|
function get_xml_items(xml, path, factory=v=>v) {
|
|
const items = xml.querySelectorAll(path)
|
|
if (!items) {
|
|
return []
|
|
}
|
|
return Array.prototype.map.call(items, factory);
|
|
}
|
|
|
|
function set_item_if_present(dst, key, xml, path, factory=v=>v) {
|
|
const value = get_xml_item(xml, path, factory, null)
|
|
if (value !== null) {
|
|
dst[key] = value
|
|
}
|
|
}
|
|
|
|
|
|
function get_xml_string(xml) {
|
|
return xml.firstChild.data
|
|
}
|
|
|
|
function get_xml_number(xml) {
|
|
return +xml.firstChild.data
|
|
}
|
|
|
|
function do_xml_item_exists(xml) {
|
|
return !!xml
|
|
}
|
|
|
|
|
|
async function fetch_stat() {
|
|
const response = await fetch(STAT_URL)
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch ${response.url}: ${response.status} ${response.statusText}`)
|
|
}
|
|
const xml = await response.text()
|
|
return new DOMParser().parseFromString(xml, "text/xml")
|
|
}
|
|
|
|
|
|
function create_element(tag, attrs={}, ...children) {
|
|
const elem = document.createElement(tag)
|
|
|
|
for (let [key, value] of Object.entries(attrs)) {
|
|
elem.setAttribute(key, value)
|
|
}
|
|
|
|
for (let child of children) {
|
|
if (is_string(child)) {
|
|
child = document.createTextNode(child)
|
|
}
|
|
|
|
elem.appendChild(child)
|
|
}
|
|
|
|
return elem
|
|
}
|
|
|
|
|
|
function is_string(obj) {
|
|
return typeof obj == "string" || obj instanceof String
|
|
}
|
|
|