Add contrast effect to effect demo

Added a contrast effect and the connection needed to apply the video effects to ExoPlayer.

The effect can be applied to the video by checking the "Contrast" card, and use the slider to change the contrast value. The effects are applied when `Apply effects` button is clicked.

PiperOrigin-RevId: 707092041
This commit is contained in:
shahddaghash 2024-12-17 07:30:32 -08:00 committed by Copybara-Service
parent 2d11a339de
commit acc41cb5f7
4 changed files with 142 additions and 10 deletions

View File

@ -76,6 +76,7 @@ dependencies {
implementation project(modulePrefix + 'lib-exoplayer') implementation project(modulePrefix + 'lib-exoplayer')
implementation project(modulePrefix + 'lib-ui') implementation project(modulePrefix + 'lib-ui')
implementation project(modulePrefix + 'lib-effect')
// For detecting and debugging leaks only. LeakCanary is not needed for demo app to work. // For detecting and debugging leaks only. LeakCanary is not needed for demo app to work.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:' + leakCanaryVersion debugImplementation 'com.squareup.leakcanary:leakcanary-android:' + leakCanaryVersion

View File

@ -23,6 +23,10 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -30,11 +34,16 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectable
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -44,6 +53,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -53,9 +63,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.Effect
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.common.util.Util.SDK_INT import androidx.media3.common.util.Util.SDK_INT
import androidx.media3.effect.Contrast
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -72,6 +84,7 @@ class EffectActivity : ComponentActivity() {
setContent { EffectDemo(playlistHolderList.value) } setContent { EffectDemo(playlistHolderList.value) }
} }
@OptIn(UnstableApi::class)
@Composable @Composable
private fun EffectDemo(playlistHolderList: List<PlaylistHolder>) { private fun EffectDemo(playlistHolderList: List<PlaylistHolder>) {
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
@ -80,6 +93,7 @@ class EffectActivity : ComponentActivity() {
val exoPlayer by remember { val exoPlayer by remember {
mutableStateOf(ExoPlayer.Builder(context).build().apply { playWhenReady = true }) mutableStateOf(ExoPlayer.Builder(context).build().apply { playWhenReady = true })
} }
var effectsEnabled by remember { mutableStateOf(false) }
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@ -96,16 +110,22 @@ class EffectActivity : ComponentActivity() {
coroutineScope.launch { snackbarHostState.showSnackbar(message) } coroutineScope.launch { snackbarHostState.showSnackbar(message) }
}, },
) { mediaItems -> ) { mediaItems ->
effectsEnabled = true
exoPlayer.apply { exoPlayer.apply {
setMediaItems(mediaItems) setMediaItems(mediaItems)
setVideoEffects(emptyList())
prepare() prepare()
} }
} }
PlayerScreen(exoPlayer) PlayerScreen(exoPlayer)
Effects( EffectControls(
onException = { message -> effectsEnabled,
coroutineScope.launch { snackbarHostState.showSnackbar(message) } onApplyEffectsClicked = { videoEffects ->
} exoPlayer.apply {
setVideoEffects(videoEffects)
prepare()
}
},
) )
} }
} }
@ -120,8 +140,8 @@ class EffectActivity : ComponentActivity() {
var showPresetInputChooser by remember { mutableStateOf(false) } var showPresetInputChooser by remember { mutableStateOf(false) }
var showLocalFileChooser by remember { mutableStateOf(false) } var showLocalFileChooser by remember { mutableStateOf(false) }
Row( Row(
modifier = Modifier.padding(vertical = dimensionResource(id = R.dimen.small_padding)), Modifier.padding(vertical = dimensionResource(id = R.dimen.regular_padding)),
horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.small_padding)), horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.regular_padding)),
) { ) {
Button(onClick = { showPresetInputChooser = true }) { Button(onClick = { showPresetInputChooser = true }) {
Text(text = stringResource(id = R.string.choose_preset_input)) Text(text = stringResource(id = R.string.choose_preset_input))
@ -245,17 +265,125 @@ class EffectActivity : ComponentActivity() {
factory = { PlayerView(context).apply { player = exoPlayer } }, factory = { PlayerView(context).apply { player = exoPlayer } },
modifier = modifier =
Modifier.height(dimensionResource(id = R.dimen.android_view_height)) Modifier.height(dimensionResource(id = R.dimen.android_view_height))
.padding(all = dimensionResource(id = R.dimen.small_padding)), .padding(all = dimensionResource(id = R.dimen.regular_padding)),
) )
} }
@OptIn(UnstableApi::class)
@Composable @Composable
private fun Effects(onException: (String) -> Unit) { private fun EffectControls(enabled: Boolean, onApplyEffectsClicked: (List<Effect>) -> Unit) {
Button(onClick = { onException("Button is not yet implemented.") }) { var effectControlsState by remember { mutableStateOf(EffectControlsState()) }
Button(
enabled = enabled && effectControlsState.effectsChanged,
onClick = {
val effectsList = mutableListOf<Effect>()
effectsList += Contrast(effectControlsState.contrastValue)
onApplyEffectsClicked(effectsList)
effectControlsState = effectControlsState.copy(effectsChanged = false)
},
) {
Text(text = stringResource(id = R.string.apply_effects)) Text(text = stringResource(id = R.string.apply_effects))
} }
EffectControlsList(enabled, effectControlsState) { newEffectControlsState ->
effectControlsState = newEffectControlsState
}
} }
@Composable
private fun EffectControlsList(
enabled: Boolean,
effectControlsState: EffectControlsState,
onEffectControlsStateChange: (EffectControlsState) -> Unit,
) {
LazyColumn(Modifier.padding(vertical = dimensionResource(id = R.dimen.small_padding))) {
item {
EffectItem(
name = stringResource(id = R.string.contrast),
enabled = enabled,
onCheckedChange = {
onEffectControlsStateChange(
effectControlsState.copy(effectsChanged = true, contrastValue = 0f)
)
},
) {
Row {
Text(
text = "%.2f".format(effectControlsState.contrastValue),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(dimensionResource(id = R.dimen.large_padding)).weight(1f),
)
Slider(
value = effectControlsState.contrastValue,
onValueChange = { newContrastValue ->
val newRoundedContrastValue = "%.2f".format(newContrastValue).toFloat()
onEffectControlsStateChange(
effectControlsState.copy(
effectsChanged = true,
contrastValue = newRoundedContrastValue,
)
)
},
valueRange = -1f..1f,
modifier = Modifier.weight(4f),
)
}
}
}
}
}
@Composable
fun EffectItem(
name: String,
enabled: Boolean,
onCheckedChange: (Boolean) -> Unit = {},
content: @Composable () -> Unit = {},
) {
var checked by rememberSaveable { mutableStateOf(false) }
Card(
modifier =
Modifier.padding(
vertical = dimensionResource(id = R.dimen.small_padding),
horizontal = dimensionResource(id = R.dimen.regular_padding),
)
.clickable(enabled = enabled && !checked) {
checked = !checked
onCheckedChange(checked)
}
) {
Column(
Modifier.padding(dimensionResource(id = R.dimen.large_padding))
.animateContentSize(animationSpec = tween(durationMillis = 200, easing = LinearEasing))
) {
Row {
Column(Modifier.weight(1f).padding(dimensionResource(id = R.dimen.large_padding))) {
Text(text = name, style = MaterialTheme.typography.bodyLarge)
}
Checkbox(
enabled = enabled,
checked = checked,
onCheckedChange = {
checked = !checked
onCheckedChange(checked)
},
)
}
if (checked) {
content()
}
}
}
}
data class EffectControlsState(
val effectsChanged: Boolean = false,
val contrastValue: Float = 0f,
)
companion object { companion object {
const val JSON_FILENAME = "media.playlist.json" const val JSON_FILENAME = "media.playlist.json"
} }

View File

@ -14,6 +14,8 @@
limitations under the License. limitations under the License.
--> -->
<resources> <resources>
<dimen name="small_padding">8dp</dimen> <dimen name="small_padding">4dp</dimen>
<dimen name="regular_padding">8dp</dimen>
<dimen name="large_padding">12dp</dimen>
<dimen name="android_view_height">256dp</dimen> <dimen name="android_view_height">256dp</dimen>
</resources> </resources>

View File

@ -23,4 +23,5 @@
<string name="no_loaded_playlists_error">There are no loaded preset inputs.</string> <string name="no_loaded_playlists_error">There are no loaded preset inputs.</string>
<string name="can_not_open_file_error">"File couldn't be opened. Please try again."</string> <string name="can_not_open_file_error">"File couldn't be opened. Please try again."</string>
<string name="permission_not_granted_error">"Permission was not granted."</string> <string name="permission_not_granted_error">"Permission was not granted."</string>
<string name="contrast">Contrast</string>
</resources> </resources>