Read FFprobe's answer without spawning FFprobe #211

Merged
JMR-dev merged 5 commits from test/ffprobe-mapping-seam into main 2026-09-06 01:00:23 +00:00
JMR-dev commented 2026-09-02 23:25:45 +00:00 (Migrated from github.com)

Closes #195. Touches production, like #210.

The split

readMediaInformation was 114 missed instructions and 24 missed branches — the second-largest block on the wave-4 report — and exactly one line of it needed a device:

FFprobeKit.getMediaInformation(path).getMediaInformation()

Everything after it reads an ordinary object, so it moves into ffprobeInfoFrom(info: MediaInformation) and the edge keeps the call plus the null check.

JVM-safe, verified rather than assumed. javap over the committed AAR's runtime jar: MediaInformation(JSONObject, List<StreamInformation>, List<Chapter>) and StreamInformation(JSONObject) are plain public constructors, and neither class's <clinit> loads the native library.

The decision worth reaching

containerFrom(formatName, video?.getCodec()). FFprobe reports matroska,webm for both MKV and WebM — they share a demuxer — so the video codec is the only thing separating them. containerFrom has thirty-three covered branches and not one can notice that argument being dropped: the mistake is at the call, not in the callee. Every existing containerFrom test stays green while every VP9 WebM quietly becomes an MKV.

Two things the tests found rather than confirmed

The format properties are nested under "format". getFormat() resolves through getStringFormatProperty, not off the top-level object. The first fixture put the keys at the top level and four cases failed with a null container. The helper's KDoc records it so the next fixture doesn't rediscover it.

One mutation survived the first pass. Reading dimensions as streams.firstNotNullOfOrNull { it.getWidth() } instead of video?.getWidth() gave the same answer, because the fixture put the dimensions on the chosen video stream — which was also the first stream carrying any. The two readings agreed, so the test could not tell them apart.

Separating them needs a chosen video stream with no dimensions and a later one that has them — a real shape, since FFprobe omits width/height for a stream it could not measure. That is now its own test, and the mutation reddens it.

Acceptance: mutations run and restored

mutation red
drop the video codec argument to containerFrom 1
take the last video stream instead of the first 1
read dimensions from any stream, not the chosen one 0 → 1 after the new case
let an unparseable duration throw instead of answering zero 1

Scope

Does not touch probeWithFFprobe's two catch arms. The Error arm is already driven by MediaProbeNativeLoadTest and ConversionViewModelProbeFailureTest; the throw e rethrow is unreachable on the JVM because the first FFmpegKit touch always yields the native-load Error first.

Verification

assembleDebug + testDebugUnitTest (full suite) + compileDebugAndroidTestKotlin + ktlintCheck + detekt + lintDebug — green. One ktlint complaint fixed with ktlintFormat, not by hand.

🤖 Generated with Claude Code

Closes #195. **Touches production**, like #210. ## The split `readMediaInformation` was 114 missed instructions and 24 missed branches — the second-largest block on the wave-4 report — and **exactly one line of it needed a device**: ```kotlin FFprobeKit.getMediaInformation(path).getMediaInformation() ``` Everything after it reads an ordinary object, so it moves into `ffprobeInfoFrom(info: MediaInformation)` and the edge keeps the call plus the null check. **JVM-safe, verified rather than assumed.** `javap` over the committed AAR's runtime jar: `MediaInformation(JSONObject, List<StreamInformation>, List<Chapter>)` and `StreamInformation(JSONObject)` are plain public constructors, and neither class's `<clinit>` loads the native library. ## The decision worth reaching `containerFrom(formatName, video?.getCodec())`. FFprobe reports `matroska,webm` for **both** MKV and WebM — they share a demuxer — so the video codec is the only thing separating them. `containerFrom` has thirty-three covered branches and **not one can notice that argument being dropped**: the mistake is at the call, not in the callee. Every existing `containerFrom` test stays green while every VP9 WebM quietly becomes an MKV. ## Two things the tests found rather than confirmed **The format properties are nested under `"format"`.** `getFormat()` resolves through `getStringFormatProperty`, not off the top-level object. The first fixture put the keys at the top level and four cases failed with a null container. The helper's KDoc records it so the next fixture doesn't rediscover it. **One mutation survived the first pass.** Reading dimensions as `streams.firstNotNullOfOrNull { it.getWidth() }` instead of `video?.getWidth()` gave the same answer, because the fixture put the dimensions on the chosen video stream — which was also the first stream carrying any. The two readings agreed, so the test could not tell them apart. Separating them needs a chosen video stream with **no** dimensions and a later one that has them — a real shape, since FFprobe omits `width`/`height` for a stream it could not measure. That is now its own test, and the mutation reddens it. ## Acceptance: mutations run and restored | mutation | red | |---|---| | drop the video codec argument to `containerFrom` | 1 | | take the **last** video stream instead of the first | 1 | | read dimensions from any stream, not the chosen one | **0 → 1** after the new case | | let an unparseable duration throw instead of answering zero | 1 | ## Scope Does not touch `probeWithFFprobe`'s two catch arms. The `Error` arm is already driven by `MediaProbeNativeLoadTest` and `ConversionViewModelProbeFailureTest`; the `throw e` rethrow is unreachable on the JVM because the first FFmpegKit touch always yields the native-load `Error` first. ## Verification `assembleDebug` + `testDebugUnitTest` (full suite) + `compileDebugAndroidTestKotlin` + `ktlintCheck` + `detekt` + `lintDebug` — green. One ktlint complaint fixed with `ktlintFormat`, not by hand. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.