# Web Demo Video

> 화면 녹화 도구 없이 웹사이트·웹앱의 제품 데모 영상(SNS용 1:1, 9:16, 16:9)을 만든다. 실제 사이트를 iframe에 띄워 진짜 마우스 이벤트로 조작하며 프레임을 캡처하고 ffmpeg로 합성한다.

- **Type:** Skill
- **Install:** `agentstack add skill-bam-bam-2-solo-skills-web-demo-video`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [bam-bam-2](https://agentstack.voostack.com/s/bam-bam-2)
- **Installs:** 0
- **Category:** [Content & Media](https://agentstack.voostack.com/c/content-and-media)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [bam-bam-2](https://github.com/bam-bam-2)
- **Source:** https://github.com/bam-bam-2/solo-skills/tree/main/skills/web-demo-video

## Install

```sh
agentstack add skill-bam-bam-2-solo-skills-web-demo-video
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# 웹 데모 영상 만들기

사용자의 웹 프로젝트(모자이크, 커뮤니티앱, 카공메이트 등) 소개 영상을 만들 때 쓰는 절차.
목업 이미지를 이어붙이는 게 아니라 **실제 사이트를 실제로 조작한 화면**을 찍는 게 핵심이다.

## 먼저 물어볼 것

영상은 어디 쓰느냐로 비율·길이가 갈린다. **만들기 전에 반드시 확인한다.**

| 용도 | 비율 | 길이 |
|---|---|---|
| 스레드·릴스·쇼츠 | 9:16 (1080x1920) | 15~25초 |
| 블로그·유튜브 임베드·랜딩 | 16:9 (1920x1080) | 30~40초 |
| 인스타 피드·카톡 공유 | 1:1 (1080x1080) | ~20초 |

사용자는 시각 산출물의 **형식**에 구체적인 기대가 있다. 레퍼런스가 있으면 먼저 달라고 한다.

## 절차

### 1. 로컬 서버로 대상 사이트를 띄운다

```bash
cd  && python3 -m http.server 8899
```

무대(stage) 페이지와 대상 사이트는 **반드시 같은 오리진**이어야 한다.
포트가 다르면 `iframe.contentDocument` 접근이 막혀서 조작이 불가능하다.
→ 무대 파일을 프로젝트 폴더 안에 임시로 두고, **작업이 끝나면 반드시 빼낸다**(안 그러면 다음 배포에 딸려 올라간다).

### 2. 무대 페이지를 만든다

`assets/stage-template.html`를 복사해서 고친다. 구조:

- `#stage` — 출력 비율 그대로의 고정 크기 div (1:1이면 540x540, dpr 2로 캡처하면 1080x1080)
- `#cap` / `#foot` — 큰 자막(상단) / 보조 문구(하단)
- `#win` — 맥 브라우저 창 목업(신호등 3개 + URL) 안에 ``을 `transform: scale()`로 축소해 넣음
- `#cursor` — 가짜 마우스 커서 SVG. **이게 있어야 조작 영상처럼 보인다**
- `#ring` — 클릭·브러시 크기를 보여주는 하이라이트 원
- `#end` — 엔딩 카드(제품명 / 한줄 설명 / 셀링포인트 3개 / URL)
- `window.__tick(frameIndex)` — 프레임 번호 하나를 받아 그 시점의 화면 상태를 만드는 **결정론적** 함수

### 3. 타임라인을 결정론적으로 짠다

`__tick(frame)` 안에서 `t = frame / FPS`로 시간을 계산하고, 모든 상태를 t로부터 다시 계산한다.
실시간 `setTimeout`을 쓰면 캡처 속도와 어긋나서 망가진다.

- 1회성 동작(이미지 로드, 모드 전환)은 `once(key, fn)` 으로 중복 실행을 막는다
- 드래그는 `drag(t, 시작초, 끝초, ptFn)` — 시작 프레임에 `mousedown`, 매 프레임 `mousemove`, 끝나면 `mouseup`
- 이벤트는 iframe의 `W.MouseEvent`로 만들어 `clientX/clientY`(iframe 기준 좌표)로 dispatch한다.
  `mousedown`은 대상 요소에, `mousemove`/`mouseup`은 보통 `window`에 붙어 있으니 확인하고 각각 맞는 타깃에 쏜다
- 좌표는 하드코딩하지 말고 **요소의 `getBoundingClientRect()` 또는 그려넣은 목업의 rect 비율**로 계산한다. 창 크기를 바꿔도 안 깨진다
- iframe 안에서 영상에 불필요한 요소(헤더, SEO 본문, 푸터, 광고 배너, 상태줄)는 `iframe` load 시점에 `` 주입으로 숨긴다

### 4. 프레임을 캡처한다

```js
for (let i = from; i  __tick(f), i);
  const buf = await page.screenshot({ clip: { x:0, y:0, width:540, height:540 } });
  await fs.writeFile(`${DIR}/f${String(i).padStart(4,'0')}.png`, buf);
}
```

- **`clip`은 CSS 픽셀 단위다.** devicePixelRatio를 곱하면 안 된다. dpr 2면 540 클립이 1080px 이미지로 나온다
- 프레임 한 장에 약 0.09초. 400장이면 40초 → REPL 120초 제한 때문에 **100~150장씩 나눠 호출**한다
- 캡처 시작 전에 페이지를 새로 로드해 상태를 초기화하고, `__tick(0)` 후 이미지 로드가 끝날 시간을 준다

### 5. ffmpeg로 합성한다

`ffmpeg`는 PATH에 없다. `export PATH="$PATH:/opt/homebrew/bin"` 를 붙인다.

```bash
ffmpeg -y -framerate 20 -i frames/f%04d.png \
  -vf "scale=1080:1080:flags=lanczos,fps=30" \
  -c:v libx264 -profile:v high -pix_fmt yuv420p -crf 20 -movflags +faststart out.mp4
```

- `-pix_fmt yuv420p`는 SNS 호환 필수
- 채팅에 붙여 보여줄 미리보기 GIF는 palettegen/paletteuse 2패스로:
  `fps=10,scale=420:-1:flags=lanczos,palettegen=stats_mode=diff` → `paletteuse=dither=bayer:bayer_scale=3`
- 콘티 확인용 컨택트시트: `select='eq(n\,20)+...',scale=340:340,tile=3x3`

### 6. 뒷정리

- 무대 파일을 프로젝트 폴더 밖으로 옮긴다 (배포에 섞이면 안 됨)
- 로컬 서버 종료: `lsof -ti:8899 | xargs kill -9`
- 캡처 탭 닫기

## 주의

- 영상에 쓰는 예시 데이터(대화 내용, 이름, 계좌번호, 전화번호, 주소)는 **전부 지어낸 가짜**로 만들고, 보고할 때 가짜라고 명시한다. 사용자의 실제 개인정보(전세금 등)나 제3자 정보는 절대 넣지 않는다
- 무음이 기본. 음악이 필요하면 사용자에게 확인받는다
- 완성본은 세션 artifacts에 두되, 다시 뽑을 수 있도록 **무대 파일을 영구 경로에 보관**한다

---

## 부록 · 블로그 본문용 GIF (브라우저 목업 없이)

SNS 영상과 목적이 다르다. 블로그 독자는 **"이 도구가 뭘 하는지"**를 3초 안에 봐야 하므로,
맥 브라우저 창 목업·엔딩 카드 없이 **작업 대상만 크게** 보여주는 편이 낫다.
무대(stage) 페이지도 로컬 서버도 필요 없다. 배포된 사이트를 직접 조작하고 캔버스를 그대로 뜬다.

### 절차

1. 배포 사이트를 열고 예시 이미지를 붙여넣기 이벤트로 주입한다(파일 선택 UI를 거치지 않아도 된다).
   ```js
   await page.evaluate(async (b64) => {
     const blob = await (await fetch('data:image/png;base64,'+b64)).blob();
     const dt = new DataTransfer(); dt.items.add(new File([blob],'s.png',{type:'image/png'}));
     document.dispatchEvent(new ClipboardEvent('paste',{clipboardData:dt,bubbles:true}));
   }, b64);
   ```
2. 영상에 불필요한 요소는 캡처 전에 숨긴다 (`adInCanvas` 같은 광고 영역).
3. **좌표는 반드시 픽셀로 탐지한다. 눈대중 금지.** 이번에 눈대중으로 잡았다가 펜·채우기가 엉뚱한 데
   찍혀서 세 번 다시 찍었다. 캔버스 픽셀을 직접 훑어 대상 글자의 bbox를 구한다.
   ```js
   const bbox = (x0,x1,y0,y1) => { /* 어두운 픽셀(r,g,b` + `loading="lazy"` + 캡션에 "예시 데이터는 전부 가짜"를 명시

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [bam-bam-2](https://github.com/bam-bam-2)
- **Source:** [bam-bam-2/solo-skills](https://github.com/bam-bam-2/solo-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** yes
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-bam-bam-2-solo-skills-web-demo-video
- Seller: https://agentstack.voostack.com/s/bam-bam-2
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
