From 18c1a99cb02cacea2516b9cb587c8c50905f24f3 Mon Sep 17 00:00:00 2001 From: YuLi Date: Fri, 24 Jul 2026 16:08:19 -0700 Subject: [PATCH] test VP9 RTP parsing through decoder --- ext-codec/VP9Rtp.cpp | 148 +++++++++++++++++++++-------------------- tests/test_vp9_rtp.cpp | 98 +++++++++++++++++++++++++++ 2 files changed, 175 insertions(+), 71 deletions(-) create mode 100644 tests/test_vp9_rtp.cpp diff --git a/ext-codec/VP9Rtp.cpp b/ext-codec/VP9Rtp.cpp index 33445e87..d90e9511 100644 --- a/ext-codec/VP9Rtp.cpp +++ b/ext-codec/VP9Rtp.cpp @@ -46,11 +46,12 @@ struct RTPPayloadVP9 { bool hasGof = false; int numberOfFramesInGof = -1; std::vector resolutions; - int parse(unsigned char* data, int dataLength); + + int parse(const unsigned char *data, int dataLength); bool keyFrame() const { return beginningOfLayerFrame && !interPicturePrediction; } - std::string dump() const { + std::string dump() const { char line[64] = {0}; - snprintf(line, sizeof(line), "%c%c%c%c%c%c%c- %d %d, %d %d", + snprintf(line, sizeof(line), "%c%c%c%c%c%c%c- %d %d, %d %d", hasPictureID ? 'I' : ' ', interPicturePrediction ? 'P' : ' ', hasLayerIndices ? 'L' : ' ', @@ -107,102 +108,107 @@ struct RTPPayloadVP9 { #define kBBit 0x08 #define kEBit 0x04 #define kVBit 0x02 -int RTPPayloadVP9::parse(unsigned char *data, int dataLength) { - const unsigned char* dataPtr = data; - const unsigned char* dataEnd = data + dataLength; - -#define VP9_CHECK_BOUNDS(n) do { if (dataPtr + (n) > dataEnd) return -1; } while (0) +int RTPPayloadVP9::parse(const unsigned char *data, int dataLength) { + if (!data || dataLength <= 0) { + return -1; + } + const unsigned char *dataPtr = data; + size_t remaining = static_cast(dataLength); + auto readByte = [&]() -> int { + if (remaining < 1) { + return -1; + } + --remaining; + return *dataPtr++; + }; + auto skipBytes = [&](size_t count) -> bool { + if (remaining < count) { + return false; + } + remaining -= count; + dataPtr += count; + return true; + }; // Parse mandatory first byte of payload descriptor - VP9_CHECK_BOUNDS(1); - this->hasPictureID = (*dataPtr & kIBit); // I bit - this->interPicturePrediction = (*dataPtr & kPBit); // P bit - this->hasLayerIndices = (*dataPtr & kLBit); // L bit - this->flexibleMode = (*dataPtr & kFBit); // F bit - this->beginningOfLayerFrame = (*dataPtr & kBBit); // B bit - this->endingOfLayerFrame = (*dataPtr & kEBit); // E bit - this->hasScalabilityStructure = (*dataPtr & kVBit); // V bit - dataPtr++; + int byte = readByte(); + if (byte < 0) return -1; + this->hasPictureID = (byte & kIBit); // I bit + this->interPicturePrediction = (byte & kPBit); // P bit + this->hasLayerIndices = (byte & kLBit); // L bit + this->flexibleMode = (byte & kFBit); // F bit + this->beginningOfLayerFrame = (byte & kBBit); // B bit + this->endingOfLayerFrame = (byte & kEBit); // E bit + this->hasScalabilityStructure = (byte & kVBit); // V bit if (this->hasPictureID) { - VP9_CHECK_BOUNDS(1); - this->largePictureID = (*dataPtr & 0x80); // M bit - this->pictureID = (*dataPtr & 0x7F); + byte = readByte(); + if (byte < 0) return -1; + this->largePictureID = (byte & 0x80); // M bit + this->pictureID = (byte & 0x7F); if (this->largePictureID) { - dataPtr++; - VP9_CHECK_BOUNDS(1); - this->pictureID = ntohs((this->pictureID << 16) + (*dataPtr & 0xFF)); + byte = readByte(); + if (byte < 0) return -1; + this->pictureID = (this->pictureID << 8) | byte; } - dataPtr++; } if (this->hasLayerIndices) { - VP9_CHECK_BOUNDS(1); - this->temporalID = (*dataPtr & 0xE0) >> 5; // T bits - this->isSwitchingUp = (*dataPtr & 0x10); // U bit - this->spatialID = (*dataPtr & 0x0E) >> 1; // S bits - this->isInterLayeredDepUsed = (*dataPtr & 0x01); // D bit - if (this->flexibleMode) { // marked in webrtc code - do { - dataPtr++; - VP9_CHECK_BOUNDS(1); - this->referenceIdx = (*dataPtr & 0xFE) >> 1; - this->additionalReferenceIdx = (*dataPtr & 0x01); // D bit - } while (this->additionalReferenceIdx); - } else { - dataPtr++; - VP9_CHECK_BOUNDS(1); - this->tl0PicIdx = (*dataPtr & 0xFF); + byte = readByte(); + if (byte < 0) return -1; + this->temporalID = (byte & 0xE0) >> 5; // T bits + this->isSwitchingUp = (byte & 0x10); // U bit + this->spatialID = (byte & 0x0E) >> 1; // S bits + this->isInterLayeredDepUsed = (byte & 0x01); // D bit + if (!this->flexibleMode) { + byte = readByte(); + if (byte < 0) return -1; + this->tl0PicIdx = byte; } - dataPtr++; } if (this->flexibleMode && this->interPicturePrediction) { - /* Skip reference indices */ - uint8_t nbit; + // The VP9 payload descriptor permits at most three reference indices. + bool nbit; + int referenceCount = 0; do { - VP9_CHECK_BOUNDS(1); - uint8_t p_diff = (*dataPtr & 0xFE) >> 1; - nbit = (*dataPtr & 0x01); - dataPtr++; + if (++referenceCount > 3) return -1; + byte = readByte(); + if (byte < 0) return -1; + this->referenceIdx = (byte & 0xFE) >> 1; + nbit = (byte & 0x01); + this->additionalReferenceIdx = nbit; } while (nbit); } if (this->hasScalabilityStructure) { - VP9_CHECK_BOUNDS(1); - this->spatialLayers = (*dataPtr & 0xE0) >> 5; // N_S bits - this->hasResolution = (*dataPtr & 0x10); // Y bit - this->hasGof = (*dataPtr & 0x08); // G bit - dataPtr++; + byte = readByte(); + if (byte < 0) return -1; + this->spatialLayers = (byte & 0xE0) >> 5; // N_S bits + this->hasResolution = (byte & 0x10); // Y bit + this->hasGof = (byte & 0x08); // G bit if (this->hasResolution) { for (int i = 0; i <= this->spatialLayers; i++) { - VP9_CHECK_BOUNDS(4); + if (remaining < 4) return -1; int width = (dataPtr[0] << 8) + dataPtr[1]; - dataPtr += 2; - int height = (dataPtr[0] << 8) + dataPtr[1]; - dataPtr += 2; - // InfoL << "got vp9 " << width << "x" << height; + int height = (dataPtr[2] << 8) + dataPtr[3]; this->resolutions.push_back({ width, height }); + skipBytes(4); } } if (this->hasGof) { - VP9_CHECK_BOUNDS(1); - this->numberOfFramesInGof = *dataPtr & 0xFF; // N_G bits - dataPtr++; + byte = readByte(); + if (byte < 0) return -1; + this->numberOfFramesInGof = byte; // N_G bits for (int frame_index = 0; frame_index < this->numberOfFramesInGof; frame_index++) { // TODO(javierc): Read these values if needed - VP9_CHECK_BOUNDS(1); - int reference_indices = (*dataPtr & 0x0C) >> 2; // R bits - dataPtr++; - VP9_CHECK_BOUNDS(reference_indices); - for (int reference_index = 0; reference_index < reference_indices; reference_index++) { - dataPtr++; - } + byte = readByte(); + if (byte < 0) return -1; + int reference_indices = (byte & 0x0C) >> 2; // R bits + if (!skipBytes(reference_indices)) return -1; } } } - -#undef VP9_CHECK_BOUNDS - + if (remaining == 0) return -1; return dataPtr - data; } @@ -239,7 +245,7 @@ bool VP9RtpDecoder::decodeRtp(const RtpPacket::Ptr &rtp) { RTPPayloadVP9 info; int offset = info.parse(payload, payload_size); - if (offset < 0) { + if (offset < 0 || static_cast(offset) >= payload_size) { WarnL << "VP9 RTP payload parse failed, seq:" << seq; return false; } diff --git a/tests/test_vp9_rtp.cpp b/tests/test_vp9_rtp.cpp new file mode 100644 index 00000000..c4242da6 --- /dev/null +++ b/tests/test_vp9_rtp.cpp @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2016-present The ZLMediaKit project authors. All Rights Reserved. + * + * This file is part of ZLMediaKit(https://github.com/ZLMediaKit/ZLMediaKit). + * + * Use of this source code is governed by MIT-like license that can be found in the + * LICENSE file in the root of the source tree. All contributing project authors + * may be found in the AUTHORS file in the root of the source tree. + */ + +#include +#include +#include +#include + +#include "ext-codec/VP9Rtp.h" + +using namespace mediakit; + +namespace { + +struct ParseCase { + const char *name; + std::vector packet; + int expected_offset; +}; + +void require(bool condition, const std::string &message) { + if (!condition) { + std::cerr << message << std::endl; + std::exit(1); + } +} + +void runCase(const ParseCase &test) { + VP9RtpDecoder decoder; + std::vector frames; + decoder.addDelegate([&](const Frame::Ptr &frame) { + frames.emplace_back(frame->data(), frame->size()); + return true; + }); + + auto packet = test.packet; + packet[0] |= 0x0C; // B/E: make a successful descriptor dispatch one complete frame. + RtpInfo rtp_info(0x12345678, 1400, 90000, 98, 0, 0); + auto rtp = rtp_info.makeRtp(TrackVideo, packet.data(), packet.size(), true, 100); + require(rtp != nullptr, std::string(test.name) + ": failed to create RTP packet"); + decoder.inputRtp(rtp, false); + + const bool should_output = test.expected_offset >= 0; + require(frames.size() == (should_output ? 1U : 0U), + std::string(test.name) + ": unexpected decoded frame count " + std::to_string(frames.size())); + if (should_output) { + require(frames[0].size() == packet.size() - static_cast(test.expected_offset), + std::string(test.name) + ": decoded frame has an unexpected size"); + require(frames[0] == std::string(reinterpret_cast(packet.data() + test.expected_offset), + packet.size() - static_cast(test.expected_offset)), + std::string(test.name) + ": decoded frame payload does not match RTP frame payload"); + } +} + +} // namespace + +int main() { + // The last byte in every successful case is frame payload. These cases cover + // all L/F/P combinations and make incorrect descriptor consumption visible. + const std::vector combinations = { + { "L0 F0 P0", { 0x00, 0xAA }, 1 }, + { "L0 F0 P1", { 0x40, 0xAA }, 1 }, + { "L0 F1 P0", { 0x10, 0xAA }, 1 }, + { "L0 F1 P1", { 0x50, 0x02, 0xAA }, 2 }, + { "L1 F0 P0", { 0x20, 0x00, 0x33, 0xAA }, 3 }, + { "L1 F0 P1", { 0x60, 0x00, 0x33, 0xAA }, 3 }, + { "L1 F1 P0 does not consume P_DIFF", { 0x30, 0x00, 0xAA }, 2 }, + { "L1 F1 P1 consumes P_DIFF once", { 0x70, 0x00, 0x02, 0xAA }, 3 }, + }; + for (const auto &test : combinations) { + runCase(test); + } + + runCase({ "three references", { 0x50, 0x03, 0x05, 0x06, 0xAA }, 4 }); + runCase({ "truncated reference chain", { 0x50, 0x03 }, -1 }); + runCase({ "more than three references", { 0x50, 0x03, 0x05, 0x07, 0x08, 0xAA }, -1 }); + runCase({ "short picture ID", { 0x80, 0x12, 0xAA }, 2 }); + runCase({ "large picture ID", { 0x80, 0x81, 0x23, 0xAA }, 3 }); + runCase({ "truncated large picture ID", { 0x80, 0x81 }, -1 }); + runCase({ "resolution", { 0x02, 0x10, 0x07, 0x80, 0x04, 0x38, 0xAA }, 6 }); + runCase({ "truncated resolution", { 0x02, 0x10, 0x07, 0x80, 0x04 }, -1 }); + + // V=1, G=1, N_G=2. Each GOF frame byte is followed immediately by + // exactly the number of reference indices it declares. + runCase({ "GOF exact boundary", { 0x02, 0x08, 0x02, 0x04, 0x11, 0x08, 0x22, 0x33, 0xAA }, 8 }); + runCase({ "GOF truncated references", { 0x02, 0x08, 0x01, 0x08, 0x22 }, -1 }); + + runCase({ "descriptor only", { 0x00 }, -1 }); + runCase({ "layer descriptor only", { 0x30, 0x00 }, -1 }); + return 0; +}