In questo articolo voglio condividere i miei tentativi di eseguire lo streaming di video tramite websocket senza utilizzare plug-in del browser di terze parti come Adobe Flash Player. Continua a leggere per scoprire cosa ne Γ¨ venuto fuori.
Adobe Flash, in precedenza Macromedia Flash, è una piattaforma per la creazione di applicazioni eseguite in un browser web. Prima dell'introduzione dell'API Media Stream, era praticamente l'unica piattaforma per lo streaming di video e voce da una webcam, nonché per creare vari tipi di conferenze e chat nel browser. Il protocollo per la trasmissione di informazioni multimediali RTMP (Real Time Messaging Protocol) in realtà è stato chiuso per molto tempo, il che significava: se volete potenziare il vostro servizio di streaming, siate così gentili da utilizzare il software della stessa Adobe - Adobe Media Server (AMS).
Dopo un poβ di tempo, nel 2012, Adobe βsi arrese e lo sputΓ² fuoriβ al pubblico.
La piattaforma Adobe Flash ha piΓΉ di 20 anni, periodo durante il quale sono state scoperte molte vulnerabilitΓ critiche, supporto
Per il mio progetto ho deciso subito di abbandonare completamente l'uso di Flash nel browser. Ho indicato il motivo principale sopra; inoltre, Flash non Γ¨ affatto supportato sulle piattaforme mobili e non volevo davvero distribuire Adobe Flash per lo sviluppo su Windows (emulatore di vino). Quindi ho deciso di scrivere un client in JavaScript. Questo sarΓ solo un prototipo, poichΓ© in seguito ho appreso che lo streaming puΓ² essere eseguito in modo molto piΓΉ efficiente basato su p2p, solo che per me sarΓ peer - server - peer, ma ne parleremo un'altra volta, perchΓ© non Γ¨ ancora pronto.
Per iniziare, abbiamo bisogno del server websocket vero e proprio. Ho realizzato quello piΓΉ semplice basato sul pacchetto Melody Go:
Codice del server
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)
}
Sul client (lato streaming), devi prima accedere alla telecamera. Questo viene fatto attraverso
Otteniamo l'accesso (autorizzazione) alla fotocamera/microfono tramite
Successivamente, getUserMedia() restituisce una Promise, alla quale passa un oggetto MediaStream, un flusso di dati audio-video. Assegniamo questo oggetto alla proprietΓ src dell'elemento video. Codice:
Lato radiodiffusione
<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>
Per trasmettere un flusso video tramite socket, Γ¨ necessario codificarlo da qualche parte, bufferarlo e trasmetterlo in parti. Il flusso video non elaborato non puΓ² essere trasmesso tramite websocket. Ecco che ci viene in aiuto
Codifichiamo il flusso video, lo spezziamo
<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>
Ora aggiungiamo la trasmissione tramite websocket. Sorprendentemente, tutto ciΓ² di cui hai bisogno Γ¨ un oggetto
Trasmettiamo il flusso video al server
<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>
Il lato trasmesso è pronto! Ora proviamo a ricevere un flusso video e visualizzarlo sul client. Di cosa abbiamo bisogno per questo? Innanzitutto, ovviamente, la connessione tramite presa. Alleghiamo un "ascoltatore" all'oggetto WebSocket e ci iscriviamo all'evento "messaggio". Dopo aver ricevuto un dato binario, il nostro server lo trasmette agli abbonati, cioè ai clienti. In questo caso, la funzione di callback associata al "ascoltatore" dell'evento "messaggio" viene attivata sul client; l'oggetto stesso viene passato all'argomento della funzione, un pezzo del flusso video codificato da vp8.
Accettiamo streaming 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'),
socket = new WebSocket('ws://127.0.0.1:3000/ws'),
arrayOfBlobs = [];
socket.addEventListener('message', function (event) {
// "ΠΊΠ»Π°Π΄Π΅ΠΌ" ΠΏΠΎΠ»ΡΡΠ΅Π½Π½ΡΠΉ ΠΊΡΡΠΎΡΠ΅ΠΊ Π² ΠΌΠ°ΡΡΠΈΠ²
arrayOfBlobs.push(event.data);
// Π·Π΄Π΅ΡΡ Π±ΡΠ΄Π΅ΠΌ ΡΠΈΡΠ°ΡΡ ΠΊΡΡΠΎΡΠΊΠΈ
readChunk();
});
</script>
Per molto tempo ho cercato di capire perchΓ© Γ¨ impossibile inviare immediatamente i pezzi ricevuti all'elemento video per la riproduzione, ma si Γ¨ scoperto che ciΓ² non Γ¨ possibile, ovviamente Γ¨ necessario prima inserire il pezzo in un buffer speciale destinato a l'elemento video e solo allora inizierΓ la riproduzione del flusso video. Per questo avrai bisogno
MediaSource funge da intermediario tra l'oggetto di riproduzione multimediale e la fonte di questo flusso multimediale. L'oggetto MediaSource contiene un buffer collegabile per l'origine del flusso video/audio. Una caratteristica Γ¨ che il buffer puΓ² contenere solo dati Uint8, quindi avrai bisogno di un FileReader per creare tale buffer. Guarda il codice e diventerΓ piΓΉ chiaro:
Riproduzione del flusso 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'),
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>
Il prototipo del servizio di streaming Γ¨ pronto. Lo svantaggio principale Γ¨ che la riproduzione del video ritarda di 100 ms rispetto al lato di trasmissione; impostiamo noi stessi quando dividiamo il flusso video prima di trasmetterlo al server. Inoltre, quando ho controllato sul mio laptop, il ritardo tra il lato di trasmissione e quello di ricezione si Γ¨ gradualmente accumulato, questo era chiaramente visibile. Ho iniziato a cercare modi per superare questo svantaggio e... mi sono imbattuto
Fonte: habr.com