Compare commits

...

6 Commits

22 changed files with 758 additions and 636 deletions

View File

@ -1,10 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
# This file should be version controlled.
version:
revision: 37de8a2a9ad217ec9d731f8af0bd7c83e8f4980c
channel: master
revision: f92f44110e87bad5ff168335c36da6f6053036e6
channel: stable
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: f92f44110e87bad5ff168335c36da6f6053036e6
base_revision: f92f44110e87bad5ff168335c36da6f6053036e6
- platform: web
create_revision: f92f44110e87bad5ff168335c36da6f6053036e6
base_revision: f92f44110e87bad5ff168335c36da6f6053036e6
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'token.dart';
class TokenException implements Exception {
@ -11,7 +12,7 @@ class TokenException implements Exception {
class API {
static Future<String> query(
String apinode, String action, Object payload) async {
final t = await Token.getInstance().getToken();
final t = await getToken();
if (t != null) {
final resp = await http.post(
Uri.parse(t.domain + '/api/$apinode/$action'),

View File

@ -1,60 +1,19 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../log/log.dart';
import 'package:openmediacentermobile/db/settings_db.dart';
class TokenT {
String token;
String domain;
String videoPath;
TokenT(this.token, this.domain, this.videoPath);
TokenT(this.token, this.domain);
}
class Token {
static final Token _token = Token._();
final _storage = const FlutterSecureStorage();
String _tokenval = "";
String _domain = "";
String _vPath = "";
static Token getInstance() {
return _token;
}
Future<TokenT?> getToken() async {
if (_tokenval == "" || _domain == "" || _vPath == "") {
Log.d("reading token store");
WidgetsFlutterBinding.ensureInitialized();
final token = await _storage.read(key: 'jwt');
final domain = await _storage.read(key: 'domain');
final vPath = await _storage.read(key: 'videoPath');
// check if value is defined in phone store
if (token != null && domain != null && vPath != null) {
_tokenval = token;
_domain = domain;
return TokenT(token, domain, vPath);
} else {
Log.d("no token defined");
Future<TokenT?> getToken() async {
final settings = await SettingsDB.getInstance().getSettings();
if (settings.token == "" || settings.domain == "") {
return null;
}
} else {
return TokenT(_tokenval, _domain, _vPath);
return TokenT(settings.token, settings.domain);
}
}
void setToken(String token, String domain, String videoPath) {
_tokenval = token;
_domain = domain;
_vPath = videoPath;
_storage.write(key: 'jwt', value: token);
_storage.write(key: 'domain', value: domain);
_storage.write(key: 'videoPath', value: videoPath);
}
Token._();
}

View File

@ -29,6 +29,8 @@ class App extends StatelessWidget {
return const MaterialApp(home: LoginScreen());
} else {
return MaterialApp(
theme: ThemeData(
appBarTheme: AppBarTheme(backgroundColor: Color(0xff0d0d0d))),
scrollBehavior: AppScrollBehavior(),
home: DrawerPage(
title: 'OpenMediaCenter',

View File

@ -1,39 +1,47 @@
import 'package:flutter/foundation.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart' as nativeffi;
import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart' as webffi;
import '../log/log.dart';
class Db {
late Database _db;
void init() async {
if (kIsWeb) {
Log.i("Database on web is not supported");
return;
}
Future<void> init() async {
String dbpath = 'previews.db';
if (defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS) {
dbpath = join(await getDatabasesPath(), dbpath);
} else if(kIsWeb) {
databaseFactory = webffi.databaseFactoryFfiWeb;
} else {
// Initialize FFI
sqfliteFfiInit();
nativeffi.sqfliteFfiInit();
// Change the default factory
databaseFactory = databaseFactoryFfi;
databaseFactory = nativeffi.databaseFactoryFfi;
}
_db = await openDatabase(
dbpath,
onCreate: (db, version) {
return db.execute(
'CREATE TABLE previews(id INTEGER PRIMARY KEY, thumbnail BLOB)',
final batch = db.batch();
batch.execute(
'CREATE TABLE previews(id INTEGER PRIMARY KEY, thumbnail BLOB);',
);
batch.execute(
'CREATE TABLE settings(domain TEXT, token TEXT, videopath TEXT, tilewidth INTEGER);',
);
batch.insert("settings",
{"domain": "", "token": "", "videopath": "", "tilewidth": 0});
return batch.commit();
},
// Set the version. This executes the onCreate function and provides a
// path to perform database upgrades and downgrades.
version: 1,
version: 2,
);
}
@ -46,10 +54,16 @@ class Db {
/// get db size in bytes
Future<int> getDbSize() async {
final int cnt = (await Db().db().rawQuery("pragma page_count;"))[0]
["page_count"] as int;
final batch = _db.batch();
batch.rawQuery("pragma page_count;");
batch.rawQuery("pragma page_size;");
final result = (await batch.commit(noResult: false));
print(result);
final int cnt =
((result[0] as List<Map<String, dynamic>>)[0]["page_count"] as int);
final int pagesize =
(await Db().db().rawQuery("pragma page_size;"))[0]["page_size"] as int;
(result[1] as List<Map<String, dynamic>>)[0]["page_size"] as int;
return cnt * pagesize;
}

51
lib/db/settings_db.dart Normal file
View File

@ -0,0 +1,51 @@
import 'database.dart';
class SettingsT {
String domain;
String token;
String videopath;
int tilewidth;
SettingsT(this.domain, this.token, this.videopath, this.tilewidth);
}
class SettingsDB {
static final SettingsDB _instance = SettingsDB._();
SettingsT _settings = SettingsT("", "", "", 0);
bool _initialized = false;
Future<SettingsT> getSettings() async {
if (!_initialized) {
final result = (await Db().db().query("settings",
where: "1",
columns: ["domain", "token", "videopath", "tilewidth"]))
.first;
_settings = SettingsT(
result["domain"] as String,
result["token"] as String,
result["videopath"] as String,
result["tilewidth"] as int);
}
return _settings;
}
Future<void> setSettings(SettingsT settings) async {
await Db().db().update(
"settings",
{
"domain": settings.domain,
"token": settings.token,
"videopath": settings.videopath,
"tilewidth": settings.tilewidth
},
where: "1");
}
static SettingsDB getInstance() {
return _instance;
}
SettingsDB._();
}

View File

@ -32,6 +32,8 @@ class _AddActorDialogState extends State<AddActorDialog> {
return Text("Error");
} else if (snapshot.hasData) {
final data = snapshot.data! as List<Actor>;
data.sort((a, b) =>
a.name.toLowerCase().compareTo(b.name.toLowerCase()));
return Column(
mainAxisSize: MainAxisSize.min,
children: data

View File

@ -32,6 +32,10 @@ class _AddTagDialogState extends State<AddTagDialog> {
return Text("Error");
} else if (snapshot.hasData) {
final data = snapshot.data! as List<Tag>;
data.sort(
(a, b) =>
a.tagName.toLowerCase().compareTo(b.tagName.toLowerCase()),
);
return Column(
mainAxisSize: MainAxisSize.min,
children: data

View File

@ -7,52 +7,32 @@ class MyDrawer extends StatelessWidget {
const MyDrawer({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
Widget build(BuildContext context) => Drawer(
backgroundColor: Color(0xff3f3f3f),
child: ListView(children: [
SizedBox(
height: 75,
),
_listelement('Home', Icons.home, Section.HOME, context),
_listelement('Shuffle', Icons.update, Section.SHUFFLE, context),
_listelement(
'Categories', Icons.category, Section.CATEGORIE, context),
_listelement('Actors', Icons.people, Section.ACTOR, context),
_listelement('Settings', Icons.settings, Section.SETTING, context),
]),
);
Widget _listelement(
String text, IconData icon, Section section, BuildContext context) {
final ctx = DrawerContext.of(context);
return Drawer(
child: ListView(children: [
ListTile(
title: const Text('Home'),
leading: const Icon(Icons.home),
return ListTile(
title: Text(text, style: TextStyle(color: Color(0xffe9e9e9))),
leading: Icon(icon, color: Color(0xffe9e9e9)),
onTap: () {
ctx.onChangePage(Section.HOME);
ctx.onChangePage(section);
Navigator.pop(context);
},
),
ListTile(
title: const Text('Shuffle'),
leading: const Icon(Icons.update),
onTap: () {
ctx.onChangePage(Section.SHUFFLE);
Navigator.pop(context);
},
),
ListTile(
title: const Text('Categories'),
leading: const Icon(Icons.category),
onTap: () {
ctx.onChangePage(Section.CATEGORIE);
Navigator.pop(context);
},
),
ListTile(
title: const Text('Actors'),
leading: const Icon(Icons.people),
onTap: () {
ctx.onChangePage(Section.ACTOR);
Navigator.pop(context);
},
),
ListTile(
title: const Text('Settings'),
leading: const Icon(Icons.settings),
onTap: () {
ctx.onChangePage(Section.SETTING);
Navigator.pop(context);
},
),
]),
);
}
}

View File

@ -23,7 +23,7 @@ class _LoginContainerState extends State<LoginContainer> {
}
void _init() async {
final token = await Token.getInstance().getToken();
final token = await getToken();
Log.i("The token value is $token");
if (token != null) {
setState(() {

View File

@ -3,8 +3,9 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import '../api/settings_api.dart';
import '../api/token.dart';
import '../db/settings_db.dart';
import '../log/log.dart';
import 'login_context.dart';
@ -44,11 +45,13 @@ class _LoginScreenState extends State<LoginScreen> {
final json = jsonDecode(resp.body);
final token = json["Token"];
// todo bit hacky
Token.getInstance().setToken(token, domain, "temp");
// we need to call this twice because we need for the loadInitialData an api token
Token.getInstance()
.setToken(token, domain, (await loadInitialData()).videoPath);
SettingsT settings = await SettingsDB.getInstance().getSettings();
settings.domain = domain;
settings.token = token;
SettingsDB.getInstance().setSettings(settings);
// we need two steps here because we need an authenticated api call for the videopath
settings.videopath = (await loadInitialData()).videoPath;
SettingsDB.getInstance().setSettings(settings);
LoginContext.of(context).onLoggin(true);
return "";

View File

@ -1,6 +1,7 @@
import "package:dart_vlc/dart_vlc.dart";
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:media_kit/media_kit.dart';
import 'app.dart';
import 'db/database.dart';
@ -10,12 +11,17 @@ import 'utils/platform.dart';
void main() async {
Log.i("App init!");
if (isDesktop()) {
DartVLC.initialize();
} else {
WidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized();
if (!kIsWeb && !isDesktop()) {
Log.i("init device info");
await loadDeviceInfo();
}
Db().init();
Log.i("Mediakit initialized");
await Db().init();
runApp(Shortcuts(shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.select): ActivateIntent(),

View File

@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
import '../utils/file_formatter.dart';
import 'package:openmediacentermobile/db/settings_db.dart';
import '../api/token.dart';
import '../db/database.dart';
import '../drawer/my_drawer.dart';
import '../login/login_context.dart';
import '../utils/file_formatter.dart';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({Key? key}) : super(key: key);
@ -24,7 +25,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
dbsize = v;
}));
Token.getInstance().getToken().then((value) => setState(() {
getToken().then((value) => setState(() {
serverUrl = value?.domain ?? "unknown";
}));
}
@ -53,7 +54,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
ElevatedButton(
onPressed: () {
loginCtx.onLoggin(false);
Token.getInstance().setToken("", "", "");
SettingsDB.getInstance()
.setSettings(SettingsT("", "", "", 0));
Db().clear();
},
child: Text("Logout"))

View File

@ -3,9 +3,9 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import '../utils/platform.dart';
import '../screen_loading.dart';
import '../types/video.dart';
import '../utils/platform.dart';
import 'preview_tile.dart';
class PreviewGrid extends StatefulWidget {
@ -60,7 +60,7 @@ class _PreviewGridState extends State<PreviewGrid> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("Error"),
Text("Error" + snapshot.error.toString()),
TextButton(
onPressed: () => loadData(), child: Text("Reload page"))
],
@ -78,7 +78,9 @@ class _PreviewGridState extends State<PreviewGrid> {
Widget _mainGrid(List<VideoT> data, double width) {
return Stack(
children: [
Column(
Container(
color: Color(0xff999999),
child: Column(
children: [
if (widget.headerBuilder != null) widget.headerBuilder!(this),
data.length > 0
@ -123,6 +125,7 @@ class _PreviewGridState extends State<PreviewGrid> {
if (widget.footerBuilder != null) widget.footerBuilder!(this),
],
),
),
if (_previewImage != null) ..._buildPreviewImage(),
],
);

View File

@ -5,8 +5,8 @@ import 'package:sqflite/sqflite.dart';
import '../api/video_api.dart';
import '../db/database.dart';
import '../log/log.dart';
import '../utils/platform.dart';
import '../types/video.dart';
import '../utils/platform.dart';
import '../video_screen/videoscreen.dart';
class PreviewTile extends StatefulWidget {
@ -76,6 +76,7 @@ class _PreviewTileState extends State<PreviewTile> {
);
// precache image to avoid loading time to render image
if(context.mounted)
await precacheImage(img.image, context);
return img;
@ -103,18 +104,28 @@ class _PreviewTileState extends State<PreviewTile> {
child: Stack(
children: [
Container(
color: const Color(0xff3f3f3f),
child: Column(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(20.0),
child: image,
),
const SizedBox(
height: 3,
),
Text(
widget.dta.title,
style: TextStyle(fontSize: isTV() ? 8 : 10.5),
style: TextStyle(
fontSize: isTV() ? 8 : 10.5, color: const Color(0xffe9e9e9)),
overflow: TextOverflow.clip,
maxLines: 1,
),
image
const SizedBox(
height: 3,
),
],
),
color: Color(0x6a94a6ff),
),
Positioned.fill(
child: Material(
@ -147,7 +158,9 @@ class _PreviewTileState extends State<PreviewTile> {
@override
Widget build(BuildContext context) {
return FutureBuilder<Image>(
return ConstrainedBox(
constraints: const BoxConstraints(minHeight: 200, minWidth: 200),
child: FutureBuilder<Image>(
future: _preview, // a previously-obtained Future<String> or null
builder: (BuildContext context, AsyncSnapshot<Image> snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
@ -162,6 +175,7 @@ class _PreviewTileState extends State<PreviewTile> {
return _buildLoader();
}
},
),
);
}
}

View File

@ -1,18 +1,17 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../api/token.dart';
import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:openmediacentermobile/db/settings_db.dart';
import '../api/video_api.dart';
import '../utils/platform.dart';
import '../screen_loading.dart';
import '../types/video.dart';
import '../types/video_data.dart';
import '../utils/platform.dart';
import 'info_view.dart';
import 'videoscreen_desktop.dart'
if (dart.library.html) 'videoscreen_mobile.dart';
import 'videoscreen_mobile.dart';
class VideoScreen extends StatefulWidget {
const VideoScreen({Key? key, required this.metaData}) : super(key: key);
final VideoT metaData;
@ -28,16 +27,19 @@ class _VideoScreenState extends State<VideoScreen> {
PageController _controller = PageController(
initialPage: 0,
);
// Create a [Player] to control playback.
late final player = Player();
// Create a [VideoController] to handle video output from [Player].
late final controller = VideoController(player);
String url = "";
void initPlayer() async {
final videodata = await _videoData;
final token = await Token.getInstance().getToken();
if (token == null) return;
final path = token.domain + token.videoPath + videodata.movieUrl;
final settings = await SettingsDB.getInstance().getSettings();
final path = settings.domain + settings.videopath + videodata.movieUrl;
player.open(Media(path));
url = path;
}
@ -53,6 +55,7 @@ class _VideoScreenState extends State<VideoScreen> {
@override
void dispose() {
super.dispose();
player.dispose();
_controller.dispose();
_appBarTimer?.cancel();
}
@ -112,13 +115,7 @@ class _VideoScreenState extends State<VideoScreen> {
controller: _controller,
children: [
Center(
child: isDesktop()
? VideoScreenDesktop(
url: url,
)
: VideoScreenMobile(
url: url,
)),
child: Video(controller: controller)),
InfoView(
videoId: widget.metaData.id,
)
@ -133,7 +130,10 @@ class _VideoScreenState extends State<VideoScreen> {
leading: new IconButton(
icon: new Icon(Icons.arrow_back_ios,
color: Colors.grey),
onPressed: () => Navigator.of(context).pop(),
onPressed: () async {
await player.stop();
Navigator.of(context).pop();
},
),
backgroundColor:
Theme.of(context).primaryColor.withOpacity(0.3),

View File

@ -1,51 +0,0 @@
import 'dart:math';
import 'package:dart_vlc/dart_vlc.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class VideoScreenDesktop extends StatefulWidget {
const VideoScreenDesktop({Key? key, required this.url}) : super(key: key);
final String url;
@override
State<VideoScreenDesktop> createState() => _VideoScreenDesktopState();
}
class _VideoScreenDesktopState extends State<VideoScreenDesktop> {
Player _player = Player(id: Random().nextInt(0x7fffffff));
@override
Widget build(BuildContext context) {
return Video(
player: _player,
scale: 1.0, // default
showControls: true);
}
@override
void initState() {
super.initState();
final media2 = Media.network(widget.url);
_player.open(
media2,
autoStart: true, // default
);
RawKeyboard.instance.addListener((value) {
if (value.logicalKey == LogicalKeyboardKey.arrowRight) {
_player.seek(_player.position.position! + const Duration(seconds: 5));
} else if (value.logicalKey == LogicalKeyboardKey.arrowLeft) {
_player.seek(_player.position.position! + const Duration(seconds: -5));
}
});
}
@override
void dispose() {
super.dispose();
_player.dispose();
}
}

View File

@ -1,59 +0,0 @@
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
class VideoScreenMobile extends StatefulWidget {
const VideoScreenMobile({Key? key, required this.url}) : super(key: key);
final String url;
@override
State<VideoScreenMobile> createState() => _VideoScreenMobileState();
}
class _VideoScreenMobileState extends State<VideoScreenMobile> {
ChewieController? _chewieController;
@override
Widget build(BuildContext context) {
if (_chewieController == null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: const [CircularProgressIndicator(), Text("loading...")],
);
}
return Chewie(
controller: _chewieController!,
);
}
@override
void dispose() {
super.dispose();
_chewieController?.videoPlayerController.dispose();
_chewieController?.dispose();
}
@override
void initState() {
super.initState();
_init();
}
void _init() async {
final VideoPlayerController _controller =
VideoPlayerController.network(widget.url);
await _controller.initialize();
_chewieController = ChewieController(
videoPlayerController: _controller,
autoPlay: true,
looping: true,
allowFullScreen: true,
allowMuting: true,
allowPlaybackSpeedChanging: true,
zoomAndPan: true);
setState(() {});
}
}

View File

@ -6,22 +6,14 @@
#include "generated_plugin_registrant.h"
#include <dart_vlc/dart_vlc_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <window_manager/window_manager_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) dart_vlc_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DartVlcPlugin");
dart_vlc_plugin_register_with_registrar(dart_vlc_registrar);
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
g_autoptr(FlPluginRegistrar) window_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar);
g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin");
media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar);
g_autoptr(FlPluginRegistrar) media_kit_video_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin");
media_kit_video_plugin_register_with_registrar(media_kit_video_registrar);
}

View File

@ -3,13 +3,12 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
dart_vlc
flutter_secure_storage_linux
screen_retriever
window_manager
media_kit_libs_linux
media_kit_video
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
media_kit_native_event_loop
)
set(PLUGIN_BUNDLED_LIBRARIES)

File diff suppressed because it is too large Load Diff

View File

@ -34,17 +34,23 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
flutter_secure_storage: ^6.0.0
logger: ^1.1.0
http: ^0.13.4
flutter_staggered_grid_view: ^0.6.1
dart_vlc: ^0.3.0
device_info_plus: ^4.0.0
video_player: ^2.3.0
chewie: ^1.3.2
device_info_plus: ^8.0.0
sqflite: ^2.0.3+1
path: ^1.8.1
sqflite_common_ffi: ^2.1.1+1
sqflite_common_ffi_web: '^0.3.6'
media_kit: ^1.0.2 # Primary package.
media_kit_video: ^1.0.2 # For video rendering.
media_kit_native_event_loop: ^1.0.6 # Support for higher number of concurrent instances & better performance.
media_kit_libs_android_video: ^1.1.1 # Android package for video native libraries.
media_kit_libs_linux: ^1.0.2 # GNU/Linux dependency package.
dev_dependencies:
flutter_test: