كيف جعلت (تقريبا) عديمة الفائدة جافا سكريبت كاميرا ويب التدفق

في هذه المقالة ، أرغب في مشاركة محاولاتي لدفق الفيديو عبر مجموعات الويب دون استخدام إضافات مستعرضات خارجية مثل Adobe Flash Player. ما جاء من هذا قرأ على.

Adobe Flash - سابقًا Macromedia Flash ، عبارة عن منصة لإنشاء التطبيقات التي تعمل في متصفح الويب. قبل تنفيذ واجهة برمجة تطبيقات Media Stream ، كان هذا هو النظام الأساسي الوحيد لدفق الفيديو والصوت من كاميرا ويب ، وكذلك لإنشاء مؤتمرات ودردشات متعددة في المتصفح. تم بالفعل إغلاق بروتوكول نقل معلومات الوسائط RTMP (بروتوكول المراسلة في الوقت الحقيقي) لفترة طويلة ، مما يعني: إذا كنت ترغب في رفع خدمة البث ، فيرجى استخدام البرنامج من Adobe نفسه - Adobe Media Server (AMS).

بعد مرور بعض الوقت في عام 2012 ، قام Adobe "بالاستسلام والبصق" لمواصفات بروتوكول RTMP ، الذي تضمن أخطاء ، وفي الواقع ، لم يكتمل. بحلول ذلك الوقت ، بدأ المطورون في تنفيذ تطبيقاتهم لهذا البروتوكول ، لذلك ظهر خادم Wowza. في عام 2011 ، رفعت Adobe دعوى قضائية ضد Wowza للاستخدام غير القانوني لبراءات الاختراع المتعلقة بـ RTMP ، بعد 4 سنوات تم حل النزاع من قبل العالم.

ظل نظام Adobe Flash موجودًا منذ أكثر من 20 عامًا ، وخلال هذا الوقت تم اكتشاف العديد من الثغرات الحرجة ، ووعدوا بالتوقف عن الدعم بحلول عام 2020 ، لذلك لا توجد بدائل كثيرة لخدمة البث.

بالنسبة لمشروعي ، قررت على الفور التخلي تمامًا عن استخدام Flash في المتصفح. السبب الرئيسي الذي أشرت إليه أعلاه ، هو أيضًا Flash غير مدعوم على الإطلاق على منصات الجوّال ، ولم أرغب حقًا في نشر Adobe Flash لتطويره على نظام windows (محاكي النبيذ). لذلك بدأت في كتابة عميل في JavaScript. سيكون هذا مجرد نموذج أولي ، كما علمت لاحقًا أن التدفق يمكن أن يتم بشكل أكثر فاعلية استنادًا إلى p2p ، فقط سيكون لدي نظراء خادم نظير ، لكن أكثر في ذلك الوقت الآخر ، لأنه ليس جاهزًا بعد.

للبدء ، نحتاج إلى خادم websockets نفسه. لقد صنعت أبسطها بناءً على باقة لحن الذهاب:

كود الخادم
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) } 


على العميل (جانب البث) ، يجب عليك أولاً الوصول إلى الكاميرا. يتم ذلك من خلال واجهة برمجة تطبيقات MediaStream .

يمكننا الوصول (الدقة) إلى الكاميرا / الميكروفون من خلال واجهة برمجة تطبيقات أجهزة الوسائط . يوفر API هذا طريقة MediaDevices.getUserMedia () ، والتي تعرض نافذة منبثقة. نافذة تطلب من المستخدم الإذن للوصول إلى الكاميرا و / أو الميكروفون. أود أن أشير إلى أنني أجريت جميع التجارب في Google Chrome ، ولكن أعتقد أن كل شيء في Firefox سيعمل بنفس الطريقة تقريبًا.

بعد ذلك ، تقوم getUserMedia () بإرجاع وعد ، يتم فيه تمرير كائن MediaStream - دفق من بيانات الفيديو والصوت. نقوم بتعيين هذا الكائن إلى خاصية عنصر الفيديو في src. كود:

جانب البث
 <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> 


لبث دفق فيديو عبر مآخذ توصيل ، تحتاج إلى ترميزه بطريقة ما في مكان ما ، وتخزينه مؤقتًا ونقله في أجزاء. لا يمكن نقل دفق الفيديو الخام عبر websockets. هذا هو المكان الذي يأتي فيه تطبيق MediaRecorder API . تسمح لك واجهة برمجة التطبيقات هذه بترميز وتقسيم الدفق إلى أجزاء. أقوم بالتشفير لضغط دفق الفيديو ، حتى أتمكن من دفع بايت أقل عبر الشبكة. بعد تقسيمها إلى أجزاء ، يمكن إرسال كل قطعة إلى websocket. كود:

نقوم بتشفير دفق الفيديو ، وفازناه على أجزاء
 <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> 


أضف الآن النقل على websockets. المثير للدهشة ، وهذا يتطلب فقط كائن WebSocket . لديها طريقتين فقط للإغلاق والإغلاق. الأسماء تتحدث عن نفسها. كود ممتد:

ننقل دفق الفيديو إلى الخادم
 <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> 


جانب البث جاهز! الآن دعونا نحاول أن نأخذ دفق الفيديو وإظهاره على العميل. ماذا نحتاج لهذا؟ بادئ ذي بدء ، بالطبع ، اتصال المقبس. نحن شنق "المستمع" على كائن WebSocket ، والاشتراك في الحدث "رسالة". بعد تلقي جزء من البيانات الثنائية ، يقوم خادمنا ببثها إلى المشتركين ، أي العملاء. في الوقت نفسه ، يتم تشغيل وظيفة رد الاتصال المتصلة بـ "المستمع" لحدث "الرسالة" على العميل ، ويتم تمرير الكائن نفسه في وسيطة الوظيفة - جزء دفق الفيديو المشفر بواسطة vp8.

نحن نقبل دفق الفيديو
 <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> 


حاولت لفترة طويلة أن أفهم سبب تعذر إرسال القطع المستلمة على الفور إلى عنصر الفيديو للتشغيل ، ولكن تبين أن ذلك مستحيل ، بالطبع ، يجب أولاً وضع القطعة في مخزن مؤقت خاص مرفق بعنصر الفيديو ، وعندها فقط سيبدأ تشغيل دفق الفيديو. للقيام بذلك ، تحتاج إلى واجهة برمجة تطبيقات MediaSource وواجهة برمجة تطبيقات FileReader .

يعمل MediaSource كنوع من الوسيط بين كائن تشغيل الوسائط ومصدر دفق الوسائط هذا. يحتوي كائن MediaSource على مخزن مؤقت قابل للتوصيل لمصدر دفق الفيديو / الصوت. ميزة واحدة هي أن المخزن المؤقت يمكن أن يحتوي فقط على بيانات Uint8 ، لذلك FileReader مطلوب لإنشاء مثل هذا المخزن المؤقت. ألقِ نظرة على الكود وسوف يصبح أكثر وضوحًا:

لعب دفق الفيديو
 <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> 


النموذج الأولي لخدمة البث جاهز. العيب الرئيسي هو أن تشغيل الفيديو سيكون 100 مللي ثانية وراء جانب الإرسال ، وضعنا ذلك بأنفسنا عند تقسيم دفق الفيديو قبل إرساله إلى الخادم. علاوة على ذلك ، عندما راجعت جهاز الكمبيوتر المحمول الخاص بي ، تراكمت تدريجياً الفجوة بين جانبي الإرسال والاستقبال ، وكان ذلك مرئيًا بوضوح. بدأت أبحث عن طرق للتغلب على هذا القصور ، و ... واجهت واجهة برمجة تطبيقات RTCPeerConnection ، والتي تتيح لك نقل دفق فيديو دون حيل مثل تقسيم الدفق إلى أجزاء. أعتقد أن التأخر المتراكم يرجع إلى حقيقة أنه في المتصفح ، قبل النقل ، يتم تحويل كل قطعة إلى تنسيق ويب. لم أعد أكثر في البحث ، لكنني بدأت في دراسة WebRTC ، أفكر في نتائج بحثي ، سأكتب مقالة منفصلة إذا وجدت أن هذا المجتمع مثيرًا للاهتمام.

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


All Articles