mirror of
https://github.com/suitenumerique/meet.git
synced 2026-09-01 05:07:56 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 26cb99b81c | |||
| 5bc04409de | |||
| 78a06ab55f | |||
| 1443ef12eb | |||
| f1d3799434 | |||
| e59aaaa998 |
@@ -75,6 +75,7 @@ db.sqlite3
|
|||||||
# IDEs
|
# IDEs
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.cursor/
|
||||||
*.iml
|
*.iml
|
||||||
.devcontainer
|
.devcontainer
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -10,6 +10,12 @@ and this project adheres to
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- ✨(frontend) share OneToOneFocusLayout between PiP and main room
|
||||||
|
|
||||||
|
## [1.29.0] - 2026-08-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
- ✨(any) let any authenticated user manage the lobby on trusted rooms
|
- ✨(any) let any authenticated user manage the lobby on trusted rooms
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@@ -24,6 +30,7 @@ and this project adheres to
|
|||||||
- ⬆️(frontend) upgrade posthog-js from 1.404.1 to 1.409.5
|
- ⬆️(frontend) upgrade posthog-js from 1.404.1 to 1.409.5
|
||||||
- ⚡️(frontend) apply frugal constraint to the active meeting audio track
|
- ⚡️(frontend) apply frugal constraint to the active meeting audio track
|
||||||
- ⚡️(backend) replace blocking Redis KEYS with cursor-based SCAN
|
- ⚡️(backend) replace blocking Redis KEYS with cursor-based SCAN
|
||||||
|
- ✨(summary) add hostname to analytics properties
|
||||||
|
|
||||||
## [1.28.0] - 2026-08-24
|
## [1.28.0] - 2026-08-24
|
||||||
|
|
||||||
@@ -62,7 +69,6 @@ and this project adheres to
|
|||||||
|
|
||||||
- 🔥(frontend) drop unused vendored ConnectionObserver
|
- 🔥(frontend) drop unused vendored ConnectionObserver
|
||||||
- 🐛(frontend) vendor formatChatMessageLinks and trim surrounding newlines
|
- 🐛(frontend) vendor formatChatMessageLinks and trim surrounding newlines
|
||||||
- ✨(summary) add hostname to analytics properties
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "agents"
|
name = "agents"
|
||||||
version = "1.28.0"
|
version = "1.29.0"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"livekit-agents==1.6.7",
|
"livekit-agents==1.6.7",
|
||||||
|
|||||||
Generated
+1
-1
@@ -9,7 +9,7 @@ resolution-markers = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agents"
|
name = "agents"
|
||||||
version = "1.28.0"
|
version = "1.29.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "livekit-agents" },
|
{ name = "livekit-agents" },
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ build-backend = "uv_build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "meet"
|
name = "meet"
|
||||||
version = "1.28.0"
|
version = "1.29.0"
|
||||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
|
|||||||
Generated
+1
-1
@@ -1187,7 +1187,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "meet"
|
name = "meet"
|
||||||
version = "1.28.0"
|
version = "1.29.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiohttp" },
|
{ name = "aiohttp" },
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "meet",
|
"name": "meet",
|
||||||
"version": "1.28.0",
|
"version": "1.29.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "meet",
|
"name": "meet",
|
||||||
"version": "1.28.0",
|
"version": "1.29.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/atkinson-hyperlegible-next": "5.3.0",
|
"@fontsource-variable/atkinson-hyperlegible-next": "5.3.0",
|
||||||
"@fontsource-variable/lexend": "5.2.11",
|
"@fontsource-variable/lexend": "5.2.11",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "meet",
|
"name": "meet",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.28.0",
|
"version": "1.29.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "panda codegen && vite",
|
"dev": "panda codegen && vite",
|
||||||
|
|||||||
@@ -42,23 +42,6 @@ export type IceCandidateReport = {
|
|||||||
working: IceCandidatePair[]
|
working: IceCandidatePair[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* True when the selected pair does not carry media over plain UDP: the browser
|
|
||||||
* fell back to a TURN relay over TCP or TLS, or even the direct route is not
|
|
||||||
* UDP. Media still flows, but quality usually degrades under load.
|
|
||||||
*
|
|
||||||
* Accepts the loosely typed `data` stored on the step result; anything that is
|
|
||||||
* not an IceCandidateReport simply yields false.
|
|
||||||
*/
|
|
||||||
export const isSuboptimalRoute = (data: unknown): boolean => {
|
|
||||||
const selected = (data as IceCandidateReport | null | undefined)?.selected
|
|
||||||
if (!selected) return false
|
|
||||||
const transport = selected.local.relayProtocol ?? selected.local.protocol
|
|
||||||
// An unreported transport is not evidence of a bad route.
|
|
||||||
if (!transport) return false
|
|
||||||
return transport.toLowerCase() !== 'udp'
|
|
||||||
}
|
|
||||||
|
|
||||||
const PROBE_WIDTH = 320
|
const PROBE_WIDTH = 320
|
||||||
const PROBE_HEIGHT = 180
|
const PROBE_HEIGHT = 180
|
||||||
const PROBE_FPS = 15
|
const PROBE_FPS = 15
|
||||||
|
|||||||
@@ -2,33 +2,18 @@ import type { ReactNode } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { ProgressBar } from 'react-aria-components'
|
import { ProgressBar } from 'react-aria-components'
|
||||||
import { css, cx } from '@/styled-system/css'
|
import { css, cx } from '@/styled-system/css'
|
||||||
import { A } from '@/primitives'
|
|
||||||
import type { ConnectionTestStats } from '../types'
|
import type { ConnectionTestStats } from '../types'
|
||||||
import { statusSquareClass } from './stepAppearance'
|
import { statusSquareClass } from './stepAppearance'
|
||||||
|
|
||||||
/**
|
type SummaryState = 'idle' | 'running' | 'passed' | 'partial' | 'failed'
|
||||||
* Network prerequisites (protocols, ports, TURN fallbacks), written for the
|
|
||||||
* reader's IT department rather than for the end user.
|
|
||||||
*/
|
|
||||||
const NETWORK_REQUIREMENTS_DOC_URL =
|
|
||||||
'https://docs.numerique.gouv.fr/docs/f2baa1b9-f29e-4d58-959d-65d4376fc6b8/'
|
|
||||||
|
|
||||||
type SummaryState =
|
/** Only a failure earns a colour: everything else stays near-black. */
|
||||||
| 'idle'
|
|
||||||
| 'running'
|
|
||||||
| 'passed'
|
|
||||||
| 'partial'
|
|
||||||
| 'failed'
|
|
||||||
| 'warning'
|
|
||||||
|
|
||||||
/** Only a failure or a degraded route earns a colour: everything else stays near-black. */
|
|
||||||
const stateColorClass: Record<SummaryState, string> = {
|
const stateColorClass: Record<SummaryState, string> = {
|
||||||
idle: css({ color: 'greyscale.1000' }),
|
idle: css({ color: 'greyscale.1000' }),
|
||||||
running: css({ color: 'greyscale.1000' }),
|
running: css({ color: 'greyscale.1000' }),
|
||||||
passed: css({ color: 'greyscale.1000' }),
|
passed: css({ color: 'greyscale.1000' }),
|
||||||
partial: css({ color: 'greyscale.1000' }),
|
partial: css({ color: 'greyscale.1000' }),
|
||||||
failed: css({ color: 'danger.600' }),
|
failed: css({ color: 'danger.600' }),
|
||||||
warning: css({ color: 'warning' }),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cardClass = css({
|
const cardClass = css({
|
||||||
@@ -191,31 +176,23 @@ const Counter = ({
|
|||||||
export const ConnectionTestSummary = ({
|
export const ConnectionTestSummary = ({
|
||||||
stats,
|
stats,
|
||||||
isRunning,
|
isRunning,
|
||||||
routeWarning = false,
|
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
stats: ConnectionTestStats
|
stats: ConnectionTestStats
|
||||||
isRunning: boolean
|
isRunning: boolean
|
||||||
/** The selected ICE route is usable but not UDP (e.g. TURN over TCP/TLS). */
|
|
||||||
routeWarning?: boolean
|
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('connectionTest')
|
const { t } = useTranslation('connectionTest')
|
||||||
|
|
||||||
// A hard failure still outranks the route warning; the warning outranks
|
|
||||||
// 'partial' because a measured degraded route matters more than skipped
|
|
||||||
// camera or microphone checks.
|
|
||||||
const state: SummaryState = isRunning
|
const state: SummaryState = isRunning
|
||||||
? 'running'
|
? 'running'
|
||||||
: !stats.hasStarted
|
: !stats.hasStarted
|
||||||
? 'idle'
|
? 'idle'
|
||||||
: stats.failed > 0
|
: stats.failed > 0
|
||||||
? 'failed'
|
? 'failed'
|
||||||
: routeWarning
|
: stats.skipped > 0
|
||||||
? 'warning'
|
? 'partial'
|
||||||
: stats.skipped > 0
|
: 'passed'
|
||||||
? 'partial'
|
|
||||||
: 'passed'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={cardClass}>
|
<section className={cardClass}>
|
||||||
@@ -229,22 +206,7 @@ export const ConnectionTestSummary = ({
|
|||||||
: t(`summary.${state}`)}
|
: t(`summary.${state}`)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className={hintClass}>
|
<p className={hintClass}>{t(`summary.${state}Hint`)}</p>
|
||||||
{t(`summary.${state}Hint`)}
|
|
||||||
{state === 'warning' && (
|
|
||||||
<>
|
|
||||||
{' '}
|
|
||||||
<A
|
|
||||||
href={NETWORK_REQUIREMENTS_DOC_URL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
{t('summary.warningDocLink')}
|
|
||||||
</A>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{stats.hasStarted && (
|
{stats.hasStarted && (
|
||||||
|
|||||||
@@ -53,9 +53,6 @@ const fromCheckInfo = (info: CheckInfo): Partial<ConnectionTestStepResult> => ({
|
|||||||
status: CHECK_STATUS_TO_STEP[info.status] ?? 'failed',
|
status: CHECK_STATUS_TO_STEP[info.status] ?? 'failed',
|
||||||
summary: info.description,
|
summary: info.description,
|
||||||
logs: info.logs,
|
logs: info.logs,
|
||||||
// Only SelectedCandidateCheck sets `data` (the ICE candidate report); for
|
|
||||||
// the stock LiveKit checks it is undefined and this is a no-op.
|
|
||||||
data: info.data as Record<string, unknown> | undefined,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const groupDevicesByKind = (devices: MediaDeviceInfo[]) => {
|
const groupDevicesByKind = (devices: MediaDeviceInfo[]) => {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { css } from '@/styled-system/css'
|
|||||||
import { Center, VStack } from '@/styled-system/jsx'
|
import { Center, VStack } from '@/styled-system/jsx'
|
||||||
import { Permissions } from '@/features/rooms/components/Permissions'
|
import { Permissions } from '@/features/rooms/components/Permissions'
|
||||||
import { useConnectionTestRunner } from '../hooks/useConnectionTestRunner'
|
import { useConnectionTestRunner } from '../hooks/useConnectionTestRunner'
|
||||||
import { isSuboptimalRoute } from '../checks/selectedCandidate'
|
|
||||||
import { ConnectionTestStepRow } from '../components/ConnectionTestStepRow'
|
import { ConnectionTestStepRow } from '../components/ConnectionTestStepRow'
|
||||||
import { ConnectionTestSummary } from '../components/ConnectionTestSummary'
|
import { ConnectionTestSummary } from '../components/ConnectionTestSummary'
|
||||||
import { CONNECTION_TEST_GROUPS, summarizeSteps } from '../types'
|
import { CONNECTION_TEST_GROUPS, summarizeSteps } from '../types'
|
||||||
@@ -71,13 +70,6 @@ const ConnectionTest = () => {
|
|||||||
() => new Map(steps.map((step) => [step.id, step] as const)),
|
() => new Map(steps.map((step) => [step.id, step] as const)),
|
||||||
[steps]
|
[steps]
|
||||||
)
|
)
|
||||||
// The summary headline downgrades to a warning when media flows but over a
|
|
||||||
// fallback route: TURN over TCP/TLS, or anything else that is not UDP.
|
|
||||||
const routeWarning = useMemo(() => {
|
|
||||||
const step = stepsById.get('selectedCandidate')
|
|
||||||
return step?.status === 'success' && isSuboptimalRoute(step.data)
|
|
||||||
}, [stepsById])
|
|
||||||
|
|
||||||
const isPublishVideoRunning =
|
const isPublishVideoRunning =
|
||||||
stepsById.get('publishVideo')?.status === 'running'
|
stepsById.get('publishVideo')?.status === 'running'
|
||||||
|
|
||||||
@@ -107,11 +99,7 @@ const ConnectionTest = () => {
|
|||||||
<CenteredContent withBackButton>
|
<CenteredContent withBackButton>
|
||||||
<Center>
|
<Center>
|
||||||
<VStack gap="1.5rem" maxWidth="40rem" width="100%">
|
<VStack gap="1.5rem" maxWidth="40rem" width="100%">
|
||||||
<ConnectionTestSummary
|
<ConnectionTestSummary stats={stats} isRunning={isRunning}>
|
||||||
stats={stats}
|
|
||||||
isRunning={isRunning}
|
|
||||||
routeWarning={routeWarning}
|
|
||||||
>
|
|
||||||
{isRunning ? (
|
{isRunning ? (
|
||||||
// A disabled "run" button while the test runs is dead weight:
|
// A disabled "run" button while the test runs is dead weight:
|
||||||
// cancelling is the only thing left to do.
|
// cancelling is the only thing left to do.
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
|
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
|
||||||
|
import { styled } from '@/styled-system/jsx'
|
||||||
|
import { cva } from '@/styled-system/css'
|
||||||
|
import { ParticipantTile } from '@/features/participantTile/components/ParticipantTile'
|
||||||
|
import { getTrackKey } from '@/features/layout/utils/trackSelection'
|
||||||
|
|
||||||
|
type OneToOneFocusLayoutProps = {
|
||||||
|
mainTrack?: TrackReferenceOrPlaceholder
|
||||||
|
thumbnailTrack?: TrackReferenceOrPlaceholder
|
||||||
|
disableTileControls?: boolean
|
||||||
|
/** Controls thumbnail dimensions – 'pip' for small PiP window, 'room' for the main viewport. */
|
||||||
|
context?: 'pip' | 'room'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Focus layout for 1-to-1 calls: one main tile filling the area (letterboxed)
|
||||||
|
* with an optional thumbnail overlay at the bottom-right.
|
||||||
|
*
|
||||||
|
* Shared between PiP and the main room – pass `disableTileControls` in PiP
|
||||||
|
* where hover controls should be hidden.
|
||||||
|
*/
|
||||||
|
export const OneToOneFocusLayout = memo(
|
||||||
|
({
|
||||||
|
mainTrack,
|
||||||
|
thumbnailTrack,
|
||||||
|
disableTileControls,
|
||||||
|
context = 'room',
|
||||||
|
}: OneToOneFocusLayoutProps) => {
|
||||||
|
return (
|
||||||
|
<FocusContainer>
|
||||||
|
{mainTrack && (
|
||||||
|
<MainSlot>
|
||||||
|
<ParticipantTile
|
||||||
|
key={getTrackKey(mainTrack)}
|
||||||
|
trackRef={mainTrack}
|
||||||
|
disableTileControls={disableTileControls}
|
||||||
|
/>
|
||||||
|
</MainSlot>
|
||||||
|
)}
|
||||||
|
{thumbnailTrack && (
|
||||||
|
<Thumbnail context={context}>
|
||||||
|
<ParticipantTile
|
||||||
|
key={getTrackKey(thumbnailTrack)}
|
||||||
|
trackRef={thumbnailTrack}
|
||||||
|
disableTileControls={disableTileControls}
|
||||||
|
/>
|
||||||
|
</Thumbnail>
|
||||||
|
)}
|
||||||
|
</FocusContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
OneToOneFocusLayout.displayName = 'OneToOneFocusLayout'
|
||||||
|
|
||||||
|
const FocusContainer = styled('div', {
|
||||||
|
base: {
|
||||||
|
position: 'relative',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
borderRadius: '8px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
backgroundColor: 'primaryDark.100',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const MainSlot = styled('div', {
|
||||||
|
base: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
borderRadius: '8px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
'& .lk-participant-tile': {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
'& .lk-participant-media-video': {
|
||||||
|
objectFit: 'contain',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const Thumbnail = styled(
|
||||||
|
'div',
|
||||||
|
cva({
|
||||||
|
base: {
|
||||||
|
position: 'absolute',
|
||||||
|
right: '1.25rem',
|
||||||
|
bottom: '1.25rem',
|
||||||
|
aspectRatio: '16 / 9',
|
||||||
|
borderRadius: '8px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
boxShadow: 'md',
|
||||||
|
zIndex: 2,
|
||||||
|
'& .lk-participant-tile': {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
context: {
|
||||||
|
pip: {
|
||||||
|
width: '42%',
|
||||||
|
maxWidth: '220px',
|
||||||
|
minWidth: '140px',
|
||||||
|
},
|
||||||
|
room: {
|
||||||
|
width: '20%',
|
||||||
|
maxWidth: '320px',
|
||||||
|
minWidth: '180px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
context: 'room',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
@@ -12,7 +12,8 @@ import {
|
|||||||
import { Track } from 'livekit-client'
|
import { Track } from 'livekit-client'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { clearPinnedTrack, layoutStore, setPinnedTrack } from '@/stores/layout'
|
import { clearPinnedTrack, layoutStore, setPinnedTrack } from '@/stores/layout'
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useMemo, useRef } from 'react'
|
||||||
|
import { OneToOneFocusLayout } from '@/features/layout/components/OneToOneFocusLayout'
|
||||||
|
|
||||||
export const StageLayout = () => {
|
export const StageLayout = () => {
|
||||||
const lastAutoFocusedScreenShareTrack =
|
const lastAutoFocusedScreenShareTrack =
|
||||||
@@ -36,6 +37,29 @@ export const StageLayout = () => {
|
|||||||
(track) => !isEqualTrackRef(track, pinnedTrackRef)
|
(track) => !isEqualTrackRef(track, pinnedTrackRef)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const cameraTracks = useMemo(
|
||||||
|
() => tracks.filter((t) => t.source === Track.Source.Camera),
|
||||||
|
[tracks]
|
||||||
|
)
|
||||||
|
|
||||||
|
const isOneToOne =
|
||||||
|
!pinnedTrackRef &&
|
||||||
|
screenShareTracks.length === 0 &&
|
||||||
|
cameraTracks.length <= 2
|
||||||
|
|
||||||
|
const oneToOneMainTrack = useMemo(() => {
|
||||||
|
if (!isOneToOne) return undefined
|
||||||
|
const remote = cameraTracks.find((t) => !t.participant?.isLocal)
|
||||||
|
const local = cameraTracks.find((t) => t.participant?.isLocal)
|
||||||
|
return remote ?? local
|
||||||
|
}, [isOneToOne, cameraTracks])
|
||||||
|
|
||||||
|
const oneToOneThumbnailTrack = useMemo(() => {
|
||||||
|
if (!isOneToOne) return undefined
|
||||||
|
const local = cameraTracks.find((t) => t.participant?.isLocal)
|
||||||
|
return oneToOneMainTrack === local ? undefined : local
|
||||||
|
}, [isOneToOne, cameraTracks, oneToOneMainTrack])
|
||||||
|
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
// Code duplicated from LiveKit; this warning will be addressed in the refactoring.
|
// Code duplicated from LiveKit; this warning will be addressed in the refactoring.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -87,7 +111,12 @@ export const StageLayout = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!pinnedTrackRef ? (
|
{isOneToOne ? (
|
||||||
|
<OneToOneFocusLayout
|
||||||
|
mainTrack={oneToOneMainTrack}
|
||||||
|
thumbnailTrack={oneToOneThumbnailTrack}
|
||||||
|
/>
|
||||||
|
) : !pinnedTrackRef ? (
|
||||||
<div className="lk-grid-layout-wrapper" style={{ height: 'auto' }}>
|
<div className="lk-grid-layout-wrapper" style={{ height: 'auto' }}>
|
||||||
<GridLayout tracks={tracks} style={{ padding: 0 }}>
|
<GridLayout tracks={tracks} style={{ padding: 0 }}>
|
||||||
<ParticipantTile />
|
<ParticipantTile />
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
import { memo } from 'react'
|
|
||||||
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
|
|
||||||
import { styled } from '@/styled-system/jsx'
|
|
||||||
import { ParticipantTile } from '@/features/participantTile/components/ParticipantTile'
|
|
||||||
import { getTrackKey } from '../../utils/pipTrackSelection'
|
|
||||||
|
|
||||||
type PipFocusLayoutProps = {
|
|
||||||
mainTrack?: TrackReferenceOrPlaceholder
|
|
||||||
thumbnailTrack?: TrackReferenceOrPlaceholder
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Focus layout used when 1-2 tracks are visible in the PiP window.
|
|
||||||
*
|
|
||||||
* The main tile is letterboxed (object-fit: contain) so the camera is
|
|
||||||
* never stretched to a non-video aspect and leaves dark padding
|
|
||||||
* above/below when the window shape doesn't match the source.
|
|
||||||
* The thumbnail keeps the usual cover fill.
|
|
||||||
*/
|
|
||||||
export const PipFocusLayout = memo(
|
|
||||||
({ mainTrack, thumbnailTrack }: PipFocusLayoutProps) => {
|
|
||||||
return (
|
|
||||||
<FocusContainer>
|
|
||||||
{mainTrack && (
|
|
||||||
<MainSlot>
|
|
||||||
<ParticipantTile
|
|
||||||
key={getTrackKey(mainTrack)}
|
|
||||||
trackRef={mainTrack}
|
|
||||||
disableTileControls
|
|
||||||
/>
|
|
||||||
</MainSlot>
|
|
||||||
)}
|
|
||||||
{thumbnailTrack && (
|
|
||||||
<Thumbnail>
|
|
||||||
<ParticipantTile
|
|
||||||
key={getTrackKey(thumbnailTrack)}
|
|
||||||
trackRef={thumbnailTrack}
|
|
||||||
disableTileControls
|
|
||||||
/>
|
|
||||||
</Thumbnail>
|
|
||||||
)}
|
|
||||||
</FocusContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
PipFocusLayout.displayName = 'PipFocusLayout'
|
|
||||||
|
|
||||||
const FocusContainer = styled('div', {
|
|
||||||
base: {
|
|
||||||
position: 'relative',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
borderRadius: '8px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
backgroundColor: 'primaryDark.100',
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const MainSlot = styled('div', {
|
|
||||||
base: {
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
borderRadius: '8px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
'& .lk-participant-media-video': {
|
|
||||||
objectFit: 'contain',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const Thumbnail = styled('div', {
|
|
||||||
base: {
|
|
||||||
position: 'absolute',
|
|
||||||
right: '1.25rem',
|
|
||||||
bottom: '1.25rem',
|
|
||||||
width: '42%',
|
|
||||||
maxWidth: '220px',
|
|
||||||
minWidth: '140px',
|
|
||||||
aspectRatio: '16 / 9',
|
|
||||||
borderRadius: '8px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
boxShadow: 'md',
|
|
||||||
zIndex: 2,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -4,7 +4,7 @@ import { styled } from '@/styled-system/jsx'
|
|||||||
import { ParticipantTile } from '@/features/participantTile/components/ParticipantTile'
|
import { ParticipantTile } from '@/features/participantTile/components/ParticipantTile'
|
||||||
import { usePipElementSize } from '../../hooks/usePipElementSize'
|
import { usePipElementSize } from '../../hooks/usePipElementSize'
|
||||||
import { computePipGridLayout } from '../../utils/pipGrid'
|
import { computePipGridLayout } from '../../utils/pipGrid'
|
||||||
import { getTrackKey } from '../../utils/pipTrackSelection'
|
import { getTrackKey } from '@/features/layout/utils/trackSelection'
|
||||||
|
|
||||||
type PipGridLayoutProps = {
|
type PipGridLayoutProps = {
|
||||||
tracks: TrackReferenceOrPlaceholder[]
|
tracks: TrackReferenceOrPlaceholder[]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { memo } from 'react'
|
|||||||
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
|
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
|
||||||
import { styled } from '@/styled-system/jsx'
|
import { styled } from '@/styled-system/jsx'
|
||||||
import { ParticipantTile } from '@/features/participantTile/components/ParticipantTile'
|
import { ParticipantTile } from '@/features/participantTile/components/ParticipantTile'
|
||||||
import { getTrackKey } from '../../utils/pipTrackSelection'
|
import { getTrackKey } from '@/features/layout/utils/trackSelection'
|
||||||
|
|
||||||
type PipScreenShareLayoutProps = {
|
type PipScreenShareLayoutProps = {
|
||||||
screenShareTrack: TrackReferenceOrPlaceholder
|
screenShareTrack: TrackReferenceOrPlaceholder
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useMemo } from 'react'
|
|||||||
import { usePagination, useTracks } from '@livekit/components-react'
|
import { usePagination, useTracks } from '@livekit/components-react'
|
||||||
import { RoomEvent, Track } from 'livekit-client'
|
import { RoomEvent, Track } from 'livekit-client'
|
||||||
import { styled } from '@/styled-system/jsx'
|
import { styled } from '@/styled-system/jsx'
|
||||||
import { PipFocusLayout } from './PipFocusLayout'
|
import { OneToOneFocusLayout } from '@/features/layout/components/OneToOneFocusLayout'
|
||||||
import { PipGridLayout } from './PipGridLayout'
|
import { PipGridLayout } from './PipGridLayout'
|
||||||
import { PipPagination } from './PipPagination'
|
import { PipPagination } from './PipPagination'
|
||||||
import { PipScreenShareLayout } from './PipScreenShareLayout'
|
import { PipScreenShareLayout } from './PipScreenShareLayout'
|
||||||
@@ -59,9 +59,11 @@ export const PipStage = () => {
|
|||||||
if (cameraTracks.length <= 1) {
|
if (cameraTracks.length <= 1) {
|
||||||
return (
|
return (
|
||||||
<StageFrame>
|
<StageFrame>
|
||||||
<PipFocusLayout
|
<OneToOneFocusLayout
|
||||||
mainTrack={screenShareTrack}
|
mainTrack={screenShareTrack}
|
||||||
thumbnailTrack={cameraTracks[0]}
|
thumbnailTrack={cameraTracks[0]}
|
||||||
|
disableTileControls
|
||||||
|
context="pip"
|
||||||
/>
|
/>
|
||||||
</StageFrame>
|
</StageFrame>
|
||||||
)
|
)
|
||||||
@@ -99,7 +101,12 @@ export const PipStage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StageFrame>
|
<StageFrame>
|
||||||
<PipFocusLayout mainTrack={mainTrack} thumbnailTrack={thumbnailTrack} />
|
<OneToOneFocusLayout
|
||||||
|
mainTrack={mainTrack}
|
||||||
|
thumbnailTrack={thumbnailTrack}
|
||||||
|
disableTileControls
|
||||||
|
context="pip"
|
||||||
|
/>
|
||||||
</StageFrame>
|
</StageFrame>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,9 +46,6 @@
|
|||||||
"passedHint": "Ihr Browser, Ihre Geräte und Ihr Netzwerk sind für eine Besprechung bereit.",
|
"passedHint": "Ihr Browser, Ihre Geräte und Ihr Netzwerk sind für eine Besprechung bereit.",
|
||||||
"partial": "Teilweiser Test",
|
"partial": "Teilweiser Test",
|
||||||
"partialHint": "Einige Prüfungen wurden übersprungen. Erlauben Sie den Zugriff auf Ihre Kamera und Ihr Mikrofon, um diese zu testen.",
|
"partialHint": "Einige Prüfungen wurden übersprungen. Erlauben Sie den Zugriff auf Ihre Kamera und Ihr Mikrofon, um diese zu testen.",
|
||||||
"warning": "Verbindung nicht optimal",
|
|
||||||
"warningHint": "Achtung: Sie befinden sich nicht in optimalen Bedingungen für die Nutzung des Tools. Ihre Medien werden nicht über UDP übertragen (Rückgriff auf ein TURN-Relay über TCP oder TLS), was die Audio- und Videoqualität beeinträchtigen kann.",
|
|
||||||
"warningDocLink": "Technische Dokumentation für Ihre IT-Abteilung",
|
|
||||||
"failed_one": "{{count}} Prüfung fehlgeschlagen",
|
"failed_one": "{{count}} Prüfung fehlgeschlagen",
|
||||||
"failed_other": "{{count}} Prüfungen fehlgeschlagen",
|
"failed_other": "{{count}} Prüfungen fehlgeschlagen",
|
||||||
"failedHint": "Öffnen Sie die fehlgeschlagenen Prüfungen für weitere Details und senden Sie den Bericht an Ihre IT-Abteilung."
|
"failedHint": "Öffnen Sie die fehlgeschlagenen Prüfungen für weitere Details und senden Sie den Bericht an Ihre IT-Abteilung."
|
||||||
|
|||||||
@@ -46,9 +46,6 @@
|
|||||||
"passedHint": "Your browser, your devices and your network are ready for a meeting.",
|
"passedHint": "Your browser, your devices and your network are ready for a meeting.",
|
||||||
"partial": "Partially tested",
|
"partial": "Partially tested",
|
||||||
"partialHint": "Some checks were skipped. Allow access to your camera and microphone to test them.",
|
"partialHint": "Some checks were skipped. Allow access to your camera and microphone to test them.",
|
||||||
"warning": "Suboptimal connection",
|
|
||||||
"warningHint": "Warning: you are not in optimal conditions to use the tool. Your media is not carried over UDP (it falls back to a TURN relay over TCP or TLS), which can degrade audio and video quality.",
|
|
||||||
"warningDocLink": "Technical documentation for your IT department",
|
|
||||||
"failed_one": "{{count}} check failed",
|
"failed_one": "{{count}} check failed",
|
||||||
"failed_other": "{{count}} checks failed",
|
"failed_other": "{{count}} checks failed",
|
||||||
"failedHint": "Open the failed checks below for details, then send the report to your IT department."
|
"failedHint": "Open the failed checks below for details, then send the report to your IT department."
|
||||||
|
|||||||
@@ -46,9 +46,6 @@
|
|||||||
"passedHint": "Votre navigateur, vos périphériques et votre réseau sont prêts pour une réunion.",
|
"passedHint": "Votre navigateur, vos périphériques et votre réseau sont prêts pour une réunion.",
|
||||||
"partial": "Test partiel",
|
"partial": "Test partiel",
|
||||||
"partialHint": "Certaines vérifications ont été ignorées. Autorisez l'accès à votre caméra et à votre microphone pour les tester.",
|
"partialHint": "Certaines vérifications ont été ignorées. Autorisez l'accès à votre caméra et à votre microphone pour les tester.",
|
||||||
"warning": "Connexion non optimale",
|
|
||||||
"warningHint": "Attention : vous n'êtes pas dans les conditions optimales pour accéder à l'outil. Vos flux ne transitent pas en UDP (repli sur un relais TURN en TCP ou TLS), ce qui peut dégrader la qualité audio et vidéo.",
|
|
||||||
"warningDocLink": "Documentation technique à destination de votre service informatique",
|
|
||||||
"failed_one": "{{count}} vérification en échec",
|
"failed_one": "{{count}} vérification en échec",
|
||||||
"failed_other": "{{count}} vérifications en échec",
|
"failed_other": "{{count}} vérifications en échec",
|
||||||
"failedHint": "Ouvrez les vérifications en échec pour voir le détail, puis transmettez le rapport à votre service informatique."
|
"failedHint": "Ouvrez les vérifications en échec pour voir le détail, puis transmettez le rapport à votre service informatique."
|
||||||
|
|||||||
@@ -46,9 +46,6 @@
|
|||||||
"passedHint": "Je browser, apparaten en netwerk zijn klaar voor een vergadering.",
|
"passedHint": "Je browser, apparaten en netwerk zijn klaar voor een vergadering.",
|
||||||
"partial": "Gedeeltelijke test",
|
"partial": "Gedeeltelijke test",
|
||||||
"partialHint": "Sommige controles zijn overgeslagen. Geef toegang tot je camera en microfoon om deze te testen.",
|
"partialHint": "Sommige controles zijn overgeslagen. Geef toegang tot je camera en microfoon om deze te testen.",
|
||||||
"warning": "Verbinding niet optimaal",
|
|
||||||
"warningHint": "Let op: je bevindt je niet in optimale omstandigheden om de tool te gebruiken. Je media loopt niet via UDP (terugval op een TURN-relay via TCP of TLS), wat de audio- en videokwaliteit kan verminderen.",
|
|
||||||
"warningDocLink": "Technische documentatie voor je IT-afdeling",
|
|
||||||
"failed_one": "{{count}} controle mislukt",
|
"failed_one": "{{count}} controle mislukt",
|
||||||
"failed_other": "{{count}} controles mislukt",
|
"failed_other": "{{count}} controles mislukt",
|
||||||
"failedHint": "Open de mislukte controles voor meer details en stuur het rapport door naar je IT-afdeling."
|
"failedHint": "Open de mislukte controles voor meer details en stuur het rapport door naar je IT-afdeling."
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mail_mjml",
|
"name": "mail_mjml",
|
||||||
"version": "1.28.0",
|
"version": "1.29.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mail_mjml",
|
"name": "mail_mjml",
|
||||||
"version": "1.28.0",
|
"version": "1.29.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@html-to/text-cli": "0.6.1",
|
"@html-to/text-cli": "0.6.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mail_mjml",
|
"name": "mail_mjml",
|
||||||
"version": "1.28.0",
|
"version": "1.29.0",
|
||||||
"description": "An util to generate html and text django's templates from mjml templates",
|
"description": "An util to generate html and text django's templates from mjml templates",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "sdk",
|
"name": "sdk",
|
||||||
"version": "1.28.0",
|
"version": "1.29.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sdk",
|
"name": "sdk",
|
||||||
"version": "1.28.0",
|
"version": "1.29.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"./library",
|
"./library",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sdk",
|
"name": "sdk",
|
||||||
"version": "1.28.0",
|
"version": "1.29.0",
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "summary"
|
name = "summary"
|
||||||
version = "1.28.0"
|
version = "1.29.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi[standard]>=0.105.0",
|
"fastapi[standard]>=0.105.0",
|
||||||
"uvicorn>=0.24.0",
|
"uvicorn>=0.24.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user