0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Windowsでネットワークカメラを利用したホームセキュリティシステムを作る(3)

Posted at

前回作成したWindowsサービスプログラムを修正して、WebRTCを利用してライブ配信の検証を行います。

1.MediaMTXの設定ファイル変更

mediamtx.yamlを修正しWebRTCのパラメータを追加します

mediamtx.yaml
- webrtc: no
+ webrtc: yes
+ webrtcAddress: :8889
+ webrtcEncryption: no
+ webrtcAllowOrigin: '*'
+ webrtcLocalUDPAddress: :8189
+ webrtcIPsFromInterfaces: yes

2.視聴用ページの作成

HLSとWebRTCの比較を行えるようにマルチ画面のページを作成します

wwwroot/MultiLiveView.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>ライブ配信</title>
    <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
    <style>
        body {
            background: #111;
            color: #eee;
            font-family: sans-serif;
            text-align: center;
        }

        .container {
            display: flex;
            flex-wrap: wrap;
            justify-content: center;
            gap: 20px;
            margin-top: 20px;
        }

        .player {
            max-width: 640px;
            width: 100%;
            position: relative;
        }

        video {
            width: 100%;
            border: 1px solid #ccc;
            background: black;
        }

        .play-fallback {
            position: absolute;
            left: 50%;
            top: 50%;
            transform: translate(-50%, -50%);
            background: rgba(0,0,0,0.6);
            color: #fff;
            padding: 8px12px;
            border-radius: 4px;
            cursor: pointer;
            display: none;
        }

        h2 {
            margin-bottom: 8px;
        }

        @media (max-width:768px) {
            .container {
                flex-direction: column;
                align-items: center;
            }
        }
    </style>
</head>
<body>
    <h1>ライブ配信</h1>
    <div class="container">
        <div class="player">
            <h2>HLSライブ配信</h2>
            <video id="video1" controls muted playsinline></video>
            <div id="playFallback1" class="play-fallback">再生</div>
        </div>
        <div class="player">
            <h2>WebRTCライブ配信</h2>
            <video id="video2" controls muted playsinline></video>
            <div id="playFallback2" class="play-fallback">再接続 / 再生</div>
        </div>
    </div>

    <script>
        // 複数の RTCPeerConnection を管理
        const pcs = new Map();
        const retryInfo = new Map();

        // 各チャンネルのURL
        const hlsUrl = 'http://localhost:8888/Camera1/index.m3u8';
        const webrtcUrl = 'http://localhost:8889/Camera1/whep';

        // HLS の設定とエラーハンドリング
        function setupHls(videoElement, url) {
            if (Hls.isSupported()) {
                const hls = new Hls({
                    enableWorker: true,
                    maxBufferLength: 30,
                    liveSyncDurationCount: 3
                });

                let retryCount = 0;
                const maxRetries = 5;

                hls.loadSource(url);
                hls.attachMedia(videoElement);

                hls.on(Hls.Events.MANIFEST_PARSED, () => {
                    videoElement.play().catch(error => {
                        console.warn('Autoplay failed (HLS):', error);
                        // フォールバックUIの表示
                        const fb = document.getElementById('playFallback1');
                        if (fb) fb.style.display = 'block';
                    });
                });

                hls.on(Hls.Events.ERROR, (event, data) => {
                    console.warn('HLS error', data);
                    if (data && data.fatal) {
                        // 簡単な再試行ロジック
                        if (retryCount < maxRetries) {
                            retryCount++;
                            const delay = Math.min(1000 * Math.pow(2, retryCount), 10000);
                            console.log(`HLS fatal error: retry #${retryCount} in ${delay}ms`);
                            setTimeout(() => {
                                try {
                                    hls.startLoad();
                                } catch (e) {
                                    console.error('HLS restart failed', e);
                                }
                            }, delay);
                        } else {
                            console.error('HLS: max retries reached');
                        }
                    }
                });

            } else if (videoElement.canPlayType('application/vnd.apple.mpegurl')) {
                videoElement.src = url;
                videoElement.addEventListener('loadedmetadata', () => {
                    videoElement.play().catch(error => {
                        console.warn('Autoplay failed (native HLS):', error);
                        const fb = document.getElementById('playFallback1');
                        if (fb) fb.style.display = 'block';
                    });
                });
            } else {
                videoElement.outerHTML += '<p>HLS not supported in this browser.</p>';
            }

            // フォールバックボタン
            const fb = document.getElementById('playFallback1');
            if (fb) {
                fb.addEventListener('click', () => {
                    fb.style.display = 'none';
                    videoElement.muted = false;
                    videoElement.play().catch(e => console.warn('Manual play failed', e));
                });
            }
        }

        // WebRTC セットアップ(async/await 化)
        async function setupWebRTC(videoElement, url) {
            // 自動再生ポリシー対策
            try {
                videoElement.muted = true;
                videoElement.setAttribute('playsinline', '');
                videoElement.autoplay = true;
            } catch (e) {
                // ignore
            }

            const videoId = videoElement.id;

            // 再接続情報初期化
            if (!retryInfo.has(videoId)) {
                retryInfo.set(videoId, { attempts: 0, backoffMs: 1000 });
            }

            // ICE サーバー構成(必要に応じてTURNを追加)
            const pcConfig = {
                iceServers: [
                    { urls: 'stun:stun.l.google.com:19302' }
                ]
            };

            let pc;

            async function createAndConnect() {
                // 古いPCが残っていたらクローズ
                if (pcs.has(videoId)) {
                    try { pcs.get(videoId).close(); } catch (_) { }
                    pcs.delete(videoId);
                }

                pc = new RTCPeerConnection(pcConfig);
                pcs.set(videoId, pc);

                pc.ontrack = event => {
                    console.log('トラック受信:', event.track.kind);
                    let stream = (event.streams && event.streams[0]) ? event.streams[0] : new MediaStream();
                    if (!event.streams || event.streams.length === 0) {
                        stream.addTrack(event.track);
                    }
                    //既存の srcObject がある場合はそれを使って track を追加する
                    if (videoElement.srcObject instanceof MediaStream) {
                        const existing = videoElement.srcObject;
                        if (!existing.getTracks().includes(event.track)) {
                            existing.addTrack(event.track);
                        }
                    } else {
                        videoElement.srcObject = stream;
                    }

                    videoElement.play().then(() => {
                        console.log('video.play() 成功');
                        // 成功したら retry 情報をリセット
                        retryInfo.set(videoId, { attempts: 0, backoffMs: 1000 });
                        const fb = document.getElementById('playFallback2'); if (fb) fb.style.display = 'none';
                    }).catch(err => {
                        console.warn('video.play()失敗:', err);
                        const fb = document.getElementById('playFallback2'); if (fb) fb.style.display = 'block';
                    });
                };

                pc.oniceconnectionstatechange = () => {
                    console.log('ICE状態:', pc.iceConnectionState);
                    if (pc.iceConnectionState === 'connected') {
                        console.log('WebRTC接続成功');
                    } else if (pc.iceConnectionState === 'failed' || pc.iceConnectionState === 'disconnected') {
                        console.warn('WebRTC接続失敗または切断');
                        scheduleReconnect();
                    }
                };

                // recvonlyでトランシーバーを追加
                try {
                    pc.addTransceiver('video', { direction: 'recvonly' });
                    pc.addTransceiver('audio', { direction: 'recvonly' });
                } catch (e) {
                    console.warn('addTransceiver failed', e);
                }

                try {
                    const offer = await pc.createOffer();
                    await pc.setLocalDescription(offer);
                    console.log('SDP Offer送信');

                    const res = await fetch(url, {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/sdp' },
                        body: offer.sdp
                    });

                    const answerSdp = await res.text();
                    console.log('SDP Answer受信');
                    if (!answerSdp.startsWith('v=')) {
                        throw new Error('SDP応答が不正です(v= 行が見つかりません)');
                    }

                    await pc.setRemoteDescription({ type: 'answer', sdp: answerSdp });
                } catch (err) {
                    console.error('WebRTC接続エラー:', err);
                    scheduleReconnect();
                }
            }

            function scheduleReconnect() {
                const info = retryInfo.get(videoId) || { attempts: 0, backoffMs: 1000 };
                if (info.attempts >= 5) {
                    console.error('最大再試行回数に到達しました');
                    const fb = document.getElementById('playFallback2'); if (fb) fb.style.display = 'block';
                    return;
                }

                info.attempts++;
                const delay = info.backoffMs;
                info.backoffMs = Math.min(info.backoffMs * 2, 30000);
                retryInfo.set(videoId, info);

                console.log(`再接続をスケジュール: attempt=${info.attempts} delay=${delay}ms`);
                const fb = document.getElementById('playFallback2'); if (fb) fb.style.display = 'block';

                setTimeout(() => {
                    createAndConnect();
                }, delay);
            }

            // 最初の接続を開始
            await createAndConnect();
        }

        // 指定した videoElement の接続を停止
        function stopWebRTC(videoElement) {
            if (!videoElement) return;
            const id = videoElement.id;
            if (pcs.has(id)) {
                try {
                    const pc = pcs.get(id);
                    // トラックを停止
                    try {
                        pc.getReceivers().forEach(r => { if (r.track) r.track.stop(); });
                    } catch (e) { }
                    pc.close();
                } catch (e) {
                    console.warn('stopWebRTC error', e);
                }
                pcs.delete(id);
            }
            try { videoElement.srcObject = null; } catch (e) { }
            const fb = document.getElementById('playFallback2'); if (fb) fb.style.display = 'none';
        }

        // フォールバックボタン設定
        (function initFallbackButtons() {
            const fb2 = document.getElementById('playFallback2');
            const video2 = document.getElementById('video2');
            if (fb2 && video2) {
                fb2.addEventListener('click', () => {
                    fb2.style.display = 'none';
                    stopWebRTC(video2);
                    // 少し遅らせて再接続
                    setTimeout(() => { setupWebRTC(video2, webrtcUrl); }, 300);
                });
            }
        })();

        // 各プレイヤーにストリームを設定
        setupHls(document.getElementById('video1'), hlsUrl);
        setupWebRTC(document.getElementById('video2'), webrtcUrl);

    </script>
</body>
</html>

メモ

  • HLSストリームの視聴はhls.jsを利用します
  • WebRTCストリームの視聴はブラウザ標準搭載のプレーヤーを利用します

3.視聴確認

Windowsサービスを再起動し、以下のURLで今回作成した視聴用画面へアクセスする
http://localhost:5000/MultiLiveView.html

メモ
WebRTCで出力した際の実映像との遅延時間は体感で0.5~1秒程度(HLSで視聴した場合はさらに5秒程度の遅延)となりました。
ただし、WebRTCは巻き戻しなどの特殊再生制御ができないようなので視聴画面でのライブ・録画の切替機能があるとよいと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?