@girardmedia/bootspring 3.3.2 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agents/accessibility-auditor.md +39 -0
- package/assets/agents/api-designer.md +40 -0
- package/assets/agents/auth-implementer.md +64 -0
- package/assets/agents/bug-hunter.md +42 -0
- package/assets/agents/bundle-analyzer.md +40 -0
- package/assets/agents/cache-optimizer.md +55 -0
- package/assets/agents/changelog-writer.md +55 -0
- package/assets/agents/ci-cd-builder.md +40 -0
- package/assets/agents/code-explainer.md +39 -0
- package/assets/agents/code-reviewer.md +39 -0
- package/assets/agents/cost-optimizer.md +57 -0
- package/assets/agents/cron-scheduler.md +51 -0
- package/assets/agents/data-seeder.md +56 -0
- package/assets/agents/database-architect.md +40 -0
- package/assets/agents/dependency-updater.md +40 -0
- package/assets/agents/deploy-checker.md +40 -0
- package/assets/agents/docker-optimizer.md +40 -0
- package/assets/agents/documentation-writer.md +40 -0
- package/assets/agents/email-builder.md +55 -0
- package/assets/agents/env-setup.md +40 -0
- package/assets/agents/error-handler.md +40 -0
- package/assets/agents/eslint-fixer.md +46 -0
- package/assets/agents/feature-flagger.md +69 -0
- package/assets/agents/git-detective.md +39 -0
- package/assets/agents/graphql-builder.md +60 -0
- package/assets/agents/incident-responder.md +59 -0
- package/assets/agents/log-analyzer.md +39 -0
- package/assets/agents/migration-planner.md +41 -0
- package/assets/agents/monorepo-navigator.md +39 -0
- package/assets/agents/nextjs-expert.md +57 -0
- package/assets/agents/notification-builder.md +56 -0
- package/assets/agents/onboarding-guide.md +39 -0
- package/assets/agents/performance-profiler.md +40 -0
- package/assets/agents/prisma-expert.md +57 -0
- package/assets/agents/rate-limiter.md +58 -0
- package/assets/agents/react-expert.md +58 -0
- package/assets/agents/refactorer.md +42 -0
- package/assets/agents/regex-builder.md +46 -0
- package/assets/agents/release-manager.md +40 -0
- package/assets/agents/s3-manager.md +58 -0
- package/assets/agents/schema-validator.md +40 -0
- package/assets/agents/search-builder.md +62 -0
- package/assets/agents/security-auditor.md +39 -0
- package/assets/agents/sitemap-generator.md +53 -0
- package/assets/agents/stripe-integrator.md +59 -0
- package/assets/agents/tailwind-expert.md +55 -0
- package/assets/agents/tech-debt-tracker.md +39 -0
- package/assets/agents/test-writer.md +42 -0
- package/assets/agents/type-fixer.md +45 -0
- package/assets/agents/webhook-builder.md +54 -0
- package/assets/rules/cpp.md +53 -0
- package/assets/rules/css.md +52 -0
- package/assets/rules/go.md +50 -0
- package/assets/rules/html.md +52 -0
- package/assets/rules/java.md +51 -0
- package/assets/rules/kotlin.md +50 -0
- package/assets/rules/php.md +51 -0
- package/assets/rules/python.md +51 -0
- package/assets/rules/ruby.md +51 -0
- package/assets/rules/rust.md +49 -0
- package/assets/rules/shell.md +52 -0
- package/assets/rules/sql.md +49 -0
- package/assets/rules/swift.md +50 -0
- package/assets/rules/typescript.md +52 -0
- package/assets/rules/yaml-json.md +51 -0
- package/assets/skills/accessibility.md +210 -0
- package/assets/skills/agent-patterns.md +387 -0
- package/assets/skills/ai-integration.md +263 -0
- package/assets/skills/animation-patterns.md +224 -0
- package/assets/skills/api-design.md +218 -0
- package/assets/skills/api-gateway.md +341 -0
- package/assets/skills/api-versioning.md +226 -0
- package/assets/skills/astro-patterns.md +233 -0
- package/assets/skills/auth-patterns.md +248 -0
- package/assets/skills/aws-patterns.md +171 -0
- package/assets/skills/background-jobs.md +162 -0
- package/assets/skills/browser-extensions.md +309 -0
- package/assets/skills/caching-patterns.md +253 -0
- package/assets/skills/ci-cd.md +251 -0
- package/assets/skills/cli-development.md +296 -0
- package/assets/skills/code-review.md +185 -0
- package/assets/skills/cron-patterns.md +327 -0
- package/assets/skills/data-fetching.md +231 -0
- package/assets/skills/database-migrations.md +346 -0
- package/assets/skills/database-patterns.md +219 -0
- package/assets/skills/debugging.md +281 -0
- package/assets/skills/design-system.md +289 -0
- package/assets/skills/django-patterns.md +182 -0
- package/assets/skills/docker-patterns.md +235 -0
- package/assets/skills/e2e-testing.md +287 -0
- package/assets/skills/edge-computing.md +268 -0
- package/assets/skills/electron-patterns.md +266 -0
- package/assets/skills/email-templates.md +206 -0
- package/assets/skills/error-handling.md +265 -0
- package/assets/skills/event-driven.md +232 -0
- package/assets/skills/express-patterns.md +239 -0
- package/assets/skills/fastapi-patterns.md +198 -0
- package/assets/skills/feature-flags.md +212 -0
- package/assets/skills/figma-to-code.md +298 -0
- package/assets/skills/file-upload.md +228 -0
- package/assets/skills/forms-patterns.md +264 -0
- package/assets/skills/gcp-patterns.md +189 -0
- package/assets/skills/git-workflow.md +187 -0
- package/assets/skills/golang-patterns.md +185 -0
- package/assets/skills/graphql-patterns.md +244 -0
- package/assets/skills/i18n-patterns.md +172 -0
- package/assets/skills/image-processing.md +350 -0
- package/assets/skills/java-springboot.md +226 -0
- package/assets/skills/kotlin-patterns.md +207 -0
- package/assets/skills/kubernetes-patterns.md +326 -0
- package/assets/skills/laravel-patterns.md +261 -0
- package/assets/skills/llm-fine-tuning.md +335 -0
- package/assets/skills/load-testing.md +303 -0
- package/assets/skills/logging-observability.md +228 -0
- package/assets/skills/markdown-processing.md +318 -0
- package/assets/skills/mcp-server-patterns.md +292 -0
- package/assets/skills/microservices.md +272 -0
- package/assets/skills/migration-patterns.md +239 -0
- package/assets/skills/mongodb-patterns.md +189 -0
- package/assets/skills/monorepo-patterns.md +287 -0
- package/assets/skills/nextjs-app-router.md +237 -0
- package/assets/skills/notification-patterns.md +348 -0
- package/assets/skills/oauth-patterns.md +246 -0
- package/assets/skills/payment-integration.md +222 -0
- package/assets/skills/pdf-generation.md +307 -0
- package/assets/skills/performance-optimization.md +277 -0
- package/assets/skills/php-patterns.md +210 -0
- package/assets/skills/prisma-patterns.md +241 -0
- package/assets/skills/prompt-engineering.md +193 -0
- package/assets/skills/pwa-patterns.md +247 -0
- package/assets/skills/python-patterns.md +158 -0
- package/assets/skills/python-testing.md +172 -0
- package/assets/skills/queue-patterns.md +295 -0
- package/assets/skills/rag-patterns.md +159 -0
- package/assets/skills/rate-limiting.md +319 -0
- package/assets/skills/react-components.md +201 -0
- package/assets/skills/react-native-patterns.md +299 -0
- package/assets/skills/real-time-patterns.md +181 -0
- package/assets/skills/redis-patterns.md +188 -0
- package/assets/skills/refactoring.md +218 -0
- package/assets/skills/regex-patterns.md +191 -0
- package/assets/skills/remix-patterns.md +262 -0
- package/assets/skills/responsive-design.md +199 -0
- package/assets/skills/ruby-rails-patterns.md +178 -0
- package/assets/skills/rust-patterns.md +211 -0
- package/assets/skills/search-patterns.md +227 -0
- package/assets/skills/security-hardening.md +237 -0
- package/assets/skills/seo-patterns.md +179 -0
- package/assets/skills/serverless-patterns.md +223 -0
- package/assets/skills/sql-optimization.md +154 -0
- package/assets/skills/state-management.md +254 -0
- package/assets/skills/storybook-patterns.md +330 -0
- package/assets/skills/svelte-patterns.md +258 -0
- package/assets/skills/swift-patterns.md +227 -0
- package/assets/skills/tailwind-patterns.md +272 -0
- package/assets/skills/tdd-workflow.md +199 -0
- package/assets/skills/terraform-patterns.md +270 -0
- package/assets/skills/testing-react.md +240 -0
- package/assets/skills/testing-vitest.md +232 -0
- package/assets/skills/typescript-strict.md +159 -0
- package/assets/skills/video-processing.md +340 -0
- package/assets/skills/vue-patterns.md +247 -0
- package/assets/skills/web-workers.md +327 -0
- package/assets/skills/webhooks-patterns.md +283 -0
- package/assets/skills/websocket-patterns.md +306 -0
- package/dist/cli/index.js +941 -958
- package/dist/core/index.d.ts +341 -11
- package/dist/core.js +58 -95
- package/dist/mcp/index.d.ts +33 -1
- package/dist/mcp-server.js +177 -255
- package/package.json +4 -1
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: video-processing
|
|
3
|
+
description: Video processing patterns with FFmpeg for transcoding, HLS streaming, thumbnail generation, upload handling, and progress tracking.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Video Processing Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Apply video processing patterns when your application handles user-uploaded videos, needs adaptive streaming, generates video thumbnails, or transcodes between formats. FFmpeg (via fluent-ffmpeg) is the standard tool for server-side video work. These patterns cover transcoding, HLS/DASH adaptive streaming, thumbnail extraction, chunked upload handling, and progress reporting. Always process videos asynchronously via job queues.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### FFmpeg Transcoding
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// src/video/transcoder.ts
|
|
17
|
+
import ffmpeg from 'fluent-ffmpeg';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
|
|
20
|
+
interface TranscodeOptions {
|
|
21
|
+
inputPath: string;
|
|
22
|
+
outputPath: string;
|
|
23
|
+
width?: number;
|
|
24
|
+
height?: number;
|
|
25
|
+
videoBitrate?: string;
|
|
26
|
+
audioBitrate?: string;
|
|
27
|
+
format?: 'mp4' | 'webm';
|
|
28
|
+
onProgress?: (percent: number) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function transcodeVideo(options: TranscodeOptions): Promise<string> {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
let command = ffmpeg(options.inputPath)
|
|
34
|
+
.videoCodec(options.format === 'webm' ? 'libvpx-vp9' : 'libx264')
|
|
35
|
+
.audioCodec(options.format === 'webm' ? 'libopus' : 'aac')
|
|
36
|
+
.audioBitrate(options.audioBitrate ?? '128k')
|
|
37
|
+
.videoBitrate(options.videoBitrate ?? '2500k')
|
|
38
|
+
.format(options.format ?? 'mp4');
|
|
39
|
+
|
|
40
|
+
if (options.width || options.height) {
|
|
41
|
+
const w = options.width ?? -2; // -2 = auto, divisible by 2
|
|
42
|
+
const h = options.height ?? -2;
|
|
43
|
+
command = command.size(`${w}x${h}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (options.format === 'mp4') {
|
|
47
|
+
command = command.outputOptions([
|
|
48
|
+
'-preset', 'fast',
|
|
49
|
+
'-crf', '23',
|
|
50
|
+
'-movflags', '+faststart', // optimize for web streaming
|
|
51
|
+
'-pix_fmt', 'yuv420p', // compatibility
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
command
|
|
56
|
+
.on('progress', (progress) => {
|
|
57
|
+
options.onProgress?.(Math.round(progress.percent ?? 0));
|
|
58
|
+
})
|
|
59
|
+
.on('end', () => resolve(options.outputPath))
|
|
60
|
+
.on('error', (err) => reject(new Error(`Transcode failed: ${err.message}`)))
|
|
61
|
+
.save(options.outputPath);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### HLS Adaptive Streaming
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// src/video/hls-generator.ts
|
|
70
|
+
import ffmpeg from 'fluent-ffmpeg';
|
|
71
|
+
import fs from 'fs/promises';
|
|
72
|
+
import path from 'path';
|
|
73
|
+
|
|
74
|
+
interface HlsVariant {
|
|
75
|
+
width: number;
|
|
76
|
+
height: number;
|
|
77
|
+
videoBitrate: string;
|
|
78
|
+
audioBitrate: string;
|
|
79
|
+
label: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const variants: HlsVariant[] = [
|
|
83
|
+
{ width: 426, height: 240, videoBitrate: '400k', audioBitrate: '64k', label: '240p' },
|
|
84
|
+
{ width: 640, height: 360, videoBitrate: '800k', audioBitrate: '96k', label: '360p' },
|
|
85
|
+
{ width: 854, height: 480, videoBitrate: '1400k', audioBitrate: '128k', label: '480p' },
|
|
86
|
+
{ width: 1280, height: 720, videoBitrate: '2800k', audioBitrate: '128k', label: '720p' },
|
|
87
|
+
{ width: 1920, height: 1080, videoBitrate: '5000k', audioBitrate: '192k', label: '1080p' },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
export async function generateHls(
|
|
91
|
+
inputPath: string,
|
|
92
|
+
outputDir: string,
|
|
93
|
+
onProgress?: (variant: string, percent: number) => void
|
|
94
|
+
): Promise<string> {
|
|
95
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
96
|
+
|
|
97
|
+
// Generate each variant
|
|
98
|
+
for (const variant of variants) {
|
|
99
|
+
const variantDir = path.join(outputDir, variant.label);
|
|
100
|
+
await fs.mkdir(variantDir, { recursive: true });
|
|
101
|
+
|
|
102
|
+
await new Promise<void>((resolve, reject) => {
|
|
103
|
+
ffmpeg(inputPath)
|
|
104
|
+
.videoCodec('libx264')
|
|
105
|
+
.audioCodec('aac')
|
|
106
|
+
.size(`${variant.width}x${variant.height}`)
|
|
107
|
+
.videoBitrate(variant.videoBitrate)
|
|
108
|
+
.audioBitrate(variant.audioBitrate)
|
|
109
|
+
.outputOptions([
|
|
110
|
+
'-preset', 'fast',
|
|
111
|
+
'-crf', '23',
|
|
112
|
+
'-g', '48', // keyframe every 2s at 24fps
|
|
113
|
+
'-keyint_min', '48',
|
|
114
|
+
'-sc_threshold', '0',
|
|
115
|
+
'-hls_time', '6', // 6-second segments
|
|
116
|
+
'-hls_list_size', '0', // keep all segments
|
|
117
|
+
'-hls_segment_filename', path.join(variantDir, 'segment_%03d.ts'),
|
|
118
|
+
'-f', 'hls',
|
|
119
|
+
])
|
|
120
|
+
.on('progress', (p) => onProgress?.(variant.label, Math.round(p.percent ?? 0)))
|
|
121
|
+
.on('end', () => resolve())
|
|
122
|
+
.on('error', (err) => reject(err))
|
|
123
|
+
.save(path.join(variantDir, 'playlist.m3u8'));
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Generate master playlist
|
|
128
|
+
const masterPlaylist = generateMasterPlaylist(variants);
|
|
129
|
+
const masterPath = path.join(outputDir, 'master.m3u8');
|
|
130
|
+
await fs.writeFile(masterPath, masterPlaylist);
|
|
131
|
+
|
|
132
|
+
return masterPath;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function generateMasterPlaylist(variants: HlsVariant[]): string {
|
|
136
|
+
let content = '#EXTM3U\n#EXT-X-VERSION:3\n\n';
|
|
137
|
+
|
|
138
|
+
for (const v of variants) {
|
|
139
|
+
const bandwidth = parseInt(v.videoBitrate) * 1000;
|
|
140
|
+
content += `#EXT-X-STREAM-INF:BANDWIDTH=${bandwidth},RESOLUTION=${v.width}x${v.height}\n`;
|
|
141
|
+
content += `${v.label}/playlist.m3u8\n\n`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return content;
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Thumbnail Generation
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// src/video/thumbnails.ts
|
|
152
|
+
import ffmpeg from 'fluent-ffmpeg';
|
|
153
|
+
|
|
154
|
+
export async function generateThumbnails(
|
|
155
|
+
inputPath: string,
|
|
156
|
+
outputDir: string,
|
|
157
|
+
count: number = 5
|
|
158
|
+
): Promise<string[]> {
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
const filenames: string[] = [];
|
|
161
|
+
|
|
162
|
+
ffmpeg(inputPath)
|
|
163
|
+
.on('filenames', (names) => filenames.push(...names))
|
|
164
|
+
.on('end', () => resolve(filenames.map((f) => path.join(outputDir, f))))
|
|
165
|
+
.on('error', (err) => reject(err))
|
|
166
|
+
.screenshots({
|
|
167
|
+
count,
|
|
168
|
+
folder: outputDir,
|
|
169
|
+
filename: 'thumb_%i.jpg',
|
|
170
|
+
size: '640x360',
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Generate a single thumbnail at specific time
|
|
176
|
+
export async function generateThumbnailAtTime(
|
|
177
|
+
inputPath: string,
|
|
178
|
+
outputPath: string,
|
|
179
|
+
timeSeconds: number
|
|
180
|
+
): Promise<string> {
|
|
181
|
+
return new Promise((resolve, reject) => {
|
|
182
|
+
ffmpeg(inputPath)
|
|
183
|
+
.seekInput(timeSeconds)
|
|
184
|
+
.frames(1)
|
|
185
|
+
.size('640x360')
|
|
186
|
+
.on('end', () => resolve(outputPath))
|
|
187
|
+
.on('error', (err) => reject(err))
|
|
188
|
+
.save(outputPath);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Generate animated GIF preview
|
|
193
|
+
export async function generateGifPreview(
|
|
194
|
+
inputPath: string,
|
|
195
|
+
outputPath: string,
|
|
196
|
+
startTime: number = 0,
|
|
197
|
+
duration: number = 3
|
|
198
|
+
): Promise<string> {
|
|
199
|
+
return new Promise((resolve, reject) => {
|
|
200
|
+
ffmpeg(inputPath)
|
|
201
|
+
.seekInput(startTime)
|
|
202
|
+
.duration(duration)
|
|
203
|
+
.outputOptions([
|
|
204
|
+
'-vf', 'fps=10,scale=320:-1:flags=lanczos',
|
|
205
|
+
'-loop', '0',
|
|
206
|
+
])
|
|
207
|
+
.on('end', () => resolve(outputPath))
|
|
208
|
+
.on('error', (err) => reject(err))
|
|
209
|
+
.save(outputPath);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Video Metadata Extraction
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// src/video/metadata.ts
|
|
218
|
+
import ffmpeg from 'fluent-ffmpeg';
|
|
219
|
+
|
|
220
|
+
export interface VideoMetadata {
|
|
221
|
+
duration: number;
|
|
222
|
+
width: number;
|
|
223
|
+
height: number;
|
|
224
|
+
fps: number;
|
|
225
|
+
bitrate: number;
|
|
226
|
+
codec: string;
|
|
227
|
+
audioCodec: string;
|
|
228
|
+
fileSize: number;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function getVideoMetadata(inputPath: string): Promise<VideoMetadata> {
|
|
232
|
+
return new Promise((resolve, reject) => {
|
|
233
|
+
ffmpeg.ffprobe(inputPath, (err, data) => {
|
|
234
|
+
if (err) return reject(err);
|
|
235
|
+
|
|
236
|
+
const videoStream = data.streams.find((s) => s.codec_type === 'video');
|
|
237
|
+
const audioStream = data.streams.find((s) => s.codec_type === 'audio');
|
|
238
|
+
|
|
239
|
+
if (!videoStream) return reject(new Error('No video stream found'));
|
|
240
|
+
|
|
241
|
+
resolve({
|
|
242
|
+
duration: data.format.duration ?? 0,
|
|
243
|
+
width: videoStream.width ?? 0,
|
|
244
|
+
height: videoStream.height ?? 0,
|
|
245
|
+
fps: eval(videoStream.r_frame_rate ?? '0') || 0,
|
|
246
|
+
bitrate: parseInt(String(data.format.bit_rate ?? '0')),
|
|
247
|
+
codec: videoStream.codec_name ?? 'unknown',
|
|
248
|
+
audioCodec: audioStream?.codec_name ?? 'none',
|
|
249
|
+
fileSize: data.format.size ?? 0,
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Chunked Upload with Resumability
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
// src/video/upload-handler.ts
|
|
260
|
+
import { Router } from 'express';
|
|
261
|
+
import fs from 'fs/promises';
|
|
262
|
+
import path from 'path';
|
|
263
|
+
|
|
264
|
+
const router = Router();
|
|
265
|
+
const UPLOAD_DIR = '/tmp/video-uploads';
|
|
266
|
+
|
|
267
|
+
// Initialize upload
|
|
268
|
+
router.post('/api/videos/upload/init', async (req, res) => {
|
|
269
|
+
const { filename, fileSize, mimeType } = req.body;
|
|
270
|
+
const uploadId = crypto.randomUUID();
|
|
271
|
+
const chunkSize = 5 * 1024 * 1024; // 5MB chunks
|
|
272
|
+
const totalChunks = Math.ceil(fileSize / chunkSize);
|
|
273
|
+
|
|
274
|
+
await fs.mkdir(path.join(UPLOAD_DIR, uploadId), { recursive: true });
|
|
275
|
+
await db.query(
|
|
276
|
+
'INSERT INTO uploads (id, filename, file_size, mime_type, total_chunks, status) VALUES ($1,$2,$3,$4,$5,$6)',
|
|
277
|
+
[uploadId, filename, fileSize, mimeType, totalChunks, 'uploading']
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
res.json({ uploadId, chunkSize, totalChunks });
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Upload a chunk
|
|
284
|
+
router.put('/api/videos/upload/:uploadId/chunk/:index', async (req, res) => {
|
|
285
|
+
const { uploadId, index } = req.params;
|
|
286
|
+
const chunkPath = path.join(UPLOAD_DIR, uploadId, `chunk_${index.padStart(5, '0')}`);
|
|
287
|
+
|
|
288
|
+
const chunks: Buffer[] = [];
|
|
289
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
290
|
+
await fs.writeFile(chunkPath, Buffer.concat(chunks));
|
|
291
|
+
|
|
292
|
+
res.json({ received: parseInt(index) });
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Finalize upload — merge chunks and queue processing
|
|
296
|
+
router.post('/api/videos/upload/:uploadId/finalize', async (req, res) => {
|
|
297
|
+
const { uploadId } = req.params;
|
|
298
|
+
const uploadDir = path.join(UPLOAD_DIR, uploadId);
|
|
299
|
+
const chunks = (await fs.readdir(uploadDir)).sort();
|
|
300
|
+
const outputPath = path.join(UPLOAD_DIR, `${uploadId}.mp4`);
|
|
301
|
+
|
|
302
|
+
// Merge chunks
|
|
303
|
+
const writeStream = require('fs').createWriteStream(outputPath);
|
|
304
|
+
for (const chunk of chunks) {
|
|
305
|
+
const data = await fs.readFile(path.join(uploadDir, chunk));
|
|
306
|
+
writeStream.write(data);
|
|
307
|
+
}
|
|
308
|
+
writeStream.end();
|
|
309
|
+
|
|
310
|
+
// Clean up chunks
|
|
311
|
+
await fs.rm(uploadDir, { recursive: true });
|
|
312
|
+
|
|
313
|
+
// Queue processing job
|
|
314
|
+
await videoQueue.add('process', { uploadId, inputPath: outputPath });
|
|
315
|
+
|
|
316
|
+
res.json({ uploadId, status: 'processing' });
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
export default router;
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Examples
|
|
323
|
+
|
|
324
|
+
| Operation | Tool | Output |
|
|
325
|
+
|-----------|------|--------|
|
|
326
|
+
| Transcode to MP4 | fluent-ffmpeg | Web-optimized MP4 (faststart) |
|
|
327
|
+
| Adaptive streaming | HLS generator | Master playlist + segment files |
|
|
328
|
+
| Thumbnail grid | ffmpeg screenshots | 5 JPEG thumbnails at intervals |
|
|
329
|
+
| GIF preview | ffmpeg filter | 3-second looping GIF |
|
|
330
|
+
| Metadata | ffprobe | Duration, resolution, codec info |
|
|
331
|
+
|
|
332
|
+
## Checklist
|
|
333
|
+
- [ ] Video processing runs asynchronously via job queue, not in request handler
|
|
334
|
+
- [ ] FFmpeg outputs use `-movflags +faststart` for web MP4 files
|
|
335
|
+
- [ ] HLS segments are 6 seconds with closed GOPs for clean seeking
|
|
336
|
+
- [ ] Multiple quality variants generated for adaptive streaming
|
|
337
|
+
- [ ] Thumbnails generated at evenly spaced intervals
|
|
338
|
+
- [ ] Chunked upload supports resumption on connection failure
|
|
339
|
+
- [ ] Progress reported to client via WebSocket or polling endpoint
|
|
340
|
+
- [ ] Temporary files cleaned up after processing completes
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vue-patterns
|
|
3
|
+
description: Vue.js patterns for Composition API, composables, Pinia stores, slots, teleport, and transitions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Vue.js Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply these patterns when building Vue 3 applications with the Composition API.
|
|
11
|
+
Use this skill for writing composables, managing state with Pinia, designing
|
|
12
|
+
reusable components with slots, rendering outside the DOM tree with Teleport,
|
|
13
|
+
and adding animations with transitions.
|
|
14
|
+
|
|
15
|
+
## How It Works
|
|
16
|
+
|
|
17
|
+
### Composition API with `<script setup>`
|
|
18
|
+
|
|
19
|
+
Use `<script setup>` for all components. It provides better TypeScript inference,
|
|
20
|
+
less boilerplate, and automatic component/directive registration.
|
|
21
|
+
|
|
22
|
+
```vue
|
|
23
|
+
<script setup lang="ts">
|
|
24
|
+
import { ref, computed, onMounted } from 'vue'
|
|
25
|
+
import { useUserStore } from '@/stores/user'
|
|
26
|
+
|
|
27
|
+
const props = defineProps<{
|
|
28
|
+
projectId: string
|
|
29
|
+
editable?: boolean
|
|
30
|
+
}>()
|
|
31
|
+
|
|
32
|
+
const emit = defineEmits<{
|
|
33
|
+
save: [data: ProjectData]
|
|
34
|
+
cancel: []
|
|
35
|
+
}>()
|
|
36
|
+
|
|
37
|
+
const userStore = useUserStore()
|
|
38
|
+
const title = ref('')
|
|
39
|
+
const isValid = computed(() => title.value.length >= 3)
|
|
40
|
+
|
|
41
|
+
onMounted(async () => {
|
|
42
|
+
const project = await fetchProject(props.projectId)
|
|
43
|
+
title.value = project.title
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
function handleSave() {
|
|
47
|
+
if (isValid.value) {
|
|
48
|
+
emit('save', { title: title.value })
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Composables
|
|
55
|
+
|
|
56
|
+
Extract reusable reactive logic into composable functions. Name them `use*`.
|
|
57
|
+
Return reactive refs and methods. Accept refs as arguments for reactivity.
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// composables/usePagination.ts
|
|
61
|
+
import { ref, computed, watch, type Ref } from 'vue'
|
|
62
|
+
|
|
63
|
+
export function usePagination<T>(
|
|
64
|
+
items: Ref<T[]>,
|
|
65
|
+
options: { pageSize?: number } = {}
|
|
66
|
+
) {
|
|
67
|
+
const pageSize = options.pageSize ?? 20
|
|
68
|
+
const currentPage = ref(1)
|
|
69
|
+
|
|
70
|
+
const totalPages = computed(() =>
|
|
71
|
+
Math.ceil(items.value.length / pageSize)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const paginatedItems = computed(() => {
|
|
75
|
+
const start = (currentPage.value - 1) * pageSize
|
|
76
|
+
return items.value.slice(start, start + pageSize)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
function goToPage(page: number) {
|
|
80
|
+
currentPage.value = Math.max(1, Math.min(page, totalPages.value))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Reset to page 1 when items change
|
|
84
|
+
watch(items, () => { currentPage.value = 1 })
|
|
85
|
+
|
|
86
|
+
return { currentPage, totalPages, paginatedItems, goToPage }
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// composables/useAsync.ts
|
|
92
|
+
export function useAsync<T>(fn: () => Promise<T>) {
|
|
93
|
+
const data = ref<T | null>(null) as Ref<T | null>
|
|
94
|
+
const error = ref<Error | null>(null)
|
|
95
|
+
const loading = ref(false)
|
|
96
|
+
|
|
97
|
+
async function execute() {
|
|
98
|
+
loading.value = true
|
|
99
|
+
error.value = null
|
|
100
|
+
try {
|
|
101
|
+
data.value = await fn()
|
|
102
|
+
} catch (e) {
|
|
103
|
+
error.value = e instanceof Error ? e : new Error(String(e))
|
|
104
|
+
} finally {
|
|
105
|
+
loading.value = false
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { data, error, loading, execute }
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Pinia Stores
|
|
114
|
+
|
|
115
|
+
Use setup-style stores for full Composition API flexibility. Keep stores focused
|
|
116
|
+
on one domain. Use `storeToRefs` for destructuring without losing reactivity.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// stores/cart.ts
|
|
120
|
+
import { defineStore } from 'pinia'
|
|
121
|
+
import { ref, computed } from 'vue'
|
|
122
|
+
|
|
123
|
+
export const useCartStore = defineStore('cart', () => {
|
|
124
|
+
const items = ref<CartItem[]>([])
|
|
125
|
+
|
|
126
|
+
const total = computed(() =>
|
|
127
|
+
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
const itemCount = computed(() =>
|
|
131
|
+
items.value.reduce((sum, item) => sum + item.quantity, 0)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
function addItem(product: Product, quantity = 1) {
|
|
135
|
+
const existing = items.value.find(i => i.productId === product.id)
|
|
136
|
+
if (existing) {
|
|
137
|
+
existing.quantity += quantity
|
|
138
|
+
} else {
|
|
139
|
+
items.value.push({ productId: product.id, name: product.name, price: product.price, quantity })
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function removeItem(productId: string) {
|
|
144
|
+
items.value = items.value.filter(i => i.productId !== productId)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function clear() {
|
|
148
|
+
items.value = []
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { items, total, itemCount, addItem, removeItem, clear }
|
|
152
|
+
})
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Slots for Composition
|
|
156
|
+
|
|
157
|
+
Use named slots for layout composition. Use scoped slots to expose data to the
|
|
158
|
+
parent. Default slot content serves as fallback.
|
|
159
|
+
|
|
160
|
+
```vue
|
|
161
|
+
<!-- BaseCard.vue -->
|
|
162
|
+
<template>
|
|
163
|
+
<div class="card">
|
|
164
|
+
<div v-if="$slots.header" class="card-header">
|
|
165
|
+
<slot name="header" />
|
|
166
|
+
</div>
|
|
167
|
+
<div class="card-body">
|
|
168
|
+
<slot :loading="loading" :error="error" />
|
|
169
|
+
</div>
|
|
170
|
+
<div v-if="$slots.footer" class="card-footer">
|
|
171
|
+
<slot name="footer" />
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</template>
|
|
175
|
+
|
|
176
|
+
<!-- Usage with scoped slot -->
|
|
177
|
+
<BaseCard>
|
|
178
|
+
<template #header>User Profile</template>
|
|
179
|
+
<template #default="{ loading, error }">
|
|
180
|
+
<Spinner v-if="loading" />
|
|
181
|
+
<ErrorBanner v-else-if="error" :error="error" />
|
|
182
|
+
<UserDetails v-else :user="user" />
|
|
183
|
+
</template>
|
|
184
|
+
</BaseCard>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Teleport
|
|
188
|
+
|
|
189
|
+
Use `<Teleport>` to render modals, tooltips, and toasts outside the component's
|
|
190
|
+
DOM hierarchy while keeping them logically owned by the component.
|
|
191
|
+
|
|
192
|
+
```vue
|
|
193
|
+
<template>
|
|
194
|
+
<button @click="showModal = true">Open</button>
|
|
195
|
+
<Teleport to="body">
|
|
196
|
+
<div v-if="showModal" class="modal-overlay" @click.self="showModal = false">
|
|
197
|
+
<div class="modal-content">
|
|
198
|
+
<slot />
|
|
199
|
+
<button @click="showModal = false">Close</button>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
</Teleport>
|
|
203
|
+
</template>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Transitions
|
|
207
|
+
|
|
208
|
+
Use `<Transition>` for enter/leave animations. Use `<TransitionGroup>` for lists.
|
|
209
|
+
Define CSS classes or use JavaScript hooks for complex animations.
|
|
210
|
+
|
|
211
|
+
```vue
|
|
212
|
+
<template>
|
|
213
|
+
<Transition name="fade" mode="out-in">
|
|
214
|
+
<component :is="currentView" :key="currentView.__name" />
|
|
215
|
+
</Transition>
|
|
216
|
+
</template>
|
|
217
|
+
|
|
218
|
+
<style>
|
|
219
|
+
.fade-enter-active, .fade-leave-active { transition: opacity 0.2s ease; }
|
|
220
|
+
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
|
221
|
+
</style>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Examples
|
|
225
|
+
|
|
226
|
+
**Pattern: Provide/Inject for deep dependency passing**
|
|
227
|
+
```typescript
|
|
228
|
+
// Parent
|
|
229
|
+
const theme = ref<'light' | 'dark'>('light')
|
|
230
|
+
provide('theme', readonly(theme))
|
|
231
|
+
|
|
232
|
+
// Deep child
|
|
233
|
+
const theme = inject<Ref<string>>('theme', ref('light'))
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Checklist
|
|
237
|
+
|
|
238
|
+
- [ ] `<script setup lang="ts">` on every component
|
|
239
|
+
- [ ] Props defined with `defineProps<T>()`, emits with `defineEmits<T>()`
|
|
240
|
+
- [ ] Composables named `use*`, return reactive refs and functions
|
|
241
|
+
- [ ] Pinia stores use setup-style syntax; `storeToRefs` for destructuring
|
|
242
|
+
- [ ] Named slots for layout regions; scoped slots for exposing data
|
|
243
|
+
- [ ] `<Teleport to="body">` for modals, tooltips, and overlays
|
|
244
|
+
- [ ] `<Transition>` with `mode="out-in"` for route/view transitions
|
|
245
|
+
- [ ] `v-model` with `defineModel()` (Vue 3.4+) for two-way binding
|
|
246
|
+
- [ ] `watchEffect` for side effects, `watch` when you need old/new values
|
|
247
|
+
- [ ] `provide/inject` with `readonly()` for deep dependency passing
|