The ffmpeg on your NAS is a different binary: no AAC decoding, no encoding worth using
Symptom
I had a batch of audio files sitting on a NAS and a one-line ffmpeg command that converted them
perfectly on my workstation. The obvious optimisation was to run the conversion on the NAS itself: the
files are already there, the box has ffmpeg on its PATH, and pushing gigabytes across the LAN twice to
do the same work seemed silly.
It did not run. The job died almost immediately, and the binary’s complaint was not about memory, disk or CPU — it reported that the codec it needed was not available in this build. Not a corrupt input, not a container problem, not a permissions issue. The decoder for the input stream simply did not exist, and when I worked around that by feeding it something else, the encoder I wanted did not exist either.
That is the whole shape of the problem, and it is worth stating plainly because the name of the command
misled me for a good twenty minutes: the ffmpeg on the appliance is a different binary from the
ffmpeg on your workstation. Same name, same rough command line, a much smaller feature set.
What it is not
The instinct, when a media job fails on a small box and succeeds on a big one, is to blame the small box’s resources. That is the wrong tree and it is an expensive one to climb, because “transcoding is heavy” is true often enough to sound convincing.
It is not RAM. An out-of-memory kill looks nothing like this: the process is terminated by the kernel, you
get a signal, not a message from ffmpeg about a codec.
It is not CPU or thermals. A slow build is still a build. Slow means it takes twenty minutes; it does not mean it fails in the first second.
It is not the file. The same input transcoded fine on the workstation, from the same bytes — the cheapest check available, and the one that immediately rules out corruption, an unusual container or a broken mount.
It is not PATH weirdness or a stale package either. There is only one ffmpeg, it is the vendor’s, and
it is doing exactly what it was compiled to do.
What I checked
Stop guessing and ask the binary. Three commands settle the question in about two seconds, and they are worth running on any machine before you design a pipeline around it:
ssh nasuser@nas.example.lan 'ffmpeg -version'
ssh nasuser@nas.example.lan 'ffmpeg -codecs'
ssh nasuser@nas.example.lan 'ffmpeg -encoders'
ffmpeg -version prints the configuration: line — the full list of --enable-* and --disable-* flags
the build was configured with. That line is the ground truth about what the binary can do. If the external
library you are counting on is not enabled there, no amount of command-line argument fiddling will conjure
it up.
ffmpeg -codecs and ffmpeg -encoders are the same answer from the other side: what is actually
registered and usable at runtime, with the decode/encode capability letters in the left column. Run the
same three commands on your workstation and diff the output. The gap between the two lists is the set of
pipelines that will work in one place and fail in the other.
On the appliance, the gap was large. The build could not decode AAC, and it could not encode to the
formats I would have reached for. It is not an ffmpeg capable of general media work; it is an ffmpeg
that covers the specific formats the vendor’s own indexing and streaming features need.
Why it is built that way
This is not vendor incompetence and it is not worth being annoyed about. Codecs carry patent and licensing
obligations, and those obligations attach to the party that ships the binary. An appliance vendor ships
what it is licensed to ship for the features it advertises — thumbnailing, library indexing, the streaming
its own media apps perform. The binary exists to serve those features. That it is also on your PATH and
answers to ffmpeg is a side effect of how the firmware is assembled, not a promise.
The generalisation is the useful part, because this is not a NAS-specific trap:
- Slim container images (
-alpine,-slim, distroless-adjacent bases) routinely carry stripped builds of familiar tools, or BusyBox applets wearing the names of GNU utilities with a fraction of the flags. - Vendor SDK images and embedded rootfs builds do the same for the same reasons: size, licensing, support surface.
- Even on a normal distro, a package can be split so that the codec-heavy parts live in a separate repo you have not enabled.
A command’s name tells you which interface it implements. It tells you nothing about which features were compiled in.
The fix: compute where the toolchain is
The working pattern is unglamorous and correct: do the media work on a general-purpose machine and use
the appliance as storage. Pull the file to the workstation, transcode there with a full ffmpeg, push
the result back. The extra LAN round trip costs seconds on a gigabit link; debugging a pipeline against a
binary that structurally cannot run it costs an afternoon and ends in the same place anyway.
Which leads directly to the second half of this incident, because the two always appear together: on this
appliance, moving the files is also not what you expect. scp and sftp both fail, because there is no
SFTP subsystem for them to talk to — I wrote up the scp failure itself
separately, since it has its own misleading error.
The way through needs nothing but a shell and cat on the far end:
# NAS -> workstation
ssh nasuser@nas.example.lan 'cat "/volume1/media/clip.m4a"' > ./clip.m4a
# transcode locally, with a full ffmpeg
# workstation -> NAS
cat ./clip.out.mp3 | ssh nasuser@nas.example.lan 'cat > "/volume1/media/clip.mp3"'
scp and sftp are protocols: the client asks the server to start a named subsystem, and if the server
has no such subsystem the session ends before a single byte of your file moves. A pipe over ssh makes no
such request. It runs a command in a shell and connects your local stdin/stdout to it — the lowest common
denominator of SSH, available anywhere a login shell exists.
The caveats are real and you should know them before you rely on this:
- No resume. A dropped connection means starting over. For large files, expect to babysit it.
- No metadata. Permissions, ownership and timestamps are not preserved; the target file gets whatever
the remote shell’s
umaskproduces. - Quoting matters twice. The path inside the single quotes is parsed by the remote shell, so paths with spaces need their own quotes inside — as above — or they silently become two arguments.
- Errors can be quiet.
cat >truncates its target before it writes. If the transfer dies halfway you are left with a plausible-looking partial file, not an empty one.
Verify after the move
That last point is why a byte count is not verification. Compare digests on both ends:
sha256sum ./clip.out.mp3
ssh nasuser@nas.example.lan 'sha256sum "/volume1/media/clip.mp3"'
And because a truncated media file very often still opens, probe the result rather than trusting that a player accepted it:
ffprobe -v error -show_format -show_streams ./clip.out.mp3
Check that the duration matches the source and that the stream is the codec you asked for. A file that opens, plays the first thirty seconds and stops is the classic outcome of a transfer nobody checked.
What to remember
On an appliance, check capability rather than inferring it from a command’s name. ffmpeg -codecs,
ffmpeg -encoders and the configuration: line from ffmpeg -version cost two seconds and they tell you
whether the pipeline you are about to design can exist on that machine at all. Doing it first turns an
afternoon of plausible wrong theories into a decision.
The wider rule is the same one in different clothing: same binary name, different build. It applies to slim container images, BusyBox environments and vendor SDK images exactly as much as it does to a NAS. When a familiar tool behaves unfamiliarly on an unfamiliar box, the first question is not what you typed — it is which build you are typing at.