diff --git a/CHANGELOG.md b/CHANGELOG.md index a2cb0951d..59ca84ec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [Unreleased] + +### Added +* Added `audioSendJitterMs`, `audioReceiveJitterMs`, and `audioRttMs` to `ObservableMetric` for audio quality monitoring +* Added `realtimePlaybackMute()` and `realtimePlaybackUnmute()` to `RealtimeControllerFacade` for muting/unmuting audio output + ## [0.25.3] - 2026-02-06 ### Fixed diff --git a/README.md b/README.md index 5d784e429..3f2c9ff10 100644 --- a/README.md +++ b/README.md @@ -384,7 +384,9 @@ meetingSession.audioVideo.start(audioVideoConfiguration) // starts the audio vid > Note: So far, you've added observers to receive device and session lifecycle events. In the following use cases, you'll use the real-time API methods to send and receive volume indicators and control mute state. -#### Use case 9. Mute and unmute an audio input. +#### Use case 9. Mute and unmute audio input and output. + +To mute and unmute audio input: ```kotlin val muted = meetingSession.audioVideo.realtimeLocalMute() // returns true if muted, false if failed @@ -392,6 +394,14 @@ val muted = meetingSession.audioVideo.realtimeLocalMute() // returns true if mut val unmuted = meetingSession.audioVideo.realtimeLocalUnmute() // returns true if unmuted, false if failed ``` +To mute and unmute audio playback (output): + +```kotlin +val muted = meetingSession.audioVideo.realtimePlaybackMute() // returns true if muted, false if failed + +val unmuted = meetingSession.audioVideo.realtimePlaybackUnmute() // returns true if unmuted, false if failed +``` + #### Use case 10. Add an observer to receive realtime events such as volume changes/signal change/muted status attendees. You can use this to build real-time indicators UI and get them updated for changes delivered by the array. diff --git a/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/audiovideo/DefaultAudioVideoFacade.kt b/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/audiovideo/DefaultAudioVideoFacade.kt index c143dd840..18690dd23 100644 --- a/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/audiovideo/DefaultAudioVideoFacade.kt +++ b/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/audiovideo/DefaultAudioVideoFacade.kt @@ -140,6 +140,14 @@ class DefaultAudioVideoFacade( return realtimeController.realtimeLocalUnmute() } + override fun realtimePlaybackMute(): Boolean { + return realtimeController.realtimePlaybackMute() + } + + override fun realtimePlaybackUnmute(): Boolean { + return realtimeController.realtimePlaybackUnmute() + } + override fun addRealtimeObserver(observer: RealtimeObserver) { realtimeController.addRealtimeObserver(observer) } diff --git a/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/audiovideo/metric/ObservableMetric.kt b/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/audiovideo/metric/ObservableMetric.kt index b574a12b4..bb742dc3f 100644 --- a/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/audiovideo/metric/ObservableMetric.kt +++ b/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/audiovideo/metric/ObservableMetric.kt @@ -20,6 +20,21 @@ enum class ObservableMetric { */ audioSendPacketLossPercent, + /** + * Upstream audio jitter (ms) + */ + audioSendJitterMs, + + /** + * Downstream audio jitter (ms) + */ + audioReceiveJitterMs, + + /** + * Audio round-trip time (ms) between client and server + */ + audioRttMs, + /** * Estimated uplink bandwidth from perspective of video client */ diff --git a/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/internal/audio/AudioClientController.kt b/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/internal/audio/AudioClientController.kt index 0615b8dcd..d83de5fdf 100644 --- a/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/internal/audio/AudioClientController.kt +++ b/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/internal/audio/AudioClientController.kt @@ -38,6 +38,7 @@ interface AudioClientController { fun stop() fun setMute(isMuted: Boolean): Boolean + fun setPlaybackMute(isMuted: Boolean): Boolean fun setVoiceFocusEnabled(enabled: Boolean): Boolean fun isVoiceFocusEnabled(): Boolean fun promoteToPrimaryMeeting(credentials: MeetingSessionCredentials, observer: PrimaryMeetingPromotionObserver) diff --git a/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/internal/audio/DefaultAudioClientController.kt b/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/internal/audio/DefaultAudioClientController.kt index 3d427ce19..f75844e20 100644 --- a/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/internal/audio/DefaultAudioClientController.kt +++ b/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/internal/audio/DefaultAudioClientController.kt @@ -325,6 +325,12 @@ class DefaultAudioClientController( ) } + override fun setPlaybackMute(isMuted: Boolean): Boolean { + return audioClientState == AudioClientState.STARTED && AudioClient.AUDIO_CLIENT_OK == audioClient.setSpkMute( + isMuted + ) + } + override fun setVoiceFocusEnabled(enabled: Boolean): Boolean { if (audioClientState == AudioClientState.STARTED) { val result = audioClient.setVoiceFocusNoiseSuppression(enabled) diff --git a/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/internal/metric/DefaultClientMetricsCollector.kt b/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/internal/metric/DefaultClientMetricsCollector.kt index faa9c7682..8b2511500 100644 --- a/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/internal/metric/DefaultClientMetricsCollector.kt +++ b/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/internal/metric/DefaultClientMetricsCollector.kt @@ -29,6 +29,12 @@ class DefaultClientMetricsCollector : metrics[AudioClient.AUDIO_CLIENT_METRIC_POST_JB_SPK_1S_PACKETS_LOST_PERCENT] cachedObservableMetrics[ObservableMetric.audioSendPacketLossPercent] = metrics[AudioClient.AUDIO_SERVER_METRIC_POST_JB_MIC_1S_PACKETS_LOST_PERCENT] + cachedObservableMetrics[ObservableMetric.audioSendJitterMs] = + metrics[AudioClient.AUDIO_SERVER_METRIC_MIC_JITTER_MS] + cachedObservableMetrics[ObservableMetric.audioReceiveJitterMs] = + metrics[AudioClient.AUDIO_CLIENT_METRIC_SPK_JITTER_MS] + cachedObservableMetrics[ObservableMetric.audioRttMs] = + metrics[AudioClient.AUDIO_CLIENT_METRIC_RTT_MS] maybeEmitMetrics() } diff --git a/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/realtime/DefaultRealtimeController.kt b/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/realtime/DefaultRealtimeController.kt index 0c54d6457..523f6759e 100644 --- a/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/realtime/DefaultRealtimeController.kt +++ b/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/realtime/DefaultRealtimeController.kt @@ -27,6 +27,14 @@ class DefaultRealtimeController( return audioClientController.setMute(false) } + override fun realtimePlaybackMute(): Boolean { + return audioClientController.setPlaybackMute(true) + } + + override fun realtimePlaybackUnmute(): Boolean { + return audioClientController.setPlaybackMute(false) + } + override fun addRealtimeObserver(observer: RealtimeObserver) { audioClientObserver.subscribeToRealTimeEvents(observer) } diff --git a/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/realtime/RealtimeControllerFacade.kt b/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/realtime/RealtimeControllerFacade.kt index 145b77c04..3e969bd25 100644 --- a/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/realtime/RealtimeControllerFacade.kt +++ b/amazon-chime-sdk/src/main/java/com/amazonaws/services/chime/sdk/meetings/realtime/RealtimeControllerFacade.kt @@ -35,6 +35,20 @@ interface RealtimeControllerFacade { */ fun realtimeLocalUnmute(): Boolean + /** + * Mutes the audio output (speaker). + * + * @return Boolean whether the mute action succeeded + */ + fun realtimePlaybackMute(): Boolean + + /** + * Unmutes the audio output (speaker). + * + * @return Boolean whether the unmute action succeeded + */ + fun realtimePlaybackUnmute(): Boolean + /** * Subscribes to real time events with an observer * diff --git a/amazon-chime-sdk/src/test/java/com/amazonaws/services/chime/sdk/meetings/realtime/DefaultRealtimeControllerTest.kt b/amazon-chime-sdk/src/test/java/com/amazonaws/services/chime/sdk/meetings/realtime/DefaultRealtimeControllerTest.kt index 0e43f0f5b..95b3ab417 100644 --- a/amazon-chime-sdk/src/test/java/com/amazonaws/services/chime/sdk/meetings/realtime/DefaultRealtimeControllerTest.kt +++ b/amazon-chime-sdk/src/test/java/com/amazonaws/services/chime/sdk/meetings/realtime/DefaultRealtimeControllerTest.kt @@ -108,6 +108,20 @@ class DefaultRealtimeControllerTest { verify { audioClientObserver.unsubscribeFromTranscriptEvent(mockTranscriptEventObserver) } } + @Test + fun `realtimePlaybackMute should call audioClientController setPlaybackMute with true and return the status`() { + every { audioClientController.setPlaybackMute(true) } returns true + assertTrue(realtimeController.realtimePlaybackMute()) + verify { audioClientController.setPlaybackMute(true) } + } + + @Test + fun `realtimePlaybackUnmute should call audioClientController setPlaybackMute with false and return the status`() { + every { audioClientController.setPlaybackMute(false) } returns true + assertTrue(realtimeController.realtimePlaybackUnmute()) + verify { audioClientController.setPlaybackMute(false) } + } + @Test fun `realtimeSetVoiceFocusEnabled(true) should call audioClientController setVoiceFocusEnabled with true and return the status`() { every { audioClientController.setVoiceFocusEnabled(true) } returns true diff --git a/app/src/main/java/com/amazonaws/services/chime/sdkdemo/fragment/MeetingFragment.kt b/app/src/main/java/com/amazonaws/services/chime/sdkdemo/fragment/MeetingFragment.kt index 27095854b..a7412a377 100644 --- a/app/src/main/java/com/amazonaws/services/chime/sdkdemo/fragment/MeetingFragment.kt +++ b/app/src/main/java/com/amazonaws/services/chime/sdkdemo/fragment/MeetingFragment.kt @@ -645,7 +645,8 @@ class MeetingFragment : Fragment(), context?.getString(if (meetingModel.isUsingCpuVideoProcessor) R.string.disable_cpu_filter else R.string.enable_cpu_filter), context?.getString(if (meetingModel.isUsingGpuVideoProcessor) R.string.disable_gpu_filter else R.string.enable_gpu_filter), context?.getString(if (meetingModel.isUsingCameraCaptureSource) R.string.disable_custom_capture_source else R.string.enable_custom_capture_source), - context?.getString(R.string.video_configuration) + context?.getString(R.string.video_configuration), + context?.getString(if (meetingModel.isPlaybackMuted) R.string.unmute_playback else R.string.mute_playback) ) if (inReplicaMeeting()) { additionalToggles.add(context?.getString(R.string.demote_from_primary_meeting)) @@ -662,7 +663,8 @@ class MeetingFragment : Fragment(), 4 -> toggleGpuDemoFilter() 5 -> toggleCustomCaptureSource() 6 -> presentVideoConfigDialog() - 7 -> { // May not be accessible + 7 -> togglePlaybackMute() + 8 -> { // May not be accessible if (inReplicaMeeting()) { demoteFromPrimaryMeeting() } else { @@ -1003,6 +1005,17 @@ class MeetingFragment : Fragment(), } } + private fun togglePlaybackMute() { + val mute = !meetingModel.isPlaybackMuted + val success = if (mute) audioVideo.realtimePlaybackMute() else audioVideo.realtimePlaybackUnmute() + if (success) { + meetingModel.isPlaybackMuted = mute + notifyHandler("Playback ${if (mute) "muted" else "unmuted"}") + } else { + notifyHandler("Failed to ${if (mute) "mute" else "unmute"} playback") + } + } + private fun toggleLiveTranscription(meetingId: String, meetingEndpointUrl: String) { if (meetingModel.isLiveTranscriptionEnabled) { uiScope.launch { diff --git a/app/src/main/java/com/amazonaws/services/chime/sdkdemo/model/MeetingModel.kt b/app/src/main/java/com/amazonaws/services/chime/sdkdemo/model/MeetingModel.kt index f13a09752..46b1284e6 100644 --- a/app/src/main/java/com/amazonaws/services/chime/sdkdemo/model/MeetingModel.kt +++ b/app/src/main/java/com/amazonaws/services/chime/sdkdemo/model/MeetingModel.kt @@ -65,6 +65,7 @@ class MeetingModel : ViewModel() { val currentCaptionIndices = mutableMapOf() var isMuted = false + var isPlaybackMuted = false var isCameraOn = false var isDeviceListDialogOn = false var isAdditionalOptionsDialogOn = false diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6f1b5305e..27986c245 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -87,6 +87,8 @@ Stop sharing screen Turn on Voice Focus (noise suppression) Turn off Voice Focus (noise suppression) + Mute Playback + Unmute Playback Turn on Live Transcription Turn off Live Transcription Turn on flashlight