​
​

    Start Here

    What is Aircore?

    Authentication

    Channels

  • Services

    • Cloud recording

      Quickstart
      API Reference
  • SDKs

    • Sync Audio

      iOS

      Quickstart
      Customization
      Samples
      Download SDK
      Releases
      API Reference

      Android

      (Compose)

      Quickstart
      Customization
      Sample
      Download SDK
      API Reference

      Android

      (View)

      Quickstart
      Customization
      Sample
      Download SDK
      API Reference

      Web

      Quickstart
      Customization
      Samples
      Download SDK
      API Reference

      Sync Chat

      iOS

      Quickstart
      Customization
      Samples
      Download SDK
      Releases
      API Reference

      Android

      (Compose)

      Quickstart
      Customization
      Sample
      Download SDK
      API Reference

      Android

      (View)

      Quickstart
      Customization
      Sample
      Download SDK
      API Reference

      Web

      Quickstart
      Customization
      Samples
      Download SDK
      API Reference

      Flex

      iOS

      (Audio and video)

      Quickstart
      More Options
      Sample
      Download SDK
      Releases
      API Reference

      Android

      (Audio)

      Quickstart
      More Options
      Sample
      Download SDK
      Releases
      API Reference

      Web

      (Audio)

      Quickstart
      More Options
      Download

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.

Note

When you update a channel, the new token must have the same user ID and the same channel ID. If either one doesn't match, the channel terminates.

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 is TERMINATED.

  • The value of terminationCause is SESSION_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:

stateDiagram-v2 notJoined: Not joined [*] --> notJoined notJoined --> Joining: join() called Joining --> Terminated: Remote or local\ntermination Joining --> Joined: join() completed Joined --> Rejoining: Network issues\nstart Rejoining --> Joined: Network issues\nstop Rejoining --> Terminated: Remote or local\ntermination Joined --> Terminated: Remote or local\ntermination

To build your UI, decide which actions to take in each join state:

NOT_JOINED
  • This is the initial state for new channels.
  • To connect to the channel, your app can automatically call join().
  • Or, you can use your UI to call join() when the user decides to connect.
JOINED and REJOINING
  • In the JOINED state, the channel is connected and receiving audio. In the REJOINING state, the channel is reconnecting.
  • Your app can automatically call createLocalStream() and start() to publish audio.
  • Or, you can call these methods when the user decides to publish audio.
TERMINATED
  • You can no longer use the channel.
  • You can show the user that the channel is disconnected.
  • You can also let the user create a new channel.

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:

  • MainFragment.kt

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.

  • on this page
  • Flex Audio SDK for Android: More options

  • Customize logging

  • Use a secret API key

  • Use channel join states with your UI

  • Create a muted stream

  • Add metadata to a local stream

  • Get notifications for a local stream

  • Get notifications for added and removed remote streams

  • Get notifications for remote stream connection status

  • More info