WebRTC connection does not resume after mobile browser is backgrounded - ios

I have a web application running on Safari on an iPad displaying a live WebRTC video stream. When the user switches away from Safari for a few seconds, and then switches back, the <video> element just shows a black rectangle.
I have added logging to the onsignalingstatechange handler, and checked the console logs for any apparent errors after resuming Safari, but there is nothing obvious indicating the failure.
How can I recover/resume/restart the stream after the user switches back to Safari?
Here is my cargo cult WebRTC code, for reference:
export default class WebRtcPlayer {
static server = "http://127.0.0.1:8083";
server = null;
stream = null;
channel = null;
webrtc = null;
mediastream = null;
video = null;
constructor(id, stream, channel) {
this.server = WebRtcPlayer.server;
this.video = document.getElementById(id);
this.stream = stream;
this.channel = channel;
this.video.addEventListener("loadeddata", () => {
this.video.play();
});
this.video.addEventListener("error", () => {
console.error("video error");
});
this.play();
}
getStreamUrl() {
// RTSPtoWeb only, not RTSPtoWebRTC
return `${this.server}/stream/${this.stream}/channel/${this.channel}/webrtc`;
}
async play() {
console.log("webrtc play");
this.mediastream = new MediaStream();
this.video.srcObject = this.mediastream;
this.webrtc = new RTCPeerConnection({
iceServers: [{
urls: ["stun:stun.l.google.com:19302"],
}],
sdpSemantics: "unified-plan"
});
this.webrtc.onnegotiationneeded = this.handleNegotiationNeeded.bind(this);
this.webrtc.onsignalingstatechange = this.handleSignalingStateChange.bind(this);
this.webrtc.ontrack = this.handleTrack.bind(this);
this.webrtc.addTransceiver("video", {
"direction": "sendrecv",
});
}
async handleNegotiationNeeded() {
console.log("handleNegotiationNeeded");
let offer = await this.webrtc.createOffer({
offerToReceiveAudio: false,
offerToReceiveVideo: true
});
await this.webrtc.setLocalDescription(offer);
}
async handleSignalingStateChange() {
console.log(`handleSignalingStateChange ${this.webrtc.signalingState}`);
switch (this.webrtc.signalingState) {
case "have-local-offer":
let formData = new FormData();
formData.append("data", btoa(this.webrtc.localDescription.sdp));
const response = await fetch(this.getStreamUrl(), {
method: "POST",
body: formData,
});
this.webrtc.setRemoteDescription(new RTCSessionDescription({
type: "answer",
sdp: atob(await response.text()),
}));
break;
case "stable":
/*
* There is no ongoing exchange of offer and answer underway.
* This may mean that the RTCPeerConnection object is new, in which case both the localDescription and remoteDescription are null;
* it may also mean that negotiation is complete and a connection has been established.
*/
break;
case "closed":
/*
* The RTCPeerConnection has been closed.
*/
break;
default:
console.log(`unhandled signalingState is ${this.webrtc.signalingState}`);
break;
}
}
handleTrack(event) {
console.log("handle track");
this.mediastream.addTrack(event.track);
}
static setServer(serv) {
this.server = serv;
}
}

I'm not sure if it's the best way, but I used the Page Visibility API to subscribe to the visibilitychange event:
constructor(id, stream, channel) {
// ...
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
console.log("Document became visible, restarting WebRTC stream.");
this.play();
}
});
// ...
}

Related

Data sharing between Safari and standalone iPhone 12 iOS 14.3

I tried to share data between Safari browser and standalone PWA on iPhone12 with iOS 14.3.
The information, that this should work are here: https://firt.dev/ios-14/
I#ve tried this: https://www.netguru.com/codestories/how-to-share-session-cookie-or-state-between-pwa-in-standalone-mode-and-safari-on-ios
Without success.
Are there any suggestions to running this? Or is it not possible ...
This is the code
const CACHE_NAME = "auth";
const TOKEN_KEY = "token";
const FAKE_TOKEN = "sRKWQu6hCJgR25lslcf5s12FFVau0ugi";
// Cache Storage was designed for caching
// network requests with service workers,
// mainly to make PWAs work offline.
// You can give it any value you want in this case.
const FAKE_ENDPOINT = "/fake-endpoint";
const saveToken = async (token: string) => {
try {
const cache = await caches.open(CACHE_NAME);
const responseBody = JSON.stringify({
[TOKEN_KEY]: token
});
const response = new Response(responseBody);
await cache.put(FAKE_ENDPOINT, response);
console.log("Token saved! 🎉");
} catch (error) {
// It's up to you how you resolve the error
console.log("saveToken error:", { error });
}
};
const getToken = async () => {
try {
const cache = await caches.open(CACHE_NAME);
const response = await cache.match(FAKE_ENDPOINT);
if (!response) {
return null;
}
const responseBody = await response.json();
return responseBody[TOKEN_KEY];
} catch (error) {
// Gotta catch 'em all
console.log("getToken error:", { error });
}
};
const displayCachedToken = async () => {
const cachedToken = await getToken();
console.log({ cachedToken });
};
// Uncomment the line below to save the fake token
// saveToken(FAKE_TOKEN);
displayCachedToken();
Without success means no result, i've tried to set data in safari and get them in standalone pwa

Multiple StreamingRecognizeRequest

I'm trying to setup a StreamingRecognize, with multiple request's. Is it possible ?
The point is that i want to send audio stream from the mic with a unknown time, so i think that i must implement multiple requests. (Considering that a request session has a max_time = 65 seconds).
Anyone can help me with this ?
Thank's alot ;)
Google sample code:
static async Task<object> StreamingMicRecognizeAsync(int seconds)
{
if (NAudio.Wave.WaveIn.DeviceCount < 1)
{
Console.WriteLine("No microphone!");
return -1;
}
var speech = SpeechClient.Create();
var streamingCall = speech.StreamingRecognize();
// Write the initial request with the config.
await streamingCall.WriteAsync(
new StreamingRecognizeRequest()
{
StreamingConfig = new StreamingRecognitionConfig()
{
Config = new RecognitionConfig()
{
Encoding =
RecognitionConfig.Types.AudioEncoding.Linear16,
SampleRateHertz = 16000,
LanguageCode = "en",
},
InterimResults = true,
}
});
// Print responses as they arrive.
Task printResponses = Task.Run(async () =>
{
while (await streamingCall.ResponseStream.MoveNext(
default(CancellationToken)))
{
foreach (var result in streamingCall.ResponseStream
.Current.Results)
{
foreach (var alternative in result.Alternatives)
{
Console.WriteLine(alternative.Transcript);
}
}
}
});
// Read from the microphone and stream to API.
object writeLock = new object();
bool writeMore = true;
var waveIn = new NAudio.Wave.WaveInEvent();
waveIn.DeviceNumber = 0;
waveIn.WaveFormat = new NAudio.Wave.WaveFormat(16000, 1);
waveIn.DataAvailable +=
(object sender, NAudio.Wave.WaveInEventArgs args) =>
{
lock (writeLock)
{
if (!writeMore) return;
streamingCall.WriteAsync(
new StreamingRecognizeRequest()
{
AudioContent = Google.Protobuf.ByteString
.CopyFrom(args.Buffer, 0, args.BytesRecorded)
}).Wait();
}
};
waveIn.StartRecording();
Console.WriteLine("Speak now.");
await Task.Delay(TimeSpan.FromSeconds(seconds));
// Stop recording and shut down.
waveIn.StopRecording();
lock (writeLock) writeMore = false;
await streamingCall.WriteCompleteAsync();
await printResponses;
return 0;
}
In Cloud Speech-to-Text audio length limit for each streaming request is around 1 minute [1]. You can either use asynchronous speech recognition [2] for audio files up to 180 minutes or renew the streaming request before it reaches to the time limit for streaming speech recognition [3].
Here is a Python example how to renew streaming request and stream audio more than 1 minute [4].

Play multiple Audio files on Safari at once

I want to play multiple Audio files simultaneously on iOS .
On the click of a button I create multiple instance of an Audio file and put them into an array.
let audio = new Audio('path.wav')
audio.play().then(() => {
audio.pause();
possibleAudiosToPlay.push(audio);
});
After a while I play them all:
possibleAudiosToPlay.forEach(el => {
el.currentTime = 0;
el.play();
});
While this plays all audio files: When a new one begins it stops the old one. (on iOS)
Apples developer guide says this isn't possible at all with HTML5 Audio:
Playing multiple simultaneous audio streams is also not supported.
But can this be achieved with the Web Audio API?
There isn't anything written about it in Apples developer guide.
Yes you can with Web Audio API. You have to create an AudioBufferSourceNode for each one of your audio sources, since each source can be played only once (you can't stop it and play it again).
const AudioContext = window.AudioContext || window.webkitAudioContext;
const ctx = new AudioContext();
const audioPaths = [
"path/to/audio_file1.wav",
"path/to/audio_file2.wav",
"path/to/audio_file3.wav"
];
let promises = [];
// utility function to load an audio file and resolve it as a decoded audio buffer
function getBuffer(url, audioCtx) {
return new Promise((resolve, reject) => {
if (!url) {
reject("Missing url!");
return;
}
if (!audioCtx) {
reject("Missing audio context!");
return;
}
let xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.responseType = "arraybuffer";
xhr.onload = function() {
let arrayBuffer = xhr.response;
audioCtx.decodeAudioData(arrayBuffer, decodedBuffer => {
resolve(decodedBuffer);
});
};
xhr.onerror = function() {
reject("An error occurred.");
};
xhr.send();
});
}
audioPaths.forEach(p => {
promises.push(getBuffer(p, ctx));
});
// Once all your sounds are loaded, create an AudioBufferSource for each one and start sound
Promise.all(promises).then(buffers => {
buffers.forEach(b => {
let source = ctx.createBufferSource();
source.buffer = b;
source.connect(ctx.destination);
source.start();
})
});

I need to manipulate focus shifting

This is the scenario I want:
I'm checking for the presence of an application (using protocol detection), if it's not present the exe file automatically starts downloading. The webpage continuously checks in regular interval whether the app has been successfully downloaded and when the installation is complete, the browser prompts to open the app.
What is working:
The download is starting automatically. The webpage is checking for the presence of the app in regular interval.
What is NOT working:
After the exe is downloaded, when I start the download, the focus shifts and the webpage thinks that the app has been launched, whereas the app actually hasn't yet been installed even.
I need a focus checking mechanism which detects when the specific app has been launched and focus shifts and not respond to any other focus shifts.
This is my code:
_registerEvent = function(target, eventType, cb) {
if (target.addEventListener) {
target.addEventListener(eventType, cb);
return {
remove: function() {
target.removeEventListener(eventType, cb);
}
};
} else {
target.attachEvent(eventType, cb);
return {
remove: function() {
target.detachEvent(eventType, cb);
}
};
}
},
checkIfHasProtocol = function($el) {
openUriWithTimeoutHack(url);
},
openUriWithTimeoutHack = function(protocol) {
var timeout = setTimeout(function() {
handler.remove();
checkIfHasProtocol(protocol);
}, 5000);
var target = window;
while (target != target.parent) {
target = target.parent;
}
var handler = _registerEvent(window, "blur", onBlur);
function onBlur() {
if (tabVisible() && isBlurred) {
clearTimeout(timeout);
handler.remove();
//App has been launched
}
}
}
window.location = protocol;
};
tabVisible(function() {
if (tabVisible() && inCheck) {
inCheck = false;
}
});
window.addEventListener("blur", function() {
isBlurred = true;
});
window.addEventListener("focus", function() {
isBlurred = false;
inCheck = false;
});

Dartlang server call my function more than once

I'm sending a data to the server like this:
save(){
var el = this.parent.nodes;
print(el);print(el.length);
request = new HttpRequest();
if(el.length == 1) print('No lines to save!');
else
{
var opt = el[i].shadowRoot.nodes[0].options[el[i].shadowRoot.nodes[0].selectedIndex].text;
print(this.parent.nodes.length);
for(var i=1; i < el.length; i++)
{
orderLines.add({
'poid': orderHeader[0]['OrderID'],
'ponum': orderHeader[0]['onum'],
'lnum' : i.toString(),
'itmid' :el[i].shadowRoot.nodes[0].value,
'icode' : opt,
'qty': el[i].shadowRoot.nodes[1].value,
'rqty': 0,
'bqty': el[i].shadowRoot.nodes[1].value,
'iqty': 0,
'biqty': el[i].shadowRoot.nodes[1].value,
'price': el[i].shadowRoot.nodes[2].value,
'rdd': orderHeader[0]['rdd'],
'eta': '',
'flag': 0
});
print(orderLines);
request.onReadyStateChange.listen(onData_save);
request.open('POST', host+'/sPO');
request.send(JSON.encode(orderLines));
}
}
}
and my server side function is:
void main() {
connections = new List<WebSocket>();
HttpServer.bind(HOST, PORT).then((HttpServer server) {
print('Server listening on port ${PORT}.');
server.listen((HttpRequest request) {
if (WebSocketTransformer.isUpgradeRequest(request)) {
WebSocketTransformer.upgrade(request).then(handleWS);
} else gotMSG(request);
});
});
}
handleWS(WebSocket ws){
connections.add(ws);
print('Client connected, there are now ${connections.length} client(s) connected.');
ws.listen((String message) {
for (WebSocket connection in connections) {
connection.add(message);
}
},
onDone: () {
connections.remove(ws);
print('Client disconnected, there are now ${connections.length} client(s) connected.');
});
}
void gotMSG(HttpRequest request) {
switch (request.method) {
case 'POST':
handlePost(request);
break;
case 'OPTIONS':
handleOptions(request);
break;
default:
defaultHandler(request);
}
}
void serveRequest(HttpRequest request){
print('Listening for GET and POST on http://$HOST:$PORT');
request.response.statusCode = HttpStatus.FORBIDDEN;
request.response.reasonPhrase = "WebSocket connections only";
request.response.close();
}
void handlePost(HttpRequest req) {
HttpResponse res = req.response;
switch (req.uri.path) {
case '/login': login(req); break;
...
case '/sPO': savePO(req); break;
default: break;
}
}
The /sPO => savePO is executed once if the order sent is of one line only, but if n lines in the order, the function is executed more than once, could not find a pattern for that,
In the SavePO I used oracledart pub, so thought something wrong in it, and tried postgresql pub, but got same results, the savePO function is:
void savePO(HttpRequest req){
HttpResponse res = req.response;
addCorsHeaders(res);
print('${req.method}: ${req.uri.path}');
Future future() => new Future.value(true);
req.listen((List<int> buffer) {
var theDataLines = JSON.decode(new String.fromCharCodes(buffer));
print(theDataLines);
connect(db).then((conn) {
for (var theData in theDataLines)
conn.execute("""
insert into pol
(poid,ponum,lnum,itmid,icode,qty,rqty,bqty,iqty,biqty,price,rdd, eta, flag)
values (#poid,#ponum,#lnum,#itmid,#icode,#qty,#rqty,#bqty,#iqty,#biqty,#price,
to_timestamp(#rdd,'YYYY-MM-DD'), to_timestamp(#eta,'YYYY-MM-DD'), #flag)
""",
{
'poid': theData['poid'],
'ponum': theData['ponum'],
'lnum' : theData['lnum'],
'itmid' : theData['itmid'],
'icode' : theData['icode'],
'qty': theData['qty'],
'rqty': theData['rqty'],
'bqty': theData['bqty'],
'iqty': theData['iqty'],
'biqty': theData['biqty'],
'price': theData['price'],
'rdd': theData['rdd'].toString(),
'eta': theData['eta'].toString(),
'flag': theData['flag']
})
.then((_)=>conn.query('commit').toList().then((rows) {print('committed');}))
.then((_){
res.write('done');
res.close();
});
}); // END of SQL
}, onError: printError); // End of server listen
} // END of function
I even tried to change the:
case '/sPO': savePO(req); break;
to be
case '/sPO': print(1); break;
the it printed the 1, 4 times after sending an order of 6 lines!!
It's hard to see for me what you actually try to accomplish.
The problem is very probably your save() method. You wrote how it behaves but not much about what you try to accomplish?
Why don't you put more lines into one JSON string and post them together in one request?
You create one request instance and call send repeatedly on this one request instance.
You also register the onReadyStateChange handler more than once on the same request object which results in onData_save being called several times when the event occurs just once.
I think you should either move request = new HttpRequest(); down just before
request.open('POST', host+'/sPO');
request.send(JSON.encode(orderLines));
or better move request.onReadyStateChange.listen(onData_save); up to request = new HttpRequest();,
add all orderlines into one JSON and call
request.open('POST', host+'/sPO');
request.send(JSON.encode(orderLines));
after the for loop.
Another problem I see is that you do a fire and forget. What if the send request fails for some reason?
I would create a sendJSON method that returns a future (with a Completer which completes on onDone and completeError when something goes wrong.
When you want to create more than one request in your save() you can use something like
// create your JSON
var futures = [];
for(i = 0; i < 5; i++) {
futures.add(sendData(myJson)); // collects the futures returned from sendData
}
// executes all futures and waits for all to respond and then returns another future
return Future.wait()
.then((results) {
results.forEach((r) {
// check result
});
});

Resources