Files
kryz-go-flutter/lib/casting_business_object.dart
T

262 lines
7.0 KiB
Dart
Executable File

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
enum CastingStatus { unavailable, idle, connecting, connected, error }
enum CastingPlaybackStatus { unknown, stopped, buffering, playing }
class CastingBusinessObject extends ChangeNotifier {
static const MethodChannel _methodChannel = MethodChannel(
'kryz_go_flutter/casting/methods',
);
static const EventChannel _eventChannel = EventChannel(
'kryz_go_flutter/casting/events',
);
StreamSubscription<dynamic>? _eventSubscription;
CastingStatus _status = CastingStatus.unavailable;
CastingPlaybackStatus _playbackStatus = CastingPlaybackStatus.unknown;
String? _connectedDeviceName;
String? _lastError;
/// Callback for when native side requests local audio to stop
VoidCallback? onStopLocalAudioRequested;
CastingStatus get status => _status;
CastingPlaybackStatus get playbackStatus => _playbackStatus;
String? get connectedDeviceName => _connectedDeviceName;
String? get lastError => _lastError;
bool get isSupportedOnThisPlatform {
if (kIsWeb) {
return false;
}
return defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.android;
}
bool get isConnected => _status == CastingStatus.connected;
bool get isPlaying => _playbackStatus == CastingPlaybackStatus.playing;
bool get isBuffering => _playbackStatus == CastingPlaybackStatus.buffering;
bool get isStopped => _playbackStatus == CastingPlaybackStatus.stopped;
Future<void> initialize() async {
if (!isSupportedOnThisPlatform) {
_setStatus(CastingStatus.unavailable);
return;
}
_eventSubscription?.cancel();
_eventSubscription = _eventChannel.receiveBroadcastStream().listen(
_handleEvent,
onError: (Object error) {
_lastError = error.toString();
_setStatus(CastingStatus.error);
},
);
// Set up handler for native method calls (e.g., stopLocalAudio)
_methodChannel.setMethodCallHandler(_handleMethodCall);
await refreshStatus();
}
Future<void> refreshStatus() async {
if (!isSupportedOnThisPlatform) {
_setStatus(CastingStatus.unavailable);
return;
}
try {
final dynamic status = await _methodChannel.invokeMethod<dynamic>(
'getStatus',
);
_handleEvent(status);
} on PlatformException catch (error) {
_lastError = error.message ?? error.code;
_setStatus(CastingStatus.error);
}
}
Future<void> showDevicePicker({
bool autoplay = false,
String? streamUrl,
String? title,
String? subtitle,
}) async {
if (!isSupportedOnThisPlatform) {
_setStatus(CastingStatus.unavailable);
return;
}
if (defaultTargetPlatform == TargetPlatform.android) {
_setStatus(CastingStatus.connecting);
}
try {
await _methodChannel.invokeMethod<void>('showDevicePicker', {
'autoplay': autoplay,
'streamUrl': streamUrl,
'title': title,
'subtitle': subtitle,
});
} on PlatformException catch (error) {
_lastError = error.message ?? error.code;
_setStatus(CastingStatus.error);
}
}
Future<void> startCastingLiveStream({
required String streamUrl,
required String title,
required String subtitle,
}) async {
if (!isSupportedOnThisPlatform) {
_setStatus(CastingStatus.unavailable);
return;
}
try {
await _methodChannel.invokeMethod<void>('startCastingLiveStream', {
'streamUrl': streamUrl,
'title': title,
'subtitle': subtitle,
});
} on PlatformException catch (error) {
_lastError = error.message ?? error.code;
// Do NOT set status to error here — native emits its own status events.
// Overriding status from Dart corrupts the UI when native considers the
// session still connected (e.g. during a retry after a race condition).
rethrow;
}
}
Future<void> disconnect() async {
if (!isSupportedOnThisPlatform) {
_setStatus(CastingStatus.unavailable);
return;
}
try {
await _methodChannel.invokeMethod<void>('disconnect');
} on PlatformException catch (error) {
_lastError = error.message ?? error.code;
_setStatus(CastingStatus.error);
}
}
Future<void> stopPlayback() async {
if (!isSupportedOnThisPlatform) {
_setStatus(CastingStatus.unavailable);
return;
}
try {
await _methodChannel.invokeMethod<void>('stopCastingPlayback');
} on PlatformException catch (error) {
_lastError = error.message ?? error.code;
_setStatus(CastingStatus.error);
}
}
void _handleEvent(dynamic event) {
if (event is! Map) {
return;
}
final String? status = event['status'] as String?;
final String? playbackStatus = event['playbackStatus'] as String?;
final String? deviceName = event['deviceName'] as String?;
final String? message = event['message'] as String?;
if (deviceName != null && deviceName.isNotEmpty) {
_connectedDeviceName = deviceName;
} else if (status != 'connected') {
_connectedDeviceName = null;
}
if (message != null && message.isNotEmpty) {
_lastError = message;
}
_setPlaybackStatus(_parsePlaybackStatus(playbackStatus));
switch (status) {
case 'idle':
_setStatus(CastingStatus.idle);
break;
case 'connecting':
_setStatus(CastingStatus.connecting);
break;
case 'connected':
_setStatus(CastingStatus.connected);
break;
case 'error':
_setStatus(CastingStatus.error);
break;
case 'unavailable':
default:
_setStatus(CastingStatus.unavailable);
break;
}
}
void _setStatus(CastingStatus status) {
if (_status == status) {
return;
}
_status = status;
notifyListeners();
}
CastingPlaybackStatus _parsePlaybackStatus(String? playbackStatus) {
switch (playbackStatus) {
case 'playing':
return CastingPlaybackStatus.playing;
case 'buffering':
return CastingPlaybackStatus.buffering;
case 'stopped':
return CastingPlaybackStatus.stopped;
default:
return CastingPlaybackStatus.unknown;
}
}
void _setPlaybackStatus(CastingPlaybackStatus status) {
if (_playbackStatus == status) {
return;
}
_playbackStatus = status;
notifyListeners();
}
@override
void dispose() {
_eventSubscription?.cancel();
super.dispose();
}
/// Handles method calls from native code
Future<dynamic> _handleMethodCall(MethodCall call) async {
switch (call.method) {
case 'stopLocalAudio':
onStopLocalAudioRequested?.call();
return null;
case 'stopAudioService':
// Stop the audio service directly (called when transferring to cast)
onStopLocalAudioRequested?.call();
return null;
case 'resumeLocalAudio':
// Audio service is resuming after cast disconnect; no action needed on Flutter side
return null;
default:
return null;
}
}
}