2023-09-28 20:35:50 -07:00
|
|
|
import 'dart:async';
|
|
|
|
|
|
2023-09-17 15:45:28 -07:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
2023-09-28 20:35:50 -07:00
|
|
|
import 'package:grpc/grpc.dart';
|
2023-09-17 15:45:28 -07:00
|
|
|
import 'package:logger/logger.dart';
|
2023-09-28 20:35:50 -07:00
|
|
|
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';
|
2023-09-17 15:45:28 -07:00
|
|
|
|
|
|
|
|
class Call extends StatefulWidget {
|
2023-09-28 20:35:50 -07:00
|
|
|
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});
|
2023-09-17 15:45:28 -07:00
|
|
|
|
|
|
|
|
@override
|
2023-09-28 20:35:50 -07:00
|
|
|
CallState createState() => CallState();
|
2023-09-17 15:45:28 -07:00
|
|
|
}
|
|
|
|
|
|
2023-09-28 20:35:50 -07:00
|
|
|
class CallState extends State<Call> {
|
2023-09-17 15:45:28 -07:00
|
|
|
Logger logger = Logger();
|
2023-09-28 20:35:50 -07:00
|
|
|
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)
|
|
|
|
|
});
|
2023-09-17 15:45:28 -07:00
|
|
|
|
2023-09-28 20:35:50 -07:00
|
|
|
var cancelCreate = Completer();
|
2023-09-17 15:45:28 -07:00
|
|
|
|
2023-09-28 20:35:50 -07:00
|
|
|
var clientSession = await widget.client.createSession(
|
|
|
|
|
pb.CreateSessionRequest(
|
|
|
|
|
session: pb.Session(
|
|
|
|
|
camera: widget.cameraID,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
options: callOptions);
|
2023-09-17 15:45:28 -07:00
|
|
|
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
|
2023-09-28 20:35:50 -07:00
|
|
|
logger.i("Got stream from remote; connecting it");
|
2023-09-17 15:45:28 -07:00
|
|
|
_remoteRenderer.srcObject = stream;
|
2023-09-28 20:35:50 -07:00
|
|
|
_ready = true;
|
|
|
|
|
setState(() {});
|
2023-09-17 15:45:28 -07:00
|
|
|
};
|
|
|
|
|
|
2023-09-28 20:35:50 -07:00
|
|
|
peerConnection.onIceCandidate = (candidate) async {
|
2023-09-17 15:45:28 -07:00
|
|
|
if (candidate.candidate == null) {
|
2023-09-28 20:35:50 -07:00
|
|
|
await widget.client.createIceMessage(
|
|
|
|
|
CreateIceMessageRequest(
|
|
|
|
|
sessionIdentifier: clientSession.id,
|
|
|
|
|
iceMessage: IceMessage(
|
|
|
|
|
noMoreCandidates: NoMoreCandidates(),
|
|
|
|
|
)),
|
|
|
|
|
options: callOptions);
|
2023-09-17 15:45:28 -07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-28 20:35:50 -07:00
|
|
|
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
|
|
|
|
|
}
|
2023-09-17 15:45:28 -07:00
|
|
|
};
|
|
|
|
|
|
2023-09-28 20:35:50 -07:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
};
|
2023-09-17 15:45:28 -07:00
|
|
|
|
|
|
|
|
peerConnection.onRemoveStream = (stream) {};
|
|
|
|
|
|
|
|
|
|
peerConnection.onDataChannel = (channel) {};
|
|
|
|
|
|
|
|
|
|
// This will find the intersection of my candidates and the remote,
|
|
|
|
|
// then propose one to use
|
2023-09-28 20:35:50 -07:00
|
|
|
var offer = await peerConnection.createOffer();
|
|
|
|
|
await peerConnection.setLocalDescription(offer);
|
2023-09-17 15:45:28 -07:00
|
|
|
// Send offer through signaling server
|
|
|
|
|
logger.i("Offer is $offer");
|
2023-09-28 20:35:50 -07:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-09-17 15:45:28 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2023-09-28 20:35:50 -07:00
|
|
|
return Column(children: [
|
|
|
|
|
Text(widget.cameraID.id),
|
|
|
|
|
Text(statusLine),
|
|
|
|
|
SizedBox(
|
|
|
|
|
height: 480,
|
|
|
|
|
child: _ready
|
|
|
|
|
? RTCVideoView(_remoteRenderer)
|
|
|
|
|
: const Text("Loading...")),
|
|
|
|
|
]);
|
2023-09-17 15:45:28 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-28 20:35:50 -07:00
|
|
|
class CallCancelled {}
|