TP-71285 | Optimised Video Views (#11777)

This commit is contained in:
Shivam Goyal
2024-08-01 20:11:23 +05:30
committed by GitHub
parent 344530169b
commit ff690e4e97
14 changed files with 349 additions and 2 deletions

View File

@@ -13,6 +13,11 @@ import android.content.Context
import android.os.Build
import android.os.Bundle
import android.view.WindowManager
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import androidx.multidex.MultiDexApplication
import coil.ImageLoader
import coil.ImageLoaderFactory
@@ -43,6 +48,7 @@ import com.navi.base.utils.NetWatchManger
import com.navi.base.utils.isNull
import com.navi.chat.base.ChatBaseActivity
import com.navi.common.CommonLibManager
import com.navi.common.media3simplecache.Media3SimpleCache
import com.navi.common.resourcemanager.manager.ResourceManager
import com.navi.common.ui.activity.BaseActivity
import com.navi.common.utils.BiometricPromptUtils
@@ -76,6 +82,7 @@ import com.naviapp.utils.QA
import com.naviapp.utils.isDifferentPackage
import dagger.Lazy
import dagger.hilt.android.HiltAndroidApp
import java.io.File
import javax.inject.Inject
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
@@ -89,12 +96,15 @@ open class NaviApplication :
MultiDexApplication(),
Application.ActivityLifecycleCallbacks,
ImageLoaderFactory,
ReactApplication {
ReactApplication,
Media3SimpleCache {
private var appForegroundCounter: Int = 0
private var enableAppUpdate = true
private var isDifferentPackage: Boolean = false
@OptIn(UnstableApi::class) private lateinit var media3SimpleCache: SimpleCache
// This will initialize NaviPayManager lazily i.e. when NaviPayManager::init() will be called
@Inject lateinit var naviPayManager: Lazy<NaviPayManager>
@@ -369,4 +379,20 @@ open class NaviApplication :
.allowHardware(false)
.build()
}
@OptIn(UnstableApi::class)
override fun getMedia3SimpleCache(): SimpleCache {
if (::media3SimpleCache.isInitialized.not()) {
val downloadContentDirectory = File(this.getExternalFilesDir(null), "downloads")
media3SimpleCache =
SimpleCache(
downloadContentDirectory,
LeastRecentlyUsedCacheEvictor(100_000_000),
StandaloneDatabaseProvider(this)
)
}
return media3SimpleCache
}
}

View File

@@ -10,7 +10,10 @@ package com.naviapp.home.compose.uiTron.renderer
import androidx.compose.foundation.ScrollState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.navi.common.uitron.model.property.VideoViewProperty
import com.navi.common.uitron.model.ui.CommonCustomViewType
import com.navi.common.uitron.render.CommonCustomUiTronRenderer
import com.navi.common.uitron.render.VideoViewRenderer
import com.navi.uitron.model.data.UiTronData
import com.navi.uitron.model.ui.UiTronView
import com.navi.uitron.render.UiTronRenderer
@@ -69,6 +72,17 @@ class HomeCustomUiTronRenderer(private val homeScrollState: () -> ScrollState) :
)
}
}
CommonCustomViewType.VideoView.name -> {
(composeView.property as? VideoViewProperty)?.let {
VideoViewRenderer(parentScrollState = homeScrollState)
.Render(
property = it,
uiTronData = dataMap?.getOrElse(it.layoutId.orEmpty()) { null },
uiTronViewModel = uiTronViewModel,
modifier = modifier
)
}
}
else -> {
super.Render(composeView, modifier, dataMap, uiTronViewModel)
}

View File

@@ -36,6 +36,7 @@ androidx-gridlayout = "1.0.0"
androidx-hilt = "1.0.0"
androidx-legacy-support-v4 = "1.0.0"
androidx-lifecycle = "2.6.2"
androidx-media3 = "1.4.0"
androidx-macro-benchmark = "1.2.4"
androidx-multidex = "2.0.1"
androidx-pagingCompose = "3.2.0"
@@ -211,6 +212,9 @@ androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-ru
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" }
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" }
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "androidx-media3" }
androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "androidx-media3" }
androidx-multidex = { module = "androidx.multidex:multidex", version.ref = "androidx-multidex" }
androidx-navigation-fragment-ktx = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigation" }

View File

@@ -77,6 +77,12 @@ dependencies {
api libs.androidx.camera.lifecycle
api libs.androidx.camera.view
api libs.androidx.lifecycle.runtime.compose
api(libs.androidx.media3.exoplayer) {
exclude(group: "com.google.guava", module: "guava")
}
api(libs.androidx.media3.ui) {
exclude(group: "com.google.guava", module: "guava")
}
api libs.androidx.room.runtime
api libs.androidx.room.ktx
api libs.anrwatchdog

View File

@@ -0,0 +1,30 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.common.media3simplecache
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.cache.SimpleCache
/**
* An interface for providing access to a [SimpleCache] instance.
*
* This interface is useful for abstracting the creation and management of a [SimpleCache] instance,
* allowing for flexibility in how it is implemented and accessed.
*/
interface Media3SimpleCache {
/**
* Returns a [SimpleCache] instance.
*
* This function is marked with `@OptIn(UnstableApi::class)` because the SimpleCache API is
* currently unstable and subject to change.
*
* @return A [SimpleCache] instance.
*/
@OptIn(UnstableApi::class) fun getMedia3SimpleCache(): SimpleCache
}

View File

@@ -10,6 +10,7 @@ package com.navi.common.uitron.deserializer
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonElement
import com.navi.common.adverse.model.AdverseViewData
import com.navi.common.uitron.model.data.VideoViewData
import com.navi.common.uitron.model.ui.CommonCustomViewType
import com.navi.naviwidgets.utils.VIEW_TYPE
import com.navi.uitron.deserializer.UiTronDataDeserializer
@@ -31,6 +32,9 @@ open class CommonUiTronDataDeserializer : UiTronDataDeserializer() {
CommonCustomViewType.AdverseView.name -> {
context?.deserialize(jsonObject, AdverseViewData::class.java)
}
CommonCustomViewType.VideoView.name -> {
context?.deserialize(jsonObject, VideoViewData::class.java)
}
else -> super.deserialize(json, typeOfT, context)
}
}

View File

@@ -10,6 +10,7 @@ package com.navi.common.uitron.deserializer
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonElement
import com.navi.common.adverse.model.AdverseViewProperty
import com.navi.common.uitron.model.property.VideoViewProperty
import com.navi.common.uitron.model.ui.CommonCustomViewType
import com.navi.uitron.deserializer.ComposePropertyDeserializer
import com.navi.uitron.model.ui.BaseProperty
@@ -31,6 +32,9 @@ open class CommonUiTronPropertyDeserializer : ComposePropertyDeserializer() {
CommonCustomViewType.AdverseView.name -> {
return context?.deserialize(jsonObject, AdverseViewProperty::class.java)
}
CommonCustomViewType.VideoView.name -> {
return context?.deserialize(jsonObject, VideoViewProperty::class.java)
}
else -> super.deserialize(json, typeOfT, context)
}
}

View File

@@ -0,0 +1,15 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.common.uitron.model.data
import com.navi.uitron.model.data.UiTronData
data class VideoViewData(
val videoUrl: String? = null,
val repeatMode: String? = null,
) : UiTronData()

View File

@@ -0,0 +1,12 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.common.uitron.model.property
import com.navi.uitron.model.ui.BaseProperty
class VideoViewProperty : BaseProperty()

View File

@@ -8,5 +8,6 @@
package com.navi.common.uitron.model.ui
enum class CommonCustomViewType {
AdverseView
AdverseView,
VideoView,
}

View File

@@ -14,6 +14,7 @@ import androidx.compose.ui.platform.LocalContext
import com.navi.adverse.sdk.adverseState.AdverseStateCallbacks
import com.navi.adverse.sdk.ui.AdverseView
import com.navi.common.adverse.model.AdverseViewData
import com.navi.common.uitron.model.property.VideoViewProperty
import com.navi.common.uitron.model.ui.CommonCustomViewType
import com.navi.uitron.model.data.UiTronData
import com.navi.uitron.model.ui.UiTronView
@@ -64,6 +65,17 @@ open class CommonCustomUiTronRenderer : CustomUiTronRenderer {
}
)
}
CommonCustomViewType.VideoView.name -> {
(composeView.property as? VideoViewProperty)?.let {
VideoViewRenderer()
.Render(
property = it,
uiTronData = dataMap?.getOrElse(it.layoutId.orEmpty()) { null },
uiTronViewModel = uiTronViewModel,
modifier = modifier
)
}
}
else -> Unit
}
}

View File

@@ -0,0 +1,211 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.common.uitron.render
import android.content.Context
import android.net.Uri
import androidx.annotation.OptIn
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.Player.STATE_READY
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.FileDataSource
import androidx.media3.datasource.cache.CacheDataSink
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM
import androidx.media3.ui.PlayerView
import com.navi.base.utils.orTrue
import com.navi.common.CommonLibManager
import com.navi.common.media3simplecache.Media3SimpleCache
import com.navi.common.uitron.model.data.VideoViewData
import com.navi.common.uitron.model.property.VideoViewProperty
import com.navi.uitron.model.data.UiTronData
import com.navi.uitron.render.Renderer
import com.navi.uitron.utils.clip
import com.navi.uitron.utils.customClickable
import com.navi.uitron.utils.customCombinedClick
import com.navi.uitron.utils.customOffset
import com.navi.uitron.utils.setBackground
import com.navi.uitron.utils.setBlur
import com.navi.uitron.utils.setHeight
import com.navi.uitron.utils.setHeightRange
import com.navi.uitron.utils.setPadding
import com.navi.uitron.utils.setTag
import com.navi.uitron.utils.setWidth
import com.navi.uitron.utils.setWidthRange
import com.navi.uitron.viewmodel.UiTronViewModel
class VideoViewRenderer(private val parentScrollState: (() -> ScrollState)? = null) :
Renderer<VideoViewProperty> {
@OptIn(UnstableApi::class)
@Composable
override fun Render(
property: VideoViewProperty,
uiTronData: UiTronData?,
uiTronViewModel: UiTronViewModel,
modifier: Modifier?
) {
super.Render(property, uiTronData, uiTronViewModel, modifier)
val videoViewData = uiTronData as? VideoViewData
if (property.visible.orTrue()) {
val viewModifier =
(modifier ?: Modifier)
.setTag(property)
.layoutId(property.layoutId.orEmpty())
.customOffset(property.offset)
.setWidth(property.width)
.setHeight(property.height)
.setWidthRange(property.widthRange)
.setHeightRange(property.heightRange)
.setPadding(property.margin)
.setBackground(
property.backgroundColor,
property.shape,
property.backGroundBrushData
)
.clip(property.clipShape)
.setPadding(property.padding)
.customClickable(
{ uiTronViewModel.handleActions(uiTronData?.onClick) },
actions = uiTronData?.onClick?.actions,
property = property
)
.customCombinedClick(property, uiTronData) { uiTronViewModel.handleActions(it) }
.graphicsLayer { alpha = property.alpha ?: 1.0f }
.setBlur(property.blurData)
val localContext = LocalContext.current
val mediaUrl = remember(videoViewData?.videoUrl) { videoViewData?.videoUrl.orEmpty() }
val exoplayerRepeatMode =
remember(videoViewData?.repeatMode) {
when (videoViewData?.repeatMode) {
"ONE" -> ExoPlayer.REPEAT_MODE_ONE
"ALL" -> ExoPlayer.REPEAT_MODE_ALL
"OFF" -> ExoPlayer.REPEAT_MODE_OFF
else -> ExoPlayer.REPEAT_MODE_OFF
}
}
val placeholderVisibility = remember { mutableStateOf(true) }
exoPlayer =
remember(mediaUrl, exoplayerRepeatMode) {
ExoPlayer.Builder(localContext).build().apply {
setMediaSource(getProgressiveMediaSource(localContext, mediaUrl))
repeatMode = exoplayerRepeatMode
volume = 0f
prepare()
addListener(
object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
if (playbackState == STATE_READY) {
play()
}
}
override fun onRenderedFirstFrame() {
super.onRenderedFirstFrame()
if (placeholderVisibility.value) {
placeholderVisibility.value = false
}
}
}
)
}
}
val playerView =
remember(exoPlayer) {
PlayerView(localContext).apply {
useController = false
player = exoPlayer
resizeMode = RESIZE_MODE_ZOOM
}
}
Box {
AndroidView(modifier = viewModifier, factory = { playerView })
if (placeholderVisibility.value) {
Box(viewModifier.background(Color.White))
}
}
parentScrollState?.let { if (it.invoke().isScrollInProgress) pause() else play() }
val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)
DisposableEffect(lifecycleOwner.value) { onDispose { releasePlayer() } }
}
}
private var exoPlayer: ExoPlayer? = null
private fun releasePlayer() {
exoPlayer?.apply {
playWhenReady = false
release()
}
exoPlayer = null
}
private fun pause() {
exoPlayer?.playWhenReady = false
}
private fun play() {
exoPlayer?.playWhenReady = true
}
companion object {
@OptIn(UnstableApi::class)
private fun getProgressiveMediaSource(context: Context, mediaUrl: String): MediaSource {
val downloadCache =
(CommonLibManager.application as Media3SimpleCache).getMedia3SimpleCache()
val cacheSink = CacheDataSink.Factory().setCache(downloadCache)
val downstreamFactory = FileDataSource.Factory()
val upstreamFactory =
DefaultDataSource.Factory(context, DefaultHttpDataSource.Factory())
val dataSourceFactory =
CacheDataSource.Factory()
.setCache(downloadCache)
.setCacheWriteDataSinkFactory(cacheSink)
.setCacheReadDataSourceFactory(downstreamFactory)
.setUpstreamDataSourceFactory(upstreamFactory)
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
return ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(Uri.parse(mediaUrl)))
}
}
}

View File

@@ -10,6 +10,7 @@ package com.navi.common.uitron.serializer
import com.google.gson.JsonElement
import com.google.gson.JsonSerializationContext
import com.navi.common.adverse.model.AdverseViewData
import com.navi.common.uitron.model.data.VideoViewData
import com.navi.common.uitron.model.ui.CommonCustomViewType
import com.navi.uitron.model.data.UiTronData
import com.navi.uitron.serializer.UiTronDataSerializer
@@ -27,6 +28,9 @@ open class CommonUiTronDataSerializer : UiTronDataSerializer() {
CommonCustomViewType.AdverseView.name -> {
context?.serialize(src as AdverseViewData, AdverseViewData::class.java)
}
CommonCustomViewType.VideoView.name -> {
context?.serialize(src as VideoViewData, VideoViewData::class.java)
}
else -> super.serialize(src, typeOfSrc, context)
}
}

View File

@@ -10,6 +10,7 @@ package com.navi.common.uitron.serializer
import com.google.gson.JsonElement
import com.google.gson.JsonSerializationContext
import com.navi.common.adverse.model.AdverseViewProperty
import com.navi.common.uitron.model.property.VideoViewProperty
import com.navi.common.uitron.model.ui.CommonCustomViewType
import com.navi.uitron.model.ui.BaseProperty
import com.navi.uitron.serializer.ComposePropertySerializer
@@ -27,6 +28,9 @@ open class CommonUiTronPropertySerializer : ComposePropertySerializer() {
CommonCustomViewType.AdverseView.name -> {
context?.serialize(src as AdverseViewProperty, AdverseViewProperty::class.java)
}
CommonCustomViewType.VideoView.name -> {
context?.serialize(src as VideoViewProperty, VideoViewProperty::class.java)
}
else -> super.serialize(src, typeOfSrc, context)
}
}