diff --git a/app/routes/dashboard/-layout.tsx b/app/routes/dashboard/-layout.tsx
index 7af49d4..e73abe7 100644
--- a/app/routes/dashboard/-layout.tsx
+++ b/app/routes/dashboard/-layout.tsx
@@ -32,15 +32,21 @@ import { prewarmTeam } from "./-team.data";
import { useVideoUploadManager } from "./-useVideoUploadManager";
import { DashboardUploadProvider } from "@/lib/dashboardUploadContext";
-const VIDEO_FILE_EXTENSIONS = /\.(mp4|mov|m4v|webm|avi|mkv)$/i;
+const SUPPORTED_FILE_EXTENSIONS = /\.(mp4|mov|m4v|webm|avi|mkv|docx|wav|aup3)$/i;
-function isVideoFile(file: File) {
- return file.type.startsWith("video/") || VIDEO_FILE_EXTENSIONS.test(file.name);
+function isSupportedFile(file: File) {
+ return (
+ file.type.startsWith("video/") ||
+ file.type.startsWith("audio/") ||
+ file.type === "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ||
+ file.type === "application/octet-stream" ||
+ SUPPORTED_FILE_EXTENSIONS.test(file.name)
+ );
}
-function getVideoFiles(files: FileList | null) {
+function getSupportedFiles(files: FileList | null) {
if (!files) return [];
- return Array.from(files).filter(isVideoFile);
+ return Array.from(files).filter(isSupportedFile);
}
function dragEventHasFiles(event: DragEvent) {
@@ -90,7 +96,7 @@ export default function DashboardLayout() {
const requestUpload = useCallback(
(inputFiles: File[], preferredProjectId?: Id<"projects">) => {
- const files = inputFiles.filter(isVideoFile);
+ const files = inputFiles.filter(isSupportedFile);
if (files.length === 0) return;
if (preferredProjectId) {
@@ -170,7 +176,7 @@ export default function DashboardLayout() {
dragDepthRef.current = 0;
setIsGlobalDragActive(false);
- const files = getVideoFiles(event.dataTransfer?.files ?? null);
+ const files = getSupportedFiles(event.dataTransfer?.files ?? null);
if (files.length === 0) return;
requestUpload(files);
};
@@ -249,7 +255,7 @@ export default function DashboardLayout() {
- Drop videos to upload
+ Drop files to upload
@@ -278,7 +284,7 @@ export default function DashboardLayout() {
Choose a project
- {pendingFiles?.length ? `Upload ${pendingFiles.length} video${pendingFiles.length > 1 ? "s" : ""} to:` : "Pick a project to start uploading."}
+ {pendingFiles?.length ? `Upload ${pendingFiles.length} file${pendingFiles.length > 1 ? "s" : ""} to:` : "Pick a project to start uploading."}
{uploadTargets === undefined ? (
diff --git a/app/routes/dashboard/-useVideoUploadManager.ts b/app/routes/dashboard/-useVideoUploadManager.ts
index cfb760d..4e5dfdf 100644
--- a/app/routes/dashboard/-useVideoUploadManager.ts
+++ b/app/routes/dashboard/-useVideoUploadManager.ts
@@ -57,7 +57,7 @@ export function useVideoUploadManager() {
projectId,
title,
fileSize: file.size,
- contentType: file.type || "video/mp4",
+ contentType: file.type || "application/octet-stream",
});
setUploads((prev) =>
@@ -72,7 +72,7 @@ export function useVideoUploadManager() {
videoId: createdVideoId,
filename: file.name,
fileSize: file.size,
- contentType: file.type || "video/mp4",
+ contentType: file.type || "application/octet-stream",
});
await new Promise((resolve, reject) => {
@@ -100,7 +100,7 @@ export function useVideoUploadManager() {
const avgSpeed =
recentSpeeds.length > 0
? recentSpeeds.reduce((sum, speed) => sum + speed, 0) /
- recentSpeeds.length
+ recentSpeeds.length
: 0;
const remaining = event.total - event.loaded;
const eta = avgSpeed > 0 ? Math.ceil(remaining / avgSpeed) : null;
@@ -109,11 +109,11 @@ export function useVideoUploadManager() {
prev.map((upload) =>
upload.id === uploadId
? {
- ...upload,
- progress: percentage,
- bytesPerSecond: avgSpeed,
- estimatedSecondsRemaining: eta,
- }
+ ...upload,
+ progress: percentage,
+ bytesPerSecond: avgSpeed,
+ estimatedSecondsRemaining: eta,
+ }
: upload,
),
);
@@ -140,7 +140,7 @@ export function useVideoUploadManager() {
});
xhr.open("PUT", url);
- xhr.setRequestHeader("Content-Type", file.type || "video/mp4");
+ xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
xhr.send(file);
});
diff --git a/convex/videoActions.ts b/convex/videoActions.ts
index 43fde9d..9becada 100644
--- a/convex/videoActions.ts
+++ b/convex/videoActions.ts
@@ -23,10 +23,18 @@ import { BUCKET_NAME, getS3Client } from "./s3";
const GIBIBYTE = 1024 ** 3;
const MAX_PRESIGNED_PUT_FILE_SIZE_BYTES = 5 * GIBIBYTE;
const ALLOWED_UPLOAD_CONTENT_TYPES = new Set([
+ // Video
"video/mp4",
"video/quicktime",
"video/webm",
"video/x-matroska",
+ // Documents
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document", // .docx
+ // Audio
+ "audio/wav",
+ "audio/x-wav",
+ // Binary / generic (covers .aup3 — Audacity project files)
+ "application/octet-stream",
]);
function getExtensionFromKey(key: string, fallback = "mp4") {
@@ -115,18 +123,24 @@ function isAllowedUploadContentType(contentType: string): boolean {
return ALLOWED_UPLOAD_CONTENT_TYPES.has(contentType);
}
+function isVideoContentType(contentType: string): boolean {
+ return contentType.startsWith("video/");
+}
+
function validateUploadRequestOrThrow(args: { fileSize: number; contentType: string }) {
if (!Number.isFinite(args.fileSize) || args.fileSize <= 0) {
- throw new Error("Video file size must be greater than zero.");
+ throw new Error("File size must be greater than zero.");
}
if (args.fileSize > MAX_PRESIGNED_PUT_FILE_SIZE_BYTES) {
- throw new Error("Video file is too large for direct upload.");
+ throw new Error("File is too large for direct upload.");
}
const normalizedContentType = normalizeContentType(args.contentType);
if (!isAllowedUploadContentType(normalizedContentType)) {
- throw new Error("Unsupported video format. Allowed: mp4, mov, webm, mkv.");
+ throw new Error(
+ "Unsupported file format. Allowed: mp4, mov, webm, mkv, docx, wav, aup3.",
+ );
}
return normalizedContentType;
@@ -138,9 +152,9 @@ function shouldDeleteUploadedObjectOnFailure(error: unknown): boolean {
}
return (
- error.message.includes("Unsupported video format") ||
- error.message.includes("Video file is too large") ||
- error.message.includes("Uploaded video file not found") ||
+ error.message.includes("Unsupported file format") ||
+ error.message.includes("File is too large") ||
+ error.message.includes("Uploaded file not found") ||
error.message.includes("Storage limit reached")
);
}
@@ -272,18 +286,20 @@ export const markUploadComplete = action({
!Number.isFinite(contentLengthRaw) ||
contentLengthRaw <= 0
) {
- throw new Error("Uploaded video file not found or empty.");
+ throw new Error("Uploaded file not found or empty.");
}
const contentLength = contentLengthRaw;
if (contentLength > MAX_PRESIGNED_PUT_FILE_SIZE_BYTES) {
- throw new Error("Video file is too large for direct upload.");
+ throw new Error("File is too large for direct upload.");
}
const normalizedContentType = normalizeContentType(
head.ContentType ?? video.contentType,
);
if (!isAllowedUploadContentType(normalizedContentType)) {
- throw new Error("Unsupported video format. Allowed: mp4, mov, webm, mkv.");
+ throw new Error(
+ "Unsupported file format. Allowed: mp4, mov, webm, mkv, docx, wav, aup3.",
+ );
}
await ctx.runMutation(internal.videos.reconcileUploadedObjectMetadata, {
@@ -292,18 +308,26 @@ export const markUploadComplete = action({
contentType: normalizedContentType,
});
- await ctx.runMutation(internal.videos.markAsProcessing, {
- videoId: args.videoId,
- });
+ if (isVideoContentType(normalizedContentType)) {
+ // ── Video path: send to Mux for transcoding ──
+ await ctx.runMutation(internal.videos.markAsProcessing, {
+ videoId: args.videoId,
+ });
- const ingestUrl = await buildSignedBucketObjectUrl(video.s3Key, {
- expiresIn: 60 * 60 * 24,
- });
- const asset = await createMuxAssetFromInputUrl(args.videoId, ingestUrl);
- if (asset.id) {
- await ctx.runMutation(internal.videos.setMuxAssetReference, {
+ const ingestUrl = await buildSignedBucketObjectUrl(video.s3Key, {
+ expiresIn: 60 * 60 * 24,
+ });
+ const asset = await createMuxAssetFromInputUrl(args.videoId, ingestUrl);
+ if (asset.id) {
+ await ctx.runMutation(internal.videos.setMuxAssetReference, {
+ videoId: args.videoId,
+ muxAssetId: asset.id,
+ });
+ }
+ } else {
+ // ── Non-video path: skip Mux, mark ready immediately ──
+ await ctx.runMutation(internal.videos.markFileAsReady, {
videoId: args.videoId,
- muxAssetId: asset.id,
});
}
} catch (error) {
@@ -325,7 +349,7 @@ export const markUploadComplete = action({
const uploadError =
shouldDeleteObject && error instanceof Error
? error.message
- : "Mux ingest failed after upload.";
+ : "Upload processing failed.";
await ctx.runMutation(internal.videos.markAsFailed, {
videoId: args.videoId,
uploadError,
diff --git a/convex/videos.ts b/convex/videos.ts
index dae50eb..c360f50 100644
--- a/convex/videos.ts
+++ b/convex/videos.ts
@@ -363,6 +363,19 @@ export const markAsReady = internalMutation({
},
});
+/** Mark a non-video file as ready (no Mux processing required). */
+export const markFileAsReady = internalMutation({
+ args: {
+ videoId: v.id("videos"),
+ },
+ handler: async (ctx, args) => {
+ await ctx.db.patch(args.videoId, {
+ uploadError: undefined,
+ status: "ready",
+ });
+ },
+});
+
export const markAsFailed = internalMutation({
args: {
videoId: v.id("videos"),
diff --git a/src/components/upload/DropZone.tsx b/src/components/upload/DropZone.tsx
index c298591..950a601 100644
--- a/src/components/upload/DropZone.tsx
+++ b/src/components/upload/DropZone.tsx
@@ -31,9 +31,7 @@ export function DropZone({ onFilesSelected, disabled, className }: DropZoneProps
if (disabled) return;
- const files = Array.from(e.dataTransfer.files).filter((file) =>
- file.type.startsWith("video/")
- );
+ const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
onFilesSelected(files);
@@ -71,7 +69,7 @@ export function DropZone({ onFilesSelected, disabled, className }: DropZoneProps
>
- {isDragActive ? "Drop to upload" : "Drop videos or click to upload"}
+ {isDragActive ? "Drop to upload" : "Drop files or click to upload"}
- MP4, MOV, WebM supported
+ Video, audio, and documents supported
diff --git a/src/components/upload/UploadButton.tsx b/src/components/upload/UploadButton.tsx
index 960881f..0752fb4 100644
--- a/src/components/upload/UploadButton.tsx
+++ b/src/components/upload/UploadButton.tsx
@@ -34,7 +34,7 @@ export function UploadButton({