Bagaimana saya membuat (hampir) streaming webcam Javascript yang tidak berguna

Pada artikel ini, saya ingin membagikan upaya saya untuk melakukan streaming video melalui websockets tanpa menggunakan plugin browser pihak ketiga seperti Adobe Flash Player. Apa yang terjadi dari ini baca terus.

Adobe Flash - sebelumnya Macromedia Flash, adalah platform untuk membuat aplikasi yang berjalan di browser web. Sebelum implementasi Media Stream API, itu praktis satu-satunya platform untuk streaming video dan suara dari webcam, serta untuk membuat berbagai konferensi dan obrolan di browser. Protokol untuk mentransmisikan informasi media RTMP (Real Time Messaging Protocol) sebenarnya telah ditutup untuk waktu yang lama, yang berarti: jika Anda ingin meningkatkan layanan streaming Anda, silakan gunakan perangkat lunak dari Adobe sendiri - Adobe Media Server (AMS).

Setelah beberapa waktu di 2012, Adobe "menyerah dan meludahkan" spesifikasi protokol RTMP, yang mengandung kesalahan dan, pada kenyataannya, tidak lengkap. Pada saat itu, pengembang mulai membuat implementasi protokol ini, sehingga server Wowza muncul. Pada tahun 2011, Adobe mengajukan gugatan terhadap Wowza untuk penggunaan paten secara ilegal terkait dengan RTMP, setelah 4 tahun konflik diselesaikan oleh dunia.

Platform Adobe Flash telah ada selama lebih dari 20 tahun, selama waktu ini banyak kerentanan kritis ditemukan, mereka berjanji akan berhenti mendukung pada tahun 2020, jadi tidak ada begitu banyak alternatif untuk layanan streaming.

Untuk proyek saya, saya segera memutuskan untuk sepenuhnya meninggalkan penggunaan Flash di browser. Alasan utama yang saya sebutkan di atas, Flash tidak didukung sama sekali pada platform mobile, dan saya benar-benar tidak ingin menggunakan Adobe Flash untuk pengembangan di windows (wine emulator). Jadi saya mulai menulis klien dalam JavaScript. Ini hanya akan menjadi prototipe, karena saya kemudian mengetahui bahwa streaming dapat dilakukan jauh lebih efisien berdasarkan p2p, hanya saja saya akan memiliki peer-server-peer, tetapi lebih pada waktu lain, karena belum siap.

Untuk memulai, kita membutuhkan server websockets itu sendiri. Saya membuat yang paling sederhana berdasarkan paket melodi go:

Kode 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) } 


Di klien (sisi penyiaran), Anda harus terlebih dahulu mengakses kamera. Ini dilakukan melalui MediaStream API .

Kami mendapatkan akses (resolusi) ke kamera / mikrofon melalui Media Perangkat API . API ini menyediakan metode MediaDevices.getUserMedia () , yang menampilkan munculan. sebuah jendela yang meminta izin kepada pengguna untuk mengakses kamera dan / atau mikrofon. Saya ingin mencatat bahwa saya melakukan semua percobaan di Google Chrome, tetapi, saya pikir, di Firefox semuanya akan bekerja dengan cara yang kira-kira sama.

Selanjutnya, getUserMedia () mengembalikan Janji, di mana objek MediaStream dilewatkan - aliran data video dan audio. Kami menetapkan objek ini ke properti elemen video di src. Kode:

Sisi siaran
 <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> 


Untuk menyiarkan streaming video melalui soket, Anda harus entah bagaimana menyandikannya di suatu tempat, buffer, dan mengirimkannya dalam beberapa bagian. Aliran video mentah tidak dapat dikirim melalui soket web. Di sinilah MediaRecorder API datang untuk menyelamatkan . API ini memungkinkan Anda untuk menyandikan dan memecah aliran menjadi beberapa bagian. Saya melakukan pengkodean untuk mengompresi aliran video, sehingga saya dapat mendorong lebih sedikit byte melalui jaringan. Setelah pecah berkeping-keping, dimungkinkan untuk mengirim setiap bagian ke websocket. Kode:

Kami menyandikan aliran video, mengalahkannya menjadi berkeping-keping
 <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> 


Sekarang tambahkan transfer di soket web. Anehnya, ini hanya membutuhkan objek WebSocket . Hanya ada dua metode kirim dan tutup. Nama-nama berbicara sendiri. Kode yang diperluas:

Kami mentransmisikan aliran video ke 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> 


Sisi penyiaran siap! Sekarang mari kita coba untuk mengambil aliran video dan menunjukkannya pada klien. Apa yang kita butuhkan untuk ini? Pertama-tama, tentu saja, koneksi soket. Kami menggantung "pendengar" pada objek WebSocket, berlangganan acara 'pesan'. Setelah menerima sepotong data biner, server kami menyiarkannya kepada pelanggan, yaitu klien. Pada saat yang sama, fungsi panggilan balik yang terhubung dengan "pendengar" dari peristiwa 'pesan' dipicu pada klien, objek itu sendiri diteruskan ke argumen fungsi - sepotong aliran video yang dikodekan oleh vp8.

Kami menerima aliran 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> 


Untuk waktu yang lama saya mencoba memahami mengapa tidak mungkin untuk segera mengirim potongan-potongan yang diterima ke elemen video untuk diputar, tetapi ternyata tidak mungkin, tentu saja, Anda harus terlebih dahulu meletakkan potongan itu dalam buffer khusus yang melekat pada elemen video, dan baru kemudian ia akan mulai memutar aliran video. Untuk melakukan ini, Anda memerlukan MediaSource API dan FileReader API .

MediaSource bertindak sebagai semacam perantara antara objek pemutaran media dan sumber aliran media ini. Objek MediaSource berisi buffer pluggable untuk sumber aliran video / audio. Salah satu fitur adalah bahwa buffer hanya dapat berisi data Uint8, sehingga FileReader diperlukan untuk membuat buffer seperti itu. Lihatlah kodenya dan itu akan menjadi lebih jelas:

Putar aliran 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> 


Prototipe layanan streaming sudah siap. Kerugian utama adalah bahwa pemutaran video akan berada 100 ms di belakang sisi transmisi, kami mengaturnya sendiri saat memisahkan aliran video sebelum mengirimnya ke server. Selain itu, ketika saya memeriksa laptop saya, saya secara bertahap mengakumulasi jeda antara sisi pengiriman dan penerima, ini jelas terlihat. Saya mulai mencari cara untuk mengatasi kekurangan ini, dan ... menemukan RTCPeerConnection API , yang memungkinkan Anda mentransfer aliran video tanpa trik seperti membagi aliran menjadi beberapa bagian. Akumulasi lag, saya pikir, disebabkan oleh fakta bahwa di browser, sebelum transfer, setiap bagian ditranskode ke format webm. Saya tidak menggali lebih jauh lagi, tetapi mulai mempelajari WebRTC, saya berpikir tentang hasil penelitian saya, saya akan menulis artikel terpisah jika saya menemukan komunitas ini menarik.

Source: https://habr.com/ru/post/id461769/


All Articles