diff --git a/example/android/build.gradle b/example/android/build.gradle index 83ae2200..3cdaac95 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/example/lib/Home.dart b/example/lib/Home.dart index ea2a9c0b..69a068e6 100644 --- a/example/lib/Home.dart +++ b/example/lib/Home.dart @@ -9,7 +9,8 @@ class _HomeState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text("Janus Client Menu")), + appBar: AppBar( + title: Text("Janus Client Menu")), body: SafeArea( child: SingleChildScrollView( child: DefaultTextStyle( diff --git a/example/lib/main.dart b/example/lib/main.dart index 56544ee3..5b0ee4b8 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -10,7 +10,15 @@ import 'typed_examples/text_room.dart'; void main() { runApp(MaterialApp( initialRoute: '/', - themeMode: ThemeMode.light, + theme: ThemeData( + colorScheme: ColorScheme.light(background: Colors.white, primary: Colors.black, secondary: Colors.black, onPrimary: Colors.white, onSecondary: Colors.white), + listTileTheme: ListTileThemeData(titleTextStyle: TextStyle(color: Colors.black)), + appBarTheme: AppBarTheme(titleTextStyle: TextStyle(color: Colors.white), backgroundColor: Colors.purple)), + darkTheme: ThemeData( + colorScheme: ColorScheme.dark(background: Colors.black, primary: Colors.white, secondary: Colors.white, onPrimary: Colors.black, onSecondary: Colors.black), + listTileTheme: ListTileThemeData(titleTextStyle: TextStyle(color: Colors.white)), + appBarTheme: AppBarTheme(titleTextStyle: TextStyle(color: Colors.white), backgroundColor: Colors.grey)), + themeMode: ThemeMode.system, debugShowCheckedModeBanner: false, routes: { "/google-meet": (c) => GoogleMeet(), diff --git a/example/lib/typed_examples/google_meet.dart b/example/lib/typed_examples/google_meet.dart index fb725b04..e55a7206 100644 --- a/example/lib/typed_examples/google_meet.dart +++ b/example/lib/typed_examples/google_meet.dart @@ -7,6 +7,85 @@ import 'package:logging/logging.dart'; import '../util.dart'; +class VideoView extends StatefulWidget { + final StreamRenderer remoteStream; + final int? myId; + final Function(int index) onSubStreamChange; + final Function(int index) onTemporalStreamChange; + const VideoView({Key? key, required this.onSubStreamChange, required this.onTemporalStreamChange, required this.myId, required this.remoteStream}) : super(key: key); + @override + State createState() => _VideoViewState(); +} + +class _VideoViewState extends State { + @override + Widget build(BuildContext context) { + return Visibility( + visible: widget.remoteStream.isVideoMuted == false, + replacement: Container( + child: Center( + child: Text("Video Paused By " + widget.remoteStream.publisherName!, style: TextStyle(color: Colors.white)), + ), + ), + child: Stack(fit: StackFit.expand, clipBehavior: Clip.none, children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: RTCVideoView( + widget.remoteStream.videoRenderer, + filterQuality: FilterQuality.none, + objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, + ), + ), + Visibility( + child: PositionedDirectional( + child: Column(children: [ + ToggleButtons( + direction: Axis.horizontal, + onPressed: widget.onSubStreamChange, + borderRadius: const BorderRadius.all(Radius.circular(8)), + selectedBorderColor: Colors.red[700], + selectedColor: Colors.white, + fillColor: Colors.red[200], + color: Colors.red[400], + constraints: const BoxConstraints( + minHeight: 20.0, + minWidth: 50.0, + ), + isSelected: widget.remoteStream.subStreamButtonState, + children: [Text('Low'), Text('Medium'), Text('High')], + ), + ToggleButtons( + direction: Axis.horizontal, + onPressed: widget.onTemporalStreamChange, + borderRadius: const BorderRadius.all(Radius.circular(8)), + selectedBorderColor: Colors.red[700], + selectedColor: Colors.white, + fillColor: Colors.red[200], + color: Colors.red[400], + constraints: const BoxConstraints( + minHeight: 20.0, + minWidth: 50.0, + ), + isSelected: widget.remoteStream.temporalButtonState, + children: [Text('T0'), Text('T1')], + ) + ]), + top: 10, + start: 10), + visible: widget.remoteStream.publisherId != widget.myId.toString(), + ), + Align( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: Text('${widget.remoteStream.videoRenderer.videoWidth}X${widget.remoteStream.videoRenderer.videoHeight}'), + ), + alignment: Alignment.bottomLeft, + ) + ]), + ); + } +} + class GoogleMeet extends StatefulWidget { @override _VideoRoomState createState() => _VideoRoomState(); @@ -52,11 +131,6 @@ class _VideoRoomState extends State { } } - initLocalMediaRenderer() { - localScreenSharingRenderer = StreamRenderer('localScreenShare'); - localVideoRenderer = StreamRenderer('local'); - } - attachPlugin({bool pop = false}) async { JanusVideoRoomPlugin? videoPlugin = await session?.attach(); videoPlugin?.typedMessages?.listen((event) async { @@ -66,7 +140,7 @@ class _VideoRoomState extends State { if (pop) { Navigator.of(context).pop(joiningDialog); } - (await videoPlugin.configure(bitrate: 3000000, sessionDescription: await videoPlugin.createOffer(audioRecv: false, videoRecv: false))); + (await videoPlugin.configure(bitrate: 0, sessionDescription: await videoPlugin.createOffer(audioRecv: false, videoRecv: false))); } if (data is VideoRoomLeavingEvent) { unSubscribeTo(data.leaving!); @@ -87,15 +161,13 @@ class _VideoRoomState extends State { apiSecret: "janusrocks", isUnifiedPlan: true, iceServers: [RTCIceServer(urls: "stun:stun1.l.google.com:19302", username: "", credential: "")], - loggerLevel: Level.FINE); + loggerLevel: Level.INFO); session = await client?.createSession(); - initLocalMediaRenderer(); } Future unSubscribeTo(int id) async { var feed = videoState.feedIdToDisplayStreamsMap[id]; if (feed == null) return; - videoState.feedIdToDisplayStreamsMap.remove(id.toString()); await videoState.streamsToBeRendered[id]?.dispose(); setState(() { @@ -118,7 +190,7 @@ class _VideoRoomState extends State { JanusEvent event = JanusEvent.fromJson(payload.event); List? streams = event.plugindata?.data['streams']; streams?.forEach((element) { - videoState.subStreamsToFeedIdMap[element['mid']] = element; + videoState.subStreamsToFeedIdMap[element['mid'].toString()] = element; // to avoid duplicate subscriptions if (videoState.feedIdToMidSubscriptionMap[element['feed_id']] == null) videoState.feedIdToMidSubscriptionMap[element['feed_id']] = {}; videoState.feedIdToMidSubscriptionMap[element['feed_id']][element['mid']] = true; @@ -131,7 +203,10 @@ class _VideoRoomState extends State { remotePlugin?.remoteTrack?.listen((event) async { print({'mid': event.mid, 'flowing': event.flowing, 'id': event.track?.id, 'kind': event.track?.kind}); - int? feedId = videoState.subStreamsToFeedIdMap[event.mid]?['feed_id']; + if (event.mid != null && event.flowing != null && event.track != null) { + await manageMuteUIEvents(event.mid!, event.track!.kind!, !event.flowing!); + } + int? feedId = videoState.subStreamsToFeedIdMap[event.mid.toString()]?['feed_id']; String? displayName = videoState.feedIdToDisplayStreamsMap[feedId]?['display']; if (feedId != null) { if (videoState.streamsToBeRendered.containsKey(feedId.toString()) && event.flowing == true && event.track?.kind == "audio") { @@ -143,7 +218,7 @@ class _VideoRoomState extends State { } if (!videoState.streamsToBeRendered.containsKey(feedId.toString()) && event.flowing == true && event.track?.kind == "video") { var localStream = StreamRenderer(feedId.toString()); - await localStream.init(); + await localStream.init(setState); localStream.mediaStream = await createLocalMediaStream(feedId.toString()); localStream.mediaStream?.addTrack(event.track!); localStream.videoRenderer.srcObject = localStream.mediaStream; @@ -166,9 +241,8 @@ class _VideoRoomState extends State { for (var streams in sources) { for (var stream in streams) { // If the publisher is VP8/VP9 and this is an older Safari, let's avoid video - if (stream['disabled'] != null) { + if (stream['disabled'] == true) { print("Disabled stream:"); - // Unsubscribe if (removed == null) removed = []; removed.add({ 'feed': stream['id'], // This is mandatory @@ -178,11 +252,10 @@ class _VideoRoomState extends State { videoState.feedIdToMidSubscriptionMap.remove(stream['id']); continue; } - if (videoState.feedIdToMidSubscriptionMap[stream['id']] != null && videoState.feedIdToMidSubscriptionMap[stream['id']][stream['mid']] == true) { + if (videoState.feedIdToMidSubscriptionMap[stream['id']]?[stream['mid']] == true) { print("Already subscribed to stream, skipping:"); continue; } - // Subscribe if (added == null) added = []; added.add({ @@ -228,9 +301,9 @@ class _VideoRoomState extends State { List mappedStreams = []; for (Map stream in publisher['streams'] ?? []) { if (stream['disabled'] == true) { - manageMuteUIEvents(stream['mid'], stream['type'], true); + await manageMuteUIEvents(stream['mid'], stream['type'], true); } else { - manageMuteUIEvents(stream['mid'], stream['type'], false); + await manageMuteUIEvents(stream['mid'], stream['type'], false); } if (videoState.feedIdToMidSubscriptionMap[publisher['id']] != null && videoState.feedIdToMidSubscriptionMap[publisher['id']]?[stream['mid']] == true) { continue; @@ -248,6 +321,7 @@ class _VideoRoomState extends State { eventMessagesHandler() async { videoPlugin?.messages?.listen((payload) async { JanusEvent event = JanusEvent.fromJson(payload.event); + print(event.plugindata?.data); List? publishers = event.plugindata?.data['publishers']; await attachSubscriberOnPublisherChange(publishers); }); @@ -265,40 +339,47 @@ class _VideoRoomState extends State { audioRecv: false, videoRecv: false, ); - await videoPlugin?.configure(sessionDescription: offer); + await videoPlugin?.configure(sessionDescription: offer, bitrate: 0); }); screenPlugin?.renegotiationNeeded?.listen((event) async { if (screenPlugin?.webRTCHandle?.peerConnection?.signalingState != RTCSignalingState.RTCSignalingStateStable) return; print('retrying to connect publisher'); var offer = await screenPlugin?.createOffer(audioRecv: false, videoRecv: false); - await screenPlugin?.configure(sessionDescription: offer); + await screenPlugin?.configure(bitrate: 0, sessionDescription: offer); }); } joinRoom() async { myId = DateTime.now().millisecondsSinceEpoch; - initLocalMediaRenderer(); + localVideoRenderer = StreamRenderer('local'); videoPlugin = await attachPlugin(pop: true); - eventMessagesHandler(); - await localVideoRenderer.init(); + await eventMessagesHandler(); + await localVideoRenderer.init(setState); localVideoRenderer.mediaStream = await videoPlugin?.initializeMediaDevices(simulcastSendEncodings: [ - RTCRtpEncoding(active: true, rid: 'h',scalabilityMode: 'L1T2',maxBitrate: 2000000,numTemporalLayers: 0, minBitrate: 1000000, ), - RTCRtpEncoding(active: true, rid: 'm',scalabilityMode: 'L1T2', maxBitrate: 1000000,scaleResolutionDownBy: 2), - RTCRtpEncoding(active: true, rid: 'l',scalabilityMode: 'L1T2', maxBitrate: 524288, scaleResolutionDownBy: 2), + RTCRtpEncoding(rid: "h", minBitrate: 2000000, maxBitrate: 2000000, active: true, scalabilityMode: 'L1T2'), + RTCRtpEncoding( + rid: "m", + minBitrate: 1000000, + maxBitrate: 1000000, + active: true, + scalabilityMode: 'L1T2', + scaleResolutionDownBy: 2, + ), + RTCRtpEncoding( + rid: "l", + minBitrate: 512000, + maxBitrate: 512000, + active: true, + scalabilityMode: 'L1T2', + scaleResolutionDownBy: 3, + ), ], mediaConstraints: { - 'video': { - 'width': {'ideal': 1280}, - 'height': {'ideal': 720} - }, + 'video': {'width': 1280, 'height': 720}, 'audio': true }); localVideoRenderer.videoRenderer.srcObject = localVideoRenderer.mediaStream; localVideoRenderer.publisherName = "You"; localVideoRenderer.publisherId = myId.toString(); - localVideoRenderer.videoRenderer.onResize = () { - // to update widthxheight when it renders - setState(() {}); - }; setState(() { videoState.streamsToBeRendered.putIfAbsent('local', () => localVideoRenderer); }); @@ -309,13 +390,13 @@ class _VideoRoomState extends State { setState(() { screenSharing = true; }); - initLocalMediaRenderer(); + localScreenSharingRenderer = StreamRenderer('localScreenShare'); screenPlugin = await session?.attach(); screenPlugin?.typedMessages?.listen((event) async { Object data = event.event.plugindata?.data; if (data is VideoRoomJoinedEvent) { myPvtId = data.privateId; - (await screenPlugin?.configure(bitrate: 3000000, sessionDescription: await screenPlugin?.createOffer(audioRecv: false, videoRecv: false))); + (await screenPlugin?.configure(bitrate: 0, sessionDescription: await screenPlugin?.createOffer(audioRecv: false, videoRecv: false))); } if (data is VideoRoomLeavingEvent) { unSubscribeTo(data.leaving!); @@ -325,10 +406,10 @@ class _VideoRoomState extends State { } screenPlugin?.handleRemoteJsep(event.jsep); }); - await localScreenSharingRenderer.init(); + await localScreenSharingRenderer.init(setState); localScreenSharingRenderer.publisherId = myId.toString(); localScreenSharingRenderer.mediaStream = await screenPlugin?.initializeMediaDevices(mediaConstraints: { - 'video': true, + 'video': {'width': 1920, 'height': 1080}, 'audio': true }, useDisplayMediaDevices: true); localScreenSharingRenderer.videoRenderer.srcObject = localScreenSharingRenderer.mediaStream; @@ -359,7 +440,7 @@ class _VideoRoomState extends State { }); await videoPlugin?.switchCamera(deviceId: await getCameraDeviceId(front)); localVideoRenderer = StreamRenderer('local'); - await localVideoRenderer.init(); + await localVideoRenderer.init(setState); localVideoRenderer.videoRenderer.srcObject = videoPlugin?.webRTCHandle!.localStream; localVideoRenderer.publisherName = "My Camera"; setState(() { @@ -394,6 +475,8 @@ class _VideoRoomState extends State { videoState.feedIdToDisplayStreamsMap.clear(); videoState.subStreamsToFeedIdMap.clear(); videoState.feedIdToMidSubscriptionMap.clear(); + audioEnabled = true; + videoEnabled = true; this.joined = false; this.screenSharing = false; }); @@ -405,6 +488,7 @@ class _VideoRoomState extends State { await screenPlugin?.dispose(); await remotePlugin?.dispose(); remotePlugin = null; + videoPlugin = null; } Future showJoiningDialog() async { @@ -459,6 +543,31 @@ class _VideoRoomState extends State { }); } + onSubStreamChange(StreamRenderer remoteStream, index, updateState) async { + updateState(() { + remoteStream.subStreamButtonState = remoteStream.subStreamButtonState.map((e) => false).toList(); + remoteStream.subStreamButtonState[index] = true; + remoteStream.subStream = ConfigureStreamQuality.values[index]; + // The button that is tapped is set to true, and the others to false. + }); + await remotePlugin?.configure( + streams: [ConfigureStream(mid: remoteStream.mid, substream: remoteStream.subStream)], + ); + updateState(() {}); + } + + onTemporalChange(StreamRenderer remoteStream, index, updateState) async { + updateState(() { + remoteStream.temporalButtonState = remoteStream.temporalButtonState.map((e) => false).toList(); + remoteStream.temporalButtonState[index] = true; + remoteStream.temporal = ConfigureStreamQuality.values[index]; + }); + await remotePlugin?.configure( + streams: [ConfigureStream(mid: remoteStream.mid, temporal: remoteStream.temporal)], + ); + updateState(() {}); + } + @override void dispose() async { super.dispose(); @@ -468,128 +577,103 @@ class _VideoRoomState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - actions: [ - IconButton( - icon: Icon( - joined ? Icons.call_end : Icons.call, - color: joined ? Colors.red : Colors.greenAccent, - ), - onPressed: () async { - if (joined) { - await callEnd(); - return; - } - await this.showJoiningDialog(); - }), - IconButton( - icon: Icon( - !screenSharing ? Icons.screen_share : Icons.stop_screen_share, - color: Colors.green, - ), - onPressed: joined - ? () async { - if (screenSharing) { - await disposeScreenSharing(); - return; + bottomNavigationBar: BottomAppBar( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + icon: Icon( + joined ? Icons.call_end : Icons.call, + color: joined ? Colors.red : Colors.greenAccent, + ), + onPressed: () async { + if (joined) { + await callEnd(); + return; + } + await this.showJoiningDialog(); + }), + IconButton( + icon: Icon( + !screenSharing ? Icons.screen_share : Icons.stop_screen_share, + color: Colors.green, + ), + onPressed: joined + ? () async { + if (screenSharing) { + await disposeScreenSharing(); + return; + } + await screenShare(); } - await screenShare(); - } - : null), - IconButton( - icon: Icon( - audioEnabled ? Icons.mic : Icons.mic_off, - color: Colors.green, - ), - onPressed: joined - ? () async { - setState(() { - audioEnabled = !audioEnabled; - }); - await mute(videoPlugin?.webRTCHandle?.peerConnection, 'audio', audioEnabled); - setState(() { - localVideoRenderer.isAudioMuted = !audioEnabled; - }); - } - : null), - IconButton( - icon: Icon( - videoEnabled ? Icons.videocam : Icons.videocam_off, - color: Colors.green, - ), - onPressed: joined - ? () async { - setState(() { - videoEnabled = !videoEnabled; - }); - await mute(videoPlugin?.webRTCHandle?.peerConnection, 'video', videoEnabled); - } - : null), - IconButton( - icon: Icon( - Icons.switch_camera, - color: Colors.white, - ), - onPressed: joined ? switchCamera : null) - ], - title: const Text('google meet clone'), + : null), + IconButton( + icon: Icon( + audioEnabled ? Icons.mic : Icons.mic_off, + color: Colors.green, + ), + onPressed: joined + ? () async { + setState(() { + audioEnabled = !audioEnabled; + }); + await mute(videoPlugin?.webRTCHandle?.peerConnection, 'audio', audioEnabled); + setState(() { + localVideoRenderer.isAudioMuted = !audioEnabled; + }); + } + : null), + IconButton( + icon: Icon( + videoEnabled ? Icons.videocam : Icons.videocam_off, + color: Colors.green, + ), + onPressed: joined + ? () async { + setState(() { + videoEnabled = !videoEnabled; + }); + await mute(videoPlugin?.webRTCHandle?.peerConnection, 'video', videoEnabled); + } + : null), + IconButton( + icon: Icon( + Icons.switch_camera, + color: Colors.green, + ), + onPressed: joined ? switchCamera : null) + ], + ), + ), + appBar: AppBar( + leadingWidth: 1, + leading: SizedBox(), + actions: [], + title: const Text('google meet'), ), body: GridView.builder( shrinkWrap: true, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), + padding: EdgeInsets.all(10), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 5, + mainAxisSpacing: 5, + ), itemCount: videoState.streamsToBeRendered.entries.length, itemBuilder: (context, index) { - List items = videoState.streamsToBeRendered.entries.map((e) => e.value).toList(); - StreamRenderer remoteStream = items[index]; + List> items = videoState.streamsToBeRendered.entries.map((e) => {'key': e.key, 'value': e.value}).toList(); + StreamRenderer remoteStream = items[index]['value']; return Stack( children: [ - Visibility( - visible: remoteStream.isVideoMuted == false, - replacement: Container( - child: Center( - child: Text("Video Paused By " + remoteStream.publisherName!, style: TextStyle(color: Colors.black)), - ), - ), - child: Stack(children: [ - RTCVideoView( - remoteStream.videoRenderer, - filterQuality: FilterQuality.none, - objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain, - ), - Visibility( - child: PositionedDirectional( - child: ToggleButtons( - direction: Axis.horizontal, - onPressed: (int index) async { - setState(() { - // The button that is tapped is set to true, and the others to false. - for (int i = 0; i < remoteStream.selectedQuality.length; i++) { - remoteStream.selectedQuality[i] = i == index; - } - }); - await remotePlugin?.send(data: {'request': "configure", 'mid': remoteStream.mid, 'substream': index}); - }, - borderRadius: const BorderRadius.all(Radius.circular(8)), - selectedBorderColor: Colors.red[700], - selectedColor: Colors.white, - fillColor: Colors.red[200], - color: Colors.red[400], - constraints: const BoxConstraints( - minHeight: 20.0, - minWidth: 50.0, - ), - isSelected: remoteStream.selectedQuality, - children: [Text('Low'), Text('Medium'), Text('High')], - ), - top: 120, - start: 20), - visible: remoteStream.publisherId != myId.toString(), - ), - Align( - child: Text('${remoteStream.videoRenderer.videoWidth}X${remoteStream.videoRenderer.videoHeight}'), - alignment: Alignment.bottomLeft, - ) - ]), + VideoView( + myId: myId, + onSubStreamChange: (index) async { + await onSubStreamChange(remoteStream, index, setState); + }, + onTemporalStreamChange: (index) async { + await onTemporalChange(remoteStream, index, setState); + }, + remoteStream: remoteStream, ), Align( alignment: AlignmentDirectional.bottomStart, @@ -604,36 +688,45 @@ class _VideoRoomState extends State { fullScreenDialog = await showDialog( context: context, builder: ((context) { - return AlertDialog( - contentPadding: EdgeInsets.all(10), - insetPadding: EdgeInsets.zero, - content: Container( - width: double.maxFinite, - padding: EdgeInsets.zero, - child: Stack( - children: [ - Positioned.fill( - child: Padding( - padding: const EdgeInsets.all(0), - child: RTCVideoView( - remoteStream.videoRenderer, - ), - )), - Align( - alignment: Alignment.topRight, - child: IconButton( - onPressed: () { - Navigator.of(context).pop(fullScreenDialog); + return StatefulBuilder(builder: (context, newSetState) { + return AlertDialog( + contentPadding: EdgeInsets.all(10), + insetPadding: EdgeInsets.zero, + content: Container( + width: double.maxFinite, + padding: EdgeInsets.zero, + child: Stack( + children: [ + Positioned.fill( + child: Padding( + padding: const EdgeInsets.all(0), + child: VideoView( + myId: myId, + onSubStreamChange: (index) async { + await onSubStreamChange(remoteStream, index, newSetState); + }, + onTemporalStreamChange: (index) async { + await onTemporalChange(remoteStream, index, newSetState); }, - icon: Icon( - Icons.close, - color: Colors.white, - )), - ) - ], + remoteStream: remoteStream, + ), + )), + Align( + alignment: Alignment.topRight, + child: IconButton( + onPressed: () { + Navigator.of(context).pop(fullScreenDialog); + }, + icon: Icon( + Icons.close, + color: Colors.white, + )), + ) + ], + ), ), - ), - ); + ); + }); })); }, icon: Icon(Icons.fullscreen)), diff --git a/example/lib/util.dart b/example/lib/util.dart index ae7d0e94..5acb93e2 100644 --- a/example/lib/util.dart +++ b/example/lib/util.dart @@ -9,7 +9,10 @@ class StreamRenderer { String? publisherName; String? mid; bool? isAudioMuted; - List selectedQuality = [false, false, true]; + List subStreamButtonState = [false, false, true]; + List temporalButtonState = [false, true]; + ConfigureStreamQuality subStream = ConfigureStreamQuality.HIGH; + ConfigureStreamQuality temporal = ConfigureStreamQuality.MEDIUM; bool? isVideoMuted; Future dispose() async { @@ -20,10 +23,13 @@ class StreamRenderer { StreamRenderer(this.id); - Future init() async { + Future init(void Function(void Function()) setState) async { mediaStream = await createLocalMediaStream('mediaStream_$id'); isAudioMuted = false; isVideoMuted = false; + videoRenderer.onResize = () { + setState(() {}); + }; await videoRenderer.initialize(); videoRenderer.srcObject = mediaStream; } @@ -43,4 +49,4 @@ class VideoRoomPluginStateManager { subStreamsToFeedIdMap.clear(); feedIdToMidSubscriptionMap.clear(); } -} \ No newline at end of file +} diff --git a/example/pubspec.lock b/example/pubspec.lock index 940d0396..c4c8a555 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.18.0" convert: dependency: transitive description: @@ -243,26 +243,26 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" package_config: dependency: transitive description: @@ -488,26 +488,26 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: @@ -528,10 +528,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.1" typed_data: dependency: transitive description: @@ -564,6 +564,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + url: "https://pub.dev" + source: hosted + version: "0.3.0" web_socket_channel: dependency: transitive description: @@ -605,5 +613,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.0.0 <3.10.6" + dart: ">=3.2.0-194.0.dev <3.10.6" flutter: ">=3.3.0" diff --git a/lib/janus_plugin.dart b/lib/janus_plugin.dart index edc42458..8622ddd9 100644 --- a/lib/janus_plugin.dart +++ b/lib/janus_plugin.dart @@ -341,8 +341,10 @@ class JanusPlugin { _onDataStreamController?.close(); _renegotiationNeededController?.close(); _wsStreamSubscription?.cancel(); - await stopAllTracksAndDispose(webRTCHandle?.localStream); + (await webRTCHandle?.peerConnection?.getTransceivers())?.forEach((element) async { + await element.stop(); + }); await webRTCHandle?.peerConnection?.close(); await webRTCHandle?.remoteStream?.dispose(); await webRTCHandle?.localStream?.dispose(); @@ -450,21 +452,10 @@ class JanusPlugin { if (_context._isUnifiedPlan && !_context._usePlanB) { _context._logger.finest('using unified plan'); webRTCHandle!.localStream!.getTracks().forEach((element) async { - if (element.kind == 'audio') { - _context._logger.finest('adding audio track in peerconnection'); - await webRTCHandle!.peerConnection!.addTrack(element, webRTCHandle!.localStream!); - return; - } - if (simulcastSendEncodings == null) { - _context._logger.finest('adding video track in peerconnection'); - await webRTCHandle?.peerConnection?.addTrack(element, webRTCHandle!.localStream!); - } else { - _context._logger.finest('simulcasting enabled, using TransReceiver with custom sendEncodings'); - await webRTCHandle!.peerConnection!.addTransceiver( - track: element, - kind: RTCRtpMediaType.RTCRtpMediaTypeVideo, - init: RTCRtpTransceiverInit(direction: TransceiverDirection.SendOnly, sendEncodings: simulcastSendEncodings)); - } + await webRTCHandle!.peerConnection!.addTransceiver( + track: element, + kind: element.kind == 'audio' ? RTCRtpMediaType.RTCRtpMediaTypeAudio : RTCRtpMediaType.RTCRtpMediaTypeVideo, + init: RTCRtpTransceiverInit(direction: TransceiverDirection.SendOnly, sendEncodings: element.kind == 'video' ? simulcastSendEncodings : null)); }); } else { _localStreamController!.sink.add(webRTCHandle!.localStream); diff --git a/lib/wrapper_plugins/janus_video_room_plugin.dart b/lib/wrapper_plugins/janus_video_room_plugin.dart index 7b01f31b..db4a5914 100644 --- a/lib/wrapper_plugins/janus_video_room_plugin.dart +++ b/lib/wrapper_plugins/janus_video_room_plugin.dart @@ -1,5 +1,81 @@ part of janus_client; +enum ConfigureStreamQuality { LOW, MEDIUM, HIGH } + +class ConfigureStream { + String? mid; + bool? send; + int? fallback; + int? audio_level_average; + int? audio_active_packets; + int? min_delay; + int? max_delay; + ConfigureStreamQuality? substream; + ConfigureStreamQuality? temporal; + ConfigureStreamQuality? spatial_layer; + ConfigureStreamQuality? temporal_layer; + + ConfigureStream({ + this.mid, + this.substream, + this.temporal, + this.spatial_layer, + this.temporal_layer, + this.send, + this.fallback, + this.audio_level_average, + this.audio_active_packets, + this.min_delay, + this.max_delay, + }); + + Map toMap() { + return { + 'mid': mid, + 'send': send, + 'substream': substream?.index, + 'temporal': temporal?.index, + 'spatial_layer': spatial_layer?.index, + 'temporal_layer': temporal_layer?.index, + 'fallback': fallback, + 'audio_level_average': audio_level_average, + 'audio_active_packets': audio_active_packets, + 'min_delay': min_delay, + 'max_delay': max_delay, + }..removeWhere((key, value) => value == null); + } + + factory ConfigureStream.fromMap(Map map) { + return ConfigureStream( + mid: map['mid'], + substream: map['substream'] != null ? ConfigureStreamQuality.values[map['substream']] : null, + temporal: map['temporal'] != null ? ConfigureStreamQuality.values[map['temporal']] : null, + spatial_layer: map['spatial_layer'] != null ? ConfigureStreamQuality.values[map['spatial_layer']] : null, + temporal_layer: map['temporal_layer'] != null ? ConfigureStreamQuality.values[map['temporal_layer']] : null, + send: map['send'], + fallback: map['fallback']?.toInt(), + audio_level_average: map['audio_level_average']?.toInt(), + audio_active_packets: map['audio_active_packets']?.toInt(), + min_delay: map['min_delay']?.toInt(), + max_delay: map['max_delay']?.toInt(), + ); + } + + String toJson() => json.encode(toMap()); + + factory ConfigureStream.fromJson(String source) => ConfigureStream.fromMap(json.decode(source)); + + @override + String toString() { + return '''ConfigureStream(mid: $mid, + substream: $substream, + temporal: $temporal, + spatial_layer: $spatial_layer, + temporal_layer: $temporal_layer, + send: $send, fallback: $fallback, audio_level_average: $audio_level_average, audio_active_packets: $audio_active_packets, min_delay: $min_delay, max_delay: $max_delay)'''; + } +} + class JanusVideoRoomPlugin extends JanusPlugin { JanusVideoRoomPlugin({handleId, context, transport, session}) : super(context: context, handleId: handleId, plugin: JanusPlugins.VIDEO_ROOM, session: session, transport: transport); @@ -192,7 +268,7 @@ class JanusVideoRoomPlugin extends JanusPlugin { dynamic audioActivePackets, int? audioLevelAverage, List>? descriptions, - List>? streams, + List? streams, bool? restart, RTCSessionDescription? sessionDescription}) async { var payload = { @@ -204,7 +280,7 @@ class JanusVideoRoomPlugin extends JanusPlugin { "display": display, "audio_active_packets": audioActivePackets, "audio_level_average": audioLevelAverage, - "streams": streams, + "streams": streams?.map((e) => e.toMap()).toList(), "restart": restart, "descriptions": descriptions }..removeWhere((key, value) => value == null);