Extensions and Plugins
======================

.. javaimport::
    com.castlabs.analytics.*
    com.castlabs.android.player.*
    com.castlabs.sdk.ima.*
    com.castlabs.android.settings.*
    com.castlabs.sdk.mediasession.*
    com.castlabs.sdk.subtitles.*
    com.castlabs.sdk.thumbs.*
    com.castlabs.sdk.broadpeak.*
    com.castlabs.android.network.RequestModifier
    com.castlabs.android.network.ResponseModifier

.. subtitles:
   
Subtitles and Closed Captions
-----------------------------

The |SDK| comes with two plugins that support subtitles and closed
captions. The default plugin leverages the ExoPlayer subtitles parser
and renderer. In addition, the |SDK| bundles the Castlabs subtitles
plugins, that offers full support for FCC requirements and an extended
TTML renderer that provides styling.

In order to use the Castlabs subtitles plugin, you need to add it as a
dependency to your application, and register the plugin with the SDK::

    dependencies {
        ...
        compile 'com.castlabs.player:subtitles-plugin:|version|'
        ...
    }

The example above will add the dependency to you build file. You then need
to register the plugin to enable it:

.. code-block:: java

    PlayerSDK.register(new SubtitlesPlugin());
    PlayerSDK.init(getApplicationContext());

Both, the ExoPlayer as well as the Castlabs subtitles plugin style can be
configured through the ``PlayerContorller``. For that, you will need to
create a ``SubtitlesStyle`` that can be passed to the
controller. The style can also be loaded from the system settings and the
``Builder`` that creates the style provides the necessary methods to be
initialized from the system settings. Please note that only since Android
API version 21, all required FCC settings can be set on a system level.

Subtitle FCC Fonts
^^^^^^^^^^^^^^^^^^

To fulfill the FCC requirements for closed captions, you need to allow the user
to select from different fonts, including "casual", "cursive", "small
capitals", and a "serif monospace" font. These fonts are not available on all
Android devices. The |SDK| bundles a module that offers a set of fonts to 
fill the gap. You can add the module to your dependencies::

    dependencies{
        ...
        compile 'com.castlabs.player:subtitles-fonts:|version|'
        ...
    }

This will bundle the missing font types with your Application. You can also add
different fonts yourself and use the ``SubtitleFonts`` class to configure the fonts.

Subtitle Styles
^^^^^^^^^^^^^^^

The |SDK| contains an example Application `subtitle_styles` that
demonstrates who you can build a settings view to configure subtitles,
store that configuration in the shared preferences of the Application and
then create a ``SubtitlesStyle`` object from these preferences and use it
during playback.

Subtitle Preview
^^^^^^^^^^^^^^^^

One of the FCC requirements is that you provide a preview when different 
caption styles are applied. The |SDK| contains a View component,
:javaref:`SubtitlesPreviewView
<com.castlabs.android.subtitles.SubtitlesPreviewView>`, that can be used
to render a preview of the subtitles with a given style.

Subtitles View
^^^^^^^^^^^^^^

The |SDK| uses the `SubtitlesView` in order to display rendered subtitles.

If you're using a ``PlayerView``, a ``SubtitlesView`` will automatically be created and added to
such ``PlayerView``.

If you are not using a ``PlayerView``. You can create a ``SubtitlesView`` programmatically just
by using its constructor, or by declaring one in a layout xml file for the system to inflate. If you
use the latest approach, you must pass your layout View to :javaref:`PlayerController#setComponentView(int, View)`
with the corresponding id, defined in :javaref:`SUBTITLES_VIEW_ID <com.castlabs.sdk.subtitles.SubtitlesPlugin#SUBTITLES_VIEW_ID>`, or
``R.id.presto_castlabs_subtitles_view`` so the ``SubtitlesPlugin`` can properly find the ``SubtitlesView``.

You can get a reference to this underlying View in two different ways.

You can use get it from the ``PlayerController``, with its :javaref:`PlayerController#getComponentView(int)` method,
again making use of the :javaref:`SUBTITLES_VIEW_ID <com.castlabs.sdk.subtitles.SubtitlesPlugin#SUBTITLES_VIEW_ID>`.

If you're using a ``PlayerView`` there's also the option of using the
:javaref:`SubtitlesViewComponent <com.castlabs.android.views.SubtitlesViewComponent>`, and then
getting the view from it through the ``getView()`` method.

.. _connectivity_checks:
   
Recovering From Loss Of Connectivity
------------------------------------

By default, the |SDK| will not check specifically for loss of connectivity
and if a device looses its internet connection during playback, fatal
player errors will be raises.
With version 3.1.2, we introduced a new mechanism to enable the player to 
recover automatically from connectivity loss without fatal errors being
raised. You will need to enable that feature explicitly before you
initialize the SDK:

.. code-block:: java

    PlayerSDK.ENABLE_CONNECTIVITY_CHECKS = true;

If the feature is enabled, the player will catch download errors
internally and check for network connectivity. If no internet connection
is available, a :javaref:`CastlabsPlayerException
<com.castlabs.android.player.exceptions.CastlabsPlayerException>` will be
passed to any registered `PlayerListener` instances. The type of the error
will be :javaref:`TYPE_CONNECTIVITY_LOST_ERROR
<com.castlabs.android.player.exceptions.CastlabsPlayerException#TYPE_CONNECTIVITY_LOST_ERROR>`,
which indicates that a download error occurred due to lack of
connectivity. The |SDK| will then register a broadcast listener and wait
for connectivity changes. During that period, the player might run out of
buffered data and will go into `Buffering` state. Once device connectivity
changes, the playback will be resumed automatically and
a :javaref:`TYPE_CONNECTIVITY_GAINED_INFO
<com.castlabs.android.player.exceptions.CastlabsPlayerException#TYPE_CONNECTIVITY_GAINED_INFO>`
error will be raised through all registered listeners. This error is not
severe and serves only informational purpose. For more
information about error handling and how to add a listener see 
:ref:`controller_error_handling`.

If you are implementing a user interface component to inform the user of
the loss of connectivity, you might want to consider waiting until the
player goes into buffering state after you observed the 
:javaref:`TYPE_CONNECTIVITY_LOST_ERROR
<com.castlabs.android.player.exceptions.CastlabsPlayerException#TYPE_CONNECTIVITY_LOST_ERROR>`
error. If the connectivity loss is short enough and the buffers contain enough 
data, the player might actually recover silently from such interruptions.

Handling Connectivity Loss for Live Playback
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

In case connectivity loss detection is enabled and you are playing live
content, you might need to manually re-start playback if the connection
loss was too long and the playback head moved behind the current live
Window. In that case the player will raise a :javaref:`TYPE_BEHIND_LIVE_WINDOW
<com.castlabs.android.player.exceptions.CastlabsPlayerException#TYPE_BEHIND_LIVE_WINDOW>`
error, which can be used as a marker for these situations. A possible re-start
might look like this:

.. code-block:: java

    @Override
    public void onError(@NonNull CastlabsPlayerException error) {
        // restart playback for live streams that fell behind the live
        // window.
        if (error.getType() == CastlabsPlayerException.TYPE_BEHIND_LIVE_WINDOW) {
            // Save the current playback state to a bundle
            Bundle store = new Bundle();
            controller.saveState(store);

            // release the player and re-open it with the bundle
            controller.release();
            try {
                controller.open(store);
            } catch (Exception e) {
                Log.e(TAG, "Error re-opening stream: " + e, e);
            }
        }
    }

Customize Connectivity Check
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The |SDK| uses its default :javaref:`DefaultConnectivityCheck
<com.castlabs.android.network.DefaultConnectivityCheck>` to check if the
device is currently connected to the internet. The default implementation
does used the Android ``ConnectivityManager`` to check basic connectivity
and then ensures that an internet connection is available by doing a DNS
host name lookup to `google.com`. You can customize the checker
implementation using the global :javaref:`PlayerSDK#CONNECTIVITY_CHECKER
<com.castlabs.android.PlayerSDK#CONNECTIVITY_CHECKER>` setting. You can
either set this to a custom implementation of the
:javaref:`ConnectivityCheck
<com.castlabs.android.network.ConnectivityCheck>` interface, or use
a custom instance of the :javaref:`DefaultConnectivityCheck
<com.castlabs.android.network.DefaultConnectivityCheck>` with a customized
host lookup name.

Please note that the default implementation does not actually open
a connection to `google.com` (or any other lookup host) but does connect
to the DNS service of the device to perform a host lookup for the given
name.

.. _offline_keys:

Offline Keys
------------

The |SDK| allows you to store offline keys for some DRM systems. If you
want to leverage this feature, you will need to enable it by setting
a unique identifier as the offline ID in the ``DrmConfiguration`` (see
:ref:`drm_offline_setting`). 

Once the license is loaded, the offline ID will be used to store
a reference to the key, the so called "keySetId", on the device. Note that
this is not the actual decryption key, but an identifier used by the DRM
system to find and load the key later. This "keySetId" needs to be stored
and the SDK does this using an implementation of the :javaref:`KeyStore
<com.castlabs.android.drm.KeyStore>` interface. The default implementation
is using your application's shared preferences in private mode to store
a mapping from your offline ID to the "keySetId". 

You can, however use a custom implementation of the ``KeyStore``. The store
can be configured globally using the
:javaref:`com.castlabs.android.PlayerSDK#DEFAULT_KEY_STORE` field. You can 
also set it explicitly for a given ``PlayerController``.

Key info
^^^^^^^^

You can get Key related info through the :javaref:`KeyStore <com.castlabs.android.drm.KeyStore>`
interface. You can get the currently used ``KeyStore`` instance with the static
:javaref:`PlayerSDK#DEFAULT_KEY_STORE <com.castlabs.android.PlayerSDK#DEFAULT_KEY_STORE>` field.

Providing the offlineId of your content, you can get an instance of :javaref:`DrmKeyStorage <com.castlabs.android.player.DrmKeyStorage>`
with ``KeyStore`` 's :javaref:`get <com.castlabs.android.drm.KeyStore#get(String)>` method.

The ``DrmKeyStorage`` object provides DRM-related for a particular key. You can use this for instance
to get the :javaref:`expiration date <com.castlabs.android.player.DrmKeyStorage#keyValidUntilMs>`.

Interactive Media Ads (IMA)
---------------------------
The |SDK| provides with IMA service including Dynamic Ads Insertion (DAI) integrated in the form of plugin.
Any VAST-compliant ad server is supported.

IMA plugin needs to be registered and enabled by the app. Optionally the plugin
can be initialized with the custom ``ImaSdkSettings`` e.g. in order to change the default ads UI language: 

.. code-block:: java

    ImaPlugin imaPlugin = new ImaPlugin();
    imaPlugin.setEnabled(true);
    
    // (Optional) Set the IMA SDK settings
    ImaSdkSettings imaSdkSettings = ImaSdkFactory.getInstance().createImaSdkSettings();
    imaSdkSettings.setLanguage("en");
    imaPlugin.setImaSdkSettings(imaSdkSettings);
    
    PlayerSDK.register(imaPlugin);

    ..
    // (Optional) Update the IMA settings run-time e.g. on a stream basis
    ImaPlugin imaPlugin = PlayerSDK.getPlugin(ImaPlugin.class);
    if (imaPlugin != null) {
        ImaSdkSettings imaSdkSettings = imaPlugin.getImaSdkSettings();
        if (imaSdkSettings != null) {
            imaSdkSettings.setLanguage("fr");
            imaPlugin.setImaSdkSettings(imaSdkSettings);
        }
    }

.. note::
    **Core Library Desugaring Required**

    The IMA plugin requires core library desugaring to be enabled in your app's ``build.gradle``:

    .. code-block:: groovy

        android {
            compileOptions {
                coreLibraryDesugaringEnabled true
            }
        }

        dependencies {
            coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
        }

    This is required because the IMA SDK (version 3.37.0+) uses Java 8+ APIs that need desugaring
    for compatibility with older Android versions.

The :javaref:`PlayerController <com.castlabs.android.player.PlayerController>` expects either
:javaref:`ImaAdRequest <com.castlabs.sdk.ima.ImaAdRequest>` or
:javaref:`ImaStreamRequest <com.castlabs.sdk.ima.ImaStreamRequest>`
when opening content video from `Bundle` (:ref:`start_from_intent`) or
:javaref:`PlayerConfig <com.castlabs.android.player.PlayerConfig>` (:ref:`start_from_player_config`).

.. code-block:: java

    // IMA:
    intent.putExtra(SdkConsts.INTENT_ADVERTS_DATA, new ImaAdRequest("https://ima.mydomain/request1").toAdRequest());

    // IMA DAI:
    // note that specifying content type is mandatory
    intent.putExtra(SdkConsts.INTENT_ADVERTS_DATA, new ImaStreamRequest("ContentSourceId", "VideoId", "ApiKey").toAdRequest());
    intent.putExtra(SdkConsts.INTENT_CONTENT_TYPE, SdkConsts.CONTENT_TYPE_DASH)


As a result, the |SDK| requests the ads, starts the ads playback according to the ads schedule and pauses 
the content video playback. After the ad is completed the content video playback is resumed automatically.

Additionally the application may need to be notified when the ad starts and completes. For instance, it may disable 
the user content video playback controls or hide them during the ads playback. In this case the application installs the 
listener as follows:

.. code-block:: java

    PlayerView playerView;
    ...
    playerView.getPlayerController().getAdInterface().addAdListener(new AdInterface.Listener() {
        @Override
        public void onAdStarted(@NonNull Ad ad) {
            Log.d(TAG, "Ad started : " + ad.id + ", position = " + ad.position);
            // May hide controls and disable touch event triggers
        }

        @Override
        public void onAdCompleted() {
            Log.d(TAG, "Ad completed");
            // May show the controls again if we keep them on screen
        }

        @Override
        public void onAdPlaybackPositionChanged(long playbackPositionMs) {
        }
    });

Custom ad UI
^^^^^^^^^^^^

The |SDK| allows you to build your own ad UI. For this, you'll need to hook up to the relevant
ad events, and hide the default ad provider UI. Additionally, an :javaref:`AdApi <com.castlabs.android.adverts.AdApi>`
is provided so that ad-related operations can be invoked.
Please note for the IMA Ad Provider the SDK requires the following Tag in the VAST response for Custom UI
to be useable. Once this is available you may check whether the Default AdProvider UI has been disabled
using ``currentAd.isUiDisabled``.

.. code-block:: xml

    <Extension type="uiSettings">
        <UiHideable>1</UiHideable>
    </Extension>

This is how a basic implementation could look:

.. code-block:: java

    protected void onCreate() {
        // ...
        customAdUi = new CustomAdUi(playerView.getAdInterface().getAdApi());
        ImaAdRequest adReq = new ImaAdRequest.Builder().tagUrl("<url>")
        // This is a NO-OP in case VAST responses do not have the hideable tag set
        							.disableDefaultAdUi(true).get()
        							.toAdRequest());
    }

    protected void onStart() {
        // ...
        playerView.getAdInterface().addAdListener(customAdUi);
    }

    protected void onStop() {
        // ...
        playerView.getAdInterface().removeAdListener(customAdUi)
    }

The ``CustomAdUi`` class would implement the ``AdInterface.Listener`` interface:

.. code-block:: java

    class CustomAdUi implements AdInterface.Listener {
        private AdApi api;
        private Button skipButton;

        CustomAdUi(AdApi adApi) {
            this.api = adApi;
            // Create Views
            // ...
            skipButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    adApi.skipAd();
                }
            });
        }

        @Override
        public void onAdStarted(@NonNull Ad ad) {
            // Show UI
        }

        @Override
        public void onAdCompleted() {
            // Hide UI
        }

        @Override
        public void onAdPlaybackPositionChanged(long playbackPositionMs) {
            // Update UI
        }
    }

IMA request options
^^^^^^^^^^^^^^^^^^^

The |SDK|'s IMA plugin supports additional options on
:javaref:`ImaAdRequest.Builder` to control ad behavior and timeouts:

- :javaref:`ImaAdRequest.Builder#playAdsAfterTimeSec(double)`: Only play ads scheduled at or after
  the given content time (in seconds). Prerolls and midrolls before this time
  are skipped. Postrolls will be played even if the value informed is larger than the content duration.
- :javaref:`ImaAdRequest.Builder#adPreloadTimeoutMs(long)`: Timeout for ad preloading. Applied if
  ``TIME_UNSET`` or a value greater than 0 is provided; otherwise the loader's
  default of 10 seconds is used.
- :javaref:`ImaAdRequest.Builder#vastLoadTimeoutMs(int)`: Timeout for loading VAST. Applied when greater
  than 0. By default no value is informed to the IMA SDK.
- :javaref:`ImaAdRequest.Builder#mediaLoadTimeoutMs(int)`: Timeout for loading ad media. Applied when
  greater than 0. By default no value is informed to the IMA SDK.
- :javaref:`ImaAdRequest.Builder#maxMediaBitrate(int)`: Maximum bitrate for ad media. Applied when
  greater than 0. By default no value is informed to the IMA SDK.
- :javaref:`ImaAdRequest.Builder#playAdBeforeStartPosition(boolean)`: Whether to play an ad scheduled before
  the content's configured start position. Default is ``true``.

.. code-block:: java

    ImaAdRequest request = new ImaAdRequest.Builder()
            .tagUrl("<ad_tag>")
            // Skip preroll/early midrolls, start ads from 8s onwards
            .playAdsAfterTimeSec(8.0)
            // Timeouts and media constraints
            .adPreloadTimeoutMs(5000)
            .vastLoadTimeoutMs(4000)
            .mediaLoadTimeoutMs(8000)
            .maxMediaBitrate(2_000_000)
            // Do not play ads that are before the start position
            .playAdBeforeStartPosition(false)
            .get();

Ad Creative Format Fallback
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Some ad creative URIs may not contain recognizable file extensions (e.g.,
``https://ads.example.com/creative?id=123``), making it impossible for the |SDK| to
automatically detect the format. By default, the |SDK| will attempt to play such
ad creatives as MP4 files, which covers most ad creatives.

You can configure the fallback format globally using
:javaref:`PlayerSDK.AD_SETTINGS <com.castlabs.android.PlayerSDK#AD_SETTINGS>`:

.. code-block:: java

    // Default behavior: use MP4 for undetectable formats (no configuration needed)
    // Ad creatives without extensions will be attempted as MP4

    // Change fallback to HLS
    PlayerSDK.AD_SETTINGS.creativeFormatFallback = SdkConsts.CONTENT_TYPE_HLS;

    // Change fallback to DASH
    PlayerSDK.AD_SETTINGS.creativeFormatFallback = SdkConsts.CONTENT_TYPE_DASH;

    // Disable fallback (strict mode - only play ads with detectable formats)
    PlayerSDK.AD_SETTINGS.creativeFormatFallback = SdkConsts.CONTENT_TYPE_UNKNOWN;

The fallback is only used when the ad creative format cannot be detected from the URI.
If the format is successfully detected (e.g., the URI ends with ``.mp4`` or ``.m3u8``),
the detected format will be used instead.

Manual ad Scheduling
^^^^^^^^^^^^^^^^^^^^

The |SDK|, when using the IMA Plugin, allows for manual ad scheduling. This enables the user to perform
ad requests without the need to inform them on playback start, but rather after playback has already
started.

In order to enable this capability, it must be first enabled in the :javaref:`PlayerConfig <com.castlabs.android.player.PlayerConfig#adSchedule>`
or in the :javaref:`Bundle <com.castlabs.android.SdkConsts#INTENT_AD_SCHEDULE>` used to start playback.

The :javaref:`AdSchedule <com.castlabs.android.adverts.AdSchedule>` configuration object should be
built using the :javaref:`manual <com.castlabs.android.adverts.AdSchedule#SCHEDULE_MANUAL>` value
for the :javaref:`scheduleType <com.castlabs.android.adverts.AdSchedule.Builder#scheduleType(int)>`.
Alternatively, you can use the :javaref:`AD_SCHEDULE_MANUAL <com.castlabs.android.SdkConsts#AD_SCHEDULE_MANUAL>` constant.

.. code-block:: java

    // Example with Bundle
    Bundle bundle = new Bundle();
    bundle.putString(SdkConsts.INTENT_URL, "http://example.com/manifest.mpd");
    bundle.putParcelable(SdkConsts.INTENT_AD_SCHEDULE, SdkConsts.AD_SCHEDULE_MANUAL);
    // ...
    playerController.open(bundle);

.. code-block:: java

    // Example with PlayerConfig
    PlayerConfig playerConfig = new PlayerConfig.Builder("http://example.com/manifest.mpd")
        .adSchedule(SdkConsts.AD_SCHEDULE_MANUAL)
        // ...
        .get();
    playerController.open(playerConfig);

In order to perform ad requests, you need to get a reference to the :javaref:`AdInterface <com.castlabs.android.player.PlayerController#getAdInterface()>`.

.. code-block:: java

    playerController.getAdInterface().scheduleAd(new ImaAdRequest(adTag).toAdRequest());

By default, ads will be scheduled to be played 1.5 seconds after the response has been received.
This is in order to provide a smooth transition into the ad and avoid running in a buffer underrun.

.. note::
    This delay is *not* intended to schedule ads in advance. The IMA SDK expects the ad to be played
    shortly after the ad response, and it may completely drop the ad and skip its
    playback if the delay is too big. For this reason, this value is limited to 5 seconds.

The can be configured on a per-request basis as follows:

.. code-block:: java

    playerController.getAdInterface().scheduleAd(new ImaAdRequest.Builder()
                                    .tagUrl(adTag)
                                    .scheduleDelayMs(4000)
                                .get()
                                .toAdRequest());

Note that there are some limitations when it comes to manual ad scheduling:

 * No ads can be scheduled while already playing an ad.
 * Only VAST ads may be requested. If a VMAP is scheduled, it will be ignored.
 * Already scheduled ads, for instance, with a VMAP AdRequest informed on playback start, will be dropped if an another request is performed.

Downloader
----------
The downloader plugin of the |SDK| allows the content to be downloaded and played back offline later on. Currently,
the DASH, Smooth Streaming and MP4 content types are supported for the download.

In order to use the Downloader plugin you must show a persistent notification, as it is a foreground
service. This is achieved through the :javaref:`DownloadNotificationProvider <com.castlabs.sdk.downloader.DownloadNotificationProvider>`
abstract class.

First, the application needs to register the downloader plugin prior to |SDK| initialization:

.. code-block:: java

    PlayerSDK.register(new DownloaderPlugin(notificationProvider));
    PlayerSDK.init(getApplicationContext());

Please note that by default the downloader will permit 10 parallel segment downloads. You can
configure this number in the `DownloaderPlugin` constructor, for example:

.. code-block:: java

    PlayerSDK.register(new DownloaderPlugin(notificationProvider, 5));
    PlayerSDK.init(getApplicationContext());

The downloader is a foreground service and the applications need to use
:javaref:`DownloadServiceBinder <com.castlabs.sdk.downloader.DownloadServiceBinder>` in order to gain access to the downloader:

.. code-block:: java

    DownloadServiceBinder downloadServiceBinder;
    ServiceConnection downloadServiceConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            downloadServiceBinder = (DownloadServiceBinder) iBinder;
        }
        public void onServiceDisconnected(ComponentName componentName) {
            downloadServiceBinder = null;
        }
    }

.. note::
    Do not start the service on your own, always interact with it through the :javaref:`DownloadServiceBinder <com.castlabs.sdk.downloader.DownloadServiceBinder>`.
    The service will automatically start itself when there's at least one download ongoing and will
    stop when there are no pending downloads.

The downloader plugin needs a :javaref:`DownloadNotificationProvider <com.castlabs.sdk.downloader.DownloadNotificationProvider>` implementation.

This is a sample implementation that will show a progress bar in such notification.

.. code-block:: java

    public class NotificationProvider extends DownloadNotificationProvider {

        public NP(int notificationId) {
            super(notificationId);
        }

        @NonNull
        @Override
        public Notification getNotification(@NonNull DownloadServiceBinder downloadServiceBinder, @NonNull Context context) {
            Notification.Builder builder;

            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
                String id = "content_download_channel";
                createNotificationChannel(context, id);
                builder = new Notification.Builder(context, id);
            } else {
                builder = new Notification.Builder(context);
            }

            builder.setContentTitle(context.getString(R.string.app_name))
                    .setAutoCancel(false)
                    .setSmallIcon(R.drawable.ic_cloud_download_black_24dp);

            List<Download> downloads;
            try {
                downloads = downloadServiceBinder.getDownloads();
            } catch (Exception e) {
                e.printStackTrace();
                return builder.build();
            }

            long totalSize = 0;
            long downloadedSize = 0;
            boolean allCompleted = true;

            // Compute sum for all downloads
            for (Download download : downloads) {
                totalSize += download.getEstimatedSize();
                downloadedSize += download.getDownloadedSize();
                allCompleted = allCompleted && download.getState() == Download.STATE_DONE;
            }

            if (allCompleted) {
                builder.setContentText("Downloads finished")
                        .setProgress(0, 0, false)
                        .setOngoing(false);
            } else {
                double progress = (double) downloadedSize / totalSize;
                builder.setContentText("Downloading content")
                        .setProgress(100, (int) (100.0 * progress), false)
                        .setOngoing(true);
            }
            return builder.build();
        }
    }

The :javaref:`getNotification <com.castlabs.sdk.downloader.DownloadNotificationProvider#getNotification(DownloadServiceBinder, Context)>` method will be called after each downloader event (explained below).

If you want to change this behaviour you can override the :javaref:`onDownloadEvent <com.castlabs.sdk.downloader.DownloadNotificationProvider#onDownloadEvent(DownloadServiceBinder, Intent)>` method.

To start with the new download the application provides :javaref:`com.castlabs.sdk.downloader.DownloadService` with the ``Bundle``
so that the downloader can fetch and parse the manifest and prepare the 
:javaref:`Download <com.castlabs.sdk.downloader.Download>` model. The ``Bundle`` should hold the following keys:

- :javaref:`INTENT_URL <com.castlabs.android.SdkConsts#INTENT_URL>` expects a ``String`` (mandatory),
- :javaref:`INTENT_DOWNLOAD_ID <com.castlabs.android.SdkConsts#INTENT_DOWNLOAD_ID>` expects a ``String`` (mandatory),
- :javaref:`INTENT_DOWNLOAD_FOLDER <com.castlabs.android.SdkConsts#INTENT_DOWNLOAD_FOLDER>` expects a ``String`` (mandatory),
- :javaref:`INTENT_DRM_CONFIGURATION <com.castlabs.android.SdkConsts#INTENT_DRM_CONFIGURATION>` expects a :javaref:`DrmConfiguration <com.castlabs.android.drm.DrmConfiguration>` (optional),
- :javaref:`INTENT_CONTENT_TYPE <com.castlabs.android.SdkConsts#INTENT_CONTENT_TYPE>` expects one of :javaref:`CONTENT_TYPE_UNKNOWN <com.castlabs.android.SdkConsts#CONTENT_TYPE_UNKNOWN>` for auto-detection, :javaref:`CONTENT_TYPE_DASH <com.castlabs.android.SdkConsts#CONTENT_TYPE_DASH>`, :javaref:`CONTENT_TYPE_SMOOTHSTREAMING <com.castlabs.android.SdkConsts#CONTENT_TYPE_SMOOTHSTREAMING>`, or :javaref:`CONTENT_TYPE_MP4 <com.castlabs.android.SdkConsts#CONTENT_TYPE_MP4>`
- :javaref:`INTENT_HD_CONTENT_FILTER <com.castlabs.android.SdkConsts#INTENT_HD_CONTENT_FILTER>` expects an integer bit field
- :javaref:`INTENT_VIDEO_SIZE_FILTER <com.castlabs.android.SdkConsts#INTENT_VIDEO_SIZE_FILTER>` expects ``Point`` defining the video size filter or one of :javaref:`VIDEO_SIZE_FILTER_NONE <com.castlabs.android.SdkConsts#VIDEO_SIZE_FILTER_NONE>`, :javaref:`VIDEO_SIZE_FILTER_AUTO <com.castlabs.android.SdkConsts#VIDEO_SIZE_FILTER_AUTO>`
- :javaref:`INTENT_VIDEO_CODEC_FILTER <com.castlabs.android.SdkConsts#INTENT_VIDEO_CODEC_FILTER>` expects one of :javaref:`VIDEO_CODEC_FILTER_NONE <com.castlabs.android.SdkConsts#VIDEO_CODEC_FILTER_NONE>`, :javaref:`VIDEO_CODEC_FILTER_CAPS <com.castlabs.android.SdkConsts#VIDEO_CODEC_FILTER_CAPS>`

.. code-block:: java

    String downloadId = "ID";
    File moviesFolder = getExternalFilesDir(Environment.DIRECTORY_MOVIES);
    File target = new File(moviesFolder, "Downloads/" + downloadId);

    Bundle bundle = new Bundle();
    bundle.putString(SdkConsts.INTENT_URL, "MANIFEST_URL");
    bundle.putString(SdkConsts.INTENT_DOWNLOAD_ID, downloadId);
    bundle.putString(SdkConsts.INTENT_DOWNLOAD_FOLDER, target.getAbsolutePath());

    downloadServiceBinder.prepareDownload(context, bundle, new Downloader.ModelReadyCallback() {
        public void onError(@NonNull Exception e) {
            Log.e(TAG, "Error while preparing download: " + e.getMessage(), e);
        }
        public void onModelAvailable(@NonNull Download download) {
            // initiate selection here of video quality, audio and subtitle tracks
            // either automatically or manually
        }
    });

The client callback :javaref:`ModelReadyCallback <com.castlabs.sdk.downloader.Downloader.ModelReadyCallback>` is invoked
when the :javaref:`Download <com.castlabs.sdk.downloader.Download>` model is ready. Now the desired video quality, 
audio and subtitle tracks have to be selected by the application either automatically or manually:

.. code-block:: java

    download.setSelectedVideoTrackQuality(0);
    download.setSelectedAudioTracks(new int[]{0});

And finally the download needs to be registered in the downloader and optionally started:

.. code-block:: java

    downloadServiceBinder.createDownload(download, true);

The downloader keeps the list of downloads with states and each download can be paused, resumed and deleted independently:

.. code-block:: java

    List<Download> downloads = downloadServiceBinder.getDownloads(); 
    
    downloadServiceBinder.pauseDownload(downloads.get(0).getId());
    downloadServiceBinder.resumeDownload(downloads.get(0).getId());
    downloadServiceBinder.deleteDownload(downloads.get(0).getId());

Note that :javaref:`createDownload <com.castlabs.sdk.downloader.DownloadServiceBinder#createDownload>`, 
:javaref:`deleteDownload <com.castlabs.sdk.downloader.DownloadServiceBinder#deleteDownload>`,
:javaref:`resumeDownload <com.castlabs.sdk.downloader.DownloadServiceBinder#resumeDownload>` and
:javaref:`pauseDownload <com.castlabs.sdk.downloader.DownloadServiceBinder#pauseDownload>` are asynchronous and the results are broadcasted using ``LocalBroadcastManager``. 
The following messages reflecting the downloader state change are defined:

- :javaref:`ACTION_DOWNLOAD_STOPPED <com.castlabs.sdk.downloader.MessageHandler#ACTION_DOWNLOAD_STOPPED>`,
- :javaref:`ACTION_DOWNLOAD_CREATED <com.castlabs.sdk.downloader.MessageHandler#ACTION_DOWNLOAD_CREATED>`,
- :javaref:`ACTION_DOWNLOAD_STARTED <com.castlabs.sdk.downloader.MessageHandler#ACTION_DOWNLOAD_STARTED>`,
- :javaref:`ACTION_DOWNLOAD_DELETED <com.castlabs.sdk.downloader.MessageHandler#ACTION_DOWNLOAD_DELETED>`,
- :javaref:`ACTION_DOWNLOAD_COMPLETED <com.castlabs.sdk.downloader.MessageHandler#ACTION_DOWNLOAD_COMPLETED>`,
- :javaref:`ACTION_DOWNLOAD_NO_PENDING <com.castlabs.sdk.downloader.MessageHandler#ACTION_DOWNLOAD_NO_PENDING>`,
- :javaref:`ACTION_DOWNLOAD_ERROR <com.castlabs.sdk.downloader.MessageHandler#ACTION_DOWNLOAD_ERROR>`,
- :javaref:`ACTION_DOWNLOAD_STORAGE_LOW <com.castlabs.sdk.downloader.MessageHandler#ACTION_DOWNLOAD_STORAGE_LOW>`,
- :javaref:`ACTION_DOWNLOAD_STORAGE_OK <com.castlabs.sdk.downloader.MessageHandler#ACTION_DOWNLOAD_STORAGE_OK>`
- :javaref:`ACTION_DOWNLOAD_PROGRESS <com.castlabs.sdk.downloader.MessageHandler#ACTION_DOWNLOAD_PROGRESS>`
- :javaref:`ACTION_DOWNLOAD_PATH_UPDATE <com.castlabs.sdk.downloader.MessageHandler#ACTION_DOWNLOAD_PATH_UPDATE>`
- :javaref:`ACTION_DOWNLOAD_SERVICE_TIMEOUT <com.castlabs.sdk.downloader.MessageHandler#ACTION_DOWNLOAD_SERVICE_TIMEOUT>`

When targeting Android 15 (API 35) and higher, the system imposes time limits for background services
of type `dataSync`, which the ``DownloadService`` uses. If this limit is hit the service will
automatically pause any ongoing downloads and immediately stop itself. It is then the responsibility
of the application to resume any paused downloads when the app returns to the foreground.

.. code-block:: java

    IntentFilter filter = new IntentFilter();
    filter.addCategory(MessageHandler.INTENT_DOWNLOAD_CATEGORY);
    filter.addAction(MessageHandler.ACTION_DOWNLOAD_ERROR);
    filter.addAction(MessageHandler.ACTION_DOWNLOAD_STOPPED);
    filter.addAction(MessageHandler.ACTION_DOWNLOAD_CREATED);
    filter.addAction(MessageHandler.ACTION_DOWNLOAD_STARTED);
    filter.addAction(MessageHandler.ACTION_DOWNLOAD_DELETED);
    filter.addAction(MessageHandler.ACTION_DOWNLOAD_COMPLETED);
    filter.addAction(MessageHandler.ACTION_DOWNLOAD_NO_PENDING);
    filter.addAction(MessageHandler.ACTION_DOWNLOAD_STORAGE_OK);
    filter.addAction(MessageHandler.ACTION_DOWNLOAD_STORAGE_LOW);
    filter.addAction(MessageHandler.ACTION_DOWNLOAD_PROGRESS);
    filter.addAction(MessageHandler.ACTION_DOWNLOAD_PATH_UPDATE);
    filter.addAction(MessageHandler.ACTION_DOWNLOAD_SERVICE_TIMEOUT);

    LocalBroadcastManager.getInstance(context).registerReceiver(new BroadcastReceiver{
        public void onReceive(Context context, Intent intent) {
            String downloadId = intent.getStringExtra(MessageHandler.INTENT_DOWNLOAD_ID);
            Log.d(TAG, "Message: " + intent.getAction() + ", download Id: " + downloadId);
            switch (intent.getAction()) {
                case MessageHandler.ACTION_DOWNLOAD_STOPPED:
                    break;
                case MessageHandler.ACTION_DOWNLOAD_CREATED:
                    break;
                case MessageHandler.ACTION_DOWNLOAD_STARTED:
                    break;
                case MessageHandler.ACTION_DOWNLOAD_DELETED:
                    break;
                case MessageHandler.ACTION_DOWNLOAD_COMPLETED:
                    break;
                case MessageHandler.ACTION_DOWNLOAD_NO_PENDING:
                    break;
                case MessageHandler.ACTION_DOWNLOAD_ERROR:
                    break;
                case MessageHandler.ACTION_DOWNLOAD_STORAGE_LOW:
                    break;
                case MessageHandler.ACTION_DOWNLOAD_STORAGE_OK:
                    break;
                case MessageHandler.ACTION_DOWNLOAD_PROGRESS:
                    break;
                case MessageHandler.ACTION_DOWNLOAD_PATH_UPDATE:
                    break;
                case MessageHandler.ACTION_DOWNLOAD_SERVICE_TIMEOUT:
                    break;
                default:
                    break;
            }
        }
    }, filter);

Note that when the system storage becomes low all the downloads will be paused automatically and message 
:javaref:`ACTION_DOWNLOAD_STORAGE_LOW <com.castlabs.sdk.downloader.MessageHandler#ACTION_DOWNLOAD_STORAGE_LOW>` will be sent. 
The device storage should then be cleaned up by the application either manually or automatically and the downloads should
be resumed.

The downloader is a sticky service and will continue downloads after the system re-creates it (e.g. when the service process is killed). 

To start the playback of the downloaded content the application should follow :ref:`start_from_intent` with some additional keys:

.. code-block:: java

    Intent intent = new Intent(this, PlayerActivity.class);
    intent.putExtra(SdkConsts.INTENT_URL, download.getLocalManifestUrl());
    intent.putExtra(SdkConsts.INTENT_DRM_CONFIGURATION, download.getDrmConfiguration());
    intent.putExtra(SdkConsts.INTENT_DOWNLOAD_FOLDER, target.getAbsolutePath());

The downloader will always download one asset (movie) at a time. The reasoning for this behaviour is
that, assuming constant bandwidth for all the assets, the amount of time for all the downloads to
complete would be equal regardless they're downloaded either sequentially or in parallel. In addition,
with sequential downloading, the first asset will be ready for playback sooner and should the
connection be lost, only one download would be affected.

Scoped Storage
^^^^^^^^^^^^^^

With the introduction of Android 10 (API 29), the concept of "scoped access" has been introduced by
Google (`link <https://developer.android.com/about/versions/10/privacy/changes/>`_).

On Android 10 and higher, all apps by default get access to this mode without any additional
permissions. Scoped storage allows them to access the Media of the device through the
`media store API <https://developer.android.com/training/data-storage/shared/media/>`_, in addition
to an app-specific directories.

There's is the possibility to keep the old permission behaviour on API 29, if the application sets
the ``requestLegacyExternalStorage`` flag to ``true`` `in the manifest <https://developer.android.com/reference/kotlin/android/R.attr#requestLegacyExternalStorage:kotlin.Int>`_.
This flag only serves for API 29, and will be ignored if the ``targetSdk`` is set to API 30 or higher.

Your app is affected by change if your downloads are **not** in one of the directories returned by
the following methods:

* `getFilesDir <https://developer.android.com/reference/android/content/Context#getFilesDir()>`_
* `getExternalFilesDir <https://developer.android.com/reference/android/content/Context#getExternalFilesDir(java.lang.String)>`_
* `getExternalFilesDirs <https://developer.android.com/reference/android/content/Context#getExternalFilesDirs(java.lang.String)>`_

If you're using any other path, and you update to ``targetSdk`` 30, access to those downloads will
be lost.

You can take care of migrating the existing Downloads to a scoped storage path on your own, and
updating the reference for those in our SDK. You can do this through the
:javaref:`updateDownloadPath <com.castlabs.sdk.downloader.DownloadServiceBinder#updateDownloadPath>`
method. Note that this method will **not** migrate the files themselves.

We are also currently working on bringing to the Downloader additional helper methods to ease
migration of data. Those will be published on a later release.

Even if you choose not to do the Downloads migration now, it is highly recommended to start using
one of the aforementioned methods to retrieve a scoped storage path in order to save the new Downloads into.

For this reason the SDK will, by default, refuse to download anything into a path which is not part
of the scoped storage locations. This behaviour can be disabled by setting :javaref:`allowNonScopedStorageDownload <com.castlabs.sdk.downloader.DownloadServiceBinder#allowNonScopedStorageDownload>`
to ``true`` if needed, but we encourage you not to do so.

Request and Response Modifiers
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The Downloader exposes hooks that let you inspect or adjust every network request/response through the
:javaref:`RequestModifier <com.castlabs.android.network.RequestModifier>` and
:javaref:`ResponseModifier <com.castlabs.android.network.ResponseModifier>` interfaces.

.. code-block:: java

    // Global modifiers
    RequestModifier requestModifier = new RequestModifier() {
        @NonNull
        @Override
        public Request onRequest(@NonNull Request request) throws IOException {
            request.addHeader("Custom-Header", "Value");
            return request;
        }
    };

    ResponseModifier responseModifier = new ResponseModifier() {
        @NonNull
        @Override
        public Response onResponse(@NonNull Response response) {
            return response;
        }
    };

    // Add modifiers
    DownloaderPlugin.addRequestModifier(requestModifier);
    DownloaderPlugin.addResponseModifier(responseModifier);

    // Remove modifiers
    DownloaderPlugin.removeRequestModifier(requestModifier);
    DownloaderPlugin.removeResponseModifier(responseModifier);

    // Scoped modifiers (e.g. only for the provided download id)
    DownloaderPlugin.addRequestModifier(new RequestModifier() {
        @NonNull
        @Override
        public Request onRequest(@NonNull Request request) throws IOException {
            // Customise requests for a single download
            return request;
        }
    }, RequestModifierFilter.builder().downloadId(downloadId).build());

    DownloaderPlugin.addResponseModifier(new ResponseModifier() {
        @NonNull
        @Override
        public Response onResponse(@NonNull Response response) {
            // Inspect responses for a single download
            return response;
        }
    }, ResponseModifierFilter.builder().downloadId(downloadId).build());

Modifiers run in the order they were added and synchronously on the network thread, so avoid long
running operations inside them.

Every :javaref:`Request <com.castlabs.android.network.Request>` and :javaref:`Response<com.castlabs.android.network.Response>`
carries a ``tag``. When a call originates from the Downloader this tag is set to the download id,
which makes it straightforward to correlate DRM licence calls or other HTTP traffic with the
underlying asset:

.. code-block:: java

    DownloaderPlugin.addRequestModifier(request -> {
        if (Request.DATA_TYPE_DRM_LICENSE == request.type) {
            String downloadId = (String) request.getTag();
            // Apply tenant specific DRM headers for this download
        }
        return request;
    });

The same tagging behaviour applies when you trigger explicit licence maintenance via
:javaref:`DownloadServiceBinder.fetchLicense<com.castlabs.sdk.downloader.DownloadServiceBinder#fetchLicense>` and
:javaref:`DownloadServiceBinder.removeLicense<com.castlabs.sdk.downloader.DownloadServiceBinder#removeLicense>`,
ensuring your modifiers observe those flows as well.

Analytics
---------

The |SDK| offers plugins to integrate the following third party analytics services:

 * `Youbora <http://youbora.com/>`_
 * `Nielsen <http://www.nielsen.com/>`_
 * `Mux <http://mux.com/>`_
 * `Conviva <http://www.conviva.com/>`_
 * `Broadpeak <http://www.broadpeak.tv/>`_
 * `Adobe <http://experienceleague.adobe.com/docs/mobile-services/android/overview.html>`_

The integrations use a common interface :javaref:`AnalyticsSession
<com.castlabs.analytics.AnalyticsSession>` to expose a running session.
Please note though that for the most common use-cases, you will not need
to interact with the analytics session directly. The plugins are
integrated with the player and trigger the calls against the analytics
session automatically.

By default the analytics session is created and started whenever a stream is opened with 
:javaref:`open(Bundle) <com.castlabs.android.player.PlayerController#open(Bundle)>`. 
However, it is also possible to create and start the analytics later, upon :javaref:`play() <com.castlabs.android.player.PlayerController#play>`.
This option is useful when the playback does not start automatically and the user has to actively trigger playback start:

.. code-block:: java
    
    // Add the analytics session option
    bundle.putInt(SdkConsts.INTENT_ANALYTICS_SESSION_TYPE, SdkConsts.ANALYTICS_SESSION_TYPE_PLAY);
    // Add the option not to start playback automatically
    bundle.putBoolean(SdkConsts.INTENT_START_PLAYING, false);

All the analytics plugins use :javaref:`AnalyticsMetaData <com.castlabs.analytics.AnalyticsMetaData>`
provided to the :javaref:`PlayerController <com.castlabs.android.player.PlayerController>` by the client application either 
explicitly through :javaref:`setAnalyticsMetaData <com.castlabs.android.player.PlayerController#setAnalyticsMetaData>`,
or through the ``Intent`` (see :ref:`start_from_intent` for an example
on how to use a ``Intent`` to configure the player). The analytics metadata has to be created and passed before starting the playback.

Passing the metadata through the intent:

.. code-block:: java

    // Create analytics meta-data
    AnalyticsMetaData analyticsMetaData = new AnalyticsMetaData(isLive, assetId);

    // Set analytics common values
    analyticsMetaData.viewerId = prefs.getUserName();
    // ...

    // Set analytics custom values
    Bundle customValues = new Bundle();
    customValues.putString("title", ".....");
    customValues.putString("cdn", ".....");
    analyticsMetaData.extra.putBundle("media", customValues);

    // Set the analytics meta-data
    intent.putExtra(SdkConsts.INTENT_ANALYTICS_DATA, analyticsMetaData);

Analytics settings
^^^^^^^^^^^^^^^^^^

Global labels used by the analytics integrations can be tweaked through
:javaref:`PlayerSDK#ANALYTICS_SETTINGS <com.castlabs.android.PlayerSDK#ANALYTICS_SETTINGS>`.
:javaref:`AnalyticsSettings <com.castlabs.android.settings.AnalyticsSettings>` allows to define the
values to report when no track is selected as well as the DRM labels shared by the analytics plugins.

Example usage:

.. code-block:: java

    // Before starting playback
    PlayerSDK.ANALYTICS_SETTINGS.disabledSubtitleLabel = "off";
    PlayerSDK.ANALYTICS_SETTINGS.drmNameWidevine = "wv-l1";

Session detaching
^^^^^^^^^^^^^^^^^

By default, `AnalyticSession` are started and stopped automatically by the `PlayerController`. Sessions start
whenever playback starts, and stop whenever content ends or the `PlayerController` is disposed.

If you want to avoid this, and persist a session beyond the `PlayerController` lifecycle, you can
do the following.

In your teardown code, get the reference to the currently used `AnalyticsSession` and detach it
from the `PlayerController` before releasing it.

.. code-block:: java

    // Keep analytics session, analyticsSession is a class variable
    analyticsSession = playerView.getPlayerController().getAnalyticsSession();
    if (analyticsSession != null) {
        // Unbind this AnalyticsSession from the PlayerController
        analyticsSession.detachFromController();
    }
    // Release player
    playerView.getPlayerController().release();

While in this state. The `AnalyticsSession` will be still alive and likely, depending on the analytics provider,
sending keep-alive traces.

When you want to re-attach the `AnalyticsSession` to the `PlayerController` instance, which can be
the same or a completely new instance, you just need to set it **before** its ``open()`` method.

.. code-block:: java

    // Restore ongoing AnalyticsSession
    playerView.getPlayerController().setAnalyticsSession(analyticsSession);
    try {
        playerView.getPlayerController().open(bundle);
    } catch (Exception e) {
        Log.e(TAG, "Error while opening player: " + e.getMessage(), e);
    }

If a non-null `AnalyticsSession` is set, it will be used instead of creating a new session.

The :javaref:`detachFromController <com.castlabs.analytics.AnalyticsSession#detachFromController>`
method returns a boolean indicating if the AnalyticsSession could successfully unbind itself from
the `PlayerController`. Currently only the Youbora and Conviva implementations support session
detaching.

You should **never use the same AnalyticsSession concurrently in more than one instance of PlayerController**.
Make sure to always call ``detachFromController()`` before setting the `AnalyticsSession` to a new
`PlayerController`.

Youbora
^^^^^^^

.. External reference to Options class
.. |options_link| raw:: html

   <a class="reference external" href="http://developer.nicepeopleatwork.com/apidocs/android6/com/npaw/youbora/lib6/plugin/Options.html" target="_blank">Options</a>

.. External reference to Options guide
.. |setting_options_link| raw:: html

   <a class="reference external" href="http://developer.nicepeopleatwork.com/plugins/general/setting-youbora-options/" target="_blank">Setting Youbora Options</a>

.. External reference to Plugin class
.. |plugin_link| raw:: html

   <a class="reference external" href="https://developer.nicepeopleatwork.com/apidocs/android6/com/npaw/youbora/lib6/plugin/Plugin.html" target="_blank">Plugin</a>

The |SDK| bundles an integration for `Youbora <http://youbora.com/>`_, the 
Analytics Service from *Nice People At Work*.

If you are coming from a |SDK| version 4.1.9 or earlier and want to update your integration refer to the
:ref:`update_youbora_4110` section.

The plugin and all its dependencies are part of the bundle repository and can be added as
a dependency to your application build::

    dependencies { 
        ... 
        compile 'com.castlabs.player:youbora-plugin:|version|'
        ...
    }

You should also add NPAW's public Maven repository to your gradle file in order to get their binaries::

    repositories {
        ...
        maven { url 'https://artifact.plugin.npaw.com/artifactory/plugins/android/' }
        ...
    }

This will add the :javaref:`YouboraPlugin
<com.castlabs.sdk.youbora.YouboraPlugin>` to your project.

You can register the plugin with the SDK.

By default, the Youbora plugin will report only fatal errors to the backend. If you wish to also
report the SDK warnings as Youbora non-fatal errors, you have to enable it before registering
the plugin::

    YouboraPlugin youboraPlugin = new YouboraPlugin(accountCode);
    // Optional: Report SDK warnings as Youbora non-fatal errors (default: false)
    // youboraPlugin.reportWarnings(true);
    PlayerSDK.register(youboraPlugin);

Note that you have to pass a valid *Youbora* *accountCode* to the
constructor in order to initialize the plugin and get access to the
backend service.

In addition, you can use the ``AnalyticsOptions`` object to customize
the plugin configuration.

You should create the ``AnalyticsOptions`` object and fill it with the desired config. This object
will be later passed to the :javaref:`createMetadata <com.castlabs.sdk.youbora.YouboraPlugin#createMetadata>`
method.

Please refer to more details about possible options to the `Youbora` documentation.

By default, the plugin overrides the following global options upon initialization:

 * ``accountCode`` is always set to the systemId you pass in the plugin constructor
 
 * ``username`` is set to ``analyticsMetadata.viewerId``

 * ``contentIsLive`` is set to ``analyticsMetadata.live``


Also, the following fields may be filled by the plugin, if they are not informed:

 * ``content_id`` inside ``contentMetadata`` will be filled with ``analyticsMetadata.assetId``

 * ``contentDuration`` will be filled with ``analyticsMetadata.durationSeconds``

 * ``contentResource`` will be filled with PlayerController's :javaref:`getPath <com.castlabs.android.player.PlayerController#getPath>` return value

The `Youbora` documentations lists more options you can pass in the global
configuration. 

Passing `Youbora` configuration to the player:

.. code-block:: java

    // Create Youbora's AnalyticsOptions object
    AnalyticsOptions analyticsOptions = new AnalyticsOptions();

    // Set any desired fields
    analyticsOptions.setContentTitle("Movie");

    // Extra properties
    Map<String, Object> properties = new HashMap<>();
    properties.put("language", "English");
    properties.put("year", "2018");
    properties.put("price", "Free");

    analyticsOptions.setContentMetadata(properties);

    // Create AnalyticsMetadata
    AnalyticsMetaData analyticsMetaData = YouboraPlugin.createMetadata(
            false,              // Live or not, will set analyticsMetadata.live
            prefs.getAssetId(), // Unique asset identifier, will set analyticsMetadata.assetId
            analyticsOptions);  // Youbora AnalyticsOptions object

    // Set a user ID
    analyticsMetaData.viewerId = prefs.getUserName();

    // Set the analytics meta-data
    intent.putExtra(SdkConsts.INTENT_ANALYTICS_DATA, analyticsMetaData);

In this example, we create the meta-data and then pass it to the Intent
Bundle that is used to start playback.

With this meta-data configuration in place and passed to the
:javaref:`PlayerController <com.castlabs.android.player.PlayerController>` either through the
Intent Bundle or explicitly trough :javaref:`setAnalyticsMetaData <com.castlabs.android.player.PlayerController#setAnalyticsMetaData>`, 
the session will be automatically started and stopped.

Youbora Sessions
++++++++++++++++

Youbora also offers application tracking, called *Youbora Sessions*. For exact details on how to
use this service please refer to NPAW's documentation. In this section we exclusively present the
minimum code required to achieve interoperability, and allow Youbora's library to match the application
Session with the video ones generated by the |SDK|.

In the Application's ``onCreate``, when initializing the SDK alongside with the :javaref:`YouboraPlugin <com.castlabs.sdk.youbora.YouboraPlugin>`,
pass in an instance of the ``NpawPlugin`` you will use to perform application tracking.

.. code-block:: java

    private static final String YOUBORA_CUSTOMER_KEY = "youboracustomerkey";
    // Plugin instance to use for application tracking
    NpawPlugin npawPlugin;

    @Override
    public void onCreate() {
        super.onCreate();

        this.npawPlugin = new NpawPlugin.Builder(this, YOUBORA_CUSTOMER_KEY)
                // ...
                .build();

        YouboraPlugin youboraPlugin = new YouboraPlugin(npawPlugin);

        PlayerSDK.register(youboraPlugin);
    }

In case Youbora Sessions are not to be enabled, there's no need to provide an ``NpawPlugin``
instance to the SDK. Instead, only the customer key is required:

.. code-block:: java

    YouboraPlugin youboraPlugin = new YouboraPlugin(YOUBORA_CUSTOMER_KEY);

.. note::

   When using Youbora Sessions, if ``deviceId`` shall be informed, it is important to set it
   in the ``AnalyticsOptions`` **before** initializing the ``NpawPlugin``. Failure to do so may
   result in an autogenerated ``deviceId`` being reported to Youbora.

   This also implies that the Youbora plugin needs to be initialized with the ``NpawPlugin`` instance
   instead of the customer key.

   .. code-block:: java

      AnalyticsOptions options = new AnalyticsOptions();
      options.setDeviceId("your_custom_device_id");
      NpawPlugin npawPlugin = new NpawPlugin.Builder(this, YOUBORA_CUSTOMER_KEY)
              .setOptions(options)
              .build();

      YouboraPlugin youboraPlugin = new YouboraPlugin(npawPlugin);

In case you want to update the ``NpawPlugin`` instance once the application has been created, this can
be achieved through the component. This should be done before starting a playback session, but
it is **usually not required** if the ``NpawPlugin`` instance stays the same throughout the whole
application lifecycle.

.. code-block:: java

        // (Optional) Set the NpawPlugin to use before opening the PlayerController
        // This is not needed if the NpawPlugin instance is the same as set on
        // the app's onCreate
        YouboraAnalyticsSession component = playerController.getComponent(YouboraAnalyticsSession.class);
        component.setPlugin(newNpawPlugin);

        playerController.open(...);

.. _update_youbora_4110:

Update from |SDK| version 4.1.9 or earlier
++++++++++++++++++++++++++++++++++++++++++

The |SDK| version 4.1.10 updates the Youbora integration to version 6. This introduces some breaking
changes that are detailed here.

The Youbora binaries are now distributed in a Maven public repository. So you will need to add NPAW's
repository to your gradle file::

    repositories {
        ...
        maven { url 'http://dl.bintray.com/npaw/youbora' }
        ...
    }

The Youbora plugin is now configured through a custom configuration object; ``Options``,
instead of a ``Map``.

Refer to the Youbora developers portal for a complete explanation about these Options and how to
migrate them to V6; |setting_options_link|.

You should create the ``Options`` object and fill it with the desired config. This object
will be later passed to the :javaref:`createMetadata <com.castlabs.sdk.youbora.YouboraPlugin#createMetadata>`
method.

Nielsen
^^^^^^^

The |SDK| is integrated with `Nielsen <http://www.nielsen.com/>`_ service in the form of a plugin.
First, the plugin needs to be added as a dependency in the application gradle:

.. code-block:: groovy

    dependencies {
        ...
        implementation 'com.castlabs.player:nielsen-plugin:|version|'
        ...
    }

Then the plugin is registered and enabled by the application:

.. code-block:: java

    PlayerSDK.register(new NielsenPlugin(appId, appName, appVersion, sfCode));
    ...
    NielsenPlugin nielsen = PlayerSDK.getPlugin(NielsenPlugin.class);
    if (nielsen != null) {
        nielsen.setEnabled(true);
    }

Or the other constructor could be used to make a new instance of ``NielsenPlugin``:

.. code-block:: java

    JSONObject config = new JSONObject();
    try {
        config.put(NielsenPlugin.CONFIG_KEY_APPID, appId);
        config.put(NielsenPlugin.CONFIG_KEY_APPNAME, appName);
        config.put(NielsenPlugin.CONFIG_KEY_APPVERSION, appVersion);
        config.put(NielsenPlugin.CONFIG_KEY_SFCODE, sfCode);
    } catch (JSONException e) {
        e.printStackTrace();
    }

    NielsenPlugin nielsenPlugin = new NielsenPlugin(config);

The `Nielsen` plugin benefits from the |SDK| integrated with `IMA` service and provides two use cases out of 
the box: content with and without `IMA` advertisements. See an example above on how to use the |SDK| plugin integrated with `IMA`.

The plugin sends content and ads metadata to the `Nielsen` backend. The content metadata is taken by the plugin from the 
:javaref:`AnalyticsMetaData <com.castlabs.analytics.AnalyticsMetaData>`. The advertisements metadata is taken from `IMA` ``Ad`` object.

The plugin uses the following variables to generate the ``channelInfo``:

 * ``channelName`` will be filled with ``analyticsMetadata.assetId``
 * ``videoUrl`` will be filled with ``playerController.getPlayerConfig().contentUrl``

And in order to generate the ``jsonMetadata`` (in loadMetadata(JSONObject jsonMetadata)):

 * ``assetId`` will be filled with ``analyticsMetadata.assetId``
 * ``length`` will be filled with ``playerController.getDuration()``

And finally to generate the advertisement metadata:

 * ``type`` is one of ``preroll``, ``midroll`` and ``postroll`` according to ``ad.position``
 * ``length`` is equal to ``TimeUtils.ms2s(ad.durationMs)``
 * ``assetid`` is equal to ``ad.id``
 * ``title`` is equal to ``ad.title``

However, when needed, the advertisement metadata can be enriched by the client application by implementing and 
registering ``AdClientInterface.Listener``:

.. code-block:: java
    
    PlayerView playerView;
    ...

    AdController adController = playerView.getPlayerController().getAdController();
    if (adController != null) {
        adController.setClientListener(new AdClientInterface.Listener() {
            @Nullable
            public Bundle onGetMetadata(@NonNull Ad ad) {
                Bundle bundle = new Bundle();
                bundle.putString("cdn", ".....");
                return bundle;
            }
        });
    }

It is also possible that instead of the integrated `IMA` service the client uses its own implementation of an ads provider.
In this case, the client needs to take care of content and ads playback switches and the plugin only forwards the 
ads metadata to the `Nielsen` backend:

.. code-block:: java

    PlayerView playerView;
    ...

    AdController adController = playerView.getPlayerController().getAdController();
    if (adController != null) {
        adController.adStarted(new Ad());
        ...
        adController.adSetPlaybackPosition(1);
        adController.adSetPlaybackPosition(2);
        ...
        adController.adCompleted();
    }

Note, that in the latter case the ads metadata enrichment is also possible.


Mux
^^^

The |SDK| is integrated with `Mux <http://mux.com/>`_ service in the form of a plugin.
First, the plugin needs to be added as a dependency in the application gradle:

.. code-block:: groovy

    dependencies {
        ...
        compile 'com.castlabs.player:mux-plugin:|version|'
        ...
    }

You also need to add Mux' Maven repository to your dependencies in gradle:

.. code-block:: groovy

    repositories{
        ...
        maven { url 'https://muxinc.jfrog.io/artifactory/default-maven-release-local/' }
        ...
    }

This will add `Mux` plugin to the project and the plugin can be registered with the |SDK|:

.. code-block:: java

    MuxPlugin muxPlugin = new MuxPlugin(MUX_ENVIRONMENT_KEY);
    PlayerSDK.register(muxPlugin);

Note that you can also omit the *Mux* *ENVIRONMENT KEY* in the constructor. If you chose not to inform it,
it must be set in Mux's ``CustomerPlayerData`` right before opening the ``PlayerController``. If the
Environment Key is informed in the ``CustomerPlayerData`` passed to the Player, it will override
the one used to create the ``MuxPlugin``.

In addition, you can use the Mux ``CustomerPlayerData`` object to add content metadata.

You should create the ``CustomerPlayerData`` object and fill it with the desired fields. This object
will be later passed to the ``createMetadata`` method.

Please refer to more details about this object to the `Mux` documentation.

The Plugin will automatically map the following values from :javaref:`AnalyticsMetaData <com.castlabs.analytics.AnalyticsMetaData>`.

 * ``videoIsLive`` is informed with the value from ``analyticsMetaData.live``
 * ``videoId`` is informed with the value from ``analyticsMetaData.assetId``

In addition, the Plugin will try to fill the following fields if they are uninformed:

 * ``videoDuration`` is taken from ``analyticsMetaData.durationSeconds`` if informed, or from the stream itself otherwise (if available)
 * ``videoSourceUrl`` filled with PlayerController's :javaref:`getPath <com.castlabs.android.player.PlayerController#getPath>` return value
 * ``videoStreamType`` possible values are `DASH`, `HLS`, `MP4`, `SmoothStreaming` and `Unknown`

The Plugin also reports Mux's optional ``renditionchange`` playback event whenever
``FormatChangeListener.onVideoFormatChange(...)`` is fired while content is playing.

Passing `Mux` metadata to the player:

.. code-block:: java

    // Create MUX CustomerData, fill in metadata
    CustomerVideoData videoData = new CustomerVideoData();
    videoData.setVideoTitle("Inception");
    videoData.setVideoLanguageCode("en");
    videoData.setVideoProducer(...);
    videoData.setVideoEncodingVariant(...);

    // Create MUX PlayerData
    CustomerPlayerData playerData = new CustomerPlayerData();
    playerData.setViewerUserId("userId");
    playerData.setExperimentName(...);

    // This will override the key set when creating the MuxPlugin
    // Not needed in most cases
    playerData.setEnvironmentKey("MUX_ENVIRONMENT_KEY");

    CustomerViewData viewData = new CustomerViewData();
    viewData.setViewSessionId("viewSessionId");

    // (Optional) Set custom dimensions data
    CustomData customData = new CustomData();
    customData.setCustomData1("customDimensionValue1");

    // (Optional) Custom Options
    CustomOptions customOptions = new CustomOptions();
    //customOptions.setBeaconDomain("beacon.example.domain.com");

    // Create CustomerData and pass all data objects
    CustomerData customerData = new CustomerData(playerData, videoData, viewData);
    customerData.setCustomData(customData);

    // Create AnalyticsMetadata
    AnalyticsMetaData analyticsMetaData = MuxPlugin.createMetadata(
            false,              // Live or not, will set analyticsMetadata.live
            prefs.getAssetId(), // Unique asset identifier, will set analyticsMetadata.assetId
            customerData,       // CustomerData
            customOptions,      // CustomOptions, or null
            null);              // Already existing AnalyticsMetaData object, or null

    // Set the analytics meta-data
    intent.putExtra(SdkConsts.INTENT_ANALYTICS_DATA, analyticsMetaData);

In this example, we create the meta-data and then pass it to the Intent
Bundle that is used to start playback.

With this meta-data configuration in place and passed to the
:javaref:`PlayerController <com.castlabs.android.player.PlayerController>` either through the
Intent Bundle or explicitly trough :javaref:`setAnalyticsMetaData <com.castlabs.android.player.PlayerController#setAnalyticsMetaData>`,
the session will be automatically started and stopped.

Conviva
^^^^^^^

The |SDK| is integrated with `Conviva <http://www.conviva.com/>`_ analytics service in the form of `Conviva` plugin. 
The `Conviva` plugin is part of the bundle repository and can be added as a dependency in the application gradle::

    dependencies {
        ...
        implementation 'com.castlabs.player:conviva-plugin:|version|'
        ...
    }

This will add `Conviva` plugin to the project and the plugin can be registered with the |SDK|:

.. code-block:: java

    ConvivaPlugin convivaPlugin = new ConvivaPlugin(CONVIVA_CUSTOMER_KEY);

    if (BuildConfig.DEBUG) {
        HashMap<String, Object> settings = new HashMap<>();
        settings.put(ConvivaSdkConstants.GATEWAY_URL, CONVIVA_TOUCHSTONE_GATEWAY);
        settings.put(ConvivaSdkConstants.LOG_LEVEL, ConvivaSdkConstants.LogLevel.DEBUG);
        convivaPlugin.setSettings(settings);
    }

    HashMap<String, String> deviceInfo = new HashMap<>();
    deviceInfo.put(ConvivaSdkConstants.DEVICEINFO.ANROID_BUILD_MODEL, ".....");
    deviceInfo.put(ConvivaSdkConstants.DEVICEINFO.OPERATING_SYSTEM_VERSION, ".....");
    deviceInfo.put(ConvivaSdkConstants.DEVICEINFO.DEVICE_BRAND, ".....");
    deviceInfo.put(ConvivaSdkConstants.DEVICEINFO.DEVICE_MANUFACTURER, ".....");
    deviceInfo.put(ConvivaSdkConstants.DEVICEINFO.DEVICE_MODEL, ".....");
    deviceInfo.put(ConvivaSdkConstants.DEVICEINFO.DEVICE_TYPE, ".....");
    deviceInfo.put(ConvivaSdkConstants.DEVICEINFO.DEVICE_VERSION, ".....");
    convivaPlugin.setDeviceInfo(deviceInfo);

    PlayerSDK.register(convivaPlugin);

The `Conviva` plugin benefits from the |SDK| integrated with `IMA` service and provides with two use cases 
out of the box: content with and without `IMA` advertisements. The content metadata is taken by the plugin from the
:javaref:`AnalyticsMetaData <com.castlabs.analytics.AnalyticsMetaData>`. The advertisements metadata is taken from 
`IMA` ``Ad`` object.

The Plugin will automatically map the following values:

 * ``ConvivaSdkConstants.FRAMEWORK_NAME`` is filled with the value from ``SdkConsts.PLAYER_NAME``
 * ``ConvivaSdkConstants.FRAMEWORK_VERSION`` is filled with the value from ``PlayerSDK.getVersion()``
 * ``ConvivaSdkConstants.STREAM_URL`` is filled with the value from ``playerController.getPath()``
 * ``ConvivaSdkConstants.ASSET_NAME`` is filled with the value from ``analyticsMetaData.assetId``
 * ``ConvivaSdkConstants.IS_LIVE`` is filled with the value from ``analyticsMetaData.live``
 * ``ConvivaSdkConstants.VIEWER_ID`` is filled with the value from ``analyticsMetaData.viewerId``
 * ``ConvivaSdkConstants.DURATION`` is filled with the value from ``TimeUtils.us2s(playerController.getDuration())`` (If the duration was not valid it will filled with ``analyticsMetaData.durationSeconds``)

Also the advertisement metadata will automatically map the following values:

 * ``ConvivaSdkConstants.AdType`` is determined with the value of ``ad.streamType`` (It will be ConvivaSdkConstants.AdType.SERVER_SIDE or ConvivaSdkConstants.AdType.CLIENT_SIDE)
 * ``ConvivaSdkConstants.AdPlayer`` is determined with the value of ``ad.playerType`` (It will be ConvivaSdkConstants.AdPlayer.CONTENT or ConvivaSdkConstants.AdPlayer.SEPARATE)

Passing `Conviva` metadata to the player:

.. code-block:: java

    // Create extra tags
    Bundle extra = new Bundle();

    // Conviva predefined tags
    extra.putString(ConvivaAnalyticsSession.META_KEY_APPLICATION_NAME, "My Application Name");
    extra.putString(ConvivaAnalyticsSession.META_KEY_DEFAULT_RESOURCE, "...");

    // Optional custom tags
    Bundle customTags = new Bundle();
    customTags.putString("sampleCustomTag", "sampleCustomValue");

    // Create analytics meta-data
    AnalyticsMetaData analyticsMetaData = new ConvivaPlugin.MetadataBuilder(live, assetId)
            .viewerId("viewerID")
            .contentMetadata(extra)
            .customTags(customTags)
            .get();

    // Set the analytics meta-data
    intent.putExtra(SdkConsts.INTENT_ANALYTICS_DATA, analyticsMetaData);

By default, Conviva average bitrate metrics include both video and audio bitrate. If audio bitrate should not be
included in average bitrate, for example for radio or other audio-only streams, disable it in the metadata builder:

.. code-block:: java

    AnalyticsMetaData analyticsMetaData = new ConvivaPlugin.MetadataBuilder(live, assetId)
            .viewerId("viewerID")
            .includeAudioBitrateInAverage(false)
            .get();

With this setting, audio-only streams report average bitrate as ``0``, while audio/video streams report video average bitrate only. Instantaneous bitrate reporting is unchanged.

The `Conviva` plugin also exposes the underlying `ConvivaVideoAnalytics` and `ConvivaAdAnalytics` instances. This may be useful for an advanced use cases, such as
sending custom playback related events.

To send Conviva custom playback related event:

.. code-block:: java

    // Get the Analytics session
    ConvivaAnalyticsSession convivaAnalyticsSession = playerView.getPlayerController().getComponent(ConvivaAnalyticsSession.class);

    if (convivaAnalyticsSession != null) {
        // Fill custom map
        HashMap<String, Object> customData = new HashMap<>();
        customData.put("now", Long.toString(System.currentTimeMillis()));
        customData.put("fizz", "buzz");

        String eventType = "customEvent";

        // Get the ConvivaVideoAnalytics instance
        ConvivaVideoAnalytics videoAnalytics = convivaAnalyticsSession.getVideoAnalytics();

        // Send event
        if (videoAnalytics != null) {
            videoAnalytics.reportPlaybackEvent(eventType, customData);
        }

        // Get the ConvivaAdAnalytics instance
        ConvivaAdAnalytics adAnalytics = convivaAnalyticsSession.getAdAnalytics();

        // Send event
        if (adAnalytics != null) {
            String errorMessage = "...";
            adAnalytics.reportAdFailed(errorMessage);
        }
    }

Conviva with ``SingleControllerPlaylist``
+++++++++++++++++++++++++++++++++++++++++

When using Conviva integration with a ``SingleControllerPlaylist``, it is important to provide an ``AnalyticsMetaData`` object for each item in the playlist. This ensures that Conviva can track each item individually.

Below is an example of how to create a playlist with multiple items, each with its own ``AnalyticsMetaData`` for Conviva.

.. code-block:: java

    PlayerConfig[] playlistItems = new PlayerConfig[]{
        new PlayerConfig.Builder("http://example.com/stream1.mpd")
                .analyticsMetaData(new ConvivaPlugin.MetadataBuilder(false, "Asset1")
                        .viewerId("viewer123")
                        .get())
                .get(),
        new PlayerConfig.Builder("http://example.com/stream2.mpd")
                .analyticsMetaData(new ConvivaPlugin.MetadataBuilder(false, "Asset2")
                        .viewerId("viewer123")
                        .get())
                .get(),
        new PlayerConfig.Builder("http://example.com/stream3.mpd")
                .analyticsMetaData(new ConvivaPlugin.MetadataBuilder(false, "Asset3")
                        .viewerId("viewer123")
                        .get())
                .get()
    };

    SingleControllerPlaylist playlist = new SingleControllerPlaylist(playlistItems);
    playerController.open(playlist);


Broadpeak
^^^^^^^^^

The |SDK| is integrated with `Broadpeak <https://www.broadpeak.tv/>`__ analytics service in the form of `Broadpeak` plugin.
The `Broadpeak` plugin is part of the bundle repository and can be added as a dependency in the application gradle::

    dependencies {
        ...
        compile 'com.castlabs.player:broadpeak-plugin:|version|'
        ...
    }

This will add `Broadpeak` plugin to the project and the plugin can be registered with the |SDK|:

.. code-block:: java

    BroadpeakPlugin broadpeakPlugin = new BroadpeakPlugin("analytics_url", "nano_cdn_host", "*")

    PlayerSDK.register(broadpeakPlugin);

    // ...

    PlayerSDK.init(getApplicationContext());

The `Broadpeak` plugin uses takes care of sending player metrics and managing view lifecycle calls
as required by the underlying `SmartLib` class.

The ``BroadpeakPlugin`` intercepts the first manifest request, and replaces it with the return value
of ``SmartLib::getURL``. It's the `SmartLib` responsibility to perform such Uri redirect. Internally,
the `SmartLib` uses the domains name list in order to decide whether to actually perform a url redirect
for the current domain or not. This domain list should be set while creating the ``BroadpeakPlugin``,
through its last argument.

.. warning::

    If the default domain names list is used (``"*"``), all manifest requests will try to be redirected
    by the ``SmartLib``. This can introduce a significant delay in start-up time as the ``SmartLib``
    tries to resolve the redirect.

    Make sure you set the domain names list appropriately, following Broadpeak's recommendation.

If you want to perform any advanced operations and interact directly with the `Broadpeak` SDK,
you can use the `SmartLib` class.

In case that the BroadpeakPlugin should be disabled for a playback session, this can be achieved
as follows, before creating the ``PlayerController``:

.. code-block:: java

    BroadpeakPlugin broadpeakPlugin = PlayerSDK.getPlugin(BroadpeakPlugin.class);
    broadpeakPlugin.setEnabled(false);

    // ...

    playerView.getPlayerController().open(...);

This way the ``BroadpeakPlugin`` can be enabled or disabled at run-time without needing to re-init
the SDK.

The `Broadpeak` session allows to set custom parameters and options. This is how those can be passed
to the player:

.. code-block:: java

    // Create Analytics Metadata for Broadpeak
    Bundle customParameters = new Bundle();
    customParameters.putString("name", "value");
    customParameters.putString("pre_startup_time", "in_ms");

    // Options
    SparseArray<Object> options = new SparseArray<Object>();
    options.put(StreamingSessionOptions.GDPR_PREFERENCE, StreamingSessionOptions.GDPR_CLEAR);
    options.put(StreamingSessionOptions.USERAGENT_AD_EVENT, "useragent");
    options.put(StreamingSessionOptions.SESSION_KEEPALIVE_FREQUENCY, 5000);

    AnalyticsMetaData analyticsMetaData = new BroadpeakPlugin.MetadataBuilder(live, assetId)
            .customParameters(customParameters)
            .options(options)
            // Optional: Disable precache
            //.precache(false)
            .get();
    put(SdkConsts.INTENT_ANALYTICS_DATA, metadata);

    // Set the analytics meta-data
    intent.putExtra(SdkConsts.INTENT_ANALYTICS_DATA, analyticsMetaData);

In this example, we create the meta-data and then pass it to the Intent
Bundle that is used to start playback.

Please check the `Broadpeak` documentation for a complete list of the available parameters and options.

Broadpeak SSAI Ads
++++++++++++++++++

The Broadpeak plugin integrates server-side ad insertion (SSAI) tracking with SmartLib.
Once the :javaref:`BroadpeakPlugin <com.castlabs.sdk.broadpeak.BroadpeakPlugin>`
is registered and ad tracking is enabled, playback state is reported to SmartLib and
SSAI ad state is surfaced through the |SDK|'s ad interfaces.

Ad data is provided by SmartLib (out of band) and associated with the current content
session by the plugin.

To enable the integration, simply turn on ad tracking right after the ``BroadpeakPlugin`` creation:

.. code-block:: java

    BroadpeakPlugin plugin = new BroadpeakPlugin(C.BROADPEAK_ANALYTICS_URL, null, "*");

    // Enable Broadpeak SSAI ad tracking
    BroadpeakPlugin.adTracking = true;

    PlayerSDK.register(plugin);
    // ...
    PlayerSDK.init(getApplicationContext());

The Broadpeak SSAI integration also listens to playlist changes and keeps ad tracking aligned
with the currently active item.

If you build a custom ad UI, use the available ad API from the player ad interface for
actions such as skip or clickthrough. For clickthrough flows, notify the ad provider first
and then handle opening the click URL in your app.

.. code-block:: java

    // Assume currentAd is captured from AdInterface.Listener#onAdStarted
    AdApi adApi = playerController.getAdInterface().getAdApi();
    if (adApi != null && currentAd != null && currentAd.clickthroughUrl != null) {
        adApi.clickthrough(); // Notify Broadpeak
        startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(currentAd.clickthroughUrl)));
    }

Note that, due to a player limitation, if using client-side through the IMA integration
(see :javaref:`ImaPlugin <com.castlabs.sdk.ima.ImaPlugin>`), those are not compatible with Broadpeak
SSAI streams.

BroadpeakSdkElementExtension
++++++++++++++++++++++++++++

The `BroadpeakSdkElementExtension` allows integration of Broadpeak SDK functionality with the
`StandaloneThumbnailProvider`. This extension enables fetching thumbnails from streams through Broadpeak's CDN if needed,
and supports setting custom Broadpeak metadata, including custom parameters and options for the Broadpeak session.

Here's how to use the extension with a `StandaloneThumbnailProvider`:

.. code-block:: java

    // Create a StandaloneThumbnailProvider
    PlayerConfig config = new PlayerConfig.Builder(streamUrl).get();
    StandaloneThumbnailProvider provider = StandaloneThumbnailFactory.getProvider(config);

    // Create custom parameters and options
    Bundle customParameters = new Bundle();
    customParameters.putString("name", "value");

    SparseArray<Object> options = new SparseArray<>();
    options.put(StreamingSessionOptions.GDPR_PREFERENCE, StreamingSessionOptions.GDPR_CLEAR);

    // Create Broadpeak analytics metadata
    AnalyticsMetaData broadpeakMetadata = new BroadpeakPlugin.MetadataBuilder(isLive, assetId)
            .customParameters(customParameters)
            .options(options)
            // Optional: Disable precache
            .precache(false)
            .get();

    // Add the extension with metadata
    provider.addExtension(new BroadpeakSdkElementExtension(broadpeakMetadata));

    // Prepare the provider
    provider.prepare();

Adobe
^^^^^

The |SDK| is integrated with `Adobe <http://experienceleague.adobe.com/docs/mobile-services/android/overview.html>`_
Mobile Services SDK in the form of a plugin.

Adobe's SDK allows for tracking app navigation events, alongside with video tracking events.
This integration takes care of the later of those. The SDK can be used from app code to
perform additional operations, such as app navigation tracking.

First, the plugin needs to be added as a dependency in the application gradle:

.. code-block:: groovy

    dependencies {
        ...
        compile 'com.castlabs.player:adobe-plugin:|version|'
        ...
    }

This will add the `Adobe` plugin to the project and the plugin can be registered with the |SDK|:

.. code-block:: java

    AdobePlugin adobePlugin = new AdobePlugin();
    PlayerSDK.register(adobePlugin);

To configure Adobe's SDK, a JSON file with name ``ADBMobileConfig.json`` must be included
in the application's ``assets`` directory. The file contains the information about your
Adobe account. At runtime, the Adobe SDK will read such JSON and use its configuration. For
more information on how to obtain the configuration JSON please refer to Adobe's
`documentation <https://experienceleague.adobe.com/docs/mobile-services/using/manage-app-settings-ug/configuring-app/download-sdk.html>`_.

To enable the `Adobe` plugin, you must create it like any other plugin, and register it in the ``PlayerSDK``:

.. code-block:: java

    AdobePlugin adobePlugin = new AdobePlugin(/* optional boolean for debug logging */);
    PlayerSDK.register(adobePlugin);
    // ...
    PlayerSDK.init(getApplicationContext());

At playback time, a ``MediaSettings`` instance must be provided to the plugin, in order for it to generate
the ``AnalyticsMetadata`` instance, which will be later passed to the Player through the ``Intent`` or
the ``PlayerConfig`` when calling one of the ``PlayerController.open(...)`` methods.

The plugin will not override any of the fields in the ``MediaSettings``, except for the
``name`` in case it is ``null``.

When starting playback, the ``MediaSettings`` must be provided to the plugin, via
:javaref:`createMetadata <com.castlabs.sdk.adobe.AdobePlugin#createMetadata(MediaSettings)>`, in order to generate
the :javaref:`AnalyticsMetaData <com.castlabs.analytics.AnalyticsMetaData>` instance, which is
then passed to the :javaref:`open() <com.castlabs.android.player.PlayerController#open(Bundle)>` method:

.. code-block:: java

    MediaSettings mediaSettings = new MediaSettings();
    // Populate MediaSettings as desired
    bundle.putParcelable(SdkConsts.INTENT_ANALYTICS_DATA, AdobePlugin.createMetadata(mediaSettings));
    // ...
    playerController.open(bundle);

Alternatively, you can also use a :javaref:`PlayerConfig <com.castlabs.android.player.PlayerConfig>`
object and set its :javaref:`analyticsMetaData <com.castlabs.android.player.PlayerConfig.Builder#analyticsMetaData(AnalyticsMetaData)>` field:

.. code-block:: java

    // ...

    // Set the analytics meta-data
    playerConfigBuilder.analyticsMetaData(analyticsMetaData);

With this meta-data configuration in place and passed to the
:javaref:`PlayerController <com.castlabs.android.player.PlayerController>` either through the
Intent Bundle or explicitly trough :javaref:`setAnalyticsMetaData <com.castlabs.android.player.PlayerController#setAnalyticsMetaData>`,
the session will be automatically started and stopped.

Crash Logging
-------------

The :javaref:`Crashlog` class can be used to hook up third party crash
reporting frameworks (such as Crashlytics). It is used throughout the SDK to
report additional data in case a crash occurs. These data are delegated to
registered :javaref:`CrashReporter` instances. 

In order to integrate a third party service, you will need to implement
the :javaref:`CrashReporter` interface and register it using the
:javaref:`addReporter <Crashlog#addReporter(CrashReporter)>` method.

The SDK logs the following meta data through the crash logger class:

.. list-table::
   :widths: 20 30
   :header-rows: 1

   * - Key
     - Description
   * - CL-Playback-URL
     - The current playback URL
   * - CL-DRM-URL
     - The license server URL
   * - CL-DRM-Offline-Id
     - The identifier used to store the offline key
   * - CL-DRM-RequestID
     - The log request ID generated for calls to DRMToday
   * - CL-DRM-AssetID
     - The last used asset ID
   * - CL-DRM-VariantID
     - The last used variant ID
   * - CL-DRM-Type
     - The last used DRM type (Widevine, Playready)
   * - CL-DRM-Audio-Type
     - The last used DRM type for Audio tracks(Widevine, Playready)
   * - CL-DRM-Type
     - The last used DRM type (Widevine, Playready)
   * - CL-DRM-Device-Level
     - The security level available for the selected DRM type
   * - CL-Playback-State
     - The current playback state 
   * - CL-SDK-Version 
     - The version of the |SDK|
   * - CL-Playback-Video 
     - The dimensions of the currently played video representation 
   * - CL-Playback-Video-Bitrate 
     - The bitrate of the currently played video representation 
   * - CL-Playback-Audio-Bitrate
     - The bitrate of the currently played audio representation
   * - CL-Playback-PositionMS 
     - The current playback position in milliseconds. 
       Please note that this is updated at most with a resolution
       of one second.

In addition to the meta-data, the |SDK| will log all Exceptions that
raised during playback as non-fatal errors to the :javaref:`Crashlog`. You
can configure this behavior globally using the global
:javaref:`com.castlabs.android.PlayerSDK#CRASHLOG_AUTO_REPORT`
configuration option.

Crashlytics Integration
^^^^^^^^^^^^^^^^^^^^^^^

The SDK package bundles a plugin that can be used to setup and integrate
Crashlytics with the SDK's :javaref:`Crashlog` mechanism. To enable the plugin,
you will need to add it as a dependency in your project setup::
 
    dependencies { 
        ... 
        compile 'com.castlabs.player:crashlytics-connector:|version|'
        ...
    }

With the connector added to your project dependencies, you can load and
register the plugin before you initialize the SDK::
    
    Fabric.with(getApplicationContext(), new Crashlytics());
    PlayerSDK.register(new CrashlyticsPlugin());
    PlayerSDK.init(getApplicationContext());

Please note that the plugin does not initialize Fabric and Crashlytics for you.
Before you register an instance of this plugin with the SDK, please make sure
that you initialize the Fabric SDK and add at least the Crashlytics Kit.


User Agent
----------

The |SDK| uses a custom user agent string for all network communications
with our DRMtoday service and to the content provider.

You can use this information to monitor which SDK version, device model, or API
are in use. As well, Castlabs compiles this information to help customers solve
issues quickly and to help improve the overall quality of the service.

The parameters found on the customer User Agent are:

 * SDK Version: The three digit number representing the used |SDK| version.
 * Model: The device model.
 * API Version: The Android API version of the device.
 * ExoPlayer Library Version: The ExoPlayer library version used by the SDK.
 * Customer Id: An identification string for the service provider.
 * Device Id: The device Id as provided by the operating system.

In addition to these predefined values, you can register additional key value
pairs using the :javaref:`UserAgent.register()
<com.castlabs.android.network.UserAgent#register(String, String)>` method.
These values will be stored globally and sent with each request.

Streamroot Plugin
-----------------

The SDK package bundles a plugin that can be used to setup and integrate  `Streamroot DNA <https://streamroot.io/>`_ multi-source content delivery solutions. To enable the plugin, you will need to add it as a dependency in your project setup::
 
    dependencies { 
        ... 
        compile 'com.castlabs.player:streamroot-plugin:|version|'
        ...
    }

The example above integrates the plugin as a dependency. You can now
register it with the SDK:

.. code-block:: java

    public class MyApp extends Application {
        @Override
        public void onCreate() {
            super.onCreate();

            PlayerSDK.register(new StreamrootPlugin(
                "backend_url",
                this
            ));

            PlayerSDK.init(getApplicationContext());
        }
    }

The streamrootKey is now set as a meta-data in the application manifest.

.. code-block:: xml

  <manifest package="com.castlabs.sdk.streamroot_demo" xmlns:android="http://schemas.android.com/apk/res/android">
     <application>
         <meta-data android:name="io.streamroot.dna.StreamrootKey" android:value="your_streamroot_key"/>

After the plugin is registered, the player will use Streamroot DNA to create manifest and segments proxy.

Streamroot Plugin requires Java 8 to operate. Streamroot SDK is built with a support range going from Android 19 (4.4 KitKat) to Android 27 (8.0 Oreo).
(for more details please check `Streamroot DNA Support <https://support.streamroot.io/>`)

OkHttp Plugin
-------------

If you want to use the popular `OkHttp Library <http://square.github.io/okhttp/>`_,
the SDK bundle contains a plugin that you can use to integrate OkHttp into 
the player.

To use the plugin, you need to integrate it as a dependency into you
Application build and register the plugin with the SDK::

   dependencies{
        ...
        compile 'com.castlabs.player:okhttp-plugin:|version|'
        ...
   }

The example above integrates the plugin as a dependency. You can now
register it with the SDK:

.. code-block:: java

    public class MyApp extends Application {
        @Override
        public void onCreate() {
            super.onCreate();

            PlayerSDK.register(new OkHttpPlugin());
            PlayerSDK.init(getApplicationContext());
        }
    }

After the plugin is registered, the player will use OkHttp to create HTTP
requests to manifests and content. 

Intercept Traffic
^^^^^^^^^^^^^^^^^

OkHttp can be also used for debugging purposes to intercept and show the
network traffic. Please note that you should **not deploy this in
production** when you publish your Application. This is a tool used for
development and debugging purposes. You can find more information about
Stetho, the library used for this, on their `Website
<http://facebook.github.io/stetho/>`_.

In order to use the network interceptor, you will need to add additional
dependencies to the Stetho library and its OkHttp integration::

   dependencies{
        ...
        compile 'com.castlabs.player:okhttp-plugin:|version|'
        compile 'com.facebook.stetho:stetho:1.3.1'
        compile 'com.facebook.stetho:stetho-okhttp3:1.3.1'
        ...
   }

With these dependencies in place, you can setup the plugin using the
network interceptor:

.. code-block:: java

    public class MyApp extends Application {
        @Override
        public void onCreate() {
            super.onCreate();

            PlayerSDK.register(new OkHttpPlugin(
                 new OkHttpClient.Builder().addNetworkInterceptor(
                     new StethoInterceptor())));
            PlayerSDK.register(new OkHttpPlugin());
            PlayerSDK.init(getApplicationContext());
        }
    }

The OkHttp plugin allows you to pass a pre-configured builder in its'
constructor. We use that to pass in a builder configured to interecept
network traffic using Stetho.

Using this extended setup allows you to use your local chrome development
tools to intercept and show network traffic. For that, run the Application
on a device connected via USB, start chrome and open a tab on
`chrome://inspect/#devices <chrome://inspect/#devices>`_. Enable USB discovery and you will see you
Application listed. You can now inspect the network traffic of your
application.

.. figure:: _static/img/okhttp_network_intercept.jpg
   
   Network traffic inspection with Chrome Dev Tools and Stetho

Idle connection eviction
^^^^^^^^^^^^^^^^^^^^^^^^

While usually there's no need to handle connections at this level, it's possible to evict all idle
connections held by OkHttp. This might be of interest to save server resources if connections are
kept alive by it for a very long time::

.. code-block:: java

    playerController.release();

    // Evict idle connections
    final OkHttpPlugin okHttpPlugin = PlayerSDK.getPlugin(OkHttpPlugin.class);
    okHttpPlugin.evictIdleConnections();

Thumbnails
----------

Since version 4 of the |SDK|, the bundle contains the Thumbnail plugin.
This plugins adds support to load and display scrubbing thumbnails in
various formats.

The plugin is distributed with the SDK and you can add it as a dependency:

.. parsed-literal::

    dependencies {
        ...
        compile 'com.castlabs.player:thumbs-plugin:|version|'
        ...
    }
    
Once added you can register it with the SDK:

.. code-block:: java

    public class MyApp extends Application {
        @Override
        public void onCreate() {
            super.onCreate();

            PlayerSDK.register(new ThumbsPlugin(true));
            PlayerSDK.init(getApplicationContext());
        }
    }

Note that we pass ``true`` as the constructor argument here. This will add
the default view that can be used for basic rendering on top of
a ``PlayerView``. You can find more about the parameter in the API
documentation for :javaref:`ThumbsPlugin <com.castlabs.sdk.thumbs.ThumbsPlugin>`.

The plugin supports the following formats:

 * BIF Containers
 * WebVTT index
 * Single or gridded JPG files with a template URL
 * DASH in-stream thumbnails

Loading strategy
^^^^^^^^^^^^^^^^
In order to improve the user experience. The Thumbnail plugin also allows for smarter preloading
approaches. This is achieved through the :javaref:`LoadingStrategy <com.castlabs.sdk.thumbs.LoadingStrategy>`
class.

By default, thumbnail load will start whenever the first thumbnail is requested. It is possible
to configure this behaviour so that loading starts immediately, or after a delay following playback
start. This can be configured by providing a delay:
:javaref:`Builder.loadStartDelayMs(long) <com.castlabs.sdk.thumbs.LoadingStrategy.Builder#loadStartDelayMs(long)>`.

A loading strategy defines loading "waves". These waves only load a subset of all the available
thumbnails for an asset. The goal being to spread out the thumbnails and to be able to provide
a reasonable thumbnail for any media time in a short time.

A more spaced-out Wave will load fewer thumbnails and finish preloading faster, but will provide
less accurate thumbnails. A less spaced Wave will provide more accurate thumbnails but takes
longer to complete.

The plugin already uses a Wave-based approach for preloading by default. Such default consists of 3
waves which will preload thumbnails in 1-minute intervals, followed by 15-second intervals followed
by all thumbnails.

A custom ``LoadingStrategy`` can be defined upon plugin creation. The following example defines four
waves:

.. code-block:: java

    public class MyApp extends Application {
        @Override
        public void onCreate() {
            super.onCreate();

            final ThumbsPlugin plugin = new ThumbsPlugin(true);
                    plugin.setLoadingStrategy(new LoadingStrategy.Builder()
                            .loadStartDelayMs(0) // Start loading immediately
                            .addPercentageWave(0.10f)
                            .addTimeWave(1, TimeUnit.MINUTES)
                            .addTimeWave(20, TimeUnit.SECONDS)
                            .addStepWave(1)
                    .get());

            PlayerSDK.register(plugin);
            PlayerSDK.init(getApplicationContext());
        }
    }

Note that waves are processed sequentially in addition order, so they should be defined in ascending
"density" order, ie. waves which will preload more thumbnails should be added last.

This can also be achieved by setting a different loading strategy at runtime, before opening the Player:

.. code-block:: java

    final ThumbnailProvider thumbs = playerController.getComponent(ThumbnailProvider.class);

    thumbs.setLoadingStrategy(new LoadingStrategy.Builder()
                .loadStartDelayMs(0) // Start loading immediately
                .addPercentageWave(0.10f)
                .addTimeWave(1, TimeUnit.MINUTES)
                .addTimeWave(20, TimeUnit.SECONDS)
                .addStepWave(1)
            .get());

    playerController.open(...);

For more details check the documentation of the ``LoadingStrategy`` class.

Disk cache configuration
^^^^^^^^^^^^^^^^^^^^^^^^
On-disk caching for thumbnails can be globally controlled via the thumbnails plugin. The cache mode
defines whether thumbnail bytes are written to disk and if cached files are cleaned up when the
provider is disposed during player release.

The default is to disable on-disk caching.

.. code-block:: java

    // Configure once during app startup or before opening content
    ThumbsPlugin.setDiskCacheMode(ThumbsPlugin.DiskCacheMode.DISABLED);          // no disk cache
    // or
    ThumbsPlugin.setDiskCacheMode(ThumbsPlugin.DiskCacheMode.ENABLED_CLEANUP);   // cache + clean on release
    // or
    ThumbsPlugin.setDiskCacheMode(ThumbsPlugin.DiskCacheMode.ENABLED_RETAIN);    // cache + retain across releases

Modes:

- ``DISABLED``: Do not write thumbnails to disk (memory-only). This avoids any file deletions during
  player release.
- ``ENABLED_CLEANUP``: Write thumbnails to disk and synchronously delete cached files when the associated
  provider is disposed on release (previous default behavior).
- ``ENABLED_RETAIN``: Write thumbnails to disk and keep cached files on release (no cleanup on release).
  Note that even if the cached files are not actively cleared, they will eventually be cleared
  at the system's discretion.

Notes:

- This setting affects default thumbnail loader creation by the SDK and is effective for embedded
  and sideloaded thumbnails when no custom loader factory is provided.
- Standalone thumbnail providers created via ``StandaloneThumbnailFactory`` default to no disk cache.

Network Configuration
^^^^^^^^^^^^^^^^^^^^^
The :javaref:`NetworkConfiguration <com.castlabs.android.network.NetworkConfiguration>` 
can be used to specify the timeouts for the thumbnail downloads.

The following example shows how you can achieve this:

.. code-block:: java

    bundle.put(SdkConsts.INTENT_NETWORK_CONFIGURATION, new NetworkConfiguration.Builder()
        // You can specify the timeouts for the thumbnails
        .thumbnailConnectionTimeoutMs(500)
        .thumbnailReadTimeoutMs(500)
        .get());

Embedded
^^^^^^^^
For embedded thumbnails, such as in a DASH in-stream thumbnails, there's no additional action required.
The SDK will automatically load the thumbnails defined in AdaptationSets onto the Player Model. You
still need to control how those will be rendered, which is detailed in the section :ref:`thumbs-view-label`.

Side-loaded
^^^^^^^^^^^

You can start loading thumbnail data directly if you registered the plugin and you are loading 
your streams through an `Intent Bundle` (see :ref:`start_from_intent`).
You need to create an instance of :javaref:`SideloadedTrack
<com.castlabs.android.player.models.SideloadedTrack>` and add it to your intent bundle
using the :javaref:`SdkConsts.INTENT_SIDELOADED_TRACKS_ARRAYLIST
<com.castlabs.android.SdkConsts#INTENT_SIDELOADED_TRACKS_ARRAYLIST>` key. For example:

.. code-block:: java

    ArrayList<SideloadedTrack> data = new ArrayList<SideloadedTrack>(){{
        add(new SideloadedTrack.ThumbnailBuilder()
                .url("thumbs/thumbs.vtt")
                .thumbnailType(ThumbnailDataTrack.TYPE_WEBVTT_INDEX)
        .get());
    }}
    ...
    intent.putParcelableArrayList(SdkConsts.INTENT_SIDELOADED_TRACKS_ARRAYLIST, data);

The ``ThumbnailBuilder`` also provides methods for configuring extra fields necessary for other
thumbnail formats, such as gridded JPEG template:

.. code-block:: java

    ArrayList<SideloadedTrack> data = new ArrayList<SideloadedTrack>(){{
        add(new SideloadedTrack.ThumbnailBuilder()
                .url("http://example.com/content/thumbs/$index$.jpg")
                .gridHeight(4)
                .gridWidth(5)
                .thumbnailType(ThumbnailDataTrack.TYPE_JPEG_TEMPLATE)
                .intervalMs(10_000)
        .get());
    }}
    ...
    intent.putParcelableArrayList(ThumbsPlugin.INTENT_SIDELOADED_TRACKS_ARRAYLIST, data);

This will create a data object and add it to the intent. Note that we are
specifying the type explicitly here. This is not strictly necessary and
the plugin will try to infer the type from the URL extension, i.e. `.vtt`
vs. `.bif` vs. `.jpg`.

With the data object added to the intent, an implementation of
:javaref:`ThumbnailProvider <com.castlabs.sdk.thumbs.ThumbnailProvider>`
will be created automatically and is exposed as a component of the
``PlayerController``. You can access the provider instance using 
:javaref:`PlayerController.getComponent() <PlayerController#getComponent(Class)>`. 
For example:

.. code-block:: java
   
    ThumbnailProvider provider = playerController.getComponent(ThumbnailProvider.class);
    
The :javaref:`ThumbnailProvider
<com.castlabs.sdk.thumbs.ThumbnailProvider>` interface is the main
interaction point to get access to thumbnails. It provides
a ``getThumbnail`` method that asynchronously loads a thumbnail for the
requested position and provides it back the callback implementation that
is passed to the method. It supports performing multiple concurrent ``getThumbnail`` calls.
Thumbnails can also be requested with a time tolerance. Check the
:ref:`thumbs-time-tolerance-label` section for more details.

At this point a view implementation can use the provider and a callback to
get a handle on the Bitmap and show it on screen. The |SDK| already
bundles a simple view implementation with a relatively flexible way to 
render and position a thumbnail.

.. _thumbs-view-label:

Using the built-in thumbnails view
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

If you registered the thumbnails plugin and enabled the usage of the
internal view component, you do not need to create a view or interact with
the provider explicitly. Instead, you add your ``ThumbnailData`` to the
`Intent Bundle` and once a `PlayerView` is initialized, you can access the 
thumbnail view component and show or hide thumbnails on to of your player
view. 

In order to properly render the thumbnails, you'll need to register a
:javaref:`SeekBarListener <com.castlabs.sdk.playerui.PlayerControllerView.SeekBarListener>` to the
``PlayerControllerView``. This listener will be invoked whenever there's a scrubbing event on
the Seekbar, so you can implement your logic regarding thumbnail rendering.

You can also specify a third parameter to fine-tune which Thumbnail should be fetched in relation
to the requested time. More info about this parameter is available in the `Thumbnail index selection` section.

Here you can see a sample implementation.

.. code-block:: java
   
    public void onSeekbarScrubbed(long positionUs, double seekBarProgressPercent) {
        ThumbsPlugin.ThumbnailViewComponent thumbsView = playerView.getComponent(
                ThumbsPlugin.ThumbnailViewComponent.class);
        if (thumbsView != null){
            thumbsView.show(positionUs, new DefaultThumbnailView.Callback() {
                @Override
                public void getThumbnailRect(Rect output, DefaultThumbnailView.ThumbnailInfo info, boolean isSmallScreen) {
                    ViewGroup videoView = playerView.getVideoView();
                    output.set(
                            videoView.getLeft(),
                            videoView.getTop(),
                            videoView.getRight(),
                            videoView.getBottom());
                }
            }, ThumbsPlugin.THUMBNAIL_INDEX_CURRENT);
        }
    }

    @Override
    public void onSeekbarReleased() {
        final ThumbsPlugin.ThumbnailViewComponent thumbsView = playerView.getComponent(
                ThumbsPlugin.ThumbnailViewComponent.class);
        if (thumbsView != null){
            thumbsView.hide();
        }
    }

The example above demonstrates the basic interactions with the
``ThumbnailViewComponent``. Here we assume we have to methods, one that is
called when a Seekbar is scrubbed and thumbnails should be shown, and the
counterpart that is triggered when scrubbing stops and the thumbnails
should be hidden. Hiding is relatively simple and a matter of calling the
``hide()`` method. Showing the thumbnails is slightly more complex. We
call the ``show()`` method and pass two parameters: the time in
microseconds for which we would like to receive a thumbnail and
a ``Callback`` implementation. The callback is triggered once the Bitmap
data are loaded. It its called with size information about the thumbnail
as well as an ``output`` rectangle. The ``output`` rectangle needs to be
filled with the coordinates and size of the desired thumbnail. The
coordinates are relative to the thumbnail view container, but the default
view container has the same dimensions as the player view. The example
above uses the exposed video view to place the thumbnail image full-screen
above the video, but treat this as a simple example. You can use the
``output`` rectangle to customize the size and the position of the
resulting thumbnail.

The returned Thumbnail might not be the corresponding one for the requested
position, as the ``DefaultThumbnailView`` uses the ``getThumbnail`` API
defining some tolerance. It is recommended that this is the behaviour used
as it improves the perceived responsibility. Nevertheless, this is a configurable
setting on the ``DefaultThumbnailView`` and you may change it through
:javaref:`DefaultThumbnailView#setThumbnailTolerance(long)`.

The ``DefaultThumbnailView`` will automatically be created if you use a ``PlayerView``.

If you don't want to use a ``PlayerView`` but still decide to use the ``DefaultThumbnailView``
available in the |SDK|, you can create it yourself, either programmatically or by defining it in
an xml layout file.
If you use the latest approach, you must pass your layout View to :javaref:`PlayerController#setComponentView(int, View)`
with the corresponding id, defined in :javaref:`PRESTO_DEFAULT_THUMBNAIL_VIEW <com.castlabs.sdk.thumbs.ThumbsPlugin#PRESTO_DEFAULT_THUMBNAIL_VIEW>`,
or ``R.id.presto_default_thumbnail_view`` so the ``ThumbsPlugin`` can properly find the ``DefaultThumbnailView``.

.. _thumbs-time-tolerance-label:

Thumbnail rendering time tolerance
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

In order to benefit from the several loading Waves approach, the ``ThumbsPlugin`` provides an API
to return a close neighbour of the desired thumbnail if its data is already available.

This tolerance represents the maximum difference of time time between the position of the requested thumbnail
and that of a potentially already-loaded thumbnail in the index. If a matching nearby
thumbnail with data within the specified tolerance is found, it will be returned immediately. Otherwise,
the exact thumbnail will be scheduled for fetching.

By default, the ``DefaultThumbnailView`` performs thumbnail requests with some tolerance, which
can be changed through :javaref:`DefaultThumbnailView#setThumbnailTolerance(long)`.

If you're performing direct thumbnail requests through a ``ThumbnailProvider``, you can specify
a tolerance with the :javaref:`ThumbnailProvider#getThumbnail(long, ThumbnailProvider$Callback, int, long)` method.

Thumbnail index selection
^^^^^^^^^^^^^^^^^^^^^^^^^

When using the ``ThumbnailViewComponent``, it's possible to pass a third parameter to the
:javaref:`show() <ThumbsPlugin.ThumbnailViewComponent#show(long, com.castlabs.sdk.thumbs.DefaultThumbnailView$Callback, int)>` method
in order to determine which Thumbnail should be returned, relative to the requested media time.

There are three possible values:

* :javaref:`ThumbsPlugin#THUMBNAIL_INDEX_CURRENT`:
  This corresponds to the thumbnail with the highest media time that is lower than the requested position.
* :javaref:`ThumbsPlugin#THUMBNAIL_INDEX_NEXT`:
  This corresponds to the thumbnail with the lowest media time that is higher than the requested position.
* :javaref:`ThumbsPlugin#THUMBNAIL_INDEX_CLOSEST`:
  This corresponds to the thumbnail with the lowest difference between its media time and the requested position.

The following scheme can better illustrate the behaviour. The example assumes Thumbnails are available
every 10 seconds, starting from the beginning (media time 0). The top marker represents the requested
time, that being the first parameter to the ``ThumbnailViewComponent.show(...)`` call, and the ones
below the timeline, the Thumbnail times for each indexing type:

.. code-block:: text

    Requested Thumbnail for position 4

              0   v     10        20        30
    Return    |---------|---------|---------|-----
    Current:  ^
    Closest:  ^
    Next:               ^


    Requested Thumbnail for position 6

              0     v   10        20        30
    Return    |---------|---------|---------|-----
    Current:  ^
    Closest:            ^
    Next:               ^


    Requested Thumbnail for position 18

              0         10      v 20        30
    Return    |---------|---------|---------|-----
    Current:            ^
    Closest:                      ^
    Next:                         ^

Downloader integration
^^^^^^^^^^^^^^^^^^^^^^

The thumbnails plugin is integrated with the Downloader Plugin, hence
there is offline support for thumbnails as well. This is currently limited
to `BIF` and `WebVTT` thumbnails. Just add the ``ThumbnailData`` to the
Intent that is used to initiate the download and the thumbnails will be
fetched and downloaded as well. When you start the local playback, pass
*the same* ``ThumbnailData`` to the local playback intent. The player will
translate any remote URLs automatically to the correct local files and
relative URLs will still resolve correctly, now relative to the local
manifest.

Thumbnail loading details
^^^^^^^^^^^^^^^^^^^^^^^^^

The thumbnail plugin can preload thumbnails in the background in order for them
to be ready as much a possible whenever a request comes in.

The background loader is multi-threaded, and the number of threads is configurable
through the :javaref:`ThumbsPlugin#setThumbnailLoadThreads(int)` static method.

Whenever a specific thumbnail request (either by asking the ``DefaultThumbnailView`` to show
a thumbnail for a time, or through one of the ``getThumbnail`` methods) is issued, it will skip the
pending queue of background thumbnail loads in order to be as responsive as possible. Successive
specific requests will be queued and executed in a FIFO fashion. To clear this queue of pending
specific thumbnail requests, the following method can be used: :javaref:`ThumbnailProvider#cancelPendingRequests()`.

Standalone Thumbnail providers
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

It is possible to obtain an instance of a ``ThumbnailProvider`` without using a
``PlayerController`` instance which will allow for fetching thumbnails independently of
any ongoing playback, even for completely different content.

In order to obtain a ``StandaloneThumbnailProvider`` to fetch embedded DASH thumbnails use :javaref:`StandaloneThumbnailFactory.getProvider <StandaloneThumbnailFactory#getProvider(PlayerConfig)>`:

.. code-block:: java

    StandaloneThumbnailProvider provider = StandaloneThumbnailFactory.getProvider(
        new PlayerConfig.Builder("https://example.com/manifest.mpd").get());

    // Add Broadpeak support if needed
    provider.addExtension(new BroadpeakSdkElementExtension());

    // Fetch thumbnails
    Future<ThumbnailProvider.ThumbnailResult> thumbnailResultFuture = provider.getThumbnail(30_000_000L, ThumbsPlugin.THUMBNAIL_INDEX_CURRENT);
    Bitmap thumbnailBitmap = thumbnailResultFuture.get().thumbnail;

    // Free up resources
    provider.destroy();

In case the provided DASH manifest is a live stream manifest updates will be performed in the background
resulting in the thumbnail index also being updated alongside. To get updates of this process it is
possible to register a :javaref:`ThumbnailProvider.IndexRefreshListener`:

.. code-block:: java

    provider.addIndexRefreshListener(new ThumbnailProvider.IndexRefreshListener() {
        @Override
        public void onIndexRefreshed(@NonNull ThumbnailProvider provider) {
            // Index has been refreshed. Fetch thumbnail for a given time
            Future<ThumbnailProvider.ThumbnailResult> thumbRequest = provider.getThumbnail(position, ThumbsPlugin.THUMBNAIL_INDEX_CURRENT);
        }
    });

It is also possible to get thumbnails out of a VTT or JPG Template ``SideloadedTrack``:

.. code-block:: java

    StandaloneThumbnailProvider provider = StandaloneThumbnailFactory.getProvider(new SideloadedTrack.ThumbnailBuilder()
                .url("https://demo.castlabs.com/media/QA/tos_thumbs/$index$.jpg")
                .gridHeight(1)
                .gridWidth(1)
                .thumbnailType(ThumbnailDataTrack.TYPE_JPEG_TEMPLATE)
                .intervalMs(5_000)
            .get());


360-Degree Playback Plugin
--------------------------

The |SDK| can be used with the optional 360 Degree player plugin to enable
360 playback using the |SDK|.

The plugin is distributed with the |SDK|. You can reference and load the
360 plugin as a separate dependency::

    dependencies {
        ...
        compile 'com.castlabs.player:360-plugin:|version|'
        ...
    }

Before you can use this plugin, you have to register it before you initialize the SDK:

.. code-block:: java

    public class MyApp extends Application {
        @Override
        public void onCreate() {
            super.onCreate();

            PlayerSDK.register(new ThreesixtyPlugin());
            PlayerSDK.init(getApplicationContext());
        }
    }

Google Cardboard
^^^^^^^^^^^^^^^^

The :javaref:`PlayerView360<com.castlabs.sdk.threesixty.PlayerView360>` is an extension of the
default `PlayerView`. You can add it as a view component to your own activity. The view exposes the
underlying `GvrView` which needs to be passed to the Google VR SDK in your activity.

You can use the `PlayerView360` in your own Activity (that usually extends `GvrActivity`). The `PlayerView360`
will handle all the transformations required to render the view.

An example of this is the `CardboardActivity` class in the 'threesixty_demo' sample app.

Google cast Manager
-------------------

The |SDK| contains the :javaref:`GoogleCastManager<com.castlabs.sdk.googlecast.GoogleCastManager>`
class that will ease the process of starting and managing remote cast session.

This class allows to start a cast session, get the current remote player state and select
audio, video and subtitle tracks.

AndroidX Media2
-------------------

:javaref:`MediaSessionPlugin<com.castlabs.sdk.mediasession.MediaSessionPlugin>` integrates |SDK| with AndroidX Media2 Session APIs providing easy to use Media2
functionality. The application does not need to create and maintain MediaSession instance, it is only
responsible for Media Session lifecycle i.e. when Media Session is created and optionally when it is destroyed.
First, :javaref:`MediaSessionPlugin<com.castlabs.sdk.mediasession.MediaSessionPlugin>` shall be enabled before |SDK| initialization:

.. code-block:: java

    public class MyApp extends Application {
        @Override
        public void onCreate() {
            super.onCreate();

            PlayerSDK.register(new MediaSessionPlugin());
            PlayerSDK.init(getApplicationContext());
        }
    }

Registering the plugin does not create Media Session yet and the application will need to explicitly
request its creation by calling :javaref:`MediaSessionPlugin.enableMediaSession()
<com.castlabs.sdk.mediasession.MediaSessionPlugin#enableMediaSession(PlayerController)>`. Media Session is linked
to the :javaref:`PlayerController <com.castlabs.android.player.PlayerController>` and therefore the application has to provide
:javaref:`PlayerController <com.castlabs.android.player.PlayerController>` instance
when enabling Media Session. :javaref:`MediaSessionPlugin<com.castlabs.sdk.mediasession.MediaSessionPlugin>`
supports both :javaref:`PlayerController <com.castlabs.android.player.PlayerController>` and
:javaref:`SingleControllerPlaylist <com.castlabs.android.player.SingleControllerPlaylist>` instances.
Media Session can be enabled any time before or after opening the playback and the only constraint is the presence of a valid
:javaref:`PlayerController <com.castlabs.android.player.PlayerController>` instance.

The code below enables Media Session when :javaref:`PlayerController <com.castlabs.android.player.PlayerController>`
is created and before the playback is opened:

.. code-block:: java

    public class SimplePlaybackDemo extends Activity {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            // ...
            PlayerView playerView = (PlayerView) findViewById(R.id.player_view);

            MediaSessionPlugin.enableMediaSession(playerView.getPlayerController());

            // ...
            PlayerConfig playerConfig;
            // playerConfig = ...
            playerController.open(playerConfig);
        }

        @Override
        protected void onResume() {
            super.onResume();
            // ...
            PlayerView playerView = (PlayerView) findViewById(R.id.player_view);

            MediaSessionPlugin.enableMediaSession(playerView.getPlayerController());
            playerView.getLifecycleDelegate().resume();
        }
    }

Media Session is closed automatically upon :javaref:`PlayerController.destroy()
<com.castlabs.android.player.PlayerController#destroy()>`. However,
it is also possible to close Media Session explicitly:

.. code-block:: java

    // ...
    PlayerView playerView = (PlayerView) findViewById(R.id.player_view);
    MediaSessionPlugin.disableMediaSession(playerView.getPlayerController());
    // ...

MediaSession can also be customized according to the application needs e.g. default seek values or callbacks
can be installed:

.. code-block:: java

    final MediaSessionBuilder mediaSessionBuilder = new MediaSessionBuilder()
			.setFastForwardIncrementMs(10000)
			.setRewindIncrementMs(10000);
    // ...
    MediaSessionPlugin.enableMediaSession(playerView.getPlayerController(), mediaSessionBuilder);
    // ...

Drm Device Time Checker
-----------------------

The |SDK| contains the :javaref:`DrmDeviceTimeCheckerPlugin<com.castlabs.sdk.drm.DrmDeviceTimeCheckerPlugin>`
Plugin which is able to perform periodic checks related to DRM licensing in order to minimize some
Widevine limitations.

The plugin is distributed with the |SDK|. You can reference and load the
Device Time Checker plugin as a separate dependency::

    dependencies {
        ...
        compile 'com.castlabs.player:drm-device-time-checker-plugin:|version|'
        ...
    }

Before you can use this plugin, you have to register it before you initialize the SDK:

.. code-block:: java

    public class MyApp extends Application {
        @Override
        public void onCreate() {
            super.onCreate();

            PlayerSDK.register(new DrmDeviceTimeCheckerPlugin());
            PlayerSDK.init(getApplicationContext());
        }
    }

Optionally, you can specify the time period at which he license checks should be performed.
Default value is every 5 minutes.

.. code-block:: java

    // ...
    PlayerSDK.register(new DrmDeviceTimeCheckerPlugin(60)); // Check every minute
    // ...

This plugin needs two extra Android permissions. These permissions allow to internally keep
track of license times.

    * :javaref:`ACTION_BOOT_COMPLETED <android.content.Intent#ACTION_BOOT_COMPLETED>`
    * :javaref:`ACTION_TIME_CHANGED <android.content.Intent#ACTION_TIME_CHANGED>`

These permissions are automatically added to your AndroidManifest file once you include this
plugin in your build file.
