一、开发环境
硬件: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);