Skip to content

Commit 0c3c9f4

Browse files
authored
Merge pull request #6830 from element-hq/feature/bma/a11y/videoPlayer
[a11y] Improve accessibility of video and audio player
2 parents a33d717 + 44df2d2 commit 0c3c9f4

4 files changed

Lines changed: 56 additions & 13 deletions

File tree

libraries/mediaviewer/impl/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ dependencies {
5050
implementation(projects.libraries.matrix.api)
5151
implementation(projects.libraries.matrixmedia.api)
5252
implementation(projects.libraries.uiStrings)
53+
implementation(projects.libraries.uiUtils)
5354
implementation(projects.libraries.voiceplayer.api)
5455
implementation(projects.services.toolbox.api)
5556

libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerView.kt

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ import androidx.compose.runtime.setValue
2828
import androidx.compose.ui.Alignment
2929
import androidx.compose.ui.Modifier
3030
import androidx.compose.ui.res.stringResource
31+
import androidx.compose.ui.semantics.contentDescription
32+
import androidx.compose.ui.semantics.semantics
33+
import androidx.compose.ui.semantics.stateDescription
3134
import androidx.compose.ui.text.style.TextAlign
3235
import androidx.compose.ui.tooling.preview.PreviewParameter
3336
import androidx.compose.ui.unit.dp
@@ -100,36 +103,54 @@ fun MediaPlayerControllerView(
100103
contentColor = ElementTheme.colors.iconOnSolidPrimary,
101104
)
102105
}
106+
val a11yPause = stringResource(CommonStrings.a11y_pause)
107+
val a11yPlay = stringResource(CommonStrings.a11y_play)
103108
IconButton(
104109
modifier = Modifier
105-
.size(36.dp),
110+
.size(36.dp)
111+
.semantics {
112+
stateDescription = if (state.isPlaying) a11yPause else a11yPlay
113+
},
106114
onClick = onTogglePlay,
107115
colors = colors,
108116
) {
109117
if (state.isPlaying) {
110118
Icon(
111119
imageVector = CompoundIcons.PauseSolid(),
112-
contentDescription = stringResource(CommonStrings.a11y_pause)
120+
contentDescription = null,
113121
)
114122
} else {
115123
Icon(
116124
imageVector = CompoundIcons.PlaySolid(),
117-
contentDescription = stringResource(CommonStrings.a11y_play)
125+
contentDescription = null,
118126
)
119127
}
120128
}
129+
val position = state.displayProgressInMillis.toHumanReadableDuration()
130+
val a11yPosition = stringResource(CommonStrings.a11y_position, position)
121131
Text(
122132
modifier = Modifier
123133
.widthIn(min = 48.dp)
124-
.padding(horizontal = 8.dp),
125-
text = state.displayProgressInMillis.toHumanReadableDuration(),
134+
.padding(horizontal = 8.dp)
135+
.semantics {
136+
contentDescription = a11yPosition
137+
},
138+
text = position,
126139
textAlign = TextAlign.Center,
127140
color = ElementTheme.colors.textPrimary,
128141
style = ElementTheme.typography.fontBodyXsMedium,
129142
)
130143
var lastSelectedValue by remember { mutableFloatStateOf(-1f) }
131144
Slider(
132-
modifier = Modifier.weight(1f),
145+
modifier = Modifier
146+
.weight(1f)
147+
.semantics {
148+
// Speak out a progress percent instead of milliseconds
149+
stateDescription = buildString {
150+
append((state.progressAsFloat * 100).toInt())
151+
append("%")
152+
}
153+
},
133154
valueRange = 0f..state.durationInMillis.toFloat(),
134155
value = lastSelectedValue.takeIf { it >= 0 }
135156
?: state.seekingToMillis?.toFloat()
@@ -146,30 +167,40 @@ fun MediaPlayerControllerView(
146167
val formattedDuration = remember(state.durationInMillis) {
147168
state.durationInMillis.toHumanReadableDuration()
148169
}
170+
val a11yDuration = stringResource(CommonStrings.a11y_duration, formattedDuration)
149171
Text(
150172
modifier = Modifier
151173
.widthIn(min = 48.dp)
152-
.padding(horizontal = 8.dp),
174+
.padding(horizontal = 8.dp)
175+
.semantics {
176+
contentDescription = a11yDuration
177+
},
153178
text = formattedDuration,
154179
textAlign = TextAlign.Center,
155180
color = ElementTheme.colors.textPrimary,
156181
style = ElementTheme.typography.fontBodyXsMedium,
157182
)
158183
if (state.canMute) {
184+
val a11yUnmute = stringResource(CommonStrings.common_unmute)
185+
val a11yMute = stringResource(CommonStrings.common_mute)
159186
IconButton(
160187
onClick = onToggleMute,
188+
modifier = Modifier
189+
.semantics {
190+
stateDescription = if (state.isMuted) a11yUnmute else a11yMute
191+
},
161192
) {
162193
if (state.isMuted) {
163194
Icon(
164195
imageVector = CompoundIcons.VolumeOffSolid(),
165196
tint = ElementTheme.colors.iconPrimary,
166-
contentDescription = stringResource(CommonStrings.common_unmute)
197+
contentDescription = null,
167198
)
168199
} else {
169200
Icon(
170201
imageVector = CompoundIcons.VolumeOnSolid(),
171202
tint = ElementTheme.colors.iconPrimary,
172-
contentDescription = stringResource(CommonStrings.common_mute)
203+
contentDescription = null,
173204
)
174205
}
175206
}

libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import io.element.android.libraries.mediaviewer.impl.local.player.rememberExoPla
5757
import io.element.android.libraries.mediaviewer.impl.local.player.seekToEnsurePlaying
5858
import io.element.android.libraries.mediaviewer.impl.local.player.togglePlay
5959
import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState
60+
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
6061
import kotlinx.coroutines.delay
6162
import me.saket.telephoto.zoomable.zoomable
6263
import timber.log.Timber
@@ -162,12 +163,20 @@ private fun ExoPlayerMediaVideoView(
162163

163164
var autoHideController by remember { mutableIntStateOf(0) }
164165

165-
LaunchedEffect(autoHideController) {
166-
delay(5.seconds)
167-
if (exoPlayer.isPlaying) {
166+
val isTalkbackActive = isTalkbackActive()
167+
LaunchedEffect(autoHideController, isTalkbackActive) {
168+
if (isTalkbackActive) {
169+
// Ensure that the controller is always visible when talkback is active
168170
mediaPlayerControllerState = mediaPlayerControllerState.copy(
169-
isVisible = false,
171+
isVisible = true,
170172
)
173+
} else {
174+
delay(5.seconds)
175+
if (exoPlayer.isPlaying) {
176+
mediaPlayerControllerState = mediaPlayerControllerState.copy(
177+
isVisible = false,
178+
)
179+
}
171180
}
172181
}
173182

libraries/ui-strings/src/main/res/values/localazy.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<item quantity="one">"%1$d digit entered"</item>
1010
<item quantity="other">"%1$d digits entered"</item>
1111
</plurals>
12+
<string name="a11y_duration">"Duration: %1$s"</string>
1213
<string name="a11y_edit_avatar">"Edit avatar"</string>
1314
<string name="a11y_edit_room_address_hint">"The full address will be %1$s"</string>
1415
<string name="a11y_encryption_details">"Encryption details"</string>
@@ -33,6 +34,7 @@
3334
<string name="a11y_playback_speed">"Playback speed"</string>
3435
<string name="a11y_poll">"Poll"</string>
3536
<string name="a11y_poll_end">"Ended poll"</string>
37+
<string name="a11y_position">"Position: %1$s"</string>
3638
<string name="a11y_qr_code">"QR Code"</string>
3739
<string name="a11y_react_with">"React with %1$s"</string>
3840
<string name="a11y_react_with_other_emojis">"React with other emojis"</string>

0 commit comments

Comments
 (0)