youtube module
This commit is contained in:
parent
8b1add722e
commit
be16d57958
|
|
@ -70,6 +70,10 @@ dependencies {
|
||||||
implementation(project(":core:architecture"))
|
implementation(project(":core:architecture"))
|
||||||
//api(project(":core:architecture-reflect"))
|
//api(project(":core:architecture-reflect"))
|
||||||
implementation(project(":core:network"))
|
implementation(project(":core:network"))
|
||||||
|
|
||||||
|
implementation(project(":youtube:core"))
|
||||||
|
implementation(project(":youtube:custom-ui"))
|
||||||
|
|
||||||
implementation libs.androidx.navigation.fragment.ktx
|
implementation libs.androidx.navigation.fragment.ktx
|
||||||
implementation(libs.startup)
|
implementation(libs.startup)
|
||||||
implementation(libs.hilt.android)
|
implementation(libs.hilt.android)
|
||||||
|
|
@ -80,8 +84,6 @@ dependencies {
|
||||||
implementation(libs.protobuf.kotlin.lite)
|
implementation(libs.protobuf.kotlin.lite)
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
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.glide) // ImageLoader在用
|
||||||
implementation(libs.okhttp.logging)
|
implementation(libs.okhttp.logging)
|
||||||
implementation(libs.retrofit)
|
implementation(libs.retrofit)
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ android {
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,5 +19,7 @@ dependencyResolutionManagement {
|
||||||
rootProject.name = "VidiDin-android"
|
rootProject.name = "VidiDin-android"
|
||||||
|
|
||||||
apply from: "./core/core.includes.gradle"
|
apply from: "./core/core.includes.gradle"
|
||||||
|
apply from: "./youtube/youtube.includes.gradle"
|
||||||
include ':app'
|
include ':app'
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<manifest
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
|
</manifest>
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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<YouTubePlayerListener>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Long, BooleanProvider>()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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) }
|
||||||
|
}
|
||||||
|
|
@ -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) {}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners
|
||||||
|
|
||||||
|
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer
|
||||||
|
|
||||||
|
interface YouTubePlayerCallback {
|
||||||
|
fun onYouTubePlayer(youTubePlayer: YouTubePlayer)
|
||||||
|
}
|
||||||
|
|
@ -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. <br></br><br></br>
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Listener>()
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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<YouTubePlayerCallback>()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<YouTubePlayerListener>()
|
||||||
|
|
||||||
|
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<YouTubePlayerListener> = 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<YouTubePlayerListener> 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("<<injectedVideoId>>", if (videoId != null) { "'$videoId'" } else { "undefined" })
|
||||||
|
.replace("<<injectedPlayerVars>>", 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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<FullscreenListener>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<style type="text/css">
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #000000;
|
||||||
|
overflow: hidden;
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<!-- defer forces the library to execute after the html page is fully parsed. -->
|
||||||
|
<!-- This is needed to avoid race conditions, where the library executes and calls `onYouTubeIframeAPIReady` before the page is fully parsed. -->
|
||||||
|
<!-- See #873 on GitHub -->
|
||||||
|
<script defer src="https://www.youtube.com/iframe_api"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="youTubePlayerDOM"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
var UNSTARTED = "UNSTARTED";
|
||||||
|
var ENDED = "ENDED";
|
||||||
|
var PLAYING = "PLAYING";
|
||||||
|
var PAUSED = "PAUSED";
|
||||||
|
var BUFFERING = "BUFFERING";
|
||||||
|
var CUED = "CUED";
|
||||||
|
|
||||||
|
var YouTubePlayerBridge = window.YouTubePlayerBridge;
|
||||||
|
var YouTubePlayerCallbacks = window.YouTubePlayerCallbacks;
|
||||||
|
var player;
|
||||||
|
|
||||||
|
var timerId;
|
||||||
|
|
||||||
|
function onYouTubeIframeAPIReady() {
|
||||||
|
|
||||||
|
YouTubePlayerBridge.sendYouTubeIFrameAPIReady();
|
||||||
|
|
||||||
|
var youtubePlayerConfig = {
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
events: {
|
||||||
|
onReady: function(event) { YouTubePlayerBridge.sendReady() },
|
||||||
|
onStateChange: function(event) { sendPlayerStateChange(event.data) },
|
||||||
|
onPlaybackQualityChange: function(event) { YouTubePlayerBridge.sendPlaybackQualityChange(event.data) },
|
||||||
|
onPlaybackRateChange: function(event) { YouTubePlayerBridge.sendPlaybackRateChange(event.data) },
|
||||||
|
onError: function(error) { YouTubePlayerBridge.sendError(error.data) },
|
||||||
|
onApiChange: function(event) { YouTubePlayerBridge.sendApiChange() }
|
||||||
|
},
|
||||||
|
playerVars: <<injectedPlayerVars>>
|
||||||
|
};
|
||||||
|
|
||||||
|
if (<<injectedVideoId>>) {
|
||||||
|
youtubePlayerConfig.videoId = <<injectedVideoId>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
player = new YT.Player('youTubePlayerDOM', youtubePlayerConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendPlayerStateChange(playerState) {
|
||||||
|
clearTimeout(timerId);
|
||||||
|
|
||||||
|
switch (playerState) {
|
||||||
|
case YT.PlayerState.UNSTARTED:
|
||||||
|
sendStateChange(UNSTARTED);
|
||||||
|
sendVideoIdFromPlaylistIfAvailable(player);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case YT.PlayerState.ENDED:
|
||||||
|
sendStateChange(ENDED);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case YT.PlayerState.PLAYING:
|
||||||
|
sendStateChange(PLAYING);
|
||||||
|
|
||||||
|
startSendCurrentTimeInterval();
|
||||||
|
sendVideoData(player);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case YT.PlayerState.PAUSED:
|
||||||
|
sendStateChange(PAUSED);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case YT.PlayerState.BUFFERING:
|
||||||
|
sendStateChange(BUFFERING);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case YT.PlayerState.CUED:
|
||||||
|
sendStateChange(CUED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendVideoData(player) {
|
||||||
|
var videoDuration = player.getDuration();
|
||||||
|
|
||||||
|
YouTubePlayerBridge.sendVideoDuration(videoDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method checks if the player is playing a playlist.
|
||||||
|
// If yes, it sends out the video id of the video being played.
|
||||||
|
function sendVideoIdFromPlaylistIfAvailable(player) {
|
||||||
|
var playlist = player.getPlaylist();
|
||||||
|
if ( typeof playlist !== 'undefined' && Array.isArray(playlist) && playlist.length > 0 ) {
|
||||||
|
var index = player.getPlaylistIndex();
|
||||||
|
var videoId = playlist[index];
|
||||||
|
YouTubePlayerBridge.sendVideoId(videoId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendStateChange(newState) {
|
||||||
|
YouTubePlayerBridge.sendStateChange(newState)
|
||||||
|
}
|
||||||
|
|
||||||
|
function startSendCurrentTimeInterval() {
|
||||||
|
timerId = setInterval(function() {
|
||||||
|
YouTubePlayerBridge.sendVideoCurrentTime( player.getCurrentTime() )
|
||||||
|
YouTubePlayerBridge.sendVideoLoadedFraction( player.getVideoLoadedFraction() )
|
||||||
|
}, 100 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// JAVA to WEB functions
|
||||||
|
|
||||||
|
function seekTo(startSeconds) {
|
||||||
|
player.seekTo(startSeconds, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pauseVideo() {
|
||||||
|
player.pauseVideo();
|
||||||
|
}
|
||||||
|
|
||||||
|
function playVideo() {
|
||||||
|
player.playVideo();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadVideo(videoId, startSeconds) {
|
||||||
|
player.loadVideoById(videoId, startSeconds);
|
||||||
|
YouTubePlayerBridge.sendVideoId(videoId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cueVideo(videoId, startSeconds) {
|
||||||
|
player.cueVideoById(videoId, startSeconds);
|
||||||
|
YouTubePlayerBridge.sendVideoId(videoId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mute() {
|
||||||
|
player.mute();
|
||||||
|
}
|
||||||
|
|
||||||
|
function unMute() {
|
||||||
|
player.unMute();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setVolume(volumePercent) {
|
||||||
|
player.setVolume(volumePercent);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPlaybackRate(playbackRate) {
|
||||||
|
player.setPlaybackRate(playbackRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextVideo() {
|
||||||
|
player.nextVideo();
|
||||||
|
}
|
||||||
|
|
||||||
|
function previousVideo() {
|
||||||
|
player.previousVideo();
|
||||||
|
}
|
||||||
|
|
||||||
|
function playVideoAt(index) {
|
||||||
|
player.playVideoAt(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLoop(loop) {
|
||||||
|
player.setLoop(loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setShuffle(shuffle) {
|
||||||
|
player.setShuffle(shuffle);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMuteValue(requestId) {
|
||||||
|
var isMuted = player.isMuted();
|
||||||
|
YouTubePlayerCallbacks.sendBooleanValue(requestId, isMuted);
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<declare-styleable name="YouTubePlayerView">
|
||||||
|
<attr name="enableAutomaticInitialization" format="boolean" />
|
||||||
|
<attr name="videoId" format="string" />
|
||||||
|
<attr name="autoPlay" format="boolean" />
|
||||||
|
<attr name="handleNetworkEvents" format="boolean" />
|
||||||
|
</declare-styleable>
|
||||||
|
</resources>
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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 = "<div>some<span>fake</span>html</div>\n<div>some<span>fake</span>html</div>"
|
||||||
|
val inputStream = html.byteInputStream(StandardCharsets.UTF_8)
|
||||||
|
|
||||||
|
val parsedHtml = readHTMLFromUTF8File(inputStream)
|
||||||
|
|
||||||
|
assertEquals(parsedHtml, html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest />
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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?
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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<MenuItem>()
|
||||||
|
private var popupWindow: PopupWindow? = null
|
||||||
|
|
||||||
|
override val itemCount: Int
|
||||||
|
get() = menuItems.size
|
||||||
|
|
||||||
|
override fun show(anchorView: View) {
|
||||||
|
popupWindow = createPopupWindow()
|
||||||
|
popupWindow?.showAsDropDown(
|
||||||
|
anchorView,
|
||||||
|
-context.resources.getDimensionPixelSize(R.dimen.ayp_8dp) * 12,
|
||||||
|
-context.resources.getDimensionPixelSize(R.dimen.ayp_8dp) * 12
|
||||||
|
)
|
||||||
|
|
||||||
|
if (menuItems.size == 0)
|
||||||
|
Log.e(YouTubePlayerMenu::class.java.name, "The menu is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dismiss() {
|
||||||
|
popupWindow?.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addItem(menuItem: MenuItem): YouTubePlayerMenu {
|
||||||
|
menuItems.add(menuItem)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeItem(itemIndex: Int): YouTubePlayerMenu {
|
||||||
|
menuItems.removeAt(itemIndex)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeItem(menuItem: MenuItem): YouTubePlayerMenu {
|
||||||
|
menuItems.remove(menuItem)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPopupWindow(): PopupWindow {
|
||||||
|
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||||
|
val view = inflater.inflate(R.layout.ayp_player_menu, null)
|
||||||
|
|
||||||
|
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_view)
|
||||||
|
recyclerView.layoutManager = LinearLayoutManager(context)
|
||||||
|
recyclerView.adapter = MenuAdapter(context, menuItems)
|
||||||
|
recyclerView.setHasFixedSize(true)
|
||||||
|
|
||||||
|
val popupWindow = PopupWindow(
|
||||||
|
view, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
popupWindow.contentView = view
|
||||||
|
popupWindow.isFocusable = true
|
||||||
|
popupWindow.width = WindowManager.LayoutParams.WRAP_CONTENT
|
||||||
|
popupWindow.height = WindowManager.LayoutParams.WRAP_CONTENT
|
||||||
|
|
||||||
|
return popupWindow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.menu.defaultMenu
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.R
|
||||||
|
import com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.menu.MenuItem
|
||||||
|
|
||||||
|
internal class MenuAdapter(private val context: Context, private val menuItems: List<MenuItem>) :
|
||||||
|
RecyclerView.Adapter<MenuAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context).inflate(R.layout.ayp_menu_item, parent, false)
|
||||||
|
return ViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
holder.root.setOnClickListener(menuItems[position].onClickListener)
|
||||||
|
holder.textView.text = menuItems[position].text
|
||||||
|
menuItems[position].icon?.let {
|
||||||
|
holder.textView.setCompoundDrawablesWithIntrinsicBounds(
|
||||||
|
ContextCompat.getDrawable(context, it),
|
||||||
|
null, null, null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return menuItems.size
|
||||||
|
}
|
||||||
|
|
||||||
|
internal inner class ViewHolder(val root: View) : RecyclerView.ViewHolder(root) {
|
||||||
|
val textView: TextView = root.findViewById(R.id.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
package com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.utils
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.view.View
|
||||||
|
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.PlayerConstants
|
||||||
|
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer
|
||||||
|
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.YouTubePlayerListener
|
||||||
|
|
||||||
|
class FadeViewHelper(val targetView: View) : YouTubePlayerListener {
|
||||||
|
companion object {
|
||||||
|
const val DEFAULT_ANIMATION_DURATION = 300L
|
||||||
|
const val DEFAULT_FADE_OUT_DELAY = 3000L
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isPlaying = false
|
||||||
|
|
||||||
|
private var canFade = false
|
||||||
|
private var isVisible = true
|
||||||
|
|
||||||
|
private var fadeOut: Runnable = Runnable { fade(0f) }
|
||||||
|
|
||||||
|
var isDisabled = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duration of the fade animation in milliseconds.
|
||||||
|
*/
|
||||||
|
var animationDuration = DEFAULT_ANIMATION_DURATION
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delay after which the view automatically fades out.
|
||||||
|
*/
|
||||||
|
var fadeOutDelay = DEFAULT_FADE_OUT_DELAY
|
||||||
|
|
||||||
|
fun toggleVisibility() {
|
||||||
|
fade(if (isVisible) 0f else 1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fade(finalAlpha: Float) {
|
||||||
|
if (!canFade || isDisabled)
|
||||||
|
return
|
||||||
|
|
||||||
|
isVisible = finalAlpha != 0f
|
||||||
|
|
||||||
|
// if the controls are shown and the player is playing they should automatically fade after a while.
|
||||||
|
// otherwise don't do anything automatically
|
||||||
|
if (finalAlpha == 1f && isPlaying)
|
||||||
|
targetView.handler?.postDelayed(fadeOut, fadeOutDelay)
|
||||||
|
else
|
||||||
|
targetView.handler?.removeCallbacks(fadeOut)
|
||||||
|
|
||||||
|
targetView.animate()
|
||||||
|
.alpha(finalAlpha)
|
||||||
|
.setDuration(animationDuration)
|
||||||
|
.setListener(object : Animator.AnimatorListener {
|
||||||
|
override fun onAnimationStart(animator: Animator) {
|
||||||
|
if (finalAlpha == 1f) targetView.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(animator: Animator) {
|
||||||
|
if (finalAlpha == 0f) targetView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationCancel(animator: Animator) {}
|
||||||
|
override fun onAnimationRepeat(animator: Animator) {}
|
||||||
|
}).start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateState(state: PlayerConstants.PlayerState) {
|
||||||
|
when (state) {
|
||||||
|
PlayerConstants.PlayerState.ENDED -> isPlaying = false
|
||||||
|
PlayerConstants.PlayerState.PAUSED -> isPlaying = false
|
||||||
|
PlayerConstants.PlayerState.PLAYING -> isPlaying = true
|
||||||
|
PlayerConstants.PlayerState.UNSTARTED -> {}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStateChange(youTubePlayer: YouTubePlayer, state: PlayerConstants.PlayerState) {
|
||||||
|
updateState(state)
|
||||||
|
|
||||||
|
when (state) {
|
||||||
|
PlayerConstants.PlayerState.PLAYING, PlayerConstants.PlayerState.PAUSED, PlayerConstants.PlayerState.VIDEO_CUED -> {
|
||||||
|
canFade = true
|
||||||
|
if (state == PlayerConstants.PlayerState.PLAYING)
|
||||||
|
targetView.handler?.postDelayed(fadeOut, fadeOutDelay)
|
||||||
|
else
|
||||||
|
targetView.handler?.removeCallbacks(fadeOut)
|
||||||
|
}
|
||||||
|
PlayerConstants.PlayerState.BUFFERING, PlayerConstants.PlayerState.UNSTARTED -> {
|
||||||
|
fade(1f)
|
||||||
|
canFade = false
|
||||||
|
}
|
||||||
|
PlayerConstants.PlayerState.UNKNOWN -> fade(1f)
|
||||||
|
PlayerConstants.PlayerState.ENDED -> fade(1f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReady(youTubePlayer: YouTubePlayer) {}
|
||||||
|
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) {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.utils
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
|
||||||
|
object TimeUtilities {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the time in seconds in a string with format "M:SS".
|
||||||
|
*/
|
||||||
|
@SuppressLint("DefaultLocale")
|
||||||
|
@JvmStatic
|
||||||
|
fun formatTime(timeInSeconds: Float): String {
|
||||||
|
val minutes = (timeInSeconds / 60).toInt()
|
||||||
|
val seconds = (timeInSeconds % 60).toInt()
|
||||||
|
return String.format("%d:%02d", minutes, seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
package com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.views
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.SeekBar
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
|
import com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.R
|
||||||
|
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.PlayerConstants
|
||||||
|
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer
|
||||||
|
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.YouTubePlayerListener
|
||||||
|
import com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.utils.TimeUtilities
|
||||||
|
|
||||||
|
class YouTubePlayerSeekBar(context: Context, attrs: AttributeSet? = null) :
|
||||||
|
LinearLayout(context, attrs), SeekBar.OnSeekBarChangeListener, YouTubePlayerListener {
|
||||||
|
|
||||||
|
private var seekBarTouchStarted = false
|
||||||
|
|
||||||
|
// I need this variable because onCurrentSecond gets called every 100 mils, so without the proper checks on this variable in onCurrentSeconds the seek bar glitches when touched.
|
||||||
|
private var newSeekBarProgress = -1
|
||||||
|
|
||||||
|
private var isPlaying = false
|
||||||
|
|
||||||
|
var showBufferingProgress = true
|
||||||
|
var youtubePlayerSeekBarListener: YouTubePlayerSeekBarListener? = null
|
||||||
|
|
||||||
|
val videoCurrentTimeTextView = TextView(context)
|
||||||
|
val videoDurationTextView = TextView(context)
|
||||||
|
val seekBar = SeekBar(context)
|
||||||
|
|
||||||
|
init {
|
||||||
|
val typedArray =
|
||||||
|
context.theme.obtainStyledAttributes(attrs, R.styleable.YouTubePlayerSeekBar, 0, 0)
|
||||||
|
|
||||||
|
val fontSize = typedArray.getDimensionPixelSize(
|
||||||
|
R.styleable.YouTubePlayerSeekBar_fontSize,
|
||||||
|
resources.getDimensionPixelSize(R.dimen.ayp_12sp)
|
||||||
|
)
|
||||||
|
val color = typedArray.getColor(
|
||||||
|
R.styleable.YouTubePlayerSeekBar_color,
|
||||||
|
ContextCompat.getColor(context, R.color.ayp_red)
|
||||||
|
)
|
||||||
|
|
||||||
|
typedArray.recycle()
|
||||||
|
|
||||||
|
val padding = resources.getDimensionPixelSize(R.dimen.ayp_8dp)
|
||||||
|
|
||||||
|
videoCurrentTimeTextView.text = resources.getString(R.string.ayp_null_time)
|
||||||
|
videoCurrentTimeTextView.setPadding(padding, padding, 0, padding)
|
||||||
|
videoCurrentTimeTextView.setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||||
|
videoCurrentTimeTextView.gravity = Gravity.CENTER_VERTICAL
|
||||||
|
|
||||||
|
videoDurationTextView.text = resources.getString(R.string.ayp_null_time)
|
||||||
|
videoDurationTextView.setPadding(0, padding, padding, padding)
|
||||||
|
videoDurationTextView.setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||||
|
videoDurationTextView.gravity = Gravity.CENTER_VERTICAL
|
||||||
|
|
||||||
|
setFontSize(fontSize.toFloat())
|
||||||
|
|
||||||
|
seekBar.setPadding(padding * 2, padding, padding * 2, padding)
|
||||||
|
setColor(color)
|
||||||
|
|
||||||
|
addView(
|
||||||
|
videoCurrentTimeTextView,
|
||||||
|
LayoutParams(
|
||||||
|
LayoutParams.WRAP_CONTENT,
|
||||||
|
LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
addView(seekBar, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
|
||||||
|
addView(
|
||||||
|
videoDurationTextView,
|
||||||
|
LayoutParams(
|
||||||
|
LayoutParams.WRAP_CONTENT,
|
||||||
|
LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
gravity = Gravity.CENTER_VERTICAL
|
||||||
|
|
||||||
|
seekBar.setOnSeekBarChangeListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param fontSize in pixels.
|
||||||
|
*/
|
||||||
|
fun setFontSize(fontSize: Float) {
|
||||||
|
videoCurrentTimeTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize)
|
||||||
|
videoDurationTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setColor(@ColorInt color: Int) {
|
||||||
|
DrawableCompat.setTint(seekBar.thumb, color)
|
||||||
|
DrawableCompat.setTint(seekBar.progressDrawable, color)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateState(state: PlayerConstants.PlayerState) {
|
||||||
|
when (state) {
|
||||||
|
PlayerConstants.PlayerState.ENDED -> isPlaying = false
|
||||||
|
PlayerConstants.PlayerState.PAUSED -> isPlaying = false
|
||||||
|
PlayerConstants.PlayerState.PLAYING -> isPlaying = true
|
||||||
|
PlayerConstants.PlayerState.UNSTARTED -> resetUi()
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetUi() {
|
||||||
|
seekBar.progress = 0
|
||||||
|
seekBar.max = 0
|
||||||
|
videoDurationTextView.post { videoDurationTextView.text = "" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seekbar
|
||||||
|
|
||||||
|
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||||
|
videoCurrentTimeTextView.text = TimeUtilities.formatTime(progress.toFloat())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
||||||
|
seekBarTouchStarted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||||
|
if (isPlaying)
|
||||||
|
newSeekBarProgress = seekBar.progress
|
||||||
|
|
||||||
|
youtubePlayerSeekBarListener?.seekTo(seekBar.progress.toFloat())
|
||||||
|
seekBarTouchStarted = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// YouTubePlayerListener
|
||||||
|
|
||||||
|
override fun onStateChange(youTubePlayer: YouTubePlayer, state: PlayerConstants.PlayerState) {
|
||||||
|
newSeekBarProgress = -1
|
||||||
|
updateState(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCurrentSecond(youTubePlayer: YouTubePlayer, second: Float) {
|
||||||
|
// ignore if the user is currently moving the SeekBar
|
||||||
|
if (seekBarTouchStarted)
|
||||||
|
return
|
||||||
|
// ignore if the current time is older than what the user selected with the SeekBar
|
||||||
|
if (newSeekBarProgress > 0 && TimeUtilities.formatTime(second) != TimeUtilities.formatTime(
|
||||||
|
newSeekBarProgress.toFloat()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
newSeekBarProgress = -1
|
||||||
|
|
||||||
|
seekBar.progress = second.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onVideoDuration(youTubePlayer: YouTubePlayer, duration: Float) {
|
||||||
|
videoDurationTextView.text = TimeUtilities.formatTime(duration)
|
||||||
|
seekBar.max = duration.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onVideoLoadedFraction(youTubePlayer: YouTubePlayer, loadedFraction: Float) {
|
||||||
|
if (showBufferingProgress)
|
||||||
|
seekBar.secondaryProgress = (loadedFraction * seekBar.max).toInt()
|
||||||
|
else
|
||||||
|
seekBar.secondaryProgress = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReady(youTubePlayer: YouTubePlayer) {}
|
||||||
|
override fun onVideoId(youTubePlayer: YouTubePlayer, videoId: String) {}
|
||||||
|
override fun onApiChange(youTubePlayer: YouTubePlayer) {}
|
||||||
|
override fun onPlaybackQualityChange(
|
||||||
|
youTubePlayer: YouTubePlayer,
|
||||||
|
playbackQuality: PlayerConstants.PlaybackQuality
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlaybackRateChange(
|
||||||
|
youTubePlayer: YouTubePlayer,
|
||||||
|
playbackRate: PlayerConstants.PlaybackRate
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(youTubePlayer: YouTubePlayer, error: PlayerConstants.PlayerError) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface YouTubePlayerSeekBarListener {
|
||||||
|
fun seekTo(time: Float)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<public />
|
||||||
|
|
||||||
|
<public name="YouTubePlayerSeekBar_fontSize" type="attr" />
|
||||||
|
<public name="YouTubePlayerSeekBar_color" type="attr" />
|
||||||
|
|
||||||
|
<public name="YouTubePlayerView_enableAutomaticInitialization" type="attr" />
|
||||||
|
<public name="YouTubePlayerView_videoId" type="attr" />
|
||||||
|
<public name="YouTubePlayerView_autoPlay" type="attr" />
|
||||||
|
<public name="YouTubePlayerView_handleNetworkEvents" type="attr" />
|
||||||
|
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<ripple
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:color="@color/ayp_item_selected">
|
||||||
|
</ripple>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_pressed="true" android:drawable="@color/ayp_item_selected" />
|
||||||
|
<item android:state_pressed="false" android:drawable="@android:color/transparent" />
|
||||||
|
</selector>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<gradient
|
||||||
|
android:startColor="@android:color/transparent"
|
||||||
|
android:endColor="@color/ayp_drop_shadow"
|
||||||
|
android:angle="270"/>
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<gradient
|
||||||
|
android:startColor="@android:color/transparent"
|
||||||
|
android:endColor="@color/ayp_drop_shadow"
|
||||||
|
android:angle="90"/>
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:width="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#FFF" android:pathData="M5,5H10V7H7V10H5V5M14,5H19V10H17V7H14V5M17,14H19V19H14V17H17V14M10,17V19H5V14H7V17H10Z" />
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:width="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#FFF" android:pathData="M14,14H19V16H16V19H14V14M5,14H10V19H8V16H5V14M8,5H10V10H5V8H8V5M19,8V10H14V5H16V8H19Z" />
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFF"
|
||||||
|
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<vector android:height="36dp" android:viewportHeight="24.0"
|
||||||
|
android:viewportWidth="24.0" android:width="36dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#fff" android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="36dp"
|
||||||
|
android:height="36dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFF"
|
||||||
|
android:pathData="M8,5v14l11,-7z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:width="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#FFF" android:pathData="M10,16.5V7.5L16,12M20,4.4C19.4,4.2 15.7,4 12,4C8.3,4 4.6,4.19 4,4.38C2.44,4.9 2,8.4 2,12C2,15.59 2.44,19.1 4,19.61C4.6,19.81 8.3,20 12,20C15.7,20 19.4,19.81 20,19.61C21.56,19.1 22,15.59 22,12C22,8.4 21.56,4.91 20,4.4Z" />
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="@android:color/white" />
|
||||||
|
<corners android:radius="2dp" />
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,201 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/panel"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:background="@android:color/black" />
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/controls_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@android:color/transparent" >
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/drop_shadow_top"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:background="@drawable/ayp_drop_shadow_top" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/drop_shadow_bottom"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:background="@drawable/ayp_drop_shadow_bottom" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/video_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text=""
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:lines="1"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_toStartOf="@+id/extra_views_container" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/extra_views_container"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
|
||||||
|
android:padding="8dp"
|
||||||
|
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:layout_alignParentEnd="true" >
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/menu_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ayp_ic_menu_24dp"
|
||||||
|
|
||||||
|
android:visibility="gone"
|
||||||
|
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:background="@drawable/ayp_background_item_selected"
|
||||||
|
|
||||||
|
android:contentDescription="@string/ayp_open_video_in_youtube" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/play_pause_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ayp_ic_play_36dp"
|
||||||
|
|
||||||
|
android:visibility="invisible"
|
||||||
|
|
||||||
|
android:padding="8dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:background="@drawable/ayp_background_item_selected"
|
||||||
|
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:contentDescription="@string/ayp_play_button" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerInParent="true" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_alignParentEnd="true" >
|
||||||
|
|
||||||
|
<com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.views.YouTubePlayerSeekBar
|
||||||
|
android:id="@+id/youtube_player_seekbar"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
|
||||||
|
app:fontSize="12sp"
|
||||||
|
app:color="@color/ayp_red"
|
||||||
|
|
||||||
|
android:maxHeight="100dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/live_video_indicator"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/ayp_live"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="12sp"
|
||||||
|
|
||||||
|
android:padding="8dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/youtube_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ayp_ic_youtube_24dp"
|
||||||
|
android:padding="8dp"
|
||||||
|
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:background="@drawable/ayp_background_item_selected"
|
||||||
|
|
||||||
|
android:contentDescription="@string/ayp_open_video_in_youtube" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/fullscreen_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ayp_ic_fullscreen_24dp"
|
||||||
|
android:padding="8dp"
|
||||||
|
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:background="@drawable/ayp_background_item_selected"
|
||||||
|
|
||||||
|
android:contentDescription="@string/ayp_full_screen_button" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/custom_action_left_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ayp_ic_play_36dp"
|
||||||
|
|
||||||
|
android:visibility="gone"
|
||||||
|
|
||||||
|
android:padding="8dp"
|
||||||
|
android:layout_margin="32dp"
|
||||||
|
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:background="@drawable/ayp_background_item_selected"
|
||||||
|
android:contentDescription="@string/ayp_custom_action_left"
|
||||||
|
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_toStartOf="@+id/play_pause_button" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/custom_action_right_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ayp_ic_play_36dp"
|
||||||
|
|
||||||
|
android:visibility="gone"
|
||||||
|
|
||||||
|
android:padding="8dp"
|
||||||
|
android:layout_margin="32dp"
|
||||||
|
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:background="@drawable/ayp_background_item_selected"
|
||||||
|
|
||||||
|
android:contentDescription="@string/ayp_custom_action_right"
|
||||||
|
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_toEndOf="@+id/play_pause_button" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="@dimen/ayp_menu_item_padding"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true" >
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
|
||||||
|
android:textSize="@dimen/ayp_menu_item_text_size"
|
||||||
|
android:textColor="@color/ayp_menu_text"
|
||||||
|
|
||||||
|
android:drawablePadding="12dp"
|
||||||
|
android:gravity="center" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/menu_root"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/ayp_menu_dialog_container_margin"
|
||||||
|
android:background="@drawable/ayp_shape_rounded_corners"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<declare-styleable name="YouTubePlayerSeekBar">
|
||||||
|
<attr name="fontSize" format="dimension" />
|
||||||
|
<attr name="color" format="color" />
|
||||||
|
</declare-styleable>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ayp_red">#F44336</color>
|
||||||
|
<color name="ayp_item_selected">#90FFFFFF</color>
|
||||||
|
<color name="ayp_drop_shadow">#66000000</color>
|
||||||
|
|
||||||
|
<color name="ayp_menu_icons">#757575</color>
|
||||||
|
<color name="ayp_menu_text">#222222</color>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<dimen name="ayp_8dp">8dp</dimen>
|
||||||
|
<dimen name="ayp_12sp">12sp</dimen>
|
||||||
|
<dimen name="ayp_menu_dialog_container_margin">8dp</dimen>
|
||||||
|
<dimen name="ayp_menu_item_padding">12dp</dimen>
|
||||||
|
<dimen name="ayp_menu_item_text_size">16sp</dimen>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<resources>
|
||||||
|
<string name="ayp_play_button">Play button</string>
|
||||||
|
<string name="ayp_null_time">0:00</string>
|
||||||
|
<string name="ayp_open_video_in_youtube">Open video in YouTube</string>
|
||||||
|
<string name="ayp_full_screen_button">Full screen button</string>
|
||||||
|
<string name="ayp_live">LIVE</string>
|
||||||
|
<string name="ayp_custom_action_left">YouTube player Custom action left</string>
|
||||||
|
<string name="ayp_custom_action_right">YouTube player custom action right</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
ext.versions = [
|
||||||
|
// Project
|
||||||
|
minSdk : 21,
|
||||||
|
compileSdk : 36,
|
||||||
|
|
||||||
|
publishVersion_core : '13.0.0',
|
||||||
|
publishVersionCode_core : 22,
|
||||||
|
|
||||||
|
publishVersion_chromecast : '0.32',
|
||||||
|
publishVersionCode_chromecast : 16,
|
||||||
|
|
||||||
|
// Plugins
|
||||||
|
gradlePlugin : '8.10.1',
|
||||||
|
dexCount : '4.0.0',
|
||||||
|
gradleNexus : '2.0.0',
|
||||||
|
dokka : '1.8.10',
|
||||||
|
|
||||||
|
// Kotlin
|
||||||
|
kotlin : '2.1.0',
|
||||||
|
|
||||||
|
// Compose
|
||||||
|
composeBom : '2025.10.01',
|
||||||
|
|
||||||
|
// AndroidX
|
||||||
|
androidxCore : '1.17.0',
|
||||||
|
appcompat : '1.7.1',
|
||||||
|
androidxAnnotations : '1.6.0',
|
||||||
|
androidxConstraintLayout : '2.2.1',
|
||||||
|
androidxRecyclerView : '1.4.0',
|
||||||
|
androidxMediarouter : '1.8.1',
|
||||||
|
androidxLifecycleRuntime : '2.9.4',
|
||||||
|
androidxActivityCompose : '1.11.0',
|
||||||
|
|
||||||
|
// Google Play
|
||||||
|
googlePlayServicesCastFramework : '21.3.0',
|
||||||
|
|
||||||
|
// psoffritti
|
||||||
|
sampleAppTemplate : 'v1.0.4',
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
junit : '4.13.2',
|
||||||
|
runner : '1.7.0',
|
||||||
|
espressoCore : '3.7.0',
|
||||||
|
|
||||||
|
// Debug
|
||||||
|
leakcanary : '2.10',
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
include(":youtube:core")
|
||||||
|
include(":youtube:custom-ui")
|
||||||
|
|
||||||
Loading…
Reference in New Issue