shitted this out in 5 mins
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
||||||
1076
package-lock.json
generated
Normal file
1076
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
package.json
Normal file
14
package.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "simplescreenshare",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Minimal WebRTC screenshare app",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "node server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"socket.io": "^4.7.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
98
public/app.js
Normal file
98
public/app.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
const socket = io();
|
||||||
|
const peerConnections = {};
|
||||||
|
const startBtn = document.getElementById('startBtn');
|
||||||
|
const localVideo = document.getElementById('localVideo');
|
||||||
|
const statusDot = document.getElementById('statusDot');
|
||||||
|
const statusText = document.getElementById('statusText');
|
||||||
|
|
||||||
|
let activeStream;
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
iceServers: [
|
||||||
|
{ urls: "stun:stun.l.google.com:19302" }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on('viewer', id => {
|
||||||
|
if (!activeStream) return;
|
||||||
|
|
||||||
|
const peerConnection = new RTCPeerConnection(config);
|
||||||
|
peerConnections[id] = peerConnection;
|
||||||
|
|
||||||
|
activeStream.getTracks().forEach(track => {
|
||||||
|
peerConnection.addTrack(track, activeStream);
|
||||||
|
});
|
||||||
|
|
||||||
|
peerConnection.onicecandidate = event => {
|
||||||
|
if (event.candidate) {
|
||||||
|
socket.emit('candidate', id, event.candidate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
peerConnection
|
||||||
|
.createOffer()
|
||||||
|
.then(sdp => peerConnection.setLocalDescription(sdp))
|
||||||
|
.then(() => {
|
||||||
|
socket.emit('offer', id, peerConnection.localDescription);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('answer', (id, description) => {
|
||||||
|
if (peerConnections[id]) {
|
||||||
|
peerConnections[id].setRemoteDescription(description);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('candidate', (id, candidate) => {
|
||||||
|
if (peerConnections[id]) {
|
||||||
|
peerConnections[id].addIceCandidate(new RTCIceCandidate(candidate));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnectPeer', id => {
|
||||||
|
if (peerConnections[id]) {
|
||||||
|
peerConnections[id].close();
|
||||||
|
delete peerConnections[id];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
startBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getDisplayMedia({
|
||||||
|
video: {
|
||||||
|
width: { ideal: 1920 },
|
||||||
|
height: { ideal: 1080 },
|
||||||
|
frameRate: { ideal: 60 }
|
||||||
|
},
|
||||||
|
audio: false
|
||||||
|
});
|
||||||
|
|
||||||
|
activeStream = stream;
|
||||||
|
localVideo.srcObject = stream;
|
||||||
|
localVideo.classList.add('active');
|
||||||
|
|
||||||
|
startBtn.style.display = 'none';
|
||||||
|
statusDot.classList.add('active');
|
||||||
|
statusText.innerText = 'Sharing Screen (1080p60)';
|
||||||
|
|
||||||
|
socket.emit('broadcaster');
|
||||||
|
|
||||||
|
stream.getVideoTracks()[0].onended = () => {
|
||||||
|
stopSharing();
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error accessing display media.", err);
|
||||||
|
statusText.innerText = 'Failed to access screen';
|
||||||
|
statusDot.style.backgroundColor = 'var(--error-color)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function stopSharing() {
|
||||||
|
startBtn.style.display = 'inline-block';
|
||||||
|
localVideo.classList.remove('active');
|
||||||
|
statusDot.classList.remove('active');
|
||||||
|
statusText.innerText = 'Not Sharing';
|
||||||
|
activeStream = null;
|
||||||
|
socket.emit('disconnect');
|
||||||
|
}
|
||||||
35
public/index.html
Normal file
35
public/index.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Screenshare - Broadcaster</title>
|
||||||
|
<link rel="stylesheet" href="style.css" />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container glass">
|
||||||
|
<h1>Share Your Screen</h1>
|
||||||
|
<p>Broadcast your screen at 1080p 60fps to viewers.</p>
|
||||||
|
<button id="startBtn" class="btn primary-btn">Start Screenshare</button>
|
||||||
|
<div class="status-indicator">
|
||||||
|
<span class="dot" id="statusDot"></span>
|
||||||
|
<span id="statusText">Not Sharing</span>
|
||||||
|
</div>
|
||||||
|
<video id="localVideo" autoplay playsinline muted></video>
|
||||||
|
<div class="info-section">
|
||||||
|
<p>
|
||||||
|
Viewers can watch at:
|
||||||
|
<a href="/viewer.html" target="_blank" class="accent-link"
|
||||||
|
>/viewer.html</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="/socket.io/socket.io.js"></script>
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
207
public/style.css
Normal file
207
public/style.css
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
:root {
|
||||||
|
--bg-color: #0f172a;
|
||||||
|
--text-primary: #f8fafc;
|
||||||
|
--text-secondary: #94a3b8;
|
||||||
|
--accent-color: #3b82f6;
|
||||||
|
--accent-glow: rgba(59, 130, 246, 0.5);
|
||||||
|
--glass-bg: rgba(30, 41, 59, 0.7);
|
||||||
|
--glass-border: rgba(255, 255, 255, 0.1);
|
||||||
|
--success-color: #10b981;
|
||||||
|
--error-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: radial-gradient(circle at top, #1e293b 0%, var(--bg-color) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 3rem;
|
||||||
|
border-radius: 24px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 800px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass {
|
||||||
|
background: var(--glass-bg);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
background: linear-gradient(to right, #fff, var(--text-secondary));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border: none;
|
||||||
|
padding: 1rem 2.5rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 9999px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 0 20px var(--accent-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 0 30px var(--accent-glow);
|
||||||
|
background-color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--text-secondary);
|
||||||
|
display: inline-block;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.active {
|
||||||
|
background-color: var(--success-color);
|
||||||
|
box-shadow: 0 0 10px var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.pulse {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
animation: pulse-animation 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-animation {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #000;
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
video.active {
|
||||||
|
display: block;
|
||||||
|
animation: fadeIn 0.5s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
border-top: 1px solid var(--glass-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accent-link {
|
||||||
|
color: var(--accent-color);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accent-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-container {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: #000;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-container video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 0;
|
||||||
|
border: none;
|
||||||
|
margin: 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-container video.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
padding: 2rem 4rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
text-align: center;
|
||||||
|
z-index: 10;
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
24
public/viewer.html
Normal file
24
public/viewer.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Screenshare - Viewer</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="viewer-container">
|
||||||
|
<div class="overlay glass" id="overlay">
|
||||||
|
<h1>Waiting for stream...</h1>
|
||||||
|
<div class="status-indicator">
|
||||||
|
<span class="dot pulse"></span>
|
||||||
|
<span>Connecting to Broadcaster</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<video id="remoteVideo" autoplay playsinline></video>
|
||||||
|
</div>
|
||||||
|
<script src="/socket.io/socket.io.js"></script>
|
||||||
|
<script src="viewer.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
61
public/viewer.js
Normal file
61
public/viewer.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
const socket = io();
|
||||||
|
const remoteVideo = document.getElementById('remoteVideo');
|
||||||
|
const overlay = document.getElementById('overlay');
|
||||||
|
|
||||||
|
let peerConnection;
|
||||||
|
const config = {
|
||||||
|
iceServers: [
|
||||||
|
{ urls: "stun:stun.l.google.com:19302" }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on('offer', (id, description) => {
|
||||||
|
peerConnection = new RTCPeerConnection(config);
|
||||||
|
|
||||||
|
peerConnection.ontrack = event => {
|
||||||
|
remoteVideo.srcObject = event.streams[0];
|
||||||
|
remoteVideo.classList.add('active');
|
||||||
|
overlay.classList.add('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
peerConnection.onicecandidate = event => {
|
||||||
|
if (event.candidate) {
|
||||||
|
socket.emit('candidate', id, event.candidate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
peerConnection
|
||||||
|
.setRemoteDescription(description)
|
||||||
|
.then(() => peerConnection.createAnswer())
|
||||||
|
.then(sdp => peerConnection.setLocalDescription(sdp))
|
||||||
|
.then(() => {
|
||||||
|
socket.emit('answer', id, peerConnection.localDescription);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('candidate', (id, candidate) => {
|
||||||
|
if (peerConnection) {
|
||||||
|
peerConnection.addIceCandidate(new RTCIceCandidate(candidate))
|
||||||
|
.catch(e => console.error(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('broadcaster', () => {
|
||||||
|
socket.emit('viewer');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnectPeer', () => {
|
||||||
|
if (peerConnection) {
|
||||||
|
peerConnection.close();
|
||||||
|
peerConnection = null;
|
||||||
|
}
|
||||||
|
remoteVideo.classList.remove('active');
|
||||||
|
remoteVideo.srcObject = null;
|
||||||
|
overlay.classList.remove('hidden');
|
||||||
|
overlay.querySelector('h1').innerText = 'Stream Ended';
|
||||||
|
overlay.querySelector('.status-indicator span:last-child').innerText = 'Waiting for new stream...';
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
socket.emit('viewer');
|
||||||
|
});
|
||||||
45
server.js
Normal file
45
server.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const app = express();
|
||||||
|
const http = require("http");
|
||||||
|
const server = http.createServer(app);
|
||||||
|
const { Server } = require("socket.io");
|
||||||
|
const io = new Server(server);
|
||||||
|
|
||||||
|
app.use(express.static("public"));
|
||||||
|
|
||||||
|
io.on("connection", (socket) => {
|
||||||
|
console.log("a user connected:", socket.id);
|
||||||
|
|
||||||
|
// When the broadcaster starts sharing
|
||||||
|
socket.on("broadcaster", () => {
|
||||||
|
socket.broadcast.emit("broadcaster");
|
||||||
|
});
|
||||||
|
|
||||||
|
// When a viewer joins
|
||||||
|
socket.on("viewer", () => {
|
||||||
|
socket.broadcast.emit("viewer", socket.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// WebRTC Signaling
|
||||||
|
socket.on("offer", (id, message) => {
|
||||||
|
socket.to(id).emit("offer", socket.id, message);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("answer", (id, message) => {
|
||||||
|
socket.to(id).emit("answer", socket.id, message);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("candidate", (id, message) => {
|
||||||
|
socket.to(id).emit("candidate", socket.id, message);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("disconnect", () => {
|
||||||
|
console.log("user disconnected", socket.id);
|
||||||
|
socket.broadcast.emit("disconnectPeer", socket.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
console.log(`listening on *:${PORT}`);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user