TP-76171 | Video view renderer in UiTron (#527)

Co-authored-by: soumya-ranjan_navi <soumya.ranjan@navi.com>
Co-authored-by: Shivam Goyal <shivam.goyal@navi.com>
This commit is contained in:
Neil Mehta
2024-09-23 13:46:29 +05:30
committed by GitHub
parent 30feb6c389
commit bfc8b883e6
17 changed files with 351 additions and 8 deletions

View File

@@ -13,6 +13,9 @@ import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import com.navi.alfred.AlfredManager
import com.navi.uitron.IUiTronDependencyProvider
import com.navi.uitron.model.UiTronConfig
@@ -20,8 +23,19 @@ import com.navi.uitron.model.ui.OutlinedTextFieldValueTransformation
import com.navi.uitron.model.ui.UiTronShape
import com.navi.uitron.utils.EMPTY
import com.navi.uitron.utils.SPACE
import java.io.File
class UiTronDependencyProvider : IUiTronDependencyProvider {
private val media3SimpleCache by lazy {
val downloadContentDirectory = File(getContext().getExternalFilesDir(null), "downloads")
SimpleCache(
downloadContentDirectory,
LeastRecentlyUsedCacheEvictor(100_000_000),
StandaloneDatabaseProvider(getContext())
)
}
private val ttFontFamily =
FontFamily(
Font(
@@ -218,6 +232,10 @@ class UiTronDependencyProvider : IUiTronDependencyProvider {
return null
}
override fun getMedia3SimpleCache(): SimpleCache {
return media3SimpleCache
}
override fun maskSensitiveUiTronComposable(
id: String,
left: Float?,

View File

@@ -1,5 +1,37 @@
{
"movingBoxes":{
"videoMock": {
"data": {
"productIcon": {
"videoUrl": "https://public-assets.prod.navi-sa.in/shivam/video-view/coverr-close-up-of-unboxing-iphone-15-1080p.mp4",
"viewType": "Video"
}
},
"view": [
{
"property": {
"width": "MATCH_PARENT",
"height": "MATCH_PARENT",
"layoutId": "productIcon",
"viewType": "Video",
"placeHolderView": {
"data": {},
"view": [
{
"property": {
"width": "MATCH_PARENT",
"height": "MATCH_PARENT",
"layoutId": "placeHolderView",
"viewType": "Box",
"backgroundColor": "#000000"
}
}
]
}
}
}
]
},
"movingBoxes": {
"view": [
{
"property": {
@@ -1504,7 +1536,7 @@
}
}
},
"spannableV2": {
"spannableV2": {
"view": [
{
"property": {
@@ -4635,7 +4667,7 @@
}
}
},
"cardShadowTemplate":{
"cardShadowTemplate": {
"parentComposeView": [
{
"property": {
@@ -4659,7 +4691,6 @@
},
"repeatMode": "Repeat"
},
"targetColor": "#011627"
}
],
@@ -10981,7 +11012,6 @@
"width": "MATCH_PARENT",
"height": "WRAP_CONTENT",
"layoutId": "parent_container",
"borderStrokeData": {
"brushData": {
"brushType": "LINEAR",
@@ -18245,7 +18275,7 @@
],
"data": {
"simple_text": {
"text":"NAVI",
"text": "NAVI",
"viewType": "Text"
},
"click_me_trigger": {

View File

@@ -9,6 +9,7 @@ androidx-constraintlayoutCompose = "1.1.0-alpha10"
androidx-core-ktx = "1.8.0"
androidx-hilt = "1.0.0"
androidx-lifecycle = "2.6.1"
androidx-media3 = "1.4.1"
androidx-pagingCompose = "3.2.0"
androidx-pagingRuntimeKtx = "3.1.1"
androidx-profileinstaller = "1.3.1"
@@ -65,6 +66,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-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" }
androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "androidx-pagingCompose" }

6
lint.xml Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<lint>
<issue id="UnsafeOptInUsageError">
<option name="opt-in" value="androidx.media3.common.util.UnstableApi" />
</issue>
</lint>

View File

@@ -84,6 +84,12 @@ publishing {
}
dependencies {
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.coil.compose
implementation platform(libs.androidx.compose.bom)

View File

@@ -12,6 +12,7 @@ import android.view.View
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.media3.datasource.cache.SimpleCache
import com.navi.uitron.model.UiTronConfig
import com.navi.uitron.model.ui.OutlinedTextFieldValueTransformation
import com.navi.uitron.model.ui.UiTronShape
@@ -60,4 +61,6 @@ interface IUiTronDependencyProvider {
fun getUiTronConfig(): UiTronConfig
fun getShape(shape: UiTronShape?): Shape?
fun getMedia3SimpleCache(): SimpleCache
}

View File

@@ -162,6 +162,9 @@ open class ComposePropertyDeserializer : JsonDeserializer<BaseProperty> {
LinearProgressIndicatorWithThumbProperty::class.java
)
}
ComposeViewType.Video.name -> {
context?.deserialize(jsonObject, VideoProperty::class.java)
}
else -> null
}
}

View File

@@ -40,6 +40,7 @@ import com.navi.uitron.model.data.TextData
import com.navi.uitron.model.data.ToastData
import com.navi.uitron.model.data.UiTronData
import com.navi.uitron.model.data.UiTronSliderData
import com.navi.uitron.model.data.VideoData
import com.navi.uitron.model.ui.ComposeViewType
import java.lang.reflect.Type
@@ -162,6 +163,9 @@ open class UiTronDataDeserializer : JsonDeserializer<UiTronData> {
LinearProgressIndicatorWithThumbData::class.java
)
}
ComposeViewType.Video.name -> {
context?.deserialize(jsonObject, VideoData::class.java)
}
else -> null
}
}

View File

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

View File

@@ -37,6 +37,7 @@ import com.navi.uitron.model.data.TextData
import com.navi.uitron.model.data.ToastData
import com.navi.uitron.model.data.UiTronData
import com.navi.uitron.model.data.UiTronSliderData
import com.navi.uitron.model.data.VideoData
import com.navi.uitron.model.ui.ComposeViewType
class UiTronDataProviderFactory {
@@ -73,6 +74,7 @@ class UiTronDataProviderFactory {
ComposeViewType.RadioGroup.name -> RadioGroupData()
ComposeViewType.LinearProgressIndicatorWithThumb.name ->
LinearProgressIndicatorWithThumbData()
ComposeViewType.Video.name -> VideoData()
else -> EmptyData()
}
}

View File

@@ -261,7 +261,8 @@ enum class ComposeViewType {
FloatingActionButton,
ToolTipPopUp,
RadioGroup,
LinearProgressIndicatorWithThumb
LinearProgressIndicatorWithThumb,
Video
}
val containerRenderers =
@@ -314,7 +315,8 @@ val elementRenderers =
ComposeViewType.CustomTextField.name,
ComposeViewType.OtpBox.name,
ComposeViewType.SlideToActButton.name,
ComposeViewType.LinearProgressIndicatorWithThumb.name
ComposeViewType.LinearProgressIndicatorWithThumb.name,
ComposeViewType.Video.name
)
enum class HorizontalArrangementType {

View File

@@ -0,0 +1,21 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.uitron.model.ui
import com.navi.uitron.model.UiTronResponse
data class VideoProperty(
var placeHolderView: UiTronResponse? = null,
) : BaseProperty() {
override fun copyNonNullFrom(property: BaseProperty?) {
super.copyNonNullFrom(property)
val videoProperty = property as? VideoProperty
videoProperty?.placeHolderView?.let { placeHolderView = it }
}
}

View File

@@ -31,6 +31,7 @@ import com.navi.uitron.provider.element.SpacerViewRenderer
import com.navi.uitron.provider.element.SpannableTextViewRenderer
import com.navi.uitron.provider.element.SwitchViewRenderer
import com.navi.uitron.provider.element.TextViewRenderer
import com.navi.uitron.provider.element.VideoViewRenderer
class ElementRendererProviderImpl : IUiTronRendererProvider {
@@ -61,6 +62,7 @@ class ElementRendererProviderImpl : IUiTronRendererProvider {
ComposeViewType.SlideToActButton.name -> SlideToActButtonViewRenderer()
ComposeViewType.LinearProgressIndicatorWithThumb.name ->
LinearProgressIndicatorWithThumbViewRenderer()
ComposeViewType.Video.name -> VideoViewRenderer()
else -> null
}
}

View File

@@ -0,0 +1,31 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.uitron.provider.element
import androidx.compose.runtime.Composable
import com.navi.uitron.model.UiTronRenderConfig
import com.navi.uitron.model.ui.VideoProperty
import com.navi.uitron.provider.IUiTronRenderer
import com.navi.uitron.render.VideoRenderer
class VideoViewRenderer : IUiTronRenderer {
@Composable
override fun RenderUiTronView(uiTronRenderConfig: UiTronRenderConfig) {
(uiTronRenderConfig.composeView.property as? VideoProperty)?.let {
VideoRenderer(uiTronRenderer = uiTronRenderConfig.uiTronRenderer)
.Render(
property = it,
uiTronData =
uiTronRenderConfig.dataMap?.getOrElse(it.layoutId.orEmpty()) { null },
uiTronViewModel = uiTronRenderConfig.uiTronViewModel,
modifier = uiTronRenderConfig.modifier
)
}
}
}

View File

@@ -0,0 +1,192 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.uitron.render
import android.content.Context
import android.net.Uri
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.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.uitron.UiTronSdkManager
import com.navi.uitron.model.data.UiTronData
import com.navi.uitron.model.data.VideoData
import com.navi.uitron.model.ui.VideoProperty
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.orTrue
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 VideoRenderer(private val uiTronRenderer: UiTronRenderer) : Renderer<VideoProperty> {
@Composable
override fun Render(
property: VideoProperty,
uiTronData: UiTronData?,
uiTronViewModel: UiTronViewModel,
modifier: Modifier?
) {
super.Render(property, uiTronData, uiTronViewModel, modifier)
val videoData = uiTronData as? VideoData
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(videoData?.videoUrl) { videoData?.videoUrl.orEmpty() }
val placeholderVisibility = remember { mutableStateOf(true) }
exoPlayer =
remember(mediaUrl) {
ExoPlayer.Builder(localContext).build().apply {
setMediaSource(getProgressiveMediaSource(localContext, mediaUrl))
repeatMode = ExoPlayer.REPEAT_MODE_ALL
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) {
property.placeHolderView?.let {
UiTronRenderer(
it.data,
uiTronViewModel,
uiTronRenderer.customUiTronRenderer,
)
.Render(
composeViews = it.parentComposeView.orEmpty(),
modifier = viewModifier
)
} ?: run { Box(viewModifier.background(Color.White)) }
}
}
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
}
companion object {
private fun getProgressiveMediaSource(context: Context, mediaUrl: String): MediaSource {
val downloadCache = UiTronSdkManager.getDependencyProvider().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

@@ -177,6 +177,9 @@ open class ComposePropertySerializer : JsonSerializer<BaseProperty> {
LinearProgressIndicatorWithThumbProperty::class.java
)
}
ComposeViewType.Video.name -> {
context?.serialize(src as VideoProperty, VideoProperty::class.java)
}
else -> null
}
}

View File

@@ -38,6 +38,7 @@ import com.navi.uitron.model.data.TextData
import com.navi.uitron.model.data.ToastData
import com.navi.uitron.model.data.UiTronData
import com.navi.uitron.model.data.UiTronSliderData
import com.navi.uitron.model.data.VideoData
import com.navi.uitron.model.ui.ComposeViewType
import java.lang.reflect.Type
@@ -154,6 +155,9 @@ open class UiTronDataSerializer : JsonSerializer<UiTronData> {
LinearProgressIndicatorWithThumbData::class.java
)
}
ComposeViewType.Video.name -> {
context?.serialize(src as VideoData, VideoData::class.java)
}
else -> null
}
}