package com.brentvatne.exoplayer;

import static androidx.media3.common.C.CONTENT_TYPE_DASH;
import static androidx.media3.common.C.CONTENT_TYPE_HLS;
import static androidx.media3.common.C.CONTENT_TYPE_OTHER;
import static androidx.media3.common.C.CONTENT_TYPE_RTSP;
import static androidx.media3.common.C.CONTENT_TYPE_SS;
import static androidx.media3.common.C.TIME_END_OF_SOURCE;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.accessibility.CaptioningManager;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Metadata;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player;
import androidx.media3.common.StreamKey;
import androidx.media3.common.Timeline;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackSelectionOverride;
import androidx.media3.common.Tracks;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DataSpec;
import androidx.media3.datasource.HttpDataSource;
import androidx.media3.exoplayer.DefaultLoadControl;
import androidx.media3.exoplayer.DefaultRenderersFactory;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.dash.DashMediaSource;
import androidx.media3.exoplayer.dash.DashUtil;
import androidx.media3.exoplayer.dash.DefaultDashChunkSource;
import androidx.media3.exoplayer.dash.manifest.AdaptationSet;
import androidx.media3.exoplayer.dash.manifest.DashManifest;
import androidx.media3.exoplayer.dash.manifest.Period;
import androidx.media3.exoplayer.dash.manifest.Representation;
import androidx.media3.exoplayer.drm.DefaultDrmSessionManager;
import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider;
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
import androidx.media3.exoplayer.drm.DrmSessionManager;
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider;
import androidx.media3.exoplayer.drm.FrameworkMediaDrm;
import androidx.media3.exoplayer.drm.HttpMediaDrmCallback;
import androidx.media3.exoplayer.drm.UnsupportedDrmException;
import androidx.media3.exoplayer.hls.HlsMediaSource;
import androidx.media3.exoplayer.ima.ImaAdsLoader;
import androidx.media3.exoplayer.mediacodec.MediaCodecInfo;
import androidx.media3.exoplayer.mediacodec.MediaCodecUtil;
import androidx.media3.exoplayer.rtsp.RtspMediaSource;
import androidx.media3.exoplayer.smoothstreaming.DefaultSsChunkSource;
import androidx.media3.exoplayer.smoothstreaming.SsMediaSource;
import androidx.media3.exoplayer.source.ClippingMediaSource;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.MergingMediaSource;
import androidx.media3.exoplayer.source.ProgressiveMediaSource;
import androidx.media3.exoplayer.source.SingleSampleMediaSource;
import androidx.media3.exoplayer.source.TrackGroupArray;
import androidx.media3.exoplayer.source.ads.AdsMediaSource;
import androidx.media3.exoplayer.trackselection.AdaptiveTrackSelection;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.trackselection.MappingTrackSelector;
import androidx.media3.exoplayer.trackselection.TrackSelection;
import androidx.media3.exoplayer.trackselection.TrackSelectionArray;
import androidx.media3.exoplayer.upstream.BandwidthMeter;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.DefaultAllocator;
import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter;
import androidx.media3.exoplayer.util.EventLogger;
import androidx.media3.extractor.metadata.emsg.EventMessage;
import androidx.media3.extractor.metadata.id3.Id3Frame;
import androidx.media3.extractor.metadata.id3.TextInformationFrame;
import androidx.media3.session.MediaSessionService;
import androidx.media3.ui.LegacyPlayerControlView;

import com.brentvatne.common.api.AdsProps;
import com.brentvatne.common.api.BufferConfig;
import com.brentvatne.common.api.BufferingStrategy;
import com.brentvatne.common.api.ControlsConfig;
import com.brentvatne.common.api.DRMProps;
import com.brentvatne.common.api.ResizeMode;
import com.brentvatne.common.api.SideLoadedTextTrack;
import com.brentvatne.common.api.Source;
import com.brentvatne.common.api.SubtitleStyle;
import com.brentvatne.common.api.TimedMetadata;
import com.brentvatne.common.api.Track;
import com.brentvatne.common.api.VideoTrack;
import com.brentvatne.common.react.VideoEventEmitter;
import com.brentvatne.common.toolbox.DebugLog;
import com.brentvatne.common.toolbox.ReactBridgeUtils;
import com.brentvatne.react.BuildConfig;
import com.brentvatne.react.R;
import com.brentvatne.react.ReactNativeVideoManager;
import com.brentvatne.receiver.AudioBecomingNoisyReceiver;
import com.brentvatne.receiver.BecomingNoisyListener;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.uimanager.ThemedReactContext;
import com.google.ads.interactivemedia.v3.api.AdError;
import com.google.ads.interactivemedia.v3.api.AdErrorEvent;
import com.google.ads.interactivemedia.v3.api.AdEvent;
import com.google.ads.interactivemedia.v3.api.ImaSdkFactory;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
import com.google.common.collect.ImmutableList;

import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

@SuppressLint("ViewConstructor")
public class ReactExoplayerView extends FrameLayout implements
        LifecycleEventListener,
        Player.Listener,
        BandwidthMeter.EventListener,
        BecomingNoisyListener,
        DrmSessionEventListener,
        AdEvent.AdEventListener,
        AdErrorEvent.AdErrorListener {

    public static final double DEFAULT_MAX_HEAP_ALLOCATION_PERCENT = 1;
    public static final double DEFAULT_MIN_BUFFER_MEMORY_RESERVE = 0;

    private static final String TAG = "ReactExoplayerView";

    private static final CookieManager DEFAULT_COOKIE_MANAGER;
    private static final int SHOW_PROGRESS = 1;

    static {
        DEFAULT_COOKIE_MANAGER = new CookieManager();
        DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
    }

    protected final VideoEventEmitter eventEmitter;
    private final ReactExoplayerConfig config;
    private final DefaultBandwidthMeter bandwidthMeter;
    private LegacyPlayerControlView playerControlView;
    private View playPauseControlContainer;
    private Player.Listener eventListener;

    private ExoPlayerView exoPlayerView;
    private FullScreenPlayerView fullScreenPlayerView;
    private ImaAdsLoader adsLoader;

    private DataSource.Factory mediaDataSourceFactory;
    private ExoPlayer player;
    private DefaultTrackSelector trackSelector;
    private boolean playerNeedsSource;
    private ServiceConnection playbackServiceConnection;
    private PlaybackServiceBinder playbackServiceBinder;

    // logger to be enable by props
    private EventLogger debugEventLogger = null;
    private boolean enableDebug = false;
    private static final String TAG_EVENT_LOGGER = "RNVExoplayer";

    private int resumeWindow;
    private long resumePosition;
    private boolean loadVideoStarted;
    private boolean isFullscreen;
    private boolean isInBackground;
    private boolean isPaused;
    private boolean isBuffering;
    private boolean muted = false;
    private boolean hasAudioFocus = false;
    private float rate = 1f;
    private AudioOutput audioOutput = AudioOutput.SPEAKER;
    private float audioVolume = 1f;
    private int minLoadRetryCount = 3;
    private BufferConfig bufferConfig = new BufferConfig();
    private int maxBitRate = 0;
    private boolean hasDrmFailed = false;
    private boolean isUsingContentResolution = false;
    private boolean selectTrackWhenReady = false;
    private final Handler mainHandler;
    private Runnable mainRunnable;
    private boolean useCache = false;
    private ControlsConfig controlsConfig = new ControlsConfig();

    /*
    * When user is seeking first called is on onPositionDiscontinuity -> DISCONTINUITY_REASON_SEEK
    * Then we set if to false when playback is back in onIsPlayingChanged -> true
    */
    private boolean isSeeking = false;
    private long seekPosition = -1;
    private boolean isSeekInProgress = false;

    // Props from React
    private Source source = new Source();
    private boolean repeat;
    private String audioTrackType;
    private String audioTrackValue;
    private String videoTrackType;
    private String videoTrackValue;
    private String textTrackType = "disabled";
    private String textTrackValue;
    private boolean disableFocus;
    private boolean focusable = true;
    private BufferingStrategy.BufferingStrategyEnum bufferingStrategy;
    private boolean disableDisconnectError;
    private boolean preventsDisplaySleepDuringVideoPlayback = true;
    private float mProgressUpdateInterval = 250.0f;
    private boolean playInBackground = false;
    private boolean mReportBandwidth = false;
    private boolean controls;

    private boolean showNotificationControls = false;
    // \ End props

    // React
    private final ThemedReactContext themedReactContext;
    private final AudioManager audioManager;
    private final AudioBecomingNoisyReceiver audioBecomingNoisyReceiver;
    private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;

    // store last progress event values to avoid sending unnecessary messages
    private long lastPos = -1;
    private long lastBufferDuration = -1;
    private long lastDuration = -1;

    private boolean viewHasDropped = false;
    private int selectedSpeedIndex = 1; // Default is 1.0x

    private final String instanceId = String.valueOf(UUID.randomUUID());

    private CmcdConfiguration.Factory cmcdConfigurationFactory;

    public void setCmcdConfigurationFactory(CmcdConfiguration.Factory factory) {
        this.cmcdConfigurationFactory = factory;
    }

    private void updateProgress() {
        if (player != null) {
            if (playerControlView != null && isPlayingAd() && controls) {
                playerControlView.hide();
            }
            long bufferedDuration = player.getBufferedPercentage() * player.getDuration() / 100;
            long duration = player.getDuration();
            long pos = player.getCurrentPosition();
            if (pos > duration) {
                pos = duration;
            }

            if (lastPos != pos
                    || lastBufferDuration != bufferedDuration
                    || lastDuration != duration) {
                lastPos = pos;
                lastBufferDuration = bufferedDuration;
                lastDuration = duration;
                eventEmitter.onVideoProgress.invoke(pos, bufferedDuration, player.getDuration(), getPositionInFirstPeriodMsForCurrentWindow(pos));
            }
        }
    }

    private final Handler progressHandler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == SHOW_PROGRESS) {
                updateProgress();
                msg = obtainMessage(SHOW_PROGRESS);
                sendMessageDelayed(msg, Math.round(mProgressUpdateInterval));
            }
        }
    };

    private void handleSeekCompletion() {
        if (player != null && player.getPlaybackState() == Player.STATE_READY && isSeekInProgress) {
            Log.d("ReactExoplayerView", "handleSeekCompletion: currentPosition=" + player.getCurrentPosition());
            eventEmitter.onVideoSeekComplete.invoke(player.getCurrentPosition());
            isSeeking = false;
            seekPosition = -1;
            isSeekInProgress = false;
        }
    }

    public double getPositionInFirstPeriodMsForCurrentWindow(long currentPosition) {
        Timeline.Window window = new Timeline.Window();
        if(!player.getCurrentTimeline().isEmpty()) {
            player.getCurrentTimeline().getWindow(player.getCurrentMediaItemIndex(), window);
        }
        return window.windowStartTimeMs + currentPosition;
    }

    public ReactExoplayerView(ThemedReactContext context, ReactExoplayerConfig config) {
        super(context);
        this.themedReactContext = context;
        this.eventEmitter = new VideoEventEmitter();
        this.config = config;
        this.bandwidthMeter = config.getBandwidthMeter();
        mainHandler = new Handler();
        createViews();

        audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        themedReactContext.addLifecycleEventListener(this);
        audioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(themedReactContext);
        audioFocusChangeListener = new OnAudioFocusChangedListener(this, themedReactContext);
    }

    private boolean isPlayingAd() {
        return player != null && player.isPlayingAd();
    }

    private void createViews() {
        if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
            CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
        }

        LayoutParams layoutParams = new LayoutParams(
                LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT);
        exoPlayerView = new ExoPlayerView(getContext());
        exoPlayerView.setLayoutParams(layoutParams);
        addView(exoPlayerView, 0, layoutParams);

        exoPlayerView.setFocusable(this.focusable);
    }

    // LifecycleEventListener implementation
    @Override
    public void onHostResume() {
        if (!playInBackground || !isInBackground) {
            setPlayWhenReady(!isPaused);
        }
        isInBackground = false;
    }

    @Override
    public void onHostPause() {
        isInBackground = true;
        if (playInBackground) {
            return;
        }
        setPlayWhenReady(false);
    }

    @Override
    public void onHostDestroy() {
        cleanUpResources();
    }

    @Override
    protected void onDetachedFromWindow() {
        cleanupPlaybackService();
        super.onDetachedFromWindow();
    }

    public void cleanUpResources() {
        stopPlayback();
        themedReactContext.removeLifecycleEventListener(this);
        releasePlayer();
        viewHasDropped = true;
    }

    //BandwidthMeter.EventListener implementation
    @Override
    public void onBandwidthSample(int elapsedMs, long bytes, long bitrate) {
        if (mReportBandwidth) {
            if (player == null) {
                eventEmitter.onVideoBandwidthUpdate.invoke(bitrate, 0, 0, null);
            } else {
                Format videoFormat = player.getVideoFormat();
                boolean isRotatedContent = videoFormat != null && (videoFormat.rotationDegrees == 90 || videoFormat.rotationDegrees == 270);
                int width = videoFormat != null ? (isRotatedContent ? videoFormat.height : videoFormat.width) : 0;
                int height = videoFormat != null ? (isRotatedContent ? videoFormat.width : videoFormat.height) : 0;
                String trackId = videoFormat != null ? videoFormat.id : null;
                eventEmitter.onVideoBandwidthUpdate.invoke(bitrate, height, width, trackId);
            }
        }
    }

    // Internal methods

    /**
     * Toggling the visibility of the player control view
     */
    private void togglePlayerControlVisibility() {
        if (player == null) return;
        reLayoutControls();
        if (playerControlView.isVisible()) {
            playerControlView.hide();
        } else {
            playerControlView.show();
        }
    }

    /**
     * Initializing Player control
     */
    private void initializePlayerControl() {
        if (playerControlView == null) {
            playerControlView = new LegacyPlayerControlView(getContext());
            playerControlView.addVisibilityListener(new LegacyPlayerControlView.VisibilityListener() {
                @Override
                public void onVisibilityChange(int visibility) {
                    eventEmitter.onControlsVisibilityChange.invoke(visibility == View.VISIBLE);
                }
            });
        }

        // Setting the player for the playerControlView
        playerControlView.setPlayer(player);
        playPauseControlContainer = playerControlView.findViewById(R.id.exo_play_pause_container);

        // Invoking onClick event for exoplayerView
        exoPlayerView.setOnClickListener((View v) -> {
            if (!isPlayingAd()) {
                togglePlayerControlVisibility();
            }
        });

        //Handling the playButton click event
        ImageButton playButton = playerControlView.findViewById(R.id.exo_play);

        playButton.setOnClickListener((View v) -> {
            if (player != null && player.getPlaybackState() == Player.STATE_ENDED) {
                player.seekTo(0);
            }
            setPausedModifier(false);
        });

        //Handling the rewind and forward button click events
        ImageButton exoRewind = playerControlView.findViewById(R.id.exo_rew);
        ImageButton exoForward = playerControlView.findViewById(R.id.exo_ffwd);
        exoRewind.setOnClickListener((View v) -> {
            seekTo(player.getCurrentPosition() - controlsConfig.getSeekIncrementMS());
        });

        exoForward.setOnClickListener((View v) -> {
            seekTo(player.getCurrentPosition() + controlsConfig.getSeekIncrementMS());
        });

        //Handling the pauseButton click event
        ImageButton pauseButton = playerControlView.findViewById(R.id.exo_pause);
        pauseButton.setOnClickListener((View v) ->
                setPausedModifier(true)
        );

        //Handling the settingButton click event
        final ImageButton settingButton = playerControlView.findViewById(R.id.exo_settings);
        settingButton.setOnClickListener(v -> openSettings());

        //Handling the fullScreenButton click event
        final ImageButton fullScreenButton = playerControlView.findViewById(R.id.exo_fullscreen);
        fullScreenButton.setOnClickListener(v -> setFullscreen(!isFullscreen));
        updateFullScreenButtonVisibility();
        refreshControlsStyles();

        // Invoking onPlaybackStateChanged and onPlayWhenReadyChanged events for Player
        eventListener = new Player.Listener() {
            @Override
            public void onPlaybackStateChanged(int playbackState) {
                View playButton = playerControlView.findViewById(R.id.exo_play);
                View pauseButton = playerControlView.findViewById(R.id.exo_pause);
                if (playButton != null && playButton.getVisibility() == GONE) {
                    playButton.setVisibility(INVISIBLE);
                }
                if (pauseButton != null && pauseButton.getVisibility() == GONE) {
                    pauseButton.setVisibility(INVISIBLE);
                }

                reLayout(playPauseControlContainer);
                //Remove this eventListener once its executed. since UI will work fine once after the reLayout is done
                player.removeListener(eventListener);
            }

            @Override
            public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
                reLayout(playPauseControlContainer);
                //Remove this eventListener once its executed. since UI will work fine once after the reLayout is done
                player.removeListener(eventListener);
            }
        };
        player.addListener(eventListener);
    }
    private void openSettings() {
        AlertDialog.Builder builder = new AlertDialog.Builder(themedReactContext);
        builder.setTitle(R.string.settings);
        String[] settingsOptions = {themedReactContext.getString(R.string.playback_speed)};
        builder.setItems(settingsOptions, (dialog, which) -> {
            if (which == 0) {
                showPlaybackSpeedOptions();
            }
        });
        builder.show();
    }

    private void showPlaybackSpeedOptions() {
        String[] speedOptions = {"0.5x", "1.0x", "1.5x", "2.0x"};
        AlertDialog.Builder builder = new AlertDialog.Builder(themedReactContext);
        builder.setTitle(R.string.select_playback_speed);

        builder.setSingleChoiceItems(speedOptions, selectedSpeedIndex, (dialog, which) -> {
            selectedSpeedIndex = which;
            float speed;
            switch (which) {
                case 0:
                    speed = 0.5f;
                    break;
                case 1:
                    speed = 1.0f;
                    break;
                case 2:
                    speed = 1.5f;
                    break;
                default:
                    speed = 1.0f;
                    break;
            }
            setRateModifier(speed);
        });
        builder.show();
    }

    /**
     * Adding Player control to the frame layout
     */
    private void addPlayerControl() {
        if (playerControlView == null) return;
        LayoutParams layoutParams = new LayoutParams(
                LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT);
        playerControlView.setLayoutParams(layoutParams);
        int indexOfPC = indexOfChild(playerControlView);
        if (indexOfPC != -1) {
            removeViewAt(indexOfPC);
        }
        addView(playerControlView, 1, layoutParams);
        reLayout(playerControlView);
    }

    /**
     * Update the layout
     * @param view  view needs to update layout
     *
     * This is a workaround for the open bug in react-native: <a href="https://github.com/facebook/react-native/issues/17968">...</a>
     */
    private void reLayout(View view) {
        if (view == null) return;
        view.measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
        view.layout(view.getLeft(), view.getTop(), view.getMeasuredWidth(), view.getMeasuredHeight());
    }

    private void refreshControlsStyles() {
        if (playerControlView == null || player == null || !controls) return;
        updateLiveContent();
        updatePlayPauseButtons();
        updateButtonVisibility(controlsConfig.getHideForward(), R.id.exo_ffwd);
        updateButtonVisibility(controlsConfig.getHideRewind(), R.id.exo_rew);
        updateButtonVisibility(controlsConfig.getHideNext(), R.id.exo_next);
        updateButtonVisibility(controlsConfig.getHidePrevious(), R.id.exo_prev);
        updateViewVisibility(playerControlView.findViewById(R.id.exo_fullscreen), controlsConfig.getHideFullscreen(), GONE);
        updateViewVisibility(playerControlView.findViewById(R.id.exo_position), controlsConfig.getHidePosition(), GONE);
        updateViewVisibility(playerControlView.findViewById(R.id.exo_progress), controlsConfig.getHideSeekBar(), INVISIBLE);
        updateViewVisibility(playerControlView.findViewById(R.id.exo_duration), controlsConfig.getHideDuration(), GONE);
        updateViewVisibility(playerControlView.findViewById(R.id.exo_settings), controlsConfig.getHideSettingButton(), GONE );
    }

    private void updateLiveContent() {
        LinearLayout exoLiveContainer = playerControlView.findViewById(R.id.exo_live_container);
        TextView exoLiveLabel = playerControlView.findViewById(R.id.exo_live_label);

        boolean isLive = false;
        Timeline timeline = player.getCurrentTimeline();

        // Determine if the content is live
        if (!timeline.isEmpty()) {
            Timeline.Window window = new Timeline.Window();
            timeline.getWindow(player.getCurrentMediaItemIndex(), window);
            isLive = window.isLive();
        }

        if (isLive && controlsConfig.getLiveLabel() != null) {
            exoLiveLabel.setText(controlsConfig.getLiveLabel());
            exoLiveContainer.setVisibility(VISIBLE);
        } else {
            exoLiveContainer.setVisibility(GONE);
        }
    }

    private void updatePlayPauseButtons() {
        final ImageButton playButton = playerControlView.findViewById(R.id.exo_play);
        final ImageButton pauseButton = playerControlView.findViewById(R.id.exo_pause);

        if (controlsConfig.getHidePlayPause()) {
            playPauseControlContainer.setAlpha(0);
            playButton.setClickable(false);
            pauseButton.setClickable(false);
        } else {
            playPauseControlContainer.setAlpha(1.0f);
            playButton.setClickable(true);
            pauseButton.setClickable(true);
        }
    }

    private void updateButtonVisibility(boolean hide, int buttonID) {
        ImageButton button = playerControlView.findViewById(buttonID);
        if (hide) {
            button.setImageAlpha(0);
            button.setClickable(false);
        } else {
            button.setImageAlpha(255);
            button.setClickable(true);
        }
    }

    private void updateViewVisibility(View view, boolean hide, int hideVisibility) {
        if (hide) {
            view.setVisibility(hideVisibility);
        } else if (view.getVisibility() == hideVisibility) {
            view.setVisibility(VISIBLE);
        }
    }



    private void reLayoutControls() {
        reLayout(exoPlayerView);
        reLayout(playerControlView);
    }

    /// returns true is adaptive bitrate shall be used
    public boolean isUsingVideoABR() {
        return videoTrackType == null || "auto".equals(videoTrackType);
    }

    public void setDebug(boolean enableDebug) {
        this.enableDebug = enableDebug;
        refreshDebugState();
    }

    private void refreshDebugState() {
        if (player == null) {
            return;
        }
        if (enableDebug) {
            debugEventLogger = new EventLogger(TAG_EVENT_LOGGER);
            player.addAnalyticsListener(debugEventLogger);
        } else if (debugEventLogger != null) {
            player.removeAnalyticsListener(debugEventLogger);
            debugEventLogger = null;
        }
    }

    public void setViewType(int viewType) {
        exoPlayerView.updateSurfaceView(viewType);
    }

    private class RNVLoadControl extends DefaultLoadControl {
        private final int availableHeapInBytes;
        private final Runtime runtime;
        public RNVLoadControl(DefaultAllocator allocator, BufferConfig config) {
            super(allocator,
                    config.getMinBufferMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt()
                            ? config.getMinBufferMs()
                            : DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
                    config.getMaxBufferMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt()
                            ? config.getMaxBufferMs()
                            : DefaultLoadControl.DEFAULT_MAX_BUFFER_MS,
                    config.getBufferForPlaybackMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt()
                            ? config.getBufferForPlaybackMs()
                            : DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS ,
                    config.getBufferForPlaybackAfterRebufferMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt()
                            ? config.getBufferForPlaybackAfterRebufferMs()
                            : DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS,
                    -1,
                    true,
                    config.getBackBufferDurationMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt()
                            ? config.getBackBufferDurationMs()
                            : DefaultLoadControl.DEFAULT_BACK_BUFFER_DURATION_MS,
                    DefaultLoadControl.DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME);
            runtime = Runtime.getRuntime();
            ActivityManager activityManager = (ActivityManager) themedReactContext.getSystemService(ThemedReactContext.ACTIVITY_SERVICE);
            double maxHeap = config.getMaxHeapAllocationPercent() != BufferConfig.Companion.getBufferConfigPropUnsetDouble()
                    ? bufferConfig.getMaxHeapAllocationPercent()
                    : DEFAULT_MAX_HEAP_ALLOCATION_PERCENT;
            availableHeapInBytes = (int) Math.floor(activityManager.getMemoryClass() * maxHeap * 1024 * 1024);
        }

        @Override
        public boolean shouldContinueLoading(long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) {
            if (bufferingStrategy == BufferingStrategy.BufferingStrategyEnum.DisableBuffering) {
                return false;
            } else if (bufferingStrategy == BufferingStrategy.BufferingStrategyEnum.DependingOnMemory) {
                // The goal of this algorithm is to pause video loading (increasing the buffer)
                // when available memory on device become low.
                int loadedBytes = getAllocator().getTotalBytesAllocated();
                boolean isHeapReached = availableHeapInBytes > 0 && loadedBytes >= availableHeapInBytes;
                if (isHeapReached) {
                    return false;
                }
                long usedMemory = runtime.totalMemory() - runtime.freeMemory();
                long freeMemory = runtime.maxMemory() - usedMemory;
                double minBufferMemoryReservePercent = bufferConfig.getMinBufferMemoryReservePercent() != BufferConfig.Companion.getBufferConfigPropUnsetDouble()
                        ? bufferConfig.getMinBufferMemoryReservePercent()
                        : ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE;
                long reserveMemory = (long) minBufferMemoryReservePercent * runtime.maxMemory();
                long bufferedMs = bufferedDurationUs / (long) 1000;
                if (reserveMemory > freeMemory && bufferedMs > 2000) {
                    // We don't have enough memory in reserve so we stop buffering to allow other components to use it instead
                    return false;
                }
                if (runtime.freeMemory() == 0) {
                    DebugLog.w(TAG, "Free memory reached 0, forcing garbage collection");
                    runtime.gc();
                    return false;
                }
            }
            // "default" case or normal case for "DependingOnMemory"
            return super.shouldContinueLoading(playbackPositionUs, bufferedDurationUs, playbackSpeed);
        }
    }

    private void initializePlayer() {
        ReactExoplayerView self = this;
        Activity activity = themedReactContext.getCurrentActivity();
        // This ensures all props have been settled, to avoid async racing conditions.
        Source runningSource = source;
        mainRunnable = () -> {
            if (viewHasDropped && runningSource == source) {
                return;
            }
            try {
                if (runningSource.getUri() == null) {
                    return;
                }
                if (player == null) {
                    // Initialize core configuration and listeners
                    initializePlayerCore(self);
                }
                if (playerNeedsSource) {
                    // Will force display of shutter view if needed
                    exoPlayerView.updateShutterViewVisibility();
                    exoPlayerView.invalidateAspectRatio();
                    // DRM session manager creation must be done on a different thread to prevent crashes so we start a new thread
                    ExecutorService es = Executors.newSingleThreadExecutor();
                    es.execute(() -> {
                        // DRM initialization must run on a different thread
                        if (viewHasDropped && runningSource == source) {
                            return;
                        }
                        if (activity == null) {
                            DebugLog.e(TAG, "Failed to initialize Player!, null activity");
                            eventEmitter.onVideoError.invoke("Failed to initialize Player!", new Exception("Current Activity is null!"), "1001");
                            return;
                        }

                        // Initialize handler to run on the main thread
                        activity.runOnUiThread(() -> {
                            if (viewHasDropped && runningSource == source) {
                                return;
                            }
                            try {
                                // Source initialization must run on the main thread
                                initializePlayerSource(runningSource);
                            } catch (Exception ex) {
                                self.playerNeedsSource = true;
                                DebugLog.e(TAG, "Failed to initialize Player! 1");
                                DebugLog.e(TAG, ex.toString());
                                ex.printStackTrace();
                                eventEmitter.onVideoError.invoke(ex.toString(), ex, "1001");
                            }
                        });
                    });
                } else if (runningSource == source) {
                    initializePlayerSource(runningSource);
                }
            } catch (Exception ex) {
                self.playerNeedsSource = true;
                DebugLog.e(TAG, "Failed to initialize Player! 2");
                DebugLog.e(TAG, ex.toString());
                ex.printStackTrace();
                eventEmitter.onVideoError.invoke(ex.toString(), ex, "1001");
            }
        };
        mainHandler.postDelayed(mainRunnable, 1);
    }

    public void getCurrentPosition(Promise promise) {
        if (player != null) {
            float currentPosition = player.getCurrentPosition() / 1000.0f;
            promise.resolve(currentPosition);
        } else {
            promise.reject("PLAYER_NOT_AVAILABLE", "Player is not initialized.");
        }
    }

    private void initializePlayerCore(ReactExoplayerView self) {
        ExoTrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory();
        self.trackSelector = new DefaultTrackSelector(getContext(), videoTrackSelectionFactory);
        self.trackSelector.setParameters(trackSelector.buildUponParameters()
                .setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate));

        DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
        RNVLoadControl loadControl = new RNVLoadControl(
                allocator,
                bufferConfig
        );
        DefaultRenderersFactory renderersFactory =
                new DefaultRenderersFactory(getContext())
                        .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF)
                        .setEnableDecoderFallback(true)
                        .forceEnableMediaCodecAsynchronousQueueing();

        DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory);
        if (useCache) {
            mediaSourceFactory.setDataSourceFactory(RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true)));
        }

        if (BuildConfig.USE_EXOPLAYER_IMA) {
            AdsProps adProps = source.getAdsProps();

            // Create an AdsLoader.
            ImaAdsLoader.Builder imaLoaderBuilder = new ImaAdsLoader
                    .Builder(themedReactContext)
                    .setAdEventListener(this)
                    .setAdErrorListener(this);

            if (adProps != null && adProps.getAdLanguage() != null) {
                ImaSdkSettings imaSdkSettings = ImaSdkFactory.getInstance().createImaSdkSettings();
                imaSdkSettings.setLanguage(adProps.getAdLanguage());
                imaLoaderBuilder.setImaSdkSettings(imaSdkSettings);
            }
            adsLoader = imaLoaderBuilder.build();
        }

        mediaSourceFactory.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView);

        player = new ExoPlayer.Builder(getContext(), renderersFactory)
                .setTrackSelector(self.trackSelector)
                .setBandwidthMeter(bandwidthMeter)
                .setLoadControl(loadControl)
                .setMediaSourceFactory(mediaSourceFactory)
            .build();
        player.addListener(self);
        ReactNativeVideoManager.Companion.getInstance().onInstanceCreated(instanceId, player);
        refreshDebugState();
        player.addListener(self);
        player.setVolume(muted ? 0.f : audioVolume * 1);
        exoPlayerView.setPlayer(player);

        if (adsLoader != null) {
            adsLoader.setPlayer(player);
        }
        audioBecomingNoisyReceiver.setListener(self);
        bandwidthMeter.addEventListener(new Handler(), self);
        setPlayWhenReady(!isPaused);
        playerNeedsSource = true;

        PlaybackParameters params = new PlaybackParameters(rate, 1f);
        player.setPlaybackParameters(params);
        changeAudioOutput(this.audioOutput);

        if(showNotificationControls) {
            setupPlaybackService();
        }
    }

    private DrmSessionManager initializePlayerDrm() {
        DrmSessionManager drmSessionManager = null;
        DRMProps drmProps = source.getDrmProps();
        // need to realign UUID in DRM Props from source
        if (drmProps != null && drmProps.getDrmType() != null) {
            UUID uuid = Util.getDrmUuid(drmProps.getDrmType());
            if (uuid != null) {
                try {
                    DebugLog.w(TAG, "drm buildDrmSessionManager");
                    drmSessionManager = buildDrmSessionManager(uuid, drmProps);
                } catch (UnsupportedDrmException e) {
                    int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported
                            : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
                            ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown);
                    eventEmitter.onVideoError.invoke(getResources().getString(errorStringId), e, "3003");
                        }
            }
        }
        return drmSessionManager;
    }

    private void initializePlayerSource(Source runningSource) {
        if (runningSource.getUri() == null) {
            return;
        }
        /// init DRM
        DrmSessionManager drmSessionManager = initializePlayerDrm();
        if (drmSessionManager == null && runningSource.getDrmProps() != null && runningSource.getDrmProps().getDrmType() != null) {
            // Failed to initialize DRM session manager - cannot continue
            DebugLog.e(TAG, "Failed to initialize DRM Session Manager Framework!");
            return;
        }
        // init source to manage ads and external text tracks
        MediaSource subtitlesSource = buildTextSource();
        MediaSource videoSource = buildMediaSource(runningSource.getUri(), runningSource.getExtension(), drmSessionManager, runningSource.getCropStartMs(), runningSource.getCropEndMs());
        MediaSource mediaSourceWithAds = null;
        Uri adTagUrl = null;
        if (source.getAdsProps() != null) {
            adTagUrl = source.getAdsProps().getAdTagUrl();
        }
        if (adTagUrl != null && adsLoader != null) {
            DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory)
                    .setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView);
            DataSpec adTagDataSpec = new DataSpec(adTagUrl);
            DebugLog.w(TAG, "ads " + adTagUrl);
            mediaSourceWithAds = new AdsMediaSource(videoSource, adTagDataSpec, ImmutableList.of(runningSource.getUri(), adTagUrl), mediaSourceFactory, adsLoader, exoPlayerView);
            exoPlayerView.showAds();
        }
        MediaSource mediaSource;
        if (subtitlesSource == null) {
            mediaSource = Objects.requireNonNullElse(mediaSourceWithAds, videoSource);
        } else {
            ArrayList<MediaSource> mediaSourceList = new ArrayList<>();
            mediaSourceList.add(subtitlesSource);
            mediaSourceList.add(0, Objects.requireNonNullElse(mediaSourceWithAds, videoSource));
            MediaSource[] mediaSourceArray = mediaSourceList.toArray(
                    new MediaSource[mediaSourceList.size()]
            );

            mediaSource = new MergingMediaSource(mediaSourceArray);
        }

        // wait for player to be set
        while (player == null) {
            try {
                wait();
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
                DebugLog.e(TAG, ex.toString());
            }
        }

        boolean haveResumePosition = resumeWindow != C.INDEX_UNSET;
        if (haveResumePosition) {
            player.seekTo(resumeWindow, resumePosition);
            player.setMediaSource(mediaSource, false);
        } else if (runningSource.getStartPositionMs() > 0) {
            player.setMediaSource(mediaSource, runningSource.getStartPositionMs());
        } else {
            player.setMediaSource(mediaSource, true);
        }
        player.prepare();
        playerNeedsSource = false;

        reLayoutControls();

        eventEmitter.onVideoLoadStart.invoke();
        loadVideoStarted = true;

        finishPlayerInitialization();
    }

    private void finishPlayerInitialization() {
        // Initializing the playerControlView
        initializePlayerControl();
        setControls(controls);
        applyModifiers();
    }

    private void setupPlaybackService() {
        if (!showNotificationControls || player == null) {
            return;
        }

        playbackServiceConnection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                playbackServiceBinder = (PlaybackServiceBinder) service;

                try {
                    Activity currentActivity = themedReactContext.getCurrentActivity();
                    if (currentActivity != null) {
                        playbackServiceBinder.getService().registerPlayer(player,
                                (Class<Activity>) currentActivity.getClass());
                    } else {
                        // Handle the case where currentActivity is null
                        DebugLog.w(TAG, "Could not register ExoPlayer: currentActivity is null");
                    }
                } catch (Exception e) {
                    DebugLog.e(TAG, "Could not register ExoPlayer: " + e.getMessage());
                }
            }

            @Override
            public void onServiceDisconnected(ComponentName name) {
                try {
                    if (playbackServiceBinder != null) {
                        playbackServiceBinder.getService().unregisterPlayer(player);
                    }
                } catch (Exception ignored) {}

                playbackServiceBinder = null;
            }

            @Override
            public void onNullBinding(ComponentName name) {
                DebugLog.e(TAG, "Could not register ExoPlayer");
            }
        };

        Intent intent = new Intent(themedReactContext, VideoPlaybackService.class);
        intent.setAction(MediaSessionService.SERVICE_INTERFACE);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            themedReactContext.startForegroundService(intent);
        } else {
            themedReactContext.startService(intent);
        }

        int flags;
        if (Build.VERSION.SDK_INT >= 29) {
            flags = Context.BIND_AUTO_CREATE | Context.BIND_INCLUDE_CAPABILITIES;
        } else {
            flags = Context.BIND_AUTO_CREATE;
        }

        themedReactContext.bindService(intent, playbackServiceConnection, flags);
    }

    private void cleanupPlaybackService() {
        try {
            if(player != null && playbackServiceBinder != null) {
                playbackServiceBinder.getService().unregisterPlayer(player);
            }

            playbackServiceBinder = null;

            if(playbackServiceConnection != null) {
                themedReactContext.unbindService(playbackServiceConnection);
            }
        } catch(Exception e) {
            DebugLog.w(TAG, "Cloud not cleanup playback service");
        }
    }

    private DrmSessionManager buildDrmSessionManager(UUID uuid, DRMProps drmProps) throws UnsupportedDrmException {
        return buildDrmSessionManager(uuid, drmProps, 0);
    }

    private DrmSessionManager buildDrmSessionManager(UUID uuid, DRMProps drmProps, int retryCount) throws UnsupportedDrmException {
        if (Util.SDK_INT < 18) {
            return null;
        }
        try {
            HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(drmProps.getDrmLicenseServer(),
                    buildHttpDataSourceFactory(false));

            String[] keyRequestPropertiesArray = drmProps.getDrmLicenseHeader();
            for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
                drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]);
            }
            FrameworkMediaDrm mediaDrm = FrameworkMediaDrm.newInstance(uuid);
            if (hasDrmFailed) {
                // When DRM fails using L1 we want to switch to L3
                mediaDrm.setPropertyString("securityLevel", "L3");
            }
            return new DefaultDrmSessionManager.Builder()
                    .setUuidAndExoMediaDrmProvider(uuid, (_uuid) -> mediaDrm)
                    .setKeyRequestParameters(null)
                    .setMultiSession(drmProps.getMultiDrm())
                    .build(drmCallback);
        } catch (UnsupportedDrmException ex) {
            // Unsupported DRM exceptions are handled by the calling method
            throw ex;
        } catch (Exception ex) {
            if (retryCount < 3) {
                // Attempt retry 3 times in case where the OS Media DRM Framework fails for whatever reason
                return buildDrmSessionManager(uuid, drmProps, ++retryCount);
            }
            // Handle the unknow exception and emit to JS
            eventEmitter.onVideoError.invoke(ex.toString(), ex, "3006");
            return null;
        }
    }

    private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager, long cropStartMs, long cropEndMs) {
        if (uri == null) {
            throw new IllegalStateException("Invalid video uri");
        }
        int type;
        if ("rtsp".equals(overrideExtension)) {
            type = CONTENT_TYPE_RTSP;
        } else {
            type = Util.inferContentType(!TextUtils.isEmpty(overrideExtension) ? "." + overrideExtension
                    : uri.getLastPathSegment());
        }
        config.setDisableDisconnectError(this.disableDisconnectError);

        MediaItem.Builder mediaItemBuilder = new MediaItem.Builder()
                .setUri(uri);

        // refresh custom Metadata
        MediaMetadata customMetadata = ConfigurationUtils.buildCustomMetadata(source.getMetadata());
        if (customMetadata != null) {
            mediaItemBuilder.setMediaMetadata(customMetadata);
        }
        if (source.getAdsProps() != null) {
            Uri adTagUrl = source.getAdsProps().getAdTagUrl();
            if (adTagUrl != null) {
                mediaItemBuilder.setAdsConfiguration(
                        new MediaItem.AdsConfiguration.Builder(adTagUrl).build()
                );
            }
        }

        MediaItem.LiveConfiguration.Builder liveConfiguration = ConfigurationUtils.getLiveConfiguration(bufferConfig);
        mediaItemBuilder.setLiveConfiguration(liveConfiguration.build());

        MediaSource.Factory mediaSourceFactory;
        DrmSessionManagerProvider drmProvider;
        List<StreamKey> streamKeys = new ArrayList<>();
        if (drmSessionManager != null) {
            drmProvider = ((_mediaItem) -> drmSessionManager);
        } else {
            drmProvider = new DefaultDrmSessionManagerProvider();
        }

        switch (type) {
            case CONTENT_TYPE_SS:
                if(!BuildConfig.USE_EXOPLAYER_SMOOTH_STREAMING) {
                    DebugLog.e("Exo Player Exception", "Smooth Streaming is not enabled!");
                    throw new IllegalStateException("Smooth Streaming is not enabled!");
                }

                mediaSourceFactory = new SsMediaSource.Factory(
                        new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
                        buildDataSourceFactory(false)
                );
                break;
            case CONTENT_TYPE_DASH:
                if(!BuildConfig.USE_EXOPLAYER_DASH) {
                    DebugLog.e("Exo Player Exception", "DASH is not enabled!");
                    throw new IllegalStateException("DASH is not enabled!");
                }

                mediaSourceFactory = new DashMediaSource.Factory(
                        new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
                        buildDataSourceFactory(false)
                );
                break;
            case CONTENT_TYPE_HLS:
                if (!BuildConfig.USE_EXOPLAYER_HLS) {
                    DebugLog.e("Exo Player Exception", "HLS is not enabled!");
                    throw new IllegalStateException("HLS is not enabled!");
                }

                mediaSourceFactory = new HlsMediaSource.Factory(
                        mediaDataSourceFactory
                ).setAllowChunklessPreparation(source.getTextTracksAllowChunklessPreparation());
                break;
            case CONTENT_TYPE_OTHER:
                if ("asset".equals(uri.getScheme())) {
                    try {
                        DataSource.Factory assetDataSourceFactory = DataSourceUtil.buildAssetDataSourceFactory(themedReactContext, uri);
                        mediaSourceFactory = new ProgressiveMediaSource.Factory(assetDataSourceFactory);
                    } catch (Exception e) {
                        throw new IllegalStateException("cannot open input file" + uri);
                    }
                } else if ("file".equals(uri.getScheme()) ||
                        !useCache) {
                    mediaSourceFactory = new ProgressiveMediaSource.Factory(
                            mediaDataSourceFactory
                    );
                } else {
                    mediaSourceFactory = new ProgressiveMediaSource.Factory(
                            RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true))
                    );

                }
                break;
            case CONTENT_TYPE_RTSP:
                if (!BuildConfig.USE_EXOPLAYER_RTSP) {
                    DebugLog.e("Exo Player Exception", "RTSP is not enabled!");
                    throw new IllegalStateException("RTSP is not enabled!");
                }

                mediaSourceFactory = new RtspMediaSource.Factory();
                break;
            default: {
                throw new IllegalStateException("Unsupported type: " + type);
            }
        }

        if (cmcdConfigurationFactory != null) {
            mediaSourceFactory = mediaSourceFactory.setCmcdConfigurationFactory(
                    cmcdConfigurationFactory::createCmcdConfiguration
            );
        }

        MediaItem mediaItem = mediaItemBuilder.setStreamKeys(streamKeys).build();
        MediaSource mediaSource = mediaSourceFactory
                .setDrmSessionManagerProvider(drmProvider)
                .setLoadErrorHandlingPolicy(
                        config.buildLoadErrorHandlingPolicy(minLoadRetryCount)
                )
                .createMediaSource(mediaItem);

        if (cropStartMs >= 0 && cropEndMs >= 0) {
            return new ClippingMediaSource(mediaSource, cropStartMs * 1000, cropEndMs * 1000);
        } else if (cropStartMs >= 0) {
            return new ClippingMediaSource(mediaSource, cropStartMs * 1000, TIME_END_OF_SOURCE);
        } else if (cropEndMs >= 0) {
            return new ClippingMediaSource(mediaSource, 0, cropEndMs * 1000);
        }

        return mediaSource;
    }

    @Nullable
    private MediaSource buildTextSource() {
        if (source.getSideLoadedTextTracks() == null) {
            return null;
        }

        List<MediaItem.SubtitleConfiguration> subtitleConfigurations = new ArrayList<>();

        for (SideLoadedTextTrack track : source.getSideLoadedTextTracks().getTracks()) {
            MediaItem.SubtitleConfiguration subtitleConfiguration = new MediaItem.SubtitleConfiguration.Builder(track.getUri())
                .setMimeType(track.getType())
                .setLanguage(track.getLanguage())
                .setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
                .setRoleFlags(C.ROLE_FLAG_SUBTITLE)
                .setLabel(track.getTitle())
                .build();
            subtitleConfigurations.add(subtitleConfiguration);
        }

        MediaItem subtitlesMediaItem = new MediaItem.Builder()
                .setUri(source.getUri())
                .setSubtitleConfigurations(subtitleConfigurations).build();

        return new DefaultMediaSourceFactory(mediaDataSourceFactory).createMediaSource(subtitlesMediaItem);
    }

    private void releasePlayer() {
        if (player != null) {
            if(playbackServiceBinder != null) {
                playbackServiceBinder.getService().unregisterPlayer(player);
                themedReactContext.unbindService(playbackServiceConnection);
            }

            updateResumePosition();
            player.release();
            player.removeListener(this);
            trackSelector = null;

            ReactNativeVideoManager.Companion.getInstance().onInstanceRemoved(instanceId, player);
            player = null;
        }

        if (adsLoader != null) {
            adsLoader.release();
            adsLoader = null;
        }
        progressHandler.removeMessages(SHOW_PROGRESS);
        audioBecomingNoisyReceiver.removeListener();
        bandwidthMeter.removeEventListener(this);

        if (mainHandler != null && mainRunnable != null) {
            mainHandler.removeCallbacks(mainRunnable);
            mainRunnable = null;
        }
    }

    private static class OnAudioFocusChangedListener implements AudioManager.OnAudioFocusChangeListener {
        private final ReactExoplayerView view;
        private final ThemedReactContext themedReactContext;

        private OnAudioFocusChangedListener(ReactExoplayerView view, ThemedReactContext themedReactContext) {
            this.view = view;
            this.themedReactContext = themedReactContext;
        }

        @Override
        public void onAudioFocusChange(int focusChange) {
            Activity activity = themedReactContext.getCurrentActivity();

            switch (focusChange) {
                case AudioManager.AUDIOFOCUS_LOSS:
                    view.hasAudioFocus = false;
                    view.eventEmitter.onAudioFocusChanged.invoke(false);
                    // FIXME this pause can cause issue if content doesn't have pause capability (can happen on live channel)
                    if (activity != null) {
                        activity.runOnUiThread(view::pausePlayback);
                    }
                    view.audioManager.abandonAudioFocus(this);
                    break;
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                    view.eventEmitter.onAudioFocusChanged.invoke(false);
                    break;
                case AudioManager.AUDIOFOCUS_GAIN:
                    view.hasAudioFocus = true;
                    view.eventEmitter.onAudioFocusChanged.invoke(true);
                    break;
                default:
                    break;
            }

            if (view.player != null && activity != null) {
                if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
                    // Lower the volume
                    if (!view.muted) {
                        activity.runOnUiThread(() ->
                                view.player.setVolume(view.audioVolume * 0.8f)
                        );
                    }
                } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
                    // Raise it back to normal
                    if (!view.muted) {
                        activity.runOnUiThread(() ->
                                view.player.setVolume(view.audioVolume * 1)
                        );
                    }
                }
            }
        }
    }

    private boolean requestAudioFocus() {
        if (disableFocus || source.getUri() == null || this.hasAudioFocus) {
            return true;
        }
        int result = audioManager.requestAudioFocus(audioFocusChangeListener,
                AudioManager.STREAM_MUSIC,
                AudioManager.AUDIOFOCUS_GAIN);
        return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
    }

    private void setPlayWhenReady(boolean playWhenReady) {
        if (player == null) {
            return;
        }

        if (playWhenReady) {
            this.hasAudioFocus = requestAudioFocus();
            if (this.hasAudioFocus) {
                player.setPlayWhenReady(true);
            }
        } else {
            player.setPlayWhenReady(false);
        }
    }

    private void resumePlayback() {
        if (player != null) {
            if (!player.getPlayWhenReady()) {
                setPlayWhenReady(true);
            }
            setKeepScreenOn(preventsDisplaySleepDuringVideoPlayback);
        }
    }

    private void pausePlayback() {
        if (player != null) {
            if (player.getPlayWhenReady()) {
                setPlayWhenReady(false);
            }
        }
        setKeepScreenOn(false);
    }

    private void stopPlayback() {
        onStopPlayback();
        releasePlayer();
    }

    private void onStopPlayback() {
        audioManager.abandonAudioFocus(audioFocusChangeListener);
    }

    private void updateResumePosition() {
        resumeWindow = player.getCurrentMediaItemIndex();
        resumePosition = player.isCurrentMediaItemSeekable() ? Math.max(0, player.getCurrentPosition())
                : C.TIME_UNSET;
    }

    private void clearResumePosition() {
        resumeWindow = C.INDEX_UNSET;
        resumePosition = C.TIME_UNSET;
    }

    /**
     * Returns a new DataSource factory.
     *
     * @param useBandwidthMeter Whether to set {@link #bandwidthMeter} as a listener to the new
     *                          DataSource factory.
     * @return A new DataSource factory.
     */
    private DataSource.Factory buildDataSourceFactory(boolean useBandwidthMeter) {
        return DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext,
                useBandwidthMeter ? bandwidthMeter : null, source.getHeaders());
    }

    /**
     * Returns a new HttpDataSource factory.
     *
     * @param useBandwidthMeter Whether to set {@link #bandwidthMeter} as a listener to the new
     *     DataSource factory.
     * @return A new HttpDataSource factory.
     */
    private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMeter) {
        return DataSourceUtil.getDefaultHttpDataSourceFactory(this.themedReactContext, useBandwidthMeter ? bandwidthMeter : null, source.getHeaders());
    }

    // AudioBecomingNoisyListener implementation
    @Override
    public void onAudioBecomingNoisy() {
        eventEmitter.onVideoAudioBecomingNoisy.invoke();
    }

    // Player.Listener implementation
    @Override
    public void onIsLoadingChanged(boolean isLoading) {
        // Do nothing.
    }

    @Override
    public void onEvents(@NonNull Player player, Player.Events events) {
        if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED) || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
            int playbackState = player.getPlaybackState();
            boolean playWhenReady = player.getPlayWhenReady();
            Log.d("ReactExoplayerView", "onEvents: playbackState=" + playbackState + ", playWhenReady=" + playWhenReady);
            String text = "onStateChanged: playWhenReady=" + playWhenReady + ", playbackState=";
            eventEmitter.onPlaybackRateChange.invoke(playWhenReady && playbackState == ExoPlayer.STATE_READY ? 1.0f : 0.0f);
            switch (playbackState) {
                case Player.STATE_IDLE:
                    text += "idle";
                    eventEmitter.onVideoIdle.invoke();
                    clearProgressMessageHandler();
                    if (!player.getPlayWhenReady()) {
                        setKeepScreenOn(false);
                    }
                    break;
                case Player.STATE_BUFFERING:
                    text += "buffering";
                    onBuffering(true);
                    clearProgressMessageHandler();
                    setKeepScreenOn(preventsDisplaySleepDuringVideoPlayback);
                    break;
                case Player.STATE_READY:
                    text += "ready";
                    eventEmitter.onReadyForDisplay.invoke();
                    onBuffering(false);
                    clearProgressMessageHandler(); // ensure there is no other message
                    startProgressHandler();
                    videoLoaded();
                    if (selectTrackWhenReady && isUsingContentResolution) {
                        selectTrackWhenReady = false;
                        setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue);
                    }
                    // Setting the visibility for the playerControlView
                    if (playerControlView != null) {
                        playerControlView.show();
                    }
                    setKeepScreenOn(preventsDisplaySleepDuringVideoPlayback);
                    Log.d("ReactExoplayerView", "Player STATE_READY: currentPosition=" + player.getCurrentPosition());
                    if (isSeekInProgress) {
                        handleSeekCompletion();
                    }
                    break;
                case Player.STATE_ENDED:
                    text += "ended";
                    updateProgress();
                    eventEmitter.onVideoEnd.invoke();
                    onStopPlayback();
                    setKeepScreenOn(false);
                    break;
                default:
                    text += "unknown";
                    break;
            }
            DebugLog.d(TAG, text);
        }
    }

    private void startProgressHandler() {
        progressHandler.sendEmptyMessage(SHOW_PROGRESS);
    }

    /**
     *  The progress message handler will duplicate recursions of the onProgressMessage handler
     *  on change of player state from any state to STATE_READY with playWhenReady is true (when
     *  the video is not paused). This clears all existing messages.
     */
    private void clearProgressMessageHandler() {
        progressHandler.removeMessages(SHOW_PROGRESS);
    }

    private void videoLoaded() {
        if (!player.isPlayingAd() && loadVideoStarted) {
            loadVideoStarted = false;
            if (audioTrackType != null) {
                setSelectedAudioTrack(audioTrackType, audioTrackValue);
            }
            if (videoTrackType != null) {
                setSelectedVideoTrack(videoTrackType, videoTrackValue);
            }
            if (textTrackType != null) {
                setSelectedTextTrack(textTrackType, textTrackValue);
            }
            Format videoFormat = player.getVideoFormat();
            boolean isRotatedContent = videoFormat != null && (videoFormat.rotationDegrees == 90 || videoFormat.rotationDegrees == 270);
            int width = videoFormat != null ? (isRotatedContent ? videoFormat.height : videoFormat.width) : 0;
            int height = videoFormat != null ? (isRotatedContent ? videoFormat.width : videoFormat.height) : 0;
            String trackId = videoFormat != null ? videoFormat.id : null;

            // Properties that must be accessed on the main thread
            long duration = player.getDuration();
            long currentPosition = player.getCurrentPosition();
            ArrayList<Track> audioTracks = getAudioTrackInfo();
            ArrayList<Track> textTracks  = getTextTrackInfo();

            if (source.getContentStartTime() != -1) {
                ExecutorService es = Executors.newSingleThreadExecutor();
                es.execute(() -> {
                    // To prevent ANRs caused by getVideoTrackInfo we run this on a different thread and notify the player only when we're done
                    ArrayList<VideoTrack> videoTracks = getVideoTrackInfoFromManifest();
                    if (videoTracks != null) {
                        isUsingContentResolution = true;
                    }
                    eventEmitter.onVideoLoad.invoke(duration, currentPosition, width, height,
                            audioTracks, textTracks, videoTracks, trackId );

                });
                return;
            }

            ArrayList<VideoTrack> videoTracks = getVideoTrackInfo();

            eventEmitter.onVideoLoad.invoke(duration, currentPosition, width, height,
                    audioTracks, textTracks, videoTracks, trackId);
            refreshControlsStyles();
        }
    }

    private static boolean isTrackSelected(TrackSelection selection, TrackGroup group,
                                           int trackIndex){
        return selection != null && selection.getTrackGroup() == group
                && selection.indexOf( trackIndex ) != C.INDEX_UNSET;
    }

    private ArrayList<Track> getAudioTrackInfo() {
        ArrayList<Track> audioTracks = new ArrayList<>();
        if (trackSelector == null) {
            // Likely player is unmounting so no audio tracks are available anymore
            return audioTracks;
        }

        MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
        int index = getTrackRendererIndex(C.TRACK_TYPE_AUDIO);
        if (info == null || index == C.INDEX_UNSET) {
            return audioTracks;
        }
        TrackGroupArray groups = info.getTrackGroups(index);
        TrackSelectionArray selectionArray = player.getCurrentTrackSelections();
        TrackSelection selection = selectionArray.get( C.TRACK_TYPE_AUDIO );

        for (int i = 0; i < groups.length; ++i) {
            TrackGroup group = groups.get(i);
            Format format = group.getFormat(0);
            Track audioTrack = exoplayerTrackToGenericTrack(format, i, selection, group);
            audioTrack.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate);
            audioTracks.add(audioTrack);
        }
        return audioTracks;
    }

    private VideoTrack exoplayerVideoTrackToGenericVideoTrack(Format format, int trackIndex) {
        VideoTrack videoTrack = new VideoTrack();
        videoTrack.setWidth(format.width == Format.NO_VALUE ? 0 : format.width);
        videoTrack.setHeight(format.height == Format.NO_VALUE ? 0 : format.height);
        videoTrack.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate);
        videoTrack.setRotation(format.rotationDegrees);
        if (format.codecs != null) videoTrack.setCodecs(format.codecs);
        videoTrack.setTrackId(format.id == null ? String.valueOf(trackIndex) : format.id);
        videoTrack.setIndex(trackIndex);
        return videoTrack;
    }

    private ArrayList<VideoTrack> getVideoTrackInfo() {
        ArrayList<VideoTrack> videoTracks = new ArrayList<>();
        if (trackSelector == null) {
            // Likely player is unmounting so no video tracks are available anymore
            return videoTracks;
        }
        MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
        int index = getTrackRendererIndex(C.TRACK_TYPE_VIDEO);
        if (info == null || index == C.INDEX_UNSET) {
            return videoTracks;
        }

        TrackGroupArray groups = info.getTrackGroups(index);
        for (int i = 0; i < groups.length; ++i) {
            TrackGroup group = groups.get(i);

            for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
                Format format = group.getFormat(trackIndex);
                if (isFormatSupported(format)) {
                    VideoTrack videoTrack = exoplayerVideoTrackToGenericVideoTrack(format, trackIndex);
                    videoTracks.add(videoTrack);
                }
            }
        }
        return videoTracks;
    }

    private ArrayList<VideoTrack> getVideoTrackInfoFromManifest() {
        return this.getVideoTrackInfoFromManifest(0);
    }

    // We need retry count to in case where minefest request fails from poor network conditions
    @WorkerThread
    private ArrayList<VideoTrack> getVideoTrackInfoFromManifest(int retryCount) {
        ExecutorService es = Executors.newSingleThreadExecutor();
        final DataSource dataSource = this.mediaDataSourceFactory.createDataSource();
        final Uri sourceUri = source.getUri();
        final long startTime = source.getContentStartTime() * 1000 - 100; // s -> ms with 100ms offset

        Future<ArrayList<VideoTrack>> result = es.submit(new Callable() {
            final DataSource ds = dataSource;
            final Uri uri = sourceUri;
            final long startTimeUs = startTime * 1000; // ms -> us

            public ArrayList<VideoTrack> call() {
                ArrayList<VideoTrack> videoTracks = new ArrayList<>();
                try  {
                    DashManifest manifest = DashUtil.loadManifest(this.ds, this.uri);
                    int periodCount = manifest.getPeriodCount();
                    for (int i = 0; i < periodCount; i++) {
                        Period period = manifest.getPeriod(i);
                        for (int adaptationIndex = 0; adaptationIndex < period.adaptationSets.size(); adaptationIndex++) {
                            AdaptationSet adaptation = period.adaptationSets.get(adaptationIndex);
                            if (adaptation.type != C.TRACK_TYPE_VIDEO) {
                                continue;
                            }
                            boolean hasFoundContentPeriod = false;
                            for (int representationIndex = 0; representationIndex < adaptation.representations.size(); representationIndex++) {
                                Representation representation = adaptation.representations.get(representationIndex);
                                Format format = representation.format;
                                if (isFormatSupported(format)) {
                                    if (representation.presentationTimeOffsetUs <= startTimeUs) {
                                        break;
                                    }
                                    hasFoundContentPeriod = true;
                                    VideoTrack videoTrack = exoplayerVideoTrackToGenericVideoTrack(format, representationIndex);
                                    videoTracks.add(videoTrack);
                                }
                            }
                            if (hasFoundContentPeriod) {
                                return videoTracks;
                            }
                        }
                    }
                } catch (Exception e) {
                    DebugLog.w(TAG, "error in getVideoTrackInfoFromManifest:" + e.getMessage());
                }
                return null;
            }
        });

        try {
            ArrayList<VideoTrack> results = result.get(3000, TimeUnit.MILLISECONDS);
            if (results == null && retryCount < 1) {
                return this.getVideoTrackInfoFromManifest(++retryCount);
            }
            es.shutdown();
            return results;
        } catch (Exception e) {
            DebugLog.w(TAG, "error in getVideoTrackInfoFromManifest handling request:" + e.getMessage());
        }

        return null;
    }

    private Track exoplayerTrackToGenericTrack(Format format, int trackIndex, TrackSelection selection, TrackGroup group) {
        Track track = new Track();
        track.setIndex(trackIndex);
        if (format.sampleMimeType != null) track.setMimeType(format.sampleMimeType);
        if (format.language != null) track.setLanguage(format.language);
        if (format.label != null) track.setTitle(format.label);
        track.setSelected(isTrackSelected(selection, group, trackIndex));
        return track;
    }

    private ArrayList<Track> getTextTrackInfo() {
        ArrayList<Track> textTracks = new ArrayList<>();
        if (trackSelector == null) {
            return textTracks;
        }
        MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
        int index = getTrackRendererIndex(C.TRACK_TYPE_TEXT);
        if (info == null || index == C.INDEX_UNSET) {
            return textTracks;
        }
        TrackSelectionArray selectionArray = player.getCurrentTrackSelections();
        TrackSelection selection = selectionArray.get( C.TRACK_TYPE_VIDEO );
        TrackGroupArray groups = info.getTrackGroups(index);

        for (int i = 0; i < groups.length; ++i) {
            TrackGroup group = groups.get(i);
            Format format = group.getFormat(0);
            Track textTrack = exoplayerTrackToGenericTrack(format, i, selection, group);
            textTracks.add(textTrack);
        }
        return textTracks;
    }

    private void onBuffering(boolean buffering) {
        if (isBuffering == buffering) {
            return;
        }

        if (isPaused && isSeeking && !buffering) {
            eventEmitter.onVideoSeek.invoke(player.getCurrentPosition(), seekPosition);
            isSeeking = false;
        }

        isBuffering = buffering;
        eventEmitter.onVideoBuffer.invoke(buffering);
    }

    @Override
    public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, @Player.DiscontinuityReason int reason) {
        Log.d("ReactExoplayerView", "onPositionDiscontinuity: reason=" + reason + ", oldPosition=" + oldPosition.positionMs + ", newPosition=" + newPosition.positionMs);
        if (reason == Player.DISCONTINUITY_REASON_SEEK) {
            isSeeking = true;
            seekPosition = newPosition.positionMs;
            if (isUsingContentResolution) {
                // We need to update the selected track to make sure that it still matches user selection if track list has changed in this period
                setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue);
            }
        }

        if (playerNeedsSource) {
            // This will only occur if the user has performed a seek whilst in the error state. Update the
            // resume position so that if the user then retries, playback will resume from the position to
            // which they seeked.
            updateResumePosition();
        }
        if (isUsingContentResolution) {
            // Discontinuity events might have a different track list so we update the selected track
            setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue);
            selectTrackWhenReady = true;
        }
        // When repeat is turned on, reaching the end of the video will not cause a state change
        // so we need to explicitly detect it.
        if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION
                && player.getRepeatMode() == Player.REPEAT_MODE_ONE) {
            updateProgress();
            eventEmitter.onVideoEnd.invoke();
        }
    }

    @Override
    public void onTimelineChanged(@NonNull Timeline timeline, int reason) {
        // Do nothing.
    }

    @Override
    public void onTracksChanged(@NonNull Tracks tracks) {
        eventEmitter.onTextTracks.invoke(getTextTrackInfo());
        eventEmitter.onAudioTracks.invoke(getAudioTrackInfo());
        eventEmitter.onVideoTracks.invoke(getVideoTrackInfo());
    }

    @Override
    public void onPlaybackParametersChanged(PlaybackParameters params) {
        eventEmitter.onPlaybackRateChange.invoke(params.speed);
    }

    @Override
    public void onVolumeChanged(float volume) {
        eventEmitter.onVolumeChange.invoke(volume);
    }

    @Override
    public void onIsPlayingChanged(boolean isPlaying) {
        if (isPlaying && isSeeking) {
            eventEmitter.onVideoSeek.invoke(player.getCurrentPosition(), seekPosition);
        }

        eventEmitter.onVideoPlaybackStateChanged.invoke(isPlaying, isSeeking);

        if (isPlaying) {
            isSeeking = false;
        }
    }

    @Override
    public void onPlayerError(@NonNull PlaybackException e) {
        String errorString = "ExoPlaybackException: " + PlaybackException.getErrorCodeName(e.errorCode);
        String errorCode = "2" + e.errorCode;
        switch(e.errorCode) {
            case PlaybackException.ERROR_CODE_DRM_DEVICE_REVOKED:
            case PlaybackException.ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED:
            case PlaybackException.ERROR_CODE_DRM_PROVISIONING_FAILED:
            case PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR:
            case PlaybackException.ERROR_CODE_DRM_UNSPECIFIED:
                if (!hasDrmFailed) {
                    // When DRM fails to reach the app level certificate server it will fail with a source error so we assume that it is DRM related and try one more time
                    hasDrmFailed = true;
                    playerNeedsSource = true;
                    updateResumePosition();
                    initializePlayer();
                    setPlayWhenReady(true);
                    return;
                }
                break;
            default:
                break;
        }
        eventEmitter.onVideoError.invoke(errorString, e, errorCode);
        playerNeedsSource = true;
        if (isBehindLiveWindow(e)) {
            clearResumePosition();
            if (player != null) {
                player.seekToDefaultPosition();
                player.prepare();
            }
        } else {
            updateResumePosition();
        }
    }

    private static boolean isBehindLiveWindow(PlaybackException e) {
        return e.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW;
    }

    public int getTrackRendererIndex(int trackType) {
        if (player != null) {
            int rendererCount = player.getRendererCount();
            for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {
                if (player.getRendererType(rendererIndex) == trackType) {
                    return rendererIndex;
                }
            }
        }
        return C.INDEX_UNSET;
    }

    @Override
    public void onMetadata(@NonNull Metadata metadata) {
        ArrayList<TimedMetadata> metadataArray = new ArrayList<>();
        for (int i = 0; i < metadata.length(); i++) {
            Metadata.Entry entry = metadata.get(i);

            if (entry instanceof Id3Frame) {
                Id3Frame frame = (Id3Frame) metadata.get(i);

                String value = "";

                if (frame instanceof TextInformationFrame) {
                    TextInformationFrame txxxFrame = (TextInformationFrame) frame;
                    value = txxxFrame.value;
                }
                TimedMetadata timedMetadata = new TimedMetadata(frame.id, value);
                metadataArray.add(timedMetadata);
            } else if (entry instanceof EventMessage) {
                EventMessage eventMessage = (EventMessage) entry;
                TimedMetadata timedMetadata = new TimedMetadata(eventMessage.schemeIdUri, eventMessage.value);
                metadataArray.add(timedMetadata);
            } else {
                DebugLog.d(TAG, "unhandled metadata " + entry);
            }
        }
        eventEmitter.onTimedMetadata.invoke(metadataArray);
    }

    public void onCues(CueGroup cueGroup) {
        if (!cueGroup.cues.isEmpty() && cueGroup.cues.get(0).text != null) {
            String subtitleText = cueGroup.cues.get(0).text.toString();
            eventEmitter.onTextTrackDataChanged.invoke(subtitleText);
        }
    }

    // ReactExoplayerViewManager public api

    public void setSrc(Source source) {
        if (source.getUri() != null) {
            clearResumePosition();
            boolean isSourceEqual = source.isEquals(this.source);
            hasDrmFailed = false;
            this.source = source;
            this.mediaDataSourceFactory =
                    DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, bandwidthMeter,
                            source.getHeaders());

            if (source.getCmcdProps() != null) {
                CMCDConfig cmcdConfig = new CMCDConfig(source.getCmcdProps());
                CmcdConfiguration.Factory factory = cmcdConfig.toCmcdConfigurationFactory();
                this.setCmcdConfigurationFactory(factory);
            } else {
                this.setCmcdConfigurationFactory(null);
            }

            if (!isSourceEqual) {
                reloadSource();
            }
        } else {
            clearSrc();
        }
    }
    public void clearSrc() {
        if (source.getUri() != null) {
            if (player != null) {
                player.stop();
                player.clearMediaItems();
            }
        }
        exoPlayerView.hideAds();
        this.source = new Source();
        this.mediaDataSourceFactory = null;
        clearResumePosition();
    }

    public void setProgressUpdateInterval(final float progressUpdateInterval) {
        mProgressUpdateInterval = progressUpdateInterval;
    }

    public void setReportBandwidth(boolean reportBandwidth) {
        mReportBandwidth = reportBandwidth;
    }

    private void reloadSource() {
        playerNeedsSource = true;
        initializePlayer();
    }

    public void setResizeModeModifier(@ResizeMode.Mode int resizeMode) {
        if (exoPlayerView != null) {
            exoPlayerView.setResizeMode(resizeMode);
        }
    }

    private void applyModifiers() {
        setRepeatModifier(repeat);
        setMutedModifier(muted);
    }

    public void setRepeatModifier(boolean repeat) {
        if (player != null) {
            if (repeat) {
                player.setRepeatMode(Player.REPEAT_MODE_ONE);
            } else {
                player.setRepeatMode(Player.REPEAT_MODE_OFF);
            }
        }
        this.repeat = repeat;
    }

    public void setPreventsDisplaySleepDuringVideoPlayback(boolean preventsDisplaySleepDuringVideoPlayback) {
        this.preventsDisplaySleepDuringVideoPlayback = preventsDisplaySleepDuringVideoPlayback;
    }

    public void disableTrack(int rendererIndex) {
        DefaultTrackSelector.Parameters disableParameters = trackSelector.getParameters()
                .buildUpon()
                .setRendererDisabled(rendererIndex, true)
                .build();
        trackSelector.setParameters(disableParameters);
    }

    public void setSelectedTrack(int trackType, String type, String value) {
        if (player == null) return;
        int rendererIndex = getTrackRendererIndex(trackType);
        if (rendererIndex == C.INDEX_UNSET) {
            return;
        }
        MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
        if (info == null) {
            return;
        }

        TrackGroupArray groups = info.getTrackGroups(rendererIndex);
        int groupIndex = C.INDEX_UNSET;
        List<Integer> tracks = new ArrayList<>();
        tracks.add(0);

        if (TextUtils.isEmpty(type)) {
            type = "default";
        }

        if ("disabled".equals(type)) {
            disableTrack(rendererIndex);
            return;
        } else if ("language".equals(type)) {
            for (int i = 0; i < groups.length; ++i) {
                Format format = groups.get(i).getFormat(0);
                if (format.language != null && format.language.equals(value)) {
                    groupIndex = i;
                    break;
                }
            }
        } else if ("title".equals(type)) {
            for (int i = 0; i < groups.length; ++i) {
                Format format = groups.get(i).getFormat(0);
                if (format.label != null && format.label.equals(value)) {
                    groupIndex = i;
                    break;
                }
            }
        } else if ("index".equals(type)) {
            int iValue = ReactBridgeUtils.safeParseInt(value, -1);
            if (iValue != -1) {
                if (trackType == C.TRACK_TYPE_VIDEO && groups.length == 1) {
                    groupIndex = 0;
                    if (iValue < groups.get(groupIndex).length) {
                        tracks.set(0, iValue);
                    }
                } else if (iValue < groups.length) {
                    groupIndex = iValue;
                }
            }
        } else if ("resolution".equals(type)) {
            int height = ReactBridgeUtils.safeParseInt(value, -1);
            if (height != -1) {
                for (int i = 0; i < groups.length; ++i) { // Search for the exact height
                    TrackGroup group = groups.get(i);
                    Format closestFormat = null;
                    int closestTrackIndex = -1;
                    boolean usingExactMatch = false;
                    for (int j = 0; j < group.length; j++) {
                        Format format = group.getFormat(j);
                        if (format.height == height) {
                            groupIndex = i;
                            tracks.set(0, j);
                            closestFormat = null;
                            closestTrackIndex = -1;
                            usingExactMatch = true;
                            break;
                        } else if (isUsingContentResolution) {
                            // When using content resolution rather than ads, we need to try and find the closest match if there is no exact match
                            if (closestFormat != null) {
                                if ((format.bitrate > closestFormat.bitrate || format.height > closestFormat.height) && format.height < height) {
                                    // Higher quality match
                                    closestFormat = format;
                                    closestTrackIndex = j;
                                }
                            } else if (format.height < height) {
                                closestFormat = format;
                                closestTrackIndex = j;
                            }
                        }
                    }
                    // This is a fallback if the new period contains only higher resolutions than the user has selected
                    if (closestFormat == null && isUsingContentResolution && !usingExactMatch) {
                        // No close match found - so we pick the lowest quality
                        int minHeight = Integer.MAX_VALUE;
                        for (int j = 0; j < group.length; j++) {
                            Format format = group.getFormat(j);
                            if (format.height < minHeight) {
                                minHeight = format.height;
                                groupIndex = i;
                                tracks.set(0, j);
                            }
                        }
                    }
                    // Selecting the closest match found
                    if (closestFormat != null && closestTrackIndex != -1) {
                        // We found the closest match instead of an exact one
                        groupIndex = i;
                        tracks.set(0, closestTrackIndex);
                    }
                }
            }
        } else if (trackType == C.TRACK_TYPE_TEXT && Util.SDK_INT > 18) { // Text default
            // Use system settings if possible
            CaptioningManager captioningManager
                    = (CaptioningManager)themedReactContext.getSystemService(Context.CAPTIONING_SERVICE);
            if (captioningManager != null && captioningManager.isEnabled()) {
                groupIndex = getGroupIndexForDefaultLocale(groups);
            }
        } else if (rendererIndex == C.TRACK_TYPE_AUDIO) { // Audio default
            groupIndex = getGroupIndexForDefaultLocale(groups);
        }

        if (groupIndex == C.INDEX_UNSET && trackType == C.TRACK_TYPE_VIDEO && groups.length != 0) { // Video auto
            // Add all tracks as valid options for ABR to choose from
            TrackGroup group = groups.get(0);
            ArrayList<Integer> allTracks = new ArrayList<>(group.length);
            groupIndex = 0;
            for (int j = 0; j < group.length; j++) {
                allTracks.add(j);
            }

            // Valiate list of all tracks and add only supported formats
            int supportedFormatLength = 0;
            for (int g = 0; g < allTracks.size(); g++) {
                Format format = group.getFormat(g);
                if (isFormatSupported(format)) {
                    supportedFormatLength++;
                }
            }
            if (allTracks.size() == 1) {
                // With only one tracks we can't remove any tracks so attempt to play it anyway
                tracks = allTracks;
            } else {
                tracks =  new ArrayList<>(supportedFormatLength + 1);
                for (int k = 0; k < allTracks.size(); k++) {
                    Format format = group.getFormat(k);
                    if (isFormatSupported(format)) {
                        tracks.add(allTracks.get(k));
                    }
                }
            }
        }

        if (groupIndex == C.INDEX_UNSET) {
            disableTrack(rendererIndex);
            return;
        }

        TrackSelectionOverride selectionOverride = new TrackSelectionOverride(groups.get(groupIndex), tracks);

        DefaultTrackSelector.Parameters.Builder selectionParameters = trackSelector.getParameters()
                .buildUpon()
                .setExceedAudioConstraintsIfNecessary(true)
                .setExceedRendererCapabilitiesIfNecessary(true)
                .setExceedVideoConstraintsIfNecessary(true)
                .setRendererDisabled(rendererIndex, false)
                .clearOverridesOfType(selectionOverride.getType());

        if (trackType == C.TRACK_TYPE_VIDEO && isUsingVideoABR()) {
            selectionParameters.setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate);
        } else {
            selectionParameters.addOverride(selectionOverride);
        }

        trackSelector.setParameters(selectionParameters.build());
    }

    private boolean isFormatSupported(Format format) {
        int width = format.width == Format.NO_VALUE ? 0 : format.width;
        int height = format.height == Format.NO_VALUE ? 0 : format.height;
        float frameRate = format.frameRate == Format.NO_VALUE ? 0 : format.frameRate;
        String mimeType = format.sampleMimeType;
        if (mimeType == null) {
            return true;
        }
        boolean isSupported;
        try {
            MediaCodecInfo codecInfo = MediaCodecUtil.getDecoderInfo(mimeType, false, false);
            isSupported = codecInfo.isVideoSizeAndRateSupportedV21(width, height, frameRate);
        } catch (Exception e) {
            // Failed to get decoder info - assume it is supported
            isSupported = true;
        }
        return isSupported;
    }

    private int getGroupIndexForDefaultLocale(TrackGroupArray groups) {
        if (groups.length == 0){
            return C.INDEX_UNSET;
        }

        int groupIndex = 0; // default if no match
        String locale2 = Locale.getDefault().getLanguage(); // 2 letter code
        String locale3 = Locale.getDefault().getISO3Language(); // 3 letter code
        for (int i = 0; i < groups.length; ++i) {
            Format format = groups.get(i).getFormat(0);
            String language = format.language;
            if (language != null && (language.equals(locale2) || language.equals(locale3))) {
                groupIndex = i;
                break;
            }
        }
        return groupIndex;
    }

    public void setSelectedVideoTrack(String type, String value) {
        videoTrackType = type;
        videoTrackValue = value;
        if (!loadVideoStarted) setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue);
    }

    public void setSelectedAudioTrack(String type, String value) {
        audioTrackType = type;
        audioTrackValue = value;
        setSelectedTrack(C.TRACK_TYPE_AUDIO, audioTrackType, audioTrackValue);
    }

    public void setSelectedTextTrack(String type, String value) {
        textTrackType = type;
        textTrackValue = value;
        setSelectedTrack(C.TRACK_TYPE_TEXT, textTrackType, textTrackValue);
    }

    public void setPausedModifier(boolean paused) {
        isPaused = paused;
        if (player != null) {
            if (!paused) {
                resumePlayback();
            } else {
                pausePlayback();
            }
        }
    }

    public void setMutedModifier(boolean muted) {
        this.muted = muted;
        if (player != null) {
            player.setVolume(muted ? 0.f : audioVolume);
        }
    }

    private void changeAudioOutput(AudioOutput output) {
        if (player != null) {
            int streamType = output.getStreamType();
            int usage = Util.getAudioUsageForStreamType(streamType);
            int contentType = Util.getAudioContentTypeForStreamType(streamType);
            AudioAttributes audioAttributes = new AudioAttributes.Builder().setUsage(usage)
                    .setContentType(contentType)
                    .build();
            player.setAudioAttributes(audioAttributes, false);
            AudioManager audioManager = (AudioManager) themedReactContext.getSystemService(Context.AUDIO_SERVICE);
            boolean isSpeakerOutput = output == AudioOutput.SPEAKER;
            audioManager.setMode(
                    isSpeakerOutput ? AudioManager.MODE_NORMAL
                            : AudioManager.MODE_IN_COMMUNICATION);
            audioManager.setSpeakerphoneOn(isSpeakerOutput);
        }
    }

    public void setAudioOutput(AudioOutput output) {
        if (audioOutput != output) {
            this.audioOutput = output;
            changeAudioOutput(output);
        }
    }

    public void setVolumeModifier(float volume) {
        audioVolume = volume;
        if (player != null) {
            player.setVolume(audioVolume);
        }
    }

    public void seekTo(long positionMs) {
        if (player != null) {
            Log.d("ReactExoplayerView", "seekTo: positionMs=" + positionMs);
            isSeekInProgress = true;
            isSeeking = true;
            seekPosition = positionMs;
            player.seekTo(positionMs);
        }
    }

    public void setRateModifier(float newRate) {
        if (newRate <= 0) {
            DebugLog.w(TAG, "cannot set rate <= 0");
            return;
        }

        rate = newRate;

        if (player != null) {
            PlaybackParameters params = new PlaybackParameters(rate, 1f);
            player.setPlaybackParameters(params);
        }
    }

    public void setMaxBitRateModifier(int newMaxBitRate) {
        maxBitRate = newMaxBitRate;
        if (player != null && isUsingVideoABR()) {
            // do not apply yet if not auto
            trackSelector.setParameters(trackSelector.buildUponParameters()
                    .setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate));
        }
    }

    public void setMinLoadRetryCountModifier(int newMinLoadRetryCount) {
        minLoadRetryCount = newMinLoadRetryCount;
        releasePlayer();
        initializePlayer();
    }

    public void setPlayInBackground(boolean playInBackground) {
        this.playInBackground = playInBackground;
    }

    public void setDisableFocus(boolean disableFocus) {
        this.disableFocus = disableFocus;
    }

    public void setFocusable(boolean focusable) {
        this.focusable = focusable;
        exoPlayerView.setFocusable(this.focusable);
    }

    public void setShowNotificationControls(boolean showNotificationControls) {
        this.showNotificationControls = showNotificationControls;

        if (playbackServiceConnection == null && showNotificationControls) {
            setupPlaybackService();
        } else if(!showNotificationControls && playbackServiceConnection != null) {
            cleanupPlaybackService();
        }
    }

    public void setBufferingStrategy(BufferingStrategy.BufferingStrategyEnum _bufferingStrategy) {
        bufferingStrategy = _bufferingStrategy;
    }

    public boolean getPreventsDisplaySleepDuringVideoPlayback() {
        return preventsDisplaySleepDuringVideoPlayback;
    }

    private void updateFullScreenButtonVisibility() {
        if (playerControlView != null) {
            final ImageButton fullScreenButton = playerControlView.findViewById(R.id.exo_fullscreen);
            //Handling the fullScreenButton click event
            if (isFullscreen && fullScreenPlayerView != null && !fullScreenPlayerView.isShowing()) {
                fullScreenButton.setVisibility(GONE);
            } else {
                fullScreenButton.setVisibility(VISIBLE);
            }
        }
    }

    public void setDisableDisconnectError(boolean disableDisconnectError) {
        this.disableDisconnectError = disableDisconnectError;
    }

    public void setFullscreen(boolean fullscreen) {
        if (fullscreen == isFullscreen) {
            return; // Avoid generating events when nothing is changing
        }
        isFullscreen = fullscreen;

        Activity activity = themedReactContext.getCurrentActivity();
        if (activity == null) {
            return;
        }

        if (isFullscreen) {
            fullScreenPlayerView = new FullScreenPlayerView(getContext(), exoPlayerView, this, playerControlView, new OnBackPressedCallback(true) {
                @Override
                public void handleOnBackPressed() {
                    setFullscreen(false);
                }
            }, controlsConfig);
            eventEmitter.onVideoFullscreenPlayerWillPresent.invoke();
            if (fullScreenPlayerView != null) {
                fullScreenPlayerView.show();
            }
            UiThreadUtil.runOnUiThread(() -> {
                eventEmitter.onVideoFullscreenPlayerDidPresent.invoke();
            });
        } else {
            eventEmitter.onVideoFullscreenPlayerWillDismiss.invoke();
            if (fullScreenPlayerView != null) {
                fullScreenPlayerView.dismiss();
                reLayoutControls();
                setControls(controls);
            }
            UiThreadUtil.runOnUiThread(() -> {
                eventEmitter.onVideoFullscreenPlayerDidDismiss.invoke();
            });
        }
        // need to be done at the end to avoid hiding fullscreen control button when fullScreenPlayerView is shown
        updateFullScreenButtonVisibility();
    }

    public void setHideShutterView(boolean hideShutterView) {
        exoPlayerView.setHideShutterView(hideShutterView);
    }

    public void setBufferConfig(BufferConfig config) {
        bufferConfig = config;
        if (bufferConfig.getCacheSize() > 0) {
            RNVSimpleCache.INSTANCE.setSimpleCache(
                    this.getContext(),
                    bufferConfig.getCacheSize()
            );
            useCache = true;
        } else {
            useCache = false;
        }
        releasePlayer();
        initializePlayer();
    }

    @Override
    public void onDrmKeysLoaded(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
        DebugLog.d("DRM Info", "onDrmKeysLoaded");
    }

    @Override
    public void onDrmSessionAcquired(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, int state) {
        DebugLog.d("DRM Info", "onDrmSessionAcquired");
    }

    @Override
    public void onDrmSessionReleased(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
        DebugLog.d("DRM Info", "onDrmSessionReleased");
    }

    @Override
    public void onDrmSessionManagerError(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, @NonNull Exception e) {
        DebugLog.d("DRM Info", "onDrmSessionManagerError");
        eventEmitter.onVideoError.invoke("onDrmSessionManagerError", e, "3002");
    }

    @Override
    public void onDrmKeysRestored(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
        DebugLog.d("DRM Info", "onDrmKeysRestored");
    }

    @Override
    public void onDrmKeysRemoved(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
        DebugLog.d("DRM Info", "onDrmKeysRemoved");
    }

    /**
     * Handling controls prop
     *
     * @param controls  Controls prop, if true enable controls, if false disable them
     */
    public void setControls(boolean controls) {
        this.controls = controls;
        if (controls) {
            addPlayerControl();
            updateFullScreenButtonVisibility();
        } else {
            int indexOfPC = indexOfChild(playerControlView);
            if (indexOfPC != -1) {
                removeViewAt(indexOfPC);
            }
        }
        refreshControlsStyles();
    }

    public void setSubtitleStyle(SubtitleStyle style) {
        exoPlayerView.setSubtitleStyle(style);
    }

    public void setShutterColor(Integer color) {
        exoPlayerView.setShutterColor(color);
    }

    @Override
    public void onAdEvent(AdEvent adEvent) {
        if (adEvent.getAdData() != null) {
            eventEmitter.onReceiveAdEvent.invoke(adEvent.getType().name(), adEvent.getAdData());
        } else {
            eventEmitter.onReceiveAdEvent.invoke(adEvent.getType().name(), null);
        }
    }

    @Override
    public void onAdError(AdErrorEvent adErrorEvent) {
        AdError error = adErrorEvent.getError();
        Map<String, String> errMap = Map.of(
                "message", error.getMessage(),
                "code", String.valueOf(error.getErrorCode()),
                "type", String.valueOf(error.getErrorType())
        );
        eventEmitter.onReceiveAdEvent.invoke("ERROR", errMap);
    }

    public void setControlsStyles(ControlsConfig controlsStyles) {
        controlsConfig = controlsStyles;
        refreshControlsStyles();
    }
}
