diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index b7d0d670079..c7e00a03a32 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -2,186 +2,179 @@ name: CI on: push: - branches: - - main + branches: [main] + tags: ["v*"] pull_request: - branches: - - main + branches: [main] + +# Least-privilege default; the release job below opts into write explicitly. +permissions: + contents: read jobs: - build-core: + build-and-test: runs-on: ubuntu-latest steps: - - name: Checkout Source Code - uses: actions/checkout@v2 - - name: Update pkg-config database - run: sudo ldconfig - - name: Setup Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v2 - - name: Cache build - id: cache-build - uses: actions/cache@v4 + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # Build the multithreaded core (emsdk 6.0.2 is multi-arch, so this builds + # natively on the amd64 runner). 6.0+ fftools require threads, so the MT + # cores are 8.x; the single-threaded cores are 5.1 (st-build-and-test). + - name: Build ffmpeg-core-mt + run: | + make prd-mt EXTRA_ARGS="--cache-from=type=gha,scope=core-mt --cache-to=type=gha,mode=max,scope=core-mt" + + - name: Set up Node.js + uses: actions/setup-node@v4 with: - path: build-cache-st - key: build-cache-st-v1-${{ hashFiles('Dockerfile', 'Makefile', 'build/*') }} - restore-keys: | - build-cache-st-v1- - - name: Build ffmpeg-core - run: make prd EXTRA_ARGS="--cache-from=type=local,src=build-cache-st --cache-to=type=local,dest=build-cache-st,mode=max" - - name: Upload core + node-version: 22 + + - name: Install dependencies + run: npm install --no-audit --no-fund + + # Build the JS packages (@ffmpeg/ffmpeg, @ffmpeg/util). npm install --no-audit --no-fund also fetches + # the headless Chromium that mocha-headless-chrome (puppeteer) drives. + - name: Build JS packages + run: npm run build + + - name: Run MT test suite + run: npx start-server-and-test "npm run serve" 3000 "npm run test:browser:ffmpeg:mt" + + - name: Upload core-mt artifact uses: actions/upload-artifact@v4 with: - name: ffmpeg-core - path: packages/core/dist/* - build-core-mt: + name: ffmpeg-core-mt + path: packages/core-mt/dist/* + + # Slim variant gate (parallel with build-and-test — no added wall-clock). Builds + # the ~6 MB slim core and runs the slim app-ops suite (copy cut/concat, single- + # input libx264 re-encode, re-encode stitch). The generic MT suite can't gate + # slim: its mp4->avi transcode needs the avi muxer/mpeg4 encoder slim drops. + slim-build-and-test: runs-on: ubuntu-latest steps: - - name: Checkout Source Code - uses: actions/checkout@v2 - - name: Setup Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v2 - - name: Cache build - id: cache-build - uses: actions/cache@v4 + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build ffmpeg-core-mt-slim + run: | + make prd-mt-slim EXTRA_ARGS="--cache-from=type=gha,scope=core-mt-slim --cache-to=type=gha,mode=max,scope=core-mt-slim" + + - name: Set up Node.js + uses: actions/setup-node@v4 with: - path: build-cache-mt - key: build-cache-mt-v1-${{ hashFiles('Dockerfile', 'Makefile', 'build/*') }} - restore-keys: | - build-cache-v1- - - name: Build ffmpet-core-mt - run: make prd-mt EXTRA_ARGS="--cache-from=type=local,src=build-cache-mt --cache-to=type=local,dest=build-cache-mt,mode=max" - - name: Upload core-mt + node-version: 22 + + - name: Install dependencies + run: npm install --no-audit --no-fund + + - name: Build JS packages + run: npm run build + + - name: Run slim app-ops suite + run: npx start-server-and-test "npm run serve" 3000 "npm run test:browser:ffmpeg:slim" + + - name: Upload core-mt-slim artifact uses: actions/upload-artifact@v4 with: - name: ffmpeg-core-mt - path: packages/core-mt/dist/* - tests: + name: ffmpeg-core-mt-slim + path: packages/core-mt-slim/dist/* + + # ST gate (parallel with the MT jobs). Builds BOTH 5.1.10 ST cores — the + # full core doubles as the input synthesizer for the cliptool suite (the + # copy core has no encoders by design) — then runs the ST suite and the + # verbatim clipping-tool-ui commands against the copy core, both on the + # non-COI server (the whole point of ST is running without COOP/COEP; the + # pages assert crossOriginIsolated === false). + st-build-and-test: runs-on: ubuntu-latest - needs: - - build-core - - build-core-mt steps: - - name: Checkout Source Code - uses: actions/checkout@v2 - - name: Download ffmpeg-core - uses: actions/download-artifact@v4 - with: - name: ffmpeg-core - path: packages/core/dist - - name: Download ffmpeg-core-mt - uses: actions/download-artifact@v4 + # submodules: the cliptool suite fetches testdata/audio-1s.wav, and + # testdata is a git submodule — without this the directory is empty in + # CI and the fixture fetch 404s. The MT jobs use only embedded fixtures. + - name: Checkout + uses: actions/checkout@v4 with: - name: ffmpeg-core-mt - path: packages/core-mt/dist - - name: Use Node.js 18 - uses: actions/setup-node@v2 - with: - node-version: 18.x - - name: Cache dependencies - id: cache-dependencies - uses: actions/cache@v4 + submodules: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build ffmpeg-core (ST full) + ffmpeg-core-copy + run: | + make prd EXTRA_ARGS="--cache-from=type=gha,scope=core-st --cache-to=type=gha,mode=max,scope=core-st" + make prd-st-copy EXTRA_ARGS="--cache-from=type=gha,scope=core-st-copy --cache-to=type=gha,mode=max,scope=core-st-copy" + + - name: Set up Node.js + uses: actions/setup-node@v4 with: - path: node_modules - key: node-modules-${{ hashFiles('package-lock.json') }} - restore-keys: | - node-modules- + node-version: 22 + - name: Install dependencies - run: npm install - - name: Install Chrome - uses: browser-actions/setup-chrome@latest + run: npm install --no-audit --no-fund + + - name: Build JS packages + run: npm run build + + - name: Run ST suite (non-isolated) + run: npx start-server-and-test "npm run serve:no-coi" 3001 "npm run test:browser:ffmpeg:st" + + - name: Run cliptool suite against the copy core (non-isolated) + run: npx start-server-and-test "npm run serve:no-coi" 3001 "npm run test:browser:cliptool:copy" + + - name: Upload core-copy artifact + uses: actions/upload-artifact@v4 with: - chrome-version: stable - - name: Run tests - env: - CHROME_HEADLESS: 1 - CHROME_PATH: chrome - CHROME_FLAGS: "--headless --disable-gpu --no-sandbox --enable-features=SharedArrayBuffer,CrossOriginIsolation" - HEADERS: '{"Cross-Origin-Opener-Policy": "same-origin", "Cross-Origin-Embedder-Policy": "require-corp"}' + name: ffmpeg-core-copy + path: packages/core-copy/dist/* + + # On a version tag, publish the vendorable artifacts (core + wrapper) as a + # GitHub Release, so downstream apps can vendor from a targeted release. + release: + if: startsWith(github.ref, 'refs/tags/v') + needs: [build-and-test, slim-build-and-test, st-build-and-test] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build core + JS packages + run: | + make prd-mt EXTRA_ARGS="--cache-from=type=gha,scope=core-mt --cache-to=type=gha,mode=max,scope=core-mt" + make prd-mt-slim EXTRA_ARGS="--cache-from=type=gha,scope=core-mt-slim --cache-to=type=gha,mode=max,scope=core-mt-slim" + make prd-st-copy EXTRA_ARGS="--cache-from=type=gha,scope=core-st-copy --cache-to=type=gha,mode=max,scope=core-st-copy" + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install + build JS packages + run: | + npm install --no-audit --no-fund + npm run build + + - name: Package vendorable artifacts run: | - # Start test server with proper headers for all tests - npm run serve -- --headers "$HEADERS" & - - # Increase wait time to ensure server is ready - sleep 15 - - # Verify headers and isolation status - echo "Checking security headers and isolation status..." - curl -v http://localhost:3000/tests/ffmpeg-core-st.test.html 2>&1 | grep -i "cross-origin" - - # Run verification script first - echo "Verifying browser environment..." - cat << EOF > verify-browser.html - - - - - - - - - - - EOF - - # Run single-threaded tests first - echo "Running single-threaded tests..." - npx mocha-headless-chrome \ - --args="$CHROME_FLAGS" \ - -a no-sandbox \ - -f http://localhost:3000/tests/ffmpeg-core-st.test.html 2>&1 | tee st-core-test.log - - npx mocha-headless-chrome \ - --args="$CHROME_FLAGS" \ - -a no-sandbox \ - -f http://localhost:3000/tests/ffmpeg-st.test.html 2>&1 | tee st-test.log - - # Run multi-threaded tests - echo "Running multi-threaded tests..." - # Create a test script to verify browser environment - cat << EOF > verify-browser.html - - - - Browser Environment Test - - - - - - EOF - - # Run the verification in Chrome - echo "Verifying browser environment..." - npx mocha-headless-chrome \ - --args="$CHROME_FLAGS --enable-features=SharedArrayBuffer,CrossOriginIsolation" \ - -a no-sandbox \ - -f http://localhost:3000/verify-browser.html - - # Run MT tests with verified configuration - npx mocha-headless-chrome \ - --args="$CHROME_FLAGS --enable-features=SharedArrayBuffer,CrossOriginIsolation" \ - -a no-sandbox \ - -f http://localhost:3000/tests/ffmpeg-core-mt.test.html 2>&1 | tee mt-core-test.log - - npx mocha-headless-chrome \ - --args="$CHROME_FLAGS --enable-features=SharedArrayBuffer,CrossOriginIsolation" \ - -a no-sandbox \ - -f http://localhost:3000/tests/ffmpeg-mt.test.html 2>&1 | tee mt-test.log - - # Display all logs for debugging - echo "=== Test Logs ===" - for log in *-test.log; do - echo "Contents of $log:" - cat $log - done + mkdir -p dist-release + tar -C packages/core-mt/dist -czf "dist-release/ffmpeg-core-mt-${GITHUB_REF_NAME}.tgz" . + tar -C packages/core-mt-slim/dist -czf "dist-release/ffmpeg-core-mt-slim-${GITHUB_REF_NAME}.tgz" . + tar -C packages/core-copy/dist -czf "dist-release/ffmpeg-core-st-copy-${GITHUB_REF_NAME}.tgz" . + tar -C packages/ffmpeg/dist -czf "dist-release/ffmpeg-wasm-${GITHUB_REF_NAME}.tgz" . + + - name: Attach artifacts to release + uses: softprops/action-gh-release@v2 + with: + files: dist-release/*.tgz diff --git a/.gitignore b/.gitignore index f513c13805c..d74d2bb5759 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,10 @@ dist /.nyc_output .DS_Store +# local buildx cache (see Makefile EXTRA_ARGS / CI cache steps) +build-cache-st/ +build-cache-mt/ + # ide .idea/ diff --git a/BASELINE-n5.1.4.md b/BASELINE-n5.1.4.md new file mode 100644 index 00000000000..6fbbfb4b30e --- /dev/null +++ b/BASELINE-n5.1.4.md @@ -0,0 +1,87 @@ +# Baseline snapshot — pre-upgrade rollback reference + +This records the exact state of the **current (pre-upgrade) `n5.1.4` build** so the +FFmpeg 7.1 upgrade (and every intermediate phase) can be diffed against a known-good +reference. Captured on the `chore/phase-0-baseline` branch. + +## Toolchain + +| Component | Version / pin | Source | +|----------------------|----------------------------------------|--------| +| FFmpeg | `n5.1.4` | `Dockerfile` `FFMPEG_VERSION` | +| Emscripten (emsdk) | `3.1.40` | `Dockerfile` base image (amd64-only) | +| Host build note | Built under **x86 emulation** on Apple Silicon (arm64); `emsdk:3.1.40` has no arm64 image | — | + +## External libraries (all from `github.com/ffmpegwasm/*` forks unless noted) + +| Library | Pin (branch/tag) | Notes for upgrade | +|------------|--------------------|-------------------| +| x264 | `4-cores` (branch) | **floating branch** — pin to SHA in Phase 4 | +| x265 | `3.4` | **DROP** in Phase 3 (HEVC encode; decode is native) | +| libvpx | `v1.13.1` | keep, consider bump | +| lame | `master` (branch) | **floating branch** — pin to SHA in Phase 4 | +| ogg | `v1.3.4` | **DROP** with vorbis/theora | +| theora | `v1.1.1` (2009) | **DROP** in Phase 3 | +| opus | `v1.3.1` | keep | +| vorbis | `v1.3.3` | **DROP** in Phase 3 | +| zlib | `v1.2.11` | **BUMP → 1.3.1** (CVE-2018-25032, CVE-2022-37434) | +| libwebp | `v1.3.2` | keep | +| freetype2 | `VER-2-10-4` (2020)| **BUMP** (CVEs) | +| fribidi | `v1.0.9` (upstream)| **BUMP → 1.0.13+** (CVE-2022-2530x) | +| harfbuzz | `5.2.0` (upstream) | **BUMP → 6.0+** (CVE-2023-25193) | +| libass | `0.15.0` | keep | +| zimg | `release-3.0.5` | keep | + +## FFmpeg configure (extracted from `packages/core/dist/umd/ffmpeg-core.wasm`) + +``` +--target-os=none --arch=x86_32 --enable-cross-compile +--disable-asm --disable-stripping --disable-programs --disable-doc --disable-debug +--disable-runtime-cpudetect --disable-autodetect +--nm=emnm --ar=emar --ranlib=emranlib --cc=emcc --cxx=em++ --objcc=emcc --dep-cc=emcc +--extra-cflags='-I/opt/include -O3 -msimd128' +--extra-cxxflags='-I/opt/include -O3 -msimd128' +--disable-pthreads --disable-w32threads --disable-os2threads # (ST variant) +--enable-gpl +--enable-libx264 --enable-libx265 --enable-libvpx --enable-libmp3lame +--enable-libtheora --enable-libvorbis --enable-libopus --enable-zlib +--enable-libwebp --enable-libfreetype --enable-libfribidi --enable-libass --enable-libzimg +``` + +Note: prod build enables `-msimd128` (Emscripten wasm SIMD); FFmpeg's own x86 asm stays +`--disable-asm`. (Corrects an earlier plan note that said SIMD was off.) + +## Artifacts + +| Artifact | Size | +|-------------------------------------------|---------| +| `packages/core/dist/umd/ffmpeg-core.wasm` | ~32.2 MB | +| `packages/core/dist/esm/ffmpeg-core.wasm` | ~32.2 MB | +| `packages/core/dist/{umd,esm}/ffmpeg-core.js` | ~112 KB | + +Target for the trimmed 7.1 build: smaller wasm after dropping x265/theora/vorbis. + +## Test results (baseline) + +Run via headless Chrome. MT suites require cross-origin isolation (COOP/COEP), served +here by a scratchpad COI server because the repo's `serve` script is broken (see gaps). + +| Suite | Result | Notes | +|--------------------------------|--------------|-------| +| `test:browser:core:st` | **12 passing** | ST core, full green | +| `test:browser:ffmpeg:st` | **11 passing** | ST wrapper, full green | +| `test:browser:ffmpeg:mt` | **11 passing** | **MT real-world path** (core runs in a Worker) — transcodes correctly | +| `test:browser:core:mt` | 8 passing / **4 failing** | ⚠️ pre-existing test-design limit — loads MT core on the page **main thread**; the 4 threaded ops fail with `Atomics.wait cannot be called in this context` (forbidden on a Window main thread by spec, unfixable by headers/flags) | +| `test:node:core:*` | ⚠️ pre-existing gap — UMD core needs browser globals (`self`, `location`); Node harness does not shim them | + +**Bottom line:** ST fully green; MT build functional and verified via the wrapper/Worker +path (the way it's actually used). The MT-core-direct and Node suites have pre-existing +harness limitations, not build defects. + +## Known baseline gaps (inform Phase 1) + +- **No transcode-correctness assertions** — tests only check exit code / non-empty / progress==1; a codec regression would pass silently. (Phase 1 adds PSNR/SSIM + stream-probe goldens.) +- **`serve` script is broken** — `package.json` `serve` passes `--headers '{...}'` to `http-server@14.1.1`, which has **no such option** (only `--cors`), so COOP/COEP headers are never sent and pages aren't cross-origin isolated. MT suites can't run against it. Phase 1 should replace it with a server that actually sets COOP/COEP (a minimal one is proven in `scratchpad/coi-server.js`). +- **MT core direct test unsupported by design** — `tests/ffmpeg-core-mt.test.html` runs the MT core on the page main thread; threaded ops hit `Atomics.wait` (illegal on main thread). Phase 1 fix: drive the core from a Worker in that test, or drop it in favor of the wrapper MT test. +- **Node test path broken** — needs `self`/`location`/`document` shims or an ESM-in-Node loader. +- **Build fragility on Apple Silicon** — amd64-only emsdk emulated; required 24 GiB Docker VM + `make -j4` cap (`build/ffmpeg.sh`) to avoid OOM. Phase 2 (native arm64 emsdk) retires this. diff --git a/Dockerfile b/Dockerfile index 51275c46464..800835a1a8e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,17 @@ # syntax=docker/dockerfile-upstream:master-labs # Base emsdk image with environment variables. -FROM emscripten/emsdk:3.1.40 AS emsdk-base +# Pinned to emscripten/emsdk:latest by manifest-list digest = Emscripten 6.0.2. +# Rationale: only the `latest` tag is multi-arch (arm64+amd64); every versioned +# emsdk tag is amd64-only, which forced slow, OOM-prone x86 emulation on Apple +# Silicon. Pinning the list digest keeps builds reproducible AND native per-arch. +FROM emscripten/emsdk@sha256:644883f58ca15c38c8be59b3a727ba0eff347729bc31d50a3348a6c9ed92bc07 AS emsdk-base ARG EXTRA_CFLAGS ARG EXTRA_LDFLAGS ARG FFMPEG_ST ARG FFMPEG_MT +ARG FFMPEG_VARIANT=full ENV INSTALL_DIR=/opt -# We cannot upgrade to n6.0 as ffmpeg bin only supports multithread at the moment. -ENV FFMPEG_VERSION=n5.1.4 ENV CFLAGS="-I$INSTALL_DIR/include $CFLAGS $EXTRA_CFLAGS" ENV CXXFLAGS="$CFLAGS" ENV LDFLAGS="-L$INSTALL_DIR/lib $LDFLAGS $CFLAGS $EXTRA_LDFLAGS" @@ -17,23 +20,18 @@ ENV EM_TOOLCHAIN_FILE=$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscript ENV PKG_CONFIG_PATH=$PKG_CONFIG_PATH:$EM_PKG_CONFIG_PATH ENV FFMPEG_ST=$FFMPEG_ST ENV FFMPEG_MT=$FFMPEG_MT +ENV FFMPEG_VARIANT=$FFMPEG_VARIANT RUN apt-get update && \ apt-get install -y pkg-config autoconf automake libtool ragel # Build x264 FROM emsdk-base AS x264-builder -ENV X264_BRANCH=4-cores -ADD https://github.com/ffmpegwasm/x264.git#$X264_BRANCH /src +# Pinned to the 4-cores branch HEAD by commit SHA (was a floating branch). +ENV X264_REF=33cac6b77d5b9259c552156013a817ab23119612 +ADD https://github.com/ffmpegwasm/x264.git#$X264_REF /src COPY build/x264.sh /src/build.sh RUN bash -x /src/build.sh -# Build x265 -FROM emsdk-base AS x265-builder -ENV X265_BRANCH=3.4 -ADD https://github.com/ffmpegwasm/x265.git#$X265_BRANCH /src -COPY build/x265.sh /src/build.sh -RUN bash -x /src/build.sh - # Build libvpx FROM emsdk-base AS libvpx-builder ENV LIBVPX_BRANCH=v1.13.1 @@ -43,26 +41,12 @@ RUN bash -x /src/build.sh # Build lame FROM emsdk-base AS lame-builder -ENV LAME_BRANCH=master -ADD https://github.com/ffmpegwasm/lame.git#$LAME_BRANCH /src +# Pinned to master HEAD by commit SHA (was a floating branch). +ENV LAME_REF=2badea1974ae36cb8312afe99cff1e6b3b5decee +ADD https://github.com/ffmpegwasm/lame.git#$LAME_REF /src COPY build/lame.sh /src/build.sh RUN bash -x /src/build.sh -# Build ogg -FROM emsdk-base AS ogg-builder -ENV OGG_BRANCH=v1.3.4 -ADD https://github.com/ffmpegwasm/Ogg.git#$OGG_BRANCH /src -COPY build/ogg.sh /src/build.sh -RUN bash -x /src/build.sh - -# Build theora -FROM emsdk-base AS theora-builder -COPY --from=ogg-builder $INSTALL_DIR $INSTALL_DIR -ENV THEORA_BRANCH=v1.1.1 -ADD https://github.com/ffmpegwasm/theora.git#$THEORA_BRANCH /src -COPY build/theora.sh /src/build.sh -RUN bash -x /src/build.sh - # Build opus FROM emsdk-base AS opus-builder ENV OPUS_BRANCH=v1.3.1 @@ -70,18 +54,12 @@ ADD https://github.com/ffmpegwasm/opus.git#$OPUS_BRANCH /src COPY build/opus.sh /src/build.sh RUN bash -x /src/build.sh -# Build vorbis -FROM emsdk-base AS vorbis-builder -COPY --from=ogg-builder $INSTALL_DIR $INSTALL_DIR -ENV VORBIS_BRANCH=v1.3.3 -ADD https://github.com/ffmpegwasm/vorbis.git#$VORBIS_BRANCH /src -COPY build/vorbis.sh /src/build.sh -RUN bash -x /src/build.sh - # Build zlib FROM emsdk-base AS zlib-builder -ENV ZLIB_BRANCH=v1.2.11 -ADD https://github.com/ffmpegwasm/zlib.git#$ZLIB_BRANCH /src +# zlib 1.3.1 from upstream (fixes CVE-2018-25032, CVE-2022-37434). The +# ffmpegwasm fork has no 1.3.1 tag; pinned to the v1.3.1 commit SHA. +ENV ZLIB_REF=925af44f3cde53c6b076611c297850091b5dc7bb +ADD https://github.com/madler/zlib.git#$ZLIB_REF /src COPY build/zlib.sh /src/build.sh RUN bash -x /src/build.sh @@ -93,37 +71,6 @@ ADD https://github.com/ffmpegwasm/libwebp.git#$LIBWEBP_BRANCH /src COPY build/libwebp.sh /src/build.sh RUN bash -x /src/build.sh -# Build freetype2 -FROM emsdk-base AS freetype2-builder -ENV FREETYPE2_BRANCH=VER-2-10-4 -ADD https://github.com/ffmpegwasm/freetype2.git#$FREETYPE2_BRANCH /src -COPY build/freetype2.sh /src/build.sh -RUN bash -x /src/build.sh - -# Build fribidi -FROM emsdk-base AS fribidi-builder -ENV FRIBIDI_BRANCH=v1.0.9 -ADD https://github.com/fribidi/fribidi.git#$FRIBIDI_BRANCH /src -COPY build/fribidi.sh /src/build.sh -RUN bash -x /src/build.sh - -# Build harfbuzz -FROM emsdk-base AS harfbuzz-builder -ENV HARFBUZZ_BRANCH=5.2.0 -ADD https://github.com/harfbuzz/harfbuzz.git#$HARFBUZZ_BRANCH /src -COPY build/harfbuzz.sh /src/build.sh -RUN bash -x /src/build.sh - -# Build libass -FROM emsdk-base AS libass-builder -COPY --from=freetype2-builder $INSTALL_DIR $INSTALL_DIR -COPY --from=fribidi-builder $INSTALL_DIR $INSTALL_DIR -COPY --from=harfbuzz-builder $INSTALL_DIR $INSTALL_DIR -ENV LIBASS_BRANCH=0.15.0 -ADD https://github.com/libass/libass.git#$LIBASS_BRANCH /src -COPY build/libass.sh /src/build.sh -RUN bash -x /src/build.sh - # Build zimg FROM emsdk-base AS zimg-builder ENV ZIMG_BRANCH=release-3.0.5 @@ -135,68 +82,37 @@ RUN bash -x /src/build.sh # Base ffmpeg image with dependencies and source code populated. FROM emsdk-base AS ffmpeg-base RUN embuilder build sdl2 sdl2-mt +# n8.1.2 (MT) or n5.1.10 (ST) — selects the FFmpeg tag AND which vendored +# fftools generation ffmpeg-wasm.sh compiles (src/fftools vs src/fftools-5.1). +# Declared here (not in emsdk-base, and below embuilder) so changing versions +# invalidates only the FFmpeg source ADD, not the version-independent +# dependency-builder and SDL2-port layers. +ARG FFMPEG_VERSION=n8.1.2 +ENV FFMPEG_VERSION=$FFMPEG_VERSION ADD https://github.com/FFmpeg/FFmpeg.git#$FFMPEG_VERSION /src COPY --from=x264-builder $INSTALL_DIR $INSTALL_DIR -COPY --from=x265-builder $INSTALL_DIR $INSTALL_DIR COPY --from=libvpx-builder $INSTALL_DIR $INSTALL_DIR COPY --from=lame-builder $INSTALL_DIR $INSTALL_DIR COPY --from=opus-builder $INSTALL_DIR $INSTALL_DIR -COPY --from=theora-builder $INSTALL_DIR $INSTALL_DIR -COPY --from=vorbis-builder $INSTALL_DIR $INSTALL_DIR COPY --from=libwebp-builder $INSTALL_DIR $INSTALL_DIR -COPY --from=libass-builder $INSTALL_DIR $INSTALL_DIR COPY --from=zimg-builder $INSTALL_DIR $INSTALL_DIR # Build ffmpeg FROM ffmpeg-base AS ffmpeg-builder COPY build/ffmpeg.sh /src/build.sh -RUN bash -x /src/build.sh \ - --enable-gpl \ - --enable-libx264 \ - --enable-libx265 \ - --enable-libvpx \ - --enable-libmp3lame \ - --enable-libtheora \ - --enable-libvorbis \ - --enable-libopus \ - --enable-zlib \ - --enable-libwebp \ - --enable-libfreetype \ - --enable-libfribidi \ - --enable-libass \ - --enable-libzimg +# Codec --enable flags are selected inside build.sh by $FFMPEG_VARIANT. +RUN bash -x /src/build.sh # Build ffmpeg.wasm FROM ffmpeg-builder AS ffmpeg-wasm-builder COPY src/bind /src/src/bind COPY src/fftools /src/src/fftools +COPY src/fftools-5.1 /src/src/fftools-5.1 COPY build/ffmpeg-wasm.sh build.sh -# libraries to link -ENV FFMPEG_LIBS \ - -lx264 \ - -lx265 \ - -lvpx \ - -lmp3lame \ - -logg \ - -ltheora \ - -lvorbis \ - -lvorbisenc \ - -lvorbisfile \ - -lopus \ - -lz \ - -lwebpmux \ - -lwebp \ - -lsharpyuv \ - -lfreetype \ - -lfribidi \ - -lharfbuzz \ - -lass \ - -lzimg +# Codec link libs are selected inside build.sh by $FFMPEG_VARIANT. RUN mkdir -p /src/dist/umd && bash -x /src/build.sh \ - ${FFMPEG_LIBS} \ -o dist/umd/ffmpeg-core.js RUN mkdir -p /src/dist/esm && bash -x /src/build.sh \ - ${FFMPEG_LIBS} \ -sEXPORT_ES6 \ -o dist/esm/ffmpeg-core.js diff --git a/FORK.md b/FORK.md new file mode 100644 index 00000000000..2a9e5bd40c4 --- /dev/null +++ b/FORK.md @@ -0,0 +1,126 @@ +# FloSports fork — FFmpeg 8.1.2 (MT) + 5.1.10 (ST) + +This is a hardened fork of ffmpeg.wasm (upstream is on 5.1.4). The primary +cores are **FFmpeg 8.1.2, MT-only**; the branch also builds **ST 5.1.10** +cores for surfaces that cannot serve cross-origin isolation headers (see the +MT-only bullet below). It targets an internal **video clipping** use case: +cut, transcode, and losslessly stitch clips in the browser. Built for +**vendoring** (not npm-published). + +## What changed vs upstream +- **FFmpeg 5.1.4 → 8.1.2**; **Emscripten 3.1.40 → 6.0.2** (digest-pinned, native arm64 + amd64). +- **Lean codec set**: kept x264, vpx, opus, mp3lame, webp, zimg + native AAC. + Dropped x265, theora, vorbis/ogg, and the subtitle/text stack (libass, freetype, + fribidi, harfbuzz) — no subtitles/text overlays. Core is **~25.3 MB** (was 32.7). +- **MT-only on 8.x.** fftools requires threads from FFmpeg **6.0** onward + (`ffmpeg_deps` gains `threads`; `pthread_create` is unconditional in + demux/mux), so no single-threaded core exists past the 5.1 branch. The MT + core requires cross-origin isolation (see Headers below). This branch also + builds **ST 5.1.10** cores from the same hardened dep pins — they need **no** + isolation headers and support multi-input filtergraphs (see capability map): + `make build-st` → `packages/core` (full, ~22.8 MB) and `make build-st-copy` + → `packages/core-copy` (**2.6 MB**, stream-copy only: mov/mpegts/concat in, + mp4 out, no encoders — sized to clipping-tool-ui's two `-c copy` commands). + Vetting evidence is in-repo: `tests/ffmpeg-cliptool.test.js` (the two + consumer commands verbatim, ST/copy/MT lanes), + `tests/ffmpeg-multiinput.test.js` (the filtergraph probe), and + `tests/ffmpeg-perf.test.js` (cross-engine timings). The standing decision + this re-opens is `docs/adr/0001-mt-only-core.md`. +- Supply chain: floating lib branches (x264, lame) pinned to commit SHAs; zlib bumped + to **1.3.1** (CVE-2018-25032, CVE-2022-37434) from upstream. +- **8.x frontend port**: the vendored `src/fftools` frontend was re-based onto 8.1.2. + vs 7.1: `objpool.c` was dropped (`thread_queue` now uses `libavutil/container_fifo`), + ffprobe's writers moved into `textformat/*`, and `libpostproc` is no longer linked. + `graph/graphprint.c` (the `-print_graphs` diagnostic) is a fork **no-op stub** — + upstream needs the `resources/resman` resource-bundling pipeline, which this + hand-rolled build doesn't reproduce; the option parses but produces no output. + +## Release variants + +Each release ships two MT cores and one ST core. Pick the smallest that +covers your pipeline. (The **full** ST 5.1.10 core stays a local `make prd` +build — CI builds it as the cliptool-suite synthesizer but does not attach +it to releases.) + +| Variant | Build | Wasm size | Vendor asset | Use when | +|-------------|-------|-----------|--------------|----------| +| **full** | all lean codecs enabled (x264, vpx, opus, mp3lame, webp, zimg + zlib + native AAC) | ~25.3 MB | `ffmpeg-core-mt-.tgz` | general use / unknown codec needs | +| **slim** | `--disable-everything` + an allowlist for exactly one pipeline: H.264/AAC over mp4/ts | **~6 MB** | `ffmpeg-core-mt-slim-.tgz` | H.264+AAC only: stream-copy clip/concat, single-input x264 re-encode, re-encode stitch | +| **st-copy** | ST 5.1.10, `--disable-everything` + stream-copy allowlist (mov/mpegts/concat in, mp4 out, no encoders) | **~2.6 MB** | `ffmpeg-core-st-copy-.tgz` (from v0.15.1) | `-c copy` clip export + stitch with **no COOP/COEP** requirement | + +All variants are identical at the wrapper/ABI level — same `_ffmpeg`/`_ffprobe` +ABI, same postMessage contract. The MT cores share the 8.1.2 scheduler +frontend; st-copy runs the 5.1 sequential frontend and loads with +`thread: false` and no isolation headers. Only the compiled-in component set +differs otherwise. + +**Slim is `--disable-everything` + an allowlist** (see `build/ffmpeg.sh`): it +strips ~all of FFmpeg's ~400 decoders / 350 demuxers / 130 filters and re-enables +only the H.264/AAC + mp4/ts + concat components its consumer uses. That's where +the 25 MB → 6 MB drop comes from — not the external codec libs (dropping those +alone only saved ~3 MB). The trade-off is precision: slim **hangs** (does not +error cleanly) if asked for a component it wasn't built with, so it must only be +driven with its supported operations. Its gate is `tests/ffmpeg-slim.test.js` +(run via `npm run test:browser:ffmpeg:slim`), not the generic suite (whose +mp4→avi transcode needs the avi muxer/mpeg4 encoder slim drops). + +`live-clipping-poc` vendors **slim**. + +## Capability map (what works in the browser) + +The dividing line is **single-input vs multi-input filtergraph**, not copy vs +re-encode: + +| Operation | Status | +|-----------|--------| +| Cut / trim (`-ss`/`-t` `-c copy`) | ✅ | +| Lossless concat (`-f concat -c copy`) | ✅ | +| Single-input re-encode (`-c:v libx264 -c:a aac`) | ✅ | +| Re-encode **stitch** via concat *demuxer* (`-f concat -i list -c:v libx264 …`) | ✅ single input → one out | +| **Multi-input filtergraph** — `overlay` (watermark), `xfade` (softened transitions), concat *filter* | ❌ deadlocks on **MT 8.x** — ✅ works on **ST 5.1** | + +The multi-input deadlock is a scheduler-in-wasm limitation of the 7.x/8.x +thread-based frontend (not thread-count), confirmed on 8.1.2 — and confirmed +**engine-specific** on 2026-08-13: the same `overlay`/`xfade`/concat-filter +graphs complete in milliseconds on the pre-scheduler ST 5.1.10 core +(`tests/ffmpeg-multiinput.test.js`, `npm run test:st:probe`). The concat +*demuxer* feeds the encoder as one stream (works everywhere); the concat +*filter* / `xfade` / `overlay` open several inputs into one graph, which the +8.x scheduler cannot drain in wasm. **Design consequence, per engine:** on the +MT 8.x core, do clip + stitch in the browser and produce watermarked output and +cross-faded transitions server-side; on the ST 5.1.10 core, all of the above +run client-side. + +## Required headers (MT / SharedArrayBuffer) +Serve the app's HTML with: +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp # or: credentialless (if loading cross-origin video) +``` +Without these, the MT core will not load. + +## Usage (single long-lived instance) +Create **one** instance, load once, reuse it for every operation (creating/terminating +many instances leaks pthread workers under emsdk 6.0.2): +```js +import { FFmpeg } from "@ffmpeg/ffmpeg"; +const ffmpeg = new FFmpeg(); +await ffmpeg.load({ coreURL: "/assets/ffmpeg-core.js", thread: true }); // your vendored path +await ffmpeg.writeFile("in.mp4", data); +await ffmpeg.exec(["-ss", "1.5", "-to", "4.0", "-i", "in.mp4", "-c", "copy", "out.mp4"]); +const out = await ffmpeg.readFile("out.mp4"); +``` + +## Build +``` +make prd-mt # builds packages/core-mt/dist (native; requires Docker) +npm ci && npm run build# builds the @ffmpeg/ffmpeg + @ffmpeg/util JS wrappers +npm test # 12/12 MT suite (headless Chrome) +``` +Apple Silicon note: builds run native arm64 on emsdk 6.0.2. `build/ffmpeg.sh` caps +`make -j` (FFMPEG_JOBS, default 4) to avoid OOM; a ~24 GiB Docker VM is recommended. + +## Vendoring from a targeted release +Push a `v*` tag → CI builds and attaches `ffmpeg-core-mt-.tgz` (the core: +`ffmpeg-core.js`, `.wasm`, `.worker.js`) and `ffmpeg-wasm-.tgz` (the wrapper) +to the GitHub Release. Vendor those into the app and serve them same-origin. diff --git a/Makefile b/Makefile index 8e260bc5bbe..c2496c2a47a 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,11 @@ all: dev +# FFmpeg version per threading model. fftools requires threads from 6.0 onward +# (ffmpeg_deps gains `threads`; pthread_create is unconditional in demux/mux), +# so the ST core pins the last single-thread-capable LTS branch, 5.1. +FFMPEG_VERSION ?= n8.1.2 +ST_FFMPEG_VERSION := n5.1.10 + MT_FLAGS := -sUSE_PTHREADS -pthread DEV_ARGS := --progress=plain @@ -14,28 +20,52 @@ clean: .PHONY: build build: +# An empty FFMPEG_VERSION would override the Dockerfile ARG default and turn +# the FFmpeg ADD ref into a bare `#`, silently fetching the default branch. +ifeq ($(strip $(FFMPEG_VERSION)),) + $(error FFMPEG_VERSION is empty; expected an FFmpeg tag such as n8.1.2) +endif make clean PKG_SUFFIX="$(PKG_SUFFIX)" EXTRA_CFLAGS="$(EXTRA_CFLAGS)" \ EXTRA_LDFLAGS="$(EXTRA_LDFLAGS)" \ FFMPEG_ST="$(FFMPEG_ST)" \ FFMPEG_MT="$(FFMPEG_MT)" \ + FFMPEG_VARIANT="$(FFMPEG_VARIANT)" \ + FFMPEG_VERSION="$(FFMPEG_VERSION)" \ docker buildx build \ --build-arg EXTRA_CFLAGS \ --build-arg EXTRA_LDFLAGS \ --build-arg FFMPEG_MT \ --build-arg FFMPEG_ST \ + --build-arg FFMPEG_VARIANT \ + --build-arg FFMPEG_VERSION \ -o ./packages/core$(PKG_SUFFIX) \ $(EXTRA_ARGS) \ . build-st: make build \ - FFMPEG_ST=yes + FFMPEG_ST=yes \ + FFMPEG_VERSION=$(ST_FFMPEG_VERSION) + +build-st-copy: + make build \ + PKG_SUFFIX=-copy \ + FFMPEG_ST=yes \ + FFMPEG_VERSION=$(ST_FFMPEG_VERSION) \ + FFMPEG_VARIANT=copy build-mt: make build \ PKG_SUFFIX=-mt \ - FFMPEG_MT=yes + FFMPEG_MT=yes \ + FFMPEG_VARIANT=full + +build-mt-slim: + make build \ + PKG_SUFFIX=-mt-slim \ + FFMPEG_MT=yes \ + FFMPEG_VARIANT=slim dev: make build-st EXTRA_CFLAGS="$(DEV_CFLAGS)" EXTRA_ARGS="$(DEV_ARGS)" @@ -46,5 +76,11 @@ dev-mt: prd: make build-st EXTRA_CFLAGS="$(PROD_CFLAGS)" +prd-st-copy: + make build-st-copy EXTRA_CFLAGS="$(PROD_CFLAGS)" + prd-mt: make build-mt EXTRA_CFLAGS="$(PROD_MT_CFLAGS)" + +prd-mt-slim: + make build-mt-slim EXTRA_CFLAGS="$(PROD_MT_CFLAGS)" diff --git a/build/ffmpeg-wasm.sh b/build/ffmpeg-wasm.sh index faa2f725ca8..a27a0c697f5 100755 --- a/build/ffmpeg-wasm.sh +++ b/build/ffmpeg-wasm.sh @@ -7,51 +7,144 @@ set -euo pipefail EXPORT_NAME="createFFmpegCore" +# fftools generation by FFmpeg version. The two frontends share no threading +# code: 5.1 is the classic sequential transcode loop (last ST-capable +# generation, 8 files); 8.x is the thread-per-stage scheduler frontend. +case "${FFMPEG_VERSION:?FFMPEG_VERSION must be set (the Dockerfile exports it); refusing to guess an fftools generation}" in + n5.*) + VERSION_FLAGS=( + -I./src/fftools-5.1 + ) + # 5.1 builds libpostproc only under --enable-gpl (removed upstream in + # 8.x); the copy variant is GPL-free, so link it only when it was built. + if [ -f libpostproc/libpostproc.a ]; then + VERSION_FLAGS+=(-Llibpostproc -lpostproc) + fi + FFTOOLS_SRCS=( + src/fftools-5.1/cmdutils.c + src/fftools-5.1/ffmpeg.c + src/fftools-5.1/ffmpeg_filter.c + src/fftools-5.1/ffmpeg_hw.c + src/fftools-5.1/ffmpeg_mux.c + src/fftools-5.1/ffmpeg_opt.c + src/fftools-5.1/opt_common.c + src/fftools-5.1/ffprobe.c + ) + ;; + *) + VERSION_FLAGS=( + -I./src/fftools + -I./compat/stdbit # FFmpeg 7.x/8.x fftools use C23 ; emsdk lacks it, use FFmpeg's compat fallback + ) + # FFmpeg 8.x fftools: scheduler-based frontend = + # ffmpeg_dec/enc/demux/mux_init/sched + sync_queue/thread_queue. vs 7.x: + # objpool.c was dropped (thread_queue now uses libavutil/container_fifo); + # ffprobe's writers were extracted into textformat/*; graph/graphprint.c is + # a fork stub (upstream needs the resources/resman resource-bundling pipeline). + FFTOOLS_SRCS=( + src/fftools/cmdutils.c + src/fftools/ffmpeg.c + src/fftools/ffmpeg_dec.c + src/fftools/ffmpeg_demux.c + src/fftools/ffmpeg_enc.c + src/fftools/ffmpeg_filter.c + src/fftools/ffmpeg_hw.c + src/fftools/ffmpeg_mux.c + src/fftools/ffmpeg_mux_init.c + src/fftools/ffmpeg_opt.c + src/fftools/ffmpeg_sched.c + src/fftools/graph/graphprint.c + src/fftools/opt_common.c + src/fftools/sync_queue.c + src/fftools/thread_queue.c + src/fftools/textformat/avtextformat.c + src/fftools/textformat/tf_compact.c + src/fftools/textformat/tf_default.c + src/fftools/textformat/tf_flat.c + src/fftools/textformat/tf_ini.c + src/fftools/textformat/tf_json.c + src/fftools/textformat/tf_mermaid.c + src/fftools/textformat/tf_xml.c + src/fftools/textformat/tw_avio.c + src/fftools/textformat/tw_buffer.c + src/fftools/textformat/tw_stdout.c + src/fftools/ffprobe.c + ) + ;; +esac + CONF_FLAGS=( - -I. - -I./src/fftools - -I$INSTALL_DIR/include - -L$INSTALL_DIR/lib - -Llibavcodec - -Llibavdevice - -Llibavfilter - -Llibavformat - -Llibavutil - -Llibpostproc - -Llibswresample - -Llibswscale - -lavcodec - -lavdevice - -lavfilter - -lavformat - -lavutil - -lpostproc - -lswresample - -lswscale - -Wno-deprecated-declarations - $LDFLAGS + -I. + "${VERSION_FLAGS[@]}" + -I$INSTALL_DIR/include + -L$INSTALL_DIR/lib + -Llibavcodec + -Llibavdevice + -Llibavfilter + -Llibavformat + -Llibavutil + -Llibswresample + -Llibswscale + -lavcodec + -lavdevice + -lavfilter + -lavformat + -lavutil + -lswresample + -lswscale + -Wno-deprecated-declarations + $LDFLAGS -sENVIRONMENT=worker -sWASM_BIGINT # enable big int support -sUSE_SDL=2 # use emscripten SDL2 lib port -sSTACK_SIZE=5MB # increase stack size to support libopus -sMODULARIZE # modularized to use as a library ${FFMPEG_MT:+ -sINITIAL_MEMORY=1024MB} # ALLOW_MEMORY_GROWTH is not recommended when using threads, thus we use a large initial memory - ${FFMPEG_MT:+ -sPTHREAD_POOL_SIZE=32} # use 32 threads + ${FFMPEG_MT:+ -sPTHREAD_POOL_SIZE=32} # scheduler threads + per-codec frame-threading (capped to 8 cores post-build) fit in 32; overflow pthread_create deadlocks in the worker. 32 also keeps multi-instance worker count sane. ${FFMPEG_ST:+ -sINITIAL_MEMORY=32MB -sALLOW_MEMORY_GROWTH} # Use just enough memory as memory usage can grow -sEXPORT_NAME="$EXPORT_NAME" # required in browser env, so that user can access this module from window object -sEXPORTED_FUNCTIONS=$(node src/bind/ffmpeg/export.js) # exported functions -sEXPORTED_RUNTIME_METHODS=$(node src/bind/ffmpeg/export-runtime.js) # exported built-in functions -lworkerfs.js --pre-js src/bind/ffmpeg/bind.js # extra bindings, contains most of the ffmpeg.wasm javascript code - # ffmpeg source code - src/fftools/cmdutils.c - src/fftools/ffmpeg.c - src/fftools/ffmpeg_filter.c - src/fftools/ffmpeg_hw.c - src/fftools/ffmpeg_mux.c - src/fftools/ffmpeg_opt.c - src/fftools/opt_common.c - src/fftools/ffprobe.c + "${FFTOOLS_SRCS[@]}" ) -emcc "${CONF_FLAGS[@]}" $@ +# Codec link libs by variant (default full). Mirrors the --enable set chosen in +# build/ffmpeg.sh. -lz is always present (PNG/zlib). Slim links only x264 + z. +case "${FFMPEG_VARIANT:-full}" in + slim) + FFMPEG_LIBS=(-lx264 -lz) + ;; + full) + FFMPEG_LIBS=(-lx264 -lvpx -lmp3lame -lopus -lz -lwebpmux -lwebp -lsharpyuv -lzimg) + ;; + copy) + # Stream-copy-only: no external codec libs at all. + FFMPEG_LIBS=() + ;; + *) + echo "ffmpeg-wasm build: unknown FFMPEG_VARIANT='${FFMPEG_VARIANT:-}'" >&2 + exit 1 + ;; +esac + +emcc "${CONF_FLAGS[@]}" "${FFMPEG_LIBS[@]}" $@ + +# Post-build patches to the emscripten output. Target only the -o file just +# built (this script runs once per variant) so patches aren't double-applied. +OUT=$(echo "$@" | sed -n 's/.*-o \([^ ]*\.js\).*/\1/p') +if [ -n "$OUT" ] && [ -f "$OUT" ]; then + # (1) emsdk 6.0.2 spawns pthread workers from `_scriptName` — the URL of the + # script that ran importScripts() (the @ffmpeg/ffmpeg wrapper worker), NOT the + # core — so pthread workers load the wrong script and load() hangs. Restore + # emscripten's mainScriptUrlOrBlob override (the wrapper sets it to the core URL). + sed -i 's/var pthreadMainJs=_scriptName/var pthreadMainJs=Module["mainScriptUrlOrBlob"]||_scriptName/g' "$OUT" + + # (2) Cap the core count FFmpeg auto-detects (av_cpu_count -> sysconf -> + # navigator.hardwareConcurrency). Uncapped, the 7.x scheduler + per-codec + # frame-threading requests ~2*cores threads and overflows the pthread pool, + # deadlocking mid-transcode; capping keeps a transcode within a modest pool + # and keeps per-instance worker count low. Explicit -threads still overrides. + sed -i 's/navigator\["hardwareConcurrency"\]/Math.min(navigator["hardwareConcurrency"]||8,8)/g' "$OUT" +fi diff --git a/build/ffmpeg.sh b/build/ffmpeg.sh index da89c451aea..1e65216cd15 100755 --- a/build/ffmpeg.sh +++ b/build/ffmpeg.sh @@ -2,6 +2,13 @@ set -euo pipefail +# Strip any shared libs from the dep prefix so FFmpeg (and the downstream core +# link, which inherits this stage) links everything statically. Some deps build +# a .so despite BUILD_SHARED_LIBS=OFF (e.g. zlib's CMake always emits libz.so), +# and emsdk 6.0.2's -l prefers the .so, turning it into a runtime dlopen +# (e.g. "404: libz.so") in a build that must be self-contained. +rm -f "$INSTALL_DIR"/lib/*.so "$INSTALL_DIR"/lib/*.so.* + CONF_FLAGS=( --target-os=none # disable target specific configs --arch=x86_32 # use x86_32 arch @@ -22,12 +29,80 @@ CONF_FLAGS=( --cxx=em++ --objcc=emcc --dep-cc=emcc - --extra-cflags="$CFLAGS" - --extra-cxxflags="$CXXFLAGS" + # -fPIC scoped to FFmpeg's build only. emsdk 6.0.2's wasm-ld rejects + # table-index relocations against function symbols in non-PIC objects + # (R_WASM_TABLE_INDEX_SLEB), which makes ./configure's lib-detection probes + # fail ("zlib requested but not found"). Keeping it here (not global) means + # the external libs stay static .a and the final core links statically — + # applying it globally made emscripten emit a MAIN_MODULE that tried to + # dlopen libz.so at runtime. + --extra-cflags="$CFLAGS -fPIC" + --extra-cxxflags="$CXXFLAGS -fPIC" # disable thread when FFMPEG_ST is NOT defined ${FFMPEG_ST:+ --disable-pthreads --disable-w32threads --disable-os2threads} ) -emconfigure ./configure "${CONF_FLAGS[@]}" $@ -emmake make -j +# Codec set by variant (default full). Common to both: --enable-gpl (x264 is +# GPL) and --enable-zlib (load-bearing — FFmpeg's PNG decoder needs zlib, and +# the watermark is a PNG overlay). Slim keeps only x264 on top of that; full +# adds the rest of the lean set. +CODEC_FLAGS=(--enable-gpl --enable-zlib --enable-libx264) +case "${FFMPEG_VARIANT:-full}" in + slim) + # Aggressive size trim: disable ALL native components, then re-enable only + # what live-clipping-poc's pipeline needs — H.264/AAC over mp4/ts (stream- + # copy clip + concat), x264/AAC re-encode, and PNG-overlay watermark. + # --disable-everything must precede the --enable-*, so redefine CODEC_FLAGS. + CODEC_FLAGS=( + --disable-everything + --enable-gpl + --enable-zlib + --enable-libx264 + --enable-protocol=file,pipe,data,concat,concatf + --enable-demuxer=mov,mpegts,concat,image2,png_pipe + --enable-muxer=mp4,mov,null + --enable-decoder=h264,aac,png + --enable-encoder=libx264,aac + --enable-parser=h264,aac,png + --enable-bsf=h264_mp4toannexb,aac_adtstoasc,extract_extradata,null + --enable-filter=overlay,scale,format,null,copy,aformat,anull,aresample,fps,setpts,asetpts,buffer,buffersink,abuffer,abuffersink + ) + ;; + full) + CODEC_FLAGS+=( + --enable-libvpx + --enable-libmp3lame + --enable-libopus + --enable-libwebp + --enable-libzimg + ) + ;; + copy) + # Stream-copy-only trim: the clipping-tool-ui pipeline is exactly two + # `-c copy` commands (clip export from TS segments + mp4 stitch), so no + # encoders, no external codec libs, no GPL, no image/subtitle stack. + # Decoders h264/aac stay for avformat_find_stream_info probing only. + CODEC_FLAGS=( + --disable-everything + --enable-protocol=file,pipe,data + --enable-demuxer=mov,mpegts,concat + # null muxer/bsf: kept for discard-output probe/diagnostic runs + # (`-f null -`); they cost ~nothing and removing them breaks debugging. + --enable-muxer=mp4,null + --enable-decoder=h264,aac + --enable-parser=h264,aac + --enable-bsf=h264_mp4toannexb,aac_adtstoasc,extract_extradata,null + ) + ;; + *) + echo "ffmpeg build: unknown FFMPEG_VARIANT='${FFMPEG_VARIANT:-}'" >&2 + exit 1 + ;; +esac + +emconfigure ./configure "${CONF_FLAGS[@]}" "${CODEC_FLAGS[@]}" $@ +# Cap parallelism: unbounded `make -j` spawns one clang per libavfilter TU and +# OOM-kills individual compilers when the toolchain runs under x86 emulation +# (amd64-only emsdk on arm64 hosts). Override with FFMPEG_JOBS if desired. +emmake make -j"${FFMPEG_JOBS:-4}" diff --git a/build/fribidi.sh b/build/fribidi.sh index 27ed9994a8e..39754929bca 100755 --- a/build/fribidi.sh +++ b/build/fribidi.sh @@ -11,6 +11,9 @@ CONF_FLAGS=( --disable-debug ) emconfigure ./autogen.sh "${CONF_FLAGS[@]}" -# A hacky to fix "Too many symbolic links" error -emmake make install -j || true +# Install serially (no -j): fribidi's c2man man-page generation is broken under +# emsdk 6.0.2 and races the library install under -j, intermittently failing +# before libfribidi.a/headers land. Serial install does the lib subdir first, +# so the (ignored) doc failure can't clobber it. +emmake make install || true mkdir -p $INSTALL_DIR/lib/pkgconfig && cp fribidi.pc $INSTALL_DIR/lib/pkgconfig/ diff --git a/docs/adr/0001-mt-only-core.md b/docs/adr/0001-mt-only-core.md new file mode 100644 index 00000000000..9e0eacc6bc7 --- /dev/null +++ b/docs/adr/0001-mt-only-core.md @@ -0,0 +1,78 @@ +# MT-only FFmpeg core (no single-threaded build on 8.x) + +- **Status**: Accepted for the 8.x MT core; the "reject ST" reasoning is + **under re-evaluation** as of 2026-08-13. Two bullets below have since been + falsified: an ST core no longer means downgrading to upstream 5.1.4 (the + fork now builds ST **5.1.10** from its own hardened pins — the ST cutoff is + after 5.1, not 8.x: fftools requires threads from 6.0 onward), and the + multi-input filtergraph deadlock does **not** reproduce on the + pre-scheduler ST core (`tests/ffmpeg-multiinput.test.js` shows + `overlay`/`xfade`/concat-filter completing). See the FORK.md capability + map. A superseding ADR follows if an ST core ships. +- **Date**: 2026-07-07 + +## Context + +This fork moved FFmpeg from 5.1.4 to 8.1.2, whose `fftools` CLI frontend runs a +thread-per-stage scheduler (`thread_queue` on `libavutil/container_fifo`) and is +thread-based by design — there is no viable single-threaded 8.1.2 core. Upstream +ffmpeg.wasm publishes both a single-threaded `@ffmpeg/core` and a multi-threaded +`@ffmpeg/core-mt`, and on 5.1.4 the single-threaded core is a valid way to avoid the +cross-origin isolation the MT core needs (COOP/COEP + `SharedArrayBuffer`). A proposal +surfaced to "switch to the single-threaded core" to shed that isolation requirement; it +conflates threading (a wasm build detail) with cross-origin isolation (a deployment +header posture), and the upstream heuristic does not carry to 8.x. + +## Decision + +Ship an **MT-only** core (`@ffmpeg/core-mt`) that requires cross-origin isolation, and +treat isolation as an infrastructure/header concern solved at the serving layer. Reject +adopting a single-threaded core, for reasons that are non-obvious and worth recording: + +- **No single-threaded 8.x core exists here.** A `build-st`/`prd-st` target survives in + the `Makefile` (inherited from upstream; `FFMPEG_ST` gates + `--disable-pthreads --disable-w32threads --disable-os2threads` in `build/ffmpeg.sh`), + but CI only runs `prd-mt` and `prd-mt-slim` (`.github/workflows/CI.yml`), `packages/core/dist` + is empty, and no ST core is built or shipped. +- **"Switch to ST" means "downgrade the engine to 5.1.4."** The only real single-threaded + `@ffmpeg/core` is upstream's 0.12.10 = FFmpeg 5.1.4 (what `tests/test-helper-st.js` + resolves to). Taking it re-inherits the CVEs this fork patched (zlib 1.3.1, pinned + x264/lame SHAs) and discards the lean/slim codec allowlist and characterized capability + map. +- **ST does not fix the deadlock people hope it fixes.** Multi-input filtergraphs + (`overlay`, `xfade`, concat *filter*) deadlock as a wasm-scheduler limitation, not a + thread-count one (confirmed on 8.1.2 in `FORK.md`). Single-threading changes nothing here. +- **ST is materially slower** (~2×+ on the browser re-encode path per upstream perf docs), + which is the already-slow part of the clipping workflow. + +Cross-origin isolation is handled where it belongs: set COOP/COEP at the serving layer, +prefer `credentialless` when the page loads cross-origin source video, and keep clipping +on a dedicated isolated route to bound COEP `require-corp`'s effect on third-party +subresources. `live-clipping-poc` already ships `COOP: same-origin` + `COEP: credentialless`. + +## Consequences + +### Pros +- Keeps the hardened 8.1.2 engine — CVE fixes, pinned supply-chain SHAs, and the slim + (`--disable-everything` + allowlist) codec set. +- Single ABI and test surface: MT-only, gated by `tests/ffmpeg-slim.test.js` (and the MT + suite), with no second single-threaded path to maintain across two FFmpeg majors. +- The isolation requirement is already satisfied in production, so no consumer-facing + regression. + +### Cons +- Consumers must serve COOP/COEP and run cross-origin isolated to load the core. +- COEP `require-corp` blocks cross-origin subresources (third-party images, ads, analytics, + embeds) unless they send CORP/CORS; `credentialless` relaxes this for no-CORS resources + at the cost of un-credentialed fetches (signed-cookie CDN paths won't authenticate). +- No single-threaded fallback exists for a surface that genuinely cannot be isolated; such + a case must run the operation server-side rather than downgrade the core. + +## Links +- `FORK.md` — MT-only rationale, the 8.x threaded-frontend requirement, required headers, + and the multi-input filtergraph deadlock capability map. +- `build/ffmpeg.sh` (`FFMPEG_ST` → `--disable-pthreads` gating), `Makefile` + (`build-st`/`build-mt`/`build-mt-slim` targets), `.github/workflows/CI.yml` + (`prd-mt`/`prd-mt-slim` only). +- `tests/test-helper-st.js` / `packages/core` — the single-threaded path resolves to + upstream `@ffmpeg/core@0.12.10` (FFmpeg 5.1.4). diff --git a/package.json b/package.json index a46a3223ff0..1fce1d6a9b5 100644 --- a/package.json +++ b/package.json @@ -7,14 +7,28 @@ "lint:root": "eslint tests", "build": "npm run build --workspace=packages --if-present", "pretest": "npm run build", - "serve": "http-server -c-1 -s -p 3000 . --cors --headers '{\"Cross-Origin-Embedder-Policy\":\"require-corp\",\"Cross-Origin-Opener-Policy\":\"same-origin\",\"Cross-Origin-Resource-Policy\":\"cross-origin\",\"Origin-Agent-Cluster\":\"?1\"}'", + "serve": "node scripts/serve.js 3000 .", + "serve:no-coi": "node scripts/serve.js 3001 . --no-coi", "test": "server-test test:browser:server 3000 test:all", - "test:all": "npm-run-all test:browser:*:*", - "test:browser": "mocha-headless-chrome -a enable-features=SharedArrayBuffer", + "test:st": "server-test serve:no-coi 3001 test:browser:ffmpeg:st", + "test:st:probe": "server-test serve:no-coi 3001 test:browser:ffmpeg:st:probe", + "test:browser:ffmpeg:st:probe": "npm run test:browser -- -t 130000 -f http://localhost:3001/tests/ffmpeg-multiinput-st.test.html", + "test:cliptool": "server-test serve:no-coi 3001 test:browser:cliptool", + "test:cliptool:copy": "server-test serve:no-coi 3001 test:browser:cliptool:copy", + "test:cliptool:mt": "server-test test:browser:server 3000 test:browser:cliptool:mt", + "test:browser:cliptool": "npm run test:browser -- -t 130000 -f http://localhost:3001/tests/ffmpeg-cliptool-st.test.html", + "test:browser:cliptool:copy": "npm run test:browser -- -t 130000 -f http://localhost:3001/tests/ffmpeg-cliptool-copy.test.html", + "test:browser:cliptool:mt": "npm run test:browser -- -t 130000 -f http://localhost:3000/tests/ffmpeg-cliptool-mt.test.html", + "test:perf:st": "server-test serve:no-coi 3001 test:browser:perf:st", + "test:perf:mt": "server-test test:browser:server 3000 test:browser:perf:mt", + "test:browser:perf:st": "npm run test:browser -- -t 310000 -f http://localhost:3001/tests/ffmpeg-perf-st.test.html", + "test:browser:perf:mt": "npm run test:browser -- -t 310000 -f http://localhost:3000/tests/ffmpeg-perf-mt.test.html", + "test:all": "npm run test:browser:ffmpeg:mt", + "test:browser": "mocha-headless-chrome -a no-sandbox -a enable-features=SharedArrayBuffer", "test:browser:core:mt": "npm run test:browser -- -f http://localhost:3000/tests/ffmpeg-core-mt.test.html", - "test:browser:core:st": "npm run test:browser -- -f http://localhost:3000/tests/ffmpeg-core-st.test.html", "test:browser:ffmpeg:mt": "npm run test:browser -- -f http://localhost:3000/tests/ffmpeg-mt.test.html", - "test:browser:ffmpeg:st": "npm run test:browser -- -f http://localhost:3000/tests/ffmpeg-st.test.html", + "test:browser:ffmpeg:slim": "npm run test:browser -- -f http://localhost:3000/tests/ffmpeg-slim-mt.test.html", + "test:browser:ffmpeg:st": "npm run test:browser -- -f http://localhost:3001/tests/ffmpeg-st.test.html", "test:browser:server": "npm run serve", "test:node": "mocha --exit --bail -t 60000", "test:node:core:mt": "npm run test:node -- --require tests/test-helper-mt.js tests/ffmpeg-core.test.js", diff --git a/scripts/serve.js b/scripts/serve.js new file mode 100644 index 00000000000..2c421673f40 --- /dev/null +++ b/scripts/serve.js @@ -0,0 +1,93 @@ +// Minimal static file server that sends the cross-origin isolation headers +// (COOP/COEP/CORP) required for SharedArrayBuffer — which the multithreaded +// ffmpeg-core needs. `http-server` has no header support (its --headers flag is +// a silent no-op), so the previous `serve` script never actually sent these and +// the MT tests could not run. Dependency-free (Node built-ins only). +// +// Usage: node scripts/serve.js [port] [rootDir] [--no-coi] +// +// --no-coi omits COOP/COEP so the page is NOT cross-origin isolated. The +// single-threaded core exists to run without isolation; its test lane must use +// this mode or it only ever proves the core works under the very headers it is +// meant to avoid. +const http = require("http"); +const fs = require("fs"); +const path = require("path"); + +const args = process.argv.slice(2); +const NO_COI = args.includes("--no-coi"); +const positional = args.filter((a) => !a.startsWith("--")); +const PORT = Number(positional[0] || 3000); +const ROOT = path.resolve(positional[1] || process.cwd()); + +const MIME = { + ".html": "text/html; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".mjs": "text/javascript; charset=utf-8", + ".cjs": "text/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".map": "application/json; charset=utf-8", + ".wasm": "application/wasm", + ".css": "text/css; charset=utf-8", + ".mp4": "video/mp4", + ".avi": "video/x-msvideo", + ".webm": "video/webm", + ".mkv": "video/x-matroska", + ".wav": "audio/wav", + ".mp3": "audio/mpeg", + ".png": "image/png", + ".jpg": "image/jpeg", + ".gif": "image/gif", + ".ttf": "font/ttf", +}; + +const COI_HEADERS = { + ...(NO_COI + ? {} + : { + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Embedder-Policy": "require-corp", + }), + "Cross-Origin-Resource-Policy": "cross-origin", + "Access-Control-Allow-Origin": "*", + "Cache-Control": "no-cache, no-store, must-revalidate", +}; + +const server = http.createServer((req, res) => { + const urlPath = decodeURIComponent(req.url.split("?")[0]); + // Health-check root: return 200 so start-server-and-test / wait-on consider + // the server ready (there is no index.html at the repo root). + if (urlPath === "/") { + res.writeHead(200, { ...COI_HEADERS, "Content-Type": "text/plain" }); + return res.end( + `ffmpeg.wasm dev server (${NO_COI ? "NOT isolated" : "cross-origin isolated"})` + ); + } + const filePath = path.join(ROOT, urlPath); + // Prevent path traversal outside ROOT. + if (!filePath.startsWith(ROOT)) { + res.writeHead(403, COI_HEADERS); + return res.end("Forbidden"); + } + fs.stat(filePath, (err, stat) => { + const target = !err && stat.isDirectory() ? path.join(filePath, "index.html") : filePath; + fs.readFile(target, (readErr, data) => { + if (readErr) { + res.writeHead(404, COI_HEADERS); + return res.end("Not found: " + urlPath); + } + res.writeHead(200, { + ...COI_HEADERS, + "Content-Type": MIME[path.extname(target).toLowerCase()] || "application/octet-stream", + "Content-Length": data.length, + }); + res.end(data); + }); + }); +}); + +server.listen(PORT, () => { + console.log( + `serve: http://localhost:${PORT} (root=${ROOT}, ${NO_COI ? "NOT isolated" : "cross-origin isolated"})` + ); +}); diff --git a/src/bind/ffmpeg/bind.js b/src/bind/ffmpeg/bind.js index 14eec22a047..00d00249e7d 100644 --- a/src/bind/ffmpeg/bind.js +++ b/src/bind/ffmpeg/bind.js @@ -55,7 +55,11 @@ function printErr(message) { function exec(..._args) { const args = [...Module["DEFAULT_ARGS"], ..._args]; try { - Module["_ffmpeg"](args.length, stringsToPtr(args)); + // FFmpeg 7.x/8.x removed exit_program(); main()/ffmpeg() now returns the exit + // code directly, so capture the return value instead of relying on a + // C-side EM_ASM setting Module.ret. The Aborted catch remains for the + // timeout path, which still abort()s. + Module["ret"] = Module["_ffmpeg"](args.length, stringsToPtr(args)); } catch (e) { if (!e.message.startsWith("Aborted")) { throw e; @@ -67,7 +71,7 @@ function exec(..._args) { function ffprobe(..._args) { const args = [...Module["DEFAULT_ARGS_FFPROBE"], ..._args]; try { - Module["_ffprobe"](args.length, stringsToPtr(args)); + Module["ret"] = Module["_ffprobe"](args.length, stringsToPtr(args)); } catch (e) { if (!e.message.startsWith("Aborted")) { throw e; diff --git a/src/fftools-5.1/Makefile b/src/fftools-5.1/Makefile new file mode 100644 index 00000000000..81ad6c4f4fd --- /dev/null +++ b/src/fftools-5.1/Makefile @@ -0,0 +1,52 @@ +AVPROGS-$(CONFIG_FFMPEG) += ffmpeg +AVPROGS-$(CONFIG_FFPLAY) += ffplay +AVPROGS-$(CONFIG_FFPROBE) += ffprobe + +AVPROGS := $(AVPROGS-yes:%=%$(PROGSSUF)$(EXESUF)) +PROGS += $(AVPROGS) + +AVBASENAMES = ffmpeg ffplay ffprobe +ALLAVPROGS = $(AVBASENAMES:%=%$(PROGSSUF)$(EXESUF)) +ALLAVPROGS_G = $(AVBASENAMES:%=%$(PROGSSUF)_g$(EXESUF)) + +OBJS-ffmpeg += \ + fftools/ffmpeg_filter.o \ + fftools/ffmpeg_hw.o \ + fftools/ffmpeg_mux.o \ + fftools/ffmpeg_opt.o \ + +define DOFFTOOL +OBJS-$(1) += fftools/cmdutils.o fftools/opt_common.o fftools/$(1).o $(OBJS-$(1)-yes) +$(1)$(PROGSSUF)_g$(EXESUF): $$(OBJS-$(1)) +$$(OBJS-$(1)): | fftools +$$(OBJS-$(1)): CFLAGS += $(CFLAGS-$(1)) +$(1)$(PROGSSUF)_g$(EXESUF): LDFLAGS += $(LDFLAGS-$(1)) +$(1)$(PROGSSUF)_g$(EXESUF): FF_EXTRALIBS += $(EXTRALIBS-$(1)) +-include $$(OBJS-$(1):.o=.d) +endef + +$(foreach P,$(AVPROGS-yes),$(eval $(call DOFFTOOL,$(P)))) + +all: $(AVPROGS) + +fftools/ffprobe.o fftools/cmdutils.o: libavutil/ffversion.h | fftools +OUTDIRS += fftools + +ifdef AVPROGS +install: install-progs install-data +endif + +install-progs-yes: +install-progs-$(CONFIG_SHARED): install-libs + +install-progs: install-progs-yes $(AVPROGS) + $(Q)mkdir -p "$(BINDIR)" + $(INSTALL) -c -m 755 $(AVPROGS) "$(BINDIR)" + +uninstall: uninstall-progs + +uninstall-progs: + $(RM) $(addprefix "$(BINDIR)/", $(ALLAVPROGS)) + +clean:: + $(RM) $(ALLAVPROGS) $(ALLAVPROGS_G) $(CLEANSUFFIXES:%=fftools/%) diff --git a/src/fftools-5.1/cmdutils.c b/src/fftools-5.1/cmdutils.c new file mode 100644 index 00000000000..ff52890cc70 --- /dev/null +++ b/src/fftools-5.1/cmdutils.c @@ -0,0 +1,1035 @@ +/* + * Various utilities for command line tools + * Copyright (c) 2000-2003 Fabrice Bellard + * + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include +#include +#include +#include +#include + +/* Include only the enabled headers since some compilers (namely, Sun + Studio) will not omit unused inline functions and create undefined + references to libraries that are not being built. */ + +#include "config.h" +#include "compat/va_copy.h" +#include "libavformat/avformat.h" +#include "libswscale/swscale.h" +#include "libswscale/version.h" +#include "libswresample/swresample.h" +#include "libavutil/avassert.h" +#include "libavutil/avstring.h" +#include "libavutil/channel_layout.h" +#include "libavutil/display.h" +#include "libavutil/getenv_utf8.h" +#include "libavutil/mathematics.h" +#include "libavutil/imgutils.h" +#include "libavutil/libm.h" +#include "libavutil/parseutils.h" +#include "libavutil/eval.h" +#include "libavutil/dict.h" +#include "libavutil/opt.h" +#include "cmdutils.h" +#include "fopen_utf8.h" +#include "opt_common.h" +#ifdef _WIN32 +#include +#include "compat/w32dlfcn.h" +#endif + +AVDictionary *sws_dict; +AVDictionary *swr_opts; +AVDictionary *format_opts, *codec_opts; + +int hide_banner = 0; + +void uninit_opts(void) +{ + av_dict_free(&swr_opts); + av_dict_free(&sws_dict); + av_dict_free(&format_opts); + av_dict_free(&codec_opts); +} + +void log_callback_help(void *ptr, int level, const char *fmt, va_list vl) +{ + vfprintf(stdout, fmt, vl); +} + +void init_dynload(void) +{ +#if HAVE_SETDLLDIRECTORY && defined(_WIN32) + /* Calling SetDllDirectory with the empty string (but not NULL) removes the + * current working directory from the DLL search path as a security pre-caution. */ + SetDllDirectory(""); +#endif +} + +static void (*program_exit)(int ret); + +void register_exit(void (*cb)(int ret)) +{ + program_exit = cb; +} + +void exit_program(int ret) +{ + if (program_exit) + program_exit(ret); + + /* + * abort() is used instead of exit() because exit() not only + * terminates ffmpeg but also the whole node.js program, which + * is not ideal. + * + * abort() terminiates the ffmpeg with an JS exception + * + * RuntimeError: Aborted... + * + * This excpetion is catch and not visible to users. + * + */ + EM_ASM({ + Module.ret = $0; + }, ret); + abort(); + // exit(ret); +} + +double parse_number_or_die(const char *context, const char *numstr, int type, + double min, double max) +{ + char *tail; + const char *error; + double d = av_strtod(numstr, &tail); + if (*tail) + error = "Expected number for %s but found: %s\n"; + else if (d < min || d > max) + error = "The value for %s was %s which is not within %f - %f\n"; + else if (type == OPT_INT64 && (int64_t)d != d) + error = "Expected int64 for %s but found %s\n"; + else if (type == OPT_INT && (int)d != d) + error = "Expected int for %s but found %s\n"; + else + return d; + av_log(NULL, AV_LOG_FATAL, error, context, numstr, min, max); + exit_program(1); + return 0; +} + +int64_t parse_time_or_die(const char *context, const char *timestr, + int is_duration) +{ + int64_t us; + if (av_parse_time(&us, timestr, is_duration) < 0) { + av_log(NULL, AV_LOG_FATAL, "Invalid %s specification for %s: %s\n", + is_duration ? "duration" : "date", context, timestr); + exit_program(1); + } + return us; +} + +void show_help_options(const OptionDef *options, const char *msg, int req_flags, + int rej_flags, int alt_flags) +{ + const OptionDef *po; + int first; + + first = 1; + for (po = options; po->name; po++) { + char buf[128]; + + if (((po->flags & req_flags) != req_flags) || + (alt_flags && !(po->flags & alt_flags)) || + (po->flags & rej_flags)) + continue; + + if (first) { + printf("%s\n", msg); + first = 0; + } + av_strlcpy(buf, po->name, sizeof(buf)); + if (po->argname) { + av_strlcat(buf, " ", sizeof(buf)); + av_strlcat(buf, po->argname, sizeof(buf)); + } + printf("-%-17s %s\n", buf, po->help); + } + printf("\n"); +} + +void show_help_children(const AVClass *class, int flags) +{ + void *iter = NULL; + const AVClass *child; + if (class->option) { + av_opt_show2(&class, NULL, flags, 0); + printf("\n"); + } + + while (child = av_opt_child_class_iterate(class, &iter)) + show_help_children(child, flags); +} + +static const OptionDef *find_option(const OptionDef *po, const char *name) +{ + while (po->name) { + const char *end; + if (av_strstart(name, po->name, &end) && (!*end || *end == ':')) + break; + po++; + } + return po; +} + +/* _WIN32 means using the windows libc - cygwin doesn't define that + * by default. HAVE_COMMANDLINETOARGVW is true on cygwin, while + * it doesn't provide the actual command line via GetCommandLineW(). */ +#if HAVE_COMMANDLINETOARGVW && defined(_WIN32) +#include +/* Will be leaked on exit */ +static char** win32_argv_utf8 = NULL; +static int win32_argc = 0; + +/** + * Prepare command line arguments for executable. + * For Windows - perform wide-char to UTF-8 conversion. + * Input arguments should be main() function arguments. + * @param argc_ptr Arguments number (including executable) + * @param argv_ptr Arguments list. + */ +static void prepare_app_arguments(int *argc_ptr, char ***argv_ptr) +{ + char *argstr_flat; + wchar_t **argv_w; + int i, buffsize = 0, offset = 0; + + if (win32_argv_utf8) { + *argc_ptr = win32_argc; + *argv_ptr = win32_argv_utf8; + return; + } + + win32_argc = 0; + argv_w = CommandLineToArgvW(GetCommandLineW(), &win32_argc); + if (win32_argc <= 0 || !argv_w) + return; + + /* determine the UTF-8 buffer size (including NULL-termination symbols) */ + for (i = 0; i < win32_argc; i++) + buffsize += WideCharToMultiByte(CP_UTF8, 0, argv_w[i], -1, + NULL, 0, NULL, NULL); + + win32_argv_utf8 = av_mallocz(sizeof(char *) * (win32_argc + 1) + buffsize); + argstr_flat = (char *)win32_argv_utf8 + sizeof(char *) * (win32_argc + 1); + if (!win32_argv_utf8) { + LocalFree(argv_w); + return; + } + + for (i = 0; i < win32_argc; i++) { + win32_argv_utf8[i] = &argstr_flat[offset]; + offset += WideCharToMultiByte(CP_UTF8, 0, argv_w[i], -1, + &argstr_flat[offset], + buffsize - offset, NULL, NULL); + } + win32_argv_utf8[i] = NULL; + LocalFree(argv_w); + + *argc_ptr = win32_argc; + *argv_ptr = win32_argv_utf8; +} +#else +static inline void prepare_app_arguments(int *argc_ptr, char ***argv_ptr) +{ + /* nothing to do */ +} +#endif /* HAVE_COMMANDLINETOARGVW */ + +static int write_option(void *optctx, const OptionDef *po, const char *opt, + const char *arg) +{ + /* new-style options contain an offset into optctx, old-style address of + * a global var*/ + void *dst = po->flags & (OPT_OFFSET | OPT_SPEC) ? + (uint8_t *)optctx + po->u.off : po->u.dst_ptr; + int *dstcount; + + if (po->flags & OPT_SPEC) { + SpecifierOpt **so = dst; + char *p = strchr(opt, ':'); + char *str; + + dstcount = (int *)(so + 1); + *so = grow_array(*so, sizeof(**so), dstcount, *dstcount + 1); + str = av_strdup(p ? p + 1 : ""); + if (!str) + return AVERROR(ENOMEM); + (*so)[*dstcount - 1].specifier = str; + dst = &(*so)[*dstcount - 1].u; + } + + if (po->flags & OPT_STRING) { + char *str; + str = av_strdup(arg); + av_freep(dst); + if (!str) + return AVERROR(ENOMEM); + *(char **)dst = str; + } else if (po->flags & OPT_BOOL || po->flags & OPT_INT) { + *(int *)dst = parse_number_or_die(opt, arg, OPT_INT64, INT_MIN, INT_MAX); + } else if (po->flags & OPT_INT64) { + *(int64_t *)dst = parse_number_or_die(opt, arg, OPT_INT64, INT64_MIN, INT64_MAX); + } else if (po->flags & OPT_TIME) { + *(int64_t *)dst = parse_time_or_die(opt, arg, 1); + } else if (po->flags & OPT_FLOAT) { + *(float *)dst = parse_number_or_die(opt, arg, OPT_FLOAT, -INFINITY, INFINITY); + } else if (po->flags & OPT_DOUBLE) { + *(double *)dst = parse_number_or_die(opt, arg, OPT_DOUBLE, -INFINITY, INFINITY); + } else if (po->u.func_arg) { + int ret = po->u.func_arg(optctx, opt, arg); + if (ret < 0) { + av_log(NULL, AV_LOG_ERROR, + "Failed to set value '%s' for option '%s': %s\n", + arg, opt, av_err2str(ret)); + return ret; + } + } + if (po->flags & OPT_EXIT) + exit_program(0); + + return 0; +} + +int parse_option(void *optctx, const char *opt, const char *arg, + const OptionDef *options) +{ + static const OptionDef opt_avoptions = { + .name = "AVOption passthrough", + .flags = HAS_ARG, + .u.func_arg = opt_default, + }; + + const OptionDef *po; + int ret; + + po = find_option(options, opt); + if (!po->name && opt[0] == 'n' && opt[1] == 'o') { + /* handle 'no' bool option */ + po = find_option(options, opt + 2); + if ((po->name && (po->flags & OPT_BOOL))) + arg = "0"; + } else if (po->flags & OPT_BOOL) + arg = "1"; + + if (!po->name) + po = &opt_avoptions; + if (!po->name) { + av_log(NULL, AV_LOG_ERROR, "Unrecognized option '%s'\n", opt); + return AVERROR(EINVAL); + } + if (po->flags & HAS_ARG && !arg) { + av_log(NULL, AV_LOG_ERROR, "Missing argument for option '%s'\n", opt); + return AVERROR(EINVAL); + } + + ret = write_option(optctx, po, opt, arg); + if (ret < 0) + return ret; + + return !!(po->flags & HAS_ARG); +} + +void parse_options(void *optctx, int argc, char **argv, const OptionDef *options, + void (*parse_arg_function)(void *, const char*)) +{ + const char *opt; + int optindex, handleoptions = 1, ret; + + /* perform system-dependent conversions for arguments list */ + prepare_app_arguments(&argc, &argv); + + /* parse options */ + optindex = 1; + while (optindex < argc) { + opt = argv[optindex++]; + + if (handleoptions && opt[0] == '-' && opt[1] != '\0') { + if (opt[1] == '-' && opt[2] == '\0') { + handleoptions = 0; + continue; + } + opt++; + + if ((ret = parse_option(optctx, opt, argv[optindex], options)) < 0) + exit_program(1); + optindex += ret; + } else { + if (parse_arg_function) + parse_arg_function(optctx, opt); + } + } +} + +int parse_optgroup(void *optctx, OptionGroup *g) +{ + int i, ret; + + av_log(NULL, AV_LOG_DEBUG, "Parsing a group of options: %s %s.\n", + g->group_def->name, g->arg); + + for (i = 0; i < g->nb_opts; i++) { + Option *o = &g->opts[i]; + + if (g->group_def->flags && + !(g->group_def->flags & o->opt->flags)) { + av_log(NULL, AV_LOG_ERROR, "Option %s (%s) cannot be applied to " + "%s %s -- you are trying to apply an input option to an " + "output file or vice versa. Move this option before the " + "file it belongs to.\n", o->key, o->opt->help, + g->group_def->name, g->arg); + return AVERROR(EINVAL); + } + + av_log(NULL, AV_LOG_DEBUG, "Applying option %s (%s) with argument %s.\n", + o->key, o->opt->help, o->val); + + ret = write_option(optctx, o->opt, o->key, o->val); + if (ret < 0) + return ret; + } + + av_log(NULL, AV_LOG_DEBUG, "Successfully parsed a group of options.\n"); + + return 0; +} + +int locate_option(int argc, char **argv, const OptionDef *options, + const char *optname) +{ + const OptionDef *po; + int i; + + for (i = 1; i < argc; i++) { + const char *cur_opt = argv[i]; + + if (*cur_opt++ != '-') + continue; + + po = find_option(options, cur_opt); + if (!po->name && cur_opt[0] == 'n' && cur_opt[1] == 'o') + po = find_option(options, cur_opt + 2); + + if ((!po->name && !strcmp(cur_opt, optname)) || + (po->name && !strcmp(optname, po->name))) + return i; + + if (!po->name || po->flags & HAS_ARG) + i++; + } + return 0; +} + +static void dump_argument(FILE *report_file, const char *a) +{ + const unsigned char *p; + + for (p = a; *p; p++) + if (!((*p >= '+' && *p <= ':') || (*p >= '@' && *p <= 'Z') || + *p == '_' || (*p >= 'a' && *p <= 'z'))) + break; + if (!*p) { + fputs(a, report_file); + return; + } + fputc('"', report_file); + for (p = a; *p; p++) { + if (*p == '\\' || *p == '"' || *p == '$' || *p == '`') + fprintf(report_file, "\\%c", *p); + else if (*p < ' ' || *p > '~') + fprintf(report_file, "\\x%02x", *p); + else + fputc(*p, report_file); + } + fputc('"', report_file); +} + +static void check_options(const OptionDef *po) +{ + while (po->name) { + if (po->flags & OPT_PERFILE) + av_assert0(po->flags & (OPT_INPUT | OPT_OUTPUT)); + po++; + } +} + +void parse_loglevel(int argc, char **argv, const OptionDef *options) +{ + int idx = locate_option(argc, argv, options, "loglevel"); + char *env; + + check_options(options); + + if (!idx) + idx = locate_option(argc, argv, options, "v"); + if (idx && argv[idx + 1]) + opt_loglevel(NULL, "loglevel", argv[idx + 1]); + idx = locate_option(argc, argv, options, "report"); + env = getenv_utf8("FFREPORT"); + if (env || idx) { + FILE *report_file = NULL; + init_report(env, &report_file); + if (report_file) { + int i; + fprintf(report_file, "Command line:\n"); + for (i = 0; i < argc; i++) { + dump_argument(report_file, argv[i]); + fputc(i < argc - 1 ? ' ' : '\n', report_file); + } + fflush(report_file); + } + } + freeenv_utf8(env); + idx = locate_option(argc, argv, options, "hide_banner"); + if (idx) + hide_banner = 1; +} + +static const AVOption *opt_find(void *obj, const char *name, const char *unit, + int opt_flags, int search_flags) +{ + const AVOption *o = av_opt_find(obj, name, unit, opt_flags, search_flags); + if(o && !o->flags) + return NULL; + return o; +} + +#define FLAGS (o->type == AV_OPT_TYPE_FLAGS && (arg[0]=='-' || arg[0]=='+')) ? AV_DICT_APPEND : 0 +int opt_default(void *optctx, const char *opt, const char *arg) +{ + const AVOption *o; + int consumed = 0; + char opt_stripped[128]; + const char *p; + const AVClass *cc = avcodec_get_class(), *fc = avformat_get_class(); +#if CONFIG_SWSCALE + const AVClass *sc = sws_get_class(); +#endif +#if CONFIG_SWRESAMPLE + const AVClass *swr_class = swr_get_class(); +#endif + + if (!strcmp(opt, "debug") || !strcmp(opt, "fdebug")) + av_log_set_level(AV_LOG_DEBUG); + + if (!(p = strchr(opt, ':'))) + p = opt + strlen(opt); + av_strlcpy(opt_stripped, opt, FFMIN(sizeof(opt_stripped), p - opt + 1)); + + if ((o = opt_find(&cc, opt_stripped, NULL, 0, + AV_OPT_SEARCH_CHILDREN | AV_OPT_SEARCH_FAKE_OBJ)) || + ((opt[0] == 'v' || opt[0] == 'a' || opt[0] == 's') && + (o = opt_find(&cc, opt + 1, NULL, 0, AV_OPT_SEARCH_FAKE_OBJ)))) { + av_dict_set(&codec_opts, opt, arg, FLAGS); + consumed = 1; + } + if ((o = opt_find(&fc, opt, NULL, 0, + AV_OPT_SEARCH_CHILDREN | AV_OPT_SEARCH_FAKE_OBJ))) { + av_dict_set(&format_opts, opt, arg, FLAGS); + if (consumed) + av_log(NULL, AV_LOG_VERBOSE, "Routing option %s to both codec and muxer layer\n", opt); + consumed = 1; + } +#if CONFIG_SWSCALE + if (!consumed && (o = opt_find(&sc, opt, NULL, 0, + AV_OPT_SEARCH_CHILDREN | AV_OPT_SEARCH_FAKE_OBJ))) { + if (!strcmp(opt, "srcw") || !strcmp(opt, "srch") || + !strcmp(opt, "dstw") || !strcmp(opt, "dsth") || + !strcmp(opt, "src_format") || !strcmp(opt, "dst_format")) { + av_log(NULL, AV_LOG_ERROR, "Directly using swscale dimensions/format options is not supported, please use the -s or -pix_fmt options\n"); + return AVERROR(EINVAL); + } + av_dict_set(&sws_dict, opt, arg, FLAGS); + + consumed = 1; + } +#else + if (!consumed && !strcmp(opt, "sws_flags")) { + av_log(NULL, AV_LOG_WARNING, "Ignoring %s %s, due to disabled swscale\n", opt, arg); + consumed = 1; + } +#endif +#if CONFIG_SWRESAMPLE + if (!consumed && (o=opt_find(&swr_class, opt, NULL, 0, + AV_OPT_SEARCH_CHILDREN | AV_OPT_SEARCH_FAKE_OBJ))) { + av_dict_set(&swr_opts, opt, arg, FLAGS); + consumed = 1; + } +#endif + + if (consumed) + return 0; + return AVERROR_OPTION_NOT_FOUND; +} + +/* + * Check whether given option is a group separator. + * + * @return index of the group definition that matched or -1 if none + */ +static int match_group_separator(const OptionGroupDef *groups, int nb_groups, + const char *opt) +{ + int i; + + for (i = 0; i < nb_groups; i++) { + const OptionGroupDef *p = &groups[i]; + if (p->sep && !strcmp(p->sep, opt)) + return i; + } + + return -1; +} + +/* + * Finish parsing an option group. + * + * @param group_idx which group definition should this group belong to + * @param arg argument of the group delimiting option + */ +static void finish_group(OptionParseContext *octx, int group_idx, + const char *arg) +{ + OptionGroupList *l = &octx->groups[group_idx]; + OptionGroup *g; + + GROW_ARRAY(l->groups, l->nb_groups); + g = &l->groups[l->nb_groups - 1]; + + *g = octx->cur_group; + g->arg = arg; + g->group_def = l->group_def; + g->sws_dict = sws_dict; + g->swr_opts = swr_opts; + g->codec_opts = codec_opts; + g->format_opts = format_opts; + + codec_opts = NULL; + format_opts = NULL; + sws_dict = NULL; + swr_opts = NULL; + + memset(&octx->cur_group, 0, sizeof(octx->cur_group)); +} + +/* + * Add an option instance to currently parsed group. + */ +static void add_opt(OptionParseContext *octx, const OptionDef *opt, + const char *key, const char *val) +{ + int global = !(opt->flags & (OPT_PERFILE | OPT_SPEC | OPT_OFFSET)); + OptionGroup *g = global ? &octx->global_opts : &octx->cur_group; + + GROW_ARRAY(g->opts, g->nb_opts); + g->opts[g->nb_opts - 1].opt = opt; + g->opts[g->nb_opts - 1].key = key; + g->opts[g->nb_opts - 1].val = val; +} + +static void init_parse_context(OptionParseContext *octx, + const OptionGroupDef *groups, int nb_groups) +{ + static const OptionGroupDef global_group = { "global" }; + int i; + + memset(octx, 0, sizeof(*octx)); + + octx->nb_groups = nb_groups; + octx->groups = av_calloc(octx->nb_groups, sizeof(*octx->groups)); + if (!octx->groups) + exit_program(1); + + for (i = 0; i < octx->nb_groups; i++) + octx->groups[i].group_def = &groups[i]; + + octx->global_opts.group_def = &global_group; + octx->global_opts.arg = ""; +} + +void uninit_parse_context(OptionParseContext *octx) +{ + int i, j; + + for (i = 0; i < octx->nb_groups; i++) { + OptionGroupList *l = &octx->groups[i]; + + for (j = 0; j < l->nb_groups; j++) { + av_freep(&l->groups[j].opts); + av_dict_free(&l->groups[j].codec_opts); + av_dict_free(&l->groups[j].format_opts); + + av_dict_free(&l->groups[j].sws_dict); + av_dict_free(&l->groups[j].swr_opts); + } + av_freep(&l->groups); + } + av_freep(&octx->groups); + + av_freep(&octx->cur_group.opts); + av_freep(&octx->global_opts.opts); + + uninit_opts(); +} + +int split_commandline(OptionParseContext *octx, int argc, char *argv[], + const OptionDef *options, + const OptionGroupDef *groups, int nb_groups) +{ + int optindex = 1; + int dashdash = -2; + + /* perform system-dependent conversions for arguments list */ + prepare_app_arguments(&argc, &argv); + + init_parse_context(octx, groups, nb_groups); + av_log(NULL, AV_LOG_DEBUG, "Splitting the commandline.\n"); + + while (optindex < argc) { + const char *opt = argv[optindex++], *arg; + const OptionDef *po; + int ret; + + av_log(NULL, AV_LOG_DEBUG, "Reading option '%s' ...", opt); + + if (opt[0] == '-' && opt[1] == '-' && !opt[2]) { + dashdash = optindex; + continue; + } + /* unnamed group separators, e.g. output filename */ + if (opt[0] != '-' || !opt[1] || dashdash+1 == optindex) { + finish_group(octx, 0, opt); + av_log(NULL, AV_LOG_DEBUG, " matched as %s.\n", groups[0].name); + continue; + } + opt++; + +#define GET_ARG(arg) \ +do { \ + arg = argv[optindex++]; \ + if (!arg) { \ + av_log(NULL, AV_LOG_ERROR, "Missing argument for option '%s'.\n", opt);\ + return AVERROR(EINVAL); \ + } \ +} while (0) + + /* named group separators, e.g. -i */ + if ((ret = match_group_separator(groups, nb_groups, opt)) >= 0) { + GET_ARG(arg); + finish_group(octx, ret, arg); + av_log(NULL, AV_LOG_DEBUG, " matched as %s with argument '%s'.\n", + groups[ret].name, arg); + continue; + } + + /* normal options */ + po = find_option(options, opt); + if (po->name) { + if (po->flags & OPT_EXIT) { + /* optional argument, e.g. -h */ + arg = argv[optindex++]; + } else if (po->flags & HAS_ARG) { + GET_ARG(arg); + } else { + arg = "1"; + } + + add_opt(octx, po, opt, arg); + av_log(NULL, AV_LOG_DEBUG, " matched as option '%s' (%s) with " + "argument '%s'.\n", po->name, po->help, arg); + continue; + } + + /* AVOptions */ + if (argv[optindex]) { + ret = opt_default(NULL, opt, argv[optindex]); + if (ret >= 0) { + av_log(NULL, AV_LOG_DEBUG, " matched as AVOption '%s' with " + "argument '%s'.\n", opt, argv[optindex]); + optindex++; + continue; + } else if (ret != AVERROR_OPTION_NOT_FOUND) { + av_log(NULL, AV_LOG_ERROR, "Error parsing option '%s' " + "with argument '%s'.\n", opt, argv[optindex]); + return ret; + } + } + + /* boolean -nofoo options */ + if (opt[0] == 'n' && opt[1] == 'o' && + (po = find_option(options, opt + 2)) && + po->name && po->flags & OPT_BOOL) { + add_opt(octx, po, opt, "0"); + av_log(NULL, AV_LOG_DEBUG, " matched as option '%s' (%s) with " + "argument 0.\n", po->name, po->help); + continue; + } + + av_log(NULL, AV_LOG_ERROR, "Unrecognized option '%s'.\n", opt); + return AVERROR_OPTION_NOT_FOUND; + } + + if (octx->cur_group.nb_opts || codec_opts || format_opts) + av_log(NULL, AV_LOG_WARNING, "Trailing option(s) found in the " + "command: may be ignored.\n"); + + av_log(NULL, AV_LOG_DEBUG, "Finished splitting the commandline.\n"); + + return 0; +} + +void print_error(const char *filename, int err) +{ + char errbuf[128]; + const char *errbuf_ptr = errbuf; + + if (av_strerror(err, errbuf, sizeof(errbuf)) < 0) + errbuf_ptr = strerror(AVUNERROR(err)); + av_log(NULL, AV_LOG_ERROR, "%s: %s\n", filename, errbuf_ptr); +} + +int read_yesno(void) +{ + int c = getchar(); + int yesno = (av_toupper(c) == 'Y'); + + while (c != '\n' && c != EOF) + c = getchar(); + + return yesno; +} + +FILE *get_preset_file(char *filename, size_t filename_size, + const char *preset_name, int is_path, + const char *codec_name) +{ + FILE *f = NULL; + int i; +#if HAVE_GETMODULEHANDLE && defined(_WIN32) + char *datadir = NULL; +#endif + char *env_home = getenv_utf8("HOME"); + char *env_ffmpeg_datadir = getenv_utf8("FFMPEG_DATADIR"); + const char *base[3] = { env_ffmpeg_datadir, + env_home, /* index=1(HOME) is special: search in a .ffmpeg subfolder */ + FFMPEG_DATADIR, }; + + if (is_path) { + av_strlcpy(filename, preset_name, filename_size); + f = fopen_utf8(filename, "r"); + } else { +#if HAVE_GETMODULEHANDLE && defined(_WIN32) + wchar_t *datadir_w = get_module_filename(NULL); + base[2] = NULL; + + if (wchartoutf8(datadir_w, &datadir)) + datadir = NULL; + av_free(datadir_w); + + if (datadir) + { + char *ls; + for (ls = datadir; *ls; ls++) + if (*ls == '\\') *ls = '/'; + + if (ls = strrchr(datadir, '/')) + { + ptrdiff_t datadir_len = ls - datadir; + size_t desired_size = datadir_len + strlen("/ffpresets") + 1; + char *new_datadir = av_realloc_array( + datadir, desired_size, sizeof *datadir); + if (new_datadir) { + datadir = new_datadir; + datadir[datadir_len] = 0; + strncat(datadir, "/ffpresets", desired_size - 1 - datadir_len); + base[2] = datadir; + } + } + } +#endif + for (i = 0; i < 3 && !f; i++) { + if (!base[i]) + continue; + snprintf(filename, filename_size, "%s%s/%s.ffpreset", base[i], + i != 1 ? "" : "/.ffmpeg", preset_name); + f = fopen_utf8(filename, "r"); + if (!f && codec_name) { + snprintf(filename, filename_size, + "%s%s/%s-%s.ffpreset", + base[i], i != 1 ? "" : "/.ffmpeg", codec_name, + preset_name); + f = fopen_utf8(filename, "r"); + } + } + } + +#if HAVE_GETMODULEHANDLE && defined(_WIN32) + av_free(datadir); +#endif + freeenv_utf8(env_ffmpeg_datadir); + freeenv_utf8(env_home); + return f; +} + +int check_stream_specifier(AVFormatContext *s, AVStream *st, const char *spec) +{ + int ret = avformat_match_stream_specifier(s, st, spec); + if (ret < 0) + av_log(s, AV_LOG_ERROR, "Invalid stream specifier: %s.\n", spec); + return ret; +} + +AVDictionary *filter_codec_opts(AVDictionary *opts, enum AVCodecID codec_id, + AVFormatContext *s, AVStream *st, const AVCodec *codec) +{ + AVDictionary *ret = NULL; + const AVDictionaryEntry *t = NULL; + int flags = s->oformat ? AV_OPT_FLAG_ENCODING_PARAM + : AV_OPT_FLAG_DECODING_PARAM; + char prefix = 0; + const AVClass *cc = avcodec_get_class(); + + if (!codec) + codec = s->oformat ? avcodec_find_encoder(codec_id) + : avcodec_find_decoder(codec_id); + + switch (st->codecpar->codec_type) { + case AVMEDIA_TYPE_VIDEO: + prefix = 'v'; + flags |= AV_OPT_FLAG_VIDEO_PARAM; + break; + case AVMEDIA_TYPE_AUDIO: + prefix = 'a'; + flags |= AV_OPT_FLAG_AUDIO_PARAM; + break; + case AVMEDIA_TYPE_SUBTITLE: + prefix = 's'; + flags |= AV_OPT_FLAG_SUBTITLE_PARAM; + break; + } + + while (t = av_dict_get(opts, "", t, AV_DICT_IGNORE_SUFFIX)) { + const AVClass *priv_class; + char *p = strchr(t->key, ':'); + + /* check stream specification in opt name */ + if (p) + switch (check_stream_specifier(s, st, p + 1)) { + case 1: *p = 0; break; + case 0: continue; + default: exit_program(1); + } + + if (av_opt_find(&cc, t->key, NULL, flags, AV_OPT_SEARCH_FAKE_OBJ) || + !codec || + ((priv_class = codec->priv_class) && + av_opt_find(&priv_class, t->key, NULL, flags, + AV_OPT_SEARCH_FAKE_OBJ))) + av_dict_set(&ret, t->key, t->value, 0); + else if (t->key[0] == prefix && + av_opt_find(&cc, t->key + 1, NULL, flags, + AV_OPT_SEARCH_FAKE_OBJ)) + av_dict_set(&ret, t->key + 1, t->value, 0); + + if (p) + *p = ':'; + } + return ret; +} + +AVDictionary **setup_find_stream_info_opts(AVFormatContext *s, + AVDictionary *codec_opts) +{ + int i; + AVDictionary **opts; + + if (!s->nb_streams) + return NULL; + opts = av_calloc(s->nb_streams, sizeof(*opts)); + if (!opts) { + av_log(NULL, AV_LOG_ERROR, + "Could not alloc memory for stream options.\n"); + exit_program(1); + } + for (i = 0; i < s->nb_streams; i++) + opts[i] = filter_codec_opts(codec_opts, s->streams[i]->codecpar->codec_id, + s, s->streams[i], NULL); + return opts; +} + +void *grow_array(void *array, int elem_size, int *size, int new_size) +{ + if (new_size >= INT_MAX / elem_size) { + av_log(NULL, AV_LOG_ERROR, "Array too big.\n"); + exit_program(1); + } + if (*size < new_size) { + uint8_t *tmp = av_realloc_array(array, new_size, elem_size); + if (!tmp) { + av_log(NULL, AV_LOG_ERROR, "Could not alloc buffer.\n"); + exit_program(1); + } + memset(tmp + *size*elem_size, 0, (new_size-*size) * elem_size); + *size = new_size; + return tmp; + } + return array; +} + +void *allocate_array_elem(void *ptr, size_t elem_size, int *nb_elems) +{ + void *new_elem; + + if (!(new_elem = av_mallocz(elem_size)) || + av_dynarray_add_nofree(ptr, nb_elems, new_elem) < 0) { + av_log(NULL, AV_LOG_ERROR, "Could not alloc buffer.\n"); + exit_program(1); + } + return new_elem; +} + +double get_rotation(int32_t *displaymatrix) +{ + double theta = 0; + if (displaymatrix) + theta = -round(av_display_rotation_get((int32_t*) displaymatrix)); + + theta -= 360*floor(theta/360 + 0.9/360); + + if (fabs(theta - 90*round(theta/90)) > 2) + av_log(NULL, AV_LOG_WARNING, "Odd rotation angle.\n" + "If you want to help, upload a sample " + "of this file to https://streams.videolan.org/upload/ " + "and contact the ffmpeg-devel mailing list. (ffmpeg-devel@ffmpeg.org)"); + + return theta; +} diff --git a/src/fftools-5.1/cmdutils.h b/src/fftools-5.1/cmdutils.h new file mode 100644 index 00000000000..d87e162ccd6 --- /dev/null +++ b/src/fftools-5.1/cmdutils.h @@ -0,0 +1,455 @@ +/* + * Various utilities for command line tools + * copyright (c) 2003 Fabrice Bellard + * + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef FFTOOLS_CMDUTILS_H +#define FFTOOLS_CMDUTILS_H + +#include + +#include "config.h" +#include "libavcodec/avcodec.h" +#include "libavfilter/avfilter.h" +#include "libavformat/avformat.h" +#include "libswscale/swscale.h" + +#ifdef _WIN32 +#undef main /* We don't want SDL to override our main() */ +#endif + +/** + * program name, defined by the program for show_version(). + */ +extern const char program_name[]; + +/** + * program birth year, defined by the program for show_banner() + */ +extern const int program_birth_year; + +extern AVDictionary *sws_dict; +extern AVDictionary *swr_opts; +extern AVDictionary *format_opts, *codec_opts; +extern int hide_banner; + +/** + * Register a program-specific cleanup routine. + */ +void register_exit(void (*cb)(int ret)); + +/** + * Wraps exit with a program-specific cleanup routine. + */ +void exit_program(int ret) av_noreturn; + +/** + * Initialize dynamic library loading + */ +void init_dynload(void); + +/** + * Uninitialize the cmdutils option system, in particular + * free the *_opts contexts and their contents. + */ +void uninit_opts(void); + +/** + * Trivial log callback. + * Only suitable for opt_help and similar since it lacks prefix handling. + */ +void log_callback_help(void* ptr, int level, const char* fmt, va_list vl); + +/** + * Fallback for options that are not explicitly handled, these will be + * parsed through AVOptions. + */ +int opt_default(void *optctx, const char *opt, const char *arg); + +/** + * Limit the execution time. + */ +int opt_timelimit(void *optctx, const char *opt, const char *arg); + +/** + * Parse a string and return its corresponding value as a double. + * Exit from the application if the string cannot be correctly + * parsed or the corresponding value is invalid. + * + * @param context the context of the value to be set (e.g. the + * corresponding command line option name) + * @param numstr the string to be parsed + * @param type the type (OPT_INT64 or OPT_FLOAT) as which the + * string should be parsed + * @param min the minimum valid accepted value + * @param max the maximum valid accepted value + */ +double parse_number_or_die(const char *context, const char *numstr, int type, + double min, double max); + +/** + * Parse a string specifying a time and return its corresponding + * value as a number of microseconds. Exit from the application if + * the string cannot be correctly parsed. + * + * @param context the context of the value to be set (e.g. the + * corresponding command line option name) + * @param timestr the string to be parsed + * @param is_duration a flag which tells how to interpret timestr, if + * not zero timestr is interpreted as a duration, otherwise as a + * date + * + * @see av_parse_time() + */ +int64_t parse_time_or_die(const char *context, const char *timestr, + int is_duration); + +typedef struct SpecifierOpt { + char *specifier; /**< stream/chapter/program/... specifier */ + union { + uint8_t *str; + int i; + int64_t i64; + uint64_t ui64; + float f; + double dbl; + } u; +} SpecifierOpt; + +typedef struct OptionDef { + const char *name; + int flags; +#define HAS_ARG 0x0001 +#define OPT_BOOL 0x0002 +#define OPT_EXPERT 0x0004 +#define OPT_STRING 0x0008 +#define OPT_VIDEO 0x0010 +#define OPT_AUDIO 0x0020 +#define OPT_INT 0x0080 +#define OPT_FLOAT 0x0100 +#define OPT_SUBTITLE 0x0200 +#define OPT_INT64 0x0400 +#define OPT_EXIT 0x0800 +#define OPT_DATA 0x1000 +#define OPT_PERFILE 0x2000 /* the option is per-file (currently ffmpeg-only). + implied by OPT_OFFSET or OPT_SPEC */ +#define OPT_OFFSET 0x4000 /* option is specified as an offset in a passed optctx */ +#define OPT_SPEC 0x8000 /* option is to be stored in an array of SpecifierOpt. + Implies OPT_OFFSET. Next element after the offset is + an int containing element count in the array. */ +#define OPT_TIME 0x10000 +#define OPT_DOUBLE 0x20000 +#define OPT_INPUT 0x40000 +#define OPT_OUTPUT 0x80000 + union { + void *dst_ptr; + int (*func_arg)(void *, const char *, const char *); + size_t off; + } u; + const char *help; + const char *argname; +} OptionDef; + +/** + * Print help for all options matching specified flags. + * + * @param options a list of options + * @param msg title of this group. Only printed if at least one option matches. + * @param req_flags print only options which have all those flags set. + * @param rej_flags don't print options which have any of those flags set. + * @param alt_flags print only options that have at least one of those flags set + */ +void show_help_options(const OptionDef *options, const char *msg, int req_flags, + int rej_flags, int alt_flags); + +/** + * Show help for all options with given flags in class and all its + * children. + */ +void show_help_children(const AVClass *class, int flags); + +/** + * Per-fftool specific help handler. Implemented in each + * fftool, called by show_help(). + */ +void show_help_default(const char *opt, const char *arg); + +/** + * Parse the command line arguments. + * + * @param optctx an opaque options context + * @param argc number of command line arguments + * @param argv values of command line arguments + * @param options Array with the definitions required to interpret every + * option of the form: -option_name [argument] + * @param parse_arg_function Name of the function called to process every + * argument without a leading option name flag. NULL if such arguments do + * not have to be processed. + */ +void parse_options(void *optctx, int argc, char **argv, const OptionDef *options, + void (* parse_arg_function)(void *optctx, const char*)); + +/** + * Parse one given option. + * + * @return on success 1 if arg was consumed, 0 otherwise; negative number on error + */ +int parse_option(void *optctx, const char *opt, const char *arg, + const OptionDef *options); + +/** + * An option extracted from the commandline. + * Cannot use AVDictionary because of options like -map which can be + * used multiple times. + */ +typedef struct Option { + const OptionDef *opt; + const char *key; + const char *val; +} Option; + +typedef struct OptionGroupDef { + /**< group name */ + const char *name; + /** + * Option to be used as group separator. Can be NULL for groups which + * are terminated by a non-option argument (e.g. ffmpeg output files) + */ + const char *sep; + /** + * Option flags that must be set on each option that is + * applied to this group + */ + int flags; +} OptionGroupDef; + +typedef struct OptionGroup { + const OptionGroupDef *group_def; + const char *arg; + + Option *opts; + int nb_opts; + + AVDictionary *codec_opts; + AVDictionary *format_opts; + AVDictionary *sws_dict; + AVDictionary *swr_opts; +} OptionGroup; + +/** + * A list of option groups that all have the same group type + * (e.g. input files or output files) + */ +typedef struct OptionGroupList { + const OptionGroupDef *group_def; + + OptionGroup *groups; + int nb_groups; +} OptionGroupList; + +typedef struct OptionParseContext { + OptionGroup global_opts; + + OptionGroupList *groups; + int nb_groups; + + /* parsing state */ + OptionGroup cur_group; +} OptionParseContext; + +/** + * Parse an options group and write results into optctx. + * + * @param optctx an app-specific options context. NULL for global options group + */ +int parse_optgroup(void *optctx, OptionGroup *g); + +/** + * Split the commandline into an intermediate form convenient for further + * processing. + * + * The commandline is assumed to be composed of options which either belong to a + * group (those with OPT_SPEC, OPT_OFFSET or OPT_PERFILE) or are global + * (everything else). + * + * A group (defined by an OptionGroupDef struct) is a sequence of options + * terminated by either a group separator option (e.g. -i) or a parameter that + * is not an option (doesn't start with -). A group without a separator option + * must always be first in the supplied groups list. + * + * All options within the same group are stored in one OptionGroup struct in an + * OptionGroupList, all groups with the same group definition are stored in one + * OptionGroupList in OptionParseContext.groups. The order of group lists is the + * same as the order of group definitions. + */ +int split_commandline(OptionParseContext *octx, int argc, char *argv[], + const OptionDef *options, + const OptionGroupDef *groups, int nb_groups); + +/** + * Free all allocated memory in an OptionParseContext. + */ +void uninit_parse_context(OptionParseContext *octx); + +/** + * Find the '-loglevel' option in the command line args and apply it. + */ +void parse_loglevel(int argc, char **argv, const OptionDef *options); + +/** + * Return index of option opt in argv or 0 if not found. + */ +int locate_option(int argc, char **argv, const OptionDef *options, + const char *optname); + +/** + * Check if the given stream matches a stream specifier. + * + * @param s Corresponding format context. + * @param st Stream from s to be checked. + * @param spec A stream specifier of the [v|a|s|d]:[\] form. + * + * @return 1 if the stream matches, 0 if it doesn't, <0 on error + */ +int check_stream_specifier(AVFormatContext *s, AVStream *st, const char *spec); + +/** + * Filter out options for given codec. + * + * Create a new options dictionary containing only the options from + * opts which apply to the codec with ID codec_id. + * + * @param opts dictionary to place options in + * @param codec_id ID of the codec that should be filtered for + * @param s Corresponding format context. + * @param st A stream from s for which the options should be filtered. + * @param codec The particular codec for which the options should be filtered. + * If null, the default one is looked up according to the codec id. + * @return a pointer to the created dictionary + */ +AVDictionary *filter_codec_opts(AVDictionary *opts, enum AVCodecID codec_id, + AVFormatContext *s, AVStream *st, const AVCodec *codec); + +/** + * Setup AVCodecContext options for avformat_find_stream_info(). + * + * Create an array of dictionaries, one dictionary for each stream + * contained in s. + * Each dictionary will contain the options from codec_opts which can + * be applied to the corresponding stream codec context. + * + * @return pointer to the created array of dictionaries. + * Calls exit() on failure. + */ +AVDictionary **setup_find_stream_info_opts(AVFormatContext *s, + AVDictionary *codec_opts); + +/** + * Print an error message to stderr, indicating filename and a human + * readable description of the error code err. + * + * If strerror_r() is not available the use of this function in a + * multithreaded application may be unsafe. + * + * @see av_strerror() + */ +void print_error(const char *filename, int err); + +/** + * Print the program banner to stderr. The banner contents depend on the + * current version of the repository and of the libav* libraries used by + * the program. + */ +void show_banner(int argc, char **argv, const OptionDef *options); + +/** + * Return a positive value if a line read from standard input + * starts with [yY], otherwise return 0. + */ +int read_yesno(void); + +/** + * Get a file corresponding to a preset file. + * + * If is_path is non-zero, look for the file in the path preset_name. + * Otherwise search for a file named arg.ffpreset in the directories + * $FFMPEG_DATADIR (if set), $HOME/.ffmpeg, and in the datadir defined + * at configuration time or in a "ffpresets" folder along the executable + * on win32, in that order. If no such file is found and + * codec_name is defined, then search for a file named + * codec_name-preset_name.avpreset in the above-mentioned directories. + * + * @param filename buffer where the name of the found filename is written + * @param filename_size size in bytes of the filename buffer + * @param preset_name name of the preset to search + * @param is_path tell if preset_name is a filename path + * @param codec_name name of the codec for which to look for the + * preset, may be NULL + */ +FILE *get_preset_file(char *filename, size_t filename_size, + const char *preset_name, int is_path, const char *codec_name); + +/** + * Realloc array to hold new_size elements of elem_size. + * Calls exit() on failure. + * + * @param array array to reallocate + * @param elem_size size in bytes of each element + * @param size new element count will be written here + * @param new_size number of elements to place in reallocated array + * @return reallocated array + */ +void *grow_array(void *array, int elem_size, int *size, int new_size); + +/** + * Atomically add a new element to an array of pointers, i.e. allocate + * a new entry, reallocate the array of pointers and make the new last + * member of this array point to the newly allocated buffer. + * Calls exit() on failure. + * + * @param array array of pointers to reallocate + * @param elem_size size of the new element to allocate + * @param nb_elems pointer to the number of elements of the array array; + * *nb_elems will be incremented by one by this function. + * @return pointer to the newly allocated entry + */ +void *allocate_array_elem(void *array, size_t elem_size, int *nb_elems); + +#define GROW_ARRAY(array, nb_elems)\ + array = grow_array(array, sizeof(*array), &nb_elems, nb_elems + 1) + +#define ALLOC_ARRAY_ELEM(array, nb_elems)\ + allocate_array_elem(&array, sizeof(*array[0]), &nb_elems) + +#define GET_PIX_FMT_NAME(pix_fmt)\ + const char *name = av_get_pix_fmt_name(pix_fmt); + +#define GET_CODEC_NAME(id)\ + const char *name = avcodec_descriptor_get(id)->name; + +#define GET_SAMPLE_FMT_NAME(sample_fmt)\ + const char *name = av_get_sample_fmt_name(sample_fmt) + +#define GET_SAMPLE_RATE_NAME(rate)\ + char name[16];\ + snprintf(name, sizeof(name), "%d", rate); + +double get_rotation(int32_t *displaymatrix); + +#endif /* FFTOOLS_CMDUTILS_H */ diff --git a/src/fftools-5.1/ffmpeg.c b/src/fftools-5.1/ffmpeg.c new file mode 100644 index 00000000000..cc0884ca8e3 --- /dev/null +++ b/src/fftools-5.1/ffmpeg.c @@ -0,0 +1,4665 @@ +/* + * Copyright (c) 2000-2003 Fabrice Bellard + * + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * @file + * multimedia converter based on the FFmpeg libraries + */ + +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if HAVE_IO_H +#include +#endif +#if HAVE_UNISTD_H +#include +#endif + +#include "libavformat/avformat.h" +#include "libavdevice/avdevice.h" +#include "libswresample/swresample.h" +#include "libavutil/opt.h" +#include "libavutil/channel_layout.h" +#include "libavutil/parseutils.h" +#include "libavutil/samplefmt.h" +#include "libavutil/fifo.h" +#include "libavutil/hwcontext.h" +#include "libavutil/internal.h" +#include "libavutil/intreadwrite.h" +#include "libavutil/dict.h" +#include "libavutil/display.h" +#include "libavutil/mathematics.h" +#include "libavutil/pixdesc.h" +#include "libavutil/avstring.h" +#include "libavutil/libm.h" +#include "libavutil/imgutils.h" +#include "libavutil/timestamp.h" +#include "libavutil/bprint.h" +#include "libavutil/time.h" +#include "libavutil/thread.h" +#include "libavutil/threadmessage.h" +#include "libavcodec/mathops.h" +#include "libavcodec/version.h" +#include "libavformat/os_support.h" + +# include "libavfilter/avfilter.h" +# include "libavfilter/buffersrc.h" +# include "libavfilter/buffersink.h" + +#if HAVE_SYS_RESOURCE_H +#include +#include +#include +#elif HAVE_GETPROCESSTIMES +#include +#endif +#if HAVE_GETPROCESSMEMORYINFO +#include +#include +#endif +#if HAVE_SETCONSOLECTRLHANDLER +#include +#endif + + +#if HAVE_SYS_SELECT_H +#include +#endif + +#if HAVE_TERMIOS_H +#include +#include +#include +#include +#elif HAVE_KBHIT +#include +#endif + +#include + +#include "ffmpeg.h" +#include "cmdutils.h" + +#include "libavutil/avassert.h" + +const char program_name[] = "ffmpeg"; +const int program_birth_year = 2000; + +static FILE *vstats_file; + +const char *const forced_keyframes_const_names[] = { + "n", + "n_forced", + "prev_forced_n", + "prev_forced_t", + "t", + NULL +}; + +typedef struct BenchmarkTimeStamps { + int64_t real_usec; + int64_t user_usec; + int64_t sys_usec; +} BenchmarkTimeStamps; + +static BenchmarkTimeStamps get_benchmark_time_stamps(void); +static int64_t getmaxrss(void); +static int ifilter_has_all_input_formats(FilterGraph *fg); + +static int64_t nb_frames_dup = 0; +static uint64_t dup_warning = 1000; +static int64_t nb_frames_drop = 0; +static int64_t decode_error_stat[2]; +unsigned nb_output_dumped = 0; + +int want_sdp = 1; + +static BenchmarkTimeStamps current_time; +AVIOContext *progress_avio = NULL; + +static uint8_t *subtitle_out; + +InputStream **input_streams = NULL; +int nb_input_streams = 0; +InputFile **input_files = NULL; +int nb_input_files = 0; + +OutputStream **output_streams = NULL; +int nb_output_streams = 0; +OutputFile **output_files = NULL; +int nb_output_files = 0; + +FilterGraph **filtergraphs; +int nb_filtergraphs; + +#if HAVE_TERMIOS_H + +/* init terminal so that we can grab keys */ +static struct termios oldtty; +static int restore_tty; +#endif + +#if HAVE_THREADS +static void free_input_threads(void); +#endif + +/* sub2video hack: + Convert subtitles to video with alpha to insert them in filter graphs. + This is a temporary solution until libavfilter gets real subtitles support. + */ + +static int sub2video_get_blank_frame(InputStream *ist) +{ + int ret; + AVFrame *frame = ist->sub2video.frame; + + av_frame_unref(frame); + ist->sub2video.frame->width = ist->dec_ctx->width ? ist->dec_ctx->width : ist->sub2video.w; + ist->sub2video.frame->height = ist->dec_ctx->height ? ist->dec_ctx->height : ist->sub2video.h; + ist->sub2video.frame->format = AV_PIX_FMT_RGB32; + if ((ret = av_frame_get_buffer(frame, 0)) < 0) + return ret; + memset(frame->data[0], 0, frame->height * frame->linesize[0]); + return 0; +} + +static void sub2video_copy_rect(uint8_t *dst, int dst_linesize, int w, int h, + AVSubtitleRect *r) +{ + uint32_t *pal, *dst2; + uint8_t *src, *src2; + int x, y; + + if (r->type != SUBTITLE_BITMAP) { + av_log(NULL, AV_LOG_WARNING, "sub2video: non-bitmap subtitle\n"); + return; + } + if (r->x < 0 || r->x + r->w > w || r->y < 0 || r->y + r->h > h) { + av_log(NULL, AV_LOG_WARNING, "sub2video: rectangle (%d %d %d %d) overflowing %d %d\n", + r->x, r->y, r->w, r->h, w, h + ); + return; + } + + dst += r->y * dst_linesize + r->x * 4; + src = r->data[0]; + pal = (uint32_t *)r->data[1]; + for (y = 0; y < r->h; y++) { + dst2 = (uint32_t *)dst; + src2 = src; + for (x = 0; x < r->w; x++) + *(dst2++) = pal[*(src2++)]; + dst += dst_linesize; + src += r->linesize[0]; + } +} + +static void sub2video_push_ref(InputStream *ist, int64_t pts) +{ + AVFrame *frame = ist->sub2video.frame; + int i; + int ret; + + av_assert1(frame->data[0]); + ist->sub2video.last_pts = frame->pts = pts; + for (i = 0; i < ist->nb_filters; i++) { + ret = av_buffersrc_add_frame_flags(ist->filters[i]->filter, frame, + AV_BUFFERSRC_FLAG_KEEP_REF | + AV_BUFFERSRC_FLAG_PUSH); + if (ret != AVERROR_EOF && ret < 0) + av_log(NULL, AV_LOG_WARNING, "Error while add the frame to buffer source(%s).\n", + av_err2str(ret)); + } +} + +void sub2video_update(InputStream *ist, int64_t heartbeat_pts, AVSubtitle *sub) +{ + AVFrame *frame = ist->sub2video.frame; + int8_t *dst; + int dst_linesize; + int num_rects, i; + int64_t pts, end_pts; + + if (!frame) + return; + if (sub) { + pts = av_rescale_q(sub->pts + sub->start_display_time * 1000LL, + AV_TIME_BASE_Q, ist->st->time_base); + end_pts = av_rescale_q(sub->pts + sub->end_display_time * 1000LL, + AV_TIME_BASE_Q, ist->st->time_base); + num_rects = sub->num_rects; + } else { + /* If we are initializing the system, utilize current heartbeat + PTS as the start time, and show until the following subpicture + is received. Otherwise, utilize the previous subpicture's end time + as the fall-back value. */ + pts = ist->sub2video.initialize ? + heartbeat_pts : ist->sub2video.end_pts; + end_pts = INT64_MAX; + num_rects = 0; + } + if (sub2video_get_blank_frame(ist) < 0) { + av_log(ist->dec_ctx, AV_LOG_ERROR, + "Impossible to get a blank canvas.\n"); + return; + } + dst = frame->data [0]; + dst_linesize = frame->linesize[0]; + for (i = 0; i < num_rects; i++) + sub2video_copy_rect(dst, dst_linesize, frame->width, frame->height, sub->rects[i]); + sub2video_push_ref(ist, pts); + ist->sub2video.end_pts = end_pts; + ist->sub2video.initialize = 0; +} + +static void sub2video_heartbeat(InputStream *ist, int64_t pts) +{ + InputFile *infile = input_files[ist->file_index]; + int i, j, nb_reqs; + int64_t pts2; + + /* When a frame is read from a file, examine all sub2video streams in + the same file and send the sub2video frame again. Otherwise, decoded + video frames could be accumulating in the filter graph while a filter + (possibly overlay) is desperately waiting for a subtitle frame. */ + for (i = 0; i < infile->nb_streams; i++) { + InputStream *ist2 = input_streams[infile->ist_index + i]; + if (!ist2->sub2video.frame) + continue; + /* subtitles seem to be usually muxed ahead of other streams; + if not, subtracting a larger time here is necessary */ + pts2 = av_rescale_q(pts, ist->st->time_base, ist2->st->time_base) - 1; + /* do not send the heartbeat frame if the subtitle is already ahead */ + if (pts2 <= ist2->sub2video.last_pts) + continue; + if (pts2 >= ist2->sub2video.end_pts || ist2->sub2video.initialize) + /* if we have hit the end of the current displayed subpicture, + or if we need to initialize the system, update the + overlayed subpicture and its start/end times */ + sub2video_update(ist2, pts2 + 1, NULL); + for (j = 0, nb_reqs = 0; j < ist2->nb_filters; j++) + nb_reqs += av_buffersrc_get_nb_failed_requests(ist2->filters[j]->filter); + if (nb_reqs) + sub2video_push_ref(ist2, pts2); + } +} + +static void sub2video_flush(InputStream *ist) +{ + int i; + int ret; + + if (ist->sub2video.end_pts < INT64_MAX) + sub2video_update(ist, INT64_MAX, NULL); + for (i = 0; i < ist->nb_filters; i++) { + ret = av_buffersrc_add_frame(ist->filters[i]->filter, NULL); + if (ret != AVERROR_EOF && ret < 0) + av_log(NULL, AV_LOG_WARNING, "Flush the frame error.\n"); + } +} + +/* end of sub2video hack */ + +static void term_exit_sigsafe(void) +{ +#if HAVE_TERMIOS_H + if(restore_tty) + tcsetattr (0, TCSANOW, &oldtty); +#endif +} + +void term_exit(void) +{ + av_log(NULL, AV_LOG_QUIET, "%s", ""); + term_exit_sigsafe(); +} + +static volatile int received_sigterm = 0; +static volatile int received_nb_signals = 0; +static atomic_int transcode_init_done = ATOMIC_VAR_INIT(0); +static volatile int ffmpeg_exited = 0; +int main_return_code = 0; +static int64_t copy_ts_first_pts = AV_NOPTS_VALUE; + +static void +sigterm_handler(int sig) +{ + int ret; + received_sigterm = sig; + received_nb_signals++; + term_exit_sigsafe(); + if(received_nb_signals > 3) { + ret = write(2/*STDERR_FILENO*/, "Received > 3 system signals, hard exiting\n", + strlen("Received > 3 system signals, hard exiting\n")); + if (ret < 0) { /* Do nothing */ }; + exit(123); + } +} + +#if HAVE_SETCONSOLECTRLHANDLER +static BOOL WINAPI CtrlHandler(DWORD fdwCtrlType) +{ + av_log(NULL, AV_LOG_DEBUG, "\nReceived windows signal %ld\n", fdwCtrlType); + + switch (fdwCtrlType) + { + case CTRL_C_EVENT: + case CTRL_BREAK_EVENT: + sigterm_handler(SIGINT); + return TRUE; + + case CTRL_CLOSE_EVENT: + case CTRL_LOGOFF_EVENT: + case CTRL_SHUTDOWN_EVENT: + sigterm_handler(SIGTERM); + /* Basically, with these 3 events, when we return from this method the + process is hard terminated, so stall as long as we need to + to try and let the main thread(s) clean up and gracefully terminate + (we have at most 5 seconds, but should be done far before that). */ + while (!ffmpeg_exited) { + Sleep(0); + } + return TRUE; + + default: + av_log(NULL, AV_LOG_ERROR, "Received unknown windows signal %ld\n", fdwCtrlType); + return FALSE; + } +} +#endif + +#ifdef __linux__ +#define SIGNAL(sig, func) \ + do { \ + action.sa_handler = func; \ + sigaction(sig, &action, NULL); \ + } while (0) +#else +#define SIGNAL(sig, func) \ + signal(sig, func) +#endif + +void term_init(void) +{ +#if defined __linux__ + struct sigaction action = {0}; + action.sa_handler = sigterm_handler; + + /* block other interrupts while processing this one */ + sigfillset(&action.sa_mask); + + /* restart interruptible functions (i.e. don't fail with EINTR) */ + action.sa_flags = SA_RESTART; +#endif + +#if HAVE_TERMIOS_H + if (stdin_interaction) { + struct termios tty; + if (tcgetattr (0, &tty) == 0) { + oldtty = tty; + restore_tty = 1; + + tty.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP + |INLCR|IGNCR|ICRNL|IXON); + tty.c_oflag |= OPOST; + tty.c_lflag &= ~(ECHO|ECHONL|ICANON|IEXTEN); + tty.c_cflag &= ~(CSIZE|PARENB); + tty.c_cflag |= CS8; + tty.c_cc[VMIN] = 1; + tty.c_cc[VTIME] = 0; + + tcsetattr (0, TCSANOW, &tty); + } + SIGNAL(SIGQUIT, sigterm_handler); /* Quit (POSIX). */ + } +#endif + + SIGNAL(SIGINT , sigterm_handler); /* Interrupt (ANSI). */ + SIGNAL(SIGTERM, sigterm_handler); /* Termination (ANSI). */ +#ifdef SIGXCPU + SIGNAL(SIGXCPU, sigterm_handler); +#endif +#ifdef SIGPIPE + signal(SIGPIPE, SIG_IGN); /* Broken pipe (POSIX). */ +#endif +#if HAVE_SETCONSOLECTRLHANDLER + SetConsoleCtrlHandler((PHANDLER_ROUTINE) CtrlHandler, TRUE); +#endif +} + +/* read a key without blocking */ +static int read_key(void) +{ + unsigned char ch; +#if HAVE_TERMIOS_H + int n = 1; + struct timeval tv; + fd_set rfds; + + FD_ZERO(&rfds); + FD_SET(0, &rfds); + tv.tv_sec = 0; + tv.tv_usec = 0; + n = select(1, &rfds, NULL, NULL, &tv); + if (n > 0) { + n = read(0, &ch, 1); + if (n == 1) + return ch; + + return n; + } +#elif HAVE_KBHIT +# if HAVE_PEEKNAMEDPIPE + static int is_pipe; + static HANDLE input_handle; + DWORD dw, nchars; + if(!input_handle){ + input_handle = GetStdHandle(STD_INPUT_HANDLE); + is_pipe = !GetConsoleMode(input_handle, &dw); + } + + if (is_pipe) { + /* When running under a GUI, you will end here. */ + if (!PeekNamedPipe(input_handle, NULL, 0, NULL, &nchars, NULL)) { + // input pipe may have been closed by the program that ran ffmpeg + return -1; + } + //Read it + if(nchars != 0) { + read(0, &ch, 1); + return ch; + }else{ + return -1; + } + } +# endif + if(kbhit()) + return(getch()); +#endif + return -1; +} + +static int decode_interrupt_cb(void *ctx) +{ + return received_nb_signals > atomic_load(&transcode_init_done); +} + +const AVIOInterruptCB int_cb = { decode_interrupt_cb, NULL }; + +static void ffmpeg_cleanup(int ret) +{ + int i, j; + + if (do_benchmark) { + int maxrss = getmaxrss() / 1024; + av_log(NULL, AV_LOG_INFO, "bench: maxrss=%ikB\n", maxrss); + } + + for (i = 0; i < nb_filtergraphs; i++) { + FilterGraph *fg = filtergraphs[i]; + avfilter_graph_free(&fg->graph); + for (j = 0; j < fg->nb_inputs; j++) { + InputFilter *ifilter = fg->inputs[j]; + struct InputStream *ist = ifilter->ist; + + if (ifilter->frame_queue) { + AVFrame *frame; + while (av_fifo_read(ifilter->frame_queue, &frame, 1) >= 0) + av_frame_free(&frame); + av_fifo_freep2(&ifilter->frame_queue); + } + av_freep(&ifilter->displaymatrix); + if (ist->sub2video.sub_queue) { + AVSubtitle sub; + while (av_fifo_read(ist->sub2video.sub_queue, &sub, 1) >= 0) + avsubtitle_free(&sub); + av_fifo_freep2(&ist->sub2video.sub_queue); + } + av_buffer_unref(&ifilter->hw_frames_ctx); + av_freep(&ifilter->name); + av_freep(&fg->inputs[j]); + } + av_freep(&fg->inputs); + for (j = 0; j < fg->nb_outputs; j++) { + OutputFilter *ofilter = fg->outputs[j]; + + avfilter_inout_free(&ofilter->out_tmp); + av_freep(&ofilter->name); + av_channel_layout_uninit(&ofilter->ch_layout); + av_freep(&fg->outputs[j]); + } + av_freep(&fg->outputs); + av_freep(&fg->graph_desc); + + av_freep(&filtergraphs[i]); + } + av_freep(&filtergraphs); + + av_freep(&subtitle_out); + + /* close files */ + for (i = 0; i < nb_output_files; i++) + of_close(&output_files[i]); + + for (i = 0; i < nb_output_streams; i++) { + OutputStream *ost = output_streams[i]; + + if (!ost) + continue; + + av_bsf_free(&ost->bsf_ctx); + + av_frame_free(&ost->filtered_frame); + av_frame_free(&ost->last_frame); + av_packet_free(&ost->pkt); + av_dict_free(&ost->encoder_opts); + + av_freep(&ost->forced_keyframes); + av_expr_free(ost->forced_keyframes_pexpr); + av_freep(&ost->avfilter); + av_freep(&ost->logfile_prefix); + + av_freep(&ost->audio_channels_map); + ost->audio_channels_mapped = 0; + + av_dict_free(&ost->sws_dict); + av_dict_free(&ost->swr_opts); + + avcodec_free_context(&ost->enc_ctx); + avcodec_parameters_free(&ost->ref_par); + + if (ost->muxing_queue) { + AVPacket *pkt; + while (av_fifo_read(ost->muxing_queue, &pkt, 1) >= 0) + av_packet_free(&pkt); + av_fifo_freep2(&ost->muxing_queue); + } + + av_freep(&output_streams[i]); + } +#if HAVE_THREADS + free_input_threads(); +#endif + for (i = 0; i < nb_input_files; i++) { + avformat_close_input(&input_files[i]->ctx); + av_packet_free(&input_files[i]->pkt); + av_freep(&input_files[i]); + } + for (i = 0; i < nb_input_streams; i++) { + InputStream *ist = input_streams[i]; + + av_frame_free(&ist->decoded_frame); + av_packet_free(&ist->pkt); + av_dict_free(&ist->decoder_opts); + avsubtitle_free(&ist->prev_sub.subtitle); + av_frame_free(&ist->sub2video.frame); + av_freep(&ist->filters); + av_freep(&ist->hwaccel_device); + av_freep(&ist->dts_buffer); + + avcodec_free_context(&ist->dec_ctx); + + av_freep(&input_streams[i]); + } + + if (vstats_file) { + if (fclose(vstats_file)) + av_log(NULL, AV_LOG_ERROR, + "Error closing vstats file, loss of information possible: %s\n", + av_err2str(AVERROR(errno))); + } + av_freep(&vstats_filename); + av_freep(&filter_nbthreads); + + av_freep(&input_streams); + av_freep(&input_files); + av_freep(&output_streams); + av_freep(&output_files); + + uninit_opts(); + + avformat_network_deinit(); + + if (received_sigterm) { + av_log(NULL, AV_LOG_INFO, "Exiting normally, received signal %d.\n", + (int) received_sigterm); + } else if (ret && atomic_load(&transcode_init_done)) { + av_log(NULL, AV_LOG_INFO, "Conversion failed!\n"); + } + term_exit(); + ffmpeg_exited = 1; +} + +void remove_avoptions(AVDictionary **a, AVDictionary *b) +{ + const AVDictionaryEntry *t = NULL; + + while ((t = av_dict_get(b, "", t, AV_DICT_IGNORE_SUFFIX))) { + av_dict_set(a, t->key, NULL, AV_DICT_MATCH_CASE); + } +} + +void assert_avoptions(AVDictionary *m) +{ + const AVDictionaryEntry *t; + if ((t = av_dict_get(m, "", NULL, AV_DICT_IGNORE_SUFFIX))) { + av_log(NULL, AV_LOG_FATAL, "Option %s not found.\n", t->key); + exit_program(1); + } +} + +static void abort_codec_experimental(const AVCodec *c, int encoder) +{ + exit_program(1); +} + +static void update_benchmark(const char *fmt, ...) +{ + if (do_benchmark_all) { + BenchmarkTimeStamps t = get_benchmark_time_stamps(); + va_list va; + char buf[1024]; + + if (fmt) { + va_start(va, fmt); + vsnprintf(buf, sizeof(buf), fmt, va); + va_end(va); + av_log(NULL, AV_LOG_INFO, + "bench: %8" PRIu64 " user %8" PRIu64 " sys %8" PRIu64 " real %s \n", + t.user_usec - current_time.user_usec, + t.sys_usec - current_time.sys_usec, + t.real_usec - current_time.real_usec, buf); + } + current_time = t; + } +} + +static void close_output_stream(OutputStream *ost) +{ + OutputFile *of = output_files[ost->file_index]; + AVRational time_base = ost->stream_copy ? ost->mux_timebase : ost->enc_ctx->time_base; + + ost->finished |= ENCODER_FINISHED; + if (of->shortest) { + int64_t end = av_rescale_q(ost->sync_opts - ost->first_pts, time_base, AV_TIME_BASE_Q); + of->recording_time = FFMIN(of->recording_time, end); + } +} + +/* + * Send a single packet to the output, applying any bitstream filters + * associated with the output stream. This may result in any number + * of packets actually being written, depending on what bitstream + * filters are applied. The supplied packet is consumed and will be + * blank (as if newly-allocated) when this function returns. + * + * If eof is set, instead indicate EOF to all bitstream filters and + * therefore flush any delayed packets to the output. A blank packet + * must be supplied in this case. + */ +static void output_packet(OutputFile *of, AVPacket *pkt, + OutputStream *ost, int eof) +{ + int ret = 0; + + /* apply the output bitstream filters */ + if (ost->bsf_ctx) { + ret = av_bsf_send_packet(ost->bsf_ctx, eof ? NULL : pkt); + if (ret < 0) + goto finish; + while ((ret = av_bsf_receive_packet(ost->bsf_ctx, pkt)) >= 0) + of_write_packet(of, pkt, ost, 0); + if (ret == AVERROR(EAGAIN)) + ret = 0; + } else if (!eof) + of_write_packet(of, pkt, ost, 0); + +finish: + if (ret < 0 && ret != AVERROR_EOF) { + av_log(NULL, AV_LOG_ERROR, "Error applying bitstream filters to an output " + "packet for stream #%d:%d.\n", ost->file_index, ost->index); + if(exit_on_error) + exit_program(1); + } +} + +static int check_recording_time(OutputStream *ost) +{ + OutputFile *of = output_files[ost->file_index]; + + if (of->recording_time != INT64_MAX && + av_compare_ts(ost->sync_opts - ost->first_pts, ost->enc_ctx->time_base, of->recording_time, + AV_TIME_BASE_Q) >= 0) { + close_output_stream(ost); + return 0; + } + return 1; +} + +static double adjust_frame_pts_to_encoder_tb(OutputFile *of, OutputStream *ost, + AVFrame *frame) +{ + double float_pts = AV_NOPTS_VALUE; // this is identical to frame.pts but with higher precision + AVCodecContext *enc = ost->enc_ctx; + if (!frame || frame->pts == AV_NOPTS_VALUE || + !enc || !ost->filter || !ost->filter->graph->graph) + goto early_exit; + + { + AVFilterContext *filter = ost->filter->filter; + + int64_t start_time = (of->start_time == AV_NOPTS_VALUE) ? 0 : of->start_time; + AVRational filter_tb = av_buffersink_get_time_base(filter); + AVRational tb = enc->time_base; + int extra_bits = av_clip(29 - av_log2(tb.den), 0, 16); + + tb.den <<= extra_bits; + float_pts = + av_rescale_q(frame->pts, filter_tb, tb) - + av_rescale_q(start_time, AV_TIME_BASE_Q, tb); + float_pts /= 1 << extra_bits; + // avoid exact midoints to reduce the chance of rounding differences, this can be removed in case the fps code is changed to work with integers + float_pts += FFSIGN(float_pts) * 1.0 / (1<<17); + + frame->pts = + av_rescale_q(frame->pts, filter_tb, enc->time_base) - + av_rescale_q(start_time, AV_TIME_BASE_Q, enc->time_base); + } + +early_exit: + + if (debug_ts) { + av_log(NULL, AV_LOG_INFO, "filter -> pts:%s pts_time:%s exact:%f time_base:%d/%d\n", + frame ? av_ts2str(frame->pts) : "NULL", + frame ? av_ts2timestr(frame->pts, &enc->time_base) : "NULL", + float_pts, + enc ? enc->time_base.num : -1, + enc ? enc->time_base.den : -1); + } + + return float_pts; +} + +static int init_output_stream(OutputStream *ost, AVFrame *frame, + char *error, int error_len); + +static int init_output_stream_wrapper(OutputStream *ost, AVFrame *frame, + unsigned int fatal) +{ + int ret = AVERROR_BUG; + char error[1024] = {0}; + + if (ost->initialized) + return 0; + + ret = init_output_stream(ost, frame, error, sizeof(error)); + if (ret < 0) { + av_log(NULL, AV_LOG_ERROR, "Error initializing output stream %d:%d -- %s\n", + ost->file_index, ost->index, error); + + if (fatal) + exit_program(1); + } + + return ret; +} + +static double psnr(double d) +{ + return -10.0 * log10(d); +} + +static void update_video_stats(OutputStream *ost, const AVPacket *pkt, int write_vstats) +{ + const uint8_t *sd = av_packet_get_side_data(pkt, AV_PKT_DATA_QUALITY_STATS, + NULL); + AVCodecContext *enc = ost->enc_ctx; + int64_t frame_number; + double ti1, bitrate, avg_bitrate; + + ost->quality = sd ? AV_RL32(sd) : -1; + ost->pict_type = sd ? sd[4] : AV_PICTURE_TYPE_NONE; + + for (int i = 0; ierror); i++) { + if (sd && i < sd[5]) + ost->error[i] = AV_RL64(sd + 8 + 8*i); + else + ost->error[i] = -1; + } + + if (!write_vstats) + return; + + /* this is executed just the first time update_video_stats is called */ + if (!vstats_file) { + vstats_file = fopen(vstats_filename, "w"); + if (!vstats_file) { + perror("fopen"); + exit_program(1); + } + } + + frame_number = ost->packets_encoded; + if (vstats_version <= 1) { + fprintf(vstats_file, "frame= %5"PRId64" q= %2.1f ", frame_number, + ost->quality / (float)FF_QP2LAMBDA); + } else { + fprintf(vstats_file, "out= %2d st= %2d frame= %5"PRId64" q= %2.1f ", ost->file_index, ost->index, frame_number, + ost->quality / (float)FF_QP2LAMBDA); + } + + if (ost->error[0]>=0 && (enc->flags & AV_CODEC_FLAG_PSNR)) + fprintf(vstats_file, "PSNR= %6.2f ", psnr(ost->error[0] / (enc->width * enc->height * 255.0 * 255.0))); + + fprintf(vstats_file,"f_size= %6d ", pkt->size); + /* compute pts value */ + ti1 = pkt->dts * av_q2d(ost->mux_timebase); + if (ti1 < 0.01) + ti1 = 0.01; + + bitrate = (pkt->size * 8) / av_q2d(enc->time_base) / 1000.0; + avg_bitrate = (double)(ost->data_size * 8) / ti1 / 1000.0; + fprintf(vstats_file, "s_size= %8.0fkB time= %0.3f br= %7.1fkbits/s avg_br= %7.1fkbits/s ", + (double)ost->data_size / 1024, ti1, bitrate, avg_bitrate); + fprintf(vstats_file, "type= %c\n", av_get_picture_type_char(ost->pict_type)); +} + +static int encode_frame(OutputFile *of, OutputStream *ost, AVFrame *frame) +{ + AVCodecContext *enc = ost->enc_ctx; + AVPacket *pkt = ost->pkt; + const char *type_desc = av_get_media_type_string(enc->codec_type); + const char *action = frame ? "encode" : "flush"; + int ret; + + if (frame) { + ost->frames_encoded++; + + if (debug_ts) { + av_log(NULL, AV_LOG_INFO, "encoder <- type:%s " + "frame_pts:%s frame_pts_time:%s time_base:%d/%d\n", + type_desc, + av_ts2str(frame->pts), av_ts2timestr(frame->pts, &enc->time_base), + enc->time_base.num, enc->time_base.den); + } + } + + update_benchmark(NULL); + + ret = avcodec_send_frame(enc, frame); + if (ret < 0 && !(ret == AVERROR_EOF && !frame)) { + av_log(NULL, AV_LOG_ERROR, "Error submitting %s frame to the encoder\n", + type_desc); + return ret; + } + + while (1) { + ret = avcodec_receive_packet(enc, pkt); + update_benchmark("%s_%s %d.%d", action, type_desc, + ost->file_index, ost->index); + + /* if two pass, output log on success and EOF */ + if ((ret >= 0 || ret == AVERROR_EOF) && ost->logfile && enc->stats_out) + fprintf(ost->logfile, "%s", enc->stats_out); + + if (ret == AVERROR(EAGAIN)) { + av_assert0(frame); // should never happen during flushing + return 0; + } else if (ret == AVERROR_EOF) { + output_packet(of, pkt, ost, 1); + return ret; + } else if (ret < 0) { + av_log(NULL, AV_LOG_ERROR, "%s encoding failed\n", type_desc); + return ret; + } + + if (debug_ts) { + av_log(NULL, AV_LOG_INFO, "encoder -> type:%s " + "pkt_pts:%s pkt_pts_time:%s pkt_dts:%s pkt_dts_time:%s " + "duration:%s duration_time:%s\n", + type_desc, + av_ts2str(pkt->pts), av_ts2timestr(pkt->pts, &enc->time_base), + av_ts2str(pkt->dts), av_ts2timestr(pkt->dts, &enc->time_base), + av_ts2str(pkt->duration), av_ts2timestr(pkt->duration, &enc->time_base)); + } + + av_packet_rescale_ts(pkt, enc->time_base, ost->mux_timebase); + + if (debug_ts) { + av_log(NULL, AV_LOG_INFO, "encoder -> type:%s " + "pkt_pts:%s pkt_pts_time:%s pkt_dts:%s pkt_dts_time:%s " + "duration:%s duration_time:%s\n", + type_desc, + av_ts2str(pkt->pts), av_ts2timestr(pkt->pts, &enc->time_base), + av_ts2str(pkt->dts), av_ts2timestr(pkt->dts, &enc->time_base), + av_ts2str(pkt->duration), av_ts2timestr(pkt->duration, &enc->time_base)); + } + + if (enc->codec_type == AVMEDIA_TYPE_VIDEO) + update_video_stats(ost, pkt, !!vstats_filename); + + ost->packets_encoded++; + + output_packet(of, pkt, ost, 0); + } + + av_assert0(0); +} + +static void do_audio_out(OutputFile *of, OutputStream *ost, + AVFrame *frame) +{ + int ret; + + adjust_frame_pts_to_encoder_tb(of, ost, frame); + + if (!check_recording_time(ost)) + return; + + if (frame->pts == AV_NOPTS_VALUE || audio_sync_method < 0) + frame->pts = ost->sync_opts; + ost->sync_opts = frame->pts + frame->nb_samples; + ost->samples_encoded += frame->nb_samples; + + ret = encode_frame(of, ost, frame); + if (ret < 0) + exit_program(1); +} + +static void do_subtitle_out(OutputFile *of, + OutputStream *ost, + AVSubtitle *sub) +{ + int subtitle_out_max_size = 1024 * 1024; + int subtitle_out_size, nb, i; + AVCodecContext *enc; + AVPacket *pkt = ost->pkt; + int64_t pts; + + if (sub->pts == AV_NOPTS_VALUE) { + av_log(NULL, AV_LOG_ERROR, "Subtitle packets must have a pts\n"); + if (exit_on_error) + exit_program(1); + return; + } + + enc = ost->enc_ctx; + + if (!subtitle_out) { + subtitle_out = av_malloc(subtitle_out_max_size); + if (!subtitle_out) { + av_log(NULL, AV_LOG_FATAL, "Failed to allocate subtitle_out\n"); + exit_program(1); + } + } + + /* Note: DVB subtitle need one packet to draw them and one other + packet to clear them */ + /* XXX: signal it in the codec context ? */ + if (enc->codec_id == AV_CODEC_ID_DVB_SUBTITLE) + nb = 2; + else + nb = 1; + + /* shift timestamp to honor -ss and make check_recording_time() work with -t */ + pts = sub->pts; + if (output_files[ost->file_index]->start_time != AV_NOPTS_VALUE) + pts -= output_files[ost->file_index]->start_time; + for (i = 0; i < nb; i++) { + unsigned save_num_rects = sub->num_rects; + + ost->sync_opts = av_rescale_q(pts, AV_TIME_BASE_Q, enc->time_base); + if (!check_recording_time(ost)) + return; + + sub->pts = pts; + // start_display_time is required to be 0 + sub->pts += av_rescale_q(sub->start_display_time, (AVRational){ 1, 1000 }, AV_TIME_BASE_Q); + sub->end_display_time -= sub->start_display_time; + sub->start_display_time = 0; + if (i == 1) + sub->num_rects = 0; + + ost->frames_encoded++; + + subtitle_out_size = avcodec_encode_subtitle(enc, subtitle_out, + subtitle_out_max_size, sub); + if (i == 1) + sub->num_rects = save_num_rects; + if (subtitle_out_size < 0) { + av_log(NULL, AV_LOG_FATAL, "Subtitle encoding failed\n"); + exit_program(1); + } + + av_packet_unref(pkt); + pkt->data = subtitle_out; + pkt->size = subtitle_out_size; + pkt->pts = av_rescale_q(sub->pts, AV_TIME_BASE_Q, ost->mux_timebase); + pkt->duration = av_rescale_q(sub->end_display_time, (AVRational){ 1, 1000 }, ost->mux_timebase); + if (enc->codec_id == AV_CODEC_ID_DVB_SUBTITLE) { + /* XXX: the pts correction is handled here. Maybe handling + it in the codec would be better */ + if (i == 0) + pkt->pts += av_rescale_q(sub->start_display_time, (AVRational){ 1, 1000 }, ost->mux_timebase); + else + pkt->pts += av_rescale_q(sub->end_display_time, (AVRational){ 1, 1000 }, ost->mux_timebase); + } + pkt->dts = pkt->pts; + output_packet(of, pkt, ost, 0); + } +} + +/* May modify/reset next_picture */ +static void do_video_out(OutputFile *of, + OutputStream *ost, + AVFrame *next_picture) +{ + int ret; + AVCodecContext *enc = ost->enc_ctx; + AVRational frame_rate; + int64_t nb_frames, nb0_frames, i; + double delta, delta0; + double duration = 0; + double sync_ipts = AV_NOPTS_VALUE; + InputStream *ist = NULL; + AVFilterContext *filter = ost->filter->filter; + + init_output_stream_wrapper(ost, next_picture, 1); + sync_ipts = adjust_frame_pts_to_encoder_tb(of, ost, next_picture); + + if (ost->source_index >= 0) + ist = input_streams[ost->source_index]; + + frame_rate = av_buffersink_get_frame_rate(filter); + if (frame_rate.num > 0 && frame_rate.den > 0) + duration = 1/(av_q2d(frame_rate) * av_q2d(enc->time_base)); + + if(ist && ist->st->start_time != AV_NOPTS_VALUE && ist->first_dts != AV_NOPTS_VALUE && ost->frame_rate.num) + duration = FFMIN(duration, 1/(av_q2d(ost->frame_rate) * av_q2d(enc->time_base))); + + if (!ost->filters_script && + !ost->filters && + (nb_filtergraphs == 0 || !filtergraphs[0]->graph_desc) && + next_picture && + ist && + lrintf(next_picture->pkt_duration * av_q2d(ist->st->time_base) / av_q2d(enc->time_base)) > 0) { + duration = lrintf(next_picture->pkt_duration * av_q2d(ist->st->time_base) / av_q2d(enc->time_base)); + } + + if (!next_picture) { + //end, flushing + nb0_frames = nb_frames = mid_pred(ost->last_nb0_frames[0], + ost->last_nb0_frames[1], + ost->last_nb0_frames[2]); + } else { + delta0 = sync_ipts - ost->sync_opts; // delta0 is the "drift" between the input frame (next_picture) and where it would fall in the output. + delta = delta0 + duration; + + /* by default, we output a single frame */ + nb0_frames = 0; // tracks the number of times the PREVIOUS frame should be duplicated, mostly for variable framerate (VFR) + nb_frames = 1; + + if (delta0 < 0 && + delta > 0 && + ost->vsync_method != VSYNC_PASSTHROUGH && + ost->vsync_method != VSYNC_DROP) { + if (delta0 < -0.6) { + av_log(NULL, AV_LOG_VERBOSE, "Past duration %f too large\n", -delta0); + } else + av_log(NULL, AV_LOG_DEBUG, "Clipping frame in rate conversion by %f\n", -delta0); + sync_ipts = ost->sync_opts; + duration += delta0; + delta0 = 0; + } + + switch (ost->vsync_method) { + case VSYNC_VSCFR: + if (ost->frame_number == 0 && delta0 >= 0.5) { + av_log(NULL, AV_LOG_DEBUG, "Not duplicating %d initial frames\n", (int)lrintf(delta0)); + delta = duration; + delta0 = 0; + ost->sync_opts = llrint(sync_ipts); + } + case VSYNC_CFR: + // FIXME set to 0.5 after we fix some dts/pts bugs like in avidec.c + if (frame_drop_threshold && delta < frame_drop_threshold && ost->frame_number) { + nb_frames = 0; + } else if (delta < -1.1) + nb_frames = 0; + else if (delta > 1.1) { + nb_frames = llrintf(delta); + if (delta0 > 1.1) + nb0_frames = llrintf(delta0 - 0.6); + } + break; + case VSYNC_VFR: + if (delta <= -0.6) + nb_frames = 0; + else if (delta > 0.6) + ost->sync_opts = llrint(sync_ipts); + break; + case VSYNC_DROP: + case VSYNC_PASSTHROUGH: + ost->sync_opts = llrint(sync_ipts); + break; + default: + av_assert0(0); + } + } + + /* + * For video, number of frames in == number of packets out. + * But there may be reordering, so we can't throw away frames on encoder + * flush, we need to limit them here, before they go into encoder. + */ + nb_frames = FFMIN(nb_frames, ost->max_frames - ost->frame_number); + nb0_frames = FFMIN(nb0_frames, nb_frames); + + memmove(ost->last_nb0_frames + 1, + ost->last_nb0_frames, + sizeof(ost->last_nb0_frames[0]) * (FF_ARRAY_ELEMS(ost->last_nb0_frames) - 1)); + ost->last_nb0_frames[0] = nb0_frames; + + if (nb0_frames == 0 && ost->last_dropped) { + nb_frames_drop++; + av_log(NULL, AV_LOG_VERBOSE, + "*** dropping frame %"PRId64" from stream %d at ts %"PRId64"\n", + ost->frame_number, ost->st->index, ost->last_frame->pts); + } + if (nb_frames > (nb0_frames && ost->last_dropped) + (nb_frames > nb0_frames)) { + if (nb_frames > dts_error_threshold * 30) { + av_log(NULL, AV_LOG_ERROR, "%"PRId64" frame duplication too large, skipping\n", nb_frames - 1); + nb_frames_drop++; + return; + } + nb_frames_dup += nb_frames - (nb0_frames && ost->last_dropped) - (nb_frames > nb0_frames); + av_log(NULL, AV_LOG_VERBOSE, "*** %"PRId64" dup!\n", nb_frames - 1); + if (nb_frames_dup > dup_warning) { + av_log(NULL, AV_LOG_WARNING, "More than %"PRIu64" frames duplicated\n", dup_warning); + dup_warning *= 10; + } + } + ost->last_dropped = nb_frames == nb0_frames && next_picture; + ost->dropped_keyframe = ost->last_dropped && next_picture && next_picture->key_frame; + + /* duplicates frame if needed */ + for (i = 0; i < nb_frames; i++) { + AVFrame *in_picture; + int forced_keyframe = 0; + double pts_time; + + if (i < nb0_frames && ost->last_frame->buf[0]) { + in_picture = ost->last_frame; + } else + in_picture = next_picture; + + if (!in_picture) + return; + + in_picture->pts = ost->sync_opts; + + if (!check_recording_time(ost)) + return; + + in_picture->quality = enc->global_quality; + in_picture->pict_type = 0; + + if (ost->forced_kf_ref_pts == AV_NOPTS_VALUE && + in_picture->pts != AV_NOPTS_VALUE) + ost->forced_kf_ref_pts = in_picture->pts; + + pts_time = in_picture->pts != AV_NOPTS_VALUE ? + (in_picture->pts - ost->forced_kf_ref_pts) * av_q2d(enc->time_base) : NAN; + if (ost->forced_kf_index < ost->forced_kf_count && + in_picture->pts >= ost->forced_kf_pts[ost->forced_kf_index]) { + ost->forced_kf_index++; + forced_keyframe = 1; + } else if (ost->forced_keyframes_pexpr) { + double res; + ost->forced_keyframes_expr_const_values[FKF_T] = pts_time; + res = av_expr_eval(ost->forced_keyframes_pexpr, + ost->forced_keyframes_expr_const_values, NULL); + ff_dlog(NULL, "force_key_frame: n:%f n_forced:%f prev_forced_n:%f t:%f prev_forced_t:%f -> res:%f\n", + ost->forced_keyframes_expr_const_values[FKF_N], + ost->forced_keyframes_expr_const_values[FKF_N_FORCED], + ost->forced_keyframes_expr_const_values[FKF_PREV_FORCED_N], + ost->forced_keyframes_expr_const_values[FKF_T], + ost->forced_keyframes_expr_const_values[FKF_PREV_FORCED_T], + res); + if (res) { + forced_keyframe = 1; + ost->forced_keyframes_expr_const_values[FKF_PREV_FORCED_N] = + ost->forced_keyframes_expr_const_values[FKF_N]; + ost->forced_keyframes_expr_const_values[FKF_PREV_FORCED_T] = + ost->forced_keyframes_expr_const_values[FKF_T]; + ost->forced_keyframes_expr_const_values[FKF_N_FORCED] += 1; + } + + ost->forced_keyframes_expr_const_values[FKF_N] += 1; + } else if ( ost->forced_keyframes + && !strncmp(ost->forced_keyframes, "source", 6) + && in_picture->key_frame==1 + && !i) { + forced_keyframe = 1; + } else if ( ost->forced_keyframes + && !strncmp(ost->forced_keyframes, "source_no_drop", 14) + && !i) { + forced_keyframe = (in_picture->key_frame == 1) || ost->dropped_keyframe; + ost->dropped_keyframe = 0; + } + + if (forced_keyframe) { + in_picture->pict_type = AV_PICTURE_TYPE_I; + av_log(NULL, AV_LOG_DEBUG, "Forced keyframe at time %f\n", pts_time); + } + + ret = encode_frame(of, ost, in_picture); + if (ret < 0) + exit_program(1); + + ost->sync_opts++; + ost->frame_number++; + } + + av_frame_unref(ost->last_frame); + if (next_picture) + av_frame_move_ref(ost->last_frame, next_picture); +} + +static void finish_output_stream(OutputStream *ost) +{ + OutputFile *of = output_files[ost->file_index]; + AVRational time_base = ost->stream_copy ? ost->mux_timebase : ost->enc_ctx->time_base; + + ost->finished = ENCODER_FINISHED | MUXER_FINISHED; + + if (of->shortest) { + int64_t end = av_rescale_q(ost->sync_opts - ost->first_pts, time_base, AV_TIME_BASE_Q); + of->recording_time = FFMIN(of->recording_time, end); + } +} + +/** + * Get and encode new output from any of the filtergraphs, without causing + * activity. + * + * @return 0 for success, <0 for severe errors + */ +static int reap_filters(int flush) +{ + AVFrame *filtered_frame = NULL; + int i; + + /* Reap all buffers present in the buffer sinks */ + for (i = 0; i < nb_output_streams; i++) { + OutputStream *ost = output_streams[i]; + OutputFile *of = output_files[ost->file_index]; + AVFilterContext *filter; + AVCodecContext *enc = ost->enc_ctx; + int ret = 0; + + if (!ost->filter || !ost->filter->graph->graph) + continue; + filter = ost->filter->filter; + + /* + * Unlike video, with audio the audio frame size matters. + * Currently we are fully reliant on the lavfi filter chain to + * do the buffering deed for us, and thus the frame size parameter + * needs to be set accordingly. Where does one get the required + * frame size? From the initialized AVCodecContext of an audio + * encoder. Thus, if we have gotten to an audio stream, initialize + * the encoder earlier than receiving the first AVFrame. + */ + if (av_buffersink_get_type(filter) == AVMEDIA_TYPE_AUDIO) + init_output_stream_wrapper(ost, NULL, 1); + + filtered_frame = ost->filtered_frame; + + while (1) { + ret = av_buffersink_get_frame_flags(filter, filtered_frame, + AV_BUFFERSINK_FLAG_NO_REQUEST); + if (ret < 0) { + if (ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) { + av_log(NULL, AV_LOG_WARNING, + "Error in av_buffersink_get_frame_flags(): %s\n", av_err2str(ret)); + } else if (flush && ret == AVERROR_EOF) { + if (av_buffersink_get_type(filter) == AVMEDIA_TYPE_VIDEO) + do_video_out(of, ost, NULL); + } + break; + } + if (ost->finished) { + av_frame_unref(filtered_frame); + continue; + } + + switch (av_buffersink_get_type(filter)) { + case AVMEDIA_TYPE_VIDEO: + if (!ost->frame_aspect_ratio.num) + enc->sample_aspect_ratio = filtered_frame->sample_aspect_ratio; + + do_video_out(of, ost, filtered_frame); + break; + case AVMEDIA_TYPE_AUDIO: + if (!(enc->codec->capabilities & AV_CODEC_CAP_PARAM_CHANGE) && + enc->ch_layout.nb_channels != filtered_frame->ch_layout.nb_channels) { + av_log(NULL, AV_LOG_ERROR, + "Audio filter graph output is not normalized and encoder does not support parameter changes\n"); + break; + } + do_audio_out(of, ost, filtered_frame); + break; + default: + // TODO support subtitle filters + av_assert0(0); + } + + av_frame_unref(filtered_frame); + } + } + + return 0; +} + +static void print_final_stats(int64_t total_size) +{ + uint64_t video_size = 0, audio_size = 0, extra_size = 0, other_size = 0; + uint64_t subtitle_size = 0; + uint64_t data_size = 0; + float percent = -1.0; + int i, j; + int pass1_used = 1; + + for (i = 0; i < nb_output_streams; i++) { + OutputStream *ost = output_streams[i]; + switch (ost->enc_ctx->codec_type) { + case AVMEDIA_TYPE_VIDEO: video_size += ost->data_size; break; + case AVMEDIA_TYPE_AUDIO: audio_size += ost->data_size; break; + case AVMEDIA_TYPE_SUBTITLE: subtitle_size += ost->data_size; break; + default: other_size += ost->data_size; break; + } + extra_size += ost->enc_ctx->extradata_size; + data_size += ost->data_size; + if ( (ost->enc_ctx->flags & (AV_CODEC_FLAG_PASS1 | AV_CODEC_FLAG_PASS2)) + != AV_CODEC_FLAG_PASS1) + pass1_used = 0; + } + + if (data_size && total_size>0 && total_size >= data_size) + percent = 100.0 * (total_size - data_size) / data_size; + + av_log(NULL, AV_LOG_INFO, "video:%1.0fkB audio:%1.0fkB subtitle:%1.0fkB other streams:%1.0fkB global headers:%1.0fkB muxing overhead: ", + video_size / 1024.0, + audio_size / 1024.0, + subtitle_size / 1024.0, + other_size / 1024.0, + extra_size / 1024.0); + if (percent >= 0.0) + av_log(NULL, AV_LOG_INFO, "%f%%", percent); + else + av_log(NULL, AV_LOG_INFO, "unknown"); + av_log(NULL, AV_LOG_INFO, "\n"); + + /* print verbose per-stream stats */ + for (i = 0; i < nb_input_files; i++) { + InputFile *f = input_files[i]; + uint64_t total_packets = 0, total_size = 0; + + av_log(NULL, AV_LOG_VERBOSE, "Input file #%d (%s):\n", + i, f->ctx->url); + + for (j = 0; j < f->nb_streams; j++) { + InputStream *ist = input_streams[f->ist_index + j]; + enum AVMediaType type = ist->dec_ctx->codec_type; + + total_size += ist->data_size; + total_packets += ist->nb_packets; + + av_log(NULL, AV_LOG_VERBOSE, " Input stream #%d:%d (%s): ", + i, j, av_get_media_type_string(type)); + av_log(NULL, AV_LOG_VERBOSE, "%"PRIu64" packets read (%"PRIu64" bytes); ", + ist->nb_packets, ist->data_size); + + if (ist->decoding_needed) { + av_log(NULL, AV_LOG_VERBOSE, "%"PRIu64" frames decoded", + ist->frames_decoded); + if (type == AVMEDIA_TYPE_AUDIO) + av_log(NULL, AV_LOG_VERBOSE, " (%"PRIu64" samples)", ist->samples_decoded); + av_log(NULL, AV_LOG_VERBOSE, "; "); + } + + av_log(NULL, AV_LOG_VERBOSE, "\n"); + } + + av_log(NULL, AV_LOG_VERBOSE, " Total: %"PRIu64" packets (%"PRIu64" bytes) demuxed\n", + total_packets, total_size); + } + + for (i = 0; i < nb_output_files; i++) { + OutputFile *of = output_files[i]; + uint64_t total_packets = 0, total_size = 0; + + av_log(NULL, AV_LOG_VERBOSE, "Output file #%d (%s):\n", + i, of->ctx->url); + + for (j = 0; j < of->ctx->nb_streams; j++) { + OutputStream *ost = output_streams[of->ost_index + j]; + enum AVMediaType type = ost->enc_ctx->codec_type; + + total_size += ost->data_size; + total_packets += ost->packets_written; + + av_log(NULL, AV_LOG_VERBOSE, " Output stream #%d:%d (%s): ", + i, j, av_get_media_type_string(type)); + if (ost->encoding_needed) { + av_log(NULL, AV_LOG_VERBOSE, "%"PRIu64" frames encoded", + ost->frames_encoded); + if (type == AVMEDIA_TYPE_AUDIO) + av_log(NULL, AV_LOG_VERBOSE, " (%"PRIu64" samples)", ost->samples_encoded); + av_log(NULL, AV_LOG_VERBOSE, "; "); + } + + av_log(NULL, AV_LOG_VERBOSE, "%"PRIu64" packets muxed (%"PRIu64" bytes); ", + ost->packets_written, ost->data_size); + + av_log(NULL, AV_LOG_VERBOSE, "\n"); + } + + av_log(NULL, AV_LOG_VERBOSE, " Total: %"PRIu64" packets (%"PRIu64" bytes) muxed\n", + total_packets, total_size); + } + if(video_size + data_size + audio_size + subtitle_size + extra_size == 0){ + av_log(NULL, AV_LOG_WARNING, "Output file is empty, nothing was encoded "); + if (pass1_used) { + av_log(NULL, AV_LOG_WARNING, "\n"); + } else { + av_log(NULL, AV_LOG_WARNING, "(check -ss / -t / -frames parameters if used)\n"); + } + } +} + +EM_JS(void, send_progress, (double progress, double time), { + Module.receiveProgress(progress, time); +}); + +static void print_report(int is_last_report, int64_t timer_start, int64_t cur_time) +{ + AVBPrint buf, buf_script; + OutputStream *ost; + AVFormatContext *oc; + int64_t total_size; + AVCodecContext *enc; + int vid, i; + double bitrate; + double speed; + int64_t pts = INT64_MIN + 1; + static int64_t last_time = -1; + static int first_report = 1; + static int qp_histogram[52]; + int hours, mins, secs, us; + const char *hours_sign; + int ret; + float t; + + if (!print_stats && !is_last_report && !progress_avio) + return; + + if (!is_last_report) { + if (last_time == -1) { + last_time = cur_time; + } + if (((cur_time - last_time) < stats_period && !first_report) || + (first_report && nb_output_dumped < nb_output_files)) + return; + last_time = cur_time; + } + + t = (cur_time-timer_start) / 1000000.0; + + + oc = output_files[0]->ctx; + + total_size = avio_size(oc->pb); + if (total_size <= 0) // FIXME improve avio_size() so it works with non seekable output too + total_size = avio_tell(oc->pb); + + vid = 0; + av_bprint_init(&buf, 0, AV_BPRINT_SIZE_AUTOMATIC); + av_bprint_init(&buf_script, 0, AV_BPRINT_SIZE_AUTOMATIC); + for (i = 0; i < nb_output_streams; i++) { + float q = -1; + ost = output_streams[i]; + enc = ost->enc_ctx; + if (!ost->stream_copy) + q = ost->quality / (float) FF_QP2LAMBDA; + + if (vid && enc->codec_type == AVMEDIA_TYPE_VIDEO) { + av_bprintf(&buf, "q=%2.1f ", q); + av_bprintf(&buf_script, "stream_%d_%d_q=%.1f\n", + ost->file_index, ost->index, q); + } + if (!vid && enc->codec_type == AVMEDIA_TYPE_VIDEO) { + float fps; + int64_t frame_number = ost->frame_number; + + fps = t > 1 ? frame_number / t : 0; + av_bprintf(&buf, "frame=%5"PRId64" fps=%3.*f q=%3.1f ", + frame_number, fps < 9.95, fps, q); + av_bprintf(&buf_script, "frame=%"PRId64"\n", frame_number); + av_bprintf(&buf_script, "fps=%.2f\n", fps); + av_bprintf(&buf_script, "stream_%d_%d_q=%.1f\n", + ost->file_index, ost->index, q); + if (is_last_report) + av_bprintf(&buf, "L"); + if (qp_hist) { + int j; + int qp = lrintf(q); + if (qp >= 0 && qp < FF_ARRAY_ELEMS(qp_histogram)) + qp_histogram[qp]++; + for (j = 0; j < 32; j++) + av_bprintf(&buf, "%X", av_log2(qp_histogram[j] + 1)); + } + + if ((enc->flags & AV_CODEC_FLAG_PSNR) && (ost->pict_type != AV_PICTURE_TYPE_NONE || is_last_report)) { + int j; + double error, error_sum = 0; + double scale, scale_sum = 0; + double p; + char type[3] = { 'Y','U','V' }; + av_bprintf(&buf, "PSNR="); + for (j = 0; j < 3; j++) { + if (is_last_report) { + error = enc->error[j]; + scale = enc->width * enc->height * 255.0 * 255.0 * frame_number; + } else { + error = ost->error[j]; + scale = enc->width * enc->height * 255.0 * 255.0; + } + if (j) + scale /= 4; + error_sum += error; + scale_sum += scale; + p = psnr(error / scale); + av_bprintf(&buf, "%c:%2.2f ", type[j], p); + av_bprintf(&buf_script, "stream_%d_%d_psnr_%c=%2.2f\n", + ost->file_index, ost->index, type[j] | 32, p); + } + p = psnr(error_sum / scale_sum); + av_bprintf(&buf, "*:%2.2f ", psnr(error_sum / scale_sum)); + av_bprintf(&buf_script, "stream_%d_%d_psnr_all=%2.2f\n", + ost->file_index, ost->index, p); + } + vid = 1; + } + /* compute min output value */ + if (av_stream_get_end_pts(ost->st) != AV_NOPTS_VALUE) { + pts = FFMAX(pts, av_rescale_q(av_stream_get_end_pts(ost->st), + ost->st->time_base, AV_TIME_BASE_Q)); + if (copy_ts) { + if (copy_ts_first_pts == AV_NOPTS_VALUE && pts > 1) + copy_ts_first_pts = pts; + if (copy_ts_first_pts != AV_NOPTS_VALUE) + pts -= copy_ts_first_pts; + } + } + + if (is_last_report) + nb_frames_drop += ost->last_dropped; + } + + /* send_progress here only works when the duration of + * input and output file are the same, other cases (ex. trim) + * still WIP. + * + * TODO: support cases like trim. + */ + int64_t duration = -1; + int64_t pts_abs = FFABS(pts); + /* Use the longest duration among all input files. + */ + for (int i = 0; i < nb_input_files; i++) { + int64_t file_duration = input_files[i]->ctx->duration; + if (file_duration > duration) { + duration = file_duration; + } + } + send_progress((double)pts_abs / (double)duration, (double)pts_abs); + + secs = FFABS(pts) / AV_TIME_BASE; + us = FFABS(pts) % AV_TIME_BASE; + mins = secs / 60; + secs %= 60; + hours = mins / 60; + mins %= 60; + hours_sign = (pts < 0) ? "-" : ""; + + bitrate = pts && total_size >= 0 ? total_size * 8 / (pts / 1000.0) : -1; + speed = t != 0.0 ? (double)pts / AV_TIME_BASE / t : -1; + + if (total_size < 0) av_bprintf(&buf, "size=N/A time="); + else av_bprintf(&buf, "size=%8.0fkB time=", total_size / 1024.0); + if (pts == AV_NOPTS_VALUE) { + av_bprintf(&buf, "N/A "); + } else { + av_bprintf(&buf, "%s%02d:%02d:%02d.%02d ", + hours_sign, hours, mins, secs, (100 * us) / AV_TIME_BASE); + } + + if (bitrate < 0) { + av_bprintf(&buf, "bitrate=N/A"); + av_bprintf(&buf_script, "bitrate=N/A\n"); + }else{ + av_bprintf(&buf, "bitrate=%6.1fkbits/s", bitrate); + av_bprintf(&buf_script, "bitrate=%6.1fkbits/s\n", bitrate); + } + + if (total_size < 0) av_bprintf(&buf_script, "total_size=N/A\n"); + else av_bprintf(&buf_script, "total_size=%"PRId64"\n", total_size); + if (pts == AV_NOPTS_VALUE) { + av_bprintf(&buf_script, "out_time_us=N/A\n"); + av_bprintf(&buf_script, "out_time_ms=N/A\n"); + av_bprintf(&buf_script, "out_time=N/A\n"); + } else { + av_bprintf(&buf_script, "out_time_us=%"PRId64"\n", pts); + av_bprintf(&buf_script, "out_time_ms=%"PRId64"\n", pts); + av_bprintf(&buf_script, "out_time=%s%02d:%02d:%02d.%06d\n", + hours_sign, hours, mins, secs, us); + } + + if (nb_frames_dup || nb_frames_drop) + av_bprintf(&buf, " dup=%"PRId64" drop=%"PRId64, nb_frames_dup, nb_frames_drop); + av_bprintf(&buf_script, "dup_frames=%"PRId64"\n", nb_frames_dup); + av_bprintf(&buf_script, "drop_frames=%"PRId64"\n", nb_frames_drop); + + if (speed < 0) { + av_bprintf(&buf, " speed=N/A"); + av_bprintf(&buf_script, "speed=N/A\n"); + } else { + av_bprintf(&buf, " speed=%4.3gx", speed); + av_bprintf(&buf_script, "speed=%4.3gx\n", speed); + } + + if (print_stats || is_last_report) { + // Always print a new line of message. + const char end = '\n'; //is_last_report ? '\n' : '\r'; + if (print_stats==1 && AV_LOG_INFO > av_log_get_level()) { + fprintf(stderr, "%s %c", buf.str, end); + } else + av_log(NULL, AV_LOG_INFO, "%s %c", buf.str, end); + + fflush(stderr); + } + av_bprint_finalize(&buf, NULL); + + if (progress_avio) { + av_bprintf(&buf_script, "progress=%s\n", + is_last_report ? "end" : "continue"); + avio_write(progress_avio, buf_script.str, + FFMIN(buf_script.len, buf_script.size - 1)); + avio_flush(progress_avio); + av_bprint_finalize(&buf_script, NULL); + if (is_last_report) { + if ((ret = avio_closep(&progress_avio)) < 0) + av_log(NULL, AV_LOG_ERROR, + "Error closing progress log, loss of information possible: %s\n", av_err2str(ret)); + } + } + + first_report = 0; + + if (is_last_report) { + // Make sure the progress is ended with 1. + if (pts_abs != duration) send_progress(1, (double)pts_abs); + print_final_stats(total_size); + } +} + +static int ifilter_parameters_from_codecpar(InputFilter *ifilter, AVCodecParameters *par) +{ + int ret; + + // We never got any input. Set a fake format, which will + // come from libavformat. + ifilter->format = par->format; + ifilter->sample_rate = par->sample_rate; + ifilter->width = par->width; + ifilter->height = par->height; + ifilter->sample_aspect_ratio = par->sample_aspect_ratio; + ret = av_channel_layout_copy(&ifilter->ch_layout, &par->ch_layout); + if (ret < 0) + return ret; + + return 0; +} + +static void flush_encoders(void) +{ + int i, ret; + + for (i = 0; i < nb_output_streams; i++) { + OutputStream *ost = output_streams[i]; + AVCodecContext *enc = ost->enc_ctx; + OutputFile *of = output_files[ost->file_index]; + + if (!ost->encoding_needed) + continue; + + // Try to enable encoding with no input frames. + // Maybe we should just let encoding fail instead. + if (!ost->initialized) { + FilterGraph *fg = ost->filter->graph; + + av_log(NULL, AV_LOG_WARNING, + "Finishing stream %d:%d without any data written to it.\n", + ost->file_index, ost->st->index); + + if (ost->filter && !fg->graph) { + int x; + for (x = 0; x < fg->nb_inputs; x++) { + InputFilter *ifilter = fg->inputs[x]; + if (ifilter->format < 0 && + ifilter_parameters_from_codecpar(ifilter, ifilter->ist->st->codecpar) < 0) { + av_log(NULL, AV_LOG_ERROR, "Error copying paramerets from input stream\n"); + exit_program(1); + } + } + + if (!ifilter_has_all_input_formats(fg)) + continue; + + ret = configure_filtergraph(fg); + if (ret < 0) { + av_log(NULL, AV_LOG_ERROR, "Error configuring filter graph\n"); + exit_program(1); + } + + finish_output_stream(ost); + } + + init_output_stream_wrapper(ost, NULL, 1); + } + + if (enc->codec_type != AVMEDIA_TYPE_VIDEO && enc->codec_type != AVMEDIA_TYPE_AUDIO) + continue; + + ret = encode_frame(of, ost, NULL); + if (ret != AVERROR_EOF) + exit_program(1); + } +} + +/* + * Check whether a packet from ist should be written into ost at this time + */ +static int check_output_constraints(InputStream *ist, OutputStream *ost) +{ + OutputFile *of = output_files[ost->file_index]; + int ist_index = input_files[ist->file_index]->ist_index + ist->st->index; + + if (ost->source_index != ist_index) + return 0; + + if (ost->finished & MUXER_FINISHED) + return 0; + + if (of->start_time != AV_NOPTS_VALUE && ist->pts < of->start_time) + return 0; + + return 1; +} + +static void do_streamcopy(InputStream *ist, OutputStream *ost, const AVPacket *pkt) +{ + OutputFile *of = output_files[ost->file_index]; + InputFile *f = input_files [ist->file_index]; + int64_t start_time = (of->start_time == AV_NOPTS_VALUE) ? 0 : of->start_time; + int64_t ost_tb_start_time = av_rescale_q(start_time, AV_TIME_BASE_Q, ost->mux_timebase); + AVPacket *opkt = ost->pkt; + + av_packet_unref(opkt); + // EOF: flush output bitstream filters. + if (!pkt) { + output_packet(of, opkt, ost, 1); + return; + } + + if (!ost->streamcopy_started && !(pkt->flags & AV_PKT_FLAG_KEY) && + !ost->copy_initial_nonkeyframes) + return; + + if (!ost->streamcopy_started && !ost->copy_prior_start) { + int64_t comp_start = start_time; + if (copy_ts && f->start_time != AV_NOPTS_VALUE) + comp_start = FFMAX(start_time, f->start_time + f->ts_offset); + if (pkt->pts == AV_NOPTS_VALUE ? + ist->pts < comp_start : + pkt->pts < av_rescale_q(comp_start, AV_TIME_BASE_Q, ist->st->time_base)) + return; + } + + if (of->recording_time != INT64_MAX && + ist->pts >= of->recording_time + start_time) { + close_output_stream(ost); + return; + } + + if (f->recording_time != INT64_MAX) { + start_time = 0; + if (copy_ts) { + start_time += f->start_time != AV_NOPTS_VALUE ? f->start_time : 0; + start_time += start_at_zero ? 0 : f->ctx->start_time; + } + if (ist->pts >= f->recording_time + start_time) { + close_output_stream(ost); + return; + } + } + + if (av_packet_ref(opkt, pkt) < 0) + exit_program(1); + + if (pkt->pts != AV_NOPTS_VALUE) + opkt->pts = av_rescale_q(pkt->pts, ist->st->time_base, ost->mux_timebase) - ost_tb_start_time; + + if (pkt->dts == AV_NOPTS_VALUE) { + opkt->dts = av_rescale_q(ist->dts, AV_TIME_BASE_Q, ost->mux_timebase); + } else if (ost->st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { + int duration = av_get_audio_frame_duration(ist->dec_ctx, pkt->size); + if(!duration) + duration = ist->dec_ctx->frame_size; + opkt->dts = av_rescale_delta(ist->st->time_base, pkt->dts, + (AVRational){1, ist->dec_ctx->sample_rate}, duration, + &ist->filter_in_rescale_delta_last, ost->mux_timebase); + /* dts will be set immediately afterwards to what pts is now */ + opkt->pts = opkt->dts - ost_tb_start_time; + } else + opkt->dts = av_rescale_q(pkt->dts, ist->st->time_base, ost->mux_timebase); + opkt->dts -= ost_tb_start_time; + + opkt->duration = av_rescale_q(pkt->duration, ist->st->time_base, ost->mux_timebase); + + ost->sync_opts += opkt->duration; + + output_packet(of, opkt, ost, 0); + + ost->streamcopy_started = 1; +} + +int guess_input_channel_layout(InputStream *ist) +{ + AVCodecContext *dec = ist->dec_ctx; + + if (dec->ch_layout.order == AV_CHANNEL_ORDER_UNSPEC) { + char layout_name[256]; + + if (dec->ch_layout.nb_channels > ist->guess_layout_max) + return 0; + av_channel_layout_default(&dec->ch_layout, dec->ch_layout.nb_channels); + if (dec->ch_layout.order == AV_CHANNEL_ORDER_UNSPEC) + return 0; + av_channel_layout_describe(&dec->ch_layout, layout_name, sizeof(layout_name)); + av_log(NULL, AV_LOG_WARNING, "Guessed Channel Layout for Input Stream " + "#%d.%d : %s\n", ist->file_index, ist->st->index, layout_name); + } + return 1; +} + +static void check_decode_result(InputStream *ist, int *got_output, int ret) +{ + if (*got_output || ret<0) + decode_error_stat[ret<0] ++; + + if (ret < 0 && exit_on_error) + exit_program(1); + + if (*got_output && ist) { + if (ist->decoded_frame->decode_error_flags || (ist->decoded_frame->flags & AV_FRAME_FLAG_CORRUPT)) { + av_log(NULL, exit_on_error ? AV_LOG_FATAL : AV_LOG_WARNING, + "%s: corrupt decoded frame in stream %d\n", input_files[ist->file_index]->ctx->url, ist->st->index); + if (exit_on_error) + exit_program(1); + } + } +} + +// Filters can be configured only if the formats of all inputs are known. +static int ifilter_has_all_input_formats(FilterGraph *fg) +{ + int i; + for (i = 0; i < fg->nb_inputs; i++) { + if (fg->inputs[i]->format < 0 && (fg->inputs[i]->type == AVMEDIA_TYPE_AUDIO || + fg->inputs[i]->type == AVMEDIA_TYPE_VIDEO)) + return 0; + } + return 1; +} + +static int ifilter_send_frame(InputFilter *ifilter, AVFrame *frame, int keep_reference) +{ + FilterGraph *fg = ifilter->graph; + AVFrameSideData *sd; + int need_reinit, ret; + int buffersrc_flags = AV_BUFFERSRC_FLAG_PUSH; + + if (keep_reference) + buffersrc_flags |= AV_BUFFERSRC_FLAG_KEEP_REF; + + /* determine if the parameters for this input changed */ + need_reinit = ifilter->format != frame->format; + + switch (ifilter->ist->st->codecpar->codec_type) { + case AVMEDIA_TYPE_AUDIO: + need_reinit |= ifilter->sample_rate != frame->sample_rate || + av_channel_layout_compare(&ifilter->ch_layout, &frame->ch_layout); + break; + case AVMEDIA_TYPE_VIDEO: + need_reinit |= ifilter->width != frame->width || + ifilter->height != frame->height; + break; + } + + if (!ifilter->ist->reinit_filters && fg->graph) + need_reinit = 0; + + if (!!ifilter->hw_frames_ctx != !!frame->hw_frames_ctx || + (ifilter->hw_frames_ctx && ifilter->hw_frames_ctx->data != frame->hw_frames_ctx->data)) + need_reinit = 1; + + if (sd = av_frame_get_side_data(frame, AV_FRAME_DATA_DISPLAYMATRIX)) { + if (!ifilter->displaymatrix || memcmp(sd->data, ifilter->displaymatrix, sizeof(int32_t) * 9)) + need_reinit = 1; + } else if (ifilter->displaymatrix) + need_reinit = 1; + + if (need_reinit) { + ret = ifilter_parameters_from_frame(ifilter, frame); + if (ret < 0) + return ret; + } + + /* (re)init the graph if possible, otherwise buffer the frame and return */ + if (need_reinit || !fg->graph) { + if (!ifilter_has_all_input_formats(fg)) { + AVFrame *tmp = av_frame_clone(frame); + if (!tmp) + return AVERROR(ENOMEM); + + ret = av_fifo_write(ifilter->frame_queue, &tmp, 1); + if (ret < 0) + av_frame_free(&tmp); + + return ret; + } + + ret = reap_filters(1); + if (ret < 0 && ret != AVERROR_EOF) { + av_log(NULL, AV_LOG_ERROR, "Error while filtering: %s\n", av_err2str(ret)); + return ret; + } + + ret = configure_filtergraph(fg); + if (ret < 0) { + av_log(NULL, AV_LOG_ERROR, "Error reinitializing filters!\n"); + return ret; + } + } + + ret = av_buffersrc_add_frame_flags(ifilter->filter, frame, buffersrc_flags); + if (ret < 0) { + if (ret != AVERROR_EOF) + av_log(NULL, AV_LOG_ERROR, "Error while filtering: %s\n", av_err2str(ret)); + return ret; + } + + return 0; +} + +static int ifilter_send_eof(InputFilter *ifilter, int64_t pts) +{ + int ret; + + ifilter->eof = 1; + + if (ifilter->filter) { + ret = av_buffersrc_close(ifilter->filter, pts, AV_BUFFERSRC_FLAG_PUSH); + if (ret < 0) + return ret; + } else { + // the filtergraph was never configured + if (ifilter->format < 0) { + ret = ifilter_parameters_from_codecpar(ifilter, ifilter->ist->st->codecpar); + if (ret < 0) + return ret; + } + if (ifilter->format < 0 && (ifilter->type == AVMEDIA_TYPE_AUDIO || ifilter->type == AVMEDIA_TYPE_VIDEO)) { + av_log(NULL, AV_LOG_ERROR, "Cannot determine format of input stream %d:%d after EOF\n", ifilter->ist->file_index, ifilter->ist->st->index); + return AVERROR_INVALIDDATA; + } + } + + return 0; +} + +// This does not quite work like avcodec_decode_audio4/avcodec_decode_video2. +// There is the following difference: if you got a frame, you must call +// it again with pkt=NULL. pkt==NULL is treated differently from pkt->size==0 +// (pkt==NULL means get more output, pkt->size==0 is a flush/drain packet) +static int decode(AVCodecContext *avctx, AVFrame *frame, int *got_frame, AVPacket *pkt) +{ + int ret; + + *got_frame = 0; + + if (pkt) { + ret = avcodec_send_packet(avctx, pkt); + // In particular, we don't expect AVERROR(EAGAIN), because we read all + // decoded frames with avcodec_receive_frame() until done. + if (ret < 0 && ret != AVERROR_EOF) + return ret; + } + + ret = avcodec_receive_frame(avctx, frame); + if (ret < 0 && ret != AVERROR(EAGAIN)) + return ret; + if (ret >= 0) + *got_frame = 1; + + return 0; +} + +static int send_frame_to_filters(InputStream *ist, AVFrame *decoded_frame) +{ + int i, ret; + + av_assert1(ist->nb_filters > 0); /* ensure ret is initialized */ + for (i = 0; i < ist->nb_filters; i++) { + ret = ifilter_send_frame(ist->filters[i], decoded_frame, i < ist->nb_filters - 1); + if (ret == AVERROR_EOF) + ret = 0; /* ignore */ + if (ret < 0) { + av_log(NULL, AV_LOG_ERROR, + "Failed to inject frame into filter network: %s\n", av_err2str(ret)); + break; + } + } + return ret; +} + +static int decode_audio(InputStream *ist, AVPacket *pkt, int *got_output, + int *decode_failed) +{ + AVFrame *decoded_frame = ist->decoded_frame; + AVCodecContext *avctx = ist->dec_ctx; + int ret, err = 0; + AVRational decoded_frame_tb; + + update_benchmark(NULL); + ret = decode(avctx, decoded_frame, got_output, pkt); + update_benchmark("decode_audio %d.%d", ist->file_index, ist->st->index); + if (ret < 0) + *decode_failed = 1; + + if (ret >= 0 && avctx->sample_rate <= 0) { + av_log(avctx, AV_LOG_ERROR, "Sample rate %d invalid\n", avctx->sample_rate); + ret = AVERROR_INVALIDDATA; + } + + if (ret != AVERROR_EOF) + check_decode_result(ist, got_output, ret); + + if (!*got_output || ret < 0) + return ret; + + ist->samples_decoded += decoded_frame->nb_samples; + ist->frames_decoded++; + + /* increment next_dts to use for the case where the input stream does not + have timestamps or there are multiple frames in the packet */ + ist->next_pts += ((int64_t)AV_TIME_BASE * decoded_frame->nb_samples) / + avctx->sample_rate; + ist->next_dts += ((int64_t)AV_TIME_BASE * decoded_frame->nb_samples) / + avctx->sample_rate; + + if (decoded_frame->pts != AV_NOPTS_VALUE) { + decoded_frame_tb = ist->st->time_base; + } else if (pkt && pkt->pts != AV_NOPTS_VALUE) { + decoded_frame->pts = pkt->pts; + decoded_frame_tb = ist->st->time_base; + }else { + decoded_frame->pts = ist->dts; + decoded_frame_tb = AV_TIME_BASE_Q; + } + if (pkt && pkt->duration && ist->prev_pkt_pts != AV_NOPTS_VALUE && + pkt->pts != AV_NOPTS_VALUE && pkt->pts - ist->prev_pkt_pts > pkt->duration) + ist->filter_in_rescale_delta_last = AV_NOPTS_VALUE; + if (pkt) + ist->prev_pkt_pts = pkt->pts; + if (decoded_frame->pts != AV_NOPTS_VALUE) + decoded_frame->pts = av_rescale_delta(decoded_frame_tb, decoded_frame->pts, + (AVRational){1, avctx->sample_rate}, decoded_frame->nb_samples, &ist->filter_in_rescale_delta_last, + (AVRational){1, avctx->sample_rate}); + ist->nb_samples = decoded_frame->nb_samples; + err = send_frame_to_filters(ist, decoded_frame); + + av_frame_unref(decoded_frame); + return err < 0 ? err : ret; +} + +static int decode_video(InputStream *ist, AVPacket *pkt, int *got_output, int64_t *duration_pts, int eof, + int *decode_failed) +{ + AVFrame *decoded_frame = ist->decoded_frame; + int i, ret = 0, err = 0; + int64_t best_effort_timestamp; + int64_t dts = AV_NOPTS_VALUE; + + // With fate-indeo3-2, we're getting 0-sized packets before EOF for some + // reason. This seems like a semi-critical bug. Don't trigger EOF, and + // skip the packet. + if (!eof && pkt && pkt->size == 0) + return 0; + + if (ist->dts != AV_NOPTS_VALUE) + dts = av_rescale_q(ist->dts, AV_TIME_BASE_Q, ist->st->time_base); + if (pkt) { + pkt->dts = dts; // ffmpeg.c probably shouldn't do this + } + + // The old code used to set dts on the drain packet, which does not work + // with the new API anymore. + if (eof) { + void *new = av_realloc_array(ist->dts_buffer, ist->nb_dts_buffer + 1, sizeof(ist->dts_buffer[0])); + if (!new) + return AVERROR(ENOMEM); + ist->dts_buffer = new; + ist->dts_buffer[ist->nb_dts_buffer++] = dts; + } + + update_benchmark(NULL); + ret = decode(ist->dec_ctx, decoded_frame, got_output, pkt); + update_benchmark("decode_video %d.%d", ist->file_index, ist->st->index); + if (ret < 0) + *decode_failed = 1; + + // The following line may be required in some cases where there is no parser + // or the parser does not has_b_frames correctly + if (ist->st->codecpar->video_delay < ist->dec_ctx->has_b_frames) { + if (ist->dec_ctx->codec_id == AV_CODEC_ID_H264) { + ist->st->codecpar->video_delay = ist->dec_ctx->has_b_frames; + } else + av_log(ist->dec_ctx, AV_LOG_WARNING, + "video_delay is larger in decoder than demuxer %d > %d.\n" + "If you want to help, upload a sample " + "of this file to https://streams.videolan.org/upload/ " + "and contact the ffmpeg-devel mailing list. (ffmpeg-devel@ffmpeg.org)\n", + ist->dec_ctx->has_b_frames, + ist->st->codecpar->video_delay); + } + + if (ret != AVERROR_EOF) + check_decode_result(ist, got_output, ret); + + if (*got_output && ret >= 0) { + if (ist->dec_ctx->width != decoded_frame->width || + ist->dec_ctx->height != decoded_frame->height || + ist->dec_ctx->pix_fmt != decoded_frame->format) { + av_log(NULL, AV_LOG_DEBUG, "Frame parameters mismatch context %d,%d,%d != %d,%d,%d\n", + decoded_frame->width, + decoded_frame->height, + decoded_frame->format, + ist->dec_ctx->width, + ist->dec_ctx->height, + ist->dec_ctx->pix_fmt); + } + } + + if (!*got_output || ret < 0) + return ret; + + if(ist->top_field_first>=0) + decoded_frame->top_field_first = ist->top_field_first; + + ist->frames_decoded++; + + if (ist->hwaccel_retrieve_data && decoded_frame->format == ist->hwaccel_pix_fmt) { + err = ist->hwaccel_retrieve_data(ist->dec_ctx, decoded_frame); + if (err < 0) + goto fail; + } + ist->hwaccel_retrieved_pix_fmt = decoded_frame->format; + + best_effort_timestamp= decoded_frame->best_effort_timestamp; + *duration_pts = decoded_frame->pkt_duration; + + if (ist->framerate.num) + best_effort_timestamp = ist->cfr_next_pts++; + + if (eof && best_effort_timestamp == AV_NOPTS_VALUE && ist->nb_dts_buffer > 0) { + best_effort_timestamp = ist->dts_buffer[0]; + + for (i = 0; i < ist->nb_dts_buffer - 1; i++) + ist->dts_buffer[i] = ist->dts_buffer[i + 1]; + ist->nb_dts_buffer--; + } + + if(best_effort_timestamp != AV_NOPTS_VALUE) { + int64_t ts = av_rescale_q(decoded_frame->pts = best_effort_timestamp, ist->st->time_base, AV_TIME_BASE_Q); + + if (ts != AV_NOPTS_VALUE) + ist->next_pts = ist->pts = ts; + } + + if (debug_ts) { + av_log(NULL, AV_LOG_INFO, "decoder -> ist_index:%d type:video " + "frame_pts:%s frame_pts_time:%s best_effort_ts:%"PRId64" best_effort_ts_time:%s keyframe:%d frame_type:%d time_base:%d/%d\n", + ist->st->index, av_ts2str(decoded_frame->pts), + av_ts2timestr(decoded_frame->pts, &ist->st->time_base), + best_effort_timestamp, + av_ts2timestr(best_effort_timestamp, &ist->st->time_base), + decoded_frame->key_frame, decoded_frame->pict_type, + ist->st->time_base.num, ist->st->time_base.den); + } + + if (ist->st->sample_aspect_ratio.num) + decoded_frame->sample_aspect_ratio = ist->st->sample_aspect_ratio; + + err = send_frame_to_filters(ist, decoded_frame); + +fail: + av_frame_unref(decoded_frame); + return err < 0 ? err : ret; +} + +static int transcode_subtitles(InputStream *ist, AVPacket *pkt, int *got_output, + int *decode_failed) +{ + AVSubtitle subtitle; + int free_sub = 1; + int i, ret = avcodec_decode_subtitle2(ist->dec_ctx, + &subtitle, got_output, pkt); + + check_decode_result(NULL, got_output, ret); + + if (ret < 0 || !*got_output) { + *decode_failed = 1; + if (!pkt->size) + sub2video_flush(ist); + return ret; + } + + if (ist->fix_sub_duration) { + int end = 1; + if (ist->prev_sub.got_output) { + end = av_rescale(subtitle.pts - ist->prev_sub.subtitle.pts, + 1000, AV_TIME_BASE); + if (end < ist->prev_sub.subtitle.end_display_time) { + av_log(ist->dec_ctx, AV_LOG_DEBUG, + "Subtitle duration reduced from %"PRId32" to %d%s\n", + ist->prev_sub.subtitle.end_display_time, end, + end <= 0 ? ", dropping it" : ""); + ist->prev_sub.subtitle.end_display_time = end; + } + } + FFSWAP(int, *got_output, ist->prev_sub.got_output); + FFSWAP(int, ret, ist->prev_sub.ret); + FFSWAP(AVSubtitle, subtitle, ist->prev_sub.subtitle); + if (end <= 0) + goto out; + } + + if (!*got_output) + return ret; + + if (ist->sub2video.frame) { + sub2video_update(ist, INT64_MIN, &subtitle); + } else if (ist->nb_filters) { + if (!ist->sub2video.sub_queue) + ist->sub2video.sub_queue = av_fifo_alloc2(8, sizeof(AVSubtitle), AV_FIFO_FLAG_AUTO_GROW); + if (!ist->sub2video.sub_queue) + exit_program(1); + + ret = av_fifo_write(ist->sub2video.sub_queue, &subtitle, 1); + if (ret < 0) + exit_program(1); + free_sub = 0; + } + + if (!subtitle.num_rects) + goto out; + + ist->frames_decoded++; + + for (i = 0; i < nb_output_streams; i++) { + OutputStream *ost = output_streams[i]; + + if (!check_output_constraints(ist, ost) || !ost->encoding_needed + || ost->enc->type != AVMEDIA_TYPE_SUBTITLE) + continue; + + do_subtitle_out(output_files[ost->file_index], ost, &subtitle); + } + +out: + if (free_sub) + avsubtitle_free(&subtitle); + return ret; +} + +static int send_filter_eof(InputStream *ist) +{ + int i, ret; + /* TODO keep pts also in stream time base to avoid converting back */ + int64_t pts = av_rescale_q_rnd(ist->pts, AV_TIME_BASE_Q, ist->st->time_base, + AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX); + + for (i = 0; i < ist->nb_filters; i++) { + ret = ifilter_send_eof(ist->filters[i], pts); + if (ret < 0) + return ret; + } + return 0; +} + +/* pkt = NULL means EOF (needed to flush decoder buffers) */ +static int process_input_packet(InputStream *ist, const AVPacket *pkt, int no_eof) +{ + int ret = 0, i; + int repeating = 0; + int eof_reached = 0; + + AVPacket *avpkt = ist->pkt; + + if (!ist->saw_first_ts) { + ist->first_dts = + ist->dts = ist->st->avg_frame_rate.num ? - ist->dec_ctx->has_b_frames * AV_TIME_BASE / av_q2d(ist->st->avg_frame_rate) : 0; + ist->pts = 0; + if (pkt && pkt->pts != AV_NOPTS_VALUE && !ist->decoding_needed) { + ist->first_dts = + ist->dts += av_rescale_q(pkt->pts, ist->st->time_base, AV_TIME_BASE_Q); + ist->pts = ist->dts; //unused but better to set it to a value thats not totally wrong + } + ist->saw_first_ts = 1; + } + + if (ist->next_dts == AV_NOPTS_VALUE) + ist->next_dts = ist->dts; + if (ist->next_pts == AV_NOPTS_VALUE) + ist->next_pts = ist->pts; + + if (pkt) { + av_packet_unref(avpkt); + ret = av_packet_ref(avpkt, pkt); + if (ret < 0) + return ret; + } + + if (pkt && pkt->dts != AV_NOPTS_VALUE) { + ist->next_dts = ist->dts = av_rescale_q(pkt->dts, ist->st->time_base, AV_TIME_BASE_Q); + if (ist->dec_ctx->codec_type != AVMEDIA_TYPE_VIDEO || !ist->decoding_needed) + ist->next_pts = ist->pts = ist->dts; + } + + // while we have more to decode or while the decoder did output something on EOF + while (ist->decoding_needed) { + int64_t duration_dts = 0; + int64_t duration_pts = 0; + int got_output = 0; + int decode_failed = 0; + + ist->pts = ist->next_pts; + ist->dts = ist->next_dts; + + switch (ist->dec_ctx->codec_type) { + case AVMEDIA_TYPE_AUDIO: + ret = decode_audio (ist, repeating ? NULL : avpkt, &got_output, + &decode_failed); + av_packet_unref(avpkt); + break; + case AVMEDIA_TYPE_VIDEO: + ret = decode_video (ist, repeating ? NULL : avpkt, &got_output, &duration_pts, !pkt, + &decode_failed); + if (!repeating || !pkt || got_output) { + if (pkt && pkt->duration) { + duration_dts = av_rescale_q(pkt->duration, ist->st->time_base, AV_TIME_BASE_Q); + } else if(ist->dec_ctx->framerate.num != 0 && ist->dec_ctx->framerate.den != 0) { + int ticks= av_stream_get_parser(ist->st) ? av_stream_get_parser(ist->st)->repeat_pict+1 : ist->dec_ctx->ticks_per_frame; + duration_dts = ((int64_t)AV_TIME_BASE * + ist->dec_ctx->framerate.den * ticks) / + ist->dec_ctx->framerate.num / ist->dec_ctx->ticks_per_frame; + } + + if(ist->dts != AV_NOPTS_VALUE && duration_dts) { + ist->next_dts += duration_dts; + }else + ist->next_dts = AV_NOPTS_VALUE; + } + + if (got_output) { + if (duration_pts > 0) { + ist->next_pts += av_rescale_q(duration_pts, ist->st->time_base, AV_TIME_BASE_Q); + } else { + ist->next_pts += duration_dts; + } + } + av_packet_unref(avpkt); + break; + case AVMEDIA_TYPE_SUBTITLE: + if (repeating) + break; + ret = transcode_subtitles(ist, avpkt, &got_output, &decode_failed); + if (!pkt && ret >= 0) + ret = AVERROR_EOF; + av_packet_unref(avpkt); + break; + default: + return -1; + } + + if (ret == AVERROR_EOF) { + eof_reached = 1; + break; + } + + if (ret < 0) { + if (decode_failed) { + av_log(NULL, AV_LOG_ERROR, "Error while decoding stream #%d:%d: %s\n", + ist->file_index, ist->st->index, av_err2str(ret)); + } else { + av_log(NULL, AV_LOG_FATAL, "Error while processing the decoded " + "data for stream #%d:%d\n", ist->file_index, ist->st->index); + } + if (!decode_failed || exit_on_error) + exit_program(1); + break; + } + + if (got_output) + ist->got_output = 1; + + if (!got_output) + break; + + // During draining, we might get multiple output frames in this loop. + // ffmpeg.c does not drain the filter chain on configuration changes, + // which means if we send multiple frames at once to the filters, and + // one of those frames changes configuration, the buffered frames will + // be lost. This can upset certain FATE tests. + // Decode only 1 frame per call on EOF to appease these FATE tests. + // The ideal solution would be to rewrite decoding to use the new + // decoding API in a better way. + if (!pkt) + break; + + repeating = 1; + } + + /* after flushing, send an EOF on all the filter inputs attached to the stream */ + /* except when looping we need to flush but not to send an EOF */ + if (!pkt && ist->decoding_needed && eof_reached && !no_eof) { + int ret = send_filter_eof(ist); + if (ret < 0) { + av_log(NULL, AV_LOG_FATAL, "Error marking filters as finished\n"); + exit_program(1); + } + } + + /* handle stream copy */ + if (!ist->decoding_needed && pkt) { + ist->dts = ist->next_dts; + switch (ist->dec_ctx->codec_type) { + case AVMEDIA_TYPE_AUDIO: + av_assert1(pkt->duration >= 0); + if (ist->dec_ctx->sample_rate) { + ist->next_dts += ((int64_t)AV_TIME_BASE * ist->dec_ctx->frame_size) / + ist->dec_ctx->sample_rate; + } else { + ist->next_dts += av_rescale_q(pkt->duration, ist->st->time_base, AV_TIME_BASE_Q); + } + break; + case AVMEDIA_TYPE_VIDEO: + if (ist->framerate.num) { + // TODO: Remove work-around for c99-to-c89 issue 7 + AVRational time_base_q = AV_TIME_BASE_Q; + int64_t next_dts = av_rescale_q(ist->next_dts, time_base_q, av_inv_q(ist->framerate)); + ist->next_dts = av_rescale_q(next_dts + 1, av_inv_q(ist->framerate), time_base_q); + } else if (pkt->duration) { + ist->next_dts += av_rescale_q(pkt->duration, ist->st->time_base, AV_TIME_BASE_Q); + } else if(ist->dec_ctx->framerate.num != 0) { + int ticks= av_stream_get_parser(ist->st) ? av_stream_get_parser(ist->st)->repeat_pict + 1 : ist->dec_ctx->ticks_per_frame; + ist->next_dts += ((int64_t)AV_TIME_BASE * + ist->dec_ctx->framerate.den * ticks) / + ist->dec_ctx->framerate.num / ist->dec_ctx->ticks_per_frame; + } + break; + } + ist->pts = ist->dts; + ist->next_pts = ist->next_dts; + } else if (!ist->decoding_needed) + eof_reached = 1; + + for (i = 0; i < nb_output_streams; i++) { + OutputStream *ost = output_streams[i]; + + if (!check_output_constraints(ist, ost) || ost->encoding_needed) + continue; + + do_streamcopy(ist, ost, pkt); + } + + return !eof_reached; +} + +static enum AVPixelFormat get_format(AVCodecContext *s, const enum AVPixelFormat *pix_fmts) +{ + InputStream *ist = s->opaque; + const enum AVPixelFormat *p; + int ret; + + for (p = pix_fmts; *p != AV_PIX_FMT_NONE; p++) { + const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(*p); + const AVCodecHWConfig *config = NULL; + int i; + + if (!(desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) + break; + + if (ist->hwaccel_id == HWACCEL_GENERIC || + ist->hwaccel_id == HWACCEL_AUTO) { + for (i = 0;; i++) { + config = avcodec_get_hw_config(s->codec, i); + if (!config) + break; + if (!(config->methods & + AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) + continue; + if (config->pix_fmt == *p) + break; + } + } + if (config && config->device_type == ist->hwaccel_device_type) { + ret = hwaccel_decode_init(s); + if (ret < 0) { + if (ist->hwaccel_id == HWACCEL_GENERIC) { + av_log(NULL, AV_LOG_FATAL, + "%s hwaccel requested for input stream #%d:%d, " + "but cannot be initialized.\n", + av_hwdevice_get_type_name(config->device_type), + ist->file_index, ist->st->index); + return AV_PIX_FMT_NONE; + } + continue; + } + + ist->hwaccel_pix_fmt = *p; + break; + } + } + + return *p; +} + +static int init_input_stream(int ist_index, char *error, int error_len) +{ + int ret; + InputStream *ist = input_streams[ist_index]; + + if (ist->decoding_needed) { + const AVCodec *codec = ist->dec; + if (!codec) { + snprintf(error, error_len, "Decoder (codec %s) not found for input stream #%d:%d", + avcodec_get_name(ist->dec_ctx->codec_id), ist->file_index, ist->st->index); + return AVERROR(EINVAL); + } + + ist->dec_ctx->opaque = ist; + ist->dec_ctx->get_format = get_format; +#if LIBAVCODEC_VERSION_MAJOR < 60 + AV_NOWARN_DEPRECATED({ + ist->dec_ctx->thread_safe_callbacks = 1; + }) +#endif + + if (ist->dec_ctx->codec_id == AV_CODEC_ID_DVB_SUBTITLE && + (ist->decoding_needed & DECODING_FOR_OST)) { + av_dict_set(&ist->decoder_opts, "compute_edt", "1", AV_DICT_DONT_OVERWRITE); + if (ist->decoding_needed & DECODING_FOR_FILTER) + av_log(NULL, AV_LOG_WARNING, "Warning using DVB subtitles for filtering and output at the same time is not fully supported, also see -compute_edt [0|1]\n"); + } + + /* Useful for subtitles retiming by lavf (FIXME), skipping samples in + * audio, and video decoders such as cuvid or mediacodec */ + ist->dec_ctx->pkt_timebase = ist->st->time_base; + + if (!av_dict_get(ist->decoder_opts, "threads", NULL, 0)) + av_dict_set(&ist->decoder_opts, "threads", "auto", 0); + /* Attached pics are sparse, therefore we would not want to delay their decoding till EOF. */ + if (ist->st->disposition & AV_DISPOSITION_ATTACHED_PIC) + av_dict_set(&ist->decoder_opts, "threads", "1", 0); + + ret = hw_device_setup_for_decode(ist); + if (ret < 0) { + snprintf(error, error_len, "Device setup failed for " + "decoder on input stream #%d:%d : %s", + ist->file_index, ist->st->index, av_err2str(ret)); + return ret; + } + + if ((ret = avcodec_open2(ist->dec_ctx, codec, &ist->decoder_opts)) < 0) { + if (ret == AVERROR_EXPERIMENTAL) + abort_codec_experimental(codec, 0); + + snprintf(error, error_len, + "Error while opening decoder for input stream " + "#%d:%d : %s", + ist->file_index, ist->st->index, av_err2str(ret)); + return ret; + } + assert_avoptions(ist->decoder_opts); + } + + ist->next_pts = AV_NOPTS_VALUE; + ist->next_dts = AV_NOPTS_VALUE; + + return 0; +} + +static InputStream *get_input_stream(OutputStream *ost) +{ + if (ost->source_index >= 0) + return input_streams[ost->source_index]; + return NULL; +} + +static int compare_int64(const void *a, const void *b) +{ + return FFDIFFSIGN(*(const int64_t *)a, *(const int64_t *)b); +} + +static int init_output_bsfs(OutputStream *ost) +{ + AVBSFContext *ctx = ost->bsf_ctx; + int ret; + + if (!ctx) + return 0; + + ret = avcodec_parameters_copy(ctx->par_in, ost->st->codecpar); + if (ret < 0) + return ret; + + ctx->time_base_in = ost->st->time_base; + + ret = av_bsf_init(ctx); + if (ret < 0) { + av_log(NULL, AV_LOG_ERROR, "Error initializing bitstream filter: %s\n", + ctx->filter->name); + return ret; + } + + ret = avcodec_parameters_copy(ost->st->codecpar, ctx->par_out); + if (ret < 0) + return ret; + ost->st->time_base = ctx->time_base_out; + + return 0; +} + +static int init_output_stream_streamcopy(OutputStream *ost) +{ + OutputFile *of = output_files[ost->file_index]; + InputStream *ist = get_input_stream(ost); + AVCodecParameters *par_dst = ost->st->codecpar; + AVCodecParameters *par_src = ost->ref_par; + AVRational sar; + int i, ret; + uint32_t codec_tag = par_dst->codec_tag; + + av_assert0(ist && !ost->filter); + + ret = avcodec_parameters_to_context(ost->enc_ctx, ist->st->codecpar); + if (ret >= 0) + ret = av_opt_set_dict(ost->enc_ctx, &ost->encoder_opts); + if (ret < 0) { + av_log(NULL, AV_LOG_FATAL, + "Error setting up codec context options.\n"); + return ret; + } + + ret = avcodec_parameters_from_context(par_src, ost->enc_ctx); + if (ret < 0) { + av_log(NULL, AV_LOG_FATAL, + "Error getting reference codec parameters.\n"); + return ret; + } + + if (!codec_tag) { + unsigned int codec_tag_tmp; + if (!of->format->codec_tag || + av_codec_get_id (of->format->codec_tag, par_src->codec_tag) == par_src->codec_id || + !av_codec_get_tag2(of->format->codec_tag, par_src->codec_id, &codec_tag_tmp)) + codec_tag = par_src->codec_tag; + } + + ret = avcodec_parameters_copy(par_dst, par_src); + if (ret < 0) + return ret; + + par_dst->codec_tag = codec_tag; + + if (!ost->frame_rate.num) + ost->frame_rate = ist->framerate; + + if (ost->frame_rate.num) + ost->st->avg_frame_rate = ost->frame_rate; + else + ost->st->avg_frame_rate = ist->st->avg_frame_rate; + + ret = avformat_transfer_internal_stream_timing_info(of->format, ost->st, ist->st, copy_tb); + if (ret < 0) + return ret; + + // copy timebase while removing common factors + if (ost->st->time_base.num <= 0 || ost->st->time_base.den <= 0) { + if (ost->frame_rate.num) + ost->st->time_base = av_inv_q(ost->frame_rate); + else + ost->st->time_base = av_add_q(av_stream_get_codec_timebase(ost->st), (AVRational){0, 1}); + } + + // copy estimated duration as a hint to the muxer + if (ost->st->duration <= 0 && ist->st->duration > 0) + ost->st->duration = av_rescale_q(ist->st->duration, ist->st->time_base, ost->st->time_base); + + if (ist->st->nb_side_data) { + for (i = 0; i < ist->st->nb_side_data; i++) { + const AVPacketSideData *sd_src = &ist->st->side_data[i]; + uint8_t *dst_data; + + dst_data = av_stream_new_side_data(ost->st, sd_src->type, sd_src->size); + if (!dst_data) + return AVERROR(ENOMEM); + memcpy(dst_data, sd_src->data, sd_src->size); + } + } + + if (ost->rotate_overridden) { + uint8_t *sd = av_stream_new_side_data(ost->st, AV_PKT_DATA_DISPLAYMATRIX, + sizeof(int32_t) * 9); + if (sd) + av_display_rotation_set((int32_t *)sd, -ost->rotate_override_value); + } + + switch (par_dst->codec_type) { + case AVMEDIA_TYPE_AUDIO: + if (audio_volume != 256) { + av_log(NULL, AV_LOG_FATAL, "-acodec copy and -vol are incompatible (frames are not decoded)\n"); + exit_program(1); + } + if((par_dst->block_align == 1 || par_dst->block_align == 1152 || par_dst->block_align == 576) && par_dst->codec_id == AV_CODEC_ID_MP3) + par_dst->block_align= 0; + if(par_dst->codec_id == AV_CODEC_ID_AC3) + par_dst->block_align= 0; + break; + case AVMEDIA_TYPE_VIDEO: + if (ost->frame_aspect_ratio.num) { // overridden by the -aspect cli option + sar = + av_mul_q(ost->frame_aspect_ratio, + (AVRational){ par_dst->height, par_dst->width }); + av_log(NULL, AV_LOG_WARNING, "Overriding aspect ratio " + "with stream copy may produce invalid files\n"); + } + else if (ist->st->sample_aspect_ratio.num) + sar = ist->st->sample_aspect_ratio; + else + sar = par_src->sample_aspect_ratio; + ost->st->sample_aspect_ratio = par_dst->sample_aspect_ratio = sar; + ost->st->avg_frame_rate = ist->st->avg_frame_rate; + ost->st->r_frame_rate = ist->st->r_frame_rate; + break; + } + + ost->mux_timebase = ist->st->time_base; + + return 0; +} + +static void set_encoder_id(OutputFile *of, OutputStream *ost) +{ + const AVDictionaryEntry *e; + + uint8_t *encoder_string; + int encoder_string_len; + int format_flags = 0; + int codec_flags = ost->enc_ctx->flags; + + if (av_dict_get(ost->st->metadata, "encoder", NULL, 0)) + return; + + e = av_dict_get(of->opts, "fflags", NULL, 0); + if (e) { + const AVOption *o = av_opt_find(of->ctx, "fflags", NULL, 0, 0); + if (!o) + return; + av_opt_eval_flags(of->ctx, o, e->value, &format_flags); + } + e = av_dict_get(ost->encoder_opts, "flags", NULL, 0); + if (e) { + const AVOption *o = av_opt_find(ost->enc_ctx, "flags", NULL, 0, 0); + if (!o) + return; + av_opt_eval_flags(ost->enc_ctx, o, e->value, &codec_flags); + } + + encoder_string_len = sizeof(LIBAVCODEC_IDENT) + strlen(ost->enc->name) + 2; + encoder_string = av_mallocz(encoder_string_len); + if (!encoder_string) + exit_program(1); + + if (!(format_flags & AVFMT_FLAG_BITEXACT) && !(codec_flags & AV_CODEC_FLAG_BITEXACT)) + av_strlcpy(encoder_string, LIBAVCODEC_IDENT " ", encoder_string_len); + else + av_strlcpy(encoder_string, "Lavc ", encoder_string_len); + av_strlcat(encoder_string, ost->enc->name, encoder_string_len); + av_dict_set(&ost->st->metadata, "encoder", encoder_string, + AV_DICT_DONT_STRDUP_VAL | AV_DICT_DONT_OVERWRITE); +} + +static void parse_forced_key_frames(char *kf, OutputStream *ost, + AVCodecContext *avctx) +{ + char *p; + int n = 1, i, size, index = 0; + int64_t t, *pts; + + for (p = kf; *p; p++) + if (*p == ',') + n++; + size = n; + pts = av_malloc_array(size, sizeof(*pts)); + if (!pts) { + av_log(NULL, AV_LOG_FATAL, "Could not allocate forced key frames array.\n"); + exit_program(1); + } + + p = kf; + for (i = 0; i < n; i++) { + char *next = strchr(p, ','); + + if (next) + *next++ = 0; + + if (!memcmp(p, "chapters", 8)) { + + AVFormatContext *avf = output_files[ost->file_index]->ctx; + int j; + + if (avf->nb_chapters > INT_MAX - size || + !(pts = av_realloc_f(pts, size += avf->nb_chapters - 1, + sizeof(*pts)))) { + av_log(NULL, AV_LOG_FATAL, + "Could not allocate forced key frames array.\n"); + exit_program(1); + } + t = p[8] ? parse_time_or_die("force_key_frames", p + 8, 1) : 0; + t = av_rescale_q(t, AV_TIME_BASE_Q, avctx->time_base); + + for (j = 0; j < avf->nb_chapters; j++) { + AVChapter *c = avf->chapters[j]; + av_assert1(index < size); + pts[index++] = av_rescale_q(c->start, c->time_base, + avctx->time_base) + t; + } + + } else { + + t = parse_time_or_die("force_key_frames", p, 1); + av_assert1(index < size); + pts[index++] = av_rescale_q(t, AV_TIME_BASE_Q, avctx->time_base); + + } + + p = next; + } + + av_assert0(index == size); + qsort(pts, size, sizeof(*pts), compare_int64); + ost->forced_kf_count = size; + ost->forced_kf_pts = pts; +} + +static void init_encoder_time_base(OutputStream *ost, AVRational default_time_base) +{ + InputStream *ist = get_input_stream(ost); + AVCodecContext *enc_ctx = ost->enc_ctx; + AVFormatContext *oc; + + if (ost->enc_timebase.num > 0) { + enc_ctx->time_base = ost->enc_timebase; + return; + } + + if (ost->enc_timebase.num < 0) { + if (ist) { + enc_ctx->time_base = ist->st->time_base; + return; + } + + oc = output_files[ost->file_index]->ctx; + av_log(oc, AV_LOG_WARNING, "Input stream data not available, using default time base\n"); + } + + enc_ctx->time_base = default_time_base; +} + +static int init_output_stream_encode(OutputStream *ost, AVFrame *frame) +{ + InputStream *ist = get_input_stream(ost); + AVCodecContext *enc_ctx = ost->enc_ctx; + AVCodecContext *dec_ctx = NULL; + OutputFile *of = output_files[ost->file_index]; + AVFormatContext *oc = of->ctx; + int ret; + + set_encoder_id(output_files[ost->file_index], ost); + + if (ist) { + dec_ctx = ist->dec_ctx; + } + + if (enc_ctx->codec_type == AVMEDIA_TYPE_VIDEO) { + if (!ost->frame_rate.num) + ost->frame_rate = av_buffersink_get_frame_rate(ost->filter->filter); + if (ist && !ost->frame_rate.num && !ost->max_frame_rate.num) { + ost->frame_rate = (AVRational){25, 1}; + av_log(NULL, AV_LOG_WARNING, + "No information " + "about the input framerate is available. Falling " + "back to a default value of 25fps for output stream #%d:%d. Use the -r option " + "if you want a different framerate.\n", + ost->file_index, ost->index); + } + + if (ost->max_frame_rate.num && + (av_q2d(ost->frame_rate) > av_q2d(ost->max_frame_rate) || + !ost->frame_rate.den)) + ost->frame_rate = ost->max_frame_rate; + + if (ost->enc->supported_framerates && !ost->force_fps) { + int idx = av_find_nearest_q_idx(ost->frame_rate, ost->enc->supported_framerates); + ost->frame_rate = ost->enc->supported_framerates[idx]; + } + // reduce frame rate for mpeg4 to be within the spec limits + if (enc_ctx->codec_id == AV_CODEC_ID_MPEG4) { + av_reduce(&ost->frame_rate.num, &ost->frame_rate.den, + ost->frame_rate.num, ost->frame_rate.den, 65535); + } + } + + switch (enc_ctx->codec_type) { + case AVMEDIA_TYPE_AUDIO: + enc_ctx->sample_fmt = av_buffersink_get_format(ost->filter->filter); + enc_ctx->sample_rate = av_buffersink_get_sample_rate(ost->filter->filter); + ret = av_buffersink_get_ch_layout(ost->filter->filter, &enc_ctx->ch_layout); + if (ret < 0) + return ret; + + if (ost->bits_per_raw_sample) + enc_ctx->bits_per_raw_sample = ost->bits_per_raw_sample; + else if (dec_ctx && ost->filter->graph->is_meta) + enc_ctx->bits_per_raw_sample = FFMIN(dec_ctx->bits_per_raw_sample, + av_get_bytes_per_sample(enc_ctx->sample_fmt) << 3); + + init_encoder_time_base(ost, av_make_q(1, enc_ctx->sample_rate)); + break; + + case AVMEDIA_TYPE_VIDEO: + init_encoder_time_base(ost, av_inv_q(ost->frame_rate)); + + if (!(enc_ctx->time_base.num && enc_ctx->time_base.den)) + enc_ctx->time_base = av_buffersink_get_time_base(ost->filter->filter); + if ( av_q2d(enc_ctx->time_base) < 0.001 && ost->vsync_method != VSYNC_PASSTHROUGH + && (ost->vsync_method == VSYNC_CFR || ost->vsync_method == VSYNC_VSCFR || + (ost->vsync_method == VSYNC_AUTO && !(of->format->flags & AVFMT_VARIABLE_FPS)))){ + av_log(oc, AV_LOG_WARNING, "Frame rate very high for a muxer not efficiently supporting it.\n" + "Please consider specifying a lower framerate, a different muxer or " + "setting vsync/fps_mode to vfr\n"); + } + + enc_ctx->width = av_buffersink_get_w(ost->filter->filter); + enc_ctx->height = av_buffersink_get_h(ost->filter->filter); + enc_ctx->sample_aspect_ratio = ost->st->sample_aspect_ratio = + ost->frame_aspect_ratio.num ? // overridden by the -aspect cli option + av_mul_q(ost->frame_aspect_ratio, (AVRational){ enc_ctx->height, enc_ctx->width }) : + av_buffersink_get_sample_aspect_ratio(ost->filter->filter); + + enc_ctx->pix_fmt = av_buffersink_get_format(ost->filter->filter); + + if (ost->bits_per_raw_sample) + enc_ctx->bits_per_raw_sample = ost->bits_per_raw_sample; + else if (dec_ctx && ost->filter->graph->is_meta) + enc_ctx->bits_per_raw_sample = FFMIN(dec_ctx->bits_per_raw_sample, + av_pix_fmt_desc_get(enc_ctx->pix_fmt)->comp[0].depth); + + if (frame) { + enc_ctx->color_range = frame->color_range; + enc_ctx->color_primaries = frame->color_primaries; + enc_ctx->color_trc = frame->color_trc; + enc_ctx->colorspace = frame->colorspace; + enc_ctx->chroma_sample_location = frame->chroma_location; + } + + enc_ctx->framerate = ost->frame_rate; + + ost->st->avg_frame_rate = ost->frame_rate; + + // Field order: autodetection + if (frame) { + if (enc_ctx->flags & (AV_CODEC_FLAG_INTERLACED_DCT | AV_CODEC_FLAG_INTERLACED_ME) && + ost->top_field_first >= 0) + frame->top_field_first = !!ost->top_field_first; + + if (frame->interlaced_frame) { + if (enc_ctx->codec->id == AV_CODEC_ID_MJPEG) + enc_ctx->field_order = frame->top_field_first ? AV_FIELD_TT:AV_FIELD_BB; + else + enc_ctx->field_order = frame->top_field_first ? AV_FIELD_TB:AV_FIELD_BT; + } else + enc_ctx->field_order = AV_FIELD_PROGRESSIVE; + } + + // Field order: override + if (ost->top_field_first == 0) { + enc_ctx->field_order = AV_FIELD_BB; + } else if (ost->top_field_first == 1) { + enc_ctx->field_order = AV_FIELD_TT; + } + + if (ost->forced_keyframes) { + if (!strncmp(ost->forced_keyframes, "expr:", 5)) { + ret = av_expr_parse(&ost->forced_keyframes_pexpr, ost->forced_keyframes+5, + forced_keyframes_const_names, NULL, NULL, NULL, NULL, 0, NULL); + if (ret < 0) { + av_log(NULL, AV_LOG_ERROR, + "Invalid force_key_frames expression '%s'\n", ost->forced_keyframes+5); + return ret; + } + ost->forced_keyframes_expr_const_values[FKF_N] = 0; + ost->forced_keyframes_expr_const_values[FKF_N_FORCED] = 0; + ost->forced_keyframes_expr_const_values[FKF_PREV_FORCED_N] = NAN; + ost->forced_keyframes_expr_const_values[FKF_PREV_FORCED_T] = NAN; + + // Don't parse the 'forced_keyframes' in case of 'keep-source-keyframes', + // parse it only for static kf timings + } else if(strncmp(ost->forced_keyframes, "source", 6)) { + parse_forced_key_frames(ost->forced_keyframes, ost, ost->enc_ctx); + } + } + break; + case AVMEDIA_TYPE_SUBTITLE: + enc_ctx->time_base = AV_TIME_BASE_Q; + if (!enc_ctx->width) { + enc_ctx->width = input_streams[ost->source_index]->st->codecpar->width; + enc_ctx->height = input_streams[ost->source_index]->st->codecpar->height; + } + break; + case AVMEDIA_TYPE_DATA: + break; + default: + abort(); + break; + } + + ost->mux_timebase = enc_ctx->time_base; + + return 0; +} + +static int init_output_stream(OutputStream *ost, AVFrame *frame, + char *error, int error_len) +{ + int ret = 0; + + if (ost->encoding_needed) { + const AVCodec *codec = ost->enc; + AVCodecContext *dec = NULL; + InputStream *ist; + + ret = init_output_stream_encode(ost, frame); + if (ret < 0) + return ret; + + if ((ist = get_input_stream(ost))) + dec = ist->dec_ctx; + if (dec && dec->subtitle_header) { + /* ASS code assumes this buffer is null terminated so add extra byte. */ + ost->enc_ctx->subtitle_header = av_mallocz(dec->subtitle_header_size + 1); + if (!ost->enc_ctx->subtitle_header) + return AVERROR(ENOMEM); + memcpy(ost->enc_ctx->subtitle_header, dec->subtitle_header, dec->subtitle_header_size); + ost->enc_ctx->subtitle_header_size = dec->subtitle_header_size; + } + if (!av_dict_get(ost->encoder_opts, "threads", NULL, 0)) + av_dict_set(&ost->encoder_opts, "threads", "auto", 0); + + ret = hw_device_setup_for_encode(ost); + if (ret < 0) { + snprintf(error, error_len, "Device setup failed for " + "encoder on output stream #%d:%d : %s", + ost->file_index, ost->index, av_err2str(ret)); + return ret; + } + + if (ist && ist->dec->type == AVMEDIA_TYPE_SUBTITLE && ost->enc->type == AVMEDIA_TYPE_SUBTITLE) { + int input_props = 0, output_props = 0; + AVCodecDescriptor const *input_descriptor = + avcodec_descriptor_get(dec->codec_id); + AVCodecDescriptor const *output_descriptor = + avcodec_descriptor_get(ost->enc_ctx->codec_id); + if (input_descriptor) + input_props = input_descriptor->props & (AV_CODEC_PROP_TEXT_SUB | AV_CODEC_PROP_BITMAP_SUB); + if (output_descriptor) + output_props = output_descriptor->props & (AV_CODEC_PROP_TEXT_SUB | AV_CODEC_PROP_BITMAP_SUB); + if (input_props && output_props && input_props != output_props) { + snprintf(error, error_len, + "Subtitle encoding currently only possible from text to text " + "or bitmap to bitmap"); + return AVERROR_INVALIDDATA; + } + } + + if ((ret = avcodec_open2(ost->enc_ctx, codec, &ost->encoder_opts)) < 0) { + if (ret == AVERROR_EXPERIMENTAL) + abort_codec_experimental(codec, 1); + snprintf(error, error_len, + "Error while opening encoder for output stream #%d:%d - " + "maybe incorrect parameters such as bit_rate, rate, width or height", + ost->file_index, ost->index); + return ret; + } + if (ost->enc->type == AVMEDIA_TYPE_AUDIO && + !(ost->enc->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE)) + av_buffersink_set_frame_size(ost->filter->filter, + ost->enc_ctx->frame_size); + assert_avoptions(ost->encoder_opts); + if (ost->enc_ctx->bit_rate && ost->enc_ctx->bit_rate < 1000 && + ost->enc_ctx->codec_id != AV_CODEC_ID_CODEC2 /* don't complain about 700 bit/s modes */) + av_log(NULL, AV_LOG_WARNING, "The bitrate parameter is set too low." + " It takes bits/s as argument, not kbits/s\n"); + + ret = avcodec_parameters_from_context(ost->st->codecpar, ost->enc_ctx); + if (ret < 0) { + av_log(NULL, AV_LOG_FATAL, + "Error initializing the output stream codec context.\n"); + exit_program(1); + } + + if (ost->enc_ctx->nb_coded_side_data) { + int i; + + for (i = 0; i < ost->enc_ctx->nb_coded_side_data; i++) { + const AVPacketSideData *sd_src = &ost->enc_ctx->coded_side_data[i]; + uint8_t *dst_data; + + dst_data = av_stream_new_side_data(ost->st, sd_src->type, sd_src->size); + if (!dst_data) + return AVERROR(ENOMEM); + memcpy(dst_data, sd_src->data, sd_src->size); + } + } + + /* + * Add global input side data. For now this is naive, and copies it + * from the input stream's global side data. All side data should + * really be funneled over AVFrame and libavfilter, then added back to + * packet side data, and then potentially using the first packet for + * global side data. + */ + if (ist) { + int i; + for (i = 0; i < ist->st->nb_side_data; i++) { + AVPacketSideData *sd = &ist->st->side_data[i]; + if (sd->type != AV_PKT_DATA_CPB_PROPERTIES) { + uint8_t *dst = av_stream_new_side_data(ost->st, sd->type, sd->size); + if (!dst) + return AVERROR(ENOMEM); + memcpy(dst, sd->data, sd->size); + if (ist->autorotate && sd->type == AV_PKT_DATA_DISPLAYMATRIX) + av_display_rotation_set((uint32_t *)dst, 0); + } + } + } + + // copy timebase while removing common factors + if (ost->st->time_base.num <= 0 || ost->st->time_base.den <= 0) + ost->st->time_base = av_add_q(ost->enc_ctx->time_base, (AVRational){0, 1}); + + // copy estimated duration as a hint to the muxer + if (ost->st->duration <= 0 && ist && ist->st->duration > 0) + ost->st->duration = av_rescale_q(ist->st->duration, ist->st->time_base, ost->st->time_base); + } else if (ost->stream_copy) { + ret = init_output_stream_streamcopy(ost); + if (ret < 0) + return ret; + } + + /* initialize bitstream filters for the output stream + * needs to be done here, because the codec id for streamcopy is not + * known until now */ + ret = init_output_bsfs(ost); + if (ret < 0) + return ret; + + ost->initialized = 1; + + ret = of_check_init(output_files[ost->file_index]); + if (ret < 0) + return ret; + + return ret; +} + +static void report_new_stream(int input_index, AVPacket *pkt) +{ + InputFile *file = input_files[input_index]; + AVStream *st = file->ctx->streams[pkt->stream_index]; + + if (pkt->stream_index < file->nb_streams_warn) + return; + av_log(file->ctx, AV_LOG_WARNING, + "New %s stream %d:%d at pos:%"PRId64" and DTS:%ss\n", + av_get_media_type_string(st->codecpar->codec_type), + input_index, pkt->stream_index, + pkt->pos, av_ts2timestr(pkt->dts, &st->time_base)); + file->nb_streams_warn = pkt->stream_index + 1; +} + +static int transcode_init(void) +{ + int ret = 0, i, j, k; + AVFormatContext *oc; + OutputStream *ost; + InputStream *ist; + char error[1024] = {0}; + + for (i = 0; i < nb_filtergraphs; i++) { + FilterGraph *fg = filtergraphs[i]; + for (j = 0; j < fg->nb_outputs; j++) { + OutputFilter *ofilter = fg->outputs[j]; + if (!ofilter->ost || ofilter->ost->source_index >= 0) + continue; + if (fg->nb_inputs != 1) + continue; + for (k = nb_input_streams-1; k >= 0 ; k--) + if (fg->inputs[0]->ist == input_streams[k]) + break; + ofilter->ost->source_index = k; + } + } + + /* init framerate emulation */ + for (i = 0; i < nb_input_files; i++) { + InputFile *ifile = input_files[i]; + if (ifile->readrate || ifile->rate_emu) + for (j = 0; j < ifile->nb_streams; j++) + input_streams[j + ifile->ist_index]->start = av_gettime_relative(); + } + + /* init input streams */ + for (i = 0; i < nb_input_streams; i++) + if ((ret = init_input_stream(i, error, sizeof(error))) < 0) { + for (i = 0; i < nb_output_streams; i++) { + ost = output_streams[i]; + avcodec_close(ost->enc_ctx); + } + goto dump_format; + } + + /* + * initialize stream copy and subtitle/data streams. + * Encoded AVFrame based streams will get initialized as follows: + * - when the first AVFrame is received in do_video_out + * - just before the first AVFrame is received in either transcode_step + * or reap_filters due to us requiring the filter chain buffer sink + * to be configured with the correct audio frame size, which is only + * known after the encoder is initialized. + */ + for (i = 0; i < nb_output_streams; i++) { + if (!output_streams[i]->stream_copy && + (output_streams[i]->enc_ctx->codec_type == AVMEDIA_TYPE_VIDEO || + output_streams[i]->enc_ctx->codec_type == AVMEDIA_TYPE_AUDIO)) + continue; + + ret = init_output_stream_wrapper(output_streams[i], NULL, 0); + if (ret < 0) + goto dump_format; + } + + /* discard unused programs */ + for (i = 0; i < nb_input_files; i++) { + InputFile *ifile = input_files[i]; + for (j = 0; j < ifile->ctx->nb_programs; j++) { + AVProgram *p = ifile->ctx->programs[j]; + int discard = AVDISCARD_ALL; + + for (k = 0; k < p->nb_stream_indexes; k++) + if (!input_streams[ifile->ist_index + p->stream_index[k]]->discard) { + discard = AVDISCARD_DEFAULT; + break; + } + p->discard = discard; + } + } + + /* write headers for files with no streams */ + for (i = 0; i < nb_output_files; i++) { + oc = output_files[i]->ctx; + if (output_files[i]->format->flags & AVFMT_NOSTREAMS && oc->nb_streams == 0) { + ret = of_check_init(output_files[i]); + if (ret < 0) + goto dump_format; + } + } + + dump_format: + /* dump the stream mapping */ + av_log(NULL, AV_LOG_INFO, "Stream mapping:\n"); + for (i = 0; i < nb_input_streams; i++) { + ist = input_streams[i]; + + for (j = 0; j < ist->nb_filters; j++) { + if (!filtergraph_is_simple(ist->filters[j]->graph)) { + av_log(NULL, AV_LOG_INFO, " Stream #%d:%d (%s) -> %s", + ist->file_index, ist->st->index, ist->dec ? ist->dec->name : "?", + ist->filters[j]->name); + if (nb_filtergraphs > 1) + av_log(NULL, AV_LOG_INFO, " (graph %d)", ist->filters[j]->graph->index); + av_log(NULL, AV_LOG_INFO, "\n"); + } + } + } + + for (i = 0; i < nb_output_streams; i++) { + ost = output_streams[i]; + + if (ost->attachment_filename) { + /* an attached file */ + av_log(NULL, AV_LOG_INFO, " File %s -> Stream #%d:%d\n", + ost->attachment_filename, ost->file_index, ost->index); + continue; + } + + if (ost->filter && !filtergraph_is_simple(ost->filter->graph)) { + /* output from a complex graph */ + av_log(NULL, AV_LOG_INFO, " %s", ost->filter->name); + if (nb_filtergraphs > 1) + av_log(NULL, AV_LOG_INFO, " (graph %d)", ost->filter->graph->index); + + av_log(NULL, AV_LOG_INFO, " -> Stream #%d:%d (%s)\n", ost->file_index, + ost->index, ost->enc ? ost->enc->name : "?"); + continue; + } + + av_log(NULL, AV_LOG_INFO, " Stream #%d:%d -> #%d:%d", + input_streams[ost->source_index]->file_index, + input_streams[ost->source_index]->st->index, + ost->file_index, + ost->index); + if (ost->sync_ist != input_streams[ost->source_index]) + av_log(NULL, AV_LOG_INFO, " [sync #%d:%d]", + ost->sync_ist->file_index, + ost->sync_ist->st->index); + if (ost->stream_copy) + av_log(NULL, AV_LOG_INFO, " (copy)"); + else { + const AVCodec *in_codec = input_streams[ost->source_index]->dec; + const AVCodec *out_codec = ost->enc; + const char *decoder_name = "?"; + const char *in_codec_name = "?"; + const char *encoder_name = "?"; + const char *out_codec_name = "?"; + const AVCodecDescriptor *desc; + + if (in_codec) { + decoder_name = in_codec->name; + desc = avcodec_descriptor_get(in_codec->id); + if (desc) + in_codec_name = desc->name; + if (!strcmp(decoder_name, in_codec_name)) + decoder_name = "native"; + } + + if (out_codec) { + encoder_name = out_codec->name; + desc = avcodec_descriptor_get(out_codec->id); + if (desc) + out_codec_name = desc->name; + if (!strcmp(encoder_name, out_codec_name)) + encoder_name = "native"; + } + + av_log(NULL, AV_LOG_INFO, " (%s (%s) -> %s (%s))", + in_codec_name, decoder_name, + out_codec_name, encoder_name); + } + av_log(NULL, AV_LOG_INFO, "\n"); + } + + if (ret) { + av_log(NULL, AV_LOG_ERROR, "%s\n", error); + return ret; + } + + atomic_store(&transcode_init_done, 1); + + return 0; +} + +/* Return 1 if there remain streams where more output is wanted, 0 otherwise. */ +static int need_output(void) +{ + int i; + + for (i = 0; i < nb_output_streams; i++) { + OutputStream *ost = output_streams[i]; + OutputFile *of = output_files[ost->file_index]; + AVFormatContext *os = output_files[ost->file_index]->ctx; + + if (ost->finished || + (os->pb && avio_tell(os->pb) >= of->limit_filesize)) + continue; + if (ost->frame_number >= ost->max_frames) { + int j; + for (j = 0; j < of->ctx->nb_streams; j++) + close_output_stream(output_streams[of->ost_index + j]); + continue; + } + + return 1; + } + + return 0; +} + +/** + * Select the output stream to process. + * + * @return selected output stream, or NULL if none available + */ +static OutputStream *choose_output(void) +{ + int i; + int64_t opts_min = INT64_MAX; + OutputStream *ost_min = NULL; + + for (i = 0; i < nb_output_streams; i++) { + OutputStream *ost = output_streams[i]; + int64_t opts = ost->last_mux_dts == AV_NOPTS_VALUE ? INT64_MIN : + av_rescale_q(ost->last_mux_dts, ost->st->time_base, + AV_TIME_BASE_Q); + if (ost->last_mux_dts == AV_NOPTS_VALUE) + av_log(NULL, AV_LOG_DEBUG, + "cur_dts is invalid st:%d (%d) [init:%d i_done:%d finish:%d] (this is harmless if it occurs once at the start per stream)\n", + ost->st->index, ost->st->id, ost->initialized, ost->inputs_done, ost->finished); + + if (!ost->initialized && !ost->inputs_done) + return ost->unavailable ? NULL : ost; + + if (!ost->finished && opts < opts_min) { + opts_min = opts; + ost_min = ost->unavailable ? NULL : ost; + } + } + return ost_min; +} + +static void set_tty_echo(int on) +{ +#if HAVE_TERMIOS_H + struct termios tty; + if (tcgetattr(0, &tty) == 0) { + if (on) tty.c_lflag |= ECHO; + else tty.c_lflag &= ~ECHO; + tcsetattr(0, TCSANOW, &tty); + } +#endif +} + +static int check_keyboard_interaction(int64_t cur_time) +{ + int i, ret, key; + static int64_t last_time; + if (received_nb_signals) + return AVERROR_EXIT; + /* read_key() returns 0 on EOF */ + if (cur_time - last_time >= 100000) { + key = read_key(); + last_time = cur_time; + }else + key = -1; + if (key == 'q') { + av_log(NULL, AV_LOG_INFO, "\n\n[q] command received. Exiting.\n\n"); + return AVERROR_EXIT; + } + if (key == '+') av_log_set_level(av_log_get_level()+10); + if (key == '-') av_log_set_level(av_log_get_level()-10); + if (key == 's') qp_hist ^= 1; + if (key == 'h'){ + if (do_hex_dump){ + do_hex_dump = do_pkt_dump = 0; + } else if(do_pkt_dump){ + do_hex_dump = 1; + } else + do_pkt_dump = 1; + av_log_set_level(AV_LOG_DEBUG); + } + if (key == 'c' || key == 'C'){ + char buf[4096], target[64], command[256], arg[256] = {0}; + double time; + int k, n = 0; + fprintf(stderr, "\nEnter command: |all