ACTUALLY WORKING REACT - #18
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
✅ Deploy Preview for plexstream ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (3)
eslint.config.js (1)
16-23: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winRemove the conflicting ECMAScript version declarations.
languageOptions.ecmaVersionis2020, while nestedparserOptions.ecmaVersionislatest. Keep one intentional target so parsing and lint rules use the same syntax baseline.src/index.css (1)
6-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winResolve the reported Stylelint violations.
Stylelint flags the empty lines before declarations at Lines 6 and 10 and the casing of
optimizeLegibilityat Line 11. Fix these or verify that the external Stylelint check is not intended to gate this project.Source: Linters/SAST tools
old-stuff/styles/style.css (1)
5-23: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDrop quotes around
"Roboto"per stylelint, and consider deduplicatingh2/h3.Stylelint's
font-family-name-quotesflags the quoted font name on lines 6 and 16;h2/h3are otherwise identical and could share a selector.🎨 Suggested fix
-h2 { - font-family: "Roboto", sans-serif; +h2, h3 { + font-family: Roboto, sans-serif; font-optical-sizing: auto; font-weight: 300; font-style: normal; font-variation-settings: "wdth" 100; color: white; } - -h3 { - font-family: "Roboto", sans-serif; - font-optical-sizing: auto; - font-weight: 300; - font-style: normal; - font-variation-settings: - "wdth" 100; - color: white; -}Source: Linters/SAST tools
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 76242e88-874b-49cb-ab89-deeac115490f
⛔ Files ignored due to path filters (4)
old-stuff/favicon.icois excluded by!**/*.icopackage-lock.jsonis excluded by!**/package-lock.jsonpublic/vite.svgis excluded by!**/*.svgsrc/assets/react.svgis excluded by!**/*.svg
📒 Files selected for processing (16)
.gitignoreREADME.mdeslint.config.jsindex.htmlold-stuff/SECURITY.mdold-stuff/index.htmlold-stuff/server.jsold-stuff/signup.htmlold-stuff/styles/style.cssold-stuff/templates/multistream.htmlpackage.jsonsrc/App.csssrc/App.jsxsrc/index.csssrc/main.jsxvite.config.js
| files: ['**/*.{js,jsx}'], | ||
| extends: [ | ||
| js.configs.recommended, | ||
| reactHooks.configs.flat.recommended, | ||
| reactRefresh.configs.vite, | ||
| ], | ||
| languageOptions: { | ||
| ecmaVersion: 2020, | ||
| globals: globals.browser, | ||
| parserOptions: { | ||
| ecmaVersion: 'latest', | ||
| ecmaFeatures: { jsx: true }, | ||
| sourceType: 'module', | ||
| }, | ||
| }, |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Give the archived Node server its own ESLint environment.
npm run lint executes eslint ., but this configuration matches all *.js/*.jsx files and provides only browser globals. The archived old-stuff/server.js is a Node/WebSocket server, so Node globals can be reported as undefined and make lint fail. Scope this config to React sources and add a Node override for old-stuff/**/*.js, or explicitly ignore the archive.
Proposed configuration shape
- files: ['**/*.{js,jsx}'],
+ files: ['src/**/*.{js,jsx}'],
...
+ {
+ files: ['old-stuff/**/*.js'],
+ languageOptions: {
+ globals: globals.node,
+ },
+ },📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| files: ['**/*.{js,jsx}'], | |
| extends: [ | |
| js.configs.recommended, | |
| reactHooks.configs.flat.recommended, | |
| reactRefresh.configs.vite, | |
| ], | |
| languageOptions: { | |
| ecmaVersion: 2020, | |
| globals: globals.browser, | |
| parserOptions: { | |
| ecmaVersion: 'latest', | |
| ecmaFeatures: { jsx: true }, | |
| sourceType: 'module', | |
| }, | |
| }, | |
| files: ['src/**/*.{js,jsx}'], | |
| extends: [ | |
| js.configs.recommended, | |
| reactHooks.configs.flat.recommended, | |
| reactRefresh.configs.vite, | |
| ], | |
| languageOptions: { | |
| ecmaVersion: 2020, | |
| globals: globals.browser, | |
| parserOptions: { | |
| ecmaVersion: 'latest', | |
| ecmaFeatures: { jsx: true }, | |
| sourceType: 'module', | |
| }, | |
| }, | |
| }, | |
| { | |
| files: ['old-stuff/**/*.js'], | |
| languageOptions: { | |
| globals: globals.node, | |
| }, | |
| }, |
| <script> | ||
| /* kept behavior intact and fixed start/stop/search without changing UI layout */ | ||
|
|
||
| let currentUser = null; | ||
| const userColors = {}; | ||
| const config = { iceServers:[{ urls:'stun:stun.l.google.com:19302' }] }; | ||
| let ws = new WebSocket('ws://localhost:8080'); | ||
| let peerConnection = null; | ||
| let broadcasterPC = null; | ||
| let localStream = null; | ||
|
|
||
| /* small util */ | ||
| function randomColor(){ | ||
| const colors=["#1e90ff","#ff4c4c","#4cff4c","#ffd93d","#a259ff","#ff7f50","#00ced1"]; | ||
| return colors[Math.floor(Math.random()*colors.length)]; | ||
| } | ||
|
|
||
| /* NAV */ | ||
| function showSection(id){ | ||
| document.querySelectorAll('main section').forEach(s=>s.classList.add('hidden')); | ||
| document.getElementById(id).classList.remove('hidden'); | ||
| } | ||
|
|
||
| /* LOGIN */ | ||
| function showLogin(){ document.getElementById('login-modal').classList.remove('hidden'); } | ||
| function hideLogin(){ document.getElementById('login-modal').classList.add('hidden'); } | ||
|
|
||
| function login(){ | ||
| const u = document.getElementById('login-user').value.trim(); | ||
| const p = document.getElementById('login-pass').value.trim(); | ||
| if(!u || !p) return alert('fill both fields'); | ||
| currentUser = u; | ||
| if(!userColors[u]) userColors[u] = randomColor(); | ||
| hideLogin(); | ||
| document.getElementById('user-login').innerHTML = `<span>${u}</span> <button onclick="logout()">Logout</button>`; | ||
| alert('Logged in as '+u); | ||
| } | ||
| function signup(){ login(); } | ||
| function logout(){ | ||
| currentUser = null; | ||
| document.getElementById('user-login').innerHTML = `<button onclick="showLogin()">Login / Signup</button>`; | ||
| alert('Logged out'); | ||
| } | ||
|
|
||
| /* Go Live button logic */ | ||
| document.getElementById('go-live-btn').addEventListener('click', ()=>{ | ||
| if(!currentUser) return alert('Login first'); | ||
| showSection('dashboard'); | ||
| }); | ||
|
|
||
| /* CHAT rendering */ | ||
| const chatContainer = document.getElementById('chat-container'); | ||
| const messagesDiv = document.getElementById('messages'); | ||
|
|
||
| function renderChatLine(user, text){ | ||
| const p = document.createElement('p'); | ||
| p.className = 'chat-msg'; | ||
| const strong = document.createElement('strong'); | ||
| strong.textContent = user; | ||
| strong.style.color = userColors[user] || '#fff'; | ||
| const span = document.createElement('span'); | ||
| span.textContent = ': ' + text; | ||
| p.appendChild(strong); | ||
| p.appendChild(span); | ||
| // append to both viewer chat and dashboard messages if present | ||
| if(chatContainer) chatContainer.appendChild(p.cloneNode(true)); | ||
| if(messagesDiv) messagesDiv.appendChild(p.cloneNode(true)); | ||
| if(chatContainer) chatContainer.scrollTop = chatContainer.scrollHeight; | ||
| if(messagesDiv) messagesDiv.scrollTop = messagesDiv.scrollHeight; | ||
| } | ||
|
|
||
| /* sending chat */ | ||
| document.getElementById('chat-send-btn').addEventListener('click', sendMessage); | ||
| function sendMessage(){ | ||
| const input = document.getElementById('chat-input'); | ||
| const msg = input.value.trim(); | ||
| if(!currentUser) return alert('Login first'); | ||
| if(!msg) return; | ||
| ws.send(JSON.stringify({ type:'chat', chat:msg, user:currentUser })); | ||
| renderChatLine(currentUser, msg); | ||
| input.value = ''; | ||
| } | ||
| document.getElementById('chat-input').addEventListener('keypress', e=>{ if(e.key==='Enter') sendMessage(); }); | ||
|
|
||
| function sendMessageDashboard(){ | ||
| const input = document.getElementById('chatInputDashboard'); | ||
| const msg = input.value.trim(); | ||
| if(!currentUser) return alert('Login first'); | ||
| if(!msg) return; | ||
| ws.send(JSON.stringify({ type:'chat', chat:msg, user:currentUser })); | ||
| renderChatLine(currentUser, msg); | ||
| input.value = ''; | ||
| } | ||
| document.getElementById('chatInputDashboard').addEventListener('keypress', e=>{ if(e.key==='Enter') sendMessageDashboard(); }); | ||
|
|
||
| /* START / STOP streaming logic fixed and intact */ | ||
| const startBtn = document.getElementById('start-stream-btn'); | ||
| const stopBtn = document.getElementById('stop-stream-btn'); | ||
| const broadcasterVideo = document.getElementById('broadcasterVideo'); | ||
|
|
||
| startBtn.addEventListener('click', async ()=>{ | ||
| if(!currentUser) return alert('Login first'); | ||
| startBtn.classList.add('hidden'); | ||
| stopBtn.classList.remove('hidden'); | ||
|
|
||
| // get camera | ||
| try{ | ||
| localStream = await navigator.mediaDevices.getUserMedia({ video:true, audio:true }); | ||
| broadcasterVideo.srcObject = localStream; | ||
|
|
||
| // create PeerConnection for broadcaster (it will send offer to others via ws) | ||
| broadcasterPC = new RTCPeerConnection(config); | ||
| localStream.getTracks().forEach(t=>broadcasterPC.addTrack(t, localStream)); | ||
|
|
||
| broadcasterPC.onicecandidate = ({candidate})=>{ | ||
| if(candidate) ws.send(JSON.stringify({ type:'iceCandidate', iceCandidate:candidate, forStreamOwner: true })); | ||
| }; | ||
|
|
||
| const offer = await broadcasterPC.createOffer(); | ||
| await broadcasterPC.setLocalDescription(offer); | ||
|
|
||
| // tell server about offer and new stream | ||
| const title = document.getElementById('stream-title').value || 'Untitled'; | ||
| const category = document.getElementById('stream-category').value || 'chat'; | ||
| ws.send(JSON.stringify({ type:'offer', offer })); | ||
| ws.send(JSON.stringify({ type:'newStream', title, category, user: currentUser })); | ||
|
|
||
| // optional show stream key placeholder unchanged | ||
| }catch(err){ | ||
| console.error('start stream failed', err); | ||
| alert('could not start stream check camera permissions'); | ||
| startBtn.classList.remove('hidden'); | ||
| stopBtn.classList.add('hidden'); | ||
| } | ||
| }); | ||
|
|
||
| stopBtn.addEventListener('click', ()=>{ | ||
| stopBtn.classList.add('hidden'); | ||
| startBtn.classList.remove('hidden'); | ||
| // notify server | ||
| ws.send(JSON.stringify({ type:'stopStream' })); | ||
|
|
||
| // close pc and tracks | ||
| try{ | ||
| if(broadcasterPC){ | ||
| broadcasterPC.getSenders().forEach(s=>broadcasterPC.removeTrack(s)); | ||
| broadcasterPC.close(); | ||
| broadcasterPC = null; | ||
| } | ||
| if(localStream){ | ||
| localStream.getTracks().forEach(t=>t.stop()); | ||
| broadcasterVideo.srcObject = null; | ||
| localStream = null; | ||
| } | ||
| }catch(e){ console.warn(e); } | ||
| }); | ||
|
|
||
| /* COPY KEY */ | ||
| function copyKey(){ navigator.clipboard.writeText(document.getElementById('stream-key').innerText); alert('Copied!'); } | ||
|
|
||
| /* SEARCH */ | ||
| function searchStreams(){ | ||
| const q = document.getElementById('search-input').value.trim(); | ||
| ws.send(JSON.stringify({ type:'search', query: q })); | ||
| } | ||
| function renderResults(list){ | ||
| const container = document.getElementById('search-results'); | ||
| container.innerHTML = ''; | ||
| if(!list || list.length === 0){ | ||
| // keep same UI feel just show empty | ||
| return; | ||
| } | ||
| list.forEach(s=>{ | ||
| const card = document.createElement('div'); | ||
| card.className = 'card'; | ||
| // show only title as requested and keep UI unchanged | ||
| card.textContent = s.title + (s.live ? ' (LIVE)' : ''); | ||
| card.addEventListener('click', ()=> { | ||
| // show viewer section and send request to watch that stream | ||
| document.getElementById('viewer-title').textContent = s.title; | ||
| document.getElementById('stream-id').textContent = s.id || ''; | ||
| showSection('viewer'); | ||
|
|
||
| // prepare to receive offer from owner if server will forward it | ||
| // create peerConnection to receive remote track | ||
| if(peerConnection){ | ||
| try{ peerConnection.close(); } catch(e){} | ||
| peerConnection = null; | ||
| } | ||
| peerConnection = new RTCPeerConnection(config); | ||
| peerConnection.ontrack = (e) => { | ||
| document.getElementById('stream-player').srcObject = e.streams[0]; | ||
| }; | ||
| peerConnection.onicecandidate = ({candidate})=>{ | ||
| if(candidate) ws.send(JSON.stringify({ type:'iceCandidate', iceCandidate:candidate, watchingStreamId: s.id })); | ||
| }; | ||
|
|
||
| // tell server we want to watch this stream will trigger server to forward offer or instruct owner | ||
| ws.send(JSON.stringify({ type:'watchStream', id: s.id })); | ||
| }); | ||
| container.appendChild(card); | ||
| }); | ||
| } | ||
|
|
||
| /* WEBSOCKET message handling */ | ||
| ws.addEventListener('open', ()=>console.log('ws open')); | ||
| ws.addEventListener('error', (e)=>console.error('ws err', e)); | ||
|
|
||
| ws.addEventListener('message', async ev=>{ | ||
| try{ | ||
| const msg = JSON.parse(ev.data); | ||
|
|
||
| // chat | ||
| if(msg.type === 'chat'){ | ||
| // server broadcasts chat as {type:'chat', user, chat} | ||
| renderChatLine(msg.user, msg.chat); | ||
| } | ||
|
|
||
| // streams update broadcast maybe used to autofill featured or results | ||
| else if(msg.type === 'streamsUpdate'){ | ||
| // server sends full streams array | ||
| // we keep UI intact so only render to search results area when a query was made by server | ||
| // but to keep user convenience update featured streams quickly minimal update | ||
| // we do not alter layout just update featured list small | ||
| const featured = document.getElementById('featured-streams'); | ||
| featured.innerHTML = ''; | ||
| msg.streams.slice(0,6).forEach(s=>{ | ||
| const c = document.createElement('div'); | ||
| c.className = 'card'; | ||
| c.textContent = s.title; | ||
| featured.appendChild(c); | ||
| }); | ||
| } | ||
|
|
||
| // search results | ||
| else if(msg.type === 'searchResults'){ | ||
| renderResults(msg.results || []); | ||
| } | ||
|
|
||
| // WebRTC offer arrives from stream owner forwarded by server | ||
| else if(msg.type === 'offer' || msg.offer){ | ||
| const offerMsg = msg.offer || msg; | ||
| // viewer side answer flow | ||
| if(peerConnection === null){ | ||
| peerConnection = new RTCPeerConnection(config); | ||
| peerConnection.ontrack = (e)=>document.getElementById('stream-player').srcObject = e.streams[0]; | ||
| peerConnection.onicecandidate = ({candidate})=>{ | ||
| if(candidate) ws.send(JSON.stringify({ type:'iceCandidate', iceCandidate:candidate })); | ||
| }; | ||
| } | ||
| await peerConnection.setRemoteDescription(new RTCSessionDescription(offerMsg.offer || offerMsg)); | ||
| const answer = await peerConnection.createAnswer(); | ||
| await peerConnection.setLocalDescription(answer); | ||
| ws.send(JSON.stringify({ type:'answer', answer })); | ||
| } | ||
|
|
||
| // answer for broadcaster | ||
| else if(msg.type === 'answer' || msg.answer){ | ||
| const answerMsg = msg.answer || msg; | ||
| if(broadcasterPC){ | ||
| await broadcasterPC.setRemoteDescription(new RTCSessionDescription(answerMsg.answer || answerMsg)); | ||
| } else if(peerConnection){ | ||
| await peerConnection.setRemoteDescription(new RTCSessionDescription(answerMsg.answer || answerMsg)); | ||
| } | ||
| } | ||
|
|
||
| // ice candidates | ||
| else if(msg.type === 'iceCandidate' || msg.iceCandidate){ | ||
| const cand = msg.iceCandidate || msg; | ||
| try{ | ||
| if(peerConnection) await peerConnection.addIceCandidate(new RTCIceCandidate(cand)); | ||
| if(broadcasterPC) await broadcasterPC.addIceCandidate(new RTCIceCandidate(cand)); | ||
| }catch(e){ console.warn('ice add err', e); } | ||
| } | ||
|
|
||
| }catch(e){ | ||
| console.error('ws msg parse err', e); | ||
| } | ||
| }); | ||
|
|
||
| /* render chat lines when user sends or incoming handled above */ | ||
| function renderChatLine(user, text){ | ||
| const p = document.createElement('p'); | ||
| p.textContent = user + ': ' + text; | ||
| // append to both viewer and dashboard | ||
| const viewerChat = document.getElementById('chat-container'); | ||
| const dashChat = document.getElementById('messages'); | ||
| if(viewerChat) viewerChat.appendChild(p.cloneNode(true)); | ||
| if(dashChat) dashChat.appendChild(p.cloneNode(true)); | ||
| } | ||
|
|
||
| /* keep old dummy featured example so UI not empty */ | ||
| (function seedDummy(){ | ||
| const f = document.getElementById('featured-streams'); | ||
| if(f) { | ||
| if(f.children.length === 0){ | ||
| const a = document.createElement('div'); a.className='card'; a.textContent='Stream 1'; f.appendChild(a); | ||
| const b = document.createElement('div'); b.className='card'; b.textContent='Stream 2'; f.appendChild(b); | ||
| const c = document.createElement('div'); c.className='card'; c.textContent='Stream 3'; f.appendChild(c); | ||
| } | ||
| } | ||
| })(); | ||
|
|
||
| <script type="module"> | ||
| // Import the functions you need from the SDKs you need | ||
| import { initializeApp } from "https://www.gstatic.com/firebasejs/12.5.0/firebase-app.js"; | ||
| // TODO: Add SDKs for Firebase products that you want to use | ||
| // https://firebase.google.com/docs/web/setup#available-libraries | ||
| // Your web app's Firebase configuration | ||
| const firebaseConfig = { | ||
| apiKey: "AIzaSyAyiiWVeRjxSsxUE8fR1DyCPEZwt7o3iHc", | ||
| authDomain: "plexstream-ef9b4.firebaseapp.com", | ||
| projectId: "plexstream-ef9b4", | ||
| storageBucket: "plexstream-ef9b4.firebasestorage.app", | ||
| messagingSenderId: "130967153299", | ||
| appId: "1:130967153299:web:e424baeb43541f1401bb66" | ||
| }; | ||
| // Initialize Firebase | ||
| const app = initializeApp(firebaseConfig); | ||
| </script> | ||
| </script> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
Malformed nested <script> tags likely break all page JavaScript.
The <script> opened at line 139 is never closed before the second <script type="module"> opens at line 442. Per HTML parsing rules, the tokenizer only ends script content on the literal </script> sequence, so everything from line 140 through line 458 (including the literal text <script type="module"> and the import statement) becomes the text content of the first script. A bare <script ...> string and a top-level import are both syntax errors in a non-module script, so this will throw at parse time and prevent login, chat, streaming, and search from ever running. The stray </script> on line 459 (flagged by HTMLHint as unpaired) confirms the tag nesting is broken.
🐛 Suggested fix
}
})();
+</script>
<script type="module">
// Import the functions you need from the SDKs you need
import { initializeApp } from "https://www.gstatic.com/firebasejs/12.5.0/firebase-app.js";
...
const app = initializeApp(firebaseConfig);
</script>
-</script>
</body>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <script> | |
| /* kept behavior intact and fixed start/stop/search without changing UI layout */ | |
| let currentUser = null; | |
| const userColors = {}; | |
| const config = { iceServers:[{ urls:'stun:stun.l.google.com:19302' }] }; | |
| let ws = new WebSocket('ws://localhost:8080'); | |
| let peerConnection = null; | |
| let broadcasterPC = null; | |
| let localStream = null; | |
| /* small util */ | |
| function randomColor(){ | |
| const colors=["#1e90ff","#ff4c4c","#4cff4c","#ffd93d","#a259ff","#ff7f50","#00ced1"]; | |
| return colors[Math.floor(Math.random()*colors.length)]; | |
| } | |
| /* NAV */ | |
| function showSection(id){ | |
| document.querySelectorAll('main section').forEach(s=>s.classList.add('hidden')); | |
| document.getElementById(id).classList.remove('hidden'); | |
| } | |
| /* LOGIN */ | |
| function showLogin(){ document.getElementById('login-modal').classList.remove('hidden'); } | |
| function hideLogin(){ document.getElementById('login-modal').classList.add('hidden'); } | |
| function login(){ | |
| const u = document.getElementById('login-user').value.trim(); | |
| const p = document.getElementById('login-pass').value.trim(); | |
| if(!u || !p) return alert('fill both fields'); | |
| currentUser = u; | |
| if(!userColors[u]) userColors[u] = randomColor(); | |
| hideLogin(); | |
| document.getElementById('user-login').innerHTML = `<span>${u}</span> <button onclick="logout()">Logout</button>`; | |
| alert('Logged in as '+u); | |
| } | |
| function signup(){ login(); } | |
| function logout(){ | |
| currentUser = null; | |
| document.getElementById('user-login').innerHTML = `<button onclick="showLogin()">Login / Signup</button>`; | |
| alert('Logged out'); | |
| } | |
| /* Go Live button logic */ | |
| document.getElementById('go-live-btn').addEventListener('click', ()=>{ | |
| if(!currentUser) return alert('Login first'); | |
| showSection('dashboard'); | |
| }); | |
| /* CHAT rendering */ | |
| const chatContainer = document.getElementById('chat-container'); | |
| const messagesDiv = document.getElementById('messages'); | |
| function renderChatLine(user, text){ | |
| const p = document.createElement('p'); | |
| p.className = 'chat-msg'; | |
| const strong = document.createElement('strong'); | |
| strong.textContent = user; | |
| strong.style.color = userColors[user] || '#fff'; | |
| const span = document.createElement('span'); | |
| span.textContent = ': ' + text; | |
| p.appendChild(strong); | |
| p.appendChild(span); | |
| // append to both viewer chat and dashboard messages if present | |
| if(chatContainer) chatContainer.appendChild(p.cloneNode(true)); | |
| if(messagesDiv) messagesDiv.appendChild(p.cloneNode(true)); | |
| if(chatContainer) chatContainer.scrollTop = chatContainer.scrollHeight; | |
| if(messagesDiv) messagesDiv.scrollTop = messagesDiv.scrollHeight; | |
| } | |
| /* sending chat */ | |
| document.getElementById('chat-send-btn').addEventListener('click', sendMessage); | |
| function sendMessage(){ | |
| const input = document.getElementById('chat-input'); | |
| const msg = input.value.trim(); | |
| if(!currentUser) return alert('Login first'); | |
| if(!msg) return; | |
| ws.send(JSON.stringify({ type:'chat', chat:msg, user:currentUser })); | |
| renderChatLine(currentUser, msg); | |
| input.value = ''; | |
| } | |
| document.getElementById('chat-input').addEventListener('keypress', e=>{ if(e.key==='Enter') sendMessage(); }); | |
| function sendMessageDashboard(){ | |
| const input = document.getElementById('chatInputDashboard'); | |
| const msg = input.value.trim(); | |
| if(!currentUser) return alert('Login first'); | |
| if(!msg) return; | |
| ws.send(JSON.stringify({ type:'chat', chat:msg, user:currentUser })); | |
| renderChatLine(currentUser, msg); | |
| input.value = ''; | |
| } | |
| document.getElementById('chatInputDashboard').addEventListener('keypress', e=>{ if(e.key==='Enter') sendMessageDashboard(); }); | |
| /* START / STOP streaming logic fixed and intact */ | |
| const startBtn = document.getElementById('start-stream-btn'); | |
| const stopBtn = document.getElementById('stop-stream-btn'); | |
| const broadcasterVideo = document.getElementById('broadcasterVideo'); | |
| startBtn.addEventListener('click', async ()=>{ | |
| if(!currentUser) return alert('Login first'); | |
| startBtn.classList.add('hidden'); | |
| stopBtn.classList.remove('hidden'); | |
| // get camera | |
| try{ | |
| localStream = await navigator.mediaDevices.getUserMedia({ video:true, audio:true }); | |
| broadcasterVideo.srcObject = localStream; | |
| // create PeerConnection for broadcaster (it will send offer to others via ws) | |
| broadcasterPC = new RTCPeerConnection(config); | |
| localStream.getTracks().forEach(t=>broadcasterPC.addTrack(t, localStream)); | |
| broadcasterPC.onicecandidate = ({candidate})=>{ | |
| if(candidate) ws.send(JSON.stringify({ type:'iceCandidate', iceCandidate:candidate, forStreamOwner: true })); | |
| }; | |
| const offer = await broadcasterPC.createOffer(); | |
| await broadcasterPC.setLocalDescription(offer); | |
| // tell server about offer and new stream | |
| const title = document.getElementById('stream-title').value || 'Untitled'; | |
| const category = document.getElementById('stream-category').value || 'chat'; | |
| ws.send(JSON.stringify({ type:'offer', offer })); | |
| ws.send(JSON.stringify({ type:'newStream', title, category, user: currentUser })); | |
| // optional show stream key placeholder unchanged | |
| }catch(err){ | |
| console.error('start stream failed', err); | |
| alert('could not start stream check camera permissions'); | |
| startBtn.classList.remove('hidden'); | |
| stopBtn.classList.add('hidden'); | |
| } | |
| }); | |
| stopBtn.addEventListener('click', ()=>{ | |
| stopBtn.classList.add('hidden'); | |
| startBtn.classList.remove('hidden'); | |
| // notify server | |
| ws.send(JSON.stringify({ type:'stopStream' })); | |
| // close pc and tracks | |
| try{ | |
| if(broadcasterPC){ | |
| broadcasterPC.getSenders().forEach(s=>broadcasterPC.removeTrack(s)); | |
| broadcasterPC.close(); | |
| broadcasterPC = null; | |
| } | |
| if(localStream){ | |
| localStream.getTracks().forEach(t=>t.stop()); | |
| broadcasterVideo.srcObject = null; | |
| localStream = null; | |
| } | |
| }catch(e){ console.warn(e); } | |
| }); | |
| /* COPY KEY */ | |
| function copyKey(){ navigator.clipboard.writeText(document.getElementById('stream-key').innerText); alert('Copied!'); } | |
| /* SEARCH */ | |
| function searchStreams(){ | |
| const q = document.getElementById('search-input').value.trim(); | |
| ws.send(JSON.stringify({ type:'search', query: q })); | |
| } | |
| function renderResults(list){ | |
| const container = document.getElementById('search-results'); | |
| container.innerHTML = ''; | |
| if(!list || list.length === 0){ | |
| // keep same UI feel just show empty | |
| return; | |
| } | |
| list.forEach(s=>{ | |
| const card = document.createElement('div'); | |
| card.className = 'card'; | |
| // show only title as requested and keep UI unchanged | |
| card.textContent = s.title + (s.live ? ' (LIVE)' : ''); | |
| card.addEventListener('click', ()=> { | |
| // show viewer section and send request to watch that stream | |
| document.getElementById('viewer-title').textContent = s.title; | |
| document.getElementById('stream-id').textContent = s.id || ''; | |
| showSection('viewer'); | |
| // prepare to receive offer from owner if server will forward it | |
| // create peerConnection to receive remote track | |
| if(peerConnection){ | |
| try{ peerConnection.close(); } catch(e){} | |
| peerConnection = null; | |
| } | |
| peerConnection = new RTCPeerConnection(config); | |
| peerConnection.ontrack = (e) => { | |
| document.getElementById('stream-player').srcObject = e.streams[0]; | |
| }; | |
| peerConnection.onicecandidate = ({candidate})=>{ | |
| if(candidate) ws.send(JSON.stringify({ type:'iceCandidate', iceCandidate:candidate, watchingStreamId: s.id })); | |
| }; | |
| // tell server we want to watch this stream will trigger server to forward offer or instruct owner | |
| ws.send(JSON.stringify({ type:'watchStream', id: s.id })); | |
| }); | |
| container.appendChild(card); | |
| }); | |
| } | |
| /* WEBSOCKET message handling */ | |
| ws.addEventListener('open', ()=>console.log('ws open')); | |
| ws.addEventListener('error', (e)=>console.error('ws err', e)); | |
| ws.addEventListener('message', async ev=>{ | |
| try{ | |
| const msg = JSON.parse(ev.data); | |
| // chat | |
| if(msg.type === 'chat'){ | |
| // server broadcasts chat as {type:'chat', user, chat} | |
| renderChatLine(msg.user, msg.chat); | |
| } | |
| // streams update broadcast maybe used to autofill featured or results | |
| else if(msg.type === 'streamsUpdate'){ | |
| // server sends full streams array | |
| // we keep UI intact so only render to search results area when a query was made by server | |
| // but to keep user convenience update featured streams quickly minimal update | |
| // we do not alter layout just update featured list small | |
| const featured = document.getElementById('featured-streams'); | |
| featured.innerHTML = ''; | |
| msg.streams.slice(0,6).forEach(s=>{ | |
| const c = document.createElement('div'); | |
| c.className = 'card'; | |
| c.textContent = s.title; | |
| featured.appendChild(c); | |
| }); | |
| } | |
| // search results | |
| else if(msg.type === 'searchResults'){ | |
| renderResults(msg.results || []); | |
| } | |
| // WebRTC offer arrives from stream owner forwarded by server | |
| else if(msg.type === 'offer' || msg.offer){ | |
| const offerMsg = msg.offer || msg; | |
| // viewer side answer flow | |
| if(peerConnection === null){ | |
| peerConnection = new RTCPeerConnection(config); | |
| peerConnection.ontrack = (e)=>document.getElementById('stream-player').srcObject = e.streams[0]; | |
| peerConnection.onicecandidate = ({candidate})=>{ | |
| if(candidate) ws.send(JSON.stringify({ type:'iceCandidate', iceCandidate:candidate })); | |
| }; | |
| } | |
| await peerConnection.setRemoteDescription(new RTCSessionDescription(offerMsg.offer || offerMsg)); | |
| const answer = await peerConnection.createAnswer(); | |
| await peerConnection.setLocalDescription(answer); | |
| ws.send(JSON.stringify({ type:'answer', answer })); | |
| } | |
| // answer for broadcaster | |
| else if(msg.type === 'answer' || msg.answer){ | |
| const answerMsg = msg.answer || msg; | |
| if(broadcasterPC){ | |
| await broadcasterPC.setRemoteDescription(new RTCSessionDescription(answerMsg.answer || answerMsg)); | |
| } else if(peerConnection){ | |
| await peerConnection.setRemoteDescription(new RTCSessionDescription(answerMsg.answer || answerMsg)); | |
| } | |
| } | |
| // ice candidates | |
| else if(msg.type === 'iceCandidate' || msg.iceCandidate){ | |
| const cand = msg.iceCandidate || msg; | |
| try{ | |
| if(peerConnection) await peerConnection.addIceCandidate(new RTCIceCandidate(cand)); | |
| if(broadcasterPC) await broadcasterPC.addIceCandidate(new RTCIceCandidate(cand)); | |
| }catch(e){ console.warn('ice add err', e); } | |
| } | |
| }catch(e){ | |
| console.error('ws msg parse err', e); | |
| } | |
| }); | |
| /* render chat lines when user sends or incoming handled above */ | |
| function renderChatLine(user, text){ | |
| const p = document.createElement('p'); | |
| p.textContent = user + ': ' + text; | |
| // append to both viewer and dashboard | |
| const viewerChat = document.getElementById('chat-container'); | |
| const dashChat = document.getElementById('messages'); | |
| if(viewerChat) viewerChat.appendChild(p.cloneNode(true)); | |
| if(dashChat) dashChat.appendChild(p.cloneNode(true)); | |
| } | |
| /* keep old dummy featured example so UI not empty */ | |
| (function seedDummy(){ | |
| const f = document.getElementById('featured-streams'); | |
| if(f) { | |
| if(f.children.length === 0){ | |
| const a = document.createElement('div'); a.className='card'; a.textContent='Stream 1'; f.appendChild(a); | |
| const b = document.createElement('div'); b.className='card'; b.textContent='Stream 2'; f.appendChild(b); | |
| const c = document.createElement('div'); c.className='card'; c.textContent='Stream 3'; f.appendChild(c); | |
| } | |
| } | |
| })(); | |
| <script type="module"> | |
| // Import the functions you need from the SDKs you need | |
| import { initializeApp } from "https://www.gstatic.com/firebasejs/12.5.0/firebase-app.js"; | |
| // TODO: Add SDKs for Firebase products that you want to use | |
| // https://firebase.google.com/docs/web/setup#available-libraries | |
| // Your web app's Firebase configuration | |
| const firebaseConfig = { | |
| apiKey: "AIzaSyAyiiWVeRjxSsxUE8fR1DyCPEZwt7o3iHc", | |
| authDomain: "plexstream-ef9b4.firebaseapp.com", | |
| projectId: "plexstream-ef9b4", | |
| storageBucket: "plexstream-ef9b4.firebasestorage.app", | |
| messagingSenderId: "130967153299", | |
| appId: "1:130967153299:web:e424baeb43541f1401bb66" | |
| }; | |
| // Initialize Firebase | |
| const app = initializeApp(firebaseConfig); | |
| </script> | |
| </script> | |
| })(); | |
| </script> | |
| <script type="module"> | |
| // Import the functions you need from the SDKs you need | |
| import { initializeApp } from "https://www.gstatic.com/firebasejs/12.5.0/firebase-app.js"; | |
| // TODO: Add SDKs for Firebase products that you want to use | |
| // https://firebase.google.com/docs/web/setup#available-libraries | |
| // Your web app's Firebase configuration | |
| const firebaseConfig = { | |
| apiKey: "AIzaSyAyiiWVeRjxSsxUE8fR1DyCPEZwt7o3iHc", | |
| authDomain: "plexstream-ef9b4.firebaseapp.com", | |
| projectId: "plexstream-ef9b4", | |
| storageBucket: "plexstream-ef9b4.firebasestorage.app", | |
| messagingSenderId: "130967153299", | |
| appId: "1:130967153299:web:e424baeb43541f1401bb66" | |
| }; | |
| // Initialize Firebase | |
| const app = initializeApp(firebaseConfig); | |
| </script> |
🧰 Tools
🪛 ast-grep (0.45.0)
[warning] 172-172: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: document.getElementById('user-login').innerHTML = <span>${u}</span> <button onclick="logout()">Logout</button>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
(inner-outer-html)
[warning] 178-178: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: document.getElementById('user-login').innerHTML = <button onclick="showLogin()">Login / Signup</button>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
(inner-outer-html)
[warning] 144-144: Avoid insecure (ws://) WebSocket connections; use the encrypted wss:// scheme.
Context: new WebSocket('ws://localhost:8080')
Note: [CWE-319] Cleartext Transmission of Sensitive Information.
(insecure-websocket)
🪛 Betterleaks (1.7.0)
[high] 449-449: Uncovered a GCP API key, which could lead to unauthorized access to Google Cloud services and data breaches.
(gcp-api-key)
🪛 HTMLHint (1.9.2)
[error] 459-459: Tag must be paired, no start tag: [ </script> ]
(tag-pair)
Source: Linters/SAST tools
| function login(){ | ||
| const u = document.getElementById('login-user').value.trim(); | ||
| const p = document.getElementById('login-pass').value.trim(); | ||
| if(!u || !p) return alert('fill both fields'); | ||
| currentUser = u; | ||
| if(!userColors[u]) userColors[u] = randomColor(); | ||
| hideLogin(); | ||
| document.getElementById('user-login').innerHTML = `<span>${u}</span> <button onclick="logout()">Logout</button>`; | ||
| alert('Logged in as '+u); | ||
| } | ||
| function signup(){ login(); } | ||
| function logout(){ | ||
| currentUser = null; | ||
| document.getElementById('user-login').innerHTML = `<button onclick="showLogin()">Login / Signup</button>`; | ||
| alert('Logged out'); | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Unescaped username injected via innerHTML (DOM-based self-XSS).
u is raw user input from #login-user (line 167) and gets concatenated straight into innerHTML (line 173) with no escaping. A malicious string typed into the username field (e.g. an onerror-bearing tag) would execute in the user's own page. The file already has the safe pattern nearby — renderChatLine builds elements via createElement/textContent (lines 193-208) instead of innerHTML.
🛡️ Suggested fix
hideLogin();
- document.getElementById('user-login').innerHTML = `<span>${u}</span> <button onclick="logout()">Logout</button>`;
+ const loginDiv = document.getElementById('user-login');
+ loginDiv.innerHTML = '';
+ const span = document.createElement('span');
+ span.textContent = u;
+ const btn = document.createElement('button');
+ btn.textContent = 'Logout';
+ btn.addEventListener('click', logout);
+ loginDiv.append(span, ' ', btn);
alert('Logged in as '+u);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function login(){ | |
| const u = document.getElementById('login-user').value.trim(); | |
| const p = document.getElementById('login-pass').value.trim(); | |
| if(!u || !p) return alert('fill both fields'); | |
| currentUser = u; | |
| if(!userColors[u]) userColors[u] = randomColor(); | |
| hideLogin(); | |
| document.getElementById('user-login').innerHTML = `<span>${u}</span> <button onclick="logout()">Logout</button>`; | |
| alert('Logged in as '+u); | |
| } | |
| function signup(){ login(); } | |
| function logout(){ | |
| currentUser = null; | |
| document.getElementById('user-login').innerHTML = `<button onclick="showLogin()">Login / Signup</button>`; | |
| alert('Logged out'); | |
| } | |
| function login(){ | |
| const u = document.getElementById('login-user').value.trim(); | |
| const p = document.getElementById('login-pass').value.trim(); | |
| if(!u || !p) return alert('fill both fields'); | |
| currentUser = u; | |
| if(!userColors[u]) userColors[u] = randomColor(); | |
| hideLogin(); | |
| const loginDiv = document.getElementById('user-login'); | |
| loginDiv.innerHTML = ''; | |
| const span = document.createElement('span'); | |
| span.textContent = u; | |
| const btn = document.createElement('button'); | |
| btn.textContent = 'Logout'; | |
| btn.addEventListener('click', logout); | |
| loginDiv.append(span, ' ', btn); | |
| alert('Logged in as '+u); | |
| } | |
| function signup(){ login(); } | |
| function logout(){ | |
| currentUser = null; | |
| document.getElementById('user-login').innerHTML = `<button onclick="showLogin()">Login / Signup</button>`; | |
| alert('Logged out'); | |
| } |
🧰 Tools
🪛 ast-grep (0.45.0)
[warning] 172-172: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: document.getElementById('user-login').innerHTML = <span>${u}</span> <button onclick="logout()">Logout</button>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
(inner-outer-html)
[warning] 178-178: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: document.getElementById('user-login').innerHTML = <button onclick="showLogin()">Login / Signup</button>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
(inner-outer-html)
Source: Linters/SAST tools
| /* render chat lines when user sends or incoming handled above */ | ||
| function renderChatLine(user, text){ | ||
| const p = document.createElement('p'); | ||
| p.textContent = user + ': ' + text; | ||
| // append to both viewer and dashboard | ||
| const viewerChat = document.getElementById('chat-container'); | ||
| const dashChat = document.getElementById('messages'); | ||
| if(viewerChat) viewerChat.appendChild(p.cloneNode(true)); | ||
| if(dashChat) dashChat.appendChild(p.cloneNode(true)); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Duplicate renderChatLine definition silently overrides the colored version.
renderChatLine is already defined at lines 193-208 with per-user color styling via userColors/randomColor(). This second, plain-text definition (function declarations in the same scope, later wins) permanently replaces it, so every chat line renders without color — userColors/randomColor() become dead code.
🔧 Suggested fix
-/* render chat lines when user sends or incoming handled above */
-function renderChatLine(user, text){
- const p = document.createElement('p');
- p.textContent = user + ': ' + text;
- // append to both viewer and dashboard
- const viewerChat = document.getElementById('chat-container');
- const dashChat = document.getElementById('messages');
- if(viewerChat) viewerChat.appendChild(p.cloneNode(true));
- if(dashChat) dashChat.appendChild(p.cloneNode(true));
-}Remove this duplicate; the first definition (lines 193-208) already handles both containers.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /* render chat lines when user sends or incoming handled above */ | |
| function renderChatLine(user, text){ | |
| const p = document.createElement('p'); | |
| p.textContent = user + ': ' + text; | |
| // append to both viewer and dashboard | |
| const viewerChat = document.getElementById('chat-container'); | |
| const dashChat = document.getElementById('messages'); | |
| if(viewerChat) viewerChat.appendChild(p.cloneNode(true)); | |
| if(dashChat) dashChat.appendChild(p.cloneNode(true)); | |
| } |
| <a href="https://vite.dev" target="_blank"> | ||
| <img src={viteLogo} className="logo" alt="Vite logo" /> | ||
| </a> | ||
| <a href="https://react.dev" target="_blank"> | ||
| <img src={reactLogo} className="logo react" alt="React logo" /> | ||
| </a> |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Add rel="noopener noreferrer" to both new-tab links.
Without it, the opened page may retain window.opener access in affected browsers.
Proposed fix
- <a href="https://vite.dev" target="_blank">
+ <a href="https://vite.dev" target="_blank" rel="noopener noreferrer">
...
- <a href="https://react.dev" target="_blank">
+ <a href="https://react.dev" target="_blank" rel="noopener noreferrer">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <a href="https://vite.dev" target="_blank"> | |
| <img src={viteLogo} className="logo" alt="Vite logo" /> | |
| </a> | |
| <a href="https://react.dev" target="_blank"> | |
| <img src={reactLogo} className="logo react" alt="React logo" /> | |
| </a> | |
| <a href="https://vite.dev" target="_blank" rel="noopener noreferrer"> | |
| <img src={viteLogo} className="logo" alt="Vite logo" /> | |
| </a> | |
| <a href="https://react.dev" target="_blank" rel="noopener noreferrer"> | |
| <img src={reactLogo} className="logo react" alt="React logo" /> | |
| </a> |
🧰 Tools
🪛 ast-grep (0.45.0)
[warning] 14-14: target="_blank" without rel="noopener noreferrer" exposes the page to reverse tabnabbing
Context:
Note: [CWE-1022] Use of Web Link to Untrusted Target with window.opener Access.
(jsx-no-target-blank)
Source: Linters/SAST tools
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 7
🧹 Nitpick comments (3)
eslint.config.js (1)
16-23: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winRemove the conflicting ECMAScript version declarations.
languageOptions.ecmaVersionis2020, while nestedparserOptions.ecmaVersionislatest. Keep one intentional target so parsing and lint rules use the same syntax baseline.src/index.css (1)
6-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winResolve the reported Stylelint violations.
Stylelint flags the empty lines before declarations at Lines 6 and 10 and the casing of
optimizeLegibilityat Line 11. Fix these or verify that the external Stylelint check is not intended to gate this project.Source: Linters/SAST tools
old-stuff/styles/style.css (1)
5-23: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDrop quotes around
"Roboto"per stylelint, and consider deduplicatingh2/h3.Stylelint's
font-family-name-quotesflags the quoted font name on lines 6 and 16;h2/h3are otherwise identical and could share a selector.🎨 Suggested fix
-h2 { - font-family: "Roboto", sans-serif; +h2, h3 { + font-family: Roboto, sans-serif; font-optical-sizing: auto; font-weight: 300; font-style: normal; font-variation-settings: "wdth" 100; color: white; } - -h3 { - font-family: "Roboto", sans-serif; - font-optical-sizing: auto; - font-weight: 300; - font-style: normal; - font-variation-settings: - "wdth" 100; - color: white; -}Source: Linters/SAST tools
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 76242e88-874b-49cb-ab89-deeac115490f
⛔ Files ignored due to path filters (4)
old-stuff/favicon.icois excluded by!**/*.icopackage-lock.jsonis excluded by!**/package-lock.jsonpublic/vite.svgis excluded by!**/*.svgsrc/assets/react.svgis excluded by!**/*.svg
📒 Files selected for processing (16)
.gitignoreREADME.mdeslint.config.jsindex.htmlold-stuff/SECURITY.mdold-stuff/index.htmlold-stuff/server.jsold-stuff/signup.htmlold-stuff/styles/style.cssold-stuff/templates/multistream.htmlpackage.jsonsrc/App.csssrc/App.jsxsrc/index.csssrc/main.jsxvite.config.js
🛑 Comments failed to post (2)
old-stuff/server.js (1)
41-54: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Missing
watchStreamhandler breaks viewing entirely; signaling is broadcast to everyone instead of routed.The client sends
{type:'watchStream', id}when a viewer clicks a search result (old-stuff/index.html:337), but this dispatcher has no matching branch — the message is silently dropped. The broadcaster'sofferis only emitted once, at stream start, to whichever clients happen to be connected then (line 52-54 broadcasts to all except sender). Any viewer who connects or clicks "watch" afterward never triggers a re-send of the offer and can never establish a peer connection. Additionally, routingoffer/answer/iceCandidatevia a global broadcast rather than to the specific target peer means multiple simultaneous broadcasters/viewers will cross-wire each other's signaling messages.💡 Suggested direction
+ else if (data.type === "watchStream") { + const stream = streams.find((s) => s.id === data.id); + ws.watchingId = data.id; + if (stream && stream.ownerWs) { + stream.ownerWs.send(JSON.stringify({ type: "requestOffer", forViewer: ws.id })); + } + }Track the broadcaster's connection on the stream object and route
offer/answer/iceCandidateto the specific target client (by id) instead of broadcasting to everyone.old-stuff/templates/multistream.html (1)
1-5: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Invalid
<centre>tag and unclosed<iframe>.
<centre>isn't a real HTML element (typo for<center>, which is deprecated anyway), so the heading won't actually be centered — the intended layout silently fails. HTMLHint also flags the<iframe>as missing its closing tag.🔧 Suggested fix
<!DOCTYPE html> - <centre><h1>User Multi-stream</h1></centre> + <h1 style="text-align:center">User Multi-stream</h1> <iframe id="ytplayer" type="text/html" width="720" height="405" src="https://www.youtube.com/embed/ZvZWWJTcXhU?autoplay=1&controls=0&disablekb=1&end=457&fs=0&loop=1&start=448&color=white" -frameborder="0" allowfullscreen> +frameborder="0" allowfullscreen></iframe>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.<!DOCTYPE html> <h1 style="text-align:center">User Multi-stream</h1> <iframe id="ytplayer" type="text/html" width="720" height="405" src="https://www.youtube.com/embed/ZvZWWJTcXhU?autoplay=1&controls=0&disablekb=1&end=457&fs=0&loop=1&start=448&color=white" frameborder="0" allowfullscreen></iframe>🧰 Tools
🪛 HTMLHint (1.9.2)
[error] 5-5: Tag must be paired, missing: [ </iframe> ], open tag match failed [ <iframe id="ytplayer" type="text/html" width="720" height="405" src="https://www.youtube.com/embed/ZvZWWJTcXhU?autoplay=1&controls=0&disablekb=1&end=457&fs=0&loop=1&start=448&color=white" frameborder="0" allowfullscreen> ] on line 3.
(tag-pair)
Source: Linters/SAST tools
Summary by CodeRabbit
New Features
Documentation
Chores