使用Ajax而不是WebSocket进行WebRTC视频聊天:有可能吗?

f0brbegy  于 2023-10-20  发布在  其他
关注(0)|答案(5)|浏览(1594)

大约六个月前,我能够成功地用PHP编写自己的WebSocket服务器脚本。通过它,我可以在本地主机上设置WebRTC视频聊天服务。我很高兴,直到我意识到为了部署它,我需要一个Web服务器,让我访问套接字。
不幸的是,没有共享的Web主机允许套接字,所有提供套接字的Web服务器都很昂贵。虽然这不是一个大规模的有效解决方案,但为了建立一个演示来展示给人们,我想将信令方法从WebSocket改为Ajax,这样我就可以展示我制作的WebRTC视频聊天服务。
为此,我在过去的几天里一直在尝试编写一些代码,但没有成功地让WebRTC对等端捕获彼此的视频。
此时,当一个客户机连接到脚本时,我使用Ajax向PHP脚本发送一个请求,该脚本检查数据库中是否有其他活动用户。如果没有,脚本将创建一个offer,并将该offer放置在DB中。之后,客户端每秒轮询一个单独的PHP脚本,以检查来自连接到该脚本的另一个客户端的答案。
之后,我从另一个客户端连接到脚本,该客户端查询相同的PHP脚本和DB,然后意识到活动用户(第一个连接)已经发布了一个offer,第二个客户端获取并设置为远程描述。然后,第二个客户端创建一个答案,并将其放置在DB中。
此时,第一个客户端(每秒轮询一次DB)检测到存在一个答案,并将该答案设置为第一个客户端的远程描述。不幸的是,即使成功地完成了所有这些,其他客户端的视频也没有弹出。
所以这里是我困惑的地方,有三个(多部分)问题:
1)我认为,在两个客户端都设置了本地描述,然后将本地描述发送给另一个客户端,另一个客户端将接收到的描述设置为onaddstream事件应该触发的远程描述,从而允许我显示远程视频。然而,这并没有发生。在我使用WebSocket之前,这工作得很好,但是对于纯Ajax,它根本不工作。我是不是漏掉了什么?WebRTC规范在过去的六个月里发生了根本性的变化吗?我试过查看WebRTC规范,但我没有看到任何重大变化。
2)在对无法使用Ajax感到沮丧之后,我回到了我的WebSocket版本,并将其加载到本地主机上。自从上次使用它(六个月前工作得很好)以来,我根本没有修改过代码,但是现在,当我尝试使用它时,有时它工作,有时它不工作。有时我会遇到与无法设置本地和/或远程描述相关的错误。这是怎么回事是否有更改的规格,将导致这种情况发生?与此相关的是,即使我不能用Ajax版本弹出远程视频,我也一直在控制台上重复很多东西,看起来Ajax版本也是如此,有时两个客户端的本地和远程描述都成功设置,有时出于任何原因尝试设置本地/远程描述时会出现错误,即使我每次都运行完全相同的脚本,没有任何更改。我用的是最新版的Chrome,我开始怀疑是不是出了什么问题。
3)建立连接是否需要onicecandidate事件处理程序?我的假设是,peers可以通过简单的有效报价和答案建立连接,并且onicecandidate事件用于提供替代路线等,这可能会导致更好的连接(但不是必需的)。我说错了吗?如果需要onicecandidate信息,您建议我如何使用Ajax作为信令方法来处理这个问题?
我知道这是很多信息和很多问题,但任何信息/见解,任何人都可以提供将非常感谢。过去几天我一直在用头撞table想弄明白这件事,但没有一件事是有意义的。

9udxz4iz

9udxz4iz1#

关于你的APP的第一个建议偶尔工作/不工作的方法是查看当前的在线工作实现。互联网上有很多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,如果您位于任何非对称NAT之后,则使用STUN的“公共”连接(由您的ISP分配的公共IP),或者用于对称NAT的TURN连接)。
在接收到这些ICE候选者时,另一个对等体将使用一些预定义的度量来对它们进行排序,以进行优先级排序,然后发布连接测试以找到好的候选者。请分享它们:另一个peer需要它们(你也需要它)。
以下是我的一些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候选
1.当远程流开始流动时停止添加ICE候选
1.不要为应答者创建对等连接,直到您获得SDP报价
你可以通过一些WebRTC有限状态机在客户端管理所有这些。请参阅参考页面,以了解他所说的“远程流开始流动”是什么意思。
尝试共享ICE候选人并将其添加到对面,至少遵循提示#1和#3,您的应用程序应该再次工作。

信令通信

您询问如何将ICE候选人从对等体转移到另一个对等体,如果ICE候选人对共享很重要(它们是)。要使用AJAX共享东西,无论这些东西是什么,你都可以使用邮箱。我相信你已经这样做了,把什么是由于客户端在数据库中。
当一个对等体需要向另一个对等体发送东西时,尽快发送(使用AJAX)。在服务器端,将此“邮件”放入目标客户端的邮箱中。当一个对等体(周期性地)轮询服务器的新邮件时,给予它所有的新邮件。
创建SDP报价后,ICE候选人将快速生成。所有这些ICE候选者和SDP描述可能会在几毫秒内进入目的地邮箱。目的对等体很有可能一次轮询所尼德的所有内容。即使ICE候选人迟到,下一次投票也会得到它。

11dmarpk

11dmarpk2#

这并没有真正回答你的问题,但是对于信令服务器,你可能想看看Socket.io(在Node上)。我写了一个代码实验室来解释如何设置它:bitbucket.org/webrtc/codelab。非常简单--完整的示例是here:信令服务器代码大约是50行。
SimpleWebRTC运行Signalmaster服务器,该服务器使用Socket.io。
(罗伯特·尼曼写了一篇很好的blog post解释这一点。
另一种选择是将XHR与Google Channel API一起使用,如apprtc.appspot.com示例所示:代码在这里。

sqxo8psd

sqxo8psd3#

任何发送消息的方式都应该等效。它可能有助于记住,只有大约4个基本的消息,你想交换:
1.对等体已加入(或离开)的通知
1.一个offer消息==> SetRemoteDescription,然后做出回答并发送它
1.一个回答消息=> SetRemoteDescription与它
1.从另一个对等体发送的ICE候选==>调用addIceCandidate
冰候选人的事情是奇怪的部分。此外,候选对象包含有趣的字符,因此当您发送它时,URI会对其进行编码。在Coffeescript中,我的看起来像这样:

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

eepp的答案很好,但其中有些建议我认为不正确。具体来说,我认为这三个技巧都是不正确的:

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

以下是我今天(2014年2月)在Chrome中工作的一系列事件。这是针对对等体1将视频流传输到对等体2的简化情况。
1.为对等点设置某种方式来交换消息。(可悲的是,人们如何实现这一点的差异使得不同的WebRTC代码样本如此不可分割。但是在你的代码组织中,试着从精神上将这个逻辑与其他逻辑分开。

  • 在每一端,为重要的信令消息设置消息处理程序。你可以把它们放起来,然后把它们放起来。需要处理和发送的4个核心消息:
  • 加入的其他对等体
  • 从另一端发送的ICE候选者==>调用addIceCandidate
  • 一个offer消息==> SetRemoteDescription与它,然后作出答复并发送它
  • 一个回答消息=> SetRemoteDescription与它
  • 在每一端,创建一个新的peerconnection对象,并为重要事件附加事件处理程序:onicecandidate、onremovestream、onaddstream等。
  • 冰候选人=>发送到另一边
  • 流添加=>将其附加到视频元素,以便您可以看到它
  • 当两个对等点都存在并且所有处理程序都已就绪时,对等点1将获得某种类型的触发消息以启动视频捕获(使用getUserMedia调用)
  • 一旦getUserMedia成功,我们就有了一个流。在对等体1的对等连接对象上调用addStream
  • 然后,也只有在这个时候,第一个对等点才会出价
  • 由于我们在第2步中设置的处理程序,对等点2得到了这个消息并发送了一个应答
  • 与此同时(有点模糊),对等连接对象开始产生ice候选者。它们在两个对等体之间来回发送并被处理(上面的步骤2和3)
  • 流本身开始,不透明,由于2个条件:
  • 提供/回答交换
  • 冰候选人收到,交换,并增加

我还没有找到一个方法来添加视频后的步骤9。当我想改变一些东西时,我回到第三步。

fquxozlt

fquxozlt4#

只是为了把我的帽子扔到戒指里(因为我花了很多时间才弄清楚这一切),我已经扩展了Nielsbaloe优秀的PHP example一点点,这对我来说使它更可靠。谢谢尼尔斯!
在这里看到我的文章:(其中包含一个链接到我的github repo)
https://peacocksoftware.com/blog/webrtc-video-chat-only-php-and-javascript-no-nodejs-or-websockets
它不使用NodeJS或WebSockets,但它使用数据库来跟踪消息。它使用AJAX轮询PHP脚本以获取更新。这些代码在技术上是针对Drupal的,但它旨在用作在任何PHP环境中构建视频聊天的指南。

nhhxz33t

nhhxz33t5#

我写了一个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>

相关问题