【问题标题】:WebRTC video chat with Ajax instead of WebSocket: Possible?使用 Ajax 而不是 WebSocket 的 WebRTC 视频聊天:可能吗?
【发布时间】:2013-06-14 05:16:12
【问题描述】:

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

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

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

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

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

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

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

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

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

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

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

【问题讨论】:

  • "所有提供套接字的网络服务器都很昂贵" 您可以使用 Amazon Web Services 在一年内免费获得所需的东西,我也会说 Google App Engine,但我不确定这个。
  • 安托万,很高兴知道。非常感谢。不幸的是,它并没有真正回答我的主要问题。

标签: php ajax websocket webrtc


【解决方案1】:

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

AJAX

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

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

ICE 候选人

现在:是的,您需要 ICE 候选人。查看我刚刚在 Chromium 27 上使用 createOffer 生成的示例 SDP offer 块:

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,“公共”连接(由您的 ISP 分配的公共 IP),如果您位于任何不对称的 NAT 后面,或者 TURN 连接用于对称 NAT)。

在收到这些 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 信令提示

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

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

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

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

信令通信

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

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

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

【讨论】:

  • eepp,这是一个非常好的答案。太感谢了。我认为你完美地回答了我所有的问题。我将此标记为正确答案,但在此之前,我只想先测试您建议的所有内容。在我让它工作(我认为我可以)之后,我会以实物回应并将您的答案标记为正确。非常感谢。这是一个非常好的答案。 (我已经说过了吗?!)
【解决方案2】:

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

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

(Robert Nyman 写了一篇很好的 blog post 来解释这一点。)

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

【讨论】:

  • 山姆,我查看了所有这些链接。很有意思。谢谢。如果我的共享网络托管计划只有一个基本的 Apache 服务器,是否可以安装 Node.js,或者这取决于网络托管公司?附带说明一下,您认为确定哪个客户应该发送报价以及哪个客户应该在发送答案之前等待报价的最佳方式是什么?我从来没有真正理解过处理这个问题的最佳方法。非常感谢。
  • Node.js 是否可用或可以安装取决于您的托管公司。有很多 Node 提供商,例如 Nodejitsu、Joyent、Heroku、EngineYard 和 Nodester。哪个客户发送报价并不重要——谁先到那里! Socket.io 房间可以很好地解决这个问题——你可以在我的代码实验室示例中看到这一点。
  • 山姆,再次感谢您的回复。我查看了您的示例代码,但我仍然对如何确定谁先到达那里感到有些困惑。或者我应该说,这是有道理的,但是你是先检查房间里是否有其他客户,如果没有,然后创建报价,或者你只是盲目地为每个客户创建报价,不管怎样,然后在事后检查其他客户,并据此决定是否使用该优惠?再次感谢您的帮助。
  • 房间中的第一个客户端将等到另一个客户端进入房间然后提出报价:第一个客户端从服务器应用程序收到一条消息,它是房间中的第一个,因此知道它是发起者(反之亦然)。
【解决方案3】:

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

  1. 对等方已加入(或离开)的通知
  2. 提供信息 ==> SetRemoteDescription 并附上,然后回复并发送
  3. 回复消息 ===> SetRemoteDescription 与它
  4. 从其他对等方发送的 ice 候选者 ==> 使用它调用 addIceCandidate

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

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

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

  • 对于回答者:永远不要添加 ICE 候选者,直到那个同伴 生成/创建答案 SDP
  • 远程时停止添加 ICE 候选 流开始流动
  • 不要为应答者创建对等连接,直到 你得到报价 SDP

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

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

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

【讨论】:

    【解决方案4】:

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

    后台源码发布in this repository

    <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>
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-07-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-07-23
      • 2019-03-24
      相关资源
      最近更新 更多