Add ESLinter and fix JS lint problems

This commit is contained in:
Alexey Khit
2023-07-07 22:06:03 +03:00
parent ddfeb6fae6
commit 39cc4610e3
12 changed files with 405 additions and 344 deletions

40
package.json Normal file
View File

@@ -0,0 +1,40 @@
{
"devDependencies": {
"eslint": "^8.44.0",
"eslint-plugin-html": "^7.1.0"
},
"eslintConfig": {
"env": {
"browser": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 2017,
"sourceType": "module"
},
"rules": {
"no-var": "error",
"no-undef": "error",
"no-unused-vars": "warn",
"prefer-const": "error",
"quotes": [
"error",
"single"
],
"semi": "error"
},
"plugins": [
"html"
],
"overrides": [
{
"files": [
"*.html"
],
"parserOptions": {
"sourceType": "script"
}
}
]
}
}

View File

@@ -1,4 +1,14 @@
# HTML5 ## Browser support
[ECMAScript 2019 (ES10)](https://caniuse.com/?search=es10) supported by [iOS 12](https://en.wikipedia.org/wiki/IOS_12) (iPhone 5S, iPad Air, iPad Mini 2, etc.).
But [ECMAScript 2017 (ES8)](https://caniuse.com/?search=es8) almost fine (`es6 + async`) and recommended for [React+TypeScript](https://github.com/typescript-cheatsheets/react).
## Known problems
- Autoplay doesn't work for WebRTC in Safari [read more](https://developer.apple.com/documentation/webkit/delivering_video_content_for_safari/).
## HTML5
**1. Autoplay video tag** **1. Autoplay video tag**

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<title>Add Stream</title> <title>Add Stream</title>
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
@@ -59,20 +59,20 @@
<script src="main.js"></script> <script src="main.js"></script>
<script> <script>
async function getStreams(url, tableID) { async function getStreams(url, tableID) {
const table = document.getElementById(tableID) const table = document.getElementById(tableID);
table.innerText = 'loading...' table.innerText = 'loading...';
const r = typeof url === 'string' ? await fetch(url, {cache: 'no-cache'}) : url const r = typeof url === 'string' ? await fetch(url, {cache: 'no-cache'}) : url;
if (!r.ok) { if (!r.ok) {
table.innerText = await r.text() table.innerText = await r.text();
return return;
} }
/** @type {{streams:Array<{name:string,url:string}>}} */ /** @type {{streams:Array<{name:string,url:string}>}} */
const data = await r.json() const data = await r.json();
table.innerHTML = data.streams.reduce((html, item) => { table.innerHTML = data.streams.reduce((html, item) => {
return html + `<tr><td>${item.name}</td><td>${item.url}</td></tr>` return html + `<tr><td>${item.name}</td><td>${item.url}</td></tr>`;
}, '<thead><tr><th>Name</th><th>Source</th></tr></thead><tbody>') + '</tbody>' }, '<thead><tr><th>Name</th><th>Source</th></tr></thead><tbody>') + '</tbody>';
} }
</script> </script>
@@ -87,19 +87,19 @@
</div> </div>
<script> <script>
document.getElementById('stream').addEventListener('click', async ev => { document.getElementById('stream').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block' ev.target.nextElementSibling.style.display = 'block';
}) });
document.getElementById('stream-form').addEventListener('submit', async ev => { document.getElementById('stream-form').addEventListener('submit', async ev => {
ev.preventDefault() ev.preventDefault();
const url = new URL('api/streams', location.href) const url = new URL('api/streams', location.href);
url.searchParams.set('name', ev.target.elements['name'].value) url.searchParams.set('name', ev.target.elements['name'].value);
url.searchParams.set('src', ev.target.elements['src'].value) url.searchParams.set('src', ev.target.elements['src'].value);
const r = await fetch(url, {method: 'PUT'}) const r = await fetch(url, {method: 'PUT'});
alert(r.ok ? 'OK' : 'ERROR') alert(r.ok ? 'OK' : 'ERROR');
}) });
</script> </script>
@@ -124,49 +124,49 @@
</div> </div>
<script> <script>
document.getElementById('homekit').addEventListener('click', async ev => { document.getElementById('homekit').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block' ev.target.nextElementSibling.style.display = 'block';
const r = await fetch('api/homekit', {cache: 'no-cache'}) const r = await fetch('api/homekit', {cache: 'no-cache'});
/** @type {Array<{id:string,name:string,addr:string,model:string,paired:boolean}>} */ /** @type {Array<{id:string,name:string,addr:string,model:string,paired:boolean}>} */
const data = await r.json() const data = await r.json();
const tbody = document.getElementById('homekit-body') const tbody = document.getElementById('homekit-body');
tbody.innerHTML = tbody.innerHTML =
data.reduce((res, item) => { data.reduce((res, item) => {
let commands = '' let commands = '';
if (item.id === "") { if (item.id === '') {
commands = `<a href="#" onclick="unpair('${item.name}')">unpair</a>` commands = `<a href="#" onclick="unpair('${item.name}')">unpair</a>`;
} else if (item.paired === false) { } else if (item.paired === false) {
commands = `<a href="#" onclick="pair('${item.id}','${item.name}')">pair</a>` commands = `<a href="#" onclick="pair('${item.id}','${item.name}')">pair</a>`;
} }
return res + `<tr> return res + `<tr>
<td>${item.name}</td> <td>${item.name}</td>
<td>${item.addr}</td> <td>${item.addr}</td>
<td>${item.model}</td> <td>${item.model}</td>
<td>${commands}</td> <td>${commands}</td>
</tr>` </tr>`;
}, '') }, '');
}) });
function pair(id, name) { function pair(id, name) {
const pin = document.querySelector('#pin').value const pin = document.querySelector('#pin').value;
fetch(`api/homekit?id=${id}&name=${name}&pin=${pin}`, {method: 'POST'}) fetch(`api/homekit?id=${id}&name=${name}&pin=${pin}`, {method: 'POST'})
.then(r => r.text()) .then(r => r.text())
.then(data => { .then(data => {
if (data.length > 0) alert(data) if (data.length > 0) alert(data);
else window.location.reload() else window.location.reload();
}) })
.catch(console.error) .catch(console.error);
} }
function unpair(src) { function unpair(src) {
fetch(`api/homekit?src=${src}`, {method: 'DELETE'}) fetch(`api/homekit?src=${src}`, {method: 'DELETE'})
.then(r => r.text()) .then(r => r.text())
.then(data => { .then(data => {
if (data.length > 0) alert(data) if (data.length > 0) alert(data);
else window.location.reload() else window.location.reload();
}) })
.catch(console.error) .catch(console.error);
} }
</script> </script>
@@ -177,9 +177,9 @@
</div> </div>
<script> <script>
document.getElementById('dvrip').addEventListener('click', async ev => { document.getElementById('dvrip').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block' ev.target.nextElementSibling.style.display = 'block';
await getStreams('api/dvrip', 'dvrip-table') await getStreams('api/dvrip', 'dvrip-table');
}) });
</script> </script>
@@ -190,9 +190,9 @@
</div> </div>
<script> <script>
document.getElementById('devices').addEventListener('click', async ev => { document.getElementById('devices').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block' ev.target.nextElementSibling.style.display = 'block';
await getStreams('api/ffmpeg/devices', 'devices-table') await getStreams('api/ffmpeg/devices', 'devices-table');
}) });
</script> </script>
@@ -203,9 +203,9 @@
</div> </div>
<script> <script>
document.getElementById('hardware').addEventListener('click', async ev => { document.getElementById('hardware').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block' ev.target.nextElementSibling.style.display = 'block';
await getStreams('api/ffmpeg/hardware', 'hardware-table') await getStreams('api/ffmpeg/hardware', 'hardware-table');
}) });
</script> </script>
@@ -223,18 +223,18 @@
</div> </div>
<script> <script>
document.getElementById('nest').addEventListener('click', async ev => { document.getElementById('nest').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block' ev.target.nextElementSibling.style.display = 'block';
}) });
document.getElementById('nest-form').addEventListener('submit', async ev => { document.getElementById('nest-form').addEventListener('submit', async ev => {
ev.preventDefault() ev.preventDefault();
const query = new URLSearchParams(new FormData(ev.target)) const query = new URLSearchParams(new FormData(ev.target));
const url = new URL('api/nest?' + query.toString(), location.href) const url = new URL('api/nest?' + query.toString(), location.href);
const r = await fetch(url, {cache: 'no-cache'}) const r = await fetch(url, {cache: 'no-cache'});
await getStreams(r, 'nest-table') await getStreams(r, 'nest-table');
}) });
</script> </script>
@@ -244,9 +244,9 @@
</div> </div>
<script> <script>
document.getElementById('hass').addEventListener('click', async ev => { document.getElementById('hass').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block' ev.target.nextElementSibling.style.display = 'block';
await getStreams('api/hass', 'hass-table') await getStreams('api/hass', 'hass-table');
}) });
</script> </script>
@@ -260,18 +260,18 @@
</div> </div>
<script> <script>
document.getElementById('onvif').addEventListener('click', async ev => { document.getElementById('onvif').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block' ev.target.nextElementSibling.style.display = 'block';
await getStreams('api/onvif', 'onvif-table') await getStreams('api/onvif', 'onvif-table');
}) });
document.getElementById('onvif-form').addEventListener('submit', async ev => { document.getElementById('onvif-form').addEventListener('submit', async ev => {
ev.preventDefault() ev.preventDefault();
const url = new URL('api/onvif', location.href) const url = new URL('api/onvif', location.href);
url.searchParams.set('src', ev.target.elements['src'].value) url.searchParams.set('src', ev.target.elements['src'].value);
await getStreams(url.toString(), 'onvif-table') await getStreams(url.toString(), 'onvif-table');
}) });
</script> </script>
@@ -287,15 +287,15 @@
</div> </div>
<script> <script>
document.getElementById('roborock').addEventListener('click', async ev => { document.getElementById('roborock').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block' ev.target.nextElementSibling.style.display = 'block';
await getStreams('api/roborock', 'roborock-table') await getStreams('api/roborock', 'roborock-table');
}) });
document.getElementById('roborock-form').addEventListener('submit', async ev => { document.getElementById('roborock-form').addEventListener('submit', async ev => {
ev.preventDefault() ev.preventDefault();
const r = await fetch('api/roborock', {method: 'POST', body: new FormData(ev.target)}) const r = await fetch('api/roborock', {method: 'POST', body: new FormData(ev.target)});
await getStreams(r, 'roborock-table') await getStreams(r, 'roborock-table');
}) });
</script> </script>
@@ -305,9 +305,9 @@
</div> </div>
<script> <script>
document.getElementById('webtorrent').addEventListener('click', async ev => { document.getElementById('webtorrent').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block' ev.target.nextElementSibling.style.display = 'block';
await getStreams('api/webtorrent', 'webtorrent-table') await getStreams('api/webtorrent', 'webtorrent-table');
}) });
</script> </script>

View File

@@ -18,21 +18,21 @@
<body> <body>
<div id="out"></div> <div id="out"></div>
<script> <script>
const out = document.getElementById("out"); const out = document.getElementById('out');
const print = (name, caps) => { const print = (name, caps) => {
out.innerText += name + "\n"; out.innerText += name + '\n';
caps.codecs.forEach((codec) => { caps.codecs.forEach((codec) => {
out.innerText += [codec.mimeType, codec.channels, codec.clockRate, codec.sdpFmtpLine] + "\n"; out.innerText += [codec.mimeType, codec.channels, codec.clockRate, codec.sdpFmtpLine] + '\n';
}); });
out.innerText += "\n"; out.innerText += '\n';
} };
if (RTCRtpReceiver.getCapabilities) { if (RTCRtpReceiver.getCapabilities) {
print("receiver video", RTCRtpReceiver.getCapabilities("video")); print('receiver video', RTCRtpReceiver.getCapabilities('video'));
print("receiver audio", RTCRtpReceiver.getCapabilities("audio")); print('receiver audio', RTCRtpReceiver.getCapabilities('audio'));
print("sender video", RTCRtpSender.getCapabilities("video")); print('sender video', RTCRtpSender.getCapabilities('video'));
print("sender audio", RTCRtpSender.getCapabilities("audio")); print('sender audio', RTCRtpSender.getCapabilities('audio'));
} }
const types = [ const types = [
@@ -48,14 +48,18 @@
'video/mp4; codecs="hvc1.1.6.L93.B0"', 'video/mp4; codecs="hvc1.1.6.L93.B0"',
'video/mp4; codecs="hev1.1.6.L93.B0"', 'video/mp4; codecs="hev1.1.6.L93.B0"',
'video/mp4; codecs="hev1.2.4.L120.B0"', 'video/mp4; codecs="hev1.2.4.L120.B0"',
'video/mp4; codecs="flac"',
'video/mp4; codecs="opus"',
'video/mp4; codecs="mp3"',
'video/mp4; codecs="null"',
'application/vnd.apple.mpegurl',
]; ];
const video = document.createElement("video"); const video = document.createElement('video');
out.innerText += "video.canPlayType\n"; out.innerText += 'video.canPlayType\n';
types.forEach(type => { types.forEach(type => {
out.innerText += `${type} = ${MediaSource.isTypeSupported(type)} / ${video.canPlayType(type)}\n`; out.innerText += `${type} = ${'MediaSource' in window && MediaSource.isTypeSupported(type)} / ${video.canPlayType(type)}\n`;
}) });
</script> </script>
</body> </body>
</html> </html>

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<title>File Editor</title> <title>File Editor</title>
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
@@ -32,8 +32,8 @@
<div id="config"></div> <div id="config"></div>
<script> <script>
ace.config.set('basePath', 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.14.0/'); ace.config.set('basePath', 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.14.0/');
const editor = ace.edit("config", { const editor = ace.edit('config', {
mode: "ace/mode/yaml", mode: 'ace/mode/yaml',
}); });
document.getElementById('save').addEventListener('click', () => { document.getElementById('save').addEventListener('click', () => {
@@ -63,7 +63,7 @@
alert(`Unknown error: ${r.statusText} (${r.status})`); alert(`Unknown error: ${r.statusText} (${r.status})`);
} }
}); });
}) });
</script> </script>
</body> </body>
</html> </html>

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
@@ -84,55 +84,55 @@
'<a href="#" data-name="{name}">delete</a>', '<a href="#" data-name="{name}">delete</a>',
]; ];
document.querySelector(".controls > button") document.querySelector('.controls > button')
.addEventListener("click", () => { .addEventListener('click', () => {
const url = new URL("stream.html", location.href); const url = new URL('stream.html', location.href);
const streams = document.querySelectorAll("#streams input"); const streams = document.querySelectorAll('#streams input');
streams.forEach(i => { streams.forEach(i => {
if (i.checked) url.searchParams.append("src", i.name); if (i.checked) url.searchParams.append('src', i.name);
}); });
if (!url.searchParams.has("src")) return; if (!url.searchParams.has('src')) return;
let mode = document.querySelectorAll(".controls input"); let mode = document.querySelectorAll('.controls input');
mode = Array.from(mode).filter(i => i.checked).map(i => i.name).join(","); mode = Array.from(mode).filter(i => i.checked).map(i => i.name).join(',');
window.location.href = `${url}&mode=${mode}`; window.location.href = `${url}&mode=${mode}`;
}); });
const tbody = document.getElementById("streams"); const tbody = document.getElementById('streams');
tbody.addEventListener("click", ev => { tbody.addEventListener('click', ev => {
if (ev.target.innerText !== "delete") return; if (ev.target.innerText !== 'delete') return;
ev.preventDefault(); ev.preventDefault();
const url = new URL("api/streams", location.href); const url = new URL('api/streams', location.href);
const src = decodeURIComponent(ev.target.dataset.name); const src = decodeURIComponent(ev.target.dataset.name);
url.searchParams.set("src", src); url.searchParams.set('src', src);
fetch(url, {method: "DELETE"}).then(reload); fetch(url, {method: 'DELETE'}).then(reload);
}); });
document.getElementById('selectall').addEventListener('change', ev => { document.getElementById('selectall').addEventListener('change', ev => {
document.querySelectorAll('#streams input').forEach(el => { document.querySelectorAll('#streams input').forEach(el => {
el.checked = ev.target.checked el.checked = ev.target.checked;
}) });
}) });
function reload() { function reload() {
const url = new URL("api/streams", location.href); const url = new URL('api/streams', location.href);
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => { fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
tbody.innerHTML = ""; tbody.innerHTML = '';
for (const [name, value] of Object.entries(data)) { for (const [name, value] of Object.entries(data)) {
const online = value && value.consumers ? value.consumers.length : 0; const online = value && value.consumers ? value.consumers.length : 0;
const src = encodeURIComponent(name); const src = encodeURIComponent(name);
const links = templates.map(link => { const links = templates.map(link => {
return link.replace("{name}", src); return link.replace('{name}', src);
}).join(" "); }).join(' ');
const tr = document.createElement("tr"); const tr = document.createElement('tr');
tr.dataset["id"] = name; tr.dataset['id'] = name;
tr.innerHTML = tr.innerHTML =
`<td><label><input type="checkbox" name="${name}">${name}</label></td>` + `<td><label><input type="checkbox" name="${name}">${name}</label></td>` +
`<td><a href="api/streams?src=${src}">${online} / info</a></td>` + `<td><a href="api/streams?src=${src}">${online} / info</a></td>` +
@@ -142,9 +142,9 @@
}); });
} }
const url = new URL("api", location.href); const url = new URL('api', location.href);
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => { fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
const info = document.querySelector(".info"); const info = document.querySelector('.info');
info.innerText = `Version: ${data.version}, Config: ${data.config_path}`; info.innerText = `Version: ${data.version}, Config: ${data.config_path}`;
}); });

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<title>go2rtc - links</title> <title>go2rtc - links</title>
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
@@ -42,21 +42,21 @@
<script src="main.js"></script> <script src="main.js"></script>
<div id="links"></div> <div id="links"></div>
<script> <script>
const src = new URLSearchParams(location.search).get('src') const src = new URLSearchParams(location.search).get('src');
document.getElementById('links').innerHTML = ` document.getElementById('links').innerHTML = `
<h2>Any codec in source</h2> <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="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> <li><a href="api/streams?src=${src}">info.json</a> page with active connections</li>
` `;
const url = new URL('api', location.href) const url = new URL('api', location.href);
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => { fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
let rtsp = location.host + ':8554' let rtsp = location.host + ':8554';
try { try {
const host = data.host.match(/^[^:]+/)[0] const host = data.host.match(/^[^:]+/)[0];
const port = data.rtsp.listen.match(/[0-9]+$/)[0] const port = data.rtsp.listen.match(/[0-9]+$/)[0];
rtsp = `${host}:${port}` rtsp = `${host}:${port}`;
} catch (e) { } catch (e) {
} }
@@ -80,8 +80,8 @@
<li><a href="stream.html?src=${src}&mode=mjpeg">stream.html</a> with MJPEG mode / browsers: all / codecs: MJPEG, JPEG</li> <li><a href="stream.html?src=${src}&mode=mjpeg">stream.html</a> with MJPEG mode / browsers: all / codecs: MJPEG, JPEG</li>
<li><a href="api/stream.mjpeg?src=${src}">stream.mjpeg</a> MJPEG stream / browsers: all / codecs: MJPEG, JPEG</li> <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> <li><a href="api/frame.jpeg?src=${src}">frame.jpeg</a> snapshot in JPEG-format / browsers: all / codecs: MJPEG, JPEG</li>
` `;
}) });
</script> </script>
<div> <div>
@@ -92,12 +92,12 @@
</div> </div>
<script> <script>
document.getElementById('send').addEventListener('click', ev => { document.getElementById('send').addEventListener('click', ev => {
ev.preventDefault() ev.preventDefault();
const url = new URL('api/streams', location.href) const url = new URL('api/streams', location.href);
url.searchParams.set('dst', src) url.searchParams.set('dst', src);
url.searchParams.set('src', document.getElementById('source').value) url.searchParams.set('src', document.getElementById('source').value);
fetch(url, {method: 'POST'}) fetch(url, {method: 'POST'});
}) });
</script> </script>
<div id="webrtc"> <div id="webrtc">
@@ -119,62 +119,62 @@
</div> </div>
<script> <script>
function webrtcLinksUpdate() { function webrtcLinksUpdate() {
const media = document.querySelector('input[name="webrtc"]:checked').value const media = document.querySelector('input[name="webrtc"]:checked').value;
const direction = media.indexOf('video') >= 0 || media === 'audio' ? 'src' : 'dst' const direction = media.indexOf('video') >= 0 || media === 'audio' ? 'src' : 'dst';
document.getElementById('local').href = `webrtc.html?${direction}=${src}&media=${media}` document.getElementById('local').href = `webrtc.html?${direction}=${src}&media=${media}`;
const share = document.getElementById('shareget') const share = document.getElementById('shareget');
share.href = `https://alexxit.github.io/go2rtc/#${share.dataset.auth}&media=${media}` share.href = `https://alexxit.github.io/go2rtc/#${share.dataset.auth}&media=${media}`;
} }
function share(method) { function share(method) {
const url = new URL('api/webtorrent', location.href) const url = new URL('api/webtorrent', location.href);
url.searchParams.set('src', src) url.searchParams.set('src', src);
return fetch(url, {method: method, cache: 'no-cache'}) return fetch(url, {method: method, cache: 'no-cache'});
} }
function onshareadd(r) { function onshareadd(r) {
document.getElementById('shareget').dataset['auth'] = `share=${r.share}&pwd=${r.pwd}` document.getElementById('shareget').dataset['auth'] = `share=${r.share}&pwd=${r.pwd}`;
document.getElementById('shareadd').style.display = 'none' document.getElementById('shareadd').style.display = 'none';
document.getElementById('shareget').style.display = '' document.getElementById('shareget').style.display = '';
document.getElementById('sharedel').style.display = '' document.getElementById('sharedel').style.display = '';
webrtcLinksUpdate() webrtcLinksUpdate();
} }
function onsharedel() { function onsharedel() {
document.getElementById('shareadd').style.display = '' document.getElementById('shareadd').style.display = '';
document.getElementById('shareget').style.display = 'none' document.getElementById('shareget').style.display = 'none';
document.getElementById('sharedel').style.display = 'none' document.getElementById('sharedel').style.display = 'none';
} }
document.getElementById('shareadd').addEventListener('click', ev => { document.getElementById('shareadd').addEventListener('click', ev => {
ev.preventDefault() ev.preventDefault();
share('POST').then(r => r.json()).then(r => onshareadd(r)) share('POST').then(r => r.json()).then(r => onshareadd(r));
}) });
document.getElementById('shareget').addEventListener('click', ev => { document.getElementById('shareget').addEventListener('click', ev => {
ev.preventDefault() ev.preventDefault();
navigator.clipboard.writeText(ev.target.href) navigator.clipboard.writeText(ev.target.href);
}) });
document.getElementById('sharedel').addEventListener('click', ev => { document.getElementById('sharedel').addEventListener('click', ev => {
ev.preventDefault() ev.preventDefault();
share('DELETE').then(r => onsharedel()) share('DELETE').then(() => onsharedel());
}) });
document.getElementById('webrtc').addEventListener('click', ev => { document.getElementById('webrtc').addEventListener('click', ev => {
if (ev.target.tagName === 'INPUT') webrtcLinksUpdate() if (ev.target.tagName === 'INPUT') webrtcLinksUpdate();
}) });
share('GET').then(r => { share('GET').then(r => {
if (r.ok) r.json().then(r => onshareadd(r)) if (r.ok) r.json().then(r => onshareadd(r));
else onsharedel() else onsharedel();
}) });
webrtcLinksUpdate() webrtcLinksUpdate();
</script> </script>
</body> </body>

View File

@@ -30,9 +30,9 @@
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
// support multiple streams and multiple modes // support multiple streams and multiple modes
const streams = params.getAll("src"); const streams = params.getAll('src');
const modes = params.getAll("mode"); const modes = params.getAll('mode');
if (modes.length === 0) modes.push(""); if (modes.length === 0) modes.push('');
while (modes.length > streams.length) { while (modes.length > streams.length) {
streams.push(streams[0]); streams.push(streams[0]);
@@ -42,19 +42,19 @@
} }
if (streams.length > 1) { if (streams.length > 1) {
document.body.className = "flex"; document.body.className = 'flex';
} }
const background = params.get("background") !== "false"; const background = params.get('background') !== 'false';
const width = "1 0 " + (params.get("width") || "320px"); const width = '1 0 ' + (params.get('width') || '320px');
for (let i = 0; i < streams.length; i++) { for (let i = 0; i < streams.length; i++) {
/** @type {VideoStream} */ /** @type {VideoStream} */
const video = document.createElement("video-stream"); const video = document.createElement('video-stream');
video.background = background; video.background = background;
video.mode = modes[i] || video.mode; video.mode = modes[i] || video.mode;
video.style.flex = width; video.style.flex = width;
video.src = new URL("api/ws?src=" + encodeURIComponent(streams[i]), location.href); video.src = new URL('api/ws?src=' + encodeURIComponent(streams[i]), location.href);
document.body.appendChild(video); document.body.appendChild(video);
} }
</script> </script>

View File

@@ -21,21 +21,22 @@ export class VideoRTC extends HTMLElement {
this.RECONNECT_TIMEOUT = 30000; this.RECONNECT_TIMEOUT = 30000;
this.CODECS = [ this.CODECS = [
"avc1.640029", // H.264 high 4.1 (Chromecast 1st and 2nd Gen) 'avc1.640029', // H.264 high 4.1 (Chromecast 1st and 2nd Gen)
"avc1.64002A", // H.264 high 4.2 (Chromecast 3rd Gen) 'avc1.64002A', // H.264 high 4.2 (Chromecast 3rd Gen)
"avc1.640033", // H.264 high 5.1 (Chromecast with Google TV) 'avc1.640033', // H.264 high 5.1 (Chromecast with Google TV)
"hvc1.1.6.L153.B0", // H.265 main 5.1 (Chromecast Ultra) 'hvc1.1.6.L153.B0', // H.265 main 5.1 (Chromecast Ultra)
"mp4a.40.2", // AAC LC 'mp4a.40.2', // AAC LC
"mp4a.40.5", // AAC HE 'mp4a.40.5', // AAC HE
"flac", // FLAC (PCM compatible) 'null', // for detecting liars (old iOS 12)
"opus", // OPUS Chrome, Firefox 'flac', // FLAC (PCM compatible)
'opus', // OPUS Chrome, Firefox
]; ];
/** /**
* [config] Supported modes (webrtc, webrtc/tcp, mse, hls, mp4, mjpeg). * [config] Supported modes (webrtc, webrtc/tcp, mse, hls, mp4, mjpeg).
* @type {string} * @type {string}
*/ */
this.mode = "webrtc,mse,hls,mjpeg"; this.mode = 'webrtc,mse,hls,mjpeg';
/** /**
* [config] Run stream when not displayed on the screen. Default `false`. * [config] Run stream when not displayed on the screen. Default `false`.
@@ -92,7 +93,7 @@ export class VideoRTC extends HTMLElement {
/** /**
* @type {string|URL} * @type {string|URL}
*/ */
this.wsURL = ""; this.wsURL = '';
/** /**
* @type {RTCPeerConnection} * @type {RTCPeerConnection}
@@ -107,7 +108,7 @@ export class VideoRTC extends HTMLElement {
/** /**
* @type {string} * @type {string}
*/ */
this.mseCodecs = ""; this.mseCodecs = '';
/** /**
* [internal] Disconnect TimeoutID. * [internal] Disconnect TimeoutID.
@@ -139,11 +140,11 @@ export class VideoRTC extends HTMLElement {
* @param {string|URL} value * @param {string|URL} value
*/ */
set src(value) { set src(value) {
if (typeof value !== "string") value = value.toString(); if (typeof value !== 'string') value = value.toString();
if (value.startsWith("http")) { if (value.startsWith('http')) {
value = "ws" + value.substring(4); value = 'ws' + value.substring(4);
} else if (value.startsWith("/")) { } else if (value.startsWith('/')) {
value = "ws" + location.origin.substring(4) + value; value = 'ws' + location.origin.substring(4) + value;
} }
this.wsURL = value; this.wsURL = value;
@@ -173,7 +174,7 @@ export class VideoRTC extends HTMLElement {
} }
codecs(type) { codecs(type) {
const test = type === "mse" const test = type === 'mse'
? codec => MediaSource.isTypeSupported(`video/mp4; codecs="${codec}"`) ? codec => MediaSource.isTypeSupported(`video/mp4; codecs="${codec}"`)
: codec => this.video.canPlayType(`video/mp4; codecs="${codec}"`); : codec => this.video.canPlayType(`video/mp4; codecs="${codec}"`);
return this.CODECS.filter(test).join(); return this.CODECS.filter(test).join();
@@ -227,30 +228,30 @@ export class VideoRTC extends HTMLElement {
* Creates child DOM elements. Called automatically once on `connectedCallback`. * Creates child DOM elements. Called automatically once on `connectedCallback`.
*/ */
oninit() { oninit() {
this.video = document.createElement("video"); this.video = document.createElement('video');
this.video.controls = true; this.video.controls = true;
this.video.playsInline = true; this.video.playsInline = true;
this.video.preload = "auto"; this.video.preload = 'auto';
this.video.style.display = "block"; // fix bottom margin 4px this.video.style.display = 'block'; // fix bottom margin 4px
this.video.style.width = "100%"; this.video.style.width = '100%';
this.video.style.height = "100%" this.video.style.height = '100%';
this.appendChild(this.video); this.appendChild(this.video);
if (this.background) return; if (this.background) return;
if ("hidden" in document && this.visibilityCheck) { if ('hidden' in document && this.visibilityCheck) {
document.addEventListener("visibilitychange", () => { document.addEventListener('visibilitychange', () => {
if (document.hidden) { if (document.hidden) {
this.disconnectedCallback(); this.disconnectedCallback();
} else if (this.isConnected) { } else if (this.isConnected) {
this.connectedCallback(); this.connectedCallback();
} }
}) });
} }
if ("IntersectionObserver" in window && this.visibilityThreshold) { if ('IntersectionObserver' in window && this.visibilityThreshold) {
const observer = new IntersectionObserver(entries => { const observer = new IntersectionObserver(entries => {
entries.forEach(entry => { entries.forEach(entry => {
if (!entry.isIntersecting) { if (!entry.isIntersecting) {
@@ -277,9 +278,9 @@ export class VideoRTC extends HTMLElement {
this.connectTS = Date.now(); this.connectTS = Date.now();
this.ws = new WebSocket(this.wsURL); this.ws = new WebSocket(this.wsURL);
this.ws.binaryType = "arraybuffer"; this.ws.binaryType = 'arraybuffer';
this.ws.addEventListener("open", ev => this.onopen(ev)); this.ws.addEventListener('open', () => this.onopen());
this.ws.addEventListener("close", ev => this.onclose(ev)); this.ws.addEventListener('close', () => this.onclose());
return true; return true;
} }
@@ -305,8 +306,8 @@ export class VideoRTC extends HTMLElement {
// CONNECTING => OPEN // CONNECTING => OPEN
this.wsState = WebSocket.OPEN; this.wsState = WebSocket.OPEN;
this.ws.addEventListener("message", ev => { this.ws.addEventListener('message', ev => {
if (typeof ev.data === "string") { if (typeof ev.data === 'string') {
const msg = JSON.parse(ev.data); const msg = JSON.parse(ev.data);
for (const mode in this.onmessage) { for (const mode in this.onmessage) {
this.onmessage[mode](msg); this.onmessage[mode](msg);
@@ -321,30 +322,30 @@ export class VideoRTC extends HTMLElement {
const modes = []; const modes = [];
if (this.mode.indexOf("mse") >= 0 && "MediaSource" in window) { // iPhone if (this.mode.indexOf('mse') >= 0 && 'MediaSource' in window) { // iPhone
modes.push("mse"); modes.push('mse');
this.onmse(); this.onmse();
} else if (this.mode.indexOf("hls") >= 0 && this.video.canPlayType("application/vnd.apple.mpegurl")) { } else if (this.mode.indexOf('hls') >= 0 && this.video.canPlayType('application/vnd.apple.mpegurl')) {
modes.push("hls"); modes.push('hls');
this.onhls(); this.onhls();
} else if (this.mode.indexOf("mp4") >= 0) { } else if (this.mode.indexOf('mp4') >= 0) {
modes.push("mp4"); modes.push('mp4');
this.onmp4(); this.onmp4();
} }
if (this.mode.indexOf("webrtc") >= 0 && "RTCPeerConnection" in window) { // macOS Desktop app if (this.mode.indexOf('webrtc') >= 0 && 'RTCPeerConnection' in window) { // macOS Desktop app
modes.push("webrtc"); modes.push('webrtc');
this.onwebrtc(); this.onwebrtc();
} }
if (this.mode.indexOf("mjpeg") >= 0) { if (this.mode.indexOf('mjpeg') >= 0) {
if (modes.length) { if (modes.length) {
this.onmessage["mjpeg"] = msg => { this.onmessage['mjpeg'] = msg => {
if (msg.type !== "error" || msg.value.indexOf(modes[0]) !== 0) return; if (msg.type !== 'error' || msg.value.indexOf(modes[0]) !== 0) return;
this.onmjpeg(); this.onmjpeg();
} };
} else { } else {
modes.push("mjpeg"); modes.push('mjpeg');
this.onmjpeg(); this.onmjpeg();
} }
} }
@@ -375,25 +376,25 @@ export class VideoRTC extends HTMLElement {
onmse() { onmse() {
const ms = new MediaSource(); const ms = new MediaSource();
ms.addEventListener("sourceopen", () => { ms.addEventListener('sourceopen', () => {
URL.revokeObjectURL(this.video.src); URL.revokeObjectURL(this.video.src);
this.send({type: "mse", value: this.codecs("mse")}); this.send({type: 'mse', value: this.codecs('mse')});
}, {once: true}); }, {once: true});
this.video.src = URL.createObjectURL(ms); this.video.src = URL.createObjectURL(ms);
this.video.srcObject = null; this.video.srcObject = null;
this.play(); this.play();
this.mseCodecs = ""; this.mseCodecs = '';
this.onmessage["mse"] = msg => { this.onmessage['mse'] = msg => {
if (msg.type !== "mse") return; if (msg.type !== 'mse') return;
this.mseCodecs = msg.value; this.mseCodecs = msg.value;
const sb = ms.addSourceBuffer(msg.value); const sb = ms.addSourceBuffer(msg.value);
sb.mode = "segments"; // segments or sequence sb.mode = 'segments'; // segments or sequence
sb.addEventListener("updateend", () => { sb.addEventListener('updateend', () => {
if (sb.updating) return; if (sb.updating) return;
try { try {
@@ -431,25 +432,25 @@ export class VideoRTC extends HTMLElement {
// console.debug(e); // console.debug(e);
} }
} }
} };
} };
} }
onwebrtc() { onwebrtc() {
const pc = new RTCPeerConnection(this.pcConfig); const pc = new RTCPeerConnection(this.pcConfig);
/** @type {HTMLVideoElement} */ /** @type {HTMLVideoElement} */
const video2 = document.createElement("video"); const video2 = document.createElement('video');
video2.addEventListener("loadeddata", ev => this.onpcvideo(ev), {once: true}); video2.addEventListener('loadeddata', ev => this.onpcvideo(ev), {once: true});
pc.addEventListener("icecandidate", ev => { pc.addEventListener('icecandidate', ev => {
if (ev.candidate && this.mode.indexOf("webrtc/tcp") >= 0 && ev.candidate.protocol === "udp") return; if (ev.candidate && this.mode.indexOf('webrtc/tcp') >= 0 && ev.candidate.protocol === 'udp') return;
const candidate = ev.candidate ? ev.candidate.toJSON().candidate : ""; const candidate = ev.candidate ? ev.candidate.toJSON().candidate : '';
this.send({type: "webrtc/candidate", value: candidate}); this.send({type: 'webrtc/candidate', value: candidate});
}); });
pc.addEventListener("track", ev => { pc.addEventListener('track', ev => {
// when stream already init // when stream already init
if (video2.srcObject !== null) return; if (video2.srcObject !== null) return;
@@ -462,8 +463,8 @@ export class VideoRTC extends HTMLElement {
video2.srcObject = ev.streams[0]; video2.srcObject = ev.streams[0];
}); });
pc.addEventListener("connectionstatechange", () => { pc.addEventListener('connectionstatechange', () => {
if (pc.connectionState === "failed" || pc.connectionState === "disconnected") { if (pc.connectionState === 'failed' || pc.connectionState === 'disconnected') {
pc.close(); // stop next events pc.close(); // stop next events
this.pcState = WebSocket.CLOSED; this.pcState = WebSocket.CLOSED;
@@ -473,29 +474,33 @@ export class VideoRTC extends HTMLElement {
} }
}); });
this.onmessage["webrtc"] = msg => { this.onmessage['webrtc'] = msg => {
switch (msg.type) { switch (msg.type) {
case "webrtc/candidate": case 'webrtc/candidate':
if (this.mode.indexOf("webrtc/tcp") >= 0 && msg.value.indexOf(" udp ") > 0) return; if (this.mode.indexOf('webrtc/tcp') >= 0 && msg.value.indexOf(' udp ') > 0) return;
pc.addIceCandidate({candidate: msg.value, sdpMid: "0"}).catch(() => console.debug); pc.addIceCandidate({candidate: msg.value, sdpMid: '0'}).catch(er => {
console.warn(er);
});
break; break;
case "webrtc/answer": case 'webrtc/answer':
pc.setRemoteDescription({type: "answer", sdp: msg.value}).catch(() => console.debug); pc.setRemoteDescription({type: 'answer', sdp: msg.value}).catch(er => {
console.warn(er);
});
break; break;
case "error": case 'error':
if (msg.value.indexOf("webrtc/offer") < 0) return; if (msg.value.indexOf('webrtc/offer') < 0) return;
pc.close(); pc.close();
} }
}; };
// Safari doesn't support "offerToReceiveVideo" // Safari doesn't support "offerToReceiveVideo"
pc.addTransceiver("video", {direction: "recvonly"}); pc.addTransceiver('video', {direction: 'recvonly'});
pc.addTransceiver("audio", {direction: "recvonly"}); pc.addTransceiver('audio', {direction: 'recvonly'});
pc.createOffer().then(offer => { pc.createOffer().then(offer => {
pc.setLocalDescription(offer).then(() => { pc.setLocalDescription(offer).then(() => {
this.send({type: "webrtc/offer", value: offer.sdp}); this.send({type: 'webrtc/offer', value: offer.sdp});
}); });
}); });
@@ -514,7 +519,7 @@ export class VideoRTC extends HTMLElement {
const state = this.pc.connectionState; const state = this.pc.connectionState;
// Firefox doesn't support pc.connectionState // Firefox doesn't support pc.connectionState
if (state === "connected" || state === "connecting" || !state) { if (state === 'connected' || state === 'connecting' || !state) {
// Video+Audio > Video, H265 > H264, Video > Audio, WebRTC > MSE // Video+Audio > Video, H265 > H264, Video > Audio, WebRTC > MSE
let rtcPriority = 0, msePriority = 0; let rtcPriority = 0, msePriority = 0;
@@ -523,9 +528,9 @@ export class VideoRTC extends HTMLElement {
if (ms.getVideoTracks().length > 0) rtcPriority += 0x220; if (ms.getVideoTracks().length > 0) rtcPriority += 0x220;
if (ms.getAudioTracks().length > 0) rtcPriority += 0x102; if (ms.getAudioTracks().length > 0) rtcPriority += 0x102;
if (this.mseCodecs.indexOf("hvc1.") >= 0) msePriority += 0x230; if (this.mseCodecs.indexOf('hvc1.') >= 0) msePriority += 0x230;
if (this.mseCodecs.indexOf("avc1.") >= 0) msePriority += 0x210; if (this.mseCodecs.indexOf('avc1.') >= 0) msePriority += 0x210;
if (this.mseCodecs.indexOf("mp4a.") >= 0) msePriority += 0x101; if (this.mseCodecs.indexOf('mp4a.') >= 0) msePriority += 0x101;
if (rtcPriority >= msePriority) { if (rtcPriority >= msePriority) {
this.video.srcObject = ms; this.video.srcObject = ms;
@@ -549,36 +554,38 @@ export class VideoRTC extends HTMLElement {
onmjpeg() { onmjpeg() {
this.ondata = data => { this.ondata = data => {
this.video.controls = false; this.video.controls = false;
this.video.poster = "data:image/jpeg;base64," + VideoRTC.btoa(data); this.video.poster = 'data:image/jpeg;base64,' + VideoRTC.btoa(data);
}; };
this.send({type: "mjpeg"}); this.send({type: 'mjpeg'});
} }
onhls() { onhls() {
this.onmessage["hls"] = msg => { this.onmessage['hls'] = msg => {
const url = "http" + this.wsURL.substring(2, this.wsURL.indexOf("/ws")) + "/hls/"; if (msg.type !== 'hls') return;
const playlist = msg.value.replace("hls/", url);
this.video.src = "data:application/vnd.apple.mpegurl;base64," + btoa(playlist);
this.play();
}
this.send({type: "hls", value: this.codecs("hls")}); const url = 'http' + this.wsURL.substring(2, this.wsURL.indexOf('/ws')) + '/hls/';
const playlist = msg.value.replace('hls/', url);
this.video.src = 'data:application/vnd.apple.mpegurl;base64,' + btoa(playlist);
this.play();
};
this.send({type: 'hls', value: this.codecs('hls')});
} }
onmp4() { onmp4() {
/** @type {HTMLCanvasElement} **/ /** @type {HTMLCanvasElement} **/
const canvas = document.createElement("canvas"); const canvas = document.createElement('canvas');
/** @type {CanvasRenderingContext2D} */ /** @type {CanvasRenderingContext2D} */
let context; let context;
/** @type {HTMLVideoElement} */ /** @type {HTMLVideoElement} */
const video2 = document.createElement("video"); const video2 = document.createElement('video');
video2.autoplay = true; video2.autoplay = true;
video2.playsInline = true; video2.playsInline = true;
video2.muted = true; video2.muted = true;
video2.addEventListener("loadeddata", ev => { video2.addEventListener('loadeddata', () => {
if (!context) { if (!context) {
canvas.width = video2.videoWidth; canvas.width = video2.videoWidth;
canvas.height = video2.videoHeight; canvas.height = video2.videoHeight;
@@ -588,20 +595,20 @@ export class VideoRTC extends HTMLElement {
context.drawImage(video2, 0, 0, canvas.width, canvas.height); context.drawImage(video2, 0, 0, canvas.width, canvas.height);
this.video.controls = false; this.video.controls = false;
this.video.poster = canvas.toDataURL("image/jpeg"); this.video.poster = canvas.toDataURL('image/jpeg');
}); });
this.ondata = data => { this.ondata = data => {
video2.src = "data:video/mp4;base64," + VideoRTC.btoa(data); video2.src = 'data:video/mp4;base64,' + VideoRTC.btoa(data);
}; };
this.send({type: "mp4", value: this.codecs("mp4")}); this.send({type: 'mp4', value: this.codecs('mp4')});
} }
static btoa(buffer) { static btoa(buffer) {
const bytes = new Uint8Array(buffer); const bytes = new Uint8Array(buffer);
const len = bytes.byteLength; const len = bytes.byteLength;
let binary = ""; let binary = '';
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]); binary += String.fromCharCode(bytes[i]);
} }

View File

@@ -1,23 +1,23 @@
import {VideoRTC} from "./video-rtc.js"; import {VideoRTC} from './video-rtc.js';
class VideoStream extends VideoRTC { class VideoStream extends VideoRTC {
set divMode(value) { set divMode(value) {
this.querySelector(".mode").innerText = value; this.querySelector('.mode').innerText = value;
this.querySelector(".status").innerText = ""; this.querySelector('.status').innerText = '';
} }
set divError(value) { set divError(value) {
const state = this.querySelector(".mode").innerText; const state = this.querySelector('.mode').innerText;
if (state !== "loading") return; if (state !== 'loading') return;
this.querySelector(".mode").innerText = "error"; this.querySelector('.mode').innerText = 'error';
this.querySelector(".status").innerText = value; this.querySelector('.status').innerText = value;
} }
/** /**
* Custom GUI * Custom GUI
*/ */
oninit() { oninit() {
console.debug("stream.oninit"); console.debug('stream.oninit');
super.oninit(); super.oninit();
this.innerHTML = ` this.innerHTML = `
@@ -43,57 +43,57 @@ class VideoStream extends VideoRTC {
</div> </div>
`; `;
const info = this.querySelector(".info") const info = this.querySelector('.info');
this.insertBefore(this.video, info); this.insertBefore(this.video, info);
} }
onconnect() { onconnect() {
console.debug("stream.onconnect"); console.debug('stream.onconnect');
const result = super.onconnect(); const result = super.onconnect();
if (result) this.divMode = "loading"; if (result) this.divMode = 'loading';
return result; return result;
} }
ondisconnect() { ondisconnect() {
console.debug("stream.ondisconnect"); console.debug('stream.ondisconnect');
super.ondisconnect(); super.ondisconnect();
} }
onopen() { onopen() {
console.debug("stream.onopen"); console.debug('stream.onopen');
const result = super.onopen(); const result = super.onopen();
this.onmessage["stream"] = msg => { this.onmessage['stream'] = msg => {
console.debug("stream.onmessge", msg); console.debug('stream.onmessge', msg);
switch (msg.type) { switch (msg.type) {
case "error": case 'error':
this.divError = msg.value; this.divError = msg.value;
break; break;
case "mse": case 'mse':
case "hls": case 'hls':
case "mp4": case 'mp4':
case "mjpeg": case 'mjpeg':
this.divMode = msg.type.toUpperCase(); this.divMode = msg.type.toUpperCase();
break; break;
} }
} };
return result; return result;
} }
onclose() { onclose() {
console.debug("stream.onclose"); console.debug('stream.onclose');
return super.onclose(); return super.onclose();
} }
onpcvideo(ev) { onpcvideo(ev) {
console.debug("stream.onpcvideo"); console.debug('stream.onpcvideo');
super.onpcvideo(ev); super.onpcvideo(ev);
if (this.pcState !== WebSocket.CLOSED) { if (this.pcState !== WebSocket.CLOSED) {
this.divMode = "RTC"; this.divMode = 'RTC';
} }
} }
} }
customElements.define("video-stream", VideoStream); customElements.define('video-stream', VideoStream);

View File

@@ -22,45 +22,45 @@
async function PeerConnection(media) { async function PeerConnection(media) {
const pc = new RTCPeerConnection({ const pc = new RTCPeerConnection({
iceServers: [{urls: 'stun:stun.l.google.com:19302'}] iceServers: [{urls: 'stun:stun.l.google.com:19302'}]
}) });
document.getElementById('video').srcObject = new MediaStream([ document.getElementById('video').srcObject = new MediaStream([
pc.addTransceiver('audio', {direction: 'sendrecv'}).receiver.track, pc.addTransceiver('audio', {direction: 'sendrecv'}).receiver.track,
pc.addTransceiver('video', {direction: 'sendrecv'}).receiver.track, pc.addTransceiver('video', {direction: 'sendrecv'}).receiver.track,
]) ]);
const tracks = await navigator.mediaDevices.getUserMedia({ const tracks = await navigator.mediaDevices.getUserMedia({
video: media.indexOf('camera') >= 0, video: media.indexOf('camera') >= 0,
audio: media.indexOf('microphone') >= 0, audio: media.indexOf('microphone') >= 0,
}) });
tracks.getTracks().forEach(track => { tracks.getTracks().forEach(track => {
pc.addTrack(track) pc.addTrack(track);
}) });
return pc return pc;
} }
function getCompleteOffer(pc, timeout) { function getCompleteOffer(pc, timeout) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
pc.addEventListener('icegatheringstatechange', () => { pc.addEventListener('icegatheringstatechange', () => {
if (pc.iceGatheringState === 'complete') resolve(pc.localDescription.sdp) if (pc.iceGatheringState === 'complete') resolve(pc.localDescription.sdp);
}) });
pc.createOffer().then(offer => pc.setLocalDescription(offer)) pc.createOffer().then(offer => pc.setLocalDescription(offer));
setTimeout(() => resolve(pc.localDescription.sdp), timeout || 3000) setTimeout(() => resolve(pc.localDescription.sdp), timeout || 3000);
}) });
} }
async function connect() { async function connect() {
const media = new URLSearchParams(location.search).get('media') const media = new URLSearchParams(location.search).get('media');
const pc = await PeerConnection(media) const pc = await PeerConnection(media);
const url = new URL('api/webrtc' + location.search, location.href) const url = new URL('api/webrtc' + location.search, location.href);
const r = await fetch(url, {method: 'POST', body: await getCompleteOffer(pc)}) const r = await fetch(url, {method: 'POST', body: await getCompleteOffer(pc)});
await pc.setRemoteDescription({type: 'answer', sdp: await r.text()}) await pc.setRemoteDescription({type: 'answer', sdp: await r.text()});
} }
connect() connect();
</script> </script>
</body> </body>
</html> </html>

View File

@@ -22,86 +22,86 @@
async function PeerConnection(media) { async function PeerConnection(media) {
const pc = new RTCPeerConnection({ const pc = new RTCPeerConnection({
iceServers: [{urls: 'stun:stun.l.google.com:19302'}] iceServers: [{urls: 'stun:stun.l.google.com:19302'}]
}) });
const localTracks = [] const localTracks = [];
if (/camera|microphone/.test(media)) { if (/camera|microphone/.test(media)) {
const tracks = await getMediaTracks('user', { const tracks = await getMediaTracks('user', {
video: media.indexOf('camera') >= 0, video: media.indexOf('camera') >= 0,
audio: media.indexOf('microphone') >= 0, audio: media.indexOf('microphone') >= 0,
}) });
tracks.forEach(track => { tracks.forEach(track => {
pc.addTransceiver(track, {direction: 'sendonly'}) pc.addTransceiver(track, {direction: 'sendonly'});
if (track.kind === 'video') localTracks.push(track) if (track.kind === 'video') localTracks.push(track);
}) });
} }
if (media.indexOf('display') >= 0) { if (media.indexOf('display') >= 0) {
const tracks = await getMediaTracks('display', { const tracks = await getMediaTracks('display', {
video: true, video: true,
audio: media.indexOf('speaker') >= 0, audio: media.indexOf('speaker') >= 0,
}) });
tracks.forEach(track => { tracks.forEach(track => {
pc.addTransceiver(track, {direction: 'sendonly'}) pc.addTransceiver(track, {direction: 'sendonly'});
if (track.kind === 'video') localTracks.push(track) if (track.kind === 'video') localTracks.push(track);
}) });
} }
if (/video|audio/.test(media)) { if (/video|audio/.test(media)) {
const tracks = ['video', 'audio'] const tracks = ['video', 'audio']
.filter(kind => media.indexOf(kind) >= 0) .filter(kind => media.indexOf(kind) >= 0)
.map(kind => pc.addTransceiver(kind, {direction: 'recvonly'}).receiver.track) .map(kind => pc.addTransceiver(kind, {direction: 'recvonly'}).receiver.track);
localTracks.push(...tracks) localTracks.push(...tracks);
} }
document.getElementById('video').srcObject = new MediaStream(localTracks) document.getElementById('video').srcObject = new MediaStream(localTracks);
return pc return pc;
} }
async function getMediaTracks(media, constraints) { async function getMediaTracks(media, constraints) {
try { try {
const stream = media === 'user' const stream = media === 'user'
? await navigator.mediaDevices.getUserMedia(constraints) ? await navigator.mediaDevices.getUserMedia(constraints)
: await navigator.mediaDevices.getDisplayMedia(constraints) : await navigator.mediaDevices.getDisplayMedia(constraints);
return stream.getTracks() return stream.getTracks();
} catch (e) { } catch (e) {
console.warn(e) console.warn(e);
return [] return [];
} }
} }
async function connect(media) { async function connect(media) {
const pc = await PeerConnection(media) const pc = await PeerConnection(media);
const url = new URL('api/ws' + location.search, location.href) const url = new URL('api/ws' + location.search, location.href);
const ws = new WebSocket('ws' + url.toString().substring(4)) const ws = new WebSocket('ws' + url.toString().substring(4));
ws.addEventListener('open', () => { ws.addEventListener('open', () => {
pc.addEventListener('icecandidate', ev => { pc.addEventListener('icecandidate', ev => {
if (!ev.candidate) return if (!ev.candidate) return;
const msg = {type: 'webrtc/candidate', value: ev.candidate.candidate} const msg = {type: 'webrtc/candidate', value: ev.candidate.candidate};
ws.send(JSON.stringify(msg)) ws.send(JSON.stringify(msg));
}) });
pc.createOffer().then(offer => pc.setLocalDescription(offer)).then(() => { pc.createOffer().then(offer => pc.setLocalDescription(offer)).then(() => {
const msg = {type: 'webrtc/offer', value: pc.localDescription.sdp} const msg = {type: 'webrtc/offer', value: pc.localDescription.sdp};
ws.send(JSON.stringify(msg)) ws.send(JSON.stringify(msg));
}) });
}) });
ws.addEventListener('message', ev => { ws.addEventListener('message', ev => {
const msg = JSON.parse(ev.data) const msg = JSON.parse(ev.data);
if (msg.type === 'webrtc/candidate') { if (msg.type === 'webrtc/candidate') {
pc.addIceCandidate({candidate: msg.value, sdpMid: '0'}) pc.addIceCandidate({candidate: msg.value, sdpMid: '0'});
} else if (msg.type === 'webrtc/answer') { } else if (msg.type === 'webrtc/answer') {
pc.setRemoteDescription({type: 'answer', sdp: msg.value}) pc.setRemoteDescription({type: 'answer', sdp: msg.value});
} }
}) });
} }
const media = new URLSearchParams(location.search).get('media') const media = new URLSearchParams(location.search).get('media');
connect(media || 'video+audio') connect(media || 'video+audio');
</script> </script>
</body> </body>
</html> </html>