使用WebRTC,WebSocket和Go将视频聊天到我的个人网站

2021-05-05 20:22:56

最近2021-05-02最近我越来越感兴趣的是WebRTC如何工作,所以几周前我决定将一个页面添加到我的网站上,我可以在那里设置点对点的视频聊天。在那里有大量的库和服务,使这极其简单,但知道WebRTC现在在浏览器和设备上本地充分支持,我想尝试使用最小的依赖性来做一切。它结果是一个有趣的项目来设置。也很难找到这个简单的例子和​​解释,所以我希望在这里提供。

我能够使用vanilla javascript和html来完成我所需要的一切。唯一需要的HTML是一些<视频>显示本地和远程视频流的元素:

我需要确定当前用户是谁以及他们想和谁交谈。我没有在我的网站上有用户或cookie或cookie,所以我' m只是使用URL查询参数,并为每个&#39生成唯一的链接;用户'下面,PEER1将能够访问与PEER2交谈的第一个链接,反之亦然:

点对点连接是这里的目标,但为了做到这一点,我需要某种方式让两个用户最初沟通,以便彼此相互了解他们在网上以及他们发送的数据是什么样的数据彼此。我' ll稍后解释后端实现,但此WebSocket将允许通过我的后端服务器发生初始通信:

此初始通信由RTCPeerConnection对象创建和处理的消息组成:

看看这个,如果你想更好地了解将建立的连接实际发生的事情以及正在交换消息的格式。我们关心三种类型的信息:提供,答案和冰(互动连接建立)候选人。优惠和应答消息主要包含有关媒体流的信息,而Ice候选消息是关于如何在Web上建立实际对等连接的信息。

Navigator.MediaDevices.getUsermedia()将请求访问当前用户' S相机和麦克风,创建一个在本地显示的媒体流,然后与PEERConnection一起使用以创建'优惠'消息并使用WS连接发送到对等体:

Navigator.MediaDevices.getUsermedia({视频:True,Audio:True})。然后(Stream => {Let Element = Document.getElementById(' local_video' compents.srcobject = stream; compentr ().then(()=> {stream.gettracks()。foreach(track => peerconnection.addtrack(track,stream); peerconnection.onnegotiationneeded =()=> {peerconnection.createoffer()。然后(报价=> {return peerconnection.setlocaldescription(优惠);})。然后(()=> {ws.send(json.stringify(peerconnection.localdescription));});});

与此同时,需要在Peerconnection上进行更多配置来设置如何显示从对等体接收的流,以及如何通过WebSocket向对等体发送冰候选消息:

peerconnection.ontrack = evt => {让元素= document.getElementByID(' remote_video');元素.srcobject = evt.streams [0];元素.play();}; peerconnection.onicCandidate = EVT => {if(evt.candidate){ws.send(json.stringify({type:'候选人' ice:evt.Candidate}); }}

这需要提供提供优惠,答案和冰候选,现在WebSocket和Peerconnection需要能够接收并采取适当的操作:

ws.onmessage =(evt)=> {const message = json.parse(evt.data);切换(message.type){案例'优惠&#39 ;: {peerconnection.setremotedescription(消息).then(()=> {return peerconnection.createanswer()})。然后(答案=> {返回peerconnection.setlocaldescription(答案)})。然后(()=> {ws.send(json.stringify(peerconnection.localdescription));});休息; }案例'答案&#39 ;: {peerconnection.setremotedescription(消息);休息; }}案例'候选人&#39 ;: {peerconnection.addiceCandidate(new rtciceCandidate(message.ice));休息; }};

在后端,我需要某种方式来处理多个活动的WebSocket连接并在它们之间传递消息。我的网站是通过go写的,我的第一个实现在地图中持有内存中的所有活动的WebSocket连接。当消息从一个同行中出现时,我可以在地图中查找另一个同行和#39;在地图中的WebSocket连接并传递给消息。

这在本地运行应用程序时工作,但由于我的网站部署在GCP云上运行多个运行实例,我可以' t依赖于同行和#39; WebSockets连接到相同的实例和同一内存。一个简单的共享地图不可行,所以我寻找别的东西。

对于在后端传递消息,GCP上铭记的第一件事是PUB / SUB,结果证明是一个很好的解决方案。首先,我设置了客户:

导入" cloud.google.com/go/pubsub" var pubsub * pubsub.clientfunc initialize()错误{pubsub,err = pubsub.newclient(context.background()," mattbutterfield" )如果err!= nil {returner} return nil}

检测当前用户是谁以及他们想要基于URL查询参数连接的人。

查找或创建同行和#39的共享PUB /子主题;此处理程序的实例可以发布和订阅。

Func VideoConnections(W http.ResponseWriter,R * Http.Request){WS,ERR:= WebSocket.accept(W,R,NIL)如果ERR!= nil {log.fatal(err)}推迟关闭(WS)UserID: = strings.tring.Tolower(r.url.query()。获得(" userid"))peerid:= strings.tolower(r.url.query()。获得(" peerid" ))对等体:= []字符串{userid,peerID} sort.strings(对等体)主题名称:= fmt.sprintf("视频 - %s-%s",同行[0],对等体[1] )主题:= pubsub.topic(topicname)主题.enableMessageordering = true ctx:= context.background()存在,err:= topic.exist(ctx)如果err!= nil {log.fatal(err)}如果存在{log.printf("主题%s不存在 - 创建它 - 创建它",主题名称)_,err = pubsub.createTopic(CTX,Pointname)如果Err!= nil {log.fatal(Err cctx,cancelfunc:= context.withcancel(ctx)go wsloop(ctx,cancelfunc,ws,主题,userid)pubsubloop(cctx,ctx,ws,主题,userid)}

WSLoop侦听到WebSocket的新消息,并通过订购密钥将它们发布到Pub / sub主题,以确保所有内容到达它发送的顺序:

func wsloop(ctx context.context,cancelfunc context.canceffunc,ws * websocket.conn,主题* pubsub.topic,userid字符串){log.printf("启动wsloop for%s ...", UserID)OrderingKey:= fmt.sprintf("%s-%s" userid,topic.id())for {if _,message,err:= ws.read(CTX); err!= nil {log.printf("读取消息%s&#34错误)break} else {log.printf("收到的消息到websocket:")msg:=&amp ; pubsub.message {data:message,属性:map [string]字符串{" sender" somentringkey:somingswey,}如果_,err = topic.publish(ctx,msg).get( CTX); err!= nil {log.printf("无法发布消息:%s",err)return}}}}}}}}}}}}}}}}} compelfunc()log.printf("关闭wsloop for%s ... ",userid)}

最后,Pubsubloop侦听发布到PUB /子主题的新消息,并将它们写入WebSocket:

Func Pubsubloop(CCTX,CTX Context.Context,WS * WebSocket.conn,主题* pubsub.topic,UserID字符串){log.printf(" logpsubloop for%s ..." hessid)subscriptionname := fmt.sprintf("%s-%s",userid,topic.id())子:= pubsub.subscription(subscriptionname)如果存在,err:= sub.exists(CTX); err!= nil {log.printf("如果sub存在的错误检查:%s" err)返回}如果!存在{log.printf("创建订阅:%s" ,supencriptionname)如果_,err = pubsub.createsubscription(context.background(),supmoctionname,pubsub.subscref {topic:主题,enablemessageoring:true,},); err!= nil {log.printf("创建订阅错误:%s"错误)return}}如果err:= sub.receive(cctx,func(c context.context,m * pubsub.message,m * pubsub.message ){m.ack()如果m.attributes ["发件人"] == userid {log.println("从self&#34跳过消息)return} log.printf(&#34 ;收到的消息到pubsub:")如果err:= ws.write(ctx,websocket.messageText,m.data); err!= nil {log.printf("错误写入%s错误写入消息: %s",userid,err)return}}; err!= = nil {log.printf("收到的错误设置:%s",err)} log.printf("关闭pubsubloop for%s ...",用户身份)}

而且,我有一个工作解决方案。我在各种计算机和移动浏览器上测试了它,在不同的网络上进行了一些距离。对等连接通常是晶体清晰的。它感觉比主流视频会议工具更好,这非常令人满意。潜入我曾经探讨过的一些技术领域是良好的,并与我理解和运作良好的东西出来。