Composable, связанный с Exoplayer, делает приложение не отвечающимAndroid

Форум для тех, кто программирует под Android
Ответить
Anonymous
 Composable, связанный с Exoplayer, делает приложение не отвечающим

Сообщение Anonymous »

У меня есть собственный ExoPlayer на основе PlayerComponent. Из-за этого все приложение не реагирует на запросы во время воспроизведения. Я думал, что проблема в слишком частых обновлениях состояния позиции, но когда я увеличил период обновления (1 секунда), практически ничего не изменилось. Я предполагаю, что проблема может быть связана со слишком большим количеством событий, обрабатываемых в лямбда-выражении pointerInput. Также мне предложили заменить Canvas на Slider, но в моем случае это не подходит, поскольку в будущем мне придется рисовать более сложную временную шкалу. Можете ли вы мне помочь? Что оказывает наиболее существенное влияние на производительность?

Код: Выделить всё

@Composable
internal fun PlayerControls(
modifier: Modifier = Modifier,
isPlaying: Boolean,
position: Long,
duration: Long,
onPlay: () -> Unit,
onPause: () -> Unit,
onSeek: (Long) -> Unit,
onSeekBack: () -> Unit,
onSeekForward: () -> Unit,
isFullScreen: Boolean = false,
onFullscreen: ((Boolean) -> Unit)? = null,
) {
val coroutineScope = rememberCoroutineScope()

var isInteracting by rememberSaveable {
mutableStateOf(false)
}
val interactionSource = remember { MutableInteractionSource() }

val controlsShowDuration = 3000
var notActiveTimer by rememberSaveable { mutableIntStateOf(0) }
var controlsAlpha by rememberSaveable { mutableFloatStateOf(0f) }
val animatedControlsAlpha by animateFloatAsState(
targetValue = controlsAlpha,
animationSpec = tween(300)
)
LaunchedEffect(notActiveTimer) {
when (notActiveTimer) {
controlsShowDuration -> controlsAlpha = 1f
0 -> controlsAlpha = 0f
}
}
LaunchedEffect(isInteracting) {
if (isInteracting) {
notActiveTimer = controlsShowDuration
} else if (notActiveTimer > 0) {
delay(controlsShowDuration.toLong())
notActiveTimer = 0
}
}
ConstraintLayout(
modifier = modifier
.clickable(
interactionSource = interactionSource,
indication = null
) {}
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent(PointerEventPass.Main)
val change = event.changes.firstOrNull() ?: continue
when {
!change.previousPressed && change.pressed -> { // Pointer down
isInteracting = true
}

change.pressed && change.positionChange() != Offset.Zero -> { // Dragging
isInteracting = true
}

change.previousPressed && !change.pressed -> { // Pointer up or Cancelled
isInteracting = false
}
}
}
}
}
) {
val (seekBack, seekForward, playBtn, seekBar) = createRefs()

/* Left area to seek backward */
/* Right area to seek forward */
if (animatedControlsAlpha > 0) {
/* Play/pause button */
if (duration >= 0) {
Column(
modifier = Modifier
.fillMaxWidth()
.graphicsLayer(alpha = animatedControlsAlpha)
.constrainAs(seekBar) {
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
) {
/* Time presentation */
Box(
modifier = Modifier
.fillMaxWidth()
.height(35.dp)
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent(PointerEventPass.Main)
val change = event.changes.firstOrNull() ?: continue
when {
!change.previousPressed && change.pressed ->  { // Pointer down
isInteracting = true
val newProgress =
(change.position.x / size.width).coerceIn(0f..1f)
val newPosition =
(newProgress * duration).toLong()
onSeek(newPosition)
}

change.pressed && change.positionChange() != Offset.Zero -> { // Dragging
isInteracting = true
val newProgress =
(change.position.x / size.width).coerceIn(0f..1f)
val newPosition =
(newProgress * duration).toLong()
onSeek(newPosition)
}

change.previousPressed && !change.pressed -> { // Pointer up or Cancelled
isInteracting = false
}
}
}
}
}
) {
val primaryColor = MaterialTheme.colorScheme.primary
Canvas(
modifier = Modifier.matchParentSize()
) {
val barHeight = 12f
val width = size.width
val height = size.height
val progress = position.toFloat() / duration
drawRoundRect(
color = Color(200f, 200f, 200f, 0.7f),
topLeft = Offset(0f, height / 2f - barHeight * 3f),
size = Size(width, barHeight),
cornerRadius = CornerRadius(barHeight)
)
drawRoundRect(
color = primaryColor,
topLeft = Offset(0f, height / 2f - barHeight * 3f),
size = Size(width * progress, barHeight),
cornerRadius = CornerRadius(barHeight)
)
drawCircle(
color = primaryColor,
radius = 1.3f * barHeight,
center = Offset(
width * progress,
height / 2f - barHeight * 3f + barHeight / 2
)
)
}
}
}
}
}
}
}
UPD
Оставил контейнер и коробку с холстом, потому что они являются основными подозреваемыми, поскольку вызывают изменения состояния и снижают производительность.
UPD 2
Пока я проверял PlayerComponent, я понял, что проблема может заключаться в перекомпоновке AndroidView для поверхности видео
Ранее мой PlayerComponent выглядел так:

Код: Выделить всё

@OptIn(UnstableApi::class)
@Composable
fun PlayerComponent(
modifier: Modifier = Modifier,
player: Player,
isFullScreen: Boolean = false,
onFullscreen: ((Boolean) -> Unit)? = null,
onRatioObtained: ((Float) ->  Unit)? = null
) {
val context = LocalContext.current
var playerState by rememberSaveable { mutableIntStateOf(player.playbackState) }
var isPlaying by rememberSaveable { mutableStateOf(player.isPlaying) }
var duration by rememberSaveable { mutableLongStateOf(player.duration) }
var position by rememberSaveable { mutableLongStateOf(player.currentPosition) }
var savedPlayState by rememberSaveable { mutableStateOf(player.isPlaying) }
var aspectRatio by rememberSaveable { mutableFloatStateOf(16f / 9) }
Box(
modifier = modifier.background(Color.Black),
contentAlignment = Alignment.Center
) {
AndroidView(
modifier = Modifier.matchParentSize(),
factory = {
PlayerView(it).apply {
useController = false
this.player = player
}
}
)
val seekRange = 5000L
PlayerControls(
modifier = Modifier.matchParentSize(),
position = position,
duration = duration,
isPlaying = isPlaying,
onPlay = player::play,
onPause = player::pause,
onSeekBack = {
val newPosition = (player.currentPosition - seekRange).coerceAtLeast(0)
player.seekTo(newPosition)
},
onSeekForward = {
val newPosition =
(player.currentPosition + seekRange).coerceAtMost(player.duration - 1)
player.seekTo(newPosition)
},
onSeek = player::seekTo,
isFullScreen = isFullScreen,
onFullscreen = onFullscreen
)
}
LaunchedEffect(isPlaying) {
val changePeriod = 250L
while (isActive && isPlaying) {
position = player.currentPosition
val delayDuration = changePeriod - (position % changePeriod)
delay(delayDuration)
}
}
}
Но затем я заменил AndroidView на свой составной VideoSurface и передал () -> Player вместо просто Player

Код: Выделить всё

@Composable
internal fun VideoSurface(
modifier: Modifier = Modifier,
playerProvider: () -> Player
) {
AndroidView(
modifier = modifier,
factory = {
PlayerView(it).apply {
useController = false
this.player = playerProvider()
}
}
)
}
Вся новая настройка следующая:

Код: Выделить всё

@OptIn(UnstableApi::class)
@Composable
fun PlayerComponent(
modifier: Modifier = Modifier,
playerProvider: () -> Player,
isFullScreen: Boolean = false,
onFullscreen: ((Boolean) -> Unit)? = null,
onRatioObtained: ((Float) ->  Unit)? = null
) {
val context = LocalContext.current
val player = retain {
playerProvider()
}
var playerState by rememberSaveable { mutableIntStateOf(player.playbackState) }
var isPlaying by rememberSaveable { mutableStateOf(player.isPlaying) }
var duration by rememberSaveable { mutableLongStateOf(player.duration) }
var position by rememberSaveable { mutableLongStateOf(player.currentPosition) }
var savedPlayState by rememberSaveable { mutableStateOf(player.isPlaying) }
var aspectRatio by rememberSaveable { mutableFloatStateOf(16f / 9) }
Box(
modifier = modifier.background(Color.Black),
contentAlignment = Alignment.Center
) {
VideoSurface(
modifier = Modifier.matchParentSize(),
playerProvider = playerProvider
)
val seekRange = 5000L
PlayerControls(
modifier = Modifier.matchParentSize(),
position = position,
duration = duration,
isPlaying = isPlaying,
onPlay = player::play,
onPause = player::pause,
onSeekBack = {
val newPosition = (player.currentPosition - seekRange).coerceAtLeast(0)
player.seekTo(newPosition)
},
onSeekForward = {
val newPosition =
(player.currentPosition + seekRange).coerceAtMost(player.duration - 1)
player.seekTo(newPosition)
},
onSeek = player::seekTo,
isFullScreen = isFullScreen,
onFullscreen = onFullscreen
)
}
LaunchedEffect(isPlaying) {
val changePeriod = 250L
while (isActive && isPlaying) {
position = player.currentPosition
val delayDuration = changePeriod - (position % changePeriod)
delay(delayDuration)
}
}

}
@Composable
internal fun PlayerControls(
modifier: Modifier = Modifier,
isPlaying: Boolean,
position: Long,
duration: Long,
onPlay: () -> Unit,
onPause: () -> Unit,
onSeek: (Long) -> Unit,
onSeekBack: () -> Unit,
onSeekForward: () -> Unit,
isFullScreen: Boolean = false,
onFullscreen: ((Boolean) -> Unit)? = null,
) {
val coroutineScope = rememberCoroutineScope()

var isInteracting by rememberSaveable {
mutableStateOf(false)
}
val interactionSource = remember { MutableInteractionSource() }

val controlsShowDuration = 3000
var notActiveTimer by rememberSaveable { mutableIntStateOf(0) }
var controlsAlpha by rememberSaveable { mutableFloatStateOf(0f) }
val animatedControlsAlpha by animateFloatAsState(
targetValue = controlsAlpha,
animationSpec = tween(300)
)
LaunchedEffect(notActiveTimer) {
when (notActiveTimer) {
controlsShowDuration -> controlsAlpha = 1f
0 -> controlsAlpha = 0f
}
}
LaunchedEffect(isInteracting) {
if (isInteracting) {
notActiveTimer = controlsShowDuration
} else if (notActiveTimer > 0) {
delay(controlsShowDuration.toLong())
notActiveTimer = 0
}
}
ConstraintLayout(
modifier = modifier
.clickable(
interactionSource = interactionSource,
indication = null
) {}
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent(PointerEventPass.Main)
val change = event.changes.firstOrNull() ?: continue
when {
!change.previousPressed && change.pressed -> { // Pointer down
isInteracting = true
}

change.pressed && change.positionChange() != Offset.Zero -> { // Dragging
isInteracting = true
}

change.previousPressed && !change.pressed ->  { // Pointer up or Cancelled
isInteracting = false
}
}
}
}
}
) {
val (seekBack, seekForward, playBtn, seekBar) = createRefs()

/* Left area to seek backward */
/* Right area to seek forward */
if (animatedControlsAlpha > 0) {
/* Play/pause button */
if (duration >= 0) {
Column(
modifier = Modifier
.fillMaxWidth()
.graphicsLayer(alpha = animatedControlsAlpha)
.constrainAs(seekBar) {
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
) {
/* Time presentation */
Box(
modifier = Modifier
.fillMaxWidth()
.height(35.dp)
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent(PointerEventPass.Main)
val change = event.changes.firstOrNull() ?: continue
when {
!change.previousPressed && change.pressed -> { // Pointer down
isInteracting = true
val newProgress =
(change.position.x / size.width).coerceIn(0f..1f)
val newPosition =
(newProgress * duration).toLong()
onSeek(newPosition)
}

change.pressed && change.positionChange() != Offset.Zero -> { // Dragging
isInteracting = true
val newProgress =
(change.position.x / size.width).coerceIn(0f..1f)
val newPosition =
(newProgress * duration).toLong()
onSeek(newPosition)
}

change.previousPressed && !change.pressed ->  { // Pointer up or Cancelled
isInteracting = false
}
}
}
}
}
) {
val primaryColor = MaterialTheme.colorScheme.primary
Canvas(
modifier = Modifier.matchParentSize()
) {
val barHeight = 12f
val width = size.width
val height = size.height
val progress = position.toFloat() / duration
drawRoundRect(
color = Color(200f, 200f, 200f, 0.7f),
topLeft = Offset(0f, height / 2f - barHeight * 3f),
size = Size(width, barHeight),
cornerRadius = CornerRadius(barHeight)
)
drawRoundRect(
color = primaryColor,
topLeft = Offset(0f, height / 2f - barHeight * 3f),
size = Size(width * progress, barHeight),
cornerRadius = CornerRadius(barHeight)
)
drawCircle(
color = primaryColor,
radius = 1.3f * barHeight,
center = Offset(
width * progress,
height / 2f - barHeight * 3f + barHeight / 2
)
)
}
}
}
}
}
}
}
Пример использования:

Код: Выделить всё

@Composable
fun Demo() {
val context = LocalContext.current
PlayerComponent(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(16f/9),
playerProvider = { ExoPlayer.Builder(context).build() },
onRatioObtained = {},
isFullScreen = false,
onFullscreen = {}
)
}
Хотя сообщение «Приложение не отвечает» сейчас не появляется, я все еще не уверен на 100%, что проблем с производительностью не возникнет в будущем. Более того, иногда кнопка воспроизведения и панель поиска внутри PlayerControls не отображаются при каждом нажатии.


Подробнее здесь: https://stackoverflow.com/questions/798 ... responsive
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «Android»