I am using ExoPlayer (Media3) in Android TV with compose, a streaming application, but when Accessibility/Talkback is ON, DPAD events are not being captured at client side, only back/Enter/Center button is getting captured.
In the normal mode i.e. Accessibility/Talkback is OFF all is working as expected.
But some how in Fire TV am getting all dpad event when Accessibility is ON and OFF
Below is the code is to inflate the media3 exoplayer.
@Composable
private fun CustomPlayerView(
modifier: Modifier = Modifier,
areControlsVisible: Boolean,
isSubtitlesVisible: Boolean,
isInTv: Boolean = false,
isInPipMode: Boolean = false,
resizeMode: Int = AspectRatioFrameLayout.RESIZE_MODE_FIT,
getPlayerView: (view: View) -> Unit
) {
val playerManager by rememberLocalPlayer()
AndroidViewBinding(
modifier = modifier,
factory = { inflater, viewGroup, attachToParent ->
val binding = CustomPlayerBinding.inflate(inflater, viewGroup, attachToParent)
binding.apply {
root.isClickable = false
root.isFocusable = false
playerView.run {
[email protected] = resizeMode
[email protected] = playerManager.player
// Show the controls forever
controllerShowTimeoutMs = Int.MAX_VALUE
showController()
}
}
return@AndroidViewBinding binding
}
Player screen code:
CustomPlayerView(
areControlsVisible = controlsUIState.controlUIState is ControlUISate.Show,
isInPipMode = false,
playbackState = playbackState,
onPlayerAction = onPlayerControlsAction,
onInitialize = {
// Initialise player
},
onRelease = { isForceReleasing ->
// Handle release
},
modifier = Modifier
.focusOnInitialVisibility()
.focusable()
.handleDPadKeyEvents(
isSeeking = trickPlayUIState.isSeeking,
onEnter = { _, _ ->
handlePlayBack()
},
onUp = { _, _ ->
//Handle Dpad UP
},
onLeft = { isLongPress, isReleased ->
//Handle Dpad LEFT
},
onRight = { isLongPress, isReleased ->
//Handle Dpad RIGHT
},
onMediaKeyFf = { isLongPress, isReleased ->
//Handle Dpad FF
},
onMediaKeyRw = { isLongPress, isReleased ->
//Handle Dpad RW
},
onKeyDown = { _, _ ->
//Handle Dpad DOWN
},
onMediaKey = {
//Handle Dpad Media Key generic
},
onMediaKeyPlayPause = { _, _ ->
handlePlayBack()
},
shouldConsumeRightEvent = true,
shouldConsumeLefEvent = true,
shouldConsumeEventPlayPauseEvent = true
),
)
Dpad ext. fun
/**
* Handles horizontal (Left & Right) D-Pad Keys and consumes the event(s) so
that the focus doesn't
* accidentally move to another element.
* */
@Composable
fun Modifier.handleDPadKeyEvents(
onLeft: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null,
onRight: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null,
onUp: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null,
onDown: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null,
onEnter: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null,
onKeyDown: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null,
onBack: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null,
onMediaKey: ((keyCode: Int) -> Unit)? = null, // Callback for media keys
onMediaKeyFf: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null,
onMediaKeyRw: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null,
onMediaKeyPlayPause: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null,
shouldConsumeEvent: Boolean = false,
shouldConsumeLefEvent: Boolean = false,
shouldConsumeRightEvent: Boolean = false,
shouldConsumeEventPlayPauseEvent: Boolean = false,
isSeeking: Boolean = false
): Modifier {
val holdThreshold = 5 // Milliseconds for press-and-hold detection
var pressStartTime by remember { mutableIntStateOf(0) }
var isLongPressDetected by remember { mutableStateOf(false) } // Flag to track if long press has been detected
LaunchedEffect(isSeeking) {
// This is required to reset the variables when long press and move focus to another view.
// This case On Action up of previous focussed view is not called, so that blocking us to reset these variable
if (!isSeeking) {
pressStartTime = 0
isLongPressDetected = false
}
}
return this.onPreviewKeyEvent { event ->
val nativeKeyEvent = event.nativeKeyEvent
fun handleKeyAction(block: (isLongPress: Boolean, isReleased: Boolean) -> Unit) {
when (nativeKeyEvent.action) {
KeyEvent.ACTION_UP -> {
block(isLongPressDetected, true)
// Reset variables
pressStartTime = 0
isLongPressDetected = false
}
KeyEvent.ACTION_DOWN -> {
// only for the first press, as we have requirement, like on key down only we need to show controller.
if (pressStartTime == 0) onKeyDown?.invoke(false, false)
pressStartTime += 1
if (pressStartTime > holdThreshold) {
block(true, false)
isLongPressDetected = true // Mark that long press was detected
}
}
}
}
when {
// Handle D-Pad Navigation Keys
DPadEventsKeyCodes.contains(nativeKeyEvent.keyCode) -> {
when (nativeKeyEvent.keyCode) {
KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT -> {
onLeft?.let {
handleKeyAction(it::invoke)
return@onPreviewKeyEvent shouldConsumeLefEvent
}
}
KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT -> {
onRight?.let {
handleKeyAction(it::invoke)
return@onPreviewKeyEvent shouldConsumeRightEvent
}
}
KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP -> {
onUp?.let { handleKeyAction(it::invoke) }
}
KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN -> {
onDown?.let { handleKeyAction(it::invoke) }
}
KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER -> {
onEnter?.let { handleKeyAction(it::invoke) }
}
}
shouldConsumeEvent
}
// Handle Media Key Events, FF/RW for media keys, rest will be handled by system .
MediaKeyCodes.contains(nativeKeyEvent.keyCode) -> {
when (nativeKeyEvent.keyCode) {
KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
onMediaKeyFf?.let {
handleKeyAction(it::invoke)
return@onPreviewKeyEvent true
}
}
KeyEvent.KEYCODE_MEDIA_REWIND -> {
onMediaKeyRw?.let {
handleKeyAction(it::invoke)
return@onPreviewKeyEvent true
}
}
// Handle Dedicated Media button Play/Pause at client side
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_MEDIA_PAUSE -> {
onMediaKeyPlayPause?.let {
handleKeyAction(it::invoke)
return@onPreviewKeyEvent shouldConsumeEventPlayPauseEvent
}
}
else -> {
onMediaKey?.invoke(nativeKeyEvent.keyCode) // Invoke media key callback
}
}
shouldConsumeEvent
}
// Handle System Key Events, we may need to handle like handleKeyAction(onMediaKey?.invoke) for system keys.
SystemKeyCodes.contains(nativeKeyEvent.keyCode) && nativeKeyEvent.action == KeyEvent.ACTION_DOWN -> {
when (nativeKeyEvent.keyCode) {
KeyEvent.KEYCODE_BACK -> onBack?.invoke(false, false)
else -> onKeyDown?.invoke(false, false)
}
shouldConsumeEvent
}
else -> shouldConsumeEvent
}
}
}
Initial focus ext. fun
/**
* This modifier can be used to gain focus on a focusable component when it becomes
visible
* for the first time.
* */
@Composable
fun Modifier.focusOnInitialVisibility(
isVisible: MutableState<Boolean> = remember { mutableStateOf(false) }
): Modifier {
val focusRequester = remember { FocusRequester() }
return focusRequester(focusRequester)
.onGloballyPositioned {
if (!isVisible.value) {
focusRequester.requestFocus()
isVisible.value = true
}
}
}
Does anyone have any idea how can I properly handle those events? The controller doesn't show up on top of the video due to this and can't perform any events such as play/pause/seek, etc.
Any help will be greatly appreciated.
Thank you!.