import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:grpc/grpc.dart'; import 'package:logger/logger.dart'; import 'package:ui/gen/signaler_service.pb.dart'; import 'package:ui/gen/signaler_service.pbgrpc.dart' as pb; import 'package:fixnum/fixnum.dart'; import 'package:ui/session_service.dart'; class Call extends StatefulWidget { final pb.SignalerServiceClient client; final SessionService sessionService; final pb.Camera_Identifier cameraID; final String home; const Call(this.client, this.sessionService, {required this.cameraID, required this.home, super.key}); @override CallState createState() => CallState(); } class CallState extends State { Logger logger = Logger(); final RTCVideoRenderer _remoteRenderer = RTCVideoRenderer(); bool _ready = false; String statusLine = "Building..."; @override initState() { super.initState(); _connect(); } _connect() async { _ready = false; logger.i("Init remote renderer"); await _remoteRenderer.initialize(); logger.i("Creating session"); await _createSesson(); } _createSesson() async { var callOptions = CallOptions(metadata: { 'Authorization': await widget.sessionService.getAuthToken(widget.home) }); var cancelCreate = Completer(); var clientSession = await widget.client.createSession( pb.CreateSessionRequest( session: pb.Session( camera: widget.cameraID, ), ), options: callOptions); RTCPeerConnection peerConnection = await createPeerConnection({ // Ice servers; just use the Google one for now 'iceServers': [ {'url': 'stun:stun.l.google.com:19302'} ], }, { /* Empty config */ }); peerConnection.onAddStream = (stream) { // Stream has been added; connect it to our renderer logger.i("Got stream from remote; connecting it"); _remoteRenderer.srcObject = stream; _ready = true; setState(() {}); }; peerConnection.onIceCandidate = (candidate) async { if (candidate.candidate == null) { await widget.client.createIceMessage( CreateIceMessageRequest( sessionIdentifier: clientSession.id, iceMessage: IceMessage( noMoreCandidates: NoMoreCandidates(), )), options: callOptions); return; } await widget.client.createIceMessage( pb.CreateIceMessageRequest( sessionIdentifier: clientSession.id, iceMessage: pb.IceMessage( candidate: pb.IceCandidate( candidate: candidate.candidate, sdpMid: candidate.sdpMid, sdpLineIndex: candidate.sdpMLineIndex, ), ), ), options: callOptions); }; peerConnection.onIceConnectionState = (state) { statusLine = "Ice state now $state"; setState(() {}); logger.i("Ice state now $state"); switch (state) { case RTCIceConnectionState.RTCIceConnectionStateClosed: case RTCIceConnectionState.RTCIceConnectionStateDisconnected: case RTCIceConnectionState.RTCIceConnectionStateFailed: cancelCreate.complete(CallCancelled()); _connect(); default: // do nothing } }; peerConnection.onIceGatheringState = (state) async { logger.i("ICE gathering state $state"); if (state == RTCIceGatheringState.RTCIceGatheringStateComplete) { await widget.client.createIceMessage( CreateIceMessageRequest( sessionIdentifier: clientSession.id, iceMessage: IceMessage( noMoreCandidates: NoMoreCandidates(), )), options: callOptions); } }; peerConnection.onRemoveStream = (stream) {}; peerConnection.onDataChannel = (channel) {}; // This will find the intersection of my candidates and the remote, // then propose one to use var offer = await peerConnection.createOffer(); await peerConnection.setLocalDescription(offer); // Send offer through signaling server logger.i("Offer is $offer"); await widget.client.createIceMessage( pb.CreateIceMessageRequest( sessionIdentifier: clientSession.id, iceMessage: pb.IceMessage( session: pb.IceSessionDescription( sdp: offer.sdp, sdpType: Int64(1), // offer ), ), ), options: callOptions); // Get candidates from remote while (true) { var someResponse = await Future.any([ widget.client.popIceMessage( pb.PopIceMessageRequest(sessionIdentifier: clientSession.id), options: callOptions), cancelCreate.future, ]); if (someResponse is CallCancelled) { break; } var resp = someResponse as pb.IceMessage; if (resp.hasCandidate()) { await peerConnection.addCandidate(RTCIceCandidate( resp.candidate.candidate, resp.candidate.sdpMid, resp.candidate.sdpLineIndex)); } else if (resp.hasNoMoreCandidates()) { logger.i("No more candidates from remote"); } else if (resp.hasSession()) { var session = resp.session; await peerConnection .setRemoteDescription(RTCSessionDescription(session.sdp, "answer")); break; } } } @override Widget build(BuildContext context) { return Column(children: [ Text(widget.cameraID.id), Text(statusLine), SizedBox( height: 480, child: _ready ? RTCVideoView(_remoteRenderer) : const Text("Loading...")), ]); } } class CallCancelled {}