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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
64
lib/src/video/ffmpeg_license.dart
Normal file
64
lib/src/video/ffmpeg_license.dart
Normal 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',
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user