iT邦幫忙

DAY 28
6

且戰且走HTML5系列 第 28

且戰且走HTML5(28) 建立視訊會議

今天先建立起簡單的視訊會議做驗證。
建立視訊會議的方式,基本上就是昨天描述的過程的實作。連線資訊的傳遞,則透過之前包裝好的ws.io模組。

首先看一下伺服器:

 var app = require('http').createServer(handler),
 	io = require('./ws.io').listen(app),
 	fs = require('fs'),
   	url = require('url');
 
 app.listen(8443);
 
 function handler (req, res) {
 	var filename = '';
 	var resource = url.parse(req.url).pathname;
 	switch(resource) {
 		case '/ws.io/ws.io.js':
 			res.setHeader('Content-Type', 'text/javascript');
 			filename = __dirname + resource;
 			break;
 		default:
 			res.setHeader('Content-Type', 'text/html');
 			filename = __dirname + '/test847.html';
 			break
 	}
 	
 	fs.readFile(filename, function (err, data) {
 		if (err) {
 			res.writeHead(500);
 			return res.end('Error loading index.html');
 		}
 		res.writeHead(200);
 		res.end(data);
 	});
 }
 
 io.sockets.on('connection', function (socket) {
   
 	socket.on('offer', function(data) {
 		socket.broadcast.emit('offer', {sdp: data.sdp});
 	});
   
 	socket.on('answer', function(data) {
 		socket.broadcast.emit('answer', {sdp: data.sdp});
 	});
 	
 	socket.on('ice', function(data) {
 		socket.broadcast.emit('ice', {sdp:data.sdp,label:data.label});
 	});
 	
 	socket.on('startice', function(data) {
 		socket.broadcast.emit('startice', {});
 	});
 
 	socket.on('hangup', function() {
 		socket.broadcast.emit('hangup', {});
 	});
 });

簡單地說,其實就是處理在caller產生offer(Session Description)傳送,callee產生answer(Session Description)回傳,傳送ICE,通知啟動ICE,通知停止通訊等事件。

再來看一下瀏覽器端:

 <style>
 video {
 	border: solid 1px #6699cc;
 	border-radius: 10px;
 	padding: 15px 15px 15px 15px;
 }
 </style>
 <script src='http://code.jquery.com/jquery-1.8.2.js'></script>
 <script src='/ws.io/ws.io.js'></script>
 <script>
 var socket = io.connect('ws://60.248.166.82:8443');
 $(document).ready(function() {
 	var localStream,remoteStream;
 	var pc = null;
 	$('#btnStart').click(function() {
 		navigator.webkitGetUserMedia({video:true,audio:true}, function(stream) {
 			localStream = stream;
 			$('#local').attr('src', webkitURL.createObjectURL(stream));
 		}, function(info) {
 			console.log('getUserMedia Error:' + info);
 		});
 		this.disabled = true;
 		$('#btnCall').attr('disabled', false);
 	});
 	$('#btnCall').click(function() {
 		this.disable = true;
 		$('#btnHangup').attr('disabled', false);
 		//pc = new webkitPeerConnection00(null, function(candidate, more) {
 		pc = new webkitPeerConnection00('STUN stun.1.google.com:19302', function(candidate, more) {
 			if(candidate) {
 				console.log(candidate);
 				socket.emit('ice', {target: 'notme',sdp:candidate.toSdp(),label:candidate.label});
 			}
 		});
 		pc.onaddstream = function(e) {
 			remoteStream = e.stream;
 			$('#remote').attr('src', webkitURL.createObjectURL(e.stream));
 			$('#btnHangup').attr('disabled', false);
 		};
 		pc.addStream(localStream);
 		var offer = pc.createOffer(null);
 		pc.setLocalDescription(pc.SDP_OFFER, offer);
 		socket.emit('offer', {target:'notme',sdp:offer.toSdp()});
 	});
 	$('#btnHangup').click(function() {
 		this.disable = true;
 		pc.close();
 		pc = null;
 		socket.emit('hangup',{});
 		$(this).attr('disabled', true);
 		$('#btnCall').attr('disabled', false);
 	});
 	socket.on('ice', function(data) {
 		pc.processIceMessage(new IceCandidate(data.label,data.sdp));
 		console.log('socket on ice: ', getIceStateDesc(pc.iceState));
 	});
 	socket.on('offer', function(data) {
 		pc = new webkitPeerConnection00(null, function(candidate, more) {
 			if(candidate) {
 				console.log(candidate);
 				socket.emit('ice', {target:'notme',sdp:candidate.toSdp(),label:candidate.label});
 			}
 		});
 		pc.onaddstream = function(e) {
 			remoteStream = e.stream;
 			$('#remote').attr('src', webkitURL.createObjectURL(e.stream));
 			$('#btnHangup').attr('disabled', false);
 		};
 		pc.addStream(localStream);
 		pc.setRemoteDescription(pc.SDP_OFFER, new SessionDescription(data.sdp));
 		var answer = pc.createAnswer(data.sdp, {has_video:true,has_audio:true});
 		pc.setLocalDescription(pc.SDP_ANSWER, answer);
 		socket.emit('answer', {target:'notme',sdp:answer.toSdp()});
 	});
 	socket.on('answer', function(data) {
 		pc.setRemoteDescription(pc.SDP_ANSWER, new SessionDescription(data.sdp));
 		socket.emit('startice', {target:'notme'});
 		pc.startIce();
 	});
 	socket.on('startice', function() {
 		pc.startIce();
 	});
 	socket.on('hangup', function() {
 		$('#btnHangup').attr('disabled', true);
 		$('#btnCall').attr('disabled', false);
 		pc.close();
 		pc = null;
 	});
 	function getIceStateDesc(state) {
 		switch(state) {
 			case 0x100:
 				return 'ICE_GATHERING';
 				break;
 			case 0x200:
 				return 'ICE_WAITING';
 				break;
 			case 0x300:
 				return 'ICE_CHECKING';
 				break;
 			case 0x400:
 				return 'ICE_CONNECTED';
 				break;
 			case 0x500:
 				return 'ICE_COMPLETED';
 				break;
 			case 0x600:
 				return 'ICE_FAILED';
 				break;
 			case 0x700:
 				return 'ICE_CLOSED';
 				break;
 			default:
 				return '';
 		}
 	}
 });
 </script>
 
 
 	<div>
 		<legend>Start a video conference</legend>
 		<video id="local" width="320" autoplay></video>
 		<video id="remote" width="320" autoplay></video><br>
 		<button class="btn" id="btnStart">start</button>
 		<button class="btn" id="btnCall" disabled>call</button>
 		<button class="btn" id="btnHangup" disabled>hangup</button>
 	</div>
 
 

過程跟做天描述的一樣。用畫面來看比較清楚:

剛開啟網頁時:

按下start,透過getUserMedia要求取得視訊資源,瀏覽器會詢問使用者是否要接受:

接受以後,就會看到本地的視訊:

遠端也要先start。接下來按下call,開始PeerConnection的流程,雙方建立連線後,對方的視訊就開始播放:

按下hangup按鈕,可以中斷網路連接:

再按下call又可以建立連接,重新收到視訊:

不過目前這樣寫,其實有三個人的時候,就會出問題。要解決的話,就需要每兩人之間都建立一組PeerConnection連線,這個就等到明天再來嘗試,不過不保証成功XD(我在WebRTC maillist上面看到很多問題,說真的,目前的實作只是可用而已)

(2012-11-13 3:52)
會後補充

Chrome23把PeerConnection的介面又大改了一次,雖然這樣改跟W3C的規格又接近了一點,不過之前寫的程式在Chrome23之後就不能動了XD

Chrome23的實作基本上是依照:http://www.w3.org/TR/webrtc/ (日期應該是2012-8-21,後續如果有正式的draft,日期可能會變動,不過Chrome23的實作是依照8/23這個版本的規格)

首先必須注意,在http://www.webrtc.org/faq-recent-topics有提到,目前的實作與W3C還是稍有不同,不依照他的「不同」寫法,會出現...DOM Exception。

把今天的範例調整過,伺服器(test851.js):

var app = require('http').createServer(handler),
	io = require('./ws.io').listen(app),
	fs = require('fs'),
  	url = require('url');

app.listen(8443);

function handler (req, res) {
	var filename = '';
	var resource = url.parse(req.url).pathname;
	switch(resource) {
		case '/ws.io/ws.io.js':
			res.setHeader('Content-Type', 'text/javascript');
			filename = __dirname + resource;
			break;
		case '/js/jquery-1.8.2.js':
			res.setHeader('Content-Type', 'text/javascript');
			filename = __dirname + resource;
			break;
		default:
			res.setHeader('Content-Type', 'text/html');
			filename = __dirname + '/test851.html';
			break
	}
	
	fs.readFile(filename, function (err, data) {
		if (err) {
			res.writeHead(500);
			return res.end('Error loading index.html');
		}
		res.writeHead(200);
		res.end(data);
	});
}

io.sockets.on('connection', function (socket) {
	socket.on('offer', function(data) {
		console.log('offer', data);
		socket.broadcast.emit('offer', {sdp: data.sdp});
	});
  
	socket.on('answer', function(data) {
		console.log('answer', data);
		socket.broadcast.emit('answer', {sdp: data.sdp});
	});
	
	socket.on('ice', function(data) {
		socket.broadcast.emit('ice', {candidate: data.candidate});
	});
	
	socket.on('startice', function(data) {
		socket.broadcast.emit('startice', {});
	});

	socket.on('hangup', function() {
		socket.broadcast.emit('hangup', {});
	});
});

主要的修改還是在client端(test851.html):

<style>
video {
	border: solid 1px #6699cc;
	border-radius: 10px;
	padding: 15px 15px 15px 15px;
}
</style>
<script src='js/jquery-1.8.2.js'></script>
<script src='/ws.io/ws.io.js'></script>
<script>
var socket = io.connect('ws://localhost:8443');
	var localStream,remoteStream;
	var pc = null;
$(document).ready(function() {
	$('#btnStart').click(function() {
		navigator.webkitGetUserMedia({video:true,audio:true}, function(stream) {
			localStream = stream;
			$('#local').attr('src', webkitURL.createObjectURL(stream));
		}, function(info) {
			console.log('getUserMedia Error:' + info);
		});
		this.disabled = true;
		$('#btnCall').attr('disabled', false);
	});
	$('#btnCall').click(function() {
		this.disable = true;
		$('#btnHangup').attr('disabled', false);
		pc = new webkitRTCPeerConnection(null);
		pc.onaddstream = function(e) {
			remoteStream = e.stream;
			$('#remote').attr('src', webkitURL.createObjectURL(e.stream));
			$('#btnHangup').attr('disabled', false);
		};
		pc.onicecandidate = function(e) {
			if(!!e.candidate) {
				socket.emit('ice', {candidate: e.candidate});
			}
		};
		pc.addStream(localStream);
		pc.createOffer(function(sdp) {
			console.log('createOffer', sdp);
			pc.setLocalDescription(sdp, function() {
				console.log('after setLocalDescription');
				socket.emit('offer', {sdp:sdp});
			});
		}, function() {
			console.log('create offer error.');
		},{has_video:true,has_audio:true});
	});
	$('#btnHangup').click(function() {
		this.disable = true;
		pc.close();
		pc = null;
		socket.emit('hangup',{});
		$(this).attr('disabled', true);
		$('#btnCall').attr('disabled', false);
	});
	socket.on('ice', function(data) {
		console.log(data.candidate);
		console.log('socket on ice: ', getIceStateDesc(pc.iceState));
		pc.addIceCandidate(new RTCIceCandidate(data.candidate));
	});
	socket.on('offer', function(data) {
		pc = new webkitRTCPeerConnection(null);
		pc.onaddstream = function(e) {
			remoteStream = e.stream;
			$('#remote').attr('src', webkitURL.createObjectURL(e.stream));
			$('#btnHangup').attr('disabled', false);
		};
		pc.onicecandidate = function(e) {
			if(!!e.candidate) {
				socket.emit('ice', {candidate: e.candidate});
			}
		};
		pc.addStream(localStream);
		var offer = new RTCSessionDescription(data.sdp);
		pc.setRemoteDescription(offer, function() {
			//pc.createAnswer is different from w3c webrtc standard api
			console.log('set remote description');
			pc.createAnswer(function(sdp) {
				console.log('createAnswer', sdp);
				pc.setLocalDescription(sdp, function() {
					console.log('set local description');
					socket.emit('answer', {sdp:sdp});
				}, function(e) {
					console.log('set local description error: '+e);
				});
			}, function(e) {
				console.log('create answer error: '+e);
			}, {has_video:true,has_audio:true});
		}, function(e) {
			console.log('set remote description error: '+e);
		});
	});
	socket.on('answer', function(data) {
		console.log('on answer triggered');
		pc.setRemoteDescription(new RTCSessionDescription(data.sdp));
	});
	socket.on('hangup', function() {
		$('#btnHangup').attr('disabled', true);
		$('#btnCall').attr('disabled', false);
		pc.close();
		pc = null;
	});
	function getIceStateDesc(state) {
		switch(state) {
			case 0x100:
				return 'ICE_GATHERING';
				break;
			case 0x200:
				return 'ICE_WAITING';
				break;
			case 0x300:
				return 'ICE_CHECKING';
				break;
			case 0x400:
				return 'ICE_CONNECTED';
				break;
			case 0x500:
				return 'ICE_COMPLETED';
				break;
			case 0x600:
				return 'ICE_FAILED';
				break;
			case 0x700:
				return 'ICE_CLOSED';
				break;
			default:
				return '';
		}
	}
});
</script>


	<div>
		<legend>Start a video conference</legend>
		<video id="local" width="320" autoplay></video>
		<video id="remote" width="320" autoplay></video><br>
		<button class="btn" id="btnStart">start</button>
		<button class="btn" id="btnCall" disabled>call</button>
		<button class="btn" id="btnHangup" disabled>hangup</button>
	</div>

上一篇
且戰且走HTML5(27) 應用的主軸:WebRTC
下一篇
且戰且走HTML5(29) 逐步完成整合應用
系列文
且戰且走HTML530
0
ted99tw
iT邦高手 1 級 ‧ 2012-11-06 21:22:39

哇,費公粉墨登場耶~~~

喜歡喜歡喜歡

0
chaocraig
iT邦新手 5 級 ‧ 2012-11-20 10:57:02

費公可以把視訊會議的程式Git上去,以饗眾粉絲嗎? ORZ ...

0
fillano
iT邦超人 1 級 ‧ 2012-11-20 11:53:54

https://gist.github.com/0bc90e7df06ea841acf8

這個是只能測試兩個peer的,多了會有問題。不過建議把ws.io換成socket.io,比較不會碰到問題。(瀏覽器要用的ws.io.js檔案,還沒有像socket.io那樣處理,會比較不方便)

另外,這是用Chrome23才支援的API寫的,所以必須使用Chrome23才會動。

要到Chrome24才能支援TURN協定,目前使用預設的STUN協定,當兩個Peer都在NAT後面時穩死。

感謝費公總是仁民愛物、大公無私! 謝謝

「當兩個Peer都在NAT後面時穩死 」是指 firewall 嗎? 還是只要是兩個不同的 IP 分享器的內網,就不能用了呢?

0
純真的人
iT邦高手 1 級 ‧ 2015-05-24 21:16:39

事隔三年看到這篇真是好久台灣都沒有出這類WebRTC的書~

我到處找視訊聊天室需求~

找到大陸公開的版本使用~他是一對多的~目前使用還ok~

供您參考~哈^^"
http://segmentfault.com/a/1190000000439103
https://github.com/LingyuCoder/SkyRTC

可惜..IE跟蘋果手機不支援WebRTC的視訊呢...

fillano iT邦超人 1 級‧ 2015-05-25 09:11:23 檢舉

「支援」還是最大的問題阿...另一個問題是成本,雖然號稱直接連線,不過實際上大部分使用者都是在NAT後面,需要透過一台TURN伺服器來relay,這樣視訊的頻寬成本就不小了...

他這個聊天室的版本是借用Google的TURN伺服器stun:stun.l.google.com:19302

如果要安裝本地TURN伺服器,就要另外下在TURN安裝套件了~

頻寬還在實驗中~如果100MB不夠使用~就多申請幾條線路來分流@@"

雖然是一對多~但目前人數都是要預約才能使用的~不是像外面的一般的聊天室可以隨時進來。

基本上我都會要求使用者必須是Google瀏覽器才可登入,其他瀏覽器不可登入。(用JQ擋登入)

我發現蘋果手機下載這套Bowser WebRTC
http://www.openwebrtc.io/bowser/

他的介紹~我在蘋果手機試他的範例是可以看的到視訊~

但是試大陸版本那套是開啟失敗的~不知道原因差異在哪裡@@"

0
satomi
iT邦新手 5 級 ‧ 2019-01-22 11:03:15

您好,最近在研究webrtc方面的問題,看了您的文章以及使用您的範例,發現無法使用
請問是什麼問題嗎?

fillano iT邦超人 1 級‧ 2019-01-22 11:31:08 檢舉

因為過了七年,有些東西可能不一樣了XD

我要找時間來看看...

1
純真的人
iT邦高手 1 級 ‧ 2019-01-29 15:03:15

createObjectURL 已經失效了(被封鎖)

WebRTC改新的讀取影像方式了...
https://ithelp.ithome.com.tw/articles/10210809

我要留言

立即登入留言