livekit / client-sdk-flutter

Flutter Client SDK for LiveKit
https://docs.livekit.io
Apache License 2.0
270 stars 133 forks source link

[bug] ParticipantWidget.widgetFor Repeated display will cause image lag #644

Open lurongshuang opened 1 day ago

lurongshuang commented 1 day ago

Describe the bug In the floating window function, local or remote images will be repeatedly obtained and given to different parent widgets. When obtaining multiple times, the view will freeze.

[✓] Flutter (Channel stable, 3.24.3, on macOS 15.1.1 24B91 darwin-arm64, locale zh-Hans-CN) [!] Android toolchain - develop for Android devices (Android SDK version 34.0.0) Android Studio (version 2024.1) Xcode - develop for iOS and macOS (Xcode 16.0)

lurongshuang commented 16 hours ago
lurongshuang commented 16 hours ago

ios 16.7.10 Iphone X

lurongshuang commented 16 hours ago

`import 'package:flutter/material.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:livekit_client/livekit_client.dart';

import 'no_video.dart'; import 'participant_info.dart';

abstract class ParticipantWidget extends StatefulWidget { // Convenience method to return relevant widget for participant static ParticipantWidget widgetFor(ParticipantTrack participantTrack, {bool showStatsLayer = false, Widget? noVideoWidget, Key? key}) { if (participantTrack.participant is LocalParticipant) { return LocalParticipantWidget( participantTrack.participant as LocalParticipant, participantTrack.type, showStatsLayer, noVideoWidget: noVideoWidget, key: key); } else if (participantTrack.participant is RemoteParticipant) { return RemoteParticipantWidget( participantTrack.participant as RemoteParticipant, participantTrack.type, showStatsLayer, noVideoWidget: noVideoWidget, key: key); } throw UnimplementedError('Unknown participant type'); }

// Must be implemented by child class abstract final Participant participant; abstract final ParticipantTrackType type; abstract final bool showStatsLayer; final VideoQuality quality; abstract final Widget? noVideoWidget;

const ParticipantWidget({this.quality = VideoQuality.MEDIUM, super.key}); }

class LocalParticipantWidget extends ParticipantWidget { @override final LocalParticipant participant; @override final ParticipantTrackType type; @override final bool showStatsLayer; @override final Widget? noVideoWidget;

const LocalParticipantWidget( this.participant, this.type, this.showStatsLayer, { this.noVideoWidget, super.key, });

@override State createState() => _LocalParticipantWidgetState(); }

class RemoteParticipantWidget extends ParticipantWidget { @override final RemoteParticipant participant; @override final ParticipantTrackType type; @override final bool showStatsLayer;

@override final Widget? noVideoWidget;

const RemoteParticipantWidget( this.participant, this.type, this.showStatsLayer, {this.noVideoWidget, super.key});

@override State createState() => _RemoteParticipantWidgetState(); }

abstract class _ParticipantWidgetState extends State { bool _visible = true;

VideoTrack? get activeVideoTrack;

AudioTrack? get activeAudioTrack;

TrackPublication? get videoPublication;

TrackPublication? get audioPublication;

bool get isScreenShare => widget.type == ParticipantTrackType.kScreenShare; EventsListener? _listener;

@override void initState() { super.initState(); _listener = widget.participant.createListener(); _listener?.on((e) { for (var seg in e.segments) { print('Transcription: ${seg.text} ${seg.isFinal}'); } });

widget.participant.addListener(_onParticipantChanged);
_onParticipantChanged();

}

@override void dispose() { widget.participant.removeListener(_onParticipantChanged); _listener?.dispose(); super.dispose(); }

@override void didUpdateWidget(covariant T oldWidget) { oldWidget.participant.removeListener(_onParticipantChanged); widget.participant.addListener(_onParticipantChanged); _onParticipantChanged(); super.didUpdateWidget(oldWidget); }

// Notify Flutter that UI re-build is required, but we don't set anything here // since the updated values are computed properties. void _onParticipantChanged() => setState(() {});

// Widgets to show above the info bar List extraWidgets(bool isScreenShare) => [];

@override Widget build(BuildContext ctx) { return activeVideoTrack != null && !activeVideoTrack!.muted ? VideoTrackRenderer( renderMode: VideoRenderMode.auto, activeVideoTrack!, fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover) : widget.noVideoWidget ?? const NoVideoWidget(); } // Container( // foregroundDecoration: BoxDecoration( // border: widget.participant.isSpeaking && !isScreenShare // ? Border.all( // width: 5, // color: TCS.darkBackground, // ) // : null, // ), // decoration: BoxDecoration( // color: Theme.of(ctx).cardColor, // ), // child: Stack(children: [ // Video // InkWell( // onTap: () => setState(() => _visible = !_visible), // child: activeVideoTrack != null && !activeVideoTrack!.muted // ? VideoTrackRenderer( // renderMode: VideoRenderMode.auto, // activeVideoTrack!, // fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain) // : const NoVideoWidget()), // Bottom bar // Align( // alignment: Alignment.topRight, // child: Column( // crossAxisAlignment: CrossAxisAlignment.stretch, // mainAxisSize: MainAxisSize.min, // children: [ // ParticipantInfoWidget( // title: widget.participant.name.isNotEmpty // ? '${widget.participant.name} (${widget.participant.identity})' // : widget.participant.identity, // audioAvailable: audioPublication?.muted == false && // audioPublication?.subscribed == true, // connectionQuality: widget.participant.connectionQuality, // isScreenShare: isScreenShare, // enabledE2EE: widget.participant.isEncrypted) // ])), // if (widget.showStatsLayer) // Positioned( // top: 30.r, // left: 5.r, // child: ParticipantStatsWidget(participant: widget.participant)), // ...extraWidgets(isScreenShare) // ])); }

class _LocalParticipantWidgetState extends _ParticipantWidgetState { @override LocalTrackPublication? get videoPublication => widget.participant.videoTrackPublications .where((element) => element.source == widget.type.lkVideoSourceType) .firstOrNull;

@override LocalTrackPublication? get audioPublication => widget.participant.audioTrackPublications .where((element) => element.source == widget.type.lkAudioSourceType) .firstOrNull;

@override VideoTrack? get activeVideoTrack => videoPublication?.track;

@override AudioTrack? get activeAudioTrack => audioPublication?.track; }

class _RemoteParticipantWidgetState extends _ParticipantWidgetState { @override RemoteTrackPublication? get videoPublication => widget.participant.videoTrackPublications .where((element) => element.source == widget.type.lkVideoSourceType) .firstOrNull;

@override RemoteTrackPublication? get audioPublication => widget.participant.audioTrackPublications .where((element) => element.source == widget.type.lkAudioSourceType) .firstOrNull;

@override VideoTrack? get activeVideoTrack => videoPublication?.track;

@override AudioTrack? get activeAudioTrack => audioPublication?.track;

@override List extraWidgets(bool isScreenShare) => [ // Row( // mainAxisSize: MainAxisSize.max, // mainAxisAlignment: MainAxisAlignment.end, // children: [ // Menu for RemoteTrackPublication // if (audioPublication != null) // RemoteTrackPublicationMenuWidget( // pub: audioPublication!, // icon: Icons.volume_up, // ), // // Menu for RemoteTrackPublication // if (videoPublication != null) // RemoteTrackPublicationMenuWidget( // pub: videoPublication!, // icon: isScreenShare ? Icons.monitor : Icons.videocam, // ), // if (videoPublication != null) // RemoteTrackFPSMenuWidget( // pub: videoPublication!, // icon: Icons.menu, // ), // if (videoPublication != null) // RemoteTrackQualityMenuWidget( // pub: videoPublication!, // icon: Icons.monitor_outlined, // ), // ], // ), ]; }

class RemoteTrackPublicationMenuWidget extends StatelessWidget { final IconData icon; final RemoteTrackPublication pub;

const RemoteTrackPublicationMenuWidget({ required this.pub, required this.icon, super.key, });

@override Widget build(BuildContext context) => Material( color: Colors.black.withOpacity(0.3), child: PopupMenuButton( tooltip: 'Subscribe menu', icon: Icon(icon, color: { TrackSubscriptionState.notAllowed: Colors.red, TrackSubscriptionState.unsubscribed: Colors.grey, TrackSubscriptionState.subscribed: Colors.green, }[pub.subscriptionState]), onSelected: (value) => value(), itemBuilder: (BuildContext context) => <PopupMenuEntry>[ // Subscribe/Unsubscribe if (pub.subscribed == false) PopupMenuItem( child: const Text('Subscribe'), value: () => pub.subscribe(), ) else if (pub.subscribed == true) PopupMenuItem( child: const Text('Un-subscribe'), value: () => pub.unsubscribe(), ), ], ), ); }

class RemoteTrackFPSMenuWidget extends StatelessWidget { final IconData icon; final RemoteTrackPublication pub;

const RemoteTrackFPSMenuWidget({ required this.pub, required this.icon, super.key, });

@override Widget build(BuildContext context) => Material( color: Colors.black.withOpacity(0.3), child: PopupMenuButton( tooltip: 'Preferred FPS', icon: Icon(icon, color: Colors.white), onSelected: (value) => value(), itemBuilder: (BuildContext context) => <PopupMenuEntry>[ PopupMenuItem( child: const Text('60'), value: () async { pub.setVideoFPS(60); }), PopupMenuItem( child: const Text('30'), value: () => pub.setVideoFPS(30), ), PopupMenuItem( child: const Text('15'), value: () => pub.setVideoFPS(15), ), PopupMenuItem( child: const Text('8'), value: () => pub.setVideoFPS(8)) ])); }

class RemoteTrackQualityMenuWidget extends StatelessWidget { final IconData icon; final RemoteTrackPublication pub;

const RemoteTrackQualityMenuWidget({ required this.pub, required this.icon, super.key, });

@override Widget build(BuildContext context) => Material( color: Colors.black.withOpacity(0.3), child: PopupMenuButton( tooltip: 'Preferred Quality', icon: Icon(icon, color: Colors.white), onSelected: (value) => value(), itemBuilder: (BuildContext context) => <PopupMenuEntry>[ PopupMenuItem( child: const Text('HIGH'), value: () => pub.setVideoQuality(VideoQuality.HIGH), ), PopupMenuItem( child: const Text('MEDIUM'), value: () => pub.setVideoQuality(VideoQuality.MEDIUM), ), PopupMenuItem( child: const Text('LOW'), value: () => pub.setVideoQuality(VideoQuality.LOW), ), ], ), ); } `

lurongshuang commented 16 hours ago

` Widget getLocalFullScreen(bool isLocal, {Widget? noVideoWidget, Key? key}) { if (isLocal) { if (_room == null || _room!.localParticipant == null) { return noVideoWidget ?? const NoVideoWidget(); } return ParticipantWidget.widgetFor( ParticipantTrack(participant: _room!.localParticipant!), noVideoWidget: noVideoWidget, key: key); }

if (participantTracks.isEmpty) {
  return noVideoWidget ?? const NoVideoWidget();
}

return ParticipantWidget.widgetFor(participantTracks.first,
    noVideoWidget: noVideoWidget, key: key);

}`

lurongshuang commented 16 hours ago

During the switching process, the frame rate kept decreasing, causing the screen to lag