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 aPlayerConfig
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 aBundle
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 thePlayerView
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’ssetPosition(long)
is called.- onVideoSizeChanged
The
onVideoSizeChanged()
will be called whenever the size of the video changes. Please consider using aStreamingEventListener
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 thePlayerController
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 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 theIdle
state when it is released.- Preparing
The player goes into
Preparing
state after a content URL was passed through one of theopen()
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 usinggetAudioTrack()
and select a track usingsetAudioTrack(AudioTrack)
.PlayerController.getSubtitleTracks()
This method exposes a list of
SubtitleTrack
instances. You can get the currently selected subtitle track usinggetSubtitleTrack()
and select a track usingsetSubtitleTrack(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 usinggetVideoQuality()
and manually select a quality usingsetVideoQuality(VideoTrackQuality)
. Please note that manually selecting a video quality will disable adaptive bitrate switching. You can passnull
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
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
andmaximum
parameters of the currentBufferConfiguration
. These parameters are used to control the fill and drain behaviour of the buffer (see API docs forBufferConfiguration
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
andmaxDurationForQualityDecreaseUs
. 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 theVideoTrackQuality
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 |
---|---|
|
The reason for the trigger is unknown |
|
The initial format selection triggered the event |
|
The manual format selection triggered the event |
|
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 |
---|---|
Events triggered by the video track |
|
Events triggered by the audio track |
|
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 theonError
callback of any connectedPlayerListener
. The type of the exception will beCastlabsPlayerException.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 theSingleControllerPlaylist
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)
.