Browse Source

Add VP8 packetizer and depacketizer

Paul-Louis Ageneau 2 tháng trước cách đây
mục cha
commit
e615052a09

+ 4 - 0
CMakeLists.txt

@@ -94,6 +94,8 @@ set(LIBDATACHANNEL_SOURCES
 	${CMAKE_CURRENT_SOURCE_DIR}/src/h265rtpdepacketizer.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/h265nalunit.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/av1rtppacketizer.cpp
+	${CMAKE_CURRENT_SOURCE_DIR}/src/vp8rtppacketizer.cpp
+	${CMAKE_CURRENT_SOURCE_DIR}/src/vp8rtpdepacketizer.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/rtcpnackresponder.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/rtp.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/capi.cpp
@@ -135,6 +137,8 @@ set(LIBDATACHANNEL_HEADERS
 	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/h265rtpdepacketizer.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/h265nalunit.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/av1rtppacketizer.hpp
+	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/vp8rtppacketizer.hpp
+	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/vp8rtpdepacketizer.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/rtcpnackresponder.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/utils.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/plihandler.hpp

+ 76 - 17
examples/media-receiver/main.cpp

@@ -26,8 +26,65 @@
 typedef int SOCKET;
 #endif
 
+#include <fstream>
+
 using nlohmann::json;
 
+// Write 32-bit little-endian
+static void write_u32_le(std::ofstream &ofs, uint32_t v) {
+    char b[4];
+    b[0] = static_cast<char>(v & 0xFF);
+    b[1] = static_cast<char>((v >> 8) & 0xFF);
+    b[2] = static_cast<char>((v >> 16) & 0xFF);
+    b[3] = static_cast<char>((v >> 24) & 0xFF);
+    ofs.write(b, 4);
+}
+
+// Write 16-bit little-endian
+static void write_u16_le(std::ofstream &ofs, uint16_t v) {
+    char b[2];
+    b[0] = static_cast<char>(v & 0xFF);
+    b[1] = static_cast<char>((v >> 8) & 0xFF);
+    ofs.write(b, 2);
+}
+
+// Write IVF file header (32 bytes)
+static void write_ivf_file_header(std::ofstream &ofs,
+                                  const char codec[4],
+                                  uint16_t width,
+                                  uint16_t height,
+                                  uint32_t framerate_num,
+                                  uint32_t framerate_den,
+                                  uint32_t frame_count) {
+    // Signature 'DKIF'
+    ofs.write("DKIF", 4);
+    // Version (2 bytes) and header size (2 bytes) -> version 0, header size 32
+    write_u16_le(ofs, 0);
+    write_u16_le(ofs, 32);
+    // FourCC codec
+    ofs.write(codec, 4);
+    // Width, Height (2 bytes each)
+    write_u16_le(ofs, width);
+    write_u16_le(ofs, height);
+    // Framerate numerator and denominator (4 bytes each)
+    write_u32_le(ofs, framerate_num);
+    write_u32_le(ofs, framerate_den);
+    // Frame count (4 bytes)
+    write_u32_le(ofs, frame_count);
+    // Unused (4 bytes)
+    write_u32_le(ofs, 0);
+}
+
+// Write per-frame header (12 bytes): size (4), 64-bit timestamp (we'll use 4 bytes low + 4 bytes high)
+static void write_ivf_frame_header(std::ofstream &ofs, uint32_t frame_size, uint64_t timestamp) {
+    write_u32_le(ofs, frame_size);
+    // IVF uses a 64-bit timestamp; write low dword then high dword (little-endian)
+    uint32_t ts_low = static_cast<uint32_t>(timestamp & 0xFFFFFFFFu);
+    uint32_t ts_high = static_cast<uint32_t>((timestamp >> 32) & 0xFFFFFFFFu);
+    write_u32_le(ofs, ts_low);
+    write_u32_le(ofs, ts_high);
+}
+
 int main() {
 	try {
 		rtc::InitLogger(rtc::LogLevel::Debug);
@@ -46,33 +103,35 @@ int main() {
 			}
 		});
 
-		SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);
-		sockaddr_in addr = {};
-		addr.sin_family = AF_INET;
-		addr.sin_addr.s_addr = inet_addr("127.0.0.1");
-		addr.sin_port = htons(5000);
-
 		rtc::Description::Video media("video", rtc::Description::Direction::RecvOnly);
-		media.addH264Codec(96);
+		media.addVP8Codec(96);
 		media.setBitrate(
 		    3000); // Request 3Mbps (Browsers do not encode more than 2.5MBps from a webcam)
 
 		auto track = pc->addTrack(media);
 
-		auto session = std::make_shared<rtc::RtcpReceivingSession>();
-		track->setMediaHandler(session);
+		track->setMediaHandler(std::make_shared<rtc::VP8RtpDepacketizer>());
+		track->chainMediaHandler(std::make_shared<rtc::RtcpReceivingSession>());
+
+		std::ofstream ofs;
+		ofs.open("dump.ivf", std::ios_base::out | std::ios_base::trunc);
 
-		track->onMessage(
-		    [session, sock, addr](rtc::binary message) {
-			    // This is an RTP packet
-			    sendto(sock, reinterpret_cast<const char *>(message.data()), int(message.size()), 0,
-			           reinterpret_cast<const struct sockaddr *>(&addr), sizeof(addr));
-		    },
-		    nullptr);
+		// Codec FourCC for VP8 is "VP80"
+		const char codec[4] = { 'V','P','8','0' };
+
+		write_ivf_file_header(ofs, codec, 1280, 720, 30, 1, 1000);
+
+		int index = 0;
+		track->onFrame([&ofs, &index](rtc::binary frame, rtc::FrameInfo info) {
+			std::cout << "Got frame, size=" << frame.size() << ", timestamp=" << info.timestampSeconds->count() << std::endl;
+			write_ivf_frame_header(ofs, frame.size(), index);
+			ofs.write(reinterpret_cast<const char *>(frame.data()), frame.size());
+			ofs.flush();
+			++index;
+		});
 
 		pc->setLocalDescription();
 
-		std::cout << "Expect RTP video traffic on localhost:5000" << std::endl;
 		std::cout << "Please copy/paste the answer provided by the browser: " << std::endl;
 		std::string sdp;
 		std::getline(std::cin, sdp);

+ 1 - 0
include/rtc/rtc.h

@@ -388,6 +388,7 @@ RTC_C_EXPORT int rtcSetMediaInterceptorCallback(int id, rtcInterceptorCallbackFu
 RTC_C_EXPORT int rtcSetH264Packetizer(int tr, const rtcPacketizerInit *init);
 RTC_C_EXPORT int rtcSetH265Packetizer(int tr, const rtcPacketizerInit *init);
 RTC_C_EXPORT int rtcSetAV1Packetizer(int tr, const rtcPacketizerInit *init);
+RTC_C_EXPORT int rtcSetVP8Packetizer(int tr, const rtcPacketizerInit *init);
 RTC_C_EXPORT int rtcSetOpusPacketizer(int tr, const rtcPacketizerInit *init);
 RTC_C_EXPORT int rtcSetAACPacketizer(int tr, const rtcPacketizerInit *init);
 RTC_C_EXPORT int rtcSetPCMUPacketizer(int tr, const rtcPacketizerInit *init);

+ 5 - 3
include/rtc/rtc.hpp

@@ -14,9 +14,9 @@
 #include "global.hpp"
 //
 #include "datachannel.hpp"
+#include "iceudpmuxlistener.hpp"
 #include "peerconnection.hpp"
 #include "track.hpp"
-#include "iceudpmuxlistener.hpp"
 
 #if RTC_ENABLE_WEBSOCKET
 
@@ -31,10 +31,14 @@
 // Media
 #include "av1rtppacketizer.hpp"
 #include "dependencydescriptor.hpp"
+#include "rtppacketizer.hpp"
+#include "rtpdepacketizer.hpp"
 #include "h264rtppacketizer.hpp"
 #include "h264rtpdepacketizer.hpp"
 #include "h265rtppacketizer.hpp"
 #include "h265rtpdepacketizer.hpp"
+#include "vp8rtppacketizer.hpp"
+#include "vp8rtpdepacketizer.hpp"
 #include "mediahandler.hpp"
 #include "plihandler.hpp"
 #include "rembhandler.hpp"
@@ -42,7 +46,5 @@
 #include "rtcpnackresponder.hpp"
 #include "rtcpreceivingsession.hpp"
 #include "rtcpsrreporter.hpp"
-#include "rtppacketizer.hpp"
-#include "rtpdepacketizer.hpp"
 
 #endif // RTC_ENABLE_MEDIA

+ 1 - 1
include/rtc/rtppacketizer.hpp

@@ -42,7 +42,7 @@ public:
 protected:
 	/// Fragment data into payloads
 	/// Default implementation returns data as a single payload
-	/// @param message Input data
+	/// @param data Input data
 	virtual std::vector<binary> fragment(binary data);
 
 	/// Creates an RTP packet for a payload

+ 34 - 0
include/rtc/vp8rtpdepacketizer.hpp

@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2026 Paul-Louis Ageneau
+ *
+ * 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 https://mozilla.org/MPL/2.0/.
+ */
+
+#ifndef RTC_VP8_RTP_DEPACKETIZER_H
+#define RTC_VP8_RTP_DEPACKETIZER_H
+
+#if RTC_ENABLE_MEDIA
+
+#include "common.hpp"
+#include "message.hpp"
+#include "rtpdepacketizer.hpp"
+
+namespace rtc {
+
+/// RTP depacketization for VP8
+class RTC_CPP_EXPORT VP8RtpDepacketizer final : public VideoRtpDepacketizer {
+public:
+	VP8RtpDepacketizer();
+	~VP8RtpDepacketizer();
+
+private:
+	message_ptr reassemble(message_buffer &buffer) override;
+};
+
+} // namespace rtc
+
+#endif // RTC_ENABLE_MEDIA
+
+#endif /* RTC_VP8_RTP_DEPACKETIZER_H */

+ 42 - 0
include/rtc/vp8rtppacketizer.hpp

@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2026 Paul-Louis Ageneau
+ *
+ * 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 https://mozilla.org/MPL/2.0/.
+ */
+
+#ifndef RTC_VP8_RTP_PACKETIZER_H
+#define RTC_VP8_RTP_PACKETIZER_H
+
+#if RTC_ENABLE_MEDIA
+
+#include "rtppacketizer.hpp"
+
+namespace rtc {
+
+/// RTP packetization for VP8
+class RTC_CPP_EXPORT VP8RtpPacketizer final : public RtpPacketizer {
+public:
+	inline static const uint32_t ClockRate = VideoClockRate;
+	[[deprecated("Use ClockRate")]] inline static const uint32_t defaultClockRate = ClockRate;
+
+	/// Constructs VP8 payload packetizer with given RTP configuration.
+	/// @note RTP configuration is used in packetization process which may change some configuration
+	/// properties such as sequence number.
+	/// @param rtpConfig RTP configuration
+	/// @param maxFragmentSize maximum size of one packet payload
+	VP8RtpPacketizer(shared_ptr<RtpPacketizationConfig> rtpConfig,
+	                 size_t maxFragmentSize = DefaultMaxFragmentSize);
+
+private:
+	std::vector<binary> fragment(binary frame) override;
+
+	const size_t mMaxFragmentSize;
+};
+
+} // namespace rtc
+
+#endif /* RTC_ENABLE_MEDIA */
+
+#endif /* RTC_VP8_RTP_PACKETIZER_H */

+ 29 - 12
src/capi.cpp

@@ -935,7 +935,8 @@ int rtcCreateDataChannelEx(int pc, const char *label, const rtcDataChannelInit *
 			dci.reliability.unordered = reliability->unordered;
 			if (reliability->unreliable) {
 				if (reliability->maxPacketLifeTime > 0)
-					dci.reliability.maxPacketLifeTime.emplace(milliseconds(reliability->maxPacketLifeTime));
+					dci.reliability.maxPacketLifeTime.emplace(
+					    milliseconds(reliability->maxPacketLifeTime));
 				else
 					dci.reliability.maxRetransmits.emplace(reliability->maxRetransmits);
 			}
@@ -999,9 +1000,10 @@ int rtcGetDataChannelReliability(int dc, rtcReliability *reliability) {
 		Reliability dcr = dataChannel->reliability();
 		std::memset(reliability, 0, sizeof(*reliability));
 		reliability->unordered = dcr.unordered;
-		if(dcr.maxPacketLifeTime) {
+		if (dcr.maxPacketLifeTime) {
 			reliability->unreliable = true;
-			reliability->maxPacketLifeTime = static_cast<unsigned int>(dcr.maxPacketLifeTime->count());
+			reliability->maxPacketLifeTime =
+			    static_cast<unsigned int>(dcr.maxPacketLifeTime->count());
 		} else if (dcr.maxRetransmits) {
 			reliability->unreliable = true;
 			reliability->maxRetransmits = *dcr.maxRetransmits;
@@ -1256,8 +1258,8 @@ int rtcSetH264Packetizer(int tr, const rtcPacketizerInit *init) {
 		emplaceRtpConfig(rtpConfig, tr);
 		// create packetizer
 		auto nalSeparator = init ? init->nalSeparator : RTC_NAL_SEPARATOR_LENGTH;
-		auto maxFragmentSize = init && init->maxFragmentSize ? init->maxFragmentSize
-		                                                     : RTC_DEFAULT_MAX_FRAGMENT_SIZE;
+		auto maxFragmentSize =
+		    init && init->maxFragmentSize ? init->maxFragmentSize : RTC_DEFAULT_MAX_FRAGMENT_SIZE;
 		auto packetizer = std::make_shared<H264RtpPacketizer>(
 		    static_cast<rtc::NalUnit::Separator>(nalSeparator), rtpConfig, maxFragmentSize);
 		track->setMediaHandler(packetizer);
@@ -1273,8 +1275,8 @@ int rtcSetH265Packetizer(int tr, const rtcPacketizerInit *init) {
 		emplaceRtpConfig(rtpConfig, tr);
 		// create packetizer
 		auto nalSeparator = init ? init->nalSeparator : RTC_NAL_SEPARATOR_LENGTH;
-		auto maxFragmentSize = init && init->maxFragmentSize ? init->maxFragmentSize
-		                                                     : RTC_DEFAULT_MAX_FRAGMENT_SIZE;
+		auto maxFragmentSize =
+		    init && init->maxFragmentSize ? init->maxFragmentSize : RTC_DEFAULT_MAX_FRAGMENT_SIZE;
 		auto packetizer = std::make_shared<H265RtpPacketizer>(
 		    static_cast<rtc::NalUnit::Separator>(nalSeparator), rtpConfig, maxFragmentSize);
 		track->setMediaHandler(packetizer);
@@ -1289,8 +1291,8 @@ int rtcSetAV1Packetizer(int tr, const rtcPacketizerInit *init) {
 		auto rtpConfig = createRtpPacketizationConfig(init);
 		emplaceRtpConfig(rtpConfig, tr);
 		// create packetizer
-		auto maxFragmentSize = init && init->maxFragmentSize ? init->maxFragmentSize
-		                                                     : RTC_DEFAULT_MAX_FRAGMENT_SIZE;
+		auto maxFragmentSize =
+		    init && init->maxFragmentSize ? init->maxFragmentSize : RTC_DEFAULT_MAX_FRAGMENT_SIZE;
 		auto packetization = init->obuPacketization == RTC_OBU_PACKETIZED_TEMPORAL_UNIT
 		                         ? AV1RtpPacketizer::Packetization::TemporalUnit
 		                         : AV1RtpPacketizer::Packetization::Obu;
@@ -1301,6 +1303,21 @@ int rtcSetAV1Packetizer(int tr, const rtcPacketizerInit *init) {
 	});
 }
 
+int rtcSetVP8Packetizer(int tr, const rtcPacketizerInit *init) {
+	return wrap([&] {
+		auto track = getTrack(tr);
+		// create RTP configuration
+		auto rtpConfig = createRtpPacketizationConfig(init);
+		emplaceRtpConfig(rtpConfig, tr);
+		// create packetizer
+		auto maxFragmentSize =
+		    init && init->maxFragmentSize ? init->maxFragmentSize : RTC_DEFAULT_MAX_FRAGMENT_SIZE;
+		auto packetizer = std::make_shared<VP8RtpPacketizer>(rtpConfig, maxFragmentSize);
+		track->setMediaHandler(packetizer);
+		return RTC_ERR_SUCCESS;
+	});
+}
+
 int rtcSetOpusPacketizer(int tr, const rtcPacketizerInit *init) {
 	return wrap([&] {
 		auto track = getTrack(tr);
@@ -1592,7 +1609,7 @@ int rtcCreateWebSocketEx(const char *url, const rtcWsConfiguration *config) {
 		else if (config->maxOutstandingPings < 0)
 			c.maxOutstandingPings = 0; // setting to 0 disables, not setting keeps default
 
-		if(config->maxMessageSize > 0)
+		if (config->maxMessageSize > 0)
 			c.maxMessageSize = size_t(config->maxMessageSize);
 
 		auto webSocket = std::make_shared<WebSocket>(std::move(c));
@@ -1632,7 +1649,7 @@ int rtcGetWebSocketPath(int ws, char *buffer, int size) {
 }
 
 int rtcCreateWebSocketServer(const rtcWsServerConfiguration *config,
-                                          rtcWebSocketClientCallbackFunc cb) {
+                             rtcWebSocketClientCallbackFunc cb) {
 	return wrap([&] {
 		if (!config)
 			throw std::invalid_argument("Unexpected null pointer for config");
@@ -1650,7 +1667,7 @@ int rtcCreateWebSocketServer(const rtcWsServerConfiguration *config,
 		c.keyPemPass = config->keyPemPass ? make_optional(string(config->keyPemPass)) : nullopt;
 		c.bindAddress = config->bindAddress ? make_optional(string(config->bindAddress)) : nullopt;
 
-		if(config->maxMessageSize > 0)
+		if (config->maxMessageSize > 0)
 			c.maxMessageSize = size_t(config->maxMessageSize);
 
 		auto webSocketServer = std::make_shared<WebSocketServer>(std::move(c));

+ 176 - 0
src/vp8rtpdepacketizer.cpp

@@ -0,0 +1,176 @@
+/**
+ * Copyright (c) 2026 Paul-Louis Ageneau
+ *
+ * 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 https://mozilla.org/MPL/2.0/.
+ */
+
+#if RTC_ENABLE_MEDIA
+
+#include "vp8rtpdepacketizer.hpp"
+#include "rtp.hpp"
+
+namespace rtc {
+
+VP8RtpDepacketizer::VP8RtpDepacketizer() {}
+
+VP8RtpDepacketizer::~VP8RtpDepacketizer() {}
+
+message_ptr VP8RtpDepacketizer::reassemble(message_buffer &buffer) {
+	/*
+	 * Implements the recommended partition reconstruction algorithm in RFC 7741
+	 * See https://datatracker.ietf.org/doc/html/rfc7741#section-4.5.2
+	 *
+	 * VP8 payload descriptor (RFC 7741)
+	 * See https://www.rfc-editor.org/rfc/rfc7741.html#section-4.2
+	 *
+	 *      0 1 2 3 4 5 6 7
+	 *     +-+-+-+-+-+-+-+-+
+	 *     |X|R|N|S|R| PID | (REQUIRED)
+	 *     +-+-+-+-+-+-+-+-+
+	 *  X: |I|L|T|K| RSV   | (OPTIONAL)
+	 *     +-+-+-+-+-+-+-+-+
+	 *  I: |M| PictureID   | (OPTIONAL)
+	 *     +-+-+-+-+-+-+-+-+
+	 *  L: |   TL0PICIDX   | (OPTIONAL)
+	 *     +-+-+-+-+-+-+-+-+
+	 * T/K:|TID|Y| KEYIDX  | (OPTIONAL)
+	 *     +-+-+-+-+-+-+-+-+
+	 *
+	 * X: Extended control bits present
+	 * R: Reserved (MUST be set to 0 and ignored by receiver)
+	 * N: Non-reference frame
+	 * S: Start of VP8 partition (1 for first fragment, 0 otherwise)
+	 * PID: Partition index
+	 * I: PictureID present
+	 * L: TL0PICIDX present
+	 * T: TID present
+	 * K: KEYIDX present
+	 * RSV: Reserved (MUST be set to 0 and ignored by receiver)
+	 * M: PictureID 15-bit extension flag
+	 */
+
+	// First byte
+	const uint8_t X = 0b10000000;
+	//const uint8_t N = 0b00100000;
+	const uint8_t S = 0b00010000;
+
+	// Extension byte
+	const uint8_t I = 0b10000000;
+	const uint8_t L = 0b01000000;
+	const uint8_t T = 0b00100000;
+	const uint8_t K = 0b00010000;
+
+	// PictureID byte
+	const uint8_t M = 0b10000000;
+
+	if (buffer.empty())
+		return nullptr;
+
+	auto first = *buffer.begin();
+	auto firstRtpHeader = reinterpret_cast<const RtpHeader *>(first->data());
+	uint8_t payloadType = firstRtpHeader->payloadType();
+	uint32_t timestamp = firstRtpHeader->timestamp();
+	uint16_t nextSeqNumber = firstRtpHeader->seqNumber();
+
+	binary frame;
+	std::vector<std::pair<const std::byte*, size_t>> payloads;
+	bool continuousSequence = false;
+	for (const auto &packet : buffer) {
+		auto rtpHeader = reinterpret_cast<const rtc::RtpHeader *>(packet->data());
+		if (rtpHeader->seqNumber() < nextSeqNumber) {
+			// Skip
+			continue;
+		}
+		if (rtpHeader->seqNumber() > nextSeqNumber) {
+			// Missing packet(s)
+			continuousSequence = false;
+		}
+		nextSeqNumber = rtpHeader->seqNumber() + 1;
+
+		auto rtpHeaderSize = rtpHeader->getSize() + rtpHeader->getExtensionHeaderSize();
+		auto paddingSize = 0;
+		if (rtpHeader->padding())
+			paddingSize = std::to_integer<uint8_t>(packet->back());
+
+		if (packet->size() <= rtpHeaderSize + paddingSize)
+			continue; // Empty payload
+
+		const std::byte *payloadData = packet->data() + rtpHeaderSize;
+		size_t payloadSize = packet->size() - rtpHeaderSize - paddingSize;
+
+		if (payloadSize < 1)
+			continue;
+
+		size_t descriptorSize = 1;
+		uint8_t firstByte = std::to_integer<uint8_t>(payloadData[0]);
+
+		if (firstByte & X) {
+			if (payloadSize < descriptorSize + 1)
+				continue;
+
+			uint8_t extensionByte = std::to_integer<uint8_t>(payloadData[descriptorSize]);
+			descriptorSize++;
+
+			if (extensionByte & I) {
+				if (payloadSize < descriptorSize + 1)
+					continue;
+				uint8_t pictureIdByte = std::to_integer<uint8_t>(payloadData[descriptorSize]);
+				descriptorSize++;
+				if (pictureIdByte & M) { // M bit, 15-bit PictureID
+					if (payloadSize < descriptorSize + 1)
+						continue;
+					descriptorSize++;
+				}
+			}
+
+			if (extensionByte & L) {
+				if (payloadSize < descriptorSize + 1)
+					continue;
+				descriptorSize++;
+			}
+
+			if ((extensionByte & T) || (extensionByte & K)) {
+				if (payloadSize < descriptorSize + 1)
+					continue;
+				descriptorSize++;
+			}
+		}
+
+		if (payloadSize < descriptorSize)
+			continue;
+
+		payloadData += descriptorSize;
+		payloadSize -= descriptorSize;
+
+		if (firstByte & S || rtpHeader->marker()) {
+			if (continuousSequence) {
+				// Sequence is continuous, append to frame
+				for (auto [data, size] : payloads)
+					frame.insert(frame.end(), data, data + size);
+
+				if (rtpHeader->marker()) {
+					// Add current payload too
+					frame.insert(frame.end(), payloadData, payloadData + payloadSize);
+				}
+			}
+			payloads.clear();
+			continuousSequence = true;
+		}
+
+		if (!rtpHeader->marker())
+			payloads.push_back(std::make_pair(payloadData, payloadSize));
+	}
+
+	if(frame.empty()) {
+		// No partition was recoverable
+		return nullptr;
+	}
+
+	return make_message(std::move(frame), createFrameInfo(timestamp, payloadType));
+}
+
+} // namespace rtc
+
+#endif // RTC_ENABLE_MEDIA

+ 121 - 0
src/vp8rtppacketizer.cpp

@@ -0,0 +1,121 @@
+/**
+ * Copyright (c) 2026 Paul-Louis Ageneau
+ *
+ * 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 https://mozilla.org/MPL/2.0/.
+ */
+
+#if RTC_ENABLE_MEDIA
+
+#include "vp8rtppacketizer.hpp"
+
+#include <cstring>
+
+namespace rtc {
+
+VP8RtpPacketizer::VP8RtpPacketizer(shared_ptr<RtpPacketizationConfig> rtpConfig,
+		size_t maxFragmentSize)
+	: RtpPacketizer(std::move(rtpConfig)), mMaxFragmentSize(maxFragmentSize) {}
+
+std::vector<binary> VP8RtpPacketizer::fragment(binary frame) {
+	/*
+	 * VP8 payload descriptor (RFC 7741)
+	 * See https://www.rfc-editor.org/rfc/rfc7741.html#section-4.2
+	 *
+	 *      0 1 2 3 4 5 6 7
+	 *     +-+-+-+-+-+-+-+-+
+	 *     |X|R|N|S|R| PID | (REQUIRED)
+	 *     +-+-+-+-+-+-+-+-+
+	 *  X: |I|L|T|K| RSV   | (OPTIONAL)
+	 *     +-+-+-+-+-+-+-+-+
+	 *  I: |M| PictureID   | (OPTIONAL)
+	 *     +-+-+-+-+-+-+-+-+
+	 *  L: |   TL0PICIDX   | (OPTIONAL)
+	 *     +-+-+-+-+-+-+-+-+
+	 * T/K:|TID|Y| KEYIDX  | (OPTIONAL)
+	 *     +-+-+-+-+-+-+-+-+
+	 *
+	 * X: Extended control bits present
+	 * R: Reserved (MUST be set to 0)
+	 * N: Non-reference frame
+	 * S: Start of VP8 partition (1 for first fragment, 0 otherwise)
+	 * PID: Partition index
+	 * I: PictureID present
+	 * L: TL0PICIDX present
+	 * T: TID present
+	 * K: KEYIDX present
+	 * M: PictureID 15-bit extension flag
+	 */
+
+	// First descriptor byte
+	const uint8_t N = 0b00100000;
+	const uint8_t S = 0b00010000;
+
+	/*
+	 * The beginning of an encoded VP8 frame is referred to as an "uncompressed data chunk"
+	 * in RFC6386 and co-serve as payload header in this RTP format. The codec bitstream
+	 * format specifies two different variants of the uncompressed data chunk: a 3 octet
+	 * version for interframes and a 10 octet version for key frames. The first 3 octets
+	 * are common to both variants.
+	 * See https://datatracker.ietf.org/doc/html/draft-ietf-payload-vp8-08#section-4.3
+	 *
+	 *  0 1 2 3 4 5 6 7
+	 * +-+-+-+-+-+-+-+-+
+	 * |Size0|H| VER |P|
+	 * +-+-+-+-+-+-+-+-+
+	 * |     Size1     |
+	 * +-+-+-+-+-+-+-+-+
+	 * |     Size2     |
+	 * +-+-+-+-+-+-+-+-+
+	 *
+	 * H: Show frame bit as defined in RFC6386.
+	 * VER: A version number as defined in RFC6386.
+	 * P: Inverse key frame flag.  When set to 0 the current frame is a key frame.
+	 *    When set to 1 the current frame is an interframe.
+	 * SizeN: The size of the first partition size in bytes is calculated
+	 *        from the 19 bits in Size0, Size1, and Size2 as 1stPartitionSize =
+	 *        Size0 + 8 * Size1 + 2048 * Size2.
+	 */
+
+	// First frame byte
+	const uint8_t P = 0b00000001;
+
+	if (frame.size() < 3)
+		return {};
+
+	const bool isKeyframe = (std::to_integer<uint8_t>(frame[0]) & P) == 0;
+
+	const size_t descriptorSize = 1;
+	if (mMaxFragmentSize <= descriptorSize)
+		return {};
+
+	std::vector<binary> payloads;
+	size_t index = 0;
+	while (index < frame.size()) {
+		size_t remaining = frame.size() - index;
+		size_t payloadSize = std::min(mMaxFragmentSize - descriptorSize, remaining);
+
+		binary payload(descriptorSize + payloadSize);
+
+		// Set 1-byte payload descriptor
+		uint8_t descriptor = 0;
+		if (!isKeyframe)
+			descriptor |= N;
+		if (index == 0)
+			descriptor |= S;
+		payload[0] = std::byte(descriptor);
+
+		// Copy data
+		std::memcpy(payload.data() + descriptorSize, frame.data() + index, payloadSize);
+
+		payloads.push_back(std::move(payload));
+		index += payloadSize;
+	}
+
+	return payloads;
+}
+
+} // namespace rtc
+
+#endif /* RTC_ENABLE_MEDIA */