diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0d4bd8ea0..cad31abad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,16 @@
+## 0.0.78
+* [BREAKING_CHANGE] Split controlsHidden into controlsHiddenStart and controlsHiddenEnd.
+* [BREAKING_CHANGE] Added to Function(bool) onPlayerVisibilityChanged to customControlsBuilder in [BetterPlayerConfiguration].
+* Migrated android native code to Kotlin.
+* Updated ExoPlayer version to 2.15.1.
+* Updated screenshots.
+* Added onTapDown handle for material and cupertino progress bar to handle show and hide of controls.
+* Fixed crash related to Android 12.
+* Fixed issue with full url of subtitle for HLS data source.
+* Fixed install page from docs.
+* Fixed one of the showcase images.
+* Fixed video in list example.
+
 ## 0.0.77
 * Fixed full screen safe area issue in cupertino controls.
 * Fixed subtitles duplication after changing data source.
diff --git a/android/build.gradle b/android/build.gradle
index 208c538da..cb0fde023 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -1,14 +1,22 @@
 group 'com.jhomlala.better_player'
-version '1.0-SNAPSHOT'
+version '0.0.77'
 
 buildscript {
+
+    ext.exoPlayerVersion = "2.15.1"
+    ext.lifecycleVersion = "2.4.0-beta01"
+    ext.annotationVersion = "1.2.0"
+    ext.workVersion = "2.7.0"
+    ext.coreVersion = "1.6.0"
+    ext.gradleVersion = "4.1.0"
+
     repositories {
         google()
         jcenter()
     }
 
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.3.0'
+        classpath "com.android.tools.build:gradle:$gradleVersion"
     }
 }
 
@@ -20,9 +28,10 @@ rootProject.allprojects {
 }
 
 apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
 
 android {
-    compileSdkVersion 28
+    compileSdkVersion 31
 
     defaultConfig {
         minSdkVersion 16
@@ -39,19 +48,25 @@ android {
     }
 
     dependencies {
-        implementation 'com.google.android.exoplayer:exoplayer-core:2.14.2'
-        implementation 'com.google.android.exoplayer:exoplayer-hls:2.14.2'
-        implementation 'com.google.android.exoplayer:exoplayer-dash:2.14.2'
-        implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.14.2'
-        implementation 'com.google.android.exoplayer:exoplayer-ui:2.14.2'
-        implementation 'com.google.android.exoplayer:extension-mediasession:2.14.2'
-        implementation "android.arch.lifecycle:runtime:1.1.1"
-        implementation "android.arch.lifecycle:common:1.1.1"
-        implementation "android.arch.lifecycle:common-java8:1.1.1"
-        implementation 'androidx.annotation:annotation:1.1.0'
-        implementation 'androidx.legacy:legacy-support-v4:1.0.0'
-        implementation "androidx.work:work-runtime:2.5.0"
+        implementation "com.google.android.exoplayer:exoplayer-core:$exoPlayerVersion"
+        implementation "com.google.android.exoplayer:exoplayer-hls:$exoPlayerVersion"
+        implementation "com.google.android.exoplayer:exoplayer-dash:$exoPlayerVersion"
+        implementation "com.google.android.exoplayer:exoplayer-smoothstreaming:$exoPlayerVersion"
+        implementation "com.google.android.exoplayer:exoplayer-ui:$exoPlayerVersion"
+        implementation "com.google.android.exoplayer:extension-mediasession:$exoPlayerVersion"
+        implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
+        implementation "androidx.lifecycle:lifecycle-common:$lifecycleVersion"
+        implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
+        implementation "androidx.annotation:annotation:$annotationVersion"
+        implementation "androidx.work:work-runtime:$workVersion"
     }
 }
+dependencies {
+    implementation "androidx.core:core-ktx:$coreVersion"
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
+}
+repositories {
+    mavenCentral()
+}
 
 
diff --git a/android/src/main/java/com/jhomlala/better_player/BetterPlayer.java b/android/src/main/java/com/jhomlala/better_player/BetterPlayer.java
deleted file mode 100644
index f37b86c49..000000000
--- a/android/src/main/java/com/jhomlala/better_player/BetterPlayer.java
+++ /dev/null
@@ -1,964 +0,0 @@
-package com.jhomlala.better_player;
-
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Handler;
-import android.support.v4.media.MediaMetadataCompat;
-import android.support.v4.media.session.MediaSessionCompat;
-import android.support.v4.media.session.PlaybackStateCompat;
-import android.util.Log;
-import android.view.Surface;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.lifecycle.Observer;
-import androidx.media.session.MediaButtonReceiver;
-import androidx.work.Data;
-import androidx.work.OneTimeWorkRequest;
-import androidx.work.WorkInfo;
-import androidx.work.WorkManager;
-
-import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.ControlDispatcher;
-import com.google.android.exoplayer2.DefaultLoadControl;
-import com.google.android.exoplayer2.ExoPlaybackException;
-import com.google.android.exoplayer2.ExoPlayer;
-import com.google.android.exoplayer2.Format;
-import com.google.android.exoplayer2.LoadControl;
-import com.google.android.exoplayer2.MediaItem;
-import com.google.android.exoplayer2.PlaybackParameters;
-import com.google.android.exoplayer2.Player;
-import com.google.android.exoplayer2.SimpleExoPlayer;
-import com.google.android.exoplayer2.Timeline;
-import com.google.android.exoplayer2.audio.AudioAttributes;
-import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
-import com.google.android.exoplayer2.drm.DrmSessionManager;
-import com.google.android.exoplayer2.drm.DummyExoMediaDrm;
-import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
-import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
-import com.google.android.exoplayer2.drm.LocalMediaDrmCallback;
-import com.google.android.exoplayer2.drm.UnsupportedDrmException;
-import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
-import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
-import com.google.android.exoplayer2.source.ClippingMediaSource;
-import com.google.android.exoplayer2.source.MediaSource;
-import com.google.android.exoplayer2.source.ProgressiveMediaSource;
-import com.google.android.exoplayer2.source.TrackGroup;
-import com.google.android.exoplayer2.source.TrackGroupArray;
-import com.google.android.exoplayer2.source.dash.DashMediaSource;
-import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
-import com.google.android.exoplayer2.source.hls.HlsMediaSource;
-import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
-import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
-import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
-import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
-import com.google.android.exoplayer2.ui.PlayerNotificationManager;
-import com.google.android.exoplayer2.upstream.DataSource;
-import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
-import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
-import com.google.android.exoplayer2.util.Util;
-
-import java.io.File;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-
-import io.flutter.plugin.common.EventChannel;
-import io.flutter.plugin.common.MethodChannel.Result;
-import io.flutter.view.TextureRegistry;
-
-import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
-import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
-import static com.jhomlala.better_player.DataSourceUtils.getDataSourceFactory;
-import static com.jhomlala.better_player.DataSourceUtils.getUserAgent;
-
-final class BetterPlayer {
-    private static final String TAG = "BetterPlayer";
-    private static final String FORMAT_SS = "ss";
-    private static final String FORMAT_DASH = "dash";
-    private static final String FORMAT_HLS = "hls";
-    private static final String FORMAT_OTHER = "other";
-    private static final String DEFAULT_NOTIFICATION_CHANNEL = "BETTER_PLAYER_NOTIFICATION";
-    private static final int NOTIFICATION_ID = 20772077;
-
-    private final SimpleExoPlayer exoPlayer;
-    private final TextureRegistry.SurfaceTextureEntry textureEntry;
-    private final QueuingEventSink eventSink = new QueuingEventSink();
-    private final EventChannel eventChannel;
-    private final DefaultTrackSelector trackSelector;
-    private final LoadControl loadControl;
-
-    private boolean isInitialized = false;
-    private Surface surface;
-    private String key;
-    private PlayerNotificationManager playerNotificationManager;
-    private Handler refreshHandler;
-    private Runnable refreshRunnable;
-    private ExoPlayer.Listener exoPlayerEventListener;
-    private Bitmap bitmap;
-    private MediaSessionCompat mediaSession;
-    private DrmSessionManager drmSessionManager;
-    private WorkManager workManager;
-    private HashMap<UUID, Observer<WorkInfo>> workerObserverMap;
-    private CustomDefaultLoadControl customDefaultLoadControl;
-    private long lastSendBufferedPosition = 0L;
-
-
-    BetterPlayer(
-            Context context,
-            EventChannel eventChannel,
-            TextureRegistry.SurfaceTextureEntry textureEntry,
-            CustomDefaultLoadControl customDefaultLoadControl,
-            Result result) {
-        com.google.android.exoplayer2.util.Log.setLogLevel(
-                com.google.android.exoplayer2.util.Log.LOG_LEVEL_ERROR);
-        this.eventChannel = eventChannel;
-        this.textureEntry = textureEntry;
-        trackSelector = new DefaultTrackSelector(context);
-
-        this.customDefaultLoadControl = customDefaultLoadControl != null ?
-                customDefaultLoadControl : new CustomDefaultLoadControl();
-        DefaultLoadControl.Builder loadBuilder = new DefaultLoadControl.Builder();
-        loadBuilder.setBufferDurationsMs(
-                this.customDefaultLoadControl.minBufferMs,
-                this.customDefaultLoadControl.maxBufferMs,
-                this.customDefaultLoadControl.bufferForPlaybackMs,
-                this.customDefaultLoadControl.bufferForPlaybackAfterRebufferMs);
-        loadControl = loadBuilder.build();
-
-        exoPlayer = new SimpleExoPlayer.Builder(context)
-                .setTrackSelector(trackSelector)
-                .setLoadControl(loadControl)
-                .build();
-        workManager = WorkManager.getInstance(context);
-        workerObserverMap = new HashMap<>();
-
-        setupVideoPlayer(eventChannel, textureEntry, result);
-    }
-
-    void setDataSource(
-            Context context, String key, String dataSource, String formatHint, Result result,
-            Map<String, String> headers, boolean useCache, long maxCacheSize, long maxCacheFileSize,
-            long overriddenDuration, String licenseUrl, Map<String, String> drmHeaders,
-            String cacheKey, String clearKey) {
-        this.key = key;
-        isInitialized = false;
-
-        Uri uri = Uri.parse(dataSource);
-        DataSource.Factory dataSourceFactory;
-
-        String userAgent = getUserAgent(headers);
-
-        if (licenseUrl != null && !licenseUrl.isEmpty()) {
-            HttpMediaDrmCallback httpMediaDrmCallback =
-                    new HttpMediaDrmCallback(licenseUrl, new DefaultHttpDataSource.Factory());
-
-            if (drmHeaders != null) {
-                for (Map.Entry<String, String> entry : drmHeaders.entrySet()) {
-                    httpMediaDrmCallback.setKeyRequestProperty(entry.getKey(), entry.getValue());
-                }
-            }
-
-            if (Util.SDK_INT < 18) {
-                Log.e(TAG, "Protected content not supported on API levels below 18");
-                drmSessionManager = null;
-            } else {
-                UUID drmSchemeUuid = Util.getDrmUuid("widevine");
-                if (drmSchemeUuid != null) {
-                    drmSessionManager =
-                            new DefaultDrmSessionManager.Builder()
-                                    .setUuidAndExoMediaDrmProvider(drmSchemeUuid,
-                                            uuid -> {
-                                                try {
-                                                    FrameworkMediaDrm mediaDrm = FrameworkMediaDrm.newInstance(uuid);
-                                                    // Force L3.
-                                                    mediaDrm.setPropertyString("securityLevel", "L3");
-                                                    return mediaDrm;
-                                                } catch (UnsupportedDrmException e) {
-                                                    return new DummyExoMediaDrm();
-                                                }
-                                            })
-                                    .setMultiSession(false)
-                                    .build(httpMediaDrmCallback);
-                }
-            }
-        } else if (clearKey != null && !clearKey.isEmpty()) {
-            if (Util.SDK_INT < 18) {
-                Log.e(TAG, "Protected content not supported on API levels below 18");
-                drmSessionManager = null;
-            } else {
-                drmSessionManager = new DefaultDrmSessionManager.Builder()
-                        .setUuidAndExoMediaDrmProvider(C.CLEARKEY_UUID, FrameworkMediaDrm.DEFAULT_PROVIDER).
-                                build(new LocalMediaDrmCallback(clearKey.getBytes()));
-            }
-
-        } else {
-            drmSessionManager = null;
-        }
-
-        if (DataSourceUtils.isHTTP(uri)) {
-            dataSourceFactory = getDataSourceFactory(userAgent, headers);
-
-            if (useCache && maxCacheSize > 0 && maxCacheFileSize > 0) {
-                dataSourceFactory =
-                        new CacheDataSourceFactory(context, maxCacheSize, maxCacheFileSize, dataSourceFactory);
-            }
-        } else {
-            dataSourceFactory = new DefaultDataSourceFactory(context, userAgent);
-        }
-
-        MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint, cacheKey, context);
-        if (overriddenDuration != 0) {
-            ClippingMediaSource clippingMediaSource = new ClippingMediaSource(mediaSource, 0, overriddenDuration * 1000);
-            exoPlayer.setMediaSource(clippingMediaSource);
-        } else {
-            exoPlayer.setMediaSource(mediaSource);
-        }
-        exoPlayer.prepare();
-
-        result.success(null);
-    }
-
-
-    public void setupPlayerNotification(Context context, String title, String author,
-                                        String imageUrl, String notificationChannelName,
-                                        String activityName) {
-
-        PlayerNotificationManager.MediaDescriptionAdapter mediaDescriptionAdapter
-                = new PlayerNotificationManager.MediaDescriptionAdapter() {
-            @NonNull
-            @Override
-            public String getCurrentContentTitle(@NonNull Player player) {
-                return title;
-            }
-
-            @Nullable
-            @Override
-            public PendingIntent createCurrentContentIntent(@NonNull Player player) {
-
-                final String packageName = context.getApplicationContext().getPackageName();
-                Intent notificationIntent = new Intent();
-                notificationIntent.setClassName(packageName,
-                        packageName + "." + activityName);
-                notificationIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
-                        | Intent.FLAG_ACTIVITY_SINGLE_TOP);
-                return PendingIntent.getActivity(context, 0,
-                        notificationIntent, 0);
-            }
-
-            @Nullable
-            @Override
-            public String getCurrentContentText(@NonNull Player player) {
-                return author;
-            }
-
-            @Nullable
-            @Override
-            public Bitmap getCurrentLargeIcon(@NonNull Player player,
-                                              @NonNull PlayerNotificationManager.BitmapCallback callback) {
-                if (imageUrl == null) {
-                    return null;
-                }
-                if (bitmap != null) {
-                    return bitmap;
-                }
-
-
-                OneTimeWorkRequest imageWorkRequest = new OneTimeWorkRequest.Builder(ImageWorker.class)
-                        .addTag(imageUrl)
-                        .setInputData(
-                                new Data.Builder()
-                                        .putString(BetterPlayerPlugin.URL_PARAMETER, imageUrl)
-                                        .build())
-                        .build();
-
-                workManager.enqueue(imageWorkRequest);
-
-                Observer<WorkInfo> workInfoObserver = workInfo -> {
-                    try {
-                        if (workInfo != null) {
-                            WorkInfo.State state = workInfo.getState();
-                            if (state == WorkInfo.State.SUCCEEDED) {
-
-                                Data outputData = workInfo.getOutputData();
-                                String filePath = outputData.getString(BetterPlayerPlugin.FILE_PATH_PARAMETER);
-                                //Bitmap here is already processed and it's very small, so it won't
-                                //break anything.
-                                bitmap = BitmapFactory.decodeFile(filePath);
-                                callback.onBitmap(bitmap);
-
-                            }
-                            if (state == WorkInfo.State.SUCCEEDED
-                                    || state == WorkInfo.State.CANCELLED
-                                    || state == WorkInfo.State.FAILED) {
-                                final UUID uuid = imageWorkRequest.getId();
-                                Observer<WorkInfo> observer = workerObserverMap.remove(uuid);
-                                if (observer != null) {
-                                    workManager.getWorkInfoByIdLiveData(uuid).removeObserver(observer);
-                                }
-                            }
-                        }
-
-
-                    } catch (Exception exception) {
-                        Log.e(TAG, "Image select error: " + exception);
-                    }
-                };
-
-                final UUID workerUuid = imageWorkRequest.getId();
-                workManager.getWorkInfoByIdLiveData(workerUuid)
-                        .observeForever(workInfoObserver);
-                workerObserverMap.put(workerUuid, workInfoObserver);
-
-                return null;
-            }
-        };
-
-        String playerNotificationChannelName = notificationChannelName;
-        if (notificationChannelName == null) {
-            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
-                int importance = NotificationManager.IMPORTANCE_LOW;
-                NotificationChannel channel = new NotificationChannel(DEFAULT_NOTIFICATION_CHANNEL,
-                        DEFAULT_NOTIFICATION_CHANNEL, importance);
-                channel.setDescription(DEFAULT_NOTIFICATION_CHANNEL);
-                NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
-                notificationManager.createNotificationChannel(channel);
-                playerNotificationChannelName = DEFAULT_NOTIFICATION_CHANNEL;
-            }
-        }
-
-        playerNotificationManager = new PlayerNotificationManager.Builder(context,
-                NOTIFICATION_ID,
-                playerNotificationChannelName,
-                mediaDescriptionAdapter).build();
-        playerNotificationManager.setPlayer(exoPlayer);
-        playerNotificationManager.setUseNextAction(false);
-        playerNotificationManager.setUsePreviousAction(false);
-        playerNotificationManager.setUseStopAction(false);
-
-
-        MediaSessionCompat mediaSession = setupMediaSession(context, false);
-        playerNotificationManager.setMediaSessionToken(mediaSession.getSessionToken());
-
-
-        playerNotificationManager.setControlDispatcher(setupControlDispatcher());
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-            refreshHandler = new Handler();
-            refreshRunnable = () -> {
-                PlaybackStateCompat playbackState;
-                if (exoPlayer.getPlayWhenReady()) {
-                    playbackState = new PlaybackStateCompat.Builder()
-                            .setActions(PlaybackStateCompat.ACTION_SEEK_TO)
-                            .setState(PlaybackStateCompat.STATE_PAUSED, getPosition(), 1.0f)
-                            .build();
-                } else {
-                    playbackState = new PlaybackStateCompat.Builder()
-                            .setActions(PlaybackStateCompat.ACTION_SEEK_TO)
-                            .setState(PlaybackStateCompat.STATE_PLAYING, getPosition(), 1.0f)
-                            .build();
-                }
-
-                mediaSession.setPlaybackState(playbackState);
-                refreshHandler.postDelayed(refreshRunnable, 1000);
-            };
-            refreshHandler.postDelayed(refreshRunnable, 0);
-        }
-
-        exoPlayerEventListener = new Player.Listener() {
-            @Override
-            public void onPlaybackStateChanged(int playbackState) {
-                mediaSession.setMetadata(new MediaMetadataCompat.Builder()
-                        .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, getDuration())
-                        .build());
-            }
-        };
-
-        exoPlayer.addListener(exoPlayerEventListener);
-        exoPlayer.seekTo(0);
-    }
-
-
-    private ControlDispatcher setupControlDispatcher() {
-        return new ControlDispatcher() {
-            @Override
-            public boolean dispatchPrepare(Player player) {
-                return false;
-            }
-
-            @Override
-            public boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady) {
-                if (player.getPlayWhenReady()) {
-                    sendEvent("pause");
-                } else {
-                    sendEvent("play");
-                }
-                return true;
-            }
-
-            @Override
-            public boolean dispatchSeekTo(Player player, int windowIndex, long positionMs) {
-                sendSeekToEvent(positionMs);
-                return true;
-            }
-
-            @Override
-            public boolean dispatchPrevious(Player player) {
-                return false;
-            }
-
-            @Override
-            public boolean dispatchNext(Player player) {
-                return false;
-            }
-
-            @Override
-            public boolean dispatchRewind(Player player) {
-                sendSeekToEvent(player.getCurrentPosition() - 5000);
-                return false;
-            }
-
-            @Override
-            public boolean dispatchFastForward(Player player) {
-                sendSeekToEvent(player.getCurrentPosition() + 5000);
-                return true;
-            }
-
-            @Override
-            public boolean dispatchSetRepeatMode(Player player, int repeatMode) {
-                return false;
-            }
-
-            @Override
-            public boolean dispatchSetShuffleModeEnabled(Player player, boolean shuffleModeEnabled) {
-                return false;
-            }
-
-            @Override
-            public boolean dispatchStop(Player player, boolean reset) {
-                return false;
-            }
-
-            @Override
-            public boolean dispatchSetPlaybackParameters(Player player, PlaybackParameters playbackParameters) {
-                return false;
-            }
-
-            @Override
-            public boolean isRewindEnabled() {
-                return true;
-            }
-
-            @Override
-            public boolean isFastForwardEnabled() {
-                return true;
-            }
-        };
-    }
-
-
-    public void disposeRemoteNotifications() {
-        if (exoPlayerEventListener != null) {
-            exoPlayer.removeListener(exoPlayerEventListener);
-        }
-        if (refreshHandler != null) {
-            refreshHandler.removeCallbacksAndMessages(null);
-            refreshHandler = null;
-            refreshRunnable = null;
-        }
-        if (playerNotificationManager != null) {
-            playerNotificationManager.setPlayer(null);
-        }
-        bitmap = null;
-    }
-
-
-    private MediaSource buildMediaSource(
-            Uri uri, DataSource.Factory mediaDataSourceFactory, String formatHint, String cacheKey,
-            Context context) {
-        int type;
-        if (formatHint == null) {
-            String lastPathSegment = uri.getLastPathSegment();
-            if (lastPathSegment == null) {
-                lastPathSegment = "";
-            }
-            type = Util.inferContentType(lastPathSegment);
-        } else {
-            switch (formatHint) {
-                case FORMAT_SS:
-                    type = C.TYPE_SS;
-                    break;
-                case FORMAT_DASH:
-                    type = C.TYPE_DASH;
-                    break;
-                case FORMAT_HLS:
-                    type = C.TYPE_HLS;
-                    break;
-                case FORMAT_OTHER:
-                    type = C.TYPE_OTHER;
-                    break;
-                default:
-                    type = -1;
-                    break;
-            }
-        }
-        MediaItem.Builder mediaItemBuilder = new MediaItem.Builder();
-        mediaItemBuilder.setUri(uri);
-        if (cacheKey != null && cacheKey.length() > 0) {
-            mediaItemBuilder.setCustomCacheKey(cacheKey);
-        }
-        MediaItem mediaItem = mediaItemBuilder.build();
-        switch (type) {
-
-            case C.TYPE_SS:
-                return new SsMediaSource.Factory(
-                        new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
-                        new DefaultDataSourceFactory(context, null, mediaDataSourceFactory))
-                        .setDrmSessionManager(drmSessionManager)
-                        .createMediaSource(mediaItem);
-            case C.TYPE_DASH:
-                return new DashMediaSource.Factory(
-                        new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
-                        new DefaultDataSourceFactory(context, null, mediaDataSourceFactory))
-                        .setDrmSessionManager(drmSessionManager)
-                        .createMediaSource(mediaItem);
-            case C.TYPE_HLS:
-                return new HlsMediaSource.Factory(mediaDataSourceFactory)
-                        .setDrmSessionManager(drmSessionManager)
-                        .createMediaSource(mediaItem);
-            case C.TYPE_OTHER:
-                return new ProgressiveMediaSource.Factory(mediaDataSourceFactory,
-                        new DefaultExtractorsFactory())
-                        .setDrmSessionManager(drmSessionManager)
-                        .createMediaSource(mediaItem);
-            default: {
-                throw new IllegalStateException("Unsupported type: " + type);
-            }
-        }
-    }
-
-    private void setupVideoPlayer(
-            EventChannel eventChannel, TextureRegistry.SurfaceTextureEntry textureEntry, Result result) {
-
-        eventChannel.setStreamHandler(
-                new EventChannel.StreamHandler() {
-                    @Override
-                    public void onListen(Object o, EventChannel.EventSink sink) {
-                        eventSink.setDelegate(sink);
-                    }
-
-                    @Override
-                    public void onCancel(Object o) {
-                        eventSink.setDelegate(null);
-                    }
-                });
-
-        surface = new Surface(textureEntry.surfaceTexture());
-        exoPlayer.setVideoSurface(surface);
-        setAudioAttributes(exoPlayer, true);
-
-        exoPlayer.addListener(new Player.Listener() {
-            @Override
-            public void onPlaybackStateChanged(int playbackState) {
-
-                if (playbackState == Player.STATE_BUFFERING) {
-                    sendBufferingUpdate(true);
-                    Map<String, Object> event = new HashMap<>();
-                    event.put("event", "bufferingStart");
-                    eventSink.success(event);
-                } else if (playbackState == Player.STATE_READY) {
-                    if (!isInitialized) {
-                        isInitialized = true;
-                        sendInitialized();
-                    }
-
-                    Map<String, Object> event = new HashMap<>();
-                    event.put("event", "bufferingEnd");
-                    eventSink.success(event);
-
-                } else if (playbackState == Player.STATE_ENDED) {
-                    Map<String, Object> event = new HashMap<>();
-                    event.put("event", "completed");
-                    event.put("key", key);
-                    eventSink.success(event);
-                }
-            }
-
-            @Override
-            public void onPlayerError(final ExoPlaybackException error) {
-                eventSink.error("VideoError", "Video player had error " + error, null);
-            }
-        });
-
-        Map<String, Object> reply = new HashMap<>();
-        reply.put("textureId", textureEntry.id());
-        result.success(reply);
-    }
-
-    void sendBufferingUpdate(boolean isFromBufferingStart) {
-        long bufferedPosition = exoPlayer.getBufferedPosition();
-        if (isFromBufferingStart || bufferedPosition != lastSendBufferedPosition) {
-            Map<String, Object> event = new HashMap<>();
-            event.put("event", "bufferingUpdate");
-            List<? extends Number> range = Arrays.asList(0, bufferedPosition);
-            // iOS supports a list of buffered ranges, so here is a list with a single range.
-            event.put("values", Collections.singletonList(range));
-            eventSink.success(event);
-            lastSendBufferedPosition = bufferedPosition;
-        }
-    }
-
-    private void setAudioAttributes(SimpleExoPlayer exoPlayer, Boolean mixWithOthers) {
-        ExoPlayer.AudioComponent audioComponent = exoPlayer.getAudioComponent();
-        if (audioComponent == null) {
-            return;
-        }
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-
-            audioComponent.setAudioAttributes(
-                    new AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MOVIE).build(), !mixWithOthers);
-        } else {
-            audioComponent.setAudioAttributes(
-                    new AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MUSIC).build(), !mixWithOthers);
-        }
-    }
-
-    void play() {
-        exoPlayer.setPlayWhenReady(true);
-    }
-
-    void pause() {
-        exoPlayer.setPlayWhenReady(false);
-    }
-
-    void setLooping(boolean value) {
-        exoPlayer.setRepeatMode(value ? REPEAT_MODE_ALL : REPEAT_MODE_OFF);
-    }
-
-    void setVolume(double value) {
-        float bracketedValue = (float) Math.max(0.0, Math.min(1.0, value));
-        exoPlayer.setVolume(bracketedValue);
-    }
-
-    void setSpeed(double value) {
-        float bracketedValue = (float) value;
-        PlaybackParameters playbackParameters = new PlaybackParameters(bracketedValue);
-        exoPlayer.setPlaybackParameters(playbackParameters);
-    }
-
-    void setTrackParameters(int width, int height, int bitrate) {
-        DefaultTrackSelector.ParametersBuilder parametersBuilder = trackSelector.buildUponParameters();
-        if (width != 0 && height != 0) {
-            parametersBuilder.setMaxVideoSize(width, height);
-        }
-        if (bitrate != 0) {
-            parametersBuilder.setMaxVideoBitrate(bitrate);
-        }
-        if (width == 0 && height == 0 && bitrate == 0) {
-            parametersBuilder.clearVideoSizeConstraints();
-            parametersBuilder.setMaxVideoBitrate(Integer.MAX_VALUE);
-        }
-
-        trackSelector.setParameters(parametersBuilder);
-    }
-
-    void seekTo(int location) {
-        exoPlayer.seekTo(location);
-    }
-
-    long getPosition() {
-        return exoPlayer.getCurrentPosition();
-    }
-
-    long getAbsolutePosition() {
-        Timeline timeline = exoPlayer.getCurrentTimeline();
-        if (!timeline.isEmpty()) {
-            long windowStartTimeMs = timeline.getWindow(0, new Timeline.Window()).windowStartTimeMs;
-            long pos = exoPlayer.getCurrentPosition();
-            return (windowStartTimeMs + pos);
-        }
-        return exoPlayer.getCurrentPosition();
-    }
-
-    @SuppressWarnings("SuspiciousNameCombination")
-    private void sendInitialized() {
-        if (isInitialized) {
-            Map<String, Object> event = new HashMap<>();
-            event.put("event", "initialized");
-            event.put("key", key);
-            event.put("duration", getDuration());
-
-            if (exoPlayer.getVideoFormat() != null) {
-                Format videoFormat = exoPlayer.getVideoFormat();
-                int width = videoFormat.width;
-                int height = videoFormat.height;
-                int rotationDegrees = videoFormat.rotationDegrees;
-                // Switch the width/height if video was taken in portrait mode
-                if (rotationDegrees == 90 || rotationDegrees == 270) {
-                    width = exoPlayer.getVideoFormat().height;
-                    height = exoPlayer.getVideoFormat().width;
-                }
-                event.put("width", width);
-                event.put("height", height);
-            }
-            eventSink.success(event);
-        }
-    }
-
-    private long getDuration() {
-        return exoPlayer.getDuration();
-    }
-
-    /**
-     * Create media session which will be used in notifications, pip mode.
-     *
-     * @param context                - android context
-     * @param setupControlDispatcher - should add control dispatcher to created MediaSession
-     * @return - configured MediaSession instance
-     */
-    public MediaSessionCompat setupMediaSession(Context context, boolean setupControlDispatcher) {
-        if (mediaSession != null) {
-            mediaSession.release();
-        }
-        ComponentName mediaButtonReceiver = new ComponentName(context, MediaButtonReceiver.class);
-        MediaSessionCompat mediaSession = new MediaSessionCompat(context, "BetterPlayer", mediaButtonReceiver, null);
-        mediaSession.setCallback(new MediaSessionCompat.Callback() {
-            @Override
-            public void onSeekTo(long pos) {
-                sendSeekToEvent(pos);
-                super.onSeekTo(pos);
-            }
-        });
-
-        mediaSession.setActive(true);
-        MediaSessionConnector mediaSessionConnector =
-                new MediaSessionConnector(mediaSession);
-        if (setupControlDispatcher) {
-            mediaSessionConnector.setControlDispatcher(setupControlDispatcher());
-        }
-        mediaSessionConnector.setPlayer(exoPlayer);
-
-        Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
-        mediaButtonIntent.setClass(context, MediaButtonReceiver.class);
-        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, mediaButtonIntent, 0);
-        mediaSession.setMediaButtonReceiver(pendingIntent);
-
-
-        this.mediaSession = mediaSession;
-        return mediaSession;
-    }
-
-    public void onPictureInPictureStatusChanged(boolean inPip) {
-        Map<String, Object> event = new HashMap<>();
-        event.put("event", inPip ? "pipStart" : "pipStop");
-        eventSink.success(event);
-    }
-
-    public void disposeMediaSession() {
-        if (mediaSession != null) {
-            mediaSession.release();
-        }
-        mediaSession = null;
-    }
-
-    private void sendEvent(String eventType) {
-        Map<String, Object> event = new HashMap<>();
-        event.put("event", eventType);
-        eventSink.success(event);
-    }
-
-    void setAudioTrack(String name, Integer index) {
-        try {
-            MappingTrackSelector.MappedTrackInfo mappedTrackInfo =
-                    trackSelector.getCurrentMappedTrackInfo();
-
-            if (mappedTrackInfo != null) {
-                for (int rendererIndex = 0; rendererIndex < mappedTrackInfo.getRendererCount();
-                     rendererIndex++) {
-                    if (mappedTrackInfo.getRendererType(rendererIndex) != C.TRACK_TYPE_AUDIO) {
-                        continue;
-                    }
-                    TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex);
-                    boolean hasElementWithoutLabel = false;
-                    boolean hasStrangeAudioTrack = false;
-                    for (int groupIndex = 0; groupIndex < trackGroupArray.length; groupIndex++) {
-                        TrackGroup group = trackGroupArray.get(groupIndex);
-                        for (int groupElementIndex = 0; groupElementIndex < group.length; groupElementIndex++) {
-                            Format format = group.getFormat(groupElementIndex);
-
-                            if (format.label == null) {
-                                hasElementWithoutLabel = true;
-                            }
-
-                            if (format.id != null && format.id.equals("1/15")) {
-                                hasStrangeAudioTrack = true;
-                            }
-                        }
-                    }
-
-                    for (int groupIndex = 0; groupIndex < trackGroupArray.length; groupIndex++) {
-                        TrackGroup group = trackGroupArray.get(groupIndex);
-                        for (int groupElementIndex = 0; groupElementIndex < group.length; groupElementIndex++) {
-                            String label = group.getFormat(groupElementIndex).label;
-
-                            if (name.equals(label) && index == groupIndex) {
-                                setAudioTrack(rendererIndex, groupIndex, groupElementIndex);
-                                return;
-                            }
-
-                            ///Fallback option
-                            if (!hasStrangeAudioTrack && hasElementWithoutLabel && index == groupIndex) {
-                                setAudioTrack(rendererIndex, groupIndex, groupElementIndex);
-                                return;
-                            }
-                            ///Fallback option
-                            if (hasStrangeAudioTrack && name.equals(label)) {
-                                setAudioTrack(rendererIndex, groupIndex, groupElementIndex);
-                                return;
-                            }
-                        }
-                    }
-                }
-            }
-        } catch (Exception exception) {
-            Log.e(TAG, "setAudioTrack failed" + exception.toString());
-        }
-    }
-
-    private void setAudioTrack(int rendererIndex, int groupIndex, int groupElementIndex) {
-        MappingTrackSelector.MappedTrackInfo mappedTrackInfo =
-                trackSelector.getCurrentMappedTrackInfo();
-        if (mappedTrackInfo != null) {
-            DefaultTrackSelector.ParametersBuilder builder =
-                    trackSelector.getParameters().buildUpon();
-            builder.clearSelectionOverrides(rendererIndex)
-                    .setRendererDisabled(rendererIndex, false);
-            int[] tracks = {groupElementIndex};
-            DefaultTrackSelector.SelectionOverride override =
-                    new DefaultTrackSelector.SelectionOverride(groupIndex, tracks);
-            builder.setSelectionOverride(rendererIndex,
-                    mappedTrackInfo.getTrackGroups(rendererIndex), override);
-            trackSelector.setParameters(builder);
-        }
-    }
-
-    private void sendSeekToEvent(long positionMs) {
-        exoPlayer.seekTo(positionMs);
-        Map<String, Object> event = new HashMap<>();
-        event.put("event", "seek");
-        event.put("position", positionMs);
-        eventSink.success(event);
-    }
-
-    public void setMixWithOthers(Boolean mixWithOthers) {
-        setAudioAttributes(exoPlayer, mixWithOthers);
-    }
-
-    //Clear cache without accessing BetterPlayerCache.
-    @SuppressWarnings("ResultOfMethodCallIgnored")
-    public static void clearCache(Context context, Result result) {
-        try {
-            File file = new File(context.getCacheDir(), "betterPlayerCache");
-            deleteDirectory(file);
-            result.success(null);
-        } catch (Exception exception) {
-            Log.e(TAG, exception.toString());
-            result.error("", "", "");
-        }
-    }
-
-    private static void deleteDirectory(File file) {
-        if (file.isDirectory()) {
-            File[] entries = file.listFiles();
-            if (entries != null) {
-                for (File entry : entries) {
-                    deleteDirectory(entry);
-                }
-            }
-        }
-        if (!file.delete()) {
-            Log.e(TAG, "Failed to delete cache dir.");
-        }
-    }
-
-
-    //Start pre cache of video. Invoke work manager job and start caching in background.
-    static void preCache(Context context, String dataSource, long preCacheSize,
-                         long maxCacheSize, long maxCacheFileSize, Map<String, String> headers,
-                         String cacheKey, Result result) {
-        Data.Builder dataBuilder = new Data.Builder()
-                .putString(BetterPlayerPlugin.URL_PARAMETER, dataSource)
-                .putLong(BetterPlayerPlugin.PRE_CACHE_SIZE_PARAMETER, preCacheSize)
-                .putLong(BetterPlayerPlugin.MAX_CACHE_SIZE_PARAMETER, maxCacheSize)
-                .putLong(BetterPlayerPlugin.MAX_CACHE_FILE_SIZE_PARAMETER, maxCacheFileSize);
-
-        if (cacheKey != null) {
-            dataBuilder.putString(BetterPlayerPlugin.CACHE_KEY_PARAMETER, cacheKey);
-        }
-        for (String headerKey : headers.keySet()) {
-            dataBuilder.putString(BetterPlayerPlugin.HEADER_PARAMETER + headerKey, headers.get(headerKey));
-        }
-
-        OneTimeWorkRequest cacheWorkRequest = new OneTimeWorkRequest.Builder(CacheWorker.class)
-                .addTag(dataSource)
-                .setInputData(dataBuilder.build()).build();
-        WorkManager.getInstance(context).enqueue(cacheWorkRequest);
-        result.success(null);
-    }
-
-    //Stop pre cache of video with given url. If there's no work manager job for given url, then
-    //it will be ignored.
-    static void stopPreCache(Context context, String url, Result result) {
-        WorkManager.getInstance(context).cancelAllWorkByTag(url);
-        result.success(null);
-    }
-
-    void dispose() {
-        disposeMediaSession();
-        disposeRemoteNotifications();
-        if (isInitialized) {
-            exoPlayer.stop();
-        }
-        textureEntry.release();
-        eventChannel.setStreamHandler(null);
-        if (surface != null) {
-            surface.release();
-        }
-        if (exoPlayer != null) {
-            exoPlayer.release();
-        }
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-
-        BetterPlayer that = (BetterPlayer) o;
-
-        if (exoPlayer != null ? !exoPlayer.equals(that.exoPlayer) : that.exoPlayer != null)
-            return false;
-        return surface != null ? surface.equals(that.surface) : that.surface == null;
-    }
-
-    @Override
-    public int hashCode() {
-        int result = exoPlayer != null ? exoPlayer.hashCode() : 0;
-        result = 31 * result + (surface != null ? surface.hashCode() : 0);
-        return result;
-    }
-
-}
-
-
-
diff --git a/android/src/main/java/com/jhomlala/better_player/BetterPlayerCache.java b/android/src/main/java/com/jhomlala/better_player/BetterPlayerCache.java
deleted file mode 100644
index c4af52bd3..000000000
--- a/android/src/main/java/com/jhomlala/better_player/BetterPlayerCache.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.jhomlala.better_player;
-
-import android.content.Context;
-import android.util.Log;
-
-import com.google.android.exoplayer2.database.ExoDatabaseProvider;
-import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
-import com.google.android.exoplayer2.upstream.cache.SimpleCache;
-
-import java.io.File;
-
-public class BetterPlayerCache {
-    private static volatile SimpleCache instance;
-
-    public static SimpleCache createCache(Context context, long cacheFileSize) {
-        if (instance == null) {
-            synchronized (BetterPlayerCache.class) {
-                if (instance == null) {
-                    instance = new SimpleCache(
-                            new File(context.getCacheDir(), "betterPlayerCache"),
-                            new LeastRecentlyUsedCacheEvictor(cacheFileSize),
-                            new ExoDatabaseProvider(context));
-                }
-            }
-        }
-        return instance;
-    }
-
-    public static void releaseCache() {
-        try {
-            if (instance != null) {
-                instance.release();
-                instance = null;
-            }
-        } catch (Exception exception) {
-            Log.e("BetterPlayerCache", exception.toString());
-        }
-    }
-}
diff --git a/android/src/main/java/com/jhomlala/better_player/BetterPlayerPlugin.java b/android/src/main/java/com/jhomlala/better_player/BetterPlayerPlugin.java
deleted file mode 100644
index 885700a78..000000000
--- a/android/src/main/java/com/jhomlala/better_player/BetterPlayerPlugin.java
+++ /dev/null
@@ -1,566 +0,0 @@
-// Copyright 2017 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-package com.jhomlala.better_player;
-
-import android.app.Activity;
-import android.app.PictureInPictureParams;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.os.Build;
-import android.os.Handler;
-import android.util.Log;
-import android.util.LongSparseArray;
-
-import androidx.annotation.NonNull;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import io.flutter.embedding.engine.loader.FlutterLoader;
-import io.flutter.embedding.engine.plugins.FlutterPlugin;
-import io.flutter.embedding.engine.plugins.activity.ActivityAware;
-import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
-import io.flutter.plugin.common.BinaryMessenger;
-import io.flutter.plugin.common.EventChannel;
-import io.flutter.plugin.common.MethodCall;
-import io.flutter.plugin.common.MethodChannel;
-import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
-import io.flutter.plugin.common.MethodChannel.Result;
-import io.flutter.view.TextureRegistry;
-
-/**
- * Android platform implementation of the VideoPlayerPlugin.
- */
-public class BetterPlayerPlugin implements FlutterPlugin, ActivityAware, MethodCallHandler {
-    private static final String TAG = "BetterPlayerPlugin";
-    private static final String CHANNEL = "better_player_channel";
-    private static final String EVENTS_CHANNEL = "better_player_channel/videoEvents";
-    private static final String DATA_SOURCE_PARAMETER = "dataSource";
-    private static final String KEY_PARAMETER = "key";
-    private static final String HEADERS_PARAMETER = "headers";
-    private static final String USE_CACHE_PARAMETER = "useCache";
-
-
-    private static final String ASSET_PARAMETER = "asset";
-    private static final String PACKAGE_PARAMETER = "package";
-    private static final String URI_PARAMETER = "uri";
-    private static final String FORMAT_HINT_PARAMETER = "formatHint";
-    private static final String TEXTURE_ID_PARAMETER = "textureId";
-    private static final String LOOPING_PARAMETER = "looping";
-    private static final String VOLUME_PARAMETER = "volume";
-    private static final String LOCATION_PARAMETER = "location";
-    private static final String SPEED_PARAMETER = "speed";
-    private static final String WIDTH_PARAMETER = "width";
-    private static final String HEIGHT_PARAMETER = "height";
-    private static final String BITRATE_PARAMETER = "bitrate";
-    private static final String SHOW_NOTIFICATION_PARAMETER = "showNotification";
-    private static final String TITLE_PARAMETER = "title";
-    private static final String AUTHOR_PARAMETER = "author";
-    private static final String IMAGE_URL_PARAMETER = "imageUrl";
-    private static final String NOTIFICATION_CHANNEL_NAME_PARAMETER = "notificationChannelName";
-    private static final String OVERRIDDEN_DURATION_PARAMETER = "overriddenDuration";
-    private static final String NAME_PARAMETER = "name";
-    private static final String INDEX_PARAMETER = "index";
-    private static final String LICENSE_URL_PARAMETER = "licenseUrl";
-    private static final String DRM_HEADERS_PARAMETER = "drmHeaders";
-    private static final String DRM_CLEARKEY_PARAMETER = "clearKey";
-    private static final String MIX_WITH_OTHERS_PARAMETER = "mixWithOthers";
-    public static final String URL_PARAMETER = "url";
-    public static final String PRE_CACHE_SIZE_PARAMETER = "preCacheSize";
-    public static final String MAX_CACHE_SIZE_PARAMETER = "maxCacheSize";
-    public static final String MAX_CACHE_FILE_SIZE_PARAMETER = "maxCacheFileSize";
-    public static final String HEADER_PARAMETER = "header_";
-    public static final String FILE_PATH_PARAMETER = "filePath";
-    public static final String ACTIVITY_NAME_PARAMETER = "activityName";
-    public static final String MIN_BUFFER_MS = "minBufferMs";
-    public static final String MAX_BUFFER_MS = "maxBufferMs";
-    public static final String BUFFER_FOR_PLAYBACK_MS = "bufferForPlaybackMs";
-    public static final String BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = "bufferForPlaybackAfterRebufferMs";
-    public static final String CACHE_KEY_PARAMETER = "cacheKey";
-
-
-    private static final String INIT_METHOD = "init";
-    private static final String CREATE_METHOD = "create";
-    private static final String SET_DATA_SOURCE_METHOD = "setDataSource";
-    private static final String SET_LOOPING_METHOD = "setLooping";
-    private static final String SET_VOLUME_METHOD = "setVolume";
-    private static final String PLAY_METHOD = "play";
-    private static final String PAUSE_METHOD = "pause";
-    private static final String SEEK_TO_METHOD = "seekTo";
-    private static final String POSITION_METHOD = "position";
-    private static final String ABSOLUTE_POSITION_METHOD = "absolutePosition";
-    private static final String SET_SPEED_METHOD = "setSpeed";
-    private static final String SET_TRACK_PARAMETERS_METHOD = "setTrackParameters";
-    private static final String SET_AUDIO_TRACK_METHOD = "setAudioTrack";
-    private static final String ENABLE_PICTURE_IN_PICTURE_METHOD = "enablePictureInPicture";
-    private static final String DISABLE_PICTURE_IN_PICTURE_METHOD = "disablePictureInPicture";
-    private static final String IS_PICTURE_IN_PICTURE_SUPPORTED_METHOD = "isPictureInPictureSupported";
-    private static final String SET_MIX_WITH_OTHERS_METHOD = "setMixWithOthers";
-    private static final String CLEAR_CACHE_METHOD = "clearCache";
-    private static final String DISPOSE_METHOD = "dispose";
-    private static final String PRE_CACHE_METHOD = "preCache";
-    private static final String STOP_PRE_CACHE_METHOD = "stopPreCache";
-
-    private final LongSparseArray<BetterPlayer> videoPlayers = new LongSparseArray<>();
-    private final LongSparseArray<Map<String, Object>> dataSources = new LongSparseArray<>();
-    private FlutterState flutterState;
-    private long currentNotificationTextureId = -1;
-    private Map<String, Object> currentNotificationDataSource;
-    private Activity activity;
-    private Handler pipHandler;
-    private Runnable pipRunnable;
-
-    @Override
-    public void onAttachedToEngine(FlutterPluginBinding binding) {
-        FlutterLoader loader = new FlutterLoader();
-        this.flutterState =
-                new FlutterState(
-                        binding.getApplicationContext(),
-                        binding.getBinaryMessenger(),
-                        loader::getLookupKeyForAsset,
-                        loader::getLookupKeyForAsset,
-                        binding.getTextureRegistry());
-        flutterState.startListening(this);
-    }
-
-    @Override
-    public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
-        if (flutterState == null) {
-            Log.wtf(TAG, "Detached from the engine before registering to it.");
-        }
-        disposeAllPlayers();
-        BetterPlayerCache.releaseCache();
-        flutterState.stopListening();
-        flutterState = null;
-    }
-
-    private void disposeAllPlayers() {
-        for (int i = 0; i < videoPlayers.size(); i++) {
-            videoPlayers.valueAt(i).dispose();
-        }
-        videoPlayers.clear();
-        dataSources.clear();
-    }
-
-
-    @Override
-    public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
-        if (flutterState == null || flutterState.textureRegistry == null) {
-            result.error("no_activity", "better_player plugin requires a foreground activity", null);
-            return;
-        }
-
-        switch (call.method) {
-            case INIT_METHOD:
-                disposeAllPlayers();
-                break;
-            case CREATE_METHOD: {
-                TextureRegistry.SurfaceTextureEntry handle =
-                        flutterState.textureRegistry.createSurfaceTexture();
-
-                EventChannel eventChannel =
-                        new EventChannel(
-                                flutterState.binaryMessenger, EVENTS_CHANNEL + handle.id());
-                CustomDefaultLoadControl customDefaultLoadControl = null;
-                if (call.hasArgument(MIN_BUFFER_MS) && call.hasArgument(MAX_BUFFER_MS) &&
-                        call.hasArgument(BUFFER_FOR_PLAYBACK_MS) &&
-                        call.hasArgument(BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS)) {
-
-                    customDefaultLoadControl =
-                            new CustomDefaultLoadControl(call.argument(MIN_BUFFER_MS),
-                                    call.argument(MAX_BUFFER_MS),
-                                    call.argument(BUFFER_FOR_PLAYBACK_MS),
-                                    call.argument(BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS)
-                            );
-                }
-
-                BetterPlayer player =
-                        new BetterPlayer(flutterState.applicationContext, eventChannel, handle,
-                                customDefaultLoadControl, result);
-
-                videoPlayers.put(handle.id(), player);
-                break;
-            }
-            case PRE_CACHE_METHOD:
-                preCache(call, result);
-                break;
-            case STOP_PRE_CACHE_METHOD:
-                stopPreCache(call, result);
-                break;
-            case CLEAR_CACHE_METHOD:
-                clearCache(result);
-                break;
-            default: {
-                long textureId = ((Number) call.argument(TEXTURE_ID_PARAMETER)).longValue();
-                BetterPlayer player = videoPlayers.get(textureId);
-
-                if (player == null) {
-                    result.error(
-                            "Unknown textureId",
-                            "No video player associated with texture id " + textureId,
-                            null);
-                    return;
-                }
-                onMethodCall(call, result, textureId, player);
-                break;
-            }
-        }
-    }
-
-    private void onMethodCall(MethodCall call, Result result, long textureId, BetterPlayer player) {
-        switch (call.method) {
-            case SET_DATA_SOURCE_METHOD: {
-                setDataSource(call, result, player);
-                break;
-            }
-            case SET_LOOPING_METHOD:
-                player.setLooping(call.argument(LOOPING_PARAMETER));
-                result.success(null);
-                break;
-            case SET_VOLUME_METHOD:
-                player.setVolume(call.argument(VOLUME_PARAMETER));
-                result.success(null);
-                break;
-            case PLAY_METHOD:
-                setupNotification(player);
-                player.play();
-                result.success(null);
-                break;
-            case PAUSE_METHOD:
-                player.pause();
-                result.success(null);
-                break;
-            case SEEK_TO_METHOD:
-                int location = ((Number) call.argument(LOCATION_PARAMETER)).intValue();
-                player.seekTo(location);
-                result.success(null);
-                break;
-            case POSITION_METHOD:
-                result.success(player.getPosition());
-                player.sendBufferingUpdate(false);
-                break;
-            case ABSOLUTE_POSITION_METHOD:
-                result.success(player.getAbsolutePosition());
-                break;
-            case SET_SPEED_METHOD:
-                player.setSpeed(call.argument(SPEED_PARAMETER));
-                result.success(null);
-                break;
-            case SET_TRACK_PARAMETERS_METHOD:
-                player.setTrackParameters(
-                        call.argument(WIDTH_PARAMETER),
-                        call.argument(HEIGHT_PARAMETER),
-                        call.argument(BITRATE_PARAMETER));
-                result.success(null);
-                break;
-            case ENABLE_PICTURE_IN_PICTURE_METHOD:
-                enablePictureInPicture(player);
-                result.success(null);
-                break;
-
-            case DISABLE_PICTURE_IN_PICTURE_METHOD:
-                disablePictureInPicture(player);
-                result.success(null);
-                break;
-
-            case IS_PICTURE_IN_PICTURE_SUPPORTED_METHOD:
-                result.success(isPictureInPictureSupported());
-                break;
-
-            case SET_AUDIO_TRACK_METHOD:
-                player.setAudioTrack(call.argument(NAME_PARAMETER), call.argument(INDEX_PARAMETER));
-                result.success(null);
-                break;
-            case SET_MIX_WITH_OTHERS_METHOD:
-                player.setMixWithOthers(call.argument(MIX_WITH_OTHERS_PARAMETER));
-                break;
-            case DISPOSE_METHOD:
-                dispose(player, textureId);
-                result.success(null);
-                break;
-            default:
-                result.notImplemented();
-                break;
-        }
-    }
-
-
-    private void setDataSource(MethodCall call, Result result, BetterPlayer player) {
-        Map<String, Object> dataSource = call.argument(DATA_SOURCE_PARAMETER);
-
-        dataSources.put(getTextureId(player), dataSource);
-        String key = getParameter(dataSource, KEY_PARAMETER, "");
-        Map<String, String> headers = getParameter(dataSource, HEADERS_PARAMETER, new HashMap<>());
-        Number overriddenDuration = getParameter(dataSource, OVERRIDDEN_DURATION_PARAMETER, 0);
-
-        if (dataSource.get(ASSET_PARAMETER) != null) {
-            String asset = getParameter(dataSource, ASSET_PARAMETER, "");
-            String assetLookupKey;
-            if (dataSource.get(PACKAGE_PARAMETER) != null) {
-                String packageParameter = getParameter(dataSource, PACKAGE_PARAMETER, "");
-                assetLookupKey =
-                        flutterState.keyForAssetAndPackageName.get(asset, packageParameter);
-            } else {
-                assetLookupKey = flutterState.keyForAsset.get(asset);
-            }
-
-                       player.setDataSource(
-                    flutterState.applicationContext,
-                    key,
-                    "asset:///" + assetLookupKey,
-                    null,
-                    result,
-                    headers,
-                    false,
-                    0L,
-                    0L,
-                    overriddenDuration.longValue(),
-                    null,
-                    null, null, null
-            );
-        } else {
-            boolean useCache = getParameter(dataSource, USE_CACHE_PARAMETER, false);
-            Number maxCacheSizeNumber = getParameter(dataSource, MAX_CACHE_SIZE_PARAMETER, 0);
-            Number maxCacheFileSizeNumber = getParameter(dataSource, MAX_CACHE_FILE_SIZE_PARAMETER, 0);
-            long maxCacheSize = maxCacheSizeNumber.longValue();
-            long maxCacheFileSize = maxCacheFileSizeNumber.longValue();
-            String uri = getParameter(dataSource, URI_PARAMETER, "");
-            String cacheKey = getParameter(dataSource, CACHE_KEY_PARAMETER, null);
-            String formatHint = getParameter(dataSource, FORMAT_HINT_PARAMETER, null);
-            String licenseUrl = getParameter(dataSource, LICENSE_URL_PARAMETER, null);
-            String clearKey = getParameter(dataSource, DRM_CLEARKEY_PARAMETER, null);
-            Map<String, String> drmHeaders = getParameter(dataSource, DRM_HEADERS_PARAMETER, new HashMap<>());
-            player.setDataSource(
-                    flutterState.applicationContext,
-                    key,
-                    uri,
-                    formatHint,
-                    result,
-                    headers,
-                    useCache,
-                    maxCacheSize,
-                    maxCacheFileSize,
-                    overriddenDuration.longValue(),
-                    licenseUrl,
-                    drmHeaders,
-                    cacheKey,
-                    clearKey
-            );
-        }
-    }
-
-    /**
-     * Start pre cache of video.
-     *
-     * @param call   - invoked method data
-     * @param result - result which should be updated
-     */
-    private void preCache(MethodCall call, Result result) {
-        Map<String, Object> dataSource = call.argument(DATA_SOURCE_PARAMETER);
-        if (dataSource != null) {
-            Number maxCacheSizeNumber = getParameter(dataSource, MAX_CACHE_SIZE_PARAMETER, 100 * 1024 * 1024);
-            Number maxCacheFileSizeNumber = getParameter(dataSource, MAX_CACHE_FILE_SIZE_PARAMETER, 10 * 1024 * 1024);
-            long maxCacheSize = maxCacheSizeNumber.longValue();
-            long maxCacheFileSize = maxCacheFileSizeNumber.longValue();
-            Number preCacheSizeNumber = getParameter(dataSource, PRE_CACHE_SIZE_PARAMETER, 3 * 1024 * 1024);
-            long preCacheSize = preCacheSizeNumber.longValue();
-            String uri = getParameter(dataSource, URI_PARAMETER, "");
-            String cacheKey = getParameter(dataSource, CACHE_KEY_PARAMETER, null);
-            Map<String, String> headers = getParameter(dataSource, HEADERS_PARAMETER, new HashMap<>());
-
-            BetterPlayer.preCache(flutterState.applicationContext,
-                    uri,
-                    preCacheSize,
-                    maxCacheSize,
-                    maxCacheFileSize,
-                    headers,
-                    cacheKey,
-                    result
-            );
-        }
-    }
-
-    /**
-     * Stop pre cache video process (if exists).
-     *
-     * @param call   - invoked method data
-     * @param result - result which should be updated
-     */
-    private void stopPreCache(MethodCall call, Result result) {
-        String url = call.argument(URL_PARAMETER);
-        BetterPlayer.stopPreCache(flutterState.applicationContext, url, result);
-    }
-
-    private void clearCache(Result result) {
-        BetterPlayer.clearCache(flutterState.applicationContext, result);
-    }
-
-    private Long getTextureId(BetterPlayer betterPlayer) {
-        for (int index = 0; index < videoPlayers.size(); index++) {
-            if (betterPlayer == videoPlayers.valueAt(index)) {
-                return videoPlayers.keyAt(index);
-            }
-        }
-        return null;
-    }
-
-    private void setupNotification(BetterPlayer betterPlayer) {
-        try {
-            Long textureId = getTextureId(betterPlayer);
-            if (textureId != null) {
-                Map<String, Object> dataSource = dataSources.get(textureId);
-                //Don't setup notification for the same source.
-                if (textureId == currentNotificationTextureId
-                        && currentNotificationDataSource != null
-                        && dataSource != null
-                        && currentNotificationDataSource == dataSource) {
-                    return;
-                }
-                currentNotificationDataSource = dataSource;
-                currentNotificationTextureId = textureId;
-                removeOtherNotificationListeners();
-                boolean showNotification = getParameter(dataSource, SHOW_NOTIFICATION_PARAMETER, false);
-                if (showNotification) {
-                    String title = getParameter(dataSource, TITLE_PARAMETER, "");
-                    String author = getParameter(dataSource, AUTHOR_PARAMETER, "");
-                    String imageUrl = getParameter(dataSource, IMAGE_URL_PARAMETER, "");
-                    String notificationChannelName = getParameter(dataSource, NOTIFICATION_CHANNEL_NAME_PARAMETER, null);
-                    String activityName = getParameter(dataSource, ACTIVITY_NAME_PARAMETER, "MainActivity");
-                    betterPlayer.setupPlayerNotification(flutterState.applicationContext,
-                            title, author, imageUrl, notificationChannelName, activityName);
-                }
-            }
-        } catch (Exception exception) {
-            Log.e(TAG, "SetupNotification failed", exception);
-        }
-    }
-
-    private void removeOtherNotificationListeners() {
-        for (int index = 0; index < videoPlayers.size(); index++) {
-            videoPlayers.valueAt(index).disposeRemoteNotifications();
-        }
-    }
-
-    @SuppressWarnings("unchecked")
-    private <T> T getParameter(Map<String, Object> parameters, String key, T defaultValue) {
-        if (parameters.containsKey(key)) {
-            Object value = parameters.get(key);
-            if (value != null) {
-                return (T) value;
-            }
-        }
-        return defaultValue;
-    }
-
-    @Override
-    public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
-        activity = binding.getActivity();
-    }
-
-    @Override
-    public void onDetachedFromActivityForConfigChanges() {
-    }
-
-    @Override
-    public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
-    }
-
-    @Override
-    public void onDetachedFromActivity() {
-    }
-
-    private boolean isPictureInPictureSupported() {
-        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
-                && activity != null
-                && activity.getPackageManager()
-                .hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
-    }
-
-    private void enablePictureInPicture(BetterPlayer player) {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-            player.setupMediaSession(flutterState.applicationContext, true);
-            activity.enterPictureInPictureMode(new PictureInPictureParams.Builder().build());
-            startPictureInPictureListenerTimer(player);
-            player.onPictureInPictureStatusChanged(true);
-        }
-    }
-
-    private void disablePictureInPicture(BetterPlayer player) {
-        stopPipHandler();
-        activity.moveTaskToBack(false);
-        player.onPictureInPictureStatusChanged(false);
-        player.disposeMediaSession();
-    }
-
-    private void startPictureInPictureListenerTimer(BetterPlayer player) {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-            pipHandler = new Handler();
-            pipRunnable = () -> {
-                if (activity.isInPictureInPictureMode()) {
-                    pipHandler.postDelayed(pipRunnable, 100);
-                } else {
-                    player.onPictureInPictureStatusChanged(false);
-                    player.disposeMediaSession();
-                    stopPipHandler();
-                }
-            };
-            pipHandler.post(pipRunnable);
-        }
-    }
-
-    private void dispose(BetterPlayer player, long textureId) {
-        player.dispose();
-        videoPlayers.remove(textureId);
-        dataSources.remove(textureId);
-        stopPipHandler();
-    }
-
-    private void stopPipHandler() {
-        if (pipHandler != null) {
-            pipHandler.removeCallbacksAndMessages(null);
-            pipHandler = null;
-        }
-        pipRunnable = null;
-    }
-
-    private interface KeyForAssetFn {
-        String get(String asset);
-    }
-
-    private interface KeyForAssetAndPackageName {
-        String get(String asset, String packageName);
-    }
-
-    private static final class FlutterState {
-        private final Context applicationContext;
-        private final BinaryMessenger binaryMessenger;
-        private final KeyForAssetFn keyForAsset;
-        private final KeyForAssetAndPackageName keyForAssetAndPackageName;
-        private final TextureRegistry textureRegistry;
-        private final MethodChannel methodChannel;
-
-        FlutterState(
-                Context applicationContext,
-                BinaryMessenger messenger,
-                KeyForAssetFn keyForAsset,
-                KeyForAssetAndPackageName keyForAssetAndPackageName,
-                TextureRegistry textureRegistry) {
-            this.applicationContext = applicationContext;
-            this.binaryMessenger = messenger;
-            this.keyForAsset = keyForAsset;
-            this.keyForAssetAndPackageName = keyForAssetAndPackageName;
-            this.textureRegistry = textureRegistry;
-            methodChannel = new MethodChannel(messenger, CHANNEL);
-        }
-
-        void startListening(BetterPlayerPlugin methodCallHandler) {
-            methodChannel.setMethodCallHandler(methodCallHandler);
-        }
-
-        void stopListening() {
-            methodChannel.setMethodCallHandler(null);
-        }
-    }
-}
diff --git a/android/src/main/java/com/jhomlala/better_player/CacheDataSourceFactory.java b/android/src/main/java/com/jhomlala/better_player/CacheDataSourceFactory.java
deleted file mode 100644
index 5451bbc05..000000000
--- a/android/src/main/java/com/jhomlala/better_player/CacheDataSourceFactory.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package com.jhomlala.better_player;
-
-import android.content.Context;
-import android.util.Log;
-
-import com.google.android.exoplayer2.upstream.DataSource;
-import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
-import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
-import com.google.android.exoplayer2.upstream.FileDataSource;
-import com.google.android.exoplayer2.upstream.cache.CacheDataSink;
-import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
-import com.google.android.exoplayer2.upstream.cache.SimpleCache;
-
-import java.io.File;
-
-class CacheDataSourceFactory implements DataSource.Factory {
-    private final Context context;
-    private final DefaultDataSourceFactory defaultDatasourceFactory;
-    private final long maxFileSize, maxCacheSize;
-
-
-    CacheDataSourceFactory(
-            Context context,
-            long maxCacheSize,
-            long maxFileSize,
-            DataSource.Factory upstreamDataSource) {
-        super();
-        this.context = context;
-        this.maxCacheSize = maxCacheSize;
-        this.maxFileSize = maxFileSize;
-        DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter.Builder(context).build();
-        defaultDatasourceFactory =
-                new DefaultDataSourceFactory(this.context, bandwidthMeter, upstreamDataSource);
-    }
-
-    @SuppressWarnings("NullableProblems")
-    @Override
-    public CacheDataSource createDataSource() {
-        SimpleCache betterPlayerCache = BetterPlayerCache.createCache(context, maxCacheSize);
-        return new CacheDataSource(
-                betterPlayerCache,
-                defaultDatasourceFactory.createDataSource(),
-                new FileDataSource(),
-                new CacheDataSink(betterPlayerCache, maxFileSize),
-                CacheDataSource.FLAG_BLOCK_ON_CACHE | CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
-                null);
-    }
-}
\ No newline at end of file
diff --git a/android/src/main/java/com/jhomlala/better_player/CacheWorker.java b/android/src/main/java/com/jhomlala/better_player/CacheWorker.java
deleted file mode 100644
index f193753f8..000000000
--- a/android/src/main/java/com/jhomlala/better_player/CacheWorker.java
+++ /dev/null
@@ -1,108 +0,0 @@
-package com.jhomlala.better_player;
-
-import android.content.Context;
-import android.net.Uri;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.work.Data;
-import androidx.work.Worker;
-import androidx.work.WorkerParameters;
-
-import com.google.android.exoplayer2.upstream.DataSource;
-import com.google.android.exoplayer2.upstream.DataSpec;
-import com.google.android.exoplayer2.upstream.HttpDataSource;
-import com.google.android.exoplayer2.upstream.cache.CacheWriter;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-
-
-/**
- * Cache worker which download part of video and save in cache for future usage. The cache job
- * will be executed in work manager.
- */
-public class CacheWorker extends Worker {
-    private static final String TAG = "CacheWorker";
-    private Context mContext;
-    private CacheWriter mCacheWriter;
-    private int mLastCacheReportIndex = 0;
-
-    public CacheWorker(
-            @NonNull Context context,
-            @NonNull WorkerParameters params) {
-        super(context, params);
-        this.mContext = context;
-    }
-
-    @NonNull
-    @Override
-    public Result doWork() {
-        try {
-            Data data = getInputData();
-            String url = data.getString(BetterPlayerPlugin.URL_PARAMETER);
-            String cacheKey = data.getString(BetterPlayerPlugin.CACHE_KEY_PARAMETER);
-            long preCacheSize = data.getLong(BetterPlayerPlugin.PRE_CACHE_SIZE_PARAMETER, 0);
-            long maxCacheSize = data.getLong(BetterPlayerPlugin.MAX_CACHE_SIZE_PARAMETER, 0);
-            long maxCacheFileSize = data.getLong(BetterPlayerPlugin.MAX_CACHE_FILE_SIZE_PARAMETER, 0);
-            Map<String, String> headers = new HashMap<>();
-            for (String key : data.getKeyValueMap().keySet()) {
-                if (key.contains(BetterPlayerPlugin.HEADER_PARAMETER)) {
-                    String keySplit = key.split(BetterPlayerPlugin.HEADER_PARAMETER)[0];
-                    headers.put(keySplit, (String) Objects.requireNonNull(data.getKeyValueMap().get(key)));
-                }
-            }
-
-            Uri uri = Uri.parse(url);
-            if (DataSourceUtils.isHTTP(uri)) {
-                String userAgent = DataSourceUtils.getUserAgent(headers);
-                DataSource.Factory dataSourceFactory = DataSourceUtils.getDataSourceFactory(userAgent, headers);
-
-                DataSpec dataSpec = new DataSpec(uri, 0, preCacheSize);
-                if (cacheKey != null && cacheKey.length() > 0) {
-                    dataSpec = dataSpec.buildUpon().setKey(cacheKey).build();
-                }
-
-                CacheDataSourceFactory cacheDataSourceFactory =
-                        new CacheDataSourceFactory(mContext, maxCacheSize, maxCacheFileSize, dataSourceFactory);
-
-                mCacheWriter = new CacheWriter(
-                        cacheDataSourceFactory.createDataSource(),
-                        dataSpec,
-                        null,
-                        (long requestLength, long bytesCached, long newBytesCached) -> {
-                                double completedData = ((bytesCached * 100f) / preCacheSize);
-                                if (completedData >= mLastCacheReportIndex * 10) {
-                                    mLastCacheReportIndex += 1;
-                                    Log.d(TAG, "Completed pre cache of " + url + ": " + (int) completedData + "%");
-                                }
-                            }
-                        );
-
-                mCacheWriter.cache();
-            } else {
-                Log.e(TAG, "Preloading only possible for remote data sources");
-                return Result.failure();
-            }
-        } catch (Exception exception) {
-            Log.e(TAG, exception.toString());
-            if (exception instanceof HttpDataSource.HttpDataSourceException) {
-                return Result.success();
-            } else {
-                return Result.failure();
-            }
-        }
-        return Result.success();
-    }
-
-    @Override
-    public void onStopped() {
-        try {
-            mCacheWriter.cancel();
-            super.onStopped();
-        } catch (Exception exception) {
-            Log.e(TAG, exception.toString());
-        }
-    }
-}
diff --git a/android/src/main/java/com/jhomlala/better_player/CustomDefaultLoadControl.java b/android/src/main/java/com/jhomlala/better_player/CustomDefaultLoadControl.java
deleted file mode 100644
index e6bf0b73d..000000000
--- a/android/src/main/java/com/jhomlala/better_player/CustomDefaultLoadControl.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package com.jhomlala.better_player;
-
-import com.google.android.exoplayer2.DefaultLoadControl;
-
-class CustomDefaultLoadControl {
-    /**
-     * The default minimum duration of media that the player will attempt to ensure is buffered
-     * at all times, in milliseconds.
-     **/
-    public final int minBufferMs;
-
-    /**
-     * The default maximum duration of media that the player will attempt to buffer, in milliseconds.
-     **/
-    public final int maxBufferMs;
-
-    /**
-     * The default duration of media that must be buffered for playback to start or resume following
-     * a user action such as a seek, in milliseconds.
-     **/
-    public final int bufferForPlaybackMs;
-
-    /**
-     * he default duration of media that must be buffered for playback to resume after a rebuffer,
-     * in milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user
-     * action.
-     **/
-    public final int bufferForPlaybackAfterRebufferMs;
-
-    CustomDefaultLoadControl() {
-        this.minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS;
-        this.maxBufferMs = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS;
-        this.bufferForPlaybackMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS;
-        this.bufferForPlaybackAfterRebufferMs =
-                DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;
-
-    }
-
-    CustomDefaultLoadControl(
-            Integer minBufferMs,
-            Integer maxBufferMs,
-            Integer bufferForPlaybackMs,
-            Integer bufferForPlaybackAfterRebufferMs
-    ) {
-        this.minBufferMs = minBufferMs != null ? minBufferMs : DefaultLoadControl.DEFAULT_MIN_BUFFER_MS;
-        this.maxBufferMs = maxBufferMs != null ? maxBufferMs : DefaultLoadControl.DEFAULT_MAX_BUFFER_MS;
-        this.bufferForPlaybackMs = bufferForPlaybackMs != null ? bufferForPlaybackMs :
-                DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS;
-        this.bufferForPlaybackAfterRebufferMs = bufferForPlaybackAfterRebufferMs != null ?
-                bufferForPlaybackAfterRebufferMs :
-                DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;
-    }
-}
diff --git a/android/src/main/java/com/jhomlala/better_player/DataSourceUtils.java b/android/src/main/java/com/jhomlala/better_player/DataSourceUtils.java
deleted file mode 100644
index 65f1cb995..000000000
--- a/android/src/main/java/com/jhomlala/better_player/DataSourceUtils.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package com.jhomlala.better_player;
-
-import android.net.Uri;
-
-import com.google.android.exoplayer2.upstream.DataSource;
-import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
-
-import java.util.Map;
-
-class DataSourceUtils {
-
-    private static final String USER_AGENT = "User-Agent";
-    private static final String USER_AGENT_PROPERTY = "http.agent";
-
-    static String getUserAgent(Map<String, String> headers){
-        String userAgent = System.getProperty(USER_AGENT_PROPERTY);
-        if (headers != null && headers.containsKey(USER_AGENT)) {
-            String userAgentHeader = headers.get(USER_AGENT);
-            if (userAgentHeader != null) {
-                userAgent = userAgentHeader;
-            }
-        }
-        return userAgent;
-    }
-
-    static DataSource.Factory getDataSourceFactory(String userAgent, Map<String, String> headers){
-        DataSource.Factory dataSourceFactory = new DefaultHttpDataSource.Factory()
-                .setUserAgent(userAgent)
-                .setAllowCrossProtocolRedirects(true)
-                .setConnectTimeoutMs(DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS)
-                .setReadTimeoutMs(DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS);
-
-        if (headers != null) {
-            ((DefaultHttpDataSource.Factory) dataSourceFactory).setDefaultRequestProperties(headers);
-        }
-        return dataSourceFactory;
-    }
-
-    static boolean isHTTP(Uri uri) {
-        if (uri == null || uri.getScheme() == null) {
-            return false;
-        }
-        String scheme = uri.getScheme();
-        return scheme.equals("http") || scheme.equals("https");
-    }
-}
diff --git a/android/src/main/java/com/jhomlala/better_player/ImageWorker.java b/android/src/main/java/com/jhomlala/better_player/ImageWorker.java
deleted file mode 100644
index a59284a6a..000000000
--- a/android/src/main/java/com/jhomlala/better_player/ImageWorker.java
+++ /dev/null
@@ -1,125 +0,0 @@
-package com.jhomlala.better_player;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.net.Uri;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.work.Data;
-import androidx.work.Worker;
-import androidx.work.WorkerParameters;
-
-import java.io.FileOutputStream;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-
-public class ImageWorker extends Worker {
-    private static final String TAG = "ImageWorker";
-    private static final String IMAGE_EXTENSION = ".png";
-    private static final int DEFAULT_NOTIFICATION_IMAGE_SIZE_PX = 256;
-
-    public ImageWorker(
-            @NonNull Context context,
-            @NonNull WorkerParameters params) {
-        super(context, params);
-    }
-
-    @NonNull
-    @Override
-    public Result doWork() {
-        try {
-            String imageUrl = getInputData().getString(BetterPlayerPlugin.URL_PARAMETER);
-            if (imageUrl == null) {
-                return Result.failure();
-            }
-            Bitmap bitmap = null;
-            if (DataSourceUtils.isHTTP(Uri.parse(imageUrl))) {
-                bitmap = getBitmapFromExternalURL(imageUrl);
-            } else {
-                bitmap = getBitmapFromInternalURL(imageUrl);
-            }
-            String fileName = imageUrl.hashCode() + IMAGE_EXTENSION;
-            String filePath = getApplicationContext().getCacheDir().getAbsolutePath() + fileName;
-
-            if (bitmap == null) {
-                return Result.failure();
-            }
-            FileOutputStream out = new FileOutputStream(filePath);
-            bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
-            Data data = new Data.Builder().putString(BetterPlayerPlugin.FILE_PATH_PARAMETER, filePath).build();
-            return Result.success(data);
-        } catch (Exception e) {
-            e.printStackTrace();
-            return Result.failure();
-        }
-    }
-
-
-    private Bitmap getBitmapFromExternalURL(String src) {
-        InputStream inputStream = null;
-        try {
-            URL url = new URL(src);
-            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
-            inputStream = connection.getInputStream();
-
-            final BitmapFactory.Options options = new BitmapFactory.Options();
-            options.inJustDecodeBounds = true;
-            BitmapFactory.decodeStream(inputStream, null, options);
-            inputStream.close();
-            connection = (HttpURLConnection) url.openConnection();
-            inputStream = connection.getInputStream();
-            options.inSampleSize = calculateBitmapInSampleSize(
-                    options);
-            options.inJustDecodeBounds = false;
-            return BitmapFactory.decodeStream(inputStream, null, options);
-
-        } catch (Exception exception) {
-            Log.e(TAG, "Failed to get bitmap from external url: " + src);
-            return null;
-        } finally {
-            try {
-                if (inputStream != null) {
-                    inputStream.close();
-                }
-            } catch (Exception exception) {
-                Log.e(TAG, "Failed to close bitmap input stream/");
-            }
-        }
-    }
-
-    private int calculateBitmapInSampleSize(
-            BitmapFactory.Options options) {
-        final int height = options.outHeight;
-        final int width = options.outWidth;
-        int inSampleSize = 1;
-
-        if (height > ImageWorker.DEFAULT_NOTIFICATION_IMAGE_SIZE_PX
-                || width > ImageWorker.DEFAULT_NOTIFICATION_IMAGE_SIZE_PX) {
-            final int halfHeight = height / 2;
-            final int halfWidth = width / 2;
-            while ((halfHeight / inSampleSize) >= ImageWorker.DEFAULT_NOTIFICATION_IMAGE_SIZE_PX
-                    && (halfWidth / inSampleSize) >= ImageWorker.DEFAULT_NOTIFICATION_IMAGE_SIZE_PX) {
-                inSampleSize *= 2;
-            }
-        }
-        return inSampleSize;
-    }
-
-    private Bitmap getBitmapFromInternalURL(String src) {
-        try {
-            final BitmapFactory.Options options = new BitmapFactory.Options();
-            options.inJustDecodeBounds = true;
-            options.inSampleSize = calculateBitmapInSampleSize(options
-            );
-            options.inJustDecodeBounds = false;
-            return BitmapFactory.decodeFile(src);
-        } catch (Exception exception) {
-            Log.e(TAG, "Failed to get bitmap from internal url: " + src);
-            return null;
-        }
-    }
-
-}
diff --git a/android/src/main/java/com/jhomlala/better_player/QueuingEventSink.java b/android/src/main/java/com/jhomlala/better_player/QueuingEventSink.java
deleted file mode 100644
index fc7450d60..000000000
--- a/android/src/main/java/com/jhomlala/better_player/QueuingEventSink.java
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright 2019 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-package com.jhomlala.better_player;
-
-import io.flutter.plugin.common.EventChannel;
-import java.util.ArrayList;
-
-/**
- * And implementation of {@link EventChannel.EventSink} which can wrap an underlying sink.
- *
- * <p>It delivers messages immediately when downstream is available, but it queues messages before
- * the delegate event sink is set with setDelegate.
- *
- * <p>This class is not thread-safe. All calls must be done on the same thread or synchronized
- * externally.
- */
-final class QueuingEventSink implements EventChannel.EventSink {
-  private EventChannel.EventSink delegate;
-  private ArrayList<Object> eventQueue = new ArrayList<>();
-  private boolean done = false;
-
-  public void setDelegate(EventChannel.EventSink delegate) {
-    this.delegate = delegate;
-    maybeFlush();
-  }
-
-  @Override
-  public void endOfStream() {
-    enqueue(new EndOfStreamEvent());
-    maybeFlush();
-    done = true;
-  }
-
-  @Override
-  public void error(String code, String message, Object details) {
-    enqueue(new ErrorEvent(code, message, details));
-    maybeFlush();
-  }
-
-  @Override
-  public void success(Object event) {
-    enqueue(event);
-    maybeFlush();
-  }
-
-  private void enqueue(Object event) {
-    if (done) {
-      return;
-    }
-    eventQueue.add(event);
-  }
-
-  private void maybeFlush() {
-    if (delegate == null) {
-      return;
-    }
-    for (Object event : eventQueue) {
-      if (event instanceof EndOfStreamEvent) {
-        delegate.endOfStream();
-      } else if (event instanceof ErrorEvent) {
-        ErrorEvent errorEvent = (ErrorEvent) event;
-        delegate.error(errorEvent.code, errorEvent.message, errorEvent.details);
-      } else {
-        delegate.success(event);
-      }
-    }
-    eventQueue.clear();
-  }
-
-  private static class EndOfStreamEvent {}
-
-  private static class ErrorEvent {
-    String code;
-    String message;
-    Object details;
-
-    ErrorEvent(String code, String message, Object details) {
-      this.code = code;
-      this.message = message;
-      this.details = details;
-    }
-  }
-}
diff --git a/android/src/main/kotlin/com/jhomlala/better_player/BetterPlayer.kt b/android/src/main/kotlin/com/jhomlala/better_player/BetterPlayer.kt
new file mode 100644
index 000000000..e1f283185
--- /dev/null
+++ b/android/src/main/kotlin/com/jhomlala/better_player/BetterPlayer.kt
@@ -0,0 +1,892 @@
+package com.jhomlala.better_player
+
+import android.annotation.SuppressLint
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.net.Uri
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import com.jhomlala.better_player.DataSourceUtils.getUserAgent
+import com.jhomlala.better_player.DataSourceUtils.isHTTP
+import com.jhomlala.better_player.DataSourceUtils.getDataSourceFactory
+import io.flutter.plugin.common.EventChannel
+import io.flutter.view.TextureRegistry.SurfaceTextureEntry
+import io.flutter.plugin.common.MethodChannel
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
+import com.google.android.exoplayer2.ui.PlayerNotificationManager
+import android.support.v4.media.session.MediaSessionCompat
+import com.google.android.exoplayer2.drm.DrmSessionManager
+import androidx.work.WorkManager
+import androidx.work.WorkInfo
+import com.google.android.exoplayer2.drm.HttpMediaDrmCallback
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
+import com.google.android.exoplayer2.drm.DefaultDrmSessionManager
+import com.google.android.exoplayer2.drm.FrameworkMediaDrm
+import com.google.android.exoplayer2.drm.UnsupportedDrmException
+import com.google.android.exoplayer2.drm.DummyExoMediaDrm
+import com.google.android.exoplayer2.drm.LocalMediaDrmCallback
+import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
+import com.google.android.exoplayer2.source.MediaSource
+import com.google.android.exoplayer2.source.ClippingMediaSource
+import com.google.android.exoplayer2.ui.PlayerNotificationManager.MediaDescriptionAdapter
+import com.google.android.exoplayer2.ui.PlayerNotificationManager.BitmapCallback
+import androidx.work.OneTimeWorkRequest
+import android.support.v4.media.session.PlaybackStateCompat
+import android.support.v4.media.MediaMetadataCompat
+import android.util.Log
+import android.view.Surface
+import androidx.lifecycle.Observer
+import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource
+import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource
+import com.google.android.exoplayer2.source.dash.DashMediaSource
+import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource
+import com.google.android.exoplayer2.source.hls.HlsMediaSource
+import com.google.android.exoplayer2.source.ProgressiveMediaSource
+import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory
+import io.flutter.plugin.common.EventChannel.EventSink
+import androidx.media.session.MediaButtonReceiver
+import androidx.work.Data
+import com.google.android.exoplayer2.*
+import com.google.android.exoplayer2.audio.AudioAttributes
+import com.google.android.exoplayer2.drm.DrmSessionManagerProvider
+import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride
+import com.google.android.exoplayer2.upstream.DataSource
+import com.google.android.exoplayer2.util.Util
+import java.io.File
+import java.lang.Exception
+import java.lang.IllegalStateException
+import java.util.*
+import kotlin.math.max
+import kotlin.math.min
+
+internal class BetterPlayer(
+    context: Context,
+    private val eventChannel: EventChannel,
+    private val textureEntry: SurfaceTextureEntry,
+    customDefaultLoadControl: CustomDefaultLoadControl?,
+    result: MethodChannel.Result
+) {
+    private val exoPlayer: SimpleExoPlayer?
+    private val eventSink = QueuingEventSink()
+    private val trackSelector: DefaultTrackSelector = DefaultTrackSelector(context)
+    private val loadControl: LoadControl
+    private var isInitialized = false
+    private var surface: Surface? = null
+    private var key: String? = null
+    private var playerNotificationManager: PlayerNotificationManager? = null
+    private var refreshHandler: Handler? = null
+    private var refreshRunnable: Runnable? = null
+    private var exoPlayerEventListener: Player.Listener? = null
+    private var bitmap: Bitmap? = null
+    private var mediaSession: MediaSessionCompat? = null
+    private var drmSessionManager: DrmSessionManager? = null
+    private val workManager: WorkManager
+    private val workerObserverMap: HashMap<UUID, Observer<WorkInfo?>>
+    private val customDefaultLoadControl: CustomDefaultLoadControl =
+        customDefaultLoadControl ?: CustomDefaultLoadControl()
+    private var lastSendBufferedPosition = 0L
+
+    init {
+        val loadBuilder = DefaultLoadControl.Builder()
+        loadBuilder.setBufferDurationsMs(
+            this.customDefaultLoadControl.minBufferMs,
+            this.customDefaultLoadControl.maxBufferMs,
+            this.customDefaultLoadControl.bufferForPlaybackMs,
+            this.customDefaultLoadControl.bufferForPlaybackAfterRebufferMs
+        )
+        loadControl = loadBuilder.build()
+        exoPlayer = SimpleExoPlayer.Builder(context)
+            .setTrackSelector(trackSelector)
+            .setLoadControl(loadControl)
+            .build()
+        workManager = WorkManager.getInstance(context)
+        workerObserverMap = HashMap()
+        setupVideoPlayer(eventChannel, textureEntry, result)
+    }
+
+    fun setDataSource(
+        context: Context,
+        key: String?,
+        dataSource: String?,
+        formatHint: String?,
+        result: MethodChannel.Result,
+        headers: Map<String, String>?,
+        useCache: Boolean,
+        maxCacheSize: Long,
+        maxCacheFileSize: Long,
+        overriddenDuration: Long,
+        licenseUrl: String?,
+        drmHeaders: Map<String, String>?,
+        cacheKey: String?,
+        clearKey: String?
+    ) {
+        this.key = key
+        isInitialized = false
+        val uri = Uri.parse(dataSource)
+        var dataSourceFactory: DataSource.Factory?
+        val userAgent = getUserAgent(headers)
+        if (licenseUrl != null && licenseUrl.isNotEmpty()) {
+            val httpMediaDrmCallback =
+                HttpMediaDrmCallback(licenseUrl, DefaultHttpDataSource.Factory())
+            if (drmHeaders != null) {
+                for ((drmKey, drmValue) in drmHeaders) {
+                    httpMediaDrmCallback.setKeyRequestProperty(drmKey, drmValue)
+                }
+            }
+            if (Util.SDK_INT < 18) {
+                Log.e(TAG, "Protected content not supported on API levels below 18")
+                drmSessionManager = null
+            } else {
+                val drmSchemeUuid = Util.getDrmUuid("widevine")
+                if (drmSchemeUuid != null) {
+                    drmSessionManager = DefaultDrmSessionManager.Builder()
+                        .setUuidAndExoMediaDrmProvider(
+                            drmSchemeUuid
+                        ) { uuid: UUID? ->
+                            try {
+                                val mediaDrm = FrameworkMediaDrm.newInstance(uuid!!)
+                                // Force L3.
+                                mediaDrm.setPropertyString("securityLevel", "L3")
+                                return@setUuidAndExoMediaDrmProvider mediaDrm
+                            } catch (e: UnsupportedDrmException) {
+                                return@setUuidAndExoMediaDrmProvider DummyExoMediaDrm()
+                            }
+                        }
+                        .setMultiSession(false)
+                        .build(httpMediaDrmCallback)
+                }
+            }
+        } else if (clearKey != null && clearKey.isNotEmpty()) {
+            drmSessionManager = if (Util.SDK_INT < 18) {
+                Log.e(TAG, "Protected content not supported on API levels below 18")
+                null
+            } else {
+                DefaultDrmSessionManager.Builder()
+                    .setUuidAndExoMediaDrmProvider(
+                        C.CLEARKEY_UUID,
+                        FrameworkMediaDrm.DEFAULT_PROVIDER
+                    ).build(LocalMediaDrmCallback(clearKey.toByteArray()))
+            }
+        } else {
+            drmSessionManager = null
+        }
+        if (isHTTP(uri)) {
+            dataSourceFactory = getDataSourceFactory(userAgent, headers)
+            if (useCache && maxCacheSize > 0 && maxCacheFileSize > 0) {
+                dataSourceFactory = CacheDataSourceFactory(
+                    context,
+                    maxCacheSize,
+                    maxCacheFileSize,
+                    dataSourceFactory
+                )
+            }
+        } else {
+            dataSourceFactory = DefaultDataSourceFactory(context, userAgent)
+        }
+        val mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint, cacheKey, context)
+        if (overriddenDuration != 0L) {
+            val clippingMediaSource = ClippingMediaSource(mediaSource, 0, overriddenDuration * 1000)
+            exoPlayer!!.setMediaSource(clippingMediaSource)
+        } else {
+            exoPlayer!!.setMediaSource(mediaSource)
+        }
+        exoPlayer.prepare()
+        result.success(null)
+    }
+
+    fun setupPlayerNotification(
+        context: Context, title: String, author: String?,
+        imageUrl: String?, notificationChannelName: String?,
+        activityName: String
+    ) {
+        val mediaDescriptionAdapter: MediaDescriptionAdapter = object : MediaDescriptionAdapter {
+            override fun getCurrentContentTitle(player: Player): String {
+                return title
+            }
+
+            @SuppressLint("UnspecifiedImmutableFlag")
+            override fun createCurrentContentIntent(player: Player): PendingIntent? {
+                val packageName = context.applicationContext.packageName
+                val notificationIntent = Intent()
+                notificationIntent.setClassName(
+                    packageName,
+                    "$packageName.$activityName"
+                )
+                notificationIntent.flags = (Intent.FLAG_ACTIVITY_CLEAR_TOP
+                        or Intent.FLAG_ACTIVITY_SINGLE_TOP)
+                return PendingIntent.getActivity(
+                    context, 0,
+                    notificationIntent,
+                    PendingIntent.FLAG_IMMUTABLE
+                )
+            }
+
+            override fun getCurrentContentText(player: Player): String? {
+                return author
+            }
+
+            override fun getCurrentLargeIcon(
+                player: Player,
+                callback: BitmapCallback
+            ): Bitmap? {
+                if (imageUrl == null) {
+                    return null
+                }
+                if (bitmap != null) {
+                    return bitmap
+                }
+                val imageWorkRequest = OneTimeWorkRequest.Builder(ImageWorker::class.java)
+                    .addTag(imageUrl)
+                    .setInputData(
+                        Data.Builder()
+                            .putString(BetterPlayerPlugin.URL_PARAMETER, imageUrl)
+                            .build()
+                    )
+                    .build()
+                workManager.enqueue(imageWorkRequest)
+                val workInfoObserver = Observer { workInfo: WorkInfo? ->
+                    try {
+                        if (workInfo != null) {
+                            val state = workInfo.state
+                            if (state == WorkInfo.State.SUCCEEDED) {
+                                val outputData = workInfo.outputData
+                                val filePath =
+                                    outputData.getString(BetterPlayerPlugin.FILE_PATH_PARAMETER)
+                                //Bitmap here is already processed and it's very small, so it won't
+                                //break anything.
+                                bitmap = BitmapFactory.decodeFile(filePath)
+                                callback.onBitmap(bitmap!!)
+                            }
+                            if (state == WorkInfo.State.SUCCEEDED || state == WorkInfo.State.CANCELLED || state == WorkInfo.State.FAILED) {
+                                val uuid = imageWorkRequest.id
+                                val observer = workerObserverMap.remove(uuid)
+                                if (observer != null) {
+                                    workManager.getWorkInfoByIdLiveData(uuid)
+                                        .removeObserver(observer)
+                                }
+                            }
+                        }
+                    } catch (exception: Exception) {
+                        Log.e(TAG, "Image select error: $exception")
+                    }
+                }
+                val workerUuid = imageWorkRequest.id
+                workManager.getWorkInfoByIdLiveData(workerUuid)
+                    .observeForever(workInfoObserver)
+                workerObserverMap[workerUuid] = workInfoObserver
+                return null
+            }
+        }
+        var playerNotificationChannelName = notificationChannelName
+        if (notificationChannelName == null) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                val importance = NotificationManager.IMPORTANCE_LOW
+                val channel = NotificationChannel(
+                    DEFAULT_NOTIFICATION_CHANNEL,
+                    DEFAULT_NOTIFICATION_CHANNEL, importance
+                )
+                channel.description = DEFAULT_NOTIFICATION_CHANNEL
+                val notificationManager = context.getSystemService(
+                    NotificationManager::class.java
+                )
+                notificationManager.createNotificationChannel(channel)
+                playerNotificationChannelName = DEFAULT_NOTIFICATION_CHANNEL
+            }
+        }
+        playerNotificationManager = PlayerNotificationManager.Builder(
+            context,
+            NOTIFICATION_ID,
+            playerNotificationChannelName!!,
+            mediaDescriptionAdapter
+        ).build()
+        playerNotificationManager!!.setPlayer(exoPlayer)
+        playerNotificationManager!!.setUseNextAction(false)
+        playerNotificationManager!!.setUsePreviousAction(false)
+        playerNotificationManager!!.setUseStopAction(false)
+        val mediaSession = setupMediaSession(context, false)
+        playerNotificationManager!!.setMediaSessionToken(mediaSession.sessionToken)
+        playerNotificationManager!!.setControlDispatcher(setupControlDispatcher())
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            refreshHandler = Handler(Looper.getMainLooper())
+            refreshRunnable = Runnable {
+                val playbackState: PlaybackStateCompat = if (exoPlayer?.isPlaying == true) {
+                    PlaybackStateCompat.Builder()
+                        .setActions(PlaybackStateCompat.ACTION_SEEK_TO)
+                        .setState(PlaybackStateCompat.STATE_PLAYING, position, 1.0f)
+                        .build()
+                } else {
+                    PlaybackStateCompat.Builder()
+                        .setActions(PlaybackStateCompat.ACTION_SEEK_TO)
+                        .setState(PlaybackStateCompat.STATE_PAUSED, position, 1.0f)
+                        .build()
+                }
+                mediaSession.setPlaybackState(playbackState)
+                refreshHandler!!.postDelayed(refreshRunnable!!, 1000)
+            }
+            refreshHandler!!.postDelayed(refreshRunnable!!, 0)
+        }
+        exoPlayerEventListener = object : Player.Listener {
+            override fun onPlaybackStateChanged(playbackState: Int) {
+                mediaSession.setMetadata(
+                    MediaMetadataCompat.Builder()
+                        .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, getDuration())
+                        .build()
+                )
+            }
+        }
+        exoPlayer!!.addListener(exoPlayerEventListener!!)
+        exoPlayer.seekTo(0)
+    }
+
+    private fun setupControlDispatcher(): ControlDispatcher {
+        return object : ControlDispatcher {
+            override fun dispatchPrepare(player: Player): Boolean {
+                return false
+            }
+
+            override fun dispatchSetPlayWhenReady(player: Player, playWhenReady: Boolean): Boolean {
+                if (player.playWhenReady) {
+                    sendEvent("pause")
+                } else {
+                    sendEvent("play")
+                }
+                return true
+            }
+
+            override fun dispatchSeekTo(
+                player: Player,
+                windowIndex: Int,
+                positionMs: Long
+            ): Boolean {
+                sendSeekToEvent(positionMs)
+                return true
+            }
+
+            override fun dispatchPrevious(player: Player): Boolean {
+                return false
+            }
+
+            override fun dispatchNext(player: Player): Boolean {
+                return false
+            }
+
+            override fun dispatchRewind(player: Player): Boolean {
+                sendSeekToEvent(player.currentPosition - 5000)
+                return false
+            }
+
+            override fun dispatchFastForward(player: Player): Boolean {
+                sendSeekToEvent(player.currentPosition + 5000)
+                return true
+            }
+
+            override fun dispatchSetRepeatMode(player: Player, repeatMode: Int): Boolean {
+                return false
+            }
+
+            override fun dispatchSetShuffleModeEnabled(
+                player: Player,
+                shuffleModeEnabled: Boolean
+            ): Boolean {
+                return false
+            }
+
+            override fun dispatchStop(player: Player, reset: Boolean): Boolean {
+                return false
+            }
+
+            override fun dispatchSetPlaybackParameters(
+                player: Player,
+                playbackParameters: PlaybackParameters
+            ): Boolean {
+                return false
+            }
+
+            override fun isRewindEnabled(): Boolean {
+                return true
+            }
+
+            override fun isFastForwardEnabled(): Boolean {
+                return true
+            }
+        }
+    }
+
+    fun disposeRemoteNotifications() {
+        if (exoPlayerEventListener != null) {
+            exoPlayer!!.removeListener(exoPlayerEventListener!!)
+        }
+        if (refreshHandler != null) {
+            refreshHandler!!.removeCallbacksAndMessages(null)
+            refreshHandler = null
+            refreshRunnable = null
+        }
+        if (playerNotificationManager != null) {
+            playerNotificationManager!!.setPlayer(null)
+        }
+        bitmap = null
+    }
+
+    private fun buildMediaSource(
+        uri: Uri,
+        mediaDataSourceFactory: DataSource.Factory,
+        formatHint: String?,
+        cacheKey: String?,
+        context: Context
+    ): MediaSource {
+        val type: Int
+        if (formatHint == null) {
+            var lastPathSegment = uri.lastPathSegment
+            if (lastPathSegment == null) {
+                lastPathSegment = ""
+            }
+            type = Util.inferContentType(lastPathSegment)
+        } else {
+            type = when (formatHint) {
+                FORMAT_SS -> C.TYPE_SS
+                FORMAT_DASH -> C.TYPE_DASH
+                FORMAT_HLS -> C.TYPE_HLS
+                FORMAT_OTHER -> C.TYPE_OTHER
+                else -> -1
+            }
+        }
+        val mediaItemBuilder = MediaItem.Builder()
+        mediaItemBuilder.setUri(uri)
+        if (cacheKey != null && cacheKey.isNotEmpty()) {
+            mediaItemBuilder.setCustomCacheKey(cacheKey)
+        }
+        val mediaItem = mediaItemBuilder.build()
+        var drmSessionManagerProvider: DrmSessionManagerProvider? = null
+        if (drmSessionManager != null) {
+            drmSessionManagerProvider = DrmSessionManagerProvider { drmSessionManager!! }
+        }
+        return when (type) {
+            C.TYPE_SS -> SsMediaSource.Factory(
+                DefaultSsChunkSource.Factory(mediaDataSourceFactory),
+                DefaultDataSourceFactory(context, null, mediaDataSourceFactory)
+            )
+                .setDrmSessionManagerProvider(drmSessionManagerProvider)
+                .createMediaSource(mediaItem)
+            C.TYPE_DASH -> DashMediaSource.Factory(
+                DefaultDashChunkSource.Factory(mediaDataSourceFactory),
+                DefaultDataSourceFactory(context, null, mediaDataSourceFactory)
+            )
+                .setDrmSessionManagerProvider(drmSessionManagerProvider)
+                .createMediaSource(mediaItem)
+            C.TYPE_HLS -> HlsMediaSource.Factory(mediaDataSourceFactory)
+                .setDrmSessionManagerProvider(drmSessionManagerProvider)
+                .createMediaSource(mediaItem)
+            C.TYPE_OTHER -> ProgressiveMediaSource.Factory(
+                mediaDataSourceFactory,
+                DefaultExtractorsFactory()
+            )
+                .setDrmSessionManagerProvider(drmSessionManagerProvider)
+                .createMediaSource(mediaItem)
+            else -> {
+                throw IllegalStateException("Unsupported type: $type")
+            }
+        }
+    }
+
+    private fun setupVideoPlayer(
+        eventChannel: EventChannel, textureEntry: SurfaceTextureEntry, result: MethodChannel.Result
+    ) {
+        eventChannel.setStreamHandler(
+            object : EventChannel.StreamHandler {
+                override fun onListen(o: Any?, sink: EventSink) {
+                    eventSink.setDelegate(sink)
+                }
+
+                override fun onCancel(o: Any?) {
+                    eventSink.setDelegate(null)
+                }
+            })
+        surface = Surface(textureEntry.surfaceTexture())
+        exoPlayer!!.setVideoSurface(surface)
+        setAudioAttributes(exoPlayer, true)
+        exoPlayer.addListener(object : Player.Listener {
+            override fun onPlaybackStateChanged(playbackState: Int) {
+                when (playbackState) {
+                    Player.STATE_BUFFERING -> {
+                        sendBufferingUpdate(true)
+                        val event: MutableMap<String, Any> = HashMap()
+                        event["event"] = "bufferingStart"
+                        eventSink.success(event)
+                    }
+                    Player.STATE_READY -> {
+                        if (!isInitialized) {
+                            isInitialized = true
+                            sendInitialized()
+                        }
+                        val event: MutableMap<String, Any> = HashMap()
+                        event["event"] = "bufferingEnd"
+                        eventSink.success(event)
+                    }
+                    Player.STATE_ENDED -> {
+                        val event: MutableMap<String, Any?> = HashMap()
+                        event["event"] = "completed"
+                        event["key"] = key
+                        eventSink.success(event)
+                    }
+                    Player.STATE_IDLE -> {
+                        //no-op
+                    }
+                }
+            }
+
+            override fun onPlayerError(error: PlaybackException) {
+                eventSink.error("VideoError", "Video player had error $error", "")
+            }
+        })
+        val reply: MutableMap<String, Any> = HashMap()
+        reply["textureId"] = textureEntry.id()
+        result.success(reply)
+    }
+
+    fun sendBufferingUpdate(isFromBufferingStart: Boolean) {
+        val bufferedPosition = exoPlayer!!.bufferedPosition
+        if (isFromBufferingStart || bufferedPosition != lastSendBufferedPosition) {
+            val event: MutableMap<String, Any> = HashMap()
+            event["event"] = "bufferingUpdate"
+            val range: List<Number?> = listOf(0, bufferedPosition)
+            // iOS supports a list of buffered ranges, so here is a list with a single range.
+            event["values"] = listOf(range)
+            eventSink.success(event)
+            lastSendBufferedPosition = bufferedPosition
+        }
+    }
+
+    private fun setAudioAttributes(exoPlayer: SimpleExoPlayer?, mixWithOthers: Boolean) {
+        val audioComponent = exoPlayer!!.audioComponent ?: return
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            audioComponent.setAudioAttributes(
+                AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MOVIE).build(),
+                !mixWithOthers
+            )
+        } else {
+            audioComponent.setAudioAttributes(
+                AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MUSIC).build(),
+                !mixWithOthers
+            )
+        }
+    }
+
+    fun play() {
+        exoPlayer!!.playWhenReady = true
+    }
+
+    fun pause() {
+        exoPlayer!!.playWhenReady = false
+    }
+
+    fun setLooping(value: Boolean) {
+        exoPlayer!!.repeatMode = if (value) Player.REPEAT_MODE_ALL else Player.REPEAT_MODE_OFF
+    }
+
+    fun setVolume(value: Double) {
+        val bracketedValue = max(0.0, min(1.0, value))
+            .toFloat()
+        exoPlayer!!.volume = bracketedValue
+    }
+
+    fun setSpeed(value: Double) {
+        val bracketedValue = value.toFloat()
+        val playbackParameters = PlaybackParameters(bracketedValue)
+        exoPlayer!!.playbackParameters = playbackParameters
+    }
+
+    fun setTrackParameters(width: Int, height: Int, bitrate: Int) {
+        val parametersBuilder = trackSelector.buildUponParameters()
+        if (width != 0 && height != 0) {
+            parametersBuilder.setMaxVideoSize(width, height)
+        }
+        if (bitrate != 0) {
+            parametersBuilder.setMaxVideoBitrate(bitrate)
+        }
+        if (width == 0 && height == 0 && bitrate == 0) {
+            parametersBuilder.clearVideoSizeConstraints()
+            parametersBuilder.setMaxVideoBitrate(Int.MAX_VALUE)
+        }
+        trackSelector.setParameters(parametersBuilder)
+    }
+
+    fun seekTo(location: Int) {
+        exoPlayer!!.seekTo(location.toLong())
+    }
+
+    val position: Long
+        get() = exoPlayer!!.currentPosition
+    val absolutePosition: Long
+        get() {
+            val timeline = exoPlayer!!.currentTimeline
+            if (!timeline.isEmpty) {
+                val windowStartTimeMs = timeline.getWindow(0, Timeline.Window()).windowStartTimeMs
+                val pos = exoPlayer.currentPosition
+                return windowStartTimeMs + pos
+            }
+            return exoPlayer.currentPosition
+        }
+
+    private fun sendInitialized() {
+        if (isInitialized) {
+            val event: MutableMap<String, Any?> = HashMap()
+            event["event"] = "initialized"
+            event["key"] = key
+            event["duration"] = getDuration()
+            if (exoPlayer!!.videoFormat != null) {
+                val videoFormat = exoPlayer.videoFormat
+                var width = videoFormat!!.width
+                var height = videoFormat.height
+                val rotationDegrees = videoFormat.rotationDegrees
+                // Switch the width/height if video was taken in portrait mode
+                if (rotationDegrees == 90 || rotationDegrees == 270) {
+                    width = exoPlayer.videoFormat!!.height
+                    height = exoPlayer.videoFormat!!.width
+                }
+                event["width"] = width
+                event["height"] = height
+            }
+            eventSink.success(event)
+        }
+    }
+
+    private fun getDuration(): Long = exoPlayer!!.duration
+
+    /**
+     * Create media session which will be used in notifications, pip mode.
+     *
+     * @param context                - android context
+     * @param setupControlDispatcher - should add control dispatcher to created MediaSession
+     * @return - configured MediaSession instance
+     */
+    fun setupMediaSession(context: Context?, setupControlDispatcher: Boolean): MediaSessionCompat {
+        mediaSession?.release()
+        val mediaButtonReceiver = ComponentName(context!!, MediaButtonReceiver::class.java)
+        val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON)
+        val pendingIntent = PendingIntent.getBroadcast(
+            context!!,
+            0, mediaButtonIntent,
+            PendingIntent.FLAG_IMMUTABLE
+        )
+        val mediaSession = MediaSessionCompat(context!!, TAG, null, pendingIntent)
+        mediaSession.setCallback(object : MediaSessionCompat.Callback() {
+            override fun onSeekTo(pos: Long) {
+                sendSeekToEvent(pos)
+                super.onSeekTo(pos)
+            }
+        })
+        mediaSession.isActive = true
+        val mediaSessionConnector = MediaSessionConnector(mediaSession)
+        if (setupControlDispatcher) {
+            mediaSessionConnector.setControlDispatcher(setupControlDispatcher())
+        }
+        mediaSessionConnector.setPlayer(exoPlayer)
+        this.mediaSession = mediaSession
+        return mediaSession
+    }
+
+    fun onPictureInPictureStatusChanged(inPip: Boolean) {
+        val event: MutableMap<String, Any> = HashMap()
+        event["event"] = if (inPip) "pipStart" else "pipStop"
+        eventSink.success(event)
+    }
+
+    fun disposeMediaSession() {
+        if (mediaSession != null) {
+            mediaSession!!.release()
+        }
+        mediaSession = null
+    }
+
+    private fun sendEvent(eventType: String) {
+        val event: MutableMap<String, Any> = HashMap()
+        event["event"] = eventType
+        eventSink.success(event)
+    }
+
+    fun setAudioTrack(name: String, index: Int) {
+        try {
+            val mappedTrackInfo = trackSelector.currentMappedTrackInfo
+            if (mappedTrackInfo != null) {
+                for (rendererIndex in 0 until mappedTrackInfo.rendererCount) {
+                    if (mappedTrackInfo.getRendererType(rendererIndex) != C.TRACK_TYPE_AUDIO) {
+                        continue
+                    }
+                    val trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex)
+                    var hasElementWithoutLabel = false
+                    var hasStrangeAudioTrack = false
+                    for (groupIndex in 0 until trackGroupArray.length) {
+                        val group = trackGroupArray[groupIndex]
+                        for (groupElementIndex in 0 until group.length) {
+                            val format = group.getFormat(groupElementIndex)
+                            if (format.label == null) {
+                                hasElementWithoutLabel = true
+                            }
+                            if (format.id != null && format.id == "1/15") {
+                                hasStrangeAudioTrack = true
+                            }
+                        }
+                    }
+                    for (groupIndex in 0 until trackGroupArray.length) {
+                        val group = trackGroupArray[groupIndex]
+                        for (groupElementIndex in 0 until group.length) {
+                            val label = group.getFormat(groupElementIndex).label
+                            if (name == label && index == groupIndex) {
+                                setAudioTrack(rendererIndex, groupIndex, groupElementIndex)
+                                return
+                            }
+
+                            ///Fallback option
+                            if (!hasStrangeAudioTrack && hasElementWithoutLabel && index == groupIndex) {
+                                setAudioTrack(rendererIndex, groupIndex, groupElementIndex)
+                                return
+                            }
+                            ///Fallback option
+                            if (hasStrangeAudioTrack && name == label) {
+                                setAudioTrack(rendererIndex, groupIndex, groupElementIndex)
+                                return
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (exception: Exception) {
+            Log.e(TAG, "setAudioTrack failed$exception")
+        }
+    }
+
+    private fun setAudioTrack(rendererIndex: Int, groupIndex: Int, groupElementIndex: Int) {
+        val mappedTrackInfo = trackSelector.currentMappedTrackInfo
+        if (mappedTrackInfo != null) {
+            val builder = trackSelector.parameters.buildUpon()
+            builder.clearSelectionOverrides(rendererIndex)
+                .setRendererDisabled(rendererIndex, false)
+            val tracks = intArrayOf(groupElementIndex)
+            val override = SelectionOverride(groupIndex, *tracks)
+            builder.setSelectionOverride(
+                rendererIndex,
+                mappedTrackInfo.getTrackGroups(rendererIndex), override
+            )
+            trackSelector.setParameters(builder)
+        }
+    }
+
+    private fun sendSeekToEvent(positionMs: Long) {
+        exoPlayer!!.seekTo(positionMs)
+        val event: MutableMap<String, Any> = HashMap()
+        event["event"] = "seek"
+        event["position"] = positionMs
+        eventSink.success(event)
+    }
+
+    fun setMixWithOthers(mixWithOthers: Boolean) {
+        setAudioAttributes(exoPlayer, mixWithOthers)
+    }
+
+    fun dispose() {
+        disposeMediaSession()
+        disposeRemoteNotifications()
+        if (isInitialized) {
+            exoPlayer!!.stop()
+        }
+        textureEntry.release()
+        eventChannel.setStreamHandler(null)
+        if (surface != null) {
+            surface!!.release()
+        }
+        exoPlayer?.release()
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || javaClass != other.javaClass) return false
+        val that = other as BetterPlayer
+        if (if (exoPlayer != null) exoPlayer != that.exoPlayer else that.exoPlayer != null) return false
+        return if (surface != null) surface == that.surface else that.surface == null
+    }
+
+    override fun hashCode(): Int {
+        var result = exoPlayer?.hashCode() ?: 0
+        result = 31 * result + if (surface != null) surface.hashCode() else 0
+        return result
+    }
+
+    companion object {
+        private const val TAG = "BetterPlayer"
+        private const val FORMAT_SS = "ss"
+        private const val FORMAT_DASH = "dash"
+        private const val FORMAT_HLS = "hls"
+        private const val FORMAT_OTHER = "other"
+        private const val DEFAULT_NOTIFICATION_CHANNEL = "BETTER_PLAYER_NOTIFICATION"
+        private const val NOTIFICATION_ID = 20772077
+
+        //Clear cache without accessing BetterPlayerCache.
+        fun clearCache(context: Context, result: MethodChannel.Result) {
+            try {
+                val file = File(context.cacheDir, "betterPlayerCache")
+                deleteDirectory(file)
+                result.success(null)
+            } catch (exception: Exception) {
+                Log.e(TAG, exception.toString())
+                result.error("", "", "")
+            }
+        }
+
+        private fun deleteDirectory(file: File) {
+            if (file.isDirectory) {
+                val entries = file.listFiles()
+                if (entries != null) {
+                    for (entry in entries) {
+                        deleteDirectory(entry)
+                    }
+                }
+            }
+            if (!file.delete()) {
+                Log.e(TAG, "Failed to delete cache dir.")
+            }
+        }
+
+        //Start pre cache of video. Invoke work manager job and start caching in background.
+        fun preCache(
+            context: Context?, dataSource: String?, preCacheSize: Long,
+            maxCacheSize: Long, maxCacheFileSize: Long, headers: Map<String, String?>,
+            cacheKey: String?, result: MethodChannel.Result
+        ) {
+            val dataBuilder = Data.Builder()
+                .putString(BetterPlayerPlugin.URL_PARAMETER, dataSource)
+                .putLong(BetterPlayerPlugin.PRE_CACHE_SIZE_PARAMETER, preCacheSize)
+                .putLong(BetterPlayerPlugin.MAX_CACHE_SIZE_PARAMETER, maxCacheSize)
+                .putLong(BetterPlayerPlugin.MAX_CACHE_FILE_SIZE_PARAMETER, maxCacheFileSize)
+            if (cacheKey != null) {
+                dataBuilder.putString(BetterPlayerPlugin.CACHE_KEY_PARAMETER, cacheKey)
+            }
+            for (headerKey in headers.keys) {
+                dataBuilder.putString(
+                    BetterPlayerPlugin.HEADER_PARAMETER + headerKey,
+                    headers[headerKey]
+                )
+            }
+            val cacheWorkRequest = OneTimeWorkRequest.Builder(CacheWorker::class.java)
+                .addTag(dataSource!!)
+                .setInputData(dataBuilder.build()).build()
+            WorkManager.getInstance(context!!).enqueue(cacheWorkRequest)
+            result.success(null)
+        }
+
+        //Stop pre cache of video with given url. If there's no work manager job for given url, then
+        //it will be ignored.
+        fun stopPreCache(context: Context?, url: String?, result: MethodChannel.Result) {
+            WorkManager.getInstance(context!!).cancelAllWorkByTag(url!!)
+            result.success(null)
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/android/src/main/kotlin/com/jhomlala/better_player/BetterPlayerCache.kt b/android/src/main/kotlin/com/jhomlala/better_player/BetterPlayerCache.kt
new file mode 100644
index 000000000..d951341d9
--- /dev/null
+++ b/android/src/main/kotlin/com/jhomlala/better_player/BetterPlayerCache.kt
@@ -0,0 +1,40 @@
+package com.jhomlala.better_player
+
+import android.content.Context
+import android.util.Log
+import com.google.android.exoplayer2.upstream.cache.SimpleCache
+import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
+import com.google.android.exoplayer2.database.ExoDatabaseProvider
+import java.io.File
+import java.lang.Exception
+
+object BetterPlayerCache {
+    @Volatile
+    private var instance: SimpleCache? = null
+    fun createCache(context: Context, cacheFileSize: Long): SimpleCache? {
+        if (instance == null) {
+            synchronized(BetterPlayerCache::class.java) {
+                if (instance == null) {
+                    instance = SimpleCache(
+                        File(context.cacheDir, "betterPlayerCache"),
+                        LeastRecentlyUsedCacheEvictor(cacheFileSize),
+                        ExoDatabaseProvider(context)
+                    )
+                }
+            }
+        }
+        return instance
+    }
+
+    @JvmStatic
+    fun releaseCache() {
+        try {
+            if (instance != null) {
+                instance!!.release()
+                instance = null
+            }
+        } catch (exception: Exception) {
+            Log.e("BetterPlayerCache", exception.toString())
+        }
+    }
+}
\ No newline at end of file
diff --git a/android/src/main/kotlin/com/jhomlala/better_player/BetterPlayerPlugin.kt b/android/src/main/kotlin/com/jhomlala/better_player/BetterPlayerPlugin.kt
new file mode 100644
index 000000000..ced52802a
--- /dev/null
+++ b/android/src/main/kotlin/com/jhomlala/better_player/BetterPlayerPlugin.kt
@@ -0,0 +1,549 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+package com.jhomlala.better_player
+
+import android.app.Activity
+import android.app.PictureInPictureParams
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import android.util.LongSparseArray
+import com.jhomlala.better_player.BetterPlayerCache.releaseCache
+import io.flutter.embedding.engine.plugins.FlutterPlugin
+import io.flutter.embedding.engine.plugins.activity.ActivityAware
+import io.flutter.plugin.common.MethodChannel.MethodCallHandler
+import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding
+import io.flutter.embedding.engine.loader.FlutterLoader
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+import io.flutter.plugin.common.EventChannel
+import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
+import io.flutter.plugin.common.BinaryMessenger
+import io.flutter.view.TextureRegistry
+import java.lang.Exception
+import java.util.HashMap
+
+/**
+ * Android platform implementation of the VideoPlayerPlugin.
+ */
+class BetterPlayerPlugin : FlutterPlugin, ActivityAware, MethodCallHandler {
+    private val videoPlayers = LongSparseArray<BetterPlayer>()
+    private val dataSources = LongSparseArray<Map<String, Any?>>()
+    private var flutterState: FlutterState? = null
+    private var currentNotificationTextureId: Long = -1
+    private var currentNotificationDataSource: Map<String, Any?>? = null
+    private var activity: Activity? = null
+    private var pipHandler: Handler? = null
+    private var pipRunnable: Runnable? = null
+    override fun onAttachedToEngine(binding: FlutterPluginBinding) {
+        val loader = FlutterLoader()
+        flutterState = FlutterState(
+            binding.applicationContext,
+            binding.binaryMessenger, object : KeyForAssetFn {
+                override fun get(asset: String?): String {
+                    return loader.getLookupKeyForAsset(
+                        asset!!
+                    )
+                }
+
+            }, object : KeyForAssetAndPackageName {
+                override fun get(asset: String?, packageName: String?): String {
+                    return loader.getLookupKeyForAsset(
+                        asset!!, packageName!!
+                    )
+                }
+            },
+            binding.textureRegistry
+        )
+        flutterState!!.startListening(this)
+    }
+
+
+    override fun onDetachedFromEngine(binding: FlutterPluginBinding) {
+        if (flutterState == null) {
+            Log.wtf(TAG, "Detached from the engine before registering to it.")
+        }
+        disposeAllPlayers()
+        releaseCache()
+        flutterState!!.stopListening()
+        flutterState = null
+    }
+
+    override fun onAttachedToActivity(binding: ActivityPluginBinding) {
+        activity = binding.activity
+    }
+
+    override fun onDetachedFromActivityForConfigChanges() {}
+
+    override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {}
+
+    override fun onDetachedFromActivity() {}
+
+    private fun disposeAllPlayers() {
+        for (i in 0 until videoPlayers.size()) {
+            videoPlayers.valueAt(i).dispose()
+        }
+        videoPlayers.clear()
+        dataSources.clear()
+    }
+
+    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
+        if (flutterState == null || flutterState!!.textureRegistry == null) {
+            result.error("no_activity", "better_player plugin requires a foreground activity", null)
+            return
+        }
+        when (call.method) {
+            INIT_METHOD -> disposeAllPlayers()
+            CREATE_METHOD -> {
+                val handle = flutterState!!.textureRegistry!!.createSurfaceTexture()
+                val eventChannel = EventChannel(
+                    flutterState!!.binaryMessenger, EVENTS_CHANNEL + handle.id()
+                )
+                var customDefaultLoadControl: CustomDefaultLoadControl? = null
+                if (call.hasArgument(MIN_BUFFER_MS) && call.hasArgument(MAX_BUFFER_MS) &&
+                    call.hasArgument(BUFFER_FOR_PLAYBACK_MS) &&
+                    call.hasArgument(BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS)
+                ) {
+                    customDefaultLoadControl = CustomDefaultLoadControl(
+                        call.argument(MIN_BUFFER_MS),
+                        call.argument(MAX_BUFFER_MS),
+                        call.argument(BUFFER_FOR_PLAYBACK_MS),
+                        call.argument(BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS)
+                    )
+                }
+                val player = BetterPlayer(
+                    flutterState!!.applicationContext, eventChannel, handle,
+                    customDefaultLoadControl, result
+                )
+                videoPlayers.put(handle.id(), player)
+            }
+            PRE_CACHE_METHOD -> preCache(call, result)
+            STOP_PRE_CACHE_METHOD -> stopPreCache(call, result)
+            CLEAR_CACHE_METHOD -> clearCache(result)
+            else -> {
+                val textureId = (call.argument<Any>(TEXTURE_ID_PARAMETER) as Number?)!!.toLong()
+                val player = videoPlayers[textureId]
+                if (player == null) {
+                    result.error(
+                        "Unknown textureId",
+                        "No video player associated with texture id $textureId",
+                        null
+                    )
+                    return
+                }
+                onMethodCall(call, result, textureId, player)
+            }
+        }
+    }
+
+    private fun onMethodCall(
+        call: MethodCall,
+        result: MethodChannel.Result,
+        textureId: Long,
+        player: BetterPlayer
+    ) {
+        when (call.method) {
+            SET_DATA_SOURCE_METHOD -> {
+                setDataSource(call, result, player)
+            }
+            SET_LOOPING_METHOD -> {
+                player.setLooping(call.argument(LOOPING_PARAMETER)!!)
+                result.success(null)
+            }
+            SET_VOLUME_METHOD -> {
+                player.setVolume(call.argument(VOLUME_PARAMETER)!!)
+                result.success(null)
+            }
+            PLAY_METHOD -> {
+                setupNotification(player)
+                player.play()
+                result.success(null)
+            }
+            PAUSE_METHOD -> {
+                player.pause()
+                result.success(null)
+            }
+            SEEK_TO_METHOD -> {
+                val location = (call.argument<Any>(LOCATION_PARAMETER) as Number?)!!.toInt()
+                player.seekTo(location)
+                result.success(null)
+            }
+            POSITION_METHOD -> {
+                result.success(player.position)
+                player.sendBufferingUpdate(false)
+            }
+            ABSOLUTE_POSITION_METHOD -> result.success(player.absolutePosition)
+            SET_SPEED_METHOD -> {
+                player.setSpeed(call.argument(SPEED_PARAMETER)!!)
+                result.success(null)
+            }
+            SET_TRACK_PARAMETERS_METHOD -> {
+                player.setTrackParameters(
+                    call.argument(WIDTH_PARAMETER)!!,
+                    call.argument(HEIGHT_PARAMETER)!!,
+                    call.argument(BITRATE_PARAMETER)!!
+                )
+                result.success(null)
+            }
+            ENABLE_PICTURE_IN_PICTURE_METHOD -> {
+                enablePictureInPicture(player)
+                result.success(null)
+            }
+            DISABLE_PICTURE_IN_PICTURE_METHOD -> {
+                disablePictureInPicture(player)
+                result.success(null)
+            }
+            IS_PICTURE_IN_PICTURE_SUPPORTED_METHOD -> result.success(
+                isPictureInPictureSupported()
+            )
+            SET_AUDIO_TRACK_METHOD -> {
+                val name = call.argument<String?>(NAME_PARAMETER)
+                val index = call.argument<Int?>(INDEX_PARAMETER)
+                if (name != null && index != null) {
+                    player.setAudioTrack(name, index)
+                }
+                result.success(null)
+            }
+            SET_MIX_WITH_OTHERS_METHOD -> {
+                val mixWitOthers = call.argument<Boolean?>(
+                    MIX_WITH_OTHERS_PARAMETER
+                )
+                if (mixWitOthers != null) {
+                    player.setMixWithOthers(mixWitOthers)
+                }
+            }
+            DISPOSE_METHOD -> {
+                dispose(player, textureId)
+                result.success(null)
+            }
+            else -> result.notImplemented()
+        }
+    }
+
+    private fun setDataSource(
+        call: MethodCall,
+        result: MethodChannel.Result,
+        player: BetterPlayer
+    ) {
+        val dataSource = call.argument<Map<String, Any?>>(DATA_SOURCE_PARAMETER)!!
+        dataSources.put(getTextureId(player)!!, dataSource)
+        val key = getParameter(dataSource, KEY_PARAMETER, "")
+        val headers: Map<String, String> = getParameter(dataSource, HEADERS_PARAMETER, HashMap())
+        val overriddenDuration: Number = getParameter(dataSource, OVERRIDDEN_DURATION_PARAMETER, 0)
+        if (dataSource[ASSET_PARAMETER] != null) {
+            val asset = getParameter(dataSource, ASSET_PARAMETER, "")
+            val assetLookupKey: String = if (dataSource[PACKAGE_PARAMETER] != null) {
+                val packageParameter = getParameter(
+                    dataSource,
+                    PACKAGE_PARAMETER,
+                    ""
+                )
+                flutterState!!.keyForAssetAndPackageName[asset, packageParameter]
+            } else {
+                flutterState!!.keyForAsset[asset]
+            }
+            player.setDataSource(
+                flutterState!!.applicationContext,
+                key,
+                "asset:///$assetLookupKey",
+                null,
+                result,
+                headers,
+                false,
+                0L,
+                0L,
+                overriddenDuration.toLong(),
+                null,
+                null, null, null
+            )
+        } else {
+            val useCache = getParameter(dataSource, USE_CACHE_PARAMETER, false)
+            val maxCacheSizeNumber: Number = getParameter(dataSource, MAX_CACHE_SIZE_PARAMETER, 0)
+            val maxCacheFileSizeNumber: Number =
+                getParameter(dataSource, MAX_CACHE_FILE_SIZE_PARAMETER, 0)
+            val maxCacheSize = maxCacheSizeNumber.toLong()
+            val maxCacheFileSize = maxCacheFileSizeNumber.toLong()
+            val uri = getParameter(dataSource, URI_PARAMETER, "")
+            val cacheKey = getParameter<String?>(dataSource, CACHE_KEY_PARAMETER, null)
+            val formatHint = getParameter<String?>(dataSource, FORMAT_HINT_PARAMETER, null)
+            val licenseUrl = getParameter<String?>(dataSource, LICENSE_URL_PARAMETER, null)
+            val clearKey = getParameter<String?>(dataSource, DRM_CLEARKEY_PARAMETER, null)
+            val drmHeaders: Map<String, String> =
+                getParameter(dataSource, DRM_HEADERS_PARAMETER, HashMap())
+            player.setDataSource(
+                flutterState!!.applicationContext,
+                key,
+                uri,
+                formatHint,
+                result,
+                headers,
+                useCache,
+                maxCacheSize,
+                maxCacheFileSize,
+                overriddenDuration.toLong(),
+                licenseUrl,
+                drmHeaders,
+                cacheKey,
+                clearKey
+            )
+        }
+    }
+
+    /**
+     * Start pre cache of video.
+     *
+     * @param call   - invoked method data
+     * @param result - result which should be updated
+     */
+    private fun preCache(call: MethodCall, result: MethodChannel.Result) {
+        val dataSource = call.argument<Map<String, Any?>>(DATA_SOURCE_PARAMETER)
+        if (dataSource != null) {
+            val maxCacheSizeNumber: Number =
+                getParameter(dataSource, MAX_CACHE_SIZE_PARAMETER, 100 * 1024 * 1024)
+            val maxCacheFileSizeNumber: Number =
+                getParameter(dataSource, MAX_CACHE_FILE_SIZE_PARAMETER, 10 * 1024 * 1024)
+            val maxCacheSize = maxCacheSizeNumber.toLong()
+            val maxCacheFileSize = maxCacheFileSizeNumber.toLong()
+            val preCacheSizeNumber: Number =
+                getParameter(dataSource, PRE_CACHE_SIZE_PARAMETER, 3 * 1024 * 1024)
+            val preCacheSize = preCacheSizeNumber.toLong()
+            val uri = getParameter(dataSource, URI_PARAMETER, "")
+            val cacheKey = getParameter<String?>(dataSource, CACHE_KEY_PARAMETER, null)
+            val headers: Map<String, String> =
+                getParameter(dataSource, HEADERS_PARAMETER, HashMap())
+            BetterPlayer.preCache(
+                flutterState!!.applicationContext,
+                uri,
+                preCacheSize,
+                maxCacheSize,
+                maxCacheFileSize,
+                headers,
+                cacheKey,
+                result
+            )
+        }
+    }
+
+    /**
+     * Stop pre cache video process (if exists).
+     *
+     * @param call   - invoked method data
+     * @param result - result which should be updated
+     */
+    private fun stopPreCache(call: MethodCall, result: MethodChannel.Result) {
+        val url = call.argument<String>(URL_PARAMETER)
+        BetterPlayer.stopPreCache(flutterState!!.applicationContext, url, result)
+    }
+
+    private fun clearCache(result: MethodChannel.Result) {
+        BetterPlayer.clearCache(flutterState!!.applicationContext, result)
+    }
+
+    private fun getTextureId(betterPlayer: BetterPlayer): Long? {
+        for (index in 0 until videoPlayers.size()) {
+            if (betterPlayer === videoPlayers.valueAt(index)) {
+                return videoPlayers.keyAt(index)
+            }
+        }
+        return null
+    }
+
+    private fun setupNotification(betterPlayer: BetterPlayer) {
+        try {
+            val textureId = getTextureId(betterPlayer)
+            if (textureId != null) {
+                val dataSource = dataSources[textureId]
+                //Don't setup notification for the same source.
+                if (textureId == currentNotificationTextureId && currentNotificationDataSource != null && dataSource != null && currentNotificationDataSource === dataSource) {
+                    return
+                }
+                currentNotificationDataSource = dataSource
+                currentNotificationTextureId = textureId
+                removeOtherNotificationListeners()
+                val showNotification = getParameter(dataSource, SHOW_NOTIFICATION_PARAMETER, false)
+                if (showNotification) {
+                    val title = getParameter(dataSource, TITLE_PARAMETER, "")
+                    val author = getParameter(dataSource, AUTHOR_PARAMETER, "")
+                    val imageUrl = getParameter(dataSource, IMAGE_URL_PARAMETER, "")
+                    val notificationChannelName =
+                        getParameter<String?>(dataSource, NOTIFICATION_CHANNEL_NAME_PARAMETER, null)
+                    val activityName =
+                        getParameter(dataSource, ACTIVITY_NAME_PARAMETER, "MainActivity")
+                    betterPlayer.setupPlayerNotification(
+                        flutterState!!.applicationContext,
+                        title, author, imageUrl, notificationChannelName, activityName
+                    )
+                }
+            }
+        } catch (exception: Exception) {
+            Log.e(TAG, "SetupNotification failed", exception)
+        }
+    }
+
+    private fun removeOtherNotificationListeners() {
+        for (index in 0 until videoPlayers.size()) {
+            videoPlayers.valueAt(index).disposeRemoteNotifications()
+        }
+    }
+    @Suppress("UNCHECKED_CAST")
+    private fun <T> getParameter(parameters: Map<String, Any?>?, key: String, defaultValue: T): T {
+        if (parameters!!.containsKey(key)) {
+            val value = parameters[key]
+            if (value != null) {
+                return value as T
+            }
+        }
+        return defaultValue
+    }
+
+
+    private fun isPictureInPictureSupported(): Boolean {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && activity != null && activity!!.packageManager
+            .hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
+    }
+
+    private fun enablePictureInPicture(player: BetterPlayer) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            player.setupMediaSession(flutterState!!.applicationContext, true)
+            activity!!.enterPictureInPictureMode(PictureInPictureParams.Builder().build())
+            startPictureInPictureListenerTimer(player)
+            player.onPictureInPictureStatusChanged(true)
+        }
+    }
+
+    private fun disablePictureInPicture(player: BetterPlayer) {
+        stopPipHandler()
+        activity!!.moveTaskToBack(false)
+        player.onPictureInPictureStatusChanged(false)
+        player.disposeMediaSession()
+    }
+
+    private fun startPictureInPictureListenerTimer(player: BetterPlayer) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            pipHandler = Handler(Looper.getMainLooper())
+            pipRunnable = Runnable {
+                if (activity!!.isInPictureInPictureMode) {
+                    pipHandler!!.postDelayed(pipRunnable!!, 100)
+                } else {
+                    player.onPictureInPictureStatusChanged(false)
+                    player.disposeMediaSession()
+                    stopPipHandler()
+                }
+            }
+            pipHandler!!.post(pipRunnable!!)
+        }
+    }
+
+    private fun dispose(player: BetterPlayer, textureId: Long) {
+        player.dispose()
+        videoPlayers.remove(textureId)
+        dataSources.remove(textureId)
+        stopPipHandler()
+    }
+
+    private fun stopPipHandler() {
+        if (pipHandler != null) {
+            pipHandler!!.removeCallbacksAndMessages(null)
+            pipHandler = null
+        }
+        pipRunnable = null
+    }
+
+    private interface KeyForAssetFn {
+        operator fun get(asset: String?): String
+    }
+
+    private interface KeyForAssetAndPackageName {
+        operator fun get(asset: String?, packageName: String?): String
+    }
+
+    private class FlutterState(
+        val applicationContext: Context,
+        val binaryMessenger: BinaryMessenger,
+        val keyForAsset: KeyForAssetFn,
+        val keyForAssetAndPackageName: KeyForAssetAndPackageName,
+        val textureRegistry: TextureRegistry?
+    ) {
+        private val methodChannel: MethodChannel = MethodChannel(binaryMessenger, CHANNEL)
+
+        fun startListening(methodCallHandler: BetterPlayerPlugin?) {
+            methodChannel.setMethodCallHandler(methodCallHandler)
+        }
+
+        fun stopListening() {
+            methodChannel.setMethodCallHandler(null)
+        }
+
+    }
+
+    companion object {
+        private const val TAG = "BetterPlayerPlugin"
+        private const val CHANNEL = "better_player_channel"
+        private const val EVENTS_CHANNEL = "better_player_channel/videoEvents"
+        private const val DATA_SOURCE_PARAMETER = "dataSource"
+        private const val KEY_PARAMETER = "key"
+        private const val HEADERS_PARAMETER = "headers"
+        private const val USE_CACHE_PARAMETER = "useCache"
+        private const val ASSET_PARAMETER = "asset"
+        private const val PACKAGE_PARAMETER = "package"
+        private const val URI_PARAMETER = "uri"
+        private const val FORMAT_HINT_PARAMETER = "formatHint"
+        private const val TEXTURE_ID_PARAMETER = "textureId"
+        private const val LOOPING_PARAMETER = "looping"
+        private const val VOLUME_PARAMETER = "volume"
+        private const val LOCATION_PARAMETER = "location"
+        private const val SPEED_PARAMETER = "speed"
+        private const val WIDTH_PARAMETER = "width"
+        private const val HEIGHT_PARAMETER = "height"
+        private const val BITRATE_PARAMETER = "bitrate"
+        private const val SHOW_NOTIFICATION_PARAMETER = "showNotification"
+        private const val TITLE_PARAMETER = "title"
+        private const val AUTHOR_PARAMETER = "author"
+        private const val IMAGE_URL_PARAMETER = "imageUrl"
+        private const val NOTIFICATION_CHANNEL_NAME_PARAMETER = "notificationChannelName"
+        private const val OVERRIDDEN_DURATION_PARAMETER = "overriddenDuration"
+        private const val NAME_PARAMETER = "name"
+        private const val INDEX_PARAMETER = "index"
+        private const val LICENSE_URL_PARAMETER = "licenseUrl"
+        private const val DRM_HEADERS_PARAMETER = "drmHeaders"
+        private const val DRM_CLEARKEY_PARAMETER = "clearKey"
+        private const val MIX_WITH_OTHERS_PARAMETER = "mixWithOthers"
+        const val URL_PARAMETER = "url"
+        const val PRE_CACHE_SIZE_PARAMETER = "preCacheSize"
+        const val MAX_CACHE_SIZE_PARAMETER = "maxCacheSize"
+        const val MAX_CACHE_FILE_SIZE_PARAMETER = "maxCacheFileSize"
+        const val HEADER_PARAMETER = "header_"
+        const val FILE_PATH_PARAMETER = "filePath"
+        const val ACTIVITY_NAME_PARAMETER = "activityName"
+        const val MIN_BUFFER_MS = "minBufferMs"
+        const val MAX_BUFFER_MS = "maxBufferMs"
+        const val BUFFER_FOR_PLAYBACK_MS = "bufferForPlaybackMs"
+        const val BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = "bufferForPlaybackAfterRebufferMs"
+        const val CACHE_KEY_PARAMETER = "cacheKey"
+        private const val INIT_METHOD = "init"
+        private const val CREATE_METHOD = "create"
+        private const val SET_DATA_SOURCE_METHOD = "setDataSource"
+        private const val SET_LOOPING_METHOD = "setLooping"
+        private const val SET_VOLUME_METHOD = "setVolume"
+        private const val PLAY_METHOD = "play"
+        private const val PAUSE_METHOD = "pause"
+        private const val SEEK_TO_METHOD = "seekTo"
+        private const val POSITION_METHOD = "position"
+        private const val ABSOLUTE_POSITION_METHOD = "absolutePosition"
+        private const val SET_SPEED_METHOD = "setSpeed"
+        private const val SET_TRACK_PARAMETERS_METHOD = "setTrackParameters"
+        private const val SET_AUDIO_TRACK_METHOD = "setAudioTrack"
+        private const val ENABLE_PICTURE_IN_PICTURE_METHOD = "enablePictureInPicture"
+        private const val DISABLE_PICTURE_IN_PICTURE_METHOD = "disablePictureInPicture"
+        private const val IS_PICTURE_IN_PICTURE_SUPPORTED_METHOD = "isPictureInPictureSupported"
+        private const val SET_MIX_WITH_OTHERS_METHOD = "setMixWithOthers"
+        private const val CLEAR_CACHE_METHOD = "clearCache"
+        private const val DISPOSE_METHOD = "dispose"
+        private const val PRE_CACHE_METHOD = "preCache"
+        private const val STOP_PRE_CACHE_METHOD = "stopPreCache"
+    }
+}
\ No newline at end of file
diff --git a/android/src/main/kotlin/com/jhomlala/better_player/CacheDataSourceFactory.kt b/android/src/main/kotlin/com/jhomlala/better_player/CacheDataSourceFactory.kt
new file mode 100644
index 000000000..a1e76c957
--- /dev/null
+++ b/android/src/main/kotlin/com/jhomlala/better_player/CacheDataSourceFactory.kt
@@ -0,0 +1,37 @@
+package com.jhomlala.better_player
+
+import android.content.Context
+import com.google.android.exoplayer2.upstream.DataSource
+import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
+import com.google.android.exoplayer2.upstream.cache.CacheDataSource
+import com.google.android.exoplayer2.upstream.FileDataSource
+import com.google.android.exoplayer2.upstream.cache.CacheDataSink
+import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter
+
+internal class CacheDataSourceFactory(
+    private val context: Context,
+    private val maxCacheSize: Long,
+    private val maxFileSize: Long,
+    upstreamDataSource: DataSource.Factory?
+) : DataSource.Factory {
+    private val defaultDatasourceFactory: DefaultDataSourceFactory
+    override fun createDataSource(): CacheDataSource {
+        val betterPlayerCache = BetterPlayerCache.createCache(context, maxCacheSize)
+            ?: throw IllegalStateException("Cache can't be null.")
+
+        return CacheDataSource(
+            betterPlayerCache,
+            defaultDatasourceFactory.createDataSource(),
+            FileDataSource(),
+            CacheDataSink(betterPlayerCache, maxFileSize),
+            CacheDataSource.FLAG_BLOCK_ON_CACHE or CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
+            null
+        )
+    }
+
+    init {
+        val bandwidthMeter = DefaultBandwidthMeter.Builder(context).build()
+        defaultDatasourceFactory =
+            DefaultDataSourceFactory(context, bandwidthMeter, upstreamDataSource!!)
+    }
+}
\ No newline at end of file
diff --git a/android/src/main/kotlin/com/jhomlala/better_player/CacheWorker.kt b/android/src/main/kotlin/com/jhomlala/better_player/CacheWorker.kt
new file mode 100644
index 000000000..e1f376c5a
--- /dev/null
+++ b/android/src/main/kotlin/com/jhomlala/better_player/CacheWorker.kt
@@ -0,0 +1,99 @@
+package com.jhomlala.better_player
+
+import android.content.Context
+import android.net.Uri
+import android.util.Log
+import com.jhomlala.better_player.DataSourceUtils.isHTTP
+import com.jhomlala.better_player.DataSourceUtils.getUserAgent
+import com.jhomlala.better_player.DataSourceUtils.getDataSourceFactory
+import androidx.work.WorkerParameters
+import com.google.android.exoplayer2.upstream.cache.CacheWriter
+import androidx.work.Worker
+import com.google.android.exoplayer2.upstream.DataSpec
+import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException
+import java.lang.Exception
+import java.util.*
+
+/**
+ * Cache worker which download part of video and save in cache for future usage. The cache job
+ * will be executed in work manager.
+ */
+class CacheWorker(
+    private val mContext: Context,
+    params: WorkerParameters
+) : Worker(mContext, params) {
+    private var mCacheWriter: CacheWriter? = null
+    private var mLastCacheReportIndex = 0
+    override fun doWork(): Result {
+        try {
+            val data = inputData
+            val url = data.getString(BetterPlayerPlugin.URL_PARAMETER)
+            val cacheKey = data.getString(BetterPlayerPlugin.CACHE_KEY_PARAMETER)
+            val preCacheSize = data.getLong(BetterPlayerPlugin.PRE_CACHE_SIZE_PARAMETER, 0)
+            val maxCacheSize = data.getLong(BetterPlayerPlugin.MAX_CACHE_SIZE_PARAMETER, 0)
+            val maxCacheFileSize = data.getLong(BetterPlayerPlugin.MAX_CACHE_FILE_SIZE_PARAMETER, 0)
+            val headers: MutableMap<String, String> = HashMap()
+            for (key in data.keyValueMap.keys) {
+                if (key.contains(BetterPlayerPlugin.HEADER_PARAMETER)) {
+                    val keySplit =
+                        key.split(BetterPlayerPlugin.HEADER_PARAMETER.toRegex()).toTypedArray()[0]
+                    headers[keySplit] = Objects.requireNonNull(data.keyValueMap[key]) as String
+                }
+            }
+            val uri = Uri.parse(url)
+            if (isHTTP(uri)) {
+                val userAgent = getUserAgent(headers)
+                val dataSourceFactory = getDataSourceFactory(userAgent, headers)
+                var dataSpec = DataSpec(uri, 0, preCacheSize)
+                if (cacheKey != null && cacheKey.isNotEmpty()) {
+                    dataSpec = dataSpec.buildUpon().setKey(cacheKey).build()
+                }
+                val cacheDataSourceFactory = CacheDataSourceFactory(
+                    mContext,
+                    maxCacheSize,
+                    maxCacheFileSize,
+                    dataSourceFactory
+                )
+                mCacheWriter = CacheWriter(
+                    cacheDataSourceFactory.createDataSource(),
+                    dataSpec,
+                    null
+                ) { _: Long, bytesCached: Long, _: Long ->
+                    val completedData = (bytesCached * 100f / preCacheSize).toDouble()
+                    if (completedData >= mLastCacheReportIndex * 10) {
+                        mLastCacheReportIndex += 1
+                        Log.d(
+                            TAG,
+                            "Completed pre cache of " + url + ": " + completedData.toInt() + "%"
+                        )
+                    }
+                }
+                mCacheWriter!!.cache()
+            } else {
+                Log.e(TAG, "Preloading only possible for remote data sources")
+                return Result.failure()
+            }
+        } catch (exception: Exception) {
+            Log.e(TAG, exception.toString())
+            return if (exception is HttpDataSourceException) {
+                Result.success()
+            } else {
+                Result.failure()
+            }
+        }
+        return Result.success()
+    }
+
+    override fun onStopped() {
+        try {
+            mCacheWriter!!.cancel()
+            super.onStopped()
+        } catch (exception: Exception) {
+            Log.e(TAG, exception.toString())
+        }
+    }
+
+    companion object {
+        private const val TAG = "CacheWorker"
+    }
+}
\ No newline at end of file
diff --git a/android/src/main/kotlin/com/jhomlala/better_player/CustomDefaultLoadControl.kt b/android/src/main/kotlin/com/jhomlala/better_player/CustomDefaultLoadControl.kt
new file mode 100644
index 000000000..07a1b0f79
--- /dev/null
+++ b/android/src/main/kotlin/com/jhomlala/better_player/CustomDefaultLoadControl.kt
@@ -0,0 +1,55 @@
+package com.jhomlala.better_player
+
+import com.google.android.exoplayer2.DefaultLoadControl
+
+internal class CustomDefaultLoadControl {
+    /**
+     * The default minimum duration of media that the player will attempt to ensure is buffered
+     * at all times, in milliseconds.
+     */
+    @JvmField
+    val minBufferMs: Int
+
+    /**
+     * The default maximum duration of media that the player will attempt to buffer, in milliseconds.
+     */
+    @JvmField
+    val maxBufferMs: Int
+
+    /**
+     * The default duration of media that must be buffered for playback to start or resume following
+     * a user action such as a seek, in milliseconds.
+     */
+    @JvmField
+    val bufferForPlaybackMs: Int
+
+    /**
+     * he default duration of media that must be buffered for playback to resume after a rebuffer,
+     * in milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user
+     * action.
+     */
+    @JvmField
+    val bufferForPlaybackAfterRebufferMs: Int
+
+    constructor() {
+        minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS
+        maxBufferMs = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS
+        bufferForPlaybackMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS
+        bufferForPlaybackAfterRebufferMs =
+            DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
+    }
+
+    constructor(
+        minBufferMs: Int?,
+        maxBufferMs: Int?,
+        bufferForPlaybackMs: Int?,
+        bufferForPlaybackAfterRebufferMs: Int?
+    ) {
+        this.minBufferMs = minBufferMs ?: DefaultLoadControl.DEFAULT_MIN_BUFFER_MS
+        this.maxBufferMs = maxBufferMs ?: DefaultLoadControl.DEFAULT_MAX_BUFFER_MS
+        this.bufferForPlaybackMs =
+            bufferForPlaybackMs ?: DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS
+        this.bufferForPlaybackAfterRebufferMs = bufferForPlaybackAfterRebufferMs
+            ?: DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
+    }
+}
\ No newline at end of file
diff --git a/android/src/main/kotlin/com/jhomlala/better_player/DataSourceUtils.kt b/android/src/main/kotlin/com/jhomlala/better_player/DataSourceUtils.kt
new file mode 100644
index 000000000..d19c0e6f1
--- /dev/null
+++ b/android/src/main/kotlin/com/jhomlala/better_player/DataSourceUtils.kt
@@ -0,0 +1,56 @@
+package com.jhomlala.better_player
+
+import android.net.Uri
+import com.google.android.exoplayer2.upstream.DataSource
+import com.jhomlala.better_player.DataSourceUtils
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
+
+internal object DataSourceUtils {
+    private const val USER_AGENT = "User-Agent"
+    private const val USER_AGENT_PROPERTY = "http.agent"
+
+    @JvmStatic
+    fun getUserAgent(headers: Map<String, String>?): String {
+        var userAgent = System.getProperty(USER_AGENT_PROPERTY)
+        if (headers != null && headers.containsKey(USER_AGENT)) {
+            val userAgentHeader = headers[USER_AGENT]
+            if (userAgentHeader != null) {
+                userAgent = userAgentHeader
+            }
+        }
+        return userAgent
+    }
+
+    @JvmStatic
+    fun getDataSourceFactory(
+        userAgent: String?,
+        headers: Map<String, String>?
+    ): DataSource.Factory {
+        val dataSourceFactory: DataSource.Factory = DefaultHttpDataSource.Factory()
+            .setUserAgent(userAgent)
+            .setAllowCrossProtocolRedirects(true)
+            .setConnectTimeoutMs(DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS)
+            .setReadTimeoutMs(DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS)
+        if (headers != null) {
+            val notNullHeaders = mutableMapOf<String, String>()
+            headers.forEach { entry ->
+                if (entry.key != null && entry.value != null) {
+                    notNullHeaders[entry.key!!] = entry.value!!
+                }
+            }
+            (dataSourceFactory as DefaultHttpDataSource.Factory).setDefaultRequestProperties(
+                notNullHeaders
+            )
+        }
+        return dataSourceFactory
+    }
+
+    @JvmStatic
+    fun isHTTP(uri: Uri?): Boolean {
+        if (uri == null || uri.scheme == null) {
+            return false
+        }
+        val scheme = uri.scheme
+        return scheme == "http" || scheme == "https"
+    }
+}
\ No newline at end of file
diff --git a/android/src/main/kotlin/com/jhomlala/better_player/ImageWorker.kt b/android/src/main/kotlin/com/jhomlala/better_player/ImageWorker.kt
new file mode 100644
index 000000000..1a7bea3c4
--- /dev/null
+++ b/android/src/main/kotlin/com/jhomlala/better_player/ImageWorker.kt
@@ -0,0 +1,120 @@
+package com.jhomlala.better_player
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.net.Uri
+import android.util.Log
+import androidx.work.Data
+import androidx.work.WorkerParameters
+import androidx.work.ListenableWorker
+import androidx.work.Worker
+import com.jhomlala.better_player.BetterPlayerPlugin
+import com.jhomlala.better_player.DataSourceUtils
+import com.jhomlala.better_player.ImageWorker
+import java.io.FileOutputStream
+import java.io.InputStream
+import java.lang.Exception
+import java.net.HttpURLConnection
+import java.net.URL
+
+class ImageWorker(
+    context: Context,
+    params: WorkerParameters
+) : Worker(context, params) {
+    override fun doWork(): Result {
+        return try {
+            val imageUrl = inputData.getString(BetterPlayerPlugin.URL_PARAMETER)
+                ?: return Result.failure()
+            var bitmap: Bitmap? = null
+            bitmap = if (DataSourceUtils.isHTTP(Uri.parse(imageUrl))) {
+                getBitmapFromExternalURL(imageUrl)
+            } else {
+                getBitmapFromInternalURL(imageUrl)
+            }
+            val fileName = imageUrl.hashCode().toString() + IMAGE_EXTENSION
+            val filePath = applicationContext.cacheDir.absolutePath + fileName
+            if (bitmap == null) {
+                return Result.failure()
+            }
+            val out = FileOutputStream(filePath)
+            bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
+            val data =
+                Data.Builder().putString(BetterPlayerPlugin.FILE_PATH_PARAMETER, filePath).build()
+            Result.success(data)
+        } catch (e: Exception) {
+            e.printStackTrace()
+            Result.failure()
+        }
+    }
+
+    private fun getBitmapFromExternalURL(src: String): Bitmap? {
+        var inputStream: InputStream? = null
+        return try {
+            val url = URL(src)
+            var connection = url.openConnection() as HttpURLConnection
+            inputStream = connection.inputStream
+            val options = BitmapFactory.Options()
+            options.inJustDecodeBounds = true
+            BitmapFactory.decodeStream(inputStream, null, options)
+            inputStream.close()
+            connection = url.openConnection() as HttpURLConnection
+            inputStream = connection.inputStream
+            options.inSampleSize = calculateBitmapInSampleSize(
+                options
+            )
+            options.inJustDecodeBounds = false
+            BitmapFactory.decodeStream(inputStream, null, options)
+        } catch (exception: Exception) {
+            Log.e(TAG, "Failed to get bitmap from external url: $src")
+            null
+        } finally {
+            try {
+                inputStream?.close()
+            } catch (exception: Exception) {
+                Log.e(TAG, "Failed to close bitmap input stream/")
+            }
+        }
+    }
+
+    private fun calculateBitmapInSampleSize(
+        options: BitmapFactory.Options
+    ): Int {
+        val height = options.outHeight
+        val width = options.outWidth
+        var inSampleSize = 1
+        if (height > DEFAULT_NOTIFICATION_IMAGE_SIZE_PX
+            || width > DEFAULT_NOTIFICATION_IMAGE_SIZE_PX
+        ) {
+            val halfHeight = height / 2
+            val halfWidth = width / 2
+            while (halfHeight / inSampleSize >= DEFAULT_NOTIFICATION_IMAGE_SIZE_PX
+                && halfWidth / inSampleSize >= DEFAULT_NOTIFICATION_IMAGE_SIZE_PX
+            ) {
+                inSampleSize *= 2
+            }
+        }
+        return inSampleSize
+    }
+
+    private fun getBitmapFromInternalURL(src: String): Bitmap? {
+        return try {
+            val options = BitmapFactory.Options()
+            options.inJustDecodeBounds = true
+            options.inSampleSize = calculateBitmapInSampleSize(
+                options
+            )
+            options.inJustDecodeBounds = false
+            BitmapFactory.decodeFile(src)
+        } catch (exception: Exception) {
+            Log.e(TAG, "Failed to get bitmap from internal url: $src")
+            null
+        }
+    }
+
+    companion object {
+        private const val TAG = "ImageWorker"
+        private const val IMAGE_EXTENSION = ".png"
+        private const val DEFAULT_NOTIFICATION_IMAGE_SIZE_PX = 256
+    }
+}
\ No newline at end of file
diff --git a/android/src/main/kotlin/com/jhomlala/better_player/QueuingEventSink.kt b/android/src/main/kotlin/com/jhomlala/better_player/QueuingEventSink.kt
new file mode 100644
index 000000000..8dd4479c8
--- /dev/null
+++ b/android/src/main/kotlin/com/jhomlala/better_player/QueuingEventSink.kt
@@ -0,0 +1,74 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+package com.jhomlala.better_player
+
+import io.flutter.plugin.common.EventChannel.EventSink
+import java.util.ArrayList
+
+/**
+ * And implementation of [EventSink] which can wrap an underlying sink.
+ * It delivers messages immediately when downstream is available, but it queues messages before
+ * the delegate event sink is set with setDelegate.
+ * This class is not thread-safe. All calls must be done on the same thread or synchronized
+ * externally.
+ */
+internal class QueuingEventSink : EventSink {
+    private var delegate: EventSink? = null
+    private val eventQueue = ArrayList<Any>()
+    private var done = false
+    fun setDelegate(delegate: EventSink?) {
+        this.delegate = delegate
+        maybeFlush()
+    }
+
+    override fun endOfStream() {
+        enqueue(EndOfStreamEvent())
+        maybeFlush()
+        done = true
+    }
+
+    override fun error(code: String, message: String, details: Any) {
+        enqueue(ErrorEvent(code, message, details))
+        maybeFlush()
+    }
+
+    override fun success(event: Any) {
+        enqueue(event)
+        maybeFlush()
+    }
+
+    private fun enqueue(event: Any) {
+        if (done) {
+            return
+        }
+        eventQueue.add(event)
+    }
+
+    private fun maybeFlush() {
+        if (delegate == null) {
+            return
+        }
+        for (event in eventQueue) {
+            when (event) {
+                is EndOfStreamEvent -> {
+                    delegate!!.endOfStream()
+                }
+                is ErrorEvent -> {
+                    delegate!!.error(event.code, event.message, event.details)
+                }
+                else -> {
+                    delegate!!.success(event)
+                }
+            }
+        }
+        eventQueue.clear()
+    }
+
+    private class EndOfStreamEvent
+    private class ErrorEvent(
+        var code: String,
+        var message: String,
+        var details: Any
+    )
+}
\ No newline at end of file
diff --git a/docs/events.md b/docs/events.md
index de80c8ca7..48ae92ff9 100644
--- a/docs/events.md
+++ b/docs/events.md
@@ -12,7 +12,8 @@ You can listen to video player events like:
   finished,
   exception,
   controlsVisible,
-  controlsHidden,
+  controlsHiddenStart,
+  controlsHiddenEnd,
   setSpeed,
   changedSubtitles,
   changedTrack,
diff --git a/doc/install.md b/docs/install.md
similarity index 97%
rename from doc/install.md
rename to docs/install.md
index 7cfc40564..a825565e3 100644
--- a/doc/install.md
+++ b/docs/install.md
@@ -4,7 +4,7 @@
 
 ```yaml
 dependencies:
-  better_player: ^0.0.76
+  better_player: ^0.0.78
 ```
 
 2. Install it
diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
index b04f5e26b..d243520f8 100644
--- a/example/android/app/build.gradle
+++ b/example/android/app/build.gradle
@@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
 apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
 
 android {
-    compileSdkVersion 29
+    compileSdkVersion 31
 
     sourceSets {
         main.java.srcDirs += 'src/main/kotlin'
@@ -37,19 +37,17 @@ android {
     }
 
     defaultConfig {
-        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
         applicationId "com.jhomlala.better_player_example"
         minSdkVersion 16
-        targetSdkVersion 28
+        targetSdkVersion 31
         versionCode flutterVersionCode.toInteger()
         versionName flutterVersionName
+        multiDexEnabled true
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
     }
 
     buildTypes {
         release {
-            // TODO: Add your own signing config for the release build.
-            // Signing with the debug keys for now, so `flutter run --release` works.
             signingConfig signingConfigs.debug
         }
     }
@@ -60,8 +58,6 @@ flutter {
 }
 
 dependencies {
-    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
-    testImplementation 'junit:junit:4.12'
-    androidTestImplementation 'androidx.test:runner:1.1.1'
-    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
+    implementation "androidx.multidex:multidex:$multidexVersion"
 }
diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
index 3074b8e02..9bcf886a4 100644
--- a/example/android/app/src/main/AndroidManifest.xml
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -1,13 +1,9 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
     package="com.jhomlala.better_player_example">
-    <!-- io.flutter.app.FlutterApplication is an android.app.Application that
-         calls FlutterMain.startInitialization(this); in its onCreate method.
-         In most cases you can leave this as-is, but you if you want to provide
-         additional functionality it is fine to subclass or reimplement
-         FlutterApplication and put your custom class here. -->
-    <application android:usesCleartextTraffic="true">
+    <application android:usesCleartextTraffic="true"
         android:name="io.flutter.app.FlutterApplication"
-        android:label="better_player_example"
+        android:label="Better Player Example"
         android:icon="@mipmap/ic_launcher">
         <activity
             android:name=".MainActivity"
@@ -18,14 +14,13 @@
             android:resizeableActivity="true"
             android:supportsPictureInPicture="true"
             android:theme="@style/LaunchTheme"
-            android:windowSoftInputMode="adjustResize">
+            android:windowSoftInputMode="adjustResize"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
-        <!-- Don't delete the meta-data below.
-             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
         <meta-data
             android:name="flutterEmbedding"
             android:value="2" />
diff --git a/example/android/app/src/main/kotlin/com/jhomlala/better_player_example/BetterPlayerService.kt b/example/android/app/src/main/kotlin/com/jhomlala/better_player_example/BetterPlayerService.kt
index 67c7dff0d..17c43945f 100644
--- a/example/android/app/src/main/kotlin/com/jhomlala/better_player_example/BetterPlayerService.kt
+++ b/example/android/app/src/main/kotlin/com/jhomlala/better_player_example/BetterPlayerService.kt
@@ -23,22 +23,26 @@ class BetterPlayerService : Service() {
 
     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
         val channelId =
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                    createNotificationChannel(channelId, "Channel")
-                } else {
-                    ""
-                }
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                createNotificationChannel(channelId, "Channel")
+            } else {
+                ""
+            }
         val notificationIntent = Intent(this, MainActivity::class.java)
-        val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0)
+        val pendingIntent =
+            PendingIntent.getActivity(
+                this, 0, notificationIntent,
+                PendingIntent.FLAG_IMMUTABLE
+            )
 
 
         val notificationBuilder = NotificationCompat.Builder(this, channelId)
-                .setContentTitle("Better Player Notification")
-                .setContentText("Better Player is running")
-                .setSmallIcon(R.mipmap.ic_launcher)
-                .setPriority(PRIORITY_MIN)
-                .setOngoing(true)
-                .setContentIntent(pendingIntent)
+            .setContentTitle("Better Player Notification")
+            .setContentText("Better Player is running")
+            .setSmallIcon(R.mipmap.ic_launcher)
+            .setPriority(PRIORITY_MIN)
+            .setOngoing(true)
+            .setContentIntent(pendingIntent)
 
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
             notificationBuilder.setCategory(Notification.CATEGORY_SERVICE);
@@ -49,8 +53,10 @@ class BetterPlayerService : Service() {
 
     @RequiresApi(Build.VERSION_CODES.O)
     private fun createNotificationChannel(channelId: String, channelName: String): String {
-        val chan = NotificationChannel(channelId,
-                channelName, NotificationManager.IMPORTANCE_NONE)
+        val chan = NotificationChannel(
+            channelId,
+            channelName, NotificationManager.IMPORTANCE_NONE
+        )
         val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
         service.createNotificationChannel(chan)
         return channelId
@@ -59,8 +65,9 @@ class BetterPlayerService : Service() {
     override fun onTaskRemoved(rootIntent: Intent?) {
         try {
             val notificationManager =
-                    getSystemService(
-                            Context.NOTIFICATION_SERVICE) as NotificationManager
+                getSystemService(
+                    Context.NOTIFICATION_SERVICE
+                ) as NotificationManager
             notificationManager.cancel(notificationId)
         } catch (exception: Exception) {
 
diff --git a/example/android/build.gradle b/example/android/build.gradle
index 3100ad2d5..ca7d37f31 100644
--- a/example/android/build.gradle
+++ b/example/android/build.gradle
@@ -1,13 +1,15 @@
 buildscript {
-    ext.kotlin_version = '1.3.50'
+    ext.kotlinVersion = "1.5.31"
+    ext.gradleVersion = "4.1.0"
+    ext.multidexVersion = "2.0.1"
     repositories {
         google()
         jcenter()
     }
 
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.5.0'
-        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+        classpath "com.android.tools.build:gradle:$gradleVersion"
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
     }
 }
 
diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties
index 296b146b7..3fcc9eb32 100644
--- a/example/android/gradle/wrapper/gradle-wrapper.properties
+++ b/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,5 @@ distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
+
diff --git a/example/lib/pages/custom_controls/change_player_theme_page.dart b/example/lib/pages/custom_controls/change_player_theme_page.dart
index 6eec22afe..3bb7b4358 100644
--- a/example/lib/pages/custom_controls/change_player_theme_page.dart
+++ b/example/lib/pages/custom_controls/change_player_theme_page.dart
@@ -104,9 +104,12 @@ class _ChangePlayerThemePageState extends State<ChangePlayerThemePage> {
                           controlsConfiguration:
                               BetterPlayerControlsConfiguration(
                             playerTheme: _playerTheme,
-                            customControlsBuilder: (controller) =>
-                                CustomControlsWidget(
+                            customControlsBuilder:
+                                (controller, onControlsVisibilityChanged) =>
+                                    CustomControlsWidget(
                               controller: controller,
+                              onControlsVisibilityChanged:
+                                  onControlsVisibilityChanged,
                             ),
                           ),
                         ),
diff --git a/example/lib/pages/custom_controls/custom_controls_widget.dart b/example/lib/pages/custom_controls/custom_controls_widget.dart
index 4644109bf..1eb230ac0 100644
--- a/example/lib/pages/custom_controls/custom_controls_widget.dart
+++ b/example/lib/pages/custom_controls/custom_controls_widget.dart
@@ -3,8 +3,13 @@ import 'package:flutter/material.dart';
 
 class CustomControlsWidget extends StatefulWidget {
   final BetterPlayerController? controller;
+  final Function(bool visbility)? onControlsVisibilityChanged;
 
-  const CustomControlsWidget({Key? key, this.controller}) : super(key: key);
+  const CustomControlsWidget({
+    Key? key,
+    this.controller,
+    this.onControlsVisibilityChanged,
+  }) : super(key: key);
 
   @override
   _CustomControlsWidgetState createState() => _CustomControlsWidgetState();
diff --git a/example/lib/pages/video_list/video_list_widget.dart b/example/lib/pages/video_list/video_list_widget.dart
index fec693e07..82b682712 100644
--- a/example/lib/pages/video_list/video_list_widget.dart
+++ b/example/lib/pages/video_list/video_list_widget.dart
@@ -49,7 +49,7 @@ class _VideoListWidgetState extends State<VideoListWidget> {
                   videoListData!.videoUrl,
                   notificationConfiguration:
                       BetterPlayerNotificationConfiguration(
-                          showNotification: true,
+                          showNotification: false,
                           title: videoListData!.videoTitle,
                           author: "Test"),
                   bufferingConfiguration: BetterPlayerBufferingConfiguration(
@@ -59,9 +59,7 @@ class _VideoListWidgetState extends State<VideoListWidget> {
                       bufferForPlaybackAfterRebufferMs: 2000),
                 ),
                 configuration: BetterPlayerConfiguration(
-                  autoPlay: false,
-                  aspectRatio: 1,
-                ),
+                    autoPlay: false, aspectRatio: 1, handleLifecycle: true),
                 //key: Key(videoListData.hashCode.toString()),
                 playFraction: 0.8,
                 betterPlayerListVideoPlayerController: controller,
diff --git a/lib/src/configuration/better_player_buffering_configuration.dart b/lib/src/configuration/better_player_buffering_configuration.dart
index edc36aef3..1e37212a8 100644
--- a/lib/src/configuration/better_player_buffering_configuration.dart
+++ b/lib/src/configuration/better_player_buffering_configuration.dart
@@ -3,10 +3,10 @@
 class BetterPlayerBufferingConfiguration {
   ///Constants values are from the offical exoplayer documentation
   ///https://exoplayer.dev/doc/reference/constant-values.html#com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS
-  static const defaultMinBufferMs = 50000;
-  static const defaultMaxBufferMs = 13107200;
-  static const defaultBufferForPlaybackMs = 2500;
-  static const defaultBufferForPlaybackAfterRebufferMs = 5000;
+  static const defaultMinBufferMs = 25000;
+  static const defaultMaxBufferMs = 6553600;
+  static const defaultBufferForPlaybackMs = 3000;
+  static const defaultBufferForPlaybackAfterRebufferMs = 6000;
 
   /// The default minimum duration of media that the player will attempt to
   /// ensure is buffered at all times, in milliseconds.
diff --git a/lib/src/configuration/better_player_controls_configuration.dart b/lib/src/configuration/better_player_controls_configuration.dart
index 180a2ab67..8d0bf8026 100644
--- a/lib/src/configuration/better_player_controls_configuration.dart
+++ b/lib/src/configuration/better_player_controls_configuration.dart
@@ -78,8 +78,8 @@ class BetterPlayerControlsConfiguration {
   final Duration controlsHideTime;
 
   ///Parameter used to build custom controls
-  final Widget Function(BetterPlayerController controller)?
-      customControlsBuilder;
+  final Widget Function(BetterPlayerController controller,
+      Function(bool) onPlayerVisibilityChanged)? customControlsBuilder;
 
   ///Parameter used to change theme of the player
   final BetterPlayerTheme? playerTheme;
diff --git a/lib/src/configuration/better_player_event_type.dart b/lib/src/configuration/better_player_event_type.dart
index 1a7391f34..20f3a8f88 100644
--- a/lib/src/configuration/better_player_event_type.dart
+++ b/lib/src/configuration/better_player_event_type.dart
@@ -11,7 +11,8 @@ enum BetterPlayerEventType {
   finished,
   exception,
   controlsVisible,
-  controlsHidden,
+  controlsHiddenStart,
+  controlsHiddenEnd,
   setSpeed,
   changedSubtitles,
   changedTrack,
diff --git a/lib/src/controls/better_player_controls_state.dart b/lib/src/controls/better_player_controls_state.dart
index 4991bbc04..dcd490fe8 100644
--- a/lib/src/controls/better_player_controls_state.dart
+++ b/lib/src/controls/better_player_controls_state.dart
@@ -22,6 +22,8 @@ abstract class BetterPlayerControlsState<T extends StatefulWidget>
 
   VideoPlayerValue? get latestValue;
 
+  bool controlsNotVisible = true;
+
   void cancelAndRestartTimer();
 
   bool isVideoFinished(VideoPlayerValue? videoPlayerValue) {
@@ -517,4 +519,15 @@ abstract class BetterPlayerControlsState<T extends StatefulWidget>
   Widget buildLTRDirectionality(Widget child) {
     return Directionality(textDirection: TextDirection.ltr, child: child);
   }
+
+  ///Called when player controls visibility should be changed.
+  void changePlayerControlsNotVisible(bool notVisible) {
+    setState(() {
+      if (notVisible) {
+        betterPlayerController?.postEvent(
+            BetterPlayerEvent(BetterPlayerEventType.controlsHiddenStart));
+      }
+      controlsNotVisible = notVisible;
+    });
+  }
 }
diff --git a/lib/src/controls/better_player_cupertino_controls.dart b/lib/src/controls/better_player_cupertino_controls.dart
index 4e67b83d1..f77d911b9 100644
--- a/lib/src/controls/better_player_cupertino_controls.dart
+++ b/lib/src/controls/better_player_cupertino_controls.dart
@@ -34,7 +34,6 @@ class _BetterPlayerCupertinoControlsState
   final marginSize = 5.0;
   VideoPlayerValue? _latestValue;
   double? _latestVolume;
-  bool _hideStuff = true;
   Timer? _hideTimer;
   Timer? _expandCollapseTimer;
   Timer? _initTimer;
@@ -108,11 +107,9 @@ class _BetterPlayerCupertinoControlsState
         if (BetterPlayerMultipleGestureDetector.of(context) != null) {
           BetterPlayerMultipleGestureDetector.of(context)!.onTap?.call();
         }
-        _hideStuff
+        controlsNotVisible
             ? cancelAndRestartTimer()
-            : setState(() {
-                _hideStuff = true;
-              });
+            : changePlayerControlsNotVisible(true);
       },
       onDoubleTap: () {
         if (BetterPlayerMultipleGestureDetector.of(context) != null) {
@@ -127,7 +124,7 @@ class _BetterPlayerCupertinoControlsState
         }
       },
       child: AbsorbPointer(
-          absorbing: _hideStuff,
+          absorbing: controlsNotVisible,
           child:
               isFullScreen ? SafeArea(child: controlsColumn) : controlsColumn),
     );
@@ -170,7 +167,7 @@ class _BetterPlayerCupertinoControlsState
       return const SizedBox();
     }
     return AnimatedOpacity(
-      opacity: _hideStuff ? 0.0 : 1.0,
+      opacity: controlsNotVisible ? 0.0 : 1.0,
       duration: _controlsConfiguration.controlsHideTime,
       onEnd: _onPlayerHide,
       child: Container(
@@ -251,7 +248,7 @@ class _BetterPlayerCupertinoControlsState
     return GestureDetector(
       onTap: _onExpandCollapse,
       child: AnimatedOpacity(
-        opacity: _hideStuff ? 0.0 : 1.0,
+        opacity: controlsNotVisible ? 0.0 : 1.0,
         duration: _controlsConfiguration.controlsHideTime,
         child: ClipRRect(
           borderRadius: BorderRadius.circular(10),
@@ -281,22 +278,16 @@ class _BetterPlayerCupertinoControlsState
       child: GestureDetector(
         onTap: _latestValue != null && _latestValue!.isPlaying
             ? () {
-                if (_hideStuff == true) {
+                if (controlsNotVisible == true) {
                   cancelAndRestartTimer();
                 } else {
                   _hideTimer?.cancel();
-
-                  setState(() {
-                    _hideStuff = true;
-                  });
+                  changePlayerControlsNotVisible(true);
                 }
               }
             : () {
                 _hideTimer?.cancel();
-
-                setState(() {
-                  _hideStuff = false;
-                });
+                changePlayerControlsNotVisible(false);
               },
         child: Container(
           color: Colors.transparent,
@@ -318,7 +309,7 @@ class _BetterPlayerCupertinoControlsState
         onShowMoreClicked();
       },
       child: AnimatedOpacity(
-        opacity: _hideStuff ? 0.0 : 1.0,
+        opacity: controlsNotVisible ? 0.0 : 1.0,
         duration: _controlsConfiguration.controlsHideTime,
         child: ClipRRect(
           borderRadius: BorderRadius.circular(10.0),
@@ -363,7 +354,7 @@ class _BetterPlayerCupertinoControlsState
         }
       },
       child: AnimatedOpacity(
-        opacity: _hideStuff ? 0.0 : 1.0,
+        opacity: controlsNotVisible ? 0.0 : 1.0,
         duration: _controlsConfiguration.controlsHideTime,
         child: ClipRRect(
           borderRadius: BorderRadius.circular(10.0),
@@ -591,10 +582,8 @@ class _BetterPlayerCupertinoControlsState
   @override
   void cancelAndRestartTimer() {
     _hideTimer?.cancel();
-    setState(() {
-      _hideStuff = false;
-      _startHideTimer();
-    });
+    changePlayerControlsNotVisible(false);
+    _startHideTimer();
   }
 
   Future<void> _initialize() async {
@@ -609,31 +598,25 @@ class _BetterPlayerCupertinoControlsState
 
     if (_controlsConfiguration.showControlsOnInitialize) {
       _initTimer = Timer(const Duration(milliseconds: 200), () {
-        setState(() {
-          _hideStuff = false;
-        });
+        changePlayerControlsNotVisible(false);
       });
     }
     _controlsVisibilityStreamSubscription =
         _betterPlayerController!.controlsVisibilityStream.listen((state) {
-      setState(() {
-        _hideStuff = !state;
-      });
-      if (!_hideStuff) {
+      changePlayerControlsNotVisible(!state);
+
+      if (!controlsNotVisible) {
         cancelAndRestartTimer();
       }
     });
   }
 
   void _onExpandCollapse() {
-    setState(() {
-      _hideStuff = true;
-
-      _betterPlayerController!.toggleFullScreen();
-      _expandCollapseTimer = Timer(_controlsConfiguration.controlsHideTime, () {
-        setState(() {
-          cancelAndRestartTimer();
-        });
+    changePlayerControlsNotVisible(true);
+    _betterPlayerController!.toggleFullScreen();
+    _expandCollapseTimer = Timer(_controlsConfiguration.controlsHideTime, () {
+      setState(() {
+        cancelAndRestartTimer();
       });
     });
   }
@@ -651,6 +634,9 @@ class _BetterPlayerCupertinoControlsState
           onDragEnd: () {
             _startHideTimer();
           },
+          onTapDown: () {
+            cancelAndRestartTimer();
+          },
           colors: BetterPlayerProgressColors(
               playedColor: _controlsConfiguration.progressBarPlayedColor,
               handleColor: _controlsConfiguration.progressBarHandleColor,
@@ -668,29 +654,28 @@ class _BetterPlayerCupertinoControlsState
     if (_latestValue?.position != null && _latestValue?.duration != null) {
       isFinished = _latestValue!.position >= _latestValue!.duration!;
     }
-    setState(() {
-      if (_controller!.value.isPlaying) {
-        _hideStuff = false;
-        _hideTimer?.cancel();
-        _betterPlayerController!.pause();
-      } else {
-        cancelAndRestartTimer();
 
-        if (!_controller!.value.initialized) {
-          if (_betterPlayerController!.betterPlayerDataSource?.liveStream ==
-              true) {
-            _betterPlayerController!.play();
-            _betterPlayerController!.cancelNextVideoTimer();
-          }
-        } else {
-          if (isFinished) {
-            _betterPlayerController!.seekTo(const Duration());
-          }
+    if (_controller!.value.isPlaying) {
+      changePlayerControlsNotVisible(false);
+      _hideTimer?.cancel();
+      _betterPlayerController!.pause();
+    } else {
+      cancelAndRestartTimer();
+
+      if (!_controller!.value.initialized) {
+        if (_betterPlayerController!.betterPlayerDataSource?.liveStream ==
+            true) {
           _betterPlayerController!.play();
           _betterPlayerController!.cancelNextVideoTimer();
         }
+      } else {
+        if (isFinished) {
+          _betterPlayerController!.seekTo(const Duration());
+        }
+        _betterPlayerController!.play();
+        _betterPlayerController!.cancelNextVideoTimer();
       }
-    });
+    }
   }
 
   void _startHideTimer() {
@@ -698,22 +683,20 @@ class _BetterPlayerCupertinoControlsState
       return;
     }
     _hideTimer = Timer(const Duration(seconds: 3), () {
-      setState(() {
-        _hideStuff = true;
-      });
+      changePlayerControlsNotVisible(true);
     });
   }
 
   void _updateState() {
     if (mounted) {
-      if (!_hideStuff ||
+      if (!controlsNotVisible ||
           isVideoFinished(_controller!.value) ||
           _wasLoading ||
           isLoading(_controller!.value)) {
         setState(() {
           _latestValue = _controller!.value;
           if (isVideoFinished(_latestValue)) {
-            _hideStuff = false;
+            changePlayerControlsNotVisible(false);
           }
         });
       }
@@ -721,8 +704,8 @@ class _BetterPlayerCupertinoControlsState
   }
 
   void _onPlayerHide() {
-    _betterPlayerController!.toggleControlsVisibility(!_hideStuff);
-    widget.onControlsVisibilityChanged(!_hideStuff);
+    _betterPlayerController!.toggleControlsVisibility(!controlsNotVisible);
+    widget.onControlsVisibilityChanged(!controlsNotVisible);
   }
 
   Widget _buildErrorWidget() {
@@ -794,7 +777,7 @@ class _BetterPlayerCupertinoControlsState
                   betterPlayerController!.betterPlayerGlobalKey!);
             },
             child: AnimatedOpacity(
-              opacity: _hideStuff ? 0.0 : 1.0,
+              opacity: controlsNotVisible ? 0.0 : 1.0,
               duration: _controlsConfiguration.controlsHideTime,
               child: ClipRRect(
                 borderRadius: BorderRadius.circular(10),
diff --git a/lib/src/controls/better_player_cupertino_progress_bar.dart b/lib/src/controls/better_player_cupertino_progress_bar.dart
index d7a3f5f19..000f44e22 100644
--- a/lib/src/controls/better_player_cupertino_progress_bar.dart
+++ b/lib/src/controls/better_player_cupertino_progress_bar.dart
@@ -14,6 +14,7 @@ class BetterPlayerCupertinoVideoProgressBar extends StatefulWidget {
     this.onDragEnd,
     this.onDragStart,
     this.onDragUpdate,
+    this.onTapDown,
     Key? key,
   })  : colors = colors ?? BetterPlayerProgressColors(),
         super(key: key);
@@ -24,6 +25,7 @@ class BetterPlayerCupertinoVideoProgressBar extends StatefulWidget {
   final Function()? onDragStart;
   final Function()? onDragEnd;
   final Function()? onDragUpdate;
+  final Function()? onTapDown;
 
   @override
   _VideoProgressBarState createState() {
@@ -113,6 +115,9 @@ class _VideoProgressBarState
 
         seekToRelativePosition(details.globalPosition);
         _setupUpdateBlockTimer();
+        if (widget.onTapDown != null) {
+          widget.onTapDown!();
+        }
       },
       child: Center(
         child: Container(
diff --git a/lib/src/controls/better_player_material_controls.dart b/lib/src/controls/better_player_material_controls.dart
index ce12034d5..ded2121f6 100644
--- a/lib/src/controls/better_player_material_controls.dart
+++ b/lib/src/controls/better_player_material_controls.dart
@@ -35,7 +35,6 @@ class _BetterPlayerMaterialControlsState
     extends BetterPlayerControlsState<BetterPlayerMaterialControls> {
   VideoPlayerValue? _latestValue;
   double? _latestVolume;
-  bool _hideStuff = true;
   Timer? _hideTimer;
   Timer? _initTimer;
   Timer? _showAfterExpandCollapseTimer;
@@ -77,18 +76,15 @@ class _BetterPlayerMaterialControlsState
         if (BetterPlayerMultipleGestureDetector.of(context) != null) {
           BetterPlayerMultipleGestureDetector.of(context)!.onTap?.call();
         }
-        _hideStuff
+        controlsNotVisible
             ? cancelAndRestartTimer()
-            : setState(() {
-                _hideStuff = true;
-              });
+            : changePlayerControlsNotVisible(true);
       },
       onDoubleTap: () {
         if (BetterPlayerMultipleGestureDetector.of(context) != null) {
           BetterPlayerMultipleGestureDetector.of(context)!.onDoubleTap?.call();
         }
         cancelAndRestartTimer();
-        //_onPlayPause();
       },
       onLongPress: () {
         if (BetterPlayerMultipleGestureDetector.of(context) != null) {
@@ -96,10 +92,9 @@ class _BetterPlayerMaterialControlsState
         }
       },
       child: AbsorbPointer(
-        absorbing: _hideStuff,
+        absorbing: controlsNotVisible,
         child: Stack(
           fit: StackFit.expand,
-          //crossAxisAlignment: CrossAxisAlignment.stretch,
           children: [
             if (_wasLoading)
               Center(child: _buildLoadingWidget())
@@ -193,28 +188,29 @@ class _BetterPlayerMaterialControlsState
     }
 
     return Container(
-        child: (_controlsConfiguration.enableOverflowMenu)
-            ? AnimatedOpacity(
-                opacity: _hideStuff ? 0.0 : 1.0,
-                duration: _controlsConfiguration.controlsHideTime,
-                onEnd: _onPlayerHide,
-                child: Container(
-                  //color: _controlsConfiguration.controlBarColor,
-                  height: _controlsConfiguration.controlBarHeight,
-                  width: double.infinity,
-                  child: Row(
-                    mainAxisAlignment: MainAxisAlignment.end,
-                    children: [
-                      if (_controlsConfiguration.enablePip)
-                        _buildPipButtonWrapperWidget(_hideStuff, _onPlayerHide)
-                      else
-                        const SizedBox(),
-                      _buildMoreButton(),
-                    ],
-                  ),
+      child: (_controlsConfiguration.enableOverflowMenu)
+          ? AnimatedOpacity(
+              opacity: controlsNotVisible ? 0.0 : 1.0,
+              duration: _controlsConfiguration.controlsHideTime,
+              onEnd: _onPlayerHide,
+              child: Container(
+                height: _controlsConfiguration.controlBarHeight,
+                width: double.infinity,
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.end,
+                  children: [
+                    if (_controlsConfiguration.enablePip)
+                      _buildPipButtonWrapperWidget(
+                          controlsNotVisible, _onPlayerHide)
+                    else
+                      const SizedBox(),
+                    _buildMoreButton(),
+                  ],
                 ),
-              )
-            : const SizedBox());
+              ),
+            )
+          : const SizedBox(),
+    );
   }
 
   Widget _buildPipButton() {
@@ -282,12 +278,11 @@ class _BetterPlayerMaterialControlsState
       return const SizedBox();
     }
     return AnimatedOpacity(
-      opacity: _hideStuff ? 0.0 : 1.0,
+      opacity: controlsNotVisible ? 0.0 : 1.0,
       duration: _controlsConfiguration.controlsHideTime,
       onEnd: _onPlayerHide,
       child: Container(
         height: _controlsConfiguration.controlBarHeight + 20.0,
-        //color: _controlsConfiguration.controlBarColor,
         child: Column(
           mainAxisAlignment: MainAxisAlignment.spaceBetween,
           children: <Widget>[
@@ -342,7 +337,7 @@ class _BetterPlayerMaterialControlsState
     return BetterPlayerMaterialClickableWidget(
       onTap: _onExpandCollapse,
       child: AnimatedOpacity(
-        opacity: _hideStuff ? 0.0 : 1.0,
+        opacity: controlsNotVisible ? 0.0 : 1.0,
         duration: _controlsConfiguration.controlsHideTime,
         child: Container(
           height: _controlsConfiguration.controlBarHeight,
@@ -368,7 +363,7 @@ class _BetterPlayerMaterialControlsState
     return Container(
       child: Center(
         child: AnimatedOpacity(
-          opacity: _hideStuff ? 0.0 : 1.0,
+          opacity: controlsNotVisible ? 0.0 : 1.0,
           duration: _controlsConfiguration.controlsHideTime,
           child: _buildMiddleRow(),
         ),
@@ -378,7 +373,7 @@ class _BetterPlayerMaterialControlsState
 
   Widget _buildMiddleRow() {
     return Container(
-      color: Colors.black54, //_controlsConfiguration.controlBarColor,
+      color: _controlsConfiguration.controlBarColor,
       width: double.infinity,
       height: double.infinity,
       child: _betterPlayerController?.isLiveStream() == true
@@ -466,18 +461,13 @@ class _BetterPlayerMaterialControlsState
         if (isFinished) {
           if (_latestValue != null && _latestValue!.isPlaying) {
             if (_displayTapped) {
-              setState(() {
-                _hideStuff = true;
-              });
+              changePlayerControlsNotVisible(true);
             } else {
               cancelAndRestartTimer();
             }
           } else {
             _onPlayPause();
-
-            setState(() {
-              _hideStuff = true;
-            });
+            changePlayerControlsNotVisible(true);
           }
         } else {
           _onPlayPause();
@@ -537,7 +527,7 @@ class _BetterPlayerMaterialControlsState
         }
       },
       child: AnimatedOpacity(
-        opacity: _hideStuff ? 0.0 : 1.0,
+        opacity: controlsNotVisible ? 0.0 : 1.0,
         duration: _controlsConfiguration.controlsHideTime,
         child: ClipRect(
           child: Container(
@@ -611,10 +601,8 @@ class _BetterPlayerMaterialControlsState
     _hideTimer?.cancel();
     _startHideTimer();
 
-    setState(() {
-      _hideStuff = false;
-      _displayTapped = true;
-    });
+    changePlayerControlsNotVisible(false);
+    _displayTapped = true;
   }
 
   Future<void> _initialize() async {
@@ -629,33 +617,26 @@ class _BetterPlayerMaterialControlsState
 
     if (_controlsConfiguration.showControlsOnInitialize) {
       _initTimer = Timer(const Duration(milliseconds: 200), () {
-        setState(() {
-          _hideStuff = false;
-        });
+        changePlayerControlsNotVisible(false);
       });
     }
 
     _controlsVisibilityStreamSubscription =
         _betterPlayerController!.controlsVisibilityStream.listen((state) {
-      setState(() {
-        _hideStuff = !state;
-      });
-      if (!_hideStuff) {
+      changePlayerControlsNotVisible(!state);
+      if (!controlsNotVisible) {
         cancelAndRestartTimer();
       }
     });
   }
 
   void _onExpandCollapse() {
-    setState(() {
-      _hideStuff = true;
-
-      _betterPlayerController!.toggleFullScreen();
-      _showAfterExpandCollapseTimer =
-          Timer(_controlsConfiguration.controlsHideTime, () {
-        setState(() {
-          cancelAndRestartTimer();
-        });
+    changePlayerControlsNotVisible(true);
+    _betterPlayerController!.toggleFullScreen();
+    _showAfterExpandCollapseTimer =
+        Timer(_controlsConfiguration.controlsHideTime, () {
+      setState(() {
+        cancelAndRestartTimer();
       });
     });
   }
@@ -667,40 +648,36 @@ class _BetterPlayerMaterialControlsState
       isFinished = _latestValue!.position >= _latestValue!.duration!;
     }
 
-    setState(() {
-      if (_controller!.value.isPlaying) {
-        _hideStuff = false;
-        _hideTimer?.cancel();
-        _betterPlayerController!.pause();
-      } else {
-        cancelAndRestartTimer();
+    if (_controller!.value.isPlaying) {
+      changePlayerControlsNotVisible(false);
+      _hideTimer?.cancel();
+      _betterPlayerController!.pause();
+    } else {
+      cancelAndRestartTimer();
 
-        if (!_controller!.value.initialized) {
-        } else {
-          if (isFinished) {
-            _betterPlayerController!.seekTo(const Duration());
-          }
-          _betterPlayerController!.play();
-          _betterPlayerController!.cancelNextVideoTimer();
+      if (!_controller!.value.initialized) {
+      } else {
+        if (isFinished) {
+          _betterPlayerController!.seekTo(const Duration());
         }
+        _betterPlayerController!.play();
+        _betterPlayerController!.cancelNextVideoTimer();
       }
-    });
+    }
   }
 
   void _startHideTimer() {
     if (_betterPlayerController!.controlsAlwaysVisible) {
       return;
     }
-    _hideTimer = Timer(const Duration(seconds: 3), () {
-      setState(() {
-        _hideStuff = true;
-      });
+    _hideTimer = Timer(const Duration(milliseconds: 3000), () {
+      changePlayerControlsNotVisible(true);
     });
   }
 
   void _updateState() {
     if (mounted) {
-      if (!_hideStuff ||
+      if (!controlsNotVisible ||
           isVideoFinished(_controller!.value) ||
           _wasLoading ||
           isLoading(_controller!.value)) {
@@ -708,7 +685,7 @@ class _BetterPlayerMaterialControlsState
           _latestValue = _controller!.value;
           if (isVideoFinished(_latestValue) &&
               _betterPlayerController?.isLiveStream() == false) {
-            _hideStuff = false;
+            changePlayerControlsNotVisible(false);
           }
         });
       }
@@ -730,6 +707,9 @@ class _BetterPlayerMaterialControlsState
           onDragEnd: () {
             _startHideTimer();
           },
+          onTapDown: () {
+            cancelAndRestartTimer();
+          },
           colors: BetterPlayerProgressColors(
               playedColor: _controlsConfiguration.progressBarPlayedColor,
               handleColor: _controlsConfiguration.progressBarHandleColor,
@@ -742,8 +722,8 @@ class _BetterPlayerMaterialControlsState
   }
 
   void _onPlayerHide() {
-    _betterPlayerController!.toggleControlsVisibility(!_hideStuff);
-    widget.onControlsVisibilityChanged(!_hideStuff);
+    _betterPlayerController!.toggleControlsVisibility(!controlsNotVisible);
+    widget.onControlsVisibilityChanged(!controlsNotVisible);
   }
 
   Widget? _buildLoadingWidget() {
diff --git a/lib/src/controls/better_player_material_progress_bar.dart b/lib/src/controls/better_player_material_progress_bar.dart
index fcc066b53..bf261c8a7 100644
--- a/lib/src/controls/better_player_material_progress_bar.dart
+++ b/lib/src/controls/better_player_material_progress_bar.dart
@@ -14,6 +14,7 @@ class BetterPlayerMaterialVideoProgressBar extends StatefulWidget {
     this.onDragEnd,
     this.onDragStart,
     this.onDragUpdate,
+    this.onTapDown,
     Key? key,
   })  : colors = colors ?? BetterPlayerProgressColors(),
         super(key: key);
@@ -24,6 +25,7 @@ class BetterPlayerMaterialVideoProgressBar extends StatefulWidget {
   final Function()? onDragStart;
   final Function()? onDragEnd;
   final Function()? onDragUpdate;
+  final Function()? onTapDown;
 
   @override
   _VideoProgressBarState createState() {
@@ -116,6 +118,9 @@ class _VideoProgressBarState
         }
         seekToRelativePosition(details.globalPosition);
         _setupUpdateBlockTimer();
+        if (widget.onTapDown != null) {
+          widget.onTapDown!();
+        }
       },
       child: Center(
         child: Container(
diff --git a/lib/src/core/better_player_controller.dart b/lib/src/core/better_player_controller.dart
index a34dd4b7d..cd259522e 100644
--- a/lib/src/core/better_player_controller.dart
+++ b/lib/src/core/better_player_controller.dart
@@ -740,7 +740,7 @@ class BetterPlayerController {
   void toggleControlsVisibility(bool isVisible) {
     _postEvent(isVisible
         ? BetterPlayerEvent(BetterPlayerEventType.controlsVisible)
-        : BetterPlayerEvent(BetterPlayerEventType.controlsHidden));
+        : BetterPlayerEvent(BetterPlayerEventType.controlsHiddenEnd));
   }
 
   ///Send player event. Shouldn't be used manually.
diff --git a/lib/src/core/better_player_with_controls.dart b/lib/src/core/better_player_with_controls.dart
index 669ad75f9..36ce76b09 100644
--- a/lib/src/core/better_player_with_controls.dart
+++ b/lib/src/core/better_player_with_controls.dart
@@ -176,8 +176,8 @@ class _BetterPlayerWithControlsState extends State<BetterPlayerWithControls> {
 
       if (controlsConfiguration.customControlsBuilder != null &&
           playerTheme == BetterPlayerTheme.custom) {
-        return controlsConfiguration
-            .customControlsBuilder!(betterPlayerController);
+        return controlsConfiguration.customControlsBuilder!(
+            betterPlayerController, onControlsVisibilityChanged);
       } else if (playerTheme == BetterPlayerTheme.material) {
         return _buildMaterialControl();
       } else if (playerTheme == BetterPlayerTheme.cupertino) {
diff --git a/lib/src/hls/better_player_hls_utils.dart b/lib/src/hls/better_player_hls_utils.dart
index 8d5ad8593..0b10ff749 100644
--- a/lib/src/hls/better_player_hls_utils.dart
+++ b/lib/src/hls/better_player_hls_utils.dart
@@ -113,7 +113,11 @@ class BetterPlayerHlsUtils {
           // ignore: use_string_buffers
           realUrl += "${split[index]}/";
         }
-        realUrl += segment.url!;
+        if (segment.url?.startsWith("http") == true) {
+          realUrl = segment.url!;
+        } else {
+          realUrl += segment.url!;
+        }
         hlsSubtitlesUrls.add(realUrl);
 
         if (isSegmented) {
diff --git a/media/15.png b/media/15.png
index 297a556d3..f48265db2 100644
Binary files a/media/15.png and b/media/15.png differ
diff --git a/media/16.png b/media/16.png
index 0b6a0810f..e99ae98a0 100644
Binary files a/media/16.png and b/media/16.png differ
diff --git a/media/2.png b/media/2.png
index 3e62a8761..08aa3b5db 100644
Binary files a/media/2.png and b/media/2.png differ
diff --git a/pubspec.yaml b/pubspec.yaml
index 71bae4471..5a5c327a4 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
 name: better_player
 description: Advanced video player based on video_player and Chewie. It's solves many typical use cases and it's easy to run.
-version: 0.0.77
+version: 0.0.78
 # Disabled because of warning from analyzer
 # authors:
 #  - Jakub Homlala <jhomlala@gmail.com>
diff --git a/test/better_player_controller_test.dart b/test/better_player_controller_test.dart
index 815621618..e6117df33 100644
--- a/test/better_player_controller_test.dart
+++ b/test/better_player_controller_test.dart
@@ -236,7 +236,7 @@ void main() {
             controlsVisibleEventCount += 1;
           }
           if (event.betterPlayerEventType ==
-              BetterPlayerEventType.controlsHidden) {
+              BetterPlayerEventType.controlsHiddenEnd) {
             controlsHiddenEventCount += 1;
           }
         });