Spaces:
Sleeping
Sleeping
| <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, | |
| div.request-box { | |
| position: relative; | |
| margin: 1em; | |
| } | |
| /* | |
| div.request-box { | |
| display: none; | |
| } | |
| #remote-streams>li.streaming.has-remote-control .request-box { | |
| display: block; | |
| }*/ | |
| 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 "offerToReceiveAudio": 1\n "offerToReceiveVideo": 1\n}\n"></textarea> | |
| </div> | |
| <div class="request-box"> | |
| <textarea rows="4" cols="50" placeholder="JSON request to send over"></textarea> | |
| <button disabled="enable">Submit request</button> | |
| </div> | |
| <div class="video"> | |
| <div class="spinner"> | |
| <div></div> | |
| <div></div> | |
| <div></div> | |
| <div></div> | |
| </div> | |
| <span class="remote-control">©</span> | |
| <video></video> | |
| <div class="fullscreen"><span title="Toggle fullscreen">▢</span></div> | |
| </div> | |
| <div class="video"> | |
| <div class="spinner"> | |
| <div></div> | |
| <div></div> | |
| <div></div> | |
| <div></div> | |
| </div> | |
| <span class="remote-control">©</span> | |
| <video></video> | |
| <div class="fullscreen"><span title="Toggle fullscreen">▢</span></div> | |
| </div> | |
| </li>`); | |
| const entryElement = document.getElementById(producerId); | |
| const videoElement = entryElement.getElementsByTagName("video")[0]; | |
| const videoRightElement = entryElement.getElementsByTagName("video")[1]; | |
| const offerTextareaElement = entryElement.getElementsByTagName("textarea")[0]; | |
| const requestTextAreaElement = entryElement.getElementsByTagName("textarea")[1]; | |
| const submitRequestButtonElement = entryElement.getElementsByTagName("button")[0]; | |
| submitRequestButtonElement.addEventListener("click", (event) => { | |
| try { | |
| let request = requestTextAreaElement.value; | |
| let id = entryElement._consumerSession.remoteController.sendControlRequest(request); | |
| } catch (ex) { | |
| console.error("Failed to parse mix matrix:", ex); | |
| return; | |
| } | |
| }); | |
| videoElement.addEventListener("playing", () => { | |
| if (entryElement.classList.contains("has-session")) { | |
| entryElement.classList.add("streaming"); | |
| } | |
| }); | |
| videoRightElement.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 (offerTextareaElement.value == '') { | |
| session = api.createConsumerSession(producerId); | |
| } else { | |
| try { | |
| let offerOptions = JSON.parse(offerTextareaElement.value); | |
| session = api.createConsumerSessionWithOfferOptions(producerId, offerOptions); | |
| } catch (ex) { | |
| console.error("Failed to parse offer options:", ex); | |
| return; | |
| } | |
| } | |
| if (session) { | |
| entryElement._consumerSession = session; | |
| session.mungeStereoHack = true; | |
| 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; | |
| videoRightElement.pause(); | |
| videoRightElement.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(() => { }); | |
| const originalStream = streams[0]; | |
| let desiredVideoTrack = null; | |
| let nb_video_tracks = 0; | |
| // Iterate through the tracks to find the desired video track | |
| originalStream.getTracks().forEach((track) => { | |
| if (track.kind === 'video') { | |
| // You can add more conditions here to select the specific track you want | |
| // For example, check track.label or track.id | |
| desiredVideoTrack = track; | |
| nb_video_tracks++; | |
| } | |
| }); | |
| if (desiredVideoTrack && nb_video_tracks > 1) { | |
| // Create a new MediaStream with only the desired video track | |
| const newStream = new MediaStream([desiredVideoTrack]); | |
| // Assign the new stream to the video element | |
| videoRightElement.srcObject = newStream; | |
| videoRightElement.play().catch(() => { }); | |
| } else { | |
| console.warn("No video track found in the stream."); | |
| } | |
| } | |
| } | |
| }); | |
| session.addEventListener("remoteControllerChanged", () => { | |
| if (entryElement._consumerSession === session) { | |
| const remoteController = session.remoteController; | |
| if (remoteController) { | |
| entryElement.classList.add("has-remote-control"); | |
| submitRequestButtonElement.disabled = false; | |
| remoteController.attachVideoElement(videoElement); | |
| remoteController.addEventListener("info", (e) => { | |
| console.log("Received info message from producer: ", e.detail); | |
| }); | |
| } else { | |
| entryElement.classList.remove("has-remote-control"); | |
| submitRequestButtonElement.disabled = true; | |
| } | |
| } | |
| }); | |
| 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> | |
| <script src="gstwebrtc-api-2.0.0.min.js"></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> |