// 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 }