/* Copyright(C) 2007-2026 VoIPobjects (voipobjects.com) * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #if defined(TARGET_WIN) # include # include #endif #if defined(TARGET_LINUX) || defined(TARGET_ANDROID) || defined(TARGET_OSX) # include #endif #include "HL_Rtp.h" #include "HL_Exception.h" #include "HL_Log.h" #include "jrtplib/src/rtprawpacket.h" #include "jrtplib/src/rtpipv4address.h" #include #include #include #include #include #define LOG_SUBSYSTEM "network" static constexpr size_t MAX_RTP_PACKET_SIZE = 65535; static const char RTPDUMP_SHEBANG[] = "#!rtpplay1.0"; // RTP fixed header (little-endian bit-field layout) struct RtpHeader { unsigned char cc:4; /* CSRC count */ unsigned char x:1; /* header extension flag */ unsigned char p:1; /* padding flag */ unsigned char version:2; /* protocol version */ unsigned char pt:7; /* payload type */ unsigned char m:1; /* marker bit */ unsigned short seq; /* sequence number */ unsigned int ts; /* timestamp */ unsigned int ssrc; /* synchronization source */ }; struct RtcpHeader { unsigned char rc:5; /* reception report count */ unsigned char p:1; /* padding flag */ unsigned char version:2; /* protocol version */ unsigned char pt; /* payload type */ uint16_t len; /* length */ uint32_t ssrc; /* synchronization source */ }; // --- IPv4 address helpers --- static std::string ipToString(uint32_t ip) { // ip in host byte order → dotted-decimal return std::to_string((ip >> 24) & 0xFF) + "." + std::to_string((ip >> 16) & 0xFF) + "." + std::to_string((ip >> 8) & 0xFF) + "." + std::to_string( ip & 0xFF); } static uint32_t stringToIp(const std::string& s) { unsigned a = 0, b = 0, c = 0, d = 0; if (std::sscanf(s.c_str(), "%u.%u.%u.%u", &a, &b, &c, &d) != 4) return 0; if (a > 255 || b > 255 || c > 255 || d > 255) return 0; return (a << 24) | (b << 16) | (c << 8) | d; } // --- RtpHelper implementation --- bool RtpHelper::isRtp(const void* buffer, size_t length) { if (length < 12) return false; const RtpHeader* h = reinterpret_cast(buffer); if (h->version != 2) return false; unsigned char pt = h->pt; bool rtp = (pt >= 96 && pt <= 127) || (pt < 35); return rtp; } bool RtpHelper::isRtpOrRtcp(const void* buffer, size_t length) { // A minimal RTCP packet (e.g. an empty receiver report) is 8 bytes if (length < 8) return false; const RtcpHeader* h = reinterpret_cast(buffer); return h->version == 2; } bool RtpHelper::isRtcp(const void* buffer, size_t length) { return (isRtpOrRtcp(buffer, length) && !isRtp(buffer, length)); } unsigned RtpHelper::findSsrc(const void* buffer, size_t length) { if (isRtp(buffer, length)) return ntohl(reinterpret_cast(buffer)->ssrc); else if (isRtpOrRtcp(buffer, length)) return ntohl(reinterpret_cast(buffer)->ssrc); return 0; } void RtpHelper::setSsrc(void* buffer, size_t length, uint32_t ssrc) { if (isRtp(buffer, length)) reinterpret_cast(buffer)->ssrc = htonl(ssrc); else if (isRtpOrRtcp(buffer, length)) reinterpret_cast(buffer)->ssrc = htonl(ssrc); } int RtpHelper::findPtype(const void* buffer, size_t length) { if (isRtp(buffer, length)) return reinterpret_cast(buffer)->pt; else return -1; } int RtpHelper::findPacketNo(const void *buffer, size_t length) { if (isRtp(buffer, length)) return ntohs(reinterpret_cast(buffer)->seq); else return -1; } int RtpHelper::findPayloadLength(const void* buffer, size_t length) { if (!isRtp(buffer, length)) return -1; const RtpHeader* h = reinterpret_cast(buffer); const uint8_t* p = static_cast(buffer); // Fixed header (12 bytes) + CSRC list (4 * CC bytes) size_t offset = 12 + 4u * h->cc; if (offset > length) return -1; // Header extension if (h->x) { if (offset + 4 > length) return -1; uint16_t extWords = (static_cast(p[offset + 2]) << 8) | p[offset + 3]; offset += 4 + 4u * extWords; if (offset > length) return -1; } size_t payloadLen = length - offset; // Padding if (h->p && payloadLen > 0) { uint8_t padBytes = p[length - 1]; if (padBytes > payloadLen) return -1; payloadLen -= padBytes; } return static_cast(payloadLen); } std::chrono::microseconds RtpHelper::toMicroseconds(const jrtplib::RTPTime& t) { return std::chrono::microseconds(uint64_t(t.GetDouble() * 1000000)); } // --- RtpDump implementation --- std::shared_ptr RtpDump::parseRtpData(const uint8_t* data, size_t len) { if (!data || len < 12 || !RtpHelper::isRtp(data, len)) return nullptr; try { // Both are heap-allocated; RTPRawPacket takes ownership and deletes them auto* addr = new jrtplib::RTPIPv4Address(uint32_t(0), uint16_t(0)); uint8_t* dataCopy = new uint8_t[len]; std::memcpy(dataCopy, data, len); jrtplib::RTPRawPacket raw(dataCopy, len, addr, jrtplib::RTPTime(0), true); auto packet = std::make_shared(raw); if (packet->GetCreationError() != 0) return nullptr; return packet; } catch (const std::exception& e) { ICELogInfo(<< "Failed to parse RTP packet: " << e.what()); return nullptr; } } RtpDump::RtpDump(const char* filename) : mFilename(filename ? filename : "") { } RtpDump::~RtpDump() = default; void RtpDump::setSource(uint32_t ip, uint16_t port) { mSourceIp = ip; mSourcePort = port; } void RtpDump::load() { if (mFilename.empty()) throw std::runtime_error("No filename specified"); std::ifstream input(mFilename, std::ios::binary); if (!input.is_open()) throw std::runtime_error("Failed to open RTP dump file: " + mFilename); mPacketList.clear(); // --- 1. Text header: "#!rtpplay1.0 /\n" --- std::string textLine; std::getline(input, textLine); if (textLine.compare(0, sizeof(RTPDUMP_SHEBANG) - 1, RTPDUMP_SHEBANG) != 0) throw std::runtime_error("Invalid rtpdump header: expected " + std::string(RTPDUMP_SHEBANG)); // Parse source address from the text line size_t spacePos = textLine.find(' '); if (spacePos != std::string::npos) { std::string addrPart = textLine.substr(spacePos + 1); size_t slashPos = addrPart.find('/'); if (slashPos != std::string::npos) { mSourceIp = stringToIp(addrPart.substr(0, slashPos)); try { mSourcePort = static_cast(std::stoi(addrPart.substr(slashPos + 1))); } catch (...) { mSourcePort = 0; } } } // --- 2. Binary file header (RD_hdr_t, 16 bytes) --- uint32_t buf32; uint16_t buf16; input.read(reinterpret_cast(&buf32), 4); mStartSec = ntohl(buf32); input.read(reinterpret_cast(&buf32), 4); mStartUsec = ntohl(buf32); input.read(reinterpret_cast(&buf32), 4); // source IP (already NBO in file) // The binary header stores IP in network byte order; convert to host mSourceIp = ntohl(buf32); input.read(reinterpret_cast(&buf16), 2); mSourcePort = ntohs(buf16); input.read(reinterpret_cast(&buf16), 2); // padding — discard if (!input.good()) throw std::runtime_error("Failed to read rtpdump binary header"); // --- 3. Packet records --- size_t packetCount = 0; while (input.good() && input.peek() != EOF) { // Packet header: length(2) + plen(2) + offset(4) = 8 bytes uint16_t recLength, plen; uint32_t offsetMs; input.read(reinterpret_cast(&recLength), 2); if (input.gcount() != 2) break; recLength = ntohs(recLength); input.read(reinterpret_cast(&plen), 2); if (input.gcount() != 2) break; plen = ntohs(plen); input.read(reinterpret_cast(&offsetMs), 4); if (input.gcount() != 4) break; offsetMs = ntohl(offsetMs); // All-zeros record signals end of file in some implementations if (recLength == 0 && plen == 0 && offsetMs == 0) break; if (plen == 0 || plen > MAX_RTP_PACKET_SIZE) throw std::runtime_error("Invalid packet payload length: " + std::to_string(plen)); if (recLength < plen + 8) throw std::runtime_error("Record length (" + std::to_string(recLength) + ") smaller than payload + header (" + std::to_string(plen + 8) + ")"); // Read body std::vector body(plen); input.read(reinterpret_cast(body.data()), plen); if (static_cast(input.gcount()) != plen) throw std::runtime_error("Incomplete packet data in rtpdump file"); // Skip any padding between plen and recLength-8 size_t pad = static_cast(recLength) - 8 - plen; if (pad > 0) input.seekg(static_cast(pad), std::ios::cur); RtpData entry; entry.mRawData = std::move(body); entry.mOffsetMs = offsetMs; entry.mPacket = parseRtpData(entry.mRawData.data(), entry.mRawData.size()); mPacketList.push_back(std::move(entry)); packetCount++; } ICELogInfo(<< "Loaded " << packetCount << " packets from " << mFilename); mLoaded = true; } size_t RtpDump::count() const { return mPacketList.size(); } jrtplib::RTPPacket& RtpDump::packetAt(size_t index) { if (index >= mPacketList.size()) throw std::out_of_range("Packet index out of range: " + std::to_string(index)); if (!mPacketList[index].mPacket) throw std::runtime_error("No parsed RTP data at index " + std::to_string(index)); return *mPacketList[index].mPacket; } const std::vector& RtpDump::rawDataAt(size_t index) const { if (index >= mPacketList.size()) throw std::out_of_range("Packet index out of range: " + std::to_string(index)); return mPacketList[index].mRawData; } uint32_t RtpDump::offsetAt(size_t index) const { if (index >= mPacketList.size()) throw std::out_of_range("Packet index out of range: " + std::to_string(index)); return mPacketList[index].mOffsetMs; } void RtpDump::add(const void* buffer, size_t len) { if (!buffer || len == 0) return; uint32_t offsetMs = 0; auto now = std::chrono::steady_clock::now(); if (!mRecording) { mRecording = true; mRecordStart = now; // Capture wall-clock start time auto wallNow = std::chrono::system_clock::now(); auto epoch = wallNow.time_since_epoch(); auto sec = std::chrono::duration_cast(epoch); auto usec = std::chrono::duration_cast(epoch - sec); mStartSec = static_cast(sec.count()); mStartUsec = static_cast(usec.count()); } else { auto elapsed = std::chrono::duration_cast(now - mRecordStart); offsetMs = static_cast(elapsed.count()); } add(buffer, len, offsetMs); } void RtpDump::add(const void* buffer, size_t len, uint32_t offsetMs) { if (!buffer || len == 0) return; // The record length field is 16-bit and covers payload + 8 byte header if (len > MAX_RTP_PACKET_SIZE - 8) throw std::runtime_error("Packet too large: " + std::to_string(len)); RtpData entry; entry.mRawData.assign(static_cast(buffer), static_cast(buffer) + len); entry.mOffsetMs = offsetMs; entry.mPacket = parseRtpData(entry.mRawData.data(), entry.mRawData.size()); mPacketList.push_back(std::move(entry)); } void RtpDump::flush() { if (mFilename.empty()) throw std::runtime_error("No filename specified"); std::ofstream output(mFilename, std::ios::binary); if (!output.is_open()) throw std::runtime_error("Failed to open file for writing: " + mFilename); // --- 1. Text header --- std::string textLine = std::string(RTPDUMP_SHEBANG) + " " + ipToString(mSourceIp) + "/" + std::to_string(mSourcePort) + "\n"; output.write(textLine.data(), static_cast(textLine.size())); // --- 2. Binary file header (16 bytes) --- uint32_t buf32; uint16_t buf16; buf32 = htonl(mStartSec); output.write(reinterpret_cast(&buf32), 4); buf32 = htonl(mStartUsec); output.write(reinterpret_cast(&buf32), 4); buf32 = htonl(mSourceIp); output.write(reinterpret_cast(&buf32), 4); buf16 = htons(mSourcePort); output.write(reinterpret_cast(&buf16), 2); buf16 = 0; // padding output.write(reinterpret_cast(&buf16), 2); // --- 3. Packet records --- size_t written = 0; for (const auto& pkt : mPacketList) { if (pkt.mRawData.empty()) continue; uint16_t plen = static_cast(pkt.mRawData.size()); uint16_t recLength = static_cast(plen + 8); buf16 = htons(recLength); output.write(reinterpret_cast(&buf16), 2); buf16 = htons(plen); output.write(reinterpret_cast(&buf16), 2); buf32 = htonl(pkt.mOffsetMs); output.write(reinterpret_cast(&buf32), 4); output.write(reinterpret_cast(pkt.mRawData.data()), plen); written++; } if (!output.good()) throw std::runtime_error("Failed to write rtpdump file: " + mFilename); ICELogInfo(<< "Wrote " << written << " packets to " << mFilename); } void RtpDump::clear() { mPacketList.clear(); mLoaded = false; mRecording = false; }