Skip to content

Add a sort by source in the LivestreamPlayer default implementation#1681

Merged
aleksandar-apostolov merged 1 commit into
developfrom
fix/livestream-player-participant-sorting
May 13, 2026
Merged

Add a sort by source in the LivestreamPlayer default implementation#1681
aleksandar-apostolov merged 1 commit into
developfrom
fix/livestream-player-participant-sorting

Conversation

@aleksandar-apostolov
Copy link
Copy Markdown
Contributor

@aleksandar-apostolov aleksandar-apostolov commented May 13, 2026

Goal

LivestreamPlayer default implementation does not consider the rank of the participant by source. This causes a WebRTC participant to be sometimes chosen instead of an RRMP participant.

Implementation

Sort by source when choosing participant with video instead of always taking the first one.

Testing

Run a livestream with 2 publishers one RTMP and other WebRTC. The RTMP one should be shown in the UI.

Summary by CodeRabbit

  • Bug Fixes
    • Improved livestream player video selection to intelligently prioritize the host video based on participant hierarchy, ensuring optimal content delivery and viewing experience.

Review Change Stack

@aleksandar-apostolov aleksandar-apostolov requested a review from a team as a code owner May 13, 2026 09:33
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 13, 2026

PR checklist ✅

All required conditions are satisfied:

  • Title length is OK (or ignored by label).
  • At least one pr: label exists.
  • Sections ### Goal, ### Implementation, and ### Testing are filled (or ignored for dependabot PRs).

🎉 Great job! This PR is ready for review.

@aleksandar-apostolov aleksandar-apostolov changed the title Add a sort by rank in the LivestreamPlayer default implementation Add a sort by source in the LivestreamPlayer default implementation May 13, 2026
@PratimMallick PratimMallick added the pr:bug Fixes a bug label May 13, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

Walkthrough

LivestreamPlayer's default host video selection logic is refactored to prioritize participants by source rank. A new participantSourceRank helper function maps ParticipantSource values to integer ranks, and the livestreamFlow selection now uses minByOrNull with this ranking to choose the highest-priority participant with video instead of selecting the first available.

Changes

Livestream Host Video Selection Priority

Layer / File(s) Summary
Participant source ranking and host video selection
stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/livestream/LivestreamPlayer.kt
participantSourceRank helper function defines priority ranks for ParticipantSource enum values. Import of ParticipantSource is added. Host video selection in livestreamFlow is changed to pick the participant with minimum rank (highest priority) when multiple have video, replacing the first-available behavior.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

🐰 A rabbit hops through streams of light,
Picking hosts by ranking might,
No longer first, but best instead,
Priority reigns where videos spread! 🎬

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers the Goal and Implementation sections with clear explanations, but is missing the required UI Changes, Testing details, Contributor Checklist, and Reviewer Checklist. Add the missing sections from the template: Testing with detailed test instructions, UI Changes section (if applicable), and complete the Contributor and Reviewer Checklists.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change of adding source-based sorting to the LivestreamPlayer's participant selection logic.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/livestream-player-participant-sorting

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/livestream/LivestreamPlayer.kt (2)

338-345: 💤 Low value

Consider adding a comment to document the priority ordering rationale.

The ranking function establishes RTMP as highest priority and WebRTC as lowest, but the reasoning isn't immediately clear from the code. A brief comment explaining why this ordering was chosen would help future maintainers understand the design decision.

📝 Suggested documentation addition
+// Ranks participant sources by priority for livestream host selection.
+// Lower values = higher priority. RTMP sources are preferred for reliability and quality.
 private fun participantSourceRank(s: ParticipantSource): Int = when (s) {
     ParticipantSource.PARTICIPANT_SOURCE_RTMP -> 0
     ParticipantSource.PARTICIPANT_SOURCE_WHIP -> 1
     ParticipantSource.PARTICIPANT_SOURCE_RTSP -> 2
     ParticipantSource.PARTICIPANT_SOURCE_SRT -> 3
     ParticipantSource.PARTICIPANT_SOURCE_WEBRTC_UNSPECIFIED -> 4
     ParticipantSource.PARTICIPANT_SOURCE_SIP -> 2
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/livestream/LivestreamPlayer.kt`
around lines 338 - 345, The participantSourceRank function assigns numeric
priority to ParticipantSource values but lacks documentation; add a concise
comment above participantSourceRank explaining the rationale for the ordering
(e.g., why RTMP is highest priority, WebRTC lowest, and why SIP/RTSP share a
rank), mention any tie-breaking or expected behavior when priorities equal, and
reference the mapped constants (ParticipantSource.PARTICIPANT_SOURCE_RTMP,
_WHIP, _RTSP, _SRT, _WEBRTC_UNSPECIFIED, _SIP) so future maintainers understand
the design decision and can update the mapping consistently.

338-345: ⚡ Quick win

Clarify the duplicate rank assignment or assign unique priorities.

Both PARTICIPANT_SOURCE_RTSP and PARTICIPANT_SOURCE_SIP are assigned rank 2. When multiple participants with the same rank exist, minByOrNull will return the first one encountered, making the selection non-deterministic. If equal priority is intentional, consider adding a comment explaining why. Otherwise, assign unique ranks to ensure deterministic selection.

📋 Suggested fix to assign unique ranks
 private fun participantSourceRank(s: ParticipantSource): Int = when (s) {
     ParticipantSource.PARTICIPANT_SOURCE_RTMP -> 0
     ParticipantSource.PARTICIPANT_SOURCE_WHIP -> 1
     ParticipantSource.PARTICIPANT_SOURCE_RTSP -> 2
     ParticipantSource.PARTICIPANT_SOURCE_SRT -> 3
     ParticipantSource.PARTICIPANT_SOURCE_WEBRTC_UNSPECIFIED -> 4
-    ParticipantSource.PARTICIPANT_SOURCE_SIP -> 2
+    ParticipantSource.PARTICIPANT_SOURCE_SIP -> 5
 }

Or, if equal priority is intentional, add a clarifying comment:

 private fun participantSourceRank(s: ParticipantSource): Int = when (s) {
     ParticipantSource.PARTICIPANT_SOURCE_RTMP -> 0
     ParticipantSource.PARTICIPANT_SOURCE_WHIP -> 1
+    // RTSP and SIP have equal priority
     ParticipantSource.PARTICIPANT_SOURCE_RTSP -> 2
     ParticipantSource.PARTICIPANT_SOURCE_SRT -> 3
     ParticipantSource.PARTICIPANT_SOURCE_WEBRTC_UNSPECIFIED -> 4
     ParticipantSource.PARTICIPANT_SOURCE_SIP -> 2
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/livestream/LivestreamPlayer.kt`
around lines 338 - 345, The participantSourceRank function assigns the same rank
(2) to PARTICIPANT_SOURCE_RTSP and PARTICIPANT_SOURCE_SIP which makes
minByOrNull selection non-deterministic; update participantSourceRank to give
each source a unique priority (e.g., change PARTICIPANT_SOURCE_SIP to a distinct
rank) or, if equal priority is intentional, add a clarifying comment inside
participantSourceRank explaining why RTSP and SIP share rank 2 and that
tie-breaking order is acceptable; ensure references to
ParticipantSource.PARTICIPANT_SOURCE_RTSP and
ParticipantSource.PARTICIPANT_SOURCE_SIP are updated accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/livestream/LivestreamPlayer.kt`:
- Around line 338-345: The participantSourceRank function assigns numeric
priority to ParticipantSource values but lacks documentation; add a concise
comment above participantSourceRank explaining the rationale for the ordering
(e.g., why RTMP is highest priority, WebRTC lowest, and why SIP/RTSP share a
rank), mention any tie-breaking or expected behavior when priorities equal, and
reference the mapped constants (ParticipantSource.PARTICIPANT_SOURCE_RTMP,
_WHIP, _RTSP, _SRT, _WEBRTC_UNSPECIFIED, _SIP) so future maintainers understand
the design decision and can update the mapping consistently.
- Around line 338-345: The participantSourceRank function assigns the same rank
(2) to PARTICIPANT_SOURCE_RTSP and PARTICIPANT_SOURCE_SIP which makes
minByOrNull selection non-deterministic; update participantSourceRank to give
each source a unique priority (e.g., change PARTICIPANT_SOURCE_SIP to a distinct
rank) or, if equal priority is intentional, add a clarifying comment inside
participantSourceRank explaining why RTSP and SIP share rank 2 and that
tie-breaking order is acceptable; ensure references to
ParticipantSource.PARTICIPANT_SOURCE_RTSP and
ParticipantSource.PARTICIPANT_SOURCE_SIP are updated accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b0f587ea-2c86-4e46-a621-e9f80f21c609

📥 Commits

Reviewing files that changed from the base of the PR and between 6e99b02 and 68670aa.

📒 Files selected for processing (1)
  • stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/livestream/LivestreamPlayer.kt

@github-actions
Copy link
Copy Markdown
Contributor

SDK Size Comparison 📏

SDK Before After Difference Status
stream-video-android-core 12.04 MB 12.04 MB 0.00 MB 🟢
stream-video-android-ui-xml 5.68 MB 5.68 MB 0.00 MB 🟢
stream-video-android-ui-compose 6.28 MB 6.28 MB 0.00 MB 🟢

@sonarqubecloud
Copy link
Copy Markdown

@aleksandar-apostolov aleksandar-apostolov merged commit 380e644 into develop May 13, 2026
13 of 16 checks passed
@aleksandar-apostolov aleksandar-apostolov deleted the fix/livestream-player-participant-sorting branch May 13, 2026 11:30
@stream-public-bot stream-public-bot added the released Included in a release label May 13, 2026
@stream-public-bot
Copy link
Copy Markdown
Collaborator

🚀 Available in v1.23.1

aleksandar-apostolov added a commit that referenced this pull request May 19, 2026
* test(core): add failing regression test for sortedParticipants channelFlow bug

SortedParticipantsState wraps its sort logic in a channelFlow whose builder block
launches two coroutines on the outer scope and returns immediately. The channel
closes as soon as the block returns, so all subsequent trySend calls from inside
the launches go to a closed channel and are silently dropped.

The existing tests in CallStateTest pass because the global DispatcherRule installs
UnconfinedTestDispatcher, which runs launched coroutines inline up to first
suspension and lets the initial sort squeak through before the channel closes.
Under any realistic production dispatcher (Main, Default, IO), the bug surfaces:
sortedParticipants never emits, OrientationVideoRenderer always falls back to the
unsorted participants map for >6-participant calls.

This test isolates SortedParticipantsState with StandardTestDispatcher to surface
the production behavior. Expected red on current develop; will go green after the
StateFlow migration.

* feat(core): add sort comparator primitives and visibility-aware decorators

Introduces a React-parity sorting toolkit under stream-video-android-core/sorting:

- combineComparators / conditional / ifInvisible / ifInvisibleOrUnknown
  composable building blocks that return -1, 0, +1 directly (no compareBy(Boolean)
  inversion bug)
- dominantSpeaker, speaking, screenSharing, publishingVideo, publishingAudio,
  raisedHand, byReactionType, byName, byJoinedAt, byRole, bySourcePriority
  primitive comparators on ParticipantState
- pinned (internal) — local pin > server pin > pinnedAt recency

bySourcePriority replaces the previous participantSourceRank, fixing the SIP=RTSP=2
collision by taking a vararg priority list and ranking by argument position.

ifInvisible vs ifInvisibleOrUnknown lets presets choose how strict the visibility
guard is — UNKNOWN counts as visible for the former, as invisible for the latter.

Adds 25 unit tests (15 primitive truth tables, 10 combinator/decorator behaviors)
verified against the React equivalents in stream-video-js. Test fixtures support
the A-F React fixture shape and 15-participant rolling-scroll scenarios.

* feat(core): add SortPreset sealed interface with Default, SpeakerLayout, AudioRoom

Three named presets for the participant grid:

- Default: screenSharing → pinned → ifInvisibleOrUnknown(dominantSpeaker → speaking
  → raisedHand → ingressSource → publishingVideo → publishingAudio). The continuous-
  scroll grid choice — UNKNOWN-as-invisible so off-screen tiles get ordered by
  activity before they render.
- SpeakerLayout: screenSharing → pinned → dominantSpeaker → ifInvisible(...). The
  dominant speaker is outside the visibility guard so the spotlight always reflects
  who's talking.
- AudioRoom: ifInvisibleOrUnknown(...) → byRole(admin, host, speaker). Role-aware
  ordering for livestream-style rooms.

The pinned comparator is internal (its PinUpdateAtTime parameter type is internal).
Presets resolve via an internal `SortPreset.build(pins)` extension invoked by the
sorter before each pass, so the latest local/server pin map participates in the
ordering.

Tests cover the React-parity composed-comparator scenarios, three Default-preset
rolling-scroll scenarios (15 participants, visible window P8..P12), the
SpeakerLayout visibility-guard difference, and the AudioRoom role priority. 13
tests total, all green.

* fix(core): rewrite SortedParticipantsState to back sortedParticipants with StateFlow

The previous implementation wrapped sorting in a channelFlow whose builder body
launched two coroutines on the outer scope and returned immediately. The channel
closed before any collector subscribed, so subsequent trySend calls hit a closed
channel and the public sortedParticipants flow never emitted under any non-
Unconfined dispatcher. OrientationVideoRenderer's `if (size > 6) sortedParticipants
else participants` always took the fallback path; the renderer effectively never
applied sorting in production.

The rewrite:

- Backs the public list with a MutableStateFlow<List<ParticipantState>>.
- Drives sorting from a combine over (participants, pinned-detailed, preset,
  custom-comparator) plus a separate launch for call.events. Both write into
  the same _sortedParticipants.value via a synchronous resort() that preserves
  the previous-order stability trick and skips emission when the order is
  unchanged.
- Accepts a SortPreset (default SortPreset.Default) and exposes setPreset(...)
  for runtime switching. updateComparator(Comparator) stays as an escape hatch
  for ad-hoc orderings.
- CallState now derives an internal _pinnedParticipantsDetailed flow combining
  _localPins + _serverPins while preserving PinUpdateAtTime (which carries the
  PinType). Local pins override server pins on key collision. The public
  pinnedParticipants StateFlow keeps its existing Map<String, OffsetDateTime>
  shape via mapState — no surface-level change.

The earlier regression test now passes (was red on develop). Two existing
CallStateTest sorting tests adjusted: switched stateIn target to backgroundScope
to avoid runTest leak detection, and updated the expected order to reflect the
new Default preset behavior (screen-sharer leads pinned because screenSharing is
the first comparator in the chain).

* refactor(compose): LivestreamPlayer consumes sortedParticipants; remove duplicated source rank

The default livestream picker now reads call.state.sortedParticipants and selects
the first participant with videoEnabled. Because the Default SortPreset's
bySourcePriority sits inside the visibility guard and viewers in a livestream
context typically render as UNKNOWN, the ingress host (RTMP / WHIP / SRT / RTSP)
naturally sorts first and the picker resolves to them.

This deletes the local participantSourceRank(s: ParticipantSource) helper added
in PR #1681, which duplicated the rank table in DefaultSortOrder.kt and carried
the same SIP=RTSP=2 collision. The canonical bySourcePriority(vararg) replaces it.

DefaultSortOrder.kt is deleted entirely — its behavior is subsumed by SortPresets.

* test(core): add SortedParticipantsState flow-behavior tests

Six tests covering the reactive surface of the new StateFlow-backed
SortedParticipantsState under StandardTestDispatcher:

- participants flow change emits an updated sorted list
- pinned flow change pushes the pinned participant to the front
- preset switch triggers a resort using the new preset
- custom comparator overrides the active preset until cleared
- repeated identical sorts are coalesced (same list instance returned, no
  redundant emission)
- call event triggers a resort against current participant + pin state,
  catching state changes that bypass the participants map (e.g. event handlers
  flipping dominantSpeaker on a ParticipantState directly)

* chore(core): apiDump for sorting changes + spotless formatting

Updates stream-video-android-core.api to reflect the new public sorting surface:

- SortPreset sealed interface with Default, SpeakerLayout, AudioRoom data objects
- Top-level comparator primitives (dominantSpeaker, speaking, screenSharing,
  publishingVideo, publishingAudio, raisedHand, byName, byJoinedAt)
- combineComparators, conditional, ifInvisible, ifInvisibleOrUnknown combinators
- bySourcePriority, byRole, byReactionType comparator factories
- CallState.setSortPreset(...) — runtime preset switching
- CallState.getSortedParticipants() return type narrowed from Flow to StateFlow

The Flow → StateFlow narrowing is a binary-compat break for Java consumers but
source-compatible for Kotlin callers (StateFlow IS-A Flow). Acceptable under the
v2 breaking-change allowance per CLAUDE.md.

Spotless reflowed ComparatorsTest and SortPresetsTest line breaks; no logic
changes.

* refactor(core): consolidate participants and sortedParticipants into a single sorted flow

CallState.participants now sources from _sortedParticipantsState.sortedParticipants
— it always returns the list ordered by the active SortPreset. The old
sortedParticipants property is kept as a @deprecated WARNING alias that points at
participants, so existing callers compile and run unchanged with a migration nudge.

This collapses the dual-flow architecture that was the root cause of a renderer
bug discovered during visual smoke testing: OrientationVideoRenderer's

    val callParticipants by remember(participants) {
        derivedStateOf { if (sortedParticipants.size > 6) sortedParticipants else participants }
    }

held two separate StateFlow subscriptions for the same conceptual list, with
remember keyed only on `participants`. The dual subscription created subtle
staleness and lifecycle interactions with the DisposableEffect setVisibility
calls that drive SFU track subscriptions — tiles rendered in the correct sort
order but their video tracks failed to display.

With a single sorted flow, OrientationVideoRenderer collapses to:

    val callParticipants by call.state.participants.collectAsStateWithLifecycle()

remoteParticipants also moves to derive from the sorted participants flow
(filtering out the local sessionId), so it's consistently sorted with
participants. Lazy-initialized to defer the forward reference to
_sortedParticipantsState.

LivestreamPlayer's default livestreamFlow and the existing CallStateTest sorting
tests switched off the deprecated alias in the same pass.

Net effect:
- One conceptual list, one StateFlow subscription, no dual-flow staleness
- Strictly fewer recompositions in scroll-heavy paths thanks to the sorter's
  lastSortOrder coalescing (visibility flips that don't change order no longer
  emit downstream)
- React parity restored — both SDKs expose participants as the single sorted list
- The previously-dead `if (size > 6) sortedParticipants` branch is gone

* test(core): cover visible-block stability end-to-end through the live flow

Two reactive-path tests added to SortedParticipantsStateTest:

- `visible-block internal order survives off-screen dominant-speaker promotion`:
  15 participants with P8..P12 marked VISIBLE, P15 (off-screen) gains
  dominantSpeaker. The call event drives a resort. Assertions: P15 jumps to
  index 0, the visible block keeps [P8, P9, P10, P11, P12] internal order.

- `visible block stays stable when a visible participant gains a signal`:
  P10 (inside the viewport) becomes dominantSpeaker. The visible/visible
  predicate returns 0 for every pair in the visible block, so the list does
  NOT reshuffle — P10 stays at its original index instead of jumping to the
  top.

These exercise the full reactive path that the existing SortPresetsTest
suite skips: direct field mutation on ParticipantState (mirroring the SDK's
DominantSpeakerChangedEvent handler), the call.events trigger, the combine
collect, the lastSortOrder coalescing, and the final StateFlow emission.
Closes the verification gap between the static comparator tests and the
visual smoke check.

* feat(core): add byUserId tiebreaker to all sort presets, mirroring iOS

Appends `ifInvisibleOrUnknown(byUserId)` (or `ifInvisible(byUserId)` for
SpeakerLayout) as a trailing decorator on each preset. Matches iOS's pattern of
`ifInvisibleBy(userId)` as the last comparator in every preset.

Effect: when two participants share every other signal (no dominant-speaker, no
pin, no raised hand, no ingress source advantage, equal track state), the sort
falls back to lexicographic userId compare instead of stable input order. The
visible block keeps its internal relative order (visible/visible pairs return 0
regardless of byUserId), and the invisible tail becomes deterministic across
re-sorts and across sessions.

In production, userIds are UUIDs so the resulting tail order is meaningless to
humans — but it's STABLE. That's the value: no surprise reshuffles when a sort
fires with no meaningful state change.

Tests:
- New `byUserId` primitive truth-table tests in ParticipantComparatorsTest.
- Updated `Default preset - two off-screen promotions bubble...` expectation to
  the new lex-ordered tail, with an explanatory comment.
- Updated `custom comparator overrides preset until cleared` to expect [a,b,c]
  after setPreset(Default) — the tiebreaker actively orders by userId now,
  replacing the previous "all signals 0 → stable input order" behavior.

For AudioRoom, the tiebreaker runs AFTER byRole(...) so role priority still
wins on equal participants — the test `AudioRoom - host role beats regular
participant` now passes without modification.

API additive: `getByUserId()` exposed alongside the other primitive comparators.

* fix(core): preserve binary compatibility of sortedParticipants return type

The previous commit narrowed CallState.sortedParticipants from Flow to StateFlow,
which is binary-incompatible for Java callers compiled against the v1 API.

Reverts the public return type of the deprecated `sortedParticipants` alias back
to `Flow<List<ParticipantState>>`. The underlying value is still a StateFlow
(returned via `get() = participants`), so consumers see identical runtime
behavior; only the declared method signature stays on the wider Flow type.

apiCheck against develop now shows only additive changes: setSortPreset(...),
the SortPreset sealed interface + 3 data objects, and the new comparator
primitives/combinators in the io.getstream.video.android.core.sorting package.
No removed or modified public symbols.

* refactor(compose): keep LivestreamPlayer's source-priority sort self-contained

Re-applies bySourcePriority(RTMP, WHIP, SRT, RTSP) inside the LivestreamPlayer's
default livestreamFlow before picking the first video-enabled participant. The
grid-level preset already orders ingress sources first, but the explicit local
sort isolates the picker from any future change to the active grid preset —
swapping the grid preset cannot accidentally affect which participant the
livestream player picks as the host.

Stable sort preserves the upstream grid order among same-source participants, so
no additional ordering rules need to be encoded here. Uses the canonical
bySourcePriority comparator from the sorting package; no local rank table.

* refactor(core): wire CallType.sortPreset and rename AudioRoom preset

Adds an open `sortPreset` property on the `CallType` sealed class so each call
type can declare its default participant sort preset, matching React's
`CallType` → `sortParticipantsBy` association. `CallType.Livestream` overrides
to `SortPreset.LivestreamOrAudioRoom`; other types stay on `SortPreset.Default`.

`CallState` resolves the preset on construction via
`CallType.fromName(call.type)?.sortPreset ?: SortPreset.Default` and passes it
as `initialPreset` to `SortedParticipantsState`. Callers can still override at
runtime via `setSortPreset(...)` or `updateParticipantSortingOrder(...)`.

`SortPreset.AudioRoom` renamed to `SortPreset.LivestreamOrAudioRoom` for
cross-platform naming parity (matches iOS `livestreamOrAudioRoomSortPreset` and
React `livestreamOrAudioRoomSortPreset`). Rename is internal to this PR —
nothing on develop referenced the old name.

`CallType.AudioCall` (Android-only 1:1 audio concept) intentionally stays on
the default preset; a dedicated `CallType.AudioRoom` for the actual group-audio
call type will land in a follow-up PR.

`LivestreamPlayer`'s local source-priority sort stays in place as defense in
depth — if anything changes the active preset at runtime, the host picker is
still correct.

6 new CallTypeSortPresetTest cases verifying the resolution path. apiCheck
against develop: still additive only.

* fix(core): address CodeRabbit review - mutex + defensive preset fallback

- Adds an `else` branch to `SortPreset.build(...)` so unknown SortPreset
  implementations (Java consumers can implement Kotlin sealed interfaces on
  JVM targets below 17) fall back to Default rather than throwing
  NoWhenBranchMatchedException. Sorting must never crash a call.

- Wraps SortedParticipantsState.resort() body in a Mutex. The state combine
  collector and the call.events collector both call resort(), which mutates
  lastSortOrder and _sortedParticipants. Production CallState scope today is
  single-threaded (Dispatchers.Main), but the contract should hold regardless
  of dispatcher choice. Mutex contention is negligible — resort body is
  microseconds of sync work.

Tests: all 55 sorting cases still pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:bug Fixes a bug released Included in a release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants