Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/lib_ccx/ccx_common_option.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ void init_options(struct ccx_s_options *options)
options->live_stream = 0; // 0 -> A regular file
options->messages_target = 1; // 1=stdout
options->print_file_reports = 0;
options->timestamp_map = 0; // Disable X-TIMESTAMP-MAP header by default
options->timestamp_map = 0; // (Deprecated) X-TIMESTAMP-MAP is now always included in WebVTT output

/* Levenshtein's parameters, for string comparison */
options->dolevdist = 1; // By default attempt to correct typos
Expand Down
2 changes: 1 addition & 1 deletion src/lib_ccx/ccx_common_option.h
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ struct ccx_s_options // Options from user parameters
>0 -> Live stream with a timeout of this value in seconds */
char *filter_profanity_file; // Extra profanity word file
int messages_target; // 0 = nowhere (quiet), 1=stdout, 2=stderr
int timestamp_map; // If 1, add WebVTT X-TIMESTAMP-MAP header
int timestamp_map; // (Deprecated) X-TIMESTAMP-MAP is now always included in WebVTT output
/* Levenshtein's parameters, for string comparison */
int dolevdist; // 0 => don't attempt to correct typos with this algorithm
int levdistmincnt, levdistmaxpct; // Means 2 fails or less is "the same", 10% or less is also "the same"
Expand Down
9 changes: 9 additions & 0 deletions src/lib_ccx/ccx_encoders_common.c
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ int write_subtitle_file_footer(struct encoder_ctx *ctx, struct ccx_s_write *out)
case CCX_OF_CCD:
ret = write(out->fh, ctx->encoded_crlf, ctx->encoded_crlf_length);
break;
case CCX_OF_WEBVTT:
// Ensure X-TIMESTAMP-MAP header is written even if no subtitles were found
write_webvtt_header(ctx);
break;
default: // Nothing to do, no footer on this format
break;
}
Expand Down Expand Up @@ -733,6 +737,8 @@ void dinit_encoder(struct encoder_ctx **arg, LLONG current_fts)
int reset_output_ctx(struct encoder_ctx *ctx, struct encoder_cfg *cfg)
{
dinit_output_ctx(ctx);
// Reset header flags for new output file (important for segmented output)
ctx->wrote_webvtt_header = 0;
return init_output_ctx(ctx, cfg);
}

Expand Down Expand Up @@ -1331,6 +1337,9 @@ void switch_output_file(struct lib_ccx_ctx *ctx, struct encoder_ctx *enc_ctx, in
free(basename);
}

// Reset header flags for new output file
enc_ctx->wrote_webvtt_header = 0;

write_subtitle_file_header(enc_ctx, enc_ctx->out);

// Reset counters as we switch output file.
Expand Down
1 change: 1 addition & 0 deletions src/lib_ccx/ccx_encoders_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ int write_stringz_as_ssa(char *string, struct encoder_ctx *context, LLONG ms_sta
int write_stringz_as_webvtt(char *string, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end);
int write_stringz_as_sami(char *string, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end);
void write_stringz_as_smptett(char *string, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end);
void write_webvtt_header(struct encoder_ctx *context);

int write_cc_bitmap_as_srt(struct cc_subtitle *sub, struct encoder_ctx *context);
int write_cc_bitmap_as_ssa(struct cc_subtitle *sub, struct encoder_ctx *context);
Expand Down
28 changes: 12 additions & 16 deletions src/lib_ccx/ccx_encoders_webvtt.c
Original file line number Diff line number Diff line change
Expand Up @@ -212,34 +212,30 @@ void write_webvtt_header(struct encoder_ctx *context)
if (context->wrote_webvtt_header) // Already done
return;

if (ccx_options.timestamp_map && context->timing != NULL && context->timing->sync_pts2fts_set)
char header_string[200];
int used;

// Always write X-TIMESTAMP-MAP for HLS compliance (RFC 8216 §3.5)
// Use timing info when available, otherwise use safe default
if (context->timing != NULL && context->timing->sync_pts2fts_set)
{
char header_string[200];
int used;
unsigned h1, m1, s1, ms1;
millis_to_time(context->timing->sync_pts2fts_fts, &h1, &m1, &s1, &ms1);

// If the user has enabled X-TIMESTAMP-MAP
snprintf(header_string, sizeof(header_string), "X-TIMESTAMP-MAP=MPEGTS:%ld,LOCAL:%02u:%02u:%02u.%03u%s",
context->timing->sync_pts2fts_pts, h1, m1, s1, ms1,
ccx_options.enc_cfg.line_terminator_lf ? "\n\n" : "\r\n\r\n");

used = encode_line(context, context->buffer, (unsigned char *)header_string);
write_wrapped(context->out->fh, context->buffer, used);
}
else
{
// Must have another newline if X-TIMESTAMP-MAP is not used
if (ccx_options.enc_cfg.line_terminator_lf == 1) // If -lf parameter is set.
{
write_wrapped(context->out->fh, "\n", 1);
}
else
{
write_wrapped(context->out->fh, "\r\n", 2);
}
// No timing info available - use safe default for HLS compatibility
snprintf(header_string, sizeof(header_string), "X-TIMESTAMP-MAP=MPEGTS:0,LOCAL:00:00:00.000%s",
ccx_options.enc_cfg.line_terminator_lf ? "\n\n" : "\r\n\r\n");
}

used = encode_line(context, context->buffer, (unsigned char *)header_string);
write_wrapped(context->out->fh, context->buffer, used);

if (ccx_options.webvtt_create_css)
{
char *basefilename = get_basename(context->first_input_file);
Expand Down
2 changes: 1 addition & 1 deletion src/lib_ccx/params.c
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ void print_usage(void)
mprint(" less or equal than the max allowed..\n");
mprint(" --analyzevideo Analyze the video stream even if it's not used for\n");
mprint(" subtitles. This allows to provide video information.\n");
mprint(" --timestamp-map Enable the X-TIMESTAMP-MAP header for WebVTT (HLS)\n");
mprint(" --timestamp-map (Deprecated - X-TIMESTAMP-MAP is now always included in WebVTT output for HLS compliance)\n");
mprint("Levenshtein distance:\n");
mprint(" --no-levdist: Don't attempt to correct typos with Levenshtein distance.\n");
mprint(" --levdistmincnt value: Minimum distance we always allow regardless\n");
Expand Down
2 changes: 1 addition & 1 deletion src/rust/lib_ccxr/src/common/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ pub struct Options {
/// Extra profanity word file
pub filter_profanity_file: PathBuf,
pub messages_target: OutputTarget,
/// If true, add WebVTT X-TIMESTAMP-MAP header
/// (Deprecated) X-TIMESTAMP-MAP is now always included in WebVTT output for HLS compliance
pub timestamp_map: bool,
/* Levenshtein's parameters, for string comparison */
/// false => don't attempt to correct typos with this algorithm
Expand Down
2 changes: 1 addition & 1 deletion src/rust/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ pub struct Args {
/// subtitles. This allows to provide video information.
#[arg(long, verbatim_doc_comment, help_heading=OPTIONS_AFFECTING_INPUT_FILES)]
pub analyzevideo: bool,
/// Enable the X-TIMESTAMP-MAP header for WebVTT (HLS)
/// (Deprecated) X-TIMESTAMP-MAP is now always included in WebVTT output for HLS compliance
#[arg(long, verbatim_doc_comment, help_heading=OPTIONS_AFFECTING_INPUT_FILES)]
pub timestamp_map: bool,
/// Don't attempt to correct typos with Levenshtein distance.
Expand Down
50 changes: 50 additions & 0 deletions tests/webvtt_timestamp_map/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# WebVTT X-TIMESTAMP-MAP Test Suite

This directory contains tests to verify that CCExtractor always includes the `X-TIMESTAMP-MAP` header in WebVTT output for HLS compliance.

## What This Tests

1. **Real video with timing info** → Uses actual MPEGTS timestamp
2. **Empty/no subtitles** → Uses fallback `MPEGTS:0,LOCAL:00:00:00.000`
3. **Invalid datapid** → Still produces header with fallback
4. **All outputs have exactly ONE header** (no duplicates)

## Running the Tests

### Linux/WSL
```bash
cd tests/webvtt_timestamp_map
chmod +x run_tests.sh
./run_tests.sh /path/to/ccextractor
```

### Windows (PowerShell)
```powershell
cd tests\webvtt_timestamp_map
.\run_tests.ps1 -CCExtractorPath "C:\path\to\ccextractor.exe"
```

## Expected Results

All generated `.vtt` files should have:
- Line 1: `WEBVTT`
- Line 2: `X-TIMESTAMP-MAP=MPEGTS:<value>,LOCAL:<time>`
- Exactly ONE `X-TIMESTAMP-MAP` header per file

## Sample Output

With timing info available:
```
WEBVTT
X-TIMESTAMP-MAP=MPEGTS:129003,LOCAL:00:00:00.000

1
00:00:01.000 --> 00:00:03.000
Hello World
```

Without timing info (fallback):
```
WEBVTT
X-TIMESTAMP-MAP=MPEGTS:0,LOCAL:00:00:00.000
```
40 changes: 40 additions & 0 deletions tests/webvtt_timestamp_map/SAMPLES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Sample Test Files

This directory contains test scripts for verifying the WebVTT X-TIMESTAMP-MAP fix.

## Test Files

The tests automatically create minimal test files (empty TS packets) to verify:
1. WebVTT header is always written
2. X-TIMESTAMP-MAP is always present
3. Fallback value is used when no timing info exists

## Using Real Video Samples

For more comprehensive testing with real video files, you can:

1. **Download a sample TS file:**
```bash
curl -L "https://filesamples.com/samples/video/ts/sample_960x540.ts" -o sample.ts
```

2. **Run the tests:**
```bash
./run_tests.sh /path/to/ccextractor
```

The test script will automatically detect and use `sample.ts` if it exists.

## Expected Output

```
==========================================
WebVTT X-TIMESTAMP-MAP Test Suite
==========================================
Test: empty_ts_no_subs... PASSED [X-TIMESTAMP-MAP=MPEGTS:0,LOCAL:00:00:00.000]
Test: invalid_datapid... PASSED [X-TIMESTAMP-MAP=MPEGTS:0,LOCAL:00:00:00.000]
Test: real_sample... PASSED [X-TIMESTAMP-MAP=MPEGTS:129003,LOCAL:00:00:00.000]
==========================================
Results: 3 passed, 0 failed
==========================================
```
115 changes: 115 additions & 0 deletions tests/webvtt_timestamp_map/run_tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# WebVTT X-TIMESTAMP-MAP Test Script (PowerShell)
# Tests that CCExtractor always includes X-TIMESTAMP-MAP header in WebVTT output

param(
[Parameter(Mandatory=$false)]
[string]$CCExtractorPath = ".\ccextractor.exe"
)

$TestDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$OutputDir = Join-Path $TestDir "output"
$Passed = 0
$Failed = 0

Write-Host "==========================================" -ForegroundColor Cyan
Write-Host "WebVTT X-TIMESTAMP-MAP Test Suite" -ForegroundColor Cyan
Write-Host "==========================================" -ForegroundColor Cyan
Write-Host "CCExtractor: $CCExtractorPath"
Write-Host ""

# Check if ccextractor exists
if (-not (Test-Path $CCExtractorPath)) {
Write-Host "ERROR: CCExtractor not found at $CCExtractorPath" -ForegroundColor Red
Write-Host "Usage: .\run_tests.ps1 -CCExtractorPath 'C:\path\to\ccextractor.exe'"
exit 1
}

# Create output directory
New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null
Remove-Item "$OutputDir\*.vtt" -ErrorAction SilentlyContinue

function Run-Test {
param(
[string]$TestName,
[string]$InputFile,
[string]$ExtraArgs = ""
)

$OutputFile = Join-Path $OutputDir "$TestName.vtt"
Write-Host -NoNewline "Test: $TestName... "

# Run CCExtractor
$args = @($InputFile, "-out=webvtt", "-o", $OutputFile)
if ($ExtraArgs) {
$args += $ExtraArgs.Split(" ")
}

& $CCExtractorPath @args 2>&1 | Out-Null

# Check if output file exists
if (-not (Test-Path $OutputFile)) {
Write-Host "FAILED (no output file)" -ForegroundColor Red
$script:Failed++
return
}

# Read file content
$content = Get-Content $OutputFile

# Check for WEBVTT header on line 1
if ($content[0] -ne "WEBVTT") {
Write-Host "FAILED (line 1 is not WEBVTT)" -ForegroundColor Red
$script:Failed++
return
}

# Check for X-TIMESTAMP-MAP on line 2
if (-not ($content[1] -match "^X-TIMESTAMP-MAP=")) {
Write-Host "FAILED (line 2 missing X-TIMESTAMP-MAP)" -ForegroundColor Red
$script:Failed++
return
}

# Check for exactly one X-TIMESTAMP-MAP header
$count = ($content | Select-String "X-TIMESTAMP-MAP").Count
if ($count -ne 1) {
Write-Host "FAILED (found $count X-TIMESTAMP-MAP headers, expected 1)" -ForegroundColor Red
$script:Failed++
return
}

Write-Host "PASSED" -ForegroundColor Green -NoNewline
Write-Host " [$($content[1])]"
$script:Passed++
}

# Create a minimal empty TS file for testing
Write-Host "Creating test files..."
$emptyTs = Join-Path $TestDir "empty_test.ts"
$bytes = New-Object byte[] (188 * 100)
[System.IO.File]::WriteAllBytes($emptyTs, $bytes)

# Test 1: Empty TS file (no subtitles)
Run-Test -TestName "empty_ts_no_subs" -InputFile $emptyTs

# Test 2: Empty TS with invalid datapid
Run-Test -TestName "invalid_datapid" -InputFile $emptyTs -ExtraArgs "--datapid 9999"

# Test 3: Real sample if available
$sampleTs = Join-Path $TestDir "sample.ts"
if (Test-Path $sampleTs) {
Run-Test -TestName "real_sample" -InputFile $sampleTs
}

# Cleanup
Remove-Item $emptyTs -ErrorAction SilentlyContinue

Write-Host ""
Write-Host "==========================================" -ForegroundColor Cyan
Write-Host "Results: $Passed passed, $Failed failed" -ForegroundColor Cyan
Write-Host "==========================================" -ForegroundColor Cyan

if ($Failed -gt 0) {
exit 1
}
exit 0
Loading
Loading