├── ALL_SERVERS
├── README.md
└── bitrate.html
├── LICENSE
├── README.md
├── python
├── README.md
└── bitrate.py
└── streamelements
├── README.md
├── widget.css
├── widget.fields
├── widget.html
└── widget.js
/ALL_SERVERS/README.md:
--------------------------------------------------------------------------------
1 | # How to edit and use `bitrate.html`:
2 | Open up the `bitrate.html` in your favorite code editor, once open follow the instructions below.
3 |
4 | ---
5 | # Configure the way the script fetches the bitrate:
6 |
7 | ## Line 127;
8 | The following line configures the interval the script fetches all of the bitrates from the server stats pages in the array, this is in milliseconds, so 2000 would equal 2 seconds.
9 | ```javascript
10 | const interval = 2000;
11 | ```
12 |
13 |
14 | ## Lines 129-131;
15 | ## Modify the following lines to match your server(s) setup, make your changes accordingly, please note you can add in as many servers you want to the server array, example you could have 4 different srt, nms, nginx servers;
16 |
17 | Line 129; if you're using SRT this would be your `streamid`, in the example below 'publish/live/feed1', if you do not want to show RTT then change 'rtt: true' to 'rtt: false'
18 | ```javascript
19 | { server: "SRT", page: "http://127.0.0.1:8181/stats", key: "publish/live/feed1", rtt: true },
20 | ```
21 |
22 | Line 130; if you're using NMS this would be your `application` + `key`, in the example below 'live' is the application and 'feed1' is the key (this also includes the default password to access the NMS server stats page).
23 | ```javascript
24 | { server: "NMS", page: "http://admin:admin@localhost:8000/api/streams/live/feed1" },
25 | ```
26 |
27 | Line 131; if you're using NGINX this would be just your `key`, so if you go with the default, it would be 'live'
28 | ```javascript
29 | { server: "NGINX", page: "http://localhost/stat", key: "live" }
30 | ```
31 | ---
32 |
33 | # Configure the way it looks:
34 |
35 | ## Modify line 114 to change how it looks when there is NO incoming bitrate:
36 |
37 | Default for line 114 will show nothing.
38 | ```javascript
39 | el.innerHTML = null;
40 | ```
41 |
42 | ### Here are some examples:
43 | Example #1; this will show: `0`
44 | ```javascript
45 | el.innerHTML = `0`;
46 | ```
47 |
48 | Example #2; this will show: `NO SIGNAL`
49 | ```javascript
50 | el.innerHTML = `NO SIGNAL`;
51 | ```
52 |
53 | Example #3; this will show: `WHATEVER`
54 | ```javascript
55 | el.innerHTML = `WHATEVER`;
56 | ```
57 | ---
58 | ## Modify line 119 to change how it looks when there IS incoming bitrate:
59 |
60 | Default for line 119 will show: `XXXX kb/s`.
61 | ```javascript
62 | el.innerHTML = `${bitrate} kb/s`;
63 | ```
64 |
65 | ### Here are some examples:
66 | Example #1; this will show: `bitrate: XXXX kb/s`
67 | ```javascript
68 | el.innerHTML = `bitrate: ${bitrate} kb/s`;
69 | ```
70 |
71 | Example #2; this will show: `blah blah XXXX kb/s`
72 | ```javascript
73 | el.innerHTML = `blah blah ${bitrate} kb/s`;
74 | ```
75 |
76 | ---
77 |
78 |
79 | After everything is configured correctly and edited to your liking, save it and add the `bitrate.html` as a browser source in OBS.
80 |
81 | Make sure to add it as a local file:
82 |
83 | 
84 |
85 |
86 | ---
87 | ### Credits:
88 |
89 | b3ck - concept, feedback and testing.
90 |
91 | 715209 - coding and feedback.
92 |
--------------------------------------------------------------------------------
/ALL_SERVERS/bitrate.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | ALL-SERVERS-STATS-FETCH
7 |
8 |
25 |
26 |
27 |
28 |
29 |
30 |
36 |
37 |
125 |
126 |
138 |
139 |
140 |
141 |
142 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 b3ck
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # server-bitrate-html
2 | ##### Simple HTML / Javascript webpage that shows current bitrate of desired NGINX/NMS RTMP & DOCKER-SLS-SRT servers.
3 |
4 | ### Use the combined version in the [/ALL_SERVERS/](https://github.com/b3ck/server-bitrate-html/tree/master/ALL_SERVERS) directory.
5 |
6 | ---
7 | I've added two different variations of this:
8 | - [StreamElements Widget](https://github.com/b3ck/server-bitrate-html/tree/master/streamelements) - to be used as a custom widget in a StreamElements overlay.
9 | - [OBS Python Script](https://github.com/b3ck/server-bitrate-html/tree/master/python) - to be used inside of OBS Studio.
10 | ---
11 |
12 | ### Find me on Discord if you need any help: @b3ck
13 |
14 | ---
15 | The great folks at [IRLHosting](https://irlhosting.com/) have provided a generator for this, check it out here: [OBS Bitrate Overlay Generator](https://irlhosting.com/bitrate/)
16 |
--------------------------------------------------------------------------------
/python/README.md:
--------------------------------------------------------------------------------
1 | Here is a python script that you can use with OBS Studio if you have Python installed, I used 3.11 with this, let me know if you have any questions,
2 | this is a work in progress, I've only tested it with Belabox Cloud, NGINX and RIST, feel free to test with other server types and let me know if they work, don't work
3 |
4 | 
5 |
6 | 
7 |
8 | 
9 |
--------------------------------------------------------------------------------
/python/bitrate.py:
--------------------------------------------------------------------------------
1 | import obspython as obs
2 | import urllib.request
3 | import urllib.error
4 | import json
5 | import xml.etree.ElementTree as ET
6 | from time import sleep
7 | from threading import Thread, Event
8 |
9 | # --- Global Variables ---
10 |
11 | source_name = ""
12 | server_type = "SRT"
13 | url = ""
14 | key = ""
15 | enable_rtt = False
16 | interval = 2000
17 | show_kbps = True
18 | show_separator = True
19 | separator_char = "•"
20 | enable_logging = True # Add this variable to control logging
21 | thread = None
22 | fetching_data = Event()
23 | is_fetching = False
24 |
25 | # --- Console Logging ---
26 |
27 | def log_to_console(message):
28 | if enable_logging:
29 | obs.script_log(obs.LOG_INFO, message)
30 |
31 | def log_error(message):
32 | if enable_logging:
33 | obs.script_log(obs.LOG_ERROR, message)
34 |
35 | # --- Button Handling ---
36 |
37 | def toggle_button_pressed(props, prop):
38 | try:
39 | global thread, is_fetching
40 | if not fetching_data.is_set():
41 | fetching_data.set()
42 | is_fetching = True
43 | thread = Thread(target=fetch_bitrate)
44 | thread.daemon = True
45 | thread.start()
46 | log_to_console("Starting data fetching.")
47 | else:
48 | fetching_data.clear()
49 | is_fetching = False
50 | log_to_console("Stopping data fetching.")
51 | update_button_text(props)
52 | except Exception as e:
53 | log_error(f"Error in toggle_button_pressed: {e}")
54 | return True
55 |
56 | def update_button_text(props):
57 | try:
58 | button = obs.obs_properties_get(props, "toggle_button")
59 | if is_fetching:
60 | obs.obs_property_set_description(button, "Stop")
61 | else:
62 | obs.obs_property_set_description(button, "Start")
63 | except Exception as e:
64 | log_error(f"Error in update_button_text: {e}")
65 |
66 | # --- Script Properties ---
67 |
68 | def script_properties():
69 | try:
70 | props = obs.obs_properties_create()
71 |
72 | button_group = obs.obs_properties_create()
73 | obs.obs_properties_add_group(
74 | props, "button_group", "Control", obs.OBS_GROUP_NORMAL, button_group
75 | )
76 |
77 | obs.obs_properties_add_button(
78 | button_group, "toggle_button", "Start", toggle_button_pressed
79 | )
80 |
81 | text_source_list = obs.obs_properties_add_list(
82 | props,
83 | "source_name",
84 | "Text Source",
85 | obs.OBS_COMBO_TYPE_EDITABLE,
86 | obs.OBS_COMBO_FORMAT_STRING,
87 | )
88 | populate_text_sources(text_source_list)
89 | server_list = obs.obs_properties_add_list(
90 | props,
91 | "server_type",
92 | "Server Type",
93 | obs.OBS_COMBO_TYPE_LIST,
94 | obs.OBS_COMBO_FORMAT_STRING,
95 | )
96 | obs.obs_property_list_add_string(server_list, "SRT", "SRT")
97 | obs.obs_property_list_add_string(server_list, "RIST", "RIST")
98 | obs.obs_property_list_add_string(server_list, "NMS", "NMS")
99 | obs.obs_property_list_add_string(server_list, "NGINX", "NGINX")
100 | obs.obs_properties_add_text(props, "url", "URL", obs.OBS_TEXT_DEFAULT)
101 | obs.obs_properties_add_text(
102 | props, "key", "Key (if applicable)", obs.OBS_TEXT_DEFAULT
103 | )
104 |
105 | obs.obs_properties_add_bool(props, "enable_rtt", "Enable RTT (for SRT/RIST)")
106 | obs.obs_properties_add_int(
107 | props, "interval", "Update Interval (ms)", 500, 10000, 100
108 | )
109 | obs.obs_properties_add_bool(props, "show_kbps", "Show 'KB/s'")
110 | obs.obs_properties_add_bool(props, "show_separator", "Show Separator")
111 | obs.obs_properties_add_text(
112 | props, "separator_char", "Separator Character", obs.OBS_TEXT_DEFAULT
113 | )
114 | obs.obs_properties_add_bool(props, "enable_logging", "Enable Logging") # Add this line
115 |
116 | update_button_text(props)
117 | log_to_console("Properties created.")
118 | return props
119 | except Exception as e:
120 | log_error(f"Error in script_properties: {e}")
121 |
122 | def populate_text_sources(prop):
123 | try:
124 | sources = obs.obs_enum_sources()
125 | if sources:
126 | for source in sources:
127 | source_id = obs.obs_source_get_id(source)
128 | if source_id in ["text_gdiplus_v2", "text_ft2_source_v2"]:
129 | name = obs.obs_source_get_name(source)
130 | obs.obs_property_list_add_string(prop, name, name)
131 | obs.source_list_release(sources)
132 | log_to_console("Text sources populated.")
133 | except Exception as e:
134 | log_error(f"Error in populate_text_sources: {e}")
135 |
136 | def script_update(settings):
137 | try:
138 | global source_name, server_type, url, key, enable_rtt, interval, show_kbps, show_separator, separator_char, enable_logging
139 |
140 | source_name = obs.obs_data_get_string(settings, "source_name")
141 | server_type = obs.obs_data_get_string(settings, "server_type")
142 | url = obs.obs_data_get_string(settings, "url")
143 | key = obs.obs_data_get_string(settings, "key")
144 | enable_rtt = obs.obs_data_get_bool(settings, "enable_rtt")
145 | interval = obs.obs_data_get_int(settings, "interval")
146 | show_kbps = obs.obs_data_get_bool(settings, "show_kbps")
147 | show_separator = obs.obs_data_get_bool(settings, "show_separator")
148 | separator_char = obs.obs_data_get_string(settings, "separator_char")
149 | enable_logging = obs.obs_data_get_bool(settings, "enable_logging") # Add this line
150 |
151 | log_to_console(
152 | f"Settings updated: source_name={source_name}, server_type={server_type}, url={url}, key={key}, enable_rtt={enable_rtt}, interval={interval}, show_kbps={show_kbps}, show_separator={show_separator}, separator_char={separator_char}, enable_logging={enable_logging}"
153 | )
154 | except Exception as e:
155 | log_error(f"Error in script_update: {e}")
156 |
157 | def fetch_bitrate():
158 | try:
159 | while fetching_data.is_set():
160 | try:
161 | if not url:
162 | log_to_console("URL is not set. Skipping fetch.")
163 | sleep(interval / 1000)
164 | continue
165 |
166 | if server_type == "SRT":
167 | bitrate, rtt = get_srt_bitrate(url, key)
168 | elif server_type == "RIST":
169 | bitrate, rtt = get_rist_bitrate(url)
170 | elif server_type == "NMS":
171 | bitrate, rtt = get_nms_bitrate(url), None
172 | elif server_type == "NGINX":
173 | bitrate, rtt = get_nginx_bitrate(url, key), None
174 | else:
175 | bitrate, rtt = None, None
176 |
177 | update_text_source(bitrate, rtt)
178 | except Exception as e:
179 | log_error(f"Error fetching bitrate: {e}")
180 |
181 | sleep(interval / 1000)
182 | except Exception as e:
183 | log_error(f"Error in fetch_bitrate: {e}")
184 |
185 | def get_srt_bitrate(url, publisher):
186 | try:
187 | with urllib.request.urlopen(url) as response:
188 | data = json.load(response)
189 | if publisher not in data["publishers"]:
190 | return None, None
191 | bitrate = data["publishers"][publisher]["bitrate"]
192 | rtt = data["publishers"][publisher].get("rtt") if enable_rtt else None
193 | return bitrate, rtt
194 | except urllib.error.URLError as e:
195 | log_error(f"Error fetching SRT bitrate: {e}")
196 | return None, None
197 |
198 | def get_rist_bitrate(url):
199 | try:
200 | with urllib.request.urlopen(url) as response:
201 | data = json.load(response)
202 | if "receiver-stats" not in data or data["receiver-stats"] is None:
203 | return None, None
204 | br_value = sum(
205 | round(peer["stats"]["bitrate"] / 1024)
206 | for peer in data["receiver-stats"]["flowinstant"]["peers"]
207 | if "bitrate" in peer["stats"]
208 | )
209 | rtt_value = next(
210 | (
211 | round(peer["stats"]["rtt"])
212 | for peer in data["receiver-stats"]["flowinstant"]["peers"]
213 | if "rtt" in peer["stats"]
214 | ),
215 | None,
216 | )
217 | return br_value, rtt_value
218 | except urllib.error.URLError as e:
219 | log_error(f"Error fetching RIST bitrate: {e}")
220 | return None, None
221 |
222 | def get_nms_bitrate(url):
223 | try:
224 | with urllib.request.urlopen(url) as response:
225 | data = json.load(response)
226 | return data["bitrate"]
227 | except urllib.error.URLError as e:
228 | log_error(f"Error fetching NMS bitrate: {e}")
229 | return None
230 |
231 | def get_nginx_bitrate(url, key):
232 | try:
233 | with urllib.request.urlopen(url) as response:
234 | root = ET.fromstring(response.read())
235 |
236 | # Log the XML for debugging purposes
237 | log_to_console(ET.tostring(root, encoding='utf8').decode('utf8'))
238 |
239 | # Iterate through all applications
240 | for app in root.findall('.//application'):
241 | live = app.find('live')
242 | if live is None:
243 | continue
244 |
245 | # Iterate through all streams within the live section
246 | for stream in live.findall('stream'):
247 | stream_name = stream.find('name')
248 | if stream_name is not None and stream_name.text == key:
249 | bw_in = stream.find('bw_in')
250 | if bw_in is not None:
251 | bitrate = int(bw_in.text) // 1024 # Convert to kbps
252 | return bitrate
253 |
254 | log_error(f"Stream with key '{key}' not found.")
255 | return None
256 | except urllib.error.URLError as e:
257 | log_error(f"Error fetching NGINX bitrate: {e}")
258 | return None
259 |
260 | def update_text_source(bitrate, rtt):
261 | try:
262 | source = obs.obs_get_source_by_name(source_name)
263 | if source:
264 | settings = obs.obs_data_create()
265 | if bitrate is not None:
266 | text = f"{bitrate}"
267 | if show_kbps:
268 | text += " kb/s"
269 | if rtt is not None and show_separator:
270 | text += f" {separator_char} {rtt} RTT"
271 | else:
272 | text = ""
273 |
274 | obs.obs_data_set_string(settings, "text", text)
275 | obs.obs_source_update(source, settings)
276 | obs.obs_data_release(settings)
277 | obs.obs_source_release(source)
278 | # log_to_console(f"Text source updated: {text}")
279 | except Exception as e:
280 | log_error(f"Error in update_text_source: {e}")
281 |
282 | def script_load(settings):
283 | try:
284 | log_to_console("Script loaded and ready.")
285 | except Exception as e:
286 | log_error(f"Error in script_load: {e}")
287 |
288 | def script_unload():
289 | try:
290 | global fetching_data
291 | fetching_data.clear()
292 | log_to_console("Script unloaded and fetching stopped.")
293 | except Exception as e:
294 | log_error(f"Error in script_unload: {e}")
295 |
--------------------------------------------------------------------------------
/streamelements/README.md:
--------------------------------------------------------------------------------
1 | Just create a custom widget in a StreamElements Overlay and then click on it, open editor, and replace the code for each tab with the code from each of the files here.
2 |
3 | 
4 |
5 | 
6 |
7 | 
8 |
9 | 
10 |
--------------------------------------------------------------------------------
/streamelements/widget.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: rgba(0, 0, 0, 0) !important;
3 | margin: 0px auto !important;
4 | font-family: '{{fontName}}' !important;
5 | overflow: hidden !important;
6 | color: {{fontColor}} !important;
7 | font-size: {{fontSize}}px !important;
8 | font-weight: {{fontWeight}} !important;
9 | text-align: {{textAlign}} !important;
10 | text-shadow: {{textShadow}} !important;
11 | padding-top: 0px !important;
12 | padding-left: {{textPadding}}px !important;
13 | padding-right: {{textPadding}}px !important;
14 | padding-bottom: {{textPadding}}px !important;
15 | }
16 |
17 | #bitrate, #rtt {
18 | color: {{fontColor}} !important;
19 | }
20 |
21 | #bitrate * {
22 | vertical-align: middle !important;
23 | }
24 |
--------------------------------------------------------------------------------
/streamelements/widget.fields:
--------------------------------------------------------------------------------
1 | {
2 | "fontName": {
3 | "type": "googleFont",
4 | "label": "Font name",
5 | "value": "Montserrat",
6 | "group": "Style"
7 | },
8 | "fontSize": {
9 | "type": "slider",
10 | "label": "Font Size:",
11 | "value": 22,
12 | "min": 0,
13 | "max": 255,
14 | "step": 1,
15 | "group": "Style"
16 | },
17 | "fontWeight": {
18 | "label": "Font Weight",
19 | "type": "dropdown",
20 | "value": "700",
21 | "options": {
22 | "100": "Thin (100)",
23 | "300": "Light (300)",
24 | "400": "Regular (400)",
25 | "500": "Medium (500)",
26 | "700": "Bold (700)",
27 | "900": "Black (900)"
28 | },
29 | "group": "Style"
30 | },
31 | "fontColor": {
32 | "type": "colorpicker",
33 | "label": "Font Color",
34 | "value": "rgba(30,144,255,1)",
35 | "group": "Style"
36 | },
37 | "textShadow": {
38 | "type": "text",
39 | "label": "Text Shadow",
40 | "value": "0px 0px 5px rgba(0,0,0,1)",
41 | "group": "Style"
42 | },
43 | "textAlign": {
44 | "label": "Text Alignment",
45 | "type": "dropdown",
46 | "value": "right",
47 | "options": {
48 | "left": "Left",
49 | "center": "Center",
50 | "right": "Right"
51 | },
52 | "group": "Style"
53 | },
54 | "textPadding": {
55 | "type": "slider",
56 | "label": "Padding:",
57 | "value": 5,
58 | "min": 0,
59 | "max": 255,
60 | "step": 1,
61 | "group": "Style"
62 | },
63 | "fontEffect": {
64 | "label": "Font Effect",
65 | "type": "dropdown",
66 | "value": "none",
67 | "options": {
68 | "none": "None",
69 | "anaglyph": "Anaglyph",
70 | "brick-sign": "Brick Sign",
71 | "canvas-print": "Canvas Print",
72 | "crackle": "Crackle",
73 | "decaying": "Decaying",
74 | "destruction": "Destruction",
75 | "distressed": "Distressed",
76 | "distressed-wood": "Distressed Wood",
77 | "emboss": "Emboss",
78 | "fire": "Fire",
79 | "fire-animation": "Fire Animation",
80 | "fragile": "Fragile",
81 | "grass": "Grass",
82 | "ice": "Ice",
83 | "mitosis": "Mitosis",
84 | "neon": "Neon",
85 | "outline": "Outline",
86 | "putting-green": "Putting Green",
87 | "scuffed-steel": "Scuffed Steel",
88 | "shadow-multiple": "Shadow Multiple",
89 | "splintered": "Splintered",
90 | "static": "Static",
91 | "stonewash": "Stonewash",
92 | "3d": "Three Dimensional",
93 | "3d-float": "Three Dimensional Float",
94 | "vintage": "Vintage",
95 | "wallpaper": "Wallpaper"
96 | },
97 | "group": "Style"
98 | },
99 | "interval": {
100 | "type": "number",
101 | "label": "Update interval (ms)",
102 | "value": 2000,
103 | "group": "Settings"
104 | },
105 | "showKbpsText": {
106 | "type": "dropdown",
107 | "label": "Show kb/s text",
108 | "value": "Yes",
109 | "options": {
110 | "yes": "Yes",
111 | "no": "No"
112 | },
113 | "group": "Settings"
114 | },
115 | "showRttText": {
116 | "type": "dropdown",
117 | "label": "Show RTT text",
118 | "value": "Yes",
119 | "options": {
120 | "yes": "Yes",
121 | "no": "No"
122 | },
123 | "group": "Settings"
124 | },
125 | "serverType1": {
126 | "type": "dropdown",
127 | "label": "Server Type 1",
128 | "value": "SRT",
129 | "options": {
130 | "SRT": "SRT",
131 | "NMS": "NMS",
132 | "NGINX": "NGINX"
133 | },
134 | "group":"Server 1"
135 | },
136 | "statsURL1": {
137 | "type": "text",
138 | "label": "Stats URL 1",
139 | "value": "",
140 | "group":"Server 1"
141 | },
142 | "key1": {
143 | "type": "text",
144 | "label": "Key 1",
145 | "value": "",
146 | "group":"Server 1"
147 | },
148 | "rtt1": {
149 | "type": "dropdown",
150 | "label": "RTT 1",
151 | "value": "no",
152 | "options": {
153 | "yes": "Yes",
154 | "no": "No"
155 | },
156 | "group":"Server 1"
157 | },
158 | "serverType2": {
159 | "type": "dropdown",
160 | "label": "Server Type 2",
161 | "value": "SRT",
162 | "options": {
163 | "SRT": "SRT",
164 | "NMS": "NMS",
165 | "NGINX": "NGINX"
166 | },
167 | "group":"Server 2"
168 | },
169 | "statsURL2": {
170 | "type": "text",
171 | "label": "Stats URL 2",
172 | "value": "",
173 | "group":"Server 2"
174 | },
175 | "key2": {
176 | "type": "text",
177 | "label": "Key 2",
178 | "value": "",
179 | "group":"Server 2"
180 | },
181 | "rtt2": {
182 | "type": "dropdown",
183 | "label": "RTT 2",
184 | "value": "no",
185 | "options": {
186 | "yes": "Yes",
187 | "no": "No"
188 | },
189 | "group":"Server 2"
190 | },
191 | "serverType3": {
192 | "type": "dropdown",
193 | "label": "Server Type 3",
194 | "value": "SRT",
195 | "options": {
196 | "SRT": "SRT",
197 | "NMS": "NMS",
198 | "NGINX": "NGINX"
199 | },
200 | "group":"Server 3"
201 | },
202 | "statsURL3": {
203 | "type": "text",
204 | "label": "Stats URL 3",
205 | "value": "",
206 | "group":"Server 3"
207 | },
208 | "key3": {
209 | "type": "text",
210 | "label": "Key 3",
211 | "value": "",
212 | "group":"Server 3"
213 | },
214 | "rtt3": {
215 | "type": "dropdown",
216 | "label": "RTT 3",
217 | "value": "no",
218 | "options": {
219 | "yes": "Yes",
220 | "no": "No"
221 | },
222 | "group":"Server 3"
223 | },
224 | "serverType4": {
225 | "type": "dropdown",
226 | "label": "Server Type 4",
227 | "value": "SRT",
228 | "options": {
229 | "SRT": "SRT",
230 | "NMS": "NMS",
231 | "NGINX": "NGINX"
232 | },
233 | "group":"Server 4"
234 | },
235 | "statsURL4": {
236 | "type": "text",
237 | "label": "Stats URL 4",
238 | "value": "",
239 | "group":"Server 4"
240 | },
241 | "key4": {
242 | "type": "text",
243 | "label": "Key 4",
244 | "value": "",
245 | "group":"Server 4"
246 | },
247 | "rtt4": {
248 | "type": "dropdown",
249 | "label": "RTT 4",
250 | "value": "no",
251 | "options": {
252 | "yes": "Yes",
253 | "no": "No"
254 | },
255 | "group":"Server 4"
256 | }
257 | }
258 |
--------------------------------------------------------------------------------
/streamelements/widget.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/streamelements/widget.js:
--------------------------------------------------------------------------------
1 | let interval = 2000;
2 | let stats = [];
3 | let showKbpsText = true;
4 | let showRttText = true;
5 |
6 | window.addEventListener('onWidgetLoad', function(obj) {
7 | const fieldData = obj.detail.fieldData;
8 | interval = fieldData.interval;
9 | stats = [
10 | { server: fieldData.serverType1, page: fieldData.statsURL1, key: fieldData.key1, rtt: fieldData.rtt1 === "yes" },
11 | { server: fieldData.serverType2, page: fieldData.statsURL2, key: fieldData.key2, rtt: fieldData.rtt2 === "yes" },
12 | { server: fieldData.serverType3, page: fieldData.statsURL3, key: fieldData.key3, rtt: fieldData.rtt3 === "yes" },
13 | { server: fieldData.serverType4, page: fieldData.statsURL4, key: fieldData.key4, rtt: fieldData.rtt4 === "yes" }
14 | ];
15 |
16 | // Read the new fields
17 | showKbpsText = fieldData.showKbpsText === "yes";
18 | showRttText = fieldData.showRttText === "yes";
19 |
20 | // Apply custom styles
21 | //const customCSS = `
22 | // body {
23 | // font-family: '${fieldData.fontName}', sans-serif;
24 | // color: ${fieldData.fontColor};
25 | // font-size: ${fieldData.fontSize}px;
26 | // font-weight: ${fieldData.fontWeight};
27 | // text-align: ${fieldData.textAlign};
28 | // text-shadow: ${fieldData.textShadow};
29 | // padding: ${fieldData.textPadding}px;
30 | // }
31 | // .font-effect-${fieldData.fontEffect} {
32 | // font-family: '${fieldData.fontName}', sans-serif;
33 | // text-shadow: ${fieldData.textShadow};
34 | // }
35 | //`;
36 | //document.getElementById('custom-css').innerHTML = customCSS;
37 |
38 | run(stats, interval);
39 | });
40 |
41 | const tryFetch = async (page) => {
42 | try {
43 | return await fetch(page);
44 | } catch (error) {
45 | console.log(page, error);
46 | }
47 | return null;
48 | }
49 |
50 | const getSrtBitrate = async (page, publisher) => {
51 | const response = await tryFetch(page);
52 | if (!response) return null;
53 |
54 | const srtdata = await response.json();
55 | if (srtdata.publishers[publisher] == null) return null;
56 |
57 | const { bitrate, rtt } = srtdata.publishers[publisher];
58 | return { bitrate, rtt };
59 | };
60 |
61 | const getNmsBitrate = async (page) => {
62 | const response = await tryFetch(page);
63 | if (!response) return null
64 |
65 | const { bitrate } = await response.json();
66 |
67 | if (!bitrate) return null;
68 |
69 | return { bitrate };
70 | };
71 |
72 | const getNginxBitrate = async (page, key) => {
73 | const response = await tryFetch(page);
74 | if (!response) return null
75 |
76 | const data = await response.text();
77 | const parse = new window.DOMParser().parseFromString(data, "text/xml");
78 | const live = parse.getElementsByTagName(key)[0];
79 |
80 | const node = live.childNodes[1].childNodes[14];
81 |
82 | if (!node) return null;
83 |
84 | const bitrate = Math.round(node.childNodes[0].nodeValue / 1024);
85 | return { bitrate };
86 | };
87 |
88 | const run = async (stats, interval) => {
89 | setText(await getBitrate(stats))
90 |
91 | setInterval(async () => {
92 | setText(await getBitrate(stats));
93 | }, interval);
94 | }
95 |
96 | const getBitrate = async (stats) => {
97 | const get = {
98 | "SRT": getSrtBitrate,
99 | "NMS": getNmsBitrate,
100 | "NGINX": getNginxBitrate,
101 | }
102 |
103 | const values = await Promise.all(stats.map(s => get[s.server](s.page, s.key)));
104 | const index = values.findIndex(el => el);
105 |
106 | return [values[index], stats[index]];
107 | }
108 |
109 | const setText = (stats) => {
110 | [stats, origin] = stats;
111 | const { bitrate, rtt } = stats || {};
112 | const el = document.getElementById('bitrate');
113 |
114 | if (!bitrate) {
115 | el.innerHTML = null;
116 | return;
117 | }
118 |
119 | let bitrateText = `${bitrate}`;
120 | if (showKbpsText) {
121 | bitrateText += " kb/s";
122 | }
123 |
124 | if (origin.rtt && rtt !== undefined) {
125 | let rttText = `• ${Math.round(rtt)}`;
126 | if (showRttText) {
127 | rttText += " RTT";
128 | }
129 | el.innerHTML = `${bitrateText} ${rttText}`;
130 | } else {
131 | el.innerHTML = `${bitrateText}`;
132 | }
133 | }
134 |
--------------------------------------------------------------------------------