diff --git a/app/build.gradle b/app/build.gradle
index 15acb8e..562f539 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -70,6 +70,10 @@ dependencies {
implementation(project(":core:architecture"))
//api(project(":core:architecture-reflect"))
implementation(project(":core:network"))
+
+ implementation(project(":youtube:core"))
+ implementation(project(":youtube:custom-ui"))
+
implementation libs.androidx.navigation.fragment.ktx
implementation(libs.startup)
implementation(libs.hilt.android)
@@ -80,8 +84,6 @@ dependencies {
implementation(libs.protobuf.kotlin.lite)
implementation(libs.kotlinx.serialization.json)
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
- implementation 'com.pierfrancescosoffritti.androidyoutubeplayer:core:13.0.0'
- implementation 'com.pierfrancescosoffritti.androidyoutubeplayer:custom-ui:13.0.0'
implementation(libs.glide) // ImageLoader在用
implementation(libs.okhttp.logging)
implementation(libs.retrofit)
diff --git a/core.build.gradle b/core.build.gradle
index d54893e..dc903df 100644
--- a/core.build.gradle
+++ b/core.build.gradle
@@ -14,7 +14,7 @@ android {
buildTypes {
release {
- minifyEnabled false
+ minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
diff --git a/settings.gradle b/settings.gradle
index 460f2c2..d6734c5 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -19,5 +19,7 @@ dependencyResolutionManagement {
rootProject.name = "VidiDin-android"
apply from: "./core/core.includes.gradle"
+apply from: "./youtube/youtube.includes.gradle"
include ':app'
+
diff --git a/youtube/core/.gitignore b/youtube/core/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/youtube/core/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/youtube/core/build.gradle b/youtube/core/build.gradle
new file mode 100644
index 0000000..f666244
--- /dev/null
+++ b/youtube/core/build.gradle
@@ -0,0 +1,44 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply from: '../libVersions.gradle'
+
+android {
+ compileSdkVersion versions.compileSdk
+
+ defaultConfig {
+ minSdkVersion versions.minSdk
+ targetSdkVersion versions.compileSdk
+ }
+
+ sourceSets {
+ main.res.srcDirs = [
+ 'src/main/res',
+ ]
+ }
+ namespace 'com.pierfrancescosoffritti.androidyoutubeplayer'
+
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ testImplementation "junit:junit:$versions.junit"
+ androidTestImplementation "androidx.test:runner:$versions.runner"
+ androidTestImplementation "androidx.test.espresso:espresso-core:$versions.espressoCore"
+
+ api "androidx.lifecycle:lifecycle-runtime-ktx:$versions.androidxLifecycleRuntime"
+}
+
diff --git a/youtube/core/src/androidTest/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/ExampleInstrumentedTest.kt b/youtube/core/src/androidTest/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..4a79864
--- /dev/null
+++ b/youtube/core/src/androidTest/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/ExampleInstrumentedTest.kt
@@ -0,0 +1,17 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core
+
+import androidx.test.runner.AndroidJUnit4
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ }
+}
diff --git a/youtube/core/src/main/AndroidManifest.xml b/youtube/core/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..cc25894
--- /dev/null
+++ b/youtube/core/src/main/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/PlayerConstants.kt b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/PlayerConstants.kt
new file mode 100644
index 0000000..5004297
--- /dev/null
+++ b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/PlayerConstants.kt
@@ -0,0 +1,39 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.player
+
+class PlayerConstants {
+
+ enum class PlayerState {
+ UNKNOWN, UNSTARTED, ENDED, PLAYING, PAUSED, BUFFERING, VIDEO_CUED
+ }
+
+ enum class PlaybackQuality {
+ UNKNOWN, SMALL, MEDIUM, LARGE, HD720, HD1080, HIGH_RES, DEFAULT
+ }
+
+ enum class PlayerError {
+ UNKNOWN,
+ INVALID_PARAMETER_IN_REQUEST,
+ HTML_5_PLAYER,
+ VIDEO_NOT_FOUND,
+ VIDEO_NOT_PLAYABLE_IN_EMBEDDED_PLAYER,
+ REQUEST_MISSING_HTTP_REFERER
+ }
+
+ enum class PlaybackRate {
+ UNKNOWN, RATE_0_25, RATE_0_5, RATE_0_75, RATE_1, RATE_1_25, RATE_1_5, RATE_1_75, RATE_2
+ }
+}
+
+fun PlayerConstants.PlaybackRate.toFloat(): Float {
+ return when (this) {
+ PlayerConstants.PlaybackRate.UNKNOWN -> 1f
+ PlayerConstants.PlaybackRate.RATE_0_25 -> 0.25f
+ PlayerConstants.PlaybackRate.RATE_0_5 -> 0.5f
+ PlayerConstants.PlaybackRate.RATE_0_75 -> 0.75f
+ PlayerConstants.PlaybackRate.RATE_1 -> 1f
+ PlayerConstants.PlaybackRate.RATE_1_25 -> 1.25f
+ PlayerConstants.PlaybackRate.RATE_1_5 -> 1.5f
+ PlayerConstants.PlaybackRate.RATE_1_75 -> 1.75f
+ PlayerConstants.PlaybackRate.RATE_2 -> 2f
+ }
+}
\ No newline at end of file
diff --git a/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/Providers.kt b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/Providers.kt
new file mode 100644
index 0000000..1d6dccf
--- /dev/null
+++ b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/Providers.kt
@@ -0,0 +1,10 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.player
+
+/**
+ * A callback that accepts a Boolean value.
+ *
+ * This interface is only required to support Java 7 and below.
+ */
+fun interface BooleanProvider {
+ fun accept(value: Boolean)
+}
diff --git a/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/YouTubePlayer.kt b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/YouTubePlayer.kt
new file mode 100644
index 0000000..76137ee
--- /dev/null
+++ b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/YouTubePlayer.kt
@@ -0,0 +1,60 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.player
+
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.YouTubePlayerListener
+
+/**
+ * Use this interface to control the playback of YouTube videos and to listen to their events.
+ */
+interface YouTubePlayer {
+ /**
+ * Loads and automatically plays the video.
+ * @param videoId id of the video
+ * @param startSeconds the time from which the video should start playing
+ */
+ fun loadVideo(videoId: String, startSeconds: Float)
+
+ /**
+ * Loads the video's thumbnail and prepares the player to play the video. Does not automatically play the video.
+ * @param videoId id of the video
+ * @param startSeconds the time from which the video should start playing
+ */
+ fun cueVideo(videoId: String, startSeconds: Float)
+
+ fun play()
+ fun pause()
+
+ /** If the player is playing a playlist, play the next video. */
+ fun nextVideo()
+ /** If the player is playing a playlist, play the previous video. */
+ fun previousVideo()
+ /** If the player is playing a playlist, play the video at position [index]. */
+ fun playVideoAt(index: Int)
+
+ /** If the player is playing a playlist, enable or disable looping of the playlist. */
+ fun setLoop(loop: Boolean)
+
+ /** If the player is playing a playlist, enable or disable shuffling of the playlist. */
+ fun setShuffle(shuffle: Boolean)
+
+ fun mute()
+ fun unMute()
+
+ /** Returns true if the player is muted, false otherwise. */
+ fun isMutedAsync(callback: BooleanProvider)
+
+ /**
+ * @param volumePercent Integer between 0 and 100
+ */
+ fun setVolume(volumePercent: Int)
+
+ /**
+ *
+ * @param time The absolute time in seconds to seek to
+ */
+ fun seekTo(time: Float)
+
+ fun setPlaybackRate(playbackRate: PlayerConstants.PlaybackRate)
+
+ fun addListener(listener: YouTubePlayerListener): Boolean
+ fun removeListener(listener: YouTubePlayerListener): Boolean
+}
diff --git a/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/YouTubePlayerBridge.kt b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/YouTubePlayerBridge.kt
new file mode 100644
index 0000000..5920a04
--- /dev/null
+++ b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/YouTubePlayerBridge.kt
@@ -0,0 +1,210 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.player
+
+import android.os.Handler
+import android.os.Looper
+import android.text.TextUtils
+import android.webkit.JavascriptInterface
+import androidx.annotation.RestrictTo
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.YouTubePlayerListener
+
+
+/**
+ * Bridge used for Javascript-Java communication.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class YouTubePlayerBridge(private val youTubePlayerOwner: YouTubePlayerBridgeCallbacks) {
+
+ companion object {
+ // these constants correspond to the values in the Javascript player
+ private const val STATE_UNSTARTED = "UNSTARTED"
+ private const val STATE_ENDED = "ENDED"
+ private const val STATE_PLAYING = "PLAYING"
+ private const val STATE_PAUSED = "PAUSED"
+ private const val STATE_BUFFERING = "BUFFERING"
+ private const val STATE_CUED = "CUED"
+
+ private const val QUALITY_SMALL = "small"
+ private const val QUALITY_MEDIUM = "medium"
+ private const val QUALITY_LARGE = "large"
+ private const val QUALITY_HD720 = "hd720"
+ private const val QUALITY_HD1080 = "hd1080"
+ private const val QUALITY_HIGH_RES = "highres"
+ private const val QUALITY_DEFAULT = "default"
+
+ private const val RATE_0_25 = "0.25"
+ private const val RATE_0_5 = "0.5"
+ private const val RATE_0_75 = "0.75"
+ private const val RATE_1 = "1"
+ private const val RATE_1_25 = "1.25"
+ private const val RATE_1_5 = "1.5"
+ private const val RATE_1_75 = "1.75"
+ private const val RATE_2 = "2"
+
+ private const val ERROR_INVALID_PARAMETER_IN_REQUEST = "2"
+ private const val ERROR_HTML_5_PLAYER = "5"
+ private const val ERROR_VIDEO_NOT_FOUND = "100"
+ private const val ERROR_VIDEO_NOT_PLAYABLE_IN_EMBEDDED_PLAYER1 = "101"
+ private const val ERROR_VIDEO_NOT_PLAYABLE_IN_EMBEDDED_PLAYER2 = "150"
+ private const val ERROR_REQUEST_MISSING_HTTP_REFERER = "153"
+ }
+
+ private val mainThreadHandler: Handler = Handler(Looper.getMainLooper())
+
+ interface YouTubePlayerBridgeCallbacks {
+ val listeners: Collection
+ fun getInstance(): YouTubePlayer
+ fun onYouTubeIFrameAPIReady()
+ }
+
+ @JavascriptInterface
+ fun sendYouTubeIFrameAPIReady() = mainThreadHandler.post { youTubePlayerOwner.onYouTubeIFrameAPIReady() }
+
+ @JavascriptInterface
+ fun sendReady() = mainThreadHandler.post {
+ youTubePlayerOwner.listeners.forEach { it.onReady(youTubePlayerOwner.getInstance()) }
+ }
+
+ @JavascriptInterface
+ fun sendStateChange(state: String) {
+ val playerState = parsePlayerState(state)
+
+ mainThreadHandler.post {
+ youTubePlayerOwner.listeners.forEach { it.onStateChange(youTubePlayerOwner.getInstance(), playerState) }
+ }
+ }
+
+ @JavascriptInterface
+ fun sendPlaybackQualityChange(quality: String) {
+ val playbackQuality = parsePlaybackQuality(quality)
+
+ mainThreadHandler.post {
+ youTubePlayerOwner.listeners.forEach { it.onPlaybackQualityChange(youTubePlayerOwner.getInstance(), playbackQuality) }
+ }
+ }
+
+ @JavascriptInterface
+ fun sendPlaybackRateChange(rate: String) {
+ val playbackRate = parsePlaybackRate(rate)
+
+ mainThreadHandler.post {
+ youTubePlayerOwner.listeners.forEach { it.onPlaybackRateChange(youTubePlayerOwner.getInstance(), playbackRate) }
+ }
+ }
+
+ @JavascriptInterface
+ fun sendError(error: String) {
+ val playerError = parsePlayerError(error)
+
+ mainThreadHandler.post {
+ youTubePlayerOwner.listeners.forEach { it.onError(youTubePlayerOwner.getInstance(), playerError) }
+ }
+ }
+
+ @JavascriptInterface
+ fun sendApiChange() = mainThreadHandler.post {
+ youTubePlayerOwner.listeners.forEach { it.onApiChange(youTubePlayerOwner.getInstance()) }
+ }
+
+ @JavascriptInterface
+ fun sendVideoCurrentTime(seconds: String) {
+ val currentTimeSeconds = try {
+ seconds.toFloat()
+ } catch (e: NumberFormatException) {
+ e.printStackTrace()
+ return
+ }
+
+ mainThreadHandler.post {
+ youTubePlayerOwner.listeners.forEach { it.onCurrentSecond(youTubePlayerOwner.getInstance(), currentTimeSeconds) }
+ }
+ }
+
+ @JavascriptInterface
+ fun sendVideoDuration(seconds: String) {
+ val videoDuration = try {
+ val finalSeconds = if (TextUtils.isEmpty(seconds)) "0" else seconds
+ finalSeconds.toFloat()
+ } catch (e: NumberFormatException) {
+ e.printStackTrace()
+ return
+ }
+
+ mainThreadHandler.post {
+ youTubePlayerOwner.listeners.forEach { it.onVideoDuration(youTubePlayerOwner.getInstance(), videoDuration) }
+ }
+ }
+
+ @JavascriptInterface
+ fun sendVideoLoadedFraction(fraction: String) {
+ val loadedFraction = try {
+ fraction.toFloat()
+ } catch (e: NumberFormatException) {
+ e.printStackTrace()
+ return
+ }
+
+ mainThreadHandler.post {
+ youTubePlayerOwner.listeners.forEach { it.onVideoLoadedFraction(youTubePlayerOwner.getInstance(), loadedFraction) }
+ }
+ }
+
+ @JavascriptInterface
+ fun sendVideoId(videoId: String) = mainThreadHandler.post {
+ youTubePlayerOwner.listeners.forEach { it.onVideoId(youTubePlayerOwner.getInstance(), videoId) }
+ }
+
+ private fun parsePlayerState(state: String): PlayerConstants.PlayerState {
+ return when {
+ state.equals(STATE_UNSTARTED, ignoreCase = true) -> PlayerConstants.PlayerState.UNSTARTED
+ state.equals(STATE_ENDED, ignoreCase = true) -> PlayerConstants.PlayerState.ENDED
+ state.equals(STATE_PLAYING, ignoreCase = true) -> PlayerConstants.PlayerState.PLAYING
+ state.equals(STATE_PAUSED, ignoreCase = true) -> PlayerConstants.PlayerState.PAUSED
+ state.equals(STATE_BUFFERING, ignoreCase = true) -> PlayerConstants.PlayerState.BUFFERING
+ state.equals(STATE_CUED, ignoreCase = true) -> PlayerConstants.PlayerState.VIDEO_CUED
+ else -> PlayerConstants.PlayerState.UNKNOWN
+ }
+ }
+
+
+ private fun parsePlaybackQuality(quality: String): PlayerConstants.PlaybackQuality {
+ return when {
+ quality.equals(QUALITY_SMALL, ignoreCase = true) -> PlayerConstants.PlaybackQuality.SMALL
+ quality.equals(QUALITY_MEDIUM, ignoreCase = true) -> PlayerConstants.PlaybackQuality.MEDIUM
+ quality.equals(QUALITY_LARGE, ignoreCase = true) -> PlayerConstants.PlaybackQuality.LARGE
+ quality.equals(QUALITY_HD720, ignoreCase = true) -> PlayerConstants.PlaybackQuality.HD720
+ quality.equals(QUALITY_HD1080, ignoreCase = true) -> PlayerConstants.PlaybackQuality.HD1080
+ quality.equals(
+ QUALITY_HIGH_RES,
+ ignoreCase = true
+ ) -> PlayerConstants.PlaybackQuality.HIGH_RES
+ quality.equals(QUALITY_DEFAULT, ignoreCase = true) -> PlayerConstants.PlaybackQuality.DEFAULT
+ else -> PlayerConstants.PlaybackQuality.UNKNOWN
+ }
+ }
+
+ private fun parsePlaybackRate(rate: String): PlayerConstants.PlaybackRate {
+ return when {
+ rate.equals(RATE_0_25, ignoreCase = true) -> PlayerConstants.PlaybackRate.RATE_0_25
+ rate.equals(RATE_0_5, ignoreCase = true) -> PlayerConstants.PlaybackRate.RATE_0_5
+ rate.equals(RATE_0_75, ignoreCase = true) -> PlayerConstants.PlaybackRate.RATE_0_75
+ rate.equals(RATE_1, ignoreCase = true) -> PlayerConstants.PlaybackRate.RATE_1
+ rate.equals(RATE_1_25, ignoreCase = true) -> PlayerConstants.PlaybackRate.RATE_1_25
+ rate.equals(RATE_1_5, ignoreCase = true) -> PlayerConstants.PlaybackRate.RATE_1_5
+ rate.equals(RATE_1_75, ignoreCase = true) -> PlayerConstants.PlaybackRate.RATE_1_75
+ rate.equals(RATE_2, ignoreCase = true) -> PlayerConstants.PlaybackRate.RATE_2
+ else -> PlayerConstants.PlaybackRate.UNKNOWN
+ }
+ }
+
+ private fun parsePlayerError(error: String): PlayerConstants.PlayerError {
+ return when {
+ error.equals(ERROR_INVALID_PARAMETER_IN_REQUEST, ignoreCase = true) -> PlayerConstants.PlayerError.INVALID_PARAMETER_IN_REQUEST
+ error.equals(ERROR_HTML_5_PLAYER, ignoreCase = true) -> PlayerConstants.PlayerError.HTML_5_PLAYER
+ error.equals(ERROR_VIDEO_NOT_FOUND, ignoreCase = true) -> PlayerConstants.PlayerError.VIDEO_NOT_FOUND
+ error.equals(ERROR_VIDEO_NOT_PLAYABLE_IN_EMBEDDED_PLAYER1, ignoreCase = true) -> PlayerConstants.PlayerError.VIDEO_NOT_PLAYABLE_IN_EMBEDDED_PLAYER
+ error.equals(ERROR_VIDEO_NOT_PLAYABLE_IN_EMBEDDED_PLAYER2, ignoreCase = true) -> PlayerConstants.PlayerError.VIDEO_NOT_PLAYABLE_IN_EMBEDDED_PLAYER
+ error.equals(ERROR_REQUEST_MISSING_HTTP_REFERER, ignoreCase = true) -> PlayerConstants.PlayerError.REQUEST_MISSING_HTTP_REFERER
+ else -> PlayerConstants.PlayerError.UNKNOWN
+ }
+ }
+}
diff --git a/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/YouTubePlayerCallbacks.kt b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/YouTubePlayerCallbacks.kt
new file mode 100644
index 0000000..8842a91
--- /dev/null
+++ b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/YouTubePlayerCallbacks.kt
@@ -0,0 +1,37 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.player
+
+import android.os.Handler
+import android.os.Looper
+import android.webkit.JavascriptInterface
+import androidx.annotation.RestrictTo
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.atomic.AtomicLong
+
+/** Bridge used to extract values from Javascript and pass them to the YouTubePlayer. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+internal class YouTubePlayerCallbacks {
+ private val mainThreadHandler: Handler = Handler(Looper.getMainLooper())
+
+ /** Callbacks registered by clients of this class to retrieve boolean values form Javascript. */
+ private val booleanCallbacks = ConcurrentHashMap()
+
+ private val requestId = AtomicLong(0)
+
+ /**
+ * Registers a callback to be called when a boolean value is received from Javascript.
+ * @return the requestId for this callback.
+ */
+ fun registerBooleanCallback(callback: BooleanProvider): Long {
+ val requestId = requestId.incrementAndGet()
+ booleanCallbacks[requestId] = callback
+ return requestId
+ }
+
+ @JavascriptInterface
+ fun sendBooleanValue(requestId: Long, value: Boolean) {
+ mainThreadHandler.post {
+ val callback = booleanCallbacks.remove(requestId)
+ callback?.accept(value)
+ }
+ }
+}
diff --git a/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/YouTubePlayerExt.kt b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/YouTubePlayerExt.kt
new file mode 100644
index 0000000..251b115
--- /dev/null
+++ b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/YouTubePlayerExt.kt
@@ -0,0 +1,9 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.player
+
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+/** Returns true if the player is muted, false otherwise. */
+suspend fun YouTubePlayer.isMuted(): Boolean = suspendCoroutine { continuation ->
+ isMutedAsync { isMuted -> continuation.resume(isMuted) }
+}
\ No newline at end of file
diff --git a/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/listeners/AbstractYouTubePlayerListener.kt b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/listeners/AbstractYouTubePlayerListener.kt
new file mode 100644
index 0000000..82f257d
--- /dev/null
+++ b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/listeners/AbstractYouTubePlayerListener.kt
@@ -0,0 +1,20 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners
+
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.PlayerConstants
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer
+
+/**
+ * Extend this class if you want to implement only some of the methods of [YouTubePlayerListener]
+ */
+abstract class AbstractYouTubePlayerListener : YouTubePlayerListener {
+ override fun onReady(youTubePlayer: YouTubePlayer) {}
+ override fun onStateChange(youTubePlayer: YouTubePlayer, state: PlayerConstants.PlayerState) {}
+ override fun onPlaybackQualityChange(youTubePlayer: YouTubePlayer, playbackQuality: PlayerConstants.PlaybackQuality) {}
+ override fun onPlaybackRateChange(youTubePlayer: YouTubePlayer, playbackRate: PlayerConstants.PlaybackRate) {}
+ override fun onError(youTubePlayer: YouTubePlayer, error: PlayerConstants.PlayerError) {}
+ override fun onApiChange(youTubePlayer: YouTubePlayer) {}
+ override fun onCurrentSecond(youTubePlayer: YouTubePlayer, second: Float) {}
+ override fun onVideoDuration(youTubePlayer: YouTubePlayer, duration: Float) {}
+ override fun onVideoLoadedFraction(youTubePlayer: YouTubePlayer, loadedFraction: Float) {}
+ override fun onVideoId(youTubePlayer: YouTubePlayer, videoId: String) {}
+}
diff --git a/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/listeners/FullscreenListener.kt b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/listeners/FullscreenListener.kt
new file mode 100644
index 0000000..6eb95dc
--- /dev/null
+++ b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/listeners/FullscreenListener.kt
@@ -0,0 +1,31 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners
+
+import android.view.View
+
+/**
+ * Interface used to keep track of full screen events
+ */
+interface FullscreenListener {
+ /**
+ * Notify the host application that the player has entered full screen mode
+ * (the full screen button in the player UI has been clicked).
+ * After this call, the video will no longer be rendered in the [YouTubePlayerView],
+ * but will instead be rendered in [fullscreenView].
+ * The host application should add this View to a container that fills the screen
+ * in order to actually display the video full screen.
+ *
+ * The application can explicitly exit fullscreen mode by invoking [exitFullscreen]
+ * (for example when the user presses the back button).
+ * However, the player will show its own UI to exist fullscreen.
+ * Regardless of how the player exits fullscreen mode, [onEnterFullscreen] will be invoked,
+ * signaling for the application to remove the custom View.
+ */
+ fun onEnterFullscreen(fullscreenView: View, exitFullscreen: () -> Unit)
+
+ /**
+ * Notify the host application that the player has exited full screen mode.
+ * The host application must hide the custom View (the View which was previously passed to
+ * [onEnterFullscreen]). After this call, the video will render in the player again.
+ */
+ fun onExitFullscreen()
+}
\ No newline at end of file
diff --git a/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/listeners/YouTubePlayerCallback.kt b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/listeners/YouTubePlayerCallback.kt
new file mode 100644
index 0000000..e0066f0
--- /dev/null
+++ b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/listeners/YouTubePlayerCallback.kt
@@ -0,0 +1,7 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners
+
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer
+
+interface YouTubePlayerCallback {
+ fun onYouTubePlayer(youTubePlayer: YouTubePlayer)
+}
\ No newline at end of file
diff --git a/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/listeners/YouTubePlayerListener.kt b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/listeners/YouTubePlayerListener.kt
new file mode 100644
index 0000000..27de9f3
--- /dev/null
+++ b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/listeners/YouTubePlayerListener.kt
@@ -0,0 +1,66 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners
+
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.PlayerConstants
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer
+
+interface YouTubePlayerListener {
+ /**
+ * Called when the player is ready to play videos. You should start using with the player only after this method is called.
+ * @param youTubePlayer The [YouTubePlayer] object.
+ */
+ fun onReady(youTubePlayer: YouTubePlayer)
+
+ /**
+ * Called every time the state of the player changes. Check [PlayerConstants.PlayerState] to see all the possible states.
+ * @param state a state from [PlayerConstants.PlayerState]
+ */
+ fun onStateChange(youTubePlayer: YouTubePlayer, state: PlayerConstants.PlayerState)
+
+ /**
+ * Called every time the quality of the playback changes. Check [PlayerConstants.PlaybackQuality] to see all the possible values.
+ * @param playbackQuality a state from [PlayerConstants.PlaybackQuality]
+ */
+ fun onPlaybackQualityChange(
+ youTubePlayer: YouTubePlayer,
+ playbackQuality: PlayerConstants.PlaybackQuality
+ )
+
+ /**
+ * Called every time the speed of the playback changes. Check [PlayerConstants.PlaybackRate] to see all the possible values.
+ * @param playbackRate a state from [PlayerConstants.PlaybackRate]
+ */
+ fun onPlaybackRateChange(youTubePlayer: YouTubePlayer, playbackRate: PlayerConstants.PlaybackRate)
+
+ /**
+ * Called when an error occurs in the player. Check [PlayerConstants.PlayerError] to see all the possible values.
+ * @param error a state from [PlayerConstants.PlayerError]
+ */
+ fun onError(youTubePlayer: YouTubePlayer, error: PlayerConstants.PlayerError)
+
+ /**
+ * Called periodically by the player, the argument is the number of seconds that have been played.
+ * @param second current second of the playback
+ */
+ fun onCurrentSecond(youTubePlayer: YouTubePlayer, second: Float)
+
+ /**
+ * Called when the total duration of the video is loaded.
+ * Note that getDuration() will return 0 until the video's metadata is loaded, which normally happens just after the video starts playing.
+ * @param duration total duration of the video
+ */
+ fun onVideoDuration(youTubePlayer: YouTubePlayer, duration: Float)
+
+ /**
+ * Called periodically by the player, the argument is the percentage of the video that has been buffered.
+ * @param loadedFraction a number between 0 and 1 that represents the percentage of the video that has been buffered.
+ */
+ fun onVideoLoadedFraction(youTubePlayer: YouTubePlayer, loadedFraction: Float)
+
+ /**
+ * Called when the id of the current video is loaded
+ * @param videoId the id of the video being played
+ */
+ fun onVideoId(youTubePlayer: YouTubePlayer, videoId: String)
+
+ fun onApiChange(youTubePlayer: YouTubePlayer)
+}
diff --git a/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/options/IFramePlayerOptions.kt b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/options/IFramePlayerOptions.kt
new file mode 100644
index 0000000..fa0ca5b
--- /dev/null
+++ b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/options/IFramePlayerOptions.kt
@@ -0,0 +1,222 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.player.options
+
+import android.content.Context
+import org.json.JSONException
+import org.json.JSONObject
+
+/**
+ * Options used to configure the IFrame Player. All the options are listed here:
+ * [IFrame player parameters](https://developers.google.com/youtube/player_parameters#Parameters)
+ */
+class IFramePlayerOptions private constructor(private val playerOptions: JSONObject) {
+
+ companion object {
+ fun getDefault(context: Context) = Builder(context).controls(1).build()
+ }
+
+ override fun toString(): String {
+ return playerOptions.toString()
+ }
+
+ internal fun getOrigin(): String {
+ return playerOptions.getString(Builder.ORIGIN)
+ }
+
+ class Builder(context: Context) {
+ companion object {
+ private const val AUTO_PLAY = "autoplay"
+ private const val MUTE = "mute"
+ private const val CONTROLS = "controls"
+ private const val ENABLE_JS_API = "enablejsapi"
+ private const val FS = "fs"
+ internal const val ORIGIN = "origin"
+ private const val REL = "rel"
+ private const val IV_LOAD_POLICY = "iv_load_policy"
+ private const val CC_LOAD_POLICY = "cc_load_policy"
+ private const val CC_LANG_PREF = "cc_lang_pref"
+ private const val LIST = "list"
+ private const val LIST_TYPE = "listType"
+ private const val START = "start"
+ private const val END = "end"
+ }
+
+ private val builderOptions = JSONObject()
+
+ init {
+ addInt(AUTO_PLAY, 0)
+ addInt(MUTE, 0)
+ addInt(CONTROLS, 0)
+ addInt(ENABLE_JS_API, 1)
+ addInt(FS, 0)
+ addString(ORIGIN, "https://${context.packageName}")
+ addInt(REL, 0)
+ addInt(IV_LOAD_POLICY, 3)
+ addInt(CC_LOAD_POLICY, 0)
+ }
+
+ fun build(): IFramePlayerOptions {
+ return IFramePlayerOptions(builderOptions)
+ }
+
+ /**
+ * Controls whether the web-based UI of the IFrame player is used or not.
+ * @param controls If set to 0: web UI is not used. If set to 1: web UI is used.
+ */
+ fun controls(controls: Int): Builder {
+ addInt(CONTROLS, controls)
+ return this
+ }
+
+ /**
+ * Controls if the video is played automatically after the player is initialized.
+ * @param autoplay if set to 1: the player will start automatically. If set to 0: the player will not start automatically
+ */
+ fun autoplay(controls: Int): Builder {
+ addInt(AUTO_PLAY, controls)
+ return this
+ }
+
+ /**
+ * Controls if the player will be initialized mute or not.
+ * @param mute if set to 1: the player will start muted and without acquiring Audio Focus. If set to 0: the player will acquire Audio Focus
+ */
+ fun mute(controls: Int): Builder {
+ addInt(MUTE, controls)
+ return this
+ }
+
+ /**
+ * Controls the related videos shown at the end of a video.
+ * @param rel If set to 0: related videos will come from the same channel as the video that was just played. If set to 1: related videos will come from multiple channels.
+ */
+ fun rel(rel: Int): Builder {
+ addInt(REL, rel)
+ return this
+ }
+
+ /**
+ * Controls video annotations.
+ * @param ivLoadPolicy if set to 1: the player will show video annotations. If set to 3: they player won't show video annotations.
+ */
+ fun ivLoadPolicy(ivLoadPolicy: Int): Builder {
+ addInt(IV_LOAD_POLICY, ivLoadPolicy)
+ return this
+ }
+
+ /**
+ * This parameter specifies the default language that the player will use to display captions.
+ * If you use this parameter and also set the cc_load_policy parameter to 1, then the player
+ * will show captions in the specified language when the player loads.
+ * If you do not also set the cc_load_policy parameter, then captions will not display by default,
+ * but will display in the specified language if the user opts to turn captions on.
+ *
+ * @param languageCode ISO 639-1 two-letter language code
+ */
+ fun langPref(languageCode: String): Builder {
+ addString(CC_LANG_PREF, languageCode)
+ return this
+ }
+
+
+ /**
+ * Controls video captions. It doesn't work with automatically generated captions.
+ * @param ccLoadPolicy if set to 1: the player will show captions. If set to 0: the player won't show captions.
+ */
+ fun ccLoadPolicy(ccLoadPolicy: Int): Builder {
+ addInt(CC_LOAD_POLICY, ccLoadPolicy)
+ return this
+ }
+
+ /**
+ * This parameter specifies the domain from which the player is running.
+ * Since the player in this library is not running from a website there should be no reason to change this.
+ * Using "https://www.youtube.com" (the default value) is recommended as some functions from the IFrame Player are only available
+ * when the player is running on a trusted domain.
+ * @param origin your domain.
+ */
+ fun origin(origin: String): Builder {
+ addString(ORIGIN, origin)
+ return this
+ }
+
+ /**
+ * The list parameter, in conjunction with the [listType] parameter, identifies the content that will load in the player.
+ * If the [listType] parameter value is "playlist", then the [list] parameter value specifies a YouTube playlist ID.
+ * @param list The playlist ID to be played.
+ * You need to prepend the playlist ID with the letters PL, for example:
+ * if playlist id is 1234, you should pass PL1234.
+ */
+ fun list(list: String): Builder {
+ addString(LIST, list)
+ return this
+ }
+
+ /**
+ * Controls if the player is playing from video IDs or from playlist IDs.
+ * If set to "playlist", you should use the "list" parameter to set the playlist ID
+ * to be played.
+ * See original documentation for more info: https://developers.google.com/youtube/player_parameters#Selecting_Content_to_Play
+ * @param listType Set to "playlist" to play playlists. Then pass the playlist id to the [list] parameter.
+ */
+ fun listType(listType: String): Builder {
+ addString(LIST_TYPE, listType)
+ return this
+ }
+
+ /**
+ * Setting this parameter to 0 prevents the fullscreen button from displaying in the player.
+ * See original documentation for more info: https://developers.google.com/youtube/player_parameters#Parameters
+ * @param fs if set to 1: the player fullscreen button will be show. If set to 0: the player fullscreen button will not be shown.
+ */
+ fun fullscreen(fs: Int): Builder {
+ addInt(FS, fs)
+ return this
+ }
+
+ /**
+ * This parameter causes the player to begin playing the video at the given number of seconds from the start of the video.
+ * The parameter value is a positive integer.
+ * @param startSeconds positive integer, number of seconds to offset playback from the start of the video.
+ */
+ fun start(startSeconds: Int): Builder {
+ addInt(START, startSeconds)
+ return this
+ }
+
+ /**
+ * This parameter specifies the time, measured in seconds from the beginning of the video, when the player should stop playing the video.
+ * The parameter value is a positive integer.
+ * @param endSeconds positive integer specifying the time, measured in seconds from the beginning of the video, when the player should stop playing the video.
+ */
+ fun end(endSeconds: Int): Builder {
+ addInt(END, endSeconds)
+ return this
+ }
+
+ /**
+ * The modestbranding parameter is deprecated and will have no effect.
+ * To align with YouTube's branding requirements, the player now determines the appropriate branding treatment based on a combination of factors, including player size, other API parameters (e.g. controls), and additional signals.
+ * See August 15, 2023 deprecation announcement: https://developers.google.com/youtube/player_parameters#release_notes_08_15_2023
+ */
+ @Deprecated("Deprecated and will have no effect")
+ fun modestBranding(modestBranding: Int): Builder {
+ return this
+ }
+
+ private fun addString(key: String, value: String) {
+ try {
+ builderOptions.put(key, value)
+ } catch (e: JSONException) {
+ throw RuntimeException("Illegal JSON value $key: $value")
+ }
+ }
+
+ private fun addInt(key: String, value: Int) {
+ try {
+ builderOptions.put(key, value)
+ } catch (e: JSONException) {
+ throw RuntimeException("Illegal JSON value $key: $value")
+ }
+ }
+ }
+}
diff --git a/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/utils/NetworkObserver.kt b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/utils/NetworkObserver.kt
new file mode 100644
index 0000000..e73bc71
--- /dev/null
+++ b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/utils/NetworkObserver.kt
@@ -0,0 +1,121 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.player.utils
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import androidx.annotation.RequiresApi
+
+/** Class used to observe changes to network state */
+internal class NetworkObserver(private val context: Context) {
+
+ interface Listener {
+ fun onNetworkAvailable()
+ fun onNetworkUnavailable()
+ }
+
+ val listeners = mutableListOf()
+
+ private var networkBroadcastReceiver: NetworkBroadcastReceiver? = null
+ private var networkCallback: ConnectivityManager.NetworkCallback? = null
+
+ /** Start observing network changes */
+ fun observeNetwork() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ doObserveNetwork(context)
+ } else {
+ doObserveNetworkLegacy(context)
+ }
+ }
+
+ /** Stop observing network changes and cleanup */
+ fun destroy() {
+ // Min API for `unregisterNetworkCallback` is L, but we use `registerDefaultNetworkCallback` only for N and above.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ val callback = networkCallback ?: return
+ val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ connectivityManager.unregisterNetworkCallback(callback)
+ }
+ else {
+ val receiver = networkBroadcastReceiver ?: return
+ runCatching { context.unregisterReceiver(receiver) }
+ }
+
+ listeners.clear()
+ networkCallback = null
+ networkBroadcastReceiver = null
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ private fun doObserveNetwork(context: Context) {
+ val callback = object : ConnectivityManager.NetworkCallback() {
+ private val mainThreadHandler = Handler(Looper.getMainLooper())
+ override fun onAvailable(network: Network) {
+ // the callback is not on the main thread
+ mainThreadHandler.post {
+ listeners.forEach { it.onNetworkAvailable() }
+ }
+ }
+
+ override fun onLost(network: Network) {
+ // the callback is not on the main thread
+ mainThreadHandler.post {
+ listeners.forEach { it.onNetworkUnavailable() }
+ }
+ }
+ }
+ networkCallback = callback
+
+ val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ connectivityManager.registerDefaultNetworkCallback(callback)
+ }
+
+ private fun doObserveNetworkLegacy(context: Context) {
+ networkBroadcastReceiver = NetworkBroadcastReceiver(
+ onNetworkAvailable = { listeners.forEach { it.onNetworkAvailable() } },
+ onNetworkUnavailable = { listeners.forEach { it.onNetworkUnavailable() } },
+ )
+ context.registerReceiver(networkBroadcastReceiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))
+ }
+}
+
+/** Broadcast receiver used to react to changes in internet connectivity */
+private class NetworkBroadcastReceiver(
+ private val onNetworkAvailable: () -> Unit,
+ private val onNetworkUnavailable: () -> Unit
+) : BroadcastReceiver() {
+
+ override fun onReceive(context: Context, intent: Intent) {
+ if (isConnectedToInternet(context)) {
+ onNetworkAvailable()
+ }
+ else {
+ onNetworkUnavailable()
+ }
+ }
+}
+
+private fun isConnectedToInternet(context: Context): Boolean {
+ val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) ?: return false
+ networkCapabilities.isConnectedToInternet()
+ } else {
+ val networkInfo = connectivityManager.activeNetworkInfo
+ networkInfo != null && networkInfo.isConnected
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+private fun NetworkCapabilities.isConnectedToInternet(): Boolean {
+ return (hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
+ hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
+ hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET))
+}
diff --git a/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/utils/PlaybackResumer.kt b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/utils/PlaybackResumer.kt
new file mode 100644
index 0000000..e7c5ed3
--- /dev/null
+++ b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/utils/PlaybackResumer.kt
@@ -0,0 +1,61 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.player.utils
+
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.PlayerConstants
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.AbstractYouTubePlayerListener
+
+/**
+ * Class responsible for resuming the playback state in case of network problems.
+ * eg: player is playing -> network goes out -> player stops -> network comes back -> player resumes playback automatically.
+ */
+internal class PlaybackResumer : AbstractYouTubePlayerListener() {
+
+ private var canLoad = false
+ private var isPlaying = false
+ private var error: PlayerConstants.PlayerError? = null
+
+ private var currentVideoId: String? = null
+ private var currentSecond: Float = 0f
+
+ fun resume(youTubePlayer: YouTubePlayer) {
+ val videoId = currentVideoId ?: return
+ if (isPlaying && error == PlayerConstants.PlayerError.HTML_5_PLAYER) {
+ youTubePlayer.loadOrCueVideo(canLoad, videoId, currentSecond)
+ }
+ else if (!isPlaying && error == PlayerConstants.PlayerError.HTML_5_PLAYER) {
+ youTubePlayer.cueVideo(videoId, currentSecond)
+ }
+
+ error = null
+ }
+
+ override fun onStateChange(youTubePlayer: YouTubePlayer, state: PlayerConstants.PlayerState) {
+ when (state) {
+ PlayerConstants.PlayerState.ENDED, PlayerConstants.PlayerState.PAUSED -> isPlaying = false
+ PlayerConstants.PlayerState.PLAYING -> isPlaying = true
+ else -> { }
+ }
+ }
+
+ override fun onError(youTubePlayer: YouTubePlayer, error: PlayerConstants.PlayerError) {
+ if (error == PlayerConstants.PlayerError.HTML_5_PLAYER) {
+ this.error = error
+ }
+ }
+
+ override fun onCurrentSecond(youTubePlayer: YouTubePlayer, second: Float) {
+ currentSecond = second
+ }
+
+ override fun onVideoId(youTubePlayer: YouTubePlayer, videoId: String) {
+ currentVideoId = videoId
+ }
+
+ fun onLifecycleResume() {
+ canLoad = true
+ }
+
+ fun onLifecycleStop() {
+ canLoad = false
+ }
+}
diff --git a/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/utils/YouTubePlayerTracker.kt b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/utils/YouTubePlayerTracker.kt
new file mode 100644
index 0000000..d195269
--- /dev/null
+++ b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/utils/YouTubePlayerTracker.kt
@@ -0,0 +1,39 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.player.utils
+
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.PlayerConstants
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.AbstractYouTubePlayerListener
+
+/**
+ * Utility class responsible for tracking the state of YouTubePlayer.
+ * This is a YouTubePlayerListener, therefore to work it has to be added as listener to a YouTubePlayer.
+ */
+class YouTubePlayerTracker : AbstractYouTubePlayerListener() {
+ /**
+ * @return the player state. A value from [PlayerConstants.PlayerState]
+ */
+ var state: PlayerConstants.PlayerState = PlayerConstants.PlayerState.UNKNOWN
+ private set
+ var currentSecond: Float = 0f
+ private set
+ var videoDuration: Float = 0f
+ private set
+ var videoId: String? = null
+ private set
+
+ override fun onStateChange(youTubePlayer: YouTubePlayer, state: PlayerConstants.PlayerState) {
+ this.state = state
+ }
+
+ override fun onCurrentSecond(youTubePlayer: YouTubePlayer, second: Float) {
+ currentSecond = second
+ }
+
+ override fun onVideoDuration(youTubePlayer: YouTubePlayer, duration: Float) {
+ videoDuration = duration
+ }
+
+ override fun onVideoId(youTubePlayer: YouTubePlayer, videoId: String) {
+ this.videoId = videoId
+ }
+}
diff --git a/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/utils/YouTubePlayerUtils.kt b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/utils/YouTubePlayerUtils.kt
new file mode 100644
index 0000000..12d06bf
--- /dev/null
+++ b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/utils/YouTubePlayerUtils.kt
@@ -0,0 +1,29 @@
+@file:JvmName("YouTubePlayerUtils")
+
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.player.utils
+
+import androidx.lifecycle.Lifecycle
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer
+
+/**
+ * Calls [YouTubePlayer.cueVideo] or [YouTubePlayer.loadVideo] depending on which one is more appropriate.
+ * If it can't decide, calls [YouTubePlayer.cueVideo] by default.
+ *
+ * In most cases you want to avoid calling [YouTubePlayer.loadVideo] if the Activity/Fragment is not in the foreground.
+ * This function automates these checks for you.
+ * @param lifecycle the lifecycle of the Activity or Fragment containing the YouTubePlayerView.
+ * @param videoId id of the video.
+ * @param startSeconds the time from which the video should start playing.
+ */
+fun YouTubePlayer.loadOrCueVideo(lifecycle: Lifecycle, videoId: String, startSeconds: Float) {
+ loadOrCueVideo(lifecycle.currentState == Lifecycle.State.RESUMED, videoId, startSeconds)
+}
+
+
+@JvmSynthetic
+internal fun YouTubePlayer.loadOrCueVideo(canLoad: Boolean, videoId: String, startSeconds: Float) {
+ if (canLoad)
+ loadVideo(videoId, startSeconds)
+ else
+ cueVideo(videoId, startSeconds)
+}
\ No newline at end of file
diff --git a/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/views/LegacyYouTubePlayerView.kt b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/views/LegacyYouTubePlayerView.kt
new file mode 100644
index 0000000..e220653
--- /dev/null
+++ b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/views/LegacyYouTubePlayerView.kt
@@ -0,0 +1,210 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.player.views
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.LayoutRes
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.PlayerConstants
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.AbstractYouTubePlayerListener
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.FullscreenListener
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.YouTubePlayerCallback
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.YouTubePlayerListener
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.options.IFramePlayerOptions
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.utils.NetworkObserver
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.utils.PlaybackResumer
+
+/**
+ * Legacy internal implementation of YouTubePlayerView. The user facing YouTubePlayerView delegates
+ * most of its actions to this one.
+ */
+internal class LegacyYouTubePlayerView(
+ context: Context,
+ listener: FullscreenListener,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : SixteenByNineFrameLayout(context, attrs, defStyleAttr) {
+
+ constructor(context: Context) : this(context, FakeWebViewYouTubeListener, null, 0)
+
+ internal val webViewYouTubePlayer = WebViewYouTubePlayer(context, listener)
+
+ private val networkObserver = NetworkObserver(context.applicationContext)
+ private val playbackResumer = PlaybackResumer()
+
+ internal var isYouTubePlayerReady = false
+ private var initialize = { }
+ private val youTubePlayerCallbacks = mutableSetOf()
+
+ internal var canPlay = true
+ private set
+
+ init {
+ addView(
+ webViewYouTubePlayer,
+ LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
+ )
+ webViewYouTubePlayer.addListener(playbackResumer)
+
+ // stop playing if the user loads a video but then leaves the app before the video starts playing.
+ webViewYouTubePlayer.addListener(object : AbstractYouTubePlayerListener() {
+ override fun onStateChange(youTubePlayer: YouTubePlayer, state: PlayerConstants.PlayerState) {
+ if (state == PlayerConstants.PlayerState.PLAYING && !isEligibleForPlayback()) {
+ youTubePlayer.pause()
+ }
+ }
+ })
+
+ webViewYouTubePlayer.addListener(object : AbstractYouTubePlayerListener() {
+ override fun onReady(youTubePlayer: YouTubePlayer) {
+ isYouTubePlayerReady = true
+
+ youTubePlayerCallbacks.forEach { it.onYouTubePlayer(youTubePlayer) }
+ youTubePlayerCallbacks.clear()
+
+ youTubePlayer.removeListener(this)
+ }
+ })
+
+ networkObserver.listeners.add(object : NetworkObserver.Listener {
+ override fun onNetworkAvailable() {
+ if (!isYouTubePlayerReady) {
+ initialize()
+ }
+ else {
+ playbackResumer.resume(webViewYouTubePlayer.youtubePlayer)
+ }
+ }
+
+ override fun onNetworkUnavailable() { }
+ })
+ }
+
+ /**
+ * Initialize the player. You must call this method before using the player.
+ * @param youTubePlayerListener listener for player events
+ * @param handleNetworkEvents if set to true a broadcast receiver will be registered and network events will be handled automatically.
+ * If set to false, you should handle network events with your own broadcast receiver.
+ * @param playerOptions customizable options for the embedded video player, can be null.
+ * @param videoId optional, used to load a video right after initialization.
+ */
+ fun initialize(
+ youTubePlayerListener: YouTubePlayerListener,
+ handleNetworkEvents: Boolean,
+ playerOptions: IFramePlayerOptions,
+ videoId: String?
+ ) {
+ if (isYouTubePlayerReady) {
+ throw IllegalStateException("This YouTubePlayerView has already been initialized.")
+ }
+
+ if (handleNetworkEvents) {
+ networkObserver.observeNetwork()
+ }
+
+ initialize = {
+ webViewYouTubePlayer.initialize({ it.addListener(youTubePlayerListener) }, playerOptions, videoId)
+ }
+
+ if (!handleNetworkEvents) {
+ initialize()
+ }
+ }
+
+ /**
+ * Initialize the player.
+ * @param playerOptions customizable options for the embedded video player.
+ *
+ * @see LegacyYouTubePlayerView.initialize
+ */
+ fun initialize(youTubePlayerListener: YouTubePlayerListener, handleNetworkEvents: Boolean, playerOptions: IFramePlayerOptions) =
+ initialize(youTubePlayerListener, handleNetworkEvents, playerOptions, null)
+
+ /**
+ * Initialize the player.
+ * @param handleNetworkEvents if set to true a broadcast receiver will be registered and network events will be handled automatically.
+ * If set to false, you should handle network events with your own broadcast receiver.
+ *
+ * @see LegacyYouTubePlayerView.initialize
+ */
+ fun initialize(youTubePlayerListener: YouTubePlayerListener, handleNetworkEvents: Boolean) =
+ initialize(youTubePlayerListener, handleNetworkEvents, IFramePlayerOptions.getDefault(context))
+
+ /**
+ * Initialize the player. Network events are automatically handled by the player.
+ * @param youTubePlayerListener listener for player events
+ *
+ * @see LegacyYouTubePlayerView.initialize
+ */
+ fun initialize(youTubePlayerListener: YouTubePlayerListener) = initialize(youTubePlayerListener, true)
+
+ /**
+ * @param youTubePlayerCallback A callback that will be called when the YouTubePlayer is ready.
+ * If the player is ready when the function is called, the callback is called immediately.
+ * This function is called only once.
+ */
+ fun getYouTubePlayerWhenReady(youTubePlayerCallback: YouTubePlayerCallback) {
+ if (isYouTubePlayerReady) {
+ youTubePlayerCallback.onYouTubePlayer(webViewYouTubePlayer.youtubePlayer)
+ }
+ else {
+ youTubePlayerCallbacks.add(youTubePlayerCallback)
+ }
+ }
+
+ /**
+ * Use this method to replace the default Ui of the player with a custom Ui.
+ *
+ * You will be responsible to manage the custom Ui from your application,
+ * the default controller obtained through [LegacyYouTubePlayerView.getPlayerUiController] won't be available anymore.
+ * @param layoutId the ID of the layout defining the custom Ui.
+ * @return The inflated View
+ */
+ fun inflateCustomPlayerUi(@LayoutRes layoutId: Int): View {
+ removeViews(1, childCount - 1)
+ return View.inflate(context, layoutId, this)
+ }
+
+ fun setCustomPlayerUi(view: View) {
+ removeViews(1, childCount - 1)
+ addView(view)
+ }
+
+ /**
+ * Call this method before destroying the host Fragment/Activity, or register this View as an observer of its host lifecycle
+ */
+ fun release() {
+ networkObserver.destroy()
+ removeView(webViewYouTubePlayer)
+ webViewYouTubePlayer.removeAllViews()
+ webViewYouTubePlayer.destroy()
+ }
+
+ internal fun onResume() {
+ playbackResumer.onLifecycleResume()
+ canPlay = true
+ }
+
+ internal fun onStop() {
+ webViewYouTubePlayer.youtubePlayer.pause()
+ playbackResumer.onLifecycleStop()
+ canPlay = false
+ }
+
+ /**
+ * Checks whether the player is in an eligible state for playback in
+ * respect of the {@link WebViewYouTubePlayer#isBackgroundPlaybackEnabled}
+ * property.
+ */
+ internal fun isEligibleForPlayback(): Boolean {
+ return canPlay || webViewYouTubePlayer.isBackgroundPlaybackEnabled
+ }
+
+ /**
+ * Don't use this method if you want to publish your app on the PlayStore. Background playback is against YouTube terms of service.
+ */
+ fun enableBackgroundPlayback(enable: Boolean) {
+ webViewYouTubePlayer.isBackgroundPlaybackEnabled = enable
+ }
+}
diff --git a/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/views/SixteenByNineFrameLayout.kt b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/views/SixteenByNineFrameLayout.kt
new file mode 100644
index 0000000..56b1bb8
--- /dev/null
+++ b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/views/SixteenByNineFrameLayout.kt
@@ -0,0 +1,29 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.player.views
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.annotation.RestrictTo
+
+/**
+ * A FrameLayout with an aspect ration of 16:9, when the height is set to wrap_content.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+open class SixteenByNineFrameLayout : FrameLayout {
+ constructor(context: Context) : this(context, null)
+ constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
+ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : super(context, attrs, defStyleAttr)
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ if (layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ val sixteenNineHeight = MeasureSpec.makeMeasureSpec(
+ MeasureSpec.getSize(widthMeasureSpec) * 9 / 16,
+ MeasureSpec.EXACTLY
+ )
+ super.onMeasure(widthMeasureSpec, sixteenNineHeight)
+ } else
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ }
+}
\ No newline at end of file
diff --git a/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/views/WebViewYouTubePlayer.kt b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/views/WebViewYouTubePlayer.kt
new file mode 100644
index 0000000..559c178
--- /dev/null
+++ b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/views/WebViewYouTubePlayer.kt
@@ -0,0 +1,185 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.player.views
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Bitmap
+import android.os.Handler
+import android.os.Looper
+import android.util.AttributeSet
+import android.view.View
+import android.webkit.WebChromeClient
+import android.webkit.WebSettings
+import android.webkit.WebView
+import androidx.annotation.GuardedBy
+import androidx.annotation.VisibleForTesting
+import com.pierfrancescosoffritti.androidyoutubeplayer.R
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.BooleanProvider
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.PlayerConstants
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayerBridge
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayerCallbacks
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.FullscreenListener
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.YouTubePlayerListener
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.options.IFramePlayerOptions
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.toFloat
+import java.io.BufferedReader
+import java.io.InputStream
+import java.io.InputStreamReader
+
+
+private class YouTubePlayerImpl(
+ private val webView: WebView,
+ private val callbacks: YouTubePlayerCallbacks
+) : YouTubePlayer {
+ private val mainThread: Handler = Handler(Looper.getMainLooper())
+
+ private val lock = Any()
+ @GuardedBy("lock")
+ private val listeners = mutableSetOf()
+
+ override fun loadVideo(videoId: String, startSeconds: Float) = webView.invoke("loadVideo", videoId, startSeconds)
+ override fun cueVideo(videoId: String, startSeconds: Float) = webView.invoke("cueVideo", videoId, startSeconds)
+ override fun play() = webView.invoke("playVideo")
+ override fun pause() = webView.invoke("pauseVideo")
+ override fun nextVideo() = webView.invoke("nextVideo")
+ override fun previousVideo() = webView.invoke("previousVideo")
+ override fun playVideoAt(index: Int) = webView.invoke("playVideoAt", index)
+ override fun setLoop(loop: Boolean) = webView.invoke("setLoop", loop)
+ override fun setShuffle(shuffle: Boolean) = webView.invoke("setShuffle", shuffle)
+ override fun mute() = webView.invoke("mute")
+ override fun unMute() = webView.invoke("unMute")
+ override fun isMutedAsync(callback: BooleanProvider) {
+ val requestId = callbacks.registerBooleanCallback(callback)
+ webView.invoke("getMuteValue", requestId)
+ }
+ override fun setVolume(volumePercent: Int) {
+ require(volumePercent in 0..100) { "Volume must be between 0 and 100" }
+ webView.invoke("setVolume", volumePercent)
+ }
+ override fun seekTo(time: Float) = webView.invoke("seekTo", time)
+ override fun setPlaybackRate(playbackRate: PlayerConstants.PlaybackRate) = webView.invoke("setPlaybackRate", playbackRate.toFloat())
+ override fun addListener(listener: YouTubePlayerListener) = synchronized(lock) { listeners.add(listener) }
+ override fun removeListener(listener: YouTubePlayerListener) = synchronized(lock) { listeners.remove(listener) }
+
+ fun getListeners(): Collection = synchronized(lock) { listeners.toList() }
+
+ fun release() {
+ synchronized(lock) { listeners.clear() }
+ mainThread.removeCallbacksAndMessages(null)
+ }
+
+ private fun WebView.invoke(function: String, vararg args: Any) {
+ val stringArgs = args.map {
+ if (it is String) {
+ "'$it'"
+ }
+ else {
+ it.toString()
+ }
+ }
+ mainThread.post { loadUrl("javascript:$function(${stringArgs.joinToString(",")})") }
+ }
+}
+
+internal object FakeWebViewYouTubeListener : FullscreenListener {
+ override fun onEnterFullscreen(fullscreenView: View, exitFullscreen: () -> Unit) {}
+ override fun onExitFullscreen() {}
+}
+
+/**
+ * WebView implementation of [YouTubePlayer]. The player runs inside the WebView, using the IFrame Player API.
+ */
+internal class WebViewYouTubePlayer constructor(
+ context: Context,
+ private val listener: FullscreenListener,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : WebView(context, attrs, defStyleAttr), YouTubePlayerBridge.YouTubePlayerBridgeCallbacks {
+
+ /** Constructor used by tools */
+ constructor(context: Context) : this(context, FakeWebViewYouTubeListener)
+
+ private val youTubePlayerCallbacks = YouTubePlayerCallbacks()
+ private val _youTubePlayer = YouTubePlayerImpl(this, youTubePlayerCallbacks)
+ internal val youtubePlayer: YouTubePlayer get() = _youTubePlayer
+
+ private lateinit var youTubePlayerInitListener: (YouTubePlayer) -> Unit
+
+ internal var isBackgroundPlaybackEnabled = false
+
+ private val youTubePlayerBridge = YouTubePlayerBridge(this)
+
+ internal fun initialize(initListener: (YouTubePlayer) -> Unit, playerOptions: IFramePlayerOptions?, videoId: String?) {
+ youTubePlayerInitListener = initListener
+ initWebView(playerOptions ?: IFramePlayerOptions.getDefault(context), videoId)
+ }
+
+ override val listeners: Collection get() = _youTubePlayer.getListeners()
+ override fun getInstance(): YouTubePlayer = _youTubePlayer
+ override fun onYouTubeIFrameAPIReady() = youTubePlayerInitListener(_youTubePlayer)
+ fun addListener(listener: YouTubePlayerListener) = _youTubePlayer.addListener(listener)
+ fun removeListener(listener: YouTubePlayerListener) = _youTubePlayer.removeListener(listener)
+
+ override fun destroy() {
+ _youTubePlayer.release()
+ super.destroy()
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ private fun initWebView(playerOptions: IFramePlayerOptions, videoId: String?) {
+ settings.apply {
+ javaScriptEnabled = true
+ mediaPlaybackRequiresUserGesture = false
+ cacheMode = WebSettings.LOAD_DEFAULT
+ }
+
+ addJavascriptInterface(youTubePlayerBridge, "YouTubePlayerBridge")
+ addJavascriptInterface(youTubePlayerCallbacks, "YouTubePlayerCallbacks")
+
+ val htmlPage = readHTMLFromUTF8File(resources.openRawResource(R.raw.ayp_youtube_player))
+ .replace("<>", if (videoId != null) { "'$videoId'" } else { "undefined" })
+ .replace("<>", playerOptions.toString())
+
+ loadDataWithBaseURL(playerOptions.getOrigin(), htmlPage, "text/html", "utf-8", null)
+
+ webChromeClient = object : WebChromeClient() {
+
+ override fun onShowCustomView(view: View, callback: CustomViewCallback) {
+ super.onShowCustomView(view, callback)
+ listener.onEnterFullscreen(view) { callback.onCustomViewHidden() }
+ }
+
+ override fun onHideCustomView() {
+ super.onHideCustomView()
+ listener.onExitFullscreen()
+ }
+
+ override fun getDefaultVideoPoster(): Bitmap? {
+ val result = super.getDefaultVideoPoster()
+ // if the video's thumbnail is not in memory, show a black screen
+ return result ?: Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565)
+ }
+ }
+ }
+
+ override fun onWindowVisibilityChanged(visibility: Int) {
+ if (isBackgroundPlaybackEnabled && (visibility == View.GONE || visibility == View.INVISIBLE)) {
+ return
+ }
+
+ super.onWindowVisibilityChanged(visibility)
+ }
+}
+
+@VisibleForTesting
+internal fun readHTMLFromUTF8File(inputStream: InputStream): String {
+ inputStream.use { stream ->
+ BufferedReader(InputStreamReader(stream, "utf-8")).use { bufferedReader ->
+ try {
+ return bufferedReader.readLines().joinToString("\n")
+ } catch (_: Exception) {
+ throw RuntimeException("Can't parse HTML file.")
+ }
+ }
+ }
+}
diff --git a/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/views/YouTubePlayerView.kt b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/views/YouTubePlayerView.kt
new file mode 100644
index 0000000..2a002a9
--- /dev/null
+++ b/youtube/core/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/player/views/YouTubePlayerView.kt
@@ -0,0 +1,250 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.player.views
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams
+import android.widget.FrameLayout
+import androidx.annotation.LayoutRes
+import androidx.lifecycle.*
+import com.pierfrancescosoffritti.androidyoutubeplayer.R
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.*
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.options.IFramePlayerOptions
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.utils.loadOrCueVideo
+
+private const val AUTO_INIT_ERROR = "YouTubePlayerView: If you want to initialize this view manually, " +
+ "you need to set 'enableAutomaticInitialization' to false."
+
+private val matchParent
+ get() = FrameLayout.LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT
+ )
+
+class YouTubePlayerView(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : SixteenByNineFrameLayout(context, attrs, defStyleAttr), LifecycleEventObserver {
+
+ constructor(context: Context) : this(context, null, 0)
+ constructor(context: Context, attrs: AttributeSet? = null) : this(context, attrs, 0)
+
+ private val fullscreenListeners = mutableListOf()
+
+ /**
+ * A single [FullscreenListener] that is always added to the WebView,
+ * responsible for calling all optional listeners added from clients of the library.
+ */
+ private val webViewFullscreenListener = object : FullscreenListener {
+ override fun onEnterFullscreen(fullscreenView: View, exitFullscreen: () -> Unit) {
+ if (fullscreenListeners.isEmpty()) {
+ throw IllegalStateException("To enter fullscreen you need to first register a FullscreenListener.")
+ }
+ fullscreenListeners.forEach { it.onEnterFullscreen(fullscreenView, exitFullscreen) }
+ }
+
+ override fun onExitFullscreen() {
+ if (fullscreenListeners.isEmpty()) {
+ throw IllegalStateException("To enter fullscreen you need to first register a FullscreenListener.")
+ }
+ fullscreenListeners.forEach { it.onExitFullscreen() }
+ }
+ }
+
+ private val legacyTubePlayerView = LegacyYouTubePlayerView(context, webViewFullscreenListener)
+
+ // this is a publicly accessible API
+ var enableAutomaticInitialization: Boolean
+
+ init {
+ addView(legacyTubePlayerView, matchParent)
+
+ val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.YouTubePlayerView, 0, 0)
+
+ enableAutomaticInitialization = typedArray.getBoolean(R.styleable.YouTubePlayerView_enableAutomaticInitialization, true)
+ val autoPlay = typedArray.getBoolean(R.styleable.YouTubePlayerView_autoPlay, false)
+ val handleNetworkEvents = typedArray.getBoolean(R.styleable.YouTubePlayerView_handleNetworkEvents, true)
+ val videoId = typedArray.getString(R.styleable.YouTubePlayerView_videoId)
+
+ typedArray.recycle()
+
+ if (autoPlay && videoId == null) {
+ throw IllegalStateException("YouTubePlayerView: videoId is not set but autoPlay is set to true. This combination is not allowed.")
+ }
+
+ val youTubePlayerListener = object : AbstractYouTubePlayerListener() {
+ override fun onReady(youTubePlayer: YouTubePlayer) {
+ videoId?.let {
+ youTubePlayer.loadOrCueVideo(legacyTubePlayerView.canPlay && autoPlay, videoId, 0f)
+ }
+
+ youTubePlayer.removeListener(this)
+ }
+ }
+
+ if (enableAutomaticInitialization) {
+ legacyTubePlayerView.initialize(
+ youTubePlayerListener,
+ handleNetworkEvents,
+ IFramePlayerOptions.getDefault(context),
+ videoId
+ )
+ }
+ }
+
+ // TODO: Use @JvmOverloads instead of duplicating the method. Unfortunately that will cause a breaking change.
+ fun initialize(youTubePlayerListener: YouTubePlayerListener, handleNetworkEvents: Boolean, playerOptions: IFramePlayerOptions, videoId: String?) {
+ if (enableAutomaticInitialization) {
+ throw IllegalStateException(AUTO_INIT_ERROR)
+ }
+ else {
+ legacyTubePlayerView.initialize(youTubePlayerListener, handleNetworkEvents, playerOptions, videoId)
+ }
+ }
+
+ /**
+ * Initialize the player. You must call this method before using the player.
+ * @param youTubePlayerListener listener for player events
+ * @param handleNetworkEvents if set to true a broadcast receiver will be registered and network events will be handled automatically.
+ * If set to false, you should handle network events with your own broadcast receiver.
+ * @param playerOptions customizable options for the embedded video player.
+ * @param videoId optional, used to load an initial video.
+ */
+ fun initialize(youTubePlayerListener: YouTubePlayerListener, handleNetworkEvents: Boolean, playerOptions: IFramePlayerOptions) {
+ if (enableAutomaticInitialization) {
+ throw IllegalStateException(AUTO_INIT_ERROR)
+ }
+ else {
+ legacyTubePlayerView.initialize(youTubePlayerListener, handleNetworkEvents, playerOptions, null)
+ }
+ }
+
+ /**
+ * Initialize the player.
+ * @param handleNetworkEvents if set to true a broadcast receiver will be registered and network events will be handled automatically.
+ * If set to false, you should handle network events with your own broadcast receiver.
+ *
+ * @see YouTubePlayerView.initialize
+ */
+ fun initialize(youTubePlayerListener: YouTubePlayerListener, handleNetworkEvents: Boolean) {
+ if (enableAutomaticInitialization) {
+ throw IllegalStateException(AUTO_INIT_ERROR)
+ }
+ else {
+ legacyTubePlayerView.initialize(youTubePlayerListener, handleNetworkEvents, IFramePlayerOptions.getDefault(context))
+ }
+ }
+
+ /**
+ * Initialize the player with player options.
+ *
+ * @see YouTubePlayerView.initialize
+ */
+ fun initialize(youTubePlayerListener: YouTubePlayerListener, playerOptions: IFramePlayerOptions) {
+ if (enableAutomaticInitialization) {
+ throw IllegalStateException(AUTO_INIT_ERROR)
+ }
+ else {
+ legacyTubePlayerView.initialize(youTubePlayerListener, true, playerOptions)
+ }
+ }
+
+ /**
+ * Initialize the player. Network events are automatically handled by the player.
+ * @param youTubePlayerListener listener for player events
+ *
+ * @see YouTubePlayerView.initialize
+ */
+ fun initialize(youTubePlayerListener: YouTubePlayerListener) {
+ if (enableAutomaticInitialization) {
+ throw IllegalStateException(AUTO_INIT_ERROR)
+ }
+ else {
+ legacyTubePlayerView.initialize(youTubePlayerListener, true)
+ }
+ }
+
+ /**
+ * @param youTubePlayerCallback A callback that will be called when the YouTubePlayer is ready.
+ * If the player is ready when the function is called, the callback will return immediately.
+ * This function is called only once.
+ */
+ fun getYouTubePlayerWhenReady(youTubePlayerCallback: YouTubePlayerCallback) = legacyTubePlayerView.getYouTubePlayerWhenReady(youTubePlayerCallback)
+
+ /**
+ * Use this method to add your own custom UI to the player.
+ *
+ * You will be responsible to manage the custom Ui from your application.
+ *
+ * WARNING: if yoy intend to publish your app on the PlayStore, using a custom UI might break YouTube terms of service.
+ *
+ * @param layoutId the ID of the layout defining the custom Ui.
+ * @return The inflated View
+ */
+ fun inflateCustomPlayerUi(@LayoutRes layoutId: Int) = legacyTubePlayerView.inflateCustomPlayerUi(layoutId)
+
+ fun setCustomPlayerUi(view: View) = legacyTubePlayerView.setCustomPlayerUi(view)
+
+ /**
+ * Don't use this method if you want to publish your app on the PlayStore. Background playback is against YouTube terms of service.
+ */
+ fun enableBackgroundPlayback(enable: Boolean) = legacyTubePlayerView.enableBackgroundPlayback(enable)
+
+ override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+ when (event) {
+ Lifecycle.Event.ON_RESUME -> onResume()
+ Lifecycle.Event.ON_STOP -> onStop()
+ Lifecycle.Event.ON_DESTROY -> release()
+ Lifecycle.Event.ON_CREATE, Lifecycle.Event.ON_START, Lifecycle.Event.ON_PAUSE, Lifecycle.Event.ON_ANY -> { }
+ }
+ }
+
+ /**
+ * Call this method before destroying the host Fragment/Activity, or register this View as an observer of its host lifecycle
+ */
+ fun release() = legacyTubePlayerView.release()
+
+ private fun onResume() = legacyTubePlayerView.onResume()
+
+ private fun onStop() = legacyTubePlayerView.onStop()
+
+ fun addYouTubePlayerListener(youTubePlayerListener: YouTubePlayerListener) = legacyTubePlayerView.webViewYouTubePlayer.addListener(youTubePlayerListener)
+
+ fun removeYouTubePlayerListener(youTubePlayerListener: YouTubePlayerListener) = legacyTubePlayerView.webViewYouTubePlayer.removeListener(youTubePlayerListener)
+
+ fun addFullscreenListener(fullscreenListener: FullscreenListener) = fullscreenListeners.add(fullscreenListener)
+
+ fun removeFullscreenListener(fullscreenListener: FullscreenListener) = fullscreenListeners.remove(fullscreenListener)
+
+ /**
+ * Convenience method to set the [YouTubePlayerView] width and height to match parent.
+ */
+ fun matchParent() {
+ setLayoutParams(
+ targetWidth = ViewGroup.LayoutParams.MATCH_PARENT,
+ targetHeight = ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ }
+
+ /**
+ * Convenience method to set the [YouTubePlayerView] width to match parent and
+ * height to wrap content.
+ */
+ fun wrapContent() {
+ setLayoutParams(
+ targetWidth = ViewGroup.LayoutParams.MATCH_PARENT,
+ targetHeight = ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ }
+
+ @Suppress("SameParameterValue")
+ private fun setLayoutParams(targetWidth: Int, targetHeight: Int) {
+ layoutParams = layoutParams.apply {
+ width = targetWidth
+ height = targetHeight
+ }
+ }
+}
diff --git a/youtube/core/src/main/res/raw/ayp_youtube_player.html b/youtube/core/src/main/res/raw/ayp_youtube_player.html
new file mode 100644
index 0000000..1acf2ea
--- /dev/null
+++ b/youtube/core/src/main/res/raw/ayp_youtube_player.html
@@ -0,0 +1,194 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/youtube/core/src/main/res/values/attrs.xml b/youtube/core/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..1cb0a4e
--- /dev/null
+++ b/youtube/core/src/main/res/values/attrs.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/youtube/core/src/main/resources/META-INF/com/pierfrancescosoffritti/androidyoutubeplayer/core/verification.properties b/youtube/core/src/main/resources/META-INF/com/pierfrancescosoffritti/androidyoutubeplayer/core/verification.properties
new file mode 100644
index 0000000..c134317
--- /dev/null
+++ b/youtube/core/src/main/resources/META-INF/com/pierfrancescosoffritti/androidyoutubeplayer/core/verification.properties
@@ -0,0 +1,3 @@
+#This is the verification token for the com.pierfrancescosoffritti.androidyoutubeplayer:core SDK.
+#Tue Sep 24 22:28:45 PDT 2024
+token=RLCF53RSTNFKXKRKVCHR7GH2LM
diff --git a/youtube/core/src/test/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/UtilsTest.kt b/youtube/core/src/test/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/UtilsTest.kt
new file mode 100644
index 0000000..07e2341
--- /dev/null
+++ b/youtube/core/src/test/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/UtilsTest.kt
@@ -0,0 +1,18 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core
+
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.views.readHTMLFromUTF8File
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.nio.charset.StandardCharsets
+
+class UtilsTest {
+ @Test
+ fun readParseHtmlCorrectly() {
+ val html = "somefakehtml
\nsomefakehtml
"
+ val inputStream = html.byteInputStream(StandardCharsets.UTF_8)
+
+ val parsedHtml = readHTMLFromUTF8File(inputStream)
+
+ assertEquals(parsedHtml, html)
+ }
+}
diff --git a/youtube/custom-ui/.gitignore b/youtube/custom-ui/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/youtube/custom-ui/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/youtube/custom-ui/build.gradle b/youtube/custom-ui/build.gradle
new file mode 100644
index 0000000..965ebc3
--- /dev/null
+++ b/youtube/custom-ui/build.gradle
@@ -0,0 +1,44 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply from: '../libVersions.gradle'
+
+android {
+ compileSdkVersion versions.compileSdk
+
+ defaultConfig {
+ minSdkVersion versions.minSdk
+ targetSdkVersion versions.compileSdk
+ }
+
+ sourceSets {
+ main.res.srcDirs = [
+ 'src/main/res',
+ 'src/main/res-public'
+ ]
+ }
+
+ namespace 'com.pierfrancescosoffritti.androidyoutubeplayer.core.customui'
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+}
+
+dependencies {
+ api project(':youtube:core')
+ implementation "androidx.core:core-ktx:$versions.androidxCore"
+ implementation "androidx.recyclerview:recyclerview:$versions.androidxRecyclerView"
+}
+
diff --git a/youtube/custom-ui/consumer-rules.pro b/youtube/custom-ui/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/youtube/custom-ui/proguard-rules.pro b/youtube/custom-ui/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/youtube/custom-ui/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/youtube/custom-ui/src/main/AndroidManifest.xml b/youtube/custom-ui/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..568741e
--- /dev/null
+++ b/youtube/custom-ui/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/youtube/custom-ui/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/customui/DefaultPlayerUiController.kt b/youtube/custom-ui/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/customui/DefaultPlayerUiController.kt
new file mode 100644
index 0000000..b6bac32
--- /dev/null
+++ b/youtube/custom-ui/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/customui/DefaultPlayerUiController.kt
@@ -0,0 +1,289 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.customui
+
+import android.content.Intent
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.util.Log
+import android.view.View
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.ProgressBar
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.PlayerConstants
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.AbstractYouTubePlayerListener
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.views.YouTubePlayerView
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.menu.YouTubePlayerMenu
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.menu.defaultMenu.DefaultYouTubePlayerMenu
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.utils.FadeViewHelper
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.views.YouTubePlayerSeekBar
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.views.YouTubePlayerSeekBarListener
+
+class DefaultPlayerUiController(
+ private val youTubePlayerView: YouTubePlayerView,
+ private val youTubePlayer: YouTubePlayer
+) : PlayerUiController {
+
+ val rootView: View = View.inflate(youTubePlayerView.context, R.layout.ayp_default_player_ui, null)
+
+ private var youTubePlayerMenu: YouTubePlayerMenu = DefaultYouTubePlayerMenu(
+ youTubePlayerView.context
+ )
+
+ /**
+ * View used for for intercepting clicks and for drawing a black background.
+ * Could have used controlsContainer, but in this way I'm able to hide all the control at once by hiding controlsContainer
+ */
+ private val panel: View = rootView.findViewById(R.id.panel)
+
+ private val controlsContainer: View = rootView.findViewById(R.id.controls_container)
+ private val extraViewsContainer: LinearLayout = rootView.findViewById(R.id.extra_views_container)
+
+ private val videoTitle: TextView = rootView.findViewById(R.id.video_title)
+ private val liveVideoIndicator: TextView = rootView.findViewById(R.id.live_video_indicator)
+
+ private val progressBar: ProgressBar = rootView.findViewById(R.id.progress)
+ private val menuButton: ImageView = rootView.findViewById(R.id.menu_button)
+ private val playPauseButton: ImageView = rootView.findViewById(R.id.play_pause_button)
+ private val youTubeButton: ImageView = rootView.findViewById(R.id.youtube_button)
+ private val fullscreenButton: ImageView = rootView.findViewById(R.id.fullscreen_button)
+
+ private val customActionLeft: ImageView = rootView.findViewById(R.id.custom_action_left_button)
+ private val customActionRight: ImageView = rootView.findViewById(R.id.custom_action_right_button)
+
+ private val youtubePlayerSeekBar: YouTubePlayerSeekBar = rootView.findViewById(R.id.youtube_player_seekbar)
+ private val fadeControlsContainer: FadeViewHelper = FadeViewHelper(controlsContainer)
+
+ private var onFullscreenButtonListener: View.OnClickListener
+ private var onMenuButtonClickListener: View.OnClickListener
+
+ private var isPlaying = false
+ private var isPlayPauseButtonEnabled = true
+ private var isCustomActionLeftEnabled = false
+ private var isCustomActionRightEnabled = false
+
+ private var isMatchParent = false
+
+ private val youTubePlayerStateListener = object : AbstractYouTubePlayerListener() {
+ override fun onStateChange(youTubePlayer: YouTubePlayer, state: PlayerConstants.PlayerState) {
+ updateState(state)
+
+ if (state === PlayerConstants.PlayerState.PLAYING || state === PlayerConstants.PlayerState.PAUSED || state === PlayerConstants.PlayerState.VIDEO_CUED) {
+ panel.setBackgroundColor(ContextCompat.getColor(panel.context, android.R.color.transparent))
+ progressBar.visibility = View.GONE
+
+ if (isPlayPauseButtonEnabled) playPauseButton.visibility = View.VISIBLE
+ if (isCustomActionLeftEnabled) customActionLeft.visibility = View.VISIBLE
+ if (isCustomActionRightEnabled) customActionRight.visibility = View.VISIBLE
+
+ updatePlayPauseButtonIcon(state === PlayerConstants.PlayerState.PLAYING)
+
+ } else {
+ updatePlayPauseButtonIcon(false)
+
+ if (state === PlayerConstants.PlayerState.BUFFERING) {
+ progressBar.visibility = View.VISIBLE
+ panel.setBackgroundColor(
+ ContextCompat.getColor(
+ panel.context,
+ android.R.color.transparent
+ )
+ )
+ if (isPlayPauseButtonEnabled) playPauseButton.visibility = View.INVISIBLE
+
+ customActionLeft.visibility = View.GONE
+ customActionRight.visibility = View.GONE
+ }
+
+ if (state === PlayerConstants.PlayerState.UNSTARTED) {
+ progressBar.visibility = View.GONE
+ if (isPlayPauseButtonEnabled) playPauseButton.visibility = View.VISIBLE
+ }
+ }
+ }
+
+ override fun onVideoId(youTubePlayer: YouTubePlayer, videoId: String) {
+ youTubeButton.setOnClickListener {
+ val intent = Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse("https://www.youtube.com/watch?v=" + videoId + "#t=" + youtubePlayerSeekBar.seekBar.progress)
+ )
+ try {
+ youTubeButton.context.startActivity(intent)
+ } catch (e: Exception) {
+ Log.e(javaClass.simpleName, e.message ?: "Can't open url to YouTube")
+ }
+ }
+ }
+ }
+
+ init {
+ onFullscreenButtonListener = View.OnClickListener {
+ isMatchParent = !isMatchParent
+ when (isMatchParent) {
+ true -> youTubePlayerView.matchParent()
+ false -> youTubePlayerView.wrapContent()
+ }
+ }
+
+ onMenuButtonClickListener = View.OnClickListener { youTubePlayerMenu.show(menuButton) }
+
+ initClickListeners()
+ }
+
+ private fun initClickListeners() {
+ youTubePlayer.addListener(youtubePlayerSeekBar)
+ youTubePlayer.addListener(fadeControlsContainer)
+ youTubePlayer.addListener(youTubePlayerStateListener)
+
+ youtubePlayerSeekBar.youtubePlayerSeekBarListener = object : YouTubePlayerSeekBarListener {
+ override fun seekTo(time: Float) = youTubePlayer.seekTo(time)
+ }
+ panel.setOnClickListener { fadeControlsContainer.toggleVisibility() }
+ playPauseButton.setOnClickListener { onPlayButtonPressed() }
+ fullscreenButton.setOnClickListener { onFullscreenButtonListener.onClick(fullscreenButton) }
+ menuButton.setOnClickListener { onMenuButtonClickListener.onClick(menuButton) }
+ }
+
+ override fun showVideoTitle(show: Boolean): PlayerUiController {
+ videoTitle.visibility = if (show) View.VISIBLE else View.GONE
+ return this
+ }
+
+ override fun setVideoTitle(videoTitle: String): PlayerUiController {
+ this.videoTitle.text = videoTitle
+ return this
+ }
+
+ override fun showUi(show: Boolean): PlayerUiController {
+ fadeControlsContainer.isDisabled = !show
+ controlsContainer.visibility = if (show) View.VISIBLE else View.INVISIBLE
+ return this
+ }
+
+ override fun showPlayPauseButton(show: Boolean): PlayerUiController {
+ playPauseButton.visibility = if (show) View.VISIBLE else View.GONE
+
+ isPlayPauseButtonEnabled = show
+ return this
+ }
+
+ override fun enableLiveVideoUi(enable: Boolean): PlayerUiController {
+ youtubePlayerSeekBar.visibility = if (enable) View.INVISIBLE else View.VISIBLE
+ liveVideoIndicator.visibility = if (enable) View.VISIBLE else View.GONE
+ return this
+ }
+
+ override fun setCustomAction1(
+ icon: Drawable,
+ clickListener: View.OnClickListener?
+ ): PlayerUiController {
+ customActionLeft.setImageDrawable(icon)
+ customActionLeft.setOnClickListener(clickListener)
+ showCustomAction1(true)
+ return this
+ }
+
+ override fun setCustomAction2(
+ icon: Drawable,
+ clickListener: View.OnClickListener?
+ ): PlayerUiController {
+ customActionRight.setImageDrawable(icon)
+ customActionRight.setOnClickListener(clickListener)
+ showCustomAction2(true)
+ return this
+ }
+
+ override fun showCustomAction1(show: Boolean): PlayerUiController {
+ isCustomActionLeftEnabled = show
+ customActionLeft.visibility = if (show) View.VISIBLE else View.GONE
+ return this
+ }
+
+ override fun showCustomAction2(show: Boolean): PlayerUiController {
+ isCustomActionRightEnabled = show
+ customActionRight.visibility = if (show) View.VISIBLE else View.GONE
+ return this
+ }
+
+ override fun showMenuButton(show: Boolean): PlayerUiController {
+ menuButton.visibility = if (show) View.VISIBLE else View.GONE
+ return this
+ }
+
+ override fun setMenuButtonClickListener(customMenuButtonClickListener: View.OnClickListener): PlayerUiController {
+ onMenuButtonClickListener = customMenuButtonClickListener
+ return this
+ }
+
+ override fun showCurrentTime(show: Boolean): PlayerUiController {
+ youtubePlayerSeekBar.videoCurrentTimeTextView.visibility = if (show) View.VISIBLE else View.GONE
+ return this
+ }
+
+ override fun showDuration(show: Boolean): PlayerUiController {
+ youtubePlayerSeekBar.videoDurationTextView.visibility = if (show) View.VISIBLE else View.GONE
+ return this
+ }
+
+ override fun showSeekBar(show: Boolean): PlayerUiController {
+ youtubePlayerSeekBar.seekBar.visibility = if (show) View.VISIBLE else View.INVISIBLE
+ return this
+ }
+
+ override fun showBufferingProgress(show: Boolean): PlayerUiController {
+ youtubePlayerSeekBar.showBufferingProgress = show
+ return this
+ }
+
+ override fun showYouTubeButton(show: Boolean): PlayerUiController {
+ youTubeButton.visibility = if (show) View.VISIBLE else View.GONE
+ return this
+ }
+
+ override fun addView(view: View): PlayerUiController {
+ extraViewsContainer.addView(view, 0)
+ return this
+ }
+
+ override fun removeView(view: View): PlayerUiController {
+ extraViewsContainer.removeView(view)
+ return this
+ }
+
+ override fun getMenu(): YouTubePlayerMenu = youTubePlayerMenu
+
+ override fun showFullscreenButton(show: Boolean): PlayerUiController {
+ fullscreenButton.visibility = if (show) View.VISIBLE else View.GONE
+ return this
+ }
+
+ override fun setFullscreenButtonClickListener(customFullscreenButtonClickListener: View.OnClickListener): PlayerUiController {
+ onFullscreenButtonListener = customFullscreenButtonClickListener
+ return this
+ }
+
+ private fun onPlayButtonPressed() {
+ if (isPlaying)
+ youTubePlayer.pause()
+ else
+ youTubePlayer.play()
+ }
+
+ private fun updateState(state: PlayerConstants.PlayerState) {
+ when (state) {
+ PlayerConstants.PlayerState.ENDED -> isPlaying = false
+ PlayerConstants.PlayerState.PAUSED -> isPlaying = false
+ PlayerConstants.PlayerState.PLAYING -> isPlaying = true
+ else -> {}
+ }
+
+ updatePlayPauseButtonIcon(!isPlaying)
+ }
+
+ private fun updatePlayPauseButtonIcon(playing: Boolean) {
+ val drawable = if (playing) R.drawable.ayp_ic_pause_36dp else R.drawable.ayp_ic_play_36dp
+ playPauseButton.setImageResource(drawable)
+ }
+}
diff --git a/youtube/custom-ui/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/customui/PlayerUiController.kt b/youtube/custom-ui/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/customui/PlayerUiController.kt
new file mode 100644
index 0000000..8786c45
--- /dev/null
+++ b/youtube/custom-ui/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/customui/PlayerUiController.kt
@@ -0,0 +1,57 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.customui
+
+import android.graphics.drawable.Drawable
+import android.view.View
+
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.menu.YouTubePlayerMenu
+
+
+interface PlayerUiController {
+ fun showUi(show: Boolean): PlayerUiController
+ fun showPlayPauseButton(show: Boolean): PlayerUiController
+
+ fun showVideoTitle(show: Boolean): PlayerUiController
+ fun setVideoTitle(videoTitle: String): PlayerUiController
+
+ fun enableLiveVideoUi(enable: Boolean): PlayerUiController
+
+ /**
+ * Set custom action to the left of the Play/Pause button
+ */
+ fun setCustomAction1(icon: Drawable, clickListener: View.OnClickListener?): PlayerUiController
+
+ /**
+ * Set custom action to the right of the Play/Pause button
+ */
+ fun setCustomAction2(icon: Drawable, clickListener: View.OnClickListener?): PlayerUiController
+ fun showCustomAction1(show: Boolean): PlayerUiController
+ fun showCustomAction2(show: Boolean): PlayerUiController
+
+ fun showFullscreenButton(show: Boolean): PlayerUiController
+ fun setFullscreenButtonClickListener(customFullscreenButtonClickListener: View.OnClickListener): PlayerUiController
+
+ fun showMenuButton(show: Boolean): PlayerUiController
+ fun setMenuButtonClickListener(customMenuButtonClickListener: View.OnClickListener): PlayerUiController
+
+ fun showCurrentTime(show: Boolean): PlayerUiController
+ fun showDuration(show: Boolean): PlayerUiController
+
+ fun showSeekBar(show: Boolean): PlayerUiController
+ fun showBufferingProgress(show: Boolean): PlayerUiController
+
+ fun showYouTubeButton(show: Boolean): PlayerUiController
+
+ /**
+ * Adds a View to the top of the player
+ * @param view View to be added
+ */
+ fun addView(view: View): PlayerUiController
+
+ /**
+ * Removes a View added with [PlayerUiController.addView]
+ * @param view View to be removed
+ */
+ fun removeView(view: View): PlayerUiController
+
+ fun getMenu(): YouTubePlayerMenu?
+}
diff --git a/youtube/custom-ui/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/customui/menu/MenuItem.kt b/youtube/custom-ui/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/customui/menu/MenuItem.kt
new file mode 100644
index 0000000..2882cdd
--- /dev/null
+++ b/youtube/custom-ui/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/customui/menu/MenuItem.kt
@@ -0,0 +1,10 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.menu
+
+import android.view.View
+import androidx.annotation.DrawableRes
+
+data class MenuItem @JvmOverloads constructor(
+ val text: String,
+ @DrawableRes val icon: Int? = null,
+ val onClickListener: View.OnClickListener
+)
\ No newline at end of file
diff --git a/youtube/custom-ui/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/customui/menu/YouTubePlayerMenu.kt b/youtube/custom-ui/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/customui/menu/YouTubePlayerMenu.kt
new file mode 100644
index 0000000..f12e454
--- /dev/null
+++ b/youtube/custom-ui/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/customui/menu/YouTubePlayerMenu.kt
@@ -0,0 +1,13 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.menu
+
+import android.view.View
+
+interface YouTubePlayerMenu {
+ val itemCount: Int
+ fun show(anchorView: View)
+ fun dismiss()
+
+ fun addItem(menuItem: MenuItem): YouTubePlayerMenu
+ fun removeItem(itemIndex: Int): YouTubePlayerMenu
+ fun removeItem(menuItem: MenuItem): YouTubePlayerMenu
+}
diff --git a/youtube/custom-ui/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/customui/menu/defaultMenu/DefaultYouTubePlayerMenu.kt b/youtube/custom-ui/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/customui/menu/defaultMenu/DefaultYouTubePlayerMenu.kt
new file mode 100644
index 0000000..7762f70
--- /dev/null
+++ b/youtube/custom-ui/src/main/java/com/pierfrancescosoffritti/androidyoutubeplayer/core/customui/menu/defaultMenu/DefaultYouTubePlayerMenu.kt
@@ -0,0 +1,73 @@
+package com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.menu.defaultMenu
+
+import android.content.Context
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.widget.PopupWindow
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.R
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.menu.MenuItem
+import com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.menu.YouTubePlayerMenu
+
+internal class DefaultYouTubePlayerMenu(private val context: Context) : YouTubePlayerMenu {
+ private val menuItems = ArrayList