Vue + WebRTC 實現音視訊直播(附自定義播放器樣式)

Ygria發表於2020-11-17

1. 什麼是WebRTC

1.1 WebRTC簡介

WebRTC,名稱源自網頁即時通訊(英語:Web Real-Time Communication)的縮寫,是一個支援網頁瀏覽器進行實時語音對話或視訊對話的實時通訊框架,提供了一系列頁面可呼叫API。

參考定義: 谷歌開放實時通訊框架

在上一篇部落格Vue +WebSocket + WaveSurferJS 實現H5聊天對話互動 中,已經涉及到WebRTC介面的使用,使用到了getUserMedia方法,用於通過瀏覽器獲取裝置麥克風,從而採集音訊。

最近專案中的需求則是與服務端建立即時通訊,實現低延遲音視訊直播。

RTC的特徵是(參考來源:https://www.zhihu.com/question/22301898)

  • 複雜度較高
  • 半可靠傳輸,對於特定情境(比如網路環境較差時)可以對音視訊進行有損傳輸,降低延遲
  • 音視訊友好:可針對音視訊做定製化優化
  • 提供端對端優化方案。 對於傳統連線模式,使用C/S架構,A=>服務端=>B,而WebRTC使用的是peer-to-peer模式,A=>B,一旦點和點之間的連線形成,它們之間的資料傳輸是不經過服務端的,大大降低了服務端的壓力。
  • 理論延遲較低,能應用在各種低延遲場景。

2. 業務描述

功能描述
實現對攝像裝置的管理列表,在裝置列表點選檢視視訊時,彈出頁面浮窗,進行攝像機攝像的視訊和音訊實時轉播。
視訊彈窗下方有自己實現的控制條,實現播放/暫停控制,能顯示播放時間、切換解析度、是否全屏等。

效果如圖
視訊浮窗 - hover狀態,顯示控制條

視訊浮窗 - 非hover狀態,隱藏控制條

3. 程式碼實現

3.1 Html模板程式碼

<el-dialog ref="videoDialog" title="視訊播放" :visible.sync="dialogShowed" :close-on-click-modal="false">
        <div id="dialog-wrap">
            <div id="video-wrap" v-if="isSuccess" v-loading="isLoading" element-loading-text="視訊載入中" element-loading-spinner="el-icon-loading"
                element-loading-background="rgba(0, 0, 0, 0.8)" />
            <div class="video-onloading" v-else v-loading="isLoading" element-loading-text="視訊載入中" element-loading-spinner="el-icon-loading"
                element-loading-background="rgba(0, 0, 0, 0.8)">
                <span><i class="el-icon-error" v-if="!isLoading" />{{errorMessage}}</span>
            </div>
            <!-- 遮罩層 -->
            <div class="cover" v-if="isSuccess">
                <div class="controls">
                  
                    <i class="el-icon-video-play" v-if="!isPlaying" @click="playOrPauseVideo" />
                    <i class="el-icon-video-pause" v-else @click="playOrPauseVideo" />
                    <div id="currentTime">播放時長:{{currentTime}}</div>
                    <div class="control-resolution">
                        解析度:
                        <el-select v-model="selectResolution" @change="changeResolution">
                            <el-option v-for="item in resolutions" :key="item" :value="item">
                                {{item}}
                            </el-option>
                        </el-select>
                    </div>
                    <i class="el-icon-full-screen" @click="onClickFullScreen"></i>
                </div>
            </div>
        </div>
    </el-dialog>

  • 使用了Element-UI框架提供的v-loading指令,該指令根據isLoading屬性決定是否在區域內載入loading動畫
    視訊載入狀態

  • 若視訊載入失敗,則顯示錯誤資訊
    顯示錯誤資訊

  • 預留標籤,用於掛載`video和audio DOM元素
    <div id="video-wrap" ></div>
    注意該標籤內最好不要再加其他元素,這樣後續判斷比較簡單。

3.2 建立連線、接收音訊

       getVideo() {
                let that = this;
                that.isLoading = true;
                that.pc = new RTCPeerConnection();
                that.pc.addTransceiver("video");
                that.pc.addTransceiver("audio");
                that.pc.ontrack = function (event) {
                    var el = document.createElement(event.track.kind);
                    el.srcObject = event.streams[0];
                    el.autoplay = true;
                    document.getElementById("video-wrap").appendChild(el);
                    if (el.nodeName === "VIDEO") {
                        el.oncanplay = () => {
                            that.isLoading = false;
                            // 播放狀態設定為true
                            that.isPlaying = true;
                            that.getVideoDuration();
                        };
                    } else if (el.nodeName === "AUDIO") {
                        el.oncanplay = () => {
   
                        };
                    }
                };
                that.pc
                    .createOffer()
                    .then((offer) => {
                        that.pc.setLocalDescription(offer);
                        let req = {
                            webrtc: offer,
                        };
                        console.log(offer);
                        return that.$api.device.getSignaling(
                            that.deviceData.id,
                            that.origin,
                            that.selectResolution,
                            req
                        );
                    })
                    .then((res) => {
                        if (res.code === 0) {
                            that.isSuccess = true;
                            that.pc.setRemoteDescription(res.body.webrtc);
                            that.connId = res.body.connId;
                        } else {
                        
                            that.errorMessage = res.message || "視訊載入錯誤";
                        }
                    })
                    .catch(alert);
            }

參考https://www.jianshu.com/p/43957ee18f1a,檢視Peer Connection建立連線的流程。
參考 https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection 檢視RTCPeerConnection 支援的介面

createOffer() 方法: 主動與其他peer建立P2P連線,把自己的SDP資訊整理好,通過signaling server轉發給其他peer。
在上面的程式碼中,通過向後端傳送POST請求,實現信令交換。

 that.pc.addTransceiver("video");
 that.pc.addTransceiver("audio");

指明同時接收音訊和視訊。

 that.pc.ontrack = function(event){
}

該方法進行音視訊的接收,使用接收到的資料建立video和audio元素。
只對pc狀態進行監聽無法監聽到實際視訊可以播放的狀態,因此需要對video新增監聽方法:

  el.oncanplay = () => {
     that.isLoading = false;
     // 播放狀態設定為true
    that.isPlaying = true;
    that.getVideoDuration();
};

在video可以播放時,才將loading狀態取消,並開始獲取video時長。

3.3 控制音視訊的JS程式碼

獲取視訊播放時長方法:

getVideoDuration() {
    var video = document.getElementsByTagName("video")[0];
    //  如果沒有獲取到視訊元素
    if (!video) {
        return;
    }
    let that = this;

    video.addEventListener("timeupdate", () => {
        that.currentTime = getTime(video.currentTime);
    });

    var getTime = function (time) {
        let hour =
            Math.floor(time / 3600) < 10
                ? "0" + Math.floor(time / 3600)
                : Math.floor(time / 3600);
        let min =
            Math.floor((time % 3600) / 60) < 10
                ? "0" + Math.floor((time % 3600) / 60)
                : Math.floor((time % 3600) / 60);
        var sec =
            Math.floor(time % 60) < 10
                ? "0" + Math.floor(time % 60)
                : Math.floor(time % 60);
        return hour + ":" + min + ":" + sec;
    };
}

控制音訊/視訊同步暫停的方法:

  playOrPauseVideo() {
    var video = document.getElementsByTagName("video")[0];
    var audio = document.getElementsByTagName("audio")[0];
    if (this.isPlaying) {
        video.pause();
        audio.pause();
    } else {
        // audio
        video.play();
        audio.play();
    }
    this.isPlaying = !this.isPlaying;
}

全屏方法

onClickFullScreen() {
    let dialogElement = document.getElementById("dialog-wrap");
    dialogElement.webkitRequestFullScreen();
}

3.4 樣式表

樣式部分較為簡單,值得注意的有以下幾點:

  • 隱藏原有視訊控制條,便於對控制條進行自定義
video::-webkit-media-controls {
    /* 去掉全屏時顯示的自帶控制條 */
    display: none !important;
}
  • 擴大hover熱區,視訊下半部分(高度為400px部分)懸浮顯示控制條
    (不設定為全部部分是因為如果設定為全部部分,則全屏狀態無法隱藏控制條)
    以下完整樣式表(scss):
    $controlFontColor: rgb(136 141 150);
    $backgroundColor: rgba(0, 0, 0, 0.8);
    $height: 60px;

    .el-dialog .el-dialog__body {
        padding: 0 !important;
        margin-bottom: 0 !important;
        width: unset !important;
    }

    .video-onloading {
        min-height: 500px;
        background-color: $backgroundColor;

        span {
            width: 100%;
            display: block;

            line-height: 500px;
            text-align: center;
            color: $controlFontColor;
            i {
                margin-right: 5px;
            }

            i::before {
                font-size: 17px;
            }
        }
    }

  .cover {
        bottom: 0px;
        height: 300px;
        position: absolute;
        width: 100%;
        z-index: 2;
        &:hover,
        &:focus,
        &:focus-within {
            .controls {
                display: flex;
            }
        }
    }
  .controls {
        width: 100%;
        height: $height;
        line-height: $height;
        font-size: 15px;
        display: none;
        z-index: 2;
        background-color: $backgroundColor;
        color: $controlFontColor;
        position: absolute;
        bottom: 0
        justify-content: space-between;

        & > [class^="el-icon-"] {
            &::before {
                font-size: 26px;
                line-height: $height;
                padding: 0 15px;
                cursor: pointer;
            }
        }

        .playStatus {
            width: 64px;
            height: $height;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
        }
        #currentTime {
            width: 140px;
            height: $height;
            text-align: center;
        }

        .control-resolution {
            line-height: $height;
            .el-input__inner {
                background: $backgroundColor;
            }
            .el-input {
                width: 95px;
            }
            input {
                border: none;
                font-size: 15px !important;
                color: $controlFontColor;
                &::-webkit-input-placeholder {
                    color: $controlFontColor;
                }
            }
        }
        #fullScreen {
            width: 32px;
            height: 32px;
            position: relative;
            top: 16px;
         
        }
    }

總結

本次的前端業務WebRTC只做了淺顯的瞭解和應用,只應用了接收流,還沒有用到推流,WebRTC還有更多用法,比如實現實時視訊通話、語音通話等,也許以後的業務中會用到,所以以這篇部落格做一個入門記錄~

相關文章