W tym artykule chcę podzielić się moimi próbami przesyłania strumieniowego wideo za pośrednictwem gniazd sieciowych bez korzystania z wtyczek do przeglądarek innych firm, takich jak Adobe Flash Player. Czytaj dalej, aby dowiedzieć się, co z tego wynikło.
Adobe Flash, dawniej Macromedia Flash, to platforma służąca do tworzenia aplikacji uruchamianych w przeglądarce internetowej. Przed wprowadzeniem API Media Stream była to praktycznie jedyna platforma do strumieniowego przesyłania obrazu i głosu z kamery internetowej, a także do tworzenia różnego rodzaju konferencji i czatów w przeglądarce. Protokół przesyłania informacji medialnych RTMP (Real Time Messaging Protocol) był właściwie już dawno zamknięty, co oznaczało, że jeśli chcesz usprawnić swoją usługę przesyłania strumieniowego, bądź na tyle uprzejmy, aby skorzystać z oprogramowania firmy Adobe - Adobe Media Server (AMS).
Po pewnym czasie w 2012 roku Adobe „poddało się i wypluło to” opinii publicznej.
Platforma Adobe Flash ma ponad 20 lat i w tym czasie odkryto wiele krytycznych luk w zabezpieczeniach, wsparcie
W przypadku mojego projektu od razu zdecydowałem się całkowicie zrezygnować z używania Flasha w przeglądarce. Główny powód wskazałem powyżej; Flash również nie jest w ogóle obsługiwany na platformach mobilnych i naprawdę nie chciałem wdrażać Adobe Flash do programowania w systemie Windows (emulator wina). Postanowiłem więc napisać klienta w JavaScript. To będzie tylko prototyp, bo później dowiedziałem się, że streaming można zrobić znacznie wydajniej w oparciu o p2p, tylko u mnie będzie to peer - serwer - peery, ale o tym innym razem, bo to jeszcze nie jest gotowe.
Aby rozpocząć, potrzebujemy rzeczywistego serwera websockets. Zrobiłem najprostszy w oparciu o pakiet Melody Go:
Kod serwera
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)
}
Najpierw musisz uzyskać dostęp do kamery po stronie klienta (po stronie przesyłania strumieniowego). Odbywa się to poprzez
Dostęp (pozwolenie) do kamery/mikrofonu uzyskujemy poprzez
Następnie getUserMedia() zwraca Promise, do którego przekazuje obiekt MediaStream – strumień danych wideo-audio. Przypisujemy ten obiekt do właściwości src elementu wideo. Kod:
Strona nadawcza
<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>
Aby transmitować strumień wideo przez gniazda, należy go gdzieś zakodować, buforować i przesyłać w częściach. Surowego strumienia wideo nie można przesyłać za pośrednictwem gniazd sieciowych. I tu właśnie przychodzi nam z pomocą
Kodujemy strumień wideo, dzielimy go na części
<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>
Dodajmy teraz transmisję poprzez websockety. Co zaskakujące, wszystko, czego potrzebujesz do tego, to przedmiot
Przesyłamy strumień wideo na serwer
<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>
Strona nadawcza jest gotowa! Spróbujmy teraz odebrać strumień wideo i wyświetlić go na kliencie. Czego potrzebujemy do tego? Po pierwsze oczywiście podłączenie do gniazdka. Dołączamy „słuchacza” do obiektu WebSocket i subskrybujemy zdarzenie „message”. Po otrzymaniu fragmentu danych binarnych nasz serwer rozsyła go do abonentów, czyli klientów. W tym przypadku na kliencie uruchamiana jest funkcja wywołania zwrotnego powiązana ze „słuchaczem” zdarzenia „message”, a do argumentu funkcji przekazywany jest sam obiekt – fragment strumienia wideo zakodowany przez vp8.
Akceptujemy strumień wideo
<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>
Długo próbowałem zrozumieć, dlaczego nie można od razu przesłać odebranych fragmentów do elementu wideo w celu odtworzenia, ale okazało się, że nie da się tego zrobić, oczywiście trzeba najpierw umieścić utwór w specjalnym buforze powiązanym z element wideo i dopiero wtedy rozpocznie odtwarzanie strumienia wideo. Do tego będziesz potrzebować
MediaSource działa jako swego rodzaju pośrednik pomiędzy obiektem odtwarzania multimediów a źródłem tego strumienia multimediów. Obiekt MediaSource zawiera podłączany bufor dla źródła strumienia wideo/audio. Jedną z cech jest to, że bufor może przechowywać tylko dane Uint8, więc do utworzenia takiego bufora będziesz potrzebować FileReader. Spójrz na kod, a stanie się jaśniejszy:
Odtwarzanie strumienia wideo
<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>
Prototyp usługi streamingowej jest gotowy. Główną wadą jest to, że odtwarzanie wideo będzie opóźnione w stosunku do strony nadawczej o 100 ms; sami ustawiamy to podczas dzielenia strumienia wideo przed przesłaniem go na serwer. Co więcej, kiedy sprawdziłem na swoim laptopie, opóźnienie między stroną nadawczą i odbiorczą stopniowo narastało, było to wyraźnie widoczne. Zacząłem szukać sposobów na pokonanie tej wady i... natknąłem się
Źródło: www.habr.com