network addition
This commit is contained in:
parent
3bd713d667
commit
2099d3d64d
27 changed files with 2237 additions and 40 deletions
135
lib/remote/remote_session.dart
Normal file
135
lib/remote/remote_session.dart
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'discovery.dart';
|
||||
import 'messages.dart';
|
||||
|
||||
/// Connection lifecycle of a remote-control session.
|
||||
enum RemoteConnStatus {
|
||||
/// Dialing / mid-handshake.
|
||||
connecting,
|
||||
|
||||
/// Handshake accepted; snapshots flowing.
|
||||
connected,
|
||||
|
||||
/// Host rejected the handshake (wrong server / version / auth).
|
||||
denied,
|
||||
|
||||
/// Cleanly closed (host went away or we detached).
|
||||
disconnected,
|
||||
|
||||
/// Transport failure (couldn't reach the host, socket dropped).
|
||||
error,
|
||||
}
|
||||
|
||||
/// The controller side of a session: dials a [DiscoveredDevice], completes the
|
||||
/// HMAC handshake, then surfaces the host's [StateSnapshot]s and forwards
|
||||
/// [RemoteCommand]s back. Pure transport — it holds no playback state and knows
|
||||
/// nothing about just_audio; `RemoteControlController` adapts it to the UI.
|
||||
class RemoteSession {
|
||||
RemoteSession({
|
||||
required this.device,
|
||||
required this.serverKey,
|
||||
required this.password,
|
||||
required this.deviceName,
|
||||
});
|
||||
|
||||
final DiscoveredDevice device;
|
||||
final String serverKey;
|
||||
final String password;
|
||||
final String deviceName;
|
||||
|
||||
WebSocket? _socket;
|
||||
StreamSubscription<dynamic>? _sub;
|
||||
|
||||
final StreamController<StateSnapshot> _snapshots =
|
||||
StreamController<StateSnapshot>.broadcast();
|
||||
final StreamController<RemoteConnStatus> _status =
|
||||
StreamController<RemoteConnStatus>.broadcast();
|
||||
RemoteConnStatus _current = RemoteConnStatus.disconnected;
|
||||
|
||||
/// Host-reported friendly name, available after [RemoteConnStatus.connected].
|
||||
String? hostDevice;
|
||||
|
||||
Stream<StateSnapshot> get snapshots => _snapshots.stream;
|
||||
Stream<RemoteConnStatus> get status => _status.stream;
|
||||
RemoteConnStatus get currentStatus => _current;
|
||||
|
||||
/// Dial the host and start the handshake. Status transitions are emitted on
|
||||
/// [status]; on success snapshots begin arriving on [snapshots].
|
||||
Future<void> connect() async {
|
||||
_set(RemoteConnStatus.connecting);
|
||||
try {
|
||||
final ws = await WebSocket.connect(device.wsUri.toString())
|
||||
.timeout(const Duration(seconds: 8));
|
||||
_socket = ws;
|
||||
_sub = ws.listen(
|
||||
_onData,
|
||||
onDone: () => _set(RemoteConnStatus.disconnected),
|
||||
onError: (_) => _set(RemoteConnStatus.error),
|
||||
cancelOnError: true,
|
||||
);
|
||||
} catch (_) {
|
||||
_set(RemoteConnStatus.error);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a control command to the host (no-op if not connected).
|
||||
void send(RemoteCommand cmd) => _sendMessage(CommandMessage(cmd));
|
||||
|
||||
Future<void> close() async {
|
||||
await _sub?.cancel();
|
||||
_sub = null;
|
||||
try {
|
||||
await _socket?.close();
|
||||
} catch (_) {
|
||||
// Already closed.
|
||||
}
|
||||
_socket = null;
|
||||
_set(RemoteConnStatus.disconnected);
|
||||
if (!_snapshots.isClosed) await _snapshots.close();
|
||||
if (!_status.isClosed) await _status.close();
|
||||
}
|
||||
|
||||
void _onData(dynamic data) {
|
||||
if (data is! String) return;
|
||||
RemoteMessage msg;
|
||||
try {
|
||||
msg = RemoteMessage.decode(data);
|
||||
} catch (_) {
|
||||
return; // ignore a malformed frame
|
||||
}
|
||||
if (msg is ChallengeMessage) {
|
||||
// Prove we know the shared password without ever sending it.
|
||||
_sendMessage(AuthMessage(
|
||||
version: kRemoteProtocolVersion,
|
||||
serverKey: serverKey,
|
||||
proof: RemoteAuth.proofFor(password: password, nonce: msg.nonce),
|
||||
device: deviceName,
|
||||
));
|
||||
} else if (msg is WelcomeMessage) {
|
||||
hostDevice = msg.device;
|
||||
_set(RemoteConnStatus.connected);
|
||||
} else if (msg is DenyMessage) {
|
||||
_set(RemoteConnStatus.denied);
|
||||
_socket?.close().catchError((_) {});
|
||||
} else if (msg is SnapshotMessage) {
|
||||
if (!_snapshots.isClosed) _snapshots.add(msg.snapshot);
|
||||
}
|
||||
// AuthMessage / CommandMessage are host-inbound only; ignore if echoed.
|
||||
}
|
||||
|
||||
void _sendMessage(RemoteMessage m) {
|
||||
try {
|
||||
_socket?.add(m.encode());
|
||||
} catch (_) {
|
||||
// Socket gone; the listen callbacks will surface the disconnect.
|
||||
}
|
||||
}
|
||||
|
||||
void _set(RemoteConnStatus s) {
|
||||
if (_current == s) return;
|
||||
_current = s;
|
||||
if (!_status.isClosed) _status.add(s);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue