+1

Tìm hiểu về giải pháp Digital Humans phần 5: AIORTC - Đưa Digital human đến gần hơn với người dùng

Sơ đồ hoạt động của hệ thống:

Dưới đây là sơ đồ hoạt động của hệ thống digital human:

image.png

Nhìn vào kiến trúc tổng thể, ta thấy rằng để hiển thị Digital Humans một cách sống động, chúng ta cần đồng bộ hóa giữa hình ảnh video và âm thanh, và đẩy cả hai vào một luồng stream duy nhất. Nhưng đó mới chỉ là một nửa câu chuyện. Trải nghiệm sẽ chẳng thể trọn vẹn nếu người dùng không thể tương tác trực tiếp, trò chuyện một cách tự nhiên với Digital Human của mình trong thời gian thực.

Đây chính là lúc chúng ta cần nghĩ đến WebRTC. WebRTC nổi tiếng được sinh ra để giải quyết bài toán giao tiếp thời gian thực, đảm bảo sự mượt mà và liền mạch cho trải nghiệm người dùng. Và thật may mắn, thư viện aiortc cung cấp cho chúng ta những công cụ có sẵn để hiện thực hóa điều này. Hãy cùng mình tìm hiểu nhé ^^

Thư viện aiortc

image.png

aiortc là gì ?

Hiểu đơn giản aiortc thực chất là một thư viện mã nguồn mở, mang sức mạnh của WebRTC đến với cộng đồng lập trình viên Python giúp nhiều nhà phát triển dễ dàng tiếp cận và đơn giản hóa việc tích hợp khả năng thời gian thực vào các dự án khác nhau.

Điểm cốt lõi làm nên sự linh hoạt của aiortc chính là việc nó được xây dựng trên asyncio, framework I/O bất đồng bộ tiêu chuẩn của Python. Điều này cho phép xử lý hiệu quả nhiều kết nối và luồng dữ liệu cùng lúc mà không làm tắc nghẽn chương trình.

Vậy WebRTC là gì?

image.png

Để dễ hình dung, WebRTC (Web Real-Time Communication) là một tập hợp các giao thức và API cho phép giao tiếp thời gian thực trực tiếp giữa các trình duyệt và thiết bị. Nó hỗ trợ truyền tải video, giọng nói và dữ liệu đa dạng giữa các "peer", giúp chúng ta xây dựng các ứng dụng giao tiếp hiệu quả mà không cần đến plugin độc quyền hay phần mềm bổ sung phức tạp.

Tại sao nên chọn aiortc?

image.png

Trong khi các triển khai WebRTC và ORTC phổ biến thường được tích hợp sẵn vào trình duyệt web hoặc tồn tại dưới dạng mã nguồn gốc (native code). Mặc dù đã được thử nghiệm rộng rãi, cấu trúc bên trong của chúng khá phức tạp và quan trọng hơn là không cung cấp "Python bindings" (cầu nối để Python có thể tương tác). Hơn nữa, chúng thường gắn chặt với một "media stack" (bộ công cụ xử lý đa phương tiện) cụ thể, gây khó khăn khi chúng ta muốn tích hợp các thuật toán xử lý âm thanh hoặc video của riêng mình.

Ngược lại, aiortc mang đến một cách tiếp cận rất khác biệt. Mã nguồn khá dễ đọc, dễ dùng, nhất là khi mình mới tìm hiểu về WebRTC (vốn đã nhiều khái niệm phức tạp). Hơn nữa do viết bằng python, điều này giúp ta dễ dàng tích hợp với các thư viện xử lý ảnh của python khác như OpenCV, NumPy,...

Nói tóm lại, vì chúng ta đang lập trình bằng python cho digital human, nên để dễ tích hợp, ta cần 1 thư viện python cho dễ code 🤣 Tuy nhiên, nếu bạn muốn một giải pháp WebRTC linh hoạt, dễ tiếp cận và tích hợp vào môi trường Python, aiortc là một lựa chọn sáng giá nha.

Các tính năng chính aiortc cung cấp:

  • Giao tiếp Ngang hàng (P2P): Giảm độ trễ, tăng hiệu suất truyền tải.
  • Xử lý Luồng Media hiệu quả: Tối ưu cho hội nghị video và livestreaming.
  • Dữ liệu đa năng: Cho phép trao đổi mọi loại dữ liệu, từ chia sẻ file thông qua kết nối.

Cài đặt:

pip install aiortc

Thực hành với code demo

Tạo một project demo chứa 3 file

demo/
├── index.html
├── client.js
└── main.py

main.py

là backend chính sử dụng thư viện aiohttp để xử lý các request HTTP và aiortc để xử lý logic WebRTC phía server:

import asyncio
import json
import os

from aiohttp import web
from av import VideoFrame
from aiortc import MediaStreamTrack, RTCPeerConnection, RTCSessionDescription, VideoStreamTrack
from aiortc.contrib.media import MediaBlackhole, MediaPlayer, MediaRecorder

ROOT = os.path.dirname(__file__)

class VideoTransformTrack(MediaStreamTrack):
    kind = "video"

    def __init__(self, track):
        super().__init__()
        self.track = track

    async def recv(self):
        frame = await self.track.recv()
        return frame

pcs = set()

async def index(request):
    content = open(os.path.join(ROOT, "index.html"), "r").read()
    return web.Response(content_type="text/html", text=content)

async def javascript(request):
    content = open(os.path.join(ROOT, "client.js"), "r").read()
    return web.Response(content_type="application/javascript", text=content)

async def offer(request):
    params = await request.json()
    offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"])

    pc = RTCPeerConnection()
    pcs.add(pc)

    @pc.on("connectionstatechange")
    async def on_connectionstatechange():
        print("Connection state is %s" % pc.connectionState)
        if pc.connectionState == "failed":
            await pc.close()
            pcs.discard(pc)

    @pc.on("track")
    def on_track(track):
        print("Track %s received" % track.kind)
        if track.kind == "video":
            local_video = VideoTransformTrack(track)
            pc.addTrack(local_video)

        @track.on("ended")
        async def on_ended():
            print("Track %s ended" % track.kind)

    # handle offer
    await pc.setRemoteDescription(offer)

    # send answer
    answer = await pc.createAnswer()
    await pc.setLocalDescription(answer)

    return web.Response(
        content_type="application/json",
        text=json.dumps(
            {"sdp": pc.localDescription.sdp, "type": pc.localDescription.type}
        ),
    )

async def on_shutdown(app):
    # close peer connections
    coros = [pc.close() for pc in pcs]
    await asyncio.gather(*coros)
    pcs.clear()

if __name__ == "__main__":
    app = web.Application()
    app.on_shutdown.append(on_shutdown)
    app.router.add_get("/", index)
    app.router.add_get("/client.js", javascript)
    app.router.add_post("/offer", offer)
    web.run_app(app, host="0.0.0.0", port=8080)

Giải thích 1 chút:

  • async def offer(request): Đây là hàm cốt lõi xử lý toàn bộ quá trình "bắt tay" (signaling) của WebRTC từ phía server.

    • Nhận "offer" từ client.
    • Tạo một đối tượng RTCPeerConnection (pc) cho client.
    • Thiết lập các trình xử lý sự kiện cho pc:
    • @pc.on("track"): Phát hiện khi server nhận được một luồng media (video) từ client. Tại đây, server sẽ lấy luồng video này, bọc nó bằng VideoTransformTrackvà thêm trở lại vào pc để gửi ngược lại cho client.
    • Tạo một "answer" và gửi "answer" này trở lại cho client.
  • class VideoTransformTrack(MediaStreamTrack): Đại diện cho luồng video mà server sẽ gửi đi. Tại đây ta có thể can thiệp và biến đổi các frame video trước khi gửi.

index.html

chịu tránh nhiệm hiển thị webcam và video stream bằng WebRTC cùng với các nút điều khiển.

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WebRTC Demo</title>
    <style>
    button {
        padding: 8px 16px;
        margin: 0 8px;
        cursor: pointer;
        background-color: #4CAF50;
        color: white;
        border: none;
        border-radius: 4px;
    }
    video {
        width: 640px;
        height: 480px;
        margin: 1em;
        background: #ddd;
    }
    .grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, 640px);
        gap: 1em;
    }
    </style>
</head>
<body>
    <h1>WebRTC Demo</h1>
    <div class="grid">
        <div>
            <h2>Local Video</h2>
            <video id="localVideo" autoplay playsinline></video>
        </div>
        <div>
            <h2>Remote Video</h2>
            <video id="remoteVideo" autoplay playsinline></video>
        </div>
    </div>
    <button id="start">Start</button>
    <button id="stop">Stop</button>

    <script src="client.js"></script>
</body>
</html>

client.js

chứa logic phía máy khách cho các kết nối WEBRTC, DOM và logic cho thao tác của người dùng.

// Get DOM elements
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');
const startButton = document.getElementById('start');
const stopButton = document.getElementById('stop');

// WebRTC configuration
const configuration = {
    iceServers: [{
        urls: 'stun:stun.l.google.com:19302'
    }]
};

let pc;
let localStream;

// Start button click handler
startButton.onclick = async () => {
    try {
        // Get local media stream
        localStream = await navigator.mediaDevices.getUserMedia({
            audio: true,
            video: true
        });
        localVideo.srcObject = localStream;

        // Create peer connection
        pc = new RTCPeerConnection(configuration);

        // Add local stream to peer connection
        localStream.getTracks().forEach(track => {
            pc.addTrack(track, localStream);
        });

        // Listen for remote stream
        pc.ontrack = (event) => {
            if (remoteVideo.srcObject !== event.streams[0]) {
                remoteVideo.srcObject = event.streams[0];
            }
        };

        // Create and send offer
        const offer = await pc.createOffer();
        await pc.setLocalDescription(offer);

        const response = await fetch('/offer', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                sdp: pc.localDescription.sdp,
                type: pc.localDescription.type
            })
        });

        const answer = await response.json();
        await pc.setRemoteDescription(answer);

        startButton.disabled = true;
        stopButton.disabled = false;
    } catch (e) {
        console.error('Error starting WebRTC:', e);
    }
};

// Stop button click handler
stopButton.onclick = () => {
    if (pc) {
        pc.close();
        pc = null;
    }
    if (localStream) {
        localStream.getTracks().forEach(track => track.stop());
        localStream = null;
    }
    localVideo.srcObject = null;
    remoteVideo.srcObject = null;
    startButton.disabled = false;
    stopButton.disabled = true;
};

// Initialize
stopButton.disabled = true;
  • startButton.onclick = async () => { ... }: Đây là hàm xử lý chính khi người dùng muốn bắt đầu phiên WebRTC.

    • navigator.mediaDevices.getUserMedia(...): Yêu cầu quyền truy cập camera và microphone của người dùng.
    • Hiển thị luồng video cục bộ lên thẻ localVideo.
    • pc = new RTCPeerConnection(configuration);: Tạo đối tượng RTCPeerConnection.
    • Sau đó thêm các luồng media (audio/video) cục bộ vào pc để gửi đi.
    • pc.ontrack = (event) => { ... }: Lắng nghe và xử lý khi nhận được luồng media từ server (hiển thị lên remoteVideo).
    • Thực hiện Signaling:
      • const offer = await pc.createOffer();: Tạo "offer" SDP.
      • await pc.setLocalDescription(offer);: Đặt "offer" làm LocalDescription.
      • fetch('/offer', { ... }): Gửi "offer" này đến server.
      • Nhận "answer" từ server.
      • await pc.setRemoteDescription(answer);: Đặt "answer" của server làm RemoteDescription, hoàn tất quá trình "bắt tay".
  • stopButton.onclick = () => { ... }: Xử lý khi người dùng muốn dừng phiên WebRTC.

Chạy thử với main.py

Kết quả

image.png

Luồng hoạt động chung như sau:

  1. Người dùng mở index.html.
  2. Người dùng nhấn "Start".
  3. client.js yêu cầu quyền truy cập camera/mic.
  4. Video từ webcam hiển thị trên localVideo.
  5. RTCPeerConnection (pc) được tạo.
  6. Client tạo "offer" và gửi lên server.
  7. Server (main.py) nhận "offer", tạo RTCPeerConnection của riêng mình, xử lý track video nhận được (qua VideoTransformTrack), tạo "answer" và gửi lại cho client.
  8. Client nhận "answer" từ server.
  9. Kết nối WebRTC được thiết lập. Server gửi lại luồng video của client cho chính client đó.
  10. Client hiển thị luồng video nhận được từ server trên remoteVideo.

Thực hành với ứng dụng lớn hơn

Tiếp theo, chúng ta sẽ cùng nhau tạo 1 ứng dụng lớn hơn nha. Ứng dụng của chúng ta sẽ là một phòng chat video đơn giản cho phép nhiều người có thể tham gia.

Mục tiêu ứng dụng

Mục tiêu là xây dựng một ứng dụng web cho phép nhiều người dùng tham gia vào một phiên chat video chung. Cụ thể, ứng dụng sẽ có các tính năng sau:

  • Người dùng có thể nhập tên người dùng và tham gia vào phòng chat video.
  • Sau khi tham gia, người dùng có thể truyền phát video và audio của mình cho những người khác trong phòng và ngược lại, nhận được video/audio từ họ.
  • Người dùng sẽ có các nút điều khiển cơ bản như:
    • Tắt/Bật tiếng
    • Dừng/Phát Video
  • Giao diện sẽ tự động cập nhật để hiển thị video của tất cả những người đang tham gia trong phòng chat.

Kiến trúc dự kiến

aiortc-test/
├── templates/
│   └── index.html 
├── static/
│   ├── css/
│   │   └── style.css 
│   └── js/
│       └── main.js
├── server.py  
└── requirements.txt 

Cài đặt các thư viện sau trong file requirements.txt:

aiortc
fastapi
uvicorn
python-multipart
aiofiles
jinja2

Cách chạy dự án

Đầu tiên là chuẩn bị môi trường phát triển và hiểu cách khởi chạy ứng dụng.

Clone repo sau về máy:

git clone https://github.com/h-oneohone/aiortc-test.git
cd aiortc-test

Tạo và kích hoạt môi trường ảo:

python -m venv venv

Trên Windows:

venv\Scripts\activate

Trên macOS/Linux:

source venv/bin/activate

Cài đặt các thư viện:

pip install -r requirements.txt

Khởi chạy Backend Server

Sau khi đã cài đặt xong các thư viện, chúng ta chạy server.py

python server.py

Sử dụng ứng dụng

Khi backend server đã hoạt động, ta có thể trải nghiệm ứng dụng video chat:

  • Mở trình duyệt web: Chrome hoặc Firefox.

  • Truy cập URL ứng dụng: http://localhost:8080

    • Bạn sẽ thấy màn hình "Join the Session"
    • Nhập tên người dùng mong muốn vào ô nhập liệu.
    • Nhấn nút "Join"
  • Trình duyệt của bạn có thể sẽ yêu cầu quyền truy cập vào camera và microphone. Hãy cấp quyền để tiếp tục.

Sau khi tham gia thành công, video của bạn sẽ xuất hiện, và bạn sẽ ở trong giao diện chính với bảng điều khiển và khu vực hiển thị người tham gia, bạn có thể:

  • Tắt/Bật tiếng audio của bạn bằng nút "Mute".
  • Dừng/Phát video của bạn bằng nút "Stop Video".

Kết luận

Hy vọng sau bài viết này bạn đã hình dung được cách dùng aiortc và cách ứng dụng của nó để tạo ra một ứng dụng giao tiếp thời gian thực. aiortc thực sự là một người bạn đồng hành tuyệt vời, giúp chúng ta dễ dàng hiểu và ứng dụng dụng WebRTC hơn trong môi trường python.

Tuy nhiên mặc dù mạnh mẽ, aiortc và WebRTC nói chung không phải là giải pháp tối ưu cho mọi tình huống streaming, vẫn còn điểm hạn chế, đặt biệt là khi ta cần phát sóng một-nhiều (One-to-Many Broadcast) với quy mô lớn. Lúc này mỗi client sẽ tạo một kết nối riêng đến server, gây quá tải nghiêm trọng cho băng thông và tài nguyên xử lý của server.

Hẹn gặp lại trong bài viết tiếp theo, nơi chúng ta sẽ cùng nhau giải quyết bài toán quy mô lớn này ^^

Reference


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.