package com.castlabs.sdk.demos;

import android.app.Activity;
import android.graphics.Rect;
import android.media.MediaCodec;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.castlabs.android.drm.MemoryKeyStore;
import com.castlabs.android.player.AbstractVideoRendererListener;
import com.castlabs.android.player.ConfigurationProvider;
import com.castlabs.android.player.DrmLicenseLoader;
import com.castlabs.android.player.ExternalSourceSelector;
import com.castlabs.android.player.models.VideoTrackQuality;
import com.castlabs.sdk.demos.ads.AdUi;
import com.castlabs.sdk.ima.ImaAdRequest;
import com.castlabs.sdk.thumbs.ThumbnailProvider;
import com.castlabs.utils.TimeUtils;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.RetryCounter;
import com.google.android.material.snackbar.BaseTransientBottomBar;
import com.google.android.material.snackbar.Snackbar;

import android.os.Handler;
import android.os.Looper;
import android.os.Parcelable;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.castlabs.android.SdkConsts;
import com.castlabs.android.adverts.Ad;
import com.castlabs.android.adverts.AdInterface;
import com.castlabs.android.drm.DrmConfiguration;
import com.castlabs.android.drm.KeyStore;
import com.castlabs.android.player.AbstractPlayerListener;
import com.castlabs.android.player.PlayerConfig;
import com.castlabs.android.player.PlayerController;
import com.castlabs.android.player.PlayerView;
import com.castlabs.android.player.exceptions.CastlabsPlayerException;
import com.castlabs.android.views.SubtitlesViewComponent;
import com.castlabs.sdk.playerui.PlayerControllerProgressBar;
import com.castlabs.sdk.playerui.PlayerControllerView;
import com.castlabs.sdk.thumbs.DefaultThumbnailView;
import com.castlabs.sdk.thumbs.ThumbsPlugin;

import java.util.ArrayList;
import java.util.Random;

public class SimplePlaybackDemo extends Activity {

	private static final String TAG = "SimplePlaybackDemo";
	private static final String SAVED_PLAYBACK_STATE_BUNDLE_KEY = "SAVED_PLAYBACK_STATE_BUNDLE_KEY";

	private AbstractPlayerListener playerListener;
	private PlayerControllerView.Listener playerControllerViewListener;
	private PlayerControllerView.SeekBarListener seekBarListener;
	private PlayerControllerView.PositionAdjuster positionAdjuster;
	private DefaultThumbnailView.Callback thumbnailViewCallback;
	private AdInterface.Listener adListener;
	private ExternalSourceSelector externalSourceSelector;
	private ConfigurationProvider configurationProvider;

	// The playback bundle
	private Bundle playbackBundle;
	@Nullable private ArrayList<Bundle> playbackPlaylist;
	private boolean wrapPlaylist = MainActivity.DEFAULT_INTENT_WRAP_PLAYLIST;
	private long autoNextMs = MainActivity.DEFAULT_INTENT_AUTO_NEXT_PLAYLIST_MS;
	private boolean waitForVideoFrame = MainActivity.DEFAULT_INTENT_WAIT_VIDEO_FRAME_PLAYLIST;
	private boolean stopOnError = MainActivity.DEFAULT_INTENT_STOP_ON_ERROR_PLAYLIST;
	@Nullable private Random randomNext;
	@Nullable private Runnable autoNextRunnable;
	@NonNull private Handler mainHandler = new Handler(Looper.getMainLooper());

	// The player view that we use to start the playback
	private PlayerView playerView;

	// The view to show controls
	private PlayerControllerView playerControllerView;

	private Button buttonPreroll;
	private Button buttonPostroll;
	private Button buttonSkippable;
	private ViewGroup thumbnailTimeContainer;
	private TextView thumbnailTime;
	private TextView requestedTime;
	private Button buttonPrevious;
	private Button buttonNext;
	private LinearLayout manualAdsLayout;
	private AdUi adUi;
	private boolean thumbnailPrepared = false;
	private boolean hideThumbnailOnPlay = false;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_simple_playback_demo);

		playerView = findViewById(R.id.player_view);
		playerControllerView = findViewById(R.id.player_controls);
		playerControllerView.showExtendedTrackInfo(true);
		playerControllerView.allowMultiTrackVideoSelection(true);
		thumbnailTimeContainer = findViewById(R.id.thumbnailTimeLayout);
		thumbnailTime = findViewById(R.id.thumbnailTime);
		requestedTime = findViewById(R.id.requestedTime);
		buttonPrevious = findViewById(R.id.buttonPrevious);
		buttonNext = findViewById(R.id.buttonNext);

		initializePlayerListener();
		initializePlayerControllerViewListener();
		initializeSeekBarListener();
		initializePositionAdjuster();
		initializeThumbnailViewCallback();
		initializeAdListener();
		initializeExternalSourceSelector();
		initializeConfigurationProvider();
		initializePlaybackBundle(savedInstanceState);

		if (isPlaylist()) {
			Bundle bundle = getIntent().getExtras();
			if (bundle != null) {
				wrapPlaylist = bundle.getBoolean(MainActivity.INTENT_WRAP_PLAYLIST, wrapPlaylist);
				autoNextMs = bundle.getLong(MainActivity.INTENT_AUTO_NEXT_PLAYLIST_MS, autoNextMs);
				waitForVideoFrame = bundle.getBoolean(MainActivity.INTENT_WAIT_VIDEO_FRAME_PLAYLIST, waitForVideoFrame);
				stopOnError = bundle.getBoolean(MainActivity.INTENT_STOP_ON_ERROR_PLAYLIST, stopOnError);
				if (autoNextMs != C.TIME_UNSET) {
					autoNextRunnable = new Runnable() {
						@Override
						public void run() {
							playNext();
						}
					};
				}
				if (bundle.getBoolean(MainActivity.INTENT_RANDOM_NEXT_PLAYLIST, MainActivity.DEFAULT_INTENT_RANDOM_NEXT_PLAYLIST)) {
					randomNext = new Random();
				}
			}
			initializePreviousButton();
			initializeNextButton();
		}

		// (Optional) Custom Ad Ui rendering
		//setupCustomAdsUI();

		if (autoNextRunnable != null) {
			if (waitForVideoFrame) {
				playerView.getPlayerController().addVideoRendererListener(new AbstractVideoRendererListener() {
					@Override
					public void onRenderedFirstFrame(Object object) {
						super.onRenderedFirstFrame(object);
						Log.d(TAG, "Rendered frame, URL = " + playerView.getPlayerController().getPlayerConfig().contentUrl);
						mainHandler.postDelayed(autoNextRunnable, getNextDelayMs());
					}
				});
			} else {
				mainHandler.postDelayed(autoNextRunnable, getNextDelayMs());
			}
		}

		openPlaybackBundle();
	}

	// Delegate the onStart event to the player views lifecycle delegate.
	// The delegate will make sure that the screen safer will be disabled and
	// the display will not go to sleep
	@Override
	protected void onStart() {
		super.onStart();
		playerView.getLifecycleDelegate().start(this);
		// Optional for custom ad ui rendering
		if (adUi != null) {
			adUi.bind(playerView.getPlayerController());
		}
	}

	// Delegate the onResume event to the player views lifecycle delegate.
	// The delegate ensures that the player recovers from a saved state. This needs to
	// be implemented to ensure the the user can for example go to the home screen and
	// come back to this activity.
	@Override
	protected void onResume() {
		super.onResume();
		// Bind the controller view and its listener
		playerControllerView.bind(playerView);
		playerControllerView.addListener(playerControllerViewListener);
		playerControllerView.setSeekBarListener(seekBarListener); // This is for Thumbnails
		playerControllerView.setPositionAdjuster(positionAdjuster);

		PlayerControllerProgressBar progressBar = (PlayerControllerProgressBar) findViewById(R.id.progress_bar);
		progressBar.bind(playerView.getPlayerController());

		// (Optional) Add ad events listener
		playerView.getPlayerController().getAdInterface().addAdListener(adListener);

		playerView.getLifecycleDelegate().resume();
	}

	// Delegate the onStop event to the player views lifecycle delegate.
	// We release the player when the activity is stopped. This will release all the player
	// resources and save the current playback state. Saving the state is required so the
	// onResume callback can recover properly.
	@Override
	protected void onStop() {
		super.onStop();
		// Optional for custom ad ui rendering
		if (adUi != null) {
			adUi.unbind();
		}

		// Unbind the player controller view and remove the listener
		playerControllerView.unbind();
		playerControllerView.removeListener(playerControllerViewListener);
		playerControllerView.setSeekBarListener(null);

		if (autoNextRunnable != null) {
			mainHandler.removeCallbacks(autoNextRunnable);
		}
		playerView.getLifecycleDelegate().releasePlayer(false);
	}

	// Save the playback state when the activity is destroyed in order to correctly
	// resume after the activity is re-created again i.e. onCreate is called
	@Override
	public void onSaveInstanceState(@NonNull Bundle outState) {
		Bundle savedStateBundle = new Bundle();
		PlayerConfig playerConfig = playerView.getPlayerController().getPlayerConfig();
		if (playerConfig != null) {
			playerView.getPlayerController().getPlayerConfig().save(savedStateBundle);
			outState.putBundle(SAVED_PLAYBACK_STATE_BUNDLE_KEY, savedStateBundle);
		}
		super.onSaveInstanceState(outState);
	}

	@Override
	public boolean dispatchKeyEvent(KeyEvent event) {
		boolean handled = false;
		if (playerControllerView != null && playerControllerView.getVisibility() == View.VISIBLE) {
			handled = playerControllerView.dispatchKeyEvent(event);
		} else { // Not visible
			if (playerView.getPlayerController().getAdInterface() != null && !playerView.getPlayerController().getAdInterface().isPlaying()) {
				if (event.getAction() == KeyEvent.ACTION_DOWN) {
					handled = playerControllerView.dispatchKeyEvent(event);
				}
			}
		}
		return handled || super.dispatchKeyEvent(event);
	}

	private long getNextDelayMs() {
		long nextDelayMs = randomNext != null ? randomNext.nextInt((int)autoNextMs) : autoNextMs;
		Log.d(TAG, "Next switch is in " + nextDelayMs + " ms");
		return nextDelayMs;
	}

	private void setupManualAdsInsertion() {
		if (SdkConsts.AD_SCHEDULE_MANUAL.equals(playbackBundle.getParcelable(SdkConsts.INTENT_AD_SCHEDULE))) {
			manualAdsLayout = findViewById(R.id.manualAdsLayout);
			manualAdsLayout.setVisibility(View.VISIBLE);
			buttonPreroll = findViewById(R.id.buttonRequestAdPreroll);
			buttonPostroll = findViewById(R.id.buttonRequestAdPostroll);
			buttonSkippable = findViewById(R.id.buttonRequestAdSkippable);
			View.OnClickListener adsClickListener = new View.OnClickListener() {
				@Override
				public void onClick(View v) {
					String adTag = null;
					if (v == buttonPreroll) {
						adTag = "https://pubads.g.doubleclick.net/gampad/ads?slotname=/124319096/external/ad_rule_samples&sz=640x480&ciu_szs=300x250&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&url=&unviewed_position_start=1&output=xml_vast3&impl=s&env=vp&gdfp_req=1&ad_rule=0&vad_type=linear&vpos=preroll&pod=1&ppos=1&lip=true&min_ad_duration=0&max_ad_duration=30000&vrid=6256&video_doc_id=short_onecue&cmsid=496&kfa=0&tfcd=0";
					} else if (v == buttonPostroll) {
						adTag = "https://pubads.g.doubleclick.net/gampad/ads?slotname=/124319096/external/ad_rule_samples&sz=640x480&ciu_szs=300x250&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&url=&unviewed_position_start=1&output=xml_vast3&impl=s&env=vp&gdfp_req=1&ad_rule=0&vad_type=linear&vpos=postroll&pod=3&ppos=1&lip=true&min_ad_duration=0&max_ad_duration=30000&vrid=6256&video_doc_id=short_onecue&cmsid=496&kfa=0&tfcd=0";
					} else if (v == buttonSkippable) {
						adTag = "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dskippablelinear&correlator=";
					}
					// Manually request ad
					final PlayerController pc = playerView.getPlayerController();
					pc.getAdInterface().scheduleAd(new ImaAdRequest.Builder()
						.tagUrl(adTag)
						//.scheduleDelayMs(4000)
						.get()
						.toAdRequest());
				}
			};
			buttonPreroll.setOnClickListener(adsClickListener);
			buttonPostroll.setOnClickListener(adsClickListener);
			buttonSkippable.setOnClickListener(adsClickListener);
		}
	}

	private void setupCustomAdsUI() {
		AdInterface adInterface = playerView.getPlayerController().getAdInterface();
		adUi = new AdUi(this);
		adUi.setApi(adInterface.getAdApi());
		playerView.addView(adUi);
	}

	/**
	 * @return item index in the playlist or {@code -1}
	 */
	private int findItemIndexInPlaylist(@NonNull Bundle item) {
		if (playbackPlaylist == null) {
			return -1;
		}
		int index = -1;
		for (int i = 0; i < playbackPlaylist.size() && index == -1; i++) {
			if (playbackPlaylist.get(i) == item) {
				index = i;
			}
		}
		return index;
	}

	private void reopenPlaylistItemByIndex(int index) {
		if (index >= 0 && playbackPlaylist != null && index < playbackPlaylist.size()) {
			playerView.getPlayerController().release();
			playbackBundle = playbackPlaylist.get(index);
			playerView.getPlayerController().open(playbackBundle);
		}
	}

	private void playPrevious() {
		if (playbackPlaylist != null) {
			int index = findItemIndexInPlaylist(playbackBundle);
			if (index != -1) {
				index--;
				if (index < 0 && wrapPlaylist) {
					index = playbackPlaylist.size() - 1;
				}
				reopenPlaylistItemByIndex(index);
			}
		}
	}

	private void playNext() {
		if (playbackPlaylist != null) {
			if (autoNextRunnable != null) {
				mainHandler.removeCallbacks(autoNextRunnable);
			}
			int index = findItemIndexInPlaylist(playbackBundle);
			if (index != -1) {
				index++;
				if (index > playbackPlaylist.size() - 1 && wrapPlaylist) {
					index = 0;
				}
				reopenPlaylistItemByIndex(index);
			}
			if (autoNextRunnable != null) {
				if (!waitForVideoFrame) {
					mainHandler.postDelayed(autoNextRunnable, getNextDelayMs());
				}
			}
		}
	}

	private void initializePreviousButton() {
		buttonPrevious.setVisibility(View.VISIBLE);
		buttonPrevious.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				if (isPlaylist()) {
					playPrevious();
				}
			}
		});
	}

	private void initializeNextButton() {
		buttonNext.setVisibility(View.VISIBLE);
		buttonNext.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				if (isPlaylist()) {
					playNext();
				}
			}
		});
	}

	private void initializePlayerListener() {
		playerListener = new AbstractPlayerListener() {
			@Nullable Snackbar snackbar;
			@NonNull final RetryCounter decryptionErrorCounter = new RetryCounter(
				2,
				RetryCounter.DEFAULT_BASE_DELAY_MS,
				RetryCounter.DEFAULT_BACKOFF_FACTOR_MS,
				RetryCounter.DEFAULT_FUZZY_FACTOR,
				RetryCounter.DEFAULT_MAX_DELAY_MS);

			@Override
			public void onStateChanged(@NonNull PlayerController.State state) {
				// Remove any errors on screen when the playback is starting / re-starting
				if (state == PlayerController.State.Preparing) {
					if (snackbar != null) {
						snackbar.dismiss();
					}
				} else if (state == PlayerController.State.Playing) {
					// Check if the thumbnails should be prepared.
					if (!thumbnailPrepared) {
						prepareThumbnailsPosition();
					}

					// Check if the thumbnail view should be hidden.
					if (hideThumbnailOnPlay) {
						hideThumbnailOnPlay = false;
						final ThumbsPlugin.ThumbnailViewComponent thumbsView = playerView.getComponent(
							ThumbsPlugin.ThumbnailViewComponent.class);
						if (thumbsView != null) {
							thumbsView.hide();
						}
					}
				} else if (state == PlayerController.State.Finished) {
					if (isPlaylist()) {
						playNext();
					}
				}
			}

			@Override
			public void onFatalErrorOccurred(@NonNull CastlabsPlayerException error) {
				// This callback is only invoked for fatal errors, before the controller
				// is released. This yields a chance to query it for status.
				//
				final PlayerController pc = playerView.getPlayerController();
				VideoTrackQuality videoQuality = pc.getVideoQuality();
				final long position = pc.getPosition();
				final long bufferTime = pc.getPreBufferTime();
				Log.e(TAG, "Fatal exception, position: " + TimeUtils.us2s(position)
					+ " seconds, buffer ahead: " + TimeUtils.us2s(bufferTime - position)
					+ " seconds, video quality: " + (videoQuality == null ? "None" : videoQuality.getBitrate()));
			}

			@Override
			public void onError(@NonNull CastlabsPlayerException error) {
				// Handle expired offline licenses.
				// Check if the error is either TYPE_KEY_EXPIRED or TYPE_VIDEO_DECRYPTION_ERROR with
				// the cause being MediaCodec.CryptoException.ERROR_NO_KEY. In the first
				// case the key is expired and we can safely decided to re-load and see if
				// we get an new key from the DRM system. The second case unfortunately has
				// more than one meaning. It could imply that no key was loaded for the
				// content, i.e. the _wrong_ key was used for instance due to a misalignment
				// with the offlineId in the keystore.
				// It could however _also_ mean that the user fiddled around with his device
				// time and the CDM loaded the license but refuses to use it. We can still
				// remove the key and load again, since the DRM response should
				// give us a license (or not) and we would have a definitive answer.
				boolean reloaded = false;
				if (error.getType() == CastlabsPlayerException.TYPE_KEY_EXPIRED) {
					reloaded = maybeDeleteLicenseAndReload();
				} else if ((error.getType() == CastlabsPlayerException.TYPE_VIDEO_DECRYPTION_ERROR ||
					error.getType() == CastlabsPlayerException.TYPE_AUDIO_DECRYPTION_ERROR) &&
					((MediaCodec.CryptoException) error.getCause()).getErrorCode() == MediaCodec.CryptoException.ERROR_NO_KEY) {
					if (decryptionErrorCounter.hasMoreAttempts()) {
						decryptionErrorCounter.attempt();
						reloaded = maybeDeleteLicenseAndReload();
					}
				}

				if (!reloaded) {
					String severityString = error.getErrorData().getString(CastlabsPlayerException.SEVERITY);
					final StringBuilder builder = new StringBuilder((severityString != null ? severityString : "Error") + ": ");
					if (error.getMessage() != null) {
						builder.append(error.getMessage());
						final String causeMessage = error.getCauseMessage();
						if (causeMessage != null) {
							builder.append(": ").append(causeMessage);
						}
						String traceId = CastlabsPlayerException.getDrmTodayTraceId(error);
						if (traceId != null) {
							Log.e(TAG, "DrmToday trace id: " + traceId);
						}
					}
					snackbar = Snackbar.make(findViewById(R.id.snack_bar), builder.toString(),
						error.getSeverity() == CastlabsPlayerException.SEVERITY_ERROR ?
							BaseTransientBottomBar.LENGTH_INDEFINITE : BaseTransientBottomBar.LENGTH_LONG);
					snackbar.show();
				}

				if (stopOnError && autoNextRunnable != null) {
					mainHandler.removeCallbacks(autoNextRunnable);
					playerView.getLifecycleDelegate().releasePlayer(false);
				}
			}

			boolean maybeDeleteLicenseAndReload() {
				final DrmConfiguration drmConfiguration = (DrmConfiguration) playbackBundle.get(SdkConsts.INTENT_DRM_CONFIGURATION);
				if (drmConfiguration == null) {
					return false;
				}
				KeyStore keyStore = PlayerController.getKeyStore();
				if (keyStore == null) {
					return false;
				}
				if (drmConfiguration.offlineId == null || !keyStore.delete(drmConfiguration.offlineId)) {
					return false;
				}
				Log.e(TAG, "DRM license key expired or not usable. Cleared key and reload");
				playerView.getPlayerController().open(playbackBundle);
				return true;
			}
		};
	}

	private boolean isPlaylist() {
		return getIntent().getExtras() != null && getIntent().getExtras().containsKey(MainActivity.INTENT_PLAYLIST);
	}

	private void initializePlayerControllerViewListener() {
		// Here we add some padding to move the subtitles view up a bit when
		// we are showing the controller.
		playerControllerViewListener = new PlayerControllerView.Listener() {
			@Override
			public void onVisibilityChanged(int visibility) {
				// This is how you can get a component, in this case the SubtitlesViewComponent
				// from the player view.
				SubtitlesViewComponent svc = playerView.getComponent(SubtitlesViewComponent.class);
				if (svc != null && svc.getView() != null) {
					int height = visibility == View.VISIBLE ? playerControllerView.getHeight() : 0;
					svc.getView().setPadding(0, 0, 0, height);
				}
			}
		};
	}

	private void initializeSeekBarListener() {
		seekBarListener = new PlayerControllerView.SeekBarListener() {
			@Override
			public void onSeekbarScrubbed(long positionUs, double seekBarProgressPercent) {
				// User interaction on the seekbar. Request Thumbnail rendering to the Thumbnail View
				ThumbsPlugin.ThumbnailViewComponent thumbsView = playerView.getComponent(
					ThumbsPlugin.ThumbnailViewComponent.class);
				if (thumbsView != null) {
					thumbsView.show(positionUs, thumbnailViewCallback, ThumbsPlugin.THUMBNAIL_INDEX_CLOSEST);
				}
			}

			@Override
			public void onSeekbarReleased() {
				hideThumbnailOnPlay = true;
				thumbnailTimeContainer.setVisibility(View.GONE);
			}
		};
	}

	private void initializePositionAdjuster() {
		positionAdjuster = new PlayerControllerView.PositionAdjuster() {
			@Override
			public long getAdjustedPositionUs(long positionUs) {
				ThumbnailProvider thumbnailProvider = playerView.getPlayerController()
					.getComponent(ThumbnailProvider.class);
				if (thumbnailProvider != null) {
					Long actualPositionUs = thumbnailProvider.getThumbnailPosition(
						positionUs,
						ThumbsPlugin.THUMBNAIL_INDEX_CLOSEST);
					if (actualPositionUs != null) {
						return actualPositionUs;
					} else {
						return positionUs;
					}
				} else {
					return positionUs;
				}
			}
		};
	}

	private void initializeThumbnailViewCallback() {
		thumbnailViewCallback = new DefaultThumbnailView.Callback() {
			@Override
			public void getThumbnailRect(Rect output, DefaultThumbnailView.ThumbnailInfo info, boolean isSmallScreen) {
				// Fill the output Rect with the coordinates where we want the Thumbnail to be displayed
				ViewGroup videoView = playerView.getVideoView();
				output.set(
					videoView.getLeft(),
					videoView.getTop(),
					videoView.getRight(),
					videoView.getBottom());
				thumbnailTimeContainer.setVisibility(View.VISIBLE);
				//noinspection SetTextI18n
				requestedTime.setText(info.getRequestedTimeUs() / 1e6f + "s");
				//noinspection SetTextI18n
				thumbnailTime.setText(info.getActualTimeUs() / 1e6f + "s");
			}
		};
	}

	private void initializeAdListener() {
		adListener = new AdInterface.Listener() {
			@Override
			public void onAdWillStart(@NonNull Ad ad) {

			}

			@Override
			public void onAdStarted(@NonNull Ad ad) {
				Log.d(TAG, "Ad started : " + ad.id + ", position = " + ad.position + ", duration: " + ad.durationMs);
				// 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 onAdError(Ad ad, CastlabsPlayerException exception) {

			}

			@Override
			public void onAdSkipped() {

			}

			@Override
			public void onAdPlaybackPositionChanged(long playbackPositionMs) {

			}

			@Override
			public void onAdPaused() {

			}

			@Override
			public void onAdResumed(Ad ad) {

			}

			@Override
			public void onAdClicked(String clickthroughUrl) {

			}
		};
	}

	private void initializeExternalSourceSelector() {
		externalSourceSelector = new ExternalSourceSelector() {
			@Nullable
			@Override
			public SourceData onError(@NonNull String manifestUrl, @NonNull Exception error) {
				// Have the fallback CDN only for one particular URL
				if (manifestUrl.equals("https://demo.cf.castlabs.com/media/QA/QA_BBB_single_4/Manifest.mpd")) {
					return new SourceData("https://demo.cf.castlabs.com/media/QA/QA_BBB_single_2/Manifest.mpd");
				}
				// No fallback CDN for other URLs
				return null;
			}

			@Nullable
			@Override
			public PlayerConfig onRestart(@NonNull String manifestUrl, @NonNull PlayerConfig playerConfig) {
				// Do the 'hard' restart for the selected URL when the 'soft' one fails
				if (manifestUrl.equals("https://demo.cf.castlabs.com/media/QA/QA_BBB_single_4/Manifest.mpd")) {
					// Simplify here and just update the currently failed player config with the fallback CDN URL
					// Usually, ensure that the DRM and other player config parameters are valid for the
					// returned player config or create a completely new player config
					return new PlayerConfig.Builder(playerConfig)
						.contentUrl("https://demo.cf.castlabs.com/media/QA/QA_BBB_single_2/Manifest.mpd")
						.get();
				}
				// No fallback CDN for other URLs
				return null;
			}
		};
	}

	private void initializePlaybackBundle(Bundle savedInstanceState) {
		if (savedInstanceState == null) {
			Log.d(TAG, "Opening playback from intent bundle");
			// This demo assumes that you send an intent to this Activity that contains the playback information.
			if (getIntent() != null) {
				Bundle bundle = getIntent().getExtras();
				if (bundle != null && bundle.containsKey(MainActivity.INTENT_PLAYLIST)) {
					Parcelable[] playlist = bundle.getParcelableArray(MainActivity.INTENT_PLAYLIST);
					if (playlist != null) {
						playbackPlaylist = new ArrayList<>();
						for (Parcelable item : playlist) {
							playbackPlaylist.add(((PlayerConfig)item).toBundle());
						}
						if (!playbackPlaylist.isEmpty()) {
							playbackBundle = playbackPlaylist.get(0);
						}
					}
				}

				if (playbackBundle == null) {
					playbackBundle = bundle;
				}
			} else {
				Snackbar.make(playerView, "No intent specified", Snackbar.LENGTH_INDEFINITE).show();
			}
		} else {
			Log.d(TAG, "Opening playback from saved state bundle");
			playbackBundle = savedInstanceState.getBundle(SAVED_PLAYBACK_STATE_BUNDLE_KEY);
		}
	}

	private void initializeConfigurationProvider() {
		configurationProvider = new ConfigurationProvider() {
			@Nullable
			@Override
			public DrmConfiguration getDrmConfiguration(@NonNull DrmConfiguration drmConfiguration) {
				// We just supply here the same DRM configuration to show how the DRM configuration
				// can be provided for the license renewal. If needed any other configuration can be created and returned here.
				// In case the DRM configuration stays the same over license renewals then this callback is not needed to be
				// installed and the same DRM configuration is used by default
				return drmConfiguration;
			}

			@Nullable
			public PlayerConfig getPlayerConfig(@NonNull PlayerConfig current) {
				return current;
			}
		};
	}

	private void openPlaybackBundle() {
		if (playbackBundle != null) {
			try {
				// Add the player listener
				playerView.getPlayerController().addPlayerListener(playerListener);
				// (Optional) Add ad events listener
				playerView.getPlayerController().getAdInterface().addAdListener(adListener);
				// (Optional) Install the CDN fallback implementation
				playerView.getPlayerController().setExternalSourceSelector(externalSourceSelector);
				// Show manual ad insertion layout if ad schedule is present
				setupManualAdsInsertion();
				// (Optional) It is only needed for Widevine automatic renewal
				if (DrmLicenseLoader.fetchLicense(playbackBundle, new MemoryKeyStore())) {
					playerView.getPlayerController().setConfigurationProvider(configurationProvider);
				}
				// Need to pass the bundle on to the PlayerController
				// to start playback. The open() method might throw an Exception in case the bundle
				// contains not all mandatory parameters or the parameters are malformed.
				playerView.getPlayerController().open(playbackBundle);
				// (Optional) Prepare thumbnails position in advance.
				prepareThumbnailsPosition();
			} catch (Exception e) {
				String message = "Error while opening player: " + e.getMessage();
				Log.e(TAG, message, e);
				showError(message);
			}
		} else {
			showError("Can not start playback: no bundle specified");
		}
	}

	private void prepareThumbnailsPosition() {
		ThumbnailProvider thumbnailProvider = playerView.getPlayerController()
			.getComponent(ThumbnailProvider.class);
		if (thumbnailProvider != null) {
			thumbnailPrepared = true;
			thumbnailProvider.getCuePointsList();
		}
	}

	private void showError(String message) {
		Snackbar.make(playerView, message, Snackbar.LENGTH_INDEFINITE).show();
	}
}
