mirror of
https://github.com/suitenumerique/meet.git
synced 2026-10-15 17:32:59 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 378e3d011f | |||
| 867a14d0a3 |
+13
-13
@@ -87,7 +87,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -117,11 +117,11 @@ jobs:
|
||||
timeout 30 sh -c 'until docker exec garage /garage health; do sleep 1; done'
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "3.14"
|
||||
python-version: "3.13"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
- name: Install the dependencies
|
||||
run: uv sync --locked --all-extras
|
||||
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -170,12 +170,12 @@ jobs:
|
||||
sudo apt-get install -y ffmpeg
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "3.14"
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
|
||||
- name: Install the project
|
||||
run: uv sync --locked --all-extras
|
||||
@@ -189,7 +189,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -208,12 +208,12 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
@@ -232,7 +232,7 @@ jobs:
|
||||
working-directory: src/sdk/library
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -255,7 +255,7 @@ jobs:
|
||||
working-directory: src/sdk/library
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ and this project adheres to
|
||||
- 🐛(frontend) omit the default speaker id sent to LiveKit
|
||||
- 🐛(frontend) play the speaker test when no output is picked
|
||||
- 🐛(frontend) keep the admin panel in place when changing a setting #1804
|
||||
- 🐛(frontend) promote the active speaker in the carousel layout
|
||||
|
||||
## [1.34.0] - 2026-10-07
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
|
||||
import { getScrollBarWidth } from '@livekit/components-core'
|
||||
import * as React from 'react'
|
||||
import { TrackLoop, useVisualStableUpdate } from '@livekit/components-react'
|
||||
import { Track } from 'livekit-client'
|
||||
import { useSize } from '@/features/rooms/livekit/hooks/useResizeObserver'
|
||||
import { useLastRemoteSpeaker } from '../hooks/useLastRemoteSpeaker'
|
||||
import { useSpeakerPromotionTrigger } from '../hooks/useSpeakerPromotionTrigger'
|
||||
|
||||
const MIN_HEIGHT = 130
|
||||
const MIN_WIDTH = 140
|
||||
@@ -66,7 +69,21 @@ export function CarouselLayout({
|
||||
setPrevTiles(tilesThatFit)
|
||||
}
|
||||
|
||||
const sortedTiles = useVisualStableUpdate(tracks, maxVisibleTiles)
|
||||
const stableTiles = useVisualStableUpdate(tracks, maxVisibleTiles)
|
||||
const speakerId = useLastRemoteSpeaker()
|
||||
|
||||
// Reordered after useVisualStableUpdate so its page size never changes,
|
||||
// which would make it re-sort every tile.
|
||||
const isLocalCamera = (t: TrackReferenceOrPlaceholder) =>
|
||||
t.source === Track.Source.Camera && t.participant.isLocal
|
||||
const isSpeakerCamera = (t: TrackReferenceOrPlaceholder) =>
|
||||
t.source === Track.Source.Camera && t.participant.identity === speakerId
|
||||
const sortedTiles = [
|
||||
...stableTiles.filter(isLocalCamera),
|
||||
...stableTiles.filter(isSpeakerCamera),
|
||||
...stableTiles.filter((t) => !isLocalCamera(t) && !isSpeakerCamera(t)),
|
||||
]
|
||||
useSpeakerPromotionTrigger(sortedTiles.slice(0, maxVisibleTiles), tracks)
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
if (asideEl.current) {
|
||||
|
||||
@@ -46,7 +46,7 @@ export function GridLayout({ tracks, ...props }: GridLayoutProps) {
|
||||
|
||||
const { layout } = useGridLayout(gridEl, tracks.length)
|
||||
const pagination = usePagination(layout.maxTiles, tracks)
|
||||
useSpeakerPromotionTrigger(pagination.tracks)
|
||||
useSpeakerPromotionTrigger(pagination.tracks, tracks)
|
||||
|
||||
useSwipe(gridEl, {
|
||||
onLeftSwipe: pagination.nextPage,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { RoomEvent } from 'livekit-client'
|
||||
import type { Participant } from 'livekit-client'
|
||||
import { useRoomContext } from '@livekit/components-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Identity of the loudest remote speaker, kept through silence until another
|
||||
* remote participant speaks. Re-renders only when that identity changes.
|
||||
*/
|
||||
export function useLastRemoteSpeaker() {
|
||||
const room = useRoomContext()
|
||||
const [identity, setIdentity] = useState<string>()
|
||||
|
||||
useEffect(() => {
|
||||
const onActiveSpeakersChanged = (speakers: Participant[]) => {
|
||||
const loudest = speakers.find((speaker) => !speaker.isLocal)
|
||||
if (loudest) {
|
||||
setIdentity(loudest.identity)
|
||||
}
|
||||
}
|
||||
room.on(RoomEvent.ActiveSpeakersChanged, onActiveSpeakersChanged)
|
||||
return () => {
|
||||
room.off(RoomEvent.ActiveSpeakersChanged, onActiveSpeakersChanged)
|
||||
}
|
||||
}, [room])
|
||||
|
||||
return identity
|
||||
}
|
||||
@@ -8,35 +8,39 @@ import { useEffect, useReducer, useRef } from 'react'
|
||||
* Tripwire for speaker promotion.
|
||||
*
|
||||
* Listens to `RoomEvent.ActiveSpeakersChanged` imperatively and forces ONE
|
||||
* re-render of the host component only when an active speaker has none of
|
||||
* their tiles are within the visible span (`maxVisibleTiles`). That render
|
||||
* re-runs `useVisualStableUpdate`, which reads live `participant.isSpeaking`
|
||||
* state and performs the actual swap.
|
||||
* re-render of the host when a speaker has a tile in `allTiles` but none in
|
||||
* `visibleTiles`. That render re-runs `useVisualStableUpdate`, which reads
|
||||
* live `participant.isSpeaking` state and performs the actual swap.
|
||||
*
|
||||
* Speakers already visible are ignored. Everything else costs zero React work.
|
||||
* Speakers already visible cost nothing, nor do speakers the layout doesn't
|
||||
* show at all (e.g. the pinned participant, rendered by the focus view).
|
||||
*
|
||||
* Requires the parent to NOT re-render on speaker events itself, i.e.
|
||||
* `useTracks(..., { updateOnlyOn: [] })` upstream.
|
||||
*/
|
||||
export function useSpeakerPromotionTrigger(
|
||||
sortedTiles: TrackReferenceOrPlaceholder[]
|
||||
/** Tiles inside the span the layout guarantees to speakers. */
|
||||
visibleTiles: TrackReferenceOrPlaceholder[],
|
||||
/** Every tile the layout shows, visible or not. */
|
||||
allTiles: TrackReferenceOrPlaceholder[]
|
||||
) {
|
||||
const room = useRoomContext()
|
||||
const [, forceRender] = useReducer((n: number) => n + 1, 0)
|
||||
|
||||
// Refs so the listener reads current values without re-subscribing
|
||||
// and without itself being a render dependency.
|
||||
const tilesRef = useRef(sortedTiles)
|
||||
tilesRef.current = sortedTiles
|
||||
// Read by the listener at event time, so it never re-subscribes.
|
||||
const tilesRef = useRef({ visibleTiles, allTiles })
|
||||
tilesRef.current = { visibleTiles, allTiles }
|
||||
|
||||
useEffect(() => {
|
||||
const onActiveSpeakersChanged = (speakers: Participant[]) => {
|
||||
const tiles = tilesRef.current
|
||||
const hiddenSpeakerExists = speakers.some(
|
||||
const { visibleTiles, allTiles } = tilesRef.current
|
||||
const visible = new Set(visibleTiles.map((t) => t.participant.identity))
|
||||
const shown = new Set(allTiles.map((t) => t.participant.identity))
|
||||
const hidden = speakers.some(
|
||||
(speaker) =>
|
||||
!tiles.some((t) => t.participant.identity === speaker.identity)
|
||||
shown.has(speaker.identity) && !visible.has(speaker.identity)
|
||||
)
|
||||
if (hiddenSpeakerExists) {
|
||||
if (hidden) {
|
||||
forceRender()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user