複数人での通信をやります
1:1は前回→webRTC for web vol1
#成果物
デモ
https://webrtc-web.netlify.com/
使うもの
- vue
- webRTC(ブラウザ埋め込みのやつ、要クロム)
- firebase
流れ
参考
入ってきた人が すでにいる人に callme を送る。
callme を受けた人は offer を送る。
あとは普通に。
実装
Wamp
簡易wampサーバーはこちら → webRTC for ios vol2
wamp.js
class Wamp{
static get config(){
return {
HandshakeEndpint: "wss://nakadoribooks-webrtc.herokuapp.com"
, Topic: {
Callme: "com.nakadoribook.webrtc.[roomId].callme"
, Close: "com.nakadoribook.webrtc.[roomId].close"
, Answer: "com.nakadoribook.webrtc.[roomId].[id].answer"
, Offer: "com.nakadoribook.webrtc.[roomId].[id].offer"
, Candidate: "com.nakadoribook.webrtc.[roomId].[id].candidate"
}
}
}
static setup(roomId, userId, callbacks){
Wamp.roomId = roomId
Wamp.userId = userId
Wamp.callbacks = callbacks
}
static connect(){
let connection = new autobahn.Connection({
url: Wamp.config.HandshakeEndpint
});
connection.onopen = (session, details) => { Wamp.onOpen(session, details) }
connection.onclose = (reason, details) => { Wamp.onClose(session, details) }
connection.open()
Wamp.connection = connection
}
static roomTopic(base){
return base.replace("[roomId]", Wamp.roomId)
}
static endpointAnswer(targetId){
return Wamp.roomTopic(Wamp.config.Topic.Answer).replace("[id]", targetId)
}
static endpointOffer(targetId){
return Wamp.roomTopic(Wamp.config.Topic.Offer).replace("[id]", targetId)
}
static endpointCandidate(targetId){
return Wamp.roomTopic(Wamp.config.Topic.Candidate).replace("[id]", targetId)
}
static endpointCallme(){
return Wamp.roomTopic(Wamp.config.Topic.Callme)
}
static endpointClose(){
return Wamp.roomTopic(Wamp.config.Topic.Close)
}
static onOpen(session, details){
Wamp.session = session
// subscribe
session.subscribe(Wamp.endpointAnswer(Wamp.userId), (args, kwArgs)=>{ Wamp.onReceiveAnswer(args, kwArgs) });
session.subscribe(Wamp.endpointOffer(Wamp.userId), (args, kwArgs)=>{ Wamp.onReceiveOffer(args, kwArgs) });
session.subscribe(Wamp.endpointCallme(), (args, kwArgs)=>{ Wamp.onReceiveCallme(args, kwArgs) });
session.subscribe(Wamp.endpointClose(), (args, kwArgs)=>{ Wamp.onCloseConnection(args, kwArgs) });
Wamp.callbacks.onOpen()
}
static onReceiveCallme(args, kwArgs){
let targetId = args[0]
if(targetId == this.userId){
return
}
Wamp.callbacks.onReceiveCallme(targetId)
}
static onReceiveAnswer(args, kwArgs){
let targetId = args[0]
let sdp = JSON.parse(args[1])
Wamp.callbacks.onReceiveAnswer(targetId, sdp)
}
static onReceiveOffer(args, kwArgs){
let targetId = args[0]
let sdp = JSON.parse(args[1])
Wamp.callbacks.onReceiveOffer(targetId, sdp)
}
static onCloseConnection(args, kwArgs){
let targetId = args[0]
if (targetId == this.userId){
return;
}
Wamp.callbacks.onCloseConnection(targetId)
}
static onClose(reason, details){
console.log("onClose")
console.log(reason)
}
}
RTCPeerConnection周り。
自分のストリームは一個だけ(Webrtc.stream)で
一人の相手ごとに一個インスタンス作る
webrtc.js
class Webrtc{
static get config(){
return {
IceUrl: "stun:stun.l.google.com:19302"
}
}
static setup(){
return new Promise(function(resolve, reject) {
navigator.mediaDevices.getUserMedia({ audio: true, video: true })
.then((stream) => {
Webrtc.stream = stream
resolve(stream)
})
});
}
constructor(callbacks){
this.callbacks = callbacks
this.setupPeerConnection()
}
createOffer(callback){
let self = this
return new Promise(function(resolve, reject) {
self.peerConnection.createOffer().then((descriptin) => {
self.peerConnection.setLocalDescription(descriptin);
resolve(descriptin)
}).catch((error)=>{
console.error(error)
});
})
}
receiveAnswer(sdp){
this.peerConnection.setRemoteDescription(sdp).then(()=>{
}).catch((error)=>{
console.error(error)
})
}
receiveOffer(sdp){
let self = this
return new Promise(function(resolve, reject) {
self.peerConnection.setRemoteDescription(sdp).then(()=>{
self.peerConnection.createAnswer().then((answerSdp) => {
self.peerConnection.setLocalDescription(answerSdp)
resolve(answerSdp)
})
})
})
}
receiveCandidate(candidate){
this.peerConnection.addIceCandidate(candidate)
}
setupPeerConnection(){
let peerConnection = new RTCPeerConnection({iceServers: [{url: Webrtc.config.IceUrl}]});
this.peerConnection = peerConnection
this.peerConnection.addStream(Webrtc.stream)
peerConnection.onicecandidate = (event) => {
let candidate = event.candidate
if (candidate == null) {
return;
}
this.callbacks.onIceCandidate(candidate)
};
peerConnection.onaddstream = (event) => {
let stream = event.stream
if (stream == null){
return
}
this.callbacks.onAddedStream(stream)
};
peerConnection.onremovestream = (event) => {
// TODO 呼ばれない??
console.log("onremovestream")
console.log(event)
}
}
close(){
this.peerConnection.removeStream(Webrtc.stream)
this.peerConnection.close()
this.peerConnection = null;
this.callbacks = null;
}
}
一個分のコネクション管理
これも、一人の相手ごとに一個インスタンス作る
connection.js
class Connection {
constructor(myId, targetId, callbacks) {
this.myId = myId
this.targetId = targetId
this.callbacks = callbacks
// webrtc
this.webrtc = new Webrtc({
onIceCandidate: (candidate) => {
this.publishCandidate(candidate)
}
, onAddedStream: (stream) => {
stream.targetId = this.targetId
this.callbacks.onAddedStream(stream)
}
, onRemoveStream: (stream) =>{
console.log("onRemoveStream")
}
})
// for tricke ice
let candidateTopic = Wamp.endpointCandidate(this.myId)
Wamp.session.subscribe(candidateTopic, (args, kwArgs)=>{
this.onReceiveCandidate(args, kwArgs)
});
}
publishOffer(){
this.webrtc.createOffer().then((sdp)=>{
let str = JSON.stringify(sdp)
let topic = Wamp.endpointOffer(this.targetId)
Wamp.session.publish(topic, [this.myId, str]);
})
}
publishAnswer(remoteSdp){
this.webrtc.receiveOffer(remoteSdp).then((answerSdp) => {
let str = JSON.stringify(answerSdp)
let topic = Wamp.endpointAnswer(this.targetId)
Wamp.session.publish(topic, [this.myId, str]);
}).catch((error)=>{
console.error(error)
})
}
publishCandidate(candidate){
let str = JSON.stringify(candidate)
let topic = Wamp.endpointCandidate(this.targetId)
Wamp.session.publish(topic, [str]);
}
receiveAnswer(answerSdp){
this.webrtc.receiveAnswer(answerSdp)
}
onReceiveCandidate(args){
let candidate = JSON.parse(args[0])
this.webrtc.receiveCandidate(candidate)
}
close(){
console.log("closeConnection")
this.callbacks = null
this.webrtc.close()
}
}
アプリケーション
UI(css)はBulmaで。
index.html
<html>
<head>
<meta charset="utf8" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.5.0/css/bulma.min.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="app"></div>
<!-- ライブラリ -->
<script src="https://unpkg.com/vue"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.24.0/babel.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/2.0.2/anime.js"></script>
<script src="https://www.gstatic.com/firebasejs/4.2.0/firebase.js"></script>
<script src="https://www.gstatic.com/firebasejs/3.8.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/3.8.0/firebase-database.js"></script>
<script src="autobahn.min.js"></script>
<!-- 実装 -->
<script src="wamp.js"></script>
<script src="webrtc.js"></script>
<script src="connection.js"></script>
<script src="script.js"></script>
</body>
</html>
script.js
// ▼ component ---------
let MyStream = {
props: ['src', 'userId'],
template: `<div class="column is-2 box videoBox myBox">
<p>You ({{userId}})</p>
<video class="video" v-bind:src="src" />
</div>`,
mounted: function(){
let video = this.$el.querySelector("video")
video.oncanplay = ()=>{
video.play()
}
}
}
let RemoteStream = {
props: ['src', 'userId'],
template: `<div class="column box videoBox remoteBox">
<p>{{userId}}</p>
<video class="video" v-bind:src="src" />
</div> `,
mounted: function(){
let video = this.$el.querySelector("video")
video.oncanplay = ()=>{
video.play()
}
}
}
// ▼ Main ---------
let app = new Vue({
el: '#app',
template: `<div class="columns" id="videoList">
<my-stream
v-bind:src="myStream.src" v-bind:userId="userId"
/>
<div class="column is-10 columns">
<remote-stream v-for="stream in remoteStreamList"
v-bind:src="stream.src" v-bind:userId="stream.targetId"
/>
</div>
</div>`,
components: {
'remote-stream': RemoteStream
, 'my-stream': MyStream
},
data:{
userId: Math.random().toString(36).slice(-8)
, myStream : {"src": ""}
, remoteStreamList: []
},
created: function(){
this.connectionList = []
// ▼ firebase
let config = {
apiKey: "AIzaSyBddi0IhET7FoWts42BPWQFWsq38N17hPA",
authDomain: "nakadoriwebrtc.firebaseapp.com",
databaseURL: "https://nakadoriwebrtc.firebaseio.com",
projectId: "nakadoriwebrtc",
storageBucket: "nakadoriwebrtc.appspot.com",
messagingSenderId: "668320959045"
};
firebase.initializeApp(config);
let ref = firebase.database().ref('rooms')
let hash = location.hash
if(hash != null && hash.length > 0){
// 既存
let roomId = hash.slice( 1 ) ;
this.roomRef = ref.child(roomId)
return
}
// 新規
this.roomRef = ref.push()
this.roomRef.set({})
location.href = location.origin + location.pathname + "#" + this.roomRef.key
},
mounted: function(){
this.setupWamp()
this.setupStream()
Wamp.connect()
},
methods: {
setupStream: function(){
Webrtc.setup().then((stream) => {
this.myStream.src = window.URL.createObjectURL(stream);
})
},
setupWamp:function(){
Wamp.setup(this.roomRef.key, this.userId, {
onOpen: () => {
console.log("onOpen")
let topic = Wamp.endpointCallme()
Wamp.session.publish(topic, [this.userId]);
},
onReceiveOffer: (targetId, sdp) =>{
console.log("onReceiveOffer")
let connection = this.createConnection(targetId)
connection.publishAnswer(sdp)
},
onReceiveAnswer: (targetId, sdp) => {
console.log("onReceiveAnswer")
let connection = this.findConnection(targetId)
connection.receiveAnswer(sdp)
},
onReceiveCallme:(targetId)=>{
console.log("onReceivCallme")
let connection = this.createConnection(targetId)
connection.publishOffer()
},
onCloseConnection:(targetId) => {
console.log("onCloseConnection", targetId)
// removeConnection
let connectionList = this.connectionList
let connectionIndex = connectionList.findIndex((element, index, array)=>{
return element.targetId == targetId
})
if(connectionIndex != null){
connectionList[connectionIndex].close()
this.connectionList.splice(connectionIndex, 1)
}
// removeStream
let remoteStreamList = this.remoteStreamList
let streamIndex = remoteStreamList.findIndex((element, index, array)=>{
return element.targetId == targetId
})
if(streamIndex != null){
this.remoteStreamList.splice(streamIndex, 1)
}
}
})
},
createConnection: function(targetId){
let connection = new Connection(this.userId, targetId, {
onAddedStream:(stream)=>{
stream.src = window.URL.createObjectURL(stream);
this.remoteStreamList.push(stream)
}
})
this.connectionList.push(connection)
return connection
},
findConnection: function(targetId){
let connectionList = this.connectionList
for(var i=0,max=connectionList.length;i<max;i++){
let connection = connectionList[i]
if (connection.targetId == targetId){
return connection
}
}
console.error("not found connection", targetId)
},
closeConnection: function(){
let topic = Wamp.endpointClose()
Wamp.session.publish(topic, [this.userId]);
let connectionList = this.connectionList
for(var i=0,max=connectionList.length;i<max;i++){
let connection = connectionList[i]
connection.close()
}
this.connectionList = []
},
}
})
window.onbeforeunload = function(e) {
app.closeConnection()
};
次
iOSで複数接続。