Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to detect who is sharing the screen in flutter agora web #1892

Closed
danilo1998271 opened this issue Jul 9, 2024 · 7 comments
Closed

How to detect who is sharing the screen in flutter agora web #1892

danilo1998271 opened this issue Jul 9, 2024 · 7 comments
Labels
waiting for customer response waiting for customer response, or closed by no-reponse bot

Comments

@danilo1998271
Copy link

import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:agora_rtc_engine_example/components/rgba_image.dart';
import 'package:agora_rtc_engine_example/components/basic_video_configuration_widget.dart';
import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
import 'package:agora_rtc_engine_example/components/log_sink.dart';
import 'package:agora_rtc_engine_example/components/remote_video_views_widget.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:ui' as ui;

/// ScreenSharing Example
class ScreenSharing extends StatefulWidget {
/// Construct the [ScreenSharing]
const ScreenSharing({Key? key}) : super(key: key);

@OverRide
State createState() => _State();
}

class _State extends State with KeepRemoteVideoViewsMixin {
late final RtcEngineEx _engine;
bool _isReadyPreview = false;
String channelId = config.channelId;
bool isJoined = false;
late TextEditingController _controller;
late final TextEditingController _localUidController;
late final TextEditingController _screenShareUidController;

bool _isScreenShared = false;
late final RtcEngineEventHandler _rtcEngineEventHandler;

@OverRide
void initState() {
super.initState();
_controller = TextEditingController(text: channelId);
_localUidController = TextEditingController(text: '1000');
_screenShareUidController = TextEditingController(text: '1001');
_initEngine();
}

@OverRide
void dispose() {
super.dispose();
_engine.unregisterEventHandler(_rtcEngineEventHandler);
_engine.release();
}

_initEngine() async {
_rtcEngineEventHandler = RtcEngineEventHandler(
onError: (ErrorCodeType err, String msg) {
logSink.log('[onError] err: $err, msg: $msg');
}, onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
logSink.log(
'[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
setState(() {
isJoined = true;
});
}, onLeaveChannel: (RtcConnection connection, RtcStats stats) {
logSink.log(
'[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
setState(() {
isJoined = false;
});
}, onLocalVideoStateChanged: (VideoSourceType source,
LocalVideoStreamState state, LocalVideoStreamReason error) {
logSink.log(
'[onLocalVideoStateChanged] source: $source, state: $state, error: $error');
if (!(source == VideoSourceType.videoSourceScreen ||
source == VideoSourceType.videoSourceScreenPrimary)) {
return;
}

  switch (state) {
    case LocalVideoStreamState.localVideoStreamStateCapturing:
    case LocalVideoStreamState.localVideoStreamStateEncoding:
      setState(() {
        _isScreenShared = true;
      });
      break;
    case LocalVideoStreamState.localVideoStreamStateStopped:
    case LocalVideoStreamState.localVideoStreamStateFailed:
      setState(() {
        _isScreenShared = false;
      });
      break;
    default:
      break;
  }
});
_engine = createAgoraRtcEngineEx();
await _engine.initialize(RtcEngineContext(
  appId: config.appId,
  channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
));
await _engine.setLogLevel(LogLevel.logLevelError);

_engine.registerEventHandler(_rtcEngineEventHandler);

await _engine.enableVideo();
await _engine.startPreview();
await _engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);

setState(() {
  _isReadyPreview = true;
});

}

void _joinChannel() async {
final localUid = int.tryParse(_localUidController.text);
if (localUid != null) {
await _engine.joinChannelEx(
token: '',
connection:
RtcConnection(channelId: _controller.text, localUid: localUid),
options: const ChannelMediaOptions(
autoSubscribeVideo: true,
autoSubscribeAudio: true,
publishCameraTrack: true,
publishMicrophoneTrack: true,
clientRoleType: ClientRoleType.clientRoleBroadcaster,
));
}

final shareShareUid = int.tryParse(_screenShareUidController.text);
if (shareShareUid != null) {
  await _engine.joinChannelEx(
      token: '',
      connection: RtcConnection(
          channelId: _controller.text, localUid: shareShareUid),
      options: const ChannelMediaOptions(
        autoSubscribeVideo: false,
        autoSubscribeAudio: false,
        publishScreenTrack: true,
        publishSecondaryScreenTrack: true,
        publishCameraTrack: false,
        publishMicrophoneTrack: false,
        publishScreenCaptureAudio: true,
        publishScreenCaptureVideo: true,
        clientRoleType: ClientRoleType.clientRoleBroadcaster,
      ));
}

}

Future _updateScreenShareChannelMediaOptions() async {
final shareShareUid = int.tryParse(_screenShareUidController.text);
if (shareShareUid == null) return;
await _engine.updateChannelMediaOptionsEx(
options: const ChannelMediaOptions(
publishScreenTrack: true,
publishSecondaryScreenTrack: true,
publishCameraTrack: false,
publishMicrophoneTrack: false,
publishScreenCaptureAudio: true,
publishScreenCaptureVideo: true,
clientRoleType: ClientRoleType.clientRoleBroadcaster,
),
connection:
RtcConnection(channelId: _controller.text, localUid: shareShareUid),
);
}

_leaveChannel() async {
await _engine.stopScreenCapture();
await _engine.leaveChannel();
}

@OverRide
Widget build(BuildContext context) {
return ExampleActionsWidget(
displayContentBuilder: (context, isLayoutHorizontal) {
if (!_isReadyPreview) return Container();
final children = [
Expanded(
flex: 1,
child: AspectRatio(
aspectRatio: 1,
child: AgoraVideoView(
controller: VideoViewController(
rtcEngine: _engine,
canvas: const VideoCanvas(
uid: 0,
),
)),
),
),
Expanded(
flex: 1,
child: AspectRatio(
aspectRatio: 1,
child: _isScreenShared
? AgoraVideoView(
controller: VideoViewController(
rtcEngine: _engine,
canvas: const VideoCanvas(
uid: 0,
sourceType: VideoSourceType.videoSourceScreen,
),
))
: Container(
color: Colors.grey[200],
child: const Center(
child: Text('Screen Sharing View'),
),
),
),
),
];
Widget localVideoView;
if (isLayoutHorizontal) {
localVideoView = Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: children,
);
} else {
localVideoView = Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: children,
);
}
return Stack(
children: [
localVideoView,
Align(
alignment: Alignment.topLeft,
child: RemoteVideoViewsWidget(
key: keepRemoteVideoViewsKey,
rtcEngine: _engine,
channelId: _controller.text,
connectionUid: int.tryParse(_localUidController.text),
),
)
],
);
},
actionsBuilder: (context, isLayoutHorizontal) {
if (!_isReadyPreview) return Container();
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _controller,
decoration: const InputDecoration(hintText: 'Channel ID'),
),
TextField(
controller: _localUidController,
decoration: const InputDecoration(hintText: 'Local Uid'),
),
TextField(
controller: _screenShareUidController,
decoration: const InputDecoration(hintText: 'Screen Sharing Uid'),
),
const SizedBox(
height: 20,
),
BasicVideoConfigurationWidget(
rtcEngine: _engine,
title: 'Video Encoder Configuration',
setConfigButtonText: const Text(
'setVideoEncoderConfiguration',
style: TextStyle(fontSize: 10),
),
onConfigChanged: (width, height, frameRate, bitrate) {
_engine.setVideoEncoderConfiguration(VideoEncoderConfiguration(
dimensions: VideoDimensions(width: width, height: height),
frameRate: frameRate,
bitrate: bitrate,
));
},
),
const SizedBox(
height: 20,
),
Row(
children: [
Expanded(
flex: 1,
child: ElevatedButton(
onPressed: isJoined ? _leaveChannel : _joinChannel,
child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
),
)
],
),
if (kIsWeb)
ScreenShareWeb(
rtcEngine: _engine,
isScreenShared: _isScreenShared,
onStartScreenShared: () {
if (isJoined) {
_updateScreenShareChannelMediaOptions();
}
},
onStopScreenShare: () {}),
if (!kIsWeb &&
(defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS))
ScreenShareMobile(
rtcEngine: _engine,
isScreenShared: _isScreenShared,
onStartScreenShared: () {
if (isJoined) {
_updateScreenShareChannelMediaOptions();
}
},
onStopScreenShare: () {}),
if (!kIsWeb &&
(defaultTargetPlatform == TargetPlatform.windows ||
defaultTargetPlatform == TargetPlatform.macOS))
ScreenShareDesktop(
rtcEngine: _engine,
isScreenShared: _isScreenShared,
onStartScreenShared: () {
if (isJoined) {
_updateScreenShareChannelMediaOptions();
}
},
onStopScreenShare: () {}),
],
);
},
);
}
}

class ScreenShareWeb extends StatefulWidget {
const ScreenShareWeb(
{Key? key,
required this.rtcEngine,
required this.isScreenShared,
required this.onStartScreenShared,
required this.onStopScreenShare})
: super(key: key);

final RtcEngine rtcEngine;
final bool isScreenShared;
final VoidCallback onStartScreenShared;
final VoidCallback onStopScreenShare;

@OverRide
State createState() => _ScreenShareWebState();
}

class _ScreenShareWebState extends State
implements ScreenShareInterface {
@OverRide
bool get isScreenShared => widget.isScreenShared;

@OverRide
void onStartScreenShared() {
widget.onStartScreenShared();
}

@OverRide
void onStopScreenShare() {
widget.onStopScreenShare();
}

@OverRide
RtcEngine get rtcEngine => widget.rtcEngine;

@OverRide
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
flex: 1,
child: ElevatedButton(
onPressed: !isScreenShared ? startScreenShare : stopScreenShare,
child: Text('${isScreenShared ? 'Stop' : 'Start'} screen share'),
),
)
],
);
}

@OverRide
void startScreenShare() async {
if (isScreenShared) return;

await rtcEngine.startScreenCapture(
    const ScreenCaptureParameters2(captureAudio: true, captureVideo: true));
await rtcEngine.startPreview(sourceType: VideoSourceType.videoSourceScreen);
onStartScreenShared();

}

@OverRide
void stopScreenShare() async {
if (!isScreenShared) return;

await rtcEngine.stopScreenCapture();
onStopScreenShare();

}
}

class ScreenShareMobile extends StatefulWidget {
const ScreenShareMobile(
{Key? key,
required this.rtcEngine,
required this.isScreenShared,
required this.onStartScreenShared,
required this.onStopScreenShare})
: super(key: key);

final RtcEngine rtcEngine;
final bool isScreenShared;
final VoidCallback onStartScreenShared;
final VoidCallback onStopScreenShare;

@OverRide
State createState() => _ScreenShareMobileState();
}

class _ScreenShareMobileState extends State
implements ScreenShareInterface {
final MethodChannel _iosScreenShareChannel =
const MethodChannel('example_screensharing_ios');

@OverRide
bool get isScreenShared => widget.isScreenShared;

@OverRide
void onStartScreenShared() {
widget.onStartScreenShared();
}

@OverRide
void onStopScreenShare() {
widget.onStopScreenShare();
}

@OverRide
RtcEngine get rtcEngine => widget.rtcEngine;

@OverRide
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
flex: 1,
child: ElevatedButton(
onPressed: !isScreenShared ? startScreenShare : stopScreenShare,
child: Text('${isScreenShared ? 'Stop' : 'Start'} screen share'),
),
)
],
);
}

@OverRide
void startScreenShare() async {
if (isScreenShared) return;

await rtcEngine.startScreenCapture(
    const ScreenCaptureParameters2(captureAudio: true, captureVideo: true));
await rtcEngine.startPreview(sourceType: VideoSourceType.videoSourceScreen);
_showRPSystemBroadcastPickerViewIfNeed();
onStartScreenShared();

}

@OverRide
void stopScreenShare() async {
if (!isScreenShared) return;

await rtcEngine.stopScreenCapture();
onStopScreenShare();

}

Future _showRPSystemBroadcastPickerViewIfNeed() async {
if (defaultTargetPlatform != TargetPlatform.iOS) {
return;
}

await _iosScreenShareChannel
    .invokeMethod('showRPSystemBroadcastPickerView');

}
}

class ScreenShareDesktop extends StatefulWidget {
const ScreenShareDesktop(
{Key? key,
required this.rtcEngine,
required this.isScreenShared,
required this.onStartScreenShared,
required this.onStopScreenShare})
: super(key: key);

final RtcEngine rtcEngine;
final bool isScreenShared;
final VoidCallback onStartScreenShared;
final VoidCallback onStopScreenShare;

@OverRide
State createState() => _ScreenShareDesktopState();
}

class _ScreenShareDesktopState extends State
implements ScreenShareInterface {
List _screenCaptureSourceInfos = [];
late ScreenCaptureSourceInfo _selectedScreenCaptureSourceInfo;

@OverRide
bool get isScreenShared => widget.isScreenShared;

@OverRide
void onStartScreenShared() {
widget.onStartScreenShared();
}

@OverRide
void onStopScreenShare() {
widget.onStopScreenShare();
}

@OverRide
RtcEngine get rtcEngine => widget.rtcEngine;

Future _initScreenCaptureSourceInfos() async {
SIZE thumbSize = const SIZE(width: 50, height: 50);
SIZE iconSize = const SIZE(width: 50, height: 50);
_screenCaptureSourceInfos = await rtcEngine.getScreenCaptureSources(
thumbSize: thumbSize, iconSize: iconSize, includeScreen: true);
_selectedScreenCaptureSourceInfo = _screenCaptureSourceInfos[0];
setState(() {});
}

Widget _createDropdownButton() {
if (_screenCaptureSourceInfos.isEmpty) return Container();
ui.PixelFormat format = ui.PixelFormat.rgba8888;
if (defaultTargetPlatform == TargetPlatform.windows) {
// The native sdk return the bgra format on Windows.
format = ui.PixelFormat.bgra8888;
}
return DropdownButton(
items: _screenCaptureSourceInfos.map((info) {
Widget image;
if (info.iconImage!.width! != 0 && info.iconImage!.height! != 0) {
image = RgbaImage(
bytes: info.iconImage!.buffer!,
width: info.iconImage!.width!,
height: info.iconImage!.height!,
format: format,
);
} else if (info.thumbImage!.width! != 0 &&
info.thumbImage!.height! != 0) {
image = RgbaImage(
bytes: info.thumbImage!.buffer!,
width: info.thumbImage!.width!,
height: info.thumbImage!.height!,
format: format,
);
} else {
image = const SizedBox(
width: 50,
height: 50,
);
}

      return DropdownMenuItem(
        value: info,
        child: Row(
          mainAxisSize: MainAxisSize.min,
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            image,
            Text('${info.sourceName}', style: const TextStyle(fontSize: 10))
          ],
        ),
      );
    }).toList(),
    value: _selectedScreenCaptureSourceInfo,
    onChanged: isScreenShared
        ? null
        : (v) {
            setState(() {
              _selectedScreenCaptureSourceInfo = v!;
            });
          });

}

@OverRide
void initState() {
super.initState();

_initScreenCaptureSourceInfos();

}

@OverRide
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_createDropdownButton(),
if (_screenCaptureSourceInfos.isNotEmpty)
Row(
children: [
Expanded(
flex: 1,
child: ElevatedButton(
onPressed:
!isScreenShared ? startScreenShare : stopScreenShare,
child:
Text('${isScreenShared ? 'Stop' : 'Start'} screen share'),
),
)
],
),
],
);
}

@OverRide
void startScreenShare() async {
if (isScreenShared) return;

final sourceId = _selectedScreenCaptureSourceInfo.sourceId;

if (_selectedScreenCaptureSourceInfo.type ==
    ScreenCaptureSourceType.screencapturesourcetypeScreen) {
  await rtcEngine.startScreenCaptureByDisplayId(
      displayId: sourceId!,
      regionRect: const Rectangle(x: 0, y: 0, width: 0, height: 0),
      captureParams: const ScreenCaptureParameters(
        captureMouseCursor: true,
        frameRate: 30,
      ));
} else if (_selectedScreenCaptureSourceInfo.type ==
    ScreenCaptureSourceType.screencapturesourcetypeWindow) {
  await rtcEngine.startScreenCaptureByWindowId(
    windowId: sourceId!,
    regionRect: const Rectangle(x: 0, y: 0, width: 0, height: 0),
    captureParams: const ScreenCaptureParameters(
      captureMouseCursor: true,
      frameRate: 30,
    ),
  );
}

onStartScreenShared();

}

@OverRide
void stopScreenShare() async {
if (!isScreenShared) return;

await rtcEngine.stopScreenCapture();
onStopScreenShare();

}
}

abstract class ScreenShareInterface {
void onStartScreenShared();

void onStopScreenShare();

bool get isScreenShared;

RtcEngine get rtcEngine;

void startScreenShare();

void stopScreenShare();
}

@littleGnAl
Copy link
Collaborator

You should know what the screen share uid is, in your case, it's 1001.

@littleGnAl littleGnAl added the waiting for customer response waiting for customer response, or closed by no-reponse bot label Jul 11, 2024
@amit3xpo
Copy link

Hi @littleGnAl , what about when we use one RtcEngine instance only to do this?

Below is the code where we can share screen track without joining as different user (1001).

await _engine.startScreenCapture(
        const ScreenCaptureParameters2(captureAudio: true, captureVideo: true));
    await _engine.startPreview(sourceType: VideoSourceType.videoSourceScreen);

    await _engine.updateChannelMediaOptions(
      const ChannelMediaOptions(
        publishScreenTrack: true,
        publishSecondaryScreenTrack: true,
        publishCameraTrack: false,
        publishMicrophoneTrack: false,
        publishScreenCaptureAudio: true,
        publishScreenCaptureVideo: true,
        clientRoleType: ClientRoleType.clientRoleBroadcaster,
      ),
    );

This shares the screen track and another remote user is able to view the screenshare track successfully when already joined inside that channel. But when that remote user leaves and rejoins the channel, this screenshare track does not load again. How can I solve this for remote user who joined after screenshare happened?

@amit3xpo
Copy link

When using the below video view, screenshare track does not load for remote users when joining after screen is already shared by another user.

AgoraVideoView(
          controller: VideoViewController.remote(
                                                              rtcEngine: _engine,
                                                              canvas: VideoCanvas(
                                                                    uid: e,
                                                                    sourceType: VideoSourceType.videoSourceScreen,
                                                                  ),
                                                                  connection: RtcConnection(
                                                                      channelId:rtcChannelName)
                                                                  )),

@littleGnAl
Copy link
Collaborator

The screen-sharing stream should not be published without calling joinChannel, if it is published by only calling the updateChannelMediaOptions , it should be a bug.

@amit3xpo
Copy link

The screen-sharing stream should not be published without calling joinChannel, if it is published by only calling the updateChannelMediaOptions , it should be a bug.

Did you mean that we cannot join a channel first and then also, share screen later with same user without joining as another user?

So far, I have always seen Agora's flutter screen sharing examples always have an extra dedicated user joined for sharing screen only. Is that the only to share screen? Join an additional "screen share only" user always?

Copy link
Contributor

Without additional information, we are unfortunately not sure how to resolve this issue. We are therefore reluctantly going to close this bug for now. If you find this problem please file a new issue with the same description, what happens, logs and the output. All system setups can be slightly different so it's always better to open new issues and reference the related ones. Thanks for your contribution.

Copy link
Contributor

github-actions bot commented Aug 1, 2024

This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please raise a new issue.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 1, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
waiting for customer response waiting for customer response, or closed by no-reponse bot
Projects
None yet
Development

No branches or pull requests

3 participants