When using the Agora Notification Center Service (NCS) to maintain the online status of your app users, your server might receive the following:
To accurately maintain the online status of users, your server needs to be able to deal with redundant notifications and handle received notifications in the same order as events occur. This page introduces how to use channel event callbacks to accomplish this.
The Agora NCS sends RTC channel event callbacks to your server. All channel events, except for 101
and 102
events, contain the clientSeq
field (Unit64) in payload
, which represents the sequence number of an event. This field is used to identify the order in which events occur on the app client. For notification callbacks reporting the activities of the same user, the value of the clientSeq
field increases as events happen.
Refer to the following steps to o use the clientSeq
field to enable your server to handle redundant messages and messages arriving out of order:
LIVE_BROADCASTING
profile: 103
, 104
, 105
, 106
, 111
, and 112
.COMMUNICATION
profile: 107
and 108.
Use the channel event callbacks to get the latest status updates about the following at your server:
clientSeq
of channel eventsWhen receiving notification callbacks of a user, search for the user in the user lists. If there is no data for the user, create data specific to the user.
Compare the value in the clientSeq
field of the latest notification callback you receive with that of the last notification callback handled by your server:
This section provides Java sample code to show how to maintain online user status in the live streaming at your app server by using channel event callbacks.
package io.agora;
import java.util.HashMap;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Logger;
public class UserRegistry {
public static int EVENT_BROADCASTER_JOIN = 103;
public static int EVENT_BROADCASTER_QUIT = 104;
public static int EVENT_AUDIENCE_JOIN = 105;
public static int EVENT_AUDIENCE_QUIT = 106;
public static int EVENT_CHANGE_ROLE_TO_BROADCASTER = 111;
public static int EVENT_CHANGE_ROLE_TO_AUDIENCE = 112;
public static int ROLE_BROADCASTER = 1;
public static int ROLE_AUDIENCE = 2;
public static int WAIT_TIMEOUT_MS = 60 * 1000;
private static Logger logger = Logger.getLogger("UserRegistry");
private Timer timer;
// The data set for a user, including user ID, user role, whether the user is in a channel, and clientSeq of the last event handled by your server.
class User {
int uid;
int role;
boolean isOnline;
long lastClientSeq;
public User(int uid, int role, boolean isOnline, long clientSeq) {
this.uid = uid;
this.role = role;
this.isOnline = isOnline;
this.lastClientSeq = clientSeq;
}
};
// Channel lists and the user lists of each channel.
class Channel {
HashMap<Integer, User> users = new HashMap<>();
};
private HashMap<String, Channel> channels;
// Logic to handle the channel event callbacks sent by the Agora NCS server.
public void HandleNcsEvent(String cname, int uid, int eventType, long clientSeq) {
// If the received notification callbacks need to be handled, the notification callbacks are to be handled as follows. If not, ignore the notification callbacks.
if (eventType != EVENT_BROADCASTER_JOIN &&
eventType != EVENT_BROADCASTER_QUIT &&
eventType != EVENT_AUDIENCE_JOIN &&
eventType != EVENT_AUDIENCE_QUIT &&
eventType != EVENT_CHANGE_ROLE_TO_BROADCASTER &&
eventType != EVENT_CHANGE_ROLE_TO_AUDIENCE) {
logger.warning("Drop un-expected NCS event type " + eventType);
return;
}
// Determine whether a user is online.
boolean isOnlineInNotice = IsUserOnlineInNotice(eventType);
// Get the user role according to event type.
int roleInNotice = GetUserRoleInNotice(eventType);
Channel channel = channels.get(cname);
if (channel == null) {
// If a channel does not exist, create a Channel object and add it to the channel list
channel = new Channel();
channels.put(cname, channel);
logger.info("New channel " + cname + " created");
}
User user = channel.users.get(uid);
// Get whether a user has left a channel.
boolean isQuit = !isOnlineInNotice && (user == null || user.isOnline);
if (user == null) {
// Create a data set for a new user and add it to the user list of the corresponding channel.
user = new User(uid, roleInNotice, isOnlineInNotice, clientSeq);
channel.users.put(uid, user);
if (!isOnlineInNotice) {
logger.info("New User " + uid + " joined");
} else {
// Set a timer for deleting a user's data after the user leaves a channel.
DelayedRemoveUserFromChannel(cname, uid, clientSeq);
}
} else if (clientSeq > user.lastClientSeq) {
// If the data for a user already exists, compare the value in the clientSeq field of the latest notification callback you receive with that of the last notification callback handled by your server.
// If the former is greater than the latter, update the user data according to the the channel event reported; otherwise, ignore the notification callback.
user.role = roleInNotice;
user.isOnline = isOnlineInNotice;
user.lastClientSeq = clientSeq;
if (isQuit) {
// Mark the status of a user as offline, and delete the user data after one minute.
logger.info("User " + uid + " quit channel " + cname);
DelayedRemoveUserFromChannel(cname, uid, clientSeq);
}
}
}
// Set a timer for deleting the data of users that are offline.
private void DelayedRemoveUserFromChannel(final String cname, final int uid, final long clientSeq) {
timer.schedule(new TimerTask() {
@Override
public void run() {
Channel channel = channels.get(cname);
if (channel == null) return;
User user = channel.users.get(uid);
if (user == null) return;
// If the value in the clientSeq field changes, do not delete the user data.
if (user.lastClientSeq != clientSeq) return;
if (!user.isOnline) {
// Delete user data only when the user is offline and the value in the clientSeq field does not change.
channel.users.remove(uid);
logger.info("Remove user " + uid + " from channel " + cname);
} else {
logger.info("User " + uid + " is online while delayed removing, cancelled");
}
if (channel.users.isEmpty()) {
channels.remove(cname);
logger.info("Remove channel " + cname);
}
}
}, WAIT_TIMEOUT_MS);
}
// Determine whether a user is online.
private static boolean IsUserOnlineInNotice(int eventType) {
return eventType == EVENT_BROADCASTER_JOIN ||
eventType == EVENT_AUDIENCE_JOIN ||
eventType == EVENT_CHANGE_ROLE_TO_BROADCASTER ||
eventType == EVENT_CHANGE_ROLE_TO_AUDIENCE;
}
// Get the user role.
private static int GetUserRoleInNotice(int eventType) {
if (eventType == EVENT_BROADCASTER_JOIN ||
eventType == EVENT_BROADCASTER_QUIT ||
eventType == EVENT_CHANGE_ROLE_TO_BROADCASTER) {
return ROLE_BROADCASTER;
} else {
return ROLE_AUDIENCE;
}
}
}
When your server receives a notification callback of event 104
with reason
as 999
, it means that the user is considered to have abnormal activities due to frequent login and logout actions.
In this case, Agora recommends that your server calls the Banning user privileges API to remove the user from the current channel one minute after receiving such notification callback; otherwise, the notification callbacks your server receive about the user's events might be redundant or arrive out of order, which makes it hard for you to accurately maintain the online status of this user.
When adopting the solutions recommended by Agora to maintain user online status, you need to recognize the following: