WebRTC:利用 JavaScript 在树莓派/服务器上释放高性能,实现 3G/4G 连接

想象一下这样的场景:你的任务是将 Raspberry Pi 或任何服务器(Linux)转换成 IP 摄像机,但有一个问题——唯一可用的连接是不可靠的 3G/4G 网络。这一挑战促使我深入研究 WebRTC,并在本文中分享我的发现。

起初,我使用 Python 来解决这个问题,这是一种适用于 Raspberry Pi 的流行高级语言。然而,当我尝试利用 aioRTC 库(WebRTC 的 Python 实现)时,我遇到了与 CPU 和内存消耗相关的重大问题。这些障碍使得我的项目在目前的状态下不可行。此外,我还需要一种定制策略来监控传输统计数据并自动调整实时分辨率,这一概念被称为动态自适应视频。即使采用了这种量身定制的方法,在特定场景下的性能仍未达到预期。

WebRTC:利用 JavaScript 在树莓派/服务器上释放高性能,实现 3G/4G 连接
Python/aioRTC VS JavaScript/Chromium

就在这个时候,我开始考虑另一种选择–Chromium 浏览器。JavaScript 提供了一种更简单的实现方式,而 Chromium 浏览器是用 C++ 构建的,并且经过了高度优化,因此是一种很有前途的选择。此外,该浏览器已经集成了 “内容提示 “功能,提高了视频传输效率和对网络条件的响应速度。在本文中,我们将探讨 WebRTC 和 JavaScript 如何结合成为此类情况下的理想解决方案,在这种情况下,稳定性和高效视频传输是最重要的。

通过浏览器 API 利用 WebRTC

要动态调整视频质量,您有两个选择,contenthint提供两个选项 :detailmotion。如果您的视频包含motion,选择motion将锁定每秒帧数 (FPS) 并降低图像质量。然而,选择detail,尤其是在有文本的情况下,可以在保持高分辨率的同时调整 FPS,以适应较差的网络连接。

在本示例中,我排除了Signaling Service,因为它会随 WebSocket、gRPC、HTTP 等解决方案的不同而变化。

1. 使用 JavaScript 在树莓派/服务器上实现

使用 JavaScript 的实现非常简单。请记住,您可以通过修改约束条件来选择要共享的设备,例如:video: {deviceId: {exact: your-device-id}}。与 linux 上传统的 /dev/video0 ID 不同,浏览器上的设备 ID 结构不同。使用 showDevices() 方法查找要共享的特定摄像头,尤其是在连接了多个摄像头的情况下。

var peer = null;

function createPeer (offerSDP) {
    let config = {
        iceServers: [
            { urls: 'stun:stun.l.google.com:19302' },
            { urls: 'stun:stun2.l.google.com:19302' }
        ]
    };
    
    peer = new RTCPeerConnection(config);
    
    captureCamera(offerSDP);
}

async function showDevices() {    
    let devices = (await navigator.mediaDevices.enumerateDevices()).filter(i=> i.kind == 'videoinput')
    console.log(devices)
}

function captureCamera (sdpOffer) {
    let constraints = {
        audio: false,
        /*
        video: {            
            deviceId: { exact: '4e9606ec398f795755efea4f4b75f206f5b5140f5a9ee8ee51e81154c220f97a' }
        }                
        */
       video: true
    }; 

    navigator.mediaDevices.getUserMedia(constraints).then(function(stream) {
        stream.getTracks().forEach(function(track) {

            applyContraints(track);

            peer.addTrack(track, stream);            
        });
        return createAnswer(sdpOffer);
    }, function(err) {
        alert('Could not acquire media: ' + err);
    });
}

async function createAnswer (sdp) {
    let offer = new RTCSessionDescription({sdp: sdp, type: 'offer'})
    await peer.setRemoteDescription(offer)
    let answer = await peer.createAnswer()

    await peer.setLocalDescription(answer)

    sendAnswerToBrowser(peer.localDescription.sdp)
}

function applyContraints (videoTrack) {
    if (videoTrack) {
    
        const videoConstraints = {
            width: { min: 320, max: 1280 },
            height: { min: 240,  max: 720 },
            frameRate: {min: 15,  max: 30 }
        };
    
        // Apply video track constraints
        videoTrack.applyConstraints(videoConstraints)
            .then(() => {
                console.log("Video track constraints applied successfully");
            })
            .catch((error) => {
                console.error("Error applying video track constraints:", error);
                setTimeout(() => {
                    applyContraints();
                }, 5000);//5seg
            });
    
        // Set content hint to 'motion' or 'detail'
        videoTrack.contentHint = 'motion';
    }
}

function sendAnswerToBrowser(answerSDP) {
    console.log('sendAnswerToBrowser')
    /**
    * implement this method with your strategy to signaling
    */
}

如果 Raspberry Pi 没有 X (UI) 服务,请确保已安装 screen 和 xvfb 软件包,以便无缝运行。使用以下命令安装它们:

$ sudo apt-get update
$ sudo apt-get install screen
$ sudo apt-get install xvfb

安装好这些软件包后,就可以通过终端在隔离会话中打开浏览器了。

$ screen -S web xvfb-run chromiu-browser --use-fake-ui-for-media-stream http://localhost:8289/

#without xvfb-run

$ screen -S web chromiu-browser --use-fake-ui-for-media-stream http://localhost:8289/

使用–use-fake-ui-for-media-stream 命令,浏览器就无需请求摄像头权限。

2 在浏览器端(前端)实施 WebRTC

场景: 前端只消费视频,不向服务器发送视频或音频。前端的 JavaScript 代码如下:

var peer = null;

const localVideoTag = document.getElementById('localVideo')


function createPeer () {
    console.log("creating a peer connection")

    let config = {
        iceServers: [
            { urls: 'stun:stun.l.google.com:19302' },
            { urls: 'stun:stun2.l.google.com:19302' }
        ]
    };
      
    peer = new RTCPeerConnection(config);  

    peer.addEventListener('track', function (evt) {
        if (evt.track.kind == 'video') {
            console.log("receiving a video")

            applyContraints( evt.streams[0].getVideoTracks()[0] );

            localVideoTag.srcObject = evt.streams[0];
        }
    });    

    peer.addTransceiver('audio', {
        direction: 'recvonly'
    });
    peer.addTransceiver('video', {
        direction: 'recvonly'
    });
}

async function negociate () {
    console.log("negociate")

    let offer = await peer.createOffer();

    await peer.setLocalDescription(offer);

    await verifyStateComplete();

    sendOfferToServer(peer.localDescription.sdp);
}

async function verifyStateComplete () {
    console.log("verifyStateComplete")

    return new Promise(function (resolve) {
        if (peer.iceGatheringState === 'complete') {
            resolve();
        } else {
            function checkState() {
                if (peer.iceGatheringState === 'complete') {
                    peer.removeEventListener('icegatheringstatechange', checkState);
                    resolve();
                }
            }
            peer.addEventListener('icegatheringstatechange', checkState);
        }
    });
}

function applyContraints (videoTrack) {
    if (videoTrack) {
    
        const videoConstraints = {
            width: { min: 320, max: 1280 },
            height: { min: 240,  max: 720 },
            frameRate: {min: 15,  max: 30 }
        };
    
        // Apply video track constraints
        videoTrack.applyConstraints(videoConstraints)
            .then(() => {
                console.log("Video track constraints applied successfully");
            })
            .catch((error) => {
                console.error("Error applying video track constraints:", error);
                setTimeout(() => {
                    applyContraints(videoTrack);
                }, 5000);//5seg
            });
    
        // Set content hint to 'motion' or 'detail'
        videoTrack.contentHint = 'motion';
    }
}

function sendOfferToServer (offerSDP) {
    console.log('sendOfferToServer')    
    /**
    * implement this method with your strategy to signaling
    */
}

/**
* <This method will be called by the signaling solution you choose....>
*/
function setAnswer (sdp) {
    console.log('setAnswer')
    let answer = {
        sdp: sdp,
        type: 'answer'
    }
    peer.setRemoteDescription(answer);
}

//start 
function start () {
    console.log('start')
    createPeer();
    negociate();
}

function stop(){
    peer.close();
}

结论

使用 chrome://webrtc-internals/ 可以轻松分析 WebRTC 流量。

WebRTC:利用 JavaScript 在树莓派/服务器上释放高性能,实现 3G/4G 连接

在观察到的指标中,您可能会注意到帧高和帧宽的变化。最初的分辨率为 640×480,但也有波动。它还旨在保持 FPS,这是通过将 “内容提示 “选择为 “运动 “来实现的。这一选择在动态调整分辨率的同时确保了 FPS 的稳定性。

难能可贵的是,你无需创建复杂的自动视频适应逻辑,浏览器开箱即提供了这一功能。它采用了一种智能算法,可分析网络指标并优化视频,以实现卓越的用户体验。

要模拟糟糕的网络条件,可以使用下面的方法故意将比特率降低到 50kbps:

/**
 * Call this method when you want to reduce the speed to
 * test content hint, simulating situations when you have bad connections
 */
function forceKbps(sdp, speed) {
    return sdp.replace(/a=mid:(.*)\r\n/g, 'a=mid:$1\r\nb=AS:' + speed + '\r\n');
}

总之,WebRTC 与浏览器中的 JavaScript 相结合,可以在要求稳定和高效视频传输的场景中改变游戏规则。浏览器的内置优化和 contentHint 功能提供了一个功能强大、用户友好的解决方案。

源代码

我使用 WebSocket 信令服务创建了一个全面的 WebRTC 项目,欢迎大家对这个项目提出宝贵意见。

https://github.com/marcus2vinicius/webrtc-raspberry-js

作者:Marcus Borges

本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/webrtc/35573.html

(0)

相关推荐

发表回复

登录后才能评论