こんにちは!がねこまさしです。「WebRTCを使ってみよう」シリーズの最新話をお送りします。今回は、簡易的な放送局を作ってみましょう。
片方向配信の特徴
WebRTCを使った音声通話、ビデオチャットのサンプルには、双方向のものが多く見られます。ライブラリもそれを前提とした作りのモノが多いようです。なので今回は、片方向配信を実際に動かしてみましょう。
片方向配信には、双方向通信とは異なる特徴があります。
- 視聴側はカメラやマイクといった機器が不要なので、参加のハードルが下がる
- Peer-to-Peerでもフルメッシュ構造にはならないので、より多くの人が同時に利用できる
特に同時接続数はは双方向では4~5人が実用範囲なのに対し、片方向では10~30人程度に対して1つのPCから配信できます。ちょっとした仲間内のイベントや、社内イベントであれば、十分にカバーできるのではないでしょうか?(社内で動かせば、社内ネットワーク内で完結するので、セキュリティ部門に怒られることもないでしょうし…)
片方向配信をつなぐまで
今回の記事は、技術的には過去の記事「WebRTCを使って複数人で話してみよう」の内容でカバーされています。違いはPeer-to-Peer接続までの手順にあります。
片方向配信では、話す側(talk)と見る側(watch)が非対称です。どちらから通信を始めるかで、2つのシナリオに分かれます。
(A) 配信中に、新たに見る人(watch)が現れて、視聴を開始する
この場合、Peer-to-Peer確立までのシグナリングの流れは、次のようになります。
- 新しい視聴者(watchA)が、同じ部屋の中にいる全員に「映像ちょうだい(talk_request)」を送る
- 他の視聴者(watchB)は、単に無視する
- 配信側(talk)は新しくOffer SDPを生成し、watchAに対して送信する
- watchAはOffer SDPを受け取ったら、Answer SDPを生成してtalkに送り返す
- その後 ICE Candidate が複数交換され、最終的にPeer-to-Peerで片方向の映像ストリームが流れる
(2)視聴者(watch)が待機しているところに、話す側(talk)が配信を開始する
こちらの場合は、Peer-to-Peer確立までのシグナリングの流れは、次のようになります。
- 配信側(talk)が、同じ部屋の中にいる全員に「始めるよー(talk_ready)」を送る
- 各視聴者(watchA, watchB)は、「映像ちょうだい(talk_request)」を返す
- 配信側(talk)は各視聴者ごとに別々のOffer SDPを生成し、それぞれ送信する
- 各視聴者(watch)はOffer SDPを受け取ったら、Answer SDPを生成してtalkに送り返す
- その後 ICE Candidate が複数交換され、最終的にPeer-to-Peerで片方向の映像ストリームが流れる
先ほどのシーケンスの前に、開始の合図(talk_ready)を加えただけですね。
シグナリングサーバーのソースコード
シグナリングサーバーは、過去の記事「WebRTCを使って複数人で話してみよう」と同じものが使えます。ただしこの時はSocket.IO v0.9を使っていたので、今回はSocket.IO v1.0/v1.1の場合を掲載しておきます。サーバー開始の部分と、部屋名の保持の仕方が少し異なります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
var BROADCAST_ID = '_broadcast_'; // -- create the socket server on the port --- var srv = require('http').Server(); var io = require('socket.io')(srv); var port = 9001; srv.listen(port); console.log('signaling server started on port:' + port); // This callback function is called every time a socket // tries to connect to the server io.on('connection', function(socket) { // ---- multi room ---- socket.on('enter', function(roomname) { socket.join(roomname); console.log('id=' + socket.id + ' enter room=' + roomname); setRoomname(roomname); }); function setRoomname(room) { //// for v0.9 //socket.set('roomname', room); // for v1.0 socket.roomname = room; } function getRoomname() { var room = null; //// for v0.9 //socket.get('roomname', function(err, _room) { // room = _room; //}); // for v1.0 room = socket.roomname; return room; } function emitMessage(type, message) { // ----- multi room ---- var roomname = getRoomname(); if (roomname) { console.log('===== message broadcast to room -->' + roomname); socket.broadcast.to(roomname).emit(type, message); } else { console.log('===== message broadcast all'); socket.broadcast.emit(type, message); } } // When a user send a SDP message // broadcast to all users in the room socket.on('message', function(message) { message.from = socket.id; // get send target var target = message.sendto; if ( (target) && (target != BROADCAST_ID) ) { console.log('===== message emit to -->' + target); socket.to(target).emit('message', message); return; } // broadcast in room emitMessage('message', message); }); // When the user hangs up // broadcast bye signal to all users in the room socket.on('disconnect', function() { console.log('-- user disconnect: ' + socket.id); // --- emit ---- emitMessage('user disconnected', {id: socket.id}); // --- leave room -- var roomname = getRoomname(); if (roomname) { socket.leave(roomname); } }); }); |
クライアント:配信側(talk)のソースコード
以前の複数人でのケースとの違いを見ていきましょう。配信側では、相手側の映像を受け取る必要がないので、複数のvideoを処理する部分はごっそり削れます。また配信側の通信処理は、複数人で話す場合ととても近いです。
Peer-to-Peerの管理
以前と同じく、複数のPeer-to-Peer接続を管理するために便宜上のクラスと、関連する関数を作っておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
// -------------- multi connections -------------------- var MAX_CONNECTION_COUNT = 10; var connections = {}; // Connection hash function Connection() { // Connection Class var self = this; var id = ""; // socket.id of partner var peerconnection = null; // RTCPeerConnection instance //var established = false; // is Already Established //var iceReady = false; } function getConnection(id) { var con = null; con = connections[id]; return con; } function addConnection(id, connection) { connections[id] = connection; } function getConnectionCount() { var count = 0; for (var id in connections) { count++; } console.log('getConnectionCount=' + count); return count; } function isConnectPossible() { if (getConnectionCount() < MAX_CONNECTION_COUNT) return true; else return false; } function getConnectionIndex(id_to_lookup) { var index = 0; for (var id in connections) { if (id == id_to_lookup) { return index; } index++; } // not found return -1; } function deleteConnection(id) { delete connections[id]; } function stopAllConnections() { for (var id in connections) { var conn = connections[id]; conn.peerconnection.close(); conn.peerconnection = null; delete connections[id]; } } function stopConnection(id) { var conn = connections[id]; if(conn) { console.log('stop and delete connection with id=' + id); conn.peerconnection.close(); conn.peerconnection = null; delete connections[id]; } else { console.log('try to stop connection, but not found id=' + id); } } function isPeerStarted() { if (getConnectionCount() > 0) { return true; } else { return false; } } |
※以前は自前の過剰なステータス管理フラグを使っていましたが、無用なのでやめました。
シグナリングサーバーへの接続とイベント処理
シグナリングサーバーに対して、socket.ioクライアントを使って接続しておきます。また、接続時(会議室への入室)、切断時、メッセージ受信時のイベントハンドラを設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
// ---- socket ------ // create socket var socketReady = false; var port = 9001; var socket = io.connect('http://signaling.yourdomain:' + port + '/'); // サーバのURLに変更 // socket: channel connected socket.on('connect', onOpened) .on('message', onMessage) .on('user disconnected', onUserDisconnect); function onOpened(evt) { console.log('socket opened.'); socketReady = true; var roomname = getRoomName(); // 会議室名を取得する socket.emit('enter', roomname); console.log('enter to ' + roomname); } // socket: accept connection request function onMessage(evt) { var id = evt.from; var target = evt.sendto; var conn = getConnection(id); if (evt.type === 'talk_request') { if (! isLocalStreamStarted()) { console.warn('local stream not started. ignore request'); return; } console.log("receive request, start offer."); sendOffer(id); return; } else if (evt.type === 'answer' && isPeerStarted()) { // ** console.log('Received answer, settinng answer SDP'); onAnswer(evt); } else if (evt.type === 'candidate' && isPeerStarted()) { // ** console.log('Received ICE candidate...'); onCandidate(evt); } else if (evt.type === 'bye') { // ** console.log("got bye."); stopConnection(id); } } function onUserDisconnect(evt) { console.log("disconnected"); if (evt) { stopConnection(evt.id); } } function getRoomName() { // たとえば、 URLに ?roomname とする var url = document.location.href; var args = url.split('?'); if (args.length > 1) { var room = args[1]; if (room != "") { return room; } } return "_defaultroom"; } |
応答するメッセージは、talk_request, answer, candidate です。talk_ready, offerは来ないはずなので、処理はしていません。
映像、音声の取得開始
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// ---------------------- video handling ----------------------- // start local video function startVideo() { navigator.webkitGetUserMedia({video: true, audio: true}, function (stream) { // success localStream = stream; localVideo.src = window.webkitURL.createObjectURL(stream); localVideo.play(); localVideo.volume = 0; // auto start tellReady(); }, function (error) { // error console.error('An error occurred:'); console.error(error); return; } ); } |
特に変わったところはありませんが、映像取得後に tellReady()を呼び、その中で「準備できたよ(talk_ready)」と通知しています。
配信要求への応答
視聴側(watch)から、配信要求(talk_request)があった場合、次の処理が呼び出されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
var mediaConstraints = {'mandatory': {'OfferToReceiveAudio':false, 'OfferToReceiveVideo':false }}; function sendOffer(id) { var conn = getConnection(id); if (!conn) { conn = prepareNewConnection(id); } conn.peerconnection.createOffer(function (sessionDescription) { // in case of success conn.peerconnection.setLocalDescription(sessionDescription); sessionDescription.sendto = id; sendSDP(sessionDescription); }, function () { // in case of error console.log("Create Offer failed"); }, mediaConstraints); } // ---------------------- connection handling ----------------------- function prepareNewConnection(id) { var pc_config = {"iceServers":[]}; var peer = null; try { peer = new webkitRTCPeerConnection(pc_config); } catch (e) { console.log("Failed to create PeerConnection, exception: " + e.message); } var conn = new Connection(); conn.id = id; conn.peerconnection = peer; peer.id = id; addConnection(id, conn); // send any ice candidates to the other peer peer.onicecandidate = function (evt) { if (evt.candidate) { console.log(evt.candidate); sendCandidate({type: "candidate", sendto: conn.id, sdpMLineIndex: evt.candidate.sdpMLineIndex, sdpMid: evt.candidate.sdpMid, candidate: evt.candidate.candidate}); } else { console.log("ICE event. phase=" + evt.eventPhase); } }; console.log('Adding local stream...'); peer.addStream(localStream); return conn; } |
複数人の双方向のケースと違い、映像や音声を受け取る必要がないので、mediaConstraintsの内容がどちらも受信不要(false)にしています。実際に通信を行うオブジェクトを用意するのは、prepareNewConnection()の中で行っています。
- RTCPeerConnectionを生成
- ICE candidate生成時のイベントハンドラを設定
配信開始時の通知
反対に、配信側(talk)から新たに配信開始を通知する処理はこちらです。単にsocket越しに部屋内にメッセージを投げるだけですね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function tellReady() { if (! isLocalStreamStarted()) { alert("Local stream not running yet. Please [Start Video] or [Start Screen]."); return; } if (! socketReady) { alert("Socket is not connected to server. Please reload and try again."); return; } // call others, in same room console.log("tell ready to others in same room, befeore offer"); socket.json.send({type: "talk_ready"}); } |
SDP、ICEのやり取り
SDPやICE Candidateも、socket越しに相手(1人)に送るだけです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function sendSDP(sdp) { var text = JSON.stringify(sdp); console.log("---sending sdp text ---"); console.log(text); // send via socket socket.json.send(sdp); } function sendCandidate(candidate) { var text = JSON.stringify(candidate); console.log("---sending candidate text ---"); console.log(text); // send via socket socket.json.send(candidate); } |
Answerや、Candidateを受け取った場合は、PeerConnectionに覚えさせます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function setAnswer(evt) { var id = evt.from; var conn = getConnection(id); if (! conn) { console.error('peerConnection not exist!'); return } conn.peerconnection.setRemoteDescription(new RTCSessionDescription(evt)); } function onCandidate(evt) { var id = evt.from; var conn = getConnection(id); if (! conn) { console.error('peerConnection not exist!'); return; } var candidate = new RTCIceCandidate({sdpMLineIndex:evt.sdpMLineIndex, sdpMid:evt.sdpMid, candidate:evt.candidate}); console.log("Received Candidate...") console.log(candidate); conn.peerconnection.addIceCandidate(candidate); } |
クライアント:視聴側(watch)のソースコード
視聴側は自分のユーザーメディアは取得しません。映像も1つだけ受け取るので処理はシンプルです。
Peer-to-Peerの管理
Peer-to-Peerは1つだけなので、本来便宜上のクラス、関数は不要です。とは言えtalk側と共通にするために(あるいは複数人からの変更を減らすため)、同じコードにしておきました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
// -------------- multi connections -------------------- var MAX_CONNECTION_COUNT = 1; var connections = {}; // Connection hash function Connection() { // Connection Class var self = this; var id = ""; // socket.id of partner var peerconnection = null; // RTCPeerConnection instance } function getConnection(id) { var con = null; con = connections[id]; return con; } function addConnection(id, connection) { connections[id] = connection; } function getConnectionCount() { var count = 0; for (var id in connections) { count++; } console.log('getConnectionCount=' + count); return count; } function isConnectPossible() { if (getConnectionCount() < MAX_CONNECTION_COUNT) return true; else return false; } function getConnectionIndex(id_to_lookup) { var index = 0; for (var id in connections) { if (id == id_to_lookup) { return index; } index++; } // not found return -1; } function deleteConnection(id) { delete connections[id]; } function stopAllConnections() { for (var id in connections) { var conn = connections[id]; conn.peerconnection.close(); conn.peerconnection = null; delete connections[id]; } } function stopConnection(id) { var conn = connections[id]; if(conn) { console.log('stop and delete connection with id=' + id); conn.peerconnection.close(); conn.peerconnection = null; delete connections[id]; } else { console.log('try to stop connection, but not found id=' + id); } } function isPeerStarted() { if (getConnectionCount() > 0) { return true; } else { return false; } } |
※同じと言いましたが、1点違いがありました。 MAX_CONNECTION_COUNT = 1 にしています。
シグナリングサーバーへの接続とイベント処理
シグナリングサーバーへの接続はtalkと同じです。処理するメッセージの種類が異なります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
// ---- socket ------ // create socket var socketReady = false; var port = 9001; var socket = io.connect('http://signaling.yourdomain:' + port + '/'); // socket: channel connected socket.on('connect', onOpened) .on('message', onMessage) .on('user disconnected', onUserDisconnect); function onOpened(evt) { console.log('socket opened.'); socketReady = true; var roomname = getRoomName(); // 会議室名を取得する socket.emit('enter', roomname); console.log('enter to ' + roomname); } // socket: accept connection request function onMessage(evt) { var id = evt.from; var target = evt.sendto; var conn = getConnection(id); if (evt.type === 'talk_ready') { if (conn) { return; // already connected } if (isConnectPossible()) { socket.json.send({type: "talk_request", sendto: id }); } else { console.warn('max connections. so ignore call'); } return; } else if (evt.type === 'offer') { console.log("Received offer, set offer, sending answer....") onOffer(evt); } else if (evt.type === 'candidate' && isPeerStarted()) { // ** console.log('Received ICE candidate...'); onCandidate(evt); } else if (evt.type === 'end_talk') { // ** console.log("got talker bye."); detachVideo(id); // force detach video stopConnection(id); } } function onUserDisconnect(evt) { console.log("disconnected"); if (evt) { detachVideo(evt.id); // force detach video stopConnection(evt.id); } } function getRoomName() { // たとえば、 URLに ?roomname とする var url = document.location.href; var args = url.split('?'); if (args.length > 1) { var room = args[1]; if (room != "") { return room; } } return "_defaultroom"; } |
応答するメッセージは talk_ready, offer, candidate です。talk_requestやanswerには反応しません。
配信の要求
配信の依頼は、socket.ioで会議室内全員に(相手を特定せずに)投げます。
1 2 3 4 5 6 7 8 9 10 11 |
function sendRequest() { if (! socketReady) { alert("Socket is not connected to server. Please reload and try again."); return; } // call others, in same room console.log("send request in same room, ask for offer"); socket.json.send({type: "talk_request"}); } |
SDP受信時の処理、ストリーム受信時の処理
talkからOffer SDPを受け取ったら、Peer-to-Peer通信の準備をします。PeerConnectionを生成し、Offerを覚えて、Answerを返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
var mediaConstraints = {'mandatory': {'OfferToReceiveAudio':true, 'OfferToReceiveVideo':true }}; function setOffer(evt) { var id = evt.from; var conn = getConnection(id); if (! conn) { conn = prepareNewConnection(id); conn.peerconnection.setRemoteDescription(new RTCSessionDescription(evt)); } else { console.error('peerConnection alreay exist!'); } } function sendAnswer(evt) { console.log('sending Answer. Creating remote session description...' ); var id = evt.from; var conn = getConnection(id); if (! conn) { console.error('peerConnection not exist!'); return; } conn.peerconnection.createAnswer(function (sessionDescription) { // in case of success conn.peerconnection.setLocalDescription(sessionDescription); sessionDescription.sendto = id; sendSDP(sessionDescription); }, function () { // in case of error console.log("Create Answer failed"); }, mediaConstraints); } |
PeerConnectionを準備しているのは、prepareNewConnection()の中です。talkとほとんど同じですが、一部異なる部分があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
// ---------------------- connection handling ----------------------- function prepareNewConnection(id) { var pc_config = {"iceServers":[]}; var peer = null; try { peer = new webkitRTCPeerConnection(pc_config); } catch (e) { console.log("Failed to create PeerConnection, exception: " + e.message); } var conn = new Connection(); conn.id = id; conn.peerconnection = peer; peer.id = id; addConnection(id, conn); // send any ice candidates to the other peer peer.onicecandidate = function (evt) { if (evt.candidate) { console.log(evt.candidate); sendCandidate({type: "candidate", sendto: conn.id, sdpMLineIndex: evt.candidate.sdpMLineIndex, sdpMid: evt.candidate.sdpMid, candidate: evt.candidate.candidate}); } else { console.log("on ice event. phase=" + evt.eventPhase); } }; //console.log('Adding local stream...'); //peer.addStream(localStream); // 自分の映像ストリームは無し // 相手のストリームのハンドラを追加 peer.addEventListener("addstream", onRemoteStreamAdded, false); peer.addEventListener("removestream", onRemoteStreamRemoved, false) // when remote adds a stream, hand it on to the local video element function onRemoteStreamAdded(event) { console.log("Added remote stream"); remoteVideo.src = window.webkitURL.createObjectURL(event.stream); } // when remote removes a stream, remove it from the local video element function onRemoteStreamRemoved(event) { console.log("Remove remote stream"); detachVideo(this.id); } return conn; } |
自分のストリーム(localStream)がない代わりに、相手のストリーム(RemoteStream)のハンドラを用意しています。
配信開始通知を受けた場合
talk側から talk_ready を受け取った場合、まだ接続が確立していなければ、talk_requestを返します。その後処理は配信の要求と同様に進みます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function onMessage(evt) { var id = evt.from; var target = evt.sendto; var conn = getConnection(id); if (evt.type === 'talk_ready') { if (conn) { return; // already connected } if (isConnectPossible()) { socket.json.send({type: "talk_request", sendto: id }); } else { console.warn('max connections. so ignore call'); } return; } |
動かしてみよう
配信前
配信開始後
このように、複数のブラウザに配信を行うことができます。ぜひ社内などで試してみてください。シグナリングサーバーを流用してテキストチャットなどを付けると、より楽しく利用できますよ!
※今回のソースには含まれていませんが、TURNサーバーを用意すれば、Firewall/NATの内側から外側に配信することも可能です。
クライアント側のソースコード(全体)
配信側と視聴側の主な違い
配信側(talk)と視聴側(watch)の仕組みは似通っていますが、違う部分もあります。改めて違う部分を整理しておきます。
(1) 配信側だけがユーザーメディアを取得する。PeerConnectionを生成したときに、そのストリームを追加する。
- peerconnection.addStream(localStream)
(2) 受信側だけが、相手のメディアストリームの接続、除去イベントを処理する。
- peer.addEventListener(“addstream”, onRemoteStreamAdded, false);
- peer.addEventListener(“removestream”, onRemoteStreamRemoved, false);
(3) 配信側と受信側は異なるメッセージに応答する。
- 配信側だけ:talk_request, answer
- 受信側だけ:talk_ready, offer
配信側(talk.html)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 |
<!DOCTYPE html> <html> <head> <title>Broadcast Talk</title> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> </head> <body> <button type="button" onclick="startVideo();">Start video</button> <button type="button" onclick="stopVideo();">Stop video</button> <button type="button" onclick="tellReady();">On Air</button> <br /> <div style="position: relative;"> <video id="local-video" autoplay style="width: 240px; height: 180px; border: 1px solid black;"></video> </div> <!---- socket ※自分のシグナリングサーバーに合わせて変更してください------> <script src="http://signaling.yourdomain:9001/socket.io/socket.io.js"></script> <script> var localVideo = document.getElementById('local-video'); var localStream = null; var mediaConstraints = {'mandatory': {'OfferToReceiveAudio':false, 'OfferToReceiveVideo':false }}; function isLocalStreamStarted() { if (localStream) { return true; } else { return false; } } // -------------- multi connections -------------------- var MAX_CONNECTION_COUNT = 10; var connections = {}; // Connection hash function Connection() { // Connection Class var self = this; var id = ""; // socket.id of partner var peerconnection = null; // RTCPeerConnection instance } function getConnection(id) { var con = null; con = connections[id]; return con; } function addConnection(id, connection) { connections[id] = connection; } function getConnectionCount() { var count = 0; for (var id in connections) { count++; } console.log('getConnectionCount=' + count); return count; } function isConnectPossible() { if (getConnectionCount() < MAX_CONNECTION_COUNT) return true; else return false; } function getConnectionIndex(id_to_lookup) { var index = 0; for (var id in connections) { if (id == id_to_lookup) { return index; } index++; } // not found return -1; } function deleteConnection(id) { delete connections[id]; } function stopAllConnections() { for (var id in connections) { var conn = connections[id]; conn.peerconnection.close(); conn.peerconnection = null; delete connections[id]; } } function stopConnection(id) { var conn = connections[id]; if(conn) { console.log('stop and delete connection with id=' + id); conn.peerconnection.close(); conn.peerconnection = null; delete connections[id]; } else { console.log('try to stop connection, but not found id=' + id); } } function isPeerStarted() { if (getConnectionCount() > 0) { return true; } else { return false; } } // ---- socket ------ // create socket var socketReady = false; var port = 9001; var socket = io.connect('http://signaling.yourdomain:' + port + '/'); // ※自分のシグナリングサーバーに合わせて変更してください // socket: channel connected socket.on('connect', onOpened) .on('message', onMessage) .on('user disconnected', onUserDisconnect); function onOpened(evt) { console.log('socket opened.'); socketReady = true; var roomname = getRoomName(); // 会議室名を取得する socket.emit('enter', roomname); console.log('enter to ' + roomname); } // socket: accept connection request function onMessage(evt) { var id = evt.from; var target = evt.sendto; var conn = getConnection(id); if (evt.type === 'talk_request') { if (! isLocalStreamStarted()) { console.warn('local stream not started. ignore request'); return; } console.log("receive request, start offer."); sendOffer(id); return; } else if (evt.type === 'answer' && isPeerStarted()) { console.log('Received answer, settinng answer SDP'); onAnswer(evt); } else if (evt.type === 'candidate' && isPeerStarted()) { console.log('Received ICE candidate...'); onCandidate(evt); } else if (evt.type === 'bye') { console.log("got bye."); stopConnection(id); } } function onUserDisconnect(evt) { console.log("disconnected"); if (evt) { stopConnection(evt.id); } } function getRoomName() { // たとえば、 URLに ?roomname とする var url = document.location.href; var args = url.split('?'); if (args.length > 1) { var room = args[1]; if (room != "") { return room; } } return "_defaultroom"; } function onAnswer(evt) { console.log("Received Answer...") console.log(evt); setAnswer(evt); } function onCandidate(evt) { var id = evt.from; var conn = getConnection(id); if (! conn) { console.error('peerConnection not exist!'); return; } var candidate = new RTCIceCandidate({sdpMLineIndex:evt.sdpMLineIndex, sdpMid:evt.sdpMid, candidate:evt.candidate}); console.log("Received Candidate...") console.log(candidate); conn.peerconnection.addIceCandidate(candidate); } function sendSDP(sdp) { var text = JSON.stringify(sdp); console.log("---sending sdp text ---"); console.log(text); // send via socket socket.json.send(sdp); } function sendCandidate(candidate) { var text = JSON.stringify(candidate); console.log("---sending candidate text ---"); console.log(text); // send via socket socket.json.send(candidate); } // ---------------------- video handling ----------------------- // start local video function startVideo() { navigator.webkitGetUserMedia({video: true, audio: true}, function (stream) { // success localStream = stream; localVideo.src = window.webkitURL.createObjectURL(stream); localVideo.play(); localVideo.volume = 0; // auto start tellReady(); }, function (error) { // error console.error('An error occurred:'); console.error(error); return; } ); } // stop local video function stopVideo() { hangUp(); localVideo.src = ""; localStream.stop(); localStream = null; } // ---------------------- connection handling ----------------------- function prepareNewConnection(id) { var pc_config = {"iceServers":[]}; var peer = null; try { peer = new webkitRTCPeerConnection(pc_config); } catch (e) { console.log("Failed to create PeerConnection, exception: " + e.message); } var conn = new Connection(); conn.id = id; conn.peerconnection = peer; peer.id = id; addConnection(id, conn); // send any ice candidates to the other peer peer.onicecandidate = function (evt) { if (evt.candidate) { console.log(evt.candidate); sendCandidate({type: "candidate", sendto: conn.id, sdpMLineIndex: evt.candidate.sdpMLineIndex, sdpMid: evt.candidate.sdpMid, candidate: evt.candidate.candidate}); } else { console.log("ICE event. phase=" + evt.eventPhase); //conn.established = true; } }; console.log('Adding local stream...'); peer.addStream(localStream); return conn; } function sendOffer(id) { var conn = getConnection(id); if (!conn) { conn = prepareNewConnection(id); } conn.peerconnection.createOffer(function (sessionDescription) { // in case of success conn.peerconnection.setLocalDescription(sessionDescription); sessionDescription.sendto = id; sendSDP(sessionDescription); }, function () { // in case of error console.log("Create Offer failed"); }, mediaConstraints); } function setAnswer(evt) { var id = evt.from; var conn = getConnection(id); if (! conn) { console.error('peerConnection not exist!'); return } conn.peerconnection.setRemoteDescription(new RTCSessionDescription(evt)); } // -------- handling user UI event ----- function tellReady() { if (! isLocalStreamStarted()) { alert("Local stream not running yet. Please [Start Video] or [Start Screen]."); return; } if (! socketReady) { alert("Socket is not connected to server. Please reload and try again."); return; } // call others, in same room console.log("tell ready to others in same room, befeore offer"); socket.json.send({type: "talk_ready"}); } // stop the connection upon user request function hangUp() { console.log("Hang up."); socket.json.send({type: "end_talk"}); stopAllConnections(); } </script> </body> </html> |
視聴側(watch.html)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 |
<!DOCTYPE html> <html> <head> <title>broadcast watch</title> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> </head> <body> <button type="button" onclick="sendRequest();">Request</button> <button type="button" onclick="hangUp();">Hang Up</button> <br /> <div style="position: relative;"> <video id="remote-video" autoplay style="width: 320px; height: 240px; border: 1px solid black;"></video> </div> <!---- socket ※自分のシグナリングサーバーに合わせて変更してください ------> <script src="http://signaling.yourdomain:9001/socket.io/socket.io.js"></script> <script> //var localVideo = document.getElementById('local-video'); var remoteVideo = document.getElementById('remote-video'); //var localStream = null; var mediaConstraints = {'mandatory': {'OfferToReceiveAudio':true, 'OfferToReceiveVideo':true }}; function detachVideo(id) { if (id) { var conn = getConnection(id); if (conn) { remoteVideo.pause(); remoteVideo.src = ""; } } else { // force detach remoteVideo.pause(); remoteVideo.src = ""; } } function resizeRemoteVideo() { console.log('--resize--'); var top_margin = 40; var left_margin = 20; var video_margin = 10; var new_width = window.innerWidth - left_margin - video_margin; var new_height = window.innerHeight - top_margin - video_margin; remoteVideo.style.width = new_width + 'px'; remoteVideo.style.height = new_height + 'px'; remoteVideo.style.top = top_margin + 'px'; remoteVideo.style.left = left_margin + 'px'; } document.body.onresize = resizeRemoteVideo; resizeRemoteVideo(); // -------------- multi connections -------------------- var MAX_CONNECTION_COUNT = 1; var connections = {}; // Connection hash function Connection() { // Connection Class var self = this; var id = ""; // socket.id of partner var peerconnection = null; // RTCPeerConnection instance } function getConnection(id) { var con = null; con = connections[id]; return con; } function addConnection(id, connection) { connections[id] = connection; } function getConnectionCount() { var count = 0; for (var id in connections) { count++; } console.log('getConnectionCount=' + count); return count; } function isConnectPossible() { if (getConnectionCount() < MAX_CONNECTION_COUNT) return true; else return false; } function getConnectionIndex(id_to_lookup) { var index = 0; for (var id in connections) { if (id == id_to_lookup) { return index; } index++; } // not found return -1; } function deleteConnection(id) { delete connections[id]; } function stopAllConnections() { for (var id in connections) { var conn = connections[id]; conn.peerconnection.close(); conn.peerconnection = null; delete connections[id]; } } function stopConnection(id) { var conn = connections[id]; if(conn) { console.log('stop and delete connection with id=' + id); conn.peerconnection.close(); conn.peerconnection = null; delete connections[id]; } else { console.log('try to stop connection, but not found id=' + id); } } function isPeerStarted() { if (getConnectionCount() > 0) { return true; } else { return false; } } // ---- socket ------ // create socket var socketReady = false; var port = 9001; var socket = io.connect('http://signaling.yourdomain:' + port + '/'); // ※自分のシグナリングサーバーに合わせて変更してください // socket: channel connected socket.on('connect', onOpened) .on('message', onMessage) .on('user disconnected', onUserDisconnect); function onOpened(evt) { console.log('socket opened.'); socketReady = true; var roomname = getRoomName(); // 会議室名を取得する socket.emit('enter', roomname); console.log('enter to ' + roomname); } // socket: accept connection request function onMessage(evt) { var id = evt.from; var target = evt.sendto; var conn = getConnection(id); console.log('onMessage() evt.type='+ evt.type); if (evt.type === 'talk_ready') { if (conn) { return; // already connected } if (isConnectPossible()) { socket.json.send({type: "talk_request", sendto: id }); } else { console.warn('max connections. so ignore call'); } return; } else if (evt.type === 'offer') { console.log("Received offer, set offer, sending answer....") onOffer(evt); } else if (evt.type === 'candidate' && isPeerStarted()) { console.log('Received ICE candidate...'); onCandidate(evt); } else if (evt.type === 'end_talk') { console.log("got talker bye."); detachVideo(id); // force detach video stopConnection(id); } } function onUserDisconnect(evt) { console.log("disconnected"); if (evt) { detachVideo(evt.id); // force detach video stopConnection(evt.id); } } function getRoomName() { // たとえば、 URLに ?roomname とする var url = document.location.href; var args = url.split('?'); if (args.length > 1) { var room = args[1]; if (room != "") { return room; } } return "_defaultroom"; } function onOffer(evt) { console.log("Received offer...") console.log(evt); setOffer(evt); sendAnswer(evt); } function onCandidate(evt) { var id = evt.from; var conn = getConnection(id); if (! conn) { console.error('peerConnection not exist!'); return; } var candidate = new RTCIceCandidate({sdpMLineIndex:evt.sdpMLineIndex, sdpMid:evt.sdpMid, candidate:evt.candidate}); console.log("Received Candidate...") console.log(candidate); conn.peerconnection.addIceCandidate(candidate); } function sendSDP(sdp) { var text = JSON.stringify(sdp); console.log("---sending sdp text ---"); console.log(text); // send via socket socket.json.send(sdp); } function sendCandidate(candidate) { var text = JSON.stringify(candidate); console.log("---sending candidate text ---"); console.log(text); // send via socket socket.json.send(candidate); } // ---------------------- connection handling ----------------------- function prepareNewConnection(id) { var pc_config = {"iceServers":[]}; var peer = null; try { peer = new webkitRTCPeerConnection(pc_config); } catch (e) { console.log("Failed to create PeerConnection, exception: " + e.message); } var conn = new Connection(); conn.id = id; conn.peerconnection = peer; peer.id = id; addConnection(id, conn); // send any ice candidates to the other peer peer.onicecandidate = function (evt) { if (evt.candidate) { console.log(evt.candidate); sendCandidate({type: "candidate", sendto: conn.id, sdpMLineIndex: evt.candidate.sdpMLineIndex, sdpMid: evt.candidate.sdpMid, candidate: evt.candidate.candidate}); } else { console.log("on ice event. phase=" + evt.eventPhase); } }; //console.log('Adding local stream...'); //peer.addStream(localStream); peer.addEventListener("addstream", onRemoteStreamAdded, false); peer.addEventListener("removestream", onRemoteStreamRemoved, false) // when remote adds a stream, hand it on to the local video element function onRemoteStreamAdded(event) { console.log("Added remote stream"); //attachVideo(this.id, event.stream); remoteVideo.src = window.webkitURL.createObjectURL(event.stream); } // when remote removes a stream, remove it from the local video element function onRemoteStreamRemoved(event) { console.log("Remove remote stream"); detachVideo(this.id); } return conn; } function setOffer(evt) { var id = evt.from; var conn = getConnection(id); if (! conn) { conn = prepareNewConnection(id); conn.peerconnection.setRemoteDescription(new RTCSessionDescription(evt)); } else { console.error('peerConnection alreay exist!'); } } function sendAnswer(evt) { console.log('sending Answer. Creating remote session description...' ); var id = evt.from; var conn = getConnection(id); if (! conn) { console.error('peerConnection not exist!'); return; } conn.peerconnection.createAnswer(function (sessionDescription) { // in case of success conn.peerconnection.setLocalDescription(sessionDescription); sessionDescription.sendto = id; sendSDP(sessionDescription); }, function () { // in case of error console.log("Create Answer failed"); }, mediaConstraints); } function sendRequest() { if (! socketReady) { alert("Socket is not connected to server. Please reload and try again."); return; } // call others, in same room console.log("send request in same room, ask for offer"); socket.json.send({type: "talk_request"}); } // stop the connection upon user request function hangUp() { console.log("Hang up."); socket.json.send({type: "bye"}); detachVideo(null); stopAllConnections(); } </script> </body> </html> |