copy from main tesi folder
This commit is contained in:
298
.gitignore
vendored
Normal file
298
.gitignore
vendored
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
## Core latex/pdflatex auxiliary files:
|
||||||
|
*.aux
|
||||||
|
*.lof
|
||||||
|
*.log
|
||||||
|
*.lot
|
||||||
|
*.fls
|
||||||
|
*.out
|
||||||
|
*.toc
|
||||||
|
*.fmt
|
||||||
|
*.fot
|
||||||
|
*.cb
|
||||||
|
*.cb2
|
||||||
|
.*.lb
|
||||||
|
|
||||||
|
## Intermediate documents:
|
||||||
|
*.dvi
|
||||||
|
*.xdv
|
||||||
|
*-converted-to.*
|
||||||
|
# these rules might exclude image files for figures etc.
|
||||||
|
# *.ps
|
||||||
|
# *.eps
|
||||||
|
*.pdf
|
||||||
|
|
||||||
|
!/logistica_tirocinio/*
|
||||||
|
!/papers/*
|
||||||
|
Tesi.pdf
|
||||||
|
.DS_STORE
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
## Bibliography auxiliary files (bibtex/biblatex/biber):
|
||||||
|
*.bbl
|
||||||
|
*.bcf
|
||||||
|
*.blg
|
||||||
|
*-blx.aux
|
||||||
|
*-blx.bib
|
||||||
|
*.run.xml
|
||||||
|
|
||||||
|
## Build tool auxiliary files:
|
||||||
|
*.fdb_latexmk
|
||||||
|
*.synctex
|
||||||
|
*.synctex(busy)
|
||||||
|
*.synctex.gz
|
||||||
|
*.synctex.gz(busy)
|
||||||
|
*.pdfsync
|
||||||
|
|
||||||
|
## Build tool directories for auxiliary files
|
||||||
|
# latexrun
|
||||||
|
latex.out/
|
||||||
|
|
||||||
|
## Auxiliary and intermediate files from other packages:
|
||||||
|
# algorithms
|
||||||
|
*.alg
|
||||||
|
*.loa
|
||||||
|
|
||||||
|
# achemso
|
||||||
|
acs-*.bib
|
||||||
|
|
||||||
|
# amsthm
|
||||||
|
*.thm
|
||||||
|
|
||||||
|
# beamer
|
||||||
|
*.nav
|
||||||
|
*.pre
|
||||||
|
*.snm
|
||||||
|
*.vrb
|
||||||
|
|
||||||
|
# changes
|
||||||
|
*.soc
|
||||||
|
|
||||||
|
# comment
|
||||||
|
*.cut
|
||||||
|
|
||||||
|
# cprotect
|
||||||
|
*.cpt
|
||||||
|
|
||||||
|
# elsarticle (documentclass of Elsevier journals)
|
||||||
|
*.spl
|
||||||
|
|
||||||
|
# endnotes
|
||||||
|
*.ent
|
||||||
|
|
||||||
|
# fixme
|
||||||
|
*.lox
|
||||||
|
|
||||||
|
# feynmf/feynmp
|
||||||
|
*.mf
|
||||||
|
*.mp
|
||||||
|
*.t[1-9]
|
||||||
|
*.t[1-9][0-9]
|
||||||
|
*.tfm
|
||||||
|
|
||||||
|
#(r)(e)ledmac/(r)(e)ledpar
|
||||||
|
*.end
|
||||||
|
*.?end
|
||||||
|
*.[1-9]
|
||||||
|
*.[1-9][0-9]
|
||||||
|
*.[1-9][0-9][0-9]
|
||||||
|
*.[1-9]R
|
||||||
|
*.[1-9][0-9]R
|
||||||
|
*.[1-9][0-9][0-9]R
|
||||||
|
*.eledsec[1-9]
|
||||||
|
*.eledsec[1-9]R
|
||||||
|
*.eledsec[1-9][0-9]
|
||||||
|
*.eledsec[1-9][0-9]R
|
||||||
|
*.eledsec[1-9][0-9][0-9]
|
||||||
|
*.eledsec[1-9][0-9][0-9]R
|
||||||
|
|
||||||
|
# glossaries
|
||||||
|
*.acn
|
||||||
|
*.acr
|
||||||
|
*.glg
|
||||||
|
*.glo
|
||||||
|
*.gls
|
||||||
|
*.glsdefs
|
||||||
|
*.lzo
|
||||||
|
*.lzs
|
||||||
|
|
||||||
|
# uncomment this for glossaries-extra (will ignore makeindex's style files!)
|
||||||
|
*.ist
|
||||||
|
|
||||||
|
# gnuplottex
|
||||||
|
*-gnuplottex-*
|
||||||
|
|
||||||
|
# gregoriotex
|
||||||
|
*.gaux
|
||||||
|
*.glog
|
||||||
|
*.gtex
|
||||||
|
|
||||||
|
# htlatex
|
||||||
|
*.4ct
|
||||||
|
*.4tc
|
||||||
|
*.idv
|
||||||
|
*.lg
|
||||||
|
*.trc
|
||||||
|
*.xref
|
||||||
|
|
||||||
|
# hyperref
|
||||||
|
*.brf
|
||||||
|
|
||||||
|
# knitr
|
||||||
|
*-concordance.tex
|
||||||
|
# TODO Uncomment the next line if you use knitr and want to ignore its generated tikz files
|
||||||
|
# *.tikz
|
||||||
|
*-tikzDictionary
|
||||||
|
|
||||||
|
# listings
|
||||||
|
*.lol
|
||||||
|
|
||||||
|
# luatexja-ruby
|
||||||
|
*.ltjruby
|
||||||
|
|
||||||
|
# makeidx
|
||||||
|
*.idx
|
||||||
|
*.ilg
|
||||||
|
*.ind
|
||||||
|
|
||||||
|
# minitoc
|
||||||
|
*.maf
|
||||||
|
*.mlf
|
||||||
|
*.mlt
|
||||||
|
*.mtc[0-9]*
|
||||||
|
*.slf[0-9]*
|
||||||
|
*.slt[0-9]*
|
||||||
|
*.stc[0-9]*
|
||||||
|
|
||||||
|
# minted
|
||||||
|
_minted*
|
||||||
|
*.pyg
|
||||||
|
|
||||||
|
# morewrites
|
||||||
|
*.mw
|
||||||
|
|
||||||
|
# newpax
|
||||||
|
*.newpax
|
||||||
|
|
||||||
|
# nomencl
|
||||||
|
*.nlg
|
||||||
|
*.nlo
|
||||||
|
*.nls
|
||||||
|
|
||||||
|
# pax
|
||||||
|
*.pax
|
||||||
|
|
||||||
|
# pdfpcnotes
|
||||||
|
*.pdfpc
|
||||||
|
|
||||||
|
# sagetex
|
||||||
|
*.sagetex.sage
|
||||||
|
*.sagetex.py
|
||||||
|
*.sagetex.scmd
|
||||||
|
|
||||||
|
# scrwfile
|
||||||
|
*.wrt
|
||||||
|
|
||||||
|
# sympy
|
||||||
|
*.sout
|
||||||
|
*.sympy
|
||||||
|
sympy-plots-for-*.tex/
|
||||||
|
|
||||||
|
# pdfcomment
|
||||||
|
*.upa
|
||||||
|
*.upb
|
||||||
|
|
||||||
|
# pythontex
|
||||||
|
*.pytxcode
|
||||||
|
pythontex-files-*/
|
||||||
|
|
||||||
|
# tcolorbox
|
||||||
|
*.listing
|
||||||
|
|
||||||
|
# thmtools
|
||||||
|
*.loe
|
||||||
|
|
||||||
|
# TikZ & PGF
|
||||||
|
*.dpth
|
||||||
|
*.md5
|
||||||
|
*.auxlock
|
||||||
|
|
||||||
|
# todonotes
|
||||||
|
*.tdo
|
||||||
|
|
||||||
|
# vhistory
|
||||||
|
*.hst
|
||||||
|
*.ver
|
||||||
|
|
||||||
|
# easy-todo
|
||||||
|
*.lod
|
||||||
|
|
||||||
|
# xcolor
|
||||||
|
*.xcp
|
||||||
|
|
||||||
|
# xmpincl
|
||||||
|
*.xmpi
|
||||||
|
|
||||||
|
# xindy
|
||||||
|
*.xdy
|
||||||
|
|
||||||
|
# xypic precompiled matrices and outlines
|
||||||
|
*.xyc
|
||||||
|
*.xyd
|
||||||
|
|
||||||
|
# endfloat
|
||||||
|
*.ttt
|
||||||
|
*.fff
|
||||||
|
|
||||||
|
# Latexian
|
||||||
|
TSWLatexianTemp*
|
||||||
|
|
||||||
|
## Editors:
|
||||||
|
# WinEdt
|
||||||
|
*.bak
|
||||||
|
*.sav
|
||||||
|
|
||||||
|
# Texpad
|
||||||
|
.texpadtmp
|
||||||
|
|
||||||
|
# LyX
|
||||||
|
*.lyx~
|
||||||
|
|
||||||
|
# Kile
|
||||||
|
*.backup
|
||||||
|
|
||||||
|
# gummi
|
||||||
|
.*.swp
|
||||||
|
|
||||||
|
# KBibTeX
|
||||||
|
*~[0-9]*
|
||||||
|
|
||||||
|
# TeXnicCenter
|
||||||
|
*.tps
|
||||||
|
|
||||||
|
# auto folder when using emacs and auctex
|
||||||
|
./auto/*
|
||||||
|
*.el
|
||||||
|
|
||||||
|
# expex forward references with \gathertags
|
||||||
|
*-tags.tex
|
||||||
|
|
||||||
|
# standalone packages
|
||||||
|
*.sta
|
||||||
|
|
||||||
|
# Makeindex log files
|
||||||
|
*.lpz
|
||||||
|
|
||||||
|
# xwatermark package
|
||||||
|
*.xwm
|
||||||
|
|
||||||
|
# REVTeX puts footnotes in the bibliography by default, unless the nofootinbib
|
||||||
|
# option is specified. Footnotes are the stored in a file with suffix Notes.bib.
|
||||||
|
# Uncomment the next line to have this generated file ignored.
|
||||||
|
#*Notes.bib
|
||||||
|
|
||||||
|
.ipynb_checkpoints/
|
||||||
|
env/
|
||||||
|
__pycache__/
|
||||||
|
test/
|
||||||
|
venv/
|
||||||
|
.venv/
|
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FROM python:3.13.1-slim-bookworm
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
COPY requirements.txt /app/requirements.txt
|
||||||
|
RUN pip install --no-cache-dir -r /app/requirements.txt
|
||||||
|
|
||||||
|
# Copy the application
|
||||||
|
COPY main.py /app
|
||||||
|
COPY utils/ /app/utils
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
CMD ["python", "main.py"]
|
41
docker-compose.yml
Normal file
41
docker-compose.yml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
services:
|
||||||
|
selenium:
|
||||||
|
container_name: selenium-standalone-chromium
|
||||||
|
image: selenium/standalone-chromium:129.0
|
||||||
|
ports:
|
||||||
|
- "7900:7900"
|
||||||
|
volumes:
|
||||||
|
- ./webrtc-internals-exporter:/tmp/webrtc-internals-exporter:ro
|
||||||
|
shm_size: "2g"
|
||||||
|
attach: false
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:4444/wd/hub/status"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
telegraf:
|
||||||
|
container_name: telegraf
|
||||||
|
image: telegraf:1.33.1
|
||||||
|
volumes:
|
||||||
|
- ./telegraf.conf:/etc/telegraf/telegraf.conf:ro
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
collector:
|
||||||
|
container_name: collector
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
depends_on:
|
||||||
|
selenium:
|
||||||
|
condition: service_healthy
|
||||||
|
telegraf:
|
||||||
|
condition: service_started
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
networks:
|
||||||
|
backend:
|
191
main.py
Normal file
191
main.py
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
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_argument("--load-extension=/tmp/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://selenium-standalone-chromium: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(), ('telegraf', 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(('collector', 9092), partial(Handler, downloadStats, driver, logger))
|
||||||
|
logger.info('Server collector started.')
|
||||||
|
httpd.serve_forever()
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
selenium
|
||||||
|
beautifulsoup4
|
33
telegraf.conf
Normal file
33
telegraf.conf
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
[[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
|
43
utils/ColoredFormatter.py
Normal file
43
utils/ColoredFormatter.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
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
|
||||||
|
)
|
24
utils/PostHandler.py
Normal file
24
utils/PostHandler.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from http.server import BaseHTTPRequestHandler
|
||||||
|
|
||||||
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
def __init__(self, custom_func, driver, logger, *args, **kwargs):
|
||||||
|
self._custom_func = custom_func
|
||||||
|
self.logger = logger
|
||||||
|
self.driver = driver
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
if self.path == '/webrtc-internals-exporter':
|
||||||
|
content_length = int(self.headers['Content-Length'])
|
||||||
|
post_data = self.rfile.read(content_length)
|
||||||
|
self.logger.log(logging.DEBUG, f"POST request,\nPath: {self.path}\nHeaders:\n{self.headers}\n\nBody:\n{post_data.decode('utf-8')}")
|
||||||
|
self._custom_func(self.driver, json.loads(post_data.decode('utf-8')))
|
||||||
|
self.send_response(200)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(b'POST request received')
|
||||||
|
else:
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(b'404 Not Found')
|
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