Hello Flutter Community:

我正在使用flutter_webrtc包添加视频通话功能到我的应用程序,它工作正常,但当try 关闭视频通话时,相机仍然在后台运行.

我使用这个类来管理整个过程

class CallVideoEntity {
  RTCVideoRenderer? localRenderer;
  RTCVideoRenderer? remoteRenderer;
  RTCPeerConnection? peerConnection;
  MediaStream? localStream;
  bool offer = false;
  bool isNotificationCallOn = false;
  bool isOpenCallVideoScreen = false;
  SDPEntity? sdpOfferEntity;
  SDPEntity? sdpAnswerEntity;
  ChatEntity? chatEntity;
  CandidateEntity? candidateMemberEntity;
  CandidateEntity? candidateUserEntity;
  BlocChangerCubit callVideoCubit = BlocChangerCubit();
  Socket socket = io(
      DataHelper.wsUrl, OptionBuilder().setTransports(['websocket']).build());

  dispose() async {
    print('cancel video call start');
    localRenderer!.srcObject = null;
    remoteRenderer!.srcObject = null;
    await peerConnection!.close();
    await peerConnection!.removeStream(localStream!);
    await peerConnection!.dispose();
    await localRenderer!.dispose();
    await remoteRenderer!.dispose();

    localRenderer = null;
    remoteRenderer = null;
    peerConnection = null;
    localStream = null;
    sdpOfferEntity = null;
    sdpAnswerEntity = null;
    candidateUserEntity = null;
    candidateMemberEntity = null;
    chatEntity = null;
    offer = false;
    isNotificationCallOn = false;
    isOpenCallVideoScreen = false;
    print('cancel video call end');
  }

  initRenderer(Function updateFunction) async {
    localRenderer = RTCVideoRenderer();
    remoteRenderer = RTCVideoRenderer();
    await localRenderer!.initialize();
    await remoteRenderer!.initialize();

    isNotificationCallOn = true;
    isOpenCallVideoScreen = true;

    _createPeerConnectionData().then((pc) async {
      peerConnection = pc;

      await Future.delayed(const Duration(milliseconds: 1000));
      callVideoCubit.update();

      if (offer) {
        //!
        await createOffer();
      } else {
        await setRemoteDescription();
        await createAnswer();
      }

      // socket.on('leaveVideo', (data) {
      //   if (isOpenCallVideoScreen) {
      //     print('tesssst');
      //     isNotificationCallOn = false;
      //     RouteGenerator.routerClient.pop();
      //   }
      // });
    });
  }

  _createPeerConnectionData() async {
    Map<String, dynamic> configuration = {
      "sdpSemantics": "plan-b",
      "iceServers": [
        {"url": 'stun:stun.l.google.com:19302'},
        {"url": 'stun:stun1.l.google.com:19302'},
        {"url": 'stun:stun2.l.google.com:19302'},
        {"url": 'stun:stun3.l.google.com:19302'},
        {"url": 'stun:stun4.l.google.com:19302'},
      ]
    };

    final Map<String, dynamic> offerSdpConstraints = {
      "mandatory": {
        "OfferToReceiveAudio": true,
        "OfferToReceiveVideo": true,
      },
      "optional": [],
    };

    localStream = await _getUserMedia();

    RTCPeerConnection pc =
        await createPeerConnection(configuration, offerSdpConstraints);

    pc.addStream(localStream!);

    pc.onIceCandidate = (e) {
      candidateMemberEntity ??= CandidateEntity(
          userSeries: 'user-1', // !chatEntity!.userSeries
          memberSeries: DataHelper.currentUser == null
              ? ''
              : DataHelper.currentUser!.series,
          type: "candicate",
          data: CandidateData(
              candidate: e.candidate ?? '',
              sdpMLineIndex: e.sdpMLineIndex ?? 0,
              sdpMid: e.sdpMid ?? '0',
              usernameFragment: '5d94bba3'));
      print('tesssCandidate:${candidateMemberEntity != null}');
    };

    pc.onIceConnectionState = (e) {
      // print(e);
    };

    pc.onAddStream = (stream) {
      // print('addStream: ' + stream.id);
      remoteRenderer!.srcObject = stream;
    };

    return pc;
  }

  _getUserMedia() async {
    final Map<String, dynamic> constraints = {
      'audio': true,
      // 'video': true
      'video': {
        'facingMode': 'user',
      },
    };

    MediaStream stream = await navigator.mediaDevices.getUserMedia(constraints);

    localRenderer!.srcObject = stream;
    // _localRenderer.mirror = true;

    return stream;
  }

  Future<void> createOffer() async {
    RTCSessionDescription description =
        await peerConnection!.createOffer({'offerToReceiveVideo': 1});

    var session = description.sdp.toString();

    peerConnection!.setLocalDescription(description);

    await Future.delayed(const Duration(milliseconds: 1000));
    SDPEntity answerSDP = SDPEntity(
        memberSeries: DataHelper.currentUser == null
            ? ''
            : DataHelper.currentUser!.series,
        userSeries: 'user-1', //! chatEntity!.userSeries
        type: "sdp",
        data: SDPData(sdp: session, type: "offer"));
    socket.emit('rtc', answerSDP.toJson());

    await Future.delayed(const Duration(milliseconds: 1000));
    CandidateEntity candidateDataEntity = CandidateEntity(
        userSeries: sdpOfferEntity == null ? '' : sdpOfferEntity!.userSeries,
        memberSeries: DataHelper.currentUser == null
            ? ''
            : DataHelper.currentUser!.series,
        type: "candicate",
        data: CandidateData(
            candidate: candidateMemberEntity!.data.candidate,
            sdpMid: candidateMemberEntity!.data.sdpMid,
            sdpMLineIndex: candidateMemberEntity!.data.sdpMLineIndex,
            usernameFragment: "5d94bba3"));
    socket.emit('rtc', candidateDataEntity.toJson());
  }

  Future<void> createAnswer() async {
    RTCSessionDescription description =
        await peerConnection!.createAnswer({'offerToReceiveVideo': 1});

    var session = description.sdp.toString();
    print('test_SDP_${session}');
    peerConnection!.setLocalDescription(description);

    SDPEntity answerSDP = SDPEntity(
        memberSeries: DataHelper.currentUser == null
            ? ''
            : DataHelper.currentUser!.series,
        userSeries: sdpAnswerEntity == null ? "" : sdpAnswerEntity!.userSeries,
        type: "sdp",
        data: SDPData(sdp: session, type: "answer"));
    socket.emit('rtc', answerSDP.toJson());

    await Future.delayed(const Duration(milliseconds: 4500));
    CandidateEntity candidateDataEntity = CandidateEntity(
        userSeries: sdpAnswerEntity!.userSeries,
        memberSeries: DataHelper.currentUser == null
            ? ''
            : DataHelper.currentUser!.series,
        type: "candicate",
        data: CandidateData(
            candidate: candidateMemberEntity!.data.candidate,
            sdpMid: candidateMemberEntity!.data.sdpMid,
            sdpMLineIndex: candidateMemberEntity!.data.sdpMLineIndex,
            usernameFragment: "5d94bba3"));
    socket.emit('rtc', candidateDataEntity.toJson());

    await Future.delayed(const Duration(milliseconds: 2000));
    callVideoCubit.update();
  }

  Future<void> setRemoteDescription() async {
    print('Offer? :${offer}');
    String jsonString =
        offer ? sdpOfferEntity!.data.sdp : sdpAnswerEntity!.data.sdp;

    String sdp = write(parse(jsonString), null);
    print('sdp? :${sdp}');

    RTCSessionDescription description =
        RTCSessionDescription(sdp, offer ? 'answer' : 'offer');
    print('description? :${description.toMap()}');

    await peerConnection!.setRemoteDescription(description);

    if (offer) {
      await Future.delayed(const Duration(milliseconds: 2000));
      await addCandidate();
    }
  }

  addCandidate() async {
    //String jsonString = candidateUserEntity!.toJson().toString();

    dynamic candidate = RTCIceCandidate(
        candidateUserEntity!.data.candidate,
        candidateUserEntity!.data.sdpMid,
        candidateUserEntity!.data.sdpMLineIndex);
    await peerConnection!.addCandidate(candidate);
    callVideoCubit.update();
  }
}

对于UI,我使用的代码:

class VideoCallPage extends StatefulWidget {
  const VideoCallPage({Key? key}) : super(key: key);

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

class _VideoCallPageState extends State<VideoCallPage> {
  bool _isAudioMuted = false;
  bool _showButtons = false;
  bool _isVideoMuted = false;

  @override
  void initState() {
    WakelockPlus.enable();
    DataHelper.callVideoEntity.initRenderer(() => setState(() {}));
    super.initState();
  }

  @override
  void deactivate() {
    DataHelper.callVideoEntity.dispose();
    WakelockPlus.disable();
    super.deactivate();
  }

  @override
  dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async {
        return false;
      },
      child: SafeArea(
        child: Scaffold(
            body: BlocChangerBuilder(
          cubit: DataHelper.callVideoEntity.callVideoCubit,
          builder: (context, state) => Stack(
            children: [
              //////////////////// * User Screen * ///////////////////
              SizedBox(
                width: MediaQuery.of(context).size.width,
                height: MediaQuery.of(context).size.height,
                child: Container(
                    key: const Key("remote"),
                    decoration: const BoxDecoration(color: Colors.black54),
                    child: RTCVideoView(
                        DataHelper.callVideoEntity.remoteRenderer!)),
              ),

              //////////////////// * Member Screen * ///////////////////
              Align(
                alignment: Alignment.bottomRight,
                child: Padding(
                  padding: const EdgeInsets.all(6.0),
                  child: SizedBox(
                    width: 147,
                    height: 260,
                    child: Container(
                        key: const Key("local"),
                        decoration: const BoxDecoration(color: Colors.black),
                        child: RTCVideoView(
                          DataHelper.callVideoEntity.localRenderer!,
                          filterQuality: FilterQuality.medium,
                        )),
                  ),
                ),
              ),

              //////////////////// * Buttons * ///////////////////
              AnimatedOpacity(
                opacity: _showButtons ? 1 : 0,
                duration: const Duration(milliseconds: 400),
                child: AnimatedSlide(
                  offset:
                      _showButtons ? const Offset(0, 0) : const Offset(-20, 0),
                  duration: const Duration(milliseconds: 300),
                  child: Align(
                    alignment: Alignment.topLeft,
                    child: Container(
                      decoration: const BoxDecoration(
                          gradient: LinearGradient(
                        colors: [Colors.black87, Colors.transparent],
                        begin: Alignment.centerLeft,
                        end: Alignment.centerRight,
                      )),
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.end,
                        mainAxisSize: MainAxisSize.max,
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          MaterialButton(
                            color: DataHelper.getCurrentColor(),
                            shape: const CircleBorder(),
                            onPressed: _toggleAudioMute,
                            child: _isAudioMuted
                                ? const Icon(
                                    Icons.mic_off,
                                    size: 23,
                                    color: Colors.white,
                                  )
                                : const Icon(Icons.mic,
                                    color: Colors.white, size: 23),
                          ),
                          const SizedBox(
                            height: 6,
                          ),
                          MaterialButton(
                            color: DataHelper.getCurrentColor(),
                            shape: const CircleBorder(),
                            onPressed: _toggleVideoMute,
                            child: _isVideoMuted
                                ? const Icon(
                                    Icons.videocam_off,
                                    size: 23,
                                    color: Colors.white,
                                  )
                                : const Icon(
                                    Icons.videocam,
                                    size: 23,
                                    color: Colors.white,
                                  ),
                          ),
                          const SizedBox(
                            height: 6,
                          ),
                          MaterialButton(
                              color: DataHelper.getCurrentColor(),
                              shape: const CircleBorder(),
                              onPressed: () {
                                DataHelper.callVideoEntity
                                    .isNotificationCallOn = false;
                                DataHelper.callVideoEntity
                                    .isOpenCallVideoScreen = false;

                                SDPEntity sdp = SDPEntity(
                                    memberSeries: DataHelper.currentUser == null
                                        ? ''
                                        : DataHelper.currentUser!.series,
                                    userSeries:
                                        'user-1', //! chatEntity!.userSeries
                                    type: "leaveVideo",
                                    data: SDPData(sdp: '', type: "leaveVideo"));
                                DataHelper.callVideoEntity.socket
                                    .emit('rtc', sdp.toJson());
                                // context.pop();
                                RouteGenerator.routerClient.pop();
                              },
                              child: const Icon(
                                Icons.close,
                                size: 23,
                                color: Colors.white,
                              )),
                          const SizedBox(
                            height: 10,
                          ),
                        ],
                      ),
                    ),
                  ),
                ),
              ),

              //////////////////// * Clicker * ///////////////////
              Align(
                alignment: Alignment.topRight,
                child: IconButton(
                  color: DataHelper.getCurrentColor(),
                  onPressed: () {
                    setState(() {
                      _showButtons = !_showButtons;
                    });
                  },
                  icon: const Icon(
                    Icons.movie_rounded,
                    size: 28,
                    color: Colors.white,
                  ),
                ),
              )
            ],
          ),
        )),
      ),
    );
  }

  void _toggleAudioMute() {
    if (DataHelper.callVideoEntity.localStream != null) {
      DataHelper.callVideoEntity.localStream!.getAudioTracks().forEach((track) {
        track.enabled = _isAudioMuted;
      });

      setState(() {
        _isAudioMuted = !_isAudioMuted;
      });
    }
  }

  void _toggleVideoMute() {
    if (DataHelper.callVideoEntity.localStream != null) {
      DataHelper.callVideoEntity.localStream!.getVideoTracks().forEach((track) {
        track.enabled = _isVideoMuted;
      });

      setState(() {
        _isVideoMuted = !_isVideoMuted;
      });
    }
  }
}

你可以看到我挂断电话的时候完全处理掉了视频通话, 然后我在日志(log)中看到这条信息重复了好几次

I/org.webrtc.Logging(18069):Camera统计数据:Camera Fps:30.

谢谢你的帮助.

try 关闭视频通话,相机仍在后台运行!

推荐答案

你可以看到,我挂断电话后,

你会这样做,但你永远不会关闭getUserMedia获取的流的轨道,这意味着相机仍然打开.try 迭代localRenderers流并停止每个轨道,如下所示:

localRenderer!.srcObject.getTracks().forEach(track => track.stop())

localRenderer!.srcObject = null;岁之前

Flutter相关问答推荐

如何等待抖动S定时器.定期取消?

带有可滚动页面(不包括分页区)的Flutter 页面视图

如何在WidgetRef引用外部的函数中使用Riverpod刷新数据

如何在2024年使用Ffltter将用户数据保存在云FireStore中?

如何将Xpriter代码页更改为代码页27?

Flutter 翼future 建造者不断开火和重建.它在每次重建时都会生成一个新的随机数--我如何防止这种情况?

Flutter 渲染HTML和数学(LaTeX和Katex)

在Ffltter中,我已经使用了Ffltter_BLOC Cupit和Pusher,但当获得新数据时,UI没有更新

如何将带有参数的函数传递给FIFTH中的自定义小部件

在 Flutter 中从 FireStore 获取数据并编译它们需要太多时间

无状态和有状态小部件,我可以同时使用吗?

如何对齐卡片小部件中的图标按钮?

Flutter 构建方法使用的是旧版本的变量?

对话框打开时如何使背景模糊?

Flutter 中出现错误空判断运算符用于空值

Flutter:如何判断嵌套列表是否包含值

函数中的变量在 Dart 中应该有什么关键字?

如何按键在 map 列表中搜索

Flutter - 检测手指何时进入容器

Flutter Desktop,如何获取windows用户配置文件名称?