How to detect network disconnects (on an RTCPeerConnection) as soon as possible or the resulting frozen video? - webrtc

I am using RTCPeerconnections to submit video and audio in a web-RTC-based video-messenger. I am able to detect network disconnects after approximately 7 seconds - yet, this is 7 seconds in which the user is staring at a frozen video and starts to randomly click buttons in the app. I would like to improve the user experience by shortening this timespan - e.g. by informing the user about a network issue if the video freezes for more than 1 second.
Status Quo: I am currently detecting respective situations by listening to the onconnectionstatechange event of the RTCPeerConnection. Yet, the event is only fired approximately 7 seconds after the disconnect. I determined the ~7 seconds by connecting two machines via normal WiFi, using a hardware switch on one of the laptops to switch off the wireless (such switches exist on some older Lenovo models / guarantee an immediate disconnect) and wait for the other machine to detect the event.
Consideration: The root cause being the interruption of the underlying network connection, it would be ideal to detect the changed network status as early as possible (even if its just transport delays). This said, the disturbance faced by the user ultimately stems from the video that instantly freezes when interrupting the network. If there was no way to detect the connection issue earlier, it could be an option to detect the frozen video instead. Is any of these two things possible (ideally event-driven, so that I don't need to poll things every second)?
Here's a very simple code snippet describing my current disconnect detection:
myRTCPeerConnection.onconnectionstatechange = (event: Event) => {
let newCS = myRTCPeerConnection.connectionState;
if (newCS == "disconnected" || newCS == "failed" || newCS == "closed") {
//do something on disconnect - e.g. show messages to user and start reconnect
}
}

(ice)connectionstatechange is the right event in general.
If you want more granularity you'll need to poll getStats and looks for stats like framesReceived. But there is no guaranteed frame rate sent from the other side (e.g. in screensharing you go below 1/s).
While the actual ICE statistics like requestsSent seem more useful they happen much less frequently, only once per second and you can loose a packet or it comes late.
In general this is a question of how reliable the detection of the network failure is. If it is too aggressive you end up with a poor UX showing a warning too often.
You might not end up that is significantly better than at the cost of introducing complexity that you need to maintain.

Thanks Philipp for your insights - this pointed me into the right direction.
I'm now looking into using getStats to identify any freezes. At first sight, polling the framesPerSecond value seems most promising to me. The good thing: it reacts instantly upon disconnect - and - it still works when the underlying video stream is paused (i'm allowing the user to pause video submission / implemented it by setting all video tracks to enabled = false). I.e. even if the video tracks are disabled on the sending side, the receiving side still continues to receive the agreed frames per second.
As the usage of the getStats function appears weak on documentation at the time of this being written / there's rarely a simple examples for its usage, please find my code extract below:
peerRTCPC
.getReceivers()
.forEach(
(
receiver: RTCRtpReceiver,
index: number,
array: RTCRtpReceiver[]
) => {
if (receiver.track.kind == "video") {
receiver.getStats().then((myStatsReport: RTCStatsReport) => {
myStatsReport.forEach(
(statValue: any, key: string, parent: RTCStatsReport) => {
if (statValue.type == "inbound-rtp") {
console.log(
"The PC stats returned the framesPerSecond value " +
statValue["framesPerSecond"] +
" while the full inbound-rtp stats reflect as " +
JSON.stringify(statValue)
);
}
}
);
});
}
}
);
Note that upon disconnect, the framesPerSecond do not necessarily go to zero, even though the webRTCInternals screen suggests the same. I am seeing undefined when a disconnect happens.
Runtime impact of polling this at high frequency / across larger numbers of connections probably needs to be looked at more closely. Yet, this seems like a good step into the right direction unless doing it way to frequently.

Related

webrtc connection gets disconnected but sound is still on

I have a video chat application using WebRTC. There is a slight problem: Below is my code for oniceconnectionstatechage:
connection.oniceconnectionstatechange = function () {
if (connection.iceConnectionState == 'disconnected' || connection.iceConnectionState == 'closed')
console.log('connection gone.')
};
The problem is that sometimes, when the internet speed is not well, my connection gets disconnected and I see "connection gone" in my console but the sound still stays on. Both sides can hear each other but the video is gone. What can I do to disconnect my connection completely is such a situation?
You see connection gone in your console when the network connection is unstable, as iceConnectionState may have the state disconnected as
a transient state, e. g. on a flaky network, that can recover by itself.
(Mozilla Developer Network)
It might be - this is an assumption - that in some/many of such cases video is dropped as the available bandwidth can't support both audio and video.
If you really want to close the connection completely when a disconnect occurs, you can replace your if-statement (incl. the console.log) in oniceconnectionchange listerner with the following code:
if (connection.iceConnectionState == 'disconnected'){
console.log('Connection gone. Closing connection.');
connection.close();
}
So each disconnect will be followed by closing the connection completely.
However, this is probably bad practice:
Why would you want to close down the whole connection just because there are temporary problems with the network?
I assume that using this code in a real application will lead to bad user experience, as disconnects will appear in many cases the user would not notice otherwise. I suggest it is better to display a warning to the user in case of (frequent) disconnects.

Difference between various kIOPMAssertionType's

What is the difference between kIOPMAssertionTypeNoIdleSleep, kIOPMAssertionTypePreventSystemSleep and kIOPMAssertionTypePreventUserIdleSystemSleep?
I am trying to create an IOPMAssertion that will prevent the mac from going to sleep autommatically but I really can't tell which of these I should use. I'm getting confused with their descriptions and cannot make sense of them (see the docs for this).
If you're curious, this is how I'm doing it in code:
IOReturn success = IOPMAssertionCreateWithName(kIOPMAssertionTypeNoIdleSleep, kIOPMAssertionLevelOn, CFSTR("My app is running"), &preventSleepAssertionID);
if (success != kIOReturnSuccess) {
NSLog(#"Could not create sleep prevention assertion");
}
Apple have published a Q&A note on this subject, which I believe answers your question. The key comments in the example code in question:
// kIOPMAssertionTypeNoDisplaySleep prevents display sleep,
// kIOPMAssertionTypeNoIdleSleep prevents idle sleep
The former prevents the screen dimming or turning off entirely. Use this if your app is going to be used in a way where the user won't be using the keyboard and mouse, e.g. video player or video chat.
The latter prevents the system itself from going to sleep, but allows the screen to dim and eventually switch off entirely. Useful for long-running computations and apps that only need e.g. audio.
The actual code mirrors what you've got:
//reasonForActivity is a descriptive string used by the system whenever it needs
// to tell the user why the system is not sleeping. For example,
// "Mail Compacting Mailboxes" would be a useful string.
// NOTE: IOPMAssertionCreateWithName limits the string to 128 characters.
CFStringRef* reasonForActivity= CFSTR("Describe Activity Type");
IOPMAssertionID assertionID;
IOReturn success = IOPMAssertionCreateWithName(kIOPMAssertionTypeNoDisplaySleep,
kIOPMAssertionLevelOn, reasonForActivity, &assertionID);
if (success == kIOReturnSuccess)
{
//Add the work you need to do without
// the system sleeping here.
success = IOPMAssertionRelease(assertionID);
//The system will be able to sleep again.
}
Power assertions can still be overridden by the user triggering sleep explicitly (e.g. closing the lid or selecting it in the  menu) or if the system is on battery power.
Are you having trouble getting your code to work?
The documentation for each value gives more information about what they do.
kIOPMAssertionTypePreventUserIdleSystemSleep
Prevents the system from sleeping automatically due to a lack of user activity. ... The system may still sleep for lid close, Apple menu, low battery, or other sleep reasons. This assertion does not put the system into Dark Wake.
kIOPMAssertionTypeNoIdleSleep
The system will not idle sleep when enabled. Note that the system may sleep for other reasons. ... the system will prefer to enter the Dark Wake state, or remain in Dark Wake if already there, rather than go to sleep.
kIOPMAssertionTypePreventSystemSleep
Prevents the system from sleeping and allows the system to reside in Dark Wake for an arbitrary length of time. ... the system will prefer to enter the Dark Wake state, or remain in Dark Wake if already there, rather than go to sleep.
This wording seems to imply that the first option prevents idle sleep due to lack of activity, the second option does the same but prefers to enter Dark Wake rather than sleep, and the third option entirely prevents sleep in favor of Dark Wake.
Looking at PMAssertions.c in the power management code, along with IOPMrootDomain.cpp in the iokit kernel code, it appears that kIOPMAssertionTypeNoIdleSleep is treated as an alias for kIOPMAssertionTypePreventUserIdleSystemSleep, whereas kIOPMAssertionTypePreventSystemSleep is handled differently. Setting kIOPMAssertionTypePreventSystemSleep creates a kernel assertion that entirely prevents system sleep except in extreme cases such as low battery or thermal emergency. Also, this only applies when the computer is connected to external power, unless a special flag is set.
In practice, it appears most Mac applications use kIOPMAssertionTypePreventUserIdleSystemSleep to prevent sleep while performing background tasks. For example Time Machine sets an assertion of this type while backing up. One exception is Internet Sharing, which uses kIOPMAssertionTypePreventSystemSleep to keep the computer awake indefinitely when connected to external power. Note that some users may find this behavior unexpected or undesirable.[1][2]
In summary:
If you want to prevent the computer from sleeping due to inactivity, use kIOPMAssertionTypePreventUserIdleSystemSleep.
Avoid using kIOPMAssertionTypePreventSystemSleep unless you have a
reason to entirely prevent system sleep.

Network activity indicator and asynchronous sockets

I have an app which continuously reads status updates from a server connection.
All is working well with a stream delegate to handle all the reading and writing asynchronously.
There's no part of the app that is "waiting" for a specific response from the server, it is just continuously handling status updates as they sporadically arrive from the socket. There are no requests on the client side that are waiting for responses.
I'm wondering what the best practice would be for the network activity indicator in this case.
I could turn it on in the stream event handler, and off before we leave the handler, but that would be a very short time (just enough for an non-blocking read or write to occur). Trying this, I only see the faintest flicker of the indicator; it needs to be on longer than just during the event handler.
What about turning it on in the stream delegate, and setting a timer to turn it off a short time later? (This would ensure it's on long enough to be seen, rather than the short time spent in the stream delegate.)
Note: I've tried this last idea: turning on the network activity indicator whenever there's stream activity, and note the NSDate; then in a timer (that I have fired every 1 second), if the time passsed is >.5 second, I turn off the indicator. This seems to give a reasonable indication of network activity.
Any better recommendations?
If the network activity is continuous then it sounds like it might be somewhat annoying to the user, especially if it's turning on and off all the time.
Perhaps better would be to test for lack-of-response up to a certain timeout value and then display an alert view to the user if you aren't getting any response from the server. Even that could be optional if you can provide feedback (like "Last update: 5 mins ago") to the user instead.

Desing pattern for background working app

I have created a web-service app and i want to populate my view controllers according to the response i fetch(via GET) in main thread. But i want to create a scheduled timer which will go and control my server, if there becomes any difference(let's say if the count of an array has changed) i will create a local notification. As far as i read from here and some google results, i cant run my app in background more then ten minutes expect from some special situations(Audio, Vo-IP, GPS).. But i need to control the server at least one per minute.. Can anyone offer some idea-or link please?
EDIT
I will not sell the app in store, just for a local area network. Let's say, from the server i will send some text messages to the users and if a new message comes, the count of messages array will increment, in this situation i will create a notification. I need to keep this 'controlling' routing alive forever, whether in foreground or background. Does GCD give such a solution do anyone have any idea?
Just simply play a mute audio file in loop in the background, OR, ping the user's location in the background. Yes, that will drain the battery a bit, but it's a simple hack for in-home applications. Just remember to enable the background types in your Info.plist!
Note: "[...] I fetch (via GET) in main thread." This is not a good approach. You should never fetch any network resources on the main thread. Why? Because your GUI, which is maintained by the main thread, will become unresponsive whenever a fetch isn't instantaneous. Any lag spike on the network results in a less than desirable user experience.
Answer: Aside from the listed special situations, you can't run background apps. The way I see it:
Don't put the app in the background. (crappy solution)
Try putting another "entity" between the app and the "server". I don't know why you "need to control the server at least one per minute" but perhaps you can delegate this "control" to another process outside the device?
.
iOS app -> some form of proxy server -> server which requires
"babysitting" every minute.

How should you handle the possibility of an NSStream blockage?

Per Apple’s “Polling Versus Run-Loop Scheduling”:
[hasSpace/BytesAvailable] can mean that there is available bytes or space or that the only way to find out is to attempt a read or a write operation (which could lead to a momentary block).
The doc does not explicitly state that hasSpace/BytesAvailable events behave the same way, only, obscurely, that they have “identical semantics."
Am I to conclude that a write/read streamError or a bytes read/written return of less than the amount expected could be due to a “momentary block”?
If so, should I attempt the transmission again? Should I use some sort of timer mechanism to give the blockage a chance to clear? This would be a lot of work to implement, so I’d rather not if it’s unlikely to help.
(It’s tempting to initiate a limited polling loop in such a case, say a while loop that makes 10 attempts, but I don’t know if it’s safe to do that at the same time as the stream is scheduled in the run loop, and I have no way to test it.)
Here is a good wrapper for sockets: https://github.com/robbiehanson/CocoaAsyncSocket
It will queue reads and writes if the connection is not available. You don't mention if you're using UDP or TCP, however I suspect you're using TCP, in which case it will handle any interruptions on its own -- provided the connection doesn't get torn down.
It’s been a long haul. Here’s some followup on this issue:
Early on, I threw out the idea of maintaining and checking a leftover cache because that would have worked only for the output stream, when further reflection suggested that the input stream could also become blocked.
Instead, I set up idling while-loops:
- (void) stream:(NSStream *)theStream handleEvent:(NSStreamEvent)eventCode {
switch (eventCode)
// RECEIVING
case NSStreamEventHasBytesAvailable: {
if (self.receiveStage == kNothingToReceive)
return;
// Get the data from the stream. (This method returns NO if bytesRead < 1.)
if (![self receiveDataViaStream:(NSInputStream *)theStream]) {
// If nothing was actually read, consider the stream to be idling.
self.bStreamIn_isIdling = YES;
// Repeatedly retry read, until (1) the read is successful, or (2) stopNetwork is called, which will clear the idler.
// (Just in case, add nil stream property as a loop breaker.)
while (self.bStreamIn_isIdling && self.streamIn) {
if ([self receiveDataViaStream:(NSInputStream *)theStream]) {
self.bStreamIn_isIdling = NO;
// The stream will have started up again; prepare for next event call.
[self assessTransmissionStage_uponReadSuccess];
}
}
}
else
// Prepare for what happens next.
[self assessTransmissionStage_uponReadSuccess];
break;
// SENDING
case NSStreamEventHasSpaceAvailable:
if (self.sendStage == kNothingToSend)
return;
if (![self sendDataViaStream:(NSOutputStream *)theStream]) {
self.bStreamOut_isIdling = YES;
while (self.bStreamOut_isIdling && self.streamOut) {
if ([self sendDataViaStream:(NSOutputStream *)theStream]) {
self.bStreamOut_isIdling = NO;
[self assessTransmissionStage_uponWriteSuccess];
}
}
}
else
[self assessTransmissionStage_uponWriteSuccess];
break;
// other event cases…
Then it came time to test a user-initiated cancellation, via a “cancel” button. Midway through the sync, there’s a pause on the Cocoa side, awaiting user input. If the user cancels at this point, the Cocoa app closes the streams and removes them from the runloop, so I expected that the streams on the other side of the connection would generate NSStreamEventEndEncountered events, or perhaps NSStreamEventErrorOccurred. But, no, only one event came through, an NSStreamEventHasBytesAvailable! Go figure.
Of course, there weren’t really any “bytes available,” as the stream had been closed on the Cocoa side, not written to — so the stream handler on the iOS side went into an infinite loop. Not so good.
Next I tested what would happen if one of the devices went to sleep. During the pause for user input, I let the iPhone sleep via auto-lock*, and then supplied the user input on the Cocoa side. Surprise again: the Cocoa app continued without perturbation to the end of the sync, and when I woke up the iPhone, the iOS app proved to have completed its side of the sync too.
Could there have been a hiccup on the iPhone side that was fixed by my idle loop? I threw in a stop-network routine to check:
if (![self receiveDataViaStream:(NSInputStream *)theStream])
[self stopNetwork]; // closes the streams, etc.
The sync still ran through to completion. There was no hiccup.
Finally, I tested what happened if the Mac (the Cocoa side) went to sleep during that pause for input. This produced a sort of backward belch: Two NSStreamEventErrorOccurred events were received — on the Mac side, after which it was no longer possible to write to the output stream. No events at all were received on the iPhone side, but if I tested the iPhone's stream status, it would return 5, NSStreamStatusAtEnd.
CONCLUSIONS & PLAN:
The "temporary block" is something of a unicorn. Either the network runs smoothly or it disconnects altogether.
If there is truly such a thing as a temporary block, there is no way to distinguish it from a complete disconnection. The only stream-status constants that seem logical for a temporary block are are NSStreamStatusAtEnd and NSStreamStatusError. But per the above experiments, these indicate disconnection.
As a result of which I’m discarding the while-loops and am detecting disconnection solely by checking for bytesRead/Written < 1.
*The iPhone won’t ever sleep if it’s slaved to Xcode. You have to run it straight from the iPhone.
You can anticipate "disconnection" whenever you attempt to write 0 bytes to the output stream, or when you receive 0 bytes on the input stream. If you want to keep the streams alive, make sure you check the length of bytes you're writing to the output stream. That way, the input stream never receives 0 bytes, which triggers the event handler for closed streams.
There's no such thing as an "idling" output stream. Only an idling provider of bytes to the output stream, which doesn't need to indicate its idleness.
If you're getting disconnected from your network connection by the sleep timer, you can disable that when you open your streams, and then disable it when you close them:
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
switch(eventCode) {
case NSStreamEventOpenCompleted:
{
[UIApplication sharedApplication].idleTimerDisabled = YES;
break;
}
case NSStreamEventEndEncountered:
{
[UIApplication sharedApplication].idleTimerDisabled = NO;
break;
}
}
}
I wouldn't delve any further into the specifics of your situation because I can tell right-off-the-bat that you aren't completely clear on what streams are, exactly. I understand that the documentation on streams is really poor at priming newbies, and is scant, to-boot; but, they model the same streams that have been around for 30 years, so any documentation on streams for any operating system (except Windows) will work perfectly at bringing you up to speed.
By the way, the other, inextricable part of streams is your network connection code, which you did not supply. I suggest that, if you're not already using NSNetService and NSNetServiceBrowser to find peers, connect to them, and acquire your streams accordingly, you should. Doing so allows you to easily monitor the state of your network connection, and quickly and easily reopen your streams should they closed unexpectedly.
I have very thorough, yet easy-to-follow sample code for this, which would require no customization on your end at all to use, if anyone would like it.