youtube module
This commit is contained in:
parent
8b1add722e
commit
be16d57958
|
|
@ -70,6 +70,10 @@ dependencies {
|
|||
implementation(project(":core:architecture"))
|
||||
//api(project(":core:architecture-reflect"))
|
||||
implementation(project(":core:network"))
|
||||
|
||||
implementation(project(":youtube:core"))
|
||||
implementation(project(":youtube:custom-ui"))
|
||||
|
||||
implementation libs.androidx.navigation.fragment.ktx
|
||||
implementation(libs.startup)
|
||||
implementation(libs.hilt.android)
|
||||
|
|
@ -80,8 +84,6 @@ dependencies {
|
|||
implementation(libs.protobuf.kotlin.lite)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
||||
implementation 'com.pierfrancescosoffritti.androidyoutubeplayer:core:13.0.0'
|
||||
implementation 'com.pierfrancescosoffritti.androidyoutubeplayer:custom-ui:13.0.0'
|
||||
implementation(libs.glide) // ImageLoader在用
|
||||
implementation(libs.okhttp.logging)
|
||||
implementation(libs.retrofit)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ android {
|
|||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,7 @@ dependencyResolutionManagement {
|
|||
rootProject.name = "VidiDin-android"
|
||||
|
||||
apply from: "./core/core.includes.gradle"
|
||||
apply from: "./youtube/youtube.includes.gradle"
|
||||
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