"Unable to RTCPeerConnection::setRemoteDescription: Failed to set remote answer sdp: Called in wrong state: kStable" - flutter

I am using flutter_webrtc to connect to users peer to peer. I am getting this error on the offer side. I have been looking all over the internet for a fix but couldn't find the solution.
class Webrtc {
bool _offer = false;
RTCPeerConnection _peerConnection;
MediaStream _localStream;
RTCVideoRenderer _localRenderer = new RTCVideoRenderer();
RTCVideoRenderer _remoteRenderer = new RTCVideoRenderer();
get localRe
nderer => _localRenderer;
get remoteRenderer => _remoteRenderer;
//final sdpController = TextEditingController();
Webrtc() {
initRenderers();
_createPeerConnection().then((pc) {
_peerConnection = pc;
});
}
initRenderers() async {
await _localRenderer.initialize();
await _remoteRenderer.initialize();
}
createOffer() async {
_offer = true;
RTCSessionDescription description =
await _peerConnection.createOffer({'offerToReceiveVideo': 1});
// var session = parse(description.sdp);
// print(json.encode(session));
// _offer = true;
var roomDef = Firestore.instance.collection("rooms").document("test");
var data = {
"offer": {
'sdp': description.sdp.toString(),
'type': description.type.toString(),
}
};
await roomDef.setData(data, merge: true);
await _peerConnection.setLocalDescription(description);
Firestore.instance.collection("rooms").document("test").snapshots().listen((event) {
if(event.data["answer"] != null){
_setRemoteDescription(event.data["answer"]);
}
});
}
createAnswer() async {
// Firestore.instance.collection("rooms").document("test").snapshots().listen((event) {
// if(event.data["offer"] != null){
// _setRemoteDescription(event.data["offer"]);
// }
// });
var doc = await Firestore.instance.collection("rooms").document("test").get();
print(doc.data["offer"]);
await _setRemoteDescription(doc.data["offer"]);
RTCSessionDescription description =
await _peerConnection.createAnswer({'offerToReceiveVideo': 1});
//var session = parse(description.sdp);
//print(json.encode(session));
await _peerConnection.setLocalDescription(description);
var data = {
"answer": {
'sdp': description.sdp.toString(),
'type': description.type.toString(),
}
};
Firestore.instance
.collection("rooms")
.document("test")
.setData(data, merge: true);
}
_setRemoteDescription(doc) async {
// String jsonString = doc.toString();
// dynamic session = await jsonDecode('$jsonString');
//String sdp = write(session, null);
// RTCSessionDescription description =
// new RTCSessionDescription(session['sdp'], session['type']);
RTCSessionDescription description =
new RTCSessionDescription(doc["sdp"],doc["type"]);
print(description.toMap());
await _peerConnection.setRemoteDescription(description);
}
void _addCandidate(data) async {
dynamic session = data;
dynamic candidate = new RTCIceCandidate(
session['candidate'], session['sdpMid'], session['sdpMlineIndex']);
await _peerConnection.addCandidate(candidate);
}
_createPeerConnection() async {
Map<String, dynamic> configuration = {
"iceServers": [
{"url": "stun:stun.l.google.com:19302"},
]
};
final Map<String, dynamic> offerSdpConstraints = {
"mandatory": {
"OfferToReceiveAudio": true,
"OfferToReceiveVideo": true,
},
"optional": [],
};
_localStream = await _getUserMedia();
RTCPeerConnection pc =
await createPeerConnection(configuration, offerSdpConstraints);
// if (pc != null) print(pc);
pc.addStream(_localStream);
pc.onIceCandidate = (e) {
if (_offer && e.candidate != null) {
Firestore.instance.collection("caller").add({
'candidate': e.candidate.toString(),
'sdpMid': e.sdpMid.toString(),
'sdpMlineIndex': e.sdpMlineIndex,
});
Firestore.instance.collection("callee").snapshots().listen((event) {
event.documentChanges.forEach((element) {
print(element.document.data);
_addCandidate(element.document.data);
});
});
}
if (!_offer && e.candidate != null) {
Firestore.instance.collection("callee").add({
'candidate': e.candidate.toString(),
'sdpMid': e.sdpMid.toString(),
'sdpMlineIndex': e.sdpMlineIndex,
});
Firestore.instance.collection("caller").snapshots().listen((event) {
event.documentChanges.forEach((element) {
print(element.document.data);
_addCandidate(element.document.data);
});
});
}
// if (e.candidate != null) {
// print(json.encode({
// 'candidate': e.candidate.toString(),
// 'sdpMid': e.sdpMid.toString(),
// 'sdpMlineIndex': e.sdpMlineIndex,
// }));
// }
};
pc.onIceConnectionState = (e) {
print(e);
};
pc.onAddStream = (stream) {
print('addStream: ' + stream.id);
_remoteRenderer.srcObject = stream;
};
return pc;
}
_getUserMedia() async {
final Map<String, dynamic> mediaConstraints = {
'audio': true,
'video': {
'facingMode': 'user',
},
};
MediaStream stream = await navigator.getUserMedia(mediaConstraints);
// _localStream = stream;
_localRenderer.srcObject = stream;
_localRenderer.mirror = true;
// _peerConnection.addStream(stream);
return stream;
}
}
I tried switching the setlocaldescption and setremotedescption but no use. Offer button calls create offer from the ui and answer button calls createanswer function.

I cannot comment yet under your post but I had this problem with the newest version of flutter_webrtc. Three days of head-scratching later and I used:
flutter_webrtc: ^0.2.7
Worked on the first try. Cheers.

Related

flutter google_speach response stream not working

final serviceAccount = ServiceAccount.fromString(r'''{
Json private keys
}''');
final speechToText = SpeechToText.viaServiceAccount(serviceAccount);
final config = RecognitionConfig(
encoding: AudioEncoding.LINEAR16,
model: RecognitionModel.basic,
enableAutomaticPunctuation: true,
sampleRateHertz: 16000,
languageCode: 'en-US');
final streamingConfig =
StreamingRecognitionConfig(config: config, interimResults: true);
Stream<List<int>> stream =
(await MicStream.microphone(sampleRate: 16000)) as Stream<List<int>>;
final responseStream =
speechToText.streamingRecognize(streamingConfig, stream);
if (_isListining) {
setState(() {
_isListining = false;
});
} else {
setState(() {
_isListining = true;
print('hello');
try {
responseStream.listen((data) {
print(data.results.length);
setState(() {
_sendController.text = data.results
.map((e) => e.alternatives.first.transcript)
.join('/n');
});
print(content);
}, onDone: () {
setState(() {
setState(() {
_isListining = false;
});
});
}, onError: (e) {
print('errorr : ' + e);
setState(() {
_isListining = false;
});
});
print('streaming');
} catch (e) {
print('not streaming');
print(e);
}
});
}
linke to packages used
https://pub.dev/packages/google_speech
https://pub.dev/packages/mic_stream
so the problem is that microphone streaming is working fine but responseStream from google apis not printing or doing anything
after reading the docs
found this
https://cloud.google.com/speech-to-text/docs/troubleshooting#returns_an_empty_response
and i dont know if it is the problem or not

Flutter can't restore purchases

This was working already but I updated package to latest version:
https://pub.dev/packages/in_app_purchase
I can make the payment and check what I bought but after refresh it doesn't restore this. If I try to buy the same version again it says that I already own it:
Future<void> initStore() async {
final bool isAvailableTemp = await inAppPurchase.isAvailable();
if (!isAvailable) {
isAvailable = isAvailableTemp;
products = [];
purchases = [];
notFoundIds = [];
purchasePending = false;
loading = false;
return;
}
if (Platform.isIOS) {
final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition =
inAppPurchase
.getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>();
await iosPlatformAddition.setDelegate(PaymentQueueDelegate());
}
final ProductDetailsResponse productDetailResponse =
await inAppPurchase.queryProductDetails(_androidProductIds.toSet());
if (productDetailResponse.error != null) {
queryProductError = productDetailResponse.error!.message;
isAvailable = isAvailable;
products = productDetailResponse.productDetails;
purchases = <PurchaseDetails>[];
notFoundIds = productDetailResponse.notFoundIDs;
purchasePending = false;
loading = false;
return;
}
}
Future<List<ProductDetails>> getProducts() async {
if (products.isEmpty) {
final ProductDetailsResponse products =
await inAppPurchase.queryProductDetails(
Platform.isAndroid
? _androidProductIds.toSet()
: _iOSProductIds.toSet(),
);
if (products.productDetails.isNotEmpty) {
// double check the order of the products
if (Helpers.containsInStringIgnoreCase(
products.productDetails[0].title, "plus")) {
this.products.addAll(products.productDetails);
} else {
// pro comes first
this.products.add(products.productDetails[1]);
this.products.add(products.productDetails[0]);
}
}
}
return products;
}
Future<List<PurchaseDetails>> getPastPurchases(BuildContext context) async {
if (purchases.isEmpty) {
final Stream<List<PurchaseDetails>> purchaseUpdated =
inAppPurchase.purchaseStream;
_subscription = purchaseUpdated.listen((purchaseDetailsList) {
if (purchaseDetailsList.isEmpty) {
detectProVideo(context);
} else {
// this.purchases.addAll(purchaseDetailsList);
listenToPurchaseUpdated(context, purchaseDetailsList);
}
_subscription.cancel();
}, onDone: () {
_subscription.cancel();
}, onError: (error) {
// handle error here.
_subscription.cancel();
});
await inAppPurchase.restorePurchases();
if (Platform.isIOS) {
Map<String, PurchaseDetails> purchases =
Map.fromEntries(this.purchases.map((PurchaseDetails purchase) {
if (purchase.pendingCompletePurchase) {
inAppPurchase.completePurchase(purchase);
}
return MapEntry<String, PurchaseDetails>(
purchase.productID, purchase);
}));
if (purchases.isEmpty) {
Provider.of<AdState>(context, listen: false).toggleAds(context, true);
} else {
Provider.of<AdState>(context, listen: false)
.toggleAds(context, false);
}
}
}
return purchases;
}
So this is always true:
if (purchaseDetailsList.isEmpty) {
Testing locally on an Android emulator

I am having problem with onSelect notifcation webengage via FCM notification while onBackground state(onPaused) for my Flutter Application

TL;DR
I am receiving notification on all state(foreground, background(on-Paused), terminated(on-Detached)) but it's redirecting me to the intended url only in(foreground and terminated state). Surprisingly, on receiving notification during foreground state, on-select notification works on-Background state(on-Paused) as well and I am redirected to my intended url. But the main problem is while recieving notification on-background state(on-Paused) without receiving notification on foreground at first, it just redirects me to where I was. Here is the code I am currently working on:
void afterInitFirebase() async {
NotificationSettings settings = await _firebaseMessaging.requestPermission(
alert: true,
announcement: false,
badge: true,
carPlay: false,
criticalAlert: false,
provisional: false,
sound: true,
);
await _firebaseMessaging
.subscribeToTopic(topic)
.onError((error, stackTrace) => {print(error)});
await _firebaseMessaging.getToken().then((value) => {
Preference.setString(fcm_token, value),
});
await _firebaseMessaging.setForegroundNotificationPresentationOptions(
alert: true,
badge: true,
sound: true,
);
await _firebaseMessaging.getInitialMessage().then((RemoteMessage? message) {
if (message != null) {
// _handleIncomingLinks();
MyNotification().initMessaging(message, isPush: true);
}
});
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
displayNotification(message);
});
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
print("${message.from} =--> ON MESSAGE OPENED APP");
});
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
HttpOverrides.global = MyHttpOverrides();
try {
await Firebase.initializeApp().then((value) {
afterInitFirebase();
});
} catch (e) {
print(
"EXCEPTION ON MAIN:" + e.toString(),
);
}
final NotificationAppLaunchDetails? notificationAppLaunchDetails =
await fitNotification.getNotificationAppLaunchDetails();
final uri = await getInitialUri();
String initialRoute = splash_page;
String id = "0";
if (notificationAppLaunchDetails!.didNotificationLaunchApp) {
selectedNotificationPayload = notificationAppLaunchDetails.payload;
print("payload $selectedNotificationPayload");
var parts = selectedNotificationPayload?.split(SEPARATOR);
if (parts != null) {
if (parts[0] == "course" ||
parts[0].toLowerCase() == "coursedetails" ||
parts[0].toLowerCase() == "coursedetails") {
id = parts[1];
//course details page
initialRoute = course_details;
} else if (parts[0].toLowerCase() == "allcourse") {
initialRoute = all_course;
if (parts.length > 1) {
id = parts[1];
}
print("payload: $initialRoute $id");
} else if (parts[0].toLowerCase() == "allplan") {
if (parts.length > 1) {
id = parts[1];
}
initialRoute = "/allPlans";
} else if (parts[0].toLowerCase() == "web") {
id = parts[1];
initialRoute = web_page;
} else if (parts[0].toLowerCase() == "plan") {
id = parts[1];
initialRoute = plans_details_page;
} else if (parts[0].toLowerCase() == "quiz") {
initialRoute = web_page_entrance;
} else if (parts[0].toLowerCase() == "wishlist") {
initialRoute = route_wish_list;
} else if (parts[0].toLowerCase() == "carts") {
initialRoute = my_carts;
} else {
initialRoute = notification_page;
}
}
}
if (uri == null) {
} else {
String path = uri.toString();
if (path.toLowerCase().contains("coursedetails") ||
path.toLowerCase().contains("/home/course")) {
String idStr = uri.path.substring(uri.path.lastIndexOf('/') + 1);
id = idStr;
initialRoute = course_details;
}
}
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
displayNotification(message);
}
void displayNotification(RemoteMessage message) {
log(message.data.toString());
MyNotification().initMessaging(message);
}
This is my MyNotification class:
const String SEPARATOR = "|";
class MyNotification {
void initMessaging(RemoteMessage message, {isPush: true}) async {
var androidInit = AndroidInitializationSettings('ic_notification');
var iosInit = IOSInitializationSettings();
var initSetting =
InitializationSettings(android: androidInit, iOS: iosInit);
await fitNotification.initialize(initSetting,
onSelectNotification: onSelectNotification);
var rand = new Random();
int id = 1;
String? title = "";
String? body = "";
String? icon = "";
String? type = "";
String? itemId = "";
String link = "";
if (message.notification != null) {
title = "${message.notification?.title}";
body = "${message.notification?.body}";
icon = "${message.notification?.android?.imageUrl}";
if (Platform.isAndroid) {
icon = "${message.notification?.android?.imageUrl}";
} else {
icon = "${message.notification?.apple?.imageUrl}";
}
}
if (message.data['source'] == "webengage") {
isPush = true;
Map<String, dynamic> messageData =
jsonDecode(message.data['message_data']);
if (messageData.containsKey("title")) {
title = messageData["title"];
body = messageData["message"];
}
if (messageData.containsKey("expandableDetails")) {
Map<String, dynamic> expDetail = messageData["expandableDetails"];
if (expDetail.containsKey("image")) {
icon = expDetail["image"];
}
if (expDetail.containsKey("style")) {
if (expDetail['style'] == "RATING_V1" ||
expDetail['style'] == "CAROUSEL_V1") {
isPush = false;
}
}
}
if (messageData.containsKey("custom")) {
List<dynamic> customData = messageData['custom'];
print("element1: ${customData.toString()}");
customData.forEach((element) {
Map<String, dynamic> maps = element;
var key = maps['key'];
var value = maps['value'];
if (key == "itemId") {
itemId = value;
}
if (key == "type") {
type = value;
}
});
}
} else {
if (message.data.containsKey("icon")) {
icon = message.data['icon'];
}
if (message.data.containsKey("title")) {
title = message.data['title'];
body = message.data['body'];
}
if (message.data.containsKey("type")) {
type = message.data['type'];
}
if (message.data.containsKey("itemId")) {
itemId = message.data["itemId"];
}
}
if (title?.isNotEmpty == true && body?.isNotEmpty == true) {
showNotification(rand.nextInt(1000), title, body, icon,
"${type}$SEPARATOR${itemId}$SEPARATOR${icon}",
isPush: isPush);
}
}
Future<Uint8List> _getByteArrayFromUrl(String url) async {
final http.Response response = await http.get(Uri.parse(url));
return response.bodyBytes;
}
Future<void> showNotification(int notificationId, String? notificationTitle,
String? notificationContent, String? icon, String payload,
{String channelId = '1234',
String channelTitle = 'Android Channel',
String channelDescription = 'Default Android Channel for notifications',
Priority notificationPriority = Priority.high,
Importance notificationImportance = Importance.max,
bool isPush = true}) async {
//with icon
if (icon != null && icon.isNotEmpty) {
final String bigPicturePath =
await _downloadAndSaveFile(icon, 'bigPicture.jpg');
final BigPictureStyleInformation bigPictureStyleInformation =
BigPictureStyleInformation(
FilePathAndroidBitmap(bigPicturePath),
largeIcon: FilePathAndroidBitmap(bigPicturePath),
);
var androidPlatformChannelSpecifics = new AndroidNotificationDetails(
channelId, channelTitle,
channelDescription: channelDescription,
playSound: false,
importance: notificationImportance,
priority: notificationPriority,
styleInformation: bigPictureStyleInformation,
icon: 'for_icon',
);
final IOSNotificationDetails iOSPlatformChannelSpecifics =
IOSNotificationDetails(attachments: <IOSNotificationAttachment>[
IOSNotificationAttachment(bigPicturePath)
]);
final MacOSNotificationDetails macOSPlatformChannelSpecifics =
MacOSNotificationDetails(attachments: <MacOSNotificationAttachment>[
MacOSNotificationAttachment(bigPicturePath)
]);
final NotificationDetails notificationDetails = NotificationDetails(
iOS: iOSPlatformChannelSpecifics,
macOS: macOSPlatformChannelSpecifics,
android: androidPlatformChannelSpecifics);
if (isPush) {
await fitNotification.show(
notificationId,
notificationTitle,
notificationContent,
notificationDetails,
payload: payload,
);
}
} else {
//with out icon
var androidPlatformChannelSpecifics = new AndroidNotificationDetails(
channelId,
channelTitle,
channelDescription: channelDescription,
playSound: false,
importance: notificationImportance,
priority: notificationPriority,
icon: 'for_icon',
);
final NotificationDetails platformChannelSpecifics =
NotificationDetails(android: androidPlatformChannelSpecifics);
if (isPush) {
await fitNotification.show(notificationId, notificationTitle,
notificationContent, platformChannelSpecifics,
payload: payload);
}
}
var parts = payload.split(SEPARATOR);
var now = new DateTime.now();
var formatter = new DateFormat('yyyy-MM-dd HH:mm:ss');
String formattedDate = formatter.format(now);
NotificationModelData model = NotificationModelData(
courseId: parts[1],
icon: "$icon",
title: notificationTitle.toString(),
description: notificationContent.toString(),
type: parts[0],
notifyTime: formattedDate,
isRead: false,
id: now.millisecondsSinceEpoch.toString());
var db = AppDatabase.instance;
db.into(db.notificationModel).insert(model).then(
(value) => print(value),
);
}
Future<String> _downloadAndSaveFile(String url, String fileName) async {
final Directory directory = await getApplicationDocumentsDirectory();
final String filePath = '${directory.path}/$fileName';
final http.Response response = await http.get(Uri.parse(url));
final File file = File(filePath);
await file.writeAsBytes(response.bodyBytes);
return filePath;
}
Future<dynamic> onSelectNotification(String? payload) async {
selectedNotificationPayload = payload;
var parts = payload!.split(SEPARATOR);
if (parts[0].toLowerCase() == "course") {
//course details page
Navigator.pushNamed(
navigatorKey.currentState!.overlay!.context, course_details,
arguments: <String, String>{
'course_id': parts[1],
'thumbnail': parts[2]
});
} else if (parts[0].toLowerCase() == "web") {
await Navigator.pushNamed(
navigatorKey.currentState!.overlay!.context, web_page,
arguments: <String, String>{'paymentUrl': "${parts[1]}"});
} else if (parts[0].toLowerCase() == "allcourse") {
Navigator.pushNamed(
navigatorKey.currentState!.overlay!.context, all_course,
arguments: <String, String>{'course_id': "${parts[1]}"});
} else if (parts[0].toLowerCase() == "plan") {
Navigator.pushNamed(
navigatorKey.currentState!.overlay!.context, plans_details_page,
arguments: <String, String>{'plan_id': "${parts[1]}"});
} else if (parts[0].toLowerCase() == "allplan") {
Navigator.pushNamed(
navigatorKey.currentState!.overlay!.context, all_plans,
arguments: <String, String>{'id': "${parts[1]}"});
} else if (parts[0].toLowerCase() == "quiz") {
Navigator.of(navigatorKey.currentState!.overlay!.context)
.pushNamed(web_page_entrance);
} else {
//notification page
await Navigator.pushNamed(
navigatorKey.currentState!.overlay!.context, notification_page);
}
}
This is my WebPage class:
var url = "";
class WebPage extends StatefulWidget {
void launchURL() async {
if (await canLaunch(url))
await launch(url);
else
throw "Could not launch $url";
}
#override
_WebPageState createState() => _WebPageState();
}
class _WebPageState extends State<WebPage>{
bool isLoading = true;
final _key = UniqueKey();
Map? _arguments;
var _webViewController;
#override
void initState() {
super.initState();
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
return widget.launchURL();
});
}
#override
Widget build(BuildContext context) {
_arguments = ModalRoute.of(context)!.settings.arguments as Map?;
if (_arguments?.containsKey("paymentUrl") == true) {
url = _arguments!["paymentUrl"];
} else if (_arguments?.containsKey("course_id") == true) {
url = _arguments!["course_id"];
} else {
print(url);
}
return Scaffold(
body: SplashPage(),
);
}
}
I hope I am being clear enough. I have been stuck for days now on this particular issue with it haunting me in my dreams too. Any help would be greatly appreciated.

Flutter WebRTC audio but no video

So I'm building a video calling application using flutter, flutterWeb, and the WebRTC package.
I have a spring boot server sitting in the middle to pass the messages between the two clients.
Each side shows the local video, but neither shows the remote. Audio does work though. I got som nasty feedback loops. Testing with headphones showed that audio does indeed work.
My singaling code
typedef void StreamStateCallback(MediaStream stream);
class CallingService {
String sendToUserId;
String currentUserId;
final String authToken;
final StompClient _client;
final StreamStateCallback onAddRemoteStream;
final StreamStateCallback onRemoveRemoteStream;
final StreamStateCallback onAddLocalStream;
RTCPeerConnection _peerConnection;
List<RTCIceCandidate> _remoteCandidates = [];
String destination;
var hasOffer = false;
var isNegotiating = false;
MediaStream _localStream;
final Map<String, dynamic> _constraints = {
'mandatory': {
'OfferToReceiveAudio': true,
'OfferToReceiveVideo': true,
},
'optional': [],
};
CallingService(
this._client,
this.sendToUserId,
this.currentUserId,
this.authToken,
this.onAddRemoteStream,
this.onRemoveRemoteStream,
this.onAddLocalStream) {
destination = '/app/start-call/$sendToUserId';
print("destination $destination");
_client.subscribe(
destination: destination,
headers: {'Authorization': "$authToken"},
callback: (StompFrame frame) => processMessage(jsonDecode(frame.body)));
}
Future<void> startCall() async {
await processRemoteStream();
RTCSessionDescription description =
await _peerConnection.createOffer(_constraints);
await _peerConnection.setLocalDescription(description);
var message = RtcMessage(RtcMessageType.OFFER, currentUserId, {
'description': {'sdp': description.sdp, 'type': description.type},
});
sendMessage(message);
}
Future<void> processMessage(Map<String, dynamic> messageJson) async {
var message = RtcMessage.fromJson(messageJson);
if (message.from == currentUserId) {
return;
}
print("processing ${message.messageType.toString()}");
switch (message.messageType) {
case RtcMessageType.BYE:
// TODO: Handle this case.
break;
case RtcMessageType.LEAVE:
// TODO: Handle this case.
break;
case RtcMessageType.CANDIDATE:
await processCandidate(message);
break;
case RtcMessageType.ANSWER:
await processAnswer(message);
break;
case RtcMessageType.OFFER:
await processOffer(message);
break;
}
}
Future<void> processCandidate(RtcMessage candidate) async {
Map<String, dynamic> map = candidate.data['candidate'];
var rtcCandidate = RTCIceCandidate(
map['candidate'],
map['sdpMid'],
map['sdpMLineIndex'],
);
if (_peerConnection != null) {
_peerConnection.addCandidate(rtcCandidate);
} else {
_remoteCandidates.add(rtcCandidate);
}
}
Future<void> processAnswer(RtcMessage answer) async {
if (isNegotiating) {
return;
}
isNegotiating = true;
var description = answer.data['description'];
if (_peerConnection == null) {
return;
}
await _peerConnection.setRemoteDescription(
RTCSessionDescription(description['sdp'], description['type']));
}
Future<void> processOffer(RtcMessage offer) async {
await processRemoteStream();
var description = offer.data['description'];
await _peerConnection.setRemoteDescription(
new RTCSessionDescription(description['sdp'], description['type']));
var answerDescription = await _peerConnection.createAnswer(_constraints);
await _peerConnection.setLocalDescription(answerDescription);
var answerMessage = RtcMessage(RtcMessageType.ANSWER, currentUserId, {
'description': {
'sdp': answerDescription.sdp,
'type': answerDescription.type
},
});
sendMessage(answerMessage);
if (_remoteCandidates.isNotEmpty) {
_remoteCandidates
.forEach((candidate) => _peerConnection.addCandidate(candidate));
_remoteCandidates.clear();
}
}
Future<void> processRemoteStream() async {
_localStream = await createStream();
_peerConnection = await createPeerConnection(_iceServers, _config);
_peerConnection.addStream(_localStream);
_peerConnection.onSignalingState = (state) {
//isNegotiating = state != RTCSignalingState.RTCSignalingStateStable;
};
_peerConnection.onAddStream = (MediaStream stream) {
this.onAddRemoteStream(stream);
};
_peerConnection.onRemoveStream =
(MediaStream stream) => this.onRemoveRemoteStream(stream);
_peerConnection.onIceCandidate = (RTCIceCandidate candidate) {
var data = {
'candidate': {
'sdpMLineIndex': candidate.sdpMlineIndex,
'sdpMid': candidate.sdpMid,
'candidate': candidate.candidate,
},
};
var message = RtcMessage(RtcMessageType.CANDIDATE, currentUserId, data);
sendMessage(message);
};
}
void sendMessage(RtcMessage message) {
_client.send(
destination: destination,
headers: {'Authorization': "$authToken"},
body: jsonEncode(message.toJson()));
}
Map<String, dynamic> _iceServers = {
'iceServers': [
{'urls': 'stun:stun.l.google.com:19302'},
/*
* turn server configuration example.
{
'url': 'turn:123.45.67.89:3478',
'username': 'change_to_real_user',
'credential': 'change_to_real_secret'
},
*/
]
};
final Map<String, dynamic> _config = {
'mandatory': {},
'optional': [
{'DtlsSrtpKeyAgreement': true},
],
};
Future<MediaStream> createStream() async {
final Map<String, dynamic> mediaConstraints = {
'audio': true,
'video': {
'mandatory': {
'minWidth': '640',
'minHeight': '480',
'minFrameRate': '30',
},
'facingMode': 'user',
'optional': [],
}
};
MediaStream stream = await navigator.getUserMedia(mediaConstraints);
if (this.onAddLocalStream != null) {
this.onAddLocalStream(stream);
}
return stream;
}
}
Here are my widgets
class _CallScreenState extends State<CallScreen> {
StompClient _client;
CallingService _callingService;
RTCVideoRenderer _localRenderer = new RTCVideoRenderer();
RTCVideoRenderer _remoteRenderer = new RTCVideoRenderer();
final UserService userService = GetIt.instance.get<UserService>();
void onConnectCallback(StompClient client, StompFrame connectFrame) async {
var currentUser = await userService.getCurrentUser();
_callingService = CallingService(
_client,
widget.intent.toUserId.toString(),
currentUser.id.toString(),
widget.intent.authToken,
onAddRemoteStream,
onRemoveRemoteStream,
onAddLocalStream);
if (widget.intent.initialMessage != null) {
_callingService.processMessage(jsonDecode(widget.intent.initialMessage));
} else {
_callingService.startCall();
}
}
void onAddRemoteStream(MediaStream stream) {
_remoteRenderer.srcObject = stream;
}
void onRemoveRemoteStream(MediaStream steam) {
_remoteRenderer.srcObject = null;
}
void onAddLocalStream(MediaStream stream) {
_localRenderer.srcObject = stream;
}
#override
void initState() {
super.initState();
_localRenderer.initialize();
_remoteRenderer.initialize();
_client = StompClient(
config: StompConfig(
url: 'ws://${DomainService.getDomainBase()}/stomp',
onConnect: onConnectCallback,
onWebSocketError: (dynamic error) => print(error.toString()),
stompConnectHeaders: {'Authorization': "${widget.intent.authToken}"},
onDisconnect: (message) => print("disconnected ${message.body}"),),
);
_client.activate();
}
#override
Widget build(BuildContext context) {
return PlatformScaffold(
pageTitle: "",
child: Flex(
direction: Axis.vertical,
children: [
Flexible(
flex: 1,
child: RTCVideoView(_localRenderer),
),
Flexible(
flex: 1,
child: RTCVideoView(_remoteRenderer),
)
],
),
);
}
}
I put a print statment in the the widget on the addRemoteStream callback, and it's getting called. So some kind of stream is being sent. I'm just not sure why the video isnt' showing.
So my problem was that I wasn't adding queued candidates to the caller.
I added
sendMessage(answerMessage);
if (_remoteCandidates.isNotEmpty) {
_remoteCandidates
.forEach((candidate) => _peerConnection.addCandidate(candidate));
_remoteCandidates.clear();
}
to the processAnswer method and it works just fine!

Flutter WebRTC Cant connect to peers. Failed to set remote answer sdp: Called in wrong state: kStable

So I'm using flutter (And flutter for web) to build a WebRTC client. I have a spring-boot server acting as the go-between for two clients. They both subscribe to a WebSocket to get messages from the other. It does nothing more than that.
I'm getting Error: InvalidStateError: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection': Failed to set remote answer sdp: Called in wrong state: kStable
I don't know why this error is happening.
Here's the code for the signalling
typedef void StreamStateCallback(MediaStream stream);
class CallingService {
String sendToUserId;
String currentUserId;
final String authToken;
final StompClient _client;
final StreamStateCallback onAddRemoteStream;
final StreamStateCallback onRemoveRemoteStream;
final StreamStateCallback onAddLocalStream;
RTCPeerConnection _peerConnection;
List<RTCIceCandidate> _remoteCandidates = [];
String destination;
var hasOffer = false;
var isNegotiating = false;
final Map<String, dynamic> _constraints = {
'mandatory': {
'OfferToReceiveAudio': true,
'OfferToReceiveVideo': true,
},
'optional': [],
};
CallingService(
this._client,
this.sendToUserId,
this.currentUserId,
this.authToken,
this.onAddRemoteStream,
this.onRemoveRemoteStream,
this.onAddLocalStream) {
destination = '/app/start-call/$sendToUserId';
print("destination $destination");
_client.subscribe(
destination: destination,
headers: {'Authorization': "$authToken"},
callback: (StompFrame frame) => processMessage(jsonDecode(frame.body)));
}
Future<void> startCall() async {
await processRemoteStream();
RTCSessionDescription description =
await _peerConnection.createOffer(_constraints);
await _peerConnection.setLocalDescription(description);
var message = RtcMessage(RtcMessageType.OFFER, currentUserId, {
'description': {'sdp': description.sdp, 'type': description.type},
});
sendMessage(message);
}
Future<void> processMessage(Map<String, dynamic> messageJson) async {
var message = RtcMessage.fromJson(messageJson);
if (message.from == currentUserId) {
return;
}
print("processing ${message.messageType.toString()}");
switch (message.messageType) {
case RtcMessageType.BYE:
// TODO: Handle this case.
break;
case RtcMessageType.LEAVE:
// TODO: Handle this case.
break;
case RtcMessageType.CANDIDATE:
await processCandidate(message);
break;
case RtcMessageType.ANSWER:
await processAnswer(message);
break;
case RtcMessageType.OFFER:
await processOffer(message);
break;
}
}
Future<void> processCandidate(RtcMessage candidate) async {
Map<String, dynamic> map = candidate.data['candidate'];
var rtcCandidate = RTCIceCandidate(
map['candidate'],
map['sdpMid'],
map['sdpMLineIndex'],
);
if (_peerConnection != null) {
_peerConnection.addCandidate(rtcCandidate);
} else {
_remoteCandidates.add(rtcCandidate);
}
}
Future<void> processAnswer(RtcMessage answer) async {
if (isNegotiating){
return;
}
var description = answer.data['description'];
await _peerConnection.setRemoteDescription(
RTCSessionDescription(description['sdp'], description['type']));
}
Future<void> processOffer(RtcMessage offer) async {
await processRemoteStream();
var description = offer.data['description'];
await _peerConnection.setRemoteDescription(
new RTCSessionDescription(description['sdp'], description['type']));
var answerDescription = await _peerConnection.createAnswer(_constraints);
await _peerConnection.setLocalDescription(answerDescription);
var answerMessage = RtcMessage(RtcMessageType.ANSWER, currentUserId, {
'description': {
'sdp': answerDescription.sdp,
'type': answerDescription.type
},
});
sendMessage(answerMessage);
if (_remoteCandidates.isNotEmpty) {
_remoteCandidates
.forEach((candidate) => _peerConnection.addCandidate(candidate));
_remoteCandidates.clear();
}
}
Future<void> processRemoteStream() async {
await createStream();
_peerConnection = await createPeerConnection(_iceServers, _config);
_peerConnection.onSignalingState = (state) {
isNegotiating = state != RTCSignalingState.RTCSignalingStateStable;
};
_peerConnection.onAddTrack = (MediaStream stream, _) {
this.onAddRemoteStream(stream);
print("sending stream from track");
};
_peerConnection.onAddStream = (MediaStream stream) {
this.onAddRemoteStream(stream);
print("sending stream");
};
_peerConnection.onRemoveStream =
(MediaStream stream) => this.onRemoveRemoteStream(stream);
_peerConnection.onIceCandidate = (RTCIceCandidate candidate) {
print("sending candidate");
var data = {
'candidate': {
'sdpMLineIndex': candidate.sdpMlineIndex,
'sdpMid': candidate.sdpMid,
'candidate': candidate.candidate,
},
};
var message = RtcMessage(RtcMessageType.CANDIDATE, currentUserId, data);
sendMessage(message);
};
}
void sendMessage(RtcMessage message) {
_client.send(
destination: destination,
headers: {'Authorization': "$authToken"},
body: jsonEncode(message.toJson()));
}
Map<String, dynamic> _iceServers = {
'iceServers': [
{"url" : "stun:stun2.1.google.com:19302"},
{'url' : 'stun:stun.l.google.com:19302'},
/*
* turn server configuration example.
{
'url': 'turn:123.45.67.89:3478',
'username': 'change_to_real_user',
'credential': 'change_to_real_secret'
},
*/
]
};
final Map<String, dynamic> _config = {
'mandatory': {},
'optional': [
{'DtlsSrtpKeyAgreement': true},
],
};
Future<MediaStream> createStream() async {
final Map<String, dynamic> mediaConstraints = {
'audio': true,
'video': {
'mandatory': {
'minWidth':
'640', // Provide your own width, height and frame rate here
'minHeight': '480',
'minFrameRate': '30',
},
'facingMode': 'user',
'optional': [],
}
};
MediaStream stream = await navigator.getUserMedia(mediaConstraints);
if (this.onAddLocalStream != null) {
this.onAddLocalStream(stream);
}
return stream;
}
}
My first problem was I was not setting the local description for the offer/answer stage.
However, when I add a new stun server, I get the same exception. Either way, I don't get a remote stream showing.
So when I was creating the offer and answer I wasn't setting local description. So there's that.
It's still not showing remote connections though.