I am building a web application using React where users can enter a group call. I have a NodeJS server that runs Socket.IO to manage the client events, and the users are connected through a peer-to-peer connection using simple-peers (WebRTC).
Everything is functional, from joining the call, seeing other users, and being able to leave. The call is "always open", similar to discord, and users can come and go as they please. However, if you leave and then try to rejoin the call without refreshing the page, the call breaks on all sides. The user leaving and rejoining gets the following error:
Error: cannot signal after peer is destroyed
For another user in the call, it logs the "user joined" event multiple times for the one user that tried to rejoin. Before it would add multiple peers as well, but I now make sure duplicates cannot exist.
However, to me, the strangest part is that when the user leaves, they send a call out to all other users in the room. The other users successfully destroy the peer connection and then remove the user from their peer array. The user who left on his turn also destroys each connection and resets the peer array to an empty array. So I'm very confused as to what PTP connection it is trying to re-establish.
const [roomSize, setRoomSize] = useState(0);
const socketRef = useRef();
const userVideo = useRef();
const peersRef = useRef([]);
const roomId = 'meeting_' + props.zoneId;
useEffect(async () => {
socketRef.current = io.connect(SERVER_URI, {
jsonp: false,
forceNew: true,
extraHeaders: {
"x-access-token": window.localStorage.getItem('accessToken'),
"zone-id": props.zoneId
}
});
}, []);
useEffect(async () => {
if (props.active) {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: {
height: window.innerHeight / 2,
width: window.innerWidth / 2
},
audio: true });
userVideo.current.srcObject = stream;
console.log(`%cJoined socket at ${SERVER_URI}, connected=${socketRef.current.connected}`, 'color: pink');
socketRef.current.emit("join room", roomId);
socketRef.current.on("all users", users => {
users.forEach(userID => {
const peer = createPeer(userID, socketRef.current.id, stream);
const peerObj = {
peerID: userID,
peer,
};
if (!peersRef.current.find(p => p.peerID === userID))
peersRef.current.push(peerObj);
});
setRoomSize(peersRef.current.length);
console.log(`%cNew Room Members: ${peersRef.current.length}; %o`, 'color: cyan', {peersRef: peersRef.current});
})
socketRef.current.on("user joined", payload => {
const peer = addPeer(payload.signal, payload.callerID, stream);
const peerObj = {
peerID: payload.callerID,
peer,
};
if (!peersRef.current.find(p => p.peerID === payload.callerID))
peersRef.current.push(peerObj);
setRoomSize(peersRef.current.length);
console.log(`%cSomeone Joined. Members: ${peersRef.current.length}; %o`, 'color: cyan', {peersRef: peersRef.current});
});
socketRef.current.on("receiving returned signal", payload => {
/** #type {Peer} */
const item = peersRef.current.find(p => p.peerID === payload.id);
item.peer.signal(payload.signal);
console.log("%creceiving return signal", 'color: lightgreen');
});
socketRef.current.on('user left', id => {
const peerObj = peersRef.current.find(p => p.peerID === id);
// console.log("user left", { peerObj });
if (peerObj)
peerObj.peer.destroy();
const peers = peersRef.current.filter(p => p.peerID !== id);
peersRef.current = peers;
setRoomSize(peersRef.current.length);
console.log(`%cSomeone Left. Members: ${peersRef.current.length}`, 'color: cyan');
});
} catch (err) {
console.trace(err);
}
}
else if (socketRef.current && socketRef.current.connected) {
socketRef.current.emit("leave room");
peersRef.current.forEach(peerObj => {
peerObj.peer.destroy();
});
peersRef.current = [];
setRoomSize(peersRef.current.length);
}
}, [props.active, peersRef.current]);
const createPeer = (userToSignal, callerID, stream) => {
const peer = new Peer({
initiator: true,
trickle: false,
stream,
});
peer.on("signal", signal => {
socketRef.current.emit("sending signal", { userToSignal, callerID, signal })
})
return peer;
}
const addPeer = (incomingSignal, callerID, stream) => {
const peer = new Peer({
initiator: false,
trickle: false,
stream,
})
peer.on("signal", signal => {
socketRef.current.emit("returning signal", { signal, callerID })
})
peer.signal(incomingSignal);
return peer;
}
Quick Edit: The above code is part of a React component that renders a video element for each peer.
When props.active becomes false is when the user leaves the call. This happens at the end of the second useEffect hook, where the client who left should have removed all their peer objects after destroying them. Why does this user receive the above error on a reconnect? And how do I keep this error from occurring?
Edit: I just noticed that when both users leave the call, and both try to rejoin without refreshing, the error does not occur. So something is different when removing a peer upon a user leaving compared to leaving yourself is my best guess.
TLDR; Put all refs you use in the useEffect body in the useEffect deps array.
I'd be sure to first check the useEffect deps array. It looks like socketRef is required in multiple places throughout that hook body, but it doesn't appear to be in the deps array. This can cause the hook to use less-than-current data.
It's also because of this that the socketRef ref object may never actually update, meaning, it may correctly remove the user from peers, as peerRefs is in the useEffect deps array, but the internal session (the room) may not recognize this; the room's internal representation of the user still exists.
Repeating myself, but just to make it clear, you mentioned:
So something is different when removing a peer upon a user leaving compared to leaving yourself is my best guess.
This is the same reason as listed above. The reason it happens when a peer leaves is because the peerRefs ref object IS in the useEffect deps array, so the effect you're describing is just 'perfect timing', if you will, since the applications state (all the refs) are correctly sync'd up with each other.
Related
I am new to PeerJs and recently starting developing an app for my school during this Covid pandemic.
I have been able to deploy code to NodeJs server with express and was able to establish connection between 2 users.
But the problem arises when video is turned off from the beginning of stream for both users and a user wants to initiate a video call.
What I need is, to send some kind of notification to user 2 that user 1 is requesting for video. So that user 2 will turn on video.
My existing code is:
var url = new URL(window.location.href);
var disableStreamInBeginning = url.searchParams.get("disableStreamInBeginning"); // To disable video in the beginning
var passwordProtectedRoom = url.searchParams.get("passwordProtectedRoom");
var muteAllInBeginning = url.searchParams.get("muteAllInBeginning");
const socket = io('/')
const localVideoDiv = document.getElementById('local-video-div')
const oneOnOneSelf = document.getElementById('local-video')
const oneOnOneRemote = document.getElementById('remote-video')
if(typeof disableStreamInBeginning !== 'undefined' && disableStreamInBeginning == 'true'){
var disbaleSelfStream = true
} else {
var disbaleSelfStream = false
}
if(typeof passwordProtectedRoom !== 'undefined' && passwordProtectedRoom == 'true'){
var passwordProtected = true
} else {
var passwordProtected = false
}
if(typeof muteAllInBeginning !== 'undefined' && muteAllInBeginning == 'true'){
var muteAll = true
} else {
var muteAll = false
}
var systemStream
oneOnOneSelf.style.opacity = 0
oneOnOneRemote.style.opacity = 0
const myPeer = new Peer(undefined, {
host: '/',
port: '443',
path: '/myapp',
secure: true
})
const ownVideoView = document.createElement('video')
const peers = {}
navigator.mediaDevices.getUserMedia({
video: true,
audio: true
}).then(ownStream => {
systemStream = ownStream
addVideoStream(ownStream, oneOnOneSelf)
myPeer.on('call', call => {
call.answer(ownStream)
call.on('stream', remoteStream => {
addVideoStream(remoteStream, oneOnOneRemote)
})
})
socket.on('user-connected', userId => {
//connectToNewUser(userId, stream)
setTimeout(connectToNewUser, 1000, userId, ownStream)
})
})
socket.on('user-disconnected', userId => {
if (peers[userId]) peers[userId].close()
})
myPeer.on('open', id => {
//Android.onPeerConnected();
socket.emit('join-room', ROOM_ID, id)
})
function connectToNewUser(userId, stream) {
const call = myPeer.call(userId, stream)
call.on('stream', remoteStream => {
//console.log('Testing')
addVideoStream(remoteStream, oneOnOneRemote)
})
call.on('close', () => {
oneOnOneRemote.remove()
})
peers[userId] = call
}
function addVideoStream(stream, videoView) {
videoView.srcObject = stream
videoView.addEventListener('loadedmetadata', () => {
if(disbaleSelfStream){
audioVideo(true)
} else {
localVideoDiv.style.opacity = 0
videoView.style.opacity = 1
videoView.play()
}
})
}
function audioVideo(bool) {
if(bool == true){
localVideoDiv.style.opacity = 1
oneOnOneSelf.style.opacity = 0
systemStream.getVideoTracks()[0].enabled = false
} else {
if(disbaleSelfStream){
console.log('Waiting For Another User To Accept') // Here is need to inform user 2 to tun on video call
} else {
localVideoDiv.style.opacity = 0
oneOnOneSelf.style.opacity = 1
systemStream.getVideoTracks()[0].enabled = true
}
}
}
function muteUnmute(bool) {
if(bool == true){
systemStream.getAudioTracks()[0].enabled = true
} else {
systemStream.getAudioTracks()[0].enabled = false
}
}
function remoteVideoClick(){
alert('Hi');
}
Please help.
You can send messages back and forth directly using peer itself
const dataConnection = peer.connect(id) will connect you to the remote peer, it returns a dataConnection class instance that you can later use with the send method of that class.
Just remember that you also want to setup listener on the other side to listen for this events, like "open" to know when the data channel is open:
dataConnection.on('open', and dataConnection.on('data...
You have a bug in your code above, I know you didn't ask about it, it is hard to see and not always will manifest. The problem will occur when your originator sends a call before the destination has had time to receive the promise back with its local video/audio stream. The solution is to invert the order of the calls and to start by setting up the event handler for peer.on("call", ... rather than by starting by waiting for a promise to return when we ask for the video stream. The failure mode will depend on how long does it take for your destination client to signal it wants and call to the originator plus how long it takes for the originator to respond versus how long it takes for the stream promise to return on the destination client. You can see a complete working example, where messages are also sent back and forth here.
// Function to obtain stream and then await until after it is obtained to go into video chat call and answer code. Critical to start the event listener ahead of everything to ensure not to miss an incoming call.
peer.on("call", async (call) => {
let stream = null;
console.log('*** "call" event received, calling call.answer(strem)');
// Obtain the stream object
try {
stream = await navigator.mediaDevices.getUserMedia(
{
audio: true,
video: true,
});
// Set up event listener for a peer media call -- peer.call, returns a mediaConnection that I name call
// Answer the call by sending this clients video stream --myVideo-- to calling remote user
call.answer(stream);
// Create new DOM element to place the remote user video when it comes
const video = document.createElement('video');
// Set up event listener for a stream coming from the remote user in response to this client answering its call
call.on("stream", (userVideoStream) => {
console.log('***"stream" event received, calling addVideoStream(UserVideoStream)');
// Add remote user video stream to this client's active videos in the DOM
addVideoStream(video, userVideoStream);
});
} catch (err) {
/* handle the error */
console.log('*** ERROR returning the stream: ' + err);
};
});
I'm having problems with the logic to build behind the webRTC multi peer connections handling.
Basically I'm trying to make a Room full of people in a videoconference call.
I'm using the basic WebSocket library provided by js, and React for the frontend and Java (spring boot) for the backend.
As of my understanding right now this is what I managed to write down (filtered based on what I "think" is relevant)
This is my web socket init method (adding listeners)
let webSocketConnection = new WebSocket(webSocketUrl);
webSocketConnection.onmessage = (msg) => {
const message = JSON.parse(msg.data);
switch (message.type) {
case "offer":
handleOfferMessage(message);
break;
case "text":
handleReceivedTextMessage(message);
break;
case "answer":
handleAnswerMessage(message);
break;
case "ice":
handleNewICECandidateMessage(message);
break;
case "join":
initFirstUserMedia(message);
break;
case "room":
setRoomID(message.data);
break;
case "peer-init":
handlePeerConnection(message);
break;
default:
console.error("Wrong type message received from server");
}
Plus of course the 'on error', 'on close' and 'on open' listeners
This is the method handling the incoming offer
const handleOfferMessage = (message) => {
console.log("Accepting Offer Message");
console.log(message);
let desc = new RTCSessionDescription(message.sdp);
let newPeerConnection = new RTCPeerConnection(peerConnectionConfig);
newPeerConnection.onicecandidate = handleICECandidateEvent;
newPeerConnection.ontrack = handleTrackEvent;
if (desc != null && message.sdp != null) {
console.log("RTC Signalling state: " + newPeerConnection.signalingState);
newPeerConnection
.setRemoteDescription(desc)
.then(function () {
console.log("Set up local media stream");
return navigator.mediaDevices.getUserMedia(mediaConstraints);
})
.then(function (stream) {
console.log("-- Local video stream obtained");
localStream = stream;
try {
videoSelf.current.srcObject = localStream;
} catch (error) {
videoSelf.current.src = window.URL.createObjectURL(stream);
}
console.log("-- Adding stream to the RTCPeerConnection");
localStream
.getTracks()
.forEach((track) => newPeerConnection.addTrack(track, localStream));
})
.then(function () {
console.log("-- Creating answer");
return newPeerConnection.createAnswer();
})
.then(function (answer) {
console.log("-- Setting local description after creating answer");
return newPeerConnection.setLocalDescription(answer);
})
.then(function () {
console.log("Sending answer packet back to other peer");
webSocketConnection.send(
JSON.stringify({
from: user,
type: "answer",
sdp: newPeerConnection.localDescription,
destination: message.from
})
);
})
.catch(handleErrorMessage);
}
peerConnections[message.from.id] = newPeerConnection;
console.log("Peer connections updated now ", peerConnections);
};
SN: I got the peer connections defined as an array of RTCPeerConnection indexed by the user unique id
let [peerConnections, setPeerConnections] = useState([]);
And here comes the part that I think I got wrong and on which I'm having trouble understanding
const handleAnswerMessage = (message) => {
console.log("The peer has accepted request");
let currentPeerConnection = peerConnections[message.from.id];
if (currentPeerConnection) {
currentPeerConnection.setRemoteDescription(message.sdp).catch(handleErrorMessage);
peerConnections[message.from.id] = currentPeerConnection;
} else {
console.error("No user was found with id ", message.from.id);
}
console.log("Peer connections updated now ", peerConnections);
};
currentPeerConnection.setRemoteDescription(message.sdp).catch(handleErrorMessage);
peerConnections[message.from.id] = currentPeerConnection;
console.log("Peer connections updated now ", peerConnections);
};
The answer and the offer work perfectly, I can clearly see the two peers communicating one by sending the offer and the other one responding with an answer. The only problem is that after that nothing happens, but from what I read about webRTC it should actually start gathering ice candidates as soon as a local description has been set.
I can understand why the peer handling the answer (caller) actually does not fire up iceecandidate and that's probably because I do not set a local description on the answer message (I don't know if it would be correct). the callee on the other hand, handling the offer message should actually start gathering iceecandidates tho, I'm setting the local description on there.
This some additional code that might help
function getMedia(constraints, peerCnnct, initiator) {
if (localStream) {
localStream.getTracks().forEach((track) => {
track.stop();
});
}
navigator.mediaDevices
.getUserMedia(constraints)
.then(stream => {
return getLocalMediaStream(stream, peerCnnct, initiator);
})
.catch(handleGetUserMediaError);
}
function getLocalMediaStream(mediaStream, peerConnection, initiator) {
localStream = mediaStream;
const video = videoSelf.current;
if (video) {
video.srcObject = mediaStream;
video.play();
}
//localVideo.srcObject = mediaStream;
console.log("Adding stream tracks to the peer connection: ", peerConnection);
if (!initiator) {
localStream
.getTracks()
.forEach((track) => peerConnection.addTrack(track, localStream));
}
}
const handlePeerConnection = (message) => {
console.info("Creating new peer connection for user ", message.from);
let newPeerConnection = new RTCPeerConnection(peerConnectionConfig);
// event handlers for the ICE negotiation process
newPeerConnection.ontrack = handleTrackEvent;
newPeerConnection.onicecandidate = handleICECandidateEvent;
getMedia(mediaConstraints, newPeerConnection, false);
newPeerConnection.onnegotiationneeded = handleNegotiationNeededEvent(newPeerConnection, webSocketConnection, user, message.from);
peerConnections[message.from.id] = newPeerConnection;
};
Here you can clearly see my desperate attempt in finding a solution and creating a peer connection just for the sake of sending the offer.
I cannot index a peer connection that has no end user because I would need his id, that I receive only after I received an answer from him when I first join the room.
(The backend should work but either way putting a debugger on the ice candidate handler method I could clearly see that it's just not fired)
What am I doing wrong?
EDIT: Now the WebSocketMessage Server side has also a destination user. This way the the new peer that connects to the room receives as many peer-init messages as the already connected peers are. Then proceeds to make one offer per peer setting it as a destination.
The problem still persists though
This feels actually wierd but I fixed it calling the getUserMedia() (which means calling the addTrack before adding the onicecandidate event definition to the peerConnection.
This at least fixed my problem
I am building a video chat application using simple-peer where users can also share the screen. The flow of the app is such that when user A creates a room, he can grab the page url and share with user B. When user B joins, an initiator peer is created for him as seen here.
function createPeer(partnerID, callerID, stream) {
const peer = new Peer({
initiator: true,
trickle: false,
stream,
});
peer.on("signal", signal => {
const payload = {
partnerID,
callerID,
signal
}
socketRef.current.emit("call partner", payload);
});
peer.on("stream", handleStream);
return peer;
}
When user A gets the offer from user B, a non initiator peer is created for him as seen here.
function addPeer(incomingSignal, callerID, stream) {
const peer = new Peer({
initiator: false,
trickle: false,
stream,
});
peer.on("signal", signal => {
const payload = {
callerID,
signal
}
socketRef.current.emit("accept call", payload);
});
peer.on("stream", handleStream);
peer.signal(incomingSignal);
return peer;
}
Now when any user decides to share their screen, this function gets called.
function shareScreen() {
navigator.mediaDevices.getDisplayMedia().then(stream => {
const track = stream.getTracks()[0];
peerRef.current.removeStream(videoStream.current);
peerRef.current.addStream(stream);
userVideoRef.current.srcObject = stream;
track.onended = function () {
userVideoRef.current.srcObject = videoStream.current;
peerRef.current.removeTrack(track, stream);
};
});
}
What's really strange about the behavior that I am getting, is that when user B, in other words the calling peer, wants to share his screen, all works well, but when user A, the callee peer, wants to share his screen, I get the following error.
index.js:17 Uncaught Error: [object RTCErrorEvent]
at makeError (index.js:17)
at RTCDataChannel._channel.onerror (index.js:490)
I am not really sure where I am going wrong.
After some testing, I discovered where the error is coming from.
What you need to do is to call the function peer.signal(incomingSignal) after calling the function addPeer(), along with the peer variable returned from the addPeer() function.
Here's an example:
function addPeer(incomingSignal, callerID, stream) {
const peer = new Peer({
initiator: false,
trickle: false,
stream,
});
peer.on("signal", signal => {
const payload = {
callerID,
signal
}
socketRef.current.emit("accept call", payload);
});
peer.on("stream", handleStream);
// peer.signal(incomingSignal);
return peer;
}
let peer = addPeer(incomingSignal, callerId, stream);
peer.signal(incomingSignal);
and it will work fine
Environment : expo 36.0.0 / React Native 0.61 / react-native-gifted-chat 0.13.0 / firebase 7.6.0
My goal: Render only messages specific to a specific chat channel on the Gifted Chat UI.
Expected results: Only messages pertaining to a given channel should be displayed on the Gifted Chat UI.
Actual results :
When I navigate from one chat channel to another the total messages cummulate.
When I come back to an earlier channel, the messages for the same chat channel repeat more than once.
Warning: Encountered two children with the same key, %s. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.%s
What have I tried so far?
1. Relaunch subscription to new chat channel every time the user navigates from one channel to another using componentDidUpdate.
2. Set State of messages array to an empty array every time user changes chat channel.
3. Unsubscribe from the previous node in Firebase and subscribe to a new node in componentDidUpdate. Here node represents the chat channel identified by an ID in Firebase. Each node contains children that are all the messages pertaining to that specific chat channel.
async componentDidMount() {
await this.getSessionInfo();
await this.getCurrentProfile();
await this.getMessages();
};
async componentDidUpdate(prevProps) {
/* sessionID represent a chat channel and corresponding node in Firebase */
/* If user navigates from one channel to another we establish a connection with new node and get the
corresponding messages */
if (this.props.navigation.state.params.sessionID !== prevProps.navigation.state.params.sessionID) {
await this.getSessionInfo();
await this.getCurrentProfile();
/* Subscribe to new chat channel */
await this.ref();
await this.getMessages();
}
};
async componentWillUnmount() {
/* Unsubscribe from database */
await this.off();
}
/* Get messages to display after component is mounted on screen */
getMessages = () => {
this.connect(message => {
this.setState(previousState => ({
messages: GiftedChat.append(previousState.messages, message),
}))
});
}
/* Each sessionID corresponds to a chat channel and node in Firebase */
ref = () => {
return database.ref(`/sessions/${this.state.sessionID}`);
}
/* Fetch last 20 messages pertaining to given chat channel and lister to parse new incoming message */
connect = (callback) => {
this.ref()
.limitToLast(20)
.on('child_added', snapshot => callback(this.parse(snapshot)));
}
/* newly created message object in GiftedChat format */
parse = snapshot => {
const { timestamp: numberStamp, text, user } = snapshot.val();
const { key: _id } = snapshot;
const timestamp = new Date(numberStamp);
const message = {
_id,
timestamp,
text,
user,
};
return message;
};
/* function that accepts an array of messages then loop through messages */
send = (messages) => {
for (let i = 0; i < messages.length; i++) {
const { text, user } = messages[i];
const message = {
text,
user
};
this.append(message);
}
};
/* append function will save the message object with a unique ID */
append = message => this.ref().push(message);
Following Brett Gregson's advice and Mike Kamerman's answer, I implemented the following solution.
When the user navigates from one channel to another, I trigger a set of functions.
this.ref() function subscribes to new channel (node in Firebase).
async componentDidUpdate(prevProps) {
if (this.props.navigation.state.params.sessionID !== prevProps.navigation.state.params.sessionID) {
await this.getSessionInfo();
await this.ref();
await this.switchSession();
}
};
I set the messages state which is an array of messages to en empty array along with async/await mechanism. The getMessages function fetches the messages of new channel. This way messages belonging to preceding group are cleared and there is no accumulation of messages in the UI.
switchSession = async () => {
await this.setState({ messages: [] });
await this.getMessages();
}
I have a firebase serviceworker that shows notifications when a message is pushed from Firebase Cloud Messaging (FCM).
It also publishes a post so that my React App can update accordingly.
/* eslint-env worker */
/* eslint no-restricted-globals: 1 */
/* global firebase */
/* global clients */
import config from './config'
importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-app.js')
importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-messaging.js')
const { FIREBASE_MESSAGING_SENDER_ID } = config
firebase.initializeApp({ messagingSenderId: FIREBASE_MESSAGING_SENDER_ID })
const messaging = firebase.messaging()
messaging.setBackgroundMessageHandler(payload => {
const title = payload.data.title
const options = {
body: payload.data.body,
icon: payload.data.icon,
data: payload.data,
}
clients.matchAll({ includeUncontrolled: true }).then(clientz => {
clientz.forEach(client => {
sendMessageToClient(client, 'NEW_USER_NOTIFICATON')
})
})
return self.registration.showNotification(title, options)
})
const sendMessageToClient = (client, message) => {
const messageChannel = new MessageChannel()
client.postMessage(message, [messageChannel.port2])
}
This all works fine, but I have added it for context.
What I want to do is have a click function that focuses on the correct window/tab and navigates to a link that is passed to it. Or if the tab is not open, open a new window and go to the link.
This is the code I have so far, added to the above file.
self.addEventListener('notificationclick', event => {
const clickedNotification = event.notification
const link = clickedNotification.data.link
clickedNotification.close()
const promiseChain = self.clients.claim()
.then(() => self.clients
.matchAll({
type: 'window',
})
)
.then(windowClients => {
let matchingClient = null
windowClients.forEach(client => {
if (client.url.includes(matching_url)) {
matchingClient = client
}
})
if (matchingClient) {
return matchingClient.navigate(link)
.then(() => matchingClient.focus())
}
return clients.openWindow(link)
})
event.waitUntil(promiseChain)
})
So, I realise that the chained navigate and focus inside a then is probably bad practice, but for now, I am just trying to get it to work. Then I will try and come up with a clever solution.
So the problem with my code is that the clients.claim() doesn't seem to be working. The matchAll doesn't return anything to the next then, the argument is an empty array.
I could simply add the includeUncontrolled: true option to the matchAll, but the navigate command only works on a controlled client instance.
If I try the often referenced Google example for claiming and navigation, it works fine:
self.addEventListener('activate', event => {
event.waitUntil(self.clients.claim().then(() => {
// See https://developer.mozilla.org/en-US/docs/Web/API/Clients/matchAll
return self.clients.matchAll({type: 'window'});
}).then(clients => {
return clients.map(client => {
// Check to make sure WindowClient.navigate() is supported.
if ('navigate' in client) {
return client.navigate('activated.html');
}
});
}));
});
So I am stuck.
The serviceworker is activated immediately, so I assume that it claim a client at any point after that.
Have I fallen for a random ServiceWorker Gotcha?
Can the claim only be used and navigated to on the handling of an activation event?
I would appreciate any help available.
Cheers
I couldn't get this to work.
But I thought it would be worth documenting my workaround.
I could not get client.navigate to work in the notificationclick event handler.
So instead I just sent a postMessage containing the URL to be picked up in my app to trigger the redirect there, without any client claiming anywhere.
self.addEventListener('notificationclick', event => {
const clickedNotification = event.notification
const link = clickedNotification.data.link
clickedNotification.close()
const promiseChain = self.clients.matchAll({
type: 'window',
includeUncontrolled: true,
})
.then(windowClients => {
let matchingClient = null
windowClients.forEach(client => {
if (client.url.includes(matching_url)) {
matchingClient = client
}
})
if (matchingClient) {
sendMessageToClient(matchingClient, { type: 'USER_NOTIFICATION_CLICKED', link })
return matchingClient.focus()
}
return clients.openWindow(link)
})
event.waitUntil(promiseChain)
})
const sendMessageToClient = (client, message) => {
const messageChannel = new MessageChannel()
client.postMessage(message, [messageChannel.port2])
}