Compare commits

...

3 Commits

32 changed files with 4 additions and 1065 deletions

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "peertube/statnerd"]
path = peertube/statnerd
url = https://gitea.kobim.cloud/kobim/peertube-collector

View File

@@ -1,5 +1,3 @@
selenium
beautifulsoup4
pymongo pymongo
pandas pandas
matplotlib matplotlib

1
peertube/statnerd Submodule

Submodule peertube/statnerd added at b8d9300bca

View File

@@ -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()

View File

@@ -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

View File

@@ -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
)

View File

@@ -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')

View File

@@ -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

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.version[data-v-4cce5501]{text-decoration:none}

File diff suppressed because one or more lines are too long

View File

@@ -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);

View File

@@ -1 +0,0 @@
.version[data-v-16634bc6]{font-size:smaller;text-decoration:none}

View File

@@ -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;
});

View File

@@ -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

View File

@@ -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://*/*"
]
}
]
}

View File

@@ -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>

View File

@@ -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;
},
});

View File

@@ -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>