diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 01cfb59..79d74e4 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -106,6 +106,11 @@ jobs:
if: steps.jars.outputs.cache-hit != 'true'
run: ./src/FFmpegKit.Android/Jars/FetchJars.sh "${{ steps.native.outputs.version }}"
+ # Runs on cache hits too: the fetch already verifies fresh downloads, but a restored
+ # cache is only as trustworthy as its last write. Seconds of hashing per build.
+ - name: Verify FFmpegKit binaries
+ run: ./src/FFmpegKit.Android/Jars/FetchJars.sh --verify "${{ steps.native.outputs.version }}"
+
- name: Pack all variants
run: ./src/FFmpegKit.Android/BuildNugets.sh "${{ inputs.version }}" "${{ steps.native.outputs.version }}"
diff --git a/Directory.Build.props b/Directory.Build.props
index 3d5683a..bd0a72f 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -16,6 +16,13 @@
8.1.7$(FFmpegVersion)
+
+ 0.2.1
+
Video
diff --git a/README.md b/README.md
index 31fac9f..f8aa191 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,9 @@
[](https://www.nuget.org/packages/FFmpegKit.Net.Video.Android)
[](https://github.com/sbokatuk/FFmpegKit.Android/actions/workflows/release.yml)
-[](#packages)
+[](#installation)
[](#about)
-[](#licence)
+[](#license)
.NET for Android and .NET MAUI bindings for the native **FFmpegKit** library.
@@ -34,6 +34,8 @@ So the `.aar` files come from the community fork, via Maven Central — see [`Fe
The version is set by `FFmpegKitNativeVersion` in [`Directory.Build.props`](Directory.Build.props), which `FetchJars.sh` reads, so the download and the `.aar` the project expects cannot drift apart.
+Every download is verified against a SHA-256 baseline committed at [`build/checksums/`](build/checksums) — one file per FFmpegKit line, recorded once with [`build/update-checksums.sh`](build/update-checksums.sh) (from Maven Central's own `.sha256` sidecars where published) and enforced on every fetch, locally and in CI. A corrupted or substituted artifact fails the fetch instead of ending up inside a shipped package.
+
The fork currently publishes three lines, each with all eight variants:
| FFmpegKit | Bundled FFmpeg | ABIs | minSdk |
@@ -46,6 +48,8 @@ The fork currently publishes three lines, each with all eight variants:
None of them ship 32-bit binaries, so dropping back to an older line does not restore `armeabi-v7a` or `x86` support.
+Both constraints are surfaced at build time in consuming apps by a small `.targets` file every package carries: **`FFMPEGKIT001`** warns when `SupportedOSPlatformVersion` is below 24, **`FFMPEGKIT002`** when the build includes 32-bit runtime identifiers (`android-arm`, `android-x86`) — each otherwise only fails on a device, when `libffmpegkit.so` does not load. Both are ordinary MSBuild warnings, suppressible per project via `NoWarn` for apps that gate FFmpegKit usage at runtime.
+
### Releasing an older line
Tags name the FFmpeg version: **`v6.1.6.4` publishes `6.1.6.4`** and builds against the FFmpegKit release that packages FFmpeg 6.1.6, looked up in [`build/native-versions.tsv`](build/native-versions.tsv). The fourth component is the binding revision and any prerelease suffix is ignored when resolving (`v8.1.2.4-rc.1` → FFmpeg `8.1.2`). A tag naming a version that is not in the mapping fails the release with the list of known builds, so an old-style tag like `v8.1.7.4` cannot be published by mistake. No branch or `Directory.Build.props` edit is needed.
@@ -148,6 +152,12 @@ Execute your FFmpeg command
FFmpegKit.Execute("-i input.mov -c:v libx264 output.mp4");
```
+> If your app's own root namespace starts with `FFmpegKit` (say, `FFmpegKit.MyApp`), the bare
+> `FFmpegKit` above resolves to your namespace instead of the class and fails to compile — add
+> `using FFmpeg = Ffmpegkit.Droid.FFmpegKit;` and call `FFmpeg.Execute(...)`, as the sample and
+> the device tests do. The [migration notes](#installation) explain why the namespace is not
+> `FFmpegKit.*` itself.
+
More examples and usage can be found in the [original FFmpegKit wiki](https://github.com/arthenica/ffmpeg-kit/wiki/Android). That repository is archived, but the Java API it documents is the one these bindings expose, so it remains the reference.
### Beyond the generated binding
@@ -195,6 +205,8 @@ stream.IsVideo / IsAudio
FFmpegKitConfig.EnableLogCallback(log => Debug.WriteLine(log.Message));
```
+Clear either hook by name — `FFmpegKitConfig.DisableLogCallback()` / `DisableStatisticsCallback()` — instead of the `EnableLogCallback((ILogCallback)null)` incantation. The delegates are held by FFmpegKit until cleared, so anything they capture stays alive too.
+
**Use managed enums.** `SessionState` and `Level` are Java enums, so they cannot be used in a `switch`, and comparing them with `==` compares managed peer references rather than the underlying constants. `ToManaged()` converts at the boundary.
### Working with user-picked files
@@ -239,7 +251,7 @@ Python 3 is also needed, for the package merge step.
./src/FFmpegKit.Android/BuildNugets.sh 8.1.2.4-rc.1 # ...or with an explicit package version
```
-`FetchJars.sh` reads the FFmpegKit version from `FFmpegKitNativeVersion` in `Directory.Build.props`, the same property the `.csproj` uses to pick the `.aar`, so the two cannot drift apart. Pass a version to override it (`./FetchJars.sh 8.2.0`) when trying a newer upstream build before committing to it.
+`FetchJars.sh` reads the FFmpegKit version from `FFmpegKitNativeVersion` in `Directory.Build.props`, the same property the `.csproj` uses to pick the `.aar`, so the two cannot drift apart. Pass a version to override it (`./FetchJars.sh 8.2.0`) when trying a newer upstream build before committing to it — record its checksum baseline first with `./build/update-checksums.sh 8.2.0`, since the fetch refuses to run against a line that has no committed baseline. `./FetchJars.sh --verify` re-checks the files already on disk without downloading anything.
### A single variant
diff --git a/build/checksums/6.0.3.sha256 b/build/checksums/6.0.3.sha256
new file mode 100644
index 0000000..fe0648b
--- /dev/null
+++ b/build/checksums/6.0.3.sha256
@@ -0,0 +1,10 @@
+c6df4ed69ab9e4717fc487afe86223033cb56ec81e492e45c9258a7e647f6426 ffmpeg-kit-audio-6.0.3.aar
+21240a3e9c4004fba6fa71adcab0da8fd70be1e2407ff1127dbea30c745ddd7d ffmpeg-kit-full-6.0.3.aar
+80b069ac31afe428f50f8f6332aef88ced20afc4d03daf78582e62a327ab8e7b ffmpeg-kit-full-gpl-6.0.3.aar
+c77da985b4ae175fefbd6ea3fd2a7048081f0e4859b9649e9707d34b32ae28f9 ffmpeg-kit-https-6.0.3.aar
+3022679a5911860343bd2070203660bad30c56ab5ab670ce72e7cb44cd3fd0c6 ffmpeg-kit-https-gpl-6.0.3.aar
+0cd6912de6ab35b958847bfd6ffa0b214ec1d11cd5020e4244e058f6761c8c55 ffmpeg-kit-min-6.0.3.aar
+f23d29e3f70eb1d59721ba16e57c01d9bc50a45f19e16a027a2a719a62cde0fc ffmpeg-kit-min-gpl-6.0.3.aar
+9522f17d5323e9f6333c85b3717e6777d1f9c205c29932c19ecc62bc3d3e16d0 ffmpeg-kit-video-6.0.3.aar
+1cad0fb4dfa01755a014331b5ed199281d2c3fab5aca5c9d7abd0b41d0ec3f7b smart-exception-common-0.2.1.jar
+5b96aaa5f191dedbef72fb0c38f1a2b01807920afc0d92a75a2acd6e0cc7703c smart-exception-java-0.2.1.jar
diff --git a/build/checksums/7.1.6.sha256 b/build/checksums/7.1.6.sha256
new file mode 100644
index 0000000..f90f4a5
--- /dev/null
+++ b/build/checksums/7.1.6.sha256
@@ -0,0 +1,10 @@
+870d2c110b05a280bf28c430e92650fa82e00bb77de5d5a85bc7e94c9d1ebad6 ffmpeg-kit-audio-7.1.6.aar
+149fe79f8009d0b849818ccf370d9c4637a1198bbef4d5b2e63204dab5e2401d ffmpeg-kit-full-7.1.6.aar
+0aa3e5f568274950a5524f336d9c141c539bc07a296ba5ec7fd42f098cd3a244 ffmpeg-kit-full-gpl-7.1.6.aar
+e80e5ee52248a3dcb1dda0524dd784fe4bbb212dcd4d575e82e15d21a002d860 ffmpeg-kit-https-7.1.6.aar
+afa1e52f817cc8d555f9ec7d6f4611347bcf686027b7a4f0325613759c8f4f92 ffmpeg-kit-https-gpl-7.1.6.aar
+f6ce02f57dadd8e6ccc407f0ed0c916567714c0db8b170eda33d9b6654ac14e1 ffmpeg-kit-min-7.1.6.aar
+fcf7fb115c7c930a907cc173441a445e0bae025e914579002ee9cc0245b3b773 ffmpeg-kit-min-gpl-7.1.6.aar
+c2e53d6c33e195a663152b4f135a3bae96d6bf0232ef62a532eadb827f9dc803 ffmpeg-kit-video-7.1.6.aar
+1cad0fb4dfa01755a014331b5ed199281d2c3fab5aca5c9d7abd0b41d0ec3f7b smart-exception-common-0.2.1.jar
+5b96aaa5f191dedbef72fb0c38f1a2b01807920afc0d92a75a2acd6e0cc7703c smart-exception-java-0.2.1.jar
diff --git a/build/checksums/8.1.7.sha256 b/build/checksums/8.1.7.sha256
new file mode 100644
index 0000000..4366356
--- /dev/null
+++ b/build/checksums/8.1.7.sha256
@@ -0,0 +1,10 @@
+9b277de25934594635a495e4fda164b902309219a2f538827f5ecc7ed77229d6 ffmpeg-kit-audio-8.1.7.aar
+c3cbc81d498175fd2aa69ee2dfe7dafbf519052a96283c2568fe5b3b16618456 ffmpeg-kit-full-8.1.7.aar
+374d3734755fd4a4f2241da23b93ab0b3bcae9bd39f01cf3bf29a5731914658b ffmpeg-kit-full-gpl-8.1.7.aar
+07d321247016c6a46cc81ad2c1cfb58c2503d575e6d3daebe310ff7efa538630 ffmpeg-kit-https-8.1.7.aar
+48425b8deb0b885fe2f6bfdb4fc027029e072789d181935d60b538d03b5fa1b2 ffmpeg-kit-https-gpl-8.1.7.aar
+591ddcf3698028e838b854dc775e60aedaf43fffd59689cb59b8af632aba3d76 ffmpeg-kit-min-8.1.7.aar
+3698fe62678cbc6c7d3c2b124ce28f4f064d0f18e1ecb414a513ed9909eee73e ffmpeg-kit-min-gpl-8.1.7.aar
+d37050bc77f3a2dac30c27a0ce464d499cabbb71634fb71704aa447f39db9309 ffmpeg-kit-video-8.1.7.aar
+1cad0fb4dfa01755a014331b5ed199281d2c3fab5aca5c9d7abd0b41d0ec3f7b smart-exception-common-0.2.1.jar
+5b96aaa5f191dedbef72fb0c38f1a2b01807920afc0d92a75a2acd6e0cc7703c smart-exception-java-0.2.1.jar
diff --git a/build/update-checksums.sh b/build/update-checksums.sh
new file mode 100755
index 0000000..0dbc561
--- /dev/null
+++ b/build/update-checksums.sh
@@ -0,0 +1,104 @@
+#!/bin/sh
+
+set -e
+
+# Records the SHA-256 baseline that Jars/FetchJars.sh verifies every download against,
+# one file per FFmpegKit line: build/checksums/.sha256.
+#
+# For the .aar files the digest is taken from Maven Central's .sha256 sidecar when one is
+# published, so the recorded value comes from repository metadata rather than whatever this
+# machine happened to download. When no sidecar exists the artifact itself is downloaded and
+# hashed - trust-on-first-use, which still pins every later fetch to today's bytes. The
+# smart-exception jars come from GitHub releases, which publish no digests, so they are
+# always download-and-hash.
+#
+# Run this once when adopting a new upstream line (a new native-versions.tsv row), review the
+# diff, and commit the result. It is deliberately not run by CI: the whole point is that CI
+# verifies against a baseline a human recorded.
+#
+# Usage:
+# build/update-checksums.sh # version from Directory.Build.props
+# build/update-checksums.sh 7.1.6 # an older line, e.g. when adding a native-versions.tsv row
+
+cd "$(dirname "$0")/.."
+
+PROPS="Directory.Build.props"
+MAVEN_BASE="https://repo1.maven.org/maven2/dev/ffmpegkit-maintained"
+
+read_property() {
+ sed -n "s:.*<$1>\(.*\)$1>.*:\1:p" "$PROPS" | head -1
+}
+
+require_version() {
+ if [ -z "$2" ]; then
+ echo "error: could not read $1 from $PROPS" >&2
+ exit 1
+ fi
+ case "$2" in
+ *[!A-Za-z0-9._-]*)
+ echo "error: invalid $1 '$2'" >&2
+ exit 1
+ ;;
+ esac
+}
+
+FFMPEG_KIT_VERSION="$1"
+if [ -z "$FFMPEG_KIT_VERSION" ]; then
+ FFMPEG_KIT_VERSION=$(read_property FFmpegKitNativeVersion)
+fi
+require_version FFmpegKitNativeVersion "$FFMPEG_KIT_VERSION"
+
+SMART_EXCEPTION_VERSION=$(read_property SmartExceptionVersion)
+require_version SmartExceptionVersion "$SMART_EXCEPTION_VERSION"
+
+OUT="build/checksums/$FFMPEG_KIT_VERSION.sha256"
+mkdir -p build/checksums
+
+STAGING=$(mktemp -d)
+trap 'rm -rf "$STAGING"' EXIT
+
+sha256_of() {
+ if command -v shasum >/dev/null 2>&1; then
+ shasum -a 256 "$1" | awk '{print $1}'
+ else
+ sha256sum "$1" | awk '{print $1}'
+ fi
+}
+
+# $1 = url, $2 = file name. Prints the digest.
+digest_of() {
+ # A sidecar is a few dozen bytes; only fall back to the full artifact when it is absent
+ # or malformed.
+ sidecar=$(curl -fsL --retry 3 --retry-delay 2 "$1.sha256" 2>/dev/null | awk '{print $1}' | tr '[:upper:]' '[:lower:]') || sidecar=""
+ case "$sidecar" in
+ *[!0-9a-f]* | "") ;;
+ *)
+ if [ ${#sidecar} -eq 64 ]; then
+ echo "$sidecar"
+ return
+ fi
+ ;;
+ esac
+
+ curl -fL --retry 3 --retry-delay 2 -o "$STAGING/$2" "$1" >&2
+ sha256_of "$STAGING/$2"
+}
+
+: > "$STAGING/baseline"
+
+for artifact in common java; do
+ name="smart-exception-$artifact-$SMART_EXCEPTION_VERSION.jar"
+ url="https://github.com/tanersener/smart-exception/releases/download/v$SMART_EXCEPTION_VERSION/$name"
+ printf '%s %s\n' "$(digest_of "$url" "$name")" "$name" >> "$STAGING/baseline"
+ echo "==> $name"
+done
+
+for variant in audio full full-gpl https https-gpl min min-gpl video; do
+ name="ffmpeg-kit-$variant-$FFMPEG_KIT_VERSION.aar"
+ url="$MAVEN_BASE/ffmpeg-kit-$variant/$FFMPEG_KIT_VERSION/$name"
+ printf '%s %s\n' "$(digest_of "$url" "$name")" "$name" >> "$STAGING/baseline"
+ echo "==> $name"
+done
+
+sort -k2 "$STAGING/baseline" > "$OUT"
+echo "==> wrote $OUT"
diff --git a/docs/release-notes/8.1.2.5.md b/docs/release-notes/8.1.2.5.md
new file mode 100644
index 0000000..7cd04d3
--- /dev/null
+++ b/docs/release-notes/8.1.2.5.md
@@ -0,0 +1,23 @@
+## What's changed
+
+Binding revision 5 of FFmpeg `8.1.2` (FFmpegKit `8.1.7`). The native binaries are unchanged; everything below is binding, packaging and pipeline work.
+
+### Build-time constraint checks
+
+Every package now ships a small `build`/`buildTransitive` `.targets` file imported into consuming **apps**:
+
+- **`FFMPEGKIT001`** warns when `SupportedOSPlatformVersion` is below 24 — ahead of the manifest merger's java-worded `AMM0000` error, and still there if that error is silenced with `tools:overrideLibrary`.
+- **`FFMPEGKIT002`** warns when the build includes 32-bit runtime identifiers (`android-arm`, `android-x86`). There are no 32-bit native libraries in any variant or version; without this warning the failure is `libffmpegkit.so` not loading on a 32-bit device.
+
+Both are ordinary MSBuild warnings with their own codes, so either can be suppressed per project via `NoWarn` by apps that gate FFmpegKit usage at runtime. Library projects are not affected.
+
+### API
+
+- The hand-written additions — async wrappers, typed media information, lambda callbacks, SAF helpers — now carry **nullable reference annotations** (`#nullable enable` per file). The generated binding surface remains unannotated, faithfully to the Java API.
+- **`FFmpegKitConfig.DisableLogCallback()`** and **`DisableStatisticsCallback()`** replace the `EnableLogCallback((ILogCallback)null!)` incantation as the way to clear the global callbacks.
+
+### Supply chain
+
+- Every native download is now **verified against a SHA-256 baseline** committed at `build/checksums/`, one file per FFmpegKit line, recorded from Maven Central's own `.sha256` sidecars where published (`build/update-checksums.sh`). A corrupted or substituted artifact fails the fetch — locally and in CI, which also re-verifies restored caches.
+- The `smart-exception` runtime jars' version now lives once in `Directory.Build.props`; the fetch script and the project file both read it, so the pairing can no longer drift and fail only on device.
+- Downloads retry on transient network errors.
diff --git a/samples/FFmpegKit.Android.Example/MainPage.xaml b/samples/FFmpegKit.Android.Example/MainPage.xaml
index f1dd9e0..063ccad 100644
--- a/samples/FFmpegKit.Android.Example/MainPage.xaml
+++ b/samples/FFmpegKit.Android.Example/MainPage.xaml
@@ -27,6 +27,13 @@
Clicked="OnConvertClicked"
HorizontalOptions="Fill" />
+
+
marshals back to the thread that created it - the UI thread here -
@@ -93,7 +97,8 @@ async void OnConvertClicked(object sender, EventArgs e)
: $"{p.Position:mm\\:ss} · {p.Speed:0.#}x";
});
- var (success, message, outputPath) = await RunConversionAsync(option, inputPath, progress, _sourceDuration);
+ var (success, message, outputPath) =
+ await RunConversionAsync(option, inputPath, progress, _sourceDuration, _cancellation.Token);
StatusLabel.Text = message;
SemanticScreenReader.Announce(message);
@@ -106,6 +111,9 @@ async void OnConvertClicked(object sender, EventArgs e)
}
finally
{
+ _cancellation.Dispose();
+ _cancellation = null;
+ CancelBtn.IsEnabled = false;
Spinner.IsRunning = false;
Spinner.IsVisible = false;
ConversionProgress.IsVisible = false;
@@ -114,11 +122,20 @@ async void OnConvertClicked(object sender, EventArgs e)
}
}
+ void OnCancelClicked(object sender, EventArgs e)
+ {
+ // Cancellation is co-operative: FFmpeg stops as soon as it notices, and the awaited
+ // session then completes with a cancelled return code rather than throwing.
+ _cancellation?.Cancel();
+ StatusLabel.Text = "Cancelling...";
+ }
+
static async Task<(bool Success, string Message, string OutputPath)> RunConversionAsync(
ConversionOption option,
string inputPath,
IProgress progress,
- TimeSpan? sourceDuration)
+ TimeSpan? sourceDuration,
+ CancellationToken cancellationToken)
{
var outputPath = Path.Combine(FileSystem.CacheDirectory, option.OutputFileName);
@@ -130,7 +147,7 @@ async void OnConvertClicked(object sender, EventArgs e)
// Awaited directly: no Task.Run, because ExecuteAsync hands the work to FFmpegKit's own
// executor rather than blocking a thread pool thread for the length of the transcode.
// The duration is what lets FFmpegKit report a percentage rather than just a position.
- var session = await FFmpeg.ExecuteAsync(command, progress, sourceDuration);
+ var session = await FFmpeg.ExecuteAsync(command, progress, sourceDuration, cancellationToken);
if (session.Succeeded())
{
@@ -138,6 +155,9 @@ async void OnConvertClicked(object sender, EventArgs e)
return (true, $"Success! Converted video written to:\n{outputPath}\n({outputSize:N0} bytes)", outputPath);
}
+ if (session.ReturnCode is { IsValueCancel: true })
+ return (false, "Conversion cancelled.", outputPath);
+
var logs = session.FailStackTrace ?? session.AllLogsAsString;
return (false, $"Conversion failed (return code {session.ReturnCode}).\n{logs}", outputPath);
}
diff --git a/src/FFmpegKit.Android/Additions/Callbacks.cs b/src/FFmpegKit.Android/Additions/Callbacks.cs
index 0d24aec..f0060f0 100644
--- a/src/FFmpegKit.Android/Additions/Callbacks.cs
+++ b/src/FFmpegKit.Android/Additions/Callbacks.cs
@@ -1,5 +1,7 @@
using System;
+#nullable enable
+
namespace Ffmpegkit.Droid
{
// Adapters adapting a delegate to FFmpegKit's single-method Java callback interfaces.
diff --git a/src/FFmpegKit.Android/Additions/Enums.cs b/src/FFmpegKit.Android/Additions/Enums.cs
index 7e51ea5..4e33364 100644
--- a/src/FFmpegKit.Android/Additions/Enums.cs
+++ b/src/FFmpegKit.Android/Additions/Enums.cs
@@ -1,5 +1,7 @@
using System;
+#nullable enable
+
namespace Ffmpegkit.Droid
{
/// Managed counterpart of the Java SessionState enum.
diff --git a/src/FFmpegKit.Android/Additions/FFmpegKit.Async.cs b/src/FFmpegKit.Android/Additions/FFmpegKit.Async.cs
index fcffe4e..b49accb 100644
--- a/src/FFmpegKit.Android/Additions/FFmpegKit.Async.cs
+++ b/src/FFmpegKit.Android/Additions/FFmpegKit.Async.cs
@@ -2,6 +2,8 @@
using System.Threading;
using System.Threading.Tasks;
+#nullable enable
+
namespace Ffmpegkit.Droid
{
public partial class FFmpegKit
diff --git a/src/FFmpegKit.Android/Additions/FFmpegKitConfig.Callbacks.cs b/src/FFmpegKit.Android/Additions/FFmpegKitConfig.Callbacks.cs
index 0a226b3..45de192 100644
--- a/src/FFmpegKit.Android/Additions/FFmpegKitConfig.Callbacks.cs
+++ b/src/FFmpegKit.Android/Additions/FFmpegKitConfig.Callbacks.cs
@@ -1,5 +1,10 @@
using System;
+// The generated binding compiles with Nullable disabled and carries no annotations; these
+// hand-written additions are annotation-correct, so nullable-enabled consumers get real
+// nullability information for at least the surface they touch most.
+#nullable enable
+
namespace Ffmpegkit.Droid
{
public partial class FFmpegKitConfig
@@ -11,8 +16,8 @@ public partial class FFmpegKitConfig
/// an FFmpegKit worker thread — marshal to the UI thread before touching UI.
///
/// The delegate is held by FFmpegKit until it is replaced, so anything it captures stays
- /// alive too — avoid capturing an Activity. There is no Disable method: clear it by
- /// calling EnableLogCallback((ILogCallback)null).
+ /// alive too — avoid capturing an Activity. Clear it with
+ /// .
///
///
public static void EnableLogCallback (Action logCallback)
@@ -36,5 +41,17 @@ public static void EnableStatisticsCallback (Action statisticsCallba
EnableStatisticsCallback (new ActionStatisticsCallback (statisticsCallback));
}
+
+ /// Stops routing FFmpeg log lines to a previously enabled callback.
+ ///
+ /// The delegate overload above deliberately rejects null; the native way to clear the
+ /// hook is passing a null interface, which nullable-enabled callers could only write as
+ /// EnableLogCallback ((ILogCallback) null!). This says the same thing by name.
+ ///
+ public static void DisableLogCallback () => EnableLogCallback ((ILogCallback) null!);
+
+ /// Stops routing FFmpeg statistics samples to a previously enabled callback.
+ /// See .
+ public static void DisableStatisticsCallback () => EnableStatisticsCallback ((IStatisticsCallback) null!);
}
}
diff --git a/src/FFmpegKit.Android/Additions/FFmpegKitConfig.Saf.cs b/src/FFmpegKit.Android/Additions/FFmpegKitConfig.Saf.cs
index df8c5bc..e2ab5c1 100644
--- a/src/FFmpegKit.Android/Additions/FFmpegKitConfig.Saf.cs
+++ b/src/FFmpegKit.Android/Additions/FFmpegKitConfig.Saf.cs
@@ -1,5 +1,7 @@
using System;
+#nullable enable
+
namespace Ffmpegkit.Droid
{
public partial class FFmpegKitConfig
diff --git a/src/FFmpegKit.Android/Additions/FFmpegProgress.cs b/src/FFmpegKit.Android/Additions/FFmpegProgress.cs
index ab4a0a1..c1f168e 100644
--- a/src/FFmpegKit.Android/Additions/FFmpegProgress.cs
+++ b/src/FFmpegKit.Android/Additions/FFmpegProgress.cs
@@ -1,5 +1,7 @@
using System;
+#nullable enable
+
namespace Ffmpegkit.Droid
{
///
diff --git a/src/FFmpegKit.Android/Additions/FFprobeKit.Async.cs b/src/FFmpegKit.Android/Additions/FFprobeKit.Async.cs
index 6033360..efdf85c 100644
--- a/src/FFmpegKit.Android/Additions/FFprobeKit.Async.cs
+++ b/src/FFmpegKit.Android/Additions/FFprobeKit.Async.cs
@@ -1,6 +1,8 @@
using System;
using System.Threading.Tasks;
+#nullable enable
+
namespace Ffmpegkit.Droid
{
public partial class FFprobeKit
diff --git a/src/FFmpegKit.Android/Additions/MediaInformation.Typed.cs b/src/FFmpegKit.Android/Additions/MediaInformation.Typed.cs
index 0083226..9ead18f 100644
--- a/src/FFmpegKit.Android/Additions/MediaInformation.Typed.cs
+++ b/src/FFmpegKit.Android/Additions/MediaInformation.Typed.cs
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
+#nullable enable
+
namespace Ffmpegkit.Droid
{
public partial class MediaInformation
diff --git a/src/FFmpegKit.Android/Additions/MediaValues.cs b/src/FFmpegKit.Android/Additions/MediaValues.cs
index 9e6b084..fc9ec0d 100644
--- a/src/FFmpegKit.Android/Additions/MediaValues.cs
+++ b/src/FFmpegKit.Android/Additions/MediaValues.cs
@@ -2,6 +2,8 @@
using System.Collections.Generic;
using System.Globalization;
+#nullable enable
+
namespace Ffmpegkit.Droid
{
///
@@ -17,23 +19,23 @@ namespace Ffmpegkit.Droid
///
internal static class MediaValues
{
- internal static double? Seconds (string value) => Number (value);
+ internal static double? Seconds (string? value) => Number (value);
- internal static long? Integer (string value) =>
+ internal static long? Integer (string? value) =>
long.TryParse (value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)
? parsed
: null;
- internal static double? Number (string value) =>
+ internal static double? Number (string? value) =>
double.TryParse (value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)
? parsed
: null;
- internal static int? Int32 (Java.Lang.Long value) =>
+ internal static int? Int32 (Java.Lang.Long? value) =>
value is null ? null : checked ((int)value.LongValue ());
/// Parses a rational such as "30/1" or "30000/1001", as FFprobe reports rates.
- internal static double? Rational (string value)
+ internal static double? Rational (string? value)
{
if (string.IsNullOrWhiteSpace (value))
return null;
@@ -51,7 +53,7 @@ internal static class MediaValues
}
/// Flattens a JSON object into string values, so callers need no Java JSON type.
- internal static IReadOnlyDictionary ToDictionary (Org.Json.JSONObject json)
+ internal static IReadOnlyDictionary ToDictionary (Org.Json.JSONObject? json)
{
var values = new Dictionary (StringComparer.OrdinalIgnoreCase);
@@ -67,7 +69,9 @@ internal static IReadOnlyDictionary ToDictionary (Org.Json.JSONO
if (string.IsNullOrEmpty (key))
continue;
- var value = json.OptString (key, null);
+ // Mono.Android annotates the fallback as non-nullable, but OptString handles null
+ // fine and it is what distinguishes "absent" from a legitimately empty value.
+ var value = json.OptString (key, null!);
if (value is not null)
values[key] = value;
diff --git a/src/FFmpegKit.Android/FFmpegKit.Android.csproj b/src/FFmpegKit.Android/FFmpegKit.Android.csproj
index 92b43ad..bd3eaaa 100644
--- a/src/FFmpegKit.Android/FFmpegKit.Android.csproj
+++ b/src/FFmpegKit.Android/FFmpegKit.Android.csproj
@@ -92,8 +92,8 @@
embedded here or every FFmpeg call throws NoClassDefFoundError on device. Bind="false":
consumers only need the classes at runtime, not a C# binding for them.
-->
-
-
+
+
@@ -117,6 +117,15 @@
+
+
+
+
diff --git a/src/FFmpegKit.Android/FFmpegKit.Net.Android.targets b/src/FFmpegKit.Android/FFmpegKit.Net.Android.targets
new file mode 100644
index 0000000..7a9a294
--- /dev/null
+++ b/src/FFmpegKit.Android/FFmpegKit.Net.Android.targets
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+ <_FFmpegKitNetAndroidRidRaw Include="$(RuntimeIdentifiers)" />
+
+
+
+
+
+
+
+ <_FFmpegKitNetAndroid32BitRid Include="@(_FFmpegKitNetAndroidRid)"
+ Condition=" '%(Identity)' == 'android-arm' or '%(Identity)' == 'android-x86' " />
+
+
+
+
+
+
+
diff --git a/src/FFmpegKit.Android/Jars/FetchJars.sh b/src/FFmpegKit.Android/Jars/FetchJars.sh
index 5b4fd74..b1e25e6 100755
--- a/src/FFmpegKit.Android/Jars/FetchJars.sh
+++ b/src/FFmpegKit.Android/Jars/FetchJars.sh
@@ -9,43 +9,127 @@ set -e
# (no prebuilt binaries since v6.1.0). The .aar files below instead come from the
# community-maintained Maven Central mirror at dev.ffmpegkit-maintained.
#
+# Every downloaded file is verified against the SHA-256 baseline committed at
+# build/checksums/.sha256. These are tens of megabytes of native code
+# that get linked into consumers' apps: a corrupted or substituted artifact must fail
+# here, not at packaging time or - worse - silently on devices. Baselines are recorded
+# by build/update-checksums.sh when a new upstream line is adopted.
+#
# Usage:
-# ./FetchJars.sh # version from Directory.Build.props
-# ./FetchJars.sh 8.2.0 # override, e.g. to try a newer upstream build before committing to it
+# ./FetchJars.sh # version from Directory.Build.props
+# ./FetchJars.sh 8.2.0 # override, e.g. to try a newer upstream build
+# ./FetchJars.sh --verify # no downloads: check the files already present here
+# ./FetchJars.sh --verify 8.2.0
cd "$(dirname "$0")"
ROOT="$(cd ../../.. && pwd)"
PROPS="$ROOT/Directory.Build.props"
MAVEN_BASE="https://repo1.maven.org/maven2/dev/ffmpegkit-maintained"
-SMART_EXCEPTION_VERSION="0.2.1"
+
+VERIFY_ONLY=0
+FFMPEG_KIT_VERSION=""
+for arg in "$@"; do
+ case "$arg" in
+ --verify) VERIFY_ONLY=1 ;;
+ -*) echo "error: unknown option '$arg'" >&2; exit 1 ;;
+ *) FFMPEG_KIT_VERSION="$arg" ;;
+ esac
+done
+
+read_property() {
+ sed -n "s:.*<$1>\(.*\)$1>.*:\1:p" "$PROPS" | head -1
+}
+
+# Versions are interpolated into URLs and file names, so reject anything exotic up front.
+require_version() {
+ if [ -z "$2" ]; then
+ echo "error: could not read $1 from $PROPS" >&2
+ exit 1
+ fi
+ case "$2" in
+ *[!A-Za-z0-9._-]*)
+ echo "error: invalid $1 '$2'" >&2
+ exit 1
+ ;;
+ esac
+}
# Read the version from Directory.Build.props rather than repeating it here: the .aar file names
# are baked into the .csproj via FFmpegKitNativeVersion, so a second copy that drifts out of sync
# fails the build with a confusing "file not found" on an .aar nobody downloaded.
-FFMPEG_KIT_VERSION="$1"
if [ -z "$FFMPEG_KIT_VERSION" ]; then
- FFMPEG_KIT_VERSION=$(sed -n 's:.*\(.*\).*:\1:p' "$PROPS" | head -1)
+ FFMPEG_KIT_VERSION=$(read_property FFmpegKitNativeVersion)
fi
+require_version FFmpegKitNativeVersion "$FFMPEG_KIT_VERSION"
-if [ -z "$FFMPEG_KIT_VERSION" ]; then
- echo "error: could not read FFmpegKitNativeVersion from $PROPS" >&2
+# The smart-exception version lives in Directory.Build.props too: the .csproj embeds the jars
+# via the same property, so there is exactly one place to bump and nothing to drift. A wrong
+# pairing here fails only on device, with NoClassDefFoundError.
+SMART_EXCEPTION_VERSION=$(read_property SmartExceptionVersion)
+require_version SmartExceptionVersion "$SMART_EXCEPTION_VERSION"
+
+CHECKSUMS="$ROOT/build/checksums/$FFMPEG_KIT_VERSION.sha256"
+if [ ! -f "$CHECKSUMS" ]; then
+ echo "error: no checksum baseline for FFmpegKit $FFMPEG_KIT_VERSION" >&2
+ echo " expected $CHECKSUMS" >&2
+ echo " run build/update-checksums.sh $FFMPEG_KIT_VERSION to record one" >&2
exit 1
fi
-# The version is interpolated into URLs and file names, so reject anything exotic up front.
-case "$FFMPEG_KIT_VERSION" in
- *[!A-Za-z0-9._-]*)
- echo "error: invalid version '$FFMPEG_KIT_VERSION'" >&2
+sha256_of() {
+ if command -v shasum >/dev/null 2>&1; then
+ shasum -a 256 "$1" | awk '{print $1}'
+ else
+ sha256sum "$1" | awk '{print $1}'
+ fi
+}
+
+# $1 = path to check, $2 = file name as recorded in the baseline
+verify_file() {
+ want=$(awk -v f="$2" '$2 == f { print $1; exit }' "$CHECKSUMS")
+ if [ -z "$want" ]; then
+ echo "error: $CHECKSUMS has no entry for $2" >&2
+ echo " run build/update-checksums.sh $FFMPEG_KIT_VERSION to record it" >&2
+ exit 1
+ fi
+ got=$(sha256_of "$1")
+ if [ "$got" != "$want" ]; then
+ echo "error: SHA-256 mismatch for $2" >&2
+ echo " expected $want" >&2
+ echo " got $got" >&2
exit 1
- ;;
-esac
+ fi
+}
+
+# Everything one FFmpegKit line consists of: the eight variant .aars plus the two
+# smart-exception jars FFmpegKitConfig's static initialiser needs at runtime.
+expected_files() {
+ for variant in audio full full-gpl https https-gpl min min-gpl video; do
+ echo "ffmpeg-kit-$variant-$FFMPEG_KIT_VERSION.aar"
+ done
+ for artifact in common java; do
+ echo "smart-exception-$artifact-$SMART_EXCEPTION_VERSION.jar"
+ done
+}
+
+if [ "$VERIFY_ONLY" = 1 ]; then
+ for file in $(expected_files); do
+ if [ ! -f "$file" ]; then
+ echo "error: $file is missing - run FetchJars.sh to download it" >&2
+ exit 1
+ fi
+ verify_file "$file" "$file"
+ done
+ echo "==> verified $(expected_files | wc -l | tr -d ' ') files against $CHECKSUMS"
+ exit 0
+fi
echo "==> fetching FFmpegKit $FFMPEG_KIT_VERSION"
-# Downloaded into a staging directory and moved into place only once every file has arrived.
-# Deleting first and downloading second leaves the working copy with no binaries at all when a
-# version turns out not to exist, which costs a 200 MB re-download to recover from.
+# Downloaded into a staging directory and moved into place only once every file has arrived and
+# verified. Deleting first and downloading second leaves the working copy with no binaries at all
+# when a version turns out not to exist, which costs a 200 MB re-download to recover from.
STAGING=$(mktemp -d)
trap 'rm -rf "$STAGING"' EXIT
@@ -53,17 +137,21 @@ trap 'rm -rf "$STAGING"' EXIT
# is not bundled in the .aar and is not declared in its .pom. Without these two jars every
# FFmpeg call fails at runtime with NoClassDefFoundError.
for artifact in common java; do
- curl -fL --output-dir "$STAGING" -O \
+ curl -fL --retry 3 --retry-delay 2 --output-dir "$STAGING" -O \
"https://github.com/tanersener/smart-exception/releases/download/v$SMART_EXCEPTION_VERSION/smart-exception-$artifact-$SMART_EXCEPTION_VERSION.jar"
done
for variant in audio full full-gpl https https-gpl min min-gpl video; do
- curl -fL --output-dir "$STAGING" -O \
+ curl -fL --retry 3 --retry-delay 2 --output-dir "$STAGING" -O \
"$MAVEN_BASE/ffmpeg-kit-$variant/$FFMPEG_KIT_VERSION/ffmpeg-kit-$variant-$FFMPEG_KIT_VERSION.aar"
done
+for file in $(expected_files); do
+ verify_file "$STAGING/$file" "$file"
+done
+
rm -f ./*.jar
rm -f ./*.aar
mv "$STAGING"/*.jar "$STAGING"/*.aar .
-echo "==> fetched $(ls ./*.aar | wc -l | tr -d ' ') .aar and $(ls ./*.jar | wc -l | tr -d ' ') .jar files"
+echo "==> fetched and verified $(ls ./*.aar | wc -l | tr -d ' ') .aar and $(ls ./*.jar | wc -l | tr -d ' ') .jar files"
diff --git a/tests/FFmpegKit.Android.DeviceTests/SmokeTests.cs b/tests/FFmpegKit.Android.DeviceTests/SmokeTests.cs
index 5a46aaf..099e9e6 100644
--- a/tests/FFmpegKit.Android.DeviceTests/SmokeTests.cs
+++ b/tests/FFmpegKit.Android.DeviceTests/SmokeTests.cs
@@ -212,7 +212,7 @@ private static void LogDelegateReceivesOutput(string workingDirectory)
}
finally
{
- FFmpegKitConfig.EnableLogCallback((ILogCallback)null!);
+ FFmpegKitConfig.DisableLogCallback();
}
Assert(Volatile.Read(ref lines) > 0, "The log delegate never fired.");
diff --git a/tests/FFmpegKit.Android.PackageTests/NativeVersionMapTests.cs b/tests/FFmpegKit.Android.PackageTests/NativeVersionMapTests.cs
index d90c0e7..dabd788 100644
--- a/tests/FFmpegKit.Android.PackageTests/NativeVersionMapTests.cs
+++ b/tests/FFmpegKit.Android.PackageTests/NativeVersionMapTests.cs
@@ -46,7 +46,43 @@ private static string ReadProperty(string name)
[Fact]
public void Mapping_lists_every_supported_build()
{
- Assert.Equal(3, Mapping.Count);
+ // Append-only: lines that have shipped stay in the mapping so their tags remain
+ // releasable. New rows are expected over time, so this is a floor, not an exact count.
+ Assert.True(Mapping.Count >= 3, $"native-versions.tsv lists {Mapping.Count} rows; the 6.x/7.x/8.x lines must not be removed.");
+ }
+
+ [Fact]
+ public void Every_mapping_row_has_a_checksum_baseline()
+ {
+ // FetchJars.sh refuses to download a line that has no committed SHA-256 baseline, so a
+ // native-versions.tsv row without one is unreleasable. build/update-checksums.sh records
+ // a baseline in one command; this test is the reminder to run it.
+ var smartException = ReadProperty("SmartExceptionVersion");
+ var variants = new[] { "audio", "full", "full-gpl", "https", "https-gpl", "min", "min-gpl", "video" };
+
+ foreach (var (_, ffmpegKit) in Mapping)
+ {
+ var path = Path.Combine(RepoRoot, "build", "checksums", $"{ffmpegKit}.sha256");
+ Assert.True(File.Exists(path), $"Missing checksum baseline {path}; run build/update-checksums.sh {ffmpegKit}.");
+
+ var entries = File.ReadAllLines(path)
+ .Where(line => line.Trim().Length > 0)
+ .Select(line =>
+ {
+ var match = Regex.Match(line, "^([0-9a-f]{64}) (\\S+)$");
+ Assert.True(match.Success, $"{path}: malformed line '{line}'.");
+ return match.Groups[2].Value;
+ })
+ .ToHashSet();
+
+ foreach (var variant in variants)
+ {
+ Assert.Contains($"ffmpeg-kit-{variant}-{ffmpegKit}.aar", entries);
+ }
+
+ Assert.Contains($"smart-exception-common-{smartException}.jar", entries);
+ Assert.Contains($"smart-exception-java-{smartException}.jar", entries);
+ }
}
[Fact]
diff --git a/tests/FFmpegKit.Android.PackageTests/PackageLayoutTests.cs b/tests/FFmpegKit.Android.PackageTests/PackageLayoutTests.cs
index f086382..63ae7e8 100644
--- a/tests/FFmpegKit.Android.PackageTests/PackageLayoutTests.cs
+++ b/tests/FFmpegKit.Android.PackageTests/PackageLayoutTests.cs
@@ -88,6 +88,30 @@ public void Package_embeds_the_smart_exception_classes_needed_at_runtime(string
}
}
+ [Theory]
+ [MemberData(nameof(Packages.Variants), MemberType = typeof(Packages))]
+ public void Package_ships_the_constraint_check_targets(string variant)
+ {
+ using var package = Packages.OpenPackage(variant);
+
+ // The native constraints (64-bit only, API 24+) fail at app runtime if this file stops
+ // being packed - nothing else in the pipeline would notice. buildTransitive matters most:
+ // apps normally reference FFmpegKit.Net., not this binding directly.
+ foreach (var folder in new[] { "build", "buildTransitive" })
+ {
+ var path = $"{folder}/{Packages.PackageId(variant)}.targets";
+ var entry = package.GetEntry(path);
+ Assert.True(entry is not null, $"{Packages.PackageId(variant)} is missing '{path}'.");
+
+ using var reader = new StreamReader(entry!.Open());
+ var content = reader.ReadToEnd();
+
+ Assert.Contains("FFMPEGKIT001", content);
+ Assert.Contains("FFMPEGKIT002", content);
+ Assert.Contains("AndroidApplication", content);
+ }
+ }
+
[Theory]
[MemberData(nameof(Packages.Variants), MemberType = typeof(Packages))]
public void Package_declares_the_expected_nuspec_metadata(string variant)