Flex Audio SDK for Android: More options
After you add the Flex SDK for Android to your app using the Quickstart, you can use the options on this page to further customize your integration.
Customize logging
By default, the SDK logs messages to the system log. Aircore media distribution servers also log events and conditions.
To help find relevant logs for debugging, provide a user agent string by calling setUserAgent()
.
You can pick a specific directory for logging by calling setLogDirectory()
.
Some paths require storage permissions. To write logs to directories that are not writable by default, get the WRITE_EXTERNAL_STORAGE
permission. For more on permissions, see the Quickstart.
Use a secret API key
Aircore uses two types of API keys: publishable and secret. See Authentication for an overview of each.
The Quickstart uses a publishable API key for simplicity. This section explains how to use a secret API key instead for better security.
When you use a secret API key, your backend gets session auth tokens from the Aircore provisioning service. You use these tokens when you create channels. Tokens expire, so you have to keep updating them.
Using a secret API key includes three main tasks:
Request and use a session auth token when you create a channel.
Update the token before it expires to keep using the channel.
Reconnect to the channel if your app uses an expired token.
Create a channel
Request a session auth token and use this syntax for createChannel()
:
import io.aircore.media.Channel;
import io.aircore.media.Engine;
// The session authorization token from your backend for joining the channel.
String token = "SESSION_AUTH_TOKEN"
// Set up variables for notification.
String appName = "";
// Message to display on the notification when the app is running in the
// background.
String message = "";
// Icon for the notification.
@DrawableRes int icon = R.mipmap.launcher;
// Create the channel.
Channel channel = Engine.getInstance(context, appName, message, icon)
.createChannel(token);
// Check if the channel was successfully created.
if (channel == null) {
// Handle this fatal error.
throw new Exception("Can't create channel");
}
The token permissions determine whether the user can publish audio.
Update tokens before they expire
When a channel's token is about to expire, the sessionAuthTokenNearingExpiry()
method is called for delegates registered to the channel.
Replace the token by using the channel's updateSessionAuthToken()
method:
import io.aircore.media.Channel;
class ChannelDelegate implements Channel.Delegate {
// Implement other notification handlers.
// ...
@Override
public void sessionAuthTokenNearingExpiry(Date date) {
String newToken = getTokenFromMyBackend();
channel.updateSessionAuthToken(newToken);
}
}
ChannelDelegate channelDelegate = new ChannelDelegate();
// Register a delegate with the channel before joining to make sure that you
// receive all notifications.
channel.addDelegate(channelDelegate);
You can update the token at any time, except when the channel's join state is TERMINATED
.
You can also change the permissions by updating the token. For example, you can have a user without permission to publish audio join a channel. To give the user permission later, you can provide a new token.
Handle expired tokens
Avoid letting tokens expire by registering a channel delegate to listen for sessionAuthTokenNearingExpiry()
.
If your app tries to join a channel with an expired token:
The channel disconnects.
The value of
JoinState
isTERMINATED
.The value of
terminationCause
isSESSION_AUTH_TOKEN_INVALID
.
You can use the join state notification to handle this issue. See Track the status of a channel.
import io.aircore.media.Channel;
String sessionAuthToken = "SESSION_AUTH_TOKEN"
// Set up variables for notification.
String appName = "";
// Message to display on the notification when the app is running in the
// background.
String message = "";
// Icon for the notification.
@DrawableRes int icon = R.mipmap.launcher;
// Create the channel.
Channel channel = Engine.getInstance(context, appName, message, icon)
.createChannel(sessionAuthToken);
// Check if the channel was successfully created.
if (channel == null) {
// Handle this fatal error.
throw new Exception("Can't create channel");
}
// Define a delegate to receive channel notifications.
class ChannelDelegate implements Channel.Delegate {
// Implement other notification handlers.
// ...
// When the channel join state changes, check if the channel terminated
// because of an expired token.
@Override
public void joinStateDidChange(Channel.JoinState newJoinState, Channel.JoinState oldJoinState) {
if (newJoinState == Channel.JoinState.TERMINATED &&
channel.getTerminationCause() == Channel.TerminationCause.SESSION_AUTH_TOKEN_INVALID) {
// Get a new token from your backend and create a new channel.
String newToken = getNewTokenFromMyBackend();
// Create a new channel.
}
}
}
channel.addDelegate(new ChannelDelegate());
Use channel join states with your UI
This diagram shows the possible changes in join state for a channel:
To build your UI, decide which actions to take in each join state:
NOT_JOINED |
|
JOINED and REJOINING |
|
TERMINATED |
|
To terminate a channel, call the leave()
method at any time, except when the join state is already TERMINATED
.
For a full implementation of the join state notification, see this file in our sample app:
Create a muted stream
You can create a muted stream by using the setInitialAudioMute()
method when you build the LocalStreamParams
object:
import io.aircore.media.Channel;
import io.aircore.media.LocalStreamParams;
// ... Create and join a channel.
// Make sure the channel is connected.
Channel.JoinState channelJoinState = channel.getJoinState();
if (channelJoinState != Channel.JoinState.JOINED && channelJoinState != Channel.JoinState.REJOINING) {
return;
}
LocalStreamParams localStreamParams = LocalStreamParams.Builder()
.setInitialAudioMute(true)
.build();
channel.createLocalStream(localStreamParams);
Add metadata to a local stream
Your app can include metadata with a local stream. For example, you can include a user-specified name that other clients can display in the UI.
The metadata is permanent for the local stream, and members of the channel can read it when they receive a RemoteStream
object.
The StreamMetadata
object contains key-value pairs, where the keys are strings and the values are simple types (string, number, boolean, or null).
To add metadata to a stream, pass a map to setStreamMetadata()
when you create the LocalStreamParams
object:
import io.aircore.media.Channel;
import io.aircore.media.LocalStream;
import io.aircore.media.LocalStreamParams;
import io.aircore.media.StreamMetadata;
// Define key-value pairs to send to RemoteStream objects.
Map<String, Object> metaData = new HashMap<>();
metaData.put("stringValue", "hello");
metaData.put("integerValue", 33);
metaData.put("floatingPointValue", 33.6f);
metaData.put("booleanValue", true);
metaData.put("nullValue", null);
// Create a LocalStream object with a specific configuration.
LocalStreamParams localStreamParams = new LocalStreamParams.Builder()
// Set StreamMetadata map to be serialized for LocalStream.
.setStreamMetadata(metaData)
.build();
if (localStreamParams.getStreamMetadata() == null) {
// Could not construct a StreamMetadata object.
// For example, the encoded metadata may exceed the 2 kilobyte maximum.
// Try reducing the number of elements in |metaData| or reducing the length
// of their keys and values.
// Alternatively, proceed to create the local stream without stream metadata.
}
channel.createLocalStream(localStreamParams);
To read metadata, call a remote stream's getStreamMetadata()
method:
import io.aircore.media.RemoteStream;
import io.aircore.media.StreamMetadata;
StreamMetadata streamMetadata = remoteStream.getStreamMetadata();
if (streamMetadata == null) {
// No MetaData object associated with this stream.
} else {
Map<String, Object> metaData = streamMetadata.getData();
if (metaData == null) {
// MetaData object contains no key-value pairs.
} else {
// You can directly extract strings.
String s = (String) metaData.getOrDefault("stringValue", "default");
// Extract all numeric MetaData as Number. The subclass is not guaranteed.
int i = ((Number) metaData.getOrDefault("integerValue", 0)).intValue();
double d = ((Number) metaData.getOrDefault("floatingPointValue", 0.0f)).floatValue();
// You can extract boolean values as Boolean.
boolean b = (Boolean) metaData.getOrDefault("booleanValue", false);
// You can check just the key for key-value pairs with null values.
boolean hasProperty = metaData.containsKey("nullValue");
}
}
Get notifications for a local stream
Register a LocalStream.Delegate
object to track muting, voice activity, and connection status for a local stream.
import io.aircore.media.Channel;
import io.aircore.media.LocalStream;
import io.aircore.media.LocalStreamParams;
class LocalStreamDelegate implements LocalStream.Delegate {
// Implement other notification handlers.
// ...
@Override
public void audioMuteStateDidChange(boolean muted) {
if (muted) {
// The local stream is now muted.
} else {
// The local stream is now unmuted.
}
}
@Override
public void connectionStateDidChange(
LocalStream.ConnectionState newState,
LocalStream.ConnectionState oldState) {
switch (newState) {
case NOT_STARTED:
// The initial state, before starting the stream.
// A notification for NOT_STARTED is not sent to the delegate.
break;
case CONNECTING:
// The local stream is connecting.
break;
case CONNECTED:
// The local stream is connected.
break;
case TERMINATED:
// The local stream is terminated and its resources are being
// cleaned up.
break;
default:
}
}
@Override
public void voiceActivityStateDidChange(boolean isActive) {
if (isActive) {
// The currently captured audio contains human speech.
} else {
// The currently captured audio does not contain human speech.
}
}
}
LocalStreamDelegate localStreamDelegate = new LocalStreamDelegate();
LocalStreamParams params = new LocalStreamParams.Builder().build();
// Make sure that the channel is connected.
Channel.JoinState joinState = channel.getJoinState();
if (joinState != Channel.JoinState.JOINED && joinState != Channel.JoinState.REJOINING) {
return;
}
// Create a local stream.
LocalStream localStream = channel.createLocalStream(params);
if (localStream == null) {
// Can't create a stream.
}
// Register a delegate with the local stream before starting it to make sure
// that you receive all notifications.
localStream.addDelegate(localStreamDelegate);
Get notifications for added and removed remote streams
Register a Channel.Delegate
object to track when remote streams are added or removed:
import io.aircore.media.Channel;
import io.aircore.media.RemoteStream;
class RemoteStreamDelegate implements RemoteStream.Delegate {
// Implement other notification handlers.
// ...
}
class ChannelDelegate implements Channel.Delegate {
// Implement other notification handlers.
// ...
@Override
public void remoteStreamWasAdded(RemoteStream remoteStream) {
// Register to receive notifications about this stream's state.
remoteStream.addDelegate(new RemoteStreamDelegate());
// Handle newly added remote stream...
}
@Override
public void remoteStreamWasRemoved(RemoteStream remoteStream, RemoteStream.TerminationCause cause) {
switch (cause) {
case STOPPED:
// The stream has stopped normally.
break;
case NOT_FOUND:
// The stream does not exist when attempting to connect.
break;
case NOT_TERMINATED:
// The stream is still active. Not reported by this
// notification.
break;
case INTERNAL_ERROR:
// The stream encountered a fatal error condition.
break;
default:
}
// Handle removal of remote stream...
}
}
ChannelDelegate channelDelegate = new ChannelDelegate();
// Register a delegate with the channel before joining to make sure that you
// receive all notifications.
channel.addDelegate(channelDelegate);
Get notifications for remote stream connection status
Register a RemoteStream.Delegate
object to track changes to the connection status for a remote stream.
import io.aircore.media.Channel;
import io.aircore.media.RemoteStream;
class RemoteStreamDelegate implements RemoteStream.Delegate {
// Implement other notification handlers.
// ...
@Override
public void connectionStateDidChange(
RemoteStream remoteStream,
RemoteStream.ConnectionState newState,
RemoteStream.ConnectionState oldState) {
switch (newState) {
case CONNECTING:
// The remote stream is connecting.
break;
case CONNECTED:
// The remote stream is connected.
break;
case TERMINATED:
// The remote stream is terminated and its resources are being
// cleaned up.
// You can check the termination cause here.
RemoteStream.TerminationCause terminationCause = remoteStream.getTerminationCause();
break;
default:
}
}
}
RemoteStreamDelegate remoteStreamDelegate = new RemoteStreamDelegate();
class ChannelDelegate implements Channel.Delegate {
// Implement other notification handlers.
// ...
@Override
public void remoteStreamWasAdded(RemoteStream remoteStream) {
// Register to receive notifications about this stream's state.
remoteStream.addDelegate(remoteStreamDelegate);
// Handle newly added remote stream...
}
}
ChannelDelegate channelDelegate = new ChannelDelegate();
// Register a delegate with the channel before joining to make sure that you
// receive all notifications.
channel.addDelegate(channelDelegate);
More info
See the full API reference.
Download our sample app.