mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2026-04-13 00:36:54 +00:00
feat: make video recording optional, deleting video tracks (#954)
* feat: make video recording optional, deleting video tracks
This commit is contained in:
committed by
GitHub
parent
5f0c5635eb
commit
ee8db36f2c
@@ -0,0 +1,43 @@
|
|||||||
|
"""add store_video to room and meeting
|
||||||
|
|
||||||
|
Revision ID: c1d2e3f4a5b6
|
||||||
|
Revises: b4c7e8f9a012
|
||||||
|
Create Date: 2026-04-08 00:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision: str = "c1d2e3f4a5b6"
|
||||||
|
down_revision: Union[str, None] = "b4c7e8f9a012"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
"room",
|
||||||
|
sa.Column(
|
||||||
|
"store_video",
|
||||||
|
sa.Boolean(),
|
||||||
|
nullable=False,
|
||||||
|
server_default=sa.false(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
op.add_column(
|
||||||
|
"meeting",
|
||||||
|
sa.Column(
|
||||||
|
"store_video",
|
||||||
|
sa.Boolean(),
|
||||||
|
nullable=False,
|
||||||
|
server_default=sa.false(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column("meeting", "store_video")
|
||||||
|
op.drop_column("room", "store_video")
|
||||||
@@ -69,6 +69,7 @@ meetings = sa.Table(
|
|||||||
sa.Column("daily_composed_video_duration", sa.Integer, nullable=True),
|
sa.Column("daily_composed_video_duration", sa.Integer, nullable=True),
|
||||||
# Email recipients for transcript notification
|
# Email recipients for transcript notification
|
||||||
sa.Column("email_recipients", JSONB, nullable=True),
|
sa.Column("email_recipients", JSONB, nullable=True),
|
||||||
|
sa.Column("store_video", sa.Boolean, nullable=False, server_default=sa.false()),
|
||||||
sa.Index("idx_meeting_room_id", "room_id"),
|
sa.Index("idx_meeting_room_id", "room_id"),
|
||||||
sa.Index("idx_meeting_calendar_event", "calendar_event_id"),
|
sa.Index("idx_meeting_calendar_event", "calendar_event_id"),
|
||||||
)
|
)
|
||||||
@@ -122,6 +123,7 @@ class Meeting(BaseModel):
|
|||||||
# Email recipients for transcript notification
|
# Email recipients for transcript notification
|
||||||
# Each entry is {"email": str, "include_link": bool} or a legacy plain str
|
# Each entry is {"email": str, "include_link": bool} or a legacy plain str
|
||||||
email_recipients: list[dict | str] | None = None
|
email_recipients: list[dict | str] | None = None
|
||||||
|
store_video: bool = False
|
||||||
|
|
||||||
|
|
||||||
class MeetingController:
|
class MeetingController:
|
||||||
@@ -152,6 +154,7 @@ class MeetingController:
|
|||||||
calendar_event_id=calendar_event_id,
|
calendar_event_id=calendar_event_id,
|
||||||
calendar_metadata=calendar_metadata,
|
calendar_metadata=calendar_metadata,
|
||||||
platform=room.platform,
|
platform=room.platform,
|
||||||
|
store_video=room.store_video,
|
||||||
)
|
)
|
||||||
query = meetings.insert().values(**meeting.model_dump())
|
query = meetings.insert().values(**meeting.model_dump())
|
||||||
await get_database().execute(query)
|
await get_database().execute(query)
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ rooms = sqlalchemy.Table(
|
|||||||
server_default=sqlalchemy.sql.false(),
|
server_default=sqlalchemy.sql.false(),
|
||||||
),
|
),
|
||||||
sqlalchemy.Column("email_transcript_to", sqlalchemy.String, nullable=True),
|
sqlalchemy.Column("email_transcript_to", sqlalchemy.String, nullable=True),
|
||||||
|
sqlalchemy.Column(
|
||||||
|
"store_video", sqlalchemy.Boolean, nullable=False, server_default=false()
|
||||||
|
),
|
||||||
sqlalchemy.Index("idx_room_is_shared", "is_shared"),
|
sqlalchemy.Index("idx_room_is_shared", "is_shared"),
|
||||||
sqlalchemy.Index("idx_room_ics_enabled", "ics_enabled"),
|
sqlalchemy.Index("idx_room_ics_enabled", "ics_enabled"),
|
||||||
)
|
)
|
||||||
@@ -94,6 +97,7 @@ class Room(BaseModel):
|
|||||||
platform: Platform = Field(default_factory=lambda: settings.DEFAULT_VIDEO_PLATFORM)
|
platform: Platform = Field(default_factory=lambda: settings.DEFAULT_VIDEO_PLATFORM)
|
||||||
skip_consent: bool = False
|
skip_consent: bool = False
|
||||||
email_transcript_to: str | None = None
|
email_transcript_to: str | None = None
|
||||||
|
store_video: bool = False
|
||||||
|
|
||||||
|
|
||||||
class RoomController:
|
class RoomController:
|
||||||
@@ -150,6 +154,7 @@ class RoomController:
|
|||||||
platform: Platform = settings.DEFAULT_VIDEO_PLATFORM,
|
platform: Platform = settings.DEFAULT_VIDEO_PLATFORM,
|
||||||
skip_consent: bool = False,
|
skip_consent: bool = False,
|
||||||
email_transcript_to: str | None = None,
|
email_transcript_to: str | None = None,
|
||||||
|
store_video: bool = False,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Add a new room
|
Add a new room
|
||||||
@@ -176,6 +181,7 @@ class RoomController:
|
|||||||
"platform": platform,
|
"platform": platform,
|
||||||
"skip_consent": skip_consent,
|
"skip_consent": skip_consent,
|
||||||
"email_transcript_to": email_transcript_to,
|
"email_transcript_to": email_transcript_to,
|
||||||
|
"store_video": store_video,
|
||||||
}
|
}
|
||||||
|
|
||||||
room = Room(**room_data)
|
room = Room(**room_data)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from reflector.hatchet.client import HatchetClientManager
|
|||||||
from reflector.hatchet.workflows.daily_multitrack_pipeline import (
|
from reflector.hatchet.workflows.daily_multitrack_pipeline import (
|
||||||
daily_multitrack_pipeline,
|
daily_multitrack_pipeline,
|
||||||
)
|
)
|
||||||
|
from reflector.hatchet.workflows.failed_runs_monitor import failed_runs_monitor
|
||||||
from reflector.hatchet.workflows.file_pipeline import file_pipeline
|
from reflector.hatchet.workflows.file_pipeline import file_pipeline
|
||||||
from reflector.hatchet.workflows.live_post_pipeline import live_post_pipeline
|
from reflector.hatchet.workflows.live_post_pipeline import live_post_pipeline
|
||||||
from reflector.hatchet.workflows.subject_processing import subject_workflow
|
from reflector.hatchet.workflows.subject_processing import subject_workflow
|
||||||
@@ -54,10 +55,6 @@ def main():
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
if _zulip_dag_enabled:
|
if _zulip_dag_enabled:
|
||||||
from reflector.hatchet.workflows.failed_runs_monitor import ( # noqa: PLC0415
|
|
||||||
failed_runs_monitor,
|
|
||||||
)
|
|
||||||
|
|
||||||
workflows.append(failed_runs_monitor)
|
workflows.append(failed_runs_monitor)
|
||||||
logger.info(
|
logger.info(
|
||||||
"FailedRunsMonitor cron enabled",
|
"FailedRunsMonitor cron enabled",
|
||||||
|
|||||||
@@ -219,6 +219,32 @@ async def _handle_recording_ready(event: RecordingReadyEvent):
|
|||||||
|
|
||||||
track_keys = [t.s3Key for t in tracks if t.type == "audio"]
|
track_keys = [t.s3Key for t in tracks if t.type == "audio"]
|
||||||
|
|
||||||
|
# Delete video tracks when store_video is disabled (same pattern as LiveKit).
|
||||||
|
# Only delete if we have a meeting AND store_video is explicitly false.
|
||||||
|
# If no meeting found, leave files alone (can't confirm user intent).
|
||||||
|
video_track_keys = [t.s3Key for t in tracks if t.type == "video"]
|
||||||
|
if video_track_keys:
|
||||||
|
meeting = await meetings_controller.get_by_room_name(room_name)
|
||||||
|
if meeting is not None and not meeting.store_video:
|
||||||
|
from reflector.storage import get_source_storage
|
||||||
|
|
||||||
|
storage = get_source_storage("daily")
|
||||||
|
for video_key in video_track_keys:
|
||||||
|
try:
|
||||||
|
await storage.delete_file(video_key)
|
||||||
|
logger.info(
|
||||||
|
"Deleted video track from raw-tracks recording",
|
||||||
|
s3_key=video_key,
|
||||||
|
room_name=room_name,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
# Non-critical — pipeline filters these out anyway
|
||||||
|
logger.warning(
|
||||||
|
"Failed to delete video track from raw-tracks recording",
|
||||||
|
s3_key=video_key,
|
||||||
|
error=str(e),
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Raw-tracks recording queuing processing",
|
"Raw-tracks recording queuing processing",
|
||||||
recording_id=recording_id,
|
recording_id=recording_id,
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class Room(BaseModel):
|
|||||||
platform: Platform
|
platform: Platform
|
||||||
skip_consent: bool = False
|
skip_consent: bool = False
|
||||||
email_transcript_to: str | None = None
|
email_transcript_to: str | None = None
|
||||||
|
store_video: bool = False
|
||||||
|
|
||||||
|
|
||||||
class RoomDetails(Room):
|
class RoomDetails(Room):
|
||||||
@@ -75,6 +76,7 @@ class Meeting(BaseModel):
|
|||||||
platform: Platform
|
platform: Platform
|
||||||
daily_composed_video_s3_key: str | None = None
|
daily_composed_video_s3_key: str | None = None
|
||||||
daily_composed_video_duration: int | None = None
|
daily_composed_video_duration: int | None = None
|
||||||
|
store_video: bool = False
|
||||||
|
|
||||||
|
|
||||||
class CreateRoom(BaseModel):
|
class CreateRoom(BaseModel):
|
||||||
@@ -95,6 +97,7 @@ class CreateRoom(BaseModel):
|
|||||||
platform: Platform
|
platform: Platform
|
||||||
skip_consent: bool = False
|
skip_consent: bool = False
|
||||||
email_transcript_to: str | None = None
|
email_transcript_to: str | None = None
|
||||||
|
store_video: bool = False
|
||||||
|
|
||||||
|
|
||||||
class UpdateRoom(BaseModel):
|
class UpdateRoom(BaseModel):
|
||||||
@@ -115,6 +118,7 @@ class UpdateRoom(BaseModel):
|
|||||||
platform: Optional[Platform] = None
|
platform: Optional[Platform] = None
|
||||||
skip_consent: Optional[bool] = None
|
skip_consent: Optional[bool] = None
|
||||||
email_transcript_to: Optional[str] = None
|
email_transcript_to: Optional[str] = None
|
||||||
|
store_video: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
class CreateRoomMeeting(BaseModel):
|
class CreateRoomMeeting(BaseModel):
|
||||||
@@ -257,6 +261,7 @@ async def rooms_create(
|
|||||||
platform=room.platform,
|
platform=room.platform,
|
||||||
skip_consent=room.skip_consent,
|
skip_consent=room.skip_consent,
|
||||||
email_transcript_to=room.email_transcript_to,
|
email_transcript_to=room.email_transcript_to,
|
||||||
|
store_video=room.store_video,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -325,6 +330,7 @@ async def rooms_create_meeting(
|
|||||||
and meeting.recording_type == room.recording_type
|
and meeting.recording_type == room.recording_type
|
||||||
and meeting.recording_trigger == room.recording_trigger
|
and meeting.recording_trigger == room.recording_trigger
|
||||||
and meeting.platform == room.platform
|
and meeting.platform == room.platform
|
||||||
|
and meeting.store_video == room.store_video
|
||||||
)
|
)
|
||||||
if not settings_match:
|
if not settings_match:
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ def build_beat_schedule(
|
|||||||
whereby_api_key=None,
|
whereby_api_key=None,
|
||||||
aws_process_recording_queue_url=None,
|
aws_process_recording_queue_url=None,
|
||||||
daily_api_key=None,
|
daily_api_key=None,
|
||||||
|
livekit_api_key=None,
|
||||||
|
livekit_url=None,
|
||||||
public_mode=False,
|
public_mode=False,
|
||||||
public_data_retention_days=None,
|
public_data_retention_days=None,
|
||||||
healthcheck_url=None,
|
healthcheck_url=None,
|
||||||
@@ -83,7 +85,7 @@ def build_beat_schedule(
|
|||||||
else:
|
else:
|
||||||
logger.info("Daily.co beat tasks disabled (no DAILY_API_KEY)")
|
logger.info("Daily.co beat tasks disabled (no DAILY_API_KEY)")
|
||||||
|
|
||||||
_livekit_enabled = bool(settings.LIVEKIT_API_KEY and settings.LIVEKIT_URL)
|
_livekit_enabled = bool(livekit_api_key and livekit_url)
|
||||||
if _livekit_enabled:
|
if _livekit_enabled:
|
||||||
beat_schedule["process_livekit_ended_meetings"] = {
|
beat_schedule["process_livekit_ended_meetings"] = {
|
||||||
"task": "reflector.worker.process.process_livekit_ended_meetings",
|
"task": "reflector.worker.process.process_livekit_ended_meetings",
|
||||||
@@ -175,6 +177,8 @@ else:
|
|||||||
whereby_api_key=settings.WHEREBY_API_KEY,
|
whereby_api_key=settings.WHEREBY_API_KEY,
|
||||||
aws_process_recording_queue_url=settings.AWS_PROCESS_RECORDING_QUEUE_URL,
|
aws_process_recording_queue_url=settings.AWS_PROCESS_RECORDING_QUEUE_URL,
|
||||||
daily_api_key=settings.DAILY_API_KEY,
|
daily_api_key=settings.DAILY_API_KEY,
|
||||||
|
livekit_api_key=settings.LIVEKIT_API_KEY,
|
||||||
|
livekit_url=settings.LIVEKIT_URL,
|
||||||
public_mode=settings.PUBLIC_MODE,
|
public_mode=settings.PUBLIC_MODE,
|
||||||
public_data_retention_days=settings.PUBLIC_DATA_RETENTION_DAYS,
|
public_data_retention_days=settings.PUBLIC_DATA_RETENTION_DAYS,
|
||||||
healthcheck_url=settings.HEALTHCHECK_URL,
|
healthcheck_url=settings.HEALTHCHECK_URL,
|
||||||
|
|||||||
@@ -562,6 +562,15 @@ async def store_cloud_recording(
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if not meeting.store_video:
|
||||||
|
logger.info(
|
||||||
|
f"Cloud recording ({source}): skipped, store_video=false",
|
||||||
|
recording_id=recording_id,
|
||||||
|
room_name=room_name,
|
||||||
|
meeting_id=meeting.id,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
success = await meetings_controller.set_cloud_recording_if_missing(
|
success = await meetings_controller.set_cloud_recording_if_missing(
|
||||||
meeting_id=meeting.id,
|
meeting_id=meeting.id,
|
||||||
s3_key=s3_key,
|
s3_key=s3_key,
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ DAILY_TASKS = {
|
|||||||
"trigger_daily_reconciliation",
|
"trigger_daily_reconciliation",
|
||||||
"reprocess_failed_daily_recordings",
|
"reprocess_failed_daily_recordings",
|
||||||
}
|
}
|
||||||
|
LIVEKIT_TASKS = {
|
||||||
|
"process_livekit_ended_meetings",
|
||||||
|
"reprocess_failed_livekit_recordings",
|
||||||
|
}
|
||||||
PLATFORM_TASKS = {
|
PLATFORM_TASKS = {
|
||||||
"process_meetings",
|
"process_meetings",
|
||||||
"sync_all_ics_calendars",
|
"sync_all_ics_calendars",
|
||||||
@@ -47,6 +51,7 @@ class TestNoPlatformConfigured:
|
|||||||
task_names = set(schedule.keys())
|
task_names = set(schedule.keys())
|
||||||
assert not task_names & WHEREBY_TASKS
|
assert not task_names & WHEREBY_TASKS
|
||||||
assert not task_names & DAILY_TASKS
|
assert not task_names & DAILY_TASKS
|
||||||
|
assert not task_names & LIVEKIT_TASKS
|
||||||
assert not task_names & PLATFORM_TASKS
|
assert not task_names & PLATFORM_TASKS
|
||||||
|
|
||||||
def test_only_healthcheck_disabled_warning(self):
|
def test_only_healthcheck_disabled_warning(self):
|
||||||
@@ -72,6 +77,7 @@ class TestWherebyOnly:
|
|||||||
assert WHEREBY_TASKS <= task_names
|
assert WHEREBY_TASKS <= task_names
|
||||||
assert PLATFORM_TASKS <= task_names
|
assert PLATFORM_TASKS <= task_names
|
||||||
assert not task_names & DAILY_TASKS
|
assert not task_names & DAILY_TASKS
|
||||||
|
assert not task_names & LIVEKIT_TASKS
|
||||||
|
|
||||||
def test_whereby_sqs_url(self):
|
def test_whereby_sqs_url(self):
|
||||||
schedule = build_beat_schedule(
|
schedule = build_beat_schedule(
|
||||||
@@ -81,6 +87,7 @@ class TestWherebyOnly:
|
|||||||
assert WHEREBY_TASKS <= task_names
|
assert WHEREBY_TASKS <= task_names
|
||||||
assert PLATFORM_TASKS <= task_names
|
assert PLATFORM_TASKS <= task_names
|
||||||
assert not task_names & DAILY_TASKS
|
assert not task_names & DAILY_TASKS
|
||||||
|
assert not task_names & LIVEKIT_TASKS
|
||||||
|
|
||||||
def test_whereby_task_count(self):
|
def test_whereby_task_count(self):
|
||||||
schedule = build_beat_schedule(whereby_api_key="test-key")
|
schedule = build_beat_schedule(whereby_api_key="test-key")
|
||||||
@@ -97,6 +104,7 @@ class TestDailyOnly:
|
|||||||
assert DAILY_TASKS <= task_names
|
assert DAILY_TASKS <= task_names
|
||||||
assert PLATFORM_TASKS <= task_names
|
assert PLATFORM_TASKS <= task_names
|
||||||
assert not task_names & WHEREBY_TASKS
|
assert not task_names & WHEREBY_TASKS
|
||||||
|
assert not task_names & LIVEKIT_TASKS
|
||||||
|
|
||||||
def test_daily_task_count(self):
|
def test_daily_task_count(self):
|
||||||
schedule = build_beat_schedule(daily_api_key="test-daily-key")
|
schedule = build_beat_schedule(daily_api_key="test-daily-key")
|
||||||
@@ -104,6 +112,33 @@ class TestDailyOnly:
|
|||||||
assert len(schedule) == 6
|
assert len(schedule) == 6
|
||||||
|
|
||||||
|
|
||||||
|
class TestLiveKitOnly:
|
||||||
|
"""When only LiveKit is configured."""
|
||||||
|
|
||||||
|
def test_livekit_keys(self):
|
||||||
|
schedule = build_beat_schedule(
|
||||||
|
livekit_api_key="test-lk-key", livekit_url="ws://livekit:7880"
|
||||||
|
)
|
||||||
|
task_names = set(schedule.keys())
|
||||||
|
assert LIVEKIT_TASKS <= task_names
|
||||||
|
assert PLATFORM_TASKS <= task_names
|
||||||
|
assert not task_names & WHEREBY_TASKS
|
||||||
|
assert not task_names & DAILY_TASKS
|
||||||
|
|
||||||
|
def test_livekit_task_count(self):
|
||||||
|
schedule = build_beat_schedule(
|
||||||
|
livekit_api_key="test-lk-key", livekit_url="ws://livekit:7880"
|
||||||
|
)
|
||||||
|
# LiveKit (2) + Platform (3) = 5
|
||||||
|
assert len(schedule) == 5
|
||||||
|
|
||||||
|
def test_livekit_needs_both_key_and_url(self):
|
||||||
|
schedule_key_only = build_beat_schedule(livekit_api_key="test-lk-key")
|
||||||
|
schedule_url_only = build_beat_schedule(livekit_url="ws://livekit:7880")
|
||||||
|
assert not set(schedule_key_only.keys()) & LIVEKIT_TASKS
|
||||||
|
assert not set(schedule_url_only.keys()) & LIVEKIT_TASKS
|
||||||
|
|
||||||
|
|
||||||
class TestBothPlatforms:
|
class TestBothPlatforms:
|
||||||
"""When both Whereby and Daily.co are configured."""
|
"""When both Whereby and Daily.co are configured."""
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ const roomInitialState = {
|
|||||||
platform: "whereby",
|
platform: "whereby",
|
||||||
skipConsent: false,
|
skipConsent: false,
|
||||||
emailTranscriptTo: "",
|
emailTranscriptTo: "",
|
||||||
|
storeVideo: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RoomsList() {
|
export default function RoomsList() {
|
||||||
@@ -185,6 +186,7 @@ export default function RoomsList() {
|
|||||||
platform: detailedEditedRoom.platform,
|
platform: detailedEditedRoom.platform,
|
||||||
skipConsent: detailedEditedRoom.skip_consent || false,
|
skipConsent: detailedEditedRoom.skip_consent || false,
|
||||||
emailTranscriptTo: detailedEditedRoom.email_transcript_to || "",
|
emailTranscriptTo: detailedEditedRoom.email_transcript_to || "",
|
||||||
|
storeVideo: detailedEditedRoom.store_video || false,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
[detailedEditedRoom],
|
[detailedEditedRoom],
|
||||||
@@ -335,6 +337,7 @@ export default function RoomsList() {
|
|||||||
platform,
|
platform,
|
||||||
skip_consent: room.skipConsent,
|
skip_consent: room.skipConsent,
|
||||||
email_transcript_to: room.emailTranscriptTo || null,
|
email_transcript_to: room.emailTranscriptTo || null,
|
||||||
|
store_video: room.storeVideo,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
@@ -400,6 +403,7 @@ export default function RoomsList() {
|
|||||||
platform: roomData.platform,
|
platform: roomData.platform,
|
||||||
skipConsent: roomData.skip_consent || false,
|
skipConsent: roomData.skip_consent || false,
|
||||||
emailTranscriptTo: roomData.email_transcript_to || "",
|
emailTranscriptTo: roomData.email_transcript_to || "",
|
||||||
|
storeVideo: roomData.store_video || false,
|
||||||
});
|
});
|
||||||
setEditRoomId(roomId);
|
setEditRoomId(roomId);
|
||||||
setIsEditing(true);
|
setIsEditing(true);
|
||||||
@@ -842,6 +846,38 @@ export default function RoomsList() {
|
|||||||
</Field.HelperText>
|
</Field.HelperText>
|
||||||
</Field.Root>
|
</Field.Root>
|
||||||
)}
|
)}
|
||||||
|
{room.platform === "daily" &&
|
||||||
|
room.recordingType === "cloud" && (
|
||||||
|
<Field.Root mt={4}>
|
||||||
|
<Checkbox.Root
|
||||||
|
name="storeVideo"
|
||||||
|
checked={room.storeVideo}
|
||||||
|
onCheckedChange={(e) => {
|
||||||
|
const syntheticEvent = {
|
||||||
|
target: {
|
||||||
|
name: "storeVideo",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: e.checked,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
handleRoomChange(syntheticEvent);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox.HiddenInput />
|
||||||
|
<Checkbox.Control>
|
||||||
|
<Checkbox.Indicator />
|
||||||
|
</Checkbox.Control>
|
||||||
|
<Checkbox.Label>
|
||||||
|
Store video recording
|
||||||
|
</Checkbox.Label>
|
||||||
|
</Checkbox.Root>
|
||||||
|
<Field.HelperText>
|
||||||
|
When enabled, a composed video recording will be
|
||||||
|
saved alongside audio. Disabling saves significant
|
||||||
|
storage.
|
||||||
|
</Field.HelperText>
|
||||||
|
</Field.Root>
|
||||||
|
)}
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
|
|
||||||
<Tabs.Content value="share" pt={6}>
|
<Tabs.Content value="share" pt={6}>
|
||||||
|
|||||||
@@ -267,12 +267,13 @@ export default function DailyRoom({ meeting, room }: DailyRoomProps) {
|
|||||||
|
|
||||||
const handleFrameJoinMeeting = useCallback(() => {
|
const handleFrameJoinMeeting = useCallback(() => {
|
||||||
if (meeting.recording_type === "cloud") {
|
if (meeting.recording_type === "cloud") {
|
||||||
console.log("Starting dual recording via REST API", {
|
console.log("Starting recording via REST API", {
|
||||||
cloudInstanceId,
|
cloudInstanceId,
|
||||||
rawTracksInstanceId,
|
rawTracksInstanceId,
|
||||||
|
storeVideo: meeting.store_video,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start both cloud and raw-tracks via backend REST API (with retry on 404)
|
// Start recordings via backend REST API (with retry on 404)
|
||||||
// Daily.co needs time to register call as "hosting" for REST API
|
// Daily.co needs time to register call as "hosting" for REST API
|
||||||
const startRecordingWithRetry = (
|
const startRecordingWithRetry = (
|
||||||
type: DailyRecordingType,
|
type: DailyRecordingType,
|
||||||
@@ -320,12 +321,17 @@ export default function DailyRoom({ meeting, room }: DailyRoomProps) {
|
|||||||
}, RECORDING_START_DELAY_MS);
|
}, RECORDING_START_DELAY_MS);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start both recordings
|
// Always start raw-tracks (needed for transcription pipeline)
|
||||||
startRecordingWithRetry("cloud", cloudInstanceId);
|
|
||||||
startRecordingWithRetry("raw-tracks", rawTracksInstanceId);
|
startRecordingWithRetry("raw-tracks", rawTracksInstanceId);
|
||||||
|
|
||||||
|
// Only start cloud (composed video) if store_video is enabled
|
||||||
|
if (meeting.store_video) {
|
||||||
|
startRecordingWithRetry("cloud", cloudInstanceId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
meeting.recording_type,
|
meeting.recording_type,
|
||||||
|
meeting.store_video,
|
||||||
meeting.id,
|
meeting.id,
|
||||||
startRecordingMutation,
|
startRecordingMutation,
|
||||||
cloudInstanceId,
|
cloudInstanceId,
|
||||||
|
|||||||
22
www/app/reflector-api.d.ts
vendored
22
www/app/reflector-api.d.ts
vendored
@@ -1134,6 +1134,11 @@ export interface components {
|
|||||||
skip_consent: boolean;
|
skip_consent: boolean;
|
||||||
/** Email Transcript To */
|
/** Email Transcript To */
|
||||||
email_transcript_to?: string | null;
|
email_transcript_to?: string | null;
|
||||||
|
/**
|
||||||
|
* Store Video
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
store_video: boolean;
|
||||||
};
|
};
|
||||||
/** CreateRoomMeeting */
|
/** CreateRoomMeeting */
|
||||||
CreateRoomMeeting: {
|
CreateRoomMeeting: {
|
||||||
@@ -1852,6 +1857,11 @@ export interface components {
|
|||||||
daily_composed_video_s3_key?: string | null;
|
daily_composed_video_s3_key?: string | null;
|
||||||
/** Daily Composed Video Duration */
|
/** Daily Composed Video Duration */
|
||||||
daily_composed_video_duration?: number | null;
|
daily_composed_video_duration?: number | null;
|
||||||
|
/**
|
||||||
|
* Store Video
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
store_video: boolean;
|
||||||
};
|
};
|
||||||
/** MeetingConsentRequest */
|
/** MeetingConsentRequest */
|
||||||
MeetingConsentRequest: {
|
MeetingConsentRequest: {
|
||||||
@@ -1955,6 +1965,11 @@ export interface components {
|
|||||||
skip_consent: boolean;
|
skip_consent: boolean;
|
||||||
/** Email Transcript To */
|
/** Email Transcript To */
|
||||||
email_transcript_to?: string | null;
|
email_transcript_to?: string | null;
|
||||||
|
/**
|
||||||
|
* Store Video
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
store_video: boolean;
|
||||||
};
|
};
|
||||||
/** RoomDetails */
|
/** RoomDetails */
|
||||||
RoomDetails: {
|
RoomDetails: {
|
||||||
@@ -2013,6 +2028,11 @@ export interface components {
|
|||||||
skip_consent: boolean;
|
skip_consent: boolean;
|
||||||
/** Email Transcript To */
|
/** Email Transcript To */
|
||||||
email_transcript_to?: string | null;
|
email_transcript_to?: string | null;
|
||||||
|
/**
|
||||||
|
* Store Video
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
store_video: boolean;
|
||||||
/** Webhook Url */
|
/** Webhook Url */
|
||||||
webhook_url: string | null;
|
webhook_url: string | null;
|
||||||
/** Webhook Secret */
|
/** Webhook Secret */
|
||||||
@@ -2389,6 +2409,8 @@ export interface components {
|
|||||||
skip_consent?: boolean | null;
|
skip_consent?: boolean | null;
|
||||||
/** Email Transcript To */
|
/** Email Transcript To */
|
||||||
email_transcript_to?: string | null;
|
email_transcript_to?: string | null;
|
||||||
|
/** Store Video */
|
||||||
|
store_video?: boolean | null;
|
||||||
};
|
};
|
||||||
/** UpdateTranscript */
|
/** UpdateTranscript */
|
||||||
UpdateTranscript: {
|
UpdateTranscript: {
|
||||||
|
|||||||
Reference in New Issue
Block a user