日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長提供免費收錄網(wǎng)站服務(wù),提交前請做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

一、什么是WebRTC

WebRTC技術(shù)是激烈的開放的Web戰(zhàn)爭中一大突破-Brendan Eich, inventor of JAVAScript。

簡單來說,WebRTC 是一個音視頻處理+及時通訊的開源庫。在實時通信中,音視頻的采集和處理是一個很復(fù)雜的過程。比如音視頻流的編解碼、降噪和回聲消除等。由google發(fā)起開源,其中包含視頻音頻采集,編解碼,數(shù)據(jù)傳輸,音視頻展示等功能,我們可以通過技術(shù)快速地構(gòu)建出一個音視頻通訊應(yīng)用。雖然其名為WebRTC,但是實際上它不只是支持Web之間的音視頻通訊,還支持Android以及IOS端,此外由于該項目是開源的,我們也可以通過編譯C++代碼,從而達到全平臺的互通。

WebRTC的架構(gòu)圖為:

使用Swoole協(xié)程實現(xiàn) WebRTC 信令服務(wù)器

 

我們可以看到模塊化和分層的設(shè)計,我們文章的目的是演示瀏覽器端對端的連接流程,焦點是服務(wù)端信令服務(wù)器的實現(xiàn)方式,但需要提前介紹一些WebRTC的基本概念和連接流程。

二、基礎(chǔ)概念

流和軌

  • Track 軌道,可以理解每一路音頻或視頻,為一個軌,互不相交,類比火車軌道。
  • MediaStream 媒體流,每個媒體流中包含若干軌道,可以將音頻軌,視頻軌打包在一起。

三、幾個關(guān)鍵類

  • MediaStream 媒體流類,MeidiaStream用于將多個MediaStreamTrack對象打包到一起。一個MediaStream可包含audio track 與video track,并且可以添加或者刪除。
  • RTCPeerConnection 連接類,包含非常多重要功能,屏蔽復(fù)雜技術(shù)細(xì)節(jié),便于應(yīng)用層使用,包括但不限于連接管理,P2P類型檢測,NAT穿透,中轉(zhuǎn)等。
  • RTCDataChannel 非音視頻數(shù)據(jù)傳輸類,這個類在我們的例子中沒有涉及到??梢院唵卫斫鉃閷⒚襟w流信息或者數(shù)據(jù)信息塞到連接中,進行傳輸。

四、端對端連接流程

兩個不同網(wǎng)絡(luò)環(huán)境瀏覽器,要實現(xiàn)點對點的實時音視頻對話,需要處理哪些問題?

媒體協(xié)商

雙方需要知道對方支持的媒體格式,SDP(Session Description Protocol)是一種會話描述協(xié)議,視頻通訊的雙方必須先交換SDP信息,才能進一步互相通信。

網(wǎng)絡(luò)協(xié)商

雙方要了解對方的網(wǎng)絡(luò)情況,嘗試尋求一個可以互相通訊的鏈路,其中有尋路選擇,如果確實沒辦法建立點對點鏈路,會使用中繼服務(wù)器來進行轉(zhuǎn)發(fā)。如果是內(nèi)網(wǎng),或者大部分NAT網(wǎng)絡(luò)環(huán)境下,是可以建立端到端連接。在解決網(wǎng)絡(luò)打通問題時候,有幾個概念。

  • STUN(Session Traversal Utilities for NAT,NAT會話穿越應(yīng)用程序)是一種網(wǎng)絡(luò)協(xié)議,它允許位于NAT后的客戶端找出自己的公網(wǎng)地址,查出自己位于哪種類型的NAT之后以及NAT在公網(wǎng)的端口映射信息。這些信息被用來在兩端創(chuàng)建UDP連接通信。
  • TURN (Traversal Using Relays around NAT),如果客戶端在NAT之后, 那么在一些網(wǎng)絡(luò)情景下,有可能建立點對點的通訊連接,這時就需要公網(wǎng)的服務(wù)器作為一個中繼, 對數(shù)據(jù)進行轉(zhuǎn)發(fā)。

學(xué)習(xí)過程中,STUN和TURN服務(wù)器我們可使用coturn開源項目來搭建。

數(shù)據(jù)交換服務(wù)-信令服務(wù)器

WebRTC實現(xiàn)并沒有規(guī)定信令服務(wù)器的實現(xiàn)方式和相關(guān)協(xié)議,這給了業(yè)務(wù)方技術(shù)選型極大的靈活。我們今天就是使用php+Swoole協(xié)程實現(xiàn)一個簡單信令服務(wù)器。下面是一個端到端連接的流程圖,整個核心流程邏輯都在圖里面。

使用Swoole協(xié)程實現(xiàn) WebRTC 信令服務(wù)器

 

五、使用Swoole實現(xiàn)信令服務(wù)器

客戶端代碼模擬

<body>
<div style="display: block">
    <button class="btn" onclick="start()">連接<tton>
    <button class="btn" onclick="leave()">離開<tton>
</div>
<div>
    <div class="videos">
        <h1>Local</h1>
        <video id="localVideo" autoplay><ideo>
    </div>
    <div class="videos">
        <h1>Remote</h1>
        <video id="remoteVideo" autoplay><ideo>
    </div>
</div>
<script src="assets/js/adapter.js"></script>
<script type="text/JavaScript">
    const ws_config = '<?= $signaling_server ?>';
    const localVideo = document.getElementById('localVideo');
    const remoteVideo = document.getElementById('remoteVideo');
    const configuration = {
        iceServers: [{
            urls: '<?= $stun_server ?>'
        }]
    };

    let room_id = getQueryVariable('room_id');
    if (room_id == '' || room_id == null) {
        room_id = Math.random().toString(36).slice(-8);
        location.href = '?room_id=' + room_id;
    }
    let subject = 'room-' + room_id;//當(dāng)前主題
    let answer = 0;
    let ws = null;
    let pc, localStream;

    function getMediaStream(stream) {
        localVideo.srcObject = localStream;
        localStream = stream;
    }

    function start() {
        ws = new WebSocket(ws_config);
        ws.onopen = function (e) {
            subscribe(subject);
            if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
                console.error('the getUserMedia is not supported!');
                return;
            }
            navigator.mediaDevices.getUserMedia({
                audio: true,
                video: true
            }).then(function (stream) {
                if (localStream) {
                    stream.getAudioTracks().forEach((track) => {
                        localStream.addTrack(track);
                        stream.removeTrack(track);
                    });
                } else {
                    localStream = stream;
                }
                localVideo.srcObject = localStream;
                publish('call', null);
            }).catch(function (e) {
                console.error('Failed to get Media Stream!', e);
            });
        };
        ws.onmessage = function (e) {
            let package = JSON.parse(e.data);
            let data = package.data;
            console.log(e);
            switch (package.event) {
                case 'call':
                    icecandidate(localStream);
                    pc.createOffer({
                        offerToReceiveAudio: 1,
                        offerToReceiveVideo: 1
                    }).then(function (desc) {
                        pc.setLocalDescription(desc).then(
                            function () {
                                publish('offer', pc.localDescription);
                            }
                        ).catch(function (e) {
                            alert(e);
                        });
                    }).catch(function (e) {
                        alert(e);
                    });
                    break;
                case 'answer':
                    pc.setRemoteDescription(new RTCSessionDescription(data),        function () {}, function (e) {
                        alert(e);
                    });
                    break;
                case 'offer':
                    icecandidate(localStream);
                    pc.setRemoteDescription(new RTCSessionDescription(data),                            function () {
                        if (!answer) {
                            pc.createAnswer(function (desc) {
                                    pc.setLocalDescription(desc, function () {
                                        publish('answer', pc.localDescription);
                                    }, function (e) {
                                        alert(e);
                                    });
                                }
                                , function (e) {
                                    alert(e);
                                });
                            answer = 1;
                        }
                    }, function (e) {
                        alert(e);
                    });
                    break;
                case 'candidate':
                    pc.addIceCandidate(new RTCIceCandidate(data), function () {
                    }, function (e) {
                        alert(e);
                    });
                    break;
            }
        };
    }

    function leave() {
        pc.close();
    }

    function icecandidate(localStream) {
        pc = new RTCPeerConnection(configuration);
        pc.onicecandidate = function (event) {
            if (event.candidate) {
                publish('candidate', event.candidate);
            }
        };
        try {
            pc.addStream(localStream);
        } catch (e) {
            let tracks = localStream.getTracks();
            for (let i = 0; i < tracks.length; i++) {
                pc.addTrack(tracks[i], localStream);
            }
        }
        pc.onaddstream = function (e) {
            remoteVideo.srcObject = e.stream;
        };
    }

    function publish(event, data) {
        let obj = {
            cmd: 'publish',
            subject: subject,
            event: event,
            data: data
        };
        console.log(obj);
        ws.send(JSON.stringify(obj));
    }

    function subscribe(subject) {
        let obj = {
            cmd: 'subscribe',
            subject: subject
        };
        console.log(obj);
        ws.send(JSON.stringify(obj));
    }

    function getQueryVariable(variable) {
        var query = window.location.search.substring(1);
        var vars = query.split("&");
        for (var i = 0; i < vars.length; i++) {
            var pair = vars[i].split("=");
            if (pair[0] == variable) {
                return pair[1];
            }
        }
        return false;
    }
</script>
</body>

信令服務(wù)端實現(xiàn)

<?php

use SwooleHttpRequest;
use SwooleHttpResponse;

const WEBROOT = __DIR__ . '/web';
$connnection_map = array();
error_reporting(E_ALL);
Corun(function () {
    $server = new SwooleCoroutineHttpServer('0.0.0.0', 9509, true);
    $server->set([
        'ssl_key_file' => __DIR__ . '/ssl/ssl.key',
        'ssl_cert_file' => __DIR__ . '/ssl/ssl.crt',
    ]);
    $server->handle('/', function (Request $req, Response $resp) {
        //websocket
        if (isset($req->header['upgrade']) and $req->header['upgrade'] == 'websocket') {
            $resp->upgrade();
            $resp->subjects = array();
            while (true) {
                $frame = $resp->recv();
                if (empty($frame)) {
                    break;
                }
                $data = json_decode($frame->data, true);
                switch ($data['cmd']) {
                    case 'subscribe':
                        subscribe($data, $resp);
                        break;
                    case 'publish':
                        publish($data, $resp);
                        break;
                }
            }
            free_connection($resp);
            return;
        }
        /tp
        $path = $req->server['request_uri'];
        if ($path == '/') {
            $resp->end(get_php_file(WEBROOT . '/index.html'));
        } else {
            $file = realpath(WEBROOT . $path);
            if (false === $file) {
                $resp->status(404);
                $resp->end('<h3>404 Not Found</h3>');
                return;
            }
            if (strpos($file, WEBROOT) !== 0) {
                $resp->status(400);
                return;
            }
            if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
                $resp->end(get_php_file($file));
                return;
            }
            if (isset($req->header['if-modified-since']) and !empty($if_modified_since = $req->header['if-modified-since'])) {
                $info = stat($file);
                $modified_time = $info ? date('D, d M Y H:i:s', $info['mtime']) . ' ' . date_default_timezone_get() : '';
                if ($modified_time === $if_modified_since) {
                    $resp->status(304);
                    $resp->end();
                    return;
                }
            }
            $resp->sendfile($file);
        }
    });
    $server->start();
});


function subscribe($data, $connection)
{
    global $connnection_map;
    $subject = $data['subject'];
    $connection->subjects[$subject] = $subject;
    $connnection_map[$subject][$connection->fd] = $connection;
}

function unsubscribe($subject, $current_conn)
{
    global $connnection_map;
    unset($connnection_map[$subject][$current_conn->fd]);
}

function publish($data, $current_conn)
{
    global $connnection_map;
    $subject = $data['subject'];
    $event = $data['event'];
    $data = $data['data'];
    //當(dāng)前主題不存在
    if (empty($connnection_map[$subject])) {
        return;
    }
    foreach ($connnection_map[$subject] as $connection) {
        //不給當(dāng)前連接發(fā)送數(shù)據(jù)
        if ($current_conn == $connection) {
            continue;
        }
        $connection->push(
            json_encode(
                array(
                    'cmd' => 'publish',
                    'event' => $event,
                    'data' => $data
                )
            )
        );
    }
}

function free_connection($connection)
{
    foreach ($connection->subjects as $subject) {
        unsubscribe($subject, $connection);
    }
}

function get_php_file($file)
{
    ob_start();
    try {
        include $file;
    } catch (Exception $e) {
        echo $e;
    }
    return ob_get_clean();
}

1. 房間入口

下面是本地的效果圖,首頁可以輸入房間號加入,如果為空會自動生成一個隨機字符

使用Swoole協(xié)程實現(xiàn) WebRTC 信令服務(wù)器

 

2. 房間內(nèi)

下圖我在本地使用兩臺筆記本實現(xiàn)的一個效果圖,使用自簽的證書,這里特意展示了兩個不同的畫面來區(qū)分視頻同步效果。

使用Swoole協(xié)程實現(xiàn) WebRTC 信令服務(wù)器

 

請求流程分析

1. 在一臺電腦上點擊連接按鈕,通過綁定的點擊事件start()函數(shù),我們可以發(fā)現(xiàn),首先會創(chuàng)建一個websocket對象并發(fā)起連接,連接成功后,向信號服務(wù)器注冊設(shè)備,并獲取當(dāng)前設(shè)備的流媒體。獲取成功后,賦值給本地元素可以展示,并且賦值給全局變量localStream。

ws.onopen = function (e) {
            subscribe(subject);
            navigator.mediaDevices.getUserMedia({
                audio: true,
                video: true
            }).then(function (stream) {
                localVideo.srcObject = stream;
                localStream = stream;
                localVideo.addEventListener('loadedmetadata', function(){
                    publish('call', null);
                })
            }).catch(function (e) {
                alert(e);
            });
        };

2. 信令服務(wù)端器在收到subscribe和publish請求后,會在內(nèi)存中維護一個連接映射關(guān)系,核心邏輯是如果有其他連接進來,會進行廣播通知,這里并沒有實現(xiàn)一些細(xì)節(jié)邏輯,比如房間內(nèi)連接數(shù)量限制,房間滿了通知,退出連接通知等。

3. 另一個客戶端點擊連接會重復(fù)上一步驟,對端在收到其他客戶端加入房間通知后。

case 'call':
        icecandidate(localStream);//創(chuàng)建連接,并注冊網(wǎng)絡(luò)協(xié)商成功后給信令服務(wù)器發(fā)送信息的事件
        pc.createOffer({
            offerToReceiveAudio: 1,
            offerToReceiveVideo: 1
        }).then(function (desc) {
            pc.setLocalDescription(desc).then(//創(chuàng)建offer成功后,設(shè)置本地描述,并服務(wù)端綁定網(wǎng)絡(luò)信息,成功后給信令服務(wù)器發(fā)送SDP offer
                function () {
                    publish('offer', pc.localDescription);
                }
            ).catch(function (e) {
                alert(e);
            });
        }).catch(function (e) {
            alert(e);
        });
        break;

4. 信令服務(wù)端收到一端offer后會轉(zhuǎn)發(fā)給另一端,觸發(fā)客戶端的相應(yīng)邏輯,同樣會創(chuàng)建連接,并注冊網(wǎng)絡(luò)協(xié)商成功后給信令服務(wù)器發(fā)送信息的事件,同時會創(chuàng)建應(yīng)答,成功后也會設(shè)置本地描述,并向服務(wù)端發(fā)送綁定信息。同時向信令服務(wù)端發(fā)送answer信息,進行中轉(zhuǎn)到對端。

case 'offer':
        icecandidate(localStream);
        pc.setRemoteDescription(new RTCSessionDescription(data), function () {
            if (!answer) {
                pc.createAnswer(function (desc) {
                        pc.setLocalDescription(desc, function () {
                            publish('answer', pc.localDescription);
                        }, function (e) {
                            alert(e);
                        });
                    }
                    , function (e) {
                        alert(e);
                    });
                answer = 1;
            }
        }, function (e) {
            alert(e);
        });
        break;

5. 對端收到answer信息,設(shè)置遠(yuǎn)端的描述信息。當(dāng)雙方都完成offer,answer步驟后,此時雙方的媒體協(xié)商已經(jīng)完成。我們已經(jīng)綁定過網(wǎng)絡(luò)信息到服務(wù)端,各端會等待接收候選者列表。

case 'answer':
    pc.setRemoteDescription(new RTCSessionDescription(data), function () {
    }, function (e) {
        alert(e);
    });
    break;

6. 收到候選者列表后,需要把各自的候選信息通過信令服務(wù)器中轉(zhuǎn)到對方。

pc.onicecandidate = function (event) {
    if (event.candidate) {
        publish('candidate', event.candidate);
    }
};

7. 各端收到對方的候選者列表后,會把對端的候選者加入當(dāng)前連接通路的候選者列表中,然后雙方會進行連接檢測等等一系列復(fù)雜的操作,當(dāng)找到一個最優(yōu)的鏈路之后,就會建立連接,進行數(shù)據(jù)交互。

pc.addIceCandidate(new RTCIceCandidate(data), function () {
    }, function (e) {
        alert(e);
    });
    break;

信令服務(wù)端

我們介紹了建立連接的過程,針對服務(wù)端代碼,可以看到信令服務(wù)器端的代碼很少,加上http的服務(wù)總計100行代碼左右,怎樣達到通過同步編程的方式實現(xiàn)異步非阻塞IO,并且可以很輕松的實現(xiàn)并發(fā)百萬呢?

  • 首先通過構(gòu)造函數(shù)$server = new SwooleCoroutineHttpServer('0.0.0.0', 9509, true);會創(chuàng)建server對象。
  • 當(dāng)調(diào)用$server->start();方法后,會循環(huán)進行accept,accept連接后,會創(chuàng)建一個協(xié)程,這個協(xié)程內(nèi)所有的消息收發(fā),都會引起協(xié)程調(diào)度。
  • 可以低成本創(chuàng)建成千上萬協(xié)程,并發(fā)百萬沒問題,底層會為每個協(xié)程開辟獨立的??臻g,并基于多路復(fù)用技術(shù)(linux下為EPOLL)來進行調(diào)度。

信令服務(wù)器利用Swoole協(xié)程技術(shù),單進程支持異步非阻塞IO高并發(fā),但編程完全是同步阻塞的模式。如果想進一步要利用多核,可以采用Process Pool,加reuse port(Linux kernel 3.9)技術(shù),開啟多個進程同時處理,代碼倉庫中有一份server_co_pool.php的相關(guān)實現(xiàn)

$resp->subjects = array();
while (true) {
    $frame = $resp->recv();
    if (empty($frame)) {
        break;
    }
    $data = json_decode($frame->data, true);
    switch ($data['cmd']) {
        case 'subscribe':
            subscribe($data, $resp);//訂閱
            break;
        case 'publish':
            publish($data, $resp);//廣播除自己以外的連接
            break;
    }
}
free_connection($resp);

服務(wù)端處理核心邏輯為將當(dāng)前連接加入內(nèi)存map中,以供新的連接到來查找廣播,連接關(guān)閉時,清理對應(yīng)的主題和fd。

到此,我們使用Swoole協(xié)程實現(xiàn)WebRTC信令服務(wù)器結(jié)束。項目源碼已上傳至https://github.com/shiguangqi/SwooleWebRTC

備注:當(dāng)前例子運行環(huán)境為

  • PHP 7.2.14 (cli)
  • Swoole v4.4.16
  • Darwin mbp 19.3.0 Darwin Kernel Version 19.3.0 和 18.04.1-Ubuntu

謝謝,歡迎各位老師批評指正。

分享到:
標(biāo)簽:WebRTC
用戶無頭像

網(wǎng)友整理

注冊時間:

網(wǎng)站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨大挑戰(zhàn)2018-06-03

數(shù)獨一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運動步數(shù)有氧達人2018-06-03

記錄運動步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績評定2018-06-03

通用課目體育訓(xùn)練成績評定