From 495995e7e687f01627bca829168fd862b096f1cb Mon Sep 17 00:00:00 2001 From: Mariana Assis Date: Mon, 20 Jul 2026 14:51:16 +0100 Subject: [PATCH 1/3] Add HEVC media format support --- Development/cmake/NmosCppLibraries.cmake | 2 + Development/nmos/video_h265.cpp | 380 +++++++++++++++++++++++ Development/nmos/video_h265.h | 374 ++++++++++++++++++++++ 3 files changed, 756 insertions(+) create mode 100644 Development/nmos/video_h265.cpp create mode 100644 Development/nmos/video_h265.h diff --git a/Development/cmake/NmosCppLibraries.cmake b/Development/cmake/NmosCppLibraries.cmake index 41bcbd6f0..8c5b0af0f 100644 --- a/Development/cmake/NmosCppLibraries.cmake +++ b/Development/cmake/NmosCppLibraries.cmake @@ -1128,6 +1128,7 @@ set(NMOS_CPP_NMOS_SOURCES nmos/system_api.cpp nmos/system_resources.cpp nmos/video_jxsv.cpp + nmos/video_h265.cpp nmos/ws_api_utils.cpp ) set(NMOS_CPP_NMOS_HEADERS @@ -1269,6 +1270,7 @@ set(NMOS_CPP_NMOS_HEADERS nmos/usb_sdp_utils.h nmos/version.h nmos/video_jxsv.h + nmos/video_h265.h nmos/vpid_code.h nmos/websockets.h nmos/ws_api_utils.h diff --git a/Development/nmos/video_h265.cpp b/Development/nmos/video_h265.cpp new file mode 100644 index 000000000..389b608f7 --- /dev/null +++ b/Development/nmos/video_h265.cpp @@ -0,0 +1,380 @@ +#include "nmos/video_h265.h" + +#include +#include +#include +#include "nmos/capabilities.h" +#include "nmos/format.h" +#include "nmos/interlace_mode.h" +#include "nmos/json_fields.h" +#include "nmos/resource.h" + +namespace nmos +{ + namespace video_h265 + { + namespace details + { + // Bit positions within the 48-bit interop-constraints, most significant bit first + // See Rec. ITU-T H.265 and https://tools.ietf.org/html/rfc7798#section-7.1 + const uint64_t progressive_source_flag = 1ull << 47; // general_progressive_source_flag + const uint64_t interlaced_source_flag = 1ull << 46; // general_interlaced_source_flag + const uint64_t non_packed_constraint_flag = 1ull << 45; // general_non_packed_constraint_flag + const uint64_t frame_only_constraint_flag = 1ull << 44; // general_frame_only_constraint_flag + + // The remaining 44 bits are the reserved bits, which for general_profile_idc 4, 5 and 9 + // carry the general constraint flags that distinguish the individual profiles: + // max_12bit, max_10bit, max_8bit, max_422chroma, max_420chroma, max_monochrome, + // intra, one_picture_only, lower_bit_rate, max_14bit, ... + // Transcribe their positions and values from Rec. ITU-T H.265 Table A.2 and A.3. + // NB: for some profiles a constraint is asserted by the flag being *clear* rather than + // set - check the standard rather than assuming. + + struct profile_entry + { + uint32_t profile_space; + uint32_t profile_id; + uint64_t interop_constraints_mask; // which bits of interop-constraints are significant + uint64_t interop_constraints_value; // the required value of those bits + nmos::video_h265::profile profile; + }; + + // See Rec. ITU-T H.265 Annex A.3 + // general_profile_idc alone identifies only Main (1), Main10 (2) and MainStillPicture (3). + // The format range extensions (4), high throughput (5) and screen content coding (9) + // families each share a profile_id and are disambiguated purely by the constraint flags, + // so each needs a mask/value pair. Populate as required; profiles not in the table are + // rejected rather than silently mapped to something plausible. + static const std::vector profile_table + { + { 0, 1, 0, 0, profiles::Main }, + { 0, 2, 0, 0, profiles::Main10 }, + { 0, 3, 0, 0, profiles::MainStillPicture } + // TODO: format range extensions (profile_id 4), high throughput (5), + // screen content coding (9) + }; + + // "Main-3.1" etc. -> tier_flag, level_id + std::pair parse_level_name(const level& level) + { + const auto dash = level.name.find(U('-')); + if (utility::string_t::npos == dash) throw std::invalid_argument("invalid H.265 level"); + + const auto tier = level.name.substr(0, dash); + uint32_t tier_flag; + if (U("Main") == tier) tier_flag = 0; + else if (U("High") == tier) tier_flag = 1; + else throw std::invalid_argument("invalid H.265 tier"); + + // the level number is either "n" or "n.m" + const auto number = level.name.substr(dash + 1); + const auto dot = number.find(U('.')); + const auto units = utility::istringstreamed(utility::string_t::npos == dot ? number : number.substr(0, dot)); + const uint32_t tenths = utility::string_t::npos == dot ? 0 : utility::istringstreamed(number.substr(dot + 1)); + + // general_level_idc == 30 * level + return{ tier_flag, 3 * (10 * units + tenths) }; + } + + // tier_flag, level_id -> "Main-3.1" etc. + level make_level_name(uint32_t tier_flag, uint32_t level_id) + { + if (0 != level_id % 3) throw std::invalid_argument("invalid H.265 level-id"); + + const auto tenths = level_id / 3; // e.g. 93 -> 31, i.e. level 3.1 + utility::ostringstream_t os; + os << (0 == tier_flag ? U("Main-") : U("High-")) << (tenths / 10); + if (0 != tenths % 10) os << U(".") << (tenths % 10); + return level{ os.str() }; + } + } + + // Map the Flow's profile and level onto the SDP profile-tier-level + sdp::video_h265::profile_tier_level make_profile_tier_level(const profile& profile, const level& level) + { + const auto found = std::find_if(details::profile_table.begin(), details::profile_table.end(), [&](const details::profile_entry& entry) + { + return entry.profile == profile; + }); + if (details::profile_table.end() == found) throw std::invalid_argument("unsupported H.265 profile"); + + const auto tier_level = details::parse_level_name(level); + + // general_profile_compatibility_flag[general_profile_idc] must be set + const uint32_t compatibility = 1u << (31 - found->profile_id); + + return sdp::video_h265::profile_tier_level{ + found->profile_space, + found->profile_id, + compatibility, + found->interop_constraints_value, + tier_level.first, + tier_level.second + }; + } + + // Map the SDP profile-tier-level onto the Flow's profile and level + std::pair parse_profile_tier_level(const sdp::video_h265::profile_tier_level& ptl) + { + const auto found = std::find_if(details::profile_table.begin(), details::profile_table.end(), [&](const details::profile_entry& entry) + { + return entry.profile_space == ptl.profile_space + && entry.profile_id == ptl.profile_id + && (ptl.interop_constraints & entry.interop_constraints_mask) == entry.interop_constraints_value; + }); + if (details::profile_table.end() == found) throw std::invalid_argument("unsupported H.265 profile-tier-level"); + + return{ found->profile, details::make_level_name(ptl.tier_flag, ptl.level_id) }; + } + } + + // Which packet transmission mode the Sender is using, derived from sprop-max-don-diff + video_h265::packet_transmission_mode get_video_H265_packet_transmission_mode(const video_H265_parameters& params) + { + return 0 == params.sprop_max_don_diff + ? video_h265::packet_transmission_modes::non_interleaved_nal_units + : video_h265::packet_transmission_modes::interleaved_nal_units; + } + + // Which parameter sets transport mode the Sender is using, derived from the sprop-* parameters + video_h265::parameter_sets_transport_mode get_video_H265_parameter_sets_transport_mode(const video_H265_parameters& params) + { + if (params.sprop_vps.empty() && params.sprop_sps.empty() && params.sprop_pps.empty()) + return video_h265::parameter_sets_transport_modes::in_band; + + // a trailing ',' indicates a terminating empty NAL unit, i.e. the parameter sets are also + // conveyed in-band; a lone "," means nothing is conveyed out-of-band + const auto trailing_comma = [](const utility::string_t& value) + { + return !value.empty() && U(',') == value.back(); + }; + if (trailing_comma(params.sprop_vps) || trailing_comma(params.sprop_sps) || trailing_comma(params.sprop_pps)) + return video_h265::parameter_sets_transport_modes::in_and_out_of_band; + + return video_h265::parameter_sets_transport_modes::out_of_band; + } + + // Get additional "video/H265" parameters from the SDP parameters + // Every RFC 7798 fmtp parameter is optional and has a default, so unlike "video/jxsv" there is + // no MissingRequiredParameter policy; the two public overloads are identical, and provided only + // for symmetry with the other coded video formats. + template + video_H265_parameters get_video_H265_parameters(const sdp_parameters& sdp_params, MissingRequiredParameter missing = MissingRequiredParameter{}) + { + video_H265_parameters params; + + (void)missing; + + // optional + const auto profile_space = details::find_fmtp(sdp_params.fmtp, sdp::video_h265::fields::profile_space); + params.ptl.profile_space = sdp_params.fmtp.end() != profile_space + ? utility::istringstreamed(profile_space->second) + : 0; + + // optional; 1 == Main + const auto profile_id = details::find_fmtp(sdp_params.fmtp, sdp::video_h265::fields::profile_id); + params.ptl.profile_id = sdp_params.fmtp.end() != profile_id + ? utility::istringstreamed(profile_id->second) + : 1; + + // optional + const auto profile_compatibility_indicator = details::find_fmtp(sdp_params.fmtp, sdp::video_h265::fields::profile_compatibility_indicator); + params.ptl.profile_compatibility_indicator = sdp_params.fmtp.end() != profile_compatibility_indicator + ? (uint32_t)sdp::video_h265::parse_hex_string(profile_compatibility_indicator->second) + : 0; + + // optional + const auto interop_constraints = details::find_fmtp(sdp_params.fmtp, sdp::video_h265::fields::interop_constraints); + params.ptl.interop_constraints = sdp_params.fmtp.end() != interop_constraints + ? sdp::video_h265::parse_hex_string(interop_constraints->second) + : 0; + + // optional; 0 == Main tier + const auto tier_flag = details::find_fmtp(sdp_params.fmtp, sdp::video_h265::fields::tier_flag); + params.ptl.tier_flag = sdp_params.fmtp.end() != tier_flag + ? utility::istringstreamed(tier_flag->second) + : 0; + + // optional; 93 == level 3.1 + const auto level_id = details::find_fmtp(sdp_params.fmtp, sdp::video_h265::fields::level_id); + params.ptl.level_id = sdp_params.fmtp.end() != level_id + ? utility::istringstreamed(level_id->second) + : 93; + + // optional; zero indicates the non-interleaved packetization mode + const auto sprop_max_don_diff = details::find_fmtp(sdp_params.fmtp, sdp::video_h265::fields::sprop_max_don_diff); + if (sdp_params.fmtp.end() != sprop_max_don_diff) params.sprop_max_don_diff = utility::istringstreamed(sprop_max_don_diff->second); + + // optional + const auto sprop_depack_buf_nalus = details::find_fmtp(sdp_params.fmtp, sdp::video_h265::fields::sprop_depack_buf_nalus); + if (sdp_params.fmtp.end() != sprop_depack_buf_nalus) params.sprop_depack_buf_nalus = utility::istringstreamed(sprop_depack_buf_nalus->second); + + // optional + const auto sprop_depack_buf_bytes = details::find_fmtp(sdp_params.fmtp, sdp::video_h265::fields::sprop_depack_buf_bytes); + if (sdp_params.fmtp.end() != sprop_depack_buf_bytes) params.sprop_depack_buf_bytes = utility::istringstreamed(sprop_depack_buf_bytes->second); + + // optional; SRST is the only mode permitted by BCP-006-03 + const auto tx_mode = details::find_fmtp(sdp_params.fmtp, sdp::video_h265::fields::tx_mode); + params.tx_mode = sdp_params.fmtp.end() != tx_mode + ? tx_mode->second + : sdp::video_h265::tx_modes::SRST; + + // optional; kept verbatim, including any trailing ',' + const auto sprop_vps = details::find_fmtp(sdp_params.fmtp, sdp::video_h265::fields::sprop_vps); + if (sdp_params.fmtp.end() != sprop_vps) params.sprop_vps = sprop_vps->second; + + // optional + const auto sprop_sps = details::find_fmtp(sdp_params.fmtp, sdp::video_h265::fields::sprop_sps); + if (sdp_params.fmtp.end() != sprop_sps) params.sprop_sps = sprop_sps->second; + + // optional + const auto sprop_pps = details::find_fmtp(sdp_params.fmtp, sdp::video_h265::fields::sprop_pps); + if (sdp_params.fmtp.end() != sprop_pps) params.sprop_pps = sprop_pps->second; + + // optional + if (sdp::bandwidth_types::application_specific == sdp_params.bandwidth.bandwidth_type) params.bit_rate = sdp_params.bandwidth.bandwidth; + + return params; + } + + // Get additional "video/H265" parameters from the SDP parameters + video_H265_parameters get_video_H265_parameters(const sdp_parameters& sdp_params) + { + return get_video_H265_parameters(sdp_params); + } + + // Get additional "video/H265" parameters from the SDP parameters + video_H265_parameters get_video_H265_parameters_or_defaults(const sdp_parameters& sdp_params) + { + return get_video_H265_parameters<>(sdp_params, [](const utility::string_t&) {}); + } + + namespace details + { + const video_H265_parameters* get_h265(const format_parameters* format) { return get(format); } + + // NMOS Parameter Registers - Capabilities register + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/capabilities/ + // + // NB: unlike "video/jxsv", the RFC 7798 fmtp carries no sampling, depth, width, height, + // exactframerate, colorimetry, tcs or range, so the corresponding Receiver Capabilities + // cannot be checked against the transport file. They are deliberately absent from this map, + // so that nmos::details::validate_sdp_parameters ignores them, i.e. those constraints fail + // *open*. Do not add matchers for them that return false; that would reject valid streams. +#define CAPS_ARGS const sdp_parameters& sdp, const format_parameters& format, const web::json::value& con + static const std::map> h265_constraints + { + { nmos::caps::format::media_type, [](CAPS_ARGS) { return nmos::match_string_constraint(get_media_type(sdp).name, con); } }, + { nmos::caps::format::profile, [](CAPS_ARGS) { auto h265 = get_h265(&format); return h265 && nmos::match_string_constraint(nmos::video_h265::parse_profile_tier_level(h265->ptl).first.name, con); } }, + { nmos::caps::format::level, [](CAPS_ARGS) { auto h265 = get_h265(&format); return h265 && nmos::match_string_constraint(nmos::video_h265::parse_profile_tier_level(h265->ptl).second.name, con); } }, + { nmos::caps::transport::bit_rate, [](CAPS_ARGS) { auto h265 = get_h265(&format); return h265 && (0 == h265->bit_rate || nmos::match_integer_constraint(h265->bit_rate, con)); } }, + { nmos::caps::transport::packet_transmission_mode, [](CAPS_ARGS) { auto h265 = get_h265(&format); return h265 && nmos::match_string_constraint(nmos::get_video_H265_packet_transmission_mode(*h265).name, con); } }, + { nmos::caps::transport::parameter_sets_transport_mode, [](CAPS_ARGS) { auto h265 = get_h265(&format); return h265 && nmos::match_string_constraint(nmos::get_video_H265_parameter_sets_transport_mode(*h265).name, con); } } + }; +#undef CAPS_ARGS + } + + // Validate SDP parameters for "video/H265" against IS-04 receiver capabilities + // cf. nmos::validate_sdp_parameters + void validate_video_H265_sdp_parameters(const web::json::value& receiver, const nmos::sdp_parameters& sdp_params) + { + // this function can only be used to validate SDP data for "video/H265"; logic error otherwise + const auto media_type = get_media_type(sdp_params); + if (nmos::media_types::video_H265 != media_type) throw std::invalid_argument("unexpected media type/encoding name"); + + const auto params = get_video_H265_parameters(sdp_params); + + // BCP-006-03 permits only Single RTP stream transport + // See https://specs.amwa.tv/bcp-006-03/branches/v1.0-dev/docs/NMOS_With_H.265.html#senders + if (sdp::video_h265::tx_modes::SRST != params.tx_mode) throw nmos::details::sdp_processing_error("unsupported tx-mode; only SRST is supported"); + + nmos::details::validate_sdp_parameters(details::h265_constraints, sdp_params, nmos::formats::video, params, receiver); + } + + // See https://specs.amwa.tv/bcp-006-03/branches/v1.0-dev/docs/NMOS_With_H.265.html#flows + // cf. nmos::make_coded_video_flow + nmos::resource make_video_H265_flow( + const nmos::id& id, + const nmos::id& source_id, + const nmos::id& device_id, + const nmos::rational& grain_rate, + unsigned int frame_width, + unsigned int frame_height, + const nmos::interlace_mode& interlace_mode, + const nmos::colorspace& colorspace, + const nmos::transfer_characteristic& transfer_characteristic, + const sdp::sampling& color_sampling, + unsigned int bit_depth, + const video_h265::profile& profile, + const video_h265::level& level, + uint64_t bit_rate, + bool constant_bit_rate, + const nmos::settings& settings) + { + using web::json::value; + + // fail early if the profile and level cannot be expressed in an SDP transport file + if (!profile.empty() && !level.empty()) video_h265::make_profile_tier_level(profile, level); + + auto resource = nmos::make_coded_video_flow( + id, source_id, device_id, + grain_rate, + frame_width, frame_height, interlace_mode, + colorspace, transfer_characteristic, color_sampling, bit_depth, + nmos::media_types::video_H265, + settings + ); + auto& data = resource.data; + + // additional attributes required by BCP-006-03 + // see https://specs.amwa.tv/bcp-006-03/branches/v1.0-dev/docs/NMOS_With_H.265.html#flows + if (!profile.empty()) data[nmos::fields::profile] = value(profile.name); + if (!level.empty()) data[nmos::fields::level] = value(level.name); + if (0 != bit_rate) data[nmos::fields::bit_rate] = value(bit_rate); + // constant_bit_rate defaults to false, so only include it when true + if (constant_bit_rate) data[nmos::fields::constant_bit_rate] = value(true); + + return resource; + } +} + + +namespace sdp{ + namespace video_h265{ + // "profile-compatibility-indicator" (32 bits) and "interop-constraints" (48 bits) are + // hexadecimal strings, groups of two hex digits separated by "..", + // e.g. "B0.00.00.00.00.00" + // See https://tools.ietf.org/html/rfc7798#section-7.1 + uint64_t parse_hex_string(const utility::string_t& value) + { + uint64_t result = 0; + for (const auto c : value) + { + if (U('.') == c) continue; + + uint64_t nibble; + if (c >= U('0') && c <= U('9')) nibble = c - U('0'); + else if (c >= U('a') && c <= U('f')) nibble = 10 + (c - U('a')); + else if (c >= U('A') && c <= U('F')) nibble = 10 + (c - U('A')); + else throw std::invalid_argument("invalid hexadecimal value"); + + result = (result << 4) | nibble; + } + return result; + } + + utility::string_t make_hex_string(uint64_t value, size_t bytes) + { + static const utility::char_t* const digits = U("0123456789ABCDEF"); + utility::string_t result; + for (size_t i = 0; i < bytes; ++i) + { + const auto byte = (value >> (8 * (bytes - 1 - i))) & 0xFF; + if (0 != i) result.push_back(U('.')); + result.push_back(digits[(byte >> 4) & 0xF]); + result.push_back(digits[byte & 0xF]); + } + return result; + } + } +} diff --git a/Development/nmos/video_h265.h b/Development/nmos/video_h265.h new file mode 100644 index 000000000..d5b08ebe5 --- /dev/null +++ b/Development/nmos/video_h265.h @@ -0,0 +1,374 @@ +#ifndef NMOS_VIDEO_H265_H +#define NMOS_VIDEO_H265_H + +#include +#include "nmos/media_type.h" +#include "nmos/node_resources.h" +#include "nmos/sdp_utils.h" +#include "nmos/video_jxsv.h" + +// H.265 / HEVC, per AMWA BCP-006-03 +// See https://specs.amwa.tv/bcp-006-03/branches/v1.0-dev/docs/NMOS_With_H.265.html +// +// This header provides parsing of SDP transport files and validation against IS-04 Receiver +// Capabilities. SDP transport files are produced by the application, not by nmos-cpp, so there +// are deliberately no make_*_sdp_parameters functions here. + +namespace sdp +{ + namespace video_h265 + { + // hexadecimal strings, ".."-separated groups of two hex digits, per RFC 7798, + // e.g. "B0.00.00.00.00.00"; used for profile-compatibility-indicator (4 bytes) + // and interop-constraints (6 bytes) + utility::string_t make_hex_string(uint64_t value, size_t bytes); + uint64_t parse_hex_string(const utility::string_t& value); + + namespace fields + { + // See https://www.iana.org/assignments/media-types/video/H265 + // and https://tools.ietf.org/html/rfc7798#section-7.1 + + // the profile-tier-level, as decimal integers + const web::json::field_with_default profile_space{ U("profile-space"), 0 }; + const web::json::field_with_default profile_id{ U("profile-id"), 1 }; // 1 == Main + const web::json::field_with_default tier_flag{ U("tier-flag"), 0 }; // 0 == Main tier + const web::json::field_with_default level_id{ U("level-id"), 93 }; // 93 == level 3.1, i.e. 30 * 3.1 + + // profile-compatibility-indicator (32 bits) and interop-constraints (48 bits), + // as hexadecimal strings, groups of two hex digits separated by ".." + const web::json::field_as_string_or profile_compatibility_indicator{ U("profile-compatibility-indicator"), U("") }; + const web::json::field_as_string_or interop_constraints{ U("interop-constraints"), U("") }; + + // out-of-band parameter sets, as comma-separated base64 NAL units + // NB: a *trailing* comma indicates a terminating empty NAL unit, which per BCP-006-03 + // signals that the parameter sets are also sent in-band, i.e. in_and_out_of_band + const web::json::field_as_string_or sprop_vps{ U("sprop-vps"), U("") }; + const web::json::field_as_string_or sprop_sps{ U("sprop-sps"), U("") }; + const web::json::field_as_string_or sprop_pps{ U("sprop-pps"), U("") }; + + // sprop-max-don-diff greater than zero indicates the interleaved packetization mode + const web::json::field_with_default sprop_max_don_diff{ U("sprop-max-don-diff"), 0 }; + const web::json::field_with_default sprop_depack_buf_nalus{ U("sprop-depack-buf-nalus"), 0 }; + const web::json::field_with_default sprop_depack_buf_bytes{ U("sprop-depack-buf-bytes"), 0 }; + + // BCP-006-03 requires Single RTP stream transport (SRST); parse it in order to reject + // a Sender using MRST or MRMT + const web::json::field_as_string_or tx_mode{ U("tx-mode"), U("SRST") }; + } + + // Single RTP stream transport is the only mode permitted by BCP-006-03 + namespace tx_modes + { + const utility::string_t SRST{ U("SRST") }; + } + + // The profile-tier-level, i.e. the six fmtp parameters that together encode the Flow's + // profile and level attributes + // See Rec. ITU-T H.265 Annex A and https://tools.ietf.org/html/rfc7798#section-7.1 + struct profile_tier_level + { + uint32_t profile_space; // general_profile_space, 0..3 + uint32_t profile_id; // general_profile_idc + uint32_t profile_compatibility_indicator; // general_profile_compatibility_flag[0..31] + uint64_t interop_constraints; // 48 bits: general_progressive_source_flag, + // general_interlaced_source_flag, + // general_non_packed_constraint_flag, + // general_frame_only_constraint_flag, and the 44 reserved + // bits carrying the RExt / HT / SCC general constraint flags + uint32_t tier_flag; // general_tier_flag, 0 == Main tier, 1 == High tier + uint32_t level_id; // general_level_idc == 30 * level + + profile_tier_level() + : profile_space(0) + , profile_id(1) + , profile_compatibility_indicator(0) + , interop_constraints(0) + , tier_flag(0) + , level_id(93) + {} + + profile_tier_level(uint32_t profile_space, uint32_t profile_id, uint32_t profile_compatibility_indicator, uint64_t interop_constraints, uint32_t tier_flag, uint32_t level_id) + : profile_space(profile_space) + , profile_id(profile_id) + , profile_compatibility_indicator(profile_compatibility_indicator) + , interop_constraints(interop_constraints) + , tier_flag(tier_flag) + , level_id(level_id) + {} + }; + } +} + +namespace nmos +{ + namespace media_types + { + // H.265 / HEVC + // See https://www.iana.org/assignments/media-types/video/H265 + // and https://tools.ietf.org/html/rfc7798 + const media_type video_H265{ U("video/H265") }; + } + + // NB: the bit_rate, profile, level and packet_transmission_mode *keys* are shared with the + // other coded video formats and are declared in nmos/video_codec.h. The keys below are used + // by H.264 and H.265 but not by JPEG XS; if BCP-006-02 (H.264) is added later, move them into + // nmos/video_codec.h. + + namespace fields + { + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/flow-attributes/#constant-bit-rate + const web::json::field_as_bool_or constant_bit_rate{ U("constant_bit_rate"), false }; + + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/sender-attributes/#parameter-sets-flow-mode + const web::json::field_as_string_or parameter_sets_flow_mode{ U("parameter_sets_flow_mode"), U("dynamic") }; + + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/sender-attributes/#parameter-sets-transport-mode + const web::json::field_as_string_or parameter_sets_transport_mode{ U("parameter_sets_transport_mode"), U("in_band") }; + } + + namespace caps + { + namespace format + { + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/capabilities/#constant-bit-rate + const web::json::field_as_value_or constant_bit_rate{ U("urn:x-nmos:cap:format:constant_bit_rate"), {} }; // boolean + } + + namespace transport + { + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/capabilities/#parameter-sets-flow-mode + const web::json::field_as_value_or parameter_sets_flow_mode{ U("urn:x-nmos:cap:transport:parameter_sets_flow_mode"), {} }; // string + + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/capabilities/#parameter-sets-transport-mode + const web::json::field_as_value_or parameter_sets_transport_mode{ U("urn:x-nmos:cap:transport:parameter_sets_transport_mode"), {} }; // string + } + } + + // The H.265 Profile, Level, Packet Transmission Mode, Parameter Sets Flow Mode and Parameter + // Sets Transport Mode types and their permitted values. These are distinct types from e.g. + // nmos::video_jxsv::profile, so that a value from one codec cannot be used with another. + namespace video_h265 + { + // Profile + // "The H.265 profile in use. Any white space Unicode character in the profile name SHALL be omitted." + // See Rec. ITU-T H.265 Annex A.3 + // and https://specs.amwa.tv/nmos-parameter-registers/branches/main/flow-attributes/#profile + DEFINE_STRING_ENUM(profile) + namespace profiles + { + // Main, Main 10 and Main Still Picture profiles (H.265 A.3.2, A.3.3, A.3.4) + const profile Main{ U("Main") }; // default + const profile Main10{ U("Main10") }; + const profile MainStillPicture{ U("MainStillPicture") }; + + // Format range extensions profiles (H.265 A.3.5) + const profile Monochrome{ U("Monochrome") }; + const profile Monochrome10{ U("Monochrome10") }; + const profile Monochrome12{ U("Monochrome12") }; + const profile Monochrome16{ U("Monochrome16") }; + const profile Main12{ U("Main12") }; + const profile Main10_422{ U("Main10-422") }; + const profile Main12_422{ U("Main12-422") }; + const profile Main_444{ U("Main-444") }; + const profile Main10_444{ U("Main10-444") }; + const profile Main12_444{ U("Main12-444") }; + const profile Main16_444{ U("Main16-444") }; + const profile MainIntra{ U("MainIntra") }; + const profile Main10Intra{ U("Main10Intra") }; + const profile Main12Intra{ U("Main12Intra") }; + const profile Main10Intra_422{ U("Main10Intra-422") }; + const profile Main12Intra_422{ U("Main12Intra-422") }; + const profile MainIntra_444{ U("MainIntra-444") }; + const profile Main10Intra_444{ U("Main10Intra-444") }; + const profile Main12Intra_444{ U("Main12Intra-444") }; + const profile Main16Intra_444{ U("Main16Intra-444") }; + const profile MainStillPicture_444{ U("MainStillPicture-444") }; + const profile Main16StillPicture_444{ U("Main16StillPicture-444") }; + + // High throughput profiles (H.265 A.3.6, A.3.7) + const profile HighThroughput_444{ U("HighThroughput-444") }; + const profile HighThroughput10_444{ U("HighThroughput10-444") }; + const profile HighThroughput14_444{ U("HighThroughput14-444") }; + const profile HighThroughput14Intra_444{ U("HighThroughput14Intra-444") }; + + // Screen content coding extensions profiles (H.265 A.3.8) + const profile ScreenExtendedMain{ U("ScreenExtendedMain") }; + const profile ScreenExtendedMain10{ U("ScreenExtendedMain10") }; + const profile ScreenExtendedMain_444{ U("ScreenExtendedMain-444") }; + const profile ScreenExtendedMain10_444{ U("ScreenExtendedMain10-444") }; + const profile ScreenExtendedHighThroughput_444{ U("ScreenExtendedHighThroughput-444") }; + const profile ScreenExtendedHighThroughput10_444{ U("ScreenExtendedHighThroughput10-444") }; + const profile ScreenExtendedHighThroughput14_444{ U("ScreenExtendedHighThroughput14-444") }; + } + + // Level, i.e. the tier name followed by the level number + // See Rec. ITU-T H.265 Annex A.4 + // and https://specs.amwa.tv/nmos-parameter-registers/branches/main/flow-attributes/#level + DEFINE_STRING_ENUM(level) + namespace levels + { + // Main tier + const level Main_1{ U("Main-1") }; + const level Main_2{ U("Main-2") }; + const level Main_2_1{ U("Main-2.1") }; + const level Main_3{ U("Main-3") }; + const level Main_3_1{ U("Main-3.1") }; // default + const level Main_4{ U("Main-4") }; + const level Main_4_1{ U("Main-4.1") }; + const level Main_5{ U("Main-5") }; + const level Main_5_1{ U("Main-5.1") }; + const level Main_5_2{ U("Main-5.2") }; + const level Main_6{ U("Main-6") }; + const level Main_6_1{ U("Main-6.1") }; + const level Main_6_2{ U("Main-6.2") }; + const level Main_8_5{ U("Main-8.5") }; + + // High tier, defined only from level 4 upwards + const level High_4{ U("High-4") }; + const level High_4_1{ U("High-4.1") }; + const level High_5{ U("High-5") }; + const level High_5_1{ U("High-5.1") }; + const level High_5_2{ U("High-5.2") }; + const level High_6{ U("High-6") }; + const level High_6_1{ U("High-6.1") }; + const level High_6_2{ U("High-6.2") }; + const level High_8_5{ U("High-8.5") }; + } + + // Packet Transmission Mode, derived from sprop-max-don-diff + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/sender-attributes/#packet-transmission-mode + DEFINE_STRING_ENUM(packet_transmission_mode) + namespace packet_transmission_modes + { + const packet_transmission_mode non_interleaved_nal_units{ U("non_interleaved_nal_units") }; // default + const packet_transmission_mode interleaved_nal_units{ U("interleaved_nal_units") }; + } + + // Parameter Sets Flow Mode, i.e. whether and how the parameter sets in the bitstream may change + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/sender-attributes/#parameter-sets-flow-mode + DEFINE_STRING_ENUM(parameter_sets_flow_mode) + namespace parameter_sets_flow_modes + { + const parameter_sets_flow_mode strict{ U("strict") }; + const parameter_sets_flow_mode statik{ U("static") }; // 'static' is a keyword + const parameter_sets_flow_mode dynamic{ U("dynamic") }; // default + } + + // Parameter Sets Transport Mode, i.e. whether the parameter sets are carried in the + // bitstream, in the SDP transport file, or both + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/sender-attributes/#parameter-sets-transport-mode + DEFINE_STRING_ENUM(parameter_sets_transport_mode) + namespace parameter_sets_transport_modes + { + const parameter_sets_transport_mode in_band{ U("in_band") }; // default + const parameter_sets_transport_mode out_of_band{ U("out_of_band") }; + const parameter_sets_transport_mode in_and_out_of_band{ U("in_and_out_of_band") }; + } + + // Map a Flow's profile and level onto the SDP profile-tier-level, and back. + // + // The table is derived from Rec. ITU-T H.265 Table A.2 and A.3. general_profile_idc alone + // is unambiguous only for Main (1), Main10 (2) and MainStillPicture (3); the format range + // extensions (4), high throughput (5) and screen content coding (9) families each share a + // profile_id and are disambiguated by the general constraint flags carried in + // interop-constraints (max_12bit, max_10bit, max_8bit, max_422chroma, max_420chroma, + // max_monochrome, intra, one_picture_only, lower_bit_rate). Transcribe these from the + // standard; do not guess. Both functions throw nmos::details::sdp_processing_error for + // values not in the table. + sdp::video_h265::profile_tier_level make_profile_tier_level(const profile& profile, const level& level); + std::pair parse_profile_tier_level(const sdp::video_h265::profile_tier_level& ptl); + } + + // Additional "video/H265" parameters, as parsed from an SDP transport file + // See https://www.iana.org/assignments/media-types/video/H265 + // and https://tools.ietf.org/html/rfc7798#section-7.1 + struct video_H265_parameters + { + // fmtp indicates format + sdp::video_h265::profile_tier_level ptl; + + // fmtp indicates packetization + uint32_t sprop_max_don_diff; // if omitted (zero), the non-interleaved mode is in use + uint32_t sprop_depack_buf_nalus; + uint32_t sprop_depack_buf_bytes; + utility::string_t tx_mode; // if omitted (empty), assume sdp::video_h265::tx_modes::SRST + + // fmtp indicates the out-of-band parameter sets, kept verbatim, including any trailing ',' + utility::string_t sprop_vps; + utility::string_t sprop_sps; + utility::string_t sprop_pps; + + // bandwidth + uint64_t bit_rate; // transport bit rate (kilobits/second); if omitted, zero + + video_H265_parameters() + : ptl() + , sprop_max_don_diff() + , sprop_depack_buf_nalus() + , sprop_depack_buf_bytes() + , bit_rate() + {} + + video_H265_parameters( + sdp::video_h265::profile_tier_level ptl, + uint32_t sprop_max_don_diff, + uint32_t sprop_depack_buf_nalus, + uint32_t sprop_depack_buf_bytes, + utility::string_t tx_mode, + utility::string_t sprop_vps, + utility::string_t sprop_sps, + utility::string_t sprop_pps, + uint64_t bit_rate + ) + : ptl(ptl) + , sprop_max_don_diff(sprop_max_don_diff) + , sprop_depack_buf_nalus(sprop_depack_buf_nalus) + , sprop_depack_buf_bytes(sprop_depack_buf_bytes) + , tx_mode(std::move(tx_mode)) + , sprop_vps(std::move(sprop_vps)) + , sprop_sps(std::move(sprop_sps)) + , sprop_pps(std::move(sprop_pps)) + , bit_rate(bit_rate) + {} + }; + + // Which packet transmission mode the Sender is using, derived from sprop-max-don-diff + video_h265::packet_transmission_mode get_video_H265_packet_transmission_mode(const video_H265_parameters& params); + + // Which parameter sets transport mode the Sender is using, derived from the sprop-* parameters: + // all absent or empty -> in_band + // present, without a trailing ',' -> out_of_band + // present, with a trailing ',' -> in_and_out_of_band + // (a lone "," means in_and_out_of_band, with nothing conveyed out-of-band) + video_h265::parameter_sets_transport_mode get_video_H265_parameter_sets_transport_mode(const video_H265_parameters& params); + + // Get additional "video/H265" parameters from the SDP parameters + video_H265_parameters get_video_H265_parameters(const sdp_parameters& sdp_params); + video_H265_parameters get_video_H265_parameters_or_defaults(const sdp_parameters& sdp_params); + + // Validate SDP parameters for "video/H265" against IS-04 receiver capabilities + void validate_video_H265_sdp_parameters(const web::json::value& receiver, const nmos::sdp_parameters& sdp_params); + + // See https://specs.amwa.tv/bcp-006-03/branches/v1.0-dev/docs/NMOS_With_H.265.html#flows + // cf. nmos::make_coded_video_flow + nmos::resource make_video_H265_flow( + const nmos::id& id, + const nmos::id& source_id, + const nmos::id& device_id, + const nmos::rational& grain_rate, + unsigned int frame_width, + unsigned int frame_height, + const nmos::interlace_mode& interlace_mode, + const nmos::colorspace& colorspace, + const nmos::transfer_characteristic& transfer_characteristic, + const sdp::sampling& color_sampling, + unsigned int bit_depth, + const video_h265::profile& profile, + const video_h265::level& level, + uint64_t bit_rate, // format bit rate (kilobits/second) + bool constant_bit_rate, + const nmos::settings& settings); +} + +#endif From 82cdc29986a3bfe0497154e573ead7bd911922ae Mon Sep 17 00:00:00 2001 From: Mariana Assis Date: Mon, 20 Jul 2026 14:51:47 +0100 Subject: [PATCH 2/3] nmos-cpp-node support for HEVC senders and receivers --- .../nmos-cpp-node/node_implementation.cpp | 114 +++++++++++++++++- 1 file changed, 112 insertions(+), 2 deletions(-) diff --git a/Development/nmos-cpp-node/node_implementation.cpp b/Development/nmos-cpp-node/node_implementation.cpp index 6bae255d9..a0aad2f71 100644 --- a/Development/nmos-cpp-node/node_implementation.cpp +++ b/Development/nmos-cpp-node/node_implementation.cpp @@ -57,6 +57,7 @@ #include "nmos/transport.h" #include "nmos/usb.h" #include "nmos/video_jxsv.h" +#include "nmos/video_h265.h" #include "sdp/sdp.h" #include "nmos/usb_sdp_utils.h" @@ -123,7 +124,7 @@ namespace impl // component_depth: controls the bits per component sample of video flows const web::json::field_as_integer_or component_depth{ U("component_depth"), 10 }; - // video_type: media type of video flows, e.g. "video/raw" or "video/jxsv", see nmos::media_types + // video_type: media type of video flows, e.g. "video/raw" or "video/jxsv" or "video/h265", see nmos::media_types const web::json::field_as_string_or video_type{ U("video_type"), U("video/raw") }; // mxl_video_type: media type of MXL video flows and receivers, e.g. "video/v210" or "video/v210a", see nmos/mxl.h @@ -144,12 +145,14 @@ namespace impl nmos::interlace_mode get_interlace_mode(const nmos::settings& settings); + uint64_t get_bit_rate(const nmos::rational& grain_rate, uint32_t frame_width, uint32_t frame_height, double bits_per_pixel); + // the different kinds of 'port' (standing for the format/media type/event type) implemented by the example node // each 'port' of the example node has a source, flow, sender and/or compatible receiver DEFINE_STRING_ENUM(port) namespace ports { - // video/raw, video/jxsv, etc. + // video/raw, video/jxsv, video/h265 etc. const port video{ U("v") }; // audio/L24 const port audio{ U("a") }; @@ -464,6 +467,18 @@ void node_implementation_init(nmos::node_model& model, nmos::experimental::contr const auto bits_per_pixel = 2.0; const auto transport_bit_rate_factor = 1.05; + // for now, some typical values for video/H265 + // see https://specs.amwa.tv/bcp-006-03/branches/v1.0-dev/docs/NMOS_With_H.265.html + const auto h265_profile = nmos::video_h265::profiles::Main10; + const auto h265_level = nmos::video_h265::levels::Main_4; + // H.265 is far more efficient than JPEG XS; ~0.05 bits/pixel is a typical + // contribution-quality operating point, ~0.2 an upper bound for the Receiver's caps + const auto h265_bits_per_pixel = 0.05; + const auto h265_max_bits_per_pixel = 0.2; + const auto h265_constant_bit_rate = false; + const auto h265_parameter_sets_flow_mode = nmos::video_h265::parameter_sets_flow_modes::statik; + const auto h265_parameter_sets_transport_mode = nmos::video_h265::parameter_sets_transport_modes::in_band; + // any delay between updates to the model resources is unnecessary unless for debugging purposes const unsigned int delay_millis{ 0 }; @@ -630,6 +645,18 @@ void node_implementation_init(nmos::node_model& model, nmos::experimental::contr model.settings ); } + else if (nmos::media_types::video_H265 == video_type) + { + const auto format_bit_rate = impl::get_bit_rate(frame_rate, frame_width, frame_height, h265_bits_per_pixel); + flow = nmos::make_video_H265_flow( + flow_id, source_id, device_id, + frame_rate, + frame_width, frame_height, interlace_mode, + colorspace, transfer_characteristic, sampling, bit_depth, + h265_profile, h265_level, format_bit_rate, h265_constant_bit_rate, + model.settings + ); + } else { flow = nmos::make_coded_video_flow( @@ -681,6 +708,18 @@ void node_implementation_init(nmos::node_model& model, nmos::experimental::contr sender.data[nmos::fields::bit_rate] = value(transport_bit_rate); sender.data[nmos::fields::st2110_21_sender_type] = value(nmos::st2110_21_sender_types::type_N.name); } + else if (impl::ports::video == port && nmos::media_types::video_H265 == video_type) + { + // additional attributes required by BCP-006-03 + // see https://specs.amwa.tv/bcp-006-03/branches/v1.0-dev/docs/NMOS_With_H.265.html#senders + const auto format_bit_rate = impl::get_bit_rate(frame_rate, frame_width, frame_height, h265_bits_per_pixel); + const auto transport_bit_rate = uint64_t(transport_bit_rate_factor * format_bit_rate / 1e3 + 0.5) * 1000; + sender.data[nmos::fields::bit_rate] = value(transport_bit_rate); + sender.data[nmos::fields::parameter_sets_flow_mode] = value(h265_parameter_sets_flow_mode.name); + sender.data[nmos::fields::parameter_sets_transport_mode] = value(h265_parameter_sets_transport_mode.name); + // packet_transmission_mode omitted; "non_interleaved_nal_units" is the default + // st2110_21_sender_type omitted; this Sender is not ST 2110-22 compliant + } impl::set_label_description(sender, port, index); impl::insert_group_hint(sender, port, index); @@ -756,6 +795,41 @@ void node_implementation_init(nmos::node_model& model, nmos::experimental::contr }) }); } + else if (nmos::media_types::video_H265 == video_type) + { + // some of the parameter constraints recommended by BCP-006-03 + // see https://specs.amwa.tv/bcp-006-03/branches/v1.0-dev/docs/NMOS_With_H.265.html#receivers + const auto max_format_bit_rate = impl::get_bit_rate(frame_rate, frame_width, frame_height, h265_max_bits_per_pixel); + const auto max_transport_bit_rate = uint64_t(transport_bit_rate_factor * max_format_bit_rate / 1e3 + 0.5) * 1000; + + receiver.data[nmos::fields::caps][nmos::fields::constraint_sets] = value_of({ + value_of({ + // this example Receiver supports the profiles it can express, i.e. those in the profile table + { nmos::caps::format::profile, nmos::make_caps_string_constraint({ + nmos::video_h265::profiles::Main.name, + nmos::video_h265::profiles::Main10.name }) }, + { nmos::caps::format::level, nmos::make_caps_string_constraint({ + nmos::video_h265::levels::Main_3_1.name, + nmos::video_h265::levels::Main_4.name, + nmos::video_h265::levels::Main_4_1.name, + nmos::video_h265::levels::Main_5.name, + nmos::video_h265::levels::Main_5_1.name }) }, + { nmos::caps::format::bit_rate, nmos::make_caps_integer_constraint({}, nmos::no_minimum(), (int64_t)max_format_bit_rate) }, + { nmos::caps::format::constant_bit_rate, nmos::make_caps_boolean_constraint({ false }) }, + { nmos::caps::transport::bit_rate, nmos::make_caps_integer_constraint({}, nmos::no_minimum(), (int64_t)max_transport_bit_rate) }, + { nmos::caps::transport::packet_transmission_mode, nmos::make_caps_string_constraint({ + nmos::video_h265::packet_transmission_modes::non_interleaved_nal_units.name }) }, + { nmos::caps::transport::parameter_sets_flow_mode, nmos::make_caps_string_constraint({ + nmos::video_h265::parameter_sets_flow_modes::strict.name, + nmos::video_h265::parameter_sets_flow_modes::statik.name, + nmos::video_h265::parameter_sets_flow_modes::dynamic.name }) }, + { nmos::caps::transport::parameter_sets_transport_mode, nmos::make_caps_string_constraint({ + nmos::video_h265::parameter_sets_transport_modes::in_band.name, + nmos::video_h265::parameter_sets_transport_modes::out_of_band.name, + nmos::video_h265::parameter_sets_transport_modes::in_and_out_of_band.name }) } + }) + }); + } receiver.data[nmos::fields::version] = receiver.data[nmos::fields::caps][nmos::fields::version] = value(nmos::make_version()); } else if (impl::ports::audio == port) @@ -2178,6 +2252,10 @@ nmos::transport_file_parser make_node_implementation_transport_file_parser() { nmos::validate_video_jxsv_sdp_parameters(receiver, sdp_params); } + else if (nmos::media_types::video_H265 == nmos::get_media_type(sdp_params)) + { + nmos::validate_video_H265_sdp_parameters(receiver, sdp_params); + } else { // validate core media types, i.e., "video/raw", "audio/L", "video/smpte291" and "video/SMPTE2022-6" @@ -2419,6 +2497,31 @@ a=setup:passive)"; const auto ts_refclk = nmos::details::make_ts_refclk(node->data, source->data, sender.data, {}); return nmos::make_sdp_parameters(nmos::fields::label(sender.data), params, nmos::details::payload_type_video_default, mids, ts_refclk); } + else if (nmos::media_types::video_H265 == video_type) + { + // nmos-cpp does not construct "video/H265" SDP transport files; the fmtp + // parameters are assembled here from the IS-04 Flow and Sender attributes + // see https://tools.ietf.org/html/rfc7798#section-7.1 + const nmos::video_h265::profile profile{ nmos::fields::profile(flow->data) }; + const nmos::video_h265::level level{ nmos::fields::level(flow->data) }; + const auto ptl = nmos::video_h265::make_profile_tier_level(profile, level); + + nmos::sdp_parameters::rtpmap_t rtpmap{ nmos::details::payload_type_video_default, U("H265"), 90000 }; + + nmos::sdp_parameters::fmtp_t fmtp; + if (0 != ptl.profile_space) fmtp.push_back({ sdp::video_h265::fields::profile_space, utility::ostringstreamed(ptl.profile_space) }); + fmtp.push_back({ sdp::video_h265::fields::profile_id, utility::ostringstreamed(ptl.profile_id) }); + fmtp.push_back({ sdp::video_h265::fields::profile_compatibility_indicator, sdp::video_h265::make_hex_string(ptl.profile_compatibility_indicator, 4) }); + fmtp.push_back({ sdp::video_h265::fields::interop_constraints, sdp::video_h265::make_hex_string(ptl.interop_constraints, 6) }); + if (0 != ptl.tier_flag) fmtp.push_back({ sdp::video_h265::fields::tier_flag, utility::ostringstreamed(ptl.tier_flag) }); + fmtp.push_back({ sdp::video_h265::fields::level_id, utility::ostringstreamed(ptl.level_id) }); + // this example Sender conveys the parameter sets in-band, so no sprop-vps/sps/pps, + // uses the non-interleaved mode, so no sprop-max-don-diff, and SRST, which is the default + + const auto bit_rate = nmos::fields::bit_rate(sender.data); + const auto ts_refclk = nmos::details::make_ts_refclk(node->data, source->data, sender.data, {}); + return nmos::sdp_parameters{ nmos::fields::label(sender.data), sdp::media_types::video, rtpmap, fmtp, (uint64_t)bit_rate, {}, {}, {}, mids, ts_refclk }; + } else { throw std::logic_error("unexpected flow media_type"); @@ -2671,6 +2774,13 @@ nmos::create_device_model_object_handler make_create_device_model_object_handler namespace impl { + // calculate a format bit rate (kilobits/second) from frame rate, dimensions and bits per pixel + uint64_t get_bit_rate(const nmos::rational& grain_rate, uint32_t frame_width, uint32_t frame_height, double bits_per_pixel) + { + const auto pixels_per_second = (int64_t)frame_width * (int64_t)frame_height * grain_rate; + return uint64_t(boost::rational_cast(pixels_per_second) * bits_per_pixel / 1e3 + 0.5); + } + nmos::interlace_mode get_interlace_mode(const nmos::settings& settings) { if (settings.has_field(impl::fields::interlace_mode)) From a0ed39909d585a0035b2ce3cd7227e69c9d50edd Mon Sep 17 00:00:00 2001 From: Mariana Assis Date: Mon, 20 Jul 2026 14:52:05 +0100 Subject: [PATCH 3/3] Implement tests for HEVC --- Development/cmake/NmosCppTest.cmake | 1 + Development/nmos/test/video_h265_test.cpp | 247 ++++++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 Development/nmos/test/video_h265_test.cpp diff --git a/Development/cmake/NmosCppTest.cmake b/Development/cmake/NmosCppTest.cmake index 083f680bf..ba9631214 100644 --- a/Development/cmake/NmosCppTest.cmake +++ b/Development/cmake/NmosCppTest.cmake @@ -69,6 +69,7 @@ set(NMOS_CPP_TEST_NMOS_TEST_SOURCES nmos/test/slog_test.cpp nmos/test/system_resources_test.cpp nmos/test/video_jxsv_test.cpp + nmos/test/video_h265_test.cpp ) set(NMOS_CPP_TEST_NMOS_TEST_HEADERS nmos/test/sdp_test_utils.h diff --git a/Development/nmos/test/video_h265_test.cpp b/Development/nmos/test/video_h265_test.cpp new file mode 100644 index 000000000..72632da98 --- /dev/null +++ b/Development/nmos/test/video_h265_test.cpp @@ -0,0 +1,247 @@ +#include "nmos/video_h265.h" + +#include "bst/test/test.h" +#include "nmos/capabilities.h" +#include "nmos/json_fields.h" +#include "nmos/node_resources.h" +#include "nmos/transport.h" +#include "nmos/resource.h" +#include "nmos/format.h" +#include "sdp/sdp.h" + +namespace +{ + // Typical SDP data for H.265, per BCP-006-03 and RFC 7798. + // Main10, level 4 (level-id 120), Main tier, parameter sets in-band, non-interleaved, SRST. + const std::string test_sdp_in_band = R"(v=0 +o=- 1443716955 1443716955 IN IP4 192.168.1.2 +s=Example H.265 Sender +t=0 0 +m=video 30000 RTP/AVP 96 +c=IN IP4 224.1.1.1/64 +b=AS:20000 +a=ts-refclk:localmac=40-a3-6b-a0-2b-d2 +a=mediaclk:direct=0 +a=source-filter: incl IN IP4 224.1.1.1 192.168.1.2 +a=rtpmap:96 H265/90000 +a=fmtp:96 profile-id=2; profile-compatibility-indicator=40.00.00.00; interop-constraints=B0.00.00.00.00.00; level-id=120 +)"; + + // The same Sender, but conveying the parameter sets out-of-band as well as in-band, + // which BCP-006-03 signals with a *trailing* comma, i.e. a terminating empty NAL unit + const std::string test_sdp_in_and_out_of_band = R"(v=0 +o=- 1443716955 1443716955 IN IP4 192.168.1.2 +s=Example H.265 Sender +t=0 0 +m=video 30000 RTP/AVP 96 +c=IN IP4 224.1.1.1/64 +b=AS:20000 +a=ts-refclk:localmac=40-a3-6b-a0-2b-d2 +a=mediaclk:direct=0 +a=rtpmap:96 H265/90000 +a=fmtp:96 profile-id=2; level-id=120; sprop-vps=QAEMAf//AWAAAAMAsAAAAwAAAwBdrAk=,; sprop-sps=QgEBAWAAAAMAsAAAAwAAAwBdoAKAgC0WNrkky/AIAAADAAgAAAMBlQ==,; sprop-pps=RAHBcrRiQA==, +)"; + + nmos::sdp_parameters parse(const std::string& test_sdp) + { + auto test_description = sdp::parse_session_description(test_sdp); + return nmos::parse_session_description(test_description).first; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSdpParametersVideoH265) +{ + const auto s = parse(test_sdp_in_band); + + BST_REQUIRE_EQUAL(nmos::media_types::video_H265, nmos::get_media_type(s)); + + const auto params = nmos::get_video_H265_parameters(s); + + BST_REQUIRE_EQUAL(0, params.ptl.profile_space); + BST_REQUIRE_EQUAL(2, params.ptl.profile_id); // Main10 + BST_REQUIRE_EQUAL(0x40000000, params.ptl.profile_compatibility_indicator); + BST_REQUIRE_EQUAL(0xB00000000000, params.ptl.interop_constraints); + BST_REQUIRE_EQUAL(0, params.ptl.tier_flag); // Main tier + BST_REQUIRE_EQUAL(120, params.ptl.level_id); // level 4, i.e. 30 * 4 + + // the defaults, since these are absent from the fmtp + BST_REQUIRE_EQUAL(0, params.sprop_max_don_diff); + BST_REQUIRE_EQUAL(sdp::video_h265::tx_modes::SRST, params.tx_mode); + BST_REQUIRE(params.sprop_vps.empty()); + BST_REQUIRE(params.sprop_sps.empty()); + BST_REQUIRE(params.sprop_pps.empty()); + + // b=AS:20000 + BST_REQUIRE_EQUAL(20000, params.bit_rate); + + // derived attributes + BST_REQUIRE_EQUAL(nmos::video_h265::packet_transmission_modes::non_interleaved_nal_units, nmos::get_video_H265_packet_transmission_mode(params)); + BST_REQUIRE_EQUAL(nmos::video_h265::parameter_sets_transport_modes::in_band, nmos::get_video_H265_parameter_sets_transport_mode(params)); + + const auto profile_level = nmos::video_h265::parse_profile_tier_level(params.ptl); + BST_REQUIRE_EQUAL(nmos::video_h265::profiles::Main10, profile_level.first); + BST_REQUIRE_EQUAL(nmos::video_h265::levels::Main_4, profile_level.second); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSdpParametersVideoH265Defaults) +{ + // an fmtp with no profile-tier-level at all must default to Main, Main tier, level 3.1 + // See https://tools.ietf.org/html/rfc7798#section-7.1 + const std::string test_sdp = R"(v=0 +o=- 1443716955 1443716955 IN IP4 192.168.1.2 +s=Example H.265 Sender +t=0 0 +m=video 30000 RTP/AVP 96 +c=IN IP4 224.1.1.1/64 +a=rtpmap:96 H265/90000 +)"; + const auto params = nmos::get_video_H265_parameters(parse(test_sdp)); + + BST_REQUIRE_EQUAL(1, params.ptl.profile_id); + BST_REQUIRE_EQUAL(0, params.ptl.tier_flag); + BST_REQUIRE_EQUAL(93, params.ptl.level_id); + BST_REQUIRE_EQUAL(0, params.bit_rate); // no b= line + + const auto profile_level = nmos::video_h265::parse_profile_tier_level(params.ptl); + BST_REQUIRE_EQUAL(nmos::video_h265::profiles::Main, profile_level.first); + BST_REQUIRE_EQUAL(nmos::video_h265::levels::Main_3_1, profile_level.second); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testParameterSetsTransportMode) +{ + // the trailing comma is what distinguishes out_of_band from in_and_out_of_band + const auto params = nmos::get_video_H265_parameters(parse(test_sdp_in_and_out_of_band)); + BST_REQUIRE_EQUAL(nmos::video_h265::parameter_sets_transport_modes::in_and_out_of_band, nmos::get_video_H265_parameter_sets_transport_mode(params)); + + // no trailing comma => out_of_band + nmos::video_H265_parameters out_of_band; + out_of_band.sprop_sps = U("QgEBAWAAAAMAsAAAAwAAAwBdoAKAgC0WNrkky/AIAAADAAgAAAMBlQ=="); + BST_REQUIRE_EQUAL(nmos::video_h265::parameter_sets_transport_modes::out_of_band, nmos::get_video_H265_parameter_sets_transport_mode(out_of_band)); + + // a lone "," means in_and_out_of_band, with nothing conveyed out-of-band + nmos::video_H265_parameters lone_comma; + lone_comma.sprop_sps = U(","); + BST_REQUIRE_EQUAL(nmos::video_h265::parameter_sets_transport_modes::in_and_out_of_band, nmos::get_video_H265_parameter_sets_transport_mode(lone_comma)); + + // absent entirely => in_band + nmos::video_H265_parameters in_band; + BST_REQUIRE_EQUAL(nmos::video_h265::parameter_sets_transport_modes::in_band, nmos::get_video_H265_parameter_sets_transport_mode(in_band)); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testProfileTierLevelRoundtrip) +{ + // every profile in the table must survive a round trip, for both tiers and a range of levels + const std::vector profiles{ + nmos::video_h265::profiles::Main, + nmos::video_h265::profiles::Main10, + nmos::video_h265::profiles::MainStillPicture + }; + const std::vector levels{ + nmos::video_h265::levels::Main_1, + nmos::video_h265::levels::Main_3_1, + nmos::video_h265::levels::Main_4, + nmos::video_h265::levels::Main_6_2, + nmos::video_h265::levels::High_4, + nmos::video_h265::levels::High_5_1, + nmos::video_h265::levels::High_8_5 + }; + + for (const auto& profile : profiles) + { + for (const auto& level : levels) + { + const auto ptl = nmos::video_h265::make_profile_tier_level(profile, level); + const auto profile_level = nmos::video_h265::parse_profile_tier_level(ptl); + BST_REQUIRE_EQUAL(profile, profile_level.first); + BST_REQUIRE_EQUAL(level, profile_level.second); + } + } + + // level-id encoding, i.e. general_level_idc == 30 * level + BST_REQUIRE_EQUAL(93, nmos::video_h265::make_profile_tier_level(nmos::video_h265::profiles::Main, nmos::video_h265::levels::Main_3_1).level_id); + BST_REQUIRE_EQUAL(120, nmos::video_h265::make_profile_tier_level(nmos::video_h265::profiles::Main, nmos::video_h265::levels::Main_4).level_id); + BST_REQUIRE_EQUAL(153, nmos::video_h265::make_profile_tier_level(nmos::video_h265::profiles::Main, nmos::video_h265::levels::High_5_1).level_id); + BST_REQUIRE_EQUAL(1, nmos::video_h265::make_profile_tier_level(nmos::video_h265::profiles::Main, nmos::video_h265::levels::High_5_1).tier_flag); + + // a profile outside the table must be rejected, not silently mapped to something plausible + BST_REQUIRE_THROW(nmos::video_h265::make_profile_tier_level(nmos::video_h265::profiles::ScreenExtendedMain10, nmos::video_h265::levels::Main_4), std::invalid_argument); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testValidateVideoH265SdpParameters) +{ + using web::json::value; + using web::json::value_of; + + const auto s = parse(test_sdp_in_band); + + auto receiver = nmos::make_receiver(U("receiver"), U("device"), nmos::transports::rtp, { U("eth0") }, nmos::formats::video, { nmos::media_types::video_H265 }, nmos::settings{}); + + // a constraint set that the Sender satisfies + receiver.data[nmos::fields::caps][nmos::fields::constraint_sets] = value_of({ + value_of({ + { nmos::caps::format::profile, nmos::make_caps_string_constraint({ nmos::video_h265::profiles::Main.name, nmos::video_h265::profiles::Main10.name }) }, + { nmos::caps::format::level, nmos::make_caps_string_constraint({ nmos::video_h265::levels::Main_4.name }) }, + { nmos::caps::transport::bit_rate, nmos::make_caps_integer_constraint({}, nmos::no_minimum(), 50000) }, + { nmos::caps::transport::packet_transmission_mode, nmos::make_caps_string_constraint({ nmos::video_h265::packet_transmission_modes::non_interleaved_nal_units.name }) }, + { nmos::caps::transport::parameter_sets_transport_mode, nmos::make_caps_string_constraint({ nmos::video_h265::parameter_sets_transport_modes::in_band.name }) } + }) + }); + BST_REQUIRE_NO_THROW(nmos::validate_video_H265_sdp_parameters(receiver.data, s)); + + // the Sender is Main10, so a Main-only Receiver must reject it + receiver.data[nmos::fields::caps][nmos::fields::constraint_sets] = value_of({ + value_of({ + { nmos::caps::format::profile, nmos::make_caps_string_constraint({ nmos::video_h265::profiles::Main.name }) } + }) + }); + BST_REQUIRE_THROW(nmos::validate_video_H265_sdp_parameters(receiver.data, s), std::runtime_error); + + // the Sender is 20000 kbit/s, so a Receiver capped lower must reject it + receiver.data[nmos::fields::caps][nmos::fields::constraint_sets] = value_of({ + value_of({ + { nmos::caps::transport::bit_rate, nmos::make_caps_integer_constraint({}, nmos::no_minimum(), 10000) } + }) + }); + BST_REQUIRE_THROW(nmos::validate_video_H265_sdp_parameters(receiver.data, s), std::runtime_error); + + // the Sender conveys parameter sets in-band, so an out-of-band-only Receiver must reject it + receiver.data[nmos::fields::caps][nmos::fields::constraint_sets] = value_of({ + value_of({ + { nmos::caps::transport::parameter_sets_transport_mode, nmos::make_caps_string_constraint({ nmos::video_h265::parameter_sets_transport_modes::out_of_band.name }) } + }) + }); + BST_REQUIRE_THROW(nmos::validate_video_H265_sdp_parameters(receiver.data, s), std::runtime_error); + + // constraints the SDP cannot speak to - width, height, sampling, depth - must fail *open*, + // not closed, since RFC 7798 carries none of them + receiver.data[nmos::fields::caps][nmos::fields::constraint_sets] = value_of({ + value_of({ + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({ 1920 }) }, + { nmos::caps::format::frame_height, nmos::make_caps_integer_constraint({ 1080 }) }, + { nmos::caps::format::color_sampling, nmos::make_caps_string_constraint({ U("YCbCr-4:2:0") }) } + }) + }); + BST_REQUIRE_NO_THROW(nmos::validate_video_H265_sdp_parameters(receiver.data, s)); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testValidateVideoH265RejectsNonSrst) +{ + // BCP-006-03 permits only Single RTP stream transport + const std::string test_sdp = R"(v=0 +o=- 1443716955 1443716955 IN IP4 192.168.1.2 +s=Example H.265 Sender +t=0 0 +m=video 30000 RTP/AVP 96 +c=IN IP4 224.1.1.1/64 +a=rtpmap:96 H265/90000 +a=fmtp:96 profile-id=1; level-id=93; tx-mode=MRST +)"; + auto receiver = nmos::make_receiver(U("receiver"), U("device"), nmos::transports::rtp, { U("eth0") }, nmos::formats::video, { nmos::media_types::video_H265 }, nmos::settings{}); + BST_REQUIRE_THROW(nmos::validate_video_H265_sdp_parameters(receiver.data, parse(test_sdp)), std::runtime_error); +}