Add WebRTC Internals Exporter extension with initial files and functionality

This commit is contained in:
kobim committed 2025-01-25 14:33:58 +01:00
1 parent 5bb7cb4aab
commit c2f9206624
26 files changed
+911 -294

No files matched your search

+103 -30
View File
@@ -4,6 +4,10 @@ import json
import time
import socket
import logging
import os
from functools import partial
from http.server import HTTPServer
from utils.PostHandler import Handler
from utils.ColoredFormatter import ColoredFormatter
from bs4 import BeautifulSoup as bs
from selenium import webdriver
@@ -26,7 +30,7 @@ def setupLogger():
logger.addHandler(logger_handler)
def interrupt_handler(signum, driver: webdriver.Chrome):
print(f'Handling signal {signum} ({signal.Signals(signum).name}).')
logger.log(logging.INFO, f'Handling signal {signum} ({signal.Signals(signum).name}).')
schedule.clear()
driver.quit()
@@ -35,29 +39,33 @@ def interrupt_handler(signum, driver: webdriver.Chrome):
def setupChromeDriver():
logger.log(logging.INFO, 'Setting up Chrome driver.')
chrome_options = Options()
chrome_options.add_argument("--headless")
#chrome_options.add_argument("--headless")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--mute-audio")
chrome_options.add_argument("--window-size=1280,720")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--no-default-browser-check")
chrome_options.add_argument("--disable-features=WebRtcHideLocalIpsWithMdns")
chrome_options.add_argument(f"--load-extension={os.path.abspath(os.path.join(os.path.dirname(__file__), 'webrtc-internals-exporter'))}")
chrome_options.add_experimental_option('prefs', {'intl.accept_languages': 'en,en_US'})
#chrome_options.add_extension('./qryn-webrtc-exporter.crx')
driver = webdriver.Chrome(options=chrome_options)
#driver = webdriver.Remote(command_executor='http://localhost:4444', options=chrome_options)
logger.log(logging.INFO, 'Chrome driver setup complete.')
return driver
def saveStats(stats: dict):
def saveStats(stats: list):
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
logger.log(logging.DEBUG, f'Saving stats: {json.dumps(stats, indent=4)}')
sock.sendto(json.dumps(stats).encode(), ('localhost', 8094))
sock.close()
print(f'Sent stats: {stats}')
logger.log(logging.DEBUG, 'Sent stats to socket.')
except socket.error as e:
print(f'Got socket error: {e}')
logger.error(f'Got socket error: {e}')
def downloadStats(driver: webdriver.Chrome):
def downloadStats(driver: webdriver.Chrome, peersDict: dict):
html = driver.find_element(By.CLASS_NAME ,'vjs-stats-list').get_attribute('innerHTML')
if html is not None:
htmlBS = bs(html, 'html.parser')
@@ -66,32 +74,91 @@ def downloadStats(driver: webdriver.Chrome):
stats = htmlBS.find_all('div', attrs={'style': 'display: block;'})
statsDict = {
playerStats = {
stat.div.text: stat.span.text.replace('\u21d3', 'down').replace('down/', 'down /').replace('\u21d1 ', 'up').replace('\u21d1', 'up').replace('\u00b7', '-').strip()
for stat in stats
}
for stat in statsDict:
keys = list(playerStats.keys())
for stat in keys:
if 'Viewport / Frames' == stat:
viewport, frames = playerStats[stat].split(' / ')
width, height = viewport.split('x')
height, devicePixelRatio = height.split('*')
dropped, total = frames.split(' of ')[0].split()[0], frames.split(' of ')[1].split()[0]
playerStats[stat] = {'Width': int(width), 'Height': int(height), 'Pixel ratio': float(devicePixelRatio), 'Frames': {'Dropped': int(dropped), 'Total': int(total)}}
if 'Codecs' == stat:
video, audio = playerStats[stat].split(' / ')
playerStats[stat] = {'Video': video, 'Audio': audio}
if 'Volume' == stat:
if ' (' in playerStats[stat]:
volume, muted = playerStats[stat].split(' (')
playerStats[stat] = {'Volume': int(volume), 'Muted': 'muted' in muted}
else:
playerStats[stat] = {'Volume': int(playerStats[stat]), 'Muted': False}
if 'Connection Speed' == stat:
speed, unit = playerStats[stat].split()
speedBytes = int(speed) * (1024 ** {'B/s': 0, 'KB/s': 1, 'MB/s': 2, 'GB/s': 3}[unit])
playerStats[stat] = {'Speed': int(speedBytes), 'Granularity': 's'}
if 'Network Activity' == stat:
downString, upString = playerStats[stat].split(' / ')
down, downUnit = downString.replace('down', '').strip().split()
up, upUnit = upString.replace('up', '').strip().split()
downBytes = int(down) * (1024 ** {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3}[downUnit])
upBytes = int(up) * (1024 ** {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3}[upUnit])
playerStats[stat] = {'Down': downBytes, 'Up': upBytes}
if 'Total Transfered' == stat:
downString, upString = playerStats[stat].split(' / ')
down, downUnit = downString.replace('down', '').strip().split()
up, upUnit = upString.replace('up', '').strip().split()
downBytes = int(down) * (1024 ** {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3}[downUnit])
upBytes = int(up) * (1024 ** {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3}[upUnit])
playerStats[stat] = {'Down': downBytes, 'Up': upBytes}
if 'Download Breakdown' == stat:
server, peer = playerStats[stat].split(' - ')
server, serverUnit = server.replace('from servers', '').strip().split()
peer, peerUnit = peer.replace('from peers', '').strip().split()
serverBytes = int(server) * (1024 ** {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3}[serverUnit])
peerBytes = int(peer) * (1024 ** {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3}[peerUnit])
playerStats[stat] = {'Server': serverBytes, 'Peer': peerBytes}
if 'Buffer State' == stat:
statsDict[stat] = statsDict[stat][1:-1].split(', ')
del(playerStats[stat])
if 'Live Latency' == stat:
latency, edge = playerStats[stat].split(' (from edge: ')
latency = sum(int(x) * 60 ** i for i, x in enumerate(reversed([part for part in latency.replace('s', '').split('m') if part])))
edge = sum(int(x) * 60 ** i for i, x in enumerate(reversed([part for part in edge.replace('s', '').replace(')', '').split('m') if part])))
playerStats[stat] = {'Latency': latency, 'Edge': edge}
stats = {
'player': playerStats,
'peers': peersDict,
'url': driver.current_url,
'timestamp': int(time.time())
}
# statsDict = {
# 'userName': dict(
# map(
# lambda stat: (
# stat.div.text,
# stat.span.text.replace('\u21d3', 'down').replace('down/', 'down /').replace('\u21d1 ', 'up').replace('\u21d1', 'up').replace('\u00b7', '-').strip()
# ), stats
# )
# )
# }
statsDict.update({'Timestamp': time.strftime('%Y-%m-%dT%H:%M:%S%z')})
statsDict['userName'] = 'user'
saveStats(statsDict)
saveStats([stats])
def setupStats(driver: webdriver.Chrome, url: str):
logger.log(logging.INFO, 'Setting up stats.')
actions = ActionChains(driver)
wait = WebDriverWait(driver, 30, poll_frequency=0.2)
@@ -99,12 +166,14 @@ def setupStats(driver: webdriver.Chrome, url: str):
wait.until(ec.presence_of_element_located((By.CLASS_NAME, 'vjs-big-play-button')))
actions.click(driver.find_element(By.CLASS_NAME ,'video-js')).perform()
actions.pause(2)
actions.context_click(driver.find_element(By.CLASS_NAME ,'video-js')).perform()
statsForNerds = driver.find_elements(By.CLASS_NAME ,'vjs-menu-item')
actions.pause(2)
actions.click(statsForNerds[-1]).perform()
wait.until(ec.text_to_be_present_in_element((By.CLASS_NAME, 'vjs-stats-list'), 'Player'))
logger.log(logging.INFO, 'Stats setup complete.')
return driver
if __name__ == '__main__':
@@ -116,6 +185,10 @@ if __name__ == '__main__':
setupStats(driver, "https://tube.kobim.cloud/w/9hAbiwai4rsbw9QnPpPkCd")
schedule.every(1).seconds.do(downloadStats, driver)
while True:
schedule.run_pending()
logger.log(logging.INFO, 'Starting server collector.')
httpd = HTTPServer(('localhost', 9092), partial(Handler, downloadStats, driver, logger))
httpd.serve_forever()
#schedule.every(2).seconds.do(downloadStats, driver)
#while True:
#schedule.run_pending()
Binary file not shown.
+1 -3
View File
@@ -1,3 +1 @@
jupyterlab
selenium
schedule
selenium
-212
View File
@@ -1,212 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "27784c31-2579-4c9f-802a-de9b9e1471d1",
"metadata": {},
"source": [
"# Statistiche per Nerd homebrew"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5d3f30ae-baa7-4703-870f-921829fd8c34",
"metadata": {},
"outputs": [],
"source": [
"import schedule\n",
"import signal\n",
"import json\n",
"import time\n",
"import socket\n",
"from bs4 import BeautifulSoup as bs\n",
"from selenium import webdriver\n",
"from selenium.webdriver.chrome.options import Options\n",
"from selenium.webdriver.common.by import By\n",
"from selenium.webdriver import ActionChains\n",
"from selenium.webdriver.support.wait import WebDriverWait\n",
"from selenium.webdriver.support import expected_conditions as ec\n",
"from IPython.display import display, DisplayHandle, Image"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "aa49216c",
"metadata": {},
"outputs": [],
"source": [
"def interrupt_handler(signum, driver: webdriver.Chrome):\n",
" print(f'Handling signal {signum} ({signal.Signals(signum).name}).')\n",
"\n",
" schedule.clear()\n",
" driver.quit()\n",
" %reset_selective -f driver\n",
" raise SystemExit"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "db04c5f7",
"metadata": {},
"outputs": [],
"source": [
"def setupChromeDriver():\n",
" chrome_options = Options()\n",
" chrome_options.add_argument(\"--headless\")\n",
" chrome_options.add_argument(\"--no-sandbox\")\n",
" chrome_options.add_argument(\"--mute-audio\")\n",
" chrome_options.add_argument(\"--window-size=1280,720\")\n",
" chrome_options.add_argument(\"--disable-dev-shm-usage\")\n",
" chrome_options.add_argument(\"--disable-features=WebRtcHideLocalIpsWithMdns\")\n",
" chrome_options.add_experimental_option('prefs', {'intl.accept_languages': 'en,en_US'})\n",
" #chrome_options.add_extension('./qryn-webrtc-exporter.crx')\n",
"\n",
" driver = webdriver.Chrome(options=chrome_options)\n",
" #driver = webdriver.Remote(command_executor='http://localhost:4444', options=chrome_options)\n",
" return driver"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9c6ed068",
"metadata": {},
"outputs": [],
"source": [
"def saveStats(stats: dict):\n",
" try:\n",
" sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\n",
" sock.sendto(json.dumps(stats).encode(), ('localhost', 8094))\n",
" sock.close()\n",
" except socket.error as e:\n",
" print(f'Got socket error: {e}')\n",
"\n",
"def downloadStats(driver: webdriver.Chrome, display_handle: DisplayHandle):\n",
" html = driver.find_element(By.CLASS_NAME ,'vjs-stats-list')\n",
" htmlBS = bs(html.get_attribute('innerHTML'), 'html.parser')\n",
"\n",
" timestampDiv = htmlBS.new_tag('div', attrs={'style': 'display: block;'})\n",
" timestampTitleDiv = htmlBS.new_tag('div')\n",
" timestampTitleDiv.string = 'Timestamp'\n",
" timestampDiv.append(timestampTitleDiv)\n",
" timestampSpan = htmlBS.new_tag('span')\n",
" timestampSpan.string = time.strftime('%Y-%m-%dT%H:%M:%S%z')\n",
" timestampDiv.append(timestampSpan)\n",
" htmlBS.div.insert_before(timestampDiv)\n",
"\n",
" peers = driver.find_element(By.CLASS_NAME ,'peers-number').text\n",
" peersDiv = htmlBS.new_tag('div', attrs={'style': 'display: block;'})\n",
" peersTitleDiv = htmlBS.new_tag('div')\n",
" peersTitleDiv.string = 'Peers'\n",
" peersDiv.append(peersTitleDiv)\n",
" peersSpan = htmlBS.new_tag('span')\n",
" peersSpan.string = peers\n",
" peersDiv.append(peersSpan)\n",
" htmlBS.div.insert_before(peersDiv)\n",
" \n",
" stats = htmlBS.find_all('div', attrs={'style': 'display: block;'})\n",
"\n",
" statsDict = {\n",
" stat.div.text: stat.span.text.replace('\\u21d3', 'down').replace('down/', 'down /').replace('\\u21d1 ', 'up').replace('\\u21d1', 'up').replace('\\u00b7', '-').strip()\n",
" for stat in stats\n",
" }\n",
" \n",
" for stat in statsDict:\n",
" if 'Buffer State' == stat:\n",
" statsDict[stat] = statsDict[stat][1:-1].split(', ')\n",
"\n",
" # statsDict = {\n",
" # 'userName': dict(\n",
" # map(\n",
" # lambda stat: (\n",
" # stat.div.text, \n",
" # stat.span.text.replace('\\u21d3', 'down').replace('down/', 'down /').replace('\\u21d1 ', 'up').replace('\\u21d1', 'up').replace('\\u00b7', '-').strip()\n",
" # ), stats\n",
" # )\n",
" # )\n",
" # }\n",
"\n",
" statsDict.update({'Timestamp': time.strftime('%Y-%m-%dT%H:%M:%S%z')})\n",
" statsDict['userName'] = 'user'\n",
"\n",
" display_handle.update(json.dumps(statsDict))\n",
"\n",
" saveStats(statsDict)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a812fea1-55f9-4c5d-87d6-d884df5ed1ba",
"metadata": {},
"outputs": [],
"source": [
"def setupStats(driver: webdriver.Chrome, url: str):\n",
" actions = ActionChains(driver)\n",
" wait = WebDriverWait(driver, 30, poll_frequency=0.2)\n",
"\n",
" driver.get(url)\n",
"\n",
" wait.until(ec.presence_of_element_located((By.CLASS_NAME, 'vjs-big-play-button')))\n",
" actions.click(driver.find_element(By.CLASS_NAME ,'video-js')).perform()\n",
" actions.context_click(driver.find_element(By.CLASS_NAME ,'video-js')).perform()\n",
" statsForNerds = driver.find_elements(By.CLASS_NAME ,'vjs-menu-item')\n",
" actions.pause(2)\n",
" actions.click(statsForNerds[-1]).perform()\n",
" wait.until(ec.text_to_be_present_in_element((By.CLASS_NAME, 'vjs-stats-list'), 'Player'))\n",
" actions.move_to_element(driver.find_element(By.CLASS_NAME ,'vjs-peertube')).perform()\n",
"\n",
" display(Image(driver.get_screenshot_as_png()))\n",
"\n",
" return driver"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "221fa21b-aaf4-493f-b344-2d1a86c85c64",
"metadata": {
"scrolled": true
},
"outputs": [],
"source": [
"if __name__ == '__main__':\n",
" driver = setupChromeDriver()\n",
"\n",
" signal.signal(signal.SIGINT, lambda signum, frame: interrupt_handler(signum, driver))\n",
" \n",
" setupStats(driver, \"https://tube.kobim.cloud/w/9hAbiwai4rsbw9QnPpPkCd\")\n",
"\n",
" display_handle = display(\"Loading...\", display_id=True)\n",
" \n",
" schedule.every(1).seconds.do(downloadStats, driver, display_handle)\n",
" while True:\n",
" schedule.run_pending()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+67 -49
View File
@@ -1,60 +1,78 @@
[[processors.dedup]]
dedup_interval = "600s"
[[outputs.file]]
files = ["stdout"]
# [[outputs.mongodb]]
# dsn = "mongodb://192.168.86.40:27017"
# database = "peertube"
# granularity = "seconds"
# authentication = "SCRAM"
# username = "root"
# password = "example"
[[inputs.socket_listener]]
service_address = "udp://:8094"
data_format = "xpath_json"
[[inputs.socket_listener.xpath]]
metric_name = "'stats'"
metric_name = "'statistics'"
metric_selection = "/*"
timestamp = "timestamp"
timestamp_format = "unix"
[inputs.socket_listener.xpath.tags]
url = "url"
#id = "''"
#state = "'connected'"
[inputs.socket_listener.xpath.fields]
P2P = "/P2P"
#Buffer_State = "string-join(/Buffer_State/*, ',')"
Buffer_State = "/text()='Buffer State'"
Resolution = "/Resolution"
player = "player"
#peerValues = "concat('[', string-join(peers/*, ','), ']')"
peers = "peers"
# [[inputs.socket_listener]]
# service_address = "udp://:8094"
# data_format = "json_v2"
# [[inputs.socket_listener.json_v2]]
# measurement_name = "stats"
#[[inputs.http_listener_v2]]
# service_address = ":9091"
# paths = ["/webrtc-internals-exporter"]
# data_format = "json_v2"
# methods = ["POST"]
# http_success_code = 204
#
# [[inputs.http_listener_v2.json_v2]]
# measurement_name = "stats"
# timestamp_path = "@this.0.values.0.timestamp"
# timestamp_format = "unix_ms"
#
# [[inputs.http_listener_v2.json_v2.object]]
# path = "@this"
# tags = ["id", "url", "state"]
# [[inputs.socket_listener.json_v2.field]]
# path = "P2P"
# [[inputs.socket_listener.json_v2.field]]
# path = "Player mode"
# [[inputs.socket_listener.json_v2.field]]
# path = "P2P"
# [[inputs.socket_listener.json_v2.field]]
# path = "Video UUID"
# [[inputs.socket_listener.json_v2.field]]
# path = "Viewport / Frames"
# [[inputs.socket_listener.json_v2.field]]
# path = "Resolution"
# [[inputs.socket_listener.json_v2.field]]
# path = "Volume"
# [[inputs.socket_listener.json_v2.field]]
# path = "Codecs"
# [[inputs.socket_listener.json_v2.field]]
# path = "Connection Speed"
# [[inputs.socket_listener.json_v2.field]]
# path = "Network Activity"
# [[inputs.socket_listener.json_v2.field]]
# path = "Total Transfered"
# [[inputs.socket_listener.json_v2.field]]
# path = "Download Breakdown"
# [[inputs.socket_listener.json_v2.field]]
# path = "Buffer State"
# [[inputs.socket_listener.json_v2.field]]
# path = "Live Latency"
[[inputs.http_listener_v2]]
service_address = ":9091"
paths = ["/webrtc-internals-exporter"]
methods = ["POST"]
http_success_code = 204
data_format = "xpath_json"
[[inputs.http_listener_v2.xpath]]
metric_name = "'statistics'"
metric_selection = "/*"
field_selection = "values/*"
field_name = "type"
field_value = "."
timestamp = "/*/values[last()]/*/timestamp"
timestamp_format = "unix_ms"
[inputs.http_listener_v2.xpath.tags]
url = "url"
state = "state"
id = "''"
[inputs.http_listener_v2.xpath.fields]
#values = "values/*"
peerConnectionId = "id"
peerValues = "concat('[', string-join(values/*, ','), ']')"
[[outputs.file]]
files = ["stdout"]
data_format = "json"
[[outputs.mongodb]]
dsn = "mongodb://192.168.68.204:27017"
database = "peertube"
granularity = "seconds"
#authentication = "SCRAM"
#username = "root"
#password = "example"
+24
View File
@@ -0,0 +1,24 @@
import json
import logging
from http.server import BaseHTTPRequestHandler
class Handler(BaseHTTPRequestHandler):
def __init__(self, custom_func, driver, logger, *args, **kwargs):
self._custom_func = custom_func
self.logger = logger
self.driver = driver
super().__init__(*args, **kwargs)
def do_POST(self):
if self.path == '/webrtc-internals-exporter':
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
self.logger.log(logging.DEBUG, f"POST request,\nPath: {self.path}\nHeaders:\n{self.headers}\n\nBody:\n{post_data.decode('utf-8')}")
self._custom_func(self.driver, json.loads(post_data.decode('utf-8')))
self.send_response(200)
self.end_headers()
self.wfile.write(b'POST request received')
else:
self.send_response(404)
self.end_headers()
self.wfile.write(b'404 Not Found')
@@ -0,0 +1,37 @@
# WebRTC Internals Exporter
A Chromium browser extension that allows to collect WebRTC stats and export them to a Prometheus PushGateway service.
## Install
### Using the Chrome Web Store
[Link](https://chromewebstore.google.com/detail/webrtc-internals-exporter/jbgkajlogkmfemdjhiiicelanbipacpa)
### Using the packed extension
Download the `.crx` file from the [releases page](https://github.com/vpalmisano/webrtc-internals-exporter/releases) and drop it
into the [chrome://extensions/](chrome://extensions/) page.
Alternatively, you can download a `.zip` or `tar.gz` file from the releases page
and load the decompressed folder as an unpacked extension.
Ref. https://developer.chrome.com/docs/extensions/mv3/hosting/
### From sources
Run the `./build.sh` script and load the `build` folder as an unpacked extension
in your Chromium browser after enabling the developer mode.
## Usage
1. Visit the extension options page, set the PushGateway URL and, optionally, the username and password.
2. Load the page where you want to collect the stats and click on the extension icon to enable the stats collection on that URL (disabled by default).
3. The stats will be collected and sent to the PushGateway service. You can use the provided [Grafana dashboard](https://github.com/vpalmisano/webrtc-internals-exporter/tree/main/grafana) to visualize them.
## Debugging
The extension logs are available in the browser console after setting:
```js
localStorage.setItem("webrtc-internal-exporter:debug", "true")
```
The running PeerConnections objects can be manually inspected using the following
command in the browser console:
```js
> webrtcInternalExporter.peerConnections
Map(1) {'b03c3616-3f91-42b5-85df-7dbebefae8bd' => RTCPeerConnection}
```
@@ -0,0 +1,5 @@
(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))l(a);new MutationObserver(a=>{for(const o of a)if(o.type==="childList")for(const i of o.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&l(i)}).observe(document,{childList:!0,subtree:!0});function n(a){const o={};return a.integrity&&(o.integrity=a.integrity),a.referrerPolicy&&(o.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?o.credentials="include":a.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function l(a){if(a.ep)return;a.ep=!0;const o=n(a);fetch(a.href,o)}})();function Ui(e,t){const n=Object.create(null),l=e.split(",");for(let a=0;a<l.length;a++)n[l[a]]=!0;return t?a=>!!n[a.toLowerCase()]:a=>!!n[a]}const Re={},gl=[],Ut=()=>{},Av=()=>!1,Ev=/^on[^a-z]/,lo=e=>Ev.test(e),Ki=e=>e.startsWith("onUpdate:"),Xe=Object.assign,Gi=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},Bv=Object.prototype.hasOwnProperty,Ae=(e,t)=>Bv.call(e,t),be=Array.isArray,hl=e=>oo(e)==="[object Map]",Wu=e=>oo(e)==="[object Set]",we=e=>typeof e=="function",Ge=e=>typeof e=="string",ao=e=>typeof e=="symbol",Ne=e=>e!==null&&typeof e=="object",Uu=e=>(Ne(e)||we(e))&&we(e.then)&&we(e.catch),Ku=Object.prototype.toString,oo=e=>Ku.call(e),$v=e=>oo(e).slice(8,-1),Gu=e=>oo(e)==="[object Object]",Yi=e=>Ge(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,La=Ui(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),io=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},Lv=/-(\w)/g,$t=io(e=>e.replace(Lv,(t,n)=>n?n.toUpperCase():"")),Ov=/\B([A-Z])/g,_l=io(e=>e.replace(Ov,"-$1").toLowerCase()),fn=io(e=>e.charAt(0).toUpperCase()+e.slice(1)),Fo=io(e=>e?`on${fn(e)}`:""),qn=(e,t)=>!Object.is(e,t),Oa=(e,t)=>{for(let n=0;n<e.length;n++)e[n](t)},za=(e,t,n)=>{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,value:n})},Qo=e=>{const t=parseFloat(e);return isNaN(t)?e:t},Fv=e=>{const t=Ge(e)?Number(e):NaN;return isNaN(t)?e:t};let lr;const ei=()=>lr||(lr=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function qi(e){if(be(e)){const t={};for(let n=0;n<e.length;n++){const l=e[n],a=Ge(l)?Nv(l):qi(l);if(a)for(const o in a)t[o]=a[o]}return t}else if(Ge(e)||Ne(e))return e}const Mv=/;(?![^(]*\))/g,Dv=/:([^]+)/,Rv=/\/\*[^]*?\*\//g;function Nv(e){const t={};return e.replace(Rv,"").split(Mv).forEach(n=>{if(n){const l=n.split(Dv);l.length>1&&(t[l[0].trim()]=l[1].trim())}}),t}function Xi(e){let t="";if(Ge(e))t=e;else if(be(e))for(let n=0;n<e.length;n++){const l=Xi(e[n]);l&&(t+=l+" ")}else if(Ne(e))for(const n in e)e[n]&&(t+=n+" ");return t.trim()}const zv="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly",Hv=Ui(zv);function Yu(e){return!!e||e===""}const jv=e=>Ge(e)?e:e==null?"":be(e)||Ne(e)&&(e.toString===Ku||!we(e.toString))?JSON.stringify(e,qu,2):String(e),qu=(e,t)=>t&&t.__v_isRef?qu(e,t.value):hl(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[l,a])=>(n[`${l} =>`]=a,n),{})}:Wu(t)?{[`Set(${t.size})`]:[...t.values()]}:Ne(t)&&!be(t)&&!Gu(t)?String(t):t;let pt;class Xu{constructor(t=!1){this.detached=t,this._active=!0,this.effects=[],this.cleanups=[],this.parent=pt,!t&&pt&&(this.index=(pt.scopes||(pt.scopes=[])).push(this)-1)}get active(){return this._active}run(t){if(this._active){const n=pt;try{return pt=this,t()}finally{pt=n}}}on(){pt=this}off(){pt=this.parent}stop(t){if(this._active){let n,l;for(n=0,l=this.effects.length;n<l;n++)this.effects[n].stop();for(n=0,l=this.cleanups.length;n<l;n++)this.cleanups[n]();if(this.scopes)for(n=0,l=this.scopes.length;n<l;n++)this.scopes[n].stop(!0);if(!this.detached&&this.parent&&!t){const a=this.parent.scopes.pop();a&&a!==this&&(this.parent.scopes[this.index]=a,a.index=this.index)}this.parent=void 0,this._active=!1}}}function Zi(e){return new Xu(e)}function Wv(e,t=pt){t&&t.active&&t.effects.push(e)}function Uv(){return pt}function bt(e){pt&&pt.cleanups.push(e)}const Ji=e=>{const t=new Set(e);return t.w=0,t.n=0,t},Zu=e=>(e.w&Vn)>0,Ju=e=>(e.n&Vn)>0,Kv=({deps:e})=>{if(e.length)for(let t=0;t<e.length;t++)e[t].w|=Vn},Gv=e=>{const{deps:t}=e;if(t.length){let n=0;for(let l=0;l<t.length;l++){const a=t[l];Zu(a)&&!Ju(a)?a.delete(e):t[n++]=a,a.w&=~Vn,a.n&=~Vn}t.length=n}},Ha=new WeakMap;let Wl=0,Vn=1;const ti=30;let zt;const Un=Symbol(""),ni=Symbol("");class Qi{constructor(t,n=null,l){this.fn=t,this.scheduler=n,this.active=!0,this.deps=[],this.parent=void 0,Wv(this,l)}run(){if(!this.active)return this.fn();let t=zt,n=wn;for(;t;){if(t===this)return;t=t.parent}try{return this.parent=zt,zt=this,wn=!0,Vn=1<<++Wl,Wl<=ti?Kv(this):ar(this),this.fn()}finally{Wl<=ti&&Gv(this),Vn=1<<--Wl,zt=this.parent,wn=n,this.parent=void 0,this.deferStop&&this.stop()}}stop(){zt===this?this.deferStop=!0:this.active&&(ar(this),this.onStop&&this.onStop(),tLine truncated
Expected #hex, #hexa, rgb(), rgba(), hsl(), hsla(), object or number`)}function rn(e){const{h:t,s:n,v:l,a}=e,o=s=>{const r=(s+t/60)%6;return l-l*n*Math.max(Math.min(r,4-r,1),0)},i=[o(5),o(3),o(1)].map(s=>Math.round(s*255));return{r:i[0],g:i[1],b:i[2],a}}function cu(e){return rn(ks(e))}function yo(e){if(!e)return{h:0,s:1,v:1,a:1};const t=e.r/255,n=e.g/255,l=e.b/255,a=Math.max(t,n,l),o=Math.min(t,n,l);let i=0;a!==o&&(a===t?i=60*(0+(n-l)/(a-o)):a===n?i=60*(2+(l-t)/(a-o)):a===l&&(i=60*(4+(t-n)/(a-o)))),i<0&&(i=i+360);const s=a===0?0:(a-o)/a,r=[i,s,a];return{h:r[0],s:r[1],v:r[2],a:e.a}}function ud(e){const{h:t,s:n,v:l,a}=e,o=l-l*n/2,i=o===1||o===0?0:(l-o)/Math.min(o,1-o);return{h:t,s:i,l:o,a}}function ks(e){const{h:t,s:n,l,a}=e,o=l+n*Math.min(l,1-l),i=o===0?0:2-2*l/o;return{h:t,s:i,v:o,a}}function cd(e){let{r:t,g:n,b:l,a}=e;return a===void 0?`rgb(${t}, ${n}, ${l})`:`rgba(${t}, ${n}, ${l}, ${a})`}function dd(e){return cd(rn(e))}function $a(e){const t=Math.round(e).toString(16);return("00".substr(0,2-t.length)+t).toUpperCase()}function fd(e){let{r:t,g:n,b:l,a}=e;return`#${[$a(t),$a(n),$a(l),a!==void 0?$a(Math.round(a*255)):""].join("")}`}function vd(e){e=Dh(e);let[t,n,l,a]=gh(e,2).map(o=>parseInt(o,16));return a=a===void 0?a:a/255,{r:t,g:n,b:l,a}}function Mh(e){const t=vd(e);return yo(t)}function md(e){return fd(rn(e))}function Dh(e){return e.startsWith("#")&&(e=e.slice(1)),e=e.replace(/([^0-9a-f])/gi,"F"),(e.length===3||e.length===4)&&(e=e.split("").map(t=>t+t).join("")),e.length!==6&&(e=Yr(Yr(e,6),8,"F")),e}function Rh(e,t){const n=id(ws(e));return n[0]=n[0]+t*10,rd(sd(n))}function Nh(e,t){const n=id(ws(e));return n[0]=n[0]-t*10,rd(sd(n))}function yi(e){const t=At(e);return ws(t)[1]}function zh(e,t){const n=yi(e),l=yi(t),a=Math.max(n,l),o=Math.min(n,l);return(a+.05)/(o+.05)}function gd(e){const t=Math.abs(ru(At(0),At(e)));return Math.abs(ru(At(16777215),At(e)))>Math.min(t,50)?"#fff":"#000"}function O(e,t){return n=>Object.keys(e).reduce((l,a)=>{const i=typeof e[a]=="object"&&e[a]!=null&&!Array.isArray(e[a])?e[a]:{type:e[a]};return n&&a in n?l[a]={...i,default:n[a]}:l[a]=i,t&&!l[a].source&&(l[a].source=t),l},{})}const se=O({class:[String,Array],style:{type:[String,Array,Object],default:null}},"component");function Ot(e){if(e._setup=e._setup??e.setup,!e.name)return e;if(e._setup){e.props=O(e.props??{},e.name)();const t=Object.keys(e.props);e.filterProps=function(l){return Jn(l,t,["class","style"])},e.props._as=String,e.setup=function(l,a){const o=Ps();if(!o.value)return e._setup(l,a);const{props:i,provideSubDefaults:s}=Xh(l,l._as??e.name,o),r=e._setup(i,a);return s(),r}}return e}function H(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:!0;return t=>(e?Ot:Dm)(t)}function Hh(e,t){return t.props=e,t}function Gt(e){let t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:"div",n=arguments.length>2?arguments[2]:void 0;return H()({name:n??fn($t(e.replace(/__/g,"-"))),props:{tag:{type:String,default:t},...se()},setup(l,a){let{slots:o}=a;return()=>{var i;return Pn(l.tag,{class:[e,l.class],style:l.style},(i=o.default)==null?void 0:i.call(o))}}})}function hd(e){if(typeof e.getRootNode!="function"){for(;e.parentNode;)e=e.parentNode;return e!==document?null:document}const t=e.getRootNode();return t!==document&&t.getRootNode({composed:!0})!==document?null:t}const ia="cubic-bezier(0.4, 0, 0.2, 1)",jh="cubic-bezier(0.0, 0, 0.2, 1)",Wh="cubic-bezier(0.4, 0, 1, 1)";function Je(e,t){const n=gs();if(!n)throw new Error(`[Vuetify] ${e} ${t||"must be called from inside a setup function"}`);return n}function mn(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:"composables";const t=Je(e).type;return Gn((t==null?void 0:t.aliasName)||(t==null?void 0:t.name))}let yd=0,Ra=new WeakMap;function ut(){const e=Je("getUid");if(Ra.has(e))return Ra.get(e);{const t=yd++;return Ra.set(e,t),t}}ut.reset=()=>{yd=0,Ra=new WeakMap};function Vs(e){let t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!1;for(;e;){if(t?Uh(e):_s(e))return e;e=e.parentElement}return document.scrollingElement}function Qa(e,t){const n=[];if(t&&e&&!t.contains(e))return n;for(;e&&(_s(e)&&n.push(e),e!==t);)e=e.parentElement;return n}function _s(e){if(!e||e.nodeType!==Node.ELEMENT_NODE)return!1;const t=window.getComputedStyle(e);return t.overflowY==="scroll"||t.overflowY==="auto"&&e.scrollHeight>e.clientHeight}function Uh(e){if(!e||e.nodeType!==Node.ELEMENT_NODE)return!1;const t=window.getComputedStyle(e);return["scroll","auto"].includes(t.overflowY)}function Kh(e){let t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:Je("injectSelf");const{provides:n}=t;if(n&&e in n)return n[e]}function Gh(e){for(;e;){if(window.getComputedStyle(e).position==="fixed")return!0;e=e.offsetParent}return!1}function Z(e){const t=Je("useRender");t.render=e}const kl=Symbol.for("vuetify:defaults");function Yh(e){return Q(e)}function Ps(){const e=Pe(kl);if(!e)throw new Error("[Vuetify] Could not find defaults instance");return e}function Ye(e,t){const n=Ps(),l=Q Line truncated
`,...n.map(l=>` ${l};
`),`}
`)}function yu(e){const t=e.dark?2:1,n=e.dark?1:2,l=[];for(const[a,o]of Object.entries(e.colors)){const i=At(o);l.push(`--v-theme-${a}: ${i.r},${i.g},${i.b}`),a.startsWith("on-")||l.push(`--v-theme-${a}-overlay-multiplier: ${yi(o)>.18?t:n}`)}for(const[a,o]of Object.entries(e.variables)){const i=typeof o=="string"&&o.startsWith("#")?At(o):void 0,s=i?`${i.r}, ${i.g}, ${i.b}`:void 0;l.push(`--v-${a}: ${s??o}`)}return l}const pi={"001":1,AD:1,AE:6,AF:6,AG:0,AI:1,AL:1,AM:1,AN:1,AR:1,AS:0,AT:1,AU:1,AX:1,AZ:1,BA:1,BD:0,BE:1,BG:1,BH:6,BM:1,BN:1,BR:0,BS:0,BT:0,BW:0,BY:1,BZ:0,CA:0,CH:1,CL:1,CM:1,CN:1,CO:0,CR:1,CY:1,CZ:1,DE:1,DJ:6,DK:1,DM:0,DO:0,DZ:6,EC:1,EE:1,EG:6,ES:1,ET:0,FI:1,FJ:1,FO:1,FR:1,GB:1,"GB-alt-variant":0,GE:1,GF:1,GP:1,GR:1,GT:0,GU:0,HK:0,HN:0,HR:1,HU:1,ID:0,IE:1,IL:0,IN:0,IQ:6,IR:6,IS:1,IT:1,JM:0,JO:6,JP:0,KE:0,KG:1,KH:0,KR:0,KW:6,KZ:1,LA:0,LB:1,LI:1,LK:1,LT:1,LU:1,LV:1,LY:6,MC:1,MD:1,ME:1,MH:0,MK:1,MM:0,MN:1,MO:0,MQ:1,MT:0,MV:5,MX:0,MY:1,MZ:0,NI:0,NL:1,NO:1,NP:0,NZ:1,OM:6,PA:0,PE:0,PH:0,PK:0,PL:1,PR:0,PT:0,PY:0,QA:6,RE:1,RO:1,RS:1,RU:1,SA:0,SD:6,SE:1,SG:0,SI:1,SK:1,SM:1,SV:0,SY:6,TH:0,TJ:1,TM:1,TR:1,TT:0,TW:0,UA:1,UM:0,US:0,UY:1,UZ:1,VA:1,VE:0,VI:0,VN:1,WS:0,XK:1,YE:0,ZA:0,ZW:0};function my(e,t){const n=[];let l=[];const a=wd(e),o=kd(e),i=(a.getDay()-pi[t.slice(-2).toUpperCase()]+7)%7,s=(o.getDay()-pi[t.slice(-2).toUpperCase()]+7)%7;for(let r=0;r<i;r++){const u=new Date(a);u.setDate(u.getDate()-(i-r)),l.push(u)}for(let r=1;r<=o.getDate();r++){const u=new Date(e.getFullYear(),e.getMonth(),r);l.push(u),l.length===7&&(n.push(l),l=[])}for(let r=1;r<7-s;r++){const u=new Date(o);u.setDate(u.getDate()+r),l.push(u)}return n.push(l),n}function wd(e){return new Date(e.getFullYear(),e.getMonth(),1)}function kd(e){return new Date(e.getFullYear(),e.getMonth()+1,0)}function gy(e){const t=e.split("-").map(Number);return new Date(t[0],t[1]-1,t[2])}const hy=/([12]\d{3}-([1-9]|0[1-9]|1[0-2])-([1-9]|0[1-9]|[12]\d|3[01]))/;function Vd(e){if(e==null)return new Date;if(e instanceof Date)return e;if(typeof e=="string"){let t;if(hy.test(e))return gy(e);if(t=Date.parse(e),!isNaN(t))return new Date(t)}return null}const bu=new Date(2e3,0,2);function yy(e){const t=pi[e.slice(-2).toUpperCase()];return jt(7).map(n=>{const l=new Date(bu);return l.setDate(bu.getDate()+t+n),new Intl.DateTimeFormat(e,{weekday:"narrow"}).format(l)})}function by(e,t,n){let l={};switch(t){case"fullDateWithWeekday":l={weekday:"long",day:"numeric",month:"long",year:"numeric"};break;case"normalDateWithWeekday":l={weekday:"short",day:"numeric",month:"short"};break;case"keyboardDate":l={day:"2-digit",month:"2-digit",year:"numeric"};break;case"monthAndDate":l={month:"long",day:"numeric"};break;case"monthAndYear":l={month:"long",year:"numeric"};break;case"dayOfMonth":l={day:"numeric"};break;case"shortDate":l={year:"numeric"};break;default:l={timeZone:"UTC",timeZoneName:"short"}}return new Intl.DateTimeFormat(n,l).format(Vd(e)??void 0)}function Sy(e,t){const n=e.toJsDate(t),l=n.getFullYear(),a=qr(String(n.getMonth()+1),2,"0"),o=qr(String(n.getDate()),2,"0");return`${l}-${a}-${o}`}function Cy(e){const[t,n,l]=e.split("-").map(Number);return new Date(t,n-1,l)}function py(e,t){const n=new Date(e);return n.setDate(n.getDate()+t),n}function xy(e,t){const n=new Date(e);return n.setMonth(n.getMonth()+t),n}function wy(e){return e.getFullYear()}function ky(e){return e.getMonth()}function Vy(e){return new Date(e.getFullYear(),0,1)}function _y(e){return new Date(e.getFullYear(),11,31)}function Py(e,t){return xi(e,t[0])&&Ty(e,t[1])}function Iy(e){if(!e||e==null)return!1;const t=new Date(e);return t instanceof Date&&!isNaN(t.getTime())}function xi(e,t){return e.getTime()>t.getTime()}function Ty(e,t){return e.getTime()<t.getTime()}function Su(e,t){return e.getTime()===t.getTime()}function Ay(e,t){return e.getDate()===t.getDate()&&e.getMonth()===t.getMonth()&&e.getFullYear()===t.getFullYear()}function Ey(e,t){return e.getMonth()===t.getMonth()&&e.getFullYear()===t.getFullYear()}function By(e,t,n){const l=new Date(e),a=new Date(t);return n==="month"?l.getMonth()-a.getMonth()+(l.getFullYear()-a.getFullYear())*12:Math.floor((l.getTime()-a.getTime())/(1e3*60*60*24))}function $y(e,t){const n=new Date(e);return n.setFullYear(t),n}function Ly(e){return new Date(e.getFullYear(),e.getMonth(),e.getDate())}function Oy(e){return new Date(e.getFullYear(),e.getMonth(),e.getDate(),23,59,59,999)}class Fy{constructor(t){this.locale=t.locale}date(t){return Vd(t)}toJsDate(t){return t}toISO(t){return Sy(this,t)}parseISO(t){return Cy(t)}addDays(t,n){return py(t,n)}addMonths(t,n){return xy(t,n)}getWeekArray(t){return my(t,this.locale)}startOfMonth(t){return wd(t)}endOfMonth(t){return kd(t)}format(t,n){return by(t,n,this.locale)}isEqual(t,n){return Su(t,n)}isValid(t){return Iy(t)}isWithinRange(t,n){return Py(t,n)}isAfter(t,n){return xi(t,n)}isBefore(t,n){return!xi(t,n)&&!Su(t,n)}isSameDay(t,n){return Ay(t,n)}isSameMonth(t,n){return Ey(t,n)}setYear(t,n){return $y(t,n)}getDiff(t,n,l){return By(t,n,l)}g Line truncated
@@ -0,0 +1,5 @@
@keyframes v-shake{59%{margin-left:0}60%,80%{margin-left:2px}70%,90%{margin-left:-2px}}.bg-black{background-color:#000!important;color:#fff!important}.bg-white{background-color:#fff!important;color:#000!important}.bg-transparent{background-color:transparent!important;color:currentColor!important}.bg-red{background-color:#f44336!important;color:#fff!important}.bg-red-lighten-5{background-color:#ffebee!important;color:#000!important}.bg-red-lighten-4{background-color:#ffcdd2!important;color:#000!important}.bg-red-lighten-3{background-color:#ef9a9a!important;color:#000!important}.bg-red-lighten-2{background-color:#e57373!important;color:#fff!important}.bg-red-lighten-1{background-color:#ef5350!important;color:#fff!important}.bg-red-darken-1{background-color:#e53935!important;color:#fff!important}.bg-red-darken-2{background-color:#d32f2f!important;color:#fff!important}.bg-red-darken-3{background-color:#c62828!important;color:#fff!important}.bg-red-darken-4{background-color:#b71c1c!important;color:#fff!important}.bg-red-accent-1{background-color:#ff8a80!important;color:#000!important}.bg-red-accent-2{background-color:#ff5252!important;color:#fff!important}.bg-red-accent-3{background-color:#ff1744!important;color:#fff!important}.bg-red-accent-4{background-color:#d50000!important;color:#fff!important}.bg-pink{background-color:#e91e63!important;color:#fff!important}.bg-pink-lighten-5{background-color:#fce4ec!important;color:#000!important}.bg-pink-lighten-4{background-color:#f8bbd0!important;color:#000!important}.bg-pink-lighten-3{background-color:#f48fb1!important;color:#000!important}.bg-pink-lighten-2{background-color:#f06292!important;color:#fff!important}.bg-pink-lighten-1{background-color:#ec407a!important;color:#fff!important}.bg-pink-darken-1{background-color:#d81b60!important;color:#fff!important}.bg-pink-darken-2{background-color:#c2185b!important;color:#fff!important}.bg-pink-darken-3{background-color:#ad1457!important;color:#fff!important}.bg-pink-darken-4{background-color:#880e4f!important;color:#fff!important}.bg-pink-accent-1{background-color:#ff80ab!important;color:#fff!important}.bg-pink-accent-2{background-color:#ff4081!important;color:#fff!important}.bg-pink-accent-3{background-color:#f50057!important;color:#fff!important}.bg-pink-accent-4{background-color:#c51162!important;color:#fff!important}.bg-purple{background-color:#9c27b0!important;color:#fff!important}.bg-purple-lighten-5{background-color:#f3e5f5!important;color:#000!important}.bg-purple-lighten-4{background-color:#e1bee7!important;color:#000!important}.bg-purple-lighten-3{background-color:#ce93d8!important;color:#fff!important}.bg-purple-lighten-2{background-color:#ba68c8!important;color:#fff!important}.bg-purple-lighten-1{background-color:#ab47bc!important;color:#fff!important}.bg-purple-darken-1{background-color:#8e24aa!important;color:#fff!important}.bg-purple-darken-2{background-color:#7b1fa2!important;color:#fff!important}.bg-purple-darken-3{background-color:#6a1b9a!important;color:#fff!important}.bg-purple-darken-4{background-color:#4a148c!important;color:#fff!important}.bg-purple-accent-1{background-color:#ea80fc!important;color:#fff!important}.bg-purple-accent-2{background-color:#e040fb!important;color:#fff!important}.bg-purple-accent-3{background-color:#d500f9!important;color:#fff!important}.bg-purple-accent-4{background-color:#a0f!important;color:#fff!important}.bg-deep-purple{background-color:#673ab7!important;color:#fff!important}.bg-deep-purple-lighten-5{background-color:#ede7f6!important;color:#000!important}.bg-deep-purple-lighten-4{background-color:#d1c4e9!important;color:#000!important}.bg-deep-purple-lighten-3{background-color:#b39ddb!important;color:#fff!important}.bg-deep-purple-lighten-2{background-color:#9575cd!important;color:#fff!important}.bg-deep-purple-lighten-1{background-color:#7e57c2!important;color:#fff!important}.bg-deep-purple-darken-1{background-color:#5e35b1!important;color:#fff!important}.bg-deep-purple-darken-2{background-color:#512da8!important;color:#fff!important}.bg-deep-purple-darken-3{background-color:#4527a0!important;color:#fff!important}.bg-deep-purple-darken-4{background-color:#311b92!important;color:#fff!important}.bg-deep-purple-accent-1{background-color:#b388ff!important;color:#fff!important}.bg-deep-purple-accent-2{background-color:#7c4dff!important;color:#fff!important}.bg-deep-purple-accent-3{background-color:#651fff!important;color:#fff!important}.bg-deep-purple-accent-4{background-color:#6200ea!important;color:#fff!important}.bg-indigo{background-color:#3f51b5!important;color:#fff!important}.bg-indigo-lighten-5{background-color:#e8eaf6!important;color:#000!important}.bg-indigo-lighten-4{background-color:#c5cae9!important;color:#000!important}.bg-indigo-lighten-3{background-color:#9fa8da!important;color:#fff!important}.bg-indigo-lighten-2{background-color:#7986cb!important;color:#fff!important}.bg-indigo-lighten-1{background-color:#5c6bc0!important;color:#fff!important}.bg-indigo-daLine truncated
* ress.css • v2.0.4
* MIT License
* github.com/filipelinhares/ress
*/html{box-sizing:border-box;overflow-y:scroll;-webkit-text-size-adjust:100%;word-break:normal;-moz-tab-size:4;tab-size:4}*,:before,:after{background-repeat:no-repeat;box-sizing:inherit}:before,:after{text-decoration:inherit;vertical-align:inherit}*{padding:0;margin:0}hr{overflow:visible;height:0}details,main{display:block}summary{display:list-item}small{font-size:80%}[hidden]{display:none}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}a{background-color:transparent}a:active,a:hover{outline-width:0}code,kbd,pre,samp{font-family:monospace,monospace}pre{font-size:1em}b,strong{font-weight:bolder}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}input{border-radius:0}[disabled]{cursor:default}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}textarea{overflow:auto;resize:vertical}button,input,optgroup,select,textarea{font:inherit}optgroup{font-weight:700}button{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit],[role=button]{cursor:pointer;color:inherit}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{outline:1px dotted ButtonText}button,html [type=button],[type=reset],[type=submit]{-webkit-appearance:button}button,input,select,textarea{background-color:transparent;border-style:none}select{-moz-appearance:none;-webkit-appearance:none}select::-ms-expand{display:none}select::-ms-value{color:currentColor}legend{border:0;color:inherit;display:table;white-space:normal;max-width:100%}::-webkit-file-upload-button{-webkit-appearance:button;color:inherit;font:inherit}::-ms-clear,::-ms-reveal{display:none}img{border-style:none}progress{vertical-align:baseline}@media screen{[hidden~=screen]{display:inherit}[hidden~=screen]:not(:active):not(:focus):not(:target){position:absolute!important;clip:rect(0 0 0 0)!important}}[aria-busy=true]{cursor:progress}[aria-controls]{cursor:pointer}[aria-disabled=true]{cursor:default}.dialog-transition-enter-active,.dialog-bottom-transition-enter-active,.dialog-top-transition-enter-active{transition-duration:225ms!important;transition-timing-function:cubic-bezier(0,0,.2,1)!important}.dialog-transition-leave-active,.dialog-bottom-transition-leave-active,.dialog-top-transition-leave-active{transition-duration:125ms!important;transition-timing-function:cubic-bezier(.4,0,1,1)!important}.dialog-transition-enter-active,.dialog-transition-leave-active,.dialog-bottom-transition-enter-active,.dialog-bottom-transition-leave-active,.dialog-top-transition-enter-active,.dialog-top-transition-leave-active{transition-property:transform,opacity!important;pointer-events:none}.dialog-transition-enter-from,.dialog-transition-leave-to{transform:scale(.9);opacity:0}.dialog-transition-enter-to,.dialog-transition-leave-from{opacity:1}.dialog-bottom-transition-enter-from,.dialog-bottom-transition-leave-to{transform:translateY(calc(50vh + 50%))}.dialog-top-transition-enter-from,.dialog-top-transition-leave-to{transform:translateY(calc(-50vh - 50%))}.picker-transition-enter-active,.picker-reverse-transition-enter-active,.picker-transition-leave-active,.picker-reverse-transition-leave-active{transition-duration:.3s!important;transition-timing-function:cubic-bezier(.4,0,.2,1)!important}.picker-transition-move,.picker-reverse-transition-move{transition-duration:.5s!important;transition-property:transform!important;transition-timing-function:cubic-bezier(.4,0,.2,1)!important}.picker-transition-enter-from,.picker-transition-leave-to,.picker-reverse-transition-enter-from,.picker-reverse-transition-leave-to{opacity:0}.picker-transition-leave-from,.picker-transition-leave-active,.picker-transition-leave-to,.picker-reverse-transition-leave-from,.picker-reverse-transition-leave-active,.picker-reverse-transition-leave-to{position:absolute!important}.picker-transition-enter-active,.picker-transition-leave-active,.picker-reverse-transition-enter-active,.picker-reverse-transition-leave-active{transition-property:transform,opacity!important}.picker-transition-enter-active,.picker-transition-leave-active{transition-duration:.3s!important;transition-timing-function:cubic-bezier(.4,0,.2,1)!important}.picker-transition-move{transition-duration:.5s!important;transition-property:transform!important;transition-timing-function:cubic-bezier(.4,0,.2,1)!important}.picker-transition-enter-from{transform:translateY(100%)}.picker-transition-leave-to{transform:translateY(-100%)}.picker-reverse-transition-enter-active,.picker-reverse-transition-leave-active{transition-duraLine truncated
@@ -0,0 +1 @@
import{g as Re,r as j,o as je,w as T,i as K,a as G,b as Ve,c as w,u as $,d as Pe,e as Ee,f as Y,p as ee,n as te,_ as Ce,h as R,j as re,k as ze,l as _,m as g,q as N,t as U,s as D,v as Se,x as Ae,y as Ie,z as ke}from"./_plugin-vue_export-helper-deb87276.js";function ne(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(s){return Object.getOwnPropertyDescriptor(e,s).enumerable})),n.push.apply(n,r)}return n}function z(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]!=null?arguments[t]:{};t%2?ne(Object(n),!0).forEach(function(r){Te(e,r,n[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):ne(Object(n)).forEach(function(r){Object.defineProperty(e,r,Object.getOwnPropertyDescriptor(n,r))})}return e}function Te(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function ae(e){let t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:[];return Object.keys(e).reduce((n,r)=>(t.includes(r)||(n[r]=$(e[r])),n),{})}function B(e){return typeof e=="function"}function Le(e){return Pe(e)||Ee(e)}function ce(e,t,n){let r=e;const s=t.split(".");for(let i=0;i<s.length;i++){if(!r[s[i]])return n;r=r[s[i]]}return r}function Z(e,t,n){return w(()=>e.some(r=>ce(t,r,{[n]:!1})[n]))}function oe(e,t,n){return w(()=>e.reduce((r,s)=>{const i=ce(t,s,{[n]:!1})[n]||[];return r.concat(i)},[]))}function de(e,t,n,r){return e.call(r,$(t),$(n),r)}function fe(e){return e.$valid!==void 0?!e.$valid:!e}function Ne(e,t,n,r,s,i,v){let{$lazy:d,$rewardEarly:u}=s,o=arguments.length>7&&arguments[7]!==void 0?arguments[7]:[],m=arguments.length>8?arguments[8]:void 0,p=arguments.length>9?arguments[9]:void 0,h=arguments.length>10?arguments[10]:void 0;const f=j(!!r.value),a=j(0);n.value=!1;const l=T([t,r].concat(o,h),()=>{if(d&&!r.value||u&&!p.value&&!n.value)return;let c;try{c=de(e,t,m,v)}catch(y){c=Promise.reject(y)}a.value++,n.value=!!a.value,f.value=!1,Promise.resolve(c).then(y=>{a.value--,n.value=!!a.value,i.value=y,f.value=fe(y)}).catch(y=>{a.value--,n.value=!!a.value,i.value=y,f.value=!0})},{immediate:!0,deep:typeof t=="object"});return{$invalid:f,$unwatch:l}}function Ue(e,t,n,r,s,i,v,d){let{$lazy:u,$rewardEarly:o}=r;const m=()=>({}),p=w(()=>{if(u&&!n.value||o&&!d.value)return!1;let h=!0;try{const f=de(e,t,v,i);s.value=f,h=fe(f)}catch(f){s.value=f}return h});return{$unwatch:m,$invalid:p}}function De(e,t,n,r,s,i,v,d,u,o,m){const p=j(!1),h=e.$params||{},f=j(null);let a,l;e.$async?{$invalid:a,$unwatch:l}=Ne(e.$validator,t,p,n,r,f,s,e.$watchTargets,u,o,m):{$invalid:a,$unwatch:l}=Ue(e.$validator,t,n,r,f,s,u,o);const c=e.$message;return{$message:B(c)?w(()=>c(ae({$pending:p,$invalid:a,$params:ae(h),$model:t,$response:f,$validator:i,$propertyPath:d,$property:v}))):c||"",$params:h,$pending:p,$invalid:a,$response:f,$unwatch:l}}function Fe(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};const t=$(e),n=Object.keys(t),r={},s={},i={};let v=null;return n.forEach(d=>{const u=t[d];switch(!0){case B(u.$validator):r[d]=u;break;case B(u):r[d]={$validator:u};break;case d==="$validationGroups":v=u;break;case d.startsWith("$"):i[d]=u;break;default:s[d]=u}}),{rules:r,nestedValidators:s,config:i,validationGroups:v}}const Be="__root";function Me(e,t,n,r,s,i,v,d,u){const o=Object.keys(e),m=r.get(s,e),p=j(!1),h=j(!1),f=j(0);if(m){if(!m.$partial)return m;m.$unwatch(),p.value=m.$dirty.value}const a={$dirty:p,$path:s,$touch:()=>{p.value||(p.value=!0)},$reset:()=>{p.value&&(p.value=!1)},$commit:()=>{}};return o.length?(o.forEach(l=>{a[l]=De(e[l],t,a.$dirty,i,v,l,n,s,u,h,f)}),a.$externalResults=w(()=>d.value?[].concat(d.value).map((l,c)=>({$propertyPath:s,$property:n,$validator:"$externalResults",$uid:`${s}-externalResult-${c}`,$message:l,$params:{},$response:null,$pending:!1})):[]),a.$invalid=w(()=>{const l=o.some(c=>$(a[c].$invalid));return h.value=l,!!a.$externalResults.value.length||l}),a.$pending=w(()=>o.some(l=>$(a[l].$pending))),a.$error=w(()=>a.$dirty.value?a.$pending.value||a.$invalid.value:!1),a.$silentErrors=w(()=>o.filter(l=>$(a[l].$invalid)).map(l=>{const c=a[l];return G({$propertyPath:s,$property:n,$validator:l,$uid:`${s}-${l}`,$message:c.$message,$params:c.$params,$response:c.$response,$pending:c.$pending})}).concat(a.$externalResults.value)),a.$errors=w(()=>a.$dirty.value?a.$silentErrors.value:[]),a.$unwatch=()=>o.forEach(l=>{a[l].$unwatch()}),a.$commit=()=>{h.value=!0,f.value=Date.now()},r.set(s,e,a),a):(m&&r.set(s,e,a),a)}function Ge(e,t,n,r,s,i,v){const d=Object.keys(e);return d.length?d.reduce((u,o)=>(u[o]=J({validations:e[o],state:t,key:o,parentKey:n,resultsCache:r,globalConfig:s,instance:i,externalResults:v}),u),{}):{}}function qe(e,t,n){const r=w(()=>[t,n].filter(a=>a).reduce((a,l)=>a.concat(Object.values($(l))),[])),s=w({get(){return e.$dirty.value||(r.value.length?r.value.every(a=>a.$dirty):!1)},set(a){e.$dirty.value=a}}),i=w(()=>{const a=$(e.$silentErrors)||[],Line truncated
@@ -0,0 +1 @@
.version[data-v-4cce5501]{text-decoration:none}
@@ -0,0 +1,2 @@
/*! pako 2.1.0 https://github.com/nodeca/pako @license (MIT AND Zlib) */
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).pako={})}(this,(function(t){"use strict";function e(t){let e=t.length;for(;--e>=0;)t[e]=0}const a=256,i=286,n=30,s=15,r=new Uint8Array([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0]),o=new Uint8Array([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13]),l=new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7]),h=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),d=new Array(576);e(d);const _=new Array(60);e(_);const f=new Array(512);e(f);const c=new Array(256);e(c);const u=new Array(29);e(u);const w=new Array(n);function m(t,e,a,i,n){this.static_tree=t,this.extra_bits=e,this.extra_base=a,this.elems=i,this.max_length=n,this.has_stree=t&&t.length}let b,g,p;function k(t,e){this.dyn_tree=t,this.max_code=0,this.stat_desc=e}e(w);const v=t=>t<256?f[t]:f[256+(t>>>7)],y=(t,e)=>{t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255},x=(t,e,a)=>{t.bi_valid>16-a?(t.bi_buf|=e<<t.bi_valid&65535,y(t,t.bi_buf),t.bi_buf=e>>16-t.bi_valid,t.bi_valid+=a-16):(t.bi_buf|=e<<t.bi_valid&65535,t.bi_valid+=a)},z=(t,e,a)=>{x(t,a[2*e],a[2*e+1])},A=(t,e)=>{let a=0;do{a|=1&t,t>>>=1,a<<=1}while(--e>0);return a>>>1},E=(t,e,a)=>{const i=new Array(16);let n,r,o=0;for(n=1;n<=s;n++)o=o+a[n-1]<<1,i[n]=o;for(r=0;r<=e;r++){let e=t[2*r+1];0!==e&&(t[2*r]=A(i[e]++,e))}},R=t=>{let e;for(e=0;e<i;e++)t.dyn_ltree[2*e]=0;for(e=0;e<n;e++)t.dyn_dtree[2*e]=0;for(e=0;e<19;e++)t.bl_tree[2*e]=0;t.dyn_ltree[512]=1,t.opt_len=t.static_len=0,t.sym_next=t.matches=0},Z=t=>{t.bi_valid>8?y(t,t.bi_buf):t.bi_valid>0&&(t.pending_buf[t.pending++]=t.bi_buf),t.bi_buf=0,t.bi_valid=0},U=(t,e,a,i)=>{const n=2*e,s=2*a;return t[n]<t[s]||t[n]===t[s]&&i[e]<=i[a]},S=(t,e,a)=>{const i=t.heap[a];let n=a<<1;for(;n<=t.heap_len&&(n<t.heap_len&&U(e,t.heap[n+1],t.heap[n],t.depth)&&n++,!U(e,i,t.heap[n],t.depth));)t.heap[a]=t.heap[n],a=n,n<<=1;t.heap[a]=i},D=(t,e,i)=>{let n,s,l,h,d=0;if(0!==t.sym_next)do{n=255&t.pending_buf[t.sym_buf+d++],n+=(255&t.pending_buf[t.sym_buf+d++])<<8,s=t.pending_buf[t.sym_buf+d++],0===n?z(t,s,e):(l=c[s],z(t,l+a+1,e),h=r[l],0!==h&&(s-=u[l],x(t,s,h)),n--,l=v(n),z(t,l,i),h=o[l],0!==h&&(n-=w[l],x(t,n,h)))}while(d<t.sym_next);z(t,256,e)},T=(t,e)=>{const a=e.dyn_tree,i=e.stat_desc.static_tree,n=e.stat_desc.has_stree,r=e.stat_desc.elems;let o,l,h,d=-1;for(t.heap_len=0,t.heap_max=573,o=0;o<r;o++)0!==a[2*o]?(t.heap[++t.heap_len]=d=o,t.depth[o]=0):a[2*o+1]=0;for(;t.heap_len<2;)h=t.heap[++t.heap_len]=d<2?++d:0,a[2*h]=1,t.depth[h]=0,t.opt_len--,n&&(t.static_len-=i[2*h+1]);for(e.max_code=d,o=t.heap_len>>1;o>=1;o--)S(t,a,o);h=r;do{o=t.heap[1],t.heap[1]=t.heap[t.heap_len--],S(t,a,1),l=t.heap[1],t.heap[--t.heap_max]=o,t.heap[--t.heap_max]=l,a[2*h]=a[2*o]+a[2*l],t.depth[h]=(t.depth[o]>=t.depth[l]?t.depth[o]:t.depth[l])+1,a[2*o+1]=a[2*l+1]=h,t.heap[1]=h++,S(t,a,1)}while(t.heap_len>=2);t.heap[--t.heap_max]=t.heap[1],((t,e)=>{const a=e.dyn_tree,i=e.max_code,n=e.stat_desc.static_tree,r=e.stat_desc.has_stree,o=e.stat_desc.extra_bits,l=e.stat_desc.extra_base,h=e.stat_desc.max_length;let d,_,f,c,u,w,m=0;for(c=0;c<=s;c++)t.bl_count[c]=0;for(a[2*t.heap[t.heap_max]+1]=0,d=t.heap_max+1;d<573;d++)_=t.heap[d],c=a[2*a[2*_+1]+1]+1,c>h&&(c=h,m++),a[2*_+1]=c,_>i||(t.bl_count[c]++,u=0,_>=l&&(u=o[_-l]),w=a[2*_],t.opt_len+=w*(c+u),r&&(t.static_len+=w*(n[2*_+1]+u)));if(0!==m){do{for(c=h-1;0===t.bl_count[c];)c--;t.bl_count[c]--,t.bl_count[c+1]+=2,t.bl_count[h]--,m-=2}while(m>0);for(c=h;0!==c;c--)for(_=t.bl_count[c];0!==_;)f=t.heap[--d],f>i||(a[2*f+1]!==c&&(t.opt_len+=(c-a[2*f+1])*a[2*f],a[2*f+1]=c),_--)}})(t,e),E(a,d,t.bl_count)},O=(t,e,a)=>{let i,n,s=-1,r=e[1],o=0,l=7,h=4;for(0===r&&(l=138,h=3),e[2*(a+1)+1]=65535,i=0;i<=a;i++)n=r,r=e[2*(i+1)+1],++o<l&&n===r||(o<h?t.bl_tree[2*n]+=o:0!==n?(n!==s&&t.bl_tree[2*n]++,t.bl_tree[32]++):o<=10?t.bl_tree[34]++:t.bl_tree[36]++,o=0,s=n,0===r?(l=138,h=3):n===r?(l=6,h=3):(l=7,h=4))},I=(t,e,a)=>{let i,n,s=-1,r=e[1],o=0,l=7,h=4;for(0===r&&(l=138,h=3),i=0;i<=a;i++)if(n=r,r=e[2*(i+1)+1],!(++o<l&&n===r)){if(o<h)do{z(t,n,t.bl_tree)}while(0!=--o);else 0!==n?(n!==s&&(z(t,n,t.bl_tree),o--),z(t,16,t.bl_tree),x(t,o-3,2)):o<=10?(z(t,17,t.bl_tree),x(t,o-3,3)):(z(t,18,t.bl_tree),x(t,o-11,7));o=0,s=n,0===r?(l=138,h=3):n===r?(l=6,h=3):(l=7,h=4)}};let F=!1;const L=(t,e,a,i)=>{x(t,0+(i?1:0),3),Z(t),y(t,a),y(t,~a),a&&t.pending_buf.set(t.window.subarray(e,e+a),t.pending),t.pending+=a};var N=(t,e,i,n)=>{let s,r,o=0;t.level>0?(2===t.strm.data_type&&(t.strm.data_type=(t=>{let e,i=4093624447;for(e=0;e<=31;e++,i>>>=1)if(1&i&&0!==t.dyn_ltree[2*e])return 0;if(0!==t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return 1;for(e=32;e<a;e++)if(0!==t.dyn_ltree[2*e])return 1;return 0})(t)),T(t,t.l_desc),T(t,t.d_desc),o=(t=>{let e;for(O(t,t.dyn_ltree,t.l_desc.max_code),O(t,t.dyn_dtree,t.d_desc.max_code),T(t,t.bl_desc),e=18;e>=3&&0===t.blLine truncated
@@ -0,0 +1 @@
import{_ as y,a as O,h as o,j as c,k as _,l as a,m as t,x,q as w,t as k,z as V}from"./_plugin-vue_export-helper-deb87276.js";const C={class:"version",href:"https://github.com/vpalmisano/webrtc-internals-exporter",target:"_blank",title:"Homepage"},B={__name:"Popup",setup(N){var i;const e=O({version:"0.1.9",error:"",info:"",enabled:!1,origin:"",enabledOrigins:{}});async function d(){const{enabledOrigins:n}=await chrome.storage.sync.get("enabledOrigins");e.enabledOrigins=n,e.enabled=!!e.enabledOrigins[e.origin]}async function p(n){if(console.log("saveOptions",e.origin,n),n)e.enabledOrigins={...e.enabledOrigins,[e.origin]:n};else{const r={...e.enabledOrigins};delete r[e.origin],e.enabledOrigins=r}chrome.storage&&await chrome.storage.sync.set({enabledOrigins:e.enabledOrigins})}return(i=chrome.tabs)==null||i.query({active:!0,currentWindow:!0}).then(n=>{const r=n[0];return chrome.scripting.executeScript({target:{tabId:r.id},function:()=>window.location.origin})}).then(n=>{e.origin=n[0].result}).then(()=>d()).catch(n=>{console.error("Load options error:",n),e.error=`Load options error: ${n.message}`}),(n,r)=>{const u=o("v-app-bar"),g=o("v-alert"),m=o("v-checkbox"),s=o("v-col"),l=o("v-row"),b=o("v-container"),v=o("v-main"),f=o("v-layout");return c(),_(f,null,{default:a(()=>[t(u,{title:"WebRTC Internals Exporter",color:"primary",density:"compact"}),t(v,{class:"d-flex align-center justify-left",style:{"min-width":"20rem"}},{default:a(()=>[t(b,null,{default:a(()=>[t(l,null,{default:a(()=>[t(s,{cols:"12",md:"12"},{default:a(()=>[e.error.length>0?(c(),_(g,{key:0,text:e.error,type:"error"},null,8,["text"])):x("",!0),t(m,{color:"indigo",modelValue:e.enabled,"onUpdate:modelValue":[r[0]||(r[0]=h=>e.enabled=h),p],label:"Enable for "+e.origin,"hide-details":""},null,8,["modelValue","label"])]),_:1})]),_:1}),t(l,null,{default:a(()=>[t(s,{cols:"12",md:"12"},{default:a(()=>[w("a",C,"v"+k(e.version),1)]),_:1})]),_:1})]),_:1})]),_:1})]),_:1})}}},I=y(B,[["__scopeId","data-v-16634bc6"]]);V(I);
@@ -0,0 +1 @@
.version[data-v-16634bc6]{font-size:smaller;text-decoration:none}
@@ -0,0 +1,373 @@
/* global chrome, pako */
function log(...args) {
console.log.apply(null, ["[webrtc-internal-exporter:background]", ...args]);
}
log("loaded");
import "/assets/pako.min.js";
const DEFAULT_OPTIONS = {
url: "http://localhost:9092",
username: "",
password: "",
updateInterval: 2,
gzip: false,
job: "webrtc-internals-exporter",
enabledOrigins: {
"https://tube.kobim.cloud": true,
},
enabledStats: ["data-channel", "local-candidate", "remote-candidate"]
};
const options = {};
// Handle install/update.
chrome.runtime.onInstalled.addListener(async ({ reason }) => {
log("onInstalled", reason);
if (reason === "install") {
await chrome.storage.sync.set(DEFAULT_OPTIONS);
} else if (reason === "update") {
const options = await chrome.storage.sync.get();
await chrome.storage.sync.set({
...DEFAULT_OPTIONS,
...options,
});
}
await chrome.alarms.create("webrtc-internals-exporter-alarm", {
delayInMinutes: 1,
periodInMinutes: 1,
});
});
async function updateTabInfo(tab) {
const tabId = tab.id;
const origin = new URL(tab.url || tab.pendingUrl).origin;
if (options.enabledOrigins && options.enabledOrigins[origin] === true) {
const { peerConnectionsPerOrigin } = await chrome.storage.local.get(
"peerConnectionsPerOrigin",
);
const peerConnections =
(peerConnectionsPerOrigin && peerConnectionsPerOrigin[origin]) || 0;
chrome.action.setTitle({
title: `WebRTC Internals Exporter\nActive Peer Connections: ${peerConnections}`,
tabId,
});
chrome.action.setBadgeText({ text: `${peerConnections}`, tabId });
chrome.action.setBadgeBackgroundColor({ color: "rgb(63, 81, 181)", tabId });
} else {
chrome.action.setTitle({
title: `WebRTC Internals Exporter (disabled)`,
tabId,
});
chrome.action.setBadgeText({ text: "", tabId });
}
}
async function optionsUpdated() {
const [tab] = await chrome.tabs.query({
active: true,
lastFocusedWindow: true,
});
await updateTabInfo(tab);
}
chrome.storage.sync.get().then((ret) => {
Object.assign(options, ret);
log("options loaded");
optionsUpdated();
});
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== "sync") return;
for (let [key, { newValue }] of Object.entries(changes)) {
options[key] = newValue;
}
log("options changed");
optionsUpdated();
});
chrome.tabs.onActivated.addListener(async ({ tabId }) => {
try {
const tab = await chrome.tabs.get(tabId);
await updateTabInfo(tab);
} catch (err) {
log(`get tab error: ${err.message}`);
}
});
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo) => {
if (!changeInfo.url) return;
await updateTabInfo({ id: tabId, url: changeInfo.url });
});
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === "webrtc-internals-exporter-alarm") {
cleanupPeerConnections().catch((err) => {
log(`cleanup peer connections error: ${err.message}`);
});
}
});
async function setPeerConnectionLastUpdate({ id, origin }, lastUpdate = 0) {
let { peerConnectionsLastUpdate } = await chrome.storage.local.get(
"peerConnectionsLastUpdate",
);
if (!peerConnectionsLastUpdate) {
peerConnectionsLastUpdate = {};
}
if (lastUpdate) {
peerConnectionsLastUpdate[id] = { origin, lastUpdate };
} else {
delete peerConnectionsLastUpdate[id];
}
await chrome.storage.local.set({ peerConnectionsLastUpdate });
const peerConnectionsPerOrigin = {};
Object.values(peerConnectionsLastUpdate).forEach(({ origin: o }) => {
if (!peerConnectionsPerOrigin[o]) {
peerConnectionsPerOrigin[o] = 0;
}
peerConnectionsPerOrigin[o]++;
});
await chrome.storage.local.set({ peerConnectionsPerOrigin });
await optionsUpdated();
}
async function cleanupPeerConnections() {
let { peerConnectionsLastUpdate } = await chrome.storage.local.get(
"peerConnectionsLastUpdate",
);
if (
!peerConnectionsLastUpdate ||
!Object.keys(peerConnectionsLastUpdate).length
) {
return;
}
log(
`checking stale peer connections (${
Object.keys(peerConnectionsLastUpdate).length
} total)`,
);
const now = Date.now();
await Promise.allSettled(
Object.entries(peerConnectionsLastUpdate)
.map(([id, { origin, lastUpdate }]) => {
if (
now - lastUpdate >
Math.max(2 * options.updateInterval, 30) * 1000
) {
return { id, origin };
}
})
.filter((ret) => !!ret?.id)
.map(({ id, origin }) => {
log(`removing stale peer connection metrics: ${id} ${origin}`);
return sendData("DELETE", { id, origin });
}),
);
}
// Send data to pushgateway.
async function sendData(method, { id, origin }, data) {
const { url, username, password, gzip, job } = options;
const headers = {
"Content-Type": "application/json",
};
if (username && password) {
headers.Authorization = "Basic " + btoa(`${username}:${password}`);
}
if (data && gzip) {
headers["Content-Encoding"] = "gzip";
data = await pako.gzip(data);
}
log(`sendData: ${data} \n ${data.length} bytes (gzip: ${gzip}) url: ${url} job: ${job}`);
const start = Date.now();
const response = await fetch(
`${url}/metrics/job/${job}/peerConnectionId/${id}`,
{
method,
headers,
body: method === "POST" ? data : undefined,
},
);
const stats = await chrome.storage.local.get([
"messagesSent",
"bytesSent",
"totalTime",
"errors",
]);
if (data) {
stats.messagesSent = (stats.messagesSent || 0) + 1;
stats.bytesSent = (stats.bytesSent || 0) + data.length;
stats.totalTime = (stats.totalTime || 0) + Date.now() - start;
}
if (!response.ok) {
stats.errors = (stats.errors || 0) + 1;
}
await chrome.storage.local.set(stats);
if (!response.ok) {
const text = await response.text();
throw new Error(`Response status: ${response.status} error: ${text}`);
}
await setPeerConnectionLastUpdate(
{ id, origin },
method === "POST" ? start : undefined,
);
return response.text();
}
async function sendJsonData(method, { id, origin }, data) {
const { url, username, password, gzip, job } = options;
const headers = {
"Content-Type": "application/json",
};
if (username && password) {
headers.Authorization = "Basic " + btoa(`${username}:${password}`);
}
if (data && gzip) {
headers["Content-Encoding"] = "gzip";
data = await pako.gzip(data);
}
log(`sendData: ${data} \n ${data.length} bytes (gzip: ${gzip}) url: ${url} job: ${job}`);
const start = Date.now();
const response = await fetch(
`${url}/${job}`,
{
method,
headers,
body: method === "POST" ? data : undefined,
},
);
const stats = await chrome.storage.local.get([
"messagesSent",
"bytesSent",
"totalTime",
"errors",
]);
if (data) {
stats.messagesSent = (stats.messagesSent || 0) + 1;
stats.bytesSent = (stats.bytesSent || 0) + data.length;
stats.totalTime = (stats.totalTime || 0) + Date.now() - start;
}
if (!response.ok) {
stats.errors = (stats.errors || 0) + 1;
}
await chrome.storage.local.set(stats);
if (!response.ok) {
const text = await response.text();
throw new Error(`Response status: ${response.status} error: ${text}`);
}
await setPeerConnectionLastUpdate(
{ id, origin },
method === "POST" ? start : undefined,
);
return response.text();
}
const QualityLimitationReasons = {
none: 0,
bandwidth: 1,
cpu: 2,
other: 3,
};
/**
* sendPeerConnectionStats
* @param {string} url
* @param {string} id
* @param {RTCPeerConnectionState} state
* @param {any} values
*/
async function sendPeerConnectionStats(url, id, state, values) {
const origin = new URL(url).origin;
if (state === "closed") {
return sendData("DELETE", { id, origin });
}
let data = "";
const sentTypes = new Set();
values.forEach((value) => {
const type = value.type.replace(/-/g, "_");
const labels = [`pageUrl="${url}"`];
const metrics = [];
if (value.type === "peer-connection") {
labels.push(`state="${state}"`);
}
Object.entries(value).forEach(([key, v]) => {
if (typeof v === "number") {
metrics.push([key, v]);
} else if (typeof v === "object") {
Object.entries(v).forEach(([subkey, subv]) => {
if (typeof subv === "number") {
metrics.push([`${key}_${subkey}`, subv]);
}
});
} else if (
key === "qualityLimitationReason" &&
QualityLimitationReasons[v] !== undefined
) {
metrics.push([key, QualityLimitationReasons[v]]);
} else if (key === "googTimingFrameInfo") {
// TODO
} else {
labels.push(`${key}="${v}"`);
}
});
metrics.forEach(([key, v]) => {
const name = `${type}_${key.replace(/-/g, "_")}`;
let typeDesc = "";
if (!sentTypes.has(name)) {
typeDesc = `# TYPE ${name} gauge\n`;
sentTypes.add(name);
}
data += `${typeDesc}${name}{${labels.join(",")}} ${v}\n`;
});
});
if (data.length > 0) {
return sendData("POST", { id, origin }, data + "\n");
}
}
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.event === "peer-connection-stats") {
const { url, id, state, values } = message.data;
sendData("POST", { id, origin: new URL(url).origin }, JSON.stringify(message.data))
.then(() => {
sendResponse({});
})
.catch((err) => {
sendResponse({ error: err.message });
});
} else if (message.event === "peer-connections-stats") {
const { stats } = message.data;
sendJsonData("POST", { id: "all", origin: "all" }, JSON.stringify(message.data))
} else {
sendResponse({ error: "unknown event" });
}
return true;
});
@@ -0,0 +1,123 @@
/* global chrome */
if (window.location.protocol.startsWith("http")) {
const log = (...args) => {
try {
if (localStorage.getItem("webrtc-internal-exporter:debug") === "true") {
console.log.apply(null, [
"[webrtc-internal-exporter:content-script]",
...args,
]);
}
} catch (error) {
// Ignore localStorage errors.
}
};
const injectScript = (file_path) => {
const head = document.querySelector("head");
const script = document.createElement("script");
script.setAttribute("type", "text/javascript");
script.setAttribute("src", file_path);
head.appendChild(script);
};
setTimeout(() => injectScript(chrome.runtime.getURL("override.js")));
// Handle options.
const options = {
url: "",
enabled: false,
updateInterval: 2000,
enabledStats: [],
};
const sendOptions = () => {
window.postMessage({
event: "webrtc-internal-exporter:options",
options,
});
};
try {
chrome.storage.sync
.get(["url", "enabledOrigins", "updateInterval", "enabledStats"])
.then((ret) => {
log(`options loaded:`, ret);
options.url = ret.url || "";
options.enabled =
ret.enabledOrigins &&
ret.enabledOrigins[window.location.origin] === true;
options.updateInterval = (ret.updateInterval || 2) * 1000;
options.enabledStats = Object.values(ret.enabledStats || {});
sendOptions();
});
chrome.storage.onChanged.addListener((changes, area) => {
if (area !== "sync") return;
let changed = false;
for (let [key, { newValue }] of Object.entries(changes)) {
if (key === "url") {
options.url = newValue;
changed = true;
} else if (key === "enabledOrigins") {
options.enabled = newValue[window.location.origin] === true;
changed = true;
} else if (key === "updateInterval") {
options.updateInterval = newValue * 1000;
changed = true;
} else if (key === "enabledStats") {
options.enabledStats = Object.values(newValue);
changed = true;
}
}
if (changed) {
log(`options changed:`, options);
sendOptions();
}
});
// Handle stats messages.
window.addEventListener("message", async (message) => {
const { event, url, id, state, values, stats } = message.data;
if (event === "webrtc-internal-exporter:ready") {
sendOptions();
} else if (event === "webrtc-internal-exporter:peer-connection-stats") {
try {
const response = await chrome.runtime.sendMessage({
event: "peer-connection-stats",
data: {
url,
id,
state,
values,
},
});
if (response.error) {
log(`error: ${response.error}`);
}
} catch (error) {
log(`error: ${error.message}`);
}
} else if (event === "webrtc-internal-exporter:peer-connections-stats") {
try {
const response = await chrome.runtime.sendMessage({
event: "peer-connections-stats",
data: stats,
});
if (response.error) {
log(`error: ${response.error}`);
}
} catch (error) {
log(`error: ${error.message}`);
}
}
});
} catch (error) {
console.error(
`[webrtc-internal-exporter:content-script] error: ${error.message}`,
error,
);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebRTC Internals Exporter</title>
<script type="module" crossorigin src="/assets/options-8c2aaa1b.js"></script>
<link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-deb87276.js">
<link rel="stylesheet" href="/assets/_plugin-vue_export-helper.css">
<link rel="stylesheet" href="/assets/options.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
@@ -0,0 +1,135 @@
function log(...args) {
console.log.apply(null, ["[webrtc-internal-exporter:override]", ...args]);
}
log("Override RTCPeerConnection.");
class WebrtcInternalExporter {
peerConnections = new Map();
url = "";
enabled = false;
updateInterval = 2000;
enabledStats = [];
constructor() {
window.addEventListener("message", async (message) => {
const { event, options } = message.data;
if (event === "webrtc-internal-exporter:options") {
log("options updated:", options);
Object.assign(this, options);
}
});
window.postMessage({ event: "webrtc-internal-exporter:ready" });
this.collectAllStats();
}
randomId() {
return (
window.crypto?.randomUUID() || (2 ** 64 * Math.random()).toString(16)
);
}
add(pc) {
const id = this.randomId();
this.peerConnections.set(id, pc);
pc.addEventListener("connectionstatechange", () => {
if (pc.connectionState === "closed") {
this.peerConnections.delete(id);
}
});
//this.collectAndPostStats(id);
}
async collectAndPostSingleStat(id) {
const stats = await this.collectStats(id, this.collectAndPostSingleStat);
if (Object.keys(stats).length === 0 || !stats) return;
window.postMessage(
{
event: "webrtc-internal-exporter:peer-connection-stats",
...stats,
},
[stats],
);
}
async collectAllStats() {
const stats = [];
for (const [id, pc] of this.peerConnections) {
if (this.url && this.enabled && pc.connectionState === "connected") {
const pcStats = await this.collectStats(id, pc);
stats.push(pcStats);
}
}
//if (stats.length !== 0) {
window.postMessage(
{
event: "webrtc-internal-exporter:peer-connections-stats",
stats,
},
[stats],
);
log(`Stats collected:`, stats);
//}
setTimeout(this.collectAllStats.bind(this), this.updateInterval);
return stats;
}
async collectStats(id, pc, binding) {
var completeStats = {};
if (!pc) {
pc = this.peerConnections.get(id);
if (!pc) return;
}
if (this.url && this.enabled && pc.connectionState === "connected") {
try {
const stats = await pc.getStats();
const values = [...stats.values()].filter(
(v) =>
["peer-connection", ...this.enabledStats].indexOf(v.type) !== -1
);
completeStats = {
url: window.location.href,
id,
state: pc.connectionState,
values,
};
} catch (error) {
log(`collectStats error: ${error.message}`);
}
}
if (pc.connectionState === "closed") {
this.peerConnections.delete(id);
} else {
if (binding) {
setTimeout(binding.bind(this), this.updateInterval, id);
}
}
return completeStats;
}
}
const webrtcInternalExporter = new WebrtcInternalExporter();
window.RTCPeerConnection = new Proxy(window.RTCPeerConnection, {
construct(target, argumentsList) {
log(`RTCPeerConnection`, argumentsList);
const pc = new target(...argumentsList);
webrtcInternalExporter.add(pc);
return pc;
},
});
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebRTC Internals Exporter</title>
<script type="module" crossorigin src="/assets/popup-7cc154e5.js"></script>
<link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-deb87276.js">
<link rel="stylesheet" href="/assets/_plugin-vue_export-helper.css">
<link rel="stylesheet" href="/assets/popup.css">
</head>
<body>
<div id="app"></div>
</body>
</html>