fix desktop workspace stream fallback
This commit is contained in:
parent
6659a8057a
commit
b2dda988a2
@ -38,7 +38,6 @@ class DesktopClient {
|
||||
|
||||
RTCPeerConnection? _peerConnection;
|
||||
RTCDataChannel? _dataChannel;
|
||||
MediaStream? _remoteStream;
|
||||
|
||||
final StreamController<MediaStream> _streamController =
|
||||
StreamController<MediaStream>.broadcast();
|
||||
@ -77,12 +76,14 @@ class DesktopClient {
|
||||
_peerConnection = await createPeerConnection(config);
|
||||
|
||||
// Listen for remote streams
|
||||
_peerConnection!.onTrack = (event) {
|
||||
_peerConnection!.onTrack = (event) async {
|
||||
if (event.track.kind == 'video') {
|
||||
if (event.streams.isNotEmpty) {
|
||||
_remoteStream = event.streams.first;
|
||||
_streamController.add(_remoteStream!);
|
||||
}
|
||||
final remoteStream = await desktopRemoteStreamFromTrack(
|
||||
track: event.track,
|
||||
streams: event.streams,
|
||||
);
|
||||
if (remoteStream == null) return;
|
||||
_streamController.add(remoteStream);
|
||||
}
|
||||
};
|
||||
|
||||
@ -204,7 +205,27 @@ class DesktopClient {
|
||||
await _peerConnection?.close();
|
||||
_dataChannel = null;
|
||||
_peerConnection = null;
|
||||
_remoteStream = null;
|
||||
_stateController.add('disconnected');
|
||||
}
|
||||
}
|
||||
|
||||
Future<MediaStream?> desktopRemoteStreamFromTrack({
|
||||
required MediaStreamTrack track,
|
||||
required List<MediaStream> streams,
|
||||
Future<MediaStream> Function(String streamId)? streamFactory,
|
||||
}) async {
|
||||
if (track.kind != 'video') {
|
||||
return null;
|
||||
}
|
||||
if (streams.isNotEmpty) {
|
||||
return streams.first;
|
||||
}
|
||||
final factory = streamFactory ?? createLocalMediaStream;
|
||||
final syntheticStream = await factory(
|
||||
'desktop-remote-${track.id}',
|
||||
);
|
||||
// The bridge may deliver a bare video track without an attached stream.
|
||||
// Wrapping it keeps RTCVideoView rendering instead of leaving the panel blank.
|
||||
syntheticStream.addTrack(track);
|
||||
return syntheticStream;
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@ import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
import 'package:xworkmate/features/desktop/desktop_client.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('DesktopClient protocol helpers', () {
|
||||
test('normalizes WebRTC connection states for view gating', () {
|
||||
expect(
|
||||
@ -42,5 +44,113 @@ void main() {
|
||||
expect(params['width'], 1280);
|
||||
expect(params['height'], 720);
|
||||
});
|
||||
|
||||
test('creates a synthetic media stream when the bridge omits streams', () async {
|
||||
final track = _FakeMediaStreamTrack(
|
||||
id: 'track-1',
|
||||
kind: 'video',
|
||||
);
|
||||
final createdStreams = <_FakeMediaStream>[];
|
||||
|
||||
final stream = await desktopRemoteStreamFromTrack(
|
||||
track: track,
|
||||
streams: const <MediaStream>[],
|
||||
streamFactory: (streamId) async {
|
||||
final stream = _FakeMediaStream(streamId);
|
||||
createdStreams.add(stream);
|
||||
return stream;
|
||||
},
|
||||
);
|
||||
|
||||
expect(stream, isNotNull);
|
||||
expect(createdStreams, hasLength(1));
|
||||
expect(stream, same(createdStreams.single));
|
||||
expect(createdStreams.single.getVideoTracks(), hasLength(1));
|
||||
expect(createdStreams.single.getVideoTracks().single, same(track));
|
||||
});
|
||||
|
||||
test('prefers the attached remote stream when one is present', () async {
|
||||
final track = _FakeMediaStreamTrack(
|
||||
id: 'track-2',
|
||||
kind: 'video',
|
||||
);
|
||||
final attachedStream = _FakeMediaStream('attached-stream');
|
||||
|
||||
final stream = await desktopRemoteStreamFromTrack(
|
||||
track: track,
|
||||
streams: <MediaStream>[attachedStream],
|
||||
streamFactory: (streamId) async => _FakeMediaStream(streamId),
|
||||
);
|
||||
|
||||
expect(stream, same(attachedStream));
|
||||
expect(stream!.getVideoTracks(), isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _FakeMediaStreamTrack extends MediaStreamTrack {
|
||||
_FakeMediaStreamTrack({required this.id, required this.kind});
|
||||
|
||||
@override
|
||||
final String id;
|
||||
|
||||
@override
|
||||
final String kind;
|
||||
|
||||
@override
|
||||
String? get label => 'fake';
|
||||
|
||||
@override
|
||||
bool get enabled => true;
|
||||
|
||||
@override
|
||||
set enabled(bool b) {}
|
||||
|
||||
@override
|
||||
bool? get muted => false;
|
||||
|
||||
@override
|
||||
Future<void> stop() async {}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {}
|
||||
}
|
||||
|
||||
class _FakeMediaStream extends MediaStream {
|
||||
_FakeMediaStream(String id) : super(id, 'test-owner');
|
||||
|
||||
final List<MediaStreamTrack> _tracks = <MediaStreamTrack>[];
|
||||
|
||||
@override
|
||||
bool? get active => _tracks.isNotEmpty;
|
||||
|
||||
@override
|
||||
Future<void> getMediaTracks() async {}
|
||||
|
||||
@override
|
||||
Future<void> addTrack(MediaStreamTrack track, {bool addToNative = true}) async {
|
||||
_tracks.add(track);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> removeTrack(
|
||||
MediaStreamTrack track, {
|
||||
bool removeFromNative = true,
|
||||
}) async {
|
||||
_tracks.remove(track);
|
||||
}
|
||||
|
||||
@override
|
||||
List<MediaStreamTrack> getTracks() => List<MediaStreamTrack>.unmodifiable(_tracks);
|
||||
|
||||
@override
|
||||
List<MediaStreamTrack> getAudioTracks() =>
|
||||
_tracks.where((track) => track.kind == 'audio').toList(growable: false);
|
||||
|
||||
@override
|
||||
List<MediaStreamTrack> getVideoTracks() =>
|
||||
_tracks.where((track) => track.kind == 'video').toList(growable: false);
|
||||
|
||||
@override
|
||||
Future<MediaStream> clone() async => _FakeMediaStream('${id}_clone');
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user