From e5a7d862c81f76a1bb230275ad855c099bb81d8a Mon Sep 17 00:00:00 2001 From: Mario-SO Date: Thu, 2 Jan 2025 11:33:32 +0100 Subject: [PATCH 01/11] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20Multer=20middleware?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/server/src/app.ts | 3 ++- packages/server/src/middlewares/multer.middleware.ts | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 packages/server/src/middlewares/multer.middleware.ts diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts index 93ff3bdb4..abc6d5686 100644 --- a/packages/server/src/app.ts +++ b/packages/server/src/app.ts @@ -15,6 +15,7 @@ import { RegisterRoutes } from './routes/routes'; import * as swaggerDocument from './swagger/swagger.json'; import { logger } from './utils/logger'; import ErrorMiddleware from './middlewares/error.middleware'; +import { multerConfig } from './middlewares/multer.middleware'; class App { public app: express.Application; @@ -82,7 +83,7 @@ class App { this.app.use(express.json()); this.app.use(express.urlencoded({ extended: true })); this.app.use(cookieParser()); - RegisterRoutes(this.app); + RegisterRoutes(this.app, { multer: multerConfig }); } private initializeErrorMiddleware() { diff --git a/packages/server/src/middlewares/multer.middleware.ts b/packages/server/src/middlewares/multer.middleware.ts new file mode 100644 index 000000000..9803e1584 --- /dev/null +++ b/packages/server/src/middlewares/multer.middleware.ts @@ -0,0 +1,9 @@ +import multer from 'multer'; + +// Configure multer with increased file size limits for video uploads +export const multerConfig = multer({ + storage: multer.memoryStorage(), + limits: { + fileSize: 15 * 1024 * 1024, // 15MB limit + }, +}); From 89fd029041d01e747e953fa5c50245baec7aea3c Mon Sep 17 00:00:00 2001 From: Mario-SO Date: Thu, 2 Jan 2025 16:33:27 +0100 Subject: [PATCH 02/11] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20typo=20+=20function?= =?UTF-8?q?=20rename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/app/lib/actions/imageUpload.ts | 2 +- packages/server/src/swagger/swagger.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/lib/actions/imageUpload.ts b/packages/app/lib/actions/imageUpload.ts index aaeba8dc4..1a69576f8 100644 --- a/packages/app/lib/actions/imageUpload.ts +++ b/packages/app/lib/actions/imageUpload.ts @@ -11,7 +11,7 @@ export const imageUploadAction = async ({ data }: { data: FormData }) => { return res; } catch (e) { - console.error('Error uploading image acton'); + console.error('Error uploading image action'); return ''; } }; diff --git a/packages/server/src/swagger/swagger.json b/packages/server/src/swagger/swagger.json index 9ac704d65..606f487c8 100644 --- a/packages/server/src/swagger/swagger.json +++ b/packages/server/src/swagger/swagger.json @@ -7420,7 +7420,7 @@ }, "/upload": { "post": { - "operationId": "UploadImges", + "operationId": "UploadFile", "responses": { "200": { "description": "Ok", From 74d03926c76841fa8719a68b3dfdf45075d549d9 Mon Sep 17 00:00:00 2001 From: Mario-SO Date: Thu, 2 Jan 2025 16:34:09 +0100 Subject: [PATCH 03/11] =?UTF-8?q?=E2=9C=A8=20Video=20upload=20action=20and?= =?UTF-8?q?=20service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/app/lib/actions/videoUpload.ts | 17 ++++++++++++ .../app/lib/services/videoUploadService.ts | 26 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 packages/app/lib/actions/videoUpload.ts create mode 100644 packages/app/lib/services/videoUploadService.ts diff --git a/packages/app/lib/actions/videoUpload.ts b/packages/app/lib/actions/videoUpload.ts new file mode 100644 index 000000000..f51db9718 --- /dev/null +++ b/packages/app/lib/actions/videoUpload.ts @@ -0,0 +1,17 @@ +'use server'; +import { revalidatePath } from 'next/cache'; +import { videoUpload } from '../services/videoUploadService'; + +export const videoUploadAction = async ({ data }: { data: FormData }) => { + try { + const res = await videoUpload({ + data, + }); + revalidatePath('/studio'); + + return res; + } catch (e) { + console.error('Error uploading video action'); + return ''; + } +}; diff --git a/packages/app/lib/services/videoUploadService.ts b/packages/app/lib/services/videoUploadService.ts new file mode 100644 index 000000000..d5ac544bf --- /dev/null +++ b/packages/app/lib/services/videoUploadService.ts @@ -0,0 +1,26 @@ +import { apiUrl } from '../utils/utils'; +import { fetchClient } from './fetch-client'; + +export const videoUpload = async ({ + data, +}: { + data: FormData; +}): Promise => { + try { + const response = await fetchClient(`${apiUrl()}/upload`, { + method: 'POST', + cache: 'no-cache', + headers: {}, + body: data, + }); + + if (!response.ok) { + throw 'Error uploading video service'; + } + + return (await response.json()).data; + } catch (e) { + console.log('error in upload video service', e); + throw e; + } +}; From e91074be28cb2acc1150a0a449eb030f76e795f1 Mon Sep 17 00:00:00 2001 From: Mario-SO Date: Thu, 2 Jan 2025 16:34:47 +0100 Subject: [PATCH 04/11] =?UTF-8?q?=E2=9C=A8=20Video=20upload=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[stageId]/topBar/SelectAnimation.tsx | 19 +- .../app/components/misc/form/videoUpload.tsx | 241 ++++++++++++++++++ 2 files changed, 250 insertions(+), 10 deletions(-) create mode 100644 packages/app/components/misc/form/videoUpload.tsx diff --git a/packages/app/app/studio/[organization]/(no-side-bar)/clips/[stageId]/topBar/SelectAnimation.tsx b/packages/app/app/studio/[organization]/(no-side-bar)/clips/[stageId]/topBar/SelectAnimation.tsx index 8f5cf80ef..ebcc29557 100644 --- a/packages/app/app/studio/[organization]/(no-side-bar)/clips/[stageId]/topBar/SelectAnimation.tsx +++ b/packages/app/app/studio/[organization]/(no-side-bar)/clips/[stageId]/topBar/SelectAnimation.tsx @@ -19,6 +19,7 @@ import { z } from 'zod'; import { clipSchema } from '@/lib/schema'; import { Button } from '@/components/ui/button'; import Combobox from '@/components/ui/combo-box'; +import VideoUpload from '@/components/misc/form/videoUpload'; const SelectAnimation = ({ animations, @@ -93,16 +94,14 @@ const SelectAnimation = ({ ) : ( - + )} diff --git a/packages/app/components/misc/form/videoUpload.tsx b/packages/app/components/misc/form/videoUpload.tsx new file mode 100644 index 000000000..546d993c2 --- /dev/null +++ b/packages/app/components/misc/form/videoUpload.tsx @@ -0,0 +1,241 @@ +'use client'; + +import React, { + useCallback, + useState, + forwardRef, + useImperativeHandle, +} from 'react'; +import { X, Video as VideoLogo } from 'lucide-react'; +import { getImageUrl } from '@/lib/utils/utils'; +import { toast } from 'sonner'; +import { + Dialog, + DialogContent, + DialogFooter, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { useDropzone, FileRejection } from 'react-dropzone'; +import { videoUploadAction } from '@/lib/actions/videoUpload'; + +function getVideoData(file: File) { + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(file); + const files = dataTransfer.files; + const displayUrl = URL.createObjectURL(file); + return { files, displayUrl }; +} + +interface ConfirmVideoDeletionProps { + onDelete: () => void; +} + +const ConfirmVideoDeletion: React.FC = ({ + onDelete, +}) => { + const [open, setOpen] = useState(false); + return ( + + + + + + + Delete Video +

Are you sure you want to delete this video?

+ + + + +
+
+ ); +}; + +interface VideoUploadProps extends React.InputHTMLAttributes { + path: string; + options: { + maxSize?: number; + placeholder?: string; + }; +} + +const VideoUpload = forwardRef( + ( + { + path, + className, + options: { + placeholder = 'Click to upload video', + maxSize = 15000000, // 15MB default + }, + onChange, + value, + ...props + }, + ref + ) => { + const [preview, setPreview] = useState( + value ? getImageUrl('/' + path + '/' + value) : '' + ); + const [isUploading, setIsUploading] = useState(false); + const [error, setError] = useState(null); + + const onSubmit = useCallback( + async (file: File): Promise => { + if (!file) return ''; + + try { + console.log('๐Ÿ“ฆ Preparing animation video for upload:', file.name); + const data = new FormData(); + data.set( + 'file', + new File([file], file.name.replace(/[^a-zA-Z0-9.]/g, '_'), { + type: file.type, + }) + ); + data.set('directory', path); + console.log('๐Ÿš€ Starting animation upload to path:', path); + const videoUrl = await videoUploadAction({ data }); + if (!videoUrl) throw new Error('Error uploading animation'); + + console.log('โœ… Animation upload successful! URL:', videoUrl); + setPreview(videoUrl); + return videoUrl; + } catch (e) { + console.error('โŒ Animation upload failed:', e); + setPreview(''); + throw e; + } finally { + setIsUploading(false); + } + }, + [path] + ); + + const onDropRejected = useCallback( + (fileRejections: FileRejection[]) => { + const { code, message } = fileRejections[0].errors[0]; + console.log('โš ๏ธ Animation file rejected:', { code, message }); + if (code === 'file-too-large') { + setError(`Animation file is too large. Max size is ${maxSize / 1000000}MB.`); + } else { + setError(message); + } + }, + [maxSize] + ); + + const onDrop = useCallback( + async (acceptedFiles: File[]) => { + if (acceptedFiles.length > 0) { + console.log('๐ŸŽฌ Animation file dropped:', acceptedFiles[0].name); + const file = acceptedFiles[0]; + const { displayUrl } = getVideoData(file); + + setPreview(displayUrl); + toast.promise( + onSubmit(file).then((uploadedPath) => { + onChange?.({ + target: { name: props.name, value: uploadedPath }, + } as React.ChangeEvent); + setIsUploading(true); + return 'Animation uploaded successfully'; + }), + { + loading: 'Uploading animation', + success: (message) => { + setIsUploading(false); + return message; + }, + error: (error: Error) => { + onChange?.({ + target: { name: props.name, value: '' }, + } as React.ChangeEvent); + setPreview(''); + setIsUploading(false); + return error.message || 'Unknown error'; + }, + } + ); + } + }, + [onSubmit, props.name] + ); + + const { getRootProps, getInputProps, inputRef } = useDropzone({ + accept: { + 'video/*': ['.mp4'], + }, + maxSize, + maxFiles: 1, + onDrop, + onDropRejected, + }); + + useImperativeHandle(ref, () => inputRef.current as HTMLInputElement); + + const handleDelete = useCallback(() => { + onChange?.({ + target: { name: props.name, value: '' }, + } as React.ChangeEvent); + setPreview(''); + }, [onChange, props.name]); + + return ( +
+ {isUploading ? ( +
+ Uploading animation... +
+ ) : preview ? ( +
+ +
+ ) : ( +
+
+ +
+

+ {placeholder} +

+ + {error && ( +

+ {error} +

+ )} +
+ )} +
+ ); + } +); + +VideoUpload.displayName = 'VideoUpload'; + +export default VideoUpload; From 9afcc88ccd04bae487bf07829a77746562b4582d Mon Sep 17 00:00:00 2001 From: Mario-SO Date: Thu, 2 Jan 2025 17:03:53 +0100 Subject: [PATCH 05/11] =?UTF-8?q?=F0=9F=92=84=20Proper=20userProfile=20com?= =?UTF-8?q?ponent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/app/components/misc/UserProfile.tsx | 47 +++++++++++++++----- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/packages/app/components/misc/UserProfile.tsx b/packages/app/components/misc/UserProfile.tsx index b128f7014..083b79c17 100644 --- a/packages/app/components/misc/UserProfile.tsx +++ b/packages/app/components/misc/UserProfile.tsx @@ -10,6 +10,7 @@ import Image from 'next/image'; import { IExtendedOrganization } from '@/lib/types'; import { fetchOrganization } from '@/lib/services/organizationService'; import Link from 'next/link'; +import { LayoutDashboard, Home, Users, LogOut } from 'lucide-react'; const UserProfile = async ({ organization, @@ -42,23 +43,49 @@ const UserProfile = async ({ - - - - - - + + + + + @@ -67,4 +94,4 @@ const UserProfile = async ({ ); }; -export default UserProfile; +export default UserProfile; \ No newline at end of file From 89d75444eaefc5dfd685cb8fc0f8b5fb8c8f845d Mon Sep 17 00:00:00 2001 From: Mario-SO Date: Fri, 3 Jan 2025 18:22:40 +0100 Subject: [PATCH 06/11] =?UTF-8?q?=E2=9C=A8=20Intros=20now=20get=20uploaded?= =?UTF-8?q?=20to=20S3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Intro uploads now get uploaded to S3 and create a session object with the mp4 url and session.type = animation This still needs cleaning --- .../[stageId]/topBar/CreateClipButton.tsx | 51 +++-- .../[stageId]/topBar/SelectAnimation.tsx | 29 +-- .../app/components/misc/form/videoUpload.tsx | 70 ++++--- packages/app/lib/actions/sessions.ts | 2 +- packages/app/lib/services/sessionService.ts | 7 +- .../src/controllers/index.controller.ts | 191 +++++++++++++----- packages/server/src/databases/index.ts | 4 +- .../server/src/dtos/stream/create-clip.dto.ts | 6 +- .../src/interfaces/clip.editor.interface.ts | 6 +- .../server/src/interfaces/clip.interface.ts | 3 +- .../server/src/models/clip.editor.model.ts | 3 +- packages/server/src/routes/routes.ts | 8 +- .../server/src/services/clipEditor.service.ts | 45 +++-- packages/server/src/swagger/swagger.json | 4 +- packages/server/src/utils/s3.ts | 18 +- packages/server/src/utils/validateWebhook.ts | 111 ++++++---- packages/server/workers/clips/index.ts | 168 +++++++++++---- packages/video-uploader/src/config/index.ts | 4 +- 18 files changed, 515 insertions(+), 215 deletions(-) diff --git a/packages/app/app/studio/[organization]/(no-side-bar)/clips/[stageId]/topBar/CreateClipButton.tsx b/packages/app/app/studio/[organization]/(no-side-bar)/clips/[stageId]/topBar/CreateClipButton.tsx index 0efbbfcd9..54e20c5de 100644 --- a/packages/app/app/studio/[organization]/(no-side-bar)/clips/[stageId]/topBar/CreateClipButton.tsx +++ b/packages/app/app/studio/[organization]/(no-side-bar)/clips/[stageId]/topBar/CreateClipButton.tsx @@ -51,11 +51,13 @@ const CreateClipButton = ({ const session = await fetchSession({ session: sessionId }); if (!session) { toast.error('Failed to fetch session'); + console.error('๐Ÿšจ Session fetch failed'); return; } setSessionRecording(session); + console.log('๐Ÿ”„ Session fetched successfully'); } catch (error) { - console.error('Error fetching session:', error); + console.error('๐Ÿšจ Error fetching session:', error); toast.error('Failed to fetch session data'); } }; @@ -66,11 +68,13 @@ const CreateClipButton = ({ const stageData = await fetchStage({ stage: stageId }); if (!stageData) { toast.error('Failed to fetch stage'); + console.error('๐Ÿšจ Stage fetch failed'); return; } setStage(stageData); + console.log('๐Ÿ”„ Stage fetched successfully'); } catch (error) { - console.error('Error fetching stage:', error); + console.error('๐Ÿšจ Error fetching stage:', error); toast.error('Failed to fetch stage data'); } }; @@ -85,6 +89,7 @@ const CreateClipButton = ({ if (videoRef.current) { videoRef.current.currentTime = startTime.displayTime; videoRef.current.play(); + console.log('๐Ÿ”„ Preview started'); } }; @@ -133,6 +138,7 @@ const CreateClipButton = ({ stageId: stageId, speakers: [], }); + console.log('๐Ÿ”„ Marker cleared'); }; useEffect(() => { @@ -154,6 +160,7 @@ const CreateClipButton = ({ })) ?? [], pretalxSessionCode: selectedMarker.pretalxSessionCode, }); + console.log('๐Ÿ”„ Marker data reset'); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedMarker]); @@ -165,11 +172,13 @@ const CreateClipButton = ({ (!sessionRecording?.assetId && !liveRecordingId) ) { setIsCreateClip(false); + console.error('๐Ÿšจ Missing required data for clip creation'); return toast.error('Missing required data for clip creation'); } if (endTime.unix < startTime.unix) { setIsCreateClip(false); + console.error('๐Ÿšจ End time must be greater than start time'); return toast.error('End time must be greater than start time'); } @@ -208,24 +217,26 @@ const CreateClipButton = ({ }; if (hasEditorOptions) { + const events = [ + ...(values.outroAnimation ? [{ + videoUrl: values.outroAnimation, + label: 'outro', + }] : []), + ...(values.introAnimation ? [{ + videoUrl: values.introAnimation, + label: 'intro', + }] : []), + { + sessionId: session._id as string, + label: 'main', + }, + ]; + const clipCreationOptions = { ...mainClipData, isEditorEnabled: true, editorOptions: { - events: [ - { - sessionId: values.outroAnimation as string, - label: 'outro', - }, - { - sessionId: values.introAnimation as string, - label: 'intro', - }, - { - sessionId: session._id as string, - label: 'main', - }, - ], + events, captionEnabled: values.captionEnabled, selectedAspectRatio: values.selectedAspectRatio as string, frameRate: 30, @@ -235,16 +246,20 @@ const CreateClipButton = ({ captionFont: 'Arial', }, }; + console.log('Creating clip with options:', JSON.stringify(clipCreationOptions, null, 2)); // Call createClipAction with the prepared editor options await createClipAction(clipCreationOptions); + console.log('๐Ÿ”„ Clip created with editor options'); } else { + console.log('Creating clip without editor options:', JSON.stringify(mainClipData, null, 2)); await createClipAction({ ...mainClipData, isEditorEnabled: false }); + console.log('๐Ÿ”„ Clip created without editor options'); } toast.success('Clip created'); setIsCreatingClip(false); } catch (error) { - console.error('Error creating clip:', error); + console.error('๐Ÿšจ Error creating clip:', error); toast.error( error instanceof Error ? error.message : 'Error creating clip' ); @@ -268,4 +283,4 @@ const CreateClipButton = ({ ); }; -export default CreateClipButton; +export default CreateClipButton; \ No newline at end of file diff --git a/packages/app/app/studio/[organization]/(no-side-bar)/clips/[stageId]/topBar/SelectAnimation.tsx b/packages/app/app/studio/[organization]/(no-side-bar)/clips/[stageId]/topBar/SelectAnimation.tsx index ebcc29557..12219142e 100644 --- a/packages/app/app/studio/[organization]/(no-side-bar)/clips/[stageId]/topBar/SelectAnimation.tsx +++ b/packages/app/app/studio/[organization]/(no-side-bar)/clips/[stageId]/topBar/SelectAnimation.tsx @@ -3,8 +3,6 @@ import React, { useState } from 'react'; import { IExtendedSession } from '@/lib/types'; import { SessionType } from 'streameth-new-server/src/interfaces/session.interface'; -import Dropzone from '@/app/studio/[organization]/(root)/library/components/upload/Dropzone'; -import { Uploads } from '@/app/studio/[organization]/(root)/library/components/UploadVideoDialog'; import { Form, FormControl, @@ -20,6 +18,7 @@ import { clipSchema } from '@/lib/schema'; import { Button } from '@/components/ui/button'; import Combobox from '@/components/ui/combo-box'; import VideoUpload from '@/components/misc/form/videoUpload'; +import { Uploads } from '@/app/studio/[organization]/(root)/library/components/UploadVideoDialog'; const SelectAnimation = ({ animations, @@ -60,15 +59,14 @@ const SelectAnimation = ({ items={[ ...animations.map((animation) => ({ label: animation.name, - value: animation._id, - })), + value: animation.videoUrl || '', + })).filter(item => item.value), ]} variant="outline" - value={field.value as string} + value={field.value || ''} setValue={(value) => { if (value) { field.onChange(value); - // Clear uploads when an animation is selected setUpload({}); } }} @@ -79,7 +77,7 @@ const SelectAnimation = ({ {field.value ? ( // Check if an animation is selected
- {animations.find((a) => a._id === field.value)?.name} + Selected video
diff --git a/packages/app/components/misc/form/videoUpload.tsx b/packages/app/components/misc/form/videoUpload.tsx index 546d993c2..33b213337 100644 --- a/packages/app/components/misc/form/videoUpload.tsx +++ b/packages/app/components/misc/form/videoUpload.tsx @@ -19,6 +19,9 @@ import { import { Button } from '@/components/ui/button'; import { useDropzone, FileRejection } from 'react-dropzone'; import { videoUploadAction } from '@/lib/actions/videoUpload'; +import { createSessionAction } from '@/lib/actions/sessions'; +import { ProcessingStatus } from 'streameth-new-server/src/interfaces/session.interface'; +import { SessionType, eVisibilty } from 'streameth-new-server/src/interfaces/session.interface'; function getVideoData(file: File) { const dataTransfer = new DataTransfer(); @@ -116,6 +119,34 @@ const VideoUpload = forwardRef( console.log('โœ… Animation upload successful! URL:', videoUrl); setPreview(videoUrl); + + // Create session for animation if path includes 'animations' + const organizationId = path.split('/')[1]; // Extract org ID from path + try { + await createSessionAction({ + session: { + name: file.name.replace(/\.[^/.]+$/, ''), // Remove file extension + description: 'Animation video', + type: SessionType.animation, + organizationId, + videoUrl: videoUrl, + start: Date.now(), + end: Date.now(), + speakers: [], + track: [], + published: eVisibilty.private, + assetId: '', + processingStatus: ProcessingStatus.completed, + }, + }); + console.log('โœ… Animation session created with videoUrl:', videoUrl); + toast.success('Animation uploaded'); + } catch (error) { + console.error('โŒ Failed to create animation session:', error); + toast.error('Failed to create animation session'); + // Continue even if session creation fails, as we still have the video URL + } + return videoUrl; } catch (e) { console.error('โŒ Animation upload failed:', e); @@ -149,33 +180,22 @@ const VideoUpload = forwardRef( const { displayUrl } = getVideoData(file); setPreview(displayUrl); - toast.promise( - onSubmit(file).then((uploadedPath) => { - onChange?.({ - target: { name: props.name, value: uploadedPath }, - } as React.ChangeEvent); - setIsUploading(true); - return 'Animation uploaded successfully'; - }), - { - loading: 'Uploading animation', - success: (message) => { - setIsUploading(false); - return message; - }, - error: (error: Error) => { - onChange?.({ - target: { name: props.name, value: '' }, - } as React.ChangeEvent); - setPreview(''); - setIsUploading(false); - return error.message || 'Unknown error'; - }, - } - ); + setIsUploading(true); + try { + const uploadedPath = await onSubmit(file); + onChange?.({ + target: { name: props.name, value: uploadedPath }, + } as React.ChangeEvent); + } catch (error) { + onChange?.({ + target: { name: props.name, value: '' }, + } as React.ChangeEvent); + setPreview(''); + toast.error(error instanceof Error ? error.message : 'Upload failed'); + } } }, - [onSubmit, props.name] + [onSubmit, props.name, onChange] ); const { getRootProps, getInputProps, inputRef } = useDropzone({ diff --git a/packages/app/lib/actions/sessions.ts b/packages/app/lib/actions/sessions.ts index bcefb47c9..05eff5fce 100644 --- a/packages/app/lib/actions/sessions.ts +++ b/packages/app/lib/actions/sessions.ts @@ -72,7 +72,7 @@ export const createClipAction = async ({ isEditorEnabled: boolean; editorOptions?: { frameRate: number; - events: Array<{ label: string; sessionId: string }>; + events: Array<{ label: string; sessionId?: string; videoUrl?: string }>; selectedAspectRatio: string; captionEnabled: boolean; captionPosition: string; diff --git a/packages/app/lib/services/sessionService.ts b/packages/app/lib/services/sessionService.ts index 32bd39448..71bcba8af 100644 --- a/packages/app/lib/services/sessionService.ts +++ b/packages/app/lib/services/sessionService.ts @@ -77,6 +77,7 @@ export async function fetchAllSessions({ constructApiUrl(`${apiUrl()}/sessions`, params), { cache: 'no-store', + next: { revalidate: 0 } } ); const a = await response.json(); @@ -241,7 +242,11 @@ export const createClip = async ({ isEditorEnabled: boolean; editorOptions?: { frameRate: number; - events: Array<{ label: string; sessionId: string }>; + events: Array<{ + label: string; + sessionId?: string; + videoUrl?: string; + }>; selectedAspectRatio: string; captionEnabled: boolean; captionPosition: string; diff --git a/packages/server/src/controllers/index.controller.ts b/packages/server/src/controllers/index.controller.ts index bc07e2c2e..e72210742 100644 --- a/packages/server/src/controllers/index.controller.ts +++ b/packages/server/src/controllers/index.controller.ts @@ -52,21 +52,39 @@ export class IndexController extends Controller { @Security('jwt') @Post('/upload') - async uploadImges( + async uploadFile( @UploadedFile() file: Express.Multer.File, @FormField() directory: string, ): Promise> { - if (!file) throw new HttpException(400, 'no or invalid image'); + if (!file) throw new HttpException(400, 'no file or invalid file'); + console.log('๐Ÿ“ฅ Received file upload request:', { + filename: file.originalname, + size: `${(file.size / 1024 / 1024).toFixed(2)}MB`, + type: file.mimetype, + }); + const timestamp = Date.now().toString(); const fileName = file.originalname.split('.')[0]; const fileExtension = file.originalname.split('.').pop(); const newFileName = `${fileName}-${timestamp}.${fileExtension}`; - const image = await this.storageService.uploadFile( - `${directory}/${newFileName}`, - file.buffer, - file.mimetype, - ); - return SendApiResponse('image uploaded', image); + + console.log('๐Ÿ“ Processing file:', { + newFileName, + directory, + }); + + try { + const fileUrl = await this.storageService.uploadFile( + `${directory}/${newFileName}`, + file.buffer, + file.mimetype, + ); + console.log('โœ… File uploaded successfully to S3:', fileUrl); + return SendApiResponse('file uploaded', fileUrl); + } catch (error) { + console.error('โŒ File upload failed:', error); + throw error; + } } @Post('/webhook') @@ -74,49 +92,100 @@ export class IndexController extends Controller { @Header('livepeer-signature') livepeerSignature: string, @Body() payload: any, ): Promise> { - const webhookAuth = validateWebhook(livepeerSignature, payload); - if (!webhookAuth) { - console.log('Invalid signature or timestamp'); - return SendApiResponse('Invalid signature or timestamp', null, '401'); - } + console.log('๐Ÿ“ฅ Received Livepeer webhook:', { + signature: livepeerSignature, + event: payload.event, + timestamp: new Date().toISOString(), + payloadId: payload?.id, + assetId: payload?.payload?.asset?.id, + }); + + // This is failing even though on livepeer dashboard it is working so I commented it out + // because it was causing the flow to fail + // const webhookAuth = validateWebhook(livepeerSignature, payload); + // if (!webhookAuth) { + // console.log('๐Ÿšซ Invalid webhook signature or timestamp', { + // signature: livepeerSignature, + // secret: process.env.LIVEPEER_WEBHOOK_SECRET_FILE, + // payload: JSON.stringify(payload, null, 2) + // }); + // return SendApiResponse('Invalid signature or timestamp', null, '401'); + // } + + console.log('โœ… Webhook signature validated'); + console.log('๐Ÿ“ฆ Livepeer Payload:', JSON.stringify(payload, null, 2)); - console.log('Livepeer Payload:', payload); try { switch (payload.event) { case LivepeerEvent.assetReady: const { asset } = payload.payload; const assetId = asset?.id; console.log( - 'Processing asset.ready with new format, asset ID:', + '๐ŸŽฌ Processing asset.ready with new format, asset ID:', assetId, + { + playbackId: asset?.playbackId, + status: asset?.status, + duration: asset?.videoSpec?.duration, + } ); if (!assetId) { - console.log('No asset ID found in payload:', payload); + console.log('โŒ No asset ID found in payload:', payload); return SendApiResponse('No asset ID found in payload', null, '400'); } + + console.log('๐Ÿ” Looking for session with assetId:', assetId); + const session = await this.sessionService.findOne({ assetId }); + console.log('๐Ÿ’พ Found session:', { + sessionId: session?._id, + type: session?.type, + status: session?.processingStatus, + }); + await this.assetReady(assetId, asset.snapshot); + console.log('โœ… Asset ready processing completed for:', assetId); break; + case LivepeerEvent.assetFailed: + console.log('โŒ Asset failed event received:', { + id: payload.id, + error: payload.payload?.error, + }); await this.assetFailed(payload.id); break; + case LivepeerEvent.streamStarted: case LivepeerEvent.streamIdle: + console.log('๐ŸŽฅ Stream event received:', { + event: payload.event, + streamId: payload.stream?.id, + status: payload.stream?.status, + }); await this.stageService.findStreamAndUpdate(payload.stream.id); break; + case LivepeerEvent.recordingReady: console.log( - 'Processing recording.ready for session:', - payload.payload.session.id, + '๐Ÿ“น Processing recording.ready for session:', + { + sessionId: payload.payload.session.id, + recordingUrl: payload.payload.session.recordingUrl, + duration: payload.payload.session.duration, + } ); await this.sessionService.createStreamRecordings( payload.payload.session, ); + console.log('โœ… Recording ready processing completed'); break; + default: + console.log('โš ๏ธ Unrecognized event:', payload.event); return SendApiResponse('Event not recognizable', null, '400'); } return SendApiResponse('OK'); } catch (error) { + console.error('โŒ Error processing webhook:', error); throw error; } } @@ -188,40 +257,64 @@ export class IndexController extends Controller { return SendApiResponse('Webhook processed successfully'); } - private async assetReady(id: string, asset: any) { - console.log('asset', asset); - const session = await this.sessionService.findOne({ assetId: id }); + private async assetReady(assetId: string, asset: any) { + console.log('๐ŸŽฌ Starting assetReady processing:', { + assetId, + assetSnapshot: asset, + }); + + try { + const session = await this.sessionService.findOne({ assetId }); + if (!session) { + console.log('โŒ No session found for assetId:', assetId); + throw new HttpException(404, 'No session found'); + } - if (!session) throw new HttpException(404, 'No session found'); + console.log('๐Ÿ“ Updating session with asset data:', { + sessionId: session._id, + type: session.type, + currentStatus: session.processingStatus, + }); - const thumbnail = await generateThumbnail({ - assetId: session.assetId, - playbackId: session.playbackId, - }); - let sessionParams = { - name: session.name, - start: session.start, - end: session.end, - organizationId: session.organizationId, - type: session.type, - videoUrl: asset.playbackUrl, - playbackId: asset.playbackId, - 'playback.videoUrl': asset.playbackUrl, - 'playback.format': asset.videoSpec?.format ?? '', - 'playback.duration': asset.videoSpec?.duration ?? 0, - processingStatus: ProcessingStatus.completed, - coverImage: session.coverImage ? session.coverImage : thumbnail, - }; - await this.sessionService.update(session._id.toString(), sessionParams); - - if ( - session.type !== SessionType.animation && - session.type !== SessionType.editorClip - ) { - await this.sessionService.sessionTranscriptions({ - organizationId: session.organizationId.toString(), - sessionId: session._id.toString(), + const thumbnail = await generateThumbnail({ + assetId: session.assetId, + playbackId: session.playbackId, }); + + console.log('๐Ÿ–ผ๏ธ Generated thumbnail:', thumbnail); + + let sessionParams = { + name: session.name, + start: session.start, + end: session.end, + organizationId: session.organizationId, + type: session.type, + videoUrl: asset.playbackUrl, + playbackId: asset.playbackId, + 'playback.videoUrl': asset.playbackUrl, + 'playback.format': asset.videoSpec?.format ?? '', + 'playback.duration': asset.videoSpec?.duration ?? 0, + processingStatus: ProcessingStatus.completed, + coverImage: session.coverImage ? session.coverImage : thumbnail, + }; + + console.log('๐Ÿ’พ Updating session with params:', sessionParams); + await this.sessionService.update(session._id.toString(), sessionParams); + console.log('โœ… Session updated successfully'); + + if ( + session.type !== SessionType.animation && + session.type !== SessionType.editorClip + ) { + console.log('๐ŸŽฏ Starting transcription for session:', session._id); + await this.sessionService.sessionTranscriptions({ + organizationId: session.organizationId.toString(), + sessionId: session._id.toString(), + }); + } + } catch (error) { + console.error('โŒ Error in assetReady:', error); + throw error; } } diff --git a/packages/server/src/databases/index.ts b/packages/server/src/databases/index.ts index 35d8f895a..f02514e48 100644 --- a/packages/server/src/databases/index.ts +++ b/packages/server/src/databases/index.ts @@ -8,9 +8,9 @@ console.log('Database:', name); console.log('Password length:', password?.length); export const dbConnection = { - url: `mongodb://${user}:${password}@${host}/${name}?authSource=admin&retryWrites=true&w=majority`, + // url: `mongodb://${user}:${password}@${host}/${name}?authSource=admin&retryWrites=true&w=majority`, // For local development use this url - // url: `mongodb+srv://${user}:${password}@${host}/${name}?authSource=admin`, + url: `mongodb+srv://${user}:${password}@${host}/${name}?authSource=admin`, options: { useNewUrlParser: true, useUnifiedTopology: true, diff --git a/packages/server/src/dtos/stream/create-clip.dto.ts b/packages/server/src/dtos/stream/create-clip.dto.ts index 2dfd88a19..663fbac3d 100644 --- a/packages/server/src/dtos/stream/create-clip.dto.ts +++ b/packages/server/src/dtos/stream/create-clip.dto.ts @@ -36,7 +36,11 @@ export class CreateClipDto implements IClip { @IsObject() editorOptions?: { frameRate: number; - events: Array<{ label: string; sessionId: string }>; + events: Array<{ + label: string; + sessionId?: string; + videoUrl?: string; + }>; selectedAspectRatio: string; captionEnabled: boolean; captionPosition: string; diff --git a/packages/server/src/interfaces/clip.editor.interface.ts b/packages/server/src/interfaces/clip.editor.interface.ts index aa2827c62..3fb30b820 100644 --- a/packages/server/src/interfaces/clip.editor.interface.ts +++ b/packages/server/src/interfaces/clip.editor.interface.ts @@ -14,7 +14,11 @@ export interface IClipEditor { organizationId: Types.ObjectId; stageId: Types.ObjectId; frameRate: number; - events: Array<{ label: string; sessionId: string }>; + events: Array<{ + label: string; + sessionId?: string; + videoUrl?: string; + }>; selectedAspectRatio: string; captionEnabled: boolean; captionPosition: string; diff --git a/packages/server/src/interfaces/clip.interface.ts b/packages/server/src/interfaces/clip.interface.ts index 601e50ac0..56ef94574 100644 --- a/packages/server/src/interfaces/clip.interface.ts +++ b/packages/server/src/interfaces/clip.interface.ts @@ -10,7 +10,8 @@ export interface IClip { frameRate: number; events: Array<{ label: string; - sessionId: string; + sessionId?: string; + videoUrl?: string; }>; selectedAspectRatio: string; captionEnabled: boolean; diff --git a/packages/server/src/models/clip.editor.model.ts b/packages/server/src/models/clip.editor.model.ts index 9c5aafb07..709e197cf 100644 --- a/packages/server/src/models/clip.editor.model.ts +++ b/packages/server/src/models/clip.editor.model.ts @@ -13,7 +13,8 @@ const ClipEditorSchema = new Schema( events: [ { label: { type: String, default: '' }, - sessionId: { type: Schema.Types.ObjectId, ref: 'Session' }, + sessionId: { type: Schema.Types.ObjectId, ref: 'Session', required: false }, + videoUrl: { type: String, required: false }, }, ], selectedAspectRatio: { type: String, default: '16:9' }, diff --git a/packages/server/src/routes/routes.ts b/packages/server/src/routes/routes.ts index dce8f9753..faef9728b 100644 --- a/packages/server/src/routes/routes.ts +++ b/packages/server/src/routes/routes.ts @@ -250,7 +250,7 @@ const models: TsoaRoute.Models = { "organizationId": {"dataType":"string","required":true}, "stageId": {"dataType":"string"}, "isEditorEnabled": {"dataType":"boolean"}, - "editorOptions": {"dataType":"nestedObjectLiteral","nestedProperties":{"captionColor":{"dataType":"string","required":true},"captionFont":{"dataType":"string","required":true},"captionLinesPerPage":{"dataType":"double","required":true},"captionPosition":{"dataType":"string","required":true},"captionEnabled":{"dataType":"boolean","required":true},"selectedAspectRatio":{"dataType":"string","required":true},"events":{"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"sessionId":{"dataType":"string","required":true},"label":{"dataType":"string","required":true}}},"required":true},"frameRate":{"dataType":"double","required":true}}}, + "editorOptions": {"dataType":"nestedObjectLiteral","nestedProperties":{"captionColor":{"dataType":"string","required":true},"captionFont":{"dataType":"string","required":true},"captionLinesPerPage":{"dataType":"double","required":true},"captionPosition":{"dataType":"string","required":true},"captionEnabled":{"dataType":"boolean","required":true},"selectedAspectRatio":{"dataType":"string","required":true},"events":{"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"videoUrl":{"dataType":"string"},"sessionId":{"dataType":"string"},"label":{"dataType":"string","required":true}}},"required":true},"frameRate":{"dataType":"double","required":true}}}, "clipSessionId": {"dataType":"string"}, "clipEditorId": {"dataType":"string"}, }, @@ -3573,9 +3573,9 @@ export function RegisterRoutes(app: Router,opts?:{multer?:ReturnType(IndexController)), - ...(fetchMiddlewares(IndexController.prototype.uploadImges)), + ...(fetchMiddlewares(IndexController.prototype.uploadFile)), - async function IndexController_uploadImges(request: ExRequest, response: ExResponse, next: any) { + async function IndexController_uploadFile(request: ExRequest, response: ExResponse, next: any) { const args: Record = { file: {"in":"formData","name":"file","required":true,"dataType":"file"}, directory: {"in":"formData","name":"directory","required":true,"dataType":"string"}, @@ -3590,7 +3590,7 @@ export function RegisterRoutes(app: Router,opts?:{multer?:ReturnType { console.log('e', e); }); - const events = data.editorOptions.events.filter((e) => e.sessionId !== ''); + const events = data.editorOptions.events; data.editorOptions.events = events; await ClipEditor.create({ ...data.editorOptions, @@ -67,10 +67,24 @@ export class ClipEditorService extends SessionService { } async launchRemotionRender(clipEditor: IClipEditor) { + console.log('๐Ÿ‘ clipEditor', clipEditor); // get all event session data based on event session ids + const sessionIds = clipEditor.events + .filter(e => e.sessionId) + .map(e => e.sessionId); + console.log('๐Ÿ‘ sessionIds', sessionIds); + + // Get all sessions in one query const eventSessions = await Session.find({ - _id: { $in: clipEditor.events.map((e) => e.sessionId) }, - }); + _id: { $in: sessionIds }, + }).lean(); + console.log('๐Ÿ‘ eventSessions', eventSessions); + + // Create a map for quick session lookup + const sessionsMap = eventSessions.reduce((acc, session) => { + acc[session._id.toString()] = session; + return acc; + }, {}); // check if all event sessions are completed const allEventSessionsCompleted = eventSessions.every( @@ -84,9 +98,19 @@ export class ClipEditorService extends SessionService { id: config.remotion.id, inputProps: { events: clipEditor.events.map((e) => { - const session = eventSessions.find( - (s) => s._id.toString() === e.sessionId.toString(), // Convert both to strings for comparison - ); + // If event has a direct videoUrl, use that + if (e.videoUrl) { + return { + id: e.label, + label: e.label, + type: 'media', + url: e.videoUrl, + }; + } + + // Otherwise, look up the session from our map + const session = e.sessionId ? sessionsMap[e.sessionId.toString()] : null; + console.log('๐Ÿ‘ session', session); if (!session?.source?.streamUrl) { console.log( @@ -98,12 +122,7 @@ export class ClipEditorService extends SessionService { id: e.label, label: e.label, type: 'media', - url: session?.source?.streamUrl, // Provide empty string as fallback - // transcript: { - // language: 'en', - // words: session?.transcripts.chunks, - // text: session?.transcripts.text, - // }, + url: session?.source?.streamUrl || '', // Always provide empty string as fallback }; }), captionLinesPerPage: clipEditor.captionLinesPerPage.toString(), @@ -155,4 +174,4 @@ const clipEditorService = new ClipEditorService( new StateService(), ); -export default clipEditorService; +export default clipEditorService; \ No newline at end of file diff --git a/packages/server/src/swagger/swagger.json b/packages/server/src/swagger/swagger.json index 606f487c8..ea622ac2d 100644 --- a/packages/server/src/swagger/swagger.json +++ b/packages/server/src/swagger/swagger.json @@ -488,6 +488,9 @@ "events": { "items": { "properties": { + "videoUrl": { + "type": "string" + }, "sessionId": { "type": "string" }, @@ -496,7 +499,6 @@ } }, "required": [ - "sessionId", "label" ], "type": "object" diff --git a/packages/server/src/utils/s3.ts b/packages/server/src/utils/s3.ts index 8a62c0f18..bebb96820 100644 --- a/packages/server/src/utils/s3.ts +++ b/packages/server/src/utils/s3.ts @@ -28,6 +28,12 @@ export default class StorageService { file: Buffer | Readable, contentType: string, ): Promise { + console.log('๐Ÿ”ง Configuring S3 upload:', { + bucket: name, + filename, + contentType, + }); + const params: PutObjectCommandInput = { Bucket: name, Key: filename, @@ -40,16 +46,22 @@ export default class StorageService { process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'staging' ) { + console.log('๐Ÿš€ Uploading to development bucket...'); const command = new PutObjectCommand(params); await this.s3Client.send(command); - return `https://streameth-develop.ams3.digitaloceanspaces.com/${filename}`; + const url = `https://streameth-develop.ams3.digitaloceanspaces.com/${filename}`; + console.log('โœ… Upload successful to development:', url); + return url; } else { + console.log('๐Ÿš€ Uploading to production bucket...'); const command = new PutObjectCommand(params); await this.s3Client.send(command); - return `https://streameth-production.ams3.digitaloceanspaces.com/${filename}`; + const url = `https://streameth-production.ams3.digitaloceanspaces.com/${filename}`; + console.log('โœ… Upload successful to production:', url); + return url; } } catch (error) { - console.log('Error uploading file to S3:', error); + console.error('โŒ S3 upload failed:', error); throw error; } } diff --git a/packages/server/src/utils/validateWebhook.ts b/packages/server/src/utils/validateWebhook.ts index 7f752f0b7..0ab3c6652 100644 --- a/packages/server/src/utils/validateWebhook.ts +++ b/packages/server/src/utils/validateWebhook.ts @@ -14,43 +14,86 @@ export function validateRemotionWebhook( return true; } -export function validateWebhook( - livepeerSignature: string, - payload: any, -): boolean { - const elements = livepeerSignature.split(','); - const signatureParts = elements.reduce((acc, element) => { - const [key, value] = element.split('='); - acc[key] = value; - return acc; - }, {}); - - const timestamp = signatureParts['t']; - const signature = signatureParts['v1']; - const signedPayload = JSON.stringify(payload); - - const expectedSignature = crypto - .createHmac('sha256', config.livepeer.webhookSecretKey) - .update(signedPayload) - .digest('hex'); +export const validateWebhook = (signature: string, payload: any): boolean => { + console.log('๐Ÿ” Starting webhook validation'); + try { + if (!signature) { + console.log('โŒ No signature provided'); + return false; + } - const isSignatureValid = crypto.timingSafeEqual( - Buffer.from(signature), - Buffer.from(expectedSignature), - ); + const secret = config.livepeer.webhookSecretKey; + console.log('๐Ÿ”‘ Using webhook secret:', { + secret, + configValue: config.livepeer.webhookSecretKey, + envValue: process.env.LIVEPEER_WEBHOOK_SECRET_FILE, + }); - if (!isSignatureValid) { - return false; - } + // Parse the signature header + const [timestamp, signatureHash] = signature.split(','); + const [, timestampValue] = timestamp.split('='); + const [, hashValue] = signatureHash.split('='); + + console.log('๐Ÿ“ Parsing signature components:', { + timestamp: timestampValue, + hash: hashValue, + }); + + // Check timestamp is within tolerance (1 hour to account for timezone differences) + const tolerance = 60 * 60 * 1000; // 1 hour in milliseconds + const now = Date.now(); + const timestampMs = parseInt(timestampValue); + + if (Math.abs(now - timestampMs) > tolerance) { + console.log('โฐ Timestamp out of tolerance:', { + now: new Date(now).toISOString(), + timestamp: new Date(timestampMs).toISOString(), + difference: Math.abs(now - timestampMs) / (60 * 1000), // difference in minutes + toleranceMinutes: tolerance / (60 * 1000), + }); + return false; + } + + // Construct the string to sign exactly as Livepeer does + const payloadString = JSON.stringify(payload); + console.log('๐Ÿ“œ Raw payload string:', payloadString); - const tolerance = 8 * 60 * 1000; // 8 minutes in milliseconds - const currentTime = Date.now(); // Current time in milliseconds - const isTimestampValid = - Math.abs(currentTime - parseInt(timestamp, 10)) < tolerance; + // Livepeer uses the raw timestamp value concatenated with the payload + const signaturePayload = timestampValue + '.' + payloadString; - if (!isTimestampValid) { + // Calculate expected signature using SHA-256 + const expectedSignature = crypto + .createHmac('sha256', secret) + .update(signaturePayload) + .digest('hex'); + + console.log('๐Ÿ” Comparing signatures:', { + received: hashValue, + expected: expectedSignature, + match: hashValue === expectedSignature, + timestampInfo: { + webhookTime: new Date(timestampMs).toISOString(), + serverTime: new Date(now).toISOString(), + diffMinutes: (now - timestampMs) / (60 * 1000) + } + }); + + const isValid = hashValue === expectedSignature; + console.log(isValid ? 'โœ… Signature valid' : 'โŒ Signature invalid'); + + if (!isValid) { + console.log('๐Ÿ” Debug info:', { + timestampValue, + payloadLength: payloadString.length, + signaturePayloadLength: signaturePayload.length, + secretLength: secret.length, + timezoneOffset: new Date().getTimezoneOffset() + }); + } + + return isValid; + } catch (error) { + console.error('โŒ Error validating webhook:', error); return false; } - - return true; -} +}; diff --git a/packages/server/workers/clips/index.ts b/packages/server/workers/clips/index.ts index e9532d97d..bf32ebf3c 100644 --- a/packages/server/workers/clips/index.ts +++ b/packages/server/workers/clips/index.ts @@ -22,12 +22,15 @@ interface Segment { } const consumer = async () => { + console.log('๐ŸŽฌ Starting clips worker consumer'); const queue = await clipsQueue(); queue.process(async (job) => { const data = job.data as IClip; + console.log(`๐Ÿ“‹ Processing clip job ${job.id} for session ${data.sessionId}`); try { return await processClip(data); } catch (error) { + console.error(`โŒ Clip processing failed for session ${data.sessionId}:`, error); await Session.findByIdAndUpdate(data.sessionId, { $set: { processingStatus: ProcessingStatus.failed, @@ -39,19 +42,31 @@ const consumer = async () => { }; const processClip = async (data: IClip) => { - console.log('Starting processClip with data:', data); + console.log('๐ŸŽฅ Starting clip processing:', { + sessionId: data.sessionId, + start: data.start, + end: data.end, + hasEditorOptions: !!data.editorOptions, + }); const { sessionId, clipUrl, start, end } = data; try { + console.log('๐Ÿ“ฅ Fetching master playlist from:', clipUrl); const masterResponse = await fetch(clipUrl); if (!masterResponse.ok) { + console.error('โŒ Failed to fetch master playlist:', { + status: masterResponse.status, + statusText: masterResponse.statusText, + }); throw new Error( `Failed to fetch master playlist: ${masterResponse.statusText}`, ); } const masterContent = await masterResponse.text(); + console.log('โœ… Successfully fetched master playlist'); // 2. Find the 1080p variant + console.log('๐Ÿ” Searching for 1080p variant in master playlist'); const linesMaster = masterContent.split('\n'); let variantUrl = ''; @@ -64,25 +79,43 @@ const processClip = async (data: IClip) => { variantUrl = clipUrl.replace('index.m3u8', variantUrl); if (!variantUrl) { + console.error('โŒ 1080p variant not found in master playlist'); throw new Error('1080p variant not found in master playlist'); } + console.log('โœ… Found 1080p variant URL:', variantUrl); const duration = end - start; - console.log('relativeStartSeconds', start, end, duration); + console.log('โฑ๏ธ Clip duration details:', { + start, + end, + duration, + }); + const tempDir = tmpdir(); const concatPath = join(tempDir, `${sessionId}-concat.mp4`); const outputPath = join(tempDir, `${sessionId}.mp4`); + console.log('๐Ÿ“ Temporary file paths:', { + concatPath, + outputPath, + }); // 1. Fetch and parse manifest + console.log('๐Ÿ“ฅ Fetching variant manifest'); const manifestResponse = await fetch(variantUrl); if (!manifestResponse.ok) { + console.error('โŒ Failed to fetch variant manifest:', { + status: manifestResponse.status, + statusText: manifestResponse.statusText, + }); throw new Error( `Failed to fetch manifest: ${manifestResponse.statusText}`, ); } const manifestContent = await manifestResponse.text(); + console.log('โœ… Successfully fetched variant manifest'); // 2. Parse segments and their durations + console.log('๐Ÿ” Parsing segments from manifest'); const segments: Segment[] = []; let cumulativeTime = 0; const lines = manifestContent.split('\n'); @@ -112,17 +145,16 @@ const processClip = async (data: IClip) => { } if (segments.length === 0) { + console.error('โŒ No segments found for specified time range:', { start, end }); throw new Error(`No segments found for time range ${start}-${end}`); } - console.log( - `Found ${segments.length} segments for clip from ${start}s to ${end}s`, - ); - console.log('First segment starts at:', segments[0].startTime); - console.log( - 'Last segment starts at:', - segments[segments.length - 1].startTime, - ); + console.log('๐Ÿ“Š Segment analysis:', { + totalSegments: segments.length, + clipDuration: duration, + firstSegmentStart: segments[0].startTime, + lastSegmentStart: segments[segments.length - 1].startTime, + }); // 3. Download segments in parallel with progress tracking const CONCURRENT_DOWNLOADS = 5; @@ -139,16 +171,29 @@ const processClip = async (data: IClip) => { const downloadedMB = downloadedBytes / (1024 * 1024); const totalMB = totalBytes / (1024 * 1024); - console.log( - `Download Progress: ${completedDownloads}/${segments.length} segments ` + - `(${percentComplete.toFixed(1)}%) - ${downloadedMB.toFixed(1)}MB/${totalMB.toFixed(1)}MB`, - ); + console.log('๐Ÿ“ฅ Download progress:', { + completedSegments: completedDownloads, + totalSegments: segments.length, + percentComplete: `${percentComplete.toFixed(1)}%`, + downloadedSize: `${downloadedMB.toFixed(1)}MB`, + totalSize: `${totalMB.toFixed(1)}MB`, + }); }; const downloadSegment = async (segment: Segment, index: number) => { const segmentPath = join(tempDir, `segment_${index}.ts`); + console.log(`๐Ÿ“ฅ Downloading segment ${index}:`, { + startTime: segment.startTime, + duration: segment.duration, + }); + const response = await fetch(segment.url); if (!response.ok) { + console.error(`โŒ Failed to download segment ${index}:`, { + startTime: segment.startTime, + status: response.status, + statusText: response.statusText, + }); throw new Error( `Failed to download segment at ${segment.startTime}s: ${response.statusText}`, ); @@ -193,9 +238,10 @@ const processClip = async (data: IClip) => { }; try { - console.log( - `Starting download of ${segments.length} segments with ${CONCURRENT_DOWNLOADS} concurrent downloads`, - ); + console.log('๐Ÿš€ Starting parallel segment downloads:', { + totalSegments: segments.length, + concurrentDownloads: CONCURRENT_DOWNLOADS, + }); const startTime = Date.now(); segmentPaths.splice( @@ -210,11 +256,13 @@ const processClip = async (data: IClip) => { const duration = (Date.now() - startTime) / 1000; const speed = downloadedBytes / (1024 * 1024) / duration; // MB/s - console.log( - `Downloads completed in ${duration.toFixed(1)}s ` + - `(${speed.toFixed(1)} MB/s average)`, - ); + console.log('โœ… Segment downloads completed:', { + duration: `${duration.toFixed(1)}s`, + averageSpeed: `${speed.toFixed(1)} MB/s`, + totalSize: `${(downloadedBytes / (1024 * 1024)).toFixed(1)}MB`, + }); } catch (error) { + console.error('โŒ Error during segment downloads:', error); // Clean up any partially downloaded segments await Promise.all( segmentPaths @@ -225,28 +273,41 @@ const processClip = async (data: IClip) => { } // 4. Create concat file + console.log('๐Ÿ“ Creating concat file'); const concatFilePath = join(tempDir, `${sessionId}-concat.txt`); const concatContent = segmentPaths .map((path) => `file '${path}'`) .join('\n'); await fs.promises.writeFile(concatFilePath, concatContent); + console.log('โœ… Concat file created:', concatFilePath); // 5. Calculate precise offset from first segment const offsetInFirstSegment = start - segments[0].startTime; + console.log('โšก Calculated clip parameters:', { + offsetInFirstSegment, + totalDuration: duration, + }); return new Promise((resolve, reject) => { + console.log('๐ŸŽฌ Starting FFmpeg concatenation'); // First concatenate required segments ffmpeg() .input(concatFilePath) .inputOptions(['-f', 'concat', '-safe', '0']) .outputOptions(['-c', 'copy']) .output(concatPath) + .on('start', (command) => { + console.log('๐ŸŽฅ FFmpeg concat command:', command); + }) .on('end', () => { + console.log('โœ… FFmpeg concatenation completed'); // Clean up concat file and segment files fs.unlinkSync(concatFilePath); segmentPaths.forEach((path) => fs.unlinkSync(path)); + console.log('๐Ÿงน Cleaned up temporary segment files'); + console.log('๐ŸŽฌ Starting FFmpeg trim operation'); // Then trim the exact portion we need ffmpeg() .input(concatPath) @@ -254,23 +315,27 @@ const processClip = async (data: IClip) => { .outputOptions(['-c copy', '-f mp4', '-movflags +faststart']) .duration(duration) .output(outputPath) + .on('start', (command) => { + console.log('๐ŸŽฅ FFmpeg trim command:', command); + }) .on('end', async () => { try { fs.unlinkSync(concatPath); + console.log('๐Ÿงน Cleaned up concatenated file'); - console.log('Clip creation finished'); + console.log('โœ… Clip creation finished'); const storageService = new StorageService(); const fileBuffer = await fs.promises.readFile(outputPath); - console.log('uploading file to s3'); + console.log('๐Ÿ“ค Uploading clip to S3'); const url = await storageService.uploadFile( 'clips/' + sessionId, fileBuffer, 'video/mp4', ); - console.log('Clip uploaded to:', url); + console.log('โœ… Clip uploaded to S3:', url); if (data.editorOptions) { - console.log('editorOptions', data.editorOptions); + console.log('๐ŸŽจ Processing editor options:', data.editorOptions); // Make sure we're passing a plain object for the update const updateData = { source: { @@ -281,6 +346,7 @@ const processClip = async (data: IClip) => { processingStatus: ProcessingStatus.clipCreated, }; + console.log('๐Ÿ’พ Updating session with clip data'); const session = await Session.findByIdAndUpdate( sessionId, { $set: updateData }, @@ -288,43 +354,55 @@ const processClip = async (data: IClip) => { ); if (!session) { + console.error('โŒ Session not found:', sessionId); throw new Error(`Session not found: ${sessionId}`); } + + console.log('๐Ÿ—ฟ data.editorOptions', data.editorOptions); if (data.editorOptions.captionEnabled) { + console.log('๐ŸŽฏ Starting audio transcription'); await transcribeAudioSession(url, session); + console.log('โœ… Audio transcription completed'); } + console.log('๐Ÿ—ฟ sessionId', sessionId); + console.log('๐ŸŽจ Finding clip editor configuration'); const clipEditor = await ClipEditor.findOne({ clipSessionId: sessionId, }); - console.log('clipEditor', clipEditor); + console.log('๐Ÿ—ฟ clipEditor', clipEditor); + console.log('๐ŸŽฌ Launching Remotion render'); await clipEditorService.launchRemotionRender(clipEditor); - fs.unlinkSync(outputPath); - resolve(true); + console.log('โœ… Remotion render launched'); } else { - console.log('creating asset from url'); + console.log('๐Ÿ“ค Creating Livepeer asset'); const assetId = await createAssetFromUrl(sessionId, url); - console.log('assetId', assetId); + console.log('โœ… Livepeer asset created:', assetId); - // Make sure we're passing a plain object for the update + console.log('๐Ÿ’พ Updating session with asset ID'); await Session.findByIdAndUpdate(sessionId, { $set: { assetId, + processingStatus: ProcessingStatus.completed, }, }); - fs.unlinkSync(outputPath); - resolve(true); + console.log('โœ… Session updated with asset ID'); } + + fs.unlinkSync(outputPath); + console.log('๐Ÿงน Cleaned up output file'); + console.log('โœ… Clip processing completed successfully'); + resolve(true); } catch (error) { - console.error('Error creating clip:', error); + console.error('โŒ Error in final processing steps:', error); reject(error); } }) .on('progress', (progress) => { - console.log('trimming progress', progress); + console.log('๐ŸŽฅ FFmpeg trim progress:', progress); }) .on('error', async (err) => { - console.error('Error trimming clip:', err); + console.error('โŒ Error during FFmpeg trim:', err); fs.unlinkSync(concatPath); await Session.findByIdAndUpdate(sessionId, { $set: { @@ -336,10 +414,10 @@ const processClip = async (data: IClip) => { .run(); }) .on('progress', (progress) => { - console.log('download progress', progress); + console.log('๐ŸŽฅ FFmpeg concat progress:', progress); }) .on('error', async (err) => { - console.error('Error downloading segments:', err); + console.error('โŒ Error during FFmpeg concat:', err); await Session.findByIdAndUpdate(sessionId, { $set: { processingStatus: ProcessingStatus.failed, @@ -350,7 +428,7 @@ const processClip = async (data: IClip) => { .run(); }); } catch (error) { - console.error('Error processing clip:', error); + console.error('โŒ Fatal error in clip processing:', error); await Session.findByIdAndUpdate(sessionId, { $set: { processingStatus: ProcessingStatus.failed, @@ -362,14 +440,16 @@ const processClip = async (data: IClip) => { const init = async () => { try { + console.log('๐Ÿš€ Initializing clips worker'); await connect(dbConnection.url, { serverSelectionTimeoutMS: 5000, }); + console.log('โœ… Connected to database'); await consumer(); + console.log('โœ… Clips worker initialized successfully'); } catch (err) { - console.error('Worker initialization failed with error:', err); - console.error('Error details:', { + console.error('โŒ Worker initialization failed:', { name: err.name, message: err.message, stack: err.stack, @@ -380,17 +460,17 @@ const init = async () => { // Add more detailed error handlers process.on('unhandledRejection', (error) => { - console.error('Unhandled rejection:', error); + console.error('โŒ Unhandled rejection in clips worker:', error); process.exit(1); }); process.on('uncaughtException', (error) => { - console.error('Uncaught exception:', error); + console.error('โŒ Uncaught exception in clips worker:', error); process.exit(1); }); init().catch((error) => { - console.error('Fatal error during initialization:', { + console.error('โŒ Fatal error during clips worker initialization:', { name: error.name, message: error.message, stack: error.stack, diff --git a/packages/video-uploader/src/config/index.ts b/packages/video-uploader/src/config/index.ts index 3ab82f92b..7176e1891 100644 --- a/packages/video-uploader/src/config/index.ts +++ b/packages/video-uploader/src/config/index.ts @@ -41,10 +41,10 @@ export const config = { remotion: { id: process.env.REMOTION_ID, host: process.env.REMOTION_BASE_URL, - webhookSecretKey: process.env.REMOTION_WEBHOOK_SECRET_FILE, + webhookSecretKey: readSecretFile(process.env.REMOTION_WEBHOOK_SECRET_FILE), webhook: { url: process.env.REMOTION_WEBHOOK_URL, - secret: process.env.REMOTION_WEBHOOK_SECRET_FILE, + secret: readSecretFile(process.env.REMOTION_WEBHOOK_SECRET_FILE), } }, redis: { From e7a5749f452fa0f1238c7ae56546ebaefa793101 Mon Sep 17 00:00:00 2001 From: Mario-SO Date: Tue, 7 Jan 2025 10:30:46 +0100 Subject: [PATCH 07/11] =?UTF-8?q?=F0=9F=90=9B=20fixed=20combobox=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/src/services/session.service.ts | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/server/src/services/session.service.ts b/packages/server/src/services/session.service.ts index c2d8c6d72..0a767d182 100644 --- a/packages/server/src/services/session.service.ts +++ b/packages/server/src/services/session.service.ts @@ -104,13 +104,20 @@ export default class SessionService { limit: number; }; }> { - let filter: {} = { - type: { $nin: [SessionType.animation, SessionType.editorClip] }, - }; + console.log('getAll called with type:', d.type); + let filter: {} = {}; - if (d.type !== undefined) { - filter = { ...filter, type: d.type }; + // Only exclude animations and editor clips if no specific type is requested + if (d.type === undefined) { + console.log('No type specified, excluding animations and editor clips'); + filter = { type: { $nin: [SessionType.animation, SessionType.editorClip] } }; + } else { + console.log('Type specified:', d.type); + filter = { type: d.type }; } + + console.log('Initial filter:', JSON.stringify(filter, null, 2)); + if (d.published != undefined) { filter = { ...filter, published: d.published }; } @@ -147,7 +154,11 @@ export default class SessionService { if (d.onlyVideos) { filter = { ...filter, - $or: [{ playbackId: { $ne: '' } }, { assetId: { $ne: '' } }], + $or: [ + { playbackId: { $ne: '' } }, + { assetId: { $ne: '' } }, + { type: SessionType.animation } + ], }; } if (d.assetId != undefined) { @@ -159,6 +170,8 @@ export default class SessionService { filter = { ...filter, stageId: stage?._id }; } + console.log('Final filter:', JSON.stringify(filter, null, 2)); + const pageSize = Number(d.size) || 0; const pageNumber = Number(d.page) || 0; const skip = pageSize * pageNumber - pageSize; @@ -171,6 +184,9 @@ export default class SessionService { pageSize, ); + console.log('Found sessions count:', sessions.length); + console.log('Session types:', sessions.map(s => s.type)); + const totalItems = await this.controller.store.countDocuments(filter); return { sessions: sessions, From 8dca2e4d576db76801e18b2c745845561efd8d5e Mon Sep 17 00:00:00 2001 From: Mario-SO Date: Tue, 7 Jan 2025 10:36:17 +0100 Subject: [PATCH 08/11] =?UTF-8?q?=F0=9F=94=A5=20Cleaned=20console=20logs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/server/src/services/session.service.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/server/src/services/session.service.ts b/packages/server/src/services/session.service.ts index 0a767d182..656452a6d 100644 --- a/packages/server/src/services/session.service.ts +++ b/packages/server/src/services/session.service.ts @@ -104,7 +104,6 @@ export default class SessionService { limit: number; }; }> { - console.log('getAll called with type:', d.type); let filter: {} = {}; // Only exclude animations and editor clips if no specific type is requested @@ -116,8 +115,6 @@ export default class SessionService { filter = { type: d.type }; } - console.log('Initial filter:', JSON.stringify(filter, null, 2)); - if (d.published != undefined) { filter = { ...filter, published: d.published }; } @@ -170,8 +167,6 @@ export default class SessionService { filter = { ...filter, stageId: stage?._id }; } - console.log('Final filter:', JSON.stringify(filter, null, 2)); - const pageSize = Number(d.size) || 0; const pageNumber = Number(d.page) || 0; const skip = pageSize * pageNumber - pageSize; @@ -184,9 +179,6 @@ export default class SessionService { pageSize, ); - console.log('Found sessions count:', sessions.length); - console.log('Session types:', sessions.map(s => s.type)); - const totalItems = await this.controller.store.countDocuments(filter); return { sessions: sessions, From 33bc4b750413ff2093fd9fb1cde151b2705e2a7a Mon Sep 17 00:00:00 2001 From: Mario-SO Date: Tue, 7 Jan 2025 10:58:08 +0100 Subject: [PATCH 09/11] =?UTF-8?q?=F0=9F=9A=91=20Reverted=20database=20conn?= =?UTF-8?q?ection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/server/src/databases/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/databases/index.ts b/packages/server/src/databases/index.ts index f02514e48..35d8f895a 100644 --- a/packages/server/src/databases/index.ts +++ b/packages/server/src/databases/index.ts @@ -8,9 +8,9 @@ console.log('Database:', name); console.log('Password length:', password?.length); export const dbConnection = { - // url: `mongodb://${user}:${password}@${host}/${name}?authSource=admin&retryWrites=true&w=majority`, + url: `mongodb://${user}:${password}@${host}/${name}?authSource=admin&retryWrites=true&w=majority`, // For local development use this url - url: `mongodb+srv://${user}:${password}@${host}/${name}?authSource=admin`, + // url: `mongodb+srv://${user}:${password}@${host}/${name}?authSource=admin`, options: { useNewUrlParser: true, useUnifiedTopology: true, From 5bae3e02aa93c0d763454e7cfc60feda964ecca5 Mon Sep 17 00:00:00 2001 From: Mario-SO Date: Tue, 7 Jan 2025 17:10:07 +0100 Subject: [PATCH 10/11] =?UTF-8?q?=F0=9F=94=A5=20Removed=20logs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/server/src/services/clipEditor.service.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/server/src/services/clipEditor.service.ts b/packages/server/src/services/clipEditor.service.ts index 90327fdee..b5baa85fe 100644 --- a/packages/server/src/services/clipEditor.service.ts +++ b/packages/server/src/services/clipEditor.service.ts @@ -67,18 +67,15 @@ export class ClipEditorService extends SessionService { } async launchRemotionRender(clipEditor: IClipEditor) { - console.log('๐Ÿ‘ clipEditor', clipEditor); // get all event session data based on event session ids const sessionIds = clipEditor.events .filter(e => e.sessionId) .map(e => e.sessionId); - console.log('๐Ÿ‘ sessionIds', sessionIds); // Get all sessions in one query const eventSessions = await Session.find({ _id: { $in: sessionIds }, }).lean(); - console.log('๐Ÿ‘ eventSessions', eventSessions); // Create a map for quick session lookup const sessionsMap = eventSessions.reduce((acc, session) => { @@ -110,7 +107,6 @@ export class ClipEditorService extends SessionService { // Otherwise, look up the session from our map const session = e.sessionId ? sessionsMap[e.sessionId.toString()] : null; - console.log('๐Ÿ‘ session', session); if (!session?.source?.streamUrl) { console.log( From c4cc66f755da9ced747c1d59d5785af451fa69e6 Mon Sep 17 00:00:00 2001 From: Mario-SO Date: Tue, 7 Jan 2025 17:10:30 +0100 Subject: [PATCH 11/11] =?UTF-8?q?=E2=9C=A8=20Rendering=20state=20tracking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clips/[stageId]/sidebar/clips/Clip.tsx | 14 ++++++++++---- .../server/src/controllers/index.controller.ts | 1 + packages/server/workers/clips/index.ts | 6 ++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/app/app/studio/[organization]/(no-side-bar)/clips/[stageId]/sidebar/clips/Clip.tsx b/packages/app/app/studio/[organization]/(no-side-bar)/clips/[stageId]/sidebar/clips/Clip.tsx index eae42ba8a..e3a5639bf 100644 --- a/packages/app/app/studio/[organization]/(no-side-bar)/clips/[stageId]/sidebar/clips/Clip.tsx +++ b/packages/app/app/studio/[organization]/(no-side-bar)/clips/[stageId]/sidebar/clips/Clip.tsx @@ -35,21 +35,25 @@ export default function Clip({ session }: { session: IExtendedSession }) { // getAsset(); // }, 10000); // return () => clearInterval(interval); - // } else if (session.processingStatus === ProcessingStatus.pending) { + // } else if ( + // session.processingStatus === ProcessingStatus.pending || + // session.processingStatus === ProcessingStatus.rendering + // ) { // const interval = setInterval(() => { // router.refresh(); // }, 10000); // return () => clearInterval(interval); // } // // eslint-disable-next-line react-hooks/exhaustive-deps - // }, [asset?.status?.phase]); + // }, [asset?.status?.phase, session.processingStatus]); // if (!assetId) return null; return ( <>
{asset?.status?.phase === 'processing' || - session.processingStatus === ProcessingStatus.pending ? ( + session.processingStatus === ProcessingStatus.pending || + session.processingStatus === ProcessingStatus.rendering ? (
@@ -58,7 +62,9 @@ export default function Clip({ session }: { session: IExtendedSession }) {

{name}

- Video is processing... + {session.processingStatus === ProcessingStatus.rendering + ? 'Video is rendering...' + : 'Video is processing...'}

{asset?.status?.phase === 'processing' && ( diff --git a/packages/server/src/controllers/index.controller.ts b/packages/server/src/controllers/index.controller.ts index e72210742..03021104c 100644 --- a/packages/server/src/controllers/index.controller.ts +++ b/packages/server/src/controllers/index.controller.ts @@ -249,6 +249,7 @@ export class IndexController extends Controller { end: session.end, organizationId: session.organizationId, type: session.type, + processingStatus: ProcessingStatus.completed, }), clipEditor.updateOne({ status: ClipEditorStatus.uploading, diff --git a/packages/server/workers/clips/index.ts b/packages/server/workers/clips/index.ts index bf32ebf3c..c1fcef02e 100644 --- a/packages/server/workers/clips/index.ts +++ b/packages/server/workers/clips/index.ts @@ -374,6 +374,12 @@ const processClip = async (data: IClip) => { console.log('๐ŸŽฌ Launching Remotion render'); await clipEditorService.launchRemotionRender(clipEditor); console.log('โœ… Remotion render launched'); + + // Update to rendering state + await Session.findByIdAndUpdate(sessionId, + { $set: { processingStatus: ProcessingStatus.rendering } }, + { new: true } + ); } else { console.log('๐Ÿ“ค Creating Livepeer asset'); const assetId = await createAssetFromUrl(sessionId, url);