Compare commits

...

6 Commits

Author SHA1 Message Date
c8024484b3 chore(main): release 0.22.3 (#761) 2025-12-02 09:08:22 +01:00
28f87c09dc fix: align daily room settings (#759)
* Switch platform ui

* Update room settings based on platform

* Add local and none recording options to daily

* Don't create tokens for unauthentikated users

* Enable knocking for private rooms

* Create new meeting on room settings change

* Always use 2-200 option for daily

* Show recording start trigger for daily

* Fix broken test
2025-12-02 09:06:36 +01:00
dabf7251db chore(main): release 0.22.2 (#756) 2025-12-01 23:39:32 -05:00
Igor Monadical
b51b7aa917 fix: Skip mixdown for multitrack (#760)
* multitrack mixdown optimisation

* skip mixdown for multitrack

* skip mixdown for multitrack

---------

Co-authored-by: Igor Loskutov <igor.loskutoff@gmail.com>
2025-12-01 23:35:12 -05:00
Igor Monadical
a8983b4e7e daily auth hotfix (#757)
Co-authored-by: Igor Loskutov <igor.loskutoff@gmail.com>
2025-11-28 14:52:59 -05:00
Igor Monadical
fe47c46489 fix: daily auto refresh fix (#755)
* daily auto refresh fix

* Update www/app/lib/AuthProvider.tsx

Co-authored-by: pr-agent-monadical[bot] <198624643+pr-agent-monadical[bot]@users.noreply.github.com>

* Update www/app/[roomName]/components/DailyRoom.tsx

Co-authored-by: pr-agent-monadical[bot] <198624643+pr-agent-monadical[bot]@users.noreply.github.com>

* fix bot lint

---------

Co-authored-by: Igor Loskutov <igor.loskutoff@gmail.com>
Co-authored-by: pr-agent-monadical[bot] <198624643+pr-agent-monadical[bot]@users.noreply.github.com>
2025-11-27 18:31:03 -05:00
11 changed files with 265 additions and 92 deletions

View File

@@ -1,5 +1,20 @@
# Changelog # Changelog
## [0.22.3](https://github.com/Monadical-SAS/reflector/compare/v0.22.2...v0.22.3) (2025-12-02)
### Bug Fixes
* align daily room settings ([#759](https://github.com/Monadical-SAS/reflector/issues/759)) ([28f87c0](https://github.com/Monadical-SAS/reflector/commit/28f87c09dc459846873d0dde65b03e3d7b2b9399))
## [0.22.2](https://github.com/Monadical-SAS/reflector/compare/v0.22.1...v0.22.2) (2025-12-02)
### Bug Fixes
* daily auto refresh fix ([#755](https://github.com/Monadical-SAS/reflector/issues/755)) ([fe47c46](https://github.com/Monadical-SAS/reflector/commit/fe47c46489c5aa0cc538109f7559cc9accb35c01))
* Skip mixdown for multitrack ([#760](https://github.com/Monadical-SAS/reflector/issues/760)) ([b51b7aa](https://github.com/Monadical-SAS/reflector/commit/b51b7aa9176c1a53ba57ad99f5e976c804a1e80c))
## [0.22.1](https://github.com/Monadical-SAS/reflector/compare/v0.22.0...v0.22.1) (2025-11-27) ## [0.22.1](https://github.com/Monadical-SAS/reflector/compare/v0.22.0...v0.22.1) (2025-11-27)

View File

@@ -40,6 +40,10 @@ class RoomProperties(BaseModel):
) )
enable_chat: bool = Field(default=True, description="Enable in-meeting chat") enable_chat: bool = Field(default=True, description="Enable in-meeting chat")
enable_screenshare: bool = Field(default=True, description="Enable screen sharing") enable_screenshare: bool = Field(default=True, description="Enable screen sharing")
enable_knocking: bool = Field(
default=False,
description="Enable knocking for private rooms (allows participants to request access)",
)
start_video_off: bool = Field( start_video_off: bool = Field(
default=False, description="Start with video off for all participants" default=False, description="Start with video off for all participants"
) )

View File

@@ -31,6 +31,7 @@ from reflector.processors import AudioFileWriterProcessor
from reflector.processors.audio_waveform_processor import AudioWaveformProcessor from reflector.processors.audio_waveform_processor import AudioWaveformProcessor
from reflector.processors.types import TitleSummary from reflector.processors.types import TitleSummary
from reflector.processors.types import Transcript as TranscriptType from reflector.processors.types import Transcript as TranscriptType
from reflector.settings import settings
from reflector.storage import Storage, get_transcripts_storage from reflector.storage import Storage, get_transcripts_storage
from reflector.utils.daily import ( from reflector.utils.daily import (
filter_cam_audio_tracks, filter_cam_audio_tracks,
@@ -631,11 +632,21 @@ class PipelineMainMultitrack(PipelineMainBase):
transcript.data_path.mkdir(parents=True, exist_ok=True) transcript.data_path.mkdir(parents=True, exist_ok=True)
if settings.SKIP_MIXDOWN:
self.logger.warning(
"SKIP_MIXDOWN enabled: Skipping mixdown and waveform generation. "
"UI will have no audio playback or waveform.",
num_tracks=len(padded_track_urls),
transcript_id=transcript.id,
)
else:
mp3_writer = AudioFileWriterProcessor( mp3_writer = AudioFileWriterProcessor(
path=str(transcript.audio_mp3_filename), path=str(transcript.audio_mp3_filename),
on_duration=self.on_duration, on_duration=self.on_duration,
) )
await self.mixdown_tracks(padded_track_urls, mp3_writer, offsets_seconds=None) await self.mixdown_tracks(
padded_track_urls, mp3_writer, offsets_seconds=None
)
await mp3_writer.flush() await mp3_writer.flush()
if not transcript.audio_mp3_filename.exists(): if not transcript.audio_mp3_filename.exists():
@@ -650,7 +661,9 @@ class PipelineMainMultitrack(PipelineMainBase):
await transcript_storage.put_file(storage_path, mp3_file) await transcript_storage.put_file(storage_path, mp3_file)
mp3_url = await transcript_storage.get_file_url(storage_path) mp3_url = await transcript_storage.get_file_url(storage_path)
await transcripts_controller.update(transcript, {"audio_location": "storage"}) await transcripts_controller.update(
transcript, {"audio_location": "storage"}
)
self.logger.info( self.logger.info(
f"Uploaded mixed audio to storage", f"Uploaded mixed audio to storage",

View File

@@ -138,6 +138,14 @@ class Settings(BaseSettings):
DAILY_WEBHOOK_UUID: str | None = ( DAILY_WEBHOOK_UUID: str | None = (
None # Webhook UUID for this environment. Not used by production code None # Webhook UUID for this environment. Not used by production code
) )
# Multitrack processing
# SKIP_MIXDOWN: When True, skips audio mixdown and waveform generation.
# Transcription still works using individual tracks. Useful for:
# - Diagnosing OOM issues in mixdown
# - Fast processing when audio playback is not needed
# Note: UI will have no audio playback or waveform when enabled.
SKIP_MIXDOWN: bool = True
# Platform Configuration # Platform Configuration
DEFAULT_VIDEO_PLATFORM: Platform = WHEREBY_PLATFORM DEFAULT_VIDEO_PLATFORM: Platform = WHEREBY_PLATFORM

View File

@@ -31,6 +31,7 @@ class DailyClient(VideoPlatformClient):
PLATFORM_NAME: Platform = "daily" PLATFORM_NAME: Platform = "daily"
TIMESTAMP_FORMAT = "%Y%m%d%H%M%S" TIMESTAMP_FORMAT = "%Y%m%d%H%M%S"
RECORDING_NONE: RecordingType = "none" RECORDING_NONE: RecordingType = "none"
RECORDING_LOCAL: RecordingType = "local"
RECORDING_CLOUD: RecordingType = "cloud" RECORDING_CLOUD: RecordingType = "cloud"
def __init__(self, config: VideoPlatformConfig): def __init__(self, config: VideoPlatformConfig):
@@ -54,19 +55,23 @@ class DailyClient(VideoPlatformClient):
timestamp = datetime.now().strftime(self.TIMESTAMP_FORMAT) timestamp = datetime.now().strftime(self.TIMESTAMP_FORMAT)
room_name = f"{room_name_prefix}{ROOM_PREFIX_SEPARATOR}{timestamp}" room_name = f"{room_name_prefix}{ROOM_PREFIX_SEPARATOR}{timestamp}"
properties = RoomProperties( enable_recording = None
if room.recording_type == self.RECORDING_LOCAL:
enable_recording = "local"
elif room.recording_type == self.RECORDING_CLOUD:
enable_recording = "raw-tracks" enable_recording = "raw-tracks"
if room.recording_type != self.RECORDING_NONE
else False, properties = RoomProperties(
enable_recording=enable_recording,
enable_chat=True, enable_chat=True,
enable_screenshare=True, enable_screenshare=True,
enable_knocking=room.is_locked,
start_video_off=False, start_video_off=False,
start_audio_off=False, start_audio_off=False,
exp=int(end_date.timestamp()), exp=int(end_date.timestamp()),
) )
# Only configure recordings_bucket if recording is enabled if room.recording_type == self.RECORDING_CLOUD:
if room.recording_type != self.RECORDING_NONE:
daily_storage = get_dailyco_storage() daily_storage = get_dailyco_storage()
assert daily_storage.bucket_name, "S3 bucket must be configured" assert daily_storage.bucket_name, "S3 bucket must be configured"
properties.recordings_bucket = RecordingsBucketConfig( properties.recordings_bucket = RecordingsBucketConfig(
@@ -172,15 +177,16 @@ class DailyClient(VideoPlatformClient):
async def create_meeting_token( async def create_meeting_token(
self, self,
room_name: DailyRoomName, room_name: DailyRoomName,
enable_recording: bool, start_cloud_recording: bool,
enable_recording_ui: bool,
user_id: NonEmptyString | None = None, user_id: NonEmptyString | None = None,
is_owner: bool = False, is_owner: bool = False,
) -> NonEmptyString: ) -> NonEmptyString:
properties = MeetingTokenProperties( properties = MeetingTokenProperties(
room_name=room_name, room_name=room_name,
user_id=user_id, user_id=user_id,
start_cloud_recording=enable_recording, start_cloud_recording=start_cloud_recording,
enable_recording_ui=False, enable_recording_ui=enable_recording_ui,
is_owner=is_owner, is_owner=is_owner,
) )
request = CreateMeetingTokenRequest(properties=properties) request = CreateMeetingTokenRequest(properties=properties)

View File

@@ -89,7 +89,7 @@ class CreateRoom(BaseModel):
ics_url: Optional[str] = None ics_url: Optional[str] = None
ics_fetch_interval: int = 300 ics_fetch_interval: int = 300
ics_enabled: bool = False ics_enabled: bool = False
platform: Optional[Platform] = None platform: Platform
class UpdateRoom(BaseModel): class UpdateRoom(BaseModel):
@@ -248,7 +248,7 @@ async def rooms_create(
ics_url=room.ics_url, ics_url=room.ics_url,
ics_fetch_interval=room.ics_fetch_interval, ics_fetch_interval=room.ics_fetch_interval,
ics_enabled=room.ics_enabled, ics_enabled=room.ics_enabled,
platform=room.platform or settings.DEFAULT_VIDEO_PLATFORM, platform=room.platform,
) )
@@ -310,6 +310,22 @@ async def rooms_create_meeting(
room=room, current_time=current_time room=room, current_time=current_time
) )
if meeting is not None:
settings_match = (
meeting.is_locked == room.is_locked
and meeting.room_mode == room.room_mode
and meeting.recording_type == room.recording_type
and meeting.recording_trigger == room.recording_trigger
and meeting.platform == room.platform
)
if not settings_match:
logger.info(
f"Room settings changed for {room_name}, creating new meeting",
room_id=room.id,
old_meeting_id=meeting.id,
)
meeting = None
if meeting is None: if meeting is None:
end_date = current_time + timedelta(hours=8) end_date = current_time + timedelta(hours=8)
@@ -549,21 +565,16 @@ async def rooms_join_meeting(
if meeting.end_date <= current_time: if meeting.end_date <= current_time:
raise HTTPException(status_code=400, detail="Meeting has ended") raise HTTPException(status_code=400, detail="Meeting has ended")
if meeting.platform == "daily": if meeting.platform == "daily" and user_id is not None:
client = create_platform_client(meeting.platform) client = create_platform_client(meeting.platform)
enable_recording = room.recording_trigger != "none"
token = await client.create_meeting_token( token = await client.create_meeting_token(
meeting.room_name, meeting.room_name,
enable_recording=enable_recording, start_cloud_recording=meeting.recording_type == "cloud",
enable_recording_ui=meeting.recording_type == "local",
user_id=user_id, user_id=user_id,
is_owner=user_id == room.user_id, is_owner=user_id == room.user_id,
) )
meeting = meeting.model_copy() meeting = meeting.model_copy()
meeting.room_url = add_query_param(meeting.room_url, "t", token) meeting.room_url = add_query_param(meeting.room_url, "t", token)
if meeting.host_room_url:
meeting.host_room_url = add_query_param(meeting.host_room_url, "t", token)
if user_id != room.user_id and meeting.platform == "whereby":
meeting.host_room_url = ""
return meeting return meeting

View File

@@ -67,6 +67,11 @@ const recordingTypeOptions: SelectOption[] = [
{ label: "Cloud", value: "cloud" }, { label: "Cloud", value: "cloud" },
]; ];
const platformOptions: SelectOption[] = [
{ label: "Whereby", value: "whereby" },
{ label: "Daily", value: "daily" },
];
const roomInitialState = { const roomInitialState = {
name: "", name: "",
zulipAutoPost: false, zulipAutoPost: false,
@@ -82,6 +87,7 @@ const roomInitialState = {
icsUrl: "", icsUrl: "",
icsEnabled: false, icsEnabled: false,
icsFetchInterval: 5, icsFetchInterval: 5,
platform: "whereby",
}; };
export default function RoomsList() { export default function RoomsList() {
@@ -99,6 +105,11 @@ export default function RoomsList() {
const recordingTypeCollection = createListCollection({ const recordingTypeCollection = createListCollection({
items: recordingTypeOptions, items: recordingTypeOptions,
}); });
const platformCollection = createListCollection({
items: platformOptions,
});
const [roomInput, setRoomInput] = useState<null | typeof roomInitialState>( const [roomInput, setRoomInput] = useState<null | typeof roomInitialState>(
null, null,
); );
@@ -143,15 +154,24 @@ export default function RoomsList() {
zulipStream: detailedEditedRoom.zulip_stream, zulipStream: detailedEditedRoom.zulip_stream,
zulipTopic: detailedEditedRoom.zulip_topic, zulipTopic: detailedEditedRoom.zulip_topic,
isLocked: detailedEditedRoom.is_locked, isLocked: detailedEditedRoom.is_locked,
roomMode: detailedEditedRoom.room_mode, roomMode:
detailedEditedRoom.platform === "daily"
? "group"
: detailedEditedRoom.room_mode,
recordingType: detailedEditedRoom.recording_type, recordingType: detailedEditedRoom.recording_type,
recordingTrigger: detailedEditedRoom.recording_trigger, recordingTrigger:
detailedEditedRoom.platform === "daily"
? detailedEditedRoom.recording_type === "cloud"
? "automatic-2nd-participant"
: "none"
: detailedEditedRoom.recording_trigger,
isShared: detailedEditedRoom.is_shared, isShared: detailedEditedRoom.is_shared,
webhookUrl: detailedEditedRoom.webhook_url || "", webhookUrl: detailedEditedRoom.webhook_url || "",
webhookSecret: detailedEditedRoom.webhook_secret || "", webhookSecret: detailedEditedRoom.webhook_secret || "",
icsUrl: detailedEditedRoom.ics_url || "", icsUrl: detailedEditedRoom.ics_url || "",
icsEnabled: detailedEditedRoom.ics_enabled || false, icsEnabled: detailedEditedRoom.ics_enabled || false,
icsFetchInterval: detailedEditedRoom.ics_fetch_interval || 5, icsFetchInterval: detailedEditedRoom.ics_fetch_interval || 5,
platform: detailedEditedRoom.platform,
} }
: null, : null,
[detailedEditedRoom], [detailedEditedRoom],
@@ -277,21 +297,32 @@ export default function RoomsList() {
return; return;
} }
const platform: "whereby" | "daily" | null =
room.platform === "whereby" || room.platform === "daily"
? room.platform
: null;
const roomData = { const roomData = {
name: room.name, name: room.name,
zulip_auto_post: room.zulipAutoPost, zulip_auto_post: room.zulipAutoPost,
zulip_stream: room.zulipStream, zulip_stream: room.zulipStream,
zulip_topic: room.zulipTopic, zulip_topic: room.zulipTopic,
is_locked: room.isLocked, is_locked: room.isLocked,
room_mode: room.roomMode, room_mode: platform === "daily" ? "group" : room.roomMode,
recording_type: room.recordingType, recording_type: room.recordingType,
recording_trigger: room.recordingTrigger, recording_trigger:
platform === "daily"
? room.recordingType === "cloud"
? "automatic-2nd-participant"
: "none"
: room.recordingTrigger,
is_shared: room.isShared, is_shared: room.isShared,
webhook_url: room.webhookUrl, webhook_url: room.webhookUrl,
webhook_secret: room.webhookSecret, webhook_secret: room.webhookSecret,
ics_url: room.icsUrl, ics_url: room.icsUrl,
ics_enabled: room.icsEnabled, ics_enabled: room.icsEnabled,
ics_fetch_interval: room.icsFetchInterval, ics_fetch_interval: room.icsFetchInterval,
platform,
}; };
if (isEditing) { if (isEditing) {
@@ -339,15 +370,21 @@ export default function RoomsList() {
zulipStream: roomData.zulip_stream, zulipStream: roomData.zulip_stream,
zulipTopic: roomData.zulip_topic, zulipTopic: roomData.zulip_topic,
isLocked: roomData.is_locked, isLocked: roomData.is_locked,
roomMode: roomData.room_mode, roomMode: roomData.platform === "daily" ? "group" : roomData.room_mode, // Daily always uses 2-200
recordingType: roomData.recording_type, recordingType: roomData.recording_type,
recordingTrigger: roomData.recording_trigger, recordingTrigger:
roomData.platform === "daily"
? roomData.recording_type === "cloud"
? "automatic-2nd-participant"
: "none"
: roomData.recording_trigger,
isShared: roomData.is_shared, isShared: roomData.is_shared,
webhookUrl: roomData.webhook_url || "", webhookUrl: roomData.webhook_url || "",
webhookSecret: roomData.webhook_secret || "", webhookSecret: roomData.webhook_secret || "",
icsUrl: roomData.ics_url || "", icsUrl: roomData.ics_url || "",
icsEnabled: roomData.ics_enabled || false, icsEnabled: roomData.ics_enabled || false,
icsFetchInterval: roomData.ics_fetch_interval || 5, icsFetchInterval: roomData.ics_fetch_interval || 5,
platform: roomData.platform,
}); });
setEditRoomId(roomId); setEditRoomId(roomId);
setIsEditing(true); setIsEditing(true);
@@ -482,6 +519,48 @@ export default function RoomsList() {
)} )}
</Field.Root> </Field.Root>
<Field.Root mt={4}>
<Field.Label>Platform</Field.Label>
<Select.Root
value={[room.platform]}
onValueChange={(e) => {
const newPlatform = e.value[0] as "whereby" | "daily";
const updates: Partial<typeof room> = {
platform: newPlatform,
};
if (newPlatform === "daily") {
updates.roomMode = "group";
updates.recordingTrigger =
room.recordingType === "cloud"
? "automatic-2nd-participant"
: "none";
}
setRoomInput({ ...room, ...updates });
}}
collection={platformCollection}
>
<Select.HiddenSelect />
<Select.Control>
<Select.Trigger>
<Select.ValueText placeholder="Select platform" />
</Select.Trigger>
<Select.IndicatorGroup>
<Select.Indicator />
</Select.IndicatorGroup>
</Select.Control>
<Select.Positioner>
<Select.Content>
{platformOptions.map((option) => (
<Select.Item key={option.value} item={option}>
{option.label}
<Select.ItemIndicator />
</Select.Item>
))}
</Select.Content>
</Select.Positioner>
</Select.Root>
</Field.Root>
<Field.Root mt={4}> <Field.Root mt={4}>
<Checkbox.Root <Checkbox.Root
name="isLocked" name="isLocked"
@@ -512,6 +591,7 @@ export default function RoomsList() {
setRoomInput({ ...room, roomMode: e.value[0] }) setRoomInput({ ...room, roomMode: e.value[0] })
} }
collection={roomModeCollection} collection={roomModeCollection}
disabled={room.platform === "daily"}
> >
<Select.HiddenSelect /> <Select.HiddenSelect />
<Select.Control> <Select.Control>
@@ -538,16 +618,26 @@ export default function RoomsList() {
<Field.Label>Recording type</Field.Label> <Field.Label>Recording type</Field.Label>
<Select.Root <Select.Root
value={[room.recordingType]} value={[room.recordingType]}
onValueChange={(e) => onValueChange={(e) => {
setRoomInput({ const newRecordingType = e.value[0];
...room, const updates: Partial<typeof room> = {
recordingType: e.value[0], recordingType: newRecordingType,
recordingTrigger: };
e.value[0] !== "cloud" // For Daily: if cloud, use automatic; otherwise none
if (room.platform === "daily") {
updates.recordingTrigger =
newRecordingType === "cloud"
? "automatic-2nd-participant"
: "none";
} else {
// For Whereby: if not cloud, set to none
updates.recordingTrigger =
newRecordingType !== "cloud"
? "none" ? "none"
: room.recordingTrigger, : room.recordingTrigger;
})
} }
setRoomInput({ ...room, ...updates });
}}
collection={recordingTypeCollection} collection={recordingTypeCollection}
> >
<Select.HiddenSelect /> <Select.HiddenSelect />
@@ -572,7 +662,7 @@ export default function RoomsList() {
</Select.Root> </Select.Root>
</Field.Root> </Field.Root>
<Field.Root mt={4}> <Field.Root mt={4}>
<Field.Label>Cloud recording start trigger</Field.Label> <Field.Label>Recording start trigger</Field.Label>
<Select.Root <Select.Root
value={[room.recordingTrigger]} value={[room.recordingTrigger]}
onValueChange={(e) => onValueChange={(e) =>
@@ -582,7 +672,11 @@ export default function RoomsList() {
}) })
} }
collection={recordingTriggerCollection} collection={recordingTriggerCollection}
disabled={room.recordingType !== "cloud"} disabled={
room.recordingType !== "cloud" ||
(room.platform === "daily" &&
room.recordingType === "cloud")
}
> >
<Select.HiddenSelect /> <Select.HiddenSelect />
<Select.Control> <Select.Control>

View File

@@ -117,15 +117,6 @@ export default function TranscriptDetails(details: TranscriptDetails) {
return <Modal title="Loading" text={"Loading transcript..."} />; return <Modal title="Loading" text={"Loading transcript..."} />;
} }
if (mp3.error) {
return (
<Modal
title="Transcription error"
text={`There was an error loading the recording. Error: ${mp3.error}`}
/>
);
}
return ( return (
<> <>
<Grid <Grid
@@ -147,7 +138,12 @@ export default function TranscriptDetails(details: TranscriptDetails) {
/> />
) : !mp3.loading && (waveform.error || mp3.error) ? ( ) : !mp3.loading && (waveform.error || mp3.error) ? (
<Box p={4} bg="red.100" borderRadius="md"> <Box p={4} bg="red.100" borderRadius="md">
<Text>Error loading this recording</Text> <Text>
Error loading{" "}
{[waveform.error && "waveform", mp3.error && "mp3"]
.filter(Boolean)
.join(" and ")}
</Text>
</Box> </Box>
) : ( ) : (
<Skeleton h={14} /> <Skeleton h={14} />

View File

@@ -11,6 +11,7 @@ import {
recordingTypeRequiresConsent, recordingTypeRequiresConsent,
} from "../../lib/consent"; } from "../../lib/consent";
import { useRoomJoinMeeting } from "../../lib/apiHooks"; import { useRoomJoinMeeting } from "../../lib/apiHooks";
import { assertExists } from "../../lib/utils";
type Meeting = components["schemas"]["Meeting"]; type Meeting = components["schemas"]["Meeting"];
@@ -22,16 +23,15 @@ export default function DailyRoom({ meeting }: DailyRoomProps) {
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const auth = useAuth(); const auth = useAuth();
const status = auth.status; const authLastUserId = auth.lastUserId;
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const joinMutation = useRoomJoinMeeting(); const joinMutation = useRoomJoinMeeting();
const [joinedMeeting, setJoinedMeeting] = useState<Meeting | null>(null); const [joinedMeeting, setJoinedMeeting] = useState<Meeting | null>(null);
const roomName = params?.roomName as string; const roomName = params?.roomName as string;
// Always call /join to get a fresh token with user_id
useEffect(() => { useEffect(() => {
if (status === "loading" || !meeting?.id || !roomName) return; if (authLastUserId === undefined || !meeting?.id || !roomName) return;
const join = async () => { const join = async () => {
try { try {
@@ -50,18 +50,17 @@ export default function DailyRoom({ meeting }: DailyRoomProps) {
}; };
join(); join();
}, [meeting?.id, roomName, status]); }, [meeting?.id, roomName, authLastUserId]);
const roomUrl = joinedMeeting?.host_room_url || joinedMeeting?.room_url; const roomUrl = joinedMeeting?.room_url;
const isLoading =
status === "loading" || joinMutation.isPending || !joinedMeeting;
const handleLeave = useCallback(() => { const handleLeave = useCallback(() => {
router.push("/browse"); router.push("/browse");
}, [router]); }, [router]);
useEffect(() => { useEffect(() => {
if (isLoading || !roomUrl || !containerRef.current) return; if (authLastUserId === undefined || !roomUrl || !containerRef.current)
return;
let frame: DailyCall | null = null; let frame: DailyCall | null = null;
let destroyed = false; let destroyed = false;
@@ -92,7 +91,15 @@ export default function DailyRoom({ meeting }: DailyRoomProps) {
frame.on("joined-meeting", async () => { frame.on("joined-meeting", async () => {
try { try {
await frame.startRecording({ type: "raw-tracks" }); const frameInstance = assertExists(
frame,
"frame object got lost somewhere after frame.on was called",
);
if (meeting.recording_type === "cloud") {
console.log("Starting cloud recording");
await frameInstance.startRecording({ type: "raw-tracks" });
}
} catch (error) { } catch (error) {
console.error("Failed to start recording:", error); console.error("Failed to start recording:", error);
} }
@@ -104,7 +111,9 @@ export default function DailyRoom({ meeting }: DailyRoomProps) {
} }
}; };
createAndJoin(); createAndJoin().catch((error) => {
console.error("Failed to create and join meeting:", error);
});
return () => { return () => {
destroyed = true; destroyed = true;
@@ -114,9 +123,9 @@ export default function DailyRoom({ meeting }: DailyRoomProps) {
}); });
} }
}; };
}, [roomUrl, isLoading, handleLeave]); }, [roomUrl, authLastUserId, handleLeave]);
if (isLoading) { if (authLastUserId === undefined) {
return ( return (
<Center width="100vw" height="100vh"> <Center width="100vw" height="100vh">
<Spinner size="xl" /> <Spinner size="xl" />

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { createContext, useContext } from "react"; import { createContext, useContext, useRef } from "react";
import { useSession as useNextAuthSession } from "next-auth/react"; import { useSession as useNextAuthSession } from "next-auth/react";
import { signOut, signIn } from "next-auth/react"; import { signOut, signIn } from "next-auth/react";
import { configureApiAuth } from "./apiClient"; import { configureApiAuth } from "./apiClient";
@@ -25,6 +25,9 @@ type AuthContextType = (
update: () => Promise<Session | null>; update: () => Promise<Session | null>;
signIn: typeof signIn; signIn: typeof signIn;
signOut: typeof signOut; signOut: typeof signOut;
// TODO probably rename isLoading to isReloading and make THIS field "isLoading"
// undefined is "not known", null is "is certainly logged out"
lastUserId: CustomSession["user"]["id"] | null | undefined;
}; };
const AuthContext = createContext<AuthContextType | undefined>(undefined); const AuthContext = createContext<AuthContextType | undefined>(undefined);
@@ -41,10 +44,15 @@ const noopAuthContext: AuthContextType = {
signOut: async () => { signOut: async () => {
throw new Error("signOut not supposed to be called"); throw new Error("signOut not supposed to be called");
}, },
lastUserId: undefined,
}; };
export function AuthProvider({ children }: { children: React.ReactNode }) { export function AuthProvider({ children }: { children: React.ReactNode }) {
const { data: session, status, update } = useNextAuthSession(); const { data: session, status, update } = useNextAuthSession();
// referential comparison done in component, must be primitive /or cached
const lastUserId = useRef<CustomSession["user"]["id"] | null | undefined>(
null,
);
const contextValue: AuthContextType = isAuthEnabled const contextValue: AuthContextType = isAuthEnabled
? { ? {
@@ -73,11 +81,16 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
case "authenticated": { case "authenticated": {
const customSession = assertCustomSession(session); const customSession = assertCustomSession(session);
if (customSession?.error === REFRESH_ACCESS_TOKEN_ERROR) { if (customSession?.error === REFRESH_ACCESS_TOKEN_ERROR) {
// warning: call order-dependent
lastUserId.current = null;
// token had expired but next auth still returns "authenticated" so show user unauthenticated state // token had expired but next auth still returns "authenticated" so show user unauthenticated state
return { return {
status: "unauthenticated" as const, status: "unauthenticated" as const,
}; };
} else if (customSession?.accessToken) { } else if (customSession?.accessToken) {
// updates anyways with updated properties below
// warning! execution order conscience, must be ran before reading lastUserId.current below
lastUserId.current = customSession.user.id;
return { return {
status, status,
accessToken: customSession.accessToken, accessToken: customSession.accessToken,
@@ -92,6 +105,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
} }
} }
case "unauthenticated": { case "unauthenticated": {
// warning: call order-dependent
lastUserId.current = null;
return { status: "unauthenticated" as const }; return { status: "unauthenticated" as const };
} }
default: { default: {
@@ -103,6 +118,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
update, update,
signIn, signIn,
signOut, signOut,
// for optimistic cases when we assume "loading" doesn't immediately invalidate the user
lastUserId: lastUserId.current,
} }
: noopAuthContext; : noopAuthContext;

View File

@@ -148,7 +148,7 @@ export const authOptions = (): AuthOptions =>
}, },
async session({ session, token }) { async session({ session, token }) {
const extendedToken = token as JWTWithAccessToken; const extendedToken = token as JWTWithAccessToken;
console.log("extendedToken", extendedToken);
const userId = await getUserId(extendedToken.accessToken); const userId = await getUserId(extendedToken.accessToken);
return { return {