diff --git a/test_app/lib/screens/participant_screen/conference_controls.dart b/test_app/lib/screens/participant_screen/conference_controls.dart index 721d0548..8a7bd5cc 100644 --- a/test_app/lib/screens/participant_screen/conference_controls.dart +++ b/test_app/lib/screens/participant_screen/conference_controls.dart @@ -1,4 +1,5 @@ import 'package:dolbyio_comms_sdk_flutter_example/conference_ext.dart'; +import 'package:dolbyio_comms_sdk_flutter_example/state_management/models/conference_model.dart'; import 'package:dolbyio_comms_sdk_flutter_example/widgets/bottom_tool_bar.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; @@ -6,6 +7,7 @@ import 'package:dolbyio_comms_sdk_flutter/dolbyio_comms_sdk_flutter.dart'; import 'package:provider/provider.dart'; import '../../widgets/dialogs.dart'; import '../../widgets/spatial_extensions/spatial_values_model.dart'; +import '../../widgets/text_form_field.dart'; import '/widgets/conference_action_icon_button.dart'; import 'dart:developer' as developer; @@ -27,10 +29,17 @@ class _ConferenceControlsState extends State { bool isMicOff = false; bool isVideoOff = false; bool isScreenShareOff = true; + bool isDialogCanceled = false; FileConverted? _fileConverted; + TextEditingController urlTextController = TextEditingController(); + final formKey = GlobalKey(); + final testVideoUrl = + 'https://storage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4'; @override Widget build(BuildContext context) { + urlTextController.text = testVideoUrl; + return BottomToolBar(children: [ ConferenceActionIconButton( onPressedIcon: () { @@ -81,6 +90,10 @@ class _ConferenceControlsState extends State { value: 1, child: Text('Share file'), ), + const PopupMenuItem( + value: 2, + child: Text('Share video'), + ), ]; }, onSelected: (value) async { @@ -98,6 +111,9 @@ class _ConferenceControlsState extends State { startFilePresentation(); } break; + case 2: + startVideoPresentation(); + break; } }, ), @@ -154,6 +170,52 @@ class _ConferenceControlsState extends State { ); } + Future enterUrlDialog(BuildContext context) async { + return showDialog( + barrierDismissible: false, + context: context, + builder: (context) { + return AlertDialog( + title: Column( + children: const [ + Text('Enter url'), + Text( + 'Should start with https://', + style: TextStyle(color: Colors.black38, fontSize: 12), + ), + ], + ), + content: Form( + key: formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: InputTextFormField( + labelText: 'Url', + controller: urlTextController, + focusColor: Colors.deepPurple, + ), + ), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + isDialogCanceled = true; + Navigator.pop(context); + }, + ), + TextButton( + child: const Text('Done'), + onPressed: () { + final isValidForm = formKey.currentState!.validate(); + if (isValidForm) { + Navigator.pop(context); + } + }, + ), + ], + ); + }); + } + Future showResultDialog( BuildContext context, String title, String text) async { await ViewDialogs.dialog( @@ -275,6 +337,30 @@ class _ConferenceControlsState extends State { } } + Future startVideoPresentation() async { + try { + var isSomeonePresentingVideo = + Provider.of(context, listen: false) + .isSomeonePresentingVideo; + if (isSomeonePresentingVideo) { + if (!mounted) return; + showResultDialog( + context, 'Error', 'Someone is already sharing the video'); + } else { + await enterUrlDialog(context); + if (!isDialogCanceled) { + _dolbyioCommsSdkFlutterPlugin.videoPresentation + .start(urlTextController.text); + } + } + } catch (error) { + if (!mounted) return; + showResultDialog(context, 'Error', error.toString()); + } finally { + isDialogCanceled = false; + } + } + Future isSomeoneScreenSharing() async { final conference = await _dolbyioCommsSdkFlutterPlugin.conference.current(); final participants = await _dolbyioCommsSdkFlutterPlugin.conference diff --git a/test_app/lib/screens/participant_screen/participant_screen.dart b/test_app/lib/screens/participant_screen/participant_screen.dart index bdaebcb4..af29b8e3 100644 --- a/test_app/lib/screens/participant_screen/participant_screen.dart +++ b/test_app/lib/screens/participant_screen/participant_screen.dart @@ -1,11 +1,13 @@ import 'package:dolbyio_comms_sdk_flutter_example/state_management/models/conference_model.dart'; import 'package:dolbyio_comms_sdk_flutter_example/widgets/file_presentation_ui.dart'; import 'package:dolbyio_comms_sdk_flutter_example/widgets/spatial_extensions/participant_spatial_values.dart'; +import 'package:dolbyio_comms_sdk_flutter_example/widgets/video_presentation_container.dart'; import 'package:provider/provider.dart'; import '../../conference_ext.dart'; import '../../widgets/file_container.dart'; import '../../widgets/spatial_extensions/spatial_values_model.dart'; import '../../widgets/status_snackbar.dart'; +import '../../widgets/video_presentation_buttons.dart'; import '../test_buttons/test_buttons.dart'; import 'conference_controls.dart'; import 'conference_title.dart'; @@ -15,6 +17,7 @@ import 'package:dolbyio_comms_sdk_flutter/dolbyio_comms_sdk_flutter.dart'; import 'participant_grid.dart'; import '/widgets/dolby_title.dart'; import '/widgets/modal_bottom_sheet.dart'; +import 'package:video_player/video_player.dart'; class ParticipantScreen extends StatefulWidget { final bool isSpatialAudio; @@ -83,12 +86,22 @@ class _ParticipantScreenContentState extends State { Event>? _onFilePresentationChangeSubscription; + StreamSubscription>? + _onVideoPresentationChangeSubscription; + + StreamSubscription>? + _onVideoPresentationStopSubscription; + Participant? _localParticipant; bool shouldCloseSessionOnLeave = false; List participants = []; bool _isScreenSharing = false; bool isFilePresenting = false; bool isLocalPresentingFile = false; + bool isLocalPresentingVideo = false; + bool isVideoStarted = false; + late VideoPlayerController? _videoPlayerController; + late Widget videoPlayerWidget; @override void initState() { @@ -164,6 +177,51 @@ class _ParticipantScreenContentState extends State { }); } }); + + _onVideoPresentationChangeSubscription = _dolbyioCommsSdkFlutterPlugin + .videoPresentation + .onVideoPresentationChange() + .listen((event) async { + if (event.type == VideoPresentationEventNames.videoPresentationStarted) { + String url = event.body.url; + await initializeVideoPlayer(url); + createVideoPlayerWidget(); + checkIfLocalPresentingVideo(event); + + if (!mounted) return; + Provider.of(context, listen: false) + .isSomeonePresentingVideo = true; + _videoPlayerController?.play(); + setState(() => isVideoStarted = true); + } else if (event.type == + VideoPresentationEventNames.videoPresentationPlayed) { + _videoPlayerController?.play(); + } else if (event.type == + VideoPresentationEventNames.videoPresentationPaused) { + _videoPlayerController?.pause(); + } else if (event.type == + VideoPresentationEventNames.videoPresentationSought) { + _videoPlayerController?.seekTo(Duration.zero); + _videoPlayerController?.play(); + } + }); + + _onVideoPresentationStopSubscription = _dolbyioCommsSdkFlutterPlugin + .videoPresentation + .onVideoPresentationStopped() + .listen((event) { + if (!mounted) { + _onVideoPresentationStopSubscription?.cancel(); + } else { + setState(() { + isLocalPresentingVideo = false; + isVideoStarted = false; + Provider.of(context, listen: false) + .isSomeonePresentingVideo = isVideoStarted; + _videoPlayerController?.pause(); + }); + } + }); } @override @@ -175,6 +233,8 @@ class _ParticipantScreenContentState extends State { _onPermissionsChangeSubsription?.cancel(); _onRecordingChangeSubscription?.cancel(); _onFilePresentationChangeSubscription?.cancel(); + _onVideoPresentationChangeSubscription?.cancel(); + _onVideoPresentationStopSubscription?.cancel(); super.deactivate(); } @@ -212,6 +272,21 @@ class _ParticipantScreenContentState extends State { child: FileContainer(), ) : const SizedBox.shrink(), + isVideoStarted + ? _videoPlayerController != null + ? Column( + children: [ + VideoPresentationContainer( + videoPlayerController: _videoPlayerController!, + videoPlayerWidget: videoPlayerWidget, + ), + isLocalPresentingVideo + ? const VideoPresentationButtons() + : const SizedBox.shrink() + ], + ) + : const SizedBox.shrink() + : const SizedBox.shrink(), Expanded( child: Stack( children: [ @@ -237,6 +312,27 @@ class _ParticipantScreenContentState extends State { ); } + Future initializeVideoPlayer(String url) async { + _videoPlayerController = VideoPlayerController.network(url); + await Future.wait([ + _videoPlayerController!.initialize(), + ]); + } + + void createVideoPlayerWidget() { + if (_videoPlayerController != null) { + videoPlayerWidget = VideoPlayer(_videoPlayerController!); + } + } + + void checkIfLocalPresentingVideo(Event event) async { + var local = + await _dolbyioCommsSdkFlutterPlugin.conference.getLocalParticipant(); + if (local.id == event.body.owner.id) { + setState(() => isLocalPresentingVideo = true); + } + } + Future setDefaultSpatialPosition() async { if (widget.isSpatialAudio) { final currentConference = await getCurrentConference(); diff --git a/test_app/lib/screens/test_buttons/test_buttons.dart b/test_app/lib/screens/test_buttons/test_buttons.dart index 5405917f..02aa90d4 100644 --- a/test_app/lib/screens/test_buttons/test_buttons.dart +++ b/test_app/lib/screens/test_buttons/test_buttons.dart @@ -6,7 +6,6 @@ import 'conference_service_test_buttons.dart'; import 'media_device_service_test_buttons.dart'; import 'notification_service_test_buttons.dart'; import 'recording_service_test_buttons.dart'; -import 'video_presentation_service_test_buttons.dart'; class TestButtons extends StatelessWidget { const TestButtons({Key? key}) : super(key: key); @@ -43,10 +42,6 @@ class TestButtons extends StatelessWidget { SizedBox(height: 10), CommandServiceTestButtons(), SizedBox(height: 10), - Text("Video presentation service"), - SizedBox(height: 10), - VideoPresentationServiceTestButtons(), - SizedBox(height: 10), Text("Notification service"), SizedBox(height: 10), NotificationServiceTestButtons(), diff --git a/test_app/lib/screens/test_buttons/video_presentation_service_test_buttons.dart b/test_app/lib/screens/test_buttons/video_presentation_service_test_buttons.dart deleted file mode 100644 index c75a236b..00000000 --- a/test_app/lib/screens/test_buttons/video_presentation_service_test_buttons.dart +++ /dev/null @@ -1,222 +0,0 @@ -import 'dart:async'; -import 'package:flutter/material.dart'; -import 'package:dolbyio_comms_sdk_flutter/dolbyio_comms_sdk_flutter.dart'; -import '../../widgets/text_form_field.dart'; -import '/widgets/secondary_button.dart'; -import '/widgets/dialogs.dart'; - -class VideoPresentationServiceTestButtons extends StatefulWidget { - const VideoPresentationServiceTestButtons({Key? key}) : super(key: key); - - @override - State createState() => - _VideoPresentationServiceTestButtonsState(); -} - -class _VideoPresentationServiceTestButtonsState - extends State { - final _dolbyioCommsSdkFlutterPlugin = DolbyioCommsSdk.instance; - bool isDialogCanceled = false; - TextEditingController urlTextController = TextEditingController(); - final formKey = GlobalKey(); - final _eventListeners = >{}; - - @override - void initState() { - super.initState(); - _eventListeners.add(_dolbyioCommsSdkFlutterPlugin.videoPresentation - .onVideoPresentationChange() - .listen((event) { - if (event.type == VideoPresentationEventNames.videoPresentationStarted) { - showResultDialog( - context, 'VideoPresentationStarted', 'On Event Change'); - } else if (event.type == - VideoPresentationEventNames.videoPresentationPaused) { - showResultDialog(context, 'VideoPresentationPaused', 'On Event Change'); - } else if (event.type == - VideoPresentationEventNames.videoPresentationPlayed) { - showResultDialog(context, 'VideoPresentationPlayed', 'On Event Change'); - } else if (event.type == - VideoPresentationEventNames.videoPresentationSought) { - showResultDialog(context, 'VideoPresentationSought', 'On Event Change'); - } - })); - - _eventListeners.add(_dolbyioCommsSdkFlutterPlugin.videoPresentation - .onVideoPresentationStopped() - .listen((event) { - showResultDialog(context, 'VideoPresentationStopped', 'On Event Change'); - })); - } - - @override - void dispose() { - for (final element in _eventListeners) { - element.cancel(); - } - _eventListeners.clear(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Wrap( - spacing: 8.0, - runSpacing: 4.0, - children: [ - SecondaryButton(text: 'Start presenting', onPressed: () => start()), - SecondaryButton(text: 'Check state', onPressed: () => state()), - SecondaryButton(text: 'Current video', onPressed: () => currentVideo()), - SecondaryButton(text: 'Play video', onPressed: () => play()), - SecondaryButton(text: 'Pause video', onPressed: () => pause()), - SecondaryButton(text: 'Seek video', onPressed: () => seek()), - SecondaryButton(text: 'Stop presenting', onPressed: () => stop()), - ], - ); - } - - Future showResultDialog( - BuildContext context, String title, String text) async { - await ViewDialogs.dialog( - context: context, - title: title, - body: text, - ); - } - - Future showInputDialog(BuildContext context) async { - return showDialog( - barrierDismissible: false, - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Enter url'), - content: Form( - key: formKey, - autovalidateMode: AutovalidateMode.onUserInteraction, - child: InputTextFormField( - labelText: "Url", - controller: urlTextController, - focusColor: Colors.deepPurple, - ), - ), - actions: [ - TextButton( - child: const Text('Cancel'), - onPressed: () { - setState(() { - isDialogCanceled = true; - Navigator.pop(context); - }); - }, - ), - TextButton( - child: const Text('Done'), - onPressed: () { - final isValidForm = formKey.currentState!.validate(); - if (isValidForm) { - setState(() => Navigator.pop(context)); - } - }, - ), - ], - ); - }); - } - - Future start() async { - try { - await showInputDialog(context); - if (!isDialogCanceled) { - _dolbyioCommsSdkFlutterPlugin.videoPresentation - .start(urlTextController.text); - if (!mounted) return; - showResultDialog(context, 'Success', 'OK'); - } - } catch (error) { - if (!mounted) return; - showResultDialog(context, 'Error', error.toString()); - } finally { - isDialogCanceled = false; - } - } - - Future state() async { - try { - var videoPresentationState = - await _dolbyioCommsSdkFlutterPlugin.videoPresentation.state(); - if (!mounted) return; - showResultDialog(context, 'Success', videoPresentationState.name); - } catch (error) { - if (!mounted) return; - showResultDialog(context, 'Error', error.toString()); - } - } - - Future stop() async { - try { - await _dolbyioCommsSdkFlutterPlugin.videoPresentation.stop(); - if (!mounted) return; - showResultDialog(context, 'Success', 'OK'); - } catch (error) { - if (!mounted) return; - showResultDialog(context, 'Error', error.toString()); - } - } - - Future play() async { - try { - await _dolbyioCommsSdkFlutterPlugin.videoPresentation.play(); - if (!mounted) return; - showResultDialog(context, 'Success', 'OK'); - } catch (error) { - if (!mounted) return; - showResultDialog(context, 'Error', error.toString()); - } - } - - Future pause() async { - try { - var currentVideo = - await _dolbyioCommsSdkFlutterPlugin.videoPresentation.currentVideo(); - await _dolbyioCommsSdkFlutterPlugin.videoPresentation - .pause(currentVideo!.timestamp); - if (!mounted) return; - showResultDialog(context, 'Success', 'OK'); - } catch (error) { - if (!mounted) return; - showResultDialog(context, 'Error', error.toString()); - } - } - - Future seek() async { - try { - var currentVideo = - await _dolbyioCommsSdkFlutterPlugin.videoPresentation.currentVideo(); - await _dolbyioCommsSdkFlutterPlugin.videoPresentation - .seek(currentVideo!.timestamp); - if (!mounted) return; - showResultDialog(context, 'Success', 'OK'); - } catch (error) { - if (!mounted) return; - showResultDialog(context, 'Error', error.toString()); - } - } - - Future currentVideo() async { - try { - var currentVideo = - await _dolbyioCommsSdkFlutterPlugin.videoPresentation.currentVideo(); - if (!mounted) return; - showResultDialog( - context, - 'Success', - 'Current video owner: ${currentVideo?.owner.info?.name}' - ', url: ${currentVideo?.url}' - ', timestamp: ${currentVideo?.timestamp}'); - } catch (error) { - if (!mounted) return; - showResultDialog(context, 'Error', error.toString()); - } - } -} diff --git a/test_app/lib/state_management/models/conference_model.dart b/test_app/lib/state_management/models/conference_model.dart index 0a704811..3ab95b60 100644 --- a/test_app/lib/state_management/models/conference_model.dart +++ b/test_app/lib/state_management/models/conference_model.dart @@ -7,6 +7,7 @@ class ConferenceModel extends ChangeNotifier { String _username = ""; String? _externalId = ""; bool _isReplayConference = false; + bool _isSomeonePresentingVideo = false; String _imageSource = ''; int _amountOfPagesInDocument = 0; @@ -15,6 +16,7 @@ class ConferenceModel extends ChangeNotifier { String get username => _username; String? get externalId => _externalId; bool get isReplayConference => _isReplayConference; + bool get isSomeonePresentingVideo => _isSomeonePresentingVideo; String get imageSource => _imageSource; int get amountOfPagesInDocument => _amountOfPagesInDocument; @@ -43,6 +45,11 @@ class ConferenceModel extends ChangeNotifier { notifyListeners(); } + set isSomeonePresentingVideo(bool value) { + _isSomeonePresentingVideo = value; + notifyListeners(); + } + set imageSource(String value) { _imageSource = value; notifyListeners(); diff --git a/test_app/lib/widgets/video_presentation_buttons.dart b/test_app/lib/widgets/video_presentation_buttons.dart new file mode 100644 index 00000000..aeb5c8d7 --- /dev/null +++ b/test_app/lib/widgets/video_presentation_buttons.dart @@ -0,0 +1,142 @@ +import 'package:dolbyio_comms_sdk_flutter/dolbyio_comms_sdk_flutter.dart'; +import 'package:dolbyio_comms_sdk_flutter_example/state_management/models/conference_model.dart'; +import 'package:dolbyio_comms_sdk_flutter_example/widgets/secondary_button.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'dialogs.dart'; + +class VideoPresentationButtons extends StatefulWidget { + const VideoPresentationButtons({Key? key}) : super(key: key); + + @override + State createState() => + _VideoPresentationButtonsState(); +} + +class _VideoPresentationButtonsState extends State { + final _dolbyioCommsSdkFlutterPlugin = DolbyioCommsSdk.instance; + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + onPressed: () => play(), + icon: const Icon(Icons.play_arrow), + ), + IconButton( + onPressed: () => pause(), + icon: const Icon(Icons.pause), + ), + IconButton( + onPressed: () => seek(), + icon: const Icon(Icons.refresh), + ), + IconButton( + onPressed: () => stop(), + icon: const Icon(Icons.stop), + ), + ], + ), + SecondaryButton( + text: 'checkVideoState', + onPressed: () => checkState(), + color: Colors.black, + ), + SecondaryButton( + text: 'currentVideoPresentation', + onPressed: () => currentVideo(), + color: Colors.black, + ), + ], + ); + } + + Future showResultDialog( + BuildContext context, String title, String text) async { + await ViewDialogs.dialog( + context: context, + title: title, + body: text, + ); + } + + Future play() async { + try { + await _dolbyioCommsSdkFlutterPlugin.videoPresentation.play(); + } catch (error) { + if (!mounted) return; + showResultDialog(context, 'Error', error.toString()); + } + } + + Future pause() async { + try { + var currentVideo = + await _dolbyioCommsSdkFlutterPlugin.videoPresentation.currentVideo(); + await _dolbyioCommsSdkFlutterPlugin.videoPresentation + .pause(currentVideo!.timestamp); + } catch (error) { + if (!mounted) return; + showResultDialog(context, 'Error', error.toString()); + } + } + + Future seek() async { + try { + await _dolbyioCommsSdkFlutterPlugin.videoPresentation.seek(0); + } catch (error) { + if (!mounted) return; + showResultDialog(context, 'Error', error.toString()); + } + } + + Future stop() async { + try { + await _dolbyioCommsSdkFlutterPlugin.videoPresentation.stop(); + if (!mounted) return; + setState(() { + Provider.of(context, listen: false) + .isSomeonePresentingVideo = false; + }); + } catch (error) { + if (!mounted) return; + showResultDialog(context, 'Error', error.toString()); + } + } + + Future checkState() async { + try { + var videoPresentationState = + await _dolbyioCommsSdkFlutterPlugin.videoPresentation.state(); + if (!mounted) return; + showResultDialog(context, 'Success', videoPresentationState.name); + } catch (error) { + if (!mounted) return; + showResultDialog(context, 'Error', error.toString()); + } + } + + Future currentVideo() async { + try { + var currentVideo = + await _dolbyioCommsSdkFlutterPlugin.videoPresentation.currentVideo(); + if (!mounted) return; + showResultDialog( + context, + 'Success', + 'Current video owner: ${currentVideo?.owner.info?.name}' + ', url: ${currentVideo?.url}' + ', timestamp: ${currentVideo?.timestamp}'); + } catch (error) { + if (!mounted) return; + showResultDialog(context, 'Error', error.toString()); + } + } +} diff --git a/test_app/lib/widgets/video_presentation_container.dart b/test_app/lib/widgets/video_presentation_container.dart new file mode 100644 index 00000000..16c9b6b4 --- /dev/null +++ b/test_app/lib/widgets/video_presentation_container.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; + +class VideoPresentationContainer extends StatelessWidget { + final VideoPlayerController videoPlayerController; + final Widget videoPlayerWidget; + + const VideoPresentationContainer( + {Key? key, + required this.videoPlayerController, + required this.videoPlayerWidget}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: MediaQuery.of(context).size.width * 0.9, + height: MediaQuery.of(context).size.height * 0.3, + child: videoPlayerController.value.isInitialized + ? VideoPlayer(videoPlayerController) + : const SizedBox.shrink(), + ); + } +} diff --git a/test_app/pubspec.yaml b/test_app/pubspec.yaml index 7d4e66b9..fffdc426 100644 --- a/test_app/pubspec.yaml +++ b/test_app/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: collection: ^1.16.0 flutter_spinkit: ^5.1.0 provider: ^6.0.0 + video_player: ^2.2.18 dolbyio_comms_sdk_flutter: # When depending on this package from a real application you should use: