flutter-webrtc / flutter-webrtc

WebRTC plugin for Flutter Mobile/Desktop/Web
MIT License
4.15k stars 1.13k forks source link

Flutter webRtc chrome remote audio, video not working #1673

Open vijaymsc opened 1 month ago

vijaymsc commented 1 month ago

Android to Android => working fine Android to web => android side remote audio, video working fine but chrome remote audio, video not working

App to Web sample Track Logs

app => Track(id: 29be68d3-7a84-442e-b64b-062f64f5e3a6, kind: audio, label: audio, enabled: true, muted: false) web => Track(id: b073fdf5-1472-420f-bf1e-94c690b79b2f, kind: audio, label: b073fdf5-1472-420f-bf1e-94c690b79b2f, enabled: true, muted: true)

Note:: web always return muted: true

call_screen.dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:path_provider/path_provider.dart';
import '../services/signalling_service.dart';

typedef void StreamStateCallback(MediaStream stream);

class CallScreen extends StatefulWidget {
  final String callerId, calleeId;
  final dynamic offer;
  const CallScreen({
    super.key,
    this.offer,
    required this.callerId,
    required this.calleeId,
  });

  @override
  State<CallScreen> createState() => _CallScreenState();
}

class _CallScreenState extends State<CallScreen> {
  // socket instance
  final socket = SignallingService.instance.socket;

  // videoRenderer for localPeer
  final _localRTCVideoRenderer = RTCVideoRenderer();

  // videoRenderer for remotePeer
  final _remoteRTCVideoRenderer = RTCVideoRenderer();

  // mediaStream for localPeer
  MediaStream? _localStream;

  MediaStream? _remoteStream;

  // RTC peer connection
  RTCPeerConnection? _rtcPeerConnection;

  // list of rtcCandidates to be sent over signalling
  List<RTCIceCandidate> rtcIceCadidates = [];

  // media status
  bool isAudioOn = true,
      isVideoOn = true,
      isFrontCameraSelected = true,
      isSpeakerOn = true,

  StreamStateCallback? onAddRemoteStream;

  MediaRecorder? _mediaRecorder;

  @override
  void initState() {
    // initializing renderers
    _localRTCVideoRenderer.initialize();
    _remoteRTCVideoRenderer.initialize();

    // setup Peer Connection
    _setupPeerConnection();

    onAddRemoteStream = ((stream) {
      _remoteRTCVideoRenderer.srcObject = stream;

      setState(() {});
    });
    super.initState();
  }

  void registerPeerConnectionListeners() {
    _rtcPeerConnection?.onIceGatheringState = (RTCIceGatheringState state) {
      print('webRtc:::ICE gathering state changed: $state');
    };

    _rtcPeerConnection?.onConnectionState = (RTCPeerConnectionState state) {
      print('webRtc:::Connection state change: $state');
    };

    _rtcPeerConnection?.onSignalingState = (RTCSignalingState state) {
      print('webRtc:::Signaling state change: $state');
    };

    _rtcPeerConnection?.onIceGatheringState = (RTCIceGatheringState state) {
      print('webRtc:::ICE connection state change: $state');
    };

    _rtcPeerConnection?.onAddStream = (MediaStream stream) {
      print("webRtc:::Add remote stream");
      onAddRemoteStream?.call(stream);
      _remoteStream = stream;
    };
  }

  @override
  void setState(fn) {
    if (mounted) {
      super.setState(fn);
    }
  }
  _setupPeerConnection() async {
    Map<String, dynamic> configuration = {
      'iceServers': [
        {
          'urls': [
            'stun:stun1.l.google.com:19302',
            'stun:stun2.l.google.com:19302'
          ]
        }
      ]
    };

    _rtcPeerConnection = await createPeerConnection(configuration);

    registerPeerConnectionListeners();

    var stream = await navigator.mediaDevices.getUserMedia({
      'video': 
       ? {'facingMode': isFrontCameraSelected ? 'user' : 'environment'}
       : false,
      'audio': isAudioOn
    });
    _localRTCVideoRenderer.srcObject = stream;
    _localStream = stream;
    _remoteRTCVideoRenderer.srcObject = await createLocalMediaStream('remoteStreamKey');

    // set source for remote media renderer
    _rtcPeerConnection?.onTrack = (RTCTrackEvent event) {
      event.streams[0].getTracks().forEach((track) {
        print('webRtc:::Add a track to the remoteStream $track');
        _remoteStream?.addTrack(track);
      });
    };

    // add mediaTrack to peerConnection
    _localStream!.getTracks().forEach((track) {
      _rtcPeerConnection!.addTrack(track, _localStream!);
    });

    // set source for local video renderer
    _localRTCVideoRenderer.srcObject = _localStream;
    setState(() {});

    // for Incoming call
    if (widget.offer != null) {
      // listen for Remote IceCandidate
      socket!.on("IceCandidate", (data) {
        String candidate = data["iceCandidate"]["candidate"];
        String sdpMid = data["iceCandidate"]["id"];
        int sdpMLineIndex = data["iceCandidate"]["label"];

        // add iceCandidate
        _rtcPeerConnection!.addCandidate(RTCIceCandidate(
          candidate,
          sdpMid,
          sdpMLineIndex,
        ));
      });

      // set SDP offer as remoteDescription for peerConnection
      await _rtcPeerConnection!.setRemoteDescription(
        RTCSessionDescription(widget.offer["sdp"], widget.offer["type"]),
      );

      // create SDP answer
      RTCSessionDescription answer = await _rtcPeerConnection!.createAnswer();

      // set SDP answer as localDescription for peerConnection
      _rtcPeerConnection!.setLocalDescription(answer);

      // send SDP answer to remote peer over signalling
      socket!.emit("answerCall", {
        "callerId": widget.callerId,
        "sdpAnswer": answer.toMap(),
      });
    }
    // for Outgoing Call
    else {
      // listen for local iceCandidate and add it to the list of IceCandidate
      _rtcPeerConnection!.onIceCandidate =
          (RTCIceCandidate candidate) => rtcIceCadidates.add(candidate);

      // when call is accepted by remote peer
      socket!.on("callAnswered", (data) async {
        // set SDP answer as remoteDescription for peerConnection
        await _rtcPeerConnection!.setRemoteDescription(
          RTCSessionDescription(
            data["sdpAnswer"]["sdp"],
            data["sdpAnswer"]["type"],
          ),
        );

        // send iceCandidate generated to remote peer over signalling
        for (RTCIceCandidate candidate in rtcIceCadidates) {
          socket!.emit("IceCandidate", {
            "calleeId": widget.calleeId,
            "iceCandidate": {
              "id": candidate.sdpMid,
              "label": candidate.sdpMLineIndex,
              "candidate": candidate.candidate
            }
          });
        }
      });

      // create SDP Offer
      RTCSessionDescription offer = await _rtcPeerConnection!.createOffer();

      // set SDP offer as localDescription for peerConnection
      await _rtcPeerConnection!.setLocalDescription(offer);

      // make a call to remote peer over signalling
      socket!.emit('makeCall', {
        "calleeId": widget.calleeId,
        "sdpOffer": offer.toMap(),
      });
    }
  }

  Future<void> _leaveCall(RTCVideoRenderer localVideo) async {
    Navigator.pop(context);
    List<MediaStreamTrack> tracks = localVideo.srcObject!.getTracks();
    tracks.forEach((track) {
      track.stop();
    });

    if (_remoteStream != null) {
      _remoteStream!.getTracks().forEach((track) => track.stop());
    }
    if (_rtcPeerConnection != null) _rtcPeerConnection!.close();

    _localStream!.dispose();
    _remoteStream?.dispose();
  }

  _toggleMic() {
    // change status
    isAudioOn = !isAudioOn;
    // enable or disable audio track
    _localStream?.getAudioTracks().forEach((track) {
      track.enabled = isAudioOn;
    });
    setState(() {});
  }

  _toggleCamera() {
    // change status
    isVideoOn = !isVideoOn;
    // enable or disable video track
    _localStream?.getVideoTracks().forEach((track) {
      track.enabled = isVideoOn;
    });
    setState(() {});
  }

  _switchCamera() {
    // change status
    isFrontCameraSelected = !isFrontCameraSelected;
    // switch camera
    _localStream?.getVideoTracks().forEach((track) {
      // ignore: deprecated_member_use
      track.switchCamera();
    });
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Theme.of(context).colorScheme.background,
      appBar: AppBar(
        title: const Text("P2P Call App"),
      ),
      body: SafeArea(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Callee ID::${widget.calleeId}',
                style:
                    const TextStyle(fontSize: 18, fontWeight: FontWeight.w900)),
            const SizedBox(height: 50),
            Container(
              alignment: Alignment.center,
              height: 120,
              width: 120,
              decoration: const BoxDecoration(
                  shape: BoxShape.circle, color: Colors.lightBlueAccent),
              child: const Icon(
                Icons.spatial_audio_off,
                size: 50,
              ),
            ),

            const SizedBox(height: 150),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: [
                IconButton(
                  icon: Icon(isAudioOn ? Icons.mic : Icons.mic_off),
                  iconSize: 50,
                  color: isAudioOn ? Colors.green : Colors.white,
                  onPressed: _toggleMic,
                ),
                IconButton(
                  icon: const Icon(Icons.volume_down),
                  iconSize: 50,
                  color: isSpeakerOn ? Colors.green : Colors.white,
                  onPressed: () async {
                    setState(() {
                      isSpeakerOn = !isSpeakerOn;
                      Helper.setSpeakerphoneOn(isSpeakerOn);
                    });
                  },
                ),
                IconButton(
                  icon: const Icon(Icons.call_end),
                  iconSize: 50,
                  color: Colors.red,
                  onPressed: () async {
                    await _leaveCall(_localRTCVideoRenderer);
                  },
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    _localRTCVideoRenderer.dispose();
    _remoteRTCVideoRenderer.dispose();
    _localStream?.dispose();
    _remoteStream?.dispose();
    _rtcPeerConnection?.dispose();
    super.dispose();
  }
}

signalling_service.dart

import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:socket_io_client/socket_io_client.dart';

class SignallingService {
  // instance of Socket
  Socket? socket;

  SignallingService._();
  static final instance = SignallingService._();

 init({required String websocketUrl, required String selfCallerID,required BuildContext context}) {
    // init Socket
    socket = io(websocketUrl, {
      "transports": ['websocket'],
      "query": {"callerId": selfCallerID}
    });

    // listen onConnect event
    socket!.onConnect((data) {
      log("Socket connected !!");
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Connected')));
    });

    // listen onConnectError event
    socket!.onConnectError((data) {
      log("Connect Error $data");
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Connect Error $data')));
    });

    // connect socket
    socket!.connect();
  }
}
vijaymsc commented 1 month ago

flutter_webrtc: ^0.11.7 socket_io_client: ^2.0.3+1

[√] Flutter (Channel stable, 3.22.3, on Microsoft Windows [Version 10.0.19045.4355], locale en-US) [√] Windows Version (Installed version of Windows is version 10 or higher) [√] Android toolchain - develop for Android devices (Android SDK version 34.0.0) [√] Chrome - develop for the web [X] Visual Studio - develop Windows apps X Visual Studio not installed; this is necessary to develop Windows apps. Download at https://visualstudio.microsoft.com/downloads/. Please install the "Desktop development with C++" workload, including all of its default components [√] Android Studio (version 2024.1) [√] Connected device (4 available) [√] Network resources

NickPolakrit commented 1 month ago

Can you try changing to this version?

flutter_webrtc: 0.9.47

vijaymsc commented 1 month ago

@NickPolakrit thanks for reply, now working fine, also do you have any idea about record audio only file and _mediaRecorder!.startWeb(stream); this method record completed but where it be store?