From a38d93a392cb5463c9ae6e81351e575a1c157c94 Mon Sep 17 00:00:00 2001 From: Nam Nguyen Hoai <1776230+eneim@users.noreply.github.com> Date: Sun, 21 Jun 2020 12:29:12 +0900 Subject: [PATCH] Nam/v1/sample tiktok (#82) * init tiktok demo * Fix a log type * Remove unused imports * Do not init cookie manager from the library * Remove default parameter values from KohiiExoPlayer * Include Playback value in the artwork hint callback * Minor change * Update changelog * Minor improvement * Support Playback locking * Minor improvement * Support Playback locking. * Updating to use buildFeatures * Update lock/unlock method * Update lock/unlock methods * Update readme and changelog * Add a convenient "get" method to the Capsule * Add base implementation for the ExoPlayerProvider * Add convenient Cache creators * Add default value to KohiiExoPlayer constructor * Update * Add global convenient creators for Kohii * Add `ExoPlayerConfig` for advance use cases. * Update * Update changelog * Update locking mechanism. * Update locking mechanism * Update navigation component version to 2.3.0 rc1 * Update TikTok demo * Update changelog * Update changelog * Docs: update the glossary * Minor fix * Remove unused methods * More power for the ExoPlayerConfig * Various changes for the ExoPlayerProvider construction. * Update document * Update document --- .gitignore | 4 + CHANGELOG.md | 9 + README.md | 12 + build.gradle | 3 + buildSrc/src/main/java/kohii/Dependencies.kt | 20 +- docs/advance-builder.md | 71 ++++++ docs/advance-manual-playback.md | 1 + .../memory-mode.md => advance-memory-mode.md} | 2 +- ...backs.md => advance-multiple-playbacks.md} | 6 +- ....md => advance-reuse-renderer-instance.md} | 4 +- docs/{usage/advance.md => advance-summary.md} | 18 +- ...renderer.md => advance-switch-renderer.md} | 2 +- .../thumbnail.md => advance-thumbnail.md} | 3 +- .../unique-tag.md => advance-unique-tag.md} | 0 docs/css/main.css | 4 +- .../custom_engine.md => custom-engine.md} | 0 docs/customize/terms.md | 37 --- docs/{usage => }/demos.md | 0 docs/{usage/start.md => getting-started.md} | 0 docs/glossary.md | 44 ++++ docs/terminology.md | 39 --- docs/{usage/basic.md => usage-basic.md} | 14 +- docs/usage/advance/builder.md | 27 -- docs/usage/advance/manual-playback.md | 1 - .../src/main/java/kohii/v1/x/Latte.kt | 8 +- .../src/main/java/kohii/v1/core/Bucket.kt | 20 +- .../src/main/java/kohii/v1/core/Engine.kt | 83 ++++++ .../src/main/java/kohii/v1/core/Group.kt | 18 +- .../src/main/java/kohii/v1/core/Manager.kt | 26 +- .../src/main/java/kohii/v1/core/Master.kt | 182 +++++++------ .../src/main/java/kohii/v1/core/Playback.kt | 39 ++- .../src/main/java/kohii/v1/utils/Capsule.kt | 2 + .../v1/exoplayer/DefaultExoPlayerProvider.kt | 77 ++---- .../DefaultMediaSourceFactoryProvider.kt | 18 ++ .../java/kohii/v1/exoplayer/ExoPlayerCache.kt | 85 +++++++ .../kohii/v1/exoplayer/ExoPlayerConfig.kt | 130 ++++++++++ .../src/main/java/kohii/v1/exoplayer/Kohii.kt | 119 ++++++++- .../java/kohii/v1/exoplayer/KohiiExoPlayer.kt | 18 +- ...hMeterFactory.kt => LoadControlFactory.kt} | 12 +- .../v1/exoplayer/PlayerViewPlayableCreator.kt | 20 +- .../v1/exoplayer/RecycledExoPlayerProvider.kt | 75 ++++++ .../v1/exoplayer/TrackSelectorFactory.kt | 32 +++ .../OfficialYouTubePlayerEngine.kt | 11 +- .../UnofficialYouTubePlayerEngine.kt | 11 +- kohii-sample-tiktok/.gitignore | 1 + kohii-sample-tiktok/build.gradle | 107 ++++++++ kohii-sample-tiktok/proguard-rules.pro | 21 ++ .../src/main/AndroidManifest.xml | 43 ++++ .../src/main/assets/caminandes.json | 239 ++++++++++++++++++ .../main/java/kohii/v1/sample/data/Data.kt | 51 ++++ .../java/kohii/v1/sample/data/Playlist.kt | 37 +++ .../main/java/kohii/v1/sample/data/Sources.kt | 29 +++ .../main/java/kohii/v1/sample/data/Tracks.kt | 27 ++ .../java/kohii/v1/sample/data/Variations.kt | 24 ++ .../main/java/kohii/v1/sample/data/Video.kt | 32 +++ .../kohii/v1/sample/tiktok/MainActivity.kt | 71 ++++++ .../java/kohii/v1/sample/tiktok/TikTokApp.kt | 41 +++ .../tiktok/ui/dashboard/DashboardFragment.kt | 47 ++++ .../tiktok/ui/dashboard/DashboardViewModel.kt | 29 +++ .../v1/sample/tiktok/ui/home/HomeFragment.kt | 62 +++++ .../v1/sample/tiktok/ui/home/HomeViewModel.kt | 29 +++ .../v1/sample/tiktok/ui/home/VideoAdapters.kt | 92 +++++++ .../sample/tiktok/ui/home/VideoViewHolder.kt | 42 +++ .../ui/notifications/NotificationsFragment.kt | 47 ++++ .../notifications/NotificationsViewModel.kt | 29 +++ .../drawable-v24/ic_launcher_foreground.xml | 46 ++++ .../res/drawable/ic_dashboard_black_24dp.xml | 25 ++ .../main/res/drawable/ic_home_black_24dp.xml | 25 ++ .../res/drawable/ic_launcher_background.xml | 186 ++++++++++++++ .../drawable/ic_notifications_black_24dp.xml | 25 ++ .../src/main/res/layout/activity_main.xml | 50 ++++ .../main/res/layout/fragment_dashboard.xml | 40 +++ .../src/main/res/layout/fragment_home.xml | 36 +++ .../res/layout/fragment_notifications.xml | 40 +++ .../main/res/layout/holder_vertical_video.xml | 54 ++++ .../src/main/res/menu/bottom_nav_menu.xml | 34 +++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 21 ++ .../mipmap-anydpi-v26/ic_launcher_round.xml | 21 ++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3593 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5339 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2636 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3388 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4926 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7472 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7909 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 11873 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10652 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 16570 bytes .../main/res/navigation/mobile_navigation.xml | 41 +++ .../src/main/res/values/colors.xml | 22 ++ .../src/main/res/values/dimens.xml | 21 ++ .../src/main/res/values/strings.xml | 22 ++ .../src/main/res/values/styles.xml | 23 ++ kohii-sample/build.gradle | 7 +- .../v1/sample/ui/combo/ExoVideoHolder.kt | 2 +- .../v1/sample/ui/echo/VideoItemHolder.kt | 1 + .../v1/sample/ui/fbook/vh/VideoViewHolder.kt | 1 + .../v1/sample/ui/grid/VideoViewHolder.kt | 1 + .../v1/sample/ui/overlay/VideoItemHolder.kt | 1 + .../v1/sample/ui/pagers/VideoViewHolder.kt | 1 + .../sample/ui/youtube1/YouTubeViewHolder.kt | 1 + .../sample/ui/youtube2/YouTubeViewHolder.kt | 1 + mkdocs.yml | 30 +-- settings.gradle | 2 +- 104 files changed, 2796 insertions(+), 391 deletions(-) create mode 100644 docs/advance-builder.md create mode 100644 docs/advance-manual-playback.md rename docs/{usage/advance/memory-mode.md => advance-memory-mode.md} (92%) rename docs/{usage/advance/multiple-playbacks.md => advance-multiple-playbacks.md} (56%) rename docs/{usage/advance/reuse-renderer-instance.md => advance-reuse-renderer-instance.md} (68%) rename docs/{usage/advance.md => advance-summary.md} (57%) rename docs/{usage/advance/switch-renderer.md => advance-switch-renderer.md} (75%) rename docs/{usage/advance/thumbnail.md => advance-thumbnail.md} (83%) rename docs/{usage/advance/unique-tag.md => advance-unique-tag.md} (100%) rename docs/{customize/custom_engine.md => custom-engine.md} (100%) delete mode 100644 docs/customize/terms.md rename docs/{usage => }/demos.md (100%) rename docs/{usage/start.md => getting-started.md} (100%) create mode 100644 docs/glossary.md delete mode 100644 docs/terminology.md rename docs/{usage/basic.md => usage-basic.md} (81%) delete mode 100644 docs/usage/advance/builder.md delete mode 100644 docs/usage/advance/manual-playback.md create mode 100644 kohii-exoplayer/src/main/java/kohii/v1/exoplayer/ExoPlayerCache.kt create mode 100644 kohii-exoplayer/src/main/java/kohii/v1/exoplayer/ExoPlayerConfig.kt rename kohii-exoplayer/src/main/java/kohii/v1/exoplayer/{DefaultBandwidthMeterFactory.kt => LoadControlFactory.kt} (64%) create mode 100644 kohii-exoplayer/src/main/java/kohii/v1/exoplayer/RecycledExoPlayerProvider.kt create mode 100644 kohii-exoplayer/src/main/java/kohii/v1/exoplayer/TrackSelectorFactory.kt create mode 100644 kohii-sample-tiktok/.gitignore create mode 100644 kohii-sample-tiktok/build.gradle create mode 100644 kohii-sample-tiktok/proguard-rules.pro create mode 100644 kohii-sample-tiktok/src/main/AndroidManifest.xml create mode 100644 kohii-sample-tiktok/src/main/assets/caminandes.json create mode 100644 kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Data.kt create mode 100644 kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Playlist.kt create mode 100644 kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Sources.kt create mode 100644 kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Tracks.kt create mode 100644 kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Variations.kt create mode 100644 kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Video.kt create mode 100644 kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/MainActivity.kt create mode 100644 kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/TikTokApp.kt create mode 100644 kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/ui/dashboard/DashboardFragment.kt create mode 100644 kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/ui/dashboard/DashboardViewModel.kt create mode 100644 kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/ui/home/HomeFragment.kt create mode 100644 kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/ui/home/HomeViewModel.kt create mode 100644 kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/ui/home/VideoAdapters.kt create mode 100644 kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/ui/home/VideoViewHolder.kt create mode 100644 kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/ui/notifications/NotificationsFragment.kt create mode 100644 kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/ui/notifications/NotificationsViewModel.kt create mode 100644 kohii-sample-tiktok/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 kohii-sample-tiktok/src/main/res/drawable/ic_dashboard_black_24dp.xml create mode 100644 kohii-sample-tiktok/src/main/res/drawable/ic_home_black_24dp.xml create mode 100644 kohii-sample-tiktok/src/main/res/drawable/ic_launcher_background.xml create mode 100644 kohii-sample-tiktok/src/main/res/drawable/ic_notifications_black_24dp.xml create mode 100644 kohii-sample-tiktok/src/main/res/layout/activity_main.xml create mode 100644 kohii-sample-tiktok/src/main/res/layout/fragment_dashboard.xml create mode 100644 kohii-sample-tiktok/src/main/res/layout/fragment_home.xml create mode 100644 kohii-sample-tiktok/src/main/res/layout/fragment_notifications.xml create mode 100644 kohii-sample-tiktok/src/main/res/layout/holder_vertical_video.xml create mode 100644 kohii-sample-tiktok/src/main/res/menu/bottom_nav_menu.xml create mode 100644 kohii-sample-tiktok/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 kohii-sample-tiktok/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 kohii-sample-tiktok/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 kohii-sample-tiktok/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 kohii-sample-tiktok/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 kohii-sample-tiktok/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 kohii-sample-tiktok/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 kohii-sample-tiktok/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 kohii-sample-tiktok/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 kohii-sample-tiktok/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 kohii-sample-tiktok/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 kohii-sample-tiktok/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 kohii-sample-tiktok/src/main/res/navigation/mobile_navigation.xml create mode 100644 kohii-sample-tiktok/src/main/res/values/colors.xml create mode 100644 kohii-sample-tiktok/src/main/res/values/dimens.xml create mode 100644 kohii-sample-tiktok/src/main/res/values/strings.xml create mode 100644 kohii-sample-tiktok/src/main/res/values/styles.xml diff --git a/.gitignore b/.gitignore index bf804a4f..06f623ab 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,7 @@ /.idea/markdown-navigator-enh.xml /.idea/jarRepositories.xml /.idea/render.experimental.xml +/.idea/navEditor.xml +/kohii-sample-tiktok/libs/ +/kohii-sample-tiktok/src/androidTest/ +/kohii-sample-tiktok/src/test/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 56995cd7..2e48fd31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,12 +15,21 @@ _Under development_ - Add method `Kohii.createControlDispatcher(Playback)`. - Add method `Playback.Controller.setupRenderer(Playback, Any?)`. - Add method `Playback.Controller.teardownRenderer(Playback, Any?)`. +- Support Playback locking: if a Playback is locked, it will still be selected but will not be played. +- Add `ExoPlayerCache` to the `kohii-exoplayer` package. It can be used to obtain a pre-built Cache easily. +- Add `ExoPlayerConfig` to gather most of the detailed setting for a `SimpleExoPlayer` instance. +- Add `TrackSelectorFactory`, `LoadControlFactory`. +- Add `createKohii` convenient methods to easily create `Kohii` instance with custom parameters. +- Add `Engine.lock*` and `Engine.unlock*` methods to support manual lock/unlock an Activity/Manager/Bucket or Playback. +- Add a simple demonstration that builds TikTok-alike UI/UX. - [Breaking] Rename `Playable#considerRequestRenderer` -> `Playable#setupRenderer`. - [Breaking] Rename `Playable#considerReleaseRenderer` -> `Playable#teardownRenderer`. - [Breaking] `RendererProvider#releaseRenderer` now needs to return a boolean. - [Breaking] `Playback#addCallback` and `Playback#removeCallback` are now internal. - [Breaking] The `DefaultControlDispatcher` is now internal. +- [Breaking] Include Playback in the `ArtworkHintListener#onArtworkHint`. +- [Breaking] Remove default implementations for `BandwidthMeterFactory`. ## 1.0.0.2010004 diff --git a/README.md b/README.md index ecde210b..4fe967f8 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,18 @@ implementation "im.ene.kohii:kohii-exoplayer:1.0.0.2010004" // default support f implementation "com.google.android.exoplayer:exoplayer:2.10.4" // required ExoPlayer implementation. ``` +You will need to set this flag to your build.gradle too: `-Xjvm-default=enable`. + +```groovy +android { + kotlinOptions { + freeCompilerArgs += [ + '-Xjvm-default=enable' + ] + } +} +``` + ## Start a playback ```Kotlin tab= diff --git a/build.gradle b/build.gradle index 863d67ae..a422fc29 100644 --- a/build.gradle +++ b/build.gradle @@ -48,6 +48,9 @@ allprojects { jcenter() maven { url 'https://jitpack.io' } maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } + flatDir { + dirs 'libs' + } } apply plugin: "org.jlleitschuh.gradle.ktlint" // Version should be inherited from parent diff --git a/buildSrc/src/main/java/kohii/Dependencies.kt b/buildSrc/src/main/java/kohii/Dependencies.kt index 2abacbd5..14971591 100644 --- a/buildSrc/src/main/java/kohii/Dependencies.kt +++ b/buildSrc/src/main/java/kohii/Dependencies.kt @@ -76,7 +76,7 @@ object Libs { // 0.10.0 render method signature after the doc, which looks pretty bad. const val dokkaPlugin = "org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.18" - val junit = "junit:junit:4.12" + val junit = "junit:junit:4.13" val junitExt = "androidx.test.ext:junit-ktx:1.1.1" val robolectric = "org.robolectric:robolectric:4.3.1" val mockitoKotlin = "com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0" @@ -103,6 +103,8 @@ object Libs { val palette = "androidx.palette:palette-ktx:1.0.0" val emoji = "androidx.emoji:emoji:1.0.0" + val vector = "androidx.vectordrawable:vectordrawable:1.1.0" + val recyclerView = "androidx.recyclerview:recyclerview:1.1.0" val recyclerViewSelection = "androidx.recyclerview:recyclerview-selection:1.1.0-rc01" @@ -121,7 +123,7 @@ object Libs { } object Navigation { - private const val version = "2.2.1" + private const val version = "2.3.0-rc01" val runtimeKtx = "androidx.navigation:navigation-runtime-ktx:$version" val commonKtx = "androidx.navigation:navigation-common-ktx:$version" @@ -131,24 +133,24 @@ object Libs { } object Fragment { - private const val version = "1.2.1" + private const val version = "1.2.4" val fragment = "androidx.fragment:fragment:$version" val fragmentKtx = "androidx.fragment:fragment-ktx:$version" } object Test { - private const val version = "1.2.0" + private const val version = "1.3.0-rc01" val core = "androidx.test:core:$version" val runner = "androidx.test:runner:$version" val rules = "androidx.test:rules:$version" - val espressoCore = "androidx.test.espresso:espresso-core:3.2.0-beta01" + val espressoCore = "androidx.test.espresso:espresso-core:3.3.0-rc01" } val archCoreTesting = "androidx.arch.core:core-testing:2.1.0" object Paging { - private const val version = "2.1.1" + private const val version = "2.1.2" val common = "androidx.paging:paging-common-ktx:$version" val runtime = "androidx.paging:paging-runtime-ktx:$version" } @@ -158,8 +160,8 @@ object Libs { // beta4 breaks the overlay demo ... val constraintLayout = "androidx.constraintlayout:constraintlayout:2.0.0-beta3" - val core = "androidx.core:core:1.2.0" - val coreKtx = "androidx.core:core-ktx:1.2.0" + val core = "androidx.core:core:1.3.0" + val coreKtx = "androidx.core:core-ktx:1.3.0" object Lifecycle { private const val version = "2.2.0" @@ -174,7 +176,7 @@ object Libs { } object Room { - private const val version = "2.2.3" + private const val version = "2.2.5" val common = "androidx.room:room-common:$version" val runtime = "androidx.room:room-runtime:$version" val compiler = "androidx.room:room-compiler:$version" diff --git a/docs/advance-builder.md b/docs/advance-builder.md new file mode 100644 index 00000000..a5e212d7 --- /dev/null +++ b/docs/advance-builder.md @@ -0,0 +1,71 @@ +## Using Builder + +**Kohii** instance can be constructed using `Builder`. By default, calling `Kohii[context]` will create or reuse an instance with default implementation. For advance users, it is more flexible to be able to customize this. **Kohii** provides `Builder` to make this happen: + +```Kotlin tab= +val playableCreator = MyCustomPlayableCreator() +val builder = Kohii.Builder(context) + .setPlayableCreator(playableCreator) +val kohii = builder.build() +``` + +```Java tab= +PlayableCreator playableCreator = new MyCustomPlayableCreator(); +Kohii.Builder builder = new Kohii.Builder(context) + .setPlayableCreator(playableCreator); +Kohii kohii = builder.build(); +``` + +If you still want to use the default `PlayerViewPlayableCreator`, it can be constructed by its own Builder too, which will requires a `PlayerViewBridgeCreatorFactory` which is of type `(Context) -> BridgeCreator`: + +```Kotlin tab= +val playableCreator: PlayableCreator = + PlayerViewPlayableCreator.Builder(this) + .setBridgeCreatorFactory(myFactory).build() +``` + +A full Kohii example: + +```kotlin +val kohii = Kohii.Builder(context) + .setPlayableCreator( + PlayerViewPlayableCreator.Builder(context) + .setBridgeCreatorFactory { + PlayerViewBridgeCreator(myPlayerProvider, myMediaSourceFactoryProvider) + } + .build() + ) + .setRendererProviderFactory(myFactory) + .build() +``` + +Please take a look at the source for all available builder parameters. + +## Using extension methods + +_From v1.1.0.2011003_ + +You also have more advance ways to construct new **Kohii** instance: + +```kotlin +val kohii = createKohii( + context = context, + config = ExoPlayerConfig.DEFAULT +) +``` + +Where [ExoPlayerConfig](../api/kohii-exoplayer/kohii.v1.exoplayer/-exo-player-config/) is the combination of many base parameters to construct ExoPlayer's components like the `LoadControl`, `DefaultTrackSelector`, `DefaultBandwidthMeter`, etc. If you have existing parameter to reuse, you can use this convenient to build a **Kohii** instance using them. `ExoPlayerConfig.DEFAULT` is the default configuration where the parameters are the same as default ExoPlayer's setup. + +If you want to reuse the already-built ExoPlayer components (`LoadControl`, `DefaultTrackSelector`, `DefaultBandwidthMeter`, etc) instead, you can also use the second convenient creator below: + +```kotlin +val kohii = createKohii( + context = context, + playerCreator = myPlayerCreator, + mediaSourceFactoryCreator = myMediaSourceFactoryCreator, + rendererProviderFactory = myFactory +) +``` + +Using this method, you can pass your custom way of creating a new [Player](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/Player.html) instance, +[MediaSourceFactory](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/source/MediaSourceFactory.html) instance and [RendererProvider](../api/kohii-core/kohii.v1.core/-renderer-provider/) instance. Each parameter comes with a default value. diff --git a/docs/advance-manual-playback.md b/docs/advance-manual-playback.md new file mode 100644 index 00000000..2f7a3e95 --- /dev/null +++ b/docs/advance-manual-playback.md @@ -0,0 +1 @@ +_Coming Soon_ diff --git a/docs/usage/advance/memory-mode.md b/docs/advance-memory-mode.md similarity index 92% rename from docs/usage/advance/memory-mode.md rename to docs/advance-memory-mode.md index 08a160ec..fe4518ed 100644 --- a/docs/usage/advance/memory-mode.md +++ b/docs/advance-memory-mode.md @@ -1,6 +1,6 @@ ## Using MemoryMode to improve UX -Your screen may contain many Videos at a time, and preload Videos forward so they can start as soon as possible is a legit requirement. In practice, preloading Videos consumes a lot of system resource like memory, network and power. To address this need in proper way, **Kohii** provides a special control flag called [MemoryMode](../../api/kohii-core/kohii.v1.core/-memory-mode/). The idea behinds `MemoryMode` is as below: +Your screen may contain many Videos at a time, and preload Videos forward so they can start as soon as possible is a legit requirement. In practice, preloading Videos consumes a lot of system resource like memory, network and power. To address this need in proper way, **Kohii** provides a special control flag called [MemoryMode](../api/kohii-core/kohii.v1.core/-memory-mode/). The idea behinds `MemoryMode` is as below: - The idea is to allow **Kohii** to _preload around Video of interest[^1]_. When _the Video of interest_ is playing, at the same time **Kohii** will prepare the closet Videos _around_ it: the first on top, the first below, the first to the left and/or the first to the right, and the second closet Videos, etc ... diff --git a/docs/usage/advance/multiple-playbacks.md b/docs/advance-multiple-playbacks.md similarity index 56% rename from docs/usage/advance/multiple-playbacks.md rename to docs/advance-multiple-playbacks.md index 9710f0d1..9d90c1b6 100644 --- a/docs/usage/advance/multiple-playbacks.md +++ b/docs/advance-multiple-playbacks.md @@ -2,11 +2,11 @@ _Available from v1.1.0.2011003_ - + From v1.1.0.2011003, **Kohii** adds _Playback Selector_ and _Playback Strategy_ to support multiple playbacks. The _Selector_ is a _Single Abstract Method_ that accepts a collection of _candidate_ (= the Playbacks that can play the media) and returns a collection of Playback that should play the media. -This feature is enabled at [Bucket](/customize/terms/#bucket-manager-and-group) level. Which means that: client can have multiple playbacks in a **Bucket** by using correct _Strategy_ and _Selector_. The setup is easy: you can set the _Strategy_ and _Selector_ at the time you add the **Bucket**. +This feature is enabled at [Bucket](glossary.md#bucket-manager-and-group) level. Which means that: client can have multiple playbacks in a **Bucket** by using correct _Strategy_ and _Selector_. The setup is easy: you can set the _Strategy_ and _Selector_ at the time you add the **Bucket**. ```Kotlin tab= kohii.register(this) @@ -29,4 +29,4 @@ The available _Strategies_ are: - `SINGLE_PLAYER`: play the first available Playback from the list selected by the _Selector_. - `NO_PLAYER`: do not let the _Selector_ select anything. -**NOTE**: Mutiple playbacks comes with a caveat. In Video playback, audio focus is an important aspect. The client needs to not only respect the audio focus given by system, but also to respect the audio focuses among a Video with the others in the same Application. Therefore, when the client enable `MULTI_PLAYER` _Strategy_, the library will forcefully mute the audio of all available Playbacks, regardless the number of Playbacks selected by the _Selector_. Changing to `SINGLE_PLAYER` or `NO_PLAYER` _Strategy_ will switch everything back to normal. +**NOTE**: Multiple playbacks comes with a caveat. In Video playback, audio focus is an important aspect. The client needs to not only respect the audio focus given by system, but also to respect the audio focuses among a Video with the others in the same Application. Therefore, when the client enable `MULTI_PLAYER` _Strategy_, the library will forcefully mute the audio of all available Playbacks, regardless the number of Playbacks selected by the _Selector_. Changing to `SINGLE_PLAYER` or `NO_PLAYER` _Strategy_ will switch everything back to normal. diff --git a/docs/usage/advance/reuse-renderer-instance.md b/docs/advance-reuse-renderer-instance.md similarity index 68% rename from docs/usage/advance/reuse-renderer-instance.md rename to docs/advance-reuse-renderer-instance.md index e933b2e5..7d5ba844 100644 --- a/docs/usage/advance/reuse-renderer-instance.md +++ b/docs/advance-reuse-renderer-instance.md @@ -2,8 +2,8 @@ Until now, the setup code is always `kohii.setUp(videoUrl).bind(playerView)` which may let you think that you will need to bind the Video to a `PlayerView` instance. -In **Kohii**, target of the method `bind` is called [`container`](../../customize/terms/#renderer-and-container). While `PlayerView` is the place where Video content is rendered (and therefore it is called [`renderer`](../../customize/terms/#renderer-and-container), it can also be a `container` which _contains itself_). +In **Kohii**, target of the method `bind` is called [`container`](glossary.md#renderer-and-container). While `PlayerView` is the place where Video content is rendered (and therefore it is called [`renderer`](glossary.md#renderer-and-container), it can also be a `container` which _contains itself_). You can bind the Video to any `ViewGroup` as container, as long as either it is a `renderer` itself, or it has no children so that it _can contain_ a `renderer` later. When you bind to a _non-renderer_ `container`, for example an empty `FrameLayout`, **Kohii** will automatically prepare and add the `PlayerView` instance to that `FrameLayout` dynamically. At the same time, the unnecessary `PlayerView` instance will be removed from `container` and put back to a `Pool`. This way, only a few `PlayerView` instances will be created and reused for as many container/Videos as possible. -By default, **Kohii** has its own logic for creating and recycling `PlayerView`, but developers can build their own by extending [**Engine**](../../api/kohii-core/kohii.v1.core/-engine) - another important component of **Kohii**. Extending **Engine** and building custom playback logic will be discussed in topics for developers. +By default, **Kohii** has its own logic for creating and recycling `PlayerView`, but developers can build their own by extending [**Engine**](../api/kohii-core/kohii.v1.core/-engine) - another important component of **Kohii**. Extending **Engine** and building custom playback logic will be discussed in topics for developers. diff --git a/docs/usage/advance.md b/docs/advance-summary.md similarity index 57% rename from docs/usage/advance.md rename to docs/advance-summary.md index e2de580a..d7792e3c 100644 --- a/docs/usage/advance.md +++ b/docs/advance-summary.md @@ -4,15 +4,15 @@ Advance usages session will focus on using **Kohii** with **ExoPlayer**. Using o Here, we assume that you are familiar with **ExoPlayer**, including its core components (Player, ExoPlayer, SimpleExoPlayer, MediaSource, etc) and UI system (PlayerView, PlayerControlView, ControlDispatcher, etc). -This section may use some keywords about components in **Kohii** like **Container**, **Bucket**, etc. You can find their definitions here: [Components](../../customize/terms). +This section may use some keywords about components in **Kohii** like **Container**, **Bucket**, etc. You can find their definitions here: [Glossary](glossary.md). Advance usages by **Kohii**: -- [Using Builder](advance/builder.md) -- [Using unique tag](advance/unique-tag.md) -- [Show/Hide thumbnail](advance/thumbnail.md) -- [Switching a Video playback between renderers](advance/switch-renderer.md) -- [Reuse PlayerView instance for multiple Videos](advance/reuse-renderer-instance.md) -- [Using MemoryMode to improve UX](advance/memory-mode.md) -- [Playing many Videos at the same time](advance/multiple-playbacks.md) -- [Manual playback](advance/manual-playback.md) +- [Using custom Kohii creators](advance-builder.md) +- [Using unique tag](advance-unique-tag.md) +- [Show/Hide thumbnail](advance-thumbnail.md) +- [Switching a Video playback between renderers](advance-switch-renderer.md) +- [Reuse PlayerView instance for multiple Videos](advance-reuse-renderer-instance.md) +- [Using MemoryMode to improve UX](advance-memory-mode.md) +- [Playing many Videos at the same time](advance-multiple-playbacks.md) +- [Manual playback](advance-manual-playback.md) diff --git a/docs/usage/advance/switch-renderer.md b/docs/advance-switch-renderer.md similarity index 75% rename from docs/usage/advance/switch-renderer.md rename to docs/advance-switch-renderer.md index ba72caf4..2c270253 100644 --- a/docs/usage/advance/switch-renderer.md +++ b/docs/advance-switch-renderer.md @@ -16,6 +16,6 @@ kohii.setUp(videoUrl) { !!! note Note that you need to set the same unique `tag` to the Video, so that after switching to another `PlayerView`, it keeps playing smoothly, without being reset to beginning. -To help you simplify the steps, the call `bind(playerView1)` with a valid tag will return an object called [`Rebinder`](../../api/kohii-core/kohii.v1.core/-rebinder/). This `Rebinder` has one method `bind` so you can reuse this object to easily rebind a Video to any `PlayerView`. `Rebinder` is also a `Parcelable`, so you can pass this object around. +To help you simplify the steps, the call `bind(playerView1)` with a valid tag will return an object called [`Rebinder`](../api/kohii-core/kohii.v1.core/-rebinder/). This `Rebinder` has one method `bind` so you can reuse this object to easily rebind a Video to any `PlayerView`. `Rebinder` is also a `Parcelable`, so you can pass this object around. Please check [this demo](https://github.com/eneim/kohii/tree/dev-v1/kohii-sample/src/main/java/kohii/v1/sample/ui/sview) to see how it uses `Rebinder` to switch a Video from `PlayerView` to dialog and back. diff --git a/docs/usage/advance/thumbnail.md b/docs/advance-thumbnail.md similarity index 83% rename from docs/usage/advance/thumbnail.md rename to docs/advance-thumbnail.md index 08bb2433..9620709c 100644 --- a/docs/usage/advance/thumbnail.md +++ b/docs/advance-thumbnail.md @@ -1,6 +1,6 @@ ## Show/Hide thumbnail -**Kohii** provides a special interface called [`ArtworkHintListener`](../../api/kohii-core/kohii.v1.core/-playback/-artwork-hint-listener/). With this interface, **Kohii** can tell client when it should show/hide thumbnail (or _artwork_ in **Kohii**'s term). Sample code: +**Kohii** provides a special interface called [`ArtworkHintListener`](../api/kohii-core/kohii.v1.core/-playback/-artwork-hint-listener/). With this interface, **Kohii** can tell client when it should show/hide thumbnail (or _artwork_ in **Kohii**'s term). Sample code: ```Kotlin // 1. Let ViewHolder implement ArtworkHintListener interface. @@ -10,6 +10,7 @@ class VideoViewHolder(itemView: View): ViewHolder(itemView), ArtworkHintListener // Override this callback to show/hide thumbnail. override fun onArtworkHint( + playback: Playback, shouldShow: Boolean, position: Long, state: Int diff --git a/docs/usage/advance/unique-tag.md b/docs/advance-unique-tag.md similarity index 100% rename from docs/usage/advance/unique-tag.md rename to docs/advance-unique-tag.md diff --git a/docs/css/main.css b/docs/css/main.css index c725640c..d63539f2 100644 --- a/docs/css/main.css +++ b/docs/css/main.css @@ -22,6 +22,6 @@ color: #353535; } -html { +/* html { font-size: 125%; -} +} */ diff --git a/docs/customize/custom_engine.md b/docs/custom-engine.md similarity index 100% rename from docs/customize/custom_engine.md rename to docs/custom-engine.md diff --git a/docs/customize/terms.md b/docs/customize/terms.md deleted file mode 100644 index 6c1a9d96..00000000 --- a/docs/customize/terms.md +++ /dev/null @@ -1,37 +0,0 @@ -# Kohii Components - -## Introduction - -This section helps you to understand the core components of **Kohii**. - -## Renderer and Container - -When play a Video to a destination `View`, we call that View a `renderer`. `VideoView` in Android framework, `PlayerView` in ExoPlayer are well-known `renderer`s. In **Kohii**, by calling `bind(someView)`, you are *placing* your Video to an *object* that can then contain a `renderer`. We call that *object* a `container`. In **Kohii**, we deal with `ViewGroup` as `container`, but theoretically it can be anything. In the future, we will try to expand the concept of `container` to other types. Also, a `renderer` can also be `container`, in which case we see the `renderer` is self-contained. - -The ideas of **container** and **renderer** allow **Kohii** to build the abstraction where a `renderer` can be attached to `container`, detached from `container` and passed around, which allows `renderer`s to be reused for unlimited number of Videos. This is actually implemented in `kohii-exoplayer` already. - -## Playback and Playable - -**Playback** is a special object defined to manage a `container`. Like `Fragment` in Android framework which (optionally) has a `View` and *lifecycle*, `Playback`'s view is the `container`, and it has a lifecycle managed by a `Manager`. A Playback's lifecycle is scoped to the `container`'s lifecycle and the *Manager*'s lifecycle, which is as large as an Activity or Fragment lifecycle. - -When you place a Video in a container by calling `bind`, **Kohii** allows you to have some configuration such as *tag*, *visible threshold* by which the Video should start or pause, etc. These configurations are passed to `Playback`, processed and passed down to lower layer to construct resource for an actual playback. So you can have a Video playing with one config in a `container`, but another config in another `container`. It is helpful when you switch a Video from list (where it plays with upto HD quality) to fullscreen (where the config allows it to play with full quality). - -**Playable** is a special object defined to manage a `renderer`. A Playable is created when client wants to place a Video to a container by calling `bind`, and there is no previously created Playable for the same Video information. In other words, a Playable is created/retrieved when there is a Playback requests for it. - -**Kohii** manages Playable globally, therefore its lifecycle is scoped to application lifetime. But once a Playable is no longer *needed*, it will be destroyed to save resource. In **Kohii**, when a Playable is not requested by any Playback, it will be considered *no longer needed* and will be scheduled to destroy. Though, during transient states like configuration change, current Playback will be destroyed (because its container is destroyed) and new Playback is still under construction, **Kohii** will give the Playable *a few more time* to wait for a Playback to request it. After this *a few more time*, if a Playable is not requested, **Kohii** will destroy it so system can reclaim its memory. - -The ideas of **Playable** and **Playback** allow client to use the same Video and player resource for different destinations (a.k.a renderers), which then allows developers to build many complicated scenario more easier. - -## Bucket, Manager and Group - -Considering an application where you have an Activity contains 3 Fragments: A, B and C. A has a RecyclerView (named RA) contains a lot of Videos. B has a ScrollView (named SB) contains some Videos and another RecyclerView (named RB) contains many Videos too. You want only RB to start the Videos automatically, and later you want to disable the automatic playback of RB and enable it for RA. **Kohii** makes this possible by using a number of management components: Bucket, Manager and Group. - -**Bucket** is designed to manage a *big* `View` like `RecyclerView` or `NestedScrollView`. Bucket has the responsibility to tell Manager about UI changes, as well as to select which Video to play from all Videos it *knows* about. For example a Bucket will know about Videos in its `RecyclerView`. To do so, Bucket has specific logic for each type of `View` to observe the scroll change, layout update, etc of that *big View* and notify the Manager about that change. Also, Bucket maintains references to all containers which are *staying* in the *big View*. For example, when you place a Video to a `FrameLayout` which is a child of a `ViewHolder` inside a `RecyclerView`, the Bucket for that `RecyclerView` will added a reference of the `FrameLayout` container to its own memory. So everytime it is asked to, Bucket will then fetch necessary information from those containers to decide which Video to play and which to pause. - -**Manager** is designed to manage a `Fragment` or an `Activity` which contain *big Views*. Manager creates and manages Buckets for Views on demand. So in our scenario, Fragment B only need to register the `RecyclerView` to **Kohii**, the Manager will then acknowledge this `RecyclerView` and create a Bucket for it. Manager also manages all **Playbacks**, and closely communicate with **Group** regarding any UI change. - -**Group** is as important as an Activity in Android framework. It contains many **Managers** just like an `Activity` contain many `Fragments`, and it listens to **Managers** request to refresh overall state. Because we will only allow a small number of videos from one *Bucket* of one *Manager* at a time to be playing, *Group* exists to take care of playback state of all *Managers*, and will carefully update the whole screen (i.e the `Activity`) on demand. - -## Master, Engine - -_COMING SOON_ diff --git a/docs/usage/demos.md b/docs/demos.md similarity index 100% rename from docs/usage/demos.md rename to docs/demos.md diff --git a/docs/usage/start.md b/docs/getting-started.md similarity index 100% rename from docs/usage/start.md rename to docs/getting-started.md diff --git a/docs/glossary.md b/docs/glossary.md new file mode 100644 index 00000000..b1edb44c --- /dev/null +++ b/docs/glossary.md @@ -0,0 +1,44 @@ +# Glossary + +This section helps you to understand the core concepts of **Kohii**. + +## Renderer and Container + +When we play a Video to a destination surface, we call that surface a `Renderer`. Some examples of a Renderer are `VideoView` in Android framework, `PlayerView` in ExoPlayer. + +A component that can contain a `Renderer` is called `Container`. If a `PlayerView` renderer is added directly to a `FrameLayout`, that `FrameLayout` is a container. In **Kohii**, by calling `bind(someView)`, you are *placing* your Video to a `Container`. The renderer can also be the container when it _contains_ itself. + +!!! Note + Currently in **Kohii**, we only support `ViewGroup` containers, but theoretically it can be anything. In the future, we will try to expand the concept of container to other types. + +The idea of **Renderer** and **Container** allows **Kohii** to build the abstractions around them where a renderer can be attached to a container, detached from a container and passed around, and therefore renderers can be reused for unlimited number of Videos. This behavior is supported out of the box by `kohii-exoplayer` and `kohii-androidx` packages. + +## Playback and Playable + +**Playback** is a component in **Kohii**, designed to manage the container. Like **Fragment** in the Android framework which (optionally) has a `View` and `lifecycle`, **Playback** manages a view which is the container, and it has a lifecycle controlled by another component called **Manager**. A Playback's lifecycle is scoped to the container's lifecycle and the Manager's lifecycle, which is as large as an Activity or a Fragment's lifecycle. + +When you place a Video in a container by calling `bind(container)`, **Kohii** allows you to have some configurations such as tag, delay, visible threshold by which the Video should start or pause, etc. These configurations are passed to the Playback, processed and passed down to lower layer to construct resources for an actual playback. So you can have a Video playing with one configuration in a container, but another configuration in another container. It is helpful when you want to switch a Video playback from list to full-screen and vice-versa. + +**Playable** is a component in **Kohii**, designed to manage the renderer. A Playable is created when the client calls `bind(container)`, and there is no Playable created previously for the same Video information. In other words, a Playable will be available when there is a Playback requests it. + +**Kohii** manages Playable globally, which means that its lifecycle is scoped to the Application lifetime. But once a Playable is no longer *needed*, it will be destroyed. In **Kohii**, when a Playable is not used by any Playback, it will be released and scheduled to be destroyed. Though, during transient states like configuration change, current Playback will be destroyed (because its container is destroyed) and new Playback is still under construction, **Kohii** will give the Playable *a few more time* to wait for a Playback to request it. After this duration, if a Playable is not requested, **Kohii** will destroy it to reclaim its memory. + +The idea of **Playable** and **Playback** allows client to use the same Video and player resource for different renderers, which help to reduce the memory usage and enable developers to build complicated scenarios much easier. + +## Bucket, Manager and Group + +Considering an Application where you have an Activity contains 3 Fragments: A, B and C. A has a RecyclerView (named RA) contains a lot of Videos. B has a ScrollView (named SB) contains many Videos and another RecyclerView (named RB) contains many Videos too. You only want RB to start playing the Videos automatically, and later you want to disable the Video playback of RB and enable it for RA. **Kohii** makes this possible by using a number of management components: Bucket, Manager and Group. + +**Bucket** is a component designed to manage a *big* `View` component like `RecyclerView`, `NestedScrollView` or `ViewPager`. Bucket is built by the Manager from the View it manages. And it has the responsibility to tell Manager about UI updates, as well as to select which Video to play from all Videos it *knows* about. The View is called the _root of a Bucket_. You can setup any View to be the root of a Bucket. A simple FrameLayout can be the root for a Bucket of all the Renderers it contains. + +Most of the time you want to use the most suitable root for your use case. For example, in your screen you have a RecyclerView whose each child is a FrameLayout with a PlayerView in it. Either the RecyclerView or each of its FrameLayout children can be the root of a Bucket, but if you want to manage all the Videos at once, you need to use build the Bucket from the RecyclerView. **Kohii** has built-in factory methods to build Bucket for `RecyclerView`, `NestedScrollView`, `ViewPager`, `ViewPager2` and the general `ViewGroup`. You can have many Bucket in a single screen to enable complicated behavior. For example: when a RecyclerView with many Videos is nested in another RecyclerView, you can build the Bucket for both the RecyclerViews to control the Videos of the nested one. + +**Manager** is a component designed to manage a `Fragment` or an `Activity` which contains *big View*s using Buckets. Manager creates and manages Buckets for Views on demand. You only need to create Bucket for the View whose Video need to be controlled. So in our scenario above, Fragment B only need to register the `RecyclerView` to **Kohii**, the Manager will then acknowledge this `RecyclerView` and create a Bucket for it. Manager also manages all **Playbacks** in all Buckets, and closely communicates with the **Group** about any UI change. + +**Group** is as important as an Activity in Android framework. It contains many **Managers** just like an `Activity` can contain many `Fragments`, and it listens to **Managers** request to refresh the overall state. Because we will only allow a small number of videos from one *Bucket* of one *Manager* at a time to be playing, *Group* exists to take care of playback state of all *Managers*, and will carefully update the whole screen (i.e the `Activity`) accordingly. + +## Master, Engine + +**Master** is the component that controls everything used by the library. In short term, it is the brain of **Kohii**. There will be only one Master instance exists at one time. It controls the Playable's lifecycle, including destroying them when they are not needed anymore. It manages all the Groups of an Application, dispatch the playback events and so on. The client should never need to access the Master directly. + +**Engine** is a component that connects the client with the Master. An Engine has 3 important responsibilities: to initialize other important components like Managers, Groups, to help the client to bind the Video resource to a Container, and to build Playable for the Video. Most of the time, developer only needs to work with the Engine. You can also build custom Engine for your need. diff --git a/docs/terminology.md b/docs/terminology.md deleted file mode 100644 index 4e161ead..00000000 --- a/docs/terminology.md +++ /dev/null @@ -1,39 +0,0 @@ -This document introduces and explain some core concepts in **Kohii**. This doc will explain those concepts by taking a Video playback scenario as demonstration. - -# The scenario - -Thinking that, a Video will be played on a piece of UI, in the shape of a rectangle View, in the middle of the device. - -The Video file is stored on the Internet and the client use a url to load it. - -In practice: a PlayerView is playing a Video stored on Cloud. The PlayerView is placed in the center of the phone. - -# The concepts - -## Master - -## Container - -## Renderer - -## Playback - -## Manager - -## Group - -## Engine - -## MemoryMode - -## Scope - -## Playable - -## Bridge - -## Binder/Rebinder - -## RendererProvider - -## PlayableCreator \ No newline at end of file diff --git a/docs/usage/basic.md b/docs/usage-basic.md similarity index 81% rename from docs/usage/basic.md rename to docs/usage-basic.md index 2973d2f0..5c23833c 100644 --- a/docs/usage/basic.md +++ b/docs/usage-basic.md @@ -4,11 +4,11 @@ This **basic usage** session will guide you step-by-step to complete this scenario: you have a `Fragment` with many Videos in a vertical `RecyclerView`. You want each Video to start playing automatically if that Video is *visible more than 65% of its full area, and stay on top of all the visible Videos (fully, or partly)*. If you scroll the list, the Video that is not visible enough will be paused automatically, and the other Video which sastisfy the condition above will start playing automatically. - + ## TL,DR -We will explains a lot of details, so it may be a lot of texts. Here is the short version if you want to start rightaway: +We will explains a lot of details, so it may be a lot of texts. Here is the short version if you want to start right away: First, add this to your `Fragment#onViewCreated` or `Activity#onCreate` @@ -20,7 +20,7 @@ kohii.register(this) ``` ```Java tab= -Kohii kohii = Kohii[this]; +Kohii kohii = Kohii.get(this); kohii.register(this) .addBucket(recyclerView) // assume that you are using a RecyclerView .addBucket(anotherRecyclerView); // yeah, 2 RVs in one place, why not. @@ -118,11 +118,11 @@ kohii.register(this@Fragment) kohii.register(this); ``` -The line above also return a [`Manager`](../../api/kohii-core/kohii.v1.core/-manager/) object. It is useful in some advance usages, but we don't need it for now. +The line above also return a [`Manager`](glossary.md#bucket-manager-and-group) object. It is useful in some advance usages, but we don't need it for now. - Which ViewGroup contains Videos? -We call that ViewGroup a [*bucket*](../../customize/terms/#bucket-manager-and-group). Because you may have more than one *bucket* in your `Fragment`, and not all of them need to be tracked by **Kohii**, you should only register ones you care about. Code for it is as below: +We call that ViewGroup a [*Bucket*](glossary.md#bucket-manager-and-group). Because you may have more than one *bucket* in your `Fragment`, and not all of them need to be tracked by **Kohii**, you should only register ones you care about. Code for it is as below: ```kotlin tab= kohii.register(this@Fragment) // or manager @@ -156,6 +156,6 @@ kohii.setUp(videoUrl).bind(playerView); But let's understand the concept behind: -In the one line above: `kohii.setUp(videoUrl)` turns the url to a [`Binder`](../../api/kohii-core/kohii.v1.core/-binder/) object which can be used to bind to a [`container`](../../customize/terms/#renderer-and-container). Once you finish the setup, you have the Video to be automatically played/paused once user scrolls the list such that the `container` is visible more (will play) or less (will pause) than 65% of its area. +In the one line above: `kohii.setUp(videoUrl)` turns the url to a [`Binder`](../api/kohii-core/kohii.v1.core/-binder/) object which can be used to bind to a [`container`](glossary.md#renderer-and-container). Once you finish the setup, you have the Video to be automatically played/paused once user scrolls the list such that the `container` is visible more (will play) or less (will pause) than 65% of its area. -Also, to ensure the playback is automatic, if the [`renderer`](../../customize/terms/#renderer-and-container) is a `PlayerView` **Kohii** will forcefully disable the `PlayerView`'s `PlayerControlView` even if you set it before. To have manual playback control enabled, you need some additional configuration which will be discussed in other session. +Also, to ensure the playback is automatic, if the [`renderer`](glossary.md#renderer-and-container) is a `PlayerView` **Kohii** will forcefully disable the `PlayerView`'s `PlayerControlView` even if you set it before. To have manual playback control enabled, you need some additional configuration which will be discussed in other session. diff --git a/docs/usage/advance/builder.md b/docs/usage/advance/builder.md deleted file mode 100644 index 9c907ac6..00000000 --- a/docs/usage/advance/builder.md +++ /dev/null @@ -1,27 +0,0 @@ -## Using Builder - -**Kohii** instance can be constructed using `Builder`. By default, calling `Kohii[context]` will create or reuse an instance with default implementation. For advance users, it is more flexible to be able to customize this. **Kohii** provides `Builder` to make this happen: - -```Kotlin tab= -val playableCreator = MyCustomPlayableCreator() -val builder = Kohii.Builder(context) - .setPlayableCreator(playableCreator) -val kohii = builder.build() -``` - -```Java tab= -PlayableCreator playableCreator = new MyCustomPlayableCreator(); -Kohii.Builder builder = new Kohii.Builder(context) - .setPlayableCreator(playableCreator); -Kohii kohii = builder.build(); -``` - -If you still want to use the default `PlayerViewPlayableCreator`, it can be constructed by its own Builder too, which will requires a `BridgeCreatorFactory` which is of type `Context.() -> BridgeCreator`: - -```Kotlin tab= -val playableCreator: PlayableCreator = - PlayerViewPlayableCreator.Builder(this) - .setBridgeCreatorFactory(factory).build() -``` - -Please take a look at the source for all available builder parameters. diff --git a/docs/usage/advance/manual-playback.md b/docs/usage/advance/manual-playback.md deleted file mode 100644 index 1333ed77..00000000 --- a/docs/usage/advance/manual-playback.md +++ /dev/null @@ -1 +0,0 @@ -TODO diff --git a/kohii-androidx/src/main/java/kohii/v1/x/Latte.kt b/kohii-androidx/src/main/java/kohii/v1/x/Latte.kt index 3b948814..4c65132c 100644 --- a/kohii-androidx/src/main/java/kohii/v1/x/Latte.kt +++ b/kohii-androidx/src/main/java/kohii/v1/x/Latte.kt @@ -39,13 +39,15 @@ class Latte private constructor( private constructor(context: Context) : this(Master[context]) - companion object : Capsule(::Latte) { + companion object { + + private val capsule = Capsule(::Latte) @JvmStatic - operator fun get(context: Context) = super.getInstance(context) + operator fun get(context: Context) = capsule.get(context) @JvmStatic - operator fun get(fragment: Fragment) = get(fragment.requireContext()) + operator fun get(fragment: Fragment) = capsule.get(fragment.requireContext()) } override fun prepare(manager: Manager) { diff --git a/kohii-core/src/main/java/kohii/v1/core/Bucket.kt b/kohii-core/src/main/java/kohii/v1/core/Bucket.kt index 90846723..b00a277c 100644 --- a/kohii-core/src/main/java/kohii/v1/core/Bucket.kt +++ b/kohii-core/src/main/java/kohii/v1/core/Bucket.kt @@ -87,9 +87,11 @@ abstract class Bucket constructor( } internal var lock: Boolean = manager.lock + get() = field || manager.lock set(value) { - if (field == value) return field = value + manager.playbacks.filter { it.value.bucket === this } + .forEach { it.value.lock = value } manager.refresh() } @@ -240,19 +242,21 @@ abstract class Bucket constructor( if (lock) return emptyList() if (strategy == NO_PLAYER) return emptyList() - val comparator = playbackComparators.getValue(orientation) - val grouped = candidates.sortedWith(comparator) + val playbackComparator = playbackComparators.getValue(orientation) + val manualToAutoPlaybackGroups = candidates.sortedWith(playbackComparator) .groupBy { it.tag != Master.NO_TAG && it.config.controller != null } .withDefault { emptyList() } - val manualCandidate = with(grouped.getValue(true)) { - val started = find { - manager.master.manuallyStartedPlayable.get() === it.playable - } + val manualCandidate = with(manualToAutoPlaybackGroups.getValue(true)) { + val started = find { manager.master.manuallyStartedPlayable.get() === it.playable } return@with listOfNotNull(started ?: this@with.firstOrNull()) } - return if (manualCandidate.isNotEmpty()) manualCandidate else selector(grouped.getValue(false)) + return if (manualCandidate.isNotEmpty()) { + manualCandidate + } else { + selector(manualToAutoPlaybackGroups.getValue(false)) + } } override fun equals(other: Any?): Boolean { diff --git a/kohii-core/src/main/java/kohii/v1/core/Engine.kt b/kohii-core/src/main/java/kohii/v1/core/Engine.kt index d39a9100..21cda699 100644 --- a/kohii-core/src/main/java/kohii/v1/core/Engine.kt +++ b/kohii-core/src/main/java/kohii/v1/core/Engine.kt @@ -18,6 +18,7 @@ package kohii.v1.core import android.content.Context import android.net.Uri +import android.view.View import android.view.ViewGroup import androidx.annotation.CallSuper import androidx.core.net.toUri @@ -28,6 +29,10 @@ import androidx.lifecycle.Lifecycle.State.STARTED import androidx.lifecycle.LifecycleOwner import kohii.v1.core.Binder.Options import kohii.v1.core.MemoryMode.LOW +import kohii.v1.core.Scope.BUCKET +import kohii.v1.core.Scope.GROUP +import kohii.v1.core.Scope.MANAGER +import kohii.v1.core.Scope.PLAYBACK import kohii.v1.media.Media import kohii.v1.media.MediaItem import kohii.v1.media.VolumeInfo @@ -116,6 +121,9 @@ abstract class Engine constructor( activeLifecycleState = activeLifecycleState ) + /** + * @see Manager.applyVolumeInfo + */ fun applyVolumeInfo( volumeInfo: VolumeInfo, target: Any, @@ -164,6 +172,81 @@ abstract class Engine constructor( } } + /** + * Locks all the Playbacks of a [FragmentActivity]. The locking [Scope] is equal to [Scope.GROUP]. + * Any call to unlock of smaller [Scope] (like [Scope.MANAGER]) will not unlock the Playbacks. + * + * @see [Master.lock] + */ + fun lockActivity(activity: FragmentActivity) { + master.lock(activity, GROUP) + } + + /** + * Unlock all the Playbacks of an [FragmentActivity]. The effective scope is [Scope.GROUP]. If it + * was locked by a call to higher [Scope] like [Scope.GLOBAL], this method does nothing. Once + * unlocked, it will unlock all locked objects within [Scope.GROUP]: [Playback], [Bucket], + * [Manager]. + */ + fun unlockActivity(activity: FragmentActivity) { + master.unlock(activity, GROUP) + } + + /** + * Locks all the Playbacks of a [Manager]. The locking [Scope] is equal to [Scope.MANAGER]. + * Any call to unlock of smaller [Scope] (like [Scope.BUCKET]) will not unlock the Playbacks. + * + * @see [Master.lock] + */ + fun lockManager(manager: Manager) { + master.lock(manager, MANAGER) + } + + /** + * Unlock all the Playbacks of a [Manager]. The effective scope is [Scope.MANAGER]. If it was + * locked by a call to higher [Scope] like [Scope.GROUP], this method does nothing. Once unlocked, + * it will unlock all locked objects within [Scope.MANAGER]: [Playback], [Bucket]. + */ + fun unlockManager(manager: Manager) { + master.unlock(manager, MANAGER) + } + + /** + * Locks all the Playbacks of a [Bucket] whose [Bucket.root] is [view]. The locking [Scope] is + * equal to [Scope.BUCKET]. Any call to unlock of smaller [Scope] (like [Scope.PLAYBACK]) will + * not unlock the Playbacks. + * + * @see [Master.lock] + */ + fun lockBucket(view: View) { + master.lock(view, BUCKET) + } + + /** + * Unlock all the Playbacks of a [Bucket] whose [Bucket.root] is [view]. The effective scope is + * [Scope.BUCKET]. If it was locked by a call to higher [Scope] like [Scope.MANAGER], this method + * does nothing. Once unlocked, it will unlock all locked objects within [Scope.BUCKET]: + * [Playback]. + */ + fun unlockBucket(view: View) { + master.unlock(view, BUCKET) + } + + /** + * Locks all the Playbacks of a [Playback]. The locking [Scope] is equal to [Scope.PLAYBACK]. + */ + fun lockPlayback(playback: Playback) { + master.lock(playback, PLAYBACK) + } + + /** + * Unlock all the Playbacks of a [Playback]. The effective scope is [Scope.PLAYBACK]. If it was + * locked by a call to higher [Scope] like [Scope.BUCKET], this method does nothing. + */ + fun unlockPlayback(playback: Playback) { + master.unlock(playback, PLAYBACK) + } + @CallSuper open fun cleanUp() { playableCreator.cleanUp() diff --git a/kohii-core/src/main/java/kohii/v1/core/Group.kt b/kohii-core/src/main/java/kohii/v1/core/Group.kt index 170c5b7b..911a8778 100644 --- a/kohii-core/src/main/java/kohii/v1/core/Group.kt +++ b/kohii-core/src/main/java/kohii/v1/core/Group.kt @@ -21,7 +21,6 @@ import android.os.Handler import android.os.Message import android.view.ViewGroup import androidx.collection.arraySetOf -import androidx.core.view.ViewCompat import androidx.fragment.app.FragmentActivity import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.Lifecycle.Event @@ -77,9 +76,9 @@ class Group( internal val volumeInfo: VolumeInfo get() = groupVolumeInfo - internal var lock: Boolean = false + internal var lock: Boolean = master.lock + get() = field || master.lock set(value) { - if (field == value) return field = value managers.forEach { it.lock = value } } @@ -163,11 +162,14 @@ class Group( } val oldSelection = selection - selection = - if (lock || activity.lifecycle.currentState < master.groupsMaxLifecycleState) - emptySet() - else - toPlay + selection = if (master.lock || + this.lock || + activity.lifecycle.currentState < master.groupsMaxLifecycleState + ) { + emptySet() + } else { + toPlay.filterTo(mutableSetOf()) { !it.lock } + } val newSelection = selection // Next: as Playbacks are split into 2 collections, we then release unused resources and prepare diff --git a/kohii-core/src/main/java/kohii/v1/core/Manager.kt b/kohii-core/src/main/java/kohii/v1/core/Manager.kt index 1ceba97a..78f009c7 100644 --- a/kohii-core/src/main/java/kohii/v1/core/Manager.kt +++ b/kohii-core/src/main/java/kohii/v1/core/Manager.kt @@ -70,8 +70,8 @@ class Manager internal constructor( } internal var lock: Boolean = group.lock + get() = field || group.lock set(value) { - if (field == value) return field = value buckets.forEach { it.lock = value } refresh() @@ -181,21 +181,18 @@ class Manager internal constructor( provider: RendererProvider ) { val prev = rendererProviders.put(type, provider) - if (prev !== provider) { - prev?.clear() - lifecycleOwner.lifecycle.addObserver(provider) + if (prev != null && prev !== provider) { + prev.clear() + lifecycleOwner.lifecycle.removeObserver(prev) } + + lifecycleOwner.lifecycle.addObserver(provider) } internal fun isChangingConfigurations(): Boolean { return group.activity.isChangingConfigurations } - @RestrictTo(LIBRARY_GROUP) - fun findPlayableForContainer(container: ViewGroup): Playable? { - return playbacks[container]?.playable - } - internal fun findBucketForContainer(container: ViewGroup): Bucket? { if (!container.isAttachedToWindow) return null return buckets.find { it.accepts(container) } @@ -281,17 +278,14 @@ class Manager internal constructor( val bucketToPlaybacks = playbacks.values.groupBy { it.bucket } // -> Map buckets.asSequence() .filter { !bucketToPlaybacks[it].isNullOrEmpty() } - .map { + .map { /* Bucket --> List */ val candidates = bucketToPlaybacks.getValue(it).filter { playback -> - val kohiiCannotPause = master.manuallyStartedPlayable.get() === playback.playable + val cannotPause = master.manuallyStartedPlayable.get() === playback.playable && master.plannedManualPlayables.contains(playback.tag) && !requireNotNull(playback.config.controller).kohiiCanPause() - return@filter kohiiCannotPause || it.allowToPlay(playback) + return@filter cannotPause || it.allowToPlay(playback) } - return@map it to candidates - } - .map { (bucket, candidates) -> - bucket.strategy(bucket.selectToPlay(candidates)) + return@map it.strategy(it.selectToPlay(candidates)) } .find { it.isNotEmpty() } ?.also { diff --git a/kohii-core/src/main/java/kohii/v1/core/Master.kt b/kohii-core/src/main/java/kohii/v1/core/Master.kt index ae4f8294..393e709f 100644 --- a/kohii-core/src/main/java/kohii/v1/core/Master.kt +++ b/kohii-core/src/main/java/kohii/v1/core/Master.kt @@ -51,6 +51,11 @@ import kohii.v1.core.MemoryMode.AUTO import kohii.v1.core.MemoryMode.BALANCED import kohii.v1.core.MemoryMode.LOW import kohii.v1.core.Playback.Config +import kohii.v1.core.Scope.BUCKET +import kohii.v1.core.Scope.GLOBAL +import kohii.v1.core.Scope.GROUP +import kohii.v1.core.Scope.MANAGER +import kohii.v1.core.Scope.PLAYBACK import kohii.v1.debugOnly import kohii.v1.findActivity import kohii.v1.internal.DynamicFragmentRendererPlayback @@ -142,6 +147,12 @@ class Master private constructor(context: Context) : PlayableManager { // TODO LruStore (temporary, short term), SqLiteStore (eternal, manual clean up), etc? private val playbackInfoStore = mutableMapOf() + internal var lock: Boolean = false + set(value) { + field = value + groups.forEach { it.lock = lock } + } + internal var groupsMaxLifecycleState: State = DESTROYED private val componentCallbacks = object : ComponentCallbacks2 { @@ -161,9 +172,9 @@ class Master private constructor(context: Context) : PlayableManager { if (isInitialStickyBroadcast) return if (context != null && intent != null) { if (intent.action == Intent.ACTION_SCREEN_OFF) { - Master[context].pause(Scope.GLOBAL) + Master[context].lock(GLOBAL) } else if (intent.action == Intent.ACTION_USER_PRESENT) { - Master[context].resume(Scope.GLOBAL) + Master[context].unlock(GLOBAL) } } } @@ -346,7 +357,7 @@ class Master private constructor(context: Context) : PlayableManager { // [Draft] return false if this [Master] wants to handle this step by itself, true to release. internal fun releasePlaybackOnInActive(playback: Playback): Boolean { - "Master#onPlaybackInActive: $playback".logDebug() + "Master#releasePlaybackOnInActive: $playback".logDebug() val playable: Playable? = manuallyStartedPlayable.get() return !(playable === playback.playable && playable?.isPlaying() == true) } @@ -533,117 +544,118 @@ class Master private constructor(context: Context) : PlayableManager { } /** - * Manually pause an object in a specific scope. After Playbacks of a Scope is paused by this method, - * only another call to [resume(scope, receiver)] with same or higher priority Scope will resume it. + * Lock an object in a specific scope. Any playing Playbacks of the same scope will be paused. + * After Playbacks of a Scope is paused by this method, only another call to [unlock] by the same + * or higher priority Scope will resume it. * For example: - * - Pausing a Playback with Scope.PLAYBACK --> a call to resume(Scope.PLAYBACK, playback) or - * resume(Scope.BUCKET, playback) will also resume it. - * - Pausing all Playback in a Manager by using Scope.MANAGER --> a call to resume(bucket, Scope.BUCKET) - * will not resume anything, including Playbacks of containers inside to that Bucket. + * - Lock a Playback with Scope.PLAYBACK --> a call to unlock(playback, Scope.PLAYBACK) or + * unlock(playback, Scope.BUCKET) will also unlock it. + * - Lock all Playbacks in a Manager by using Scope.MANAGER --> a call to unlock(bucket, Scope.BUCKET) + * will not resume anything, including Playbacks of containers inside that Bucket. * - * To be able to change the scope of Playbacks need to be paused, client must: - * - Resume all Playbacks of the same or higher priority Scope. - * - Call this method to pause Playbacks of expected scope. + * To change the lock scope of a Playback, the client must: + * - Unlock all Playbacks of the same or higher priority scope. + * - Call this method to lock Playbacks by the expected scope. */ - internal fun pause( - scope: Scope = Scope.GLOBAL, - receiver: Any? = null + internal fun lock( + target: Any? = null, + scope: Scope = GLOBAL ) { when (scope) { - Scope.GLOBAL -> - this.groups.forEach { - this.pause(Scope.GROUP, it) - } - Scope.GROUP -> - when (receiver) { - is Group -> receiver.lock = true // will lock all managers - is Manager -> this.pause(Scope.GROUP, receiver.group) + GLOBAL -> this.lock = true + GROUP -> { + when (target) { + is Group -> target.lock = true // will lock all managers + is Manager -> lock(target.group, GROUP) + is FragmentActivity -> { + val group = groups.firstOrNull { it.activity === target } + if (group != null) lock(group, GROUP) + } else -> throw IllegalArgumentException( "Receiver for scope $scope must be a Manager or a Group" ) } - Scope.MANAGER -> { - require(receiver is Manager) { "Receiver for scope $scope must be a Manager" } - receiver.lock = true } - Scope.BUCKET -> - when (receiver) { - is Bucket -> receiver.lock = true - is Playback -> this.pause(Scope.BUCKET, receiver.bucket) + MANAGER -> { + when (target) { + is Manager -> target.lock = true + is Bucket -> lock(target.manager, MANAGER) + is Playback -> lock(target.manager, MANAGER) + else -> throw IllegalArgumentException("Target for scope $scope must be a Manager") + } + } + BUCKET -> { + when (target) { + is Bucket -> target.lock = true + is Playback -> lock(target.bucket, BUCKET) else -> { val bucket = groups.asSequence() .flatMap { it.managers.asSequence() } .flatMap { it.buckets.asSequence() } - .firstOrNull { it.root === receiver } - if (bucket != null) this.pause(Scope.BUCKET, bucket) + .firstOrNull { it.root === target } + if (bucket != null) lock(bucket, BUCKET) } } - Scope.PLAYBACK -> { - require(receiver is Playback) { "Receiver for scope $scope must be a Playback" } - receiver.playable?.let { pause(it) } + } + PLAYBACK -> { + if (target is Playback) target.lock = true + else throw IllegalArgumentException("Target for scope $scope must be a Playback") } } } /** - * Manually pause an object in a specific scope. After Playbacks of a Scope is paused by this method, - * only another call to [resume(scope, receiver)] with same or higher priority Scope will resume it. - * For example: - * - Pausing a Playback with Scope.PLAYBACK --> a call to resume(Scope.PLAYBACK, playback) or - * resume(Scope.BUCKET, playback) will also resume it. - * - Pausing all Playback in a Manager by using Scope.MANAGER --> a call to resume(bucket, Scope.BUCKET) - * will not resume anything, including Playbacks of containers inside to that Bucket. + * Unlock an object in a specific scope. It can only unlock those Playbacks that was locked by the + * same or lower scope. * - * To be able to change the scope of Playbacks need to be paused, client must: - * - Resume all Playbacks of the same or higher priority Scope. - * - Call this method to pause Playbacks of expected scope. + * @see lock + * @see Scope */ - internal fun resume( - scope: Scope = Scope.GLOBAL, - receiver: Any? = null + internal fun unlock( + target: Any? = null, + scope: Scope = GLOBAL ) { - when { - scope === Scope.GLOBAL -> - this.groups.forEach { - this.resume(Scope.GROUP, it) - } - scope === Scope.GROUP -> - when (receiver) { - is Group -> { - receiver.lock = false - receiver.managers - .forEach { this.resume(Scope.MANAGER, it) } + when (scope) { + GLOBAL -> this.lock = false + GROUP -> { + when (target) { + is Group -> if (!target.master.lock) target.lock = false // -> unlock managers internally + is Manager -> unlock(target.group, GROUP) + is FragmentActivity -> { + val group = groups.firstOrNull { it.activity === target } + if (group != null) unlock(group, GROUP) } - is Manager -> this.resume(Scope.GROUP, receiver.group) else -> throw IllegalArgumentException( "Receiver for scope $scope must be a Manager or a Group" ) } - scope === Scope.MANAGER -> - (receiver as? Manager)?.let { - it.lock = false - it.buckets.forEach { bucket -> this.resume(Scope.BUCKET, bucket) } - } ?: throw IllegalArgumentException("Receiver for scope $scope must be a Manager") - scope === Scope.BUCKET -> - when (receiver) { - is Bucket -> { - receiver.lock = false - receiver.manager.refresh() - } - is Playback -> this.resume(Scope.BUCKET, receiver.bucket) + } + MANAGER -> { + when (target) { + is Manager -> if (!target.group.lock) target.lock = false + is Bucket -> unlock(target.manager, MANAGER) + is Playback -> unlock(target.manager, MANAGER) + else -> throw IllegalArgumentException("Target for scope $scope must be a Manager") + } + } + BUCKET -> { + when (target) { + is Bucket -> if (!target.manager.lock) target.lock = false + is Playback -> unlock(target.bucket, BUCKET) else -> { - // Find the TargetHost whose host is this receiver + // Find the Bucket whose root is this receiver val bucket = groups.asSequence() .flatMap { it.managers.asSequence() } .flatMap { it.buckets.asSequence() } - .firstOrNull { it.root === receiver } + .firstOrNull { it.root === target } - if (bucket != null) this.resume(Scope.BUCKET, bucket) + if (bucket != null) unlock(bucket, BUCKET) } } - scope === Scope.PLAYBACK -> { - require(receiver is Playback) { "Receiver for scope $scope must be a Playback" } - receiver.manager.refresh() + } + PLAYBACK -> { + if (target is Playback) if (!target.bucket.lock) target.lock = false + else throw IllegalArgumentException("Target for scope $scope must be a Playback") } } } @@ -795,11 +807,13 @@ class Master private constructor(context: Context) : PlayableManager { // Public APIs - fun lock() { - this.pause(Scope.GLOBAL) - } + /** + * Globally lock the behavior. + */ + fun lock() = lock(scope = GLOBAL) - fun unlock() { - this.resume(Scope.GLOBAL) - } + /** + * Globally unlock the behavior. + */ + fun unlock() = unlock(scope = GLOBAL) } diff --git a/kohii-core/src/main/java/kohii/v1/core/Playback.kt b/kohii-core/src/main/java/kohii/v1/core/Playback.kt index 593d59d9..0025d53e 100644 --- a/kohii-core/src/main/java/kohii/v1/core/Playback.kt +++ b/kohii-core/src/main/java/kohii/v1/core/Playback.kt @@ -30,6 +30,7 @@ import kohii.v1.core.Bucket.Companion.NONE_AXIS import kohii.v1.core.Bucket.Companion.VERTICAL import kohii.v1.core.Common.STATE_ENDED import kohii.v1.core.Common.STATE_IDLE +import kohii.v1.core.Playback.Controller import kohii.v1.internal.PlayerParametersChangeListener import kohii.v1.logDebug import kohii.v1.media.PlaybackInfo @@ -276,6 +277,7 @@ abstract class Playback( playbackState = STATE_ACTIVE callbacks.forEach { it.onActive(this) } artworkHintListener?.onArtworkHint( + this, playable?.isPlaying() == false, playbackInfo.resumePosition, playerState @@ -286,7 +288,7 @@ abstract class Playback( internal open fun onInActive() { "Playback#onInActive $this".logDebug() playbackState = STATE_INACTIVE - artworkHintListener?.onArtworkHint(true, playbackInfo.resumePosition, playerState) + artworkHintListener?.onArtworkHint(this, true, playbackInfo.resumePosition, playerState) playable?.teardownRenderer(this) callbacks.forEach { it.onInActive(this) } } @@ -296,7 +298,7 @@ abstract class Playback( "Playback#onPlay $this".logDebug() container.keepScreenOn = true artworkHintListener?.onArtworkHint( - playerState == STATE_ENDED, playbackInfo.resumePosition, playerState + this, playerState == STATE_ENDED, playbackInfo.resumePosition, playerState ) } @@ -304,13 +306,20 @@ abstract class Playback( internal open fun onPause() { container.keepScreenOn = false "Playback#onPause $this".logDebug() - artworkHintListener?.onArtworkHint(true, playbackInfo.resumePosition, playerState) + artworkHintListener?.onArtworkHint(this, true, playbackInfo.resumePosition, playerState) } // Will be updated everytime 'onRefresh' is called. private var playbackToken: Token = Token(config.threshold, -1F, Rect(), 0, 0) + internal var lock: Boolean = bucket.lock + get() = field || bucket.lock + set(value) { + field = value + manager.refresh() + } + internal val token: Token get() = playbackToken @@ -483,6 +492,7 @@ abstract class Playback( } val playable = this.playable artworkHintListener?.onArtworkHint( + playback = this, shouldShow = if (playable != null) !playable.isPlaying() else true, position = playbackInfo.resumePosition, state = playerState @@ -640,7 +650,11 @@ abstract class Playback( interface ArtworkHintListener { + /** + * @param position current position of the playback in milliseconds. + */ fun onArtworkHint( + playback: Playback, shouldShow: Boolean, position: Long, state: Int @@ -657,3 +671,22 @@ abstract class Playback( fun onNetworkTypeChanged(networkType: NetworkType): PlayerParameters } } + +/** Extension functions for a Playback */ + +/** + * Quickly setup the [Controller] that only needs to setup the renderer. + * + * @param kohiiCanStart same as [Controller.kohiiCanStart] + * @param kohiiCanPause same as [Controller.kohiiCanPause] + * @param setupRenderer same as [Controller.setupRenderer] + */ +inline fun controller( + kohiiCanStart: Boolean = true, + kohiiCanPause: Boolean = true, + crossinline setupRenderer: (playback: Playback, renderer: Any?) -> Unit +): Controller = object : Controller { + override fun kohiiCanStart(): Boolean = kohiiCanStart + override fun kohiiCanPause(): Boolean = kohiiCanPause + override fun setupRenderer(playback: Playback, renderer: Any?) = setupRenderer(playback, renderer) +} diff --git a/kohii-core/src/main/java/kohii/v1/utils/Capsule.kt b/kohii-core/src/main/java/kohii/v1/utils/Capsule.kt index 61ea37e9..038cfda5 100644 --- a/kohii-core/src/main/java/kohii/v1/utils/Capsule.kt +++ b/kohii-core/src/main/java/kohii/v1/utils/Capsule.kt @@ -53,4 +53,6 @@ open class Capsule( } } } + + fun get(arg: A): T = getInstance(arg) } diff --git a/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/DefaultExoPlayerProvider.kt b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/DefaultExoPlayerProvider.kt index 3f813064..c4e25aa2 100644 --- a/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/DefaultExoPlayerProvider.kt +++ b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/DefaultExoPlayerProvider.kt @@ -17,71 +17,32 @@ package kohii.v1.exoplayer import android.content.Context -import androidx.core.util.Pools -import com.google.android.exoplayer2.DefaultLoadControl import com.google.android.exoplayer2.DefaultRenderersFactory -import com.google.android.exoplayer2.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF -import com.google.android.exoplayer2.LoadControl import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.RenderersFactory -import com.google.android.exoplayer2.SimpleExoPlayer -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector +import com.google.android.exoplayer2.util.Clock import com.google.android.exoplayer2.util.Util -import kohii.v1.media.Media -import kohii.v1.onEachAcquired -import java.net.CookieHandler -import java.net.CookieManager -import java.net.CookiePolicy -import kotlin.math.max /** * @author eneim (2018/10/27). */ class DefaultExoPlayerProvider @JvmOverloads constructor( - private val context: Context, - private val bandwidthMeterFactory: BandwidthMeterFactory = DefaultBandwidthMeterFactory(), - private val loadControl: LoadControl = DefaultLoadControl(), - private val renderersFactory: RenderersFactory = DefaultRenderersFactory( - context.applicationContext - ).setExtensionRendererMode(EXTENSION_RENDERER_MODE_OFF) -) : ExoPlayerProvider { - - companion object { - // Max number of Player instance are cached in the Pool - // Magic number: Build.VERSION.SDK_INT / 6 --> API 16 ~ 18 will set pool size to 2, etc. - internal val MAX_POOL_SIZE = max(Util.SDK_INT / 6, Runtime.getRuntime().availableProcessors()) - } - - // Cache... - private val plainPlayerPool = Pools.SimplePool(MAX_POOL_SIZE) - - override fun acquirePlayer(media: Media): Player { - val result = plainPlayerPool.acquire() ?: KohiiExoPlayer( - context, - renderersFactory, - DefaultTrackSelector(context.applicationContext), - loadControl, - bandwidthMeterFactory.createBandwidthMeter(this.context), - Util.getLooper() - ) - - result.playWhenReady = false - (result as? SimpleExoPlayer)?.also { it.setAudioAttributes(it.audioAttributes, false) } - return result - } - - override fun releasePlayer( - media: Media, - player: Player - ) { - // player.stop(true) // client must stop/do proper cleanup by itself. - if (!plainPlayerPool.release(player)) { - // No more space in pool --> this Player has no where to go --> release it. - player.release() - } - } - - override fun cleanUp() { - plainPlayerPool.onEachAcquired { it.release() } - } + context: Context, + private val clock: Clock = Clock.DEFAULT, + private val bandwidthMeterFactory: BandwidthMeterFactory = ExoPlayerConfig.DEFAULT, + private val trackSelectorFactory: TrackSelectorFactory = ExoPlayerConfig.DEFAULT, + private val loadControlFactory: LoadControlFactory = ExoPlayerConfig.DEFAULT, + private val renderersFactory: RenderersFactory = + DefaultRenderersFactory(context.applicationContext) +) : RecycledExoPlayerProvider(context) { + + override fun createExoPlayer(context: Context): Player = KohiiExoPlayer( + context, + clock, + renderersFactory, + trackSelectorFactory.createDefaultTrackSelector(context), + loadControlFactory.createLoadControl(), + bandwidthMeterFactory.createBandwidthMeter(context), + Util.getLooper() + ) } diff --git a/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/DefaultMediaSourceFactoryProvider.kt b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/DefaultMediaSourceFactoryProvider.kt index ddd6ad11..3a97efce 100644 --- a/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/DefaultMediaSourceFactoryProvider.kt +++ b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/DefaultMediaSourceFactoryProvider.kt @@ -16,6 +16,7 @@ package kohii.v1.exoplayer +import android.content.Context import android.net.Uri import com.google.android.exoplayer2.C import com.google.android.exoplayer2.drm.DrmSessionManager @@ -26,11 +27,17 @@ import com.google.android.exoplayer2.source.dash.DashMediaSource import com.google.android.exoplayer2.source.hls.HlsMediaSource import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource import com.google.android.exoplayer2.upstream.DataSource +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory import com.google.android.exoplayer2.upstream.FileDataSource +import com.google.android.exoplayer2.upstream.HttpDataSource import com.google.android.exoplayer2.upstream.cache.Cache import com.google.android.exoplayer2.upstream.cache.CacheDataSource import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory import com.google.android.exoplayer2.util.Util +import kohii.v1.BuildConfig +import kohii.v1.core.Common +import kohii.v1.exoplayer.ExoPlayerCache.lruCacheSingleton import kohii.v1.media.Media /** @@ -42,6 +49,17 @@ class DefaultMediaSourceFactoryProvider @JvmOverloads constructor( mediaCache: Cache? = null ) : MediaSourceFactoryProvider { + constructor(context: Context, dataSourceFactory: HttpDataSource.Factory) : this( + dataSourceFactory = DefaultDataSourceFactory(context, dataSourceFactory), + drmSessionManagerProvider = DefaultDrmSessionManagerProvider(context, dataSourceFactory), + mediaCache = lruCacheSingleton.get(context) + ) + + constructor(context: Context) : this( + context, + DefaultHttpDataSourceFactory(Common.getUserAgent(context, BuildConfig.LIB_NAME)) + ) + private val dataSourceFactory: DataSource.Factory = if (mediaCache != null) { CacheDataSourceFactory( mediaCache, diff --git a/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/ExoPlayerCache.kt b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/ExoPlayerCache.kt new file mode 100644 index 00000000..50379c0a --- /dev/null +++ b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/ExoPlayerCache.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2020 Nam Nguyen, nam@ene.im + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kohii.v1.exoplayer + +import android.content.Context +import com.google.android.exoplayer2.database.ExoDatabaseProvider +import com.google.android.exoplayer2.upstream.cache.Cache +import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor +import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor +import com.google.android.exoplayer2.upstream.cache.SimpleCache +import kohii.v1.exoplayer.ExoPlayerCache.downloadCacheSingleton +import kohii.v1.exoplayer.ExoPlayerCache.lruCacheSingleton +import kohii.v1.utils.Capsule +import java.io.File + +/** + * A convenient object to help creating and reusing a [Cache] for the media content. It supports + * a [lruCacheSingleton] which is a [SimpleCache] that uses the [LeastRecentlyUsedCacheEvictor] + * internally, and a [downloadCacheSingleton] which is a [SimpleCache] that doesn't evict cache, + * which is useful to store downloaded content. + */ +object ExoPlayerCache { + + private const val CACHE_CONTENT_DIRECTORY = "kohii_content" + private const val DOWNLOAD_CONTENT_DIRECTORY = "kohii_content_download" + private const val CACHE_SIZE = 24 * 1024 * 1024L // 24 Megabytes + + private val lruCacheCreator: (Context) -> Cache = { context -> + SimpleCache( + File( + context.getExternalFilesDir(null) ?: context.filesDir, + CACHE_CONTENT_DIRECTORY + ), + LeastRecentlyUsedCacheEvictor(CACHE_SIZE), + ExoDatabaseProvider(context) + ) + } + + private val downloadCacheCreator: (Context) -> Cache = { context -> + SimpleCache( + File( + context.getExternalFilesDir(null) ?: context.filesDir, + DOWNLOAD_CONTENT_DIRECTORY + ), + NoOpCacheEvictor(), + ExoDatabaseProvider(context) + ) + } + + /** + * A reusable [Cache] that uses the [LeastRecentlyUsedCacheEvictor] internally. + * + * Usage: + * + * ```kotlin + * val cache = ExoPlayerCache.lruCacheSingleton.get(context) + * ``` + */ + val lruCacheSingleton = Capsule(lruCacheCreator) + + /** + * A reusable [Cache] that uses the [NoOpCacheEvictor] internally. + * + * Usage: + * + * ```kotlin + * val cache = ExoPlayerCache.downloadCacheSingleton.get(context) + * ``` + */ + val downloadCacheSingleton = Capsule(downloadCacheCreator) +} diff --git a/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/ExoPlayerConfig.kt b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/ExoPlayerConfig.kt new file mode 100644 index 00000000..1c1f67fe --- /dev/null +++ b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/ExoPlayerConfig.kt @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2020 Nam Nguyen, nam@ene.im + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kohii.v1.exoplayer + +import android.content.Context +import com.google.android.exoplayer2.C +import com.google.android.exoplayer2.DefaultLoadControl +import com.google.android.exoplayer2.DefaultRenderersFactory +import com.google.android.exoplayer2.LoadControl +import com.google.android.exoplayer2.mediacodec.MediaCodecSelector +import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Parameters +import com.google.android.exoplayer2.trackselection.TrackSelection +import com.google.android.exoplayer2.upstream.BandwidthMeter +import com.google.android.exoplayer2.upstream.DefaultAllocator +import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter +import com.google.android.exoplayer2.upstream.cache.Cache +import com.google.android.exoplayer2.util.Clock + +/** + * Detailed config for building a [com.google.android.exoplayer2.SimpleExoPlayer]. Only for + * advanced user. + * + * @see createKohii + */ +data class ExoPlayerConfig( + internal val clock: Clock = Clock.DEFAULT, + // DefaultTrackSelector parameters + internal val trackSelectorParameters: Parameters = Parameters.DEFAULT_WITHOUT_CONTEXT, + internal val trackSelectionFactory: TrackSelection.Factory = AdaptiveTrackSelection.Factory(), + // DefaultBandwidthMeter parameters + internal val overrideInitialBitrateEstimate: Long = -1, + internal val resetOnNetworkTypeChange: Boolean = true, + internal val slidingWindowMaxWeight: Int = DefaultBandwidthMeter.DEFAULT_SLIDING_WINDOW_MAX_WEIGHT, + // DefaultRenderersFactory parameters + internal val enableDecoderFallback: Boolean = true, + internal val allowedVideoJoiningTimeMs: Long = DefaultRenderersFactory.DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS, + internal val extensionRendererMode: Int = DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF, + internal val playClearSamplesWithoutKeys: Boolean = false, + internal val mediaCodecSelector: MediaCodecSelector = MediaCodecSelector.DEFAULT, + // DefaultLoadControl parameters + internal val allocator: DefaultAllocator = DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE), + internal val minBufferMs: Int = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS, + internal val maxBufferMs: Int = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS, + internal val bufferForPlaybackMs: Int = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, + internal val bufferForPlaybackAfterRebufferMs: Int = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, + internal val prioritizeTimeOverSizeThresholds: Boolean = DefaultLoadControl.DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS, + internal val targetBufferBytes: Int = DefaultLoadControl.DEFAULT_TARGET_BUFFER_BYTES, + internal val backBufferDurationMs: Int = DefaultLoadControl.DEFAULT_BACK_BUFFER_DURATION_MS, + internal val retainBackBufferFromKeyframe: Boolean = DefaultLoadControl.DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME, + // Other configurations + internal val cache: Cache? = null, + internal val drmSessionManagerProvider: DefaultDrmSessionManagerProvider? = null +) : LoadControlFactory, BandwidthMeterFactory, TrackSelectorFactory { + + companion object { + /** + * Every fields are default, following the setup by ExoPlayer. + */ + @JvmStatic + val DEFAULT = ExoPlayerConfig() + + /** + * Reduce some setting for fast start playback. + */ + @JvmStatic + val FAST_START = ExoPlayerConfig( + minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS / 10, + maxBufferMs = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS / 10, + bufferForPlaybackMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS / 10, + bufferForPlaybackAfterRebufferMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS / 10 + ) + } + + override fun createLoadControl(): LoadControl = DefaultLoadControl.Builder() + .setAllocator(allocator) + .setBackBuffer( + backBufferDurationMs, + retainBackBufferFromKeyframe + ) + .setBufferDurationsMs( + minBufferMs, + maxBufferMs, + bufferForPlaybackMs, + bufferForPlaybackAfterRebufferMs + ) + .setPrioritizeTimeOverSizeThresholds(prioritizeTimeOverSizeThresholds) + .setTargetBufferBytes(targetBufferBytes) + .createDefaultLoadControl() + + override fun createBandwidthMeter(context: Context): BandwidthMeter = + DefaultBandwidthMeter.Builder(context.applicationContext) + .setClock(clock) + .setResetOnNetworkTypeChange(resetOnNetworkTypeChange) + .setSlidingWindowMaxWeight(slidingWindowMaxWeight) + .apply { + if (overrideInitialBitrateEstimate > 0) { + setInitialBitrateEstimate(overrideInitialBitrateEstimate) + } + } + .build() + + override fun createDefaultTrackSelector(context: Context): DefaultTrackSelector { + val parameters: Parameters = + if (trackSelectorParameters === Parameters.DEFAULT_WITHOUT_CONTEXT) { + trackSelectorParameters.buildUpon() + .setViewportSizeToPhysicalDisplaySize(context, true) + .build() + } else { + trackSelectorParameters + } + + return DefaultTrackSelector(parameters, trackSelectionFactory) + } +} diff --git a/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/Kohii.kt b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/Kohii.kt index e584aad8..9d8281ba 100644 --- a/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/Kohii.kt +++ b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/Kohii.kt @@ -19,17 +19,25 @@ package kohii.v1.exoplayer import android.content.Context import androidx.fragment.app.Fragment import com.google.android.exoplayer2.ControlDispatcher +import com.google.android.exoplayer2.DefaultRenderersFactory +import com.google.android.exoplayer2.Player +import com.google.android.exoplayer2.source.MediaSourceFactory import com.google.android.exoplayer2.ui.PlayerView +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory +import com.google.android.exoplayer2.upstream.cache.Cache +import kohii.v1.BuildConfig +import kohii.v1.core.Common import kohii.v1.core.Engine import kohii.v1.core.Manager import kohii.v1.core.Master import kohii.v1.core.PlayableCreator import kohii.v1.core.Playback import kohii.v1.core.RendererProviderFactory +import kohii.v1.exoplayer.ExoPlayerCache.lruCacheSingleton +import kohii.v1.exoplayer.Kohii.Builder +import kohii.v1.media.Media import kohii.v1.utils.Capsule -import java.net.CookieHandler -import java.net.CookieManager -import java.net.CookiePolicy class Kohii private constructor( master: Master, @@ -39,23 +47,25 @@ class Kohii private constructor( private constructor(context: Context) : this(Master[context]) - companion object : Capsule(::Kohii) { + companion object { + + private val capsule = Capsule(::Kohii) @JvmStatic // convenient static call for Java - operator fun get(context: Context) = super.getInstance(context) + operator fun get(context: Context) = capsule.get(context) @JvmStatic // convenient static call for Java - operator fun get(fragment: Fragment) = get(fragment.requireContext()) + operator fun get(fragment: Fragment) = capsule.get(fragment.requireContext()) } - init { - // Adapt from ExoPlayer demo app. + // Adapt from ExoPlayer demo app. + /* init { val cookieManager = CookieManager() cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER) if (CookieHandler.getDefault() !== cookieManager) { CookieHandler.setDefault(cookieManager) } - } + } */ override fun prepare(manager: Manager) { manager.registerRendererProvider(PlayerView::class.java, rendererProviderFactory()) @@ -100,3 +110,94 @@ class Kohii private constructor( } } } + +/** + * Creates a new [Kohii] instance using an [ExoPlayerConfig]. Note that an application should not + * hold many instance of [Kohii]. + * + * @param context the [Context]. + * @param config the [ExoPlayerConfig]. + */ +fun createKohii(context: Context, config: ExoPlayerConfig): Kohii { + val bridgeCreatorFactory: PlayerViewBridgeCreatorFactory = { appContext -> + val userAgent = Common.getUserAgent(appContext, BuildConfig.LIB_NAME) + val httpDataSource = DefaultHttpDataSourceFactory(userAgent) + + val playerProvider = DefaultExoPlayerProvider( + appContext, + clock = config.clock, + bandwidthMeterFactory = config, + trackSelectorFactory = config, + loadControlFactory = config, + renderersFactory = DefaultRenderersFactory(appContext) + .setEnableDecoderFallback(config.enableDecoderFallback) + .setAllowedVideoJoiningTimeMs(config.allowedVideoJoiningTimeMs) + .setExtensionRendererMode(config.extensionRendererMode) + .setMediaCodecSelector(config.mediaCodecSelector) + .setPlayClearSamplesWithoutKeys(config.playClearSamplesWithoutKeys) + ) + val mediaCache: Cache = config.cache ?: lruCacheSingleton.get(context) + val drmSessionManagerProvider = + config.drmSessionManagerProvider ?: DefaultDrmSessionManagerProvider( + appContext, httpDataSource + ) + val upstreamFactory = DefaultDataSourceFactory(appContext, httpDataSource) + val mediaSourceFactoryProvider = DefaultMediaSourceFactoryProvider( + upstreamFactory, drmSessionManagerProvider, mediaCache + ) + PlayerViewBridgeCreator(playerProvider, mediaSourceFactoryProvider) + } + + val playableCreator = PlayerViewPlayableCreator.Builder(context.applicationContext) + .setBridgeCreatorFactory(bridgeCreatorFactory) + .build() + + return Builder(context).setPlayableCreator(playableCreator).build() +} + +/** + * Creates a new [Kohii] instance using a custom [playerCreator], [mediaSourceFactoryCreator] and + * [rendererProviderFactory]. Note that an application should not hold many instance of [Kohii]. + * + * @param context the [Context]. + * @param playerCreator the custom creator for the [Player]. If `null`, it will use the default one. + * @param mediaSourceFactoryCreator the custom creator for the [MediaSourceFactory]. If `null`, it + * will use the default one. + * @param rendererProviderFactory the custom [RendererProviderFactory]. + */ +@JvmOverloads +fun createKohii( + context: Context, + playerCreator: ((Context) -> Player)? = null, + mediaSourceFactoryCreator: ((Media) -> MediaSourceFactory)? = null, + rendererProviderFactory: RendererProviderFactory = { PlayerViewProvider() } +): Kohii { + val playerProvider = if (playerCreator == null) { + DefaultExoPlayerProvider(context) + } else { + object : RecycledExoPlayerProvider(context) { + override fun createExoPlayer(context: Context): Player = playerCreator(context) + } + } + + val mediaSourceFactoryProvider = + if (mediaSourceFactoryCreator == null) { + DefaultMediaSourceFactoryProvider(context) + } else { + object : MediaSourceFactoryProvider { + override fun provideMediaSourceFactory(media: Media): MediaSourceFactory = + mediaSourceFactoryCreator(media) + } + } + + return Builder(context) + .setPlayableCreator( + PlayerViewPlayableCreator.Builder(context) + .setBridgeCreatorFactory { + PlayerViewBridgeCreator(playerProvider, mediaSourceFactoryProvider) + } + .build() + ) + .setRendererProviderFactory(rendererProviderFactory) + .build() +} diff --git a/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/KohiiExoPlayer.kt b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/KohiiExoPlayer.kt index 84d8bd53..22c6e9d6 100644 --- a/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/KohiiExoPlayer.kt +++ b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/KohiiExoPlayer.kt @@ -44,16 +44,16 @@ import kotlin.LazyThreadSafetyMode.NONE */ open class KohiiExoPlayer( context: Context, - renderersFactory: RenderersFactory = DefaultRenderersFactory( - context.applicationContext - ).setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF), + clock: Clock = Clock.DEFAULT, + renderersFactory: RenderersFactory = DefaultRenderersFactory(context.applicationContext), // TrackSelector is initialized at the same time a new Player instance is created. // This process will set the BandwidthMeter to the TrackSelector. Therefore we need to have // unique TrackSelector per Player instance. - override val trackSelector: DefaultTrackSelector = DefaultTrackSelector(context.applicationContext), - loadControl: LoadControl = DefaultLoadControl(), - bandwidthMeter: BandwidthMeter = DefaultBandwidthMeter.Builder(context.applicationContext) - .build(), + override val trackSelector: DefaultTrackSelector = + DefaultTrackSelector(context.applicationContext), + loadControl: LoadControl = DefaultLoadControl.Builder().createDefaultLoadControl(), + bandwidthMeter: BandwidthMeter = + DefaultBandwidthMeter.Builder(context.applicationContext).build(), looper: Looper = Util.getLooper() ) : SimpleExoPlayer( context, @@ -61,8 +61,8 @@ open class KohiiExoPlayer( trackSelector, loadControl, bandwidthMeter, - AnalyticsCollector(Clock.DEFAULT), - Clock.DEFAULT, + AnalyticsCollector(clock), + clock, looper ), VolumeInfoController, DefaultTrackSelectorHolder { diff --git a/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/DefaultBandwidthMeterFactory.kt b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/LoadControlFactory.kt similarity index 64% rename from kohii-exoplayer/src/main/java/kohii/v1/exoplayer/DefaultBandwidthMeterFactory.kt rename to kohii-exoplayer/src/main/java/kohii/v1/exoplayer/LoadControlFactory.kt index 729a066c..6e101711 100644 --- a/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/DefaultBandwidthMeterFactory.kt +++ b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/LoadControlFactory.kt @@ -16,12 +16,12 @@ package kohii.v1.exoplayer -import android.content.Context -import com.google.android.exoplayer2.upstream.BandwidthMeter -import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter +import com.google.android.exoplayer2.LoadControl -class DefaultBandwidthMeterFactory : BandwidthMeterFactory { +interface LoadControlFactory { - override fun createBandwidthMeter(context: Context): BandwidthMeter = - DefaultBandwidthMeter.Builder(context.applicationContext).build() + /** + * Returns a [LoadControl]. + */ + fun createLoadControl(): LoadControl } diff --git a/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/PlayerViewPlayableCreator.kt b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/PlayerViewPlayableCreator.kt index 2e6990eb..d4f0c803 100644 --- a/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/PlayerViewPlayableCreator.kt +++ b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/PlayerViewPlayableCreator.kt @@ -17,13 +17,10 @@ package kohii.v1.exoplayer import android.content.Context -import com.google.android.exoplayer2.database.ExoDatabaseProvider import com.google.android.exoplayer2.ui.PlayerView import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory import com.google.android.exoplayer2.upstream.cache.Cache -import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor -import com.google.android.exoplayer2.upstream.cache.SimpleCache import kohii.v1.BuildConfig import kohii.v1.core.BridgeCreator import kohii.v1.core.Common @@ -31,8 +28,8 @@ import kohii.v1.core.Master import kohii.v1.core.Playable import kohii.v1.core.Playable.Config import kohii.v1.core.PlayableCreator +import kohii.v1.exoplayer.ExoPlayerCache.lruCacheSingleton import kohii.v1.media.Media -import java.io.File import kotlin.LazyThreadSafetyMode.NONE typealias PlayerViewBridgeCreatorFactory = (Context) -> BridgeCreator @@ -45,8 +42,6 @@ class PlayerViewPlayableCreator internal constructor( constructor(context: Context) : this(Master[context.applicationContext]) companion object { - private const val CACHE_CONTENT_DIRECTORY = "kohii_content" - private const val CACHE_SIZE = 24 * 1024 * 1024L // 24 Megabytes // Only pass Application to this method. private val defaultBridgeCreatorFactory: PlayerViewBridgeCreatorFactory = { context -> @@ -54,19 +49,10 @@ class PlayerViewPlayableCreator internal constructor( val httpDataSource = DefaultHttpDataSourceFactory(userAgent) // ExoPlayerProvider - val playerProvider: ExoPlayerProvider = DefaultExoPlayerProvider( - context, - DefaultBandwidthMeterFactory() - ) + val playerProvider: ExoPlayerProvider = DefaultExoPlayerProvider(context) // MediaSourceFactoryProvider - val fileDir = context.getExternalFilesDir(null) ?: context.filesDir - val contentDir = File(fileDir, CACHE_CONTENT_DIRECTORY) - val mediaCache: Cache = SimpleCache( - contentDir, - LeastRecentlyUsedCacheEvictor(CACHE_SIZE), - ExoDatabaseProvider(context) - ) + val mediaCache: Cache = lruCacheSingleton.get(context) val upstreamFactory = DefaultDataSourceFactory(context, httpDataSource) val drmSessionManagerProvider = DefaultDrmSessionManagerProvider(context, httpDataSource) val mediaSourceFactoryProvider: MediaSourceFactoryProvider = diff --git a/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/RecycledExoPlayerProvider.kt b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/RecycledExoPlayerProvider.kt new file mode 100644 index 00000000..56d6e750 --- /dev/null +++ b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/RecycledExoPlayerProvider.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2020 Nam Nguyen, nam@ene.im + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kohii.v1.exoplayer + +import android.content.Context +import androidx.core.util.Pools +import com.google.android.exoplayer2.Player +import com.google.android.exoplayer2.Player.AudioComponent +import com.google.android.exoplayer2.util.Util +import kohii.v1.media.Media +import kohii.v1.onEachAcquired +import kotlin.math.max + +/** + * Base implementation of the [ExoPlayerProvider] that uses a [Pools.SimplePool] to store the + * [Player] instance for reuse. + * + * @see DefaultExoPlayerProvider + */ +abstract class RecycledExoPlayerProvider(context: Context) : ExoPlayerProvider { + + companion object { + // Max number of Player instance are cached in the Pool + // Magic number: Build.VERSION.SDK_INT / 6 --> API 16 ~ 18 will set pool size to 2, etc. + internal val MAX_POOL_SIZE = max(Util.SDK_INT / 6, Runtime.getRuntime().availableProcessors()) + } + + private val context = context.applicationContext + + // Cache... + private val plainPlayerPool = Pools.SimplePool(MAX_POOL_SIZE) + + /** + * Create a new [Player] instance, given a [Context] of the Application. + */ + abstract fun createExoPlayer(context: Context): Player + + override fun acquirePlayer(media: Media): Player { + val result = plainPlayerPool.acquire() ?: createExoPlayer(context) + result.playWhenReady = false + if (result is AudioComponent) { + result.setAudioAttributes(result.audioAttributes, false) + } + return result + } + + override fun releasePlayer( + media: Media, + player: Player + ) { + // player.stop(true) // client must stop/do proper cleanup by itself. + if (!plainPlayerPool.release(player)) { + // No more space in pool --> this Player has no where to go --> release it. + player.release() + } + } + + override fun cleanUp() { + plainPlayerPool.onEachAcquired { it.release() } + } +} diff --git a/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/TrackSelectorFactory.kt b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/TrackSelectorFactory.kt new file mode 100644 index 00000000..4f0eb9d5 --- /dev/null +++ b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/TrackSelectorFactory.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 Nam Nguyen, nam@ene.im + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kohii.v1.exoplayer + +import android.content.Context +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector + +/** + * An interface that is used by the [DefaultExoPlayerProvider] to create a new + * [DefaultTrackSelector] when needed. + */ +interface TrackSelectorFactory { + + /** + * Creates a new [DefaultTrackSelector] instance, given the [Context] of the Application. + */ + fun createDefaultTrackSelector(context: Context): DefaultTrackSelector +} diff --git a/kohii-experiments/src/main/java/kohii/v1/experiments/OfficialYouTubePlayerEngine.kt b/kohii-experiments/src/main/java/kohii/v1/experiments/OfficialYouTubePlayerEngine.kt index 39d69ea1..46acda6d 100644 --- a/kohii-experiments/src/main/java/kohii/v1/experiments/OfficialYouTubePlayerEngine.kt +++ b/kohii-experiments/src/main/java/kohii/v1/experiments/OfficialYouTubePlayerEngine.kt @@ -36,15 +36,16 @@ class OfficialYouTubePlayerEngine private constructor( private constructor(context: Context) : this(Master[context]) - companion object : Capsule( - ::OfficialYouTubePlayerEngine - ) { + companion object { + + private val capsule = + Capsule(::OfficialYouTubePlayerEngine) @JvmStatic // convenient static call for Java - operator fun get(context: Context) = super.getInstance(context) + operator fun get(context: Context) = capsule.get(context) @JvmStatic // convenient static call for Java - operator fun get(fragment: Fragment) = get(fragment.requireContext()) + operator fun get(fragment: Fragment) = capsule.get(fragment.requireContext()) } override fun prepare(manager: Manager) { diff --git a/kohii-experiments/src/main/java/kohii/v1/experiments/UnofficialYouTubePlayerEngine.kt b/kohii-experiments/src/main/java/kohii/v1/experiments/UnofficialYouTubePlayerEngine.kt index 0635b7ff..94bb1307 100644 --- a/kohii-experiments/src/main/java/kohii/v1/experiments/UnofficialYouTubePlayerEngine.kt +++ b/kohii-experiments/src/main/java/kohii/v1/experiments/UnofficialYouTubePlayerEngine.kt @@ -35,15 +35,16 @@ class UnofficialYouTubePlayerEngine private constructor( private constructor(context: Context) : this(Master[context]) - companion object : Capsule( - ::UnofficialYouTubePlayerEngine - ) { + companion object { + + private val capsule = + Capsule(::UnofficialYouTubePlayerEngine) @JvmStatic // convenient static call for Java - operator fun get(context: Context) = super.getInstance(context) + operator fun get(context: Context) = capsule.get(context) @JvmStatic // convenient static call for Java - operator fun get(fragment: Fragment) = get(fragment.requireContext()) + operator fun get(fragment: Fragment) = capsule.get(fragment.requireContext()) } override fun prepare(manager: Manager) { diff --git a/kohii-sample-tiktok/.gitignore b/kohii-sample-tiktok/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/kohii-sample-tiktok/.gitignore @@ -0,0 +1 @@ +/build diff --git a/kohii-sample-tiktok/build.gradle b/kohii-sample-tiktok/build.gradle new file mode 100644 index 00000000..626f140a --- /dev/null +++ b/kohii-sample-tiktok/build.gradle @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2020 Nam Nguyen, nam@ene.im + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import kohii.BuildConfig +import kohii.Libs + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion BuildConfig.compileSdkVersion + + defaultConfig { + applicationId "kohii.v1.sample.tiktok" + minSdkVersion 21 /* BuildConfig.minSdkVersion */ + targetSdkVersion BuildConfig.targetSdkVersion + versionCode BuildConfig.releaseVersionCode + versionName BuildConfig.releaseVersionName + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testApplicationId 'kohii.v1.sample.tiktok.test' + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + packagingOptions { + exclude 'META-INF/services/javax.annotation.processing.Processor' + exclude 'META-INF/DEPENDENCIES' + } + + buildFeatures { + viewBinding true + } + + productFlavors {} + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + implementation project(':kohii-core') + implementation project(':kohii-exoplayer') + + implementation Libs.ExoPlayer.all + implementation Libs.AndroidX.Media.widget + + implementation Libs.Kotlin.stdlibJdk8 + + implementation Libs.AndroidX.coreKtx + implementation Libs.AndroidX.Fragment.fragmentKtx + implementation Libs.AndroidX.appcompat + implementation Libs.AndroidX.recyclerView + implementation Libs.AndroidX.recyclerViewSelection + implementation Libs.AndroidX.constraintLayout + implementation Libs.Google.material + + implementation(Libs.AndroidX.Navigation.uiKtx) + implementation(Libs.AndroidX.Navigation.commonKtx) + implementation(Libs.AndroidX.Navigation.runtimeKtx) + implementation(Libs.AndroidX.Navigation.fragmentKtx) + + implementation Libs.AndroidX.Lifecycle.extensions + implementation Libs.AndroidX.Lifecycle.runtime + implementation Libs.AndroidX.Lifecycle.viewModel + implementation Libs.AndroidX.Lifecycle.liveData + implementation Libs.AndroidX.Lifecycle.java8 + + implementation Libs.Square.moshi + kapt Libs.Square.moshiCodegen + implementation Libs.Square.okio + + implementation(Libs.Coil.coilBase) + + implementation(Libs.AndroidX.vector) + + testImplementation(Libs.Common.junit) + androidTestImplementation(Libs.Common.junitExt) + androidTestImplementation(Libs.AndroidX.Test.espressoCore) +} diff --git a/kohii-sample-tiktok/proguard-rules.pro b/kohii-sample-tiktok/proguard-rules.pro new file mode 100644 index 00000000..f1b42451 --- /dev/null +++ b/kohii-sample-tiktok/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 diff --git a/kohii-sample-tiktok/src/main/AndroidManifest.xml b/kohii-sample-tiktok/src/main/AndroidManifest.xml new file mode 100644 index 00000000..9ffaca77 --- /dev/null +++ b/kohii-sample-tiktok/src/main/AndroidManifest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + diff --git a/kohii-sample-tiktok/src/main/assets/caminandes.json b/kohii-sample-tiktok/src/main/assets/caminandes.json new file mode 100644 index 00000000..735e73a9 --- /dev/null +++ b/kohii-sample-tiktok/src/main/assets/caminandes.json @@ -0,0 +1,239 @@ +[ + { + "feed_instance_id": "45b7900f-c00b-4418-a35a-2b43b6f49a7c", + "title": "Caminandes 1: Llama Drama", + "kind": "Single Item", + "playlist": [ + { + "mediaid": "Cl6EVHgQ", + "description": "Caminandes is a Creative Commons movie made by Pablo Vazquez, Beorn Leonard, and Francesco Siddi. Music and Sound by Jan Morgenstern.", + "pubdate": 1333238400, + "tags": "blender", + "image": "https://content.jwplatform.com/thumbs/Cl6EVHgQ-720.jpg", + "title": "Caminandes 1: Llama Drama", + "variations": {}, + "sources": [ + { + "type": "application/dash+xml", + "mediaTypes": [ + "audio/webm; codecs=\"vorbis\"", + "video/webm; codecs=\"vp9\"" + ], + "file": "https://content.jwplatform.com/manifests/Cl6EVHgQ.mpd" + }, + { + "type": "application/vnd.apple.mpegurl", + "file": "https://content.jwplatform.com/manifests/Cl6EVHgQ.m3u8" + }, + { + "width": 320, + "height": 180, + "type": "video/mp4", + "file": "https://content.jwplatform.com/videos/Cl6EVHgQ-3gJZg1AH.mp4", + "label": "180p" + }, + { + "width": 480, + "height": 270, + "type": "video/mp4", + "file": "https://content.jwplatform.com/videos/Cl6EVHgQ-AZtqUUiX.mp4", + "label": "270p" + }, + { + "width": 720, + "height": 406, + "type": "video/mp4", + "file": "https://content.jwplatform.com/videos/Cl6EVHgQ-VJXGKLtY.mp4", + "label": "406p" + }, + { + "width": 1280, + "height": 720, + "type": "video/mp4", + "file": "https://content.jwplatform.com/videos/Cl6EVHgQ-oQOe5Prq.mp4", + "label": "720p" + }, + { + "type": "audio/mp4", + "file": "https://content.jwplatform.com/videos/Cl6EVHgQ-mQKzw3z9.m4a", + "label": "AAC Audio" + }, + { + "width": 1920, + "height": 1080, + "type": "video/mp4", + "file": "https://content.jwplatform.com/videos/Cl6EVHgQ-TkIjsDEe.mp4", + "label": "1080p" + } + ], + "tracks": [ + { + "kind": "thumbnails", + "file": "https://content.jwplatform.com/strips/Cl6EVHgQ-120.vtt" + } + ], + "link": "https://content.jwplatform.com/previews/Cl6EVHgQ", + "duration": 90 + } + ], + "description": "Caminandes is a Creative Commons movie made by Pablo Vazquez, Beorn Leonard, and Francesco Siddi. Music and Sound by Jan Morgenstern." + }, + { + "feed_instance_id": "69d54006-912a-4edf-94ea-3e9178291c7d", + "title": "Caminandes 2: Gran Dillama", + "kind": "Single Item", + "playlist": [ + { + "mediaid": "0G7vaSoF", + "description": "Caminandes: Episode 2 is an Open Movie produced by Blender Institute in Amsterdam, the Netherlands. You can support the makers and open source projects by purchasing the 8 GB USB card with all the movie data and tutorials.", + "pubdate": 1380585600, + "tags": "blender", + "image": "https://content.jwplatform.com/thumbs/0G7vaSoF-720.jpg", + "title": "Caminandes 2: Gran Dillama", + "variations": {}, + "sources": [ + { + "type": "application/dash+xml", + "mediaTypes": [ + "audio/webm; codecs=\"vorbis\"", + "video/webm; codecs=\"vp9\"" + ], + "file": "https://content.jwplatform.com/manifests/0G7vaSoF.mpd" + }, + { + "type": "application/vnd.apple.mpegurl", + "file": "https://content.jwplatform.com/manifests/0G7vaSoF.m3u8" + }, + { + "width": 320, + "height": 180, + "type": "video/mp4", + "file": "https://content.jwplatform.com/videos/0G7vaSoF-3gJZg1AH.mp4", + "label": "180p" + }, + { + "width": 480, + "height": 270, + "type": "video/mp4", + "file": "https://content.jwplatform.com/videos/0G7vaSoF-AZtqUUiX.mp4", + "label": "270p" + }, + { + "width": 720, + "height": 406, + "type": "video/mp4", + "file": "https://content.jwplatform.com/videos/0G7vaSoF-VJXGKLtY.mp4", + "label": "406p" + }, + { + "width": 1280, + "height": 720, + "type": "video/mp4", + "file": "https://content.jwplatform.com/videos/0G7vaSoF-oQOe5Prq.mp4", + "label": "720p" + }, + { + "type": "audio/mp4", + "file": "https://content.jwplatform.com/videos/0G7vaSoF-mQKzw3z9.m4a", + "label": "AAC Audio" + }, + { + "width": 1920, + "height": 1080, + "type": "video/mp4", + "file": "https://content.jwplatform.com/videos/0G7vaSoF-TkIjsDEe.mp4", + "label": "1080p" + } + ], + "tracks": [ + { + "kind": "thumbnails", + "file": "https://content.jwplatform.com/strips/0G7vaSoF-120.vtt" + } + ], + "link": "https://content.jwplatform.com/previews/0G7vaSoF", + "duration": 146 + } + ], + "description": "Caminandes: Episode 2 is an Open Movie produced by Blender Institute in Amsterdam, the Netherlands. You can support the makers and open source projects by purchasing the 8 GB USB card with all the movie data and tutorials." + }, + { + "feed_instance_id": "7ce4a93e-56e4-4882-8d5a-1d001b76ffaa", + "title": "Caminandes 3: Llamigos", + "kind": "Single Item", + "playlist": [ + { + "mediaid": "Dn90E0Ca", + "description": "In this episode of the Caminandes cartoon series we get to know our hero Koro even better! It's winter in Patagonia, food is getting scarce. Koro the Llama engages with Oti the pesky penguin in an epic fight over that last tasty berry.", + "pubdate": 1467331200, + "tags": "blender", + "image": "https://content.jwplatform.com/thumbs/Dn90E0Ca-720.jpg", + "title": "Caminandes 3: Llamigos", + "variations": {}, + "sources": [ + { + "type": "application/dash+xml", + "mediaTypes": [ + "audio/webm; codecs=\"vorbis\"", + "video/webm; codecs=\"vp9\"" + ], + "file": "https://content.jwplatform.com/manifests/Dn90E0Ca.mpd" + }, + { + "type": "application/vnd.apple.mpegurl", + "file": "https://content.jwplatform.com/manifests/Dn90E0Ca.m3u8" + }, + { + "width": 320, + "height": 180, + "type": "video/mp4", + "file": "https://content.jwplatform.com/videos/Dn90E0Ca-3gJZg1AH.mp4", + "label": "180p" + }, + { + "width": 480, + "height": 270, + "type": "video/mp4", + "file": "https://content.jwplatform.com/videos/Dn90E0Ca-AZtqUUiX.mp4", + "label": "270p" + }, + { + "width": 720, + "height": 406, + "type": "video/mp4", + "file": "https://content.jwplatform.com/videos/Dn90E0Ca-VJXGKLtY.mp4", + "label": "406p" + }, + { + "width": 1280, + "height": 720, + "type": "video/mp4", + "file": "https://content.jwplatform.com/videos/Dn90E0Ca-oQOe5Prq.mp4", + "label": "720p" + }, + { + "type": "audio/mp4", + "file": "https://content.jwplatform.com/videos/Dn90E0Ca-mQKzw3z9.m4a", + "label": "AAC Audio" + }, + { + "width": 1920, + "height": 1080, + "type": "video/mp4", + "file": "https://content.jwplatform.com/videos/Dn90E0Ca-TkIjsDEe.mp4", + "label": "1080p" + } + ], + "tracks": [ + { + "kind": "thumbnails", + "file": "https://content.jwplatform.com/strips/Dn90E0Ca-120.vtt" + } + ], + "link": "https://content.jwplatform.com/previews/Dn90E0Ca", + "duration": 150 + } + ], + "description": "In this episode of the Caminandes cartoon series we get to know our hero Koro even better! It's winter in Patagonia, food is getting scarce. Koro the Llama engages with Oti the pesky penguin in an epic fight over that last tasty berry." + } +] \ No newline at end of file diff --git a/kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Data.kt b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Data.kt new file mode 100644 index 00000000..2aefcb48 --- /dev/null +++ b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Data.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019 Nam Nguyen, nam@ene.im + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kohii.v1.sample.data + +import android.os.Parcelable +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import kohii.v1.media.MediaDrm +import kotlinx.android.parcel.Parcelize + +/** + * @author eneim (2018/10/30). + */ +@JsonClass(generateAdapter = true) +@Parcelize +data class Item( + val name: String, + val uri: String, + val extension: String?, + @Json(name = "drm_scheme") val drmScheme: String?, + @Json(name = "drm_license_url") val drmLicenseUrl: String? +) : Parcelable + +@JsonClass(generateAdapter = true) +@Parcelize +data class DrmItem( + val item: Item +) : MediaDrm { + override val type: String + get() = item.drmScheme!! + override val licenseUrl: String + get() = requireNotNull(item.drmLicenseUrl) + override val keyRequestPropertiesArray: Array? + get() = null + override val multiSession: Boolean + get() = false +} diff --git a/kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Playlist.kt b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Playlist.kt new file mode 100644 index 00000000..6ffbaade --- /dev/null +++ b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Playlist.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019 Nam Nguyen, nam@ene.im + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kohii.v1.sample.data + +import android.os.Parcelable +import com.squareup.moshi.JsonClass +import kotlinx.android.parcel.Parcelize + +@Suppress("SpellCheckingInspection") +@JsonClass(generateAdapter = true) +@Parcelize +data class Playlist( + val mediaid: String, + val description: String, + val pubdate: Int, + val tags: String, + val image: String, + val title: String, + val variations: Variations, + val sources: List, + val tracks: List, + val link: String, + val duration: Int +) : Parcelable diff --git a/kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Sources.kt b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Sources.kt new file mode 100644 index 00000000..4da70ced --- /dev/null +++ b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Sources.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019 Nam Nguyen, nam@ene.im + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kohii.v1.sample.data + +import android.os.Parcelable +import com.squareup.moshi.JsonClass +import kotlinx.android.parcel.Parcelize + +@Suppress("unused") +@JsonClass(generateAdapter = true) +@Parcelize +class Sources( + val type: String, + val mediaTypes: List?, + val file: String +) : Parcelable diff --git a/kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Tracks.kt b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Tracks.kt new file mode 100644 index 00000000..f2ead10b --- /dev/null +++ b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Tracks.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019 Nam Nguyen, nam@ene.im + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kohii.v1.sample.data + +import android.os.Parcelable +import com.squareup.moshi.JsonClass +import kotlinx.android.parcel.Parcelize + +@JsonClass(generateAdapter = true) +@Parcelize +class Tracks( + val kind: String, + val file: String +) : Parcelable diff --git a/kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Variations.kt b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Variations.kt new file mode 100644 index 00000000..ec8c2b60 --- /dev/null +++ b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Variations.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2019 Nam Nguyen, nam@ene.im + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kohii.v1.sample.data + +import android.os.Parcelable +import com.squareup.moshi.JsonClass +import kotlinx.android.parcel.Parcelize + +@JsonClass(generateAdapter = true) +@Parcelize +class Variations(val meta: String?) : Parcelable diff --git a/kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Video.kt b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Video.kt new file mode 100644 index 00000000..411f1585 --- /dev/null +++ b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/data/Video.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019 Nam Nguyen, nam@ene.im + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kohii.v1.sample.data + +import android.os.Parcelable +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import kotlinx.android.parcel.Parcelize + +@JsonClass(generateAdapter = true) +@Parcelize +data class Video( + @Json(name = "feed_instance_id") + val feedInstanceId: String, + val title: String, + val kind: String, + val playlist: List, + val description: String +) : Parcelable diff --git a/kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/MainActivity.kt b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/MainActivity.kt new file mode 100644 index 00000000..c1166cf7 --- /dev/null +++ b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/MainActivity.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2020 Nam Nguyen, nam@ene.im + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kohii.v1.sample.tiktok + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentFactory +import androidx.navigation.findNavController +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.setupActionBarWithNavController +import androidx.navigation.ui.setupWithNavController +import kohii.v1.sample.tiktok.databinding.ActivityMainBinding +import kohii.v1.sample.tiktok.ui.dashboard.DashboardFragment +import kohii.v1.sample.tiktok.ui.home.HomeFragment +import kohii.v1.sample.tiktok.ui.notifications.NotificationsFragment + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // Reuse Fragments instead of creating new to keep the last visible video position in list. + // This is not a good practice, since it consume more memory. + // Take a look here for a better way: [NavigationAdvancedSample](https://github.com/android/architecture-components-samples/blob/master/NavigationAdvancedSample/app/src/main/java/com/example/android/navigationadvancedsample/NavigationExtensions.kt) + val fragments = hashMapOf( + DashboardFragment::class.java.name to DashboardFragment(), + HomeFragment::class.java.name to HomeFragment(), + NotificationsFragment::class.java.name to NotificationsFragment() + ) + + // Must call before setContentView. + val defaultFactory = supportFragmentManager.fragmentFactory + supportFragmentManager.fragmentFactory = object : FragmentFactory() { + override fun instantiate(classLoader: ClassLoader, className: String): Fragment { + return fragments.getOrElse( + className, { defaultFactory.instantiate(classLoader, className) }) + } + } + + val binding: ActivityMainBinding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + val navController = findNavController(R.id.nav_host_fragment) + + // Passing each menu ID as a set of Ids because each + // menu should be considered as top level destinations. + val appBarConfiguration = AppBarConfiguration( + setOf( + R.id.navigation_home, + R.id.navigation_dashboard, + R.id.navigation_notifications + ) + ) + + setupActionBarWithNavController(navController, appBarConfiguration) + binding.navView.setupWithNavController(navController) + } +} diff --git a/kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/TikTokApp.kt b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/TikTokApp.kt new file mode 100644 index 00000000..34caa933 --- /dev/null +++ b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/TikTokApp.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 Nam Nguyen, nam@ene.im + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kohii.v1.sample.tiktok + +import android.app.Application +import androidx.fragment.app.Fragment +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import kohii.v1.sample.data.Video +import okio.buffer +import okio.source +import kotlin.LazyThreadSafetyMode.NONE + +class TikTokApp : Application() { + + private val moshi: Moshi = Moshi.Builder() + .build() + + val videos by lazy(NONE) { + val jsonAdapter: JsonAdapter> = + moshi.adapter(Types.newParameterizedType(List::class.java, Video::class.java)) + jsonAdapter.fromJson(assets.open("caminandes.json").source().buffer()) ?: emptyList() + } +} + +fun Fragment.getApp(): TikTokApp = requireActivity().application as TikTokApp diff --git a/kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/ui/dashboard/DashboardFragment.kt b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/ui/dashboard/DashboardFragment.kt new file mode 100644 index 00000000..c3652e8c --- /dev/null +++ b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/ui/dashboard/DashboardFragment.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 Nam Nguyen, nam@ene.im + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kohii.v1.sample.tiktok.ui.dashboard + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import kohii.v1.sample.tiktok.R + +class DashboardFragment : Fragment() { + + private lateinit var dashboardViewModel: DashboardViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + dashboardViewModel = + ViewModelProviders.of(this).get(DashboardViewModel::class.java) + val root = inflater.inflate(R.layout.fragment_dashboard, container, false) + val textView: TextView = root.findViewById(R.id.text_dashboard) + dashboardViewModel.text.observe(viewLifecycleOwner, Observer { + textView.text = it + }) + return root + } +} diff --git a/kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/ui/dashboard/DashboardViewModel.kt b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/ui/dashboard/DashboardViewModel.kt new file mode 100644 index 00000000..25e2ecc6 --- /dev/null +++ b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/ui/dashboard/DashboardViewModel.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 Nam Nguyen, nam@ene.im + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kohii.v1.sample.tiktok.ui.dashboard + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class DashboardViewModel : ViewModel() { + + private val _text = MutableLiveData().apply { + value = "This is dashboard Fragment" + } + val text: LiveData = _text +} diff --git a/kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/ui/home/HomeFragment.kt b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/ui/home/HomeFragment.kt new file mode 100644 index 00000000..dddc1651 --- /dev/null +++ b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/ui/home/HomeFragment.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020 Nam Nguyen, nam@ene.im + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kohii.v1.sample.tiktok.ui.home + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle.State +import androidx.recyclerview.widget.PagerSnapHelper +import kohii.v1.core.MemoryMode.HIGH +import kohii.v1.exoplayer.ExoPlayerConfig +import kohii.v1.exoplayer.createKohii +import kohii.v1.sample.tiktok.databinding.FragmentHomeBinding +import kohii.v1.sample.tiktok.getApp + +class HomeFragment : Fragment() { + + private var _binding: FragmentHomeBinding? = null + private val binding: FragmentHomeBinding get() = requireNotNull(_binding) + + private val pagerSnapHelper = PagerSnapHelper() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _binding = FragmentHomeBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val kohii = createKohii(requireContext(), ExoPlayerConfig.FAST_START) + kohii.register(this, memoryMode = HIGH, activeLifecycleState = State.RESUMED) + .addBucket(binding.videos) + binding.videos.adapter = VideoAdapters(getApp().videos, kohii) + pagerSnapHelper.attachToRecyclerView(binding.videos) + } + + override fun onDestroyView() { + super.onDestroyView() + pagerSnapHelper.attachToRecyclerView(null) + _binding = null + } +} diff --git a/kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/ui/home/HomeViewModel.kt b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/ui/home/HomeViewModel.kt new file mode 100644 index 00000000..58f91f12 --- /dev/null +++ b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/ui/home/HomeViewModel.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 Nam Nguyen, nam@ene.im + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kohii.v1.sample.tiktok.ui.home + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class HomeViewModel : ViewModel() { + + private val _text = MutableLiveData().apply { + value = "This is home Fragment" + } + val text: LiveData = _text +} diff --git a/kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/ui/home/VideoAdapters.kt b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/ui/home/VideoAdapters.kt new file mode 100644 index 00000000..4a4e60a0 --- /dev/null +++ b/kohii-sample-tiktok/src/main/java/kohii/v1/sample/tiktok/ui/home/VideoAdapters.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2020 Nam Nguyen, nam@ene.im + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kohii.v1.sample.tiktok.ui.home + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView.Adapter +import coil.api.load +import kohii.v1.core.Common +import kohii.v1.core.Playback +import kohii.v1.core.Playback.ArtworkHintListener +import kohii.v1.core.controller +import kohii.v1.exoplayer.Kohii +import kohii.v1.sample.data.Video +import kohii.v1.sample.tiktok.R +import kohii.v1.sample.tiktok.databinding.HolderVerticalVideoBinding + +class VideoAdapters( + private val videos: List