(ΠŸΠΎΡ‡Ρ‚ΠΈ) бСсполСзный стриминг Π²Π΅Π±ΠΊΠ°ΠΌΠ΅Ρ€Ρ‹ ΠΈΠ· Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°. Media Stream ΠΈ Websockets

Π’ ΡΡ‚Π°Ρ‚ΡŒΠ΅ я Ρ…ΠΎΡ‡Ρƒ ΠΏΠΎΠ΄Π΅Π»ΠΈΡ‚ΡŒΡΡ своими ΠΏΠΎΠΏΡ‹Ρ‚ΠΊΠ°ΠΌΠΈ ΡΠ΄Π΅Π»Π°Ρ‚ΡŒ стримминг Π²ΠΈΠ΄Π΅ΠΎ Ρ‡Π΅Ρ€Π΅Π· websockets Π±Π΅Π· использования сторонних ΠΏΠ»Π°Π³ΠΈΠ½ΠΎΠ² Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π° Ρ‚ΠΈΠΏΠ° Adobe Flash Player. Π§Ρ‚ΠΎ ΠΈΠ· этого ΠΏΠΎΠ»ΡƒΡ‡ΠΈΠ»ΠΎΡΡŒ Ρ‡ΠΈΡ‚Π°ΠΉΡ‚Π΅ Π΄Π°Π»Π΅Π΅.

Adobe Flash β€” Ρ€Π°Π½Π΅Π΅ Macromedia Flash, это ΠΏΠ»Π°Ρ‚Ρ„ΠΎΡ€ΠΌΠ° для создания ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΉ, Ρ€Π°Π±ΠΎΡ‚Π°ΡŽΡ‰ΠΈΡ… Π² Π²Π΅Π±-Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π΅. Π”ΠΎ внСдрСния Media Stream API это Π±Ρ‹Π»Π° практичСски СдинствСнная ΠΏΠ»Π°Ρ‚Ρ„ΠΎΡ€ΠΌΠ° для стримминга с Π²Π΅Π±-ΠΊΠ°ΠΌΠ΅Ρ€Ρ‹ Π²ΠΈΠ΄Π΅ΠΎ ΠΈ голоса, Π° Ρ‚Π°ΠΊΠΆΠ΅ для создания Ρ€Π°Π·Π»ΠΈΡ‡Π½ΠΎΠ³ΠΎ Ρ€ΠΎΠ΄Π° ΠΊΠΎΠ½Ρ„Π΅Ρ€Π΅Π½Ρ†ΠΈΠΉ ΠΈ Ρ‡Π°Ρ‚ΠΎΠ² Π² Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π΅. ΠŸΡ€ΠΎΡ‚ΠΎΠΊΠΎΠ» для ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‡ΠΈ ΠΌΠ΅Π΄ΠΈΠ°-ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΠΈ RTMP (Real Time Messaging Protocol), Π±Ρ‹Π» фактичСски Π·Π°ΠΊΡ€Ρ‹Ρ‚Ρ‹ΠΌ Π΄ΠΎΠ»Π³ΠΎΠ΅ врСмя, Ρ‡Ρ‚ΠΎ ΠΎΠ·Π½Π°Ρ‡Π°Π»ΠΎ: Ссли Ρ…ΠΎΡ‡Π΅ΡˆΡŒ ΠΏΠΎΠ΄Π½ΡΡ‚ΡŒ свой стримминг-сСрвис, Π±ΡƒΠ΄ΡŒ Π΄ΠΎΠ±Ρ€ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉ софт ΠΎΡ‚ самих Adobe β€” Adobe Media Server (AMS).

Π§Π΅Ρ€Π΅Π· Π½Π΅ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ врСмя Π² 2012 Adobe «сдались ΠΈ Π²Ρ‹ΠΏΠ»ΡŽΠ½ΡƒΠ»ΠΈΒ» Π½Π° суд ΠΏΡƒΠ±Π»ΠΈΠΊΠΈ ΡΠΏΠ΅Ρ†ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΡŽ ΠΏΡ€ΠΎΡ‚ΠΎΠΊΠΎΠ»Π° RTMP, которая содСрТала ошибки ΠΈ ΠΏΠΎ сути Π±Ρ‹Π»Π° Π½Π΅ ΠΏΠΎΠ»Π½ΠΎΠΉ. К Ρ‚ΠΎΠΌΡƒ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΈ Π½Π°Ρ‡Π°Π»ΠΈ Π΄Π΅Π»Π°Ρ‚ΡŒ свои Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ этого ΠΏΡ€ΠΎΡ‚ΠΎΠΊΠΎΠ»Π°, Ρ‚Π°ΠΊ появился сСрвСр Wowza. Π’ 2011 Adobe ΠΏΠΎΠ΄Π°Π»Π° иск Π½Π° Wowza Π·Π° нСлСгальноС использованиС ΠΏΠ°Ρ‚Π΅Π½Ρ‚ΠΎΠ² связанных с RTMP, Ρ‡Π΅Ρ€Π΅Π· 4 Π³ΠΎΠ΄Π° ΠΊΠΎΠ½Ρ„Π»ΠΈΠΊΡ‚ Ρ€Π°Π·Ρ€Π΅ΡˆΠΈΠ»ΡΡ ΠΌΠΈΡ€ΠΎΠΌ.

Adobe Flash ΠΏΠ»Π°Ρ‚Ρ„ΠΎΡ€ΠΌΠ΅ ΡƒΠΆΠ΅ большС 20 Π»Π΅Ρ‚, Π·Π° это врСмя ΠΎΠ±Π½Π°Ρ€ΡƒΠΆΠΈΠ»ΠΎΡΡŒ мноТСство критичСских уязвимостСй, ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΡƒ ΠΎΠ±Π΅Ρ‰Π°Π»ΠΈ ΠΏΡ€Π΅ΠΊΡ€Π°Ρ‚ΠΈΡ‚ΡŒ ΠΊ 2020 Π³ΠΎΠ΄Ρƒ, Ρ‚Π°ΠΊ Ρ‡Ρ‚ΠΎ Π°Π»ΡŒΡ‚Π΅Ρ€Π½Π°Ρ‚ΠΈΠ² для стримингового сСрвиса остаСтся Π½Π΅ Ρ‚Π°ΠΊ ΡƒΠΆ ΠΈ ΠΌΠ½ΠΎΠ³ΠΎ.

Для своСго ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π° я сразу Ρ€Π΅ΡˆΠΈΠ» ΠΏΠΎΠ»Π½ΠΎΡΡ‚ΡŒΡŽ ΠΎΡ‚ΠΊΠ°Π·Π°Ρ‚ΡŒΡΡ ΠΎΡ‚ использования Flash Π² Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π΅. ΠžΡΠ½ΠΎΠ²Π½ΡƒΡŽ ΠΏΡ€ΠΈΡ‡ΠΈΠ½Ρƒ я ΡƒΠΊΠ°Π·Π°Π» Π²Ρ‹ΡˆΠ΅, Ρ‚Π°ΠΊΠΆΠ΅ Flash совсСм Π½Π΅ поддСрТиваСтся Π½Π° ΠΌΠΎΠ±ΠΈΠ»ΡŒΠ½Ρ‹Ρ… ΠΏΠ»Π°Ρ‚Ρ„ΠΎΡ€ΠΌΠ°Ρ…, Π΄Π° ΠΈ Ρ€Π°Π·Π²ΠΎΡ€Π°Ρ‡ΠΈΠ²Π°Ρ‚ΡŒ Adobe Flash для Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ Π½Π° windows (эмуляторС wine) совсСм ΡƒΠΆ Π½Π΅ Ρ…ΠΎΡ‚Π΅Π»ΠΎΡΡŒ. ΠŸΠΎΡΡ‚ΠΎΠΌΡƒ я взялся ΠΏΠΈΡΠ°Ρ‚ΡŒ ΠΊΠ»ΠΈΠ΅Π½Ρ‚ Π½Π° JavaScript. Π­Ρ‚ΠΎ Π±ΡƒΠ΄Π΅Ρ‚ всСго лишь ΠΏΡ€ΠΎΡ‚ΠΎΡ‚ΠΈΠΏ, Ρ‚Π°ΠΊ ΠΊΠ°ΠΊ Π² дальнСйшСм я ΡƒΠ·Π½Π°Π», Ρ‡Ρ‚ΠΎ стриминг ΠΌΠΎΠΆΠ½ΠΎ ΡΠ΄Π΅Π»Π°Ρ‚ΡŒ Π³ΠΎΡ€Π°Π·Π΄ΠΎ эффСктивнСС Π½Π° основС p2p, Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Ρƒ мСня это Π±ΡƒΠ΄Π΅Ρ‚ peer β€” server β€” peers, Π½ΠΎ ΠΎΠ± этом Π² Π΄Ρ€ΡƒΠ³ΠΎΠΉ Ρ€Π°Π·, ΠΏΠΎΡ‚ΠΎΠΌΡƒ Ρ‡Ρ‚ΠΎ это Π΅Ρ‰Π΅ Π½Π΅ Π³ΠΎΡ‚ΠΎΠ²ΠΎ.

Для Π½Π°Ρ‡Π°Π»Π° Ρ€Π°Π±ΠΎΡ‚Ρ‹ Π½Π°ΠΌ Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌ собствСнно websockets-сСрвСр. Π― сдСлал ΠΏΡ€ΠΎΡΡ‚Π΅ΠΉΡˆΠΈΠΉ Π½Π° основС go-ΠΏΠ°ΠΊΠ΅Ρ‚Π° melody:

Код сСрвСрной части

package main

import (
	"errors"
	"github.com/go-chi/chi"
	"gopkg.in/olahol/melody.v1"
	"log"
	"net/http"
	"time"
)

func main() {
	r := chi.NewRouter()
	m := melody.New()

	m.Config.MaxMessageSize = 204800

	r.Get("/", func(w http.ResponseWriter, r *http.Request) {
		http.ServeFile(w, r, "public/index.html")
	})
	r.Get("/ws", func(w http.ResponseWriter, r *http.Request) {
		m.HandleRequest(w, r)
	})

         // Бродкастим Π²ΠΈΠ΄Π΅ΠΎ ΠΏΠΎΡ‚ΠΎΠΊ 
	m.HandleMessageBinary(func(s *melody.Session, msg []byte) {
		m.BroadcastBinary(msg)
	})

	log.Println("Starting server...")

	http.ListenAndServe(":3000", r)
}

На ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π΅ (Ρ‚Ρ€Π°Π½ΡΠ»ΠΈΡ€ΡƒΡŽΡ‰Π΅ΠΉ сторонС) сначала Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΠΎ ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ доступ ΠΊ ΠΊΠ°ΠΌΠ΅Ρ€Π΅. ДСлаСтся это Ρ‡Π΅Ρ€Π΅Π· MediaStream API.

ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ доступ (Ρ€Π°Π·Ρ€Π΅ΡˆΠ΅Π½ΠΈΠ΅) ΠΊ ΠΊΠ°ΠΌΠ΅Ρ€Π΅/ΠΌΠΈΠΊΡ€ΠΎΡ„ΠΎΠ½Ρƒ Ρ‡Π΅Ρ€Π΅Π· Media Devices API. Π­Ρ‚ΠΎ API прСдоставляСт ΠΌΠ΅Ρ‚ΠΎΠ΄ MediaDevices.getUserMedia(), ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΏΠΎΠΊΠ°Π·Ρ‹Π²Π°Π΅Ρ‚ вспл. окошко, ΡΠΏΡ€Π°ΡˆΠΈΠ²Π°ΡŽΡ‰Π΅Π΅ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Ρ€Π°Π·Ρ€Π΅ΡˆΠ΅Π½ΠΈΡ доступа ΠΊ ΠΊΠ°ΠΌΠ΅Ρ€Π΅ ΠΈΠ»ΠΈ/ΠΈ ΠΌΠΈΠΊΡ€ΠΎΡ„ΠΎΠ½Ρƒ. Π₯ΠΎΡ‚Π΅Π»ΠΎΡΡŒ Π±Ρ‹ ΠΎΡ‚ΠΌΠ΅Ρ‚ΠΈΡ‚ΡŒ, Ρ‡Ρ‚ΠΎ всС экспСримСнты я ΠΏΡ€ΠΎΠ²ΠΎΠ΄ΠΈΠ» Π² Google Chrome, Π½ΠΎ, Π΄ΡƒΠΌΠ°ΡŽ, Π² Firefox всС Π±ΡƒΠ΄Π΅Ρ‚ Ρ€Π°Π±ΠΎΡ‚Π°Ρ‚ΡŒ ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π½ΠΎ Ρ‚Π°ΠΊΠΆΠ΅.

Π”Π°Π»Π΅Π΅ getUserMedia() Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ Promise, Π² ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ ΠΏΠ΅Ρ€Π΅Π΄Π°Π΅Ρ‚ MediaStream ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ β€” ΠΏΠΎΡ‚ΠΎΠΊ Π²ΠΈΠ΄Π΅ΠΎ-Π°ΡƒΠ΄ΠΈΠΎ Π΄Π°Π½Π½Ρ‹Ρ…. Π­Ρ‚ΠΎΡ‚ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ ΠΌΡ‹ присваиваСм Π² src свойство элСмСнта video. Код:

Π’Ρ€Π°Π½ΡΠ»ΠΈΡ€ΡƒΡŽΡ‰Π°Ρ сторона

<style>
  #videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; }
</style>
</head>
<body>
<!-- Π—Π΄Π΅ΡΡŒ Π² этом "ΠΎΠΊΠΎΡˆΠ΅Ρ‡ΠΊΠ΅" ΠΊΠ»ΠΈΠ΅Π½Ρ‚ Π±ΡƒΠ΄Π΅Ρ‚ Π²ΠΈΠ΄Π΅Ρ‚ΡŒ сСбя -->
<video autoplay id="videoObjectHtml5ApiServer"></video>

<script type="application/javascript">
  var
        video = document.getElementById('videoObjectHtml5ApiServer');

// Ссли доступСн MediaDevices API, пытаСмся ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ доступ ΠΊ ΠΊΠ°ΠΌΠ΅Ρ€Π΅ (ΠΌΠΎΠΆΠ½ΠΎ Π΅Ρ‰Π΅ ΠΈ ΠΊ ΠΌΠΈΠΊΡ€ΠΎΡ„ΠΎΠ½Ρƒ)
// getUserMedia Π²Π΅Ρ€Π½Π΅Ρ‚ ΠΎΠ±Π΅Ρ‰Π°Π½ΠΈΠ΅, Π½Π° ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ подписываСмся ΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½Π½Ρ‹ΠΉ Π²ΠΈΠ΄Π΅ΠΎΠΏΠΎΡ‚ΠΎΠΊ Π² ΠΊΠΎΠ»Π±Π΅ΠΊΠ΅ направляСм Π² video ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ Π½Π° страницС

if (navigator.mediaDevices.getUserMedia) {
        navigator.mediaDevices.getUserMedia({video: true}).then(function (stream) {
          // Π²ΠΈΠ΄Π΅ΠΎ ΠΏΠΎΡ‚ΠΎΠΊ привязываСм ΠΊ video Ρ‚Π΅Π³Ρƒ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΊΠ»ΠΈΠ΅Π½Ρ‚ ΠΌΠΎΠ³ Π²ΠΈΠ΄Π΅Ρ‚ΡŒ сСбя ΠΈ ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ 
          video.srcObject = stream;
        });
}
</script>

Π§Ρ‚ΠΎΠ±Ρ‹ Ρ‚Ρ€Π°Π½ΡΠ»ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Π²ΠΈΠ΄Π΅ΠΎΠΏΠΎΡ‚ΠΎΠΊ Ρ‡Π΅Ρ€Π΅Π· сокСты, Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΠΎ Π΅Π³ΠΎ ΠΊΠ°ΠΊ-Ρ‚ΠΎ Π³Π΄Π΅-Ρ‚ΠΎ ΠΊΠΎΠ΄ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ, Π±ΡƒΡ„Π΅Ρ€ΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ, ΠΈ ΠΏΠ΅Ρ€Π΅Π΄Π°Π²Π°Ρ‚ΡŒ частями. Π‘Ρ‹Ρ€ΠΎΠΉ Π²ΠΈΠ΄Π΅ΠΎΠΏΠΎΡ‚ΠΎΠΊ Π½Π΅ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‚ΡŒ Ρ‡Π΅Ρ€Π΅Π· websockets. Π’ΡƒΡ‚ Π½Π° ΠΏΠΎΠΌΠΎΡ‰ΡŒ Π½Π°ΠΌ ΠΏΡ€ΠΈΡ…ΠΎΠ΄ΠΈΡ‚ MediaRecorder API. Π”Π°Π½Π½Ρ‹ΠΉ API позволяСт ΠΊΠΎΠ΄ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΈ Ρ€Π°Π·Π±ΠΈΠ²Π°Ρ‚ΡŒ ΠΏΠΎΡ‚ΠΎΠΊ Π½Π° кусочки. ΠšΠΎΠ΄ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ я дСлаю для сТатия Π²ΠΈΠ΄Π΅ΠΎΠΏΠΎΡ‚ΠΎΠΊΠ°, Ρ‡Ρ‚ΠΎΠ±Ρ‹ мСньшС Π³ΠΎΠ½ΡΡ‚ΡŒ Π±Π°ΠΉΡ‚ΠΎΠ² ΠΏΠΎ сСти. Π Π°Π·Π±ΠΈΠ² Π½Π° куски, ΠΌΠΎΠΆΠ½ΠΎ ΠΊΠ°ΠΆΠ΄Ρ‹ΠΉ кусок ΠΎΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ Π² websocket. Код:

ΠšΠΎΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ Π²ΠΈΠ΄Π΅ΠΎΠΏΠΎΡ‚ΠΎΠΊ, бьСм Π΅Π³ΠΎ Π½Π° части

<style>
  #videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; }
</style>
</head>
<body>
<!-- Π—Π΄Π΅ΡΡŒ Π² этом "ΠΎΠΊΠΎΡˆΠ΅Ρ‡ΠΊΠ΅" ΠΊΠ»ΠΈΠ΅Π½Ρ‚ Π±ΡƒΠ΄Π΅Ρ‚ Π²ΠΈΠ΄Π΅Ρ‚ΡŒ сСбя -->
<video autoplay id="videoObjectHtml5ApiServer"></video>

<script type="application/javascript">
  var
        video = document.getElementById('videoObjectHtml5ApiServer');

// Ссли доступСн MediaDevices API, пытаСмся ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ доступ ΠΊ ΠΊΠ°ΠΌΠ΅Ρ€Π΅ (ΠΌΠΎΠΆΠ½ΠΎ Π΅Ρ‰Π΅ ΠΈ ΠΊ ΠΌΠΈΠΊΡ€ΠΎΡ„ΠΎΠ½Ρƒ)
// getUserMedia Π²Π΅Ρ€Π½Π΅Ρ‚ ΠΎΠ±Π΅Ρ‰Π°Π½ΠΈΠ΅, Π½Π° ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ подписываСмся ΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½Π½Ρ‹ΠΉ Π²ΠΈΠ΄Π΅ΠΎΠΏΠΎΡ‚ΠΎΠΊ Π² ΠΊΠΎΠ»Π±Π΅ΠΊΠ΅ направляСм Π² video ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ Π½Π° страницС

if (navigator.mediaDevices.getUserMedia) {
        navigator.mediaDevices.getUserMedia({video: true}).then(function (stream) {
          // Π²ΠΈΠ΄Π΅ΠΎ ΠΏΠΎΡ‚ΠΎΠΊ привязываСм ΠΊ video Ρ‚Π΅Π³Ρƒ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΊΠ»ΠΈΠ΅Π½Ρ‚ ΠΌΠΎΠ³ Π²ΠΈΠ΄Π΅Ρ‚ΡŒ сСбя ΠΈ ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ 
          video.srcObject = s;
          var
            recorderOptions = {
                mimeType: 'video/webm; codecs=vp8' // Π±ΡƒΠ΄Π΅ΠΌ ΠΊΠΎΠ΄ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Π²ΠΈΠ΄Π΅ΠΎΠΏΠΎΡ‚ΠΎΠΊ Π² Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ webm ΠΊΠΎΠ΄Π΅ΠΊΠΎΠΌ vp8
              },
              mediaRecorder = new MediaRecorder(s, recorderOptions ); // ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ MediaRecorder

               mediaRecorder.ondataavailable = function(e) {
                if (e.data && e.data.size > 0) {
                  // ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ кусочСк Π²ΠΈΠ΄Π΅ΠΎΠΏΠΎΡ‚ΠΎΠΊΠ° Π² e.data
                }
            }

            mediaRecorder.start(100); // Π΄Π΅Π»ΠΈΡ‚ ΠΏΠΎΡ‚ΠΎΠΊ Π½Π° кусочки ΠΏΠΎ 100 мс ΠΊΠ°ΠΆΠ΄Ρ‹ΠΉ

        });
}
</script>

Π’Π΅ΠΏΠ΅Ρ€ΡŒ Π΄ΠΎΠ±Π°Π²ΠΈΠΌ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‡Ρƒ ΠΏΠΎ websockets. Как Π½ΠΈ ΡƒΠ΄ΠΈΠ²ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎ, для этого Π½ΡƒΠΆΠ΅Π½ лишь ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ WebSocket. Π˜ΠΌΠ΅Π΅Ρ‚ всСго Π΄Π²Π° ΠΌΠ΅Ρ‚ΠΎΠ΄Π° send ΠΈ close. Названия говорят сами Π·Π° сСбя. Π”ΠΎΠΏΠΎΠ»Π½Π΅Π½Π½Ρ‹ΠΉ ΠΊΠΎΠ΄:

ΠŸΠ΅Ρ€Π΅Π΄Π°Π΅ΠΌ Π²ΠΈΠ΄Π΅ΠΎΠΏΠΎΡ‚ΠΎΠΊ Π½Π° сСрвСр

<style>
  #videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; }
</style>
</head>
<body>
<!-- Π—Π΄Π΅ΡΡŒ Π² этом "ΠΎΠΊΠΎΡˆΠ΅Ρ‡ΠΊΠ΅" ΠΊΠ»ΠΈΠ΅Π½Ρ‚ Π±ΡƒΠ΄Π΅Ρ‚ Π²ΠΈΠ΄Π΅Ρ‚ΡŒ сСбя -->
<video autoplay id="videoObjectHtml5ApiServer"></video>

<script type="application/javascript">
  var
        video = document.getElementById('videoObjectHtml5ApiServer');

// Ссли доступСн MediaDevices API, пытаСмся ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ доступ ΠΊ ΠΊΠ°ΠΌΠ΅Ρ€Π΅ (ΠΌΠΎΠΆΠ½ΠΎ Π΅Ρ‰Π΅ ΠΈ ΠΊ ΠΌΠΈΠΊΡ€ΠΎΡ„ΠΎΠ½Ρƒ)
// getUserMedia Π²Π΅Ρ€Π½Π΅Ρ‚ ΠΎΠ±Π΅Ρ‰Π°Π½ΠΈΠ΅, Π½Π° ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ подписываСмся ΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½Π½Ρ‹ΠΉ Π²ΠΈΠ΄Π΅ΠΎΠΏΠΎΡ‚ΠΎΠΊ Π² ΠΊΠΎΠ»Π±Π΅ΠΊΠ΅ направляСм Π² video ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ Π½Π° страницС

if (navigator.mediaDevices.getUserMedia) {
        navigator.mediaDevices.getUserMedia({video: true}).then(function (stream) {
          // Π²ΠΈΠ΄Π΅ΠΎ ΠΏΠΎΡ‚ΠΎΠΊ привязываСм ΠΊ video Ρ‚Π΅Π³Ρƒ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΊΠ»ΠΈΠ΅Π½Ρ‚ ΠΌΠΎΠ³ Π²ΠΈΠ΄Π΅Ρ‚ΡŒ сСбя ΠΈ ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ 
          video.srcObject = s;
          var
            recorderOptions = {
                mimeType: 'video/webm; codecs=vp8' // Π±ΡƒΠ΄Π΅ΠΌ ΠΊΠΎΠ΄ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Π²ΠΈΠ΄Π΅ΠΎΠΏΠΎΡ‚ΠΎΠΊ Π² Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ webm ΠΊΠΎΠ΄Π΅ΠΊΠΎΠΌ vp8
              },
              mediaRecorder = new MediaRecorder(s, recorderOptions ), // ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ MediaRecorder
              socket = new WebSocket('ws://127.0.0.1:3000/ws');

               mediaRecorder.ondataavailable = function(e) {
                if (e.data && e.data.size > 0) {
                  // ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ кусочСк Π²ΠΈΠ΄Π΅ΠΎΠΏΠΎΡ‚ΠΎΠΊΠ° Π² e.data
                 socket.send(e.data);
                }
            }

            mediaRecorder.start(100); // Π΄Π΅Π»ΠΈΡ‚ ΠΏΠΎΡ‚ΠΎΠΊ Π½Π° кусочки ΠΏΠΎ 100 мс ΠΊΠ°ΠΆΠ΄Ρ‹ΠΉ

        }).catch(function (err) { console.log(err); });
}
</script>

Π’Ρ€Π°Π½ΡΠ»ΠΈΡ€ΡƒΡŽΡ‰Π°Ρ сторона Π³ΠΎΡ‚ΠΎΠ²Π°! Π’Π΅ΠΏΠ΅Ρ€ΡŒ Π΄Π°Π²Π°ΠΉΡ‚Π΅ ΠΏΠΎΠΏΡ€ΠΎΠ±ΡƒΠ΅ΠΌ ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Ρ‚ΡŒ Π²ΠΈΠ΄Π΅ΠΎΠΏΠΎΡ‚ΠΎΠΊ ΠΈ ΠΏΠΎΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ Π΅Π³ΠΎ Π½Π° ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π΅. Π§Ρ‚ΠΎ Π½Π°ΠΌ для этого понадобится? Π’ΠΎ-ΠΏΠ΅Ρ€Π²Ρ‹Ρ… ΠΊΠΎΠ½Π΅Ρ‡Π½ΠΎ ΠΆΠ΅ сокСт-соСдинСниС. На ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ WebSocket вСшаСм Β«ΡΠ»ΡƒΡˆΠ°Ρ‚Π΅Π»ΡŒΒ» (listener), подписываСмся Π½Π° событиС ‘message’. ΠŸΠΎΠ»ΡƒΡ‡ΠΈΠ² кусочСк Π±ΠΈΠ½Π°Ρ€Π½Ρ‹Ρ… Π΄Π°Π½Π½Ρ‹Ρ… наш сСрвСр бродкастит Π΅Π³ΠΎ подписчикам, Ρ‚ΠΎ Π΅ΡΡ‚ΡŒ ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π°ΠΌ. На ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π΅ ΠΏΡ€ΠΈ этом срабатываСт callback-функция связанная с Β«ΡΠ»ΡƒΡˆΠ°Ρ‚Π΅Π»Π΅ΠΌΒ» события ‘message’, Π² Π°Ρ€Π³ΡƒΠΌΠ΅Π½Ρ‚ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ пСрСдаСтся собствСнно сам ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ β€” кусочСк Π²ΠΈΠ΄Π΅ΠΎΠΏΠΎΡ‚ΠΎΠΊΠ°, Π·Π°ΠΊΠΎΠ΄ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹ΠΉ vp8.

ΠŸΡ€ΠΈΠ½ΠΈΠΌΠ°Π΅ΠΌ Π²ΠΈΠ΄Π΅ΠΎΠΏΠΎΡ‚ΠΎΠΊ

<style>
  #videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; }
</style>
</head>
<body>
<!-- Π—Π΄Π΅ΡΡŒ Π² этом "ΠΎΠΊΠΎΡˆΠ΅Ρ‡ΠΊΠ΅" ΠΊΠ»ΠΈΠ΅Π½Ρ‚ Π±ΡƒΠ΄Π΅Ρ‚ Π²ΠΈΠ΄Π΅Ρ‚ΡŒ тСбя -->
<video autoplay id="videoObjectHtml5ApiServer"></video>

<script type="application/javascript">
  var
        video = document.getElementById('videoObjectHtml5ApiServer'),
         socket = new WebSocket('ws://127.0.0.1:3000/ws'), 
         arrayOfBlobs = [];

         socket.addEventListener('message', function (event) {
                // "ΠΊΠ»Π°Π΄Π΅ΠΌ" ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½Π½Ρ‹ΠΉ кусочСк Π² массив 
                arrayOfBlobs.push(event.data);
                // здСсь Π±ΡƒΠ΄Π΅ΠΌ Ρ‡ΠΈΡ‚Π°Ρ‚ΡŒ кусочки
                readChunk();
            });
</script>

Π― Π΄ΠΎΠ»Π³ΠΎΠ΅ врСмя пытался ΠΏΠΎΠ½ΡΡ‚ΡŒ, ΠΏΠΎΡ‡Π΅ΠΌΡƒ нСльзя ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½Π½Ρ‹Π΅ кусочки сразу ΠΆΠ΅ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ Π½Π° воспроизвСдСниС элСмСнту video, Π½ΠΎ оказалось Ρ‚Π°ΠΊ ΠΊΠΎΠ½Π΅Ρ‡Π½ΠΎ нСльзя Π΄Π΅Π»Π°Ρ‚ΡŒ, Π½ΡƒΠΆΠ½ΠΎ сначала кусочСк ΠΏΠΎΠ»ΠΎΠΆΠΈΡ‚ΡŒ Π² ΡΠΏΠ΅Ρ†ΠΈΠ°Π»ΡŒΠ½Ρ‹ΠΉ Π±ΡƒΡ„Π΅Ρ€, привязанный ΠΊ элСмСнту video, ΠΈ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Ρ‚ΠΎΠ³Π΄Π° Π½Π°Ρ‡Π½Π΅Ρ‚ Π²ΠΎΡΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚ΡŒ Π²ΠΈΠ΄Π΅ΠΎΠΏΠΎΡ‚ΠΎΠΊ. Для этого понадобится MediaSource API ΠΈ FileReader API.

MediaSource выступаСт Π½Π΅ΠΊΠΈΠΌ посрСдником ΠΌΠ΅ΠΆΠ΄Ρƒ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ΠΎΠΌ воспроизвСдСния media ΠΈ источником Π΄Π°Π½Π½ΠΎΠ³ΠΎ ΠΏΠΎΡ‚ΠΎΠΊΠ° ΠΌΠ΅Π΄ΠΈΠ°. MediaSource ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ содСрТит ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π°Π΅ΠΌΡ‹ΠΉ Π±ΡƒΡ„Π΅Ρ€ для источника Π²ΠΈΠ΄Π΅ΠΎ/Π°ΡƒΠ΄ΠΈΠΎ ΠΏΠΎΡ‚ΠΎΠΊΠ°. Одна ΠΎΡΠΎΠ±Π΅Π½Π½ΠΎΡΡ‚ΡŒ Π·Π°ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ΡΡ Π² Ρ‚ΠΎΠΌ, Ρ‡Ρ‚ΠΎ Π±ΡƒΡ„Π΅Ρ€ ΠΌΠΎΠΆΠ΅Ρ‚ ΡΠΎΠ΄Π΅Ρ€ΠΆΠ°Ρ‚ΡŒ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π΄Π°Π½Π½Ρ‹Π΅ Ρ‚ΠΈΠΏΠ° Uint8, поэтому для создания Ρ‚Π°ΠΊΠΎΠ³ΠΎ Π±ΡƒΡ„Π΅Ρ€Π° потрСбуСтся FileReader. ΠŸΠΎΡΠΌΠΎΡ‚Ρ€ΠΈΡ‚Π΅ ΠΊΠΎΠ΄, ΠΈ станСт Π±ΠΎΠ»Π΅Π΅ понятно:

Воспроизводим Π²ΠΈΠ΄Π΅ΠΎΠΏΠΎΡ‚ΠΎΠΊ

<style>
  #videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; }
</style>
</head>
<body>
<!-- Π—Π΄Π΅ΡΡŒ Π² этом "ΠΎΠΊΠΎΡˆΠ΅Ρ‡ΠΊΠ΅" ΠΊΠ»ΠΈΠ΅Π½Ρ‚ Π±ΡƒΠ΄Π΅Ρ‚ Π²ΠΈΠ΄Π΅Ρ‚ΡŒ тСбя -->
<video autoplay id="videoObjectHtml5ApiServer"></video>

<script type="application/javascript">
  var
        video = document.getElementById('videoObjectHtml5ApiServer'),
         socket = new WebSocket('ws://127.0.0.1:3000/ws'),
        mediaSource = new MediaSource(), // ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ MediaSource
        vid2url = URL.createObjectURL(mediaSource), // создаСм ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ URL для связывания Π²ΠΈΠ΄Π΅ΠΎΠΏΠΎΡ‚ΠΎΠΊΠ° с ΠΏΡ€ΠΎΠΈΠ³Ρ€Ρ‹Π²Π°Ρ‚Π΅Π»Π΅ΠΌ
        arrayOfBlobs = [],
        sourceBuffer = null; // Π±ΡƒΡ„Π΅Ρ€, ΠΏΠΎΠΊΠ° Π½ΡƒΠ»ΡŒ-ΠΎΠ±ΡŠΠ΅ΠΊΡ‚

         socket.addEventListener('message', function (event) {
                // "ΠΊΠ»Π°Π΄Π΅ΠΌ" ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½Π½Ρ‹ΠΉ кусочСк Π² массив 
                arrayOfBlobs.push(event.data);
                // здСсь Π±ΡƒΠ΄Π΅ΠΌ Ρ‡ΠΈΡ‚Π°Ρ‚ΡŒ кусочки
                readChunk();
            });

         // ΠΊΠ°ΠΊ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ MediaSource Π±ΡƒΠ΄Π΅Ρ‚ ΠΎΠΏΠΎΠ²Π΅Ρ‰Π΅Π½ , Ρ‡Ρ‚ΠΎ источник Π³ΠΎΡ‚ΠΎΠ² ΠΎΡ‚Π΄Π°Π²Π°Ρ‚ΡŒ кусочки 
        // Π²ΠΈΠ΄Π΅ΠΎ/Π°ΡƒΠ΄ΠΈΠΎ ΠΏΠΎΡ‚ΠΎΠΊΠ°
        // создаСм Π±ΡƒΡ„Π΅Ρ€ , слСдуСт ΠΎΠ±Ρ€Π°Ρ‚ΠΈΡ‚ΡŒ Π²Π½ΠΈΠΌΠ°Π½ΠΈΠ΅, Ρ‡Ρ‚ΠΎ Π±ΡƒΡ„Π΅Ρ€ Π΄ΠΎΠ»ΠΆΠ΅Π½ Π·Π½Π°Ρ‚ΡŒ Π² ΠΊΠ°ΠΊΠΎΠΌ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Π΅ 
        // ΠΊΠ°ΠΊΠΈΠΌ ΠΊΠΎΠ΄Π΅ΠΊΠΎΠΌ Π±Ρ‹Π» Π·Π°ΠΊΠΎΠ΄ΠΈΡ€ΠΎΠ²Π°Π½ ΠΏΠΎΡ‚ΠΎΠΊ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Ρ‚Π΅ΠΌ ΠΆΠ΅ способом ΠΏΡ€ΠΎΡ‡ΠΈΡ‚Π°Ρ‚ΡŒ Π²ΠΈΠ΄Π΅ΠΎΠΏΠΎΡ‚ΠΎΠΊ
         mediaSource.addEventListener('sourceopen', function() {
            var mediaSource = this;
            sourceBuffer = mediaSource.addSourceBuffer("video/webm; codecs="vp8"");
        });

      function readChunk() {
        var reader = new FileReader();
        reader.onload = function(e) { 
          // ΠΊΠ°ΠΊ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ FileReader Π±ΡƒΠ΄Π΅Ρ‚ Π³ΠΎΡ‚ΠΎΠ², ΠΈ Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ сСбС кусочСк Π²ΠΈΠ΄Π΅ΠΎΠΏΠΎΡ‚ΠΎΠΊΠ°
          // ΠΌΡ‹ "прицСпляСм" ΠΏΠ΅Ρ€Π΅ΠΊΠΎΠ΄ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹ΠΉ Π² Uint8Array (Π±Ρ‹Π» Blob) кусочСк Π² Π±ΡƒΡ„Π΅Ρ€, связанный
          // с ΠΏΡ€ΠΎΠΈΠ³Ρ€Ρ‹Π²Π°Ρ‚Π΅Π»Π΅ΠΌ, ΠΈ ΠΏΡ€ΠΎΠΈΠ³Ρ€Ρ‹Π²Π°Ρ‚Π΅Π»ΡŒ Π½Π°Ρ‡ΠΈΠ½Π°Π΅Ρ‚ Π²ΠΎΡΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚ΡŒ ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½Π½Ρ‹ΠΉ кусочСк Π²ΠΈΠ΄Π΅ΠΎ/Π°ΡƒΠ΄ΠΈΠΎ
          sourceBuffer.appendBuffer(new Uint8Array(e.target.result));

          reader.onload = null;
        }
        reader.readAsArrayBuffer(arrayOfBlobs.shift());
      }
</script>

ΠŸΡ€ΠΎΡ‚ΠΎΡ‚ΠΈΠΏ стримминг-сСрвиса Π³ΠΎΡ‚ΠΎΠ². Основной минус Π² Ρ‚ΠΎΠΌ, Ρ‡Ρ‚ΠΎ видСовоспроизвСдСниС Π±ΡƒΠ΄Π΅Ρ‚ ΠΎΡ‚ΡΡ‚Π°Π²Π°Ρ‚ΡŒ ΠΎΡ‚ ΠΏΠ΅Ρ€Π΅Π΄Π°ΡŽΡ‰Π΅ΠΉ стороны Π½Π° 100 мс, это ΠΌΡ‹ Π·Π°Π΄Π°Π»ΠΈ сами ΠΏΡ€ΠΈ Ρ€Π°Π·Π±ΠΈΠ΅Π½ΠΈΠΈ Π²ΠΈΠ΄Π΅ΠΎΠΏΠΎΡ‚ΠΎΠΊΠ° ΠΏΠ΅Ρ€Π΅Π΄ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‡Π΅ΠΉ Π½Π° сСрвСр. Π‘ΠΎΠ»Π΅Π΅ Ρ‚ΠΎΠ³ΠΎ, ΠΊΠΎΠ³Π΄Π° я провСрял Ρƒ сСбя Π½Π° Π½ΠΎΡƒΡ‚Π±ΡƒΠΊΠ΅, Ρƒ мСня постСпСнно копился Π»Π°Π³ ΠΌΠ΅ΠΆΠ΄Ρƒ ΠΏΠ΅Ρ€Π΅Π΄Π°ΡŽΡ‰Π΅ΠΉ ΠΈ ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°ΡŽΡ‰Π΅ΠΉ стороной, это Π±Ρ‹Π»ΠΎ Ρ…ΠΎΡ€ΠΎΡˆΠΎ Π²ΠΈΠ΄Π½ΠΎ. Π― Π½Π°Ρ‡Π°Π» ΠΈΡΠΊΠ°Ρ‚ΡŒ способы ΠΊΠ°ΠΊ ΠΏΠΎΠ±ΠΎΡ€ΠΎΡ‚ΡŒ Π΄Π°Π½Π½Ρ‹ΠΉ нСдостаток, и… Π½Π°Π±Ρ€Π΅Π» Π½Π° RTCPeerConnection API, ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ позволяСт ΠΏΠ΅Ρ€Π΅Π΄Π°Π²Π°Ρ‚ΡŒ Π²ΠΈΠ΄Π΅ΠΎΠΏΠΎΡ‚ΠΎΠΊ Π±Π΅Π· ΡƒΡ…ΠΈΡ‰Ρ€Π΅Π½ΠΈΠΉ Ρ‚ΠΈΠΏΠ° разбиСния ΠΏΠΎΡ‚ΠΎΠΊΠ° Π½Π° кусочки. ΠΠ°ΠΊΠ°ΠΏΠ»ΠΈΠ²Π°ΡŽΡ‰ΠΈΠΉΡΡ Π»Π°Π³, я Π΄ΡƒΠΌΠ°ΡŽ, ΠΈΠ·-Π·Π° Ρ‚ΠΎΠ³ΠΎ, Ρ‡Ρ‚ΠΎ Π² Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π΅ ΠΏΠ΅Ρ€Π΅Π΄ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‡Π΅ΠΉ происходит ΠΏΠ΅Ρ€Π΅ΠΊΠΎΠ΄ΠΈΡ€ΠΎΠ²ΠΊΠ° ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ кусочка Π² Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ webm. Π― ΡƒΠΆΠ΅ Π½Π΅ стал дальшС ΠΊΠΎΠΏΠ°Ρ‚ΡŒ, Π° Π½Π°Ρ‡Π°Π» ΠΈΠ·ΡƒΡ‡Π°Ρ‚ΡŒ WebRTC, ΠΎ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Π°Ρ… ΠΌΠΎΠΈΡ… изысканий я Π΄ΡƒΠΌΠ°ΡŽ, Π½Π°ΠΏΠΈΡˆΡƒ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΡƒΡŽ ΡΡ‚Π°Ρ‚ΡŒΡŽ, Ссли ΠΏΠΎΡΡ‡ΠΈΡ‚Π°ΡŽ Ρ‚Π°ΠΊΡƒΡŽ интСрСсной сообщСству.

Π˜ΡΡ‚ΠΎΡ‡Π½ΠΈΠΊ: habr.com

Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ