Update WebUI design

This commit is contained in:
Alex X
2025-11-15 21:08:00 +03:00
parent e2c7d06730
commit 1fe602679e
7 changed files with 700 additions and 792 deletions
+319 -322
View File
@@ -1,41 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>go2rtc - Add Stream</title>
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<style>
body {
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
html, body {
main > button {
background-color: #444;
color: white;
cursor: pointer;
padding: 14px;
width: 100%;
height: 100%;
border: none;
text-align: left;
font-size: 16px;
font-weight: bold;
}
.module {
main > div {
display: none;
padding: 10px;
gap: 10px;
}
table tbody td {
font-size: 13px;
}
</style>
</head>
<body>
<script src="main.js"></script>
<script>
function drawTable(table, data) {
const cols = ['id', 'name', 'info', 'url', 'location'];
const th = (row) => cols.reduce((html, k) => k in row ? `${html}<th>${k}</th>` : html, '<tr>') + '</tr>';
const td = (row) => cols.reduce((html, k) => k in row ? `${html}<td style="word-break: break-word;white-space: normal;">${row[k]}</td>` : html, '<tr>') + '</tr>';
const td = (row) => cols.reduce((html, k) => k in row ? `${html}<td style="word-break: break-word; white-space: normal;">${row[k]}</td>` : html, '<tr>') + '</tr>';
const thead = th(data.sources[0]);
const tbody = data.sources.reduce((html, source) => `${html}${td(source)}`, '');
@@ -57,329 +53,330 @@
}
</script>
<button id="stream">Temporary stream</button>
<div class="module">
<form id="stream-form" style="padding: 10px">
<input type="text" name="name" placeholder="name">
<input type="text" name="src" placeholder="url">
<input type="submit" value="add">
</form>
</div>
<script>
document.getElementById('stream').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block';
});
document.getElementById('stream-form').addEventListener('submit', async ev => {
ev.preventDefault();
const url = new URL('api/streams', location.href);
url.searchParams.set('name', ev.target.elements['name'].value);
url.searchParams.set('src', ev.target.elements['src'].value);
const r = await fetch(url, {method: 'PUT'});
alert(r.ok ? 'OK' : 'ERROR: ' + await r.text());
});
</script>
<button id="alsa">ALSA (Linux audio)</button>
<div class="module">
<table id="alsa-table"></table>
</div>
<script>
document.getElementById('alsa').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block';
await getSources('alsa-table', 'api/alsa');
});
</script>
<button id="homekit">Apple HomeKit</button>
<div class="module">
<form id="homekit-pair" style="margin-bottom: 10px">
<input type="text" name="id" placeholder="stream id" size="20">
<input type="text" name="src" placeholder="src" size="40">
<input type="text" name="pin" placeholder="pin" size="10">
<input type="submit" value="Pair">
</form>
<form id="homekit-unpair" style="margin-bottom: 10px">
<input type="text" name="id" placeholder="stream id" size="20">
<input type="submit" value="Unpair">
</form>
<table id="homekit-table"></table>
</div>
<script>
async function reloadHomeKit() {
await getSources('homekit-table', 'api/discovery/homekit');
const rows = document.querySelectorAll('#homekit-table tr');
rows.forEach((row, i) => {
let commands = '';
if (row.children[2].innerText.indexOf('status=1') > 0) {
commands += '<a href="#">pair</a>';
} else if (i > 0 && row.children[3].innerText) {
commands += '<a href="#">unpair</a>';
}
row.innerHTML += `<td>${commands}</td>`;
<main>
<button id="stream">Temporary stream</button>
<div>
<form id="stream-form">
<input type="text" name="name" placeholder="name">
<input type="text" name="src" placeholder="url" required size="30">
<button type="submit">add</button>
</form>
</div>
<script>
document.getElementById('stream').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'grid';
});
}
document.getElementById('homekit').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block';
await reloadHomeKit();
});
document.getElementById('stream-form').addEventListener('submit', async ev => {
ev.preventDefault();
document.getElementById('homekit-table').addEventListener('click', ev => {
if (ev.target.innerText === 'pair') {
const form = document.querySelector('#homekit-pair');
const row = ev.target.closest('tr');
form.children[0].value = row.children[0].innerText;
form.children[1].value = row.children[2].innerText;
} else if (ev.target.innerText === 'unpair') {
const form = document.querySelector('#homekit-unpair');
const row = ev.target.closest('tr');
form.children[0].value = row.children[3].innerText;
}
});
const url = new URL('api/streams', location.href);
url.searchParams.set('name', ev.target.elements['name'].value);
url.searchParams.set('src', ev.target.elements['src'].value);
document.getElementById('homekit-pair').addEventListener('submit', async ev => {
ev.preventDefault();
const params = new URLSearchParams(new FormData(ev.target));
const r = await fetch('api/homekit', {method: 'POST', body: params});
alert(r.ok ? 'OK' : 'ERROR: ' + await r.text());
await reloadHomeKit();
});
document.getElementById('homekit-unpair').addEventListener('submit', async ev => {
ev.preventDefault();
const params = new URLSearchParams(new FormData(ev.target));
const r = await fetch('api/homekit?' + params.toString(), {method: 'DELETE'});
alert(r.ok ? 'OK' : 'ERROR: ' + await r.text());
await reloadHomeKit();
});
</script>
const r = await fetch(url, {method: 'PUT'});
alert(r.ok ? 'OK' : 'ERROR: ' + await r.text());
});
</script>
<button id="dvrip">DVRIP</button>
<div class="module">
<table id="dvrip-table"></table>
</div>
<script>
document.getElementById('dvrip').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block';
await getSources('dvrip-table', 'api/dvrip');
});
</script>
<button id="alsa">ALSA (Linux audio)</button>
<div>
<table id="alsa-table"></table>
</div>
<script>
document.getElementById('alsa').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'grid';
await getSources('alsa-table', 'api/alsa');
});
</script>
<button id="devices">FFmpeg Devices (USB)</button>
<div class="module">
<table id="devices-table"></table>
</div>
<script>
document.getElementById('devices').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block';
await getSources('devices-table', 'api/ffmpeg/devices');
});
</script>
<button id="homekit">Apple HomeKit</button>
<div>
<form id="homekit-pair">
<input type="text" name="id" placeholder="stream id" required>
<input type="text" name="src" placeholder="src" required size="30">
<input type="text" name="pin" placeholder="pin" required size="10">
<button type="submit">pair</button>
</form>
<form id="homekit-unpair">
<input type="text" name="id" placeholder="stream id" required>
<button type="submit">unpair</button>
</form>
<table id="homekit-table"></table>
</div>
<script>
async function reloadHomeKit() {
await getSources('homekit-table', 'api/discovery/homekit');
<button id="hardware">FFmpeg Hardware</button>
<div class="module">
<table id="hardware-table"></table>
</div>
<script>
document.getElementById('hardware').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block';
await getSources('hardware-table', 'api/ffmpeg/hardware');
});
</script>
<button id="nest">Google Nest</button>
<div class="module">
<form id="nest-form" style="margin-bottom: 10px">
<input type="text" name="client_id" placeholder="client_id">
<input type="text" name="client_secret" placeholder="client_secret">
<input type="text" name="refresh_token" placeholder="refresh_token">
<input type="text" name="project_id" placeholder="project_id">
<input type="submit" value="Login">
</form>
<table id="nest-table"></table>
</div>
<script>
document.getElementById('nest').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block';
});
document.getElementById('nest-form').addEventListener('submit', async ev => {
ev.preventDefault();
const query = new URLSearchParams(new FormData(ev.target));
const url = new URL('api/nest?' + query.toString(), location.href);
const r = await fetch(url, {cache: 'no-cache'});
await getSources('nest-table', r);
});
</script>
<button id="ring">Ring</button>
<div class="module">
<form id="ring-credentials-form" style="margin-bottom: 10px">
<input type="email" name="email" placeholder="email">
<input type="password" name="password" placeholder="password">
<div id="tfa-field" style="display: none">
<input type="text" name="code" placeholder="2FA code">
<div id="tfa-prompt"></div>
</div>
<input type="submit" value="Login">
</form>
<form id="ring-token-form" style="margin-bottom: 10px">
<input type="text" name="refresh_token" placeholder="refresh_token">
<input type="submit" value="Login">
</form>
<table id="ring-table"></table>
</div>
<script>
document.getElementById('ring').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block';
});
async function handleRingAuth(ev) {
ev.preventDefault();
const table = document.getElementById('ring-table');
table.innerText = 'loading...';
const query = new URLSearchParams(new FormData(ev.target));
const url = new URL('api/ring?' + query.toString(), location.href);
const r = await fetch(url, {cache: 'no-cache'});
if (!r.ok) {
table.innerText = (await r.text()) || 'Unknown error';
return;
const rows = document.querySelectorAll('#homekit-table tr');
rows.forEach((row, i) => {
let commands = '';
if (row.children[2].innerText.indexOf('status=1') > 0) {
commands += '<a href="#">pair</a>';
} else if (i > 0 && row.children[3].innerText) {
commands += '<a href="#">unpair</a>';
}
row.innerHTML += i > 0 ? `<td>${commands}</td>` : '<th>commands</th>';
});
}
const data = await r.json();
document.getElementById('homekit').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'grid';
await reloadHomeKit();
});
table.innerText = '';
document.getElementById('homekit-table').addEventListener('click', ev => {
if (ev.target.innerText === 'pair') {
const form = document.querySelector('#homekit-pair');
const row = ev.target.closest('tr');
form.children[0].value = row.children[0].innerText;
form.children[1].value = row.children[2].innerText;
} else if (ev.target.innerText === 'unpair') {
const form = document.querySelector('#homekit-unpair');
const row = ev.target.closest('tr');
form.children[0].value = row.children[3].innerText;
}
});
if (data.needs_2fa) {
document.getElementById('tfa-field').style.display = 'block';
document.getElementById('tfa-prompt').textContent = data.prompt || 'Enter 2FA code';
return;
document.getElementById('homekit-pair').addEventListener('submit', async ev => {
ev.preventDefault();
const params = new URLSearchParams(new FormData(ev.target));
const r = await fetch('api/homekit', {method: 'POST', body: params});
alert(r.ok ? 'OK' : 'ERROR: ' + await r.text());
await reloadHomeKit();
});
document.getElementById('homekit-unpair').addEventListener('submit', async ev => {
ev.preventDefault();
const params = new URLSearchParams(new FormData(ev.target));
const r = await fetch('api/homekit?' + params.toString(), {method: 'DELETE'});
alert(r.ok ? 'OK' : 'ERROR: ' + await r.text());
await reloadHomeKit();
});
</script>
<button id="dvrip">DVRIP</button>
<div>
<table id="dvrip-table"></table>
</div>
<script>
document.getElementById('dvrip').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'grid';
await getSources('dvrip-table', 'api/dvrip');
});
</script>
<button id="devices">FFmpeg Devices (USB)</button>
<div>
<table id="devices-table"></table>
</div>
<script>
document.getElementById('devices').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'grid';
await getSources('devices-table', 'api/ffmpeg/devices');
});
</script>
<button id="hardware">FFmpeg Hardware</button>
<div>
<table id="hardware-table"></table>
</div>
<script>
document.getElementById('hardware').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'grid';
await getSources('hardware-table', 'api/ffmpeg/hardware');
});
</script>
<button id="nest">Google Nest</button>
<div>
<form id="nest-form">
<input type="text" name="client_id" placeholder="client_id" required>
<input type="text" name="client_secret" placeholder="client_secret" required>
<input type="text" name="refresh_token" placeholder="refresh_token" required>
<input type="text" name="project_id" placeholder="project_id" required>
<button type="submit">login</button>
</form>
<table id="nest-table"></table>
</div>
<script>
document.getElementById('nest').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'grid';
});
document.getElementById('nest-form').addEventListener('submit', async ev => {
ev.preventDefault();
const query = new URLSearchParams(new FormData(ev.target));
const url = new URL('api/nest?' + query.toString(), location.href);
const r = await fetch(url, {cache: 'no-cache'});
await getSources('nest-table', r);
});
</script>
<button id="gopro">GoPro</button>
<div>
<table id="gopro-table"></table>
</div>
<script>
document.getElementById('gopro').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'grid';
await getSources('gopro-table', 'api/gopro');
});
</script>
<button id="hass">Home Assistant</button>
<div>
<table id="hass-table"></table>
</div>
<script>
document.getElementById('hass').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'grid';
await getSources('hass-table', 'api/hass');
});
</script>
<button id="onvif">ONVIF</button>
<div>
<form id="onvif-form">
<input type="text" name="src" placeholder="onvif://user:pass@192.168.1.123:80" required size="30">
<button type="submit">test</button>
</form>
<table id="onvif-table"></table>
</div>
<script>
document.getElementById('onvif').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'grid';
await getSources('onvif-table', 'api/onvif');
});
document.getElementById('onvif-form').addEventListener('submit', async ev => {
ev.preventDefault();
const url = new URL('api/onvif', location.href);
url.searchParams.set('src', ev.target.elements['src'].value);
await getSources('onvif-table', url.toString());
});
</script>
<button id="ring">Ring</button>
<div>
<form id="ring-credentials-form">
<input type="email" name="email" placeholder="email" required>
<input type="password" name="password" placeholder="password" required>
<div id="tfa-field" style="display: none">
<input type="text" name="code" placeholder="2FA code">
<div id="tfa-prompt"></div>
</div>
<button type="submit">login</button>
</form>
<form id="ring-token-form">
<input type="text" name="refresh_token" placeholder="refresh_token" required>
<button type="submit">login</button>
</form>
<table id="ring-table"></table>
</div>
<script>
document.getElementById('ring').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'grid';
});
async function handleRingAuth(ev) {
ev.preventDefault();
const table = document.getElementById('ring-table');
table.innerText = 'loading...';
const query = new URLSearchParams(new FormData(ev.target));
const url = new URL('api/ring?' + query.toString(), location.href);
const r = await fetch(url, {cache: 'no-cache'});
if (!r.ok) {
table.innerText = (await r.text()) || 'Unknown error';
return;
}
const data = await r.json();
table.innerText = '';
if (data.needs_2fa) {
document.getElementById('tfa-field').style.display = 'block';
document.getElementById('tfa-prompt').textContent = data.prompt || 'Enter 2FA code';
return;
}
drawTable(table, data);
}
drawTable(table, data);
}
document.getElementById('ring-credentials-form').addEventListener('submit', handleRingAuth);
document.getElementById('ring-token-form').addEventListener('submit', handleRingAuth);
</script>
<button id="gopro">GoPro</button>
<div class="module">
<table id="gopro-table"></table>
</div>
<script>
document.getElementById('gopro').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block';
await getSources('gopro-table', 'api/gopro');
});
</script>
document.getElementById('ring-credentials-form').addEventListener('submit', handleRingAuth);
document.getElementById('ring-token-form').addEventListener('submit', handleRingAuth);
</script>
<button id="hass">Home Assistant</button>
<div class="module">
<table id="hass-table"></table>
</div>
<script>
document.getElementById('hass').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block';
await getSources('hass-table', 'api/hass');
});
</script>
<button id="roborock">Roborock</button>
<div>
<form id="roborock-form">
<input type="text" name="username" placeholder="username" required>
<input type="password" name="password" placeholder="password" required>
<button type="submit">login</button>
</form>
<table id="roborock-table">
</table>
</div>
<script>
document.getElementById('roborock').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'grid';
await getSources('roborock-table', 'api/roborock');
});
document.getElementById('roborock-form').addEventListener('submit', async ev => {
ev.preventDefault();
const r = await fetch('api/roborock', {method: 'POST', body: new FormData(ev.target)});
await getSources('roborock-table', r);
});
</script>
<button id="onvif">ONVIF</button>
<div class="module">
<form id="onvif-form" style="padding: 10px">
<input type="text" name="src" placeholder="onvif://user:pass@192.168.1.123:80" size="50">
<input type="submit" value="test">
</form>
<table id="onvif-table"></table>
</div>
<script>
document.getElementById('onvif').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block';
await getSources('onvif-table', 'api/onvif');
});
document.getElementById('onvif-form').addEventListener('submit', async ev => {
ev.preventDefault();
const url = new URL('api/onvif', location.href);
url.searchParams.set('src', ev.target.elements['src'].value);
await getSources('onvif-table', url.toString());
});
</script>
<button id="v4l2">V4L2 (Linux video)</button>
<div>
<table id="v4l2-table"></table>
</div>
<script>
document.getElementById('v4l2').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'grid';
await getSources('v4l2-table', 'api/v4l2');
});
</script>
<button id="roborock">Roborock</button>
<div class="module">
<form id="roborock-form" style="margin-bottom: 10px">
<input type="text" name="username" placeholder="username">
<input type="password" name="password" placeholder="password">
<input type="submit" value="Login">
</form>
<table id="roborock-table">
</table>
</div>
<script>
document.getElementById('roborock').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block';
await getSources('roborock-table', 'api/roborock');
});
document.getElementById('roborock-form').addEventListener('submit', async ev => {
ev.preventDefault();
const r = await fetch('api/roborock', {method: 'POST', body: new FormData(ev.target)});
await getSources('roborock-table', r);
});
</script>
<button id="v4l2">V4L2 (Linux video)</button>
<div class="module">
<table id="v4l2-table"></table>
</div>
<script>
document.getElementById('v4l2').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block';
await getSources('v4l2-table', 'api/v4l2');
});
</script>
<button id="webtorrent">WebTorrent Shares</button>
<div class="module">
<table id="webtorrent-table"></table>
</div>
<script>
document.getElementById('webtorrent').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block';
await getSources('webtorrent-table', 'api/webtorrent');
});
</script>
<button id="webtorrent">WebTorrent Shares</button>
<div>
<table id="webtorrent-table"></table>
</div>
<script>
document.getElementById('webtorrent').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'grid';
await getSources('webtorrent-table', 'api/webtorrent');
});
</script>
</main>
</body>
</html>
+17 -21
View File
@@ -1,41 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>go2rtc - File Editor</title>
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>go2rtc - Config</title>
<script src="https://unpkg.com/ace-builds@1.33.1/src-min/ace.js"></script>
<style>
body {
font-family: Arial, Helvetica, sans-serif;
background-color: white;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
html, body, #config {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<script src="main.js"></script>
<div>
<button id="save">Save & Restart</button>
</div>
<br>
<div id="config"></div>
<script>
let dump;
<script src="main.js"></script>
<main>
<div>
<button id="save">Save & Restart</button>
</div>
</main>
<div id="config"></div>
<script>
/* global ace */
ace.config.set('basePath', 'https://unpkg.com/ace-builds@1.33.1/src-min/');
const editor = ace.edit('config', {
mode: 'ace/mode/yaml',
});
let dump;
document.getElementById('save').addEventListener('click', async () => {
let r = await fetch('api/config', {cache: 'no-cache'});
if (r.ok && dump !== await r.text()) {
@@ -67,5 +62,6 @@
}
});
</script>
</body>
</html>
+34 -45
View File
@@ -1,61 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="apple-touch-icon" href="https://alexxit.github.io/go2rtc/icons/apple-touch-icon-180x180.png" sizes="180x180">
<link rel="icon" href="https://alexxit.github.io/go2rtc/icons/favicon.ico">
<link rel="manifest" href="https://alexxit.github.io/go2rtc/manifest.json">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>go2rtc</title>
<style>
body {
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
table tbody td {
font-size: 13px;
}
label {
.controls {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.controls {
display: flex;
padding: 5px;
}
.controls > label {
margin-left: 10px;
.info {
color: #888;
}
</style>
</head>
<body>
<script src="main.js"></script>
<div class="info"></div>
<div class="controls">
<button>stream</button>
<label><input type="checkbox" name="webrtc" checked>webrtc</label>
<label><input type="checkbox" name="mse" checked>mse</label>
<label><input type="checkbox" name="hls" checked>hls</label>
<label><input type="checkbox" name="mjpeg" checked>mjpeg</label>
</div>
<table>
<thead>
<tr>
<th><label><input id="selectall" type="checkbox">Name</label></th>
<th>Online</th>
<th>Commands</th>
</tr>
</thead>
<tbody id="streams">
</tbody>
</table>
<main>
<div class="controls">
<button>stream</button>
modes
<label><input type="checkbox" name="webrtc" checked>webrtc</label>
<label><input type="checkbox" name="mse" checked>mse</label>
<label><input type="checkbox" name="hls" checked>hls</label>
<label><input type="checkbox" name="mjpeg" checked>mjpeg</label>
</div>
<table>
<thead>
<tr>
<th><label><input id="selectall" type="checkbox">name</label></th>
<th>online</th>
<th>commands</th>
</tr>
</thead>
<tbody id="streams">
</tbody>
</table>
<div class="info"></div>
</main>
<script>
const templates = [
'<a href="stream.html?src={name}">stream</a>',
@@ -159,10 +147,11 @@
const url = new URL('api', location.href);
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
const info = document.querySelector('.info');
info.innerText = `Version: ${data.version}, Config: ${data.config_path}`;
info.innerText = `version: ${data.version} / config: ${data.config_path}`;
});
reload();
</script>
</body>
</html>
+168 -163
View File
@@ -1,27 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>go2rtc - links</title>
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<style>
body {
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
html, body {
width: 100%;
height: 100%;
}
div {
padding: 10px;
}
div > li {
list-style-type: none;
padding-left: 10px;
@@ -36,28 +19,33 @@
</style>
</head>
<body>
<script src="main.js"></script>
<div id="links"></div>
<script>
const src = new URLSearchParams(location.search).get('src').replace(/[<">]/g, ''); // sanitize
document.getElementById('links').innerHTML = `
<script src="main.js"></script>
<main>
<div id="links"></div>
<script>
const src = new URLSearchParams(location.search).get('src').replace(/[<">]/g, ''); // sanitize
const links = document.getElementById('links');
links.innerHTML = `
<h2>Any codec in source</h2>
<li><a href="stream.html?src=${src}">stream.html</a> with auto-select mode / browsers: all / codecs: H264, H265*, MJPEG, JPEG, AAC, PCMU, PCMA, OPUS</li>
<li><a href="api/streams?src=${src}">info.json</a> page with active connections</li>
`;
const url = new URL('api', location.href);
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
let rtsp = location.host + ':8554';
try {
const host = data.host.match(/^[^:]+/)[0];
const port = data.rtsp.listen.match(/[0-9]+$/)[0];
rtsp = `${host}:${port}`;
} catch (e) {
}
const url = new URL('api', location.href);
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
let rtsp = location.host + ':8554';
try {
const host = data.host.match(/^[^:]+/)[0];
const port = data.rtsp.listen.match(/[0-9]+$/)[0];
rtsp = `${host}:${port}`;
} catch (e) {
}
document.getElementById('links').innerHTML += `
links.innerHTML += `
<li><a href="rtsp://${rtsp}/${src}">rtsp</a> with only one video and one audio / codecs: any</li>
<li><a href="rtsp://${rtsp}/${src}?mp4">rtsp</a> for MP4 recording (Hass or Frigate) / codecs: H264, H265, AAC</li>
<li><a href="rtsp://${rtsp}/${src}?video=all&audio=all">rtsp</a> with all tracks / codecs: any</li>
@@ -80,148 +68,165 @@
<li><a href="api/stream.mjpeg?src=${src}">stream.mjpeg</a> MJPEG stream / browsers: all / codecs: MJPEG, JPEG</li>
<li><a href="api/frame.jpeg?src=${src}">frame.jpeg</a> snapshot in JPEG-format / browsers: all / codecs: MJPEG, JPEG</li>
`;
});
</script>
});
</script>
<div>
<h2>Play audio</h2>
<label><input type="radio" name="play" value="file" checked>file - play remote (https://example.com/song.mp3) or local (/media/song.mp3) file</label><br>
<label><input type="radio" name="play" value="live">live - play remote live stream (radio, etc.)</label><br>
<label><input type="radio" name="play" value="text">text - play Text To Speech (if your FFmpeg support this)</label><br>
<br>
<input id="play-url" type="text" placeholder="path / url / text">
<a id="play-send" href="#">send</a> / cameras with two way audio support
</div>
<script>
document.getElementById('play-send').addEventListener('click', ev => {
ev.preventDefault();
// action - file / live / text
const action = document.querySelector('input[name="play"]:checked').value;
const url = new URL('api/ffmpeg', location.href);
url.searchParams.set('dst', src);
url.searchParams.set(action, document.getElementById('play-url').value);
fetch(url, {method: 'POST'});
});
</script>
<div>
<h2>Play audio</h2>
<label><input type="radio" name="play" value="file" checked>
file - play remote (https://example.com/song.mp3) or local (/media/song.mp3) file
</label>
<label><input type="radio" name="play" value="live">
live - play remote live stream (radio, etc.)
</label>
<label><input type="radio" name="play" value="text">
text - play Text To Speech (if your FFmpeg support this)
</label>
<br>
<input id="play-url" type="text" placeholder="path / url / text">
<button id="play-send">send</button>
/ cameras with two way audio support
</div>
<script>
document.getElementById('play-send').addEventListener('click', ev => {
ev.preventDefault();
// action - file / live / text
const action = document.querySelector('input[name="play"]:checked').value;
const url = new URL('api/ffmpeg', location.href);
url.searchParams.set('dst', src);
url.searchParams.set(action, document.getElementById('play-url').value);
fetch(url, {method: 'POST'});
});
</script>
<div>
<h2>Publish stream</h2>
<pre>YouTube: rtmps://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx
<div>
<h2>Publish stream</h2>
<pre>YouTube: rtmps://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx
Telegram: rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx</pre>
<input id="pub-url" type="text" placeholder="url">
<a id="pub-send" href="#">send</a> / Telegram RTMPS server
</div>
<script>
document.getElementById('pub-send').addEventListener('click', ev => {
ev.preventDefault();
const url = new URL('api/streams', location.href);
url.searchParams.set('src', src);
url.searchParams.set('dst', document.getElementById('pub-url').value);
fetch(url, {method: 'POST'});
});
</script>
<input id="pub-url" type="text" placeholder="url">
<button id="pub-send">send</button>
/ Telegram RTMPS server
</div>
<script>
document.getElementById('pub-send').addEventListener('click', ev => {
ev.preventDefault();
const url = new URL('api/streams', location.href);
url.searchParams.set('src', src);
url.searchParams.set('dst', document.getElementById('pub-url').value);
fetch(url, {method: 'POST'});
});
</script>
<div id="webrtc">
<h2>WebRTC Magic</h2>
<label><input type="radio" name="webrtc" value="video+audio" checked>video+audio = simple viewer</label><br>
<label><input type="radio" name="webrtc" value="video+audio+microphone">video+audio+microphone = two way audio from camera</label><br>
<label><input type="radio" name="webrtc" value="camera+microphone">camera+microphone = stream from browser</label><br>
<label><input type="radio" name="webrtc" value="display+speaker">display+speaker = broadcast software</label><br>
<div id="webrtc">
<h2>WebRTC Magic</h2>
<label><input type="radio" name="webrtc" value="video+audio" checked>
video+audio = simple viewer
</label>
<label><input type="radio" name="webrtc" value="video+audio+microphone">
video+audio+microphone = two way audio from camera
</label>
<label><input type="radio" name="webrtc" value="camera+microphone">
camera+microphone = stream from browser
</label>
<label><input type="radio" name="webrtc" value="display+speaker">
display+speaker = broadcast software
</label>
<br>
<li><a id="local" href="webrtc.html?src=">webrtc.html</a> local WebRTC viewer</li>
<br>
<li><a id="local" href="webrtc.html?src=">webrtc.html</a> local WebRTC viewer</li>
<li>
<a id="shareadd" href="#">share link</a>
<a id="shareget" href="#">copy link</a>
<a id="sharedel" href="#">delete</a>
external WebRTC viewer
</li>
</div>
<script>
function webrtcLinksUpdate() {
const media = document.querySelector('input[name="webrtc"]:checked').value;
<li>
<a id="shareadd" href="#">share link</a>
<a id="shareget" href="#">copy link</a>
<a id="sharedel" href="#">delete</a>
external WebRTC viewer
</li>
</div>
<script>
function webrtcLinksUpdate() {
const media = document.querySelector('input[name="webrtc"]:checked').value;
const direction = media.indexOf('video') >= 0 || media === 'audio' ? 'src' : 'dst';
document.getElementById('local').href = `webrtc.html?${direction}=${src}&media=${media}`;
const direction = media.indexOf('video') >= 0 || media === 'audio' ? 'src' : 'dst';
document.getElementById('local').href = `webrtc.html?${direction}=${src}&media=${media}`;
const share = document.getElementById('shareget');
share.href = `https://alexxit.github.io/go2rtc/#${share.dataset.auth}&media=${media}`;
}
const share = document.getElementById('shareget');
share.href = `https://alexxit.github.io/go2rtc/#${share.dataset.auth}&media=${media}`;
}
function share(method) {
const url = new URL('api/webtorrent', location.href);
url.searchParams.set('src', src);
return fetch(url, {method: method, cache: 'no-cache'});
}
function share(method) {
const url = new URL('api/webtorrent', location.href);
url.searchParams.set('src', src);
return fetch(url, {method: method, cache: 'no-cache'});
}
function onshareadd(r) {
document.getElementById('shareget').dataset['auth'] = `share=${r.share}&pwd=${r.pwd}`;
function onshareadd(r) {
document.getElementById('shareget').dataset['auth'] = `share=${r.share}&pwd=${r.pwd}`;
document.getElementById('shareadd').style.display = 'none';
document.getElementById('shareget').style.display = '';
document.getElementById('sharedel').style.display = '';
document.getElementById('shareadd').style.display = 'none';
document.getElementById('shareget').style.display = '';
document.getElementById('sharedel').style.display = '';
webrtcLinksUpdate();
}
function onsharedel() {
document.getElementById('shareadd').style.display = '';
document.getElementById('shareget').style.display = 'none';
document.getElementById('sharedel').style.display = 'none';
}
function copyTextToClipboard(text) {
// https://web.dev/patterns/clipboard/copy-text
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).catch(err => {
console.error(err.name, err.message);
});
} else {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
document.execCommand('copy');
} catch (err) {
console.error(err.name, err.message);
}
document.body.removeChild(textarea);
}
}
document.getElementById('shareadd').addEventListener('click', ev => {
ev.preventDefault();
share('POST').then(r => r.json()).then(r => onshareadd(r));
});
document.getElementById('shareget').addEventListener('click', ev => {
ev.preventDefault();
copyTextToClipboard(ev.target.href);
});
document.getElementById('sharedel').addEventListener('click', ev => {
ev.preventDefault();
share('DELETE').then(() => onsharedel());
});
document.getElementById('webrtc').addEventListener('click', ev => {
if (ev.target.tagName === 'INPUT') webrtcLinksUpdate();
});
share('GET').then(r => {
if (r.ok) r.json().then(r => onshareadd(r));
else onsharedel();
});
webrtcLinksUpdate();
}
function onsharedel() {
document.getElementById('shareadd').style.display = '';
document.getElementById('shareget').style.display = 'none';
document.getElementById('sharedel').style.display = 'none';
}
function copyTextToClipboard(text) {
// https://web.dev/patterns/clipboard/copy-text
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).catch(err => {
console.error(err.name, err.message);
});
} else {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
document.execCommand('copy');
} catch (err) {
console.error(err.name, err.message);
}
document.body.removeChild(textarea);
}
}
document.getElementById('shareadd').addEventListener('click', ev => {
ev.preventDefault();
share('POST').then(r => r.json()).then(r => onshareadd(r));
});
document.getElementById('shareget').addEventListener('click', ev => {
ev.preventDefault();
copyTextToClipboard(ev.target.href);
});
document.getElementById('sharedel').addEventListener('click', ev => {
ev.preventDefault();
share('DELETE').then(() => onsharedel());
});
document.getElementById('webrtc').addEventListener('click', ev => {
if (ev.target.tagName === 'INPUT') webrtcLinksUpdate();
});
share('GET').then(r => {
if (r.ok) r.json().then(r => onshareadd(r));
else onsharedel();
});
webrtcLinksUpdate();
</script>
</script>
</main>
</body>
</html>
+42 -46
View File
@@ -1,69 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>go2rtc - Logs</title>
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<style>
body {
font-family: Arial, Helvetica, sans-serif;
background-color: white;
margin: 0;
padding: 0;
main > div {
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: 10px;
}
html, body {
width: 100%;
height: 100%;
}
table tbody td {
table tbody {
font-size: 13px;
vertical-align: top;
}
.info {
color: #0174DF;
}
.debug {
color: #808080;
}
.error {
color: #DF0101;
}
.trace {
color: #585858;
color: #585858 !important;
}
.debug {
color: #808080 !important;
}
.info {
color: #0174DF !important;
}
.warn {
color: #FF9966;
color: #FF9966 !important;
}
.error {
color: #DF0101 !important;
}
</style>
</head>
<body>
<script src="main.js"></script>
<div>
<button id="clean">Clean</button>
<button id="update">Auto Update: ON</button>
<button id="reverse">Reverse Log Order: OFF</button>
</div>
<br>
<table>
<thead>
<tr>
<th style="width: 100px">Time</th>
<th style="width: 40px">Level</th>
<th>Message</th>
</tr>
</thead>
<tbody id="log">
</tbody>
</table>
<main>
<div>
<button id="clean">Clean</button>
<button id="update">Auto Update: ON</button>
<button id="reverse">Reverse Log Order: OFF</button>
</div>
<table>
<thead>
<tr>
<th style="width: 100px">Time</th>
<th style="width: 40px">Level</th>
<th>Message</th>
</tr>
</thead>
<tbody id="log">
</tbody>
</table>
</main>
<script>
document.getElementById('clean').addEventListener('click', async () => {
const r = await fetch('api/log', {method: 'DELETE'});
@@ -145,5 +140,6 @@
if (autoUpdateEnabled) reload();
}, 5000);
</script>
</body>
</html>
+114 -180
View File
@@ -1,200 +1,134 @@
// main menu
document.body.innerHTML = `
document.head.innerHTML += `
<style>
ul {
list-style: none;
margin: 0 auto;
}
body {
display: flex;
flex-direction: column;
font-family: Arial, sans-serif;
margin: 0;
}
a {
text-decoration: none;
font-family: 'Lora', serif;
transition: .5s linear;
}
/* navigation block */
nav {
background-color: #333;
overflow: hidden;
}
i {
margin-right: 10px;
}
nav a {
float: left;
display: block;
color: #f2f2f2;
text-align: center;
padding: 14px 16px;
text-decoration: none;
font-size: 17px;
}
nav {
display: block;
margin: 0 auto 10px;
}
nav a:hover {
background-color: #ddd;
color: black;
}
nav ul {
padding: 1em 0;
background: #ECDAD6;
}
/* main block */
main {
padding: 10px;
display: flex;
flex-direction: column;
gap: 10px;
}
nav a {
padding: 1em;
background: rgba(177, 152, 145, .3);
border-right: 1px solid #b19891;
color: #695753;
}
/* checkbox */
label {
display: flex;
gap: 5px;
align-items: center;
cursor: pointer;
}
nav a:hover {
background: #b19891;
}
input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
nav li {
display: inline;
}
/* form */
form {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
body {
font-family: Arial, Helvetica, sans-serif;
background-color: white;
}
table {
background-color: white;
text-align: left;
border-collapse: collapse;
}
table thead {
background: #CFCFCF;
background: linear-gradient(to bottom, #dbdbdb 0%, #d3d3d3 66%, #CFCFCF 100%);
border-bottom: 3px solid black;
}
table thead th {
font-size: 15px;
font-weight: bold;
color: black;
text-align: center;
}
table td, table th {
border: 1px solid black;
padding: 5px 5px;
}
input[type="text"] {
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 16px;
}
/* Dark mode styles */
body.dark-mode {
background-color: #121212;
color: #e0e0e0;
}
button {
padding: 10px 20px;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
body.dark-mode nav ul {
background: #333;
}
/* table */
table {
width: 100%;
background-color: white;
border-collapse: collapse;
margin: 0 auto;
overflow: hidden;
}
body.dark-mode a {
background: rgba(45, 45, 45, .8);
border-right: 1px solid #2c2c2c;
color: #c7c7c7;
}
th, td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
body.dark-mode a:hover {
background: #555;
}
th {
background-color: #444;
color: white;
}
body.dark-mode a:visited {
color: #999;
}
tr:nth-child(even) {
background-color: #fafafa;
}
body.dark-mode table {
background-color: #222;
color: #ddd;
}
tr:hover {
background-color: #edf7ff;
transition: background-color 0.3s ease;
}
body.dark-mode table thead {
background: linear-gradient(to bottom, #444 0%, #3d3d3d 66%, #333 100%);
border-bottom: 3px solid #888;
}
body.dark-mode table thead th {
font-size: 15px;
font-weight: bold;
color: #ddd;
text-align: center;
}
body.dark-mode table td, body.dark-mode table th {
border: 1px solid #444;
}
/* table on mobile */
@media (max-width: 480px) {
table, thead, tbody, th, td, tr {
display: block;
}
body.dark-mode button {
background: rgba(255, 255, 255, .1);
border: 1px solid #444;
color: #ccc;
}
th, td {
box-sizing: border-box;
width: 100% !important;
border: none;
}
body.dark-mode input,
body.dark-mode select,
body.dark-mode textarea {
background-color: #333;
color: #e0e0e0;
border: 1px solid #444;
}
body.dark-mode input::placeholder,
body.dark-mode textarea::placeholder {
color: #bbb;
}
body.dark-mode hr {
border-top: 1px solid #444;
}
tr {
margin-bottom: 10px;
border-radius: 4px;
}
}
</style>
<nav>
<ul>
<li><a href="index.html">Streams</a></li>
<li><a href="add.html">Add</a></li>
<li><a href="editor.html">Config</a></li>
<li><a href="log.html">Log</a></li>
<li><a href="network.html">Net</a></li>
<li><a href="#" id="darkModeToggle">
&#127769;
</a>
</li>
</ul>
</nav>
`;
document.body.innerHTML = `
<header>
<nav>
<a href="index.html"><b>go2rtc</b></a>
<a href="add.html">add</a>
<a href="config.html">config</a>
<a href="log.html">log</a>
<a href="net.html">net</a>
</nav>
</header>
` + document.body.innerHTML;
const sunIcon = '&#9728;&#65039;';
const moonIcon = '&#127765;';
document.addEventListener('DOMContentLoaded', () => {
const darkModeToggle = document.getElementById('darkModeToggle');
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
const isDarkModeEnabled = () => document.body.classList.contains('dark-mode');
// Update the toggle button based on the dark mode state
const updateToggleButton = () => {
if (isDarkModeEnabled()) {
darkModeToggle.innerHTML = sunIcon;
darkModeToggle.setAttribute('aria-label', 'Enable light mode');
} else {
darkModeToggle.innerHTML = moonIcon;
darkModeToggle.setAttribute('aria-label', 'Enable dark mode');
}
};
const updateDarkMode = () => {
if (localStorage.getItem('darkMode') === 'enabled' || prefersDarkScheme.matches && localStorage.getItem('darkMode') !== 'disabled') {
document.body.classList.add('dark-mode');
} else {
document.body.classList.remove('dark-mode');
}
updateEditorTheme();
updateToggleButton();
};
// Update the editor theme based on the dark mode state
const updateEditorTheme = () => {
if (typeof editor !== 'undefined') {
editor.setTheme(isDarkModeEnabled() ? 'ace/theme/tomorrow_night_eighties' : 'ace/theme/github');
}
};
// Initial update for dark mode and toggle button
updateDarkMode();
// Listen for changes in the system's color scheme preference
prefersDarkScheme.addEventListener('change', updateDarkMode); // Modern approach
// Toggle dark mode and update local storage on button click
darkModeToggle.addEventListener('click', () => {
const enabled = document.body.classList.toggle('dark-mode');
localStorage.setItem('darkMode', enabled ? 'enabled' : 'disabled');
updateToggleButton(); // Update the button after toggling
updateEditorTheme();
});
});
+6 -15
View File
@@ -2,31 +2,21 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>go2rtc - Network</title>
<script src="https://unpkg.com/vis-network@9.1.9/standalone/umd/vis-network.min.js"></script>
<style>
body {
font-family: Arial, Helvetica, sans-serif;
background-color: white;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
html, body, #network {
height: 100%;
width: 100%;
}
#network {
flex-grow: 1;
}
</style>
</head>
<body>
<div id="network"></div>
<script src="main.js"></script>
<div id="network"></div>
<script>
/* global vis */
window.addEventListener('load', () => {
@@ -79,5 +69,6 @@
update();
});
</script>
</body>
</html>