Compare commits
15 Commits
main
...
b9a45869ef
Author | SHA1 | Date | |
---|---|---|---|
b9a45869ef | |||
8b7073ba73 | |||
1b544e410e | |||
6950fcd3eb | |||
a53338e645 | |||
24623b7825 | |||
6bbe5b5155 | |||
dacb59437a | |||
6b9fb5d5ee | |||
81d04091cd | |||
aab4726e45 | |||
3c4972e5a2 | |||
6b648f5491 | |||
6ba64413d1 | |||
ad97310d7e |
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "peertube/statnerd"]
|
||||
path = peertube/statnerd
|
||||
url = ssh://git@gitea.kobim.cloud:30009/kobim/peertube-collector.git
|
1
peertube/README.md
Normal file
1
peertube/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# TODO
|
@@ -1,5 +1,3 @@
|
||||
selenium
|
||||
beautifulsoup4
|
||||
pymongo
|
||||
pandas
|
||||
matplotlib
|
1
peertube/statnerd
Submodule
1
peertube/statnerd
Submodule
Submodule peertube/statnerd added at 3a31a30fc7
@@ -1,17 +0,0 @@
|
||||
## Statistiche per nerd da Peer Tube
|
||||
|
||||
spero funzioni
|
||||
|
||||
per installarlo baste creare un nuovo ambiente e installare le dipendenze con:
|
||||
|
||||
```
|
||||
python -m venv env
|
||||
source .\env\bin\activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
per eseguire il tutto basta lanciare:
|
||||
|
||||
```
|
||||
jupyter-lab
|
||||
```
|
@@ -1,190 +0,0 @@
|
||||
import signal
|
||||
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
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver import ActionChains
|
||||
from selenium.webdriver.support.wait import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def setupLogger():
|
||||
logging_format = "[%(asctime)s] (%(levelname)s) %(module)s - %(funcName)s: %(message)s"
|
||||
logging.basicConfig(level=logging.INFO, format=logging_format)
|
||||
(logger := logging.getLogger(__name__)).setLevel(logging.INFO)
|
||||
logger.propagate = False
|
||||
(logger_handler := logging.StreamHandler()).setFormatter(
|
||||
ColoredFormatter(fmt=logging_format)
|
||||
)
|
||||
logger.addHandler(logger_handler)
|
||||
|
||||
def interrupt_handler(signum, driver: webdriver.Chrome):
|
||||
logger.info(f'Handling signal {signum} ({signal.Signals(signum).name}).')
|
||||
|
||||
driver.quit()
|
||||
raise SystemExit
|
||||
|
||||
def setupChromeDriver():
|
||||
logger.log(logging.INFO, 'Setting up Chrome driver.')
|
||||
chrome_options = Options()
|
||||
#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'})
|
||||
|
||||
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: 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()
|
||||
logger.log(logging.DEBUG, 'Sent stats to socket.')
|
||||
except socket.error as e:
|
||||
logger.error(f'Got socket error: {e}')
|
||||
|
||||
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')
|
||||
else:
|
||||
raise ValueError("html is None")
|
||||
|
||||
stats = htmlBS.find_all('div', attrs={'style': 'display: block;'})
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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, 'Peers': peerBytes}
|
||||
|
||||
if 'Buffer State' == stat:
|
||||
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() * 1000),
|
||||
'session': driver.session_id
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
driver.get(url)
|
||||
|
||||
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()
|
||||
wait.until(ec.visibility_of_element_located((By.CLASS_NAME, 'vjs-control-bar')))
|
||||
actions.context_click(driver.find_element(By.CLASS_NAME ,'video-js')).perform()
|
||||
statsForNerds = driver.find_elements(By.CLASS_NAME ,'vjs-menu-item')
|
||||
actions.click(statsForNerds[-1]).perform()
|
||||
wait.until(ec.text_to_be_present_in_element((By.CLASS_NAME, 'vjs-stats-list'), 'Player'))
|
||||
actions.move_to_element(driver.find_element(By.CLASS_NAME ,'vjs-control-bar')).perform()
|
||||
logger.log(logging.INFO, 'Stats setup complete.')
|
||||
|
||||
return driver
|
||||
|
||||
if __name__ == '__main__':
|
||||
setupLogger()
|
||||
|
||||
driver = setupChromeDriver()
|
||||
|
||||
signal.signal(signal.SIGINT, lambda signum, frame: interrupt_handler(signum, driver))
|
||||
|
||||
setupStats(driver, "https://tube.kobim.cloud/w/iN2T8PmbSb4HJTDA2rV3sg")
|
||||
|
||||
logger.log(logging.INFO, 'Starting server collector.')
|
||||
httpd = HTTPServer(('localhost', 9092), partial(Handler, downloadStats, driver, logger))
|
||||
logger.info('Server collector started.')
|
||||
httpd.serve_forever()
|
@@ -1,33 +0,0 @@
|
||||
[[processors.dedup]]
|
||||
dedup_interval = "600s"
|
||||
|
||||
[[inputs.socket_listener]]
|
||||
service_address = "udp://:8094"
|
||||
data_format = "xpath_json"
|
||||
[[inputs.socket_listener.xpath]]
|
||||
metric_name = "'peertube'"
|
||||
metric_selection = "/*"
|
||||
|
||||
timestamp = "timestamp"
|
||||
timestamp_format = "unix_ms"
|
||||
|
||||
[inputs.socket_listener.xpath.tags]
|
||||
url = "url"
|
||||
session = "session"
|
||||
#id = ??
|
||||
#state = ??
|
||||
|
||||
[inputs.socket_listener.xpath.fields]
|
||||
player = "player"
|
||||
peers = "peers"
|
||||
|
||||
[[outputs.file]]
|
||||
files = ["stdout"]
|
||||
data_format = "json"
|
||||
|
||||
[[outputs.mongodb]]
|
||||
dsn = "mongodb://stats_user:%40z%5EVFhN7q%25vzit@192.168.86.120:27017/?authSource=statistics"
|
||||
database = "statistics"
|
||||
granularity = "seconds"
|
||||
|
||||
# docker run --rm -v .\peertube\statnerd\telegraf.conf:/etc/telegraf/telegraf.conf:ro -p 8094:8094/udp telegraf
|
@@ -1,43 +0,0 @@
|
||||
import logging
|
||||
|
||||
class ColoredFormatter(logging.Formatter):
|
||||
"""Colored formatter for the logging package."""
|
||||
|
||||
def __init__(
|
||||
self, fmt=None, datefmt=None, style="%", validate=True, *, defaults=None
|
||||
):
|
||||
"""Colored formatter for the logging package."""
|
||||
fmt = fmt or "%(levelname)s: %(message)s"
|
||||
super().__init__(fmt, datefmt, style, validate, defaults=defaults)
|
||||
colors = {
|
||||
"red": "\x1b[31;20m",
|
||||
"bold_red": "\x1b[31;1m",
|
||||
"green": "\x1b[32;20m",
|
||||
"bold_green": "\x1b[32;1m",
|
||||
"yellow": "\x1b[33;20m",
|
||||
"bold_yellow": "\x1b[33;1m",
|
||||
"blue": "\x1b[34;20m",
|
||||
"bold_blue": "\x1b[34;1m",
|
||||
"grey": "\x1b[37;20m",
|
||||
"bold_grey": "\x1b[37;1m",
|
||||
"reset": "\x1b[0m",
|
||||
}
|
||||
self._default_formatter = logging.Formatter(fmt)
|
||||
self._formatters = {
|
||||
100: logging.Formatter(colors["bold_blue"] + fmt + colors["reset"]),
|
||||
logging.DEBUG: logging.Formatter(colors["grey"] + fmt + colors["reset"]),
|
||||
logging.INFO: logging.Formatter(colors["green"] + fmt + colors["reset"]),
|
||||
logging.WARNING: logging.Formatter(
|
||||
colors["yellow"] + fmt + colors["reset"]
|
||||
),
|
||||
logging.ERROR: logging.Formatter(colors["red"] + fmt + colors["reset"]),
|
||||
logging.CRITICAL: logging.Formatter(
|
||||
colors["bold_red"] + fmt + colors["reset"]
|
||||
),
|
||||
}
|
||||
|
||||
def format(self, record):
|
||||
"""Override of logging.Formatter.format"""
|
||||
return self._formatters.get(record.levelno, self._default_formatter).format(
|
||||
record
|
||||
)
|
@@ -1,24 +0,0 @@
|
||||
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')
|
@@ -1,37 +0,0 @@
|
||||
# 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}
|
||||
```
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.version[data-v-4cce5501]{text-decoration:none}
|
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
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);
|
@@ -1 +0,0 @@
|
||||
.version[data-v-16634bc6]{font-size:smaller;text-decoration:none}
|
@@ -1,373 +0,0 @@
|
||||
/* 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;
|
||||
});
|
@@ -1,123 +0,0 @@
|
||||
/* 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.
Before Width: | Height: | Size: 5.6 KiB |
Binary file not shown.
Before Width: | Height: | Size: 718 B |
Binary file not shown.
Before Width: | Height: | Size: 2.0 KiB |
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"name": "WebRTC Internals Exporter",
|
||||
"description": "WebRTC Internals Exporter",
|
||||
"author": "Vittorio Palmisano",
|
||||
"version": "0.1.9",
|
||||
"manifest_version": 3,
|
||||
"icons": {
|
||||
"16": "images/icon16.png",
|
||||
"48": "images/icon48.png",
|
||||
"128": "images/icon128.png"
|
||||
},
|
||||
"permissions": [
|
||||
"storage",
|
||||
"activeTab",
|
||||
"tabs",
|
||||
"scripting",
|
||||
"alarms"
|
||||
],
|
||||
"host_permissions": [],
|
||||
"action": {
|
||||
"default_title": "WebRTC Internals Exporter",
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
"options_ui": {
|
||||
"page": "options.html",
|
||||
"open_in_tab": true
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"https://*/*",
|
||||
"http://*/*"
|
||||
],
|
||||
"js": [
|
||||
"content-script.js"
|
||||
],
|
||||
"run_at": "document_start",
|
||||
"all_frames": true,
|
||||
"match_about_blank": true
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js",
|
||||
"type": "module"
|
||||
},
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": [
|
||||
"override.js"
|
||||
],
|
||||
"matches": [
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
<!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>
|
@@ -1,135 +0,0 @@
|
||||
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;
|
||||
},
|
||||
});
|
@@ -1,16 +0,0 @@
|
||||
<!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>
|
Reference in New Issue
Block a user