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",
"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"
],
}
]
}

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
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 \
@ -9,3 +11,12 @@ pigeon: # Generates the typesafe bridge between host and flutter
deploy-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
* Send custom message
* Listen to received custom messages
* Load RemoteMediaRequestData
* Play, Pause, Stop media
* Expanded controls
* Cast Button
* Chromecast connection
@ -137,3 +140,13 @@ I used this project to test the capabilities of the following technologies:
* Chromecast API (Sender - Android SDK)
* Flutter
* 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();
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<MyApp> {
});
}
void _onRemoteMediaClientStatusUpdated(PlayerState playerState) {
void _onRemoteMediaClientStatusUpdated() {
final playerState = castFramework
.castContext.sessionManager.remoteMediaClient.playerState.value;
debugPrint("RemoteMediaClient status updated - playerState $playerState");
}

View file

@ -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;

View file

@ -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<CastState> state = ValueNotifier(CastState.unavailable);
final CastHostApi _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() {
_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
}

View file

@ -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<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) {
_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<MediaInfo> 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
}

View file

@ -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<SessionState> 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,
}

View file

@ -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(

View file

@ -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<String> 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<String> 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<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
List<String?> 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
}

View file

@ -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';

View file

@ -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"

View file

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