En este artículo quiero compartir mis intentos de transmitir vídeo a través de websockets sin utilizar complementos de navegador de terceros, como Adobe Flash Player. Siga leyendo para descubrir qué resultó de esto.
Adobe Flash, anteriormente Macromedia Flash, es una plataforma para crear aplicaciones que se ejecutan en un navegador web. Antes de la introducción de la API Media Stream, era prácticamente la única plataforma para transmitir vídeo y voz desde una cámara web, así como para crear varios tipos de conferencias y chats en el navegador. El protocolo para transmitir información multimedia RTMP (Protocolo de mensajería en tiempo real) estuvo cerrado durante mucho tiempo, lo que significa que si desea mejorar su servicio de transmisión, tenga la amabilidad de utilizar el software de Adobe: Adobe Media Server (AMS).
Después de algún tiempo en 2012, Adobe “se rindió y lo escupió” al público.
La plataforma Adobe Flash tiene más de 20 años, tiempo durante el cual se han descubierto muchas vulnerabilidades críticas, soporte
Para mi proyecto, inmediatamente decidí abandonar por completo el uso de Flash en el navegador. Indiqué la razón principal anteriormente; Flash tampoco es compatible en absoluto con plataformas móviles y realmente no quería implementar Adobe Flash para el desarrollo en Windows (emulador de Wine). Entonces me propuse escribir un cliente en JavaScript. Esto será solo un prototipo, ya que luego supe que el streaming se puede hacer mucho más eficientemente basado en p2p, solo que para mí será peer - server - peers, pero hablaremos de eso en otro momento, porque aún no está listo.
Para comenzar, necesitamos el servidor websockets real. Hice el más simple basado en el paquete Melody Go:
código de servidor
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)
}
En el cliente (lado de transmisión), primero debe acceder a la cámara. Esto se hace a través de
Obtenemos acceso (permiso) a la cámara/micrófono a través de
A continuación, getUserMedia() devuelve una Promesa, a la que le pasa un objeto MediaStream: un flujo de datos de vídeo y audio. Asignamos este objeto a la propiedad src del elemento de video. Código:
Lado de radiodifusión
<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>
Para transmitir una transmisión de video a través de sockets, debe codificarla en algún lugar, almacenarla en un buffer y transmitirla en partes. La transmisión de video sin procesar no se puede transmitir a través de websockets. Aquí es donde viene en nuestra ayuda.
Codificamos la transmisión de video, la dividimos en pedazos.
<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>
Ahora agreguemos la transmisión a través de websockets. Sorprendentemente, todo lo que necesitas para esto es un objeto.
Transmitimos la transmisión de video al servidor.
<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>
¡La parte de transmisión está lista! Ahora intentemos recibir una transmisión de video y mostrarla en el cliente. ¿Qué necesitamos para esto? En primer lugar, por supuesto, la conexión del enchufe. Adjuntamos un "escucha" al objeto WebSocket y nos suscribimos al evento 'mensaje'. Habiendo recibido un dato binario, nuestro servidor lo transmite a los suscriptores, es decir, a los clientes. En este caso, la función de devolución de llamada asociada con el "escucha" del evento 'mensaje' se activa en el cliente; el objeto en sí se pasa al argumento de la función, una parte de la transmisión de video codificada por vp8.
Aceptamos transmisión de 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>
Durante mucho tiempo traté de entender por qué es imposible enviar inmediatamente las piezas recibidas al elemento de video para su reproducción, pero resultó que esto no se puede hacer, por supuesto, primero debes colocar la pieza en un búfer especial vinculado a el elemento de vídeo, y sólo entonces comenzará a reproducir la secuencia de vídeo. Para esto necesitarás
MediaSource actúa como una especie de intermediario entre el objeto de reproducción multimedia y la fuente de este flujo multimedia. El objeto MediaSource contiene un búfer conectable para la fuente de la transmisión de video/audio. Una característica es que el búfer solo puede contener datos Uint8, por lo que necesitará un FileReader para crear dicho búfer. Mire el código y quedará más claro:
Reproduciendo la transmisión de 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>
El prototipo del servicio de streaming está listo. La principal desventaja es que la reproducción de video se retrasará con respecto al lado de transmisión en 100 ms; esto lo configuramos nosotros mismos al dividir la transmisión de video antes de transmitirla al servidor. Además, cuando revisé en mi computadora portátil, el retraso entre los lados transmisor y receptor se acumuló gradualmente, esto era claramente visible. Empecé a buscar formas de superar esta desventaja y... encontré
Fuente: habr.com