Refactor classes and improve docs

This commit is contained in:
gianlucaparadise 2021-12-03 07:08:27 +01:00
parent 97425c97f6
commit d31ff882b5
14 changed files with 246 additions and 105 deletions

23
.vscode/launch.json vendored
View file

@ -5,9 +5,30 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Flutter", "name": "Current Device",
"request": "launch", "request": "launch",
"type": "dart" "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"
],
} }
] ]
} }

View file

@ -1,6 +1,8 @@
.PHONY: pigeon deploy-receiver open-android open-ios run-all
pigeon: # Generates the typesafe bridge between host and flutter pigeon: # Generates the typesafe bridge between host and flutter
flutter pub run pigeon \ flutter pub run pigeon \
--input lib/src/PlatformBridgeApisDefinition.dart \ --input pigeon/PlatformBridgeApisDefinition.dart \
--dart_out lib/src/PlatformBridgeApis.dart \ --dart_out lib/src/PlatformBridgeApis.dart \
--objc_header_out ios/Classes/PlatformBridgeApis.h \ --objc_header_out ios/Classes/PlatformBridgeApis.h \
--objc_source_out ios/Classes/PlatformBridgeApis.m \ --objc_source_out ios/Classes/PlatformBridgeApis.m \
@ -9,3 +11,12 @@ pigeon: # Generates the typesafe bridge between host and flutter
deploy-receiver: deploy-receiver:
surge receiver surge receiver
open-android:
studio example/android
open-ios:
open example/ios/Runner.xcworkspace
run-all:
cd example && flutter run -d all

View file

@ -12,6 +12,9 @@ Currently only the following APIs are integrated (both Android and iOS):
* Session state * Session state
* Send custom message * Send custom message
* Listen to received custom messages * Listen to received custom messages
* Load RemoteMediaRequestData
* Play, Pause, Stop media
* Expanded controls
* Cast Button * Cast Button
* Chromecast connection * Chromecast connection
@ -137,3 +140,13 @@ I used this project to test the capabilities of the following technologies:
* Chromecast API (Sender - Android SDK) * Chromecast API (Sender - Android SDK)
* Flutter * Flutter
* Flutter custom platform-specific code * 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

View file

@ -32,18 +32,27 @@ class _MyAppState extends State<MyApp> {
super.initState(); super.initState();
castFramework = FlutterCastFramework.create([castNamespace]); castFramework = FlutterCastFramework.create([castNamespace]);
castFramework.castContext.state.addListener(_onCastStateChanged); castFramework.castContext.state.addListener(_onCastStateChanged);
castFramework.castContext.sessionManager.state
.addListener(_onSessionStateChanged); final sessionManager = castFramework.castContext.sessionManager;
castFramework.castContext.sessionManager.onMessageReceived = sessionManager.state.addListener(_onSessionStateChanged);
_onMessageReceived; sessionManager.onMessageReceived = _onMessageReceived;
castFramework.castContext.sessionManager.onStatusUpdated = sessionManager.remoteMediaClient.playerState
_onRemoteMediaClientStatusUpdated; .addListener(_onRemoteMediaClientStatusUpdated);
} }
@override @override
void dispose() { void dispose() {
// Clean up the controller when the widget is disposed. // Clean up the controller when the widget is disposed.
textMessageController.dispose(); 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(); super.dispose();
} }
@ -68,7 +77,9 @@ class _MyAppState extends State<MyApp> {
}); });
} }
void _onRemoteMediaClientStatusUpdated(PlayerState playerState) { void _onRemoteMediaClientStatusUpdated() {
final playerState = castFramework
.castContext.sessionManager.remoteMediaClient.playerState.value;
debugPrint("RemoteMediaClient status updated - playerState $playerState"); debugPrint("RemoteMediaClient status updated - playerState $playerState");
} }

View file

@ -2,5 +2,6 @@ library cast;
export 'src/cast/CastContext.dart'; export 'src/cast/CastContext.dart';
export 'src/cast/SessionManager.dart'; export 'src/cast/SessionManager.dart';
export 'src/cast/RemoteMediaClient.dart';
export 'src/flutter_cast_framework.dart'; export 'src/flutter_cast_framework.dart';
export 'src/PlatformBridgeApis.dart'; export 'src/PlatformBridgeApis.dart' hide CastFlutterApi, CastHostApi;

View file

@ -1,22 +1,33 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:meta/meta.dart';
import '../PlatformBridgeApis.dart'; import '../PlatformBridgeApis.dart';
import 'SessionManager.dart'; import 'SessionManager.dart';
/// Class wrapping the global context fot the Cast SDK
class CastContext { class CastContext {
final ValueNotifier<CastState> state = ValueNotifier(CastState.unavailable);
final CastHostApi _hostApi;
CastContext(this._hostApi); CastContext(this._hostApi);
final CastHostApi _hostApi;
/// Listenable connection state of the cast device
ValueListenable<CastState> get state => _stateNotifier;
final _stateNotifier = ValueNotifier(CastState.unavailable);
/// Display the native dialog to select the cast device to connect
void showCastChooserDialog() { void showCastChooserDialog() {
_hostApi.showCastDialog(); _hostApi.showCastDialog();
} }
/// Internal method that shouldn't be visible
@internal
void onCastStateChanged(int castState) { void onCastStateChanged(int castState) {
state.value = CastState.values[castState]; _stateNotifier.value = CastState.values[castState];
} }
SessionManager? _sessionManager; SessionManager? _sessionManager;
/// Returns the SessionManager.
SessionManager get sessionManager { SessionManager get sessionManager {
var result = _sessionManager; var result = _sessionManager;
if (result == null) { if (result == null) {
@ -26,10 +37,16 @@ class CastContext {
} }
} }
/// The possible casting states.
enum CastState { enum CastState {
/// Cast connection has never been initialized.
idle, // 0 idle, // 0
/// No Cast devices are available.
unavailable, // 1 unavailable, // 1
/// Cast devices are available, but a Cast session is not established.
unconnected, // 2 unconnected, // 2
/// A Cast session is being established.
connecting, // 3 connecting, // 3
/// A Cast session is established.
connected, // 4 connected, // 4
} }

View file

@ -1,31 +1,88 @@
import 'package:flutter/foundation.dart';
import 'package:meta/meta.dart';
import '../../cast.dart';
import '../PlatformBridgeApis.dart'; import '../PlatformBridgeApis.dart';
typedef ProgressListener = void Function(int progressMs, int durationMs); typedef ProgressListener = void Function(int progressMs, int durationMs);
/// Class for controlling a media player application running on a receiver.
class RemoteMediaClient { class RemoteMediaClient {
final CastHostApi _hostApi;
ProgressListener? onProgressUpdated;
RemoteMediaClient(this._hostApi); RemoteMediaClient(this._hostApi);
final CastHostApi _hostApi;
/// Listenable state of the remote media player
ValueListenable<PlayerState> 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) { void load(MediaLoadRequestData request) {
_hostApi.loadMediaLoadRequestData(request); _hostApi.loadMediaLoadRequestData(request);
} }
/// Begins (or resumes) playback of the current media item.
void play() { void play() {
_hostApi.play(); _hostApi.play();
} }
/// Pauses playback of the current media item.
void pause() { void pause() {
_hostApi.pause(); _hostApi.pause();
} }
/// Stops playback of the current media item.
void stop() { void stop() {
_hostApi.stop(); _hostApi.stop();
} }
/// Returns the current media information
Future<MediaInfo> getMediaInfo() async { Future<MediaInfo> getMediaInfo() async {
// FIXME: can remove future? we could avoid to call host and rely on listener callbacks (maybe onMetadataUpdated)
return await _hostApi.getMediaInfo(); 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
} }

View file

@ -1,27 +1,33 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:meta/meta.dart';
import '../PlatformBridgeApis.dart'; import '../PlatformBridgeApis.dart';
import 'RemoteMediaClient.dart'; import 'RemoteMediaClient.dart';
/// A class that manages Session instances. The application can attach a
/// listeners to be notified of session events.
class SessionManager { class SessionManager {
final CastHostApi _hostApi;
SessionManager(this._hostApi); SessionManager(this._hostApi);
final state = ValueNotifier(SessionState.idle); final CastHostApi _hostApi;
final playerState = ValueNotifier(PlayerState.unknown);
/// Listenable session state of the cast device
ValueListenable<SessionState> get state => _stateNotifier;
final _stateNotifier = ValueNotifier(SessionState.idle);
/// Internal method that shouldn't be visible
@internal
void onSessionStateChanged(SessionState sessionState) { void onSessionStateChanged(SessionState sessionState) {
switch (sessionState) { switch (sessionState) {
case SessionState.session_starting: case SessionState.starting:
case SessionState.session_started: case SessionState.started:
case SessionState.session_start_failed: case SessionState.start_failed:
case SessionState.session_ending: case SessionState.ending:
case SessionState.session_ended: case SessionState.ended:
case SessionState.session_resuming: case SessionState.resuming:
case SessionState.session_resumed: case SessionState.resumed:
case SessionState.session_resume_failed: case SessionState.resume_failed:
case SessionState.session_suspended: case SessionState.suspended:
state.value = sessionState; _stateNotifier.value = sessionState;
break; break;
case SessionState.idle: case SessionState.idle:
// Not raised // Not raised
@ -29,21 +35,11 @@ class SessionManager {
} }
} }
void dispatchOnPlayerStateUpdated(PlayerState playerState) { /// Callback called when the Cast Receiver sent a message
this.playerState.value = playerState;
onStatusUpdated?.call(playerState);
}
MessageReceivedCallback? onMessageReceived; MessageReceivedCallback? onMessageReceived;
StatusUpdatedCallback? onStatusUpdated; /// Internal method that shouldn't be visible
VoidCallback? onMetadataUpdated; @internal
VoidCallback? onQueueStatusUpdated;
VoidCallback? onPreloadStatusUpdated;
VoidCallback? onSendingRemoteMediaRequest;
VoidCallback? onAdBreakStatusUpdated;
VoidCallback? onMediaError;
void platformOnMessageReceived(CastMessage castMessage) { void platformOnMessageReceived(CastMessage castMessage) {
var thisOnMessageReceived = onMessageReceived; var thisOnMessageReceived = onMessageReceived;
@ -54,6 +50,7 @@ class SessionManager {
thisOnMessageReceived(namespace, message); thisOnMessageReceived(namespace, message);
} }
/// Send a string message to the Cast Receiver using the input namespace
void sendMessage(String namespace, String message) { void sendMessage(String namespace, String message) {
final castMessage = CastMessage(); final castMessage = CastMessage();
castMessage.namespace = namespace; castMessage.namespace = namespace;
@ -62,6 +59,8 @@ class SessionManager {
} }
RemoteMediaClient? _remoteMediaClient; RemoteMediaClient? _remoteMediaClient;
/// Returns the RemoteMediaClient for remote media control.
RemoteMediaClient get remoteMediaClient { RemoteMediaClient get remoteMediaClient {
var result = _remoteMediaClient; var result = _remoteMediaClient;
if (result == null) { if (result == null) {
@ -74,26 +73,16 @@ class SessionManager {
typedef MessageReceivedCallback = void Function( typedef MessageReceivedCallback = void Function(
String namespace, String message); String namespace, String message);
/// State of the session
enum SessionState { enum SessionState {
idle, idle,
session_starting, starting,
session_started, started,
session_start_failed, start_failed,
session_ending, ending,
session_ended, ended,
session_resuming, resuming,
session_resumed, resumed,
session_resume_failed, resume_failed,
session_suspended, suspended,
}
typedef StatusUpdatedCallback = void Function(PlayerState);
enum PlayerState {
unknown, // 0
idle, // 1
playing, // 2
paused, // 3
buffering, // 4
loading, // 5
} }

View file

@ -71,7 +71,7 @@ class ExpandedControlsPlayer extends StatelessWidget {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 36, vertical: 0), padding: const EdgeInsets.symmetric(horizontal: 36, vertical: 0),
child: ValueListenableBuilder( child: ValueListenableBuilder(
valueListenable: sessionManager.playerState, valueListenable: sessionManager.remoteMediaClient.playerState,
builder: (context, value, child) { builder: (context, value, child) {
final playerState = value as PlayerState; final playerState = value as PlayerState;
return Row( return Row(

View file

@ -1,24 +1,51 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_cast_framework/cast.dart'; import 'package:flutter_cast_framework/cast.dart';
import 'package:flutter_cast_framework/src/cast/RemoteMediaClient.dart';
import 'PlatformBridgeApis.dart'; import 'PlatformBridgeApis.dart';
import 'cast/CastContext.dart'; import 'cast/CastContext.dart';
class FlutterCastFramework extends CastFlutterApi { /// Entrypoint for the Flutter Cast Framework
class FlutterCastFramework {
final _hostApi = CastHostApi(); final _hostApi = CastHostApi();
late CastContext castContext; late _CastFlutterApiImplementor _castFlutterApiImplementor;
/// List of namespaces to listen for custom messages /// Get the entrypoint for the Cast SDK. This is immutable and it is expected to never change.
late List<String> namespaces = []; CastContext get castContext => _castFlutterApiImplementor.castContext;
/// Create the Flutter Cast Framework.
/// namespaces is the list of namespaces to listen for custom messages.
FlutterCastFramework.create(List<String> namespaces) { FlutterCastFramework.create(List<String> namespaces) {
debugPrint("FlutterCastFramework created!"); debugPrint("FlutterCastFramework created!");
this.namespaces = namespaces; final castContext = CastContext(_hostApi);
this.castContext = CastContext(_hostApi); this._castFlutterApiImplementor = new _CastFlutterApiImplementor(
CastFlutterApi.setup(this); 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<String> namespaces = [];
SessionManager get sessionManager => castContext.sessionManager;
RemoteMediaClient get remoteMediaClient =>
castContext.sessionManager.remoteMediaClient;
_CastFlutterApiImplementor({
required this.castContext,
List<String>? namespaces,
}) {
if (namespaces != null) {
this.namespaces.addAll(namespaces);
}
} }
//region CastFlutterApi implementation
@override @override
List<String?> getSessionMessageNamespaces() { List<String?> getSessionMessageNamespaces() {
return namespaces; return namespaces;
@ -31,106 +58,96 @@ class FlutterCastFramework extends CastFlutterApi {
@override @override
void onMessageReceived(CastMessage castMessage) { void onMessageReceived(CastMessage castMessage) {
castContext.sessionManager.platformOnMessageReceived(castMessage); sessionManager.platformOnMessageReceived(castMessage);
} }
//region Session State handling //region Session State handling
@override @override
void onSessionEnded() { void onSessionEnded() {
castContext.sessionManager sessionManager.onSessionStateChanged(SessionState.ended);
.onSessionStateChanged(SessionState.session_ended);
} }
@override @override
void onSessionEnding() { void onSessionEnding() {
castContext.sessionManager sessionManager.onSessionStateChanged(SessionState.ending);
.onSessionStateChanged(SessionState.session_ending);
} }
@override @override
void onSessionResumeFailed() { void onSessionResumeFailed() {
castContext.sessionManager sessionManager.onSessionStateChanged(SessionState.resume_failed);
.onSessionStateChanged(SessionState.session_resume_failed);
} }
@override @override
void onSessionResumed() { void onSessionResumed() {
castContext.sessionManager sessionManager.onSessionStateChanged(SessionState.resumed);
.onSessionStateChanged(SessionState.session_resumed);
} }
@override @override
void onSessionResuming() { void onSessionResuming() {
castContext.sessionManager sessionManager.onSessionStateChanged(SessionState.resuming);
.onSessionStateChanged(SessionState.session_resuming);
} }
@override @override
void onSessionStartFailed() { void onSessionStartFailed() {
castContext.sessionManager sessionManager.onSessionStateChanged(SessionState.start_failed);
.onSessionStateChanged(SessionState.session_start_failed);
} }
@override @override
void onSessionStarted() { void onSessionStarted() {
castContext.sessionManager sessionManager.onSessionStateChanged(SessionState.started);
.onSessionStateChanged(SessionState.session_started);
} }
@override @override
void onSessionStarting() { void onSessionStarting() {
castContext.sessionManager sessionManager.onSessionStateChanged(SessionState.starting);
.onSessionStateChanged(SessionState.session_starting);
} }
@override @override
void onSessionSuspended() { void onSessionSuspended() {
castContext.sessionManager sessionManager.onSessionStateChanged(SessionState.suspended);
.onSessionStateChanged(SessionState.session_suspended);
} }
//endregion //endregion
//region RemoteMediaClient //region RemoteMediaClient
@override @override
void onAdBreakStatusUpdated() { void onAdBreakStatusUpdated() {
castContext.sessionManager.onAdBreakStatusUpdated?.call(); remoteMediaClient.onAdBreakStatusUpdated?.call();
} }
@override @override
void onMediaError() { void onMediaError() {
castContext.sessionManager.onMediaError?.call(); remoteMediaClient.onMediaError?.call();
} }
@override @override
void onMetadataUpdated() { void onMetadataUpdated() {
castContext.sessionManager.onMetadataUpdated?.call(); remoteMediaClient.onMetadataUpdated?.call();
} }
@override @override
void onPreloadStatusUpdated() { void onPreloadStatusUpdated() {
castContext.sessionManager.onPreloadStatusUpdated?.call(); remoteMediaClient.onPreloadStatusUpdated?.call();
} }
@override @override
void onQueueStatusUpdated() { void onQueueStatusUpdated() {
castContext.sessionManager.onQueueStatusUpdated?.call(); remoteMediaClient.onQueueStatusUpdated?.call();
} }
@override @override
void onSendingRemoteMediaRequest() { void onSendingRemoteMediaRequest() {
castContext.sessionManager.onSendingRemoteMediaRequest?.call(); remoteMediaClient.onSendingRemoteMediaRequest?.call();
} }
@override @override
void onStatusUpdated(int playerStateRaw) { void onStatusUpdated(int playerStateRaw) {
final playerState = PlayerState.values[playerStateRaw]; final playerState = PlayerState.values[playerStateRaw];
castContext.sessionManager.dispatchOnPlayerStateUpdated(playerState); remoteMediaClient.dispatchPlayerStateUpdate(playerState);
} }
@override @override
void onProgressUpdated(int progressMs, int durationMs) { void onProgressUpdated(int progressMs, int durationMs) {
castContext.sessionManager.remoteMediaClient.onProgressUpdated remoteMediaClient.onProgressUpdated?.call(progressMs, durationMs);
?.call(progressMs, durationMs);
} }
//endregion //endregion
} }

View file

@ -3,3 +3,6 @@ library widgets;
export 'src/cast/widgets/CastButton.dart'; export 'src/cast/widgets/CastButton.dart';
export 'src/cast/widgets/CastIcon.dart'; export 'src/cast/widgets/CastIcon.dart';
export 'src/cast/widgets/expanded_controls/ExpandedControls.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';

View file

@ -131,7 +131,7 @@ packages:
source: hosted source: hosted
version: "0.12.10" version: "0.12.10"
meta: meta:
dependency: transitive dependency: "direct main"
description: description:
name: meta name: meta
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"

View file

@ -10,6 +10,7 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_svg: ^0.22.0 flutter_svg: ^0.22.0
meta: ^1.7.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: