How to create a live video player app in Java using Android Studio

This guide focuses on the minimum required to start playback in an Android app written in Java and built using Android Studio 3.5.3. You can find Android Studio on developer.android.com/studio.

For simplicity, this example loads the latest broadcast available through the broadcast metadata API.

If you want to clone a sample project similar to this guide, see the Player example for Android project on our GitHub page.

Create a new Application

  • Open Android Studio
  • Choose Start a new Android Studio project
  • Select the Phone and Tablet form factor.
  • Choose the Empty Activity template.
  • Enter a suitable application name and your company domain.
  • Set the Minimum SDK to at least API 16: Android 4.1, which is the oldest API supported by the BroadcastPlayer.

Add the Bambuser SDK for Android

  • Log in to the Bambuser site and download the latest SDK bundle for Android from the Developer page.
  • Open the downloaded zip file and extract it.
  • In Android Studio, in the project tree on the left-hand-side, right click on your app module and choose Open Module Settings.
  • Click the + in the upper left corner of the list of modules.
  • Choose Import .JAR/.AAR Package.
  • Navigate to the SDK files you extracted and import the .aar file.

You may now need to close and reopen the Project Structure screen, for the library module to show up in the list. Some recent versions of Android Studio fail to add new modules to the settings.gradle file. If this happens, add include ':libbambuser' manually at the end of the file and sync the project, then reopen the Project Structure screen.

  • Switch to the Dependencies tab and keep the app module selected.
  • Click the + in the dependency pane and choose Module dependency.
  • Choose the imported library module, click OK

In order to do http requests in a convenient way, add a dependency on the OkHttp library.

  • Click the + in the dependency pane again and choose Library dependency.

  • Search for com.squareup.okhttp3 and choose version 3.12 of the library, as shown below. This is the last branch to support down to Android API 16.

The list of dependencies for the app should now contain both the libbambuser module and the okhttp library.

  • Click OK to return from the Project Structure screen.

Configure Gradle to build for supported architectures

The Bambuser SDK contains native code built for the armeabi-v7a, arm64-v8a, x86 and x86_64 ABIs. If you don't want to bundle all of them, or if other libraries contain native code for other architectures, an ABI filter is needed to ensure that the generated APK contains the greatest common denominator.

The armeabi-v7a and arm64-v8a ABIs are compatible with all modern devices and ARM emulator images. The x86 and x86_64 ABIs are in practice only necessary when developing on x86 emulator images, as real x86 devices can translate ARM machine code.

  • Open the build.gradle file for the app module.

  • Add an NDK ABI filter to the android defaultConfig block. For example:

android {
    defaultConfig {
        // ...
        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a'
        }
    }
}

Add required Android app permissions

The Bambuser player for Android requires at least the INTERNET permission.

  • Open the manifests/AndroidManifest.xml file in the Project tree on the left.

  • Add the permission tag before the <application /> tag:

<uses-permission android:name="android.permission.INTERNET"/>

Add a SurfaceView for video playback

  • In activity_main.xml replace the auto-generated TextView with a SurfaceViewWithAutoAR and a TextView for showing player state.

  • Give the container layout a name such as PlayerContentView so we can grab a reference to it later.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/PlayerContentView"
    tools:context=".MainActivity">

    <com.bambuser.broadcaster.SurfaceViewWithAutoAR
        android:id="@+id/VideoSurfaceView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>
    <TextView
        android:id="@+id/PlayerStatusTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/colorAccent"
        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

In the generated MainActivity.java import and add references to the SurfaceViewWithAutoAR and TextView from the layout.

import com.bambuser.broadcaster.SurfaceViewWithAutoAR;
import android.widget.TextView;
import android.view.View;
// ...

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ...
        mVideoSurface = findViewById(R.id.VideoSurfaceView);
        mPlayerStatusTextView = findViewById(R.id.PlayerStatusTextView);
        mPlayerContentView = findViewById(R.id.PlayerContentView);
    }

    SurfaceViewWithAutoAR mVideoSurface;
    TextView mPlayerStatusTextView;
    View mPlayerContentView;
}

Authentication

To access broadcasts, your app needs to identify itself to Bambuser. Head over to the Developer page on the Bambuser site and get the Sandbox applicationId which we'll use when constructing the BroadcastPlayer.

To access the metadata API, your app also needs an API key. Get an API key for your Sandbox environment from the Developer page on the Bambuser site.

Remember to replace the Sandbox id and key with a Production applicationId and a Production API key before you release your app!

Preparations for playback

Prepare MainActivity.java for integration of the BroadcastPlayer by importing the related classes, adding an implementation of the BroadcastPlayer.Observer interface, and inserting your Sandbox applicationId and API key.

import com.bambuser.broadcaster.BroadcastPlayer;
import com.bambuser.broadcaster.PlayerState;
// ...
public class MainActivity extends AppCompatActivity {
    private static final String APPLICATION_ID = "PLEASE INSERT YOUR APPLICATION SPECIFIC ID PROVIDED BY BAMBUSER";
    private static final String API_KEY = "PLEASE INSERT AN API KEY PROVIDED BY BAMBUSER";
    // ...
    BroadcastPlayer.Observer mBroadcastPlayerObserver = new BroadcastPlayer.Observer() {
        @Override
        public void onStateChange(PlayerState playerState) {
            if (mPlayerStatusTextView != null)
                mPlayerStatusTextView.setText("Status: " + playerState);
        }
        @Override
        public void onBroadcastLoaded(boolean live, int width, int height) {
        }
    };
    // ...
}

Get resourceUri for latest broadcast

To demonstrate how the Bambuser metadata API can be used, we fetch information about the latest broadcasts in your Sandbox environment, and choose the latest one for playback.

Import classes needed for OkHttp and JSON parsing, initiate an asynchronous http request to https://api.bambuser.com/broadcasts using your API key in the onResume() method, then parse the latest resourceUri from the results. The network request must be enqueued on a background thread, while anything related to the UI must be done on the main thread of the app.

Remember to cancel any outstanding http request and drop the reference to the SurfaceView in the onPause() method, to prevent a late response from causing playback in the background.

import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
// ...
public class MainActivity extends AppCompatActivity {
    // ...

    @Override
    protected void onPause() {
        super.onPause();
        mOkHttpClient.dispatcher().cancelAll();
        mVideoSurface = null;
    }

    @Override
    protected void onResume() {
        super.onResume();
        mVideoSurface = findViewById(R.id.VideoSurfaceView);
        mPlayerStatusTextView.setText("Loading latest broadcast");
        getLatestResourceUri();
    }

    void getLatestResourceUri() {
        Request request = new Request.Builder()
        .url("https://api.bambuser.com/broadcasts")
        .addHeader("Accept", "application/vnd.bambuser.v1+json")
        .addHeader("Content-Type", "application/json")
        .addHeader("Authorization", "Bearer " + API_KEY)
        .get()
        .build();
        mOkHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(final Call call, final IOException e) {
                runOnUiThread(new Runnable() { @Override public void run() {
                    if (mPlayerStatusTextView != null)
                        mPlayerStatusTextView.setText("Http exception: " + e);
                }});
            }
            @Override
            public void onResponse(final Call call, final Response response) throws IOException {
                String body = response.body().string();
                String resourceUri = null;
                try {
                    JSONObject json = new JSONObject(body);
                    JSONArray results = json.getJSONArray("results");
                    JSONObject latestBroadcast = results.optJSONObject(0);
                    resourceUri = latestBroadcast.optString("resourceUri");
                } catch (Exception ignored) {}
                final String uri = resourceUri;
                runOnUiThread(new Runnable() { @Override public void run() {
                    initPlayer(uri);
                }});
            }
        });
    }

    void initPlayer(String resourceUri) {
        if (resourceUri == null) {
            if (mPlayerStatusTextView != null)
                mPlayerStatusTextView.setText("Could not get info about latest broadcast");
            return;
        }
        if (mVideoSurface == null) {
            // UI no longer active
            return;
        }
    }
    // ...
    final OkHttpClient mOkHttpClient = new OkHttpClient();
}

Initiate a BroadcastPlayer

Import the BroadcastPlayer class, create an instance of it in the initPlayer(resourceUri) method, using your resourceUri, applicationId, SurfaceView and BroadcastPlayer.Observer.

Call load() to start loading the requested resourceUri.

Make sure to close the BroadcastPlayer when the Activity is no longer active.

import com.bambuser.broadcaster.BroadcastPlayer;
// ...
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onPause() {
        // ...
        if (mBroadcastPlayer != null)
            mBroadcastPlayer.close();
        mBroadcastPlayer = null;
    }

    // ...
    void initPlayer(String resourceUri) {
        // ...
        if (mBroadcastPlayer != null)
            mBroadcastPlayer.close();
        mBroadcastPlayer = new BroadcastPlayer(this, resourceUri, APPLICATION_ID, mBroadcastPlayerObserver);
        mBroadcastPlayer.setSurfaceView(mVideoSurface);
        mBroadcastPlayer.load();
    }

    // ...
    BroadcastPlayer mBroadcastPlayer;
}

Sizing the video surface

By default, the SurfaceViewWithAutoAR will use crop to fill, avoiding black borders. However, if the video is an entirely different aspect ratio than the user's screen, crop to fill will crop more than desired. In this case, it's often better to use shrink to fit.

  • Import Display and add a reference to the properties.

  • Import Point and implement a method that gets the Display dimensions.

  • Compare the video and display aspect ratio when a broadcast is loaded and set the scaling mode according to your liking.

import android.graphics.Point;
import android.view.Display;
// ...
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ...
        mDefaultDisplay = getWindowManager().getDefaultDisplay();
    }

    private Point getScreenSize() {
        if (mDefaultDisplay == null)
            mDefaultDisplay = getWindowManager().getDefaultDisplay();
        Point size = new Point();
        try {
            // this is officially supported since SDK 17 and said to work down to SDK 14 through reflection,
            // so it might be everything we need.
            mDefaultDisplay.getClass().getMethod("getRealSize", Point.class).invoke(mDefaultDisplay, size);
        } catch (Exception e) {
            // fallback to approximate size.
            mDefaultDisplay.getSize(size);
        }
        return size;
    }

    BroadcastPlayer.Observer mBroadcastPlayerObserver = new BroadcastPlayer.Observer() {
        // ...
        @Override
        public void onBroadcastLoaded(boolean live, int width, int height) {
            Point size = getScreenSize();
            float screenAR = size.x / (float) size.y;
            float videoAR = width / (float) height;
            float arDiff = screenAR - videoAR;
            mVideoSurface.setCropToParent(Math.abs(arDiff) < 0.2);
        }
    };
    // ...
    Display mDefaultDisplay;
}

Running the app

The above should be enough for playback of the latest video. Connect your mobile device to your PC and follow the Android Developers guide for running your app.

Provided that you have broadcast something to your Sandbox environment, the player should automatically find and play the latest broadcast.

Adding player controls

To use Android's standard MediaController widget, start by importing the class. Check the player state in the onStateChange(playerState) callback we added earlier. When an archived broadcast is successfully loaded, construct an instance of the MediaController, attach it to the BroadcastPlayer and anchor it to mPlayerContentView. Import the MotionEvent class and show / hide the MediaController when the user taps the player.

import android.widget.MediaController;
import android.view.MotionEvent;
// ...
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onPause() {
        // ...
        if (mMediaController != null)
            mMediaController.hide();
        mMediaController = null;
    }
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (ev.getActionMasked() == MotionEvent.ACTION_UP && mBroadcastPlayer != null && mMediaController != null) {
            PlayerState state = mBroadcastPlayer.getState();
            if (state == PlayerState.PLAYING ||
                state == PlayerState.BUFFERING ||
                state == PlayerState.PAUSED ||
                state == PlayerState.COMPLETED) {
                if (mMediaController.isShowing())
                    mMediaController.hide();
                else
                    mMediaController.show();
            } else {
                mMediaController.hide();
            }
        }
        return false;
    }
    // ...
    BroadcastPlayer.Observer mBroadcastPlayerObserver = new BroadcastPlayer.Observer() {
        @Override
        public void onStateChange(PlayerState playerState) {
            // ...
            if (playerState == PlayerState.PLAYING || playerState == PlayerState.PAUSED || playerState == PlayerState.COMPLETED) {
                if (mMediaController == null && mBroadcastPlayer != null && !mBroadcastPlayer.isTypeLive()) {
                    mMediaController = new MediaController(MainActivity.this);
                    mMediaController.setAnchorView(mPlayerContentView);
                    mMediaController.setMediaPlayer(mBroadcastPlayer);
                }
                if (mMediaController != null) {
                    mMediaController.setEnabled(true);
                    mMediaController.show();
                }
            } else if (playerState == PlayerState.ERROR || playerState == PlayerState.CLOSED) {
                if (mMediaController != null) {
                    mMediaController.setEnabled(false);
                    mMediaController.hide();
                }
                mMediaController = null;
            }
        }
        // ...
    };
    // ...
    MediaController mMediaController = null;
}

When playing an archived broadcast in the app, controls for pause, play and seeking should be available, as in the screenshot below.