diff --git a/build.gradle.kts b/build.gradle.kts index 4b37ac619..7c0e4a1a6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,6 +4,7 @@ */ import io.gitlab.arturbosch.detekt.Detekt // Top-level build file where you can add configuration options common to all sub-projects/modules. +@Suppress("DSL_SCOPE_VIOLATION") // TODO Remove once KTIJ-19369 is fixed plugins { // known bug for libs : https://developer.android.com/studio/preview/features#gradle-version-catalogs-known-issues alias(libs.plugins.android.application) apply false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0e96f8f41..9a650a341 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,6 +29,11 @@ tagCommanderServerSide = "5.5.0" # ComScore : https://github.com/comScore/ComScore-Android comscore = "6.10.0" androidxComposeBom = "2023.08.00" +leanback = "1.0.0" +tvCompose = "1.0.0-alpha09" +androidx-test-ext-junit = "1.1.5" +appcompat = "1.6.1" +material = "1.9.0" [libraries] accompanist-navigation-material = { module = "com.google.accompanist:accompanist-navigation-material", version.ref = "accompanist" } @@ -37,6 +42,8 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose", ver androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "pagingCompose" } +androidx-tv-foundation = { module = "androidx.tv:tv-foundation", version.ref = "tvCompose" } +androidx-tv-material = { module = "androidx.tv:tv-material", version.ref = "tvCompose" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorVersion" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktorVersion" } @@ -61,6 +68,7 @@ kotlinx-coroutines-guava = { group = "org.jetbrains.kotlinx", name = "kotlinx-co kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" } androidx-media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" } +androidx-media3-ui-leanback = { group = "androidx.media3", name = "media3-ui-leanback", version.ref = "media3" } androidx-media3-dash = { group = "androidx.media3", name = "media3-exoplayer-dash", version.ref = "media3" } androidx-media3-hls = { group = "androidx.media3", name = "media3-exoplayer-hls", version.ref = "media3" } androidx-media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3" } @@ -80,6 +88,10 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } +leanback = { group = "androidx.leanback", name = "leanback", version.ref = "leanback" } +androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } +appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } diff --git a/pillarbox-analytics/build.gradle.kts b/pillarbox-analytics/build.gradle.kts index d9c078a73..0c854402e 100644 --- a/pillarbox-analytics/build.gradle.kts +++ b/pillarbox-analytics/build.gradle.kts @@ -2,6 +2,7 @@ * Copyright (c) 2022. SRG SSR. All rights reserved. * License information is available from the LICENSE file. */ +@Suppress("DSL_SCOPE_VIOLATION") // TODO Remove once KTIJ-19369 is fixed plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) diff --git a/pillarbox-core-business/build.gradle.kts b/pillarbox-core-business/build.gradle.kts index 593b5dea0..1baed4f74 100644 --- a/pillarbox-core-business/build.gradle.kts +++ b/pillarbox-core-business/build.gradle.kts @@ -2,6 +2,7 @@ * Copyright (c) 2022. SRG SSR. All rights reserved. * License information is available from the LICENSE file. */ +@Suppress("DSL_SCOPE_VIOLATION") // TODO Remove once KTIJ-19369 is fixed plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) diff --git a/pillarbox-demo-shared/.gitignore b/pillarbox-demo-shared/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/pillarbox-demo-shared/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/pillarbox-demo-shared/build.gradle.kts b/pillarbox-demo-shared/build.gradle.kts new file mode 100644 index 000000000..1a781ce1f --- /dev/null +++ b/pillarbox-demo-shared/build.gradle.kts @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023. SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +@Suppress("DSL_SCOPE_VIOLATION") // TODO Remove once KTIJ-19369 is fixed +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "ch.srgssr.pillarbox.demo.shared" + compileSdk = AppConfig.compileSdk + + defaultConfig { + minSdk = AppConfig.minSdk + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + compileOnly(project(mapOf("path" to ":pillarbox-core-business"))) + implementation(libs.androidx.ktx) +} diff --git a/pillarbox-demo-shared/consumer-rules.pro b/pillarbox-demo-shared/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/pillarbox-demo-shared/proguard-rules.pro b/pillarbox-demo-shared/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/pillarbox-demo-shared/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/pillarbox-demo-shared/src/main/AndroidManifest.xml b/pillarbox-demo-shared/src/main/AndroidManifest.xml new file mode 100644 index 000000000..09709e5c1 --- /dev/null +++ b/pillarbox-demo-shared/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/data/DemoBrowser.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/DemoBrowser.kt similarity index 96% rename from pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/data/DemoBrowser.kt rename to pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/DemoBrowser.kt index 1bade0629..d29957fe7 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/data/DemoBrowser.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/DemoBrowser.kt @@ -1,8 +1,8 @@ /* - * Copyright (c) 2022. SRG SSR. All rights reserved. + * Copyright (c) 2023. SRG SSR. All rights reserved. * License information is available from the LICENSE file. */ -package ch.srgssr.pillarbox.demo.data +package ch.srgssr.pillarbox.demo.shared.data import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/data/DemoItem.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/DemoItem.kt similarity index 99% rename from pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/data/DemoItem.kt rename to pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/DemoItem.kt index eefe86d95..0bf3ac259 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/data/DemoItem.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/DemoItem.kt @@ -1,10 +1,10 @@ /* - * Copyright (c) 2022. SRG SSR. All rights reserved. + * Copyright (c) 2023. SRG SSR. All rights reserved. * License information is available from the LICENSE file. */ @file:Suppress("MaximumLineLength", "MaxLineLength") -package ch.srgssr.pillarbox.demo.data +package ch.srgssr.pillarbox.demo.shared.data import android.net.Uri import androidx.media3.common.C diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/data/MixedMediaItemSource.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/MixedMediaItemSource.kt similarity index 91% rename from pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/data/MixedMediaItemSource.kt rename to pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/MixedMediaItemSource.kt index c404c9d17..aaf3d9dc8 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/data/MixedMediaItemSource.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/MixedMediaItemSource.kt @@ -1,8 +1,8 @@ /* - * Copyright (c) 2022. SRG SSR. All rights reserved. + * Copyright (c) 2023. SRG SSR. All rights reserved. * License information is available from the LICENSE file. */ -package ch.srgssr.pillarbox.demo.data +package ch.srgssr.pillarbox.demo.shared.data import androidx.media3.common.MediaItem import ch.srgssr.pillarbox.core.business.MediaCompositionMediaItemSource diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/data/Playlist.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/Playlist.kt similarity index 99% rename from pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/data/Playlist.kt rename to pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/Playlist.kt index 60bdfc8d2..a7a3c2019 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/data/Playlist.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/Playlist.kt @@ -1,8 +1,8 @@ /* - * Copyright (c) 2022. SRG SSR. All rights reserved. + * Copyright (c) 2023. SRG SSR. All rights reserved. * License information is available from the LICENSE file. */ -package ch.srgssr.pillarbox.demo.data +package ch.srgssr.pillarbox.demo.shared.data import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/di/PlayerModule.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/di/PlayerModule.kt similarity index 94% rename from pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/di/PlayerModule.kt rename to pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/di/PlayerModule.kt index da0d68b0f..1a7794665 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/di/PlayerModule.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/di/PlayerModule.kt @@ -2,7 +2,7 @@ * Copyright (c) 2023. SRG SSR. All rights reserved. * License information is available from the LICENSE file. */ -package ch.srgssr.pillarbox.demo.di +package ch.srgssr.pillarbox.demo.shared.di import android.content.Context import androidx.media3.exoplayer.source.DefaultMediaSourceFactory @@ -12,7 +12,7 @@ import ch.srgssr.pillarbox.core.business.akamai.AkamaiTokenDataSource import ch.srgssr.pillarbox.core.business.integrationlayer.service.DefaultMediaCompositionDataSource import ch.srgssr.pillarbox.core.business.integrationlayer.service.Vector.getVector import ch.srgssr.pillarbox.core.business.tracker.DefaultMediaItemTrackerRepository -import ch.srgssr.pillarbox.demo.data.MixedMediaItemSource +import ch.srgssr.pillarbox.demo.shared.data.MixedMediaItemSource import ch.srgssr.pillarbox.player.PillarboxPlayer import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory diff --git a/pillarbox-demo-tv/.gitignore b/pillarbox-demo-tv/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/pillarbox-demo-tv/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/pillarbox-demo-tv/build.gradle.kts b/pillarbox-demo-tv/build.gradle.kts new file mode 100644 index 000000000..fe0bbad6b --- /dev/null +++ b/pillarbox-demo-tv/build.gradle.kts @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023. SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +@Suppress("DSL_SCOPE_VIOLATION") // TODO Remove once KTIJ-19369 is fixed +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "ch.srgssr.pillarbox.demo.tv" + compileSdk = AppConfig.compileSdk + + defaultConfig { + applicationId = "ch.srgssr.pillarbox.demo.tv" + minSdk = AppConfig.minSdk + targetSdk = AppConfig.targetSdk + versionCode = VersionConfig.versionCode() + versionName = VersionConfig.versionName() + } + + signingConfigs { + create("release") { + val password = System.getenv("DEMO_KEY_PASSWORD") ?: extra.properties["pillarbox.keystore.password"] as String? + storeFile = file("./demo.keystore") + storePassword = password + keyAlias = "demo" + keyPassword = password + } + } + + buildTypes { + debug { + applicationIdSuffix = ".debug" + versionNameSuffix = "-debug" + } + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = AppConfig.composeCompiler + } +} + +dependencies { + implementation(project(mapOf("path" to ":pillarbox-core-business"))) + implementation(project(mapOf("path" to ":pillarbox-analytics"))) + implementation(project(mapOf("path" to ":pillarbox-ui"))) + implementation(project(mapOf("path" to ":pillarbox-demo-shared"))) + implementation(libs.androidx.ktx) + implementation(libs.leanback) + + val composeBom = libs.androidx.compose.bom + implementation(platform(composeBom)) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.compose.material.icons.extended) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.tooling.preview) + + // Compose for TV dependencies + implementation(libs.androidx.tv.foundation) + implementation(libs.androidx.tv.material) + implementation(libs.androidx.media3.ui.leanback) +} diff --git a/pillarbox-demo-tv/proguard-rules.pro b/pillarbox-demo-tv/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/pillarbox-demo-tv/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/pillarbox-demo-tv/src/main/AndroidManifest.xml b/pillarbox-demo-tv/src/main/AndroidManifest.xml new file mode 100644 index 000000000..bd6184d16 --- /dev/null +++ b/pillarbox-demo-tv/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/DemoTvApplication.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/DemoTvApplication.kt new file mode 100644 index 000000000..cb5489b96 --- /dev/null +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/DemoTvApplication.kt @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2023. SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.demo.tv + +import android.app.Application + +/** + * Demo tv application + * + * @constructor Create empty Demo tv application + */ +class DemoTvApplication : Application() diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/MainActivity.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/MainActivity.kt new file mode 100644 index 000000000..81ad7af41 --- /dev/null +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/MainActivity.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023. SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +@file:OptIn(ExperimentalTvMaterial3Api::class) + +package ch.srgssr.pillarbox.demo.tv + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.unit.dp +import androidx.tv.material3.Border +import androidx.tv.material3.ExperimentalTvMaterial3Api +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Surface +import ch.srgssr.pillarbox.demo.shared.data.DemoItem +import ch.srgssr.pillarbox.demo.tv.examples.ExamplesHome +import ch.srgssr.pillarbox.demo.tv.player.PlayerActivity + +/** + * Main activity + * + * @constructor Create empty Main activity + */ +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + MaterialTheme { + Surface( + shape = RectangleShape, + border = Border.None + ) { + MainView( + modifier = Modifier + .fillMaxSize() + .padding( + start = HorizontalPadding, + end = HorizontalPadding, + top = VerticalPadding, + ), + this@MainActivity::openPlayer + ) + } + } + } + } + + private fun openPlayer(item: DemoItem) { + PlayerActivity.startPlayer(this, item) + } + + @Composable + private fun MainView( + modifier: Modifier = Modifier, + onItemSelected: (DemoItem) -> Unit + ) { + ExamplesHome(modifier = modifier, onItemSelected = onItemSelected) + } + + companion object { + private val HorizontalPadding = 32.dp + private val VerticalPadding = 16.dp + } +} diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/examples/Examples.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/examples/Examples.kt new file mode 100644 index 000000000..12d7376f4 --- /dev/null +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/examples/Examples.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2023. SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.demo.tv.examples + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.tv.foundation.ExperimentalTvFoundationApi +import androidx.tv.foundation.PivotOffsets +import androidx.tv.foundation.lazy.list.TvLazyColumn +import androidx.tv.foundation.lazy.list.items +import androidx.tv.material3.ExperimentalTvMaterial3Api +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Surface +import androidx.tv.material3.Text +import ch.srgssr.pillarbox.demo.shared.data.DemoItem +import ch.srgssr.pillarbox.demo.shared.data.Playlist +import ch.srgssr.pillarbox.demo.tv.item.DemoItemView +import ch.srgssr.pillarbox.demo.tv.item.PlaylistHeader + +/** + * Examples home + * + * @param modifier + * @param onItemSelected + * @receiver + */ +@OptIn(ExperimentalTvFoundationApi::class, ExperimentalTvMaterial3Api::class) +@Composable +fun ExamplesHome( + modifier: Modifier = Modifier, + onItemSelected: (DemoItem) -> Unit = {}, +) { + val listItems = remember { + listOf( + Playlist.StreamUrls, + Playlist.StreamUrns, + Playlist.PlaySuisseStreams, + Playlist.StreamApples, + Playlist.StreamGoogles, + Playlist.BitmovinSamples, + Playlist.UnifiedStreaming, + Playlist.UnifiedStreamingDash, + ) + } + TvLazyColumn( + modifier = modifier, + pivotOffsets = PivotOffsets(0.5f, 0f), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + stickyHeader { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 12.dp) + .background(color = MaterialTheme.colorScheme.surface) + ) { + Text(text = "Samples", style = MaterialTheme.typography.displayMedium) + } + } + for (playlist in listItems) { + item { + PlaylistHeader( + modifier = Modifier.padding(vertical = 6.dp), + title = playlist.title + ) + } + items(playlist.items) { item -> + DemoItemView( + modifier = Modifier.fillMaxWidth(), + title = item.title, + subtitle = item.description, + onClick = { onItemSelected(item) } + ) + } + } + } +} + +@OptIn(ExperimentalTvMaterial3Api::class) +@Preview +@Composable +private fun ExamplesPreview() { + MaterialTheme() { + Surface { + ExamplesHome() + } + } +} diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/item/DemoItemView.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/item/DemoItemView.kt new file mode 100644 index 000000000..6e0a8e1e4 --- /dev/null +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/item/DemoItemView.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2023. SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.demo.tv.item + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.tv.material3.Card +import androidx.tv.material3.CardScale +import androidx.tv.material3.ExperimentalTvMaterial3Api +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Text + +/** + * Demo item view + * + * @param title + * @param onClick + * @param modifier + * @param subtitle + * @receiver + */ +@OptIn(ExperimentalTvMaterial3Api::class) +@Composable +fun DemoItemView( + title: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + subtitle: String? = null +) { + Card( + modifier = modifier, + onClick = onClick, + scale = CardScale.None + ) { + Column( + modifier = Modifier.padding(8.dp), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + modifier = Modifier, + text = title, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleLarge, + ) + subtitle?.let { + Text( + modifier = Modifier, + text = subtitle, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.bodyLarge, + ) + } + } + } +} + +@OptIn(ExperimentalTvMaterial3Api::class) +@Preview +@Composable +private fun DemoItemPreview() { + MaterialTheme { + Column( + modifier = Modifier + .width(400.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + val itemModifier = Modifier + .fillMaxWidth() + DemoItemView(modifier = itemModifier, title = "Title 1", subtitle = "Subtitle 1", onClick = { }) + DemoItemView(modifier = itemModifier, title = "Title 2", subtitle = null, onClick = {}) + DemoItemView(modifier = itemModifier, title = "Title 3", subtitle = "Subtitle 3", onClick = { }) + } + } +} diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/item/PlaylistHeader.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/item/PlaylistHeader.kt new file mode 100644 index 000000000..d6dc24701 --- /dev/null +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/item/PlaylistHeader.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023. SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.demo.tv.item + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.tv.material3.ExperimentalTvMaterial3Api +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Text + +/** + * Playlist header + * + * @param title The title of the playlist. + * @param modifier + */ +@OptIn(ExperimentalTvMaterial3Api::class) +@Composable +fun PlaylistHeader(title: String, modifier: Modifier = Modifier) { + Text( + modifier = modifier, + text = title, + style = MaterialTheme.typography.headlineSmall, + ) +} diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/PlayerActivity.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/PlayerActivity.kt new file mode 100644 index 000000000..a0f50d762 --- /dev/null +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/PlayerActivity.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2023. SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.demo.tv.player + +import android.app.Activity +import android.content.Intent +import android.os.Build +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier +import androidx.media3.session.MediaSession +import androidx.tv.material3.ExperimentalTvMaterial3Api +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Surface +import ch.srgssr.pillarbox.demo.shared.data.DemoItem +import ch.srgssr.pillarbox.demo.shared.di.PlayerModule +import ch.srgssr.pillarbox.demo.tv.player.compose.TvPlayerView +import ch.srgssr.pillarbox.player.PillarboxPlayer + +/** + * Player activity + * + * @constructor Create empty Player activity + */ +class PlayerActivity : ComponentActivity() { + private lateinit var player: PillarboxPlayer + private lateinit var mediaSession: MediaSession + + @OptIn(ExperimentalTvMaterial3Api::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + player = PlayerModule.provideDefaultPlayer(this) + mediaSession = MediaSession.Builder(this, player) + .build() + val demoItem = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getSerializableExtra(ARG_ITEM, DemoItem::class.java) + } else { + intent.getSerializableExtra(ARG_ITEM) as DemoItem? + } + demoItem?.let { + player.setMediaItem(it.toMediaItem()) + } + player.apply { + player.prepare() + player.trackingEnabled = false + player.playWhenReady = true + } + + setContent { + MaterialTheme { + Surface(modifier = Modifier.fillMaxSize()) { + TvPlayerView(player = player) + } + } + } + } + + override fun onResume() { + super.onResume() + player.play() + } + + override fun onPause() { + super.onPause() + player.pause() + } + + override fun onDestroy() { + super.onDestroy() + mediaSession.release() + player.stop() + player.release() + } + + companion object { + private const val ARG_ITEM = "demo_item" + + /** + * Start player with Leanback fragment. + * + * @param context + * @param demoItem The item to play. + */ + fun startPlayer(context: Activity, demoItem: DemoItem) { + val intent = Intent(context, PlayerActivity::class.java) + intent.putExtra(ARG_ITEM, demoItem) + context.startActivity(intent) + } + } +} diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/TvPlaybackRow.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/TvPlaybackRow.kt new file mode 100644 index 000000000..593d3eb98 --- /dev/null +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/TvPlaybackRow.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2023. SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.demo.tv.player.compose + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.FastForward +import androidx.compose.material.icons.filled.FastRewind +import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.SkipNext +import androidx.compose.material.icons.filled.SkipPrevious +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.unit.dp +import androidx.media3.common.Player +import androidx.tv.material3.ExperimentalTvMaterial3Api +import androidx.tv.material3.Icon +import androidx.tv.material3.IconButton +import ch.srgssr.pillarbox.player.canSeekBack +import ch.srgssr.pillarbox.player.canSeekForward +import ch.srgssr.pillarbox.player.canSeekToNext +import ch.srgssr.pillarbox.player.canSeekToPrevious +import ch.srgssr.pillarbox.ui.availableCommandsAsState +import ch.srgssr.pillarbox.ui.isPlayingAsState + +/** + * Tv playback row + * + * @param player + * @param modifier + */ +@OptIn(ExperimentalTvMaterial3Api::class) +@Composable +fun TvPlaybackRow( + player: Player, + modifier: Modifier = Modifier, +) { + val isPlaying = player.isPlayingAsState() + val focusRequester = remember { + FocusRequester() + } + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + IconButton( + modifier = Modifier, + enabled = player.availableCommandsAsState().canSeekToPrevious(), + onClick = { + player.seekToPrevious() + }, + ) { + Icon(imageVector = Icons.Default.SkipPrevious, contentDescription = null) + } + + IconButton( + modifier = Modifier, + enabled = player.availableCommandsAsState().canSeekBack(), + onClick = { + player.seekBack() + }, + ) { + Icon(imageVector = Icons.Default.FastRewind, contentDescription = null) + } + + IconButton( + modifier = Modifier + .focusRequester(focusRequester), + onClick = { + player.playWhenReady = !player.playWhenReady + }, + ) { + if (isPlaying) { + Icon(imageVector = Icons.Default.Pause, contentDescription = null) + } else { + Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) + } + } + + IconButton( + modifier = Modifier, + enabled = player.availableCommandsAsState().canSeekForward(), + onClick = { + player.seekForward() + }, + ) { + Icon(imageVector = Icons.Default.FastForward, contentDescription = null) + } + + IconButton( + modifier = Modifier, + enabled = player.availableCommandsAsState().canSeekToNext(), + onClick = { + player.seekToNext() + }, + ) { + Icon(imageVector = Icons.Default.SkipNext, contentDescription = null) + } + } +} diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/TvPlayerView.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/TvPlayerView.kt new file mode 100644 index 000000000..8c9f3cc3b --- /dev/null +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/TvPlayerView.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023. SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.demo.tv.player.compose + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.media3.common.Player +import ch.srgssr.pillarbox.ui.PlayerSurface +import ch.srgssr.pillarbox.ui.ToggleView +import ch.srgssr.pillarbox.ui.rememberToggleState +import kotlin.time.Duration.Companion.seconds + +/** + * Tv player view + * + * @param player + * @param modifier + */ +@Composable +fun TvPlayerView( + player: Player, + modifier: Modifier = Modifier +) { + Box(modifier = modifier) { + PlayerSurface( + player = player, + modifier = Modifier.fillMaxSize() + ) { + val toggleState = rememberToggleState(visible = true, duration = 4.seconds) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxSize() + .clickable { + toggleState.toggleVisible() + } + ) { + ToggleView(toggleState = toggleState) { + TvPlaybackRow( + player = player, + modifier = Modifier.matchParentSize() + ) + } + } + } + } +} diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/leanback/LeanbackPlayerActivity.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/leanback/LeanbackPlayerActivity.kt new file mode 100644 index 000000000..0fe368683 --- /dev/null +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/leanback/LeanbackPlayerActivity.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023. SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.demo.tv.player.leanback + +import android.app.Activity +import android.content.Intent +import android.os.Build +import android.os.Bundle +import androidx.fragment.app.FragmentActivity +import ch.srgssr.pillarbox.demo.shared.data.DemoItem +import ch.srgssr.pillarbox.demo.tv.R + +/** + * Player activity using Android Leanback. + * + * Leanback is no more update by google and very complicated to implement. + * This demo just show how to integrate Leanback with Pillarbox. + */ +class LeanbackPlayerActivity : FragmentActivity() { + + private lateinit var leanbackPlayerFragment: LeanbackPlayerFragment + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_player) + leanbackPlayerFragment = supportFragmentManager.findFragmentById(R.id.fragment_container) as LeanbackPlayerFragment + val demoItem = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getSerializableExtra(ARG_ITEM, DemoItem::class.java) + } else { + intent.getSerializableExtra(ARG_ITEM) as DemoItem? + } + demoItem?.let { + leanbackPlayerFragment.setDemoItem(it) + } + } + + companion object { + private const val ARG_ITEM = "demo_item" + + /** + * Start player with Leanback fragment. + * + * @param context + * @param demoItem The item to play. + */ + fun startPlayer(context: Activity, demoItem: DemoItem) { + val intent = Intent(context, LeanbackPlayerActivity::class.java) + intent.putExtra(ARG_ITEM, demoItem) + context.startActivity(intent) + } + } +} diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/leanback/LeanbackPlayerFragment.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/leanback/LeanbackPlayerFragment.kt new file mode 100644 index 000000000..e019418db --- /dev/null +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/leanback/LeanbackPlayerFragment.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2023. SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.demo.tv.player.leanback + +import android.os.Bundle +import android.util.Log +import androidx.leanback.app.VideoSupportFragment +import androidx.leanback.app.VideoSupportFragmentGlueHost +import androidx.leanback.media.PlaybackGlue +import androidx.leanback.media.PlaybackTransportControlGlue +import androidx.leanback.widget.PlaybackSeekDataProvider +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.media3.common.Player +import androidx.media3.ui.leanback.LeanbackPlayerAdapter +import ch.srgssr.pillarbox.core.business.SRGErrorMessageProvider +import ch.srgssr.pillarbox.demo.shared.data.DemoItem +import ch.srgssr.pillarbox.demo.shared.di.PlayerModule +import ch.srgssr.pillarbox.player.PillarboxPlayer +import ch.srgssr.pillarbox.player.currentMediaMetadataAsFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +private const val UpdateInterval = 1_000 + +/** + * Leanback player fragment + * + * A simple leanback player sample. + * Lot of work is still needed to have a good player experience. + */ +class LeanbackPlayerFragment : VideoSupportFragment() { + private lateinit var player: PillarboxPlayer + + /** + * Set demo item to [PillarboxPlayer] + * + * @param demoItem + */ + fun setDemoItem(demoItem: DemoItem) { + player.setMediaItem(demoItem.toMediaItem()) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + player = PlayerModule.provideDefaultPlayer(requireContext()).apply { + prepare() + setHandleAudioFocus(true) + } + val playerGlue = PlaybackTransportControlGlue( + requireActivity(), + LeanbackPlayerAdapter( + requireActivity(), + player, + UpdateInterval + ).apply { + setErrorMessageProvider(SRGErrorMessageProvider(requireContext())) + } + ) + playerGlue.host = VideoSupportFragmentGlueHost(this) + playerGlue.addPlayerCallback(object : PlaybackGlue.PlayerCallback() { + override fun onPreparedStateChanged(glue: PlaybackGlue) { + if (glue.isPrepared) { + playerGlue.seekProvider = PlaybackSeekDataProvider() + playerGlue.play() + } + } + }) + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) { + player.currentMediaMetadataAsFlow().flowWithLifecycle(lifecycle).collectLatest { + playerGlue.subtitle = it.title + playerGlue.title = it.subtitle + } + } + } + } + + override fun onResume() { + super.onResume() + if (player.playerError == null || player.playbackState == Player.STATE_ENDED) { + player.seekToDefaultPosition() + player.prepare() + } + player.play() + } + + override fun onPause() { + Log.d("Coucou", "PlayerFragment:onPause") + super.onPause() + player.pause() + } + + override fun onDestroy() { + super.onDestroy() + Log.d("Coucou", "PlayerFragment:onDestroy") + player.release() + } +} diff --git a/pillarbox-demo-tv/src/main/res/layout/activity_player.xml b/pillarbox-demo-tv/src/main/res/layout/activity_player.xml new file mode 100644 index 000000000..19106601e --- /dev/null +++ b/pillarbox-demo-tv/src/main/res/layout/activity_player.xml @@ -0,0 +1,9 @@ + + diff --git a/pillarbox-demo-tv/src/main/res/mipmap-hdpi/ic_launcher.webp b/pillarbox-demo-tv/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 000000000..c209e78ec Binary files /dev/null and b/pillarbox-demo-tv/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/pillarbox-demo-tv/src/main/res/mipmap-mdpi/ic_launcher.webp b/pillarbox-demo-tv/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 000000000..4f0f1d64e Binary files /dev/null and b/pillarbox-demo-tv/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/pillarbox-demo-tv/src/main/res/mipmap-xhdpi/ic_launcher.webp b/pillarbox-demo-tv/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 000000000..948a3070f Binary files /dev/null and b/pillarbox-demo-tv/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/pillarbox-demo-tv/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/pillarbox-demo-tv/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 000000000..28d4b77f9 Binary files /dev/null and b/pillarbox-demo-tv/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/pillarbox-demo-tv/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/pillarbox-demo-tv/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 000000000..aa7d6427e Binary files /dev/null and b/pillarbox-demo-tv/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/pillarbox-demo-tv/src/main/res/values/strings.xml b/pillarbox-demo-tv/src/main/res/values/strings.xml new file mode 100644 index 000000000..c742e1224 --- /dev/null +++ b/pillarbox-demo-tv/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Pillarbox Demo Tv + \ No newline at end of file diff --git a/pillarbox-demo-tv/src/main/res/values/themes.xml b/pillarbox-demo-tv/src/main/res/values/themes.xml new file mode 100644 index 000000000..ee4d5bbf7 --- /dev/null +++ b/pillarbox-demo-tv/src/main/res/values/themes.xml @@ -0,0 +1,8 @@ + + + +