From f4a24d6c7bdb7f56539f0a6355dd58109cccf3fa Mon Sep 17 00:00:00 2001 From: Daniel Campos Ramos Date: Mon, 5 Oct 2026 13:42:11 -0300 Subject: [PATCH] Add --frame-packing option to emit the frame packing arrangement SEI x265 defined the SEI payload type FRAME_PACKING but had no way to write it, so an HEVC encode of side-by-side or top-and-bottom 3D video could not carry the in-stream mark that players and 3D televisions read (see #970). --frame-packing takes the values H.265 defines in Table D-8: 3 side by side, 4 top and bottom, 5 temporal interleaving. Any other value is an error. It is disabled by default and the bitstream does not change without it. As x264 does, frame 0 is signalled as the left view (content_interpretation_type 1). The message follows H.265 D.2.16 and is written as a prefix SEI with each IRAP picture (types 3 and 4, persistence flag set) or with every picture (type 5, where current_frame_is_frame0_flag alternates and the persistence flag is clear). H.265 D.3.16 defines packing in terms of frames even when field_seq_flag indicates field pictures, and current_frame_is_frame0_flag identifies the current decoded frame for type 5. With --interlace each picture is a field, whether the input is already fields or --field splits input frames, so take the view from POC >> 1 in both cases: L L R R. Keep field_views_flag zero as required by D.3.16; VUI field_seq_flag (E.3.1) identifies field pictures. The first encoded picture is the left view, so an odd --seek swaps the views; this is documented. The view count continues across CVS boundaries. The message describes the packed pictures of the base layer. It is written with nuh_layer_id 0 only: the alpha layer of --alpha is an auxiliary picture, not a view. It is rejected with --num-views greater than 1, because each layer of an MV-HEVC stream (H.265 F.7 and G.7) is a full view and a frame packing arrangement SEI would describe it as half of a packed picture. Keep the veryfast field regression consistent with its neighbours and set b-adapt to 2 explicitly, as --field currently rejects b-adapt 0. X265_BUILD is incremented as x265_param gets a new member. SEIFramePacking::writeSEI() carries an inline cppcheck suppression for missingOverride, like the other SEI classes it has no override specifier: the default build is -std=gnu++98, where the specifier is not available. Assisted-by: Claude Sonnet 5.5 via Claude Code Assisted-by: gpt-6.1-sol via Codex --- doc/reST/cli.rst | 26 ++++++++++++++++++++ source/CMakeLists.txt | 2 +- source/common/param.cpp | 10 ++++++++ source/encoder/frameencoder.cpp | 16 ++++++++++++ source/encoder/sei.h | 42 ++++++++++++++++++++++++++++++++ source/test/regression-tests.txt | 6 +++++ source/test/smoke-tests.txt | 3 +++ source/x265.h | 11 +++++++++ source/x265cli.cpp | 5 ++++ source/x265cli.h | 1 + 10 files changed, 121 insertions(+), 1 deletion(-) diff --git a/doc/reST/cli.rst b/doc/reST/cli.rst index 82040d1e6..76747fe76 100755 --- a/doc/reST/cli.rst +++ b/doc/reST/cli.rst @@ -2767,6 +2767,32 @@ Bitstream options Emit SEI messages in a single NAL unit instead of multiple NALs. Default disabled. When HRD SEI is enabled the HM decoder will throw a warning. +.. option:: --frame-packing + + Emit the frame packing arrangement SEI message (H.265 D.2.16, D.3.16) that + tells the decoder how the two views of a stereoscopic video are packed into + the coded pictures. Frame 0 is always the left view. The values are those + that H.265 defines (Table D.8), any other value is an error: + + 3. Side by side: the left view is on the left, the right view on the right. + 4. Top and bottom: the left view is on top, the right view on the bottom. + 5. Temporal interleaving: alternate frames in output order are the left + and right views. + + Types 3 and 4 write the message with each IRAP picture, and it persists until + the next one or the end of the coded video sequence. Type 5 writes it with every picture, because + current_frame_is_frame0_flag alternates with the frame. The first encoded + picture is the left view, so an odd :option:`--seek` swaps the views. + With :option:`--interlace` both fields of a frame belong to the same view, + whether the input pictures are already fields or :option:`--field` splits + each input frame into fields. + + The message is written on the base layer only, so with :option:`--alpha` the + alpha layer does not carry it. It cannot be combined with + :option:`--num-views` greater than 1: each layer is then a full view and the + message would describe it as packed, so x265 gives an error. + The source must already be packed this way, x265 only signals it. Default disabled. + .. option:: --film-grain Refers to the film grain model characteristics for signal enhancement information transmission. diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index cbda0ca65..e18930d89 100755 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -32,7 +32,7 @@ option(NATIVE_BUILD "Target the build CPU" OFF) option(STATIC_LINK_CRT "Statically link C and C++ runtimes for release builds" OFF) mark_as_advanced(FPROFILE_USE FPROFILE_GENERATE NATIVE_BUILD) # X265_BUILD must be incremented each time the public API is changed -set(X265_BUILD 217) +set(X265_BUILD 218) configure_file("${PROJECT_SOURCE_DIR}/x265.def.in" "${PROJECT_BINARY_DIR}/x265.def") configure_file("${PROJECT_SOURCE_DIR}/x265_config.h.in" diff --git a/source/common/param.cpp b/source/common/param.cpp index 3ba88204a..1423cab5b 100755 --- a/source/common/param.cpp +++ b/source/common/param.cpp @@ -406,6 +406,7 @@ void x265_param_default(x265_param* param) /* SEI messages */ param->preferredTransferCharacteristics = -1; + param->framePacking = -1; param->pictureStructure = -1; param->bEmitCLL = 1; @@ -1436,6 +1437,7 @@ int x265_param_parse(x265_param* p, const char* name, const char* value) OPT("dynamic-refine") p->bDynamicRefine = atobool(value); OPT("single-sei") p->bSingleSeiNal = atobool(value); OPT("atc-sei") p->preferredTransferCharacteristics = atoi(value); + OPT("frame-packing") p->framePacking = atoi(value); OPT("pic-struct") p->pictureStructure = atoi(value); OPT("chunk-start") p->chunkStart = atoi(value); OPT("chunk-end") p->chunkEnd = atoi(value); @@ -2024,6 +2026,7 @@ int x265_check_params(x265_param* param) || param->bEmitIDRRecoverySEI || !!param->interlaceMode || param->preferredTransferCharacteristics > 1 + || param->framePacking > -1 || strlen(param->toneMapFile) || strlen(param->naluFile)); @@ -2032,6 +2035,10 @@ int x265_check_params(x265_param* param) param->bSingleSeiNal = 0; x265_log(param, X265_LOG_WARNING, "None of the SEI messages are enabled. Disabling Single SEI NAL\n"); } + CHECK(param->framePacking != -1 && (param->framePacking < 3 || param->framePacking > 5), + "Frame packing arrangement type must be 3 (side by side), 4 (top and bottom) or 5 (temporal interleaving)"); + CHECK(param->framePacking != -1 && param->numViews > 1, + "Frame packing arrangement SEI cannot be used with multi-view encoding, each layer is a full view"); CHECK(param->confWinRightOffset < 0, "Conformance Window Right Offset must be 0 or greater"); CHECK(param->confWinBottomOffset < 0, "Conformance Window Bottom Offset must be 0 or greater"); CHECK(param->decoderVbvMaxRate < 0, "Invalid Decoder Vbv Maxrate. Value can not be less than zero"); @@ -2508,6 +2515,8 @@ char *x265_param2string(x265_param* p, int padx, int pady) BOOL(p->bHDR10Opt, "hdr10-opt"); BOOL(p->bDhdr10opt, "dhdr10-opt"); BOOL(p->bEmitIDRRecoverySEI, "idr-recovery-sei"); + if (p->framePacking > -1) + s += snprintf(s, bufSize - (s - buf), " frame-packing=%d", p->framePacking); if (strlen(p->analysisSave)) s += snprintf(s, bufSize - (s - buf), " analysis-save"); if (strlen(p->analysisLoad)) @@ -2897,6 +2906,7 @@ void x265_copy_params(x265_param* dst, x265_param* src) dst->cbQpOffset = src->cbQpOffset; dst->crQpOffset = src->crQpOffset; dst->preferredTransferCharacteristics = src->preferredTransferCharacteristics; + dst->framePacking = src->framePacking; dst->pictureStructure = src->pictureStructure; dst->rc.rateControlMode = src->rc.rateControlMode; diff --git a/source/encoder/frameencoder.cpp b/source/encoder/frameencoder.cpp index 77d013222..9ba7ca51e 100644 --- a/source/encoder/frameencoder.cpp +++ b/source/encoder/frameencoder.cpp @@ -854,6 +854,21 @@ void FrameEncoder::compressFrame(int layer) m_seiAlternativeTC.m_preferredTransferCharacteristics = m_param->preferredTransferCharacteristics; m_seiAlternativeTC.writeSEImessages(m_bs, *slice->m_sps, NAL_UNIT_PREFIX_SEI, m_nalList, m_param->bSingleSeiNal, layer); } + + /* The message describes the packed pictures of the base layer: an alpha layer is not a view. */ + bool writeFramePacking = m_param->framePacking > -1 && !layer && (slice->isIRAP() || m_param->framePacking == 5); + if (writeFramePacking) + { + /* Type 5 alternates the views frame by frame, so every picture carries its own message. The + * other types persist until the next IRAP, which repeats the message. With --interlace the + * pictures are fields and both fields of a frame belong to the same view. */ + int frameIdx = m_param->interlaceMode ? m_frame[layer]->m_poc >> 1 : m_frame[layer]->m_poc; + SEIFramePacking m_seiFramePacking; + m_seiFramePacking.m_arrangementType = m_param->framePacking; + m_seiFramePacking.m_currentFrameIsFrame0 = m_param->framePacking == 5 && !(frameIdx & 1); + m_seiFramePacking.m_persistence = m_param->framePacking != 5; + m_seiFramePacking.writeSEImessages(m_bs, *slice->m_sps, NAL_UNIT_PREFIX_SEI, m_nalList, m_param->bSingleSeiNal, layer); + } /* Write Film grain characteristics if present */ if (this->m_top->m_filmGrainIn) { @@ -898,6 +913,7 @@ void FrameEncoder::compressFrame(int layer) bool isSei = ((m_frame[layer]->m_lowres.bKeyframe && m_param->bRepeatHeaders) || m_param->bEmitHRDSEI || !!m_param->interlaceMode || (m_frame[layer]->m_lowres.sliceType == X265_TYPE_IDR && m_param->bEmitIDRRecoverySEI) || + writeFramePacking || m_frame[layer]->m_userSEI.numPayloads); if (isSei && m_param->bSingleSeiNal) diff --git a/source/encoder/sei.h b/source/encoder/sei.h index b7aaaee20..9e4b56b7c 100644 --- a/source/encoder/sei.h +++ b/source/encoder/sei.h @@ -902,5 +902,47 @@ class SEIAlternativeTC : public SEI } }; +class SEIFramePacking : public SEI +{ +public: + int m_arrangementType; + bool m_currentFrameIsFrame0; + bool m_persistence; + SEIFramePacking() + { + m_payloadType = FRAME_PACKING; + m_payloadSize = 0; + m_arrangementType = -1; + m_currentFrameIsFrame0 = false; + m_persistence = false; + } + + // cppcheck-suppress missingOverride + void writeSEI(const SPS&) + { + WRITE_UVLC(0, "frame_packing_arrangement_id"); + WRITE_FLAG(0, "frame_packing_arrangement_cancel_flag"); + WRITE_CODE(m_arrangementType, 7, "frame_packing_arrangement_type"); + WRITE_FLAG(0, "quincunx_sampling_flag"); + WRITE_CODE(1, 6, "content_interpretation_type"); /* frame 0 is the left view */ + WRITE_FLAG(0, "spatial_flipping_flag"); + WRITE_FLAG(0, "frame0_flipped_flag"); + WRITE_FLAG(0, "field_views_flag"); + WRITE_FLAG(m_currentFrameIsFrame0, "current_frame_is_frame0_flag"); + WRITE_FLAG(0, "frame0_self_contained_flag"); + WRITE_FLAG(0, "frame1_self_contained_flag"); + if (m_arrangementType != 5) + { + WRITE_CODE(0, 4, "frame0_grid_position_x"); + WRITE_CODE(0, 4, "frame0_grid_position_y"); + WRITE_CODE(0, 4, "frame1_grid_position_x"); + WRITE_CODE(0, 4, "frame1_grid_position_y"); + } + WRITE_CODE(0, 8, "frame_packing_arrangement_reserved_byte"); + WRITE_FLAG(m_persistence, "frame_packing_arrangement_persistence_flag"); + WRITE_FLAG(0, "upsampled_aspect_ratio_flag"); + } +}; + } #endif // ifndef X265_SEI_H diff --git a/source/test/regression-tests.txt b/source/test/regression-tests.txt index 7e96a2188..b967c6b2d 100644 --- a/source/test/regression-tests.txt +++ b/source/test/regression-tests.txt @@ -215,3 +215,9 @@ crowd_run_1080p50.y4m, --bitrate 5000 --selective-mcstf --preset veryslow #Fovea tests nature_1920x1080_25.yuv,--crf 28 --fovea-gaze 960,540 --fovea-delta 20 nature_1920x1080_25.yuv,--qp 28 --fovea-delta 20 --fovea-gaze-file gaze_nature.txt + +#Frame packing arrangement SEI tests +big_buck_bunny_360p24.y4m,--preset ultrafast --frame-packing 3 +big_buck_bunny_360p24.y4m,--preset superfast --bframes 4 --frame-packing 4 +big_buck_bunny_360p24.y4m,--preset veryfast --frame-packing 5 +big_buck_bunny_360p24.y4m,--preset veryfast --b-adapt 2 --bframes 0 --interlace tff --field --frame-packing 5 diff --git a/source/test/smoke-tests.txt b/source/test/smoke-tests.txt index 03c40ec1f..c9c4a9f62 100644 --- a/source/test/smoke-tests.txt +++ b/source/test/smoke-tests.txt @@ -28,3 +28,6 @@ CrowdRun_1920x1080_50_10bit_444.yuv,--preset=superfast --bitrate 7000 --sao --li #Threaded ME tests BasketballDrive_1920x1080_50.y4m,--preset=superfast --threaded-me --rect --amp --qp 28 + +#Frame packing arrangement SEI test +big_buck_bunny_360p24.y4m,--preset ultrafast --frame-packing 3 diff --git a/source/x265.h b/source/x265.h index 54ed2a2fc..ca749ec97 100644 --- a/source/x265.h +++ b/source/x265.h @@ -2403,6 +2403,17 @@ typedef struct x265_param * When set, overrides foveaGazeX/foveaGazeY with per-frame values. * NULL = use static gaze from foveaGazeX/foveaGazeY. */ char* foveaGazeFile; + + /* Frame packing arrangement SEI message (H.265 D.2.16): the 3D layout of the + * source frames. 3 = side by side, 4 = top and bottom (frame 0 is the left view), + * 5 = temporal interleaving (alternate frames are the left and right views). + * The first encoded picture is the left view, so an odd --seek swaps the views. + * With interlaceMode both fields of a frame belong to the same view. + * Other values are an error. + * Types 3 and 4 write the message with each IRAP picture, type 5 with every + * picture. It is written on the base layer only and cannot be combined with + * numViews > 1. Default -1 (disabled). */ + int framePacking; } x265_param; /* x265_param_alloc: diff --git a/source/x265cli.cpp b/source/x265cli.cpp index 0b1a2b63c..b4a85265a 100755 --- a/source/x265cli.cpp +++ b/source/x265cli.cpp @@ -379,6 +379,11 @@ namespace X265_NS { H0(" --[no-]eos Emit end of sequence nal unit at the end of every coded video sequence. Default %s\n", OPT(param->bEnableEndOfSequence)); H1(" --hash Decoded Picture Hash SEI 0: disabled, 1: MD5, 2: CRC, 3: Checksum. Default %d\n", param->decodedPictureHashSEI); H0(" --atc-sei Emit the alternative transfer characteristics SEI message where the integer is the preferred transfer characteristic. Default disabled\n"); + H0(" --frame-packing Emit the frame packing arrangement SEI message for stereoscopic video. Other values are an error. Default disabled\n"); + H0(" - 3: side by side - left view on the left, right view on the right\n"); + H0(" - 4: top and bottom - left view on top, right view on the bottom\n"); + H0(" - 5: temporal interleaving - alternate frames are the left and right views. The first encoded picture is the left view\n"); + H0(" An odd --seek swaps the views. With --interlace both fields of a frame belong to the same view\n"); H0(" --pic-struct Set the picture structure and emit it in the picture timing SEI message. Values in the range 0..12. See D.3.3 of the HEVC spec. for a detailed explanation.\n"); H0(" --log2-max-poc-lsb Maximum of the picture order count\n"); H0(" --[no-]vui-timing-info Emit VUI timing information in the bitstream. Default %s\n", OPT(param->bEmitVUITimingInfo)); diff --git a/source/x265cli.h b/source/x265cli.h index 6ac7d2ab1..4b7e59510 100644 --- a/source/x265cli.h +++ b/source/x265cli.h @@ -345,6 +345,7 @@ static const struct option long_options[] = { "single-sei", no_argument, NULL, 0 }, { "no-single-sei", no_argument, NULL, 0 }, { "atc-sei", required_argument, NULL, 0 }, + { "frame-packing", required_argument, NULL, 0 }, { "pic-struct", required_argument, NULL, 0 }, { "nalu-file", required_argument, NULL, 0 }, { "dolby-vision-rpu", required_argument, NULL, 0 },