Joining files picked through the system picker fails on the stream-copy path #238

Closed
opened 2026-09-06 05:29:10 +00:00 by JMR-dev · 0 comments
JMR-dev commented 2026-09-06 05:29:10 +00:00 (Migrated from github.com)

Found by the first test ever to hand the join path a real content:// input (#225). This is a production defect, not a test problem.

Joining files picked through the system picker fails whenever the strategy is stream copy — which is the matched-files case the UI advertises as "Files matched, so they were joined without re-encoding — no quality loss."

The failure

FFmpegException: Joining failed (1):
  [ffkitsaf @ 0x7c6fc348c980] Protocol 'ffkitsaf' not on whitelist 'file,crypto,data'!
  [in#0] Error opening input: Invalid argument
  Error opening input file /data/.../cache/joined_from_content.concat_list.txt

Reproduced on a local API 34 emulator by ContentUriInputTest.contentUriInputsJoinThroughTheSafBridge, joining two matching clips served over content://.

Why every real user hits it

JoinScreen picks with ActivityResultContracts.OpenMultipleDocuments, so the URIs are always content://. ConcatEngine.join maps each through FFmpegKitConfig.getSafParameterForRead, which yields an ffkitsaf: path, and FFmpegConcatCommand.listFileContents writes those into the list file.

FFmpeg's concat demuxer applies its own protocol whitelist, defaulting to file,crypto,data. ffkitsaf is not on it, so the demuxer refuses every entry.

FFmpegConcatCommand.build passes -safe 0 — which permits absolute paths — and nothing that permits the protocol:

add("-f"); add("concat")
add("-safe"); add("0")
add("-i"); add(listFile.absolutePath)

Why nothing caught it

Two reasons, and they compound:

  • Only the stream-copy strategy uses the list file. ConcatStrategy.REENCODE passes each input with its own -i, where the demuxer whitelist does not apply — so the re-encode path works over SAF and only the stream-copy path is broken.
  • Every existing join test passes Uri.fromFile(...), which takes ConcatEngine's uri.path arm instead of the bridge. ConcatEngineTest.matchingClipsAreJoinedByStreamCopy therefore exercises stream copy with a file: path and passes.

So the one combination that is broken — stream copy and a content URI — is exactly the combination no test produced, while being the default for real users.

Severity

Stream copy is chosen whenever the inputs match, which is the common case and the one the feature is sold on. For those users Join fails outright with a message naming an internal protocol.

The fix

Add ffkitsaf to the demuxer's whitelist alongside the defaults, on the stream-copy branch:

-f concat -safe 0 -protocol_whitelist file,crypto,data,ffkitsaf -i <list>

It only widens the default set. A JVM test in FFmpegConcatCommandTest can pin the flag's presence; the device test above is what proves it works end to end.

_Found by the first test ever to hand the join path a real `content://` input (#225). This is a production defect, not a test problem._ **Joining files picked through the system picker fails whenever the strategy is stream copy** — which is the matched-files case the UI advertises as *"Files matched, so they were joined without re-encoding — no quality loss."* ## The failure ``` FFmpegException: Joining failed (1): [ffkitsaf @ 0x7c6fc348c980] Protocol 'ffkitsaf' not on whitelist 'file,crypto,data'! [in#0] Error opening input: Invalid argument Error opening input file /data/.../cache/joined_from_content.concat_list.txt ``` Reproduced on a local API 34 emulator by `ContentUriInputTest.contentUriInputsJoinThroughTheSafBridge`, joining two matching clips served over `content://`. ## Why every real user hits it `JoinScreen` picks with `ActivityResultContracts.OpenMultipleDocuments`, so the URIs are **always** `content://`. `ConcatEngine.join` maps each through `FFmpegKitConfig.getSafParameterForRead`, which yields an `ffkitsaf:` path, and `FFmpegConcatCommand.listFileContents` writes those into the list file. FFmpeg's `concat` demuxer applies its own protocol whitelist, defaulting to `file,crypto,data`. `ffkitsaf` is not on it, so the demuxer refuses every entry. `FFmpegConcatCommand.build` passes `-safe 0` — which permits *absolute paths* — and nothing that permits the *protocol*: ```kotlin add("-f"); add("concat") add("-safe"); add("0") add("-i"); add(listFile.absolutePath) ``` ## Why nothing caught it Two reasons, and they compound: - **Only the stream-copy strategy uses the list file.** `ConcatStrategy.REENCODE` passes each input with its own `-i`, where the demuxer whitelist does not apply — so the re-encode path works over SAF and only the stream-copy path is broken. - **Every existing join test passes `Uri.fromFile(...)`**, which takes `ConcatEngine`'s `uri.path` arm instead of the bridge. `ConcatEngineTest.matchingClipsAreJoinedByStreamCopy` therefore exercises stream copy with a `file:` path and passes. So the one combination that is broken — stream copy **and** a content URI — is exactly the combination no test produced, while being the default for real users. ## Severity Stream copy is chosen whenever the inputs match, which is the common case and the one the feature is sold on. For those users Join fails outright with a message naming an internal protocol. ## The fix Add `ffkitsaf` to the demuxer's whitelist alongside the defaults, on the stream-copy branch: ``` -f concat -safe 0 -protocol_whitelist file,crypto,data,ffkitsaf -i <list> ``` It only widens the default set. A JVM test in `FFmpegConcatCommandTest` can pin the flag's presence; the device test above is what proves it works end to end.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: JMR-dev/LibreMediaConverter#238