Tosh Velaga
Tosh Velaga

Reputation: 155

How to ONLY accept video streams from other participants in webRTC video chat without offering my own

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 What the app looks like on localhost

Upvotes: 1

Views: 1161

Answers (1)

ndotie
ndotie

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

Related Questions