【问题标题】:Failed to set remote answer sdp: Called in wrong state: stable无法设置远程应答 sdp:在错误状态下调用:stable
【发布时间】:2021-05-09 18:04:49
【问题描述】:

我正在尝试使用socket.io 编写WebRTC 应用程序。

信令服务器是用python写的,长这样。

import socketio
import uvicorn
from starlette.applications import Starlette

ROOM = 'room'


sio = socketio.AsyncServer(async_mode='asgi', cors_allowed_origins='*')
star_app = Starlette(debug=True)
app = socketio.ASGIApp(sio, star_app)


@sio.event
async def connect(sid, environ):
    await sio.emit('ready', room=ROOM, skip_sid=sid)
    sio.enter_room(sid, ROOM)


@sio.event
async def data(sid, data):
    await sio.emit('data', data, room=ROOM, skip_sid=sid)


@sio.event
async def disconnect(sid):
    sio.leave_room(sid, ROOM)


if __name__ == '__main__':
    uvicorn.run(app, host='0.0.0.0', port=8003)

客户端是这样的

<script>
    const SIGNALING_SERVER_URL = 'http://127.0.0.1:8003?session_id=1';
    // WebRTC config: you don't have to change this for the example to work
    // If you are testing on localhost, you can just use PC_CONFIG = {}
    const PC_CONFIG = {};

    // Signaling methods
    let socket = io(SIGNALING_SERVER_URL, {autoConnect: false});

    socket.on('data', (data) => {
        console.log('Data received: ', data);
        handleSignalingData(data);
    });

    socket.on('ready', () => {
        console.log('Ready');
        // Connection with signaling server is ready, and so is local stream
        createPeerConnection();
        sendOffer();
    });

    let sendData = (data) => {
        socket.emit('data', data);
    };

    // WebRTC methods
    let pc;
    let localStream;
    let remoteStreamElement = document.querySelector('#remoteStream');

    let getLocalStream = () => {
        navigator.mediaDevices.getUserMedia({audio: true, video: true})
            .then((stream) => {
                console.log('Stream found');
                localStream = stream;
                // Connect after making sure that local stream is availble
                socket.connect();
            })
            .catch(error => {
                console.error('Stream not found: ', error);
            });
    }

    let createPeerConnection = () => {
        try {
            pc = new RTCPeerConnection(PC_CONFIG);
            pc.onicecandidate = onIceCandidate;
            pc.onaddstream = onAddStream;
            pc.addStream(localStream);
            console.log('PeerConnection created');
        } catch (error) {
            console.error('PeerConnection failed: ', error);
        }
    };

    let sendOffer = () => {
        console.log('Send offer');
        pc.createOffer().then(
            setAndSendLocalDescription,
            (error) => {
                console.error('Send offer failed: ', error);
            }
        );
    };

    let sendAnswer = () => {
        console.log('Send answer');
        pc.createAnswer().then(
            setAndSendLocalDescription,
            (error) => {
                console.error('Send answer failed: ', error);
            }
        );
    };

    let setAndSendLocalDescription = (sessionDescription) => {
        pc.setLocalDescription(sessionDescription);
        console.log('Local description set');
        sendData(sessionDescription);
    };

    let onIceCandidate = (event) => {
        if (event.candidate) {
            console.log('ICE candidate');
            sendData({
                type: 'candidate',
                candidate: event.candidate
            });
        }
    };

    let onAddStream = (event) => {
        console.log('Add stream');
        remoteStreamElement.srcObject = event.stream;
    };

    let handleSignalingData = (data) => {
        // let msg = JSON.parse(data);
        switch (data.type) {
            case 'offer':
                createPeerConnection();
                pc.setRemoteDescription(new RTCSessionDescription(data));
                sendAnswer();
                break;
            case 'answer':
                pc.setRemoteDescription(new RTCSessionDescription(data));
                break;
            case 'candidate':
                pc.addIceCandidate(new RTCIceCandidate(data.candidate));
                break;
        }
    };

    // Start connection
    getLocalStream();
</script>

我也将此代码用于客户端作为 socket.io

https://github.com/socketio/socket.io/blob/master/client-dist/socket.io.js

当两个人处于联系中时,一切都会很好。 但是一旦第三个用户尝试连接到他们,流式传输就会停止并出现错误

未捕获(承诺中)DOMException:执行失败 “RTCPeerConnection”上的“setRemoteDescription”:无法设置远程 answer sdp:在错误状态下调用:stable

我对@9​​87654328@ 了解不多,所以我需要你的帮助。谢谢。

附注我在所有浏览器中都看到了这个错误。

查看此存储库

https://github.com/pfertyk/webrtc-working-example

查看此说明

https://pfertyk.me/2020/03/webrtc-a-working-example/

【问题讨论】:

  • RTCPeerConnection 是两个对等点/一对对等点之间的连接。要获得完全连接,您需要为每个远程对等方创建单独的连接 - 通常对于您的 n 用户,每个人都必须拥有 n - 1 连接。似乎在您的情况下 - 对于第三个用户 - 您尝试重新使用现有连接而不是创建新连接。
  • 你是对的......
  • 但是我不知道怎么解决
  • @unknown,为每个peer创建一个peerconnection...
  • @unknown,这次我添加了一个带有一些示例代码的新解决方案,我相信这就是您要寻找的。​​span>

标签: javascript python socket.io webrtc streaming


【解决方案1】:

您收到此错误消息的原因是,当第三个用户加入时,它会向之前连接的 2 个用户发送要约,因此会收到 2 个答案。 由于一个 RTCPeerConnection 只能建立一个点对点连接,当它尝试 setRemoteDescription 对稍后到达的 answer 时会报错,因为它已经与 SDP answer 到达的 peer 建立了稳定的连接第一的。 要处理多个用户,您需要为每个远程对等方实例化一个新的 RTCPeerConnection。

也就是说,您可以使用某种字典或列表结构来管理多个 RTCPeerConnection。通过您的信令服务器,每当用户连接时,您都可以发出一个唯一的用户 ID(可能是套接字 ID)。收到此 id 时,您只需实例化一个新的 RTCPeerConnection 并将收到的 id 映射到新创建的对等连接,然后您必须在数据结构的所有条目上设置远程描述。

当您覆盖仍在使用的对等连接变量“pc”时,这也将消除每次新用户加入时代码中的内存泄漏。

但请注意,此解决方案根本不可扩展,因为您将成倍地创建新的对等连接,大约 6 时,您的通话质量已经很糟糕了。 如果您打算拥有一间会议室,您应该考虑使用SFU,但请注意,通常设置它非常麻烦。

签出Janus videoroom 插件,用于开源 SFU 实现。

【讨论】:

    【解决方案2】:

    如您所知,您应该为每个对等点创建单独的对等点连接,因此在您的代码中,错误的部分是全局变量 pc,每次您在 createPeerConnection 函数中设置它。

    相反,例如,您应该有一个 pcs 数组,每次获得 offer 时,您都会在 createPeerConnection 函数中创建一个新的 pc,并为其设置本地和远程描述pc 并将生成的answer 发送到您的信令服务器。

    【讨论】:

    • 谢谢。我明白了,但你能提供一些示例代码吗?
    【解决方案3】:

    我已经在上面详细回答了这个问题,说明为什么您会遇到这个问题。但似乎您真正在寻找的是一些关于如何修复它的示例工作代码......所以你去:

    index.html:稍微更新 HTML 页面,所以现在我们有一个 div,我们将附加传入的远程视频。

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>WebRTC working example</title>
    </head>
    <body>
        <div id="remoteStreams"></div>
        <script src="socket.io.js"></script>
        <script src="main.js"></script>
    </body>
    </html>
    
    

    app.py:更新数据并准备好事件处理程序,以便我们正确地将套接字 id 发送给其他对等方。

    import socketio
    import uvicorn
    from starlette.applications import Starlette
    
    ROOM = 'room'
    
    sio = socketio.AsyncServer(async_mode='asgi', cors_allowed_origins='*')
    star_app = Starlette(debug=True)
    app = socketio.ASGIApp(sio, star_app)
    
    
    @sio.event
    async def connect(sid, environ):
        await sio.emit('ready', {'sid': sid}, room=ROOM, skip_sid=sid)
        sio.enter_room(sid, ROOM)
    
    
    @sio.event
    async def data(sid, data):
        peerToSend = None
        if 'sid' in data:
          peerToSend = data['sid']
        data['sid'] = sid
        await sio.emit('data', data, room=peerToSend if peerToSend else ROOM, skip_sid=sid)
    
    
    @sio.event
    async def disconnect(sid):
        sio.leave_room(sid, ROOM)
    
    
    if __name__ == '__main__':
        uvicorn.run(app, host='localhost', port=8003)
    

    main.js:创建此 peers 对象以将套接字 id 映射到 RTCPeerConnections 并更新一些函数以使用它而不是 pc 变量。

    const SIGNALING_SERVER_URL = 'ws://127.0.0.1:8003';
    // WebRTC config: you don't have to change this for the example to work
    // If you are testing on localhost, you can just use PC_CONFIG = {}
    const PC_CONFIG = {};
    
    // Signaling methods
    let socket = io(SIGNALING_SERVER_URL, {autoConnect: false});
    
    socket.on('data', (data) => {
        console.log('Data received: ', data);
        handleSignalingData(data);
    });
    
    socket.on('ready', (msg) => {
        console.log('Ready');
        // Connection with signaling server is ready, and so is local stream
        peers[msg.sid] = createPeerConnection();
        sendOffer(msg.sid);
        addPendingCandidates(msg.sid);
    });
    
    let sendData = (data) => {
        socket.emit('data', data);
    };
    
    // WebRTC methods
    let peers = {}
    let pendingCandidates = {}
    let localStream;
    
    let getLocalStream = () => {
        navigator.mediaDevices.getUserMedia({audio: true, video: true})
            .then((stream) => {
                console.log('Stream found');
                localStream = stream;
                // Connect after making sure thzat local stream is availble
                socket.connect();
            })
            .catch(error => {
                console.error('Stream not found: ', error);
            });
    }
    
    let createPeerConnection = () => {
        const pc = new RTCPeerConnection(PC_CONFIG);
        pc.onicecandidate = onIceCandidate;
        pc.onaddstream = onAddStream;
        pc.addStream(localStream);
        console.log('PeerConnection created');
        return pc;
    };
    
    let sendOffer = (sid) => {
        console.log('Send offer');
        peers[sid].createOffer().then(
            (sdp) => setAndSendLocalDescription(sid, sdp),
            (error) => {
                console.error('Send offer failed: ', error);
            }
        );
    };
    
    let sendAnswer = (sid) => {
        console.log('Send answer');
        peers[sid].createAnswer().then(
            (sdp) => setAndSendLocalDescription(sid, sdp),
            (error) => {
                console.error('Send answer failed: ', error);
            }
        );
    };
    
    let setAndSendLocalDescription = (sid, sessionDescription) => {
        peers[sid].setLocalDescription(sessionDescription);
        console.log('Local description set');
        sendData({sid, type: sessionDescription.type, sdp: sessionDescription.sdp});
    };
    
    let onIceCandidate = (event) => {
        if (event.candidate) {
            console.log('ICE candidate');
            sendData({
                type: 'candidate',
                candidate: event.candidate
            });
        }
    };
    
    let onAddStream = (event) => {
        console.log('Add stream');
        const newRemoteStreamElem = document.createElement('video');
        newRemoteStreamElem.autoplay = true;
        newRemoteStreamElem.srcObject = event.stream;
        document.querySelector('#remoteStreams').appendChild(newRemoteStreamElem);
    };
    
    let addPendingCandidates = (sid) => {
        if (sid in pendingCandidates) {
            pendingCandidates[sid].forEach(candidate => {
                peers[sid].addIceCandidate(new RTCIceCandidate(candidate))
            });
        }
    }
    
    let handleSignalingData = (data) => {
        // let msg = JSON.parse(data);
        console.log(data)
        const sid = data.sid;
        delete data.sid;
        switch (data.type) {
            case 'offer':
                peers[sid] = createPeerConnection();
                peers[sid].setRemoteDescription(new RTCSessionDescription(data));
                sendAnswer(sid);
                addPendingCandidates(sid);
                break;
            case 'answer':
                peers[sid].setRemoteDescription(new RTCSessionDescription(data));
                break;
            case 'candidate':
                if (sid in peers) {
                    peers[sid].addIceCandidate(new RTCIceCandidate(data.candidate));
                } else {
                    if (!(sid in pendingCandidates)) {
                        pendingCandidates[sid] = [];
                    }
                    pendingCandidates[sid].push(data.candidate)
                }
                break;
        }
    };
    
    // Start connection
    getLocalStream();
    

    我尝试尽可能少地更改您的代码,因此您应该能够只复制粘贴并使其正常工作。

    这是我的工作代码:https://github.com/lnogueir/webrtc-socketio

    如果您在运行它时遇到任何问题,请告诉我或在那里打开问题,我会尽力提供帮助。

    【讨论】:

    • Cannot read property 'addIceCandidate' of undefined。我在您的存储库中打开了问题。
    • 刚刚解决了这个问题,似乎 chrome 上的扩展运算符存在另一个问题,也修复了这个问题。将在这里更新答案。
    • 我无法理解这里的流程。在“准备好”时,用户“发出”此信号不会收到信号(因为 skip_sid=sid)。因此,当 User 2 进入时,User 1 会收到信号。创建报价后,将在 User 1 浏览器中创建具有 User 2 sid 的 PC。这意味着peerToSend 将是 User 2,位于用户 1 浏览器/实例的服务器端。所以在发出报价时,消息正在跳过 user 1 并且room 等于 user 2,谁刚刚创建了报价?
    【解决方案4】:

    简而言之,您需要确保每个对等点有一个对等连接,并且您的信号协议允许区分谁向您发送了提议或答复。

    有关两个连接的情况,请参阅规范示例https://webrtc.github.io/samples/src/content/peerconnection/multiple/

    为了通过 socket.io 将其推广到多个对等点,(现已弃用且未维护的)simplewebrtc 包可能有用:https://github.com/simplewebrtc/SimpleWebRTC

    simple-peer library 提供了类似的功能,但您必须自己集成 socketio。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-08-04
      • 2016-09-21
      • 2022-10-01
      • 1970-01-01
      • 2016-10-13
      • 1970-01-01
      相关资源
      最近更新 更多