Reputation: 155
I am working on a webRTC application that that uses react, node, and socket.io. I am trying to implement a feature to be able to join a meeting and only receive the video streams from other participants in the meeting without having to add my own video stream to the meeting. Is this possible using webRTC?
For example if I enter the room with the url http://localhost:3000/demo?ghost it would only show me the participants from http://localhost:3000/demo without adding me to the room. Any tips on how to implement this? The eventual goal is so that I could record the conference as a ghost participant by joining with the param ?ghost. Here is the code I have so far for the server (server.js) and client (Video.js) below.
Here is the deployed app and here is the full code on Github.
server.js (backend)
const io = require('socket.io')(server, {
cors: {
origin: '*',
},
})
let connections = {}
// connections example: { 'http://localhost:8000/demo':
// [ 'msZkzHGsJ9pd3IzHAAAB', '748f9D8giwRR5uXtAAAD' ]
// }
let timeOnline = {}
// timeOnline example: {
// H0RbYU2nFg9dDcjwAAAB: 2022-02-21T06:47:23.715Z,
// uZ9Fj1R0Q2VS3EgOAAAD: 2022-02-21T06:47:35.652Z
// }
const removeQueryParamFromUrl = (url) => {
return url.split('?')[0]
}
io.on('connection', (socket) => {
socket.on('join-call', (path) => {
//editedPath is without query params
const editedPath = removeQueryParamFromUrl(path)
console.log(path.includes('?ghost'))
// if no connection array exists for this path, create new one with empty array
if (connections[editedPath] === undefined) {
connections[editedPath] = []
}
// push socket.id into array if path does not include ?ghost
if (!path.includes('?ghost')) {
connections[editedPath].push(socket.id)
timeOnline[socket.id] = new Date()
}
// loop over length of array in room which contains users
for (let a = 0; a < connections[editedPath].length; ++a) {
// emit to each user
io.to(connections[editedPath][a]).emit(
'user-joined',
socket.id,
connections[editedPath]
)
}
})
socket.on('signal', (toId, message) => {
io.to(toId).emit('signal', socket.id, message)
})
socket.on('disconnect', () => {
var key
// loop over keys and values of connections object which is now an array
for (const [k, v] of JSON.parse(
JSON.stringify(Object.entries(connections))
)) {
for (let a = 0; a < v.length; ++a) {
if (v[a] === socket.id) {
key = k
for (let a = 0; a < connections[key].length; ++a) {
// emit to all other users in room that user with socket.id has left
io.to(connections[key][a]).emit('user-left', socket.id)
}
var index = connections[key].indexOf(socket.id)
// remove user from room
connections[key].splice(index, 1)
// delete room if no users are present
if (connections[key].length === 0) {
delete connections[key]
}
}
}
}
})
})
server.listen(app.get('port'), () => {
console.log('listening on', app.get('port'))
})
Video.js (frontend)
const Video = () => {
const localVideoref = useRef(null)
const [video, setvideo] = useState(true)
const [videoPreview, setvideoPreview] = useState(true)
const [streams, setstreams] = useState([])
var connections = {}
var socket = null
var socketId = null
var elms = 0
const peerConnectionConfig = {
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
}
useEffect(() => {
// GET USER VIDEO PREVIEW BEFORE ENTERING ROOM IF ghost IS NOT IN QUERY PARAM
if (!window.location.href.includes('ghost')) {
getPermissions()
}
}, [])
const getPermissions = async () => {
await navigator.mediaDevices
.getUserMedia({
video: true,
audio: true,
})
.then((stream) => {
streams.push(stream)
window.localStream = stream
localVideoref.current.srcObject = stream
})
.catch((e) => console.log(e))
}
const getUserMedia = () => {
navigator.mediaDevices
.getUserMedia({ video: video, audio: true })
.then((stream) => {
getUserMediaSuccess(stream)
})
.catch((e) => console.log(e))
}
const getUserMediaSuccess = (stream) => {
streams.push(window.localStream)
window.localStream = stream
localVideoref.current.srcObject = stream
for (let id in connections) {
if (id === socketId) continue
stream.getTracks().forEach((track) => {
connections[id].addTrack(track, stream)
})
// Create offers to connect with other users who join room
// eslint-disable-next-line no-loop-func
connections[id].createOffer().then((description) => {
connections[id]
.setLocalDescription(description)
.then(() => {
// emit local description to other users
socket.emit(
'signal',
id,
JSON.stringify({ sdp: connections[id].localDescription })
)
})
.catch((e) => console.log(e))
})
}
}
const gotMessageFromServer = (fromId, message) => {
var signal = JSON.parse(message)
if (fromId !== socketId) {
if (signal.sdp) {
connections[fromId]
.setRemoteDescription(new RTCSessionDescription(signal.sdp))
.then(() => {
if (signal.sdp.type === 'offer') {
connections[fromId]
.createAnswer()
.then((description) => {
connections[fromId]
.setLocalDescription(description)
.then(() => {
socket.emit(
'signal',
fromId,
JSON.stringify({
sdp: connections[fromId].localDescription,
})
)
})
.catch((e) => console.log(e))
})
.catch((e) => console.log(e))
}
})
.catch((e) => console.log(e))
}
// ADD NEW ICE CANDIDATES
if (signal.ice) {
connections[fromId]
.addIceCandidate(new RTCIceCandidate(signal.ice))
.catch((e) => console.log(e))
}
}
}
const connectToSocketServer = () => {
socket = io.connect('http://localhost:4001', { secure: true })
socket.on('signal', gotMessageFromServer)
socket.on('connect', () => {
socket.emit('join-call', window.location.href)
socketId = socket.id
// REMOVE VIDEO WHEN USER LEAVES
socket.on('user-left', (id) => {
let video = document.querySelector(`[data-socket="${id}"]`)
if (video !== null) {
elms--
//remove video from DOM
video.parentNode.removeChild(video)
let main = document.getElementById('main')
// resize the remaining videos height and width after user leaves
changeCssVideos(main, elms)
}
})
socket.on('user-joined', (id, clients) => {
clients.forEach((socketListId) => {
connections[socketListId] = new RTCPeerConnection(
peerConnectionConfig
)
// Wait for their ice candidate
connections[socketListId].onicecandidate = (event) => {
if (event.candidate != null) {
socket.emit(
'signal',
socketListId,
JSON.stringify({ ice: event.candidate })
)
}
}
// Wait for their video stream
connections[socketListId].ontrack = (event) => {
var searchVideo = document.querySelector(
`[data-socket="${socketListId}"]`
)
if (searchVideo !== null) {
searchVideo.srcObject = event.streams[0]
} else {
// ADD NEW VIDEO ELEMENT TO THE DOM AND CHANGE CSS WIDTH + HEIGHT OF VIDEOS
elms = clients.length
let main = document.getElementById('main')
let cssMesure = changeCssVideos(main, elms)
let video = document.createElement('video')
let css = {
minWidth: cssMesure.minWidth,
minHeight: cssMesure.minHeight,
maxHeight: '100%',
objectFit: 'fill',
}
for (let i in css) video.style[i] = css[i]
video.style.setProperty('width', cssMesure.width)
video.style.setProperty('height', cssMesure.height)
video.setAttribute('data-socket', socketListId)
video.srcObject = event.stream
video.autoplay = true
video.playsinline = true
main.appendChild(video)
}
}
// Add the local video stream's tracks
if (window.localStream !== undefined && window.localStream !== null) {
window.localStream.getTracks().forEach(function (track) {
connections[socketListId].addTrack(track, window.localStream)
})
}
})
if (id === socketId) {
for (let id2 in connections) {
if (id2 === socketId) continue
try {
window.localStream.getTracks().forEach(function (track) {
connections[id2].addTrack(track, window.localStream)
})
} catch (e) {}
// eslint-disable-next-line no-loop-func
connections[id2].createOffer().then((description) => {
connections[id2]
.setLocalDescription(description)
.then(() => {
socket.emit(
'signal',
id2,
JSON.stringify({ sdp: connections[id2].localDescription })
)
})
.catch((e) => console.log(e))
})
}
}
})
})
}
const connect = () => {
setvideoPreview(false)
getUserMedia()
connectToSocketServer()
}
return (
<div>
{videoPreview === true ? (
<>
{/* VIDEO PREVIEW BEFORE ENTERING ROOM */}
<div className='video-preview-container'>
<video
id='my-video'
className='video-preview'
ref={localVideoref}
autoPlay
muted
></video>
</div>
<Button variant='contained' color='primary' onClick={connect}>
Connect
</Button>
</>
) : (
<>
{/* THE ACTUAL VIDEOS IN THE ROOM WITH OTHER CLIENTS */}
<div className='container'>
<Row id='main' className='flex-container'>
<video
id='my-video'
ref={localVideoref}
autoPlay
muted
className='my-video'
></video>
</Row>
</div>
</>
)}
</div>
)
}
export default Video
What the room looks like with 3 participants
Upvotes: 1
Views: 1161
Reputation: 2150
Sure that can be done mate, If you only want to receive other videos without pushing your own the set video top false as below
...
const getPermissions = async () => {
await navigator.mediaDevices
.getUserMedia({
video: false, //this here... you only send audio
audio: true,
})
...
but that means you'll be sending your audio... so maybe you want to be dr. ghost all in. Then you can choose not to share both audio and video by not adding a track at all as below
stream.getTracks().forEach((track) => {
//connections[id].addTrack(track, stream)
}
)
dont call connection.addTrack()
as you can see i've commented it out
Upvotes: 1