2024-04-27 05:12:26 +00:00
|
|
|
import 'dart:async';
|
2024-04-26 17:36:54 +00:00
|
|
|
import 'dart:convert';
|
|
|
|
|
|
|
|
import 'package:flutter/material.dart';
|
2024-04-27 05:12:26 +00:00
|
|
|
import 'package:livekit_client/livekit_client.dart';
|
2024-04-26 17:36:54 +00:00
|
|
|
import 'package:provider/provider.dart';
|
|
|
|
import 'package:solian/models/call.dart';
|
|
|
|
import 'package:solian/providers/auth.dart';
|
|
|
|
import 'package:solian/router.dart';
|
|
|
|
import 'package:solian/utils/service_url.dart';
|
2024-04-28 11:36:06 +00:00
|
|
|
import 'package:solian/widgets/chat/call/controls.dart';
|
2024-04-27 05:12:26 +00:00
|
|
|
import 'package:solian/widgets/chat/call/exts.dart';
|
|
|
|
import 'package:solian/widgets/chat/call/participant.dart';
|
2024-04-28 15:05:59 +00:00
|
|
|
import 'package:solian/widgets/chat/call/participant_menu.dart';
|
2024-04-26 17:36:54 +00:00
|
|
|
import 'package:solian/widgets/indent_wrapper.dart';
|
2024-04-27 05:12:26 +00:00
|
|
|
import 'package:permission_handler/permission_handler.dart';
|
2024-04-26 17:36:54 +00:00
|
|
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
2024-04-27 12:10:15 +00:00
|
|
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
2024-04-27 05:12:26 +00:00
|
|
|
import 'dart:math' as math;
|
|
|
|
|
2024-04-26 17:36:54 +00:00
|
|
|
class ChatCall extends StatefulWidget {
|
|
|
|
final Call call;
|
|
|
|
|
|
|
|
const ChatCall({super.key, required this.call});
|
|
|
|
|
|
|
|
@override
|
|
|
|
State<ChatCall> createState() => _ChatCallState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _ChatCallState extends State<ChatCall> {
|
|
|
|
String? _token;
|
2024-04-27 05:12:26 +00:00
|
|
|
String? _endpoint;
|
|
|
|
|
|
|
|
bool _isMounted = false;
|
|
|
|
|
|
|
|
StreamSubscription? _subscription;
|
|
|
|
List<MediaDevice> _audioInputs = [];
|
|
|
|
List<MediaDevice> _videoInputs = [];
|
|
|
|
|
|
|
|
bool _enableAudio = true;
|
|
|
|
bool _enableVideo = false;
|
|
|
|
LocalAudioTrack? _audioTrack;
|
|
|
|
LocalVideoTrack? _videoTrack;
|
|
|
|
MediaDevice? _videoDevice;
|
|
|
|
MediaDevice? _audioDevice;
|
|
|
|
|
|
|
|
final VideoParameters _videoParameters = VideoParametersPresets.h720_169;
|
|
|
|
|
|
|
|
late Room _callRoom;
|
|
|
|
late EventsListener<RoomEvent> _callListener;
|
|
|
|
|
|
|
|
List<ParticipantTrack> _participantTracks = [];
|
2024-04-28 15:05:59 +00:00
|
|
|
ParticipantTrack? _focusParticipant;
|
2024-04-27 05:12:26 +00:00
|
|
|
|
|
|
|
Future<void> checkPermissions() async {
|
2024-04-28 15:05:59 +00:00
|
|
|
if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) return;
|
2024-04-28 13:49:03 +00:00
|
|
|
|
2024-04-27 05:12:26 +00:00
|
|
|
await Permission.camera.request();
|
|
|
|
await Permission.microphone.request();
|
|
|
|
await Permission.bluetooth.request();
|
|
|
|
await Permission.bluetoothConnect.request();
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<(String, String)> exchangeToken() async {
|
|
|
|
await checkPermissions();
|
2024-04-26 17:36:54 +00:00
|
|
|
|
|
|
|
final auth = context.read<AuthProvider>();
|
|
|
|
if (!await auth.isAuthorized()) {
|
|
|
|
router.pop();
|
|
|
|
throw Error();
|
|
|
|
}
|
|
|
|
|
|
|
|
var uri = getRequestUri('messaging', '/api/channels/${widget.call.channel.alias}/calls/ongoing/token');
|
|
|
|
|
|
|
|
var res = await auth.client!.post(uri);
|
|
|
|
if (res.statusCode == 200) {
|
|
|
|
final result = jsonDecode(utf8.decode(res.bodyBytes));
|
|
|
|
_token = result['token'];
|
2024-04-27 05:12:26 +00:00
|
|
|
_endpoint = 'wss://${result['endpoint']}';
|
|
|
|
joinRoom(_endpoint!, _token!);
|
|
|
|
return (_token!, _endpoint!);
|
2024-04-26 17:36:54 +00:00
|
|
|
} else {
|
|
|
|
var message = utf8.decode(res.bodyBytes);
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
|
|
SnackBar(content: Text("Something went wrong... $message")),
|
|
|
|
);
|
|
|
|
throw Exception(message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-27 05:12:26 +00:00
|
|
|
void joinRoom(String url, String token) async {
|
2024-04-27 12:10:15 +00:00
|
|
|
if (_isMounted) {
|
2024-04-27 05:12:26 +00:00
|
|
|
return;
|
|
|
|
} else {
|
|
|
|
_isMounted = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
ScaffoldMessenger.of(context).clearSnackBars();
|
|
|
|
|
|
|
|
final notify = ScaffoldMessenger.of(context).showSnackBar(
|
|
|
|
SnackBar(
|
|
|
|
content: Text(AppLocalizations.of(context)!.connectingServer),
|
|
|
|
duration: const Duration(minutes: 1),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
|
|
|
try {
|
|
|
|
await _callRoom.connect(
|
|
|
|
url,
|
|
|
|
token,
|
|
|
|
roomOptions: RoomOptions(
|
|
|
|
dynacast: true,
|
|
|
|
adaptiveStream: true,
|
|
|
|
defaultAudioPublishOptions: const AudioPublishOptions(
|
|
|
|
name: 'call_voice',
|
|
|
|
stream: 'call_stream',
|
|
|
|
),
|
|
|
|
defaultVideoPublishOptions: const VideoPublishOptions(
|
|
|
|
name: 'call_video',
|
|
|
|
stream: 'call_stream',
|
|
|
|
simulcast: true,
|
|
|
|
backupVideoCodec: BackupVideoCodec(enabled: true),
|
|
|
|
),
|
|
|
|
defaultScreenShareCaptureOptions: const ScreenShareCaptureOptions(
|
|
|
|
useiOSBroadcastExtension: true,
|
|
|
|
params: VideoParameters(
|
|
|
|
dimensions: VideoDimensionsPresets.h1080_169,
|
|
|
|
encoding: VideoEncoding(maxBitrate: 3 * 1000 * 1000, maxFramerate: 30),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
defaultCameraCaptureOptions: CameraCaptureOptions(maxFrameRate: 30, params: _videoParameters),
|
|
|
|
),
|
|
|
|
fastConnectOptions: FastConnectOptions(
|
|
|
|
microphone: TrackOption(track: _audioTrack),
|
|
|
|
camera: TrackOption(track: _videoTrack),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
|
|
|
setupRoom();
|
|
|
|
} catch (e) {
|
|
|
|
final message = e.toString();
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
|
|
SnackBar(content: Text("Something went wrong... $message")),
|
|
|
|
);
|
|
|
|
} finally {
|
|
|
|
notify.close();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-27 12:10:15 +00:00
|
|
|
void autoPublish() async {
|
2024-04-27 05:12:26 +00:00
|
|
|
try {
|
2024-04-27 16:21:16 +00:00
|
|
|
if (_enableVideo) await _callRoom.localParticipant?.setCameraEnabled(true);
|
2024-04-27 05:12:26 +00:00
|
|
|
} catch (error) {
|
|
|
|
await context.showErrorDialog(error);
|
|
|
|
}
|
|
|
|
try {
|
2024-04-27 16:21:16 +00:00
|
|
|
if (_enableAudio) await _callRoom.localParticipant?.setMicrophoneEnabled(true);
|
2024-04-27 05:12:26 +00:00
|
|
|
} catch (error) {
|
|
|
|
await context.showErrorDialog(error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void setupRoom() {
|
|
|
|
_callRoom.addListener(onRoomDidUpdate);
|
|
|
|
setupRoomListeners();
|
|
|
|
sortParticipants();
|
2024-04-27 12:10:15 +00:00
|
|
|
WidgetsBindingCompatible.instance?.addPostFrameCallback((_) => autoPublish());
|
2024-04-27 05:12:26 +00:00
|
|
|
|
|
|
|
if (lkPlatformIsMobile()) {
|
|
|
|
Hardware.instance.setSpeakerphoneOn(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void setupRoomListeners() {
|
|
|
|
_callListener
|
|
|
|
..on<RoomDisconnectedEvent>((event) async {
|
|
|
|
if (event.reason != null) {
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
|
|
|
content: Text('Call disconnected... ${event.reason}'),
|
|
|
|
));
|
|
|
|
}
|
|
|
|
if (router.canPop()) router.pop();
|
|
|
|
})
|
|
|
|
..on<ParticipantEvent>((event) => sortParticipants())
|
|
|
|
..on<RoomRecordingStatusChanged>((event) {
|
|
|
|
context.showRecordingStatusChangedDialog(event.activeRecording);
|
|
|
|
})
|
|
|
|
..on<LocalTrackPublishedEvent>((_) => sortParticipants())
|
|
|
|
..on<LocalTrackUnpublishedEvent>((_) => sortParticipants())
|
|
|
|
..on<TrackSubscribedEvent>((_) => sortParticipants())
|
|
|
|
..on<TrackUnsubscribedEvent>((_) => sortParticipants())
|
|
|
|
..on<ParticipantNameUpdatedEvent>((event) {
|
|
|
|
sortParticipants();
|
|
|
|
})
|
|
|
|
..on<AudioPlaybackStatusChanged>((event) async {
|
|
|
|
if (!_callRoom.canPlaybackAudio) {
|
|
|
|
bool? yesno = await context.showPlayAudioManuallyDialog();
|
|
|
|
if (yesno == true) {
|
|
|
|
await _callRoom.startAudio();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void sortParticipants() {
|
|
|
|
List<ParticipantTrack> screenTracks = [];
|
2024-04-28 11:36:06 +00:00
|
|
|
Map<String, ParticipantTrack> userMediaTracks = {};
|
2024-04-27 05:12:26 +00:00
|
|
|
for (var participant in _callRoom.remoteParticipants.values) {
|
2024-04-28 11:36:06 +00:00
|
|
|
userMediaTracks[participant.sid] = ParticipantTrack(
|
|
|
|
participant: participant,
|
|
|
|
videoTrack: null,
|
|
|
|
isScreenShare: false,
|
|
|
|
);
|
|
|
|
|
|
|
|
for (var t in participant.videoTrackPublications) {
|
2024-04-27 05:12:26 +00:00
|
|
|
if (t.isScreenShare) {
|
|
|
|
screenTracks.add(ParticipantTrack(
|
|
|
|
participant: participant,
|
2024-04-27 16:07:32 +00:00
|
|
|
videoTrack: t.track as VideoTrack,
|
2024-04-27 05:12:26 +00:00
|
|
|
isScreenShare: true,
|
|
|
|
));
|
|
|
|
} else {
|
2024-04-28 11:36:06 +00:00
|
|
|
userMediaTracks[participant.sid]?.videoTrack = t.track;
|
2024-04-27 05:12:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-04-28 11:36:06 +00:00
|
|
|
|
|
|
|
final userMediaTrackList = userMediaTracks.values.toList();
|
|
|
|
userMediaTrackList.sort((a, b) {
|
2024-04-27 05:12:26 +00:00
|
|
|
// Loudest people first
|
|
|
|
if (a.participant.isSpeaking && b.participant.isSpeaking) {
|
|
|
|
if (a.participant.audioLevel > b.participant.audioLevel) {
|
|
|
|
return -1;
|
|
|
|
} else {
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Last spoke first
|
|
|
|
final aSpokeAt = a.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
|
|
|
|
final bSpokeAt = b.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
|
|
|
|
|
|
|
|
if (aSpokeAt != bSpokeAt) {
|
|
|
|
return aSpokeAt > bSpokeAt ? -1 : 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Has video first
|
|
|
|
if (a.participant.hasVideo != b.participant.hasVideo) {
|
|
|
|
return a.participant.hasVideo ? -1 : 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// First joined people first
|
|
|
|
return a.participant.joinedAt.millisecondsSinceEpoch - b.participant.joinedAt.millisecondsSinceEpoch;
|
|
|
|
});
|
|
|
|
|
2024-04-28 11:36:06 +00:00
|
|
|
ParticipantTrack localTrack = ParticipantTrack(
|
|
|
|
participant: _callRoom.localParticipant!,
|
|
|
|
videoTrack: null,
|
|
|
|
isScreenShare: false,
|
|
|
|
);
|
|
|
|
if (_callRoom.localParticipant != null) {
|
|
|
|
final localParticipantTracks = _callRoom.localParticipant?.videoTrackPublications;
|
|
|
|
if (localParticipantTracks != null) {
|
|
|
|
for (var t in localParticipantTracks) {
|
|
|
|
if (t.isScreenShare) {
|
|
|
|
screenTracks.add(ParticipantTrack(
|
|
|
|
participant: _callRoom.localParticipant!,
|
|
|
|
videoTrack: t.track as VideoTrack,
|
|
|
|
isScreenShare: true,
|
|
|
|
));
|
|
|
|
} else {
|
|
|
|
localTrack.videoTrack = t.track;
|
|
|
|
}
|
2024-04-27 05:12:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-04-27 16:21:16 +00:00
|
|
|
|
2024-04-27 05:12:26 +00:00
|
|
|
setState(() {
|
2024-04-28 11:36:06 +00:00
|
|
|
_participantTracks = [...screenTracks, localTrack, ...userMediaTrackList];
|
2024-04-28 15:05:59 +00:00
|
|
|
_focusParticipant ??= _participantTracks.first;
|
2024-04-27 05:12:26 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void onRoomDidUpdate() => sortParticipants();
|
|
|
|
|
|
|
|
void revertDevices(List<MediaDevice> devices) async {
|
|
|
|
_audioInputs = devices.where((d) => d.kind == 'audioinput').toList();
|
|
|
|
_videoInputs = devices.where((d) => d.kind == 'videoinput').toList();
|
|
|
|
|
|
|
|
if (_audioInputs.isNotEmpty) {
|
|
|
|
if (_audioDevice == null && _enableAudio) {
|
|
|
|
_audioDevice = _audioInputs.first;
|
|
|
|
Future.delayed(const Duration(milliseconds: 100), () async {
|
|
|
|
await changeLocalAudioTrack();
|
|
|
|
setState(() {});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_videoInputs.isNotEmpty) {
|
|
|
|
if (_videoDevice == null && _enableVideo) {
|
|
|
|
_videoDevice = _videoInputs.first;
|
|
|
|
Future.delayed(const Duration(milliseconds: 100), () async {
|
|
|
|
await changeLocalVideoTrack();
|
|
|
|
setState(() {});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setState(() {});
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> setEnableVideo(value) async {
|
|
|
|
_enableVideo = value;
|
|
|
|
if (!_enableVideo) {
|
|
|
|
await _videoTrack?.stop();
|
|
|
|
_videoTrack = null;
|
|
|
|
} else {
|
|
|
|
await changeLocalVideoTrack();
|
|
|
|
}
|
|
|
|
setState(() {});
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> setEnableAudio(value) async {
|
|
|
|
_enableAudio = value;
|
|
|
|
if (!_enableAudio) {
|
|
|
|
await _audioTrack?.stop();
|
|
|
|
_audioTrack = null;
|
|
|
|
} else {
|
|
|
|
await changeLocalAudioTrack();
|
|
|
|
}
|
|
|
|
setState(() {});
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> changeLocalAudioTrack() async {
|
|
|
|
if (_audioTrack != null) {
|
|
|
|
await _audioTrack!.stop();
|
|
|
|
_audioTrack = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_audioDevice != null) {
|
|
|
|
_audioTrack = await LocalAudioTrack.create(AudioCaptureOptions(
|
|
|
|
deviceId: _audioDevice!.deviceId,
|
|
|
|
));
|
|
|
|
await _audioTrack!.start();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> changeLocalVideoTrack() async {
|
|
|
|
if (_videoTrack != null) {
|
|
|
|
await _videoTrack!.stop();
|
|
|
|
_videoTrack = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_videoDevice != null) {
|
|
|
|
_videoTrack = await LocalVideoTrack.createCameraTrack(CameraCaptureOptions(
|
|
|
|
deviceId: _videoDevice!.deviceId,
|
|
|
|
params: _videoParameters,
|
|
|
|
));
|
|
|
|
await _videoTrack!.start();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
super.initState();
|
|
|
|
_subscription = Hardware.instance.onDeviceChange.stream.listen(revertDevices);
|
|
|
|
_callRoom = Room();
|
|
|
|
_callListener = _callRoom.createListener();
|
|
|
|
Hardware.instance.enumerateDevices().then(revertDevices);
|
2024-04-27 12:10:15 +00:00
|
|
|
WakelockPlus.enable();
|
2024-04-27 05:12:26 +00:00
|
|
|
}
|
|
|
|
|
2024-04-26 17:36:54 +00:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return IndentWrapper(
|
|
|
|
title: AppLocalizations.of(context)!.chatCall,
|
|
|
|
hideDrawer: true,
|
|
|
|
child: FutureBuilder(
|
|
|
|
future: exchangeToken(),
|
|
|
|
builder: (context, snapshot) {
|
|
|
|
if (!snapshot.hasData || snapshot.data == null) {
|
|
|
|
return const Center(child: CircularProgressIndicator());
|
|
|
|
}
|
|
|
|
|
2024-04-27 05:12:26 +00:00
|
|
|
return Stack(
|
|
|
|
children: [
|
|
|
|
Column(
|
|
|
|
children: [
|
|
|
|
Expanded(
|
2024-04-27 16:07:32 +00:00
|
|
|
child: Container(
|
|
|
|
color: Theme.of(context).colorScheme.surfaceVariant,
|
2024-04-28 15:05:59 +00:00
|
|
|
child: _focusParticipant != null
|
|
|
|
? InteractiveParticipantWidget(
|
|
|
|
participant: _focusParticipant!,
|
|
|
|
onTap: () {},
|
|
|
|
)
|
2024-04-27 16:07:32 +00:00
|
|
|
: Container(),
|
|
|
|
),
|
2024-04-27 05:12:26 +00:00
|
|
|
),
|
2024-04-27 16:07:32 +00:00
|
|
|
if (_callRoom.localParticipant != null) ControlsWidget(_callRoom, _callRoom.localParticipant!),
|
2024-04-27 05:12:26 +00:00
|
|
|
],
|
|
|
|
),
|
|
|
|
Positioned(
|
|
|
|
left: 0,
|
|
|
|
right: 0,
|
|
|
|
top: 0,
|
|
|
|
child: SizedBox(
|
2024-04-28 15:05:59 +00:00
|
|
|
height: 128,
|
2024-04-27 05:12:26 +00:00
|
|
|
child: ListView.builder(
|
|
|
|
scrollDirection: Axis.horizontal,
|
2024-04-28 15:05:59 +00:00
|
|
|
itemCount: math.max(0, _participantTracks.length),
|
|
|
|
itemBuilder: (BuildContext context, int index) {
|
|
|
|
final track = _participantTracks[index];
|
|
|
|
if (track.participant.sid == _focusParticipant?.participant.sid) {
|
|
|
|
return Container();
|
|
|
|
}
|
|
|
|
|
|
|
|
return Padding(
|
|
|
|
padding: const EdgeInsets.only(top: 8, left: 8),
|
|
|
|
child: ClipRRect(
|
|
|
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
|
|
child: InteractiveParticipantWidget(
|
|
|
|
width: 120,
|
|
|
|
height: 120,
|
|
|
|
color: Theme.of(context).cardColor,
|
|
|
|
participant: track,
|
|
|
|
onTap: () {
|
|
|
|
if (track.participant.sid != _focusParticipant?.participant.sid) {
|
|
|
|
setState(() => _focusParticipant = track);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
),
|
2024-04-27 16:21:16 +00:00
|
|
|
),
|
2024-04-28 15:05:59 +00:00
|
|
|
);
|
|
|
|
},
|
2024-04-27 05:12:26 +00:00
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
2024-04-26 17:36:54 +00:00
|
|
|
},
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2024-04-27 05:12:26 +00:00
|
|
|
|
|
|
|
@override
|
|
|
|
void deactivate() {
|
|
|
|
_subscription?.cancel();
|
|
|
|
super.deactivate();
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void dispose() {
|
2024-04-27 12:10:15 +00:00
|
|
|
WakelockPlus.disable();
|
2024-04-27 05:12:26 +00:00
|
|
|
(() async {
|
|
|
|
_callRoom.removeListener(onRoomDidUpdate);
|
|
|
|
await _callListener.dispose();
|
2024-04-27 16:07:32 +00:00
|
|
|
await _callRoom.disconnect();
|
2024-04-27 05:12:26 +00:00
|
|
|
await _callRoom.dispose();
|
|
|
|
})();
|
|
|
|
super.dispose();
|
|
|
|
}
|
2024-04-26 17:36:54 +00:00
|
|
|
}
|
2024-04-28 15:05:59 +00:00
|
|
|
|
|
|
|
class InteractiveParticipantWidget extends StatelessWidget {
|
|
|
|
final double? width;
|
|
|
|
final double? height;
|
|
|
|
final Color? color;
|
|
|
|
final ParticipantTrack participant;
|
|
|
|
final Function() onTap;
|
|
|
|
|
|
|
|
const InteractiveParticipantWidget({
|
|
|
|
super.key,
|
|
|
|
this.width,
|
|
|
|
this.height,
|
|
|
|
this.color,
|
|
|
|
required this.participant,
|
|
|
|
required this.onTap,
|
|
|
|
});
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return InkWell(
|
|
|
|
child: Container(
|
|
|
|
width: width,
|
|
|
|
height: height,
|
|
|
|
color: color,
|
|
|
|
child: ParticipantWidget.widgetFor(participant),
|
|
|
|
),
|
|
|
|
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,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|