moonlantern1 commited on
Commit
1dba6ec
·
verified ·
1 Parent(s): ff790f0

Render reviews as HD social exports

Browse files
src/app/admin/reviews/page.tsx CHANGED
@@ -116,6 +116,10 @@ function PhoneVideoPreview({ src }: { src: string }) {
116
  );
117
  }
118
 
 
 
 
 
119
  export default async function AdminReviewsPage() {
120
  const { submissions, sourceLabel } = await listReviewsForAdminPage();
121
  const recorderHref = remoteRecorderHref();
@@ -211,6 +215,14 @@ export default async function AdminReviewsPage() {
211
  <div className="rounded-md bg-cream/[0.04] px-3 py-2 text-sm leading-5 text-cream/70">
212
  {submission.feedback}
213
  </div>
 
 
 
 
 
 
 
 
214
  </div>
215
  </div>
216
  </article>
 
116
  );
117
  }
118
 
119
+ function downloadFilename(submission: AdminReviewSubmission) {
120
+ return `${submission.submissionId}.mp4`;
121
+ }
122
+
123
  export default async function AdminReviewsPage() {
124
  const { submissions, sourceLabel } = await listReviewsForAdminPage();
125
  const recorderHref = remoteRecorderHref();
 
215
  <div className="rounded-md bg-cream/[0.04] px-3 py-2 text-sm leading-5 text-cream/70">
216
  {submission.feedback}
217
  </div>
218
+
219
+ <a
220
+ href={submission.previewUrl}
221
+ download={downloadFilename(submission)}
222
+ className="inline-flex h-10 items-center justify-center rounded-full border border-sage/40 bg-sage/15 px-4 text-sm font-semibold text-sage transition hover:bg-sage/25"
223
+ >
224
+ Download HD MP4
225
+ </a>
226
  </div>
227
  </div>
228
  </article>
src/lib/server/serverClipRenderer.ts CHANGED
@@ -20,8 +20,8 @@ export type ServerRenderResult = {
20
  };
21
 
22
  const WORK_DIR = path.join(process.cwd(), '.local-review-data', 'server-renders');
23
- const VIDEO_WIDTH = 540;
24
- const VIDEO_HEIGHT = 960;
25
  const VIDEO_FPS = 24;
26
  const MIN_VIDEO_CLIP_SECONDS = 5;
27
  const MAX_VIDEO_CLIP_SECONDS = 7;
@@ -176,7 +176,15 @@ export async function renderClipsOnServer(input: ServerRenderInput): Promise<Ser
176
  );
177
  const baseVideoDurationSeconds = cappedVideoDurations.reduce((total, duration) => total + duration, 0);
178
  const renderTargetSeconds =
179
- hasVoiceover ? FINAL_VIDEO_MAX_SECONDS : Math.max(1, baseVideoDurationSeconds);
 
 
 
 
 
 
 
 
180
  const loopFrameCount = Math.max(1, Math.ceil(baseVideoDurationSeconds * VIDEO_FPS));
181
 
182
  const inputArgs = [
@@ -226,11 +234,12 @@ export async function renderClipsOnServer(input: ServerRenderInput): Promise<Ser
226
  ? `${audioInputs}concat=n=${audioPaths.length}:v=0:a=1[acat]`
227
  : '';
228
  const audioFinalize = audioPaths.length
229
- ? `[acat]atrim=duration=${FINAL_VIDEO_MAX_SECONDS},asetpts=PTS-STARTPTS[a]`
 
230
  : '';
231
 
232
- // Cap every voiceover render to a short-form 17 seconds. The b-roll loops
233
- // only to that cap, and -shortest ends the file with the actual audio.
234
  const filterComplex = [videoFilters, videoConcat, videoFinalize, audioFilters, audioConcat, audioFinalize]
235
  .filter(Boolean)
236
  .join(';');
@@ -257,7 +266,9 @@ export async function renderClipsOnServer(input: ServerRenderInput): Promise<Ser
257
  '30',
258
  '-r',
259
  String(VIDEO_FPS),
260
- ...(audioPaths.length ? ['-shortest', '-c:a', 'aac', '-b:a', '96k'] : ['-an']),
 
 
261
  '-movflags',
262
  '+faststart',
263
  '-avoid_negative_ts',
 
20
  };
21
 
22
  const WORK_DIR = path.join(process.cwd(), '.local-review-data', 'server-renders');
23
+ const VIDEO_WIDTH = 1080;
24
+ const VIDEO_HEIGHT = 1920;
25
  const VIDEO_FPS = 24;
26
  const MIN_VIDEO_CLIP_SECONDS = 5;
27
  const MAX_VIDEO_CLIP_SECONDS = 7;
 
176
  );
177
  const baseVideoDurationSeconds = cappedVideoDurations.reduce((total, duration) => total + duration, 0);
178
  const renderTargetSeconds =
179
+ hasVoiceover
180
+ ? Math.max(
181
+ 1,
182
+ Math.min(
183
+ FINAL_VIDEO_MAX_SECONDS,
184
+ audioDurationSeconds > 0 ? audioDurationSeconds : FINAL_VIDEO_MAX_SECONDS,
185
+ ),
186
+ )
187
+ : Math.max(1, baseVideoDurationSeconds);
188
  const loopFrameCount = Math.max(1, Math.ceil(baseVideoDurationSeconds * VIDEO_FPS));
189
 
190
  const inputArgs = [
 
234
  ? `${audioInputs}concat=n=${audioPaths.length}:v=0:a=1[acat]`
235
  : '';
236
  const audioFinalize = audioPaths.length
237
+ ? `[acat]atrim=duration=${formatDuration(renderTargetSeconds)},` +
238
+ `apad=whole_dur=${formatDuration(renderTargetSeconds)},asetpts=PTS-STARTPTS[a]`
239
  : '';
240
 
241
+ // Cap every voiceover render to a short-form 17 seconds, then make audio
242
+ // and video share one target duration so playback never freezes under speech.
243
  const filterComplex = [videoFilters, videoConcat, videoFinalize, audioFilters, audioConcat, audioFinalize]
244
  .filter(Boolean)
245
  .join(';');
 
266
  '30',
267
  '-r',
268
  String(VIDEO_FPS),
269
+ ...(audioPaths.length ? ['-c:a', 'aac', '-b:a', '96k'] : ['-an']),
270
+ '-t',
271
+ formatDuration(renderTargetSeconds),
272
  '-movflags',
273
  '+faststart',
274
  '-avoid_negative_ts',