FFmpeg is weird...

...and it has to be! As the undisputed multimedia tool that interfaces with tons of other dependencies, it needs to work alongside, and with, conflicting licenses, patents, etc.

The result is that to access the best of what the multimedia world has to offer, you'd need to compile FFmpeg yourself - a tedious process that has spawned tons and tons of scripts that purport to "help" users who need specific FFmpeg dependencies.

I personally find these schemes to be terrible, and working with FFmpeg to be terrible as a whole (not the CLI - I actually think the FFmpeg CLI is pretty intuitive if you have a good understanding of multimedia fundamentals) as such. It took me working on FFmpeg itself to understand how to actually compile FFmpeg with specific dependencies.

Enter: Nix

Pulling out the "Purely Functional Software Deployment Model" paper is now kind of a meme at this point - but deterministic environments like the ones afforded to us by Nix actually solves the FFmpeg issue!

Wait, but isn't this just a variation of the "build scripts" that you purport to hate?

It's roughly in the same idea-space, but since we're piggybacking outselves onto the Nix package manager rather than some script that is undocumented and easy to break

Wait but the Nix package manager is pretty inscrutible itself

Yeah. I hope it gets better too. I don't think Nix's issues with being absolutely difficult to use is quite the same as my gripe with the typical build scripts, though.

Example 1: Dev flake

This example only requries the presense of the Nix package manager, and is how I invoke FFmpeg on my server. We define a flake our usual way:

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };
  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem
      (system:
        let
          pkgs = import nixpkgs {
            inherit system;
          };
        in
        with pkgs;
        {
          devShells.default = mkShell {
            buildInputs = [
                # We add our packages here
            ];
          };
        }
      );
}

then add an overlay to the output

    ...
    flake-utils.lib.eachDefaultSystem
      (system:
        let
          pkgs = import nixpkgs {
            inherit system;
            overlays = [
                (self: super: {ffmpeg-c = super.ffmpeg.override {
                    ## We define the build options here
                    withUnfree = true; 
                    withDav1d = true;
                };})
            ];
          };
        in
    ...

and then add this newly defined package to our devShell

        ...
        with pkgs;
        {
          devShells.default = mkShell {
            buildInputs = [
                ffmpeg-c
            ];
          };
        }
        ...

Hey, why are we defining the package to a new name? I thought the point with overlays is to override packages?

I'm glad you asked! In this example, because only the devShell is depenedent on our new ffmpeg-c package, we don't see big issues by naming it ffmpeg, but of course, this just forshadowing for the next part, where...

Example 2: Overriding in NixOS

...we're no longer just defining just a devShell, but a NixOS system as a whole.

In a "system" flake for NixOS or nix-darwin, instead of devShells.default as the output of our flake, we have nixOSConfigurations and darwinConfigurations. Also remember - FFmpeg is everywhere. Most fresh Linux systems have a non-encumbered version of FFmpeg somewhere to handle video - the first step in making a system usable is making it play cat videos.

So let's try defining an overlay module in overlays/default.nix that overrides ffmpeg!

# ./overlays/default.nix
{ config, pkgs, lib, ... }:

{
  nixpkgs.overlays = [
    (self: super: {
        ffmpeg = super.ffmpeg.override {
            withUnfree = true;
            withDav1d = true;
        };
    })
  ];
}

and let our system use it,

    ...
    mySystem = nixpkgs.lib.nixosSystem {
        specialArgs = { inherit inputs; }; # Pass flake inputs to our config
        modules = [
            ...
            ./nixos/atlantis/configuration.nix
            (import ./overlays/default.nix)
            ...
        ];
    };
    ...

then try running nixos-rebuild switch...

Hey, it's been three hours and nixos is trying to rebuild all of KDE from source. What the hell is going on?

We're running into the issue that the ffmpeg package is no longer what nixpkgs says it is - we've overridden it, and NixOS needs to evaluate if the system even compiles with the new config. Obviously, we want to avoid that. We want to use binaries that other people spent their precious machine hours on.

The solution is what I alluded to earlier in Example 1:

# ./overlays/default.nix
{ config, pkgs, lib, ... }:

{
  nixpkgs.overlays = [
    (self: super: {
        ffmpeg-c = super.ffmpeg.override {
            withUnfree = true;
            withDav1d = true;
        };
    })
  ];
}

Introducing a new package that other packages don't depend on. This does mean that you'll have two FFmpeg binaries floating around in your system, but that's a small price to pay.

Despite all that, this system of managing FFmpeg's dependencies is still the cleanest and most robust solution to the FFmpeg dependency problem. Not only that, but you also learn a little bit about how to manage your own devShells for niche use cases - something that will almost certainly help.

Have fun with your multimedia!

-r/c/s