import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:livekit_client/livekit_client.dart'; import 'package:surface/types/account.dart'; import 'package:surface/types/chat.dart'; import 'package:surface/widgets/chat/call/call_no_content.dart'; import 'package:surface/widgets/chat/call/call_participant_info.dart'; import 'package:surface/widgets/chat/call/call_participant_menu.dart'; import 'package:surface/widgets/chat/call/call_participant_stats.dart'; abstract class ParticipantWidget extends StatefulWidget { static ParticipantWidget widgetFor(ParticipantTrack participantTrack, {bool isFixed = false, bool showStatsLayer = false}) { if (participantTrack.participant is LocalParticipant) { return LocalParticipantWidget( participantTrack.participant as LocalParticipant, participantTrack.videoTrack, isFixed, participantTrack.isScreenShare, showStatsLayer, ); } else if (participantTrack.participant is RemoteParticipant) { return RemoteParticipantWidget( participantTrack.participant as RemoteParticipant, participantTrack.videoTrack, isFixed, participantTrack.isScreenShare, showStatsLayer, ); } throw UnimplementedError('Unknown participant type'); } abstract final Participant participant; abstract final VideoTrack? videoTrack; abstract final bool isScreenShare; abstract final bool isFixed; abstract final bool showStatsLayer; final VideoQuality quality; const ParticipantWidget({ super.key, this.quality = VideoQuality.MEDIUM, }); } class LocalParticipantWidget extends ParticipantWidget { @override final LocalParticipant participant; @override final VideoTrack? videoTrack; @override final bool isFixed; @override final bool isScreenShare; @override final bool showStatsLayer; const LocalParticipantWidget( this.participant, this.videoTrack, this.isFixed, this.isScreenShare, this.showStatsLayer, { super.key, }); @override State createState() => _LocalParticipantWidgetState(); } class RemoteParticipantWidget extends ParticipantWidget { @override final RemoteParticipant participant; @override final VideoTrack? videoTrack; @override final bool isFixed; @override final bool isScreenShare; @override final bool showStatsLayer; const RemoteParticipantWidget( this.participant, this.videoTrack, this.isFixed, this.isScreenShare, this.showStatsLayer, { super.key, }); @override State createState() => _RemoteParticipantWidgetState(); } abstract class _ParticipantWidgetState extends State { VideoTrack? get _activeVideoTrack; TrackPublication? get _firstAudioPublication; SnAccount? _userinfoMetadata; @override void initState() { super.initState(); widget.participant.addListener(onParticipantChanged); onParticipantChanged(); } @override void dispose() { widget.participant.removeListener(onParticipantChanged); super.dispose(); } @override void didUpdateWidget(covariant T oldWidget) { oldWidget.participant.removeListener(onParticipantChanged); widget.participant.addListener(onParticipantChanged); onParticipantChanged(); super.didUpdateWidget(oldWidget); } void onParticipantChanged() { setState(() { if (widget.participant.metadata != null) { _userinfoMetadata = SnAccount.fromJson( jsonDecode(widget.participant.metadata!), ); } }); } @override Widget build(BuildContext ctx) { return Stack( children: [ _activeVideoTrack != null && !_activeVideoTrack!.muted ? VideoTrackRenderer( _activeVideoTrack!, fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain, ) : NoContentWidget( userinfo: _userinfoMetadata, isFixed: widget.isFixed, isSpeaking: widget.participant.isSpeaking, ), if (widget.showStatsLayer) Positioned( top: 30, right: 30, child: ParticipantStatsWidget(participant: widget.participant), ), Align( alignment: Alignment.bottomCenter, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ ParticipantInfoWidget( title: widget.participant.name.isNotEmpty ? widget.participant.name : widget.participant.identity, audioAvailable: _firstAudioPublication?.muted == false && _firstAudioPublication?.subscribed == true, connectionQuality: widget.participant.connectionQuality, isScreenShare: widget.isScreenShare, ), ], ), ), ], ); } } class _LocalParticipantWidgetState extends _ParticipantWidgetState { @override LocalTrackPublication? get _firstAudioPublication => widget.participant.audioTrackPublications.firstOrNull; @override VideoTrack? get _activeVideoTrack => widget.videoTrack; } class _RemoteParticipantWidgetState extends _ParticipantWidgetState { @override RemoteTrackPublication? get _firstAudioPublication => widget.participant.audioTrackPublications.firstOrNull; @override VideoTrack? get _activeVideoTrack => widget.videoTrack; } class InteractiveParticipantWidget extends StatelessWidget { final double? width; final double? height; final Color? color; final bool isFixedAvatar; final ParticipantTrack participant; final Function() onTap; const InteractiveParticipantWidget({ super.key, this.width, this.height, this.color, this.isFixedAvatar = false, required this.participant, required this.onTap, }); @override Widget build(BuildContext context) { return GestureDetector( child: Container( width: width, height: height, color: color, child: ParticipantWidget.widgetFor(participant, isFixed: isFixedAvatar), ), onTap: () => onTap(), onLongPress: () { if (participant.participant is LocalParticipant) return; showModalBottomSheet( context: context, builder: (context) => ParticipantMenu( participant: participant.participant as RemoteParticipant, videoTrack: participant.videoTrack, isScreenShare: participant.isScreenShare, ), ); }, ); } }