flutter_webrtc: Cannot rejoin to the channel I left earlier - flutter

I am developing a streaming application: one application (server) broadcasts a video, and another application (client) displays this video. I used the flutter_webrtc package for real-time communication. I followed the following tutroial:
https://www.youtube.com/watch?v=hAKQzNQmNe0
https://github.com/md-weber/webrtc_tutorial/
Currently, the server application can successfully create a channel and broadcast the video, and the client application can join this channel and watch the video. But when the client leaves the channel and later tries to connect to the same channel, it can't get the video, only a black screen is displayed. There are no errors shown.
I used flutter_riverpod as state management and all codes below are inside StateNotifiers.
Function to create a channel from server application side.
Future<String> startStream(RTCVideoRenderer localVideo) async {
state = ProcessState.loading;
final stream = await navigator.mediaDevices.getUserMedia(<String, dynamic>{
'video': true,
'audio': true,
});
localVideo.srcObject = stream;
localStream = stream;
final roomId = await _createRoom();
await Wakelock.enable();
state = ProcessState.working;
return roomId;
}
Future<String> _createRoom() async {
final db = FirebaseFirestore.instance;
final roomRef = db.collection('rooms').doc();
peerConnection = await createPeerConnection(configuration);
localStream?.getTracks().forEach((track) {
peerConnection?.addTrack(track, localStream!);
});
// Code for collecting ICE candidates below
final callerCandidatesCollection = roomRef.collection('callerCandidates');
peerConnection?.onIceCandidate = (RTCIceCandidate candidate) {
callerCandidatesCollection.add(candidate.toMap() as Map<String, dynamic>);
};
// Add code for creating a room
final offer = await peerConnection!.createOffer();
await peerConnection!.setLocalDescription(offer);
final roomWithOffer = <String, dynamic>{'offer': offer.toMap()};
await roomRef.set(roomWithOffer);
roomId = roomRef.id;
// Listening for remote session description below
roomRef.snapshots().listen((snapshot) async {
final data = snapshot.data();
if (peerConnection?.getRemoteDescription() != null && data != null && data['answer'] != null){
final answer = data['answer'] as Map<String, dynamic>;
final description = RTCSessionDescription(
answer['sdp'] as String?,
answer['type'] as String?,
);
await peerConnection?.setRemoteDescription(description);
}
});
// Listen for remote Ice candidates below
roomRef.collection('calleeCandidates').snapshots().listen((snapshot) {
for (final change in snapshot.docChanges) {
if (change.type == DocumentChangeType.added) {
final data = change.doc.data();
peerConnection!.addCandidate(
RTCIceCandidate(
data?['candidate'] as String?,
data?['sdpMid'] as String?,
data?['sdpMLineIndex'] as int?,
),
);
}
}
});
return roomId!;
}
Function to join a channel from client application side.
Future<bool> startStream(String? roomId, RTCVideoRenderer remoteVideo) async {
if (roomId == null || roomId.isEmpty) {
return false;
}
state = ProcessState.loading;
final result = await _joinRoom(roomId, remoteVideo);
if (result) {
state = ProcessState.working;
await Wakelock.enable();
} else {
state = ProcessState.notInitialized;
}
return result;
}
Future<bool> _joinRoom(String roomId, RTCVideoRenderer remoteVideo) async {
final db = FirebaseFirestore.instance;
final DocumentReference roomRef = db.collection('rooms').doc(roomId);
final roomSnapshot = await roomRef.get();
if (roomSnapshot.exists) {
peerConnection = await createPeerConnection(configuration);
peerConnection?.onAddStream = (MediaStream stream) {
onAddRemoteStream?.call(stream);
remoteStream = stream;
};
// Code for collecting ICE candidates below
final calleeCandidatesCollection = roomRef.collection('calleeCandidates');
peerConnection?.onIceCandidate = (RTCIceCandidate candidate) {
final candidateMap = candidate.toMap() as Map<String, dynamic>;
calleeCandidatesCollection.add(candidateMap);
};
peerConnection?.onTrack = (RTCTrackEvent event) {
event.streams[0].getTracks().forEach((track) {
remoteStream?.addTrack(track);
});
};
// Code for creating SDP answer below
final data = roomSnapshot.data() as Map<String, dynamic>?;
final offer = data?['offer'] as Map<String, dynamic>?;
await peerConnection?.setRemoteDescription(
RTCSessionDescription(
offer?['sdp'] as String?,
offer?['type'] as String?,
),
);
final answer = await peerConnection!.createAnswer();
await peerConnection!.setLocalDescription(answer);
final roomWithAnswer = <String, dynamic>{
'answer': {
'type': answer.type,
'sdp': answer.sdp,
}
};
await roomRef.update(roomWithAnswer);
// Listening for remote ICE candidates below
roomRef.collection('callerCandidates').snapshots().listen((snapshot) {
for (final document in snapshot.docChanges) {
final data = document.doc.data();
peerConnection!.addCandidate(
RTCIceCandidate(
data?['candidate'] as String?,
data?['sdpMid'] as String?,
data?['sdpMLineIndex'] as int?,
),
);
}
});
this.roomId = roomId;
return true;
}
return false;
}

After some research, I found that my problem is the same as in the question Failed to set remote answer sdp: Called in wrong state: stable. This was caused by the fact that one RTCPeerConnection can only establish one peer-to-peer connection. So I could fix this by creating a new RTCPeerConnection on the server side every time a new client wants to join the channel.
Future<String> _createRoom() async {
final db = FirebaseFirestore.instance;
final roomRef = db.collection('rooms').doc();
await newPeerConnection(roomRef);
roomId = roomRef.id;
// Listening for remote session description below
roomRef.snapshots().listen((snapshot) async {
final data = snapshot.data();
if (data != null && data['answer'] != null) {
final answer = data['answer'] as Map<String, dynamic>;
final description = RTCSessionDescription(
answer['sdp'] as String?,
answer['type'] as String?,
);
await peerConnectionList.last.setRemoteDescription(description);
await newPeerConnection(roomRef);
}
});
// Listen for remote Ice candidates below
roomRef.collection('calleeCandidates').snapshots().listen((snapshot) {
for (final change in snapshot.docChanges) {
if (change.type == DocumentChangeType.added) {
final data = change.doc.data();
peerConnectionList.last.addCandidate(
RTCIceCandidate(
data?['candidate'] as String?,
data?['sdpMid'] as String?,
data?['sdpMLineIndex'] as int?,
),
);
}
}
});
return roomId!;
}
Future<void> newPeerConnection(DocumentReference roomRef) async {
final peerConnection = await createPeerConnection(
configuration,
offerSdpConstraints,
);
_registerPeerConnectionListeners(peerConnection);
localStream?.getTracks().forEach((track) {
peerConnection.addTrack(track, localStream!);
});
final offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
final roomWithOffer = <String, dynamic>{'offer': offer.toMap()};
await roomRef.set(roomWithOffer);
peerConnectionList.add(peerConnection);
}

Related

how to enable ear speaker instead of loud speaker in flutter

I had done with calling peer to peer in flutter but there is a problem how can I enable ear speaker instead of the loudspeaker in a flutter. Please help me to do this,
while establishing calling from one device to another device it only enables the loudspeaker instead of ear speaker. Thanks in advance.
Future<void> joinRoom(String roomId, RTCVideoRenderer remoteVideo) async {
FirebaseFirestore db = FirebaseFirestore.instance;
DocumentReference roomRef = db.collection('rooms').doc('$roomId');
var roomSnapshot = await roomRef.get();
log('Got room ${roomSnapshot.exists}');
if (roomSnapshot.exists) {
log('Create PeerConnection with configuration: $configuration');
peerConnection = await createPeerConnection(configuration);
registerPeerConnectionListeners();
localStream?.getTracks()?.forEach((track) {
peerConnection?.addTrack(track, localStream);
});
// Code for collecting ICE candidates below
var calleeCandidatesCollection = roomRef.collection('calleeCandidates');
peerConnection.onIceCandidate = (RTCIceCandidate candidate) {
if (candidate == null) {
log('onIceCandidate: complete!');
return;
}
log('onIceCandidate: ${candidate.toMap()}');
calleeCandidatesCollection.add(candidate.toMap());
};
// Code for collecting ICE candidate above
peerConnection?.onTrack = (RTCTrackEvent event) {
log('Got remote track: ${event.streams[0]}');
event.streams[0].getTracks().forEach((track) {
log('Add a track to the remoteStream: $track');
remoteStream?.addTrack(track);
});
};
// Code for creating SDP answer below
var data = roomSnapshot.data();
log('Got offer $data');
var offer = data['offer'];
await peerConnection?.setRemoteDescription(
RTCSessionDescription(offer['sdp'], offer['type']),
);
var answer = await peerConnection.createAnswer();
log('Created Answer $answer');
await peerConnection.setLocalDescription(answer);
Map<String, dynamic> roomWithAnswer = {
'answer': {'type': answer.type, 'sdp': answer.sdp}
};
await roomRef.update(roomWithAnswer);
// Finished creating SDP answer
// Listening for remote ICE candidates below
roomRef.collection('callerCandidates').snapshots().listen((snapshot) {
snapshot.docChanges.forEach((document) {
var data = document.doc.data();
// log(data);
log('Got new remote ICE candidate: $data');
peerConnection.addCandidate(
RTCIceCandidate(
data['candidate'],
data['sdpMid'],
data['sdpMLineIndex'],
),
);
});
});
}
}
Future<void> openUserMedia(RTCVideoRenderer localVideo,
RTCVideoRenderer remoteVideo, BuildContext contextt) async {
context = contextt;
chatProvider = Provider.of<ChatProvider>(context, listen: false);
var stream = await navigator.mediaDevices.getUserMedia({'audio': true});
localVideo.srcObject = stream;
localStream = stream;
remoteVideo.srcObject = await createLocalMediaStream('key');
}

AlarmManager not starting when app is removed from the background

I am trying to get the location data on some particular time. On every time we want to fetch the location data, we are sending a silent notification to our app. When app sees a silent notification It tries to get the data and upload back to the server. Every thing is working fine when app is running in foreground, but when i close the app and clear from the background, alarm manager is not getting fired to fetch and save the location.
_firebaseMessaging.configure(
onMessage: (
Map<String, dynamic> message,
) async {
final Map data =
message.containsKey('data') ? message['data'] as Map : null;
if (data?.isEmpty ?? false) {
_handleNotification(message);
} else {
_handleSilentEvent(message); // This method will be called when server will make call to fetch location data
}
},
onResume: (Map<String, dynamic> message) async {},
onLaunch: (Map<String, dynamic> message) async {},
onBackgroundMessage: _handleSilentEvent,
);
Future<dynamic> _handleSilentEvent(
final Map<String, dynamic> message,
) async {
final Map data = message.containsKey('data') ? message['data'] as Map : null;
if (data?.isNotEmpty ?? false) {
if (data.containsKey('getLocation')) {
await LocationService.triggerServiceToGetAndSaveLocation();
} else {
_handleWebEngage(data);
}
}
}
abstract class LocationService {
static const int _alarmManagerId = 1001;
static final ILocationFacade _locationFacade = getIt<ILocationFacade>();
LocationService._();
static Future<void> triggerServiceToGetAndSaveLocation() async {
AndroidAlarmManager.oneShotAt(
DateTime.now(),
_alarmManagerId,
LocationService._fetchAndSaveLocation,
wakeup: true,
exact: true,
);
}
static Future<void> _fetchAndSaveLocation() async {
final position = await _getCurrentLocation();
final capturedAt = DateTime.now().millisecondsSinceEpoch.toString();
final gpsEnabled = await Geolocator.isLocationServiceEnabled();
final LocationInfo locationInfo = LocationInfo(
lat: position.latitude,
lng: position.longitude,
capturedAt: capturedAt,
gpsEnabled: gpsEnabled,
);
print(
'onMessage:: Lat: ${position.latitude} Long: ${position.longitude} capturedAt: $capturedAt gpsEnabled: $gpsEnabled'); // This print is getting called when app is in foreground, when i kill the app this print is not event getting called.
// await _locationFacade.saveLocationInfo(locationInfo);
}
static Future<Position> _getCurrentLocation() async {
final _gpsServiceEnabled = await Geolocator.isLocationServiceEnabled();
if (_gpsServiceEnabled) {
return Geolocator.getCurrentPosition();
}
return Geolocator.getLastKnownPosition();
}
}
Can someone point me out where i am making the mistake?

Flutter how to check if product is already added to the basket

I am trying to check if the product already added to the basket and give the message with snackbar "This product already added". So user can not add the same product to the cart again.
here is the code of mine :
//Button calling controller adding item to cart
InkWell(
onTap:(){
_con.addItemToCart(_con.product);
});
//Controller adding item to cart
Future addItemToCart(Product product, {bool reset = false}) async {
setState(() {
this.loadCart = true;
});
Cart value =
await addToCart(product, scaffoldKey, context, quantity: quantity);
setState(() {
this.cart = value;
this.loadCart = false;
this.totalProductsInStock = this.product.availability;
this.totalProductsInStock -= this.totalProductsInStock - this.quantity ;
});
}
Future<Cart> addToCart(Product product, GlobalKey<ScaffoldState> scaffoldKey, BuildContext buildContext, {bool reset = false, int quantity = 1}) async {
var _cartPoduct = new CartProduct();
_cartPoduct.productId = product.id;
_cartPoduct.optionId = product.optionId;
_cartPoduct.quantity = quantity;
Cart value = await CartRepository.addCart(_cartPoduct, reset);
return value;
}
//Request
static Future<Cart> addCart(CartProduct cartProduct, bool reset) async {
User _user = userRepo.currentUser.value;
if (_user.apiToken == null) {
return new Cart();
}
Map<String, dynamic> decodedJSON = {};
cartProduct.userId = _user.id;
final String url =
'${GlobalConfiguration().getString('api_base_url')}';
final client = new http.Client();
final response = await client
.get(url, headers: {HttpHeaders.contentTypeHeader: 'application/json'});
try {
decodedJSON = json.decode(response.body)['data'] as Map<String, dynamic>;
} on FormatException catch (e) {
}
return Cart.fromJSON(decodedJSON);
}
I created a temporary list to check if the product has been added to the cart, and I have it checked with provider. You can check it maybe it will be useful for you
link

Flutter Riverpod : How to Implement FutureProvider?

I using Flutter Riverpod package to handling http request. I have simple Http get request to show all user from server, and i using manage it using FutureProvider from Flutter Riverpod package.
API
class UserGoogleApi {
Future<List<UserGoogleModel>> getAllUser() async {
final result = await reusableRequestServer.requestServer(() async {
final response =
await http.get('${appConfig.baseApiUrl}/${appConfig.userGoogleController}/getAllUser');
final Map<String, dynamic> responseJson = json.decode(response.body);
if (responseJson['status'] == 'ok') {
final List list = responseJson['data'];
final listUser = list.map((e) => UserGoogleModel.fromJson(e)).toList();
return listUser;
} else {
throw responseJson['message'];
}
});
return result;
}
}
User Provider
class UserProvider extends StateNotifier<UserGoogleModel> {
UserProvider([UserGoogleModel state]) : super(UserGoogleModel());
Future<UserGoogleModel> searchUserByIdOrEmail({
String idUser,
String emailuser,
String idOrEmail = 'email_user',
}) async {
final result = await _userGoogleApi.getUserByIdOrEmail(
idUser: idUser,
emailUser: emailuser,
idOrEmail: idOrEmail,
);
UserGoogleModel temp;
for (var item in result) {
temp = item;
}
state = UserGoogleModel(
idUser: temp.idUser,
createdDate: temp.createdDate,
emailUser: temp.emailUser,
imageUser: temp.emailUser,
nameUser: temp.nameUser,
tokenFcm: temp.tokenFcm,
listUser: state.listUser,
);
return temp;
}
Future<List<UserGoogleModel>> showAllUser() async {
final result = await _userGoogleApi.getAllUser();
state.listUser = result;
return result;
}
}
final userProvider = StateNotifierProvider((ref) => UserProvider());
final showAllUser = FutureProvider.autoDispose((ref) async {
final usrProvider = ref.read(userProvider);
final result = await usrProvider.showAllUser();
return result;
});
After that setup, i simply can call showAllUser like this :
Consumer((ctx, read) {
final provider = read(showAllUser);
return provider.when(
data: (value) {
return ListView.builder(
itemCount: value.length,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
final result = value[index];
return Text(result.nameUser);
},
);
},
loading: () => const CircularProgressIndicator(),
error: (error, stackTrace) => Text('Error $error'),
);
}),
it's no problem if http request don't have required parameter, but i got problem if my http request required parameter. I don't know how to handle this.
Let's say , i have another http get to show specific user from id user or email user. Then API look like :
API
Future<List<UserGoogleModel>> getUserByIdOrEmail({
#required String idUser,
#required String emailUser,
#required String idOrEmail,
}) async {
final result = await reusableRequestServer.requestServer(() async {
final baseUrl =
'${appConfig.baseApiUrl}/${appConfig.userGoogleController}/getUserByIdOrEmail';
final chooseURL = idOrEmail == 'id_user'
? '$baseUrl?id_or_email=$idOrEmail&id_user=$idUser'
: '$baseUrl?id_or_email=$idOrEmail&email_user=$emailUser';
final response = await http.get(chooseURL);
final Map<String, dynamic> responseJson = json.decode(response.body);
if (responseJson['status'] == 'ok') {
final List list = responseJson['data'];
final listUser = list.map((e) => UserGoogleModel.fromJson(e)).toList();
return listUser;
} else {
throw responseJson['message'];
}
});
return result;
}
User Provider
final showSpecificUser = FutureProvider.autoDispose((ref) async {
final usrProvider = ref.read(userProvider);
final result = await usrProvider.searchUserByIdOrEmail(
idOrEmail: 'id_user',
idUser: usrProvider.state.idUser, // => warning on "state"
);
return result;
});
When i access idUser from userProvider using usrProvider.state.idUser , i got this warning.
The member 'state' can only be used within instance members of subclasses of 'package:state_notifier/state_notifier.dart'.
It's similiar problem with my question on this, but on that problem i already know to solved using read(userProvider.state) , but in FutureProvider i can't achieved same result using ref(userProvider).
I missed something ?
Warning: This is not a long-term solution
Assuming that your FutureProvider is being properly disposed after each use that should be a suitable workaround until the new changes to Riverpod are live. I did a quick test to see and it does work. Make sure you define a getter like this and don't override the default defined by StateNotifier.
class A extends StateNotifier<B> {
...
static final provider = StateNotifierProvider((ref) => A());
getState() => state;
...
}
final provider = FutureProvider.autoDispose((ref) async {
final a = ref.read(A.provider);
final t = a.getState();
print(t);
});
Not ideal but seems like a fine workaround. I believe the intention of state being inaccessible outside is to ensure state manipulations are handled by the StateNotifier itself, so using a getter in the meantime wouldn't be the end of the world.

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.