diff --git a/.github/labeler.yml b/.github/labeler.yml index 3e0a4af1d..3e56bda45 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -65,6 +65,8 @@ - packages/url_launcher/**/* "p: video_player": - packages/video_player/**/* +"p: video_player_videohole": + - packages/video_player_videohole/**/* "p: wakelock": - packages/wakelock/**/* "p: wearable_rotary": diff --git a/.github/recipe.yaml b/.github/recipe.yaml index fa4e20807..bae29be6d 100644 --- a/.github/recipe.yaml +++ b/.github/recipe.yaml @@ -35,6 +35,7 @@ plugins: flutter_webrtc: [] geolocator: [] network_info_plus: [] + video_player_videohole: [] # Only testable with the drive command: https://github.com/flutter-tizen/plugins/issues/272 tizen_app_control: [] diff --git a/README.md b/README.md index c99cf8f18..1cd177833 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_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_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 | | [**wearable_rotary**](packages/wearable_rotary) | (Tizen-only) | [![pub package](https://img.shields.io/pub/v/wearable_rotary.svg)](https://pub.dev/packages/wearable_rotary) | N/A | | [**webview_flutter_lwe**](packages/webview_flutter_lwe) | [webview_flutter](https://pub.dev/packages/webview_flutter) (1st-party) | [![pub package](https://img.shields.io/pub/v/webview_flutter_lwe.svg)](https://pub.dev/packages/webview_flutter_lwe) | No | @@ -56,7 +57,7 @@ The _"non-endorsed"_ status means that the plugin is not endorsed by the origina | Package name | API level | Watch | Watch
emulator | TV | TV
emulator | Remarks | |-|:-:|:-:|:-:|:-:|:-:|-| -| [**audioplayers_tizen**](packages/audioplayers) | 4.0 | ✔️ | ✔️ | ⚠️ | ⚠️ | Functional limitations | +| [**audioplayers_tizen**](packages/audioplayers) | 4.0 | ✔️ | ✔️ | ✔️ | ✔️ | | [**battery_plus_tizen**](packages/battery_plus) | 4.0 | ✔️ | ✔️ | ❌ | ❌ | No battery | | [**camera_tizen**](packages/camera) | 4.0 | ❌ | ❌ | ❌ | ❌ | No camera | | [**connectivity_plus_tizen**](packages/connectivity_plus) | 4.0 | ✔️ | ⚠️ | ✔️ | ✔️ | Returns incorrect connection status | @@ -88,9 +89,9 @@ The _"non-endorsed"_ status means that the plugin is not endorsed by the origina | [**tizen_package_manager**](packages/tizen_package_manager) | 4.0 | ✔️ | ✔️ | ✔️ | ✔️ | | [**tizen_rpc_port**](packages/tizen_rpc_port) | 6.5 | ✔️ | ✔️ | ✔️ | ✔️ | | [**url_launcher_tizen**](packages/url_launcher) | 4.0 | ✔️ | ❌ | ✔️ | ❌ | No browser app | -| [**video_player_tizen**](packages/video_player) | 4.0 | ✔️ | ✔️ | ⚠️ | ❌ | Functional limitations,
TV emulator issue | +| [**video_player_tizen**](packages/video_player) | 4.0 | ✔️ | ✔️ | ✔️ | ❌ | TV emulator issue | +| [**video_player_videohole**](packages/video_player_videohole) | 4.0 | ❌ | ❌ | ✔️ | ❌ | Only for TV devices | | [**wakelock_tizen**](packages/wakelock) | 4.0 | ✔️ | ✔️ | ❌ | ❌ | Cannot override system settings | | [**wearable_rotary**](packages/wearable_rotary) | 4.0 | ✔️ | ✔️ | ❌ | ❌ | Not applicable for TV | | [**webview_flutter_lwe**](packages/webview_flutter_lwe) | 5.5 | ✔️ | ✔️ | ✔️ | ✔️ | Not for production use | | [**webview_flutter_tizen**](packages/webview_flutter) | 5.5 | ❌ | ❌ | ✔️ | ✔️ | API not supported | - diff --git a/packages/video_player_videohole/.gitignore b/packages/video_player_videohole/.gitignore new file mode 100644 index 000000000..e9dc58d3d --- /dev/null +++ b/packages/video_player_videohole/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ diff --git a/packages/video_player_videohole/CHANGELOG.md b/packages/video_player_videohole/CHANGELOG.md new file mode 100644 index 000000000..607323422 --- /dev/null +++ b/packages/video_player_videohole/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 + +* Initial release. diff --git a/packages/video_player_videohole/LICENSE b/packages/video_player_videohole/LICENSE new file mode 100644 index 000000000..036fb575e --- /dev/null +++ b/packages/video_player_videohole/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_videohole/README.md b/packages/video_player_videohole/README.md new file mode 100644 index 000000000..b123ec123 --- /dev/null +++ b/packages/video_player_videohole/README.md @@ -0,0 +1,124 @@ +# video_player_videohole + +[![pub package](https://img.shields.io/pub/v/video_player_videohole.svg)](https://pub.dev/packages/video_player_videohole) + +A fork of the [`video_player`](https://pub.dev/packages/video_player) plugin to support playback of DRM streams (Widevine and PlayReady) on Tizen TV devices. + +This plugin is only supported on Tizen TV devices. If you are targeting other device types or don't plan to play 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_videohole` as a dependency in your `pubspec.yaml` file. + +```yaml +dependencies: + video_player_videohole: ^0.1.0 +``` + +Then you can import `video_player_videohole` in your Dart code: + +```dart +import 'package:video_player_videohole/video_player.dart'; +``` + +Note that `video_player_videohole` 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_videohole` in the files respectively. + +### Example + +```dart +import 'package:flutter/material.dart'; +import 'package:video_player_videohole/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 + +This plugin is not supported on TV emulators. + +The following options are not currently supported. + +- The `httpHeaders` option of `VideoPlayerController.network` +- `VideoPlayerOptions.allowBackgroundPlayback` +- `VideoPlayerOptions.mixWithOthers` + +This plugin has the following limitations. + +- 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. +- Dash sidecar subtitles are only supported on Tizen 7.0 and later. diff --git a/packages/video_player_videohole/example/.gitignore b/packages/video_player_videohole/example/.gitignore new file mode 100644 index 000000000..1adcc2fb3 --- /dev/null +++ b/packages/video_player_videohole/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_videohole/example/README.md b/packages/video_player_videohole/example/README.md new file mode 100644 index 000000000..524e2e207 --- /dev/null +++ b/packages/video_player_videohole/example/README.md @@ -0,0 +1,7 @@ +# video_player_videohole_example + +Demonstrates how to use the video_player_videohole 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_videohole/example/assets/Audio.mp3 b/packages/video_player_videohole/example/assets/Audio.mp3 new file mode 100644 index 000000000..355eb9b2e Binary files /dev/null and b/packages/video_player_videohole/example/assets/Audio.mp3 differ diff --git a/packages/video_player_videohole/example/assets/Butterfly-209.mp4 b/packages/video_player_videohole/example/assets/Butterfly-209.mp4 new file mode 100644 index 000000000..c8489799f Binary files /dev/null and b/packages/video_player_videohole/example/assets/Butterfly-209.mp4 differ diff --git a/packages/video_player_videohole/example/integration_test/video_player_test.dart b/packages/video_player_videohole/example/integration_test/video_player_test.dart new file mode 100644 index 000000000..cf2fcd259 --- /dev/null +++ b/packages/video_player_videohole/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_videohole/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_videohole/example/lib/main.dart b/packages/video_player_videohole/example/lib/main.dart new file mode 100644 index 000000000..48c775374 --- /dev/null +++ b/packages/video_player_videohole/example/lib/main.dart @@ -0,0 +1,488 @@ +// 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 + +/// 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_videohole/video_player.dart'; + +void main() { + runApp( + MaterialApp( + home: _App(), + ), + ); +} + +class _App extends StatelessWidget { + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 5, + 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'), + ], + ), + ), + body: TabBarView( + children: [ + _Mp4RemoteVideo(), + _HlsRomoteVideo(), + _DashRomoteVideo(), + _DrmRemoteVideo(), + _DrmRemoteVideo2(), + ], + ), + ), + ); + } +} + +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'); + + _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; + }, + ), + ); + + _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 _ControlsOverlay extends StatelessWidget { + const _ControlsOverlay({Key? key, required this.controller}) + : super(key: key); + + 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'), + ), + ), + ), + ], + ); + } +} diff --git a/packages/video_player_videohole/example/pubspec.yaml b/packages/video_player_videohole/example/pubspec.yaml new file mode 100644 index 000000000..36f406b03 --- /dev/null +++ b/packages/video_player_videohole/example/pubspec.yaml @@ -0,0 +1,34 @@ +name: video_player_videohole_example +description: Demonstrates how to use the video_player_videohole plugin. +publish_to: "none" + +dependencies: + flutter: + sdk: flutter + http: ^0.13.0 + video_player_videohole: + 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 + +environment: + flutter: ">=3.0.0" + sdk: ">=2.12.0 <3.0.0" diff --git a/packages/video_player_videohole/example/test_driver/integration_test.dart b/packages/video_player_videohole/example/test_driver/integration_test.dart new file mode 100644 index 000000000..b38629cca --- /dev/null +++ b/packages/video_player_videohole/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_videohole/example/tizen/.gitignore b/packages/video_player_videohole/example/tizen/.gitignore new file mode 100644 index 000000000..750f3af1b --- /dev/null +++ b/packages/video_player_videohole/example/tizen/.gitignore @@ -0,0 +1,5 @@ +flutter/ +.vs/ +*.user +bin/ +obj/ diff --git a/packages/video_player_videohole/example/tizen/App.cs b/packages/video_player_videohole/example/tizen/App.cs new file mode 100644 index 000000000..6dd4a6356 --- /dev/null +++ b/packages/video_player_videohole/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_videohole/example/tizen/Runner.csproj b/packages/video_player_videohole/example/tizen/Runner.csproj new file mode 100644 index 000000000..351a83987 --- /dev/null +++ b/packages/video_player_videohole/example/tizen/Runner.csproj @@ -0,0 +1,26 @@ + + + + Exe + tizen40 + + + + portable + + + none + + + + + + + + + + %(RecursiveDir) + + + + diff --git a/packages/video_player_videohole/example/tizen/shared/res/ic_launcher.png b/packages/video_player_videohole/example/tizen/shared/res/ic_launcher.png new file mode 100644 index 000000000..4d6372eeb Binary files /dev/null and b/packages/video_player_videohole/example/tizen/shared/res/ic_launcher.png differ diff --git a/packages/video_player_videohole/example/tizen/tizen-manifest.xml b/packages/video_player_videohole/example/tizen/tizen-manifest.xml new file mode 100644 index 000000000..7ff19bbd3 --- /dev/null +++ b/packages/video_player_videohole/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_videohole/lib/src/closed_caption_file.dart b/packages/video_player_videohole/lib/src/closed_caption_file.dart new file mode 100644 index 000000000..324ffc471 --- /dev/null +++ b/packages/video_player_videohole/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_videohole/lib/src/drm_configs.dart b/packages/video_player_videohole/lib/src/drm_configs.dart new file mode 100644 index 000000000..025237ffb --- /dev/null +++ b/packages/video_player_videohole/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_videohole/lib/src/hole.dart b/packages/video_player_videohole/lib/src/hole.dart new file mode 100644 index 000000000..63922b7b3 --- /dev/null +++ b/packages/video_player_videohole/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({Key? key}) : super(key: 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_videohole/lib/src/messages.g.dart b/packages/video_player_videohole/lib/src/messages.g.dart new file mode 100644 index 000000000..05f8f52bd --- /dev/null +++ b/packages/video_player_videohole/lib/src/messages.g.dart @@ -0,0 +1,594 @@ +// Autogenerated from Pigeon (v6.0.3), 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 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, + }); + + String? asset; + + String? uri; + + String? packageName; + + String? formatHint; + + Map? httpHeaders; + + Map? drmConfigs; + + Object encode() { + return [ + asset, + uri, + packageName, + formatHint, + httpHeaders, + drmConfigs, + ]; + } + + 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(), + ); + } +} + +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 _TizenVideoPlayerApiCodec extends StandardMessageCodec { + const _TizenVideoPlayerApiCodec(); + @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 VolumeMessage) { + buffer.putUint8(135); + 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 VolumeMessage.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class TizenVideoPlayerApi { + /// Constructor for [TizenVideoPlayerApi]. 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. + TizenVideoPlayerApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _TizenVideoPlayerApiCodec(); + + Future initialize() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.TizenVideoPlayerApi.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.TizenVideoPlayerApi.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.TizenVideoPlayerApi.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.TizenVideoPlayerApi.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.TizenVideoPlayerApi.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.TizenVideoPlayerApi.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.TizenVideoPlayerApi.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 position(PlayerMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.TizenVideoPlayerApi.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.TizenVideoPlayerApi.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.TizenVideoPlayerApi.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.TizenVideoPlayerApi.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.TizenVideoPlayerApi.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_videohole/lib/src/sub_rip.dart b/packages/video_player_videohole/lib/src/sub_rip.dart new file mode 100644 index 000000000..7b807cd4d --- /dev/null +++ b/packages/video_player_videohole/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_videohole/lib/src/video_player_tizen.dart b/packages/video_player_videohole/lib/src/video_player_tizen.dart new file mode 100644 index 000000000..1f08c982c --- /dev/null +++ b/packages/video_player_videohole/lib/src/video_player_tizen.dart @@ -0,0 +1,177 @@ +// 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'; + +/// An implementation of [VideoPlayerPlatform] that uses the +/// Pigeon-generated [TizenVideoPlayerApi]. +class VideoPlayerTizen extends VideoPlayerPlatform { + final TizenVideoPlayerApi _api = TizenVideoPlayerApi(); + + @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(); + 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 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 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('flutter.io/videoPlayer/videoEvents$playerId'); + } + + static const Map _videoFormatStringMap = + { + VideoFormat.ss: 'ss', + VideoFormat.hls: 'hls', + VideoFormat.dash: 'dash', + VideoFormat.other: 'other', + }; +} diff --git a/packages/video_player_videohole/lib/src/web_vtt.dart b/packages/video_player_videohole/lib/src/web_vtt.dart new file mode 100644 index 000000000..5527e62b6 --- /dev/null +++ b/packages/video_player_videohole/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_videohole/lib/video_player.dart b/packages/video_player_videohole/lib/video_player.dart new file mode 100644 index 000000000..40181840f --- /dev/null +++ b/packages/video_player_videohole/lib/video_player.dart @@ -0,0 +1,1142 @@ +// 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:ffi' hide Size; +import 'dart:io'; +import 'dart:isolate'; + +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 'video_player_platform_interface.dart'; + +export 'src/closed_caption_file.dart'; +export 'src/drm_configs.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.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; + + /// 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, + 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, + 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, ' + 'buffered: $buffered, ' + 'isInitialized: $isInitialized, ' + 'isPlaying: $isPlaying, ' + 'isLooping: $isLooping, ' + 'isBuffering: $isBuffering, ' + 'volume: $volume, ' + 'playbackSpeed: $playbackSpeed, ' + 'errorDescription: $errorDescription)'; + } +} + +typedef _InitDartApi = int Function(Pointer); +typedef _InitDartApiNative = IntPtr Function(Pointer); + +typedef _RegisterSendPort = void Function(int, int); +typedef _RegisterSendPortNative = Void Function(Int64, Int64); + +class _CppRequest { + _CppRequest.fromList(List message) + : replyPort = message[0]! as SendPort, + pendingCall = message[1]! as int, + method = message[2]! as String, + data = message[3]! as Uint8List; + + final SendPort replyPort; + final int pendingCall; + final String method; + final Uint8List data; +} + +class _CppResponse { + _CppResponse(this.pendingCall, this.data); + + final int pendingCall; + final Uint8List data; + + List toList() => [pendingCall, data]; +} + +/// 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, + 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, + }) : 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, + 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, + 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; + + /// **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; + + /// 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, + ); + 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) { + final DynamicLibrary process = DynamicLibrary.process(); + final _InitDartApi initDartApi = + process.lookupFunction<_InitDartApiNative, _InitDartApi>( + 'VideoPlayerTizenPluginInitDartApi'); + initDartApi(NativeApi.initializeApiDLData); + + final ReceivePort receivePort = ReceivePort(); + receivePort.listen((dynamic message) async { + final _CppRequest request = + _CppRequest.fromList(message as List); + + if (request.method == 'onLicenseChallenge') { + final Uint8List challenge = request.data; + final Uint8List result = + await drmConfigs!.licenseCallback!(challenge); + + final _CppResponse response = + _CppResponse(request.pendingCall, result); + request.replyPort.send(response.toList()); + } + }); + + final _RegisterSendPort registerSendPort = + process.lookupFunction<_RegisterSendPortNative, _RegisterSendPort>( + 'VideoPlayerTizenPluginRegisterSendPort'); + registerSendPort(_playerId, receivePort.sendPort.nativePort); + } + + 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 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 _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); + } + + /// 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, {Key? key}) : super(key: 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 = _currentRect; + 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 get _currentRect { + final RenderObject? renderObject = + _videoBoxKey.currentContext?.findRenderObject(); + if (renderObject == null) { + return Rect.zero; + } + 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, { + Key? key, + this.colors = const VideoProgressColors(), + required this.allowScrubbing, + this.padding = const EdgeInsets.only(top: 5.0), + }) : super(key: key); + + /// 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({Key? key, this.text, this.textStyle}) : super(key: key); + + /// 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_videohole/lib/video_player_platform_interface.dart b/packages/video_player_videohole/lib/video_player_platform_interface.dart new file mode 100644 index 000000000..6d87887fa --- /dev/null +++ b/packages/video_player_videohole/lib/video_player_platform_interface.dart @@ -0,0 +1,392 @@ +// 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/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.'); + } + + /// 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.'); + } + + /// 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, + }); + + /// 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; +} + +/// 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_videohole/pigeons/messages.dart b/packages/video_player_videohole/pigeons/messages.dart new file mode 100644 index 000000000..dd602eba6 --- /dev/null +++ b/packages/video_player_videohole/pigeons/messages.dart @@ -0,0 +1,80 @@ +// 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 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; +} + +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 TizenVideoPlayerApi { + 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); + 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_videohole/pubspec.yaml b/packages/video_player_videohole/pubspec.yaml new file mode 100644 index 000000000..a70287c12 --- /dev/null +++ b/packages/video_player_videohole/pubspec.yaml @@ -0,0 +1,28 @@ +name: video_player_videohole +description: Flutter plugin for displaying inline video. +homepage: https://github.com/flutter-tizen/plugins +repository: https://github.com/flutter-tizen/plugins/tree/master/packages/video_player_videohole +version: 0.1.0 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=3.0.0" + +flutter: + plugin: + platforms: + tizen: + pluginClass: VideoPlayerTizenPlugin + fileName: video_player_tizen_plugin.h + +dependencies: + ffi: ^2.0.1 + flutter: + sdk: flutter + html: ^0.15.0 + plugin_platform_interface: ^2.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + pigeon: ^6.0.1 diff --git a/packages/video_player_videohole/tizen/.gitignore b/packages/video_player_videohole/tizen/.gitignore new file mode 100644 index 000000000..ae3a59232 --- /dev/null +++ b/packages/video_player_videohole/tizen/.gitignore @@ -0,0 +1,5 @@ +/.cproject +/.sign +/crash-info/ +/Debug/ +/Release/ diff --git a/packages/video_player_videohole/tizen/inc/video_player_tizen_plugin.h b/packages/video_player_videohole/tizen/inc/video_player_tizen_plugin.h new file mode 100644 index 000000000..ff405ea7d --- /dev/null +++ b/packages/video_player_videohole/tizen/inc/video_player_tizen_plugin.h @@ -0,0 +1,31 @@ +#ifndef FLUTTER_PLUGIN_VIDEO_PLAYER_TIZEN_PLUGIN_H_ +#define FLUTTER_PLUGIN_VIDEO_PLAYER_TIZEN_PLUGIN_H_ + +#include +#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); + +FLUTTER_PLUGIN_EXPORT intptr_t VideoPlayerTizenPluginInitDartApi(void *data); + +FLUTTER_PLUGIN_EXPORT void VideoPlayerTizenPluginRegisterSendPort( + int64_t player_id, Dart_Port send_port); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_VIDEO_PLAYER_TIZEN_PLUGIN_H_ diff --git a/packages/video_player_videohole/tizen/project_def.prop b/packages/video_player_videohole/tizen/project_def.prop new file mode 100644 index 000000000..6d1317a80 --- /dev/null +++ b/packages/video_player_videohole/tizen/project_def.prop @@ -0,0 +1,20 @@ +# See https://docs.tizen.org/application/tizen-studio/native-tools/project-conversion +# for details. + +APPNAME = video_player_tizen_plugin +type = staticLib +profile = common-4.0 + +# Source files +USER_SRCS += src/*.cc + +# User defines +USER_DEFS = +USER_UNDEFS = +USER_CPP_DEFS = FLUTTER_PLUGIN_IMPL +USER_CPP_UNDEFS = + +# User includes +USER_INC_DIRS = inc src +USER_INC_FILES = +USER_CPP_INC_FILES = diff --git a/packages/video_player_videohole/tizen/src/drm_license_helper.cc b/packages/video_player_videohole/tizen/src/drm_license_helper.cc new file mode 100644 index 000000000..56d789b62 --- /dev/null +++ b/packages/video_player_videohole/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] %s:%d 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_videohole/tizen/src/drm_license_helper.h b/packages/video_player_videohole/tizen/src/drm_license_helper.h new file mode 100644 index 000000000..9701c6438 --- /dev/null +++ b/packages/video_player_videohole/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_videohole/tizen/src/drm_manager.cc b/packages/video_player_videohole/tizen/src/drm_manager.cc new file mode 100644 index 000000000..f2ad971b6 --- /dev/null +++ b/packages/video_player_videohole/tizen/src/drm_manager.cc @@ -0,0 +1,251 @@ +// 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 "drm_license_helper.h" +#include "log.h" + +static std::string GetDrmSubType(int drm_type) { + switch (drm_type) { + case DRM_TYPE_PLAYREADAY: + return "com.microsoft.playready"; + case DRM_TYPE_WIDEVINECDM: + default: + return "com.widevine.alpha"; + } +} + +DrmManager::DrmManager(int drm_type, const std::string &license_server_url, + player_h player) + : drm_type_(drm_type), + license_server_url_(license_server_url), + player_(player) {} + +DrmManager::~DrmManager() {} + +bool DrmManager::InitializeDrmSession(const std::string &url) { + drm_manager_handle_ = OpenDrmManager(); + if (!drm_manager_handle_) { + LOG_ERROR("[DrmManager] Failed to dlopen libdrmmanager."); + return false; + } + int ret = InitDrmManager(drm_manager_handle_); + if (ret != DM_ERROR_NONE) { + LOG_ERROR("[DrmManager] Failed to initialize DRM manager: %s", + get_error_message(ret)); + return false; + } + + media_player_handle_ = OpenMediaPlayer(); + if (!media_player_handle_) { + LOG_ERROR("[DrmManager] Failed to dlopen libcapi-media-player."); + return false; + } + ret = InitMediaPlayer(media_player_handle_); + if (ret != DM_ERROR_NONE) { + LOG_ERROR("[DrmManager] Failed to initialize Media Player: %s", + get_error_message(ret)); + return false; + } + + if (!CreateDrmSession()) { + LOG_ERROR("[DrmManager] Failed to create a DRM session."); + return false; + } + + if (!SetPlayerDrm(url)) { + LOG_ERROR("[DrmManager] Failed to set player DRM handle."); + return false; + } + + if (!SetChallengeCondition()) { + LOG_ERROR("[DrmManager] Failed to set challenge condition."); + return false; + } + + ret = DMGRSetData(drm_session_, "Initialize", nullptr); + if (ret != DM_ERROR_NONE) { + LOG_ERROR("[DrmManager] Failed to initialize DRM session."); + return false; + } + return true; +} + +bool DrmManager::CreateDrmSession() { + std::string sub_type = GetDrmSubType(drm_type_); + LOG_INFO("[DrmManager] drm_sub_type: %s", sub_type.c_str()); + + drm_session_ = DMGRCreateDRMSession(DM_TYPE_EME, sub_type.c_str()); + if (!drm_session_) { + LOG_ERROR("[DrmManager] DMGRCreateDRMSession failed."); + return false; + } + LOG_INFO("[DrmManager] drm_session: %p", drm_session_); + + return true; +} + +bool DrmManager::SetPlayerDrm(const std::string &url) { + int ret = DMGRSetData(drm_session_, "set_playready_manifest", + static_cast(const_cast(url.c_str()))); + if (ret != DM_ERROR_NONE) { + LOG_ERROR("[DrmManager] Setting set_playready_manifest failed: %s", + get_error_message(ret)); + return false; + } + + SetDataParam_t configure_param = {}; + configure_param.param1 = reinterpret_cast(OnDrmManagerError); + configure_param.param2 = drm_session_; + ret = DMGRSetData(drm_session_, "error_event_callback", &configure_param); + if (ret != DM_ERROR_NONE) { + LOG_ERROR("[DrmManager] Setting error_event_callback failed: %s", + get_error_message(ret)); + return false; + } + + int drm_handle = 0; + ret = DMGRGetData(drm_session_, "drm_handle", &drm_handle); + if (ret != DM_ERROR_NONE) { + LOG_ERROR("[DrmManager] Getting drm_handle failed: %s", + get_error_message(ret)); + return false; + } + LOG_INFO("[DrmManager] drm_handle: %d", drm_handle); + + ret = player_set_drm_handle(player_, PLAYER_DRM_TYPE_EME, drm_handle); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[DrmManager] player_set_drm_handle failed: %s", + get_error_message(ret)); + return false; + } + + // IMPORTANT: SetDataParam_t cannot be stack allocated because + // DMGRSecurityInitCompleteCB is called multiple times during video playback + // and the parameter should always be available. + security_param_ = {}; + security_param_.param1 = player_; + security_param_.param2 = drm_session_; + ret = player_set_drm_init_complete_cb(player_, DMGRSecurityInitCompleteCB, + &security_param_); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[DrmManager] player_set_drm_init_complete_cb failed: %s", + get_error_message(ret)); + return false; + } + + ret = player_set_drm_init_data_cb(player_, UpdatePsshDataCB, this); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[DrmManager] player_set_drm_init_data_cb failed: %s", + get_error_message(ret)); + return false; + } + return true; +} + +bool DrmManager::SetChallengeCondition() { + 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] Setting eme_request_key_callback failed: %s", + get_error_message(ret)); + return false; + } + return true; +} + +int DrmManager::OnChallengeData(void *session_id, int message_type, + void *message, int message_length, + void *user_data) { + LOG_INFO("[DrmManager] session_id: %s", session_id); + DrmManager *self = static_cast(user_data); + + LOG_INFO("[DrmManager] drm_type: %d", self->drm_type_); + LOG_INFO("[DrmManager] license_server_url: %s", + self->license_server_url_.c_str()); + LOG_INFO("[DrmManager] Challenge length: %d", message_length); + + std::vector response; + if (!self->license_server_url_.empty()) { + // Get license via the license server. + unsigned char *response_data = nullptr; + unsigned long response_length = 0; + DRM_RESULT ret = DrmLicenseHelper::DoTransactionTZ( + self->license_server_url_.c_str(), message, message_length, + &response_data, &response_length, + static_cast(self->drm_type_), nullptr, + nullptr); + LOG_INFO("[DrmManager] Transaction result: 0x%lx", ret); + response = + std::vector(response_data, response_data + response_length); + free(response_data); + } else { + // Get license via the Dart callback. + std::vector challenge( + static_cast(message), + static_cast(message) + message_length); + response = self->challenge_callback_(challenge); + } + LOG_INFO("[DrmManager] Response length: %d", response.size()); + + SetDataParam_t license_param = {}; + license_param.param1 = session_id; + license_param.param2 = response.data(); + license_param.param3 = reinterpret_cast(response.size()); + int ret = DMGRSetData(self->drm_session_, "install_eme_key", &license_param); + if (ret != DM_ERROR_NONE) { + LOG_ERROR("[DrmManager] Setting install_eme_key failed: %s", + get_error_message(ret)); + } + return 0; +} + +int DrmManager::UpdatePsshDataCB(drm_init_data_type type, void *data, + int length, void *user_data) { + DrmManager *self = static_cast(user_data); + LOG_INFO("[DrmManager] drm_session: %p", self->drm_session_); + + SetDataParam_t pssh_data_param = {}; + pssh_data_param.param1 = data; + pssh_data_param.param2 = reinterpret_cast(length); + int ret = + DMGRSetData(self->drm_session_, "update_pssh_data", &pssh_data_param); + if (DM_ERROR_NONE != ret) { + LOG_ERROR("[DrmManager] Setting update_pssh_data failed: %s", + get_error_message(ret)); + return 0; + } + return 1; +} + +void DrmManager::ReleaseDrmSession() { + if (drm_session_) { + int ret = DMGRSetData(drm_session_, "Finalize", nullptr); + if (ret != DM_ERROR_NONE) { + LOG_ERROR("[DrmManager] Finalize failed: %s", get_error_message(ret)); + } + ret = DMGRReleaseDRMSession(drm_session_); + if (ret != DM_ERROR_NONE) { + LOG_ERROR("[DrmManager] Releasing DRM session failed: %s", + get_error_message(ret)); + } + drm_session_ = nullptr; + } + + // Close dlopen handles. + CloseDrmManager(drm_manager_handle_); + drm_manager_handle_ = nullptr; + CloseMediaPlayer(media_player_handle_); + media_player_handle_ = nullptr; +} + +void DrmManager::OnDrmManagerError(long error_code, char *error_message, + void *user_data) { + LOG_ERROR("[DrmManager] DRM manager had error: [%ld][%s]", error_code, + error_message); +} diff --git a/packages/video_player_videohole/tizen/src/drm_manager.h b/packages/video_player_videohole/tizen/src/drm_manager.h new file mode 100644 index 000000000..924b3e044 --- /dev/null +++ b/packages/video_player_videohole/tizen/src/drm_manager.h @@ -0,0 +1,59 @@ +// 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 +#include +#include +#include + +#include "drm_manager_service_proxy.h" + +typedef std::function( + const std::vector &challenge)> + ChallengeCallback; + +class DrmManager { + public: + explicit DrmManager(int drm_type, const std::string &license_server_url, + player_h player); + ~DrmManager(); + + bool InitializeDrmSession(const std::string &url); + void ReleaseDrmSession(); + + void SetChallengeCallback(ChallengeCallback callback) { + challenge_callback_ = callback; + } + + private: + bool CreateDrmSession(); + bool SetPlayerDrm(const std::string &url); + bool SetChallengeCondition(); + + static int OnChallengeData(void *session_id, int message_type, void *message, + int message_length, void *user_data); + static int UpdatePsshDataCB(drm_init_data_type type, void *data, int length, + void *user_data); + static void OnDrmManagerError(long error_code, char *error_message, + void *user_data); + + SetDataParam_t security_param_; + DRMSessionHandle_t drm_session_ = nullptr; + void *drm_manager_handle_ = nullptr; + void *media_player_handle_ = nullptr; + + int drm_type_; + std::string license_server_url_; + player_h player_; + + ChallengeCallback challenge_callback_; +}; + +#endif // FLUTTER_PLUGIN_DRM_MANAGER_H_ diff --git a/packages/video_player_videohole/tizen/src/drm_manager_service_proxy.cc b/packages/video_player_videohole/tizen/src/drm_manager_service_proxy.cc new file mode 100644 index 000000000..df855918b --- /dev/null +++ b/packages/video_player_videohole/tizen/src/drm_manager_service_proxy.cc @@ -0,0 +1,93 @@ +// 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_service_proxy.h" + +#include + +FuncDMGRSetData DMGRSetData = nullptr; +FuncDMGRGetData DMGRGetData = nullptr; +FuncDMGRCreateDRMSession DMGRCreateDRMSession = nullptr; +FuncDMGRSecurityInitCompleteCB DMGRSecurityInitCompleteCB = nullptr; +FuncDMGRReleaseDRMSession DMGRReleaseDRMSession = nullptr; +FuncPlayerSetDrmHandle player_set_drm_handle = nullptr; +FuncPlayerSetDrmInitCompleteCB player_set_drm_init_complete_cb = nullptr; +FuncPlayerSetDrmInitDataCB player_set_drm_init_data_cb = nullptr; + +void* OpenDrmManager() { return dlopen("libdrmmanager.so.0", RTLD_LAZY); } + +void* OpenMediaPlayer() { + return dlopen("libcapi-media-player.so.0", RTLD_LAZY); +} + +int InitDrmManager(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; + } + + 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; +} + +int InitMediaPlayer(void* handle) { + player_set_drm_handle = reinterpret_cast( + dlsym(handle, "player_set_drm_handle")); + if (!player_set_drm_handle) { + return DM_ERROR_DL; + } + + player_set_drm_init_complete_cb = + reinterpret_cast( + dlsym(handle, "player_set_drm_init_complete_cb")); + if (!player_set_drm_init_complete_cb) { + return DM_ERROR_DL; + } + + player_set_drm_init_data_cb = reinterpret_cast( + dlsym(handle, "player_set_drm_init_data_cb")); + if (!player_set_drm_init_data_cb) { + return DM_ERROR_DL; + } + + return DM_ERROR_NONE; +} + +void CloseDrmManager(void* handle) { + if (handle) { + dlclose(handle); + } +} + +void CloseMediaPlayer(void* handle) { + if (handle) { + dlclose(handle); + } +} diff --git a/packages/video_player_videohole/tizen/src/drm_manager_service_proxy.h b/packages/video_player_videohole/tizen/src/drm_manager_service_proxy.h new file mode 100644 index 000000000..4583a9462 --- /dev/null +++ b/packages/video_player_videohole/tizen/src/drm_manager_service_proxy.h @@ -0,0 +1,171 @@ +// 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_SERVICE_PROXY_H_ +#define FLUTTER_PLUGIN_DRM_MANAGER_SERVICE_PROXY_H_ + +#include + +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 { + 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 { + 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 enum { + DRM_TYPE_NONE, + DRM_TYPE_PLAYREADAY, + DRM_TYPE_WIDEVINECDM, +} DRMTYPE; + +typedef enum { + CENC = 0, + KEYIDS = 1, + WEBM = 2, +} drm_init_data_type; + +typedef void* DRMSessionHandle_t; + +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); +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 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); +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); + +void* OpenDrmManager(); +void* OpenMediaPlayer(); +int InitDrmManager(void* handle); +int InitMediaPlayer(void* handle); +void CloseDrmManager(void* handle); +void CloseMediaPlayer(void* handle); + +extern FuncDMGRSetData DMGRSetData; +extern FuncDMGRGetData DMGRGetData; +extern FuncDMGRCreateDRMSession DMGRCreateDRMSession; +extern FuncDMGRSecurityInitCompleteCB DMGRSecurityInitCompleteCB; +extern FuncDMGRReleaseDRMSession DMGRReleaseDRMSession; +extern FuncPlayerSetDrmHandle player_set_drm_handle; +extern FuncPlayerSetDrmInitCompleteCB player_set_drm_init_complete_cb; +extern FuncPlayerSetDrmInitDataCB player_set_drm_init_data_cb; + +#endif // FLUTTER_PLUGIN_DRM_MANAGER_SERVICE_PROXY_H_ diff --git a/packages/video_player_videohole/tizen/src/log.h b/packages/video_player_videohole/tizen/src/log.h new file mode 100644 index 000000000..5aedc9cd9 --- /dev/null +++ b/packages/video_player_videohole/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 "VideoPlayerVideoHolePlugin" + +#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_videohole/tizen/src/messages.cc b/packages/video_player_videohole/tizen/src/messages.cc new file mode 100644 index 000000000..f36fe2c06 --- /dev/null +++ b/packages/video_player_videohole/tizen/src/messages.cc @@ -0,0 +1,905 @@ +// Autogenerated from Pigeon (v6.0.3), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#undef _HAS_EXCEPTIONS + +#include "messages.h" + +#include +#include +#include +#include + +#include +#include +#include + +// PlayerMessage + +int64_t PlayerMessage::player_id() const { return player_id_; } +void PlayerMessage::set_player_id(int64_t value_arg) { player_id_ = value_arg; } + +flutter::EncodableList PlayerMessage::ToEncodableList() const { + return flutter::EncodableList{ + flutter::EncodableValue(player_id_), + }; +} + +PlayerMessage::PlayerMessage() {} + +PlayerMessage::PlayerMessage(const flutter::EncodableList& list) { + auto& encodable_player_id = list[0]; + if (const int32_t* pointer_player_id = + std::get_if(&encodable_player_id)) + player_id_ = *pointer_player_id; + else if (const int64_t* pointer_player_id_64 = + std::get_if(&encodable_player_id)) + player_id_ = *pointer_player_id_64; +} + +// LoopingMessage + +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; } + +flutter::EncodableList LoopingMessage::ToEncodableList() const { + return flutter::EncodableList{ + flutter::EncodableValue(player_id_), + flutter::EncodableValue(is_looping_), + }; +} + +LoopingMessage::LoopingMessage() {} + +LoopingMessage::LoopingMessage(const flutter::EncodableList& list) { + auto& encodable_player_id = list[0]; + if (const int32_t* pointer_player_id = + std::get_if(&encodable_player_id)) + player_id_ = *pointer_player_id; + else if (const int64_t* pointer_player_id_64 = + std::get_if(&encodable_player_id)) + player_id_ = *pointer_player_id_64; + auto& encodable_is_looping = list[1]; + if (const bool* pointer_is_looping = + std::get_if(&encodable_is_looping)) { + is_looping_ = *pointer_is_looping; + } +} + +// VolumeMessage + +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; } + +flutter::EncodableList VolumeMessage::ToEncodableList() const { + return flutter::EncodableList{ + flutter::EncodableValue(player_id_), + flutter::EncodableValue(volume_), + }; +} + +VolumeMessage::VolumeMessage() {} + +VolumeMessage::VolumeMessage(const flutter::EncodableList& list) { + auto& encodable_player_id = list[0]; + if (const int32_t* pointer_player_id = + std::get_if(&encodable_player_id)) + player_id_ = *pointer_player_id; + else if (const int64_t* pointer_player_id_64 = + std::get_if(&encodable_player_id)) + player_id_ = *pointer_player_id_64; + auto& encodable_volume = list[1]; + if (const double* pointer_volume = std::get_if(&encodable_volume)) { + volume_ = *pointer_volume; + } +} + +// PlaybackSpeedMessage + +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; } + +flutter::EncodableList PlaybackSpeedMessage::ToEncodableList() const { + return flutter::EncodableList{ + flutter::EncodableValue(player_id_), + flutter::EncodableValue(speed_), + }; +} + +PlaybackSpeedMessage::PlaybackSpeedMessage() {} + +PlaybackSpeedMessage::PlaybackSpeedMessage(const flutter::EncodableList& list) { + auto& encodable_player_id = list[0]; + if (const int32_t* pointer_player_id = + std::get_if(&encodable_player_id)) + player_id_ = *pointer_player_id; + else if (const int64_t* pointer_player_id_64 = + std::get_if(&encodable_player_id)) + player_id_ = *pointer_player_id_64; + auto& encodable_speed = list[1]; + if (const double* pointer_speed = std::get_if(&encodable_speed)) { + speed_ = *pointer_speed; + } +} + +// PositionMessage + +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; } + +flutter::EncodableList PositionMessage::ToEncodableList() const { + return flutter::EncodableList{ + flutter::EncodableValue(player_id_), + flutter::EncodableValue(position_), + }; +} + +PositionMessage::PositionMessage() {} + +PositionMessage::PositionMessage(const flutter::EncodableList& list) { + auto& encodable_player_id = list[0]; + if (const int32_t* pointer_player_id = + std::get_if(&encodable_player_id)) + player_id_ = *pointer_player_id; + else if (const int64_t* pointer_player_id_64 = + std::get_if(&encodable_player_id)) + player_id_ = *pointer_player_id_64; + auto& encodable_position = list[1]; + if (const int32_t* pointer_position = + std::get_if(&encodable_position)) + position_ = *pointer_position; + else if (const int64_t* pointer_position_64 = + std::get_if(&encodable_position)) + position_ = *pointer_position_64; +} + +// CreateMessage + +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 flutter::EncodableMap* CreateMessage::http_headers() const { + return http_headers_ ? &(*http_headers_) : nullptr; +} +void CreateMessage::set_http_headers(const flutter::EncodableMap* value_arg) { + http_headers_ = value_arg ? std::optional(*value_arg) + : std::nullopt; +} +void CreateMessage::set_http_headers(const flutter::EncodableMap& value_arg) { + http_headers_ = value_arg; +} + +const flutter::EncodableMap* CreateMessage::drm_configs() const { + return drm_configs_ ? &(*drm_configs_) : nullptr; +} +void CreateMessage::set_drm_configs(const flutter::EncodableMap* value_arg) { + drm_configs_ = value_arg ? std::optional(*value_arg) + : std::nullopt; +} +void CreateMessage::set_drm_configs(const flutter::EncodableMap& value_arg) { + drm_configs_ = value_arg; +} + +flutter::EncodableList CreateMessage::ToEncodableList() const { + return flutter::EncodableList{ + asset_ ? flutter::EncodableValue(*asset_) : flutter::EncodableValue(), + uri_ ? flutter::EncodableValue(*uri_) : flutter::EncodableValue(), + package_name_ ? flutter::EncodableValue(*package_name_) + : flutter::EncodableValue(), + format_hint_ ? flutter::EncodableValue(*format_hint_) + : flutter::EncodableValue(), + http_headers_ ? flutter::EncodableValue(*http_headers_) + : flutter::EncodableValue(), + drm_configs_ ? flutter::EncodableValue(*drm_configs_) + : flutter::EncodableValue(), + }; +} + +CreateMessage::CreateMessage() {} + +CreateMessage::CreateMessage(const flutter::EncodableList& list) { + auto& encodable_asset = list[0]; + if (const std::string* pointer_asset = + std::get_if(&encodable_asset)) { + asset_ = *pointer_asset; + } + auto& encodable_uri = list[1]; + if (const std::string* pointer_uri = + std::get_if(&encodable_uri)) { + uri_ = *pointer_uri; + } + auto& encodable_package_name = list[2]; + if (const std::string* pointer_package_name = + std::get_if(&encodable_package_name)) { + package_name_ = *pointer_package_name; + } + auto& encodable_format_hint = list[3]; + if (const std::string* pointer_format_hint = + std::get_if(&encodable_format_hint)) { + format_hint_ = *pointer_format_hint; + } + auto& encodable_http_headers = list[4]; + if (const flutter::EncodableMap* pointer_http_headers = + std::get_if(&encodable_http_headers)) { + http_headers_ = *pointer_http_headers; + } + auto& encodable_drm_configs = list[5]; + if (const flutter::EncodableMap* pointer_drm_configs = + std::get_if(&encodable_drm_configs)) { + drm_configs_ = *pointer_drm_configs; + } +} + +// MixWithOthersMessage + +bool MixWithOthersMessage::mix_with_others() const { return mix_with_others_; } +void MixWithOthersMessage::set_mix_with_others(bool value_arg) { + mix_with_others_ = value_arg; +} + +flutter::EncodableList MixWithOthersMessage::ToEncodableList() const { + return flutter::EncodableList{ + flutter::EncodableValue(mix_with_others_), + }; +} + +MixWithOthersMessage::MixWithOthersMessage() {} + +MixWithOthersMessage::MixWithOthersMessage(const flutter::EncodableList& list) { + auto& encodable_mix_with_others = list[0]; + if (const bool* pointer_mix_with_others = + std::get_if(&encodable_mix_with_others)) { + mix_with_others_ = *pointer_mix_with_others; + } +} + +// GeometryMessage + +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; } + +flutter::EncodableList GeometryMessage::ToEncodableList() const { + return flutter::EncodableList{ + flutter::EncodableValue(player_id_), flutter::EncodableValue(x_), + flutter::EncodableValue(y_), flutter::EncodableValue(width_), + flutter::EncodableValue(height_), + }; +} + +GeometryMessage::GeometryMessage() {} + +GeometryMessage::GeometryMessage(const flutter::EncodableList& list) { + auto& encodable_player_id = list[0]; + if (const int32_t* pointer_player_id = + std::get_if(&encodable_player_id)) + player_id_ = *pointer_player_id; + else if (const int64_t* pointer_player_id_64 = + std::get_if(&encodable_player_id)) + player_id_ = *pointer_player_id_64; + auto& encodable_x = list[1]; + if (const int32_t* pointer_x = std::get_if(&encodable_x)) + x_ = *pointer_x; + else if (const int64_t* pointer_x_64 = std::get_if(&encodable_x)) + x_ = *pointer_x_64; + auto& encodable_y = list[2]; + if (const int32_t* pointer_y = std::get_if(&encodable_y)) + y_ = *pointer_y; + else if (const int64_t* pointer_y_64 = std::get_if(&encodable_y)) + y_ = *pointer_y_64; + auto& encodable_width = list[3]; + if (const int32_t* pointer_width = std::get_if(&encodable_width)) + width_ = *pointer_width; + else if (const int64_t* pointer_width_64 = + std::get_if(&encodable_width)) + width_ = *pointer_width_64; + auto& encodable_height = list[4]; + if (const int32_t* pointer_height = std::get_if(&encodable_height)) + height_ = *pointer_height; + else if (const int64_t* pointer_height_64 = + std::get_if(&encodable_height)) + height_ = *pointer_height_64; +} + +TizenVideoPlayerApiCodecSerializer::TizenVideoPlayerApiCodecSerializer() {} +flutter::EncodableValue TizenVideoPlayerApiCodecSerializer::ReadValueOfType( + uint8_t type, flutter::ByteStreamReader* stream) const { + switch (type) { + case 128: + return flutter::CustomEncodableValue( + CreateMessage(std::get(ReadValue(stream)))); + + case 129: + return flutter::CustomEncodableValue( + GeometryMessage(std::get(ReadValue(stream)))); + + case 130: + return flutter::CustomEncodableValue( + LoopingMessage(std::get(ReadValue(stream)))); + + case 131: + return flutter::CustomEncodableValue(MixWithOthersMessage( + std::get(ReadValue(stream)))); + + case 132: + return flutter::CustomEncodableValue(PlaybackSpeedMessage( + std::get(ReadValue(stream)))); + + case 133: + return flutter::CustomEncodableValue( + PlayerMessage(std::get(ReadValue(stream)))); + + case 134: + return flutter::CustomEncodableValue( + PositionMessage(std::get(ReadValue(stream)))); + + case 135: + return flutter::CustomEncodableValue( + VolumeMessage(std::get(ReadValue(stream)))); + + default: + return flutter::StandardCodecSerializer::ReadValueOfType(type, stream); + } +} + +void TizenVideoPlayerApiCodecSerializer::WriteValue( + const flutter::EncodableValue& value, + flutter::ByteStreamWriter* stream) const { + if (const flutter::CustomEncodableValue* custom_value = + std::get_if(&value)) { + if (custom_value->type() == typeid(CreateMessage)) { + stream->WriteByte(128); + WriteValue( + flutter::EncodableValue( + std::any_cast(*custom_value).ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(GeometryMessage)) { + stream->WriteByte(129); + WriteValue( + flutter::EncodableValue( + std::any_cast(*custom_value).ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(LoopingMessage)) { + stream->WriteByte(130); + WriteValue( + flutter::EncodableValue( + std::any_cast(*custom_value).ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(MixWithOthersMessage)) { + stream->WriteByte(131); + WriteValue(flutter::EncodableValue( + std::any_cast(*custom_value) + .ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(PlaybackSpeedMessage)) { + stream->WriteByte(132); + WriteValue(flutter::EncodableValue( + std::any_cast(*custom_value) + .ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(PlayerMessage)) { + stream->WriteByte(133); + WriteValue( + flutter::EncodableValue( + std::any_cast(*custom_value).ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(PositionMessage)) { + stream->WriteByte(134); + WriteValue( + flutter::EncodableValue( + std::any_cast(*custom_value).ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(VolumeMessage)) { + stream->WriteByte(135); + WriteValue( + flutter::EncodableValue( + std::any_cast(*custom_value).ToEncodableList()), + stream); + return; + } + } + flutter::StandardCodecSerializer::WriteValue(value, stream); +} + +/// The codec used by TizenVideoPlayerApi. +const flutter::StandardMessageCodec& TizenVideoPlayerApi::GetCodec() { + return flutter::StandardMessageCodec::GetInstance( + &TizenVideoPlayerApiCodecSerializer::GetInstance()); +} + +// Sets up an instance of `TizenVideoPlayerApi` to handle messages through the +// `binary_messenger`. +void TizenVideoPlayerApi::SetUp(flutter::BinaryMessenger* binary_messenger, + TizenVideoPlayerApi* api) { + { + auto channel = std::make_unique>( + binary_messenger, "dev.flutter.pigeon.TizenVideoPlayerApi.initialize", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const flutter::EncodableValue& message, + const flutter::MessageReply& reply) { + try { + std::optional output = api->Initialize(); + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + flutter::EncodableList wrapped; + wrapped.push_back(flutter::EncodableValue()); + reply(flutter::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.TizenVideoPlayerApi.create", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const flutter::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; + } + flutter::EncodableList wrapped; + wrapped.push_back( + flutter::CustomEncodableValue(std::move(output).TakeValue())); + reply(flutter::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.TizenVideoPlayerApi.dispose", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const flutter::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; + } + flutter::EncodableList wrapped; + wrapped.push_back(flutter::EncodableValue()); + reply(flutter::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.TizenVideoPlayerApi.setLooping", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const flutter::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; + } + flutter::EncodableList wrapped; + wrapped.push_back(flutter::EncodableValue()); + reply(flutter::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.TizenVideoPlayerApi.setVolume", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const flutter::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; + } + flutter::EncodableList wrapped; + wrapped.push_back(flutter::EncodableValue()); + reply(flutter::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.TizenVideoPlayerApi.setPlaybackSpeed", &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const flutter::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; + } + flutter::EncodableList wrapped; + wrapped.push_back(flutter::EncodableValue()); + reply(flutter::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.TizenVideoPlayerApi.play", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const flutter::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; + } + flutter::EncodableList wrapped; + wrapped.push_back(flutter::EncodableValue()); + reply(flutter::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.TizenVideoPlayerApi.position", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const flutter::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; + } + flutter::EncodableList wrapped; + wrapped.push_back( + flutter::CustomEncodableValue(std::move(output).TakeValue())); + reply(flutter::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.TizenVideoPlayerApi.seekTo", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const flutter::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; + } + flutter::EncodableList wrapped; + wrapped.push_back(flutter::EncodableValue()); + reply(flutter::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.TizenVideoPlayerApi.pause", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const flutter::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; + } + flutter::EncodableList wrapped; + wrapped.push_back(flutter::EncodableValue()); + reply(flutter::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.TizenVideoPlayerApi.setMixWithOthers", &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const flutter::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; + } + flutter::EncodableList wrapped; + wrapped.push_back(flutter::EncodableValue()); + reply(flutter::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.TizenVideoPlayerApi.setDisplayGeometry", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const flutter::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; + } + flutter::EncodableList wrapped; + wrapped.push_back(flutter::EncodableValue()); + reply(flutter::EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } +} + +flutter::EncodableValue TizenVideoPlayerApi::WrapError( + std::string_view error_message) { + return flutter::EncodableValue(flutter::EncodableList{ + flutter::EncodableValue(std::string(error_message)), + flutter::EncodableValue("Error"), flutter::EncodableValue()}); +} +flutter::EncodableValue TizenVideoPlayerApi::WrapError( + const FlutterError& error) { + return flutter::EncodableValue(flutter::EncodableList{ + flutter::EncodableValue(error.message()), + flutter::EncodableValue(error.code()), error.details()}); +} diff --git a/packages/video_player_videohole/tizen/src/messages.h b/packages/video_player_videohole/tizen/src/messages.h new file mode 100644 index 000000000..e9b85f466 --- /dev/null +++ b/packages/video_player_videohole/tizen/src/messages.h @@ -0,0 +1,290 @@ +// Autogenerated from Pigeon (v6.0.3), 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) { new (&v_) T(rhs); } + ErrorOr(const T&& rhs) { v_ = std::move(rhs); } + ErrorOr(const FlutterError& rhs) { new (&v_) FlutterError(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 TizenVideoPlayerApi; + 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: + PlayerMessage(); + int64_t player_id() const; + void set_player_id(int64_t value_arg); + + private: + PlayerMessage(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class TizenVideoPlayerApi; + friend class TizenVideoPlayerApiCodecSerializer; + int64_t player_id_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class LoopingMessage { + public: + LoopingMessage(); + 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: + LoopingMessage(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class TizenVideoPlayerApi; + friend class TizenVideoPlayerApiCodecSerializer; + int64_t player_id_; + bool is_looping_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class VolumeMessage { + public: + VolumeMessage(); + int64_t player_id() const; + void set_player_id(int64_t value_arg); + + double volume() const; + void set_volume(double value_arg); + + private: + VolumeMessage(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class TizenVideoPlayerApi; + friend class TizenVideoPlayerApiCodecSerializer; + int64_t player_id_; + double volume_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class PlaybackSpeedMessage { + public: + PlaybackSpeedMessage(); + int64_t player_id() const; + void set_player_id(int64_t value_arg); + + double speed() const; + void set_speed(double value_arg); + + private: + PlaybackSpeedMessage(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class TizenVideoPlayerApi; + friend class TizenVideoPlayerApiCodecSerializer; + int64_t player_id_; + double speed_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class PositionMessage { + public: + PositionMessage(); + 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: + PositionMessage(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class TizenVideoPlayerApi; + friend class TizenVideoPlayerApiCodecSerializer; + int64_t player_id_; + int64_t position_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class CreateMessage { + public: + CreateMessage(); + 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); + + private: + CreateMessage(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class TizenVideoPlayerApi; + friend class TizenVideoPlayerApiCodecSerializer; + std::optional asset_; + std::optional uri_; + std::optional package_name_; + std::optional format_hint_; + std::optional http_headers_; + std::optional drm_configs_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class MixWithOthersMessage { + public: + MixWithOthersMessage(); + bool mix_with_others() const; + void set_mix_with_others(bool value_arg); + + private: + MixWithOthersMessage(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class TizenVideoPlayerApi; + friend class TizenVideoPlayerApiCodecSerializer; + bool mix_with_others_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class GeometryMessage { + public: + GeometryMessage(); + 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: + GeometryMessage(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class TizenVideoPlayerApi; + friend class TizenVideoPlayerApiCodecSerializer; + int64_t player_id_; + int64_t x_; + int64_t y_; + int64_t width_; + int64_t height_; +}; + +class TizenVideoPlayerApiCodecSerializer + : public flutter::StandardCodecSerializer { + public: + inline static TizenVideoPlayerApiCodecSerializer& GetInstance() { + static TizenVideoPlayerApiCodecSerializer sInstance; + return sInstance; + } + + TizenVideoPlayerApiCodecSerializer(); + + public: + 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 TizenVideoPlayerApi { + public: + TizenVideoPlayerApi(const TizenVideoPlayerApi&) = delete; + TizenVideoPlayerApi& operator=(const TizenVideoPlayerApi&) = delete; + virtual ~TizenVideoPlayerApi(){}; + 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 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 TizenVideoPlayerApi. + static const flutter::StandardMessageCodec& GetCodec(); + // Sets up an instance of `TizenVideoPlayerApi` to handle messages through the + // `binary_messenger`. + static void SetUp(flutter::BinaryMessenger* binary_messenger, + TizenVideoPlayerApi* api); + static flutter::EncodableValue WrapError(std::string_view error_message); + static flutter::EncodableValue WrapError(const FlutterError& error); + + protected: + TizenVideoPlayerApi() = default; +}; +#endif // PIGEON_MESSAGES_H_ diff --git a/packages/video_player_videohole/tizen/src/pending_call.h b/packages/video_player_videohole/tizen/src/pending_call.h new file mode 100644 index 000000000..9728a6f7b --- /dev/null +++ b/packages/video_player_videohole/tizen/src/pending_call.h @@ -0,0 +1,90 @@ +// 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. + +#ifndef FLUTTER_PLUGIN_PENDING_CALL_H_ +#define FLUTTER_PLUGIN_PENDING_CALL_H_ + +#include + +#include +#include + +#include "log.h" + +class PendingCall { + public: + PendingCall(void **buffer, size_t *length) + : response_buffer_(buffer), response_length_(length) { + receive_port_ = + Dart_NewNativePort_DL("cpp-response", &PendingCall::HandleResponse, + /*handle_concurrently=*/false); + } + ~PendingCall() { Dart_CloseNativePort_DL(receive_port_); } + + Dart_Port port() const { return receive_port_; } + + void PostAndWait(Dart_Port port, Dart_CObject *object) { + std::unique_lock lock(mutex); + const bool success = Dart_PostCObject_DL(port, object); + if (!success) { + LOG_ERROR("[ffi] Failed to send message, invalid port or isolate died."); + return; + } + + LOG_INFO("[ffi] Waiting for result."); + while (!notified) { + cv.wait(lock); + } + } + + static void HandleResponse(Dart_Port port, Dart_CObject *message) { + if (message->type != Dart_CObject_kArray) { + LOG_ERROR("[ffi] Wrong Data: message->type != Dart_CObject_kArray"); + } + Dart_CObject **c_response_args = message->value.as_array.values; + Dart_CObject *c_pending_call = c_response_args[0]; + Dart_CObject *c_message = c_response_args[1]; + LOG_INFO("[ffi] HandleResponse (call: %d)", + reinterpret_cast(c_pending_call)); + + auto *pending_call = reinterpret_cast( + c_pending_call->type == Dart_CObject_kInt64 + ? c_pending_call->value.as_int64 + : c_pending_call->value.as_int32); + + pending_call->ResolveCall(c_message); + } + + private: + static bool NonEmptyBuffer(void **value) { return *value != nullptr; } + + void ResolveCall(Dart_CObject *bytes) { + assert(bytes->type == Dart_CObject_kTypedData); + if (bytes->type != Dart_CObject_kTypedData) { + LOG_ERROR("[ffi] Wrong Data: bytes->type != Dart_CObject_kTypedData"); + } + const intptr_t response_length = bytes->value.as_typed_data.length; + const uint8_t *response_buffer = bytes->value.as_typed_data.values; + + void *buffer = malloc(response_length); + memmove(buffer, response_buffer, response_length); + + *response_buffer_ = buffer; + *response_length_ = response_length; + + LOG_INFO("[ffi] Notify result ready."); + notified = true; + cv.notify_one(); + } + + std::mutex mutex; + std::condition_variable cv; + bool notified = false; + + Dart_Port receive_port_; + void **response_buffer_; + size_t *response_length_; +}; + +#endif // FLUTTER_PLUGIN_PENDING_CALL_H_ diff --git a/packages/video_player_videohole/tizen/src/video_player.cc b/packages/video_player_videohole/tizen/src/video_player.cc new file mode 100644 index 000000000..0f585efd0 --- /dev/null +++ b/packages/video_player_videohole/tizen/src/video_player.cc @@ -0,0 +1,563 @@ +// 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 + +#include + +#include "log.h" +#include "pending_call.h" + +static int64_t player_index = 1; + +VideoPlayer::VideoPlayer(flutter::PluginRegistrar *plugin_registrar, + void *native_window) + : plugin_registrar_(plugin_registrar), native_window_(native_window) {} + +bool VideoPlayer::SetDisplay() { + int x = 0, y = 0, width = 0, height = 0; + void *ecore_lib_handle = dlopen("libecore_wl2.so.1", RTLD_LAZY); + if (ecore_lib_handle) { + FuncEcoreWl2WindowGeometryGet ecore_wl2_window_geometry_get = + reinterpret_cast( + dlsym(ecore_lib_handle, "ecore_wl2_window_geometry_get")); + if (ecore_wl2_window_geometry_get) { + ecore_wl2_window_geometry_get(native_window_, &x, &y, &width, &height); + } else { + LOG_ERROR("[VideoPlayer] Symbol not found: %s", dlerror()); + dlclose(ecore_lib_handle); + return false; + } + dlclose(ecore_lib_handle); + } else { + LOG_ERROR("[VideoPlayer] dlopen failed: %s", dlerror()); + return false; + } + + void *player_lib_handle = dlopen("libcapi-media-player.so.0", RTLD_LAZY); + if (player_lib_handle) { + FuncPlayerSetEcoreWlDisplay player_set_ecore_wl_display = + reinterpret_cast( + dlsym(player_lib_handle, "player_set_ecore_wl_display")); + if (player_set_ecore_wl_display) { + int ret = + player_set_ecore_wl_display(player_, PLAYER_DISPLAY_TYPE_OVERLAY, + native_window_, x, y, width, height); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_set_ecore_wl_display failed: %s", + get_error_message(ret)); + dlclose(player_lib_handle); + return false; + } + } else { + LOG_ERROR("[VideoPlayer] Symbol not found: %s", dlerror()); + dlclose(ecore_lib_handle); + return false; + } + dlclose(player_lib_handle); + } else { + LOG_ERROR("[VideoPlayer] dlopen failed: %s", dlerror()); + return false; + } + + int ret = player_set_display_mode(player_, PLAYER_DISPLAY_MODE_DST_ROI); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_set_display_mode failed: %s", + get_error_message(ret)); + return false; + } + return true; +} + +int64_t VideoPlayer::Create(const std::string &uri, int drm_type, + const std::string &license_server_url) { + LOG_INFO("[VideoPlayer] uri: %s, drm_type: %d", uri.c_str(), drm_type); + + player_id_ = player_index++; + + if (uri.empty()) { + LOG_ERROR("[VideoPlayer] The uri must not be empty."); + return -1; + } + + int ret = player_create(&player_); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_create failed: %s", get_error_message(ret)); + return -1; + } + + if (drm_type != DRM_TYPE_NONE) { + drm_manager_ = + std::make_unique(drm_type, license_server_url, player_); + drm_manager_->SetChallengeCallback( + [this](const std::vector &challenge) -> std::vector { + return OnLicenseChallenge(challenge); + }); + + if (!drm_manager_->InitializeDrmSession(uri)) { + LOG_ERROR("[VideoPlayer] Failed to initialize the DRM session."); + drm_manager_->ReleaseDrmSession(); + } + } + + ret = player_set_uri(player_, uri.c_str()); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_set_uri failed: %s", + get_error_message(ret)); + return -1; + } + + if (!SetDisplay()) { + LOG_ERROR("[VideoPlayer] Failed to set display."); + return -1; + } + SetDisplayRoi(0, 0, 1, 1); + + ret = player_set_display_visible(player_, true); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] 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("[VideoPlayer] 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("[VideoPlayer] 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("[VideoPlayer] 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("[VideoPlayer] player_set_error_cb failed: %s", + get_error_message(ret)); + return -1; + } + + ret = player_prepare_async(player_, OnPrepared, this); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_prepare_async failed: %s", + get_error_message(ret)); + return -1; + } + + ret = player_set_subtitle_updated_cb(player_, OnSubtitleUpdated, this); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_set_subtitle_updated_cb failed: %s", + get_error_message(ret)); + } + + SetUpEventChannel(plugin_registrar_->messenger()); + + return player_id_; +} + +void VideoPlayer::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("[VideoPlayer] player_set_display_roi_area failed: %s", + get_error_message(ret)); + } +} + +VideoPlayer::~VideoPlayer() { + if (drm_manager_) { + drm_manager_->ReleaseDrmSession(); + } + Dispose(); +} + +void VideoPlayer::Play() { + LOG_INFO("[VideoPlayer] Player starting."); + + player_state_e state = PLAYER_STATE_NONE; + int ret = player_get_state(player_, &state); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] Unable to get player state."); + return; + } + if (state == PLAYER_STATE_NONE || state == PLAYER_STATE_IDLE) { + LOG_ERROR("[VideoPlayer] Player not ready."); + return; + } + if (state == PLAYER_STATE_PLAYING) { + LOG_INFO("[VideoPlayer] Player already playing."); + return; + } + + ret = player_start(player_); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_start failed: %s", get_error_message(ret)); + } +} + +void VideoPlayer::Pause() { + LOG_INFO("[VideoPlayer] Player pausing."); + + player_state_e state = PLAYER_STATE_NONE; + int ret = player_get_state(player_, &state); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] Unable to get player state."); + return; + } + if (state == PLAYER_STATE_NONE || state == PLAYER_STATE_IDLE) { + LOG_ERROR("[VideoPlayer] Player not ready."); + return; + } + if (state != PLAYER_STATE_PLAYING) { + LOG_INFO("[VideoPlayer] Player not playing."); + return; + } + + ret = player_pause(player_); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_pause failed: %s", get_error_message(ret)); + } +} + +void VideoPlayer::SetLooping(bool is_looping) { + LOG_INFO("[VideoPlayer] is_looping: %d", is_looping); + + int ret = player_set_looping(player_, is_looping); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_set_looping failed: %s", + get_error_message(ret)); + } +} + +void VideoPlayer::SetVolume(double volume) { + LOG_INFO("[VideoPlayer] volume: %f", volume); + + int ret = player_set_volume(player_, volume, volume); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_set_volume failed: %s", + get_error_message(ret)); + } +} + +void VideoPlayer::SetPlaybackSpeed(double speed) { + LOG_INFO("[VideoPlayer] speed: %f", speed); + + int ret = player_set_playback_rate(player_, speed); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_set_playback_rate failed: %s", + get_error_message(ret)); + } +} + +void VideoPlayer::SeekTo(int32_t position, SeekCompletedCallback callback) { + LOG_INFO("[VideoPlayer] 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) { + LOG_ERROR("[VideoPlayer] player_set_play_position failed: %s", + get_error_message(ret)); + } +} + +int32_t VideoPlayer::GetPosition() { + int position = 0; + int ret = player_get_play_position(player_, &position); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_get_play_position failed: %s", + get_error_message(ret)); + } + return position; +} + +void VideoPlayer::Dispose() { + LOG_INFO("[VideoPlayer] Player disposing."); + + is_initialized_ = false; + event_sink_ = nullptr; + event_channel_->SetStreamHandler(nullptr); + + if (player_) { + player_unprepare(player_); + player_unset_buffering_cb(player_); + player_unset_completed_cb(player_); + player_unset_interrupted_cb(player_); + player_unset_error_cb(player_); + player_destroy(player_); + player_ = nullptr; + } +} + +void VideoPlayer::SetUpEventChannel(flutter::BinaryMessenger *messenger) { + std::string channel_name = + "flutter.io/videoPlayer/videoEvents" + std::to_string(player_id_); + auto channel = + std::make_unique>( + 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); + Initialize(); + return nullptr; + }, + [&](const flutter::EncodableValue *arguments) + -> std::unique_ptr> { + event_sink_ = nullptr; + return nullptr; + }); + channel->SetStreamHandler(std::move(handler)); + + event_channel_ = std::move(channel); +} + +void VideoPlayer::Initialize() { + player_state_e state = PLAYER_STATE_NONE; + int ret = player_get_state(player_, &state); + if (ret == PLAYER_ERROR_NONE) { + LOG_INFO("[VideoPlayer] Player state: %d", state); + if (state == PLAYER_STATE_READY && !is_initialized_) { + SendInitialized(); + } + } else { + LOG_ERROR("[VideoPlayer] player_get_state failed: %s", + get_error_message(ret)); + } +} + +void VideoPlayer::SendInitialized() { + if (!is_initialized_ && !is_interrupted_ && event_sink_) { + 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)); + return; + } + LOG_INFO("[VideoPlayer] Video duration: %d", duration); + + 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)); + return; + } + LOG_INFO("[VideoPlayer] Video width: %d, height: %d", width, height); + + 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)); + } else { + if (rotation == PLAYER_DISPLAY_ROTATION_90 || + rotation == PLAYER_DISPLAY_ROTATION_270) { + std::swap(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)}, + }; + event_sink_->Success(flutter::EncodableValue(result)); + } +} + +void VideoPlayer::SendBufferingStart() { + if (event_sink_) { + flutter::EncodableMap result = { + {flutter::EncodableValue("event"), + flutter::EncodableValue("bufferingStart")}, + }; + event_sink_->Success(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)); + } +} + +void VideoPlayer::SendBufferingEnd() { + if (event_sink_) { + flutter::EncodableMap result = { + {flutter::EncodableValue("event"), + flutter::EncodableValue("bufferingEnd")}, + }; + event_sink_->Success(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)); + } +} + +void VideoPlayer::OnSubtitleUpdated(unsigned long duration, char *text, + void *data) { + LOG_INFO("[VideoPlayer] duration: %ld, text: %s", duration, text); + + VideoPlayer *player = static_cast(data); + player->SendSubtitleUpdate(duration, std::string(text)); +} + +void VideoPlayer::OnPrepared(void *data) { + LOG_INFO("[VideoPlayer] Player prepared."); + + VideoPlayer *player = static_cast(data); + if (!player->is_initialized_) { + player->SendInitialized(); + } +} + +void VideoPlayer::OnBuffering(int percent, void *data) { + LOG_INFO("[VideoPlayer] percent: %d", percent); + + VideoPlayer *player = static_cast(data); + if (percent == 100) { + player->SendBufferingEnd(); + player->is_buffering_ = false; + } else if (!player->is_buffering_ && percent <= 5) { + player->SendBufferingStart(); + player->is_buffering_ = true; + } else { + player->SendBufferingUpdate(percent); + } +} + +void VideoPlayer::OnSeekCompleted(void *data) { + LOG_INFO("[VideoPlayer] Seek completed."); + + VideoPlayer *player = static_cast(data); + if (player->on_seek_completed_) { + player->on_seek_completed_(); + player->on_seek_completed_ = nullptr; + } +} + +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)); + } + player->Pause(); +} + +void VideoPlayer::OnError(int error_code, void *data) { + LOG_ERROR("[VideoPlayer] Error code: %d (%s)", error_code, + 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)); + } +} + +void VideoPlayer::OnInterrupted(player_interrupted_code_e code, void *data) { + LOG_ERROR("[VideoPlayer] Interrupt code: %d", code); + + VideoPlayer *player = static_cast(data); + player->is_interrupted_ = true; + if (player->event_sink_) { + player->event_sink_->Error("Player interrupted", + "Video player has been interrupted."); + } +} + +std::vector VideoPlayer::OnLicenseChallenge( + const std::vector &challenge) { + const char *method_name = "onLicenseChallenge"; + size_t request_length = challenge.size(); + void *request_buffer = malloc(request_length); + memcpy(request_buffer, challenge.data(), challenge.size()); + + void *response_buffer = nullptr; + size_t response_length = 0; + PendingCall pending_call(&response_buffer, &response_length); + + Dart_CObject c_send_port; + c_send_port.type = Dart_CObject_kSendPort; + c_send_port.value.as_send_port.id = pending_call.port(); + c_send_port.value.as_send_port.origin_id = ILLEGAL_PORT; + + Dart_CObject c_pending_call; + c_pending_call.type = Dart_CObject_kInt64; + c_pending_call.value.as_int64 = reinterpret_cast(&pending_call); + + Dart_CObject c_method_name; + c_method_name.type = Dart_CObject_kString; + c_method_name.value.as_string = const_cast(method_name); + + Dart_CObject c_request_data; + c_request_data.type = Dart_CObject_kExternalTypedData; + c_request_data.value.as_external_typed_data.type = Dart_TypedData_kUint8; + c_request_data.value.as_external_typed_data.length = request_length; + c_request_data.value.as_external_typed_data.data = + static_cast(request_buffer); + c_request_data.value.as_external_typed_data.peer = request_buffer; + c_request_data.value.as_external_typed_data.callback = + [](void *isolate_callback_data, void *peer) { free(peer); }; + + Dart_CObject *c_request_arr[] = {&c_send_port, &c_pending_call, + &c_method_name, &c_request_data}; + Dart_CObject c_request; + c_request.type = Dart_CObject_kArray; + c_request.value.as_array.values = c_request_arr; + c_request.value.as_array.length = + sizeof(c_request_arr) / sizeof(c_request_arr[0]); + + pending_call.PostAndWait(send_port_, &c_request); + LOG_INFO("[ffi] Received result (size: %d)", response_length); + + return std::vector( + static_cast(response_buffer), + static_cast(response_buffer) + response_length); +} diff --git a/packages/video_player_videohole/tizen/src/video_player.h b/packages/video_player_videohole/tizen/src/video_player.h new file mode 100644 index 000000000..077559c99 --- /dev/null +++ b/packages/video_player_videohole/tizen/src/video_player.h @@ -0,0 +1,91 @@ +// 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 "drm_manager.h" +#include "video_player_options.h" + +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); + +class VideoPlayer { + public: + using SeekCompletedCallback = std::function; + + explicit VideoPlayer(flutter::PluginRegistrar *plugin_registrar, + void *native_window); + ~VideoPlayer(); + + int64_t Create(const std::string &uri, int drm_type, + const std::string &license_server_url); + void Dispose(); + + void SetDisplayRoi(int32_t x, int32_t y, int32_t width, int32_t height); + void Play(); + void Pause(); + void SetLooping(bool is_looping); + void SetVolume(double volume); + void SetPlaybackSpeed(double speed); + void SeekTo(int32_t position, SeekCompletedCallback callback); + int32_t GetPosition(); + + void RegisterSendPort(Dart_Port send_port) { send_port_ = send_port; } + + private: + bool SetDisplay(); + void SetUpEventChannel(flutter::BinaryMessenger *messenger); + void Initialize(); + + void SendInitialized(); + void SendBufferingStart(); + void SendBufferingUpdate(int32_t value); + void SendBufferingEnd(); + void SendSubtitleUpdate(int32_t duration, const std::string &text); + + static void OnPrepared(void *data); + static void OnBuffering(int percent, void *data); + static void OnSeekCompleted(void *data); + static void OnPlayCompleted(void *data); + static void OnError(int error_code, void *data); + static void OnInterrupted(player_interrupted_code_e code, void *data); + static void OnSubtitleUpdated(unsigned long duration, char *text, void *data); + + std::vector OnLicenseChallenge( + const std::vector &challenge); + + std::unique_ptr> + event_channel_; + std::unique_ptr> event_sink_; + + player_h player_ = nullptr; + flutter::PluginRegistrar *plugin_registrar_; + void *native_window_; + int64_t player_id_ = -1; + std::unique_ptr drm_manager_; + + bool is_initialized_ = false; + bool is_interrupted_ = false; + bool is_buffering_ = false; + + SeekCompletedCallback on_seek_completed_; + Dart_Port send_port_; +}; + +#endif // FLUTTER_PLUGIN_VIDEO_PLAYER_H_ diff --git a/packages/video_player_videohole/tizen/src/video_player_options.h b/packages/video_player_videohole/tizen/src/video_player_options.h new file mode 100644 index 000000000..ae2ffd7fb --- /dev/null +++ b/packages/video_player_videohole/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_videohole/tizen/src/video_player_tizen_plugin.cc b/packages/video_player_videohole/tizen/src/video_player_tizen_plugin.cc new file mode 100644 index 000000000..9ef5e626d --- /dev/null +++ b/packages/video_player_videohole/tizen/src/video_player_tizen_plugin.cc @@ -0,0 +1,285 @@ +// 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 "messages.h" +#include "video_player.h" +#include "video_player_options.h" + +namespace { + +class VideoPlayerTizenPlugin : public flutter::Plugin, + public TizenVideoPlayerApi { + 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; + std::optional Play(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) { + TizenVideoPlayerApi::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::unique_ptr player = + std::make_unique(plugin_registrar_, native_window); + + std::string uri; + int32_t drm_type = 0; // DRM_TYPE_NONE + std::string license_server_url; + + 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(); + + 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); + } + } + } + } else { + return FlutterError("Invalid argument", "Either asset or uri must be set."); + } + + int64_t player_id = player->Create(uri, drm_type, license_server_url); + if (player_id == -1) { + return FlutterError("Operation failed", "Failed to create a player."); + } + players_[player_id] = std::move(player); + + PlayerMessage result; + result.set_player_id(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."); + } + player->SetLooping(msg.is_looping()); + + 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."); + } + player->SetVolume(msg.volume()); + + 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."); + } + player->SetPlaybackSpeed(msg.speed()); + + return std::nullopt; +} + +std::optional VideoPlayerTizenPlugin::Play( + const PlayerMessage &msg) { + VideoPlayer *player = FindPlayerById(msg.player_id()); + if (!player) { + return FlutterError("Invalid argument", "Player not found."); + } + player->Play(); + + return std::nullopt; +} + +std::optional VideoPlayerTizenPlugin::Pause( + const PlayerMessage &msg) { + VideoPlayer *player = FindPlayerById(msg.player_id()); + if (!player) { + return FlutterError("Invalid argument", "Player not found."); + } + player->Pause(); + + 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; + result.set_player_id(msg.player_id()); + result.set_position(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; + } + player->SeekTo(msg.position(), [result]() -> void { result(std::nullopt); }); +} + +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)); +} + +intptr_t VideoPlayerTizenPluginInitDartApi(void *data) { + return Dart_InitializeApiDL(data); +} + +void VideoPlayerTizenPluginRegisterSendPort(int64_t player_id, + Dart_Port send_port) { + VideoPlayer *player = VideoPlayerTizenPlugin::FindPlayerById(player_id); + if (player) { + player->RegisterSendPort(send_port); + } +}