Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}"

Expand Down
7 changes: 7 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
<FFmpegKitNativeVersion>8.1.7</FFmpegKitNativeVersion>
<VersionPrefix>$(FFmpegVersion)</VersionPrefix>

<!--
Runtime dependency of FFmpegKitConfig's static initialiser, embedded into every package.
Jars/FetchJars.sh downloads the jars named by this property and the .csproj embeds the
same names, so this is the only place the version appears.
-->
<SmartExceptionVersion>0.2.1</SmartExceptionVersion>

<!-- Which FFmpegKit variant to build. CI builds each of these in turn. -->
<FFmpegKitBuildType Condition=" '$(FFmpegKitBuildType)' == '' ">Video</FFmpegKitBuildType>

Expand Down
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

[![NuGet](https://img.shields.io/nuget/v/FFmpegKit.Net.Video.Android?label=nuget)](https://www.nuget.org/packages/FFmpegKit.Net.Video.Android)
[![release](https://github.com/sbokatuk/FFmpegKit.Android/actions/workflows/release.yml/badge.svg)](https://github.com/sbokatuk/FFmpegKit.Android/actions/workflows/release.yml)
[![Targets: net8.0 | net9.0 | net10.0](https://img.shields.io/badge/targets-net8.0%20%7C%20net9.0%20%7C%20net10.0-512BD4)](#packages)
[![Targets: net8.0 | net9.0 | net10.0](https://img.shields.io/badge/targets-net8.0%20%7C%20net9.0%20%7C%20net10.0-512BD4)](#installation)
[![ffmpeg 8.1.2](https://img.shields.io/badge/ffmpeg-8.1.2-632CA6)](#about)
[![Licence: MIT AND LGPL-3.0 or GPL-3.0](https://img.shields.io/badge/licence-MIT%20AND%20LGPL--3.0%20or%20GPL--3.0-orange)](#licence)
[![Licence: MIT AND LGPL-3.0 or GPL-3.0](https://img.shields.io/badge/licence-MIT%20AND%20LGPL--3.0%20or%20GPL--3.0-orange)](#license)

.NET for Android and .NET MAUI bindings for the native **FFmpegKit** library.

Expand Down Expand Up @@ -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 |
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
10 changes: 10 additions & 0 deletions build/checksums/6.0.3.sha256
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions build/checksums/7.1.6.sha256
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions build/checksums/8.1.7.sha256
Original file line number Diff line number Diff line change
@@ -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
104 changes: 104 additions & 0 deletions build/update-checksums.sh
Original file line number Diff line number Diff line change
@@ -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/<ffmpegkit version>.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"
23 changes: 23 additions & 0 deletions docs/release-notes/8.1.2.5.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 7 additions & 0 deletions samples/FFmpegKit.Android.Example/MainPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@
Clicked="OnConvertClicked"
HorizontalOptions="Fill" />

<Button
x:Name="CancelBtn"
Text="Cancel"
Clicked="OnCancelClicked"
IsEnabled="False"
HorizontalOptions="Fill" />

<ActivityIndicator
x:Name="Spinner"
IsRunning="False"
Expand Down
26 changes: 23 additions & 3 deletions samples/FFmpegKit.Android.Example/MainPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ sealed record ConversionOption(string Name, string OutputFileName, Func<string,

string? _inputPath;
TimeSpan? _sourceDuration;
CancellationTokenSource? _cancellation;

public MainPage()
{
Expand Down Expand Up @@ -70,6 +71,7 @@ async void OnConvertClicked(object sender, EventArgs e)
var inputPath = _inputPath;

ConvertBtn.IsEnabled = false;
CancelBtn.IsEnabled = true;
Spinner.IsVisible = true;
Spinner.IsRunning = true;
ConversionProgress.Progress = 0;
Expand All @@ -81,6 +83,8 @@ async void OnConvertClicked(object sender, EventArgs e)
AfterPlayer.Stop();
AfterPlayer.Source = null;

_cancellation = new CancellationTokenSource();

try
{
// Progress<T> marshals back to the thread that created it - the UI thread here -
Expand All @@ -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);

Expand All @@ -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;
Expand All @@ -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<FFmpegProgress> progress,
TimeSpan? sourceDuration)
TimeSpan? sourceDuration,
CancellationToken cancellationToken)
{
var outputPath = Path.Combine(FileSystem.CacheDirectory, option.OutputFileName);

Expand All @@ -130,14 +147,17 @@ 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())
{
var outputSize = new FileInfo(outputPath).Length;
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);
}
Expand Down
2 changes: 2 additions & 0 deletions src/FFmpegKit.Android/Additions/Callbacks.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;

#nullable enable

namespace Ffmpegkit.Droid
{
// Adapters adapting a delegate to FFmpegKit's single-method Java callback interfaces.
Expand Down
2 changes: 2 additions & 0 deletions src/FFmpegKit.Android/Additions/Enums.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;

#nullable enable

namespace Ffmpegkit.Droid
{
/// <summary>Managed counterpart of the Java <c>SessionState</c> enum.</summary>
Expand Down
2 changes: 2 additions & 0 deletions src/FFmpegKit.Android/Additions/FFmpegKit.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using System.Threading;
using System.Threading.Tasks;

#nullable enable

namespace Ffmpegkit.Droid
{
public partial class FFmpegKit
Expand Down
Loading
Loading