LoginSignup
1

More than 5 years have passed since last update.

[webRTC for web vol2] 複数人での接続

Last updated at Posted at 2017-08-06

複数人での通信をやります
1:1は前回→webRTC for web vol1

成果物

デモ
https://webrtc-web.netlify.com/

使うもの

  • vue
  • webRTC(ブラウザ埋め込みのやつ、要クロム)
  • firebase

流れ

参考

flow.png

入ってきた人が すでにいる人に 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で複数接続。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1