# 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).

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 as well as in the following table.

Castlabs player error codes
Constant | Code | Description
--- | --- | ---
`TYPE_UNKNOWN` | `0` | Error raised with an unknown type
`TYPE_AUDIO_TRACK_INITIALIZATION` | `1` | Error raised when the track can not be initialized. The cause of this error is a `com.google.android.exoplayer2.audio.AudioSink.InitializationException` that contains the audio track state reported by `android.media.AudioTrack#getState()`. This is reported as `#SEVERITY_ERROR` since this means that no audio playback will be possible.
`TYPE_AUDIO_DECODER_INITIALIZATION` | `2` | Error raised when the audio decoder can not be initialized. The cause of this error is a `com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException`. This is reported as `#SEVERITY_ERROR` since this means that no audio playback will be possible.
`TYPE_AUDIO_WRITE_ERROR` | `3` | Raised when an error occurs while writing to the audio track. The cause of this error is a `com.google.android.exoplayer2.audio.AudioSink.WriteException` that exposes the `com.google.android.exoplayer2.audio.AudioSink.WriteException#errorCode` that is returned from `android.media.AudioTrack#write(byte[], int, int)` This is reported as `#SEVERITY_WARNING` since it might be possible that audio playback will recover.
`TYPE_AUDIO_DECRYPTION_ERROR` | `4` | Raised when an error occurs while decrypting audio data. The cause of this error is a `MediaCodec.CryptoException` that exposes an error code using `MediaCodec.CryptoException#getErrorCode()`. The error code reported by the cause is one of `MediaCodec.CryptoException#ERROR_NO_KEY`, `MediaCodec.CryptoException#ERROR_INSUFFICIENT_OUTPUT_PROTECTION`, `MediaCodec.CryptoException#ERROR_KEY_EXPIRED`, `MediaCodec.CryptoException#ERROR_RESOURCE_BUSY`, or `MediaCodec.CryptoException#ERROR_SESSION_NOT_OPENED` This is reported as `#SEVERITY_ERROR`.
`TYPE_VIDEO_DECODER_INITIALIZATION` | `5` | Raised when the video decoder can not be initialized. The cause of this error is a `com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException`. This is reported as `#SEVERITY_ERROR` since this means that no video playback will be possible.
`TYPE_VIDEO_DECRYPTION_ERROR` | `6` | Raised when an error occurs while decrypting audio data. The cause of this error is a `MediaCodec.CryptoException` that exposes an error code using `MediaCodec.CryptoException#getErrorCode()`. The error code reported by the cause is one of `MediaCodec.CryptoException#ERROR_NO_KEY`, `MediaCodec.CryptoException#ERROR_INSUFFICIENT_OUTPUT_PROTECTION`, `MediaCodec.CryptoException#ERROR_KEY_EXPIRED`, `MediaCodec.CryptoException#ERROR_RESOURCE_BUSY`, or `MediaCodec.CryptoException#ERROR_SESSION_NOT_OPENED` This is reported as `#SEVERITY_ERROR`.
`TYPE_MANIFEST_LOADING_FAILED` | `7` | This error type is raised when loading the player manifest (i.e. the DASH mpd, or the HLS playlist) failed. The cause of this error is an `java.io.IOException`. This is reported as `#SEVERITY_ERROR`.
`TYPE_SDK_INIT_ERROR` | `8` | Error raised when the SDK could not be initialised successfully. The cause of the error is a `Throwable`. This is reported as `#SEVERITY_ERROR`.
`TYPE_NO_RENDERER_FOUND` | `9` | Error raised when no plugin was found that can be used to create a renderer to playback a given DRM and content combination. An error of this type will be raised for example if you are trying to playback a DASH stream using OMA-DRM but the OMA-Plugin was not loaded. It is possible also that this Exception is thrown if a Renderer has been temporarily disabled. A few use cases, such as performing background playback ,can disable a number of Renderers. This error has no cause attached to it. This is reported as `#SEVERITY_WARNING`.
`TYPE_NO_PLAYABLE_CONTENT` | `10` | Error raised when the applied filters were not able to find any viable content that can be displayed on the current device. This can happen for example when you are trying to playback 4K video on a device that does not support it and 4K is the only representation found in the Manifest. This is reported as `#SEVERITY_ERROR`.
`TYPE_PLAYBACK_ERROR` | `11` | Error raised when the ExoPlayer reports a general playback error. The cause of this error is a `ExoPlaybackException`. This is reported as `#SEVERITY_ERROR`.
`TYPE_API_MISMATCH` | `12` | This error is raised if a method is called that is not supported on the current API level. This will for example be triggered if you try to use Widevine DRM on devices that do not support it. This is reported as `#SEVERITY_ERROR`.
`TYPE_UNSUPPORTED_DRM` | `13` | This error is raised if you are trying to use a DRM system that is not supported by the device. The cause if this error is a `UnsupportedDrmException`. This is reported as `#SEVERITY_ERROR`.
`TYPE_SDK_NOT_INITIALIZED` | `14` | Error raised when the SDK was not initialized using one of `com.castlabs.android.PlayerSDK#init(Context)` or `com.castlabs.android.PlayerSDK#init(Context, LicenseLoader)`. This is reported as `#SEVERITY_ERROR`.
`TYPE_DRMTODAY_EXCEPTION` | `15` | This error is raised if an exception occurred while the player was communicating with DRMToday to fetch a license. The cause of this error is a `com.castlabs.android.drm.DrmTodayException`. Note that this error usually occurs during license acquisition and before any decryption errors are reported by the decoder. This is reported as `#SEVERITY_ERROR`.
`TYPE_DOWNLOAD_ERROR` | `16` | This error is raised if an exception occurred while downloading playback data. The cause of this error is a `DownloadException` that you can use to identify the root cause and the id of the track that caused the error. This is reported as `#SEVERITY_ERROR`
`TYPE_SECONDARY_DISPLAY` | `17` | This error is raised if a secondary display is detected and the current configuration does not permit playback on that display. This is reported as `#SEVERITY_ERROR`.
`TYPE_KEY_EXPIRED` | `18` | This error is raised if a DRM key expires during playback. This is reported as `#SEVERITY_ERROR`.
`TYPE_DRM_KEY_DOWNLOAD_ERROR` | `19` | This error is raised if a DRM key could not be fetched successfully. This is usually caused when offline lincenses are loaded. This is reported as `#SEVERITY_ERROR`.
`TYPE_CONNECTIVITY_LOST_ERROR` | `20` | This error is raised if an exception occurred while downloading playback and no network connectivity is available. This is reported as `#SEVERITY_WARNING` and the player will automatically try to recover once connectivity is re-gained. This can be treated as in informative warning.
`TYPE_CONNECTIVITY_GAINED_INFO` | `21` | This exception is raised after the player lost connectivity (`#TYPE_CONNECTIVITY_LOST_ERROR`), once connectivity was re-gained. This is reported as `#SEVERITY_INFO` and can be treated as in informative message.
`TYPE_BEHIND_LIVE_WINDOW` | `22` | Error raised when the current playback position is behind the live edge in a live stream. The cause of this error is a `com.google.android.exoplayer2.source.BehindLiveWindowException`. When this error is raised, the player is in a fatal state and you will need to restart/reopen the stream to continue playback at the live edge. Typically, this error is caused by loss of connectivity where the player is not able to keep updating the manifests to stay at the live edge or within the live window. This is reported as `#SEVERITY_ERROR`.
`TYPE_INVALID_PLAYER_LICENSE` | `23` | Error raised when the Castlabs player license is invalid or expired. Please contact support if you see this error unexpectedly. Note that if this error occurs, playback is prevented and the player will stop working. This is reported as `#SEVERITY_ERROR`.
`TYPE_DATA_DOWLOAD_ERROR` | `24` | This error is raised if an exception occurred while downloading data. The cause can be used to identify the root cause. In contrast to `#TYPE_DOWNLOAD_ERROR` this type is raised for any download issues around non track specific data. This is reported as `#SEVERITY_WARNING` since it depends on where and when this exception is raised. This type might contain the `#URL` key in the `#errorData` bundle.
`TYPE_DRM_EXCEPTION` | `25` | This error is raised if an exception occurred while the player was communicating with DRM server to fetch a license. The cause of this error is a `com.google.android.exoplayer2.drm.DrmSession.DrmSessionException`. Note that this error usually occurs during license acquisition and before any decryption errors are reported by the decoder. This is reported as `#SEVERITY_ERROR`.
`TYPE_AUDIO_UNSUPPORTED` | `26` | This error is raised if all audio tracks are filtered out as unsupported. It is reported as `#SEVERITY_WARNING`
`TYPE_VIDEO_UNSUPPORTED` | `27` | This error is raised if all video tracks are filtered out as unsupported. It is reported as `#SEVERITY_WARNING`
`TYPE_TEXT_UNSUPPORTED` | `28` | This error is raised if all text tracks are filtered out as unsupported. It is reported as `#SEVERITY_WARNING`
`TYPE_USER_ID_NOT_PROVIDED` | `29` | Error raised when the userID is mandatory and has not been provided. This is reported as `#SEVERITY_ERROR`.
`TYPE_MANIFEST_PARSING_FAILED` | `30` | This error type is raised when parsing the player manifest (i.e. the DASH mpd, or the HLS playlist) failed. The cause of this error is an `com.google.android.exoplayer2.ParserException`. This is reported as `#SEVERITY_ERROR`.
`TYPE_AD_REQUEST_UNSUPPORTED` | `31` | Error raised when trying to play Ads but the current Player setup doesn’t support it. This is the case when using a `SingleControllerPlaylist` and informing `com.castlabs.android.player.PlayerConfig#adRequest`. This is reported as `#SEVERITY_WARNING`
`TYPE_DRM_PROVISION_ERROR` | `32` | Error raised during device provisioning This is reported as `#SEVERITY_ERROR`.
`TYPE_CSL_LIMIT_REACHED` | `33` | Error raised when concurrent stream limiting reaches maximum clients This is reported as `#SEVERITY_ERROR`.
`TYPE_CSL_NETWORK_ERROR` | `34` | Error raised when there’s a network error with the concurrent stream limiting service This is reported as `#SEVERITY_ERROR`.
`TYPE_HDCP_CONNECTION_WARNING` | `35` | This exception is raised after the player sees changes in HDCP connectivity. This is reported as `#SEVERITY_INFO` and can be treated as in informative message.
`TYPE_AD_ERROR` | `36` | This error is raised when a general error occurs at initializing or during an Ad.
`TYPE_DNS_SERVER_ERROR` | `37` | This error is raised when a DNS server error occurs.
`TYPE_TIME_OUT_ERROR` | `38` | This error is raised when a timeout error occurs.
`TYPE_MANIFEST_INVALID` | `39` | This error happens when the player found a semantic issue with a Manifest. This could be due to a change in between manifest updates deemed as illegal by the corresponding spec.
`TYPE_TUNNELING_ERROR` | `40` | This error happens when tunneling mode can not be enabled on a device and the playback in non-tunneling mode continues. It is reported as `#SEVERITY_WARNING` and can be treated as an informative message.
`TYPE_VIDEO_BLOCKLIST_FAILED` | `41` | This error is raised when video quality blocklisting fails as the current quality is the only video quality left for playback. It is reported as `#SEVERITY_WARNING`
`TYPE_PLAYLIST_ITEM_PREFETCH_FAILED` | `42` | Error raised when an item fails to load during prefetch (while another item is playing). This is a recoverable warning - the application can handle it and decide how to proceed. This is reported as `#SEVERITY_WARNING`. @see PlaylistException
`TYPE_PLAYLIST_ITEM_TRANSITION_FAILED` | `43` | Error raised when the player tries to transition to an item that failed to load. This occurs either when the application didn’t handle the prefetch warning and the current item finishes playback, or when direct navigation (via `nextItem()`, `previousItem()`, or `playItem()`) targets an item that cannot be loaded. This is reported as `#SEVERITY_ERROR` (fatal). The `#getErrorData() error data bundle` exposes additional data accessible through the following keys: `#PLAYLIST_CURRENT_ITEM_INDEX` `#PLAYLIST_FAILED_ITEM_INDEX` `#PLAYLIST_FAILED_ITEM` `#PLAYLIST_SIZE` `#PLAYLIST_STATE` @see PlaylistException
`TYPE_DOWNLOAD_WARNING` | `44` | This error is raised if an exception occurred while downloading playback data. The cause of this error is a `DownloadException` that you can use to identify the root cause and the id of the track that caused the error. This is reported as `#SEVERITY_WARNING`

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:

> - `SdkConsts.INTENT_AUDIO_TRACK_IDX` to pre-select an audio track

> - `SdkConsts.INTENT_SUBTITLE_TRACK_IDX` to pre-select a subtitle track

> - `SdkConsts.INTENT_ABR_CONFIGURATION` to specify a custom ABR configuration. See Adaptive Bitrate Switching for more information.

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

- `PlayerSDK.CODEC_WEIGHT_IMPL_HW`

- `PlayerSDK.CODEC_WEIGHT_IMPL_SW`

- `PlayerSDK.CODEC_WEIGHT_MIME_H265_HW`

- `PlayerSDK.CODEC_WEIGHT_MIME_H265_SW`

- `PlayerSDK.CODEC_WEIGHT_MIME_H264_HW`

- `PlayerSDK.CODEC_WEIGHT_MIME_H264_SW`

- `PlayerSDK.CODEC_WEIGHT_MIME_OTHER`

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.

#### Playlist error handling

When a playlist item fails to load, the `SingleControllerPlaylist` reports errors through the standard `onError` callback using `CastlabsPlayerException`. The `PlaylistException` is attached as the cause and provides the failed item index, failed item config, current item index, and a snapshot of the playlist state via getters. The same metadata is also exposed through `CastlabsPlayerException.getErrorData()`.

There are two types of playlist errors:

- Prefetch warning (`SEVERITY_WARNING`): Raised when an item fails during background prefetch. The application can handle this warning and replace the failing item before playback reaches it.

- Fatal error (`SEVERITY_ERROR`): Raised when attempting to play an item that cannot be loaded. This can happen either when the playlist transitions automatically to a previously failed item, or when the application explicitly navigates to an item (via `nextItem()`, `previousItem()`, or `playItem()`) that cannot be loaded.

Here is an example of handling a prefetch warning by replacing the broken item:

    playlist.addPlayerListener(new AbstractPlayerListener() {
        @Override
        public void onError(@NonNull CastlabsPlayerException error) {
            if (error.getCause() instanceof PlaylistException) {
                PlaylistException playlistError = (PlaylistException) error.getCause();
                if (error.getSeverity() == CastlabsPlayerException.SEVERITY_WARNING) {
                    // Prefetch warning - replace the broken item
                    int failedIndex = playlistError.getFailedItemIndex();
                    PlayerConfig failedConfig = playlistError.getFailedConfig();
                    PlayerConfig replacement = new PlayerConfig.Builder("http://backup.url/content.mpd").get();
                    playlist.removeItem(failedConfig);
                    playlist.addItem(failedIndex, replacement);
                }
            }
        }
    });

The `PlaylistException` provides the following methods for recovery:

- `getFailedItemIndex()` - Index of the failed item

- `getFailedConfig()` - The PlayerConfig that failed

- `getCurrentItemIndex()` - Index of the current item

- `getPlaylistSnapshot()` - Snapshot of the playlist at the time of the error

Playlist metadata is also available through `CastlabsPlayerException.getErrorData()` using these keys:

- `PLAYLIST_FAILED_ITEM_INDEX` - Index of the failed item

- `PLAYLIST_FAILED_ITEM` - The failed PlayerConfig (via `Bundle.getParcelable`)

- `PLAYLIST_CURRENT_ITEM_INDEX` - Index of the current item

- `PLAYLIST_SIZE` - Total number of items in the playlist

- `PLAYLIST_STATE` - Playlist snapshot as `ArrayList<PlayerConfig>` (via `Bundle.getParcelableArrayList`)

#### Bandwidth estimation reset modes

For adaptive playback scenarios it is sometimes desirable to reset the ABR bandwidth model when the playlist switches items. The `SingleControllerPlaylist` now exposes `setBandwidthResetMode(int)` so that you can opt-in to these behaviours on a per-playlist basis.

The reset modes are exposed as a flag-based bitmask:

- `BANDWIDTH_RESET_MODE_NONE` keeps the current bandwidth estimation across item changes (default).

- `BANDWIDTH_RESET_MODE_EXPLICIT_ITEM_CHANGE` resets the estimation whenever you explicitly request a different item (for example via `playItem(int)`).

- `BANDWIDTH_RESET_MODE_AUTOMATIC_ITEM_TRANSITION` resets the estimation when playback automatically progresses to the next playlist item.

You can combine the flags to cover both explicit transitions triggered from the Playlist API and automatic transitions:

    playlist.setBandwidthResetMode(
        SingleControllerPlaylist.BANDWIDTH_RESET_MODE_EXPLICIT_ITEM_CHANGE
            | SingleControllerPlaylist.BANDWIDTH_RESET_MODE_AUTOMATIC_ITEM_TRANSITION
    );

Use `getBandwidthResetMode()` to inspect the active configuration at runtime.

##### 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:

> - `SingleControllerPlaylist.setPlaylistWindowBefore(int)`

> - `SingleControllerPlaylist.setPlaylistWindowAfter(int)`

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)`.