FFmpeg software H.264 decoder: opt-in via pubspec flag

- Gate buildFfmpegJni + jniLibs packaging on `ux: enable_ffmpeg` in the
  consuming app's pubspec (default off) -- no LGPL / H.264-patent
  exposure unless explicitly enabled
- appInfoBuilder generates kUxEnableFfmpeg from the same flag so apps
  register the FFmpeg LGPL notice eagerly, pubspec-only (no dart-define)
- Add registerFfmpegLicense() + bundled LGPL-2.1 text asset
- FFmpeg compliance docs (LICENSES-3RDPARTY.md, android/ffmpeg/README.md)
- Network video streaming: XVideoPlayerController.network
This commit is contained in:
agra
2026-06-15 19:16:16 +03:00
parent 27cfc87def
commit 36b5143cb3
12 changed files with 800 additions and 34 deletions

View File

@@ -22,6 +22,8 @@ class _AppInfoBuilder implements Builder {
final buildNumber =
plus < 0 ? 0 : int.tryParse(combined.substring(plus + 1)) ?? 0;
final enableFfmpeg = _readUxFlag(raw, 'enable_ffmpeg');
await buildStep.writeAsString(
AssetId(pkg, 'lib/app_info.g.dart'),
'''
@@ -29,7 +31,35 @@ class _AppInfoBuilder implements Builder {
import 'package:ux/ux.dart';
const kAppInfo = AppInfo(version: '$version', buildNumber: $buildNumber);
/// Mirrors the app's `ux: enable_ffmpeg` pubspec setting as a compile-time
/// constant. Gate FFmpeg-dependent code (e.g. registerFfmpegLicense()) on
/// this — pubspec stays the single source of truth.
const kUxEnableFfmpeg = $enableFfmpeg;
''',
);
}
/// Reads a boolean under the top-level `ux:` mapping, e.g. `ux: { $key: ... }`.
/// Scans manually (no YAML dep) and ignores the indented `ux:` dependency
/// entry by only entering the block on a non-indented `ux:` line.
static bool _readUxFlag(String pubspec, String key) {
var inUxBlock = false;
var enabled = false;
for (final rawLine in pubspec.split('\n')) {
final hash = rawLine.indexOf('#');
final line = hash >= 0 ? rawLine.substring(0, hash) : rawLine;
if (line.trim().isEmpty) continue;
final indented = line.startsWith(' ') || line.startsWith('\t');
if (!indented) {
inUxBlock = line.trimRight() == 'ux:' || line.startsWith('ux:');
} else if (inUxBlock) {
final t = line.trim();
if (t.startsWith('$key:')) {
enabled = t.substring(key.length + 1).trim().toLowerCase() == 'true';
}
}
}
return enabled;
}
}

View File

@@ -0,0 +1,64 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart' show rootBundle;
/// Whether the FFmpeg notice has already been added to [LicenseRegistry].
/// Registration adds a stream provider, so guard against duplicates.
bool _registered = false;
/// Registers the FFmpeg LGPL-2.1 attribution with [LicenseRegistry].
///
/// Call this at startup gated on the generated `kUxEnableFfmpeg` constant
/// (from `app_info.g.dart`, which the ux build_runner builder derives from the
/// app's `ux: enable_ffmpeg` pubspec flag) — e.g.
/// `if (kUxEnableFfmpeg) registerFfmpegLicense();`. That keeps pubspec the
/// single source of truth, registers eagerly (the notice shows without playing
/// a video), and needs no `--dart-define`. Idempotent — registers at most once.
void registerFfmpegLicense() {
if (_registered) return;
_registered = true;
LicenseRegistry.addLicense(_ffmpegLicenseEntries);
}
/// Attribution shown above the LGPL text. Keep in sync with
/// `android/ffmpeg/build_ffmpeg.sh` (`FFMPEG_TAG`) and
/// `android/ffmpeg/README.md`.
const String _attribution = '''
This application includes FFmpeg, used unmodified as a software video
decoder on Android via the "ux" package (libffmpegJNI.so, loaded at runtime).
FFmpeg is copyright (c) the FFmpeg developers and is licensed under the
GNU Lesser General Public License, version 2.1 (LGPL-2.1). The binary is
dynamically loaded by the app; the LGPL boundary is preserved and no
copyleft is imposed on the application.
Version: FFmpeg release/6.0, built from upstream source with the H.264
decoder enabled only (decode-only, no encoders). It is configured WITHOUT
--enable-gpl and WITHOUT --enable-nonfree, so the binary is LGPL-only.
The corresponding FFmpeg source and the build/link scripts (which document
the exact configure flags) are available on request for at least three
years; the application's own source code is not included in that offer.
The shared library can be rebuilt and replaced from those sources, as
required by section 6 of the LGPL. Contact: alex@swipelab.co
The full text of the LGPL v2.1 follows.''';
Stream<LicenseEntry> _ffmpegLicenseEntries() async* {
String lgpl;
try {
lgpl = await rootBundle
.loadString('packages/ux/assets/licenses/ffmpeg_LGPL-2.1.txt');
} catch (error, stack) {
// A missing asset must never break license collection; the attribution
// paragraph below still names FFmpeg and its license.
FlutterError.reportError(
FlutterErrorDetails(exception: error, stack: stack),
);
lgpl = '';
}
yield LicenseEntryWithLineBreaks(
const <String>['FFmpeg (libavcodec, libavutil, libswresample)'],
lgpl.isEmpty ? _attribution : '$_attribution\n\n$lgpl',
);
}

View File

@@ -130,17 +130,26 @@ class XVideoPlayerValue {
/// Owns one native video-player session. The surface mirrors the
/// subset of `package:video_player`'s `VideoPlayerController` the app
/// currently uses — only the `.file(...)` constructor, the standard
/// playback controls, and the `value` getters compose / gallery /
/// outgoing-media read.
/// currently uses — the `.file(...)` / `.network(...)` constructors, the
/// standard playback controls, and the `value` getters compose /
/// gallery / outgoing-media read.
///
/// Lifecycle: construct → [initialize] → use → [dispose]. After
/// [dispose] every other method throws [XVideoPlayerException("disposed")].
class XVideoPlayerController extends ChangeNotifier
implements ValueListenable<XVideoPlayerValue> {
XVideoPlayerController.file(io.File file) : _file = file;
/// Play a local file.
XVideoPlayerController.file(io.File file) : _uri = 'file://${file.path}';
final io.File _file;
/// Stream from a network URL. The native side hands the URI straight
/// to Media3 (Android, `MediaItem.fromUri`) / `AVURLAsset` (Apple),
/// both of which stream http(s) natively — no extra setup.
XVideoPlayerController.network(Uri uri) : _uri = uri.toString();
/// Fully-qualified source URI handed to the native side at
/// [initialize]. `file://...` for [XVideoPlayerController.file], the
/// raw URL for [XVideoPlayerController.network].
final String _uri;
XVideoPlayerValue _value = XVideoPlayerValue.uninitialized;
@@ -185,7 +194,7 @@ class XVideoPlayerController extends ChangeNotifier
Future<void> _initInternal() async {
try {
final result = await XVideoPlayerBackend.instance.create(
uri: 'file://${_file.path}',
uri: _uri,
);
_handle = result.handle;
_textureId = result.textureId;

View File

@@ -16,8 +16,9 @@ abstract class XVideoPlayerBackend {
/// Swap to inject a fake before any UI code mounts a controller.
static XVideoPlayerBackend instance = MethodChannelXVideoPlayerBackend();
/// Allocate a native player instance bound to [uri] (currently always
/// `file://...`). Returns the handle and, on Apple platforms, the
/// Allocate a native player instance bound to [uri] (`file://...` for
/// local playback, or an http(s) URL for network streaming). Returns
/// the handle and, on Apple platforms, the
/// Flutter texture id; Android returns `textureId: null` because the
/// render path is a platform view that takes the handle as creation
/// params.

View File

@@ -14,6 +14,7 @@ export 'src/camera/camera.dart';
export 'src/camera/camera_backend.dart' show XCameraBackend, XCameraCreateResult, XCameraEvent, XCameraDeviceOrientationChanged, XCameraSessionError, XCameraSessionInterrupted, XCameraSessionResumed, XCameraDiagnostic, XCameraPreviewSizeChanged;
export 'src/camera/camera_channel.dart' show MethodChannelXCameraBackend;
export 'src/camera/camera_preview.dart';
export 'src/video/ffmpeg_license.dart' show registerFfmpegLicense;
export 'src/video/x_video_player.dart';
export 'src/video/x_video_player_backend.dart' show XVideoPlayerBackend, XVideoPlayerCreateResult, XVideoPlayerMetadata, XVideoPlayerEvent, XVideoPlayerStateChanged, XVideoPlayerSizeChanged, XVideoPlayerCompleted, XVideoPlayerError;
export 'src/video/x_video_player_channel.dart' show MethodChannelXVideoPlayerBackend;