mirror of
https://github.com/CCExtractor/ccextractor.git
synced 2026-07-08 18:06:30 +00:00
Merge branch 'CCExtractor:master' into final
This commit is contained in:
2
.github/workflows/homebrew.yml
vendored
2
.github/workflows/homebrew.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Update Homebrew formula
|
||||
uses: dawidd6/action-homebrew-bump-formula@v4
|
||||
uses: dawidd6/action-homebrew-bump-formula@v7
|
||||
with:
|
||||
token: ${{ secrets.HOMEBREW_GITHUB_API_TOKEN }}
|
||||
formula: ccextractor
|
||||
|
||||
24
README.md
24
README.md
@@ -66,6 +66,30 @@ You can find sample files on [our website](https://ccextractor.org/public/genera
|
||||
|
||||
- [Building on Windows using WSL](docs/build-wsl.md)
|
||||
|
||||
#### Linux (Autotools) build notes
|
||||
|
||||
CCExtractor also supports an autotools-based build system under the `linux/`
|
||||
directory.
|
||||
|
||||
Important notes:
|
||||
- The autotools workflow lives inside `linux/`. The `configure` script is
|
||||
generated there and should be run from that directory.
|
||||
- Typical build steps are:
|
||||
```
|
||||
cd linux
|
||||
./autogen.sh
|
||||
./configure
|
||||
make
|
||||
```
|
||||
- Rust support is enabled automatically if `cargo` and `rustc` are available
|
||||
on the system. In that case, Rust components are built and linked during
|
||||
`make`.
|
||||
- If you encounter unexpected build or linking issues, a clean rebuild
|
||||
(`make clean` or a fresh clone) is recommended, especially when Rust is
|
||||
involved.
|
||||
|
||||
This build flow has been tested on Linux and WSL.
|
||||
|
||||
## Compiling CCExtractor
|
||||
|
||||
To learn more about how to compile and build CCExtractor for your platform check the [compilation guide](https://github.com/CCExtractor/ccextractor/blob/master/docs/COMPILATION.MD).
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
0.96.6 (unreleased)
|
||||
-------------------
|
||||
- Fix: Prevent infinite loop on truncated MKV files
|
||||
- Fix: Various memory safety and stability fixes in demuxers (MP4, PS, MKV, DVB)
|
||||
- Fix: Delete empty output files instead of leaving 0-byte files (#1282)
|
||||
|
||||
0.96.5 (2026-01-05)
|
||||
-------------------
|
||||
- New: CCExtractor is available again via Homebrew on macOS and Linux.
|
||||
- New: Add support for raw CDP (Caption Distribution Packet) files (#1406)
|
||||
- New: Add --scc-accurate-timing option for bandwidth-aware SCC output (#1120)
|
||||
- Fix: MXF files containing CEA-708 captions not being detected/extracted (#1647)
|
||||
@@ -56,6 +63,7 @@
|
||||
- Extract multiple teletext pages simultaneously with separate output files
|
||||
- Use --tpage multiple times (e.g., --tpage 100 --tpage 200)
|
||||
- Output files are named with page suffix (e.g., output_p100.srt, output_p200.srt)
|
||||
- Fix: SPUPNG subtitle offset calculation to center based on actual image dimensions
|
||||
|
||||
- New: Added --list-tracks (-L) option to list all tracks in media files without processing
|
||||
New: Chinese, Korean, Japanese support - proper encoding and OCR.
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
# Installation
|
||||
|
||||
## Homebrew
|
||||
The easiest way to install CCExtractor for Mac and Linux is through Homebrew:
|
||||
|
||||
```bash
|
||||
brew install ccextractor
|
||||
```
|
||||
Note: If you don't have Homebrew installed, see [brew.sh](https://brew.sh/)
|
||||
for installation instructions.
|
||||
|
||||
---
|
||||
|
||||
# Compiling CCExtractor
|
||||
|
||||
You may compile CCExtractor across all major platforms using `CMakeLists.txt` stored under `ccextractor/src/` directory. Autoconf and custom build scripts are also available. See platform specific instructions in the below sections.
|
||||
|
||||
@@ -778,6 +778,7 @@ struct encoder_ctx *init_encoder(struct encoder_cfg *opt)
|
||||
return NULL;
|
||||
}
|
||||
ctx->in_fileformat = opt->in_format;
|
||||
ctx->is_pal = (opt->in_format == 2);
|
||||
|
||||
/** used in case of SUB_EOD_MARKER */
|
||||
ctx->prev_start = -1;
|
||||
|
||||
@@ -182,10 +182,10 @@ struct encoder_ctx
|
||||
|
||||
// OCR in SPUPNG
|
||||
int nospupngocr;
|
||||
int is_pal;
|
||||
|
||||
// Teletext multi-page output (issue #665)
|
||||
struct ccx_s_write *tlt_out[MAX_TLT_PAGES_EXTRACT]; // Output files per teletext page
|
||||
uint16_t tlt_out_pages[MAX_TLT_PAGES_EXTRACT]; // Page numbers for each output slot
|
||||
struct ccx_s_write *tlt_out[MAX_TLT_PAGES_EXTRACT]; // Output files per teletext page
|
||||
uint16_t tlt_out_pages[MAX_TLT_PAGES_EXTRACT]; // Page numbers for each output slot
|
||||
unsigned int tlt_srt_counter[MAX_TLT_PAGES_EXTRACT]; // SRT counter per page
|
||||
int tlt_out_count; // Number of teletext output files
|
||||
};
|
||||
|
||||
@@ -251,6 +251,9 @@ void set_spupng_offset(void *ctx, int x, int y)
|
||||
sp->xOffset = x;
|
||||
sp->yOffset = y;
|
||||
}
|
||||
|
||||
// Forward declaration for calculate_spupng_offsets
|
||||
static void calculate_spupng_offsets(struct spupng_t *sp, struct encoder_ctx *ctx);
|
||||
int save_spupng(const char *filename, uint8_t *bitmap, int w, int h,
|
||||
png_color *palette, png_byte *alpha, int nb_color)
|
||||
{
|
||||
@@ -384,7 +387,7 @@ int write_cc_bitmap_as_spupng(struct cc_subtitle *sub, struct encoder_ctx *conte
|
||||
struct cc_bitmap *rect;
|
||||
png_color *palette = NULL;
|
||||
png_byte *alpha = NULL;
|
||||
int wrote_opentag = 1;
|
||||
int wrote_opentag = 0; // Track if we actually wrote the tag
|
||||
|
||||
x_pos = -1;
|
||||
y_pos = -1;
|
||||
@@ -395,13 +398,11 @@ int write_cc_bitmap_as_spupng(struct cc_subtitle *sub, struct encoder_ctx *conte
|
||||
return 0;
|
||||
|
||||
inc_spupng_fileindex(sp);
|
||||
write_sputag_open(sp, sub->start_time, sub->end_time - 1);
|
||||
|
||||
if (sub->nb_data == 0 && (sub->flags & SUB_EOD_MARKER))
|
||||
{
|
||||
context->prev_start = -1;
|
||||
if (wrote_opentag)
|
||||
write_sputag_close(sp);
|
||||
// No subtitle data, skip writing
|
||||
return 0;
|
||||
}
|
||||
rect = sub->data;
|
||||
@@ -440,7 +441,13 @@ int write_cc_bitmap_as_spupng(struct cc_subtitle *sub, struct encoder_ctx *conte
|
||||
}
|
||||
}
|
||||
filename = get_spupng_filename(sp);
|
||||
set_spupng_offset(sp, x_pos, y_pos);
|
||||
|
||||
// Set image dimensions for offset calculation
|
||||
sp->img_w = width;
|
||||
sp->img_h = height;
|
||||
|
||||
// Calculate centered offsets based on screen size (PAL/NTSC)
|
||||
calculate_spupng_offsets(sp, context);
|
||||
if (sub->flags & SUB_EOD_MARKER)
|
||||
context->prev_start = sub->start_time;
|
||||
pbuf = (uint8_t *)malloc(width * height);
|
||||
@@ -475,6 +482,15 @@ int write_cc_bitmap_as_spupng(struct cc_subtitle *sub, struct encoder_ctx *conte
|
||||
|
||||
/* TODO do rectangle wise, one color table should not be used for all rectangles */
|
||||
mapclut_paletee(palette, alpha, (uint32_t *)rect[0].data1, rect[0].nb_colors);
|
||||
|
||||
// Save PNG file first
|
||||
save_spupng(filename, pbuf, width, height, palette, alpha, rect[0].nb_colors);
|
||||
freep(&pbuf);
|
||||
|
||||
// Write XML tag with calculated centered offsets
|
||||
write_sputag_open(sp, sub->start_time, sub->end_time - 1);
|
||||
wrote_opentag = 1; // Mark that we wrote the tag
|
||||
|
||||
#ifdef ENABLE_OCR
|
||||
if (!context->nospupngocr)
|
||||
{
|
||||
@@ -487,8 +503,6 @@ int write_cc_bitmap_as_spupng(struct cc_subtitle *sub, struct encoder_ctx *conte
|
||||
}
|
||||
}
|
||||
#endif
|
||||
save_spupng(filename, pbuf, width, height, palette, alpha, rect[0].nb_colors);
|
||||
freep(&pbuf);
|
||||
|
||||
end:
|
||||
if (wrote_opentag)
|
||||
@@ -991,6 +1005,8 @@ int spupng_export_string2png(struct spupng_t *sp, char *str, FILE *output)
|
||||
*/
|
||||
|
||||
// Save image
|
||||
sp->img_w = canvas_width;
|
||||
sp->img_h = canvas_height;
|
||||
write_image(buffer, output, canvas_width, canvas_height);
|
||||
free(tmp);
|
||||
free(buffer);
|
||||
@@ -1081,6 +1097,28 @@ int eia608_to_str(struct encoder_ctx *context, struct eia608_screen *data, char
|
||||
|
||||
// string needs to be in UTF-8 encoding.
|
||||
// This function will take care of encoding.
|
||||
static void calculate_spupng_offsets(struct spupng_t *sp, struct encoder_ctx *ctx)
|
||||
{
|
||||
int screen_w = 720;
|
||||
int screen_h;
|
||||
|
||||
/* Teletext is always PAL */
|
||||
if (ctx->in_fileformat == 2 || ctx->is_pal)
|
||||
{
|
||||
screen_h = 576;
|
||||
}
|
||||
else
|
||||
{
|
||||
screen_h = 480;
|
||||
}
|
||||
|
||||
sp->xOffset = (screen_w - sp->img_w) / 2;
|
||||
sp->yOffset = (screen_h - sp->img_h) / 2;
|
||||
|
||||
// SPU / DVD requires even yOffset (interlacing)
|
||||
if (sp->yOffset & 1)
|
||||
sp->yOffset++;
|
||||
}
|
||||
int spupng_write_string(struct spupng_t *sp, char *string, LLONG start_time, LLONG end_time,
|
||||
struct encoder_ctx *context)
|
||||
{
|
||||
@@ -1099,6 +1137,7 @@ int spupng_write_string(struct spupng_t *sp, char *string, LLONG start_time, LLO
|
||||
}
|
||||
// free(string_utf32);
|
||||
fclose(sp->fppng);
|
||||
calculate_spupng_offsets(sp, context);
|
||||
write_sputag_open(sp, start_time, end_time);
|
||||
write_spucomment(sp, string);
|
||||
write_sputag_close(sp);
|
||||
|
||||
@@ -39,6 +39,8 @@ struct spupng_t
|
||||
int fileIndex;
|
||||
int xOffset;
|
||||
int yOffset;
|
||||
int img_w;
|
||||
int img_h;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1926,7 +1926,7 @@ static int write_dvb_sub(struct lib_cc_decode *dec_ctx, struct cc_subtitle *sub)
|
||||
ctx->ocr_ctx = init_ocr(ctx->lang_index);
|
||||
ctx->ocr_initialized = 1; // Mark as initialized even if init_ocr returns NULL
|
||||
}
|
||||
if (ctx->ocr_ctx)
|
||||
if (ctx->ocr_ctx && region)
|
||||
{
|
||||
// DEBUG: Dump before OCR
|
||||
// dump_rect_and_log("before_ocr", rect->data0, rect->w, rect->h, rect->linesize0, 1, 0, 0);
|
||||
|
||||
@@ -66,6 +66,7 @@ void prepare_for_new_file(struct lib_ccx_ctx *ctx)
|
||||
{
|
||||
// Init per file variables
|
||||
ctx->last_reported_progress = -1;
|
||||
ctx->min_global_timestamp_offset = -1; // -1 means not yet initialized
|
||||
ctx->stat_numuserheaders = 0;
|
||||
ctx->stat_dvdccheaders = 0;
|
||||
ctx->stat_scte20ccheaders = 0;
|
||||
|
||||
@@ -1727,7 +1727,24 @@ int general_loop(struct lib_ccx_ctx *ctx)
|
||||
|
||||
if (ctx->live_stream)
|
||||
{
|
||||
int cur_sec = (int)(get_fts(dec_ctx->timing, dec_ctx->current_field) / 1000);
|
||||
LLONG t = get_fts(dec_ctx->timing, dec_ctx->current_field);
|
||||
if (!t && ctx->demux_ctx->global_timestamp_inited)
|
||||
t = ctx->demux_ctx->global_timestamp - ctx->demux_ctx->min_global_timestamp;
|
||||
// Handle multi-program TS timing
|
||||
if (ctx->demux_ctx->global_timestamp_inited)
|
||||
{
|
||||
LLONG offset = ctx->demux_ctx->global_timestamp - ctx->demux_ctx->min_global_timestamp;
|
||||
if (ctx->min_global_timestamp_offset < 0 || offset < ctx->min_global_timestamp_offset)
|
||||
ctx->min_global_timestamp_offset = offset;
|
||||
// Only use timestamps from the program with the lowest base
|
||||
if (offset - ctx->min_global_timestamp_offset < 60000)
|
||||
t = offset - ctx->min_global_timestamp_offset;
|
||||
else
|
||||
t = ctx->min_global_timestamp_offset > 0 ? 0 : t;
|
||||
if (t < 0)
|
||||
t = 0;
|
||||
}
|
||||
int cur_sec = (int)(t / 1000);
|
||||
int th = cur_sec / 10;
|
||||
if (ctx->last_reported_progress != th)
|
||||
{
|
||||
@@ -1745,6 +1762,28 @@ int general_loop(struct lib_ccx_ctx *ctx)
|
||||
LLONG t = get_fts(dec_ctx->timing, dec_ctx->current_field);
|
||||
if (!t && ctx->demux_ctx->global_timestamp_inited)
|
||||
t = ctx->demux_ctx->global_timestamp - ctx->demux_ctx->min_global_timestamp;
|
||||
// For multi-program TS files, different programs can have different
|
||||
// PCR bases (e.g., one at 25h, another at 23h). This causes the
|
||||
// global_timestamp to jump between different bases, resulting in
|
||||
// wildly different offset values. Track the minimum offset seen
|
||||
// and only display times from the program with the lowest base.
|
||||
if (ctx->demux_ctx->global_timestamp_inited)
|
||||
{
|
||||
LLONG offset = ctx->demux_ctx->global_timestamp - ctx->demux_ctx->min_global_timestamp;
|
||||
// Track minimum offset (this is the PCR base of the program
|
||||
// with the lowest timestamp, which represents true file time)
|
||||
if (ctx->min_global_timestamp_offset < 0 || offset < ctx->min_global_timestamp_offset)
|
||||
ctx->min_global_timestamp_offset = offset;
|
||||
// Only use timestamps from the program with the lowest base.
|
||||
// If current offset is significantly larger than minimum (by > 60s),
|
||||
// it's from a program with a higher PCR base - use minimum instead.
|
||||
if (offset - ctx->min_global_timestamp_offset < 60000)
|
||||
t = offset - ctx->min_global_timestamp_offset;
|
||||
else
|
||||
t = ctx->min_global_timestamp_offset > 0 ? 0 : t; // fallback to minimum-based time
|
||||
if (t < 0)
|
||||
t = 0;
|
||||
}
|
||||
int cur_sec = (int)(t / 1000);
|
||||
activity_progress(progress, cur_sec / 60, cur_sec % 60);
|
||||
ctx->last_reported_progress = progress;
|
||||
|
||||
@@ -118,6 +118,7 @@ struct lib_ccx_ctx
|
||||
LLONG total_past; // Only in binary concat mode
|
||||
|
||||
int last_reported_progress;
|
||||
LLONG min_global_timestamp_offset; // Track minimum (global - min) for multi-program TS
|
||||
|
||||
/* Stats */
|
||||
int stat_numuserheaders;
|
||||
|
||||
@@ -122,6 +122,8 @@ void parse_ebml(FILE *file)
|
||||
{
|
||||
code <<= 8;
|
||||
code += mkv_read_byte(file);
|
||||
if (feof(file))
|
||||
break;
|
||||
code_len++;
|
||||
|
||||
switch (code)
|
||||
@@ -186,6 +188,8 @@ void parse_segment_info(FILE *file)
|
||||
{
|
||||
code <<= 8;
|
||||
code += mkv_read_byte(file);
|
||||
if (feof(file))
|
||||
break;
|
||||
code_len++;
|
||||
|
||||
switch (code)
|
||||
@@ -484,6 +488,8 @@ void parse_segment_cluster_block_group(struct matroska_ctx *mkv_ctx, ULLONG clus
|
||||
{
|
||||
code <<= 8;
|
||||
code += mkv_read_byte(file);
|
||||
if (feof(file))
|
||||
break;
|
||||
code_len++;
|
||||
|
||||
switch (code)
|
||||
@@ -612,6 +618,8 @@ void parse_segment_cluster(struct matroska_ctx *mkv_ctx)
|
||||
{
|
||||
code <<= 8;
|
||||
code += mkv_read_byte(file);
|
||||
if (feof(file))
|
||||
break;
|
||||
code_len++;
|
||||
|
||||
switch (code)
|
||||
@@ -845,6 +853,8 @@ void parse_segment_track_entry(struct matroska_ctx *mkv_ctx)
|
||||
{
|
||||
code <<= 8;
|
||||
code += mkv_read_byte(file);
|
||||
if (feof(file))
|
||||
break;
|
||||
code_len++;
|
||||
|
||||
switch (code)
|
||||
@@ -1197,6 +1207,8 @@ void parse_segment_tracks(struct matroska_ctx *mkv_ctx)
|
||||
{
|
||||
code <<= 8;
|
||||
code += mkv_read_byte(file);
|
||||
if (feof(file))
|
||||
break;
|
||||
code_len++;
|
||||
|
||||
switch (code)
|
||||
@@ -1241,6 +1253,8 @@ void parse_segment(struct matroska_ctx *mkv_ctx)
|
||||
{
|
||||
code <<= 8;
|
||||
code += mkv_read_byte(file);
|
||||
if (feof(file))
|
||||
break;
|
||||
code_len++;
|
||||
switch (code)
|
||||
{
|
||||
@@ -1915,6 +1929,9 @@ void matroska_parse(struct matroska_ctx *mkv_ctx)
|
||||
{
|
||||
code <<= 8;
|
||||
code += mkv_read_byte(file);
|
||||
// Check for EOF after reading - feof() is only set after a failed read
|
||||
if (feof(file))
|
||||
break;
|
||||
code_len++;
|
||||
|
||||
switch (code)
|
||||
|
||||
@@ -14,7 +14,19 @@ void dinit_write(struct ccx_s_write *wb)
|
||||
return;
|
||||
}
|
||||
if (wb->fh > 0)
|
||||
{
|
||||
// Check if the file is empty before closing
|
||||
off_t file_size = lseek(wb->fh, 0, SEEK_END);
|
||||
close(wb->fh);
|
||||
|
||||
// Delete empty output files to avoid generating useless 0-byte files
|
||||
// This commonly happens with -12 option when one field has no captions
|
||||
if (file_size == 0 && wb->filename != NULL)
|
||||
{
|
||||
unlink(wb->filename);
|
||||
mprint("Deleted empty output file: %s\n", wb->filename);
|
||||
}
|
||||
}
|
||||
freep(&wb->filename);
|
||||
freep(&wb->original_filename);
|
||||
if (wb->with_semaphore && wb->semaphore_filename)
|
||||
|
||||
@@ -225,9 +225,6 @@ impl Timestamp {
|
||||
let m = millis / 60000 - 60 * h;
|
||||
let s = millis / 1000 - 3600 * h - 60 * m;
|
||||
let u = millis - 3600000 * h - 60000 * m - 1000 * s;
|
||||
if h > 24 {
|
||||
println!("{h}")
|
||||
}
|
||||
Ok((h.try_into()?, m as u8, s as u8, u as u16))
|
||||
}
|
||||
|
||||
|
||||
@@ -226,12 +226,10 @@ pub fn user_data_registered_itu_t_t35(ctx: &mut AvcContextRust, userbuf: &[u8])
|
||||
}
|
||||
|
||||
// Save the data and process once we know the sequence number
|
||||
if ((ctx.cc_count as usize + local_cc_count) * 3) + 1 > ctx.cc_databufsize {
|
||||
let required_size = ((ctx.cc_count as usize + local_cc_count) * 3) + 1;
|
||||
if required_size > ctx.cc_data.len() {
|
||||
let new_size = ((ctx.cc_count as usize + local_cc_count) * 6) + 1;
|
||||
unsafe {
|
||||
ctx.cc_data.set_len(new_size);
|
||||
}
|
||||
ctx.cc_data.reserve(new_size);
|
||||
ctx.cc_data.resize(new_size, 0);
|
||||
ctx.cc_databufsize = new_size;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
use crate::bindings::{lib_ccx_ctx, list_head};
|
||||
use lib_ccxr::common::{Codec, Decoder608Report, DecoderDtvccReport, StreamMode, StreamType};
|
||||
use lib_ccxr::time::Timestamp;
|
||||
use std::os::raw::c_void;
|
||||
use std::ptr::null_mut;
|
||||
|
||||
extern "C" {
|
||||
fn free(ptr: *mut c_void);
|
||||
}
|
||||
|
||||
// Size of the Startbytes Array in CcxDemuxer - const 1MB
|
||||
pub(crate) const ARRAY_SIZE: usize = 1024 * 1024;
|
||||
|
||||
@@ -109,7 +114,9 @@ impl Default for PSIBuffer {
|
||||
fn default() -> Self {
|
||||
PSIBuffer {
|
||||
prev_ccounter: 0,
|
||||
buffer: Box::into_raw(Box::new(0u8)),
|
||||
// Initialize with null to avoid unnecessary heap allocations and
|
||||
// signal that the buffer is currently empty.
|
||||
buffer: std::ptr::null_mut(),
|
||||
buffer_length: 0,
|
||||
ccounter: 0,
|
||||
}
|
||||
@@ -274,21 +281,21 @@ impl Default for CcxDemuxer<'_> {
|
||||
/// null pointers which are safely ignored.
|
||||
impl Drop for CcxDemuxer<'_> {
|
||||
fn drop(&mut self) {
|
||||
// Free all non-null PSIBuffer pointers (Rust-owned from Box::into_raw)
|
||||
// Free all non-null PSIBuffer pointers.
|
||||
// These are freed using C's free to be compatible with memory that might be allocated by C.
|
||||
for ptr in self.pid_buffers.drain(..) {
|
||||
if !ptr.is_null() {
|
||||
// SAFETY: These pointers were created via Box::into_raw in copy_demuxer_from_c_to_rust
|
||||
unsafe {
|
||||
drop(Box::from_raw(ptr));
|
||||
free(ptr as *mut c_void);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Free all non-null PMTEntry pointers (Rust-owned from Box::into_raw)
|
||||
// Free all non-null PMTEntry pointers.
|
||||
// These are freed using C's free to be compatible with memory that might be allocated by C.
|
||||
for ptr in self.pids_programs.drain(..) {
|
||||
if !ptr.is_null() {
|
||||
// SAFETY: These pointers were created via Box::into_raw in copy_demuxer_from_c_to_rust
|
||||
unsafe {
|
||||
drop(Box::from_raw(ptr));
|
||||
free(ptr as *mut c_void);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,10 +331,15 @@ unsafe fn detect_stream_type_common(ctx: &mut CcxDemuxer, ccx_options: &mut Opti
|
||||
}
|
||||
|
||||
// Now check for PS (Needs PACK header)
|
||||
// The loop below checks 4 consecutive bytes (i, i+1, i+2, i+3), so we need
|
||||
// to stop 3 bytes before the end to avoid out-of-bounds access.
|
||||
// - If buffer < 50000: limit = buffer_size - 3 (scan entire buffer)
|
||||
// - If buffer >= 50000: limit = 49997 (= 50000 - 3, cap the scan range)
|
||||
// We use saturating_sub to safely handle tiny buffers (< 3 bytes).
|
||||
let limit = if ctx.startbytes_avail < 50000 {
|
||||
ctx.startbytes_avail - 3
|
||||
ctx.startbytes_avail.saturating_sub(3)
|
||||
} else {
|
||||
49997
|
||||
50000 - 3 // Don't scan huge buffers entirely; 50KB is enough
|
||||
} as usize;
|
||||
for i in 0..limit {
|
||||
if ctx.startbytes[i] == 0x00
|
||||
@@ -427,15 +432,21 @@ pub fn is_valid_mp4_box(
|
||||
)
|
||||
);
|
||||
|
||||
// If the box type is "moov", check if it contains a valid movie header (mvhd)
|
||||
if idx == 2
|
||||
&& !(buffer[position + 12] == b'm'
|
||||
// If the box type is "moov", it must contain "mvhd" to be valid.
|
||||
// We need 16 bytes from position to check bytes 12-15 for "mvhd".
|
||||
if idx == 2 {
|
||||
if position + 16 > buffer.len() {
|
||||
// Not enough bytes to verify mvhd - skip this box
|
||||
continue;
|
||||
}
|
||||
if !(buffer[position + 12] == b'm'
|
||||
&& buffer[position + 13] == b'v'
|
||||
&& buffer[position + 14] == b'h'
|
||||
&& buffer[position + 15] == b'd')
|
||||
{
|
||||
// If "moov" doesn't have "mvhd", skip it.
|
||||
continue;
|
||||
{
|
||||
// moov without mvhd is not valid - skip it
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Box name matches. Do a crude validation of possible box size,
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::demuxer::common_types::{
|
||||
};
|
||||
use lib_ccxr::common::{Codec, Options, StreamMode, StreamType};
|
||||
use lib_ccxr::time::Timestamp;
|
||||
use std::alloc::{alloc_zeroed, Layout};
|
||||
use std::ffi::CStr;
|
||||
use std::os::raw::{c_char, c_int, c_uchar, c_uint, c_void};
|
||||
|
||||
@@ -18,10 +17,12 @@ const POISON_PTR_PATTERN: usize = 0xcdcdcdcdcdcdcdcd;
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
const POISON_PTR_PATTERN: usize = 0xcdcdcdcd;
|
||||
|
||||
// External C function declarations
|
||||
extern "C" {
|
||||
fn activity_input_file_closed();
|
||||
fn close(fd: c_int) -> c_int;
|
||||
fn malloc(size: usize) -> *mut c_void;
|
||||
fn free(ptr: *mut c_void);
|
||||
fn calloc(nmemb: usize, size: usize) -> *mut c_void;
|
||||
}
|
||||
|
||||
pub fn copy_c_array_to_rust_vec(
|
||||
@@ -98,61 +99,89 @@ pub unsafe fn copy_demuxer_from_rust_to_c(c_demuxer: *mut ccx_demuxer, rust_demu
|
||||
c.global_timestamp_inited = rust_demuxer.global_timestamp_inited.millis() as c_int;
|
||||
|
||||
// PID buffers - extra defensive version
|
||||
// We iterate through all possible PIDs (up to 8191 for PSI) to ensure state synchronization.
|
||||
// CRITICAL: We must free existing pointers in the C structure before overwriting them
|
||||
// to prevent massive memory leaks during the demuxing process, as this function
|
||||
// is called repeatedly to sync state between Rust and C.
|
||||
let pid_buffers_len = rust_demuxer.pid_buffers.len().min(8191);
|
||||
for i in 0..pid_buffers_len {
|
||||
let pid_buffer = rust_demuxer.pid_buffers[i];
|
||||
if !pid_buffer.is_null() {
|
||||
// Try to safely access the pointer
|
||||
match std::panic::catch_unwind(|| unsafe { &*pid_buffer }) {
|
||||
Ok(rust_psi) => {
|
||||
let c_psi = unsafe { rust_psi.to_ctype() };
|
||||
let c_ptr = Box::into_raw(Box::new(c_psi));
|
||||
c.PID_buffers[i] = c_ptr;
|
||||
}
|
||||
Err(_) => {
|
||||
// Pointer was invalid, set to null
|
||||
eprintln!("Warning: Invalid PID buffer pointer at index {i}");
|
||||
c.PID_buffers[i] = std::ptr::null_mut();
|
||||
for i in 0..8191 {
|
||||
// Free existing pointer if any.
|
||||
// SAFETY: We use C's free to be compatible with memory that might be allocated by C.
|
||||
// We also check for POISON_PTR_PATTERN for safety in debug builds.
|
||||
if !c.PID_buffers[i].is_null() && c.PID_buffers[i] as usize != POISON_PTR_PATTERN {
|
||||
unsafe {
|
||||
free(c.PID_buffers[i] as *mut c_void);
|
||||
c.PID_buffers[i] = std::ptr::null_mut();
|
||||
}
|
||||
}
|
||||
|
||||
if i < pid_buffers_len {
|
||||
let pid_buffer = rust_demuxer.pid_buffers[i];
|
||||
if !pid_buffer.is_null() {
|
||||
// Try to safely access the pointer using catch_unwind to prevent
|
||||
// a panic in Rust from crashing the entire C application.
|
||||
// This is a defensive measure for FFI robustness.
|
||||
match std::panic::catch_unwind(|| unsafe { &*pid_buffer }) {
|
||||
Ok(rust_psi) => {
|
||||
let c_psi = unsafe { rust_psi.to_ctype() };
|
||||
let c_ptr =
|
||||
unsafe { malloc(std::mem::size_of::<crate::bindings::PSI_buffer>()) }
|
||||
as *mut crate::bindings::PSI_buffer;
|
||||
if !c_ptr.is_null() {
|
||||
unsafe {
|
||||
std::ptr::write(c_ptr, c_psi);
|
||||
}
|
||||
c.PID_buffers[i] = c_ptr;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Pointer was invalid, log and skip
|
||||
eprintln!("Warning: Invalid PID buffer pointer at index {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
c.PID_buffers[i] = std::ptr::null_mut();
|
||||
}
|
||||
}
|
||||
|
||||
// Clear remaining slots if rust array is smaller than C array
|
||||
for i in pid_buffers_len..8191 {
|
||||
c.PID_buffers[i] = std::ptr::null_mut();
|
||||
}
|
||||
|
||||
// PIDs programs - extra defensive version
|
||||
// Similar to PID_buffers, we manage ownership of PMT entries.
|
||||
// We check for POISON_PTR_PATTERN to avoid freeing uninitialized memory in debug builds.
|
||||
let pids_programs_len = rust_demuxer.pids_programs.len().min(65536);
|
||||
for i in 0..pids_programs_len {
|
||||
let pmt_entry = rust_demuxer.pids_programs[i];
|
||||
if !pmt_entry.is_null() {
|
||||
// Try to safely access the pointer
|
||||
match std::panic::catch_unwind(|| unsafe { &*pmt_entry }) {
|
||||
Ok(rust_pmt) => {
|
||||
let c_pmt = unsafe { rust_pmt.to_ctype() };
|
||||
let c_ptr = Box::into_raw(Box::new(c_pmt));
|
||||
c.PIDs_programs[i] = c_ptr;
|
||||
}
|
||||
Err(_) => {
|
||||
// Pointer was invalid, set to null
|
||||
eprintln!("Warning: Invalid PMT entry pointer at index {i}");
|
||||
c.PIDs_programs[i] = std::ptr::null_mut();
|
||||
for i in 0..65536 {
|
||||
// Free existing pointer if any and it's not a poison pattern.
|
||||
// SAFETY: We use C's free to be compatible with memory that might be allocated by C.
|
||||
if !c.PIDs_programs[i].is_null() && c.PIDs_programs[i] as usize != POISON_PTR_PATTERN {
|
||||
unsafe {
|
||||
free(c.PIDs_programs[i] as *mut c_void);
|
||||
c.PIDs_programs[i] = std::ptr::null_mut();
|
||||
}
|
||||
}
|
||||
|
||||
if i < pids_programs_len {
|
||||
let pmt_entry = rust_demuxer.pids_programs[i];
|
||||
if !pmt_entry.is_null() {
|
||||
// Safely convert and move ownership to C
|
||||
match std::panic::catch_unwind(|| unsafe { &*pmt_entry }) {
|
||||
Ok(rust_pmt) => {
|
||||
let c_pmt = unsafe { rust_pmt.to_ctype() };
|
||||
let c_ptr =
|
||||
unsafe { malloc(std::mem::size_of::<crate::bindings::PMT_entry>()) }
|
||||
as *mut crate::bindings::PMT_entry;
|
||||
if !c_ptr.is_null() {
|
||||
unsafe {
|
||||
std::ptr::write(c_ptr, c_pmt);
|
||||
}
|
||||
c.PIDs_programs[i] = c_ptr;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
eprintln!("Warning: Invalid PMT entry pointer at index {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
c.PIDs_programs[i] = std::ptr::null_mut();
|
||||
}
|
||||
}
|
||||
|
||||
// Clear remaining slots if rust array is smaller than C array
|
||||
for i in pids_programs_len..65536 {
|
||||
c.PIDs_programs[i] = std::ptr::null_mut();
|
||||
}
|
||||
|
||||
// PIDs seen array
|
||||
for (i, &val) in rust_demuxer.pids_seen.iter().take(65536).enumerate() {
|
||||
c.PIDs_seen[i] = val as c_int;
|
||||
@@ -265,7 +294,15 @@ pub unsafe fn copy_demuxer_from_c_to_rust(ccx: *const ccx_demuxer) -> CcxDemuxer
|
||||
if buffer_ptr.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(Box::into_raw(Box::new(PSIBuffer::from_ctype(*buffer_ptr)?)))
|
||||
let rust_item = PSIBuffer::from_ctype(*buffer_ptr)?;
|
||||
let rust_ptr =
|
||||
unsafe { malloc(std::mem::size_of::<PSIBuffer>()) } as *mut PSIBuffer;
|
||||
if !rust_ptr.is_null() {
|
||||
unsafe {
|
||||
std::ptr::write(rust_ptr, rust_item);
|
||||
}
|
||||
}
|
||||
Some(rust_ptr)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -276,7 +313,14 @@ pub unsafe fn copy_demuxer_from_c_to_rust(ccx: *const ccx_demuxer) -> CcxDemuxer
|
||||
if buffer_ptr.is_null() || buffer_ptr as usize == POISON_PTR_PATTERN {
|
||||
None
|
||||
} else {
|
||||
Some(Box::into_raw(Box::new(PMTEntry::from_ctype(*buffer_ptr)?)))
|
||||
let rust_item = PMTEntry::from_ctype(*buffer_ptr)?;
|
||||
let rust_ptr = unsafe { malloc(std::mem::size_of::<PMTEntry>()) } as *mut PMTEntry;
|
||||
if !rust_ptr.is_null() {
|
||||
unsafe {
|
||||
std::ptr::write(rust_ptr, rust_item);
|
||||
}
|
||||
}
|
||||
Some(rust_ptr)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -367,8 +411,7 @@ pub unsafe fn copy_demuxer_from_c_to_rust(ccx: *const ccx_demuxer) -> CcxDemuxer
|
||||
///
|
||||
/// This function is unsafe because we are calling a C struct and using alloc_zeroed to initialize it.
|
||||
pub unsafe fn alloc_new_demuxer() -> *mut ccx_demuxer {
|
||||
let layout = Layout::new::<ccx_demuxer>();
|
||||
let ptr = alloc_zeroed(layout) as *mut ccx_demuxer;
|
||||
let ptr = calloc(1, std::mem::size_of::<ccx_demuxer>()) as *mut ccx_demuxer;
|
||||
|
||||
if ptr.is_null() {
|
||||
panic!("Failed to allocate memory for ccx_demuxer");
|
||||
|
||||
Reference in New Issue
Block a user