├── 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 | ![image](https://user-images.githubusercontent.com/1740542/148205807-36c35fe7-f004-43cd-ad17-c785df2d9076.png) 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 |
31 | 32 |
33 |
34 | 35 |
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 | ![image](https://github.com/user-attachments/assets/ed379bb3-d1e5-485f-8130-b244b76f0856) 5 | 6 | ![image](https://github.com/user-attachments/assets/ab7e1d7f-1043-423c-9c75-9dbd39843279) 7 | 8 | ![image](https://github.com/user-attachments/assets/5e4b4926-10bc-4f5d-8a84-9b7ba258b42e) 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 | ![image](https://github.com/user-attachments/assets/8636f5c5-e16b-4986-a302-82251cb924d8) 4 | 5 | ![image](https://github.com/user-attachments/assets/490dc0ee-d694-490d-b453-586b85d60ba3) 6 | 7 | ![image](https://github.com/user-attachments/assets/7a179032-c3d9-4298-b97f-19ea4294792b) 8 | 9 | ![image](https://github.com/user-attachments/assets/ea1c3266-6df2-4019-a7c7-67988eab47a4) 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 |
8 |
9 |
10 |
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 | --------------------------------------------------------------------------------