@su-record/vibe 2.7.6 → 2.7.9

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.
Files changed (69) hide show
  1. package/dist/cli/commands/init.d.ts +10 -0
  2. package/dist/cli/commands/init.d.ts.map +1 -1
  3. package/dist/cli/commands/init.js +78 -2
  4. package/dist/cli/commands/init.js.map +1 -1
  5. package/dist/cli/commands/update.d.ts.map +1 -1
  6. package/dist/cli/commands/update.js +17 -2
  7. package/dist/cli/commands/update.js.map +1 -1
  8. package/dist/cli/postinstall/codex-agents.d.ts +12 -0
  9. package/dist/cli/postinstall/codex-agents.d.ts.map +1 -0
  10. package/dist/cli/postinstall/codex-agents.js +51 -0
  11. package/dist/cli/postinstall/codex-agents.js.map +1 -0
  12. package/dist/cli/postinstall/codex-instruction.d.ts +10 -0
  13. package/dist/cli/postinstall/codex-instruction.d.ts.map +1 -0
  14. package/dist/cli/postinstall/codex-instruction.js +56 -0
  15. package/dist/cli/postinstall/codex-instruction.js.map +1 -0
  16. package/dist/cli/postinstall/constants.d.ts.map +1 -1
  17. package/dist/cli/postinstall/constants.js +1 -0
  18. package/dist/cli/postinstall/constants.js.map +1 -1
  19. package/dist/cli/postinstall/gemini-agents.d.ts +12 -0
  20. package/dist/cli/postinstall/gemini-agents.d.ts.map +1 -0
  21. package/dist/cli/postinstall/gemini-agents.js +80 -0
  22. package/dist/cli/postinstall/gemini-agents.js.map +1 -0
  23. package/dist/cli/postinstall/gemini-instruction.d.ts +10 -0
  24. package/dist/cli/postinstall/gemini-instruction.d.ts.map +1 -0
  25. package/dist/cli/postinstall/gemini-instruction.js +59 -0
  26. package/dist/cli/postinstall/gemini-instruction.js.map +1 -0
  27. package/dist/cli/postinstall/index.d.ts +4 -0
  28. package/dist/cli/postinstall/index.d.ts.map +1 -1
  29. package/dist/cli/postinstall/index.js +4 -0
  30. package/dist/cli/postinstall/index.js.map +1 -1
  31. package/dist/cli/postinstall/main.d.ts.map +1 -1
  32. package/dist/cli/postinstall/main.js +34 -1
  33. package/dist/cli/postinstall/main.js.map +1 -1
  34. package/dist/cli/postinstall.d.ts +1 -1
  35. package/dist/cli/postinstall.d.ts.map +1 -1
  36. package/dist/cli/postinstall.js +1 -1
  37. package/dist/cli/postinstall.js.map +1 -1
  38. package/dist/cli/setup/ProjectSetup.d.ts +15 -0
  39. package/dist/cli/setup/ProjectSetup.d.ts.map +1 -1
  40. package/dist/cli/setup/ProjectSetup.js +159 -0
  41. package/dist/cli/setup/ProjectSetup.js.map +1 -1
  42. package/dist/cli/setup.d.ts +1 -1
  43. package/dist/cli/setup.d.ts.map +1 -1
  44. package/dist/cli/setup.js +1 -1
  45. package/dist/cli/setup.js.map +1 -1
  46. package/dist/cli/utils/cli-detector.d.ts +25 -0
  47. package/dist/cli/utils/cli-detector.d.ts.map +1 -0
  48. package/dist/cli/utils/cli-detector.js +55 -0
  49. package/dist/cli/utils/cli-detector.js.map +1 -0
  50. package/hooks/gemini-hooks.json +73 -0
  51. package/package.json +1 -1
  52. package/skills/agents-md/SKILL.md +120 -0
  53. package/skills/brand-assets/SKILL.md +8 -0
  54. package/skills/characterization-test/SKILL.md +4 -0
  55. package/skills/commerce-patterns/SKILL.md +36 -338
  56. package/skills/commit-push-pr/SKILL.md +21 -64
  57. package/skills/core-capabilities/SKILL.md +26 -142
  58. package/skills/e2e-commerce/SKILL.md +37 -284
  59. package/skills/frontend-design/SKILL.md +12 -31
  60. package/skills/git-worktree/SKILL.md +34 -146
  61. package/skills/handoff/SKILL.md +8 -0
  62. package/skills/parallel-research/SKILL.md +7 -0
  63. package/skills/priority-todos/SKILL.md +34 -213
  64. package/skills/seo-checklist/SKILL.md +38 -225
  65. package/skills/tool-fallback/SKILL.md +53 -143
  66. package/skills/typescript-advanced-types/SKILL.md +30 -685
  67. package/skills/ui-ux-pro-max/SKILL.md +40 -220
  68. package/skills/vercel-react-best-practices/SKILL.md +38 -283
  69. package/skills/video-production/SKILL.md +35 -206
@@ -1,304 +1,59 @@
1
1
  ---
2
2
  name: vercel-react-best-practices
3
- description: "React/Next.js performance optimization guide (based on Vercel engineering). Auto-activates when writing, reviewing, or refactoring React components. 45 rules across 8 priority categories."
3
+ description: "React/Next.js performance gotchas from Vercel engineering. Non-intuitive pitfalls that LLMs commonly miss."
4
4
  triggers: [react, next.js, performance, optimization, vercel, component, rendering]
5
5
  priority: 60
6
6
  ---
7
7
 
8
8
  # Vercel React Best Practices
9
9
 
10
- Comprehensive React/Next.js performance optimization guide from the Vercel engineering team. 45 rules organized into 8 categories with an impact-based priority system.
10
+ ## Pre-check (K1)
11
11
 
12
- ## When to Apply
12
+ > Is this a React/Next.js performance issue? Standard React patterns (useState, useEffect, components) don't need this skill. Activate only for performance optimization or code review.
13
13
 
14
- - When writing React components or Next.js pages
15
- - When implementing data fetching (client/server)
16
- - During performance-focused code reviews
17
- - When refactoring existing React/Next.js code
18
- - When optimizing bundle size or load times
14
+ ## CRITICAL Gotchas
19
15
 
20
- ## Priority System by Category
16
+ ### Waterfall Elimination
21
17
 
22
- | Priority | Category | Impact | Prefix |
23
- |----------|----------|--------|--------|
24
- | 1 | Waterfall Elimination | CRITICAL | `async-` |
25
- | 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
26
- | 3 | Server-side Performance | HIGH | `server-` |
27
- | 4 | Client Data Fetching | MEDIUM-HIGH | `client-` |
28
- | 5 | Re-render Optimization | MEDIUM | `rerender-` |
29
- | 6 | Rendering Performance | MEDIUM | `rendering-` |
30
- | 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
31
- | 8 | Advanced Patterns | LOW | `advanced-` |
18
+ | Gotcha | Why Non-obvious |
19
+ |--------|----------------|
20
+ | **Sequential awaits** | `const a = await f1(); const b = await f2();` creates waterfall. Use `Promise.all([f1(), f2()])` for independent ops |
21
+ | **Await placement** | Move `await` to the branch where value is actually used, not at declaration |
22
+ | **Missing Suspense** | Wrap slow server components in `<Suspense>` to stream — don't block entire page |
32
23
 
33
- ## Quick Reference — 45 Rule Index
24
+ ### Bundle Size
34
25
 
35
- ### 1. Waterfall Elimination (CRITICAL)
26
+ | Gotcha | Why Non-obvious |
27
+ |--------|----------------|
28
+ | **Barrel imports** | `import { Button } from "@/components"` pulls entire barrel. Use `import { Button } from "@/components/Button"` |
29
+ | **Third-party in initial bundle** | Load analytics/logging AFTER hydration with `next/dynamic` or lazy `useEffect` |
30
+ | **Heavy components** | Charts, editors, maps → `next/dynamic` with `{ ssr: false }` |
36
31
 
37
- | Rule | Description |
38
- |------|-------------|
39
- | `async-defer-await` | Move await to the branch where the value is actually used |
40
- | `async-parallel` | Use `Promise.all()` for independent operations |
41
- | `async-dependencies` | Use better-all for partial dependencies |
42
- | `async-api-routes` | Start Promises early, await late |
43
- | `async-suspense-boundaries` | Stream content with Suspense |
32
+ ## HIGH Gotchas
44
33
 
45
- **Key Example — `async-parallel` (CRITICAL):**
34
+ ### Server-side
46
35
 
47
- ```typescript
48
- // ❌ Bad: sequential execution → waterfall
49
- const user = await getUser(id);
50
- const posts = await getPosts(id);
51
- const comments = await getComments(id);
36
+ | Gotcha | Fix |
37
+ |--------|-----|
38
+ | Duplicate DB calls across server components | Wrap with `React.cache()` for per-request dedup |
39
+ | Large data serialized to client | Pick only needed fields before passing to client components |
40
+ | Blocking post-processing (logging, analytics) | Use `after()` for non-blocking tasks |
52
41
 
53
- // Good: parallel execution
54
- const [user, posts, comments] = await Promise.all([
55
- getUser(id),
56
- getPosts(id),
57
- getComments(id),
58
- ]);
59
- ```
42
+ ## MEDIUM Gotchas
60
43
 
61
- ### 2. Bundle Size Optimization (CRITICAL)
44
+ | Gotcha | Fix |
45
+ |--------|-----|
46
+ | Expensive computation re-runs on parent re-render | Isolate in `memo()` wrapped component |
47
+ | Static JSX recreated every render | Hoist outside component: `const HEADER = <header>...</header>` |
48
+ | Long lists render all items | `content-visibility: auto; contain-intrinsic-size: 0 80px;` on list items |
49
+ | `{count && <Item />}` renders `0` | Use ternary: `{count > 0 ? <Item /> : null}` |
50
+ | Event handler changes every render → effect re-runs | Store handlers in `useRef` for stable effects |
51
+ | Object in useEffect deps | Use primitive values (id, not entire object) as dependencies |
62
52
 
63
- | Rule | Description |
64
- |------|-------------|
65
- | `bundle-barrel-imports` | Avoid barrel files, use direct imports |
66
- | `bundle-dynamic-imports` | Use `next/dynamic` for heavy components |
67
- | `bundle-defer-third-party` | Load analytics/logging after hydration |
68
- | `bundle-conditional` | Load modules only when feature is enabled |
69
- | `bundle-preload` | Preload on hover/focus for perceived speed |
53
+ ## Done Criteria (K4)
70
54
 
71
- **Key Example `bundle-barrel-imports` (CRITICAL):**
72
-
73
- ```typescript
74
- // Bad: barrel import included in entire bundle
75
- import { Button } from "@/components";
76
-
77
- // ✅ Good: direct import → tree-shakeable
78
- import { Button } from "@/components/Button";
79
- ```
80
-
81
- ### 3. Server-side Performance (HIGH)
82
-
83
- | Rule | Description |
84
- |------|-------------|
85
- | `server-cache-react` | Deduplicate per-request with `React.cache()` |
86
- | `server-cache-lru` | Cross-request caching with LRU cache |
87
- | `server-serialization` | Minimize data passed to client components |
88
- | `server-parallel-fetching` | Redesign component structure for parallel fetching |
89
- | `server-after-nonblocking` | Non-blocking post-processing with `after()` |
90
-
91
- **Key Example — `server-cache-react` (HIGH):**
92
-
93
- ```typescript
94
- // ❌ Bad: duplicate DB calls within same request
95
- async function Layout() {
96
- const user = await getUser(); // call 1
97
- return <Header user={user}><Content /></Header>;
98
- }
99
- async function Content() {
100
- const user = await getUser(); // call 2 (duplicate)
101
- return <div>{user.name}</div>;
102
- }
103
-
104
- // ✅ Good: per-request deduplication with React.cache
105
- import { cache } from "react";
106
- const getUser = cache(async () => {
107
- return await db.user.findUnique({ where: { id: currentUserId } });
108
- });
109
- ```
110
-
111
- ### 4. Client Data Fetching (MEDIUM-HIGH)
112
-
113
- | Rule | Description |
114
- |------|-------------|
115
- | `client-swr-dedup` | Auto-deduplicate requests with SWR |
116
- | `client-event-listeners` | Prevent duplicate global event listener registration |
117
-
118
- **Key Example — `client-swr-dedup` (MEDIUM-HIGH):**
119
-
120
- ```typescript
121
- // ❌ Bad: duplicate API calls from multiple components
122
- function Header() {
123
- const [user, setUser] = useState(null);
124
- useEffect(() => { fetch("/api/user").then(r => r.json()).then(setUser); }, []);
125
- return <div>{user?.name}</div>;
126
- }
127
- function Sidebar() {
128
- const [user, setUser] = useState(null);
129
- useEffect(() => { fetch("/api/user").then(r => r.json()).then(setUser); }, []);
130
- return <div>{user?.email}</div>;
131
- }
132
-
133
- // ✅ Good: auto-deduplication + caching with SWR
134
- import useSWR from "swr";
135
- function useUser() {
136
- return useSWR("/api/user", fetcher);
137
- }
138
- function Header() {
139
- const { data: user } = useUser(); // same key → auto-deduplicated
140
- return <div>{user?.name}</div>;
141
- }
142
- function Sidebar() {
143
- const { data: user } = useUser(); // only 1 network request
144
- return <div>{user?.email}</div>;
145
- }
146
- ```
147
-
148
- ### 5. Re-render Optimization (MEDIUM)
149
-
150
- | Rule | Description |
151
- |------|-------------|
152
- | `rerender-defer-reads` | Avoid subscribing to state used only in callbacks |
153
- | `rerender-memo` | Isolate expensive computations in memoized components |
154
- | `rerender-dependencies` | Use primitive values for effect dependencies |
155
- | `rerender-derived-state` | Subscribe to derived booleans instead of raw data |
156
- | `rerender-functional-setstate` | Use functional setState for stable callbacks |
157
- | `rerender-lazy-state-init` | Pass functions for expensive initial values |
158
- | `rerender-transitions` | Use startTransition for non-urgent updates |
159
-
160
- **Key Example — `rerender-memo` (MEDIUM):**
161
-
162
- ```typescript
163
- // ❌ Bad: expensive computation re-runs on every parent re-render
164
- function Dashboard({ data, filter }) {
165
- const processed = expensiveProcess(data); // runs every time
166
- return <Chart data={processed} />;
167
- }
168
-
169
- // ✅ Good: isolate expensive computation in memoized component
170
- const ProcessedChart = memo(function ProcessedChart({ data }: { data: Data[] }) {
171
- const processed = expensiveProcess(data);
172
- return <Chart data={processed} />;
173
- });
174
- ```
175
-
176
- ### 6. Rendering Performance (MEDIUM)
177
-
178
- | Rule | Description |
179
- |------|-------------|
180
- | `rendering-animate-svg-wrapper` | Animate div wrapper instead of SVG elements |
181
- | `rendering-content-visibility` | Apply content-visibility to long lists |
182
- | `rendering-hoist-jsx` | Hoist static JSX outside components |
183
- | `rendering-svg-precision` | Reduce SVG coordinate precision |
184
- | `rendering-hydration-no-flicker` | Prevent client-only data flicker with inline scripts |
185
- | `rendering-activity` | Use Activity component for show/hide |
186
- | `rendering-conditional-render` | Use ternary instead of `&&` |
187
-
188
- **Key Example — `rendering-content-visibility` (MEDIUM):**
189
-
190
- ```css
191
- /* ❌ Bad: render all items in a long list immediately */
192
- .list-item {
193
- /* default: render all items */
194
- }
195
-
196
- /* ✅ Good: defer rendering for off-viewport items */
197
- .list-item {
198
- content-visibility: auto;
199
- contain-intrinsic-size: 0 80px;
200
- }
201
- ```
202
-
203
- **Key Example — `rendering-hoist-jsx` (MEDIUM):**
204
-
205
- ```typescript
206
- // ❌ Bad: static JSX recreated on every re-render
207
- function Page({ data }) {
208
- return (
209
- <div>
210
- <header><h1>My App</h1><nav>...</nav></header>
211
- <main>{data.map(item => <Card key={item.id} {...item} />)}</main>
212
- </div>
213
- );
214
- }
215
-
216
- // ✅ Good: hoist static JSX outside component
217
- const HEADER = <header><h1>My App</h1><nav>...</nav></header>;
218
-
219
- function Page({ data }) {
220
- return (
221
- <div>
222
- {HEADER}
223
- <main>{data.map(item => <Card key={item.id} {...item} />)}</main>
224
- </div>
225
- );
226
- }
227
- ```
228
-
229
- ### 7. JavaScript Performance (LOW-MEDIUM)
230
-
231
- | Rule | Description |
232
- |------|-------------|
233
- | `js-batch-dom-css` | Batch CSS changes via class or cssText |
234
- | `js-index-maps` | Index repeated lookups with Map |
235
- | `js-cache-property-access` | Cache object property access in loops |
236
- | `js-cache-function-results` | Cache function results in module-level Map |
237
- | `js-cache-storage` | Cache localStorage/sessionStorage reads |
238
- | `js-combine-iterations` | Combine multiple filter/map into single loop |
239
- | `js-length-check-first` | Check array length before expensive comparisons |
240
- | `js-early-exit` | Early return from functions |
241
- | `js-hoist-regexp` | Hoist RegExp creation out of loops |
242
- | `js-min-max-loop` | Calculate min/max with loop instead of sort |
243
- | `js-set-map-lookups` | Use Set/Map for O(1) lookups |
244
- | `js-tosorted-immutable` | Use toSorted() for immutable sorting |
245
-
246
- ### 8. Advanced Patterns (LOW)
247
-
248
- | Rule | Description |
249
- |------|-------------|
250
- | `advanced-event-handler-refs` | Store event handlers in refs |
251
- | `advanced-use-latest` | Use useLatest for stable callback refs |
252
-
253
- **Key Example — `advanced-event-handler-refs` (LOW):**
254
-
255
- ```typescript
256
- // ❌ Bad: event handler changes every render → effect re-runs
257
- function useInterval(callback: () => void, delay: number) {
258
- useEffect(() => {
259
- const id = setInterval(callback, delay);
260
- return () => clearInterval(id);
261
- }, [callback, delay]); // callback changes every time
262
- }
263
-
264
- // ✅ Good: ref holds latest handler → stable effect
265
- function useInterval(callback: () => void, delay: number) {
266
- const savedCallback = useRef(callback);
267
- useEffect(() => { savedCallback.current = callback; });
268
- useEffect(() => {
269
- const id = setInterval(() => savedCallback.current(), delay);
270
- return () => clearInterval(id);
271
- }, [delay]); // only depends on delay
272
- }
273
- ```
274
-
275
- ## Rule Count Summary by Category
276
-
277
- | Category | Rules | Impact |
278
- |----------|-------|--------|
279
- | Waterfall Elimination | 5 | CRITICAL |
280
- | Bundle Size Optimization | 5 | CRITICAL |
281
- | Server-side Performance | 5 | HIGH |
282
- | Client Data Fetching | 2 | MEDIUM-HIGH |
283
- | Re-render Optimization | 7 | MEDIUM |
284
- | Rendering Performance | 7 | MEDIUM |
285
- | JavaScript Performance | 12 | LOW-MEDIUM |
286
- | Advanced Patterns | 2 | LOW |
287
- | **Total** | **45** | |
288
-
289
- ## Usage
290
-
291
- This skill provides a Quick Reference index and key examples per category. For detailed explanations and full code examples of each rule, refer to the CCPP AGENTS.md.
292
-
293
- ### Priority-Based Application Strategy
294
-
295
- 1. **CRITICAL (Priority 1-2)**: Must apply in all projects
296
- 2. **HIGH (Priority 3)**: Apply when using server components
297
- 3. **MEDIUM (Priority 4-6)**: Review first when performance issues arise
298
- 4. **LOW (Priority 7-8)**: Selectively apply during optimization phase
299
-
300
- ### VIBE Tool Integration
301
-
302
- - `core_analyze_complexity` — Analyze component complexity
303
- - `core_validate_code_quality` — Validate code quality
304
- - `/vibe.review` — 13+ agent parallel review (includes performance-reviewer)
55
+ - [ ] No sequential awaits for independent operations
56
+ - [ ] No barrel imports for tree-shakeable modules
57
+ - [ ] Server component data is `React.cache()`-wrapped where reused
58
+ - [ ] Heavy third-party loaded after hydration
59
+ - [ ] Long lists use `content-visibility: auto`
@@ -1,222 +1,51 @@
1
1
  ---
2
2
  name: video-production
3
- description: "Video processing and production patterns — FFmpeg, transcoding, streaming, subtitles."
3
+ description: "Video processing gotchas — FFmpeg, transcoding, streaming, subtitles."
4
4
  triggers: [video, ffmpeg, transcode, encode, stream, media, subtitle, thumbnail, hls, dash]
5
5
  priority: 60
6
6
  ---
7
7
 
8
- # Video Production Skill
8
+ # Video Production
9
9
 
10
- FFmpeg 기반 비디오 처리, 트랜스코딩, 스트리밍 구성, 자막 처리 패턴 가이드.
10
+ ## Pre-check (K1)
11
11
 
12
- ## Core Concepts
12
+ > Are you processing video files programmatically? If just embedding a YouTube/Vimeo player, this skill is not needed.
13
13
 
14
- ### FFmpeg CLI Wrapper Pattern
14
+ ## Gotchas
15
15
 
16
- FFmpeg 호출은 반드시 래퍼를 통해 수행한다. 직접 CLI 문자열을 조합하지 않는다.
16
+ | Gotcha | Consequence | Prevention |
17
+ |--------|-------------|------------|
18
+ | Direct CLI string concatenation | Command injection risk | Always use wrapper library (fluent-ffmpeg for TS, ffmpeg-python for Python) |
19
+ | No input validation | Crash on corrupted files | Always `ffprobe` input before processing — check codec, resolution, duration |
20
+ | No temp file cleanup | Disk fills up silently | `try/finally` or cleanup handler — never leave partial outputs |
21
+ | No progress callback | Long encoding appears frozen | Implement progress events for any operation >10s |
22
+ | Memory loading large files | OOM on 4K+ video | Use streaming I/O, never read entire file into memory |
23
+ | Assuming codec availability | Fails on different FFmpeg builds | Check `ffmpeg -codecs` at runtime before encoding |
24
+ | Fixed bitrate encoding | Inconsistent quality | Use CRF-based quality (18-28 for H.264) instead |
25
+ | No timeout | Encoding hangs forever | Set timeout + kill process on expiry |
17
26
 
18
- **TypeScript (fluent-ffmpeg):**
19
- ```typescript
20
- import ffmpeg from 'fluent-ffmpeg';
27
+ ## Codec Quick Reference
21
28
 
22
- function transcodeVideo(
23
- input: string,
24
- output: string,
25
- options: TranscodeOptions,
26
- ): Promise<void> {
27
- return new Promise((resolve, reject) => {
28
- ffmpeg(input)
29
- .outputOptions(buildOutputOptions(options))
30
- .output(output)
31
- .on('end', resolve)
32
- .on('error', reject)
33
- .run();
34
- });
35
- }
36
- ```
29
+ | Use Case | Codec | Note |
30
+ |----------|-------|------|
31
+ | Maximum compatibility | H.264 (libx264) | CRF 23 default |
32
+ | Smaller files | H.265 (libx265) | 50% smaller, slower, licensing issues |
33
+ | Open source | VP9 (libvpx-vp9) | Good for WebM |
34
+ | Best compression | AV1 (libaom-av1) | Very slow encoding |
37
35
 
38
- **Python (ffmpeg-python):**
39
- ```python
40
- import ffmpeg
36
+ ## Resolution Presets
41
37
 
42
- def transcode_video(
43
- input_path: str,
44
- output_path: str,
45
- codec: str = "libx264",
46
- crf: int = 23,
47
- ) -> None:
48
- (
49
- ffmpeg
50
- .input(input_path)
51
- .output(output_path, vcodec=codec, crf=crf)
52
- .overwrite_output()
53
- .run(capture_stdout=True, capture_stderr=True)
54
- )
55
- ```
38
+ | Preset | Resolution | Bitrate (H.264) |
39
+ |--------|-----------|-----------------|
40
+ | 360p | 640x360 | 800 kbps |
41
+ | 720p | 1280x720 | 3 Mbps |
42
+ | 1080p | 1920x1080 | 6 Mbps |
43
+ | 4K | 3840x2160 | 15 Mbps |
56
44
 
57
- ### Error Handling
45
+ ## Done Criteria (K4)
58
46
 
59
- 비디오 처리는 실패할 있다. 반드시 에러를 처리한다.
60
-
61
- | 에러 유형 | 원인 | 대응 |
62
- |-----------|------|------|
63
- | Codec not found | FFmpeg 빌드에 코덱 미포함 | 런타임 시 코덱 가용성 확인 |
64
- | Out of memory | 큰 파일 + 고해상도 | 스트리밍 처리, chunk 분할 |
65
- | Corrupted input | 깨진 소스 파일 | `ffprobe`로 사전 검증 |
66
- | Permission denied | 파일 잠금 | 임시 디렉토리 사용, 정리 보장 |
67
- | Timeout | 장시간 인코딩 | progress 콜백 + 타임아웃 설정 |
68
-
69
- ```typescript
70
- // 입력 파일 사전 검증
71
- async function probeVideo(filePath: string): Promise<VideoMetadata> {
72
- return new Promise((resolve, reject) => {
73
- ffmpeg.ffprobe(filePath, (err, data) => {
74
- if (err) return reject(new Error(`Invalid video: ${err.message}`));
75
- const video = data.streams.find(s => s.codec_type === 'video');
76
- if (!video) return reject(new Error('No video stream found'));
77
- resolve({
78
- width: video.width ?? 0,
79
- height: video.height ?? 0,
80
- duration: Number(data.format.duration ?? 0),
81
- codec: video.codec_name ?? 'unknown',
82
- bitrate: Number(data.format.bit_rate ?? 0),
83
- });
84
- });
85
- });
86
- }
87
- ```
88
-
89
- ## Common Operations
90
-
91
- ### Thumbnail Generation
92
-
93
- ```typescript
94
- function extractThumbnail(
95
- input: string,
96
- output: string,
97
- timeSeconds: number = 1,
98
- ): Promise<void> {
99
- return new Promise((resolve, reject) => {
100
- ffmpeg(input)
101
- .screenshots({
102
- timestamps: [timeSeconds],
103
- filename: path.basename(output),
104
- folder: path.dirname(output),
105
- size: '320x240',
106
- })
107
- .on('end', resolve)
108
- .on('error', reject);
109
- });
110
- }
111
- ```
112
-
113
- ### Resolution Presets
114
-
115
- | Preset | Resolution | Bitrate (H.264) | Use Case |
116
- |--------|-----------|-----------------|----------|
117
- | 360p | 640x360 | 800 kbps | Mobile preview |
118
- | 480p | 854x480 | 1.5 Mbps | Standard mobile |
119
- | 720p | 1280x720 | 3 Mbps | HD streaming |
120
- | 1080p | 1920x1080 | 6 Mbps | Full HD |
121
- | 4K | 3840x2160 | 15 Mbps | Ultra HD |
122
-
123
- ### Codec Selection
124
-
125
- | Codec | Format | Pros | Cons |
126
- |-------|--------|------|------|
127
- | H.264 (libx264) | MP4 | Universal compatibility | Larger file size |
128
- | H.265 (libx265) | MP4 | 50% smaller than H.264 | Slower encoding, licensing |
129
- | VP9 (libvpx-vp9) | WebM | Open source, good quality | Slow encoding |
130
- | AV1 (libaom-av1) | MP4/WebM | Best compression | Very slow encoding |
131
-
132
- ### HLS Streaming
133
-
134
- ```typescript
135
- function generateHLS(
136
- input: string,
137
- outputDir: string,
138
- variants: HLSVariant[] = DEFAULT_VARIANTS,
139
- ): Promise<void> {
140
- return new Promise((resolve, reject) => {
141
- const cmd = ffmpeg(input);
142
- for (const v of variants) {
143
- cmd
144
- .output(path.join(outputDir, `${v.name}.m3u8`))
145
- .outputOptions([
146
- `-vf scale=${v.width}:${v.height}`,
147
- `-b:v ${v.bitrate}`,
148
- '-hls_time 6',
149
- '-hls_list_size 0',
150
- '-hls_segment_filename',
151
- path.join(outputDir, `${v.name}_%03d.ts`),
152
- ]);
153
- }
154
- cmd.on('end', resolve).on('error', reject).run();
155
- });
156
- }
157
- ```
158
-
159
- ### Subtitle Processing
160
-
161
- ```typescript
162
- // SRT → VTT 변환
163
- function srtToVtt(srtContent: string): string {
164
- const vtt = srtContent
165
- .replace(/\r\n/g, '\n')
166
- .replace(/(\d{2}):(\d{2}):(\d{2}),(\d{3})/g, '$1:$2:$3.$4');
167
- return `WEBVTT\n\n${vtt}`;
168
- }
169
-
170
- // 자막 burn-in (하드코딩)
171
- function burnSubtitles(
172
- input: string,
173
- subtitleFile: string,
174
- output: string,
175
- ): Promise<void> {
176
- return new Promise((resolve, reject) => {
177
- ffmpeg(input)
178
- .outputOptions([`-vf subtitles=${subtitleFile}`])
179
- .output(output)
180
- .on('end', resolve)
181
- .on('error', reject)
182
- .run();
183
- });
184
- }
185
- ```
186
-
187
- ### Watermark
188
-
189
- ```typescript
190
- function addWatermark(
191
- input: string,
192
- watermark: string,
193
- output: string,
194
- position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' = 'bottom-right',
195
- ): Promise<void> {
196
- const overlayMap: Record<string, string> = {
197
- 'top-left': '10:10',
198
- 'top-right': 'W-w-10:10',
199
- 'bottom-left': '10:H-h-10',
200
- 'bottom-right': 'W-w-10:H-h-10',
201
- };
202
- return new Promise((resolve, reject) => {
203
- ffmpeg(input)
204
- .input(watermark)
205
- .complexFilter([`overlay=${overlayMap[position]}`])
206
- .output(output)
207
- .on('end', resolve)
208
- .on('error', reject)
209
- .run();
210
- });
211
- }
212
- ```
213
-
214
- ## Best Practices
215
-
216
- 1. **항상 ffprobe로 입력 검증** — 처리 전에 코덱, 해상도, 무결성 확인
217
- 2. **임시 파일 정리 보장** — try/finally 또는 cleanup handler 사용
218
- 3. **Progress 콜백 구현** — 장시간 작업에 진행률 피드백
219
- 4. **스트리밍 처리 선호** — 대용량 파일은 메모리에 올리지 않음
220
- 5. **코덱 가용성 런타임 확인** — `ffmpeg -codecs`로 빌드 지원 확인
221
- 6. **CRF 기반 품질 제어** — 비트레이트 고정보다 CRF (18-28) 사용
222
- 7. **하드웨어 가속 활용** — NVIDIA NVENC, Intel QSV, Apple VideoToolbox 가용 시 사용
47
+ - [ ] All FFmpeg calls go through wrapper library (no raw CLI strings)
48
+ - [ ] Input files validated with ffprobe before processing
49
+ - [ ] Temp files cleaned up in all paths (success + error)
50
+ - [ ] Progress reporting for long operations
51
+ - [ ] Codec availability checked at runtime