如何在 webRTC 视频聊天中仅接受来自其他参与者的视频流而不提供我自己的视频流
我正在开发一个使用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
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
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
当然可以,伙计,
如果您只想接收其他视频而不推送自己的视频,请设置视频顶部为 false,如下所示
,但这意味着您将发送音频......所以也许您想成为博士。然后你可以选择不共享音频和视频,根本不添加轨道,如下所示
不要调用
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
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
dont call
connection.addTrack()
as you can see i've commented it out