ez-ffmpeg 0.16.0

A safe and ergonomic Rust interface for FFmpeg integration, designed for ease of use.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
# CI for ez-ffmpeg.
#
# Linux build and test jobs run on both native GitHub-hosted architectures
# (x64 and ARM64) in a two-lane FFmpeg matrix, because the crate supports
# linking either major and the two lanes are provisioned differently:
#
#   * ffmpeg 8.1 lane — `--features ffmpeg-sys-next/build`: the sys crate's
#     build script git-clones FFmpeg `release/8.1` (the branch is hardcoded to
#     the crate's own major.minor) and compiles/statically links it. This is
#     the same mechanism the pre-8 CI used, which then produced FFmpeg 7.1 —
#     after the dependency bump the SAME flag silently produces 8.1, which is
#     exactly why the 7.x lane below must NOT use it.
#
#   * ffmpeg 7.1 lane — explicit from-source build of FFmpeg `release/7.1`
#     (shared libs, default components, no external deps) installed under
#     $HOME/ffmpeg71 and linked via pkg-config. This proves the bump keeps
#     FFmpeg 7.x users working: ffmpeg-sys-next 8.1 probes the installed
#     headers and only enables cfg flags up to ffmpeg_7_1.
#
# Ubuntu's apt FFmpeg (6.1 on 24.04) matches neither lane, so it is never
# installed; the apt packages below are just the FFmpeg build toolchain
# (nasm/yasm/clang/pkg-config/zlib). The 7.1 install is cached; the 8.1 lane
# rebuilds FFmpeg per job because its source-build helper tunes for the current
# runner CPU.
#
# Cargo caches deliberately keep only registry and git sources, never `target`.
# ffmpeg-sys-next's native source build adds `-march=native -mtune=native`, and
# GitHub can schedule the same runner architecture on different CPU generations;
# restoring those compiled artifacts can otherwise terminate tests with SIGILL.
#
# Native platform coverage outside Linux runs both FFmpeg lanes on every
# GitHub-hosted architecture: Apple Silicon macOS, Windows x64, and Windows
# ARM64. macOS builds FFmpeg 8.1 from source and uses Homebrew's versioned
# FFmpeg 7 formula; Windows checks out pinned vcpkg revisions whose ports carry
# FFmpeg 8.1.2 and 7.1.2. These jobs build every functional non-static feature,
# compile the corresponding test trees, and run the CPU-only library suite.
#
# GPU-dependent code: the `opengl` frame filter needs a real GPU/display and
# its tests fail headless ("Failed to create Surfman connection"), so the
# `test` job omits that feature; the `build` job still compiles it (via
# `--all-features`) so it stays type-checked. The `wgpu` feature runs in the
# test job: its GPU oracles self-skip without an adapter while its CPU-only
# gates (offline naga shader validation, layout pins, builders) run for real.
# The macOS-only `videotoolbox` scheduler tests are
# `#[cfg(target_os = "macos")]`, so they do not run on Linux.
#
# The `build` job (all-features build + a default-features build + clippy) and
# the `test` job run per Linux architecture and FFmpeg lane. Two specialized
# jobs run once on Linux x64:
#
#   * `docs` — compiles the crate under `--cfg docsrs` (`DOCS_RS=1`), which stubs
#     ez-ffmpeg's own FFI call sites; the gate catches docsrs-stub compile gaps.
#     ffmpeg-sys-next still needs a real FFmpeg for its bindings, so this job builds
#     one via `ffmpeg-sys-next/build` (not a literal docs.rs run — docs.rs has no
#     network to clone FFmpeg — but it exercises the same `--cfg docsrs` paths).
#   * `asan` — AddressSanitizer over the library unit tests and the
#     lifecycle/teardown integration tests, guarding the use-after-free fixes.
#     FFmpeg is statically linked via ffmpeg-sys-next/build; ASan's interceptors
#     see its alloc/free, so a UAF where instrumented Rust touches FFmpeg-freed
#     memory is caught (one entirely inside un-instrumented FFmpeg C is not
#     reliably covered — those regressions are Rust-side here).
#
# All jobs are enforced gates.

name: CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

env:
  CARGO_TERM_COLOR: always
  RUST_BACKTRACE: 1

jobs:
  build:
    name: Build & Clippy (${{ matrix.arch }}, FFmpeg ${{ matrix.ffmpeg }})
    runs-on: ${{ matrix.runner }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - runner: ubuntu-24.04
            arch: x64
            ffmpeg: "8.1"
            ffmpeg-features: "--features ffmpeg-sys-next/build"
          - runner: ubuntu-24.04
            arch: x64
            ffmpeg: "7.1"
            ffmpeg-features: ""
          - runner: ubuntu-24.04-arm
            arch: arm64
            ffmpeg: "8.1"
            ffmpeg-features: "--features ffmpeg-sys-next/build"
          - runner: ubuntu-24.04-arm
            arch: arm64
            ffmpeg: "7.1"
            ffmpeg-features: ""
    steps:
      - uses: actions/checkout@v7

      # The 8.1 lane git-clones and statically builds FFmpeg inside target/
      # alongside the crate's own build tree, and the sum has exhausted the
      # stock image's free disk. Drop preinstalled toolchains this job never
      # touches (~25 GiB) before the first build.
      - name: Free disk space (from-source FFmpeg lane)
        if: matrix.ffmpeg == '8.1'
        run: |
          sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
          sudo docker image prune --all --force >/dev/null 2>&1 || true
          df -h /

      - name: Install FFmpeg build toolchain
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            nasm yasm clang pkg-config make git zlib1g-dev
          echo "nasm: $(nasm --version | head -1)"

      - uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy

      - name: Cache FFmpeg 7.1 install
        if: matrix.ffmpeg == '7.1'
        uses: actions/cache@v6
        with:
          path: ~/ffmpeg71
          key: ${{ runner.os }}-${{ runner.arch }}-ffmpeg71-shared-v1

      - name: Build FFmpeg 7.1 from source (if not cached)
        if: matrix.ffmpeg == '7.1'
        run: |
          if [ ! -f "$HOME/ffmpeg71/lib/pkgconfig/libavcodec.pc" ]; then
            git clone --depth=1 -b release/7.1 https://github.com/FFmpeg/FFmpeg ffmpeg-71-src
            cd ffmpeg-71-src
            ./configure --prefix="$HOME/ffmpeg71" \
                        --disable-programs --disable-doc \
                        --disable-static --enable-shared
            make -j"$(nproc)"
            make install
          fi

      - name: Point pkg-config at FFmpeg 7.1
        if: matrix.ffmpeg == '7.1'
        run: |
          echo "PKG_CONFIG_PATH=$HOME/ffmpeg71/lib/pkgconfig" >> "$GITHUB_ENV"
          echo "LD_LIBRARY_PATH=$HOME/ffmpeg71/lib" >> "$GITHUB_ENV"

      - name: Cache cargo sources
        uses: actions/cache@v6
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
          key: ${{ runner.os }}-cargo-sources-v1-${{ hashFiles('**/Cargo.toml') }}

      - name: Build
        run: cargo build --all-features ${{ matrix.ffmpeg-features }} --verbose

      # Compiles every #[cfg(test)] tree under --all-features WITHOUT running:
      # the opengl/wgpu suites need a GPU/display to RUN, but their tests carry
      # compile-time signature pins (e.g. the opengl typed-error fn-pointer
      # bindings) that only a test build type-checks. `--no-run` is headless-safe.
      - name: Build tests (all features, no run)
        run: cargo test --lib --tests --all-features ${{ matrix.ffmpeg-features }} --no-run

      # The crate's default feature set is empty (`default = []`), yet every job
      # here builds `--all-features`; this proves the no-feature build and its cfg
      # gates still compile. The ffmpeg-features flag only controls how FFmpeg is
      # provisioned, so "default features" still carries it.
      - name: Build (default features)
        run: cargo build ${{ matrix.ffmpeg-features }} --verbose

      # Safety-hygiene lints surface here. Not `-D warnings` yet: the crate has a
      # pre-existing unused-import / undocumented-unsafe backlog to clear first.
      - name: Clippy
        run: cargo clippy --all-features ${{ matrix.ffmpeg-features }} --lib

  test:
    name: Test (${{ matrix.arch }}, FFmpeg ${{ matrix.ffmpeg }})
    runs-on: ${{ matrix.runner }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - runner: ubuntu-24.04
            arch: x64
            ffmpeg: "8.1"
            ffmpeg-features: "--features ffmpeg-sys-next/build"
          - runner: ubuntu-24.04
            arch: x64
            ffmpeg: "7.1"
            ffmpeg-features: ""
          - runner: ubuntu-24.04-arm
            arch: arm64
            ffmpeg: "8.1"
            ffmpeg-features: "--features ffmpeg-sys-next/build"
          - runner: ubuntu-24.04-arm
            arch: arm64
            ffmpeg: "7.1"
            ffmpeg-features: ""
    # Runs the full library suite MINUS the opengl feature, whose tests need a
    # GPU/display (surfman connection creation fails headless). The wgpu
    # feature IS included: its GPU oracles self-skip when no adapter exists,
    # while its CPU-only gates — offline naga shader validation, uniform
    # layout pins, builder checks — run for real. Media assets are committed
    # as test.mp4.
    steps:
      - uses: actions/checkout@v7

      # The 8.1 lane git-clones and statically builds FFmpeg inside target/
      # alongside the crate's own build tree, and the sum has exhausted the
      # stock image's free disk. Drop preinstalled toolchains this job never
      # touches (~25 GiB) before the first build.
      - name: Free disk space (from-source FFmpeg lane)
        if: matrix.ffmpeg == '8.1'
        run: |
          sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
          sudo docker image prune --all --force >/dev/null 2>&1 || true
          df -h /

      - name: Install FFmpeg build toolchain
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            nasm yasm clang pkg-config make git zlib1g-dev

      - uses: dtolnay/rust-toolchain@stable

      - name: Cache FFmpeg 7.1 install
        if: matrix.ffmpeg == '7.1'
        uses: actions/cache@v6
        with:
          path: ~/ffmpeg71
          key: ${{ runner.os }}-${{ runner.arch }}-ffmpeg71-shared-v1

      - name: Build FFmpeg 7.1 from source (if not cached)
        if: matrix.ffmpeg == '7.1'
        run: |
          if [ ! -f "$HOME/ffmpeg71/lib/pkgconfig/libavcodec.pc" ]; then
            git clone --depth=1 -b release/7.1 https://github.com/FFmpeg/FFmpeg ffmpeg-71-src
            cd ffmpeg-71-src
            ./configure --prefix="$HOME/ffmpeg71" \
                        --disable-programs --disable-doc \
                        --disable-static --enable-shared
            make -j"$(nproc)"
            make install
          fi

      - name: Point pkg-config at FFmpeg 7.1
        if: matrix.ffmpeg == '7.1'
        run: |
          echo "PKG_CONFIG_PATH=$HOME/ffmpeg71/lib/pkgconfig" >> "$GITHUB_ENV"
          echo "LD_LIBRARY_PATH=$HOME/ffmpeg71/lib" >> "$GITHUB_ENV"

      - name: Cache cargo sources
        uses: actions/cache@v6
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
          key: ${{ runner.os }}-cargo-sources-v1-${{ hashFiles('**/Cargo.toml') }}

      - name: Test
        run: cargo test --lib --test avoption_warnings --test build_probe --test builder_option_validation --test frame_export_test --test frame_export_uniform_test --test frame_export_audio_test --test hls_fmp4 --test video_writer --test rawvideo_seek_gating --test per_output_video_filter --test packet_sink --test packet_sink_negative --test packet_sink_teardown --test packet_sink_unwind --test cli_emit_hardening --test hdr_to_sdr_detection --test logger_reentrancy --test packet_sink_job_failure --features async,rtmp,flv,subtitle,wgpu,cli ${{ matrix.ffmpeg-features }}

      # The packet-sink examples are API surface: keep them compiling on the
      # runtime lane (native-AAC cases in the suites above do real work even
      # without libx264; x264 cases self-skip at runtime).
      - name: Check packet-sink examples
        run: cargo check --example packet_sink_avc --example packet_sink_aac_channel --example packet_sink_av_transport --example hdr_to_sdr ${{ matrix.ffmpeg-features }}

      - name: Doctests
        run: cargo test --doc --features async,rtmp,flv,subtitle,wgpu,cli ${{ matrix.ffmpeg-features }}

      # aarch64 second-stage confirmation for the NAL start-code scan
      # refactor (perf/nal-stride3-scan, C2). Its x86-64 win is established
      # (paired same-round benchmark: 19 corpora, medians 1.01-3.3x, no pair
      # below parity); ARM throughput was the one open item in the
      # SHIP-TWO-STAGE disposition. This runs the in-repo #[ignore]
      # micro-benchmark on the arm64 runner, prints the full same-round
      # paired table (parent byte scan vs shipped stride scan) to the CI log
      # for review via `gh run view --log`, then applies a lenient
      # net-regression gate: nal_scan_parity_gate.py requires each corpus's
      # pooled median paired ratio >= 0.85 on both walks. That floor sits
      # below the x86 win (near-parity 1-2 byte-NAL corpora hover around 1.0
      # even unpinned) so it will not flake, yet a breach means the stride
      # scan is a material per-corpus loss on ARM and the aarch64 byte-scan
      # fallback (#[cfg(target_arch = "aarch64")] over the parent byte
      # finder) should be gated in before release tagging.
      #
      # Pinned to the 7.1 arm64 lane only: 7.1 links a cached shared FFmpeg,
      # so the release build recompiles Rust only (8.1 rebuilds FFmpeg
      # statically in release); the scanner is core and feature-independent,
      # so no extra cargo features are needed; and x64 needs no run (the x86
      # evidence is complete on the branch). Three invocations pool fifteen
      # same-round rounds per corpus so the median stays stable on a shared
      # runner (the near-parity tiny-NAL corpora are the noise-sensitive
      # ones and this keeps their pooled median clear of the floor).
      - name: NAL start-code scan aarch64 parity benchmark
        if: matrix.arch == 'arm64' && matrix.ffmpeg == '7.1'
        run: |
          set -o pipefail
          : > nal_scan_bench.log
          for i in 1 2 3; do
            cargo test --release --lib bench_nal_startcode_scan ${{ matrix.ffmpeg-features }} \
              -- --ignored --nocapture | tee -a nal_scan_bench.log
          done
          python3 .github/scripts/nal_scan_parity_gate.py nal_scan_bench.log

  cli-goldens:
    # Strict semantic-golden lane for the cli feature: builds a
    # version-matched FFmpeg WITH programs and libx264 (GPL is fine for CI
    # testing; nothing GPL ships in the crate) and runs the golden suite with
    # EZ_FFMPEG_CLI pointing at that binary, so a skip IS a failure. The
    # verified-profile table only lists lines whose lane has passed here:
    # 7.1 today. An 8.1 row is a matrix addition (same recipe, release/8.1
    # branch and an ffmpeg81full cache key) once the build matrix carries it.
    name: CLI goldens (FFmpeg ${{ matrix.ffmpeg }}, strict)
    runs-on: ubuntu-24.04
    strategy:
      fail-fast: false
      matrix:
        include:
          - ffmpeg: "7.1"
            branch: release/7.1
            prefix: ffmpeg71full
    steps:
      - uses: actions/checkout@v7

      - name: Install FFmpeg build toolchain
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            nasm yasm clang pkg-config make git zlib1g-dev libx264-dev

      - uses: dtolnay/rust-toolchain@stable

      - name: Cache FFmpeg ${{ matrix.ffmpeg }} full install
        uses: actions/cache@v6
        with:
          path: ~/${{ matrix.prefix }}
          key: ${{ runner.os }}-${{ runner.arch }}-${{ matrix.prefix }}-shared-v1

      - name: Build FFmpeg ${{ matrix.ffmpeg }} with programs (if not cached)
        run: |
          if [ ! -x "$HOME/${{ matrix.prefix }}/bin/ffmpeg" ]; then
            git clone --depth=1 -b ${{ matrix.branch }} https://github.com/FFmpeg/FFmpeg ffmpeg-golden-src
            cd ffmpeg-golden-src
            ./configure --prefix="$HOME/${{ matrix.prefix }}" \
                        --disable-doc \
                        --disable-static --enable-shared \
                        --enable-gpl --enable-libx264
            make -j"$(nproc)"
            make install
          fi

      - name: Point pkg-config at the golden FFmpeg
        run: |
          echo "PKG_CONFIG_PATH=$HOME/${{ matrix.prefix }}/lib/pkgconfig" >> "$GITHUB_ENV"
          echo "LD_LIBRARY_PATH=$HOME/${{ matrix.prefix }}/lib" >> "$GITHUB_ENV"

      - name: Cache cargo sources
        uses: actions/cache@v6
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
          key: ${{ runner.os }}-cargo-sources-v1-${{ hashFiles('**/Cargo.toml') }}

      - name: Build the pinned emitted examples
        run: cargo build --features cli --examples

      - name: Run the cli suite with strict goldens
        run: |
          EZ_FFMPEG_CLI="$HOME/${{ matrix.prefix }}/bin/ffmpeg" cargo test --features cli --lib cli::

      # The `cli::` filter above cannot reach the frame-export byte-parity
      # golden (`core::frame_export::color_goldens::default_tier_matches_cli_bytes`),
      # whose gate compares libswscale builds and treats a skip as a failure
      # under EZ_FFMPEG_CLI. The crate links the same install the strict
      # binary comes from, so the parity comparison actually executes here.
      - name: Run the frame-export goldens with the strict CLI
        run: |
          EZ_FFMPEG_CLI="$HOME/${{ matrix.prefix }}/bin/ffmpeg" cargo test --features cli --lib core::frame_export::color_goldens

      - name: Emit-hardening integration tests
        run: cargo test --features cli --test cli_emit_hardening

      # RTMP loopback suite: real-TCP rml_rtmp watchers against the embedded
      # RTMP server (late-joiner GOP replay, EOF status delivery, StreamBuilder
      # port release, AVCC parser hardening). The publish leg re-encodes
      # test.mp4 to H.264 because create_rtmp_input pins h264/aac, so it needs
      # an FFmpeg with a working H.264 encoder — this lane's libx264 build is
      # the only one in the matrix that has one: the from-source and vcpkg
      # lanes carry no H.264 encoder, and the runners expose no V4L2 device or
      # VideoToolbox session for the hardware fallbacks. The load-harness
      # measurements in the same file stay #[ignore]d.
      - name: RTMP loopback tests
        run: cargo test --features rtmp --test rtmp_loopback

  platform:
    name: Platform (${{ matrix.platform }}, FFmpeg ${{ matrix.ffmpeg }})
    runs-on: ${{ matrix.runner }}
    timeout-minutes: 120
    strategy:
      fail-fast: false
      matrix:
        include:
          - runner: macos-15
            platform: macos-15-arm64
            os: macos
            target: aarch64-apple-darwin
            ffmpeg: "8.1"
            build_features: ffmpeg-sys-next/build-videotoolbox,async,opengl,wgpu,rtmp,flv,subtitle,cli
            test_features: ffmpeg-sys-next/build-videotoolbox,async,rtmp,flv,subtitle,wgpu,cli
            default_ffmpeg_features: --features ffmpeg-sys-next/build-videotoolbox
            test_args: >-
              --
              --skip core::scheduler::ffmpeg_scheduler::tests::test_hwaccel
              --skip core::scheduler::ffmpeg_scheduler::tests::test_to_stdout
          - runner: macos-15
            platform: macos-15-arm64
            os: macos
            target: aarch64-apple-darwin
            ffmpeg: "7.1"
            build_features: async,opengl,wgpu,rtmp,flv,subtitle,cli
            test_features: async,rtmp,flv,subtitle,wgpu,cli
            default_ffmpeg_features: ""
            test_args: >-
              --
              --skip core::scheduler::ffmpeg_scheduler::tests::test_hwaccel
              --skip core::scheduler::ffmpeg_scheduler::tests::test_to_stdout
          - runner: windows-2025
            platform: windows-2025-x64
            os: windows
            target: x86_64-pc-windows-msvc
            ffmpeg: "8.1"
            vcpkg_triplet: x64-windows
            vcpkg_ref: b5229343b4b80264ed51e89c6a7dcd0cbe85e9cc
            vcpkg_cache: b5229343-ffmpeg-8.1.2-v1
            build_features: async,opengl,wgpu,rtmp,flv,subtitle,cli
            test_features: async,rtmp,flv,subtitle,wgpu,cli
            default_ffmpeg_features: ""
            test_args: ""
          - runner: windows-2025
            platform: windows-2025-x64
            os: windows
            target: x86_64-pc-windows-msvc
            ffmpeg: "7.1"
            vcpkg_triplet: x64-windows
            vcpkg_ref: 6070d5cb2f818921c10ccdf6f35f3415a2503369
            vcpkg_cache: 6070d5cb-ffmpeg-7.1.2-p5-v1
            build_features: async,opengl,wgpu,rtmp,flv,subtitle,cli
            test_features: async,rtmp,flv,subtitle,wgpu,cli
            default_ffmpeg_features: ""
            test_args: ""
          - runner: windows-11-arm
            platform: windows-11-arm64
            os: windows
            target: aarch64-pc-windows-msvc
            ffmpeg: "8.1"
            vcpkg_triplet: arm64-windows
            vcpkg_ref: b5229343b4b80264ed51e89c6a7dcd0cbe85e9cc
            vcpkg_cache: b5229343-ffmpeg-8.1.2-v1
            build_features: async,opengl,wgpu,rtmp,flv,subtitle,cli
            test_features: async,rtmp,flv,subtitle,wgpu,cli
            default_ffmpeg_features: ""
            test_args: -- --test-threads=1
          - runner: windows-11-arm
            platform: windows-11-arm64
            os: windows
            target: aarch64-pc-windows-msvc
            ffmpeg: "7.1"
            vcpkg_triplet: arm64-windows
            vcpkg_ref: 6070d5cb2f818921c10ccdf6f35f3415a2503369
            vcpkg_cache: 6070d5cb-ffmpeg-7.1.2-p5-v1
            build_features: async,opengl,wgpu,rtmp,flv,subtitle,cli
            test_features: async,rtmp,flv,subtitle,wgpu,cli
            default_ffmpeg_features: ""
            test_args: -- --test-threads=1
    env:
      CARGO_BUILD_JOBS: 2
      CARGO_INCREMENTAL: 0
    steps:
      - uses: actions/checkout@v7

      - uses: dtolnay/rust-toolchain@stable

      - name: Verify native Rust target
        shell: bash
        run: |
          rustc -vV
          test "$(rustc -vV | sed -n 's/^host: //p')" = "${{ matrix.target }}"

      - name: Cache cargo sources
        uses: actions/cache@v6
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
          key: ${{ runner.os }}-cargo-sources-v1-${{ hashFiles('**/Cargo.toml') }}

      # ffmpeg@7 is keg-only and uses absolute Homebrew install names, so its
      # shared libraries remain discoverable without DYLD_LIBRARY_PATH. The
      # versioned formula tracks the latest 7.1 patch release.
      - name: Install FFmpeg 7.1 with Homebrew
        if: matrix.os == 'macos' && matrix.ffmpeg == '7.1'
        shell: bash
        env:
          HOMEBREW_NO_AUTO_UPDATE: "1"
          HOMEBREW_NO_INSTALL_CLEANUP: "1"
        run: |
          # GitHub's image may carry an unrelated untrusted aws/tap. Remove it
          # before Homebrew runs diagnostics so the job stays annotation-free.
          brew untap aws/tap >/dev/null 2>&1 || true
          brew install ffmpeg@7
          prefix="$(brew --prefix ffmpeg@7)"
          version="$("$prefix/bin/ffmpeg" -version | sed -n 's/^ffmpeg version \([^ ]*\).*/\1/p')"
          case "$version" in
            7.1*) ;;
            *) echo "Expected FFmpeg 7.1.x, found $version" >&2; exit 1 ;;
          esac
          echo "PKG_CONFIG_PATH=$prefix/lib/pkgconfig" >> "$GITHUB_ENV"
          echo "$prefix/bin" >> "$GITHUB_PATH"
          PKG_CONFIG_PATH="$prefix/lib/pkgconfig" pkg-config --modversion libavcodec

      - name: Configure vcpkg
        if: matrix.os == 'windows'
        shell: pwsh
        run: |
          $root = $env:VCPKG_INSTALLATION_ROOT
          $cache = Join-Path $env:GITHUB_WORKSPACE "vcpkg-bincache"
          New-Item -ItemType Directory -Force -Path $cache | Out-Null
          "VCPKG_ROOT=$root" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
          "VCPKG_DEFAULT_TRIPLET=${{ matrix.vcpkg_triplet }}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
          "VCPKGRS_TRIPLET=${{ matrix.vcpkg_triplet }}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
          "VCPKGRS_DYNAMIC=1" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
          "VCPKG_DEFAULT_BINARY_CACHE=$cache" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
          "$root\installed\${{ matrix.vcpkg_triplet }}\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append

      - name: Cache vcpkg binary packages
        if: matrix.os == 'windows'
        uses: actions/cache@v6
        with:
          path: vcpkg-bincache
          key: ${{ runner.os }}-${{ runner.arch }}-vcpkg-${{ matrix.vcpkg_cache }}

      - name: Install FFmpeg ${{ matrix.ffmpeg }} with vcpkg
        if: matrix.os == 'windows'
        shell: pwsh
        run: |
          git config --global --add safe.directory $env:VCPKG_ROOT
          if ($LASTEXITCODE -ne 0) { throw "git config exited with code $LASTEXITCODE" }
          git -C $env:VCPKG_ROOT fetch --depth=1 origin ${{ matrix.vcpkg_ref }}
          if ($LASTEXITCODE -ne 0) { throw "git fetch exited with code $LASTEXITCODE" }
          git -C $env:VCPKG_ROOT checkout --force FETCH_HEAD
          if ($LASTEXITCODE -ne 0) { throw "git checkout exited with code $LASTEXITCODE" }
          & "$env:VCPKG_ROOT\bootstrap-vcpkg.bat" -disableMetrics
          if ($LASTEXITCODE -ne 0) { throw "bootstrap-vcpkg exited with code $LASTEXITCODE" }
          & "$env:VCPKG_ROOT\vcpkg.exe" install "ffmpeg:${{ matrix.vcpkg_triplet }}" --clean-after-build
          if ($LASTEXITCODE -ne 0) { throw "vcpkg install exited with code $LASTEXITCODE" }
          $ffmpeg = & "$env:VCPKG_ROOT\vcpkg.exe" list | Select-String "^ffmpeg:"
          if ($LASTEXITCODE -ne 0) { throw "vcpkg list exited with code $LASTEXITCODE" }
          if (-not $ffmpeg) { throw "The FFmpeg vcpkg package was not installed" }
          $expectedVersion = [regex]::Escape("${{ matrix.ffmpeg }}")
          if ("$ffmpeg" -notmatch "\s$expectedVersion\.") {
            throw "Expected FFmpeg ${{ matrix.ffmpeg }}.x, found: $ffmpeg"
          }
          $ffmpeg

      # The crate-level `static` feature is intentionally omitted on Windows:
      # these vcpkg triplets provide shared FFmpeg libraries. macOS uses the
      # dependency's source-build feature for 8.1 and Homebrew's shared 7.1
      # libraries for the compatibility lane. Every functional optional feature
      # is type-checked on each native platform.
      - name: Build all non-static features
        run: cargo build --features "${{ matrix.build_features }}" --verbose

      - name: Build all non-static feature tests
        run: cargo test --lib --tests --features "${{ matrix.build_features }}" --no-run

      # This keeps the crate's own default feature set empty while carrying only
      # the dependency feature needed to provision source-built FFmpeg 8.1 on
      # macOS. The system/vcpkg lanes pass an empty string here.
      - name: Build default crate features
        run: cargo build ${{ matrix.default_ffmpeg_features }} --verbose

      # GPU-dependent features are compile-only above. GitHub's macOS runner
      # also cannot create a VideoToolbox compression session, so its two
      # hardware-encoder tests stay compile-only. The remaining suite exercises
      # the platform scheduler, device, socket, RTMP, subtitle, and async paths.
      # The cli suite is profile-aware: on linked 8.1 its execution tests
      # assert the typed UnverifiedRuntimeProfile refusal, on the vcpkg 7.1
      # lanes the semantic goldens skip deterministically (today no
      # version-matched ffmpeg binary is on PATH, and even if one ever lands
      # there the goldens' lenient gate skips every shape whose canonical
      # command names an encoder this libx264-less link lacks; the
      # in-process gates accept EncoderUnavailable on that link), and on the
      # Homebrew 7.1 lane they run in full against the matching CLI. The
      # emit-hardening tests type-check emitted programs with
      # `rustc --emit=metadata`, needing no native linking.
      #
      # xnu regrows an unread TCP receive window past an explicitly pinned
      # SO_RCVBUF (observed on these runners: pinned to 16384 at accept,
      # grown back past 500k by the packet-sink wedge test's park — the
      # test's own drain probes feed the autotuner), so a never-reading peer
      # cannot hold a sender blocked under the 4 MiB default ceiling. Cap
      # the ceilings to a bound the stream saturates within the test's
      # first second. The bound must stay comfortably above the loopback
      # MSS (16 KiB): at 16384 a drained window cannot re-announce itself
      # and the wedge stages starve waiting for a refill that never comes.
      - name: Cap TCP buffer autotuning (macOS)
        if: matrix.os == 'macos'
        run: |
          sudo sysctl -w net.inet.tcp.autorcvbufmax=131072
          sudo sysctl -w net.inet.tcp.autosndbufmax=131072
      - name: Run CPU-only library tests
        run: cargo test --lib --test avoption_warnings --test build_probe --test builder_option_validation --test frame_export_test --test frame_export_uniform_test --test frame_export_audio_test --test hls_fmp4 --test video_writer --test rawvideo_seek_gating --test per_output_video_filter --test packet_sink --test packet_sink_negative --test packet_sink_teardown --test packet_sink_unwind --test cli_emit_hardening --test hdr_to_sdr_detection --test logger_reentrancy --test packet_sink_job_failure --features "${{ matrix.test_features }}" ${{ matrix.test_args }}

      - name: Check packet-sink examples
        run: cargo check --example packet_sink_avc --example packet_sink_aac_channel --example packet_sink_av_transport --example hdr_to_sdr --features "${{ matrix.test_features }}"

  docs:
    name: Docs (docsrs cfg compile)
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v7

      - name: Install FFmpeg build toolchain
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            nasm yasm clang pkg-config make git zlib1g-dev

      - uses: dtolnay/rust-toolchain@stable

      - name: Cache cargo sources
        uses: actions/cache@v6
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
          key: ${{ runner.os }}-cargo-sources-v1-${{ hashFiles('**/Cargo.toml') }}

      # `DOCS_RS=1` makes ez-ffmpeg's build.rs pass `--cfg docsrs`, compiling its FFI
      # call sites as stubs; this gate catches a docsrs-stub compile gap (a fn that
      # takes a `#[cfg(not(docsrs))]` type but is itself ungated). ffmpeg-sys-next's
      # build.rs does NOT honor DOCS_RS, so it still needs a real FFmpeg for its
      # bindings; `ffmpeg-sys-next/build` compiles one from source (the runner has
      # the network docs.rs lacks), keeping the job self-contained. The features
      # otherwise match `[package.metadata.docs.rs]`. Not `-D warnings`: docs.rs does
      # not, and the crate has a pre-existing intra-doc-link backlog (a separate
      # task); this lane is the stub-compile gate, not doc-link cleanup.
      - name: cargo doc (docsrs cfg)
        env:
          DOCS_RS: "1"
        run: cargo doc --no-deps --features ffmpeg-sys-next/build,async,opengl,rtmp,flv,subtitle,wgpu

  asan:
    name: ASAN (UAF, FFmpeg 8.1)
    runs-on: ubuntu-24.04
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v7

      - name: Install FFmpeg build toolchain
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            nasm yasm clang pkg-config make git zlib1g-dev

      - uses: dtolnay/rust-toolchain@nightly

      - name: Cache cargo sources
        uses: actions/cache@v6
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
          key: ${{ runner.os }}-cargo-sources-v1-${{ hashFiles('**/Cargo.toml') }}

      # `-Zsanitizer=address` instruments the target crates' Rust code. The explicit
      # `--target` scopes the sanitizer to those crates and off the host build
      # scripts / proc-macros; std is reused un-instrumented from the sysroot (so no
      # `-Zbuild-std`), which suffices because the UAF regressions here are detected
      # on the Rust side. `detect_leaks=0` because FFmpeg's one-time global
      # allocations are not leaks. `--lib` runs the library unit tests under the
      # same instrumentation as the lifecycle/teardown integration suites. GPU
      # features are omitted here (the test job runs wgpu's self-skipping suite;
      # opengl needs a display either way) — the tests under ASAN do not need
      # them.
      - name: ASAN UAF tests
        env:
          RUSTFLAGS: "-Zsanitizer=address"
          RUSTDOCFLAGS: "-Zsanitizer=address"
          ASAN_OPTIONS: "detect_leaks=0"
        run: |
          cargo +nightly test --target x86_64-unknown-linux-gnu \
            --features ffmpeg-sys-next/build,async,rtmp,flv,subtitle \
            --lib \
            --test bsf --test mux_teardown --test lifecycle \
            --test start_failure_join --test start_panic_unwind \
            --test shortest --test frame_pipeline_hardening \
            --test build_probe --test frame_export_test \
            --test frame_export_uniform_test --test frame_export_audio_test \
            --test video_writer --test packet_sink_teardown --test packet_sink_unwind \
            --test logger_reentrancy