Add support for individual, raw, and composite call recordings#1610
Add support for individual, raw, and composite call recordings#1610aleksandar-apostolov merged 9 commits intodevelopfrom
Conversation
PR checklist ✅All required conditions are satisfied:
🎉 Great job! This PR is ready for review. |
SDK Size Comparison 📏
|
c97b424 to
4542064
Compare
…ng 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
4542064 to
a1fd973
Compare
WalkthroughThis PR adds support for multiple call recording types (individual, raw, composite) in the Android Video SDK demo app. It introduces state tracking for each recording type, UI controls for selecting recording types in the menu system, refactors the dynamic menu navigation, and updates code generation configuration to skip specific event classes. Changes
Sequence DiagramsequenceDiagram
actor User
participant UI as Recording Menu UI
participant Handler as onSelectRecordingType Handler
participant Call as Call State/API
participant State as Recording State
User->>UI: Select Recording Type (Raw/Individual/Composite)
activate UI
UI->>Handler: onSelectRecordingType(RecordingType)
deactivate UI
activate Handler
alt Recording Type is Disabled
Handler->>Call: startRecording(type)
else Recording Type is Enabled
Handler->>Call: stopRecording(type)
end
deactivate Handler
activate Call
Call->>State: Update compositeRecording/rawRecording/individualRecording
deactivate Call
activate State
State->>UI: derivedStateOf updates isRecording
deactivate State
activate UI
UI->>UI: Refresh UI with new recording state
deactivate UI
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested labels
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Tip Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt (1)
134-151:⚠️ Potential issue | 🔴 CriticalBug: stale
dynamicItemsshown when navigating forward into a secondDynamicSubMenuItem.When the user navigates from one dynamic submenu into another (e.g., clicks a
DynamicSubMenuIteminside already-loaded dynamic items),dynamicMenuRefandhistoryTitlesare updated (lines 147-150), butloadedItemsremainstruefrom the previous load. On the next recomposition theif (!loadedItems)guard on line 138 is skipped, so the staledynamicItemsfrom the previous dynamic menu are rendered instead of triggering a fresh load.Reset
loadedItems(and cleardynamicItems) when navigating forward into a new dynamic submenu:Proposed fix
if (dynamicItems.isNotEmpty()) { menuItems(dynamicItems) { if (it is DynamicSubMenuItem) { dynamicMenuRef.value = it } + loadedItems = false + dynamicItems.clear() historyTitles.add(it.title) }demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt (1)
827-847:⚠️ Potential issue | 🟡 MinorRemove the unused "End recording" dialog—it's dead code and its functionality is already handled by SettingsMenu with proper per-type controls.
showEndRecordingDialogis never set totrueanywhere in the codebase (only initialized and reset tofalse), making this dialog unreachable. Additionally, if triggered,call.stopRecording()would only stop the Composite recording since it defaults toRecordingType.Composite. The SettingsMenu already provides the correct solution: it tracks individual, raw, and composite recording states separately and callscall.stopRecording(recordingType)with the specific type when stopping any recording.
🤖 Fix all issues with AI agents
In
`@demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt`:
- Around line 71-75: The single-slot dynamicMenuRef cannot track nested dynamic
submenus and gets overwritten; replace it with a small keyed stack or map (e.g.,
a mutableStateMapOf<String, DynamicSubMenuItem> or a MutableList stack) keyed by
submenu title so each DynamicSubMenuItem is preserved per title; update lookup
logic that uses dynamicMenuRef (references in dynamicMenuRef,
DynamicSubMenuItem, currentTitle, isDynamic, and findSubMenuItem) to read/write
from the map/stack (push on enter, pop on back, and lookup by currentTitle) so
nested dynamic menus resolve correctly and prior dynamic state isn’t lost.
- Around line 113-118: The bug is that loadedItems is only reset on back
navigation (inside the IconButton click handler) but not on forward navigation,
causing stale items; update the logic so loadedItems is reset whenever the
submenu changes—either by calling dynamicMenuRef.value?.onNewSubmenu(...) (or
the existing onNewSubmenu callback) to perform loadedItems = false for all
transitions, or derive loadedItems from currentTitle so it is recomputed on
every title change; locate references to loadedItems, the IconButton back
handler, the forward-navigation where historyTitles is appended, and the
onNewSubmenu callback (or currentTitle) and move the reset there to centralize
behavior.
🧹 Nitpick comments (4)
demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt (1)
174-182:findSubMenuItemrelies on unique titles across the entire menu tree.If two
SubMenuItems at different nesting levels share the sametitle, this depth-first search returns the first match, which may not be the intended submenu. This is fine for the current demo app menu structure, but worth noting as a fragility if menus grow or titles are localized identically.demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt (1)
283-332: UselistOfinstead ofarrayListOf— the returned list doesn't need to be mutable.
arrayListOfallocates a mutableArrayListthat is never mutated. The return type isList<MenuItem>, solistOfis more idiomatic and communicates immutability.Also, the repeated
selectedRecordingTypes.contains(…)calls per item can be simplified with local booleans.♻️ Suggested simplification
fun recordingTypeMenu(onSelectRecording: (RecordingType) -> Unit, selectedRecordingTypes: Set<RecordingType>): List<MenuItem> { - return arrayListOf( - ActionMenuItem( - title = if (selectedRecordingTypes.contains( - RecordingType.Raw, - ) - ) { - "Stop raw recording" - } else { - "Start raw recording" - }, - icon = if (selectedRecordingTypes.contains( - RecordingType.Raw, - ) - ) { - Icons.Default.RawOn - } else { - Icons.Default.RawOff - }, - highlight = selectedRecordingTypes.contains(RecordingType.Raw), - action = { onSelectRecording(RecordingType.Raw) }, - ), - ActionMenuItem( - title = if (selectedRecordingTypes.contains( - RecordingType.Individual, - ) - ) { - "Stop individual recording" - } else { - "Start individual recording" - }, - icon = Icons.Default.Person, - highlight = selectedRecordingTypes.contains(RecordingType.Individual), - action = { onSelectRecording(RecordingType.Individual) }, - ), - ActionMenuItem( - title = if (selectedRecordingTypes.contains( - RecordingType.Composite, - ) - ) { - "Stop composite recording" - } else { - "Start composite recording" - }, - icon = Icons.Default.CropFree, - highlight = selectedRecordingTypes.contains(RecordingType.Composite), - action = { onSelectRecording(RecordingType.Composite) }, - ), - ) + val isRaw = RecordingType.Raw in selectedRecordingTypes + val isIndividual = RecordingType.Individual in selectedRecordingTypes + val isComposite = RecordingType.Composite in selectedRecordingTypes + + return listOf( + ActionMenuItem( + title = if (isRaw) "Stop raw recording" else "Start raw recording", + icon = if (isRaw) Icons.Default.RawOn else Icons.Default.RawOff, + highlight = isRaw, + action = { onSelectRecording(RecordingType.Raw) }, + ), + ActionMenuItem( + title = if (isIndividual) "Stop individual recording" else "Start individual recording", + icon = Icons.Default.Person, + highlight = isIndividual, + action = { onSelectRecording(RecordingType.Individual) }, + ), + ActionMenuItem( + title = if (isComposite) "Stop composite recording" else "Start composite recording", + icon = Icons.Default.CropFree, + highlight = isComposite, + action = { onSelectRecording(RecordingType.Composite) }, + ), + ) }demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt (1)
265-277: Thewhenis redundant — all branches perform the same action.Every sealed subclass of
RecordingTypeis collapsed into a single branch, so thewhenadds no value. A simpleif/elseon the set membership is sufficient. If the intent is to get a compile error on futureRecordingTypeadditions, thewhenshould be exhaustive (e.g., drop the combined branch and handle each case separately) — but since they all do the same thing, that won't help either.Also, consider surfacing the
ResultfromstartRecording/stopRecordingto show a toast on failure, consistent with how other debug actions in this file showToastmessages.♻️ Simplified handler
val onSelectRecordingType = { recordingType: RecordingType -> scope.launch { - when (recordingType) { - RecordingType.Raw, RecordingType.Individual, RecordingType.Composite -> - if (enabledRecordingTypes.contains(recordingType)) { - call.stopRecording(recordingType) - } else { - call.startRecording(recordingType) - } + if (recordingType in enabledRecordingTypes) { + call.stopRecording(recordingType) + } else { + call.startRecording(recordingType) } } Unit }demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt (1)
796-804: SimplifyisRecording— building aSetjust to check emptiness is unnecessary overhead.Since you only need a boolean, a simple disjunction avoids the
Setallocation on every derivation pass.♻️ Simplified derivation
val isRecording by remember { derivedStateOf { - buildSet { - if (compositeRecording) add(RecordingType.Composite) - if (individualRecording) add(RecordingType.Individual) - if (rawRecording) add(RecordingType.Raw) - }.isNotEmpty() + compositeRecording || individualRecording || rawRecording } }Note: this
buildSetpattern is also duplicated inSettingsMenu.kt(lines 256-264). Consider extracting a shared helper if both need the sameSet<RecordingType>representation, or simplify both to plain boolean logic where only a boolean is needed.
demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt
Outdated
Show resolved
Hide resolved
demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt
Outdated
Show resolved
Hide resolved
|
The change to suspend fun startRecording(): Result<Any> = startRecording(RecordingType.Composite)
suspend fun stopRecording(): Result<Any> = stopRecording(RecordingType.Composite)
suspend fun startRecording(recordingType: RecordingType): Result<Any> { ... }
suspend fun stopRecording(recordingType: RecordingType): Result<Any> { ... } |
aleksandar-apostolov
left a comment
There was a problem hiding this comment.
Recording indicator is a privacy concern — users must know when ANY recording is active. _recording.value should be set to true for all recording types, not just Composite. The specific flows (individualRecording, rawRecording) track which type, but recording must be true whenever any recording is happening.
CallState.kt line ~940:
is CallRecordingStartedEvent -> {
_recording.value = true // Always true for privacy - any recording active
when (event.recordingType) {
Individual -> _individualRecording.value = true
Raw -> _rawRecording.value = true
else -> {}
}
}|
To maintain backward compatibility and ensure privacy (users must always know when ANY recording is active), the existing // Existing - keep as "any recording active" for backward compatibility & privacy
val recording: StateFlow<Boolean> // true when ANY recording type is active
// New additions
val compositeRecording: StateFlow<Boolean>
val individualRecording: StateFlow<Boolean>
val rawRecording: StateFlow<Boolean>This way apps using |
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.
The 'test recording types are independent' test was leaving individualRecording and rawRecording as true, which polluted subsequent tests that share the same call object.
|


Goal
Add support for individual, raw, and composite call recordings
Implementation
New APIs
startRecording(recordingType: RecordingType)stopRecording(recordingType: RecordingType)New Public Enum
RecordingType Model
🎨 UI Changes
Screen_Recording_20260206_184817_Stream.Video.Calls.Development.mp4
Testing
CallRecordingStartedEventCallRecordingStoppedEventSummary by CodeRabbit
Release Notes
New Features
Chores
Core changes
io.getstream.video.android.compose.ui.components.call.CallAppBar- To correctly render Recording LabelProductvideoApi- Api Interfaceio.getstream.video.android.core.recording.RecordingType- IntroducedRecordingTypewhich can be used by Call Class or UI classesCall- Only params changesCallState-StreamVideoClient- Only params changesDemo app changes
CallScreen- To match existing behavior ofisRecordingMenuDefinitions&SettingsMenu- To add menu option for raw, individual and composite recordingsDynamicMenu- Made changes to correctly update sub-menus composeable as previously it was not updating unless I reopen the submenu