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?

MediaMTXで映像配信

Last updated at Posted at 2025-12-07

MediaMTXを使って、WebRTCまたはRTSPで映像を配信します。

WebRTCにもSFUという1対多の通信方式がありますが、少々面倒です。MediaMTXを使うと1対1で通信したものが、簡単に複数に配信することができます。
また、いろんな通信方式も取り込めるのもいいです。例えば、後述しますが、ESP32のカメラ映像も取り込んだり、SwitchBotの見守り映像を取り込むことができます。
私の自宅では、QNAPのNASがありますので、そこにMediaMTXをDockerを使ってセットアップします。

以下の図のような構成となります。

image.png

QNAPにMediaMTXをインストール

QNAPにDocker(Container Station)を使ってインストールします。
docker-compose.yamlは以下の通りです。

docker-compose.yaml
version: '3.8'
services:
  mediamtx:
    image: bluenviron/mediamtx:latest-ffmpeg
    user: "1000:100"
    container_name: mediamtx
    restart: unless-stopped
    network_mode: host
    volumes:
      - /share/Container/mediamtx/mediamtx.yml:/mediamtx.yml:ro
      - /share/Documents/mediamtx/media/:/media
      - /share/Documents/mediamtx/recordings:/recordings
    environment:
      - TZ=Asia/Tokyo

ffmpegは任意です。後で、ESP32との連携で使います。ffmpegを付けるかどうかでimageを以下のように選択してください。

ffmpeg付き:bluenviron/mediamtx:latest-ffmpeg
ffmpeg無し:bluenviron/mediamtx:latest

ホスト側から設定ファイルmediamtx.yamlを編集できるようにvolumesを設定しています。
頻繁に書き換えるので。あらかじめフォルダ「/share/Container/mediamtx/」を作っておきます。
ネットワークモードはhostにしています。MediaMTXが使うポートがそのままQNAPのポートになります。

使うポートは以下の通りです。

用途 ポート番号 TCP/UDP
API 9997 TCP
RTSP 8554 TCP
WebRTC 8889 TCP
WebRTC 8189 UDP
HLS 8888 TCP
Playback 9996 TCP

インストールができたら、設定ファイル(mediamtx.yaml)をカスタマイズします。

アクセスするためのユーザ・パスワードを設定します。外部HTTPサーバで扱うようにして動的に認証することもできますが、まずは簡単にmediamtx.yaml内に記載します。
authMethodはinternalにします。

mediamtx.yaml
authMethod: internal
authInternalUsers:
- user: user
  pass: password
  ips: []
  permissions:
  - action: publish
    path:
  - action: read
    path:
  - action: playback
    path:

- user: admin
  pass: password
  ips: ['127.0.0.1', '::1', '192.168.1.0/24']
  permissions:
  - action: api
  - action: metrics
  - action: pprof

変更には、QNAPのアプリ「Text Editor」を使います。
その他、自分の好みで編集します。

RTSPは、UDPを使わずTCPだけにしたかったので以下に変更

mediamtx.yaml
rtspTransports: [tcp]

MediaMTXを再起動します。

管理コンソールで確認

設定状態や動作状態を確認するための簡単な管理コンソールを用意しました。

さきに、以下の部分にセットアップしたMediaMTXのホスト名またはIPアドレスを指定します。

public/mediamtx_console/js/start.js
const base_url = 'http:// 【MediaMTXサーバのホスト名】:9997';

以下をブラウザから開きます。

public/mediamtx_console/index.html

更新ボタンを押すと、現在の設定状況を確認できます。

image.png

ちょっと試しに、ブラウザを開いているPCまたはスマホに接続されているカメラをMediaMTXに配信してもらいましょう。
右側のPublishボタンを押します。

image.png

nameは適当にpccamera、user/passwordは、先ほどmediamtx.yamlに設定したuser/passwordを設定します。
そうすると、カメラの使用の許可を求められたのち、成功すると、Publishの右に赤い●が表示されます。
更新ボタンを押すと、pathsにpccameraが追加されたことがわかります。

pccameraの右端に表示ボタンがありますので、押すと配信されている映像を見ることができます。

image.png

WebRTCで見てみる

WebRTCで見てみます。
先ほどの方法のほかに以下の方法もあります。

http://【MediaMTXサーバのホスト名】:8889/【パス名】

パス名にはさきほどのpccameraを指定します。

RTSPで見てみる

RTSPはVLC medial playerで見てみます。

URLは、rtsp://【MediaMTXサーバのホスト名】:8554/【パス名】です。user/passwordを聞かれるので入力すると、映像が表示されます。
もしくは、rtsp://user:password@【MediaMTXサーバのホスト名】:8554/【パス名】でも大丈夫です。

HSLで見てみる

HSLは、ブラウザから以下を入力します。

http:// 【MediaMTXサーバのホスト名】:8888/【パス名】/
または
http://user:password@【MediaMTXサーバのホスト名】:8888/【パス名】/

ブラウザがサポートするビデオコーデックによっては、うまく表示さない場合があるようです

外部から参照できるようにする

インターネットからも参照できるようにします。
その前に、もろもろHTTPS化をすると都合がよいです。
セキュリティが高いのもありますが、ブラウザからWebカメラを使うときには、HTTPSのページである必要があります。

そのため、まずはQNAPのWebサーバをHTTPS化しておきます。
世の中にはたくさん記事があるのでチャレンジしてみてください。
以降は、「コントロールパネル」の「SSL証明書とプライベートキー」にHTTPSのサーバ証明書が登録されている前提です。

準備ができたら、まずは、MediaMTXの各TCPのポートをHTTPSでアクセスできるようにします。
「コントロールパネル」の「ネットワークアクセス」を選択し、タブ「リバースプロキシ」を選択します。
以下のように、ポート番号とHTTPSでのホスト名に変換をします。

用途 オリジナル 変換後 TCP/UDP
API 9997 29997 TCP
RTSP 8554 28554 TCP
WebRTC 8889 28889 TCP
HLS 8888 28888 TCP
Playback 9996 29996 TCP

QNAPのWebサーバのフォルダに管理コンソールのソースをコピーします
必要なのは、/public/mediamtx_consoleです。
先に、以下の部分を先ほどのHTTPSのホスト名とポート番号に変更します。

public/mediamtx_console/js/start.js
const base_url = 'https://【HTTPSでのホスト名】:29997';
const webrtc_base_url = "https:// 【HTTPSでのホスト名】:28889";

以下のURLにブラウザからアクセスします。

https://【HTTPSでのホスト名】/mediamtx_console

以前と同様に操作できるかと思います。

次に、外部ネットワークからも表示できるようにします。
私の場合は、QNAPからPPPoEで外部に接続しているので、QNAPのQuFirewallでファイアウォールの設定をします。
ルールとして以下を許可するようにします。

用途 ポート番号 TCP/UDP
API 29997 TCP
RTSP 28554 TCP
WebRTC 28889 TCP
WebRTC 8189 UDP
HLS 28888 TCP
Playback 29996 TCP

次に、mediamtx.yamlを以下のように変更します。

mediamtx.yaml
webrtcIPsFromInterfacesList: [br0, ppp0]
webrtcAdditionalHosts: [【HTTPSでのホスト名】]
webrtcICEServers2:
- url: stun:stun.l.google.com:19302

webrtcIPsFromInterfacesListは、WebRTCのネゴシエーションの際に、余計なネットワークを返さないようにするための指定です。webrtcAdditionalHostsも同様で、外部からWebRTC接続する際のホスト名になります。

以下は必要に応じて変更します。

mediamtx.yaml
webrtcHandshakeTimeout: 20s
webrtcTrackGatherTimeout: 20s
webrtcSTUNGatherTimeout: 10s

外部ネットワークから以下を開きます

https:// 【HTTPSでのホスト名】/mediamtx_console

上記は、443ポートもQuFirewallで許可している前提です。
この場合も、以前と同様に操作はできるかと思います。

WebRTCのWHEP/WHIP

MediaMTXでは、WebRTCの映像を受け取ったり配信するときに、WHEP/WHIPのプロトコルを使います。
詳細は分からないのですが、以下関数化しておきました。

webrtc_send_connectがWebRTC映像をMediaMTXに入力する場合、 webrtc_receive_connectがMediaMTXからWebRTCで映像を受信する場合です。

public/js/start.js
function webrtc_disconnect(pc){
    const senders = pc.getSenders();
    senders.forEach(sender => {
        const track = sender.track;
        if (track) {
            track.stop();
        }
        pc.removeTrack(sender);
    });
    pc.close();
}

// input: user, password, timeout, name
async function webrtc_receive_connect(input, callback)
{
    var { user, password, timeout, name } = input;

    const pc = new RTCPeerConnection({
        iceServers: [ { urls: "stun:stun.l.google.com:19302" } ],
        iceTransportPolicy: 'all',
    });

    pc.addEventListener('track', event => {
        if (callback) callback('peer', { type: 'track', kind: event.track.kind, streams: event.streams, track: event.track });
    });

    pc.addEventListener('icecandidate', (event) => {
        if (callback) callback('peer', { type: 'icecandidate', candidate: event });
    });
    pc.addEventListener('connectionstatechange', (event) => {
        if (callback) callback('peer', { type: 'connectionstatechange', connectionState: event.target.connectionState });
    });
    pc.addEventListener('negotiationneeded', (event) => {
        if (callback) callback('peer', { type: 'negotiationneeded' });
    });
    pc.addEventListener('icegatheringstatechange', (event) => {
        if (callback) callback('peer', { type: 'icegatheringstatechange', iceGatheringState: event.target.iceGatheringState });
    });
    pc.addEventListener('iceconnectionstatechange', (event) => {
        if (callback) callback('peer', { type: 'iceconnectionstatechange', iceConnectionState: event.target.iceConnectionState });
    });
    pc.addEventListener('icecandidateerror', (event) => {
        if (callback) callback('peer', { type: 'icecandidateerror', errorCode: event.errorCode, errorText: event.errorText });
    });
    pc.addEventListener('signalingstatechange', (event) => {
        if (callback) callback('peer', { type: 'signalingstatechange', signalingState: event.target.signalingState });
    });

    pc.addTransceiver( 'video', { direction: "recvonly" });
    pc.addTransceiver( 'audio', { direction: "recvonly" });

    var offer = await pc.createOffer();
    await pc.setLocalDescription(offer);

    await new Promise(resolve => {
        var timerid = setTimeout(resolve, timeout);
        pc.onicegatheringstatechange = () => {
            if (pc.iceGatheringState === "complete") {
                clearTimeout(timerid);
                resolve();
            }
        };
    });

    var input = {
        url: `${webrtc_base_url}/${name}/whep`,
        headers: {
            "Authorization": "Basic " + btoa(user + ":" + password)
        },
        content_type: "application/sdp",
        body: pc.localDescription.sdp,
        response_type: "text"
    };
    const answerSDP = await do_http(input);
    await pc.setRemoteDescription({ type: "answer", sdp: answerSDP });

    return pc;
}

// input: stream, user, password, timeout, name
async function webrtc_send_connect(input, callback)
{
    var { stream, user, password, timeout, name } = input;

    const pc = new RTCPeerConnection({
        iceServers: [ { urls: "stun:stun.l.google.com:19302" } ],
        iceTransportPolicy: 'all',
    });

    pc.addEventListener('track', event => {
        if (callback) callback('peer', { type: 'track', kind: event.track.kind, streams: event.streams, track: event.track });
    });
    pc.addEventListener('icecandidate', (event) => {
        if (callback) callback('peer', { type: 'icecandidate', candidate: event });
    });
    pc.addEventListener('connectionstatechange', (event) => {
        if (event.target.connectionState === "disconnected" || event.target.connectionState === "failed" || event.target.connectionState === "closed") {
            stream.getTracks().forEach(track => track.stop());
            console.log("PeerConnection disconnected, MediaStream stopped.");
        }
        if (callback) callback('peer', { type: 'connectionstatechange', connectionState: event.target.connectionState });
    });
    pc.addEventListener('negotiationneeded', (event) => {
        if (callback) callback('peer', { type: 'negotiationneeded' });
    });
    pc.addEventListener('icegatheringstatechange', (event) => {
        if (callback) callback('peer', { type: 'icegatheringstatechange', iceGatheringState: event.target.iceGatheringState });
    });
    pc.addEventListener('iceconnectionstatechange', (event) => {
        if (callback) callback('peer', { type: 'iceconnectionstatechange', iceConnectionState: event.target.iceConnectionState });
    });
    pc.addEventListener('icecandidateerror', (event) => {
        if (callback) callback('peer', { type: 'icecandidateerror', errorCode: event.errorCode, errorText: event.errorText });
    });
    pc.addEventListener('signalingstatechange', (event) => {
        if (callback) callback('peer', { type: 'signalingstatechange', signalingState: event.target.signalingState });
    });

    stream.getTracks().forEach(track => pc.addTrack(track, stream));

    // pc.addTransceiver("video", { direction: "sendonly" });
    // pc.addTransceiver("audio", { direction: "sendonly" });

    var offer = await pc.createOffer();
    await pc.setLocalDescription(offer);

    await new Promise(resolve => {
        var timerid = setTimeout(resolve, timeout);
        pc.onicegatheringstatechange = (event) => {
            if (pc.iceGatheringState === "complete") {
                clearTimeout(timerid);
                resolve();
            }
        };
    });

    var input = {
        url: `${webrtc_base_url}/${name}/whip`,
        headers: {
            "Authorization": "Basic " + btoa(user + ":" + password)
        },
        content_type: "application/sdp",
        body: pc.localDescription.sdp,
        response_type: "text"
    };
    const answerSDP = await do_http(input);
    await pc.setRemoteDescription({ type: "answer", sdp: answerSDP });

    return pc;
}

SwitchBot見守りカメラからの映像をMediaMTXに入力する

SwitchBot見守りカメラ Plus 3MPは、RTSPに対応しています。

(参考情報) Home AssistantにSwitchBotカメラ/テレビドアホンの映像を表示する

上記に記載されている通り、「SwitchBot」アプリにSwitchBot見守りカメラ Plus 3MPを登録後、詳細設定からカメラのアカウントを払い出します。
mediamtx.yamlに以下部分にを追記します。

mediamtx.yaml
paths:
  switchcamera:
    source: rtsp://【カメラアカウントのユーザ名】:【カメラアカウントのパスワード】@【カメラのIPアドレス】:554/live0

こうすると、「switchcamera」というパスがMediaMTXにでき、WebRTCなどで映像を受信することができます。

終わりに

MediaMTXは便利で楽しいです。

次回は、ESP32のカメラからの動画をMediaMTXで配信してみます。

以上

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?