mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-09-27 04:36:12 +08:00
Add ESLinter and fix JS lint problems
This commit is contained in:
40
package.json
Normal file
40
package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@@ -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**
|
||||||
|
|
||||||
|
148
www/add.html
148
www/add.html
@@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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>
|
@@ -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>
|
||||||
|
@@ -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}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
193
www/video-rtc.js
193
www/video-rtc.js
@@ -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]);
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
|
@@ -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>
|
@@ -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>
|
Reference in New Issue
Block a user