Last active
June 18, 2020 23:10
-
-
Save dakeshi19/f170733d25b4299009d00c87359858f7 to your computer and use it in GitHub Desktop.
WebRTC
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
https://itdepends.hateblo.jp/entry/2020/05/04/120500 | |
WebRTCの勉強のためのサンプルプログラムです。 | |
自由に改変してかまいません。 | |
利用は自己責任でお願いします。 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* シグナリングサーバー(socket.ioによるWebSocketサーバー) + ランデブー用のページを配信するWebサーバー | |
*/ | |
const port = 443 | |
const http = require('http') | |
const https = require('https') | |
const fs = require('fs') | |
const express = require('express') | |
const app = express() | |
const options = { | |
key: fs.readFileSync('pem/orekey.pem'), | |
cert: fs.readFileSync('pem/orecert.pem') | |
} | |
/* Webサーバー */ | |
const docroot = '/public' | |
app.use(express.static(__dirname + docroot)) | |
app.get('/', function (req, res) { | |
res.sendFile(__dirname + docroot + '/index.html'); | |
}) | |
//const server = http.createServer(app) // node.js自体はhttpで良い場合はこちらを利用 | |
const server = https.createServer(options, app) | |
/* シグナリングサーバー(WebSocketサーバー(socket.ioを利用)) */ | |
const io = require('socket.io')(server) | |
let pubid = null | |
let connections = {} | |
io.on('connect', socket => { | |
console.log('io', '%%%%% connect %%%%%:', socket.id) | |
connections[socket.id] = true | |
// チャットサーバー用の待ち受け-------- | |
socket.on('chat message up', (msg) => { io.emit('chat message2', socket.id + '◆ ' + msg) }) | |
// シグナリングサーバー用の待ち受けロジック -------- | |
// 配信者入室 | |
socket.on('pub_enter', () => { | |
if (pubid !== null) { // 排他制御する | |
console.log(socket.id) | |
socket.emit('shimedashi') | |
return null | |
} | |
pubid = socket.id | |
io.emit('chat message2', pubid + 'さんが放送室に入室しました') // | |
}) | |
// 視聴者入室 | |
socket.on('sub_enter', () => { | |
io.emit('chat message2', socket.id + 'さんが入室しました') // | |
}) | |
// 配信側の準備OKを受信 | |
socket.on('now_on_air', () => { | |
pubid = socket.id | |
io.emit('chat message2', '放送室の' + pubid + 'さんがライブを開始しました') | |
socket.broadcast.emit('now_on_air') | |
}) | |
// 受信側からの配信要求を配信側へ渡す | |
socket.on('request', () => { | |
console.log('request', socket.id, '->', pubid) | |
socket.to(pubid).emit('request', { cid: socket.id }) | |
io.emit('chat message2', 'LOG:' + socket.id + ' から配信元 ' + pubid + 'に接続要求') // | |
}) | |
// 配信側からのオファーをもともと要求をかけてきた特定の受信へ渡す | |
socket.on('offer', ({ offer, cid }) => { | |
console.log('offer', pubid, '->', cid) | |
if (socket.id == pubid) { | |
socket.to(cid).emit('offer', { offer }) | |
} | |
}) | |
// 受信側からのアンサーを配信側へ渡す | |
socket.on('answer', ({ answer }) => { | |
console.log('answer', socket.id) | |
socket.to(pubid).emit('answer', { cid: socket.id, answer }) | |
}) | |
// 接続が切れた場合に通知する | |
socket.on('disconnect', () => { | |
console.log('io', '%%%%% disconnect %%%%%:', socket.id) | |
let msgprfx = '' | |
delete connections[socket.id] | |
if (socket.id == pubid) { //配信者の退室の場合は、全ての受信者にそれを伝達する(videoを停止してもらう) | |
pubid = null | |
socket.broadcast.emit('pub_exit') | |
msgprfx = '放送室の' //誤記ではないです。アドホックな方法... | |
} | |
io.emit('chat message2', msgprfx + socket.id + "さんが退室") // | |
}) | |
}) | |
server.listen(port) // https前提でwelknown port ※ httpsの場合もそうでない場合も実際のportに合わせて変更してください。 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
} | |
.menu { | |
height:50px; | |
width: 100%; | |
} | |
button:disabled { | |
background:red; | |
} | |
.video { | |
background: #ccc; | |
} | |
#chat { | |
background: #fff; | |
} | |
form { | |
background: #999; | |
padding: 3px; | |
width: 100%; | |
} | |
form input { | |
border: 0; | |
padding: 10px; | |
width: 60%; | |
margin-right: .5%; | |
} | |
form button { | |
width: 9%; | |
background:#8ef; | |
border: none; | |
padding: 10px; | |
} | |
#messages { | |
list-style-type: none; | |
margin: 0; | |
padding: 0; | |
} | |
#messages li { | |
padding: 5px 10px; | |
} | |
#messages li:nth-child(odd) { | |
background: #eee; | |
} | |
#messages { | |
margin-bottom: 40px | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8" /> | |
<title>画面共有</title> | |
</head> | |
<body> | |
<ul> | |
<li><a href='pub.html'>配信する</a></li> | |
<li><a href='sub.html'>受信する</a></li> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function init_chat(socket, form, messages, msgInput) { | |
form.submit(function () { | |
socket.emit('chat message up', msgInput.val()) | |
msgInput.val('') | |
return false | |
}) | |
socket.on('chat message2', function (msg) { | |
messages.prepend($('<li>').text(msg)) | |
window.scrollTo(0, document.body.scrollHeight) | |
}) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8" /> | |
<script type="text/javascript" src="socket.io/socket.io.js"></script> | |
<link rel="stylesheet" href="css/chat.css"> | |
<title>放送室</title> | |
</head> | |
<body> | |
<div class="menu"> | |
<button id="startbutton" /> 共有する(メディアを指定) </button> | |
<a href='sub.html' style="position:fixed; right:0;">受信側に移動</a> | |
</div> | |
<div id="chat"> | |
<form action=""> | |
<input id="m" autocomplete="off" /><button>コメント送信</button> | |
</form> | |
<ul id="messages"></ul> | |
</div> | |
<script src="https://code.jquery.com/jquery-3.3.1.js"></script> | |
<script src="js/chat.js"></script> | |
<script> | |
; (async () => { | |
const socket = io() | |
const connections = {} | |
/* チャット機能 ------------- */ | |
init_chat(socket, $('form'), $('#messages'), $('#m')) | |
socket.emit('pub_enter') | |
/* 画面共有(配信)機能 -------------- */ | |
// 緩めの排他制御 | |
socket.on('shimedashi', function () { | |
alert('他の人が入室中なので受信室に移動します') | |
location.href = "sub.html" | |
return null | |
}) | |
/* | |
<このアプリでのシグナリングの考え方> | |
共有ボタン押下 > 【now_on_air】→ > | |
【request】← > 配信手続き~【(offer)】→ > | |
【(answer)】← > setRemoteDescription:コネクション成立 > | |
受信側の「ontrack」〜受信開始 > 以上 | |
*/ | |
const startbutton = document.querySelector('#startbutton') | |
startbutton.addEventListener('click', async evt => { | |
// 画面共有起動 | |
const stream = await navigator.mediaDevices.getDisplayMedia( | |
{ | |
video: { | |
width: 1280, height: 720, | |
frameRate: 1 | |
}, | |
audio: false | |
} | |
) | |
// 受信側(依頼元のIDはcidで規定)からの配信依頼があれば、配信手続きを始める | |
// 受信側が配信先全てを覚える実装モデルなので、それぞれのcidを管理する方式になっている | |
socket.on('request', async ({ cid }) => { | |
const pcConfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] } | |
//const pcConfig = { iceServers: [] } //LAN内の端末間どおしならこれでも動作するらしい | |
const peer = new RTCPeerConnection(pcConfig) | |
connections[cid] = peer | |
stream.getTracks().forEach(track => peer.addTrack(track, stream)) | |
//Vanilla ICE 〜 オファー発行 | |
const offer = await peer.createOffer() | |
await peer.setLocalDescription(offer) | |
peer.onicecandidate = async (evt) => { | |
if (evt.candidate) { //Vanilla ICEスタイルなので追加のcandidateを取得できていても今回は我慢する(何もしない) | |
return false | |
} | |
//集められるものは全て収集した(打ち止めになった)ので、offerを伝達する | |
// また、シグナリングサーバ側でofferをoffer元にのみ伝達できるように「cid」を送り返す | |
socket.emit('offer', { offer: peer.localDescription, cid }) | |
} | |
} | |
) | |
socket.on('answer', async ({ cid, answer }) => { | |
if (cid in connections) { | |
await connections[cid].setRemoteDescription(answer) | |
} | |
}) // P2Pの接続確立の事後手続き(setRemoteDescription呼び出し) | |
// ここまででコールバックの設定がひととおり完了 --- | |
// LIVE開始を伝達する | |
// ※あくまで開始連絡のみとなる。これをキッカケにシグナリング開始 | |
document.getElementById("startbutton").setAttribute("disabled", true) | |
alert("LIVEを開始しました。\n配信者のブラウザには配信内容のキャプチャを表示しない仕様なので注意してください。\n また、配信するウィンドウや画面を変更する場合は、ページをリロードしてください。") | |
socket.emit('now_on_air') | |
}) | |
})() | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8" /> | |
<script type="text/javascript" src="socket.io/socket.io.js"></script> | |
<link rel="stylesheet" href="css/chat.css"> | |
<title>視聴室</title> | |
</head> | |
<body> | |
<div class="menu"> | |
<a href='pub.html' style="position:fixed; right:0;">配信側に移動</a> | |
</div> | |
<div class="video"> | |
<video autoplay playsinline id="video" width="1280" height="720"></video> | |
</div> | |
<div id="chat"> | |
<form action=""> | |
<input id="m" autocomplete="off" /><button>コメント送信</button> | |
</form> | |
<ul id="messages"></ul> | |
</div> | |
<script src="https://code.jquery.com/jquery-3.3.1.js"></script> | |
<script src="js/chat.js"></script> | |
<script> | |
; (async () => { | |
const socket = io() | |
/* チャット機能 ------------- */ | |
init_chat(socket, $('form'), $('#messages'), $('#m')) | |
socket.emit('sub_enter') | |
/* 画面共有(受信)機能 -------------- */ | |
/* ポイント */ | |
/* 受信側は受信専任で、配信側がLIVEを開始するのを待つスタンスの設計である */ | |
const video = document.querySelector('video') | |
let connection = null | |
//配信側がLIVEを始めるのを待つ。開始連絡があれば、接続要求のrequestを投げる | |
socket.on('now_on_air', () => socket.emit('request')) | |
//配信側から'offer'を受け取ったら、answerを送り返すとともに、LIVEの受信のための関数setVideoを起動する | |
socket.on('offer', setVideo) | |
async function setVideo({ offer }) { | |
const pcConfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] } | |
//const pcConfig = { iceServers: [] } //LAN内の端末間どおしならこれでも動作するらしい | |
const peer = new RTCPeerConnection(pcConfig) | |
connection = peer | |
//Vanilla ICE 〜アンサー発行 | |
peer.setRemoteDescription(offer) | |
peer.onicecandidate = evt => { | |
if (evt.candidate) { //Vanilla ICEスタイルなので追加のcandidateを取得できていても今回は我慢する(何もしない) | |
return false | |
} | |
//集められるものは全て収集した(打ち止めになった)ので、answerを返す | |
socket.emit('answer', { answer: peer.localDescription }) | |
} | |
peer.ontrack = evt => { | |
console.log('%%%%% ontrack %%%%%') | |
video.srcObject = evt.streams[0] | |
} | |
peer.setLocalDescription(await peer.createAnswer()) | |
} | |
//配信側が退出したら、ビデオを停止して、受信を終了する | |
socket.on('pub_exit', () => { | |
console.log('***** pub_exit *****') | |
stopVideo() | |
}) | |
function stopVideo() { | |
if (connection) { | |
video.pause() | |
video.srcObject = null | |
connection.close() | |
connection = null | |
} | |
} | |
// ここまででコールバックの設定がひととおり完了 --- | |
//ひとまず起動時にはダメ元でrequestを投げる(配信側がすでに配信を始めていれば、受信手続きにつながっていく) | |
socket.emit('request') | |
})() | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment