基于libdatachannel的本地usb摄像头拉流项目

一、开发环境

硬件:rk3588s、usb摄像头模组两个

系统:unbuntu20.04

库版本:opencv4.9、ffmpeg6.1

编译器版本:c++17,在libdatachannel库中使用非常多的c++17的特性。

二、核心功能

将本地usb摄像头进行拉取编码,再通过webrtc协议进行推流到前端。支持多个摄像头切换,支持摄像头热插拔。

三、文章内容

视频推流服务核心代码分析讲解,libdatachannel库源码讲解、例程讲解,(在写代码的时候发现网上关于libdatachannel的相关资料很少,都很浅,ai也笨笨的,只能自己撸源码了)。前端代码分享。

四、环境配置

整个流程为:图像采集-->视频编码-->webrtc推流。

图像采集部分使用opencv,采集得到cv::mat类,方便图像识别。
视频编码需要ffmpeg库和libx264。

推流部分就是libdatachannel了,这里讲一下技术选型的考量,主要是使用原生的webrtc的库需在谷歌的服务器下载30g左右的文件进行编译,太麻烦了,所以选择了libdatachannle这个库,但是有点小众,资料比较少。开发时有不少坑。

以下是环境配置指令:

#opencv
#相关依赖
sudo apt-get update
sudo apt-get install build-essential cmake git libgtk2.0-dev pkg-config libavcodec-dev libavformat-dev libswscale-dev
sudo apt-get install python3.8-dev python3-numpy libtbb2 libtbb-dev libjpeg-dev libpng-dev libtiff-dev libdc1394-22-dev
#库本身
sudo apt-get install libopencv-dev
#如果想要使用opencv4.9的话自己在官网下载编译即可,在此项目中opencv版本没什么影响



#libx264
git clone https://code.videolan.org/videolan/x264.git
cd x264
./configure --enable-shared
make
sudo make install

#ffmpeg
#官网:https://ffmpeg.org//releases/

#在官网选择合适版本下载到3588,下面以6.1版本为例

~~~
tar -xf ffmpeg-6.1tar.gz
cd ffmpeg-6.1
./configure --enable-gpl --enable-libx264 --enable-libx265
make 
sudo make install


#libdatachannel
#在该链接下有libdatachannle完整的构建方法
https://gitcode.com/gh_mirrors/li/libdatachannel/blob/master/BUILDING.md

git clone https://github.com/paullouisageneau/libdatachannel.git
cd libdatachannel
git submodule update --init --recursive --depth 1

cmake -B build -DUSE_GNUTLS=0 -DUSE_NICE=0 -DCMAKE_BUILD_TYPE=Release

cd build

make -j5

sudo make install

五、例程讲解

在我们以及编译好/libdatachannel/build/examples文件夹下,有如下

这是已经编译好的文件,里面都有相关的可执行文件,简单介绍一下media-sender和streamer两个项目,media-sender的核心功能是通过socket套接字将一个网络摄像头的流使用webrtc转发出去。

streamer这个项目则是拉取本地的音视频文件将其编码然后推出去。

简单理解media-sender只进行网络转发,streamer则是进行转码发送,但是实际上两个项目在建立peerconnection链接的过程也有很大差别。这点后续会有详细的讲解。

首先我们先将media-sender这个项目跑起来:

然后会显示

我们需要将

{"sdp":"v=0\r\no=rtc 671392597 0 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE video\r\na=group:LS video\r\na=msid-semantic:WMS *\r\na=ice-options:ice2,trickle\r\na=fingerprint:sha-256 CE:7A:5D:46:03:36:58:B3:04:D3:80:C0:A6:D9:5D:6B:C1:17:6C:2F:FF:26:CE:7C:1C:78:F2:41:8F:61:2C:69\r\nm=video 33596 UDP/TLS/RTP/SAVPF 96\r\nc=IN IP4 192.168.1.144\r\na=mid:video\r\na=sendonly\r\na=ssrc:42 cname:video-send\r\na=rtcp-mux\r\na=rtpmap:96 H264/90000\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 nack pli\r\na=rtcp-fb:96 goog-remb\r\na=fmtp:96 profile-level-id=42e01f;packetization-mode=1;level-asymmetry-allowed=1\r\na=setup:actpass\r\na=ice-ufrag:LuKQ\r\na=ice-pwd:0XsHGntBKUloQ6uHLGbZ4g\r\na=candidate:1 1 UDP 2122317823 192.168.1.144 33596 typ host\r\na=end-of-candidates\r\n","type":"offer"}

这一段复制到网页上。

在/libdatachannel/examples/media-sender/下有

我们将main.html复制到win环境的电脑下,打开就OK了,网页如下:

将文本复制到框里,点击按钮,会生成对应的“answer”,然后将生成的answer复制回3588的命令行然后回车,就建立起了链接了,这里就不过多赘述了。但是记住这个由我们自己完成的消息传递过程,这就是“信令传递”’,我们帮发送方发送了一个offer,帮接受方回复了一个answer,我们就扮演了信令服务器的角色。

在streamer这个项目中信令服务器就完全由代码实现,在/libdatachannel/examples目录下有:

这四个版本的信令服务器,大家可以根据自己需求去部署,但是一般信令服务器没有太高的性能需求,所以python版本就够用,下载python3,然后再使用pip下载websockets,就可以执行了。

sudo apt install python3

pip install websockets

运行效果如下,注意自己修改ip,默认为127.0.0.0:8000

服务器已经跑起来了,我们需要运行客户端,再开启一个终端,进入libdatachannel/build/examples/streamer/

然后执行

./streamer

这样发送端也就跑起来了,我们在win下打开\libdatachannel\examples\streamer下的

但是记得修改同级目录下的

修改其中ip为自己3588的ip,电脑也需要和3588有网络连接且在同一网段。

成功链接上后,start后变为可点击模式,点击后开始接受视频。

大家正常情况下应该播放的是

我在这为了测试自己原采集的视频编码是否成功,对原视频进行了更换。

这样我们就完整的建立一次webrtc的视频链接,并且成功的进行了视频传输。这个项目与我们的预期差距在于不是实时的摄像头采集,而是本地mp4文件。接下里我们要开始讨论技术细节,以及源码的探究。

首先我们要理解信令的整个运行流程。

六、信令机制

在media-sender项目中,我们自己扮演了信令服务器,交换了offer和answer。

现在我们要去扒在例程中,信息是怎样传输的。

(1)websocket连接建立

无论是视频的接收端还是发送端都会尝试去建立一个websocket链接,这个链接用来去链接到信令服务器。需要注意的是,在进行套接字创建的时候,

webSocket.open("ws://192.168.1.144:8000/server");

都会是ws:://ip:port/only_id,这样的形式。ip和端口号都是信令服务器的端口号,而only_id就是服务器来确定你是谁的位置标识符。

所以在信息格式下就会有如下三栏

{
   id:“目的端id”
   type:“消息种类”
   xxx:“可自设置消息名称,一般为消息内容”
}

在type中一般会有:

request:id交换

offer:发送方的链接申请

answer:接收方的应答回复

candidate:用于建立直接的点对点连接的相关信息。

steamer整体连接逻辑为:

接收方发送一个request请求,信令服务器在转发这个请求时,会把id进行修改,接收方发送请求时,id栏的内容为service,但是信令服务器在转发时会将其修改为发送发本身的id,这样发送方在收到消息时就知道了谁发送了这份请求,并将这个解析出的id用在后续所有回复的id中。

也就是说对于信令服务器来说,他的业务逻辑很简单,解析收到所有消息的id和type,如果type不是request,那么消息原封不动,id是什么,消息就被转发给谁。如果消息是request,那么修改id为发送者的id。

但是其实信令服务器还有更复杂的业务,·因为当前项目为同网段,不需要进行打洞,所以相对简单。

了解清楚了信令交换的机制,那么在代码部分就相对简单:

#include "../include/rtc/rtc.hpp"
....
....


struct AppState {
    atomic<bool> signalReceived{false};
    atomic<bool> encodingDone{false};
    queue<EncodedFrame> frameQueue;
    mutex queueMutex;
    shared_ptr<PeerConnection> pc;
    shared_ptr<Track> videoTrack;
    WebSocket webSocket;
    cv::VideoCapture cap;
    const int targetFps = 30;
    atomic<uint64_t> baseTimestamp{0}; // 时间基准(微秒)
    uint64_t frameDuration_us;    
    mutex pcMutex;
    atomic<int> currentCameraIndex{0}; // 当前摄像头索引
    mutex cameraMutex;
};
AppState g_state;
//创建一个rtc::WebSocket类
using namespace rtc;
WebSocket webSocket;
//设置回调函数,以及连接信令服务器地址
setupWebSocketCallbacks();
webSocket.open("ws://192.168.1.144:8000/server");
//回调函数基本定义
void setupWebSocketCallbacks() {
    g_state.webSocket.onOpen([]() {
        cout << "WebSocket连接成功" << endl;
         auto now = chrono::steady_clock::now();
        g_state.baseTimestamp = chrono::duration_cast<chrono::microseconds>(
            now.time_since_epoch()
        ).count();
    //setupPeerConnection();
    });

    g_state.webSocket.onMessage([](auto data) {
        handleSignalingMessage(data);
    });
}

//消息接收函数定义
void handleSignalingMessage(const variant<binary, string>& data) {

if (!holds_alternative<string>(data)) {
        cerr << "收到非文本信令消息" << endl;
        return;
    }

    try {
        auto rawMsg = get<string>(data);
        auto msg = json::parse(rawMsg);
        cout << "收到原始消息: " << rawMsg << endl;

        // 验证消息格式
        if (!msg.contains("id") || !msg.contains("type")) {
            cerr << "无效消息格式,缺少必要字段" << endl;
            return;
        }

.........
 g_state.pc->onGatheringStateChange([](PeerConnection::GatheringState state) {
                cout << "ICE收集状态: ";
                switch (state) {
                    case PeerConnection::GatheringState::New: cout << "New"; break;
                    case PeerConnection::GatheringState::InProgress: cout << "InProgress"; break;
                    case PeerConnection::GatheringState::Complete: cout << "Complete"; break;
                }
                cout << endl;

                if (state == PeerConnection::GatheringState::Complete) {
                    lock_guard<mutex> lock(g_state.pcMutex);
                    if (!g_state.pc) {
                        cerr << "错误:PeerConnection已释放" << endl;
                        return;
                    }

                    auto description = g_state.pc->localDescription();
                    cout << "本地描述生成成功,类型: " << description->typeString() << endl;
                    cout << "SDP内容:\n" << description.value() << endl;

                    json message = {
                        {"id", "test"},
                        {"type", description->typeString()},
                        {"sdp", string(description.value())}
                    };
                    cout << "准备发送Offer消息" << endl;
                    g_state.webSocket.send(message.dump());
                }
......
}

消息接收函数涉及到具体业务实现根据实际情况编写,但是必要的内容已经展示。

 

七、视频编码

视频编码部分,复用性比较高,对整体做了解耦,封装成了一个功能类,主要是还是使用ffmeg的api:

class FFmpegH264Encoder {
public:
    FFmpegH264Encoder(int width, int height, int fps) 
        : width(width), height(height), fps(fps) {
        initialize();
    }

    ~FFmpegH264Encoder() {
        avcodec_free_context(&codecCtx);
        av_frame_free(&frame);
        sws_freeContext(swsCtx);
    }

    EncodedFrame  encode(const cv::Mat& bgrFrame) {
        EncodedFrame frame_s;

        // 转换颜色空间 BGR -> YUV420P
        const int srcStride = static_cast<int>(bgrFrame.step);
        const uint8_t* srcData[] = { bgrFrame.data };
        int srcLinesize[] = { srcStride }; 
        sws_scale(swsCtx, srcData, srcLinesize, 0, height,
                 frame->data, frame->linesize);

        frame->pts = pts++; 
    
        // 保存当前时间戳
      
        
        // 发送帧到编码器
        if (avcodec_send_frame(codecCtx, frame) < 0) {
            throw std::runtime_error("发送帧到编码器失败");
        }

        // 接收编码包
        AVPacket* pkt = av_packet_alloc();
        while (true) {
            int ret = avcodec_receive_packet(codecCtx, pkt);
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
            if (ret < 0) {
                av_packet_free(&pkt);
                throw std::runtime_error("编码错误");
            }

            frame_s.data.insert(frame_s.data.end(), pkt->data, pkt->data + pkt->size);
          
            av_packet_unref(pkt);
        }
        av_packet_free(&pkt);
        auto now = chrono::steady_clock::now();
        frame_s.timestamp_us = chrono::duration_cast<chrono::microseconds>(
        now.time_since_epoch()
        ).count() - g_state.baseTimestamp;
    
        return frame_s;
    }

private:
    void initialize() {
        const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_H264);
       
        if (!codec) throw std::runtime_error("找不到H.264编码器");

        codecCtx = avcodec_alloc_context3(codec);
        if (!codecCtx) throw std::runtime_error("无法分配编码上下文");

        // 编码参数配置
        codecCtx->bit_rate = width * height * fps; // 根据分辨率调整
        codecCtx->width = width;
        codecCtx->height = height;
        codecCtx->time_base = {1, fps};
        codecCtx->framerate = {fps, 1};
        codecCtx->gop_size = 5; 
        codecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
       
        codecCtx->bit_rate = 5000000;
        codecCtx->max_b_frames = 0; 
          
        AVDictionary* opts = nullptr;
        av_dict_set(&opts, "x264-params", "annexb=0", 0); // 禁用Annex B
        av_dict_set(&opts, "bf", "0", 0);
        av_dict_set(&opts, "profile", "baseline", 0);  
        av_dict_set(&opts, "tune", "zerolatency", 0);
        if (avcodec_open2(codecCtx, codec, &opts) < 0)
           {throw std::runtime_error("无法打开编码器");} 
         av_dict_free(&opts);
        frame = av_frame_alloc();
        frame->format = codecCtx->pix_fmt;
        frame->width = codecCtx->width;
        frame->height = codecCtx->height;
        if (av_frame_get_buffer(frame, 32) < 0)
            throw std::runtime_error("分配帧缓冲失败");
        
        swsCtx = sws_getContext(width, height, AV_PIX_FMT_BGR24,
                              width, height, AV_PIX_FMT_YUV420P,
                              SWS_BILINEAR, nullptr, nullptr, nullptr);
    }
    
    
    int width, height, fps;
    AVCodecContext* codecCtx = nullptr;
    AVFrame* frame = nullptr;
    struct SwsContext* swsCtx = nullptr;
    int64_t pts = 0;
    int fileCounter = 0;  // 文件计数器
    std::mutex fileMutex; // 线程安全锁(如果多线程需要)
};


//使用起来还是比较方便的
//初始化
FFmpegH264Encoder encoder(640, 480,  Fps);
//单帧编码
 encoded = encoder.encode(frame);

这里有两个细节,首先是av_dict_set(&opts, "x264-params", "annexb=0", 0);这涉及到了一些知识。需要详细讲解一下:

首先,我使用webrtc去做视频传输,本质上是rtp传输,rtp的具体细节可以自行查阅,但是有一个比较关键的点,单词的rtp传输内容大小加上头会被限制在1500比特之内,实际上可用传输内容只有1400比特左右,我们编一个一帧H264帧的大小会远远大于这个大小,这个时候就涉及到了分包操作,以及nalus的结构分析了。

首先我们去看libdatachannel库如何进行分包,不断阅读源码我们找到了相关功能的实现,在线阅读项目文件预览 - libdatachannel:C/C++ WebRTC network library featuring Data Channels, Media Transport, and WebSockets - GitCode

或者在下载好的项目阅读都可,具体位于libdatachannel/src/h264rtppacketizer.cpp中,有这么一段:


shared_ptr<H265NalUnits> H265RtpPacketizer::splitMessage(binary_ptr message) {
	auto nalus = std::make_shared<H265NalUnits>();
	if (separator == NalUnit::Separator::Length) {
		size_t index = 0;
		while (index < message->size()) {
			assert(index + 4 < message->size());
			if (index + 4 >= message->size()) {
				LOG_WARNING << "Invalid NAL Unit data (incomplete length), ignoring!";
				break;
			}
			uint32_t length;
			std::memcpy(&length, message->data() + index, sizeof(uint32_t));
			length = ntohl(length);
			auto naluStartIndex = index + 4;
			auto naluEndIndex = naluStartIndex + length;

			assert(naluEndIndex <= message->size());
			if (naluEndIndex > message->size()) {
				LOG_WARNING << "Invalid NAL Unit data (incomplete unit), ignoring!";
				break;
			}
			auto begin = message->begin() + naluStartIndex;
			auto end = message->begin() + naluEndIndex;
			nalus->push_back(std::make_shared<H265NalUnit>(begin, end));
			index = naluEndIndex;
		}
	} else {
		NalUnitStartSequenceMatch match = NUSM_noMatch;
		size_t index = 0;
		while (index < message->size()) {
			match = NalUnit::StartSequenceMatchSucc(match, (*message)[index++], separator);
			if (match == NUSM_longMatch || match == NUSM_shortMatch) {
				match = NUSM_noMatch;
				break;
			}
		}

		size_t naluStartIndex = index;

		while (index < message->size()) {
			match = NalUnit::StartSequenceMatchSucc(match, (*message)[index], separator);
			if (match == NUSM_longMatch || match == NUSM_shortMatch) {
				auto sequenceLength = match == NUSM_longMatch ? 4 : 3;
				size_t naluEndIndex = index - sequenceLength;
				match = NUSM_noMatch;
				auto begin = message->begin() + naluStartIndex;
				auto end = message->begin() + naluEndIndex + 1;
				nalus->push_back(std::make_shared<H265NalUnit>(begin, end));
				naluStartIndex = index + 1;
			}
			index++;
		}
		auto begin = message->begin() + naluStartIndex;
		auto end = message->end();
		nalus->push_back(std::make_shared<H265NalUnit>(begin, end));
	}
	return nalus;
}

就该代码来说,分为两个模式,一个模式为Length,以及StartSequence。对于这两个不同模式,进行了不同的分包模式,在讲解代码之前,我们得先讲讲编码中对应着两种模式在数据格式上的区别。首先讲明:

av_dict_set(&opts, "x264-params", "annexb=1", 0);

auto packetizer = make_shared<H264RtpPacketizer>(
    NalUnit::Separator::StartSequence,  // 使用起始码分隔
    rtpConfig
);

 

av_dict_set(&opts, "x264-params", "annexb=0", 0);

auto packetizer = make_shared<H264RtpPacketizer>(
    NalUnit::Separator::Length,  // 使用起始码分隔
    rtpConfig
);

 是一一对应的,否则前端无法正确解析视频内容,无法播放,现在在详细讲解两者之间的区别。

概括来讲,前缀内容不同。StartSequence的前缀使用0x0000001来分割nalus单元,而Length则是用四字节的内容描述后续的nalus的长度。

对应的分割方式也就不同,具体实现在贴出的splitMessage函数中。

还需要注意编码器的一处设置简单来说就是“0延迟模式”,

av_dict_set(&opts, "tune", "zerolatency", 0);

如果不显示声明使用的话,默认会带来0.5-1s的延迟,这与编码业务所在机器的性能无关,只是默认一种较保守的编码方式,如果不介意1s作用的延迟可以不启用。

八、推流

我在这部分的涉及是两个线程,一个编码线程一个发送线程。编码线程将编码好的数据放置在指定容器中,发送容器周期性的进行读取,给容器加上互斥锁,这样的涉及的初衷是想避免,编码延迟带来的阻塞、延迟。但是在后续的性能测试中,对编码时间进行了打印,3588的性能是足够优秀的。

首先我们需要了解track这个类,在libdatachannle中,该类其实是跟webrtc有较大差距的,加上网络上相关内容比较少,使用的一些ai都在胡说,这里直接去撸了源码,看了一些实现。具体位于/libdatachannel/src/track.cpp中,它又继承了一部分impl::trcak,所以也需要阅读/libdatachannel/src/imp/track.cpp,这样就能完整了解整个track类。

简单来说,track可以当做媒体轨道类,可以接受然后发送数据,在类中有如下函数:

void Track::close() { impl()->close(); }

bool Track::send(message_variant data) { return impl()->outgoing(make_message(std::move(data))); }

bool Track::send(const byte *data, size_t size) { return send(binary(data, data + size)); }

bool Track::isOpen(void) const { return impl()->isOpen(); }

bool Track::isClosed(void) const { return impl()->isClosed(); }

track的状态判断,消息发送以及关闭、开启。开启主要是一种回调触发,只要成功建立连接就会成功触发为open状态,这里主要看send函数能接受的消息内容:

有两种重载,一种为message_variant,一种为byte,byte为c++17标准引入的一个数据类型,暂且不表,message_variant类型经过查找最终找到include/rtc/common.hpp

using binary = std::vector<byte>;
using binary_ptr = shared_ptr<binary>;
using message_variant = variant<binary, string>;

这下我们确定了转发前需要最终生成的数据类型。

这里有一个很坑很坑的点,在按照这个思路进行下去最终确实能完成视频传输,但是会出现一个很恶心的现象,视频会周期性的卡顿。经过不断排查最终发现一个细节,在gitcode中
项目文件预览 - libdatachannel:C/C++ WebRTC network library featuring Data Channels, Media Transport, and WebSockets - GitCode

 他是这样写的:

shared_ptr<Stream> createStream(const string h264Samples, const unsigned fps, const string opusSamples) {
    // video source
    auto video = make_shared<H264FileParser>(h264Samples, fps, true);
    // audio source
    auto audio = make_shared<OPUSFileParser>(opusSamples, true);

    auto stream = make_shared<Stream>(video, audio);
    // set callback responsible for sample sending
    stream->onSample([ws = make_weak_ptr(stream)](Stream::StreamSourceType type, uint64_t sampleTime, rtc::binary sample) {
        vector<ClientTrack> tracks{};
        string streamType = type == Stream::StreamSourceType::Video ? "video" : "audio";
        // get track for given type
        function<optional<shared_ptr<ClientTrackData>> (shared_ptr<Client>)> getTrackData = [type](shared_ptr<Client> client) {
            return type == Stream::StreamSourceType::Video ? client->video : client->audio;
        };
        // get all clients with Ready state
        for(auto id_client: clients) {
            auto id = id_client.first;
            auto client = id_client.second;
            auto optTrackData = getTrackData(client);
            if (client->getState() == Client::State::Ready && optTrackData.has_value()) {
                auto trackData = optTrackData.value();
                tracks.push_back(ClientTrack(id, trackData));
            }
        }
        if (!tracks.empty()) {
            for (auto clientTrack: tracks) {
                auto client = clientTrack.id;
                auto trackData = clientTrack.trackData;
                auto rtpConfig = trackData->sender->rtpConfig;

                // sample time is in us, we need to convert it to seconds
                auto elapsedSeconds = double(sampleTime) / (1000 * 1000);
                // get elapsed time in clock rate
                uint32_t elapsedTimestamp = rtpConfig->secondsToTimestamp(elapsedSeconds);
                // set new timestamp
                rtpConfig->timestamp = rtpConfig->startTimestamp + elapsedTimestamp;

                // get elapsed time in clock rate from last RTCP sender report
                auto reportElapsedTimestamp = rtpConfig->timestamp - trackData->sender->lastReportedTimestamp();
                // check if last report was at least 1 second ago
                if (rtpConfig->timestampToSeconds(reportElapsedTimestamp) > 1) {
                    trackData->sender->setNeedsToReport();
                }

                cout << "Sending " << streamType << " sample with size: " << to_string(sample.size()) << " to " << client << endl;
                try {
                    // send sample
                    trackData->track->send(sample);
                } catch (const std::exception &e) {
                    cerr << "Unable to send "<< streamType << " packet: " << e.what() << endl;
                }
            }
        }
        MainThread.dispatch([ws]() {
            if (clients.empty()) {
                // we have no clients, stop the stream
                if (auto stream = ws.lock()) {
                    stream->stop();
                }
            }
        });
    });
    return stream;
}

这一部分就是在gitcode上关于视频发送部分代码的部分,但是这跟我通过git clone下来的代码是不同的,
 

shared_ptr<Stream> createStream(const string h264Samples, const unsigned fps, const string opusSamples) {
    // video source
    auto video = make_shared<H264FileParser>(h264Samples, fps, true);
    // audio source
    auto audio = make_shared<OPUSFileParser>(opusSamples, true);

    auto stream = make_shared<Stream>(video, audio);
    // set callback responsible for sample sending
    stream->onSample([ws = make_weak_ptr(stream)](Stream::StreamSourceType type, uint64_t sampleTime, rtc::binary sample) {
        vector<ClientTrack> tracks{};
        string streamType = type == Stream::StreamSourceType::Video ? "video" : "audio";
        // get track for given type
        function<optional<shared_ptr<ClientTrackData>> (shared_ptr<Client>)> getTrackData = [type](shared_ptr<Client> client) {
            return type == Stream::StreamSourceType::Video ? client->video : client->audio;
        };
        // get all clients with Ready state
        for(auto id_client: clients) {
            auto id = id_client.first;
            auto client = id_client.second;
            auto optTrackData = getTrackData(client);
            if (client->getState() == Client::State::Ready && optTrackData.has_value()) {
                auto trackData = optTrackData.value();
                tracks.push_back(ClientTrack(id, trackData));
            }
        }
        if (!tracks.empty()) {
            for (auto clientTrack: tracks) {
                auto client = clientTrack.id;
                auto trackData = clientTrack.trackData;

                cout << "Sending " << streamType << " sample with size: " << to_string(sample.size()) << " to " << client << endl;
                try {
                    // send sample
                    trackData->track->sendFrame(sample, std::chrono::duration<double, std::micro>(sampleTime));
                } catch (const std::exception &e) {
                    cerr << "Unable to send "<< streamType << " packet: " << e.what() << endl;
                }
            }
        }
        MainThread.dispatch([ws]() {
            if (clients.empty()) {
                // we have no clients, stop the stream
                if (auto stream = ws.lock()) {
                    stream->stop();
                }
            }
        });
    });
    return stream;
}

差别在于发送函数的使用,一个是send,一个是sendFrame,以及sendFrame第二个参数sampleTime。

先说结果,前端的现象是,视频卡主一段时间,然后快速播放几帧,然后继续卡主,周期性往复。

造成这种现象的原因是时间戳无法对齐。

接下来讲具体技术细节,不放看看例程是如何配sendFrame这个参数的。

首先去看stream的回调函数定义:

void Stream::sendSample() {
    std::lock_guard lock(mutex);
    if (!isRunning) {
        return;
    }
    auto ssSST = unsafePrepareForSample();
    auto ss = ssSST.first;
    auto sst = ssSST.second;
    auto sample = ss->getSample();
    sampleHandler(sst, ss->getSampleTime_us(), sample);
    ss->loadNextSample();
    dispatchQueue.dispatch([this]() {
        this->sendSample();
    });
}

void Stream::onSample(std::function<void (StreamSourceType, uint64_t, rtc::binary)> handler) {
    sampleHandler = handler;
}
------------------------------------------------
sampleHandler(sst, ss->getSampleTime_us(), sample);
//这个SampleTime是通过getSampleTime_us()调取的

然后在streamlibdatachannel\examples\streamer\fileparser.cpp中有详细定义:


#include "fileparser.hpp"
#include <fstream>

using namespace std;

FileParser::FileParser(string directory, string extension, uint32_t samplesPerSecond, bool loop) {
    this->directory = directory;
    this->extension = extension;
    this->loop = loop;
    this->sampleDuration_us = 1000 * 1000 / samplesPerSecond;
}

FileParser::~FileParser() {
	stop();
}

void FileParser::start() {
    sampleTime_us = std::numeric_limits<uint64_t>::max() - sampleDuration_us + 1;
    loadNextSample();
}

void FileParser::stop() {
    sample = {};
    sampleTime_us = 0;
    counter = -1;
}

void FileParser::loadNextSample() {
    string frame_id = to_string(++counter);

    string url = directory + "/sample-" + frame_id + extension;
    ifstream source(url, ios_base::binary);
    if (!source) {
        if (loop && counter > 0) {
            loopTimestampOffset = sampleTime_us;
            counter = -1;
            loadNextSample();
            return;
        }
        sample = {};
        return;
    }

    vector<char> contents((std::istreambuf_iterator<char>(source)), std::istreambuf_iterator<char>());
    auto *b = reinterpret_cast<const std::byte*>(contents.data());
    sample.assign(b, b + contents.size());
    sampleTime_us += sampleDuration_us;
}

rtc::binary FileParser::getSample() {
	return sample;
}

uint64_t FileParser::getSampleTime_us() {
	return sampleTime_us;
}

uint64_t FileParser::getSampleDuration_us() {
	return sampleDuration_us;
}

简单来说,就是当前帧的持续时间设定,下一帧的开始时间,当前帧的开始时间,跟随sendFrame发送走的就是当前帧的开始时间,咱们进行一样的实现就可以实现。

需要注意的是,时间的单位是μs,必须是μs。

sampleDuration_us = 1000 * 1000 / samplesPerSecond;
 

到此为止就完整实现了本地usb摄像头的webrtc推流,opencv采集部分过于简单了,示例如下:

 g_state.cap.open(0, cv::CAP_V4L2);
    if (!g_state.cap.isOpened()) {
        cerr << "无法打开摄像头" << endl;
        return -1;
    }
    g_state.cap.set(cv::CAP_PROP_FOURCC, cv::VideoWriter::fourcc('M','J','P','G'));
    g_state.cap.set(cv::CAP_PROP_FRAME_WIDTH, 640);  // 修复属性设置
    g_state.cap.set(cv::CAP_PROP_FRAME_HEIGHT, 480);
    g_state.cap.set(cv::CAP_PROP_FPS,  g_state.targetFps);
  cv::Mat frame;
g_state.cap.read(frame);

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值