Создание видео трансляции на JS

Доброго времени суток, дорогой хабрачитатель.
Сегодня я расскажу, как можно организовать прямую трансляцию изображения с веб-камеры с помощью HTML5/JS и NodeJS, а также PHP.

В статье наблюдается довольно большое количество кода. Также эта статья ориентирована скорей на начинающих веб разработчиков, чем на профессионалов.

Клиентская часть

Для начала нужно создать элемент типа «video», в который будет копироваться поток из navigator.getUserMedia, а также canvas, из которого будет браться изображение для отправки:

<video autoplay id="vid" style="display:none;"></video>
<canvas id="canvas" width="640" height="480" style="border:1px solid #d3d3d3;"></canvas><br>


Далее нужно направить поток из getUserMedia в video:

var video = document.querySelector("#vid"),
       canvas = document.querySelector('#canvas'),
       ctx = canvas.getContext('2d'),
       localMediaStream = null,
       onCameraFail = function (e) {
            console.log('Camera did not work.', e); // Исключение на случай, если камера не работает
        };
       navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
        window.URL = window.URL || window.webkitURL;
        navigator.getUserMedia({video: true}, function(stream) {
            video.src = window.URL.createObjectURL(stream);
            localMediaStream = stream;
        }, onCameraFail);


Ну что же, на данном этапе в тег «video», который кстати не видно, клонируется видеопоток. Теперь надо сделать так, чтобы изображение постоянно копировалось в canvas. Для этого поставим таймер, вызывающий процедуру копирования, и, соответственно, саму процедуру копирования.

cameraInterval = setInterval(function(){ snapshot();}, 1);
function snapshot(){
       if(localMediaStream){
              ctx.drawImage(video, 0, 0);
        }
}


Хорошо, теперь можно посмотреть на себя, но данные ещё никуда не посылаются. Для отправки данных надо убедиться в синхронности происходящего и в том, что пользователь согласен вести трансляцию, поэтому добавим пару кнопок для включения и отключения потока.

<button onclick="startBroadcasting()">Start Broadcasting</button>
<button onclick="stopBroadcasting()">Stop Broadcasting</button>


И напишем процедуру отправки данных на сервер.
Но каких данных? Для решения этой задачи можно использовать base64 сжатие всего, что находится на canvas'е, и, соответственно, отправлять эту строку на сервер.

var isBroadcasting = false,
	broadcastingTimer;
function sendSnapshot(){
	if(localMediaStream && !isBroadcasting){
		isBroadcasting = true;
                $.post("/",
			{
				p: "new",
				text: ctx.canvas.toDataURL("image/webp", quality); // quality - качество изображения(float)
			},
			function(result){
				console.log(result); // На случай, если что-то пойдёт не так
				isBroadcasting = false;
			}
		);
	}
}
// И добавим обработчики кнопок начала и завершения вещания
function startBroadcasting(){
	broadcastingTimer = setInterval(sendSnapshot, 1);
}
function stopBroadcasting(){
	clearInterval(broadcastingTimer);
}


С клиентской частью транслирования покончено.

Серверная часть(Node JS)

Для такого приложения не требуется каких-либо изощрений или серверных фреймворков. Сервер создаётся при помощи функции http.createServer.
Главное здесь дописать обработчик на случай, если пришёл post запрос.

var qs = require('querystring');
var imageData = "";
var myId = 1;
/* Запуск сервера */
if(req.method == "POST"){
	var fullBody = "";
	req.on('data', function(chunk){
		fullBody += chunk.toString();
	});
	req.on('end', function(){
		res.writeHead(200, {'Content-Type': 'text/html'});
		var POST = qs.parse(fullBody);
		if(POST["p"] == "new"){ // Смена изображения
			imageData = POST["text"];
			myId += 1;
			res.write(imageData);
		}else if(POST["p"] == "ajax"){
			if(myId > parseInt(POST["last"])){
				if(typeof(imageData) != "undefined"){
					res.write(document.body.innerHTML = ('<img src=" + '"' + imageData + '"' + "/>');" + "\n");
					res.write("last_message_id = " + myId + ";");
				}
			}
		}
		res.end();
	});
}else{ /* Здесь идёт отдача всего, что пользователь просит. */}


Аналогично с php:

Код на php
if($_GET['p'] == "ajax"){
	Header("Cache-Control: no-cache, must-revalidate");
	Header("Pragma: no-cache");
	Header("Content-Type: text/javascript; charset=windows-1251");
	$file = file("monitor_id.txt");
	$id = $file[0];
	if($id > $_GET['last']){
		$text_file = file("monitor_command.txt");
		$count = count($text_file);
		$last = $id;
		echo "var main = $('#main'); \n";
		for($i = 0; $i < 1; $i++){
			$s = $text_file[$i];
			while(strpos($s, chr(92)) !== false){
				$s = str_replace(chr(92), "", $s);
			}
			echo $s;
		}
		echo "\n";
    	echo "last_message_id = $id;";
	}
}elseif((isset($_GET['p']) && $_GET['p'] == "new") || (isset($_POST['p']))){
	$file = file("monitor_id.txt");
	$id = $file[0];
	$fh = fopen("monitor_command.txt", "w+");
	$get_text = $_POST['text'];
	$gt = $get_text;
	while(strpos($get_text, "\r\n") !== false){
		$get_text = str_replace("\r\n", "</br>", $get_text);
	}
	fwrite($fh, "document.body.innerHTML = ('<img src=".'"'.".$get_text.'"'."/>);\n");
	fclose($fh);
	$fhn = fopen("monitor_id.txt", "w+");
	fwrite($fhn, $id + 1);
	fclose($fhn);
	echo $get_text;
}



Клиентская часть(просмотр трансляции)

Как ни странно, клиентская часть очень проста, от неё лишь требуется исполнение кода, приходящего от сервера.
var last_message_id = 0, 
	load_in_process = false; 
function Load() {
    if(!load_in_process)
    {
	    load_in_process = true;
    	$.post("/", 
    	{
      	    p: "ajax", 
      	    last: last_message_id,
			version: version
    	},
   	    function (result) {
		    eval(result);
		    load_in_process = false; 
    	});
    }
}
var loadInterval = setInterval(Load, 1);


Заключение

Сегодня мы написали простой сервис для организации односторонних видео трансляций посредством html5/js. Стоит лишь отметить то, что пока данное решение работает не очень прытко вследствие отсутствия нормального сжатия, а так-же из-за того, что все операции по обработке изображения могут производиться только на клиенте, а от этого скорость передачи кадров уменьшается и начинают наблюдаться «подвисания».

Но этот довольно простой пример — лишь доказательство того, что html5 уже не так далёк от flash, и что реализация многих вещей становится возможной.

Спасибо за внимание!
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 10

    +1
    У меня несколько вопросов:
    1) Зачем копировать через setTimeout, когда можно использовать html5demos.com/gum-canvas?
    2) Зачем передавать эти кусочки, когда есть PeerConnection (http://webrtc.googlecode.com/svn/trunk/samples/js/demos/html/pc1.html)?
      0
      PeerConnection это не html5. Видимо по этому.
        0
        Также как и getUserMedia. Но WebRTC более бы подошел для передачи потоков. Хотя, как альтернатива, эта идея очень интересная :)
      0
      Пост весьма неплох, но хотелось бы увидеть демонстрацию работы сего. И ещё, если у вас объект POST, то ты можно просто POST.p вместо POST[«p»]. Легче не правда ли?
        +2
        Зато при необходимости динамически что-то делать, POST[«p»] меняется на POST[var], а POST.p на POST.var не заменишь. Другого объяснения не вижу :)
          0
          Да, полностью с вами согласен. Так писать(POST[str]) имеет смысл, только если в свойстве(str) содержатся такие символы, как "-", "+" и.т.д, ну или, как написал ssneq, свойство динамическое
          0
          А звук?
            +3
            Мне кажется, или сервер от такого количества запросов быстра ляжет, если будет несколько клиентов?
              0
              Да. Нагрузка на клиенте и на сервере приличная из-за необходимости лишний раз обрабатывать данные, а не просто «отправил»->«получил и положил». Использовал такое.
              0
              ужас X_X… При мало мальски серьёзной нащрузке сервер ляжет… Зачем писать что-то в файлы? Почему не написать сервер? Почему у файла захардкожено имя? Если уж это html5 почему не используются web-сокеты?

              Only users with full accounts can post comments. Log in, please.