Skip to content

Commit 46d5363

Browse files
Add support for individual, raw, and composite call recordings (#1610)
* feature: Introduce new recording types raw and individual, the existing default one is now named as composite. Details: https://linear.app/stream/issue/AND-1028/add-support-for-new-call-recording-types-individual-raw-composite-in * chore: Improve menu items for demo-app * chore: fix breaking changes * chore: update tests * chore: revert commit * chore: revert ui changes * fix: recording state only false when all recordings inactive Previously, stopping any recording type would set recording=false even if other recording types were still active. Now recording remains true as long as any recording type (composite, individual, or raw) is active. * test: cleanup recording state to prevent test pollution The 'test recording types are independent' test was leaving individualRecording and rawRecording as true, which polluted subsequent tests that share the same call object. --------- Co-authored-by: Aleksandar Apostolov <apostolov.alexandar@gmail.com>
1 parent 78429c6 commit 46d5363

360 files changed

Lines changed: 3941 additions & 522 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import androidx.compose.material.rememberModalBottomSheetState
5454
import androidx.compose.runtime.Composable
5555
import androidx.compose.runtime.LaunchedEffect
5656
import androidx.compose.runtime.collectAsState
57+
import androidx.compose.runtime.derivedStateOf
5758
import androidx.compose.runtime.getValue
5859
import androidx.compose.runtime.mutableIntStateOf
5960
import androidx.compose.runtime.mutableStateListOf
@@ -118,6 +119,7 @@ import io.getstream.video.android.core.RealtimeConnection
118119
import io.getstream.video.android.core.call.state.ChooseLayout
119120
import io.getstream.video.android.core.model.PreferredVideoResolution
120121
import io.getstream.video.android.core.pip.PictureInPictureConfiguration
122+
import io.getstream.video.android.core.recording.RecordingType
121123
import io.getstream.video.android.core.utils.isEnabled
122124
import io.getstream.video.android.filters.video.BlurredBackgroundVideoFilter
123125
import io.getstream.video.android.filters.video.VirtualBackgroundVideoFilter
@@ -174,7 +176,9 @@ fun CallScreen(
174176
val orientation = LocalConfiguration.current.orientation
175177
var showEndRecordingDialog by remember { mutableStateOf(false) }
176178
var acceptedCallRecording by remember { mutableStateOf(false) }
177-
val isRecording by call.state.recording.collectAsStateWithLifecycle()
179+
val compositeRecording by call.state.recording.collectAsStateWithLifecycle()
180+
val rawRecording by call.state.rawRecording.collectAsStateWithLifecycle()
181+
val individualRecording by call.state.individualRecording.collectAsStateWithLifecycle()
178182
val participantsSize by call.state.participants.collectAsStateWithLifecycle()
179183
val messages: MutableList<MessageItemState> = remember { mutableStateListOf() }
180184
var messagesVisibility by remember { mutableStateOf(false) }
@@ -789,7 +793,15 @@ fun CallScreen(
789793
onDismissed = { isShowingAvailableDeviceMenu = false },
790794
)
791795
}
792-
796+
val isRecording by remember {
797+
derivedStateOf {
798+
buildSet {
799+
if (compositeRecording) add(RecordingType.Composite)
800+
if (individualRecording) add(RecordingType.Individual)
801+
if (rawRecording) add(RecordingType.Raw)
802+
}.isNotEmpty()
803+
}
804+
}
793805
// TODO: AAP, move recording and actions in separate composables.
794806
if (isRecording && !showRecordingWarning) {
795807
StreamDialogPositiveNegative(

demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ import androidx.compose.material.icons.filled.ClosedCaptionOff
3131
import androidx.compose.material.icons.filled.Crop
3232
import androidx.compose.material.icons.filled.CropFree
3333
import androidx.compose.material.icons.filled.Feedback
34+
import androidx.compose.material.icons.filled.Person
3435
import androidx.compose.material.icons.filled.RadioButtonChecked
36+
import androidx.compose.material.icons.filled.RawOff
37+
import androidx.compose.material.icons.filled.RawOn
3538
import androidx.compose.material.icons.filled.Replay
3639
import androidx.compose.material.icons.filled.RestartAlt
3740
import androidx.compose.material.icons.filled.SettingsBackupRestore
@@ -46,6 +49,7 @@ import androidx.compose.material.icons.filled.VideocamOff
4649
import io.getstream.video.android.compose.ui.components.video.VideoScalingType
4750
import io.getstream.video.android.core.audio.StreamAudioDevice
4851
import io.getstream.video.android.core.model.PreferredVideoResolution
52+
import io.getstream.video.android.core.recording.RecordingType
4953
import io.getstream.video.android.ui.closedcaptions.ClosedCaptionUiState
5054
import io.getstream.video.android.ui.menu.base.ActionMenuItem
5155
import io.getstream.video.android.ui.menu.base.DynamicSubMenuItem
@@ -89,6 +93,8 @@ fun defaultStreamMenu(
8993
audioDeviceUiStateList: List<AudioDeviceUiState> = emptyList(),
9094
audioUsageUiState: AudioUsageUiState = AudioUsageVoiceCommunicationUiState,
9195
onToggleAudioUsage: () -> Unit = {},
96+
selectedRecordingTypes: Set<RecordingType> = emptySet(),
97+
onSelectRecordingType: (RecordingType) -> Unit = {},
9298
) = buildList<MenuItem> {
9399
if (noiseCancellationFeatureEnabled) {
94100
add(
@@ -165,6 +171,8 @@ fun defaultStreamMenu(
165171
loadTranscriptions,
166172
audioUsageUiState,
167173
onToggleAudioUsage,
174+
selectedRecordingTypes,
175+
onSelectRecordingType,
168176
),
169177
),
170178
)
@@ -272,6 +280,51 @@ fun scaleTypeMenu(onSelectScaleType: (VideoScalingType) -> Unit): List<MenuItem>
272280
),
273281
)
274282

283+
fun recordingTypeMenu(onSelectRecording: (RecordingType) -> Unit, selectedRecordingTypes: Set<RecordingType>): List<MenuItem> {
284+
return arrayListOf(
285+
ActionMenuItem(
286+
title = if (selectedRecordingTypes.contains(RecordingType.Raw)) {
287+
"Stop raw recording"
288+
} else {
289+
"Start raw recording"
290+
},
291+
icon = if (selectedRecordingTypes.contains(RecordingType.Raw)) {
292+
Icons.Default.RawOn
293+
} else {
294+
Icons.Default.RawOff
295+
},
296+
highlight = selectedRecordingTypes.contains(RecordingType.Raw),
297+
action = { onSelectRecording(RecordingType.Raw) },
298+
),
299+
ActionMenuItem(
300+
title = if (selectedRecordingTypes.contains(
301+
RecordingType.Individual,
302+
)
303+
) {
304+
"Stop individual recording"
305+
} else {
306+
"Start individual recording"
307+
},
308+
icon = Icons.Default.Person,
309+
highlight = selectedRecordingTypes.contains(RecordingType.Individual),
310+
action = { onSelectRecording(RecordingType.Individual) },
311+
),
312+
ActionMenuItem(
313+
title = if (selectedRecordingTypes.contains(
314+
RecordingType.Composite,
315+
)
316+
) {
317+
"Stop composite recording"
318+
} else {
319+
"Start composite recording"
320+
},
321+
icon = Icons.Default.CropFree,
322+
highlight = selectedRecordingTypes.contains(RecordingType.Composite),
323+
action = { onSelectRecording(RecordingType.Composite) },
324+
),
325+
)
326+
}
327+
275328
/**
276329
* Optionally defines the debug sub-menu of the demo app.
277330
*/
@@ -294,6 +347,8 @@ fun debugSubmenu(
294347
loadTranscriptions: suspend () -> List<MenuItem>,
295348
audioUsageUiState: AudioUsageUiState,
296349
onToggleAudioUsage: () -> Unit,
350+
selectedRecordingTypes: Set<RecordingType>,
351+
onSelectRecordingType: (RecordingType) -> Unit,
297352
) = listOf(
298353
DynamicSubMenuItem(
299354
title = "List Transcriptions",
@@ -380,18 +435,10 @@ fun debugSubmenu(
380435
highlight = audioUsageUiState.highlight,
381436
action = onToggleAudioUsage,
382437
),
383-
ActionMenuItem(
438+
SubMenuItem(
384439
title = "Start/stop recording",
385440
icon = Icons.Default.RadioButtonChecked,
386-
action = {
387-
// scope.launch {
388-
// if (isRecording) {
389-
// showEndRecordingDialog = true
390-
// } else {
391-
// call.startRecording()
392-
// }
393-
// }
394-
},
441+
items = recordingTypeMenu(onSelectRecordingType, selectedRecordingTypes),
395442
),
396443
DynamicSubMenuItem(
397444
title = "Recordings",

demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ import androidx.compose.material.icons.filled.SpeakerPhone
4040
import androidx.compose.material.icons.filled.VideoFile
4141
import androidx.compose.runtime.Composable
4242
import androidx.compose.runtime.LaunchedEffect
43+
import androidx.compose.runtime.derivedStateOf
4344
import androidx.compose.runtime.getValue
4445
import androidx.compose.runtime.remember
46+
import androidx.compose.runtime.rememberCoroutineScope
4547
import androidx.compose.ui.Alignment
4648
import androidx.compose.ui.Modifier
4749
import androidx.compose.ui.graphics.Color
@@ -62,6 +64,7 @@ import io.getstream.video.android.core.audio.StreamAudioDevice
6264
import io.getstream.video.android.core.call.audio.InputAudioFilter
6365
import io.getstream.video.android.core.mapper.ReactionMapper
6466
import io.getstream.video.android.core.model.PreferredVideoResolution
67+
import io.getstream.video.android.core.recording.RecordingType
6568
import io.getstream.video.android.tooling.extensions.toPx
6669
import io.getstream.video.android.ui.call.ReactionsMenu
6770
import io.getstream.video.android.ui.closedcaptions.ClosedCaptionUiState
@@ -70,6 +73,7 @@ import io.getstream.video.android.ui.menu.base.DynamicMenu
7073
import io.getstream.video.android.ui.menu.base.MenuItem
7174
import io.getstream.video.android.ui.menu.transcriptions.TranscriptionUiStateManager
7275
import io.getstream.video.android.util.filters.SampleAudioFilter
76+
import kotlinx.coroutines.launch
7377
import java.nio.ByteBuffer
7478

7579
@OptIn(ExperimentalPermissionsApi::class)
@@ -94,6 +98,7 @@ internal fun SettingsMenu(
9498
onClosedCaptionsToggle: () -> Unit,
9599
) {
96100
val context = LocalContext.current
101+
val scope = rememberCoroutineScope()
97102
val availableDevices by call.microphone.devices.collectAsStateWithLifecycle()
98103
val currentAudioUsage by call.speaker.audioUsage.collectAsStateWithLifecycle()
99104

@@ -245,6 +250,31 @@ internal fun SettingsMenu(
245250
}
246251
}
247252
}
253+
val compositeRecording by call.state.compositeRecording.collectAsStateWithLifecycle()
254+
val individualRecording by call.state.individualRecording.collectAsStateWithLifecycle()
255+
val rawRecording by call.state.rawRecording.collectAsStateWithLifecycle()
256+
val enabledRecordingTypes by remember {
257+
derivedStateOf {
258+
buildSet {
259+
if (compositeRecording) add(RecordingType.Composite)
260+
if (individualRecording) add(RecordingType.Individual)
261+
if (rawRecording) add(RecordingType.Raw)
262+
}
263+
}
264+
}
265+
val onSelectRecordingType = { recordingType: RecordingType ->
266+
scope.launch {
267+
when (recordingType) {
268+
RecordingType.Raw, RecordingType.Individual, RecordingType.Composite ->
269+
if (enabledRecordingTypes.contains(recordingType)) {
270+
call.stopRecording(recordingType)
271+
} else {
272+
call.startRecording(recordingType)
273+
}
274+
}
275+
}
276+
Unit
277+
}
248278

249279
Popup(
250280
offset = IntOffset(
@@ -263,9 +293,11 @@ internal fun SettingsMenu(
263293
tint = Color.White,
264294
imageVector = Icons.Default.Close,
265295
contentDescription = Icons.Default.Close.name,
266-
modifier = Modifier.padding(horizontal = 8.dp, vertical = 10.dp).clickable {
267-
onDismissed()
268-
},
296+
modifier = Modifier
297+
.padding(horizontal = 8.dp, vertical = 10.dp)
298+
.clickable {
299+
onDismissed()
300+
},
269301
)
270302
ReactionsMenu(
271303
call = call,
@@ -319,6 +351,8 @@ internal fun SettingsMenu(
319351
audioDeviceUiStateList = audioDeviceUiStateList,
320352
audioUsageUiState = audioUsageUiState,
321353
onToggleAudioUsage = onToggleAudioUsage,
354+
selectedRecordingTypes = enabledRecordingTypes,
355+
onSelectRecordingType = onSelectRecordingType,
322356
),
323357
)
324358
}

0 commit comments

Comments
 (0)