Merge pull request #6830 from element-hq/feature/bma/a11y/videoPlayer
[a11y] Improve accessibility of video and audio player
This commit is contained in:
commit
aa3427381c
4 changed files with 56 additions and 13 deletions
|
|
@ -50,6 +50,7 @@ dependencies {
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.matrixmedia.api)
|
implementation(projects.libraries.matrixmedia.api)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
|
implementation(projects.libraries.uiUtils)
|
||||||
implementation(projects.libraries.voiceplayer.api)
|
implementation(projects.libraries.voiceplayer.api)
|
||||||
implementation(projects.services.toolbox.api)
|
implementation(projects.services.toolbox.api)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,9 @@ 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
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.semantics.stateDescription
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -100,36 +103,54 @@ fun MediaPlayerControllerView(
|
||||||
contentColor = ElementTheme.colors.iconOnSolidPrimary,
|
contentColor = ElementTheme.colors.iconOnSolidPrimary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val a11yPause = stringResource(CommonStrings.a11y_pause)
|
||||||
|
val a11yPlay = stringResource(CommonStrings.a11y_play)
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(36.dp),
|
.size(36.dp)
|
||||||
|
.semantics {
|
||||||
|
stateDescription = if (state.isPlaying) a11yPause else a11yPlay
|
||||||
|
},
|
||||||
onClick = onTogglePlay,
|
onClick = onTogglePlay,
|
||||||
colors = colors,
|
colors = colors,
|
||||||
) {
|
) {
|
||||||
if (state.isPlaying) {
|
if (state.isPlaying) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CompoundIcons.PauseSolid(),
|
imageVector = CompoundIcons.PauseSolid(),
|
||||||
contentDescription = stringResource(CommonStrings.a11y_pause)
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CompoundIcons.PlaySolid(),
|
imageVector = CompoundIcons.PlaySolid(),
|
||||||
contentDescription = stringResource(CommonStrings.a11y_play)
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val position = state.displayProgressInMillis.toHumanReadableDuration()
|
||||||
|
val a11yPosition = stringResource(CommonStrings.a11y_position, position)
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.widthIn(min = 48.dp)
|
.widthIn(min = 48.dp)
|
||||||
.padding(horizontal = 8.dp),
|
.padding(horizontal = 8.dp)
|
||||||
text = state.displayProgressInMillis.toHumanReadableDuration(),
|
.semantics {
|
||||||
|
contentDescription = a11yPosition
|
||||||
|
},
|
||||||
|
text = position,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
color = ElementTheme.colors.textPrimary,
|
color = ElementTheme.colors.textPrimary,
|
||||||
style = ElementTheme.typography.fontBodyXsMedium,
|
style = ElementTheme.typography.fontBodyXsMedium,
|
||||||
)
|
)
|
||||||
var lastSelectedValue by remember { mutableFloatStateOf(-1f) }
|
var lastSelectedValue by remember { mutableFloatStateOf(-1f) }
|
||||||
Slider(
|
Slider(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.semantics {
|
||||||
|
// Speak out a progress percent instead of milliseconds
|
||||||
|
stateDescription = buildString {
|
||||||
|
append((state.progressAsFloat * 100).toInt())
|
||||||
|
append("%")
|
||||||
|
}
|
||||||
|
},
|
||||||
valueRange = 0f..state.durationInMillis.toFloat(),
|
valueRange = 0f..state.durationInMillis.toFloat(),
|
||||||
value = lastSelectedValue.takeIf { it >= 0 }
|
value = lastSelectedValue.takeIf { it >= 0 }
|
||||||
?: state.seekingToMillis?.toFloat()
|
?: state.seekingToMillis?.toFloat()
|
||||||
|
|
@ -146,30 +167,40 @@ fun MediaPlayerControllerView(
|
||||||
val formattedDuration = remember(state.durationInMillis) {
|
val formattedDuration = remember(state.durationInMillis) {
|
||||||
state.durationInMillis.toHumanReadableDuration()
|
state.durationInMillis.toHumanReadableDuration()
|
||||||
}
|
}
|
||||||
|
val a11yDuration = stringResource(CommonStrings.a11y_duration, formattedDuration)
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.widthIn(min = 48.dp)
|
.widthIn(min = 48.dp)
|
||||||
.padding(horizontal = 8.dp),
|
.padding(horizontal = 8.dp)
|
||||||
|
.semantics {
|
||||||
|
contentDescription = a11yDuration
|
||||||
|
},
|
||||||
text = formattedDuration,
|
text = formattedDuration,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
color = ElementTheme.colors.textPrimary,
|
color = ElementTheme.colors.textPrimary,
|
||||||
style = ElementTheme.typography.fontBodyXsMedium,
|
style = ElementTheme.typography.fontBodyXsMedium,
|
||||||
)
|
)
|
||||||
if (state.canMute) {
|
if (state.canMute) {
|
||||||
|
val a11yUnmute = stringResource(CommonStrings.common_unmute)
|
||||||
|
val a11yMute = stringResource(CommonStrings.common_mute)
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onToggleMute,
|
onClick = onToggleMute,
|
||||||
|
modifier = Modifier
|
||||||
|
.semantics {
|
||||||
|
stateDescription = if (state.isMuted) a11yUnmute else a11yMute
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
if (state.isMuted) {
|
if (state.isMuted) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CompoundIcons.VolumeOffSolid(),
|
imageVector = CompoundIcons.VolumeOffSolid(),
|
||||||
tint = ElementTheme.colors.iconPrimary,
|
tint = ElementTheme.colors.iconPrimary,
|
||||||
contentDescription = stringResource(CommonStrings.common_unmute)
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CompoundIcons.VolumeOnSolid(),
|
imageVector = CompoundIcons.VolumeOnSolid(),
|
||||||
tint = ElementTheme.colors.iconPrimary,
|
tint = ElementTheme.colors.iconPrimary,
|
||||||
contentDescription = stringResource(CommonStrings.common_mute)
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ import io.element.android.libraries.mediaviewer.impl.local.player.rememberExoPla
|
||||||
import io.element.android.libraries.mediaviewer.impl.local.player.seekToEnsurePlaying
|
import io.element.android.libraries.mediaviewer.impl.local.player.seekToEnsurePlaying
|
||||||
import io.element.android.libraries.mediaviewer.impl.local.player.togglePlay
|
import io.element.android.libraries.mediaviewer.impl.local.player.togglePlay
|
||||||
import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState
|
import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState
|
||||||
|
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import me.saket.telephoto.zoomable.zoomable
|
import me.saket.telephoto.zoomable.zoomable
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -162,12 +163,20 @@ private fun ExoPlayerMediaVideoView(
|
||||||
|
|
||||||
var autoHideController by remember { mutableIntStateOf(0) }
|
var autoHideController by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
LaunchedEffect(autoHideController) {
|
val isTalkbackActive = isTalkbackActive()
|
||||||
delay(5.seconds)
|
LaunchedEffect(autoHideController, isTalkbackActive) {
|
||||||
if (exoPlayer.isPlaying) {
|
if (isTalkbackActive) {
|
||||||
|
// Ensure that the controller is always visible when talkback is active
|
||||||
mediaPlayerControllerState = mediaPlayerControllerState.copy(
|
mediaPlayerControllerState = mediaPlayerControllerState.copy(
|
||||||
isVisible = false,
|
isVisible = true,
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
delay(5.seconds)
|
||||||
|
if (exoPlayer.isPlaying) {
|
||||||
|
mediaPlayerControllerState = mediaPlayerControllerState.copy(
|
||||||
|
isVisible = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<item quantity="one">"%1$d digit entered"</item>
|
<item quantity="one">"%1$d digit entered"</item>
|
||||||
<item quantity="other">"%1$d digits entered"</item>
|
<item quantity="other">"%1$d digits entered"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="a11y_duration">"Duration: %1$s"</string>
|
||||||
<string name="a11y_edit_avatar">"Edit avatar"</string>
|
<string name="a11y_edit_avatar">"Edit avatar"</string>
|
||||||
<string name="a11y_edit_room_address_hint">"The full address will be %1$s"</string>
|
<string name="a11y_edit_room_address_hint">"The full address will be %1$s"</string>
|
||||||
<string name="a11y_encryption_details">"Encryption details"</string>
|
<string name="a11y_encryption_details">"Encryption details"</string>
|
||||||
|
|
@ -33,6 +34,7 @@
|
||||||
<string name="a11y_playback_speed">"Playback speed"</string>
|
<string name="a11y_playback_speed">"Playback speed"</string>
|
||||||
<string name="a11y_poll">"Poll"</string>
|
<string name="a11y_poll">"Poll"</string>
|
||||||
<string name="a11y_poll_end">"Ended poll"</string>
|
<string name="a11y_poll_end">"Ended poll"</string>
|
||||||
|
<string name="a11y_position">"Position: %1$s"</string>
|
||||||
<string name="a11y_qr_code">"QR Code"</string>
|
<string name="a11y_qr_code">"QR Code"</string>
|
||||||
<string name="a11y_react_with">"React with %1$s"</string>
|
<string name="a11y_react_with">"React with %1$s"</string>
|
||||||
<string name="a11y_react_with_other_emojis">"React with other emojis"</string>
|
<string name="a11y_react_with_other_emojis">"React with other emojis"</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue