iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 26
0
Modern Web

ElectronJS系列 第 26

[ Day 26 ] - 分享螢幕(三) - 分享攝像頭畫面 HTML 版

[ Day 26 ] - 分享螢幕(三) - 分享攝像頭畫面 HTML 版

原本今天想要做一個可以分享個人桌面給其他人的 Electron 應用程式 , 
沒將程式做出來 , 才臨時改成介紹 HTML 的版本 (×_×)

我們使用 WebRTC 作為分享螢幕的工具 , 今天先製作存 HTML 可用的版本 , 明天將其轉換成 Electron 的版本

需要使用 Socket.io 傳遞 WebRTC 的 Signaling , 下方建立 socket.io 的 server

// Node.js
const port = process.env.PORT || 3000;

const server = require('http').createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.write('Hello World!');
    res.end();
}).listen(port);

// Socket.io
const io = require('socket.io')(server);

function findNowRoom(client) {
    return Object.keys(client.rooms).find(item => {
        return item !== client.id
    });
}

io.on('connection', client => {
    console.log(`socket 用戶連接 ${client.id}`);

    client.on('joinRoom', room => {
        console.log(room);

        const nowRoom = findNowRoom(client);
        if (nowRoom) {
            client.leave(nowRoom);
        }
        client.join(room, () => {
            io.sockets.in(room).emit('roomBroadcast', '已有新人加入聊天室!');
        });
    });

    client.on('peerconnectSignaling', message => {
        console.log('接收資料:', message);

        const nowRoom = findNowRoom(client);
        client.to(nowRoom).emit('peerconnectSignaling', message)
    });

    client.on('disconnect', () => {
        console.log(`socket 用戶離開 ${client.id}`);
    });
});

然後下方做一個 HTML 使用 WebRTC

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>WebRTC</title>
    <script src="https://cdn.jsdelivr.net/npm/socket.io-client@2/dist/socket.io.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
</head>
<body>
<div id="app">
    <div class="row m-3 ml-5">
        <button class="btn btn-primary" @click="initPeerConnection">初始化</button>
        <button class="btn btn-primary btnCall ml-3" @click="createSignal(true)">send offer</button>
        <input type="text" class="form-control ml-3 col-2" v-model="room" placeholder="請輸入房號">
        <button class="btn btn-primary ml-3" @click="joinRoom">加入房間</button>
        <input type="text" class="form-control ml-3 col-2" v-model="text" placeholder="請輸入訊息"
               @keypress.enter="sendText">
        <button class="btn btn-primary ml-3" @click="sendText">送出訊息</button>
    </div>
    <div class="row ml-5 border border-info mr-5 position-relative">
        <video width="200" height="200" class="rtc" autoplay
               id="myVideo" ref="myVideo" muted playsinline></video>
        <video width="500" height="500" class="rtc ml-5" autoplay
               id="remoteVideo" ref="remoteVideo" playsinline></video>
        <div class="col-3 position-absolute" style="top: 250px">
            <button class="btn btn-info ml-1" @click="closeTrack('audio', !audioTracks[0].enabled)">
                {{ audioTracks && audioTracks[0].enabled ? '音訊On': '音訊Off' }}
            </button>
            <button class="btn btn-info ml-3" @click="closeTrack('video', !videoTracks[0].enabled)">
                {{ videoTracks && videoTracks[0].enabled ? '視訊On': '視訊Off' }}
            </button>
        </div>
    </div>
</div>

<script>
    const socket = io('http://localhost:3000');

    const app = new Vue({
        el: '#app',
        data: {
            localstream: null,
            pc: null,
            videoTracks: null,
            audioTracks: null,
            configuration: {
                iceServers: [{
                    urls: 'stun:stun.l.google.com:19302' // Google's public STUN server
                }]
            },
            signalOption: {
                offerToReceiveAudio: 1,
                offerToReceiveVideo: 1
            },
            offer: null,
            room: null,
            text: null,
        },
        created() {
            this.onSocket();
        },
        methods: {
            async initPeerConnection() {
                await this.createMedia();
                this.getAudioVideo();
                this.createPeerConnection();
                this.addLocalStream();
                this.onIceCandidates();
                this.onIceconnectionStateChange();
                this.onAddStream();
            },
            async createMedia() {
                // 儲存本地流到全域
                this.localstream = await window.navigator.mediaDevices.getUserMedia({ audio: true, video: true })

                this.$refs.myVideo.srcObject = this.localstream;
            },
            // 取得裝置名稱
            getAudioVideo() {
                this.videoTracks = this.localstream.getVideoTracks();
                this.audioTracks = this.localstream.getAudioTracks();

                if (this.videoTracks.length > 0) {
                    console.log(`影像配置: ${this.videoTracks[0].label}`)
                };
                if (this.audioTracks.length > 0) {
                    console.log(`聲音配置: ${this.audioTracks[0].label}`)
                };
            },
            createPeerConnection() {
                // 建立 P2P 連接
                this.pc = new RTCPeerConnection(this.configuration);
                console.log(`建立 peer connection`);
            },
            addLocalStream(){
                // 增加本地流
                this.pc.addStream(this.localstream)
            },
            onIceCandidates() {
                // 監聽 Ice Server
                // 找尋到 ICE 候選位置後,送去 server 與另一位配對
                this.pc.onicecandidate = ({ candidate }) => {
                    if (!candidate) { return; }
                    console.log('onIceCandidate => ', candidate);
                    socket.emit("peerconnectSignaling", {
                        candidate,
                        to: 'jedy-0',
                        from: 'hiro-1',
                        room: '0509'
                    });
                };
            },
            onIceconnectionStateChange() {
                // 監聽 Ice 連接狀態
                this.pc.oniceconnectionstatechange = (evt) => {
                    console.log('ICE 伺服器狀態變更 => ', evt.target.iceConnectionState);
                };
            },
            onAddStream() {
                // 監聽是否有流傳入,如果有的話就顯示影像
                this.pc.onaddstream = (event) => {
                    console.log('this => ', this)
                    if(!this.$refs.remoteVideo.srcObject && event.stream){
                        this.$refs.remoteVideo.srcObject = event.stream;
                        console.log('接收流並顯示於遠端視訊!', event);
                    }
                }
            },
            // --------------
            sendSignalingMessage(desc, offer) {
                const isOffer = offer ? "offer" : "answer";
                console.log(`寄出 ${isOffer}`);
                socket.emit("peerconnectSignaling", {
                    desc: desc,
                    to: 'jedy-0',
                    from: 'hiro-1',
                    room: '0509'
                });
            },
            async createSignal(isOffer) {
                try {
                    if (!this.pc) {
                        console.log('尚未開啟視訊')
                        return;
                    }
                    this.offer = await this.pc[`create${isOffer ? 'Offer' : 'Answer'}`](this.signalOption);

                    await this.pc.setLocalDescription(this.offer);
                    this.sendSignalingMessage(this.pc.localDescription, isOffer ? true : false)
                } catch(err) {
                    console.log(err);
                }
            },
            onSocket() {
                const vm = this;
                socket.on('peerconnectSignaling', async ({ desc, from, candidate }) => {
                    if (desc && !vm.pc.currentRemoteDescription) {
                        console.log('desc => ', desc);
                        await vm.pc.setRemoteDescription(new RTCSessionDescription(desc));
                        await vm.createSignal(desc.type === 'answer' ? true : false);
                    } else if (candidate) {
                        console.log('candidate =>', candidate);
                        vm.pc.addIceCandidate(new RTCIceCandidate(candidate));
                    }
                });
                socket.on('message', message => {
                    console.log('房間接收 => ', message);
                });
                socket.on('roomBroadcast', message => {
                    console.log('房間廣播 => ', message);
                });
            },
            joinRoom() {
                if (!this.room) {
                    return;
                }
                socket.emit('joinRoom', this.room);
                this.room = null;
            },
            sendText() {
                if (!this.text) {
                    return;
                }
                socket.emit('message', this.text);
                this.text = null;
            },
            closeTrack(trackName, isOpen) {
                // console.log(this.videoTracks);
                this[`${trackName}Tracks`][0].enabled = isOpen
                this[`${trackName}Tracks`] = this.localstream[trackName === 'video'? 'getVideoTracks' : 'getAudioTracks']()
            }
        },
    });
</script>

<style>
    #myVideo{
        width: 200px;
        height: 200px;
        background: rgba(141, 167, 167, 0.178);
        transform: scaleX(-1);
    }

    #remoteVideo{
        width: 500px;
        height: 500px;
        background: rgba(141, 167, 143, 0.178);
    }
</style>
</body>
</html>

之後成功用 WebRTC 做視訊聊天

參考資料

今年小弟第一次參加 `鐵人賽` , 如文章有誤 , 請各位前輩提出指正 , 感謝  <(_ _)>

上一篇
[ Day 25 ] - 分享螢幕(二) - 錄製螢幕訊息
下一篇
[ Day 27 ] - 分享螢幕(四) - 分享螢幕 Electron 版
系列文
ElectronJS38
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言