Compare commits

..

11 Commits

Author SHA1 Message Date
Carlos Fernandez
1789dc6d4c test(rust): Add comprehensive FFI integration tests for CEA-708 decoder
Add 18 new tests covering the FFI boundary between C and Rust code:

Lifecycle tests:
- ccxr_dtvcc_init creates valid context
- ccxr_dtvcc_init with null returns null
- ccxr_dtvcc_free with null is safe (no crash)
- ccxr_dtvcc_is_active with null returns zero
- Complete lifecycle test (init -> set_encoder -> process -> flush -> free)

Encoder tests:
- ccxr_dtvcc_set_encoder with valid context
- ccxr_dtvcc_set_encoder with null context is safe

Process data tests:
- ccxr_dtvcc_process_data packet start sets state correctly
- ccxr_dtvcc_process_data with null is safe
- ccxr_dtvcc_process_data state persists across calls (key fix verification)

ccxr_process_cc_data FFI entry point tests:
- Returns error with null context
- Returns error with null data
- Returns error with zero count
- Returns error with null dtvcc_rust pointer
- Processes valid CEA-708 data correctly
- Skips invalid CC pairs

Flush tests:
- ccxr_flush_active_decoders with null is safe
- ccxr_flush_active_decoders with valid context

These tests specifically cover the FFI gap identified where:
- The Rust-enabled path (dtvcc=NULL, dtvcc_rust set) wasn't tested
- Null pointer edge cases at the C→Rust boundary weren't verified
- State persistence across FFI calls wasn't validated

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:59:16 +01:00
Carlos Fernandez
e45b2528b7 fix(rust): Check dtvcc_rust instead of dtvcc in ccxr_process_cc_data
When Rust CEA-708 decoder is enabled, dec_ctx.dtvcc is set to NULL
and dec_ctx.dtvcc_rust holds the actual DtvccRust context. The null
check was incorrectly checking dtvcc, causing the function to return
early and skip all CEA-708 data processing.

This fixes tests 21, 31, 32, 105, 137, 141-149 which were failing
with exit code 10 (EXIT_NO_CAPTIONS) because no captions were being
extracted from CEA-708 streams.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:51:55 +01:00
Carlos
a2a844da5c fix(rust): remove double-increment of cb_708 counter
The cb_708 counter was being incremented twice for each CEA-708 data block:
1. In do_cb_dtvcc_rust() in Rust (src/rust/src/lib.rs)
2. In do_cb() in C (src/lib_ccx/ccx_decoders_common.c)

Since FTS calculation uses cb_708 (fts = fts_now + fts_global + cb_708 * 1001 / 30),
the double-increment caused timestamps to advance ~2x as fast as expected,
resulting in incorrect milliseconds in start timestamps.

This fix removes the increment from the Rust code since the C code already
handles it in do_cb().

Fixes timestamp issues reported in PR #1782 tests where start times like
00:00:20,688 were incorrectly output as 00:00:20,737.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:51:55 +01:00
Carlos
85263161cc chore: Remove plan file from repo and add plans/ to .gitignore
- Move PLAN_PR1618_REIMPLEMENTATION.md to local plans/ folder
- Add plans/ to .gitignore to keep plans local

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:51:55 +01:00
Carlos
473b3b4c46 fix(rust): Use persistent DtvccRust context in ccxr_process_cc_data
The ccxr_process_cc_data function was still accessing dec_ctx.dtvcc
(which is NULL when Rust is enabled), causing a null pointer panic.

Changed to use dec_ctx.dtvcc_rust (the persistent DtvccRust context)
instead, which fixes the crash when processing CEA-708 data.

Added do_cb_dtvcc_rust() function that works with DtvccRust instead
of the old Dtvcc struct.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:51:55 +01:00
Carlos
a6596ad65b style(c): Fix clang-format issues in Phase 3 code
- Remove extra space before comment in ccx_decoders_common.c
- Fix comment indentation in mp4.c

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:51:55 +01:00
Carlos
2180ab5bfa feat(c): Use Rust CEA-708 decoder in C code (Phase 3)
- init_cc_decode(): Initialize dtvcc_rust via ccxr_dtvcc_init()
- dinit_cc_decode(): Free dtvcc_rust via ccxr_dtvcc_free()
- flush_cc_decode(): Flush via ccxr_flush_active_decoders()
- general_loop.c: Set encoder via ccxr_dtvcc_set_encoder() (3 locations)
- mp4.c: Use ccxr_dtvcc_set_encoder() and ccxr_dtvcc_process_data()
- Add ccxr_dtvcc_is_active() declaration to ccx_dtvcc.h
- Fix clippy warnings in tv_screen.rs (unused assignments)
- All changes guarded with #ifndef DISABLE_RUST
- Update implementation plan to mark Phase 3 complete

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:51:55 +01:00
Carlos
ad3b1decc6 feat(c): Add C header declarations for Rust CEA-708 FFI (Phase 2)
- Add void *dtvcc_rust field to lib_cc_decode struct
- Declare ccxr_dtvcc_init, ccxr_dtvcc_free, ccxr_dtvcc_process_data in ccx_dtvcc.h
- Declare ccxr_dtvcc_set_encoder in lib_ccx.h
- Declare ccxr_flush_active_decoders in ccx_decoders_common.h
- All declarations guarded with #ifndef DISABLE_RUST
- Update implementation plan to mark Phase 2 complete

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:51:55 +01:00
Carlos
693c3aaba7 fix(rust): Address PR review - use existing DTVCC_MAX_SERVICES constant
- Remove duplicate CCX_DTVCC_MAX_SERVICES constant from decoder/mod.rs
- Import existing DTVCC_MAX_SERVICES from lib_ccxr::common
- Fix clippy uninlined_format_args warnings in avc/core.rs and decoder/mod.rs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:51:55 +01:00
Carlos
e53921ef2c style(rust): Apply cargo fmt formatting
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:51:55 +01:00
Carlos
0f01679ca3 feat(rust): Add persistent DtvccRust context for CEA-708 decoder (Phase 1)
This is Phase 1 of the fix for issue #1499. It adds the Rust-side
infrastructure for a persistent CEA-708 decoder context without
modifying any C code, ensuring backward compatibility.

Problem:
The current Rust CEA-708 decoder creates a new Dtvcc struct on every
call to ccxr_process_cc_data(), causing all state to be reset. This
breaks stateful caption processing.

Solution:
Add a new DtvccRust struct that:
- Owns its decoder state (rather than borrowing from C)
- Persists across processing calls
- Is managed via FFI functions callable from C

Changes:
- Add DtvccRust struct in decoder/mod.rs with owned decoders
- Add CCX_DTVCC_MAX_SERVICES constant (63)
- Add FFI functions in lib.rs:
  - ccxr_dtvcc_init(): Create persistent context
  - ccxr_dtvcc_free(): Free context and all owned memory
  - ccxr_dtvcc_set_encoder(): Set encoder (not available at init)
  - ccxr_dtvcc_process_data(): Process CC data
  - ccxr_flush_active_decoders(): Flush all active decoders
  - ccxr_dtvcc_is_active(): Check if context is active
- Add unit tests for DtvccRust
- Use heap allocation for large structs to avoid stack overflow

The existing Dtvcc struct and ccxr_process_cc_data() remain unchanged
for backward compatibility. Phase 2-3 will add C header declarations
and modify C code to use the new functions.

Fixes: #1499 (partial)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:51:55 +01:00
88 changed files with 1426 additions and 4150 deletions

View File

@@ -1,283 +0,0 @@
name: Build Linux .deb Package
on:
# Build on releases
release:
types: [published]
# Allow manual trigger
workflow_dispatch:
inputs:
build_type:
description: 'Build type (all, basic, hardsubx)'
required: false
default: 'all'
# Build on pushes to workflow file for testing
push:
paths:
- '.github/workflows/build_deb.yml'
jobs:
build-deb:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
build_type: [basic, hardsubx]
steps:
- name: Check if should build this variant
id: should_build
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
INPUT_TYPE="${{ github.event.inputs.build_type }}"
if [ "$INPUT_TYPE" = "all" ] || [ "$INPUT_TYPE" = "${{ matrix.build_type }}" ]; then
echo "should_build=true" >> $GITHUB_OUTPUT
else
echo "should_build=false" >> $GITHUB_OUTPUT
fi
else
echo "should_build=true" >> $GITHUB_OUTPUT
fi
- name: Checkout repository
if: steps.should_build.outputs.should_build == 'true'
uses: actions/checkout@v6
- name: Get version
if: steps.should_build.outputs.should_build == 'true'
id: version
run: |
# Extract version from source or use tag
if [ "${{ github.event_name }}" = "release" ]; then
VERSION="${{ github.event.release.tag_name }}"
VERSION="${VERSION#v}" # Remove 'v' prefix if present
else
# Extract version from lib_ccx.h (e.g., #define VERSION "0.96.5")
VERSION=$(grep -oP '#define VERSION "\K[^"]+' src/lib_ccx/lib_ccx.h || echo "0.96")
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Building version: $VERSION"
- name: Install base dependencies
if: steps.should_build.outputs.should_build == 'true'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential \
cmake \
pkg-config \
zlib1g-dev \
libpng-dev \
libjpeg-dev \
libfreetype-dev \
libxml2-dev \
libcurl4-gnutls-dev \
libssl-dev \
clang \
libclang-dev \
tesseract-ocr \
libtesseract-dev \
libleptonica-dev \
patchelf
- name: Install FFmpeg dependencies (HardSubX)
if: steps.should_build.outputs.should_build == 'true' && matrix.build_type == 'hardsubx'
run: |
sudo apt-get install -y --no-install-recommends \
libavcodec-dev \
libavformat-dev \
libavutil-dev \
libswscale-dev \
libswresample-dev \
libavfilter-dev \
libavdevice-dev
- name: Install Rust toolchain
if: steps.should_build.outputs.should_build == 'true'
uses: dtolnay/rust-toolchain@stable
- name: Cache GPAC build
if: steps.should_build.outputs.should_build == 'true'
id: cache-gpac
uses: actions/cache@v5
with:
path: ~/gpac-install
key: gpac-abi-16.4-ubuntu24-deb
- name: Build GPAC
if: steps.should_build.outputs.should_build == 'true' && steps.cache-gpac.outputs.cache-hit != 'true'
run: |
git clone -b abi-16.4 --depth 1 https://github.com/gpac/gpac
cd gpac
./configure --prefix=/usr
make -j$(nproc)
make DESTDIR=$HOME/gpac-install install-lib
- name: Install GPAC to system
if: steps.should_build.outputs.should_build == 'true'
run: |
sudo cp -r $HOME/gpac-install/usr/lib/* /usr/lib/
sudo cp -r $HOME/gpac-install/usr/include/* /usr/include/
sudo ldconfig
- name: Build CCExtractor
if: steps.should_build.outputs.should_build == 'true'
run: |
mkdir build && cd build
if [ "${{ matrix.build_type }}" = "hardsubx" ]; then
cmake ../src -DCMAKE_BUILD_TYPE=Release -DWITH_OCR=ON -DWITH_HARDSUBX=ON
else
cmake ../src -DCMAKE_BUILD_TYPE=Release -DWITH_OCR=ON
fi
make -j$(nproc)
- name: Test build
if: steps.should_build.outputs.should_build == 'true'
run: ./build/ccextractor --version
- name: Create .deb package structure
if: steps.should_build.outputs.should_build == 'true'
run: |
VERSION="${{ steps.version.outputs.version }}"
VARIANT="${{ matrix.build_type }}"
if [ "$VARIANT" = "basic" ]; then
PKG_NAME="ccextractor_${VERSION}_amd64"
else
PKG_NAME="ccextractor-${VARIANT}_${VERSION}_amd64"
fi
mkdir -p ${PKG_NAME}/DEBIAN
mkdir -p ${PKG_NAME}/usr/bin
mkdir -p ${PKG_NAME}/usr/lib/ccextractor
mkdir -p ${PKG_NAME}/usr/share/doc/ccextractor
mkdir -p ${PKG_NAME}/usr/share/man/man1
# Copy binary
cp build/ccextractor ${PKG_NAME}/usr/bin/
# Copy GPAC library
cp $HOME/gpac-install/usr/lib/libgpac.so* ${PKG_NAME}/usr/lib/ccextractor/
# Set rpath so ccextractor finds bundled libgpac
patchelf --set-rpath '/usr/lib/ccextractor:$ORIGIN/../lib/ccextractor' ${PKG_NAME}/usr/bin/ccextractor
# Copy documentation
cp docs/CHANGES.TXT ${PKG_NAME}/usr/share/doc/ccextractor/changelog
cp LICENSE.txt ${PKG_NAME}/usr/share/doc/ccextractor/copyright
gzip -9 -n ${PKG_NAME}/usr/share/doc/ccextractor/changelog
# Generate man page
help2man --no-info --name="closed captions and teletext subtitle extractor" \
./build/ccextractor > ${PKG_NAME}/usr/share/man/man1/ccextractor.1 2>/dev/null || true
if [ -f ${PKG_NAME}/usr/share/man/man1/ccextractor.1 ]; then
gzip -9 -n ${PKG_NAME}/usr/share/man/man1/ccextractor.1
fi
# Create control file
if [ "$VARIANT" = "basic" ]; then
PKG_DESCRIPTION="CCExtractor - closed captions and teletext subtitle extractor"
else
PKG_DESCRIPTION="CCExtractor (with HardSubX) - closed captions and teletext subtitle extractor"
fi
INSTALLED_SIZE=$(du -sk ${PKG_NAME}/usr | cut -f1)
# Determine dependencies based on build variant (Ubuntu 24.04)
if [ "$VARIANT" = "hardsubx" ]; then
DEPENDS="libc6, libtesseract5, liblept5, libcurl3t64-gnutls, libavcodec60, libavformat60, libavutil58, libswscale7, libavdevice60, libswresample4, libavfilter9"
else
DEPENDS="libc6, libtesseract5, liblept5, libcurl3t64-gnutls"
fi
cat > ${PKG_NAME}/DEBIAN/control << CTRL
Package: ccextractor
Version: ${VERSION}
Section: utils
Priority: optional
Architecture: amd64
Installed-Size: ${INSTALLED_SIZE}
Depends: ${DEPENDS}
Maintainer: CCExtractor Development Team <carlos@ccextractor.org>
Homepage: https://www.ccextractor.org
Description: ${PKG_DESCRIPTION}
CCExtractor is a tool that extracts closed captions and teletext subtitles
from video files and streams. It supports a wide variety of input formats
including MPEG, H.264/AVC, H.265/HEVC, MP4, MKV, WTV, and transport streams.
.
This package includes a bundled GPAC library for MP4 support.
CTRL
# Remove leading spaces from control file
sed -i 's/^ //' ${PKG_NAME}/DEBIAN/control
# Create postinst to update library cache
cat > ${PKG_NAME}/DEBIAN/postinst << 'POSTINST'
#!/bin/sh
set -e
ldconfig
POSTINST
chmod 755 ${PKG_NAME}/DEBIAN/postinst
# Create postrm to update library cache
cat > ${PKG_NAME}/DEBIAN/postrm << 'POSTRM'
#!/bin/sh
set -e
ldconfig
POSTRM
chmod 755 ${PKG_NAME}/DEBIAN/postrm
# Set permissions
chmod 755 ${PKG_NAME}/usr/bin/ccextractor
chmod 755 ${PKG_NAME}/usr/lib/ccextractor
find ${PKG_NAME}/usr/lib/ccextractor -name "*.so*" -exec chmod 644 {} \;
# Build the .deb
dpkg-deb --build --root-owner-group ${PKG_NAME}
echo "deb_name=${PKG_NAME}.deb" >> $GITHUB_OUTPUT
- name: Test .deb package
if: steps.should_build.outputs.should_build == 'true'
run: |
VERSION="${{ steps.version.outputs.version }}"
VARIANT="${{ matrix.build_type }}"
if [ "$VARIANT" = "basic" ]; then
PKG_NAME="ccextractor_${VERSION}_amd64"
else
PKG_NAME="ccextractor-${VARIANT}_${VERSION}_amd64"
fi
# Install and test (apt handles dependencies automatically)
sudo apt-get update
sudo apt-get install -y ./${PKG_NAME}.deb
ccextractor --version
- name: Get .deb filename
if: steps.should_build.outputs.should_build == 'true'
id: deb_name
run: |
VERSION="${{ steps.version.outputs.version }}"
VARIANT="${{ matrix.build_type }}"
if [ "$VARIANT" = "basic" ]; then
echo "name=ccextractor_${VERSION}_amd64.deb" >> $GITHUB_OUTPUT
else
echo "name=ccextractor-${VARIANT}_${VERSION}_amd64.deb" >> $GITHUB_OUTPUT
fi
- name: Upload .deb artifact
if: steps.should_build.outputs.should_build == 'true'
uses: actions/upload-artifact@v6
with:
name: ${{ steps.deb_name.outputs.name }}
path: ${{ steps.deb_name.outputs.name }}
- name: Upload to Release
if: steps.should_build.outputs.should_build == 'true' && github.event_name == 'release'
uses: softprops/action-gh-release@v2
with:
files: ${{ steps.deb_name.outputs.name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,275 +0,0 @@
name: Build Debian 13 .deb Package
on:
# Build on releases
release:
types: [published]
# Allow manual trigger
workflow_dispatch:
inputs:
build_type:
description: 'Build type (all, basic, hardsubx)'
required: false
default: 'all'
# Build on pushes to workflow file for testing
push:
paths:
- '.github/workflows/build_deb_debian13.yml'
jobs:
build-deb:
runs-on: ubuntu-latest
container:
image: debian:trixie
strategy:
fail-fast: false
matrix:
build_type: [basic, hardsubx]
steps:
- name: Check if should build this variant
id: should_build
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
INPUT_TYPE="${{ github.event.inputs.build_type }}"
if [ "$INPUT_TYPE" = "all" ] || [ "$INPUT_TYPE" = "${{ matrix.build_type }}" ]; then
echo "should_build=true" >> $GITHUB_OUTPUT
else
echo "should_build=false" >> $GITHUB_OUTPUT
fi
else
echo "should_build=true" >> $GITHUB_OUTPUT
fi
- name: Install git and dependencies for checkout
if: steps.should_build.outputs.should_build == 'true'
run: |
apt-get update
apt-get install -y git ca-certificates
- name: Checkout repository
if: steps.should_build.outputs.should_build == 'true'
uses: actions/checkout@v6
- name: Get version
if: steps.should_build.outputs.should_build == 'true'
id: version
run: |
# Extract version from source or use tag
if [ "${{ github.event_name }}" = "release" ]; then
VERSION="${{ github.event.release.tag_name }}"
VERSION="${VERSION#v}" # Remove 'v' prefix if present
else
# Extract version from lib_ccx.h (e.g., #define VERSION "0.96.5")
VERSION=$(grep -oP '#define VERSION "\K[^"]+' src/lib_ccx/lib_ccx.h || echo "0.96")
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Building version: $VERSION"
- name: Install base dependencies
if: steps.should_build.outputs.should_build == 'true'
run: |
apt-get install -y --no-install-recommends \
build-essential \
cmake \
pkg-config \
zlib1g-dev \
libpng-dev \
libjpeg-dev \
libfreetype-dev \
libxml2-dev \
libcurl4-gnutls-dev \
libssl-dev \
clang \
libclang-dev \
tesseract-ocr \
libtesseract-dev \
libleptonica-dev \
patchelf \
curl
- name: Install FFmpeg dependencies (HardSubX)
if: steps.should_build.outputs.should_build == 'true' && matrix.build_type == 'hardsubx'
run: |
apt-get install -y --no-install-recommends \
libavcodec-dev \
libavformat-dev \
libavutil-dev \
libswscale-dev \
libswresample-dev \
libavfilter-dev \
libavdevice-dev
- name: Install Rust toolchain
if: steps.should_build.outputs.should_build == 'true'
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Build GPAC
if: steps.should_build.outputs.should_build == 'true'
run: |
git clone -b abi-16.4 --depth 1 https://github.com/gpac/gpac
cd gpac
./configure --prefix=/usr
make -j$(nproc)
make install-lib
ldconfig
- name: Build CCExtractor
if: steps.should_build.outputs.should_build == 'true'
run: |
export PATH="$HOME/.cargo/bin:$PATH"
mkdir build && cd build
if [ "${{ matrix.build_type }}" = "hardsubx" ]; then
cmake ../src -DCMAKE_BUILD_TYPE=Release -DWITH_OCR=ON -DWITH_HARDSUBX=ON
else
cmake ../src -DCMAKE_BUILD_TYPE=Release -DWITH_OCR=ON
fi
make -j$(nproc)
- name: Test build
if: steps.should_build.outputs.should_build == 'true'
run: ./build/ccextractor --version
- name: Create .deb package structure
if: steps.should_build.outputs.should_build == 'true'
id: create_deb
run: |
VERSION="${{ steps.version.outputs.version }}"
VARIANT="${{ matrix.build_type }}"
if [ "$VARIANT" = "basic" ]; then
PKG_NAME="ccextractor_${VERSION}_debian13_amd64"
else
PKG_NAME="ccextractor-${VARIANT}_${VERSION}_debian13_amd64"
fi
mkdir -p ${PKG_NAME}/DEBIAN
mkdir -p ${PKG_NAME}/usr/bin
mkdir -p ${PKG_NAME}/usr/lib/ccextractor
mkdir -p ${PKG_NAME}/usr/share/doc/ccextractor
mkdir -p ${PKG_NAME}/usr/share/man/man1
# Copy binary
cp build/ccextractor ${PKG_NAME}/usr/bin/
# Copy GPAC library
cp /usr/lib/libgpac.so* ${PKG_NAME}/usr/lib/ccextractor/
# Set rpath so ccextractor finds bundled libgpac
patchelf --set-rpath '/usr/lib/ccextractor:$ORIGIN/../lib/ccextractor' ${PKG_NAME}/usr/bin/ccextractor
# Copy documentation
cp docs/CHANGES.TXT ${PKG_NAME}/usr/share/doc/ccextractor/changelog
cp LICENSE.txt ${PKG_NAME}/usr/share/doc/ccextractor/copyright
gzip -9 -n ${PKG_NAME}/usr/share/doc/ccextractor/changelog
# Create control file
if [ "$VARIANT" = "basic" ]; then
PKG_DESCRIPTION="CCExtractor - closed captions and teletext subtitle extractor"
else
PKG_DESCRIPTION="CCExtractor (with HardSubX) - closed captions and teletext subtitle extractor"
fi
INSTALLED_SIZE=$(du -sk ${PKG_NAME}/usr | cut -f1)
# Determine dependencies based on build variant (Debian 13 Trixie)
if [ "$VARIANT" = "hardsubx" ]; then
DEPENDS="libc6, libtesseract5, libleptonica6, libcurl3t64-gnutls, libavcodec61, libavformat61, libavutil59, libswscale8, libavdevice61, libswresample5, libavfilter10"
else
DEPENDS="libc6, libtesseract5, libleptonica6, libcurl3t64-gnutls"
fi
cat > ${PKG_NAME}/DEBIAN/control << CTRL
Package: ccextractor
Version: ${VERSION}
Section: utils
Priority: optional
Architecture: amd64
Installed-Size: ${INSTALLED_SIZE}
Depends: ${DEPENDS}
Maintainer: CCExtractor Development Team <carlos@ccextractor.org>
Homepage: https://www.ccextractor.org
Description: ${PKG_DESCRIPTION}
CCExtractor is a tool that extracts closed captions and teletext subtitles
from video files and streams. It supports a wide variety of input formats
including MPEG, H.264/AVC, H.265/HEVC, MP4, MKV, WTV, and transport streams.
.
This package includes a bundled GPAC library for MP4 support.
Built for Debian 13 (Trixie).
CTRL
# Remove leading spaces from control file
sed -i 's/^ //' ${PKG_NAME}/DEBIAN/control
# Create postinst to update library cache
cat > ${PKG_NAME}/DEBIAN/postinst << 'POSTINST'
#!/bin/sh
set -e
ldconfig
POSTINST
chmod 755 ${PKG_NAME}/DEBIAN/postinst
# Create postrm to update library cache
cat > ${PKG_NAME}/DEBIAN/postrm << 'POSTRM'
#!/bin/sh
set -e
ldconfig
POSTRM
chmod 755 ${PKG_NAME}/DEBIAN/postrm
# Set permissions
chmod 755 ${PKG_NAME}/usr/bin/ccextractor
chmod 755 ${PKG_NAME}/usr/lib/ccextractor
find ${PKG_NAME}/usr/lib/ccextractor -name "*.so*" -exec chmod 644 {} \;
# Build the .deb
dpkg-deb --build --root-owner-group ${PKG_NAME}
echo "deb_name=${PKG_NAME}.deb" >> $GITHUB_OUTPUT
- name: Test .deb package
if: steps.should_build.outputs.should_build == 'true'
run: |
VERSION="${{ steps.version.outputs.version }}"
VARIANT="${{ matrix.build_type }}"
if [ "$VARIANT" = "basic" ]; then
PKG_NAME="ccextractor_${VERSION}_debian13_amd64"
else
PKG_NAME="ccextractor-${VARIANT}_${VERSION}_debian13_amd64"
fi
# Install and test (apt handles dependencies automatically)
apt-get update
apt-get install -y ./${PKG_NAME}.deb
ccextractor --version
- name: Get .deb filename
if: steps.should_build.outputs.should_build == 'true'
id: deb_name
run: |
VERSION="${{ steps.version.outputs.version }}"
VARIANT="${{ matrix.build_type }}"
if [ "$VARIANT" = "basic" ]; then
echo "name=ccextractor_${VERSION}_debian13_amd64.deb" >> $GITHUB_OUTPUT
else
echo "name=ccextractor-${VARIANT}_${VERSION}_debian13_amd64.deb" >> $GITHUB_OUTPUT
fi
- name: Upload .deb artifact
if: steps.should_build.outputs.should_build == 'true'
uses: actions/upload-artifact@v6
with:
name: ${{ steps.deb_name.outputs.name }}
path: ${{ steps.deb_name.outputs.name }}
- name: Upload to Release
if: steps.should_build.outputs.should_build == 'true' && github.event_name == 'release'
uses: softprops/action-gh-release@v2
with:
files: ${{ steps.deb_name.outputs.name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -8,8 +8,6 @@ on:
- 'docker/**'
- '**.c'
- '**.h'
- '**CMakeLists.txt'
- '**.cmake'
- 'src/rust/**'
pull_request:
types: [opened, synchronize, reopened]
@@ -18,8 +16,6 @@ on:
- 'docker/**'
- '**.c'
- '**.h'
- '**CMakeLists.txt'
- '**.cmake'
- 'src/rust/**'
jobs:

View File

@@ -7,8 +7,6 @@ on:
- '.github/workflows/build_linux.yml'
- '**.c'
- '**.h'
- '**CMakeLists.txt'
- '**.cmake'
- '**Makefile**'
- 'linux/**'
- 'package_creators/**'
@@ -19,8 +17,6 @@ on:
- '.github/workflows/build_linux.yml'
- '**.c'
- '**.h'
- '**CMakeLists.txt'
- '**.cmake'
- '**Makefile**'
- 'linux/**'
- 'package_creators/**'

View File

@@ -7,8 +7,6 @@ on:
- '.github/workflows/build_mac.yml'
- '**.c'
- '**.h'
- '**CMakeLists.txt'
- '**.cmake'
- '**Makefile**'
- 'mac/**'
- 'package_creators/**'
@@ -19,8 +17,6 @@ on:
- '.github/workflows/build_mac.yml'
- '**.c'
- '**.h'
- '**CMakeLists.txt'
- '**.cmake'
- '**Makefile**'
- 'mac/**'
- 'package_creators/**'

View File

@@ -1,51 +0,0 @@
name: Build CCExtractor Snap
on:
workflow_dispatch:
release:
types: [published]
jobs:
build_snap:
name: Build Snap package
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install snapd
run: |
sudo apt update
sudo apt install -y snapd
- name: Start snapd
run: |
sudo systemctl start snapd.socket
sudo systemctl start snapd
- name: Install Snapcraft
run: |
sudo snap install core22
sudo snap install snapcraft --classic
- name: Show Snapcraft version
run: snapcraft --version
- name: Build snap
run: sudo snapcraft --destructive-mode
- name: List generated snap
run: ls -lh *.snap
- name: Upload snap as workflow artifact
uses: actions/upload-artifact@v6
with:
name: CCExtractor Snap
path: "*.snap"
- name: Upload snap to GitHub Release
if: github.event_name == 'release'
uses: softprops/action-gh-release@v2
with:
files: "*.snap"

View File

@@ -3,6 +3,7 @@ name: Build CCExtractor on Windows
env:
RUSTFLAGS: -Ctarget-feature=+crt-static
VCPKG_DEFAULT_TRIPLET: x64-windows-static
VCPKG_DEFAULT_BINARY_CACHE: C:\vcpkg\.cache
VCPKG_COMMIT: ab2977be50c702126336e5088f4836060733c899
on:
@@ -12,8 +13,6 @@ on:
- ".github/workflows/build_windows.yml"
- "**.c"
- "**.h"
- "**CMakeLists.txt"
- "**.cmake"
- "windows/**"
- "src/rust/**"
pull_request:
@@ -22,118 +21,108 @@ on:
- ".github/workflows/build_windows.yml"
- "**.c"
- "**.h"
- "**CMakeLists.txt"
- "**.cmake"
- "windows/**"
- "src/rust/**"
jobs:
build:
build_release:
runs-on: windows-2022
steps:
- name: Check out repository
uses: actions/checkout@v6
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@v2.0.0
with:
msbuild-architecture: x64
# Install GPAC (fast, ~30s, not worth caching complexity)
- name: Install gpac
run: choco install gpac --version 2.4.0 --no-progress
# Use lukka/run-vcpkg for better caching
run: choco install gpac --version 2.4.0
- name: Setup vcpkg
uses: lukka/run-vcpkg@v11
id: runvcpkg
with:
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT }}
vcpkgDirectory: ${{ github.workspace }}/vcpkg
vcpkgJsonGlob: 'windows/vcpkg.json'
# Cache vcpkg installed packages separately for faster restores
- name: Cache vcpkg installed packages
id: vcpkg-installed-cache
uses: actions/cache@v5
with:
path: ${{ github.workspace }}/vcpkg/installed
key: vcpkg-installed-${{ runner.os }}-${{ env.VCPKG_COMMIT }}-${{ hashFiles('windows/vcpkg.json') }}
restore-keys: |
vcpkg-installed-${{ runner.os }}-${{ env.VCPKG_COMMIT }}-
- name: Install vcpkg dependencies
if: steps.vcpkg-installed-cache.outputs.cache-hit != 'true'
run: ${{ github.workspace }}/vcpkg/vcpkg.exe install --x-install-root ${{ github.workspace }}/vcpkg/installed/
working-directory: windows
# Cache Rust/Cargo artifacts
- name: Cache Cargo registry
run: mkdir C:\vcpkg\.cache
- name: Cache vcpkg
id: cache
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-registry-
# Cache Cargo build artifacts - rust.bat sets CARGO_TARGET_DIR to windows/
# which results in artifacts at windows/x86_64-pc-windows-msvc/
- name: Cache Cargo build artifacts
uses: actions/cache@v5
C:\vcpkg\.cache
key: vcpkg-${{ runner.os }}-${{ env.VCPKG_COMMIT }}
- name: Build vcpkg
run: |
git clone https://github.com/microsoft/vcpkg
./vcpkg/bootstrap-vcpkg.bat
- name: Install dependencies
run: ${{ github.workspace }}/vcpkg/vcpkg.exe install --x-install-root ${{ github.workspace }}/vcpkg/installed/
working-directory: windows
- uses: actions-rs/toolchain@v1
with:
path: ${{ github.workspace }}/windows/x86_64-pc-windows-msvc
key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('src/rust/**/*.rs') }}
restore-keys: |
${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }}-
${{ runner.os }}-cargo-build-
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
toolchain: stable
override: true
- name: Install Win 10 SDK
uses: ilammy/msvc-dev-cmd@v1
# Build Release-Full
- name: Build Release-Full
- name: build Release-Full
env:
LIBCLANG_PATH: "C:\\Program Files\\LLVM\\lib"
LLVM_CONFIG_PATH: "C:\\Program Files\\LLVM\\bin\\llvm-config"
CARGO_TARGET_DIR: "..\\..\\windows"
BINDGEN_EXTRA_CLANG_ARGS: -fmsc-version=0
VCPKG_ROOT: ${{ github.workspace }}/vcpkg
run: msbuild ccextractor.sln /p:Configuration=Release-Full /p:Platform=x64
working-directory: ./windows
- name: Display Release version information
- name: Display version information
run: ./ccextractorwinfull.exe --version
working-directory: ./windows/x64/Release-Full
- name: Upload Release artifact
uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v6
with:
name: CCExtractor Windows Release build
path: |
./windows/x64/Release-Full/ccextractorwinfull.exe
./windows/x64/Release-Full/*.dll
# Build Debug-Full (reuses cached Cargo artifacts)
- name: Build Debug-Full
build_debug:
runs-on: windows-2022
steps:
- name: Check out repository
uses: actions/checkout@v6
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@v2.0.0
with:
msbuild-architecture: x64
- name: Install gpac
run: choco install gpac --version 2.4.0
- name: Setup vcpkg
run: mkdir C:\vcpkg\.cache
- name: Cache vcpkg
id: cache
uses: actions/cache@v5
with:
path: |
C:\vcpkg\.cache
key: vcpkg-${{ runner.os }}-${{ env.VCPKG_COMMIT }}
- name: Build vcpkg
run: |
git clone https://github.com/microsoft/vcpkg
./vcpkg/bootstrap-vcpkg.bat
- name: Install dependencies
run: ${{ github.workspace }}/vcpkg/vcpkg.exe install --x-install-root ${{ github.workspace }}/vcpkg/installed/
working-directory: windows
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Install Win 10 SDK
uses: ilammy/msvc-dev-cmd@v1
- name: build Debug-Full
env:
LIBCLANG_PATH: "C:\\Program Files\\LLVM\\lib"
LLVM_CONFIG_PATH: "C:\\Program Files\\LLVM\\bin\\llvm-config"
CARGO_TARGET_DIR: "..\\..\\windows"
BINDGEN_EXTRA_CLANG_ARGS: -fmsc-version=0
VCPKG_ROOT: ${{ github.workspace }}/vcpkg
run: msbuild ccextractor.sln /p:Configuration=Debug-Full /p:Platform=x64
working-directory: ./windows
- name: Display Debug version information
- name: Display version information
continue-on-error: true
run: ./ccextractorwinfull.exe --version
working-directory: ./windows/x64/Debug-Full
- name: Upload Debug artifact
uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v6
with:
name: CCExtractor Windows Debug build
path: |

View File

@@ -1,15 +0,0 @@
name: Bump Homebrew Formula
on:
release:
types: [published]
jobs:
homebrew:
runs-on: ubuntu-latest
steps:
- name: Update Homebrew formula
uses: dawidd6/action-homebrew-bump-formula@v7
with:
token: ${{ secrets.HOMEBREW_GITHUB_API_TOKEN }}
formula: ccextractor

View File

@@ -4,7 +4,7 @@ MAINTAINER = Marc Espie <espie@openbsd.org>
CATEGORIES = multimedia
COMMENT = closed caption subtitles extractor
HOMEPAGE = https://ccextractor.org
V = 0.96.5
V = 0.96.3
DISTFILES = ccextractor.${V:S/.//}-src.zip
MASTER_SITES = ${MASTER_SITE_SOURCEFORGE:=ccextractor/}
DISTNAME = ccextractor-$V

View File

@@ -28,7 +28,6 @@ The core functionality is written in C. Other languages used include C++ and Pyt
Downloads for precompiled binaries and source code can be found [on our website](https://ccextractor.org/public/general/downloads/).
### Windows Package Managers
**WinGet:**
@@ -62,34 +61,6 @@ You can also find the list of parameters and their brief description by running
You can find sample files on [our website](https://ccextractor.org/public/general/tvsamples/) to test the software.
### Building from Source
- [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).

View File

@@ -1,34 +1,3 @@
0.96.6 (unreleased)
-------------------
- New: Add Snap packaging support with Snapcraft configuration and GitHub Actions CI workflow.
- Fix: Clear status line output on Linux/WSL to prevent text artifacts (#2017)
- 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)
- Fix: --mkvlang now supports BCP 47 language tags (e.g., en-US, zh-Hans-CN) and multiple codes
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)
- Docs: Add Windows WSL build instructions
- Fix: Security fixes (out-of-bounds read/write) in a few places in the legacy C code.
0.96.4 (2026-01-01)
-------------------
- New: Persistent CEA-708 decoder context - maintains state across multiple calls for proper subtitle continuity
- New: OCR character blacklist options (--ocr-blacklist, --ocr-blacklist-file) for improved accuracy
- New: OCR line-split option (--ocr-splitontimechange) for better subtitle segmentation
- Fix: 32-bit build failures on i686 and armv7l architectures
- Fix: Legacy command-line argument compatibility (-1, -2, -12, --sc, --svc)
- Fix: Prevent heap buffer overflow in Teletext processing (security fix)
- Fix: Prevent integer overflow leading to heap buffer overflow in Transport Stream handling (security fix)
- Fix: Lazy OCR initialization - only initialize when first DVB subtitle is encountered
- Build: Optimized Windows CI workflow for faster builds
- Fix: Updated GUI with version 0.7.1. A blind attempt to fix a hang on start on some Windows.
0.96.3 (2025-12-29)
-------------------
- New: VOBSUB subtitle extraction with OCR support for MP4 files
@@ -62,7 +31,6 @@
- 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.

View File

@@ -1,16 +1,3 @@
# 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.

View File

@@ -1,137 +0,0 @@
# Building CCExtractor on Windows using WSL
This guide explains how to build CCExtractor on Windows using WSL (Ubuntu).
It is based on a fresh setup and includes all required dependencies and
common build issues encountered during compilation.
---
## Prerequisites
- Windows 10 or Windows 11
- WSL enabled
- Ubuntu installed via Microsoft Store
---
## Install WSL and Ubuntu
From PowerShell (run as Administrator):
```powershell
wsl --install -d Ubuntu
```
Restart the system if prompted, then launch Ubuntu from the Start menu.
---
## Update system packages
```bash
sudo apt update
```
---
## Install basic build tools
```bash
sudo apt install -y build-essential git pkg-config
```
---
## Install Rust (required)
CCExtractor includes Rust components, so Rust and Cargo are required.
```bash
curl https://sh.rustup.rs -sSf | sh
source ~/.cargo/env
```
Verify installation:
```bash
cargo --version
rustc --version
```
---
## Install required libraries
```bash
sudo apt install -y \
libclang-dev clang \
libtesseract-dev tesseract-ocr \
libgpac-dev
```
---
## Clone the repository
```bash
git clone https://github.com/CCExtractor/ccextractor.git
cd ccextractor
```
---
## Build CCExtractor
```bash
cd linux
./build
```
After a successful build, verify by running:
```bash
./ccextractor
```
You should see the help/usage output.
---
## Common build issues
### cargo: command not found
```bash
source ~/.cargo/env
```
---
### Unable to find libclang
```bash
sudo apt install libclang-dev clang
```
---
### gpac/isomedia.h: No such file or directory
```bash
sudo apt install libgpac-dev
```
---
### please install tesseract development library
```bash
sudo apt install libtesseract-dev tesseract-ocr
```
---
## Notes
- Compiler warnings during the build process are expected and do not indicate failure.
- This guide was tested on Ubuntu (WSL) running on Windows 11.

View File

@@ -2,7 +2,7 @@
# Process this file with autoconf to produce a configure script.
AC_PREREQ([2.71])
AC_INIT([CCExtractor], [0.96.5], [carlos@ccextractor.org])
AC_INIT([CCExtractor], [0.96.3], [carlos@ccextractor.org])
AC_CONFIG_AUX_DIR([build-conf])
AC_CONFIG_SRCDIR([../src/ccextractor.c])
AM_INIT_AUTOMAKE([foreign subdir-objects])

View File

@@ -42,16 +42,7 @@ while [[ $# -gt 0 ]]; do
esac
done
# Determine architecture based on cargo (to ensure consistency with Rust part)
CARGO_ARCH=$(file $(which cargo) | grep -o 'x86_64\|arm64')
if [[ "$CARGO_ARCH" == "x86_64" ]]; then
echo "Detected Intel (x86_64) Cargo. Forcing x86_64 build to match Rust and libraries..."
BLD_ARCH="-arch x86_64"
else
BLD_ARCH="-arch arm64"
fi
BLD_FLAGS="$BLD_ARCH -std=gnu99 -Wno-write-strings -Wno-pointer-sign -D_FILE_OFFSET_BITS=64 -DVERSION_FILE_PRESENT -Dfopen64=fopen -Dopen64=open -Dlseek64=lseek"
BLD_FLAGS="-std=gnu99 -Wno-write-strings -Wno-pointer-sign -D_FILE_OFFSET_BITS=64 -DVERSION_FILE_PRESENT -Dfopen64=fopen -Dopen64=open -Dlseek64=lseek"
# Add flags for bundled libraries (not needed when using system libs)
if [[ "$USE_SYSTEM_LIBS" != "true" ]]; then

View File

@@ -2,7 +2,7 @@
# Process this file with autoconf to produce a configure script.
AC_PREREQ([2.71])
AC_INIT([CCExtractor],[0.96.5],[carlos@ccextractor.org])
AC_INIT([CCExtractor],[0.96.3],[carlos@ccextractor.org])
AC_CONFIG_AUX_DIR([build-conf])
AC_CONFIG_SRCDIR([../src/ccextractor.c])
AM_INIT_AUTOMAKE([foreign subdir-objects])

View File

@@ -1,5 +1,5 @@
pkgname=ccextractor
pkgver=0.96.5
pkgver=0.96.3
pkgrel=1
pkgdesc="A closed captions and teletext subtitles extractor for video streams."
arch=('i686' 'x86_64')

View File

@@ -1,5 +1,5 @@
Name: ccextractor
Version: 0.96.5
Version: 0.96.3
Release: 1
Summary: A closed captions and teletext subtitles extractor for video streams.
Group: Applications/Internet

View File

@@ -1,7 +1,7 @@
#!/bin/bash
TYPE="debian" # can be one of 'slackware', 'debian', 'rpm'
PROGRAM_NAME="ccextractor"
VERSION="0.96.5"
VERSION="0.96.3"
RELEASE="1"
LICENSE="GPL-2.0"
MAINTAINER="carlos@ccextractor.org"

View File

@@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
<metadata>
<id>ccextractor</id>
<version>0.96.5</version>
<version>0.96.3</version>
<title>CCExtractor</title>
<authors>CCExtractor Development Team</authors>
<owners>CCExtractor</owners>

View File

@@ -7,7 +7,7 @@ $toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
$packageArgs = @{
packageName = $packageName
fileType = 'MSI'
url64bit = 'https://github.com/CCExtractor/ccextractor/releases/download/v0.96.5/CCExtractor.0.96.5.msi'
url64bit = 'https://github.com/CCExtractor/ccextractor/releases/download/v0.96.3/CCExtractor.0.96.3.msi'
checksum64 = 'FFCAB0D766180AFC2832277397CDEC885D15270DECE33A9A51947B790F1F095B'
checksumType64 = 'sha256'
silentArgs = '/quiet /norestart'

View File

@@ -1,6 +1,6 @@
# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.9.0.schema.json
PackageIdentifier: CCExtractor.CCExtractor
PackageVersion: 0.96.5
PackageVersion: 0.96.3
Platform:
- Windows.Desktop
MinimumOSVersion: 10.0.0.0
@@ -15,7 +15,7 @@ UpgradeBehavior: install
Installers:
- Architecture: x64
InstallerType: msi
InstallerUrl: https://github.com/CCExtractor/ccextractor/releases/download/v0.96.5/CCExtractor.0.96.5.msi
InstallerUrl: https://github.com/CCExtractor/ccextractor/releases/download/v0.96.3/CCExtractor.0.96.3.msi
InstallerSha256: FFCAB0D766180AFC2832277397CDEC885D15270DECE33A9A51947B790F1F095B
ManifestType: installer
ManifestVersion: 1.9.0

View File

@@ -1,6 +1,6 @@
# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.9.0.schema.json
PackageIdentifier: CCExtractor.CCExtractor
PackageVersion: 0.96.5
PackageVersion: 0.96.3
PackageLocale: en-US
Publisher: CCExtractor Development
PublisherUrl: https://ccextractor.org

View File

@@ -1,6 +1,6 @@
# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.9.0.schema.json
PackageIdentifier: CCExtractor.CCExtractor
PackageVersion: 0.96.5
PackageVersion: 0.96.3
DefaultLocale: en-US
ManifestType: version
ManifestVersion: 1.9.0

View File

@@ -1,19 +0,0 @@
#!/bin/sh
set -e
# Default fallback
LIB_TRIPLET="x86_64-linux-gnu"
# Detect multiarch directory if present
for d in "$SNAP/usr/lib/"*-linux-gnu; do
if [ -d "$d" ]; then
LIB_TRIPLET=$(basename "$d")
break
fi
done
export LD_LIBRARY_PATH="$SNAP/usr/lib:\
$SNAP/usr/lib/$LIB_TRIPLET:\
$SNAP/usr/lib/$LIB_TRIPLET/blas:\
$SNAP/usr/lib/$LIB_TRIPLET/lapack:\
$SNAP/usr/lib/$LIB_TRIPLET/pulseaudio:\
${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
shift
exec "$SNAP/usr/local/bin/ccextractor" "$@"

View File

@@ -1,104 +0,0 @@
name: ccextractor
base: core22
version: '0.96.5'
summary: Closed Caption Extractor
description: |
CCExtractor is a tool for extracting closed captions from video files.
website: https://www.ccextractor.org
source-code: https://github.com/CCExtractor/ccextractor
confinement: classic
apps:
ccextractor:
command: usr/local/bin/ccextractor
command-chain:
- local/run-ccextractor.sh
plugs:
- home
parts:
gpac:
plugin: make
source: https://github.com/gpac/gpac.git
source-tag: abi-16.4
build-packages:
- build-essential
- pkg-config
- zlib1g-dev
- libssl-dev
- libfreetype6-dev
- libjpeg-dev
- libpng-dev
override-build: |
set -eux
./configure --prefix=/usr
make -j$(nproc)
make DESTDIR=$SNAPCRAFT_PART_INSTALL install-lib
sed -i "s|^prefix=.*|prefix=$SNAPCRAFT_STAGE/usr|" $SNAPCRAFT_PART_INSTALL/usr/lib/pkgconfig/gpac.pc
stage:
- usr/lib/libgpac*
- usr/lib/pkgconfig/gpac.pc
- usr/include/gpac
ccextractor:
after: [gpac]
plugin: cmake
source: .
source-subdir: src
build-environment:
- PKG_CONFIG_PATH: "$SNAPCRAFT_STAGE/usr/lib/pkgconfig:$PKG_CONFIG_PATH"
build-snaps:
- cmake/latest/stable
- rustup/latest/stable
build-packages:
- build-essential
- pkg-config
- clang
- llvm-dev
- libclang-dev
- libzvbi-dev
- libtesseract-dev
- libavcodec-dev
- libavformat-dev
- libavdevice-dev
- libavfilter-dev
- libswscale-dev
- libx11-dev
- libxcb1-dev
- libxcb-shm0-dev
- libpng-dev
- zlib1g-dev
- libblas3
- liblapack3
stage-packages:
- libzvbi0
- libfreetype6
- libpng16-16
- libprotobuf-c1
- libutf8proc2
- libgl1
- libglu1-mesa
- libavcodec58
- libavformat58
- libavutil56
- libavdevice58
- libavfilter7
- libswscale5
- libjpeg-turbo8
- libvorbis0a
- libtheora0
- libxvidcore4
- libfaad2
- libmad0
- liba52-0.7.4
- libpulse0
- pulseaudio-utils
override-build: |
set -eux
rustup toolchain install stable
rustup default stable
export PATH="$HOME/.cargo/bin:$PATH"
snapcraftctl build
install -D -m 0755 \
$SNAPCRAFT_PROJECT_DIR/snap/local/run-ccextractor.sh \
$SNAPCRAFT_PART_INSTALL/local/run-ccextractor.sh

View File

@@ -9,7 +9,7 @@ option (WITH_HARDSUBX "Build with support for burned-in subtitles" OFF)
# Version number
set (CCEXTRACTOR_VERSION_MAJOR 0)
set (CCEXTRACTOR_VERSION_MINOR 96)
set (CCEXTRACTOR_VERSION_MINOR 89)
# Get project directory
get_filename_component(BASE_PROJ_DIR ../ ABSOLUTE)
@@ -255,13 +255,4 @@ endif (PKG_CONFIG_FOUND)
target_link_libraries (ccextractor ${EXTRA_LIBS})
target_include_directories (ccextractor PUBLIC ${EXTRA_INCLUDES})
# ccx_rust (Rust) calls C functions from ccx (like decode_vbi).
# Force the linker to pull these symbols from ccx before processing ccx_rust.
if (NOT WIN32 AND NOT APPLE)
target_link_options (ccextractor PRIVATE
-Wl,--undefined=decode_vbi
-Wl,--undefined=do_cb
-Wl,--undefined=store_hdcc)
endif()
install (TARGETS ccextractor DESTINATION bin)

View File

@@ -435,9 +435,6 @@ int main(int argc, char *argv[])
int compile_ret = ccxr_parse_parameters(argc, argv);
// Update the Rust logger target after parsing so --quiet is respected
ccxr_update_logger_target();
if (compile_ret == EXIT_NO_INPUT_FILES)
{
print_usage();

View File

@@ -1,9 +1,9 @@
cmake_policy (SET CMP0037 NEW)
if(MSVC)
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -W3 /wd4005 /wd4996")
set (CMAKE_C_FLAGS "-W3 /wd4005 /wd4996")
else (MSVC)
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wno-pointer-sign -g -std=gnu99")
set (CMAKE_C_FLAGS "-Wall -Wno-pointer-sign -g -std=gnu99")
endif(MSVC)
if(WIN32)

View File

@@ -392,24 +392,20 @@ void sei_rbsp(struct avc_ctx *ctx, unsigned char *seibuf, unsigned char *seiend)
unsigned char *sei_message(struct avc_ctx *ctx, unsigned char *seibuf, unsigned char *seiend)
{
int payload_type = 0;
while (seibuf < seiend && *seibuf == 0xff)
while (*seibuf == 0xff)
{
payload_type += 255;
seibuf++;
}
if (seibuf >= seiend)
return NULL;
payload_type += *seibuf;
seibuf++;
int payload_size = 0;
while (seibuf < seiend && *seibuf == 0xff)
while (*seibuf == 0xff)
{
payload_size += 255;
seibuf++;
}
if (seibuf >= seiend)
return NULL;
payload_size += *seibuf;
seibuf++;
@@ -996,9 +992,9 @@ void slice_header(struct encoder_ctx *enc_ctx, struct lib_cc_decode *dec_ctx, un
if (nal_unit_type == 5)
{
// idr_pic_id: Read to advance bitstream position; value not needed for caption extraction
tmp = read_exp_golomb_unsigned(&q1);
dvprint("idr_pic_id= % 4lld (%#llX)\n", tmp, tmp);
// TODO
}
if (dec_ctx->avc_ctx->pic_order_cnt_type == 0)
{

View File

@@ -141,9 +141,8 @@ void init_options(struct ccx_s_options *options)
options->enc_cfg.services_charsets = NULL;
options->enc_cfg.all_services_charset = NULL;
options->enc_cfg.with_semaphore = 0;
options->enc_cfg.force_dropframe = 0; // Assume No Drop Frame for MCC Encode.
options->enc_cfg.scc_framerate = 0; // Default: 29.97fps for SCC output
options->enc_cfg.scc_accurate_timing = 0; // Default: off for backwards compatibility (issue #1120)
options->enc_cfg.force_dropframe = 0; // Assume No Drop Frame for MCC Encode.
options->enc_cfg.scc_framerate = 0; // Default: 29.97fps for SCC output
options->enc_cfg.extract_only_708 = 0;
options->settings_dtvcc.enabled = 0;

View File

@@ -76,8 +76,7 @@ struct encoder_cfg
int force_dropframe; // 1 if dropframe frame count should be used. defaults to no drop frame.
// SCC output framerate
int scc_framerate; // SCC output framerate: 0=29.97 (default), 1=24, 2=25, 3=30
int scc_accurate_timing; // If 1, use bandwidth-aware timing for broadcast compliance (issue #1120)
int scc_framerate; // SCC output framerate: 0=29.97 (default), 1=24, 2=25, 3=30
// text -> png (text render)
char *render_font; // The font used to render text if needed (e.g. teletext->spupng)

View File

@@ -201,9 +201,6 @@ void delete_to_end_of_row(ccx_decoder_608_context *context)
{
if (context->mode != MODE_TEXT)
{
if (context->cursor_row >= CCX_DECODER_608_SCREEN_ROWS)
return;
struct eia608_screen *use_buffer = get_writing_buffer(context);
for (int i = context->cursor_column; i <= CCX_DECODER_608_SCREEN_WIDTH - 1; i++)
{
@@ -224,10 +221,6 @@ void write_char(const unsigned char c, ccx_decoder_608_context *context)
/* printf ("\rWriting char [%c] at %s:%d:%d\n",c,
use_buffer == &wb->data608->buffer1?"B1":"B2",
wb->data608->cursor_row,wb->data608->cursor_column); */
if (context->cursor_row >= CCX_DECODER_608_SCREEN_ROWS || context->cursor_column >= CCX_DECODER_608_SCREEN_WIDTH)
return;
use_buffer->characters[context->cursor_row][context->cursor_column] = c;
use_buffer->colors[context->cursor_row][context->cursor_column] = context->current_color;
use_buffer->fonts[context->cursor_row][context->cursor_column] = context->font;
@@ -323,20 +316,10 @@ int write_cc_buffer(ccx_decoder_608_context *context, struct cc_subtitle *sub)
if (!data->empty && context->output_format != CCX_OF_NULL)
{
size_t new_size;
if (sub->nb_data + 1 > SIZE_MAX / sizeof(struct eia608_screen))
{
ccx_common_logging.log_ftn("Too many screens, cannot allocate more memory.\n");
return 0;
}
new_size = (sub->nb_data + 1) * sizeof(struct eia608_screen);
struct eia608_screen *new_data = (struct eia608_screen *)realloc(sub->data, new_size);
struct eia608_screen *new_data = (struct eia608_screen *)realloc(sub->data, (sub->nb_data + 1) * sizeof(*data));
if (!new_data)
{
ccx_common_logging.log_ftn("Out of memory while reallocating screen buffer\n");
ccx_common_logging.log_ftn("No Memory left");
return 0;
}
sub->data = new_data;
@@ -403,20 +386,10 @@ int write_cc_line(ccx_decoder_608_context *context, struct cc_subtitle *sub)
if (!data->empty)
{
size_t new_size;
if (sub->nb_data + 1 > SIZE_MAX / sizeof(struct eia608_screen))
{
ccx_common_logging.log_ftn("Too many screens, cannot allocate more memory.\n");
return 0;
}
new_size = (sub->nb_data + 1) * sizeof(struct eia608_screen);
struct eia608_screen *new_data = (struct eia608_screen *)realloc(sub->data, new_size);
struct eia608_screen *new_data = (struct eia608_screen *)realloc(sub->data, (sub->nb_data + 1) * sizeof(*data));
if (!new_data)
{
ccx_common_logging.log_ftn("Out of memory while reallocating screen buffer\n");
ccx_common_logging.log_ftn("No Memory left");
return 0;
}
sub->data = new_data;

View File

@@ -998,14 +998,6 @@ void dtvcc_handle_DFx_DefineWindow(dtvcc_service_decoder *decoder, int window_id
int row_count = (data[4] & 0xf) + 1; // according to CEA-708-D
int anchor_point = data[4] >> 4;
int col_count = (data[5] & 0x3f) + 1; // according to CEA-708-D
if (row_count > CCX_DTVCC_MAX_ROWS || col_count > CCX_DTVCC_MAX_COLUMNS)
{
ccx_common_logging.log_ftn("[CEA-708] Invalid window size %dx%d (max %dx%d), rejecting window definition\n",
row_count, col_count, CCX_DTVCC_MAX_ROWS, CCX_DTVCC_MAX_COLUMNS);
return;
}
int pen_style = data[6] & 0x7;
int win_style = (data[6] >> 3) & 0x7;
@@ -1349,14 +1341,6 @@ void dtvcc_handle_SPL_SetPenLocation(dtvcc_service_decoder *decoder, unsigned ch
}
dtvcc_window *window = &decoder->windows[decoder->current_window];
if (row >= window->row_count || col >= window->col_count)
{
ccx_common_logging.log_ftn("[CEA-708] dtvcc_handle_SPL_SetPenLocation: "
"Invalid pen location %d:%d for window size %dx%d, rejecting command\n",
row, col, window->row_count, window->col_count);
return;
}
window->pen_row = row;
window->pen_column = col;
}
@@ -1495,12 +1479,7 @@ int dtvcc_handle_C0(dtvcc_ctx *dtvcc,
else if (c0 >= 0x18 && c0 <= 0x1F)
{
if (c0 == DTVCC_C0_P16) // PE16
{
if (data_length >= 3)
dtvcc_handle_C0_P16(decoder, data + 1);
else
ccx_common_logging.debug_ftn(CCX_DMT_708, "[CEA-708] dtvcc_handle_C0: Not enough data for P16\n");
}
dtvcc_handle_C0_P16(decoder, data + 1);
len = 3;
}
if (len == -1)
@@ -1654,9 +1633,6 @@ int dtvcc_handle_extended_char(dtvcc_service_decoder *decoder, unsigned char *da
ccx_common_logging.debug_ftn(CCX_DMT_708, "[CEA-708] In dtvcc_handle_extended_char, "
"first data code: [%c], length: [%u]\n",
data[0], data_length);
if (data_length < 1)
return 0;
unsigned char c = 0x20; // Default to space
unsigned char code = data[0];
if (/* data[i]>=0x00 && */ code <= 0x1F) // Comment to silence warning
@@ -1725,17 +1701,8 @@ void dtvcc_process_service_block(dtvcc_ctx *dtvcc,
}
else // Use extended set
{
if (i + 1 >= data_length)
{
used = 1; // skip EXT1
}
else
{
used = dtvcc_handle_extended_char(decoder,
data + i + 1,
data_length - i - 1) +
1;
}
used = dtvcc_handle_extended_char(decoder, data + i + 1, data_length - 1);
used++; // Since we had DTVCC_C0_EXT1
}
i += used;
}
@@ -1787,12 +1754,6 @@ void dtvcc_process_current_packet(dtvcc_ctx *dtvcc, int len)
if (service_number == 7) // There is an extended header
{
if (pos + 1 >= dtvcc->current_packet + len)
{
ccx_common_logging.debug_ftn(CCX_DMT_708, "[CEA-708] dtvcc_process_current_packet: "
"Truncated extended header, stopping.\n");
break;
}
pos++;
service_number = (pos[0] & 0x3F); // 6 more significant bits
// printf ("Extended header: Service number: [%d]\n",service_number);

View File

@@ -724,17 +724,16 @@ static int parse_csi(ISDBSubContext *ctx, const uint8_t *buf, int len)
// Copy buf in arg
for (i = 0; *buf != 0x20; i++)
{
if (i >= sizeof(arg) - 1)
if (i >= (sizeof(arg)) + 1)
{
isdb_log("UnExpected CSI: too long");
isdb_log("UnExpected CSI %d >= %d", sizeof(arg) + 1, i);
break;
}
arg[i] = *buf;
buf++;
}
/* ignore terminating 0x20 character */
if (i < sizeof(arg))
arg[i] = *buf++;
arg[i] = *buf++;
switch (*buf)
{

View File

@@ -285,9 +285,6 @@ static void ccx_demuxer_print_cfg(struct ccx_demuxer *ctx)
case CCX_SM_MXF:
mprint("MXF");
break;
case CCX_SM_SCC:
mprint("SCC");
break;
#ifdef WTV_DEBUG
case CCX_SM_HEX_DUMP:
mprint("Hex");

View File

@@ -75,15 +75,12 @@ enum MXFLocalTag
void update_tid_lut(struct MXFContext *ctx, uint32_t track_id, uint8_t *track_number, struct ccx_rational edit_rate)
{
int i;
debug("update_tid_lut: track_id=%u (0x%x), track_number=%02X%02X%02X%02X, cap_track_id=%u\n",
track_id, track_id, track_number[0], track_number[1], track_number[2], track_number[3], ctx->cap_track_id);
// Update essence element key if we have track Id of caption
if (ctx->cap_track_id == track_id)
{
memcpy(ctx->cap_essence_key, mxf_essence_element_key, 12);
memcpy(ctx->cap_essence_key + 12, track_number, 4);
ctx->edit_rate = edit_rate;
debug("MXF: Found caption track, track_id=%u\n", track_id);
}
for (i = 0; i < ctx->nb_tracks; i++)
@@ -251,7 +248,6 @@ static int mxf_read_vanc_vbi_desc(struct ccx_demuxer *demux, uint64_t size)
{
case MXF_TAG_LTRACK_ID:
ctx->cap_track_id = buffered_get_be32(demux);
debug("MXF: VANC/VBI descriptor found, Linked Track ID = %u\n", ctx->cap_track_id);
update_cap_essence_key(ctx, ctx->cap_track_id);
break;
default:
@@ -308,17 +304,6 @@ static int mxf_read_cdp_data(struct ccx_demuxer *demux, int size, struct demuxer
log("Incomplete CDP packet\n");
ret = buffered_read(demux, data->buffer + data->len, cc_count * 3);
// Log first few bytes of cc_data for debugging
if (cc_count > 0)
{
unsigned char *cc_ptr = data->buffer + data->len;
debug("cc_data (first 6 triplets): ");
for (int j = 0; j < (cc_count < 6 ? cc_count : 6); j++)
{
debug("%02X%02X%02X ", cc_ptr[j * 3], cc_ptr[j * 3 + 1], cc_ptr[j * 3 + 2]);
}
debug("\n");
}
data->len += cc_count * 3;
demux->past += cc_count * 3;
len += ret;
@@ -376,10 +361,7 @@ static int mxf_read_vanc_data(struct ccx_demuxer *demux, uint64_t size, struct d
// uint8_t count; /* Currently unused */
if (size < 19)
{
debug("VANC data too small: %" PRIu64 " < 19\n", size);
goto error;
}
ret = buffered_read(demux, vanc_header, 16);
@@ -388,39 +370,31 @@ static int mxf_read_vanc_data(struct ccx_demuxer *demux, uint64_t size, struct d
return CCX_EOF;
len += ret;
debug("VANC header: num_packets=%d, line=0x%02x%02x, wrap_type=0x%02x, sample_config=0x%02x\n",
vanc_header[1], vanc_header[2], vanc_header[3], vanc_header[4], vanc_header[5]);
for (int i = 0; i < vanc_header[1]; i++)
{
DID = buffered_get_byte(demux);
len++;
debug("VANC packet %d: DID=0x%02x\n", i, DID);
if (!(DID == 0x61 || DID == 0x80))
{
debug("DID 0x%02x not recognized as caption DID\n", DID);
goto error;
}
SDID = buffered_get_byte(demux);
len++;
debug("VANC packet %d: SDID=0x%02x\n", i, SDID);
if (SDID == 0x01)
debug("Caption Type 708\n");
else if (SDID == 0x02)
debug("Caption Type 608\n");
cdp_size = buffered_get_byte(demux);
debug("VANC packet %d: cdp_size=%d\n", i, cdp_size);
if (cdp_size + 19 > size)
{
log("Incomplete cdp(%d) in anc data(%" PRIu64 ")\n", cdp_size, size);
debug("Incomplete cdp(%d) in anc data(%d)\n", cdp_size, size);
goto error;
}
len++;
ret = mxf_read_cdp_data(demux, cdp_size, data);
debug("mxf_read_cdp_data returned %d, data->len=%d\n", ret, data->len);
len += ret;
// len += (3 + count + 4);
}
@@ -437,33 +411,15 @@ static int mxf_read_essence_element(struct ccx_demuxer *demux, uint64_t size, st
int ret;
struct MXFContext *ctx = demux->private_data;
debug("mxf_read_essence_element: ctx->type=%d (ANC=%d, VBI=%d), size=%" PRIu64 "\n",
ctx->type, MXF_CT_ANC, MXF_CT_VBI, size);
if (ctx->type == MXF_CT_ANC)
{
data->bufferdatatype = CCX_RAW_TYPE;
ret = mxf_read_vanc_data(demux, size, data);
debug("mxf_read_vanc_data returned %d, data->len=%d\n", ret, data->len);
// Calculate PTS in 90kHz units from frame count and edit rate
// edit_rate is frames per second (e.g., 25/1 for 25fps)
// PTS = frame_count * 90000 / fps = frame_count * 90000 * edit_rate.den / edit_rate.num
if (ctx->edit_rate.num > 0 && ctx->edit_rate.den > 0)
{
data->pts = (int64_t)ctx->cap_count * 90000 * ctx->edit_rate.den / ctx->edit_rate.num;
}
else
{
// Fallback to 25fps if edit_rate not set
data->pts = (int64_t)ctx->cap_count * 90000 / 25;
}
debug("Frame %d, PTS=%" PRId64 " (edit_rate=%d/%d)\n",
ctx->cap_count, data->pts, ctx->edit_rate.num, ctx->edit_rate.den);
data->pts = ctx->cap_count;
ctx->cap_count++;
}
else
{
debug("Skipping essence element (not ANC type)\n");
ret = buffered_skip(demux, size);
demux->past += ret;
}
@@ -558,7 +514,6 @@ static int read_packet(struct ccx_demuxer *demux, struct demuxer_data *data)
KLVPacket klv;
const MXFReadTableEntry *reader;
struct MXFContext *ctx = demux->private_data;
static int first_essence_logged = 0;
while ((ret = klv_read_packet(&klv, demux)) == 0)
{
debug("Key %02X%02X%02X%02X%02X%02X%02X%02X.%02X%02X%02X%02X%02X%02X%02X%02X size %" PRIu64 "\n",
@@ -568,25 +523,8 @@ static int read_packet(struct ccx_demuxer *demux, struct demuxer_data *data)
klv.key[12], klv.key[13], klv.key[14], klv.key[15],
klv.length);
// Check if this is an essence element key (first 12 bytes match)
if (IS_KLV_KEY(klv.key, mxf_essence_element_key) && !first_essence_logged)
{
debug("MXF: First essence element key: %02X%02X%02X%02X%02X%02X%02X%02X.%02X%02X%02X%02X%02X%02X%02X%02X\n",
klv.key[0], klv.key[1], klv.key[2], klv.key[3],
klv.key[4], klv.key[5], klv.key[6], klv.key[7],
klv.key[8], klv.key[9], klv.key[10], klv.key[11],
klv.key[12], klv.key[13], klv.key[14], klv.key[15]);
debug("MXF: cap_essence_key: %02X%02X%02X%02X%02X%02X%02X%02X.%02X%02X%02X%02X%02X%02X%02X%02X\n",
ctx->cap_essence_key[0], ctx->cap_essence_key[1], ctx->cap_essence_key[2], ctx->cap_essence_key[3],
ctx->cap_essence_key[4], ctx->cap_essence_key[5], ctx->cap_essence_key[6], ctx->cap_essence_key[7],
ctx->cap_essence_key[8], ctx->cap_essence_key[9], ctx->cap_essence_key[10], ctx->cap_essence_key[11],
ctx->cap_essence_key[12], ctx->cap_essence_key[13], ctx->cap_essence_key[14], ctx->cap_essence_key[15]);
first_essence_logged = 1;
}
if (IS_KLV_KEY(klv.key, ctx->cap_essence_key))
{
debug("MXF: Found ANC essence element, size=%" PRIu64 "\n", klv.length);
mxf_read_essence_element(demux, klv.length, data);
if (data->len > 0)
break;
@@ -628,15 +566,8 @@ int ccx_mxf_getmoredata(struct lib_ccx_ctx *ctx, struct demuxer_data **ppdata)
data->program_number = 1;
data->stream_pid = 1;
data->codec = CCX_CODEC_ATSC_CC;
// PTS is already calculated in 90kHz units by mxf_read_essence_element
data->tb.num = 1;
data->tb.den = 90000;
// Enable CEA-708 (DTVCC) decoder for MXF files with VANC captions
if (ctx->dec_global_setting && ctx->dec_global_setting->settings_dtvcc)
{
ctx->dec_global_setting->settings_dtvcc->enabled = 1;
}
data->tb.num = 1001;
data->tb.den = 30000;
}
else
{
@@ -645,11 +576,6 @@ int ccx_mxf_getmoredata(struct lib_ccx_ctx *ctx, struct demuxer_data **ppdata)
ret = read_packet(ctx->demux_ctx, data);
// Ensure timebase is 90kHz since PTS is calculated in 90kHz units
// CDP parsing may have set a frame-based timebase which would cause incorrect conversion
data->tb.num = 1;
data->tb.den = 90000;
return ret;
}

View File

@@ -25,7 +25,7 @@ void dtvcc_process_data(struct dtvcc_ctx *dtvcc,
ccx_common_logging.debug_ftn(CCX_DMT_708, "[CEA-708] dtvcc_process_data: DTVCC Channel Packet Data\n");
if (cc_valid && dtvcc->is_current_packet_header_parsed)
{
if (dtvcc->current_packet_length + 2 > CCX_DTVCC_MAX_PACKET_LENGTH)
if (dtvcc->current_packet_length > 253)
{
ccx_common_logging.debug_ftn(CCX_DMT_708, "[CEA-708] dtvcc_process_data: "
"Warning: Legal packet size exceeded (1), data not added.\n");
@@ -51,7 +51,7 @@ void dtvcc_process_data(struct dtvcc_ctx *dtvcc,
ccx_common_logging.debug_ftn(CCX_DMT_708, "[CEA-708] dtvcc_process_data: DTVCC Channel Packet Start\n");
if (cc_valid)
{
if (dtvcc->current_packet_length + 2 > CCX_DTVCC_MAX_PACKET_LENGTH)
if (dtvcc->current_packet_length > CCX_DTVCC_MAX_PACKET_LENGTH - 1)
{
ccx_common_logging.debug_ftn(CCX_DMT_708, "[CEA-708] dtvcc_process_data: "
"Warning: Legal packet size exceeded (2), data not added.\n");

View File

@@ -17,7 +17,6 @@ extern void ccxr_dtvcc_free(void *dtvcc_rust);
extern void ccxr_dtvcc_process_data(void *dtvcc_rust, const unsigned char cc_valid,
const unsigned char cc_type, const unsigned char data1, const unsigned char data2);
extern int ccxr_dtvcc_is_active(void *dtvcc_rust);
extern void ccxr_dtvcc_set_active(void *dtvcc_rust, int active);
#endif
#endif // CCEXTRACTOR_CCX_DTVCC_H

View File

@@ -775,7 +775,6 @@ 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;
@@ -842,9 +841,6 @@ struct encoder_ctx *init_encoder(struct encoder_cfg *opt)
ctx->segment_last_key_frame = 0;
ctx->nospupngocr = opt->nospupngocr;
ctx->scc_framerate = opt->scc_framerate;
ctx->scc_accurate_timing = opt->scc_accurate_timing;
ctx->scc_last_transmission_end = 0;
ctx->scc_last_display_end = 0;
// Initialize teletext multi-page output arrays (issue #665)
ctx->tlt_out_count = 0;

View File

@@ -156,11 +156,6 @@ struct encoder_ctx
// SCC output framerate
int scc_framerate; // SCC output framerate: 0=29.97 (default), 1=24, 2=25, 3=30
// SCC accurate timing (issue #1120)
int scc_accurate_timing; // If 1, use bandwidth-aware timing for broadcast compliance
LLONG scc_last_transmission_end; // When last caption transmission ends (ms)
LLONG scc_last_display_end; // When last caption display ends (ms)
int new_sentence; // Capitalize next letter?
int program_number;
@@ -182,12 +177,12 @@ 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
unsigned int tlt_srt_counter[MAX_TLT_PAGES_EXTRACT]; // SRT counter per page
int tlt_out_count; // Number of teletext output files
int tlt_out_count; // Number of teletext output files
};
#define INITIAL_ENC_BUFFER_CAPACITY 2048

View File

@@ -10,171 +10,6 @@ unsigned char odd_parity(const unsigned char byte)
return byte | !(cc608_parity(byte) % 2) << 7;
}
/**
* SCC Accurate Timing Implementation (Issue #1120)
*
* EIA-608 bandwidth constraints:
* - 2 bytes per frame at 29.97 FPS (or configured frame rate)
* - Captions must be pre-loaded before display time
* - Each control code takes 2 bytes (sent twice for reliability = 4 bytes total)
* - Text characters take 1 byte each
*/
// Get frame rate value from scc_framerate setting
// 0=29.97 (default), 1=24, 2=25, 3=30
static float get_scc_fps_internal(int scc_framerate)
{
switch (scc_framerate)
{
case 1:
return 24.0f;
case 2:
return 25.0f;
case 3:
return 30.0f;
default:
return 29.97f;
}
}
/**
* Calculate total bytes needed to transmit a caption
*
* Byte costs:
* - Control code (RCL, EOC, ENM, EDM): 2 bytes x 2 (sent twice) = 4 bytes
* - Preamble code: 2 bytes x 2 = 4 bytes
* - Tab offset: 2 bytes x 2 = 4 bytes
* - Mid-row code (color/style): 2 bytes x 2 = 4 bytes
* - Text character: 1 byte each
* - Padding: 1 byte if odd number of text bytes
*/
static unsigned int calculate_caption_bytes(const struct eia608_screen *data)
{
unsigned int total_bytes = 0;
// RCL (Resume Caption Loading): 4 bytes
total_bytes += 4;
for (unsigned char row = 0; row < 15; ++row)
{
if (!data->row_used[row])
continue;
int first, last;
find_limit_characters(data->characters[row], &first, &last, CCX_DECODER_608_SCREEN_WIDTH);
if (first > last)
continue;
// Assume we need at least one preamble per row: 4 bytes
total_bytes += 4;
// Count characters on this row
unsigned int char_count = 0;
enum font_bits prev_font = FONT_REGULAR;
enum ccx_decoder_608_color_code prev_color = COL_WHITE;
int prev_col = -1;
for (int col = first; col <= last; ++col)
{
// Check if we need position codes
if (prev_col != col - 1 && prev_col != -1)
{
// Need preamble + possible tab offset: 4-8 bytes
total_bytes += 4;
if (col % 4 != 0)
total_bytes += 4; // Tab offset
}
// Check if we need mid-row style codes
if (data->fonts[row][col] != prev_font || data->colors[row][col] != prev_color)
{
total_bytes += 4; // Mid-row code
prev_font = data->fonts[row][col];
prev_color = data->colors[row][col];
}
// Text character
char_count++;
prev_col = col;
}
// Add text bytes (1 per character, rounded up to even)
total_bytes += char_count;
if (char_count % 2 == 1)
total_bytes++; // Padding
}
// EOC (End of Caption): 4 bytes
total_bytes += 4;
// ENM (Erase Non-displayed Memory): 4 bytes
total_bytes += 4;
return total_bytes;
}
/**
* Calculate the pre-roll start time for a caption
*
* @param display_time When the caption should appear on screen (ms)
* @param total_bytes Total bytes to transmit
* @param fps Frame rate
* @return Time to begin loading the caption (ms)
*/
static LLONG calculate_preroll_time(LLONG display_time, unsigned int total_bytes, float fps)
{
// Calculate transmission time in milliseconds
// 2 bytes per frame, so frames_needed = (total_bytes + 1) / 2
float ms_per_frame = 1000.0f / fps;
unsigned int frames_needed = (total_bytes + 1) / 2;
LLONG transmission_time_ms = (LLONG)(frames_needed * ms_per_frame);
// Add 1 frame for EOC to be sent before display
LLONG one_frame_ms = (LLONG)ms_per_frame;
LLONG preroll_start = display_time - transmission_time_ms - one_frame_ms;
// Don't go negative
if (preroll_start < 0)
preroll_start = 0;
return preroll_start;
}
/**
* Check for collision with previous caption transmission and resolve it
*
* @param context Encoder context with timing state
* @param preroll_start Proposed pre-roll start time (will be modified if collision)
* @param display_time Caption display time (may be adjusted)
* @param fps Frame rate
* @return true if timing was adjusted due to collision
*/
static bool resolve_collision(struct encoder_ctx *context, LLONG *preroll_start,
LLONG *display_time, float fps)
{
// Check if our preroll would start before previous caption finishes transmitting
// This prevents bandwidth collision but allows visual overlap (like scc_tools)
// Visual overlap is fine - the EOC command swaps buffers atomically
if (context->scc_last_transmission_end > 0 &&
*preroll_start < context->scc_last_transmission_end)
{
// Bandwidth collision detected - shift our caption forward
// Add 1 frame buffer to ensure no overlap
LLONG one_frame_ms = (LLONG)(1000.0f / fps);
LLONG new_preroll = context->scc_last_transmission_end + one_frame_ms;
LLONG shift = new_preroll - *preroll_start;
*preroll_start = new_preroll;
*display_time += shift;
return true;
}
return false;
}
struct control_code_info
{
unsigned int byte1_odd;
@@ -854,13 +689,8 @@ void add_timestamp(const struct encoder_ctx *context, LLONG time, const bool dis
// SMPTE format - use configurable frame rate (issue #1191)
float fps = get_scc_fps(context->scc_framerate);
// Calculate frame number from milliseconds, ensuring it stays in valid range 0 to fps-1
// Use floor to avoid rounding up to fps (e.g., 29.97 -> 30 is invalid)
int max_frames = (int)fps;
int frame = (int)(milli * fps / 1000.0f);
if (frame >= max_frames)
frame = max_frames - 1; // Cap at max valid frame (e.g., 29 for 29.97fps)
fdprintf(context->out->fh, "%02u:%02u:%02u:%02d\t", hour, minute, second, frame);
float frame = milli * fps / 1000;
fdprintf(context->out->fh, "%02u:%02u:%02u:%02.f\t", hour, minute, second, frame);
}
void clear_screen(const struct encoder_ctx *context, LLONG end_time, const unsigned char channel, const bool disassemble)
@@ -880,51 +710,8 @@ int write_cc_buffer_as_scenarist(const struct eia608_screen *data, struct encode
unsigned char current_row = UINT8_MAX;
unsigned char current_column = UINT8_MAX;
// Timing variables for accurate timing mode (issue #1120)
LLONG actual_start_time = data->start_time; // When caption should display
LLONG actual_end_time = data->end_time; // When caption should clear
LLONG preroll_start = data->start_time; // When to start loading (default: same as display)
float fps = get_scc_fps_internal(context->scc_framerate);
bool use_separate_display_time = false; // Whether to write EOC at separate timestamp
// If accurate timing is enabled, calculate pre-roll and handle collisions
if (context->scc_accurate_timing)
{
// Calculate total bytes needed for this caption
unsigned int total_bytes = calculate_caption_bytes(data);
// Calculate when we need to start loading
preroll_start = calculate_preroll_time(actual_start_time, total_bytes, fps);
// Check for collisions with previous caption and resolve
if (resolve_collision(context, &preroll_start, &actual_start_time, fps))
{
// Timing was adjusted due to collision
// Also adjust end time by the same amount
LLONG shift = actual_start_time - data->start_time;
actual_end_time = data->end_time + shift;
}
// Update timing state for next caption
float ms_per_frame = 1000.0f / fps;
unsigned int frames_needed = (total_bytes + 1) / 2;
LLONG transmission_time_ms = (LLONG)(frames_needed * ms_per_frame);
context->scc_last_transmission_end = preroll_start + transmission_time_ms;
context->scc_last_display_end = actual_end_time;
// Enable separate display timing (like scc_tools)
use_separate_display_time = true;
// 1. Load the caption at pre-roll time
add_timestamp(context, preroll_start, disassemble);
}
else
{
// Legacy mode: use original timing
// 1. Load the caption
add_timestamp(context, data->start_time, disassemble);
}
// 1. Load the caption
add_timestamp(context, data->start_time, disassemble);
write_control_code(context->out->fh, data->channel, RCL, disassemble, &bytes_written);
for (uint8_t row = 0; row < 15; ++row)
{
@@ -1007,26 +794,12 @@ int write_cc_buffer_as_scenarist(const struct eia608_screen *data, struct encode
check_padding(context->out->fh, disassemble, &bytes_written);
}
// 2. Show the caption (EOC = End of Caption, makes it visible)
if (use_separate_display_time)
{
// For accurate timing: write display command at actual display time
// This matches scc_tools behavior where load and display are separate
add_timestamp(context, actual_start_time, disassemble);
}
// 2. Show the caption
write_control_code(context->out->fh, data->channel, EOC, disassemble, &bytes_written);
write_control_code(context->out->fh, data->channel, ENM, disassemble, &bytes_written);
// 3. Clear the caption at the end time
// In accurate timing mode, skip clear - the next caption's EOC will handle the transition
// This matches scc_tools behavior which doesn't write EDM between consecutive captions
if (!use_separate_display_time)
{
// Legacy mode: always write clear
clear_screen(context, actual_end_time, data->channel, disassemble);
}
// In accurate timing mode, scc_last_display_end is still tracked for reference
// but we don't write the clear command to avoid out-of-order timestamps
// 3. Clear the caption
clear_screen(context, data->end_time, data->channel, disassemble);
return 1;
}

View File

@@ -251,9 +251,6 @@ 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)
{
@@ -387,7 +384,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 = 0; // Track if we actually wrote the tag
int wrote_opentag = 1;
x_pos = -1;
y_pos = -1;
@@ -398,11 +395,13 @@ 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;
// No subtitle data, skip writing
if (wrote_opentag)
write_sputag_close(sp);
return 0;
}
rect = sub->data;
@@ -441,13 +440,7 @@ int write_cc_bitmap_as_spupng(struct cc_subtitle *sub, struct encoder_ctx *conte
}
}
filename = get_spupng_filename(sp);
// 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);
set_spupng_offset(sp, x_pos, y_pos);
if (sub->flags & SUB_EOD_MARKER)
context->prev_start = sub->start_time;
pbuf = (uint8_t *)malloc(width * height);
@@ -482,15 +475,6 @@ 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)
{
@@ -503,6 +487,8 @@ 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)
@@ -1005,8 +991,6 @@ 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);
@@ -1097,28 +1081,6 @@ 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)
{
@@ -1137,7 +1099,6 @@ 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);

View File

@@ -39,8 +39,6 @@ struct spupng_t
int fileIndex;
int xOffset;
int yOffset;
int img_w;
int img_h;
};
#endif

View File

@@ -182,7 +182,6 @@ typedef struct DVBSubContext
LLONG time_out;
#ifdef ENABLE_OCR
void *ocr_ctx;
int ocr_initialized; // Flag to track if OCR has been lazily initialized
#endif
DVBSubRegion *region_list;
DVBSubCLUT *clut_list;
@@ -443,11 +442,7 @@ void *dvbsub_init_decoder(struct dvb_config *cfg)
}
#ifdef ENABLE_OCR
// Lazy OCR initialization: don't init here, wait until a bitmap actually needs OCR
// This avoids ~10 second Tesseract startup overhead for files that have DVB streams
// but don't actually produce any bitmap subtitles (e.g., files with CEA-608 captions)
ctx->ocr_ctx = NULL;
ctx->ocr_initialized = 0;
ctx->ocr_ctx = init_ocr(ctx->lang_index);
#endif
ctx->version = -1;
@@ -1706,13 +1701,7 @@ static int write_dvb_sub(struct lib_cc_decode *dec_ctx, struct cc_subtitle *sub)
// Perform OCR
#ifdef ENABLE_OCR
char *ocr_str = NULL;
// Lazy OCR initialization: only init when we actually have a bitmap to process
if (!ctx->ocr_initialized)
{
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 && region)
if (ctx->ocr_ctx)
{
int ret = ocr_rect(ctx->ocr_ctx, rect, &ocr_str, region->bgcolor, dec_ctx->ocr_quantmode);
if (ret >= 0)

View File

@@ -142,7 +142,7 @@ int user_data(struct encoder_ctx *enc_ctx, struct lib_cc_decode *dec_ctx, struct
{
if ((ud_header[1] & 0x7F) == 0x01)
{
unsigned char cc_data[3 * 32]; // Increased for safety margin, 31 is max count
unsigned char cc_data[3 * 31 + 1]; // Maximum cc_count is 31
dec_ctx->stat_scte20ccheaders++;
read_bytes(ustream, 2); // "03 01"
@@ -370,7 +370,6 @@ int user_data(struct encoder_ctx *enc_ctx, struct lib_cc_decode *dec_ctx, struct
dbg_print(CCX_DMT_PARSE, "%s", debug_608_to_ASC(dishdata, 0));
dbg_print(CCX_DMT_PARSE, "%s:\n", debug_608_to_ASC(dishdata + 3, 0));
dishdata[cc_count * 3] = 0xFF; // Ensure termination for store_hdcc
store_hdcc(enc_ctx, dec_ctx, dishdata, cc_count, dec_ctx->timing->current_tref, dec_ctx->timing->fts_now, sub);
// Ignore 4 (0x020A, followed by two unknown) bytes.
@@ -485,10 +484,7 @@ int user_data(struct encoder_ctx *enc_ctx, struct lib_cc_decode *dec_ctx, struct
mprint("MPEG:VBI: only support Luma line\n");
if (udatalen < 720)
{
mprint("MPEG:VBI: Minimum 720 bytes in luma line required, skipping truncated packet.\n");
return 1;
}
mprint("MPEG:VBI: Minimum 720 bytes in luma line required\n");
decode_vbi(dec_ctx, field, ustream->pos, 720, sub);
dbg_print(CCX_DMT_VERBOSE, "GXF (vbi line %d) user data:\n", line_nb);

View File

@@ -66,7 +66,6 @@ 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;

View File

@@ -18,7 +18,6 @@
#include "ccx_gxf.h"
#include "dvd_subtitle_decoder.h"
#include "ccx_demuxer_mxf.h"
#include "ccx_dtvcc.h"
int end_of_file = 0; // End of file?
@@ -567,104 +566,6 @@ static size_t process_raw_for_mcc(struct encoder_ctx *enc_ctx, struct lib_cc_dec
}
// Raw file process
// Parse raw CDP (Caption Distribution Packet) data
// Returns number of bytes processed
static size_t process_raw_cdp(struct encoder_ctx *enc_ctx, struct lib_cc_decode *dec_ctx,
struct cc_subtitle *sub, unsigned char *buffer, size_t len)
{
size_t pos = 0;
int cdp_count = 0;
while (pos + 10 < len) // Minimum CDP size
{
// Check for CDP identifier
if (buffer[pos] != 0x96 || buffer[pos + 1] != 0x69)
{
pos++;
continue;
}
unsigned char cdp_length = buffer[pos + 2];
if (pos + cdp_length > len)
break; // Incomplete CDP packet
unsigned char framerate_byte = buffer[pos + 3];
int framerate_code = framerate_byte >> 4;
// Skip to find cc_data section (0x72)
size_t cdp_pos = pos + 4; // After identifier, length, framerate
int cc_count = 0;
unsigned char *cc_data = NULL;
// Skip header sequence counter (2 bytes)
cdp_pos += 2;
// Look for cc_data section (0x72) within CDP
while (cdp_pos < pos + cdp_length - 4)
{
if (buffer[cdp_pos] == 0x72) // cc_data section
{
cc_count = buffer[cdp_pos + 1] & 0x1F;
cc_data = buffer + cdp_pos + 2;
break;
}
else if (buffer[cdp_pos] == 0x71) // time code section
{
cdp_pos += 5; // Skip time code section
}
else if (buffer[cdp_pos] == 0x73) // service info section
{
break; // Past cc_data
}
else if (buffer[cdp_pos] == 0x74) // footer
{
break;
}
else
{
cdp_pos++;
}
}
if (cc_count > 0 && cc_data != NULL)
{
// Calculate PTS based on CDP frame count and frame rate
static const int fps_table[] = {0, 24, 24, 25, 30, 30, 50, 60, 60};
int fps = (framerate_code < 9) ? fps_table[framerate_code] : 30;
LLONG pts = (LLONG)cdp_count * 90000 / fps;
// Set timing if not already set
if (dec_ctx->timing->pts_set == 0)
{
dec_ctx->timing->min_pts = pts;
dec_ctx->timing->pts_set = 2;
dec_ctx->timing->sync_pts = pts;
}
set_current_pts(dec_ctx->timing, pts);
set_fts(dec_ctx->timing);
#ifndef DISABLE_RUST
// Enable DTVCC decoder for CEA-708 captions
if (dec_ctx->dtvcc_rust)
{
int is_active = ccxr_dtvcc_is_active(dec_ctx->dtvcc_rust);
if (!is_active)
{
ccxr_dtvcc_set_active(dec_ctx->dtvcc_rust, 1);
}
}
#endif
// Process cc_data triplets through process_cc_data for 708 support
process_cc_data(enc_ctx, dec_ctx, cc_data, cc_count, sub);
cdp_count++;
}
pos += cdp_length;
}
return pos;
}
int raw_loop(struct lib_ccx_ctx *ctx)
{
LLONG ret;
@@ -675,7 +576,6 @@ int raw_loop(struct lib_ccx_ctx *ctx)
int caps = 0;
int is_dvdraw = 0; // Flag to track if this is DVD raw format
int is_scc = 0; // Flag to track if this is SCC format
int is_cdp = 0; // Flag to track if this is raw CDP format
int is_mcc_output = 0; // Flag for MCC output format
dec_ctx = update_decoder_list(ctx);
@@ -721,15 +621,7 @@ int raw_loop(struct lib_ccx_ctx *ctx)
mprint("Detected SCC (Scenarist Closed Caption) format\n");
}
// Check if this is raw CDP format (starts with 0x9669)
if (!is_cdp && !is_scc && !is_dvdraw && data->len >= 2 &&
data->buffer[0] == 0x96 && data->buffer[1] == 0x69)
{
is_cdp = 1;
mprint("Detected raw CDP (Caption Distribution Packet) format\n");
}
if (is_mcc_output && !is_dvdraw && !is_scc && !is_cdp)
if (is_mcc_output && !is_dvdraw && !is_scc)
{
// For MCC output, encode raw data directly without decoding
// This preserves the original CEA-608 byte pairs in CDP format
@@ -747,13 +639,6 @@ int raw_loop(struct lib_ccx_ctx *ctx)
// Use Rust SCC implementation - handles timing internally via SMPTE timecodes
ret = ccxr_process_scc(dec_ctx, dec_sub, data->buffer, (unsigned int)data->len, ccx_options.scc_framerate);
}
else if (is_cdp)
{
// Process raw CDP packets (e.g., from SDI VANC capture)
ret = process_raw_cdp(enc_ctx, dec_ctx, dec_sub, data->buffer, data->len);
if (ret > 0)
caps = 1;
}
else
{
ret = process_raw(dec_ctx, dec_sub, data->buffer, data->len);
@@ -976,34 +861,7 @@ int process_data(struct encoder_ctx *enc_ctx, struct lib_cc_decode *dec_ctx, str
}
else if (data_node->bufferdatatype == CCX_RAW_TYPE)
{
// CCX_RAW_TYPE contains cc_data triplets (cc_type + 2 data bytes each)
// Used by MXF and GXF demuxers
// Initialize timing if not set (use caption PTS as reference)
if (dec_ctx->timing->pts_set == 0 && data_node->pts != CCX_NOPTS)
{
dec_ctx->timing->min_pts = data_node->pts;
dec_ctx->timing->pts_set = 2; // MinPtsSet
dec_ctx->timing->sync_pts = data_node->pts;
set_fts(dec_ctx->timing);
}
#ifndef DISABLE_RUST
// Enable DTVCC decoder for CEA-708 captions from MXF/GXF
if (dec_ctx->dtvcc_rust)
{
int is_active = ccxr_dtvcc_is_active(dec_ctx->dtvcc_rust);
if (!is_active)
{
ccxr_dtvcc_set_active(dec_ctx->dtvcc_rust, 1);
}
}
#endif
// Use process_cc_data to properly invoke DTVCC decoder for 708 captions
int cc_count = data_node->len / 3;
process_cc_data(enc_ctx, dec_ctx, data_node->buffer, cc_count, dec_sub);
got = data_node->len;
got = process_raw_with_field(dec_ctx, dec_sub, data_node->buffer, data_node->len);
}
else if (data_node->bufferdatatype == CCX_ISDB_SUBTITLE)
{
@@ -1508,24 +1366,7 @@ int general_loop(struct lib_ccx_ctx *ctx)
}
if (ctx->live_stream)
{
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 cur_sec = (int)(get_fts(dec_ctx->timing, dec_ctx->current_field) / 1000);
int th = cur_sec / 10;
if (ctx->last_reported_progress != th)
{
@@ -1543,28 +1384,6 @@ 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;

View File

@@ -1,7 +1,7 @@
#ifndef CCX_CCEXTRACTOR_H
#define CCX_CCEXTRACTOR_H
#define VERSION "0.96.5"
#define VERSION "0.96.3"
// Load common includes and constants for library usage
#include "ccx_common_platform.h"
@@ -90,7 +90,6 @@ 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;
@@ -161,7 +160,6 @@ struct lib_ccx_ctx *init_libraries(struct ccx_s_options *opt);
void dinit_libraries(struct lib_ccx_ctx **ctx);
extern void ccxr_init_basic_logger();
extern void ccxr_update_logger_target();
// ccextractor.c
void print_end_msg(void);

View File

@@ -122,8 +122,6 @@ void parse_ebml(FILE *file)
{
code <<= 8;
code += mkv_read_byte(file);
if (feof(file))
break;
code_len++;
switch (code)
@@ -188,8 +186,6 @@ void parse_segment_info(FILE *file)
{
code <<= 8;
code += mkv_read_byte(file);
if (feof(file))
break;
code_len++;
switch (code)
@@ -488,8 +484,6 @@ 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)
@@ -618,8 +612,6 @@ void parse_segment_cluster(struct matroska_ctx *mkv_ctx)
{
code <<= 8;
code += mkv_read_byte(file);
if (feof(file))
break;
code_len++;
switch (code)
@@ -742,24 +734,14 @@ int process_avc_frame_mkv(struct matroska_ctx *mkv_ctx, struct matroska_avc_fram
{
uint32_t nal_length;
if (i + nal_unit_size > frame.len)
break;
nal_length =
((uint32_t)frame.data[i] << 24) |
((uint32_t)frame.data[i + 1] << 16) |
((uint32_t)frame.data[i + 2] << 8) |
(uint32_t)frame.data[i + 3];
nal_length = bswap32(*(long *)&frame.data[i]);
i += nal_unit_size;
if (nal_length > frame.len - i)
break;
if (nal_length > 0)
do_NAL(enc_ctx, dec_ctx, (unsigned char *)&frame.data[i], nal_length, &mkv_ctx->dec_sub);
do_NAL(enc_ctx, dec_ctx, (unsigned char *)&(frame.data[i]), nal_length, &mkv_ctx->dec_sub);
i += nal_length;
} // outer for
assert(i == frame.len);
mkv_ctx->current_second = (int)(get_fts(dec_ctx->timing, dec_ctx->current_field) / 1000);
@@ -787,22 +769,11 @@ int process_hevc_frame_mkv(struct matroska_ctx *mkv_ctx, struct matroska_avc_fra
{
uint32_t nal_length;
if (i + nal_unit_size > frame.len)
break;
nal_length =
((uint32_t)frame.data[i] << 24) |
((uint32_t)frame.data[i + 1] << 16) |
((uint32_t)frame.data[i + 2] << 8) |
(uint32_t)frame.data[i + 3];
nal_length = bswap32(*(long *)&frame.data[i]);
i += nal_unit_size;
if (nal_length > frame.len - i)
break;
if (nal_length > 0)
do_NAL(enc_ctx, dec_ctx, (unsigned char *)&frame.data[i], nal_length, &mkv_ctx->dec_sub);
do_NAL(enc_ctx, dec_ctx, (unsigned char *)&(frame.data[i]), nal_length, &mkv_ctx->dec_sub);
i += nal_length;
}
@@ -874,8 +845,6 @@ 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)
@@ -1228,8 +1197,6 @@ void parse_segment_tracks(struct matroska_ctx *mkv_ctx)
{
code <<= 8;
code += mkv_read_byte(file);
if (feof(file))
break;
code_len++;
switch (code)
@@ -1274,8 +1241,6 @@ void parse_segment(struct matroska_ctx *mkv_ctx)
{
code <<= 8;
code += mkv_read_byte(file);
if (feof(file))
break;
code_len++;
switch (code)
{
@@ -1950,9 +1915,6 @@ 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)

View File

@@ -899,11 +899,6 @@ int processmp4(struct lib_ccx_ctx *ctx, struct ccx_s_mp4Cfg *cfg, char *file)
#endif
memset(&dec_sub, 0, sizeof(dec_sub));
if (file == NULL)
{
mprint("Error: NULL file path provided to processmp4\n");
return -1;
}
mprint("Opening \'%s\': ", file);
#ifdef MP4_DEBUG
gf_log_set_tool_level(GF_LOG_CONTAINER, GF_LOG_DEBUG);

View File

@@ -14,19 +14,7 @@ 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)

View File

@@ -434,21 +434,10 @@ void remap_g0_charset(uint8_t c)
{
if (c != primary_charset.current)
{
if (c >= 56)
{
fprintf(stderr, "- G0 Latin National Subset ID 0x%1x.%1x is out of bounds\n", (c >> 3), (c & 0x7));
return;
}
uint8_t m = G0_LATIN_NATIONAL_SUBSETS_MAP[c];
if (m == 0xff)
{
fprintf(stderr, "- G0 Latin National Subset ID 0x%1x.%1x is not implemented\n", (c >> 3), (c & 0x7));
return;
}
else if (m >= 14)
{
fprintf(stderr, "- G0 Latin National Subset index %d is out of bounds\n", m);
return;
}
else
{
@@ -1403,7 +1392,7 @@ int tlt_process_pes_packet(struct lib_cc_decode *dec_ctx, uint8_t *buffer, uint1
uint8_t pes_ext_flag;
// extension
uint32_t t = 0;
uint32_t i;
uint16_t i;
struct TeletextCtx *ctx = dec_ctx->private_data;
ctx->sentence_cap = sentence_cap;
@@ -1479,9 +1468,6 @@ int tlt_process_pes_packet(struct lib_cc_decode *dec_ctx, uint8_t *buffer, uint1
if (pes_packet_length > size)
pes_packet_length = size;
if (size < 9)
return CCX_OK;
// optional PES header marker bits (10.. ....)
if ((buffer[6] & 0xc0) == 0x80)
{
@@ -1494,16 +1480,8 @@ int tlt_process_pes_packet(struct lib_cc_decode *dec_ctx, uint8_t *buffer, uint1
{
if ((optional_pes_header_included == YES) && ((buffer[7] & 0x80) > 0))
{
if (size < 14)
{
ctx->using_pts = NO;
dbg_print(CCX_DMT_TELETEXT, "- PID 0xbd PTS signaled but packet too short, using TS PCR\n");
}
else
{
ctx->using_pts = YES;
dbg_print(CCX_DMT_TELETEXT, "- PID 0xbd PTS available\n");
}
ctx->using_pts = YES;
dbg_print(CCX_DMT_TELETEXT, "- PID 0xbd PTS available\n");
}
else
{
@@ -1576,17 +1554,11 @@ int tlt_process_pes_packet(struct lib_cc_decode *dec_ctx, uint8_t *buffer, uint1
if (optional_pes_header_included == YES)
i += 3 + optional_pes_header_length;
while (i + 2 <= pes_packet_length)
while (i <= pes_packet_length - 6)
{
uint8_t data_unit_id = buffer[i++];
uint8_t data_unit_len = buffer[i++];
if (i + data_unit_len > pes_packet_length)
{
dbg_print(CCX_DMT_TELETEXT, "- Teletext data unit length %u exceeds PES packet length, stopping.\n", data_unit_len);
break;
}
if ((data_unit_id == DATA_UNIT_EBU_TELETEXT_NONSUBTITLE) || (data_unit_id == DATA_UNIT_EBU_TELETEXT_SUBTITLE))
{
// teletext payload has always size 44 bytes

View File

@@ -6,7 +6,6 @@
#include "dvb_subtitle_decoder.h"
#include "ccx_decoders_isdb.h"
#include "file_buffer.h"
#include <inttypes.h>
#ifdef DEBUG_SAVE_TS_PACKETS
#include <sys/types.h>
@@ -569,13 +568,6 @@ int copy_capbuf_demux_data(struct ccx_demuxer *ctx, struct demuxer_data **data,
if (cinfo->codec == CCX_CODEC_TELETEXT)
{
if (cinfo->capbuflen > BUFSIZE - ptr->len)
{
fatal(CCX_COMMON_EXIT_BUG_BUG,
"Teletext packet (%" PRId64 ") larger than remaining buffer (%" PRId64 ").\n",
cinfo->capbuflen, (int64_t)(BUFSIZE - ptr->len));
}
memcpy(ptr->buffer + ptr->len, cinfo->capbuf, cinfo->capbuflen);
ptr->len += cinfo->capbuflen;
return CCX_OK;
@@ -670,6 +662,7 @@ void cinfo_cremation(struct ccx_demuxer *ctx, struct demuxer_data **data)
int copy_payload_to_capbuf(struct cap_info *cinfo, struct ts_payload *payload)
{
int newcapbuflen;
if (cinfo->ignore == CCX_TRUE &&
((cinfo->stream != CCX_STREAM_TYPE_VIDEO_MPEG2 &&
@@ -695,22 +688,17 @@ int copy_payload_to_capbuf(struct cap_info *cinfo, struct ts_payload *payload)
}
// copy payload to capbuf
if (payload->length > INT64_MAX - cinfo->capbuflen)
newcapbuflen = cinfo->capbuflen + payload->length;
if (newcapbuflen > cinfo->capbufsize)
{
mprint("Error: capbuf size overflow\n");
return -1;
}
int64_t newcapbuflen = (int64_t)cinfo->capbuflen + payload->length;
if (newcapbuflen > (int64_t)cinfo->capbufsize)
{
unsigned char *new_capbuf = (unsigned char *)realloc(cinfo->capbuf, (size_t)newcapbuflen);
unsigned char *new_capbuf = (unsigned char *)realloc(cinfo->capbuf, newcapbuflen);
if (!new_capbuf)
return -1;
cinfo->capbuf = new_capbuf;
cinfo->capbufsize = newcapbuflen; // Note: capbufsize is int in struct cap_info
cinfo->capbufsize = newcapbuflen;
}
memcpy(cinfo->capbuf + cinfo->capbuflen, payload->start, payload->length);
cinfo->capbuflen = newcapbuflen; // Note: capbuflen is int in struct cap_info
cinfo->capbuflen = newcapbuflen;
return CCX_OK;
}

View File

@@ -50,8 +50,8 @@ struct EPG_rating
struct EPG_event
{
uint32_t id;
char start_time_string[74]; // "YYYYMMDDHHMMSS +0000" = 20 chars, 74 to silence compiler warning
char end_time_string[74];
char start_time_string[21]; //"YYYYMMDDHHMMSS +0000" = 20 chars
char end_time_string[21];
uint8_t running_status;
uint8_t free_ca_mode;
char ISO_639_language_code[4];

View File

@@ -411,18 +411,9 @@ int parse_PMT(struct ccx_demuxer *ctx, unsigned char *buf, int len, struct progr
{
// if this any generally used video stream tyoe get clashed with ATSC/SCTE standard
// then this code can go in some atsc flag
// Validate ES_info_length against buffer bounds to prevent heap overflow
if (i + 5 + ES_info_length > len)
break;
unsigned char *es_info = buf + i + 5;
unsigned char *es_info_end = buf + i + 5 + ES_info_length;
for (desc_len = 0; es_info_end > es_info; es_info += desc_len)
for (desc_len = 0; (buf + i + 5 + ES_info_length) > es_info; es_info += desc_len)
{
// Need at least 2 bytes for descriptor_tag and desc_len
if (es_info + 2 > es_info_end)
break;
enum ccx_mpeg_descriptor descriptor_tag = (enum ccx_mpeg_descriptor)(*es_info++);
int nb_service;
int is_608;
@@ -446,18 +437,9 @@ int parse_PMT(struct ccx_demuxer *ctx, unsigned char *buf, int len, struct progr
if (IS_FEASIBLE(ctx->codec, ctx->nocodec, CCX_CODEC_TELETEXT) && ES_info_length && stream_type == CCX_STREAM_TYPE_PRIVATE_MPEG2) // MPEG-2 Packetized Elementary Stream packets containing private data
{
// Validate ES_info_length against buffer bounds
if (i + 5 + ES_info_length > len)
continue;
unsigned char *es_info = buf + i + 5;
unsigned char *es_info_end = buf + i + 5 + ES_info_length;
for (desc_len = 0; es_info_end > es_info; es_info += desc_len)
for (desc_len = 0; (buf + i + 5 + ES_info_length) - es_info; es_info += desc_len)
{
// Need at least 2 bytes for descriptor_tag and desc_len
if (es_info + 2 > es_info_end)
break;
enum ccx_mpeg_descriptor descriptor_tag = (enum ccx_mpeg_descriptor)(*es_info++);
desc_len = (*es_info++);
if (!IS_VALID_TELETEXT_DESC(descriptor_tag))
@@ -592,15 +574,6 @@ void ts_buffer_psi_packet(struct ccx_demuxer *ctx)
else if (ccounter == ctx->PID_buffers[pid]->prev_ccounter + 1 || (ctx->PID_buffers[pid]->prev_ccounter == 0x0f && ccounter == 0))
{
ctx->PID_buffers[pid]->prev_ccounter = ccounter;
// Check for integer overflow and reasonable size limit (1MB)
if (ctx->PID_buffers[pid]->buffer_length > 1024 * 1024 ||
payload_length > 1024 * 1024 ||
ctx->PID_buffers[pid]->buffer_length + payload_length > 1024 * 1024)
{
dbg_print(CCX_DMT_GENERIC_NOTICES, "\rWarning: PSI buffer for PID %u exceeded reasonable limit (1MB), discarding.\n", pid);
return;
}
void *tmp = realloc(ctx->PID_buffers[pid]->buffer, ctx->PID_buffers[pid]->buffer_length + payload_length);
if (tmp == NULL)
{
@@ -639,10 +612,6 @@ int parse_PAT(struct ccx_demuxer *ctx)
payload_start = ctx->PID_buffers[0]->buffer + pointer_field + 1;
payload_length = ctx->PID_buffers[0]->buffer_length - (pointer_field + 1);
// Need at least 8 bytes to read header fields
if (payload_length < 8)
return 0;
section_number = payload_start[6];
last_section_number = payload_start[7];

View File

@@ -125,7 +125,7 @@ void EPG_ATSC_calc_time(char *output, uint32_t time)
timeinfo.tm_hour = 0;
timeinfo.tm_isdst = -1;
mktime(&timeinfo);
snprintf(output, 74, "%02d%02d%02d%02d%02d%02d +0000", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
snprintf(output, 21, "%02d%02d%02d%02d%02d%02d +0000", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
}
// Fills event.start_time_string in XMLTV format with passed DVB time

View File

@@ -179,21 +179,16 @@ void mprint(const char *fmt, ...)
if (!ccx_options.messages_target)
return;
va_start(args, fmt);
FILE *target = (ccx_options.messages_target == CCX_MESSAGES_STDOUT) ? stdout : stderr;
if (fmt[0] == '\r')
if (ccx_options.messages_target == CCX_MESSAGES_STDOUT)
{
#ifndef _WIN32
fprintf(target, "\r\033[K"); // Clear the line first
fmt++; // Skip the '\r' so only the clean text gets printed next
#endif
vfprintf(stdout, fmt, args);
fflush(stdout);
}
else
{
vfprintf(stderr, fmt, args);
fflush(stderr);
}
// Windows (legacy console) does not support ANSI sequences; fallback to standard \r; and vfprintf below handles it the old-fashioned way.
vfprintf(target, fmt, args);
fflush(target);
va_end(args);
}

66
src/rust/Cargo.lock generated
View File

@@ -129,26 +129,6 @@ dependencies = [
"syn 2.0.111",
]
[[package]]
name = "bindgen"
version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags 2.10.0",
"cexpr",
"clang-sys",
"itertools",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash 2.1.1",
"shlex",
"syn 2.0.111",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -161,12 +141,6 @@ version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "by_address"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
[[package]]
name = "camino"
version = "1.2.1"
@@ -177,7 +151,7 @@ checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609"
name = "ccx_rust"
version = "0.1.0"
dependencies = [
"bindgen 0.72.1",
"bindgen 0.64.0",
"cfg-if",
"clap",
"encoding_rs",
@@ -361,18 +335,21 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "fast-srgb8"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "find-crate"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2"
dependencies = [
"toml",
]
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@@ -822,26 +799,26 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "palette"
version = "0.7.6"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6"
checksum = "8f9cd68f7112581033f157e56c77ac4a5538ec5836a2e39284e65bd7d7275e49"
dependencies = [
"approx",
"fast-srgb8",
"num-traits",
"palette_derive",
"phf",
]
[[package]]
name = "palette_derive"
version = "0.7.6"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30"
checksum = "05eedf46a8e7c27f74af0c9cfcdb004ceca158cb1b918c6f68f8d7a549b3e427"
dependencies = [
"by_address",
"find-crate",
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 1.0.109",
]
[[package]]
@@ -1439,6 +1416,15 @@ dependencies = [
"zerovec",
]
[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]]
name = "toml_datetime"
version = "0.7.3"

View File

@@ -13,7 +13,7 @@ crate-type = ["staticlib"]
[dependencies]
log = "0.4.26"
env_logger = "0.8.4"
palette = "0.7"
palette = "0.6.1"
tesseract-sys = { version = "0.5.15", optional = true, default-features = false }
leptonica-sys = { version = "= 0.4.6", optional = true, default-features = false }
clap = { version = "4.5.31", features = ["derive"] }

View File

@@ -84,12 +84,7 @@ fn main() {
{
builder = builder.clang_arg("-DENABLE_HARDSUBX");
// Check FFMPEG_INCLUDE_DIR environment variable (works on all platforms)
if let Ok(ffmpeg_include) = env::var("FFMPEG_INCLUDE_DIR") {
builder = builder.clang_arg(format!("-I{}", ffmpeg_include));
}
// Add FFmpeg include paths for Mac (Homebrew)
// Add FFmpeg include paths for Mac
if cfg!(target_os = "macos") {
// Try common Homebrew paths
if std::path::Path::new("/opt/homebrew/include").exists() {
@@ -103,23 +98,22 @@ fn main() {
if std::path::Path::new(cellar_ffmpeg).exists() {
// Find the FFmpeg version directory
if let Ok(entries) = std::fs::read_dir(cellar_ffmpeg) {
for entry in entries.flatten() {
let include_path = entry.path().join("include");
if include_path.exists() {
builder = builder.clang_arg(format!("-I{}", include_path.display()));
break;
for entry in entries {
if let Ok(entry) = entry {
let include_path = entry.path().join("include");
if include_path.exists() {
builder =
builder.clang_arg(format!("-I{}", include_path.display()));
break;
}
}
}
}
}
}
// On Linux, try pkg-config to find FFmpeg include paths
if cfg!(target_os = "linux") {
if let Ok(lib) = pkg_config::Config::new().probe("libavcodec") {
for path in lib.include_paths {
builder = builder.clang_arg(format!("-I{}", path.display()));
}
// Also check environment variable
if let Ok(ffmpeg_include) = env::var("FFMPEG_INCLUDE_DIR") {
builder = builder.clang_arg(format!("-I{}", ffmpeg_include));
}
}
}

View File

@@ -147,11 +147,7 @@ pub const CCX_DECODER_608_SCREEN_WIDTH: usize = 32;
pub const ONEPASS: usize = 120; // Bytes we can always look ahead without going out of limits
pub const BUFSIZE: usize = 2048 * 1024 + ONEPASS; // 2 Mb plus the safety pass
pub const MAX_CLOSED_CAPTION_DATA_PER_PICTURE: usize = 32;
/// CEA-708 Service Input Buffer size.
/// Specification minimum is 128 bytes per service, but we use 2048 bytes
/// (16x the minimum) to provide a safety margin for buffer management.
/// Reference: CEA-708-E Section 8.4.3 - Service Input Buffers
pub const EIA_708_BUFFER_LENGTH: usize = 2048;
pub const EIA_708_BUFFER_LENGTH: usize = 2048; // TODO: Find out what the real limit is
pub const TS_PACKET_PAYLOAD_LENGTH: usize = 184; // From specs
pub const SUBLINESIZE: usize = 2048; // Max. length of a .srt line - TODO: Get rid of this
pub const STARTBYTESLENGTH: usize = 1024 * 1024;

View File

@@ -1,385 +0,0 @@
//! MKV language filtering support.
//!
//! Matroska files support two language code formats:
//! - ISO 639-2 (3-letter bibliographic codes): "eng", "fre", "chi"
//! - BCP 47 / IETF language tags: "en-US", "fr-CA", "zh-Hans"
//!
//! This module provides [`MkvLangFilter`] for parsing and matching language codes.
use std::fmt;
use std::str::FromStr;
/// A filter for matching MKV track languages.
///
/// Supports comma-separated lists of language codes in either:
/// - ISO 639-2 format (3-letter codes like "eng", "fre")
/// - BCP 47 format (tags like "en-US", "fr-CA", "zh-Hans")
///
/// # Examples
///
/// ```
/// use lib_ccxr::common::MkvLangFilter;
///
/// // Single language
/// let filter: MkvLangFilter = "eng".parse().unwrap();
/// assert!(filter.matches("eng", None));
///
/// // Multiple languages
/// let filter: MkvLangFilter = "eng,fre,chi".parse().unwrap();
/// assert!(filter.matches("fre", None));
///
/// // BCP 47 matching
/// let filter: MkvLangFilter = "en-US,fr-CA".parse().unwrap();
/// assert!(filter.matches("eng", Some("en-US")));
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MkvLangFilter {
/// The original input string (used for C FFI)
raw: String,
/// Parsed and validated language codes
codes: Vec<LanguageCode>,
}
/// A single language code, either ISO 639-2 or BCP 47.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LanguageCode {
/// The normalized (lowercase) code
code: String,
}
/// Error type for invalid language codes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InvalidLanguageCode {
/// The invalid code
pub code: String,
/// Description of what's wrong
pub reason: &'static str,
}
impl fmt::Display for InvalidLanguageCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid language code '{}': {}", self.code, self.reason)
}
}
impl std::error::Error for InvalidLanguageCode {}
impl LanguageCode {
/// Validates and creates a new language code.
///
/// Accepts:
/// - ISO 639-2 codes: 3 ASCII letters (e.g., "eng", "fre")
/// - BCP 47 tags: primary language with optional subtags separated by hyphens
/// (e.g., "en-US", "fr-CA", "zh-Hans-CN")
///
/// # BCP 47 Structure
/// - Primary language: 2-3 letters
/// - Script (optional): 4 letters (e.g., "Hans", "Latn")
/// - Region (optional): 2 letters or 3 digits (e.g., "US", "419")
/// - Variant (optional): 5-8 alphanumeric characters
pub fn new(code: &str) -> Result<Self, InvalidLanguageCode> {
let code = code.trim();
if code.is_empty() {
return Err(InvalidLanguageCode {
code: code.to_string(),
reason: "empty language code",
});
}
// Check for valid characters (alphanumeric and hyphens only)
if !code.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
return Err(InvalidLanguageCode {
code: code.to_string(),
reason: "must contain only ASCII letters, digits, and hyphens",
});
}
// Cannot start or end with hyphen
if code.starts_with('-') || code.ends_with('-') {
return Err(InvalidLanguageCode {
code: code.to_string(),
reason: "cannot start or end with hyphen",
});
}
// Cannot have consecutive hyphens
if code.contains("--") {
return Err(InvalidLanguageCode {
code: code.to_string(),
reason: "cannot have consecutive hyphens",
});
}
// Validate subtag structure
let subtags: Vec<&str> = code.split('-').collect();
// First subtag must be the primary language (2-3 letters)
let primary = subtags[0];
if primary.len() < 2 || primary.len() > 3 {
return Err(InvalidLanguageCode {
code: code.to_string(),
reason: "primary language subtag must be 2-3 letters",
});
}
if !primary.chars().all(|c| c.is_ascii_alphabetic()) {
return Err(InvalidLanguageCode {
code: code.to_string(),
reason: "primary language subtag must contain only letters",
});
}
// Validate subsequent subtags
for subtag in subtags.iter().skip(1) {
if subtag.is_empty() {
return Err(InvalidLanguageCode {
code: code.to_string(),
reason: "empty subtag",
});
}
let len = subtag.len();
let all_alpha = subtag.chars().all(|c| c.is_ascii_alphabetic());
let all_digit = subtag.chars().all(|c| c.is_ascii_digit());
let all_alnum = subtag.chars().all(|c| c.is_ascii_alphanumeric());
// Valid subtag types:
// - Script: 4 letters (e.g., "Hans")
// - Region: 2 letters or 3 digits (e.g., "US", "419")
// - Variant: 5-8 alphanumeric, or 4 starting with digit
// - Extension: single letter followed by more subtags
// - Private use: 'x' followed by 1-8 char subtags
let valid = match len {
1 => subtag.chars().all(|c| c.is_ascii_alphanumeric()), // Extension singleton
2 => all_alpha, // Region (2 letters)
3 => all_alpha || all_digit, // 3 letters or 3 digits
4 => all_alpha || (subtag.chars().next().unwrap().is_ascii_digit() && all_alnum), // Script or variant starting with digit
5..=8 => all_alnum, // Variant
_ => false,
};
if !valid {
return Err(InvalidLanguageCode {
code: code.to_string(),
reason: "invalid subtag format",
});
}
}
Ok(Self {
code: code.to_lowercase(),
})
}
/// Returns the normalized (lowercase) code.
pub fn as_str(&self) -> &str {
&self.code
}
/// Checks if this code matches a track's language.
///
/// Matching rules:
/// 1. Exact match (case-insensitive)
/// 2. Prefix match for BCP 47 (e.g., "en" matches "en-US")
pub fn matches(&self, iso639: &str, bcp47: Option<&str>) -> bool {
let iso639_lower = iso639.to_lowercase();
let bcp47_lower = bcp47.map(|s| s.to_lowercase());
// Exact match on ISO 639-2
if self.code == iso639_lower {
return true;
}
// Exact match on BCP 47
if let Some(ref bcp) = bcp47_lower {
if self.code == *bcp {
return true;
}
}
// Prefix match: "en" matches "en-US", "eng" matches track with bcp47 "en-US"
// The filter code could be a prefix of the track's BCP 47 tag
if let Some(ref bcp) = bcp47_lower {
if bcp.starts_with(&self.code) && bcp[self.code.len()..].starts_with('-') {
return true;
}
// Or the track's BCP 47 could be a prefix of the filter
if self.code.starts_with(bcp.as_str()) && self.code[bcp.len()..].starts_with('-') {
return true;
}
}
false
}
}
impl FromStr for LanguageCode {
type Err = InvalidLanguageCode;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
impl fmt::Display for LanguageCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.code)
}
}
impl MkvLangFilter {
/// Creates a new filter from a comma-separated list of language codes.
pub fn new(input: &str) -> Result<Self, InvalidLanguageCode> {
let input = input.trim();
if input.is_empty() {
return Err(InvalidLanguageCode {
code: String::new(),
reason: "empty language filter",
});
}
let codes: Result<Vec<LanguageCode>, _> = input.split(',').map(LanguageCode::new).collect();
Ok(Self {
raw: input.to_string(),
codes: codes?,
})
}
/// Returns the raw input string (for C FFI compatibility).
pub fn as_raw_str(&self) -> &str {
&self.raw
}
/// Returns the parsed language codes.
pub fn codes(&self) -> &[LanguageCode] {
&self.codes
}
/// Checks if any of the filter's codes match a track's language.
///
/// # Arguments
/// - `iso639`: The track's ISO 639-2 language code (e.g., "eng")
/// - `bcp47`: The track's BCP 47 language tag, if available (e.g., "en-US")
pub fn matches(&self, iso639: &str, bcp47: Option<&str>) -> bool {
self.codes.iter().any(|code| code.matches(iso639, bcp47))
}
}
impl FromStr for MkvLangFilter {
type Err = InvalidLanguageCode;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
impl fmt::Display for MkvLangFilter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.raw)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_iso639_codes() {
// Valid 3-letter codes
assert!(LanguageCode::new("eng").is_ok());
assert!(LanguageCode::new("fre").is_ok());
assert!(LanguageCode::new("chi").is_ok());
assert!(LanguageCode::new("ENG").is_ok()); // Case insensitive
// 2-letter codes (ISO 639-1 style, valid in BCP 47)
assert!(LanguageCode::new("en").is_ok());
assert!(LanguageCode::new("fr").is_ok());
}
#[test]
fn test_bcp47_codes() {
// Language + region
assert!(LanguageCode::new("en-US").is_ok());
assert!(LanguageCode::new("fr-CA").is_ok());
assert!(LanguageCode::new("pt-BR").is_ok());
// Language + script
assert!(LanguageCode::new("zh-Hans").is_ok());
assert!(LanguageCode::new("zh-Hant").is_ok());
assert!(LanguageCode::new("sr-Latn").is_ok());
// Language + script + region
assert!(LanguageCode::new("zh-Hans-CN").is_ok());
assert!(LanguageCode::new("zh-Hant-TW").is_ok());
// UN M.49 numeric region codes
assert!(LanguageCode::new("es-419").is_ok()); // Latin America
}
#[test]
fn test_invalid_codes() {
// Too short
assert!(LanguageCode::new("a").is_err());
// Invalid characters
assert!(LanguageCode::new("en_US").is_err()); // Underscore not allowed
assert!(LanguageCode::new("en US").is_err()); // Space not allowed
assert!(LanguageCode::new("ça").is_err()); // Non-ASCII
// Invalid structure
assert!(LanguageCode::new("-en").is_err()); // Leading hyphen
assert!(LanguageCode::new("en-").is_err()); // Trailing hyphen
assert!(LanguageCode::new("en--US").is_err()); // Double hyphen
// Empty
assert!(LanguageCode::new("").is_err());
}
#[test]
fn test_filter_multiple_codes() {
let filter = MkvLangFilter::new("eng,fre,chi").unwrap();
assert_eq!(filter.codes().len(), 3);
assert!(filter.matches("eng", None));
assert!(filter.matches("fre", None));
assert!(filter.matches("chi", None));
assert!(!filter.matches("spa", None));
}
#[test]
fn test_filter_bcp47_matching() {
let filter = MkvLangFilter::new("en-US,fr-CA").unwrap();
// Exact BCP 47 match
assert!(filter.matches("eng", Some("en-US")));
assert!(filter.matches("fre", Some("fr-CA")));
// No match
assert!(!filter.matches("eng", Some("en-GB")));
assert!(!filter.matches("eng", None));
}
#[test]
fn test_filter_mixed_formats() {
let filter = MkvLangFilter::new("eng,fr-CA,zh-Hans").unwrap();
assert!(filter.matches("eng", None));
assert!(filter.matches("fre", Some("fr-CA")));
assert!(filter.matches("chi", Some("zh-Hans")));
}
#[test]
fn test_case_insensitivity() {
let filter = MkvLangFilter::new("ENG,FR-CA").unwrap();
assert!(filter.matches("eng", None));
assert!(filter.matches("ENG", None));
assert!(filter.matches("fre", Some("fr-ca")));
assert!(filter.matches("FRE", Some("FR-CA")));
}
#[test]
fn test_raw_string_preserved() {
let filter = MkvLangFilter::new("eng,fre").unwrap();
assert_eq!(filter.as_raw_str(), "eng,fre");
}
}

View File

@@ -18,10 +18,8 @@
mod bitstream;
mod constants;
mod mkv_lang;
mod options;
pub use bitstream::*;
pub use constants::*;
pub use mkv_lang::*;
pub use options::*;

View File

@@ -466,9 +466,8 @@ pub struct Options {
pub ocr_line_split: bool,
/// If true, use character blacklist to prevent common OCR errors (e.g. | vs I)
pub ocr_blacklist: bool,
/// Language filter for MKV subtitle tracks.
/// Accepts comma-separated ISO 639-2 codes (e.g., "eng,fre") or BCP 47 tags (e.g., "en-US,fr-CA").
pub mkvlang: Option<super::MkvLangFilter>,
/// The name of the language stream for MKV
pub mkvlang: Option<Language>,
/// If true, the video stream will be processed even if we're using a different one for subtitles.
pub analyze_video_stream: bool,
@@ -524,8 +523,6 @@ pub struct Options {
pub segment_on_key_frames_only: bool,
/// SCC input framerate: 0=29.97 (default), 1=24, 2=25, 3=30
pub scc_framerate: i32,
/// SCC accurate timing (issue #1120): if true, use bandwidth-aware timing for broadcast compliance
pub scc_accurate_timing: bool,
pub debug_mask: DebugMessageMask,
#[cfg(feature = "with_libcurl")]
@@ -629,8 +626,7 @@ impl Default for Options {
multiprogram: Default::default(),
out_interval: -1,
segment_on_key_frames_only: Default::default(),
scc_framerate: 0, // 0 = 29.97fps (default)
scc_accurate_timing: false, // Off by default for backwards compatibility (issue #1120)
scc_framerate: 0, // 0 = 29.97fps (default)
debug_mask: DebugMessageMask::new(
DebugMessageFlag::GENERIC_NOTICE,
DebugMessageFlag::VERBOSE,

View File

@@ -82,6 +82,7 @@ impl<'a> SendTarget<'a> {
"Unable to connect, address passed is null\n"
);
}
info!("Target address: {}\n", config.target_addr); // TODO remove this
info!("Target port: {}\n", config.port.unwrap_or(DEFAULT_TCP_PORT));
let tcp_stream = TcpStream::connect((
config.target_addr,

View File

@@ -1154,9 +1154,10 @@ impl<'a> TeletextContext<'a> {
}
if v >= 0x20 {
let u = char::from_u32(v as u32).unwrap_or(char::REPLACEMENT_CHARACTER);
let u = char::from_u32(v as u32).unwrap();
self.page_buffer_cur.get_or_insert("".into()).push(u);
if logger().expect("could not access logger").is_gui_mode() {
// For now we just handle the easy stuff
eprint!("{u}");
}
}
@@ -1224,15 +1225,13 @@ impl<'a> TeletextContext<'a> {
}
}
_ => {
if let Some(cur) = self.page_buffer_cur.take() {
ans = Some(Subtitle::new_text(
cur.into(),
self.page_buffer.show_timestamp,
self.page_buffer.hide_timestamp + Timestamp::from_millis(1),
None,
"TLT".into(),
));
}
ans = Some(Subtitle::new_text(
self.page_buffer_cur.take().unwrap().into(),
self.page_buffer.show_timestamp,
self.page_buffer.hide_timestamp + Timestamp::from_millis(1),
None,
"TLT".into(),
));
}
}
@@ -1252,43 +1251,34 @@ impl<'a> TeletextContext<'a> {
capitalization_list: &[String],
) {
// variable names conform to ETS 300 706, chapter 7.1.2
let Some(addr1) = decode_hamming_8_4(packet.address[1]) else {
return;
};
let Some(addr0) = decode_hamming_8_4(packet.address[0]) else {
return;
};
let address = (addr1 << 4) | addr0;
let address = (decode_hamming_8_4(packet.address[1]).unwrap() << 4)
| decode_hamming_8_4(packet.address[0]).unwrap();
let mut m = address & 0x7;
if m == 0 {
m = 8;
}
let y = (address >> 3) & 0x1f;
let designation_code = if y > 25 {
decode_hamming_8_4(packet.data[0]).unwrap_or(0x00)
decode_hamming_8_4(packet.data[0]).unwrap()
} else {
0x00
};
if y == 0 {
// CC map
let h1 = decode_hamming_8_4(packet.data[1]).unwrap_or(0);
let h0 = decode_hamming_8_4(packet.data[0]).unwrap_or(0);
let i = (h1 << 4) | h0;
let flag_subtitle = (decode_hamming_8_4(packet.data[5]).unwrap_or(0) & 0x08) >> 3;
let i = (decode_hamming_8_4(packet.data[1]).unwrap() << 4)
| decode_hamming_8_4(packet.data[0]).unwrap();
let flag_subtitle = (decode_hamming_8_4(packet.data[5]).unwrap() & 0x08) >> 3;
self.cc_map[i as usize] |= flag_subtitle << (m - 1);
let flag_subtitle = flag_subtitle != 0;
if flag_subtitle && (i < 0xff) {
let h1 = decode_hamming_8_4(packet.data[1]).unwrap_or(0) as u32;
let h0 = decode_hamming_8_4(packet.data[0]).unwrap_or(0) as u32;
let mut thisp = ((m as u32) << 8) | (h1 << 4) | h0;
let t1 = format!("{thisp:x}");
// Fallback to original value if parsing fails to avoid panics on malformed BCD
thisp = t1.parse().unwrap_or(thisp);
let mut thisp = ((m as u32) << 8)
| ((decode_hamming_8_4(packet.data[1]).unwrap() as u32) << 4)
| (decode_hamming_8_4(packet.data[0]).unwrap() as u32);
let t1 = format!("{thisp:x}"); // Example: 1928 -> 788
thisp = t1.parse().unwrap();
if !self.seen_sub_page[thisp as usize] {
self.seen_sub_page[thisp as usize] = true;
info!(
@@ -1298,28 +1288,36 @@ impl<'a> TeletextContext<'a> {
}
}
if (self.config.page.get() == 0.into()) && flag_subtitle && (i < 0xff) {
let h1 = decode_hamming_8_4(packet.data[1]).unwrap_or(0) as u16;
let h0 = decode_hamming_8_4(packet.data[0]).unwrap_or(0) as u16;
self.config
.page
.replace((((m as u16) << 8) | (h1 << 4) | h0).into());
self.config.page.replace(
(((m as u16) << 8)
| ((decode_hamming_8_4(packet.data[1]).unwrap() as u16) << 4)
| (decode_hamming_8_4(packet.data[0]).unwrap() as u16))
.into(),
);
info!("- No teletext page specified, first received suitable page is {}, not guaranteed\n", self.config.page.get());
}
// Page number and control bits
let h1 = decode_hamming_8_4(packet.data[1]).unwrap_or(0) as u16;
let h0 = decode_hamming_8_4(packet.data[0]).unwrap_or(0) as u16;
let page_number: TeletextPageNumber = (((m as u16) << 8) | (h1 << 4) | h0).into();
let page_number: TeletextPageNumber = (((m as u16) << 8)
| ((decode_hamming_8_4(packet.data[1]).unwrap() as u16) << 4)
| (decode_hamming_8_4(packet.data[0]).unwrap() as u16))
.into();
let charset = ((decode_hamming_8_4(packet.data[7]).unwrap() & 0x08)
| (decode_hamming_8_4(packet.data[7]).unwrap() & 0x04)
| (decode_hamming_8_4(packet.data[7]).unwrap() & 0x02))
>> 1;
// let flag_suppress_header = decode_hamming_8_4(packet.data[6]).unwrap() & 0x01;
// let flag_inhibit_display = (decode_hamming_8_4(packet.data[6]).unwrap() & 0x08) >> 3;
let c7 = decode_hamming_8_4(packet.data[7]).unwrap_or(0);
let charset = (c7 & 0x08 | c7 & 0x04 | c7 & 0x02) >> 1;
// ETS 300 706, chapter 9.3.1.3:
// When set to '1' the service is designated to be in Serial mode and the transmission of a page is terminated
// by the next page header with a different page number.
// When set to '0' the service is designated to be in Parallel mode and the transmission of a page is terminated
// by the next page header with a different page number but the same magazine number.
self.transmission_mode = if c7 & 0x01 == 0 {
// The same setting shall be used for all page headers in the service.
// ETS 300 706, chapter 7.2.1: Page is terminated by and excludes the next page header packet
// having the same magazine address in parallel transmission mode, or any magazine address in serial transmission mode.
self.transmission_mode = if decode_hamming_8_4(packet.data[7]).unwrap() & 0x01 == 0 {
TransmissionMode::Parallel
} else {
TransmissionMode::Serial
@@ -1355,17 +1353,19 @@ impl<'a> TeletextContext<'a> {
// Now we have the begining of page transmission; if there is page_buffer pending, process it
if self.page_buffer.tainted {
// Convert telx to UCS-2 before processing
for yt in 1..=23 {
for it in 0..40 {
if self.page_buffer.text[yt][it] != 0x00
&& !self.page_buffer.g2_char_present[yt][it]
{
if let Ok(c) = self.page_buffer.text[yt][it].try_into() {
self.page_buffer.text[yt][it] = self.g0_charset.ucs2_char(c);
}
self.page_buffer.text[yt][it] = self
.g0_charset
.ucs2_char(self.page_buffer.text[yt][it].try_into().unwrap());
}
}
}
// it would be nice, if subtitle hides on previous video frame, so we contract 40 ms (1 frame @25 fps)
self.page_buffer.hide_timestamp = timestamp - Timestamp::from_millis(40);
if self.page_buffer.hide_timestamp > timestamp {
self.page_buffer.hide_timestamp = Timestamp::from_millis(0);
@@ -1544,14 +1544,12 @@ impl<'a> TeletextContext<'a> {
info!("- Programme Identification Data = ");
for i in 20..40 {
let c = self.g0_charset.ucs2_char(packet.data[i]);
// strip any control codes from PID, eg. TVP station
if c < 0x20 {
continue;
}
info!(
"{}",
char::from_u32(c as u32).unwrap_or(char::REPLACEMENT_CHARACTER)
);
info!("{}", char::from_u32(c as u32).unwrap());
}
info!("\n");
@@ -1582,7 +1580,7 @@ impl<'a> TeletextContext<'a> {
info!(
"- Universal Time Co-ordinated = {}\n",
t0.to_ctime().as_deref().unwrap_or("unknown")
t0.to_ctime().unwrap()
);
debug!(msg_type = DebugMessageFlag::TELETEXT; "- Transmission mode = {:?}\n", self.transmission_mode);
@@ -1591,13 +1589,8 @@ impl<'a> TeletextContext<'a> {
&& matches!(self.config.date_format, TimestampFormat::Date { .. })
&& !self.config.noautotimeref
{
info!(
"- Broadcast Service Data Packet received, resetting UTC referential value to {}\n",
t0.to_ctime().as_deref().unwrap_or("unknown")
);
if let Ok(mut lock) = UTC_REFVALUE.write() {
*lock = t as u64;
}
info!("- Broadcast Service Data Packet received, resetting UTC referential value to {}\n", t0.to_ctime().unwrap());
*UTC_REFVALUE.write().unwrap() = t as u64;
self.states.pts_initialized = false;
}
@@ -1617,14 +1610,15 @@ impl<'a> TeletextContext<'a> {
if let Some(subtitles) = subtitles {
// output any pending close caption
if self.page_buffer.tainted {
// Convert telx to UCS-2 before processing
for yt in 1..=23 {
for it in 0..40 {
if self.page_buffer.text[yt][it] != 0x00
&& !self.page_buffer.g2_char_present[yt][it]
{
if let Ok(c) = self.page_buffer.text[yt][it].try_into() {
self.page_buffer.text[yt][it] = self.g0_charset.ucs2_char(c);
}
self.page_buffer.text[yt][it] = self
.g0_charset
.ucs2_char(self.page_buffer.text[yt][it].try_into().unwrap());
}
}
}

View File

@@ -225,6 +225,9 @@ 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))
}

View File

@@ -269,11 +269,6 @@ impl<'a> CCExtractorLogger {
self.target
}
/// Sets the target for logging messages.
pub fn set_target(&mut self, target: OutputTarget) {
self.target = target;
}
/// Check if the messages are intercepted by GUI.
pub fn is_gui_mode(&self) -> bool {
self.gui_mode
@@ -281,16 +276,8 @@ impl<'a> CCExtractorLogger {
fn print(&self, args: &Arguments<'a>) {
match &self.target {
OutputTarget::Stdout => {
print!("{args}");
// Flush stdout to ensure output appears immediately, especially when
// mixing with C code that also writes to stdout
let _ = std::io::Write::flush(&mut std::io::stdout());
}
OutputTarget::Stderr => {
eprint!("{args}");
let _ = std::io::Write::flush(&mut std::io::stderr());
}
OutputTarget::Stdout => print!("{args}"),
OutputTarget::Stderr => eprint!("{args}"),
OutputTarget::Quiet => {}
}
}

View File

@@ -28,7 +28,7 @@ const BURNEDIN_SUBTITLE_EXTRACTION: &str = "Burned-in subtitle extraction";
#[derive(Debug, Parser)]
#[command(name = "CCExtractor")]
#[command(author = "Carlos Fernandez Sanz, Volker Quetschke.")]
#[command(version = "0.96.5")]
#[command(version = "1.0")]
#[command(about = "Teletext portions taken from Petr Kutalek's telxcc
--------------------------------------------------------------------------
Originally based on McPoodle's tools. Check his page for lots of information
@@ -295,13 +295,6 @@ pub struct Args {
/// Example: --scc-framerate 25
#[arg(long="scc-framerate", verbatim_doc_comment, value_name="fps", help_heading=OPTIONS_AFFECTING_INPUT_FILES)]
pub scc_framerate: Option<String>,
/// Enable bandwidth-aware timing for SCC output (issue #1120).
/// When enabled, captions are pre-loaded ahead of their display time
/// based on the EIA-608 transmission bandwidth (2 bytes/frame).
/// This ensures YouTube and broadcast compliance by preventing
/// caption collisions. Use this for professional SCC output.
#[arg(long="scc-accurate-timing", verbatim_doc_comment, help_heading=OPTIONS_AFFECTING_INPUT_FILES)]
pub scc_accurate_timing: bool,
/// By default, ccextractor will process input files in
/// sequence as if they were all one large file (i.e.
/// split by a generic, non video-aware tool. If you
@@ -402,10 +395,10 @@ pub struct Args {
/// reference to the received data. Use this parameter if
/// you prefer your own reference. Note: Current this only
/// affects Teletext in timed transcript with --datets.
#[arg(long, alias="noautotimeref", verbatim_doc_comment, help_heading=OPTIONS_AFFECTING_INPUT_FILES)]
#[arg(long, verbatim_doc_comment, help_heading=OPTIONS_AFFECTING_INPUT_FILES)]
pub no_autotimeref: bool,
/// Ignore SCTE-20 data if present.
#[arg(long, alias="noscte20", verbatim_doc_comment, help_heading=OPTIONS_AFFECTING_INPUT_FILES)]
#[arg(long, verbatim_doc_comment, help_heading=OPTIONS_AFFECTING_INPUT_FILES)]
pub no_scte20: bool,
/// Create a separate file for CSS instead of inline.
#[arg(long, verbatim_doc_comment, help_heading=OPTIONS_AFFECTING_INPUT_FILES)]
@@ -460,7 +453,7 @@ pub struct Args {
/// Do not append a BOM (Byte Order Mark) to output
/// files. Note that this may break files when using
/// Windows. This is the default in non-Windows builds.
#[arg(long, alias="nobom", verbatim_doc_comment, conflicts_with="bom", help_heading=OUTPUT_AFFECTING_OUTPUT_FILES)]
#[arg(long, verbatim_doc_comment, conflicts_with="bom", help_heading=OUTPUT_AFFECTING_OUTPUT_FILES)]
pub no_bom: bool,
/// Encode subtitles in Unicode instead of Latin-1.
#[arg(long, verbatim_doc_comment, help_heading=OUTPUT_AFFECTING_OUTPUT_FILES)]
@@ -701,7 +694,7 @@ pub struct Args {
/// If you hate the repeated lines caused by the roll-up
/// emulation, you can have ccextractor write only one
/// line at a time, getting rid of these repeated lines.
#[arg(long, alias="noru", verbatim_doc_comment, help_heading=OUTPUT_AFFECTING_BUFFERING)]
#[arg(long, verbatim_doc_comment, help_heading=OUTPUT_AFFECTING_BUFFERING)]
pub no_rollup: bool,
/// roll-up captions can consist of 2, 3 or 4 visible
/// lines at any time (the number of lines is part of
@@ -830,10 +823,10 @@ pub struct Args {
#[arg(long, verbatim_doc_comment, help_heading=OUTPUT_AFFECTING_DEBUG_DATA)]
pub parsedebug: bool,
/// Print Program Association Table dump.
#[arg(long="parsePAT", alias="pat", verbatim_doc_comment, help_heading=OUTPUT_AFFECTING_DEBUG_DATA)]
#[arg(long="parsePAT", verbatim_doc_comment, help_heading=OUTPUT_AFFECTING_DEBUG_DATA)]
pub parse_pat: bool,
/// Print Program Map Table dump.
#[arg(long="parsePMT", alias="pmt", verbatim_doc_comment, help_heading=OUTPUT_AFFECTING_DEBUG_DATA)]
#[arg(long="parsePMT", verbatim_doc_comment, help_heading=OUTPUT_AFFECTING_DEBUG_DATA)]
pub parse_pmt: bool,
/// Hex-dump defective TS packets.
#[arg(long, verbatim_doc_comment, help_heading=OUTPUT_AFFECTING_DEBUG_DATA)]
@@ -868,7 +861,7 @@ pub struct Args {
/// for video streams that have both teletext packets
/// and CEA-608/708 packets (if teletext is processed
/// then CEA-608/708 processing is disabled).
#[arg(long, alias="noteletext", verbatim_doc_comment, conflicts_with="teletext", help_heading=TELETEXT_OPTIONS)]
#[arg(long, verbatim_doc_comment, conflicts_with="teletext", help_heading=TELETEXT_OPTIONS)]
pub no_teletext: bool,
/// Use the passed format to customize the (Timed) Transcript
/// output. The format must be like this: 1100100 (7 digits).
@@ -997,8 +990,6 @@ pub enum InFormat {
Mkv,
/// Material Exchange Format (MXF).
Mxf,
/// Scenarist Closed Caption (SCC).
Scc,
#[cfg(feature = "wtv_debug")]
// For WTV Debug mode only
Hex,

View File

@@ -21,19 +21,6 @@ pub unsafe extern "C" fn ccxr_process_avc(
return 0;
}
// In report-only mode (-out=report), enc_ctx is NULL because no encoder is created.
// Skip AVC processing in this case since we can't output captions without an encoder.
// Return the full buffer length to indicate we've "consumed" the data.
if enc_ctx.is_null() {
return avcbuflen;
}
// dec_ctx and sub should never be NULL in normal operation, but check defensively
if dec_ctx.is_null() || sub.is_null() {
info!("Warning: dec_ctx or sub is NULL in ccxr_process_avc");
return avcbuflen;
}
// Create a safe slice from the raw pointer
let avc_slice = std::slice::from_raw_parts_mut(avcbuf, avcbuflen);

View File

@@ -50,7 +50,7 @@ pub fn sei_message(ctx: &mut AvcContextRust, seibuf: &[u8]) -> usize {
return 0;
}
let mut payload_type: u32 = 0;
let mut payload_type = 0;
while seibuf_idx < seibuf.len() && seibuf[seibuf_idx] == 0xff {
payload_type += 255;
seibuf_idx += 1;
@@ -60,10 +60,10 @@ pub fn sei_message(ctx: &mut AvcContextRust, seibuf: &[u8]) -> usize {
return seibuf_idx;
}
payload_type += seibuf[seibuf_idx] as u32;
payload_type += seibuf[seibuf_idx] as i32;
seibuf_idx += 1;
let mut payload_size: u32 = 0;
let mut payload_size = 0;
while seibuf_idx < seibuf.len() && seibuf[seibuf_idx] == 0xff {
payload_size += 255;
seibuf_idx += 1;
@@ -73,7 +73,7 @@ pub fn sei_message(ctx: &mut AvcContextRust, seibuf: &[u8]) -> usize {
return seibuf_idx;
}
payload_size += seibuf[seibuf_idx] as u32;
payload_size += seibuf[seibuf_idx] as i32;
seibuf_idx += 1;
let mut broken = false;
@@ -226,10 +226,12 @@ pub fn user_data_registered_itu_t_t35(ctx: &mut AvcContextRust, userbuf: &[u8])
}
// Save the data and process once we know the sequence number
let required_size = ((ctx.cc_count as usize + local_cc_count) * 3) + 1;
if required_size > ctx.cc_data.len() {
if ((ctx.cc_count as usize + local_cc_count) * 3) + 1 > ctx.cc_databufsize {
let new_size = ((ctx.cc_count as usize + local_cc_count) * 6) + 1;
ctx.cc_data.resize(new_size, 0);
unsafe {
ctx.cc_data.set_len(new_size);
}
ctx.cc_data.reserve(new_size);
ctx.cc_databufsize = new_size;
}

View File

@@ -18,7 +18,6 @@ use lib_ccxr::common::DtvccServiceCharset;
use lib_ccxr::common::EncoderConfig;
use lib_ccxr::common::EncodersTranscriptFormat;
use lib_ccxr::common::Language;
use lib_ccxr::common::MkvLangFilter;
use lib_ccxr::common::Options;
use lib_ccxr::common::OutputFormat;
use lib_ccxr::common::SelectCodec;
@@ -184,9 +183,9 @@ pub unsafe fn copy_from_rust(ccx_s_options: *mut ccx_s_options, options: Options
(*ccx_s_options).ocr_quantmode = options.ocr_quantmode as _;
(*ccx_s_options).ocr_line_split = options.ocr_line_split as _;
(*ccx_s_options).ocr_blacklist = options.ocr_blacklist as _;
if let Some(ref mkvlang) = options.mkvlang {
if let Some(mkvlang) = options.mkvlang {
(*ccx_s_options).mkvlang =
replace_rust_c_string((*ccx_s_options).mkvlang, mkvlang.as_raw_str());
replace_rust_c_string((*ccx_s_options).mkvlang, mkvlang.to_ctype().as_str());
}
(*ccx_s_options).analyze_video_stream = options.analyze_video_stream as _;
(*ccx_s_options).hardsubx_ocr_mode = options.hardsubx_ocr_mode.to_ctype();
@@ -212,9 +211,11 @@ pub unsafe fn copy_from_rust(ccx_s_options: *mut ccx_s_options, options: Options
replace_rust_c_string((*ccx_s_options).udpaddr, &options.udpaddr.clone().unwrap());
}
(*ccx_s_options).udpport = options.udpport as _;
if let Some(tcpport) = options.tcpport {
(*ccx_s_options).tcpport =
replace_rust_c_string((*ccx_s_options).tcpport, &tcpport.to_string());
if options.tcpport.is_some() {
(*ccx_s_options).tcpport = replace_rust_c_string(
(*ccx_s_options).tcpport,
&options.tcpport.unwrap().to_string(),
);
}
if options.tcp_password.is_some() {
(*ccx_s_options).tcp_password = replace_rust_c_string(
@@ -234,9 +235,11 @@ pub unsafe fn copy_from_rust(ccx_s_options: *mut ccx_s_options, options: Options
&options.srv_addr.clone().unwrap(),
);
}
if let Some(srv_port) = options.srv_port {
(*ccx_s_options).srv_port =
replace_rust_c_string((*ccx_s_options).srv_port, &srv_port.to_string());
if options.srv_port.is_some() {
(*ccx_s_options).srv_port = replace_rust_c_string(
(*ccx_s_options).srv_port,
&options.srv_port.unwrap().to_string(),
);
}
(*ccx_s_options).noautotimeref = options.noautotimeref as _;
(*ccx_s_options).input_source = options.input_source as _;
@@ -250,12 +253,15 @@ pub unsafe fn copy_from_rust(ccx_s_options: *mut ccx_s_options, options: Options
// Subsequent calls from ccxr_demuxer_open/close should NOT modify inputfile because
// C code holds references to those strings throughout processing.
// Freeing them would cause use-after-free and double-free errors.
if let Some(ref inputfile) = options.inputfile {
if (*ccx_s_options).inputfile.is_null() {
(*ccx_s_options).inputfile = string_to_c_chars(inputfile.clone());
(*ccx_s_options).num_input_files =
inputfile.iter().filter(|s| !s.is_empty()).count() as _;
}
if options.inputfile.is_some() && (*ccx_s_options).inputfile.is_null() {
(*ccx_s_options).inputfile = string_to_c_chars(options.inputfile.clone().unwrap());
(*ccx_s_options).num_input_files = options
.inputfile
.as_ref()
.unwrap()
.iter()
.filter(|s| !s.is_empty())
.count() as _;
}
(*ccx_s_options).demux_cfg = options.demux_cfg.to_ctype();
// Only set enc_cfg on the first call (when output_filename is null).
@@ -274,7 +280,6 @@ pub unsafe fn copy_from_rust(ccx_s_options: *mut ccx_s_options, options: Options
(*ccx_s_options).scc_framerate = options.scc_framerate;
// Also copy to enc_cfg so the encoder uses the same frame rate for SCC output
(*ccx_s_options).enc_cfg.scc_framerate = options.scc_framerate;
(*ccx_s_options).enc_cfg.scc_accurate_timing = options.scc_accurate_timing.into();
#[cfg(feature = "with_libcurl")]
{
if options.curlposturl.is_some() {
@@ -419,10 +424,12 @@ pub unsafe fn copy_to_rust(ccx_s_options: *const ccx_s_options) -> Options {
options.ocr_line_split = (*ccx_s_options).ocr_line_split != 0;
options.ocr_blacklist = (*ccx_s_options).ocr_blacklist != 0;
// Handle mkvlang (C string to Option<MkvLangFilter>)
// Handle mkvlang (C string to Option<Language>)
if !(*ccx_s_options).mkvlang.is_null() {
let lang_str = c_char_to_string((*ccx_s_options).mkvlang);
options.mkvlang = MkvLangFilter::new(&lang_str).ok();
options.mkvlang = Some(
Language::from_str(&c_char_to_string((*ccx_s_options).mkvlang))
.expect("Invalid language"),
)
}
options.analyze_video_stream = (*ccx_s_options).analyze_video_stream != 0;
@@ -532,7 +539,6 @@ pub unsafe fn copy_to_rust(ccx_s_options: *const ccx_s_options) -> Options {
options.out_interval = (*ccx_s_options).out_interval;
options.segment_on_key_frames_only = (*ccx_s_options).segment_on_key_frames_only != 0;
options.scc_framerate = (*ccx_s_options).scc_framerate;
options.scc_accurate_timing = (*ccx_s_options).enc_cfg.scc_accurate_timing != 0;
// Handle optional features with conditional compilation
#[cfg(feature = "with_libcurl")]
@@ -976,7 +982,6 @@ impl CType<encoder_cfg> for EncoderConfig {
},
extract_only_708: self.extract_only_708 as _,
scc_framerate: 0, // Will be set from ccx_options.scc_framerate in copy_to_c
scc_accurate_timing: 0, // Will be set from ccx_options.scc_accurate_timing in copy_to_c
}
}
}

View File

@@ -615,6 +615,50 @@ impl FromCType<ccx_demux_report> for CcxDemuxReport {
}
}
/// # Safety
/// This function is unsafe because it takes a raw pointer to a C struct.
impl FromCType<*mut PMT_entry> for *mut PMTEntry {
unsafe fn from_ctype(buffer_ptr: *mut PMT_entry) -> Option<Self> {
if buffer_ptr.is_null() {
return None;
}
let buffer = unsafe { &*buffer_ptr };
let program_number = if buffer.program_number != 0 {
buffer.program_number
} else {
0
};
let elementary_pid = if buffer.elementary_PID != 0 {
buffer.elementary_PID
} else {
0
};
let stream_type = if buffer.stream_type != 0 {
StreamType::from_ctype(buffer.stream_type as u32).unwrap_or(StreamType::Unknownstream)
} else {
StreamType::Unknownstream
};
let printable_stream_type = if buffer.printable_stream_type != 0 {
buffer.printable_stream_type
} else {
0
};
let mut pmt_entry = PMTEntry {
program_number,
elementary_pid,
stream_type,
printable_stream_type,
};
Some(&mut pmt_entry as *mut PMTEntry)
}
}
impl FromCType<ccx_bufferdata_type> for BufferdataType {
unsafe fn from_ctype(c_value: ccx_bufferdata_type) -> Option<Self> {
let rust_value = match c_value {

View File

@@ -515,10 +515,8 @@ impl DtvccRust {
}
if let Some(decoder) = &mut self.decoders[i] {
// Check if there's content to flush: either cc_count > 0 (already printed)
// or any window has visible content (needs to be printed during flush)
let has_visible_windows = decoder.windows.iter().any(|w| is_true(w.visible));
if decoder.cc_count > 0 || has_visible_windows {
if decoder.cc_count > 0 {
// Flush this decoder
self.flush_decoder(i);
}
}

View File

@@ -1259,7 +1259,6 @@ extern "C" fn ccxr_flush_decoder(dtvcc: *mut dtvcc_ctx, decoder: *mut dtvcc_serv
mod test {
use super::*;
use crate::utils::get_zero_allocated_obj;
use std::alloc::{alloc_zeroed, dealloc, Layout};
fn setup_test_decoder_with_memory() -> dtvcc_service_decoder {
let mut decoder = get_zero_allocated_obj::<dtvcc_service_decoder>();
@@ -1350,17 +1349,10 @@ mod test {
decoder.current_window = 1;
decoder.windows[1].pen_column = 12;
decoder.windows[1].pen_row = 1;
let layout = Layout::array::<dtvcc_symbol>(CCX_DTVCC_MAX_COLUMNS as usize).unwrap();
for i in 0..CCX_DTVCC_MAX_ROWS as usize {
decoder.windows[1].rows[i] = unsafe { alloc_zeroed(layout) } as *mut dtvcc_symbol;
}
decoder.windows[1].rows[1] = Box::into_raw(Box::new(dtvcc_symbol::new(1)));
decoder.windows[1].rows[2] = Box::into_raw(Box::new(dtvcc_symbol::new(1)));
decoder.windows[1].memory_reserved = 1;
unsafe {
*decoder.windows[1].rows[1] = dtvcc_symbol::new(1);
*decoder.windows[1].rows[2] = dtvcc_symbol::new(1);
}
decoder.process_hcr();
assert_eq!(decoder.windows[1].pen_column, 0);
@@ -1375,13 +1367,6 @@ mod test {
unsafe { decoder.windows[1].rows[2].as_mut() },
Some(&mut dtvcc_symbol { sym: 1, init: 1 }),
);
// Cleanup
for i in 0..CCX_DTVCC_MAX_ROWS as usize {
unsafe {
dealloc(decoder.windows[1].rows[i] as *mut u8, layout);
}
}
}
#[test]
@@ -1391,16 +1376,8 @@ mod test {
decoder.windows[1].pen_column = 2;
decoder.windows[1].pen_row = 1;
decoder.windows[1].memory_reserved = 1;
let layout = Layout::array::<dtvcc_symbol>(CCX_DTVCC_MAX_COLUMNS as usize).unwrap();
for i in 0..CCX_DTVCC_MAX_ROWS as usize {
decoder.windows[1].rows[i] = unsafe { alloc_zeroed(layout) } as *mut dtvcc_symbol;
}
decoder.windows[1].memory_reserved = 1;
unsafe {
*decoder.windows[1].rows[1] = dtvcc_symbol::new(1);
*decoder.windows[1].rows[2] = dtvcc_symbol::new(1);
}
decoder.windows[1].rows[1] = Box::into_raw(Box::new(dtvcc_symbol::new(1)));
decoder.windows[1].rows[2] = Box::into_raw(Box::new(dtvcc_symbol::new(1)));
decoder.process_ff();
@@ -1417,13 +1394,6 @@ mod test {
unsafe { decoder.windows[1].rows[2].as_mut() },
Some(&mut dtvcc_symbol::default()),
);
// Cleanup
for i in 0..CCX_DTVCC_MAX_ROWS as usize {
unsafe {
dealloc(decoder.windows[1].rows[i] as *mut u8, layout);
}
}
}
#[test]

View File

@@ -167,9 +167,7 @@ impl dtvcc_window {
} else {
let layout = layout.unwrap();
// deallocate previous memory
if !self.rows[row_index].is_null() {
dealloc(self.rows[row_index] as *mut u8, layout);
}
dealloc(self.rows[row_index] as *mut u8, layout);
// allocate new zero initialized memory
let ptr = alloc_zeroed(layout);

View File

@@ -1,13 +1,8 @@
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;
@@ -114,9 +109,7 @@ impl Default for PSIBuffer {
fn default() -> Self {
PSIBuffer {
prev_ccounter: 0,
// Initialize with null to avoid unnecessary heap allocations and
// signal that the buffer is currently empty.
buffer: std::ptr::null_mut(),
buffer: Box::into_raw(Box::new(0u8)),
buffer_length: 0,
ccounter: 0,
}
@@ -281,21 +274,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.
// These are freed using C's free to be compatible with memory that might be allocated by C.
// Free all non-null PSIBuffer pointers (Rust-owned from Box::into_raw)
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 {
free(ptr as *mut c_void);
drop(Box::from_raw(ptr));
}
}
}
// Free all non-null PMTEntry pointers.
// These are freed using C's free to be compatible with memory that might be allocated by C.
// Free all non-null PMTEntry pointers (Rust-owned from Box::into_raw)
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 {
free(ptr as *mut c_void);
drop(Box::from_raw(ptr));
}
}
}

View File

@@ -327,9 +327,6 @@ impl CcxDemuxer<'_> {
StreamMode::Mxf => {
info!("MXF");
}
StreamMode::Scc => {
info!("SCC");
}
#[cfg(feature = "wtv_debug")]
StreamMode::HexDump => {
info!("Hex");

View File

@@ -331,15 +331,10 @@ 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.saturating_sub(3)
ctx.startbytes_avail - 3
} else {
50000 - 3 // Don't scan huge buffers entirely; 50KB is enough
49997
} as usize;
for i in 0..limit {
if ctx.startbytes[i] == 0x00
@@ -432,21 +427,15 @@ pub fn is_valid_mp4_box(
)
);
// 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'
// If the box type is "moov", check if it contains a valid movie header (mvhd)
if idx == 2
&& !(buffer[position + 12] == b'm'
&& buffer[position + 13] == b'v'
&& buffer[position + 14] == b'h'
&& buffer[position + 15] == b'd')
{
// moov without mvhd is not valid - skip it
continue;
}
{
// If "moov" doesn't have "mvhd", skip it.
continue;
}
// Box name matches. Do a crude validation of possible box size,

View File

@@ -278,8 +278,7 @@ pub unsafe fn user_data(
if !proceed {
debug!(msg_type = DebugMessageFlag::VERBOSE; "\rThe following payload is not properly terminated.");
let mut cc_data_copy = cc_data.to_vec();
dump(cc_data_copy.as_mut_ptr(), (cc_count * 3 + 1) as _, 0, 0);
dump(cc_data.to_vec().as_mut_ptr(), (cc_count * 3 + 1) as _, 0, 0);
}
debug!(msg_type = DebugMessageFlag::VERBOSE; "Reading {} HD CC blocks", cc_count);
@@ -290,11 +289,10 @@ pub unsafe fn user_data(
// Please note we store the current value of the global
// fts_now variable (and not get_fts()) as we are going to
// re-create the timeline in process_hdcc() (Slightly ugly).
let mut cc_data_copy = cc_data.to_vec();
store_hdcc(
enc_ctx,
dec_ctx,
cc_data_copy.as_mut_ptr(),
cc_data.to_vec().as_mut_ptr(),
cc_count as _,
(*dec_ctx.timing).current_tref,
(*dec_ctx.timing).fts_now,
@@ -342,10 +340,6 @@ pub unsafe fn user_data(
let dcd_pos = ustream.pos; // dish caption data position
match pattern_type {
0x02 => {
if ustream.data.len() - ustream.pos < 4 {
info!("Dish Network caption: insufficient data");
return Ok(1);
}
// Two byte caption - always on B-frame
// The following 4 bytes are:
// 0 : 0x09
@@ -393,10 +387,6 @@ pub unsafe fn user_data(
// Ignore 3 (0x0A, followed by two unknown) bytes.
}
0x04 => {
if ustream.data.len() - ustream.pos < 5 {
info!("Dish Network caption: insufficient data");
return Ok(1);
}
// Four byte caption - always on B-frame
// The following 5 bytes are:
// 0 : 0x09
@@ -433,10 +423,6 @@ pub unsafe fn user_data(
// Ignore 4 (0x020A, followed by two unknown) bytes.
}
0x05 => {
if ustream.data.len() - ustream.pos < 12 {
info!("Dish Network caption: insufficient data");
return Ok(1);
}
// Buffered caption - always on I-/P-frame
// The following six bytes are:
// 0 : 0x04
@@ -444,7 +430,7 @@ pub unsafe fn user_data(
// 1 : prev dcd[2]
// 2-3: prev dcd[3-4]
// 4-5: prev dcd[5-6]
let dcd_data = &ustream.data[dcd_pos..dcd_pos + 12]; // Need more bytes for this case
let dcd_data = &ustream.data[dcd_pos..dcd_pos + 10]; // Need more bytes for this case
debug!(msg_type = DebugMessageFlag::PARSE; " - {:02X} pch: {:02X} {:5} {:02X}:{:02X}",
dcd_data[0], dcd_data[1],
(dcd_data[2] as u32) * 256 + (dcd_data[3] as u32),
@@ -546,12 +532,10 @@ pub unsafe fn user_data(
if udatalen < 720 {
info!("MPEG:VBI: Minimum 720 bytes in luma line required");
return Ok(1);
}
let vbi_data = &ustream.data[ustream.pos..ustream.pos + 720];
let mut vbi_data_copy = vbi_data.to_vec();
decode_vbi(dec_ctx, field, vbi_data_copy.as_mut_ptr(), 720, sub);
decode_vbi(dec_ctx, field, vbi_data.to_vec().as_mut_ptr(), 720, sub);
debug!(msg_type = DebugMessageFlag::VERBOSE; "GXF (vbi line {}) user data:", line_nb);
} else {
// Some other user data
@@ -559,8 +543,14 @@ pub unsafe fn user_data(
debug!(msg_type = DebugMessageFlag::VERBOSE; "Unrecognized user data:");
let udatalen = ustream.data.len() - ustream.pos;
let dump_len = if udatalen > 128 { 128 } else { udatalen };
let mut data_copy = ustream.data[ustream.pos..ustream.pos + dump_len].to_vec();
dump(data_copy.as_mut_ptr(), dump_len as _, 0, 0);
dump(
ustream.data[ustream.pos..ustream.pos + dump_len]
.to_vec()
.as_mut_ptr(),
dump_len as _,
0,
0,
);
}
debug!(msg_type = DebugMessageFlag::VERBOSE; "User data - processed");

View File

@@ -129,14 +129,10 @@ pub fn sleepandchecktimeout(start: u64, ccx_options: &mut Options) {
.expect("System time went backwards")
.as_secs();
if let Some(live_stream) = ccx_options.live_stream {
if live_stream.seconds() != 0 {
if current_time > start + live_stream.millis() as u64 {
// Timeout elapsed
ccx_options.live_stream = Option::from(Timestamp::from_millis(0));
} else {
sleep_secs(1);
}
if ccx_options.live_stream.is_some() && ccx_options.live_stream.unwrap().seconds() != 0 {
if current_time > start + ccx_options.live_stream.unwrap().millis() as u64 {
// Timeout elapsed
ccx_options.live_stream = Option::from(Timestamp::from_millis(0));
} else {
sleep_secs(1);
}

View File

@@ -12,7 +12,7 @@ pub extern "C" fn rgb_to_hsv(R: f32, G: f32, B: f32, H: &mut f32, S: &mut f32, V
let hsv_rep = Hsv::from_color(rgb);
*H = hsv_rep.hue.into_positive_degrees();
*H = hsv_rep.hue.to_positive_degrees();
*S = hsv_rep.saturation;
*V = hsv_rep.value;
}

View File

@@ -373,20 +373,6 @@ pub extern "C" fn ccxr_dtvcc_is_active(dtvcc_ptr: *mut std::ffi::c_void) -> i32
}
}
/// Enable or disable the DTVCC decoder
/// This allows enabling the decoder after initialization
///
/// # Safety
/// dtvcc_ptr must be a valid pointer to a DtvccRust struct or null
#[no_mangle]
pub extern "C" fn ccxr_dtvcc_set_active(dtvcc_ptr: *mut std::ffi::c_void, active: i32) {
if dtvcc_ptr.is_null() {
return;
}
let dtvcc = unsafe { &mut *(dtvcc_ptr as *mut DtvccRust) };
dtvcc.is_active = active != 0;
}
/// Process cc_data
///
/// # Safety
@@ -416,7 +402,6 @@ extern "C" fn ccxr_process_cc_data(
let mut cc_data: Vec<u8> = (0..cc_count * 3)
.map(|x| unsafe { *data.add(x as usize) })
.collect();
// Use the persistent DtvccRust context from dtvcc_rust
let dtvcc_rust = dec_ctx.dtvcc_rust as *mut DtvccRust;
if dtvcc_rust.is_null() {
@@ -462,10 +447,6 @@ extern "C" fn ccxr_process_cc_data(
const CC_SOLID_BLANK: u8 = 0x7F;
pub fn validate_cc_pair(cc_block: &mut [u8]) -> bool {
if cc_block.len() != 3 {
return false;
}
let cc_valid = (cc_block[0] & 4) >> 2;
let cc_type = cc_block[0] & 3;
if cc_valid == 0 {
@@ -637,20 +618,12 @@ extern "C" fn ccxr_close_handle(handle: RawHandle) {
/// - Double-dash options (e.g., `--quiet`) are left unchanged
/// - Single-letter short options (e.g., `-o`) are left unchanged
/// - Non-option arguments (e.g., `file.ts`) are left unchanged
/// - Numeric options `-1`, `-2`, `-12` are converted to `--output-field=N` for CEA-608 field selection
/// - Numeric options (e.g., `-1`, `-12`) are left unchanged (these are valid short options)
fn normalize_legacy_option(arg: String) -> String {
// Handle legacy numeric options for CEA-608 field extraction
// These map to --output-field which is the modern equivalent
match arg.as_str() {
"-1" => return "--output-field=1".to_string(),
"-2" => return "--output-field=2".to_string(),
"-12" => return "--output-field=12".to_string(),
_ => {}
}
// Check if it's a single-dash option with multiple characters (e.g., -quiet)
// but not a short option with a value (e.g., -o filename)
// Single-letter options like -o, -s should be left unchanged
// Numeric options like -1, -12 should also be left unchanged
if arg.starts_with('-')
&& !arg.starts_with("--")
&& arg.len() > 2
@@ -809,15 +782,6 @@ mod test {
assert!(!validate_cc_pair(&mut cc_block));
}
#[test]
fn test_validate_cc_pair_invalid_length() {
let mut short = [0x97, 0x1F];
assert!(!validate_cc_pair(&mut short));
let mut long = [0x97, 0x1F, 0x3C, 0x00];
assert!(!validate_cc_pair(&mut long));
}
#[test]
fn test_do_cb() {
let mut dtvcc_ctx = crate::decoder::test::initialize_dtvcc_ctx();
@@ -879,18 +843,12 @@ mod test {
#[test]
fn test_normalize_legacy_option_numeric_options() {
// Legacy numeric options for CEA-608 field selection are converted to --output-field
assert_eq!(
normalize_legacy_option("-1".to_string()),
"--output-field=1".to_string()
);
assert_eq!(
normalize_legacy_option("-2".to_string()),
"--output-field=2".to_string()
);
// Numeric options should remain unchanged (these are valid ccextractor options)
assert_eq!(normalize_legacy_option("-1".to_string()), "-1".to_string());
assert_eq!(normalize_legacy_option("-2".to_string()), "-2".to_string());
assert_eq!(
normalize_legacy_option("-12".to_string()),
"--output-field=12".to_string()
"-12".to_string()
);
}
@@ -920,4 +878,349 @@ mod test {
// Double dash alone (end of options marker)
assert_eq!(normalize_legacy_option("--".to_string()), "--".to_string());
}
// =========================================================================
// FFI Integration Tests
// =========================================================================
//
// These tests verify the FFI boundary - the extern "C" functions that are
// called from C code. They test the actual C→Rust call path with realistic
// struct states.
mod ffi_integration_tests {
use super::*;
use crate::decoder::test::create_test_dtvcc_settings;
use crate::utils::get_zero_allocated_obj;
/// Helper to create a lib_cc_decode struct configured for Rust-enabled path
/// (dtvcc=NULL, dtvcc_rust=valid pointer)
fn create_rust_enabled_decode_ctx() -> (Box<lib_cc_decode>, *mut std::ffi::c_void) {
let mut ctx = get_zero_allocated_obj::<lib_cc_decode>();
// Create the DtvccRust context via the FFI function
let settings = create_test_dtvcc_settings();
let dtvcc_rust = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
// Set up the timing context (required for processing)
let timing = get_zero_allocated_obj::<ccx_common_timing_ctx>();
ctx.timing = Box::into_raw(timing);
// Simulate Rust-enabled mode: dtvcc is NULL, dtvcc_rust is set
ctx.dtvcc = std::ptr::null_mut();
ctx.dtvcc_rust = dtvcc_rust;
(ctx, dtvcc_rust)
}
// -----------------------------------------------------------------
// ccxr_dtvcc_init / ccxr_dtvcc_free lifecycle tests
// -----------------------------------------------------------------
#[test]
fn test_ffi_dtvcc_init_creates_valid_context() {
let settings = create_test_dtvcc_settings();
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
// Should return a valid (non-null) pointer
assert!(!dtvcc_ptr.is_null());
// Verify we can check if it's active
let is_active = unsafe { ccxr_dtvcc_is_active(dtvcc_ptr) };
assert_eq!(is_active, 1);
// Clean up
ccxr_dtvcc_free(dtvcc_ptr);
}
#[test]
fn test_ffi_dtvcc_init_with_null_returns_null() {
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(std::ptr::null()) };
assert!(dtvcc_ptr.is_null());
}
#[test]
fn test_ffi_dtvcc_free_with_null_is_safe() {
// Should not crash when called with null
ccxr_dtvcc_free(std::ptr::null_mut());
}
#[test]
fn test_ffi_dtvcc_is_active_with_null_returns_zero() {
let result = unsafe { ccxr_dtvcc_is_active(std::ptr::null_mut()) };
assert_eq!(result, 0);
}
// -----------------------------------------------------------------
// ccxr_dtvcc_set_encoder tests
// -----------------------------------------------------------------
#[test]
fn test_ffi_set_encoder_with_valid_context() {
let settings = create_test_dtvcc_settings();
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
// Create an encoder
let encoder = Box::new(encoder_ctx::default());
let encoder_ptr = Box::into_raw(encoder);
// Set the encoder
unsafe { ccxr_dtvcc_set_encoder(dtvcc_ptr, encoder_ptr) };
// Verify by checking the internal state
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
assert_eq!(dtvcc.encoder, encoder_ptr);
// Clean up
ccxr_dtvcc_free(dtvcc_ptr);
unsafe { drop(Box::from_raw(encoder_ptr)) };
}
#[test]
fn test_ffi_set_encoder_with_null_context_is_safe() {
let encoder = Box::new(encoder_ctx::default());
let encoder_ptr = Box::into_raw(encoder);
// Should not crash
unsafe { ccxr_dtvcc_set_encoder(std::ptr::null_mut(), encoder_ptr) };
// Clean up
unsafe { drop(Box::from_raw(encoder_ptr)) };
}
// -----------------------------------------------------------------
// ccxr_dtvcc_process_data tests
// -----------------------------------------------------------------
#[test]
fn test_ffi_process_data_packet_start() {
let settings = create_test_dtvcc_settings();
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
// Process a packet start (cc_type = 3)
unsafe { ccxr_dtvcc_process_data(dtvcc_ptr, 1, 3, 0xC2, 0x00) };
// Verify state changed
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
assert!(dtvcc.is_header_parsed);
assert_eq!(dtvcc.packet_length, 2);
// Clean up
ccxr_dtvcc_free(dtvcc_ptr);
}
#[test]
fn test_ffi_process_data_with_null_is_safe() {
// Should not crash
unsafe { ccxr_dtvcc_process_data(std::ptr::null_mut(), 1, 3, 0xC2, 0x00) };
}
#[test]
fn test_ffi_process_data_state_persists_across_calls() {
// This is THE key test - verifying the fix for issue #1499
let settings = create_test_dtvcc_settings();
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
// First call: start a packet (packet length = 8 bytes)
unsafe { ccxr_dtvcc_process_data(dtvcc_ptr, 1, 3, 0xC4, 0x00) };
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
assert!(dtvcc.is_header_parsed);
assert_eq!(dtvcc.packet_length, 2);
// Second call: add more data (cc_type = 2)
unsafe { ccxr_dtvcc_process_data(dtvcc_ptr, 1, 2, 0x21, 0x00) };
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
assert_eq!(dtvcc.packet_length, 4);
// Third call: add more data
unsafe { ccxr_dtvcc_process_data(dtvcc_ptr, 1, 2, 0x00, 0x00) };
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
assert_eq!(dtvcc.packet_length, 6);
// State persisted across all calls!
assert!(dtvcc.is_header_parsed);
// Clean up
ccxr_dtvcc_free(dtvcc_ptr);
}
// -----------------------------------------------------------------
// ccxr_process_cc_data tests (the main FFI entry point from C)
// -----------------------------------------------------------------
#[test]
fn test_ffi_ccxr_process_cc_data_with_null_ctx_returns_error() {
let data: [u8; 3] = [0x97, 0x1F, 0x3C];
let result = unsafe { ccxr_process_cc_data(std::ptr::null_mut(), data.as_ptr(), 1) };
assert_eq!(result, -1);
}
#[test]
fn test_ffi_ccxr_process_cc_data_with_null_data_returns_error() {
let (ctx, dtvcc_ptr) = create_rust_enabled_decode_ctx();
let result =
unsafe { ccxr_process_cc_data(Box::into_raw(ctx), std::ptr::null(), 1) };
assert_eq!(result, -1);
// Clean up
ccxr_dtvcc_free(dtvcc_ptr);
}
#[test]
fn test_ffi_ccxr_process_cc_data_with_zero_count_returns_error() {
let (ctx, dtvcc_ptr) = create_rust_enabled_decode_ctx();
let data: [u8; 3] = [0x97, 0x1F, 0x3C];
let result =
unsafe { ccxr_process_cc_data(Box::into_raw(ctx), data.as_ptr(), 0) };
assert_eq!(result, -1);
// Clean up
ccxr_dtvcc_free(dtvcc_ptr);
}
#[test]
fn test_ffi_ccxr_process_cc_data_with_null_dtvcc_rust_returns_error() {
let mut ctx = get_zero_allocated_obj::<lib_cc_decode>();
// Set up timing but leave dtvcc_rust as null
let timing = get_zero_allocated_obj::<ccx_common_timing_ctx>();
ctx.timing = Box::into_raw(timing);
ctx.dtvcc_rust = std::ptr::null_mut();
let data: [u8; 3] = [0x97, 0x1F, 0x3C];
let result =
unsafe { ccxr_process_cc_data(Box::into_raw(ctx), data.as_ptr(), 1) };
assert_eq!(result, -1);
}
#[test]
fn test_ffi_ccxr_process_cc_data_processes_708_data() {
let (ctx, dtvcc_ptr) = create_rust_enabled_decode_ctx();
// Set an encoder so processing can complete
let encoder = get_zero_allocated_obj::<encoder_ctx>();
ccxr_dtvcc_set_encoder(dtvcc_ptr, Box::into_raw(encoder));
// CEA-708 packet start (cc_type=3, cc_valid=1)
// Header byte breakdown: cc_valid is bit 2, cc_type is bits 0-1
// For cc_type=3 (bits 0-1 = 11) and cc_valid=1 (bit 2 = 1):
// 0xFF = 11111111 -> cc_valid=1, cc_type=3
let data: [u8; 3] = [0xFF, 0xC2, 0x00];
let ctx_ptr = Box::into_raw(ctx);
let result = ccxr_process_cc_data(ctx_ptr, data.as_ptr(), 1);
// Should return 0 (success) for valid 708 data
assert_eq!(result, 0);
// Verify the context was updated
let ctx = unsafe { &*ctx_ptr };
assert_eq!(ctx.cc_stats[3], 1); // cc_type 3 was processed
assert_eq!(ctx.current_field, 3); // Field set to 3 for 708 data
// Clean up
ccxr_dtvcc_free(dtvcc_ptr);
}
#[test]
fn test_ffi_ccxr_process_cc_data_skips_invalid_pairs() {
let (ctx, dtvcc_ptr) = create_rust_enabled_decode_ctx();
// Invalid pair (cc_valid = 0)
// Header byte: 0xF9 = 11111001 -> cc_valid=0, cc_type=1
let data: [u8; 3] = [0xF9, 0x00, 0x00];
let ctx_ptr = Box::into_raw(ctx);
let result = unsafe { ccxr_process_cc_data(ctx_ptr, data.as_ptr(), 1) };
// Should return -1 (no valid data processed)
assert_eq!(result, -1);
// Clean up
ccxr_dtvcc_free(dtvcc_ptr);
}
// -----------------------------------------------------------------
// ccxr_flush_active_decoders tests
// -----------------------------------------------------------------
#[test]
fn test_ffi_flush_with_null_is_safe() {
// Should not crash
unsafe { ccxr_flush_active_decoders(std::ptr::null_mut()) };
}
#[test]
fn test_ffi_flush_with_valid_context() {
let settings = create_test_dtvcc_settings();
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
// Set an encoder (required for flushing)
let encoder = Box::new(encoder_ctx::default());
unsafe { ccxr_dtvcc_set_encoder(dtvcc_ptr, Box::into_raw(encoder)) };
// Should not crash
unsafe { ccxr_flush_active_decoders(dtvcc_ptr) };
// Clean up
ccxr_dtvcc_free(dtvcc_ptr);
}
// -----------------------------------------------------------------
// Full lifecycle integration test
// -----------------------------------------------------------------
#[test]
fn test_ffi_complete_lifecycle() {
// This test simulates the complete lifecycle as it would be used from C code:
// 1. Initialize context
// 2. Set encoder
// 3. Process multiple CC data packets
// 4. Flush
// 5. Free
// 1. Initialize
let settings = create_test_dtvcc_settings();
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
assert!(!dtvcc_ptr.is_null());
// Verify initial state
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
assert!(dtvcc.is_active, "DtvccRust should be active after init");
assert_eq!(dtvcc.packet_length, 0);
assert!(!dtvcc.is_header_parsed);
// 2. Set encoder
let encoder = get_zero_allocated_obj::<encoder_ctx>();
let encoder_ptr = Box::into_raw(encoder);
ccxr_dtvcc_set_encoder(dtvcc_ptr, encoder_ptr);
// 3. Process a packet start (cc_type=3) - this should set is_header_parsed
// Use 0xC4 for packet header: 0xC4 & 0x3F = 4, so max_len = 4*2 = 8 bytes
// This way the packet won't be completed until we've added enough data
ccxr_dtvcc_process_data(dtvcc_ptr, 1, 3, 0xC4, 0x00);
// Verify packet start was processed
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
assert!(
dtvcc.is_header_parsed,
"is_header_parsed should be true after packet start"
);
assert_eq!(dtvcc.packet_length, 2, "packet_length should be 2");
// Process packet data (cc_type=2) - packet is not complete yet (need 8 bytes)
ccxr_dtvcc_process_data(dtvcc_ptr, 1, 2, 0x21, 0x00);
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
assert_eq!(dtvcc.packet_length, 4, "packet_length should be 4");
// Add more data
ccxr_dtvcc_process_data(dtvcc_ptr, 1, 2, 0x00, 0x00);
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
assert_eq!(dtvcc.packet_length, 6, "packet_length should be 6");
// 4. Flush
ccxr_flush_active_decoders(dtvcc_ptr);
// 5. Free
ccxr_dtvcc_free(dtvcc_ptr);
unsafe { drop(Box::from_raw(encoder_ptr)) };
}
}
}

View File

@@ -7,6 +7,7 @@ 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};
@@ -17,12 +18,10 @@ 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(
@@ -99,89 +98,61 @@ 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..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}");
}
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();
}
}
} else {
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..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();
}
}
// 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();
}
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}");
}
// PIDs programs - extra defensive version
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();
}
}
} 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;
@@ -294,15 +265,7 @@ pub unsafe fn copy_demuxer_from_c_to_rust(ccx: *const ccx_demuxer) -> CcxDemuxer
if buffer_ptr.is_null() {
None
} else {
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)
Some(Box::into_raw(Box::new(PSIBuffer::from_ctype(*buffer_ptr)?)))
}
})
.collect::<Vec<_>>();
@@ -313,14 +276,7 @@ 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 {
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)
Some(Box::into_raw(Box::new(PMTEntry::from_ctype(*buffer_ptr)?)))
}
})
.collect::<Vec<_>>();
@@ -411,7 +367,8 @@ 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 ptr = calloc(1, std::mem::size_of::<ccx_demuxer>()) as *mut ccx_demuxer;
let layout = Layout::new::<ccx_demuxer>();
let ptr = alloc_zeroed(layout) as *mut ccx_demuxer;
if ptr.is_null() {
panic!("Failed to allocate memory for ccx_demuxer");

View File

@@ -33,11 +33,10 @@ pub unsafe extern "C" fn ccxr_init_basic_logger() {
.unwrap_or(DebugMessageFlag::VERBOSE);
let mask = DebugMessageMask::new(debug_mask, debug_mask_on_debug);
let gui_mode_reports = ccx_options.gui_mode_reports != 0;
// CCX_MESSAGES_QUIET=0, CCX_MESSAGES_STDOUT=1, CCX_MESSAGES_STDERR=2
let messages_target = match ccx_options.messages_target {
0 => OutputTarget::Quiet,
1 => OutputTarget::Stdout,
2 => OutputTarget::Stderr,
0 => OutputTarget::Stdout,
1 => OutputTarget::Stderr,
2 => OutputTarget::Quiet,
_ => OutputTarget::Stderr, // Default to stderr for invalid values
};
let _ = set_logger(CCExtractorLogger::new(
@@ -47,28 +46,6 @@ pub unsafe extern "C" fn ccxr_init_basic_logger() {
));
}
/// Updates the logger target after command-line arguments have been parsed.
/// This is needed because the logger is initialized before argument parsing,
/// and options like --quiet need to be applied afterwards.
///
/// # Safety
///
/// `ccx_options` in C must be properly initialized and the logger must have
/// been initialized via `ccxr_init_basic_logger` before calling this function.
#[no_mangle]
pub unsafe extern "C" fn ccxr_update_logger_target() {
// CCX_MESSAGES_QUIET=0, CCX_MESSAGES_STDOUT=1, CCX_MESSAGES_STDERR=2
let messages_target = match ccx_options.messages_target {
0 => OutputTarget::Quiet,
1 => OutputTarget::Stdout,
2 => OutputTarget::Stderr,
_ => OutputTarget::Stderr,
};
if let Some(mut logger) = logger_mut() {
logger.set_target(messages_target);
}
}
/// Rust equivalent for `verify_crc32` function in C. Uses C-native types as input and output.
///
/// # Safety
@@ -77,10 +54,6 @@ pub unsafe extern "C" fn ccxr_update_logger_target() {
/// or less than `len`.
#[no_mangle]
pub unsafe extern "C" fn ccxr_verify_crc32(buf: *const u8, len: c_int) -> c_int {
// Safety: avoid NULL pointer and negative length causing usize wraparound
if buf.is_null() || len < 0 {
return 0;
}
let buf = std::slice::from_raw_parts(buf, len as usize);
if verify_crc32(buf) {
1

File diff suppressed because it is too large Load Diff

View File

@@ -19,4 +19,4 @@ Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
EndGlobal