diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist
index f2872cf..8c6e561 100644
--- a/example/ios/Flutter/AppFrameworkInfo.plist
+++ b/example/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
CFBundleVersion
1.0
MinimumOSVersion
- 9.0
+ 12.0
diff --git a/example/ios/Podfile b/example/ios/Podfile
index 1e8c3c9..279576f 100644
--- a/example/ios/Podfile
+++ b/example/ios/Podfile
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
-# platform :ios, '9.0'
+# platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index 9214757..a653c95 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -13,8 +13,9 @@ PODS:
- CocoaAsyncSocket
- KTVHTTPCache (2.0.1):
- KTVCocoaHTTPServer
- - path_provider_ios (0.0.1):
+ - path_provider_foundation (0.0.1):
- Flutter
+ - FlutterMacOS
- PINCache (3.0.3):
- PINCache/Arc-exception-safe (= 3.0.3)
- PINCache/Core (= 3.0.3)
@@ -29,7 +30,7 @@ PODS:
DEPENDENCIES:
- better_player (from `.symlinks/plugins/better_player/ios`)
- Flutter (from `Flutter`)
- - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
+ - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- wakelock (from `.symlinks/plugins/wakelock/ios`)
SPEC REPOS:
@@ -46,23 +47,23 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/better_player/ios"
Flutter:
:path: Flutter
- path_provider_ios:
- :path: ".symlinks/plugins/path_provider_ios/ios"
+ path_provider_foundation:
+ :path: ".symlinks/plugins/path_provider_foundation/darwin"
wakelock:
:path: ".symlinks/plugins/wakelock/ios"
SPEC CHECKSUMS:
- better_player: f10a109209a2738647404fe1865d76cb9027d567
+ better_player: f52c69d950ca39e57e1ac54be7058ebd342a0bc6
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
- Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
+ Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
GCDWebServer: 2c156a56c8226e2d5c0c3f208a3621ccffbe3ce4
KTVCocoaHTTPServer: df8d7b861e603ff8037e9b2138aca2563a6b768d
KTVHTTPCache: 588c3eb16f6bd1e6fde1e230dabfb7bd4e490a4d
- path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
+ path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
PINCache: 7a8fc1a691173d21dbddbf86cd515de6efa55086
PINOperation: 00c935935f1e8cf0d1e2d6b542e75b88fc3e5e20
- wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
+ wakelock: cc82bb76a8260a57c002d11c0a508b434618379f
-PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
+PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011
-COCOAPODS: 1.11.3
+COCOAPODS: 1.16.2
diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj
index c8cb374..6f85988 100644
--- a/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/example/ios/Runner.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 51;
+ objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@@ -155,7 +155,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
- LastUpgradeCheck = 1300;
+ LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
@@ -199,10 +199,12 @@
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
+ "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
@@ -230,6 +232,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@@ -339,7 +342,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -362,7 +365,7 @@
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -426,7 +429,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -475,7 +478,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -493,14 +496,14 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = N7JZ68X9C5;
+ DEVELOPMENT_TEAM = XUMY2D8NX7;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -532,7 +535,7 @@
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index 3db53b6..e67b280 100644
--- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -1,6 +1,6 @@
UIViewControllerBasedStatusBarAppearance
+ UIApplicationSupportsIndirectInputEvents
+
diff --git a/example/lib/better_player.dart b/example/lib/better_player.dart
index 4056cdb..2f09d13 100644
--- a/example/lib/better_player.dart
+++ b/example/lib/better_player.dart
@@ -7,7 +7,7 @@ class TheBetterPlayer extends StatefulWidget {
}
class _BetterPlayerState extends State {
- BetterPlayerController ctl;
+ BetterPlayerController? ctl;
@override
Widget build(BuildContext context) {
@@ -23,7 +23,7 @@ class _BetterPlayerState extends State {
body: AspectRatio(
aspectRatio: 9 / 16,
child: BetterPlayer(
- controller: ctl,
+ controller: ctl!,
),
),
);
diff --git a/example/lib/main.dart b/example/lib/main.dart
index b44a19e..869552b 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,6 +1,5 @@
import 'package:player_test/video_page.dart';
-import 'better_player.dart';
import 'package:flutter/material.dart';
import 'package:better_player/better_player.dart';
@@ -61,7 +60,7 @@ class _AppState extends State {
child: BetterPlayer(controller: k),
onTap: () {
print(k.isPlaying());
- if (k.isPlaying()) {
+ if (k.isPlaying()!) {
k.pause();
} else {
k.play();
diff --git a/example/lib/video_page.dart b/example/lib/video_page.dart
index 947eb65..ef844e1 100644
--- a/example/lib/video_page.dart
+++ b/example/lib/video_page.dart
@@ -1,154 +1,219 @@
import 'package:better_player/better_player.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart' show rootBundle;
-import 'package:http/http.dart';
-import 'dart:typed_data';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
/// Simple stateful widget to reproduce the bug.
/// Replaced the App() widget in the main function to run this one instead.
-///
-/// The issue can be noticed when the Play Icon will still be there (means that the video should be paused)
-/// and the video will continue playing.
class VideoPage extends StatefulWidget {
- VideoPage({Key key}) : super(key: key);
+ VideoPage({Key? key}) : super(key: key);
@override
State createState() => _VideoPageState();
}
class _VideoPageState extends State {
- ByteData k;
- List necStuff;
- String path;
- bool isPlaying = true;
+ String? path;
+ List? videoBytes;
+ bool isLoading = true;
+
+ // Track current page index
+ int currentPageIndex = 0;
+
+ // Store controllers with page index as key
+ Map controllers = {};
+
+ // Track play state for each video
+ Map playingStates = {};
+
String exampleManifestUrl =
'https://replied-resources.s3.amazonaws.com/transcoded-videos/'
'Sc9xBOOx3cblM82doaUK9rr8xYx1/dd03019b-5390-4378-9952-287122b47944/master.m3u8';
- List but = new List(3);
- int cI = 0;
-
- Future dothing() async {
- var content = await rootBundle.load("assets/testtest.mp4");
- Directory directory = await getApplicationDocumentsDirectory();
- this.path = directory.path;
- var file = File("${directory.path}/testvideo.mp4");
- file.writeAsBytesSync(content.buffer.asUint8List());
- return "hi";
- }
@override
void initState() {
- dothing().then((ku) {
- this.necStuff = File("${this.path}/testvideo.mp4")
- .readAsBytesSync()
- .buffer
- .asUint8List();
- //print(this.necStuff);
- });
- but[0] = getController();
super.initState();
+ setState(() {
+ isLoading = true;
+ });
+ prepareVideo();
}
- @override
- void dispose() {
- super.dispose();
- }
+ Future prepareVideo() async {
+ try {
+ var content = await rootBundle.load("assets/testtest.mp4");
+ Directory directory = await getApplicationDocumentsDirectory();
+ path = directory.path;
- BetterPlayerController getController() {
- var before = DateTime.now().microsecondsSinceEpoch;
- print(this.necStuff == null);
- var ctl = BetterPlayerController(
- BetterPlayerConfiguration(
- placeholderOnTop: true,
- placeholder: Center(child: CupertinoActivityIndicator()),
- aspectRatio: 9 / 16,
- autoPlay: true,
- looping: true,
- autoDispose: true,
- controlsConfiguration: BetterPlayerControlsConfiguration(
- showControls: false,
- enableOverflowMenu: false,
- enablePlayPause: false,
- enableMute: false)),
- betterPlayerDataSource: BetterPlayerDataSource(
- //this.necStuff == null
- //? BetterPlayerDataSourceType.network
- //: BetterPlayerDataSourceType.memory,
- BetterPlayerDataSourceType.network,
- //this.necStuff == null ? exampleManifestUrl : "",
- //bytes: this.necStuff,
- exampleManifestUrl,
- videoExtension: "mp4"));
- var after = DateTime.now().microsecondsSinceEpoch;
- print(after - before);
- return ctl;
- }
+ var file = File("${directory.path}/testvideo.mp4");
+ file.writeAsBytesSync(content.buffer.asUint8List());
+
+ videoBytes = file.readAsBytesSync().buffer.asUint8List();
+
+ // Initialize first controller
+ controllers[0] = createController();
+ playingStates[0] = true;
- Future toggleController(BetterPlayerController controller) async {
- if (controller.isPlaying()) {
- await controller.pause().then((value) => print('Paused'));
setState(() {
- isPlaying = false;
+ isLoading = false;
});
- } else {
- await controller.play().then((value) => print('Played'));
+ } catch (e) {
+ print("Error preparing video: $e");
setState(() {
- isPlaying = true;
+ isLoading = false;
});
}
- print('Controller is playing? ${controller.isPlaying()}');
}
- void sleepthing(BetterPlayerController prevCtl) async {
- await Future.delayed(Duration(seconds: 1)).then((dynamic k) {
- prevCtl?.dispose();
- prevCtl = null;
- });
+ BetterPlayerController createController() {
+ return BetterPlayerController(
+ BetterPlayerConfiguration(
+ placeholderOnTop: true,
+ placeholder: Center(child: CupertinoActivityIndicator()),
+ aspectRatio: 9 / 16,
+ autoPlay: true,
+ looping: true,
+ autoDispose: false,
+ controlsConfiguration: BetterPlayerControlsConfiguration(
+ showControls: false,
+ enableOverflowMenu: false,
+ enablePlayPause: false,
+ enableMute: false,
+ ),
+ ),
+ betterPlayerDataSource: BetterPlayerDataSource(
+ BetterPlayerDataSourceType.file,
+ "${path}/testvideo.mp4",
+ bytes: videoBytes,
+ videoExtension: "mp4",
+ ),
+ );
+ }
+
+ Future togglePlayPause(int index) async {
+ final controller = controllers[index];
+ if (controller == null) return;
+
+ final isPlaying = await controller.isPlaying() ?? false;
+
+ if (isPlaying) {
+ await controller.pause();
+ if (mounted) {
+ setState(() {
+ playingStates[index] = false;
+ });
+ }
+ print('Paused video $index');
+ } else {
+ await controller.play();
+ if (mounted) {
+ setState(() {
+ playingStates[index] = true;
+ });
+ }
+ print('Playing video $index');
+ }
+ }
+
+ @override
+ void dispose() {
+ // Clean up all controllers
+ for (var controller in controllers.values) {
+ controller.dispose();
+ }
+ super.dispose();
}
- Widget getNext(BuildContext ctx, int index) {
- but[(index) % 3] = getController();
- var ctl = but[index % 3];
- ctl.play();
+ Widget buildVideoPage(BuildContext context, int index) {
+ // Create controller if it doesn't exist for this index
+ if (!controllers.containsKey(index)) {
+ controllers[index] = createController();
+ playingStates[index] = true;
+ }
+
+ // Get the controller for this page
+ final controller = controllers[index]!;
- var prevCtl = but[(index - 1) % 3];
- prevCtl?.pause();
- sleepthing(prevCtl);
+ // Get play state for this page
+ final isPlaying = playingStates[index] ?? true;
return CupertinoPageScaffold(
child: GestureDetector(
child: Stack(
children: [
- BetterPlayer(controller: ctl),
+ BetterPlayer(controller: controller),
if (!isPlaying)
Center(
child: Icon(
CupertinoIcons.play_arrow_solid,
size: 50,
- color: CupertinoColors.black,
+ color: CupertinoColors.white.withOpacity(0.8),
),
)
],
),
- onTap: () {
- BetterPlayerController.preCache(exampleManifestUrl);
- //ctl.isPlaying() ? ctl.pause() : ctl.play();
- },
+ onTap: () => togglePlayPause(index),
),
);
}
+ void onPageChanged(int index) {
+ // Pause previous page's video
+ if (controllers.containsKey(currentPageIndex)) {
+ controllers[currentPageIndex]?.pause();
+ if (mounted) {
+ setState(() {
+ playingStates[currentPageIndex] = false;
+ });
+ }
+ }
+
+ // Play current page's video
+ if (controllers.containsKey(index)) {
+ controllers[index]?.play();
+ if (mounted) {
+ setState(() {
+ playingStates[index] = true;
+ currentPageIndex = index;
+ });
+ }
+ }
+
+ // Clean up controllers that are far away (memory management)
+ cleanupDistantControllers(index);
+ }
+
+ void cleanupDistantControllers(int currentIndex) {
+ // Keep only nearby controllers (current, previous, next)
+ final keysToKeep = [currentIndex - 1, currentIndex, currentIndex + 1];
+
+ final keysToDispose =
+ controllers.keys.where((key) => !keysToKeep.contains(key)).toList();
+
+ for (final key in keysToDispose) {
+ controllers[key]?.dispose();
+ controllers.remove(key);
+ playingStates.remove(key);
+ }
+ }
+
@override
Widget build(BuildContext context) {
return CupertinoApp(
- debugShowCheckedModeBanner: false,
- home: Directionality(
- textDirection: TextDirection.ltr,
- child: PageView.builder(
- itemBuilder: getNext,
- scrollDirection: Axis.vertical,
- )));
+ debugShowCheckedModeBanner: false,
+ home: Directionality(
+ textDirection: TextDirection.ltr,
+ child: isLoading
+ ? Center(
+ child: CupertinoActivityIndicator(),
+ )
+ : PageView.builder(
+ itemBuilder: buildVideoPage,
+ scrollDirection: Axis.vertical,
+ onPageChanged: onPageChanged,
+ ),
+ ),
+ );
}
}
diff --git a/example/pubspec.lock b/example/pubspec.lock
index adb82c7..0dc48a2 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -5,9 +5,10 @@ packages:
dependency: transitive
description:
name: async
- url: "https://pub.dartlang.org"
+ sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
+ url: "https://pub.dev"
source: hosted
- version: "2.8.2"
+ version: "2.11.0"
better_player:
dependency: "direct main"
description:
@@ -19,72 +20,66 @@ packages:
dependency: transitive
description:
name: boolean_selector
- url: "https://pub.dartlang.org"
+ sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
+ url: "https://pub.dev"
source: hosted
- version: "2.1.0"
+ version: "2.1.1"
characters:
dependency: transitive
description:
name: characters
- url: "https://pub.dartlang.org"
+ sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
+ url: "https://pub.dev"
source: hosted
- version: "1.2.0"
- charcode:
- dependency: transitive
- description:
- name: charcode
- url: "https://pub.dartlang.org"
- source: hosted
- version: "1.3.1"
+ version: "1.3.0"
clock:
dependency: transitive
description:
name: clock
- url: "https://pub.dartlang.org"
+ sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
+ url: "https://pub.dev"
source: hosted
- version: "1.1.0"
+ version: "1.1.1"
collection:
dependency: transitive
description:
name: collection
- url: "https://pub.dartlang.org"
+ sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
+ url: "https://pub.dev"
source: hosted
- version: "1.16.0"
+ version: "1.18.0"
csslib:
dependency: transitive
description:
name: csslib
- url: "https://pub.dartlang.org"
+ sha256: "831883fb353c8bdc1d71979e5b342c7d88acfbc643113c14ae51e2442ea0f20f"
+ url: "https://pub.dev"
source: hosted
- version: "0.17.2"
+ version: "0.17.3"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
- url: "https://pub.dartlang.org"
+ sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
+ url: "https://pub.dev"
source: hosted
- version: "1.0.5"
+ version: "1.0.8"
fake_async:
dependency: transitive
description:
name: fake_async
- url: "https://pub.dartlang.org"
+ sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
+ url: "https://pub.dev"
source: hosted
- version: "1.3.0"
+ version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
- url: "https://pub.dartlang.org"
- source: hosted
- version: "2.0.1"
- file:
- dependency: transitive
- description:
- name: file
- url: "https://pub.dartlang.org"
+ sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
+ url: "https://pub.dev"
source: hosted
- version: "6.1.2"
+ version: "2.1.3"
flutter:
dependency: "direct main"
description: flutter
@@ -104,142 +99,170 @@ packages:
dependency: transitive
description:
name: flutter_widget_from_html_core
- url: "https://pub.dartlang.org"
+ sha256: f519d7c454f514d54079aca423d6635756bf4ab520ffdc32f770a2a2faf89ef9
+ url: "https://pub.dev"
source: hosted
version: "0.6.2"
html:
dependency: transitive
description:
name: html
- url: "https://pub.dartlang.org"
+ sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
+ url: "https://pub.dev"
source: hosted
- version: "0.15.0"
+ version: "0.15.4"
http:
dependency: "direct main"
description:
name: http
- url: "https://pub.dartlang.org"
+ sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
+ url: "https://pub.dev"
source: hosted
- version: "0.13.4"
+ version: "1.3.0"
http_parser:
dependency: transitive
description:
name: http_parser
- url: "https://pub.dartlang.org"
+ sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
+ url: "https://pub.dev"
source: hosted
- version: "4.0.1"
+ version: "4.0.2"
js:
dependency: transitive
description:
name: js
- url: "https://pub.dartlang.org"
+ sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.6.7"
+ leak_tracker:
+ dependency: transitive
+ description:
+ name: leak_tracker
+ sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
+ url: "https://pub.dev"
source: hosted
- version: "0.6.4"
+ version: "10.0.5"
+ leak_tracker_flutter_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_flutter_testing
+ sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.5"
+ leak_tracker_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_testing
+ sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.1"
matcher:
dependency: transitive
description:
name: matcher
- url: "https://pub.dartlang.org"
+ sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
+ url: "https://pub.dev"
source: hosted
- version: "0.12.11"
+ version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
- url: "https://pub.dartlang.org"
+ sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
+ url: "https://pub.dev"
source: hosted
- version: "0.1.4"
+ version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
- url: "https://pub.dartlang.org"
+ sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
+ url: "https://pub.dev"
source: hosted
- version: "1.7.0"
+ version: "1.15.0"
path:
dependency: transitive
description:
name: path
- url: "https://pub.dartlang.org"
+ sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
+ url: "https://pub.dev"
source: hosted
- version: "1.8.1"
+ version: "1.9.0"
path_provider:
dependency: "direct main"
description:
name: path_provider
- url: "https://pub.dartlang.org"
+ sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
+ url: "https://pub.dev"
source: hosted
- version: "2.0.11"
+ version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
- url: "https://pub.dartlang.org"
+ sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2"
+ url: "https://pub.dev"
source: hosted
- version: "2.0.16"
- path_provider_ios:
+ version: "2.2.15"
+ path_provider_foundation:
dependency: transitive
description:
- name: path_provider_ios
- url: "https://pub.dartlang.org"
+ name: path_provider_foundation
+ sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
+ url: "https://pub.dev"
source: hosted
- version: "2.0.10"
+ version: "2.4.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
- url: "https://pub.dartlang.org"
- source: hosted
- version: "2.1.7"
- path_provider_macos:
- dependency: transitive
- description:
- name: path_provider_macos
- url: "https://pub.dartlang.org"
+ sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
+ url: "https://pub.dev"
source: hosted
- version: "2.0.6"
+ version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
- url: "https://pub.dartlang.org"
+ sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
+ url: "https://pub.dev"
source: hosted
- version: "2.0.4"
+ version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
- url: "https://pub.dartlang.org"
+ sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
+ url: "https://pub.dev"
source: hosted
- version: "2.1.0"
+ version: "2.3.0"
pedantic:
dependency: transitive
description:
name: pedantic
- url: "https://pub.dartlang.org"
+ sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602"
+ url: "https://pub.dev"
source: hosted
version: "1.11.1"
platform:
dependency: transitive
description:
name: platform
- url: "https://pub.dartlang.org"
+ sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
+ url: "https://pub.dev"
source: hosted
- version: "3.1.0"
+ version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
- url: "https://pub.dartlang.org"
- source: hosted
- version: "2.1.2"
- process:
- dependency: transitive
- description:
- name: process
- url: "https://pub.dartlang.org"
+ sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
+ url: "https://pub.dev"
source: hosted
- version: "4.2.4"
+ version: "2.1.8"
sky_engine:
dependency: transitive
description: flutter
@@ -249,114 +272,146 @@ packages:
dependency: transitive
description:
name: source_span
- url: "https://pub.dartlang.org"
+ sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
+ url: "https://pub.dev"
source: hosted
- version: "1.8.2"
+ version: "1.10.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
- url: "https://pub.dartlang.org"
+ sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
+ url: "https://pub.dev"
source: hosted
- version: "1.10.0"
+ version: "1.11.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
- url: "https://pub.dartlang.org"
+ sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
+ url: "https://pub.dev"
source: hosted
- version: "2.1.0"
+ version: "2.1.2"
string_scanner:
dependency: transitive
description:
name: string_scanner
- url: "https://pub.dartlang.org"
+ sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
+ url: "https://pub.dev"
source: hosted
- version: "1.1.0"
+ version: "1.2.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
- url: "https://pub.dartlang.org"
+ sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
+ url: "https://pub.dev"
source: hosted
- version: "1.2.0"
+ version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
- url: "https://pub.dartlang.org"
+ sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
+ url: "https://pub.dev"
source: hosted
- version: "0.4.9"
+ version: "0.7.2"
typed_data:
dependency: transitive
description:
name: typed_data
- url: "https://pub.dartlang.org"
+ sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
+ url: "https://pub.dev"
source: hosted
- version: "1.3.1"
+ version: "1.4.0"
vector_math:
dependency: transitive
description:
name: vector_math
- url: "https://pub.dartlang.org"
+ sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
+ url: "https://pub.dev"
source: hosted
- version: "2.1.2"
+ version: "2.1.4"
visibility_detector:
dependency: transitive
description:
name: visibility_detector
- url: "https://pub.dartlang.org"
+ sha256: ec932527913f32f65aa01d3a393504240b9e9021ecc77123f017755605e48832
+ url: "https://pub.dev"
source: hosted
version: "0.2.2"
+ vm_service:
+ dependency: transitive
+ description:
+ name: vm_service
+ sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc
+ url: "https://pub.dev"
+ source: hosted
+ version: "14.2.4"
wakelock:
dependency: transitive
description:
name: wakelock
- url: "https://pub.dartlang.org"
+ sha256: da22c0789e1f849bec43688a52f2290f4e66268056f7cd77cb71245aef4917a0
+ url: "https://pub.dev"
source: hosted
version: "0.5.6"
wakelock_macos:
dependency: transitive
description:
name: wakelock_macos
- url: "https://pub.dartlang.org"
+ sha256: "047c6be2f88cb6b76d02553bca5a3a3b95323b15d30867eca53a19a0a319d4cd"
+ url: "https://pub.dev"
source: hosted
version: "0.4.0"
wakelock_platform_interface:
dependency: transitive
description:
name: wakelock_platform_interface
- url: "https://pub.dartlang.org"
+ sha256: "1f4aeb81fb592b863da83d2d0f7b8196067451e4df91046c26b54a403f9de621"
+ url: "https://pub.dev"
source: hosted
version: "0.3.0"
wakelock_web:
dependency: transitive
description:
name: wakelock_web
- url: "https://pub.dartlang.org"
+ sha256: "1b256b811ee3f0834888efddfe03da8d18d0819317f20f6193e2922b41a501b5"
+ url: "https://pub.dev"
source: hosted
version: "0.4.0"
wakelock_windows:
dependency: transitive
description:
name: wakelock_windows
- url: "https://pub.dartlang.org"
+ sha256: "857f77b3fe6ae82dd045455baa626bc4b93cb9bb6c86bf3f27c182167c3a5567"
+ url: "https://pub.dev"
source: hosted
- version: "0.2.0"
+ version: "0.2.1"
+ web:
+ dependency: transitive
+ description:
+ name: web
+ sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.1"
win32:
dependency: transitive
description:
name: win32
- url: "https://pub.dartlang.org"
+ sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4
+ url: "https://pub.dev"
source: hosted
- version: "2.7.0"
+ version: "3.1.4"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
- url: "https://pub.dartlang.org"
+ sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
+ url: "https://pub.dev"
source: hosted
- version: "0.2.0+1"
+ version: "1.1.0"
sdks:
- dart: ">=2.17.0 <3.0.0"
- flutter: ">=3.0.0"
+ dart: ">=3.5.0 <4.0.0"
+ flutter: ">=3.24.0"
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index a09009d..1a2d715 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -18,7 +18,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
- sdk: ">=2.7.0 <3.0.0"
+ sdk: ">=2.12.0 <3.0.0"
dependencies:
flutter:
@@ -29,7 +29,7 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.0
- http: ^0.13.3
+ http: ^1.3.0
path_provider: ^2.0.11
dev_dependencies:
@@ -50,6 +50,7 @@ flutter:
assets:
- image/yoyo_logo.png
- assets/testtest.mp4
+ - assets/testtest2.mp4
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
diff --git a/ios/Classes/FLTBetterPlayerPlugin.h b/ios/Classes/FLTBetterPlayerPlugin.h
deleted file mode 100644
index 7d01b81..0000000
--- a/ios/Classes/FLTBetterPlayerPlugin.h
+++ /dev/null
@@ -1,10 +0,0 @@
-// Copyright 2017 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#import
-
-@class CacheManager;
-
-@interface FLTBetterPlayerPlugin : NSObject
-@end
diff --git a/ios/Classes/FLTBetterPlayerPlugin.m b/ios/Classes/FLTBetterPlayerPlugin.m
deleted file mode 100644
index 8a25766..0000000
--- a/ios/Classes/FLTBetterPlayerPlugin.m
+++ /dev/null
@@ -1,1190 +0,0 @@
-// Copyright 2017 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#import "FLTBetterPlayerPlugin.h"
-#import
-#import
-#import
-#import
-#import
-#import
-
-#if !__has_feature(objc_arc)
-#error Code Requires ARC.
-#endif
-
-int64_t FLTCMTimeToMillis(CMTime time) {
- if (time.timescale == 0) return 0;
- return time.value * 1000 / time.timescale;
-}
-
-int64_t FLTNSTimeIntervalToMillis(NSTimeInterval interval) {
- return (int64_t)(interval * 1000.0);
-}
-
-@interface FLTFrameUpdater : NSObject
-@property(nonatomic) int64_t textureId;
-@property(nonatomic, weak, readonly) NSObject* registry;
-- (void)onDisplayLink:(CADisplayLink*)link;
-@end
-
-@implementation FLTFrameUpdater
-- (FLTFrameUpdater*)initWithRegistry:(NSObject*)registry {
- NSAssert(self, @"super init cannot be nil");
- if (self == nil) return nil;
- _registry = registry;
- return self;
-}
-
-- (void)onDisplayLink:(CADisplayLink*)link {
- [_registry textureFrameAvailable:_textureId];
-}
-@end
-
-@interface FLTBetterPlayer : NSObject
-@property(readonly, nonatomic) AVPlayer* player;
-@property(readonly, nonatomic) AVPlayerItemVideoOutput* videoOutput;
-@property(readonly, nonatomic) CADisplayLink* displayLink;
-@property(nonatomic) FlutterEventChannel* eventChannel;
-@property(nonatomic) FlutterEventSink eventSink;
-@property(nonatomic) CGAffineTransform preferredTransform;
-@property(nonatomic, readonly) bool disposed;
-@property(nonatomic, readonly) bool isPlaying;
-@property(nonatomic) bool isSeeking;
-@property(nonatomic) bool isLooping;
-@property(nonatomic, readonly) bool isInitialized;
-@property(nonatomic, readonly) NSString* key;
-@property(nonatomic, readonly) CVPixelBufferRef prevBuffer;
-@property(nonatomic, readonly) int failedCount;
-@property(nonatomic) AVPlayerLayer* _playerLayer;
-@property(nonatomic) bool _observersAdded;
-@property(nonatomic) int stalledCount;
-@property(nonatomic) float playerRate;
-- (void)play;
-- (void)pause;
-- (void)setIsLooping:(bool)isLooping;
-- (void)updatePlayingState;
-- (int64_t) duration;
-- (int64_t) position;
-@end
-
-
-static void* timeRangeContext = &timeRangeContext;
-static void* statusContext = &statusContext;
-static void* playbackLikelyToKeepUpContext = &playbackLikelyToKeepUpContext;
-static void* playbackBufferEmptyContext = &playbackBufferEmptyContext;
-static void* playbackBufferFullContext = &playbackBufferFullContext;
-static void* presentationSizeContext = &presentationSizeContext;
-
-
-#if TARGET_OS_IOS
-API_AVAILABLE(ios(9.0))
-#endif
-
-
-@implementation FLTBetterPlayer
-- (instancetype)initWithFrameUpdater:(FLTFrameUpdater*)frameUpdater {
- self = [super init];
- NSAssert(self, @"super init cannot be nil");
- _isInitialized = false;
- _isPlaying = false;
- _disposed = false;
- _isSeeking = false;
- _player = [[AVPlayer alloc] init];
- _player.actionAtItemEnd = AVPlayerActionAtItemEndNone;
-
- ///Fix for loading large videos
- if (@available(iOS 10.0, *)) {
- _player.automaticallyWaitsToMinimizeStalling = false;
- }
- _displayLink = [CADisplayLink displayLinkWithTarget:frameUpdater
- selector:@selector(onDisplayLink:)];
- [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
- _displayLink.paused = YES;
- self._observersAdded = false;
- return self;
-}
-
-- (void)addObservers:(AVPlayerItem*)item {
- if (!self._observersAdded){
- [_player addObserver:self forKeyPath:@"rate" options:0 context:nil];
- [item addObserver:self forKeyPath:@"loadedTimeRanges" options:0 context:timeRangeContext];
- [item addObserver:self forKeyPath:@"status" options:0 context:statusContext];
- [item addObserver:self forKeyPath:@"presentationSize" options:0 context:presentationSizeContext];
- [item addObserver:self
- forKeyPath:@"playbackLikelyToKeepUp"
- options:0
- context:playbackLikelyToKeepUpContext];
- [item addObserver:self
- forKeyPath:@"playbackBufferEmpty"
- options:0
- context:playbackBufferEmptyContext];
- [item addObserver:self
- forKeyPath:@"playbackBufferFull"
- options:0
- context:playbackBufferFullContext];
- [[NSNotificationCenter defaultCenter] addObserver:self
- selector:@selector(itemDidPlayToEndTime:)
- name:AVPlayerItemDidPlayToEndTimeNotification
- object:item];
- self._observersAdded = true;
- }
-}
-
-- (void)removeVideoOutput {
- _videoOutput = nil;
- if (_player.currentItem == nil) {
- return;
- }
- NSArray* outputs = [[_player currentItem] outputs];
- for (AVPlayerItemOutput* output in outputs) {
- [[_player currentItem] removeOutput:output];
- }
-}
-
-- (void)clear {
- _displayLink.paused = YES;
- _isInitialized = false;
- _isPlaying = false;
- _disposed = false;
- _videoOutput = nil;
- _failedCount = 0;
- _key = nil;
- if (_player.currentItem == nil) {
- return;
- }
-
- if (_player.currentItem == nil) {
- return;
- }
-
- [self removeObservers];
- AVAsset* asset = [_player.currentItem asset];
- [asset cancelLoading];
-}
-
-- (void) removeObservers{
- if (self._observersAdded){
- [_player removeObserver:self forKeyPath:@"rate" context:nil];
- [[_player currentItem] removeObserver:self forKeyPath:@"status" context:statusContext];
- [[_player currentItem] removeObserver:self forKeyPath:@"presentationSize" context:presentationSizeContext];
- [[_player currentItem] removeObserver:self
- forKeyPath:@"loadedTimeRanges"
- context:timeRangeContext];
- [[_player currentItem] removeObserver:self
- forKeyPath:@"playbackLikelyToKeepUp"
- context:playbackLikelyToKeepUpContext];
- [[_player currentItem] removeObserver:self
- forKeyPath:@"playbackBufferEmpty"
- context:playbackBufferEmptyContext];
- [[_player currentItem] removeObserver:self
- forKeyPath:@"playbackBufferFull"
- context:playbackBufferFullContext];
- [[NSNotificationCenter defaultCenter] removeObserver:self];
- self._observersAdded = false;
- }
-}
-
-- (void)itemDidPlayToEndTime:(NSNotification*)notification {
- if (_isLooping) {
- // to prevent reloading the entire video again, we seek to 1
- // My guess is that this happens because the first millisecond isn't buffered at the start
- // so when we seek to 0, it will buffer as if nothing was previously buffered
- [self seekTo:1];
- } else {
- if (_eventSink) {
- _eventSink(@{@"event" : @"completed", @"key" : _key});
- [ self removeObservers];
-
- }
- [_player pause];
- _isPlaying = false;
- _displayLink.paused = YES;
- }
-}
-
-
-static inline CGFloat radiansToDegrees(CGFloat radians) {
- // Input range [-pi, pi] or [-180, 180]
- CGFloat degrees = GLKMathRadiansToDegrees((float)radians);
- if (degrees < 0) {
- // Convert -90 to 270 and -180 to 180
- return degrees + 360;
- }
- // Output degrees in between [0, 360[
- return degrees;
-};
-
-- (AVMutableVideoComposition*)getVideoCompositionWithTransform:(CGAffineTransform)transform
- withAsset:(AVAsset*)asset
- withVideoTrack:(AVAssetTrack*)videoTrack {
- AVMutableVideoCompositionInstruction* instruction =
- [AVMutableVideoCompositionInstruction videoCompositionInstruction];
- instruction.timeRange = CMTimeRangeMake(kCMTimeZero, [asset duration]);
- AVMutableVideoCompositionLayerInstruction* layerInstruction =
- [AVMutableVideoCompositionLayerInstruction
- videoCompositionLayerInstructionWithAssetTrack:videoTrack];
- [layerInstruction setTransform:_preferredTransform atTime:kCMTimeZero];
-
- AVMutableVideoComposition* videoComposition = [AVMutableVideoComposition videoComposition];
- instruction.layerInstructions = @[ layerInstruction ];
- videoComposition.instructions = @[ instruction ];
-
- // If in portrait mode, switch the width and height of the video
- CGFloat width = videoTrack.naturalSize.width;
- CGFloat height = videoTrack.naturalSize.height;
- NSInteger rotationDegrees =
- (NSInteger)round(radiansToDegrees(atan2(_preferredTransform.b, _preferredTransform.a)));
- if (rotationDegrees == 90 || rotationDegrees == 270) {
- width = videoTrack.naturalSize.height;
- height = videoTrack.naturalSize.width;
- }
- videoComposition.renderSize = CGSizeMake(width, height);
-
- // TODO(@recastrodiaz): should we use videoTrack.nominalFrameRate ?
- // Currently set at a constant 30 FPS
- videoComposition.frameDuration = CMTimeMake(1, 30);
-
- return videoComposition;
-}
-
-- (void)addVideoOutput {
- if (_player.currentItem == nil) {
- return;
- }
-
- if (_videoOutput) {
- NSArray* outputs = [[_player currentItem] outputs];
- for (AVPlayerItemOutput* output in outputs) {
- if (output == _videoOutput) {
- return;
- }
- }
- }
-
- NSDictionary* pixBuffAttributes = @{
- (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA),
- (id)kCVPixelBufferIOSurfacePropertiesKey : @{}
- };
- _videoOutput = [[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:pixBuffAttributes];
- [_player.currentItem addOutput:_videoOutput];
-}
-
-- (CGAffineTransform)fixTransform:(AVAssetTrack*)videoTrack {
- CGAffineTransform transform = videoTrack.preferredTransform;
- // TODO(@recastrodiaz): why do we need to do this? Why is the preferredTransform incorrect?
- // At least 2 user videos show a black screen when in portrait mode if we directly use the
- // videoTrack.preferredTransform Setting tx to the height of the video instead of 0, properly
- // displays the video https://github.com/flutter/flutter/issues/17606#issuecomment-413473181
- if (transform.tx == 0 && transform.ty == 0) {
- NSInteger rotationDegrees = (NSInteger)round(radiansToDegrees(atan2(transform.b, transform.a)));
- NSLog(@"TX and TY are 0. Rotation: %ld. Natural width,height: %f, %f", (long)rotationDegrees,
- videoTrack.naturalSize.width, videoTrack.naturalSize.height);
- if (rotationDegrees == 90) {
- NSLog(@"Setting transform tx");
- transform.tx = videoTrack.naturalSize.height;
- transform.ty = 0;
- } else if (rotationDegrees == 270) {
- NSLog(@"Setting transform ty");
- transform.tx = 0;
- transform.ty = videoTrack.naturalSize.width;
- }
- }
- return transform;
-}
-
-- (void)setDataSourceAsset:(NSString*)asset withKey:(NSString*)key overriddenDuration:(int) overriddenDuration{
- NSString* path = [[NSBundle mainBundle] pathForResource:asset ofType:nil];
- return [self setDataSourceURL:[NSURL fileURLWithPath:path] withKey:key withHeaders: @{} withCache: false overriddenDuration:overriddenDuration];
-}
-
-- (void)setDataSourceURL:(NSURL*)url withKey:(NSString*)key withHeaders:(NSDictionary*)headers withCache:(BOOL)useCache overriddenDuration:(int) overriddenDuration{
- if (headers == [NSNull null]){
- headers = @{};
- }
- AVPlayerItem* item;
- if (useCache){
- [KTVHTTPCache downloadSetAdditionalHeaders:headers];
- NSURL *proxyURL = [KTVHTTPCache proxyURLWithOriginalURL:url];
- item = [AVPlayerItem playerItemWithURL:proxyURL];
- } else {
- AVURLAsset* asset = [AVURLAsset URLAssetWithURL:url
- options:@{@"AVURLAssetHTTPHeaderFieldsKey" : headers}];
- item = [AVPlayerItem playerItemWithAsset:asset];
- if (@available(iOS 10.0, *)) {
- double k = 3;
- item.preferredForwardBufferDuration = k;
- }
- }
-
- if (@available(iOS 10.0, *) && overriddenDuration > 0) {
- item.forwardPlaybackEndTime = CMTimeMake(overriddenDuration/1000, 1);
- }
-
- return [self setDataSourcePlayerItem:item withKey:key];
-}
-
-- (void)setDataSourcePlayerItem:(AVPlayerItem*)item withKey:(NSString*)key{
- _key = key;
- _stalledCount = 0;
- _playerRate = 1;
- [_player replaceCurrentItemWithPlayerItem:item];
-
- AVAsset* asset = [item asset];
- void (^assetCompletionHandler)(void) = ^{
- if ([asset statusOfValueForKey:@"tracks" error:nil] == AVKeyValueStatusLoaded) {
- NSArray* tracks = [asset tracksWithMediaType:AVMediaTypeVideo];
- if ([tracks count] > 0) {
- AVAssetTrack* videoTrack = tracks[0];
- void (^trackCompletionHandler)(void) = ^{
- if (self->_disposed) return;
- if ([videoTrack statusOfValueForKey:@"preferredTransform"
- error:nil] == AVKeyValueStatusLoaded) {
- // Rotate the video by using a videoComposition and the preferredTransform
- self->_preferredTransform = [self fixTransform:videoTrack];
- // Note:
- // https://developer.apple.com/documentation/avfoundation/avplayeritem/1388818-videocomposition
- // Video composition can only be used with file-based media and is not supported for
- // use with media served using HTTP Live Streaming.
- AVMutableVideoComposition* videoComposition =
- [self getVideoCompositionWithTransform:self->_preferredTransform
- withAsset:asset
- withVideoTrack:videoTrack];
- item.videoComposition = videoComposition;
- }
- };
- [videoTrack loadValuesAsynchronouslyForKeys:@[ @"preferredTransform" ]
- completionHandler:trackCompletionHandler];
- }
- }
- };
-
- [asset loadValuesAsynchronouslyForKeys:@[ @"tracks" ] completionHandler:assetCompletionHandler];
- [self addObservers:item];
-}
-
--(void)handleStalled {
- if (_player.currentItem.playbackLikelyToKeepUp ||
- [self availableDuration] - CMTimeGetSeconds(_player.currentItem.currentTime) > 10.0) {
- // TODO: Check to see if this play statement actually makes sense. Why would we continue playing?
- // Especially if it's already playing?
- //[self play];
- } else {
- _stalledCount++;
- if (_stalledCount > 50){
- _eventSink([FlutterError
- errorWithCode:@"VideoError"
- message:@"Failed to load video: playback stalled"
- details:nil]);
- return;
- }
- [self performSelector:@selector(handleStalled) withObject:nil afterDelay:1];
- }
-}
-
-- (NSTimeInterval) availableDuration
-{
- NSArray *loadedTimeRanges = [[_player currentItem] loadedTimeRanges];
- if (loadedTimeRanges.count > 0){
- CMTimeRange timeRange = [[loadedTimeRanges objectAtIndex:0] CMTimeRangeValue];
- Float64 startSeconds = CMTimeGetSeconds(timeRange.start);
- Float64 durationSeconds = CMTimeGetSeconds(timeRange.duration);
- NSTimeInterval result = startSeconds + durationSeconds;
- return result;
- } else {
- return 0;
- }
-
-}
-
-- (void)observeValueForKeyPath:(NSString*)path
- ofObject:(id)object
- change:(NSDictionary*)change
- context:(void*)context {
-
- if ([path isEqualToString:@"rate"]) {
- if (_player.rate == 0 && //if player rate dropped to 0
- CMTIME_COMPARE_INLINE(_player.currentItem.currentTime, >, kCMTimeZero) && //if video was started
- CMTIME_COMPARE_INLINE(_player.currentItem.currentTime, <, _player.currentItem.duration) && //but not yet finished
- _isPlaying) { //instance variable to handle overall state (changed to YES when user triggers playback)
- [self handleStalled];
- }
- }
-
- if (context == timeRangeContext) {
- if (_eventSink != nil) {
- NSMutableArray*>* values = [[NSMutableArray alloc] init];
- for (NSValue* rangeValue in [object loadedTimeRanges]) {
- CMTimeRange range = [rangeValue CMTimeRangeValue];
- int64_t start = FLTCMTimeToMillis(range.start);
- int64_t end = start + FLTCMTimeToMillis(range.duration);
- if (!CMTIME_IS_INVALID(_player.currentItem.forwardPlaybackEndTime)) {
- int64_t endTime = FLTCMTimeToMillis(_player.currentItem.forwardPlaybackEndTime);
- if (end > endTime){
- end = endTime;
- }
- }
-
- [values addObject:@[ @(start), @(end) ]];
- }
- _eventSink(@{@"event" : @"bufferingUpdate", @"values" : values, @"key" : _key});
- }
- }
- else if (context == presentationSizeContext){
- [self onReadyToPlay];
- }
-
- else if (context == statusContext) {
- AVPlayerItem* item = (AVPlayerItem*)object;
- switch (item.status) {
- case AVPlayerItemStatusFailed:
- NSLog(@"Failed to load video:");
- NSLog(item.error.debugDescription);
-
- if (_eventSink != nil) {
- _eventSink([FlutterError
- errorWithCode:@"VideoError"
- message:[@"Failed to load video: "
- stringByAppendingString:[item.error localizedDescription]]
- details:nil]);
- }
- break;
- case AVPlayerItemStatusUnknown:
- break;
- case AVPlayerItemStatusReadyToPlay:
- [self onReadyToPlay];
- break;
- }
- } else if (context == playbackLikelyToKeepUpContext) {
- if ([[_player currentItem] isPlaybackLikelyToKeepUp]) {
- [self updatePlayingState];
- if (_eventSink != nil) {
- _eventSink(@{@"event" : @"bufferingEnd", @"key" : _key});
- }
- }
- } else if (context == playbackBufferEmptyContext) {
- if (_eventSink != nil) {
- _eventSink(@{@"event" : @"bufferingStart", @"key" : _key});
- }
- } else if (context == playbackBufferFullContext) {
- if (_eventSink != nil) {
- _eventSink(@{@"event" : @"bufferingEnd", @"key" : _key});
- }
- }
-}
-
-- (void)updatePlayingState {
- if (!_isInitialized || !_key) {
- NSLog(@"not initalized and paused!!");
- _displayLink.paused = YES;
- return;
- }
- if (!self._observersAdded){
- [self addObservers:[_player currentItem]];
- }
-
- if (_isPlaying) {
- if (@available(iOS 10.0, *)) {
- [_player playImmediatelyAtRate:1.0];
- _player.rate = _playerRate;
- } else {
- [_player play];
- _player.rate = _playerRate;
- }
- } else {
- [_player pause];
- }
- // set display link appropriately, however, don't pause
- // the display link of the video isSeeking, as we don't
- // want to block the UI from updating during seeking.
- if (_isPlaying) {
- _displayLink.paused = !_isPlaying;
- } else if (!_isSeeking) {
- _displayLink.paused = !_isPlaying;
- }
-}
-
-- (void)onReadyToPlay {
- if (_eventSink && !_isInitialized && _key) {
- if (!_player.currentItem) {
- return;
- }
- if (_player.status != AVPlayerStatusReadyToPlay) {
- return;
- }
-
- CGSize size = [_player currentItem].presentationSize;
- CGFloat width = size.width;
- CGFloat height = size.height;
-
-
- AVAsset *asset = _player.currentItem.asset;
- bool onlyAudio = [[asset tracksWithMediaType:AVMediaTypeVideo] count] == 0;
-
- // The player has not yet initialized.
- if (!onlyAudio && height == CGSizeZero.height && width == CGSizeZero.width) {
- return;
- }
- const BOOL isLive = CMTIME_IS_INDEFINITE([_player currentItem].duration);
- // The player may be initialized but still needs to determine the duration.
- if (isLive == false && [self duration] == 0) {
- return;
- }
-
- //Fix from https://github.com/flutter/flutter/issues/66413
- AVPlayerItemTrack *track = [self.player currentItem].tracks.firstObject;
- CGSize naturalSize = track.assetTrack.naturalSize;
- CGAffineTransform prefTrans = track.assetTrack.preferredTransform;
- CGSize realSize = CGSizeApplyAffineTransform(naturalSize, prefTrans);
-
- _isInitialized = true;
- [self addVideoOutput];
- [self updatePlayingState];
- _eventSink(@{
- @"event" : @"initialized",
- @"duration" : @([self duration]),
- @"width" : @(fabs(realSize.width) ? : width),
- @"height" : @(fabs(realSize.height) ? : height),
- @"key" : _key
- });
- }
-}
-
-- (void)play {
- _stalledCount = 0;
- _isPlaying = true;
- [self updatePlayingState];
- if (@available(iOS 10.0, *)) {
- AVPlayerItem *k = [self player].currentItem;
- [[self player] replaceCurrentItemWithPlayerItem:k];
- }
-}
-
-- (void)pause {
- _isPlaying = false;
- [self updatePlayingState];
-}
-
-- (int64_t)position {
- return FLTCMTimeToMillis([_player currentTime]);
-}
-
-- (int64_t)absolutePosition {
- return FLTNSTimeIntervalToMillis([[[_player currentItem] currentDate] timeIntervalSince1970]);
-}
-
-- (int64_t)duration {
- CMTime time;
- if (@available(iOS 13, *)) {
- time = [[_player currentItem] duration];
- } else {
- time = [[[_player currentItem] asset] duration];
- }
- if (!CMTIME_IS_INVALID(_player.currentItem.forwardPlaybackEndTime)) {
- time = [[_player currentItem] forwardPlaybackEndTime];
- }
-
- return FLTCMTimeToMillis(time);
-}
-
-- (void)seekTo:(int)location {
- ///When player is playing, pause video, seek to new position and start again. This will prevent issues with seekbar jumps.
- // bool wasPlaying = _isPlaying;
- // if (wasPlaying){
- // [_player pause];
- // }
- _isSeeking = true;
- _displayLink.paused = NO; // to see seeking in video output
- [_player seekToTime:CMTimeMake(location, 1000)
- toleranceBefore:kCMTimeZero
- toleranceAfter:kCMTimeZero
- completionHandler:^(BOOL finished){
-
- // run async query to not run on main UI thread
- // really buggy otherwise
- dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- dispatch_async(queue, ^{
- // sleep for 2 frames (time is defined by displayLink duration)
- [NSThread sleepForTimeInterval:2*_displayLink.duration];
- _isSeeking = false;
- // set display link as appropriate
- _displayLink.paused = !_isPlaying;
- });
- }];
-
-}
-
-- (void)setIsLooping:(bool)isLooping {
- _isLooping = isLooping;
-}
-
-- (void)setVolume:(double)volume {
- _player.volume = (float)((volume < 0.0) ? 0.0 : ((volume > 1.0) ? 1.0 : volume));
-}
-
-- (void)setSpeed:(double)speed result:(FlutterResult)result {
- if (speed == 1.0 || speed == 0.0) {
- _playerRate = 1;
- result(nil);
- } else if (speed < 0 || speed > 2.0) {
- result([FlutterError errorWithCode:@"unsupported_speed"
- message:@"Speed must be >= 0.0 and <= 2.0"
- details:nil]);
- } else if ((speed > 1.0 && _player.currentItem.canPlayFastForward) ||
- (speed < 1.0 && _player.currentItem.canPlaySlowForward)) {
- _playerRate = speed;
- result(nil);
- } else {
- if (speed > 1.0) {
- result([FlutterError errorWithCode:@"unsupported_fast_forward"
- message:@"This video cannot be played fast forward"
- details:nil]);
- } else {
- result([FlutterError errorWithCode:@"unsupported_slow_forward"
- message:@"This video cannot be played slow forward"
- details:nil]);
- }
- }
-
- if (_isPlaying){
- _player.rate = _playerRate;
- }
-}
-
-
-- (void)setTrackParameters:(int) width: (int) height: (int)bitrate {
- _player.currentItem.preferredPeakBitRate = bitrate;
- if (@available(iOS 11.0, *)) {
- if (width == 0 && height == 0){
- _player.currentItem.preferredMaximumResolution = CGSizeZero;
- } else {
- _player.currentItem.preferredMaximumResolution = CGSizeMake(width, height);
- }
- }
-}
-
-#if TARGET_OS_IOS
-
-- (void)usePlayerLayer: (CGRect) frame
-{
- if( _player )
- {
- // Create new controller passing reference to the AVPlayerLayer
- self._playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player];
- UIViewController* vc = [[[UIApplication sharedApplication] keyWindow] rootViewController];
- self._playerLayer.frame = frame;
- self._playerLayer.needsDisplayOnBoundsChange = YES;
- // [self._playerLayer addObserver:self forKeyPath:readyForDisplayKeyPath options:NSKeyValueObservingOptionNew context:nil];
- [vc.view.layer addSublayer:self._playerLayer];
- vc.view.layer.needsDisplayOnBoundsChange = YES;
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)),
- dispatch_get_main_queue(), ^{
- });
- }
-}
-#endif
-
-#if TARGET_OS_IOS
-
-- (void) setAudioTrack:(NSString*) name index:(int) index{
- AVMediaSelectionGroup *audioSelectionGroup = [[[_player currentItem] asset] mediaSelectionGroupForMediaCharacteristic: AVMediaCharacteristicAudible];
- NSArray* options = audioSelectionGroup.options;
-
-
- for (int i = 0; i < [options count]; i++) {
- AVMediaSelectionOption* option = [options objectAtIndex:i];
- NSArray *metaDatas = [AVMetadataItem metadataItemsFromArray:option.commonMetadata withKey:@"title" keySpace:@"comn"];
- if (metaDatas.count > 0) {
- NSString *title = ((AVMetadataItem*)[metaDatas objectAtIndex:0]).stringValue;
- if (title == name && index == i ){
- [[_player currentItem] selectMediaOption:option inMediaSelectionGroup: audioSelectionGroup];
- }
- }
-
- }
-
-}
-
-- (void)setMixWithOthers:(bool)mixWithOthers {
- if (mixWithOthers) {
- [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
- withOptions:AVAudioSessionCategoryOptionMixWithOthers
- error:nil];
- } else {
- [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
- }
-}
-
-
-#endif
-// This workaround if you will change dataSource. Flutter engine caches CVPixelBufferRef and if you
-// return NULL from method copyPixelBuffer Flutter will use cached CVPixelBufferRef. If you will
-// change your datasource you can see frame from previeous video. Thats why we should return
-// trasparent frame for this situation
-- (CVPixelBufferRef)prevTransparentBuffer {
- if (_prevBuffer) {
- CVPixelBufferLockBaseAddress(_prevBuffer, 0);
-
- int bufferWidth = CVPixelBufferGetWidth(_prevBuffer);
- int bufferHeight = CVPixelBufferGetHeight(_prevBuffer);
- unsigned char* pixel = (unsigned char*)CVPixelBufferGetBaseAddress(_prevBuffer);
-
- for (int row = 0; row < bufferHeight; row++) {
- for (int column = 0; column < bufferWidth; column++) {
- pixel[0] = 0;
- pixel[1] = 0;
- pixel[2] = 0;
- pixel[3] = 0;
- pixel += 4;
- }
- }
- CVPixelBufferUnlockBaseAddress(_prevBuffer, 0);
- return _prevBuffer;
- }
- return _prevBuffer;
-}
-
-
-- (CVPixelBufferRef)copyPixelBuffer {
- //Disabled because of black frame issue
- /*if (!_videoOutput || !_isInitialized || !_isPlaying || !_key || ![_player currentItem] ||
- ![[_player currentItem] isPlaybackLikelyToKeepUp]) {
- return [self prevTransparentBuffer];
- }*/
-
- CMTime outputItemTime = [_videoOutput itemTimeForHostTime:CACurrentMediaTime()];
- if ([_videoOutput hasNewPixelBufferForItemTime:outputItemTime]) {
- _failedCount = 0;
- _prevBuffer = [_videoOutput copyPixelBufferForItemTime:outputItemTime itemTimeForDisplay:NULL];
- return _prevBuffer;
- } else {
- // AVPlayerItemVideoOutput.hasNewPixelBufferForItemTime doesn't work correctly
- _failedCount++;
- if (_failedCount > 100) {
- _failedCount = 0;
- [self removeVideoOutput];
- [self addVideoOutput];
- }
- return NULL;
- }
-}
-
-- (void)onTextureUnregistered {
- dispatch_async(dispatch_get_main_queue(), ^{
- [self dispose];
- });
-}
-
-- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments {
- _eventSink = nil;
- return nil;
-}
-
-- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments
- eventSink:(nonnull FlutterEventSink)events {
- _eventSink = events;
- // TODO(@recastrodiaz): remove the line below when the race condition is resolved:
- // https://github.com/flutter/flutter/issues/21483
- // This line ensures the 'initialized' event is sent when the event
- // 'AVPlayerItemStatusReadyToPlay' fires before _eventSink is set (this function
- // onListenWithArguments is called)
- [self onReadyToPlay];
- return nil;
-}
-
-/// This method allows you to dispose without touching the event channel. This
-/// is useful for the case where the Engine is in the process of deconstruction
-/// so the channel is going to die or is already dead.
-- (void)disposeSansEventChannel {
- @try{
- [self clear];
- [_displayLink invalidate];
- }
- @catch(NSException *exception) {
- NSLog(exception.debugDescription);
- }
-}
-
-- (void)dispose {
- [self disposeSansEventChannel];
- [_eventChannel setStreamHandler:nil];
- _disposed = true;
-}
-
-@end
-
-@interface FLTBetterPlayerPlugin ()
-@property(readonly, weak, nonatomic) NSObject* registry;
-@property(readonly, weak, nonatomic) NSObject* messenger;
-@property(readonly, strong, nonatomic) NSMutableDictionary* players;
-@property(readonly, strong, nonatomic) NSObject* registrar;
-@property(readonly, strong, nonatomic) CacheManager* cacheManager;
-@end
-
-@implementation FLTBetterPlayerPlugin
-NSMutableDictionary* _dataSourceDict;
-NSMutableDictionary* _timeObserverIdDict;
-NSMutableDictionary* _artworkImageDict;
-
-
-+ (void)registerWithRegistrar:(NSObject*)registrar {
- FlutterMethodChannel* channel =
- [FlutterMethodChannel methodChannelWithName:@"better_player_channel"
- binaryMessenger:[registrar messenger]];
- FLTBetterPlayerPlugin* instance = [[FLTBetterPlayerPlugin alloc] initWithRegistrar:registrar];
- [registrar addMethodCallDelegate:instance channel:channel];
- [registrar publish:instance];
-}
-
-- (instancetype)initWithRegistrar:(NSObject*)registrar {
- self = [super init];
- NSAssert(self, @"super init cannot be nil");
- _registry = [registrar textures];
- _messenger = [registrar messenger];
- _registrar = registrar;
- _players = [NSMutableDictionary dictionaryWithCapacity:1];
- _timeObserverIdDict = [NSMutableDictionary dictionary];
- _artworkImageDict = [NSMutableDictionary dictionary];
- _dataSourceDict = [NSMutableDictionary dictionary];
- [KTVHTTPCache proxyStart:nil];
- _cacheManager = [[CacheManager alloc] init];
- return self;
-}
-
-- (void)detachFromEngineForRegistrar:(NSObject*)registrar {
- for (NSNumber* textureId in _players.allKeys) {
- FLTBetterPlayer* player = _players[textureId];
- [player disposeSansEventChannel];
- }
- [_players removeAllObjects];
-}
-
-- (void)onPlayerSetup:(FLTBetterPlayer*)player
- frameUpdater:(FLTFrameUpdater*)frameUpdater
- result:(FlutterResult)result {
- int64_t textureId = [_registry registerTexture:player];
- frameUpdater.textureId = textureId;
- FlutterEventChannel* eventChannel = [FlutterEventChannel
- eventChannelWithName:[NSString stringWithFormat:@"better_player_channel/videoEvents%lld",
- textureId]
- binaryMessenger:_messenger];
- [player setMixWithOthers:false];
- [eventChannel setStreamHandler:player];
- player.eventChannel = eventChannel;
- _players[@(textureId)] = player;
- result(@{@"textureId" : @(textureId)});
-}
-
-- (void) setupRemoteNotification :(FLTBetterPlayer*) player{
- [self stopOtherUpdateListener:player];
- NSDictionary* dataSource = [_dataSourceDict objectForKey:[self getTextureId:player]];
- BOOL showNotification = false;
- id showNotificationObject = [dataSource objectForKey:@"showNotification"];
- if (showNotificationObject != [NSNull null]) {
- showNotification = [[dataSource objectForKey:@"showNotification"] boolValue];
- }
- NSString* title = dataSource[@"title"];
- NSString* author = dataSource[@"author"];
- NSString* imageUrl = dataSource[@"imageUrl"];
-
- if (showNotification){
- [self setRemoteCommandsNotificationActive];
- [self setupRemoteCommands: player];
- [self setupRemoteCommandNotification: player, title, author, imageUrl];
- [self setupUpdateListener: player, title, author, imageUrl];
- }
-}
-
-- (void) setRemoteCommandsNotificationActive{
- [[AVAudioSession sharedInstance] setActive:true error:nil];
- [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
-}
-
-- (void) setRemoteCommandsNotificationNotActive{
- if ([_players count] == 0) {
- [[AVAudioSession sharedInstance] setActive:false error:nil];
- }
- [[UIApplication sharedApplication] endReceivingRemoteControlEvents];
-}
-
-
-- (void) setupRemoteCommands:(FLTBetterPlayer*)player {
- MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter];
- [commandCenter.togglePlayPauseCommand setEnabled:YES];
- [commandCenter.playCommand setEnabled:YES];
- [commandCenter.pauseCommand setEnabled:YES];
- [commandCenter.nextTrackCommand setEnabled:NO];
- [commandCenter.previousTrackCommand setEnabled:NO];
- if (@available(iOS 9.1, *)) {
- [commandCenter.changePlaybackPositionCommand setEnabled:YES];
- }
-
- [commandCenter.togglePlayPauseCommand addTargetWithHandler: ^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
- if (player.isPlaying){
- player.eventSink(@{@"event" : @"play"});
-
- } else {
- player.eventSink(@{@"event" : @"pause"});
-
- }
- return MPRemoteCommandHandlerStatusSuccess;
- }];
-
- [commandCenter.playCommand addTargetWithHandler: ^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
- player.eventSink(@{@"event" : @"play"});
- return MPRemoteCommandHandlerStatusSuccess;
- }];
-
- [commandCenter.pauseCommand addTargetWithHandler: ^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
- player.eventSink(@{@"event" : @"pause"});
- return MPRemoteCommandHandlerStatusSuccess;
- }];
-
-
-
- if (@available(iOS 9.1, *)) {
- [commandCenter.changePlaybackPositionCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
- MPChangePlaybackPositionCommandEvent * playbackEvent = (MPChangePlaybackRateCommandEvent * ) event;
- CMTime time = CMTimeMake(playbackEvent.positionTime, 1);
- int64_t millis = FLTCMTimeToMillis(time);
- [player seekTo: millis];
- player.eventSink(@{@"event" : @"seek", @"position": @(millis)});
- return MPRemoteCommandHandlerStatusSuccess;
- }];
-
- }
-}
-
-- (void) setupRemoteCommandNotification:(FLTBetterPlayer*)player, NSString* title, NSString* author , NSString* imageUrl{
- float positionInSeconds = player.position /1000;
- float durationInSeconds = player.duration/ 1000;
-
-
- NSMutableDictionary * nowPlayingInfoDict = [@{MPMediaItemPropertyArtist: author,
- MPMediaItemPropertyTitle: title,
- MPNowPlayingInfoPropertyElapsedPlaybackTime: [ NSNumber numberWithFloat : positionInSeconds],
- MPMediaItemPropertyPlaybackDuration: [NSNumber numberWithFloat:durationInSeconds],
- MPNowPlayingInfoPropertyPlaybackRate: @1,
- } mutableCopy];
-
- if (imageUrl != [NSNull null]){
- NSString* key = [self getTextureId:player];
- MPMediaItemArtwork* artworkImage = [_artworkImageDict objectForKey:key];
-
- if (key != [NSNull null]){
- if (artworkImage){
- [nowPlayingInfoDict setObject:artworkImage forKey:MPMediaItemPropertyArtwork];
- [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nowPlayingInfoDict;
-
- } else {
- dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- dispatch_async(queue, ^{
- @try{
- UIImage * tempArtworkImage = nil;
- if ([imageUrl rangeOfString:@"http"].location == NSNotFound){
- tempArtworkImage = [UIImage imageWithContentsOfFile:imageUrl];
- } else {
- NSURL *nsImageUrl =[NSURL URLWithString:imageUrl];
- tempArtworkImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:nsImageUrl]];
- }
- if(tempArtworkImage)
- {
- MPMediaItemArtwork* artworkImage = [[MPMediaItemArtwork alloc] initWithImage: tempArtworkImage];
- [_artworkImageDict setObject:artworkImage forKey:key];
- [nowPlayingInfoDict setObject:artworkImage forKey:MPMediaItemPropertyArtwork];
- }
- [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nowPlayingInfoDict;
- }
- @catch(NSException *exception) {
-
- }
- });
- }
- }
- } else {
- [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nowPlayingInfoDict;
- }
-}
-
-
-
-- (NSString*) getTextureId: (FLTBetterPlayer*) player{
- NSArray* temp = [_players allKeysForObject: player];
- NSString* key = [temp lastObject];
- return key;
-}
-
-- (void) setupUpdateListener:(FLTBetterPlayer*)player,NSString* title, NSString* author,NSString* imageUrl {
- id _timeObserverId = [player.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:NULL usingBlock:^(CMTime time){
- [self setupRemoteCommandNotification:player, title, author, imageUrl];
- }];
-
- NSString* key = [self getTextureId:player];
- [ _timeObserverIdDict setObject:_timeObserverId forKey: key];
-}
-
-
-- (void) disposeNotificationData: (FLTBetterPlayer*)player{
- NSString* key = [self getTextureId:player];
- id _timeObserverId = _timeObserverIdDict[key];
- [_timeObserverIdDict removeObjectForKey: key];
- [_artworkImageDict removeObjectForKey:key];
- if (_timeObserverId){
- [player.player removeTimeObserver:_timeObserverId];
- _timeObserverId = nil;
- }
- [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = @{};
-}
-
-- (void) stopOtherUpdateListener: (FLTBetterPlayer*) player{
- NSString* currentPlayerTextureId = [self getTextureId:player];
- for (NSString* textureId in _timeObserverIdDict.allKeys) {
- if (currentPlayerTextureId == textureId){
- continue;
- }
-
- id timeObserverId = [_timeObserverIdDict objectForKey:textureId];
- FLTBetterPlayer* playerToRemoveListener = [_players objectForKey:textureId];
- [playerToRemoveListener.player removeTimeObserver: timeObserverId];
- }
- [_timeObserverIdDict removeAllObjects];
-
-}
-
-
-- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
-
-
- if ([@"init" isEqualToString:call.method]) {
- // Allow audio playback when the Ring/Silent switch is set to silent
- for (NSNumber* textureId in _players) {
- [_registry unregisterTexture:[textureId unsignedIntegerValue]];
- [_players[textureId] dispose];
- }
-
- [_players removeAllObjects];
- result(nil);
- } else if ([@"create" isEqualToString:call.method]) {
- FLTFrameUpdater* frameUpdater = [[FLTFrameUpdater alloc] initWithRegistry:_registry];
- FLTBetterPlayer* player = [[FLTBetterPlayer alloc] initWithFrameUpdater:frameUpdater];
- [self onPlayerSetup:player frameUpdater:frameUpdater result:result];
- } else {
- NSDictionary* argsMap = call.arguments;
- int64_t textureId = ((NSNumber*)argsMap[@"textureId"]).unsignedIntegerValue;
- FLTBetterPlayer* player = _players[@(textureId)];
- if ([@"setDataSource" isEqualToString:call.method]) {
- [player clear];
- // This call will clear cached frame because we will return transparent frame
- [_registry textureFrameAvailable:textureId];
-
- NSDictionary* dataSource = argsMap[@"dataSource"];
- [_dataSourceDict setObject:dataSource forKey:[self getTextureId:player]];
- NSString* assetArg = dataSource[@"asset"];
- NSString* uriArg = dataSource[@"uri"];
- NSString* key = dataSource[@"key"];
- NSDictionary* headers = dataSource[@"headers"];
- int overriddenDuration = 0;
- if ([dataSource objectForKey:@"overriddenDuration"] != [NSNull null]){
- overriddenDuration = [dataSource[@"overriddenDuration"] intValue];
- }
-
- BOOL useCache = false;
- id useCacheObject = [dataSource objectForKey:@"useCache"];
- if (useCacheObject != [NSNull null]) {
- useCache = [[dataSource objectForKey:@"useCache"] boolValue];
- }
-
- if (headers == nil){
- headers = @{};
- }
- if (assetArg) {
- NSString* assetPath;
- NSString* package = dataSource[@"package"];
- if (![package isEqual:[NSNull null]]) {
- assetPath = [_registrar lookupKeyForAsset:assetArg fromPackage:package];
- } else {
- assetPath = [_registrar lookupKeyForAsset:assetArg];
- }
- [player setDataSourceAsset:assetPath withKey:key overriddenDuration:overriddenDuration];
- } else if (uriArg) {
- if ([uriArg hasPrefix:@"file://"]) {
- [player setDataSourceURL:[NSURL URLWithString:uriArg] withKey:key withHeaders:headers withCache: useCache overriddenDuration:overriddenDuration];
- } else {
- NSURL *proxyURL = [_cacheManager getCacheUrl:uriArg];
- [player setDataSourceURL:proxyURL withKey:key withHeaders:headers withCache: useCache overriddenDuration:overriddenDuration];
- }
- } else {
- result(FlutterMethodNotImplemented);
- }
- result(nil);
- } else if ([@"dispose" isEqualToString:call.method]) {
- [player clear];
- [self disposeNotificationData:player];
- [self setRemoteCommandsNotificationNotActive];
- [_registry unregisterTexture:textureId];
- [_players removeObjectForKey:@(textureId)];
- // If the Flutter contains https://github.com/flutter/engine/pull/12695,
- // the `player` is disposed via `onTextureUnregistered` at the right time.
- // Without https://github.com/flutter/engine/pull/12695, there is no guarantee that the
- // texture has completed the un-reregistration. It may leads a crash if we dispose the
- // `player` before the texture is unregistered. We add a dispatch_after hack to make sure the
- // texture is unregistered before we dispose the `player`.
- //
- // TODO(cyanglaz): Remove this dispatch block when
- // https://github.com/flutter/flutter/commit/8159a9906095efc9af8b223f5e232cb63542ad0b is in
- // stable And update the min flutter version of the plugin to the stable version.
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)),
- dispatch_get_main_queue(), ^{
- if (!player.disposed) {
- [player dispose];
- }
- });
- if ([_players count] == 0) {
- [[AVAudioSession sharedInstance] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
- }
- result(nil);
- } else if ([@"setLooping" isEqualToString:call.method]) {
- [player setIsLooping:[argsMap[@"looping"] boolValue]];
- result(nil);
- } else if ([@"setVolume" isEqualToString:call.method]) {
- [player setVolume:[argsMap[@"volume"] doubleValue]];
- result(nil);
- } else if ([@"play" isEqualToString:call.method]) {
- [self setupRemoteNotification:player];
- [player play];
- result(nil);
- } else if ([@"position" isEqualToString:call.method]) {
- result(@([player position]));
- } else if ([@"absolutePosition" isEqualToString:call.method]) {
- result(@([player absolutePosition]));
- } else if ([@"seekTo" isEqualToString:call.method]) {
- [player seekTo:[argsMap[@"location"] intValue]];
- result(nil);
- } else if ([@"pause" isEqualToString:call.method]) {
- [player pause];
- result(nil);
- } else if ([@"setTrackParameters" isEqualToString:call.method]) {
- int width = [argsMap[@"width"] intValue];
- int height = [argsMap[@"height"] intValue];
- int bitrate = [argsMap[@"bitrate"] intValue];
-
- [player setTrackParameters:width: height : bitrate];
- result(nil);
- } else if ([@"setAudioTrack" isEqualToString:call.method]){
- NSString* name = argsMap[@"name"];
- int index = [argsMap[@"index"] intValue];
- [player setAudioTrack:name index: index];
- } else if ([@"setMixWithOthers" isEqualToString:call.method]){
- [player setMixWithOthers:[argsMap[@"mixWithOthers"] boolValue]];
- } else if ([@"clearCache" isEqualToString:call.method]){
- [KTVHTTPCache cacheDeleteAllCaches];
- } else if ([@"preCache" isEqualToString:call.method]) {
- NSString* url = argsMap[@"dataSource"];
- [_cacheManager preCache:url];
- } else {
- result(FlutterMethodNotImplemented);
- }
- }
-}
-@end
diff --git a/ios/Classes/FLTBetterPlayerPlugin.swift b/ios/Classes/FLTBetterPlayerPlugin.swift
new file mode 100644
index 0000000..73d5a13
--- /dev/null
+++ b/ios/Classes/FLTBetterPlayerPlugin.swift
@@ -0,0 +1,1482 @@
+import Flutter
+import AVFoundation
+import GLKit
+import KTVHTTPCache
+import MediaPlayer
+import AVKit
+
+// Helper functions
+
+// This function converts a CMTime object
+// (used in AVFoundation to represent time values) into an
+// integer value representing milliseconds (Int64).
+func FLTCMTimeToMillis(_ time: CMTime) -> Int64 {
+ if time.timescale == 0 { return 0 }
+ return Int64(time.value * 1000 / Int64(time.timescale))
+}
+
+// This function converts a TimeInterval
+// (a Double representing time in seconds) into an integer value
+// representing milliseconds (Int64).
+func FLTNSTimeIntervalToMillis(_ interval: TimeInterval) -> Int64 {
+ return Int64(interval * 1000.0)
+}
+
+
+// FLTFrameUpdater
+// This class is a helper class designed to manage frame updates for a video texture in a Flutter application.
+// It works in conjunction with the FlutterTextureRegistry to notify Flutter when a new video frame is available for rendering.
+class FLTFrameUpdater: NSObject {
+ // An identifier (Int64) for the texture associated with the video. This ID is used to notify Flutter about frame updates for the specific texture.
+ var textureId: Int64 = 0
+
+ // A weak reference to the FlutterTextureRegistry, which is responsible for managing textures in the Flutter engine.
+ weak var registry: FlutterTextureRegistry?
+
+ // This method initializes the FLTFrameUpdater with a reference to the FlutterTextureRegistry.
+ // This allows the class to notify Flutter when a new frame is available.
+ init(registry: FlutterTextureRegistry) {
+ self.registry = registry
+ super.init()
+ }
+
+ // This method is called periodically by a CADisplayLink, which is a timer that synchronizes with the display's refresh rate (e.g., 60Hz).
+ @objc func onDisplayLink(_ link: CADisplayLink) {
+ // The textureFrameAvailable function of the FlutterTextureRegistry is called with the textureId.
+ // This notifies Flutter that a new frame is available for the texture, prompting Flutter to render the updated frame.
+ registry?.textureFrameAvailable(textureId)
+ }
+}
+
+// KVO Context Pointers
+// These pointers are used for Key-Value Observing to monitor changes in the player's properties
+private var rateContext = 0
+private var timeRangeContext = 0
+private var statusContext = 0
+private var playbackLikelyToKeepUpContext = 0
+private var playbackBufferEmptyContext = 0
+private var playbackBufferFullContext = 0
+private var presentationSizeContext = 0
+
+
+// FLTBetterPlayer
+// This class implements the core video player functionality and interfaces with Flutter
+class FLTBetterPlayer: NSObject, FlutterTexture, FlutterStreamHandler {
+ // Properties
+ private(set) var player: AVPlayer // The AVPlayer instance that handles media playback
+ private(set) var videoOutput: AVPlayerItemVideoOutput? // Outputs video frames for rendering
+ private(set) var displayLink: CADisplayLink // Synchronizes frame updates with the display refresh rate
+ var eventChannel: FlutterEventChannel? // Channel for sending events to Flutter
+ var eventSink: FlutterEventSink? // Sink for sending events through the eventChannel
+ var preferredTransform: CGAffineTransform = .identity // Transform for adjusting video orientation
+ private(set) var disposed: Bool = false // Flag indicating if the player has been disposed
+ private(set) var isPlaying: Bool = false // Flag indicating if video is currently playing
+ var isSeeking: Bool = false // Flag indicating if seeking operation is in progress
+ var isLooping: Bool = false // Flag indicating if video should loop
+ private(set) var isInitialized: Bool = false // Flag indicating if player is initialized
+ private(set) var key: String? // Unique identifier for the video
+ private(set) var prevBuffer: CVPixelBuffer? // Previous video frame buffer
+ private(set) var failedCount: Int = 0 // Counter for tracking frame retrieval failures
+ var _playerLayer: AVPlayerLayer? // Layer for rendering video when not using texture
+ var _observersAdded: Bool = false // Flag indicating if KVO observers have been added
+ var stalledCount: Int = 0 // Counter for tracking stall events
+ var playerRate: Float = 1.0 // Playback speed rate
+ weak var frameUpdater: FLTFrameUpdater? // Reference to the frame updater
+
+ // Initialization
+ init(frameUpdater: FLTFrameUpdater) {
+ self.player = AVPlayer()
+ self.player.actionAtItemEnd = .none
+ isInitialized = false
+ isPlaying = false
+ disposed = false
+ isSeeking = false
+ self.frameUpdater = frameUpdater
+
+ // Create display link to synchronize with screen refresh rate
+ self.displayLink = CADisplayLink(target: frameUpdater, selector: #selector(FLTFrameUpdater.onDisplayLink))
+
+ super.init()
+
+ // Fix for loading large videos - prevents automatic stalling for buffering
+ if #available(iOS 10.0, *) {
+ player.automaticallyWaitsToMinimizeStalling = false
+ }
+
+ // Add the display link to the current run loop
+ displayLink.add(to: RunLoop.current, forMode: .common)
+ displayLink.isPaused = true
+ _observersAdded = false
+ }
+
+ // Returns the texture identifier assigned to this player
+ func textureId() -> Int64 {
+ return frameUpdater?.textureId ?? 0
+ }
+
+ // Observer Management
+ // Adds Key-Value Observers to monitor player and item state changes
+ func addObservers(to item: AVPlayerItem) {
+ if !_observersAdded {
+ // Add observers for player and item properties to track state changes
+ player.addObserver(self, forKeyPath: "rate", options: [], context: &rateContext)
+ item.addObserver(self, forKeyPath: "loadedTimeRanges", options: [], context: &timeRangeContext)
+ item.addObserver(self, forKeyPath: "status", options: [], context: &statusContext)
+ item.addObserver(self, forKeyPath: "presentationSize", options: [], context: &presentationSizeContext)
+ item.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: [], context: &playbackLikelyToKeepUpContext)
+ item.addObserver(self, forKeyPath: "playbackBufferEmpty", options: [], context: &playbackBufferEmptyContext)
+ item.addObserver(self, forKeyPath: "playbackBufferFull", options: [], context: &playbackBufferFullContext)
+
+ // Add notification observer for when the video finishes playing
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(itemDidPlayToEndTime),
+ name: .AVPlayerItemDidPlayToEndTime,
+ object: item
+ )
+ _observersAdded = true
+ }
+ }
+
+ // Removes the video output from the current item
+ func removeVideoOutput() {
+ videoOutput = nil
+ guard let currentItem = player.currentItem else { return }
+
+ // Remove all outputs from the current item
+ for output in currentItem.outputs {
+ currentItem.remove(output)
+ }
+ }
+
+ // Resets the player state and cleans up resources
+ func clear() {
+ displayLink.isPaused = true
+ isInitialized = false
+ isPlaying = false
+ disposed = false
+ videoOutput = nil
+ failedCount = 0
+ key = nil
+
+ guard let currentItem = player.currentItem else { return }
+
+ removeObservers()
+ currentItem.asset.cancelLoading()
+ }
+
+ // Removes all observers to prevent memory leaks
+ func removeObservers() {
+ if _observersAdded {
+ // Remove all KVO observers
+ player.removeObserver(self, forKeyPath: "rate", context: &rateContext)
+
+ if let currentItem = player.currentItem {
+ currentItem.removeObserver(self, forKeyPath: "status", context: &statusContext)
+ currentItem.removeObserver(self, forKeyPath: "presentationSize", context: &presentationSizeContext)
+ currentItem.removeObserver(self, forKeyPath: "loadedTimeRanges", context: &timeRangeContext)
+ currentItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp", context: &playbackLikelyToKeepUpContext)
+ currentItem.removeObserver(self, forKeyPath: "playbackBufferEmpty", context: &playbackBufferEmptyContext)
+ currentItem.removeObserver(self, forKeyPath: "playbackBufferFull", context: &playbackBufferFullContext)
+ }
+
+ NotificationCenter.default.removeObserver(self)
+ _observersAdded = false
+ }
+ }
+
+ // Playback Control
+ // Handles end of playback - either loops or sends completion event
+ @objc func itemDidPlayToEndTime(_ notification: Notification) {
+ if isLooping {
+ // To prevent reloading the entire video again, we seek to 1ms
+ // This happens because the first millisecond isn't buffered at the start
+ // so when we seek to 0, it will buffer as if nothing was previously buffered
+ seekTo(1)
+ } else {
+ if let eventSink = eventSink, let key = key {
+ // Notify Flutter that playback has completed
+ eventSink(["event": "completed", "key": key])
+ removeObservers()
+ }
+ player.pause()
+ isPlaying = false
+ displayLink.isPaused = true
+ }
+ }
+
+ // Helper function to convert radians to degrees
+ fileprivate func radiansToDegrees(_ radians: CGFloat) -> CGFloat {
+ let degrees = GLKMathRadiansToDegrees(Float(radians))
+ if degrees < 0 {
+ return CGFloat(degrees) + 360
+ }
+ // Output degrees in between [0, 360)
+ return CGFloat(degrees)
+ }
+
+ // Video Composition
+ // Creates a video composition to handle video orientation/transformation
+ func getVideoComposition(withTransform transform: CGAffineTransform,
+ withAsset asset: AVAsset,
+ withVideoTrack videoTrack: AVAssetTrack) -> AVMutableVideoComposition {
+ // Create composition instruction for the full video duration
+ let instruction = AVMutableVideoCompositionInstruction()
+ instruction.timeRange = CMTimeRange(start: .zero, duration: asset.duration)
+
+ // Create layer instruction with the preferred transform
+ let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
+ layerInstruction.setTransform(preferredTransform, at: .zero)
+
+ // Create and configure the video composition
+ let videoComposition = AVMutableVideoComposition()
+ instruction.layerInstructions = [layerInstruction]
+ videoComposition.instructions = [instruction]
+
+ // If in portrait mode, switch the width and height of the video
+ var width = videoTrack.naturalSize.width
+ var height = videoTrack.naturalSize.height
+ let rotationDegrees =
+ Int(round(radiansToDegrees(atan2(preferredTransform.b, preferredTransform.a))))
+
+ if rotationDegrees == 90 || rotationDegrees == 270 {
+ width = videoTrack.naturalSize.height
+ height = videoTrack.naturalSize.width
+ }
+ videoComposition.renderSize = CGSize(width: width, height: height)
+
+ videoComposition.frameDuration = CMTime(value: 1, timescale: 30)
+
+ return videoComposition
+ }
+
+ // Video Output Configuration
+ // Adds a video output to the current player item for frame retrieval
+ func addVideoOutput() {
+ guard let currentItem = player.currentItem else {
+ return
+ }
+
+ // Check if video output is already added to avoid duplication
+ if let videoOutput = videoOutput {
+ let outputs = currentItem.outputs
+ for output in outputs {
+ if output === videoOutput {
+ return
+ }
+ }
+ }
+
+ // Configure pixel buffer attributes for the video output
+ let pixBuffAttributes: [String: Any] = [
+ kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA,
+ kCVPixelBufferIOSurfacePropertiesKey as String: [:]
+ ]
+
+ // Create and add the video output to the player item
+ videoOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: pixBuffAttributes)
+ if let videoOutput = videoOutput {
+ currentItem.add(videoOutput)
+ }
+ }
+
+ // Fixes the transform for video orientation based on track metadata
+ func fixTransform(_ videoTrack: AVAssetTrack) -> CGAffineTransform {
+ var transform = videoTrack.preferredTransform
+ if transform.tx == 0 && transform.ty == 0 {
+ let rotationDegrees = Int(round(radiansToDegrees(atan2(transform.b, transform.a))))
+ NSLog("TX and TY are 0. Rotation: %d. Natural width,height: %f, %f", rotationDegrees,
+ videoTrack.naturalSize.width, videoTrack.naturalSize.height)
+
+ // Fix transform for different rotation angles
+ if rotationDegrees == 90 {
+ NSLog("Setting transform tx")
+ transform.tx = videoTrack.naturalSize.height
+ transform.ty = 0
+ } else if rotationDegrees == 270 {
+ NSLog("Setting transform ty")
+ transform.tx = 0
+ transform.ty = videoTrack.naturalSize.width
+ }
+ }
+ return transform
+ }
+
+ // Data Source Setup
+ // Sets up a local asset file as the data source
+ func setDataSourceAsset(_ asset: String, withKey key: String, overriddenDuration: Int) {
+ let path = Bundle.main.path(forResource: asset, ofType: nil)
+ if let path = path {
+ let url = URL(fileURLWithPath: path)
+ setDataSourceURL(url, withKey: key, withHeaders: [:], withCache: false, overriddenDuration: overriddenDuration)
+ }
+ }
+
+ // Sets up a URL as the data source, handling caching and custom headers
+ func setDataSourceURL(_ url: URL, withKey key: String, withHeaders headers: [String: String], withCache useCache: Bool, overriddenDuration: Int) {
+ var headersDictionary = headers
+ if headers is NSNull {
+ headersDictionary = [:]
+ }
+
+ let item: AVPlayerItem
+
+ if useCache {
+ // Use KTVHTTPCache for caching if enabled
+ KTVHTTPCache.downloadSetAdditionalHeaders(headersDictionary)
+ if let proxyURL = KTVHTTPCache.proxyURL(withOriginalURL: url) {
+ item = AVPlayerItem(url: proxyURL)
+ } else {
+ item = AVPlayerItem(url: url) // Fallback to the original URL if proxyURL is nil
+ }
+ } else {
+ // Convert headers dictionary to [String: String] format
+ let stringHeaders = headersDictionary.reduce(into: [String: String]()) { result, element in
+ if let stringValue = element.value as? String {
+ result[element.key] = stringValue
+ }
+ }
+
+ // Create asset with custom HTTP headers
+ let asset = AVURLAsset(
+ url: url,
+ options: ["AVURLAssetHTTPHeaderFieldsKey": stringHeaders]
+ )
+ item = AVPlayerItem(asset: asset)
+ if #available(iOS 10.0, *) {
+ let k = 3.0
+ item.preferredForwardBufferDuration = k
+ }
+ }
+
+ // Set custom duration if provided (iOS 10+ only)
+ if #available(iOS 10.0, *), overriddenDuration > 0 {
+ // Convert overriddenDuration from milliseconds to a proper CMTime
+ item.forwardPlaybackEndTime = CMTimeMake(value: Int64(overriddenDuration), timescale: 1000)
+ }
+
+ setDataSourcePlayerItem(item, withKey: key)
+ }
+
+ // Sets up a player item as the data source and configures it
+ func setDataSourcePlayerItem(_ item: AVPlayerItem, withKey key: String) {
+ self.key = key
+ stalledCount = 0
+ playerRate = 1.0
+ player.replaceCurrentItem(with: item)
+
+ let asset = item.asset
+
+ // Completion handler for asset loading
+ let assetCompletionHandler: () -> Void = { [weak self] in
+ guard let self = self else { return }
+
+ if asset.statusOfValue(forKey: "tracks", error: nil) == .loaded {
+ let tracks = asset.tracks(withMediaType: .video)
+ if !tracks.isEmpty {
+ let videoTrack = tracks[0]
+
+ // Completion handler for video track loading
+ let trackCompletionHandler: () -> Void = { [weak self] in
+ guard let self = self, !self.disposed else { return }
+
+ if videoTrack.statusOfValue(forKey: "preferredTransform", error: nil) == .loaded {
+ // Rotate the video by using a videoComposition and the preferredTransform
+ self.preferredTransform = self.fixTransform(videoTrack)
+
+ // Use the asset's duration explicitly
+ let videoComposition = self.getVideoComposition(
+ withTransform: self.preferredTransform,
+ withAsset: asset,
+ withVideoTrack: videoTrack
+ )
+ item.videoComposition = videoComposition
+ }
+ }
+
+ // Load video track properties asynchronously
+ videoTrack.loadValuesAsynchronously(forKeys: ["preferredTransform"],
+ completionHandler: trackCompletionHandler)
+ }
+ }
+ }
+
+ // Load asset properties asynchronously
+ asset.loadValuesAsynchronously(forKeys: ["tracks"], completionHandler: assetCompletionHandler)
+ addObservers(to: item)
+ }
+
+ // Stall Handling
+ // Handles video playback stalls
+ @objc func handleStalled() {
+ if player.currentItem?.isPlaybackLikelyToKeepUp == true ||
+ availableDuration() - CMTimeGetSeconds(player.currentItem?.currentTime() ?? .zero) > 10.0 {
+ // If playback is likely to keep up or we have more than 10 seconds buffered,
+ // we don't need to do anything special
+ } else {
+ stalledCount += 1
+ if stalledCount > 50 {
+ // Too many stalls, report error to Flutter
+ eventSink?(FlutterError(
+ code: "VideoError",
+ message: "Failed to load video: playback stalled",
+ details: nil)
+ )
+ return
+ }
+ // Recursively check for stall again after 1 second
+ perform(#selector(handleStalled), with: nil, afterDelay: 1.0)
+ }
+ }
+
+ // Calculates available playback duration based on loaded time ranges
+ func availableDuration() -> TimeInterval {
+ guard let currentItem = player.currentItem,
+ let loadedTimeRanges = currentItem.loadedTimeRanges as? [NSValue],
+ loadedTimeRanges.count > 0 else {
+ return 0
+ }
+
+ let timeRange = loadedTimeRanges[0].timeRangeValue
+ let startSeconds = CMTimeGetSeconds(timeRange.start)
+ let durationSeconds = CMTimeGetSeconds(timeRange.duration)
+ let result = startSeconds + durationSeconds
+
+ return result
+ }
+
+ // KVO Observation
+ // Handles all Key-Value Observation callbacks
+ override func observeValue(forKeyPath path: String?,
+ of object: Any?,
+ change: [NSKeyValueChangeKey: Any]?,
+ context: UnsafeMutableRawPointer?) {
+ guard let path = path else { return }
+
+ if context == &rateContext {
+ // Handle changes to playback rate (detect stalls)
+ if player.rate == 0, // if player rate dropped to 0
+ let currentTime = player.currentItem?.currentTime(),
+ let duration = player.currentItem?.duration,
+ currentTime > CMTime.zero, // video was started
+ currentTime < duration, // but not yet finished
+ isPlaying { // instance variable to handle overall state (changed to true when user triggers playback)
+ handleStalled()
+ }
+ } else if context == &timeRangeContext {
+ // Handle changes to loaded time ranges (buffering progress)
+ guard let eventSink = eventSink, let key = key else { return }
+
+ let values = NSMutableArray()
+ if let object = object as? AVPlayerItem, let loadedTimeRanges = object.loadedTimeRanges as? [NSValue] {
+ for rangeValue in loadedTimeRanges {
+ let range = rangeValue.timeRangeValue
+ let start = FLTCMTimeToMillis(range.start)
+ var end = start + FLTCMTimeToMillis(range.duration)
+
+ // Ensure we don't exceed any custom end time
+ if let forwardPlaybackEndTime = player.currentItem?.forwardPlaybackEndTime,
+ !CMTIME_IS_INVALID(forwardPlaybackEndTime) {
+ let endTime = FLTCMTimeToMillis(forwardPlaybackEndTime)
+ if end > endTime {
+ end = endTime
+ }
+ }
+
+ values.add([NSNumber(value: start), NSNumber(value: end)])
+ }
+ }
+ // Send buffering update event to Flutter
+ eventSink(["event": "bufferingUpdate", "values": values, "key": key])
+ } else if context == &presentationSizeContext {
+ // Video size is now known, try to initialize the player
+ onReadyToPlay()
+ } else if context == &statusContext {
+ // Handle player item status changes
+ guard let item = object as? AVPlayerItem else { return }
+
+ switch item.status {
+ case .failed:
+ NSLog("Failed to load video:")
+ NSLog("%@", item.error.map { String(describing: $0) } ?? "unknown error")
+
+ if let eventSink = eventSink {
+ let message = "Failed to load video: " + (item.error?.localizedDescription ?? "")
+ eventSink(FlutterError(code: "VideoError", message: message, details: nil))
+ }
+ case .unknown:
+ // Still loading, nothing to do yet
+ break
+ case .readyToPlay:
+ // Item is ready to play, initialize the player
+ onReadyToPlay()
+ @unknown default:
+ break
+ }
+ } else if context == &playbackLikelyToKeepUpContext {
+ // Handle playback likely to keep up changes (buffering state)
+ if player.currentItem?.isPlaybackLikelyToKeepUp == true {
+ updatePlayingState()
+ if let eventSink = eventSink, let key = key {
+ eventSink(["event": "bufferingEnd", "key": key])
+ }
+ }
+ } else if context == &playbackBufferEmptyContext {
+ // Handle buffer empty state (start buffering)
+ if let eventSink = eventSink, let key = key {
+ eventSink(["event": "bufferingStart", "key": key])
+ }
+ } else if context == &playbackBufferFullContext {
+ // Handle buffer full state (end buffering)
+ if let eventSink = eventSink, let key = key {
+ eventSink(["event": "bufferingEnd", "key": key])
+ }
+ } else {
+ // Handle unexpected key paths or contexts gracefully
+ NSLog("Unhandled key path: \(path), context: \(String(describing: context))")
+ }
+ }
+
+ // Player Initialization
+ // Updates playback state based on isPlaying flag
+ func updatePlayingState() {
+ guard isInitialized, let key = key else {
+ NSLog("not initialized and paused!!")
+ displayLink.isPaused = true
+ return
+ }
+
+ // Ensure observers are added if needed
+ if !_observersAdded {
+ if let currentItem = player.currentItem {
+ addObservers(to: currentItem)
+ }
+ }
+
+ // Update player playback state based on isPlaying flag
+ if isPlaying {
+ if #available(iOS 10.0, *) {
+ player.playImmediately(atRate: 1.0)
+ player.rate = playerRate
+ } else {
+ player.play()
+ player.rate = playerRate
+ }
+ } else {
+ player.pause()
+ }
+
+ // Set display link appropriately, however, don't pause
+ // the display link of the video isSeeking, as we don't
+ // want to block the UI from updating during seeking.
+ if isPlaying {
+ displayLink.isPaused = !isPlaying
+ } else if !isSeeking {
+ displayLink.isPaused = !isPlaying
+ }
+ }
+
+ // Called when the player is ready to play (initialized)
+ func onReadyToPlay() {
+ guard let eventSink = eventSink,
+ !isInitialized,
+ let key = key,
+ let currentItem = player.currentItem,
+ player.status == .readyToPlay else {
+ return
+ }
+
+ let size = currentItem.presentationSize
+ let width = size.width
+ let height = size.height
+
+ let asset = currentItem.asset
+ let onlyAudio = asset.tracks(withMediaType: .video).isEmpty
+
+ // The player has not yet initialized.
+ if !onlyAudio && height == CGSize.zero.height && width == CGSize.zero.width {
+ return
+ }
+
+ let isLive = CMTIME_IS_INDEFINITE(currentItem.duration)
+ // The player may be initialized but still needs to determine the duration.
+ if !isLive && duration() == 0 {
+ return
+ }
+
+ // Fix from https://github.com/flutter/flutter/issues/66413
+ guard let track = player.currentItem?.tracks.first,
+ let assetTrack = track.assetTrack else {
+ return
+ }
+
+ // Get the natural size and apply the preferred transform to get the real size
+ let naturalSize = assetTrack.naturalSize
+ let prefTrans = assetTrack.preferredTransform
+ let realSize = naturalSize.applying(prefTrans)
+
+ // Mark as initialized and set up for playback
+ isInitialized = true
+ addVideoOutput()
+ updatePlayingState()
+
+ // Send initialized event to Flutter with video dimensions
+ eventSink([
+ "event": "initialized",
+ "duration": NSNumber(value: duration()),
+ "width": NSNumber(value: abs(realSize.width) != 0 ? abs(realSize.width) : width),
+ "height": NSNumber(value: abs(realSize.height) != 0 ? abs(realSize.height) : height),
+ "key": key
+ ])
+ }
+
+ // Playback Control Methods
+ // Start playing the video
+ func play() {
+ stalledCount = 0
+ isPlaying = true
+ updatePlayingState()
+
+ // iOS 10+ workaround to ensure playback starts correctly
+ if #available(iOS 10.0, *) {
+ if let currentItem = player.currentItem {
+ player.replaceCurrentItem(with: currentItem)
+ }
+ }
+ }
+
+ // Pause the video
+ func pause() {
+ isPlaying = false
+ updatePlayingState()
+ }
+
+ // Get the current playback position in milliseconds
+ func position() -> Int64 {
+ return FLTCMTimeToMillis(player.currentTime())
+ }
+
+ // Get the absolute timestamp for live streams
+ func absolutePosition() -> Int64 {
+ if let currentDate = player.currentItem?.currentDate() {
+ return FLTNSTimeIntervalToMillis(currentDate.timeIntervalSince1970)
+ }
+ return 0
+ }
+
+ // Get the total duration of the video in milliseconds
+ func duration() -> Int64 {
+ var time: CMTime
+
+ if #available(iOS 13, *) {
+ time = player.currentItem?.duration ?? .zero
+ } else {
+ time = player.currentItem?.asset.duration ?? .zero
+ }
+
+ // Use custom end time if set
+ if let currentItem = player.currentItem,
+ !CMTIME_IS_INVALID(currentItem.forwardPlaybackEndTime) {
+ time = currentItem.forwardPlaybackEndTime
+ }
+
+ return FLTCMTimeToMillis(time)
+ }
+
+ // Seek to a specific position in milliseconds
+ func seekTo(_ location: Int) {
+ isSeeking = true
+ displayLink.isPaused = false // to see seeking in video output
+
+ // Perform the seek operation
+ player.seek(to: CMTimeMake(value: Int64(location), timescale: 1000),
+ toleranceBefore: .zero,
+ toleranceAfter: .zero) { [weak self] finished in
+
+ // run async query to not run on main UI thread
+ // really buggy otherwise
+ let queue = DispatchQueue.global(qos: .default)
+ queue.async { [weak self] in
+ guard let self = self else { return }
+
+ // sleep for 2 frames (time is defined by displayLink duration)
+ Thread.sleep(forTimeInterval: 2 * self.displayLink.duration)
+ self.isSeeking = false
+ // set display link as appropriate
+ self.displayLink.isPaused = !self.isPlaying
+ }
+ }
+ }
+
+ // Set whether the video should loop when it reaches the end
+ func setIsLooping(_ isLooping: Bool) {
+ self.isLooping = isLooping
+ }
+
+ // Set the volume level (0.0 to 1.0)
+ func setVolume(_ volume: Double) {
+ player.volume = Float(max(0.0, min(1.0, volume)))
+ }
+
+ // Set the playback speed
+ func setSpeed(_ speed: Double, result: @escaping FlutterResult) {
+ if speed == 1.0 || speed == 0.0 {
+ playerRate = 1.0
+ result(nil)
+ } else if speed < 0 || speed > 2.0 {
+ result(FlutterError(
+ code: "unsupported_speed",
+ message: "Speed must be >= 0.0 and <= 2.0",
+ details: nil
+ ))
+ } else if (speed > 1.0 && player.currentItem?.canPlayFastForward == true) ||
+ (speed < 1.0 && player.currentItem?.canPlaySlowForward == true) {
+ playerRate = Float(speed)
+ result(nil)
+ } else {
+ if speed > 1.0 {
+ result(FlutterError(
+ code: "unsupported_fast_forward",
+ message: "This video cannot be played fast forward",
+ details: nil
+ ))
+ } else {
+ result(FlutterError(
+ code: "unsupported_slow_forward",
+ message: "This video cannot be played slow forward",
+ details: nil
+ ))
+ }
+ }
+
+ // Apply rate if currently playing
+ if isPlaying {
+ player.rate = Float(playerRate)
+ }
+ }
+
+ // Set track parameters for quality control
+ func setTrackParameters(width: Int, height: Int, bitrate: Int) {
+ player.currentItem?.preferredPeakBitRate = Double(bitrate)
+
+ if #available(iOS 11.0, *) {
+ if width == 0 && height == 0 {
+ player.currentItem?.preferredMaximumResolution = .zero
+ } else {
+ player.currentItem?.preferredMaximumResolution = CGSize(width: width, height: height)
+ }
+ }
+ }
+
+ #if os(iOS)
+ // Create and configure a player layer for UIKit-based rendering
+ func usePlayerLayer(frame: CGRect) {
+ // Create new controller passing reference to the AVPlayerLayer
+ _playerLayer = AVPlayerLayer(player: player)
+
+ if let rootViewController = UIApplication.shared.keyWindow?.rootViewController {
+ _playerLayer?.frame = frame
+ _playerLayer?.needsDisplayOnBoundsChange = true
+ // [self._playerLayer addObserver:self forKeyPath:readyForDisplayKeyPath options:NSKeyValueObservingOptionNew context:nil];
+ rootViewController.view.layer.addSublayer(_playerLayer!)
+ rootViewController.view.layer.needsDisplayOnBoundsChange = true
+
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
+ // Empty completion handler (keeping the same structure as Obj-C code)
+ }
+ }
+ }
+ #endif
+
+ #if os(iOS)
+ // Set the audio track by name and index
+ func setAudioTrack(_ name: String, index: Int) {
+ guard let currentItem = player.currentItem,
+ let audioSelectionGroup = currentItem.asset.mediaSelectionGroup(forMediaCharacteristic: .audible) else {
+ return
+ }
+
+ let asset = currentItem.asset
+
+ let options = audioSelectionGroup.options
+
+ // Find and select the specified audio track
+ for i in 0.. 0,
+ let title = metaDatas[0].stringValue,
+ title == name && index == i {
+ player.currentItem?.select(option, in: audioSelectionGroup)
+ }
+ }
+ }
+
+ // Set whether audio should mix with other apps' audio
+ func setMixWithOthers(_ mixWithOthers: Bool) {
+ do {
+ if mixWithOthers {
+ try AVAudioSession.sharedInstance().setCategory(.playback,
+ options: .mixWithOthers)
+ } else {
+ try AVAudioSession.sharedInstance().setCategory(.playback)
+ }
+ } catch {
+ NSLog("Failed to set audio session category: \(error.localizedDescription)")
+ }
+ }
+ #endif
+
+ // Texture Generation
+ // Create a transparent buffer for when no frame is available
+ func prevTransparentBuffer() -> CVPixelBuffer? {
+ if let prevBuffer = prevBuffer {
+ CVPixelBufferLockBaseAddress(prevBuffer, .init(rawValue: 0))
+
+ let bufferWidth = CVPixelBufferGetWidth(prevBuffer)
+ let bufferHeight = CVPixelBufferGetHeight(prevBuffer)
+
+ if let baseAddress = CVPixelBufferGetBaseAddress(prevBuffer) {
+ var pixel = baseAddress.assumingMemoryBound(to: UInt8.self)
+
+ // Set all pixels to transparent black
+ for _ in 0.. Unmanaged? {
+ let outputItemTime = videoOutput?.itemTime(forHostTime: CACurrentMediaTime())
+
+ guard let outputTime = outputItemTime else { return nil }
+
+ if videoOutput?.hasNewPixelBuffer(forItemTime: outputTime) == true {
+ failedCount = 0
+ if let buffer = videoOutput?.copyPixelBuffer(forItemTime: outputTime, itemTimeForDisplay: nil) {
+ prevBuffer = buffer
+ return Unmanaged.passRetained(buffer)
+ }
+ return nil
+ } else {
+ // AVPlayerItemVideoOutput.hasNewPixelBufferForItemTime doesn't work correctly
+ failedCount += 1
+ if failedCount > 100 {
+ failedCount = 0
+ removeVideoOutput()
+ addVideoOutput()
+ }
+
+ // Return the previous buffer if available
+ if let buffer = prevBuffer {
+ return Unmanaged.passRetained(buffer)
+ }
+ return nil
+ }
+ }
+
+ // Texture Lifecycle
+ // Called when the texture is unregistered
+ func onTextureUnregistered() {
+ DispatchQueue.main.async { [weak self] in
+ self?.dispose()
+ }
+ }
+
+ // FlutterStreamHandler Protocol Implementation
+ // Called when event listening is cancelled
+ func onCancel(withArguments arguments: Any?) -> FlutterError? {
+ eventSink = nil
+ return nil
+ }
+
+ // Called when Flutter starts listening for events
+ func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
+ eventSink = events
+
+ // TODO(@recastrodiaz): remove the line below when the race condition is resolved:
+ // https://github.com/flutter/flutter/issues/21483
+ // This line ensures the 'initialized' event is sent when the event
+ // 'AVPlayerItemStatusReadyToPlay' fires before eventSink is set (this function
+ // onListen is called)
+ onReadyToPlay()
+
+ return nil
+ }
+
+ // Disposal
+ /// This method allows you to dispose without touching the event channel. This
+ /// is useful for the case where the Engine is in the process of deconstruction
+ /// so the channel is going to die or is already dead.
+ func disposeSansEventChannel() {
+ do {
+ clear()
+ displayLink.invalidate()
+ } catch let exception as NSException {
+ NSLog("%@", exception.debugDescription)
+ } catch {
+ NSLog("Unknown error during disposal")
+ }
+ }
+
+ // Fully dispose of the player and its resources
+ func dispose() {
+ disposeSansEventChannel()
+ eventChannel?.setStreamHandler(nil)
+ disposed = true
+ }
+}
+
+// FLTBetterPlayerPlugin
+// Main plugin class that interfaces with Flutter
+public class FLTBetterPlayerPlugin: NSObject, FlutterPlugin {
+ // Properties
+ private weak var registry: FlutterTextureRegistry?
+ private weak var messenger: FlutterBinaryMessenger?
+ private var players: [Int64: FLTBetterPlayer] = [:]
+ private weak var registrar: FlutterPluginRegistrar?
+ private var cacheManager: CacheManager
+
+ // Static properties that were class variables in Objective-C
+ private static var dataSourceDict: [String: Any] = [:]
+ private static var timeObserverIdDict: [String: Any] = [:]
+ private static var artworkImageDict: [String: MPMediaItemArtwork] = [:]
+
+ // Plugin Registration
+ public static func register(with registrar: FlutterPluginRegistrar) {
+ let channel = FlutterMethodChannel(
+ name: "better_player_channel",
+ binaryMessenger: registrar.messenger()
+ )
+ let instance = FLTBetterPlayerPlugin(registrar: registrar)
+ registrar.addMethodCallDelegate(instance, channel: channel)
+ registrar.publish(instance)
+ }
+
+ // Initialization
+ init(registrar: FlutterPluginRegistrar) {
+ self.registry = registrar.textures()
+ self.messenger = registrar.messenger()
+ self.registrar = registrar
+ self.players = [:]
+
+ // Initialize dictionaries
+ FLTBetterPlayerPlugin.timeObserverIdDict = [:]
+ FLTBetterPlayerPlugin.artworkImageDict = [:]
+ FLTBetterPlayerPlugin.dataSourceDict = [:]
+
+ // Start HTTP cache
+ do {
+ try KTVHTTPCache.proxyStart()
+ } catch {
+ print("Failed to start KTVHTTPCache proxy: \(error)")
+ }
+
+ // Initialize cache manager
+ self.cacheManager = CacheManager()
+
+ super.init()
+ }
+
+ // Plugin Lifecycle
+ public func detachFromEngine(for registrar: FlutterPluginRegistrar) {
+ // Clean up all players when the plugin is detached
+ for (textureId, player) in players {
+ player.disposeSansEventChannel()
+ }
+ players.removeAll()
+ }
+
+ // Player Setup
+ // Set up a new player instance and register it with Flutter
+ func onPlayerSetup(_ player: FLTBetterPlayer,
+ frameUpdater: FLTFrameUpdater,
+ result: @escaping FlutterResult) {
+ guard let registry = registry else {
+ result(FlutterError(code: "player_setup_failed",
+ message: "Registry is not available",
+ details: nil))
+ return
+ }
+
+ // Register the player with the texture registry
+ let textureId = registry.register(player)
+ frameUpdater.textureId = textureId
+
+ // Create event channel for this specific texture
+ let channelName = "better_player_channel/videoEvents\(textureId)"
+ guard let messenger = messenger else {
+ print("BinaryMessenger is nil")
+ return
+ }
+
+ let eventChannel = FlutterEventChannel(
+ name: channelName,
+ binaryMessenger: messenger
+ )
+
+ // Configure the player
+ player.setMixWithOthers(false)
+ eventChannel.setStreamHandler(player)
+ player.eventChannel = eventChannel
+ players[textureId] = player
+
+ // Return the texture ID to Flutter
+ result(["textureId": NSNumber(value: textureId)])
+ }
+
+ // Remote Control and Notification Setup
+ // Set up remote control notifications for the player
+ func setupRemoteNotification(for player: FLTBetterPlayer) {
+ stopOtherUpdateListener(for: player)
+
+ guard let textureId = getTextureId(player: player),
+ let dataSource = FLTBetterPlayerPlugin.dataSourceDict[textureId] as? [String: Any] else {
+ return
+ }
+
+ var showNotification = false
+ if let showNotificationObject = dataSource["showNotification"],
+ !(showNotificationObject is NSNull) {
+ showNotification = (dataSource["showNotification"] as? Bool) ?? false
+ }
+
+ let title = dataSource["title"] as? String ?? ""
+ let author = dataSource["author"] as? String ?? ""
+ let imageUrl = dataSource["imageUrl"] as? String
+
+ if showNotification {
+ setRemoteCommandsNotificationActive()
+ setupRemoteCommands(for: player)
+ setupRemoteCommandNotification(player: player, title: title, author: author, imageUrl: imageUrl)
+ setupUpdateListener(player: player, title: title, author: author, imageUrl: imageUrl)
+ }
+ }
+
+ // Activate the audio session for remote control commands
+ func setRemoteCommandsNotificationActive() {
+ do {
+ try AVAudioSession.sharedInstance().setActive(true)
+ UIApplication.shared.beginReceivingRemoteControlEvents()
+ } catch {
+ NSLog("Failed to activate audio session: %@", error.localizedDescription)
+ }
+ }
+
+ // Deactivate the audio session when no more players are active
+ func setRemoteCommandsNotificationNotActive() {
+ do {
+ if players.isEmpty {
+ try AVAudioSession.sharedInstance().setActive(false)
+ }
+ UIApplication.shared.endReceivingRemoteControlEvents()
+ } catch {
+ NSLog("Failed to deactivate audio session: %@", error.localizedDescription)
+ }
+ }
+
+ // Set up remote control commands (play, pause, etc.)
+ func setupRemoteCommands(for player: FLTBetterPlayer) {
+ let commandCenter = MPRemoteCommandCenter.shared()
+
+ // Enable relevant commands
+ commandCenter.togglePlayPauseCommand.isEnabled = true
+ commandCenter.playCommand.isEnabled = true
+ commandCenter.pauseCommand.isEnabled = true
+ commandCenter.nextTrackCommand.isEnabled = false
+ commandCenter.previousTrackCommand.isEnabled = false
+
+ if #available(iOS 9.1, *) {
+ commandCenter.changePlaybackPositionCommand.isEnabled = true
+ }
+
+ // Add handlers for each command
+ commandCenter.togglePlayPauseCommand.addTarget { [weak player] event in
+ guard let player = player else { return .commandFailed }
+
+ if player.isPlaying {
+ player.eventSink?(["event": "play"])
+ } else {
+ player.eventSink?(["event": "pause"])
+ }
+ return .success
+ }
+
+ commandCenter.playCommand.addTarget { [weak player] event in
+ guard let player = player else { return .commandFailed }
+
+ player.eventSink?(["event": "play"])
+ return .success
+ }
+
+ commandCenter.pauseCommand.addTarget { [weak player] event in
+ guard let player = player else { return .commandFailed }
+
+ player.eventSink?(["event": "pause"])
+ return .success
+ }
+
+ if #available(iOS 9.1, *) {
+ commandCenter.changePlaybackPositionCommand.addTarget { [weak player] event in
+ guard let player = player,
+ let playbackEvent = event as? MPChangePlaybackPositionCommandEvent else {
+ return .commandFailed
+ }
+
+ let time = CMTimeMake(value: Int64(playbackEvent.positionTime), timescale: 1)
+ let millis = FLTCMTimeToMillis(time)
+
+ player.seekTo(Int(millis))
+ player.eventSink?(["event": "seek", "position": NSNumber(value: millis)])
+
+ return .success
+ }
+ }
+ }
+
+ // Set up the control center/lock screen media info
+ func setupRemoteCommandNotification(player: FLTBetterPlayer, title: String, author: String, imageUrl: String?) {
+ let positionInSeconds = Float(player.position()) / 1000.0
+ let durationInSeconds = Float(player.duration()) / 1000.0
+
+ // Create the now playing info dict with basic metadata
+ var nowPlayingInfoDict: [String: Any] = [
+ MPMediaItemPropertyArtist: author,
+ MPMediaItemPropertyTitle: title,
+ MPNowPlayingInfoPropertyElapsedPlaybackTime: positionInSeconds,
+ MPMediaItemPropertyPlaybackDuration: durationInSeconds,
+ MPNowPlayingInfoPropertyPlaybackRate: 1.0
+ ]
+
+ // Handle artwork image if provided
+ if let imageUrl = imageUrl, !(imageUrl is NSNull) {
+ if let key = getTextureId(player: player),
+ !(key is NSNull) {
+
+ if let artworkImage = FLTBetterPlayerPlugin.artworkImageDict[key] {
+ // Use cached artwork image if available
+ nowPlayingInfoDict[MPMediaItemPropertyArtwork] = artworkImage
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfoDict
+ } else {
+ // Load artwork image asynchronously
+ let queue = DispatchQueue.global(qos: .default)
+ queue.async {
+ do {
+ var tempArtworkImage: UIImage?
+
+ if (!imageUrl.contains("http")) {
+ // Local file
+ tempArtworkImage = UIImage(contentsOfFile: imageUrl)
+ } else {
+ // Remote URL
+ if let nsImageUrl = URL(string: imageUrl),
+ let imageData = try? Data(contentsOf: nsImageUrl) {
+ tempArtworkImage = UIImage(data: imageData)
+ }
+ }
+
+ if let tempArtworkImage = tempArtworkImage {
+ let artworkImage = MPMediaItemArtwork(image: tempArtworkImage)
+ FLTBetterPlayerPlugin.artworkImageDict[key] = artworkImage
+ nowPlayingInfoDict[MPMediaItemPropertyArtwork] = artworkImage
+ }
+
+ DispatchQueue.main.async {
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfoDict
+ }
+ } catch {
+ // Handle exceptions silently as in Obj-C version
+ }
+ }
+ }
+ }
+ } else {
+ // No artwork image, just set the info dict
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfoDict
+ }
+ }
+
+ // Helper to get the texture ID string for a player
+ func getTextureId(player: FLTBetterPlayer) -> String? {
+ // Find the key (textureId) for the given player
+ for (id, p) in players {
+ if p === player {
+ return String(id)
+ }
+ }
+ return nil
+ }
+
+ // Set up periodic updates for lock screen info
+ func setupUpdateListener(player: FLTBetterPlayer, title: String, author: String, imageUrl: String?) {
+ let timeObserverId = player.player.addPeriodicTimeObserver(
+ forInterval: CMTimeMake(value: 1, timescale: 1),
+ queue: nil
+ ) { [weak self] time in
+ guard let self = self else { return }
+ self.setupRemoteCommandNotification(
+ player: player,
+ title: title,
+ author: author,
+ imageUrl: imageUrl
+ )
+ }
+
+ // Store the observer for later removal
+ if let key = getTextureId(player: player) {
+ FLTBetterPlayerPlugin.timeObserverIdDict[key] = timeObserverId
+ }
+ }
+
+ // Clean up notification data for a player
+ func disposeNotificationData(for player: FLTBetterPlayer) {
+ guard let key = getTextureId(player: player) else { return }
+
+ if let timeObserverId = FLTBetterPlayerPlugin.timeObserverIdDict[key] {
+ FLTBetterPlayerPlugin.timeObserverIdDict.removeValue(forKey: key)
+ FLTBetterPlayerPlugin.artworkImageDict.removeValue(forKey: key)
+
+ player.player.removeTimeObserver(timeObserverId)
+ }
+
+ // Clear lock screen/control center media info
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = [:]
+ }
+
+ // Stop update listeners for other players when a new one is started
+ func stopOtherUpdateListener(for player: FLTBetterPlayer) {
+ guard let currentPlayerTextureId = getTextureId(player: player) else { return }
+
+ for textureId in FLTBetterPlayerPlugin.timeObserverIdDict.keys {
+ // Skip the current player
+ if currentPlayerTextureId == textureId {
+ continue
+ }
+
+ if let timeObserverId = FLTBetterPlayerPlugin.timeObserverIdDict[textureId],
+ let textureIdInt = Int64(textureId),
+ let playerToRemoveListener = players[textureIdInt] as? FLTBetterPlayer {
+ playerToRemoveListener.player.removeTimeObserver(timeObserverId)
+ }
+ }
+
+ FLTBetterPlayerPlugin.timeObserverIdDict.removeAll()
+ }
+
+ // Flutter Method Channel Handler
+ public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+ if call.method == "init" {
+ // Allow audio playback when the Ring/Silent switch is set to silent
+ for (textureId, player) in players {
+ registry?.unregisterTexture(textureId)
+ player.dispose()
+ }
+
+ players.removeAll()
+ result(nil)
+ } else if call.method == "create" {
+ guard let registry = registry else {
+ result(FlutterError(code: "player_creation_failed", message: "Registry is not available", details: nil))
+ return
+ }
+
+ let frameUpdater = FLTFrameUpdater(registry: registry)
+ let player = FLTBetterPlayer(frameUpdater: frameUpdater)
+ onPlayerSetup(player, frameUpdater: frameUpdater, result: result)
+ } else {
+ guard let argsMap = call.arguments as? [String: Any],
+ let textureIdValue = argsMap["textureId"] as? NSNumber,
+ let player = players[textureIdValue.int64Value] as? FLTBetterPlayer else {
+ result(FlutterMethodNotImplemented)
+ return
+ }
+
+ let textureId = textureIdValue.int64Value
+
+ switch call.method {
+ case "setDataSource":
+ // Clear existing player and prepare for new data source
+ player.clear()
+ // This call will clear cached frame because we will return transparent frame
+ registry?.textureFrameAvailable(textureId)
+
+ guard let dataSource = argsMap["dataSource"] as? [String: Any] else {
+ result(FlutterMethodNotImplemented)
+ return
+ }
+
+ // Store data source info for later reference (e.g., notifications)
+ if let textureIdString = getTextureId(player: player) {
+ FLTBetterPlayerPlugin.dataSourceDict[textureIdString] = dataSource
+ }
+
+ let assetArg = dataSource["asset"] as? String
+ let uriArg = dataSource["uri"] as? String
+ let key = dataSource["key"] as? String ?? ""
+
+ var headers = dataSource["headers"] as? [String: String] ?? [:]
+
+ var overriddenDuration = 0
+ if let overriddenDurationValue = dataSource["overriddenDuration"],
+ !(overriddenDurationValue is NSNull) {
+ overriddenDuration = (dataSource["overriddenDuration"] as? Int) ?? 0
+ }
+
+ var useCache = false
+ if let useCacheObject = dataSource["useCache"],
+ !(useCacheObject is NSNull) {
+ useCache = (dataSource["useCache"] as? Bool) ?? false
+ }
+
+ // Set up data source based on asset or URI
+ if let assetArg = assetArg {
+ var assetPath: String?
+ if let package = dataSource["package"] as? String,
+ !(package is NSNull) {
+ assetPath = registrar?.lookupKey(forAsset: assetArg, fromPackage: package)
+ } else {
+ assetPath = registrar?.lookupKey(forAsset: assetArg)
+ }
+
+ if let assetPath = assetPath {
+ player.setDataSourceAsset(assetPath, withKey: key, overriddenDuration: overriddenDuration)
+ }
+ result(nil)
+ } else if let uriArg = uriArg {
+ if uriArg.hasPrefix("file://") {
+ if let url = URL(string: uriArg) {
+ player.setDataSourceURL(url, withKey: key, withHeaders: headers, withCache: useCache, overriddenDuration: overriddenDuration)
+ }
+ } else {
+ if let proxyURL = cacheManager.getCacheUrl(uriArg as NSString) as? URL {
+ player.setDataSourceURL(proxyURL, withKey: key, withHeaders: headers, withCache: useCache, overriddenDuration: overriddenDuration)
+ }
+ }
+ result(nil)
+ } else {
+ result(FlutterMethodNotImplemented)
+ }
+
+ case "dispose":
+ // Dispose the player and clean up resources
+ player.clear()
+ disposeNotificationData(for: player)
+ setRemoteCommandsNotificationNotActive()
+ registry?.unregisterTexture(textureId)
+ players.removeValue(forKey: textureId)
+
+ // If the Flutter contains https://github.com/flutter/engine/pull/12695,
+ // the `player` is disposed via `onTextureUnregistered` at the right time.
+ // Without https://github.com/flutter/engine/pull/12695, there is no guarantee that the
+ // texture has completed the un-reregistration. It may leads a crash if we dispose the
+ // `player` before the texture is unregistered. We add a dispatch_after hack to make sure the
+ // texture is unregistered before we dispose the `player`.
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak player] in
+ if let player = player, !player.disposed {
+ player.dispose()
+ }
+ }
+
+ if players.isEmpty {
+ try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
+ }
+ result(nil)
+
+ case "setLooping":
+ // Configure looping behavior
+ if let looping = argsMap["looping"] as? Bool {
+ player.setIsLooping(looping)
+ }
+ result(nil)
+
+ case "setVolume":
+ // Set player volume
+ if let volume = argsMap["volume"] as? Double {
+ player.setVolume(volume)
+ }
+ result(nil)
+
+ case "play":
+ // Start playback and set up remote notifications
+ setupRemoteNotification(for: player)
+ player.play()
+ result(nil)
+
+ case "position":
+ // Get current playback position
+ result(NSNumber(value: player.position()))
+
+ case "absolutePosition":
+ // Get absolute position (for live streams)
+ result(NSNumber(value: player.absolutePosition()))
+
+ case "seekTo":
+ // Seek to specific position
+ if let location = argsMap["location"] as? Int {
+ player.seekTo(location)
+ }
+ result(nil)
+
+ case "pause":
+ // Pause playback
+ player.pause()
+ result(nil)
+
+ case "setTrackParameters":
+ // Set video quality parameters
+ let width = argsMap["width"] as? Int ?? 0
+ let height = argsMap["height"] as? Int ?? 0
+ let bitrate = argsMap["bitrate"] as? Int ?? 0
+
+ player.setTrackParameters(width: width, height: height, bitrate: bitrate)
+ result(nil)
+
+ case "setAudioTrack":
+ // Select specific audio track
+ if let name = argsMap["name"] as? String,
+ let index = argsMap["index"] as? Int {
+ player.setAudioTrack(name, index: index)
+ }
+ result(nil)
+
+ case "setMixWithOthers":
+ // Configure audio mixing behavior
+ if let mixWithOthers = argsMap["mixWithOthers"] as? Bool {
+ player.setMixWithOthers(mixWithOthers)
+ }
+ result(nil)
+
+ case "clearCache":
+ // Clear video cache
+ KTVHTTPCache.cacheDeleteAllCaches()
+ result(nil)
+
+ case "preCache":
+ // Pre-cache a video for future playback
+ if let url = argsMap["dataSource"] as? String {
+ cacheManager.preCache(url as NSString)
+ }
+ result(nil)
+
+ default:
+ result(FlutterMethodNotImplemented)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/ios/better_player.podspec b/ios/better_player.podspec
index 3108387..fbe0f19 100644
--- a/ios/better_player.podspec
+++ b/ios/better_player.podspec
@@ -13,7 +13,6 @@ A new flutter plugin project.
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
- s.public_header_files = 'Classes/**/*.h'
s.dependency 'Flutter'
# KTVHTTPCache
s.dependency 'KTVHTTPCache', '~> 2.0.0'
diff --git a/pubspec.lock b/pubspec.lock
index 9f9a278..d3fcf80 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -1,81 +1,100 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
+ args:
+ dependency: transitive
+ description:
+ name: args
+ sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.7.0"
async:
dependency: transitive
description:
name: async
- url: "https://pub.dartlang.org"
+ sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
+ url: "https://pub.dev"
source: hosted
- version: "2.8.2"
+ version: "2.11.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
- url: "https://pub.dartlang.org"
+ sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
+ url: "https://pub.dev"
source: hosted
- version: "2.1.0"
+ version: "2.1.1"
characters:
dependency: transitive
description:
name: characters
- url: "https://pub.dartlang.org"
- source: hosted
- version: "1.2.0"
- charcode:
- dependency: transitive
- description:
- name: charcode
- url: "https://pub.dartlang.org"
+ sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
+ url: "https://pub.dev"
source: hosted
- version: "1.3.1"
+ version: "1.3.0"
clock:
dependency: transitive
description:
name: clock
- url: "https://pub.dartlang.org"
+ sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
+ url: "https://pub.dev"
source: hosted
- version: "1.1.0"
+ version: "1.1.1"
collection:
dependency: "direct main"
description:
name: collection
- url: "https://pub.dartlang.org"
+ sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
+ url: "https://pub.dev"
source: hosted
- version: "1.16.0"
+ version: "1.18.0"
csslib:
dependency: transitive
description:
name: csslib
- url: "https://pub.dartlang.org"
+ sha256: f857285c8dc0b4f2f77b49a1c083ff8c75223a7549de20f3e607df58cf497a43
+ url: "https://pub.dev"
source: hosted
version: "0.17.0"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
- url: "https://pub.dartlang.org"
+ sha256: caac504f942f41dfadcf45229ce8c47065b93919a12739f20d6173a883c5ec73
+ url: "https://pub.dev"
source: hosted
version: "1.0.2"
+ dbus:
+ dependency: transitive
+ description:
+ name: dbus
+ sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.7.11"
fake_async:
dependency: transitive
description:
name: fake_async
- url: "https://pub.dartlang.org"
+ sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
+ url: "https://pub.dev"
source: hosted
- version: "1.3.0"
+ version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
- url: "https://pub.dartlang.org"
+ sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
+ url: "https://pub.dev"
source: hosted
- version: "1.0.0"
+ version: "2.1.3"
file:
dependency: transitive
description:
name: file
- url: "https://pub.dartlang.org"
+ sha256: "9fd2163d866769f60f4df8ac1dc59f52498d810c356fe78022e383dd3c57c0e1"
+ url: "https://pub.dev"
source: hosted
version: "6.1.0"
flutter:
@@ -102,126 +121,200 @@ packages:
dependency: "direct main"
description:
name: flutter_widget_from_html_core
- url: "https://pub.dartlang.org"
+ sha256: "5ac2944b95aea1c3c29dfced964fc83e97283c417c22b1164ec302d2dd0386d2"
+ url: "https://pub.dev"
source: hosted
version: "0.6.0-rc.2021030401"
html:
dependency: transitive
description:
name: html
- url: "https://pub.dartlang.org"
+ sha256: bfef906cbd4e78ef49ae511d9074aebd1d2251482ef601a280973e8b58b51bbf
+ url: "https://pub.dev"
source: hosted
version: "0.15.0"
+ http:
+ dependency: transitive
+ description:
+ name: http
+ sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.0"
+ http_parser:
+ dependency: transitive
+ description:
+ name: http_parser
+ sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.2"
intl:
dependency: transitive
description:
name: intl
- url: "https://pub.dartlang.org"
+ sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
+ url: "https://pub.dev"
source: hosted
- version: "0.17.0"
- js:
+ version: "0.19.0"
+ leak_tracker:
dependency: transitive
description:
- name: js
- url: "https://pub.dartlang.org"
+ name: leak_tracker
+ sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
+ url: "https://pub.dev"
source: hosted
- version: "0.6.4"
+ version: "10.0.5"
+ leak_tracker_flutter_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_flutter_testing
+ sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.5"
+ leak_tracker_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_testing
+ sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.1"
lint:
dependency: "direct dev"
description:
name: lint
- url: "https://pub.dartlang.org"
+ sha256: "697cff175f613d90e99e0eebe26946f2632d1ae4a4db63756b7ba003667ac4c8"
+ url: "https://pub.dev"
source: hosted
version: "1.5.2"
matcher:
dependency: transitive
description:
name: matcher
- url: "https://pub.dartlang.org"
+ sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
+ url: "https://pub.dev"
source: hosted
- version: "0.12.11"
+ version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
- url: "https://pub.dartlang.org"
+ sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
+ url: "https://pub.dev"
source: hosted
- version: "0.1.4"
+ version: "0.11.1"
meta:
dependency: "direct main"
description:
name: meta
- url: "https://pub.dartlang.org"
+ sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
+ url: "https://pub.dev"
source: hosted
- version: "1.7.0"
+ version: "1.15.0"
+ package_info_plus:
+ dependency: transitive
+ description:
+ name: package_info_plus
+ sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191"
+ url: "https://pub.dev"
+ source: hosted
+ version: "8.3.0"
+ package_info_plus_platform_interface:
+ dependency: transitive
+ description:
+ name: package_info_plus_platform_interface
+ sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.0"
path:
dependency: transitive
description:
name: path
- url: "https://pub.dartlang.org"
+ sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
+ url: "https://pub.dev"
source: hosted
- version: "1.8.1"
+ version: "1.9.0"
path_provider:
dependency: "direct main"
description:
name: path_provider
- url: "https://pub.dartlang.org"
+ sha256: aa1b3572707c240d72569ce01756728cf0c8dca0cc381253d8ca2858c13edfe4
+ url: "https://pub.dev"
source: hosted
version: "2.0.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
- url: "https://pub.dartlang.org"
+ sha256: "938d2b6591587bcb009d2109a6ea464fd8fb2a75dc6423171b0d0afb1d27c708"
+ url: "https://pub.dev"
source: hosted
version: "2.0.0"
path_provider_macos:
dependency: transitive
description:
name: path_provider_macos
- url: "https://pub.dartlang.org"
+ sha256: eb58b896ea3a504f0b0fa7870646bda6935a6f752b2a54df33f97070dacca8d4
+ url: "https://pub.dev"
source: hosted
version: "2.0.0"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
- url: "https://pub.dartlang.org"
+ sha256: c2af5a8a6369992d915f8933dfc23172071001359d17896e83db8be57db8a397
+ url: "https://pub.dev"
source: hosted
version: "2.0.1"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
- url: "https://pub.dartlang.org"
+ sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96"
+ url: "https://pub.dev"
source: hosted
- version: "2.0.0"
+ version: "2.1.7"
pedantic:
dependency: "direct main"
description:
name: pedantic
- url: "https://pub.dartlang.org"
+ sha256: "8f6460c77a98ad2807cd3b98c67096db4286f56166852d0ce5951bb600a63594"
+ url: "https://pub.dev"
source: hosted
version: "1.11.0"
+ petitparser:
+ dependency: transitive
+ description:
+ name: petitparser
+ sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.0.2"
platform:
dependency: transitive
description:
name: platform
- url: "https://pub.dartlang.org"
+ sha256: ebc79f16b5f6b609aad4a5e63447d4795d16f7adee46e93ed03200848c006735
+ url: "https://pub.dev"
source: hosted
version: "3.0.0"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
- url: "https://pub.dartlang.org"
+ sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
+ url: "https://pub.dev"
source: hosted
- version: "2.0.0"
+ version: "2.1.8"
process:
dependency: transitive
description:
name: process
- url: "https://pub.dartlang.org"
+ sha256: c7b9f7d8a6ee4407ab4f8a7d4a951f8f5659c40df14c0924e2e97c32372e9b14
+ url: "https://pub.dev"
source: hosted
version: "4.1.0"
sky_engine:
@@ -233,107 +326,130 @@ packages:
dependency: transitive
description:
name: source_span
- url: "https://pub.dartlang.org"
+ sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
+ url: "https://pub.dev"
source: hosted
- version: "1.8.2"
+ version: "1.10.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
- url: "https://pub.dartlang.org"
+ sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
+ url: "https://pub.dev"
source: hosted
- version: "1.10.0"
+ version: "1.11.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
- url: "https://pub.dartlang.org"
+ sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
+ url: "https://pub.dev"
source: hosted
- version: "2.1.0"
+ version: "2.1.2"
string_scanner:
dependency: transitive
description:
name: string_scanner
- url: "https://pub.dartlang.org"
+ sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
+ url: "https://pub.dev"
source: hosted
- version: "1.1.0"
+ version: "1.2.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
- url: "https://pub.dartlang.org"
+ sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
+ url: "https://pub.dev"
source: hosted
- version: "1.2.0"
+ version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
- url: "https://pub.dartlang.org"
+ sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
+ url: "https://pub.dev"
source: hosted
- version: "0.4.9"
+ version: "0.7.2"
+ typed_data:
+ dependency: transitive
+ description:
+ name: typed_data
+ sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.0"
vector_math:
dependency: transitive
description:
name: vector_math
- url: "https://pub.dartlang.org"
+ sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
+ url: "https://pub.dev"
source: hosted
- version: "2.1.2"
+ version: "2.1.4"
visibility_detector:
dependency: "direct main"
description:
name: visibility_detector
- url: "https://pub.dartlang.org"
+ sha256: "8e7e0a5f8cb7dca52518687440a478db6f589ec6e30cb96b965851cadada8063"
+ url: "https://pub.dev"
source: hosted
version: "0.2.0-nullsafety.1"
- wakelock:
- dependency: "direct main"
- description:
- name: wakelock
- url: "https://pub.dartlang.org"
- source: hosted
- version: "0.5.0+2"
- wakelock_macos:
+ vm_service:
dependency: transitive
description:
- name: wakelock_macos
- url: "https://pub.dartlang.org"
+ name: vm_service
+ sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc
+ url: "https://pub.dev"
source: hosted
- version: "0.1.0"
- wakelock_platform_interface:
- dependency: transitive
+ version: "14.2.4"
+ wakelock_plus:
+ dependency: "direct main"
description:
- name: wakelock_platform_interface
- url: "https://pub.dartlang.org"
+ name: wakelock_plus
+ sha256: b90fbcc8d7bdf3b883ea9706d9d76b9978cb1dfa4351fcc8014d6ec31a493354
+ url: "https://pub.dev"
source: hosted
- version: "0.2.0"
- wakelock_web:
+ version: "1.2.11"
+ wakelock_plus_platform_interface:
dependency: transitive
description:
- name: wakelock_web
- url: "https://pub.dartlang.org"
+ name: wakelock_plus_platform_interface
+ sha256: "70e780bc99796e1db82fe764b1e7dcb89a86f1e5b3afb1db354de50f2e41eb7a"
+ url: "https://pub.dev"
source: hosted
- version: "0.2.0"
- wakelock_windows:
+ version: "1.2.2"
+ web:
dependency: transitive
description:
- name: wakelock_windows
- url: "https://pub.dartlang.org"
+ name: web
+ sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
+ url: "https://pub.dev"
source: hosted
- version: "0.1.0"
+ version: "1.1.1"
win32:
dependency: transitive
description:
name: win32
- url: "https://pub.dartlang.org"
+ sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e
+ url: "https://pub.dev"
source: hosted
- version: "2.0.0"
+ version: "5.10.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
- url: "https://pub.dartlang.org"
+ sha256: "0186b3f2d66be9a12b0295bddcf8b6f8c0b0cc2f85c6287344e2a6366bc28457"
+ url: "https://pub.dev"
source: hosted
version: "0.2.0"
+ xml:
+ dependency: transitive
+ description:
+ name: xml
+ sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.5.0"
sdks:
- dart: ">=2.17.0-0 <3.0.0"
- flutter: ">=2.0.0"
+ dart: ">=3.5.0 <4.0.0"
+ flutter: ">=3.22.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index 38cae2d..0744acc 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -7,14 +7,14 @@ authors:
homepage: https://github.com/jhomlala/betterplayer
environment:
- sdk: '>=2.12.0 <3.0.0'
+ sdk: ">=2.12.0 <3.0.0"
flutter: ">=1.10.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
- wakelock: ^0.5.0+2
+ wakelock_plus: ^1.2.10
pedantic: ^1.11.0
meta: ^1.3.0
flutter_widget_from_html_core: ^0.6.0-rc.2021030201