2024-04-27 16:07:32 +00:00
|
|
|
import 'dart:convert';
|
|
|
|
|
2024-04-27 05:12:26 +00:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
|
|
|
import 'package:livekit_client/livekit_client.dart';
|
2024-04-27 16:07:32 +00:00
|
|
|
import 'package:solian/models/account.dart';
|
2024-04-27 05:12:26 +00:00
|
|
|
import 'package:solian/models/call.dart';
|
2024-04-27 16:07:32 +00:00
|
|
|
import 'package:solian/widgets/chat/call/no_content.dart';
|
2024-04-27 05:12:26 +00:00
|
|
|
import 'package:solian/widgets/chat/call/participant_info.dart';
|
|
|
|
import 'package:solian/widgets/chat/call/participant_stats.dart';
|
|
|
|
|
|
|
|
abstract class ParticipantWidget extends StatefulWidget {
|
|
|
|
static ParticipantWidget widgetFor(ParticipantTrack participantTrack, {bool showStatsLayer = false}) {
|
|
|
|
if (participantTrack.participant is LocalParticipant) {
|
|
|
|
return LocalParticipantWidget(participantTrack.participant as LocalParticipant, participantTrack.videoTrack,
|
|
|
|
participantTrack.isScreenShare, showStatsLayer);
|
|
|
|
} else if (participantTrack.participant is RemoteParticipant) {
|
|
|
|
return RemoteParticipantWidget(participantTrack.participant as RemoteParticipant, participantTrack.videoTrack,
|
|
|
|
participantTrack.isScreenShare, showStatsLayer);
|
|
|
|
}
|
|
|
|
throw UnimplementedError('Unknown participant type');
|
|
|
|
}
|
|
|
|
|
|
|
|
abstract final Participant participant;
|
|
|
|
abstract final VideoTrack? videoTrack;
|
|
|
|
abstract final bool isScreenShare;
|
|
|
|
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 isScreenShare;
|
|
|
|
@override
|
|
|
|
final bool showStatsLayer;
|
|
|
|
|
|
|
|
const LocalParticipantWidget(
|
|
|
|
this.participant,
|
|
|
|
this.videoTrack,
|
|
|
|
this.isScreenShare,
|
|
|
|
this.showStatsLayer, {
|
|
|
|
super.key,
|
|
|
|
});
|
|
|
|
|
|
|
|
@override
|
|
|
|
State<StatefulWidget> createState() => _LocalParticipantWidgetState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class RemoteParticipantWidget extends ParticipantWidget {
|
|
|
|
@override
|
|
|
|
final RemoteParticipant participant;
|
|
|
|
@override
|
|
|
|
final VideoTrack? videoTrack;
|
|
|
|
@override
|
|
|
|
final bool isScreenShare;
|
|
|
|
@override
|
|
|
|
final bool showStatsLayer;
|
|
|
|
|
|
|
|
const RemoteParticipantWidget(
|
|
|
|
this.participant,
|
|
|
|
this.videoTrack,
|
|
|
|
this.isScreenShare,
|
|
|
|
this.showStatsLayer, {
|
|
|
|
super.key,
|
|
|
|
});
|
|
|
|
|
|
|
|
@override
|
|
|
|
State<StatefulWidget> createState() => _RemoteParticipantWidgetState();
|
|
|
|
}
|
|
|
|
|
|
|
|
abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends State<T> {
|
|
|
|
VideoTrack? get _activeVideoTrack;
|
|
|
|
|
|
|
|
TrackPublication? get _videoPublication;
|
|
|
|
|
|
|
|
TrackPublication? get _firstAudioPublication;
|
|
|
|
|
2024-04-27 16:07:32 +00:00
|
|
|
Account? _userinfoMetadata;
|
|
|
|
|
2024-04-27 05:12:26 +00:00
|
|
|
@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);
|
|
|
|
}
|
|
|
|
|
2024-04-27 16:07:32 +00:00
|
|
|
void onParticipantChanged() {
|
|
|
|
setState(() {
|
|
|
|
if (widget.participant.metadata != null) {
|
|
|
|
_userinfoMetadata = Account.fromJson(jsonDecode(widget.participant.metadata!));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2024-04-27 05:12:26 +00:00
|
|
|
|
|
|
|
@override
|
2024-04-27 16:07:32 +00:00
|
|
|
Widget build(BuildContext ctx) {
|
2024-04-27 16:10:20 +00:00
|
|
|
return Stack(
|
|
|
|
children: [
|
2024-04-28 15:05:59 +00:00
|
|
|
_activeVideoTrack != null && !_activeVideoTrack!.muted
|
|
|
|
? VideoTrackRenderer(
|
|
|
|
_activeVideoTrack!,
|
|
|
|
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
|
|
|
)
|
|
|
|
: NoContentWidget(
|
|
|
|
userinfo: _userinfoMetadata,
|
|
|
|
isSpeaking: widget.participant.isSpeaking,
|
|
|
|
),
|
2024-04-27 16:10:20 +00:00
|
|
|
if (widget.showStatsLayer)
|
|
|
|
Positioned(
|
|
|
|
top: 30,
|
|
|
|
right: 30,
|
|
|
|
child: ParticipantStatsWidget(
|
|
|
|
participant: widget.participant,
|
2024-04-27 16:07:32 +00:00
|
|
|
),
|
|
|
|
),
|
2024-04-27 16:10:20 +00:00
|
|
|
Align(
|
|
|
|
alignment: Alignment.bottomCenter,
|
|
|
|
child: Column(
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
children: [
|
|
|
|
ParticipantInfoWidget(
|
|
|
|
title: widget.participant.name.isNotEmpty
|
2024-04-28 15:05:59 +00:00
|
|
|
? widget.participant.name
|
2024-04-27 16:10:20 +00:00
|
|
|
: widget.participant.identity,
|
|
|
|
audioAvailable: _firstAudioPublication?.muted == false && _firstAudioPublication?.subscribed == true,
|
|
|
|
connectionQuality: widget.participant.connectionQuality,
|
|
|
|
isScreenShare: widget.isScreenShare,
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
2024-04-27 16:07:32 +00:00
|
|
|
);
|
|
|
|
}
|
2024-04-27 05:12:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
class _LocalParticipantWidgetState extends _ParticipantWidgetState<LocalParticipantWidget> {
|
|
|
|
@override
|
|
|
|
LocalTrackPublication<LocalVideoTrack>? get _videoPublication =>
|
|
|
|
widget.participant.videoTrackPublications.where((element) => element.sid == widget.videoTrack?.sid).firstOrNull;
|
|
|
|
|
|
|
|
@override
|
|
|
|
LocalTrackPublication<LocalAudioTrack>? get _firstAudioPublication =>
|
|
|
|
widget.participant.audioTrackPublications.firstOrNull;
|
|
|
|
|
|
|
|
@override
|
|
|
|
VideoTrack? get _activeVideoTrack => widget.videoTrack;
|
|
|
|
}
|
|
|
|
|
|
|
|
class _RemoteParticipantWidgetState extends _ParticipantWidgetState<RemoteParticipantWidget> {
|
|
|
|
@override
|
|
|
|
RemoteTrackPublication<RemoteVideoTrack>? get _videoPublication =>
|
|
|
|
widget.participant.videoTrackPublications.where((element) => element.sid == widget.videoTrack?.sid).firstOrNull;
|
|
|
|
|
|
|
|
@override
|
|
|
|
RemoteTrackPublication<RemoteAudioTrack>? get _firstAudioPublication =>
|
|
|
|
widget.participant.audioTrackPublications.firstOrNull;
|
|
|
|
|
|
|
|
@override
|
|
|
|
VideoTrack? get _activeVideoTrack => widget.videoTrack;
|
|
|
|
}
|