11

大约六个月前,我能够成功地用 PHP 编写自己的 WebSocket 服务器脚本。通过它,我能够在本地主机上设置 WebRTC 视频聊天服务。我很高兴,直到我意识到为了部署它,我需要一个允许我访问套接字的 Web 服务器。

不幸的是,没有共享网络托管允许套接字,并且所有提供套接字的网络服务器都很昂贵。虽然不是大规模的有效解决方案,但为了建立一个演示给人们看,我想将信令方法从 WebSocket 更改为 Ajax,以便我可以展示我制作的 WebRTC 视频聊天服务。

为此,过去几天我一直在尝试编写一些代码,但没有成功让 WebRTC 对等方捕获彼此的视频。

目前,当一个客户端连接到脚本时,我正在使用 Ajax 向 PHP 脚本发送请求,该脚本检查数据库中是否有任何其他活动用户。如果不是,则脚本然后创建一个报价,并将报价放在数据库中。之后,客户端每秒轮询一个单独的 PHP 脚本,以检查来自连接到该脚本的另一个客户端的答案。

之后,我从另一个客户端连接到脚本,该客户端查询相同的 PHP 脚本和数据库,然后意识到活动用户(第一个连接)已经发布了一个报价,第二个客户端获取并为远程设置描述。然后第二个客户端创建一个答案,该答案被放置在数据库中。

此时,第一个客户端(每秒轮询 DB)检测到存在答案并将该答案设置为第一个客户端的远程描述。不幸的是,即使成功完成所有这些操作,其他客户的视频也没有弹出。

所以这就是我感到困惑并有三个(多部分)问题的地方:

1)我认为在两个客户端都设置了他们的本地描述然后将该本地描述发送给另一个客户端和另一个客户端设置接收描述作为远程描述之后 onaddstream 事件应该触发,从而允许我显示远程视频。然而,这并没有发生。这在我使用 WebSocket 之前工作得很好,但它根本不适用于纯 Ajax。我有什么特别想念的吗?在过去的六个月里,WebRTC 规范是否发生了根本性的变化?我试过查看 WebRTC 规范,但没有看到任何重大变化。

2) 在对无法使用 Ajax 感到沮丧之后,我回到了我的 WebSocket 版本并将其加载到我的本地主机上。自从上次使用它(六个月前运行良好)以来,我根本没有更改代码,但是现在,当我尝试使用它时,有时它可以工作,有时它不会。有时我会收到与无法设置本地和/或远程描述相关的错误。这是怎么回事?是否对规格进行了更改会导致这种情况发生?与此相关,即使我无法使用 Ajax 版本弹出远程视频,但我一直在向控制台回显很多内容,而且似乎与 Ajax 版本一样,有时是本地和成功设置两个客户端的远程描述,有时无论出于何种原因尝试设置本地/远程描述时都会发生错误,即使我每次都运行完全相同的脚本而没有任何更改。我正在使用最新版本的 Chrome,我开始怀疑它是否存在错误或其他问题。

3) 是否需要 onicecandidate 事件处理程序来建立连接?我的假设是,对等点可以通过简单的有效提议和答案建立连接,并且 onececandidate 事件用于提供替代路线等,这可能会导致更好的连接(但不是必需的)。我错了吗?如果需要 onececandidate 信息,你建议我如何使用 Ajax 作为信号方法来处理这个问题?

我知道这是很多信息和很多问题,但是任何人都可以提供的任何信息/见解将不胜感激。在过去的几天里,我一直在用头撞桌子,试图弄清楚这一点,但没有任何意义。

4

4 回答 4

14

我对您的应用程序的第一个建议。偶尔工作/不工作是查看当前的在线工作实现。互联网上有很多 WebRTC 演示。

AJAX

关于 AJAX:为什么它不起作用?我目前正在做和你一样的事情,而且每次都能正常工作(我暂时不能透露来源)。客户端定期轮询服务器,他们可以通过这种方式将 SDP 描述/ICE 候选发送到特定的其他客户端。服务器充当一个简单的桥梁(这是信令的基础)。

无论是 WebSocket、AJAX 还是IPoAC,只要您将他需要的一切都传输到其他客户端(并且在适当的时候,稍后会详细介绍),它应该可以工作。我什至做了一个演示,您可以使用文本区域手动复制/粘贴 SDP 描述和 ICE 候选,然后单击按钮以在信令过程中前进,当然,这也很好。

ICE候选人

现在:是的,你需要 ICE 候选人。查看我刚刚在 Chromium 27 上生成的示例SDP 提供块:createOffer

v=0
o=- 3866099361 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE audio video
a=msid-semantic: WMS 9kTlKaNe1exIs6JgEFYfXlu6E5f4B5R3I2D8
m=audio 1 RTP/SAVPF 111 103 104 0 8 107 106 105 13 126
c=IN IP4 0.0.0.0
a=rtcp:1 IN IP4 0.0.0.0
a=ice-ufrag:l8Qu31Vu4VG5YApS
a=ice-pwd:TpyQ5iESUH4HvYGE4ay8JUhe
a=ice-options:google-ice
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=sendrecv
a=mid:audio
a=rtcp-mux
a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:bC5YSe2xCmui0wSxUHWKIi9INbZ2y0VrO1swoZbl
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:107 CN/48000
a=rtpmap:106 CN/32000
a=rtpmap:105 CN/16000
a=rtpmap:13 CN/8000
a=rtpmap:126 telephone-event/8000
a=maxptime:60
a=ssrc:1976175890 cname:/+lKYsttecoiyiu5
a=ssrc:1976175890 msid:9kTlKaNe1exIs6JgEFYfXlu6E5f4B5R3I2D8 9kTlKaNe1exIs6JgEFYfXlu6E5f4B5R3I2D8a0
a=ssrc:1976175890 mslabel:9kTlKaNe1exIs6JgEFYfXlu6E5f4B5R3I2D8
a=ssrc:1976175890 label:9kTlKaNe1exIs6JgEFYfXlu6E5f4B5R3I2D8a0
m=video 1 RTP/SAVPF 100 116 117
c=IN IP4 0.0.0.0
a=rtcp:1 IN IP4 0.0.0.0
a=ice-ufrag:l8Qu31Vu4VG5YApS
a=ice-pwd:TpyQ5iESUH4HvYGE4ay8JUhe
a=ice-options:google-ice
a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
a=sendrecv
a=mid:video
a=rtcp-mux
a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:bC5YSe2xCmui0wSxUHWKIi9INbZ2y0VrO1swoZbl
a=rtpmap:100 VP8/90000
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack 
a=rtpmap:116 red/90000
a=rtpmap:117 ulpfec/90000
a=ssrc:3452335690 cname:/+lKYsttecoiyiu5
a=ssrc:3452335690 msid:9kTlKaNe1exIs6JgEFYfXlu6E5f4B5R3I2D8 9kTlKaNe1exIs6JgEFYfXlu6E5f4B5R3I2D8v0
a=ssrc:3452335690 mslabel:9kTlKaNe1exIs6JgEFYfXlu6E5f4B5R3I2D8
a=ssrc:3452335690 label:9kTlKaNe1exIs6JgEFYfXlu6E5f4B5R3I2D8v0

您是否看到任何可以帮助其他客户端连接到我的机器的东西?我不这么认为。所有这些 ICE 机制的目的是收集候选连接(192.168.1.15如果您在任何不对称的 NAT 后面,则使用 STUN 收集连接候选(本地连接,例如“公共”连接(由您的 ISP 分配的公共 IP),或对称 NAT 的 TURN 连接)) .

在收到这些 ICE 候选者后,其他对等方将使用一些预定义的指标对它们进行排序以进行优先级排序,然后发出连接测试以找到一个好的候选者。所以请分享它们:其他同伴需要它们(你也需要它)。

以下是我的一些 ICE 候选人:

a=candidate:303249700 1 udp 2113937151 192.168.50.238 43806 typ host generation 0
a=candidate:303249700 2 udp 2113937151 192.168.50.238 43806 typ host generation 0
a=candidate:1552991700 1 tcp 1509957375 192.168.50.238 35630 typ host generation 0

现在这些是具体的(尽管只是本地的,因为我没有使用任何 STUN URL 配置 RTC 对等连接)方式让另一个对等连接到我的机器。

WebRTC 信令提示

本页底部有一些有趣的提示。老实说,我现在不能告诉你为什么你应该遵循这些或为什么它们首先存在,但我确实遵循了它们并且没有信号问题。他们来了:

  1. 对于回答者:永远不要添加 ICE 候选者,直到该对等方生成/创建答案 SDP
  2. 当远程流开始流动时停止添加 ICE 候选
  3. 在获得 offer SDP 之前不要为应答者创建对等连接

您可以通过一些 WebRTC 有限状态机在客户端管理所有这些。请参阅参考页面以了解他所说的远程流开始流动是什么意思。

尝试分享 ICE 候选人并将它们添加到对面,至少遵循提示 #1 和 #3,您的应用程序应该可以再次运行。

信令通信

您询问了如何将 ICE 候选人从同行转移到另一个人,以防 ICE 候选人对分享很重要(他们确实如此)。要使用 AJAX 共享内容,无论这些内容是什么,您都可以使用邮箱。我相信您已经通过在数据库中放置客户应得的东西来做到这一点。

每当一个对等点需要向另一个对等点发送某些东西时,请尽快发送(使用 AJAX)。在服务器端,将此“邮件”放在目标客户端的邮箱中。当对等点(定期)轮询服务器以获取新邮件时,将其所有新邮件都提供给它。

创建 SDP 报价时,会快速生成 ICE 候选人。所有这些 ICE 候选者和 SDP 描述可能会在几毫秒内进入目标邮箱。目标对等点很有可能会立即轮询所有需要的东西。即使 ICE 候选人迟到,其下一次民意调查也会得到它。

于 2013-06-18T15:17:42.250 回答
3

这并不能真正回答您的问题,但对于信令服务器,您可能需要查看Socket.io(在 Node 上)。我写了一个代码实验室来解释如何设置它:bitbucket.org/webrtc/codelab。这很简单——完整的例子在这里:信令服务器代码大约 50 行。

SimpleWebRTC运行使用 Socket.io 的Signalmaster服务器。

(Robert Nyman 写了一篇很好的博客文章来解释这一点。)

另一种选择是将 XHR 与 Google Channel API 结合使用,如apprtc.appspot.com示例所示:code here

于 2013-06-20T12:51:03.653 回答
2

任何发送消息的方式都应该等效。请记住,您要交换的基本信息只有大约 4 条:

  1. 对等点已加入(或离开)的通知
  2. 一个报价消息 ==>SetRemoteDescription用它,然后回答并发送它
  3. 回复消息 ===>SetRemoteDescription与它
  4. 从其他对等方发送的一个冰候选 ==>addIceCandidate用它调用

冰候选的东西是奇怪的部分。此外,候选对象包含有趣的字符,所以当你发送它时,URI 对其进行编码。在 Coffeescript 中,我的看起来像:

peer_connection.onicecandidate = (e) ->
   send { 
          line_index: e.candidate.sdpMLineIndex
          candidate: encodeURIComponent(e.candidate.candidate) }

eepp 的回答很好,但它包含一些我认为不正确的建议。具体来说,我认为这三个提示都是不正确的:

  • 对于回答者:永远不要添加 ICE 候选者,直到该对等方生成/创建答案 SDP
  • 当远程流开始流动时停止添加 ICE 候选
  • 在获得 offer SDP 之前不要为应答者创建对等连接

这是我今天(2014 年 2 月)在 Chrome 中工作的一系列事件。这是一个简化的情况,其中对等方 1 将视频流传输到对等方 2。

  1. 为对等方设置某种方式来交换消息。(可悲的是,人们如何实现这一点的差异使得不同的 WebRTC 代码示例如此不可通约。但在精神上,在您的代码组织中,请尝试将此逻辑与其他逻辑分开。)
  2. 在每一侧,为重要的信令消息设置消息处理程序。您可以设置它们并保留它们。有 4 个核心消息需要处理和发送:
    • 其他同行加入
    • 从另一端发送的冰候选人 ==>addIceCandidate与它通话
    • 一个报价消息 ==>SetRemoteDescription用它,然后回答并发送它
    • 回复消息 ===>SetRemoteDescription与它
  3. 在每一侧,创建一个新的 peerconnection 对象并将事件处理程序附加到它以处理重要事件:onicecandidate、onremovestream、onaddstream 等。
    • 冰候选人 ===> 将其发送到另一端
    • 流添加 ===> 将其附加到视频元素,以便您可以看到它
  4. 当两个对等点都存在并且所有处理程序都到位时,对等点 1 会收到某种触发消息以开始视频捕获(使用getUserMedia调用)
  5. 一旦getUserMedia成功,我们就有了一个流。调用addStream对等体 1 的对等连接对象。
  6. 然后 - 并且只有这样 - 对等方 1 提出要约
  7. 由于我们在步骤 2 中设置的处理程序,对等方 2 得到了这个并发送了一个答案
  8. 与此同时(并且有些模糊),对等连接对象开始产生候选冰。它们在两个对等点之间来回发送并处理(上面的步骤 2 和 3)
  9. 由于 2 个条件,流式传输本身不透明地开始:
    • 提供/回答交换
    • 收到、交换和添加的ICE候选人

在第 9 步之后我还没有找到添加视频的方法。当我想更改某些内容时,我会回到第 3 步。

于 2014-02-06T19:05:36.293 回答
0

我写了一个 Ajax 示例,它将在没有 socket.io 或其他任何东西的情况下共享 ICE 和 SDP。这可能有用

后端源代码发布在此存储库中

<span></span>
<div id="root"></div>
<script src="./peer-to-peer.js"></script>
<script>
    (async function(global) {

        const root = document.querySelector('#root');
        const span = document.querySelector('span');
        const peerHub = new Map();

        global.peerHub = peerHub;

        const sleep = (timeout = 1000) => new Promise((res) => setTimeout(() => res(), timeout));

        const createVideo = (stream) => {
            const video = document.createElement('video');
            video.srcObject = stream;
            root.appendChild(video);
            video.play();
        };

        const request = async (url = '', params = {}) => {
            const builder = new URL(url, location.origin);
            Object.entries(params).forEach(([k, v]) => builder.searchParams.set(k, v));
            const data = await fetch(builder.toString());
            const json = await data.json();
            return json;
        };

        const currentStream = await navigator.mediaDevices.getUserMedia({
            video: { 
                facingMode: 'user',
                width: {
                    min: 0,
                    max: window.screen.width,
                },
                height: {
                    min: 0,
                    max: window.screen.height,
                },
            },
        });

        const currentUserId = await request('api/join-room');
        span.innerText = `Joined as user_${currentUserId}`;
        createVideo(currentStream);

        const createConnection = (toUserId = -1, initiator = false) => {
            const p2p = new PeerToPeer({
                mediaStream: currentStream,
                fromUserId: currentUserId,
                initiator,
                toUserId,
            });
            p2p.on('stream', ({ stream }) => createVideo(stream));
            p2p.on('sdp', ({ sdp, toUserId }) => {
                console.log(`Outgoing sdp ${toUserId} ${Date.now()}`, { sdp });
                request('api/create-sdp', {
                    currentUserId,
                    toUserId,
                    sdp,
                });
            });
            p2p.on('ice', ({ ice, toUserId }) => {
                console.log(`Outgoing ice ${toUserId} ${Date.now()}`, { ice });
                request('api/create-ice', {
                    currentUserId,
                    toUserId,
                    ice,
                });
            });
            peerHub.set(toUserId, p2p);
            return p2p;
        };

        const userList = await request('api/user-list');
        const targetList = userList.filter((id) => id !== currentUserId);
        targetList.forEach((toUserId) => createConnection(toUserId, true));

        const browseForIncomingSdp = async () => {
            const sdpList = await request('api/read-sdp', {
                currentUserId,
            });
            console.log({sdpList});
            await Promise.all(sdpList.map(async ({
                fromUserId,
                sdp,
            }) => {
                await request('api/mark-sdp', {
                    currentUserId,
                    fromUserId,
                });
                if (peerHub.has(fromUserId)) {
                    console.log(`Incoming answer sdp ${fromUserId} ${Date.now()}`, { sdp });
                    const p2p = peerHub.get(fromUserId);
                    p2p.setRemoteSdp(sdp);
                } else {
                    console.log(`Incoming offer sdp ${fromUserId} ${Date.now()}`, { sdp });
                    const p2p = createConnection(fromUserId, false);
                    p2p.setRemoteSdp(sdp);
                }
            }));
        };

        const browseForIncomingIce = async () => {
            const iceList = await request('api/read-ice', {
                currentUserId
            });
            console.log({iceList});
            await Promise.all(iceList.map(async ({
                fromUserId,
                ice,
            }) => {
                await request('api/mark-ice', {
                    currentUserId,
                    fromUserId,
                });
                console.log(`Incoming ice ${fromUserId} ${Date.now()}`, { ice });
                const p2p = peerHub.get(fromUserId);
                p2p.setRemoteIce(ice);
            }));
        };

        const tick = async () => {
            await browseForIncomingSdp();
            await browseForIncomingIce();
        };

        do {
            await tick();
            await sleep(5_000);
        } while (true);

        // button.addEventListener('click', tick);

    })(window);
</script>
于 2021-10-07T21:33:57.240 回答