Skip to content

Instantly share code, notes, and snippets.

@dakeshi19
Last active June 18, 2020 23:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dakeshi19/f170733d25b4299009d00c87359858f7 to your computer and use it in GitHub Desktop.
Save dakeshi19/f170733d25b4299009d00c87359858f7 to your computer and use it in GitHub Desktop.
WebRTC
https://itdepends.hateblo.jp/entry/2020/05/04/120500
WebRTCの勉強のためのサンプルプログラムです。
自由に改変してかまいません。
利用は自己責任でお願いします。
/**
* シグナリングサーバー(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に合わせて変更してください。
* {
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
}
<!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>
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)
})
}
<!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>
<!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