This article details how to integrate CDN Mesh Delivery into your mobile app using Brightcove Player on Android.
Not into tutorials? |
Brightcove Player
The Brightcove Android Player SDK is a mobile player that provide tools for building video-enabled apps for Android.
To learn more about the Brightcove Player SDKs, please refer to their documentation.
Project setup
Dependencies
The easiest way to get the Streamroot SDK is to add it as a Gradle dependency. We assume you are using Android Studio with the latest tools updates as recommended by Google. If not, drop us a line.
repositories {
maven {
url 'https://sdk.streamroot.io/android'
}
}
Next, add the Streamroot dependency to the build.gradle file of your app module.
implementation 'io.streamroot.dna:dna-core:3.21.0'
Java version requirement
The latest versions of our SDK use the lambda feature of Java 8, which is supported by all Android devices (cf. Android documentation).
As mentioned in the official documentation, you have to configure your project with the following snippet.
android {
// Configure only for each module that uses Java 8
// language features (either in its source code or
// through dependencies).
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
Integrate Streamroot SDK
1. Set streamrootKey
Before doing anything, you must set the streamrootKey
as a meta-data in the application manifest.
<meta-data
android:name="io.streamroot.dna.StreamrootKey"
android:value="streamrootKey"/>
streamrootKey
: Refers to your Streamroot unique identifier that you will find in the Account section of the Streamroot dashboard. If you don't have a Streamroot Key, you can ask a free trial on our website.
It is also possible to pass your Streamroot Key at Mesh Delivery Client instantiation.
2. Create a new Mesh Delivery Client instance
Now that you have set the streamrootKey, you are able to create the Mesh Delivery Client instances.
dnaClient = DnaClient.newBuilder()
.context(<Context>)
.playerInteractor(BrightcoveInteractor(<Player>))
.latency(latency)
.start(Uri.parse(firstHlsSource.getUrl()));
Mandatory builder parameters:
context
: An instance of Context.playerInteractor
: An object that implementsPlayerInteractor
. See section below for more details.streamrootKey
: A method to pass your Streamroot Key. It will override the one set in the application manifest.latency
: A method that works for HLS and MPEG-DASH on live content. If it's set for a VoD content, this parameter will be ignored. The value must be strictly positive and in seconds. We recommend setting the value to 30s.start
: A method that will create and start a new instance of the SDK. It expects the URL of your stream.
Other DNA options can be called in the builder. Please refer to this article to learn more about them.
On Live streaming, to improve offload efficiency, we recommend to change the default value of the minimal buffer target to 10s. The maximum buffer target will be randomly generated between this minimum and the latency (30s as recommended above) . In order to do so, you need to change the instantiation of the default class LoadControlConfig
with this one:
LoadControlConfig loadControlConfig = new LoadControlConfig.Builder()
.setAllocatorConfig(allocatorConfig)
.setMinBufferMs(10000)
.setMaxBufferMs((DnaClient.generateBufferTarget(10000, 30000, 30000)))
.build();
3. Give your player the new URL
Once you have a running instance of the SDK, you must retrieve its URL and input it to your player instead of your original one.
The SDK exposes a manifestUrl method that will do it as expected.
dnaClient.manifestUrl : <Uri>
4. Implement the PlayerInteractor
The PlayerInteractor is how the SDK gets player specifics. The SDK will need the following information to work optimally:
- Looper
looper must return the looper used to create the player.
Loaded TimeRanges
loadedTimeRanges must return a list of TimeRange (in milliseconds).
Each TimeRange represents a continuous interval of content present in the buffer. It is mandatory to return at least the TimeRange going from the playback time to the end of the buffered interval containing the playback time. - Current playback time
playbackTime must return the current time of the player in milliseconds. - Buffer target
bufferTarget must return the target the player will try to buffer.
setBufferTarget allows us to dynamically change the buffer target of the player to optimize DNA performances.
import android.os.Looper;
import com.brightcove.player.display.ExoPlayerVideoDisplayComponent;
import com.brightcove.player.display.VideoDisplayComponent;
import com.brightcove.player.event.Event;
import com.brightcove.player.event.EventListener;
import com.brightcove.player.event.EventType;
import com.brightcove.player.view.BrightcoveExoPlayerVideoView;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List;
import io.streamroot.dna.core.PlayerInteractor;
import io.streamroot.dna.core.TimeRange;
final class BrightcoveInteractor implements PlayerInteractor {
private ExoPlayer mPlayer;
private BrightcoveExoPlayerVideoView brightcoveVideoView;
BrightcoveInteractor(BrightcoveExoPlayerVideoView brightcoveVideoView) {
this.brightcoveVideoView = brightcoveVideoView;
setExoWhenAvailable(brightcoveVideoView);
}
private void setExoWhenAvailable(BrightcoveExoPlayerVideoView brightcoveVideoView) {
this.brightcoveVideoView.addListener(EventType.DID_SET_VIDEO,new EventListener() {
@Override
public void processEvent(Event event) {
VideoDisplayComponent videoDisplayComponent = brightcoveVideoView.getVideoDisplay();
if (videoDisplayComponent instanceof ExoPlayerVideoDisplayComponent) {
// Get ExoPlayer
ExoPlayer exoPlayer = ((ExoPlayerVideoDisplayComponent) videoDisplayComponent).getExoPlayer();
if (exoPlayer instanceof SimpleExoPlayer) {
// Get SimpleExoPlayer
mPlayer = (SimpleExoPlayer) exoPlayer;
}
}
}
});
}
@NotNull
@Override
public List<TimeRange> loadedTimeRanges() {
if (mPlayer == null) { return Collections.emptyList(); }
long shift = getCurrentWindowShift();
long rangeDurationMs = mPlayer.getBufferedPosition() - mPlayer.getCurrentPosition();
return (rangeDurationMs > 0) ?
Collections.singletonList(new TimeRange(shift +
mPlayer.getCurrentPosition(), rangeDurationMs))
: Collections.emptyList();
}
@Nullable
@Override
public Looper looper() {
return (mPlayer == null) ? null : mPlayer.getPlaybackLooper();
}
@Override
public long playbackTime() {
return (mPlayer == null)
? brightcoveVideoView.getCurrentPosition()
: getCurrentWindowShift() + mPlayer.getCurrentPosition();
}
private long getCurrentWindowShift() {
Timeline current = mPlayer.getCurrentTimeline();
Timeline.Window timeLimeWindow = new Timeline.Window();
long shift = 0;
if (mPlayer.getCurrentWindowIndex() < current.getWindowCount()) {
mPlayer.getCurrentTimeline().getWindow(mPlayer.getCurrentWindowIndex(), timeLimeWindow);
shift = timeLimeWindow.getPositionInFirstPeriodMs();
}
return shift;
}
@Override
public double bufferTarget() { return 0; }
@Override
public void setBufferTarget(double target) {}
}
5. Implement the BandwidthMeter
The BandwidthMeter allows making an estimation of the bandwidth needed to download the video chunks. The SDK will need:
- A bandwidth listener
Implementation of Streamroot BandwidthListener for Brightcove Player.
Please refer to the example below.
import com.brightcove.player.display.ExoPlayerVideoDisplayComponent;
import com.google.android.exoplayer2.Player;
import io.streamroot.dna.core.BandwidthListener;
final class BrightcoveBandwidthListener implements BandwidthListener {
private ExoPlayerVideoDisplayComponent displayComponent;
BrightcoveBandwidthListener(ExoPlayerVideoDisplayComponent displayComponent) {
this.displayComponent = displayComponent;
}
@Override
public void onBandwidthChange(long estimatedBandwidth) {
displayComponent.setPeakBitrate((int) estimatedBandwidth);
}
}
It is then required to call the BandwidthListener in the builder.
dnaClient = DnaClient.newBuilder()
.context(<Context>)
.playerInteractor(BrightcoveInteractor(<Player>))
...
.bandwidthListener(<BandwidthListener>)
...
.start(<String>);
It is also requiered to pass the
bandwidthListener
during the player instantiation.
6. Integrate the QoS module (optional)
This QoS module will link Brightcove Player events to our SDK in order to get accurate metrics regarding the client session.
If this QoS module isn't implemented, a default module directly embedded in the SDK will be used instead.
Hereunder is an example of the class to add to the app in order to do so.
import com.brightcove.player.event.Default;
import com.brightcove.player.event.Event;
import com.brightcove.player.event.EventListener;
import com.brightcove.player.event.EventType;
import com.brightcove.player.view.BrightcoveExoPlayerVideoView;
import io.streamroot.dna.core.PlaybackState;
import io.streamroot.dna.core.QosModule;
final class BrightcoveQoSModule extends QosModule {
private BrightcoveExoPlayerVideoView brightcoveVideoView;
BrightcoveQoSModule(BrightcoveExoPlayerVideoView brightcoveVideoView) {
super();
this.brightcoveVideoView = brightcoveVideoView;
initListeners();
}
void initListeners() {
{
this.brightcoveVideoView.addListener(EventType.SOURCE_NOT_PLAYABLE, new EventListener() {
@Default
public void processEvent(Event event) {
playbackErrorOccurred();
}
});
this.brightcoveVideoView.addListener(EventType.COMPLETED, new EventListener() {
public void processEvent(Event event) {
playbackStateChange(PlaybackState.ENDED);
}
});
this.brightcoveVideoView.addListener(EventType.SEEK_TO, new EventListener() {
public void processEvent(Event event) {
playbackStateChange(PlaybackState.SEEKING);
}
});
this.brightcoveVideoView.addListener(EventType.DID_PLAY, new EventListener() {
public void processEvent(Event event) {
playbackStateChange(PlaybackState.PLAYING);
}
});
this.brightcoveVideoView.addListener(EventType.DID_STOP, new EventListener() {
public void processEvent(Event event) {
playbackStateChange(PlaybackState.ENDED);
}
});
this.brightcoveVideoView.addListener(EventType.DID_PAUSE, new EventListener() {
public void processEvent(Event event) {
playbackStateChange(PlaybackState.PAUSING);
}
});
this.brightcoveVideoView.addListener(EventType.WILL_INTERRUPT_CONTENT, new EventListener() {
public void processEvent(Event event) {
playbackErrorOccurred();
playbackStateChange(PlaybackState.ENDED);
}
});
}
}
}
It is then required to call the BrightcoveQoSModule in the builder.
dnaClient = DnaClient.newBuilder()
.context(<Context>)
.playerInteractor(BrightcoveInteractor((BrightcoveExoPlayerVideoView)brightcoveVideoView))
...
.qosModule(BrightcoveQoSModule((BrightcoveExoPlayerVideoView)brightcoveVideoView))
...
.start(<String>);
Stop the Mesh Delivery Client
Once the video is done playing, you have to stop the SDK you created earlier. Calling the following method will finish the ongoing tasks and release the resources.
dnaClient.close();
Network security configuration
Starting with Android 9 (API level 28), cleartext support is disabled by default. As Streamroot SDK works as a local HTTP proxy, it is required to do one of the following
- Activate back cleartext to reactivate all HTTP calls in the AndroidManifest.xml (Not recommended)
<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
<application
...
android:usesCleartextTraffic="true">
...
</application>
</manifest>
- Add 'localhost' in the allowed domains in a new network_security_config.xml file
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
</domain-config>
</network-security-config>
And add this file in the AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
<application
...
android:networkSecurityConfig="@xml/network_security_config">
...
</application>
</manifest>
More info can be found in the Android documentation.