Skip to content

Commit 87fbb5b

Browse files
cfsmp3claude
andcommitted
fix(hevc): Add HEVC/H.265 caption extraction support
Fixes #1690 - Captions fail to extract on HEVC video stream HEVC video streams with embedded EIA-608/708 captions weren't being extracted, even though VLC/MPV could display them. Root causes fixed: 1. HEVC stream type (0x24) wasn't recognized for CC extraction 2. HEVC NAL parsing used H.264 format (1-byte) instead of HEVC (2-byte) 3. HEVC SEI types (39/40) weren't handled (only H.264 SEI type 6) 4. CC data accumulation across SEIs caused u8 overflow/garbled output Changes: - C code: Add HEVC stream detection, CCX_HEVC buffer type, is_hevc flag - Rust code: HEVC NAL header parsing (2-byte, type=(byte[0]>>1)&0x3F), HEVC SEI handling (PREFIX_SEI=39, SUFFIX_SEI=40), immediate CC flush Thanks to @trufio465-bot for the initial research in PR #1735. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 6c44100 commit 87fbb5b

File tree

10 files changed

+304
-113
lines changed

10 files changed

+304
-113
lines changed

src/lib_ccx/avc_functions.c

Lines changed: 80 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ struct avc_ctx *init_avc(void)
4848
ctx->cc_databufsize = 1024;
4949
ctx->cc_buffer_saved = CCX_TRUE; // Was the CC buffer saved after it was last updated?
5050

51+
ctx->is_hevc = 0;
5152
ctx->got_seq_para = 0;
5253
ctx->nal_ref_idc = 0;
5354
ctx->seq_parameter_set_id = 0;
@@ -87,62 +88,108 @@ struct avc_ctx *init_avc(void)
8788
return ctx;
8889
}
8990

91+
// HEVC NAL unit types for SEI messages
92+
#define HEVC_NAL_PREFIX_SEI 39
93+
#define HEVC_NAL_SUFFIX_SEI 40
94+
#define HEVC_NAL_VPS 32
95+
#define HEVC_NAL_SPS 33
96+
#define HEVC_NAL_PPS 34
97+
9098
void do_NAL(struct encoder_ctx *enc_ctx, struct lib_cc_decode *dec_ctx, unsigned char *NAL_start, LLONG NAL_length, struct cc_subtitle *sub)
9199
{
92100
unsigned char *NAL_stop;
93-
enum ccx_avc_nal_types nal_unit_type = *NAL_start & 0x1F;
101+
int nal_unit_type;
102+
int nal_header_size;
103+
unsigned char *payload_start;
104+
105+
// Determine if this is HEVC or H.264 based on NAL header
106+
// H.264 NAL header: 1 byte, type in bits [4:0]
107+
// HEVC NAL header: 2 bytes, type in bits [6:1] of first byte
108+
if (dec_ctx->avc_ctx->is_hevc)
109+
{
110+
// HEVC: NAL type is in bits [6:1] of byte 0
111+
nal_unit_type = (NAL_start[0] >> 1) & 0x3F;
112+
nal_header_size = 2;
113+
}
114+
else
115+
{
116+
// H.264: NAL type is in bits [4:0] of byte 0
117+
nal_unit_type = NAL_start[0] & 0x1F;
118+
nal_header_size = 1;
119+
}
94120

95121
NAL_stop = NAL_length + NAL_start;
96-
NAL_stop = remove_03emu(NAL_start + 1, NAL_stop); // Add +1 to NAL_stop for TS, without it for MP4. Still don't know why
122+
NAL_stop = remove_03emu(NAL_start + nal_header_size, NAL_stop);
123+
payload_start = NAL_start + nal_header_size;
97124

98-
dvprint("BEGIN NAL unit type: %d length %d ref_idc: %d - Buffered captions before: %d\n",
99-
nal_unit_type, NAL_stop - NAL_start - 1, dec_ctx->avc_ctx->nal_ref_idc, !dec_ctx->avc_ctx->cc_buffer_saved);
125+
dvprint("BEGIN NAL unit type: %d length %d ref_idc: %d - Buffered captions before: %d (HEVC: %d)\n",
126+
nal_unit_type, NAL_stop - NAL_start - nal_header_size, dec_ctx->avc_ctx->nal_ref_idc,
127+
!dec_ctx->avc_ctx->cc_buffer_saved, dec_ctx->avc_ctx->is_hevc);
100128

101129
if (NAL_stop == NULL) // remove_03emu failed.
102130
{
103131
mprint("\rNotice: NAL of type %u had to be skipped because remove_03emu failed.\n", nal_unit_type);
104132
return;
105133
}
106134

107-
if (nal_unit_type == CCX_NAL_TYPE_ACCESS_UNIT_DELIMITER_9)
108-
{
109-
// Found Access Unit Delimiter
110-
}
111-
else if (nal_unit_type == CCX_NAL_TYPE_SEQUENCE_PARAMETER_SET_7)
112-
{
113-
// Found sequence parameter set
114-
// We need this to parse NAL type 1 (CCX_NAL_TYPE_CODED_SLICE_NON_IDR_PICTURE_1)
115-
dec_ctx->avc_ctx->num_nal_unit_type_7++;
116-
seq_parameter_set_rbsp(dec_ctx->avc_ctx, NAL_start + 1, NAL_stop);
117-
dec_ctx->avc_ctx->got_seq_para = 1;
118-
}
119-
else if (dec_ctx->avc_ctx->got_seq_para && (nal_unit_type == CCX_NAL_TYPE_CODED_SLICE_NON_IDR_PICTURE_1 ||
120-
nal_unit_type == CCX_NAL_TYPE_CODED_SLICE_IDR_PICTURE)) // Only if nal_unit_type=1
135+
if (dec_ctx->avc_ctx->is_hevc)
121136
{
122-
// Found coded slice of a non-IDR picture
123-
// We only need the slice header data, no need to implement
124-
// slice_layer_without_partitioning_rbsp( );
125-
slice_header(enc_ctx, dec_ctx, NAL_start + 1, NAL_stop, nal_unit_type, sub);
126-
}
127-
else if (dec_ctx->avc_ctx->got_seq_para && nal_unit_type == CCX_NAL_TYPE_SEI)
128-
{
129-
// Found SEI (used for subtitles)
130-
// set_fts(ctx->timing); // FIXME - check this!!!
131-
sei_rbsp(dec_ctx->avc_ctx, NAL_start + 1, NAL_stop);
137+
// HEVC NAL unit processing
138+
if (nal_unit_type == HEVC_NAL_VPS || nal_unit_type == HEVC_NAL_SPS || nal_unit_type == HEVC_NAL_PPS)
139+
{
140+
// Found HEVC parameter set - mark as having sequence params
141+
// We don't parse HEVC SPS fully, but we need to enable SEI processing
142+
dec_ctx->avc_ctx->got_seq_para = 1;
143+
}
144+
else if (nal_unit_type == HEVC_NAL_PREFIX_SEI || nal_unit_type == HEVC_NAL_SUFFIX_SEI)
145+
{
146+
// Found HEVC SEI (used for subtitles)
147+
// SEI payload format is similar to H.264
148+
sei_rbsp(dec_ctx->avc_ctx, payload_start, NAL_stop);
149+
}
132150
}
133-
else if (dec_ctx->avc_ctx->got_seq_para && nal_unit_type == CCX_NAL_TYPE_PICTURE_PARAMETER_SET)
151+
else
134152
{
135-
// Found Picture parameter set
153+
// H.264 NAL unit processing (original code)
154+
if (nal_unit_type == CCX_NAL_TYPE_ACCESS_UNIT_DELIMITER_9)
155+
{
156+
// Found Access Unit Delimiter
157+
}
158+
else if (nal_unit_type == CCX_NAL_TYPE_SEQUENCE_PARAMETER_SET_7)
159+
{
160+
// Found sequence parameter set
161+
// We need this to parse NAL type 1 (CCX_NAL_TYPE_CODED_SLICE_NON_IDR_PICTURE_1)
162+
dec_ctx->avc_ctx->num_nal_unit_type_7++;
163+
seq_parameter_set_rbsp(dec_ctx->avc_ctx, payload_start, NAL_stop);
164+
dec_ctx->avc_ctx->got_seq_para = 1;
165+
}
166+
else if (dec_ctx->avc_ctx->got_seq_para && (nal_unit_type == CCX_NAL_TYPE_CODED_SLICE_NON_IDR_PICTURE_1 ||
167+
nal_unit_type == CCX_NAL_TYPE_CODED_SLICE_IDR_PICTURE))
168+
{
169+
// Found coded slice of a non-IDR picture
170+
// We only need the slice header data
171+
slice_header(enc_ctx, dec_ctx, payload_start, NAL_stop, nal_unit_type, sub);
172+
}
173+
else if (dec_ctx->avc_ctx->got_seq_para && nal_unit_type == CCX_NAL_TYPE_SEI)
174+
{
175+
// Found SEI (used for subtitles)
176+
sei_rbsp(dec_ctx->avc_ctx, payload_start, NAL_stop);
177+
}
178+
else if (dec_ctx->avc_ctx->got_seq_para && nal_unit_type == CCX_NAL_TYPE_PICTURE_PARAMETER_SET)
179+
{
180+
// Found Picture parameter set
181+
}
136182
}
183+
137184
if (temp_debug)
138185
{
139-
int len = NAL_stop - (NAL_start + 1);
186+
int len = NAL_stop - payload_start;
140187
dbg_print(CCX_DMT_VIDES, "\n After decoding, the actual thing was (length =%d)\n", len);
141-
dump(CCX_DMT_VIDES, NAL_start + 1, len > 160 ? 160 : len, 0, 0);
188+
dump(CCX_DMT_VIDES, payload_start, len > 160 ? 160 : len, 0, 0);
142189
}
143190

144191
dvprint("END NAL unit type: %d length %d ref_idc: %d - Buffered captions after: %d\n",
145-
nal_unit_type, NAL_stop - NAL_start - 1, dec_ctx->avc_ctx->nal_ref_idc, !dec_ctx->avc_ctx->cc_buffer_saved);
192+
nal_unit_type, NAL_stop - NAL_start - nal_header_size, dec_ctx->avc_ctx->nal_ref_idc, !dec_ctx->avc_ctx->cc_buffer_saved);
146193
}
147194

148195
// Process inbuf bytes in buffer holding and AVC (H.264) video stream.

src/lib_ccx/avc_functions.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ struct avc_ctx
99
long cc_databufsize;
1010
int cc_buffer_saved; // Was the CC buffer saved after it was last updated?
1111

12+
int is_hevc; // Flag to indicate HEVC (H.265) mode vs H.264
1213
int got_seq_para;
1314
unsigned nal_ref_idc;
1415
LLONG seq_parameter_set_id;

src/lib_ccx/ccx_common_constants.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,8 @@ enum ccx_bufferdata_type
237237
CCX_ISDB_SUBTITLE = 8,
238238
/* BUffer where cc data contain 3 byte cc_valid ccdata 1 ccdata 2 */
239239
CCX_RAW_TYPE = 9,
240-
CCX_DVD_SUBTITLE = 10
240+
CCX_DVD_SUBTITLE = 10,
241+
CCX_HEVC = 11
241242
};
242243

243244
enum ccx_frame_type

src/lib_ccx/general_loop.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,13 @@ int process_data(struct encoder_ctx *enc_ctx, struct lib_cc_decode *dec_ctx, str
763763
else if (data_node->bufferdatatype == CCX_H264) // H.264 data from TS file
764764
{
765765
dec_ctx->in_bufferdatatype = CCX_H264;
766+
dec_ctx->avc_ctx->is_hevc = 0;
767+
got = process_avc(enc_ctx, dec_ctx, data_node->buffer, data_node->len, dec_sub);
768+
}
769+
else if (data_node->bufferdatatype == CCX_HEVC) // HEVC data from TS file
770+
{
771+
dec_ctx->in_bufferdatatype = CCX_H264; // Use same internal type for NAL processing
772+
dec_ctx->avc_ctx->is_hevc = 1;
766773
got = process_avc(enc_ctx, dec_ctx, data_node->buffer, data_node->len, dec_sub);
767774
}
768775
else if (data_node->bufferdatatype == CCX_RAW_TYPE)

src/lib_ccx/ts_functions.c

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ enum ccx_bufferdata_type get_buffer_type(struct cap_info *cinfo)
135135
}
136136
else if (cinfo->stream == CCX_STREAM_TYPE_VIDEO_HEVC)
137137
{
138-
return CCX_H264; // HEVC uses same buffer type as H264
138+
return CCX_HEVC;
139139
}
140140
else if (cinfo->stream == CCX_STREAM_TYPE_PRIVATE_MPEG2 && cinfo->codec == CCX_CODEC_DVB)
141141
{
@@ -661,7 +661,9 @@ int copy_payload_to_capbuf(struct cap_info *cinfo, struct ts_payload *payload)
661661
int newcapbuflen;
662662

663663
if (cinfo->ignore == CCX_TRUE &&
664-
(cinfo->stream != CCX_STREAM_TYPE_VIDEO_MPEG2 || !ccx_options.analyze_video_stream))
664+
((cinfo->stream != CCX_STREAM_TYPE_VIDEO_MPEG2 &&
665+
cinfo->stream != CCX_STREAM_TYPE_VIDEO_H264 &&
666+
cinfo->stream != CCX_STREAM_TYPE_VIDEO_HEVC) || !ccx_options.analyze_video_stream))
665667
{
666668
return CCX_OK;
667669
}
@@ -948,7 +950,9 @@ long ts_readstream(struct ccx_demuxer *ctx, struct demuxer_data **data)
948950
continue;
949951
}
950952
else if (cinfo->ignore == CCX_TRUE &&
951-
(cinfo->stream != CCX_STREAM_TYPE_VIDEO_MPEG2 || !ccx_options.analyze_video_stream))
953+
((cinfo->stream != CCX_STREAM_TYPE_VIDEO_MPEG2 &&
954+
cinfo->stream != CCX_STREAM_TYPE_VIDEO_H264 &&
955+
cinfo->stream != CCX_STREAM_TYPE_VIDEO_HEVC) || !ccx_options.analyze_video_stream))
952956
{
953957
if (cinfo->codec_private_data)
954958
{

src/lib_ccx/ts_info.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,9 @@ int get_video_stream(struct ccx_demuxer *ctx)
102102
struct cap_info *iter;
103103
list_for_each_entry(iter, &ctx->cinfo_tree.all_stream, all_stream, struct cap_info)
104104
{
105-
if (iter->stream == CCX_STREAM_TYPE_VIDEO_MPEG2)
105+
if (iter->stream == CCX_STREAM_TYPE_VIDEO_MPEG2 ||
106+
iter->stream == CCX_STREAM_TYPE_VIDEO_H264 ||
107+
iter->stream == CCX_STREAM_TYPE_VIDEO_HEVC)
106108
return iter->pid;
107109
}
108110
return -1;

src/rust/src/avc/common_types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub struct AvcContextRust {
99
pub cc_databufsize: usize,
1010
pub cc_buffer_saved: bool,
1111

12+
pub is_hevc: bool,
1213
pub got_seq_para: bool,
1314
pub nal_ref_idc: u32,
1415
pub seq_parameter_set_id: i64,
@@ -55,6 +56,7 @@ impl Default for AvcContextRust {
5556
cc_databufsize: 1024,
5657
cc_buffer_saved: true,
5758

59+
is_hevc: false,
5860
got_seq_para: false,
5961
nal_ref_idc: 0,
6062
seq_parameter_set_id: 0,

0 commit comments

Comments
 (0)