UNPKG

@tomoxv/gstwebrtc-api

Version:

Javascript API to integrate GStreamer WebRTC streams (webrtcsrc/webrtcsink) in a web browser

458 lines (388 loc) 13 kB
<!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.host}/webrtc`, }; 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>