DISC-433: Mute/un-mute controls - #2556
Conversation
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #2556 +/- ##
============================================
+ Coverage 65.15% 65.28% +0.12%
+ Complexity 2564 2563 -1
============================================
Files 405 405
Lines 32467 32686 +219
Branches 4612 4630 +18
============================================
+ Hits 21155 21340 +185
- Misses 8889 8916 +27
- Partials 2423 2430 +7 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds a mute/unmute control to the VideoFeed video player, hoisting mute state to the feed level so the preference persists across swipes and wiring the toggle into existing CTA-click analytics.
Changes:
- Add
isMuted+onMuteToggleAPI toKSVideoPlayer, apply mute viaExoPlayer.volume, and render a glassmorphism mute/unmute button alongside existing controls. - Hoist mute state in
VideoFeedScreenusingrememberSaveable, forward toggle callbacks up toVideoFeedActivityfor analytics. - Extend analytics context values and add/expand Robolectric tests for player behavior, feed UI behavior, and Segment event properties.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| app/src/main/java/com/kickstarter/ui/compose/designsystem/videoplayer/KSVideoPlayer.kt | Adds mute state plumbing, volume application, and a new animated mute/unmute control. |
| app/src/main/java/com/kickstarter/features/videofeed/ui/VideoFeedScreen.kt | Hoists isMuted with rememberSaveable and forwards mute toggles to callers. |
| app/src/main/java/com/kickstarter/features/videofeed/ui/VideoFeedActivity.kt | Tracks mute/unmute as CTA क्लिक events via the existing ViewModel pipeline. |
| app/src/main/java/com/kickstarter/libs/utils/EventContextValues.kt | Adds VIDEO_MUTE / VIDEO_UNMUTE CTA context values. |
| app/src/main/res/values/strings.xml | Adds localized strings for “Mute” / “Unmute”. |
| app/src/main/java/com/kickstarter/ui/compose/designsystem/videoplayer/icons/Icons.kt | Adds Mute and VolumeUp vector assets (and previews). |
| app/src/main/java/com/kickstarter/ui/compose/designsystem/KSDimensions.kt | Introduces size/offset dimensions for positioning the mute control. |
| app/src/test/java/com/kickstarter/ui/compose/designsystem/videoplayer/KSVideoPlayerTest.kt | Adds unit tests for mute button visibility, callback state, and volume changes. |
| app/src/test/java/com/kickstarter/features/videofeed/ui/VideoFeedScreenTest.kt | Adds integration-style tests for mute UI labeling and callback reporting. |
| app/src/test/java/com/kickstarter/libs/SegmentTest.kt | Adds Segment property assertions for mute/unmute CTA-clicked events. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.
Suppressed comments (4)
app/src/main/java/com/kickstarter/ui/compose/designsystem/KSDimensions.kt:104
- These new dimension fields default to
Dp.Unspecified, but they’re used directly inModifier.size(...)/Modifier.offset(...)(inKSVideoPlayer). If any theme/dimension preset instantiatesKSDimensionswithout explicitly setting these, it can lead to invalid layout values at runtime. Prefer providing safe non-Unspecifieddefaults (matchingKSStandardDimensions), or ensure everyKSDimensionspreset in the codebase sets these fields.
val videoFeedCloseButtonSize: Dp = Dp.Unspecified,
val videoFeedCloseButtonTopPadding: Dp = Dp.Unspecified,
val videoFeedHideUiIconSize: Dp = Dp.Unspecified,
val videoFeedMuteButtonSize: Dp = Dp.Unspecified,
val videoFeedMuteButtonCenterOffset: Dp = Dp.Unspecified,
app/src/main/java/com/kickstarter/ui/compose/designsystem/videoplayer/KSVideoPlayer.kt:324
- Unmuting forces the player's volume to
1f, which overwrites any pre-configured/custom player volume (e.g., if the caller set a different volume for mixing). Consider capturing the player's previous volume when muting and restoring it on unmute (or accepting a caller-providedunmutedVolume), so mute toggling doesn’t unexpectedly change intended volume levels.
LaunchedEffect(exoPlayer, isMuted) {
exoPlayer.volume = if (isMuted) 0f else 1f
}
app/src/main/java/com/kickstarter/ui/compose/designsystem/videoplayer/KSVideoPlayer.kt:699
- The control is a toggle, but semantics only expose an action label (“Mute”/“Unmute”). For assistive tech, it’s typically clearer to expose both control identity and current state (e.g.,
stateDescriptionof “Muted/Unmuted”, or use toggle semantics liketoggleablewith a role) rather than only the action text. This improves screen reader clarity without changing the UI.
val label = stringResource(id = if (isMuted) R.string.fpo_Unmute else R.string.fpo_Mute)
KSControlIcon(
icon = if (isMuted) Mute else VolumeUp,
size = dimensions.videoFeedMuteButtonSize,
onClick = onClick,
hazeState = hazeState,
contentDescription = label,
onClickLabel = label,
modifier = Modifier.testTag(KSVideoPlayerTestTag.VIDEO_PLAYER_MUTE_BUTTON.name)
)
app/src/test/java/com/kickstarter/features/videofeed/ui/VideoFeedScreenTest.kt:862
- These assertions hardcode the English accessibility labels ("Mute"/"Unmute"). Since the implementation uses string resources, tests will become locale-sensitive (and can fail if the test environment runs under a non-English locale or if strings change). Prefer fetching the expected strings from resources (e.g., via a context) and asserting against those values.
composeTestRule.onNodeWithContentDescription("Mute", useUnmergedTree = true)
.assertDoesNotExist()
// - Tapping the video reveals the controls; videos start unmuted, so the action is "Mute".
composeTestRule.onNodeWithTag(KSVideoPlayerTestTag.VIDEO_PLAYER_SURFACE.name).performClick()
composeTestRule.waitForIdle()
composeTestRule.onNodeWithContentDescription("Mute", useUnmergedTree = true)
.assertIsDisplayed()
.performClick()
composeTestRule.waitForIdle()
// - After tapping it is muted, so the button's action becomes "Unmute".
composeTestRule.onNodeWithContentDescription("Unmute", useUnmergedTree = true)
.assertIsDisplayed()
}
📲 What
Adds a mute / unmute control to the VideoFeed player. Each video now shows a circular mute toggle just below the play button; tapping it silences or restores the video's audio. The choice persists as you scroll through the feed until you change it, and every toggle is tracked as an analytics event.
🤔 Why
The VideoFeed autoplays videos, but until now there was no way for a user to control audio — videos always played at full volume with no escape hatch, and no way to turn sound on if they wanted it. A mute toggle is table-stakes for a vertical video feed (TikTok/Reels-style) and was called for in the Mobile Video Discovery designs. We also want to measure how often people mute/unmute to inform future audio defaults.
🛠 How
State — a single isMuted flag is hoisted in VideoFeedScreen above the pager, so it's shared across all pages and survives configuration changes. Default is unmuted. It's passed into every page's KSVideoPlayer.
Player — KSVideoPlayer gains isMuted / onMuteToggle(isMuted) params. A LaunchedEffect(exoPlayer, isMuted) applies exoPlayer.volume = if (isMuted) 0f else 1f, keyed on the player too so a freshly created player after a swipe inherits the current preference.
UI — a glassmorphism centered just below the play control. The icon reflects the current state. It appears and disappears in lockstep with the play/pause control (same showControls flag, same fade+scale transition), so the two read as one controls cluster. the Mute/VolumeUp vector icons were added to Icons.kt;
Analytics — mirrors the existing play/pause pattern. Two new CtaContextName values (video_mute, video_unmute);
trackVideoFeedCTAClickedfires a single CTA Clicked event with context_page = video_feed, context_cta = video_mute | video_unmute, and the project/video ids.Analytics ID fix (all CTAs) — while wiring this up, review surfaced that
trackVideoFeedCTAClickedwas reportingvideo_feed_video_id = project.id(), inconsistent with the impression / progress-bar events which use the video's ownitem.videoId. Fixed by threading the realvideoIdthrough the CTA pipeline (VideoFeedScreen → VideoFeedActivity → VideoFeedViewModel.onCTAClicked → AnalyticEvents). This correctsvideo_feed_video_idfor every VideoFeed CTA event — play/pause, save, creator, share, and the new mute/unmute — not just this mute/unmute feature.Tests:
👀 See
mute.mp4
| --- | --- |
| | |
📋 QA
Story 📖
DISC-443