{"id":330041,"date":"2022-02-23T21:00:20","date_gmt":"2022-02-23T21:00:20","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=330041"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=330041","title":{"rendered":"<span>\u041f\u0435\u0440\u0432\u0430\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0441\u0435\u0431\u044f \u0432 WEB \u0438\u043b\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0430 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u0434\u0438\u0441\u0442\u0430\u043d\u0446\u0438\u043e\u043d\u043d\u043e\u0433\u043e \u043e\u0431\u0443\u0447\u0435\u043d\u0438\u044f, \u0447\u0430\u0441\u0442\u044c II<\/span>"},"content":{"rendered":"<div><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u0412 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0435\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u0442\u0443\u0442\u043e\u0440\u0438\u0430\u043b\u0430 \u044f \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u043b \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u044b \u0432\u0438\u0434\u0435\u043e\u0441\u0432\u044f\u0437\u0438 \u0438 \u0441\u043a\u0430\u0437\u0430\u043b, \u0447\u0442\u043e \u043e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043b\u0441\u044f \u043d\u0430 webRTC. \u0420\u0430\u0441\u0441\u043a\u0430\u0437\u0430\u043b \u043a\u0430\u043a \u043e\u043d \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0438 \u0440\u0430\u0441\u0441\u043a\u0430\u0437\u0430\u043b \u0442\u0435\u043e\u0440\u0435\u0442\u0438\u0447\u0435\u0441\u043a\u0443\u044e \u0435\u0433\u043e \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e. \u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u044f \u043e\u043f\u0438\u0448\u0443 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0441\u0430\u043c\u043e\u0433\u043e \u0432\u0438\u0434\u0435\u043e\u0447\u0430\u0442\u0430 \u0438 \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0443 \u043a\u043e\u0434, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u043d\u0430 GitHub.<\/p>\n<h2>\u0417\u0430\u0434\u0443\u043c\u043a\u0430<\/h2>\n<p>\u041a\u043e\u0433\u0434\u0430 \u044f \u0443\u0436\u0435 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u043b\u0441\u044f \u0441\u043e \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043e\u043c \u0432\u0438\u0434\u0435\u043e\u0441\u0432\u044f\u0437\u0438, \u0442\u043e \u043d\u0430\u0447\u0430\u043b \u0434\u0443\u043c\u0430\u0442\u044c \u043d\u0430 \u043a\u0430\u043a\u043e\u043c \u044f\u0437\u044b\u043a\u0435 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u0430\u043c \u0447\u0430\u0442. \u041d\u0435 \u0434\u043e\u043b\u0433\u043e \u0434\u0443\u043c\u0430\u044f \u044f \u0432\u044b\u0431\u0440\u0430\u043b \u0434\u0432\u0430 \u044f\u0437\u044b\u043a\u0430: JavaScript \u0434\u043b\u044f \u0441\u0430\u0439\u0442\u0430 \u0438 \u0432\u0438\u0434\u0435\u043e\u0447\u0430\u0442\u0430. <\/p>\n<p>\u0412 JavaScript \u0435\u0441\u0442\u044c \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 React, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0441\u0430\u0439\u0442. \u0412\u044b\u0431\u0440\u0430\u043b \u044f \u043d\u0435 \u0431\u0435\u0437\u0434\u0443\u043c\u043d\u043e, \u0432\u0435\u0434\u044c React \u043c\u043e\u0436\u043d\u043e \u0441\u0432\u044f\u0437\u0430\u0442\u044c \u0441 \u0441\u0430\u0439\u0442\u043e\u043c \u043d\u0430 Jango(\u043f\u0438\u0442\u043e\u043d), \u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u0432 \u0431\u0443\u0434\u0443\u0449\u0435\u043c \u043c\u043e\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0434\u0438\u0437\u0430\u0439\u043d \u0438 \u0431\u0443\u0434\u0435\u0442 \u043a\u0440\u0430\u0441\u0438\u0432\u0435\u0435 \u0438 \u043f\u0440\u0438\u0432\u043b\u0435\u043a\u0430\u0442\u0435\u043b\u044c\u043d\u0435\u0435. \u0410 \u0441\u0435\u0439\u0447\u0430\u0441 \u0436\u0435 \u0443 \u043c\u0435\u043d\u044f \u0441\u0442\u043e\u044f\u043b\u0430 \u0437\u0430\u0434\u0430\u0447\u0430 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u044b\u0439 \u0432\u0438\u0434\u0435\u043e\u0447\u0430\u0442. <\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w780q1\/getpro\/habr\/upload_files\/64b\/c80\/b60\/64bc80b6061e8ba05a7eae5927c1ffaf.jpg\" width=\"1800\" height=\"600\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/64b\/c80\/b60\/64bc80b6061e8ba05a7eae5927c1ffaf.jpg\" data-blurred=\"true\"\/><figcaption><\/figcaption><\/figure>\n<h2>\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0438 \u0447\u0430\u0442\u0430<\/h2>\n<p>\u0422\u0430\u043a \u043a\u0430\u043a \u044f \u0445\u043e\u0442\u0435\u043b \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0438 \u0441\u0430\u0439\u0442 \u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0442\u043e \u043f\u0435\u0440\u0432\u044b\u043c \u0434\u0435\u043b\u043e\u043c \u044f \u0440\u0435\u0448\u0438\u043b \u0432\u0437\u044f\u0442\u044c\u0441\u044f \u0437\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e \u0432\u0435\u0431-\u0432\u0435\u0440\u0441\u0438\u0438, \u0430 \u043f\u043e\u0442\u043e\u043c \u0443\u0436\u0435 \u043f\u0435\u0440\u0435\u043d\u0435\u0441\u0442\u0438 \u0435\u0435 \u0432 desctop \u0432\u0435\u0440\u0441\u0438\u044e. <\/p>\n<p>\u0422\u0430\u043a \u043a\u0430\u043a \u0441\u0435\u0440\u0432\u0435\u0440 \u0443 \u043c\u0435\u043d\u044f \u0431\u0443\u0434\u0435\u0442 \u043d\u0430 Js, \u0442\u043e \u044f \u0441\u0440\u0430\u0437\u0443 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043b Node Js, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u0439 \u043a\u043e\u0434 \u043d\u0430 \u044d\u0442\u043e\u043c \u044f\u0437\u044b\u043a\u0435 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f. \u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u044f \u0441\u043e\u0437\u0434\u0430\u043b \u0441\u0430\u043c\u043e react-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435.<\/p>\n<pre><code class=\"bash\">npx create-react-app video-chat-webrtc<\/code><\/pre>\n<p>\u0417\u0430\u0442\u0435\u043c \u044f \u043d\u0430\u0447\u0430\u043b \u043f\u043e\u0434\u0442\u044f\u0433\u0438\u0432\u0430\u0442\u044c \u0432\u0441\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 \u0434\u043b\u044f \u043f\u043e\u043b\u043d\u043e\u0446\u0435\u043d\u043d\u043e\u0439 \u0438 \u043a\u043e\u043c\u0444\u043e\u0440\u0442\u043d\u043e\u0439 \u0440\u0430\u0431\u043e\u0442\u044b \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.<\/p>\n<pre><code>cd video-chat-webrtc npm i express socket.io socket.io-client react-router react-router-dom uuid freeice --save npm run start<\/code><\/pre>\n<p>\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0439 \u0441\u0442\u0440\u043e\u043a\u043e\u0439 \u043c\u044b \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u043b\u0438 \u043d\u0430\u0448 \u0441\u0435\u0440\u0432\u0435\u0440, \u043f\u043e\u0441\u043b\u0435 \u0447\u0435\u0433\u043e \u043c\u044b \u043c\u043e\u0436\u0435\u043c \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0435\u0433\u043e \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/767\/797\/745\/7677977456b9a69c0099eb931cb4310c.png\" width=\"1200\" height=\"550\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/767\/797\/745\/7677977456b9a69c0099eb931cb4310c.png\"\/><figcaption><\/figcaption><\/figure>\n<p>\u0417\u0430\u0442\u0435\u043c, \u044f \u0438\u0437\u043c\u0435\u043d\u0438\u043b \u0444\u0430\u0439\u043b \/video-chat-webrtc\/src\/App.js. \u0422\u0430\u043a \u043a\u0430\u043a \u0443 \u043c\u0435\u043d\u044f \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u043a\u0430 3 \u043f\u0443\u0442\u0438, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u044b \u043c\u043e\u0436\u0435\u043c \u043f\u0435\u0440\u0435\u0439\u0442\u0438(Room, Main, NotFound404). \u0422\u0430\u043a\u0436\u0435 \u044f \u0441\u0434\u0435\u043b\u0430\u043b \u043f\u0430\u043f\u043a\u0443 pages, \u0433\u0434\u0435 \u043d\u0430\u0445\u043e\u0434\u044f\u0442\u0441\u044f \u0434\u0440\u0443\u0433\u0438\u0435 3 \u043f\u0430\u043f\u043a\u0438: Room, Main \u0438 NotFound404, \u0433\u0434\u0435 \u0431\u0443\u0434\u0443\u0442 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f Js \u0444\u0430\u0439\u043b\u044b, \u043a\u0430\u0436\u0434\u044b\u0439 \u043e\u0442\u0432\u0435\u0447\u0430\u044e\u0449\u0438\u0439 \u0437\u0430 \u0441\u0432\u043e\u044e \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u043d\u0430 \u0441\u0430\u0439\u0442\u0435. <\/p>\n<pre><code class=\"javascript\">import {BrowserRouter, Switch, Route} from 'react-router-dom'; import Room from '.\/pages\/Room'; import Main from '.\/pages\/Main'; import NotFound404 from '.\/pages\/NotFound404';  function App() {   return (     &lt;BrowserRouter>       &lt;Switch>         &lt;Route exact path='\/room\/:id' component={Room}\/>         &lt;Route exact path='\/' component={Main}\/>         &lt;Route component={NotFound404}\/>       &lt;\/Switch>     &lt;\/BrowserRouter>   ); }  export default App;<\/code><\/pre>\n<p>\u0417\u0430\u0442\u0435\u043c, \u043f\u043e\u0441\u043b\u0435 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0440\u043e\u0443\u0442\u0435\u0440\u043e\u0432 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b \u0441\u0430\u0439\u0442\u0430, \u044f \u043d\u0430\u043a\u043e\u043d\u0435\u0446 \u0437\u0430\u0434\u0443\u043c\u0430\u043b\u0441\u044f \u043d\u0430\u0434 \u0442\u0435\u043c, \u043d\u0430 \u043a\u0430\u043a\u043e\u043c \u043f\u043e\u0440\u0442\u0435 \u0431\u0443\u0434\u0435\u0442 \u0432\u0438\u0441\u0435\u0442\u044c \u0441\u0430\u0439\u0442, \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0437\u0430\u0449\u0438\u0449\u0435\u043d\u043d\u044b\u0439 \u0438\u043b\u0438 \u043d\u0435\u0442 \u0438 ,\u043d\u0430\u043a\u043e\u043d\u0435\u0446, \u043d\u0430\u0434 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435\u043c \u0441\u0430\u043c\u043e\u0439 \u043b\u043e\u0433\u0438\u043a\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0438 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0430. \u0422\u043e \u0435\u0441\u0442\u044c \u044f \u0437\u0430\u0434\u0443\u043c\u0430\u043b\u0441\u044f \u043d\u0430\u0434 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435\u043c \u0444\u0430\u0439\u043b\u0430, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0432\u0441\u0435 \u044d\u0442\u043e \u0431\u0443\u0434\u0435\u0442 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u043e. \u0412 \u043c\u043e\u0435\u043c \u043f\u0440\u043e\u0435\u043a\u0442\u0435 \u044d\u0442\u043e \u0444\u0430\u0439\u043b &#8212; server.js<\/p>\n<pre><code class=\"javascript\">const fs = require('fs'); const options = { key: fs.readFileSync('key.pem'), cert: fs.readFileSync('cert.pem') };  const path = require('path'); const express = require('express'); const app = express(); const server = require('http').createServer(app); const serverHttps = require('https').createServer(options, app); const io = require('socket.io')(serverHttps); const PORT = process.env.PORT || 3006;  server.listen(PORT, () => { console.log('Server Started!') } )  serverHttps.listen(3010, () => { console.log(\"Https server Started!\") } )<\/code><\/pre>\n<p>\u041a\u0430\u043a \u0432\u044b \u0432\u0438\u0434\u0438\u0442\u0435, \u043e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u0441\u0442\u0430\u0440\u0442\u0443\u0435\u0442 2 \u0441\u0435\u0440\u0432\u0435\u0440\u0430: http \u0438 https. \u041f\u043e\u0447\u0435\u043c\u0443 \u0442\u0430\u043a? \u041a\u043e\u0433\u0434\u0430 \u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u043b \u0432\u043f\u0435\u0440\u0432\u044b\u0435, \u0442\u043e \u043f\u0440\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a http \u0441\u0435\u0440\u0432\u0435\u0440\u0443, \u0434\u0430\u043d\u043d\u044b\u0435 \u043d\u0435 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u043b\u0438\u0441\u044c, \u0442\u043e \u0435\u0441\u0442\u044c \u0431\u0440\u0430\u0443\u0437\u0435\u0440 \u043d\u0435 \u0437\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u043b \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u043d\u0430 \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0443 \u043c\u0435\u0434\u0438\u0430\u043a\u043e\u043d\u0442\u0435\u043d\u0442\u0430. \u041f\u043e\u0441\u0438\u0434\u0435\u0432 \u0432 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0435 \u0438 \u043f\u043e\u0438\u0441\u043a\u0430\u0432 \u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b, \u044f \u043f\u043e\u043d\u044f\u043b, \u0447\u0442\u043e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c https \u0441\u0435\u0440\u0432\u0435\u0440, \u0442\u0430\u043a \u043a\u0430\u043a \u043e\u043d \u043f\u0435\u0440\u0435\u0434\u0430\u0435\u0442 \u043c\u0435\u0434\u0438\u0430\u043a\u043e\u043d\u0442\u0435\u043d\u0442 \u043f\u043e \u0437\u0430\u0449\u0438\u0449\u0435\u043d\u043d\u043e\u043c\u0443 \u043a\u0430\u043d\u0430\u043b\u0443. \u0421\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u043c\u043d\u0435 \u043d\u0430\u0434\u043e \u0431\u044b\u043b\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u044b. \u0422\u0430\u043a \u043a\u0430\u043a \u044f \u0431\u044b\u043b \u043d\u0430 \u043b\u0438\u043d\u0443\u043a\u0441\u0435, \u0442\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0442\u0430\u043a\u0438\u0435 \u043d\u0435 \u0441\u043e\u0441\u0442\u0430\u0432\u0438\u043b\u043e \u0442\u0440\u0443\u0434\u0430, \u043f\u0440\u0430\u0432\u0434\u0430 \u043e\u043d \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0441\u044f \u0431\u0438\u0442\u044b\u043c, \u0438\u0437-\u0437\u0430 \u0447\u0435\u0433\u043e \u0431\u0440\u0430\u0443\u0437\u0435\u0440 \u0433\u043e\u0432\u043e\u0440\u0438\u043b, \u0447\u0442\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043d\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e \u0438 \u043d\u0435 \u0437\u0430\u0449\u0438\u0449\u0435\u043d\u043d\u043e, \u043d\u043e \u0435\u0433\u043e \u0436\u0435 \u0434\u0435\u043b\u0430\u043b \u044f, \u0442\u0430\u043a \u0447\u0442\u043e \u043c\u043d\u0435 \u043d\u0435\u0447\u0435\u0433\u043e \u0431\u044b\u043b\u043e \u0431\u043e\u044f\u0442\u044c\u0441\u044f.<\/p>\n<p>\u0414\u0430\u043b\u0435\u0435 \u044f \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043d\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0435(\u0442.\u0435 \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043c, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u044e\u0437\u0435\u0440\u0430 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443, \u0438 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u043d\u0430 \u0441\u0442\u043e\u0440\u043e\u043d\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0430). \u0412 \u043f\u0430\u043f\u043a\u0435 scr \u044f \u0441\u0434\u0435\u043b\u0430\u043b \u043d\u043e\u0432\u0443\u044e \u043f\u0430\u043f\u043a\u0443 socket, \u0433\u0434\u0435 \u0441\u0434\u0435\u043b\u0430\u043b \u0444\u0430\u0439\u043b index.js.<\/p>\n<pre><code class=\"javascript\">import {io} from 'socket.io-client';  const options = { \"force new connection\": true, reconnectionAttempts: \"Infinity\", \/\/ avoid having user reconnect manually in order to prevent dead clients after a server restart timeout : 10000, \/\/ before connect_error and connect_timeout are emitted. transports : [\"websocket\"] }  const socket = io('\/', options);  export default socket;<\/code><\/pre>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c, \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0432 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443, \u044f \u043f\u0440\u0438\u0441\u0442\u0443\u043f\u0438\u043b \u043a \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044e \u043c\u0435\u0442\u043e\u0434\u0430 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u043a\u043e\u043c\u043d\u0430\u0442, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u0441\u043e\u0437\u0434\u0430\u043d\u044b, \u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044e \u043c\u0435\u0434\u0438\u0430\u043a\u043e\u043d\u0442\u0435\u043d\u0442\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d \u043e\u0442 \u0434\u0440\u0443\u0433\u0438\u0445 \u043a\u043e\u043d\u0435\u043a\u0442\u043e\u0432. \u0418 \u0442\u0430\u043a\u0436\u0435 \u044f \u043e\u043f\u0438\u0441\u0430\u043b \u0432\u0441\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c \u0441\u043e\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044b \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435. \u0424\u0430\u0439\u043b actions.js \u044f \u0441\u0434\u0435\u043b\u0430\u043b \u0432 \u0442\u043e\u0439 \u0436\u0435 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u0438, \u0447\u0442\u043e \u0438 index.js, \u043e\u0442\u0432\u0435\u0447\u0430\u044e\u0449\u0438\u0439 \u0437\u0430 \u043a\u043e\u043d\u0435\u043a\u0442 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443.<\/p>\n<pre><code class=\"javascript\">const ACTIONS = { JOIN: 'join', LEAVE: 'leave', SHARE_ROOMS: 'share-rooms', ADD_PEER: 'add-peer', REMOVE_PEER: 'remove-peer', RELAY_SDP: 'relay-sdp', RELAY_ICE: 'relay-ice', ICE_CANDIDATE: 'ice-candidate', SESSION_DESCRIPTION: 'session-description' };  module.exports = ACTIONS;<\/code><\/pre>\n<p>\u0412 \u0444\u0430\u0439\u043b server.js \u044f \u0434\u043e\u0431\u0430\u0432\u0438\u043b:<\/p>\n<pre><code class=\"javascript\">const ACTIONS = require('.\/src\/socket\/actions');  function getClientRooms() { const {rooms} = io.sockets.adapter;  return Array.from(rooms.keys()); }  function shareRoomsInfo() { io.emit(ACTIONS.SHARE_ROOMS, { rooms: getClientRooms() }) }  io.on('connection', socket => { shareRoomsInfo();  socket.on(ACTIONS.JOIN, config => { const {room: roomID} = config; const {rooms: joinedRooms} = socket;  if (Array.from(joinedRooms).includes(roomID)) { return console.warn(`Already joined to ${roomID}`); }  const clients = Array.from(io.sockets.adapter.rooms.get(roomID) || []);  clients.forEach(clientID => { io.to(clientID).emit(ACTIONS.ADD_PEER, { peerID: socket.id, createOffer: false });  socket.emit(ACTIONS.ADD_PEER, { peerID: clientID, createOffer: true, }); });  socket.join(roomID); shareRoomsInfo(); });  function leaveRoom() { const {rooms} = socket;  Array.from(rooms) \/\/ LEAVE ONLY CLIENT CREATED ROOM .forEach(roomID => { const clients = Array.from(io.sockets.adapter.rooms.get(roomID) || []);  clients.forEach(clientID => { io.to(clientID).emit(ACTIONS.REMOVE_PEER, { peerID: socket.id, });  socket.emit(ACTIONS.REMOVE_PEER, { peerID: clientID, }); });  socket.leave(roomID); });  shareRoomsInfo(); }  socket.on(ACTIONS.LEAVE, leaveRoom); socket.on('disconnecting', leaveRoom);<\/code><\/pre>\n<p>\u041f\u043e\u0441\u043b\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u0444\u0443\u043d\u043a\u0446\u0438\u0439 \u0432 server.js \u044f \u043d\u0430\u0447\u0430\u043b \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c \u043a\u043d\u043e\u043f\u043a\u0438 \u043d\u0430 \u0441\u0430\u0439\u0442\u0435 \u0438 \u043f\u0440\u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0442\u044c \u043a \u043d\u0438\u043c \u043b\u043e\u0433\u0438\u043a\u0443. \u0412 \u043f\u0435\u0440\u0432\u0443\u044e \u043e\u0447\u0435\u0440\u0435\u0434\u044c \u044f \u043f\u0435\u0440\u0435\u0448\u0435\u043b \u043a \u0444\u0430\u0439\u043b\u0443, \u043e\u0442\u0432\u0435\u0447\u0430\u044e\u0449\u0435\u043c\u0443 \u0437\u0430 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0433\u043b\u0430\u0432\u043d\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b. \u0422\u0430\u043c \u044f \u043f\u0440\u043e\u043f\u0438\u0441\u044b\u0432\u0430\u043b \u043b\u043e\u0433\u0438\u043a\u0443 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0445 \u043a\u043e\u043c\u043d\u0430\u0442 \u0438 \u043a\u043d\u043e\u043f\u043a\u0443 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f room.<\/p>\n<pre><code class=\"javascript\">import {useState, useEffect, useRef} from 'react'; import socket from '..\/..\/socket'; import ACTIONS from '..\/..\/socket\/actions'; import {useHistory} from 'react-router'; import {v4} from 'uuid';  export default function Main() { const history = useHistory(); const [rooms, updateRooms] = useState([]); const rootNode = useRef();  useEffect(() => { socket.on(ACTIONS.SHARE_ROOMS, ({rooms = []} = {}) => {  }); }, []);  return ( &lt;div> &lt;h1>Available Rooms&lt;\/h1>  &lt;ul> {rooms.map(roomID => ( &lt;li key={roomID}> {roomID} &lt;button onClick={() => { history.push(`\/room\/${roomID}`); }}>JOIN ROOM&lt;\/button> &lt;\/li> ))} &lt;\/ul>  &lt;button onClick={() => { history.push(`\/room\/${v4()}`); }}>Create New Room&lt;\/button> &lt;\/div> ); }<\/code><\/pre>\n<p>\u0422\u0430\u043a \u0436\u0435 \u044f \u043f\u0435\u0440\u0435\u043f\u0438\u0441\u0430\u043b server.js, \u0442.\u043a \u043f\u0440\u0438 \u043e\u0442\u043a\u0440\u044b\u0442\u0438\u0438 \u0441\u0430\u0439\u0442\u0430 \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u043b\u043e\u0441\u044c, \u0447\u0442\u043e \u043a\u043e\u043c\u043d\u0430\u0442\u0430 \u0443\u0436\u0435 \u0435\u0441\u0442\u044c, \u0445\u043e\u0442\u044f \u0435\u0435 \u043d\u0438\u043a\u0442\u043e \u043d\u0435 \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u043b. \u042d\u0442\u043e \u0441\u0432\u044f\u0437\u0430\u043d\u043e \u0441 \u0442\u0435\u043c, \u0447\u0442\u043e \u043f\u0440\u0438 \u0437\u0430\u0445\u043e\u0434\u0435 \u043d\u0430 \u0441\u0430\u0439\u0442, \u043d\u0430\u0448 \u0441\u043e\u043a\u0435\u0442 \u0443\u0436\u0435 \u043a \u0447\u0435\u043c\u0443-\u0442\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d, \u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u043d\u0443\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u043e\u0442\u0444\u0438\u043b\u044c\u0442\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u0432\u044b\u0432\u043e\u0434\u0438\u043c\u044b\u0445 \u043a\u043e\u043c\u043d\u0430\u0442 \u043d\u0430 \u044d\u043a\u0440\u0430\u043d. \u042d\u0442\u0430 \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 server.js.<\/p>\n<pre><code class=\"javascript\">function getClientRooms() { const {rooms} = io.sockets.adapter; return Array.from(rooms.keys()).filter(roomID => validate(roomID) &amp;&amp; version(roomID) === 4); }<\/code><\/pre>\n<p>\u041f\u043e\u0442\u043e\u043c \u044f \u0441\u0442\u0430\u043b \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u044b\u0432\u0430\u0442\u044c \u0441\u0430\u043c\u0438 \u043a\u043e\u043c\u043d\u0430\u0442\u044b. \u0414\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0439 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b \u0431\u044b\u043b\u0438 \u0445\u0443\u043a\u0438, \u0432 \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u0431\u0443\u0434\u0435\u043c \u043f\u043e\u0434\u043f\u0438\u0441\u044b\u0432\u0430\u0442\u044c\u0441\u044f \u043d\u0430 \u0432\u0441\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f. \u042f \u0441\u043e\u0437\u0434\u0430\u043b \u043f\u0430\u043f\u043a\u0443 src\/hooks, \u0430 \u0432 \u043d\u0435\u0439 \u0444\u0430\u0439\u043b useWebRTC.js<\/p>\n<pre><code class=\"javascript\">import {useEffect, useRef, useCallback} from 'react'; import freeice from 'freeice'; import useStateWithCallback from '.\/useStateWithCallback'; import socket from '..\/socket'; import ACTIONS from '..\/socket\/actions';  export const LOCAL_VIDEO = 'LOCAL_VIDEO';   export default function useWebRTC(roomID) {   const [clients, updateClients] = useStateWithCallback([]);    const addNewClient = useCallback((newClient, cb) => {     updateClients(list => {       if (!list.includes(newClient)) {         return [...list, newClient]       }        return list;     }, cb);   }, [clients, updateClients]);    const peerConnections = useRef({});   const localMediaStream = useRef(null);   const peerMediaElements = useRef({     [LOCAL_VIDEO]: null,   });    useEffect(() => {     async function handleNewPeer({peerID, createOffer}) {       if (peerID in peerConnections.current) {         return console.warn(`Already connected to peer ${peerID}`);       }        peerConnections.current[peerID] = new RTCPeerConnection({         iceServers: freeice(),       });        peerConnections.current[peerID].onicecandidate = event => {         if (event.candidate) {           socket.emit(ACTIONS.RELAY_ICE, {             peerID,             iceCandidate: event.candidate,           });         }       }        let tracksNumber = 0;       peerConnections.current[peerID].ontrack = ({streams: [remoteStream]}) => {         tracksNumber++          if (tracksNumber === 2) { \/\/ video &amp; audio tracks received           tracksNumber = 0;           addNewClient(peerID, () => {             if (peerMediaElements.current[peerID]) {               peerMediaElements.current[peerID].srcObject = remoteStream;             } else {               \/\/ FIX LONG RENDER IN CASE OF MANY CLIENTS               let settled = false;               const interval = setInterval(() => {                 if (peerMediaElements.current[peerID]) {                   peerMediaElements.current[peerID].srcObject = remoteStream;                   settled = true;                 }                  if (settled) {                   clearInterval(interval);                 }               }, 1000);             }           });         }       }        localMediaStream.current.getTracks().forEach(track => {         peerConnections.current[peerID].addTrack(track, localMediaStream.current);       });        if (createOffer) {         const offer = await peerConnections.current[peerID].createOffer();          await peerConnections.current[peerID].setLocalDescription(offer);          socket.emit(ACTIONS.RELAY_SDP, {           peerID,           sessionDescription: offer,         });       }     }      socket.on(ACTIONS.ADD_PEER, handleNewPeer);      return () => {       socket.off(ACTIONS.ADD_PEER);     }   }, []);    useEffect(() => {     async function setRemoteMedia({peerID, sessionDescription: remoteDescription}) {       await peerConnections.current[peerID]?.setRemoteDescription(         new RTCSessionDescription(remoteDescription)       );        if (remoteDescription.type === 'offer') {         const answer = await peerConnections.current[peerID].createAnswer();          await peerConnections.current[peerID].setLocalDescription(answer);          socket.emit(ACTIONS.RELAY_SDP, {           peerID,           sessionDescription: answer,         });       }     }      socket.on(ACTIONS.SESSION_DESCRIPTION, setRemoteMedia)      return () => {       socket.off(ACTIONS.SESSION_DESCRIPTION);     }   }, []);    useEffect(() => {     socket.on(ACTIONS.ICE_CANDIDATE, ({peerID, iceCandidate}) => {       peerConnections.current[peerID]?.addIceCandidate(         new RTCIceCandidate(iceCandidate)       );     });      return () => {       socket.off(ACTIONS.ICE_CANDIDATE);     }   }, []);    useEffect(() => {     const handleRemovePeer = ({peerID}) => {       if (peerConnections.current[peerID]) {         peerConnections.current[peerID].close();       }        delete peerConnections.current[peerID];       delete peerMediaElements.current[peerID];        updateClients(list => list.filter(c => c !== peerID));     };      socket.on(ACTIONS.REMOVE_PEER, handleRemovePeer);      return () => {       socket.off(ACTIONS.REMOVE_PEER);     }   }, []);    useEffect(() => {     async function startCapture() {       localMediaStream.current = await navigator.mediaDevices.getUserMedia({         audio: true,         video: {           width: 1280,           height: 720,         }       });        addNewClient(LOCAL_VIDEO, () => {         const localVideoElement = peerMediaElements.current[LOCAL_VIDEO];          if (localVideoElement) {           localVideoElement.volume = 0;           localVideoElement.srcObject = localMediaStream.current;         }       });     }      startCapture()       .then(() => socket.emit(ACTIONS.JOIN, {room: roomID}))       .catch(e => console.error('Error getting userMedia:', e));      return () => {       localMediaStream.current.getTracks().forEach(track => track.stop());        socket.emit(ACTIONS.LEAVE);     };   }, [roomID]);    const provideMediaRef = useCallback((id, node) => {     peerMediaElements.current[id] = node;   }, []);    return {     clients,     provideMediaRef   }; }<\/code><\/pre>\n<p>\u0412 \u044d\u0442\u043e\u043c \u0445\u0443\u043a\u0435 \u044f \u0431\u0443\u0434\u0443 \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0432\u0441\u0435 \u043a\u043e\u043d\u0435\u043a\u0442\u044b, \u0441\u0441\u044b\u043b\u043a\u0443 \u043d\u0430 \u043c\u043e\u0439 \u043c\u0435\u0434\u0438\u0430\u043a\u043e\u043d\u0442\u0435\u043d\u0442 \u0438 \u043d\u0430 \u0432\u0435\u0441\u044c \u043c\u0435\u0434\u0438\u0430\u043a\u043e\u043d\u0442\u0435\u043d\u0442, \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0439 \u043e\u0442 \u0434\u0440\u0443\u0433\u0438\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432, \u0430 \u0442\u0430\u043a \u0436\u0435 \u0431\u0443\u0434\u0443 \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0432\u0441\u0435\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0430\u0445\u043e\u0434\u044f\u0442\u0441\u044f \u0432 \u043a\u043e\u043c\u043d\u0430\u0442\u0435. \u0422\u0430\u043a\u0436\u0435, \u043f\u0440\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043d\u043e\u0432\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0438\u0437\u043c\u0435\u043d\u044f\u0442\u044c peerMediaElements \u0438 \u0431\u044b\u0442\u044c \u0443\u0432\u0435\u0440\u0435\u043d\u043d\u044b\u043c \u0432 \u0442\u043e\u043c, \u0447\u0442\u043e \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043e\u0442\u0440\u0435\u043d\u0434\u0435\u0440\u0435\u043d\u044b. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u044f \u043d\u0430\u043f\u0438\u0441\u0430\u043b \u0435\u0449\u0435 \u043e\u0434\u0438\u043d \u0445\u0443\u043a, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u0432\u0435\u0447\u0430\u0442\u044c \u0437\u0430 \u044d\u0442\u043e.<\/p>\n<pre><code class=\"javascript\">import {useEffect, useRef, useCallback} from 'react'; import freeice from 'freeice'; import useStateWithCallback from '.\/useStateWithCallback'; import socket from '..\/socket'; import ACTIONS from '..\/socket\/actions';  export const LOCAL_VIDEO = 'LOCAL_VIDEO';   export default function useWebRTC(roomID) {   const [clients, updateClients] = useStateWithCallback([]);    const addNewClient = useCallback((newClient, cb) => {     updateClients(list => {       if (!list.includes(newClient)) {         return [...list, newClient]       }        return list;     }, cb);   }, [clients, updateClients]);    const peerConnections = useRef({});   const localMediaStream = useRef(null);   const peerMediaElements = useRef({     [LOCAL_VIDEO]: null,   });    useEffect(() => {     async function handleNewPeer({peerID, createOffer}) {       if (peerID in peerConnections.current) {         return console.warn(`Already connected to peer ${peerID}`);       }        peerConnections.current[peerID] = new RTCPeerConnection({         iceServers: freeice(),       });        peerConnections.current[peerID].onicecandidate = event => {         if (event.candidate) {           socket.emit(ACTIONS.RELAY_ICE, {             peerID,             iceCandidate: event.candidate,           });         }       }        let tracksNumber = 0;       peerConnections.current[peerID].ontrack = ({streams: [remoteStream]}) => {         tracksNumber++          if (tracksNumber === 2) { \/\/ video &amp; audio tracks received           tracksNumber = 0;           addNewClient(peerID, () => {             if (peerMediaElements.current[peerID]) {               peerMediaElements.current[peerID].srcObject = remoteStream;             } else {               \/\/ FIX LONG RENDER IN CASE OF MANY CLIENTS               let settled = false;               const interval = setInterval(() => {                 if (peerMediaElements.current[peerID]) {                   peerMediaElements.current[peerID].srcObject = remoteStream;                   settled = true;                 }                  if (settled) {                   clearInterval(interval);                 }               }, 1000);             }           });         }       }        localMediaStream.current.getTracks().forEach(track => {         peerConnections.current[peerID].addTrack(track, localMediaStream.current);       });        if (createOffer) {         const offer = await peerConnections.current[peerID].createOffer();          await peerConnections.current[peerID].setLocalDescription(offer);          socket.emit(ACTIONS.RELAY_SDP, {           peerID,           sessionDescription: offer,         });       }     }      socket.on(ACTIONS.ADD_PEER, handleNewPeer);      return () => {       socket.off(ACTIONS.ADD_PEER);     }   }, []);    useEffect(() => {     async function setRemoteMedia({peerID, sessionDescription: remoteDescription}) {       await peerConnections.current[peerID]?.setRemoteDescription(         new RTCSessionDescription(remoteDescription)       );        if (remoteDescription.type === 'offer') {         const answer = await peerConnections.current[peerID].createAnswer();          await peerConnections.current[peerID].setLocalDescription(answer);          socket.emit(ACTIONS.RELAY_SDP, {           peerID,           sessionDescription: answer,         });       }     }      socket.on(ACTIONS.SESSION_DESCRIPTION, setRemoteMedia)      return () => {       socket.off(ACTIONS.SESSION_DESCRIPTION);     }   }, []);    useEffect(() => {     socket.on(ACTIONS.ICE_CANDIDATE, ({peerID, iceCandidate}) => {       peerConnections.current[peerID]?.addIceCandidate(         new RTCIceCandidate(iceCandidate)       );     });      return () => {       socket.off(ACTIONS.ICE_CANDIDATE);     }   }, []);    useEffect(() => {     const handleRemovePeer = ({peerID}) => {       if (peerConnections.current[peerID]) {         peerConnections.current[peerID].close();       }        delete peerConnections.current[peerID];       delete peerMediaElements.current[peerID];        updateClients(list => list.filter(c => c !== peerID));     };      socket.on(ACTIONS.REMOVE_PEER, handleRemovePeer);      return () => {       socket.off(ACTIONS.REMOVE_PEER);     }   }, []);    useEffect(() => {     async function startCapture() {       localMediaStream.current = await navigator.mediaDevices.getUserMedia({         audio: true,         video: {           width: 1280,           height: 720,         }       });        addNewClient(LOCAL_VIDEO, () => {         const localVideoElement = peerMediaElements.current[LOCAL_VIDEO];          if (localVideoElement) {           localVideoElement.volume = 0;           localVideoElement.srcObject = localMediaStream.current;         }       });     }      startCapture()       .then(() => socket.emit(ACTIONS.JOIN, {room: roomID}))       .catch(e => console.error('Error getting userMedia:', e));      return () => {       localMediaStream.current.getTracks().forEach(track => track.stop());        socket.emit(ACTIONS.LEAVE);     };   }, [roomID]);    const provideMediaRef = useCallback((id, node) => {     peerMediaElements.current[id] = node;   }, []);    return {     clients,     provideMediaRef   }; }<\/code><\/pre>\n<p>\u041f\u043e\u0442\u043e\u043c \u044f \u043d\u0430\u0447\u0430\u043b \u043f\u0435\u0440\u0435\u043f\u0438\u0441\u044b\u0432\u0430\u0442\u044c \u0441\u0430\u043c\u0443 \u043b\u043e\u0433\u0438\u043a\u0443 \u043a\u043e\u043c\u043d\u0430\u0442. \u0422\u0430\u043c \u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u044e \u0432\u0441\u0435\u0445 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439, \u0447\u044c\u0438 \u043a\u043e\u043d\u0435\u043a\u0442\u044b \u0435\u0441\u0442\u044c \u0443 \u043d\u0430\u0441, \u0438 \u0442\u0435\u0445, \u043a\u0442\u043e \u0441\u043e\u0433\u043b\u0430\u0441\u0438\u043b\u0441\u044f \u043d\u0430 \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0443 \u043c\u0435\u0434\u0438\u0430\u043a\u043e\u043d\u0442\u0435\u043d\u0442\u0430.<\/p>\n<pre><code class=\"javascript\">import {useParams} from 'react-router'; import useWebRTC, {LOCAL_VIDEO} from '..\/..\/hooks\/useWebRTC';  function layout(clientsNumber = 1) {   const pairs = Array.from({length: clientsNumber})     .reduce((acc, next, index, arr) => {       if (index % 2 === 0) {         acc.push(arr.slice(index, index + 2));       }        return acc;     }, []);    const rowsNumber = pairs.length;   const height = `${100 \/ rowsNumber}%`;    return pairs.map((row, index, arr) => {      if (index === arr.length - 1 &amp;&amp; row.length === 1) {       return [{         width: '100%',         height,       }];     }      return row.map(() => ({       width: '50%',       height,     }));   }).flat(); }  export default function Room() {   const {id: roomID} = useParams();   const {clients, provideMediaRef} = useWebRTC(roomID);   const videoLayout = layout(clients.length);    return (     &lt;div style={{       display: 'flex',       alignItems: 'center',       justifyContent: 'center',       flexWrap: 'wrap',       height: '100vh',     }}>       {clients.map((clientID, index) => {         return (           &lt;div key={clientID} style={videoLayout[index]} id={clientID}>             &lt;video               width='100%'               height='100%'               ref={instance => {                 provideMediaRef(clientID, instance);               }}               autoPlay               playsInline               muted={clientID === LOCAL_VIDEO}             \/>           &lt;\/div>         );       })}     &lt;\/div>   ); }<\/code><\/pre>\n<p>\u041a\u0430\u0436\u0434\u0430\u044f \u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0442\u044c\u0441\u044f \u0432 \u0442\u043e\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u044f \u0443\u043a\u0430\u0436\u0443, \u0438\u0437-\u0437\u0430 \u0447\u0435\u0433\u043e \u0442\u0430\u043a\u043e\u0439 \u0447\u0430\u0442 \u0438\u043c\u0435\u0435\u0442 \u043e\u0433\u0440\u043e\u043c\u043d\u043e\u0435 \u043f\u0440\u0435\u0438\u043c\u0443\u0449\u0435\u0441\u0442\u0432\u043e, \u0432\u0435\u0434\u044c \u043e\u043d \u043f\u043e\u0447\u0442\u0438 \u043d\u0435 \u0441\u0436\u0438\u043c\u0430\u0435\u0442 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0438 \u043f\u0435\u0440\u0435\u0434\u0430\u0435\u0442 \u0435\u0433\u043e \u0442\u0430\u043a\u0438\u043c, \u043a\u0430\u043a\u0438\u043c \u043e\u043d\u043e \u043f\u043e\u0441\u0442\u0443\u043f\u0438\u043b\u043e \u0432 answer \u0438\u043b\u0438 \u0432 offer.<\/p>\n<h2>\u0417\u0430\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0432\u0442\u043e\u0440\u043e\u0439 \u0447\u0430\u0441\u0442\u0438<\/h2>\n<p>\u0412 \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438, \u0445\u043e\u0447\u0443 \u0441\u043a\u0430\u0437\u0430\u0442\u044c, \u0447\u0442\u043e \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0432\u0438\u0434\u0435\u043e\u0447\u0430\u0442\u0430 \u0431\u044b\u043b\u0430 \u0441\u0430\u043c\u043e\u0439 \u0441\u043b\u043e\u0436\u043d\u043e\u0439 \u0447\u0430\u0441\u0442\u044c\u044e \u043f\u0440\u043e\u0435\u043a\u0442\u0430. \u0415\u0441\u043b\u0438 \u044f \u0447\u0442\u043e-\u0442\u043e \u043d\u0435 \u043d\u0430\u043f\u0438\u0441\u0430\u043b \u0432 \u0442\u0435\u043a\u0441\u0442\u043e\u0432\u043e\u043c \u0444\u043e\u0440\u043c\u0430\u0442\u0435, \u0442\u043e \u044f \u043f\u0440\u0438\u043b\u043e\u0436\u0443 \u0441\u0441\u044b\u043b\u043a\u0443 \u043d\u0430 GitHub, \u0433\u0434\u0435 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u0445\u043e\u0434\u043d\u0438\u043a \u044d\u0442\u043e\u0433\u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0430, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043d\u0443\u0436\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c.<\/p>\n<p>\u0418\u0442\u0430\u043a, \u044d\u0442\u043e \u0431\u044b\u043b\u0430 \u0432\u0442\u043e\u0440\u0430\u044f \u0447\u0430\u0441\u0442\u044c, \u0432 \u043a\u043e\u043d\u0446\u0435 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u044f \u0436\u0434\u0443 \u043e\u0442 \u0432\u0430\u0441 \u043a\u0440\u0438\u0442\u0438\u043a\u0438, \u0432\u0435\u0434\u044c \u043e\u043d\u0430 \u043f\u043e\u043c\u043e\u0433\u0430\u0435\u0442 \u043c\u043d\u0435 \u0441\u043e\u0432\u0435\u0440\u0448\u0435\u043d\u0441\u0442\u0432\u043e\u0432\u0430\u0442\u044c\u0441\u044f)<\/p>\n<p>https:\/\/github.com\/DeverG3nt\/video-chat-webrtc<\/p>\n<\/div>\n<\/div>\n<\/div>\n<div class=\"v-portal\" style=\"display:none;\"><\/div>\n<\/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\/post\/653199\/\"> https:\/\/habr.com\/ru\/post\/653199\/<\/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_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u0412 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0435\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u0442\u0443\u0442\u043e\u0440\u0438\u0430\u043b\u0430 \u044f \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u043b \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u044b \u0432\u0438\u0434\u0435\u043e\u0441\u0432\u044f\u0437\u0438 \u0438 \u0441\u043a\u0430\u0437\u0430\u043b, \u0447\u0442\u043e \u043e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043b\u0441\u044f \u043d\u0430 webRTC. \u0420\u0430\u0441\u0441\u043a\u0430\u0437\u0430\u043b \u043a\u0430\u043a \u043e\u043d \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0438 \u0440\u0430\u0441\u0441\u043a\u0430\u0437\u0430\u043b \u0442\u0435\u043e\u0440\u0435\u0442\u0438\u0447\u0435\u0441\u043a\u0443\u044e \u0435\u0433\u043e \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e. \u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u044f \u043e\u043f\u0438\u0448\u0443 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0441\u0430\u043c\u043e\u0433\u043e \u0432\u0438\u0434\u0435\u043e\u0447\u0430\u0442\u0430 \u0438 \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0443 \u043a\u043e\u0434, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u043d\u0430 GitHub.<\/p>\n<h2>\u0417\u0430\u0434\u0443\u043c\u043a\u0430<\/h2>\n<p>\u041a\u043e\u0433\u0434\u0430 \u044f \u0443\u0436\u0435 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u043b\u0441\u044f \u0441\u043e \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043e\u043c \u0432\u0438\u0434\u0435\u043e\u0441\u0432\u044f\u0437\u0438, \u0442\u043e \u043d\u0430\u0447\u0430\u043b \u0434\u0443\u043c\u0430\u0442\u044c \u043d\u0430 \u043a\u0430\u043a\u043e\u043c \u044f\u0437\u044b\u043a\u0435 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u0430\u043c \u0447\u0430\u0442. \u041d\u0435 \u0434\u043e\u043b\u0433\u043e \u0434\u0443\u043c\u0430\u044f \u044f \u0432\u044b\u0431\u0440\u0430\u043b \u0434\u0432\u0430 \u044f\u0437\u044b\u043a\u0430: JavaScript \u0434\u043b\u044f \u0441\u0430\u0439\u0442\u0430 \u0438 \u0432\u0438\u0434\u0435\u043e\u0447\u0430\u0442\u0430. <\/p>\n<p>\u0412 JavaScript \u0435\u0441\u0442\u044c \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 React, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0441\u0430\u0439\u0442. \u0412\u044b\u0431\u0440\u0430\u043b \u044f \u043d\u0435 \u0431\u0435\u0437\u0434\u0443\u043c\u043d\u043e, \u0432\u0435\u0434\u044c React \u043c\u043e\u0436\u043d\u043e \u0441\u0432\u044f\u0437\u0430\u0442\u044c \u0441 \u0441\u0430\u0439\u0442\u043e\u043c \u043d\u0430 Jango(\u043f\u0438\u0442\u043e\u043d), \u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u0432 \u0431\u0443\u0434\u0443\u0449\u0435\u043c \u043c\u043e\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0434\u0438\u0437\u0430\u0439\u043d \u0438 \u0431\u0443\u0434\u0435\u0442 \u043a\u0440\u0430\u0441\u0438\u0432\u0435\u0435 \u0438 \u043f\u0440\u0438\u0432\u043b\u0435\u043a\u0430\u0442\u0435\u043b\u044c\u043d\u0435\u0435. \u0410 \u0441\u0435\u0439\u0447\u0430\u0441 \u0436\u0435 \u0443 \u043c\u0435\u043d\u044f \u0441\u0442\u043e\u044f\u043b\u0430 \u0437\u0430\u0434\u0430\u0447\u0430 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u044b\u0439 \u0432\u0438\u0434\u0435\u043e\u0447\u0430\u0442. <\/p>\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<h2>\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0438 \u0447\u0430\u0442\u0430<\/h2>\n<p>\u0422\u0430\u043a \u043a\u0430\u043a \u044f \u0445\u043e\u0442\u0435\u043b \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0438 \u0441\u0430\u0439\u0442 \u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0442\u043e \u043f\u0435\u0440\u0432\u044b\u043c \u0434\u0435\u043b\u043e\u043c \u044f \u0440\u0435\u0448\u0438\u043b \u0432\u0437\u044f\u0442\u044c\u0441\u044f \u0437\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e \u0432\u0435\u0431-\u0432\u0435\u0440\u0441\u0438\u0438, \u0430 \u043f\u043e\u0442\u043e\u043c \u0443\u0436\u0435 \u043f\u0435\u0440\u0435\u043d\u0435\u0441\u0442\u0438 \u0435\u0435 \u0432 desctop \u0432\u0435\u0440\u0441\u0438\u044e. <\/p>\n<p>\u0422\u0430\u043a \u043a\u0430\u043a \u0441\u0435\u0440\u0432\u0435\u0440 \u0443 \u043c\u0435\u043d\u044f \u0431\u0443\u0434\u0435\u0442 \u043d\u0430 Js, \u0442\u043e \u044f \u0441\u0440\u0430\u0437\u0443 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043b Node Js, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u0439 \u043a\u043e\u0434 \u043d\u0430 \u044d\u0442\u043e\u043c \u044f\u0437\u044b\u043a\u0435 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f. \u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u044f \u0441\u043e\u0437\u0434\u0430\u043b \u0441\u0430\u043c\u043e react-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435.<\/p>\n<pre><code class=\"bash\">npx create-react-app video-chat-webrtc<\/code><\/pre>\n<p>\u0417\u0430\u0442\u0435\u043c \u044f \u043d\u0430\u0447\u0430\u043b \u043f\u043e\u0434\u0442\u044f\u0433\u0438\u0432\u0430\u0442\u044c \u0432\u0441\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 \u0434\u043b\u044f \u043f\u043e\u043b\u043d\u043e\u0446\u0435\u043d\u043d\u043e\u0439 \u0438 \u043a\u043e\u043c\u0444\u043e\u0440\u0442\u043d\u043e\u0439 \u0440\u0430\u0431\u043e\u0442\u044b \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.<\/p>\n<pre><code>cd video-chat-webrtc npm i express socket.io socket.io-client react-router react-router-dom uuid freeice --save npm run start<\/code><\/pre>\n<p>\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0439 \u0441\u0442\u0440\u043e\u043a\u043e\u0439 \u043c\u044b \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u043b\u0438 \u043d\u0430\u0448 \u0441\u0435\u0440\u0432\u0435\u0440, \u043f\u043e\u0441\u043b\u0435 \u0447\u0435\u0433\u043e \u043c\u044b \u043c\u043e\u0436\u0435\u043c \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0435\u0433\u043e \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435.<\/p>\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<p>\u0417\u0430\u0442\u0435\u043c, \u044f \u0438\u0437\u043c\u0435\u043d\u0438\u043b \u0444\u0430\u0439\u043b \/video-chat-webrtc\/src\/App.js. \u0422\u0430\u043a \u043a\u0430\u043a \u0443 \u043c\u0435\u043d\u044f \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u043a\u0430 3 \u043f\u0443\u0442\u0438, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u044b \u043c\u043e\u0436\u0435\u043c \u043f\u0435\u0440\u0435\u0439\u0442\u0438(Room, Main, NotFound404). \u0422\u0430\u043a\u0436\u0435 \u044f \u0441\u0434\u0435\u043b\u0430\u043b \u043f\u0430\u043f\u043a\u0443 pages, \u0433\u0434\u0435 \u043d\u0430\u0445\u043e\u0434\u044f\u0442\u0441\u044f \u0434\u0440\u0443\u0433\u0438\u0435 3 \u043f\u0430\u043f\u043a\u0438: Room, Main \u0438 NotFound404, \u0433\u0434\u0435 \u0431\u0443\u0434\u0443\u0442 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f Js \u0444\u0430\u0439\u043b\u044b, \u043a\u0430\u0436\u0434\u044b\u0439 \u043e\u0442\u0432\u0435\u0447\u0430\u044e\u0449\u0438\u0439 \u0437\u0430 \u0441\u0432\u043e\u044e \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u043d\u0430 \u0441\u0430\u0439\u0442\u0435. <\/p>\n<pre><code class=\"javascript\">import {BrowserRouter, Switch, Route} from 'react-router-dom'; import Room from '.\/pages\/Room'; import Main from '.\/pages\/Main'; import NotFound404 from '.\/pages\/NotFound404';  function App() {   return (     &lt;BrowserRouter>       &lt;Switch>         &lt;Route exact path='\/room\/:id' component={Room}\/>         &lt;Route exact path='\/' component={Main}\/>         &lt;Route component={NotFound404}\/>       &lt;\/Switch>     &lt;\/BrowserRouter>   ); }  export default App;<\/code><\/pre>\n<p>\u0417\u0430\u0442\u0435\u043c, \u043f\u043e\u0441\u043b\u0435 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0440\u043e\u0443\u0442\u0435\u0440\u043e\u0432 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b \u0441\u0430\u0439\u0442\u0430, \u044f \u043d\u0430\u043a\u043e\u043d\u0435\u0446 \u0437\u0430\u0434\u0443\u043c\u0430\u043b\u0441\u044f \u043d\u0430\u0434 \u0442\u0435\u043c, \u043d\u0430 \u043a\u0430\u043a\u043e\u043c \u043f\u043e\u0440\u0442\u0435 \u0431\u0443\u0434\u0435\u0442 \u0432\u0438\u0441\u0435\u0442\u044c \u0441\u0430\u0439\u0442, \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0437\u0430\u0449\u0438\u0449\u0435\u043d\u043d\u044b\u0439 \u0438\u043b\u0438 \u043d\u0435\u0442 \u0438 ,\u043d\u0430\u043a\u043e\u043d\u0435\u0446, \u043d\u0430\u0434 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435\u043c \u0441\u0430\u043c\u043e\u0439 \u043b\u043e\u0433\u0438\u043a\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0438 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0430. \u0422\u043e \u0435\u0441\u0442\u044c \u044f \u0437\u0430\u0434\u0443\u043c\u0430\u043b\u0441\u044f \u043d\u0430\u0434 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435\u043c \u0444\u0430\u0439\u043b\u0430, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0432\u0441\u0435 \u044d\u0442\u043e \u0431\u0443\u0434\u0435\u0442 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u043e. \u0412 \u043c\u043e\u0435\u043c \u043f\u0440\u043e\u0435\u043a\u0442\u0435 \u044d\u0442\u043e \u0444\u0430\u0439\u043b &#8212; server.js<\/p>\n<pre><code class=\"javascript\">const fs = require('fs'); const options = { key: fs.readFileSync('key.pem'), cert: fs.readFileSync('cert.pem') };  const path = require('path'); const express = require('express'); const app = express(); const server = require('http').createServer(app); const serverHttps = require('https').createServer(options, app); const io = require('socket.io')(serverHttps); const PORT = process.env.PORT || 3006;  server.listen(PORT, () => { console.log('Server Started!') } )  serverHttps.listen(3010, () => { console.log(\"Https server Started!\") } )<\/code><\/pre>\n<p>\u041a\u0430\u043a \u0432\u044b \u0432\u0438\u0434\u0438\u0442\u0435, \u043e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u0441\u0442\u0430\u0440\u0442\u0443\u0435\u0442 2 \u0441\u0435\u0440\u0432\u0435\u0440\u0430: http \u0438 https. \u041f\u043e\u0447\u0435\u043c\u0443 \u0442\u0430\u043a? \u041a\u043e\u0433\u0434\u0430 \u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u043b \u0432\u043f\u0435\u0440\u0432\u044b\u0435, \u0442\u043e \u043f\u0440\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a http \u0441\u0435\u0440\u0432\u0435\u0440\u0443, \u0434\u0430\u043d\u043d\u044b\u0435 \u043d\u0435 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u043b\u0438\u0441\u044c, \u0442\u043e \u0435\u0441\u0442\u044c \u0431\u0440\u0430\u0443\u0437\u0435\u0440 \u043d\u0435 \u0437\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u043b \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u043d\u0430 \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0443 \u043c\u0435\u0434\u0438\u0430\u043a\u043e\u043d\u0442\u0435\u043d\u0442\u0430. \u041f\u043e\u0441\u0438\u0434\u0435\u0432 \u0432 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0435 \u0438 \u043f\u043e\u0438\u0441\u043a\u0430\u0432 \u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b, \u044f \u043f\u043e\u043d\u044f\u043b, \u0447\u0442\u043e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c https \u0441\u0435\u0440\u0432\u0435\u0440, \u0442\u0430\u043a \u043a\u0430\u043a \u043e\u043d \u043f\u0435\u0440\u0435\u0434\u0430\u0435\u0442 \u043c\u0435\u0434\u0438\u0430\u043a\u043e\u043d\u0442\u0435\u043d\u0442 \u043f\u043e \u0437\u0430\u0449\u0438\u0449\u0435\u043d\u043d\u043e\u043c\u0443 \u043a\u0430\u043d\u0430\u043b\u0443. \u0421\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u043c\u043d\u0435 \u043d\u0430\u0434\u043e \u0431\u044b\u043b\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u044b. \u0422\u0430\u043a \u043a\u0430\u043a \u044f \u0431\u044b\u043b \u043d\u0430 \u043b\u0438\u043d\u0443\u043a\u0441\u0435, \u0442\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0442\u0430\u043a\u0438\u0435 \u043d\u0435 \u0441\u043e\u0441\u0442\u0430\u0432\u0438\u043b\u043e \u0442\u0440\u0443\u0434\u0430, \u043f\u0440\u0430\u0432\u0434\u0430 \u043e\u043d \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0441\u044f \u0431\u0438\u0442\u044b\u043c, \u0438\u0437-\u0437\u0430 \u0447\u0435\u0433\u043e \u0431\u0440\u0430\u0443\u0437\u0435\u0440 \u0433\u043e\u0432\u043e\u0440\u0438\u043b, \u0447\u0442\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043d\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e \u0438 \u043d\u0435 \u0437\u0430\u0449\u0438\u0449\u0435\u043d\u043d\u043e, \u043d\u043e \u0435\u0433\u043e \u0436\u0435 \u0434\u0435\u043b\u0430\u043b \u044f, \u0442\u0430\u043a \u0447\u0442\u043e \u043c\u043d\u0435 \u043d\u0435\u0447\u0435\u0433\u043e \u0431\u044b\u043b\u043e \u0431\u043e\u044f\u0442\u044c\u0441\u044f.<\/p>\n<p>\u0414\u0430\u043b\u0435\u0435 \u044f \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043d\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0435(\u0442.\u0435 \u0430\u043b\u0433\u043e\u0440\u0438\u0442\u043c, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u044e\u0437\u0435\u0440\u0430 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443, \u0438 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u043d\u0430 \u0441\u0442\u043e\u0440\u043e\u043d\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0430). \u0412 \u043f\u0430\u043f\u043a\u0435 scr \u044f \u0441\u0434\u0435\u043b\u0430\u043b \u043d\u043e\u0432\u0443\u044e \u043f\u0430\u043f\u043a\u0443 socket, \u0433\u0434\u0435 \u0441\u0434\u0435\u043b\u0430\u043b \u0444\u0430\u0439\u043b index.js.<\/p>\n<pre><code class=\"javascript\">import {io} from 'socket.io-client';  const options = { \"force new connection\": true, reconnectionAttempts: \"Infinity\", \/\/ avoid having user reconnect manually in order to prevent dead clients after a server restart timeout : 10000, \/\/ before connect_error and connect_timeout are emitted. transports : [\"websocket\"] }  const socket = io('\/', options);  export default socket;<\/code><\/pre>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c, \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0432 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443, \u044f \u043f\u0440\u0438\u0441\u0442\u0443\u043f\u0438\u043b \u043a \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044e \u043c\u0435\u0442\u043e\u0434\u0430 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u043a\u043e\u043c\u043d\u0430\u0442, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u0441\u043e\u0437\u0434\u0430\u043d\u044b, \u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044e \u043c\u0435\u0434\u0438\u0430\u043a\u043e\u043d\u0442\u0435\u043d\u0442\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d \u043e\u0442 \u0434\u0440\u0443\u0433\u0438\u0445 \u043a\u043e\u043d\u0435\u043a\u0442\u043e\u0432. \u0418 \u0442\u0430\u043a\u0436\u0435 \u044f \u043e\u043f\u0438\u0441\u0430\u043b \u0432\u0441\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c \u0441\u043e\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044b \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435. \u0424\u0430\u0439\u043b actions.js \u044f \u0441\u0434\u0435\u043b\u0430\u043b \u0432 \u0442\u043e\u0439 \u0436\u0435 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u0438, \u0447\u0442\u043e \u0438 index.js, \u043e\u0442\u0432\u0435\u0447\u0430\u044e\u0449\u0438\u0439 \u0437\u0430 \u043a\u043e\u043d\u0435\u043a\u0442 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443.<\/p>\n<pre><code class=\"javascript\">const ACTIONS = { JOIN: 'join', LEAVE: 'leave', SHARE_ROOMS: 'share-rooms', ADD_PEER: 'add-peer', REMOVE_PEER: 'remove-peer', RELAY_SDP: 'relay-sdp', RELAY_ICE: 'relay-ice', ICE_CANDIDATE: 'ice-candidate', SESSION_DESCRIPTION: 'session-description' };  module.exports = ACTIONS;<\/code><\/pre>\n<p>\u0412 \u0444\u0430\u0439\u043b server.js \u044f \u0434\u043e\u0431\u0430\u0432\u0438\u043b:<\/p>\n<pre><code class=\"javascript\">const ACTIONS = require('.\/src\/socket\/actions');  function getClientRooms() { const {rooms} = io.sockets.adapter;  return Array.from(rooms.keys()); }  function shareRoomsInfo() { io.emit(ACTIONS.SHARE_ROOMS, { rooms: getClientRooms() }) }  io.on('connection', socket => { shareRoomsInfo();  socket.on(ACTIONS.JOIN, config => { const {room: roomID} = config; const {rooms: joinedRooms} = socket;  if (Array.from(joinedRooms).includes(roomID)) { return console.warn(`Already joined to ${roomID}`); }  const clients = Array.from(io.sockets.adapter.rooms.get(roomID) || []);  clients.forEach(clientID => { io.to(clientID).emit(ACTIONS.ADD_PEER, { peerID: socket.id, createOffer: false });  socket.emit(ACTIONS.ADD_PEER, { peerID: clientID, createOffer: true, }); });  socket.join(roomID); shareRoomsInfo(); });  function leaveRoom() { const {rooms} = socket;  Array.from(rooms) \/\/ LEAVE ONLY CLIENT CREATED ROOM .forEach(roomID => { const clients = Array.from(io.sockets.adapter.rooms.get(roomID) || []);  clients.forEach(clientID => { io.to(clientID).emit(ACTIONS.REMOVE_PEER, { peerID: socket.id, });  socket.emit(ACTIONS.REMOVE_PEER, { peerID: clientID, }); });  socket.leave(roomID); });  shareRoomsInfo(); }  socket.on(ACTIONS.LEAVE, leaveRoom); socket.on('disconnecting', leaveRoom);<\/code><\/pre>\n<p>\u041f\u043e\u0441\u043b\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u0444\u0443\u043d\u043a\u0446\u0438\u0439 \u0432 server.js \u044f \u043d\u0430\u0447\u0430\u043b \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c \u043a\u043d\u043e\u043f\u043a\u0438 \u043d\u0430 \u0441\u0430\u0439\u0442\u0435 \u0438 \u043f\u0440\u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0442\u044c \u043a \u043d\u0438\u043c \u043b\u043e\u0433\u0438\u043a\u0443. \u0412 \u043f\u0435\u0440\u0432\u0443\u044e \u043e\u0447\u0435\u0440\u0435\u0434\u044c \u044f \u043f\u0435\u0440\u0435\u0448\u0435\u043b \u043a \u0444\u0430\u0439\u043b\u0443, \u043e\u0442\u0432\u0435\u0447\u0430\u044e\u0449\u0435\u043c\u0443 \u0437\u0430 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0433\u043b\u0430\u0432\u043d\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b. \u0422\u0430\u043c \u044f \u043f\u0440\u043e\u043f\u0438\u0441\u044b\u0432\u0430\u043b \u043b\u043e\u0433\u0438\u043a\u0443 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0445 \u043a\u043e\u043c\u043d\u0430\u0442 \u0438 \u043a\u043d\u043e\u043f\u043a\u0443 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f room.<\/p>\n<pre><code class=\"javascript\">import {useState, useEffect, useRef} from 'react'; import socket from '..\/..\/socket'; import ACTIONS from '..\/..\/socket\/actions'; import {useHistory} from 'react-router'; import {v4} from 'uuid';  export default function Main() { const history = useHistory(); const [rooms, updateRooms] = useState([]); const rootNode = useRef();  useEffect(() => { socket.on(ACTIONS.SHARE_ROOMS, ({rooms = []} = {}) => {  }); }, []);  return ( &lt;div> &lt;h1>Available Rooms&lt;\/h1>  &lt;ul> {rooms.map(roomID => ( &lt;li key={roomID}> {roomID} &lt;button onClick={() => { history.push(`\/room\/${roomID}`); }}>JOIN ROOM&lt;\/button> &lt;\/li> ))} &lt;\/ul>  &lt;button onClick={() => { history.push(`\/room\/${v4()}`); }}>Create New Room&lt;\/button> &lt;\/div> ); }<\/code><\/pre>\n<p>\u0422\u0430\u043a \u0436\u0435 \u044f \u043f\u0435\u0440\u0435\u043f\u0438\u0441\u0430\u043b server.js, \u0442.\u043a \u043f\u0440\u0438 \u043e\u0442\u043a\u0440\u044b\u0442\u0438\u0438 \u0441\u0430\u0439\u0442\u0430 \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u043b\u043e\u0441\u044c, \u0447\u0442\u043e \u043a\u043e\u043c\u043d\u0430\u0442\u0430 \u0443\u0436\u0435 \u0435\u0441\u0442\u044c, \u0445\u043e\u0442\u044f \u0435\u0435 \u043d\u0438\u043a\u0442\u043e \u043d\u0435 \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u043b. \u042d\u0442\u043e \u0441\u0432\u044f\u0437\u0430\u043d\u043e \u0441 \u0442\u0435\u043c, \u0447\u0442\u043e \u043f\u0440\u0438 \u0437\u0430\u0445\u043e\u0434\u0435 \u043d\u0430 \u0441\u0430\u0439\u0442, \u043d\u0430\u0448 \u0441\u043e\u043a\u0435\u0442 \u0443\u0436\u0435 \u043a \u0447\u0435\u043c\u0443-\u0442\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d, \u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u043d\u0443\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u043e\u0442\u0444\u0438\u043b\u044c\u0442\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u0432\u044b\u0432\u043e\u0434\u0438\u043c\u044b\u0445 \u043a\u043e\u043c\u043d\u0430\u0442 \u043d\u0430 \u044d\u043a\u0440\u0430\u043d. \u042d\u0442\u0430 \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 server.js.<\/p>\n<pre><code class=\"javascript\">function getClientRooms() { const {rooms} = io.sockets.adapter; return Array.from(rooms.keys()).filter(roomID => validate(roomID) &amp;&amp; version(roomID) === 4); }<\/code><\/pre>\n<p>\u041f\u043e\u0442\u043e\u043c \u044f \u0441\u0442\u0430\u043b \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u044b\u0432\u0430\u0442\u044c \u0441\u0430\u043c\u0438 \u043a\u043e\u043c\u043d\u0430\u0442\u044b. \u0414\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0439 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b \u0431\u044b\u043b\u0438 \u0445\u0443\u043a\u0438, \u0432 \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u0431\u0443\u0434\u0435\u043c \u043f\u043e\u0434\u043f\u0438\u0441\u044b\u0432\u0430\u0442\u044c\u0441\u044f \u043d\u0430 \u0432\u0441\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f. \u042f \u0441\u043e\u0437\u0434\u0430\u043b \u043f\u0430\u043f\u043a\u0443 src\/hooks, \u0430 \u0432 \u043d\u0435\u0439 \u0444\u0430\u0439\u043b useWebRTC.js<\/p>\n<pre><code class=\"javascript\">import {useEffect, useRef, useCallback} from 'react'; import freeice from 'freeice'; import useStateWithCallback from '.\/useStateWithCallback'; import socket from '..\/socket'; import ACTIONS from '..\/socket\/actions';  export const LOCAL_VIDEO = 'LOCAL_VIDEO';   export default function useWebRTC(roomID) {   const [clients, updateClients] = useStateWithCallback([]);    const addNewClient = useCallback((newClient, cb) => {     updateClients(list => {       if (!list.includes(newClient)) {         return [...list, newClient]       }        return list;     }, cb);   }, [clients, updateClients]);    const peerConnections = useRef({});   const localMediaStream = useRef(null);   const peerMediaElements = useRef({     [LOCAL_VIDEO]: null,   });    useEffect(() => {     async function handleNewPeer({peerID, createOffer}) {       if (peerID in peerConnections.current) {         return console.warn(`Already connected to peer ${peerID}`);       }        peerConnections.current[peerID] = new RTCPeerConnection({         iceServers: freeice(),       });        peerConnections.current[peerID].onicecandidate = event => {         if (event.candidate) {           socket.emit(ACTIONS.RELAY_ICE, {             peerID,             iceCandidate: event.candidate,           });         }       }        let tracksNumber = 0;       peerConnections.current[peerID].ontrack = ({streams: [remoteStream]}) => {         tracksNumber++          if (tracksNumber === 2) { \/\/ video &amp; audio tracks received           tracksNumber = 0;           addNewClient(peerID, () => {             if (peerMediaElements.current[peerID]) {               peerMediaElements.current[peerID].srcObject = remoteStream;             } else {               \/\/ FIX LONG RENDER IN CASE OF MANY CLIENTS               let settled = false;               const interval = setInterval(() => {                 if (peerMediaElements.current[peerID]) {                   peerMediaElements.current[peerID].srcObject = remoteStream;                   settled = true;                 }                  if (settled) {<\/code><\/pre>\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-330041","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/330041","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=330041"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/330041\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=330041"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=330041"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=330041"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}