The Player Controller

The Player Controller

This chapter covers the details about the PlayerController class. The controller class is the primary way to interact with the video player backend. It is used to trigger interactions such as starting and pausing playback, or seeking to a given position. In addition, the controller provides ways to register callback listeners for various events issued by the player.

The controller is also used to configure certain parts of the player backend such as the handling of secondary screens and additional parameters that need to be sent when content is requested.

Basic Controls

The player controller offers the following methods for basic playback controls:

PlayerController.open()

The controller provides two methods to load content. The open(PlayerConfig) takes a PlayerConfig that contains all the mandatory information required to load content. open(Bundle) can be used to provide the mandatory playback information as well as addition configuration options. See Starting Playback from an Intent for an example on how to use a Bundle to configure the player.

PlayerController.play()

Call this method to start playback. Once enough data is provided, playback will start.

PlayerController.pause()

Call this method to pause playback.

PlayerController.setPosition(long)

Call this method to seek to a give position.

PlayerController.release()

Call this method to release the player if you want to use the same instance to play different content. You need to call this method before you can call open() again. Please note that you do not need to call this explicitly if you are using the PlayerView life cycle delegate (see Mandatory Callbacks) and you are leaving an activity. This method needs to be called usually only when you want to reuse a given controller instance and load different content.

PlayerController.destroy()

Call this method to release the player and all it resources. This needs to be called when the player activity is stopped or destroyed. Please note that you do not need to call this explicitly if you are using the PlayerView life cycle delegate (see Mandatory Callbacks).

Player Callbacks

The primary way to get information about the current playback state and events that occur during a playback session is to use the addPlayerListener(PlayerListener) method on the controller to register a new PlayerListener.

Please note that the SDK offers the AbstractPlayerListener class and we encourage you to use that instead of the interface implementation. It will allow you to overwrite only a subset of the callback methods and it will make updates to the listener API easier. In case we need to change the PlayerListener API (and for instance add another callback method), your application implementation will not be effected by this if you are relying on the abstract implementation rather than the interface.

Once a listener is added to the controller, the following callbacks will be triggered:

onError

The onError() will be called by the player if an error occur during preparation or playback. See Error Handling for more information about the different error scenarios.

onStateChanged

The onStateChanged() is called when the player switches state. See Player States for more information about the states and state transitions.

onSeekTo

The onSeekTo(long) will be called when the controller’s setPosition(long) is called.

onVideoSizeChanged

The onVideoSizeChanged() will be called whenever the size of the video changes. Please consider using a StreamingEventListener if you are interested in resolution and bitrate changes for analytical purposes. See Streaming and Playback Events for more information.

onSeekRangeChanged

The onSeekRangeChanged() is triggered when the available seek range changes. This is the event you should use if you are implementing DVR playback of live events. In these cases, the seek range is dynamic and the player will report a duration of a region of media currently available for playback. Use the seek range reported by this event.

onPlaybackPositionChanged

The onPlaybackPositionChanged() is triggered regularly but at most once per second. Use this callback if you need to update seekbars or the current playback position in your UI. This event will not be triggered when playback is paused.

onDisplayChanged

The onDisplayChanged() is the callback triggered to handle secondary displays. See Secondary Displays for more information about how to configure the SDK to handle multiple displays and screen mirroring.

onDurationChanged

The onDurationChanged() is the callback triggered when the stream duration has been refreshed.

onPlayerModelChanged

The onPlayerModelChanged() is the callback triggered when the player model has been changed and the PlayerController can now be queried for available video tracks and qualities, audio tracks and subtitle tracks and also selected tracks. The player model is changed every time upon the playback initial start-up or the media period change.

onFullyBuffered

The onFullyBuffered() is the callback triggered when the player has successfully loaded the content until its end. From this point onwards no more network activity is required to finish playing the current media. This can be useful to perform some other network-demanding activity without affecting the playback.

Player States

During preparation and playback, the player transitions through different states. The states are reported through an attached PlayerListener (see Player Callbacks).

_images/player_states.png

The possible player states and their transitions.

The following states are reported by the player:

Idle

The Idle state is the initial state of the player when no data is loaded and no preparation is in progress. The player also goes back to the Idle state when it is released.

Preparing

The player goes into Preparing state after a content URL was passed through one of the open() methods in the controller.

Buffering

When the buffers do not contain enough data to start or continue playback, the player will be in the Buffering state.

Pausing

When enough data is available to play but playback is paused, the player is in the Pausing state.

Playing

The player switches to the Playing state when enough data is available and playback is enabled.

Finished

Once the end of a stream is reached, the player goes into the Finished state.

Error Handling

Because all player preparations and most of the playback operations occur asynchronously, errors are reported through the PlayerListener (see Player Callbacks) and you are encouraged to attach at least one listener to the PlayerController once it is available and before opening any content. Opening content might already raise an issue, for example if the Manifest URL does not provide data, and these issues will only be reported through the onError callback in the listener.

You can find an exhaustive list of the errors in the SDK alongside their descriptions in the reference for the CastlabsPlayerException class. The error constant values can be found in here.

The onError callback provides a CastlabsPlayerException that contains details about the error event and wraps the original exception that caused the issue. The API documentation of the CastlabsPlayerException contains information about the different errors that are reported by the system.

The PlayerController is already released before the onError() is executed in case of fatal errors. The application may need to know the playback state before the PlayerController is released and for that it shall listen for the onFatalErrorOccurred() and grab the playback state or any other data from the PlayerController like currently playing tracks etc.

The application may also want to handle some specific fatal errors and re-start the playback accordingly. The recommended way is to listen for onFatalErrorOccurred() to get any needed data or player config from the PlayerController and then open playback with saved PlayerConfig in onError():

playerView.getPlayerController().addPlayerListener(new AbstractPlayerListener() {
    PlayerConfig playerConfig = null;

    @Override
    public void onFatalErrorOccurred(@NonNull CastlabsPlayerException error) {
        super.onFatalErrorOccurred(error);
        playerConfig = playerView.getPlayerController().getPlayerConfig();
    }

    @Override
    public void onError(@NonNull CastlabsPlayerException error) {
        super.onError(error);
        if (playerConfig != null) {
            playerView.getPlayerController().open(playerConfig);
        }
    }
});

Video Resolution Filters

The SDK default setting is to allow HD content playback only for unencrypted clear content or if a DRM is used that provides video decryption and rendering through a secure media path. The latter requirement is met on most Android devices with API level > 18 and Widevine DRM. Also the PlayReady implementation on devices such as the Amazon FireTV is considered safe. The OMA DRM implementation on the other hand is a pure software-based DRM solution and does not offer a fully protected media path.

The DrmUtils class provides static methods to query the available DRM systems and their security level on a given device.

The PRESTOplay SDK for Android allows playback of HD content. The PlayerController can be configured through the PlayerController.setHdPlaybackEnabled(int) method programmatically or through the SdkConsts.INTENT_HD_CONTENT_FILTER Intent parameter. The setting is a bit field that can be constructed from the following constants:

SdkConsts.ALLOW_HD_CLEAR_CONTENT

If this bit is set, the player allows playback of HD content for clear unprotected content.

SdkConsts.ALLOW_HD_DRM_SECURE_MEDIA_PATH

If this bit is set, the player allows playback of DRM protected HD content only of the selected DRM system support a secure media path and its security level is reported as SecurityLevel.SECURE_MEDIA_PATH.

SdkConsts.ALLOW_HD_DRM_ROOT_OF_TRUST

If this bit is set, the player allows playback of DRM protected HD content only if the selected DRM system supports a root of trust security and its security level is reported at least as SecurityLevel.ROOT_OF_TRUST.

SdkConsts.ALLOW_HD_DRM_SOFTWARE

If this bit is set, the player allows playback of DRM protected HD content. All DRM systems offer at least software protection.

SdkConsts.ALLOW_HD_NEVER

If this bit is set, the player will never allow HD content playback.

See DRM Systems and Security Levels for more information on security levels.

The flag is evaluated when the playback manifest is loaded and applied to all video representations after the manifest is parsed. Discarded representations will not be listed in the controller.

You can apply these filter setting programmatically:

playerView.getPlayerController().setHdPlaybackEnabled(
       SdkConsts.ALLOW_HD_CLEAR_CONTENT | SdkConsts.ALLOW_HD_DRM_SECURE_MEDIA_PATH);

The HD filter setting can also be configured using an Intent parameter:

Intent intent = ...
intent.putExtra(SdkConsts.INTENT_HD_CONTENT_FILTER,
        SdkConsts.ALLOW_HD_CLEAR_CONTENT | SdkConsts.ALLOW_HD_DRM_SECURE_MEDIA_PATH
        );

Beside the per-controller configuration, you can also configure the HD filter globally using the PlayerSDK.PLAYBACK_HD_CONTENT field.

This is not the only filter that might discard video representations. The SDK includes a content-to-device resolution matching.

When a user plays an adaptive bitrate format (such as MPEG-DASH, HLS, or Smooth Streaming) on their device, the maximum bitrate resolution the SDK will stream from the content’s available file-set will always be the best-quality match based on the device’s resolution limit.

This is done to provide the best picture quality on a user’s device while avoiding unnecessary bandwidth usage.

Examples:

  • A stream is available in four resolutions (480p, 720p, 1080p, and 4K), but the user’s device only supports up to 1080p. In this case, the 1080p bitrate would be the maximum resolution streamed, with the 4K bitrate ignored. This is because the 4K resolution would need to be down-sampled to 1080p in order to display on the device’s screen which would waste bandwidth and provide no benefit to picture quality.

  • A stream is available in three resolutions (480p, 720p, and 1090p), but the user’s device only supports up to 1080p. In this case, the 1090p bitrate would be the maximum resolution streamed. Even though the user’s device can only render 1080p, down-sampling the video to 1080p would provide a superior picture quality compared to using the 720p bitrate option.

Please note that this field is a bit-field. To disable all HD filtering, you have to enable it for all scenarios:

intent.putExtra(SdkConsts.INTENT_HD_CONTENT_FILTER,
    SdkConsts.ALLOW_HD_CLEAR_CONTENT
    | SdkConsts.ALLOW_HD_DRM_SOFTWARE
    | SdkConsts.ALLOW_HD_DRM_ROOT_OF_TRUST
    | SdkConsts.ALLOW_HD_DRM_SECURE_MEDIA_PATH);

Player Model and Track Selection

Once the controller has loaded content, it’s internal player model is created and the event onPlayerModelChanged() is sent. At this point, you can get information about the loaded content from the controller such as duration, available and selected audio tracks, subtitles, video tracks and qualities.

PlayerController.getAudioTracks()

This method exposes a list of AudioTrack instances. You can get the currently selected audio track using getAudioTrack() and select a track using setAudioTrack(AudioTrack).

PlayerController.getSubtitleTracks()

This method exposes a list of SubtitleTrack instances. You can get the currently selected subtitle track using getSubtitleTrack() and select a track using setSubtitleTrack(SubtitleTrack). Note that the playback will not be blocked whilst the subtitles are loading.

PlayerController.getVideoQualities()

This method exposes a list of VideoTrackQuality instances. You can get the currently selected quality using getVideoQuality() and manually select a quality using setVideoQuality(VideoTrackQuality). Please note that manually selecting a video quality will disable adaptive bitrate switching. You can pass null to re-enable adaptive bitrate switching.

In addition to these setters, you can configure audio, subtitle, and video quality selection before you open content. For example, if the user has a known preference for a specific language or quality setting. You can control the initial selections programmatically using the PlayerConfig for audio and subtitle selections and the AbrConfiguration for the video quality. Alternatively, you can use Intent parameters with following keys:

When selecting an initial video quality, you can also leverage the constants SdkConsts.VIDEO_QUALITY_LOWEST and SdkConsts.VIDEO_QUALITY_HIGHEST to select the lowest or highest available bitrate respectively.

Because the initial track selection happens before the content is loaded and the track model is generated, the initial selection is index based. For example, you can get the current index for the audio tracks using:

PlayerController controller = playerView.getPlayerController();
AudioTrack currentAudioTrack = controller.getAudioTrack();
if (currentAudioTrack != null) {
    int currentTrackIndex = controller.getAudioTracks().indexOf(
        currentAudioTrack);
}

If you want to save the current playback state including the selections for audio and subtitle tracks into a Bundle, please consider using the saveState(Bundle) method. This method will save the current player state with all supported Intent parameters into the given bundle. The bundle can then be used to resume playback with the exact same settings using the controller’s open(Bundle) method. All states will be recovered, except for the video quality settings.

The video quality will be stored as the initial quality in the bundle if adaptive bitrate switching is turned off and the quality was selected manually. In that case, adaptive bitrate switching will be turned off when the bundle is used to re-open content.

In case all tracks of the particular type e.g. video, audio or text are filtered out, the FilterException is raised with the reason being set. This exception is not critical and will not stop the playback thus enabling the application to handle the exception on its own way.

Video track selection

When multiple video tracks (e.g. DASH Adaptation sets) are present in the manifest, the PRESTOplay SDK for Android will select the default one to play based on the following:

  • The track with maximum number of pixels to display and

  • The track with preferred codec type.

Preferred codec types are defined as codec weights and can be customized by changing the parameters

By default, the preferred codec type is H265 with hardware acceleration having the weight 1.0f.

Video tracks merging option

This option is currently available only for DASH streams and is enabled by default. When needed, it can be switched off or on either globally with the PlayerSDK.MERGE_VIDEO_TRACKS or per play session with SdkConsts.INTENT_MERGE_VIDEO_TRACKS.

When multiple video Adaptation sets are present in the manifest the PRESTOplay SDK for Android will try to merge them according to their Representation types thus allowing the ABR algorithm to walk over the video Adaptation sets. The default types are SD, HD and UHD.

  • Example: there are two SD (SD1, SD2) and two HD (HD1, HD2) Adaptation sets, the merge will result in four video tracks with each of them having SD+HD combinations: SD1+HD1, SD1+HD2, SD2+HD1, SD2+HD2.

The default Representation types can be overwritten by implementing custom TrackTypeProvider.

Audio track selection

Audio track selection can be achieved also via INTENT, through which you can select a specific track within an audio track group. Two configuration parameters have to be used for this INTENT_AUDIO_TRACK_IDX and INTENT_AUDIO_TRACK_GROUP_IDX.

INTENT_AUDIO_TRACK_GROUP_IDX sets the index of the audio adaptation set you want to select, while INTENT_AUDIO_TRACK_IDX specifies the index of the representation inside that specific adaptation set.

You can use our example from demos application in SDK examples:

new Demo("Simple Playback (Clear)", "Plays an unencrypted stream using a PlayerView",
SimplePlaybackDemo.class,
    new BundleBuilder()
        .put(SdkConsts.INTENT_URL, "http://demo.cf.castlabs.com/media/TOS/abr/Manifest_clean_sizes.mpd")
        .put(SdkConsts.INTENT_AUDIO_TRACK_IDX, 0)
        .put(SdkConsts.INTENT_AUDIO_TRACK_GROUP_IDX,2)
        .get())

In this example you are selecting first representation in the 2nd adaptation set with ID tearsofsteel_4k.ITA_aac.mp4.

Adaptive Bitrate Switching

When multiple Representations or Renditions are available, the player will try to automatically use the Representation best suited for the current network environment. This is called Adaptive Bitrate Switching (ABR).

The Intent Parameter SdkConsts.INTENT_ABR_CONFIGURATION can be used to specify a custom ABR configuration. The ABR configuration is provided as an instance of AbrConfiguration, which controls the details of the algorithm used to decide which Representation to play at a given time.

ABR Log Statements And Parameters

The ABR algorithm will log information for each decision. This log statements contain the most crucial information considered by the Algorithm and the resulting selection. For example:

VideoTrackSelection: Track selection [T: Initial (ABR)] [BT: C:00:26|Min:00:15|Max:01:00] [BM: 30.47% C:4.88 MB|T:16 MB] [BE: 6.8 Mbps (Effective: 5.1 Mbps Fraction: 0.75)] [SB: 00:10/00:25] [I: 0/4] [F: 960x540 @ 1.6 Mbps avc1.4D401F 25.00 fps]

The following fields are logged:

  • T - ABR type and mode. This idicates a reason for a specific selection, i.e ABR or Manual

  • BT - Buffer Times. Information about the current Buffer (C) and the minimum and maximum parameters of the current BufferConfiguration. These parameters are used to control the fill and drain behaviour of the buffer (see API docs for BufferConfiguration for more information).

  • BM - Buffer Memory. The current buffer memory in percentage as well as in bytes (C) and the target buffer size in bytes (T). Note that the target size is not a hard bound. If the player needs more memory to fullfill the current buffer configuration with respect to minimum times, the memory bound might be exceeded.

  • BE - Bandwidth Estimation. The reported bandwidth estimation for the current ABR decision. The Effective value is what is used by the algorithm. The Fraction controls how the effective value is calcualted and can be controlled through the configurations bandwidthFraction parameter.

  • SB - Switch Boudaries. The configurations minDurationForQualityIncreaseUs and maxDurationForQualityDecreaseUs. These parameters configure how much data need to be available in the buffer before the ABR algorithm is allowed to switch up (min) and until how much data in the buffer down switches by the ABR algorithm are prevented (max).

  • I - The selected Index and number of available formats. The formats are ordered in decreasing orders, which means that a selection of 0 represents the selection of the highest available bitrate.

  • F - The selected video rendition Format

Common Media Server Data (CMSD) header

The PRESTOplay SDK for Android implements parsing of the CMSD-Dynamic header, defined in CTA-5006 , in order to extract the backend-informed bandwidth estimation. Using such estimation could result beneficial in cases where the client-side estimation cannot be carried out with accuracy, such as in ultra low-latency scenarios.

To allow the PRESTOplay SDK for Android to use this information when available, you must enable the usage of the CMSD header in the AbrConfiguration:

AbrConfiguration abrCfg = new AbrConfiguration.Builder()
    .useCMSD(true)
.get();

In case the header is not found or cannot be properly parsed, the Player will fall back to using whatever ABR method has been selected.

Additional Query Parameter

If you need to send additional parameters when downloading content, i.e. MP4 segments or Manifest data, you can use the PlayerController to set those parameters. This is required for example when you are using a CDN where you need to pass a token to access the data.

The player supports two kinds of parameters:

  • Header Parameters are added to the HTTP header of the request

  • Query Parameters are appended to the request URL directly

You have two possibilities to set additional parameters. Programmatically using the PlayerController or using Bundle parameters (see Starting Playback from an Intent).

If you prefer to set the parameters programmatically, use the PlayerController.getDataSourceFactory() to access the DataSourceFactory of the controller. If no factory was set explicitly, the default implementation will be returned.

The DataSourceFactory is used by the controller to create requests and it provides the DataSourceFactory.addHeaderParameter(java.lang.String, java.lang.String) method to add header parameters and DataSourceFactory.addQueryParameter(java.lang.String, java.lang.String) to add query parameters. For example:

If you are using a Bundle to configure the player controller, an alternative approach is to add the additional parameters directly to the Bundle. The PlayerController will set them automatically. For example:

Bundle headerParams = new Bundle();
headerParams.putString("t", "15128381afe8e8f7a6e");

Bundle queryParams = new Bundle();
queryParams.putString("e", "12345");

Intent intent = ...
intent.putExtra(SdkConsts.INTENT_QUERY_PARAMS_BUNDLE, queryParams);
intent.putExtra(SdkConsts.INTENT_HEADER_PARAMS_BUNDLE, headerParams);
...

To store the additional parameters in the Intents’ bundle, you need to put the keys and values into a separate Bundle that works as a key/value map and store that Bundle in the Intent using either SdkConsts.INTENT_QUERY_PARAMS_BUNDLE or SdkConsts.INTENT_HEADER_PARAMS_BUNDLE as key.

Request Modifiers

Since version 4.1.2 you can also use the new RequestModifier. Request modifiers can be added on the PlayerController (see addRequestModifier()) and you should do that before you load content. The modifier will then receive a callback with a Request instance that you can modify to change the URI or add header parameters.

Note that you can also add query parameters by changing the URI directly. request.getUri().buildUpon() will give you a builder that can be used to change the URI. Once changed you can set the new URI in the request.

Here is an example of a request modifier that adds a header parameter and a query parameter for all manifest requests:

// We need the player controller to attach a modifier
PlayerController pc = ...

// Add a custom modifier that add a header and query parameter
pc.addRequestModifier(new RequestModifier() {
    @NonNull
    @Override
    public Request onRequest(@NonNull Request request) {
        // Add a header parameter
        if (request.type == Request.DATA_TYPE_MANIFEST){
            request.headers.put("Header-Key", "Header Value");

            // add a query parameter to the URI by building upon the current URI
            // and then set the new URL
            request.setUri(request
                    .getUri()
                    .buildUpon()
                    .appendQueryParameter("Query-Param", "Query Value")
                    .build()
            );
        }
        return request;
    }
});
...

Please note that request modifiers are evaluated _after_ header and query parameter modifications of the underlying data source factory are applied. That means that request modifiers will not interfere with any existing setups but can be used as a new extension. All header and query modifications that you apply using the DataSourceFactory exposed by the controller will also apply to all request modifier. Modifiers however will grant you more fine grained control over the request type and you can differentiate for example between manifest and segment requests.

Network Configuration

The underlying network configuration can controlled through the NetworkConfiguration. The configuration can be passed as an intent parameter when configuring the player and allows you to specify network timeouts globally as well as for Manifest and Segment downloads separately. In addition, the network configuration can be used to configure the retry behaviour of the player through the RetryConfiguration. The retry configuration specifies how often and with which delays the player will try to download a segment or manifest after a loading error such as a 404 response from the server.

The following example shows how the configuration can be used:

bundle.put(SdkConsts.INTENT_NETWORK_CONFIGURATION, new NetworkConfiguration.Builder()
    // You can configure the global connection and read timeouts.
    // The connection timeout is used when a connection is opened
    // while the read timeout is used when we read data from an
    // open connection. Using the general method will use the values
    // for both manifests and segment downloads
    .connectionTimeoutMs(5000)
    .readTimeoutMs(8000)

    // You can also specify the timeouts separately for
    // manifest and segments downloads
    .manifestConnectionTimeoutMs(3000)
    .manifestReadTimeoutMs(5000)
    .segmentsConnectionTimeoutMs(8000)
    .segmentsConnectionTimeoutMs(10000)

    // Configure the global retry behaviour. This will be applied
    // for both segment and manifest. Please take a look at the
    // API documentation of the RetryConfiguration for more
    // information about the effect of the parameters and how
    // the backoff algorithm works
    .retryConfiguration(new RetryConfiguration.Builder()
            // We allow for 5 attempts at most. That means the
            // first load and then at most 4 retries in case
            // of any download errors
            .maxAttempts(5)
            // We configure the base delay for the first retry
            .baseDelayMs(500)
            .get())

    // Similar to the timeouts you can also set separate
    // retry configurations for manifest and segment downloads
    .manifestRetryConfiguration(new RetryConfiguration.Builder()
            .maxAttempts(2)
            .baseDelayMs(1000)
            .get())
    .segmentsRetryConfiguration(new RetryConfiguration.Builder()
            .maxAttempts(4)
            .baseDelayMs(800)
            .get())
    .get());

CDN fallback feature

(DASH-only)

It is possible to provide more sources of the main content so that the player has a fallback URL to continue playing in case of a download error. There are two ways to inform the player of alternative URLs: either in-manifest BaseURLs or with installed custom ExternalSourceSelector.

DASH multiple BaseURLs can be specified in the manifest on different layers, e.g. top layer:

<MPD type="static" xmlns="urn:mpeg:dash:schema:mpd:2011" >
    <ProgramInformation moreInformationURL="www.castLabs.com"/>
    <BaseURL>https://content.url1</BaseURL>
    <BaseURL>https://content.url2</BaseURL>
    <Period id="0" > ...

PRESTOplay SDK for Android has the default implementation of InternalSourceSelector handling multiple URLs, where in case of the first URL being unavailable, the next one is automatically tried and so on. If the last URL in the list fails then the playback exception is generated. It is also possible to install a custom InternalSourceSelector to PlayerController, for instance the following selector restores the stock ExoPlayer behavior when the first BaseURL is always selected:

PlayerController pc = ...
pc.setInternalSourceSelectorFactory(new FirstEntrySourceSelector.Factory());

Another way to configure the CDN fallback is to install a custom ExternalSourceSelector allowing to re-download fallback manifests and perform soft or hard (not identical content) restart of the playback:

PlayerController pc = ...
pc.setExternalSourceSelector(new ExternalSourceSelector() {
     @Nullable
     @Override
     public SourceData onError(@NonNull String manifestUrl, @NonNull Exception error) {
         return new SourceData("https://content.url1");
     }

     @Nullable
     @Override
     public PlayerConfig onRestart(@NonNull String manifestUrl, @NonNull PlayerConfig playerConfig) {
         return new PlayerConfig.Builder(playerConfig)
                 .contentUrl("https://content.url1")
             .get();
         }
     });

Since there are multiple fallback mechanisms in place, here is the list of actions in decreasing priority being taken when the content becomes unavailable:

Retries

Re-trying the same content URL according to configured RetryCounter

Multiple BaseURLs in the manifest

InternalSourceSelector tries to select a different URL

Temporary blacklisting (ABR and video-only, HTTP errors 404 and 410)

The currently downloaded quality is blacklisted and the closest is selected

ExternalSourceSelector

If ExternalSourceSelector is installed then it tries to use a fallback URL

CastlabsPlayerException is raised and the playback is released.

Sideloaded Subtitle Tracks

You can use the PlayerController to side-load additional subtitles tracks that are not part of your Manifest or HLS playlists. Please note that you will need to do the side-loading before you use open() on the PlayerController to load the Manifest or playlist.

Loading additional subtitles can be done either programmatically through the controllers addSubtitleTrack() or using SdkConsts.INTENT_SUBTITLE_BUNDLE_ARRAYLIST as the key and storing the additional tracks as Intent parameters.

Adding a subtitle track through the controller looks like:

playerView.getPlayerController().addSubtitleTrack(
        "http://myserver.com/extra_subtitles.vtt", // the URL
        SdkConsts.MIME_TYPE_VTT, // The mime type
        "en", // Two letter language code (optional)
        "English Subtitles" // Name (optional)
);

The method expects a URL and a MIME type as mandatory parameters. The last two parameters are a two letter language code and a name and. Both can be null.

Adding the same track through the Intent would look like:

ArrayList<Bundle> tracks = new ArrayList<>();
Bundle bundle = new Bundle();
bundle.putString(SdkConsts.INTENT_SUBTITLE_URL, "http://myserver.com/extra_subtitles.vtt");
bundle.putString(SdkConsts.INTENT_SUBTITLE_MIME_TYPE, SdkConsts.MIME_TYPE_VTT);
bundle.putString(SdkConsts.INTENT_SUBTITLE_LANGUAGE, "en");
bundle.putString(SdkConsts.INTENT_SUBTITLE_NAME, "English Subtitles");
tracks.add(bundle);

Intent intent = new Intent(...);
intent.putExtra(SdkConsts.INTENT_SUBTITLE_BUNDLE_ARRAYLIST, tracks);
...

The subtitle track information is added to a Bundle and a list of these bundles is added to the Intent. The PlayerController will then take care of adding the side-loaded track when the Intents’ bundle is passed to the controller’s open() method.

Streaming and Playback Events

If you are interested in download and bitrate statistics, the SDK offers the StreamingEventListener interface. Instances of this listener can be registered using the controller’s addStreamingEventListener() method. The following callbacks will be triggered during playback:

onVideoFormatChange

This callback will be triggered when the video format changes. The passed Format instance and the VideoTrackQuality contain information about the new format and its bitrate.

onAudioFormatChange

The same as the onVideoFormatChange callback, but this time for the audio track.

onLoadCompleted

Triggered when a download completed.

onLoadStarted

Triggered when a download was started.

onLoadCanceled

Triggered when a download was canceled.

onUpstreamDiscarded

Invoked when data is removed from the back of the buffer, typically so that it can be re-buffered using a different representation.

Both the video and audio format change callbacks provide information about the trigger that caused the format change (the second parameter of the callback method). The following triggers are currently reported:

Trigger

Description

TRIGGER_UNSPECIFIED

The reason for the trigger is unknown

TRIGGER_INITIAL

The initial format selection triggered the event

TRIGGER_MANUAL

The manual format selection triggered the event

TRIGGER_ADAPTIVE

The adaptation algorithm triggered this event and selected a format

All the download-related callbacks provide information about the track that caused the event as a first parameter. The following tracks are currently supported and report download events:

Track Type

Description

PlayerController.VIDEO_RENDERER

Events triggered by the video track

PlayerController.AUDIO_RENDERER

Events triggered by the audio track

PlayerController.TEXT_RENDERER

Events triggered by the subtitle track

Live Playback

Live playback is supported for MPEG-DASH, SmoothStreaming, and HLS. The setup and configuration is the same as for video-on-demand (VoD) content. In order to check whether the stream is live or not isLive() can be used. If you want to provide the ability to seek back in the live stream, you can use the onSeekRangeChanged() callback of the PlayerListener. This callback will provide information about the available window in which you can seek in the stream. If you want to seek back to the live edge of the stream, specify a position beyond the current seek range, and the player will seek to the current live edge. Additionally the current position of the playback since epoch can be calculated as getLiveStartTime() + getPosition().

Live configuraiton

There’s a configuration parameter which offers fine-grained control over a set of live-related settings. You can get an instance of the LiveConfiguration, object through its Builder.

You can set this parameter in an intent Bundle with the SdkConsts.INTENT_LIVE_CONFIGURATION key, or with the PlayerConfig’s liveConfiguration field.

Please refer to the LiveConfiguration docs to get a detailed description of each configurable field.

Secondary Displays

Secondary screens in this context are supplementary displays connected through an HDMI connection or Wifi screen mirroring. The default behavior of the player is to prevent playback on secondary screens for protected content if the secondary screen does not provide a secure media path. Unprotected content can always be displayed on a secondary screen.

You can configure the behavior using the controller’s PlayerController.setSecondaryDisplay(int) method explicitly, or send the configuration through the Intent bundle using the SdkConsts.INTENT_SECONDARY_DISPLAY key.

When a secondary display is connected, two things happen:

  • The PlayerListener onDisplayChanged() callback will be triggered, containing information about the display. This callback is also triggered when the display is disconnected and the last parameter of the method indicates if playback will continue.

  • If the secondary display settings prevent playback while the display is connected, a CastlabsPlayerException is thrown to the onError callback of any connected PlayerListener. The type of the exception will be CastlabsPlayerException.TYPE_SECONDARY_DISPLAY.

Beside the per-controller configuration, you can also configure this behavior globally using the PlayerSDK.SECONDARY_DISPLAY field.

Tunneling Support

Since API 21 some Android devices, especially Android TV, support “tunneling” or “tunneled video playback”. Since version 4.0.0 the PRESTOplay SDK for Android supports tunneling and you can enable it either in the PlayerConfig or by setting the Intent Bundle Parameter SdkConsts.INTENT_TUNNELING_ENABLED to true when you open content. This will only have an effect on devices that support tunneling.

Please note that although tunneled playback provides some advantages on supported platforms such as better AV sync, support for HDR, and better performance for 4k and high frame rate content, it also has some limitations:

  • You can only use a SurfaceView to render the video, which mean 360 video playback will not be possible.

  • Tunneling is only supported if the content has both an active video and audio track.

  • Not all codec implementations on a device might be supported.

Playlist Support

The PRESTOplay SDK for Android provides Playlist-like behaviour through the Playlist interface.

Currently there are two implementations of the Playlist interface.

MultiControllerPlaylist

This implementation takes an optional PlayerView and uses underlying PlayerControllers for seamless transition.

In order to use the MultiControllerPlaylist, first you must create it and attach a listener to it. This listener will be called on Playlist item change, and is where you should place your UI unbind/bind logic to for the next PlayerController.

As with the PlayerController, the MultiControllerPlaylist exposes open(...) methods. Although in this case, an array or list of Items is expected.

// ...

// Create MultiControllerPlaylist, and bind it to our PlayerView
multiControllerPlaylist = new MultiControllerPlaylist(playerView);

// Add listener to MultiControllerPlaylist
multiControllerPlaylist.setListener(new MultiControllerPlaylist.PlaylistListener() {
    @Override
    public void onItemChange(@NonNull PlayerConfig newConfig, @Nullable PlayerController oldController, @Nullable PlayerController newController) {
        // Here we need to unbind from the old controller and bind to the new one
        // we also use the migratePlayerListeners helper method
        MultiControllerPlaylist.migratePlayerListeners(oldController, newController);
        playerControllerView.unbind();
        PlayerControllerProgressBar progressBar = findViewById(R.id.progress_bar);
        progressBar.unbind();

        if (newController != null) {
            playerControllerView.bind(playerView);
            progressBar.bind(newController);
        }
    }
    @Override
    public void onControllerCreate(@NonNull PlayerController playerController) {
    }

    @Nullable
    @Override
    public PlayerConfig onControllerLoad(@NonNull PlayerConfig config, @NonNull PlayerController playerController) {
        return config;
    }

    @Override
    public void onControllerRelease(@NonNull PlayerController playerController) {
    }

    @Override
    public void onControllerDestroy(@NonNull PlayerController playerController) {
    }

    @Override
    public void onControllerError(@NonNull PlayerController controller, @NonNull CastlabsPlayerException error, @NonNull PlayerConfig config) {
        if (error.getSeverity() == CastlabsPlayerException.SEVERITY_ERROR) {
            // In case of fatal error, we show it and remove such item from the playlist
            Snackbar.make(playerView, "onControllerError: " + error.getCauseMessage(), Snackbar.LENGTH_SHORT).show();
            multiControllerPlaylist.removeItem(config);
        }
    }

    @Override
    public void onPlaylistEndReached(boolean willLoop) {
    }
});

// ...

// Start Playback
multiControllerPlaylist.open(new PlayerConfig.Builder("http://demo.com/asset1.mpd").get(),
                        new PlayerConfig.Builder("http://demo.com/asset2.mpd").get(),
                        new PlayerConfig.Builder("http://demo.com/asset3.mpd").get());

You can also use AbstractPlaylistListener instead, which is an abstract implementation of the PlaylistListener, thus only needing to implement a subset of the callbacks and also potentially simplifying updates of the API.

The MultiControllerPlaylist also provides a set of methods to manipulate the underlying playlist, as well as skipping to the next or previous items.

Access the MultiControllerPlaylist javadoc for more insights and extensive details.

SingleControllerPlaylist

The SingleControllerPlaylist relies in the usage of a single

PlayerController, which is the SingleControllerPlaylist itself.

This implementation allows for multiple PlayerConfigs to be loaded and played in sequential order. You can start playback with one of the open methods.

You should also register a PlaylistListener in the constructor to get Playlist-specific events, such as when a Playlist item change occurs.

In order to use the SingleControllerPlaylist you should explicitly create it through its constructor. If you’re using a PlayerView, you should also set it there.

Here you can see an example implementation:

// ...

// Create Bundle Array
Bundle[] configs = new Bundle[2]; // Fill bundles

// Here we create the SingleControllerPlaylist, passing a PlaylistListener
final SingleControllerPlaylist pc = new SingleControllerPlaylist(this, new SingleControllerPlaylist.PlaylistListener() {
    @Override
    public void onItemChange(@NonNull PlayerConfig config) {
        Log.d(TAG, "onItemChange: " + config.contentUrl);
    }
});

// Set it as the PlayerController in our PlayerView
playerView.setPlayerController(pc);

// Start playback
pc.open(configs);

The SingleControllerPlaylist currently only supports either clear or Widevine-protected streams, both VoD and Live. Mixing clear and protected streams could result in unexpected behaviour.

Due to internal Exoplayer limitations, the SingleControllerPlaylist is not compatible with Advertisements. If you provide Ad data in your config, it will be ignored and a non-fatal warning will be fired.

Access the SingleControllerPlaylist javadoc for more insights and extensive details.

Advanced Playlist configuration

The SingleControllerPlaylist only loads a subset of the provided PlayerConfigs at a given point in time. This is done in an effort to reduce the device and network load, especially when loading live content requiring frequent manifest updates.

The currently-loaded set of items is a window, as in a window of ready-to-be-played consecutive items. Such window can be configured through two different params defining the number of items to load before and after the current item:

When transitioning to a new item, or on first item playback start, all the other items within the window will be loaded and thus have their Timelines prepared in order to speed up an eventual transition.

If wrapping is enabled, it will also be taken into account for item window loading.

Additionally, the playlist will keep a pool of recently-loaded items. This value is configurable through SingleControllerPlaylist.setMaximumLoadedItems(int).

Previous topic: Advanced Setup
Next topic: The Player View