如何在 webRTC 视频聊天中仅接受来自其他参与者的视频流而不提供我自己的视频流

发布于 2025-01-10 00:43:46 字数 11386 浏览 0 评论 0原文

我正在开发一个使用react、node 和socket.io 的webRTC 应用程序。我正在尝试实现一项功能,以便能够加入会议并且仅接收来自会议中其他参与者的视频流,而不必将我自己的视频流添加到会议中。使用 webRTC 可以吗?

例如,如果我使用 URL http://localhost:3000/demo?ghost 进入房间,它只会向我显示来自 http://localhost:3000/demo 的参与者,而不会将我添加到房间。关于如何实现这一点有什么建议吗?最终的目标是,我可以通过加入参数 ?ghost 以幽灵参与者的身份记录会议。下面是我迄今为止为服务器 (server.js) 和客户端 (Video.js) 编写的代码。

这是部署的应用,这是Github 上的完整代码

server.js(后端)

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(前端)

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

3 名参与者的房间是什么样子

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

蓦然回首 2025-01-17 00:43:46

当然可以,伙计,
如果您只想接收其他视频而不推送自己的视频,请设置视频顶部为 false,如下所示

...
const getPermissions = async () => {
    await navigator.mediaDevices
      .getUserMedia({
        video: false, //this here... you only send audio
        audio: true,
      })
...

,但这意味着您将发送音频......所以也许您想成为博士。然后你可以选择不共享音频和视频,根本不添加轨道,如下所示

stream.getTracks().forEach((track) => {
        //connections[id].addTrack(track, stream)
      }
 )

不要调用 connection.addTrack() 因为你可以看到我已经将其注释掉了

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

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文