「自社サービスにビデオ通話を付けたい」「社内向けに簡単なWeb会議ツールを作れないか」。そんな相談を受けたとき、最初にぶつかるのが「ローカルでは動いたのに、別のPCやスマホから開くとカメラが使えない」という壁です。原因の多くは、WebRTC(=ブラウザ同士で映像・音声を直接やりとりする標準技術)のコードではなく、ページがHTTPSで開かれているかどうかにあります。
「NuxtでWebRTCのビデオチャットを作りたいけど、何から始めればいい?スマホから開くとカメラが使えないのはなぜ?」
この連載では、Nuxt 4 だけで1対1、そして3〜4人のビデオチャットを段階的に作っていきます。第0回の今回は、連載の全体像を紹介したうえで、Nuxt 4 プロジェクトの土台を作り、「このページでカメラを使えるか」を判定する環境チェック画面を実装します。結論を先に言うと、カメラ・マイクはHTTPS(または開発用の localhost)でしか使えないため、開発環境・検証環境・本番環境のすべてで「どうやってHTTPSで開くか」を最初に決めておくのが近道です。
この連載で作るビデオチャットの全体像
完成形は、招待リンクを共有した相手とブラウザだけでビデオ通話できるWebアプリです。別言語のサーバーは立てず、Nuxt のサーバー機能(Nitro)で接続の仲介まで行います。
構成:シグナリングはNuxt、映像はブラウザ同士(P2P)
WebRTC の通話は、大きく2種類の通信でできています。
| 通信 | 何を送るか | 経路 |
|---|---|---|
| シグナリング(=接続の段取りの連絡) | 「通話したい」「私の映像の形式はこれ」「私にはこの経路で届く」といったテキスト | ブラウザ → Nuxt サーバー(WebSocket)→ 相手のブラウザ |
| メディア(映像・音声)とデータ | カメラ映像・マイク音声・チャット文字 | 原則ブラウザ同士が直接(P2P)。つながらない時は TURN サーバー経由 |
サーバーを通るのは接続交渉の短いテキストだけで、映像そのものは原則サーバーを通りません。ただし、会社のネットワークなどで直接つながらない場合は、映像を中継する TURN サーバー(=中継役のサーバー)が必要になります。これは第6回で扱います。
全12回の目次(予定)
| 回 | タイトル(予定) | 作る機能 |
|---|---|---|
| 第0回 | Nuxt 4でビデオチャットの土台を作る:なぜHTTPSが必要なのか(この記事) | プロジェクト作成、環境チェック画面 |
| 第1回 | カメラとマイクを取得してプレビューし、デバイスを選べるようにする | getUserMedia、エラー表示、デバイス選択 |
| 第2回 | Nitro の WebSocket でシグナリングサーバーを作る | ルーム単位のメッセージ中継 |
| 第3回 | RTCPeerConnection で1対1通話をつなぐ(offer/answer/ICE) | 1対1の映像通話 |
| 第4回 | ルーム作成と招待リンク、入室前プレビュー | 招待URL、定員チェック |
| 第5回 | ミュートとカメラオフ | 状態の相手への通知 |
| 第6回 | STUN/TURN と本番デプロイ:「社外とつながらない」を解消する | TURN認証情報の発行、HTTPS配下へのデプロイ |
| 第7回 | 画面共有 | getDisplayMedia と映像の差し替え |
| 第8回 | DataChannel でテキストチャット | P2P のチャット |
| 第9回 | MediaRecorder で録画する前に考えること | ローカル録画と同意の表示 |
| 第10回 | 切断・再接続とエラーハンドリング | 自動再接続、ICE再起動 |
| 第11回 | 少人数通話へ:メッシュ実装と SFU という選択肢 | 3〜4人通話、構成の限界 |
各回は「1回=1機能」で、前の回のコードに差分を足していきます。サンプルコードは毎回 npm run build と型チェックが通る状態にしてから記事にしています。
使う技術(執筆時点:2026年8月)
| 項目 | バージョン |
|---|---|
| Node.js | 24(Nuxt 4 が対応する LTS 系) |
| Nuxt | 4(サーバー部分は Nitro v2) |
| Vue | 3(Composition API、<script setup lang=”ts”>) |
| TypeScript | 6(型チェックは vue-tsc 3) |
| WebRTC / Media Capture | ブラウザ標準API(追加ライブラリなし) |
WebRTC の部分はブラウザ標準APIだけで書きます。ライブラリに頼らないぶんコードは増えますが、何が起きているかを追いやすく、どのフレームワークにも応用できます。
なぜビデオチャットにHTTPSが必要なのか
カメラ・マイクは「Secure Context」でしか使えない
ブラウザには Secure Context(=安全な接続で開かれたページ)という考え方があり、カメラやマイクのような強い権限を持つ機能は、Secure Context でしか使えないようになっています。
📰 出典:MDN「Secure contexts」
MDN によると、次のようなページが「安全」とみなされます。
| 開き方 | Secure Context |
|---|---|
| https://example.com | 安全(HTTPS) |
| http://localhost:3000 / http://127.0.0.1:3000 | 安全(自分のPC内=ループバック) |
| file:///… | 安全(ローカルファイル) |
| http://192.168.x.x:3000(LAN内の別PCから) | 安全ではない |
| http://example.com | 安全ではない |
つまり、開発中に自分のPCで http://localhost:3000 を開くと動くのに、同じWi-Fiのスマホから http://192.168.x.x:3000 で開くと動かない、ということが起こります。これはコードの不具合ではなく、ブラウザの仕様どおりの動きです。
安全でないページでは getUserMedia 自体が存在しない
カメラ・マイクの取得に使う navigator.mediaDevices.getUserMedia() は、安全でないページでは呼び出す前の段階で使えません。
📰 出典:MDN「MediaDevices: getUserMedia() method」
MDN には、安全に読み込まれていないページでは navigator.mediaDevices が undefined になると書かれています。そのため navigator.mediaDevices.getUserMedia() と書くと、権限ダイアログすら出ずにエラーになります。「ボタンを押しても何も起きない」という問い合わせの正体がこれ、というのはよくある話です。
一方で、RTCPeerConnection(通話の本体)や WebSocket は HTTP のページでも存在します。ただし本番で WebSocket を使う場合も、HTTPS のページからは暗号化された wss:// で接続するのが前提です。
実装:Nuxt 4 プロジェクトの土台を作る
ここからはサンプルコードを見ていきます。ファイルはすべて blogs/it_hacchu/series/webrtc-video/code/ 配下(以下 code/)にあります。
サンプルの全体構成(第0回時点)
code/
├── nuxt.config.ts … Nuxt の設定(WebSocket 有効化・runtimeConfig)
├── .env.example … 環境変数のキー名だけ(値は書かない)
├── package.json
├── tsconfig.json … Nuxt が生成する .nuxt/tsconfig.*.json を参照
└── app/
├── app.vue … レイアウト + ページの入れ物
├── layouts/default.vue
└── pages/index.vue … 環境チェック画面(今回の主役)
Nuxt 4 では、画面側のコードを app/ ディレクトリにまとめる構成が標準です。第2回からはサーバー側の server/、サーバーとブラウザで共有する型を置く shared/ が加わります。
package.json:型チェックのスクリプトを用意する
code/package.json(scripts 部分の抜粋。依存パッケージは nuxt・vue・vue-router、開発用に typescript・vue-tsc)
{
"scripts": {
"dev": "nuxt dev",
"build": "nuxt build",
"preview": "nuxt preview",
"typecheck": "nuxt typecheck",
"postinstall": "nuxt prepare"
}
}
nuxt typecheck は内部で vue-tsc(= .vue ファイルも含めて型チェックするツール)を使います。執筆時点では TypeScript の最新メジャーが 7 ですが、7 はコンパイラの作りが変わっており、従来の API を前提にする vue-tsc と組み合わせると動かない可能性があるため、サンプルでは TypeScript 6 系に固定しました。
nuxt.config.ts:WebSocket と秘密情報の置き場所を先に決める
code/nuxt.config.ts
// https://nuxt.com/docs/4.x/api/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2026-08-01',
devtools: { enabled: true },
ssr: true,
app: {
head: {
htmlAttrs: { lang: 'ja' },
title: 'Nuxt WebRTC Video Chat',
meta: [{ name: 'viewport', content: 'width=device-width, initial-scale=1' }],
},
},
nitro: {
// Nuxt 4 は Nitro v2 (nitropack 2.x) を使用。WebSocket は実験的機能のため明示的に有効化する。
// https://v2.nitro.build/guide/websocket
// シグナリングサーバーのハンドラ (server/routes/_ws.ts) は第2回で追加する。
experimental: {
websocket: true,
},
},
runtimeConfig: {
// 秘密情報は .env (NUXT_* 環境変数) から注入する。コードに直書きしない。
// 例: NUXT_TURN_SECRET=... (第6回 STUN/TURN で使用)
turnSecret: '',
public: {
// 例: NUXT_PUBLIC_STUN_URLS=stun:stun.example.com:3478
stunUrls: '',
},
},
typescript: {
strict: true,
},
})
ポイントは2つです。
- nitro.experimental.websocket: true:Nuxt のサーバー部分である Nitro(v2)では、WebSocket は実験的機能という扱いで、このフラグで有効にします。第2回でシグナリングサーバーを作るときの前提になります。
- runtimeConfig:TURN サーバーの共有シークレットのような秘密情報は、コードに書かず環境変数から入れます。public の外に置いた値はサーバー側でしか読めず、public の中の値はブラウザにも渡ります。
📰 出典:Nitro v2「WebSocket」
code/.env.example
# コピーして .env を作成(.env はコミットしない)
# 第6回(STUN/TURN)で使用
NUXT_PUBLIC_STUN_URLS=
NUXT_TURN_SECRET=
.env 本体は .gitignore で除外し、リポジトリにはキー名だけの .env.example を置きます。値は今は空で構いません。
app/pages/index.vue:環境チェック画面
code/app/pages/index.vue(script 部分)
<script setup lang="ts">
// 第0回: ベースライン。第1回以降でカメラ・マイク取得 → シグナリング → 通話 と機能を足していく。
useHead({ title: 'ホーム | Nuxt WebRTC Video Chat' })
type Check = { label: string; ok: boolean | null; hint: string }
// ブラウザ専用 API(window / navigator)は SSR 中に存在しないため、
// 初期値は null(=確認中)にしておき、onMounted でブラウザ側だけ判定する。
const origin = ref('')
const checks = ref<Check[]>([
{ label: 'Secure Context', ok: null, hint: 'HTTPS か http://localhost で開いてください' },
{ label: 'navigator.mediaDevices.getUserMedia', ok: null, hint: 'Secure Context でないと undefined になります' },
{ label: 'RTCPeerConnection', ok: null, hint: 'WebRTC に対応したブラウザで開いてください' },
{ label: 'WebSocket', ok: null, hint: 'WebSocket に対応したブラウザで開いてください' },
])
onMounted(() => {
origin.value = window.location.origin
const results = [
window.isSecureContext,
typeof navigator.mediaDevices?.getUserMedia === 'function',
typeof window.RTCPeerConnection === 'function',
typeof window.WebSocket === 'function',
]
checks.value = checks.value.map((c, i) => ({ ...c, ok: results[i] ?? false }))
})
</script>
code/app/pages/index.vue(template 部分)
<template>
<section>
<h1>Nuxt WebRTC Video Chat</h1>
<p>連載のサンプルプロジェクト(第0回:ベースライン)です。ここに通話機能を順番に追加していきます。</p>
<h2>環境チェック</h2>
<p>表示中のオリジン: <code>{{ origin || '確認中…' }}</code></p>
<ul class="checks">
<li v-for="c in checks" :key="c.label" :data-ok="c.ok">
{{ c.label }}:
<strong>{{ c.ok === null ? '確認中…' : c.ok ? 'OK' : 'NG' }}</strong>
<span v-if="c.ok === false" class="checks__hint">({{ c.hint }})</span>
</li>
</ul>
</section>
</template>
ここで大事なのは、判定を onMounted の中で行っていることです。Nuxt は最初の表示をサーバー側で組み立てる SSR(=サーバーサイドレンダリング)を行うため、サーバーで実行される瞬間には window も navigator もありません。ブラウザ専用のAPIに触る処理は onMounted(=画面がブラウザに表示された後に動く処理)に入れる、というのが連載全体の決まりです。初期値を null にして「確認中…」と表示しているのも、サーバーでは判定できないことを画面上で区別するためです。
navigator.mediaDevices?.getUserMedia の ?. は、mediaDevices が undefined(=安全でないページ)の場合にエラーにせず undefined を返すための書き方です。
レイアウト(code/app/layouts/default.vue)はヘッダー・本文・フッターを並べるだけのものなので、ここでは省略します。code/app/app.vue は <NuxtLayout> の中に <NuxtPage /> を置くだけです。
動作確認の方法
サンプルでは次の手順で確認しました。
- code/ で npm install → npm run dev を実行する
- 同じPCのブラウザで http://localhost:3000 を開く → 4項目すべて「OK」になる
- 同じPCの LAN 側 IP アドレス(http://192.168.x.x:3000 のような形)で開く → 「Secure Context」と「getUserMedia」が「NG」、「RTCPeerConnection」「WebSocket」は「OK」になる
- npm run build と npm run typecheck がエラーなく終わる
LAN の別端末から開きたい場合、npm run dev はそのままでは自分のPCからしか接続を受けないことがあるので、npx nuxt dev –host 0.0.0.0 のように待ち受けるアドレスを指定します(社内ネットワークに開発サーバーを公開することになるので、使う場所には注意してください)。
LAN内のスマホで試したいときは開発用HTTPSを使う
スマホ実機で試す段階になったら、開発サーバーをHTTPSで起動します。Nuxt の CLI には –https オプションがあり、自己署名証明書(=自分で発行した、ブラウザが信頼していない証明書)を自動生成できます。
npx nuxt dev --https --host 0.0.0.0
筆者の環境(Chromium 系ブラウザ)では、https://<PCのIPアドレス>:3000 を開いて証明書の警告を承認すると、環境チェックが4項目とも「OK」になりました。ただし、自己署名証明書の扱いはブラウザや端末ごとに異なり、iOS の Safari などでは同じようにいかない可能性があります(実機では未確認です)。うまくいかない場合は、開発端末に信頼させるローカル証明書を用意する、HTTPS のステージング環境にデプロイする、といった方法を検討してください。
つまずきやすい点と注意
- http://127.0.0.1 と LAN の IP は別物:自分のPC内を指すループバックアドレスは安全扱いですが、同じPCでも LAN 側の IP で開くと安全扱いになりません。
- SSR 中にブラウザAPIに触らない:<script setup> の直下で navigator.mediaDevices を参照すると、サーバー側でエラーになります。必ず onMounted 内か <ClientOnly> の中で扱います。
- HTTPS の警告を「とりあえず無視」を本番に持ち込まない:開発用の自己署名証明書は、あくまで手元の検証用です。本番や社外の人が使う検証環境には、正式な証明書を付けたドメインを用意します。
- 秘密情報は最初から .env へ:後から「とりあえずコードに書いた値」を探して消すのは手間がかかり、履歴にも残ります。runtimeConfig の置き場所を第0回で決めておくのはそのためです。
- 本番運用で省略していること:この連載のサンプルは学習用です。認証、アクセス制限、監視、負荷対策などは必要に応じて別途設計が必要で、該当する回で注意点として触れます。
発注者向けメモ:ビデオ通話を頼む前に決めておきたいこと
ビデオ通話機能は「画面を1枚作る」のとは違い、動かすための環境そのものに条件がある機能です。見積もりや打ち合わせの前に、次の点を確認しておくと手戻りが減ります。
- ☐ 本番だけでなく、検証環境(ステージング)にも証明書付きのドメインを用意する前提になっているか
- ☐ 社内での動作確認をスマホ実機で行うか(行うなら、開発中からHTTPSで開ける仕組みが必要)
- ☐ 利用者の端末とブラウザ(PC/スマホ、Chrome/Safari 等)の範囲を決めているか
- ☐ 常時動いているサーバー(WebSocket を扱えるもの)を運用する前提で、保守の担当を決めているか
- ☐ 社外の相手とつながる必要があるか(あるなら TURN サーバーの運用が必要になる。第6回で解説)
開発会社への質問例:
- 「検証環境もHTTPSで用意してもらえますか?証明書やドメインの手配はどちらが行いますか?」
- 「スマホ実機での確認は、開発のどの段階から、どの端末・ブラウザで行う予定ですか?」
- 「通話の接続を仲介するサーバーは、どこで、どんな形で動かす想定ですか?」
- 「社外ネットワークの相手とつながらない場合の対策(TURN サーバー)は見積もりに含まれていますか?」
「ローカルでは動きました」という報告は、ビデオ通話では完成の半分手前だと考えておくと、スケジュールの読み違いを防げます。
まとめと次回予告
- WebRTC のビデオチャットは「シグナリング(Nuxt サーバー経由)」と「映像・音声(原則 P2P)」の2つの通信でできている
- カメラ・マイクは Secure Context(HTTPS か localhost)でしか使えず、LAN の IP で開いた HTTP ページでは navigator.mediaDevices 自体が存在しない
- Nuxt では、ブラウザ専用APIは onMounted 内で扱い、秘密情報は runtimeConfig と .env に置く
- WebSocket を使うため、nitro.experimental.websocket を最初に有効にしておく
次回(第1回)は「カメラとマイクを取得してプレビューし、デバイスを選べるようにする」です。getUserMedia で自分の映像を映し、権限が拒否された・カメラが見つからない・他のアプリが使用中、といったエラーを利用者に分かる言葉で表示するところまで作ります。
この連載の記事一覧
この記事は連載「NuxtとWebRTCで作るビデオチャット」の1回です。連載のほかの回は次のとおりです(連載の一覧ページ)。
- 【NuxtとWebRTCで作るビデオチャット 第0回】Nuxt 4でビデオチャットの土台を作る:なぜHTTPSが必要なのか(この記事)
- 【NuxtとWebRTCで作るビデオチャット 第1回】カメラとマイクを取得してプレビューし、デバイスを選べるようにする
- 【NuxtとWebRTCで作るビデオチャット 第2回】Nitro の WebSocket でシグナリングサーバーを作る
- 【NuxtとWebRTCで作るビデオチャット 第3回】RTCPeerConnection で1対1通話をつなぐ(offer/answer/ICE)
- 【NuxtとWebRTCで作るビデオチャット 第4回】ルーム作成と招待リンク、入室前プレビュー
- 【NuxtとWebRTCで作るビデオチャット 第5回】ミュートとカメラオフ
- 【NuxtとWebRTCで作るビデオチャット 第6回】STUN/TURN と本番デプロイ:「社外とつながらない」を解消する
- 【NuxtとWebRTCで作るビデオチャット 第7回】画面共有
- 【NuxtとWebRTCで作るビデオチャット 第8回】DataChannel でテキストチャット
- 【NuxtとWebRTCで作るビデオチャット 第9回】MediaRecorder で録画する前に考えること
- 【NuxtとWebRTCで作るビデオチャット 第10回】切断・再接続とエラーハンドリング
- 【NuxtとWebRTCで作るビデオチャット 第11回】少人数通話へ:メッシュ実装と SFU という選択肢










コメント
コメント一覧 (1件)
[…] ビデオチャットで最初に作るのは、通話そのものではなく「自分のカメラとマイクが正しく使えるか」を確かめる画面です。前回の第0回:Nuxt 4でビデオチャットの土台を作る:なぜHTTPSが必要なのかでは、Nuxt 4 プロジェクトを作り、カメラが使えるページ(Secure Context)かどうかを判定する画面を用意しました。 […]