diff --git a/.github/labeler.yml b/.github/labeler.yml index 59237e5bc..ec0b3f540 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -67,6 +67,8 @@ - packages/url_launcher/**/* "p: video_player": - packages/video_player/**/* +"p: video_player_avplay": + - packages/video_player_avplay/**/* "p: video_player_videohole": - packages/video_player_videohole/**/* "p: wakelock": diff --git a/.github/recipe.yaml b/.github/recipe.yaml index 0161210ce..14f04ac70 100644 --- a/.github/recipe.yaml +++ b/.github/recipe.yaml @@ -36,6 +36,7 @@ plugins: geolocator: [] in_app_purchase: [] network_info_plus: [] + video_player_avplay: [] video_player_videohole: [] # Only testable with the drive command: https://github.com/flutter-tizen/plugins/issues/272 diff --git a/README.md b/README.md index 92e146b4e..4fb258cb3 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ The _"non-endorsed"_ status means that the plugin is not endorsed by the origina | [**tizen_package_manager**](packages/tizen_package_manager) | (Tizen-only) | [![pub package](https://img.shields.io/pub/v/tizen_package_manager.svg)](https://pub.dev/packages/tizen_package_manager) | N/A | | [**tizen_rpc_port**](packages/tizen_rpc_port) | (Tizen-only) | [![pub package](https://img.shields.io/pub/v/tizen_rpc_port.svg)](https://pub.dev/packages/tizen_rpc_port) | N/A | | [**url_launcher_tizen**](packages/url_launcher) | [url_launcher](https://pub.dev/packages/url_launcher) (1st-party) | [![pub package](https://img.shields.io/pub/v/url_launcher_tizen.svg)](https://pub.dev/packages/url_launcher_tizen) | No | +| [**video_player_avplay**](packages/video_player_avplay) | (Tizen-only) | [![pub package](https://img.shields.io/pub/v/video_player_avplay.svg)](https://pub.dev/packages/video_player_avplay) | N/A | | [**video_player_tizen**](packages/video_player) | [video_player](https://pub.dev/packages/video_player) (1st-party) | [![pub package](https://img.shields.io/pub/v/video_player_tizen.svg)](https://pub.dev/packages/video_player_tizen) | No | | [**video_player_videohole**](packages/video_player_videohole) | (Tizen-only) | [![pub package](https://img.shields.io/pub/v/video_player_videohole.svg)](https://pub.dev/packages/video_player_videohole) | N/A | | [**wakelock_tizen**](packages/wakelock) | [wakelock](https://pub.dev/packages/wakelock) (3rd-party) | [![pub package](https://img.shields.io/pub/v/wakelock_tizen.svg)](https://pub.dev/packages/wakelock_tizen) | No | @@ -91,6 +92,7 @@ The _"non-endorsed"_ status means that the plugin is not endorsed by the origina | [**tizen_package_manager**](packages/tizen_package_manager) | ✔️ | ✔️ | ✔️ | ✔️ | | [**tizen_rpc_port**](packages/tizen_rpc_port) | ✔️ | ✔️ | ✔️ | ✔️ | | [**url_launcher_tizen**](packages/url_launcher) | ✔️ | ❌ | ✔️ | ❌ | No browser app | +| [**video_player_avplay**](packages/video_player_avplay) | ❌ | ❌ | ✔️ | ⚠️ | See README for details | | [**video_player_tizen**](packages/video_player) | ✔️ | ✔️ | ✔️ | ❌ | TV emulator issue | | [**video_player_videohole**](packages/video_player_videohole) | ❌ | ❌ | ✔️ | ⚠️ | See README for details | | [**wakelock_tizen**](packages/wakelock) | ✔️ | ✔️ | ❌ | ❌ | Cannot override system settings | diff --git a/packages/connectivity_plus/CHANGELOG.md b/packages/connectivity_plus/CHANGELOG.md index 75e33f296..8a0d20ad8 100644 --- a/packages/connectivity_plus/CHANGELOG.md +++ b/packages/connectivity_plus/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.1.4 + +* Update connectivity_plus to 4.0.1. +* Update connectivity_plus_platform_interface to 1.2.4. +* Update the example app. + ## 1.1.3 * Remove unnecessary `StreamHandlerError` implementation. diff --git a/packages/connectivity_plus/README.md b/packages/connectivity_plus/README.md index b50682cb0..8ac92ecfb 100644 --- a/packages/connectivity_plus/README.md +++ b/packages/connectivity_plus/README.md @@ -10,8 +10,8 @@ This package is not an _endorsed_ implementation of `connectivity_plus`. Therefo ```yaml dependencies: - connectivity_plus: ^2.3.0 - connectivity_plus_tizen: ^1.1.3 + connectivity_plus: ^4.0.1 + connectivity_plus_tizen: ^1.1.4 ``` Then you can import `connectivity_plus` in your Dart code: diff --git a/packages/connectivity_plus/example/integration_test/connectivity_plus_test.dart b/packages/connectivity_plus/example/integration_test/connectivity_plus_test.dart index 01ba081c7..38664930d 100644 --- a/packages/connectivity_plus/example/integration_test/connectivity_plus_test.dart +++ b/packages/connectivity_plus/example/integration_test/connectivity_plus_test.dart @@ -10,14 +10,14 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('Connectivity test driver', () { - late Connectivity _connectivity; + late Connectivity connectivity; setUpAll(() async { - _connectivity = Connectivity(); + connectivity = Connectivity(); }); testWidgets('test connectivity result', (WidgetTester tester) async { - final result = await _connectivity.checkConnectivity(); + final result = await connectivity.checkConnectivity(); expect(result, isNotNull); }); }); diff --git a/packages/connectivity_plus/example/lib/main.dart b/packages/connectivity_plus/example/lib/main.dart index 99eae054e..118a2b121 100644 --- a/packages/connectivity_plus/example/lib/main.dart +++ b/packages/connectivity_plus/example/lib/main.dart @@ -24,7 +24,8 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( - primarySwatch: Colors.blue, + useMaterial3: true, + colorSchemeSeed: const Color(0x9f4376f8), ), home: const MyHomePage(title: 'Flutter Demo Home Page'), ); @@ -37,7 +38,7 @@ class MyHomePage extends StatefulWidget { final String title; @override - _MyHomePageState createState() => _MyHomePageState(); + State createState() => _MyHomePageState(); } class _MyHomePageState extends State { @@ -92,6 +93,7 @@ class _MyHomePageState extends State { return Scaffold( appBar: AppBar( title: const Text('Connectivity example app'), + elevation: 4, ), body: Center( child: Text('Connection Status: ${_connectionStatus.toString()}')), diff --git a/packages/connectivity_plus/example/pubspec.yaml b/packages/connectivity_plus/example/pubspec.yaml index 430adf8b1..e2cab9531 100644 --- a/packages/connectivity_plus/example/pubspec.yaml +++ b/packages/connectivity_plus/example/pubspec.yaml @@ -7,7 +7,7 @@ environment: flutter: ">=3.3.0" dependencies: - connectivity_plus: ^2.3.0 + connectivity_plus: ^4.0.1 connectivity_plus_tizen: path: ../ flutter: @@ -22,7 +22,7 @@ dev_dependencies: sdk: flutter integration_test_tizen: path: ../../integration_test/ - flutter_lints: ^1.0.4 + flutter_lints: ^2.0.1 flutter: uses-material-design: true diff --git a/packages/connectivity_plus/pubspec.yaml b/packages/connectivity_plus/pubspec.yaml index 5759eaac7..b486b8478 100644 --- a/packages/connectivity_plus/pubspec.yaml +++ b/packages/connectivity_plus/pubspec.yaml @@ -2,7 +2,7 @@ name: connectivity_plus_tizen description: Tizen implementation of the connectivity_plus plugin. homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/master/packages/connectivity_plus -version: 1.1.3 +version: 1.1.4 environment: sdk: ">=2.18.0 <4.0.0" @@ -16,9 +16,9 @@ flutter: fileName: connectivity_plus_tizen_plugin.h dependencies: - connectivity_plus_platform_interface: ^1.2.0 + connectivity_plus_platform_interface: ^1.2.4 flutter: sdk: flutter dev_dependencies: - flutter_lints: ^1.0.4 + flutter_lints: ^2.0.1 diff --git a/packages/sensors_plus/CHANGELOG.md b/packages/sensors_plus/CHANGELOG.md index 557ef48a2..9df081147 100644 --- a/packages/sensors_plus/CHANGELOG.md +++ b/packages/sensors_plus/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.1.3 + +* Update sensors_plus to 3.0.2. +* Update sensors_plus_platform_interface to 1.1.3. +* Update the example app. + ## 1.1.2 * Remove unnecessary `StreamHandlerError` implementation. diff --git a/packages/sensors_plus/README.md b/packages/sensors_plus/README.md index c3dd62db5..ca4143861 100644 --- a/packages/sensors_plus/README.md +++ b/packages/sensors_plus/README.md @@ -10,8 +10,8 @@ This package is not an _endorsed_ implementation of 'sensors_plus'. Therefore, y ```yaml dependencies: - sensors_plus: ^1.2.1 - sensors_plus_tizen: ^1.1.2 + sensors_plus: ^3.0.2 + sensors_plus_tizen: ^1.1.3 ``` Then you can import `sensors_plus` in your Dart code: diff --git a/packages/sensors_plus/example/integration_test/sensors_plus_test.dart b/packages/sensors_plus/example/integration_test/sensors_plus_test.dart index 0d1f72536..9981a2834 100644 --- a/packages/sensors_plus/example/integration_test/sensors_plus_test.dart +++ b/packages/sensors_plus/example/integration_test/sensors_plus_test.dart @@ -10,7 +10,7 @@ import 'package:integration_test/integration_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Can subscript to accelerometerEvents and get non-null events', + testWidgets('Can subscribe to accelerometerEvents and get non-null events', (WidgetTester tester) async { final completer = Completer(); late StreamSubscription subscription; diff --git a/packages/sensors_plus/example/lib/main.dart b/packages/sensors_plus/example/lib/main.dart index 77b686a94..5453b596a 100644 --- a/packages/sensors_plus/example/lib/main.dart +++ b/packages/sensors_plus/example/lib/main.dart @@ -5,12 +5,22 @@ // ignore_for_file: public_member_api_docs import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:sensors_plus/sensors_plus.dart'; import 'snake.dart'; void main() { + WidgetsFlutterBinding.ensureInitialized(); + SystemChrome.setPreferredOrientations( + [ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ], + ); + runApp(const MyApp()); } @@ -22,7 +32,8 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Sensors Demo', theme: ThemeData( - primarySwatch: Colors.blue, + useMaterial3: true, + colorSchemeSeed: const Color(0x9f4376f8), ), home: const MyHomePage(title: 'Flutter Demo Home Page'), ); @@ -35,7 +46,7 @@ class MyHomePage extends StatefulWidget { final String? title; @override - _MyHomePageState createState() => _MyHomePageState(); + State createState() => _MyHomePageState(); } class _MyHomePageState extends State { @@ -43,27 +54,28 @@ class _MyHomePageState extends State { static const int _snakeColumns = 20; static const double _snakeCellSize = 10.0; - List? _accelerometerValues; List? _userAccelerometerValues; + List? _accelerometerValues; List? _gyroscopeValues; List? _magnetometerValues; final _streamSubscriptions = >[]; @override Widget build(BuildContext context) { + final userAccelerometer = _userAccelerometerValues + ?.map((double v) => v.toStringAsFixed(1)) + .toList(); final accelerometer = _accelerometerValues?.map((double v) => v.toStringAsFixed(1)).toList(); final gyroscope = _gyroscopeValues?.map((double v) => v.toStringAsFixed(1)).toList(); - final userAccelerometer = _userAccelerometerValues - ?.map((double v) => v.toStringAsFixed(1)) - .toList(); final magnetometer = _magnetometerValues?.map((double v) => v.toStringAsFixed(1)).toList(); return Scaffold( appBar: AppBar( - title: const Text('Sensor Example'), + title: const Text('Sensors Plus Example'), + elevation: 4, ), body: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, @@ -89,7 +101,7 @@ class _MyHomePageState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Accelerometer: $accelerometer'), + Text('UserAccelerometer: $userAccelerometer'), ], ), ), @@ -98,7 +110,7 @@ class _MyHomePageState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('UserAccelerometer: $userAccelerometer'), + Text('Accelerometer: $accelerometer'), ], ), ), @@ -136,6 +148,27 @@ class _MyHomePageState extends State { @override void initState() { super.initState(); + _streamSubscriptions.add( + userAccelerometerEvents.listen( + (UserAccelerometerEvent event) { + setState(() { + _userAccelerometerValues = [event.x, event.y, event.z]; + }); + }, + onError: (e) { + showDialog( + context: context, + builder: (context) { + return const AlertDialog( + title: Text("Sensor Not Found"), + content: Text( + "It seems that your device doesn't support Accelerometer Sensor"), + ); + }); + }, + cancelOnError: true, + ), + ); _streamSubscriptions.add( accelerometerEvents.listen( (AccelerometerEvent event) { @@ -143,6 +176,18 @@ class _MyHomePageState extends State { _accelerometerValues = [event.x, event.y, event.z]; }); }, + onError: (e) { + showDialog( + context: context, + builder: (context) { + return const AlertDialog( + title: Text("Sensor Not Found"), + content: Text( + "It seems that your device doesn't support Gyroscope Sensor"), + ); + }); + }, + cancelOnError: true, ), ); _streamSubscriptions.add( @@ -152,15 +197,18 @@ class _MyHomePageState extends State { _gyroscopeValues = [event.x, event.y, event.z]; }); }, - ), - ); - _streamSubscriptions.add( - userAccelerometerEvents.listen( - (UserAccelerometerEvent event) { - setState(() { - _userAccelerometerValues = [event.x, event.y, event.z]; - }); + onError: (e) { + showDialog( + context: context, + builder: (context) { + return const AlertDialog( + title: Text("Sensor Not Found"), + content: Text( + "It seems that your device doesn't support User Accelerometer Sensor"), + ); + }); }, + cancelOnError: true, ), ); _streamSubscriptions.add( @@ -170,6 +218,18 @@ class _MyHomePageState extends State { _magnetometerValues = [event.x, event.y, event.z]; }); }, + onError: (e) { + showDialog( + context: context, + builder: (context) { + return const AlertDialog( + title: Text("Sensor Not Found"), + content: Text( + "It seems that your device doesn't support Magnetometer Sensor"), + ); + }); + }, + cancelOnError: true, ), ); } diff --git a/packages/sensors_plus/example/pubspec.yaml b/packages/sensors_plus/example/pubspec.yaml index 2bc853464..21f0c13fd 100644 --- a/packages/sensors_plus/example/pubspec.yaml +++ b/packages/sensors_plus/example/pubspec.yaml @@ -9,18 +9,20 @@ environment: dependencies: flutter: sdk: flutter - sensors_plus: ^1.2.1 + sensors_plus: ^3.0.2 sensors_plus_tizen: path: ../ dev_dependencies: flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter integration_test_tizen: path: ../../integration_test/ - flutter_lints: ^1.0.4 + flutter_lints: ^2.0.1 flutter: uses-material-design: true diff --git a/packages/sensors_plus/pubspec.yaml b/packages/sensors_plus/pubspec.yaml index ab5eb794a..c7bc88fce 100644 --- a/packages/sensors_plus/pubspec.yaml +++ b/packages/sensors_plus/pubspec.yaml @@ -2,7 +2,7 @@ name: sensors_plus_tizen description: Tizen implementation of the sensors plugin. homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/master/packages/sensors_plus -version: 1.1.2 +version: 1.1.3 environment: sdk: ">=2.18.0 <4.0.0" @@ -18,7 +18,7 @@ flutter: dependencies: flutter: sdk: flutter - sensors_plus_platform_interface: ^1.1.0 + sensors_plus_platform_interface: ^1.1.3 dev_dependencies: - flutter_lints: ^1.0.4 + flutter_lints: ^2.0.1 diff --git a/packages/sensors_plus/tizen/src/device_sensor.cc b/packages/sensors_plus/tizen/src/device_sensor.cc index 2788ef2df..a840c510c 100644 --- a/packages/sensors_plus/tizen/src/device_sensor.cc +++ b/packages/sensors_plus/tizen/src/device_sensor.cc @@ -14,6 +14,8 @@ sensor_type_e ToTizenSensorType(const SensorType &sensor_type) { return SENSOR_ACCELEROMETER; case SensorType::kGyroscope: return SENSOR_GYROSCOPE; + case SensorType::kMagnetometer: + return SENSOR_MAGNETIC; case SensorType::kUserAccel: default: return SENSOR_LINEAR_ACCELERATION; @@ -37,6 +39,12 @@ DeviceSensor::~DeviceSensor() { } bool DeviceSensor::StartListen(SensorEventCallback callback) { + if (sensor_type_ == SensorType::kMagnetometer) { + LOG_ERROR("Not supported sensor type."); + last_error_ = SENSOR_ERROR_NOT_SUPPORTED; + return false; + } + if (is_listening_) { LOG_WARN("Already listening."); last_error_ = SENSOR_ERROR_OPERATION_FAILED; diff --git a/packages/sensors_plus/tizen/src/device_sensor.h b/packages/sensors_plus/tizen/src/device_sensor.h index 215692c10..ed55b148d 100644 --- a/packages/sensors_plus/tizen/src/device_sensor.h +++ b/packages/sensors_plus/tizen/src/device_sensor.h @@ -12,7 +12,7 @@ #include #include -enum class SensorType { kAccelerometer, kGyroscope, kUserAccel }; +enum class SensorType { kAccelerometer, kGyroscope, kUserAccel, kMagnetometer }; typedef std::vector SensorEvent; typedef std::function SensorEventCallback; diff --git a/packages/sensors_plus/tizen/src/sensors_plus_plugin.cc b/packages/sensors_plus/tizen/src/sensors_plus_plugin.cc index e7b1998cd..e36cb0c39 100644 --- a/packages/sensors_plus/tizen/src/sensors_plus_plugin.cc +++ b/packages/sensors_plus/tizen/src/sensors_plus_plugin.cc @@ -34,6 +34,7 @@ class DeviceSensorStreamHandler : public FlStreamHandler { events_->Success(flutter::EncodableValue(sensor_event)); }; if (!sensor_.StartListen(callback)) { + events_->Error(sensor_.GetLastErrorString()); return std::make_unique( std::to_string(sensor_.GetLastError()), sensor_.GetLastErrorString(), nullptr); @@ -67,30 +68,39 @@ class SensorsPlusPlugin : public flutter::Plugin { private: void SetupEventChannels(flutter::PluginRegistrar *registrar) { - accelerometer_channel_ = std::make_unique( - registrar->messenger(), - "dev.fluttercommunity.plus/sensors/accelerometer", - &flutter::StandardMethodCodec::GetInstance()); - accelerometer_channel_->SetStreamHandler( + std::unique_ptr accelerometer_channel = + std::make_unique( + registrar->messenger(), + "dev.fluttercommunity.plus/sensors/accelerometer", + &flutter::StandardMethodCodec::GetInstance()); + accelerometer_channel->SetStreamHandler( std::make_unique( SensorType::kAccelerometer)); - gyroscope_channel_ = std::make_unique( - registrar->messenger(), "dev.fluttercommunity.plus/sensors/gyroscope", - &flutter::StandardMethodCodec::GetInstance()); - gyroscope_channel_->SetStreamHandler( + std::unique_ptr gyroscope_channel = + std::make_unique( + registrar->messenger(), + "dev.fluttercommunity.plus/sensors/gyroscope", + &flutter::StandardMethodCodec::GetInstance()); + gyroscope_channel->SetStreamHandler( std::make_unique(SensorType::kGyroscope)); - user_accel_channel_ = std::make_unique( - registrar->messenger(), "dev.fluttercommunity.plus/sensors/user_accel", - &flutter::StandardMethodCodec::GetInstance()); - user_accel_channel_->SetStreamHandler( + std::unique_ptr user_accel_channel = + std::make_unique( + registrar->messenger(), + "dev.fluttercommunity.plus/sensors/user_accel", + &flutter::StandardMethodCodec::GetInstance()); + user_accel_channel->SetStreamHandler( std::make_unique(SensorType::kUserAccel)); - } - std::unique_ptr accelerometer_channel_; - std::unique_ptr gyroscope_channel_; - std::unique_ptr user_accel_channel_; + std::unique_ptr magnetometer_channel = + std::make_unique( + registrar->messenger(), + "dev.fluttercommunity.plus/sensors/magnetometer", + &flutter::StandardMethodCodec::GetInstance()); + magnetometer_channel->SetStreamHandler( + std::make_unique(SensorType::kMagnetometer)); + } }; void SensorsPlusPluginRegisterWithRegistrar( diff --git a/packages/sqflite/CHANGELOG.md b/packages/sqflite/CHANGELOG.md index 01ca170a3..9d314d42f 100644 --- a/packages/sqflite/CHANGELOG.md +++ b/packages/sqflite/CHANGELOG.md @@ -1,6 +1,8 @@ -## NEXT +## 0.1.3 +* Update sqflite to 2.3.0. * Increase the minimum Flutter version to 3.3. +* Increase the minimum SDK version to 3.0.0. ## 0.1.2 diff --git a/packages/sqflite/README.md b/packages/sqflite/README.md index 0b814d75f..7787a4409 100644 --- a/packages/sqflite/README.md +++ b/packages/sqflite/README.md @@ -10,8 +10,8 @@ This package is not an _endorsed_ implementation of `sqflite`. Therefore, you ha ```yaml dependencies: - sqflite: ^2.2.8 - sqflite_tizen: ^0.1.2 + sqflite: ^2.3.0 + sqflite_tizen: ^0.1.3 ``` Then you can import `sqflite` in your Dart code: diff --git a/packages/sqflite/analysis_options.yaml b/packages/sqflite/example/analysis_options.yaml similarity index 89% rename from packages/sqflite/analysis_options.yaml rename to packages/sqflite/example/analysis_options.yaml index cf5ccebd4..5b4bb8036 100644 --- a/packages/sqflite/analysis_options.yaml +++ b/packages/sqflite/example/analysis_options.yaml @@ -11,6 +11,10 @@ include: package:flutter_lints/flutter.yaml # https://github.com/flutter/flutter/blob/master/analysis_options.yaml analyzer: + language: + strict-casts: true + strict-inference: true + errors: # treat missing required parameters as a warning (not a hint) missing_required_param: warning @@ -89,9 +93,18 @@ linter: - unnecessary_new - unnecessary_null_in_if_null_operators - use_rethrow_when_possible + + - dangling_library_doc_comments + - deprecated_member_use_from_same_package + - implicit_reopen + - invalid_case_patterns + - no_literal_bool_comparisons + - no_self_assignments + - no_wildcard_variable_uses + - type_literal_in_constant_pattern # === doc rules === - public_member_api_docs # - - prefer_final_locals + # - prefer_final_locals - sort_constructors_first - sort_unnamed_constructors_first diff --git a/packages/sqflite/example/lib/batch_test_page.dart b/packages/sqflite/example/lib/batch_test_page.dart index 2f3f1c66f..66cb011c3 100644 --- a/packages/sqflite/example/lib/batch_test_page.dart +++ b/packages/sqflite/example/lib/batch_test_page.dart @@ -9,7 +9,7 @@ class BatchTestPage extends TestPage { BatchTestPage({Key? key}) : super('Batch tests', key: key) { test('BatchQuery', () async { // await Sqflite.devSetDebugModeOn(); - final path = await initDeleteDb('batch.db'); + final path = await initDeleteDb('batch_query.db'); final db = await openDatabase(path); // empty batch @@ -42,7 +42,7 @@ class BatchTestPage extends TestPage { await db.close(); }); test('Batch', () async { - // await Sqflite.devSetDebugModeOn(); + // await databaseFactory.devSetDebugModeOn(); final path = await initDeleteDb('batch.db'); final db = await openDatabase(path); @@ -50,7 +50,7 @@ class BatchTestPage extends TestPage { var batch = db.batch(); var results = await batch.commit(); expect(results.length, 0); - expect(results, []); + expect(results, isEmpty); // one create table batch = db.batch(); @@ -112,7 +112,7 @@ class BatchTestPage extends TestPage { where: 'name = ?', whereArgs: ['item']); batch.delete('Test', where: 'name = ?', whereArgs: ['item']); results = await batch.commit(noResult: true); - expect(results, []); + expect(results, isEmpty); await db.close(); }); @@ -139,6 +139,47 @@ class BatchTestPage extends TestPage { await db.close(); }); + test('Apply in database', () async { + // await Sqflite.devSetDebugModeOn(); + final path = await initDeleteDb('apply_in_database.db'); + final db = await openDatabase(path); + + late List results; + + final batch1 = db.batch(); + batch1.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)'); + final batch2 = db.batch(); + batch2.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item1']); + results = await batch1.apply(); + expect(results, [null]); + + results = await batch2.apply(); + expect(results, [1]); + await db.close(); + }); + + test('Apply in transaction', () async { + // await Sqflite.devSetDebugModeOn(); + final path = await initDeleteDb('apply_in_transaction.db'); + final db = await openDatabase(path); + + late List results; + + await db.transaction((txn) async { + final batch1 = txn.batch(); + batch1.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT)'); + final batch2 = txn.batch(); + batch2.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item1']); + results = await batch1.apply(); + expect(results, [null]); + + results = await batch2.apply(); + expect(results, [1]); + }); + + await db.close(); + }); + test('Batch continue on error', () async { // await Sqflite.devSetDebugModeOn(); final path = await initDeleteDb('batch_continue_on_error.db'); diff --git a/packages/sqflite/example/lib/database/database.dart b/packages/sqflite/example/lib/database/database.dart index b8fb4877b..0fb2b9b9f 100644 --- a/packages/sqflite/example/lib/database/database.dart +++ b/packages/sqflite/example/lib/database/database.dart @@ -1,25 +1,2 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:path/path.dart'; -import 'package:sqflite/sqflite.dart'; - -/// delete the db, create the folder and returns its path -Future initDeleteDb(String dbName) async { - final databasePath = await getDatabasesPath(); - final path = join(databasePath, dbName); - - // make sure the folder exists - // ignore: avoid_slow_async_io - if (await Directory(dirname(path)).exists()) { - await deleteDatabase(path); - } else { - try { - await Directory(dirname(path)).create(recursive: true); - } catch (e) { - // ignore: avoid_print - print(e); - } - } - return path; -} +export 'database_impl.dart'; +export 'database_io.dart' if (dart.library.html) 'database_web.dart'; diff --git a/packages/sqflite/example/lib/database/database_impl.dart b/packages/sqflite/example/lib/database/database_impl.dart new file mode 100644 index 000000000..ab9c3a028 --- /dev/null +++ b/packages/sqflite/example/lib/database/database_impl.dart @@ -0,0 +1,90 @@ +import 'dart:typed_data'; + +import 'package:sqflite/sqflite.dart'; + +import 'database.dart'; + +/// Custom platform Handler, need to handle Web or IO differently or from a +/// custom app +abstract class PlatformHandler { + /// delete the db, create the folder and returns its path + Future initDeleteDb(String dbName) async { + if (await databaseExists(dbName)) { + await deleteDatabase(dbName); + } + return dbName; + } + + /// Write the db file directly to the file system + Future writeFileAsBytes(String path, List bytes, + {bool flush = false}); + + /// Read a file as bytes + Future readFileAsBytes(String path); + + /// Write a file as a string + Future writeFileAsString(String path, String text, + {bool flush = false}); + + /// Read a file as a string + Future readFileAsString(String path); + + /// Check if a path exists. + Future pathExists(String path); + + /// Recursively create a directory + Future createDirectory(String path); + + /// Recursively delete a directory + Future deleteDirectory(String path); + + /// Check if a directory exists + Future existsDirectory(String path); +} + +// --- +// Compat, to keep the example page as is +// --- + +/// delete the db, create the folder and returnes its path +Future initDeleteDb(String dbName) => + platformHandler.initDeleteDb(dbName); + +/// Write the db file directly to the file system +Future writeFileAsBytes(String path, List bytes, + {bool flush = false}) => + platformHandler.writeFileAsBytes(path, bytes, flush: flush); + +/// Read a file as bytes +Future readFileAsBytes(String path) => + platformHandler.readFileAsBytes(path); + +/// Write a file as a string +Future writeFileAsString(String path, String text, + {bool flush = false}) => + platformHandler.writeFileAsString(path, text, flush: flush); + +/// Read a file as a string +Future readFileAsString(String path) => + platformHandler.readFileAsString(path); + +/// Check if a path exists. +Future pathExists(String path) => platformHandler.pathExists(path); + +/// Recursively create a directory +Future createDirectory(String path) => + platformHandler.createDirectory(path); + +/// Recursively delete a directory +Future deleteDirectory(String path) => + platformHandler.deleteDirectory(path); + +/// Check if a directory exists +Future existsDirectory(String path) => + platformHandler.existsDirectory(path); + +PlatformHandler? _platformHandler; + +/// Platform handler (can be overriden, needed for the web test app) +PlatformHandler get platformHandler => _platformHandler ??= platformHandlerIo; +set platformHandler(PlatformHandler handler) => _platformHandler = handler; diff --git a/packages/sqflite/example/lib/database/database_io.dart b/packages/sqflite/example/lib/database/database_io.dart new file mode 100644 index 000000000..52d22eefe --- /dev/null +++ b/packages/sqflite/example/lib/database/database_io.dart @@ -0,0 +1,86 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:path/path.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:sqflite_tizen_example/database/database.dart'; + +class _PlatformHandlerIo extends PlatformHandler { + /// delete the db, create the folder and returns its path + @override + Future initDeleteDb(String dbName) async { + final databasePath = await getDatabasesPath(); + // print(databasePath); + final path = join(databasePath, dbName); + + // make sure the folder exists + // ignore: avoid_slow_async_io + if (await Directory(dirname(path)).exists()) { + await deleteDatabase(path); + } else { + try { + await Directory(dirname(path)).create(recursive: true); + } catch (e) { + // ignore: avoid_print + print(e); + } + } + return path; + } + + /// Write the db file directly to the file system + @override + Future writeFileAsBytes(String path, List bytes, + {bool flush = false}) async { + await File(path).writeAsBytes(bytes, flush: flush); + } + + /// Read a file as bytes + @override + Future readFileAsBytes(String path) async { + return File(path).readAsBytes(); + } + + /// Write a file as a string + @override + Future writeFileAsString(String path, String text, + {bool flush = false}) async { + await File(path).writeAsString(text, flush: true); + } + + /// Read a file as a string + @override + Future readFileAsString(String path) async { + return File(path).readAsString(); + } + + /// Check if a path exists. + @override + Future pathExists(String path) async { + // ignore: avoid_slow_async_io + return File(path).exists(); + } + + /// Recursively create a directory + @override + Future createDirectory(String path) async { + await Directory(dirname(path)).create(recursive: true); + } + + /// Recursively delete a directory + @override + Future deleteDirectory(String path) async { + await Directory(path).delete(recursive: true); + } + + /// Check if a directory exists + @override + Future existsDirectory(String path) async { + // ignore: avoid_slow_async_io + return Directory(path).exists(); + } +} + +/// Io platform handler +PlatformHandler platformHandlerIo = _PlatformHandlerIo(); diff --git a/packages/sqflite/example/lib/database/database_web.dart b/packages/sqflite/example/lib/database/database_web.dart new file mode 100644 index 000000000..e303b693a --- /dev/null +++ b/packages/sqflite/example/lib/database/database_web.dart @@ -0,0 +1,5 @@ +import 'package:sqflite_tizen_example/database/database.dart'; + +/// platform handler io not supported on the web. +PlatformHandler get platformHandlerIo => + throw UnsupportedError('platform handler io not supported on the web'); diff --git a/packages/sqflite/example/lib/exception_test_page.dart b/packages/sqflite/example/lib/exception_test_page.dart index 7c49e5ecf..e360990f6 100644 --- a/packages/sqflite/example/lib/exception_test_page.dart +++ b/packages/sqflite/example/lib/exception_test_page.dart @@ -1,11 +1,8 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - import 'package:flutter/foundation.dart'; import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sql.dart'; +import 'src/common_import.dart'; import 'test_page.dart'; // ignore_for_file: avoid_print @@ -364,7 +361,7 @@ class ExceptionTestPage extends TestPage { }); test('Bind no argument (no iOS)', () async { - if (!Platform.isIOS) { + if (!platform.isIOS) { // await Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('bind_no_arg_failed.db'); final db = await openDatabase(path); @@ -383,7 +380,7 @@ class ExceptionTestPage extends TestPage { test('crash ios (no iOS)', () async { // This crashes natively on iOS...can't catch it yet - if (!Platform.isIOS) { + if (!platform.isIOS) { //if (true) { // await Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('bind_no_arg_failed.db'); diff --git a/packages/sqflite/example/lib/exp_test_page.dart b/packages/sqflite/example/lib/exp_test_page.dart index 326b7cacc..24b2e451f 100644 --- a/packages/sqflite/example/lib/exp_test_page.dart +++ b/packages/sqflite/example/lib/exp_test_page.dart @@ -1,6 +1,3 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; import 'dart:isolate'; import 'dart:typed_data'; @@ -9,6 +6,8 @@ import 'package:flutter/services.dart'; import 'package:path/path.dart'; import 'package:sqflite/sqflite.dart'; import 'package:sqflite_common/sqflite_dev.dart'; +import 'package:sqflite_tizen_example/src/common_import.dart'; +import 'package:sqflite_tizen_example/utils.dart'; import 'test_page.dart'; @@ -63,6 +62,7 @@ class ExpTestPage extends TestPage { }); test('in', () async { + //await Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('simple_exp.db'); final db = await openDatabase(path); @@ -101,6 +101,7 @@ class ExpTestPage extends TestPage { }); test('Raw escaping', () async { + //await Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('raw_escaping_fields.db'); final db = await openDatabase(path); @@ -129,6 +130,7 @@ class ExpTestPage extends TestPage { }); test('Escaping fields', () async { + //await Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('escaping_fields.db'); final db = await openDatabase(path); @@ -155,6 +157,7 @@ class ExpTestPage extends TestPage { }); test('Functions', () async { + //await Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('exp_functions.db'); final db = await openDatabase(path); @@ -199,6 +202,7 @@ class ExpTestPage extends TestPage { }); test('Alias', () async { + //await Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('exp_alias.db'); final db = await openDatabase(path); @@ -221,6 +225,7 @@ class ExpTestPage extends TestPage { }); test('Dart2 query', () async { + // await Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('exp_dart2_query.db'); final db = await openDatabase(path); @@ -280,6 +285,7 @@ class ExpTestPage extends TestPage { return rawResult; */ test('Issue#48', () async { + // Sqflite.devSetDebugModeOn(true); // devPrint('issue #48'); // Try to query on a non-indexed field final path = await initDeleteDb('exp_issue_48.db'); @@ -339,10 +345,10 @@ class ExpTestPage extends TestPage { await deleteDatabase(path); // Copy from asset - final data = await rootBundle.load(join('assets', 'issue_64.db')); + final data = await rootBundle.load(url.join('assets', 'issue_64.db')); final bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); - await File(path).writeAsBytes(bytes); + await databaseFactory.writeDatabaseBytes(path, bytes); // open the database final db = await openDatabase(path); @@ -387,17 +393,17 @@ INSERT INTO test (value) VALUES (1); INSERT INTO test (value) VALUES (10); '''; await db.execute(sql); - // that should be the expected result // var expectedResult = [ // {'value': 1}, // {'value': 10} // ]; final result = await db.rawQuery('SELECT * FROM $table'); + print(json.encode(result)); + // However (at least on Android) // result is empty, only the first statement is executed - print(json.encode(result)); - expect(result, []); + expect(result, isEmpty); } finally { await db.close(); } @@ -470,46 +476,80 @@ CREATE TABLE test ( } }); - test('Issue#206', () async { - final path = await initDeleteDb('issue_206.db'); + test('ATTACH database', () async { + final db1Path = await initDeleteDb('attach1.db'); + final db2Path = await initDeleteDb('attach2.db'); - final db = await openDatabase(path); + // Create some data on db1 and close it + var db1 = await databaseFactory.openDatabase(db1Path); try { - final sqls = LineSplitter.split( - '''CREATE VIRTUAL TABLE Food using fts4(description TEXT) - INSERT Into Food (description) VALUES ('banana') - INSERT Into Food (description) VALUES ('apple')'''); - final batch = db.batch(); - for (var sql in sqls) { - batch.execute(sql); - } + var batch = db1.batch(); + batch.execute('CREATE TABLE table1 (col1 INTEGER)'); + batch.insert('table1', {'col1': 1234}); await batch.commit(); + } finally { + await db1.close(); + } - final results = await db.rawQuery( - 'SELECT description, matchinfo(Food) as matchinfo FROM Food WHERE Food MATCH ?', - ['ban*']); - print(results); - // matchinfo is currently returned as binary bloc - expect(results.length, 1); - final map = results.first; - final matchInfo = map['matchinfo'] as Uint8List; - - // Convert to Uint32List - final uint32ListLength = matchInfo.length ~/ 4; - final uint32List = Uint32List(uint32ListLength); - final data = ByteData.view( - matchInfo.buffer, matchInfo.offsetInBytes, matchInfo.length); - for (var i = 0; i < uint32ListLength; i++) { - uint32List[i] = data.getUint32(i * 4, Endian.host); - } - // print(uint32List); - expect(uint32List, [1, 1, 1, 1, 1]); - expect(map['matchinfo'], const TypeMatcher()); + // Open a new db2 database, attach db1 and query it + + var db2 = await databaseFactory.openDatabase(db2Path); + try { + await db2.execute('ATTACH DATABASE \'$db1Path\' AS db1'); + var rows = await db2.query('db1.table1'); + expect(rows, [ + {'col1': 1234} + ]); } finally { - await db.close(); + await db2.close(); } }); + /// fts4 + var fts4Supports = supportsCompatMode; + if (fts4Supports) { + test('Issue#206', () async { + //await Sqflite.devSetDebugModeOn(true); + final path = await initDeleteDb('issue_206.db'); + + final db = await openDatabase(path); + try { + final sqls = LineSplitter.split( + '''CREATE VIRTUAL TABLE Food using fts4(description TEXT) + INSERT Into Food (description) VALUES ('banana') + INSERT Into Food (description) VALUES ('apple')'''); + final batch = db.batch(); + for (var sql in sqls) { + batch.execute(sql); + } + await batch.commit(); + + final results = await db.rawQuery( + 'SELECT description, matchinfo(Food) as matchinfo FROM Food WHERE Food MATCH ?', + ['ban*']); + print(results); + // matchinfo is currently returned as binary bloc + expect(results.length, 1); + final map = results.first; + final matchInfo = map['matchinfo'] as Uint8List; + + // Convert to Uint32List + final uint32ListLength = matchInfo.length ~/ 4; + final uint32List = Uint32List(uint32ListLength); + final data = ByteData.view( + matchInfo.buffer, matchInfo.offsetInBytes, matchInfo.length); + for (var i = 0; i < uint32ListLength; i++) { + uint32List[i] = data.getUint32(i * 4, Endian.host); + } + // print(uint32List); + expect(uint32List, [1, 1, 1, 1, 1]); + expect(map['matchinfo'], const TypeMatcher()); + } finally { + await db.close(); + } + }); + } + test('Log level', () async { // test setting log level Database? db; @@ -533,11 +573,81 @@ CREATE TABLE test ( } }); + Future testBigBlog(int size) async { + // await Sqflite.devSetDebugModeOn(true); + final path = await initDeleteDb('big_blob.db'); + var db = await openDatabase(path, version: 1, + onCreate: (Database db, int version) async { + await db + .execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, value BLOB)'); + }); + try { + var blob = + Uint8List.fromList(List.generate(size, (index) => index % 256)); + var id = await db.insert('Test', {'value': blob}); + + /// Get the value field from a given id + Future getValue(int id) async { + return ((await db.query('Test', where: 'id = $id')).first)['value'] + as Uint8List; + } + + expect((await getValue(id)).length, blob.length); + } finally { + await db.close(); + } + } + + // We don't test automatically above as it crashes seriously on Android + test('big blob 800 Ko', () async { + await testBigBlog(800000); + }); + + Future testBigText(int size) async { + // await Sqflite.devSetDebugModeOn(true); + final path = await initDeleteDb('big_text.db'); + var db = await openDatabase(path, version: 1, + onCreate: (Database db, int version) async { + await db + .execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, value TEXT)'); + }); + try { + var text = List.generate(size, (index) => 'A').join(); + var id = await db.insert('Test', {'value': text}); + + /// Get the value field from a given id + Future getValue(int id) async { + return ((await db.query('Test', where: 'id = $id')).first)['value'] + as String; + } + + expect((await getValue(id)).length, text.length); + } finally { + await db.close(); + } + } + + // We don't test automatically above as it crashes seriously on Android + test('big text 800 Ko', () async { + await testBigText(800000); + }); + /* + test('big blob 1500 Ko (fails on Android sqlite)', () async { + await testBigBlog(1500000); + }); + test('big blob 2 Mo (fails on Android sqlite)', () async { + await testBigBlog(2000000); + }); + test('big blob 15 Mo (fails on Android sqlite)', () async { + await testBigBlog(15000000); + }); + */ /* test('Isolate', () async { // This test does not work yet // Need background registration. I Kept the code for future reference await Future.sync(() async { + // await Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('isolate.db'); // Open the db in the main isolate @@ -558,7 +668,7 @@ CREATE TABLE test ( int index = 0; SendPort sendPort; List> results; - var completer = Completer(); + var completer = Completer(); var subscription = receivePort.listen((data) { switch (index++) { case 0: @@ -588,6 +698,40 @@ CREATE TABLE test ( }).timeout(Duration(seconds: 3)); }); */ + test('missing parameter', () async { + var db = await openDatabase(inMemoryDatabasePath); + await db.execute( + 'CREATE TABLE IF NOT EXISTS foo (id int primary key, name text)'); + var missingParameterShouldFail = !supportsCompatMode; + try { + await db.rawQuery('SELECT * FROM foo WHERE id=?'); + } catch (e) { + expect(missingParameterShouldFail, isTrue); + } + await db.close(); + }); + // Issue https://github.com/tekartik/sqflite/issues/929 + // Pragma has to use rawQuery...why, on sqflite Android + test('wal', () async { + // await Sqflite.devSetDebugModeOn(true); + var db = await openDatabase(inMemoryDatabasePath); + try { + await db.execute('PRAGMA journal_mode=WAL'); + } catch (e) { + print(e); + await db.rawQuery('PRAGMA journal_mode=WAL'); + } + await db.execute('CREATE TABLE test (id INTEGER)'); + await db.insert('test', {'id': 1}); + try { + var resultSet = await db.rawQuery('SELECT id FROM test'); + expect(resultSet, [ + {'id': 1}, + ]); + } finally { + await db.close(); + } + }); } } diff --git a/packages/sqflite/example/lib/main.dart b/packages/sqflite/example/lib/main.dart index 25d2d5d32..f395209c1 100644 --- a/packages/sqflite/example/lib/main.dart +++ b/packages/sqflite/example/lib/main.dart @@ -17,18 +17,27 @@ import 'todo_test_page.dart'; import 'type_test_page.dart'; void main() { - runApp(const MyApp()); + mainExampleApp(); +} + +/// Example app main entry point, exported for external application +/// +/// might move to a different shared package. +void mainExampleApp() { + WidgetsFlutterBinding.ensureInitialized(); + // debugAutoStartRouteName = testOpenRoute; + runApp(const SqfliteExampleApp()); } /// Sqflite test app -class MyApp extends StatefulWidget { +class SqfliteExampleApp extends StatefulWidget { /// test app. - const MyApp({Key? key}) : super(key: key); + const SqfliteExampleApp({Key? key}) : super(key: key); // This widget is the root of your application. @override // ignore: library_private_types_in_public_api - _MyAppState createState() => _MyAppState(); + _SqfliteExampleAppState createState() => _SqfliteExampleAppState(); } /// Simple test page. @@ -61,7 +70,7 @@ const String testExpRoute = '/test/exp'; /// Deprecated test page. const String testDeprecatedRoute = '/test/deprecated'; -class _MyAppState extends State { +class _SqfliteExampleAppState extends State { var routes = { '/test': (BuildContext context) => MyHomePage(), testRawRoute: (BuildContext context) => RawTestPage(), @@ -155,7 +164,7 @@ class _MyHomePageState extends State { void initState() { super.initState(); - Future.delayed(Duration.zero).then((_) async { + Future.delayed(Duration.zero).then((_) async { if (mounted) { // Use it to auto start a test page if (debugAutoStartRouteName != null) { diff --git a/packages/sqflite/example/lib/manual_test_page.dart b/packages/sqflite/example/lib/manual_test_page.dart index 187aa43e2..543e84f54 100644 --- a/packages/sqflite/example/lib/manual_test_page.dart +++ b/packages/sqflite/example/lib/manual_test_page.dart @@ -1,4 +1,4 @@ -import 'dart:async'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:sqflite/sqflite.dart'; @@ -8,10 +8,11 @@ import 'package:sqflite/utils/utils.dart'; import 'package:sqflite_tizen_example/src/item_widget.dart'; import 'package:sqflite_tizen_example/utils.dart'; -import 'model/item.dart'; - // ignore_for_file: avoid_print +import 'model/item.dart'; +import 'src/common_import.dart'; + /// Manual test page. class ManualTestPage extends StatefulWidget { /// Test page. @@ -26,6 +27,13 @@ class _ManualTestPageState extends State { Database? database; static const String dbName = 'manual_test.db'; + Future showToast(String message) async { + ScaffoldMessenger.of(context) + ..clearSnackBars() + ..showSnackBar(SnackBar( + content: Text(message), duration: const Duration(milliseconds: 300))); + } + Future _openDatabase() async { return database ??= await databaseFactory.openDatabase(dbName); } @@ -48,17 +56,59 @@ class _ManualTestPageState extends State { late List items; late List itemWidgets; - Future pop() async { - return true; + Future _addAndQuery({int? msDelay, bool? noSynchronized}) async { + // await databaseFactory.debugSetLogLevel(sqfliteLogLevelVerbose); + var db = await _openDatabase(); + + // ignore: invalid_use_of_visible_for_testing_member + db.internalsDoNotUseSynchronized = noSynchronized ?? false; + await db.transaction((txn) async { + await txn.execute( + 'CREATE TABLE IF NOT EXISTS Task(id INTEGER PRIMARY KEY, name TEXT)'); + await txn.execute('INSERT INTO Task(name) VALUES (?)', + ['task ${DateTime.now().toIso8601String()}']); + var count = + firstIntValue(await txn.query('Task', columns: [sqlCountColumn])); + unawaited(showToast('$count task(s)')); + if (msDelay != null) { + await Future.delayed(Duration(milliseconds: msDelay)); + } + }); } @override void didChangeDependencies() { super.didChangeDependencies(); items = [ + SqfMenuItem('SQLite version', () async { + final db = await openDatabase(inMemoryDatabasePath); + + final results = await db.rawQuery('select sqlite_version()'); + print('select sqlite_version(): $results'); + var version = results.first.values.first; + print('sqlite version: $version'); + await db.close(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('select sqlite_version(): $version'), + )); + } + }, summary: 'select sqlite_version()'), + SqfMenuItem('Factory information', () async { + var info = databaseFactory.toString(); + print('sqlite database factory: $info'); + unawaited(showToast(info)); + }, summary: 'toString()'), SqfMenuItem('openDatabase', () async { await _openDatabase(); }, summary: 'Open the database'), + SqfMenuItem('transaction add and query and pause', () async { + await _addAndQuery(msDelay: 5000); + }, summary: 'open/create table/add/query/pause'), + SqfMenuItem('transaction add and query and pause no synchronized', + () async { + await _addAndQuery(msDelay: 5000, noSynchronized: true); + }, summary: 'open/create table/add/query/pause'), SqfMenuItem('BEGIN EXCLUSIVE', () async { final db = await _openDatabase(); await db.execute('BEGIN EXCLUSIVE'); @@ -99,16 +149,48 @@ class _ManualTestPageState extends State { print(info.toString()); }, summary: 'Implementation info (dev only)'), SqfMenuItem('Increment version', () async { - await _incrementVersion(); + print(await _incrementVersion()); }, summary: 'Implementation info (dev only)'), SqfMenuItem('Multiple db', () async { - await Navigator.of(context).push(MaterialPageRoute(builder: (_) { + await Navigator.of(context).push(MaterialPageRoute(builder: (_) { return const MultipleDbTestPage(); })); - }, summary: 'Open multiple databases') + }, summary: 'Open multiple databases'), + ...[800000, 1500000, 15000000, 150000000] + .map((size) => SqfMenuItem('Big blob $size', () async { + await testBigBlog(size); + })) ]; } + Future testBigBlog(int size) async { + // await Sqflite.devSetDebugModeOn(true); + var db = await openDatabase(inMemoryDatabasePath, version: 1, + onCreate: (Database db, int version) async { + await db + .execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, value BLOB)'); + }); + try { + var blob = + Uint8List.fromList(List.generate(size, (index) => index % 256)); + var id = await db.insert('Test', {'value': blob}); + + /// Get the value field from a given id + Future getValue(int id) async { + return ((await db.query('Test', where: 'id = $id')).first)['value'] + as Uint8List; + } + + var ok = (await getValue(id)).length == blob.length; + if (mounted) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text('$size: $ok'))); + } + } finally { + await db.close(); + } + } + @override Widget build(BuildContext context) { itemWidgets = items @@ -133,11 +215,8 @@ class _ManualTestPageState extends State { appBar: AppBar( title: const Text('Manual tests'), ), - body: WillPopScope( - onWillPop: pop, - child: ListView( - children: itemWidgets, - ), + body: ListView( + children: itemWidgets, ), ); } @@ -154,7 +233,7 @@ class MultipleDbTestPage extends StatelessWidget { return ListTile( title: Text(name), onTap: () { - Navigator.of(context).push(MaterialPageRoute(builder: (_) { + Navigator.of(context).push(MaterialPageRoute(builder: (_) { return SimpleDbTestPage( dbName: name, ); diff --git a/packages/sqflite/example/lib/model/item.dart b/packages/sqflite/example/lib/model/item.dart index a6437ea97..ee0ac3df3 100644 --- a/packages/sqflite/example/lib/model/item.dart +++ b/packages/sqflite/example/lib/model/item.dart @@ -40,7 +40,7 @@ class SqfMenuItem extends Item { /// Run the item. Future run() { state = ItemState.running; - return Future.delayed(const Duration()).then((_) async { + return Future.delayed(const Duration()).then((_) async { try { await body(); state = ItemState.success; diff --git a/packages/sqflite/example/lib/open_test_page.dart b/packages/sqflite/example/lib/open_test_page.dart index 27f68adf9..c5835a7a7 100644 --- a/packages/sqflite/example/lib/open_test_page.dart +++ b/packages/sqflite/example/lib/open_test_page.dart @@ -1,6 +1,3 @@ -import 'dart:async'; -import 'dart:io'; - import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:flutter/services.dart'; @@ -12,11 +9,10 @@ import 'package:sqflite/src/database_mixin.dart' // ignore: implementation_impor import 'package:sqflite/src/factory_mixin.dart' // ignore: implementation_imports show SqfliteDatabaseFactoryMixin; -import 'package:sqflite_tizen_example/src/dev_utils.dart'; import 'package:synchronized/synchronized.dart'; +import 'src/common_import.dart'; import 'test_page.dart'; - // ignore_for_file: avoid_slow_async_io // ignore_for_file: avoid_print @@ -25,7 +21,7 @@ class OpenCallbacks { /// Open callbacks. OpenCallbacks() { onConfigure = (Database db) { - //print('onConfigure'); + // devPrint('onConfigure'); //verify(!onConfigureCalled, 'onConfigure must be called once'); expect(onConfigureCalled, false, reason: @@ -139,12 +135,13 @@ class OpenTestPage extends TestPage { final factory = databaseFactory; test('Databases path', () async { + // await Sqflite.devSetDebugModeOn(false); final databasesPath = await factory.getDatabasesPath(); // On Android we know it is current a 'databases' folder in the package folder print('databasesPath: $databasesPath'); - if (Platform.isAndroid) { + if (platform.isAndroid) { expect(basename(databasesPath), 'databases'); - } else if (Platform.isIOS) { + } else if (platform.isIOS) { expect(basename(databasesPath), 'Documents'); } final path = join(databasesPath, 'in_default_directory.db'); @@ -153,33 +150,36 @@ class OpenTestPage extends TestPage { await db.close(); }); test('Delete database', () async { + // await Sqflite.devSetDebugModeOn(false); final path = await initDeleteDb('delete_database.db'); expect(await databaseExists(path), false); final db = await openDatabase(path); await db.close(); - expect((await File(path).exists()), true); + expect((await pathExists(path)), true); expect(await databaseExists(path), true); print('Deleting database $path'); await deleteDatabase(path); - expect((await File(path).exists()), false); + expect((await pathExists(path)), false); expect(await databaseExists(path), false); }); test('Open no version', () async { + //await Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('open_no_version.db'); - expect((await File(path).exists()), false); + expect((await pathExists(path)), false); final db = await openDatabase(path); - verify(await File(path).exists()); + verify(await pathExists(path)); expect(await db.getVersion(), 0); await db.close(); }); test('isOpen', () async { + //await Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('is_open.db'); - expect((await File(path).exists()), false); + expect((await pathExists(path)), false); final db = await openDatabase(path); expect(db.isOpen, true); - verify(await File(path).exists()); + verify(await pathExists(path)); await db.close(); expect(db.isOpen, false); }); @@ -187,7 +187,7 @@ class OpenTestPage extends TestPage { test('Open no version onCreate', () async { // should fail final path = await initDeleteDb('open_no_version_on_create.db'); - verify(!(await File(path).exists())); + verify(!(await pathExists(path))); Database? db; try { db = await openDatabase(path, onCreate: (Database db, int version) { @@ -196,11 +196,12 @@ class OpenTestPage extends TestPage { }); verify(false); } on ArgumentError catch (_) {} - verify(!await File(path).exists()); + verify(!await pathExists(path)); expect(db, null); }); test('Open onCreate', () async { + // await Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('open_test2.db'); var onCreate = false; var onCreateTransaction = false; @@ -221,6 +222,7 @@ class OpenTestPage extends TestPage { }); test('Simple onCreate', () async { + // await Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('open_simple_on_create.db'); expect(await isDatabase(path), isFalse); @@ -249,6 +251,7 @@ class OpenTestPage extends TestPage { }); test('Open 2 databases', () async { + //await Sqflite.devSetDebugModeOn(true); final path1 = await initDeleteDb('open_db_1.db'); final path2 = await initDeleteDb('open_db_2.db'); final db1 = await openDatabase(path1, version: 1); @@ -258,6 +261,7 @@ class OpenTestPage extends TestPage { }); test('Open onUpgrade', () async { + // await Sqflite.devSetDebugModeOn(true); var onUpgrade = false; final path = await initDeleteDb('open_on_upgrade.db'); var database = await openDatabase(path, version: 1, @@ -293,6 +297,7 @@ class OpenTestPage extends TestPage { }); test('Open onDowngrade', () async { + // await Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('open_on_downgrade.db'); var database = await openDatabase(path, version: 2, onCreate: (Database db, int version) async { @@ -327,6 +332,7 @@ class OpenTestPage extends TestPage { }); test('Open asset database', () async { + // await Sqflite.devSetDebugModeOn(false); final databasesPath = await getDatabasesPath(); final path = join(databasesPath, 'asset_example.db'); @@ -335,15 +341,14 @@ class OpenTestPage extends TestPage { // Make sure the parent directory exists try { - await Directory(dirname(path)).create(recursive: true); + await createDirectory(path); } catch (_) {} - // Copy from asset - final data = await rootBundle.load(join('assets', 'example.db')); - final List bytes = +// Copy from asset to a database file. + final data = await rootBundle.load(url.join('assets', 'example.db')); + final bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); - // Write and flush the bytes written - await File(path).writeAsBytes(bytes, flush: true); + await databaseFactory.writeDatabaseBytes(path, bytes); // open the database final db = await openDatabase(path); @@ -378,6 +383,8 @@ class OpenTestPage extends TestPage { }); test('Open onDowngrade delete', () async { + // await Sqflite.devSetDebugModeOn(false); + final path = await initDeleteDb('open_on_downgrade_delete.db'); var database = await openDatabase(path, version: 3, onCreate: (Database db, int version) async { @@ -432,51 +439,54 @@ class OpenTestPage extends TestPage { }); test('All open callback', () async { + // await Sqflite.devSetDebugModeOn(false); final path = await initDeleteDb('open_all_callbacks.db'); var step = 1; final openCallbacks = OpenCallbacks(); var db = await openCallbacks.open(path, version: 1); - verify(openCallbacks.onConfigureCalled, 'onConfiguredCalled $step'); - verify(openCallbacks.onCreateCalled, 'onCreateCalled $step'); - verify(openCallbacks.onOpenCalled, 'onOpenCalled $step'); - verify(!openCallbacks.onUpgradeCalled!, 'onUpgradeCalled $step'); - verify(!openCallbacks.onDowngradeCalled!, 'onDowngradCalled $step'); - await db.close(); - - ++step; - db = await openCallbacks.open(path, version: 3); - verify(openCallbacks.onConfigureCalled, 'onConfiguredCalled $step'); - verify(!openCallbacks.onCreateCalled!, 'onCreateCalled $step'); - verify(openCallbacks.onOpenCalled, 'onOpenCalled $step'); - verify(openCallbacks.onUpgradeCalled, 'onUpgradeCalled $step'); - verify(!openCallbacks.onDowngradeCalled!, 'onDowngradCalled $step'); - await db.close(); - - ++step; - db = await openCallbacks.open(path, version: 2); - verify(openCallbacks.onConfigureCalled, 'onConfiguredCalled $step'); - verify(!openCallbacks.onCreateCalled!, 'onCreateCalled $step'); - verify(openCallbacks.onOpenCalled, 'onOpenCalled $step'); - verify(!openCallbacks.onUpgradeCalled!, 'onUpgradeCalled $step'); - verify(openCallbacks.onDowngradeCalled, 'onDowngradCalled $step'); - await db.close(); + try { + verify(openCallbacks.onConfigureCalled, 'onConfiguredCalled $step'); + verify(openCallbacks.onCreateCalled, 'onCreateCalled $step'); + verify(openCallbacks.onOpenCalled, 'onOpenCalled $step'); + verify(!openCallbacks.onUpgradeCalled!, 'onUpgradeCalled $step'); + verify(!openCallbacks.onDowngradeCalled!, 'onDowngradeCalled $step'); + await db.close(); - openCallbacks.onDowngrade = onDatabaseDowngradeDelete; - var configureCount = 0; - final callback = openCallbacks.onConfigure; - // allow being called twice - openCallbacks.onConfigure = (Database db) { - if (configureCount == 1) { - openCallbacks.onConfigureCalled = false; - } - configureCount++; - callback!(db); - }; - ++step; - db = await openCallbacks.open(path, version: 1); + ++step; + db = await openCallbacks.open(path, version: 3); + verify(openCallbacks.onConfigureCalled, 'onConfiguredCalled $step'); + verify(!openCallbacks.onCreateCalled!, 'onCreateCalled $step'); + verify(openCallbacks.onOpenCalled, 'onOpenCalled $step'); + verify(openCallbacks.onUpgradeCalled, 'onUpgradeCalled $step'); + verify(!openCallbacks.onDowngradeCalled!, 'onDowngradeCalled $step'); + await db.close(); - /* + ++step; + // devPrint('downgrading'); + db = await openCallbacks.open(path, version: 2); + verify(openCallbacks.onConfigureCalled, 'onConfiguredCalled $step'); + verify(!openCallbacks.onCreateCalled!, 'onCreateCalled $step'); + verify(openCallbacks.onOpenCalled, 'onOpenCalled $step'); + verify(!openCallbacks.onUpgradeCalled!, 'onDowngradeCalled $step'); + verify(openCallbacks.onDowngradeCalled, 'onDowngradCalled $step'); + await db.close(); + // devPrint('downgrading delete'); + openCallbacks.onDowngrade = onDatabaseDowngradeDelete; + var configureCount = 0; + final callback = openCallbacks.onConfigure; + // allow being called twice + openCallbacks.onConfigure = (Database db) { + if (configureCount == 1) { + openCallbacks.onConfigureCalled = false; + } + configureCount++; + callback!(db); + }; + ++step; + db = await openCallbacks.open(path, version: 1); + + /* verify(openCallbacks.onConfigureCalled,'onConfiguredCalled $step'); verify(configureCount == 2, 'onConfigure count'); verify(openCallbacks.onCreateCalled, 'onCreateCalled $step'); @@ -484,10 +494,13 @@ class OpenTestPage extends TestPage { verify(!openCallbacks.onUpgradeCalled, 'onUpgradeCalled $step'); verify(!openCallbacks.onDowngradeCalled, 'onDowngradCalled $step'); */ - await db.close(); + } finally { + await db.close(); + } }); test('Open batch', () async { + // await Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('open_batch.db'); Future onConfigure(Database db) async { @@ -498,13 +511,15 @@ class OpenTestPage extends TestPage { Future onCreate(Database db, int version) async { final batch = db.batch(); - batch.rawInsert('INSERT INTO Test(value) VALUES("value1")'); + // await db.execute('INSERT INTO Test(value) VALUES("value1")'); This does not work using ffi! + batch.execute('INSERT INTO Test(value) VALUES(?)', ['value1']); await batch.commit(); } Future onOpen(Database db) async { final batch = db.batch(); - batch.rawInsert('INSERT INTO Test(value) VALUES("value2")'); + //batch.rawInsert('INSERT INTO Test(value) VALUES("value2")'); + batch.rawInsert('INSERT INTO Test(value) VALUES(?)', ['value2']); await batch.commit(); } @@ -521,12 +536,14 @@ class OpenTestPage extends TestPage { }); test('Open read-only', () async { + // await Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('open_read_only.db'); Future onCreate(Database db, int version) async { final batch = db.batch(); batch.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, value TEXT)'); - batch.rawInsert('INSERT INTO Test(value) VALUES("value1")'); + //batch.rawInsert('INSERT INTO Test(value) VALUES("value1")'); This does not work using ffi + batch.rawInsert('INSERT INTO Test(value) VALUES(?)', ['value1']); await batch.commit(); } @@ -543,7 +560,7 @@ class OpenTestPage extends TestPage { 1); try { - await db.rawInsert('INSERT INTO Test(value) VALUES("value1")'); + await db.rawInsert('INSERT INTO Test(value) VALUES(?)', ['value1']); fail('should fail'); } on DatabaseException catch (e) { // Error DatabaseException(attempt to write a readonly database (code 8)) running Open read-only @@ -558,6 +575,7 @@ class OpenTestPage extends TestPage { }); test('Open demo (doc)', () async { + // await Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('open_read_only.db'); { @@ -623,10 +641,10 @@ class OpenTestPage extends TestPage { print('Creating new copy from asset'); // Copy from asset - final data = await rootBundle.load(join('assets', 'example.db')); + final data = await rootBundle.load(url.join('assets', 'example.db')); final bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); - await File(path).writeAsBytes(bytes); + await writeFileAsBytes(path, bytes); // open the database db = await openDatabase(path, readOnly: true); @@ -639,6 +657,7 @@ class OpenTestPage extends TestPage { }); test('Database locked (doc)', () async { + // await Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('open_locked.db'); final helper = Helper(path); @@ -652,6 +671,7 @@ class OpenTestPage extends TestPage { }); test('single/multi instance (using factory)', () async { + // await Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('instances_test.db'); Database? db1, db2, db3; try { @@ -671,6 +691,7 @@ class OpenTestPage extends TestPage { }); test('single/multi instance', () async { + // await Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('instances_test.db'); final db1 = await openDatabase(path, singleInstance: false); final db2 = await openDatabase(path, singleInstance: true); @@ -708,6 +729,7 @@ class OpenTestPage extends TestPage { }); test('Not in memory database', () async { + // await Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('not_in_memory.db'); var db = await openDatabase(path); @@ -727,37 +749,41 @@ class OpenTestPage extends TestPage { await db.close(); }); - test('open in sub directory fails', () async { + test('open in sub directory', () async { final databasesPath = await factory.getDatabasesPath(); final path = join(databasesPath, 'sub_that_should_not_exists'); try { - await Directory(path).delete(recursive: true); + await deleteDirectory(path); } catch (_) {} final dbPath = join(path, 'open.db'); try { - await factory.openDatabase(dbPath); + final db = await factory.openDatabase(dbPath); + await db.close(); } catch (e) { - print("couldn't open database in sub directory"); + print("couldn't open database in sub sub directory"); } }); - test('open in sub sub directory fails', () async { + test('open in sub sub directory', () async { + // await Sqflite.devSetDebugModeOn(true); final databasesPath = await factory.getDatabasesPath(); final path = join(databasesPath, 'sub2_that_should_not_exists', 'sub_sub'); try { - await Directory(path).delete(recursive: true); + await deleteDirectory(path); } catch (_) {} - expect(await Directory(path).exists(), false); + expect(await existsDirectory(path), false); final dbPath = join(path, 'open.db'); try { - await factory.openDatabase(dbPath); + final db = await factory.openDatabase(dbPath); + await db.close(); } catch (e) { print("couldn't open database in sub sub directory"); } }); test('open_close_open_no_wait', () async { + // await Sqflite.devSetDebugModeOn(true); const path = 'open_close_open_no_wait.db'; final factory = databaseFactory; await factory.deleteDatabase(path); @@ -779,6 +805,7 @@ class OpenTestPage extends TestPage { } }); test('close in transaction', () async { + // await Sqflite.devSetDebugModeOn(true); const path = 'test_close_in_transaction.db'; final factory = databaseFactory; await factory.deleteDatabase(path); @@ -796,8 +823,9 @@ class OpenTestPage extends TestPage { } }); - test('open in transaction', () async { - const path = 'test_close_in_transaction.db'; + test('Open in transaction', () async { + // await Sqflite.devSetDebugModeOn(true); + const path = 'test_open_in_transaction.db'; final factory = databaseFactory; await factory.deleteDatabase(path); var db = await factory.openDatabase(path, @@ -829,15 +857,16 @@ class OpenTestPage extends TestPage { test('Open non sqlite file', () async { // Kind of corruption simulation + // await Sqflite.devSetDebugModeOn(true); final factory = databaseFactory; final path = join(await factory.getDatabasesPath(), 'test_non_sqlite_file.db'); await factory.deleteDatabase(path); // Write dummy content - await File(path).writeAsString('dummy', flush: true); + await writeFileAsString(path, 'dummy', flush: true); // check content - expect(await File(path).readAsString(), 'dummy'); + expect(await readFileAsString(path), 'dummy'); // try read-only { @@ -856,14 +885,13 @@ class OpenTestPage extends TestPage { await db.close(); // check content - expect(await File(path).readAsString(), 'dummy'); + expect(await readFileAsString(path), 'dummy'); } expect(await isDatabase(path), isFalse); // try read-write const minExpectedSize = 1000; - expect( - (await File(path).readAsBytes()).length, lessThan(minExpectedSize)); + expect((await readFileAsBytes(path)).length, lessThan(minExpectedSize)); var db = await factory.openDatabase(path); try { @@ -877,10 +905,42 @@ class OpenTestPage extends TestPage { db = await factory.openDatabase(path, options: OpenDatabaseOptions(version: 1)); } catch (e) { - print('openDatabase error'); + print('getVersion error'); } await db.close(); }); + test('Read/write bytes', () async { + var path = await initDeleteDb('database_read_bytes.db'); + var writtenPath = await initDeleteDb('database_written_bytes.db'); + var db = await factory.openDatabase(path, + options: OpenDatabaseOptions( + version: 1, + onCreate: (db, version) async { + await db.execute( + 'CREATE TABLE Test(id INTEGER PRIMARY KEY, value TEXT)'); + })); + var textValue = 'value_to_read'; + await db.insert('Test', {'id': 1, 'value': textValue}); + expect(await db.query('Test'), [ + {'id': 1, 'value': textValue} + ]); + await db.close(); + var bytes = await factory.readDatabaseBytes(path); + //expect(bytes.length, 8192); + expect(bytes.sublist(0, 4), [ + 83, + 81, + 76, + 105, + ]); + + await factory.writeDatabaseBytes(writtenPath, bytes); + db = await factory.openDatabase(writtenPath); + expect(await db.query('Test'), [ + {'id': 1, 'value': textValue} + ]); + await db.close(); + }); } } diff --git a/packages/sqflite/example/lib/raw_test_page.dart b/packages/sqflite/example/lib/raw_test_page.dart index 42f9850d1..d02b8fc1b 100644 --- a/packages/sqflite/example/lib/raw_test_page.dart +++ b/packages/sqflite/example/lib/raw_test_page.dart @@ -1,13 +1,11 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; +import 'dart:io' as io; import 'package:flutter/foundation.dart'; import 'package:path/path.dart'; import 'package:sqflite/sqflite.dart'; import 'package:sqflite/utils/utils.dart'; -import 'package:sqflite_tizen_example/src/dev_utils.dart'; +import 'src/common_import.dart'; import 'test_page.dart'; // ignore_for_file: avoid_print @@ -17,6 +15,8 @@ class RawTestPage extends TestPage { /// Raw test page. RawTestPage({Key? key}) : super('Raw tests', key: key) { test('Simple', () async { + // await Sqflite.devSetDebugModeOn(true); + final path = await initDeleteDb('raw_simple.db'); final db = await openDatabase(path); try { @@ -44,6 +44,8 @@ class RawTestPage extends TestPage { }); test('Options', () async { + // Sqflite.devSetDebugModeOn(true); + final path = await initDeleteDb('raw_query_format.db'); final db = await openDatabase(path); try { @@ -54,36 +56,9 @@ class RawTestPage extends TestPage { batch.rawInsert('INSERT INTO Test (name) VALUES (?)', ['item 2']); await batch.commit(); - // ignore: deprecated_member_use, deprecated_member_use_from_same_package - var sqfliteOptions = SqfliteOptions()..queryAsMapList = true; - // ignore: deprecated_member_use - await Sqflite.devSetOptions(sqfliteOptions); var sql = 'SELECT id, name FROM Test'; // ignore: deprecated_member_use - var result = await db.devInvokeSqlMethod('query', sql); - var expected = [ - {'id': 1, 'name': 'item 1'}, - {'id': 2, 'name': 'item 2'} - ]; - print('result as map list $result'); - expect(result, expected); - - // empty - sql = 'SELECT id, name FROM Test WHERE id=1234'; - // ignore: deprecated_member_use - result = await db.devInvokeSqlMethod('query', sql); - expected = []; - print('result as map list $result'); - expect(result, expected); - - // ignore: deprecated_member_use, deprecated_member_use_from_same_package - sqfliteOptions = SqfliteOptions()..queryAsMapList = false; - // ignore: deprecated_member_use - await Sqflite.devSetOptions(sqfliteOptions); - - sql = 'SELECT id, name FROM Test'; - // ignore: deprecated_member_use - var resultSet = await db.devInvokeSqlMethod('query', sql); + var resultSet = await db.devInvokeSqlMethod('query', sql); var expectedResultSetMap = { 'columns': ['id', 'name'], 'rows': [ @@ -100,13 +75,24 @@ class RawTestPage extends TestPage { resultSet = await db.devInvokeSqlMethod('query', sql); expectedResultSetMap = {}; print('result as r/c $resultSet'); - expect(resultSet, expectedResultSetMap); + try { + // This might be just for compatibility + expect(resultSet, expectedResultSetMap); + } catch (e) { + // Allow empty result + expectedResultSetMap = { + 'columns': ['id', 'name'], + 'rows': [] + }; + expect(resultSet, expectedResultSetMap); + } } finally { await db.close(); } }); test('Transaction', () async { + //Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('simple_transaction.db'); final db = await openDatabase(path); try { @@ -117,7 +103,7 @@ class RawTestPage extends TestPage { await db.transaction((txn) async { final count = Sqflite.firstIntValue( await txn.rawQuery('SELECT COUNT(*) FROM Test'))!; - await Future.delayed(const Duration(milliseconds: 40)); + await Future.delayed(const Duration(milliseconds: 40)); await txn .rawInsert('INSERT INTO Test (name) VALUES (?)', ['item $i']); //print(await db.query('SELECT COUNT(*) FROM Test')); @@ -138,12 +124,13 @@ class RawTestPage extends TestPage { }); test('Concurrency 1', () async { + // Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('simple_concurrency_1.db'); final db = await openDatabase(path); try { - final step1 = Completer(); - final step2 = Completer(); - final step3 = Completer(); + final step1 = Completer(); + final step2 = Completer(); + final step3 = Completer(); Future action1() async { await db @@ -193,12 +180,13 @@ class RawTestPage extends TestPage { }); test('Concurrency 2', () async { + // Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('simple_concurrency_2.db'); final db = await openDatabase(path); try { - final step1 = Completer(); - final step2 = Completer(); - final step3 = Completer(); + final step1 = Completer(); + final step2 = Completer(); + final step3 = Completer(); Future action1() async { await db @@ -272,6 +260,7 @@ class RawTestPage extends TestPage { }); test('Transaction open twice', () async { + //Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('transaction_open_twice.db'); final db = await openDatabase(path); Database? db2; @@ -308,6 +297,7 @@ class RawTestPage extends TestPage { }); test('Demo', () async { + // await Sqflite.devSetDebugModeOn(); final path = await initDeleteDb('simple_demo.db'); final database = await openDatabase(path); try { @@ -323,8 +313,11 @@ class RawTestPage extends TestPage { 'CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT, value INTEGER, num REAL)'); print('table created'); var id = await database.rawInsert( - 'INSERT INTO Test(name, value, num) VALUES("some name",1234,?)', - [456.789]); + // This does not work using ffi + // 'INSERT INTO Test(name, value, num) VALUES("some name",1234,?)', + // [456.789]); + 'INSERT INTO Test(name, value, num) VALUES(?,1234,?)', + ['some name', 456.789]); print('inserted1: $id'); id = await database.rawInsert( 'INSERT INTO Test(name, value) VALUES(?, ?)', @@ -369,8 +362,8 @@ class RawTestPage extends TestPage { // Make sure the directory exists try { // ignore: avoid_slow_async_io - if (!await Directory(databasesPath).exists()) { - await Directory(databasesPath).create(recursive: true); + if (!await io.Directory(databasesPath).exists()) { + await io.Directory(databasesPath).create(recursive: true); } } catch (_) {} @@ -390,7 +383,9 @@ class RawTestPage extends TestPage { // Insert some records in a transaction await database.transaction((txn) async { final id1 = await txn.rawInsert( - 'INSERT INTO Test(name, value, num) VALUES("some name", 1234, 456.789)'); + // 'INSERT INTO Test(name, value, num) VALUES("some name", 1234, 456.789)'); This does not work using ffi + 'INSERT INTO Test(name, value, num) VALUES(?, 1234, 456.789)', + ['some name']); print('inserted1: $id1'); final id2 = await txn.rawInsert( 'INSERT INTO Test(name, value, num) VALUES(?, ?, ?)', @@ -430,6 +425,7 @@ class RawTestPage extends TestPage { }); test('Open twice', () async { + // Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('open_twice.db'); final db = await openDatabase(path); Database? db2; @@ -448,6 +444,7 @@ class RawTestPage extends TestPage { }); test('text primary key', () async { + // Sqflite.devSetDebugModeOn(true); final path = await initDeleteDb('text_primary_key.db'); final db = await openDatabase(path); try { @@ -473,10 +470,9 @@ class RawTestPage extends TestPage { } }); - test('without rowid', () async { - // Inserts into WITHOUT ROWID tables are not recorded. If no successful INSERTs - // into rowid tables have ever occurred on the database connection, then returns zero. - // Ref: https://www.sqlite.org/c3ref/last_insert_rowid.html + test('Without rowid', () async { + // Sqflite.devSetDebugModeOn(true); + // this fails on iOS late Database db; try { @@ -488,8 +484,19 @@ class RawTestPage extends TestPage { .execute('CREATE TABLE Test (name TEXT PRIMARY KEY) WITHOUT ROWID'); var id = await db.insert('Test', {'name': 'test'}); expect(id, 0); + id = await db.insert('Test', {'name': 'other'}); expect(id, 0); + + // Insert conflict + // Only tested on Android for now... + try { + id = await db.insert('Test', {'name': 'other'}); + } on DatabaseException catch (e) { + // Test.name (code 1555 SQLITE_CONSTRAINT_PRIMARYKEY)) sql 'INSERT INTO Test (name) VALUES (?)' args [other] running without rowid + expect(e.getResultCode(), 1555); + } + // notice the order is based on the primary key final list = await db.query('Test'); expect(list, [ @@ -532,7 +539,7 @@ class RawTestPage extends TestPage { } }); - test('Binding null', () async { + test('Binding null (fails on Android)', () async { final db = await openDatabase(inMemoryDatabasePath); try { for (var value in [null, 2]) { @@ -545,5 +552,78 @@ class RawTestPage extends TestPage { await db.close(); } }); + + test('Query by page', () async { + // await databaseFactory.debugSetLogLevel(sqfliteLogLevelVerbose); + + //final path = await initDeleteDb('query_by_page.db'); + //final db = await openDatabase(path); + final db = await openDatabase(inMemoryDatabasePath); + try { + await db.execute(''' + CREATE TABLE test ( + id INTEGER PRIMARY KEY + )'''); + await db.insert('test', {'id': 1}); + await db.insert('test', {'id': 2}); + await db.insert('test', {'id': 3}); + var resultsList = []; + + // Use a cursor + var cursor = + await db.rawQueryCursor('SELECT * FROM test', null, bufferSize: 2); + resultsList.clear(); + var results = >[]; + while (await cursor.moveNext()) { + results.add(cursor.current); + } + expect(results, [ + {'id': 1}, + {'id': 2}, + {'id': 3} + ]); + + // Multiple cursors a cursor + var cursor1 = + await db.rawQueryCursor('SELECT * FROM test', null, bufferSize: 2); + var cursor2 = + await db.rawQueryCursor('SELECT * FROM test', null, bufferSize: 1); + await cursor1.moveNext(); + expect(cursor1.current.values, [1]); + await cursor2.moveNext(); + await cursor2.moveNext(); + expect(cursor2.current.values, [2]); + await cursor1.moveNext(); + expect(cursor1.current.values, [2]); + await cursor1.close(); + await cursor1.close(); // ok to call twice + try { + cursor1.current.values; + fail('should fail get current'); + } on StateError catch (_) {} + await cursor2.moveNext(); + expect(cursor2.current.values, [3]); + expect(await cursor2.moveNext(), isFalse); + expect(await cursor1.moveNext(), isFalse); + try { + cursor2.current.values; + fail('should fail get current'); + } on StateError catch (_) {} + + // No data + cursor = await db.rawQueryCursor('SELECT * FROM test WHERE id > ?', [3], + bufferSize: 2); + expect(await cursor.moveNext(), isFalse); + + // Matching page size + cursor = await db.rawQueryCursor('SELECT * FROM test WHERE id > ?', [1], + bufferSize: 2); + expect(await cursor.moveNext(), isTrue); + expect(await cursor.moveNext(), isTrue); + expect(await cursor.moveNext(), isFalse); + } finally { + await db.close(); + } + }); } } diff --git a/packages/sqflite/example/lib/slow_test_page.dart b/packages/sqflite/example/lib/slow_test_page.dart index c3c060229..a1d8a0c3f 100644 --- a/packages/sqflite/example/lib/slow_test_page.dart +++ b/packages/sqflite/example/lib/slow_test_page.dart @@ -1,8 +1,7 @@ -import 'dart:io'; - import 'package:flutter/foundation.dart'; import 'package:sqflite/sqflite.dart'; +import 'src/common_import.dart'; import 'test_page.dart'; // ignore_for_file: avoid_print @@ -79,7 +78,7 @@ class SlowTestPage extends TestPage { await perfDo(count); }); - if (Platform.isAndroid) { + if (platform.isAndroid) { test('Perf android NORMAL_PRIORITY', () async { // ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package await Sqflite.devSetOptions( @@ -147,4 +146,8 @@ class SlowTestPage extends TestPage { print('1000 insert ${sw.elapsed}'); await db.close(); } + +// 2019-02-26 + +// BACKGROUND } diff --git a/packages/sqflite/example/lib/src/common_import.dart b/packages/sqflite/example/lib/src/common_import.dart index edafc0cf4..9637df3b1 100644 --- a/packages/sqflite/example/lib/src/common_import.dart +++ b/packages/sqflite/example/lib/src/common_import.dart @@ -2,5 +2,7 @@ export 'dart:async'; export 'dart:convert'; export 'package:collection/collection.dart'; +export 'package:sqflite_common/src/internals.dart'; +export 'package:sqflite_common/src/platform/platform.dart'; export 'dev_utils.dart'; diff --git a/packages/sqflite/example/lib/src/expect.dart b/packages/sqflite/example/lib/src/expect.dart index a4d2ffd86..46c9e8d20 100644 --- a/packages/sqflite/example/lib/src/expect.dart +++ b/packages/sqflite/example/lib/src/expect.dart @@ -49,10 +49,10 @@ typedef ErrorFormatter = String Function(dynamic actual, Matcher matcher, /// you want to wait for the matcher to complete before continuing the test, you /// can call [expectLater] instead and `await` the result. void expect( - actual, - matcher, { + Object? actual, + Object? matcher, { String? reason, - skip, + Object? skip, }) { _expect(actual, matcher, reason: reason, skip: skip); } @@ -68,10 +68,11 @@ void expect( /// /// If the matcher fails asynchronously, that failure is piped to the returned /// future where it can be handled by user code. -Future expectLater(actual, matcher, {String? reason, skip}) => +Future expectLater(Object? actual, Object? matcher, + {String? reason, Object? skip}) => _expect(actual, matcher, reason: reason, skip: skip) as Future; -String _formatFailure(Matcher expected, actual, String which, +String _formatFailure(Matcher expected, Object? actual, String which, {String? reason}) { final buffer = StringBuffer(); buffer.writeln(indent(prettyPrint(expected), first: 'Expected: ')); @@ -82,9 +83,9 @@ String _formatFailure(Matcher expected, actual, String which, } /// The implementation of [expect] and [expectLater]. -FutureOr _expect(actual, matcher, +FutureOr _expect(Object? actual, Object? matcher, {String? reason, - skip, + Object? skip, bool verbose = false, // ignore: deprecated_member_use, deprecated_member_use_from_same_package ErrorFormatter? formatter}) { @@ -103,7 +104,7 @@ FutureOr _expect(actual, matcher, matcher = wrapMatcher(matcher); - final matchState = {}; + final matchState = {}; try { if ((matcher as Matcher).matches(actual, matchState)) { return Future.sync(() {}); @@ -116,7 +117,7 @@ FutureOr _expect(actual, matcher, /// Convenience method for throwing a new [TestFailure] with the provided /// [message]. -void fail([String? message]) => throw TestFailure(message ?? 'should fail'); +Never fail([String? message]) => throw TestFailure(message ?? 'should fail'); /// index text helper. String indent(String text, {String? first}) { @@ -136,7 +137,8 @@ String prettyPrint(dynamic text, {String? first}) { /// The default error formatter. @Deprecated('Will be removed in 0.13.0.') -String formatFailure(Matcher expected, actual, String which, {String? reason}) { +String formatFailure(Matcher expected, Object? actual, String which, + {String? reason}) { final buffer = StringBuffer(); buffer.writeln(indent(prettyPrint(expected), first: 'Expected: ')); buffer.writeln(indent(prettyPrint(actual), first: ' Actual: ')); diff --git a/packages/sqflite/example/lib/src/item_widget.dart b/packages/sqflite/example/lib/src/item_widget.dart index 80c4f601c..f263690f4 100644 --- a/packages/sqflite/example/lib/src/item_widget.dart +++ b/packages/sqflite/example/lib/src/item_widget.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; - -import '../model/item.dart'; +import 'package:sqflite_tizen_example/model/item.dart'; /// Item widget. class ItemWidget extends StatefulWidget { @@ -15,7 +14,7 @@ class ItemWidget extends StatefulWidget { final Item item; /// Action when pressed (typically run). - final Function(Item item) onTap; // = Function(MainItem item); + final void Function(Item item) onTap; // = Function(MainItem item); @override // ignore: library_private_types_in_public_api diff --git a/packages/sqflite/example/lib/src/main_item_widget.dart b/packages/sqflite/example/lib/src/main_item_widget.dart index 03696db7d..26fbf8146 100644 --- a/packages/sqflite/example/lib/src/main_item_widget.dart +++ b/packages/sqflite/example/lib/src/main_item_widget.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; - -import '../model/main_item.dart'; +import 'package:sqflite_tizen_example/model/main_item.dart'; /// Main item widget. class MainItemWidget extends StatefulWidget { @@ -11,7 +10,7 @@ class MainItemWidget extends StatefulWidget { final MainItem item; /// onTap action (typically run or open). - final Function(MainItem item) onTap; // = Function(MainItem item); + final void Function(MainItem item) onTap; // = Function(MainItem item); @override // ignore: library_private_types_in_public_api diff --git a/packages/sqflite/example/lib/test_page.dart b/packages/sqflite/example/lib/test_page.dart index 2a056e801..ecc11f45d 100644 --- a/packages/sqflite/example/lib/test_page.dart +++ b/packages/sqflite/example/lib/test_page.dart @@ -12,7 +12,6 @@ export 'package:matcher/matcher.dart'; export 'package:sqflite_tizen_example/database/database.dart'; export 'src/expect.dart' show expect, fail; - // ignore_for_file: avoid_print /// Base test page. @@ -71,7 +70,7 @@ bool? verify(bool? condition, [String? message]) { } /// Group. -abstract class Group { +abstract mixin class Group { /// List of tests. List get tests; diff --git a/packages/sqflite/example/lib/type_test_page.dart b/packages/sqflite/example/lib/type_test_page.dart index 60c3dd385..65546d37c 100644 --- a/packages/sqflite/example/lib/type_test_page.dart +++ b/packages/sqflite/example/lib/type_test_page.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:sqflite/sqflite.dart'; +import 'package:sqflite_tizen_example/utils.dart'; import 'test_page.dart'; @@ -50,10 +51,10 @@ class TypeTestPage extends TestPage { id = await insertValue(value); //devPrint('${value} ${await getValue(id)}'); expect(await getValue(id), value, reason: '$value ${await getValue(id)}'); + /* id = await insertValue(pow(2, 63)); - // devPrint('2^63: ${pow(2, 63)} ${await getValue(id)}'); - assert(await getValue(id) == pow(2, 63), - '2^63: ${pow(2, 63)} ${await getValue(id)}'); + devPrint('2^63: ${pow(2, 63)} ${await getValue(id)}'); + assert(await getValue(id) == pow(2, 63), '2^63: ${pow(2, 63)} ${await getValue(id)}'); // more then 64 bits id = await insertValue(pow(2, 65)); @@ -62,6 +63,7 @@ class TypeTestPage extends TestPage { // more then 128 bits id = await insertValue(pow(2, 129)); assert(await getValue(id) == pow(2, 129)); + */ await data.db.close(); }); @@ -141,11 +143,14 @@ class TypeTestPage extends TestPage { //print(await getValue(id)); //assert(eq.equals(await getValue(id), [])); - final blob1234 = [1, 2, 3, 4]; + var blob1234 = [1, 2, 3, 4]; + if (!supportsCompatMode) { + blob1234 = Uint8List.fromList(blob1234); + } id = await insertValue(blob1234); - final value = (await getValue(id)) as List; + dynamic value = (await getValue(id)) as List; print(value); - print('${value.length}'); + print('${(value as List).length}'); expect(value, blob1234, reason: '${await getValue(id)}'); // test hex feature on sqlite @@ -153,12 +158,12 @@ class TypeTestPage extends TestPage { .rawQuery('SELECT hex(value) FROM Test WHERE id = ?', [id]); expect(hexResult[0].values.first, '01020304'); - // try blob lookup + // try blob lookup (works on Android since 2022-09-19) var rows = await data.db .rawQuery('SELECT * FROM Test WHERE value = ?', [blob1234]); expect(rows.length, 1); - // // try blob lookup using hex + // try blob lookup using hex rows = await data.db.rawQuery( 'SELECT * FROM Test WHERE hex(value) = ?', [Sqflite.hex(blob1234)]); expect(rows.length, 1); @@ -169,7 +174,6 @@ class TypeTestPage extends TestPage { // final blobEmpty = Uint8List(0); // id = await insertValue(blobEmpty); // value = await getValue(id); - // print(value); // expect(value, const TypeMatcher()); // expect(value, isEmpty); } finally { @@ -235,7 +239,10 @@ class TypeTestPage extends TestPage { } on ArgumentError catch (_) { failed = true; } - expect(failed, isFalse); + if (supportsCompatMode) { + print('for now bool are accepted but inconsistent on iOS/Android'); + expect(failed, isFalse); + } } finally { await data.db.close(); } diff --git a/packages/sqflite/example/lib/utils.dart b/packages/sqflite/example/lib/utils.dart index de4d5650d..cf8cfc7c6 100644 --- a/packages/sqflite/example/lib/utils.dart +++ b/packages/sqflite/example/lib/utils.dart @@ -1,5 +1,12 @@ -export 'dart:io' hide sleep; +import 'package:sqflite/sqflite.dart'; +import 'package:sqflite/sqflite_dev.dart'; /// Usage: await sleep(500); Future sleep([int milliseconds = 0]) => - Future.delayed(Duration(milliseconds: milliseconds)); + Future.delayed(Duration(milliseconds: milliseconds)); + +/// Supports compat mode (devSetDebugModeOn, queryAsMap, fts4, some error handled - missing parameter, bad file) +bool get supportsCompatMode { + // ignore: invalid_use_of_visible_for_testing_member + return databaseFactory == sqfliteDatabaseFactoryDefault; +} diff --git a/packages/sqflite/example/pubspec.yaml b/packages/sqflite/example/pubspec.yaml index 805cdda75..8e27f923a 100644 --- a/packages/sqflite/example/pubspec.yaml +++ b/packages/sqflite/example/pubspec.yaml @@ -3,7 +3,7 @@ description: Demonstrates how to use the sqflite_tizen plugin. publish_to: "none" environment: - sdk: ">=2.18.0 <4.0.0" + sdk: ">=3.0.0 <4.0.0" flutter: ">=3.3.0" dependencies: @@ -11,7 +11,7 @@ dependencies: collection: any flutter: sdk: flutter - sqflite: ^2.2.8 + sqflite: ^2.3.0 sqflite_common: sqflite_tizen: path: ../ @@ -21,7 +21,7 @@ dependencies: dev_dependencies: pedantic: ^1.11.0 - flutter_lints: + flutter_lints: ^2.0.0 flutter_test: sdk: flutter flutter_driver: diff --git a/packages/sqflite/lib/sqflite_tizen.dart b/packages/sqflite/lib/sqflite_tizen.dart index ae494cf2a..4890440a0 100644 --- a/packages/sqflite/lib/sqflite_tizen.dart +++ b/packages/sqflite/lib/sqflite_tizen.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: avoid_classes_with_only_static_members + import 'package:sqflite/sqflite.dart'; /// A class to initialize the plugin. diff --git a/packages/sqflite/pubspec.yaml b/packages/sqflite/pubspec.yaml index 4abdca4e0..e076fb171 100644 --- a/packages/sqflite/pubspec.yaml +++ b/packages/sqflite/pubspec.yaml @@ -2,10 +2,10 @@ name: sqflite_tizen description: Tizen implementation of the sqflite plugin. homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/master/packages/sqflite -version: 0.1.2 +version: 0.1.3 environment: - sdk: ">=2.18.0 <4.0.0" + sdk: ">=3.0.0 <4.0.0" flutter: ">=3.3.0" flutter: @@ -19,7 +19,4 @@ flutter: dependencies: flutter: sdk: flutter - sqflite: ^2.2.8 - -dev_dependencies: - flutter_lints: ^1.0.4 + sqflite: ^2.3.0 diff --git a/packages/video_player/CHANGELOG.md b/packages/video_player/CHANGELOG.md index e10a675d3..13478ee63 100644 --- a/packages/video_player/CHANGELOG.md +++ b/packages/video_player/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.4.9 + +* Fix event channel issue, sending messages from native to Flutter on the platform thread. + +## 2.4.8 + +* Disable screensaver when player is playing. + ## 2.4.7 * Update pigeon to 10.0.0. diff --git a/packages/video_player/README.md b/packages/video_player/README.md index b1f06545c..426a663cc 100644 --- a/packages/video_player/README.md +++ b/packages/video_player/README.md @@ -15,7 +15,7 @@ This package is not an _endorsed_ implementation of `video_player`. Therefore, y ```yaml dependencies: video_player: ^2.4.2 - video_player_tizen: ^2.4.7 + video_player_tizen: ^2.4.9 ``` Then you can import `video_player` in your Dart code: diff --git a/packages/video_player/pubspec.yaml b/packages/video_player/pubspec.yaml index 1f6c7c07e..55dec9357 100644 --- a/packages/video_player/pubspec.yaml +++ b/packages/video_player/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_tizen description: Tizen implementation of the video_player plugin. homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/master/packages/video_player -version: 2.4.7 +version: 2.4.9 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/video_player/tizen/src/video_player.cc b/packages/video_player/tizen/src/video_player.cc index c2113a878..18b855d8e 100644 --- a/packages/video_player/tizen/src/video_player.cc +++ b/packages/video_player/tizen/src/video_player.cc @@ -4,6 +4,7 @@ #include "video_player.h" +#include #include #include @@ -78,9 +79,45 @@ FlutterDesktopGpuSurfaceDescriptor *VideoPlayer::ObtainGpuSurface( return gpu_surface_.get(); } +void VideoPlayer::InitScreenSaverApi() { + screensaver_handle_ = dlopen("libcapi-screensaver.so", RTLD_LAZY); + if (!screensaver_handle_) { + LOG_ERROR("[VideoPlayer] dlopen failed: %s", dlerror()); + return; + } + + screensaver_reset_timeout_ = reinterpret_cast( + dlsym(screensaver_handle_, "screensaver_reset_timeout")); + if (!screensaver_reset_timeout_) { + LOG_ERROR("[VideoPlayer] Symbol not found: %s", dlerror()); + return; + } + + ScreensaverOverrideReset screensaver_override_reset = + reinterpret_cast( + dlsym(screensaver_handle_, "screensaver_override_reset")); + if (!screensaver_override_reset) { + LOG_ERROR("[VideoPlayer] Symbol not found: %s", dlerror()); + return; + } + + int ret = screensaver_override_reset(false); + if (ret != 0) { + LOG_ERROR("screensaver_override_reset failed: %s", get_error_message(ret)); + return; + } +} + VideoPlayer::VideoPlayer(flutter::PluginRegistrar *plugin_registrar, flutter::TextureRegistrar *texture_registrar, const std::string &uri, VideoPlayerOptions &options) { + sink_event_pipe_ = ecore_pipe_add( + [](void *data, void *buffer, unsigned int nbyte) -> void { + auto *self = static_cast(data); + self->SendPendingEvents(); + }, + this); + texture_registrar_ = texture_registrar; texture_variant_ = @@ -155,6 +192,9 @@ VideoPlayer::VideoPlayer(flutter::PluginRegistrar *plugin_registrar, get_error_message(ret)); } +#ifdef TV_PROFILE + InitScreenSaverApi(); +#endif SetUpEventChannel(plugin_registrar->messenger()); } @@ -172,6 +212,45 @@ VideoPlayer::~VideoPlayer() { } } +void VideoPlayer::SendPendingEvents() { + std::lock_guard lock(queue_mutex_); + while (!encodable_event_queue_.empty()) { + if (event_sink_) { + event_sink_->Success(encodable_event_queue_.front()); + } + encodable_event_queue_.pop(); + } + + while (!error_event_queue_.empty()) { + if (event_sink_) { + event_sink_->Error(error_event_queue_.front().first, + error_event_queue_.front().second); + } + error_event_queue_.pop(); + } +} + +void VideoPlayer::PushEvent(const flutter::EncodableValue &encodable_value) { + if (!event_sink_) { + LOG_ERROR("[VideoPlayer] event sink is nullptr."); + return; + } + std::lock_guard lock(queue_mutex_); + encodable_event_queue_.push(encodable_value); + ecore_pipe_write(sink_event_pipe_, nullptr, 0); +} + +void VideoPlayer::SendError(const std::string &error_code, + const std::string &error_message) { + if (!event_sink_) { + LOG_ERROR("[VideoPlayer] event sink is nullptr."); + return; + } + std::lock_guard lock(queue_mutex_); + error_event_queue_.push(std::make_pair(error_code, error_message)); + ecore_pipe_write(sink_event_pipe_, nullptr, 0); +} + void VideoPlayer::Play() { LOG_DEBUG("[VideoPlayer] Player starting."); @@ -188,6 +267,9 @@ void VideoPlayer::Play() { if (ret != PLAYER_ERROR_NONE) { throw VideoPlayerError("player_start failed", get_error_message(ret)); } +#ifdef TV_PROFILE + timer_ = ecore_timer_add(30, ResetScreensaverTimeout, this); +#endif } void VideoPlayer::Pause() { @@ -206,6 +288,12 @@ void VideoPlayer::Pause() { if (ret != PLAYER_ERROR_NONE) { throw VideoPlayerError("player_pause failed", get_error_message(ret)); } + + if (timer_) { + LOG_DEBUG("[VideoPlayer] Delete ecore timer."); + ecore_timer_del(timer_); + timer_ = nullptr; + } } void VideoPlayer::SetLooping(bool is_looping) { @@ -264,6 +352,11 @@ void VideoPlayer::Dispose() { std::lock_guard lock(mutex_); is_initialized_ = false; + + if (sink_event_pipe_) { + ecore_pipe_del(sink_event_pipe_); + } + event_sink_ = nullptr; event_channel_->SetStreamHandler(nullptr); @@ -285,6 +378,16 @@ void VideoPlayer::Dispose() { texture_registrar_->UnregisterTexture(texture_id_, nullptr); texture_registrar_ = nullptr; } + + if (screensaver_handle_) { + dlclose(screensaver_handle_); + screensaver_handle_ = nullptr; + } + + if (timer_) { + ecore_timer_del(timer_); + timer_ = nullptr; + } } void VideoPlayer::SetUpEventChannel(flutter::BinaryMessenger *messenger) { @@ -332,7 +435,7 @@ void VideoPlayer::SendInitialized() { int duration = 0; int ret = player_get_duration(player_, &duration); if (ret != PLAYER_ERROR_NONE) { - event_sink_->Error("player_get_duration failed", get_error_message(ret)); + SendError("player_get_duration failed", get_error_message(ret)); return; } LOG_DEBUG("[VideoPlayer] Video duration: %d", duration); @@ -340,8 +443,7 @@ void VideoPlayer::SendInitialized() { int width = 0, height = 0; ret = player_get_video_size(player_, &width, &height); if (ret != PLAYER_ERROR_NONE) { - event_sink_->Error("player_get_video_size failed", - get_error_message(ret)); + SendError("player_get_video_size failed", get_error_message(ret)); return; } LOG_DEBUG("[VideoPlayer] Video width: %d, height: %d", width, height); @@ -349,8 +451,7 @@ void VideoPlayer::SendInitialized() { player_display_rotation_e rotation = PLAYER_DISPLAY_ROTATION_NONE; ret = player_get_display_rotation(player_, &rotation); if (ret != PLAYER_ERROR_NONE) { - event_sink_->Error("player_get_display_rotation failed", - get_error_message(ret)); + SendError("player_get_display_rotation failed", get_error_message(ret)); } else { LOG_DEBUG("[VideoPlayer] rotation: %s", RotationToString(rotation).c_str()); @@ -369,10 +470,26 @@ void VideoPlayer::SendInitialized() { {flutter::EncodableValue("width"), flutter::EncodableValue(width)}, {flutter::EncodableValue("height"), flutter::EncodableValue(height)}, }; - event_sink_->Success(flutter::EncodableValue(result)); + PushEvent(flutter::EncodableValue(result)); } } +Eina_Bool VideoPlayer::ResetScreensaverTimeout(void *data) { + LOG_DEBUG("[VideoPlayer] Reset screen saver timeout."); + + auto *player = static_cast(data); + if (!player->screensaver_reset_timeout_) { + return ECORE_CALLBACK_CANCEL; + } + int ret = player->screensaver_reset_timeout_(); + if (ret != 0) { + LOG_ERROR("screensaver_reset_timeout failed: %s", get_error_message(ret)); + return ECORE_CALLBACK_CANCEL; + } + + return ECORE_CALLBACK_RENEW; +} + void VideoPlayer::OnPrepared(void *data) { LOG_DEBUG("[VideoPlayer] Player prepared."); @@ -400,13 +517,10 @@ void VideoPlayer::OnPlayCompleted(void *data) { LOG_DEBUG("[VideoPlayer] Play completed."); auto *player = static_cast(data); - if (player->event_sink_) { - flutter::EncodableMap result = { - {flutter::EncodableValue("event"), - flutter::EncodableValue("completed")}, - }; - player->event_sink_->Success(flutter::EncodableValue(result)); - } + flutter::EncodableMap result = { + {flutter::EncodableValue("event"), flutter::EncodableValue("completed")}, + }; + player->PushEvent(flutter::EncodableValue(result)); player->Pause(); } @@ -415,10 +529,7 @@ void VideoPlayer::OnInterrupted(player_interrupted_code_e code, void *data) { LOG_ERROR("[VideoPlayer] Interrupt code: %d", code); auto *player = static_cast(data); - if (player->event_sink_) { - player->event_sink_->Error("Interrupted error", - "Video player has been interrupted."); - } + player->SendError("Interrupted error", "Video player has been interrupted."); } void VideoPlayer::OnError(int error_code, void *data) { @@ -426,10 +537,8 @@ void VideoPlayer::OnError(int error_code, void *data) { get_error_message(error_code)); auto *player = static_cast(data); - if (player->event_sink_) { - player->event_sink_->Error( - "Player error", std::string("Error: ") + get_error_message(error_code)); - } + player->SendError("Player error", + std::string("Error: ") + get_error_message(error_code)); } void VideoPlayer::OnVideoFrameDecoded(media_packet_h packet, void *data) { diff --git a/packages/video_player/tizen/src/video_player.h b/packages/video_player/tizen/src/video_player.h index a7e4cebae..d58ea6ba4 100644 --- a/packages/video_player/tizen/src/video_player.h +++ b/packages/video_player/tizen/src/video_player.h @@ -5,6 +5,7 @@ #ifndef FLUTTER_PLUGIN_VIDEO_PLAYER_H_ #define FLUTTER_PLUGIN_VIDEO_PLAYER_H_ +#include #include #include #include @@ -19,6 +20,9 @@ #include "video_player_options.h" +typedef int (*ScreensaverResetTimeout)(void); +typedef int (*ScreensaverOverrideReset)(bool onoff); + class VideoPlayer { public: using SeekCompletedCallback = std::function; @@ -40,12 +44,17 @@ class VideoPlayer { int64_t GetTextureId() { return texture_id_; } private: + void SendPendingEvents(); + void PushEvent(const flutter::EncodableValue &encodable_value); + void SendError(const std::string &error_code, + const std::string &error_message); FlutterDesktopGpuSurfaceDescriptor *ObtainGpuSurface(size_t width, size_t height); void SetUpEventChannel(flutter::BinaryMessenger *messenger); void Initialize(); void SendInitialized(); + void InitScreenSaverApi(); static void OnPrepared(void *data); static void OnBuffering(int percent, void *data); @@ -55,6 +64,7 @@ class VideoPlayer { static void OnError(int error_code, void *data); static void OnVideoFrameDecoded(media_packet_h packet, void *data); static void ReleaseMediaPacket(void *packet); + static Eina_Bool ResetScreensaverTimeout(void *data); void RequestRendering(); void OnRenderingCompleted(); @@ -79,6 +89,15 @@ class VideoPlayer { std::queue packet_queue_; SeekCompletedCallback on_seek_completed_; + + void *screensaver_handle_; + ScreensaverResetTimeout screensaver_reset_timeout_; + Ecore_Timer *timer_; + + Ecore_Pipe *sink_event_pipe_ = nullptr; + std::mutex queue_mutex_; + std::queue encodable_event_queue_; + std::queue> error_event_queue_; }; #endif // FLUTTER_PLUGIN_VIDEO_PLAYER_H_ diff --git a/packages/video_player_avplay/.gitignore b/packages/video_player_avplay/.gitignore new file mode 100644 index 000000000..e9dc58d3d --- /dev/null +++ b/packages/video_player_avplay/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ diff --git a/packages/video_player_avplay/CHANGELOG.md b/packages/video_player_avplay/CHANGELOG.md new file mode 100644 index 000000000..607323422 --- /dev/null +++ b/packages/video_player_avplay/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 + +* Initial release. diff --git a/packages/video_player_avplay/LICENSE b/packages/video_player_avplay/LICENSE new file mode 100644 index 000000000..036fb575e --- /dev/null +++ b/packages/video_player_avplay/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2023 Samsung Electronics Co., Ltd. All rights reserved. +Copyright (c) 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the names of the copyright holders nor the names of the + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/video_player_avplay/README.md b/packages/video_player_avplay/README.md new file mode 100644 index 000000000..28fa8d488 --- /dev/null +++ b/packages/video_player_avplay/README.md @@ -0,0 +1,130 @@ +# video_player_avplay + +[![pub package](https://img.shields.io/pub/v/video_player_avplay.svg)](https://pub.dev/packages/video_player_avplay) + +A downloadable plugin which supports MMPlayer and PlusPlayer(PlusPlayer is a new multimedia player object-oriented designed) on Tizen TV devices. + +This plugin is only supported on Tizen TV devices. If you are targeting other types of devices or are not interested in playing DRM content in your app, use [`video_player`](https://pub.dev/packages/video_player) and [`video_player_tizen`](https://pub.dev/packages/video_player_tizen) instead. + +## Usage + +To use this package, add `video_player_avplay` as a dependency in your `pubspec.yaml` file. + +```yaml +dependencies: + video_player_avplay: ^0.1.0 +``` + +Then you can import `video_player_avplay` in your Dart code: + +```dart +import 'package:video_player_avplay/video_player.dart'; +``` + +Note that `video_player_avplay` is not compatible with the original `video_player` plugin. If you're writing a cross-platform app for Tizen and other platforms, it is recommended to create two separate source files and import `video_player` and `video_player_avplay` in the files respectively. + +Note that `video_player_avplay` uses a compiled dynamic library, the api-version is your TV version, change it in tizen-manifest.xml: + +```xml + +``` + +### Example + +```dart +import 'package:flutter/material.dart'; +import 'package:video_player_avplay/video_player.dart'; + +class RemoteVideo extends StatefulWidget { + const RemoteVideo({Key? key}) : super(key: key); + + @override + State createState() => _RemoteVideoState(); +} + +class _RemoteVideoState extends State { + late VideoPlayerController _controller; + + @override + void initState() { + super.initState(); + _controller = VideoPlayerController.network( + 'https://media.w3.org/2010/05/bunny/trailer.mp4', + drmConfigs: const DrmConfigs( + type: DrmType.playready, + licenseServerUrl: + 'http://test.playready.microsoft.com/service/rightsmanager.asmx', + ), + ); + _controller.addListener(() => setState(() {})); + _controller.initialize().then((_) => setState(() {})); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Center( + child: AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + VideoPlayer(_controller), + ClosedCaption(text: _controller.value.caption.text), + GestureDetector( + onTap: () { + _controller.value.isPlaying + ? _controller.pause() + : _controller.play(); + }, + ), + VideoProgressIndicator(_controller, allowScrubbing: true), + ], + ), + ), + ); + } +} +``` + +## Required privileges + +To use this plugin, you may need to declare the following privileges in your `tizen-manifest.xml` file. + +```xml + + http://tizen.org/privilege/mediastorage + http://tizen.org/privilege/externalstorage + http://tizen.org/privilege/internet + http://developer.samsung.com/privilege/drmplay + +``` + +- The mediastorage privilege (`http://tizen.org/privilege/mediastorage`) is required to play video files located in the internal storage. +- The externalstorage privilege (`http://tizen.org/privilege/externalstorage`) is required to play video files located in the external storage. +- The internet privilege (`http://tizen.org/privilege/internet`) is required to play any URL from the network. +- The drmplay privilege (`http://developer.samsung.com/privilege/drmplay`) is required to play DRM content. The app must be signed with a [partner-level certificate](https://docs.tizen.org/application/dotnet/get-started/certificates/creating-certificates) to use this privilege. + +For detailed information on Tizen privileges, see [Tizen Docs: API Privileges](https://docs.tizen.org/application/dotnet/get-started/api-privileges). + +## Limitations + +TV emulator support is experimental. DRM content playback is not supported on TV emulators. + +The following options are not currently supported. + +- `VideoPlayerOptions.allowBackgroundPlayback` +- `VideoPlayerOptions.mixWithOthers` + +This plugin has the following limitations. + +- The `httpHeaders` option of `VideoPlayerController.network` only support `Cookie` and `User-Agent`. +- The `setPlaybackSpeed` method will fail if triggered within the last 3 seconds of the video. +- The playback speed will reset to 1.0 when the video is replayed in loop mode. +- The `seekTo` method works only when the playback speed is 1.0, and it sets the video position to the nearest keyframe, not the exact value passed. +- diff --git a/packages/video_player_avplay/example/.gitignore b/packages/video_player_avplay/example/.gitignore new file mode 100644 index 000000000..1adcc2fb3 --- /dev/null +++ b/packages/video_player_avplay/example/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +#vscode +/.vscode/ diff --git a/packages/video_player_avplay/example/README.md b/packages/video_player_avplay/example/README.md new file mode 100644 index 000000000..555f5d8a0 --- /dev/null +++ b/packages/video_player_avplay/example/README.md @@ -0,0 +1,7 @@ +# video_player_avplay_example + +Demonstrates how to use the video_player_avplay plugin. + +## Getting Started + +To run this app on your Tizen device, use [flutter-tizen](https://github.com/flutter-tizen/flutter-tizen). diff --git a/packages/video_player_avplay/example/assets/Audio.mp3 b/packages/video_player_avplay/example/assets/Audio.mp3 new file mode 100644 index 000000000..355eb9b2e Binary files /dev/null and b/packages/video_player_avplay/example/assets/Audio.mp3 differ diff --git a/packages/video_player_avplay/example/assets/Butterfly-209.mp4 b/packages/video_player_avplay/example/assets/Butterfly-209.mp4 new file mode 100644 index 000000000..c8489799f Binary files /dev/null and b/packages/video_player_avplay/example/assets/Butterfly-209.mp4 differ diff --git a/packages/video_player_avplay/example/integration_test/video_player_test.dart b/packages/video_player_avplay/example/integration_test/video_player_test.dart new file mode 100644 index 000000000..4167278ae --- /dev/null +++ b/packages/video_player_avplay/example/integration_test/video_player_test.dart @@ -0,0 +1,333 @@ +// Copyright 2013 The Flutter 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 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:video_player_avplay/video_player.dart'; + +const Duration _playDuration = Duration(seconds: 1); + +// Use WebM for web to allow CI to use Chromium. +const String _videoAssetKey = + kIsWeb ? 'assets/Butterfly-209.webm' : 'assets/Butterfly-209.mp4'; + +// Returns the URL to load an asset from this example app as a network source. +String getUrlForAssetAsNetworkSource(String assetKey) { + return 'https://github.com/flutter/plugins/blob/' + // This hash can be rolled forward to pick up newly-added assets. + 'cba393233e559c925a4daf71b06b4bb01c606762' + '/packages/video_player/video_player/example/' + '$assetKey' + '?raw=true'; +} + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + late VideoPlayerController controller; + tearDown(() async => controller.dispose()); + + group('asset videos', () { + setUp(() { + controller = VideoPlayerController.asset(_videoAssetKey); + }); + + testWidgets('can be initialized', (WidgetTester tester) async { + await controller.initialize(); + + expect(controller.value.isInitialized, true); + expect(controller.value.position, Duration.zero); + expect(controller.value.isPlaying, false); + // The WebM version has a slightly different duration than the MP4. + expect(controller.value.duration, + const Duration(seconds: 7, milliseconds: kIsWeb ? 544 : 540)); + }); + + testWidgets( + 'live stream duration != 0', + (WidgetTester tester) async { + final VideoPlayerController networkController = + VideoPlayerController.network( + 'https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8', + ); + await networkController.initialize(); + + expect(networkController.value.isInitialized, true); + // Live streams should have either a positive duration or C.TIME_UNSET if the duration is unknown + // See https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/Player.html#getDuration-- + expect(networkController.value.duration, + (Duration duration) => duration != Duration.zero); + }, + skip: kIsWeb, + ); + + testWidgets( + 'can be played', + (WidgetTester tester) async { + await controller.initialize(); + // Mute to allow playing without DOM interaction on Web. + // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes + await controller.setVolume(0); + + await controller.play(); + await tester.pumpAndSettle(_playDuration); + + expect(controller.value.isPlaying, true); + expect(controller.value.position, + (Duration position) => position > Duration.zero); + }, + ); + + testWidgets( + 'can seek', + (WidgetTester tester) async { + await controller.initialize(); + + await controller.seekTo(const Duration(seconds: 3)); + + expect(controller.value.position, const Duration(seconds: 3)); + }, + ); + + testWidgets( + 'can be paused', + (WidgetTester tester) async { + await controller.initialize(); + // Mute to allow playing without DOM interaction on Web. + // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes + await controller.setVolume(0); + + // Play for a second, then pause, and then wait a second. + await controller.play(); + await tester.pumpAndSettle(_playDuration); + await controller.pause(); + final Duration pausedPosition = controller.value.position; + await tester.pumpAndSettle(_playDuration); + + // Verify that we stopped playing after the pause. + expect(controller.value.isPlaying, false); + expect(controller.value.position, pausedPosition); + }, + ); + + testWidgets( + 'stay paused when seeking after video completed', + (WidgetTester tester) async { + await controller.initialize(); + // Mute to allow playing without DOM interaction on Web. + // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes + await controller.setVolume(0); + final Duration timeBeforeEnd = + controller.value.duration - const Duration(milliseconds: 500); + await controller.seekTo(timeBeforeEnd); + await controller.play(); + await tester.pumpAndSettle(_playDuration); + expect(controller.value.isPlaying, false); + expect(controller.value.position, controller.value.duration); + + await controller.seekTo(timeBeforeEnd); + await tester.pumpAndSettle(_playDuration); + + expect(controller.value.isPlaying, false); + expect(controller.value.position, timeBeforeEnd); + }, + ); + + testWidgets( + 'do not exceed duration on play after video completed', + (WidgetTester tester) async { + await controller.initialize(); + // Mute to allow playing without DOM interaction on Web. + // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes + await controller.setVolume(0); + await controller.seekTo( + controller.value.duration - const Duration(milliseconds: 500)); + await controller.play(); + await tester.pumpAndSettle(_playDuration); + expect(controller.value.isPlaying, false); + expect(controller.value.position, controller.value.duration); + + await controller.play(); + await tester.pumpAndSettle(_playDuration); + + expect(controller.value.position, + lessThanOrEqualTo(controller.value.duration)); + }, + ); + + testWidgets('test video player view with local asset', + (WidgetTester tester) async { + Future started() async { + await controller.initialize(); + await controller.play(); + return true; + } + + await tester.pumpWidget(Material( + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: FutureBuilder( + future: started(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.data ?? false) { + return AspectRatio( + aspectRatio: controller.value.aspectRatio, + child: VideoPlayer(controller), + ); + } else { + return const Text('waiting for video to load'); + } + }, + ), + ), + ), + )); + + await tester.pumpAndSettle(); + expect(controller.value.isPlaying, true); + }, + skip: kIsWeb || // Web does not support local assets. + // Extremely flaky on iOS: https://github.com/flutter/flutter/issues/86915 + defaultTargetPlatform == TargetPlatform.iOS); + }); + + group('file-based videos', () { + setUp(() async { + // Load the data from the asset. + final String tempDir = (await getTemporaryDirectory()).path; + final ByteData bytes = await rootBundle.load(_videoAssetKey); + + // Write it to a file to use as a source. + final String filename = _videoAssetKey.split('/').last; + final File file = File('$tempDir/$filename'); + await file.writeAsBytes(bytes.buffer.asInt8List()); + + controller = VideoPlayerController.file(file); + }); + + testWidgets('test video player using static file() method as constructor', + (WidgetTester tester) async { + await controller.initialize(); + + await controller.play(); + expect(controller.value.isPlaying, true); + + await controller.pause(); + expect(controller.value.isPlaying, false); + }, skip: kIsWeb); + }); + + group('network videos', () { + setUp(() { + controller = VideoPlayerController.network( + getUrlForAssetAsNetworkSource(_videoAssetKey)); + }); + + testWidgets( + 'reports buffering status', + (WidgetTester tester) async { + await controller.initialize(); + // Mute to allow playing without DOM interaction on Web. + // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes + await controller.setVolume(0); + final Completer started = Completer(); + final Completer ended = Completer(); + controller.addListener(() { + if (!started.isCompleted && controller.value.isBuffering) { + started.complete(); + } + if (started.isCompleted && + !controller.value.isBuffering && + !ended.isCompleted) { + ended.complete(); + } + }); + + await controller.play(); + await controller.seekTo(const Duration(seconds: 5)); + await tester.pumpAndSettle(_playDuration); + await controller.pause(); + + expect(controller.value.isPlaying, false); + expect(controller.value.position, + (Duration position) => position > Duration.zero); + + await expectLater(started.future, completes); + await expectLater(ended.future, completes); + }, + skip: !(kIsWeb || defaultTargetPlatform == TargetPlatform.android), + ); + }); + + // Audio playback is tested to prevent accidental regression, + // but could be removed in the future. + group('asset audios', () { + setUp(() { + controller = VideoPlayerController.asset('assets/Audio.mp3'); + }); + + testWidgets('can be initialized', (WidgetTester tester) async { + await controller.initialize(); + + expect(controller.value.isInitialized, true); + expect(controller.value.position, Duration.zero); + expect(controller.value.isPlaying, false); + // Due to the duration calculation accurancy between platforms, + // the milliseconds on Web will be a slightly different from natives. + // The audio was made with 44100 Hz, 192 Kbps CBR, and 32 bits. + expect( + controller.value.duration, + const Duration(seconds: 5, milliseconds: kIsWeb ? 42 : 41), + ); + }); + + testWidgets('can be played', (WidgetTester tester) async { + await controller.initialize(); + // Mute to allow playing without DOM interaction on Web. + // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes + await controller.setVolume(0); + + await controller.play(); + await tester.pumpAndSettle(_playDuration); + + expect(controller.value.isPlaying, true); + expect( + controller.value.position, + (Duration position) => position > Duration.zero, + ); + }); + + testWidgets('can seek', (WidgetTester tester) async { + await controller.initialize(); + await controller.seekTo(const Duration(seconds: 3)); + + expect(controller.value.position, const Duration(seconds: 3)); + }); + + testWidgets('can be paused', (WidgetTester tester) async { + await controller.initialize(); + // Mute to allow playing without DOM interaction on Web. + // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes + await controller.setVolume(0); + + // Play for a second, then pause, and then wait a second. + await controller.play(); + await tester.pumpAndSettle(_playDuration); + await controller.pause(); + final Duration pausedPosition = controller.value.position; + await tester.pumpAndSettle(_playDuration); + + // Verify that we stopped playing after the pause. + expect(controller.value.isPlaying, false); + expect(controller.value.position, pausedPosition); + }); + }); +} diff --git a/packages/video_player_avplay/example/lib/main.dart b/packages/video_player_avplay/example/lib/main.dart new file mode 100644 index 000000000..d7431dd13 --- /dev/null +++ b/packages/video_player_avplay/example/lib/main.dart @@ -0,0 +1,687 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs, avoid_print, use_build_context_synchronously + +/// An example of using the plugin, controlling lifecycle and playback of the +/// video. + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:http/http.dart' as http; +import 'package:video_player_avplay/video_player.dart'; +import 'package:video_player_avplay/video_player_platform_interface.dart'; + +void main() { + runApp( + MaterialApp( + home: _App(), + ), + ); +} + +class _App extends StatelessWidget { + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 6, + child: Scaffold( + key: const ValueKey('home_page'), + appBar: AppBar( + title: const Text('Video player example'), + bottom: const TabBar( + isScrollable: true, + tabs: [ + Tab(icon: Icon(Icons.cloud), text: 'MP4'), + Tab(icon: Icon(Icons.cloud), text: 'HLS'), + Tab(icon: Icon(Icons.cloud), text: 'Dash'), + Tab(icon: Icon(Icons.cloud), text: 'DRM Widevine'), + Tab(icon: Icon(Icons.cloud), text: 'DRM PlayReady'), + Tab(icon: Icon(Icons.cloud), text: 'Track'), + ], + ), + ), + body: TabBarView( + children: [ + _Mp4RemoteVideo(), + _HlsRomoteVideo(), + _DashRomoteVideo(), + _DrmRemoteVideo(), + _DrmRemoteVideo2(), + _TrackTest(), + ], + ), + ), + ); + } +} + +class _HlsRomoteVideo extends StatefulWidget { + @override + State<_HlsRomoteVideo> createState() => _HlsRomoteVideoState(); +} + +class _HlsRomoteVideoState extends State<_HlsRomoteVideo> { + late VideoPlayerController _controller; + + @override + void initState() { + super.initState(); + _controller = VideoPlayerController.network( + 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8'); + + _controller.addListener(() { + if (_controller.value.hasError) { + print(_controller.value.errorDescription); + } + setState(() {}); + }); + _controller.setLooping(true); + _controller.initialize().then((_) => setState(() {})); + _controller.play(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + Container( + padding: const EdgeInsets.only(top: 20.0), + ), + const Text('With Hls'), + Container( + padding: const EdgeInsets.all(20), + child: AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + VideoPlayer(_controller), + ClosedCaption(text: _controller.value.caption.text), + _ControlsOverlay(controller: _controller), + VideoProgressIndicator(_controller, allowScrubbing: true), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _DashRomoteVideo extends StatefulWidget { + @override + State<_DashRomoteVideo> createState() => _DashRomoteVideoState(); +} + +class _DashRomoteVideoState extends State<_DashRomoteVideo> { + late VideoPlayerController _controller; + + @override + void initState() { + super.initState(); + _controller = VideoPlayerController.network( + 'https://dash.akamaized.net/dash264/TestCasesUHD/2b/11/MultiRate.mpd', + formatHint: VideoFormat.dash, + ); + + _controller.addListener(() { + if (_controller.value.hasError) { + print(_controller.value.errorDescription); + } + setState(() {}); + }); + _controller.setLooping(true); + _controller.initialize().then((_) => setState(() {})); + _controller.play(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + Container(padding: const EdgeInsets.only(top: 20.0)), + const Text('With Dash'), + Container( + padding: const EdgeInsets.all(20), + child: AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + VideoPlayer(_controller), + ClosedCaption(text: _controller.value.caption.text), + _ControlsOverlay(controller: _controller), + VideoProgressIndicator(_controller, allowScrubbing: true), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _Mp4RemoteVideo extends StatefulWidget { + @override + State<_Mp4RemoteVideo> createState() => _Mp4RemoteVideoState(); +} + +class _Mp4RemoteVideoState extends State<_Mp4RemoteVideo> { + late VideoPlayerController _controller; + + @override + void initState() { + super.initState(); + _controller = VideoPlayerController.network( + 'https://media.w3.org/2010/05/bunny/trailer.mp4'); + + _controller.addListener(() { + if (_controller.value.hasError) { + print(_controller.value.errorDescription); + } + setState(() {}); + }); + _controller.setLooping(true); + _controller.initialize().then((_) => setState(() {})); + _controller.play(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + Container(padding: const EdgeInsets.only(top: 20.0)), + const Text('With remote mp4'), + Container( + padding: const EdgeInsets.all(20), + child: AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + VideoPlayer(_controller), + ClosedCaption(text: _controller.value.caption.text), + _ControlsOverlay(controller: _controller), + VideoProgressIndicator(_controller, allowScrubbing: true), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _DrmRemoteVideo extends StatefulWidget { + @override + State<_DrmRemoteVideo> createState() => _DrmRemoteVideoState(); +} + +class _DrmRemoteVideoState extends State<_DrmRemoteVideo> { + late VideoPlayerController _controller; + + @override + void initState() { + super.initState(); + + _controller = VideoPlayerController.network( + 'https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd', + drmConfigs: DrmConfigs( + type: DrmType.widevine, + licenseCallback: (Uint8List challenge) async { + final http.Response response = await http.post( + Uri.parse('https://proxy.uat.widevine.com/proxy'), + body: challenge, + ); + return response.bodyBytes; + }, + ), + formatHint: VideoFormat.dash, + ); + + _controller.addListener(() { + if (_controller.value.hasError) { + print(_controller.value.errorDescription); + } + setState(() {}); + }); + _controller.setLooping(true); + _controller.initialize().then((_) => setState(() {})); + _controller.play(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + Container(padding: const EdgeInsets.only(top: 20.0)), + const Text('Play DRM Widevine'), + Container( + padding: const EdgeInsets.all(20), + child: AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + VideoPlayer(_controller), + ClosedCaption(text: _controller.value.caption.text), + _ControlsOverlay(controller: _controller), + VideoProgressIndicator(_controller, allowScrubbing: true), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _DrmRemoteVideo2 extends StatefulWidget { + @override + State<_DrmRemoteVideo2> createState() => _DrmRemoteVideoState2(); +} + +class _DrmRemoteVideoState2 extends State<_DrmRemoteVideo2> { + late VideoPlayerController _controller; + + @override + void initState() { + super.initState(); + + _controller = VideoPlayerController.network( + 'https://test.playready.microsoft.com/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest', + drmConfigs: const DrmConfigs( + type: DrmType.playready, + licenseServerUrl: + 'http://test.playready.microsoft.com/service/rightsmanager.asmx', + ), + ); + + _controller.addListener(() { + if (_controller.value.hasError) { + print(_controller.value.errorDescription); + } + setState(() {}); + }); + _controller.setLooping(true); + _controller.initialize().then((_) => setState(() {})); + _controller.play(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + Container(padding: const EdgeInsets.only(top: 20.0)), + const Text('Play DRM PlayReady'), + Container( + padding: const EdgeInsets.all(20), + child: AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + VideoPlayer(_controller), + ClosedCaption(text: _controller.value.caption.text), + _ControlsOverlay(controller: _controller), + VideoProgressIndicator(_controller, allowScrubbing: true), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _TrackTest extends StatefulWidget { + @override + State<_TrackTest> createState() => _TrackTestState(); +} + +class _TrackTestState extends State<_TrackTest> { + late VideoPlayerController _controller; + + @override + void initState() { + super.initState(); + + _controller = VideoPlayerController.network( + 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8'); + + _controller.addListener(() { + if (_controller.value.hasError) { + print(_controller.value.errorDescription); + } + setState(() {}); + }); + _controller.setLooping(true); + _controller.initialize().then((_) => setState(() {})); + _controller.play(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + Container(padding: const EdgeInsets.only(top: 20.0)), + const Text('track selections test'), + Container( + padding: const EdgeInsets.all(20), + child: AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + VideoPlayer(_controller), + ClosedCaption(text: _controller.value.caption.text), + _ControlsOverlay(controller: _controller), + VideoProgressIndicator(_controller, allowScrubbing: true), + ], + ), + ), + ), + _GetVideoTrackButton(controller: _controller), + _GetAudioTrackButton(controller: _controller), + _GetTextTrackButton(controller: _controller), + ], + ), + ); + } +} + +class _ControlsOverlay extends StatelessWidget { + const _ControlsOverlay({required this.controller}); + + static const List _exampleCaptionOffsets = [ + Duration(seconds: -10), + Duration(seconds: -3), + Duration(seconds: -1, milliseconds: -500), + Duration(milliseconds: -250), + Duration.zero, + Duration(milliseconds: 250), + Duration(seconds: 1, milliseconds: 500), + Duration(seconds: 3), + Duration(seconds: 10), + ]; + static const List _examplePlaybackRates = [ + 0.25, + 0.5, + 1.0, + 1.5, + 2.0, + 3.0, + 5.0, + 10.0, + ]; + + final VideoPlayerController controller; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 50), + reverseDuration: const Duration(milliseconds: 200), + child: controller.value.isPlaying + ? const SizedBox.shrink() + : Container( + color: Colors.black26, + child: const Center( + child: Icon( + Icons.play_arrow, + color: Colors.white, + size: 100.0, + semanticLabel: 'Play', + ), + ), + ), + ), + GestureDetector( + onTap: () { + controller.value.isPlaying ? controller.pause() : controller.play(); + }, + ), + Align( + alignment: Alignment.topLeft, + child: PopupMenuButton( + initialValue: controller.value.captionOffset, + tooltip: 'Caption Offset', + onSelected: (Duration delay) { + controller.setCaptionOffset(delay); + }, + itemBuilder: (BuildContext context) { + return >[ + for (final Duration offsetDuration in _exampleCaptionOffsets) + PopupMenuItem( + value: offsetDuration, + child: Text('${offsetDuration.inMilliseconds}ms'), + ) + ]; + }, + child: Padding( + padding: const EdgeInsets.symmetric( + // Using less vertical padding as the text is also longer + // horizontally, so it feels like it would need more spacing + // horizontally (matching the aspect ratio of the video). + vertical: 12, + horizontal: 16, + ), + child: Text('${controller.value.captionOffset.inMilliseconds}ms'), + ), + ), + ), + Align( + alignment: Alignment.topRight, + child: PopupMenuButton( + initialValue: controller.value.playbackSpeed, + tooltip: 'Playback speed', + onSelected: (double speed) { + controller.setPlaybackSpeed(speed); + }, + itemBuilder: (BuildContext context) { + return >[ + for (final double speed in _examplePlaybackRates) + PopupMenuItem( + value: speed, + child: Text('${speed}x'), + ) + ]; + }, + child: Padding( + padding: const EdgeInsets.symmetric( + // Using less vertical padding as the text is also longer + // horizontally, so it feels like it would need more spacing + // horizontally (matching the aspect ratio of the video). + vertical: 12, + horizontal: 16, + ), + child: Text('${controller.value.playbackSpeed}x'), + ), + ), + ), + ], + ); + } +} + +class _GetVideoTrackButton extends StatelessWidget { + const _GetVideoTrackButton({required this.controller}); + + final VideoPlayerController controller; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 20.0), + child: MaterialButton( + child: const Text('Get Video Track'), + onPressed: () async { + final List? videotracks = await controller.videoTracks; + if (videotracks == null) { + return; + } + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Video'), + content: SizedBox( + height: 200, + width: 200, + child: ListView.builder( + itemCount: videotracks.length, + itemBuilder: (BuildContext context, int index) { + return ListTile( + title: Text( + '${videotracks[index].width}x${videotracks[index].height},${(videotracks[index].bitrate / 1000000).toStringAsFixed(2)}Mbps'), + onTap: () { + controller + .setTrackSelection(videotracks[index]); + }, + ); + }, + )), + ); + }); + }), + ); + } +} + +class _GetAudioTrackButton extends StatelessWidget { + const _GetAudioTrackButton({required this.controller}); + + final VideoPlayerController controller; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 20.0), + child: MaterialButton( + child: const Text('Get Audio Track'), + onPressed: () async { + final List? audioTracks = await controller.audioTracks; + if (audioTracks == null) { + return; + } + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Audio'), + content: SizedBox( + height: 200, + width: 200, + child: ListView.builder( + itemCount: audioTracks.length, + itemBuilder: (BuildContext context, int index) { + return ListTile( + title: Text( + 'language:${audioTracks[index].language}'), + onTap: () { + controller + .setTrackSelection(audioTracks[index]); + }, + ); + }, + )), + ); + }); + }), + ); + } +} + +class _GetTextTrackButton extends StatelessWidget { + const _GetTextTrackButton({required this.controller}); + + final VideoPlayerController controller; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 20.0), + child: MaterialButton( + child: const Text('Get Text Track'), + onPressed: () async { + final List? textTracks = await controller.textTracks; + if (textTracks == null) { + return; + } + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Text'), + content: SizedBox( + height: 200, + width: 200, + child: ListView.builder( + itemCount: textTracks.length, + itemBuilder: (BuildContext context, int index) { + return ListTile( + title: Text( + 'language:${textTracks[index].language}'), + onTap: () { + controller.setTrackSelection(textTracks[index]); + }, + ); + }, + )), + ); + }); + }), + ); + } +} diff --git a/packages/video_player_avplay/example/pubspec.yaml b/packages/video_player_avplay/example/pubspec.yaml new file mode 100644 index 000000000..f32e1231a --- /dev/null +++ b/packages/video_player_avplay/example/pubspec.yaml @@ -0,0 +1,34 @@ +name: video_player_avplay_example +description: Demonstrates how to use the video_player_avplay plugin. +publish_to: "none" + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + http: ^0.13.0 + video_player_avplay: + path: ../ + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + integration_test_tizen: + path: ../../integration_test/ + path_provider: ^2.0.6 + path_provider_tizen: + path: ../../path_provider/ + test: any + +flutter: + assets: + - assets/Butterfly-209.mp4 + - assets/Audio.mp3 + uses-material-design: true diff --git a/packages/video_player_avplay/example/test_driver/integration_test.dart b/packages/video_player_avplay/example/test_driver/integration_test.dart new file mode 100644 index 000000000..b38629cca --- /dev/null +++ b/packages/video_player_avplay/example/test_driver/integration_test.dart @@ -0,0 +1,3 @@ +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/video_player_avplay/example/tizen/.gitignore b/packages/video_player_avplay/example/tizen/.gitignore new file mode 100644 index 000000000..750f3af1b --- /dev/null +++ b/packages/video_player_avplay/example/tizen/.gitignore @@ -0,0 +1,5 @@ +flutter/ +.vs/ +*.user +bin/ +obj/ diff --git a/packages/video_player_avplay/example/tizen/App.cs b/packages/video_player_avplay/example/tizen/App.cs new file mode 100644 index 000000000..6dd4a6356 --- /dev/null +++ b/packages/video_player_avplay/example/tizen/App.cs @@ -0,0 +1,20 @@ +using Tizen.Flutter.Embedding; + +namespace Runner +{ + public class App : FlutterApplication + { + protected override void OnCreate() + { + base.OnCreate(); + + GeneratedPluginRegistrant.RegisterPlugins(this); + } + + static void Main(string[] args) + { + var app = new App(); + app.Run(args); + } + } +} diff --git a/packages/video_player_avplay/example/tizen/Runner.csproj b/packages/video_player_avplay/example/tizen/Runner.csproj new file mode 100644 index 000000000..351a83987 --- /dev/null +++ b/packages/video_player_avplay/example/tizen/Runner.csproj @@ -0,0 +1,26 @@ + + + + Exe + tizen40 + + + + portable + + + none + + + + + + + + + + %(RecursiveDir) + + + + diff --git a/packages/video_player_avplay/example/tizen/shared/res/ic_launcher.png b/packages/video_player_avplay/example/tizen/shared/res/ic_launcher.png new file mode 100644 index 000000000..4d6372eeb Binary files /dev/null and b/packages/video_player_avplay/example/tizen/shared/res/ic_launcher.png differ diff --git a/packages/video_player_avplay/example/tizen/tizen-manifest.xml b/packages/video_player_avplay/example/tizen/tizen-manifest.xml new file mode 100644 index 000000000..65da0752a --- /dev/null +++ b/packages/video_player_avplay/example/tizen/tizen-manifest.xml @@ -0,0 +1,16 @@ + + + + + + ic_launcher.png + + + + + http://tizen.org/privilege/mediastorage + http://tizen.org/privilege/externalstorage + http://tizen.org/privilege/internet + http://developer.samsung.com/privilege/drmplay + + diff --git a/packages/video_player_avplay/lib/src/closed_caption_file.dart b/packages/video_player_avplay/lib/src/closed_caption_file.dart new file mode 100644 index 000000000..324ffc471 --- /dev/null +++ b/packages/video_player_avplay/lib/src/closed_caption_file.dart @@ -0,0 +1,77 @@ +// Copyright 2013 The Flutter 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 'package:flutter/foundation.dart' show objectRuntimeType; + +import 'sub_rip.dart'; +import 'web_vtt.dart'; + +export 'sub_rip.dart' show SubRipCaptionFile; +export 'web_vtt.dart' show WebVTTCaptionFile; + +/// A structured representation of a parsed closed caption file. +/// +/// A closed caption file includes a list of captions, each with a start and end +/// time for when the given closed caption should be displayed. +/// +/// The [captions] are a list of all captions in a file, in the order that they +/// appeared in the file. +/// +/// See: +/// * [SubRipCaptionFile]. +/// * [WebVTTCaptionFile]. +abstract class ClosedCaptionFile { + /// The full list of captions from a given file. + /// + /// The [captions] will be in the order that they appear in the given file. + List get captions; +} + +/// A representation of a single caption. +/// +/// A typical closed captioning file will include several [Caption]s, each +/// linked to a start and end time. +class Caption { + /// Creates a new [Caption] object. + /// + /// This is not recommended for direct use unless you are writing a parser for + /// a new closed captioning file type. + const Caption({ + required this.number, + required this.start, + required this.end, + required this.text, + }); + + /// The number that this caption was assigned. + final int number; + + /// When in the given video should this [Caption] begin displaying. + final Duration start; + + /// When in the given video should this [Caption] be dismissed. + final Duration end; + + /// The actual text that should appear on screen to be read between [start] + /// and [end]. + final String text; + + /// A no caption object. This is a caption with [start] and [end] durations of zero, + /// and an empty [text] string. + static const Caption none = Caption( + number: 0, + start: Duration.zero, + end: Duration.zero, + text: '', + ); + + @override + String toString() { + return '${objectRuntimeType(this, 'Caption')}(' + 'number: $number, ' + 'start: $start, ' + 'end: $end, ' + 'text: $text)'; + } +} diff --git a/packages/video_player_avplay/lib/src/drm_configs.dart b/packages/video_player_avplay/lib/src/drm_configs.dart new file mode 100644 index 000000000..025237ffb --- /dev/null +++ b/packages/video_player_avplay/lib/src/drm_configs.dart @@ -0,0 +1,58 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; + +/// The DRM scheme for the video. +enum DrmType { + /// None. + none, + + /// PlayReady. + playready, + + /// Widevine CDM. + widevine, +} + +/// Callback that returns a DRM license from the given [challenge] data. +typedef LicenseCallback = Future Function(Uint8List challenge); + +/// Configurations for playing DRM content. +class DrmConfigs { + /// Creates a new [DrmConfigs]. + const DrmConfigs({ + this.type = DrmType.none, + this.licenseServerUrl, + this.licenseCallback, + }); + + /// The DRM type. + final DrmType type; + + /// The URL of the DRM license server. + /// + /// This is optional. Either [licenseServerUrl] or [licenseCallback] can be + /// specified. + final String? licenseServerUrl; + + /// A callback to retrieve a DRM license. + /// + /// This is optional. Either [licenseServerUrl] or [licenseCallback] can be + /// specified. + /// + /// This callback is called multiple times while the video is playing. Note + /// that the platform thread (main thread) is blocked while this callback is + /// running. If the execution of this callback is delayed, the program may + /// hang or fail to process user input. + final LicenseCallback? licenseCallback; + + /// Converts to a map. + Map toMap() { + return { + 'drmType': type.index, + 'licenseServerUrl': licenseServerUrl, + }; + } +} diff --git a/packages/video_player_avplay/lib/src/hole.dart b/packages/video_player_avplay/lib/src/hole.dart new file mode 100644 index 000000000..74a4a6e6d --- /dev/null +++ b/packages/video_player_avplay/lib/src/hole.dart @@ -0,0 +1,66 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +/// A widget that creates a transparent hole in the Flutter UI. +class Hole extends LeafRenderObjectWidget { + /// Creates a [Hole]. + const Hole({super.key}); + + @override + RenderBox createRenderObject(BuildContext context) => _HoleBox(); +} + +/// A render object of the [Hole] widget. +class _HoleBox extends RenderBox { + @override + bool get sizedByParent => true; + + @override + bool get alwaysNeedsCompositing => true; + + @override + bool get isRepaintBoundary => true; + + @override + void performResize() { + size = constraints.biggest; + } + + @override + bool hitTestSelf(Offset position) { + return true; + } + + @override + void paint(PaintingContext context, Offset offset) { + context.addLayer(_HoleLayer(rect: offset & size)); + } +} + +/// A composite layer that draws a rect with blend mode. +class _HoleLayer extends Layer { + _HoleLayer({required this.rect}); + + final Rect rect; + + @override + void addToScene(SceneBuilder builder, [Offset layerOffset = Offset.zero]) { + builder.addPicture(layerOffset, _createHolePicture(rect)); + } + + Picture _createHolePicture(Rect holeRect) { + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + final Paint paint = Paint(); + paint.color = Colors.transparent; + paint.blendMode = BlendMode.src; + canvas.drawRect(rect, paint); + return recorder.endRecording(); + } +} diff --git a/packages/video_player_avplay/lib/src/messages.g.dart b/packages/video_player_avplay/lib/src/messages.g.dart new file mode 100644 index 000000000..ecb5d8716 --- /dev/null +++ b/packages/video_player_avplay/lib/src/messages.g.dart @@ -0,0 +1,798 @@ +// Autogenerated from Pigeon (v10.0.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +class PlayerMessage { + PlayerMessage({ + required this.playerId, + }); + + int playerId; + + Object encode() { + return [ + playerId, + ]; + } + + static PlayerMessage decode(Object result) { + result as List; + return PlayerMessage( + playerId: result[0]! as int, + ); + } +} + +class LoopingMessage { + LoopingMessage({ + required this.playerId, + required this.isLooping, + }); + + int playerId; + + bool isLooping; + + Object encode() { + return [ + playerId, + isLooping, + ]; + } + + static LoopingMessage decode(Object result) { + result as List; + return LoopingMessage( + playerId: result[0]! as int, + isLooping: result[1]! as bool, + ); + } +} + +class VolumeMessage { + VolumeMessage({ + required this.playerId, + required this.volume, + }); + + int playerId; + + double volume; + + Object encode() { + return [ + playerId, + volume, + ]; + } + + static VolumeMessage decode(Object result) { + result as List; + return VolumeMessage( + playerId: result[0]! as int, + volume: result[1]! as double, + ); + } +} + +class PlaybackSpeedMessage { + PlaybackSpeedMessage({ + required this.playerId, + required this.speed, + }); + + int playerId; + + double speed; + + Object encode() { + return [ + playerId, + speed, + ]; + } + + static PlaybackSpeedMessage decode(Object result) { + result as List; + return PlaybackSpeedMessage( + playerId: result[0]! as int, + speed: result[1]! as double, + ); + } +} + +class TrackMessage { + TrackMessage({ + required this.playerId, + required this.tracks, + }); + + int playerId; + + List?> tracks; + + Object encode() { + return [ + playerId, + tracks, + ]; + } + + static TrackMessage decode(Object result) { + result as List; + return TrackMessage( + playerId: result[0]! as int, + tracks: (result[1] as List?)!.cast?>(), + ); + } +} + +class TrackTypeMessage { + TrackTypeMessage({ + required this.playerId, + required this.trackType, + }); + + int playerId; + + String trackType; + + Object encode() { + return [ + playerId, + trackType, + ]; + } + + static TrackTypeMessage decode(Object result) { + result as List; + return TrackTypeMessage( + playerId: result[0]! as int, + trackType: result[1]! as String, + ); + } +} + +class SelectedTracksMessage { + SelectedTracksMessage({ + required this.playerId, + required this.trackId, + required this.trackType, + }); + + int playerId; + + int trackId; + + String trackType; + + Object encode() { + return [ + playerId, + trackId, + trackType, + ]; + } + + static SelectedTracksMessage decode(Object result) { + result as List; + return SelectedTracksMessage( + playerId: result[0]! as int, + trackId: result[1]! as int, + trackType: result[2]! as String, + ); + } +} + +class PositionMessage { + PositionMessage({ + required this.playerId, + required this.position, + }); + + int playerId; + + int position; + + Object encode() { + return [ + playerId, + position, + ]; + } + + static PositionMessage decode(Object result) { + result as List; + return PositionMessage( + playerId: result[0]! as int, + position: result[1]! as int, + ); + } +} + +class CreateMessage { + CreateMessage({ + this.asset, + this.uri, + this.packageName, + this.formatHint, + this.httpHeaders, + this.drmConfigs, + this.playerOptions, + }); + + String? asset; + + String? uri; + + String? packageName; + + String? formatHint; + + Map? httpHeaders; + + Map? drmConfigs; + + Map? playerOptions; + + Object encode() { + return [ + asset, + uri, + packageName, + formatHint, + httpHeaders, + drmConfigs, + playerOptions, + ]; + } + + static CreateMessage decode(Object result) { + result as List; + return CreateMessage( + asset: result[0] as String?, + uri: result[1] as String?, + packageName: result[2] as String?, + formatHint: result[3] as String?, + httpHeaders: + (result[4] as Map?)?.cast(), + drmConfigs: + (result[5] as Map?)?.cast(), + playerOptions: + (result[6] as Map?)?.cast(), + ); + } +} + +class MixWithOthersMessage { + MixWithOthersMessage({ + required this.mixWithOthers, + }); + + bool mixWithOthers; + + Object encode() { + return [ + mixWithOthers, + ]; + } + + static MixWithOthersMessage decode(Object result) { + result as List; + return MixWithOthersMessage( + mixWithOthers: result[0]! as bool, + ); + } +} + +class GeometryMessage { + GeometryMessage({ + required this.playerId, + required this.x, + required this.y, + required this.width, + required this.height, + }); + + int playerId; + + int x; + + int y; + + int width; + + int height; + + Object encode() { + return [ + playerId, + x, + y, + width, + height, + ]; + } + + static GeometryMessage decode(Object result) { + result as List; + return GeometryMessage( + playerId: result[0]! as int, + x: result[1]! as int, + y: result[2]! as int, + width: result[3]! as int, + height: result[4]! as int, + ); + } +} + +class _VideoPlayerAvplayApiCodec extends StandardMessageCodec { + const _VideoPlayerAvplayApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is CreateMessage) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is GeometryMessage) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is LoopingMessage) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is MixWithOthersMessage) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is PlaybackSpeedMessage) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else if (value is PlayerMessage) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is PositionMessage) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else if (value is SelectedTracksMessage) { + buffer.putUint8(135); + writeValue(buffer, value.encode()); + } else if (value is TrackMessage) { + buffer.putUint8(136); + writeValue(buffer, value.encode()); + } else if (value is TrackTypeMessage) { + buffer.putUint8(137); + writeValue(buffer, value.encode()); + } else if (value is VolumeMessage) { + buffer.putUint8(138); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return CreateMessage.decode(readValue(buffer)!); + case 129: + return GeometryMessage.decode(readValue(buffer)!); + case 130: + return LoopingMessage.decode(readValue(buffer)!); + case 131: + return MixWithOthersMessage.decode(readValue(buffer)!); + case 132: + return PlaybackSpeedMessage.decode(readValue(buffer)!); + case 133: + return PlayerMessage.decode(readValue(buffer)!); + case 134: + return PositionMessage.decode(readValue(buffer)!); + case 135: + return SelectedTracksMessage.decode(readValue(buffer)!); + case 136: + return TrackMessage.decode(readValue(buffer)!); + case 137: + return TrackTypeMessage.decode(readValue(buffer)!); + case 138: + return VolumeMessage.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class VideoPlayerAvplayApi { + /// Constructor for [VideoPlayerAvplayApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + VideoPlayerAvplayApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _VideoPlayerAvplayApiCodec(); + + Future initialize() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerAvplayApi.initialize', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future create(CreateMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerAvplayApi.create', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as PlayerMessage?)!; + } + } + + Future dispose(PlayerMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerAvplayApi.dispose', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future setLooping(LoopingMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerAvplayApi.setLooping', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future setVolume(VolumeMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerAvplayApi.setVolume', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future setPlaybackSpeed(PlaybackSpeedMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerAvplayApi.setPlaybackSpeed', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future play(PlayerMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerAvplayApi.play', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future setDeactivate(PlayerMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerAvplayApi.setDeactivate', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as bool?)!; + } + } + + Future setActivate(PlayerMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerAvplayApi.setActivate', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as bool?)!; + } + } + + Future track(TrackTypeMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerAvplayApi.track', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as TrackMessage?)!; + } + } + + Future setTrackSelection(SelectedTracksMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerAvplayApi.setTrackSelection', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as bool?)!; + } + } + + Future position(PlayerMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerAvplayApi.position', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as PositionMessage?)!; + } + } + + Future seekTo(PositionMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerAvplayApi.seekTo', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future pause(PlayerMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerAvplayApi.pause', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future setMixWithOthers(MixWithOthersMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerAvplayApi.setMixWithOthers', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future setDisplayGeometry(GeometryMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerAvplayApi.setDisplayGeometry', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } +} diff --git a/packages/video_player_avplay/lib/src/sub_rip.dart b/packages/video_player_avplay/lib/src/sub_rip.dart new file mode 100644 index 000000000..7b807cd4d --- /dev/null +++ b/packages/video_player_avplay/lib/src/sub_rip.dart @@ -0,0 +1,135 @@ +// Copyright 2013 The Flutter 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 'dart:convert'; + +import 'closed_caption_file.dart'; + +/// Represents a [ClosedCaptionFile], parsed from the SubRip file format. +/// See: https://en.wikipedia.org/wiki/SubRip +class SubRipCaptionFile extends ClosedCaptionFile { + /// Parses a string into a [ClosedCaptionFile], assuming [fileContents] is in + /// the SubRip file format. + /// * See: https://en.wikipedia.org/wiki/SubRip + SubRipCaptionFile(this.fileContents) + : _captions = _parseCaptionsFromSubRipString(fileContents); + + /// The entire body of the SubRip file. + // TODO(cyanglaz): Remove this public member as it doesn't seem need to exist. + // https://github.com/flutter/flutter/issues/90471 + final String fileContents; + + @override + List get captions => _captions; + + final List _captions; +} + +List _parseCaptionsFromSubRipString(String file) { + final List captions = []; + for (final List captionLines in _readSubRipFile(file)) { + if (captionLines.length < 3) { + break; + } + + final int captionNumber = int.parse(captionLines[0]); + final _CaptionRange captionRange = + _CaptionRange.fromSubRipString(captionLines[1]); + + final String text = captionLines.sublist(2).join('\n'); + + final Caption newCaption = Caption( + number: captionNumber, + start: captionRange.start, + end: captionRange.end, + text: text, + ); + if (newCaption.start != newCaption.end) { + captions.add(newCaption); + } + } + + return captions; +} + +class _CaptionRange { + _CaptionRange(this.start, this.end); + + final Duration start; + final Duration end; + + // Assumes format from an SubRip file. + // For example: + // 00:01:54,724 --> 00:01:56,760 + static _CaptionRange fromSubRipString(String line) { + final RegExp format = + RegExp(_subRipTimeStamp + _subRipArrow + _subRipTimeStamp); + + if (!format.hasMatch(line)) { + return _CaptionRange(Duration.zero, Duration.zero); + } + + final List times = line.split(_subRipArrow); + + final Duration start = _parseSubRipTimestamp(times[0]); + final Duration end = _parseSubRipTimestamp(times[1]); + + return _CaptionRange(start, end); + } +} + +// Parses a time stamp in an SubRip file into a Duration. +// For example: +// +// _parseSubRipTimestamp('00:01:59,084') +// returns +// Duration(hours: 0, minutes: 1, seconds: 59, milliseconds: 084) +Duration _parseSubRipTimestamp(String timestampString) { + if (!RegExp(_subRipTimeStamp).hasMatch(timestampString)) { + return Duration.zero; + } + + final List commaSections = timestampString.split(','); + final List hoursMinutesSeconds = commaSections[0].split(':'); + + final int hours = int.parse(hoursMinutesSeconds[0]); + final int minutes = int.parse(hoursMinutesSeconds[1]); + final int seconds = int.parse(hoursMinutesSeconds[2]); + final int milliseconds = int.parse(commaSections[1]); + + return Duration( + hours: hours, + minutes: minutes, + seconds: seconds, + milliseconds: milliseconds, + ); +} + +// Reads on SubRip file and splits it into Lists of strings where each list is one +// caption. +List> _readSubRipFile(String file) { + final List lines = LineSplitter.split(file).toList(); + + final List> captionStrings = >[]; + List currentCaption = []; + int lineIndex = 0; + for (final String line in lines) { + final bool isLineBlank = line.trim().isEmpty; + if (!isLineBlank) { + currentCaption.add(line); + } + + if (isLineBlank || lineIndex == lines.length - 1) { + captionStrings.add(currentCaption); + currentCaption = []; + } + + lineIndex += 1; + } + + return captionStrings; +} + +const String _subRipTimeStamp = r'\d\d:\d\d:\d\d,\d\d\d'; +const String _subRipArrow = r' --> '; diff --git a/packages/video_player_avplay/lib/src/tracks.dart b/packages/video_player_avplay/lib/src/tracks.dart new file mode 100755 index 000000000..923b9c200 --- /dev/null +++ b/packages/video_player_avplay/lib/src/tracks.dart @@ -0,0 +1,123 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Type of the track. +enum TrackType { + /// The video track. + video, + + /// The audio track. + audio, + + /// The text track. + text, +} + +/// Type of the track audio channel for [TrackType.audio]. +enum AudioTrackChannelType { + /// The mono channel. + mono, + + /// The stereo channel. + stereo, + + /// The surround channel. + surround, +} + +/// Type of the track subtitle type for [TrackType.text]. +enum TextTrackSubtitleType { + /// The text subtitle. + text, + + /// The picture subtitle. + picture, +} + +/// A representation of a single track. +/// +/// A typical video file will include several [Track]s.Such as [VideoTrack]s, [AudioTrack]s, [TextTrack]s. +class Track { + /// Creates an instance of [Track]. + /// + /// The [trackId] and [trackType] arguments are required. + /// + const Track({ + required this.trackId, + required this.trackType, + }); + + /// The track id of track that uses to determine track. + final int trackId; + + /// The type of the track. + final TrackType trackType; +} + +/// A representation of a video track. +class VideoTrack extends Track { + /// Creates an instance of [VideoTrack]. + /// + /// The [width], [height] and [bitrate] argument is required. + /// + /// [trackType] is [TrackType.video]. + VideoTrack({ + required super.trackId, + super.trackType = TrackType.video, + required this.width, + required this.height, + required this.bitrate, + }); + + /// The width of video track. + final int width; + + /// The height of video track. + final int height; + + /// The bitrate of video track. + final int bitrate; +} + +/// A representation of a audio track. +class AudioTrack extends Track { + /// Creates an instance of [AudioTrack]. + /// + /// The [language], [channel] and [bitrate] arguments are required. + /// + /// [trackType] is [TrackType.audio]. + AudioTrack({ + required super.trackId, + super.trackType = TrackType.audio, + required this.language, + required this.channel, + required this.bitrate, + }); + + /// The language of audio track. + final String language; + + /// The channel of audio track. + final AudioTrackChannelType channel; + + /// The bitrate of audio track. + final int bitrate; +} + +/// A representation of a text track. +class TextTrack extends Track { + /// Creates an instance of [TextTrack]. + /// + /// The [language] arguments are required. + /// + /// [trackType] is [TrackType.text]. + TextTrack({ + required super.trackId, + super.trackType = TrackType.text, + required this.language, + }); + + /// The language of text track. + final String language; +} diff --git a/packages/video_player_avplay/lib/src/video_player_tizen.dart b/packages/video_player_avplay/lib/src/video_player_tizen.dart new file mode 100644 index 000000000..11526dd5e --- /dev/null +++ b/packages/video_player_avplay/lib/src/video_player_tizen.dart @@ -0,0 +1,277 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Copyright 2013 The Flutter 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 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +import '../video_player_platform_interface.dart'; +import 'messages.g.dart'; +import 'tracks.dart'; + +/// An implementation of [VideoPlayerPlatform] that uses the +/// Pigeon-generated [VideoPlayerAvplayApi]. +class VideoPlayerTizen extends VideoPlayerPlatform { + final VideoPlayerAvplayApi _api = VideoPlayerAvplayApi(); + + @override + Future init() { + return _api.initialize(); + } + + @override + Future dispose(int playerId) { + return _api.dispose(PlayerMessage(playerId: playerId)); + } + + @override + Future create(DataSource dataSource) async { + final CreateMessage message = CreateMessage(); + + switch (dataSource.sourceType) { + case DataSourceType.asset: + message.asset = dataSource.asset; + message.packageName = dataSource.package; + break; + case DataSourceType.network: + message.uri = dataSource.uri; + message.formatHint = _videoFormatStringMap[dataSource.formatHint]; + message.httpHeaders = dataSource.httpHeaders; + message.drmConfigs = dataSource.drmConfigs?.toMap(); + message.playerOptions = dataSource.playerOptions; + break; + case DataSourceType.file: + message.uri = dataSource.uri; + break; + case DataSourceType.contentUri: + message.uri = dataSource.uri; + break; + } + + final PlayerMessage response = await _api.create(message); + return response.playerId; + } + + @override + Future setLooping(int playerId, bool looping) { + return _api + .setLooping(LoopingMessage(playerId: playerId, isLooping: looping)); + } + + @override + Future play(int playerId) { + return _api.play(PlayerMessage(playerId: playerId)); + } + + @override + Future setActivate(int playerId) { + return _api.setActivate(PlayerMessage(playerId: playerId)); + } + + @override + Future setDeactivate(int playerId) { + return _api.setDeactivate(PlayerMessage(playerId: playerId)); + } + + @override + Future pause(int playerId) { + return _api.pause(PlayerMessage(playerId: playerId)); + } + + @override + Future setVolume(int playerId, double volume) { + return _api.setVolume(VolumeMessage(playerId: playerId, volume: volume)); + } + + @override + Future setPlaybackSpeed(int playerId, double speed) { + assert(speed > 0); + + return _api.setPlaybackSpeed( + PlaybackSpeedMessage(playerId: playerId, speed: speed)); + } + + @override + Future seekTo(int playerId, Duration position) { + return _api.seekTo( + PositionMessage(playerId: playerId, position: position.inMilliseconds)); + } + + @override + Future> getVideoTracks(int playerId) async { + final TrackMessage response = await _api.track(TrackTypeMessage( + playerId: playerId, + trackType: TrackType.video.name, + )); + + final List videoTracks = []; + for (final Map? trackMap in response.tracks) { + final int trackId = trackMap!['trackId']! as int; + final int bitrate = trackMap['bitrate']! as int; + final int width = trackMap['width']! as int; + final int height = trackMap['height']! as int; + + videoTracks.add(VideoTrack( + trackId: trackId, + width: width, + height: height, + bitrate: bitrate, + )); + } + + return videoTracks; + } + + @override + Future> getAudioTracks(int playerId) async { + final TrackMessage response = await _api.track(TrackTypeMessage( + playerId: playerId, + trackType: TrackType.audio.name, + )); + + final List audioTracks = []; + for (final Map? trackMap in response.tracks) { + final int trackId = trackMap!['trackId']! as int; + final String language = trackMap['language']! as String; + final AudioTrackChannelType channelType = + _intChannelTypeMap[trackMap['channel']]!; + final int bitrate = trackMap['bitrate']! as int; + + audioTracks.add(AudioTrack( + trackId: trackId, + language: language, + channel: channelType, + bitrate: bitrate, + )); + } + + return audioTracks; + } + + @override + Future> getTextTracks(int playerId) async { + final TrackMessage response = await _api.track(TrackTypeMessage( + playerId: playerId, + trackType: TrackType.text.name, + )); + + final List textTracks = []; + for (final Map? trackMap in response.tracks) { + final int trackId = trackMap!['trackId']! as int; + final String language = trackMap['language']! as String; + + textTracks.add(TextTrack( + trackId: trackId, + language: language, + )); + } + + return textTracks; + } + + @override + Future setTrackSelection(int playerId, Track track) { + return _api.setTrackSelection(SelectedTracksMessage( + playerId: playerId, + trackId: track.trackId, + trackType: track.trackType.name, + )); + } + + @override + Future getPosition(int playerId) async { + final PositionMessage response = + await _api.position(PlayerMessage(playerId: playerId)); + return Duration(milliseconds: response.position); + } + + @override + Stream videoEventsFor(int playerId) { + return _eventChannelFor(playerId) + .receiveBroadcastStream() + .map((dynamic event) { + final Map map = event as Map; + switch (map['event']) { + case 'initialized': + return VideoEvent( + eventType: VideoEventType.initialized, + duration: Duration(milliseconds: map['duration']! as int), + size: Size((map['width'] as num?)?.toDouble() ?? 0.0, + (map['height'] as num?)?.toDouble() ?? 0.0), + ); + case 'completed': + return VideoEvent( + eventType: VideoEventType.completed, + ); + case 'bufferingUpdate': + final int value = map['value']! as int; + + return VideoEvent( + buffered: value, + eventType: VideoEventType.bufferingUpdate, + ); + case 'bufferingStart': + return VideoEvent(eventType: VideoEventType.bufferingStart); + case 'bufferingEnd': + return VideoEvent(eventType: VideoEventType.bufferingEnd); + case 'subtitleUpdate': + return VideoEvent( + eventType: VideoEventType.subtitleUpdate, + text: map['text']! as String, + ); + default: + return VideoEvent(eventType: VideoEventType.unknown); + } + }); + } + + @override + Widget buildView(int playerId) { + return Texture(textureId: playerId); + } + + @override + Future setMixWithOthers(bool mixWithOthers) { + return _api + .setMixWithOthers(MixWithOthersMessage(mixWithOthers: mixWithOthers)); + } + + @override + Future setDisplayGeometry( + int playerId, + int x, + int y, + int width, + int height, + ) { + return _api.setDisplayGeometry(GeometryMessage( + playerId: playerId, + x: x, + y: y, + width: width, + height: height, + )); + } + + EventChannel _eventChannelFor(int playerId) { + return EventChannel('tizen/video_player/video_events_$playerId'); + } + + static const Map _videoFormatStringMap = + { + VideoFormat.ss: 'ss', + VideoFormat.hls: 'hls', + VideoFormat.dash: 'dash', + VideoFormat.other: 'other', + }; + + static const Map _intChannelTypeMap = + { + 1: AudioTrackChannelType.mono, + 2: AudioTrackChannelType.stereo, + 3: AudioTrackChannelType.surround, + }; +} diff --git a/packages/video_player_avplay/lib/src/web_vtt.dart b/packages/video_player_avplay/lib/src/web_vtt.dart new file mode 100644 index 000000000..5527e62b6 --- /dev/null +++ b/packages/video_player_avplay/lib/src/web_vtt.dart @@ -0,0 +1,215 @@ +// Copyright 2013 The Flutter 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 'dart:convert'; + +import 'package:html/dom.dart'; +import 'package:html/parser.dart' as html_parser; + +import 'closed_caption_file.dart'; + +/// Represents a [ClosedCaptionFile], parsed from the WebVTT file format. +/// See: https://en.wikipedia.org/wiki/WebVTT +class WebVTTCaptionFile extends ClosedCaptionFile { + /// Parses a string into a [ClosedCaptionFile], assuming [fileContents] is in + /// the WebVTT file format. + /// * See: https://en.wikipedia.org/wiki/WebVTT + WebVTTCaptionFile(String fileContents) + : _captions = _parseCaptionsFromWebVTTString(fileContents); + + @override + List get captions => _captions; + + final List _captions; +} + +List _parseCaptionsFromWebVTTString(String file) { + final List captions = []; + + // Ignore metadata + final Set metadata = {'HEADER', 'NOTE', 'REGION', 'WEBVTT'}; + + int captionNumber = 1; + for (final List captionLines in _readWebVTTFile(file)) { + // CaptionLines represent a complete caption. + // E.g + // [ + // [00:00.000 --> 01:24.000 align:center] + // ['Introduction'] + // ] + // If caption has just header or time, but no text, `captionLines.length` will be 1. + if (captionLines.length < 2) { + continue; + } + + // If caption has header equal metadata, ignore. + final String metadaType = captionLines[0].split(' ')[0]; + if (metadata.contains(metadaType)) { + continue; + } + + // Caption has header + final bool hasHeader = captionLines.length > 2; + if (hasHeader) { + final int? tryParseCaptionNumber = int.tryParse(captionLines[0]); + if (tryParseCaptionNumber != null) { + captionNumber = tryParseCaptionNumber; + } + } + + final _CaptionRange? captionRange = _CaptionRange.fromWebVTTString( + hasHeader ? captionLines[1] : captionLines[0], + ); + + if (captionRange == null) { + continue; + } + + final String text = captionLines.sublist(hasHeader ? 2 : 1).join('\n'); + + // TODO(cyanglaz): Handle special syntax in VTT captions. + // https://github.com/flutter/flutter/issues/90007. + final String textWithoutFormat = _extractTextFromHtml(text); + + final Caption newCaption = Caption( + number: captionNumber, + start: captionRange.start, + end: captionRange.end, + text: textWithoutFormat, + ); + captions.add(newCaption); + captionNumber++; + } + + return captions; +} + +class _CaptionRange { + _CaptionRange(this.start, this.end); + + final Duration start; + final Duration end; + + // Assumes format from an VTT file. + // For example: + // 00:09.000 --> 00:11.000 + static _CaptionRange? fromWebVTTString(String line) { + final RegExp format = + RegExp(_webVTTTimeStamp + _webVTTArrow + _webVTTTimeStamp); + + if (!format.hasMatch(line)) { + return null; + } + + final List times = line.split(_webVTTArrow); + + final Duration? start = _parseWebVTTTimestamp(times[0]); + final Duration? end = _parseWebVTTTimestamp(times[1]); + + if (start == null || end == null) { + return null; + } + + return _CaptionRange(start, end); + } +} + +String _extractTextFromHtml(String htmlString) { + final Document document = html_parser.parse(htmlString); + final Element? body = document.body; + if (body == null) { + return ''; + } + final Element? bodyElement = html_parser.parse(body.text).documentElement; + return bodyElement?.text ?? ''; +} + +// Parses a time stamp in an VTT file into a Duration. +// +// Returns `null` if `timestampString` is in an invalid format. +// +// For example: +// +// _parseWebVTTTimestamp('00:01:08.430') +// returns +// Duration(hours: 0, minutes: 1, seconds: 8, milliseconds: 430) +Duration? _parseWebVTTTimestamp(String timestampString) { + if (!RegExp(_webVTTTimeStamp).hasMatch(timestampString)) { + return null; + } + + final List dotSections = timestampString.split('.'); + final List timeComponents = dotSections[0].split(':'); + + // Validating and parsing the `timestampString`, invalid format will result this method + // to return `null`. See https://www.w3.org/TR/webvtt1/#webvtt-timestamp for valid + // WebVTT timestamp format. + if (timeComponents.length > 3 || timeComponents.length < 2) { + return null; + } + int hours = 0; + if (timeComponents.length == 3) { + final String hourString = timeComponents.removeAt(0); + if (hourString.length < 2) { + return null; + } + hours = int.parse(hourString); + } + final int minutes = int.parse(timeComponents.removeAt(0)); + if (minutes < 0 || minutes > 59) { + return null; + } + final int seconds = int.parse(timeComponents.removeAt(0)); + if (seconds < 0 || seconds > 59) { + return null; + } + + final List milisecondsStyles = dotSections[1].split(' '); + + // TODO(cyanglaz): Handle caption styles. + // https://github.com/flutter/flutter/issues/90009. + // ```dart + // if (milisecondsStyles.length > 1) { + // List styles = milisecondsStyles.sublist(1); + // } + // ``` + // For a better readable code style, style parsing should happen before + // calling this method. See: https://github.com/flutter/plugins/pull/2878/files#r713381134. + final int milliseconds = int.parse(milisecondsStyles[0]); + + return Duration( + hours: hours, + minutes: minutes, + seconds: seconds, + milliseconds: milliseconds, + ); +} + +// Reads on VTT file and splits it into Lists of strings where each list is one +// caption. +List> _readWebVTTFile(String file) { + final List lines = LineSplitter.split(file).toList(); + + final List> captionStrings = >[]; + List currentCaption = []; + int lineIndex = 0; + for (final String line in lines) { + final bool isLineBlank = line.trim().isEmpty; + if (!isLineBlank) { + currentCaption.add(line); + } + + if (isLineBlank || lineIndex == lines.length - 1) { + captionStrings.add(currentCaption); + currentCaption = []; + } + + lineIndex += 1; + } + + return captionStrings; +} + +const String _webVTTTimeStamp = r'(\d+):(\d{2})(:\d{2})?\.(\d{3})'; +const String _webVTTArrow = r' --> '; diff --git a/packages/video_player_avplay/lib/video_player.dart b/packages/video_player_avplay/lib/video_player.dart new file mode 100644 index 000000000..86548d7bf --- /dev/null +++ b/packages/video_player_avplay/lib/video_player.dart @@ -0,0 +1,1181 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Copyright 2013 The Flutter 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 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'src/closed_caption_file.dart'; +import 'src/drm_configs.dart'; +import 'src/hole.dart'; +import 'src/tracks.dart'; +import 'video_player_platform_interface.dart'; + +export 'src/closed_caption_file.dart'; +export 'src/drm_configs.dart'; +export 'src/tracks.dart'; + +VideoPlayerPlatform? _lastVideoPlayerPlatform; + +VideoPlayerPlatform get _videoPlayerPlatform { + final VideoPlayerPlatform currentInstance = VideoPlayerPlatform.instance; + if (_lastVideoPlayerPlatform != currentInstance) { + // This will clear all open videos on the platform when a full restart is + // performed. + currentInstance.init(); + _lastVideoPlayerPlatform = currentInstance; + } + return currentInstance; +} + +/// The duration, current position, buffering state, error state and settings +/// of a [VideoPlayerController]. +class VideoPlayerValue { + /// Constructs a video with the given values. Only [duration] is required. The + /// rest will initialize with default values when unset. + VideoPlayerValue({ + required this.duration, + this.size = Size.zero, + this.position = Duration.zero, + this.caption = Caption.none, + this.captionOffset = Duration.zero, + this.tracks = const [], + this.buffered = 0, + this.isInitialized = false, + this.isPlaying = false, + this.isLooping = false, + this.isBuffering = false, + this.volume = 1.0, + this.playbackSpeed = 1.0, + this.errorDescription, + }); + + /// Returns an instance for a video that hasn't been loaded. + VideoPlayerValue.uninitialized() + : this(duration: Duration.zero, isInitialized: false); + + /// Returns an instance with the given [errorDescription]. + VideoPlayerValue.erroneous(String errorDescription) + : this( + duration: Duration.zero, + isInitialized: false, + errorDescription: errorDescription); + + /// This constant is just to indicate that parameter is not passed to [copyWith] + /// workaround for this issue https://github.com/dart-lang/language/issues/2009 + static const String _defaultErrorDescription = 'defaultErrorDescription'; + + /// The total duration of the video. + /// + /// The duration is [Duration.zero] if the video hasn't been initialized. + final Duration duration; + + /// The current playback position. + final Duration position; + + /// The [Caption] that should be displayed based on the current [position]. + /// + /// This field will never be null. If there is no caption for the current + /// [position], this will be a [Caption.none] object. + final Caption caption; + + /// The [Duration] that should be used to offset the current [position] to get the correct [Caption]. + /// + /// Defaults to Duration.zero. + final Duration captionOffset; + + /// The currently buffered size. + final int buffered; + + /// True if the video is playing. False if it's paused. + final bool isPlaying; + + /// True if the video is looping. + final bool isLooping; + + /// True if the video is currently buffering. + final bool isBuffering; + + /// The current volume of the playback. + final double volume; + + /// The current speed of the playback. + final double playbackSpeed; + + /// The current playback tracks. + final List tracks; + + /// A description of the error if present. + /// + /// If [hasError] is false this is `null`. + final String? errorDescription; + + /// The [size] of the currently loaded video. + final Size size; + + /// Indicates whether or not the video has been loaded and is ready to play. + final bool isInitialized; + + /// Indicates whether or not the video is in an error state. If this is true + /// [errorDescription] should have information about the problem. + bool get hasError => errorDescription != null; + + /// Returns [size.width] / [size.height]. + /// + /// Will return `1.0` if: + /// * [isInitialized] is `false` + /// * [size.width], or [size.height] is equal to `0.0` + /// * aspect ratio would be less than or equal to `0.0` + double get aspectRatio { + if (!isInitialized || size.width == 0 || size.height == 0) { + return 1.0; + } + final double aspectRatio = size.width / size.height; + if (aspectRatio <= 0) { + return 1.0; + } + return aspectRatio; + } + + /// Returns a new instance that has the same values as this current instance, + /// except for any overrides passed in as arguments to [copyWidth]. + VideoPlayerValue copyWith({ + Duration? duration, + Size? size, + Duration? position, + Caption? caption, + Duration? captionOffset, + List? tracks, + int? buffered, + bool? isInitialized, + bool? isPlaying, + bool? isLooping, + bool? isBuffering, + double? volume, + double? playbackSpeed, + String? errorDescription = _defaultErrorDescription, + }) { + return VideoPlayerValue( + duration: duration ?? this.duration, + size: size ?? this.size, + position: position ?? this.position, + caption: caption ?? this.caption, + captionOffset: captionOffset ?? this.captionOffset, + tracks: tracks ?? this.tracks, + buffered: buffered ?? this.buffered, + isInitialized: isInitialized ?? this.isInitialized, + isPlaying: isPlaying ?? this.isPlaying, + isLooping: isLooping ?? this.isLooping, + isBuffering: isBuffering ?? this.isBuffering, + volume: volume ?? this.volume, + playbackSpeed: playbackSpeed ?? this.playbackSpeed, + errorDescription: errorDescription != _defaultErrorDescription + ? errorDescription + : this.errorDescription, + ); + } + + @override + String toString() { + return '${objectRuntimeType(this, 'VideoPlayerValue')}(' + 'duration: $duration, ' + 'size: $size, ' + 'position: $position, ' + 'caption: $caption, ' + 'captionOffset: $captionOffset, ' + 'tracks: $tracks, ' + 'buffered: $buffered, ' + 'isInitialized: $isInitialized, ' + 'isPlaying: $isPlaying, ' + 'isLooping: $isLooping, ' + 'isBuffering: $isBuffering, ' + 'volume: $volume, ' + 'playbackSpeed: $playbackSpeed, ' + 'errorDescription: $errorDescription)'; + } +} + +/// Controls a platform video player, and provides updates when the state is +/// changing. +/// +/// Instances must be initialized with initialize. +/// +/// The video is displayed in a Flutter app by creating a [VideoPlayer] widget. +/// +/// To reclaim the resources used by the player call [dispose]. +/// +/// After [dispose] all further calls are ignored. +class VideoPlayerController extends ValueNotifier { + /// Constructs a [VideoPlayerController] playing a video from an asset. + /// + /// The name of the asset is given by the [dataSource] argument and must not be + /// null. The [package] argument must be non-null when the asset comes from a + /// package and null otherwise. + VideoPlayerController.asset( + this.dataSource, { + this.package, + this.closedCaptionFile, + this.videoPlayerOptions, + }) : dataSourceType = DataSourceType.asset, + formatHint = null, + httpHeaders = const {}, + drmConfigs = null, + playerOptions = const {}, + super(VideoPlayerValue(duration: Duration.zero)); + + /// Constructs a [VideoPlayerController] playing a video from obtained from + /// the network. + /// + /// The URI for the video is given by the [dataSource] argument and must not be + /// null. + /// **Android only**: The [formatHint] option allows the caller to override + /// the video format detection code. + /// [httpHeaders] option allows to specify HTTP headers + /// for the request to the [dataSource]. + VideoPlayerController.network( + this.dataSource, { + this.formatHint, + this.closedCaptionFile, + this.videoPlayerOptions, + this.httpHeaders = const {}, + this.drmConfigs, + this.playerOptions, + }) : dataSourceType = DataSourceType.network, + package = null, + super(VideoPlayerValue(duration: Duration.zero)); + + /// Constructs a [VideoPlayerController] playing a video from a file. + /// + /// This will load the file from the file-URI given by: + /// `'file://${file.path}'`. + VideoPlayerController.file( + File file, { + this.closedCaptionFile, + this.videoPlayerOptions, + }) : dataSource = 'file://${file.path}', + dataSourceType = DataSourceType.file, + package = null, + formatHint = null, + httpHeaders = const {}, + drmConfigs = null, + playerOptions = const {}, + super(VideoPlayerValue(duration: Duration.zero)); + + /// Constructs a [VideoPlayerController] playing a video from a contentUri. + /// + /// This will load the video from the input content-URI. + /// This is supported on Android only. + VideoPlayerController.contentUri( + Uri contentUri, { + this.closedCaptionFile, + this.videoPlayerOptions, + }) : assert(defaultTargetPlatform == TargetPlatform.android, + 'VideoPlayerController.contentUri is only supported on Android.'), + dataSource = contentUri.toString(), + dataSourceType = DataSourceType.contentUri, + package = null, + formatHint = null, + httpHeaders = const {}, + drmConfigs = null, + playerOptions = const {}, + super(VideoPlayerValue(duration: Duration.zero)); + + /// The URI to the video file. This will be in different formats depending on + /// the [DataSourceType] of the original video. + final String dataSource; + + /// HTTP headers used for the request to the [dataSource]. + /// Only for [VideoPlayerController.network]. + /// Always empty for other video types. + final Map httpHeaders; + + /// Configurations for playing DRM content (optional). + /// Only for [VideoPlayerController.network]. + final DrmConfigs? drmConfigs; + + /// Player Options used for add additional parameters. + /// Only for [VideoPlayerController.network]. + final Map? playerOptions; + + /// **Android only**. Will override the platform's generic file format + /// detection with whatever is set here. + final VideoFormat? formatHint; + + /// Describes the type of data source this [VideoPlayerController] + /// is constructed with. + final DataSourceType dataSourceType; + + /// Provide additional configuration options (optional). Like setting the audio mode to mix + final VideoPlayerOptions? videoPlayerOptions; + + /// Only set for [asset] videos. The package that the asset was loaded from. + final String? package; + + /// Optional field to specify a file containing the closed + /// captioning. + /// + /// This future will be awaited and the file will be loaded when + /// [initialize()] is called. + final Future? closedCaptionFile; + + ClosedCaptionFile? _closedCaptionFile; + Timer? _timer; + bool _isDisposed = false; + Completer? _creatingCompleter; + StreamSubscription? _eventSubscription; + _VideoAppLifeCycleObserver? _lifeCycleObserver; + + /// The id of a player that hasn't been initialized. + @visibleForTesting + static const int kUninitializedPlayerId = -1; + int _playerId = kUninitializedPlayerId; + + /// This is just exposed for testing. It shouldn't be used by anyone depending + /// on the plugin. + @visibleForTesting + int get playerId => _playerId; + + final MethodChannel _channel = + const MethodChannel('dev.flutter.videoplayer.drm'); + + /// Attempts to open the given [dataSource] and load metadata about the video. + Future initialize() async { + final bool allowBackgroundPlayback = + videoPlayerOptions?.allowBackgroundPlayback ?? false; + if (!allowBackgroundPlayback) { + _lifeCycleObserver = _VideoAppLifeCycleObserver(this); + } + _lifeCycleObserver?.initialize(); + _creatingCompleter = Completer(); + + late DataSource dataSourceDescription; + switch (dataSourceType) { + case DataSourceType.asset: + dataSourceDescription = DataSource( + sourceType: DataSourceType.asset, + asset: dataSource, + package: package, + ); + break; + case DataSourceType.network: + dataSourceDescription = DataSource( + sourceType: DataSourceType.network, + uri: dataSource, + formatHint: formatHint, + httpHeaders: httpHeaders, + drmConfigs: drmConfigs, + playerOptions: playerOptions, + ); + break; + case DataSourceType.file: + dataSourceDescription = DataSource( + sourceType: DataSourceType.file, + uri: dataSource, + ); + break; + case DataSourceType.contentUri: + dataSourceDescription = DataSource( + sourceType: DataSourceType.contentUri, + uri: dataSource, + ); + break; + } + + if (videoPlayerOptions?.mixWithOthers != null) { + await _videoPlayerPlatform + .setMixWithOthers(videoPlayerOptions!.mixWithOthers); + } + + _playerId = (await _videoPlayerPlatform.create(dataSourceDescription)) ?? + kUninitializedPlayerId; + _creatingCompleter!.complete(null); + final Completer initializingCompleter = Completer(); + + void eventListener(VideoEvent event) { + if (_isDisposed) { + return; + } + + switch (event.eventType) { + case VideoEventType.initialized: + value = value.copyWith( + duration: event.duration, + size: event.size, + isInitialized: event.duration != null, + errorDescription: null, + ); + initializingCompleter.complete(null); + _applyLooping(); + _applyVolume(); + _applyPlayPause(); + break; + case VideoEventType.completed: + // In this case we need to stop _timer, set isPlaying=false, and + // position=value.duration. Instead of setting the values directly, + // we use pause() and seekTo() to ensure the platform stops playing + // and seeks to the last frame of the video. + pause().then((void pauseResult) => seekTo(value.duration)); + break; + case VideoEventType.bufferingUpdate: + value = value.copyWith(buffered: event.buffered); + break; + case VideoEventType.bufferingStart: + value = value.copyWith(isBuffering: true); + break; + case VideoEventType.bufferingEnd: + value = value.copyWith(isBuffering: false); + break; + case VideoEventType.subtitleUpdate: + final Caption caption = Caption( + number: 0, + start: value.position, + end: value.position + (event.duration ?? Duration.zero), + text: event.text ?? '', + ); + value = value.copyWith(caption: caption); + break; + case VideoEventType.unknown: + break; + } + } + + if (closedCaptionFile != null) { + _closedCaptionFile ??= await closedCaptionFile; + value = value.copyWith(caption: _getCaptionAt(value.position)); + } + + if (drmConfigs?.licenseCallback != null) { + _channel.setMethodCallHandler((MethodCall call) async { + if (call.method == 'requestLicense') { + final Map argumentsMap = + call.arguments as Map; + final Uint8List message = argumentsMap['message']! as Uint8List; + return drmConfigs!.licenseCallback!(message); + } else { + throw Exception('not implemented ${call.method}'); + } + }); + } + + void errorListener(Object obj) { + final PlatformException e = obj as PlatformException; + value = VideoPlayerValue.erroneous(e.message!); + _timer?.cancel(); + if (!initializingCompleter.isCompleted) { + initializingCompleter.completeError(obj); + } + } + + _eventSubscription = _videoPlayerPlatform + .videoEventsFor(_playerId) + .listen(eventListener, onError: errorListener); + return initializingCompleter.future; + } + + @override + Future dispose() async { + if (_creatingCompleter != null) { + await _creatingCompleter!.future; + if (!_isDisposed) { + _isDisposed = true; + _timer?.cancel(); + await _eventSubscription?.cancel(); + await _videoPlayerPlatform.dispose(_playerId); + } + _lifeCycleObserver?.dispose(); + } + _isDisposed = true; + super.dispose(); + } + + /// Starts playing the video. + /// + /// If the video is at the end, this method starts playing from the beginning. + /// + /// This method returns a future that completes as soon as the "play" command + /// has been sent to the platform, not when playback itself is totally + /// finished. + Future play() async { + if (value.position == value.duration) { + await seekTo(Duration.zero); + } + value = value.copyWith(isPlaying: true); + await _applyPlayPause(); + } + + /// Sets the video activated. Use it if create two native players. + Future activate() async { + return _applyActivate(); + } + + /// Sets the video deactivated. Use it if create two native players. + Future deactivate() async { + return _applyDeactivate(); + } + + /// Sets whether or not the video should loop after playing once. See also + /// [VideoPlayerValue.isLooping]. + Future setLooping(bool looping) async { + value = value.copyWith(isLooping: looping); + await _applyLooping(); + } + + /// Pauses the video. + Future pause() async { + value = value.copyWith(isPlaying: false); + await _applyPlayPause(); + } + + Future _applyLooping() async { + if (_isDisposedOrNotInitialized) { + return; + } + await _videoPlayerPlatform.setLooping(_playerId, value.isLooping); + } + + Future _applyActivate() async { + if (_isDisposedOrNotInitialized) { + return false; + } + return _videoPlayerPlatform.setActivate(_playerId); + } + + Future _applyDeactivate() async { + if (_isDisposedOrNotInitialized) { + return false; + } + return _videoPlayerPlatform.setDeactivate(_playerId); + } + + Future _applyPlayPause() async { + if (_isDisposedOrNotInitialized) { + return; + } + if (value.isPlaying) { + await _videoPlayerPlatform.play(_playerId); + + // Cancel previous timer. + _timer?.cancel(); + _timer = Timer.periodic( + const Duration(milliseconds: 500), + (Timer timer) async { + if (_isDisposed) { + return; + } + final Duration? newPosition = await position; + if (newPosition == null) { + return; + } + _updatePosition(newPosition); + }, + ); + + // This ensures that the correct playback speed is always applied when + // playing back. This is necessary because we do not set playback speed + // when paused. + await _applyPlaybackSpeed(); + } else { + _timer?.cancel(); + await _videoPlayerPlatform.pause(_playerId); + } + } + + Future _applyVolume() async { + if (_isDisposedOrNotInitialized) { + return; + } + await _videoPlayerPlatform.setVolume(_playerId, value.volume); + } + + Future _applyPlaybackSpeed() async { + if (_isDisposedOrNotInitialized) { + return; + } + + // Setting the playback speed on iOS will trigger the video to play. We + // prevent this from happening by not applying the playback speed until + // the video is manually played from Flutter. + if (!value.isPlaying) { + return; + } + + await _videoPlayerPlatform.setPlaybackSpeed( + _playerId, + value.playbackSpeed, + ); + } + + /// The position in the current video. + Future get position async { + if (_isDisposed) { + return null; + } + return _videoPlayerPlatform.getPosition(_playerId); + } + + /// Sets the video's current timestamp to be at [moment]. The next + /// time the video is played it will resume from the given [moment]. + /// + /// If [moment] is outside of the video's full range it will be automatically + /// and silently clamped. + Future seekTo(Duration position) async { + if (_isDisposedOrNotInitialized) { + return; + } + if (position > value.duration) { + position = value.duration; + } else if (position < Duration.zero) { + position = Duration.zero; + } + await _videoPlayerPlatform.seekTo(_playerId, position); + _updatePosition(position); + } + + /// The video tracks in the current video. + Future?> get videoTracks async { + if (!value.isInitialized || _isDisposed) { + return null; + } + return _videoPlayerPlatform.getVideoTracks(_playerId); + } + + /// The audio tracks in the current video. + Future?> get audioTracks async { + if (!value.isInitialized || _isDisposed) { + return null; + } + return _videoPlayerPlatform.getAudioTracks(_playerId); + } + + /// The text tracks in the current video. + Future?> get textTracks async { + if (!value.isInitialized || _isDisposed) { + return null; + } + return _videoPlayerPlatform.getTextTracks(_playerId); + } + + /// Sets the selected tracks. + Future setTrackSelection(Track track) async { + if (!value.isInitialized || _isDisposed) { + return false; + } + return _videoPlayerPlatform.setTrackSelection(_playerId, track); + } + + /// Sets the audio volume of [this]. + /// + /// [volume] indicates a value between 0.0 (silent) and 1.0 (full volume) on a + /// linear scale. + Future setVolume(double volume) async { + value = value.copyWith(volume: volume.clamp(0.0, 1.0)); + await _applyVolume(); + } + + /// Sets the playback speed of [this]. + /// + /// [speed] indicates a speed value with different platforms accepting + /// different ranges for speed values. The [speed] must be greater than 0. + /// + /// The values will be handled as follows: + /// * On web, the audio will be muted at some speed when the browser + /// determines that the sound would not be useful anymore. For example, + /// "Gecko mutes the sound outside the range `0.25` to `5.0`" (see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/playbackRate). + /// * On Android, some very extreme speeds will not be played back accurately. + /// Instead, your video will still be played back, but the speed will be + /// clamped by ExoPlayer (but the values are allowed by the player, like on + /// web). + /// * On iOS, you can sometimes not go above `2.0` playback speed on a video. + /// An error will be thrown for if the option is unsupported. It is also + /// possible that your specific video cannot be slowed down, in which case + /// the plugin also reports errors. + Future setPlaybackSpeed(double speed) async { + if (speed < 0) { + throw ArgumentError.value( + speed, + 'Negative playback speeds are generally unsupported.', + ); + } else if (speed == 0) { + throw ArgumentError.value( + speed, + 'Zero playback speed is generally unsupported. Consider using [pause].', + ); + } + + value = value.copyWith(playbackSpeed: speed); + await _applyPlaybackSpeed(); + } + + /// Sets the caption offset. + /// + /// The [offset] will be used when getting the correct caption for a specific position. + /// The [offset] can be positive or negative. + /// + /// The values will be handled as follows: + /// * 0: This is the default behaviour. No offset will be applied. + /// * >0: The caption will have a negative offset. So you will get caption text from the past. + /// * <0: The caption will have a positive offset. So you will get caption text from the future. + void setCaptionOffset(Duration offset) { + value = value.copyWith( + captionOffset: offset, + caption: _getCaptionAt(value.position), + ); + } + + /// The closed caption based on the current [position] in the video. + /// + /// If there are no closed captions at the current [position], this will + /// return an empty [Caption]. + /// + /// If no [closedCaptionFile] was specified, this will always return an empty + /// [Caption]. + Caption _getCaptionAt(Duration position) { + if (_closedCaptionFile == null) { + return value.caption; + } + + final Duration delayedPosition = position + value.captionOffset; + // TODO(johnsonmh): This would be more efficient as a binary search. + for (final Caption caption in _closedCaptionFile!.captions) { + if (caption.start <= delayedPosition && caption.end >= delayedPosition) { + return caption; + } + } + + return Caption.none; + } + + void _updatePosition(Duration position) { + value = value.copyWith( + position: position, + caption: _getCaptionAt(position), + ); + } + + @override + void removeListener(VoidCallback listener) { + // Prevent VideoPlayer from causing an exception to be thrown when attempting to + // remove its own listener after the controller has already been disposed. + if (!_isDisposed) { + super.removeListener(listener); + } + } + + bool get _isDisposedOrNotInitialized => _isDisposed || !value.isInitialized; +} + +class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver { + _VideoAppLifeCycleObserver(this._controller); + + bool _wasPlayingBeforePause = false; + final VideoPlayerController _controller; + + void initialize() { + _ambiguate(WidgetsBinding.instance)!.addObserver(this); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.paused) { + _wasPlayingBeforePause = _controller.value.isPlaying; + _controller.pause(); + } else if (state == AppLifecycleState.resumed) { + if (_wasPlayingBeforePause) { + _controller.play(); + } + } + } + + void dispose() { + _ambiguate(WidgetsBinding.instance)!.removeObserver(this); + } +} + +/// Widget that displays the video controlled by [controller]. +class VideoPlayer extends StatefulWidget { + /// Uses the given [controller] for all video rendered in this widget. + const VideoPlayer(this.controller, {super.key}); + + /// The [VideoPlayerController] responsible for the video being rendered in + /// this widget. + final VideoPlayerController controller; + + @override + State createState() => _VideoPlayerState(); +} + +class _VideoPlayerState extends State { + _VideoPlayerState() { + _listener = () { + final int newPlayerId = widget.controller.playerId; + if (newPlayerId != _playerId) { + setState(() { + _playerId = newPlayerId; + }); + } + }; + } + + late VoidCallback _listener; + + late int _playerId; + + final GlobalKey _videoBoxKey = GlobalKey(); + Rect _playerRect = Rect.zero; + + @override + void initState() { + super.initState(); + _playerId = widget.controller.playerId; + // Need to listen for initialization events since the actual player ID + // becomes available after asynchronous initialization finishes. + widget.controller.addListener(_listener); + + WidgetsBinding.instance.addPostFrameCallback(_afterFrameLayout); + } + + void _afterFrameLayout(_) { + if (widget.controller.value.isInitialized) { + final Rect currentRect = _getCurrentRect(); + if (currentRect != Rect.zero && _playerRect != currentRect) { + _videoPlayerPlatform.setDisplayGeometry( + _playerId, + currentRect.left.toInt(), + currentRect.top.toInt(), + currentRect.width.toInt(), + currentRect.height.toInt(), + ); + _playerRect = currentRect; + } + } + WidgetsBinding.instance.addPostFrameCallback(_afterFrameLayout); + } + + Rect _getCurrentRect() { + final RenderObject? renderObject = + _videoBoxKey.currentContext?.findRenderObject(); + if (renderObject == null) { + return Rect.zero; + } + // ignore: deprecated_member_use + final double pixelRatio = WidgetsBinding.instance.window.devicePixelRatio; + final RenderBox renderBox = renderObject as RenderBox; + final Offset offset = renderBox.localToGlobal(Offset.zero) * pixelRatio; + final Size size = renderBox.size * pixelRatio; + return offset & size; + } + + @override + void didUpdateWidget(VideoPlayer oldWidget) { + super.didUpdateWidget(oldWidget); + oldWidget.controller.removeListener(_listener); + _playerId = widget.controller.playerId; + widget.controller.addListener(_listener); + } + + @override + void deactivate() { + super.deactivate(); + widget.controller.removeListener(_listener); + } + + @override + Widget build(BuildContext context) { + return Container(key: _videoBoxKey, child: const Hole()); + } +} + +/// Used to configure the [VideoProgressIndicator] widget's colors for how it +/// describes the video's status. +/// +/// The widget uses default colors that are customizeable through this class. +class VideoProgressColors { + /// Any property can be set to any color. They each have defaults. + /// + /// [playedColor] defaults to red at 70% opacity. This fills up a portion of + /// the [VideoProgressIndicator] to represent how much of the video has played + /// so far. + /// + /// [bufferedColor] defaults to blue at 20% opacity. This fills up a portion + /// of [VideoProgressIndicator] to represent how much of the video has + /// buffered so far. + /// + /// [backgroundColor] defaults to gray at 50% opacity. This is the background + /// color behind both [playedColor] and [bufferedColor] to denote the total + /// size of the video compared to either of those values. + const VideoProgressColors({ + this.playedColor = const Color.fromRGBO(255, 0, 0, 0.7), + this.bufferedColor = const Color.fromRGBO(50, 50, 200, 0.2), + this.backgroundColor = const Color.fromRGBO(200, 200, 200, 0.5), + }); + + /// [playedColor] defaults to red at 70% opacity. This fills up a portion of + /// the [VideoProgressIndicator] to represent how much of the video has played + /// so far. + final Color playedColor; + + /// [bufferedColor] defaults to blue at 20% opacity. This fills up a portion + /// of [VideoProgressIndicator] to represent how much of the video has + /// buffered so far. + final Color bufferedColor; + + /// [backgroundColor] defaults to gray at 50% opacity. This is the background + /// color behind both [playedColor] and [bufferedColor] to denote the total + /// size of the video compared to either of those values. + final Color backgroundColor; +} + +class _VideoScrubber extends StatefulWidget { + const _VideoScrubber({ + required this.child, + required this.controller, + }); + + final Widget child; + final VideoPlayerController controller; + + @override + _VideoScrubberState createState() => _VideoScrubberState(); +} + +class _VideoScrubberState extends State<_VideoScrubber> { + bool _controllerWasPlaying = false; + + VideoPlayerController get controller => widget.controller; + + @override + Widget build(BuildContext context) { + void seekToRelativePosition(Offset globalPosition) { + final RenderBox box = context.findRenderObject()! as RenderBox; + final Offset tapPos = box.globalToLocal(globalPosition); + final double relative = tapPos.dx / box.size.width; + final Duration position = controller.value.duration * relative; + controller.seekTo(position); + } + + return GestureDetector( + behavior: HitTestBehavior.opaque, + child: widget.child, + onHorizontalDragStart: (DragStartDetails details) { + if (!controller.value.isInitialized) { + return; + } + _controllerWasPlaying = controller.value.isPlaying; + if (_controllerWasPlaying) { + controller.pause(); + } + }, + onHorizontalDragUpdate: (DragUpdateDetails details) { + if (!controller.value.isInitialized) { + return; + } + seekToRelativePosition(details.globalPosition); + }, + onHorizontalDragEnd: (DragEndDetails details) { + if (_controllerWasPlaying && + controller.value.position != controller.value.duration) { + controller.play(); + } + }, + onTapDown: (TapDownDetails details) { + if (!controller.value.isInitialized) { + return; + } + seekToRelativePosition(details.globalPosition); + }, + ); + } +} + +/// Displays the play/buffering status of the video controlled by [controller]. +/// +/// If [allowScrubbing] is true, this widget will detect taps and drags and +/// seek the video accordingly. +/// +/// [padding] allows to specify some extra padding around the progress indicator +/// that will also detect the gestures. +class VideoProgressIndicator extends StatefulWidget { + /// Construct an instance that displays the play/buffering status of the video + /// controlled by [controller]. + /// + /// Defaults will be used for everything except [controller] if they're not + /// provided. [allowScrubbing] defaults to false, and [padding] will default + /// to `top: 5.0`. + const VideoProgressIndicator( + this.controller, { + super.key, + this.colors = const VideoProgressColors(), + required this.allowScrubbing, + this.padding = const EdgeInsets.only(top: 5.0), + }); + + /// The [VideoPlayerController] that actually associates a video with this + /// widget. + final VideoPlayerController controller; + + /// The default colors used throughout the indicator. + /// + /// See [VideoProgressColors] for default values. + final VideoProgressColors colors; + + /// When true, the widget will detect touch input and try to seek the video + /// accordingly. The widget ignores such input when false. + /// + /// Defaults to false. + final bool allowScrubbing; + + /// This allows for visual padding around the progress indicator that can + /// still detect gestures via [allowScrubbing]. + /// + /// Defaults to `top: 5.0`. + final EdgeInsets padding; + + @override + State createState() => _VideoProgressIndicatorState(); +} + +class _VideoProgressIndicatorState extends State { + _VideoProgressIndicatorState() { + listener = () { + if (!mounted) { + return; + } + setState(() {}); + }; + } + + late VoidCallback listener; + + VideoPlayerController get controller => widget.controller; + + VideoProgressColors get colors => widget.colors; + + @override + void initState() { + super.initState(); + controller.addListener(listener); + } + + @override + void deactivate() { + controller.removeListener(listener); + super.deactivate(); + } + + @override + Widget build(BuildContext context) { + Widget progressIndicator; + if (controller.value.isInitialized) { + final int duration = controller.value.duration.inMilliseconds; + final int position = controller.value.position.inMilliseconds; + + progressIndicator = Stack( + fit: StackFit.passthrough, + children: [ + LinearProgressIndicator( + value: duration != 0 ? position / duration : 0, + valueColor: AlwaysStoppedAnimation(colors.playedColor), + backgroundColor: Colors.transparent, + ), + ], + ); + } else { + progressIndicator = LinearProgressIndicator( + valueColor: AlwaysStoppedAnimation(colors.playedColor), + backgroundColor: colors.backgroundColor, + ); + } + final Widget paddedProgressIndicator = Padding( + padding: widget.padding, + child: progressIndicator, + ); + if (widget.allowScrubbing) { + return _VideoScrubber( + controller: controller, + child: paddedProgressIndicator, + ); + } else { + return paddedProgressIndicator; + } + } +} + +/// Widget for displaying closed captions on top of a video. +/// +/// If [text] is null, this widget will not display anything. +/// +/// If [textStyle] is supplied, it will be used to style the text in the closed +/// caption. +/// +/// Note: in order to have closed captions, you need to specify a +/// [VideoPlayerController.closedCaptionFile]. +/// +/// Usage: +/// +/// ```dart +/// Stack(children: [ +/// VideoPlayer(_controller), +/// ClosedCaption(text: _controller.value.caption.text), +/// ]), +/// ``` +class ClosedCaption extends StatelessWidget { + /// Creates a a new closed caption, designed to be used with + /// [VideoPlayerValue.caption]. + /// + /// If [text] is null or empty, nothing will be displayed. + const ClosedCaption({super.key, this.text, this.textStyle}); + + /// The text that will be shown in the closed caption, or null if no caption + /// should be shown. + /// If the text is empty the caption will not be shown. + final String? text; + + /// Specifies how the text in the closed caption should look. + /// + /// If null, defaults to [DefaultTextStyle.of(context).style] with size 36 + /// font colored white. + final TextStyle? textStyle; + + @override + Widget build(BuildContext context) { + final String? text = this.text; + if (text == null || text.isEmpty) { + return const SizedBox.shrink(); + } + + final TextStyle effectiveTextStyle = textStyle ?? + DefaultTextStyle.of(context).style.copyWith( + fontSize: 36.0, + color: Colors.white, + ); + + return Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only(bottom: 24.0), + child: DecoratedBox( + decoration: BoxDecoration( + color: const Color(0xB8000000), + borderRadius: BorderRadius.circular(2.0), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 2.0), + child: Text(text, style: effectiveTextStyle), + ), + ), + ), + ); + } +} + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +// TODO(ianh): Remove this once we roll stable in late 2021. +T? _ambiguate(T? value) => value; diff --git a/packages/video_player_avplay/lib/video_player_platform_interface.dart b/packages/video_player_avplay/lib/video_player_platform_interface.dart new file mode 100644 index 000000000..db56b9a3f --- /dev/null +++ b/packages/video_player_avplay/lib/video_player_platform_interface.dart @@ -0,0 +1,427 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Copyright 2013 The Flutter 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 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'src/drm_configs.dart'; +import 'src/tracks.dart'; +import 'src/video_player_tizen.dart'; + +/// The interface that implementations of video_player must implement. +/// +/// Platform implementations should extend this class rather than implement it as `video_player` +/// does not consider newly added methods to be breaking changes. Extending this class +/// (using `extends`) ensures that the subclass will get the default implementation, while +/// platform implementations that `implements` this interface will be broken by newly added +/// [VideoPlayerPlatform] methods. +abstract class VideoPlayerPlatform extends PlatformInterface { + /// Constructs a VideoPlayerPlatform. + VideoPlayerPlatform() : super(token: _token); + + static final Object _token = Object(); + + static VideoPlayerPlatform _instance = VideoPlayerTizen(); + + /// The default instance of [VideoPlayerPlatform] to use. + /// + /// Defaults to [VideoPlayerTizen]. + static VideoPlayerPlatform get instance => _instance; + + /// Platform-specific plugins should override this with their own + /// platform-specific class that extends [VideoPlayerPlatform] when they + /// register themselves. + static set instance(VideoPlayerPlatform instance) { + PlatformInterface.verify(instance, _token); + _instance = instance; + } + + /// Initializes the platform interface and disposes all existing players. + /// + /// This method is called when the plugin is first initialized + /// and on every full restart. + Future init() { + throw UnimplementedError('init() has not been implemented.'); + } + + /// Clears one video. + Future dispose(int playerId) { + throw UnimplementedError('dispose() has not been implemented.'); + } + + /// Creates an instance of a video player and returns its playerId. + Future create(DataSource dataSource) { + throw UnimplementedError('create() has not been implemented.'); + } + + /// Returns a Stream of [VideoEventType]s. + Stream videoEventsFor(int playerId) { + throw UnimplementedError('videoEventsFor() has not been implemented.'); + } + + /// Sets the looping attribute of the video. + Future setLooping(int playerId, bool looping) { + throw UnimplementedError('setLooping() has not been implemented.'); + } + + /// Starts the video playback. + Future play(int playerId) { + throw UnimplementedError('play() has not been implemented.'); + } + + /// Stops the video playback. + Future pause(int playerId) { + throw UnimplementedError('pause() has not been implemented.'); + } + + /// Set the video activated. + Future setActivate(int playerId) { + throw UnimplementedError('setActivate() has not been implemented.'); + } + + /// Set the video deactivated. + Future setDeactivate(int playerId) { + throw UnimplementedError('setDeactivate() has not been implemented.'); + } + + /// Sets the volume to a range between 0.0 and 1.0. + Future setVolume(int playerId, double volume) { + throw UnimplementedError('setVolume() has not been implemented.'); + } + + /// Sets the video position to a [Duration] from the start. + Future seekTo(int playerId, Duration position) { + throw UnimplementedError('seekTo() has not been implemented.'); + } + + /// Gets the video tracks as a list of [VideoTrack]. + Future> getVideoTracks(int playerId) { + throw UnimplementedError('getVideoTracks() has not been implemented.'); + } + + /// Gets the audio tracks as a list of [AudioTrack]. + Future> getAudioTracks(int playerId) { + throw UnimplementedError('getAudioTracks() has not been implemented.'); + } + + /// Gets the text tracks as a list of [TextTrack]. + Future> getTextTracks(int playerId) { + throw UnimplementedError('getTextTracks() has not been implemented.'); + } + + /// Sets the selected track. + Future setTrackSelection(int playerId, Track track) { + throw UnimplementedError('setTrackSelection() has not been implemented.'); + } + + /// Sets the playback speed to a [speed] value indicating the playback rate. + Future setPlaybackSpeed(int playerId, double speed) { + throw UnimplementedError('setPlaybackSpeed() has not been implemented.'); + } + + /// Gets the video position as [Duration] from the start. + Future getPosition(int playerId) { + throw UnimplementedError('getPosition() has not been implemented.'); + } + + /// Returns a widget displaying the video with a given playerId. + Widget buildView(int playerId) { + throw UnimplementedError('buildView() has not been implemented.'); + } + + /// Sets the audio mode to mix with other sources. + Future setMixWithOthers(bool mixWithOthers) { + throw UnimplementedError('setMixWithOthers() has not been implemented.'); + } + + /// Sets the video display geometry. + Future setDisplayGeometry( + int playerId, + int x, + int y, + int width, + int height, + ) { + throw UnimplementedError('setDisplayGeometry() has not been implemented.'); + } +} + +/// Description of the data source used to create an instance of +/// the video player. +class DataSource { + /// Constructs an instance of [DataSource]. + /// + /// The [sourceType] is always required. + /// + /// The [uri] argument takes the form of `'https://example.com/video.mp4'` or + /// `'file://${file.path}'`. + /// + /// The [formatHint] argument can be null. + /// + /// The [asset] argument takes the form of `'assets/video.mp4'`. + /// + /// The [package] argument must be non-null when the asset comes from a + /// package and null otherwise. + DataSource({ + required this.sourceType, + this.uri, + this.formatHint, + this.asset, + this.package, + this.httpHeaders = const {}, + this.drmConfigs, + this.playerOptions, + }); + + /// The way in which the video was originally loaded. + /// + /// This has nothing to do with the video's file type. It's just the place + /// from which the video is fetched from. + final DataSourceType sourceType; + + /// The URI to the video file. + /// + /// This will be in different formats depending on the [DataSourceType] of + /// the original video. + final String? uri; + + /// **Android only**. Will override the platform's generic file format + /// detection with whatever is set here. + final VideoFormat? formatHint; + + /// HTTP headers used for the request to the [uri]. + /// Only for [DataSourceType.network] videos. + /// Always empty for other video types. + Map httpHeaders; + + /// The name of the asset. Only set for [DataSourceType.asset] videos. + final String? asset; + + /// The package that the asset was loaded from. Only set for + /// [DataSourceType.asset] videos. + final String? package; + + /// Configurations for playing DRM content. + DrmConfigs? drmConfigs; + + /// Set additional optional player settings. + Map? playerOptions; +} + +/// The way in which the video was originally loaded. +/// +/// This has nothing to do with the video's file type. It's just the place +/// from which the video is fetched from. +enum DataSourceType { + /// The video was included in the app's asset files. + asset, + + /// The video was downloaded from the internet. + network, + + /// The video was loaded off of the local filesystem. + file, + + /// The video is available via contentUri. Android only. + contentUri, +} + +/// The file format of the given video. +enum VideoFormat { + /// Dynamic Adaptive Streaming over HTTP, also known as MPEG-DASH. + dash, + + /// HTTP Live Streaming. + hls, + + /// Smooth Streaming. + ss, + + /// Any format other than the other ones defined in this enum. + other, +} + +/// Event emitted from the platform implementation. +@immutable +class VideoEvent { + /// Creates an instance of [VideoEvent]. + /// + /// The [eventType] argument is required. + /// + /// Depending on the [eventType], the [duration], [size] and [buffered] + /// arguments can be null. + // TODO(stuartmorgan): Temporarily suppress warnings about not using const + // in all of the other video player packages, fix this, and then update + // the other packages to use const. + // ignore: prefer_const_constructors_in_immutables + VideoEvent({ + required this.eventType, + this.duration, + this.size, + this.buffered, + this.text, + }); + + /// The type of the event. + final VideoEventType eventType; + + /// Duration of the video. + /// + /// Only used if [eventType] is [VideoEventType.initialized]. + final Duration? duration; + + /// Size of the video. + /// + /// Only used if [eventType] is [VideoEventType.initialized]. + final Size? size; + + /// Buffered size of the video. + /// + /// Only used if [eventType] is [VideoEventType.bufferingUpdate]. + final int? buffered; + + /// Subtitle text of the video. + /// + /// Only used if [eventType] is [VideoEventType.subtitleUpdate]. + final String? text; + + @override + bool operator ==(Object other) { + return identical(this, other) || + other is VideoEvent && + runtimeType == other.runtimeType && + eventType == other.eventType && + duration == other.duration && + size == other.size && + buffered == other.buffered && + text == other.text; + } + + @override + int get hashCode => + eventType.hashCode ^ + duration.hashCode ^ + size.hashCode ^ + buffered.hashCode ^ + text.hashCode; +} + +/// Type of the event. +/// +/// Emitted by the platform implementation when the video is initialized or +/// completed or to communicate buffering events. +enum VideoEventType { + /// The video has been initialized. + initialized, + + /// The playback has ended. + completed, + + /// Updated information on the buffering state. + bufferingUpdate, + + /// The video started to buffer. + bufferingStart, + + /// The video stopped to buffer. + bufferingEnd, + + /// Updated the video subtitle text. + subtitleUpdate, + + /// An unknown event has been received. + unknown, +} + +/// Describes a discrete segment of time within a video using a [start] and +/// [end] [Duration]. +@immutable +class DurationRange { + /// Trusts that the given [start] and [end] are actually in order. They should + /// both be non-null. + // TODO(stuartmorgan): Temporarily suppress warnings about not using const + // in all of the other video player packages, fix this, and then update + // the other packages to use const. + // ignore: prefer_const_constructors_in_immutables + DurationRange(this.start, this.end); + + /// The beginning of the segment described relative to the beginning of the + /// entire video. Should be shorter than or equal to [end]. + /// + /// For example, if the entire video is 4 minutes long and the range is from + /// 1:00-2:00, this should be a `Duration` of one minute. + final Duration start; + + /// The end of the segment described as a duration relative to the beginning of + /// the entire video. This is expected to be non-null and longer than or equal + /// to [start]. + /// + /// For example, if the entire video is 4 minutes long and the range is from + /// 1:00-2:00, this should be a `Duration` of two minutes. + final Duration end; + + /// Assumes that [duration] is the total length of the video that this + /// DurationRange is a segment form. It returns the percentage that [start] is + /// through the entire video. + /// + /// For example, assume that the entire video is 4 minutes long. If [start] has + /// a duration of one minute, this will return `0.25` since the DurationRange + /// starts 25% of the way through the video's total length. + double startFraction(Duration duration) { + return start.inMilliseconds / duration.inMilliseconds; + } + + /// Assumes that [duration] is the total length of the video that this + /// DurationRange is a segment form. It returns the percentage that [start] is + /// through the entire video. + /// + /// For example, assume that the entire video is 4 minutes long. If [end] has a + /// duration of two minutes, this will return `0.5` since the DurationRange + /// ends 50% of the way through the video's total length. + double endFraction(Duration duration) { + return end.inMilliseconds / duration.inMilliseconds; + } + + @override + String toString() => + '${objectRuntimeType(this, 'DurationRange')}(start: $start, end: $end)'; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DurationRange && + runtimeType == other.runtimeType && + start == other.start && + end == other.end; + + @override + int get hashCode => start.hashCode ^ end.hashCode; +} + +/// [VideoPlayerOptions] can be optionally used to set additional player settings +@immutable +class VideoPlayerOptions { + /// set additional optional player settings + // TODO(stuartmorgan): Temporarily suppress warnings about not using const + // in all of the other video player packages, fix this, and then update + // the other packages to use const. + // ignore: prefer_const_constructors_in_immutables + VideoPlayerOptions({ + this.mixWithOthers = false, + this.allowBackgroundPlayback = false, + }); + + /// Set this to true to keep playing video in background, when app goes in background. + /// The default value is false. + final bool allowBackgroundPlayback; + + /// Set this to true to mix the video players audio with other audio sources. + /// The default value is false + /// + /// Note: This option will be silently ignored in the web platform (there is + /// currently no way to implement this feature in this platform). + final bool mixWithOthers; +} diff --git a/packages/video_player_avplay/pigeons/messages.dart b/packages/video_player_avplay/pigeons/messages.dart new file mode 100644 index 000000000..4dbd9e026 --- /dev/null +++ b/packages/video_player_avplay/pigeons/messages.dart @@ -0,0 +1,104 @@ +// Copyright 2013 The Flutter 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 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + dartOut: 'lib/src/messages.g.dart', + cppHeaderOut: 'tizen/src/messages.h', + cppSourceOut: 'tizen/src/messages.cc', +)) +class PlayerMessage { + PlayerMessage(this.playerId); + int playerId; +} + +class LoopingMessage { + LoopingMessage(this.playerId, this.isLooping); + int playerId; + bool isLooping; +} + +class VolumeMessage { + VolumeMessage(this.playerId, this.volume); + int playerId; + double volume; +} + +class PlaybackSpeedMessage { + PlaybackSpeedMessage(this.playerId, this.speed); + int playerId; + double speed; +} + +class TrackMessage { + TrackMessage(this.playerId, this.tracks); + int playerId; + List?> tracks; +} + +class TrackTypeMessage { + TrackTypeMessage(this.playerId, this.trackType); + int playerId; + String trackType; +} + +class SelectedTracksMessage { + SelectedTracksMessage(this.playerId, this.trackId, this.trackType); + int playerId; + int trackId; + String trackType; +} + +class PositionMessage { + PositionMessage(this.playerId, this.position); + int playerId; + int position; +} + +class CreateMessage { + CreateMessage(); + String? asset; + String? uri; + String? packageName; + String? formatHint; + Map? httpHeaders; + Map? drmConfigs; + Map? playerOptions; +} + +class MixWithOthersMessage { + MixWithOthersMessage(this.mixWithOthers); + bool mixWithOthers; +} + +class GeometryMessage { + GeometryMessage(this.playerId, this.x, this.y, this.width, this.height); + int playerId; + int x; + int y; + int width; + int height; +} + +@HostApi() +abstract class VideoPlayerAvplayApi { + void initialize(); + PlayerMessage create(CreateMessage msg); + void dispose(PlayerMessage msg); + void setLooping(LoopingMessage msg); + void setVolume(VolumeMessage msg); + void setPlaybackSpeed(PlaybackSpeedMessage msg); + void play(PlayerMessage msg); + bool setDeactivate(PlayerMessage msg); + bool setActivate(PlayerMessage msg); + TrackMessage track(TrackTypeMessage msg); + bool setTrackSelection(SelectedTracksMessage msg); + PositionMessage position(PlayerMessage msg); + @async + void seekTo(PositionMessage msg); + void pause(PlayerMessage msg); + void setMixWithOthers(MixWithOthersMessage msg); + void setDisplayGeometry(GeometryMessage msg); +} diff --git a/packages/video_player_avplay/pubspec.yaml b/packages/video_player_avplay/pubspec.yaml new file mode 100644 index 000000000..c597b505c --- /dev/null +++ b/packages/video_player_avplay/pubspec.yaml @@ -0,0 +1,27 @@ +name: video_player_avplay +description: Flutter plugin for displaying inline video on Tizen TV devices. +homepage: https://github.com/flutter-tizen/plugins +repository: https://github.com/flutter-tizen/plugins/tree/master/packages/video_player_avplay +version: 0.1.0 + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" + +flutter: + plugin: + platforms: + tizen: + pluginClass: VideoPlayerTizenPlugin + fileName: video_player_tizen_plugin.h + +dependencies: + flutter: + sdk: flutter + html: ^0.15.0 + plugin_platform_interface: ^2.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + pigeon: ^10.0.0 diff --git a/packages/video_player_avplay/tizen/.gitignore b/packages/video_player_avplay/tizen/.gitignore new file mode 100644 index 000000000..ae3a59232 --- /dev/null +++ b/packages/video_player_avplay/tizen/.gitignore @@ -0,0 +1,5 @@ +/.cproject +/.sign +/crash-info/ +/Debug/ +/Release/ diff --git a/packages/video_player_avplay/tizen/inc/plusplayer/plusplayer_wrapper.h b/packages/video_player_avplay/tizen/inc/plusplayer/plusplayer_wrapper.h new file mode 100644 index 000000000..eb3118b26 --- /dev/null +++ b/packages/video_player_avplay/tizen/inc/plusplayer/plusplayer_wrapper.h @@ -0,0 +1,538 @@ +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_PLUSPLAYER_WRAPPER_H +#define FLUTTER_PLUGIN_PLUSPLAYER_WRAPPER_H + +#include +#include +#include + +#include +#include +#include +#include + +#define PLUS_PLAYER_EXPORT __attribute__((visibility("default"))) +#define PLUSPLAYER_ERROR_CLASS TIZEN_ERROR_PLAYER | 0x20 +/* This is for custom defined player error. */ +#define PLUSPLAYER_CUSTOM_ERROR_CLASS TIZEN_ERROR_PLAYER | 0x1000 + +namespace plusplayer { + +enum class DisplayType { kNone, kOverlay, kEvas, kMixer }; + +enum class DisplayMode { + kLetterBox, + kOriginSize, + kFullScreen, + kCroppedFull, + kOriginOrLetter, + kDstRoi, + kAutoAspectRatio, + kMax +}; + +enum TrackType { + kTrackTypeAudio = 0, + kTrackTypeVideo, + kTrackTypeSubtitle, + kTrackTypeMax +}; + +enum PlayerType { + kDefault, + kDASH, +}; + +enum class DisplayRotation { kNone, kRotate90, kRotate180, kRotate270 }; + +enum class State { + kNone, /**< Player is created, but not opened */ + kIdle, /**< Player is opened, but not prepared or player is stopped */ + kTypeFinderReady, /**< TypeFinder prepared */ + kTrackSourceReady, /**< TrackSource prepared */ + kReady, /**< Player is ready to play(start) */ + kPlaying, /**< Player is playing media */ + kPaused /**< Player is paused while playing media */ +}; + +struct Geometry { + int x = 0, y = 0; + int w = 1920, h = 1080; +}; + +struct PlayerMemento { + uint64_t playing_time = 0; /**< Playing time of current player */ + State state = State::kNone; /**< Player status of current player */ + DisplayMode display_mode = + DisplayMode::kMax; /**< Display mode of current player */ + DisplayType display_type = + DisplayType::kNone; /**< Display type of current player */ + Geometry display_area; /**< Display area of current player */ + std::map + buffer_config; /**< Buffer config of current player */ + bool is_live = false; /**< Live status of current player */ + double current_playback_rate = 1.0; /**< Playback rate of current player */ +}; + +enum class ErrorType { + kNone = TIZEN_ERROR_NONE, /**< Successful */ + kOutOfMemory = TIZEN_ERROR_OUT_OF_MEMORY, /**< Out of memory */ + kInvalidParameter = TIZEN_ERROR_INVALID_PARAMETER, /**< Invalid parameter */ + kNoSuchFile = TIZEN_ERROR_NO_SUCH_FILE, /**< No such file or directory */ + kInvalidOperation = TIZEN_ERROR_INVALID_OPERATION, /**< Invalid operation */ + kFileNoSpaceOnDevice = + TIZEN_ERROR_FILE_NO_SPACE_ON_DEVICE, /**< No space left on the device */ + kFeatureNotSupportedOnDevice = + TIZEN_ERROR_NOT_SUPPORTED, /**< Not supported */ + kSeekFailed = PLUSPLAYER_ERROR_CLASS | 0x01, /**< Seek operation failure */ + kInvalidState = PLUSPLAYER_ERROR_CLASS | 0x02, /**< Invalid state */ + kNotSupportedFile = + PLUSPLAYER_ERROR_CLASS | 0x03, /**< File format not supported */ + kInvalidUri = PLUSPLAYER_ERROR_CLASS | 0x04, /**< Invalid URI */ + kSoundPolicy = PLUSPLAYER_ERROR_CLASS | 0x05, /**< Sound policy error */ + kConnectionFailed = + PLUSPLAYER_ERROR_CLASS | 0x06, /**< Streaming connection failed */ + kVideoCaptureFailed = + PLUSPLAYER_ERROR_CLASS | 0x07, /**< Video capture failed */ + kDrmExpired = PLUSPLAYER_ERROR_CLASS | 0x08, /**< Expired license */ + kDrmNoLicense = PLUSPLAYER_ERROR_CLASS | 0x09, /**< No license */ + kDrmFutureUse = PLUSPLAYER_ERROR_CLASS | 0x0a, /**< License for future use */ + kDrmNotPermitted = PLUSPLAYER_ERROR_CLASS | 0x0b, /**< Format not permitted */ + kResourceLimit = PLUSPLAYER_ERROR_CLASS | 0x0c, /**< Resource limit */ + kPermissionDenied = TIZEN_ERROR_PERMISSION_DENIED, /**< Permission denied */ + kServiceDisconnected = + PLUSPLAYER_ERROR_CLASS | 0x0d, /**< Socket connection lost (Since 3.0) */ + kBufferSpace = + TIZEN_ERROR_BUFFER_SPACE, /**< No buffer space available (Since 3.0)*/ + kNotSupportedAudioCodec = + PLUSPLAYER_ERROR_CLASS | + 0x0e, // < Not supported audio codec but video can be played (Since 4.0) + kNotSupportedVideoCodec = + PLUSPLAYER_ERROR_CLASS | + 0x0f, //< Not supported video codec but audio can be played (Since 4.0) + kNotSupportedSubtitle = + PLUSPLAYER_ERROR_CLASS | + 0x10, /**< Not supported subtitle format (Since 4.0) */ + + // TODO(euna7.ko) Can be removed. refer to + // http://168.219.243.246:8090/pages/viewpage.action?pageId=27269511 + kDrmInfo = + PLUSPLAYER_CUSTOM_ERROR_CLASS | 0x05, /**< playready drm error info */ + kNotSupportedFormat = PLUSPLAYER_CUSTOM_ERROR_CLASS | 0x08, + kStreamingPlayer = PLUSPLAYER_CUSTOM_ERROR_CLASS | 0x09, + kDtcpFsk = PLUSPLAYER_CUSTOM_ERROR_CLASS | 0x0a, + kPreLoadingTimeOut = PLUSPLAYER_CUSTOM_ERROR_CLASS | + 0x0b, /**< can't finish preloading in time*/ + kNetworkError = + PLUSPLAYER_CUSTOM_ERROR_CLASS | 0x0c, /**< for network error */ + kChannelSurfingFailed = + PLUSPLAYER_CUSTOM_ERROR_CLASS | 0x0d, /**< for channel surfing error */ + + kUnknown +}; + +enum class StreamingMessageType { + kNone = 0, + // kResolutionChanged, + // kAdEnd, + // kAdStart, + // kRenderDone, + kBitrateChange, + // kFragmentInfo, + kSparseTrackDetect, + // kStreamingEvent, + // kDrmChallengeData, + kDrmInitData, + // kHttpErrorCode, + // kDrmRenewSessionData, + kStreamEventType, + kStreamEventData, + kStreamSyncFlush, + kStreamMrsUrlChanged, + kDrmKeyRotation, + kFragmentDownloadInfo, + kDvrLiveLag, + kSparseTrackData, + kConnectionRetry, + kConfigLowLatency, + kCurlErrorDebugInfo +}; +enum class SourceType { + kNone, + kBase, + kHttp, + kHls, + kDash, + kSmooth, + kFile, + kExternalSubtitle, + kNotFound, + kMax +}; + +enum class ContentFormat { + kNone, + kMP4Mov, + kMpegts, + k3GpMov, + kAudioMpeg, + kAudioMpegAac, + kMkv, + kAvi, + kVideoAsf, + kAppXid3, + kAudioOgg, + kAudioFlac, + kFlv, + kVideoMpeg, + kUnknown +}; + +enum class DecodedVideoFrameBufferType { + kNone, + kCopy, + kReference, + kScale, + kManualCopy, +}; +enum class RscType { kVideoRenderer }; + +struct MessageParam { + std::string data; + int size = 0; + int code = 0; // Error or warning code +}; +struct PlayerAppInfo { + std::string id; /**< App id */ + std::string version; /**< App version */ + std::string type; /**< App type. ex)"MSE", "HTML5", etc.. */ +}; + +const int kInvalidTrackIndex = -1; + +struct Track { + int index = kInvalidTrackIndex; + int id = 0; + std::string mimetype; + std::string streamtype; + std::string container_type; + TrackType type = kTrackTypeMax; + std::shared_ptr codec_data; + unsigned int codec_tag = 0; + int codec_data_len = 0; + int width = 0; + int height = 0; + int maxwidth = 0; + int maxheight = 0; + int framerate_num = 0; + int framerate_den = 0; + int sample_rate = 0; + int sample_format = 0; + int channels = 0; + int version = 0; + int layer = 0; + int bits_per_sample = 0; + int block_align = 0; + int bitrate = 0; + int endianness = 1234; // little endian : 1234 others big endian + bool is_signed = false; + bool active = false; + bool use_swdecoder = false; + std::string language_code; + std::string subtitle_format; + Track() {} + Track(int _index, int _id, std::string _mimetype, std::string _streamtype, + std::string _container_type, TrackType _type, + std::shared_ptr _codec_data, unsigned int _codec_tag, + int _codec_data_len, int _width, int _height, int _maxwidth, + int _maxheight, int _framerate_num, int _framerate_den, + int _sample_rate, int _sample_format, int _channels, int _version, + int _layer, int _bits_per_sample, int _block_align, int _bitrate, + int _endianness, bool _is_signed, bool _active, bool _use_swdecoder, + std::string _language_code, std::string _subtitle_format) + : index(_index), + id(_id), + mimetype(_mimetype), + streamtype(_streamtype), + container_type(_container_type), + type(_type), + codec_data(_codec_data), + codec_tag(_codec_tag), + codec_data_len(_codec_data_len), + width(_width), + height(_height), + maxwidth(_maxwidth), + maxheight(_maxheight), + framerate_num(_framerate_num), + framerate_den(_framerate_den), + sample_rate(_sample_rate), + sample_format(_sample_format), + channels(_channels), + version(_version), + layer(_layer), + bits_per_sample(_bits_per_sample), + block_align(_block_align), + bitrate(_bitrate), + endianness(_endianness), + is_signed(_is_signed), + active(_active), + use_swdecoder(_use_swdecoder), + language_code(_language_code), + subtitle_format(_subtitle_format) {} +}; + +namespace drm { +using LicenseAcquiredCb = void*; +using UserData = void*; +using DrmHandle = int; + +enum class Type { + kNone = 0, + kPlayready, + kMarlin, + kVerimatrix, + kWidevineClassic, + kSecuremedia, + kSdrm, + kWidevineCdm = 8, + kMax +}; + +struct Property { + Type type = Type::kNone; // Drm type + DrmHandle handle = 0; // Drm handle + bool external_decryption = false; // External Decryption Mode + LicenseAcquiredCb license_acquired_cb = + nullptr; // The cb will be invoked when license was acquired. + UserData license_acquired_userdata = + nullptr; // The userdata will be sent by license_acquired_cb +}; +} // namespace drm + +enum class SubtitleType { kText, kPicture, kInvalid }; + +} // namespace plusplayer + +#if defined(__cplusplus) +extern "C" { +#endif + +typedef void (*OnPlayerPrepared)(bool ret, void* user_data); +typedef void (*OnPlayerSeekCompleted)(void* user_data); +typedef void (*OnPlayerResourceConflicted)(void* user_data); +typedef void (*OnPlayerBuffering)(int percent, void* user_data); +typedef void (*OnPlayerCompleted)(void* user_data); +typedef void (*OnPlayerPlaying)(void* user_data); +typedef void (*OnPlayerError)(const plusplayer::ErrorType& error_code, + void* user_data); +typedef void (*OnPlayerErrorMessage)(const plusplayer::ErrorType& error_code, + const char* error_msg, void* user_data); +typedef void (*OnPlayerAdaptiveStreamingControl)( + const plusplayer::StreamingMessageType& type, + const plusplayer::MessageParam& msg, void* user_data); +typedef void (*OnPlayerDrmInitData)(int* drmhandle, unsigned int len, + unsigned char* psshdata, + plusplayer::TrackType type, + void* user_data); +typedef void (*OnPlayerClosedCaptionData)(std::unique_ptr data, + const int size, void* user_data); +typedef void (*OnPlayerCueEvent)(const char* CueData, void* userdata); +typedef void (*OnPlayerDateRangeEvent)(const char* DateRangeData, + void* user_data); +typedef void (*OnPlayerStopReachEvent)(bool StopReach, void* user_data); +typedef void (*OnPlayerCueOutContEvent)(const char* CueOutContData, + void* user_data); +typedef void (*OnPlayerChangeSourceDone)(bool ret, void* user_data); +typedef void (*OnPlayerStateChangedToPlaying)(void* user_data); +typedef void (*OnPlayerDrmType)(plusplayer::drm::Type drmtype, void* user_data); +typedef void (*OnPlayerSubtitleData)(char* data, const int size, + const plusplayer::SubtitleType& type, + const uint64_t duration, void* user_data); + +struct PlusplayerListener { + OnPlayerPrepared prepared_callback{nullptr}; + OnPlayerSeekCompleted seek_completed_callback{nullptr}; + OnPlayerResourceConflicted resource_conflicted_callback{nullptr}; + OnPlayerBuffering buffering_callback{nullptr}; + OnPlayerCompleted completed_callback{nullptr}; + OnPlayerPlaying playing_callback{nullptr}; + OnPlayerError error_callback{nullptr}; + OnPlayerErrorMessage error_message_callback{nullptr}; + OnPlayerAdaptiveStreamingControl adaptive_streaming_control_callback{nullptr}; + OnPlayerDrmInitData drm_init_data_callback{nullptr}; + OnPlayerClosedCaptionData closed_caption_data_callback{nullptr}; + OnPlayerCueEvent cue_event_callback{nullptr}; + OnPlayerDateRangeEvent data_range_event_callback{nullptr}; + OnPlayerStopReachEvent stop_reach_event_callback{nullptr}; + OnPlayerCueOutContEvent cue_out_cont_event_callback{nullptr}; + OnPlayerChangeSourceDone change_source_done_callback{nullptr}; + OnPlayerStateChangedToPlaying state_changed_to_playing_callback{nullptr}; + OnPlayerDrmType drm_type_callback{nullptr}; + OnPlayerSubtitleData subtitle_data_callback{nullptr}; +}; + +struct Plusplayer; +typedef struct Plusplayer* PlusplayerRef; + +PLUS_PLAYER_EXPORT PlusplayerRef CreatePlayer(plusplayer::PlayerType type); + +PLUS_PLAYER_EXPORT bool Activate(PlusplayerRef player, + const plusplayer::TrackType type); + +PLUS_PLAYER_EXPORT bool Deactivate(PlusplayerRef player, + const plusplayer::TrackType type); + +PLUS_PLAYER_EXPORT bool SetVolume(PlusplayerRef player, int volume); + +PLUS_PLAYER_EXPORT bool Open(PlusplayerRef player, const std::string& uri); + +PLUS_PLAYER_EXPORT void SetAppId(PlusplayerRef player, + const std::string& app_id); + +PLUS_PLAYER_EXPORT void SetPrebufferMode(PlusplayerRef player, + bool is_prebuffer_mode); + +PLUS_PLAYER_EXPORT void SetAppInfo(PlusplayerRef player, + const plusplayer::PlayerAppInfo& app_info); + +PLUS_PLAYER_EXPORT bool StopSource(PlusplayerRef player); + +PLUS_PLAYER_EXPORT bool ChangeSource( + PlusplayerRef player, const std::string& uri, + const plusplayer::SourceType source_type, + const plusplayer::ContentFormat format_type, + const uint64_t time_milliseconds, const bool is_seamless); + +PLUS_PLAYER_EXPORT bool Prepare(PlusplayerRef player); + +PLUS_PLAYER_EXPORT bool PrepareAsync(PlusplayerRef player); + +PLUS_PLAYER_EXPORT bool Start(PlusplayerRef player); + +PLUS_PLAYER_EXPORT bool Stop(PlusplayerRef player); + +PLUS_PLAYER_EXPORT void SetDrm(PlusplayerRef player, + const plusplayer::drm::Property& property); + +PLUS_PLAYER_EXPORT bool Pause(PlusplayerRef player); + +PLUS_PLAYER_EXPORT bool Resume(PlusplayerRef player); + +PLUS_PLAYER_EXPORT bool Close(PlusplayerRef player); + +PLUS_PLAYER_EXPORT bool Seek(PlusplayerRef player, + const uint64_t time_millisecond); + +PLUS_PLAYER_EXPORT void SetStopPosition(PlusplayerRef player, + const uint64_t time_millisecond); + +PLUS_PLAYER_EXPORT bool Suspend(PlusplayerRef player); + +PLUS_PLAYER_EXPORT bool Restore(PlusplayerRef player, plusplayer::State state); + +PLUS_PLAYER_EXPORT bool GetMemento(PlusplayerRef player, + plusplayer::PlayerMemento* memento); + +PLUS_PLAYER_EXPORT bool SetDisplay(PlusplayerRef player, + const plusplayer::DisplayType& type, + const uint32_t serface_id, const int x, + const int y, const int w, const int h); + +PLUS_PLAYER_EXPORT bool SetDisplayMode(PlusplayerRef player, + const plusplayer::DisplayMode& mode); + +PLUS_PLAYER_EXPORT bool SetDisplayRoi(PlusplayerRef player, + const plusplayer::Geometry& roi); + +PLUS_PLAYER_EXPORT bool SetDisplayRotate( + PlusplayerRef player, const plusplayer::DisplayRotation& rotate); + +PLUS_PLAYER_EXPORT bool GetDisplayRotate(PlusplayerRef player, + plusplayer::DisplayRotation* rotate); + +PLUS_PLAYER_EXPORT bool GetDisplayRotationSupport(PlusplayerRef player, + bool& can_rotate); + +PLUS_PLAYER_EXPORT bool IsRotatableDevice(PlusplayerRef player); + +PLUS_PLAYER_EXPORT bool SetDisplayVisible(PlusplayerRef player, + bool is_visible); + +PLUS_PLAYER_EXPORT bool SetAudioMute(PlusplayerRef player, bool is_mute); + +PLUS_PLAYER_EXPORT bool SetBufferConfig( + PlusplayerRef player, const std::pair& config); + +PLUS_PLAYER_EXPORT plusplayer::State GetState(PlusplayerRef player); + +PLUS_PLAYER_EXPORT std::string GetTrackLanguageCode(PlusplayerRef player, + plusplayer::TrackType type, + int index); + +PLUS_PLAYER_EXPORT int GetTrackCount(PlusplayerRef player, + plusplayer::TrackType type); + +PLUS_PLAYER_EXPORT std::vector GetTrackInfo( + PlusplayerRef player); + +PLUS_PLAYER_EXPORT std::vector GetActiveTrackInfo( + PlusplayerRef player); + +PLUS_PLAYER_EXPORT bool GetDuration(PlusplayerRef player, + int64_t* duration_in_milliseconds); + +PLUS_PLAYER_EXPORT bool GetPlayingTime(PlusplayerRef player, + uint64_t* time_in_milliseconds); + +PLUS_PLAYER_EXPORT bool SetSilentSubtitle(PlusplayerRef player, bool onoff); + +PLUS_PLAYER_EXPORT bool SetPlaybackRate(PlusplayerRef player, + const double speed); + +PLUS_PLAYER_EXPORT bool SetPlaybackRateBySeek(PlusplayerRef player, + const double rate); + +PLUS_PLAYER_EXPORT void DestroyPlayer(PlusplayerRef player); + +PLUS_PLAYER_EXPORT void DrmLicenseAcquiredDone(PlusplayerRef player, + plusplayer::TrackType type); + +PLUS_PLAYER_EXPORT void SetStreamingProperty(PlusplayerRef player, + const std::string& type, + const std::string& value); + +PLUS_PLAYER_EXPORT std::string GetStreamingProperty(PlusplayerRef player, + const std::string& type); + +PLUS_PLAYER_EXPORT bool SelectTrack(PlusplayerRef player, + plusplayer::TrackType type, int index); + +PLUS_PLAYER_EXPORT bool SetSubtitlePath(PlusplayerRef player, + const std::string& path); + +PLUS_PLAYER_EXPORT bool SetPreferredLanguage( + PlusplayerRef player, plusplayer::TrackType type, + const std::string& primary_language, const std::string& secondary_language, + const std::string& tertiary_language); + +PLUS_PLAYER_EXPORT void SetVideoFrameBufferType( + PlusplayerRef player, const plusplayer::DecodedVideoFrameBufferType type); + +PLUS_PLAYER_EXPORT bool GetVirtualRscId(PlusplayerRef player, + const plusplayer::RscType type, + int* virtual_id); + +PLUS_PLAYER_EXPORT void RegisterListener(PlusplayerRef player, + PlusplayerListener* listener, + void* user_data); + +PLUS_PLAYER_EXPORT void UnregisterListener(PlusplayerRef player); +}; +#if defined(__cplusplus) +// extern "C" +#endif + +#endif // FLUTTER_PLUGIN_PLUSPLAYER_WRAPPER_H diff --git a/packages/video_player_avplay/tizen/inc/video_player_tizen_plugin.h b/packages/video_player_avplay/tizen/inc/video_player_tizen_plugin.h new file mode 100644 index 000000000..ab68b2f19 --- /dev/null +++ b/packages/video_player_avplay/tizen/inc/video_player_tizen_plugin.h @@ -0,0 +1,25 @@ +#ifndef FLUTTER_PLUGIN_VIDEO_PLAYER_TIZEN_PLUGIN_H_ +#define FLUTTER_PLUGIN_VIDEO_PLAYER_TIZEN_PLUGIN_H_ + +#include + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default"))) +#else +#define FLUTTER_PLUGIN_EXPORT +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void VideoPlayerTizenPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_VIDEO_PLAYER_TIZEN_PLUGIN_H_ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.0/libavcodec_common.so b/packages/video_player_avplay/tizen/lib/armel/6.0/libavcodec_common.so new file mode 100755 index 000000000..6fd9b93f9 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.0/libavcodec_common.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.0/libavformat_mmdash.so b/packages/video_player_avplay/tizen/lib/armel/6.0/libavformat_mmdash.so new file mode 100755 index 000000000..bcbf1fd5b Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.0/libavformat_mmdash.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.0/libavformat_mmhls.so b/packages/video_player_avplay/tizen/lib/armel/6.0/libavformat_mmhls.so new file mode 100755 index 000000000..225724a67 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.0/libavformat_mmhls.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.0/libavformat_mmhttp.so b/packages/video_player_avplay/tizen/lib/armel/6.0/libavformat_mmhttp.so new file mode 100755 index 000000000..52cf8ac75 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.0/libavformat_mmhttp.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.0/libavutil_common.so b/packages/video_player_avplay/tizen/lib/armel/6.0/libavutil_common.so new file mode 100755 index 000000000..d5a730da4 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.0/libavutil_common.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.0/libdash.so b/packages/video_player_avplay/tizen/lib/armel/6.0/libdash.so new file mode 100755 index 000000000..efd201a5a Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.0/libdash.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.0/libdashplusplayer_tvplus.so b/packages/video_player_avplay/tizen/lib/armel/6.0/libdashplusplayer_tvplus.so new file mode 100755 index 000000000..01c5e073d Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.0/libdashplusplayer_tvplus.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.0/libgstdash.so b/packages/video_player_avplay/tizen/lib/armel/6.0/libgstdash.so new file mode 100755 index 000000000..b8fe44f39 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.0/libgstdash.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.0/libgsthls.so b/packages/video_player_avplay/tizen/lib/armel/6.0/libgsthls.so new file mode 100755 index 000000000..fcdab55d3 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.0/libgsthls.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.0/libgsthttpdemux.so b/packages/video_player_avplay/tizen/lib/armel/6.0/libgsthttpdemux.so new file mode 100755 index 000000000..8099c3243 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.0/libgsthttpdemux.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.0/libgstmmhttpsrc.so b/packages/video_player_avplay/tizen/lib/armel/6.0/libgstmmhttpsrc.so new file mode 100755 index 000000000..f679bb322 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.0/libgstmmhttpsrc.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.0/libgstsubtitle_tvplus.so b/packages/video_player_avplay/tizen/lib/armel/6.0/libgstsubtitle_tvplus.so new file mode 100755 index 000000000..e2ae14608 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.0/libgstsubtitle_tvplus.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.0/libgstsubtitleparse_tvplus.so b/packages/video_player_avplay/tizen/lib/armel/6.0/libgstsubtitleparse_tvplus.so new file mode 100755 index 000000000..582435230 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.0/libgstsubtitleparse_tvplus.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.0/libhls.so b/packages/video_player_avplay/tizen/lib/armel/6.0/libhls.so new file mode 100755 index 000000000..ee311b9fd Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.0/libhls.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.0/libplusplayer-wrapper.so b/packages/video_player_avplay/tizen/lib/armel/6.0/libplusplayer-wrapper.so new file mode 100755 index 000000000..d3b2fbd69 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.0/libplusplayer-wrapper.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.0/libplusplayer_tvplus.so b/packages/video_player_avplay/tizen/lib/armel/6.0/libplusplayer_tvplus.so new file mode 100755 index 000000000..7ad45ff94 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.0/libplusplayer_tvplus.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.0/libplusplayercore_tvplus.so b/packages/video_player_avplay/tizen/lib/armel/6.0/libplusplayercore_tvplus.so new file mode 100755 index 000000000..13fed566d Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.0/libplusplayercore_tvplus.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.0/libtracksource_tvplus.so b/packages/video_player_avplay/tizen/lib/armel/6.0/libtracksource_tvplus.so new file mode 100755 index 000000000..209bca6f5 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.0/libtracksource_tvplus.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.5/libavcodec_common.so b/packages/video_player_avplay/tizen/lib/armel/6.5/libavcodec_common.so new file mode 100755 index 000000000..6fd9b93f9 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.5/libavcodec_common.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.5/libavformat_mmdash.so b/packages/video_player_avplay/tizen/lib/armel/6.5/libavformat_mmdash.so new file mode 100755 index 000000000..bcbf1fd5b Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.5/libavformat_mmdash.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.5/libavformat_mmhls.so b/packages/video_player_avplay/tizen/lib/armel/6.5/libavformat_mmhls.so new file mode 100755 index 000000000..225724a67 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.5/libavformat_mmhls.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.5/libavformat_mmhttp.so b/packages/video_player_avplay/tizen/lib/armel/6.5/libavformat_mmhttp.so new file mode 100755 index 000000000..52cf8ac75 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.5/libavformat_mmhttp.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.5/libavutil_common.so b/packages/video_player_avplay/tizen/lib/armel/6.5/libavutil_common.so new file mode 100755 index 000000000..5d291f4e5 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.5/libavutil_common.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.5/libdash.so b/packages/video_player_avplay/tizen/lib/armel/6.5/libdash.so new file mode 100755 index 000000000..7bf96391d Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.5/libdash.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.5/libdashplusplayer_tvplus.so b/packages/video_player_avplay/tizen/lib/armel/6.5/libdashplusplayer_tvplus.so new file mode 100755 index 000000000..02683cae1 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.5/libdashplusplayer_tvplus.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.5/libgstdash.so b/packages/video_player_avplay/tizen/lib/armel/6.5/libgstdash.so new file mode 100755 index 000000000..121349d90 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.5/libgstdash.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.5/libgsthls.so b/packages/video_player_avplay/tizen/lib/armel/6.5/libgsthls.so new file mode 100755 index 000000000..3758d4a89 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.5/libgsthls.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.5/libgsthttpdemux.so b/packages/video_player_avplay/tizen/lib/armel/6.5/libgsthttpdemux.so new file mode 100755 index 000000000..8099c3243 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.5/libgsthttpdemux.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.5/libgstmmhttpsrc.so b/packages/video_player_avplay/tizen/lib/armel/6.5/libgstmmhttpsrc.so new file mode 100755 index 000000000..bf97b0d63 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.5/libgstmmhttpsrc.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.5/libgstsubtitle_tvplus.so b/packages/video_player_avplay/tizen/lib/armel/6.5/libgstsubtitle_tvplus.so new file mode 100755 index 000000000..e2ae14608 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.5/libgstsubtitle_tvplus.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.5/libgstsubtitleparse_tvplus.so b/packages/video_player_avplay/tizen/lib/armel/6.5/libgstsubtitleparse_tvplus.so new file mode 100755 index 000000000..582435230 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.5/libgstsubtitleparse_tvplus.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.5/libhls.so b/packages/video_player_avplay/tizen/lib/armel/6.5/libhls.so new file mode 100755 index 000000000..54d4326bd Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.5/libhls.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.5/libplusplayer-wrapper.so b/packages/video_player_avplay/tizen/lib/armel/6.5/libplusplayer-wrapper.so new file mode 100755 index 000000000..ab21b64f2 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.5/libplusplayer-wrapper.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.5/libplusplayer_tvplus.so b/packages/video_player_avplay/tizen/lib/armel/6.5/libplusplayer_tvplus.so new file mode 100755 index 000000000..4bc3beb0e Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.5/libplusplayer_tvplus.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.5/libplusplayercore_tvplus.so b/packages/video_player_avplay/tizen/lib/armel/6.5/libplusplayercore_tvplus.so new file mode 100755 index 000000000..3b6f0bdac Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.5/libplusplayercore_tvplus.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/6.5/libtracksource_tvplus.so b/packages/video_player_avplay/tizen/lib/armel/6.5/libtracksource_tvplus.so new file mode 100755 index 000000000..41afdc034 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/6.5/libtracksource_tvplus.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/7.0/libavcodec_common.so b/packages/video_player_avplay/tizen/lib/armel/7.0/libavcodec_common.so new file mode 100755 index 000000000..a834c5c39 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/7.0/libavcodec_common.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/7.0/libavformat_mmdash.so b/packages/video_player_avplay/tizen/lib/armel/7.0/libavformat_mmdash.so new file mode 100755 index 000000000..62b45ffe4 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/7.0/libavformat_mmdash.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/7.0/libavformat_mmhls.so b/packages/video_player_avplay/tizen/lib/armel/7.0/libavformat_mmhls.so new file mode 100755 index 000000000..29e3a2b4f Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/7.0/libavformat_mmhls.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/7.0/libavformat_mmhttp.so b/packages/video_player_avplay/tizen/lib/armel/7.0/libavformat_mmhttp.so new file mode 100755 index 000000000..b142e8a47 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/7.0/libavformat_mmhttp.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/7.0/libavutil_common.so b/packages/video_player_avplay/tizen/lib/armel/7.0/libavutil_common.so new file mode 100755 index 000000000..dc39bbfdb Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/7.0/libavutil_common.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/7.0/libdash.so b/packages/video_player_avplay/tizen/lib/armel/7.0/libdash.so new file mode 100755 index 000000000..0d4488e43 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/7.0/libdash.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/7.0/libdashplusplayer_tvplus.so b/packages/video_player_avplay/tizen/lib/armel/7.0/libdashplusplayer_tvplus.so new file mode 100755 index 000000000..e573ea1fe Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/7.0/libdashplusplayer_tvplus.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/7.0/libgstdash.so b/packages/video_player_avplay/tizen/lib/armel/7.0/libgstdash.so new file mode 100755 index 000000000..caa9c744d Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/7.0/libgstdash.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/7.0/libgsthls.so b/packages/video_player_avplay/tizen/lib/armel/7.0/libgsthls.so new file mode 100755 index 000000000..692bec5e8 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/7.0/libgsthls.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/7.0/libgsthttpdemux.so b/packages/video_player_avplay/tizen/lib/armel/7.0/libgsthttpdemux.so new file mode 100755 index 000000000..72a8eb942 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/7.0/libgsthttpdemux.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/7.0/libgstmmhttpsrc.so b/packages/video_player_avplay/tizen/lib/armel/7.0/libgstmmhttpsrc.so new file mode 100755 index 000000000..958fe82b2 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/7.0/libgstmmhttpsrc.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/7.0/libgstsubtitle_tvplus.so b/packages/video_player_avplay/tizen/lib/armel/7.0/libgstsubtitle_tvplus.so new file mode 100755 index 000000000..d7fa06a8d Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/7.0/libgstsubtitle_tvplus.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/7.0/libgstsubtitleparse_tvplus.so b/packages/video_player_avplay/tizen/lib/armel/7.0/libgstsubtitleparse_tvplus.so new file mode 100755 index 000000000..6f33f75ff Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/7.0/libgstsubtitleparse_tvplus.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/7.0/libhls.so b/packages/video_player_avplay/tizen/lib/armel/7.0/libhls.so new file mode 100755 index 000000000..18ffdea38 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/7.0/libhls.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/7.0/libplusplayer-wrapper.so b/packages/video_player_avplay/tizen/lib/armel/7.0/libplusplayer-wrapper.so new file mode 100755 index 000000000..a1c5eba5a Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/7.0/libplusplayer-wrapper.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/7.0/libplusplayer_tvplus.so b/packages/video_player_avplay/tizen/lib/armel/7.0/libplusplayer_tvplus.so new file mode 100755 index 000000000..a8cf484d4 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/7.0/libplusplayer_tvplus.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/7.0/libplusplayercore_tvplus.so b/packages/video_player_avplay/tizen/lib/armel/7.0/libplusplayercore_tvplus.so new file mode 100755 index 000000000..cd191425a Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/7.0/libplusplayercore_tvplus.so differ diff --git a/packages/video_player_avplay/tizen/lib/armel/7.0/libtracksource_tvplus.so b/packages/video_player_avplay/tizen/lib/armel/7.0/libtracksource_tvplus.so new file mode 100755 index 000000000..795ed0425 Binary files /dev/null and b/packages/video_player_avplay/tizen/lib/armel/7.0/libtracksource_tvplus.so differ diff --git a/packages/video_player_avplay/tizen/project_def.prop b/packages/video_player_avplay/tizen/project_def.prop new file mode 100644 index 000000000..49d181531 --- /dev/null +++ b/packages/video_player_avplay/tizen/project_def.prop @@ -0,0 +1,29 @@ +# See https://docs.tizen.org/application/tizen-studio/native-tools/project-conversion +# for details. + +APPNAME = video_player_tizen_plugin +type = sharedLib +profile = common-5.5 + +# Source files +USER_SRCS += src/*.cc + +# User defines +USER_DEFS = +USER_UNDEFS = +USER_CPP_DEFS = FLUTTER_PLUGIN_IMPL +USER_CPP_UNDEFS = + +# Compiler flags +USER_CFLAGS_MISC = +USER_CPPFLAGS_MISC = + +# User includes +USER_INC_DIRS = inc src inc/plusplayer +USER_INC_FILES = +USER_CPP_INC_FILES = + +# Linker options +USER_LIB_DIRS = lib/${BUILD_ARCH}/${API_VERSION} +USER_LFLAGS = -Wl,-rpath='$$ORIGIN' +USER_LIBS = plusplayer-wrapper gstsubtitle_tvplus gstsubtitleparse_tvplus plusplayercore_tvplus tracksource_tvplus plusplayer_tvplus dashplusplayer_tvplus diff --git a/packages/video_player_avplay/tizen/res/dash_default_settings.jsonx b/packages/video_player_avplay/tizen/res/dash_default_settings.jsonx new file mode 100644 index 000000000..41f35863b --- /dev/null +++ b/packages/video_player_avplay/tizen/res/dash_default_settings.jsonx @@ -0,0 +1,33 @@ +{ + "debug": { + "curl_debug": false + }, + "streaming": { + "multiQueueDuration": 0, + "engineBufferDurationMs": 0, + "maxConnectionTimeout": 10, + "MaxDownloadRetryCount": 10, + "L1ServerTimeUs": -1, + "lowLatencyEnabled": false, + "SwitchMode": "NORMAL_SENSITIVITY", + "DmgrInputBufferSize": 5242880, + "maxTransferVideoBufferByte": 10485760, + "maxTransferAudioBufferByte": 5242880, + "noSwitchForStartbitrateHighestUs": 10000000, + "abr": { + "rule_name": "abrDynamic", + "bandwidthSafetyFactor": 0.9, + "video_overhead_factor": 1.1 + }, + "bw_checker": { + "rule_name": "AVG" + }, + "advertisement": { + "SCTE35": false + } + }, + "accessibility": { + "audio_description": false + } +} + diff --git a/packages/video_player_avplay/tizen/src/drm_license_helper.cc b/packages/video_player_avplay/tizen/src/drm_license_helper.cc new file mode 100644 index 000000000..b4517cf70 --- /dev/null +++ b/packages/video_player_avplay/tizen/src/drm_license_helper.cc @@ -0,0 +1,804 @@ +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "drm_license_helper.h" + +#include +#include +#include +#include +#include + +#include "log.h" + +#define DEFAULT_USER_AGENT_PLAYREADY "User-Agent: PlayReadyClient" +#define DEFAULT_USER_AGENT_WIDEVINE "User-Agent: Widevine CDM v1.0" +#define HTTP_HEADER_PLAYREADY_LICGET \ + "Content-Type: text/xml; charset=utf-8\r\nSOAPAction: " \ + "\"http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense\"" +#define HTTP_HEADER_WIDEVINE_LICGET "Content-Type: application/octet-stream" + +#define INFO(...) LOG_INFO(__VA_ARGS__) +#define INFO_CURL_HEADERS(headers) \ + { \ + INFO("REQ Headers: BEGIN"); \ + struct curl_slist* p; \ + for (p = headers; p != nullptr; p = p->next) { \ + INFO("%s", p->data); \ + } \ + INFO("REQ Headers: END"); \ + } + +#define CHECK_CURL_FAIL(expr) \ + { \ + if (expr != CURLE_OK) { \ + INFO("Error %d ", __LINE__); \ + goto ErrorExit; \ + } \ + } + +#define MAX_POPUP_MESSAGE_LENGTH (1024) +#define ERROR_TITLE_LICENSE_FETCHING_FAILURE "Fetching License Failed" + +namespace { + +struct SDynamicBuf { + unsigned char* data; + size_t size; + size_t allocated; +}; + +struct SHttpSession { + void* curl_handle; + unsigned char* post_data; // request body + size_t post_data_len; // length of request body + DrmLicenseHelper::DrmType type; + size_t send_data_len; // length of send already + SDynamicBuf header; // response header + SDynamicBuf body; // response body + long res_code; +}; + +// Internal Static Functions +static size_t ReceiveHeader(void* ptr, size_t size, size_t nmemb, void* stream); +static size_t ReceiveBody(void* ptr, size_t size, size_t nmemb, void* stream); +static size_t SendBody(void* ptr, size_t size, size_t nmemb, void* stream); +static bool AppendData(SDynamicBuf* append_buf, const void* append_data, + size_t append_size); +static char* GetRedirectLocation(const char* headers, bool support_https); +static struct curl_slist* CurlSlistAppend(struct curl_slist* list, + const char* append_string); +static DRM_RESULT ComposePostDataTZ(SHttpSession* http_session, + const char* post_data, int post_data_len, + const char* soap_header); +static struct curl_slist* SetHttpHeader(CURL* http_curl, + DrmLicenseHelper::DrmType type, + const char* http_cookie, + const char* http_header, + const char* http_user_agent); +static SHttpSession* HttpOpen(void); +static int CbCurlProgress(void* ptr, double total_to_download, + double now_downloaded, double total_to_upload, + double now_uploaded); +static DRM_RESULT HttpStartTransaction( + SHttpSession* http_session, const char* http_url, const void* post_data, + unsigned post_data_len, DrmLicenseHelper::DrmType type, + const char* http_cookie, const char* http_soap_header, + const char* http_header, const char* http_user_agent, + bool* http_cancel_request); +static void HttpClose(SHttpSession* http_session); + +bool AppendData(SDynamicBuf* buffer, const void* append_data, + size_t append_size) { + size_t new_size = buffer->size + append_size; + if (buffer->allocated < new_size) { + new_size += 1024; + unsigned char* buf = + static_cast(realloc(buffer->data, new_size)); + if (!buf) { + LOG_ERROR("[DrmLicenseHelper] AppendData: realloc fail"); + return false; + } + buffer->data = buf; + buffer->allocated = new_size; + LOG_DEBUG( + "[DrmLicenseHelper] AppendData: realloc append_size(%d), size(%d) " + "buffer->allocated(%d)", + append_size, buffer->size, buffer->allocated); + } + memcpy(buffer->data + buffer->size, append_data, append_size); + buffer->size += append_size; + + return true; +} + +char* GetRedirectLocation(const char* headers, bool support_https) { + if (!headers) { + return nullptr; + } + + // Get the header's location value. + const char* location = strcasestr(headers, "Location"); + if (!location) { + return nullptr; + } + const char* ptr = location + strlen("Location"); + + while (*ptr == ':') { + ptr++; + } + while (*ptr == ' ') { + ptr++; + } + + unsigned i = 0; + while (ptr[i] && (ptr[i] != ' ') && (ptr[i] != '\n') && (ptr[i] != '\r')) { + i++; + } + + if (support_https) { + // [soyoung] get redirection location + // for using https itself + char* ret = static_cast(malloc(i + 1)); + if (!ret) { + return nullptr; + } + memcpy(ret, ptr, i); + ret[i] = 0; + return ret; + } else { + // Convert Redirection Location from https to http + // [soyoung] + // Redirect location from https to http + // If the petition URL contains "https," the client may use SSL for the + // connection. (For non-SSL transport, remove the "s" in "https" from the + // URL.) If SSL is used, the client should check the server's certificate to + // ensure it is current, matches the domain, and is properly signed by a + // trusted authority. + int len = i; + const char* p = ptr + 4; + const char http_str[6] = "http\0"; + if (i < 7) { + return nullptr; // wrong location, no space even for http:// + } + + if (strncasecmp(ptr, "https", 5) == 0) { + len--; + p++; + } + + char* ret = static_cast(malloc(len + 1)); + if (!ret) { + return nullptr; + } + + memcpy(ret, http_str, 4); + memcpy(ret + 4, p, len - 4); + ret[len] = 0; + return ret; + } +} + +struct curl_slist* CurlSlistAppend(struct curl_slist* list, + const char* append_string) { + if (!list) { + return nullptr; + } + + struct curl_slist* new_list = curl_slist_append(list, append_string); + if (!new_list) { // allocation failed + curl_slist_free_all(list); + } + + return new_list; +} + +DRM_RESULT ComposePostDataTZ(SHttpSession* http_session, const char* post_data, + int post_data_len, const char* soap_header) { + DRM_RESULT drm_result = DRM_SUCCESS; + const char* pointer; + char* dest; + int dest_len; + int remain; + + free(http_session->post_data); + http_session->post_data = nullptr; + http_session->post_data_len = 0; + + int soap_header_len = soap_header ? strlen(soap_header) : 0; + + dest_len = post_data_len; + + if (soap_header_len > 0) { + dest_len += soap_header_len + sizeof("\r\n\r"); + } + + http_session->post_data = static_cast(malloc(dest_len + 1)); + if (http_session->post_data == nullptr) { + LOG_ERROR("[DrmLicenseHelper] Failed to alloc post data."); + return DRM_E_POINTER; + } + dest = reinterpret_cast(http_session->post_data); + remain = post_data_len; + + if (soap_header_len > 0) { + /* append to the last in an existing soap header */ + pointer = strstr(post_data, ""); + if (pointer > post_data && pointer < post_data + remain) { + int header_len = pointer - post_data; + memcpy(dest, post_data, header_len); + dest += header_len; + dest_len -= header_len; + remain -= header_len; + + memcpy(dest, soap_header, soap_header_len); + dest += soap_header_len; + if (*dest == '\0') { + dest--; + } + } else { + /* insert soap header in front of soap body */ + pointer = strstr(post_data, ""); + if (pointer > post_data && pointer < post_data + remain) { + int header_len = pointer - post_data; + memcpy(dest, post_data, header_len); + dest += header_len; + dest_len -= header_len; + remain -= header_len; + *dest = '\0'; + strncat(dest, "", dest_len); + header_len = strlen(dest); + dest += header_len; + dest_len -= header_len; + + memcpy(dest, soap_header, soap_header_len); + header_len = soap_header_len; + dest += header_len; + dest_len -= header_len; + + *dest = '\0'; + strncat(dest, "", dest_len); + header_len = strlen(dest); + dest += header_len; + dest_len -= header_len; + } else { + /* not a SOAP message */ + pointer = post_data; + } + } + } else { + pointer = post_data; + } + + memcpy(dest, pointer, remain); + dest += remain; + *dest = '\0'; + + http_session->post_data_len = + dest - reinterpret_cast(http_session->post_data); + if (soap_header_len > 0) { + LOG_INFO("[DrmLicenseHelper] [soap header added %d] %s", + http_session->post_data_len, http_session->post_data); + } + + return drm_result; +} + +struct curl_slist* SetHttpHeader(CURL* http_curl, + DrmLicenseHelper::DrmType type, + const char* http_cookie, + const char* http_header, + const char* http_user_agent) { + const char* user_agent = nullptr; + const char* header = nullptr; + + switch (type) { + case DrmLicenseHelper::kPlayReady: + user_agent = DEFAULT_USER_AGENT_PLAYREADY; + header = HTTP_HEADER_PLAYREADY_LICGET; + break; + case DrmLicenseHelper::kWidevine: + user_agent = DEFAULT_USER_AGENT_WIDEVINE; + header = HTTP_HEADER_WIDEVINE_LICGET; + break; + default: + LOG_ERROR("[DrmLicenseHelper] Invalid DRM Type"); + return nullptr; + } + + struct curl_slist* headers = nullptr; + if (http_user_agent) { + const char* user_agent_prefix = "User-Agent: "; + unsigned prefix_len = strlen(user_agent_prefix); + unsigned user_agent_len = strlen(http_user_agent); + + char* user_agent_string = + static_cast(malloc(prefix_len + user_agent_len + 1)); + if (nullptr == user_agent_string) { + LOG_ERROR("[DrmLicenseHelper] Memory allocation failed."); + return nullptr; + } + + memcpy(user_agent_string, user_agent_prefix, prefix_len); + memcpy(user_agent_string + prefix_len, http_user_agent, user_agent_len); + user_agent_string[prefix_len + user_agent_len] = 0; + LOG_INFO( + "[DrmLicenseHelper] SetHttpHeader: user-agent added to header --- (%s)", + user_agent_string); + + headers = curl_slist_append(nullptr, user_agent_string); + free(user_agent_string); + } else { + headers = curl_slist_append(nullptr, user_agent); + } + + if (nullptr == headers) { + LOG_ERROR("[DrmLicenseHelper] UserAgent attach failed."); + return nullptr; + } + + LOG_DEBUG( + "[DrmLicenseHelper] SetHttpHeader: type(%d), http_cookie(%s), " + "http_header(%s)", + type, http_cookie, http_header); + + headers = CurlSlistAppend(headers, header); + + if (http_cookie) { + const char* cookie_prefix = "Cookie: "; + unsigned prefix_len = strlen(cookie_prefix); + unsigned cookie_len = strlen(http_cookie); + + char* cookie = static_cast(malloc(prefix_len + cookie_len + 1)); + + if (cookie) { + memcpy(cookie, cookie_prefix, prefix_len); + memcpy(cookie + prefix_len, http_cookie, cookie_len); + cookie[prefix_len + cookie_len] = '\0'; + + headers = CurlSlistAppend(headers, cookie); + + LOG_INFO( + "[DrmLicenseHelper] SetHttpHeader: cookie added to header --- (%s)", + cookie); + + free(cookie); + } + } + + if (http_header) { + LOG_INFO( + "[DrmLicenseHelper] SetHttpHeader: HttpHeader added to header --- (%s)", + http_header); + headers = CurlSlistAppend(headers, http_header); + } + + if (headers) { + curl_easy_setopt(http_curl, CURLOPT_HTTPHEADER, headers); + } + + return headers; +} + +static SHttpSession* HttpOpen(void) { + SHttpSession* http_session = nullptr; + + CURL* http_curl = curl_easy_init(); + if (http_curl) { + http_session = static_cast(malloc(sizeof(SHttpSession))); + if (http_session) { + memset(http_session, 0, sizeof(SHttpSession)); + http_session->curl_handle = http_curl; + return http_session; + } + curl_easy_cleanup(http_curl); + } + LOG_ERROR( + "[DrmLicenseHelper] Can't create CURL object, curl_global_init missed"); + return nullptr; +} + +int CbCurlProgress(void* ptr, double total_to_download, double now_downloaded, + double total_to_upload, double now_uploaded) { + bool* http_cancel_request = static_cast(ptr); + + if (http_cancel_request) { + LOG_INFO("[DrmLicenseHelper] http_cancel_request: (%d)", + *http_cancel_request); + + if (*http_cancel_request) { + LOG_INFO("[DrmLicenseHelper] curl works canceled."); + return 1; + } + } + + return 0; +} + +DRM_RESULT HttpStartTransaction( + SHttpSession* http_session, const char* http_url, const void* post_data, + unsigned post_data_len, DrmLicenseHelper::DrmType type, + const char* http_cookie, const char* http_soap_header, + const char* http_header, const char* http_user_agent, + bool* http_cancel_request) { + CURLcode res = CURLE_OK; + struct curl_slist* headers = nullptr; + CURL* http_curl = http_session->curl_handle; + + // 1. Set Post Data + http_session->post_data_len = post_data_len; + http_session->send_data_len = 0; + http_session->body.size = 0; + http_session->header.size = 0; + + LOG_INFO("[DrmLicenseHelper] HttpStartTransaction: type(%d)", type); + if (http_url) { + LOG_INFO("[DrmLicenseHelper] http_url: %s", http_url); + } + + // 2. Set Header type + http_session->type = type; + headers = + SetHttpHeader(http_curl, type, http_cookie, http_header, http_user_agent); + if (!headers) { + LOG_ERROR("[DrmLicenseHelper] Failed to set HTTP header."); + return DRM_E_NETWORK_HEADER; + } + + curl_easy_setopt(http_curl, CURLOPT_VERBOSE, 0L); + + // Check + curl_easy_setopt(http_curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); + + int soap_flag = 0; + + if (post_data && post_data_len > 0) { + if (http_soap_header != nullptr) { + DRM_RESULT drm_result = + ComposePostDataTZ(http_session, static_cast(post_data), + post_data_len, http_soap_header); + if (drm_result != DRM_SUCCESS) { + LOG_ERROR( + "[DrmLicenseHelper] Failed to compose post data, drm_result: 0x%lx", + drm_result); + return drm_result; + } else if (drm_result == DRM_SUCCESS) { + soap_flag = 1; + } + } + + res = curl_easy_setopt(http_curl, CURLOPT_POST, 1L); + CHECK_CURL_FAIL(res); + + if (soap_flag == 0) { + if (!(http_session->post_data = + static_cast(malloc(post_data_len)))) { + if (headers != nullptr) { + curl_slist_free_all(headers); + } + LOG_ERROR("[DrmLicenseHelper] Failed to alloc post data."); + return DRM_E_POINTER; + } + + if (http_session->post_data) { + memcpy(http_session->post_data, post_data, post_data_len); + http_session->post_data_len = post_data_len; + } + } + + res = curl_easy_setopt(http_curl, CURLOPT_READFUNCTION, SendBody); + CHECK_CURL_FAIL(res); + + res = curl_easy_setopt(http_curl, CURLOPT_POSTFIELDSIZE, + http_session->post_data_len); + CHECK_CURL_FAIL(res); + + res = curl_easy_setopt(http_curl, CURLOPT_READDATA, http_session); + CHECK_CURL_FAIL(res); + } else { + curl_easy_setopt(http_curl, CURLOPT_HTTPGET, 1L); + } + + curl_easy_setopt(http_curl, CURLOPT_USE_SSL, 1L); + curl_easy_setopt(http_curl, CURLOPT_SSL_VERIFYPEER, 1L); // 0L + curl_easy_setopt(http_curl, CURLOPT_SSL_VERIFYHOST, 2L); // 0L + + // set timeout 10 seconds + curl_easy_setopt(http_curl, CURLOPT_TIMEOUT, 10); + + res = curl_easy_setopt(http_curl, CURLOPT_URL, http_url); + CHECK_CURL_FAIL(res); + + res = curl_easy_setopt(http_curl, CURLOPT_NOPROGRESS, 0L); + CHECK_CURL_FAIL(res); + res = curl_easy_setopt(http_curl, CURLOPT_PROGRESSFUNCTION, CbCurlProgress); + CHECK_CURL_FAIL(res); + res = curl_easy_setopt(http_curl, CURLOPT_PROGRESSDATA, http_cancel_request); + CHECK_CURL_FAIL(res); + + res = curl_easy_setopt(http_curl, CURLOPT_HEADERFUNCTION, ReceiveHeader); + CHECK_CURL_FAIL(res); + + res = curl_easy_setopt(http_curl, CURLOPT_BUFFERSIZE, 1024L * 20L); + CHECK_CURL_FAIL(res); + + res = curl_easy_setopt(http_curl, CURLOPT_WRITEFUNCTION, ReceiveBody); + CHECK_CURL_FAIL(res); + + res = curl_easy_setopt(http_curl, CURLOPT_WRITEHEADER, http_session); + CHECK_CURL_FAIL(res); + + res = curl_easy_setopt(http_curl, CURLOPT_WRITEDATA, http_session); + CHECK_CURL_FAIL(res); + + res = curl_easy_setopt(http_curl, CURLOPT_NOSIGNAL, 1); + CHECK_CURL_FAIL(res); + + res = curl_easy_perform(http_curl); + + if (res == CURLE_OK) { + LOG_INFO("[DrmLicenseHelper] after curl_easy_perform: res(%d)", res); + curl_easy_getinfo(http_curl, CURLINFO_RESPONSE_CODE, + &http_session->res_code); + LOG_INFO( + "[DrmLicenseHelper] after curl_easy_perform: " + "http_session->res_code(%ld)", + http_session->res_code); + } + // Secure Clock Petition Server returns wrong size .. + else if (res == CURLE_PARTIAL_FILE) { + LOG_INFO("[DrmLicenseHelper] after curl_easy_perform: res(%d)", res); + curl_easy_getinfo(http_curl, CURLINFO_RESPONSE_CODE, + &http_session->res_code); + LOG_INFO( + "[DrmLicenseHelper] after curl_easy_perform: " + "http_session->res_code(%ld)", + http_session->res_code); + res = CURLE_OK; + } else if (res == CURLE_SEND_ERROR) { + LOG_INFO("[DrmLicenseHelper] after curl_easy_perform: res(%d)", res); + curl_easy_getinfo(http_curl, CURLINFO_RESPONSE_CODE, + &http_session->res_code); + LOG_INFO( + "[DrmLicenseHelper] after curl_easy_perform: " + "http_session->res_code(%ld)", + http_session->res_code); + res = CURLE_OK; + } else { + LOG_INFO("[DrmLicenseHelper] after curl_easy_perform: res(%d)", res); + curl_easy_getinfo(http_curl, CURLINFO_RESPONSE_CODE, + &http_session->res_code); + LOG_INFO( + "[DrmLicenseHelper] after curl_easy_perform: " + "http_session->res_code(%ld)", + http_session->res_code); + if (res == CURLE_OPERATION_TIMEDOUT) { + LOG_INFO("[DrmLicenseHelper] CURLE_OPERATION_TIMEDOUT occurred"); + } + + if (headers != nullptr) { + curl_slist_free_all(headers); + } + + if (res == CURLE_OUT_OF_MEMORY) { + LOG_ERROR("[DrmLicenseHelper] Failed to alloc from curl."); + return DRM_E_POINTER; + } else if (res == CURLE_ABORTED_BY_CALLBACK) { + *http_cancel_request = false; + LOG_ERROR("[DrmLicenseHelper] Network job canceled by caller."); + return DRM_E_NETWORK_CANCELED; + } else { + LOG_ERROR("[DrmLicenseHelper] Failed from curl, curl message: %s", + curl_easy_strerror(res)); + return DRM_E_NETWORK_CURL; + } + } + +ErrorExit: + if (headers != nullptr) { + INFO_CURL_HEADERS(headers); + curl_slist_free_all(headers); + } + + if (res != CURLE_OK) { + if (res == CURLE_OUT_OF_MEMORY) { + LOG_ERROR("[DrmLicenseHelper] Failed to alloc from curl."); + return DRM_E_POINTER; + } else { + LOG_ERROR("[DrmLicenseHelper] Failed from curl, curl message: %s", + curl_easy_strerror(res)); + return DRM_E_NETWORK_CURL; + } + } + + return DRM_SUCCESS; +} + +void HttpClose(SHttpSession* http_session) { + if (!http_session) { + return; + } + + if (http_session->curl_handle != nullptr) { + curl_easy_cleanup(http_session->curl_handle); + } + + if (http_session->post_data) { + free(http_session->post_data); + } + + if (http_session->body.data) { + free(http_session->body.data); + } + + if (http_session->header.data) { + free(http_session->header.data); + } + + free(http_session); +} + +size_t ReceiveHeader(void* ptr, size_t size, size_t nmemb, void* stream) { + LOG_DEBUG("[DrmLicenseHelper] size:%d nmemb:%d", size, nmemb); + + size_t data_size = size * nmemb; + + if (data_size > 0) { + SHttpSession* http_session = reinterpret_cast(stream); + + if (!AppendData(&http_session->header, ptr, data_size)) { + return 0; + } + } + return data_size; +} + +size_t ReceiveBody(void* ptr, size_t size, size_t nmemb, void* stream) { + LOG_DEBUG("[DrmLicenseHelper] size:%d nmemb:%d", size, nmemb); + + size_t data_size = size * nmemb; + + if (data_size > 0) { + SHttpSession* http_session = reinterpret_cast(stream); + + if (!AppendData(&http_session->body, ptr, data_size)) { + return 0; + } + } + return data_size; +} + +size_t SendBody(void* ptr, size_t size, size_t nmemb, void* stream) { + LOG_DEBUG("[DrmLicenseHelper] size:%d nmemb:%d", size, nmemb); + + SHttpSession* http_session = reinterpret_cast(stream); + + size_t avail_data = http_session->post_data_len - http_session->send_data_len; + size_t can_send = size * nmemb; + + if (avail_data == 0) { + return 0; + } + + if (can_send > avail_data) { + can_send = avail_data; + } + + memcpy(ptr, http_session->post_data + http_session->send_data_len, can_send); + http_session->send_data_len += can_send; + + return can_send; +} + +} // namespace + +DRM_RESULT DrmLicenseHelper::DoTransactionTZ( + const char* http_server_url, const void* challenge, + unsigned long challenge_len, unsigned char** response, + unsigned long* response_len, DrmType type, const char* http_cookie, + SExtensionCtxTZ* http_ext_ctx) { + *response = nullptr; + *response_len = 0; + + const char* http_url = http_server_url; + SHttpSession* http_session; + char* redirect_url = nullptr; + + DRM_RESULT drm_result = DRM_SUCCESS; + + // Redirection 3 times.. + for (int i = 0; i < 3; i++) { + if (!(http_session = HttpOpen())) { + LOG_ERROR("[DrmLicenseHelper] Failed to open HTTP session."); + break; + } + + char* soap_header = nullptr; + char* http_header = nullptr; + char* user_agent = nullptr; + bool* cancel_request = nullptr; + + if (http_ext_ctx != nullptr) { + if (http_ext_ctx->http_soap_header) { + soap_header = http_ext_ctx->http_soap_header; + } + + if (http_ext_ctx->http_header) { + http_header = http_ext_ctx->http_header; + } + + if (http_ext_ctx->http_user_agent) { + user_agent = http_ext_ctx->http_user_agent; + } + + cancel_request = &(http_ext_ctx->cancel_request); + } + + drm_result = HttpStartTransaction( + http_session, http_url, challenge, challenge_len, type, http_cookie, + soap_header, http_header, user_agent, cancel_request); + if (drm_result != DRM_SUCCESS) { + LOG_ERROR( + "[DrmLicenseHelper] Failed on network transaction(%d/%d), " + "drm_result: 0x%lx", + i + 1, 3, drm_result); + break; + } + + if (http_session->res_code == 301 || http_session->res_code == 302) { + // Convert https to http for GETSECURECLOCKSERVER_URL + redirect_url = GetRedirectLocation( + reinterpret_cast(http_session->header.data), true); + + HttpClose(http_session); + http_session = nullptr; + if (!redirect_url) { + LOG_ERROR("[DrmLicenseHelper] Failed to get redirect URL"); + break; + } + http_url = redirect_url; + } else { + if (http_session->res_code != 200) { + LOG_ERROR( + "[DrmLicenseHelper] Server returns response Code %ld [%s][%d]", + http_session->res_code, http_session->body.data, + http_session->body.size); + + if (http_session->res_code >= 400 && http_session->res_code < 500) { + drm_result = DRM_E_NETWORK_CLIENT; + } else if (http_session->res_code >= 500 && + http_session->res_code < 600) { + drm_result = DRM_E_NETWORK_SERVER; + } else { + drm_result = DRM_E_NETWORK; + } + break; + } + + *response = http_session->body.data; + *response_len = http_session->body.size; + + http_session->body.data = nullptr; + http_session->body.size = 0; + http_session->body.allocated = 0; + drm_result = DRM_SUCCESS; + break; + } + } + + if (redirect_url) { + free(redirect_url); + redirect_url = nullptr; + } + + HttpClose(http_session); + + if (drm_result != DRM_SUCCESS) { + LOG_ERROR( + "[DrmLicenseHelper] Failed on network transaction, drm_result: 0x%lx", + drm_result); + } + + return drm_result; +} diff --git a/packages/video_player_avplay/tizen/src/drm_license_helper.h b/packages/video_player_avplay/tizen/src/drm_license_helper.h new file mode 100644 index 000000000..9701c6438 --- /dev/null +++ b/packages/video_player_avplay/tizen/src/drm_license_helper.h @@ -0,0 +1,47 @@ +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_DRM_LICENSE_HELPER_H_ +#define FLUTTER_PLUGIN_DRM_LICENSE_HELPER_H_ + +typedef long DRM_RESULT; + +const DRM_RESULT DRM_SUCCESS = 0x00000000L; +const DRM_RESULT DRM_E_POINTER = 0x80004003L; +const DRM_RESULT DRM_E_INVALIDARG = 0x80070057L; +const DRM_RESULT DRM_E_NETWORK = 0x91000000L; +const DRM_RESULT DRM_E_NETWORK_CURL = 0x91000001L; +const DRM_RESULT DRM_E_NETWORK_HOST = 0x91000002L; +const DRM_RESULT DRM_E_NETWORK_CLIENT = 0x91000003L; +const DRM_RESULT DRM_E_NETWORK_SERVER = 0x91000004L; +const DRM_RESULT DRM_E_NETWORK_HEADER = 0x91000005L; +const DRM_RESULT DRM_E_NETWORK_REQUEST = 0x91000006L; +const DRM_RESULT DRM_E_NETWORK_RESPONSE = 0x91000007L; +const DRM_RESULT DRM_E_NETWORK_CANCELED = 0x91000008L; + +class DrmLicenseHelper { + public: + enum DrmType { + kNone = 0, + kPlayReady, + kWidevine, + }; + + struct SExtensionCtxTZ { + char* http_soap_header = nullptr; + char* http_header = nullptr; + char* http_user_agent = nullptr; + bool cancel_request = false; + }; + + static DRM_RESULT DoTransactionTZ(const char* http_server_url, + const void* challenge, + unsigned long challenge_len, + unsigned char** response, + unsigned long* response_len, DrmType type, + const char* http_cookie, + SExtensionCtxTZ* http_ext_ctx); +}; + +#endif // FLUTTER_PLUGIN_DRM_LICENSE_HELPER_H_ diff --git a/packages/video_player_avplay/tizen/src/drm_manager.cc b/packages/video_player_avplay/tizen/src/drm_manager.cc new file mode 100644 index 000000000..ddad2b170 --- /dev/null +++ b/packages/video_player_avplay/tizen/src/drm_manager.cc @@ -0,0 +1,328 @@ +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "drm_manager.h" + +#include +#include + +#include "drm_license_helper.h" +#include "drm_manager_proxy.h" +#include "log.h" + +static std::string GetDrmSubType(int drm_type) { + switch (drm_type) { + case DrmManager::DRM_TYPE_PLAYREADAY: + return "com.microsoft.playready"; + case DrmManager::DRM_TYPE_WIDEVINECDM: + default: + return "com.widevine.alpha"; + } +} + +DrmManager::DrmManager() : drm_type_(DM_TYPE_NONE) { + drm_manager_proxy_ = OpenDrmManagerProxy(); + if (drm_manager_proxy_) { + int ret = InitDrmManagerProxy(drm_manager_proxy_); + if (ret != DM_ERROR_NONE) { + LOG_ERROR("[DrmManager] Fail to initialize DRM manager: %s", + get_error_message(ret)); + CloseDrmManagerProxy(drm_manager_proxy_); + drm_manager_proxy_ = nullptr; + } + } else { + LOG_ERROR("[DrmManager] Fail to dlopen libdrmmanager."); + } +} + +DrmManager::~DrmManager() { + ReleaseDrmSession(); + + if (drm_manager_proxy_) { + CloseDrmManagerProxy(drm_manager_proxy_); + drm_manager_proxy_ = nullptr; + } +} + +bool DrmManager::CreateDrmSession(int drm_type, bool local_mode) { + if (!drm_manager_proxy_) { + LOG_ERROR("[DrmManager] Invalid handle of libdrmmanager."); + return false; + } + + if (local_mode) { + DMGRSetDRMLocalMode(); + } + + drm_type_ = drm_type; + std::string sub_type = GetDrmSubType(drm_type); + LOG_INFO("[DrmManager] drm type is %s", sub_type.c_str()); + drm_session_ = DMGRCreateDRMSession(DM_TYPE_EME, sub_type.c_str()); + if (!drm_session_) { + LOG_ERROR("[DrmManager] Fail to create drm session."); + return false; + } + LOG_INFO("[DrmManager] Drm session is created, drm_session: %p", + drm_session_); + + SetDataParam_t configure_param = {}; + configure_param.param1 = reinterpret_cast(OnDrmManagerError); + configure_param.param2 = drm_session_; + int ret = DMGRSetData(drm_session_, "error_event_callback", &configure_param); + if (ret != DM_ERROR_NONE) { + LOG_ERROR( + "[DrmManager] Fail to set error_event_callback to drm session: %s", + get_error_message(ret)); + ReleaseDrmSession(); + return false; + } + + return true; +} + +bool DrmManager::SetChallenge(const std::string &media_url, + flutter::BinaryMessenger *binary_messenger) { + request_license_channel_ = + std::make_unique>( + binary_messenger, "dev.flutter.videoplayer.drm", + &flutter::StandardMethodCodec::GetInstance()); + return DM_ERROR_NONE == SetChallenge(media_url); +} + +bool DrmManager::SetChallenge(const std::string &media_url, + const std::string &license_server_url) { + license_server_url_ = license_server_url; + return DM_ERROR_NONE == SetChallenge(media_url); +} + +void DrmManager::ReleaseDrmSession() { + if (source_id_ > 0) { + g_source_remove(source_id_); + } + source_id_ = 0; + + if (drm_session_) { + int ret = 0; + if (initialized_) { + ret = DMGRSetData(drm_session_, "Finalize", nullptr); + if (ret == DM_ERROR_NONE) { + initialized_ = false; + } else { + LOG_ERROR("[DrmManager] Fail to set finalize to drm session: %s", + get_error_message(ret)); + } + } + ret = DMGRReleaseDRMSession(drm_session_); + if (ret == DM_ERROR_NONE) { + drm_session_ = nullptr; + } else { + LOG_ERROR("[DrmManager] Fail to release drm session: %s", + get_error_message(ret)); + } + } +} + +bool DrmManager::GetDrmHandle(int *handle) { + if (drm_session_) { + *handle = 0; + int ret = DMGRGetData(drm_session_, "drm_handle", handle); + if (ret != DM_ERROR_NONE) { + LOG_ERROR("[DrmManager] Fail to get drm_handle from drm session: %s", + get_error_message(ret)); + return false; + } + LOG_INFO("[DrmManager] Get drm handle: %d", *handle); + return true; + } else { + LOG_ERROR("[DrmManager] Invalid drm session."); + return false; + } +} + +int DrmManager::UpdatePsshData(const void *data, int length) { + if (!drm_session_) { + LOG_ERROR("[DrmManager] Invalid drm session."); + return DM_ERROR_INVALID_SESSION; + } + + SetDataParam_t pssh_data_param = {}; + pssh_data_param.param1 = const_cast(data); + pssh_data_param.param2 = reinterpret_cast(length); + int ret = DMGRSetData(drm_session_, "update_pssh_data", &pssh_data_param); + if (DM_ERROR_NONE != ret) { + LOG_ERROR("[DrmManager] Fail to set update_pssh_data to drm session: %s", + get_error_message(ret)); + } + return ret; +} + +bool DrmManager::SecurityInitCompleteCB(int *drm_handle, unsigned int len, + unsigned char *pssh_data, + void *user_data) { + // IMPORTANT: SetDataParam_t cannot be stack allocated because + // DMGRSecurityInitCompleteCB is called multiple times during video playback + // and the parameter should always be available. + SetDataParam_t security_param = {}; + if (user_data) { + security_param.param1 = user_data; + } + security_param.param2 = drm_session_; + + return DMGRSecurityInitCompleteCB(drm_handle, len, pssh_data, + &security_param); +} + +int DrmManager::SetChallenge(const std::string &media_url) { + if (!drm_session_) { + LOG_ERROR("[DrmManager] Invalid drm session."); + return DM_ERROR_INVALID_SESSION; + } + + SetDataParam_t challenge_data_param = {}; + challenge_data_param.param1 = reinterpret_cast(OnChallengeData); + challenge_data_param.param2 = this; + int ret = DMGRSetData(drm_session_, "eme_request_key_callback", + &challenge_data_param); + if (ret != DM_ERROR_NONE) { + LOG_ERROR( + "[DrmManager] Fail to set eme_request_key_callback to drm session: " + "%s", + get_error_message(ret)); + return ret; + } + + ret = DMGRSetData(drm_session_, "set_playready_manifest", + static_cast(const_cast(media_url.c_str()))); + if (ret != DM_ERROR_NONE) { + LOG_ERROR( + "[DrmManager] Fail to set set_playready_manifest to drm session: %s", + get_error_message(ret)); + return ret; + } + + ret = DMGRSetData(drm_session_, "Initialize", nullptr); + if (ret != DM_ERROR_NONE) { + LOG_ERROR("[DrmManager] Fail to set initialize to drm session: %s", + get_error_message(ret)); + return ret; + } + initialized_ = true; + return ret; +} + +int DrmManager::OnChallengeData(void *session_id, int message_type, + void *message, int message_length, + void *user_data) { + LOG_INFO("[DrmManager] challenge data: %s, challenge length: %d", message, + message_length); + + DrmManager *self = static_cast(user_data); + LOG_INFO("[DrmManager] drm_type: %d, license server: %s", self->drm_type_, + self->license_server_url_.c_str()); + DataForLicenseProcess *data = + new DataForLicenseProcess(session_id, message, message_length); + data->user_data = self; + self->source_id_ = g_idle_add(ProcessLicense, data); + if (self->source_id_ <= 0) { + LOG_ERROR("[DrmManager] Fail to add g_idle."); + delete data; + return DM_ERROR_INTERNAL_ERROR; + } + return DM_ERROR_NONE; +} + +void DrmManager::OnDrmManagerError(long error_code, char *error_message, + void *user_data) { + LOG_ERROR("[DrmManager] DRM manager had an error: [%ld][%s]", error_code, + error_message); +} + +gboolean DrmManager::ProcessLicense(void *user_data) { + LOG_INFO("[DrmManager] Start process license."); + + DataForLicenseProcess *data = static_cast(user_data); + DrmManager *self = static_cast(data->user_data); + + if (!self->license_server_url_.empty()) { + // Get license via the license server. + unsigned char *response_data = nullptr; + unsigned long response_len = 0; + DRM_RESULT ret = DrmLicenseHelper::DoTransactionTZ( + self->license_server_url_.c_str(), data->message.c_str(), + data->message.size(), &response_data, &response_len, + static_cast(self->drm_type_), nullptr, + nullptr); + if (DRM_SUCCESS != ret || nullptr == response_data || 0 == response_len) { + LOG_ERROR("[DrmManager] Fail to get respone by license server url."); + delete data; + return false; + } + LOG_INFO("[DrmManager] Response length : %d", response_len); + self->InstallKey(const_cast(reinterpret_cast( + data->session_id.c_str())), + static_cast(response_data), + reinterpret_cast(response_len)); + } else if (self->request_license_channel_) { + // Get license via the Dart callback. + self->RequestLicense(data->session_id, data->message); + } else { + LOG_ERROR("[DrmManager] No way to request license."); + } + + delete data; + return false; +} + +void DrmManager::InstallKey(void *session_id, void *response_data, + void *response_len) { + LOG_INFO("[DrmManager] Start install license."); + + SetDataParam_t license_param = {}; + license_param.param1 = session_id; + license_param.param2 = response_data; + license_param.param3 = response_len; + int ret = DMGRSetData(drm_session_, "install_eme_key", &license_param); + if (ret != DM_ERROR_NONE) { + LOG_ERROR("[DrmManager] Fail to install eme key: %s", + get_error_message(ret)); + } +} + +void DrmManager::RequestLicense(std::string &session_id, std::string &message) { + LOG_INFO("[DrmManager] Start request license."); + + if (request_license_channel_ == nullptr) { + LOG_ERROR("[DrmManager] request license channel is null."); + return; + } + + std::vector message_vec(message.begin(), message.end()); + flutter::EncodableMap args_map = { + {flutter::EncodableValue("message"), + flutter::EncodableValue(message_vec)}, + }; + auto result_handler = + std::make_unique>( + + [session_id, this](const flutter::EncodableValue *success_value) { + std::vector response; + if (std::holds_alternative>(*success_value)) { + response = std::get>(*success_value); + } else { + LOG_ERROR("[DrmManager] Fail to get response."); + return; + } + LOG_INFO("[DrmManager] Response length : %d", response.size()); + InstallKey(const_cast( + reinterpret_cast(session_id.c_str())), + reinterpret_cast(response.data()), + reinterpret_cast(response.size())); + }, + nullptr, nullptr); + request_license_channel_->InvokeMethod( + "requestLicense", + std::make_unique( + flutter::EncodableValue(args_map)), + std::move(result_handler)); +} diff --git a/packages/video_player_avplay/tizen/src/drm_manager.h b/packages/video_player_avplay/tizen/src/drm_manager.h new file mode 100644 index 000000000..f1e1b9f96 --- /dev/null +++ b/packages/video_player_avplay/tizen/src/drm_manager.h @@ -0,0 +1,67 @@ +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_DRM_MANAGER_H_ +#define FLUTTER_PLUGIN_DRM_MANAGER_H_ + +#include +#include + +#include "drm_manager_proxy.h" + +class DrmManager { + public: + typedef enum { + DRM_TYPE_NONE, + DRM_TYPE_PLAYREADAY, + DRM_TYPE_WIDEVINECDM, + } DrmType; + + explicit DrmManager(); + ~DrmManager(); + + bool CreateDrmSession(int drm_type, bool local_mode); + bool SetChallenge(const std::string &media_url, + const std::string &license_server_url); + bool SetChallenge(const std::string &media_url, + flutter::BinaryMessenger *binary_messenger); + bool GetDrmHandle(int *handle); + bool SecurityInitCompleteCB(int *drm_handle, unsigned int len, + unsigned char *pssh_data, void *user_data); + int UpdatePsshData(const void *data, int length); + void ReleaseDrmSession(); + + private: + struct DataForLicenseProcess { + DataForLicenseProcess(void *session_id, void *message, int message_length) + : session_id(static_cast(session_id)), + message(static_cast(message), message_length) {} + std::string session_id; + std::string message; + void *user_data; + }; + + void RequestLicense(std::string &session_id, std::string &message); + void InstallKey(void *session_id, void *response_data, void *response_len); + int SetChallenge(const std::string &media_url); + + static int OnChallengeData(void *session_id, int message_type, void *message, + int message_length, void *user_data); + static void OnDrmManagerError(long error_code, char *error_message, + void *user_data); + static gboolean ProcessLicense(void *user_data); + + std::unique_ptr> + request_license_channel_; + + void *drm_session_ = nullptr; + void *drm_manager_proxy_ = nullptr; + + int drm_type_; + std::string license_server_url_; + unsigned int source_id_ = 0; + bool initialized_ = false; +}; + +#endif // FLUTTER_PLUGIN_DRM_MANAGER_H_ diff --git a/packages/video_player_avplay/tizen/src/drm_manager_proxy.cc b/packages/video_player_avplay/tizen/src/drm_manager_proxy.cc new file mode 100644 index 000000000..5a59c9c14 --- /dev/null +++ b/packages/video_player_avplay/tizen/src/drm_manager_proxy.cc @@ -0,0 +1,64 @@ +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "drm_manager_proxy.h" + +#include + +FuncDMGRSetData DMGRSetData = nullptr; +FuncDMGRGetData DMGRGetData = nullptr; +FuncDMGRSetDRMLocalMode DMGRSetDRMLocalMode = nullptr; +FuncDMGRCreateDRMSession DMGRCreateDRMSession = nullptr; +FuncDMGRSecurityInitCompleteCB DMGRSecurityInitCompleteCB = nullptr; +FuncDMGRReleaseDRMSession DMGRReleaseDRMSession = nullptr; + +void* OpenDrmManagerProxy() { return dlopen("libdrmmanager.so.0", RTLD_LAZY); } + +int InitDrmManagerProxy(void* handle) { + if (!handle) { + return DM_ERROR_INVALID_PARAM; + } + + DMGRSetData = reinterpret_cast(dlsym(handle, "DMGRSetData")); + if (!DMGRSetData) { + return DM_ERROR_DL; + } + + DMGRGetData = reinterpret_cast(dlsym(handle, "DMGRGetData")); + if (!DMGRGetData) { + return DM_ERROR_DL; + } + + DMGRSetDRMLocalMode = reinterpret_cast( + dlsym(handle, "DMGRSetDRMLocalMode")); + if (!DMGRSetDRMLocalMode) { + return DM_ERROR_DL; + } + + DMGRCreateDRMSession = reinterpret_cast( + dlsym(handle, "DMGRCreateDRMSession")); + if (!DMGRCreateDRMSession) { + return DM_ERROR_DL; + } + + DMGRSecurityInitCompleteCB = reinterpret_cast( + dlsym(handle, "DMGRSecurityInitCompleteCB")); + if (!DMGRSecurityInitCompleteCB) { + return DM_ERROR_DL; + } + + DMGRReleaseDRMSession = reinterpret_cast( + dlsym(handle, "DMGRReleaseDRMSession")); + if (!DMGRReleaseDRMSession) { + return DM_ERROR_DL; + } + + return DM_ERROR_NONE; +} + +void CloseDrmManagerProxy(void* handle) { + if (handle) { + dlclose(handle); + } +} diff --git a/packages/video_player_avplay/tizen/src/drm_manager_proxy.h b/packages/video_player_avplay/tizen/src/drm_manager_proxy.h new file mode 100644 index 000000000..02261d1b1 --- /dev/null +++ b/packages/video_player_avplay/tizen/src/drm_manager_proxy.h @@ -0,0 +1,121 @@ +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_DRM_MANAGER_PROXY_H_ +#define FLUTTER_PLUGIN_DRM_MANAGER_PROXY_H_ + +typedef enum { + DM_ERROR_NONE = 0, /**< Success */ + DM_ERROR_INVALID_PARAM, /**< Invalid parameter */ + DM_ERROR_INVALID_OPERATE, /**< Invalid operation */ + DM_ERROR_INVALID_HANDLE, /**< Invalid handle */ + DM_ERROR_INTERNAL_ERROR, /**< Internal error */ + DM_ERROR_TIMEOUT, /**< Timeout */ + DM_ERROR_MANIFEST_DOWNLOAD_ERROR, /**< Manifest download error */ + DM_ERROR_MANIFEST_PARSE_ERROR, /**< Manifest parse error */ + DM_ERROR_FIND_NOPSSHDATA, /**< No pssh data */ + + DM_ERROR_MALLOC = 10, /**< Malloc error */ + DM_ERROR_DL, /**< Load so error */ + + DM_ERROR_INVALID_URL = 20, /**< Invalid url */ + DM_ERROR_INVALID_SESSION, /**< Invalid session */ + DM_ERROR_UNSUPPORTED_URL_SUFFIX, /**< Unsupported url suffix */ + DM_ERROR_INITIALIZE_FAILED, /**< Failed to initialize DRM */ + + DM_ERROR_DASH_INIT = 30, /**< DASH init failed */ + DM_ERROR_DASH_CLOSE, /**< DASH close failed */ + DM_ERROR_DASH_OPEN, /**< DASH open failed */ + + DM_ERROR_DRM_WEB_SET = 40, /**< DRM web set failed */ + + DM_ERROR_PR_HANDLE_CREATE = 50, /**< Playready handle create failed */ + DM_ERROR_PR_OPEN, /**< Playready open failed */ + DM_ERROR_PR_DESTROY, /**< Playready destroy failed */ + DM_ERROR_PR_GENCHALLENGE, /**< Playready genchallenge failed */ + DM_ERROR_PR_INSTALL_LICENSE, /**< Playready install license failed */ + DM_ERROR_PR_GETRIGHTS, /**< Playready get rights failed */ + DM_ERROR_PR_STATUS, /**< Playready get status failed */ + + DM_ERROR_VMX_HANDLE_CREATE = 60, /**< Verimatrix handle create failed */ + DM_ERROR_VMX_FINALIZE, /**< Verimatrix finalize failed */ + DM_ERROR_VMX_GET_UNIQUE_ID, /**< Verimatrix get unique ID failed */ + + DM_ERROR_MARLIN_OPEN = 70, /**< Marlin open failed */ + DM_ERROR_MARLIN_CLOSE, /**< Marlin close failed */ + DM_ERROR_MARLIN_GET_RIGHTS, /**< Marlin get rights failed */ + DM_ERROR_MARLIN_GET_LICENSE, /**< Marlin get license failed */ + + DM_ERROR_WVCDM_HANDLE_CREATE = 80, /**< Widevinecdm handle create failed */ + DM_ERROR_WVCDM_DESTROY, /**< Widevinecdm destroy failed */ + DM_ERROR_WVCDM_OPEN_SESSION, /**< Widevinecdm open failed */ + DM_ERROR_WVCDM_CLOSE_SESSION, /**< Widevinecdm close failed */ + DM_ERROR_WVCDM_GET_PROVISION, /**< Widevinecdm get provision failed */ + DM_ERROR_WVCDM_GENERATE_KEYREQUEST, /**< Widevinecdm generate key request + failed */ + DM_ERROR_WVCDM_ADD_KEY, /**< Widevinecdm add key failed */ + DM_ERROR_WVCDM_REGISTER_EVENT, /**< Widevinecdm register event failed */ + + DM_ERROR_EME_SESSION_HANDLE_CREATE = 90, /**< EME handle create failed */ + DM_ERROR_EME_SESSION_CREATE, /**< EME session create failed */ + DM_ERROR_EME_SESSION_DESTROY, /**< EME session destroy failed */ + DM_ERROR_EME_SESSION_UPDATE, /**< EME session update failed */ + DM_ERROR_EME_SESSION_REQUEST, /**< EME session request failed */ + DM_ERROR_EME_WEB_OPERATION, /**< EME web operation failed */ + DM_ERROR_EME_TYPE_NOT_SUPPORTED, /**< EME type not supported */ + //... + DM_ERROR_UNKOWN, +} dm_error_e; + +typedef enum { + DM_TYPE_NONE = 0, /**< None */ + DM_TYPE_PLAYREADY = 1, /**< Playready */ + DM_TYPE_MARLINMS3 = 2, /**< Marlinms3 */ + DM_TYPE_VERIMATRIX = 3, /**< Verimatrix */ + DM_TYPE_WIDEVINE_CLASSIC = 4, /**< Widevine classic */ + DM_TYPE_SECUREMEDIA = 5, /**< Securemedia */ + DM_TYPE_SDRM = 6, /**< SDRM */ + DM_TYPE_VUDU = 7, /**< Vudu */ + DM_TYPE_WIDEVINE = 8, /**< Widevine cdm */ + DM_TYPE_LYNK = 9, /**< Lynk */ + DM_TYPE_CLEARKEY = 13, /**< Clearkey */ + DM_TYPE_EME = 14, /**< EME */ + //... + DM_TYPE_MAX, +} dm_type_e; + +typedef struct SetDataParam_s { + void* param1; /**< Parameter 1 */ + void* param2; /**< Parameter 2 */ + void* param3; /**< Parameter 3 */ + void* param4; /**< Parameter 4 */ +} SetDataParam_t; + +typedef void* DRMSessionHandle_t; + +typedef int (*FuncDMGRSetData)(DRMSessionHandle_t drm_session, + const char* data_type, void* input_data); +typedef int (*FuncDMGRGetData)(DRMSessionHandle_t drm_session, + const char* data_type, void* output_data); +typedef void (*FuncDMGRSetDRMLocalMode)(); +typedef DRMSessionHandle_t (*FuncDMGRCreateDRMSession)( + dm_type_e type, const char* drm_sub_type); +typedef bool (*FuncDMGRSecurityInitCompleteCB)(int* drm_handle, + unsigned int len, + unsigned char* pssh_data, + void* user_data); +typedef int (*FuncDMGRReleaseDRMSession)(DRMSessionHandle_t drm_session); + +void* OpenDrmManagerProxy(); +int InitDrmManagerProxy(void* handle); +void CloseDrmManagerProxy(void* handle); + +extern FuncDMGRSetData DMGRSetData; +extern FuncDMGRGetData DMGRGetData; +extern FuncDMGRSetDRMLocalMode DMGRSetDRMLocalMode; +extern FuncDMGRCreateDRMSession DMGRCreateDRMSession; +extern FuncDMGRSecurityInitCompleteCB DMGRSecurityInitCompleteCB; +extern FuncDMGRReleaseDRMSession DMGRReleaseDRMSession; + +#endif // FLUTTER_PLUGIN_DRM_MANAGER_PROXY_H_ diff --git a/packages/video_player_avplay/tizen/src/ecore_wl2_window_proxy.cc b/packages/video_player_avplay/tizen/src/ecore_wl2_window_proxy.cc new file mode 100644 index 000000000..0e8891ffb --- /dev/null +++ b/packages/video_player_avplay/tizen/src/ecore_wl2_window_proxy.cc @@ -0,0 +1,60 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ecore_wl2_window_proxy.h" + +#include + +#include "log.h" + +typedef void (*FuncEcoreWl2WindowGeometryGet)(void *window, int *x, int *y, + int *width, int *height); +typedef int (*FuncEcoreWl2WindowSurfaceIdGet)(void *window); + +EcoreWl2WindowProxy::EcoreWl2WindowProxy() { + ecore_wl2_window_handle_ = dlopen("libecore_wl2.so.1", RTLD_LAZY); + if (ecore_wl2_window_handle_ == nullptr) { + LOG_ERROR("Failed to open ecore wl2."); + } +} + +void EcoreWl2WindowProxy::ecore_wl2_window_geometry_get(void *window, int *x, + int *y, int *width, + int *height) { + if (!ecore_wl2_window_handle_) { + LOG_ERROR("ecore_wl2_window_handle_ not valid"); + return; + } + + FuncEcoreWl2WindowGeometryGet ecore_wl2_window_geometry_get = + reinterpret_cast( + dlsym(ecore_wl2_window_handle_, "ecore_wl2_window_geometry_get")); + if (!ecore_wl2_window_geometry_get) { + LOG_ERROR("Fail to find ecore_wl2_window_geometry_get."); + return; + } + ecore_wl2_window_geometry_get(window, x, y, width, height); +} + +int EcoreWl2WindowProxy::ecore_wl2_window_surface_id_get(void *window) { + if (!ecore_wl2_window_handle_) { + LOG_ERROR("ecore_wl2_window_handle_ not valid"); + return -1; + } + FuncEcoreWl2WindowSurfaceIdGet ecore_wl2_window_surface_id_get = + reinterpret_cast( + dlsym(ecore_wl2_window_handle_, "ecore_wl2_window_surface_id_get")); + if (!ecore_wl2_window_surface_id_get) { + LOG_ERROR("Fail to find ecore_wl2_window_surface_id_get."); + return -1; + } + return ecore_wl2_window_surface_id_get(window); +} + +EcoreWl2WindowProxy::~EcoreWl2WindowProxy() { + if (ecore_wl2_window_handle_) { + dlclose(ecore_wl2_window_handle_); + ecore_wl2_window_handle_ = nullptr; + } +} diff --git a/packages/video_player_avplay/tizen/src/ecore_wl2_window_proxy.h b/packages/video_player_avplay/tizen/src/ecore_wl2_window_proxy.h new file mode 100644 index 000000000..66c4aa3cc --- /dev/null +++ b/packages/video_player_avplay/tizen/src/ecore_wl2_window_proxy.h @@ -0,0 +1,20 @@ +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_ECORE_WL2_WINDOW_PROXY_H_ +#define FLUTTER_PLUGIN_ECORE_WL2_WINDOW_PROXY_H_ + +class EcoreWl2WindowProxy { + public: + EcoreWl2WindowProxy(); + ~EcoreWl2WindowProxy(); + void ecore_wl2_window_geometry_get(void *window, int *x, int *y, int *width, + int *height); + int ecore_wl2_window_surface_id_get(void *window); + + private: + void *ecore_wl2_window_handle_ = nullptr; +}; + +#endif // FLUTTER_PLUGIN_ECORE_WL2_WINDOW_PROXY_H_ diff --git a/packages/video_player_avplay/tizen/src/log.h b/packages/video_player_avplay/tizen/src/log.h new file mode 100644 index 000000000..6c7b8d5be --- /dev/null +++ b/packages/video_player_avplay/tizen/src/log.h @@ -0,0 +1,24 @@ +#ifndef __LOG_H__ +#define __LOG_H__ + +#include + +#ifdef LOG_TAG +#undef LOG_TAG +#endif +#define LOG_TAG "VideoPlayerAVPlayPlugin" + +#ifndef __MODULE__ +#define __MODULE__ strrchr("/" __FILE__, '/') + 1 +#endif + +#define LOG(prio, fmt, arg...) \ + dlog_print(prio, LOG_TAG, "%s: %s(%d) > " fmt, __MODULE__, __func__, \ + __LINE__, ##arg) + +#define LOG_DEBUG(fmt, args...) LOG(DLOG_DEBUG, fmt, ##args) +#define LOG_INFO(fmt, args...) LOG(DLOG_INFO, fmt, ##args) +#define LOG_WARN(fmt, args...) LOG(DLOG_WARN, fmt, ##args) +#define LOG_ERROR(fmt, args...) LOG(DLOG_ERROR, fmt, ##args) + +#endif // __LOG_H__ diff --git a/packages/video_player_avplay/tizen/src/media_player.cc b/packages/video_player_avplay/tizen/src/media_player.cc new file mode 100644 index 000000000..39a702743 --- /dev/null +++ b/packages/video_player_avplay/tizen/src/media_player.cc @@ -0,0 +1,669 @@ +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media_player.h" + +#include + +#include "log.h" + +static std::string RotationToString(player_display_rotation_e rotation) { + switch (rotation) { + case PLAYER_DISPLAY_ROTATION_NONE: + return "PLAYER_DISPLAY_ROTATION_NONE"; + case PLAYER_DISPLAY_ROTATION_90: + return "PLAYER_DISPLAY_ROTATION_90"; + case PLAYER_DISPLAY_ROTATION_180: + return "PLAYER_DISPLAY_ROTATION_180"; + case PLAYER_DISPLAY_ROTATION_270: + return "PLAYER_DISPLAY_ROTATION_270"; + } + return std::string(); +} + +static player_stream_type_e ConvertTrackType(std::string track_type) { + if (track_type == "video") { + return PLAYER_STREAM_TYPE_VIDEO; + } + if (track_type == "audio") { + return PLAYER_STREAM_TYPE_AUDIO; + } + if (track_type == "text") { + return PLAYER_STREAM_TYPE_TEXT; + } +} + +MediaPlayer::MediaPlayer(flutter::BinaryMessenger *messenger, + void *native_window) + : VideoPlayer(messenger), native_window_(native_window) { + media_player_proxy_ = std::make_unique(); +} + +MediaPlayer::~MediaPlayer() { Dispose(); } + +int64_t MediaPlayer::Create(const std::string &uri, int drm_type, + const std::string &license_server_url, + bool is_prebuffer_mode, + flutter::EncodableMap &http_headers) { + LOG_INFO("[MediaPlayer] uri: %s, drm_type: %d.", uri.c_str(), drm_type); + + if (uri.empty()) { + LOG_ERROR("[MediaPlayer] The uri must not be empty."); + return -1; + } + + int ret = player_create(&player_); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_create failed: %s.", + get_error_message(ret)); + return -1; + } + + if (!http_headers.empty()) { + auto iter = http_headers.find(flutter::EncodableValue("Cookie")); + if (iter != http_headers.end()) { + if (std::holds_alternative(iter->second)) { + std::string cookie = std::get(iter->second); + ret = + player_set_streaming_cookie(player_, cookie.c_str(), cookie.size()); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_set_streaming_cookie failed: %s.", + get_error_message(ret)); + } + } + } + + iter = http_headers.find(flutter::EncodableValue("User-Agent")); + if (iter != http_headers.end()) { + if (std::holds_alternative(iter->second)) { + std::string user_agent = std::get(iter->second); + ret = player_set_streaming_user_agent(player_, user_agent.c_str(), + user_agent.size()); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_set_streaming_user_agent failed: %s.", + get_error_message(ret)); + } + } + } + } + + if (drm_type != 0) { + if (!SetDrm(uri, drm_type, license_server_url)) { + LOG_ERROR("[MediaPlayer] Failed to set drm."); + return -1; + } + } + + if (!SetDisplay()) { + LOG_ERROR("[MediaPlayer] Failed to set display."); + return -1; + } + + SetDisplayRoi(0, 0, 1, 1); + + ret = player_set_uri(player_, uri.c_str()); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_set_uri failed : %s.", + get_error_message(ret)); + return -1; + } + + ret = player_set_display_visible(player_, true); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_set_display_visible failed : %s.", + get_error_message(ret)); + return -1; + } + + ret = player_set_buffering_cb(player_, OnBuffering, this); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_set_buffering_cb failed : %s.", + get_error_message(ret)); + return -1; + } + + ret = player_set_completed_cb(player_, OnPlayCompleted, this); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_set_completed_cb failed : %s.", + get_error_message(ret)); + return -1; + } + + ret = player_set_interrupted_cb(player_, OnInterrupted, this); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_set_interrupted_cb failed : %s.", + get_error_message(ret)); + return -1; + } + + ret = player_set_error_cb(player_, OnError, this); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_set_error_cb failed : %s.", + get_error_message(ret)); + return -1; + } + + ret = player_set_subtitle_updated_cb(player_, OnSubtitleUpdated, this); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_set_subtitle_updated_cb failed : %s.", + get_error_message(ret)); + return -1; + } + + ret = player_prepare_async(player_, OnPrepared, this); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_prepare_async failed : %s.", + get_error_message(ret)); + return -1; + } + + return SetUpEventChannel(); +} + +void MediaPlayer::Dispose() { + LOG_INFO("[MediaPlayer] Disposing."); + + if (player_) { + if (is_initialized_) { + player_unprepare(player_); + is_initialized_ = false; + } + player_destroy(player_); + player_ = nullptr; + } + + // drm should be released after destroy of player + if (drm_manager_) { + drm_manager_->ReleaseDrmSession(); + } +} + +void MediaPlayer::SetDisplayRoi(int32_t x, int32_t y, int32_t width, + int32_t height) { + int ret = player_set_display_roi_area(player_, x, y, width, height); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_set_display_roi_area failed: %s.", + get_error_message(ret)); + } +} + +bool MediaPlayer::Play() { + LOG_INFO("[MediaPlayer] Player starting."); + + player_state_e state = PLAYER_STATE_NONE; + int ret = player_get_state(player_, &state); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] Unable to get player state."); + } + if (state == PLAYER_STATE_NONE || state == PLAYER_STATE_IDLE) { + LOG_ERROR("[MediaPlayer] Player not ready."); + return false; + } + if (state == PLAYER_STATE_PLAYING) { + LOG_INFO("[MediaPlayer] Player already playing."); + return false; + } + ret = player_start(player_); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_start failed: %s.", get_error_message(ret)); + return false; + } + return true; +} + +bool MediaPlayer::Pause() { + LOG_INFO("[MediaPlayer] Player pausing."); + + player_state_e state = PLAYER_STATE_NONE; + int ret = player_get_state(player_, &state); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] Unable to get player state."); + } + if (state == PLAYER_STATE_NONE || state == PLAYER_STATE_IDLE) { + LOG_ERROR("[MediaPlayer] Player not ready."); + return false; + } + if (state != PLAYER_STATE_PLAYING) { + LOG_INFO("[MediaPlayer] Player not playing."); + return false; + } + ret = player_pause(player_); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_pause failed: %s.", get_error_message(ret)); + return false; + } + return true; +} + +bool MediaPlayer::SetLooping(bool is_looping) { + LOG_INFO("[MediaPlayer] is_looping: %d.", is_looping); + + int ret = player_set_looping(player_, is_looping); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_set_looping failed: %s.", + get_error_message(ret)); + return false; + } + return true; +} + +bool MediaPlayer::SetVolume(double volume) { + LOG_INFO("[MediaPlayer] volume: %f.", volume); + + int ret = player_set_volume(player_, volume, volume); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_set_volume failed: %s.", + get_error_message(ret)); + return false; + } + return true; +} + +bool MediaPlayer::SetPlaybackSpeed(double speed) { + LOG_INFO("[MediaPlayer] speed: %f.", speed); + + int ret = player_set_playback_rate(player_, speed); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_set_playback_rate failed: %s.", + get_error_message(ret)); + return false; + } + return true; +} + +bool MediaPlayer::SeekTo(int64_t position, SeekCompletedCallback callback) { + LOG_INFO("[MediaPlayer] position: %d.", position); + + on_seek_completed_ = std::move(callback); + int ret = + player_set_play_position(player_, position, true, OnSeekCompleted, this); + if (ret != PLAYER_ERROR_NONE) { + on_seek_completed_ = nullptr; + LOG_ERROR("[MediaPlayer] player_set_play_position failed: %s.", + get_error_message(ret)); + return false; + } + return true; +} + +int64_t MediaPlayer::GetPosition() { + int position = 0; + int ret = player_get_play_position(player_, &position); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_get_play_position failed: %s.", + get_error_message(ret)); + } + LOG_DEBUG("[MediaPlayer] Video current position : %d.", position); + return position; +} + +int64_t MediaPlayer::GetDuration() { + int duration = 0; + int ret = player_get_duration(player_, &duration); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_get_duration failed: %s.", + get_error_message(ret)); + } + LOG_INFO("[MediaPlayer] Video duration: %d.", duration); + return duration; +} + +void MediaPlayer::GetVideoSize(int32_t *width, int32_t *height) { + int w = 0, h = 0; + int ret = player_get_video_size(player_, &w, &h); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_get_video_size failed: %s.", + get_error_message(ret)); + } + LOG_INFO("[MediaPlayer] Video width: %d, height: %d.", w, h); + + player_display_rotation_e rotation = PLAYER_DISPLAY_ROTATION_NONE; + ret = player_get_display_rotation(player_, &rotation); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_get_display_rotation failed: %s.", + get_error_message(ret)); + } + LOG_DEBUG("[MediaPlayer] Video rotation: %s.", + RotationToString(rotation).c_str()); + if (rotation == PLAYER_DISPLAY_ROTATION_90 || + rotation == PLAYER_DISPLAY_ROTATION_270) { + std::swap(w, h); + } + + *width = w; + *height = h; +} + +bool MediaPlayer::IsReady() { + player_state_e state = PLAYER_STATE_NONE; + int ret = player_get_state(player_, &state); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_get_state failed: %s.", + get_error_message(ret)); + return false; + } + + LOG_INFO("[MediaPlayer] Player state : %d.", state); + return PLAYER_STATE_READY == state; +} + +bool MediaPlayer::SetDisplay() { + int x = 0, y = 0, width = 0, height = 0; + ecore_wl2_window_proxy_->ecore_wl2_window_geometry_get(native_window_, &x, &y, + &width, &height); + int ret = media_player_proxy_->player_set_ecore_wl_display( + player_, PLAYER_DISPLAY_TYPE_OVERLAY, native_window_, x, y, width, + height); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_set_ecore_wl_display failed: %s.", + get_error_message(ret)); + return false; + } + + ret = player_set_display_mode(player_, PLAYER_DISPLAY_MODE_DST_ROI); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_set_display_mode failed: %s.", + get_error_message(ret)); + return false; + } + return true; +} + +flutter::EncodableList MediaPlayer::GetTrackInfo(std::string track_type) { + player_state_e state = PLAYER_STATE_NONE; + int ret = player_get_state(player_, &state); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_get_state failed: %s", + get_error_message(ret)); + return {}; + } + if (state == PLAYER_STATE_NONE || state == PLAYER_STATE_IDLE) { + LOG_ERROR("[MediaPlayer] Player not ready."); + return {}; + } + + player_stream_type_e type = ConvertTrackType(track_type); + int track_count = 0; + ret = media_player_proxy_->player_get_track_count_v2(player_, type, + &track_count); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_get_track_count_v2 failed: %s", + get_error_message(ret)); + return {}; + } + if (track_count <= 0) { + return {}; + } + + flutter::EncodableList trackSelections = {}; + flutter::EncodableMap trackSelection = {}; + trackSelection.insert( + {flutter::EncodableValue("trackType"), flutter::EncodableValue(type)}); + if (type == PLAYER_STREAM_TYPE_VIDEO) { + LOG_INFO("[MediaPlayer] video_count: %d", track_count); + + for (int video_index = 0; video_index < track_count; video_index++) { + player_video_track_info_v2 *video_track_info = nullptr; + + ret = media_player_proxy_->player_get_video_track_info_v2( + player_, video_index, &video_track_info); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_get_video_track_info_v2 failed: %s", + get_error_message(ret)); + return {}; + } + LOG_INFO( + "[MediaPlayer] video track info: width[%d], height[%d], " + "bitrate[%d]", + video_track_info->width, video_track_info->height, + video_track_info->bit_rate); + + trackSelection.insert_or_assign(flutter::EncodableValue("trackId"), + flutter::EncodableValue(video_index)); + trackSelection.insert_or_assign( + flutter::EncodableValue("width"), + flutter::EncodableValue(video_track_info->width)); + trackSelection.insert_or_assign( + flutter::EncodableValue("height"), + flutter::EncodableValue(video_track_info->height)); + trackSelection.insert_or_assign( + flutter::EncodableValue("bitrate"), + flutter::EncodableValue(video_track_info->bit_rate)); + + trackSelections.push_back(flutter::EncodableValue(trackSelection)); + } + + } else if (type == PLAYER_STREAM_TYPE_AUDIO) { + LOG_INFO("[MediaPlayer] audio_count: %d", track_count); + + for (int audio_index = 0; audio_index < track_count; audio_index++) { + player_audio_track_info_v2 *audio_track_info = nullptr; + + ret = media_player_proxy_->player_get_audio_track_info_v2( + player_, audio_index, &audio_track_info); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_get_audio_track_info_v2 failed: %s", + get_error_message(ret)); + return {}; + } + LOG_INFO( + "[MediaPlayer] audio track info: language[%s], channel[%d], " + "sample_rate[%d], bitrate[%d]", + audio_track_info->language, audio_track_info->channel, + audio_track_info->sample_rate, audio_track_info->bit_rate); + + trackSelection.insert_or_assign(flutter::EncodableValue("trackId"), + flutter::EncodableValue(audio_index)); + trackSelection.insert_or_assign( + flutter::EncodableValue("language"), + flutter::EncodableValue(std::string(audio_track_info->language))); + trackSelection.insert_or_assign( + flutter::EncodableValue("channel"), + flutter::EncodableValue(audio_track_info->channel)); + trackSelection.insert_or_assign( + flutter::EncodableValue("bitrate"), + flutter::EncodableValue(audio_track_info->bit_rate)); + + trackSelections.push_back(flutter::EncodableValue(trackSelection)); + } + + } else if (type == PLAYER_STREAM_TYPE_TEXT) { + LOG_INFO("[MediaPlayer] subtitle_count: %d", track_count); + + for (int sub_index = 0; sub_index < track_count; sub_index++) { + player_subtitle_track_info_v2 *sub_track_info = nullptr; + + ret = media_player_proxy_->player_get_subtitle_track_info_v2( + player_, sub_index, &sub_track_info); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_get_subtitle_track_info_v2 failed: %s", + get_error_message(ret)); + return {}; + } + LOG_INFO("[MediaPlayer] subtitle track info: language[%s]", + sub_track_info->language); + + trackSelection.insert_or_assign(flutter::EncodableValue("trackId"), + flutter::EncodableValue(sub_index)); + trackSelection.insert_or_assign( + flutter::EncodableValue("language"), + flutter::EncodableValue(std::string(sub_track_info->language))); + + trackSelections.push_back(flutter::EncodableValue(trackSelection)); + } + } + + return trackSelections; +} + +bool MediaPlayer::SetTrackSelection(int32_t track_id, std::string track_type) { + LOG_INFO("[MediaPlayer] track_id: %d,track_type: %s", track_id, + track_type.c_str()); + + player_state_e state = PLAYER_STATE_NONE; + int ret = player_get_state(player_, &state); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_get_state failed: %s", + get_error_message(ret)); + return false; + } + if (state == PLAYER_STATE_NONE || state == PLAYER_STATE_IDLE) { + LOG_ERROR("[MediaPlayer] Player not ready."); + return false; + } + + ret = player_select_track(player_, ConvertTrackType(track_type), track_id); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_select_track failed: %s", + get_error_message(ret)); + return false; + } + + return true; +} + +bool MediaPlayer::SetDrm(const std::string &uri, int drm_type, + const std::string &license_server_url) { + drm_manager_ = std::make_unique(); + if (!drm_manager_->CreateDrmSession(drm_type, false)) { + LOG_ERROR("[MediaPlayer] Failed to create drm session."); + return false; + } + + int drm_handle = 0; + if (!drm_manager_->GetDrmHandle(&drm_handle)) { + LOG_ERROR("[MediaPlayer] Failed to get drm handle."); + return false; + } + + int ret = media_player_proxy_->player_set_drm_handle( + player_, PLAYER_DRM_TYPE_EME, drm_handle); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_set_drm_handle failed : %s.", + get_error_message(ret)); + return false; + } + + ret = media_player_proxy_->player_set_drm_init_complete_cb( + player_, OnDrmSecurityInitComplete, this); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_set_drm_init_complete_cb failed : %s.", + get_error_message(ret)); + return false; + } + + ret = media_player_proxy_->player_set_drm_init_data_cb( + player_, OnDrmUpdatePsshData, this); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_set_drm_init_complete_cb failed : %s.", + get_error_message(ret)); + return false; + } + + if (license_server_url.empty()) { + bool success = drm_manager_->SetChallenge(uri, binary_messenger_); + if (!success) { + LOG_ERROR("[MediaPlayer] Failed to set challenge."); + return false; + } + } else { + if (!drm_manager_->SetChallenge(uri, license_server_url)) { + LOG_ERROR("[MediaPlayer] Failed to set challenge."); + return false; + } + } + return true; +} + +void MediaPlayer::OnPrepared(void *user_data) { + LOG_INFO("[MediaPlayer] Player prepared."); + + MediaPlayer *self = static_cast(user_data); + if (!self->is_initialized_) { + self->SendInitialized(); + } +} + +void MediaPlayer::OnBuffering(int percent, void *user_data) { + LOG_INFO("[MediaPlayer] Buffering percent: %d.", percent); + + MediaPlayer *self = static_cast(user_data); + if (percent == 100) { + self->SendBufferingEnd(); + self->is_buffering_ = false; + } else if (!self->is_buffering_ && percent <= 5) { + self->SendBufferingStart(); + self->is_buffering_ = true; + } else { + self->SendBufferingUpdate(percent); + } +} + +void MediaPlayer::OnSeekCompleted(void *user_data) { + LOG_INFO("[MediaPlayer] Seek completed."); + + MediaPlayer *self = static_cast(user_data); + if (self->on_seek_completed_) { + self->on_seek_completed_(); + self->on_seek_completed_ = nullptr; + } +} + +void MediaPlayer::OnPlayCompleted(void *user_data) { + LOG_INFO("[MediaPlayer] Play completed."); + + MediaPlayer *self = static_cast(user_data); + self->SendPlayCompleted(); + self->Pause(); +} + +void MediaPlayer::OnInterrupted(player_interrupted_code_e code, + void *user_data) { + LOG_ERROR("[MediaPlayer] Interrupt code: %d.", code); + + MediaPlayer *self = static_cast(user_data); + self->SendError("Interrupted error", "Media player has been interrupted."); +} + +void MediaPlayer::OnError(int error_code, void *user_data) { + LOG_ERROR("An error occurred for media player, error: %d (%s).", error_code, + get_error_message(error_code)); + + MediaPlayer *self = static_cast(user_data); + self->SendError("Media Player error", get_error_message(error_code)); +} + +void MediaPlayer::OnSubtitleUpdated(unsigned long duration, char *text, + void *user_data) { + LOG_INFO("[MediaPlayer] Subtitle updated, duration: %ld, text: %s.", duration, + text); + + MediaPlayer *self = static_cast(user_data); + self->SendSubtitleUpdate(duration, std::string(text)); +} + +bool MediaPlayer::OnDrmSecurityInitComplete(int *drm_handle, + unsigned int length, + unsigned char *pssh_data, + void *user_data) { + LOG_INFO("[MediaPlayer] Drm init completed."); + + MediaPlayer *self = static_cast(user_data); + if (self->drm_manager_) { + return self->drm_manager_->SecurityInitCompleteCB(drm_handle, length, + pssh_data, self->player_); + } + return false; +} + +int MediaPlayer::OnDrmUpdatePsshData(drm_init_data_type init_type, void *data, + int data_length, void *user_data) { + LOG_INFO("[MediaPlayer] Drm update pssh data."); + + MediaPlayer *self = static_cast(user_data); + if (self->drm_manager_) { + return self->drm_manager_->UpdatePsshData(data, data_length); + } + return 0; +} diff --git a/packages/video_player_avplay/tizen/src/media_player.h b/packages/video_player_avplay/tizen/src/media_player.h new file mode 100644 index 000000000..e8860c3d9 --- /dev/null +++ b/packages/video_player_avplay/tizen/src/media_player.h @@ -0,0 +1,71 @@ +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_MEDIA_PLAYER_H_ +#define FLUTTER_PLUGIN_MEDIA_PLAYER_H_ + +#include + +#include +#include +#include + +#include "drm_manager.h" +#include "media_player_proxy.h" +#include "video_player.h" + +class MediaPlayer : public VideoPlayer { + public: + explicit MediaPlayer(flutter::BinaryMessenger *messenger, + void *native_window); + ~MediaPlayer(); + + int64_t Create(const std::string &uri, int drm_type, + const std::string &license_server_url, bool is_prebuffer_mode, + flutter::EncodableMap &http_headers) override; + void Dispose() override; + + void SetDisplayRoi(int32_t x, int32_t y, int32_t width, + int32_t height) override; + bool Play() override; + bool Pause() override; + bool SetLooping(bool is_looping) override; + bool SetVolume(double volume) override; + bool SetPlaybackSpeed(double speed) override; + bool SeekTo(int64_t position, SeekCompletedCallback callback) override; + int64_t GetPosition() override; + int64_t GetDuration() override; + void GetVideoSize(int32_t *width, int32_t *height) override; + bool IsReady() override; + flutter::EncodableList GetTrackInfo(std::string track_type) override; + bool SetTrackSelection(int32_t track_id, std::string track_type) override; + + private: + bool SetDisplay(); + bool SetDrm(const std::string &uri, int drm_type, + const std::string &license_server_url); + + static void OnPrepared(void *user_data); + static void OnBuffering(int percent, void *user_data); + static void OnSeekCompleted(void *user_data); + static void OnPlayCompleted(void *user_data); + static void OnInterrupted(player_interrupted_code_e code, void *user_data); + static void OnError(int error_code, void *user_data); + static void OnSubtitleUpdated(unsigned long duration, char *text, + void *user_data); + static bool OnDrmSecurityInitComplete(int *drm_handle, unsigned int length, + unsigned char *pssh_data, + void *user_data); + static int OnDrmUpdatePsshData(drm_init_data_type init_type, void *data, + int data_length, void *user_data); + + player_h player_ = nullptr; + std::unique_ptr media_player_proxy_ = nullptr; + std::unique_ptr drm_manager_; + bool is_buffering_ = false; + void *native_window_ = nullptr; + SeekCompletedCallback on_seek_completed_; +}; + +#endif // FLUTTER_PLUGIN_MEDIA_PLAYER_H_ diff --git a/packages/video_player_avplay/tizen/src/media_player_proxy.cc b/packages/video_player_avplay/tizen/src/media_player_proxy.cc new file mode 100644 index 000000000..06adafac1 --- /dev/null +++ b/packages/video_player_avplay/tizen/src/media_player_proxy.cc @@ -0,0 +1,181 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media_player_proxy.h" + +#include + +#include "log.h" + +typedef int (*FuncPlayerSetEcoreWlDisplay)(player_h player, + player_display_type_e type, + void* ecore_wl_window, int x, int y, + int width, int height); + +typedef int (*FuncPlayerSetDrmHandle)(player_h player, + player_drm_type_e drm_type, + int drm_handle); +typedef int (*FuncPlayerSetDrmInitCompleteCB)( + player_h player, security_init_complete_cb callback, void* user_data); +typedef int (*FuncPlayerSetDrmInitDataCB)(player_h player, + set_drm_init_data_cb callback, + void* user_data); +typedef int (*FuncPlayerGetTrackCountV2)(player_h player, + player_stream_type_e type, + int* pcount); +typedef int (*FuncPlayerGetVideoTrackInfoV2)( + player_h player, int index, player_video_track_info_v2** track_info); +typedef int (*FuncPlayerGetAudioTrackInfoV2)( + player_h player, int index, player_audio_track_info_v2** track_info); +typedef int (*FuncPlayerGetSubtitleTrackInfoV2)( + player_h player, int index, player_subtitle_track_info_v2** track_info); + +MediaPlayerProxy::MediaPlayerProxy() { + media_player_handle_ = dlopen("libcapi-media-player.so.0", RTLD_LAZY); + if (media_player_handle_ == nullptr) { + LOG_ERROR("Failed to open media player."); + } +} + +MediaPlayerProxy::~MediaPlayerProxy() { + if (media_player_handle_) { + dlclose(media_player_handle_); + media_player_handle_ = nullptr; + } +} + +int MediaPlayerProxy::player_set_ecore_wl_display(player_h player, + player_display_type_e type, + void* ecore_wl_window, int x, + int y, int width, + int height) { + if (!media_player_handle_) { + LOG_ERROR("media_player_handle_ not valid"); + return PLAYER_ERROR_NOT_AVAILABLE; + } + FuncPlayerSetEcoreWlDisplay player_set_ecore_wl_display = + reinterpret_cast( + dlsym(media_player_handle_, "player_set_ecore_wl_display")); + if (!player_set_ecore_wl_display) { + LOG_ERROR("Fail to find player_set_ecore_wl_display."); + return PLAYER_ERROR_NOT_AVAILABLE; + } + return player_set_ecore_wl_display(player, type, ecore_wl_window, x, y, width, + height); +} + +int MediaPlayerProxy::player_set_drm_handle(player_h player, + player_drm_type_e drm_type, + int drm_handle) { + if (!media_player_handle_) { + LOG_ERROR("media_player_handle_ not valid"); + return PLAYER_ERROR_NOT_AVAILABLE; + } + FuncPlayerSetDrmHandle player_set_drm_handle = + reinterpret_cast( + dlsym(media_player_handle_, "player_set_drm_handle")); + if (!player_set_drm_handle) { + LOG_ERROR("Fail to find player_set_ecore_wl_display."); + return PLAYER_ERROR_NOT_AVAILABLE; + } + return player_set_drm_handle(player, drm_type, drm_handle); +} + +int MediaPlayerProxy::player_set_drm_init_complete_cb( + player_h player, security_init_complete_cb callback, void* user_data) { + if (!media_player_handle_) { + LOG_ERROR("media_player_handle_ not valid"); + return PLAYER_ERROR_NOT_AVAILABLE; + } + FuncPlayerSetDrmInitCompleteCB player_set_drm_init_complete_cb = + reinterpret_cast( + dlsym(media_player_handle_, "player_set_drm_init_complete_cb")); + if (!player_set_drm_init_complete_cb) { + LOG_ERROR("Fail to find player_set_drm_init_complete_cb."); + return PLAYER_ERROR_NOT_AVAILABLE; + } + return player_set_drm_init_complete_cb(player, callback, user_data); +} + +int MediaPlayerProxy::player_set_drm_init_data_cb(player_h player, + set_drm_init_data_cb callback, + void* user_data) { + if (!media_player_handle_) { + LOG_ERROR("media_player_handle_ not valid"); + return PLAYER_ERROR_NOT_AVAILABLE; + } + FuncPlayerSetDrmInitDataCB player_set_drm_init_data_cb = + reinterpret_cast( + dlsym(media_player_handle_, "player_set_drm_init_data_cb")); + if (!player_set_drm_init_data_cb) { + LOG_ERROR("Fail to find player_set_drm_init_data_cb."); + return PLAYER_ERROR_NOT_AVAILABLE; + } + return player_set_drm_init_data_cb(player, callback, user_data); +} + +int MediaPlayerProxy::player_get_track_count_v2(player_h player, + player_stream_type_e type, + int* pcount) { + if (!media_player_handle_) { + LOG_ERROR("media_player_handle_ not valid"); + return PLAYER_ERROR_NOT_AVAILABLE; + } + FuncPlayerGetTrackCountV2 player_get_track_count_v2 = + reinterpret_cast( + dlsym(media_player_handle_, "player_get_track_count_v2")); + if (!player_get_track_count_v2) { + LOG_ERROR("Fail to find player_get_track_count_v2."); + return PLAYER_ERROR_NOT_AVAILABLE; + } + return player_get_track_count_v2(player, type, pcount); +} + +int MediaPlayerProxy::player_get_video_track_info_v2( + player_h player, int index, player_video_track_info_v2** track_info) { + if (!media_player_handle_) { + LOG_ERROR("media_player_handle_ not valid"); + return PLAYER_ERROR_NOT_AVAILABLE; + } + FuncPlayerGetVideoTrackInfoV2 player_get_video_track_info_v2 = + reinterpret_cast( + dlsym(media_player_handle_, "player_get_video_track_info_v2")); + if (!player_get_video_track_info_v2) { + LOG_ERROR("Fail to find player_get_video_track_info_v2."); + return PLAYER_ERROR_NOT_AVAILABLE; + } + return player_get_video_track_info_v2(player, index, track_info); +} + +int MediaPlayerProxy::player_get_audio_track_info_v2( + player_h player, int index, player_audio_track_info_v2** track_info) { + if (!media_player_handle_) { + LOG_ERROR("media_player_handle_ not valid"); + return PLAYER_ERROR_NOT_AVAILABLE; + } + FuncPlayerGetAudioTrackInfoV2 player_get_audio_track_info_v2 = + reinterpret_cast( + dlsym(media_player_handle_, "player_get_audio_track_info_v2")); + if (!player_get_audio_track_info_v2) { + LOG_ERROR("Fail to find player_get_audio_track_info_v2."); + return PLAYER_ERROR_NOT_AVAILABLE; + } + return player_get_audio_track_info_v2(player, index, track_info); +} + +int MediaPlayerProxy::player_get_subtitle_track_info_v2( + player_h player, int index, player_subtitle_track_info_v2** track_info) { + if (!media_player_handle_) { + LOG_ERROR("media_player_handle_ not valid"); + return PLAYER_ERROR_NOT_AVAILABLE; + } + FuncPlayerGetSubtitleTrackInfoV2 player_get_subtitle_track_info_v2 = + reinterpret_cast( + dlsym(media_player_handle_, "player_get_subtitle_track_info_v2")); + if (!player_get_subtitle_track_info_v2) { + LOG_ERROR("Fail to find player_get_subtitle_track_info_v2."); + return PLAYER_ERROR_NOT_AVAILABLE; + } + return player_get_subtitle_track_info_v2(player, index, track_info); +} diff --git a/packages/video_player_avplay/tizen/src/media_player_proxy.h b/packages/video_player_avplay/tizen/src/media_player_proxy.h new file mode 100644 index 000000000..6e9e94d90 --- /dev/null +++ b/packages/video_player_avplay/tizen/src/media_player_proxy.h @@ -0,0 +1,98 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_MEDIA_PLAYER_PROXY_H_ +#define FLUTTER_PLUGIN_MEDIA_PLAYER_PROXY_H_ + +#include + +#define MAX_STRING_NAME_LEN 255 +#define MMPLAYER_FOUR_CC_LEN 14 +#define PLAYER_LANG_NAME_SIZE 10 + +typedef struct { + char fourCC[MMPLAYER_FOUR_CC_LEN + 1]; /**< codec fourcc */ + char name[MAX_STRING_NAME_LEN]; /**< name: video/audio, it maybe not exit in + some track*/ + /*dynamic infos in hls,ss,dash streams*/ + int width; /**< resolution width */ + int height; /**< resolution height */ + int bit_rate; /**< bitrate in bps */ +} player_video_track_info_v2; + +typedef struct { + char fourCC[MMPLAYER_FOUR_CC_LEN + 1]; /**< codec fourcc */ + char language[PLAYER_LANG_NAME_SIZE]; /**< language info*/ + /*dynamic infos in hls,ss,dash streams*/ + int sample_rate; /**< sample rate in this track*/ + int channel; /**< channel in this track*/ + int bit_rate; /**< bitrate in this track*/ +} player_audio_track_info_v2; + +typedef struct { + char fourCC[MMPLAYER_FOUR_CC_LEN + 1]; /**< codec fourcc */ + char language[PLAYER_LANG_NAME_SIZE]; /**< language info*/ + int subtitle_type; /**< text subtitle = 0, picture subtitle = 1 */ +} player_subtitle_track_info_v2; + +typedef enum { + PLAYER_DRM_TYPE_NONE = 0, + PLAYER_DRM_TYPE_PLAYREADY, + PLAYER_DRM_TYPE_MARLIN, + PLAYER_DRM_TYPE_VERIMATRIX, + PLAYER_DRM_TYPE_WIDEVINE_CLASSIC, + PLAYER_DRM_TYPE_SECUREMEDIA, + PLAYER_DRM_TYPE_SDRM, + PLAYER_DRM_TYPE_VUDU, + PLAYER_DRM_TYPE_WIDEVINE_CDM, + PLAYER_DRM_TYPE_AES128, + PLAYER_DRM_TYPE_HDCP, + PLAYER_DRM_TYPE_DTCP, + PLAYER_DRM_TYPE_SCSA, + PLAYER_DRM_TYPE_CLEARKEY, + PLAYER_DRM_TYPE_EME, + PLAYER_DRM_TYPE_MAX_COUNT, +} player_drm_type_e; + +typedef enum { + CENC = 0, + KEYIDS = 1, + WEBM = 2, +} drm_init_data_type; + +typedef bool (*security_init_complete_cb)(int* drmhandle, unsigned int length, + unsigned char* psshdata, + void* user_data); +typedef int (*set_drm_init_data_cb)(drm_init_data_type init_type, void* data, + int data_length, void* user_data); + +class MediaPlayerProxy { + public: + MediaPlayerProxy(); + ~MediaPlayerProxy(); + int player_set_ecore_wl_display(player_h player, player_display_type_e type, + void* ecore_wl_window, int x, int y, + int width, int height); + int player_set_drm_handle(player_h player, player_drm_type_e drm_type, + int drm_handle); + int player_set_drm_init_complete_cb(player_h player, + security_init_complete_cb callback, + void* user_data); + int player_set_drm_init_data_cb(player_h player, + set_drm_init_data_cb callback, + void* user_data); + int player_get_track_count_v2(player_h player, player_stream_type_e type, + int* pcount); + int player_get_video_track_info_v2(player_h player, int index, + player_video_track_info_v2** track_info); + int player_get_audio_track_info_v2(player_h player, int index, + player_audio_track_info_v2** track_info); + int player_get_subtitle_track_info_v2( + player_h player, int index, player_subtitle_track_info_v2** track_info); + + private: + void* media_player_handle_ = nullptr; +}; + +#endif // FLUTTER_PLUGIN_MEDIA_PLAYER_PROXY_H_ diff --git a/packages/video_player_avplay/tizen/src/messages.cc b/packages/video_player_avplay/tizen/src/messages.cc new file mode 100644 index 000000000..79ec33fd0 --- /dev/null +++ b/packages/video_player_avplay/tizen/src/messages.cc @@ -0,0 +1,1183 @@ +// Autogenerated from Pigeon (v10.0.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#undef _HAS_EXCEPTIONS + +#include "messages.h" + +#include +#include +#include +#include + +#include +#include +#include + +using flutter::BasicMessageChannel; +using flutter::CustomEncodableValue; +using flutter::EncodableList; +using flutter::EncodableMap; +using flutter::EncodableValue; + +// PlayerMessage + +PlayerMessage::PlayerMessage(int64_t player_id) : player_id_(player_id) {} + +int64_t PlayerMessage::player_id() const { return player_id_; } + +void PlayerMessage::set_player_id(int64_t value_arg) { player_id_ = value_arg; } + +EncodableList PlayerMessage::ToEncodableList() const { + EncodableList list; + list.reserve(1); + list.push_back(EncodableValue(player_id_)); + return list; +} + +PlayerMessage PlayerMessage::FromEncodableList(const EncodableList& list) { + PlayerMessage decoded(list[0].LongValue()); + return decoded; +} + +// LoopingMessage + +LoopingMessage::LoopingMessage(int64_t player_id, bool is_looping) + : player_id_(player_id), is_looping_(is_looping) {} + +int64_t LoopingMessage::player_id() const { return player_id_; } + +void LoopingMessage::set_player_id(int64_t value_arg) { + player_id_ = value_arg; +} + +bool LoopingMessage::is_looping() const { return is_looping_; } + +void LoopingMessage::set_is_looping(bool value_arg) { is_looping_ = value_arg; } + +EncodableList LoopingMessage::ToEncodableList() const { + EncodableList list; + list.reserve(2); + list.push_back(EncodableValue(player_id_)); + list.push_back(EncodableValue(is_looping_)); + return list; +} + +LoopingMessage LoopingMessage::FromEncodableList(const EncodableList& list) { + LoopingMessage decoded(list[0].LongValue(), std::get(list[1])); + return decoded; +} + +// VolumeMessage + +VolumeMessage::VolumeMessage(int64_t player_id, double volume) + : player_id_(player_id), volume_(volume) {} + +int64_t VolumeMessage::player_id() const { return player_id_; } + +void VolumeMessage::set_player_id(int64_t value_arg) { player_id_ = value_arg; } + +double VolumeMessage::volume() const { return volume_; } + +void VolumeMessage::set_volume(double value_arg) { volume_ = value_arg; } + +EncodableList VolumeMessage::ToEncodableList() const { + EncodableList list; + list.reserve(2); + list.push_back(EncodableValue(player_id_)); + list.push_back(EncodableValue(volume_)); + return list; +} + +VolumeMessage VolumeMessage::FromEncodableList(const EncodableList& list) { + VolumeMessage decoded(list[0].LongValue(), std::get(list[1])); + return decoded; +} + +// PlaybackSpeedMessage + +PlaybackSpeedMessage::PlaybackSpeedMessage(int64_t player_id, double speed) + : player_id_(player_id), speed_(speed) {} + +int64_t PlaybackSpeedMessage::player_id() const { return player_id_; } + +void PlaybackSpeedMessage::set_player_id(int64_t value_arg) { + player_id_ = value_arg; +} + +double PlaybackSpeedMessage::speed() const { return speed_; } + +void PlaybackSpeedMessage::set_speed(double value_arg) { speed_ = value_arg; } + +EncodableList PlaybackSpeedMessage::ToEncodableList() const { + EncodableList list; + list.reserve(2); + list.push_back(EncodableValue(player_id_)); + list.push_back(EncodableValue(speed_)); + return list; +} + +PlaybackSpeedMessage PlaybackSpeedMessage::FromEncodableList( + const EncodableList& list) { + PlaybackSpeedMessage decoded(list[0].LongValue(), std::get(list[1])); + return decoded; +} + +// TrackMessage + +TrackMessage::TrackMessage(int64_t player_id, const EncodableList& tracks) + : player_id_(player_id), tracks_(tracks) {} + +int64_t TrackMessage::player_id() const { return player_id_; } + +void TrackMessage::set_player_id(int64_t value_arg) { player_id_ = value_arg; } + +const EncodableList& TrackMessage::tracks() const { return tracks_; } + +void TrackMessage::set_tracks(const EncodableList& value_arg) { + tracks_ = value_arg; +} + +EncodableList TrackMessage::ToEncodableList() const { + EncodableList list; + list.reserve(2); + list.push_back(EncodableValue(player_id_)); + list.push_back(EncodableValue(tracks_)); + return list; +} + +TrackMessage TrackMessage::FromEncodableList(const EncodableList& list) { + TrackMessage decoded(list[0].LongValue(), std::get(list[1])); + return decoded; +} + +// TrackTypeMessage + +TrackTypeMessage::TrackTypeMessage(int64_t player_id, + const std::string& track_type) + : player_id_(player_id), track_type_(track_type) {} + +int64_t TrackTypeMessage::player_id() const { return player_id_; } + +void TrackTypeMessage::set_player_id(int64_t value_arg) { + player_id_ = value_arg; +} + +const std::string& TrackTypeMessage::track_type() const { return track_type_; } + +void TrackTypeMessage::set_track_type(std::string_view value_arg) { + track_type_ = value_arg; +} + +EncodableList TrackTypeMessage::ToEncodableList() const { + EncodableList list; + list.reserve(2); + list.push_back(EncodableValue(player_id_)); + list.push_back(EncodableValue(track_type_)); + return list; +} + +TrackTypeMessage TrackTypeMessage::FromEncodableList( + const EncodableList& list) { + TrackTypeMessage decoded(list[0].LongValue(), std::get(list[1])); + return decoded; +} + +// SelectedTracksMessage + +SelectedTracksMessage::SelectedTracksMessage(int64_t player_id, + int64_t track_id, + const std::string& track_type) + : player_id_(player_id), track_id_(track_id), track_type_(track_type) {} + +int64_t SelectedTracksMessage::player_id() const { return player_id_; } + +void SelectedTracksMessage::set_player_id(int64_t value_arg) { + player_id_ = value_arg; +} + +int64_t SelectedTracksMessage::track_id() const { return track_id_; } + +void SelectedTracksMessage::set_track_id(int64_t value_arg) { + track_id_ = value_arg; +} + +const std::string& SelectedTracksMessage::track_type() const { + return track_type_; +} + +void SelectedTracksMessage::set_track_type(std::string_view value_arg) { + track_type_ = value_arg; +} + +EncodableList SelectedTracksMessage::ToEncodableList() const { + EncodableList list; + list.reserve(3); + list.push_back(EncodableValue(player_id_)); + list.push_back(EncodableValue(track_id_)); + list.push_back(EncodableValue(track_type_)); + return list; +} + +SelectedTracksMessage SelectedTracksMessage::FromEncodableList( + const EncodableList& list) { + SelectedTracksMessage decoded(list[0].LongValue(), list[1].LongValue(), + std::get(list[2])); + return decoded; +} + +// PositionMessage + +PositionMessage::PositionMessage(int64_t player_id, int64_t position) + : player_id_(player_id), position_(position) {} + +int64_t PositionMessage::player_id() const { return player_id_; } + +void PositionMessage::set_player_id(int64_t value_arg) { + player_id_ = value_arg; +} + +int64_t PositionMessage::position() const { return position_; } + +void PositionMessage::set_position(int64_t value_arg) { position_ = value_arg; } + +EncodableList PositionMessage::ToEncodableList() const { + EncodableList list; + list.reserve(2); + list.push_back(EncodableValue(player_id_)); + list.push_back(EncodableValue(position_)); + return list; +} + +PositionMessage PositionMessage::FromEncodableList(const EncodableList& list) { + PositionMessage decoded(list[0].LongValue(), list[1].LongValue()); + return decoded; +} + +// CreateMessage + +CreateMessage::CreateMessage() {} + +CreateMessage::CreateMessage(const std::string* asset, const std::string* uri, + const std::string* package_name, + const std::string* format_hint, + const EncodableMap* http_headers, + const EncodableMap* drm_configs, + const EncodableMap* player_options) + : asset_(asset ? std::optional(*asset) : std::nullopt), + uri_(uri ? std::optional(*uri) : std::nullopt), + package_name_(package_name ? std::optional(*package_name) + : std::nullopt), + format_hint_(format_hint ? std::optional(*format_hint) + : std::nullopt), + http_headers_(http_headers ? std::optional(*http_headers) + : std::nullopt), + drm_configs_(drm_configs ? std::optional(*drm_configs) + : std::nullopt), + player_options_(player_options + ? std::optional(*player_options) + : std::nullopt) {} + +const std::string* CreateMessage::asset() const { + return asset_ ? &(*asset_) : nullptr; +} + +void CreateMessage::set_asset(const std::string_view* value_arg) { + asset_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void CreateMessage::set_asset(std::string_view value_arg) { + asset_ = value_arg; +} + +const std::string* CreateMessage::uri() const { + return uri_ ? &(*uri_) : nullptr; +} + +void CreateMessage::set_uri(const std::string_view* value_arg) { + uri_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void CreateMessage::set_uri(std::string_view value_arg) { uri_ = value_arg; } + +const std::string* CreateMessage::package_name() const { + return package_name_ ? &(*package_name_) : nullptr; +} + +void CreateMessage::set_package_name(const std::string_view* value_arg) { + package_name_ = + value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void CreateMessage::set_package_name(std::string_view value_arg) { + package_name_ = value_arg; +} + +const std::string* CreateMessage::format_hint() const { + return format_hint_ ? &(*format_hint_) : nullptr; +} + +void CreateMessage::set_format_hint(const std::string_view* value_arg) { + format_hint_ = + value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void CreateMessage::set_format_hint(std::string_view value_arg) { + format_hint_ = value_arg; +} + +const EncodableMap* CreateMessage::http_headers() const { + return http_headers_ ? &(*http_headers_) : nullptr; +} + +void CreateMessage::set_http_headers(const EncodableMap* value_arg) { + http_headers_ = + value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void CreateMessage::set_http_headers(const EncodableMap& value_arg) { + http_headers_ = value_arg; +} + +const EncodableMap* CreateMessage::drm_configs() const { + return drm_configs_ ? &(*drm_configs_) : nullptr; +} + +void CreateMessage::set_drm_configs(const EncodableMap* value_arg) { + drm_configs_ = + value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void CreateMessage::set_drm_configs(const EncodableMap& value_arg) { + drm_configs_ = value_arg; +} + +const EncodableMap* CreateMessage::player_options() const { + return player_options_ ? &(*player_options_) : nullptr; +} + +void CreateMessage::set_player_options(const EncodableMap* value_arg) { + player_options_ = + value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void CreateMessage::set_player_options(const EncodableMap& value_arg) { + player_options_ = value_arg; +} + +EncodableList CreateMessage::ToEncodableList() const { + EncodableList list; + list.reserve(7); + list.push_back(asset_ ? EncodableValue(*asset_) : EncodableValue()); + list.push_back(uri_ ? EncodableValue(*uri_) : EncodableValue()); + list.push_back(package_name_ ? EncodableValue(*package_name_) + : EncodableValue()); + list.push_back(format_hint_ ? EncodableValue(*format_hint_) + : EncodableValue()); + list.push_back(http_headers_ ? EncodableValue(*http_headers_) + : EncodableValue()); + list.push_back(drm_configs_ ? EncodableValue(*drm_configs_) + : EncodableValue()); + list.push_back(player_options_ ? EncodableValue(*player_options_) + : EncodableValue()); + return list; +} + +CreateMessage CreateMessage::FromEncodableList(const EncodableList& list) { + CreateMessage decoded; + auto& encodable_asset = list[0]; + if (!encodable_asset.IsNull()) { + decoded.set_asset(std::get(encodable_asset)); + } + auto& encodable_uri = list[1]; + if (!encodable_uri.IsNull()) { + decoded.set_uri(std::get(encodable_uri)); + } + auto& encodable_package_name = list[2]; + if (!encodable_package_name.IsNull()) { + decoded.set_package_name(std::get(encodable_package_name)); + } + auto& encodable_format_hint = list[3]; + if (!encodable_format_hint.IsNull()) { + decoded.set_format_hint(std::get(encodable_format_hint)); + } + auto& encodable_http_headers = list[4]; + if (!encodable_http_headers.IsNull()) { + decoded.set_http_headers(std::get(encodable_http_headers)); + } + auto& encodable_drm_configs = list[5]; + if (!encodable_drm_configs.IsNull()) { + decoded.set_drm_configs(std::get(encodable_drm_configs)); + } + auto& encodable_player_options = list[6]; + if (!encodable_player_options.IsNull()) { + decoded.set_player_options( + std::get(encodable_player_options)); + } + return decoded; +} + +// MixWithOthersMessage + +MixWithOthersMessage::MixWithOthersMessage(bool mix_with_others) + : mix_with_others_(mix_with_others) {} + +bool MixWithOthersMessage::mix_with_others() const { return mix_with_others_; } + +void MixWithOthersMessage::set_mix_with_others(bool value_arg) { + mix_with_others_ = value_arg; +} + +EncodableList MixWithOthersMessage::ToEncodableList() const { + EncodableList list; + list.reserve(1); + list.push_back(EncodableValue(mix_with_others_)); + return list; +} + +MixWithOthersMessage MixWithOthersMessage::FromEncodableList( + const EncodableList& list) { + MixWithOthersMessage decoded(std::get(list[0])); + return decoded; +} + +// GeometryMessage + +GeometryMessage::GeometryMessage(int64_t player_id, int64_t x, int64_t y, + int64_t width, int64_t height) + : player_id_(player_id), x_(x), y_(y), width_(width), height_(height) {} + +int64_t GeometryMessage::player_id() const { return player_id_; } + +void GeometryMessage::set_player_id(int64_t value_arg) { + player_id_ = value_arg; +} + +int64_t GeometryMessage::x() const { return x_; } + +void GeometryMessage::set_x(int64_t value_arg) { x_ = value_arg; } + +int64_t GeometryMessage::y() const { return y_; } + +void GeometryMessage::set_y(int64_t value_arg) { y_ = value_arg; } + +int64_t GeometryMessage::width() const { return width_; } + +void GeometryMessage::set_width(int64_t value_arg) { width_ = value_arg; } + +int64_t GeometryMessage::height() const { return height_; } + +void GeometryMessage::set_height(int64_t value_arg) { height_ = value_arg; } + +EncodableList GeometryMessage::ToEncodableList() const { + EncodableList list; + list.reserve(5); + list.push_back(EncodableValue(player_id_)); + list.push_back(EncodableValue(x_)); + list.push_back(EncodableValue(y_)); + list.push_back(EncodableValue(width_)); + list.push_back(EncodableValue(height_)); + return list; +} + +GeometryMessage GeometryMessage::FromEncodableList(const EncodableList& list) { + GeometryMessage decoded(list[0].LongValue(), list[1].LongValue(), + list[2].LongValue(), list[3].LongValue(), + list[4].LongValue()); + return decoded; +} + +VideoPlayerAvplayApiCodecSerializer::VideoPlayerAvplayApiCodecSerializer() {} + +EncodableValue VideoPlayerAvplayApiCodecSerializer::ReadValueOfType( + uint8_t type, flutter::ByteStreamReader* stream) const { + switch (type) { + case 128: + return CustomEncodableValue(CreateMessage::FromEncodableList( + std::get(ReadValue(stream)))); + case 129: + return CustomEncodableValue(GeometryMessage::FromEncodableList( + std::get(ReadValue(stream)))); + case 130: + return CustomEncodableValue(LoopingMessage::FromEncodableList( + std::get(ReadValue(stream)))); + case 131: + return CustomEncodableValue(MixWithOthersMessage::FromEncodableList( + std::get(ReadValue(stream)))); + case 132: + return CustomEncodableValue(PlaybackSpeedMessage::FromEncodableList( + std::get(ReadValue(stream)))); + case 133: + return CustomEncodableValue(PlayerMessage::FromEncodableList( + std::get(ReadValue(stream)))); + case 134: + return CustomEncodableValue(PositionMessage::FromEncodableList( + std::get(ReadValue(stream)))); + case 135: + return CustomEncodableValue(SelectedTracksMessage::FromEncodableList( + std::get(ReadValue(stream)))); + case 136: + return CustomEncodableValue(TrackMessage::FromEncodableList( + std::get(ReadValue(stream)))); + case 137: + return CustomEncodableValue(TrackTypeMessage::FromEncodableList( + std::get(ReadValue(stream)))); + case 138: + return CustomEncodableValue(VolumeMessage::FromEncodableList( + std::get(ReadValue(stream)))); + default: + return flutter::StandardCodecSerializer::ReadValueOfType(type, stream); + } +} + +void VideoPlayerAvplayApiCodecSerializer::WriteValue( + const EncodableValue& value, flutter::ByteStreamWriter* stream) const { + if (const CustomEncodableValue* custom_value = + std::get_if(&value)) { + if (custom_value->type() == typeid(CreateMessage)) { + stream->WriteByte(128); + WriteValue( + EncodableValue( + std::any_cast(*custom_value).ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(GeometryMessage)) { + stream->WriteByte(129); + WriteValue( + EncodableValue( + std::any_cast(*custom_value).ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(LoopingMessage)) { + stream->WriteByte(130); + WriteValue( + EncodableValue( + std::any_cast(*custom_value).ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(MixWithOthersMessage)) { + stream->WriteByte(131); + WriteValue( + EncodableValue(std::any_cast(*custom_value) + .ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(PlaybackSpeedMessage)) { + stream->WriteByte(132); + WriteValue( + EncodableValue(std::any_cast(*custom_value) + .ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(PlayerMessage)) { + stream->WriteByte(133); + WriteValue( + EncodableValue( + std::any_cast(*custom_value).ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(PositionMessage)) { + stream->WriteByte(134); + WriteValue( + EncodableValue( + std::any_cast(*custom_value).ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(SelectedTracksMessage)) { + stream->WriteByte(135); + WriteValue( + EncodableValue(std::any_cast(*custom_value) + .ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(TrackMessage)) { + stream->WriteByte(136); + WriteValue( + EncodableValue( + std::any_cast(*custom_value).ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(TrackTypeMessage)) { + stream->WriteByte(137); + WriteValue( + EncodableValue( + std::any_cast(*custom_value).ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(VolumeMessage)) { + stream->WriteByte(138); + WriteValue( + EncodableValue( + std::any_cast(*custom_value).ToEncodableList()), + stream); + return; + } + } + flutter::StandardCodecSerializer::WriteValue(value, stream); +} + +/// The codec used by VideoPlayerAvplayApi. +const flutter::StandardMessageCodec& VideoPlayerAvplayApi::GetCodec() { + return flutter::StandardMessageCodec::GetInstance( + &VideoPlayerAvplayApiCodecSerializer::GetInstance()); +} + +// Sets up an instance of `VideoPlayerAvplayApi` to handle messages through the +// `binary_messenger`. +void VideoPlayerAvplayApi::SetUp(flutter::BinaryMessenger* binary_messenger, + VideoPlayerAvplayApi* api) { + { + auto channel = std::make_unique>( + binary_messenger, "dev.flutter.pigeon.VideoPlayerAvplayApi.initialize", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + std::optional output = api->Initialize(); + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = std::make_unique>( + binary_messenger, "dev.flutter.pigeon.VideoPlayerAvplayApi.create", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_msg_arg = args.at(0); + if (encodable_msg_arg.IsNull()) { + reply(WrapError("msg_arg unexpectedly null.")); + return; + } + const auto& msg_arg = std::any_cast( + std::get(encodable_msg_arg)); + ErrorOr output = api->Create(msg_arg); + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back( + CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = std::make_unique>( + binary_messenger, "dev.flutter.pigeon.VideoPlayerAvplayApi.dispose", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_msg_arg = args.at(0); + if (encodable_msg_arg.IsNull()) { + reply(WrapError("msg_arg unexpectedly null.")); + return; + } + const auto& msg_arg = std::any_cast( + std::get(encodable_msg_arg)); + std::optional output = api->Dispose(msg_arg); + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = std::make_unique>( + binary_messenger, "dev.flutter.pigeon.VideoPlayerAvplayApi.setLooping", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_msg_arg = args.at(0); + if (encodable_msg_arg.IsNull()) { + reply(WrapError("msg_arg unexpectedly null.")); + return; + } + const auto& msg_arg = std::any_cast( + std::get(encodable_msg_arg)); + std::optional output = api->SetLooping(msg_arg); + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = std::make_unique>( + binary_messenger, "dev.flutter.pigeon.VideoPlayerAvplayApi.setVolume", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_msg_arg = args.at(0); + if (encodable_msg_arg.IsNull()) { + reply(WrapError("msg_arg unexpectedly null.")); + return; + } + const auto& msg_arg = std::any_cast( + std::get(encodable_msg_arg)); + std::optional output = api->SetVolume(msg_arg); + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = std::make_unique>( + binary_messenger, + "dev.flutter.pigeon.VideoPlayerAvplayApi.setPlaybackSpeed", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_msg_arg = args.at(0); + if (encodable_msg_arg.IsNull()) { + reply(WrapError("msg_arg unexpectedly null.")); + return; + } + const auto& msg_arg = std::any_cast( + std::get(encodable_msg_arg)); + std::optional output = + api->SetPlaybackSpeed(msg_arg); + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = std::make_unique>( + binary_messenger, "dev.flutter.pigeon.VideoPlayerAvplayApi.play", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_msg_arg = args.at(0); + if (encodable_msg_arg.IsNull()) { + reply(WrapError("msg_arg unexpectedly null.")); + return; + } + const auto& msg_arg = std::any_cast( + std::get(encodable_msg_arg)); + std::optional output = api->Play(msg_arg); + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = std::make_unique>( + binary_messenger, + "dev.flutter.pigeon.VideoPlayerAvplayApi.setDeactivate", &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_msg_arg = args.at(0); + if (encodable_msg_arg.IsNull()) { + reply(WrapError("msg_arg unexpectedly null.")); + return; + } + const auto& msg_arg = std::any_cast( + std::get(encodable_msg_arg)); + ErrorOr output = api->SetDeactivate(msg_arg); + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = std::make_unique>( + binary_messenger, "dev.flutter.pigeon.VideoPlayerAvplayApi.setActivate", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_msg_arg = args.at(0); + if (encodable_msg_arg.IsNull()) { + reply(WrapError("msg_arg unexpectedly null.")); + return; + } + const auto& msg_arg = std::any_cast( + std::get(encodable_msg_arg)); + ErrorOr output = api->SetActivate(msg_arg); + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = std::make_unique>( + binary_messenger, "dev.flutter.pigeon.VideoPlayerAvplayApi.track", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_msg_arg = args.at(0); + if (encodable_msg_arg.IsNull()) { + reply(WrapError("msg_arg unexpectedly null.")); + return; + } + const auto& msg_arg = std::any_cast( + std::get(encodable_msg_arg)); + ErrorOr output = api->Track(msg_arg); + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back( + CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = std::make_unique>( + binary_messenger, + "dev.flutter.pigeon.VideoPlayerAvplayApi.setTrackSelection", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_msg_arg = args.at(0); + if (encodable_msg_arg.IsNull()) { + reply(WrapError("msg_arg unexpectedly null.")); + return; + } + const auto& msg_arg = std::any_cast( + std::get(encodable_msg_arg)); + ErrorOr output = api->SetTrackSelection(msg_arg); + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = std::make_unique>( + binary_messenger, "dev.flutter.pigeon.VideoPlayerAvplayApi.position", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_msg_arg = args.at(0); + if (encodable_msg_arg.IsNull()) { + reply(WrapError("msg_arg unexpectedly null.")); + return; + } + const auto& msg_arg = std::any_cast( + std::get(encodable_msg_arg)); + ErrorOr output = api->Position(msg_arg); + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back( + CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = std::make_unique>( + binary_messenger, "dev.flutter.pigeon.VideoPlayerAvplayApi.seekTo", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_msg_arg = args.at(0); + if (encodable_msg_arg.IsNull()) { + reply(WrapError("msg_arg unexpectedly null.")); + return; + } + const auto& msg_arg = std::any_cast( + std::get(encodable_msg_arg)); + api->SeekTo(msg_arg, + [reply](std::optional&& output) { + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = std::make_unique>( + binary_messenger, "dev.flutter.pigeon.VideoPlayerAvplayApi.pause", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_msg_arg = args.at(0); + if (encodable_msg_arg.IsNull()) { + reply(WrapError("msg_arg unexpectedly null.")); + return; + } + const auto& msg_arg = std::any_cast( + std::get(encodable_msg_arg)); + std::optional output = api->Pause(msg_arg); + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = std::make_unique>( + binary_messenger, + "dev.flutter.pigeon.VideoPlayerAvplayApi.setMixWithOthers", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_msg_arg = args.at(0); + if (encodable_msg_arg.IsNull()) { + reply(WrapError("msg_arg unexpectedly null.")); + return; + } + const auto& msg_arg = std::any_cast( + std::get(encodable_msg_arg)); + std::optional output = + api->SetMixWithOthers(msg_arg); + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = std::make_unique>( + binary_messenger, + "dev.flutter.pigeon.VideoPlayerAvplayApi.setDisplayGeometry", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_msg_arg = args.at(0); + if (encodable_msg_arg.IsNull()) { + reply(WrapError("msg_arg unexpectedly null.")); + return; + } + const auto& msg_arg = std::any_cast( + std::get(encodable_msg_arg)); + std::optional output = + api->SetDisplayGeometry(msg_arg); + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } +} + +EncodableValue VideoPlayerAvplayApi::WrapError(std::string_view error_message) { + return EncodableValue( + EncodableList{EncodableValue(std::string(error_message)), + EncodableValue("Error"), EncodableValue()}); +} + +EncodableValue VideoPlayerAvplayApi::WrapError(const FlutterError& error) { + return EncodableValue(EncodableList{EncodableValue(error.code()), + EncodableValue(error.message()), + error.details()}); +} diff --git a/packages/video_player_avplay/tizen/src/messages.h b/packages/video_player_avplay/tizen/src/messages.h new file mode 100644 index 000000000..d4e863492 --- /dev/null +++ b/packages/video_player_avplay/tizen/src/messages.h @@ -0,0 +1,394 @@ +// Autogenerated from Pigeon (v10.0.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#ifndef PIGEON_MESSAGES_H_ +#define PIGEON_MESSAGES_H_ +#include +#include +#include +#include + +#include +#include +#include + +// Generated class from Pigeon. + +class FlutterError { + public: + explicit FlutterError(const std::string& code) : code_(code) {} + explicit FlutterError(const std::string& code, const std::string& message) + : code_(code), message_(message) {} + explicit FlutterError(const std::string& code, const std::string& message, + const flutter::EncodableValue& details) + : code_(code), message_(message), details_(details) {} + + const std::string& code() const { return code_; } + const std::string& message() const { return message_; } + const flutter::EncodableValue& details() const { return details_; } + + private: + std::string code_; + std::string message_; + flutter::EncodableValue details_; +}; + +template +class ErrorOr { + public: + ErrorOr(const T& rhs) : v_(rhs) {} + ErrorOr(const T&& rhs) : v_(std::move(rhs)) {} + ErrorOr(const FlutterError& rhs) : v_(rhs) {} + ErrorOr(const FlutterError&& rhs) : v_(std::move(rhs)) {} + + bool has_error() const { return std::holds_alternative(v_); } + const T& value() const { return std::get(v_); }; + const FlutterError& error() const { return std::get(v_); }; + + private: + friend class VideoPlayerAvplayApi; + ErrorOr() = default; + T TakeValue() && { return std::get(std::move(v_)); } + + std::variant v_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class PlayerMessage { + public: + // Constructs an object setting all fields. + explicit PlayerMessage(int64_t player_id); + + int64_t player_id() const; + void set_player_id(int64_t value_arg); + + private: + static PlayerMessage FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class VideoPlayerAvplayApi; + friend class VideoPlayerAvplayApiCodecSerializer; + int64_t player_id_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class LoopingMessage { + public: + // Constructs an object setting all fields. + explicit LoopingMessage(int64_t player_id, bool is_looping); + + int64_t player_id() const; + void set_player_id(int64_t value_arg); + + bool is_looping() const; + void set_is_looping(bool value_arg); + + private: + static LoopingMessage FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class VideoPlayerAvplayApi; + friend class VideoPlayerAvplayApiCodecSerializer; + int64_t player_id_; + bool is_looping_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class VolumeMessage { + public: + // Constructs an object setting all fields. + explicit VolumeMessage(int64_t player_id, double volume); + + int64_t player_id() const; + void set_player_id(int64_t value_arg); + + double volume() const; + void set_volume(double value_arg); + + private: + static VolumeMessage FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class VideoPlayerAvplayApi; + friend class VideoPlayerAvplayApiCodecSerializer; + int64_t player_id_; + double volume_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class PlaybackSpeedMessage { + public: + // Constructs an object setting all fields. + explicit PlaybackSpeedMessage(int64_t player_id, double speed); + + int64_t player_id() const; + void set_player_id(int64_t value_arg); + + double speed() const; + void set_speed(double value_arg); + + private: + static PlaybackSpeedMessage FromEncodableList( + const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class VideoPlayerAvplayApi; + friend class VideoPlayerAvplayApiCodecSerializer; + int64_t player_id_; + double speed_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class TrackMessage { + public: + // Constructs an object setting all fields. + explicit TrackMessage(int64_t player_id, + const flutter::EncodableList& tracks); + + int64_t player_id() const; + void set_player_id(int64_t value_arg); + + const flutter::EncodableList& tracks() const; + void set_tracks(const flutter::EncodableList& value_arg); + + private: + static TrackMessage FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class VideoPlayerAvplayApi; + friend class VideoPlayerAvplayApiCodecSerializer; + int64_t player_id_; + flutter::EncodableList tracks_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class TrackTypeMessage { + public: + // Constructs an object setting all fields. + explicit TrackTypeMessage(int64_t player_id, const std::string& track_type); + + int64_t player_id() const; + void set_player_id(int64_t value_arg); + + const std::string& track_type() const; + void set_track_type(std::string_view value_arg); + + private: + static TrackTypeMessage FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class VideoPlayerAvplayApi; + friend class VideoPlayerAvplayApiCodecSerializer; + int64_t player_id_; + std::string track_type_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class SelectedTracksMessage { + public: + // Constructs an object setting all fields. + explicit SelectedTracksMessage(int64_t player_id, int64_t track_id, + const std::string& track_type); + + int64_t player_id() const; + void set_player_id(int64_t value_arg); + + int64_t track_id() const; + void set_track_id(int64_t value_arg); + + const std::string& track_type() const; + void set_track_type(std::string_view value_arg); + + private: + static SelectedTracksMessage FromEncodableList( + const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class VideoPlayerAvplayApi; + friend class VideoPlayerAvplayApiCodecSerializer; + int64_t player_id_; + int64_t track_id_; + std::string track_type_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class PositionMessage { + public: + // Constructs an object setting all fields. + explicit PositionMessage(int64_t player_id, int64_t position); + + int64_t player_id() const; + void set_player_id(int64_t value_arg); + + int64_t position() const; + void set_position(int64_t value_arg); + + private: + static PositionMessage FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class VideoPlayerAvplayApi; + friend class VideoPlayerAvplayApiCodecSerializer; + int64_t player_id_; + int64_t position_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class CreateMessage { + public: + // Constructs an object setting all non-nullable fields. + CreateMessage(); + + // Constructs an object setting all fields. + explicit CreateMessage(const std::string* asset, const std::string* uri, + const std::string* package_name, + const std::string* format_hint, + const flutter::EncodableMap* http_headers, + const flutter::EncodableMap* drm_configs, + const flutter::EncodableMap* player_options); + + const std::string* asset() const; + void set_asset(const std::string_view* value_arg); + void set_asset(std::string_view value_arg); + + const std::string* uri() const; + void set_uri(const std::string_view* value_arg); + void set_uri(std::string_view value_arg); + + const std::string* package_name() const; + void set_package_name(const std::string_view* value_arg); + void set_package_name(std::string_view value_arg); + + const std::string* format_hint() const; + void set_format_hint(const std::string_view* value_arg); + void set_format_hint(std::string_view value_arg); + + const flutter::EncodableMap* http_headers() const; + void set_http_headers(const flutter::EncodableMap* value_arg); + void set_http_headers(const flutter::EncodableMap& value_arg); + + const flutter::EncodableMap* drm_configs() const; + void set_drm_configs(const flutter::EncodableMap* value_arg); + void set_drm_configs(const flutter::EncodableMap& value_arg); + + const flutter::EncodableMap* player_options() const; + void set_player_options(const flutter::EncodableMap* value_arg); + void set_player_options(const flutter::EncodableMap& value_arg); + + private: + static CreateMessage FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class VideoPlayerAvplayApi; + friend class VideoPlayerAvplayApiCodecSerializer; + std::optional asset_; + std::optional uri_; + std::optional package_name_; + std::optional format_hint_; + std::optional http_headers_; + std::optional drm_configs_; + std::optional player_options_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class MixWithOthersMessage { + public: + // Constructs an object setting all fields. + explicit MixWithOthersMessage(bool mix_with_others); + + bool mix_with_others() const; + void set_mix_with_others(bool value_arg); + + private: + static MixWithOthersMessage FromEncodableList( + const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class VideoPlayerAvplayApi; + friend class VideoPlayerAvplayApiCodecSerializer; + bool mix_with_others_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class GeometryMessage { + public: + // Constructs an object setting all fields. + explicit GeometryMessage(int64_t player_id, int64_t x, int64_t y, + int64_t width, int64_t height); + + int64_t player_id() const; + void set_player_id(int64_t value_arg); + + int64_t x() const; + void set_x(int64_t value_arg); + + int64_t y() const; + void set_y(int64_t value_arg); + + int64_t width() const; + void set_width(int64_t value_arg); + + int64_t height() const; + void set_height(int64_t value_arg); + + private: + static GeometryMessage FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class VideoPlayerAvplayApi; + friend class VideoPlayerAvplayApiCodecSerializer; + int64_t player_id_; + int64_t x_; + int64_t y_; + int64_t width_; + int64_t height_; +}; + +class VideoPlayerAvplayApiCodecSerializer + : public flutter::StandardCodecSerializer { + public: + VideoPlayerAvplayApiCodecSerializer(); + inline static VideoPlayerAvplayApiCodecSerializer& GetInstance() { + static VideoPlayerAvplayApiCodecSerializer sInstance; + return sInstance; + } + + void WriteValue(const flutter::EncodableValue& value, + flutter::ByteStreamWriter* stream) const override; + + protected: + flutter::EncodableValue ReadValueOfType( + uint8_t type, flutter::ByteStreamReader* stream) const override; +}; + +// Generated interface from Pigeon that represents a handler of messages from +// Flutter. +class VideoPlayerAvplayApi { + public: + VideoPlayerAvplayApi(const VideoPlayerAvplayApi&) = delete; + VideoPlayerAvplayApi& operator=(const VideoPlayerAvplayApi&) = delete; + virtual ~VideoPlayerAvplayApi() {} + virtual std::optional Initialize() = 0; + virtual ErrorOr Create(const CreateMessage& msg) = 0; + virtual std::optional Dispose(const PlayerMessage& msg) = 0; + virtual std::optional SetLooping(const LoopingMessage& msg) = 0; + virtual std::optional SetVolume(const VolumeMessage& msg) = 0; + virtual std::optional SetPlaybackSpeed( + const PlaybackSpeedMessage& msg) = 0; + virtual std::optional Play(const PlayerMessage& msg) = 0; + virtual ErrorOr SetDeactivate(const PlayerMessage& msg) = 0; + virtual ErrorOr SetActivate(const PlayerMessage& msg) = 0; + virtual ErrorOr Track(const TrackTypeMessage& msg) = 0; + virtual ErrorOr SetTrackSelection(const SelectedTracksMessage& msg) = 0; + virtual ErrorOr Position(const PlayerMessage& msg) = 0; + virtual void SeekTo( + const PositionMessage& msg, + std::function reply)> result) = 0; + virtual std::optional Pause(const PlayerMessage& msg) = 0; + virtual std::optional SetMixWithOthers( + const MixWithOthersMessage& msg) = 0; + virtual std::optional SetDisplayGeometry( + const GeometryMessage& msg) = 0; + + // The codec used by VideoPlayerAvplayApi. + static const flutter::StandardMessageCodec& GetCodec(); + // Sets up an instance of `VideoPlayerAvplayApi` to handle messages through + // the `binary_messenger`. + static void SetUp(flutter::BinaryMessenger* binary_messenger, + VideoPlayerAvplayApi* api); + static flutter::EncodableValue WrapError(std::string_view error_message); + static flutter::EncodableValue WrapError(const FlutterError& error); + + protected: + VideoPlayerAvplayApi() = default; +}; +#endif // PIGEON_MESSAGES_H_ diff --git a/packages/video_player_avplay/tizen/src/plus_player.cc b/packages/video_player_avplay/tizen/src/plus_player.cc new file mode 100644 index 000000000..27bec246d --- /dev/null +++ b/packages/video_player_avplay/tizen/src/plus_player.cc @@ -0,0 +1,736 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "plus_player.h" + +#include +#include + +#include + +#include "log.h" + +static std::vector split(const std::string &s, char delim) { + std::stringstream ss(s); + std::string item; + std::vector tokens; + while (getline(ss, item, delim)) { + tokens.push_back(item); + } + return tokens; +} + +static plusplayer::TrackType ConvertTrackType(std::string track_type) { + if (track_type == "video") { + return plusplayer::TrackType::kTrackTypeVideo; + } + if (track_type == "audio") { + return plusplayer::TrackType::kTrackTypeAudio; + } + if (track_type == "text") { + return plusplayer::TrackType::kTrackTypeSubtitle; + } +} + +PlusPlayer::PlusPlayer(flutter::BinaryMessenger *messenger, void *native_window, + std::string &video_format) + : VideoPlayer(messenger), + native_window_(native_window), + video_format_(video_format) {} + +PlusPlayer::~PlusPlayer() { Dispose(); } + +void PlusPlayer::RegisterListener() { + listener_.buffering_callback = OnBufferStatus; + listener_.adaptive_streaming_control_callback = + OnAdaptiveStreamingControlEvent; + listener_.completed_callback = OnEos; + listener_.drm_init_data_callback = OnDrmInitData; + listener_.error_callback = OnError; + listener_.error_message_callback = OnErrorMsg; + listener_.prepared_callback = OnPrepareDone; + listener_.seek_completed_callback = OnSeekDone; + listener_.subtitle_data_callback = OnSubtitleData; + ::RegisterListener(player_, &listener_, this); +} + +int64_t PlusPlayer::Create(const std::string &uri, int drm_type, + const std::string &license_server_url, + bool is_prebuffer_mode, + flutter::EncodableMap &http_headers) { + LOG_INFO("[PlusPlayer] Create player."); + + if (video_format_ == "dash") { + player_ = CreatePlayer(plusplayer::PlayerType::kDASH); + } else { + player_ = CreatePlayer(plusplayer::PlayerType::kDefault); + } + + if (!player_) { + LOG_ERROR("[PlusPlayer] Fail to create player."); + return -1; + } + + if (!http_headers.empty()) { + auto iter = http_headers.find(flutter::EncodableValue("Cookie")); + if (iter != http_headers.end()) { + if (std::holds_alternative(iter->second)) { + std::string cookie = std::get(iter->second); + SetStreamingProperty(player_, "COOKIE", cookie); + } + } + + iter = http_headers.find(flutter::EncodableValue("User-Agent")); + if (iter != http_headers.end()) { + if (std::holds_alternative(iter->second)) { + std::string user_agent = std::get(iter->second); + SetStreamingProperty(player_, "USER_AGENT", user_agent); + } + } + } + + if (!Open(player_, uri)) { + LOG_ERROR("[PlusPlayer] Fail to open uri : %s.", uri.c_str()); + return -1; + } + LOG_INFO("[PlusPlayer] Uri: %s", uri.c_str()); + + char *appId = nullptr; + int ret = app_manager_get_app_id(getpid(), &appId); + if (ret != APP_MANAGER_ERROR_NONE) { + LOG_ERROR("[PlusPlayer] Fail to get app id: %s.", get_error_message(ret)); + return -1; + } + SetAppId(player_, std::string(appId)); + free(appId); + + RegisterListener(); + + if (drm_type != 0) { + if (!SetDrm(uri, drm_type, license_server_url)) { + LOG_ERROR("[PlusPlayer] Fail to set drm."); + return -1; + } + } + + if (!SetDisplay()) { + LOG_ERROR("[PlusPlayer] Fail to set display."); + return -1; + } + + SetDisplayRoi(0, 0, 1, 1); + + if (is_prebuffer_mode) { + SetPrebufferMode(player_, true); + is_prebuffer_mode_ = true; + } + + if (!PrepareAsync(player_)) { + LOG_ERROR("[PlusPlayer] Player fail to prepare."); + return -1; + } + return SetUpEventChannel(); +} + +void PlusPlayer::Dispose() { + LOG_INFO("[PlusPlayer] Player disposing."); + + if (!player_) { + LOG_ERROR("[PlusPlayer] Player not created."); + return; + } + if (!Stop(player_)) { + LOG_INFO("[PlusPlayer] Player fail to stop."); + return; + } + + plusplayer::State state = GetState(player_); + if (state == plusplayer::State::kIdle || state == plusplayer::State::kNone) { + if (!Close(player_)) { + LOG_INFO("[PlusPlayer] Player fail to close."); + return; + } + } + UnregisterListener(player_); + DestroyPlayer(player_); + player_ = nullptr; + + if (drm_manager_) { + drm_manager_->ReleaseDrmSession(); + } +} + +void PlusPlayer::SetDisplayRoi(int32_t x, int32_t y, int32_t width, + int32_t height) { + plusplayer::Geometry roi; + roi.x = x; + roi.y = y; + roi.w = width; + roi.h = height; + if (!::SetDisplayRoi(player_, roi)) { + LOG_ERROR("[PlusPlayer] Player fail to set display roi."); + } +} + +bool PlusPlayer::Play() { + LOG_INFO("[PlusPlayer] Player starting."); + + plusplayer::State state = GetState(player_); + if (state < plusplayer::State::kTrackSourceReady) { + LOG_ERROR("[PlusPlayer] Player is not ready."); + return false; + } + + if (state <= plusplayer::State::kReady) { + if (!Start(player_)) { + LOG_ERROR("[PlusPlayer] Player fail to start."); + return false; + } + return true; + } else if (state == plusplayer::State::kPaused) { + if (!Resume(player_)) { + LOG_ERROR("[PlusPlayer] Player fail to resume."); + return false; + } + return true; + } + return false; +} + +bool PlusPlayer::Activate() { + if (!::Activate(player_, plusplayer::kTrackTypeVideo)) { + LOG_ERROR("[PlusPlayer] Fail to activate video."); + return false; + } + if (!::Activate(player_, plusplayer::kTrackTypeAudio)) { + LOG_ERROR("[PlusPlayer] Fail to activate audio."); + return false; + } + if (!::Activate(player_, plusplayer::kTrackTypeSubtitle)) { + LOG_ERROR("[PlusPlayer] Fail to activate subtitle."); + } + + return true; +} + +bool PlusPlayer::Deactivate() { + if (is_prebuffer_mode_) { + Stop(player_); + return true; + } + + if (!::Deactivate(player_, plusplayer::kTrackTypeVideo)) { + LOG_ERROR("[PlusPlayer] Fail to activate video."); + return false; + } + if (!::Deactivate(player_, plusplayer::kTrackTypeAudio)) { + LOG_ERROR("[PlusPlayer] Fail to activate audio."); + return false; + } + if (!::Deactivate(player_, plusplayer::kTrackTypeSubtitle)) { + LOG_ERROR("[PlusPlayer] Fail to activate subtitle."); + } + + return true; +} + +bool PlusPlayer::Pause() { + LOG_INFO("[PlusPlayer] Player pausing."); + + plusplayer::State state = GetState(player_); + if (state < plusplayer::State::kReady) { + LOG_ERROR("[PlusPlayer] Player is not ready."); + return false; + } + + if (state != plusplayer::State::kPlaying) { + LOG_INFO("[PlusPlayer] Player not playing."); + return false; + } + + if (!::Pause(player_)) { + LOG_ERROR("[PlusPlayer] Player fail to pause."); + return false; + } + + return true; +} + +bool PlusPlayer::SetLooping(bool is_looping) { + LOG_ERROR("[PlusPlayer] Not support to set looping."); + return false; +} + +bool PlusPlayer::SetVolume(double volume) { + LOG_INFO("[PlusPlayer] Volume: %f", volume); + + if (GetState(player_) != plusplayer::State::kPlaying || + GetState(player_) != plusplayer::State::kPaused) { + LOG_ERROR("[PlusPlayer] Player is in invalid state"); + return false; + } + + if (!::SetVolume(player_, volume)) { + LOG_ERROR("[PlusPlayer] Fail to set volume."); + return false; + } + return true; +} + +bool PlusPlayer::SetPlaybackSpeed(double speed) { + LOG_INFO("[PlusPlayer] Speed: %f", speed); + + if (GetState(player_) <= plusplayer::State::kIdle) { + LOG_ERROR("[PlusPlayer] Player is not prepared."); + return false; + } + if (!SetPlaybackRate(player_, speed)) { + LOG_ERROR("[PlusPlayer] Player fail to set playback rate."); + return false; + } + return true; +} + +bool PlusPlayer::SeekTo(int64_t position, SeekCompletedCallback callback) { + LOG_INFO("[PlusPlayer] Seek to position: %lld", position); + + if (GetState(player_) < plusplayer::State::kReady) { + LOG_ERROR("[PlusPlayer] Player is not ready."); + return false; + } + + if (on_seek_completed_) { + LOG_ERROR("[PlusPlayer] Player is already seeking."); + return false; + } + + on_seek_completed_ = std::move(callback); + plusplayer::PlayerMemento memento; + if (!GetMemento(player_, &memento)) { + LOG_ERROR("[PlusPlayer] Player fail to get memento."); + } + + if (memento.is_live) { + std::string str = GetStreamingProperty(player_, "GET_LIVE_DURATION"); + if (str.empty()) { + LOG_ERROR("[PlusPlayer] Player fail to get live duration."); + return false; + } + std::vector time_str = split(str, '|'); + int64_t start_time = std::stoll(time_str[0].c_str()); + int64_t end_time = std::stoll(time_str[1].c_str()); + + if (position < start_time || position > end_time) { + on_seek_completed_ = nullptr; + LOG_ERROR("[PlusPlayer] Position out of range."); + return false; + } + + if (!Seek(player_, position)) { + on_seek_completed_ = nullptr; + LOG_ERROR("[PlusPlayer] Player fail to seek."); + return false; + } + } else { + if (!Seek(player_, position)) { + on_seek_completed_ = nullptr; + LOG_ERROR("[PlusPlayer] Player fail to seek."); + return false; + } + } + return true; +} + +int64_t PlusPlayer::GetPosition() { + uint64_t position = 0; + plusplayer::State state = GetState(player_); + if (state == plusplayer::State::kPlaying || + state == plusplayer::State::kPaused) { + if (!GetPlayingTime(player_, &position)) { + LOG_ERROR("[PlusPlayer] Player fail to get the current playing time."); + } + } + return static_cast(position); +} + +int64_t PlusPlayer::GetDuration() { + int64_t duration = 0; + if (GetState(player_) >= plusplayer::State::kTrackSourceReady) { + plusplayer::PlayerMemento memento; + if (!GetMemento(player_, &memento)) { + LOG_ERROR("[PlusPlayer] Player fail to get memento."); + } + + if (memento.is_live) { + std::string str = GetStreamingProperty(player_, "GET_LIVE_DURATION"); + if (str.empty()) { + LOG_ERROR("[PlusPlayer] Player fail to get live duration."); + return duration; + } + std::vector time_str = split(str, '|'); + int64_t start_time = std::stoll(time_str[0].c_str()); + int64_t end_time = std::stoll(time_str[1].c_str()); + + duration = end_time - start_time; + } else { + if (!::GetDuration(player_, &duration)) { + LOG_ERROR("[PlusPlayer] Player fail to get the duration."); + } + } + } + + LOG_INFO("[PlusPlayer] Video duration: %lld.", duration); + return duration; +} + +void PlusPlayer::GetVideoSize(int32_t *width, int32_t *height) { + if (GetState(player_) >= plusplayer::State::kTrackSourceReady) { + bool found = false; + std::vector tracks = GetActiveTrackInfo(player_); + for (auto track : tracks) { + if (track.type == plusplayer::TrackType::kTrackTypeVideo) { + *width = track.width; + *height = track.height; + found = true; + break; + } + } + if (!found) { + LOG_ERROR("[PlusPlayer] Player fail to get video size."); + } else { + LOG_INFO("[PlusPlayer] Video width: %d, height: %d.", *width, *height); + } + } +} + +bool PlusPlayer::IsReady() { + return plusplayer::State::kReady == GetState(player_); +} + +bool PlusPlayer::SetDisplay() { + int x = 0, y = 0, width = 0, height = 0; + ecore_wl2_window_proxy_->ecore_wl2_window_geometry_get(native_window_, &x, &y, + &width, &height); + int surface_id = + ecore_wl2_window_proxy_->ecore_wl2_window_surface_id_get(native_window_); + if (surface_id < 0) { + LOG_ERROR("[PlusPlayer] Fail to get surface id."); + return false; + } + bool ret = ::SetDisplay(player_, plusplayer::DisplayType::kOverlay, + surface_id, x, y, width, height); + if (!ret) { + LOG_ERROR("[PlusPlayer] Player fail to set display."); + return false; + } + + ret = SetDisplayMode(player_, plusplayer::DisplayMode::kDstRoi); + if (!ret) { + LOG_ERROR("[PlusPlayer] Player fail to set display mode."); + return false; + } + + return true; +} + +flutter::EncodableList PlusPlayer::GetTrackInfo(std::string track_type) { + if (!player_) { + LOG_ERROR("[PlusPlayer] Player not created."); + return {}; + } + + plusplayer::State state = GetState(player_); + if (state < plusplayer::State::kTrackSourceReady) { + LOG_ERROR("[PlusPlayer] Player is in invalid state."); + return {}; + } + + plusplayer::TrackType type = ConvertTrackType(track_type); + + int track_count = GetTrackCount(player_, type); + if (track_count <= 0) { + return {}; + } + + const std::vector track_info = ::GetTrackInfo(player_); + if (track_info.empty()) { + return {}; + } + + flutter::EncodableList trackSelections = {}; + flutter::EncodableMap trackSelection = {}; + trackSelection.insert( + {flutter::EncodableValue("trackType"), flutter::EncodableValue(type)}); + if (type == plusplayer::TrackType::kTrackTypeVideo) { + LOG_INFO("[PlusPlayer] Video track count: %d", track_count); + for (const auto &track : track_info) { + if (track.type == plusplayer::kTrackTypeVideo) { + trackSelection.insert_or_assign(flutter::EncodableValue("trackId"), + flutter::EncodableValue(track.index)); + trackSelection.insert_or_assign(flutter::EncodableValue("width"), + flutter::EncodableValue(track.width)); + trackSelection.insert_or_assign(flutter::EncodableValue("height"), + flutter::EncodableValue(track.height)); + trackSelection.insert_or_assign(flutter::EncodableValue("bitrate"), + flutter::EncodableValue(track.bitrate)); + LOG_INFO( + "[PlusPlayer] video track info[%d]: width[%d], height[%d], " + "bitrate[%d]", + track.index, track.width, track.height, track.bitrate); + + trackSelections.push_back(flutter::EncodableValue(trackSelection)); + } + } + } else if (type == plusplayer::TrackType::kTrackTypeAudio) { + LOG_INFO("[PlusPlayer] Audio track count: %d", track_count); + for (const auto &track : track_info) { + if (track.type == plusplayer::kTrackTypeAudio) { + trackSelection.insert_or_assign(flutter::EncodableValue("trackId"), + flutter::EncodableValue(track.index)); + trackSelection.insert_or_assign( + flutter::EncodableValue("language"), + flutter::EncodableValue(track.language_code)); + trackSelection.insert_or_assign( + flutter::EncodableValue("channel"), + flutter::EncodableValue(track.channels)); + trackSelection.insert_or_assign(flutter::EncodableValue("bitrate"), + flutter::EncodableValue(track.bitrate)); + LOG_INFO( + "[PlusPlayer] Audio track info[%d]: language[%s], channel[%d], " + "sample_rate[%d], bitrate[%d]", + track.index, track.language_code.c_str(), track.channels, + track.sample_rate, track.bitrate); + + trackSelections.push_back(flutter::EncodableValue(trackSelection)); + } + } + } else if (type == plusplayer::TrackType::kTrackTypeSubtitle) { + LOG_INFO("[PlusPlayer] Subtitle track count: %d", track_count); + for (const auto &track : track_info) { + if (track.type == plusplayer::kTrackTypeSubtitle) { + trackSelection.insert_or_assign(flutter::EncodableValue("trackId"), + flutter::EncodableValue(track.index)); + trackSelection.insert_or_assign( + flutter::EncodableValue("language"), + flutter::EncodableValue(track.language_code)); + LOG_INFO("[PlusPlayer] Subtitle track info[%d]: language[%s]", + track.index, track.language_code.c_str()); + + trackSelections.push_back(flutter::EncodableValue(trackSelection)); + } + } + } + + return trackSelections; +} + +bool PlusPlayer::SetTrackSelection(int32_t track_id, std::string track_type) { + LOG_INFO("[PlusPlayer] Track id is: %d,track type is: %s", track_id, + track_type.c_str()); + + if (!player_) { + LOG_ERROR("[PlusPlayer] Player not created."); + return false; + } + + plusplayer::State state = GetState(player_); + if (state < plusplayer::State::kTrackSourceReady) { + LOG_ERROR("[PlusPlayer] Player is in invalid state."); + return false; + } + + if (!SelectTrack(player_, ConvertTrackType(track_type), track_id)) { + LOG_ERROR("[PlusPlayer] Player fail to select track."); + return false; + } + return true; +} + +bool PlusPlayer::SetDrm(const std::string &uri, int drm_type, + const std::string &license_server_url) { + drm_manager_ = std::make_unique(); + if (!drm_manager_->CreateDrmSession(drm_type, true)) { + LOG_ERROR("[PlusPlayer] Fail to create drm session."); + return false; + } + + int drm_handle = 0; + if (!drm_manager_->GetDrmHandle(&drm_handle)) { + LOG_ERROR("[PlusPlayer] Fail to get drm handle."); + return false; + } + + plusplayer::drm::Type type; + switch (drm_type) { + case DrmManager::DrmType::DRM_TYPE_PLAYREADAY: + type = plusplayer::drm::Type::kPlayready; + break; + case DrmManager::DrmType::DRM_TYPE_WIDEVINECDM: + type = plusplayer::drm::Type::kWidevineCdm; + break; + default: + type = plusplayer::drm::Type::kNone; + break; + } + + plusplayer::drm::Property property; + property.handle = drm_handle; + property.type = type; + property.license_acquired_cb = + reinterpret_cast(OnLicenseAcquired); + property.license_acquired_userdata = + reinterpret_cast(this); + property.external_decryption = false; + ::SetDrm(player_, property); + + if (license_server_url.empty()) { + bool success = drm_manager_->SetChallenge(uri, binary_messenger_); + if (!success) { + LOG_ERROR("[PlusPlayer]Fail to set challenge."); + return false; + } + } else { + if (!drm_manager_->SetChallenge(uri, license_server_url)) { + LOG_ERROR("[PlusPlayer]Fail to set challenge."); + return false; + } + } + return true; +} + +bool PlusPlayer::OnLicenseAcquired(int *drm_handle, unsigned int length, + unsigned char *pssh_data, void *user_data) { + LOG_INFO("[PlusPlayer] License acquired."); + PlusPlayer *self = static_cast(user_data); + + if (self->drm_manager_) { + return self->drm_manager_->SecurityInitCompleteCB(drm_handle, length, + pssh_data, nullptr); + } + return false; +} + +void PlusPlayer::OnPrepareDone(bool ret, void *user_data) { + LOG_INFO("[PlusPlayer] Prepare done, result: %d.", ret); + PlusPlayer *self = reinterpret_cast(user_data); + + if (!self->is_initialized_ && ret) { + self->SendInitialized(); + } +} + +void PlusPlayer::OnBufferStatus(int percent, void *user_data) { + LOG_INFO("[PlusPlayer] Buffering percent: %d.", percent); + PlusPlayer *self = reinterpret_cast(user_data); + + if (percent == 100) { + self->SendBufferingEnd(); + self->is_buffering_ = false; + } else if (!self->is_buffering_ && percent <= 5) { + self->SendBufferingStart(); + self->is_buffering_ = true; + } else { + self->SendBufferingUpdate(percent); + } +} + +void PlusPlayer::OnSeekDone(void *user_data) { + LOG_INFO("[PlusPlayer] Seek completed."); + PlusPlayer *self = reinterpret_cast(user_data); + + if (self->on_seek_completed_) { + self->on_seek_completed_(); + self->on_seek_completed_ = nullptr; + } +} + +void PlusPlayer::OnEos(void *user_data) { + LOG_INFO("[PlusPlayer] Play completed."); + PlusPlayer *self = reinterpret_cast(user_data); + + self->SendPlayCompleted(); +} + +void PlusPlayer::OnSubtitleData(char *data, const int size, + const plusplayer::SubtitleType &type, + const uint64_t duration, void *user_data) { + LOG_INFO("[PlusPlayer] Subtitle updated, duration: %llu, text: %s", duration, + data); + PlusPlayer *self = reinterpret_cast(user_data); + + self->SendSubtitleUpdate(duration, data); +} + +void PlusPlayer::OnResourceConflicted(void *user_data) { + LOG_ERROR("[PlusPlayer] Resource conflicted."); + PlusPlayer *self = reinterpret_cast(user_data); + + self->SendError("PlusPlayer error", "Resource conflicted"); +} + +void PlusPlayer::OnError(const plusplayer::ErrorType &error_code, + void *user_data) { + LOG_ERROR("[PlusPlayer] Error code: %d", error_code); + PlusPlayer *self = reinterpret_cast(user_data); + + self->SendError("[PlusPlayer] OnError", ""); +} + +void PlusPlayer::OnErrorMsg(const plusplayer::ErrorType &error_code, + const char *error_msg, void *user_data) { + LOG_ERROR("[PlusPlayer] Error code: %d, message: %s.", error_code, error_msg); + PlusPlayer *self = reinterpret_cast(user_data); + + self->SendError("PlusPlayer error", error_msg); +} + +void PlusPlayer::OnDrmInitData(int *drm_handle, unsigned int len, + unsigned char *pssh_data, + plusplayer::TrackType type, void *user_data) { + LOG_INFO("[PlusPlayer] Drm init completed."); + PlusPlayer *self = reinterpret_cast(user_data); + + if (self->drm_manager_) { + if (self->drm_manager_->SecurityInitCompleteCB(drm_handle, len, pssh_data, + nullptr)) { + DrmLicenseAcquiredDone(self->player_, type); + } + } +} + +void PlusPlayer::OnAdaptiveStreamingControlEvent( + const plusplayer::StreamingMessageType &type, + const plusplayer::MessageParam &msg, void *user_data) { + LOG_INFO("[PlusPlayer] Message type: %d, is DrmInitData (%d)", type, + type == plusplayer::StreamingMessageType::kDrmInitData); + PlusPlayer *self = reinterpret_cast(user_data); + + if (type == plusplayer::StreamingMessageType::kDrmInitData) { + if (msg.data.empty() || 0 == msg.size) { + LOG_ERROR("[PlusPlayer] Empty message."); + return; + } + + if (self->drm_manager_) { + self->drm_manager_->UpdatePsshData(msg.data.data(), msg.size); + } + } +} + +void PlusPlayer::OnClosedCaptionData(std::unique_ptr data, + const int size, void *user_data) {} + +void PlusPlayer::OnCueEvent(const char *cue_data, void *user_data) {} + +void PlusPlayer::OnDateRangeEvent(const char *date_range_data, + void *user_data) {} + +void PlusPlayer::OnStopReachEvent(bool stop_reach, void *user_data) {} + +void PlusPlayer::OnCueOutContEvent(const char *cue_out_cont_data, + void *user_data) {} + +void PlusPlayer::OnChangeSourceDone(bool ret, void *user_data) {} + +void PlusPlayer::OnStateChangedToPlaying(void *user_data) {} diff --git a/packages/video_player_avplay/tizen/src/plus_player.h b/packages/video_player_avplay/tizen/src/plus_player.h new file mode 100644 index 000000000..399ef059b --- /dev/null +++ b/packages/video_player_avplay/tizen/src/plus_player.h @@ -0,0 +1,90 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_PLUS_PLAYER_H_ +#define FLUTTER_PLUGIN_PLUS_PLAYER_H_ + +#include + +#include +#include + +#include "drm_manager.h" +#include "plusplayer/plusplayer_wrapper.h" +#include "video_player.h" + +class PlusPlayer : public VideoPlayer { + public: + explicit PlusPlayer(flutter::BinaryMessenger *messenger, void *native_window, + std::string &video_format); + ~PlusPlayer(); + + int64_t Create(const std::string &uri, int drm_type, + const std::string &license_server_url, bool is_prebuffer_mode, + flutter::EncodableMap &http_headers) override; + void Dispose() override; + + void SetDisplayRoi(int32_t x, int32_t y, int32_t width, + int32_t height) override; + bool Play() override; + bool Deactivate() override; + bool Activate() override; + bool Pause() override; + bool SetLooping(bool is_looping) override; + bool SetVolume(double volume) override; + bool SetPlaybackSpeed(double speed) override; + bool SeekTo(int64_t position, SeekCompletedCallback callback) override; + int64_t GetPosition() override; + int64_t GetDuration() override; + void GetVideoSize(int32_t *width, int32_t *height) override; + bool IsReady() override; + flutter::EncodableList GetTrackInfo(std::string track_type) override; + bool SetTrackSelection(int32_t track_id, std::string track_type) override; + + private: + bool SetDisplay(); + bool SetDrm(const std::string &uri, int drm_type, + const std::string &license_server_url); + void RegisterListener(); + static bool OnLicenseAcquired(int *drm_handle, unsigned int length, + unsigned char *pssh_data, void *user_data); + + static void OnPrepareDone(bool ret, void *user_data); + static void OnBufferStatus(const int percent, void *user_data); + static void OnSeekDone(void *user_data); + static void OnEos(void *user_data); + static void OnSubtitleData(char *data, const int size, + const plusplayer::SubtitleType &type, + const uint64_t duration, void *user_data); + static void OnResourceConflicted(void *user_data); + static void OnError(const plusplayer::ErrorType &error_code, void *user_data); + static void OnErrorMsg(const plusplayer::ErrorType &error_code, + const char *error_msg, void *user_data); + static void OnDrmInitData(int *drm_andle, unsigned int len, + unsigned char *pssh_data, + plusplayer::TrackType type, void *user_data); + static void OnAdaptiveStreamingControlEvent( + const plusplayer::StreamingMessageType &type, + const plusplayer::MessageParam &msg, void *user_data); + static void OnClosedCaptionData(std::unique_ptr data, const int size, + void *user_data); + static void OnCueEvent(const char *cue_data, void *user_data); + static void OnDateRangeEvent(const char *date_range_data, void *user_data); + static void OnStopReachEvent(bool stop_reach, void *user_data); + static void OnCueOutContEvent(const char *cue_out_cont_data, void *user_data); + static void OnChangeSourceDone(bool ret, void *user_data); + static void OnStateChangedToPlaying(void *user_data); + + PlusplayerRef player_ = nullptr; + PlusplayerListener listener_; + std::unique_ptr drm_manager_; + + void *native_window_; + std::string video_format_; + bool is_buffering_ = false; + bool is_prebuffer_mode_ = false; + SeekCompletedCallback on_seek_completed_; +}; + +#endif // FLUTTER_PLUGIN_PLUS_PLAYER_H_ diff --git a/packages/video_player_avplay/tizen/src/video_player.cc b/packages/video_player_avplay/tizen/src/video_player.cc new file mode 100644 index 000000000..ef5ac6b8d --- /dev/null +++ b/packages/video_player_avplay/tizen/src/video_player.cc @@ -0,0 +1,162 @@ +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "video_player.h" + +#include +#include + +#include "log.h" + +static int64_t player_index = 1; + +VideoPlayer::VideoPlayer(flutter::BinaryMessenger *messenger) + : ecore_wl2_window_proxy_(std::make_unique()), + binary_messenger_(messenger) { + sink_event_pipe_ = ecore_pipe_add( + [](void *data, void *buffer, unsigned int nbyte) -> void { + auto *self = static_cast(data); + self->ExecuteSinkEvents(); + }, + this); +} + +VideoPlayer::~VideoPlayer() { + if (sink_event_pipe_) { + ecore_pipe_del(sink_event_pipe_); + } + event_sink_ = nullptr; + if (event_channel_) { + event_channel_->SetStreamHandler(nullptr); + } +} + +int64_t VideoPlayer::SetUpEventChannel() { + int64_t player_id = player_index++; + std::string channel_name = + "tizen/video_player/video_events_" + std::to_string(player_id); + auto channel = + std::make_unique>( + binary_messenger_, channel_name, + &flutter::StandardMethodCodec::GetInstance()); + auto handler = std::make_unique< + flutter::StreamHandlerFunctions>( + [&](const flutter::EncodableValue *arguments, + std::unique_ptr> &&events) + -> std::unique_ptr> { + event_sink_ = std::move(events); + if (IsReady()) { + SendInitialized(); + } else { + LOG_INFO("[VideoPlayer] Player is not ready."); + } + return nullptr; + }, + [&](const flutter::EncodableValue *arguments) + -> std::unique_ptr> { + event_sink_ = nullptr; + return nullptr; + }); + channel->SetStreamHandler(std::move(handler)); + event_channel_ = std::move(channel); + return player_id; +} + +void VideoPlayer::ExecuteSinkEvents() { + std::lock_guard lock(queue_mutex_); + while (!encodable_event_queue_.empty()) { + if (event_sink_) { + event_sink_->Success(encodable_event_queue_.front()); + } + encodable_event_queue_.pop(); + } + + while (!error_event_queue_.empty()) { + if (event_sink_) { + event_sink_->Error(error_event_queue_.front().first, + error_event_queue_.front().second); + } + error_event_queue_.pop(); + } +} + +void VideoPlayer::PushEvent(flutter::EncodableValue encodable_value) { + std::lock_guard lock(queue_mutex_); + if (event_sink_ == nullptr) { + LOG_ERROR("[VideoPlayer] event sink is nullptr."); + return; + } + encodable_event_queue_.push(encodable_value); + ecore_pipe_write(sink_event_pipe_, nullptr, 0); +} + +void VideoPlayer::SendInitialized() { + if (!is_initialized_ && event_sink_) { + int32_t width = 0, height = 0; + int64_t duration = GetDuration(); + GetVideoSize(&width, &height); + is_initialized_ = true; + flutter::EncodableMap result = { + {flutter::EncodableValue("event"), + flutter::EncodableValue("initialized")}, + {flutter::EncodableValue("duration"), + flutter::EncodableValue(duration)}, + {flutter::EncodableValue("width"), flutter::EncodableValue(width)}, + {flutter::EncodableValue("height"), flutter::EncodableValue(height)}, + }; + PushEvent(flutter::EncodableValue(result)); + } +} + +void VideoPlayer::SendBufferingStart() { + flutter::EncodableMap result = { + {flutter::EncodableValue("event"), + flutter::EncodableValue("bufferingStart")}, + }; + PushEvent(flutter::EncodableValue(result)); +} + +void VideoPlayer::SendBufferingUpdate(int32_t value) { + flutter::EncodableMap result = { + {flutter::EncodableValue("event"), + flutter::EncodableValue("bufferingUpdate")}, + {flutter::EncodableValue("value"), flutter::EncodableValue(value)}, + }; + PushEvent(flutter::EncodableValue(result)); +} + +void VideoPlayer::SendBufferingEnd() { + flutter::EncodableMap result = { + {flutter::EncodableValue("event"), + flutter::EncodableValue("bufferingEnd")}, + }; + PushEvent(flutter::EncodableValue(result)); +} + +void VideoPlayer::SendSubtitleUpdate(int32_t duration, + const std::string &text) { + flutter::EncodableMap result = { + {flutter::EncodableValue("event"), + flutter::EncodableValue("subtitleUpdate")}, + {flutter::EncodableValue("duration"), flutter::EncodableValue(duration)}, + {flutter::EncodableValue("text"), flutter::EncodableValue(text)}, + }; + PushEvent(flutter::EncodableValue(result)); +} + +void VideoPlayer::SendPlayCompleted() { + flutter::EncodableMap result = { + {flutter::EncodableValue("event"), flutter::EncodableValue("completed")}, + }; + PushEvent(flutter::EncodableValue(result)); +} + +void VideoPlayer::SendError(const std::string &error_code, + const std::string &error_message) { + if (event_sink_) { + std::lock_guard lock(queue_mutex_); + error_event_queue_.push(std::make_pair(error_code, error_message)); + ecore_pipe_write(sink_event_pipe_, nullptr, 0); + } +} diff --git a/packages/video_player_avplay/tizen/src/video_player.h b/packages/video_player_avplay/tizen/src/video_player.h new file mode 100644 index 000000000..7ea6a62e7 --- /dev/null +++ b/packages/video_player_avplay/tizen/src/video_player.h @@ -0,0 +1,82 @@ +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_VIDEO_PLAYER_H_ +#define FLUTTER_PLUGIN_VIDEO_PLAYER_H_ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "ecore_wl2_window_proxy.h" + +class VideoPlayer { + public: + using SeekCompletedCallback = std::function; + + explicit VideoPlayer(flutter::BinaryMessenger *messenger); + VideoPlayer(const VideoPlayer &) = delete; + VideoPlayer &operator=(const VideoPlayer &) = delete; + virtual ~VideoPlayer(); + + virtual int64_t Create(const std::string &uri, int drm_type, + const std::string &license_server_url, + bool is_prebuffer_mode, + flutter::EncodableMap &http_headers) = 0; + virtual void Dispose() = 0; + + virtual void SetDisplayRoi(int32_t x, int32_t y, int32_t width, + int32_t height) = 0; + virtual bool Play() = 0; + virtual bool Deactivate() { return false; }; + virtual bool Activate() { return false; }; + virtual bool Pause() = 0; + virtual bool SetLooping(bool is_looping) = 0; + virtual bool SetVolume(double volume) = 0; + virtual bool SetPlaybackSpeed(double speed) = 0; + virtual bool SeekTo(int64_t position, SeekCompletedCallback callback) = 0; + virtual int64_t GetPosition() = 0; + virtual int64_t GetDuration() = 0; + virtual bool IsReady() = 0; + virtual flutter::EncodableList GetTrackInfo(std::string track_type) = 0; + virtual bool SetTrackSelection(int32_t track_id, std::string track_type) = 0; + + protected: + virtual void GetVideoSize(int32_t *width, int32_t *height) = 0; + int64_t SetUpEventChannel(); + void SendInitialized(); + void SendBufferingStart(); + void SendBufferingUpdate(int32_t value); + void SendBufferingEnd(); + void SendSubtitleUpdate(int32_t duration, const std::string &text); + void SendPlayCompleted(); + void SendError(const std::string &error_code, + const std::string &error_message); + + std::mutex queue_mutex_; + std::unique_ptr ecore_wl2_window_proxy_ = nullptr; + flutter::BinaryMessenger *binary_messenger_; + + bool is_initialized_ = false; + + private: + void ExecuteSinkEvents(); + void PushEvent(flutter::EncodableValue encodable_value); + + std::queue encodable_event_queue_; + std::queue> error_event_queue_; + std::unique_ptr> + event_channel_; + std::unique_ptr> event_sink_; + Ecore_Pipe *sink_event_pipe_ = nullptr; +}; + +#endif // FLUTTER_PLUGIN_VIDEO_PLAYER_H_ diff --git a/packages/video_player_avplay/tizen/src/video_player_options.h b/packages/video_player_avplay/tizen/src/video_player_options.h new file mode 100644 index 000000000..ae2ffd7fb --- /dev/null +++ b/packages/video_player_avplay/tizen/src/video_player_options.h @@ -0,0 +1,25 @@ +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_VIDEO_PLAYER_OPTIONS_H_ +#define FLUTTER_PLUGIN_VIDEO_PLAYER_OPTIONS_H_ + +class VideoPlayerOptions { + public: + VideoPlayerOptions() {} + ~VideoPlayerOptions() = default; + + VideoPlayerOptions(const VideoPlayerOptions &other) = default; + VideoPlayerOptions &operator=(const VideoPlayerOptions &other) = default; + + void SetMixWithOthers(bool mix_with_others) { + mix_with_others_ = mix_with_others; + } + bool GetMixWithOthers() const { return mix_with_others_; } + + private: + bool mix_with_others_ = true; +}; + +#endif // FLUTTER_PLUGIN_VIDEO_PLAYER_OPTIONS_H_ diff --git a/packages/video_player_avplay/tizen/src/video_player_tizen_plugin.cc b/packages/video_player_avplay/tizen/src/video_player_tizen_plugin.cc new file mode 100644 index 000000000..e0d2d03f5 --- /dev/null +++ b/packages/video_player_avplay/tizen/src/video_player_tizen_plugin.cc @@ -0,0 +1,354 @@ +// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "video_player_tizen_plugin.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "media_player.h" +#include "messages.h" +#include "plus_player.h" +#include "video_player_options.h" + +namespace { + +class VideoPlayerTizenPlugin : public flutter::Plugin, + public VideoPlayerAvplayApi { + public: + static void RegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar_ref, + flutter::PluginRegistrar *plugin_registrar); + + VideoPlayerTizenPlugin(FlutterDesktopPluginRegistrarRef registrar_ref, + flutter::PluginRegistrar *plugin_registrar); + virtual ~VideoPlayerTizenPlugin(); + + std::optional Initialize() override; + ErrorOr Create(const CreateMessage &msg) override; + std::optional Dispose(const PlayerMessage &msg) override; + std::optional SetLooping(const LoopingMessage &msg) override; + std::optional SetVolume(const VolumeMessage &msg) override; + std::optional SetPlaybackSpeed( + const PlaybackSpeedMessage &msg) override; + ErrorOr Track(const TrackTypeMessage &msg) override; + ErrorOr SetTrackSelection(const SelectedTracksMessage &msg) override; + std::optional Play(const PlayerMessage &msg) override; + ErrorOr SetDeactivate(const PlayerMessage &msg) override; + ErrorOr SetActivate(const PlayerMessage &msg) override; + ErrorOr Position(const PlayerMessage &msg) override; + void SeekTo( + const PositionMessage &msg, + std::function reply)> result) override; + std::optional Pause(const PlayerMessage &msg) override; + std::optional SetMixWithOthers( + const MixWithOthersMessage &msg) override; + std::optional SetDisplayGeometry( + const GeometryMessage &msg) override; + + static VideoPlayer *FindPlayerById(int64_t player_id) { + auto iter = players_.find(player_id); + if (iter != players_.end()) { + return iter->second.get(); + } + return nullptr; + } + + private: + void DisposeAllPlayers(); + + FlutterDesktopPluginRegistrarRef registrar_ref_; + flutter::PluginRegistrar *plugin_registrar_; + VideoPlayerOptions options_; + + static inline std::map> players_; +}; + +void VideoPlayerTizenPlugin::RegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar_ref, + flutter::PluginRegistrar *plugin_registrar) { + auto plugin = + std::make_unique(registrar_ref, plugin_registrar); + plugin_registrar->AddPlugin(std::move(plugin)); +} + +VideoPlayerTizenPlugin::VideoPlayerTizenPlugin( + FlutterDesktopPluginRegistrarRef registrar_ref, + flutter::PluginRegistrar *plugin_registrar) + : registrar_ref_(registrar_ref), plugin_registrar_(plugin_registrar) { + VideoPlayerAvplayApi::SetUp(plugin_registrar->messenger(), this); +} + +VideoPlayerTizenPlugin::~VideoPlayerTizenPlugin() { DisposeAllPlayers(); } + +void VideoPlayerTizenPlugin::DisposeAllPlayers() { + for (const auto &[id, player] : players_) { + player->Dispose(); + } + players_.clear(); +} + +std::optional VideoPlayerTizenPlugin::Initialize() { + DisposeAllPlayers(); + return std::nullopt; +} + +ErrorOr VideoPlayerTizenPlugin::Create( + const CreateMessage &msg) { + FlutterDesktopViewRef flutter_view = + FlutterDesktopPluginRegistrarGetView(registrar_ref_); + if (!flutter_view) { + return FlutterError("Operation failed", "Could not get a Flutter view."); + } + void *native_window = FlutterDesktopViewGetNativeHandle(flutter_view); + if (!native_window) { + return FlutterError("Operation failed", + "Could not get a native window handle."); + } + + std::string uri; + int32_t drm_type = 0; // DRM_TYPE_NONE + std::string license_server_url; + bool prebuffer_mode; + std::string format; + flutter::EncodableMap http_headers = {}; + + if (msg.asset() && !msg.asset()->empty()) { + char *res_path = app_get_resource_path(); + if (res_path) { + uri = uri + res_path + "flutter_assets/" + *msg.asset(); + free(res_path); + } else { + return FlutterError("Internal error", "Failed to get resource path."); + } + } else if (msg.uri() && !msg.uri()->empty()) { + uri = *msg.uri(); + if (msg.format_hint() && !msg.format_hint()->empty()) { + format = *msg.format_hint(); + } + + const flutter::EncodableMap *drm_configs = msg.drm_configs(); + if (drm_configs) { + auto iter = drm_configs->find(flutter::EncodableValue("drmType")); + if (iter != drm_configs->end()) { + if (std::holds_alternative(iter->second)) { + drm_type = std::get(iter->second); + } + } + iter = drm_configs->find(flutter::EncodableValue("licenseServerUrl")); + if (iter != drm_configs->end()) { + if (std::holds_alternative(iter->second)) { + license_server_url = std::get(iter->second); + } + } + } + + const flutter::EncodableMap *player_options = msg.player_options(); + if (player_options) { + auto iter = + player_options->find(flutter::EncodableValue("prebufferMode")); + if (iter != player_options->end()) { + if (std::holds_alternative(iter->second)) { + prebuffer_mode = std::get(iter->second); + } + } + } + + const flutter::EncodableMap *http_headers_map = msg.http_headers(); + if (http_headers_map) { + http_headers = *http_headers_map; + } + + } else { + return FlutterError("Invalid argument", "Either asset or uri must be set."); + } + + int64_t player_id = 0; + if (uri.substr(0, 4) == "http") { + auto player = std::make_unique(plugin_registrar_->messenger(), + native_window, format); + player_id = player->Create(uri, drm_type, license_server_url, + prebuffer_mode, http_headers); + if (player_id == -1) { + return FlutterError("Operation failed", "Failed to create a player."); + } + players_[player_id] = std::move(player); + } else { + auto player = std::make_unique(plugin_registrar_->messenger(), + native_window); + player_id = player->Create(uri, drm_type, license_server_url, + prebuffer_mode, http_headers); + if (player_id == -1) { + return FlutterError("Operation failed", "Failed to create a player."); + } + players_[player_id] = std::move(player); + } + + PlayerMessage result(player_id); + return result; +} + +std::optional VideoPlayerTizenPlugin::Dispose( + const PlayerMessage &msg) { + auto iter = players_.find(msg.player_id()); + if (iter != players_.end()) { + iter->second->Dispose(); + players_.erase(iter); + } + return std::nullopt; +} + +std::optional VideoPlayerTizenPlugin::SetLooping( + const LoopingMessage &msg) { + VideoPlayer *player = FindPlayerById(msg.player_id()); + if (!player) { + return FlutterError("Invalid argument", "Player not found"); + } + if (!player->SetLooping(msg.is_looping())) { + return FlutterError("SetLooping", "Player set looping failed"); + } + return std::nullopt; +} + +std::optional VideoPlayerTizenPlugin::SetVolume( + const VolumeMessage &msg) { + VideoPlayer *player = FindPlayerById(msg.player_id()); + if (!player) { + return FlutterError("Invalid argument", "Player not found"); + } + if (!player->SetVolume(msg.volume())) { + return FlutterError("SetVolume", "Player set volume failed"); + } + return std::nullopt; +} + +std::optional VideoPlayerTizenPlugin::SetPlaybackSpeed( + const PlaybackSpeedMessage &msg) { + VideoPlayer *player = FindPlayerById(msg.player_id()); + if (!player) { + return FlutterError("Invalid argument", "Player not found"); + } + if (!player->SetPlaybackSpeed(msg.speed())) { + return FlutterError("SetPlaybackSpeed", "Player set playback speed failed"); + } + return std::nullopt; +} + +ErrorOr VideoPlayerTizenPlugin::Track( + const TrackTypeMessage &msg) { + VideoPlayer *player = FindPlayerById(msg.player_id()); + + if (!player) { + return FlutterError("Invalid argument", "Player not found"); + } + + TrackMessage result(msg.player_id(), player->GetTrackInfo(msg.track_type())); + return result; +} + +ErrorOr VideoPlayerTizenPlugin::SetTrackSelection( + const SelectedTracksMessage &msg) { + VideoPlayer *player = FindPlayerById(msg.player_id()); + if (!player) { + return FlutterError("Invalid argument", "Player not found"); + } + return player->SetTrackSelection(msg.track_id(), msg.track_type()); +} + +std::optional VideoPlayerTizenPlugin::Play( + const PlayerMessage &msg) { + VideoPlayer *player = FindPlayerById(msg.player_id()); + if (!player) { + return FlutterError("Invalid argument", "Player not found"); + } + if (!player->Play()) { + return FlutterError("Play", "Player play failed"); + } + return std::nullopt; +} + +ErrorOr VideoPlayerTizenPlugin::SetDeactivate(const PlayerMessage &msg) { + VideoPlayer *player = FindPlayerById(msg.player_id()); + if (!player) { + return FlutterError("Invalid argument", "Player not found"); + } + return player->Deactivate(); +} + +ErrorOr VideoPlayerTizenPlugin::SetActivate(const PlayerMessage &msg) { + VideoPlayer *player = FindPlayerById(msg.player_id()); + if (!player) { + return FlutterError("Invalid argument", "Player not found"); + } + return player->Activate(); +} + +std::optional VideoPlayerTizenPlugin::Pause( + const PlayerMessage &msg) { + VideoPlayer *player = FindPlayerById(msg.player_id()); + if (!player) { + return FlutterError("Invalid argument", "Player not found"); + } + if (!player->Pause()) { + return FlutterError("Pause", "Player pause failed"); + } + return std::nullopt; +} + +ErrorOr VideoPlayerTizenPlugin::Position( + const PlayerMessage &msg) { + VideoPlayer *player = FindPlayerById(msg.player_id()); + if (!player) { + return FlutterError("Invalid argument", "Player not found"); + } + PositionMessage result(msg.player_id(), player->GetPosition()); + return result; +} + +void VideoPlayerTizenPlugin::SeekTo( + const PositionMessage &msg, + std::function reply)> result) { + VideoPlayer *player = FindPlayerById(msg.player_id()); + if (!player) { + result(FlutterError("Invalid argument", "Player not found")); + return; + } + if (!player->SeekTo(msg.position(), + [result]() -> void { result(std::nullopt); })) { + result(FlutterError("SeekTo", "Player seek to failed")); + } +} + +std::optional VideoPlayerTizenPlugin::SetDisplayGeometry( + const GeometryMessage &msg) { + VideoPlayer *player = FindPlayerById(msg.player_id()); + if (!player) { + return FlutterError("Invalid argument", "Player not found"); + } + player->SetDisplayRoi(msg.x(), msg.y(), msg.width(), msg.height()); + return std::nullopt; +} + +std::optional VideoPlayerTizenPlugin::SetMixWithOthers( + const MixWithOthersMessage &msg) { + options_.SetMixWithOthers(msg.mix_with_others()); + return std::nullopt; +} + +} // namespace + +void VideoPlayerTizenPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + VideoPlayerTizenPlugin::RegisterWithRegistrar( + registrar, flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} diff --git a/packages/video_player_videohole/CHANGELOG.md b/packages/video_player_videohole/CHANGELOG.md index a9563e341..07752697b 100644 --- a/packages/video_player_videohole/CHANGELOG.md +++ b/packages/video_player_videohole/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.2.0 + +* Implement functionality of selecting video, audio and text tracks. + +## 0.1.3 + +* Fix event channel issue, sending messages from native to Flutter on the platform thread. + ## 0.1.2 * Increase the minimum Flutter version to 3.3. diff --git a/packages/video_player_videohole/README.md b/packages/video_player_videohole/README.md index 15bda9318..252c1cd97 100644 --- a/packages/video_player_videohole/README.md +++ b/packages/video_player_videohole/README.md @@ -12,7 +12,7 @@ To use this package, add `video_player_videohole` as a dependency in your `pubsp ```yaml dependencies: - video_player_videohole: ^0.1.1 + video_player_videohole: ^0.2.0 ``` Then you can import `video_player_videohole` in your Dart code: diff --git a/packages/video_player_videohole/example/lib/main.dart b/packages/video_player_videohole/example/lib/main.dart index 24f6a7c90..873360c4d 100644 --- a/packages/video_player_videohole/example/lib/main.dart +++ b/packages/video_player_videohole/example/lib/main.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// ignore_for_file: public_member_api_docs, avoid_print +// ignore_for_file: public_member_api_docs, avoid_print, use_build_context_synchronously /// An example of using the plugin, controlling lifecycle and playback of the /// video. @@ -37,6 +37,7 @@ class _App extends StatelessWidget { Tab(icon: Icon(Icons.cloud), text: 'Dash'), Tab(icon: Icon(Icons.cloud), text: 'DRM Widevine'), Tab(icon: Icon(Icons.cloud), text: 'DRM PlayReady'), + Tab(icon: Icon(Icons.cloud), text: 'Track'), ], ), ), @@ -47,6 +48,7 @@ class _App extends StatelessWidget { _DashRomoteVideo(), _DrmRemoteVideo(), _DrmRemoteVideo2(), + _TrackTest(), ], ), ), @@ -370,6 +372,69 @@ class _DrmRemoteVideoState2 extends State<_DrmRemoteVideo2> { } } +class _TrackTest extends StatefulWidget { + @override + State<_TrackTest> createState() => _TrackTestState(); +} + +class _TrackTestState extends State<_TrackTest> { + late VideoPlayerController _controller; + + @override + void initState() { + super.initState(); + + _controller = VideoPlayerController.network( + 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8'); + + _controller.addListener(() { + if (_controller.value.hasError) { + print(_controller.value.errorDescription); + } + setState(() {}); + }); + _controller.setLooping(true); + _controller.initialize().then((_) => setState(() {})); + _controller.play(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + Container(padding: const EdgeInsets.only(top: 20.0)), + const Text('track selections test'), + Container( + padding: const EdgeInsets.all(20), + child: AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + VideoPlayer(_controller), + ClosedCaption(text: _controller.value.caption.text), + _ControlsOverlay(controller: _controller), + VideoProgressIndicator(_controller, allowScrubbing: true), + ], + ), + ), + ), + _GetVideoTrackButton(controller: _controller), + _GetAudioTrackButton(controller: _controller), + _GetTextTrackButton(controller: _controller), + ], + ), + ); + } +} + class _ControlsOverlay extends StatelessWidget { const _ControlsOverlay({required this.controller}); @@ -485,3 +550,134 @@ class _ControlsOverlay extends StatelessWidget { ); } } + +class _GetVideoTrackButton extends StatelessWidget { + const _GetVideoTrackButton({required this.controller}); + + final VideoPlayerController controller; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 20.0), + child: MaterialButton( + child: const Text('Get Video Track'), + onPressed: () async { + final List? videotracks = await controller.videoTracks; + if (videotracks == null) { + return; + } + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Video'), + content: SizedBox( + height: 200, + width: 200, + child: ListView.builder( + itemCount: videotracks.length, + itemBuilder: (BuildContext context, int index) { + return ListTile( + title: Text( + '${videotracks[index].width}x${videotracks[index].height},${(videotracks[index].bitrate / 1000000).toStringAsFixed(2)}Mbps'), + onTap: () { + controller + .setTrackSelection(videotracks[index]); + }, + ); + }, + )), + ); + }); + }), + ); + } +} + +class _GetAudioTrackButton extends StatelessWidget { + const _GetAudioTrackButton({required this.controller}); + + final VideoPlayerController controller; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 20.0), + child: MaterialButton( + child: const Text('Get Audio Track'), + onPressed: () async { + final List? audioTracks = await controller.audioTracks; + if (audioTracks == null) { + return; + } + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Audio'), + content: SizedBox( + height: 200, + width: 200, + child: ListView.builder( + itemCount: audioTracks.length, + itemBuilder: (BuildContext context, int index) { + return ListTile( + title: Text( + 'language:${audioTracks[index].language}'), + onTap: () { + controller + .setTrackSelection(audioTracks[index]); + }, + ); + }, + )), + ); + }); + }), + ); + } +} + +class _GetTextTrackButton extends StatelessWidget { + const _GetTextTrackButton({required this.controller}); + + final VideoPlayerController controller; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 20.0), + child: MaterialButton( + child: const Text('Get Text Track'), + onPressed: () async { + final List? textTracks = await controller.textTracks; + if (textTracks == null) { + return; + } + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Text'), + content: SizedBox( + height: 200, + width: 200, + child: ListView.builder( + itemCount: textTracks.length, + itemBuilder: (BuildContext context, int index) { + return ListTile( + title: Text( + 'language:${textTracks[index].language}'), + onTap: () { + controller.setTrackSelection(textTracks[index]); + }, + ); + }, + )), + ); + }); + }), + ); + } +} diff --git a/packages/video_player_videohole/lib/src/messages.g.dart b/packages/video_player_videohole/lib/src/messages.g.dart index 1dcbd4698..ea6b377ce 100644 --- a/packages/video_player_videohole/lib/src/messages.g.dart +++ b/packages/video_player_videohole/lib/src/messages.g.dart @@ -107,6 +107,89 @@ class PlaybackSpeedMessage { } } +class TrackMessage { + TrackMessage({ + required this.playerId, + required this.tracks, + }); + + int playerId; + + List?> tracks; + + Object encode() { + return [ + playerId, + tracks, + ]; + } + + static TrackMessage decode(Object result) { + result as List; + return TrackMessage( + playerId: result[0]! as int, + tracks: (result[1] as List?)!.cast?>(), + ); + } +} + +class TrackTypeMessage { + TrackTypeMessage({ + required this.playerId, + required this.trackType, + }); + + int playerId; + + int trackType; + + Object encode() { + return [ + playerId, + trackType, + ]; + } + + static TrackTypeMessage decode(Object result) { + result as List; + return TrackTypeMessage( + playerId: result[0]! as int, + trackType: result[1]! as int, + ); + } +} + +class SelectedTracksMessage { + SelectedTracksMessage({ + required this.playerId, + required this.trackId, + required this.trackType, + }); + + int playerId; + + int trackId; + + int trackType; + + Object encode() { + return [ + playerId, + trackId, + trackType, + ]; + } + + static SelectedTracksMessage decode(Object result) { + result as List; + return SelectedTracksMessage( + playerId: result[0]! as int, + trackId: result[1]! as int, + trackType: result[2]! as int, + ); + } +} + class PositionMessage { PositionMessage({ required this.playerId, @@ -268,9 +351,18 @@ class _VideoPlayerVideoholeApiCodec extends StandardMessageCodec { } else if (value is PositionMessage) { buffer.putUint8(134); writeValue(buffer, value.encode()); - } else if (value is VolumeMessage) { + } else if (value is SelectedTracksMessage) { buffer.putUint8(135); writeValue(buffer, value.encode()); + } else if (value is TrackMessage) { + buffer.putUint8(136); + writeValue(buffer, value.encode()); + } else if (value is TrackTypeMessage) { + buffer.putUint8(137); + writeValue(buffer, value.encode()); + } else if (value is VolumeMessage) { + buffer.putUint8(138); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -294,6 +386,12 @@ class _VideoPlayerVideoholeApiCodec extends StandardMessageCodec { case 134: return PositionMessage.decode(readValue(buffer)!); case 135: + return SelectedTracksMessage.decode(readValue(buffer)!); + case 136: + return TrackMessage.decode(readValue(buffer)!); + case 137: + return TrackTypeMessage.decode(readValue(buffer)!); + case 138: return VolumeMessage.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -518,6 +616,55 @@ class VideoPlayerVideoholeApi { } } + Future track(TrackTypeMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerVideoholeApi.track', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as TrackMessage?)!; + } + } + + Future setTrackSelection(SelectedTracksMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerVideoholeApi.setTrackSelection', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + Future pause(PlayerMessage arg_msg) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerVideoholeApi.pause', codec, diff --git a/packages/video_player_videohole/lib/src/tracks.dart b/packages/video_player_videohole/lib/src/tracks.dart new file mode 100644 index 000000000..fe68ab1df --- /dev/null +++ b/packages/video_player_videohole/lib/src/tracks.dart @@ -0,0 +1,127 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Type of the track. +enum TrackType { + /// The video track. + video, + + /// The audio track. + audio, + + /// The text track. + text, +} + +/// Type of the track audio channel for [TrackType.audio]. +enum AudioTrackChannelType { + /// The mono channel. + mono, + + /// The stereo channel. + stereo, + + /// The surround channel. + surround, +} + +/// Type of the track subtitle type for [TrackType.text]. +enum TextTrackSubtitleType { + /// The text subtitle. + text, + + /// The picture subtitle. + picture, +} + +/// A representation of a single track. +/// +/// A typical video file will include several [Track]s.Such as [VideoTrack]s, [AudioTrack]s, [TextTrack]s. +class Track { + /// Creates an instance of [Track]. + /// + /// The [trackId] and [trackType] arguments are required. + /// + const Track({ + required this.trackId, + required this.trackType, + }); + + /// The track id of track that uses to determine track. + final int trackId; + + /// The type of the track. + final TrackType trackType; +} + +/// A representation of a video track. +class VideoTrack extends Track { + /// Creates an instance of [VideoTrack]. + /// + /// The [width], [height] and [bitrate] argument is required. + /// + /// [trackType] is [TrackType.video]. + VideoTrack({ + required super.trackId, + super.trackType = TrackType.video, + required this.width, + required this.height, + required this.bitrate, + }); + + /// The width of video track. + final int width; + + /// The height of video track. + final int height; + + /// The bitrate of video track. + final int bitrate; +} + +/// A representation of a audio track. +class AudioTrack extends Track { + /// Creates an instance of [AudioTrack]. + /// + /// The [language], [channel] and [bitrate] arguments are required. + /// + /// [trackType] is [TrackType.audio]. + AudioTrack({ + required super.trackId, + super.trackType = TrackType.audio, + required this.language, + required this.channel, + required this.bitrate, + }); + + /// The language of audio track. + final String language; + + /// The channel of audio track. + final AudioTrackChannelType channel; + + /// The bitrate of audio track. + final int bitrate; +} + +/// A representation of a text track. +class TextTrack extends Track { + /// Creates an instance of [TextTrack]. + /// + /// The [language] and [subtitleType] arguments are required. + /// + /// [trackType] is [TrackType.text]. + TextTrack({ + required super.trackId, + super.trackType = TrackType.text, + required this.language, + required this.subtitleType, + }); + + /// The language of text track. + final String language; + + /// The subtitle type of track. + final TextTrackSubtitleType subtitleType; +} diff --git a/packages/video_player_videohole/lib/src/video_player_tizen.dart b/packages/video_player_videohole/lib/src/video_player_tizen.dart index 2e5427472..94dc871af 100644 --- a/packages/video_player_videohole/lib/src/video_player_tizen.dart +++ b/packages/video_player_videohole/lib/src/video_player_tizen.dart @@ -10,6 +10,7 @@ import 'package:flutter/widgets.dart'; import '../video_player_platform_interface.dart'; import 'messages.g.dart'; +import 'tracks.dart'; /// An implementation of [VideoPlayerPlatform] that uses the /// Pigeon-generated [TizenVideoPlayerApi]. @@ -88,6 +89,98 @@ class VideoPlayerTizen extends VideoPlayerPlatform { PositionMessage(playerId: playerId, position: position.inMilliseconds)); } + @override + Future> getVideoTracks(int playerId) async { + final TrackMessage response = await _api.track(TrackTypeMessage( + playerId: playerId, + trackType: _intTrackTypeMap.keys.firstWhere( + (int key) => _intTrackTypeMap[key] == TrackType.video, + orElse: () => -1), + )); + + final List videoTracks = []; + for (final Map? trackMap in response.tracks) { + final int trackId = trackMap!['trackId']! as int; + final int bitrate = trackMap['bitrate']! as int; + final int width = trackMap['width']! as int; + final int height = trackMap['height']! as int; + + videoTracks.add(VideoTrack( + trackId: trackId, + width: width, + height: height, + bitrate: bitrate, + )); + } + + return videoTracks; + } + + @override + Future> getAudioTracks(int playerId) async { + final TrackMessage response = await _api.track(TrackTypeMessage( + playerId: playerId, + trackType: _intTrackTypeMap.keys.firstWhere( + (int key) => _intTrackTypeMap[key] == TrackType.audio, + orElse: () => -1), + )); + + final List audioTracks = []; + for (final Map? trackMap in response.tracks) { + final int trackId = trackMap!['trackId']! as int; + final String language = trackMap['language']! as String; + final AudioTrackChannelType channelType = + _intChannelTypeMap[trackMap['channel']]!; + final int bitrate = trackMap['bitrate']! as int; + + audioTracks.add(AudioTrack( + trackId: trackId, + language: language, + channel: channelType, + bitrate: bitrate, + )); + } + + return audioTracks; + } + + @override + Future> getTextTracks(int playerId) async { + final TrackMessage response = await _api.track(TrackTypeMessage( + playerId: playerId, + trackType: _intTrackTypeMap.keys.firstWhere( + (int key) => _intTrackTypeMap[key] == TrackType.text, + orElse: () => -1), + )); + + final List textTracks = []; + for (final Map? trackMap in response.tracks) { + final int trackId = trackMap!['trackId']! as int; + final String language = trackMap['language']! as String; + final TextTrackSubtitleType subtitleType = + _intSubtitleTypeMap[trackMap['subtitleType']]!; + + textTracks.add(TextTrack( + trackId: trackId, + language: language, + subtitleType: subtitleType, + )); + } + + return textTracks; + } + + @override + Future setTrackSelection(int playerId, Track track) { + return _api.setTrackSelection(SelectedTracksMessage( + playerId: playerId, + trackId: track.trackId, + trackType: _intTrackTypeMap.keys.firstWhere( + (int key) => _intTrackTypeMap[key] == track.trackType, + orElse: () => -1), + )); + } + @override Future getPosition(int playerId) async { final PositionMessage response = @@ -174,4 +267,23 @@ class VideoPlayerTizen extends VideoPlayerPlatform { VideoFormat.dash: 'dash', VideoFormat.other: 'other', }; + + static const Map _intTrackTypeMap = { + 1: TrackType.audio, + 2: TrackType.video, + 3: TrackType.text, + }; + + static const Map _intChannelTypeMap = + { + 1: AudioTrackChannelType.mono, + 2: AudioTrackChannelType.stereo, + 3: AudioTrackChannelType.surround, + }; + + static const Map _intSubtitleTypeMap = + { + 0: TextTrackSubtitleType.text, + 1: TextTrackSubtitleType.picture, + }; } diff --git a/packages/video_player_videohole/lib/video_player.dart b/packages/video_player_videohole/lib/video_player.dart index d510253f3..5f8d7beaf 100644 --- a/packages/video_player_videohole/lib/video_player.dart +++ b/packages/video_player_videohole/lib/video_player.dart @@ -15,10 +15,12 @@ import 'src/drm_configs.dart'; import 'src/hole.dart'; import 'src/register_drm_callback_stub.dart' if (dart.library.ffi) 'src/register_drm_callback_real.dart'; +import 'src/tracks.dart'; import 'video_player_platform_interface.dart'; export 'src/closed_caption_file.dart'; export 'src/drm_configs.dart'; +export 'src/tracks.dart'; VideoPlayerPlatform? _lastVideoPlayerPlatform; @@ -44,6 +46,7 @@ class VideoPlayerValue { this.position = Duration.zero, this.caption = Caption.none, this.captionOffset = Duration.zero, + this.tracks = const [], this.buffered = 0, this.isInitialized = false, this.isPlaying = false, @@ -106,6 +109,9 @@ class VideoPlayerValue { /// The current speed of the playback. final double playbackSpeed; + /// The current playback tracks. + final List tracks; + /// A description of the error if present. /// /// If [hasError] is false this is `null`. @@ -146,6 +152,7 @@ class VideoPlayerValue { Duration? position, Caption? caption, Duration? captionOffset, + List? tracks, int? buffered, bool? isInitialized, bool? isPlaying, @@ -161,6 +168,7 @@ class VideoPlayerValue { position: position ?? this.position, caption: caption ?? this.caption, captionOffset: captionOffset ?? this.captionOffset, + tracks: tracks ?? this.tracks, buffered: buffered ?? this.buffered, isInitialized: isInitialized ?? this.isInitialized, isPlaying: isPlaying ?? this.isPlaying, @@ -182,6 +190,7 @@ class VideoPlayerValue { 'position: $position, ' 'caption: $caption, ' 'captionOffset: $captionOffset, ' + 'tracks: $tracks, ' 'buffered: $buffered, ' 'isInitialized: $isInitialized, ' 'isPlaying: $isPlaying, ' @@ -577,6 +586,38 @@ class VideoPlayerController extends ValueNotifier { _updatePosition(position); } + /// The video tracks in the current video. + Future?> get videoTracks async { + if (!value.isInitialized || _isDisposed) { + return null; + } + return _videoPlayerPlatform.getVideoTracks(_playerId); + } + + /// The audio tracks in the current video. + Future?> get audioTracks async { + if (!value.isInitialized || _isDisposed) { + return null; + } + return _videoPlayerPlatform.getAudioTracks(_playerId); + } + + /// The text tracks in the current video. + Future?> get textTracks async { + if (!value.isInitialized || _isDisposed) { + return null; + } + return _videoPlayerPlatform.getTextTracks(_playerId); + } + + /// Sets the selected tracks. + Future setTrackSelection(Track track) async { + if (!value.isInitialized || _isDisposed) { + return; + } + await _videoPlayerPlatform.setTrackSelection(_playerId, track); + } + /// Sets the audio volume of [this]. /// /// [volume] indicates a value between 0.0 (silent) and 1.0 (full volume) on a diff --git a/packages/video_player_videohole/lib/video_player_platform_interface.dart b/packages/video_player_videohole/lib/video_player_platform_interface.dart index 6d87887fa..e8e63ce21 100644 --- a/packages/video_player_videohole/lib/video_player_platform_interface.dart +++ b/packages/video_player_videohole/lib/video_player_platform_interface.dart @@ -8,6 +8,7 @@ import 'package:flutter/widgets.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'src/drm_configs.dart'; +import 'src/tracks.dart'; import 'src/video_player_tizen.dart'; /// The interface that implementations of video_player must implement. @@ -86,6 +87,26 @@ abstract class VideoPlayerPlatform extends PlatformInterface { throw UnimplementedError('seekTo() has not been implemented.'); } + /// Gets the video tracks as a list of [VideoTrack]. + Future> getVideoTracks(int playerId) { + throw UnimplementedError('getVideoTracks() has not been implemented.'); + } + + /// Gets the audio tracks as a list of [AudioTrack]. + Future> getAudioTracks(int playerId) { + throw UnimplementedError('getAudioTracks() has not been implemented.'); + } + + /// Gets the text tracks as a list of [TextTrack]. + Future> getTextTracks(int playerId) { + throw UnimplementedError('getTextTracks() has not been implemented.'); + } + + /// Sets the selected track. + Future setTrackSelection(int playerId, Track track) { + throw UnimplementedError('setTrackSelection() has not been implemented.'); + } + /// Sets the playback speed to a [speed] value indicating the playback rate. Future setPlaybackSpeed(int playerId, double speed) { throw UnimplementedError('setPlaybackSpeed() has not been implemented.'); diff --git a/packages/video_player_videohole/pigeons/messages.dart b/packages/video_player_videohole/pigeons/messages.dart index 1a7e946f9..25bf8173b 100644 --- a/packages/video_player_videohole/pigeons/messages.dart +++ b/packages/video_player_videohole/pigeons/messages.dart @@ -32,6 +32,25 @@ class PlaybackSpeedMessage { double speed; } +class TrackMessage { + TrackMessage(this.playerId, this.tracks); + int playerId; + List?> tracks; +} + +class TrackTypeMessage { + TrackTypeMessage(this.playerId, this.trackType); + int playerId; + int trackType; +} + +class SelectedTracksMessage { + SelectedTracksMessage(this.playerId, this.trackId, this.trackType); + int playerId; + int trackId; + int trackType; +} + class PositionMessage { PositionMessage(this.playerId, this.position); int playerId; @@ -74,6 +93,8 @@ abstract class VideoPlayerVideoholeApi { PositionMessage position(PlayerMessage msg); @async void seekTo(PositionMessage msg); + TrackMessage track(TrackTypeMessage msg); + void setTrackSelection(SelectedTracksMessage msg); void pause(PlayerMessage msg); void setMixWithOthers(MixWithOthersMessage msg); void setDisplayGeometry(GeometryMessage msg); diff --git a/packages/video_player_videohole/pubspec.yaml b/packages/video_player_videohole/pubspec.yaml index 09bf84847..876ebac9d 100644 --- a/packages/video_player_videohole/pubspec.yaml +++ b/packages/video_player_videohole/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_videohole description: Flutter plugin for displaying inline video on Tizen TV devices. homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/master/packages/video_player_videohole -version: 0.1.2 +version: 0.2.0 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/video_player_videohole/tizen/src/messages.cc b/packages/video_player_videohole/tizen/src/messages.cc index 80ddfce8e..090d888ff 100644 --- a/packages/video_player_videohole/tizen/src/messages.cc +++ b/packages/video_player_videohole/tizen/src/messages.cc @@ -123,6 +123,106 @@ PlaybackSpeedMessage PlaybackSpeedMessage::FromEncodableList( return decoded; } +// TrackMessage + +TrackMessage::TrackMessage(int64_t player_id, const EncodableList& tracks) + : player_id_(player_id), tracks_(tracks) {} + +int64_t TrackMessage::player_id() const { return player_id_; } + +void TrackMessage::set_player_id(int64_t value_arg) { player_id_ = value_arg; } + +const EncodableList& TrackMessage::tracks() const { return tracks_; } + +void TrackMessage::set_tracks(const EncodableList& value_arg) { + tracks_ = value_arg; +} + +EncodableList TrackMessage::ToEncodableList() const { + EncodableList list; + list.reserve(2); + list.push_back(EncodableValue(player_id_)); + list.push_back(EncodableValue(tracks_)); + return list; +} + +TrackMessage TrackMessage::FromEncodableList(const EncodableList& list) { + TrackMessage decoded(list[0].LongValue(), std::get(list[1])); + return decoded; +} + +// TrackTypeMessage + +TrackTypeMessage::TrackTypeMessage(int64_t player_id, int64_t track_type) + : player_id_(player_id), track_type_(track_type) {} + +int64_t TrackTypeMessage::player_id() const { return player_id_; } + +void TrackTypeMessage::set_player_id(int64_t value_arg) { + player_id_ = value_arg; +} + +int64_t TrackTypeMessage::track_type() const { return track_type_; } + +void TrackTypeMessage::set_track_type(int64_t value_arg) { + track_type_ = value_arg; +} + +EncodableList TrackTypeMessage::ToEncodableList() const { + EncodableList list; + list.reserve(2); + list.push_back(EncodableValue(player_id_)); + list.push_back(EncodableValue(track_type_)); + return list; +} + +TrackTypeMessage TrackTypeMessage::FromEncodableList( + const EncodableList& list) { + TrackTypeMessage decoded(list[0].LongValue(), list[1].LongValue()); + return decoded; +} + +// SelectedTracksMessage + +SelectedTracksMessage::SelectedTracksMessage(int64_t player_id, + int64_t track_id, + int64_t track_type) + : player_id_(player_id), track_id_(track_id), track_type_(track_type) {} + +int64_t SelectedTracksMessage::player_id() const { return player_id_; } + +void SelectedTracksMessage::set_player_id(int64_t value_arg) { + player_id_ = value_arg; +} + +int64_t SelectedTracksMessage::track_id() const { return track_id_; } + +void SelectedTracksMessage::set_track_id(int64_t value_arg) { + track_id_ = value_arg; +} + +int64_t SelectedTracksMessage::track_type() const { return track_type_; } + +void SelectedTracksMessage::set_track_type(int64_t value_arg) { + track_type_ = value_arg; +} + +EncodableList SelectedTracksMessage::ToEncodableList() const { + EncodableList list; + list.reserve(3); + list.push_back(EncodableValue(player_id_)); + list.push_back(EncodableValue(track_id_)); + list.push_back(EncodableValue(track_type_)); + return list; +} + +SelectedTracksMessage SelectedTracksMessage::FromEncodableList( + const EncodableList& list) { + SelectedTracksMessage decoded(list[0].LongValue(), list[1].LongValue(), + list[2].LongValue()); + return decoded; +} + // PositionMessage PositionMessage::PositionMessage(int64_t player_id, int64_t position) @@ -388,6 +488,15 @@ EncodableValue VideoPlayerVideoholeApiCodecSerializer::ReadValueOfType( return CustomEncodableValue(PositionMessage::FromEncodableList( std::get(ReadValue(stream)))); case 135: + return CustomEncodableValue(SelectedTracksMessage::FromEncodableList( + std::get(ReadValue(stream)))); + case 136: + return CustomEncodableValue(TrackMessage::FromEncodableList( + std::get(ReadValue(stream)))); + case 137: + return CustomEncodableValue(TrackTypeMessage::FromEncodableList( + std::get(ReadValue(stream)))); + case 138: return CustomEncodableValue(VolumeMessage::FromEncodableList( std::get(ReadValue(stream)))); default: @@ -455,8 +564,32 @@ void VideoPlayerVideoholeApiCodecSerializer::WriteValue( stream); return; } - if (custom_value->type() == typeid(VolumeMessage)) { + if (custom_value->type() == typeid(SelectedTracksMessage)) { stream->WriteByte(135); + WriteValue( + EncodableValue(std::any_cast(*custom_value) + .ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(TrackMessage)) { + stream->WriteByte(136); + WriteValue( + EncodableValue( + std::any_cast(*custom_value).ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(TrackTypeMessage)) { + stream->WriteByte(137); + WriteValue( + EncodableValue( + std::any_cast(*custom_value).ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(VolumeMessage)) { + stream->WriteByte(138); WriteValue( EncodableValue( std::any_cast(*custom_value).ToEncodableList()), @@ -772,6 +905,75 @@ void VideoPlayerVideoholeApi::SetUp(flutter::BinaryMessenger* binary_messenger, channel->SetMessageHandler(nullptr); } } + { + auto channel = std::make_unique>( + binary_messenger, "dev.flutter.pigeon.VideoPlayerVideoholeApi.track", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_msg_arg = args.at(0); + if (encodable_msg_arg.IsNull()) { + reply(WrapError("msg_arg unexpectedly null.")); + return; + } + const auto& msg_arg = std::any_cast( + std::get(encodable_msg_arg)); + ErrorOr output = api->Track(msg_arg); + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back( + CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = std::make_unique>( + binary_messenger, + "dev.flutter.pigeon.VideoPlayerVideoholeApi.setTrackSelection", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_msg_arg = args.at(0); + if (encodable_msg_arg.IsNull()) { + reply(WrapError("msg_arg unexpectedly null.")); + return; + } + const auto& msg_arg = std::any_cast( + std::get(encodable_msg_arg)); + std::optional output = + api->SetTrackSelection(msg_arg); + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } { auto channel = std::make_unique>( binary_messenger, "dev.flutter.pigeon.VideoPlayerVideoholeApi.pause", diff --git a/packages/video_player_videohole/tizen/src/messages.h b/packages/video_player_videohole/tizen/src/messages.h index 6effb80c4..3a40ddbb3 100644 --- a/packages/video_player_videohole/tizen/src/messages.h +++ b/packages/video_player_videohole/tizen/src/messages.h @@ -134,6 +134,76 @@ class PlaybackSpeedMessage { double speed_; }; +// Generated class from Pigeon that represents data sent in messages. +class TrackMessage { + public: + // Constructs an object setting all fields. + explicit TrackMessage(int64_t player_id, + const flutter::EncodableList& tracks); + + int64_t player_id() const; + void set_player_id(int64_t value_arg); + + const flutter::EncodableList& tracks() const; + void set_tracks(const flutter::EncodableList& value_arg); + + private: + static TrackMessage FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class VideoPlayerVideoholeApi; + friend class VideoPlayerVideoholeApiCodecSerializer; + int64_t player_id_; + flutter::EncodableList tracks_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class TrackTypeMessage { + public: + // Constructs an object setting all fields. + explicit TrackTypeMessage(int64_t player_id, int64_t track_type); + + int64_t player_id() const; + void set_player_id(int64_t value_arg); + + int64_t track_type() const; + void set_track_type(int64_t value_arg); + + private: + static TrackTypeMessage FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class VideoPlayerVideoholeApi; + friend class VideoPlayerVideoholeApiCodecSerializer; + int64_t player_id_; + int64_t track_type_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class SelectedTracksMessage { + public: + // Constructs an object setting all fields. + explicit SelectedTracksMessage(int64_t player_id, int64_t track_id, + int64_t track_type); + + int64_t player_id() const; + void set_player_id(int64_t value_arg); + + int64_t track_id() const; + void set_track_id(int64_t value_arg); + + int64_t track_type() const; + void set_track_type(int64_t value_arg); + + private: + static SelectedTracksMessage FromEncodableList( + const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class VideoPlayerVideoholeApi; + friend class VideoPlayerVideoholeApiCodecSerializer; + int64_t player_id_; + int64_t track_id_; + int64_t track_type_; +}; + // Generated class from Pigeon that represents data sent in messages. class PositionMessage { public: @@ -293,6 +363,9 @@ class VideoPlayerVideoholeApi { virtual void SeekTo( const PositionMessage& msg, std::function reply)> result) = 0; + virtual ErrorOr Track(const TrackTypeMessage& msg) = 0; + virtual std::optional SetTrackSelection( + const SelectedTracksMessage& msg) = 0; virtual std::optional Pause(const PlayerMessage& msg) = 0; virtual std::optional SetMixWithOthers( const MixWithOthersMessage& msg) = 0; diff --git a/packages/video_player_videohole/tizen/src/video_player.cc b/packages/video_player_videohole/tizen/src/video_player.cc index 7c2f12e16..81d1df2e7 100644 --- a/packages/video_player_videohole/tizen/src/video_player.cc +++ b/packages/video_player_videohole/tizen/src/video_player.cc @@ -17,7 +17,53 @@ static int64_t player_index = 1; VideoPlayer::VideoPlayer(flutter::PluginRegistrar *plugin_registrar, void *native_window) - : plugin_registrar_(plugin_registrar), native_window_(native_window) {} + : plugin_registrar_(plugin_registrar), native_window_(native_window) { + sink_event_pipe_ = ecore_pipe_add( + [](void *data, void *buffer, unsigned int nbyte) -> void { + auto *self = static_cast(data); + self->SendPendingEvents(); + }, + this); +} + +void VideoPlayer::SendPendingEvents() { + std::lock_guard lock(queue_mutex_); + while (!encodable_event_queue_.empty()) { + if (event_sink_) { + event_sink_->Success(encodable_event_queue_.front()); + } + encodable_event_queue_.pop(); + } + + while (!error_event_queue_.empty()) { + if (event_sink_) { + event_sink_->Error(error_event_queue_.front().first, + error_event_queue_.front().second); + } + error_event_queue_.pop(); + } +} + +void VideoPlayer::PushEvent(const flutter::EncodableValue &encodable_value) { + if (!event_sink_) { + LOG_ERROR("[VideoPlayer] event sink is nullptr."); + return; + } + std::lock_guard lock(queue_mutex_); + encodable_event_queue_.push(encodable_value); + ecore_pipe_write(sink_event_pipe_, nullptr, 0); +} + +void VideoPlayer::SendError(const std::string &error_code, + const std::string &error_message) { + if (!event_sink_) { + LOG_ERROR("[VideoPlayer] event sink is nullptr."); + return; + } + std::lock_guard lock(queue_mutex_); + error_event_queue_.push(std::make_pair(error_code, error_message)); + ecore_pipe_write(sink_event_pipe_, nullptr, 0); +} bool VideoPlayer::SetDisplay() { int x = 0, y = 0, width = 0, height = 0; @@ -265,6 +311,214 @@ void VideoPlayer::SetPlaybackSpeed(double speed) { } } +flutter::EncodableList VideoPlayer::getTrackInfo(int32_t track_type) { + player_state_e state = PLAYER_STATE_NONE; + int ret = player_get_state(player_, &state); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_get_state failed: %s", + get_error_message(ret)); + return {}; + } + if (state == PLAYER_STATE_NONE || state == PLAYER_STATE_IDLE) { + LOG_ERROR("[VideoPlayer] Player not ready."); + return {}; + } + + void *player_lib_handle = dlopen("libcapi-media-player.so.0", RTLD_LAZY); + if (!player_lib_handle) { + LOG_ERROR("[VideoPlayer] dlopen failed: %s", dlerror()); + return {}; + } + + FuncPlayerGetTrackCountV2 player_get_track_count_v2 = + reinterpret_cast( + dlsym(player_lib_handle, "player_get_track_count_v2")); + if (!player_get_track_count_v2) { + LOG_ERROR("[VideoPlayer] Symbol not found: %s", dlerror()); + dlclose(player_lib_handle); + return {}; + } + + int track_count = 0; + ret = player_get_track_count_v2(player_, (player_stream_type_e)track_type, + &track_count); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_get_track_count_v2 failed: %s", + get_error_message(ret)); + dlclose(player_lib_handle); + return {}; + } + if (track_count <= 0) { + return {}; + } + + flutter::EncodableList trackSelections = {}; + if (track_type == PLAYER_STREAM_TYPE_VIDEO) { + LOG_INFO("[VideoPlayer] video_count: %d", track_count); + + FuncPlayerGetVideoTrackInfoV2 player_get_video_track_info_v2 = + reinterpret_cast( + dlsym(player_lib_handle, "player_get_video_track_info_v2")); + if (!player_get_video_track_info_v2) { + LOG_ERROR("[VideoPlayer] Symbol not found: %s", dlerror()); + dlclose(player_lib_handle); + return {}; + } + + for (int video_index = 0; video_index < track_count; video_index++) { + flutter::EncodableMap trackSelection = {}; + player_video_track_info_v2 *video_track_info = nullptr; + + ret = player_get_video_track_info_v2(player_, video_index, + &video_track_info); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_get_video_track_info_v2 failed: %s", + get_error_message(ret)); + dlclose(player_lib_handle); + return {}; + } + LOG_INFO( + "[VideoPlayer] video track info: width[%d], height[%d], " + "bitrate[%d]", + video_track_info->width, video_track_info->height, + video_track_info->bit_rate); + + trackSelection.insert( + {flutter::EncodableValue("trackType"), + flutter::EncodableValue(PLAYER_STREAM_TYPE_VIDEO)}); + trackSelection.insert({flutter::EncodableValue("trackId"), + flutter::EncodableValue(video_index)}); + trackSelection.insert({flutter::EncodableValue("width"), + flutter::EncodableValue(video_track_info->width)}); + trackSelection.insert( + {flutter::EncodableValue("height"), + flutter::EncodableValue(video_track_info->height)}); + trackSelection.insert( + {flutter::EncodableValue("bitrate"), + flutter::EncodableValue(video_track_info->bit_rate)}); + + trackSelections.push_back(flutter::EncodableValue(trackSelection)); + } + + } else if (track_type == PLAYER_STREAM_TYPE_AUDIO) { + LOG_INFO("[VideoPlayer] audio_count: %d", track_count); + + FuncPlayerGetAudioTrackInfoV2 player_get_audio_track_info_v2 = + reinterpret_cast( + dlsym(player_lib_handle, "player_get_audio_track_info_v2")); + if (!player_get_audio_track_info_v2) { + LOG_ERROR("[VideoPlayer] Symbol not found: %s", dlerror()); + dlclose(player_lib_handle); + return {}; + } + + for (int audio_index = 0; audio_index < track_count; audio_index++) { + flutter::EncodableMap trackSelection = {}; + player_audio_track_info_v2 *audio_track_info = nullptr; + + ret = player_get_audio_track_info_v2(player_, audio_index, + &audio_track_info); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_get_audio_track_info_v2 failed: %s", + get_error_message(ret)); + dlclose(player_lib_handle); + return {}; + } + LOG_INFO( + "[VideoPlayer] audio track info: language[%s], channel[%d], " + "sample_rate[%d], bitrate[%d]", + audio_track_info->language, audio_track_info->channel, + audio_track_info->sample_rate, audio_track_info->bit_rate); + + trackSelection.insert( + {flutter::EncodableValue("trackType"), + flutter::EncodableValue(PLAYER_STREAM_TYPE_AUDIO)}); + trackSelection.insert({flutter::EncodableValue("trackId"), + flutter::EncodableValue(audio_index)}); + trackSelection.insert( + {flutter::EncodableValue("language"), + flutter::EncodableValue(std::string(audio_track_info->language))}); + trackSelection.insert( + {flutter::EncodableValue("channel"), + flutter::EncodableValue(audio_track_info->channel)}); + trackSelection.insert( + {flutter::EncodableValue("bitrate"), + flutter::EncodableValue(audio_track_info->bit_rate)}); + + trackSelections.push_back(flutter::EncodableValue(trackSelection)); + } + + } else if (track_type == PLAYER_STREAM_TYPE_TEXT) { + LOG_INFO("[VideoPlayer] subtitle_count: %d", track_count); + + FuncPlayerGetSubtitleTrackInfoV2 player_get_subtitle_track_info_v2 = + reinterpret_cast( + dlsym(player_lib_handle, "player_get_subtitle_track_info_v2")); + if (!player_get_subtitle_track_info_v2) { + LOG_ERROR("[VideoPlayer] Symbol not found: %s", dlerror()); + dlclose(player_lib_handle); + return {}; + } + + for (int sub_index = 0; sub_index < track_count; sub_index++) { + flutter::EncodableMap trackSelection = {}; + player_subtitle_track_info_v2 *sub_track_info = nullptr; + + ret = player_get_subtitle_track_info_v2(player_, sub_index, + &sub_track_info); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_get_subtitle_track_info_v2 failed: %s", + get_error_message(ret)); + dlclose(player_lib_handle); + return {}; + } + LOG_INFO( + "[VideoPlayer] subtitle track info: language[%s], " + "subtitle_type[%d]", + sub_track_info->language, sub_track_info->subtitle_type); + + trackSelection.insert({flutter::EncodableValue("trackType"), + flutter::EncodableValue(PLAYER_STREAM_TYPE_TEXT)}); + trackSelection.insert({flutter::EncodableValue("trackId"), + flutter::EncodableValue(sub_index)}); + trackSelection.insert( + {flutter::EncodableValue("language"), + flutter::EncodableValue(std::string(sub_track_info->language))}); + trackSelection.insert( + {flutter::EncodableValue("subtitleType"), + flutter::EncodableValue(sub_track_info->subtitle_type)}); + + trackSelections.push_back(flutter::EncodableValue(trackSelection)); + } + } + + dlclose(player_lib_handle); + return trackSelections; +} + +void VideoPlayer::SetTrackSelection(int32_t track_id, int32_t track_type) { + LOG_INFO("[VideoPlayer] track_id: %d,track_type: %d", track_id, track_type); + + player_state_e state = PLAYER_STATE_NONE; + int ret = player_get_state(player_, &state); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_get_state failed: %s", + get_error_message(ret)); + return; + } + if (state == PLAYER_STATE_NONE || state == PLAYER_STATE_IDLE) { + LOG_ERROR("[VideoPlayer] Player not ready."); + return; + } + + ret = + player_select_track(player_, (player_stream_type_e)track_type, track_id); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_select_track failed: %s", + get_error_message(ret)); + } +} + void VideoPlayer::SeekTo(int32_t position, SeekCompletedCallback callback) { LOG_INFO("[VideoPlayer] position: %d", position); @@ -293,6 +547,9 @@ void VideoPlayer::Dispose() { is_initialized_ = false; event_sink_ = nullptr; event_channel_->SetStreamHandler(nullptr); + if (sink_event_pipe_) { + ecore_pipe_del(sink_event_pipe_); + } if (player_) { player_unprepare(player_); @@ -350,7 +607,7 @@ void VideoPlayer::SendInitialized() { int duration = 0; int ret = player_get_duration(player_, &duration); if (ret != PLAYER_ERROR_NONE) { - event_sink_->Error("player_get_duration failed", get_error_message(ret)); + SendError("player_get_duration failed", get_error_message(ret)); return; } LOG_INFO("[VideoPlayer] Video duration: %d", duration); @@ -358,8 +615,7 @@ void VideoPlayer::SendInitialized() { int width = 0, height = 0; ret = player_get_video_size(player_, &width, &height); if (ret != PLAYER_ERROR_NONE) { - event_sink_->Error("player_get_video_size failed", - get_error_message(ret)); + SendError("player_get_video_size failed", get_error_message(ret)); return; } LOG_INFO("[VideoPlayer] Video width: %d, height: %d", width, height); @@ -367,8 +623,7 @@ void VideoPlayer::SendInitialized() { player_display_rotation_e rotation = PLAYER_DISPLAY_ROTATION_NONE; ret = player_get_display_rotation(player_, &rotation); if (ret != PLAYER_ERROR_NONE) { - event_sink_->Error("player_get_display_rotation failed", - get_error_message(ret)); + SendError("player_get_display_rotation failed", get_error_message(ret)); } else { if (rotation == PLAYER_DISPLAY_ROTATION_90 || rotation == PLAYER_DISPLAY_ROTATION_270) { @@ -385,53 +640,44 @@ void VideoPlayer::SendInitialized() { {flutter::EncodableValue("width"), flutter::EncodableValue(width)}, {flutter::EncodableValue("height"), flutter::EncodableValue(height)}, }; - event_sink_->Success(flutter::EncodableValue(result)); + PushEvent(flutter::EncodableValue(result)); } } void VideoPlayer::SendBufferingStart() { - if (event_sink_) { - flutter::EncodableMap result = { - {flutter::EncodableValue("event"), - flutter::EncodableValue("bufferingStart")}, - }; - event_sink_->Success(flutter::EncodableValue(result)); - } + flutter::EncodableMap result = { + {flutter::EncodableValue("event"), + flutter::EncodableValue("bufferingStart")}, + }; + PushEvent(flutter::EncodableValue(result)); } void VideoPlayer::SendBufferingUpdate(int32_t value) { - if (event_sink_) { - flutter::EncodableMap result = { - {flutter::EncodableValue("event"), - flutter::EncodableValue("bufferingUpdate")}, - {flutter::EncodableValue("value"), flutter::EncodableValue(value)}, - }; - event_sink_->Success(flutter::EncodableValue(result)); - } + flutter::EncodableMap result = { + {flutter::EncodableValue("event"), + flutter::EncodableValue("bufferingUpdate")}, + {flutter::EncodableValue("value"), flutter::EncodableValue(value)}, + }; + PushEvent(flutter::EncodableValue(result)); } void VideoPlayer::SendBufferingEnd() { - if (event_sink_) { - flutter::EncodableMap result = { - {flutter::EncodableValue("event"), - flutter::EncodableValue("bufferingEnd")}, - }; - event_sink_->Success(flutter::EncodableValue(result)); - } + flutter::EncodableMap result = { + {flutter::EncodableValue("event"), + flutter::EncodableValue("bufferingEnd")}, + }; + PushEvent(flutter::EncodableValue(result)); } void VideoPlayer::SendSubtitleUpdate(int32_t duration, const std::string &text) { - if (event_sink_) { - flutter::EncodableMap result = { - {flutter::EncodableValue("event"), - flutter::EncodableValue("subtitleUpdate")}, - {flutter::EncodableValue("duration"), - flutter::EncodableValue(duration)}, - {flutter::EncodableValue("text"), flutter::EncodableValue(text)}, - }; - event_sink_->Success(flutter::EncodableValue(result)); - } + flutter::EncodableMap result = { + {flutter::EncodableValue("event"), + flutter::EncodableValue("subtitleUpdate")}, + {flutter::EncodableValue("duration"), flutter::EncodableValue(duration)}, + {flutter::EncodableValue("text"), flutter::EncodableValue(text)}, + }; + PushEvent(flutter::EncodableValue(result)); } void VideoPlayer::OnSubtitleUpdated(unsigned long duration, char *text, @@ -480,13 +726,11 @@ void VideoPlayer::OnPlayCompleted(void *data) { LOG_INFO("[VideoPlayer] Play completed."); VideoPlayer *player = static_cast(data); - if (player->event_sink_) { - flutter::EncodableMap result = { - {flutter::EncodableValue("event"), - flutter::EncodableValue("completed")}, - }; - player->event_sink_->Success(flutter::EncodableValue(result)); - } + flutter::EncodableMap result = { + {flutter::EncodableValue("event"), flutter::EncodableValue("completed")}, + }; + player->PushEvent(flutter::EncodableValue(result)); + player->Pause(); } @@ -495,10 +739,8 @@ void VideoPlayer::OnError(int error_code, void *data) { get_error_message(error_code)); VideoPlayer *player = static_cast(data); - if (player->event_sink_) { - player->event_sink_->Error( - "Player error", std::string("Error: ") + get_error_message(error_code)); - } + player->SendError("Player error", + std::string("Error: ") + get_error_message(error_code)); } void VideoPlayer::OnInterrupted(player_interrupted_code_e code, void *data) { @@ -506,10 +748,7 @@ void VideoPlayer::OnInterrupted(player_interrupted_code_e code, void *data) { VideoPlayer *player = static_cast(data); player->is_interrupted_ = true; - if (player->event_sink_) { - player->event_sink_->Error("Player interrupted", - "Video player has been interrupted."); - } + player->SendError("Player interrupted", "Video player has been interrupted."); } std::vector VideoPlayer::OnLicenseChallenge( diff --git a/packages/video_player_videohole/tizen/src/video_player.h b/packages/video_player_videohole/tizen/src/video_player.h index 077559c99..1fdf9038a 100644 --- a/packages/video_player_videohole/tizen/src/video_player.h +++ b/packages/video_player_videohole/tizen/src/video_player.h @@ -5,6 +5,7 @@ #ifndef FLUTTER_PLUGIN_VIDEO_PLAYER_H_ #define FLUTTER_PLUGIN_VIDEO_PLAYER_H_ +#include #include #include #include @@ -12,18 +13,58 @@ #include #include +#include +#include #include #include #include "drm_manager.h" #include "video_player_options.h" +#define MAX_STRING_NAME_LEN 255 +#define MMPLAYER_FOUR_CC_LEN 14 +#define PLAYER_LANG_NAME_SIZE 10 + +typedef struct { + char fourCC[MMPLAYER_FOUR_CC_LEN + 1]; /**< codec fourcc */ + char name[MAX_STRING_NAME_LEN]; /**< name: video/audio, it maybe not exit in + some track*/ + /*dynamic infos in hls,ss,dash streams*/ + int width; /**< resolution width */ + int height; /**< resolution height */ + int bit_rate; /**< bitrate in bps */ +} player_video_track_info_v2; + +typedef struct { + char fourCC[MMPLAYER_FOUR_CC_LEN + 1]; /**< codec fourcc */ + char language[PLAYER_LANG_NAME_SIZE]; /**< language info*/ + /*dynamic infos in hls,ss,dash streams*/ + int sample_rate; /**< sample rate in this track*/ + int channel; /**< channel in this track*/ + int bit_rate; /**< bitrate in this track*/ +} player_audio_track_info_v2; + +typedef struct { + char fourCC[MMPLAYER_FOUR_CC_LEN + 1]; /**< codec fourcc */ + char language[PLAYER_LANG_NAME_SIZE]; /**< language info*/ + int subtitle_type; /**< text subtitle = 0, picture subtitle = 1 */ +} player_subtitle_track_info_v2; + typedef void (*FuncEcoreWl2WindowGeometryGet)(void *window, int *x, int *y, int *width, int *height); typedef int (*FuncPlayerSetEcoreWlDisplay)(player_h player, player_display_type_e type, void *ecore_wl_window, int x, int y, int width, int height); +typedef int (*FuncPlayerGetTrackCountV2)(player_h player, + player_stream_type_e type, + int *pcount); +typedef int (*FuncPlayerGetVideoTrackInfoV2)( + player_h player, int index, player_video_track_info_v2 **track_info); +typedef int (*FuncPlayerGetAudioTrackInfoV2)( + player_h player, int index, player_audio_track_info_v2 **track_info); +typedef int (*FuncPlayerGetSubtitleTrackInfoV2)( + player_h player, int index, player_subtitle_track_info_v2 **track_info); class VideoPlayer { public: @@ -45,10 +86,16 @@ class VideoPlayer { void SetPlaybackSpeed(double speed); void SeekTo(int32_t position, SeekCompletedCallback callback); int32_t GetPosition(); + flutter::EncodableList getTrackInfo(int32_t track_type); + void SetTrackSelection(int32_t track_id, int32_t track_type); void RegisterSendPort(Dart_Port send_port) { send_port_ = send_port; } private: + void SendPendingEvents(); + void PushEvent(const flutter::EncodableValue &encodable_value); + void SendError(const std::string &error_code, + const std::string &error_message); bool SetDisplay(); void SetUpEventChannel(flutter::BinaryMessenger *messenger); void Initialize(); @@ -86,6 +133,11 @@ class VideoPlayer { SeekCompletedCallback on_seek_completed_; Dart_Port send_port_; + + Ecore_Pipe *sink_event_pipe_ = nullptr; + std::mutex queue_mutex_; + std::queue encodable_event_queue_; + std::queue> error_event_queue_; }; #endif // FLUTTER_PLUGIN_VIDEO_PLAYER_H_ diff --git a/packages/video_player_videohole/tizen/src/video_player_tizen_plugin.cc b/packages/video_player_videohole/tizen/src/video_player_tizen_plugin.cc index b72ae520a..11a2962a3 100644 --- a/packages/video_player_videohole/tizen/src/video_player_tizen_plugin.cc +++ b/packages/video_player_videohole/tizen/src/video_player_tizen_plugin.cc @@ -44,6 +44,9 @@ class VideoPlayerTizenPlugin : public flutter::Plugin, void SeekTo( const PositionMessage &msg, std::function reply)> result) override; + virtual ErrorOr Track(const TrackTypeMessage &msg) override; + std::optional SetTrackSelection( + const SelectedTracksMessage &msg) override; std::optional Pause(const PlayerMessage &msg) override; std::optional SetMixWithOthers( const MixWithOthersMessage &msg) override; @@ -243,6 +246,29 @@ void VideoPlayerTizenPlugin::SeekTo( player->SeekTo(msg.position(), [result]() -> void { result(std::nullopt); }); } +ErrorOr VideoPlayerTizenPlugin::Track( + const TrackTypeMessage &msg) { + VideoPlayer *player = FindPlayerById(msg.player_id()); + + if (!player) { + return FlutterError("Invalid argument", "Player not found."); + } + + TrackMessage result(msg.player_id(), player->getTrackInfo(msg.track_type())); + return result; +} + +std::optional VideoPlayerTizenPlugin::SetTrackSelection( + const SelectedTracksMessage &msg) { + VideoPlayer *player = FindPlayerById(msg.player_id()); + if (!player) { + return FlutterError("Invalid argument", "Player not found."); + } + player->SetTrackSelection(msg.track_id(), msg.track_type()); + + return std::nullopt; +} + std::optional VideoPlayerTizenPlugin::SetDisplayGeometry( const GeometryMessage &msg) { VideoPlayer *player = FindPlayerById(msg.player_id()); diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index fe58788fe..4bbc7bd7d 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md @@ -1,5 +1,9 @@ -## NEXT +## 0.8.0 +* Update webivew_flutter to 4.2.3. +* Update webview_flutter_platform_interface to 2.3.0. +* Update integration_test. +* Implement `NavigationDelegate(onUrlChange)`. * Increase the minimum Flutter version to 3.3. ## 0.7.1 diff --git a/packages/webview_flutter/README.md b/packages/webview_flutter/README.md index cab6b438b..c4f668d34 100644 --- a/packages/webview_flutter/README.md +++ b/packages/webview_flutter/README.md @@ -22,8 +22,8 @@ This package is not an _endorsed_ implementation of `webview_flutter`. Therefore ```yaml dependencies: - webview_flutter: ^4.0.2 - webview_flutter_tizen: ^0.7.1 + webview_flutter: ^4.2.3 + webview_flutter_tizen: ^0.8.0 ``` ## Example diff --git a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart index a3941c155..f538e3eff 100644 --- a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart @@ -20,7 +20,7 @@ Future main() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); final HttpServer server = await HttpServer.bind(InternetAddress.anyIPv4, 0); - server.forEach((HttpRequest request) { + unawaited(server.forEach((HttpRequest request) { if (request.uri.path == '/hello.txt') { request.response.writeln('Hello, world.'); } else if (request.uri.path == '/secondary.txt') { @@ -33,7 +33,7 @@ Future main() async { fail('unexpected request: ${request.method} ${request.uri}'); } request.response.close(); - }); + })); final String prefixUrl = 'http://${server.address.address}:${server.port}'; final String primaryUrl = '$prefixUrl/hello.txt'; final String secondaryUrl = '$prefixUrl/secondary.txt'; @@ -42,11 +42,11 @@ Future main() async { testWidgets('loadRequest', (WidgetTester tester) async { final Completer pageFinished = Completer(); - final WebViewController controller = WebViewController() - ..setNavigationDelegate( - NavigationDelegate(onPageFinished: (_) => pageFinished.complete()), - ) - ..loadRequest(Uri.parse(primaryUrl)); + final WebViewController controller = WebViewController(); + unawaited(controller.setNavigationDelegate( + NavigationDelegate(onPageFinished: (_) => pageFinished.complete()), + )); + unawaited(controller.loadRequest(Uri.parse(primaryUrl))); await tester.pumpWidget(WebViewWidget(controller: controller)); @@ -59,12 +59,12 @@ Future main() async { testWidgets('runJavaScriptReturningResult', (WidgetTester tester) async { final Completer pageFinished = Completer(); - final WebViewController controller = WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setNavigationDelegate( - NavigationDelegate(onPageFinished: (_) => pageFinished.complete()), - ) - ..loadRequest(Uri.parse(primaryUrl)); + final WebViewController controller = WebViewController(); + unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted)); + unawaited(controller.setNavigationDelegate( + NavigationDelegate(onPageFinished: (_) => pageFinished.complete()), + )); + unawaited(controller.loadRequest(Uri.parse(primaryUrl))); await tester.pumpWidget(WebViewWidget(controller: controller)); @@ -83,15 +83,15 @@ Future main() async { final StreamController pageLoads = StreamController(); - final WebViewController controller = WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setNavigationDelegate( - NavigationDelegate(onPageFinished: (String url) => pageLoads.add(url)), - ); + final WebViewController controller = WebViewController(); + unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted)); + unawaited(controller.setNavigationDelegate( + NavigationDelegate(onPageFinished: (String url) => pageLoads.add(url)), + )); await tester.pumpWidget(WebViewWidget(controller: controller)); - controller.loadRequest(Uri.parse(headersUrl), headers: headers); + unawaited(controller.loadRequest(Uri.parse(headersUrl), headers: headers)); await pageLoads.stream.firstWhere((String url) => url == headersUrl); @@ -103,11 +103,11 @@ Future main() async { testWidgets('JavascriptChannel', (WidgetTester tester) async { final Completer pageFinished = Completer(); - final WebViewController controller = WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setNavigationDelegate( - NavigationDelegate(onPageFinished: (_) => pageFinished.complete()), - ); + final WebViewController controller = WebViewController(); + unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted)); + unawaited(controller.setNavigationDelegate( + NavigationDelegate(onPageFinished: (_) => pageFinished.complete()), + )); final Completer channelCompleter = Completer(); await controller.addJavaScriptChannel( @@ -164,13 +164,13 @@ Future main() async { testWidgets('set custom userAgent', (WidgetTester tester) async { final Completer pageFinished = Completer(); - final WebViewController controller = WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setNavigationDelegate(NavigationDelegate( - onPageFinished: (_) => pageFinished.complete(), - )) - ..setUserAgent('Custom_User_Agent1') - ..loadRequest(Uri.parse('about:blank')); + final WebViewController controller = WebViewController(); + unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted)); + unawaited(controller.setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageFinished.complete(), + ))); + unawaited(controller.setUserAgent('Custom_User_Agent1')); + unawaited(controller.loadRequest(Uri.parse('about:blank'))); await tester.pumpWidget(WebViewWidget(controller: controller)); @@ -193,14 +193,14 @@ Future main() async { base64Encode(const Utf8Encoder().convert(getTitleTest)); final Completer pageLoaded = Completer(); - final WebViewController controller = WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setNavigationDelegate(NavigationDelegate( - onPageFinished: (_) => pageLoaded.complete(), - )) - ..loadRequest( - Uri.parse('data:text/html;charset=utf-8;base64,$getTitleTestBase64'), - ); + final WebViewController controller = WebViewController(); + unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted)); + unawaited(controller.setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageLoaded.complete(), + ))); + unawaited(controller.loadRequest( + Uri.parse('data:text/html;charset=utf-8;base64,$getTitleTestBase64'), + )); await tester.pumpWidget(WebViewWidget(controller: controller)); @@ -243,14 +243,14 @@ Future main() async { base64Encode(const Utf8Encoder().convert(scrollTestPage)); final Completer pageLoaded = Completer(); - final WebViewController controller = WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setNavigationDelegate(NavigationDelegate( - onPageFinished: (_) => pageLoaded.complete(), - )) - ..loadRequest(Uri.parse( - 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', - )); + final WebViewController controller = WebViewController(); + unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted)); + unawaited(controller.setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageLoaded.complete(), + ))); + unawaited(controller.loadRequest(Uri.parse( + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + ))); await tester.pumpWidget(WebViewWidget(controller: controller)); @@ -290,20 +290,20 @@ Future main() async { testWidgets('can allow requests', (WidgetTester tester) async { Completer pageLoaded = Completer(); - final WebViewController controller = WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setNavigationDelegate(NavigationDelegate( - onPageFinished: (_) => pageLoaded.complete(), - onNavigationRequest: (NavigationRequest navigationRequest) { - return (navigationRequest.url.contains('youtube.com')) - ? NavigationDecision.prevent - : NavigationDecision.navigate; - }, - )); + final WebViewController controller = WebViewController(); + unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted)); + unawaited(controller.setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageLoaded.complete(), + onNavigationRequest: (NavigationRequest navigationRequest) { + return (navigationRequest.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + ))); await tester.pumpWidget(WebViewWidget(controller: controller)); - controller.loadRequest(Uri.parse(blankPageEncoded)); + unawaited(controller.loadRequest(Uri.parse(blankPageEncoded))); await pageLoaded.future; // Wait for initial page load. @@ -319,13 +319,14 @@ Future main() async { final Completer errorCompleter = Completer(); - final WebViewController controller = WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setNavigationDelegate( - NavigationDelegate(onWebResourceError: (WebResourceError error) { - errorCompleter.complete(error); - })) - ..loadRequest(Uri.parse('https://www.notawebsite..com')); + final WebViewController controller = WebViewController(); + unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted)); + unawaited(controller.setNavigationDelegate( + NavigationDelegate(onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }))); + unawaited( + controller.loadRequest(Uri.parse('https://www.notawebsite..com'))); await tester.pumpWidget(WebViewWidget(controller: controller)); @@ -339,17 +340,17 @@ Future main() async { Completer(); final Completer pageFinishCompleter = Completer(); - final WebViewController controller = WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setNavigationDelegate(NavigationDelegate( - onPageFinished: (_) => pageFinishCompleter.complete(), - onWebResourceError: (WebResourceError error) { - errorCompleter.complete(error); - }, - )) - ..loadRequest( - Uri.parse('data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+'), - ); + final WebViewController controller = WebViewController(); + unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted)); + unawaited(controller.setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageFinishCompleter.complete(), + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + ))); + unawaited(controller.loadRequest( + Uri.parse('data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+'), + )); await tester.pumpWidget(WebViewWidget(controller: controller)); @@ -360,19 +361,19 @@ Future main() async { testWidgets('can block requests', (WidgetTester tester) async { Completer pageLoaded = Completer(); - final WebViewController controller = WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setNavigationDelegate(NavigationDelegate( - onPageFinished: (_) => pageLoaded.complete(), - onNavigationRequest: (NavigationRequest navigationRequest) { - return (navigationRequest.url.contains('youtube.com')) - ? NavigationDecision.prevent - : NavigationDecision.navigate; - })); + final WebViewController controller = WebViewController(); + unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted)); + unawaited(controller.setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageLoaded.complete(), + onNavigationRequest: (NavigationRequest navigationRequest) { + return (navigationRequest.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }))); await tester.pumpWidget(WebViewWidget(controller: controller)); - controller.loadRequest(Uri.parse(blankPageEncoded)); + unawaited(controller.loadRequest(Uri.parse(blankPageEncoded))); await pageLoaded.future; // Wait for initial page load. @@ -392,21 +393,21 @@ Future main() async { testWidgets('supports asynchronous decisions', (WidgetTester tester) async { Completer pageLoaded = Completer(); - final WebViewController controller = WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setNavigationDelegate(NavigationDelegate( - onPageFinished: (_) => pageLoaded.complete(), - onNavigationRequest: (NavigationRequest navigationRequest) async { - NavigationDecision decision = NavigationDecision.prevent; - decision = await Future.delayed( - const Duration(milliseconds: 10), - () => NavigationDecision.navigate); - return decision; - })); + final WebViewController controller = WebViewController(); + unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted)); + unawaited(controller.setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageLoaded.complete(), + onNavigationRequest: (NavigationRequest navigationRequest) async { + NavigationDecision decision = NavigationDecision.prevent; + decision = await Future.delayed( + const Duration(milliseconds: 10), + () => NavigationDecision.navigate); + return decision; + }))); await tester.pumpWidget(WebViewWidget(controller: controller)); - controller.loadRequest(Uri.parse(blankPageEncoded)); + unawaited(controller.loadRequest(Uri.parse(blankPageEncoded))); await pageLoaded.future; // Wait for initial page load. @@ -417,6 +418,62 @@ Future main() async { final String? currentUrl = await controller.currentUrl(); expect(currentUrl, secondaryUrl); }); + + testWidgets('can receive url changes', (WidgetTester tester) async { + final Completer pageLoaded = Completer(); + + final WebViewController controller = WebViewController(); + final Completer urlChangeCompleter = Completer(); + unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted)); + unawaited(controller.setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageLoaded.complete(), + ))); + unawaited(controller.loadRequest(Uri.parse(blankPageEncoded))); + + await tester.pumpWidget(WebViewWidget(controller: controller)); + + await pageLoaded.future; + + await controller.setNavigationDelegate(NavigationDelegate( + onUrlChange: (UrlChange change) { + urlChangeCompleter.complete(change.url); + }, + )); + await controller.runJavaScript('location.href = "$primaryUrl"'); + + await expectLater(urlChangeCompleter.future, completion(primaryUrl)); + }); + + testWidgets('can receive updates to history state', + (WidgetTester tester) async { + final Completer pageLoaded = Completer(); + + final NavigationDelegate navigationDelegate = NavigationDelegate( + onPageFinished: (_) => pageLoaded.complete(), + ); + + final WebViewController controller = WebViewController(); + unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted)); + unawaited(controller.setNavigationDelegate(navigationDelegate)); + unawaited(controller.loadRequest(Uri.parse(primaryUrl))); + + await tester.pumpWidget(WebViewWidget(controller: controller)); + + await pageLoaded.future; + + final Completer urlChangeCompleter = Completer(); + await controller.setNavigationDelegate(NavigationDelegate( + onUrlChange: (UrlChange change) { + urlChangeCompleter.complete(change.url); + }, + )); + + await controller.runJavaScript( + 'window.history.pushState({}, "", "secondary.txt");', + ); + + await expectLater(urlChangeCompleter.future, completion(secondaryUrl)); + }); }); } diff --git a/packages/webview_flutter/example/lib/main.dart b/packages/webview_flutter/example/lib/main.dart index 99a0f6a4d..8265578d8 100644 --- a/packages/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/example/lib/main.dart @@ -115,6 +115,9 @@ Page resource error: debugPrint('allowing navigation to ${request.url}'); return NavigationDecision.navigate; }, + onUrlChange: (UrlChange change) { + debugPrint('url change to ${change.url}'); + }, ), ) ..addJavaScriptChannel( @@ -407,7 +410,7 @@ class SampleMenu extends StatelessWidget { } Widget _getCookieList(String cookies) { - if (cookies == null || cookies == '""') { + if (cookies == '""') { return Container(); } final List cookieList = cookies.split(';'); diff --git a/packages/webview_flutter/example/pubspec.yaml b/packages/webview_flutter/example/pubspec.yaml index bed8be057..c59c02c68 100644 --- a/packages/webview_flutter/example/pubspec.yaml +++ b/packages/webview_flutter/example/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: path_provider: ^2.0.7 path_provider_tizen: path: ../../path_provider/ - webview_flutter: ^4.0.2 + webview_flutter: ^4.2.3 webview_flutter_tizen: path: ../ diff --git a/packages/webview_flutter/lib/src/tizen_webview.dart b/packages/webview_flutter/lib/src/tizen_webview.dart index 06c558356..ca2a68f4f 100644 --- a/packages/webview_flutter/lib/src/tizen_webview.dart +++ b/packages/webview_flutter/lib/src/tizen_webview.dart @@ -18,6 +18,10 @@ class TizenWebView { /// /// Defaults to false. bool hasNavigationDelegate = false; + late int _viewId; + + /// Get the if of tizen webview. + int get viewId => _viewId; late final MethodChannel _tizenWebViewChannel; bool _isCreated = false; @@ -58,6 +62,7 @@ class TizenWebView { /// Called when [TizenView] is created. Invokes the requested method call before [TizenWebView] is created. void onCreate(int viewId) { _isCreated = true; + _viewId = viewId; _tizenWebViewChannel = MethodChannel(kTizenWebViewChannelName + viewId.toString()); _tizenWebViewChannel.setMethodCallHandler(_onMethodCall); diff --git a/packages/webview_flutter/lib/src/tizen_webview_controller.dart b/packages/webview_flutter/lib/src/tizen_webview_controller.dart index af6307147..de79235f2 100644 --- a/packages/webview_flutter/lib/src/tizen_webview_controller.dart +++ b/packages/webview_flutter/lib/src/tizen_webview_controller.dart @@ -28,10 +28,10 @@ class TizenWebViewController extends PlatformWebViewController { /// Called when [TizenView] is created. void onCreate(int viewId) { + _webview.onCreate(viewId); if (_webview.hasNavigationDelegate) { - _tizenNavigationDelegate.onCreate(viewId); + _tizenNavigationDelegate.createNavigationDelegateChannel(viewId); } - _webview.onCreate(viewId); } @override @@ -139,7 +139,11 @@ class TizenWebViewController extends PlatformWebViewController { Future setPlatformNavigationDelegate( covariant TizenNavigationDelegate handler) async { _tizenNavigationDelegate = handler; - _webview.hasNavigationDelegate = true; + if (_webview.hasNavigationDelegate) { + _tizenNavigationDelegate.createNavigationDelegateChannel(_webview.viewId); + } else { + _webview.hasNavigationDelegate = true; + } } @override @@ -158,6 +162,17 @@ class TizenWebViewController extends PlatformWebViewController { @override Future setUserAgent(String? userAgent) => _webview.setUserAgent(userAgent); + + @override + Future setOnPlatformPermissionRequest( + void Function( + PlatformWebViewPermissionRequest request, + ) onPermissionRequest, + ) async { + throw UnimplementedError( + 'This version of `TizenWebViewController` currently has no ' + 'implementation.'); + } } /// An implementation of [PlatformWebViewWidget] with the Tizen WebView API. @@ -272,9 +287,10 @@ class TizenNavigationDelegate extends PlatformNavigationDelegate { ProgressCallback? _onProgress; WebResourceErrorCallback? _onWebResourceError; NavigationRequestCallback? _onNavigationRequest; + UrlChangeCallback? _onUrlChange; /// Called when [TizenView] is created. - void onCreate(int viewId) { + void createNavigationDelegateChannel(int viewId) { _navigationDelegateChannel = MethodChannel(kTizenNavigationDelegateChannelName + viewId.toString()); _navigationDelegateChannel.setMethodCallHandler((MethodCall call) async { @@ -309,6 +325,11 @@ class TizenNavigationDelegate extends PlatformNavigationDelegate { )); } return null; + case 'onUrlChange': + if (_onUrlChange != null) { + _onUrlChange!(UrlChange(url: arguments['url']! as String)); + } + return null; } throw MissingPluginException( @@ -381,4 +402,9 @@ class TizenNavigationDelegate extends PlatformNavigationDelegate { ) async { _onWebResourceError = onWebResourceError; } + + @override + Future setOnUrlChange(UrlChangeCallback onUrlChange) async { + _onUrlChange = onUrlChange; + } } diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index 63094431a..d3becad4e 100644 --- a/packages/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_tizen description: Tizen implementation of the webview_flutter plugin. homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/master/packages/webview_flutter -version: 0.7.1 +version: 0.8.0 environment: sdk: ">=2.18.0 <4.0.0" @@ -20,5 +20,5 @@ dependencies: flutter: sdk: flutter flutter_tizen: ^0.2.1 - webview_flutter: ^4.0.2 - webview_flutter_platform_interface: ^2.0.1 + webview_flutter: ^4.2.3 + webview_flutter_platform_interface: ^2.3.0 diff --git a/packages/webview_flutter/tizen/src/webview.cc b/packages/webview_flutter/tizen/src/webview.cc index d81e131e2..abd15aa13 100644 --- a/packages/webview_flutter/tizen/src/webview.cc +++ b/packages/webview_flutter/tizen/src/webview.cc @@ -165,6 +165,8 @@ void WebView::Dispose() { evas_object_smart_callback_del(webview_instance_, "policy,navigation,decide", &WebView::OnNavigationPolicy); + evas_object_smart_callback_del(webview_instance_, "url,changed", + &WebView::OnUrlChange); evas_object_del(webview_instance_); } } @@ -297,6 +299,8 @@ void WebView::InitWebView() { &WebView::OnConsoleMessage, this); evas_object_smart_callback_add(webview_instance_, "policy,navigation,decide", &WebView::OnNavigationPolicy, this); + evas_object_smart_callback_add(webview_instance_, "url,changed", + &WebView::OnUrlChange, this); Resize(width_, height_); evas_object_show(webview_instance_); @@ -665,6 +669,15 @@ void WebView::OnNavigationPolicy(void* data, Evas_Object* obj, std::move(result)); } +void WebView::OnUrlChange(void* data, Evas_Object* obj, void* event_info) { + WebView* webview = static_cast(data); + std::string url = std::string(ewk_view_url_get(webview->webview_instance_)); + flutter::EncodableMap args = { + {flutter::EncodableValue("url"), flutter::EncodableValue(url)}}; + webview->navigation_delegate_channel_->InvokeMethod( + "onUrlChange", std::make_unique(args)); +} + void WebView::OnEvaluateJavaScript(Evas_Object* obj, const char* result_value, void* user_data) { FlMethodResult* result = static_cast(user_data); diff --git a/packages/webview_flutter/tizen/src/webview.h b/packages/webview_flutter/tizen/src/webview.h index 7f5a1d0b0..a3eb8ebc8 100644 --- a/packages/webview_flutter/tizen/src/webview.h +++ b/packages/webview_flutter/tizen/src/webview.h @@ -75,6 +75,7 @@ class WebView : public PlatformView { static void OnConsoleMessage(void* data, Evas_Object* obj, void* event_info); static void OnNavigationPolicy(void* data, Evas_Object* obj, void* event_info); + static void OnUrlChange(void* data, Evas_Object* obj, void* event_info); static void OnEvaluateJavaScript(Evas_Object* obj, const char* result_value, void* user_data); static void OnJavaScriptMessage(Evas_Object* obj, Ewk_Script_Message message);