Skip to content

Commit c4483d5

Browse files
committed
feat(ssa): add guarded ASS \pos positioning for CEA-608 captions
1 parent d573548 commit c4483d5

File tree

3 files changed

+88
-2
lines changed

3 files changed

+88
-2
lines changed

docs/CHANGES.TXT

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
0.96 (2025-12-21)
22
-----------------
3+
- New: Added ASS/SSA \pos-based positioning for CEA-608 captions when layout
34
- New: Added --list-tracks (-L) option to list all tracks in media files without processing
45
- Fix: Garbled captions from HDHomeRun and I/P-only H.264 streams (#1109)
56
- Fix: Enable stdout output for CEA-708 captions on Windows (#1693)

src/lib_ccx/ccx_encoders_common.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ static const char *ssa_header =
4848
"[Script Info]\n\
4949
Title: Default file\n\
5050
ScriptType: v4.00+\n\
51+
PlayResX: 384\n\
52+
PlayResY: 288\n\
5153
\n\
5254
[V4+ Styles]\n\
5355
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n\

src/lib_ccx/ccx_encoders_ssa.c

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,25 @@
55
#include "ccx_encoders_helpers.h"
66
#include "ocr.h"
77

8+
static void ass_position_from_row(
9+
int row,
10+
int play_res_x,
11+
int play_res_y,
12+
int *out_x,
13+
int *out_y)
14+
{
15+
// Center horizontally
16+
*out_x = play_res_x / 2;
17+
18+
// Map CEA-608 row (0–14) to ASS Y coordinate
19+
// SSA default PlayResY is 288
20+
int top = play_res_y * 60 / 100; // start of lower third
21+
int bottom = play_res_y * 95 / 100;
22+
23+
int y = top + (row * (bottom - top) / 14);
24+
*out_y = y;
25+
}
26+
827
int write_stringz_as_ssa(char *string, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end)
928
{
1029
int used;
@@ -168,11 +187,18 @@ int write_cc_buffer_as_ssa(struct eia608_screen *data, struct encoder_ctx *conte
168187
unsigned h1, m1, s1, ms1;
169188
unsigned h2, m2, s2, ms2;
170189
int wrote_something = 0;
190+
int used_row_count = 0;
171191

172192
int prev_line_start = -1, prev_line_end = -1; // Column in which the previous line started and ended, for autodash
173193
int prev_line_center1 = -1, prev_line_center2 = -1; // Center column of previous line text
174194
int empty_buf = 1;
175-
for (int i = 0; i < 15; i++)
195+
196+
int first_row = -1;
197+
int x, y;
198+
char pos_tag[64];
199+
int i;
200+
201+
for (i = 0; i < 15; i++)
176202
{
177203
if (data->row_used[i])
178204
{
@@ -183,6 +209,12 @@ int write_cc_buffer_as_ssa(struct eia608_screen *data, struct encoder_ctx *conte
183209
if (empty_buf)
184210
return 0;
185211

212+
for (i = 0; i < 15; i++)
213+
{
214+
if (data->row_used[i])
215+
used_row_count++;
216+
}
217+
186218
millis_to_time(data->start_time, &h1, &m1, &s1, &ms1);
187219
millis_to_time(data->end_time - 1, &h2, &m2, &s2, &ms2); // -1 To prevent overlapping with next line.
188220
char timeline[128];
@@ -194,8 +226,59 @@ int write_cc_buffer_as_ssa(struct eia608_screen *data, struct encoder_ctx *conte
194226
dbg_print(CCX_DMT_DECODER_608, "%s", timeline);
195227

196228
write_wrapped(context->out->fh, context->buffer, used);
229+
230+
/*
231+
* ASS precise positioning note:
232+
* We emit {\an2\pos(x,y)} using ASS script resolution coordinates.
233+
* PlayResX/PlayResY are explicitly declared in the SSA header (384x288),
234+
* which is the SSA/libass default resolution and ensures consistent
235+
* positioning across players.
236+
*
237+
* Positioning is intentionally guarded to avoid regressions when
238+
* caption layout information is ambiguous.
239+
*/
240+
241+
/* ---- ASS precise positioning ---- */
242+
243+
first_row = -1;
244+
245+
/*
246+
* Only apply ASS positioning when:
247+
* - At least one row is present
248+
* - AND there is a single logical caption region
249+
* Otherwise, fall back to legacy SSA behavior.
250+
*/
251+
252+
if (used_row_count > 0 && used_row_count <= 2)
253+
{
254+
for (i = 0; i < 15; i++)
255+
{
256+
if (data->row_used[i])
257+
{
258+
first_row = i;
259+
break;
260+
}
261+
}
262+
263+
if (first_row >= 0)
264+
{
265+
// SSA default resolution (used by libass / Aegisub)
266+
ass_position_from_row(first_row, 384, 288, &x, &y);
267+
268+
snprintf(
269+
pos_tag,
270+
sizeof(pos_tag),
271+
"{\\an2\\pos(%d,%d)}",
272+
x, y);
273+
274+
write_wrapped(context->out->fh, pos_tag, strlen(pos_tag));
275+
}
276+
}
277+
278+
/* ---- end ASS positioning ---- */
279+
197280
int line_count = 0;
198-
for (int i = 0; i < 15; i++)
281+
for (i = 0; i < 15; i++)
199282
{
200283
if (data->row_used[i])
201284
{

0 commit comments

Comments
 (0)