Files
home-sensors/ui/lib/call.dart
T
2024-01-15 22:44:05 -08:00

206 lines
6.1 KiB
Dart

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<Call> {
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 _createSession();
}
_createSession() async {
var callOptions = CallOptions(metadata: {
'Authorization': await widget.sessionService.getAuthToken(widget.home)
});
var cancelCreate = Completer();
var sendIceCandidates = 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 {
await sendIceCandidates.future;
logger.i("Sending ICE candidate");
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 = "$state";
setState(() {});
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
await widget.client.createIceMessage(
pb.CreateIceMessageRequest(
sessionIdentifier: clientSession.id,
iceMessage: pb.IceMessage(
session: pb.IceSessionDescription(
sdp: offer.sdp,
sdpType: Int64(1), // offer
),
),
),
options: callOptions);
// Expect back a response
var someResponse = await Future.any([
widget.client.popIceMessage(
pb.PopIceMessageRequest(sessionIdentifier: clientSession.id),
options: callOptions),
cancelCreate.future,
]);
var resp = someResponse as pb.IceMessage;
var session = resp.session;
await peerConnection
.setRemoteDescription(RTCSessionDescription(session.sdp, "answer"));
sendIceCandidates.complete();
// 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");
break;
}
}
}
@override
Widget build(BuildContext context) {
return Column(children: [
Text(widget.cameraID.id),
Text(statusLine),
SizedBox(
height: 320,
child: _ready
? RTCVideoView(_remoteRenderer)
: const Text("Loading...")),
]);
}
}
class CallCancelled {}