Autonomy Software C++ 24.5.1
Welcome to the Autonomy Software repository of the Mars Rover Design Team (MRDT) at Missouri University of Science and Technology (Missouri S&T)! API reference contains the source code and other resources for the development of the autonomy software for our Mars rover. The Autonomy Software project aims to compete in the University Rover Challenge (URC) by demonstrating advanced autonomous capabilities and robust navigation algorithms.
Loading...
Searching...
No Matches
WebRTC Class Reference

This class is used to establish a connection with the RoveSoSimulator and retrieve video streams from it. More...

#include <WebRTC.h>

Collaboration diagram for WebRTC:

Public Member Functions

 WebRTC (const std::string &szSignallingServerURL, const std::string &szStreamerID)
 Construct a new Web RTC::WebRTC object.
 
 ~WebRTC ()
 Destroy the Web RTC::WebRTC object.
 
void SetOnFrameReceivedCallback (std::function< void(cv::Mat &)> fnOnFrameReceivedCallback, const AVPixelFormat eOutputPixelFormat=AV_PIX_FMT_BGR24)
 Set the callback function for when a new frame is received.
 
bool GetIsConnected () const
 Get the connection status of the WebRTC object.
 

Private Member Functions

bool ConnectToSignallingServer (const std::string &szSignallingServerURL)
 Connected to the Unreal Engine 5 hosted Signalling Server for WebRTC negotiation.
 
bool InitializeH264Decoder ()
 Initialize the H264 decoder. Creates the AVCodecContext, AVFrame, and AVPacket.
 
bool DecodeH264BytesToCVMat (const std::vector< uint8_t > &vH264EncodedBytes, cv::Mat &cvDecodedFrame, const AVPixelFormat eOutputPixelFormat)
 Decodes H264 encoded bytes to a cv::Mat using FFmpeg.
 
bool RequestKeyFrame ()
 Requests a key frame from the given video track. This is useful for when the video track is out of sync or has lost frames.
 
bool SendCommandToStreamer (const std::string &szCommand)
 This method sends a command to the streamer via the data channel. The command is a JSON string that is sent as a binary message. The PixelStreaming plugin handles the command very weirdly, so we have to sort of encode the command in a specific way. This handles that encoding.
 

Private Attributes

std::string m_szSignallingServerURL
 
std::string m_szStreamerID
 
std::shared_ptr< rtc::WebSocket > m_pWebSocket
 
std::shared_ptr< rtc::PeerConnection > m_pPeerConnection
 
std::shared_ptr< rtc::DataChannel > m_pDataChannel
 
std::shared_ptr< rtc::Track > m_pVideoTrack1
 
std::shared_ptr< rtc::H264RtpDepacketizer > m_pTrack1H264DepacketizationHandler
 
std::shared_ptr< rtc::RtcpReceivingSession > m_pTrack1RtcpReceivingSession
 
std::chrono::system_clock::time_point m_tmLastKeyFrameRequestTime
 
AVCodecContext * m_pAVCodecContext
 
AVFrame * m_pFrame
 
AVPacket * m_pPacket
 
SwsContext * m_pSWSContext
 
AVPixelFormat m_eOutputPixelFormat
 
std::shared_mutex m_muDecoderMutex
 
cv::Mat m_cvFrame
 
std::function< void(cv::Mat &)> m_fnOnFrameReceivedCallback
 

Detailed Description

This class is used to establish a connection with the RoveSoSimulator and retrieve video streams from it.

Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2024-11-30

Constructor & Destructor Documentation

◆ WebRTC()

WebRTC::WebRTC ( const std::string &  szSignallingServerURL,
const std::string &  szStreamerID 
)

Construct a new Web RTC::WebRTC object.

Parameters
szSignallingServerURL- The URL of the signalling server.
szStreamerID- The ID of the streamer.
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2024-12-02
29{
30 // Set member variables.
31 m_szSignallingServerURL = szSignallingServerURL;
32 m_szStreamerID = szStreamerID;
33 m_tmLastKeyFrameRequestTime = std::chrono::system_clock::now();
34
35 // Setup the FFMPEG H264 decoder.
37
38 // Enable logging from the WebRTC LibDataChannel library for debugging.
39 // rtc::InitLogger(rtc::LogLevel::Verbose);
40
41 // Construct the WebRTC peer connection and data channel for receiving data from the simulator.
42 rtc::WebSocket::Configuration rtcWebSocketConfig;
43 rtc::Configuration rtcPeerConnectionConfig;
44 rtcPeerConnectionConfig.forceMediaTransport = true;
45 rtcPeerConnectionConfig.maxMessageSize = 100000000;
46 m_pWebSocket = std::make_shared<rtc::WebSocket>(rtcWebSocketConfig);
47 m_pPeerConnection = std::make_shared<rtc::PeerConnection>(rtcPeerConnectionConfig);
48 m_pDataChannel = m_pPeerConnection->createDataChannel("webrtc-datachannel");
49
50 // Attempt to connect to the signalling server.
51 this->ConnectToSignallingServer(szSignallingServerURL);
52}
bool InitializeH264Decoder()
Initialize the H264 decoder. Creates the AVCodecContext, AVFrame, and AVPacket.
Definition WebRTC.cpp:563
bool ConnectToSignallingServer(const std::string &szSignallingServerURL)
Connected to the Unreal Engine 5 hosted Signalling Server for WebRTC negotiation.
Definition WebRTC.cpp:167
Here is the call graph for this function:

◆ ~WebRTC()

WebRTC::~WebRTC ( )

Destroy the Web RTC::WebRTC object.

Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2024-12-02
62{
63 // Check if the smart pointers are valid before calling any methods.
64 if (m_pVideoTrack1)
65 {
66 m_pVideoTrack1->close();
67 }
68 if (m_pPeerConnection)
69 {
70 m_pPeerConnection->close();
71 }
72 if (m_pDataChannel)
73 {
74 m_pDataChannel->close();
75 }
76 if (m_pWebSocket)
77 {
78 m_pWebSocket->close();
79 }
80
81 // Wait for all connections to close.
82 while ((m_pVideoTrack1 && !m_pVideoTrack1->isClosed()) || (m_pDataChannel && !m_pDataChannel->isClosed()) || (m_pWebSocket && !m_pWebSocket->isClosed()))
83 {
84 std::this_thread::sleep_for(std::chrono::milliseconds(100));
85 }
86
87 // Manually destroy the smart pointers.
88 m_pVideoTrack1.reset();
89 m_pPeerConnection.reset();
90 m_pDataChannel.reset();
91 m_pWebSocket.reset();
92
93 // Free the codec context.
94 if (m_pSWSContext)
95 {
96 sws_freeContext(m_pSWSContext);
97 }
98 if (m_pFrame)
99 {
100 av_frame_free(&m_pFrame);
101 }
102 if (m_pPacket)
103 {
104 av_packet_free(&m_pPacket);
105 }
106 if (m_pAVCodecContext)
107 {
108 avcodec_free_context(&m_pAVCodecContext);
109 }
110
111 // Set dangling pointers to nullptr.
112 m_pAVCodecContext = nullptr;
113 m_pFrame = nullptr;
114 m_pPacket = nullptr;
115 m_pSWSContext = nullptr;
116}

Member Function Documentation

◆ SetOnFrameReceivedCallback()

void WebRTC::SetOnFrameReceivedCallback ( std::function< void(cv::Mat &)>  fnOnFrameReceivedCallback,
const AVPixelFormat  eOutputPixelFormat = AV_PIX_FMT_BGR24 
)

Set the callback function for when a new frame is received.

Parameters
fnOnFrameReceivedCallback- The callback function to set.
eOutputPixelFormat- The output pixel format to use.
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2024-12-02
128{
129 // Set the callback function.
130 m_fnOnFrameReceivedCallback = fnOnFrameReceivedCallback;
131 // Set the output pixel format.
132 m_eOutputPixelFormat = eOutputPixelFormat;
133}

◆ GetIsConnected()

bool WebRTC::GetIsConnected ( ) const

Get the connection status of the WebRTC object.

Returns
true - The WebRTC object is connected.
false - The WebRTC object is not connected.
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2024-12-26
145{
146 // Check if the datachannel shared pointer is valid.
147 if (m_pWebSocket != nullptr)
148 {
149 // Check if the datachannel is open.
150 return m_pWebSocket->isOpen();
151 }
152
153 // Return false if the datachannel is not valid.
154 return false;
155}
Here is the caller graph for this function:

◆ ConnectToSignallingServer()

bool WebRTC::ConnectToSignallingServer ( const std::string &  szSignallingServerURL)
private

Connected to the Unreal Engine 5 hosted Signalling Server for WebRTC negotiation.

Parameters
szSignallingServerURL- The full URL of the signalling server. Should be in the format of "ws://<IP>:<PORT>"
Returns
true - Successfully connected to the signalling server.
false - Failed to connect to the signalling server.
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2024-11-11
168{
169 // Connect to the signalling server via a websocket to handle WebRTC negotiation and signalling.
170 m_pWebSocket->open(szSignallingServerURL);
171
173 // Set some callbacks on important events for the websocket.
175
176 // WebSocket has been opened.
177 m_pWebSocket->onOpen(
178 [this]()
179 {
180 // Submit logger message.
181 LOG_INFO(logging::g_qSharedLogger, "Connected to the signalling server via {}. Checking if stream {} exists...", m_szSignallingServerURL, m_szStreamerID);
182
183 // Request the streamer list from the server. This also kicks off the negotiation process.
184 nlohmann::json jsnStreamList;
185 jsnStreamList["type"] = "listStreamers";
186 m_pWebSocket->send(jsnStreamList.dump());
187 });
188
189 // WebSocket has been closed.
190 m_pWebSocket->onClosed(
191 [this]()
192 {
193 // Submit logger message.
194 LOG_INFO(logging::g_qSharedLogger, "Closed {} stream and disconnected from the signalling server.", m_szStreamerID);
195 });
196
197 // Handling signalling server messages. (offer/answer/ICE candidate)
198 m_pWebSocket->onMessage(
199 [this](std::variant<rtc::binary, rtc::string> rtcMessage)
200 {
201 try
202 {
203 // Create instance variables.
204 nlohmann::json jsnMessage;
205
206 // Check if data is of type rtc::string.
207 if (std::holds_alternative<rtc::string>(rtcMessage))
208 {
209 // Retrieve the string message
210 std::string szMessage = std::get<rtc::string>(rtcMessage);
211
212 // Parse the JSON message from the signaling server.
213 jsnMessage = nlohmann::json::parse(szMessage);
214 LOG_DEBUG(logging::g_qSharedLogger, "Received message from signalling server: {}", szMessage);
215 }
216 else if (std::holds_alternative<rtc::binary>(rtcMessage))
217 {
218 // Retrieve the binary message.
219 rtc::binary rtcBinaryData = std::get<rtc::binary>(rtcMessage);
220 // Print length of binary data.
221 LOG_DEBUG(logging::g_qSharedLogger, "Received binary data of length: {}", rtcBinaryData.size());
222
223 // Convert the binary data to a string.
224 std::string szBinaryDataStr(reinterpret_cast<const char*>(rtcBinaryData.data()), rtcBinaryData.size());
225 // Print the binary data as a string.
226 LOG_DEBUG(logging::g_qSharedLogger, "Received binary data: {}", szBinaryDataStr);
227 // Parse the binary data as JSON.
228 jsnMessage = nlohmann::json::parse(szBinaryDataStr);
229 }
230 else
231 {
232 LOG_ERROR(logging::g_qSharedLogger, "Received unknown message type from signalling server");
233 }
234
235 // Check if the message contains a type.
236 if (jsnMessage.contains("type"))
237 {
238 std::string szType = jsnMessage["type"];
239 // If the message from the server is a config message, do nothing.
240 if (szType == "config")
241 {
242 // Submit logger message.
243 LOG_DEBUG(logging::g_qSharedLogger, "Received config message from signalling server: {}", jsnMessage.dump());
244 }
245 // If the message from the server is an offer, set the remote description offer.
246 else if (szType == "offer")
247 {
248 // Get the SDP offer and set it as the remote description.
249 std::string sdp = jsnMessage["sdp"];
250 m_pPeerConnection->setRemoteDescription(rtc::Description(sdp, "offer"));
251 LOG_DEBUG(logging::g_qSharedLogger, "Processing SDP offer from signalling server: {}", sdp);
252 }
253 // If the message from the server is an answer, set the remote description answer.
254 else if (szType == "answer")
255 {
256 // Get the SDP answer and set it as the remote description.
257 std::string sdp = jsnMessage["sdp"];
258 m_pPeerConnection->setRemoteDescription(rtc::Description(sdp, "answer"));
259 LOG_DEBUG(logging::g_qSharedLogger, "Processing SDP answer from signalling server: {}", sdp);
260 }
261 // If the message from the server is advertising an ICE candidate, add it to the peer connection.
262 else if (szType == "iceCandidate")
263 {
264 // Handle ICE candidate
265 nlohmann::json jsnCandidate = jsnMessage["candidate"];
266 std::string szCandidateStr = jsnCandidate["candidate"];
267
268 rtc::Candidate rtcCandidate = rtc::Candidate(szCandidateStr);
269 m_pPeerConnection->addRemoteCandidate(rtcCandidate);
270 LOG_DEBUG(logging::g_qSharedLogger, "Added ICE candidate to peer connection: {}", szCandidateStr);
271 }
272 else if (szType == "streamerList")
273 {
274 // Print the streamer list.
275 LOG_DEBUG(logging::g_qSharedLogger, "Streamer List: {}", jsnMessage.dump());
276
277 // Check that the streamer ID given by the user is in the streamer list.
278 if (jsnMessage.contains("ids"))
279 {
280 std::vector<std::string> streamerList = jsnMessage["ids"].get<std::vector<std::string>>();
281 if (std::find(streamerList.begin(), streamerList.end(), m_szStreamerID) != streamerList.end())
282 {
283 // Send what stream we want to the server.
284 nlohmann::json jsnStream;
285 jsnStream["type"] = "subscribe";
286 jsnStream["streamerId"] = m_szStreamerID;
287 m_pWebSocket->send(jsnStream.dump());
288 // Submit logger message.
289 LOG_DEBUG(logging::g_qSharedLogger, "Streamer ID {} found in the streamer list. Subscribing to stream...", m_szStreamerID);
290 }
291 else
292 {
293 LOG_ERROR(logging::g_qSharedLogger, "Streamer ID {} not found in the streamer list!", m_szStreamerID);
294 }
295 }
296 else
297 {
298 LOG_ERROR(logging::g_qSharedLogger, "Streamer list does not contain 'ids' field!");
299 }
300 }
301 else
302 {
303 LOG_ERROR(logging::g_qSharedLogger, "Unknown message type received from signalling server: {}", szType);
304 }
305 }
306 }
307 catch (const std::exception& e)
308 {
309 // Submit logger message.
310 LOG_ERROR(logging::g_qSharedLogger, "Error occurred while negotiating with the Signalling Server: {}", e.what());
311 }
312 });
313
314 m_pWebSocket->onError(
315 [this](const std::string& szError)
316 {
317 // Submit logger message.
318 LOG_ERROR(logging::g_qSharedLogger, "Error occurred on WebSocket: {}", szError);
319 });
320
322 // Set some callbacks on important events for the peer connection.
324
325 m_pPeerConnection->onLocalDescription(
326 [this](rtc::Description rtcDescription)
327 {
328 // Check the type of the description.
329 if (rtcDescription.typeString() == "offer")
330 {
331 return;
332 }
333
334 // First lets send some preconfig stuff.
335 nlohmann::json jsnConfigMessage;
336 jsnConfigMessage["type"] = "layerPreference";
337 jsnConfigMessage["spatialLayer"] = 0;
338 jsnConfigMessage["temporalLayer"] = 0;
339 jsnConfigMessage["playerId"] = "";
340 m_pWebSocket->send(jsnConfigMessage.dump());
341
342 // Send the local description to the signalling server
343 nlohmann::json jsnMessage;
344 jsnMessage["type"] = rtcDescription.typeString();
345 // This next bit is specific to the Unreal Engine 5 Signalling Server, we must append the min/max bitrate to the message.
346 jsnMessage["minBitrateBps"] = 0;
347 jsnMessage["maxBitrateBps"] = 0;
348 // Here's our actual SDP.
349 std::string szSDP = rtcDescription.generateSdp();
350 // Munger the SDP to add the bitrate.
351 std::string szMungedSDP =
352 std::regex_replace(szSDP, std::regex("(a=fmtp:\\d+ level-asymmetry-allowed=.*)\r\n"), "$1;x-google-start-bitrate=10000;x-google-max-bitrate=100000\r\n");
353 jsnMessage["sdp"] = szMungedSDP;
354 // Send the message.
355 m_pWebSocket->send(jsnMessage.dump());
356
357 // Submit logger message.
358 LOG_DEBUG(logging::g_qSharedLogger, "Sending local description to signalling server: {}", jsnMessage.dump());
359 });
360
361 m_pPeerConnection->onTrack(
362 [this](std::shared_ptr<rtc::Track> rtcTrack)
363 {
364 // Submit logger message.
365 rtc::Description::Media rtcMediaDescription = rtcTrack->description();
366 // Get some information about the track.
367 std::string szMediaType = rtcMediaDescription.type();
368
369 // Check if the track is a video track.
370 if (szMediaType != "video")
371 {
372 return;
373 }
374
375 // Set member variable to the video track.
376 m_pVideoTrack1 = rtcTrack;
377
378 // Create a H264 depacketization handler and rtcp receiving session.
379 m_pTrack1H264DepacketizationHandler = std::make_shared<rtc::H264RtpDepacketizer>(rtc::NalUnit::Separator::LongStartSequence);
380 m_pTrack1RtcpReceivingSession = std::make_shared<rtc::RtcpReceivingSession>();
381 m_pTrack1H264DepacketizationHandler->addToChain(m_pTrack1RtcpReceivingSession);
382 m_pVideoTrack1->setMediaHandler(m_pTrack1H264DepacketizationHandler);
383
384 // Set the onMessage callback for the video track.
385 m_pVideoTrack1->onFrame(
386 [this](rtc::binary rtcBinaryMessage, rtc::FrameInfo rtcFrameInfo)
387 {
388 // Assuming 96 is the H264 payload type.
389 if (rtcFrameInfo.payloadType == 96)
390 {
391 // Change the rtc::Binary (std::vector<std::byte>) to a std::vector<uint8_t>.
392 std::vector<uint8_t> vH264EncodedBytes;
393 vH264EncodedBytes.reserve(rtcBinaryMessage.size());
394 for (std::byte stdByte : rtcBinaryMessage)
395 {
396 vH264EncodedBytes.push_back(static_cast<uint8_t>(stdByte));
397 }
398
399 // Acquire a mutex lock on the shared_mutex before calling sws_scale.
400 std::unique_lock<std::shared_mutex> lkDecoderLock(m_muDecoderMutex);
401 // Pass to FFmpeg decoder
402 this->DecodeH264BytesToCVMat(vH264EncodedBytes, m_cvFrame, m_eOutputPixelFormat);
403
404 // Check if the callback function is set before calling it.
405 if (m_fnOnFrameReceivedCallback)
406 {
407 // Call the user's callback function.
408 m_fnOnFrameReceivedCallback(m_cvFrame);
409 }
410 // Release the lock on the shared_mutex.
411 lkDecoderLock.unlock();
412 }
413 });
414 });
415
416 m_pPeerConnection->onGatheringStateChange(
417 [this](rtc::PeerConnection::GatheringState eGatheringState)
418 {
419 // Switch to translate the state to a string.
420 switch (eGatheringState)
421 {
422 case rtc::PeerConnection::GatheringState::Complete: LOG_DEBUG(logging::g_qSharedLogger, "PeerConnection ICE gathering state changed to: Complete"); break;
423
424 case rtc::PeerConnection::GatheringState::InProgress:
425 LOG_DEBUG(logging::g_qSharedLogger, "PeerConnection ICE gathering state changed to: InProgress");
426 break;
427 case rtc::PeerConnection::GatheringState::New: LOG_DEBUG(logging::g_qSharedLogger, "PeerConnection ICE gathering state changed to: New"); break;
428 default: LOG_DEBUG(logging::g_qSharedLogger, "Peer connection ICE gathering state changed to: Unknown"); break;
429 }
430 });
431
432 m_pPeerConnection->onIceStateChange(
433 [this](rtc::PeerConnection::IceState eIceState)
434 {
435 // Switch to translate the state to a string.
436 switch (eIceState)
437 {
438 case rtc::PeerConnection::IceState::Checking: LOG_DEBUG(logging::g_qSharedLogger, "PeerConnection ICE state changed to: Checking"); break;
439 case rtc::PeerConnection::IceState::Closed: LOG_DEBUG(logging::g_qSharedLogger, "PeerConnection ICE state changed to: Closed"); break;
440 case rtc::PeerConnection::IceState::Completed: LOG_DEBUG(logging::g_qSharedLogger, "PeerConnection ICE state changed to: Completed"); break;
441 case rtc::PeerConnection::IceState::Connected: LOG_DEBUG(logging::g_qSharedLogger, "PeerConnection ICE state changed to: Connected"); break;
442 case rtc::PeerConnection::IceState::Disconnected: LOG_DEBUG(logging::g_qSharedLogger, "PeerConnection ICE state changed to: Disconnected"); break;
443 case rtc::PeerConnection::IceState::Failed: LOG_DEBUG(logging::g_qSharedLogger, "PeerConnection ICE state changed to: Failed"); break;
444 case rtc::PeerConnection::IceState::New: LOG_DEBUG(logging::g_qSharedLogger, "PeerConnection ICE state changed to: New"); break;
445 default: LOG_DEBUG(logging::g_qSharedLogger, "Peer connection ICE state changed to: Unknown"); break;
446 }
447 });
448 m_pPeerConnection->onSignalingStateChange(
449 [this](rtc::PeerConnection::SignalingState eSignalingState)
450 {
451 // Switch to translate the state to a string.
452 switch (eSignalingState)
453 {
454 case rtc::PeerConnection::SignalingState::HaveLocalOffer:
455 {
456 LOG_DEBUG(logging::g_qSharedLogger, "PeerConnection signaling state changed to: HaveLocalOffer");
457 break;
458 }
459 case rtc::PeerConnection::SignalingState::HaveLocalPranswer:
460 LOG_DEBUG(logging::g_qSharedLogger, "PeerConnection signaling state changed to: HaveLocalPranswer");
461 break;
462 case rtc::PeerConnection::SignalingState::HaveRemoteOffer:
463 LOG_DEBUG(logging::g_qSharedLogger, "PeerConnection signaling state changed to: HaveRemoteOffer");
464 break;
465 case rtc::PeerConnection::SignalingState::HaveRemotePranswer:
466 LOG_DEBUG(logging::g_qSharedLogger, "PeerConnection signaling state changed to: HaveRemotePrAnswer");
467 break;
468 case rtc::PeerConnection::SignalingState::Stable: LOG_DEBUG(logging::g_qSharedLogger, "PeerConnection signaling state changed to: Stable"); break;
469 default: LOG_DEBUG(logging::g_qSharedLogger, "Peer connection signaling state changed to: Unknown"); break;
470 }
471 });
472
473 m_pPeerConnection->onStateChange(
474 [this](rtc::PeerConnection::State eState)
475 {
476 // Switch to translate the state to a string.
477 switch (eState)
478 {
479 case rtc::PeerConnection::State::Closed: LOG_DEBUG(logging::g_qSharedLogger, "Peer connection state changed to: Closed"); break;
480 case rtc::PeerConnection::State::Connected: LOG_DEBUG(logging::g_qSharedLogger, "Peer connection state changed to: Connected"); break;
481 case rtc::PeerConnection::State::Connecting: LOG_DEBUG(logging::g_qSharedLogger, "Peer connection state changed to: Connecting"); break;
482 case rtc::PeerConnection::State::Disconnected: LOG_DEBUG(logging::g_qSharedLogger, "Peer connection state changed to: Disconnected"); break;
483 case rtc::PeerConnection::State::Failed: LOG_DEBUG(logging::g_qSharedLogger, "Peer connection state changed to: Failed"); break;
484 case rtc::PeerConnection::State::New: LOG_DEBUG(logging::g_qSharedLogger, "Peer connection state changed to: New"); break;
485 default: LOG_DEBUG(logging::g_qSharedLogger, "Peer connection state changed to: Unknown"); break;
486 }
487 });
488
490 // Set some callbacks on important events for the data channel.
492
493 m_pDataChannel->onOpen(
494 [this]()
495 {
496 // Submit logger message.
497 LOG_INFO(logging::g_qSharedLogger, "Data channel opened.");
498
499 // Request quality control of the stream.
500 m_pDataChannel->send(std::string(1, static_cast<char>(1)));
501 });
502
503 m_pDataChannel->onMessage(
504 [this](std::variant<rtc::binary, rtc::string> rtcMessage)
505 {
506 try
507 {
508 // Create instance variables.
509 nlohmann::json jsnMessage;
510
511 // Check if data is of type rtc::string.
512 if (std::holds_alternative<rtc::string>(rtcMessage))
513 {
514 // Retrieve the string message
515 std::string szMessage = std::get<rtc::string>(rtcMessage);
516
517 // Parse the JSON message from the signaling server.
518 jsnMessage = nlohmann::json::parse(szMessage);
519 LOG_NOTICE(logging::g_qSharedLogger, "DATA_CHANNEL Received message from peer: {}", szMessage);
520 }
521 else if (std::holds_alternative<rtc::binary>(rtcMessage))
522 {
523 // Retrieve the binary message.
524 rtc::binary rtcBinaryData = std::get<rtc::binary>(rtcMessage);
525
526 // Convert the binary data to a string, ignoring non-printable characters.
527 std::string szBinaryDataStr;
528 for (auto byte : rtcBinaryData)
529 {
530 if (std::isprint(static_cast<unsigned char>(byte)))
531 {
532 szBinaryDataStr += static_cast<char>(byte);
533 }
534 }
535
536 // Print the binary data as a string.
537 LOG_DEBUG(logging::g_qSharedLogger, "DATA_CHANNEL Received binary data ({} bytes): {}", rtcBinaryData.size(), szBinaryDataStr);
538 }
539 else
540 {
541 LOG_ERROR(logging::g_qSharedLogger, "Received unknown message type from peer");
542 }
543 }
544 catch (const std::exception& e)
545 {
546 // Submit logger message.
547 LOG_ERROR(logging::g_qSharedLogger, "Error occurred while negotiating with the datachannel: {}", e.what());
548 }
549 });
550
551 return true;
552}
bool DecodeH264BytesToCVMat(const std::vector< uint8_t > &vH264EncodedBytes, cv::Mat &cvDecodedFrame, const AVPixelFormat eOutputPixelFormat)
Decodes H264 encoded bytes to a cv::Mat using FFmpeg.
Definition WebRTC.cpp:625
Here is the call graph for this function:
Here is the caller graph for this function:

◆ InitializeH264Decoder()

bool WebRTC::InitializeH264Decoder ( )
private

Initialize the H264 decoder. Creates the AVCodecContext, AVFrame, and AVPacket.

Returns
true - Successfully initialized the H264 decoder.
false - Failed to initialize the H264 decoder.
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2024-12-27
564{
565 // Configure logging level from FFMPEG library.
566 av_log_set_level(AV_LOG_DEBUG);
567
568 // Find the H264 decoder
569 const AVCodec* avCodec = avcodec_find_decoder(AV_CODEC_ID_H264);
570 if (!avCodec)
571 {
572 LOG_ERROR(logging::g_qSharedLogger, "H264 codec not found!");
573 return false;
574 }
575 // Create codec context
576 m_pAVCodecContext = avcodec_alloc_context3(avCodec);
577 if (!m_pAVCodecContext)
578 {
579 LOG_ERROR(logging::g_qSharedLogger, "Failed to allocate codec context!");
580 return false;
581 }
582 // Set codec context options.
583 m_pAVCodecContext->flags |= AV_CODEC_FLAG2_FAST;
584 m_pAVCodecContext->err_recognition = AV_EF_COMPLIANT | AV_EF_CAREFUL;
585 m_pAVCodecContext->rc_buffer_size = 50 * 1024 * 1024; // 50 MB buffer size.
586 av_opt_set_int(m_pAVCodecContext, "refcounted_frames", 1, 0);
587 av_opt_set_int(m_pAVCodecContext, "error_concealment", FF_EC_GUESS_MVS | FF_EC_DEBLOCK, 0);
588 av_opt_set_int(m_pAVCodecContext, "threads", 4, 0);
589
590 // Open the codec
591 if (avcodec_open2(m_pAVCodecContext, avCodec, nullptr) < 0)
592 {
593 LOG_ERROR(logging::g_qSharedLogger, "Failed to open codec!");
594 return false;
595 }
596
597 // Allocate the AVPacket.
598 m_pPacket = av_packet_alloc();
599 // Allocate the AVFrame.
600 m_pFrame = av_frame_alloc();
601 if (!m_pPacket || !m_pFrame)
602 {
603 LOG_ERROR(logging::g_qSharedLogger, "Failed to allocate packet or frame!");
604 return false;
605 }
606
607 // Set the SWSScale context to nullptr.
608 m_pSWSContext = nullptr;
609
610 return true;
611}
Here is the caller graph for this function:

◆ DecodeH264BytesToCVMat()

bool WebRTC::DecodeH264BytesToCVMat ( const std::vector< uint8_t > &  vH264EncodedBytes,
cv::Mat cvDecodedFrame,
const AVPixelFormat  eOutputPixelFormat 
)
private

Decodes H264 encoded bytes to a cv::Mat using FFmpeg.

Parameters
vH264EncodedBytes- The H264 encoded bytes.
cvDecodedFrame- The decoded frame.
eOutputPixelFormat- The output pixel format.
Returns
true - Frame was successfully decoded.
false - Frame was not successfully decoded.
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2024-11-16
626{
627 // Initialize packet data.
628 m_pPacket->data = const_cast<uint8_t*>(vH264EncodedBytes.data());
629 m_pPacket->size = static_cast<int>(vH264EncodedBytes.size());
630
631 // Send the packet to the decoder.
632 int nReturnCode = avcodec_send_packet(m_pAVCodecContext, m_pPacket);
633 if (nReturnCode < 0)
634 {
635 // Get the error message.
636 char aErrorBuffer[AV_ERROR_MAX_STRING_SIZE];
637 av_strerror(nReturnCode, aErrorBuffer, AV_ERROR_MAX_STRING_SIZE);
638 // Submit logger message.
639 LOG_NOTICE(logging::g_qSharedLogger,
640 "Failed to send packet to decoder! Error code: {} {}. This is not a serious problem and is likely just because some of the UDP RTP packets didn't "
641 "make it to us.",
642 nReturnCode,
643 aErrorBuffer);
644 // Request a new keyframe from the video track.
645 this->RequestKeyFrame();
646
647 return false;
648 }
649
650 // Receive decoded frames in a loop
651 while (true)
652 {
653 nReturnCode = avcodec_receive_frame(m_pAVCodecContext, m_pFrame);
654 if (nReturnCode == AVERROR(EAGAIN) || nReturnCode == AVERROR_EOF)
655 {
656 // No more frames available in stream.
657 break;
658 }
659 else if (nReturnCode < 0)
660 {
661 // Get the error message.
662 char aErrorBuffer[AV_ERROR_MAX_STRING_SIZE];
663 av_strerror(nReturnCode, aErrorBuffer, AV_ERROR_MAX_STRING_SIZE);
664 // Submit logger message.
665 LOG_WARNING(logging::g_qSharedLogger, "Failed to receive frame from decoder! Error code: {} {}", nReturnCode, aErrorBuffer);
666 // Request a new keyframe from the video track.
667 this->RequestKeyFrame();
668
669 return false;
670 }
671
672 // Check if the user want to keep the YUV420P data un-altered.
673 if (eOutputPixelFormat == AV_PIX_FMT_YUV420P)
674 {
675 // The frame received from the FFMPEG H264 decoder is already in YUV420P format.
676 // We want to keep the raw YUV420P byte data un-altered, but store that data in a RGB 3 channel Mat.
677 // Absolutely no colorspace conversion or the binary data will be corrupted.
678
679 // Extract the Y, U, and V planes.
680 cv::Mat cvYPlane(m_pFrame->height, m_pFrame->width, CV_8UC1, m_pFrame->data[0]);
681 cv::Mat cvUPlane(m_pFrame->height / 2, m_pFrame->width / 2, CV_8UC1, m_pFrame->data[1]);
682 cv::Mat cvVPlane(m_pFrame->height / 2, m_pFrame->width / 2, CV_8UC1, m_pFrame->data[2]);
683 // Upsample the U and V planes to match the Y plane.
684 cv::Mat cvUPlaneUpsampled, cvVPlaneUpsampled;
685 cv::resize(cvUPlane, cvUPlaneUpsampled, cv::Size(m_pFrame->width, m_pFrame->height), 0, 0, cv::INTER_NEAREST);
686 cv::resize(cvVPlane, cvVPlaneUpsampled, cv::Size(m_pFrame->width, m_pFrame->height), 0, 0, cv::INTER_NEAREST);
687 // Merge the Y, U, and V planes into a single 3 channel Mat.
688 std::vector<cv::Mat> vYUVPlanes = {cvYPlane, cvUPlaneUpsampled, cvVPlaneUpsampled};
689 cv::merge(vYUVPlanes, cvDecodedFrame);
690 }
691 else
692 {
693 // Convert the decoded frame to cv::Mat using sws_scale.
694 if (m_pSWSContext == nullptr)
695 {
696 m_pSWSContext = sws_getContext(m_pFrame->width,
697 m_pFrame->height,
698 static_cast<AVPixelFormat>(m_pFrame->format),
699 m_pFrame->width,
700 m_pFrame->height,
701 eOutputPixelFormat,
702 SWS_FAST_BILINEAR,
703 nullptr,
704 nullptr,
705 nullptr);
706 if (m_pSWSContext == nullptr)
707 {
708 // Submit logger message.
709 LOG_WARNING(logging::g_qSharedLogger, "Failed to initialize SwsContext!");
710 // Request a new keyframe from the video track.
711 this->RequestKeyFrame();
712
713 return false;
714 }
715
716 // Allocate buffer for the frame's data
717 int nRetCode = av_image_alloc(m_pFrame->data, m_pFrame->linesize, m_pFrame->width, m_pFrame->height, static_cast<AVPixelFormat>(m_pFrame->format), 32);
718 if (nRetCode < 0)
719 {
720 // Submit logger message.
721 LOG_WARNING(logging::g_qSharedLogger, "Failed to allocate image buffer!");
722 return false;
723 }
724 }
725
726 // Create new mat for the decoded frame.
727 cvDecodedFrame.create(m_pFrame->height, m_pFrame->width, CV_8UC3);
728 std::array<uint8_t*, 4> aDest = {cvDecodedFrame.data, nullptr, nullptr, nullptr};
729 std::array<int, 4> aDestLinesize = {static_cast<int>(cvDecodedFrame.step[0]), 0, 0, 0};
730
731 // Convert the frame to the output pixel format.
732 sws_scale(m_pSWSContext, m_pFrame->data, m_pFrame->linesize, 0, m_pFrame->height, aDest.data(), aDestLinesize.data());
733 }
734
735 // Calculate the time since the last key frame request.
736 std::chrono::duration<double> tmTimeSinceLastKeyFrameRequest = std::chrono::system_clock::now() - m_tmLastKeyFrameRequestTime;
737 // Check if the time since the last key frame request is greater than the key frame request interval.
738 if (tmTimeSinceLastKeyFrameRequest.count() > 1.0)
739 {
740 // Request a new key frame from the video track.
741 // this->RequestKeyFrame();
742
743 // Set the QP factor to 0. (Max quality)
744 this->SendCommandToStreamer("{\"Encoder.MaxQP\":" + std::to_string(constants::SIM_WEBRTC_QP) + "}");
745 // Set the bitrate limits.
746 this->SendCommandToStreamer(R"({"WebRTC.MinBitrate":99999})");
747 this->SendCommandToStreamer(R"({"WebRTC.MaxBitrate":99999999})");
748 // Set FPS to 30.
749 this->SendCommandToStreamer(R"({"WebRTC.Fps":30})");
750 this->SendCommandToStreamer(R"({"WebRTC.MaxFps":30})");
751 // Here's the magic, this might work. Target bitrate is what has been causing the issues.
752 this->SendCommandToStreamer(R"({"Encoder.TargetBitrate":99999999})");
753
754 // Update the time of the last key frame request.
755 m_tmLastKeyFrameRequestTime = std::chrono::system_clock::now();
756 }
757 }
758
759 return true;
760}
bool RequestKeyFrame()
Requests a key frame from the given video track. This is useful for when the video track is out of sy...
Definition WebRTC.cpp:772
bool SendCommandToStreamer(const std::string &szCommand)
This method sends a command to the streamer via the data channel. The command is a JSON string that i...
Definition WebRTC.cpp:805
uchar * data
void create(int rows, int cols, int type)
MatStep step
void merge(const Mat *mv, size_t count, OutputArray dst)
void resize(InputArray src, OutputArray dst, Size dsize, double fx=0, double fy=0, int interpolation=INTER_LINEAR)
INTER_NEAREST
::uint8_t uint8_t
Here is the call graph for this function:
Here is the caller graph for this function:

◆ RequestKeyFrame()

bool WebRTC::RequestKeyFrame ( )
private

Requests a key frame from the given video track. This is useful for when the video track is out of sync or has lost frames.

Returns
true - Key frame was successfully requested.
false - Key frame was not successfully requested.
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2024-11-30
773{
774 // Check if the video track is valid.
775 if (!m_pVideoTrack1)
776 {
777 LOG_ERROR(logging::g_qSharedLogger, "Invalid video track!");
778 return false;
779 }
780
781 // Submit logger message.
782 LOG_DEBUG(logging::g_qSharedLogger, "Requested key frame from video track. Success?: {}", m_pVideoTrack1->requestKeyframe());
783
784 // Request a key frame from the video track.
785 return true;
786}
Here is the caller graph for this function:

◆ SendCommandToStreamer()

bool WebRTC::SendCommandToStreamer ( const std::string &  szCommand)
private

This method sends a command to the streamer via the data channel. The command is a JSON string that is sent as a binary message. The PixelStreaming plugin handles the command very weirdly, so we have to sort of encode the command in a specific way. This handles that encoding.

Parameters
szCommand- The command to send to the streamer.
Returns
true - Command was successfully sent.
false - Command was not successfully sent.
Note
This will only work for valid COMMANDS with ID of type 51. Check the PixelStreamingInfrastructure repo for more information. https://github.com/EpicGamesExt/PixelStreamingInfrastructure/blob/13ce022d3a09d315d4ca85c05b61a8d3fe92741c/Extras/JSStreamer/src/protocol.ts#L196
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2025-01-01
806{
807 // Check if the data channel is valid.
808 if (!m_pDataChannel)
809 {
810 LOG_ERROR(logging::g_qSharedLogger, "Invalid data channel!");
811 return false;
812 }
813
814 // Check if the command is empty.
815 if (szCommand.empty())
816 {
817 LOG_ERROR(logging::g_qSharedLogger, "Empty command!");
818 return false;
819 }
820
821 // Set the max QP to 0.
822 std::string szID(1, static_cast<char>(51)); // This is the ID for COMMAND.
823 std::string szSize(1, static_cast<char>(szCommand.size()));
824 std::string szFinal = szID + szSize + szCommand;
825 // Loop through the string and build a binary message.
826 rtc::binary rtcBinaryMessage;
827 // First byte is the ID.
828 rtcBinaryMessage.push_back(static_cast<std::byte>(szID[0]));
829 // Next two bytes are the size.
830 rtcBinaryMessage.push_back(static_cast<std::byte>(szSize[0]));
831 rtcBinaryMessage.push_back(static_cast<std::byte>(szSize[1]));
832 // The rest of the bytes are the command, but this is utf16 so we need to add a null byte before each character.
833 for (char cLetter : szCommand)
834 {
835 rtcBinaryMessage.push_back(static_cast<std::byte>(cLetter));
836 rtcBinaryMessage.push_back(static_cast<std::byte>(0));
837 }
838
839 // Send the binary message.
840 return m_pDataChannel->send(rtcBinaryMessage);
841}
Here is the caller graph for this function:

The documentation for this class was generated from the following files: