gst-plugins-rs/net/webrtc/gstwebrtc-api/index.html
Mathieu Duponchelle 9080c90120 net/webrtc: add support for answering to webrtcsink
Support was added to the base class when the AWS KVS signaller was
implemented, but the default signaller still only supported the case
where the producer was creating the offer.

Also extend the javascript API

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1702>
2024-08-09 14:02:48 +02:00

473 lines
14 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, div.offer-options {
position: relative;
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="offer-options">
<textarea rows="5" cols="50" placeholder="offer options, empty to answer. For example:\n{\n &quot;offerToReceiveAudio&quot;: 1\n &quot;offerToReceiveVideo&quot;: 1\n}\n"></textarea>
</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];
const textareaElement = entryElement.getElementsByTagName("textarea")[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 {
let session = null;
if (textareaElement.value == '') {
session = api.createConsumerSession(producerId);
} else {
try {
let offerOptions = JSON.parse(textareaElement.value);
console.log("Offer options: ", offerOptions);
session = api.createConsumerSessionWithOfferOptions(producerId, offerOptions);
} catch (ex) {
console.error("Failed to parse offer options:", ex);
return;
}
}
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>