Trong bài viết này, tôi muốn chia sẻ những nỗ lực của mình để truyền phát video qua websockets mà không cần sử dụng plugin trình duyệt của bên thứ ba như Adobe Flash Player. Đọc tiếp để tìm hiểu những gì đã xảy ra.
Adobe Flash, trước đây là Macromedia Flash, là một nền tảng để tạo các ứng dụng chạy trên trình duyệt web. Trước khi giới thiệu API Media Stream, đây thực tế là nền tảng duy nhất để truyền phát video và giọng nói từ webcam cũng như để tạo nhiều loại hội nghị và trò chuyện khác nhau trong trình duyệt. Giao thức truyền thông tin đa phương tiện RTMP (Giao thức nhắn tin thời gian thực) thực sự đã bị đóng từ lâu, điều đó có nghĩa là: nếu bạn muốn phát triển dịch vụ phát trực tuyến của mình, hãy vui lòng sử dụng phần mềm từ chính Adobe - Adobe Media Server (AMS).
Sau một thời gian vào năm 2012, Adobe đã “từ bỏ và công khai” với công chúng.
Nền tảng Adobe Flash đã hơn 20 năm tuổi, trong thời gian đó nhiều lỗ hổng nghiêm trọng đã được phát hiện, hỗ trợ
Đối với dự án của mình, tôi ngay lập tức quyết định từ bỏ hoàn toàn việc sử dụng Flash trong trình duyệt. Tôi đã nêu lý do chính ở trên; Flash cũng không được hỗ trợ trên nền tảng di động và tôi thực sự không muốn triển khai Adobe Flash để phát triển trên Windows (trình giả lập rượu vang). Vì vậy, tôi bắt đầu viết một ứng dụng khách bằng JavaScript. Đây sẽ chỉ là một nguyên mẫu, vì sau này tôi biết rằng việc phát trực tuyến có thể được thực hiện hiệu quả hơn nhiều dựa trên p2p, chỉ đối với tôi nó sẽ là ngang hàng - máy chủ - ngang hàng, nhưng sẽ còn nhiều hơn thế vào lúc khác, vì nó vẫn chưa sẵn sàng.
Để bắt đầu, chúng tôi cần máy chủ websockets thực tế. Tôi đã thực hiện cách đơn giản nhất dựa trên gói giai điệu:
Mã máy chủ
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)
}
Trên máy khách (phía phát trực tuyến), trước tiên bạn cần truy cập vào máy ảnh. Điều này được thực hiện thông qua
Chúng tôi có quyền truy cập (quyền) vào máy ảnh/micrô thông qua
Tiếp theo, getUserMedia() trả về một Promise, qua đó nó chuyển một đối tượng MediaStream - một luồng dữ liệu âm thanh video. Chúng ta gán đối tượng này cho thuộc tính src của thành phần video. Mã số:
Bên phát sóng
<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>
Để phát luồng video qua ổ cắm, bạn cần mã hóa luồng video đó ở đâu đó, lưu vào bộ đệm và truyền theo từng phần. Luồng video thô không thể được truyền qua ổ cắm web. Đây là nơi chúng tôi hỗ trợ
Chúng tôi mã hóa luồng video, chia nó thành nhiều phầ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 = 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>
Bây giờ hãy thêm đường truyền qua websockets. Đáng ngạc nhiên là tất cả những gì bạn cần cho việc này là một đồ vật
Chúng tôi truyền luồng video đến máy chủ
<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>
Phía phát sóng đã sẵn sàng! Bây giờ hãy thử nhận một luồng video và hiển thị nó trên máy khách. Chúng ta cần gì cho việc này? Đầu tiên, tất nhiên là kết nối ổ cắm. Chúng tôi đính kèm một “người nghe” vào đối tượng WebSocket và đăng ký sự kiện 'tin nhắn'. Sau khi nhận được một phần dữ liệu nhị phân, máy chủ của chúng tôi sẽ phát nó tới những người đăng ký, tức là khách hàng. Trong trường hợp này, chức năng gọi lại được liên kết với “người nghe” của sự kiện 'tin nhắn' được kích hoạt trên máy khách; chính đối tượng đó được chuyển vào đối số của hàm - một phần của luồng video được mã hóa bởi vp8.
Chúng tôi chấp nhận luồng 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>
Trong một thời gian dài, tôi đã cố gắng hiểu tại sao không thể gửi ngay đoạn đã nhận đến phần tử video để phát lại, nhưng hóa ra điều này không thể thực hiện được, tất nhiên, trước tiên bạn phải đặt đoạn đó vào một bộ đệm đặc biệt được ràng buộc với phần tử video và chỉ khi đó nó mới bắt đầu phát luồng video. Đối với điều này bạn sẽ cần
MediaSource hoạt động như một loại trung gian giữa đối tượng phát lại phương tiện và nguồn của luồng phương tiện này. Đối tượng MediaSource chứa bộ đệm có thể cắm được cho nguồn của luồng video/âm thanh. Một tính năng là bộ đệm chỉ có thể chứa dữ liệu Uint8, vì vậy bạn sẽ cần FileReader để tạo bộ đệm như vậy. Nhìn vào mã và nó sẽ trở nên rõ ràng hơn:
Phát luồng 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>
Nguyên mẫu của dịch vụ phát trực tuyến đã sẵn sàng. Nhược điểm chính là việc phát lại video sẽ chậm hơn phía truyền 100 ms; chúng tôi tự thiết lập điều này khi tách luồng video trước khi truyền nó đến máy chủ. Hơn nữa, khi tôi kiểm tra trên laptop, độ trễ giữa bên truyền và bên nhận dần dần tích tụ, điều này có thể thấy rõ. Tôi bắt đầu tìm cách khắc phục nhược điểm này và… tình cờ gặp được
Nguồn: www.habr.com