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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 151 additions & 1 deletion .github/workflows/prebuild.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,149 @@ jobs:
path: prebuilds/
retention-days: 30

# Statically-linked FFmpeg prebuilds, shipped as @node-webcodecs/static-* packages.
# Linux builds run in a bullseye container so the .node needs only glibc >= 2.31.
static:
runs-on: ${{ matrix.os }}
container: ${{ matrix.container }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
platform: linux
arch: x64
container: node:20-bullseye
- os: ubuntu-24.04-arm
platform: linux
arch: arm64
container: node:20-bullseye
- os: macos-14
platform: darwin
Comment on lines +77 to +85

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 glibc symbol check is informational-only

The objdump pipeline prints the highest GLIBC_x.y symbol but does not fail the build if that version exceeds the intended minimum. If a future code change or library update pulls in a newer glibc symbol (e.g., GLIBC_2.33), CI would silently pass and users on older distros would get a load error at runtime. Consider adding an explicit guard to make the check enforcing.

Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/prebuild.yml
Line: 77-85

Comment:
**glibc symbol check is informational-only**

The `objdump` pipeline prints the highest `GLIBC_x.y` symbol but does not fail the build if that version exceeds the intended minimum. If a future code change or library update pulls in a newer glibc symbol (e.g., `GLIBC_2.33`), CI would silently pass and users on older distros would get a load error at runtime. Consider adding an explicit guard to make the check enforcing.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

arch: arm64
container: ''

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
if: runner.os == 'macOS'
uses: actions/setup-node@v4
with:
node-version: 20

- name: Cache static FFmpeg
id: ffmpeg-cache
uses: actions/cache@v4
with:
path: .ffmpeg-static/${{ matrix.platform }}-${{ matrix.arch }}
key: ffmpeg-static-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('scripts/ffmpeg-static/build.sh') }}

- name: Install FFmpeg build toolchain (Linux)
if: runner.os == 'Linux' && steps.ffmpeg-cache.outputs.cache-hit != 'true'
run: |
apt-get update
apt-get install -y build-essential pkg-config nasm python3-pip
# bullseye's cmake/meson are too old for SVT-AV1/dav1d
pip3 install cmake meson ninja

- name: Install FFmpeg build toolchain (macOS)
if: runner.os == 'macOS' && steps.ffmpeg-cache.outputs.cache-hit != 'true'
run: brew install nasm meson ninja

- name: Build static FFmpeg
if: steps.ffmpeg-cache.outputs.cache-hit != 'true'
run: ./scripts/ffmpeg-static/build.sh

- name: Install compiler for addon (Linux, cache hit)
if: runner.os == 'Linux' && steps.ffmpeg-cache.outputs.cache-hit == 'true'
run: |
apt-get update
apt-get install -y build-essential pkg-config cmake

- name: Install dependencies
run: npm ci --ignore-scripts

- name: Build static addon
run: |
export PKG_CONFIG_PATH="$PWD/.ffmpeg-static/${{ matrix.platform }}-${{ matrix.arch }}/lib/pkgconfig"
npx cmake-js compile --CDWEBCODECS_STATIC_FFMPEG=ON --out build-static

- name: Strip and inspect binary
run: |
if [ "$RUNNER_OS" = "Linux" ]; then
strip --strip-unneeded build-static/Release/webcodecs_node.node
objdump -T build-static/Release/webcodecs_node.node | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -1
else
strip -x build-static/Release/webcodecs_node.node
fi
ls -la build-static/Release/webcodecs_node.node

- name: Package static prebuild
run: node scripts/make-static-package.js ${{ matrix.platform }} ${{ matrix.arch }} build-static/Release/webcodecs_node.node

- name: Test against static build
run: |
npm run build:ts
mkdir -p node_modules/@node-webcodecs
ln -s "$PWD/npm-static/${{ matrix.platform }}-${{ matrix.arch }}" "node_modules/@node-webcodecs/static-${{ matrix.platform }}-${{ matrix.arch }}"
NODE_WEBCODECS_FORCE=static npm test
NODE_WEBCODECS_FORCE=static npm run test:integration

- name: Upload static package
uses: actions/upload-artifact@v4
with:
name: static-${{ matrix.platform }}-${{ matrix.arch }}
path: npm-static/
retention-days: 30

publish-static:
needs: static
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')

# Required for npm OIDC trusted publishing
permissions:
contents: read
id-token: write

steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
# no registry-url: it writes an _authToken .npmrc line, which
# makes npm use token auth instead of OIDC trusted publishing

- name: Download static packages
uses: actions/download-artifact@v4
with:
path: npm-static
pattern: static-*
merge-multiple: true

# npm trusted publishing (OIDC) requires npm >= 11.5.1
- name: Update npm for trusted publishing
run: npm install -g npm@latest

# Each @node-webcodecs/static-* package needs its own trusted-publisher
# config on npmjs.com pointing at this repo + workflow.
- name: Publish static packages
run: |
for dir in npm-static/*/; do
name=$(cd "$dir" && npm pkg get name | tr -d '"')
ver=$(cd "$dir" && npm pkg get version | tr -d '"')
# tolerate reruns after a partial publish
if npm view "$name@$ver" version >/dev/null 2>&1; then
echo "$name@$ver already published, skipping"
continue
fi
echo "publishing $name@$ver"
(cd "$dir" && npm publish --access public --provenance)
done

publish:
needs: prebuild
needs: [prebuild, publish-static]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')

Expand Down Expand Up @@ -119,5 +260,14 @@ jobs:
- name: Update npm for trusted publishing
run: npm install -g npm@latest

# Injected at publish time (not committed) so local installs before the
# static packages exist on npm don't fail resolution.
- name: Add static prebuild optionalDependencies
run: |
VERSION=$(node -p "require('./package.json').version")
npm pkg set "optionalDependencies.@node-webcodecs/static-linux-x64=$VERSION" \
"optionalDependencies.@node-webcodecs/static-linux-arm64=$VERSION" \
"optionalDependencies.@node-webcodecs/static-darwin-arm64=$VERSION"

- name: Publish to npm with provenance
run: npm publish --access public --provenance
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ node_modules/

# Build outputs
build/
build-static/
dist/
prebuilds/
npm-static/
.ffmpeg-static/

# IDE
.vscode/
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## [1.3.0] - unreleased

### Added
- **Zero-dependency installs**: statically-linked FFmpeg prebuilds for linux-x64, linux-arm64, and darwin-arm64, shipped as platform-specific `@node-webcodecs/static-*` optional dependencies (~15 MB each, only your platform downloads). `npm install node-webcodecs` now works in any Docker base image (including `node:22`), Lambda, and Fly with no FFmpeg installed.
- Loader resolution order: dynamic prebuild (system FFmpeg, preferred) → static prebuild → source build. Override with `NODE_WEBCODECS_FORCE=dynamic|static|source`; inspect with the new `getNativeVariant()` export.
- The static build is LGPL-only (FFmpeg `--disable-gpl` + BSD codec libs): H.264 encode via openh264, AV1 encode via SVT-AV1, AV1 decode via dav1d, VP8/VP9 via libvpx, plus native AAC/FLAC/PCM, libopus, and libmp3lame. Software HEVC encode is unavailable in the static variant (no x265); hardware HEVC (VideoToolbox, NVENC) still works.


## [1.2.2] - 2026-07-14

### Added
Expand Down
56 changes: 42 additions & 14 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,49 @@ add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${CMAKE_JS_SRC})
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")

# Link FFmpeg libraries
target_link_libraries(${PROJECT_NAME}
${CMAKE_JS_LIB}
${AVCODEC_LIBRARIES}
${AVUTIL_LIBRARIES}
${SWSCALE_LIBRARIES}
${SWRESAMPLE_LIBRARIES}
)
option(WEBCODECS_STATIC_FFMPEG "Link FFmpeg statically (PKG_CONFIG_PATH must point at a static prefix)" OFF)

# Link directories
target_link_directories(${PROJECT_NAME} PRIVATE
${AVCODEC_LIBRARY_DIRS}
${AVUTIL_LIBRARY_DIRS}
${SWSCALE_LIBRARY_DIRS}
${SWRESAMPLE_LIBRARY_DIRS}
)
if(WEBCODECS_STATIC_FFMPEG)
# _STATIC_LDFLAGS carries -L/-l plus Libs.private (codec libs, frameworks).
# Dependents before dependencies: avcodec/swscale/swresample, then avutil.
# pkg_check_modules splits "-framework X" into two list items; re-join them
# so target_link_libraries doesn't turn "X" into -lX.
set(FFMPEG_STATIC_LINK_FLAGS)
set(_pending_framework FALSE)
foreach(_flag IN LISTS AVCODEC_STATIC_LDFLAGS SWSCALE_STATIC_LDFLAGS SWRESAMPLE_STATIC_LDFLAGS AVUTIL_STATIC_LDFLAGS)
if(_pending_framework)
list(APPEND FFMPEG_STATIC_LINK_FLAGS "-framework ${_flag}")
set(_pending_framework FALSE)
elseif(_flag STREQUAL "-framework")
set(_pending_framework TRUE)
else()
list(APPEND FFMPEG_STATIC_LINK_FLAGS "${_flag}")
endif()
endforeach()
target_link_libraries(${PROJECT_NAME}
${CMAKE_JS_LIB}
${FFMPEG_STATIC_LINK_FLAGS}
)
if(NOT APPLE)
# Don't re-export FFmpeg symbols from the .node — avoids collisions if
# another module loads a system FFmpeg into the same process.
target_link_options(${PROJECT_NAME} PRIVATE -Wl,--exclude-libs,ALL)
endif()
else()
target_link_libraries(${PROJECT_NAME}
${CMAKE_JS_LIB}
${AVCODEC_LIBRARIES}
${AVUTIL_LIBRARIES}
${SWSCALE_LIBRARIES}
${SWRESAMPLE_LIBRARIES}
)
target_link_directories(${PROJECT_NAME} PRIVATE
${AVCODEC_LIBRARY_DIRS}
${AVUTIL_LIBRARY_DIRS}
${SWSCALE_LIBRARY_DIRS}
${SWRESAMPLE_LIBRARY_DIRS}
)
endif()

# Define N-API version
target_compile_definitions(${PROJECT_NAME} PRIVATE NAPI_VERSION=8)
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,20 @@ Native WebCodecs API implementation for Node.js, using FFmpeg for encoding and d
## Requirements

- Node.js 18+
- FFmpeg libraries (libavcodec, libavutil, libswscale, libswresample)
- No system dependencies on **linux-x64**, **linux-arm64**, and **macOS arm64** — see below

Prebuilt binaries ship for **macOS arm64**, **Linux x64**, and **Linux arm64**, so on those platforms nothing compiles at install time — you only need the FFmpeg runtime libraries. The prebuilds link dynamically, so the installed FFmpeg major version must match what they were built against: FFmpeg 6.x on Linux (e.g. Ubuntu 24.04; Debian bookworm ships 5.x) and the current Homebrew FFmpeg on macOS. When no prebuild matches, the addon builds from source, which additionally requires `cmake`, `pkg-config`, and a C++ compiler (Xcode Command Line Tools on macOS, build-essential on Linux).
The loader resolves a native binding in this order:

1. **Dynamic prebuild** — links your system FFmpeg (all codecs your FFmpeg has, including GPL ones like x264/x265). Requires FFmpeg runtime libraries whose major version matches the prebuild: FFmpeg 6.x on Linux (e.g. Ubuntu 24.04), current Homebrew FFmpeg on macOS.
2. **Static prebuild** — a statically-linked, LGPL-only FFmpeg shipped as a platform-specific `@node-webcodecs/static-*` optional dependency (~15 MB, only your platform is downloaded). Zero system dependencies: works in any Docker base image, Lambda, Fly, etc. H.264 encode uses openh264, AV1 encode uses SVT-AV1, AV1 decode uses dav1d. No GPL components, so **software HEVC encode is unavailable** (hardware HEVC encode still works where present).
3. **Source build** — `cmake-js` compiles against your FFmpeg dev headers. Requires `cmake`, `pkg-config`, a C++ compiler, and FFmpeg dev packages.

Set `NODE_WEBCODECS_FORCE=dynamic|static|source` to pin a specific variant (both at install time and at runtime), and call `getNativeVariant()` to see which one loaded.

### Installing Dependencies

Only needed for the dynamic prebuild (preferred when present) or source builds — installs with none of this work via the static prebuild.

**macOS (Homebrew):**
```bash
brew install ffmpeg
Expand Down
50 changes: 24 additions & 26 deletions docs/getting-started/installation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@ description: "Platform-specific installation guide for node-webcodecs"
## Requirements

- **Node.js** 18 or higher (or Bun 1.0+)
- **FFmpeg libraries** (libavcodec, libavutil, libswscale, libswresample)
- **pkg-config** (only when building from source)
- **C++ compiler** (only when building from source; Xcode Command Line Tools on macOS, build-essential on Linux)
- On **linux-x64**, **linux-arm64**, and **macOS arm64**: nothing else. `npm install node-webcodecs` works with zero system dependencies.
- Elsewhere: FFmpeg libraries (libavcodec, libavutil, libswscale, libswresample), plus pkg-config and a C++ compiler when building from source

The loader picks the first native binding that works:

1. **Dynamic prebuild** — links your system FFmpeg (all its codecs, including GPL ones like x264/x265). Requires matching FFmpeg runtime libraries: 6.x on Linux (e.g. Ubuntu 24.04), current Homebrew FFmpeg on macOS.
2. **Static prebuild** — bundled LGPL-only FFmpeg, shipped as a platform-specific `@node-webcodecs/static-*` optional dependency (~15 MB). Works in any Docker image, Lambda, or Fly with no FFmpeg installed. H.264 encode via openh264, AV1 encode via SVT-AV1, AV1 decode via dav1d; software HEVC *encode* is unavailable (hardware HEVC still works).
3. **Source build** — compiles against your FFmpeg dev headers via cmake-js.

<Note>
Prebuilt binaries ship for macOS arm64 and Linux x64 — on those platforms nothing compiles at install time, so pkg-config and a compiler are not needed. Only the FFmpeg runtime libraries are required.
Set `NODE_WEBCODECS_FORCE=dynamic|static|source` to pin a variant, and call `getNativeVariant()` to see which one loaded.
</Note>

## Platform-Specific Setup
Expand Down Expand Up @@ -148,35 +153,28 @@ Prebuilt binaries ship for macOS arm64 and Linux x64 — on those platforms noth
<Tab title="Docker">
### Docker Installation

Create a `Dockerfile` with FFmpeg pre-installed:
No FFmpeg needed — the static prebuild is used automatically in images without FFmpeg:

```dockerfile
FROM node:20

# Install FFmpeg and dependencies
RUN apt-get update && apt-get install -y \
ffmpeg \
libavcodec-dev \
libavutil-dev \
libswscale-dev \
libswresample-dev \
pkg-config \
build-essential \
&& rm -rf /var/lib/apt/lists/*
FROM node:22-slim

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies (includes node-webcodecs compilation)
RUN npm install

COPY . .

CMD ["node", "index.js"]
```

To use the dynamic prebuild instead (adds GPL codecs like x264/x265 software encode), install FFmpeg runtime libraries in an image whose FFmpeg major version matches (6.x on Linux):

```dockerfile
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y nodejs npm \
libavcodec60 libavutil58 libswscale7 libswresample4 \
&& rm -rf /var/lib/apt/lists/*
```

Build and run:

```bash
Expand Down Expand Up @@ -269,11 +267,11 @@ console.log('VP9 decoder:', hasCodec('vp9', 'decoder'));
</Accordion>

<Accordion title="Using prebuilt binaries">
v1.0.0+ includes prebuilt binaries for:
- macOS (arm64, x64)
- Linux (x64)
Dynamic prebuilds (system FFmpeg required at runtime): macOS arm64/x64, Linux x64/arm64.

Static prebuilds (no FFmpeg required, v1.3.0+): linux-x64, linux-arm64, darwin-arm64 via `@node-webcodecs/static-*` optional dependencies.

These are automatically used if your platform matches. You still need FFmpeg libraries installed at runtime.
Check which one loaded with `getNativeVariant()` — returns `'prebuild'`, `'static'`, or `'source'`.
</Accordion>
</AccordionGroup>

Expand Down
5 changes: 3 additions & 2 deletions native/hw_accel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ static const std::vector<EncoderMapping> h264Encoders = {
{"h264_v4l2m2m", Type::V4L2M2M, AV_PIX_FMT_YUV420P},
#endif
{"libx264", Type::None, AV_PIX_FMT_YUV420P}, // Software fallback
{"libopenh264", Type::None, AV_PIX_FMT_YUV420P}, // LGPL static builds (no x264)
};

// HEVC encoders by priority
Expand Down Expand Up @@ -159,8 +160,8 @@ static std::string getCodecType(const std::string& codecString) {
}

// FFmpeg encoder names
if (codecString == "libx264" || codecString == "h264" ||
codecString.find("h264_") == 0) {
if (codecString == "libx264" || codecString == "libopenh264" ||
codecString == "h264" || codecString.find("h264_") == 0) {
return "h264";
} else if (codecString == "libx265" || codecString == "hevc" ||
codecString.find("hevc_") == 0) {
Expand Down
1 change: 0 additions & 1 deletion native/image_decoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
}

Expand Down
Loading
Loading