.NET Core第35天_整併signalr及peer.js(WebRTC)搭建的直播串流功能模組



使用.net6 mvc
在此先來更改一下預設範本的HomeController.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;

namespace BingoSys.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return Redirect($"/{Guid.NewGuid()}");
        }

        [HttpGet("/{roomId}")]
        public IActionResult Room(string roomId)
        {
            ViewBag.roomId = roomId;
            return View();
        }

    }
}







預設加入直播房空間都需要有一個獨一無二的id,在此用guid來實踐


接著到微軟signalr的文檔介紹
https://dotnet.microsoft.com/en-us/apps/aspnet/signalr

Get Started 點入後


新增用戶端連結 SignalR 庫
這裡我們透過libman來安裝client端需要的signalr js

libman install @microsoft/signalr@latest -p unpkg -d wwwroot/js/signalr --files dist/browser/signalr.js --files dist/browser/signalr.js


可於visual studio ide開啟終端機 (選命令提式字元)
下指令: libman --help
來學習這個libman指令集如何使用



或者也可以透過visual studio ide介面方式來配置



在此我想嘗鮮用指令模式(感覺比較快,ui挑選之前用過了換一下)


就成功配置到專案中了
LibMan 會 wwwroot/js/signalr 建立資料夾,並將選取的檔案複製到該資料夾。


於專案慕錄下創建一個Hubs目錄並在裏頭建立一個Hub子類

~\Hubs\DefaultHub.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
using Microsoft.AspNetCore.SignalR;

namespace BingoSys.Hubs
{
    public class DefaultHub : Hub
    {
        public async Task JoinRoom(string roomId , string userId)
        {
            await Groups.AddToGroupAsync(Context.ConnectionId, roomId);
            await Clients.Group(roomId).SendAsync("user-connected",userId);
        }
    }
}

調正相應Signalr服務註冊與Hub路由設置

註冊 SignalR 服務,並將編寫的 Hub 類映射到終結點(訪問地址),如此一來可將 SignalR 請求傳遞給 Hub 中心。

~\Program.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using BingoSys.Hubs;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddSignalR();
var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapAreaControllerRoute(
    name: "Admin",
    areaName: "Admin",
    pattern: "Admin/{controller=Home}/{action=Index}/{id?}"
);

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.MapHub<DefaultHub>("/meeting");

app.Run();


編修Room的檢視 
~\Views\Home\Room.cshtml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<h1 style="text-align:center">@ViewBag.roomId</h1>

<script src="~/js/signalr/dist/browser/signalr.js"></script>
<script>
    const connection = new signalR.HubConnectionBuilder()
                            .withUrl("/meeting").build();

    const startSignalR = async () => {
        await connection.start();
        await connection.invoke("JoinRoom" , 'roomId' , '10');
        connection.on('user-connected' , id => {
            console.log(`User connected : ${id}`);
        })
    }

    startSignalR();
</script>

運行做meeting路由功能觸發成功與否確認



連接 SignalR 客戶端需要使用一個唯一的 Connection ID 建立連接。SignalR 客戶端連接 Hub 中心時,SignalR 中心必須處於運行狀態。
在此Connection Id都暫時寫死為10 做測試


使用 peer.js 

https://peerjs.com/

眾所周知,WebRTC (Web Real Time Communication)網路即時通信,能允許網頁應用不需要透過中間伺服器就能直接互相傳送任一形態資料,應用於音樂串流、視訊串流、檔案等等。漸漸也成為了瀏覽器的一套規範。

在webrtc中,有一個特定的協定用於描述媒體訊息、網路資訊和其它一些關鍵訊息,稱為SDP(Session Description Protocol-會話描述協定)。而上述介紹的交換媒體資訊、網路資訊的過程,也稱為媒體協商,也就是SDP的交換。



以上圖為例就是Amy跟Bob之間通信過程,Amy 要先發一個Offer(即: 描述Amy自己會話的SDP), Bob收到後,做出Answer回應(即:描述Bob自己會話的SDP), 雙方完成SDP交換後, 根據前面的分析,取出二份SDP的交集, 即完成了媒體協商。


以下是mozilla開發者官網提供的一張圖

描述了關於WebRTC底層處理流程

  1. A透過STUN伺服器,收集自己的網路訊息
  2. A建立Offer SDP,透過Signal Channel(訊號伺服器)給到B
  3. B做出回應產生Answer SDP,透過Signal Channel給到A
  4. B透過STUN收集自己的網路訊息,透過Signal Channel給到A
備註:
若A,B彼此之間無法直接穿透(即:無法建立點對點的P2P直連),則將透過TURN伺服器做中轉。


peerjs 開源專案幫開發者簡化了WebRtc的開發過程,把SDP交換這些偏底層的細節都做了封裝,開發人員只需要關注應用程式本身就行了。

將Peer.js 的 CDN 直接複製起來引入做使用
<script src="https://unpkg.com/peerjs@1.5.1/dist/peerjs.min.js"></script>

編修Room的檢視畫面程式整併peer.js
~\Views\Home\Room.cshtml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@*<h1 style="text-align:center">@ViewBag.roomId</h1>*@

<div video-grid>

</div>

<script src="~/js/signalr/dist/browser/signalr.js"></script>
<script src="https://unpkg.com/peerjs@1.5.1/dist/peerjs.min.js"></script>

<script>
    const room_id = '@ViewBag.roomId';
    let user_id = null;
    let localStream = null;
    const connection = new signalR.HubConnectionBuilder()
                            .withUrl("/meeting").build();
    const myPeer = new Peer();
    myPeer.on('open' , id => {
        user_id = id;
        const startSignalR = async () => {
            await connection.start();
            await connection.invoke("JoinRoom", room_id, user_id);
            //connection.on('user-connected', id => {
            //    console.log(`User connected : ${id}`);
            //})
        }
        startSignalR();
    });

    const videoGrid = document.querySelector('[video-grid]');
    const myVideo = document.createElement('video');
    myVideo.muted = true;
    navigator.mediaDevices.getUserMedia({
        audio:true,
        video:true
    }).then(stream => {
        addVideoStream(myVideo,stream);
        localStream = stream;
    });

    connection.on('user-connected' , id => {
        console.log(`User connected : ${id}`);
    });

    const addVideoStream = (video , stream) => {
        video.srcObject = stream;
        video.addEventListener('loadedmetadata',() => {
            video.play();
        })
        videoGrid.appendChild(video);
    };

</script>

SignalR 和 PeerJS:這兩個函式庫用於實現即時通訊和點對點通訊。

signalR.HubConnectionBuilder 建立了一個 SignalR 連線物件,用於與伺服器進行即時通訊。
它連接到 /meeting 位址。

Peer() 用來創建了一個 PeerJS 連接物件,用於實現點對點通訊。

使用者可以透過 PeerJS 獲得唯一的使用者ID,在此儲存在 user_id 變數中。
然後,使用者將會透過 SignalR 連接到伺服器,並加入特定的房間(藉由 JoinRoom 方法),並通知其他用戶有新用戶連線。

使用 navigator.mediaDevices.getUserMedia 
來取得本機使用者的音訊和視訊串流,並將其儲存在 localStream 變數中。

定義了 addVideoStream 函數,用於將視訊串流新增至網頁中,並在頁面載入時自動播放。



調整css樣式
~\wwwroot\css\site.css

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
html {
  font-size: 14px;
}

@media (min-width: 768px) {
  html {
    font-size: 16px;
  }
}

html {
  position: relative;
  min-height: 100%;
}

body {
  margin-bottom: 60px;
}

div[video-grid]{
    display:grid;
    grid-template-columns:repeat(auto-fit,300px);
    grid-auto-rows:300px;
    gap:25px;
}

div[video-grid] > video{
    width:100%;
    height:100%;
    object-fit:cover;
}


HTML <div> 元素部分,是一個具有自訂屬性 video-grid 的 <div> 元素,用於將視訊顯示區域放置在頁面上。

peerjs的核心物件Peer,它有幾個常用方法:
  • peer.connect 建立點對點的連接
  • peer.call 向另1個peer端發起音視頻即時通信
  • peer.on 對各種事件的監控回調
  • peer.disconnect 斷開連接
  • peer.reconnect 重新連接
  • peer.destroy 銷毀對象

運行效果


https://www.3770.cc/BQ/38598

就能測試到有開啟視訊與音訊

不過這邊有一些BUG要修正比方相同user id連上來的防呆
比方我把URL後面串的相同GUID在另一個新視窗戳打後可以看到
第一次粉紅框輸出
第二次棕色框輸出

將防呆處裡補上




調整最終版本 ~\Views\Home\Room.cshtml
更改成類似加入Teams群聊房概念

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
@*<h1 style="text-align:center">@ViewBag.roomId</h1>*@

<div video-grid>

</div>

<script src="~/js/signalr/dist/browser/signalr.js"></script>
<script src="https://unpkg.com/peerjs@1.5.1/dist/peerjs.min.js"></script>

<script>
    const room_id = '@ViewBag.roomId';
    let user_id = null;
    let localStream = null;
    const connection = new signalR.HubConnectionBuilder()
                            .withUrl("/meeting").build();
    const myPeer = new Peer();
    myPeer.on('open' , id => {
        user_id = id;
        const startSignalR = async () => {
            await connection.start();
            await connection.invoke("JoinRoom", room_id, user_id);
            //connection.on('user-connected', id => {
            //    console.log(`User connected : ${id}`);
            //})
        }
        startSignalR();
    });
    const Peers = [];
    const videoGrid = document.querySelector('[video-grid]');
    const myVideo = document.createElement('video');
    myVideo.muted = true;
    navigator.mediaDevices.getUserMedia({
        audio:true,
        video:true
    }).then(stream => {
        addVideoStream(myVideo,stream);
        localStream = stream;
    });

    connection.on('user-connected' , id => {
        if(user_id === id)
            return;
        console.log(`User connected : ${id}`);
        connectNewUser(id , localStream);
    });

    connection.on('user-disconnected' , id => {
        console.log(`User disconnected: ${id}`);
        if( Peers[id]){
            Peers[id].close();
        }
    });

    myPeer.on('call' , call => {
        call.answer(localStream);

        const userVideo = document.createElement('video');
        call.on('stream' , userVideoStream => {
            addVideoStream(userVideo , userVideoStream);
        });
    });

    const addVideoStream = (video , stream) => {
        video.srcObject = stream;
        video.addEventListener('loadedmetadata',() => {
            video.play();
        })
        videoGrid.appendChild(video);
    };

    const connectNewUser = (userId , localStream) => {
        const userVideo = document.createElement('video');
        const call = myPeer.call(userId,localStream)

        call.on('stream' , userVideoStream => {
            addVideoStream(userVideo,userVideoStream)
        });

        call.on('close', () => {
            userVideo.remove();
        });

        Peers[userId] = call;
    }


</script>


~\Hubs\ 
多新增Users.cs裡面定義連線的dictionary
在 Users.cs 檔案中,定義了一個靜態字典 list,用於儲存連接的使用者資訊。

1
2
3
4
5
6
7
namespace BingoSys.Hubs
{
    public static class Users
    {
        public static IDictionary<string,string> list = new Dictionary<string,string>();
    }
}


最終調整~\Hubs\DefaultHub.cs 加上斷掉連線功能

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
using Microsoft.AspNetCore.SignalR;

namespace BingoSys.Hubs
{
    public class DefaultHub : Hub
    {
        public async Task JoinRoom(string roomId , string userId)
        {
            Users.list.Add(Context.ConnectionId, userId);
            await Groups.AddToGroupAsync(Context.ConnectionId, roomId);
            await Clients.Group(roomId).SendAsync("user-connected",userId);
        }

        public override Task OnDisconnectedAsync(Exception? exception)
        {
            Clients.All.SendAsync("user-disconnected" , Users.list[Context.ConnectionId]);
            return base.OnDisconnectedAsync(exception);
        }

    }
}

DefaultHub.cs 檔案中定義了一個 SignalR Hub 類,包含了處理使用者加入房間和斷開連接的方法。當使用者加入房間時,其資訊會被加入到 Users.list 中,並通知房間內的其他使用者。當用戶斷開連線時,通知所有用戶該用戶已中斷連線。


總結:使用了 SignalR 實現即時通信,PeerJS 實現點對點視訊通話,以及瀏覽器的媒體設備存取來獲取用戶的音訊和視訊串流。


在最後我們透過ngrok來測試
ngrok.exe http https://localhost:{你的應用port}




測試完再將此process關閉










Ref:
https://juejin.cn/post/7065913779761971214
https://www.cnblogs.com/yjmyzz/p/peerjs-tutorial.html

留言

這個網誌中的熱門文章

何謂淨重(Net Weight)、皮重(Tare Weight)與毛重(Gross Weight)

Architecture(架構) 和 Framework(框架) 有何不同?_軟體設計前的事前規劃的藍圖概念

經得起原始碼資安弱點掃描的程式設計習慣培養(五)_Missing HSTS Header