From 652c3d16fb9f80ada4026ab869e4dc28f0ad8cda Mon Sep 17 00:00:00 2001 From: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Date: Thu, 27 Jun 2024 19:41:15 +0530 Subject: [PATCH] fix: reduce cal recording bitrate (#15588) * fix: reduce cal recording bitrate * chore: bitrate * chore: add enable recording ui * fix: genereate meeting token * chore: add wait for recording --- apps/web/modules/videos/ai/ai-transcribe.tsx | 101 +++++++++++++++--- .../videos-single-view.getServerSideProps.tsx | 11 +- .../videos/views/videos-single-view.tsx | 8 +- apps/web/public/start-recording.svg | 1 + apps/web/public/stop-recording.svg | 1 + .../dailyvideo/lib/VideoApiAdapter.ts | 95 ++++++---------- packages/app-store/dailyvideo/lib/types.ts | 66 ++++++++++++ packages/lib/constants.ts | 8 ++ 8 files changed, 211 insertions(+), 80 deletions(-) create mode 100644 apps/web/public/start-recording.svg create mode 100644 apps/web/public/stop-recording.svg create mode 100644 packages/app-store/dailyvideo/lib/types.ts diff --git a/apps/web/modules/videos/ai/ai-transcribe.tsx b/apps/web/modules/videos/ai/ai-transcribe.tsx index fa5327f6c179e6..5e7bf9152ca2d8 100644 --- a/apps/web/modules/videos/ai/ai-transcribe.tsx +++ b/apps/web/modules/videos/ai/ai-transcribe.tsx @@ -1,10 +1,48 @@ -import { useTranscription } from "@daily-co/daily-react"; +import { useTranscription, useRecording } from "@daily-co/daily-react"; import { useDaily, useDailyEvent } from "@daily-co/daily-react"; import React, { Fragment, useCallback, useRef, useState, useLayoutEffect, useEffect } from "react"; -import { TRANSCRIPTION_STARTED_ICON, TRANSCRIPTION_STOPPED_ICON } from "@calcom/lib/constants"; +import { + TRANSCRIPTION_STARTED_ICON, + RECORDING_IN_PROGRESS_ICON, + TRANSCRIPTION_STOPPED_ICON, + RECORDING_DEFAULT_ICON, +} from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +const BUTTONS = { + STOP_TRANSCRIPTION: { + label: "Stop", + tooltip: "Stop transcription", + iconPath: TRANSCRIPTION_STARTED_ICON, + iconPathDarkMode: TRANSCRIPTION_STARTED_ICON, + }, + START_TRANSCRIPTION: { + label: "Cal.ai", + tooltip: "Transcription powered by AI", + iconPath: TRANSCRIPTION_STOPPED_ICON, + iconPathDarkMode: TRANSCRIPTION_STOPPED_ICON, + }, + START_RECORDING: { + label: "Record", + tooltip: "Start recording", + iconPath: RECORDING_DEFAULT_ICON, + iconPathDarkMode: RECORDING_DEFAULT_ICON, + }, + WAIT_FOR_RECORDING_TO_START: { + label: "Starting..", + tooltip: "Please wait while we start recording", + iconPath: RECORDING_DEFAULT_ICON, + iconPathDarkMode: RECORDING_DEFAULT_ICON, + }, + STOP_RECORDING: { + label: "Stop", + tooltip: "Stop recording", + iconPath: RECORDING_IN_PROGRESS_ICON, + iconPathDarkMode: RECORDING_IN_PROGRESS_ICON, + }, +}; + export const CalAiTranscribe = () => { const daily = useDaily(); const { t } = useLocale(); @@ -15,6 +53,7 @@ export const CalAiTranscribe = () => { const transcriptRef = useRef(null); const transcription = useTranscription(); + const recording = useRecording(); useDailyEvent( "app-message", @@ -26,36 +65,64 @@ export const CalAiTranscribe = () => { useDailyEvent("transcription-started", (ev) => { daily?.updateCustomTrayButtons({ - transcription: { - label: "Stop", - tooltip: "Stop transcription", - iconPath: TRANSCRIPTION_STARTED_ICON, - iconPathDarkMode: TRANSCRIPTION_STARTED_ICON, - }, + recording: recording?.isRecording ? BUTTONS.STOP_RECORDING : BUTTONS.START_RECORDING, + transcription: BUTTONS.STOP_TRANSCRIPTION, + }); + }); + + useDailyEvent("recording-started", (ev) => { + daily?.updateCustomTrayButtons({ + recording: BUTTONS.STOP_RECORDING, + transcription: transcription?.isTranscribing ? BUTTONS.STOP_TRANSCRIPTION : BUTTONS.START_TRANSCRIPTION, }); }); useDailyEvent("transcription-stopped", (ev) => { daily?.updateCustomTrayButtons({ - transcription: { - label: "Cal.ai", - tooltip: "Transcription powered by AI", - iconPath: TRANSCRIPTION_STOPPED_ICON, - iconPathDarkMode: TRANSCRIPTION_STOPPED_ICON, - }, + recording: recording?.isRecording ? BUTTONS.STOP_RECORDING : BUTTONS.START_RECORDING, + transcription: BUTTONS.START_TRANSCRIPTION, + }); + }); + + useDailyEvent("recording-stopped", (ev) => { + daily?.updateCustomTrayButtons({ + recording: BUTTONS.START_RECORDING, + transcription: transcription?.isTranscribing ? BUTTONS.STOP_TRANSCRIPTION : BUTTONS.START_TRANSCRIPTION, }); }); - useDailyEvent("custom-button-click", (ev) => { - if (ev?.button_id !== "transcription") { - return; + const toggleRecording = async () => { + if (recording?.isRecording) { + await daily?.stopRecording(); + } else { + daily?.updateCustomTrayButtons({ + recording: BUTTONS.WAIT_FOR_RECORDING_TO_START, + transcription: transcription?.isTranscribing + ? BUTTONS.STOP_TRANSCRIPTION + : BUTTONS.START_TRANSCRIPTION, + }); + + await daily?.startRecording({ + // 480p + videoBitrate: 2000, + }); } + }; + const toggleTranscription = async () => { if (transcription?.isTranscribing) { daily?.stopTranscription(); } else { daily?.startTranscription(); } + }; + + useDailyEvent("custom-button-click", async (ev) => { + if (ev?.button_id === "recording") { + toggleRecording(); + } else if (ev?.button_id === "transcription") { + toggleTranscription(); + } }); useLayoutEffect(() => { diff --git a/apps/web/modules/videos/views/videos-single-view.getServerSideProps.tsx b/apps/web/modules/videos/views/videos-single-view.getServerSideProps.tsx index b5c3d085c9669b..6549defb36209c 100644 --- a/apps/web/modules/videos/views/videos-single-view.getServerSideProps.tsx +++ b/apps/web/modules/videos/views/videos-single-view.getServerSideProps.tsx @@ -1,6 +1,7 @@ import MarkdownIt from "markdown-it"; import type { GetServerSidePropsContext } from "next"; +import { generateGuestMeetingTokenFromOwnerMeetingToken } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { getCalVideoReference } from "@calcom/features/get-cal-video-reference"; import { UserRepository } from "@calcom/lib/server/repository/user"; @@ -103,12 +104,18 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { const session = await getServerSession({ req }); - // set meetingPassword to null for guests + // set meetingPassword for guests if (session?.user.id !== bookingObj.user?.id) { + const videoReference = getCalVideoReference(bookingObj.references); + const guestMeetingPassword = await generateGuestMeetingTokenFromOwnerMeetingToken( + videoReference.meetingPassword + ); + bookingObj.references.forEach((bookRef) => { - bookRef.meetingPassword = null; + bookRef.meetingPassword = guestMeetingPassword; }); } + const videoReference = getCalVideoReference(bookingObj.references); return { diff --git a/apps/web/modules/videos/views/videos-single-view.tsx b/apps/web/modules/videos/views/videos-single-view.tsx index 4261092467fe6a..95dac5e05bb8ab 100644 --- a/apps/web/modules/videos/views/videos-single-view.tsx +++ b/apps/web/modules/videos/views/videos-single-view.tsx @@ -9,7 +9,7 @@ import { useState, useEffect, useRef } from "react"; import dayjs from "@calcom/dayjs"; import classNames from "@calcom/lib/classNames"; import { APP_NAME, SEO_IMG_OGIMG_VIDEO, WEBSITE_URL } from "@calcom/lib/constants"; -import { TRANSCRIPTION_STOPPED_ICON } from "@calcom/lib/constants"; +import { TRANSCRIPTION_STOPPED_ICON, RECORDING_DEFAULT_ICON } from "@calcom/lib/constants"; import { formatToLocalizedDate, formatToLocalizedTime } from "@calcom/lib/date-fns"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; @@ -50,6 +50,12 @@ export default function JoinCall(props: PageProps) { ...(typeof meetingPassword === "string" && { token: meetingPassword }), ...(hasTeamPlan && { customTrayButtons: { + recording: { + label: "Record", + tooltip: "Start or stop recording", + iconPath: RECORDING_DEFAULT_ICON, + iconPathDarkMode: RECORDING_DEFAULT_ICON, + }, transcription: { label: "Cal.ai", tooltip: "Transcription powered by AI", diff --git a/apps/web/public/start-recording.svg b/apps/web/public/start-recording.svg new file mode 100644 index 00000000000000..83c3215d48be3e --- /dev/null +++ b/apps/web/public/start-recording.svg @@ -0,0 +1 @@ + diff --git a/apps/web/public/stop-recording.svg b/apps/web/public/stop-recording.svg new file mode 100644 index 00000000000000..5264d7790f3a11 --- /dev/null +++ b/apps/web/public/stop-recording.svg @@ -0,0 +1 @@ + diff --git a/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts b/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts index 4442727595c15e..980c54746efafc 100644 --- a/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts +++ b/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts @@ -16,60 +16,14 @@ import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapt import { ZSubmitBatchProcessorJobRes, ZGetTranscriptAccessLink } from "../zod"; import type { TSubmitBatchProcessorJobRes, TGetTranscriptAccessLink, batchProcessorBody } from "../zod"; import { getDailyAppKeys } from "./getDailyAppKeys"; - -/** @link https://docs.daily.co/reference/rest-api/rooms/create-room */ -const dailyReturnTypeSchema = z.object({ - /** Long UID string ie: 987b5eb5-d116-4a4e-8e2c-14fcb5710966 */ - id: z.string(), - /** Not a real name, just a random generated string ie: "ePR84NQ1bPigp79dDezz" */ - name: z.string(), - api_created: z.boolean(), - privacy: z.union([z.literal("private"), z.literal("public")]), - /** https://api-demo.daily.co/ePR84NQ1bPigp79dDezz */ - url: z.string(), - created_at: z.string(), - config: z.object({ - /** Timestamps expressed in seconds, not in milliseconds */ - nbf: z.number().optional(), - /** Timestamps expressed in seconds, not in milliseconds */ - exp: z.number(), - enable_chat: z.boolean(), - enable_knocking: z.boolean(), - enable_prejoin_ui: z.boolean(), - enable_transcription_storage: z.boolean().default(false), - }), -}); - -const getTranscripts = z.object({ - total_count: z.number(), - data: z.array( - z.object({ - transcriptId: z.string(), - domainId: z.string(), - roomId: z.string(), - mtgSessionId: z.string(), - duration: z.number(), - status: z.string(), - }) - ), -}); - -const getBatchProcessJobs = z.object({ - total_count: z.number(), - data: z.array( - z.object({ - id: z.string(), - preset: z.string(), - status: z.string(), - }) - ), -}); - -const getRooms = z - .object({ - id: z.string(), - }) - .passthrough(); +import { + dailyReturnTypeSchema, + getTranscripts, + getBatchProcessJobs, + getRooms, + meetingTokenSchema, + ZGetMeetingTokenResponseSchema, +} from "./types"; export interface DailyEventResult { id: string; @@ -88,10 +42,6 @@ export interface DailyVideoCallData { url: string; } -const meetingTokenSchema = z.object({ - token: z.string(), -}); - /** @deprecated use metadata on index file */ export const FAKE_DAILY_CREDENTIAL: CredentialPayload & { invalid: boolean } = { id: 0, @@ -151,6 +101,21 @@ async function processTranscriptsInBatches(transcriptIds: Array) { return allTranscriptsAccessLinks; } +export const generateGuestMeetingTokenFromOwnerMeetingToken = async (meetingToken: string | null) => { + if (!meetingToken) return null; + + const token = await fetcher(`/meeting-tokens/${meetingToken}`).then(ZGetMeetingTokenResponseSchema.parse); + const guestMeetingToken = await postToDailyAPI("/meeting-tokens", { + properties: { + room_name: token.room_name, + exp: token.exp, + enable_recording_ui: false, + }, + }).then(meetingTokenSchema.parse); + + return guestMeetingToken.token; +}; + const DailyVideoApiAdapter = (): VideoApiAdapter => { async function createOrUpdateMeeting(endpoint: string, event: CalendarEvent): Promise { if (!event.uid) { @@ -159,7 +124,12 @@ const DailyVideoApiAdapter = (): VideoApiAdapter => { const body = await translateEvent(event); const dailyEvent = await postToDailyAPI(endpoint, body).then(dailyReturnTypeSchema.parse); const meetingToken = await postToDailyAPI("/meeting-tokens", { - properties: { room_name: dailyEvent.name, exp: dailyEvent.config.exp, is_owner: true }, + properties: { + room_name: dailyEvent.name, + exp: dailyEvent.config.exp, + is_owner: true, + enable_recording_ui: false, + }, }).then(meetingTokenSchema.parse); return Promise.resolve({ @@ -233,7 +203,12 @@ const DailyVideoApiAdapter = (): VideoApiAdapter => { const dailyEvent = await postToDailyAPI("/rooms", body).then(dailyReturnTypeSchema.parse); const meetingToken = await postToDailyAPI("/meeting-tokens", { - properties: { room_name: dailyEvent.name, exp: dailyEvent.config.exp, is_owner: true }, + properties: { + room_name: dailyEvent.name, + exp: dailyEvent.config.exp, + is_owner: true, + enable_recording_ui: false, + }, }).then(meetingTokenSchema.parse); return Promise.resolve({ diff --git a/packages/app-store/dailyvideo/lib/types.ts b/packages/app-store/dailyvideo/lib/types.ts new file mode 100644 index 00000000000000..16945a102ae24d --- /dev/null +++ b/packages/app-store/dailyvideo/lib/types.ts @@ -0,0 +1,66 @@ +import { z } from "zod"; + +/** @link https://docs.daily.co/reference/rest-api/rooms/create-room */ +export const dailyReturnTypeSchema = z.object({ + /** Long UID string ie: 987b5eb5-d116-4a4e-8e2c-14fcb5710966 */ + id: z.string(), + /** Not a real name, just a random generated string ie: "ePR84NQ1bPigp79dDezz" */ + name: z.string(), + api_created: z.boolean(), + privacy: z.union([z.literal("private"), z.literal("public")]), + /** https://api-demo.daily.co/ePR84NQ1bPigp79dDezz */ + url: z.string(), + created_at: z.string(), + config: z.object({ + /** Timestamps expressed in seconds, not in milliseconds */ + nbf: z.number().optional(), + /** Timestamps expressed in seconds, not in milliseconds */ + exp: z.number(), + enable_chat: z.boolean(), + enable_knocking: z.boolean(), + enable_prejoin_ui: z.boolean(), + enable_transcription_storage: z.boolean().default(false), + }), +}); + +export const getTranscripts = z.object({ + total_count: z.number(), + data: z.array( + z.object({ + transcriptId: z.string(), + domainId: z.string(), + roomId: z.string(), + mtgSessionId: z.string(), + duration: z.number(), + status: z.string(), + }) + ), +}); + +export const getBatchProcessJobs = z.object({ + total_count: z.number(), + data: z.array( + z.object({ + id: z.string(), + preset: z.string(), + status: z.string(), + }) + ), +}); + +export const getRooms = z + .object({ + id: z.string(), + }) + .passthrough(); + +export const meetingTokenSchema = z.object({ + token: z.string(), +}); + +export const ZGetMeetingTokenResponseSchema = z + .object({ + room_name: z.string(), + exp: z.number(), + }) + .passthrough(); diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index 418bb795c2752b..5deb46ea7c6419 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -165,3 +165,11 @@ export const TRANSCRIPTION_STARTED_ICON = IS_PRODUCTION export const TRANSCRIPTION_STOPPED_ICON = IS_PRODUCTION ? `${WEBAPP_URL}/sparkles.svg` : `https://app.cal.com/sparkles.svg`; + +export const RECORDING_DEFAULT_ICON = IS_PRODUCTION + ? `${WEBAPP_URL}/start-recording.svg` + : `https://app.cal.com/start-recording.svg`; + +export const RECORDING_IN_PROGRESS_ICON = IS_PRODUCTION + ? `${WEBAPP_URL}/stop-recording.svg` + : `https://app.cal.com/stop-recording.svg`;