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

Issue with Screen Sharing on Flutter iOS with video call #1882

Open
1 of 5 tasks
vaibhavbhus opened this issue Jul 4, 2024 · 15 comments
Open
1 of 5 tasks

Issue with Screen Sharing on Flutter iOS with video call #1882

vaibhavbhus opened this issue Jul 4, 2024 · 15 comments

Comments

@vaibhavbhus
Copy link

Version of the agora_rtc_engine

6.3.2

Platforms affected

  • Android
  • iOS
  • macOS
  • Windows
  • Web

Steps to reproduce

  1. Implement screen sharing following the Agora documentation.
  2. Launch the app on an iOS device.
  3. Observe the app crashing on the splash screen.

Expected results

The app should not crash and screen sharing should work as intended.

Actual results

Actual Behavior:
The app crashes on the splash screen after implementing the changes.

I am facing an issue with screen sharing in Flutter specifically on iOS in video call. The feature works as expected on Android.

Initially, I encountered a MissingPluginException with the following message but the video call was working as expected:
MissingPluginException(No implementation found for method show RPSystemBroadcastPickerView on channel example_screensharing_ios)

I followed the steps provided in the Agora documentation for screen sharing. (https://docs.agora.io/en/3.x/video-calling/basic-features/screensharing?platform=flutter)
However, after implementing these changes, my app crashes on the splash screen.

Additional Information:
Environment:
Flutter version: 3.16.1
iOS version: 17.1.1
Agora SDK version: 6.3.2

Please provide guidance on resolving this issue. Thank you.

Code sample

Code sample
import 'dart:ui' as ui;

import 'package:Omniva/component/basic_video_configuration_widget.dart';
import 'package:Omniva/component/example_actions_widget.dart';
import 'package:Omniva/component/remote_video_views_widget.dart';
import 'package:Omniva/component/rgba_image.dart';
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';

import '../../../ids.dart';

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

  @override
  State<StatefulWidget> createState() => _State();
}

class _State extends State<ScreenSharing> with KeepRemoteVideoViewsMixin {
  late final RtcEngineEx _engine;
  bool _isReadyPreview = false;
  String channelId = "test";
  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 {
      await [Permission.microphone, Permission.camera].request();
      _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: appId,
        channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
      ));
      await _engine.setLogLevel(LogLevel.logLevelError);

      _engine.registerEventHandler(_rtcEngineEventHandler);

      await _engine.enableVideo();
      try {
        await _engine.startPreview();
      } catch (e, s) {
        print(e.toString());
        print(s.toString());
      }
      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: 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: 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<void> _updateScreenShareChannelMediaOptions() async {
    try {
      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,
          autoSubscribeVideo: true,
          clientRoleType: ClientRoleType.clientRoleBroadcaster,
        ),
        connection:
            RtcConnection(channelId: _controller.text, localUid: shareShareUid),
      );
      print("e.toString()");
    } catch (e, s) {
      print("e.toString()");
      print(s.toString());
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return ExampleActionsWidget(
      displayContentBuilder: (context, isLayoutHorizontal) {
        if (!_isReadyPreview) return Container();
        final children = <Widget>[
          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<ScreenShareWeb> createState() => _ScreenShareWebState();
}

class _ScreenShareWebState extends State<ScreenShareWeb>
    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<ScreenShareMobile> createState() => _ScreenShareMobileState();
}

class _ScreenShareMobileState extends State<ScreenShareMobile>
    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<void> _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<ScreenShareDesktop> createState() => _ScreenShareDesktopState();
}

class _ScreenShareDesktopState extends State<ScreenShareDesktop>
    implements ScreenShareInterface {
  List<ScreenCaptureSourceInfo> _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<void> _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<ScreenCaptureSourceInfo>(
        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();
}

Screenshots or Video

Screenshots / Video demonstration
7706E30A-BE98-476D-A458-9D54E440C42E.MP4

Logs

Logs
[Paste your logs here]

Flutter Doctor output

Doctor output
[✓] Flutter (Channel stable, 3.16.9, on macOS 14.3.1 23D60 darwin-arm64, locale
    en-IN)
    • Flutter version 3.16.9 on channel stable at
      /Users/user/fvm/versions/3.16.1
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 41456452f2 (5 months ago), 2024-01-25 10:06:23 -0800
    • Engine revision f40e976bed
    • Dart version 3.2.6
    • DevTools version 2.28.5

[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
    • Android SDK at /Users/user/Library/Android/sdk
    • Platform android-34, build-tools 34.0.0
    • Java binary at: /Applications/Android
      Studio.app/Contents/jre/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build
      11.0.12+0-b1504.28-7817840)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 15.2)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 15C500b
    • CocoaPods version 1.14.3

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2021.2)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build
      11.0.12+0-b1504.28-7817840)

[✓] Android Studio (version 4.2)
    • Android Studio at /Users/user/Downloads/android studio/Android
      Studio 4.2.2.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.8+10-b944.6916264)

[✓] Android Studio (version 2021.2)
    • Android Studio at /Users/user/Downloads/android studio/Android
      Studio chipmunk patch 1.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build
      11.0.12+0-b1504.28-7817840)

[✓] VS Code (version 1.90.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.90.0

[✓] Connected device (5 available)
    • sdk gphone arm64 (mobile) • emulator-5554             • android-arm64  • Android 11 (API 30) (emulator)
    • Jenish’s iPhone (mobile)  • 00008030-00110DDC0ED0802E • ios            • iOS 17.5.1 21F90
    • iMac’s iPhone (mobile)    • 00008030-000325A91A90802E • ios            • iOS 17.1.1 21B91
    • macOS (desktop)           • macos                     • darwin-arm64   • macOS 14.3.1 23D60 darwin-arm64
    • Chrome (web)              • chrome                    • web-javascript • Google Chrome 126.0.6478.127
    ! Error: Browsing on the local area network for HARDIK’s iPhone. Ensure the device is unlocked and attached with a cable or associated with the same local area network as this Mac.
      The device must be opted into Developer Mode to connect wirelessly. (code -27)
    ! Error: Browsing on the local area network for Fenil’s iPhone. Ensure the device is unlocked and attached with a cable or associated with the same local area network as this Mac.
      The device must be opted into Developer Mode to connect wirelessly. (code -27)
    ! Error: Browsing on the local area network for Tele’s iPhone. Ensure the device is unlocked and attached with a cable or associated with the same local area network as this Mac.
      The device must be opted into Developer Mode to connect wirelessly. (code -27)
    ! Error: Browsing on the local area network for Pankit’s iPhone. Ensure the device is unlocked and attached with a cable or associated with the same local area network as this Mac.
      The device must be opted into Developer Mode to connect wirelessly. (code -27)

[✓] Network resources
    • All expected network resources are available.
@vaibhavbhus
Copy link
Author

vaibhavbhus commented Jul 4, 2024

@yeahren @maxxfrazer @littleGnAl @LichKing-2234 @devsideal @roip890
Please provide guidance on resolving this issue. Thank you.

@littleGnAl
Copy link
Contributor

It is most likely you referred to an outdated doc which caused the crash.

Initially, I encountered a MissingPluginException with the following message but the video call was working as expected:
MissingPluginException(No implementation found for method show RPSystemBroadcastPickerView on channel example_screensharing_ios)

For this error, you also need to copy the MethodChannel implementation on the native side
https://github.com/AgoraIO-Extensions/Agora-Flutter-SDK/blob/main/example/ios/Runner/AppDelegate.m#L23-L44

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

vaibhavbhus commented Jul 5, 2024

You've shared Objective-C code file link. SO I asked ChatGPT to convert the Objective-C code into Swift, I received the following Swift code:

let screensharingIOSChannel = FlutterMethodChannel(name: "example_screensharing_ios", binaryMessenger: controller.binaryMessenger)
screensharingIOSChannel.setMethodCallHandler { (call, result) in
    if #available(iOS 12.0, *) {
        DispatchQueue.main.async {
            if let url = Bundle.main.url(forResource: nil, withExtension: "appex", subdirectory: "PlugIns"),
               let bundle = Bundle(url: url) {
                
                let picker = RPSystemBroadcastPickerView(frame: CGRect(x: 0, y: 0, width: 100, height: 200))
                picker.showsMicrophoneButton = true
                picker.preferredExtension = bundle.bundleIdentifier
                
                for view in picker.subviews {
                    if let button = view as? UIButton {
                        button.sendActions(for: .allTouchEvents)
                    }
                }
            }
        }
    }
}

After adding this code to my AppDelegate.swift, I encountered the error "No such module 'Flutter'" and I'm unable to build the project.

I have also tried the following code

var screensharingIOSChannel = FlutterMethodChannel(
            name: "example_screensharing_ios",
            binaryMessenger: controller as! FlutterBinaryMessenger)
screensharingIOSChannel.setMethodCallHandler({
            (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
            if #available(iOS 12.0, *) {
                DispatchQueue.main.async(execute: {
                    if let url = Bundle.main.url(forResource: nil, withExtension: "appex", subdirectory: "PlugIns") {
                        if let bundle = Bundle(url: url) {
                            let picker = RPSystemBroadcastPickerView(frame: CGRect(x: 0, y: 0, width: 100, height: 200))
                            picker.showsMicrophoneButton = true
                            picker.preferredExtension = bundle.bundleIdentifier
                            for view in picker.subviews() {
                                if view is UIButton {
                                    (view as? UIButton)?.sendActions(for: .allTouchEvents)
                                }
                            }
                        }
                    }
                })
            }
        })
        ```
        
still facing the same error of no such module found 'Flutter'.

@github-actions github-actions bot removed the waiting for customer response waiting for customer response, or closed by no-reponse bot label Jul 5, 2024
@littleGnAl
Copy link
Contributor

Have you imported the Flutter module like this?

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

yes

@github-actions github-actions bot removed the waiting for customer response waiting for customer response, or closed by no-reponse bot label Jul 5, 2024
@littleGnAl
Copy link
Contributor

Did you run it in the command line?

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

yes I also tried to run it from command line and and from xcode on real device

@github-actions github-actions bot removed the waiting for customer response waiting for customer response, or closed by no-reponse bot label Jul 8, 2024
@littleGnAl
Copy link
Contributor

Can you provide a reproducible demo so we can see what went wrong?

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

RPReplay_Final1720504709.1.1.1.mp4

I've solved that error but by adding following code in my appdelegate.swift
`
var screensharingIOSChannel = FlutterMethodChannel(
name: "example_screensharing_ios",
binaryMessenger: controller.binaryMessenger)

            screensharingIOSChannel.setMethodCallHandler({
                (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
                if #available(iOS 12.0, *) {
                    DispatchQueue.main.async(execute: {
                        if let url = Bundle.main.url(forResource: nil, withExtension: "appex", subdirectory: "PlugIns") {
                            if let bundle = Bundle(url: url) {
                                let picker = RPSystemBroadcastPickerView(frame: CGRect(x: 0, y: 0, width: 100, height: 200))
                                               picker.showsMicrophoneButton = true
                                               picker.preferredExtension = bundle.bundleIdentifier
                                               for view in picker.subviews {
                                                   if let button = view as? UIButton {
                                                       button.sendActions(for: .touchUpInside)
                                                   }
                                               }
                            }
                        }
                    })
                }
            })

`

but I am facing the new error I am not getting my app in screen sharing popup how can I do that. I've attached the video for reference.

@github-actions github-actions bot removed the waiting for customer response waiting for customer response, or closed by no-reponse bot label Jul 9, 2024
@littleGnAl
Copy link
Contributor

I think you may need to refer to the doc to set up the project first
https://docs.agora.io/en/video-calling/core-functionality/screen-sharing?platform=flutter#set-up-your-project

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

I've done all the project setup as per the doc
image

@github-actions github-actions bot removed the waiting for customer response waiting for customer response, or closed by no-reponse bot label Jul 9, 2024
@littleGnAl
Copy link
Contributor

Can you provide a reproducible demo so we can see what went wrong?

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

vaibhavbhus commented Jul 12, 2024

Hi,

I can see the name of my app in the popup, but when I tap on "Start Recording," nothing happens. I am sharing the method channel code that I wrote in the AppDelegate.swift file. Please let me know what am I doing wrong here.

`var screensharingIOSChannel = FlutterMethodChannel(
name: "example_screensharing_ios",
binaryMessenger: controller.binaryMessenger)

            screensharingIOSChannel.setMethodCallHandler({
                (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
                if #available(iOS 12.0, *) {
                    DispatchQueue.main.async(execute: {
                        if let url = Bundle.main.url(forResource: nil, withExtension: "appex", subdirectory: "PlugIns") {
                            if let bundle = Bundle(url: url) {
                                let picker = RPSystemBroadcastPickerView(frame: CGRect(x: 0, y: 0, width: 100, height: 200))
                                               picker.showsMicrophoneButton = true
                                               picker.preferredExtension = bundle.bundleIdentifier
                                               for view in picker.subviews {
                                                   if let button = view as? UIButton {
                                                       button.sendActions(for: .touchUpInside)
                                                   }
                                               }
                            }
                        }
                    })
                }
            })`

@github-actions github-actions bot removed the waiting for customer response waiting for customer response, or closed by no-reponse bot label Jul 12, 2024
@littleGnAl
Copy link
Contributor

I can see the name of my app in the popup, but when I tap on "Start Recording," nothing happens. I am sharing the method channel code that I wrote in the AppDelegate.swift file. Please let me know what am I doing wrong here.

The issue should not related to these codes, if the popup is shown, that means these codes work fine.

I'm not sure which step went wrong in your configuration. Again, can you provide a reproducible demo so we can investigate more?

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

I have attached a video that demonstrates the actual problem. Additionally, I have included my AppDelegate file, where I have written a method channel. I have also included a screenshot of a SampleHandler file from a newly created broadcast extension.

The line await _showRPSystemBroadcastPickerViewIfNeed(); is getting executed, but the code written after that line is not executing.

AppDelegate.swift:
`import UIKit
import Flutter
//import SendBirdSDK
import Firebase
import FirebaseMessaging
//import FirebaseAnalytics

import UserNotifications
import ReplayKit

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
let nativeViewFactory = NativeViewFactory()
var notifcationClick:(([String : Any])->())?
var chatChannel : FlutterMethodChannel?
var videoChannel : FlutterMethodChannel?
var appStatusChannel : FlutterMethodChannel?

override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
            let controller : FlutterViewController = window?.rootViewController as! FlutterViewController

            let flavorChannel = FlutterMethodChannel(
                name: "flavor",
                binaryMessenger: controller.binaryMessenger)

            flavorChannel.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
                // Note: this method is invoked on the UI thread
                let flavor = Bundle.main.infoDictionary?["App - Flavor"]
                result(flavor)
            })
    
    let appVersionChannel = FlutterMethodChannel(
        name: "get_version",
        binaryMessenger: controller.binaryMessenger)
    appVersionChannel.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
        // Note: this method is invoked on the UI thread
        let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
        result(appVersion)
    })
    
    if(FirebaseApp.app()==nil){
        FirebaseApp.configure()}
    Messaging.messaging().delegate = self
    UNUserNotificationCenter.current().delegate = self
    
    // flutter channel
      var screensharingIOSChannel = FlutterMethodChannel(
                name: "example_screensharing_ios",
                binaryMessenger: controller.binaryMessenger)
            
            screensharingIOSChannel.setMethodCallHandler({
                (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
                if #available(iOS 12.0, *) {
                    DispatchQueue.main.async(execute: {
                        if let url = Bundle.main.url(forResource: nil, withExtension: "appex", subdirectory: "PlugIns") {
                            if let bundle = Bundle(url: url) {
                                let picker = RPSystemBroadcastPickerView(frame: CGRect(x: 0, y: 0, width: 100, height: 200))
                                               picker.showsMicrophoneButton = true
                                               picker.preferredExtension = bundle.bundleIdentifier
                                               for view in picker.subviews {
                                                   if let button = view as? UIButton {
                                                       button.sendActions(for: .touchUpInside)
                                                   }
                                               }
                            }
                        }
                    })
                }
            })
    
    // 2
    let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
    UNUserNotificationCenter.current().requestAuthorization(
        options: authOptions) { _, _ in }
    // 3
    application.registerForRemoteNotifications()
    registerRemoteNotification()
    notificationHandler()
    tokenHandler()
    videoMethodHandler()
    appStatusHandler()
    VideoCallHelper.sharedInstance.loadSound()
    registrar(forPlugin: "Runner")?.register(nativeViewFactory, withId: "VideoCallView")
    let flutterViewController: FlutterViewController = window?.rootViewController as! FlutterViewController
    let navigationController = UINavigationController(rootViewController: flutterViewController)
    navigationController.isNavigationBarHidden = true
    window?.rootViewController = navigationController
    window?.makeKeyAndVisible()
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
    let content = notification.request.content
    // Process notification content
    print("\(content.userInfo)")
    completionHandler([.alert, .sound]) // Display notification Banner
    
}
//App Status Handler
func appStatusHandler() {
    
    let controller = window.rootViewController as? FlutterViewController
    appStatusChannel = FlutterMethodChannel(name: "appStatus", binaryMessenger: controller!.binaryMessenger)
}
//Notiification
func notificationHandler() {
    
    let controller = window.rootViewController as? FlutterViewController
    let channel = FlutterMethodChannel(name: "com.base/notification", binaryMessenger: controller!.binaryMessenger)
    channel.setMethodCallHandler { (call, result) in
        
        if call.method == "getNotification" {
            
            if let notification = UserDefaults.standard.dictionary(forKey: "notification") {
                result(notification)
                UserDefaults.standard.removeObject(forKey: "notification")
                return
            }
            
            result(nil)
        }
    }
    
    self.notifcationClick = { data in
        channel.invokeMethod("onNotificationReceived", arguments: data)
    }
}
//Push Token
func tokenHandler() {
    
    let controller = window.rootViewController as? FlutterViewController
    let channel = FlutterMethodChannel(name: "SnsChannel", binaryMessenger: controller!.binaryMessenger)
    channel.setMethodCallHandler { (call, result) in
        
        if call.method == "FetchToken" {
            
            if let token = UserDefaults.standard.string(forKey: "deviceToken"),token.count > 0 {
                result(["NotificationToken" : token])
                return
            }
            
            result(nil)
        }
    }
}
//Video Method Handler
func videoMethodHandler() {
    
    let controller = window.rootViewController as? FlutterViewController
    videoChannel = FlutterMethodChannel(name: "vonage", binaryMessenger: controller!.binaryMessenger)
    videoChannel?.setMethodCallHandler { (call, result) in
        
        let argument = call.arguments as? [String : Any]
        if call.method == "videoCall" {
            
            VideoCallHelper.sharedInstance.startVideoCall(json: argument!)
            result(nil)
        }
        else if call.method == "disconnectCall" {
            VideoCallHelper.sharedInstance.stopSound()
            
            
            if let vc = UIViewController.current() as? AudioIncommingVC {
                vc.dismiss(animated: true, completion: nil)
            }
            
            result(nil)
        }
        else if call.method == "checkNetwork" {
            
            VideoCallHelper.sharedInstance.checkNetworkQuality(json: argument!)
            VideoCallHelper.sharedInstance.networkTestComplete = { response in
                result(response)
            }
        }
        else{
            result(nil)
        }
    }
}

}
extension AppDelegate {

public override func applicationDidBecomeActive(_ application: UIApplication) {
    
    appStatusChannel?.invokeMethod("onResume", arguments: nil)
}

override func applicationDidEnterBackground(_ application: UIApplication) {
    
    
    appStatusChannel?.invokeMethod("onPause", arguments: nil)
}

func registerRemoteNotification() {
    
    if #available(iOS 10.0, *) {
        let center = UNUserNotificationCenter.current()
        center.delegate = self
        center.requestAuthorization(options: [UNAuthorizationOptions.alert,UNAuthorizationOptions.sound,UNAuthorizationOptions.badge], completionHandler: { (granted, error) in
            
            if error == nil {
                DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() }
                
            }
        })
    } else {
        // Fallback on earlier versions
        let types: UIUserNotificationType = [UIUserNotificationType.badge, UIUserNotificationType.alert,UIUserNotificationType.sound]
        UIApplication.shared.registerUserNotificationSettings(UIUserNotificationSettings(types: types, categories: nil))
        UIApplication.shared.registerForRemoteNotifications()
        UIApplication.shared.delegate = self
    }
}

override func application(_ application: UIApplication,continue userActivity: NSUserActivity,restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {

// // 1
// guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
// let url = userActivity.webpageURL,
// let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
// return false
// }
//
// // 2
// if let computer = ItemHandler.sharedInstance.items
// .filter({ $0.path == components.path}).first {
//// presentDetailViewController(computer)
// return true
// }
//
// // 3
// if let webpageUrl = URL(string: "http://rw-universal-links-final.herokuapp.com") {
// application.open(webpageUrl)
// return false
// }

    return false
}

override func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
    print("Failed to register with error: \(error)")
}

//    override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
//
//        if let token = UserDefaults.standard.string(forKey: "deviceToken"),token.count > 0 {
//
//            return
//        }
//
//
//        SBDMain.registerDevicePushToken(deviceToken, unique: true) { (status, error) in}
//        let deviceTokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
//        print("Device Token :- \(deviceTokenString)")
//        UserDefaults.standard.set(deviceTokenString, forKey: "deviceToken")
//    }

override func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
    
    let userInfo = response.notification.request.content.userInfo
    
    /*if let appointmentId = ((userInfo["sendbird"] as? [String : Any])?["channel"] as? [String : Any])?["name"] as? String {
     
     //send Bird Notification
     let data : [String : Any] = ["appointmentId" : appointmentId,"isChat" : true]
     if let block = self.notifcationClick {
     block(data)
     }
     else{
     UserDefaults.standard.set(data, forKey: "notification")
     }
     }
     else if let data = ((userInfo["aps"] as? [String : Any])?["data"] as? [[String : Any]])?.first?["data"] as? [String : Any] {
     
     //Server Notification
     if data["module"] as? Int == 7 && data["action"] as? Int == 18 {
     
     var finalData = data
     finalData["isIncomingCall"] = true
     finalData["isAudioCall"] = (data["callType"] as? Int) == 1 ? true : false // Call is Audio call
     
     if (data["callToType"] as? Int) == 5 {
     finalData["token"] = data["physicianToken"] as? String
     finalData["isPhysician"] = true
     
     }
     else{
     finalData["token"] = data["patientToken"] as? String
     finalData["isPhysician"] = false
     }
     
     
     DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
     VideoCallHelper.sharedInstance.startVideoCall(json: finalData)
     }
     }
     else{
     if let block = self.notifcationClick {
     block(data)
     }
     else{
     UserDefaults.standard.set(data, forKey: "notification")
     }
     }
     }
     else if let payLoad = userInfo["data"] as? String {
     
     //InApp Notification
     let data : [String : Any] = ["appointmentId" : payLoad,"isChat" : true]
     if let block = self.notifcationClick {
     block(data)
     }
     else{
     UserDefaults.standard.set(data, forKey: "notification")
     }
     }*/
    if let data = userInfo["data"]  as? String {
        let dict = convertToDictionary(text: data)
        if let notificationData = dict as NSDictionary?{
            let appointmentType = notificationData["notification_type"] as! String
            let appointmentId = notificationData["appointmentId"] as! String
            let userType = notificationData["user_type"] as? String
            
            let data : [String : Any] = ["appointmentType" :  appointmentType,"appointmentId":appointmentId,"userType" :  userType]
            if let block = self.notifcationClick {
                block(data)
            }
            else{
                UserDefaults.standard.set(data, forKey: "notification")
            }
        }
        //InApp Notification
        
    }
}


private func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
    print("Notification Response:\(userInfo)")
}

}
func convertToDictionary(text: String) -> [String: Any]? {
if let data = text.data(using: .utf8) {
do {
return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
} catch {
print(error.localizedDescription)
}
}
return nil
}
extension AppDelegate: MessagingDelegate {
override func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
Messaging.messaging().apnsToken = deviceToken
}
func messaging(
_ messaging: Messaging,
didReceiveRegistrationToken fcmToken: String?
) {
let tokenDict = [ "token": fcmToken ?? ""]
print("FCMToken:(fcmToken)")
NotificationCenter.default.post(
name: Notification.Name("FCMToken"),
object: nil,
userInfo: tokenDict)
UserDefaults.standard.set(fcmToken, forKey: "deviceToken")
}

}
`

SampleHandler.swift
`
import ReplayKit

class SampleHandler: RPBroadcastSampleHandler {

override func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) {
    // User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional. 
}

override func broadcastPaused() {
    // User has requested to pause the broadcast. Samples will stop being delivered.
}

override func broadcastResumed() {
    // User has requested to resume the broadcast. Samples delivery will resume.
}

override func broadcastFinished() {
    // User has requested to finish the broadcast.
}

override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
    switch sampleBufferType {
    case RPSampleBufferType.video:
        // Handle video sample buffer
        break



    case RPSampleBufferType.audioApp:
        // Handle audio sample buffer for app audio
        break
    case RPSampleBufferType.audioMic:
        // Handle audio sample buffer for mic audio
        break
    @unknown default:
        // Handle other sample buffer types
        fatalError("Unknown type of sample buffer")
    }
}

}
`

WhatsApp.Video.2024-07-24.at.5.04.21.PM.mp4

@github-actions github-actions bot removed the waiting for customer response waiting for customer response, or closed by no-reponse bot label Jul 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants