Skip to content

Commit

Permalink
fix: reduce cal recording bitrate (#15588)
Browse files Browse the repository at this point in the history
* fix: reduce cal recording bitrate

* chore: bitrate

* chore: add enable recording ui

* fix: genereate meeting token

* chore: add wait for recording
  • Loading branch information
Udit-takkar authored Jun 27, 2024
1 parent e5816d6 commit 652c3d1
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 80 deletions.
101 changes: 84 additions & 17 deletions apps/web/modules/videos/ai/ai-transcribe.tsx
Original file line number Diff line number Diff line change
@@ -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();

Check warning on line 48 in apps/web/modules/videos/ai/ai-transcribe.tsx

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

apps/web/modules/videos/ai/ai-transcribe.tsx#L48

[@typescript-eslint/no-unused-vars] 't' is assigned a value but never used. Allowed unused vars must match /^_/u.
Expand All @@ -15,6 +53,7 @@ export const CalAiTranscribe = () => {
const transcriptRef = useRef<HTMLDivElement | null>(null);

const transcription = useTranscription();
const recording = useRecording();

useDailyEvent(
"app-message",
Expand All @@ -26,36 +65,64 @@ export const CalAiTranscribe = () => {

useDailyEvent("transcription-started", (ev) => {

Check warning on line 66 in apps/web/modules/videos/ai/ai-transcribe.tsx

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

apps/web/modules/videos/ai/ai-transcribe.tsx#L66

[@typescript-eslint/no-unused-vars] 'ev' is defined but never used. Allowed unused args must match /^_/u.
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) => {

Check warning on line 73 in apps/web/modules/videos/ai/ai-transcribe.tsx

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

apps/web/modules/videos/ai/ai-transcribe.tsx#L73

[@typescript-eslint/no-unused-vars] 'ev' is defined but never used. Allowed unused args must match /^_/u.
daily?.updateCustomTrayButtons({
recording: BUTTONS.STOP_RECORDING,
transcription: transcription?.isTranscribing ? BUTTONS.STOP_TRANSCRIPTION : BUTTONS.START_TRANSCRIPTION,
});
});

useDailyEvent("transcription-stopped", (ev) => {

Check warning on line 80 in apps/web/modules/videos/ai/ai-transcribe.tsx

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

apps/web/modules/videos/ai/ai-transcribe.tsx#L80

[@typescript-eslint/no-unused-vars] 'ev' is defined but never used. Allowed unused args must match /^_/u.
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) => {

Check warning on line 87 in apps/web/modules/videos/ai/ai-transcribe.tsx

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

apps/web/modules/videos/ai/ai-transcribe.tsx#L87

[@typescript-eslint/no-unused-vars] 'ev' is defined but never used. Allowed unused args must match /^_/u.
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(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 {
Expand Down
8 changes: 7 additions & 1 deletion apps/web/modules/videos/views/videos-single-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions apps/web/public/start-recording.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/web/public/stop-recording.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
95 changes: 35 additions & 60 deletions packages/app-store/dailyvideo/lib/VideoApiAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -151,6 +101,21 @@ async function processTranscriptsInBatches(transcriptIds: Array<string>) {
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<VideoCallData> {
if (!event.uid) {
Expand All @@ -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({
Expand Down Expand Up @@ -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({
Expand Down
66 changes: 66 additions & 0 deletions packages/app-store/dailyvideo/lib/types.ts
Original file line number Diff line number Diff line change
@@ -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();
Loading

0 comments on commit 652c3d1

Please sign in to comment.