何が配られたのか
Zoom Video SDK に、AI コーディングツール向けのドキュメントが同梱されるようになりました。
- Video SDK for Web — バージョン 2.5.0 から。npm パッケージに agent skill と JSON 型定義が入り、CLI でインストールする
-
ネイティブ SDK(Android / iOS / Linux / macOS / Windows) — バージョン 2.5.7 から。パッケージ内の
Docs/フォルダに同梱される
公式ドキュメントは Using AI assistants with the Video SDK にあります。
この記事では Web 版を扱います。導入はコマンド一発です。
npm install @zoom/videosdk
npx @zoom/videosdk install-skill --target claude # cursor | codex | all
実行するとこうなります。
Installed Video SDK skill (claude) → /path/to/project/.claude/skills/video-sdk-web
Done. Restart Cursor / Claude Code / Codex if it is already open.
展開されたのは 46 ファイル。SKILL.md 本体、references/ が 45 本(認証、セッション、イベント、音声、映像、画面共有、チャット、録画、フレームワーク統合、ブラウザ対応、エラーコード)、troubleshooting/ が 1 本です。( 2.5.0 時点)
--global を付ければホームディレクトリに入れて全プロジェクトで使えます。
さて、これを入れると実際どう変わるのか。同じプロンプトを投げて比べてみました。
検証条件
何と何を比べるか
| SDK | 同梱物 | |
|---|---|---|
| 選手A | 2.4.7 |
.d.ts 型定義のみ。skill 同梱なし |
| 選手B | 2.5.0 | agent skill + JSON 型定義 + .d.ts
|
2.4.7 には skill/ も bin/(CLI 本体)も dist/types-json/ もありません。dist/types/*.d.ts だけがあります。
バージョンが違うから比較できなくない?
型定義の差分を実測しました。
| ファイル | 2.4.7 | 2.5.0 | 差分行数 |
|---|---|---|---|
videoclient.d.ts |
1304 行 | 1304 行 | 0 |
media.d.ts |
3157 行 | 3176 行 | 19 |
event-callback.d.ts |
2459 行 | 2462 行 | 17 |
attachVideo / detachVideo / attachShareView などのコア API は 2.4.7 の型定義にも全て存在します。API 面はほぼ同一です。
Web 検索は両選手ともオン
片方だけ切ると変数が 2 つ動いてしまうため、両方オンにしました。
そもそも skill の references/ は、各ファイルの先頭に
<!-- Source: https://developers.zoom.us/docs/video-sdk/web/... -->
<!-- Fetched: 2026-06-15 -->
というヘッダーが付いた公式ドキュメントのスナップショットです。情報そのものは Web にもある。だから差が出るとすれば、それは「情報の有無」ではなく「必ず読ませる構造」の効果だと言えます。
環境の隔離でハマった話
ここが地味に一番時間を食いました。
Claude Code はセッションを跨いで知識を運ぶ仕組みを持っています。CLAUDE.md と auto memory です。これが残っていると「先攻が学んだ API 名を後攻が盗み見」かねません。
そこで HOME を差し替えて起動しました。
HOME=/tmp/clean-home claude
ところが /memory を叩くと、まだ /Users/xxx/.claude/CLAUDE.md が読み込まれている。
原因は HOME ではなく cwd でした。Claude Code は現在ディレクトリから親を遡って CLAUDE.md を集めます。プロジェクトを ~/SDK/VideoSDK/Web に置いていたので、/Users/xxx まで登って拾っていたわけです。
プロジェクトを /tmp 配下に移したら消えました。
mkdir -p /tmp/vsdk-lab/01247 && cd /tmp/vsdk-lab/01247
HOME=/tmp/clean-home claude
/memory が空になったことを確認してから実験開始。選手A と選手B で HOME も別々にしています。
ただの余談ですが、Claude Code のドキュメントには「CLAUDE.md は文脈であって強制設定ではない(treats them as context, not enforced configuration)」と明記されています。つまりプロンプトで「メモリを参照しないで」と指示しても意味がありません。物理的に隔離するしかないです。
投げたプロンプト
両選手に一字一句同じものを投げました。取り急ぎミニマムの比較ということでシンプルにアプリ部のみ。認証なし。しかもビデオと音声のみ。画面共有すらなし。
Zoom Video SDK for Web を使って、最小構成のビデオ会議 Web アプリを実装してください。
## 環境
- TypeScript + Vite、フレームワークなしの Vanilla DOM
- パッケージ: @zoom/videosdk (node_modules に導入済み)
- 署名済みの Video SDK JWT、セッション名、表示名は .env から読み込む
(JWT の生成処理は実装対象外)
## 機能要件
1. セッションへの参加と退出
2. マイクの開始とミュート切り替え
3. カメラの開始と停止
4. 自分と他の参加者、全員の映像を画面に表示する
5. 参加順に関わらず、セッションにいる全員の映像が表示されること
(後から参加した場合も、先にいた参加者の映像が見えること)
6. 参加者名の一覧表示
7. 現在の接続状態を UI に表示すること
切断中・再接続中・セッション終了をユーザーが判別できること
8. 退出時にイベントリスナーと DOM を後片付けすること
## 実装条件
- UI テキストとコード内コメントは英語
- ボタン連打による二重実行を防ぐこと
- エラーは原因が分かる形でユーザーに提示すること
- Chrome 2 タブで同一セッションに参加して動作確認できる状態にすること
## 出力
実装ファイル一式と起動手順。
最後に、使用した API・イベント名・DOM 要素について、
公式に存在することを確認できたものと、確認できていないものを区別して報告してください。
API 名・イベント名・DOM 要素名は一切与えていません。 要件5と要件7は、機能要件としては自然な日本語ですが、満たすには特定の API とイベントを知っている必要があります。
「推測で書くな」とも書いていません。これも skill に記述済みだったので前提としては与えず。最後の「確認できたもの / できていないものを区別して報告」が答案用紙になります。
skill には次の一文があります。
Never invent SDK API surface. Do not guess, recall, or "reconstruct" method names, event strings, enum values... This rule overrides convenience and speed.
これが効いているかどうかは、コードだけでなく報告の書き方にも出てきそうです。
結果:面白かった違い
1. レンダリング先の設計が根本的に違った
これが最大の差でした。
選手A は参加者ごとに「タイル」 <div class="tile"> を作り、その中に「コンテナ」 <video-player-container> を動的生成します。参加者が増えるたびに container が増えます。
選手B は index.html に container を 1 つだけ静的配置し、そこへ <video-player> を追加していきます。
<!--
Single, stable video-player-container reused for the whole session.
The SDK owns its rendering surface, so we never destroy/recreate it.
-->
<video-player-container id="video-grid" class="video-grid"></video-player-container>
コメントで理由まで書いています。skill にはこう書かれています。
In React/Vue, do not place it behind conditional rendering that destroys it — keep it mounted and toggle visibility with CSS
video-player-container を破棄・再生成すると、ブラウザのレンダリングサーフェス上限を食い潰して映像が出なくなる、という話です。skill の記述が設計判断としてそのまま転写されているのがわかります。
実際、参加者が 2 人 → 4 人 → 2 人と変動しても container は作り直されず、<video-player> が足し引きされるだけでした。CSS の flex: 0 0 calc(50% - 4px) が効いて 2 列 → 2×2 → 2 列と綺麗に並びます。(画像は加工しております)
選手A の方式では、ここで container の生成・破棄が走り回っていました。
2. 報告の質が段違いだった
選手A の自己申告は、ファイル冒頭のコメント 1 行だけでした。
All identifiers used against the SDK (methods, events, payload fields) were taken from the bundled type definitions in
node_modules/@zoom/videosdk/dist/types. (SDKで使用されるすべての識別子(メソッド、イベント、ペイロードフィールド)は、node_modules/@zoom/videosdk/dist/typesにバンドルされている型定義から取得されました)
対して選手B は、シンボルごとに行番号付きの検証表を出してきました。
| Symbol | Source file |
|---|---|
ZoomVideo.createClient() |
zoomvideo.d.ts:18 |
client.join(topic, token, userName, sessionPassword?) |
videoclient.d.ts:1151 |
stream.attachVideo(userId, VideoQuality, element?) |
media.d.ts:1850 |
VideoQuality.Video_360P (=2) |
common.d.ts:361 |
さらに面白いのがこの 2 行です。
Corrected during implementation (initially guessed, then fixed)
stream.getAudioTrack()— does not exist in v2.5.0. Replaced with the verifiedgetCurrentUserInfo().audio === 'computer'check.Participantas a named import — it is not exported from the package.
「一度推測で実装しようとして、型定義で照合して間違いに気づき、直した」というプロセスを自己開示している。
getAudioTrack() は存在しないメソッドです。代わりに使ったのは Participant の audio フィールド('' | 'computer' | 'phone' の 3 値)で、'computer' ならコンピュータオーディオで参加済み、という判定です。
「推測するな」という指示が、コードだけでなく出力の書き方まで変えたわけです。
3. .env に無い変数を、型定義から補完してきた
これは偶然見つかった話です。
選手A のとき .env.example がアクセス権ルール( .env や .env.example は読み込ませない、というポリシールール)の都合で読めていませんでした。なので当然ながら必須となるトピック、ユーザー名、そしてトークンの3つだけを .env から読み込む実装となっていました。そこで選手B では env.example.txt にリネームして確実に読ませたのですが、パスコードの変数は条件を同じくするために、あえて入れませんでした。
にもかかわらず、選手B のコードにはこう書かれていました。
const PASSCODE = import.meta.env.VITE_ZOOM_SESSION_PASSCODE ?? "";
// ...
await client.join(TOPIC, TOKEN, USER_NAME, PASSCODE);
.env に無いものを補完しています。参照元は client.join(topic, token, userName, sessionPassword?) という型定義のシグネチャしかありません。空文字フォールバックまで入れて、あれば渡す・なければ渡らない、という作りになっています。えらい。とても偉い。
4. 二重クリック防止が「問題ごと消えた」
選手A も二重クリック防止は実装していました。guard() という関数でボタンを disabled にする方式です。ただし復元ロジックにバグがあり、join() が意図的に設定した disabled を finally が上書きしてしまい、セッション中なのに Join ボタンが押せる状態になっていました。片手オチ感がありますね。
選手B は違うアプローチを取りました。
function syncButtons(): void {
els.btnJoin.disabled = inSession || busy.join;
els.btnLeave.disabled = !inSession || busy.leave;
// ...
}
inSession と busy.* から毎回状態を導出する設計です。ボタンの状態を個別に書き換える箇所が存在しないので、上書きバグが起きようがない。「二重クリック対策」というより「状態管理の設計」で解決しています。
5. ダークテーマへのこだわり(趣味?)
選手B の CSS にこんな箇所があります。
/*
* The SDK renders video onto <video-player> children of this container.
* Keep it transparent (no opaque background) so rendered video is visible.
*/
video-player-container.video-grid {
background: #0d0f12; /* outer letterbox only; players sit on top */
}
「透明に保て」と自分でコメントに書いた直後に不透明色を敷いています。
skill には「container / player に不透明な背景を敷くと映像が塗り潰される」という警告があり、それを認識した上で「video-player は container の直接の子だから必ず手前に来る。だから letterbox としてなら問題ない」と判断して逸脱している。実際、映像はきちんと表示されました。
盲目的に従うのでも無視するのでもなく、根拠を書いてから逸脱する。地味に感心したポイントです。今回 Claude Code を使ったんですが、Claude さんの趣味ということにしておきます。
採点表
skill 自身が持っている "Common implementation checks" をベースに評価軸を作り、実験前に固定しました。
| # | 項目 | 選手A | 選手B |
|---|---|---|---|
| 1 |
init() → join() の順序 |
○ | ○ |
| 2 | join 後に getMediaStream()
|
○ | ○ |
| 3 |
attachVideo() の戻り値を container に append |
○ | ○ |
| 4 |
video-player に CSS で寸法 |
○ | ○ |
| 5 | 不透明 background を敷いていない | × | ○(上記の通り) |
| 6 |
peer-video-state-change をバインド |
○ | ○ |
| 7 | join 後 getAllUser() で既存参加者の映像を復元 |
○ | ○ |
| 8 |
connection-change の 3 状態を処理 |
○ | ○ + errorCode も表示 |
| 9 |
active-media-failed をバインド |
× | ○ |
| 10 | 二重クリック防止 | △(バグあり) | ○ |
| 11 | leave 後の destroyClient()
|
× | × |
| 12 | patchJsMedia |
○ | ○ |
| 13 | checkSystemRequirements() |
× | ○ |
| 14 |
sessionPasscode を join に渡す |
× | ○ |
選手A も SDK API の使い方自体はかなり正しく書けています。attachVideo の使い方も、getAllUser() による復元も通っている。差が出たのは「知らないと入れられないもの」でした。
#9 の active-media-failed がわかりやすい例です。選手A は auto-play-audio-failed を拾っていました。方向性は合っているけれど別です。「近いイベントは知っている」のが選手A、「イベント一覧を型定義と照合して並べた」のが選手B。
#13 の checkSystemRequirements() も同じ構図です。プロンプトの要件に書いていない項目ですが、選手B だけが入れてきました。
今回 skill があっても直らなかったもの
一つ注目したのは、選手Bの唯一のバツ。
destroyClient() を両選手とも落としました。 セッション終了後に init() を呼び直しており、コンソールに PubSub already loaded, using existing version が出ます。
面白いのは、skill にはちゃんとガイダンスがあったことです。実験後に検索したところ、references/sessions.md の "Clean up the Video SDK" セクションと、React / Vue / Svelte / Angular / Nuxt の各フレームワーク統合ドキュメントに記述がありました。選手B 本人にも聞いてみました。
(ぼく) 一つ質問なんだけど、セッション終了後に再度 SDK を init する場合、先に
desroyClient()するといったガイダンスは skill には存在しなかった?直さなくていいから教えて。
(選手B ) はい、存在します。 references/sessions.md("Clean up the Video SDK" セクション)に、セッション終了後は映像のレンダリングを止め destroyClient() を呼んで SDK を破棄し、再利用できるようにするというガイダンスがあります。
今回の実装は destroyClient() を呼んでおらず leave() + 自前の teardown のみなので、この点はスキルのガイダンスを取りこぼしています。修正しましょうか?
つまり skill を入れても、そこに書いてあることが全部反映されるわけではない。読ませる構造があっても取りこぼしは起きます。
もう 1 つ。選手B には [hidden] 属性に対応する CSS 定義が無く、プレースホルダが消えずに映像を覆い隠す、という不具合がありました。これは素の CSS の話で、Video SDK とは無関係だったんですが、考えてるうちに自分の書いたものを忘れちゃったようです。
まとめ
やってみた実感として、skill の効果は 3 つに整理できます。
1. 手直しが減る。 選手A は動かすまでに CSS の修正が必要でした。選手B はそのまま動きました。加えて選手A のコードには、二重クリック防止のバグ、sessionPasscode の未使用など、レビューで指摘すべき点が複数残っていました。
2. 「知らないと入れられないもの」が入る。 active-media-failed、checkSystemRequirements()、isInFailover による再接続表示。要件に書いていない・書きようがない項目が、skill 側から供給されます。
3. 設計が変わる。 これが一番大きい。video-player-container を使い回す設計は、後から気づいて直すのが面倒な種類の判断です。最初から正しく始められる価値は大きい。
一方で万能ではありません。destroyClient() のように、skill に書いてあっても落ちるものはあります。レビューは要ります。 ただしレビューの中身が「API 名が実在するか」から「設計判断が妥当か」に上がる。これは大きな違いだと思います。
Web 版は
npx @zoom/videosdk install-skill
の一行です。SDK をアップグレードしたら skill も入れ直すのを忘れずに。
検証環境
- Claude Code v2.1.172 / Opus 4.8
-
@zoom/videosdk2.4.7 / 2.5.0 - Vanilla TypeScript + Vite、Chrome 2 タブで動作確認
- 試行回数 n=1。LLM の出力は毎回変わるため、この記事の差分がそのまま再現される保証はありません
今回の skill の scope は Video SDK for Web のみです。埋め込み型の Zoom Meetings / Webinars は Meeting SDK の管轄なので、この skill では扱えません。ですが、Zoom 公式の Github には全ての Build Platform 製品を含む開発者向けの skill 群が網羅されています!こちらもぜひ活用してみてください

