mirror of
https://github.com/suitenumerique/meet.git
synced 2026-07-27 12:19:10 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 839a8f1c71 | |||
| 40ef420cda | |||
| d995bd9041 | |||
| 8ac4c2409d | |||
| e47ad42656 | |||
| 2509452c52 | |||
| 1022780027 |
@@ -82,7 +82,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "18"
|
||||
node-version: "22"
|
||||
|
||||
- name: Restore the mail templates
|
||||
uses: actions/cache@v5
|
||||
|
||||
@@ -8,6 +8,11 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- 🗑️(settings) deprecate SUMMARY_SERVICE_VERSION=1
|
||||
- ⬆️(mail) update mjml to v5 and @html-to/text-cli
|
||||
|
||||
## [1.23.0] - 2026-07-08
|
||||
|
||||
### Added
|
||||
@@ -50,6 +55,7 @@ and this project adheres to
|
||||
- ✨(backend) add fallback to save recordings without S3/MinIO webhooks
|
||||
- 🩹(frontend) enable screen share button in PiP #1458
|
||||
- 🐛(backend) support unencoded S3 notification object keys #1455
|
||||
- ✨(frontend) prioritize screen share in picture-in-picture layout #1467
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
+12
@@ -15,3 +15,15 @@ the following command inside your docker container:
|
||||
(Note : in your development environment, you can `make migrate`.)
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## v1.23.0
|
||||
|
||||
As part of the 1.23.0 release, the legacy `api/v1` implementation has been removed from the _experimental_ Summary service and Meet has been migrated to the new `api/v2`.
|
||||
|
||||
**To avoid a breaking change, the Meet backend continues to use the Summary service's v1-compatible API format by default (`SUMMARY_SERVICE_VERSION` setting defaults to `1`).**
|
||||
|
||||
If you are deploying both Meet and Summary from this repository, you must configure the Meet backend to use the v2 API by setting the following environment variable `SUMMARY_SERVICE_VERSION=2`.
|
||||
|
||||
If you are upgrading only the Meet deployment while keeping an older Summary v1 compatible deployment, no action is required, as the v1-compatible API remains the default.
|
||||
|
||||
Note that we plan on removing the legacy `v1` summary compatibility in a future major version. If you have your own implementation for the summary service, we recommend updating its API contract and setting `SUMMARY_SERVICE_VERSION=2`.
|
||||
|
||||
@@ -13,6 +13,7 @@ https://docs.djangoproject.com/en/3.1/ref/settings/
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
import json
|
||||
import warnings
|
||||
from os import path
|
||||
from socket import gethostbyname, gethostname
|
||||
|
||||
@@ -1128,6 +1129,20 @@ class Base(Configuration):
|
||||
"FILE_UPLOAD_TMP_PATH cannot be the same as FILE_UPLOAD_PATH"
|
||||
)
|
||||
|
||||
if (
|
||||
cls.SUMMARY_SERVICE_VERSION == 1
|
||||
and cls.SUMMARY_SERVICE_ENDPOINT is not None
|
||||
):
|
||||
warnings.warn(
|
||||
"SUMMARY_SERVICE_VERSION=1 is deprecated. "
|
||||
"The legacy v1 API has been removed from the experimental "
|
||||
"Summary service. Please update your Summary service deployment to "
|
||||
"the v2 API and set SUMMARY_SERVICE_VERSION=2.",
|
||||
# We use UserWarning to make sure it shows up in production deployment
|
||||
UserWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
# The SENTRY_DSN setting should be available to activate sentry for an environment
|
||||
if cls.SENTRY_DSN is not None:
|
||||
sentry_sdk.init(
|
||||
|
||||
@@ -26,6 +26,7 @@ export const PipFocusLayout = memo(
|
||||
<ParticipantTile
|
||||
key={getTrackKey(mainTrack)}
|
||||
trackRef={mainTrack}
|
||||
disableTileControls
|
||||
/>
|
||||
</MainSlot>
|
||||
)}
|
||||
@@ -34,6 +35,7 @@ export const PipFocusLayout = memo(
|
||||
<ParticipantTile
|
||||
key={getTrackKey(thumbnailTrack)}
|
||||
trackRef={thumbnailTrack}
|
||||
disableTileControls
|
||||
/>
|
||||
</Thumbnail>
|
||||
)}
|
||||
@@ -48,9 +50,10 @@ const FocusContainer = styled('div', {
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '4px',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'primaryDark.100',
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -58,6 +61,8 @@ const MainSlot = styled('div', {
|
||||
base: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
'& .lk-participant-media-video': {
|
||||
objectFit: 'contain',
|
||||
},
|
||||
@@ -67,13 +72,13 @@ const MainSlot = styled('div', {
|
||||
const Thumbnail = styled('div', {
|
||||
base: {
|
||||
position: 'absolute',
|
||||
right: '1rem',
|
||||
bottom: '1rem',
|
||||
right: '1.25rem',
|
||||
bottom: '1.25rem',
|
||||
width: '42%',
|
||||
maxWidth: '220px',
|
||||
minWidth: '140px',
|
||||
aspectRatio: '16 / 9',
|
||||
borderRadius: '4px',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: 'md',
|
||||
zIndex: 2,
|
||||
|
||||
@@ -41,7 +41,7 @@ export const PipGridLayout = memo(({ tracks }: PipGridLayoutProps) => {
|
||||
<GridContainer ref={containerRef} style={gridStyle}>
|
||||
{tracks.map((track, index) => (
|
||||
<GridCell key={getTrackKey(track)} style={placements[index]}>
|
||||
<ParticipantTile trackRef={track} />
|
||||
<ParticipantTile trackRef={track} disableTileControls />
|
||||
</GridCell>
|
||||
))}
|
||||
</GridContainer>
|
||||
@@ -54,7 +54,8 @@ const GridContainer = styled('div', {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'grid',
|
||||
gap: '0.25rem',
|
||||
gap: '0.5rem',
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -63,7 +64,7 @@ const GridCell = styled('div', {
|
||||
position: 'relative',
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
borderRadius: '4px',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'primaryDark.100',
|
||||
// Paint on own layer so FLIP transforms don't trigger layout thrash.
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { memo } from 'react'
|
||||
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile'
|
||||
import { getTrackKey } from '../../utils/pipTrackSelection'
|
||||
|
||||
type PipScreenShareLayoutProps = {
|
||||
screenShareTrack: TrackReferenceOrPlaceholder
|
||||
cameraTracks: TrackReferenceOrPlaceholder[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout when a screen share is active.
|
||||
* Camera tiles are shown as a compact row at the top; the screen share occupies
|
||||
* the remaining space below, much larger than the camera tiles.
|
||||
*/
|
||||
export const PipScreenShareLayout = memo(
|
||||
({ screenShareTrack, cameraTracks }: PipScreenShareLayoutProps) => {
|
||||
return (
|
||||
<LayoutContainer>
|
||||
{cameraTracks.length > 0 && (
|
||||
<CameraTilesRow>
|
||||
{cameraTracks.map((track) => (
|
||||
<CameraTile key={getTrackKey(track)}>
|
||||
<ParticipantTile trackRef={track} disableTileControls />
|
||||
</CameraTile>
|
||||
))}
|
||||
</CameraTilesRow>
|
||||
)}
|
||||
<ScreenShareSlot>
|
||||
<ParticipantTile trackRef={screenShareTrack} disableTileControls />
|
||||
</ScreenShareSlot>
|
||||
</LayoutContainer>
|
||||
)
|
||||
}
|
||||
)
|
||||
PipScreenShareLayout.displayName = 'PipScreenShareLayout'
|
||||
|
||||
const LayoutContainer = styled('div', {
|
||||
base: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.5rem',
|
||||
boxSizing: 'border-box',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
})
|
||||
|
||||
const CameraTilesRow = styled('div', {
|
||||
base: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
gap: '0.5rem',
|
||||
flexShrink: 0,
|
||||
height: '22%',
|
||||
minHeight: '60px',
|
||||
maxHeight: '120px',
|
||||
},
|
||||
})
|
||||
|
||||
const CameraTile = styled('div', {
|
||||
base: {
|
||||
position: 'relative',
|
||||
flex: '0 1 auto',
|
||||
height: '100%',
|
||||
aspectRatio: '16 / 9',
|
||||
minWidth: 0,
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'primaryDark.100',
|
||||
'& .lk-participant-tile': {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const ScreenShareSlot = styled('div', {
|
||||
base: {
|
||||
position: 'relative',
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'primaryDark.100',
|
||||
'& .lk-participant-tile': {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
'& .lk-participant-media-video': {
|
||||
objectFit: 'contain',
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useMemo } from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import { usePagination, useTracks } from '@livekit/components-react'
|
||||
import { RoomEvent, Track } from 'livekit-client'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { PipFocusLayout } from './PipFocusLayout'
|
||||
import { PipGridLayout } from './PipGridLayout'
|
||||
import { PipPagination } from './PipPagination'
|
||||
import { PipScreenShareLayout } from './PipScreenShareLayout'
|
||||
import { StageFrame } from './StageFrame'
|
||||
import { MAX_PIP_TILES } from '../../utils/pipGrid'
|
||||
import {
|
||||
@@ -13,9 +14,12 @@ import {
|
||||
} from '@livekit/components-core'
|
||||
|
||||
/**
|
||||
* PipStage picks between two layouts based on track count:
|
||||
* - Focus mode (≤ 2 tracks): one main track + one thumbnail overlay.
|
||||
* - Grid mode (3+ tracks): adaptive tiling.
|
||||
* PipStage picks between three layouts:
|
||||
* - Screen share mode (any screen share active):
|
||||
* small camera tiles in a row at the top, large screen share below.
|
||||
* - Grid mode (3+ camera tracks, no screen share): adaptive tiling.
|
||||
* - Focus mode (≤ 2 camera tracks, no screen share): one main track
|
||||
* + one thumbnail overlay.
|
||||
*/
|
||||
export const PipStage = () => {
|
||||
const tracks = useTracks(
|
||||
@@ -41,51 +45,55 @@ export const PipStage = () => {
|
||||
[tracks]
|
||||
)
|
||||
|
||||
// Grid mode order: screen share leads, then cameras (already ordered by
|
||||
// active speaker via the `ActiveSpeakersChanged` update above).
|
||||
const gridTracks = useMemo(
|
||||
() =>
|
||||
screenShareTrack ? [screenShareTrack, ...cameraTracks] : cameraTracks,
|
||||
[screenShareTrack, cameraTracks]
|
||||
)
|
||||
// Cap camera tiles in screen-share mode. Called unconditionally for hook rules.
|
||||
const paginatedCameraTracks = usePagination(MAX_PIP_TILES - 1, cameraTracks)
|
||||
|
||||
// Cap the grid at MAX_PIP_TILES per page. `usePagination` keeps the visible
|
||||
// page visually stable (active/recent speakers stay put) via its internal
|
||||
// `useVisualStableUpdate`. Called unconditionally to respect hook rules.
|
||||
const pagination = usePagination(MAX_PIP_TILES, gridTracks)
|
||||
// Cap the grid at MAX_PIP_TILES per page for the non-screenshare grid mode.
|
||||
const pagination = usePagination(MAX_PIP_TILES, cameraTracks)
|
||||
|
||||
if (tracks.length === 0) return null
|
||||
|
||||
/**
|
||||
* The focus layout shows one main track with one thumbnail overlay,
|
||||
* so it can only fit 2 tracks. Beyond that we switch to the grid.
|
||||
*/
|
||||
if (gridTracks.length > 2) {
|
||||
return (
|
||||
<StageWrapper>
|
||||
// Screen share active
|
||||
if (screenShareTrack) {
|
||||
// Solo presenter: screen share fills the area, camera as small thumbnail
|
||||
if (cameraTracks.length <= 1) {
|
||||
return (
|
||||
<StageFrame>
|
||||
<PipGridLayout tracks={pagination.tracks} />
|
||||
<PipFocusLayout
|
||||
mainTrack={screenShareTrack}
|
||||
thumbnailTrack={cameraTracks[0]}
|
||||
/>
|
||||
</StageFrame>
|
||||
<PipPagination
|
||||
totalPageCount={pagination.totalPageCount}
|
||||
currentPage={pagination.currentPage}
|
||||
nextPage={pagination.nextPage}
|
||||
prevPage={pagination.prevPage}
|
||||
)
|
||||
}
|
||||
// Multiple cameras: camera row at top, screen share below
|
||||
return (
|
||||
<PaginatedStage pagination={paginatedCameraTracks}>
|
||||
<PipScreenShareLayout
|
||||
screenShareTrack={screenShareTrack}
|
||||
cameraTracks={paginatedCameraTracks.tracks}
|
||||
/>
|
||||
</StageWrapper>
|
||||
</PaginatedStage>
|
||||
)
|
||||
}
|
||||
|
||||
// 3+ camera tracks → adaptive grid
|
||||
if (cameraTracks.length > 2) {
|
||||
return (
|
||||
<PaginatedStage pagination={pagination}>
|
||||
<PipGridLayout tracks={pagination.tracks} />
|
||||
</PaginatedStage>
|
||||
)
|
||||
}
|
||||
|
||||
// ≤ 2 camera tracks → focus layout (main + optional thumbnail)
|
||||
const localCameraTrack = cameraTracks.find(
|
||||
(track) => track.participant?.isLocal
|
||||
)
|
||||
|
||||
const remoteCameraTrack = cameraTracks.find(
|
||||
(track) => !track.participant?.isLocal
|
||||
)
|
||||
|
||||
const mainTrack = screenShareTrack ?? remoteCameraTrack ?? localCameraTrack
|
||||
|
||||
const mainTrack = remoteCameraTrack ?? localCameraTrack
|
||||
const thumbnailTrack =
|
||||
mainTrack === localCameraTrack ? undefined : localCameraTrack
|
||||
|
||||
@@ -96,6 +104,31 @@ export const PipStage = () => {
|
||||
)
|
||||
}
|
||||
|
||||
type PaginationResult = {
|
||||
totalPageCount: number
|
||||
currentPage: number
|
||||
nextPage: () => void
|
||||
prevPage: () => void
|
||||
}
|
||||
|
||||
const PaginatedStage = ({
|
||||
pagination,
|
||||
children,
|
||||
}: {
|
||||
pagination: PaginationResult
|
||||
children: React.ReactNode
|
||||
}) => (
|
||||
<StageWrapper>
|
||||
<StageFrame>{children}</StageFrame>
|
||||
<PipPagination
|
||||
totalPageCount={pagination.totalPageCount}
|
||||
currentPage={pagination.currentPage}
|
||||
nextPage={pagination.nextPage}
|
||||
prevPage={pagination.prevPage}
|
||||
/>
|
||||
</StageWrapper>
|
||||
)
|
||||
|
||||
const StageWrapper = styled('div', {
|
||||
base: {
|
||||
display: 'flex',
|
||||
|
||||
@@ -25,8 +25,8 @@ const Container = styled('div', {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
marginLeft: '0.5rem',
|
||||
marginRight: '0.5rem',
|
||||
padding: '0.5rem',
|
||||
boxSizing: 'border-box',
|
||||
borderRadius: '4px',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
|
||||
@@ -52,6 +52,7 @@ export function TrackRefContextIfNeeded(
|
||||
|
||||
interface ParticipantTileExtendedProps extends ParticipantTileProps {
|
||||
disableMetadata?: boolean
|
||||
disableTileControls?: boolean
|
||||
}
|
||||
|
||||
export const ParticipantTile: (
|
||||
@@ -66,6 +67,7 @@ export const ParticipantTile: (
|
||||
onParticipantClick,
|
||||
disableSpeakingIndicator,
|
||||
disableMetadata,
|
||||
disableTileControls,
|
||||
...htmlProps
|
||||
}: ParticipantTileExtendedProps,
|
||||
ref
|
||||
@@ -231,7 +233,7 @@ export const ParticipantTile: (
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!disableMetadata && (
|
||||
{!disableMetadata && !disableTileControls && (
|
||||
<ParticipantTileFocus
|
||||
trackRef={trackReference}
|
||||
hasKeyboardFocus={hasKeyboardFocus}
|
||||
|
||||
@@ -6,4 +6,4 @@ DIR_MAILS="../backend/core/templates/mail/html/"
|
||||
if [ ! -d "${DIR_MAILS}" ]; then
|
||||
mkdir -p "${DIR_MAILS}";
|
||||
fi
|
||||
mjml mjml/*.mjml -o "${DIR_MAILS}";
|
||||
mjml mjml/*.mjml -o "${DIR_MAILS}" --config.allowIncludes true;
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
<mj-include path="./partial/header.mjml" />
|
||||
|
||||
<mj-body mj-class="bg--blue-100">
|
||||
<!--
|
||||
We load django tags here so they appear in the body of the HTML output.
|
||||
This ensures html-to-text also includes them in the plain text template.
|
||||
-->
|
||||
<mj-raw>{% load i18n static extra_tags %}</mj-raw>
|
||||
<mj-wrapper css-class="wrapper" padding="0 40px 40px 40px">
|
||||
<mj-section css-class="wrapper-logo">
|
||||
<mj-column>
|
||||
@@ -59,7 +64,7 @@
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-wrapper>
|
||||
</mj-body>
|
||||
|
||||
<mj-include path="./partial/footer.mjml" />
|
||||
<mj-include path="./partial/footer.mjml" />
|
||||
</mj-body>
|
||||
</mjml>
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
<mj-head>
|
||||
<mj-title>{{ title }}</mj-title>
|
||||
<mj-preview>
|
||||
<!--
|
||||
We load django tags here, in this way there are put within the body in html output
|
||||
so the html-to-text command includes it within its output
|
||||
-->
|
||||
{% load i18n static extra_tags %}
|
||||
{{ title }}
|
||||
</mj-preview>
|
||||
<mj-preview>{{ title }}</mj-preview>
|
||||
<mj-font name="Roboto" href="https://fonts.bunny.net/css?family=roboto:400,700,900" />
|
||||
<mj-attributes>
|
||||
<mj-font name="Roboto" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700;900&display=swap" />
|
||||
<mj-all
|
||||
font-family="Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif"
|
||||
font-size="16px"
|
||||
line-height="1.5em"
|
||||
color="#3A3A3A"
|
||||
/>
|
||||
<mj-text font-family="Roboto, sans-serif" />
|
||||
<mj-button font-family="Roboto, sans-serif" />
|
||||
</mj-attributes>
|
||||
<mj-style>
|
||||
/* Reset */
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
<mj-include path="./partial/header.mjml" />
|
||||
|
||||
<mj-body mj-class="bg--blue-100">
|
||||
<!--
|
||||
We load django tags here so they appear in the body of the HTML output.
|
||||
This ensures html-to-text also includes them in the plain text template.
|
||||
-->
|
||||
<mj-raw>{% load i18n static extra_tags %}</mj-raw>
|
||||
<mj-wrapper css-class="wrapper" padding="5px 25px 0px 25px">
|
||||
<mj-section css-class="wrapper-logo">
|
||||
<mj-column>
|
||||
|
||||
Generated
+1902
-940
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,8 @@
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@html-to/text-cli": "0.5.4",
|
||||
"mjml": "4.18.0"
|
||||
"@html-to/text-cli": "0.6.0",
|
||||
"mjml": "5.4.0"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user