copy from main tesi folder
This commit is contained in:
37
webrtc-internals-exporter/README.md
Normal file
37
webrtc-internals-exporter/README.md
Normal file
@@ -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}
|
||||
```
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
webrtc-internals-exporter/assets/materialdesignicons-webfont.eot
Normal file
BIN
webrtc-internals-exporter/assets/materialdesignicons-webfont.eot
Normal file
Binary file not shown.
BIN
webrtc-internals-exporter/assets/materialdesignicons-webfont.ttf
Normal file
BIN
webrtc-internals-exporter/assets/materialdesignicons-webfont.ttf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
1
webrtc-internals-exporter/assets/options-8c2aaa1b.js
Normal file
1
webrtc-internals-exporter/assets/options-8c2aaa1b.js
Normal file
File diff suppressed because one or more lines are too long
1
webrtc-internals-exporter/assets/options.css
Normal file
1
webrtc-internals-exporter/assets/options.css
Normal file
@@ -0,0 +1 @@
|
||||
.version[data-v-4cce5501]{text-decoration:none}
|
2
webrtc-internals-exporter/assets/pako.min.js
vendored
Normal file
2
webrtc-internals-exporter/assets/pako.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
webrtc-internals-exporter/assets/popup-7cc154e5.js
Normal file
1
webrtc-internals-exporter/assets/popup-7cc154e5.js
Normal file
@@ -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);
|
1
webrtc-internals-exporter/assets/popup.css
Normal file
1
webrtc-internals-exporter/assets/popup.css
Normal file
@@ -0,0 +1 @@
|
||||
.version[data-v-16634bc6]{font-size:smaller;text-decoration:none}
|
373
webrtc-internals-exporter/background.js
Normal file
373
webrtc-internals-exporter/background.js
Normal file
@@ -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://collector: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;
|
||||
});
|
123
webrtc-internals-exporter/content-script.js
Normal file
123
webrtc-internals-exporter/content-script.js
Normal file
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
BIN
webrtc-internals-exporter/images/icon128.png
Normal file
BIN
webrtc-internals-exporter/images/icon128.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.6 KiB |
BIN
webrtc-internals-exporter/images/icon16.png
Normal file
BIN
webrtc-internals-exporter/images/icon16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 718 B |
BIN
webrtc-internals-exporter/images/icon48.png
Normal file
BIN
webrtc-internals-exporter/images/icon48.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
57
webrtc-internals-exporter/manifest.json
Normal file
57
webrtc-internals-exporter/manifest.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"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://*/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
16
webrtc-internals-exporter/options.html
Normal file
16
webrtc-internals-exporter/options.html
Normal file
@@ -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>
|
135
webrtc-internals-exporter/override.js
Normal file
135
webrtc-internals-exporter/override.js
Normal file
@@ -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;
|
||||
},
|
||||
});
|
16
webrtc-internals-exporter/popup.html
Normal file
16
webrtc-internals-exporter/popup.html
Normal file
@@ -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>
|
Reference in New Issue
Block a user