From d31ff882b5150fa55121a81b175c633af8fe205b Mon Sep 17 00:00:00 2001 From: gianlucaparadise Date: Fri, 3 Dec 2021 07:08:27 +0100 Subject: [PATCH] Refactor classes and improve docs --- .vscode/launch.json | 23 ++++- Makefile | 15 +++- README.md | 15 +++- example/lib/main.dart | 25 ++++-- lib/cast.dart | 3 +- lib/src/cast/CastContext.dart | 25 +++++- lib/src/cast/RemoteMediaClient.dart | 65 +++++++++++++- lib/src/cast/SessionManager.dart | 83 ++++++++--------- .../ExpandedControlsPlayer.dart | 2 +- lib/src/flutter_cast_framework.dart | 89 +++++++++++-------- lib/widgets.dart | 3 + .../PlatformBridgeApisDefinition.dart | 0 pubspec.lock | 2 +- pubspec.yaml | 1 + 14 files changed, 246 insertions(+), 105 deletions(-) rename {lib/src => pigeon}/PlatformBridgeApisDefinition.dart (100%) diff --git a/.vscode/launch.json b/.vscode/launch.json index 3287bb6..4c03884 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,9 +5,30 @@ "version": "0.2.0", "configurations": [ { - "name": "Flutter", + "name": "Current Device", "request": "launch", "type": "dart" + }, + { + "name": "Android", + "request": "launch", + "type": "dart", + "deviceId": "android" + }, + { + "name": "iPhone", + "request": "launch", + "type": "dart", + "deviceId": "iphone" + }, + ], + "compounds": [ + { + "name": "All Devices", + "configurations": [ + "Android", + "iPhone" + ], } ] } \ No newline at end of file diff --git a/Makefile b/Makefile index 67844f9..efa159d 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,8 @@ +.PHONY: pigeon deploy-receiver open-android open-ios run-all + pigeon: # Generates the typesafe bridge between host and flutter flutter pub run pigeon \ - --input lib/src/PlatformBridgeApisDefinition.dart \ + --input pigeon/PlatformBridgeApisDefinition.dart \ --dart_out lib/src/PlatformBridgeApis.dart \ --objc_header_out ios/Classes/PlatformBridgeApis.h \ --objc_source_out ios/Classes/PlatformBridgeApis.m \ @@ -8,4 +10,13 @@ pigeon: # Generates the typesafe bridge between host and flutter --java_package "com.gianlucaparadise.flutter_cast_framework" deploy-receiver: - surge receiver \ No newline at end of file + surge receiver + +open-android: + studio example/android + +open-ios: + open example/ios/Runner.xcworkspace + +run-all: + cd example && flutter run -d all \ No newline at end of file diff --git a/README.md b/README.md index 405bec0..89ed1f6 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,9 @@ Currently only the following APIs are integrated (both Android and iOS): * Session state * Send custom message * Listen to received custom messages +* Load RemoteMediaRequestData +* Play, Pause, Stop media +* Expanded controls * Cast Button * Chromecast connection @@ -136,4 +139,14 @@ I used this project to test the capabilities of the following technologies: * Chromecast API (Sender - Android SDK) * Flutter -* Flutter custom platform-specific code \ No newline at end of file +* Flutter custom platform-specific code + +## Roadmap + +* Volume in Expanded Controls +* Currently connected cast device name +* CC in Expanded Controls +* Handle Ad Break +* Handle progress seek +* Handle queue +* Handle mini-player diff --git a/example/lib/main.dart b/example/lib/main.dart index 29c02b1..8136448 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -32,18 +32,27 @@ class _MyAppState extends State { super.initState(); castFramework = FlutterCastFramework.create([castNamespace]); castFramework.castContext.state.addListener(_onCastStateChanged); - castFramework.castContext.sessionManager.state - .addListener(_onSessionStateChanged); - castFramework.castContext.sessionManager.onMessageReceived = - _onMessageReceived; - castFramework.castContext.sessionManager.onStatusUpdated = - _onRemoteMediaClientStatusUpdated; + + final sessionManager = castFramework.castContext.sessionManager; + sessionManager.state.addListener(_onSessionStateChanged); + sessionManager.onMessageReceived = _onMessageReceived; + sessionManager.remoteMediaClient.playerState + .addListener(_onRemoteMediaClientStatusUpdated); } @override void dispose() { // Clean up the controller when the widget is disposed. textMessageController.dispose(); + + castFramework.castContext.state.removeListener(_onCastStateChanged); + + final sessionManager = castFramework.castContext.sessionManager; + sessionManager.state.removeListener(_onSessionStateChanged); + sessionManager.onMessageReceived = null; + sessionManager.remoteMediaClient.playerState + .removeListener(_onRemoteMediaClientStatusUpdated); + super.dispose(); } @@ -68,7 +77,9 @@ class _MyAppState extends State { }); } - void _onRemoteMediaClientStatusUpdated(PlayerState playerState) { + void _onRemoteMediaClientStatusUpdated() { + final playerState = castFramework + .castContext.sessionManager.remoteMediaClient.playerState.value; debugPrint("RemoteMediaClient status updated - playerState $playerState"); } diff --git a/lib/cast.dart b/lib/cast.dart index f099cd0..43090a7 100644 --- a/lib/cast.dart +++ b/lib/cast.dart @@ -2,5 +2,6 @@ library cast; export 'src/cast/CastContext.dart'; export 'src/cast/SessionManager.dart'; +export 'src/cast/RemoteMediaClient.dart'; export 'src/flutter_cast_framework.dart'; -export 'src/PlatformBridgeApis.dart'; +export 'src/PlatformBridgeApis.dart' hide CastFlutterApi, CastHostApi; diff --git a/lib/src/cast/CastContext.dart b/lib/src/cast/CastContext.dart index 7c48dd8..1d70916 100644 --- a/lib/src/cast/CastContext.dart +++ b/lib/src/cast/CastContext.dart @@ -1,22 +1,33 @@ import 'package:flutter/foundation.dart'; +import 'package:meta/meta.dart'; + import '../PlatformBridgeApis.dart'; import 'SessionManager.dart'; +/// Class wrapping the global context fot the Cast SDK class CastContext { - final ValueNotifier state = ValueNotifier(CastState.unavailable); - final CastHostApi _hostApi; - CastContext(this._hostApi); + final CastHostApi _hostApi; + + /// Listenable connection state of the cast device + ValueListenable get state => _stateNotifier; + final _stateNotifier = ValueNotifier(CastState.unavailable); + + /// Display the native dialog to select the cast device to connect void showCastChooserDialog() { _hostApi.showCastDialog(); } + /// Internal method that shouldn't be visible + @internal void onCastStateChanged(int castState) { - state.value = CastState.values[castState]; + _stateNotifier.value = CastState.values[castState]; } SessionManager? _sessionManager; + + /// Returns the SessionManager. SessionManager get sessionManager { var result = _sessionManager; if (result == null) { @@ -26,10 +37,16 @@ class CastContext { } } +/// The possible casting states. enum CastState { + /// Cast connection has never been initialized. idle, // 0 + /// No Cast devices are available. unavailable, // 1 + /// Cast devices are available, but a Cast session is not established. unconnected, // 2 + /// A Cast session is being established. connecting, // 3 + /// A Cast session is established. connected, // 4 } diff --git a/lib/src/cast/RemoteMediaClient.dart b/lib/src/cast/RemoteMediaClient.dart index 37f5600..b672f07 100644 --- a/lib/src/cast/RemoteMediaClient.dart +++ b/lib/src/cast/RemoteMediaClient.dart @@ -1,31 +1,88 @@ +import 'package:flutter/foundation.dart'; +import 'package:meta/meta.dart'; + +import '../../cast.dart'; import '../PlatformBridgeApis.dart'; typedef ProgressListener = void Function(int progressMs, int durationMs); +/// Class for controlling a media player application running on a receiver. class RemoteMediaClient { - final CastHostApi _hostApi; - - ProgressListener? onProgressUpdated; - RemoteMediaClient(this._hostApi); + final CastHostApi _hostApi; + + /// Listenable state of the remote media player + ValueListenable get playerState => _playerStateNotifier; + final _playerStateNotifier = ValueNotifier(PlayerState.unknown); + + /// Callback to get updates on the progress of the currently playing media. + ProgressListener? onProgressUpdated; + + /// Called when updated media metadata is received. + VoidCallback? onMetadataUpdated; + + /// Called when updated player queue status information is received. + VoidCallback? onQueueStatusUpdated; + + /// Called when updated player queue preload status information is received, + /// for example, the next item to play has been preloaded. + VoidCallback? onPreloadStatusUpdated; + + /// Called when there is an outgoing request to the receiver. + VoidCallback? onSendingRemoteMediaRequest; + + /// Called when updated ad break status information is received. + VoidCallback? onAdBreakStatusUpdated; + + /// Called when receiving media error message. + VoidCallback? onMediaError; + + /// Loads a new media item with specified options. void load(MediaLoadRequestData request) { _hostApi.loadMediaLoadRequestData(request); } + /// Begins (or resumes) playback of the current media item. void play() { _hostApi.play(); } + /// Pauses playback of the current media item. void pause() { _hostApi.pause(); } + /// Stops playback of the current media item. void stop() { _hostApi.stop(); } + /// Returns the current media information Future getMediaInfo() async { + // FIXME: can remove future? we could avoid to call host and rely on listener callbacks (maybe onMetadataUpdated) return await _hostApi.getMediaInfo(); } + + /// Internal method that shouldn't be visible + @internal + void dispatchPlayerStateUpdate(PlayerState playerState) { + this._playerStateNotifier.value = playerState; + } +} + +/// State of the remote media player +enum PlayerState { + /// Constant indicating unknown player state. + unknown, // 0 + /// Constant indicating that the media player is idle. + idle, // 1 + /// Constant indicating that the media player is playing. + playing, // 2 + /// Constant indicating that the media player is paused. + paused, // 3 + /// Constant indicating that the media player is buffering. + buffering, // 4 + /// Constant indicating that the media player is loading. + loading, // 5 } diff --git a/lib/src/cast/SessionManager.dart b/lib/src/cast/SessionManager.dart index 5b38521..85fc57a 100644 --- a/lib/src/cast/SessionManager.dart +++ b/lib/src/cast/SessionManager.dart @@ -1,27 +1,33 @@ import 'package:flutter/foundation.dart'; +import 'package:meta/meta.dart'; import '../PlatformBridgeApis.dart'; import 'RemoteMediaClient.dart'; +/// A class that manages Session instances. The application can attach a +/// listeners to be notified of session events. class SessionManager { - final CastHostApi _hostApi; - SessionManager(this._hostApi); - final state = ValueNotifier(SessionState.idle); - final playerState = ValueNotifier(PlayerState.unknown); + final CastHostApi _hostApi; + /// Listenable session state of the cast device + ValueListenable get state => _stateNotifier; + final _stateNotifier = ValueNotifier(SessionState.idle); + + /// Internal method that shouldn't be visible + @internal void onSessionStateChanged(SessionState sessionState) { switch (sessionState) { - case SessionState.session_starting: - case SessionState.session_started: - case SessionState.session_start_failed: - case SessionState.session_ending: - case SessionState.session_ended: - case SessionState.session_resuming: - case SessionState.session_resumed: - case SessionState.session_resume_failed: - case SessionState.session_suspended: - state.value = sessionState; + case SessionState.starting: + case SessionState.started: + case SessionState.start_failed: + case SessionState.ending: + case SessionState.ended: + case SessionState.resuming: + case SessionState.resumed: + case SessionState.resume_failed: + case SessionState.suspended: + _stateNotifier.value = sessionState; break; case SessionState.idle: // Not raised @@ -29,21 +35,11 @@ class SessionManager { } } - void dispatchOnPlayerStateUpdated(PlayerState playerState) { - this.playerState.value = playerState; - onStatusUpdated?.call(playerState); - } - + /// Callback called when the Cast Receiver sent a message MessageReceivedCallback? onMessageReceived; - StatusUpdatedCallback? onStatusUpdated; - VoidCallback? onMetadataUpdated; - VoidCallback? onQueueStatusUpdated; - VoidCallback? onPreloadStatusUpdated; - VoidCallback? onSendingRemoteMediaRequest; - VoidCallback? onAdBreakStatusUpdated; - VoidCallback? onMediaError; - + /// Internal method that shouldn't be visible + @internal void platformOnMessageReceived(CastMessage castMessage) { var thisOnMessageReceived = onMessageReceived; @@ -54,6 +50,7 @@ class SessionManager { thisOnMessageReceived(namespace, message); } + /// Send a string message to the Cast Receiver using the input namespace void sendMessage(String namespace, String message) { final castMessage = CastMessage(); castMessage.namespace = namespace; @@ -62,6 +59,8 @@ class SessionManager { } RemoteMediaClient? _remoteMediaClient; + + /// Returns the RemoteMediaClient for remote media control. RemoteMediaClient get remoteMediaClient { var result = _remoteMediaClient; if (result == null) { @@ -74,26 +73,16 @@ class SessionManager { typedef MessageReceivedCallback = void Function( String namespace, String message); +/// State of the session enum SessionState { idle, - session_starting, - session_started, - session_start_failed, - session_ending, - session_ended, - session_resuming, - session_resumed, - session_resume_failed, - session_suspended, -} - -typedef StatusUpdatedCallback = void Function(PlayerState); - -enum PlayerState { - unknown, // 0 - idle, // 1 - playing, // 2 - paused, // 3 - buffering, // 4 - loading, // 5 + starting, + started, + start_failed, + ending, + ended, + resuming, + resumed, + resume_failed, + suspended, } diff --git a/lib/src/cast/widgets/expanded_controls/ExpandedControlsPlayer.dart b/lib/src/cast/widgets/expanded_controls/ExpandedControlsPlayer.dart index a4990e6..b8f50cf 100644 --- a/lib/src/cast/widgets/expanded_controls/ExpandedControlsPlayer.dart +++ b/lib/src/cast/widgets/expanded_controls/ExpandedControlsPlayer.dart @@ -71,7 +71,7 @@ class ExpandedControlsPlayer extends StatelessWidget { return Padding( padding: const EdgeInsets.symmetric(horizontal: 36, vertical: 0), child: ValueListenableBuilder( - valueListenable: sessionManager.playerState, + valueListenable: sessionManager.remoteMediaClient.playerState, builder: (context, value, child) { final playerState = value as PlayerState; return Row( diff --git a/lib/src/flutter_cast_framework.dart b/lib/src/flutter_cast_framework.dart index 9af2af1..fafed77 100644 --- a/lib/src/flutter_cast_framework.dart +++ b/lib/src/flutter_cast_framework.dart @@ -1,24 +1,51 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_cast_framework/cast.dart'; +import 'package:flutter_cast_framework/src/cast/RemoteMediaClient.dart'; import 'PlatformBridgeApis.dart'; import 'cast/CastContext.dart'; -class FlutterCastFramework extends CastFlutterApi { +/// Entrypoint for the Flutter Cast Framework +class FlutterCastFramework { final _hostApi = CastHostApi(); - late CastContext castContext; + late _CastFlutterApiImplementor _castFlutterApiImplementor; - /// List of namespaces to listen for custom messages - late List namespaces = []; + /// Get the entrypoint for the Cast SDK. This is immutable and it is expected to never change. + CastContext get castContext => _castFlutterApiImplementor.castContext; + /// Create the Flutter Cast Framework. + /// namespaces is the list of namespaces to listen for custom messages. FlutterCastFramework.create(List namespaces) { debugPrint("FlutterCastFramework created!"); - this.namespaces = namespaces; - this.castContext = CastContext(_hostApi); - CastFlutterApi.setup(this); + final castContext = CastContext(_hostApi); + this._castFlutterApiImplementor = new _CastFlutterApiImplementor( + castContext: castContext, + namespaces: namespaces, + ); + + CastFlutterApi.setup(this._castFlutterApiImplementor); + } +} + +/// This implements Pigeon's API called by the Host platform. This is implemented +/// in a separate class to hide the methods +class _CastFlutterApiImplementor extends CastFlutterApi { + final CastContext castContext; + final List namespaces = []; + + SessionManager get sessionManager => castContext.sessionManager; + RemoteMediaClient get remoteMediaClient => + castContext.sessionManager.remoteMediaClient; + + _CastFlutterApiImplementor({ + required this.castContext, + List? namespaces, + }) { + if (namespaces != null) { + this.namespaces.addAll(namespaces); + } } - //region CastFlutterApi implementation @override List getSessionMessageNamespaces() { return namespaces; @@ -31,106 +58,96 @@ class FlutterCastFramework extends CastFlutterApi { @override void onMessageReceived(CastMessage castMessage) { - castContext.sessionManager.platformOnMessageReceived(castMessage); + sessionManager.platformOnMessageReceived(castMessage); } //region Session State handling @override void onSessionEnded() { - castContext.sessionManager - .onSessionStateChanged(SessionState.session_ended); + sessionManager.onSessionStateChanged(SessionState.ended); } @override void onSessionEnding() { - castContext.sessionManager - .onSessionStateChanged(SessionState.session_ending); + sessionManager.onSessionStateChanged(SessionState.ending); } @override void onSessionResumeFailed() { - castContext.sessionManager - .onSessionStateChanged(SessionState.session_resume_failed); + sessionManager.onSessionStateChanged(SessionState.resume_failed); } @override void onSessionResumed() { - castContext.sessionManager - .onSessionStateChanged(SessionState.session_resumed); + sessionManager.onSessionStateChanged(SessionState.resumed); } @override void onSessionResuming() { - castContext.sessionManager - .onSessionStateChanged(SessionState.session_resuming); + sessionManager.onSessionStateChanged(SessionState.resuming); } @override void onSessionStartFailed() { - castContext.sessionManager - .onSessionStateChanged(SessionState.session_start_failed); + sessionManager.onSessionStateChanged(SessionState.start_failed); } @override void onSessionStarted() { - castContext.sessionManager - .onSessionStateChanged(SessionState.session_started); + sessionManager.onSessionStateChanged(SessionState.started); } @override void onSessionStarting() { - castContext.sessionManager - .onSessionStateChanged(SessionState.session_starting); + sessionManager.onSessionStateChanged(SessionState.starting); } @override void onSessionSuspended() { - castContext.sessionManager - .onSessionStateChanged(SessionState.session_suspended); + sessionManager.onSessionStateChanged(SessionState.suspended); } //endregion //region RemoteMediaClient @override void onAdBreakStatusUpdated() { - castContext.sessionManager.onAdBreakStatusUpdated?.call(); + remoteMediaClient.onAdBreakStatusUpdated?.call(); } @override void onMediaError() { - castContext.sessionManager.onMediaError?.call(); + remoteMediaClient.onMediaError?.call(); } @override void onMetadataUpdated() { - castContext.sessionManager.onMetadataUpdated?.call(); + remoteMediaClient.onMetadataUpdated?.call(); } @override void onPreloadStatusUpdated() { - castContext.sessionManager.onPreloadStatusUpdated?.call(); + remoteMediaClient.onPreloadStatusUpdated?.call(); } @override void onQueueStatusUpdated() { - castContext.sessionManager.onQueueStatusUpdated?.call(); + remoteMediaClient.onQueueStatusUpdated?.call(); } @override void onSendingRemoteMediaRequest() { - castContext.sessionManager.onSendingRemoteMediaRequest?.call(); + remoteMediaClient.onSendingRemoteMediaRequest?.call(); } @override void onStatusUpdated(int playerStateRaw) { final playerState = PlayerState.values[playerStateRaw]; - castContext.sessionManager.dispatchOnPlayerStateUpdated(playerState); + remoteMediaClient.dispatchPlayerStateUpdate(playerState); } @override void onProgressUpdated(int progressMs, int durationMs) { - castContext.sessionManager.remoteMediaClient.onProgressUpdated - ?.call(progressMs, durationMs); + remoteMediaClient.onProgressUpdated?.call(progressMs, durationMs); } //endregion } diff --git a/lib/widgets.dart b/lib/widgets.dart index 4497735..31ecb87 100644 --- a/lib/widgets.dart +++ b/lib/widgets.dart @@ -3,3 +3,6 @@ library widgets; export 'src/cast/widgets/CastButton.dart'; export 'src/cast/widgets/CastIcon.dart'; export 'src/cast/widgets/expanded_controls/ExpandedControls.dart'; +export 'src/cast/widgets/expanded_controls/ExpandedControlsPlayer.dart'; +export 'src/cast/widgets/expanded_controls/ExpandedControlsProgress.dart'; +export 'src/cast/widgets/expanded_controls/ExpandedControlsToolbar.dart'; diff --git a/lib/src/PlatformBridgeApisDefinition.dart b/pigeon/PlatformBridgeApisDefinition.dart similarity index 100% rename from lib/src/PlatformBridgeApisDefinition.dart rename to pigeon/PlatformBridgeApisDefinition.dart diff --git a/pubspec.lock b/pubspec.lock index 9af0e54..9dcf218 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -131,7 +131,7 @@ packages: source: hosted version: "0.12.10" meta: - dependency: transitive + dependency: "direct main" description: name: meta url: "https://pub.dartlang.org" diff --git a/pubspec.yaml b/pubspec.yaml index 8b5ffb0..4e0186e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,7 @@ dependencies: flutter: sdk: flutter flutter_svg: ^0.22.0 + meta: ^1.7.0 dev_dependencies: flutter_test: