- initial work to fix RTPdump decoder + more fixes
This commit is contained in:
@@ -341,6 +341,8 @@ set (LIBS_STATIC ${LIBS_STATIC} jrtplib g729_codec gsm_codec opus
|
|||||||
|
|
||||||
if (USE_AMR_CODEC)
|
if (USE_AMR_CODEC)
|
||||||
#include (${LIB_PLATFORM}/platform_libs.cmake)
|
#include (${LIB_PLATFORM}/platform_libs.cmake)
|
||||||
|
set (OPENCORE_AMRNB opencore-amrnb)
|
||||||
|
set (OPENCORE_AMRWB opencore-amrwb)
|
||||||
message("Media: AMR NB and WB codecs will be included.")
|
message("Media: AMR NB and WB codecs will be included.")
|
||||||
set (DEFINES ${DEFINES} -DUSE_AMR_CODEC)
|
set (DEFINES ${DEFINES} -DUSE_AMR_CODEC)
|
||||||
set (LIBS_STATIC ${LIBS_STATIC} ${OPENCORE_AMRNB} ${OPENCORE_AMRWB})
|
set (LIBS_STATIC ${LIBS_STATIC} ${OPENCORE_AMRNB} ${OPENCORE_AMRWB})
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const std::string Status_FailedToOpenFile = "failed to open file";
|
|||||||
const std::string Status_NoActiveProvider = "no active provider";
|
const std::string Status_NoActiveProvider = "no active provider";
|
||||||
const std::string Status_NoMediaAction = "no valid media action";
|
const std::string Status_NoMediaAction = "no valid media action";
|
||||||
const std::string Status_NoCommand = "no valid command";
|
const std::string Status_NoCommand = "no valid command";
|
||||||
|
const std::string Status_NoAudioManager = "no audio manager";
|
||||||
|
|
||||||
#define LOG_SUBSYSTEM "Agent"
|
#define LOG_SUBSYSTEM "Agent"
|
||||||
|
|
||||||
@@ -336,7 +337,7 @@ void AgentImpl::processStartSession(JsonCpp::Value& request, JsonCpp::Value& ans
|
|||||||
{
|
{
|
||||||
// Agent was not started
|
// Agent was not started
|
||||||
ICELogError(<< "No audio manager installed.");
|
ICELogError(<< "No audio manager installed.");
|
||||||
answer["status"] = "Audio manager not started. Most probably agent is not started.";
|
answer["status"] = Status_NoAudioManager;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,6 +431,13 @@ void AgentImpl::processAcceptSession(JsonCpp::Value& request, JsonCpp::Value& an
|
|||||||
std::unique_lock<std::recursive_mutex> l(mAgentMutex);
|
std::unique_lock<std::recursive_mutex> l(mAgentMutex);
|
||||||
auto sessionIter = mSessionMap.find(request["session_id"].asInt());
|
auto sessionIter = mSessionMap.find(request["session_id"].asInt());
|
||||||
if (sessionIter != mSessionMap.end())
|
if (sessionIter != mSessionMap.end())
|
||||||
|
{
|
||||||
|
if (!mAudioManager)
|
||||||
|
{
|
||||||
|
ICELogError(<< "No audio manager installed.");
|
||||||
|
answer["status"] = Status_NoAudioManager;
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
// Ensure audio manager is here
|
// Ensure audio manager is here
|
||||||
mAudioManager->start(mUseNativeAudio ? AudioManager::atReceiver : AudioManager::atNull);
|
mAudioManager->start(mUseNativeAudio ? AudioManager::atReceiver : AudioManager::atNull);
|
||||||
@@ -450,9 +458,9 @@ void AgentImpl::processAcceptSession(JsonCpp::Value& request, JsonCpp::Value& an
|
|||||||
|
|
||||||
answer["status"] = Status_Ok;
|
answer["status"] = Status_Ok;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
answer["status"] = Status_SessionNotFound;
|
answer["status"] = Status_SessionNotFound;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AgentImpl::processDestroySession(JsonCpp::Value& request, JsonCpp::Value& answer)
|
void AgentImpl::processDestroySession(JsonCpp::Value& request, JsonCpp::Value& answer)
|
||||||
|
|||||||
@@ -18,14 +18,24 @@ DataWindow::DataWindow()
|
|||||||
DataWindow::~DataWindow()
|
DataWindow::~DataWindow()
|
||||||
{
|
{
|
||||||
if (mData)
|
if (mData)
|
||||||
|
{
|
||||||
free(mData);
|
free(mData);
|
||||||
|
mData = nullptr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DataWindow::setCapacity(int capacity)
|
void DataWindow::setCapacity(int capacity)
|
||||||
{
|
{
|
||||||
Lock l(mMutex);
|
Lock l(mMutex);
|
||||||
int tail = capacity - mCapacity;
|
int tail = capacity - mCapacity;
|
||||||
|
char* buffer = mData;
|
||||||
mData = (char*)realloc(mData, capacity);
|
mData = (char*)realloc(mData, capacity);
|
||||||
|
if (!mData)
|
||||||
|
{
|
||||||
|
// Realloc failed
|
||||||
|
mData = buffer;
|
||||||
|
throw std::bad_alloc();
|
||||||
|
}
|
||||||
if (tail > 0)
|
if (tail > 0)
|
||||||
memset(mData + mCapacity, 0, tail);
|
memset(mData + mCapacity, 0, tail);
|
||||||
mCapacity = capacity;
|
mCapacity = capacity;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* Copyright(C) 2007-2017 VoIP objects (voipobjects.com)
|
/* Copyright(C) 2007-2026 VoIP objects (voipobjects.com)
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
* 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
|
* 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/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
#include "../engine_config.h"
|
#include "../engine_config.h"
|
||||||
#include "HL_NetworkSocket.h"
|
#include "HL_NetworkSocket.h"
|
||||||
|
#include "HL_Log.h"
|
||||||
|
|
||||||
#if defined(TARGET_OSX) || defined(TARGET_LINUX)
|
#if defined(TARGET_OSX) || defined(TARGET_LINUX)
|
||||||
# include <fcntl.h>
|
# include <fcntl.h>
|
||||||
@@ -19,11 +20,11 @@
|
|||||||
#endif
|
#endif
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
|
|
||||||
|
#define LOG_SUBSYSTEM "network"
|
||||||
|
|
||||||
DatagramSocket::DatagramSocket()
|
DatagramSocket::DatagramSocket()
|
||||||
:mFamily(AF_INET), mHandle(INVALID_SOCKET), mLocalPort(0)
|
:mFamily(AF_INET), mHandle(INVALID_SOCKET), mLocalPort(0)
|
||||||
{
|
{}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
DatagramSocket::~DatagramSocket()
|
DatagramSocket::~DatagramSocket()
|
||||||
{
|
{
|
||||||
@@ -160,6 +161,12 @@ void DatagramAgreggator::addSocket(PDatagramSocket socket)
|
|||||||
if (socket->mHandle == INVALID_SOCKET)
|
if (socket->mHandle == INVALID_SOCKET)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (mSocketVector.size() >= 62)
|
||||||
|
{
|
||||||
|
ICELogError(<< "fd_set overflow; too much sockets");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
FD_SET(socket->mHandle, &mReadSet);
|
FD_SET(socket->mHandle, &mReadSet);
|
||||||
if (socket->mHandle > mMaxHandle)
|
if (socket->mHandle > mMaxHandle)
|
||||||
mMaxHandle = socket->mHandle;
|
mMaxHandle = socket->mHandle;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* Copyright(C) 2007-2017 VoIPobjects (voipobjects.com)
|
/* Copyright(C) 2007-2026 VoIPobjects (voipobjects.com)
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
* 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
|
* 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/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
@@ -8,26 +8,29 @@
|
|||||||
# include <Windows.h>
|
# include <Windows.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(TARGET_LINUX) || defined(TARGET_ANDROID)
|
#if defined(TARGET_LINUX) || defined(TARGET_ANDROID) || defined(TARGET_OSX)
|
||||||
# include <arpa/inet.h>
|
# include <arpa/inet.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "HL_Rtp.h"
|
#include "HL_Rtp.h"
|
||||||
#include "HL_Exception.h"
|
#include "HL_Exception.h"
|
||||||
#include "HL_String.h"
|
#include "HL_Log.h"
|
||||||
|
|
||||||
#if defined(USE_RTP_DUMP)
|
|
||||||
#include "jrtplib/src/rtprawpacket.h"
|
#include "jrtplib/src/rtprawpacket.h"
|
||||||
#include "jrtplib/src/rtpipv4address.h"
|
#include "jrtplib/src/rtpipv4address.h"
|
||||||
#endif
|
|
||||||
|
|
||||||
#if !defined(TARGET_WIN)
|
#include <stdexcept>
|
||||||
# include <alloca.h>
|
#include <fstream>
|
||||||
#endif
|
#include <cstring>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
#include <sstream>
|
#define LOG_SUBSYSTEM "RtpDump"
|
||||||
#include <tuple>
|
|
||||||
|
|
||||||
|
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
|
struct RtpHeader
|
||||||
{
|
{
|
||||||
unsigned char cc:4; /* CSRC count */
|
unsigned char cc:4; /* CSRC count */
|
||||||
@@ -46,32 +49,54 @@ struct RtcpHeader
|
|||||||
unsigned char rc:5; /* reception report count */
|
unsigned char rc:5; /* reception report count */
|
||||||
unsigned char p:1; /* padding flag */
|
unsigned char p:1; /* padding flag */
|
||||||
unsigned char version:2; /* protocol version */
|
unsigned char version:2; /* protocol version */
|
||||||
unsigned char pt:8; /* payload type */
|
unsigned char pt; /* payload type */
|
||||||
uint16_t len; /* length */
|
uint16_t len; /* length */
|
||||||
uint32_t ssrc; /* synchronization source */
|
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)
|
bool RtpHelper::isRtp(const void* buffer, size_t length)
|
||||||
{
|
{
|
||||||
if (length < 12)
|
if (length < 12)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const RtpHeader* h = reinterpret_cast<const RtpHeader*>(buffer);
|
const RtpHeader* h = reinterpret_cast<const RtpHeader*>(buffer);
|
||||||
if (h->version != 0b10)
|
if (h->version != 2)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
unsigned char pt = h->pt;
|
unsigned char pt = h->pt;
|
||||||
bool rtp = ( (pt & 0x7F) >= 96 && (pt & 0x7F) <= 127) || ((pt & 0x7F) < 35);
|
bool rtp = (pt >= 96 && pt <= 127) || (pt < 35);
|
||||||
return rtp;
|
return rtp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
bool RtpHelper::isRtpOrRtcp(const void* buffer, size_t length)
|
bool RtpHelper::isRtpOrRtcp(const void* buffer, size_t length)
|
||||||
{
|
{
|
||||||
if (length < 12)
|
if (length < 12)
|
||||||
return false;
|
return false;
|
||||||
const RtcpHeader* h = reinterpret_cast<const RtcpHeader*>(buffer);
|
const RtcpHeader* h = reinterpret_cast<const RtcpHeader*>(buffer);
|
||||||
return h->version == 0b10;
|
return h->version == 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RtpHelper::isRtcp(const void* buffer, size_t length)
|
bool RtpHelper::isRtcp(const void* buffer, size_t length)
|
||||||
@@ -83,15 +108,16 @@ unsigned RtpHelper::findSsrc(const void* buffer, size_t length)
|
|||||||
{
|
{
|
||||||
if (isRtp(buffer, length))
|
if (isRtp(buffer, length))
|
||||||
return ntohl(reinterpret_cast<const RtpHeader*>(buffer)->ssrc);
|
return ntohl(reinterpret_cast<const RtpHeader*>(buffer)->ssrc);
|
||||||
else
|
else if (isRtpOrRtcp(buffer, length))
|
||||||
return ntohl(reinterpret_cast<const RtcpHeader*>(buffer)->ssrc);
|
return ntohl(reinterpret_cast<const RtcpHeader*>(buffer)->ssrc);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RtpHelper::setSsrc(void* buffer, size_t length, uint32_t ssrc)
|
void RtpHelper::setSsrc(void* buffer, size_t length, uint32_t ssrc)
|
||||||
{
|
{
|
||||||
if (isRtp(buffer, length))
|
if (isRtp(buffer, length))
|
||||||
reinterpret_cast<RtpHeader*>(buffer)->ssrc = htonl(ssrc);
|
reinterpret_cast<RtpHeader*>(buffer)->ssrc = htonl(ssrc);
|
||||||
else
|
else if (isRtpOrRtcp(buffer, length))
|
||||||
reinterpret_cast<RtcpHeader*>(buffer)->ssrc = htonl(ssrc);
|
reinterpret_cast<RtcpHeader*>(buffer)->ssrc = htonl(ssrc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,48 +139,187 @@ int RtpHelper::findPacketNo(const void *buffer, size_t length)
|
|||||||
|
|
||||||
int RtpHelper::findPayloadLength(const void* buffer, size_t length)
|
int RtpHelper::findPayloadLength(const void* buffer, size_t length)
|
||||||
{
|
{
|
||||||
if (isRtp(buffer, length))
|
if (!isRtp(buffer, length))
|
||||||
{
|
return -1;
|
||||||
return length - 12;
|
|
||||||
}
|
const RtpHeader* h = reinterpret_cast<const RtpHeader*>(buffer);
|
||||||
else
|
const uint8_t* p = static_cast<const uint8_t*>(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<uint16_t>(p[offset + 2]) << 8) | p[offset + 3];
|
||||||
|
offset += 4 + 4u * extWords;
|
||||||
|
if (offset > length)
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(USE_RTPDUMP)
|
size_t payloadLen = length - offset;
|
||||||
RtpDump::RtpDump(const char *filename)
|
|
||||||
:mFilename(filename)
|
|
||||||
{}
|
|
||||||
|
|
||||||
RtpDump::~RtpDump()
|
// Padding
|
||||||
{
|
if (h->p && payloadLen > 0) {
|
||||||
flush();
|
uint8_t padBytes = p[length - 1];
|
||||||
for (PacketList::iterator packetIter=mPacketList.begin(); packetIter!=mPacketList.end(); ++packetIter)
|
if (padBytes > payloadLen)
|
||||||
{
|
return -1;
|
||||||
//free(packetIter->mData);
|
payloadLen -= padBytes;
|
||||||
delete packetIter->mPacket;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return static_cast<int>(payloadLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- RtpDump implementation ---
|
||||||
|
|
||||||
|
std::shared_ptr<jrtplib::RTPPacket> 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<jrtplib::RTPPacket>(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()
|
void RtpDump::load()
|
||||||
{
|
{
|
||||||
FILE* f = fopen(mFilename.c_str(), "rb");
|
if (mFilename.empty())
|
||||||
if (!f)
|
throw std::runtime_error("No filename specified");
|
||||||
throw Exception(ERR_WAVFILE_FAILED);
|
|
||||||
|
|
||||||
while (!feof(f))
|
std::ifstream input(mFilename, std::ios::binary);
|
||||||
{
|
if (!input.is_open())
|
||||||
RtpData data;
|
throw std::runtime_error("Failed to open RTP dump file: " + mFilename);
|
||||||
fread(&data.mLength, sizeof data.mLength, 1, f);
|
|
||||||
data.mData = new char[data.mLength];
|
mPacketList.clear();
|
||||||
fread(data.mData, 1, data.mLength, f);
|
|
||||||
jrtplib::RTPIPv4Address addr(jrtplib::RTPAddress::IPv4Address);
|
// --- 1. Text header: "#!rtpplay1.0 <ip>/<port>\n" ---
|
||||||
jrtplib::RTPTime t(0);
|
std::string textLine;
|
||||||
jrtplib::RTPRawPacket* raw = new jrtplib::RTPRawPacket((unsigned char*)data.mData, data.mLength, &addr, t, true);
|
std::getline(input, textLine);
|
||||||
data.mPacket = new jrtplib::RTPPacket(*raw);
|
if (textLine.compare(0, sizeof(RTPDUMP_SHEBANG) - 1, RTPDUMP_SHEBANG) != 0)
|
||||||
mPacketList.push_back(data);
|
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<uint16_t>(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<char*>(&buf32), 4);
|
||||||
|
mStartSec = ntohl(buf32);
|
||||||
|
|
||||||
|
input.read(reinterpret_cast<char*>(&buf32), 4);
|
||||||
|
mStartUsec = ntohl(buf32);
|
||||||
|
|
||||||
|
input.read(reinterpret_cast<char*>(&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<char*>(&buf16), 2);
|
||||||
|
mSourcePort = ntohs(buf16);
|
||||||
|
|
||||||
|
input.read(reinterpret_cast<char*>(&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<char*>(&recLength), 2);
|
||||||
|
if (input.gcount() != 2) break;
|
||||||
|
recLength = ntohs(recLength);
|
||||||
|
|
||||||
|
input.read(reinterpret_cast<char*>(&plen), 2);
|
||||||
|
if (input.gcount() != 2) break;
|
||||||
|
plen = ntohs(plen);
|
||||||
|
|
||||||
|
input.read(reinterpret_cast<char*>(&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<uint8_t> body(plen);
|
||||||
|
input.read(reinterpret_cast<char*>(body.data()), plen);
|
||||||
|
if (static_cast<size_t>(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<size_t>(recLength) - 8 - plen;
|
||||||
|
if (pad > 0)
|
||||||
|
input.seekg(static_cast<std::streamoff>(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
|
size_t RtpDump::count() const
|
||||||
{
|
{
|
||||||
@@ -163,39 +328,142 @@ size_t RtpDump::count() const
|
|||||||
|
|
||||||
jrtplib::RTPPacket& RtpDump::packetAt(size_t index)
|
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;
|
return *mPacketList[index].mPacket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::vector<uint8_t>& 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)
|
void RtpDump::add(const void* buffer, size_t len)
|
||||||
{
|
{
|
||||||
RtpData data;
|
if (!buffer || len == 0)
|
||||||
data.mData = malloc(len);
|
return;
|
||||||
memcpy(data.mData, buffer, len);
|
|
||||||
data.mLength = len;
|
|
||||||
|
|
||||||
jrtplib::RTPIPv4Address addr(jrtplib::RTPAddress::IPv4Address);
|
uint32_t offsetMs = 0;
|
||||||
jrtplib::RTPTime t(0);
|
auto now = std::chrono::steady_clock::now();
|
||||||
jrtplib::RTPRawPacket* raw = new jrtplib::RTPRawPacket((unsigned char*)const_cast<void*>(data.mData), data.mLength, &addr, t, true);
|
|
||||||
data.mPacket = new jrtplib::RTPPacket(*raw);
|
if (!mRecording) {
|
||||||
//delete raw;
|
mRecording = true;
|
||||||
mPacketList.push_back(data);
|
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<std::chrono::seconds>(epoch);
|
||||||
|
auto usec = std::chrono::duration_cast<std::chrono::microseconds>(epoch - sec);
|
||||||
|
mStartSec = static_cast<uint32_t>(sec.count());
|
||||||
|
mStartUsec = static_cast<uint32_t>(usec.count());
|
||||||
|
} else {
|
||||||
|
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - mRecordStart);
|
||||||
|
offsetMs = static_cast<uint32_t>(elapsed.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
add(buffer, len, offsetMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RtpDump::add(const void* buffer, size_t len, uint32_t offsetMs)
|
||||||
|
{
|
||||||
|
if (!buffer || len == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (len > MAX_RTP_PACKET_SIZE)
|
||||||
|
throw std::runtime_error("Packet too large: " + std::to_string(len));
|
||||||
|
|
||||||
|
RtpData entry;
|
||||||
|
entry.mRawData.assign(static_cast<const uint8_t*>(buffer),
|
||||||
|
static_cast<const uint8_t*>(buffer) + len);
|
||||||
|
entry.mOffsetMs = offsetMs;
|
||||||
|
entry.mPacket = parseRtpData(entry.mRawData.data(), entry.mRawData.size());
|
||||||
|
|
||||||
|
mPacketList.push_back(std::move(entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
void RtpDump::flush()
|
void RtpDump::flush()
|
||||||
{
|
{
|
||||||
FILE* f = fopen(mFilename.c_str(), "wb");
|
if (mFilename.empty())
|
||||||
if (!f)
|
throw std::runtime_error("No filename specified");
|
||||||
throw Exception(ERR_WAVFILE_FAILED);
|
|
||||||
|
|
||||||
PacketList::iterator packetIter = mPacketList.begin();
|
std::ofstream output(mFilename, std::ios::binary);
|
||||||
for (;packetIter != mPacketList.end(); ++packetIter)
|
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<std::streamsize>(textLine.size()));
|
||||||
|
|
||||||
|
// --- 2. Binary file header (16 bytes) ---
|
||||||
|
uint32_t buf32;
|
||||||
|
uint16_t buf16;
|
||||||
|
|
||||||
|
buf32 = htonl(mStartSec);
|
||||||
|
output.write(reinterpret_cast<const char*>(&buf32), 4);
|
||||||
|
|
||||||
|
buf32 = htonl(mStartUsec);
|
||||||
|
output.write(reinterpret_cast<const char*>(&buf32), 4);
|
||||||
|
|
||||||
|
buf32 = htonl(mSourceIp);
|
||||||
|
output.write(reinterpret_cast<const char*>(&buf32), 4);
|
||||||
|
|
||||||
|
buf16 = htons(mSourcePort);
|
||||||
|
output.write(reinterpret_cast<const char*>(&buf16), 2);
|
||||||
|
|
||||||
|
buf16 = 0; // padding
|
||||||
|
output.write(reinterpret_cast<const char*>(&buf16), 2);
|
||||||
|
|
||||||
|
// --- 3. Packet records ---
|
||||||
|
size_t written = 0;
|
||||||
|
|
||||||
|
for (const auto& pkt : mPacketList) {
|
||||||
|
if (pkt.mRawData.empty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
uint16_t plen = static_cast<uint16_t>(pkt.mRawData.size());
|
||||||
|
uint16_t recLength = static_cast<uint16_t>(plen + 8);
|
||||||
|
|
||||||
|
buf16 = htons(recLength);
|
||||||
|
output.write(reinterpret_cast<const char*>(&buf16), 2);
|
||||||
|
|
||||||
|
buf16 = htons(plen);
|
||||||
|
output.write(reinterpret_cast<const char*>(&buf16), 2);
|
||||||
|
|
||||||
|
buf32 = htonl(pkt.mOffsetMs);
|
||||||
|
output.write(reinterpret_cast<const char*>(&buf32), 4);
|
||||||
|
|
||||||
|
output.write(reinterpret_cast<const char*>(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()
|
||||||
{
|
{
|
||||||
RtpData& data = *packetIter;
|
mPacketList.clear();
|
||||||
// Disabled for debugging only
|
mLoaded = false;
|
||||||
//fwrite(&data.mLength, sizeof data.mLength, 1, f);
|
mRecording = false;
|
||||||
fwrite(data.mData, data.mLength, 1, f);
|
|
||||||
}
|
}
|
||||||
fclose(f);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* Copyright(C) 2007-2025 VoIPobjects (voipobjects.com)
|
/* Copyright(C) 2007-2026 VoIPobjects (voipobjects.com)
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
* 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
|
* 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/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
@@ -6,12 +6,14 @@
|
|||||||
#ifndef __HL_RTP_H
|
#ifndef __HL_RTP_H
|
||||||
#define __HL_RTP_H
|
#define __HL_RTP_H
|
||||||
|
|
||||||
#if defined(USE_RTPDUMP)
|
|
||||||
#include "jrtplib/src/rtppacket.h"
|
#include "jrtplib/src/rtppacket.h"
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <stdlib.h>
|
#include <cstdlib>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <memory>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
// Class to carry rtp/rtcp socket pair
|
// Class to carry rtp/rtcp socket pair
|
||||||
template<class T>
|
template<class T>
|
||||||
@@ -27,7 +29,7 @@ struct RtpPair
|
|||||||
:mRtp(rtp), mRtcp(rtcp)
|
:mRtp(rtp), mRtcp(rtcp)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
bool multiplexed() { return mRtp == mRtcp; }
|
bool multiplexed() const { return mRtp == mRtcp; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class RtpHelper
|
class RtpHelper
|
||||||
@@ -43,31 +45,104 @@ public:
|
|||||||
static int findPayloadLength(const void* buffer, size_t length);
|
static int findPayloadLength(const void* buffer, size_t length);
|
||||||
};
|
};
|
||||||
|
|
||||||
#if defined(USE_RTPDUMP)
|
/**
|
||||||
|
* @brief Standard rtpdump file format (rtptools / Wireshark compatible)
|
||||||
|
*
|
||||||
|
* Conforms to the rtpdump format defined by rtptools:
|
||||||
|
* https://formats.kaitai.io/rtpdump/
|
||||||
|
*
|
||||||
|
* File layout:
|
||||||
|
* 1. Text header line:
|
||||||
|
* "#!rtpplay1.0 <source_ip>/<source_port>\n"
|
||||||
|
*
|
||||||
|
* 2. Binary file header (RD_hdr_t, 16 bytes, all big-endian):
|
||||||
|
* uint32_t start_sec - recording start time, seconds since epoch
|
||||||
|
* uint32_t start_usec - recording start time, microseconds
|
||||||
|
* uint32_t source_ip - source IP address (network byte order)
|
||||||
|
* uint16_t source_port - source port
|
||||||
|
* uint16_t padding - always 0
|
||||||
|
*
|
||||||
|
* 3. Packet records (repeated until EOF):
|
||||||
|
* Per-packet header (RD_packet_t, 8 bytes, all big-endian):
|
||||||
|
* uint16_t length - total record length (this 8-byte header + plen)
|
||||||
|
* uint16_t plen - RTP/RTCP payload length in bytes
|
||||||
|
* uint32_t offset - milliseconds since recording start
|
||||||
|
* Followed by plen bytes of RTP/RTCP packet data.
|
||||||
|
*
|
||||||
|
* Maximum single packet payload: 65535 bytes (enforced for safety).
|
||||||
|
*/
|
||||||
class RtpDump
|
class RtpDump
|
||||||
{
|
{
|
||||||
protected:
|
protected:
|
||||||
struct RtpData
|
struct RtpData
|
||||||
{
|
{
|
||||||
jrtplib::RTPPacket* mPacket;
|
std::shared_ptr<jrtplib::RTPPacket> mPacket;
|
||||||
void* mData;
|
std::vector<uint8_t> mRawData;
|
||||||
size_t mLength;
|
uint32_t mOffsetMs = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef std::vector<RtpData> PacketList;
|
typedef std::vector<RtpData> PacketList;
|
||||||
PacketList mPacketList;
|
PacketList mPacketList;
|
||||||
std::string mFilename;
|
std::string mFilename;
|
||||||
|
bool mLoaded = false;
|
||||||
|
|
||||||
|
// File header fields
|
||||||
|
uint32_t mSourceIp = 0;
|
||||||
|
uint16_t mSourcePort = 0;
|
||||||
|
uint32_t mStartSec = 0;
|
||||||
|
uint32_t mStartUsec = 0;
|
||||||
|
|
||||||
|
// Auto-compute packet offsets during recording
|
||||||
|
bool mRecording = false;
|
||||||
|
std::chrono::steady_clock::time_point mRecordStart;
|
||||||
|
|
||||||
|
std::shared_ptr<jrtplib::RTPPacket> parseRtpData(const uint8_t* data, size_t len);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
RtpDump(const char* filename);
|
explicit RtpDump(const char* filename);
|
||||||
~RtpDump();
|
~RtpDump();
|
||||||
|
|
||||||
|
/** Set source address for the file header (host byte order). */
|
||||||
|
void setSource(uint32_t ip, uint16_t port);
|
||||||
|
uint32_t sourceIp() const { return mSourceIp; }
|
||||||
|
uint16_t sourcePort() const { return mSourcePort; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Load packets from an rtpdump file
|
||||||
|
* @throws std::runtime_error on file/format error
|
||||||
|
*/
|
||||||
void load();
|
void load();
|
||||||
|
bool isLoaded() const { return mLoaded; }
|
||||||
|
|
||||||
size_t count() const;
|
size_t count() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get parsed RTP packet at index
|
||||||
|
* @throws std::out_of_range if index is invalid
|
||||||
|
* @throws std::runtime_error if packet could not be parsed as RTP
|
||||||
|
*/
|
||||||
jrtplib::RTPPacket& packetAt(size_t index);
|
jrtplib::RTPPacket& packetAt(size_t index);
|
||||||
|
|
||||||
|
/** @brief Get raw packet bytes at index */
|
||||||
|
const std::vector<uint8_t>& rawDataAt(size_t index) const;
|
||||||
|
|
||||||
|
/** @brief Get packet time offset in milliseconds */
|
||||||
|
uint32_t offsetAt(size_t index) const;
|
||||||
|
|
||||||
|
/** @brief Add a packet; time offset is auto-computed from first add() call */
|
||||||
void add(const void* data, size_t len);
|
void add(const void* data, size_t len);
|
||||||
|
|
||||||
|
/** @brief Add a packet with an explicit millisecond offset */
|
||||||
|
void add(const void* data, size_t len, uint32_t offsetMs);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Write all packets to file in rtpdump format
|
||||||
|
* @throws std::runtime_error on file error
|
||||||
|
*/
|
||||||
void flush();
|
void flush();
|
||||||
|
|
||||||
|
void clear();
|
||||||
|
const std::string& filename() const { return mFilename; }
|
||||||
};
|
};
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -280,7 +280,8 @@ size_t TimerQueue::cancel(uint64_t id) {
|
|||||||
//! Cancels all timers
|
//! Cancels all timers
|
||||||
// \return
|
// \return
|
||||||
// The number of timers cancelled
|
// The number of timers cancelled
|
||||||
size_t TimerQueue::cancelAll() {
|
size_t TimerQueue::cancelAll()
|
||||||
|
{
|
||||||
// Setting all "end" to 0 (for immediate execution) is ok,
|
// Setting all "end" to 0 (for immediate execution) is ok,
|
||||||
// since it maintains the heap integrity
|
// since it maintains the heap integrity
|
||||||
std::unique_lock<std::mutex> lk(m_mtx);
|
std::unique_lock<std::mutex> lk(m_mtx);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* Copyright(C) 2007-2016 VoIP objects (voipobjects.com)
|
/* Copyright(C) 2007-2026 VoIP objects (voipobjects.com)
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
* 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
|
* 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/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
@@ -6,10 +6,11 @@
|
|||||||
#ifndef __MT_CODEC_H
|
#ifndef __MT_CODEC_H
|
||||||
#define __MT_CODEC_H
|
#define __MT_CODEC_H
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
#include "resiprocate/resip/stack/SdpContents.hxx"
|
#include "resiprocate/resip/stack/SdpContents.hxx"
|
||||||
#include "../helper/HL_Types.h"
|
#include "../helper/HL_Types.h"
|
||||||
#include <map>
|
|
||||||
#include "../helper/HL_Pointer.h"
|
|
||||||
#include "../audio/Audio_Interface.h"
|
#include "../audio/Audio_Interface.h"
|
||||||
|
|
||||||
namespace MT
|
namespace MT
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ bool DtmfContext::getInband(int milliseconds, int rate, ByteBuffer& output)
|
|||||||
//
|
//
|
||||||
Dtmf& d = mQueue.front();
|
Dtmf& d = mQueue.front();
|
||||||
|
|
||||||
output.resize(milliseconds * rate / 1000 * 2);
|
output.resize((uint64_t)milliseconds * rate / 1000 * 2);
|
||||||
DtmfBuilder::buildInband(d.mTone, d.mCurrentTime, d.mCurrentTime + milliseconds, rate, (short*)output.mutableData());
|
DtmfBuilder::buildInband(d.mTone, d.mCurrentTime, d.mCurrentTime + milliseconds, rate, (short*)output.mutableData());
|
||||||
d.mCurrentTime += milliseconds;
|
d.mCurrentTime += milliseconds;
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -13,14 +13,12 @@
|
|||||||
using namespace MT;
|
using namespace MT;
|
||||||
|
|
||||||
SingleAudioStream::SingleAudioStream(const CodecList::Settings& codecSettings, Statistics& stat)
|
SingleAudioStream::SingleAudioStream(const CodecList::Settings& codecSettings, Statistics& stat)
|
||||||
:mReceiver(codecSettings, stat), mDtmfReceiver(stat)
|
:mReceiver(codecSettings, stat),
|
||||||
{
|
mDtmfReceiver(stat)
|
||||||
}
|
{}
|
||||||
|
|
||||||
SingleAudioStream::~SingleAudioStream()
|
SingleAudioStream::~SingleAudioStream()
|
||||||
{
|
{}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void SingleAudioStream::process(const std::shared_ptr<jrtplib::RTPPacket>& packet)
|
void SingleAudioStream::process(const std::shared_ptr<jrtplib::RTPPacket>& packet)
|
||||||
{
|
{
|
||||||
|
|||||||
6
test/rtp_decode/CMakeLists.txt
Normal file
6
test/rtp_decode/CMakeLists.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
set (CMAKE_CXX_STANDARD 20)
|
||||||
|
set (CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
add_executable(rtp_decode main.cpp)
|
||||||
|
add_subdirectory(../../src build_rtphone)
|
||||||
|
target_link_libraries(rtp_decode PRIVATE rtphone)
|
||||||
284
test/rtp_decode/main.cpp
Normal file
284
test/rtp_decode/main.cpp
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
// rtp_decode — read an rtpdump file, decode RTP with a given codec, write WAV.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// rtp_decode <input.rtp> <output.wav> --codec <name> [--pt <N>] [--rate <N>] [--channels <N>]
|
||||||
|
|
||||||
|
#include "helper/HL_Rtp.h"
|
||||||
|
#include "media/MT_CodecList.h"
|
||||||
|
#include "media/MT_Codec.h"
|
||||||
|
#include "audio/Audio_WavFile.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// CLI helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static void usage(const char* progname)
|
||||||
|
{
|
||||||
|
fprintf(stderr,
|
||||||
|
"Usage: %s <input.rtp> <output.wav> --codec <name> [--pt <N>] [--rate <N>] [--channels <N>]\n"
|
||||||
|
"\n"
|
||||||
|
"Codecs: pcmu pcma g722 g729 opus gsm gsmhr gsmefr amrnb amrwb evs ilbc20 ilbc30 isac16 isac32\n"
|
||||||
|
"\n"
|
||||||
|
"Options:\n"
|
||||||
|
" --codec <name> Codec name (required)\n"
|
||||||
|
" --pt <N> Override RTP payload type\n"
|
||||||
|
" --rate <N> Sample rate hint for Opus (default 48000)\n"
|
||||||
|
" --channels <N> Channel count hint for Opus (default 2)\n",
|
||||||
|
progname);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char* getOption(int argc, char* argv[], const char* name)
|
||||||
|
{
|
||||||
|
for (int i = 1; i < argc - 1; ++i) {
|
||||||
|
if (strcmp(argv[i], name) == 0)
|
||||||
|
return argv[i + 1];
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Default payload types for codecs without a fixed standard PT
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
struct CodecDefaults
|
||||||
|
{
|
||||||
|
const char* name;
|
||||||
|
int defaultPt; // -1 = must be specified via --pt
|
||||||
|
bool needsPt; // true if --pt is required when no default exists
|
||||||
|
};
|
||||||
|
|
||||||
|
static const CodecDefaults kCodecTable[] = {
|
||||||
|
{ "pcmu", 0, false },
|
||||||
|
{ "pcma", 8, false },
|
||||||
|
{ "g722", 9, false },
|
||||||
|
{ "g729", 18, false },
|
||||||
|
{ "gsm", 3, false },
|
||||||
|
{ "opus", 106, false },
|
||||||
|
{ "amrnb", -1, true },
|
||||||
|
{ "amrwb", -1, true },
|
||||||
|
{ "gsmhr", -1, true },
|
||||||
|
{ "gsmefr", 126, false },
|
||||||
|
{ "evs", 127, false },
|
||||||
|
{ "ilbc20", -1, true },
|
||||||
|
{ "ilbc30", -1, true },
|
||||||
|
{ "isac16", -1, true },
|
||||||
|
{ "isac32", -1, true },
|
||||||
|
};
|
||||||
|
|
||||||
|
static const CodecDefaults* findCodecDefaults(const std::string& name)
|
||||||
|
{
|
||||||
|
for (auto& c : kCodecTable)
|
||||||
|
if (name == c.name)
|
||||||
|
return &c;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Build CodecList::Settings for the requested codec
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static MT::CodecList::Settings buildSettings(const std::string& codecName, int pt,
|
||||||
|
int opusRate, int opusChannels)
|
||||||
|
{
|
||||||
|
MT::CodecList::Settings s;
|
||||||
|
|
||||||
|
if (codecName == "opus") {
|
||||||
|
s.mOpusSpec.push_back(MT::CodecList::Settings::OpusSpec(pt, opusRate, opusChannels));
|
||||||
|
} else if (codecName == "gsm") {
|
||||||
|
s.mGsmFrPayloadType = pt;
|
||||||
|
} else if (codecName == "gsmhr") {
|
||||||
|
s.mGsmHrPayloadType = pt;
|
||||||
|
} else if (codecName == "gsmefr") {
|
||||||
|
s.mGsmEfrPayloadType = pt;
|
||||||
|
} else if (codecName == "amrnb") {
|
||||||
|
s.mAmrNbOctetPayloadType.insert(pt);
|
||||||
|
} else if (codecName == "amrwb") {
|
||||||
|
s.mAmrWbOctetPayloadType.insert(pt);
|
||||||
|
} else if (codecName == "evs") {
|
||||||
|
MT::CodecList::Settings::EvsSpec ev;
|
||||||
|
ev.mPayloadType = pt;
|
||||||
|
s.mEvsSpec.push_back(ev);
|
||||||
|
} else if (codecName == "ilbc20") {
|
||||||
|
s.mIlbc20PayloadType = pt;
|
||||||
|
} else if (codecName == "ilbc30") {
|
||||||
|
s.mIlbc30PayloadType = pt;
|
||||||
|
} else if (codecName == "isac16") {
|
||||||
|
s.mIsac16KPayloadType = pt;
|
||||||
|
} else if (codecName == "isac32") {
|
||||||
|
s.mIsac32KPayloadType = pt;
|
||||||
|
}
|
||||||
|
// pcmu, pcma, g722, g729 — fixed PT, auto-registered by CodecList::init()
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// main
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
int main(int argc, char* argv[])
|
||||||
|
{
|
||||||
|
if (argc < 4) {
|
||||||
|
usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* inputPath = argv[1];
|
||||||
|
const char* outputPath = argv[2];
|
||||||
|
|
||||||
|
const char* codecArg = getOption(argc, argv, "--codec");
|
||||||
|
if (!codecArg) {
|
||||||
|
fprintf(stderr, "Error: --codec is required\n\n");
|
||||||
|
usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::string codecName = codecArg;
|
||||||
|
|
||||||
|
const auto* defaults = findCodecDefaults(codecName);
|
||||||
|
if (!defaults) {
|
||||||
|
fprintf(stderr, "Error: unknown codec '%s'\n\n", codecArg);
|
||||||
|
usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve payload type
|
||||||
|
int pt = defaults->defaultPt;
|
||||||
|
const char* ptArg = getOption(argc, argv, "--pt");
|
||||||
|
if (ptArg) {
|
||||||
|
pt = atoi(ptArg);
|
||||||
|
} else if (defaults->needsPt) {
|
||||||
|
fprintf(stderr, "Error: --pt is required for codec '%s'\n\n", codecArg);
|
||||||
|
usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int opusRate = 48000;
|
||||||
|
int opusChannels = 2;
|
||||||
|
const char* rateArg = getOption(argc, argv, "--rate");
|
||||||
|
if (rateArg)
|
||||||
|
opusRate = atoi(rateArg);
|
||||||
|
const char* chArg = getOption(argc, argv, "--channels");
|
||||||
|
if (chArg)
|
||||||
|
opusChannels = atoi(chArg);
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// 1. Load rtpdump
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
RtpDump dump(inputPath);
|
||||||
|
try {
|
||||||
|
dump.load();
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
fprintf(stderr, "Error loading rtpdump '%s': %s\n", inputPath, e.what());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dump.count() == 0) {
|
||||||
|
fprintf(stderr, "No packets in '%s'\n", inputPath);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
fprintf(stderr, "Loaded %zu packets from '%s'\n", dump.count(), inputPath);
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// 2. Create codec
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
auto settings = buildSettings(codecName, pt, opusRate, opusChannels);
|
||||||
|
MT::CodecList codecList(settings);
|
||||||
|
MT::PCodec codec = codecList.createCodecByPayloadType(pt);
|
||||||
|
if (!codec) {
|
||||||
|
fprintf(stderr, "Error: could not create codec for payload type %d\n", pt);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto codecInfo = codec->info();
|
||||||
|
fprintf(stderr, "Codec: %s samplerate=%d channels=%d pcmLength=%d frameTime=%dms\n",
|
||||||
|
codecInfo.mName.c_str(), codecInfo.mSamplerate, codecInfo.mChannels,
|
||||||
|
codecInfo.mPcmLength, codecInfo.mFrameTime);
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// 3. Open WAV writer
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
Audio::WavFileWriter writer;
|
||||||
|
if (!writer.open(outputPath, codecInfo.mSamplerate, codecInfo.mChannels)) {
|
||||||
|
fprintf(stderr, "Error: could not open WAV file '%s' for writing\n", outputPath);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// 4. Decode loop
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
std::vector<uint8_t> pcmBuffer(65536);
|
||||||
|
size_t totalDecodedBytes = 0;
|
||||||
|
size_t packetsDecoded = 0;
|
||||||
|
size_t packetsSkipped = 0;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < dump.count(); ++i) {
|
||||||
|
const auto& rawData = dump.rawDataAt(i);
|
||||||
|
|
||||||
|
// Verify it's actually RTP
|
||||||
|
if (!RtpHelper::isRtp(rawData.data(), rawData.size())) {
|
||||||
|
++packetsSkipped;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse RTP to get payload
|
||||||
|
jrtplib::RTPPacket& rtpPacket = dump.packetAt(i);
|
||||||
|
|
||||||
|
// Check payload type matches what we expect
|
||||||
|
int pktPt = rtpPacket.GetPayloadType();
|
||||||
|
if (pktPt != pt) {
|
||||||
|
++packetsSkipped;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* payloadData = rtpPacket.GetPayloadData();
|
||||||
|
size_t payloadLen = rtpPacket.GetPayloadLength();
|
||||||
|
|
||||||
|
if (!payloadData || payloadLen == 0) {
|
||||||
|
++packetsSkipped;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::span<const uint8_t> input(payloadData, payloadLen);
|
||||||
|
std::span<uint8_t> output(pcmBuffer.data(), pcmBuffer.size());
|
||||||
|
|
||||||
|
try {
|
||||||
|
auto result = codec->decode(input, output);
|
||||||
|
if (result.mDecoded > 0) {
|
||||||
|
writer.write(pcmBuffer.data(), result.mDecoded);
|
||||||
|
totalDecodedBytes += result.mDecoded;
|
||||||
|
++packetsDecoded;
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
fprintf(stderr, "Warning: decode error at packet %zu: %s\n", i, e.what());
|
||||||
|
++packetsSkipped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// 5. Close WAV and print summary
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
writer.close();
|
||||||
|
|
||||||
|
size_t totalSamples = totalDecodedBytes / (sizeof(int16_t) * codecInfo.mChannels);
|
||||||
|
double durationSec = (codecInfo.mSamplerate > 0)
|
||||||
|
? static_cast<double>(totalSamples) / codecInfo.mSamplerate
|
||||||
|
: 0.0;
|
||||||
|
|
||||||
|
fprintf(stderr, "\nDone.\n");
|
||||||
|
fprintf(stderr, " Packets decoded: %zu\n", packetsDecoded);
|
||||||
|
fprintf(stderr, " Packets skipped: %zu\n", packetsSkipped);
|
||||||
|
fprintf(stderr, " Decoded PCM: %zu bytes\n", totalDecodedBytes);
|
||||||
|
fprintf(stderr, " Duration: %.3f seconds\n", durationSec);
|
||||||
|
fprintf(stderr, " Output: %s\n", outputPath);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user