gst-plugins-rs/net/webrtc/gstwebrtc-api/index.html
Mathieu Duponchelle 86039dd5c1 webrtc-api example: do not rely on webpack / npm proxying websocket
Instead simply use the desired address directly from the reference
example, this makes it work out of the box without placing expectations
on the web server.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1674>
2024-07-30 16:29:54 +00:00

457 lines
13 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GstWebRTC API</title>
<style>
body {
background-color: #3a3f44;
color: #c8c8c8;
}
section {
border-top: 2px solid #272b30;
}
main {
border-bottom: 2px solid #272b30;
padding-bottom: 1em;
}
.button {
cursor: pointer;
border-radius: 10px;
user-select: none;
}
.button:disabled {
cursor: default;
}
button.button {
box-shadow: 4px 4px 14px 1px #272b30;
border: none;
}
.spinner {
display: inline-block;
position: absolute;
width: 80px;
height: 80px;
}
.spinner>div {
box-sizing: border-box;
display: block;
position: absolute;
width: 64px;
height: 64px;
margin: 8px;
border: 8px solid #fff;
border-radius: 50%;
animation: spinner 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: #fff transparent transparent transparent;
}
.spinner div:nth-child(1) {
animation-delay: -0.45s;
}
.spinner div:nth-child(2) {
animation-delay: -0.3s;
}
.spinner div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
video:focus-visible,
video:focus {
outline: none;
}
div.video {
position: relative;
display: inline-block;
margin: 1em;
}
div.video>div.fullscreen {
position: absolute;
top: 0;
right: 0;
width: 2.6em;
height: 2.6em;
}
div.video>div.fullscreen>span {
position: absolute;
top: 0.3em;
right: 0.4em;
font-size: 1.5em;
font-weight: bolder;
cursor: pointer;
user-select: none;
display: none;
text-shadow: 1px 1px 4px #272b30;
}
div.video>video {
width: 320px;
height: 240px;
background-color: #202020;
border-radius: 15px;
box-shadow: 4px 4px 14px 1px #272b30;
}
div.video>.spinner {
top: 80px;
left: 120px;
}
#capture {
padding-top: 1.2em;
}
#capture>.button {
vertical-align: top;
margin-top: 1.5em;
margin-left: 1em;
background-color: #98d35e;
width: 5em;
height: 5em;
}
#capture>.client-id {
display: block;
}
#capture>.client-id::before {
content: "Client ID:";
margin-right: 0.5em;
}
#capture.has-session>.button {
background-color: #e36868;
}
#capture>.button::after {
content: "Start Capture";
}
#capture.has-session>.button::after {
content: "Stop Capture";
}
#capture .spinner {
display: none;
}
#capture.starting .spinner {
display: inline-block;
}
#remote-streams {
list-style: none;
padding-left: 1em;
}
#remote-streams>li .button::before {
content: "\2799";
padding-right: 0.2em;
}
#remote-streams>li.has-session .button::before {
content: "\2798";
}
#remote-streams>li div.video {
display: none;
}
#remote-streams>li.has-session div.video {
display: inline-block;
}
#remote-streams>li.streaming .spinner {
display: none;
}
#remote-streams>li.streaming>div.video>div.fullscreen:hover>span {
display: block;
}
#remote-streams .remote-control {
display: none;
position: absolute;
top: 0.2em;
left: 0.3em;
font-size: 1.8em;
font-weight: bolder;
animation: blink 1s ease-in-out infinite alternate;
text-shadow: 1px 1px 4px #272b30;
}
@keyframes blink {
to {
opacity: 0;
}
}
#remote-streams>li.streaming.has-remote-control .remote-control {
display: block;
}
#remote-streams>li.streaming.has-remote-control>div.video>video {
width: 640px;
height: 480px;
}
</style>
<script>
function initCapture(api) {
const captureSection = document.getElementById("capture");
const clientIdElement = captureSection.querySelector(".client-id");
const videoElement = captureSection.getElementsByTagName("video")[0];
const listener = {
connected: function(clientId) { clientIdElement.textContent = clientId; },
disconnected: function() { clientIdElement.textContent = "none"; }
};
api.registerConnectionListener(listener);
document.getElementById("capture-button").addEventListener("click", (event) => {
event.preventDefault();
if (captureSection._producerSession) {
captureSection._producerSession.close();
} else if (!captureSection.classList.contains("starting")) {
captureSection.classList.add("starting");
const constraints = {
video: { width: 1280, height: 720 }
};
navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
const session = api.createProducerSession(stream);
if (session) {
captureSection._producerSession = session;
session.addEventListener("error", (event) => {
if (captureSection._producerSession === session) {
console.error(event.message, event.error);
}
});
session.addEventListener("closed", () => {
if (captureSection._producerSession === session) {
videoElement.pause();
videoElement.srcObject = null;
captureSection.classList.remove("has-session", "starting");
delete captureSection._producerSession;
}
});
session.addEventListener("stateChanged", (event) => {
if ((captureSection._producerSession === session) &&
(event.target.state === GstWebRTCAPI.SessionState.streaming)) {
videoElement.srcObject = stream;
videoElement.play().catch(() => {});
captureSection.classList.remove("starting");
}
});
session.addEventListener("clientConsumerAdded", (event) => {
if (captureSection._producerSession === session) {
console.info(`client consumer added: ${event.detail.peerId}`);
}
});
session.addEventListener("clientConsumerRemoved", (event) => {
if (captureSection._producerSession === session) {
console.info(`client consumer removed: ${event.detail.peerId}`);
}
});
captureSection.classList.add("has-session");
session.start();
} else {
for (const track of stream.getTracks()) {
track.stop();
}
captureSection.classList.remove("starting");
}
}).catch((error) => {
console.error("cannot have access to webcam and microphone", error);
captureSection.classList.remove("starting");
});
}
});
}
function initRemoteStreams(api) {
const remoteStreamsElement = document.getElementById("remote-streams");
const listener = {
producerAdded: function(producer) {
const producerId = producer.id
if (!document.getElementById(producerId)) {
remoteStreamsElement.insertAdjacentHTML("beforeend",
`<li id="${producerId}">
<div class="button">${producer.meta.name || producerId}</div>
<div class="video">
<div class="spinner">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<span class="remote-control">&#xA9;</span>
<video></video>
<div class="fullscreen"><span title="Toggle fullscreen">&#x25A2;</span></div>
</div>
</li>`);
const entryElement = document.getElementById(producerId);
const videoElement = entryElement.getElementsByTagName("video")[0];
videoElement.addEventListener("playing", () => {
if (entryElement.classList.contains("has-session")) {
entryElement.classList.add("streaming");
}
});
entryElement.addEventListener("click", (event) => {
event.preventDefault();
if (!event.target.classList.contains("button")) {
return;
}
if (entryElement._consumerSession) {
entryElement._consumerSession.close();
} else {
const session = api.createConsumerSession(producerId);
if (session) {
entryElement._consumerSession = session;
session.addEventListener("error", (event) => {
if (entryElement._consumerSession === session) {
console.error(event.message, event.error);
}
});
session.addEventListener("closed", () => {
if (entryElement._consumerSession === session) {
videoElement.pause();
videoElement.srcObject = null;
entryElement.classList.remove("has-session", "streaming", "has-remote-control");
delete entryElement._consumerSession;
}
});
session.addEventListener("streamsChanged", () => {
if (entryElement._consumerSession === session) {
const streams = session.streams;
if (streams.length > 0) {
videoElement.srcObject = streams[0];
videoElement.play().catch(() => {});
}
}
});
session.addEventListener("remoteControllerChanged", () => {
if (entryElement._consumerSession === session) {
const remoteController = session.remoteController;
if (remoteController) {
entryElement.classList.add("has-remote-control");
remoteController.attachVideoElement(videoElement);
} else {
entryElement.classList.remove("has-remote-control");
}
}
});
entryElement.classList.add("has-session");
session.connect();
}
}
});
}
},
producerRemoved: function(producer) {
const element = document.getElementById(producer.id);
if (element) {
if (element._consumerSession) {
element._consumerSession.close();
}
element.remove();
}
}
};
api.registerProducersListener(listener);
for (const producer of api.getAvailableProducers()) {
listener.producerAdded(producer);
}
}
window.addEventListener("DOMContentLoaded", () => {
document.addEventListener("click", (event) => {
if (event.target.matches("div.video>div.fullscreen:hover>span")) {
event.preventDefault();
event.target.parentNode.previousElementSibling.requestFullscreen();
}
});
const signalingProtocol = window.location.protocol.startsWith("https") ? "wss" : "ws";
const gstWebRTCConfig = {
meta: { name: `WebClient-${Date.now()}` },
signalingServerUrl: `${signalingProtocol}://${window.location.hostname}:8443`,
};
const api = new GstWebRTCAPI(gstWebRTCConfig);
initCapture(api);
initRemoteStreams(api);
});
</script>
</head>
<body>
<header>
<h1>GstWebRTC API</h1>
</header>
<main>
<section id="capture">
<span class="client-id">none</span>
<button class="button" id="capture-button"></button>
<div class="video">
<div class="spinner">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<video></video>
</div>
</section>
<section>
<h1>Remote Streams</h1>
<ul id="remote-streams"></ul>
</section>
</main>
</body>
</html>