MENU

問い合わせ


    【NuxtとWebRTCで作るビデオチャット 第0回】Nuxt 4でビデオチャットの土台を作る:なぜHTTPSが必要なのか

    「自社サービスにビデオ通話を付けたい」「社内向けに簡単な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.js24(Nuxt 4 が対応する LTS 系)
    Nuxt4(サーバー部分は Nitro v2)
    Vue3(Composition API、<script setup lang=”ts”>)
    TypeScript6(型チェックは 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 /> を置くだけです。

    動作確認の方法

    サンプルでは次の手順で確認しました。

    1. code/ で npm install → npm run dev を実行する
    2. 同じPCのブラウザで http://localhost:3000 を開く → 4項目すべて「OK」になる
    3. 同じPCの LAN 側 IP アドレス(http://192.168.x.x:3000 のような形)で開く → 「Secure Context」と「getUserMedia」が「NG」、「RTCPeerConnection」「WebSocket」は「OK」になる
    4. 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回です。連載のほかの回は次のとおりです(連載の一覧ページ)。

    システム制作・運用・保守のお問い合わせはこちら


      よかったらシェアしてね!
      • URLをコピーしました!
      • URLをコピーしました!

      この記事を書いた人

      株式会社THIRD HERO代表取締役 朝野貴朗
      Webシステム開発を中心に、toC向けサービスサイトの運営、ツール開発などを行ってまいりました。

      コメント

      コメント一覧 (1件)

      目次