{"id":386901,"date":"2024-06-29T06:59:33","date_gmt":"2024-06-29T06:59:33","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=386901"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=386901","title":{"rendered":"<span>\u0423\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c \u0440\u043e\u0431\u043e\u0442\u0430\u043c\u0438 \u0438\u0437 VR. \u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0435\u043d\u0438\u0435 1<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u041f\u0435\u0440\u0432\u0430\u044f \u0441\u0442\u0430\u0442\u044c\u044f \u2013 <a href=\"https:\/\/habr.com\/ru\/articles\/730164\/\" rel=\"noopener noreferrer nofollow\">\u0423\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c \u0440\u043e\u0431\u043e\u0442\u0430\u043c\u0438 \u0438\u0437 VR<\/a><\/p>\n<p>\u0412 \u043f\u0440\u043e\u0448\u043b\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435, \u043c\u044b \u043f\u0440\u043e\u0432\u0435\u043b\u0438 \u043d\u0430\u0447\u0430\u043b\u044c\u043d\u0443\u044e \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043a\u0443 \u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043b\u0438 \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e \u0434\u043b\u044f \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u0432 \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430\u043c\u0438 websocket. \u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u043c\u044b \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u043c \u0440\u0430\u0431\u043e\u0442\u0443 \u043f\u043e WebRTC (\u0427\u0430\u0441\u0442\u0438 3, 4).<\/p>\n<h2>\u0427\u0430\u0441\u0442\u044c 3. \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 WebRTC Connection + DataChannel  <\/h2>\n<p>\u0418\u0442\u0430\u043a, \u0443 \u043d\u0430\u0441 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d \u0441\u0435\u0440\u0432\u0435\u0440 \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043c\u044b \u0441\u043c\u043e\u0436\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043b\u044f \u043e\u0431\u043c\u0435\u043d\u0430 \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043e\u043c WebRTC &#8212; offer, answer, ice, \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u043f\u0440\u043e webrtc \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0447\u0438\u0442\u0430\u0442\u044c \u0432 <a href=\"https:\/\/habr.com\/ru\/companies\/timeweb\/articles\/656947\/\" rel=\"noopener noreferrer nofollow\">\u0442\u0443\u0442<\/a>, \u043d\u0435 \u0431\u0443\u0434\u0435\u043c \u043e\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0442\u044c\u0441\u044f \u043d\u0430 \u0434\u0435\u0442\u0430\u043b\u044f\u0445.<\/p>\n<p>\u0412 \u044d\u0442\u043e\u0439 \u0447\u0430\u0441\u0442\u0438 \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u044b\u0432\u0430\u0442\u044c \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e WebRTC \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f, \u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 DataChannel \u043c\u0435\u0436\u0434\u0443 2\u043c\u044f \u043f\u0438\u0440\u0430\u043c\u0438 \u0432 JS-Python-C#.<\/p>\n<h3>\u0421\u0435\u0440\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442<\/h3>\n<p>\u0414\u043e\u0431\u0430\u0432\u0438\u043c \u0432 \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u0443\u044e \u0440\u0430\u043d\u0435\u0435 \u0440\u0430\u0437\u043c\u0435\u0442\u043a\u0443 main.html \u043d\u043e\u0432\u044b\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b:<\/p>\n<details class=\"spoiler\">\n<summary>main.html<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"xml\">...   &lt;div class=\"row-md-6\">     &lt;form class=\"form-inline\">       &lt;div class=\"form-group\">         &lt;label for=\"content\">Send offer to:&lt;\/label>         &lt;input type=\"text\" id=\"offerTo\" class=\"form-control\" placeholder=\"Offer to...\">       &lt;\/div>       &lt;button id=\"createOffer\" class=\"btn btn-primary\" type=\"submit\">Create Offer&lt;\/button>     &lt;\/form>   &lt;\/div>   &lt;div class=\"row-md-6\">     &lt;form class=\"form-inline\">       &lt;div class=\"form-group\">         &lt;label for=\"content\">Message directly:&lt;\/label>         &lt;input type=\"text\" id=\"content2\" class=\"form-control\" placeholder=\"Message here...\">       &lt;\/div>       &lt;button id=\"sendMessageDirectly\" class=\"btn btn-primary\" type=\"submit\">Directly message&lt;\/button>     &lt;\/form>   &lt;\/div> ...<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u0414\u043b\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 datachannel \u043d\u0430 \u0441\u0442\u043e\u0440\u043e\u043d\u0435 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430 \u0434\u043e\u0440\u0430\u0431\u043e\u0442\u0430\u0435\u043c \u043d\u0430\u0448 \u0441\u043a\u0440\u0438\u043f\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c \u2013 \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u043e\u0431\u043c\u0435\u043d\u0430 sdp\/ice \u0434\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0438 \u043f\u0440\u0438\u0435\u043c\u0430 OFFER. \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 DataCh\u0430annel \u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0443 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u043c\u0435\u0436\u0434\u0443 \u043f\u0438\u0440\u0430\u043c\u0438:<\/p>\n<details class=\"spoiler\">\n<summary>robo.js<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"javascript\">var connection; var userId = 'unknown'; var peerConnection; var dataChannel; var configuration = {   \"iceServers\" : [{   \"urls\" : \"stun:stun2.1.google.com:19302\"   }] };  function connect(){   connection = new WebSocket('wss:\/\/' + window.location.host + '\/robopi_webrtc');   console.log(\"Connsection sucsess\");      initRTCPeerConnection();      connection.onmessage = function(msg) {          var resp = JSON.parse(msg.data);            if(resp.type == 'USERID'){       console.log();       userId = resp.data;       document.getElementById(\"username\").textContent = userId;     }     if(resp.type == 'NEWMEMBER'){       if(userId != resp.userId){         console.log(\"NEWMEMDER:\" + resp.userId);       }     }     if(resp.type == 'OFFER'){       if(userId != resp.userId){         console.log(resp);         handleOffer(resp.payload, resp.userId)       }     }     if(resp.type == 'ICE'){       if(userId != resp.userId){         console.log(resp);         receiveIceCandidate(resp.payload);       }     }     if(resp.type == 'ANSWER'){       if(userId != resp.userId){         console.log(resp);         handleAnswer(resp.payload);       }     }   } }  function login() {   connection.send(JSON.stringify({'userId' : '', 'type' : 'LOGIN', 'data' : '' , 'toUserId' : ''})); }  function newmember() {   connection.send(JSON.stringify({'userId' : userId, 'type' : 'NEWMEMBER', 'data' : '' , 'toUserId' : ''})); }  function initRTCPeerConnection(){   peerConnection = new RTCPeerConnection(configuration);   console.log(\"peerConnection created\");   \/\/ remote datachannel handler   peerConnection.ondatachannel = function(event){     dataChannel = event.channel;     \/\/ open handling     dataChannel.onopen = function(){     console.log(\"Data channel is open!\");     }     \/\/ error handling     dataChannel.onerror = function(error){       console.log(\"Error in datachannel:\", error);     };     \/\/ messaging handler     dataChannel.onmessage = function(event) {       console.log(\"incoming message:\", event.data);     };     \/\/ closing handler     dataChannel.onclose = function() {       console.log(\"Data channel is closed\");     };   }; }  function createRTCDatachannel(){   dataChannel = peerConnection.createDataChannel(\"datachannel\", {      reliable: true    });   \/\/ open handling   dataChannel.onopen = function(){     console.log(\"Data channel is open!\");   }   \/\/ error handling   dataChannel.onerror = function(error){     console.log(\"Error in datachannel:\", error);   };   \/\/ messaging handler   dataChannel.onmessage = function(event) {     console.log(\"incoming message:\", event.data);   };   \/\/ closing handler   dataChannel.onclose = function() {     console.log(\"Data channel is closed\");   }; }  function createOffer(){   createRTCDatachannel();   peerConnection.createOffer(function(offer) {     connection.send(JSON.stringify({'userId' : userId, 'type' : 'OFFER', 'payload' : offer , 'toUserId' : $(\"#offerTo\").val()}));     peerConnection.setLocalDescription(offer);   },   function(error) {     console.log(\"error in offer creating:\" + error);   });   sendIceCandidate(); }  function sendIceCandidate(){   peerConnection.onicecandidate = function(event) {   if (event.candidate) {     console.log(\"sending ice candidate:\" + event.candidate);     connection.send(JSON.stringify({'userId' : userId, 'type' : 'ICE', 'payload' : event.candidate , 'toUserId' : $(\"#offerTo\").val()}));   }   }; }  function receiveIceCandidate(candidate){   peerConnection.addIceCandidate(new RTCIceCandidate(candidate));   console.log(\"sucsess receiving ice candidate\"); }  function handleAnswer(answer){   peerConnection.setRemoteDescription(new RTCSessionDescription(answer));   console.log(\"handling amswer successfully!!\"); }  function handleOffer(offer, fromUser) {   peerConnection.setRemoteDescription(new RTCSessionDescription(offer));   \/\/ create and send an answer to an offer   peerConnection.createAnswer(function(answer) {     peerConnection.setLocalDescription(answer);     connection.send(JSON.stringify({'userId' : userId, 'type' : 'ANSWER', 'payload' : answer , 'toUserId' : fromUser}));   }, function(error) {     console.log(\"error creating answer:\" + error);   }); };  function sendMessageDirectly(){   var message = $(\"#content2\").val();   dataChannel.send(message);   console.log(\"send message:\" + message); }  $(function () {   $(\"form\").on('submit', function (e) {   e.preventDefault();   });    $( \"#connect\" ).click(function() { connect(); }); $( \"#login\" ).click(function() { login(); }); $( \"#newmember\" ).click(function() { newmember(); }); $( \"#createOffer\" ).click(function() { createOffer(); }); $( \"#sendMessageDirectly\" ).click(function() { sendMessageDirectly(); }); });<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u0414\u0435\u043b\u0430\u0435\u043c \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u043c\u0435\u0436\u0434\u0443 2\u043c\u044f \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430\u043c\u0438, \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u043c \u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u043e\u0441\u0442\u044c, \u0443 \u043d\u0430\u0441 \u0434\u043e\u043b\u0436\u0435\u043d \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c\u0441\u044f datachannel \u0438 \u043c\u044b \u043c\u043e\u0436\u0435\u043c \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0442\u044c \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043c\u0435\u0436\u0434\u0443 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430\u043c\u0438:<\/p>\n<details class=\"spoiler\">\n<summary>\u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430:<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/09f\/009\/505\/09f00950533a1395dfd6de7ef3438003.png\" width=\"794\" height=\"512\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/09f\/009\/505\/09f00950533a1395dfd6de7ef3438003.png\"\/><\/figure>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/25d\/985\/3fa\/25d9853fa458b2a1cc0ee56a5fe49bf2.png\" width=\"642\" height=\"456\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/25d\/985\/3fa\/25d9853fa458b2a1cc0ee56a5fe49bf2.png\"\/><\/figure>\n<\/p>\n<\/div>\n<\/details>\n<h3>\u0418\u0441\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442(Python-\u0441\u043a\u0440\u0438\u043f\u0442 \u043d\u0430 RPI)<\/h3>\n<p>\u0414\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441  WebRTC \u0438\u0437 \u043f\u0430\u0439\u0442\u043e\u043d \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0431\u0438\u0431\u0438\u043b\u0438\u043e\u0442\u0435\u043a\u0443 aiortc:<\/p>\n<p> <code>pip3 install aiortc<\/code><\/p>\n<p>\u0422\u0443\u0442 \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u044e\u0442 \u043f\u0435\u0440\u0432\u044b\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 \u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u043e\u0441\u0442\u044c\u044e \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0439 WebRTC (ICE) \u0438 \u0443\u0447\u0438\u0442\u044b\u0432\u0430\u0435\u043c \u043e\u0441\u043e\u0431\u0435\u043d\u043d\u043e\u0441\u0442\u044c \u2013 Python \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u044f\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u0438\u043d\u0438\u0446\u0438\u0430\u0442\u043e\u0440\u043e\u043c \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f, \u0442.\u00a0\u0435. \u043e\u043d \u0434\u043e\u043b\u0436\u0435\u043d \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0442\u044c \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u043e\u0444\u0444\u0435\u0440\u044b:<\/p>\n<details class=\"spoiler\">\n<summary>part2.py<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"python\">import asyncio import websockets import json import ssl from websockets import WebSocketClientProtocol from aiortc import RTCIceCandidate, RTCPeerConnection, RTCSessionDescription, RTCConfiguration, RTCIceServer  async def wsconsume(wsurl: str) -> None:     ssl_context = ssl.SSLContext()     async with websockets.connect(wsurl, ssl=ssl_context) as websocket:         await websocket.send(json.dumps({\"userId\": \"\", \"type\": \"LOGIN\", \"data\": \"\", \"payload\": \"\", \"toUserId\": \"\"}))         await wsconsumer_handler(websocket)   async def wsconsumer_handler(websocket: WebSocketClientProtocol) -> None:     local_user_id = \"\"     ice_servers = [RTCIceServer(urls=[\"stun:stun2.l.google.com:19302\"])]     peer_conn = RTCPeerConnection(RTCConfiguration(iceServers=ice_servers))          @peer_conn.on(\"connectionstatechange\")     async def on_connectionstatechange():         print(\"Connection state is %s\" % peer_conn.connectionState)         if peer_conn.connectionState == \"failed\":             await peer_conn.close()      @peer_conn.on(\"signalingstatechange\")     async def on_signalingstatechange():         print(f\"changed signalingstatechange {peer_conn.signalingState}\")      @peer_conn.on(\"icegatheringstatechange\")     async def on_icegatheringstatechange():         print(f\"changed icegatheringstatechange {peer_conn.iceGatheringState}\")              @peer_conn.on(\"datachannel\")     async def on_datachannel(channel):         print(f\"changed datachannel to {channel}\")         @channel.on(\"message\")         async def on_message(rtc_message):             if isinstance(rtc_message, str):                 print(\"New message from datachannel \" + rtc_message)                 channel.send(\"Reply from PyPi - \" + rtc_message)          async for message in websocket:          msg = json.loads(message)          if msg.get(\"type\") == 'USERID' and local_user_id != msg.get(\"userId\"):             local_user_id = msg.get(\"data\")             print(\"SET UID: \" + local_user_id)             await websocket.send(json.dumps({\"userId\": local_user_id, \"type\": \"NEWMEMBER\", \"data\": \"\",                                              \"payload\": \"\", \"toUserId\": \"\"}))          if msg.get(\"type\") == 'OFFER' and local_user_id == msg.get(\"toUserId\"):             print(\"Handling offer: \" + str(msg.get(\"payload\")))                          await peer_conn.setRemoteDescription(                 RTCSessionDescription(sdp=msg.get(\"payload\").get(\"sdp\"), type=msg.get(\"payload\").get(\"type\")))                           answer = await peer_conn.createAnswer()             print(\"Creating answer:\" + str(answer))             await peer_conn.setLocalDescription(answer)              await websocket.send(json.dumps({\"userId\": local_user_id, \"type\": \"ANSWER\", \"data\": \"\",                                              \"payload\": {\"sdp\": answer.sdp, \"type\": answer.type},                                              \"toUserId\": msg.get(\"userId\")}))                      if msg.get(\"type\") == 'ICE' and local_user_id == msg.get(\"toUserId\"):             print(\"ICE INCOMING\")             candidate = msg.get(\"payload\").get(\"candidate\").split()             new_ice = RTCIceCandidate(                 component=int(candidate[1]),                 foundation=candidate[0].split(\":\")[1],                 ip=candidate[4],                 port=int(candidate[5]),                 priority=int(candidate[3]),                 protocol=candidate[2],                 type=candidate[7],                 relatedAddress=None,                 relatedPort=None,                 sdpMid=msg.get(\"payload\").get(\"sdpMid\"),                 sdpMLineIndex=int(msg.get(\"payload\").get(\"sdpMLineIndex\")),                 tcpType=None             )             await peer_conn.addIceCandidate(new_ice)          if msg.get(\"type\") == 'ANSWER' and local_user_id == msg.get(\"toUserId\"):             print(\"ANSWER INCOMING\")             await peer_conn.setRemoteDescription(                 RTCSessionDescription(sdp=msg.get(\"payload\").get(\"sdp\"), type=msg.get(\"payload\").get(\"type\")))  async def main():     task = asyncio.create_task(wsconsume('wss:\/\/192.168.10.146:9000\/robopi_webrtc'))     await task   if __name__ == '__main__':     loop = asyncio.get_event_loop()     try:         loop.run_until_complete(main())     except KeyboardInterrupt:         loop.stop()         pass<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u0414\u0435\u043b\u0430\u0435\u043c \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u043c\u0435\u0436\u0434\u0443 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u043e\u043c \u0438 \u043f\u0430\u0439\u0442\u043e\u043d \u0441\u043a\u0440\u0438\u043f\u0442\u043e\u043c, \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u043c \u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u043e\u0441\u0442\u044c, \u0443 \u043d\u0430\u0441 \u0434\u043e\u043b\u0436\u0435\u043d \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c\u0441\u044f datachannel \u0438 \u043c\u044b \u043c\u043e\u0436\u0435\u043c \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0442\u044c \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043c\u0435\u0436\u0434\u0443 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u043e\u043c \u0438 python \u0441\u043a\u0440\u0438\u043f\u0442\u043e\u043c:<\/p>\n<details class=\"spoiler\">\n<summary>\u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430:<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/24a\/8e7\/28a\/24a8e728a194a4fd7808fb0bb5234091.png\" width=\"794\" height=\"462\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/24a\/8e7\/28a\/24a8e728a194a4fd7808fb0bb5234091.png\"\/><\/figure>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/d00\/500\/da9\/d00500da9845212be5e733bca708c52b.png\" width=\"794\" height=\"380\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/d00\/500\/da9\/d00500da9845212be5e733bca708c52b.png\"\/><\/figure>\n<\/p>\n<\/div>\n<\/details>\n<h3>\u0423\u043f\u0440\u0430\u0432\u043b\u044f\u044e\u0449\u0438\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442(Unity VR)<\/h3>\n<p>\u0414\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 WebRTC unity \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443 <a href=\"https:\/\/docs.unity3d.com\/Packages\/com.unity.webrtc@3.0\/manual\/index.html\" rel=\"noopener noreferrer nofollow\">WebRTC for Unity<\/a><\/p>\n<p>\u0414\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u0435\u0435 \u043f\u043e \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0438, \u0442\u0430\u043c \u0436\u0435 \u043f\u0440\u0438\u0432\u0435\u0434\u0435\u043d\u044b \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u0440\u0435\u0444\u0435\u0440\u0435\u043d\u0441\u044b \u043f\u043e \u0440\u0430\u0431\u043e\u0442\u0435(\u0447\u0430c\u0442\u044c \u043d\u0435 \u0441\u043e\u0432\u0441\u0435\u043c \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u0430, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043d\u0443\u0436\u043d\u043e \u0432\u043d\u0438\u043c\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u043d\u0430 \u0441\u044d\u043c\u043f\u043b\u044b \u043a \u043f\u0430\u043a\u0435\u0442\u0443!)<\/p>\n<details class=\"spoiler\">\n<summary>\u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435:<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/062\/a99\/ece\/062a99ece6a9bda1cb60db1fc74fcbc0.png\" width=\"642\" height=\"447\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/062\/a99\/ece\/062a99ece6a9bda1cb60db1fc74fcbc0.png\"\/><\/figure>\n<\/p>\n<\/div>\n<\/details>\n<p>C\u043a\u0440\u0438\u043f\u0442\u044b \u0438\u0437 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430 \u0438 Unity \u043c\u043e\u0433\u0443\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0438 \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0442\u044c offer \u0432 \u043e\u0431\u0435 \u0441\u0442\u043e\u0440\u043e\u043d\u044b, \u0442.\u0435. \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c \u0438\u043d\u0438\u0446\u0438\u0430\u0442\u043e\u0440\u0430\u043c\u0438 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f, \u0441\u043a\u0440\u0438\u043f\u0442 \u0438\u0437 Python \u043c\u043e\u0436\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0442\u044c \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0439 offer \u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0432\u0441\u0442\u0440\u0435\u0447\u043d\u044b\u0439 offer \u043f\u043e\u0441\u043b\u0435 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430, \u043d\u043e \u0438\u043d\u0438\u0446\u0438\u0430\u0442\u043e\u0440\u043e\u043c \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u043e\u043d \u043d\u0435 \u0431\u0443\u0434\u0435\u0442.<\/p>\n<details class=\"spoiler\">\n<summary>\u0414\u043e\u043f\u043e\u043b\u043d\u044f\u0435\u043c \u043d\u0430\u0448 UI \u0432 Unity \u043d\u043e\u0432\u044b\u043c\u0438 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u043c\u0438:<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/228\/676\/c6c\/228676c6ca8b52f2b2a8943a257b3c99.png\" width=\"551\" height=\"289\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/228\/676\/c6c\/228676c6ca8b52f2b2a8943a257b3c99.png\"\/><\/figure>\n<p><strong>Dropdown<\/strong> \u2013 \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f Member.<\/p>\n<p><strong>Create Offer<\/strong> \u2013 \u0434\u043b\u044f \u0438\u043d\u0438\u0446\u0438\u0430\u0446\u0438\u0438 WebRTC call.<\/p>\n<p><strong>Send Hello<\/strong> \u2013 \u0434\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f, \u043f\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044e \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u0432 datachannel \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u0442\u0438\u043f\u043e\u0432\u043e\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0441 \u0434\u0430\u0442\u043e\u0439\/\u0432\u0440\u0435\u043c\u0435\u043d\u0435\u043c. \u0414\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u0438\u0437 datachannel \u0431\u0443\u0434\u0435\u043c \u043f\u0438\u0441\u0430\u0442\u044c \u0438\u0445 \u043f\u0440\u043e\u0441\u0442\u043e \u0432 debug log Unity.<\/p>\n<\/div>\n<\/details>\n<p>\u0414\u043e\u043f\u043e\u043b\u043d\u044f\u0435\u043c \u043d\u0430\u0448 \u0441\u043a\u0440\u0438\u043f\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<details class=\"spoiler\">\n<summary>Connection.cs<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cs\">using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using Unity.WebRTC; using UnityEngine; using UnityEngine.UI; using WebSocketSharp;  public class Connection : MonoBehaviour {      private GameObject uuid;     private WebSocket ws;     private ConcurrentQueue&lt;string> incomingWebsocketMessages;     private string userId = \"unknown\";      private GameObject wscandidates;     private List&lt;string> dropOptions;      private RTCPeerConnection webrtcConnection;     private RTCDataChannel dataChannel, remoteDataChannel;      private DelegateOnDataChannel onDataChannel;     private DelegateOnOpen onDataChannelOpen;     private DelegateOnMessage onDataChannelMessage;     private DelegateOnClose onDataChannelClose;     private DelegateOnIceConnectionChange onIceConnectionChange;     private DelegateOnIceCandidate onIceCandidate;      void Start()     {         uuid = GameObject.FindGameObjectWithTag(\"uuid\");         incomingWebsocketMessages = new ConcurrentQueue&lt;string>();          ws = new WebSocket(\"wss:\/\/192.168.10.146:9000\/robopi_webrtc\");         ws.SslConfiguration.EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls12;          ws.OnOpen += (sender, e) =>         {             Debug.Log(\"OPEN WEBSOCKET\");         };          ws.OnMessage += (sender, e) =>         {             if (e.IsText)             {                 incomingWebsocketMessages.Enqueue(e.Data);                 Debug.Log(\"Incoming websocket message:\" + e.Data);             }         };          ws.OnClose += (sender, e) => {             Debug.Log(\"CLOSE WEBSOCKET:\" + e.Reason);         };          dropOptions = new List&lt;string> { \"No candidates!\" };         wscandidates = GameObject.FindGameObjectWithTag(\"candidates\");         wscandidates.GetComponent&lt;Dropdown>().AddOptions(dropOptions);          webrtcConnection = new RTCPeerConnection();          onDataChannelOpen = () =>         {             Debug.Log(\"OPEN LOCAL DATACHANNEL\");             var welcome = \"Welcome to Unity WebRTC! Local creation!\";             dataChannel.Send(welcome);             dataChannel.OnMessage = onDataChannelMessage;         };          onDataChannel = channel =>         {             remoteDataChannel = channel;             Debug.Log(\"OPEN REMOTE DATACHANNEL\");             var welcome = \"Welcome to Unity WebRTC! Remote creation!\";             remoteDataChannel.Send(welcome);             remoteDataChannel.OnMessage = onDataChannelMessage;         };          onDataChannelMessage = bytes =>         {             var messageText = System.Text.Encoding.UTF8.GetString(bytes);             Debug.Log(\"Incoming datachannel message: \" + messageText);         };          onDataChannelClose = () =>         {             Debug.Log(\"CLOSE DATACHANNEL\");         };          onIceConnectionChange = state => {             OnIceConnectionChange(webrtcConnection, state);         };          onIceCandidate = candidate => {             OnIceCandidate(webrtcConnection, candidate);         };      }      void Update()     {         if (incomingWebsocketMessages.TryDequeue(out var wsmessage))         {             var answer = JsonUtility.FromJson&lt;WSMessage&lt;string>>(wsmessage);              if (answer.type.Equals(\"USERID\") &amp;&amp; !answer.data.Equals(userId))             {                 userId = answer.data;                 SetUserId(userId);             }             else if (answer.type.Equals(\"NEWMEMBER\") &amp;&amp; !answer.userId.Equals(userId))             {                 string newcandidate = answer.userId;                 AddNewWsCandidate(newcandidate);             }             else if (answer.type.Equals(\"OFFER\") &amp;&amp; !answer.userId.Equals(userId))             {                 var incomingOffer = JsonUtility.FromJson&lt;WSMessage&lt;Offer>>(wsmessage);                 Debug.Log(\"INCOMING OFFER:\" + incomingOffer);                  var desc = new RTCSessionDescription();                 desc.type = RTCSdpType.Offer;                 desc.sdp = incomingOffer.payload.sdp;                  wscandidates.GetComponent&lt;Dropdown>().captionText.text = answer.userId;                  StartCoroutine(HandleCall(desc));             }             else if (answer.type.Equals(\"ICE\") &amp;&amp; !answer.userId.Equals(userId))             {                 var iceCandidate = JsonUtility.FromJson&lt;WSMessage&lt;Ice>>(wsmessage);                  Debug.Log(\"INCOMING ICE:\" + answer);                  RTCIceCandidateInit init = new RTCIceCandidateInit();                 init.candidate = iceCandidate.payload.candidate;                 init.sdpMid = iceCandidate.payload.sdpMid;                 init.sdpMLineIndex = iceCandidate.payload.sdpMLineIndex;                  RemoteIceCandidate(webrtcConnection, new RTCIceCandidate(init));             }             else if (answer.type.Equals(\"ANSWER\") &amp;&amp; !answer.userId.Equals(userId))             {                 var incomingAnswer = JsonUtility.FromJson&lt;WSMessage&lt;Answer>>(wsmessage);                 Debug.Log(\"INCOMING ANSWER:\" + incomingAnswer.payload.type + \"  sdp:\" + incomingAnswer.payload.sdp);                  var desc = new RTCSessionDescription();                 desc.type = RTCSdpType.Answer;                 desc.sdp = incomingAnswer.payload.sdp;                  StartCoroutine(ConsumeAnswer(desc));             }         }     }     public void AddNewWsCandidate(string candidate)     {         dropOptions.Add(candidate);         wscandidates.GetComponent&lt;Dropdown>().ClearOptions();         wscandidates.GetComponent&lt;Dropdown>().AddOptions(dropOptions);         wscandidates.GetComponent&lt;Dropdown>().RefreshShownValue();     }      IEnumerator HandleCall(RTCSessionDescription desc)     {          webrtcConnection.OnIceCandidate = onIceCandidate;         webrtcConnection.OnIceConnectionChange = onIceConnectionChange;         webrtcConnection.OnDataChannel = onDataChannel;          var op = webrtcConnection.SetRemoteDescription(ref desc);         yield return op;          if (!op.IsError)         {             Debug.Log(\"Set Remote Description complete\");         }         else         {             var error = op.Error;             Debug.Log(\"ERROR Set Session Description: \" + error);         }          var op2 = webrtcConnection.CreateAnswer();         yield return op2;         if (!op2.IsError)         {             string remoteUuid = wscandidates.GetComponent&lt;Dropdown>().captionText.text;              var wsAnswer = new WSMessage&lt;Answer>             {                 userId = userId,                 type = \"ANSWER\",                 data = \"\",                 payload = new Answer(op2.Desc.sdp, \"answer\"),                 toUserId = remoteUuid             };              Debug.Log(\"CREATE ANSWER:\" + JsonUtility.ToJson(wsAnswer));              ws.Send(JsonUtility.ToJson(wsAnswer));              Debug.Log(\"SEND ANSWER:\" + JsonUtility.ToJson(wsAnswer));              yield return OnCreateAnswerSuccess(op2.Desc);         }         else         {             Debug.Log(\"ERROR Create Session Description:\" + op2.Error.message);         }      }      IEnumerator OnCreateAnswerSuccess(RTCSessionDescription desc)     {         var op = webrtcConnection.SetLocalDescription(ref desc);         yield return op;          if (!op.IsError)         {             Debug.Log(\"Set Local Description complete\");         }         else         {             var error = op.Error;             Debug.Log(\"ERROR Set Session Description: \" + error.message);         }     }      IEnumerator ConsumeAnswer(RTCSessionDescription desc)     {          var op = webrtcConnection.SetRemoteDescription(ref desc);         yield return op;          if (!op.IsError)         {             Debug.Log(\"Set Remote Description complete\");         }         else         {             var error = op.Error;             Debug.Log(\"ERROR Set Session Description: \" + error.message);         }      }      public void CallWebRTC()     {         StartCoroutine(Call());     }      IEnumerator Call()     {         webrtcConnection.OnIceCandidate = onIceCandidate;         webrtcConnection.OnIceConnectionChange = onIceConnectionChange;          var option = new RTCDataChannelInit();         dataChannel = webrtcConnection.CreateDataChannel(\"datachannel\", option);         dataChannel.OnOpen = onDataChannelOpen;         dataChannel.OnClose = onDataChannelClose;         webrtcConnection.OnDataChannel = onDataChannel;          var op = webrtcConnection.CreateOffer();         yield return op;          if (!op.IsError)         {              string remoteUuid = wscandidates.GetComponent&lt;Dropdown>().captionText.text;              var wsOffer = new WSMessage&lt;Offer>             {                 userId = userId,                 type = \"OFFER\",                 data = \"\",                 payload = new Offer(op.Desc.sdp, \"offer\"),                 toUserId = remoteUuid             };              Debug.Log(\"CREATE OFFER:\" + JsonUtility.ToJson(wsOffer));              ws.Send(JsonUtility.ToJson(wsOffer));              Debug.Log(\"SEND OFFER:\" + JsonUtility.ToJson(wsOffer));              yield return StartCoroutine(OnCreateOfferSuccess(op.Desc));         }         else         {             Debug.Log(\"ERROR Create Session Description: \" + op.Error);         }      }      IEnumerator OnCreateOfferSuccess(RTCSessionDescription desc)     {         var op = webrtcConnection.SetLocalDescription(ref desc);         yield return op;          if (!op.IsError)         {             Debug.Log(\"Set Local Description complete\");         }         else         {             var error = op.Error;             Debug.Log(\"ERROR Set Session Description: \" + error);         }      }      public void SendDirectMessage()     {         var message = \"Message from Unity \" + DateTime.Now;         if (dataChannel != null &amp;&amp; dataChannel.ReadyState == RTCDataChannelState.Open) dataChannel.Send(message);         if (remoteDataChannel != null &amp;&amp; remoteDataChannel.ReadyState == RTCDataChannelState.Open) remoteDataChannel.Send(message);     }      void OnIceConnectionChange(RTCPeerConnection pc, RTCIceConnectionState state)     {         switch (state)         {             case RTCIceConnectionState.New:                 Debug.Log(\"IceConnectionState: New\");                 break;             case RTCIceConnectionState.Checking:                 Debug.Log(\"IceConnectionState: Checking\");                 break;             case RTCIceConnectionState.Closed:                 Debug.Log(\"IceConnectionState: Closed\");                 break;             case RTCIceConnectionState.Completed:                 Debug.Log(\"IceConnectionState: Completed\");                 break;             case RTCIceConnectionState.Connected:                 Debug.Log(\"IceConnectionState: Connected\");                 break;             case RTCIceConnectionState.Disconnected:                 Debug.Log(\"IceConnectionState: Disconnected\");                 break;             case RTCIceConnectionState.Failed:                 Debug.Log(\"IceConnectionState: Failed\");                 break;             case RTCIceConnectionState.Max:                 Debug.Log(\"IceConnectionState: New\");                 break;             default:                 break;         }     }      void OnIceCandidate(RTCPeerConnection webrtcConnection, RTCIceCandidate candidate)     {         webrtcConnection.AddIceCandidate(candidate);         Debug.Log(\"ADDED local ICE candidate:\" + candidate.Candidate);         string remoteUuid = wscandidates.GetComponent&lt;Dropdown>().captionText.text;         var iceCand = new WSMessage&lt;Ice>         {             userId = userId,             type = \"ICE\",             data = \"\",             payload = new Ice(candidate.Candidate, (int)candidate.SdpMLineIndex, candidate.SdpMid, candidate.UserNameFragment),             toUserId = remoteUuid         };         ws.Send(JsonUtility.ToJson(iceCand));         Debug.Log(\"SEND ICE:\" + JsonUtility.ToJson(iceCand));     }      void RemoteIceCandidate(RTCPeerConnection webrtcConnection, RTCIceCandidate candidate)     {         webrtcConnection.AddIceCandidate(candidate);         Debug.Log(\"ADDED remote ICE candidate:\" + candidate.Candidate);     }      public void ConnectWebsocket()     {         ws.Connect();     }      public void DisconnectWebsocket()     {         ws.Close();     }      public void LoginWebsocket()     {         var hello = new WSMessage&lt;string>         {             userId = \"\",             type = \"LOGIN\",             data = \"\",             payload = \"\",             toUserId = \"\"         };         ws.Send(JsonUtility.ToJson(hello));     }      public void SendNewmember()     {         var newmember = new WSMessage&lt;string>         {             userId = userId,             type = \"NEWMEMBER\",             data = \"\",             payload = \"\",             toUserId = \"\"         };          ws.Send(JsonUtility.ToJson(newmember));     }      void SetUserId(string userId)     {         uuid.GetComponent&lt;UnityEngine.UI.Text>().text = userId;     }  }  [Serializable] public class WSMessage&lt;T> {     public string userId;     public string type;     public string data;     public T payload;     public string toUserId; }  [Serializable] public class Offer {     public string type;     public string sdp;      public Offer(string sdp, string type)     {         this.type = type;         this.sdp = sdp;     } }  [Serializable] public class Answer {     public string type;     public string sdp;     public Answer(string sdp, string type)     {         this.type = type;         this.sdp = sdp;     } }  [Serializable] public class Ice {     public string candidate;     public int sdpMLineIndex;     public string sdpMid;     public string usernameFragment;      public Ice(string candidate, int sdpMLineIndex, string sdpMid, string usernameFragment)     {         this.candidate = candidate;         this.sdpMLineIndex = sdpMLineIndex;         this.sdpMid = sdpMid;         this.usernameFragment = usernameFragment;     } }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u0414\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u043d\u0430\u0448\u0438 \u043c\u0435\u0442\u043e\u0434\u044b SendDirectMessage \u0438 CallWebRTC \u043d\u0430 \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0435 Button \u0438 \u043c\u043e\u0436\u0435\u043c \u043f\u0440\u0438\u0441\u0442\u0443\u043f\u0438\u0442\u044c \u043a \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0435.<\/p>\n<h4>\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u0432 <\/h4>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043c\u043e\u0436\u0435\u043c \u043f\u0440\u043e\u0432\u0435\u0441\u0442\u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u043c\u0435\u0436\u0434\u0443 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u043e\u043c \u0438 Unity \u0441 \u0434\u0432\u0443\u0445 \u0441\u0442\u043e\u0440\u043e\u043d \u0438 \u043c\u0435\u0436\u0434\u0443 Unity \u0438 Python \u0441\u043a\u0440\u0438\u043f\u0442\u043e\u043c. <\/p>\n<details class=\"spoiler\">\n<summary>\u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430:<\/summary>\n<div class=\"spoiler__content\">\n<p><strong>Unity \u2013 \u0411\u0440\u0430\u0443\u0437\u0435\u0440 (\u0436\u0435\u043b\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u0432 \u043e\u0431\u0435 \u0441\u0442\u043e\u0440\u043e\u043d\u044b)<\/strong><\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/b76\/a39\/e01\/b76a39e013b54f79592fced644918e97.png\" width=\"642\" height=\"429\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/b76\/a39\/e01\/b76a39e013b54f79592fced644918e97.png\"\/><\/figure>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/dd7\/bc0\/918\/dd7bc091874499fe08ba18be4940b322.png\" width=\"642\" height=\"281\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/dd7\/bc0\/918\/dd7bc091874499fe08ba18be4940b322.png\"\/><\/figure>\n<p><strong>Unity \u2013 Python:<\/strong><\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/085\/8ba\/c28\/0858bac28a1b4794266b69e426cf15d2.png\" width=\"642\" height=\"437\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/085\/8ba\/c28\/0858bac28a1b4794266b69e426cf15d2.png\"\/><\/figure>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/408\/892\/b66\/408892b66293e4b535a9fdba8610a6ea.png\" width=\"642\" height=\"342\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/408\/892\/b66\/408892b66293e4b535a9fdba8610a6ea.png\"\/><\/figure>\n<\/p>\n<\/div>\n<\/details>\n<p>\u0412\u0443\u0430\u043b\u044f, \u043c\u044b \u0432\u0438\u0434\u0438\u043c, \u0447\u0442\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0438 \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u044e\u0442\u0441\u044f, \u0430 \u044d\u0442\u043e \u0437\u043d\u0430\u0447\u0438\u0442 \u0447\u0442\u043e \u0443 \u043d\u0430\u0441 \u0437\u0430\u043a\u043e\u043d\u0447\u0438\u043b\u0430\u0441\u044c 3 \u0447\u0430\u0441\u0442\u044c.<\/p>\n<hr\/>\n<h2>\u0427\u0430\u0441\u0442\u044c 4. \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 WebRTC Media streaming<\/h2>\n<p>\u0412 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0445 \u0447\u0430\u0441\u0442\u044f\u0445 \u0443 \u043d\u0430\u0441 \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u043e\u0441\u044c \u0441\u0434\u0435\u043b\u0430\u0442\u044c datachannel \u043c\u0435\u0436\u0434\u0443 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u043e\u043c-python-unity, \u0442\u0435\u043f\u0435\u0440\u044c \u044d\u0442\u043e\u0442 \u043a\u043e\u0434 \u043f\u043e\u0440\u0430 \u043e\u0431\u043e\u0433\u0430\u0442\u0438\u0442\u044c \u043c\u0435\u0434\u0438\u0430\u043a\u043e\u043d\u0442\u0435\u043d\u0442\u043e\u043c.<\/p>\n<h3>\u0421\u0435\u0440\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442<\/h3>\n<p> \u0414\u043e\u0431\u0430\u0432\u0438\u043c \u0432 \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u0443\u044e \u0440\u0430\u0437\u043c\u0435\u0442\u043a\u0443 main.html \u043d\u043e\u0432\u044b\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u0434\u043b\u044f \u0432\u0438\u0434\u0435\u043e<\/p>\n<details class=\"spoiler\">\n<summary>main.html<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"xml\">... &lt;div class=\"row-md-6\">   &lt;form class=\"form-inline\">     &lt;button id=\"startStream\" class=\"btn btn-primary\" type=\"submit\">Start media streaming&lt;\/button>   &lt;\/form>   &lt;\/div>   &lt;div class=\"row-md-6\">     &lt;video autoplay id=\"myvideo\" width=\"320\" height=\"240\" controls>&lt;\/video>     &lt;video autoplay id=\"remoteVideo\" width=\"320\" height=\"240\" controls>&lt;\/video> &lt;\/div> ...<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p> \u0414\u043e\u0440\u0430\u0431\u043e\u0442\u0430\u0435\u043c \u043d\u0430\u0448 \u0441\u043a\u0440\u0438\u043f\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<details class=\"spoiler\">\n<summary>robo.js<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"javascript\">... const constraints = {   video: true, audio: true }; var videoStream; ...  \/\/ \u0414\u043e\u0431\u0430\u0432\u0438\u043c \u0432 function initRTCPeerConnection() \/\/ video handling const remoteVideo = document.getElementById('remoteVideo'); peerConnection.addEventListener('track', async (event) => {   const remoteStream = event.streams[0];   remoteVideo.srcObject = remoteStream;   console.log('Received remote stream'); });  \/\/ \u0414\u043e\u0431\u0430\u0432\u0438\u043c \u0441\u0442\u0430\u0440\u0442 \u043c\u0435\u0434\u0438\u0430 \u0441\u0442\u0440\u0438\u043c\u0438\u043d\u0433\u0430: function statrStream(){   navigator.mediaDevices.getUserMedia(constraints)   .then(stream => {     videoStream = stream;     stream.getTracks().forEach(track => {       peerConnection.addTrack(track, videoStream);       console.log(\"added track:\" + track);     });       console.log(\"sucsess started media stream\");     recreateOffer();   })   .catch(function(err){     console.log(\"errors when media stream\");   });   attachVideoToScreen(); }  function recreateOffer(){   peerConnection.createOffer(function(offer) {     connection.send(JSON.stringify({'userId' : userId, 'type' : 'OFFER', 'payload' : offer , 'toUserId' : $(\"#offerTo\").val()}));     peerConnection.setLocalDescription(offer);   },   function(error) {     console.log(\"error in offer creating:\" + error);   }); }  function attachVideoToScreen(){   const localvideo = document.getElementById('myvideo');   if (videoStream != null) {     localvideo.srcObject = videoStream;     localvideo.onloadedmetadata = () => {       localvideo.play();     }     console.log(\"sucsess attached media stream\");   } else {     setTimeout(attachVideoToScreen, 500);   } }  \/\/ \u041d\u0435 \u0437\u0430\u0431\u044b\u0432\u0430\u0435\u043c \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u043a\u043d\u043e\u043f\u043a\u0443 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0439: ... $( \"#startStream\" ).click(function() { statrStream(); }); ...<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u043c USB-\u043a\u0430\u043c\u0435\u0440\u0443 \u043a \u043a\u043e\u043c\u043f\u044c\u044e\u0442\u0435\u0440\u0443 \u0438 \u043c\u043e\u0436\u0435\u043c \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0440\u0430\u0431\u043e\u0442\u044b \u0432\u0438\u0434\u0435\u043e\/\u0430\u0443\u0434\u0438\u043e \u043c\u0435\u0436\u0434\u0443 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430\u043c\u0438 \u0432 \u043e\u0431\u0435 \u0441\u0442\u043e\u0440\u043e\u043d\u044b, \u044f \u0434\u0435\u043b\u0430\u044e \u043c\u0435\u0436\u0434\u0443 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u043e\u043c \u043a\u043e\u043c\u043f\u044c\u044e\u0442\u0435\u0440\u0430 \u0438 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0430 \u0438 \u0432\u0438\u0434\u0438\u043c, \u0447\u0442\u043e \u0432\u0441\u0435 \u0438\u0434\u0435\u0442 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.<\/p>\n<h3>\u0418\u0441\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442(Python-\u0441\u043a\u0440\u0438\u043f\u0442 \u043d\u0430 RPI)<\/h3>\n<p>\u041f\u0440\u0438 \u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0438 offer \u043e\u0442 \u0423\u043f\u0440\u0430\u0432\u043b\u044f\u044e\u0449\u0435\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0432\u0438\u0434\u0435\u043e \u0441 USB-\u043a\u0430\u043c\u0435\u0440\u044b \u0438 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0432\u0438\u0434\u0435\u043e\u0442\u0440\u0430\u043a, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043a webrtc-connection. \u0412 \u043f\u0430\u043a\u0435\u0442\u0435 aiortc \u0434\u043b\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u043c\u0435\u0434\u0438\u0430 \u0435\u0441\u0442\u044c \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0435 <a href=\"https:\/\/aiortc.readthedocs.io\/en\/latest\/helpers.html\" rel=\"noopener noreferrer nofollow\">\u0445\u0435\u043b\u043f\u0435\u0440\u044b<\/a>. \u0420\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u043c \u0438\u0445 \u0432 \u043d\u0430\u0448\u0435\u043c Python \u0441\u043a\u0440\u0438\u043f\u0442\u0435:<\/p>\n<details class=\"spoiler\">\n<summary>part3.py<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"python\">## \u0434\u043e\u0431\u0430\u0432\u0438\u043c 2 \u043c\u0435\u0442\u043e\u0434\u0430: def force_codec(pc, sender, forced_codec):     kind = forced_codec.split(\"\/\")[0]     codecs = RTCRtpSender.getCapabilities(kind).codecs     transceiver = next(t for t in pc.getTransceivers() if t.sender == sender)     transceiver.setCodecPreferences(         [codec for codec in codecs if codec.mimeType == forced_codec]     )   async def add_video(peer_conn) -> None:     options = {\"framerate\": \"30\", \"video_size\": \"640x480\", \"preset\": \"fast\"}     relay_v = MediaRelay()     webcam = MediaPlayer(\"\/dev\/video0\", format=\"v4l2\", options=options)     video_sender = peer_conn.addTrack(relay_v.subscribe(webcam.video, buffered=False))     force_codec(peer_conn, video_sender, \"video\/H264\")  ## \u0418 \u0432 \u0440\u0430\u0437\u0434\u0435\u043b \u0445\u0435\u043d\u0434\u043b\u0438\u043d\u0433\u0430 \u043e\u0444\u0444\u0435\u0440\u0430 \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u0432\u044b\u0437\u043e\u0432 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0432\u0438\u0434\u0435\u043e \u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0443 \u043e\u0444\u0444\u0435\u0440\u0430 \u0441 \u043d\u043e\u0432\u044b\u043c \u043a\u0430\u043d\u0434\u0438\u0434\u0430\u0442\u043e\u043c.              await add_video(peer_conn)                          offer = await peer_conn.createOffer()             await peer_conn.setLocalDescription(offer)             await websocket.send(json.dumps({\"userId\": local_user_id, \"type\": \"OFFER\", \"data\": \"\",                                              \"payload\": {\"sdp\": offer.sdp, \"type\": offer.type},                                              \"toUserId\": msg.get(\"userId\")}))<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u0425\u0435\u043b\u043f\u0435\u0440\u044b \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u044b \u043d\u0430 ffmpeg \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0432 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043e\u043f\u0446\u0438\u0439 \u043c\u044b \u043c\u043e\u0436\u0435\u043c \u0432\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 \u043e\u043f\u0446\u0438\u0438 \u0434\u043b\u044f \u0443\u0441\u043a\u043e\u0440\u0435\u043d\u0438\u044f ffmpeg, \u044d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043e\u0447\u0435\u043d\u044c \u043f\u043e\u043b\u0435\u0437\u043d\u043e \u0434\u043b\u044f \u0441\u043d\u0438\u0436\u0435\u043d\u0438\u044f \u0437\u0430\u0434\u0435\u0440\u0436\u0435\u043a \u0432\u0438\u0434\u0435\u043e.<\/p>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043c\u043e\u0436\u0435\u043c \u043f\u0440\u043e\u0432\u0435\u0441\u0442\u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0440\u0430\u0431\u043e\u0442\u044b \u043c\u0435\u0436\u0434\u0443 Python \u0438 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u043e\u043c. \u0415\u0441\u043b\u0438 \u0432\u0434\u0440\u0443\u0433 \u043c\u0435\u0434\u0438\u0430 \u0442\u0440\u0430\u043a\u0438 \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u044e\u0442\u0441\u044f \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435, \u043d\u043e \u0432\u0438\u0434\u0435\u043e \u043d\u0435 \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0441\u044f &#8212; \u0442\u043e \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u043c \u0445\u0440\u043e\u043c, \u0442\u0430\u043a \u0436\u0435 \u043c\u043e\u0436\u0435\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u043e\u043c &#8212; <em>chrome:\/\/webrtc-internals\/<\/em><\/p>\n<details class=\"spoiler\">\n<summary>\u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430:<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/fe4\/4ce\/983\/fe44ce9833d8d108f34eeeeaab789576.png\" width=\"642\" height=\"339\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/fe4\/4ce\/983\/fe44ce9833d8d108f34eeeeaab789576.png\"\/><\/figure>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/978\/22b\/7a1\/97822b7a1a3169eb30fbfc1f9af418d5.png\" width=\"642\" height=\"335\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/978\/22b\/7a1\/97822b7a1a3169eb30fbfc1f9af418d5.png\"\/><\/figure>\n<\/p>\n<\/div>\n<\/details>\n<h3>\u0423\u043f\u0440\u0430\u0432\u043b\u044f\u044e\u0449\u0438\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442(Unity VR)<\/h3>\n<p>\u0421 \u044d\u0442\u043e\u0439 \u0447\u0430\u0441\u0442\u044c\u044e \u0432\u0441\u0435 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u043b\u043e\u0436\u043d\u0435\u0435, \u0434\u043b\u044f \u043d\u0430\u0447\u0430\u043b\u0430 \u043c\u044b \u0441\u0434\u0435\u043b\u0430\u0435\u043c panel \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0432\u0438\u0434\u0435\u043e, \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043f\u0440\u043e\u0441\u0442\u043e \u043f\u0440\u043e\u0434\u0443\u0431\u043b\u0438\u0440\u0443\u0435\u043c \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0439 \u0440\u0430\u043d\u0435\u0435 Connection Panel, \u043f\u0435\u0440\u0435\u0438\u043c\u0435\u043d\u0443\u0435\u043c \u0435\u0433\u043e \u0432 Robo Video Panel, \u0443\u0434\u0430\u043b\u0438\u043c \u0438\u0437 \u043d\u0435\u0433\u043e \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438 \u043f\u043e\u043c\u0435\u0441\u0442\u0438\u043c \u0432 \u043d\u0435\u0433\u043e 2 \u043d\u043e\u0432\u044b\u0445 \u043e\u0431\u044a\u0435\u043a\u0442\u0430:<\/p>\n<ul>\n<li>\n<p>Raw Image &#8212; \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0432\u0438\u0434\u0435\u043e \u0438\u0437 \u0432\u0438\u0434\u0435\u043e\u0442\u0440\u0435\u043a\u0430, \u0441\u0442\u0430\u0432\u0438\u043c \u0442\u044d\u0433 \u00abremoteimage\u00bb<\/p>\n<\/li>\n<li>\n<p>Audiosource &#8212; \u0434\u043b\u044f \u0430\u0442\u0442\u0430\u0447\u0430 \u0430\u0443\u0434\u0438\u043e \u0438\u0437 \u0430\u0443\u0434\u0438\u043e\u0442\u0440\u0435\u043a\u0430 webrtc, \u0441\u0442\u0430\u0432\u0438\u043c \u0442\u044d\u0433 \u00abreceivedaudiosource\u00bb(\u044d\u0442\u043e \u043d\u0430 \u0431\u0443\u0434\u0443\u0449\u0435\u0435)<\/p>\n<\/li>\n<\/ul>\n<details class=\"spoiler\">\n<summary>\u0412\u043e\u0442 \u0442\u0430\u043a:<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/193\/19c\/2d6\/19319c2d6d148dc92b7d4780cf6d96f3.png\" width=\"642\" height=\"298\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/193\/19c\/2d6\/19319c2d6d148dc92b7d4780cf6d96f3.png\"\/><\/figure>\n<\/p>\n<\/div>\n<\/details>\n<p>\u0438 \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u0441\u0430\u043c\u043e \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0432\u0438\u0434\u0435\u043e\/\u0430\u0443\u0434\u0438\u043e \u0438\u0437 \u0442\u0440\u0430\u043a\u043e\u0432:<\/p>\n<details class=\"spoiler\">\n<summary>Connection.cs:<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cs\">... private RawImage remoteImage; private DelegateOnTrack webrtc\u0421onnOntrack; private AudioSource receivedAudioSource; ...  \/\/ \u0410\u0442\u0442\u0430\u0447\u0438\u043c \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u043f\u043e \u0442\u0435\u0433\u0430\u043c \u0432 Start \u043c\u0435\u0442\u043e\u0434\u0435 \u0438 \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u0434\u0435\u043b\u0430\u0433\u0430\u0442 \u043d\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0442\u0440\u0430\u043a\u043e\u0432: void Start() \u2026. remoteImage = GameObject.FindGameObjectWithTag(\"remoteimage\").GetComponent&lt;RawImage>(); receivedAudioSource = GameObject.FindGameObjectWithTag(\"receivedAudioSource\").GetComponent&lt;AudioSource>(); \u2026. webrtc\u0421onnOntrack = e =>         {             if (e != null)             {                 Debug.Log(\"NEW TRACK Recived: \" + e.Track.Id);             }             if (e.Track is VideoStreamTrack video)             {                 video.OnVideoReceived += tex =>                 {                     remoteImage.texture = tex;                 };             }             if (e.Track is AudioStreamTrack track)             {                 receivedAudioSource.SetTrack(track);                 receivedAudioSource.loop = true;                 receivedAudioSource.Play();             }         };  \/\/ \u0422\u0435\u043f\u0435\u0440\u044c \u0432 \u0445\u0435\u043d\u0434\u043b\u0438\u043d\u0433\u0435 \u043e\u0444\u0444\u0435\u0440\u0430 \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u043d\u0430 OnTrack \u0434\u0435\u043b\u0435\u0433\u0430\u0442 webrtc\u0421onnOntrack IEnumerator HandleCall(RTCSessionDescription desc) {     webrtcConnection.OnTrack = webrtc\u0421onnOntrack; ... \/\/ \u0418 \u043e\u0447\u0435\u043d\u044c \u0432\u0430\u0436\u043d\u044b\u0439 \u043f\u0443\u043d\u043a\u0442 \u043f\u043e\u0441\u043b\u0435 \u0445\u0435\u043d\u0434\u043b\u0438\u043d\u0433\u0430 \u043e\u0444\u0444\u0435\u0440\u0430 \u2014 \u043f\u0440\u043e\u0432\u0435\u0441\u0442\u0438 WebRTC.Update(). ... StartCoroutine(WebRTC.Update()); } <\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>PS: \u0442\u0430\u043a \u0436\u0435 \u043c\u043e\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0438 \u0442\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u044e \u0441 \u043a\u0430\u043c\u0435\u0440\u044b \u0438\u0437 Unity \u0432 \u043c\u0435\u0434\u0438\u0430 \u0441\u0442\u0440\u0438\u043c \u0435\u0441\u043b\u0438 \u0445\u043e\u0447\u0435\u0442\u0441\u044f \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435, \u0447\u0442\u043e \u0434\u0435\u043b\u0430\u0435\u0442\u0441\u044f \u0432 Unity:<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cs\">MediaStream videoStream = customCamera.CaptureStream(1280, 720); MediaStreamTrack trackLocal = videoStream.GetVideoTracks().First(); webrtcConnection.AddTrack(track, videoStream); Debug.Log(\"ADDED OUTGOING TRACK:\" + trackLocal.Id);<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<h4>\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u0432<\/h4>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u043c \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u0432, \u0441\u043d\u0430\u0447\u0430\u043b\u0430 Unity-\u0431\u0440\u0430\u0443\u0437\u0435\u0440, \u043f\u043e\u0442\u043e\u043c Unity-Python, \u043a\u0430\u043a \u0432\u0438\u0434\u0438\u043c \u043e\u0431\u043c\u0435\u043d \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043e\u043c \u0443\u0441\u043f\u0435\u0448\u0435\u043d \u0438 \u0432\u0438\u0434\u0435\u043e \u0441 \u043a\u0430\u043c\u0435\u0440\u044b \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044f \u0432 Unity.<\/p>\n<details class=\"spoiler\">\n<summary>\u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430:<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/949\/cc2\/c83\/949cc2c83098cda8bf040c5e822f4c7f.png\" width=\"642\" height=\"368\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/949\/cc2\/c83\/949cc2c83098cda8bf040c5e822f4c7f.png\"\/><\/figure>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/89d\/05f\/923\/89d05f923cc08b8f917c49934bd3ec7c.png\" width=\"633\" height=\"426\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/89d\/05f\/923\/89d05f923cc08b8f917c49934bd3ec7c.png\"\/><\/figure>\n<p><strong>Unity \u2013 Python:<\/strong><\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/71a\/e31\/b4a\/71ae31b4af84c5381ba1287f8b576ed6.png\" width=\"614\" height=\"431\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/71a\/e31\/b4a\/71ae31b4af84c5381ba1287f8b576ed6.png\"\/><\/figure>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/1b2\/a28\/595\/1b2a28595aa1cac3887b40e22bdc7e41.png\" width=\"633\" height=\"330\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/1b2\/a28\/595\/1b2a28595aa1cac3887b40e22bdc7e41.png\"\/><\/figure>\n<\/p>\n<\/div>\n<\/details>\n<p><strong>PS:<\/strong> \u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043b \u0432\u0438\u0434\u0435\u043e\u043a\u0430\u043c\u0435\u0440\u0443, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043f\u043e \u0438\u0434\u0435\u0435 \u0434\u043e\u043b\u0436\u043d\u0430 \u0432\u044b\u0434\u0430\u0432\u0430\u0442\u044c \u043c\u0435\u0434\u0438\u0430 \u0441 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u0435\u043c 1280\u0445720 30fps, \u043e\u0434\u043d\u0430\u043a\u043e \u043f\u043e \u0444\u0430\u043a\u0442\u0443 \u043e\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u043d\u0430 640\u0445480, \u0442.\u043a. \u0434\u0430\u0436\u0435 \u0441 \u043e\u043f\u0442\u0438\u043c\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c\u0438 \u043d\u0430 ffmpeg \u043c\u043d\u0435 \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u0440\u0435\u0436\u0438\u043c\u0430 \u0441 \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0439 \u0437\u0430\u0434\u0435\u0440\u0436\u043a\u043e\u0439 \u0432\u0438\u0434\u0435\u043e. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u0435\u043b\u043e \u0432 \u043a\u0430\u043c\u0435\u0440\u0435, \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0432 \u043a\u0440\u0438\u0432\u044b\u0445 \u0440\u0443\u043a\u0430\u0445.<\/p>\n<p><code>options = {\"framerate\": \"25\", \"input_format\": \"mjpeg\", \"video_size\": \"1280x720\", \"preset\": \"ultrafast\", \"tune\":\"zerolatency\"}<\/code><\/p>\n<p><strong>PS2:<\/strong> \u0441\u0443\u0434\u044f \u043f\u043e \u0438\u043d\u0444\u043e \u043f\u043e aiortc \u0441 \u043a\u0430\u043c\u0435\u0440\u044b \u0442\u0430\u043a \u0436\u0435 \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0430\u0443\u0434\u0438\u043e\u043f\u043e\u0442\u043e\u043a, \u043e\u0434\u043d\u0430\u043a\u043e \u0443 \u043c\u0435\u043d\u044f \u044d\u0442\u043e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0445\u0435\u043b\u043f\u0435\u0440\u0430 MediaPlayer() \u0441 alsa \u043d\u0435 \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u043e\u0441\u044c, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u044f \u043e\u0441\u0442\u0430\u0432\u0438\u043b \u044d\u0442\u043e\u0442 \u0432\u043e\u043f\u0440\u043e\u0441.<\/p>\n<p>\u041e\u0447\u0435\u0440\u0435\u0434\u043d\u0430\u044f \u0447\u0430\u0441\u0442\u044c \u0437\u0430\u043a\u043e\u043d\u0447\u0435\u043d\u0430, \u043f\u043e\u0440\u0430 \u043f\u0440\u0438\u0441\u0442\u0443\u043f\u0438\u0442\u044c \u043a \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f.<\/p>\n<p>\u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0435\u043d\u0438\u0435 \u0432 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u2013 <a href=\"https:\/\/habr.com\/ru\/articles\/730522\/\" rel=\"noopener noreferrer nofollow\">\u0423\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c \u0440\u043e\u0431\u043e\u0442\u0430\u043c\u0438 \u0438\u0437 VR. \u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0435\u043d\u0438\u0435 2<\/a><\/p>\n<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/730450\/\"> https:\/\/habr.com\/ru\/articles\/730450\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u041f\u0435\u0440\u0432\u0430\u044f \u0441\u0442\u0430\u0442\u044c\u044f \u2013 <a href=\"https:\/\/habr.com\/ru\/articles\/730164\/\" rel=\"noopener noreferrer nofollow\">\u0423\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c \u0440\u043e\u0431\u043e\u0442\u0430\u043c\u0438 \u0438\u0437 VR<\/a><\/p>\n<p>\u0412 \u043f\u0440\u043e\u0448\u043b\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435, \u043c\u044b \u043f\u0440\u043e\u0432\u0435\u043b\u0438 \u043d\u0430\u0447\u0430\u043b\u044c\u043d\u0443\u044e \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043a\u0443 \u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043b\u0438 \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e \u0434\u043b\u044f \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u0432 \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430\u043c\u0438 websocket. \u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u043c\u044b \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u043c \u0440\u0430\u0431\u043e\u0442\u0443 \u043f\u043e WebRTC (\u0427\u0430\u0441\u0442\u0438 3, 4).<\/p>\n<h2>\u0427\u0430\u0441\u0442\u044c 3. \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 WebRTC Connection + DataChannel  <\/h2>\n<p>\u0418\u0442\u0430\u043a, \u0443 \u043d\u0430\u0441 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d \u0441\u0435\u0440\u0432\u0435\u0440 \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043c\u044b \u0441\u043c\u043e\u0436\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043b\u044f \u043e\u0431\u043c\u0435\u043d\u0430 \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043e\u043c WebRTC &#8212; offer, answer, ice, \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u043f\u0440\u043e webrtc \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0447\u0438\u0442\u0430\u0442\u044c \u0432 <a href=\"https:\/\/habr.com\/ru\/companies\/timeweb\/articles\/656947\/\" rel=\"noopener noreferrer nofollow\">\u0442\u0443\u0442<\/a>, \u043d\u0435 \u0431\u0443\u0434\u0435\u043c \u043e\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0442\u044c\u0441\u044f \u043d\u0430 \u0434\u0435\u0442\u0430\u043b\u044f\u0445.<\/p>\n<p>\u0412 \u044d\u0442\u043e\u0439 \u0447\u0430\u0441\u0442\u0438 \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u044b\u0432\u0430\u0442\u044c \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e WebRTC \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f, \u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 DataChannel \u043c\u0435\u0436\u0434\u0443 2\u043c\u044f \u043f\u0438\u0440\u0430\u043c\u0438 \u0432 JS-Python-C#.<\/p>\n<h3>\u0421\u0435\u0440\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442<\/h3>\n<p>\u0414\u043e\u0431\u0430\u0432\u0438\u043c \u0432 \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u0443\u044e \u0440\u0430\u043d\u0435\u0435 \u0440\u0430\u0437\u043c\u0435\u0442\u043a\u0443 main.html \u043d\u043e\u0432\u044b\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b:<\/p>\n<details class=\"spoiler\">\n<summary>main.html<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"xml\">...   &lt;div class=\"row-md-6\">     &lt;form class=\"form-inline\">       &lt;div class=\"form-group\">         &lt;label for=\"content\">Send offer to:&lt;\/label>         &lt;input type=\"text\" id=\"offerTo\" class=\"form-control\" placeholder=\"Offer to...\">       &lt;\/div>       &lt;button id=\"createOffer\" class=\"btn btn-primary\" type=\"submit\">Create Offer&lt;\/button>     &lt;\/form>   &lt;\/div>   &lt;div class=\"row-md-6\">     &lt;form class=\"form-inline\">       &lt;div class=\"form-group\">         &lt;label for=\"content\">Message directly:&lt;\/label>         &lt;input type=\"text\" id=\"content2\" class=\"form-control\" placeholder=\"Message here...\">       &lt;\/div>       &lt;button id=\"sendMessageDirectly\" class=\"btn btn-primary\" type=\"submit\">Directly message&lt;\/button>     &lt;\/form>   &lt;\/div> ...<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u0414\u043b\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 datachannel \u043d\u0430 \u0441\u0442\u043e\u0440\u043e\u043d\u0435 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430 \u0434\u043e\u0440\u0430\u0431\u043e\u0442\u0430\u0435\u043c \u043d\u0430\u0448 \u0441\u043a\u0440\u0438\u043f\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c \u2013 \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u043e\u0431\u043c\u0435\u043d\u0430 sdp\/ice \u0434\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0438 \u043f\u0440\u0438\u0435\u043c\u0430 OFFER. \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 DataCh\u0430annel \u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0443 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u043c\u0435\u0436\u0434\u0443 \u043f\u0438\u0440\u0430\u043c\u0438:<\/p>\n<details class=\"spoiler\">\n<summary>robo.js<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"javascript\">var connection; var userId = 'unknown'; var peerConnection; var dataChannel; var configuration = {   \"iceServers\" : [{   \"urls\" : \"stun:stun2.1.google.com:19302\"   }] };  function connect(){   connection = new WebSocket('wss:\/\/' + window.location.host + '\/robopi_webrtc');   console.log(\"Connsection sucsess\");      initRTCPeerConnection();      connection.onmessage = function(msg) {          var resp = JSON.parse(msg.data);            if(resp.type == 'USERID'){       console.log();       userId = resp.data;       document.getElementById(\"username\").textContent = userId;     }     if(resp.type == 'NEWMEMBER'){       if(userId != resp.userId){         console.log(\"NEWMEMDER:\" + resp.userId);       }     }     if(resp.type == 'OFFER'){       if(userId != resp.userId){         console.log(resp);         handleOffer(resp.payload, resp.userId)       }     }     if(resp.type == 'ICE'){       if(userId != resp.userId){         console.log(resp);         receiveIceCandidate(resp.payload);       }     }     if(resp.type == 'ANSWER'){       if(userId != resp.userId){         console.log(resp);         handleAnswer(resp.payload);       }     }   } }  function login() {   connection.send(JSON.stringify({'userId' : '', 'type' : 'LOGIN', 'data' : '' , 'toUserId' : ''})); }  function newmember() {   connection.send(JSON.stringify({'userId' : userId, 'type' : 'NEWMEMBER', 'data' : '' , 'toUserId' : ''})); }  function initRTCPeerConnection(){   peerConnection = new RTCPeerConnection(configuration);   console.log(\"peerConnection created\");   \/\/ remote datachannel handler   peerConnection.ondatachannel = function(event){     dataChannel = event.channel;     \/\/ open handling     dataChannel.onopen = function(){     console.log(\"Data channel is open!\");     }     \/\/ error handling     dataChannel.onerror = function(error){       console.log(\"Error in datachannel:\", error);     };     \/\/ messaging handler     dataChannel.onmessage = function(event) {       console.log(\"incoming message:\", event.data);     };     \/\/ closing handler     dataChannel.onclose = function() {       console.log(\"Data channel is closed\");     };   }; }  function createRTCDatachannel(){   dataChannel = peerConnection.createDataChannel(\"datachannel\", {      reliable: true    });   \/\/ open handling   dataChannel.onopen = function(){     console.log(\"Data channel is open!\");   }   \/\/ error handling   dataChannel.onerror = function(error){     console.log(\"Error in datachannel:\", error);   };   \/\/ messaging handler   dataChannel.onmessage = function(event) {     console.log(\"incoming message:\", event.data);   };   \/\/ closing handler   dataChannel.onclose = function() {     console.log(\"Data channel is closed\");   }; }  function createOffer(){   createRTCDatachannel();   peerConnection.createOffer(function(offer) {     connection.send(JSON.stringify({'userId' : userId, 'type' : 'OFFER', 'payload' : offer , 'toUserId' : $(\"#offerTo\").val()}));     peerConnection.setLocalDescription(offer);   },   function(error) {     console.log(\"error in offer creating:\" + error);   });   sendIceCandidate(); }  function sendIceCandidate(){   peerConnection.onicecandidate = function(event) {   if (event.candidate) {     console.log(\"sending ice candidate:\" + event.candidate);     connection.send(JSON.stringify({'userId' : userId, 'type' : 'ICE', 'payload' : event.candidate , 'toUserId' : $(\"#offerTo\").val()}));   }   }; }  function receiveIceCandidate(candidate){   peerConnection.addIceCandidate(new RTCIceCandidate(candidate));   console.log(\"sucsess receiving ice candidate\"); }  function handleAnswer(answer){   peerConnection.setRemoteDescription(new RTCSessionDescription(answer));   console.log(\"handling amswer successfully!!\"); }  function handleOffer(offer, fromUser) {   peerConnection.setRemoteDescription(new RTCSessionDescription(offer));   \/\/ create and send an answer to an offer   peerConnection.createAnswer(function(answer) {     peerConnection.setLocalDescription(answer);     connection.send(JSON.stringify({'userId' : userId, 'type' : 'ANSWER', 'payload' : answer , 'toUserId' : fromUser}));   }, function(error) {     console.log(\"error creating answer:\" + error);   }); };  function sendMessageDirectly(){   var message = $(\"#content2\").val();   dataChannel.send(message);   console.log(\"send message:\" + message); }  $(function () {   $(\"form\").on('submit', function (e) {   e.preventDefault();   });    $( \"#connect\" ).click(function() { connect(); }); $( \"#login\" ).click(function() { login(); }); $( \"#newmember\" ).click(function() { newmember(); }); $( \"#createOffer\" ).click(function() { createOffer(); }); $( \"#sendMessageDirectly\" ).click(function() { sendMessageDirectly(); }); });<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u0414\u0435\u043b\u0430\u0435\u043c \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u043c\u0435\u0436\u0434\u0443 2\u043c\u044f \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430\u043c\u0438, \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u043c \u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u043e\u0441\u0442\u044c, \u0443 \u043d\u0430\u0441 \u0434\u043e\u043b\u0436\u0435\u043d \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c\u0441\u044f datachannel \u0438 \u043c\u044b \u043c\u043e\u0436\u0435\u043c \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0442\u044c \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043c\u0435\u0436\u0434\u0443 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430\u043c\u0438:<\/p>\n<details class=\"spoiler\">\n<summary>\u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430:<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><\/figure>\n<figure class=\"full-width\"><\/figure>\n<\/p>\n<\/div>\n<\/details>\n<h3>\u0418\u0441\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442(Python-\u0441\u043a\u0440\u0438\u043f\u0442 \u043d\u0430 RPI)<\/h3>\n<p>\u0414\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441  WebRTC \u0438\u0437 \u043f\u0430\u0439\u0442\u043e\u043d \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0431\u0438\u0431\u0438\u043b\u0438\u043e\u0442\u0435\u043a\u0443 aiortc:<\/p>\n<p> <code>pip3 install aiortc<\/code><\/p>\n<p>\u0422\u0443\u0442 \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u044e\u0442 \u043f\u0435\u0440\u0432\u044b\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 \u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u043e\u0441\u0442\u044c\u044e \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0439 WebRTC (ICE) \u0438 \u0443\u0447\u0438\u0442\u044b\u0432\u0430\u0435\u043c \u043e\u0441\u043e\u0431\u0435\u043d\u043d\u043e\u0441\u0442\u044c \u2013 Python \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u044f\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u0438\u043d\u0438\u0446\u0438\u0430\u0442\u043e\u0440\u043e\u043c \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f, \u0442.\u00a0\u0435. \u043e\u043d \u0434\u043e\u043b\u0436\u0435\u043d \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0442\u044c \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u043e\u0444\u0444\u0435\u0440\u044b:<\/p>\n<details class=\"spoiler\">\n<summary>part2.py<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"python\">import asyncio import websockets import json import ssl from websockets import WebSocketClientProtocol from aiortc import RTCIceCandidate, RTCPeerConnection, RTCSessionDescription, RTCConfiguration, RTCIceServer  async def wsconsume(wsurl: str) -> None:     ssl_context = ssl.SSLContext()     async with websockets.connect(wsurl, ssl=ssl_context) as websocket:         await websocket.send(json.dumps({\"userId\": \"\", \"type\": \"LOGIN\", \"data\": \"\", \"payload\": \"\", \"toUserId\": \"\"}))         await wsconsumer_handler(websocket)   async def wsconsumer_handler(websocket: WebSocketClientProtocol) -> None:     local_user_id = \"\"     ice_servers = [RTCIceServer(urls=[\"stun:stun2.l.google.com:19302\"])]     peer_conn = RTCPeerConnection(RTCConfiguration(iceServers=ice_servers))          @peer_conn.on(\"connectionstatechange\")     async def on_connectionstatechange():         print(\"Connection state is %s\" % peer_conn.connectionState)         if peer_conn.connectionState == \"failed\":             await peer_conn.close()      @peer_conn.on(\"signalingstatechange\")     async def on_signalingstatechange():         print(f\"changed signalingstatechange {peer_conn.signalingState}\")      @peer_conn.on(\"icegatheringstatechange\")     async def on_icegatheringstatechange():         print(f\"changed icegatheringstatechange {peer_conn.iceGatheringState}\")              @peer_conn.on(\"datachannel\")     async def on_datachannel(channel):         print(f\"changed datachannel to {channel}\")         @channel.on(\"message\")         async def on_message(rtc_message):             if isinstance(rtc_message, str):                 print(\"New message from datachannel \" + rtc_message)                 channel.send(\"Reply from PyPi - \" + rtc_message)          async for message in websocket:          msg = json.loads(message)          if msg.get(\"type\") == 'USERID' and local_user_id != msg.get(\"userId\"):             local_user_id = msg.get(\"data\")             print(\"SET UID: \" + local_user_id)             await websocket.send(json.dumps({\"userId\": local_user_id, \"type\": \"NEWMEMBER\", \"data\": \"\",                                              \"payload\": \"\", \"toUserId\": \"\"}))          if msg.get(\"type\") == 'OFFER' and local_user_id == msg.get(\"toUserId\"):             print(\"Handling offer: \" + str(msg.get(\"payload\")))                          await peer_conn.setRemoteDescription(                 RTCSessionDescription(sdp=msg.get(\"payload\").get(\"sdp\"), type=msg.get(\"payload\").get(\"type\")))                           answer = await peer_conn.createAnswer()             print(\"Creating answer:\" + str(answer))             await peer_conn.setLocalDescription(answer)              await websocket.send(json.dumps({\"userId\": local_user_id, \"type\": \"ANSWER\", \"data\": \"\",                                              \"payload\": {\"sdp\": answer.sdp, \"type\": answer.type},                                              \"toUserId\": msg.get(\"userId\")}))                      if msg.get(\"type\") == 'ICE' and local_user_id == msg.get(\"toUserId\"):             print(\"ICE INCOMING\")             candidate = msg.get(\"payload\").get(\"candidate\").split()             new_ice =<\/code><\/pre>\n<\/div>\n<\/details>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-386901","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/386901","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=386901"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/386901\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=386901"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=386901"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=386901"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}