Xây dựng tính năng Random Video Chat với WebRTC và WebSocket (Tập 1)

Trong chuỗi blog này, mình sẽ hướng dẫn các bạn xây dựng tính năng ghép cặp ngẫu nhiên cho video chat với WebRTC và WebSocket. Sản phẩm cuối cùng chúng ta nhận được sẽ như sau

Demo kết quả cuối cùng

Mặc dù đã biết đến WebRTC đã lâu nhưng mãi đến gần đây mình mới có cơ hội nghiên cứu và áp dụng vào dự án cá nhân. Cụ thể hơn thì hiện tại dự án mình đang đảm nhiệm yêu cầu thiết lập kết nối trực tiếp theo liên kết peer-to-peer nhằm cho mục đích video conference.

Video conference của Skype

Hầu như các ứng dụng trò chuyện trực tuyến như Skype hay Messenger đều dưới dạng là Direct Signal hay Tín hiệu trực tiếp. Tuy nhiên, trong ứng dựng này, chúng ta sẽ phải xử lý việc Matchmaking hay Ghép cặp thưởng thấy trong các tựa game online multiplayer.

Trong bài viết này, cụ thể là tập 1, mình sẽ chỉ nếu sơ lược qua về cách kiến trúc cũng như các kiến thức cần thiết để xây dựng tính năng này nhé.


Sơ lược về các công nghệ

Các công nghệ và phương pháp mình sử dụng để triển khai tính năng này bao gồm

  • WebRTC: Trong những năm trở lại đây thì WebRTC đã có không quá đỗi là xa lạ với cộng đồng lập trình viên. Với công nghệ kết nối peer-to-peer hỗ trợ cho việc hai client được kết nối trực tiếp với nhau dưới dạng là các peer nhằm trao đổi các tệp dữ liệu trong thời gian thực. WebRTC là một giải pháp hiệu quả nhằm phát triển các tính năng như giao tiếp bằng trò chuyện video hoặc audio.
  • WebSocket: Mặc dù nghe thì WebRTC có những nét tương đồng với WebSocket nhưng thật ra, đây là 2 công nghệ hoàn toàn khác nhau. Nếu WebRTC thiết lập kết nối peer-to-peer giữa các máy người dùng nhằm gửi/nhận dữ liệu không thông qua máy chủ thì WebSocket tạo ra kết nối TCP hoặc UDP để vận chuyển dữ liệu hai chiều từ client-server và server-client (bidirectional transmission). Mặc dù WebRTC không cần máy chủ trung gian cho việc truyền dữ liệu, tuy nhiên, để các máy khách (client) có thể kết nối được với nhau thì chúng ta cần một máy chủ tín hiệu (signaling server). Và WebSocket sẽ được dùng để kiến trúc máy chủ ấy.
  • Flutter: Trong những năm gần đây, Flutter khá là phổ biến trong mảng phát triển ứng dụng di động với hệ sinh thái mạnh mẽ cùng với cộng đồng vững mạnh. Do đó, để demo tính năng này, mình sẽ sử dụng Flutter, cụ thể Flutter Web nhé.

Kiến trúc của kết nối RTC

Thành phần và cách thức hoạt động của WebRTC

Trước hết là về thành phần của WebRTC. WebRTC bao gồm một số thành phần cốt yếu để thiết lập được kết nối và hiển thị dữ liệu (ở đây là video). Các bạn có thể đọc qua bài viết này để có một cái nhìn rõ nhất về WebRTC nhé.

WebRTC là gì? Cách viết ứng dụng gọi video bằng WebRTC và Firebase - Trung tâm hỗ trợ kỹ thuật | MATBAO.NET
[kkstarratings force=“false” valign=“top” align=“right”]WebRTC là gì? WebRTC là các API viết bằng javascript giúp giao tiếp theo thời gian thực mà không cần cài plugin hay phần mềm hỗ trợ. WebRTC có khả năng hỗ trợ trình duyệt giao tiếp thời gian thực thông qua Video Call, Voice Call hay transfer da…

Trong khuôn khổ bài viết này, chúng ta chỉ cần quan tâm đến MediaStream và RTCPeerConnection. Từ bài viết của Mắt Bão, thì có thể hiểu như sau:

  • MediaStream: là một stream dữ liệu âm thanh và hình ảnh, bằng cách gọi hàm getUserMedia để khởi tạo khi làm việc cục bộ. MediaStream sẽ cho phép truy cập vào stream của một máy tính sau khi một kết nối WebRTC được thiết lập với một máy tính khác. Một MediaStream sẽ có input và output với input dùng để lấy dữ liệu hình ảnh và âm thanh của local trong khi output dùng để hiển thị các dữ liệu này lên view hoặc được RTCPeerConnection sử dụng.
  • RTCPeerConnection: Là phần quan trọng giúp kết nối MediaStream và RTCDataChannel trở thành WebRTC. RTCPeerConnection là API giúp kết nối giữa hai trình duyệt, cung cấp các phương thức để kết nối, duy trì kết nối và đóng kết nối khi không còn nhu cầu sử dụng.

Và để các máy khách có thể kết nối với nhau và nhận được các dữ liệu video từ máy khách còn lại, mình sẽ lấy ví dụ là Amy là caller (người gọi) cần kết nối tới Bob (người được gọi) nhé thì cần thông qua các bước sau:

Sơ đồ mô tả cách các máy khách gửi/nhận tin hiệu

Bước 1: Thiết lập kết nối RTCPeerConnection

RTCPeerConnection createPeerConnection(){
	Map<String, dynamic> configuration = {
      "iceServers": [
        {"url": "stun:stun.l.google.com:19302"}
      ]
    };

    // SDP: Session Description Protocol
    final Map<String, dynamic> offerSdpConstraints = {
      "mandatory": {"OfferToReceiveAudio": true, "OfferToReceiveVideo": true},
      "optional": []
    };

    // Get local user media
    _localStream = await getUserMedia();

    // Create a peer connection
    RTCPeerConnection pc =
        await createPeerConnection(configuration, offerSdpConstraints);

    // Add a local stream to peer connection
    pc.addStream(_localStream!);
    
    return pc;
}
Code để tạo peerConnection

Giải thích qua về các dòng code phức tạp ở trên thì để khởi tạo được peerConnection, chúng ta cần khai báo cấu hình cũng như các giới hạn bắt buộc và không bắt buộc cho kết nối đó. Khi nhìn vào code để tạo cấu hình (configuration), chúng ta sẽ dẫn đến một đường dẫn liên kết đến STUN server. Để tìm hiểu về STUN server, các bạn có thể đọc bài viết này.

Stun server là gì? Tổng quan về Stun server mà bạn cần biết - BKNS.VN
Stun server được giao tiếp thông qua cổng UDP 3478. Stun server có 2 địa chỉ IP, nó sẽ gợi ý cho Stun client thử kết nối với IP và một số cổng khác.

Bước 2: Amy tạo offer

Để bắt đầu nhận tín hiệu từ một máy khách nào đó, nếu đóng vai trò là một host, Amy sẽ tạo một offer thông qua API createOffer của WebRTC, API này sẽ trả về session description (mô tả phiên). Định nghĩa của session description như sau:

Cấu hình của một điểm cuối giao tiếp (endpoint) trong kết nối WebRTC được gọi là mô tả phiên hay session description. Mô tả này sẽ bao gồm các thông tin của phương tiện truyền thông được gửi đi, định dạng của nó, giao thức truyền tải được sử dụng, địa chỉ IP và cổng của điểm cuối giao tiếp, và các thông tin khác cần thiết để mô tả một điểm cuối gia tiếp truyền tải phương tiện truyền thông (media transfer endpoint).
RTCSessionDescription description =
        await _peerConnection!.createOffer({"offerToReceiveVideo": 1});
Code để tạo offer trong Flutter

Sau khi đã tạo offer và nhận được description, Amy sẽ setLocalDescription với description vừa được khai báo. Có thể hiểu setLocalDescription là thiết lập mô tả phiên nội bộ (trên máy của Amy).

Future<String> offer() async {
    // Step 1: caller creates offer
    RTCSessionDescription description =
        await _peerConnection!.createOffer({"offerToReceiveVideo": 1});
    var session = parse(description.sdp as String);
    print(json.encode(session));
    _offer = true;
    // Step 2: caller sets localDescription
    await _peerConnection!.setLocalDescription(description);
    return json.encode(session);
  }

Bước 3: Bob nhận mô tả phiên của Amy và tạo answer

Mô tả phiên của Amy sẽ được xử lý bởi máy chủ tín hiệu và gửi về cho Bob sau quá trình matchmaking. Bob khi nhận được mô tả phiên, sẽ setRemoteDescription (tạm dịch là thiết lập phiên trên remote) với loại mô tả phiên là offer

void setRemoteDescription(String jsonString, String? type) async {
    dynamic session = await jsonDecode('$jsonString');

    String sdp = write(session, null);
    RTCSessionDescription description = new RTCSessionDescription(
        sdp, type == null ? (_offer ? "answer" : "offer") : type);

    new Logger().log(Level.info, json.encode(description.toMap()));

    await _peerConnection!.setRemoteDescription(description);
  }

Sau đó thì các bước được lập lại như lúc Amy khởi tạo offer. Bob sẽ tạo answer với API là createAnswer và setLocalDescription là mô tả phiên được trả về từ createAnswer

Future<String> answer() async {
    // Step 5: callee creates answer
    RTCSessionDescription description =
        await _peerConnection!.createAnswer({"offerToReceiveVideo": 1});

    var session = parse(description.sdp as String);
    print(json.encode(session));
    _offer = false;
    // Step 6: callee sets local description
    await _peerConnection!.setLocalDescription(description);
    return json.encode(session);
  }

Bước 4: Amy nhận answer

Cũng như bước 3, Mô tả phiên của Bob sẽ được xử lý bởi máy chủ tín hiệu và gửi về cho Amy. Bob khi nhận được mô tả phiên, sẽ setRemoteDescription với loại mô tả phiên là answerb.


Đón xem phần tiếp theo trong tập 2. Trong tập tiếp theo, mình sẽ tiếp tục một vài thứ về WebRTC và giới thiệu qua cách mình sử dụng WebSocket làm máy chủ tín hiệu.

Góc Của Chung

Góc Của Chung