diff --git a/media/codec_signaling/src/codec_signaling.c b/media/codec_signaling/src/codec_signaling.c index ac75553..a7be621 100644 --- a/media/codec_signaling/src/codec_signaling.c +++ b/media/codec_signaling/src/codec_signaling.c @@ -548,8 +548,12 @@ static moq_result_t build_avcc(const moq_codec_init_data_cfg_t *cfg, /* * Parse the fields an hvcC header needs from an HEVC SPS NAL (payload after * the 2-byte NAL header), per ISO/IEC 23008-2 7.3.2.2.1. The 12-byte general - * profile_tier_level maps directly onto hvcC bytes 1..12. Sub-layer PTL - * (sps_max_sub_layers_minus1 > 0) is not parsed. + * profile_tier_level maps directly onto hvcC bytes 1..12. + * + * When sps_max_sub_layers_minus1 > 0 the SPS carries sub-layer PTL syntax + * between the general PTL and sps_seq_parameter_set_id. hvcC needs none of it, + * but it is variable-length and has to be stepped over to reach + * chroma_format_idc and the bit depths. */ static moq_result_t parse_hevc_sps(const uint8_t *sps, size_t sps_len, uint8_t ptl[12], @@ -562,7 +566,9 @@ static moq_result_t parse_hevc_sps(const uint8_t *sps, size_t sps_len, if (sps_len < 3) { return MOQ_ERR_PROTO; } - uint8_t rbsp[64]; + /* Large enough for the general PTL plus a full set of sub-layer PTLs + * (7 x 96 bits) and the fields we read after them. */ + uint8_t rbsp[256]; size_t n = deemulate(sps + 2, sps_len - 2, rbsp, sizeof(rbsp)); if (n < 13) { return MOQ_ERR_PROTO; @@ -574,10 +580,37 @@ static moq_result_t parse_hevc_sps(const uint8_t *sps, size_t sps_len, uint32_t tid_nest = br_u(&br, 1); /* sps_temporal_id_nesting */ memcpy(ptl, rbsp + 1, 12); /* general profile_tier_level */ + br.bitpos = 8 + 12 * 8; + + /* profile_tier_level() sub-layer part, 23008-2 7.3.3. The general half is + * the fixed 12 bytes already copied above; what follows is present only + * when there is more than one temporal sub-layer. max_sub is 3 bits, so + * it never exceeds 7 and the arrays below are always in range. */ if (max_sub > 0) { - return MOQ_ERR_UNSUPPORTED; /* sub-layer PTL not handled */ + bool profile_present[7] = { false }; + bool level_present[7] = { false }; + for (uint32_t i = 0; i < max_sub; i++) { + profile_present[i] = br_u(&br, 1) != 0; + level_present[i] = br_u(&br, 1) != 0; + } + for (uint32_t i = max_sub; i < 8; i++) { + br_u(&br, 2); /* reserved_zero_2bits */ + } + for (uint32_t i = 0; i < max_sub; i++) { + if (profile_present[i]) { + /* Same layout as the general PTL minus its 8-bit level. */ + br_u(&br, 32); + br_u(&br, 32); + br_u(&br, 24); + } + if (level_present[i]) { + br_u(&br, 8); /* sub_layer_level_idc */ + } + } + if (br.overrun) { + return MOQ_ERR_PROTO; + } } - br.bitpos = 8 + 12 * 8; br_ue(&br); /* sps_seq_parameter_set_id */ uint32_t chroma = br_ue(&br); diff --git a/media/codec_signaling/tests/test_codec_signaling.c b/media/codec_signaling/tests/test_codec_signaling.c index 0661334..d4ecd06 100644 --- a/media/codec_signaling/tests/test_codec_signaling.c +++ b/media/codec_signaling/tests/test_codec_signaling.c @@ -665,6 +665,46 @@ int main(void) CHECK(out_len == sizeof(expect) && memcmp(out, expect, sizeof(expect)) == 0); } + /* -- HEVC Annex B with temporal sub-layers -> hvcC -------------- */ + { + /* VPS + SPS + PPS captured from Apple's low-latency HEVC encoder + * (com.apple.videotoolbox.videoencoder.hevc.rtvc) at 1280x720. Its SPS + * carries sps_max_sub_layers_minus1 = 1, which the parser used to + * reject outright with MOQ_ERR_UNSUPPORTED -- no hvcC meant no video + * track was ever announced, so a stream encoded this way came out + * audio-only. The sub-layer PTL is skipped, not interpreted. */ + static const uint8_t annexb[] = { + 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x03, 0xff, 0xff, 0x01, 0x60, + 0x00, 0x00, 0x03, 0x00, 0xb0, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, + 0x5d, 0x00, 0x00, 0x1b, 0x02, 0x40, 0x00, 0x00, 0x00, 0x01, 0x42, 0x01, + 0x03, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0xb0, 0x00, 0x00, 0x03, 0x00, + 0x00, 0x03, 0x00, 0x5d, 0x00, 0x00, 0xa0, 0x02, 0x80, 0x80, 0x2d, 0x16, + 0x20, 0x6e, 0xe4, 0x52, 0x32, 0xe7, 0xe1, 0x3d, 0x0b, 0xea, 0x1b, 0xd5, + 0x29, 0xa8, 0x10, 0x10, 0x10, 0x1f, 0xc2, 0x01, 0x04, 0x00, 0x00, 0x00, + 0x01, 0x44, 0x01, 0xc0, 0x72, 0xf0, 0x5b, 0x24, + }; + /* general profile_tier_level, hvcC bytes 1..12: Main profile, Main + * tier, general_level_idc 0x5d (level 4.0). */ + static const uint8_t ptl[] = { + 0x01, 0x60, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5d, + }; + moq_codec_init_data_cfg_t cfg; + moq_codec_init_data_cfg_init(&cfg); + cfg.source_format = MOQ_CODEC_SOURCE_HEVC_ANNEXB; + cfg.source = bytes(annexb, sizeof(annexb)); + + uint8_t out[256]; + size_t out_len = 0; + moq_result_t rc = moq_codec_init_data_build(&cfg, out, sizeof(out), &out_len); + CHECK(rc == MOQ_OK); + CHECK(out_len >= 23); + CHECK(out[0] == 0x01); /* configurationVersion */ + CHECK(memcmp(out + 1, ptl, sizeof(ptl)) == 0); + /* byte 21: constantFrameRate(2)=0 | numTemporalLayers(3)=2 | + * temporalIdNested(1)=1 | lengthSizeMinusOne(2)=3 */ + CHECK(out[21] == 0x17); + } + /* -- AV1 OBU -> av1C (header vs ffmpeg; seq header verbatim) ---- */ { /* Temporal delimiter + sequence header OBU from SVT-AV1. */