MediaMTXを使って、WebRTCまたはRTSPで映像を配信します。
WebRTCにもSFUという1対多の通信方式がありますが、少々面倒です。MediaMTXを使うと1対1で通信したものが、簡単に複数に配信することができます。
また、いろんな通信方式も取り込めるのもいいです。例えば、後述しますが、ESP32のカメラ映像も取り込んだり、SwitchBotの見守り映像を取り込むことができます。
私の自宅では、QNAPのNASがありますので、そこにMediaMTXをDockerを使ってセットアップします。
以下の図のような構成となります。
QNAPにMediaMTXをインストール
QNAPにDocker(Container Station)を使ってインストールします。
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にします。
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だけにしたかったので以下に変更
rtspTransports: [tcp]
MediaMTXを再起動します。
管理コンソールで確認
設定状態や動作状態を確認するための簡単な管理コンソールを用意しました。
さきに、以下の部分にセットアップしたMediaMTXのホスト名またはIPアドレスを指定します。
const base_url = 'http:// 【MediaMTXサーバのホスト名】:9997';
以下をブラウザから開きます。
public/mediamtx_console/index.html
更新ボタンを押すと、現在の設定状況を確認できます。
ちょっと試しに、ブラウザを開いているPCまたはスマホに接続されているカメラをMediaMTXに配信してもらいましょう。
右側のPublishボタンを押します。
nameは適当にpccamera、user/passwordは、先ほどmediamtx.yamlに設定したuser/passwordを設定します。
そうすると、カメラの使用の許可を求められたのち、成功すると、Publishの右に赤い●が表示されます。
更新ボタンを押すと、pathsにpccameraが追加されたことがわかります。
pccameraの右端に表示ボタンがありますので、押すと配信されている映像を見ることができます。
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のホスト名とポート番号に変更します。
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を以下のように変更します。
webrtcIPsFromInterfacesList: [br0, ppp0]
webrtcAdditionalHosts: [【HTTPSでのホスト名】]
webrtcICEServers2:
- url: stun:stun.l.google.com:19302
webrtcIPsFromInterfacesListは、WebRTCのネゴシエーションの際に、余計なネットワークを返さないようにするための指定です。webrtcAdditionalHostsも同様で、外部からWebRTC接続する際のホスト名になります。
以下は必要に応じて変更します。
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で映像を受信する場合です。
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に以下部分にを追記します。
paths:
switchcamera:
source: rtsp://【カメラアカウントのユーザ名】:【カメラアカウントのパスワード】@【カメラのIPアドレス】:554/live0
こうすると、「switchcamera」というパスがMediaMTXにでき、WebRTCなどで映像を受信することができます。
終わりに
MediaMTXは便利で楽しいです。
次回は、ESP32のカメラからの動画をMediaMTXで配信してみます。
以上



