mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
MediaRecorder gives us WebM/Opus, and that is exactly what we stored and served back. Browsers and desktop players read it, but no editing suite does: DaVinci Resolve, Premiere and Final Cut all refuse the container outright, so a voice note downloaded byte-for-byte was useless to the editor it was recorded for. The browser already decodes these formats in order to play them, so the conversion costs nothing but a RIFF header. lib/audio-to-wav.ts decodes through an OfflineAudioContext and writes interleaved 16-bit PCM. This runs at download time rather than at record time, so the stored object stays the small Opus file, uploads keep their 10MB limit, and self-hosted installs gain no server-side ffmpeg dependency. Voice comments had no download control at all, only a play button, so reviewers were saving files straight off the audio element and getting a bare UUID. They now get a download button on both comments and replies, named after the reviewer and the frame they were talking about, gated on the same download permission as the video and asset downloads. Audio assets get a WAV / Original menu. Files already in an editable container (wav, mp3, m4a) are handed over untouched: audio assets are not only recordings, and decoding an uploaded master back out would resample it to 48 kHz and requantise it to 16 bit for no gain. When a browser cannot decode the stored format at all, the original is saved and the user is told.
257 lines
6.3 KiB
TypeScript
257 lines
6.3 KiB
TypeScript
export interface Version {
|
|
id: string;
|
|
versionNumber: number;
|
|
versionLabel: string | null;
|
|
providerId: string;
|
|
videoId: string;
|
|
originalUrl: string;
|
|
title: string | null;
|
|
thumbnailUrl: string | null;
|
|
duration: number | null;
|
|
isActive: boolean;
|
|
_count: { comments: number };
|
|
}
|
|
|
|
export interface CommentTag {
|
|
id: string;
|
|
name: string;
|
|
color: string;
|
|
}
|
|
|
|
export interface VideoAsset {
|
|
id: string;
|
|
videoId: string;
|
|
kind: 'IMAGE' | 'VIDEO' | 'AUDIO';
|
|
provider: 'R2_IMAGE' | 'YOUTUBE' | 'BUNNY' | 'R2_AUDIO' | 'R2_VIDEO';
|
|
displayName: string;
|
|
sourceUrl: string | null;
|
|
providerVideoId: string | null;
|
|
thumbnailUrl: string | null;
|
|
uploadedByUserId: string | null;
|
|
uploadedByGuestName: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
uploadedByUser: { id: string; name: string | null; image: string | null } | null;
|
|
canDelete: boolean;
|
|
}
|
|
|
|
/**
|
|
* What the player's CC menu needs to know about one track. Our own uploaded tracks and
|
|
* the ones a YouTube video brings with it are different things underneath, and the menu
|
|
* is the one place that does not have to care.
|
|
*/
|
|
export interface SubtitleTrackOption {
|
|
id: string;
|
|
language: string;
|
|
label: string;
|
|
canDelete: boolean;
|
|
}
|
|
|
|
export interface Subtitle extends SubtitleTrackOption {
|
|
versionId: string;
|
|
url: string;
|
|
sizeBytes: number;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
uploadedByUser: { id: string; name: string | null; image: string | null } | null;
|
|
}
|
|
|
|
export interface ApprovalDecision {
|
|
id: string;
|
|
approverId: string;
|
|
status: 'PENDING' | 'APPROVED' | 'REJECTED';
|
|
note: string | null;
|
|
respondedAt: string | null;
|
|
createdAt: string;
|
|
approver: {
|
|
id: string;
|
|
name: string | null;
|
|
email: string | null;
|
|
image: string | null;
|
|
};
|
|
}
|
|
|
|
export interface ApprovalRequest {
|
|
id: string;
|
|
status: 'PENDING' | 'APPROVED' | 'REJECTED' | 'CANCELED';
|
|
requestedById: string;
|
|
message: string | null;
|
|
resolvedAt: string | null;
|
|
canceledAt: string | null;
|
|
canceledById: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
requestedBy: {
|
|
id: string;
|
|
name: string | null;
|
|
email: string | null;
|
|
image: string | null;
|
|
};
|
|
canceledBy: {
|
|
id: string;
|
|
name: string | null;
|
|
email: string | null;
|
|
image: string | null;
|
|
} | null;
|
|
decisions: ApprovalDecision[];
|
|
}
|
|
|
|
export interface CommentImage {
|
|
id: string;
|
|
url: string;
|
|
}
|
|
|
|
export interface CommentReply {
|
|
id: string;
|
|
content: string | null;
|
|
timestamp: number;
|
|
timestampEnd: number | null;
|
|
voiceUrl: string | null;
|
|
voiceDuration: number | null;
|
|
images: CommentImage[];
|
|
annotationData: string | null;
|
|
createdAt: string;
|
|
author: { id: string; name: string | null; image: string | null } | null;
|
|
guestName: string | null;
|
|
canEdit?: boolean;
|
|
canDelete?: boolean;
|
|
tag: CommentTag | null;
|
|
}
|
|
|
|
export interface Comment {
|
|
id: string;
|
|
content: string | null;
|
|
timestamp: number;
|
|
timestampEnd: number | null;
|
|
voiceUrl: string | null;
|
|
voiceDuration: number | null;
|
|
images: CommentImage[];
|
|
annotationData: string | null;
|
|
isResolved: boolean;
|
|
createdAt: string;
|
|
author: { id: string; name: string | null; image: string | null } | null;
|
|
guestName: string | null;
|
|
canEdit?: boolean;
|
|
canDelete?: boolean;
|
|
tag: CommentTag | null;
|
|
replies: CommentReply[];
|
|
}
|
|
|
|
export interface VideoData {
|
|
id: string;
|
|
title: string;
|
|
description: string | null;
|
|
projectId: string;
|
|
project: {
|
|
name: string;
|
|
ownerId: string;
|
|
members?: { role: string }[];
|
|
visibility?: string;
|
|
};
|
|
versions: (Version & { comments: Comment[] })[];
|
|
isAuthenticated: boolean;
|
|
currentUserId: string | null;
|
|
currentUserName: string | null;
|
|
canComment?: boolean;
|
|
canDownload?: boolean;
|
|
canManageTags?: boolean;
|
|
canResolveComments?: boolean;
|
|
canRequestApproval?: boolean;
|
|
canShareVideo?: boolean;
|
|
canUploadAssets?: boolean;
|
|
canDownloadAssets?: boolean;
|
|
}
|
|
|
|
export interface BunnyQualityOption {
|
|
level: number;
|
|
label: string;
|
|
}
|
|
|
|
export type BunnyPlaybackState = 'none' | 'processing' | 'error';
|
|
export type BunnyDownloadPreference = 'original' | 'compressed';
|
|
export type DownloadTarget = BunnyDownloadPreference | 'direct';
|
|
/** Voice notes add one more option: converted to WAV in the browser on the way out. */
|
|
export type AssetDownloadPreference = BunnyDownloadPreference | 'wav';
|
|
|
|
export interface CommentMarker {
|
|
id: string;
|
|
timestamp: number;
|
|
timestampEnd: number | null;
|
|
color: string;
|
|
annotationData: string | null;
|
|
preview: string;
|
|
}
|
|
|
|
export interface PlayerAdapter {
|
|
playVideo: () => void;
|
|
pauseVideo: () => void;
|
|
seekTo: (time: number, allowSeekAhead?: boolean) => void;
|
|
mute: () => void;
|
|
unMute: () => void;
|
|
isMuted: () => boolean;
|
|
getCurrentTime: () => number;
|
|
getDuration: () => number;
|
|
getPlayerState: () => number;
|
|
setPlaybackRate: (rate: number) => void;
|
|
destroy: () => void;
|
|
off?: (event: string) => void;
|
|
}
|
|
|
|
export interface WatchProgressConfig {
|
|
videoId: string;
|
|
activeVersionId: string | null;
|
|
isAuthenticated: boolean;
|
|
pathname: string;
|
|
}
|
|
|
|
export interface WatchProgressState {
|
|
savedProgress: number | null;
|
|
showResumePrompt: boolean;
|
|
}
|
|
|
|
export interface CommentActionsConfig {
|
|
videoId: string;
|
|
}
|
|
|
|
export type DirectUploadProvider = 'bunny' | 'r2';
|
|
|
|
export interface VersionActionsConfig {
|
|
projectId?: string;
|
|
videoId: string;
|
|
directUploadsEnabled?: boolean;
|
|
directUploadProvider?: DirectUploadProvider;
|
|
}
|
|
|
|
export interface VideoPageHeaderActions {
|
|
onVersionSelect: (versionId: string) => void;
|
|
onDeleteCurrentVersionClick: () => void;
|
|
onDownload: (preference?: BunnyDownloadPreference) => void;
|
|
onOpenCompare: () => void;
|
|
onCreateVersion: () => void;
|
|
}
|
|
|
|
export interface VideoPageCommentsActions {
|
|
onExportComments: (format: 'csv' | 'pdf') => void;
|
|
onResolveComment: (commentId: string, currentlyResolved: boolean) => void;
|
|
onEditComment: (commentId: string) => void;
|
|
onDeleteComment: (commentId: string) => void;
|
|
onReplyComment: (
|
|
parentId: string,
|
|
voiceData?: { url: string; duration: number },
|
|
imageUrls?: string[]
|
|
) => void;
|
|
onSubmitReplyWithMedia: (parentId: string) => void;
|
|
onStartEditAnnotation: () => void;
|
|
}
|
|
|
|
export interface VideoPageComposerActions {
|
|
onSubmitCommentWithMedia: () => void;
|
|
onAddComment: () => void;
|
|
onPauseVideoForAnnotation: () => void;
|
|
}
|
|
|
|
export interface VideoPageCompareActions {
|
|
onToggleVersion: (versionId: string) => void;
|
|
onCompare: () => void;
|
|
}
|