mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2026-04-09 07:16:47 +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),
|
||||
# Email recipients for transcript notification
|
||||
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_calendar_event", "calendar_event_id"),
|
||||
)
|
||||
@@ -122,6 +123,7 @@ class Meeting(BaseModel):
|
||||
# Email recipients for transcript notification
|
||||
# Each entry is {"email": str, "include_link": bool} or a legacy plain str
|
||||
email_recipients: list[dict | str] | None = None
|
||||
store_video: bool = False
|
||||
|
||||
|
||||
class MeetingController:
|
||||
@@ -152,6 +154,7 @@ class MeetingController:
|
||||
calendar_event_id=calendar_event_id,
|
||||
calendar_metadata=calendar_metadata,
|
||||
platform=room.platform,
|
||||
store_video=room.store_video,
|
||||
)
|
||||
query = meetings.insert().values(**meeting.model_dump())
|
||||
await get_database().execute(query)
|
||||
|
||||
@@ -64,6 +64,9 @@ rooms = sqlalchemy.Table(
|
||||
server_default=sqlalchemy.sql.false(),
|
||||
),
|
||||
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_ics_enabled", "ics_enabled"),
|
||||
)
|
||||
@@ -94,6 +97,7 @@ class Room(BaseModel):
|
||||
platform: Platform = Field(default_factory=lambda: settings.DEFAULT_VIDEO_PLATFORM)
|
||||
skip_consent: bool = False
|
||||
email_transcript_to: str | None = None
|
||||
store_video: bool = False
|
||||
|
||||
|
||||
class RoomController:
|
||||
@@ -150,6 +154,7 @@ class RoomController:
|
||||
platform: Platform = settings.DEFAULT_VIDEO_PLATFORM,
|
||||
skip_consent: bool = False,
|
||||
email_transcript_to: str | None = None,
|
||||
store_video: bool = False,
|
||||
):
|
||||
"""
|
||||
Add a new room
|
||||
@@ -176,6 +181,7 @@ class RoomController:
|
||||
"platform": platform,
|
||||
"skip_consent": skip_consent,
|
||||
"email_transcript_to": email_transcript_to,
|
||||
"store_video": store_video,
|
||||
}
|
||||
|
||||
room = Room(**room_data)
|
||||
|
||||
@@ -10,6 +10,7 @@ from reflector.hatchet.client import HatchetClientManager
|
||||
from reflector.hatchet.workflows.daily_multitrack_pipeline import (
|
||||
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.live_post_pipeline import live_post_pipeline
|
||||
from reflector.hatchet.workflows.subject_processing import subject_workflow
|
||||
@@ -54,10 +55,6 @@ def main():
|
||||
]
|
||||
)
|
||||
if _zulip_dag_enabled:
|
||||
from reflector.hatchet.workflows.failed_runs_monitor import ( # noqa: PLC0415
|
||||
failed_runs_monitor,
|
||||
)
|
||||
|
||||
workflows.append(failed_runs_monitor)
|
||||
logger.info(
|
||||
"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"]
|
||||
|
||||
# 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(
|
||||
"Raw-tracks recording queuing processing",
|
||||
recording_id=recording_id,
|
||||
|
||||
@@ -45,6 +45,7 @@ class Room(BaseModel):
|
||||
platform: Platform
|
||||
skip_consent: bool = False
|
||||
email_transcript_to: str | None = None
|
||||
store_video: bool = False
|
||||
|
||||
|
||||
class RoomDetails(Room):
|
||||
@@ -75,6 +76,7 @@ class Meeting(BaseModel):
|
||||
platform: Platform
|
||||
daily_composed_video_s3_key: str | None = None
|
||||
daily_composed_video_duration: int | None = None
|
||||
store_video: bool = False
|
||||
|
||||
|
||||
class CreateRoom(BaseModel):
|
||||
@@ -95,6 +97,7 @@ class CreateRoom(BaseModel):
|
||||
platform: Platform
|
||||
skip_consent: bool = False
|
||||
email_transcript_to: str | None = None
|
||||
store_video: bool = False
|
||||
|
||||
|
||||
class UpdateRoom(BaseModel):
|
||||
@@ -115,6 +118,7 @@ class UpdateRoom(BaseModel):
|
||||
platform: Optional[Platform] = None
|
||||
skip_consent: Optional[bool] = None
|
||||
email_transcript_to: Optional[str] = None
|
||||
store_video: Optional[bool] = None
|
||||
|
||||
|
||||
class CreateRoomMeeting(BaseModel):
|
||||
@@ -257,6 +261,7 @@ async def rooms_create(
|
||||
platform=room.platform,
|
||||
skip_consent=room.skip_consent,
|
||||
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_trigger == room.recording_trigger
|
||||
and meeting.platform == room.platform
|
||||
and meeting.store_video == room.store_video
|
||||
)
|
||||
if not settings_match:
|
||||
logger.info(
|
||||
|
||||
@@ -30,6 +30,8 @@ def build_beat_schedule(
|
||||
whereby_api_key=None,
|
||||
aws_process_recording_queue_url=None,
|
||||
daily_api_key=None,
|
||||
livekit_api_key=None,
|
||||
livekit_url=None,
|
||||
public_mode=False,
|
||||
public_data_retention_days=None,
|
||||
healthcheck_url=None,
|
||||
@@ -83,7 +85,7 @@ def build_beat_schedule(
|
||||
else:
|
||||
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:
|
||||
beat_schedule["process_livekit_ended_meetings"] = {
|
||||
"task": "reflector.worker.process.process_livekit_ended_meetings",
|
||||
@@ -175,6 +177,8 @@ else:
|
||||
whereby_api_key=settings.WHEREBY_API_KEY,
|
||||
aws_process_recording_queue_url=settings.AWS_PROCESS_RECORDING_QUEUE_URL,
|
||||
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_data_retention_days=settings.PUBLIC_DATA_RETENTION_DAYS,
|
||||
healthcheck_url=settings.HEALTHCHECK_URL,
|
||||
|
||||
@@ -562,6 +562,15 @@ async def store_cloud_recording(
|
||||
)
|
||||
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(
|
||||
meeting_id=meeting.id,
|
||||
s3_key=s3_key,
|
||||
|
||||
@@ -32,6 +32,10 @@ DAILY_TASKS = {
|
||||
"trigger_daily_reconciliation",
|
||||
"reprocess_failed_daily_recordings",
|
||||
}
|
||||
LIVEKIT_TASKS = {
|
||||
"process_livekit_ended_meetings",
|
||||
"reprocess_failed_livekit_recordings",
|
||||
}
|
||||
PLATFORM_TASKS = {
|
||||
"process_meetings",
|
||||
"sync_all_ics_calendars",
|
||||
@@ -47,6 +51,7 @@ class TestNoPlatformConfigured:
|
||||
task_names = set(schedule.keys())
|
||||
assert not task_names & WHEREBY_TASKS
|
||||
assert not task_names & DAILY_TASKS
|
||||
assert not task_names & LIVEKIT_TASKS
|
||||
assert not task_names & PLATFORM_TASKS
|
||||
|
||||
def test_only_healthcheck_disabled_warning(self):
|
||||
@@ -72,6 +77,7 @@ class TestWherebyOnly:
|
||||
assert WHEREBY_TASKS <= task_names
|
||||
assert PLATFORM_TASKS <= task_names
|
||||
assert not task_names & DAILY_TASKS
|
||||
assert not task_names & LIVEKIT_TASKS
|
||||
|
||||
def test_whereby_sqs_url(self):
|
||||
schedule = build_beat_schedule(
|
||||
@@ -81,6 +87,7 @@ class TestWherebyOnly:
|
||||
assert WHEREBY_TASKS <= task_names
|
||||
assert PLATFORM_TASKS <= task_names
|
||||
assert not task_names & DAILY_TASKS
|
||||
assert not task_names & LIVEKIT_TASKS
|
||||
|
||||
def test_whereby_task_count(self):
|
||||
schedule = build_beat_schedule(whereby_api_key="test-key")
|
||||
@@ -97,6 +104,7 @@ class TestDailyOnly:
|
||||
assert DAILY_TASKS <= task_names
|
||||
assert PLATFORM_TASKS <= task_names
|
||||
assert not task_names & WHEREBY_TASKS
|
||||
assert not task_names & LIVEKIT_TASKS
|
||||
|
||||
def test_daily_task_count(self):
|
||||
schedule = build_beat_schedule(daily_api_key="test-daily-key")
|
||||
@@ -104,6 +112,33 @@ class TestDailyOnly:
|
||||
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:
|
||||
"""When both Whereby and Daily.co are configured."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user