Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion meteor-backend/server/pulsevault.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,11 @@ const core = createPulseVaultCore({
// Pulse Cam and the web fallback both upload one pre-recorded MP4 per
// session rather than per-clip "beats".
uploadUnit: 'merged',
allowedExtensions: { video: ['.mp4'], captions: ['.vtt', '.srt'] },
allowedExtensions: {
video: ['.mp4'],
captions: ['.vtt', '.srt'],
thumbnail: ['.jpg', '.jpeg', '.png'],
},
authorize: async (request, ctx) => {
console.log('[pulsevault][hook] authorize called', {
phase: ctx.phase,
Expand Down Expand Up @@ -118,6 +122,21 @@ const core = createPulseVaultCore({
},
onUploadComplete: async (_request, ctx) => {
console.log('[pulsevault][hook] onUploadComplete called', JSON.stringify(ctx));

// Handle thumbnail uploads separately - they relate to another video via relatedTo
if (ctx.kind === 'thumbnail') {
console.log('[pulsevault][hook] Thumbnail upload complete, relatedTo:', ctx.relatedTo);
if (ctx.relatedTo) {
// Update the media item with the thumbnail
await rawDb().collection('mediaitems').updateOne(
{ videoid: ctx.relatedTo },
{ $set: { thumbnail: `/pulsevault/artifacts/${ctx.artifactId}` } }
);
console.log('[pulsevault] Updated thumbnail for video:', ctx.relatedTo);
}
return;
}

console.log('[pulsevault][hook] reservationContext keys:', [...reservationContext.keys()]);
const reservation = reservationContext.get(ctx.artifactId);
reservationContext.delete(ctx.artifactId);
Expand Down
40 changes: 35 additions & 5 deletions src/features/media/PulseUploadButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import { QRCodeSVG } from 'qrcode.react';
import * as tus from 'tus-js-client';
import React, { useEffect, useRef, useState } from 'react';

import { attachmentApi, TIMECORE_BASE_URL, videoApi } from '../../lib/api';
import { attachmentApi, TIMECORE_BASE_URL, videoApi, mediaApi } from '../../lib/api';
import { extractThumbnailFromVideoUrl, extractVideoThumbnail } from '../../lib/videoThumbnail';

/**
* The Pulse Cam server base for deep links — origin plus the `/pulsevault`
Expand Down Expand Up @@ -104,21 +105,34 @@ export const PulseUploadButton: React.FC<PulseUploadButtonProps> = ({
const interval = setInterval(async () => {
try {
const attachments = await attachmentApi.list('ticket', ticketId);
const hasNew = attachments.some(
const newVideo = attachments.find(
(a) => a.type === 'video' && !knownAttachmentIds.current.has(a.id),
);
if (hasNew) {
if (newVideo) {
clearInterval(interval);
clearStoredVideoid(ticketId);
setModalOpen(false);

// Extract and upload thumbnail for the video uploaded via Pulse.
// This replaces the thumbnail that Pulse tries (and fails) to upload via TUS
// with kind='thumbnail' (unsupported by PulseVault).
if (videoid && newVideo.url) {
extractThumbnailFromVideoUrl(newVideo.url)
.then((thumbnailBlob) => mediaApi.uploadThumbnail(videoid, thumbnailBlob))
.catch((err) => {
console.warn('[PulseUpload] Thumbnail extraction/upload failed:', err);
// Non-blocking — video upload already succeeded
});
}

onUploadComplete();
}
} catch {
// ignore transient polling errors
}
}, 3000);
return () => clearInterval(interval);
}, [modalOpen, ticketId, onUploadComplete]);
}, [modalOpen, ticketId, videoid, onUploadComplete]);

const doReserve = async (): Promise<{ videoid: string; uploadLink: string } | null> => {
setReserving(true);
Expand Down Expand Up @@ -172,6 +186,9 @@ export const PulseUploadButton: React.FC<PulseUploadButtonProps> = ({
setError(null);
setProgress(0);

// Start thumbnail extraction in parallel with the upload.
const thumbnailPromise = extractVideoThumbnail(file).catch(() => null);

const upload = new tus.Upload(file, {
endpoint: videoApi.uploadEndpoint(),
retryDelays: [0, 3000, 5000, 10000],
Expand All @@ -180,10 +197,23 @@ export const PulseUploadButton: React.FC<PulseUploadButtonProps> = ({
onProgress(bytesUploaded, bytesTotal) {
setProgress(Math.round((bytesUploaded / bytesTotal) * 100));
},
onSuccess() {
async onSuccess() {
clearStoredVideoid(ticketId);
setUploadToken(null);
setProgress(null);

// Upload thumbnail if extraction succeeded.
// This replaces the thumbnail that would be uploaded via TUS with an unsupported kind.
const thumbnailBlob = await thumbnailPromise;
if (thumbnailBlob && videoid) {
try {
await mediaApi.uploadThumbnail(videoid, thumbnailBlob);
} catch (err) {
console.warn('[PulseUpload] Thumbnail upload failed:', err);
// Non-blocking — video upload already succeeded
}
}

onUploadComplete();
},
onError(err) {
Expand Down
Loading