🐷

続・IPカメラの動画をWEBに表示しようとしたら地獄だった話

2023/11/02に公開

まさか続くとはね...

本記事は「カメラ動画をWEBに表示しようとしたら地獄だった話」の続きです。
解決したと思っていましたが、いや、していたのですが、新たな仕様に更に頭を抱えることになりましたので、続編を書きます。

キーワード

キーワード多すぎて(ほぼ関係ないものもありますが)ざっと並べます。
IPカメラ(ネットワークカメラ)、RTSP、RTMP、HLS、Node、npm、ws、wss、rtsp-relay、@ffmpeg-installer/ffmpeg、ffmpeg、node-media-server、Vue、Nuxt、Express、SSR、エックスサーバー、VPS、Nginx、Ubuntu

前回のあらすじ

IPカメラの映像をWEB上で低遅延で表示したい(と同時に物体検知側でも映像を利用したい)という問題に取り組みました。
最終的には以下の解決策に落ち着きました。
・エックスサーバーのVPSを借りる
・プロトコルはRTMPを使用する
・ポート開放してIPカメラ映像を取得する
・映像配信にはライブラリ「rtsp-relay」を使用する(JSMpeg × WebSocket)。
・Nuxt2のSSRで、バックエンドは「serverMiddleware」に記述する。

前回の補足・界隈の情報

前回はまだ知識が浅い状態で記事を書いていたので、説明の弱い部分がありました。ちょっと補足します。
*誤りは指摘をお願いします。

映像の配信方式と構成

IPカメラ(でなくとも動画)の映像を配信する方法は大きく2つあると思います。
一つ目はカメラに映像を取りに行く方法です。プルと呼んだりします。
二つ目はカメラから映像を送り付ける方法です。プッシュと呼んだりします。

プル方式

プルには以下の問題があると"思われます(イメージ)"。

  • カメラから取れるストリームが限られる:
    カメラの性能にもよりますが、たくさんの人が一つのカメラに「映像ください」と集まることになります。カメラの処理速度や通信網など、何処かで限界が来ます。今回使用したカメラはストリーム2本で限界でした。
  • カメラにアクセスできる必要がある(設定、セキュリティの問題):
    前回ポート開放をしたと思います。カメラに映像を貰いに行くためには、カメラにアクセスできる必要があります。つまり、自宅のLANに他人が入ってくるわけで、あまり良い状態ではありません。ちなみに、今回使用した「H.View 防犯カメラ ( https://amzn.asia/d/aHwii2I )」にはポート開放を不要(或いは便利)にする「UPnP」や「DDNS」の機能も付いていますが、今回は割愛します。
  • 配信ソフト(OBS等)は基本対応していない。
    上記の問題があるため、基本的に配信ではプルが使われることはないでしょう。

ただし、カメラから直接映像を受信するため、低遅延や画質の面ではプッシュ方式より優れていると思われます。

プッシュ方式

プッシュ方式は、様々な配信環境で利用されています。例えばOBSからYouTubeで配信する場合、YouTubeから送り先を教えてもらい、OBSからそこに送り付けます。ツイキャスなどでも同様です。

こちらの問題は、プッシュされた映像を受け取る中継サーバを用意する必要があることでしょう。
なお、プッシュされた映像は、各視聴者にはプルで配信されることになります。プッシュのみでは基本的に視聴できません。

一般的な構成

一般的な映像配信は上記のプッシュとプル両方を組み合わせて行われます。
ざっくり示すと以下のような形です。

「A.IPカメラやOBS」 -プッシュ→ 「B.メディアサーバ」 -プル→ 「C.視聴者」

映像配信の手順としては

  • 1.配信者はメディアサーバのURL等(映像の送り先やパスワード)を入手する
  • 2.IPカメラや配信ソフトに送り先等を設定する
  • 3.映像を送信(プッシュ)する
  • 4.「B.メディアサーバ」が配信の多重化や動画形式の変換を行う
  • 5.「C.視聴者」がアプリ等を通して「B.メディアサーバ」に映像を要求する
  • 6.「C.視聴者」に映像が配信される(プル)

このような形です。
一般的には、AからBへのプッシュにはRTMP(場合によってはRTSP)が使用されます。
しかし、映像の配信、特にWEBへの配信では、(Flash Playerが廃止された今となっては)RTMPのままでは再生できません。Bのメディアサーバで動画形式の変換が必要です。
よって、WEBでの動画視聴を考える場合、上の構成を理解しておく必要があります。

メディア(ストリーミング)サーバの用意・一覧

さて、本記事では一貫してメディアサーバを自前で用意する方針で進めていますが、本来は、特に大規模な配信を実装する場合は、既存のサービスを利用するべきです。既存のサービスは用途に合わせて高度に詳細にチューニングしてあり、個人で開発するよりも、低遅延・高画質・安定性などにメリットがあるはずです。

以下に既存のサービスを並べてみます。探してみるとかなりあります、ご参考に。

  • Wowza(Wowza Streaming Cloud):管理ポータルGUI便利
  • AWS(AWS Elemental MediaLiveなど):AWSのフルマネージドサービス
  • Red5 Pro:Wowzaの対抗馬(参考:https://www.samuraiz.co.jp/red5pro/column/06.html)
  • ImageFlux Live Streaming(SAKURA):噂では良いらしい
  • Agora.io
  • UIshare
  • necfru
  • SmartSTREAMビデオプラットフォームサービス
  • クラストリーム
  • viaPlatz
  • メガDOGA
  • millviポータル
  • Cloud Video Intelligence API(GCP):おまけ、本来は動画周辺の便利機能がメインのようですが、配信周りの機能もあるらしい

さて、こういった諸々のサービスがある中で、何故自前で構築するのか、その理由は

  • 大規模配信は不要
  • 自前の方が自由度が高い
  • APIやSDKの勉強が面倒
  • 高い!!

です。
一番の理由はサービスの利用料金が圧倒的に高いことです。上のサービスも料金を調べてみてください。月額費用が比較的安いものもありますが、騙されてはいけません。料金は配信時間で変わりますので詳細に確認してください。

何故高いか、最近のトレンドの仕組みはAWSの上に構築されているものがほとんどですが、AWSは通信量に対してかなり高い金額を請求します(ぼったくりではありません)。それ故、高画質動画の無制限配信などした日には高くならざるを得ません。

YouTubeを無料で視聴しているあなた、奇跡です。通常広告がついたからと言って無料で見れるものではありません。YouTubeの広告をブロックしようなど、もっての外です、うん。

さて、そのような環境で適切に動画を制御しようとすると、サービスのAPIやSDKをきちんと把握して実装しなくてはなりません。サーバに多少なり知識があるならば寧ろ、自前でサーバを用意したほうが楽、と思わなくもないわけです。

配信プロトコルの復習

ここらで配信のプロトコルについて復習しましょう。プロトコルについて知ることで、何故メディアサーバが必要なのか、今使われる仕組み、これから標準となる仕組みが見えてきます。
なお、このプロトコルには、動画形式・データ保持形式・配信方式など様々な要素があるので、それぞれが同様の立ち位置ではなく、組み合わせて使われる場合などもあるかと思いますが、ここでは一列横並び&主観で紹介します。

RTMP:Real-Time Messaging Protocol

Adobeが開発した。古い。再生に「Flashプレーヤー」が必要だったが廃止された。にもかかわらず現役。異常事態。通信の安定性が高い。

RTSP:Real Time Streaming Protocol

古い。高画質な動画配信に向いているが、そもそも通信が安定しないため結果としてはほとんど用いられていない。とはいえ例えば同LAN内では安定するので、エッジで直接処理したり、変換する前提ではそこそこ使用される。

SRT:Secure Reliable Transport

Haivision社(誰?)が開発。インターネット経由で高品質且つセキュアな低遅延配信を行う。最強やん。超新しい、というか次世代技術。
まだまだ新しいのに既にいろんなライブラリが対応していたり、なんとOBSも対応しているらしい。もう確実に主権取るやんけ。とはいえ普及はまだ全然。あとWEBへの配信はやっぱり変換しないとダメ。

HLS:HTTP Live Streaming

Appleが開発したHTTPベースのストリーミングプロトコル。つまり「Flashプレーヤー」無き今のWEB用。比較的新しい。超人気。動画を分割して保持しておくので基本的にストレージを持たないIPカメラなどは直接は取り扱えない。し、まあまあ遅延する。

HDS:HTTP Dynamic Streaming

Adobe版HLS。存在感0。多分Wikiすらない。

DASH:MPEG-DASH:Dynamic Adaptive Streaming Over HTTP

ISO規格。HLSとほぼ同じだけど少し便利な標準規格。比較的新しい。HLSを置き換えるか、、、。動画を分割して保持しておくので、基本的にストレージを持たないIPカメラなどは直接は取り扱えない。し、まあまあ遅延する。

CMAF:Common Media Application Format

AppleとMicrosoftが開始、Google、Netflix、Akamaiなども参加。HLSとDASHをくっつけてもっと便利にしようという仕組み。新しい。多分まだほとんど使われてない。ツイキャスは2021年に導入したらしい。ほとんど情報もないのにどうやって、、、すごい。

WebRTC:Web Real-Time Communications

ウェブブラウザやモバイルアプリの通信の安定しない状況でリアルタイム通信を実現するための仕組み。そこそこ新しい。P2Pが基本なので他の仕組みとは毛色が異なる。低遅延。WEB会議とかでそこそこ使われている。
ちなみに「WebRTC SFU」はP2Pではなくサーバを介する。
仕組みはかなり複雑らしく、サーバ等を含めた自前実装はやめたほうがいい(らしい)。

JSMpeg

これもちょっと毛色が違うが、JavaScriptのMPEG1ビデオおよびMP2オーディオのデコーダライブラリ。WebGLおよびCanvas2Dレンダラーで映像表示、およびWebAudioサウンドで音声再生を行う。
技術は古いがライブラリはそこそこ新しい。
動画を送ろう、ではなく、WebSocketで画像情報をガンガン送り付けてキャンバスに表示したら動画になるやん、みたいな発想か?(厳密にはちゃんと動画を送ってJSでデコードしてる、多分)。
兎に角低遅延、一方で安定した正確な動画情報ではない。

参考:https://qiita.com/daisukeoda/items/2f1badd24980c4fc3195

低遅延配信と安定配信

映像配信、特にライブ配信には、低遅延と安定配信の両方が求められます。一方、一般的には、その二つは相反するものです。安定した配信のためにはバッファが必要で、欠落した部分を再度配信してもらったり、届いたデータを正しく並び替えたりできる一方、バッファを取れば当然配信は遅延します。

SRTのように、最近のプロトコルではそれを両立する試みもありますが、少なくとも現在主流の仕組みでは両立は難しく、そしてほとんどの場合、「安定」が優先されます。
YouTubeの動画でも、ネトフリだろうがHuluだろうが、配信がリアルタイムである必要はほとんどないからです。また、ツイキャスやYouTubeのライブ配信では、コメントに反応するためのある程度のリアルタイム性が必要ですが、それは5秒程度の遅延であって、遅延を1秒以内に抑えていというレベルではありません。

WEB会議など、遅延を1秒以内に抑えたい、という場合にはWebRTCのような技術が使用され、これはむしろ安定性をある程度犠牲にしたものです。その上で、WebRTCはP2Pという特殊性から、いろんな場面への活用は難しいと言えます。

しかし、なんとなんと今回私の直面した問題は、規模は小さいとはいえその両方を解決しなければなりませんでした。
というのも、今取り組んでいる仕組みは、カメラ映像から物体検知を行うという安定配信が求められるものでありながら、一方で、ユーザがWEB上でカメラ映像を見ながら設定を行う必要のある、5秒~10秒の遅延は許されない仕組みだからです。

そこで前回私がとった作戦は、「カメラに直接アクセス」して、物体検知側はRTMPで安定した映像を受け取る、WEB側は遅延の少ない「JSMpeg」を利用する、というものでした。

ところが、、

新たな問題

ここで新たな問題、というか仕様変更、というか要件の追加、が行われました。
各問題を以下に示します。

  • カメラのストリームが弱い:カメラの性能はさして良いものではなく、ストリームを2本取るとギリギリの状態でした。2本はそれぞれ「物体検知」側と「WEB配信」側で使用されており、これ以上の活用が難しく、また配信が途切れないか怖い状況でした。
  • ネット環境が弱くても使いたい:本システムを実証するにあたり、ネットワークの弱い環境でも動作することが求められました。そもそもが2本別々に送信しなければならないことに加え、負荷を抑えるために5fpsに制限しましたが、これがさらなる問題を生むことになりました。
  • ポート開放は嫌だ:利用者側にポート開放を行ってもらう操作が難しく、セキュリティの面からもポート開放は中々許されない環境も多く、対応が必要になりました。

解決へ

まずはメディアサーバを用意する

そもそも前回まではカメラに直接つないでいました。これがストリームが弱い問題を生んでいたわけで、メディアサーバを立てて、そこで多重化すれば解決です。
メディアサーバはNuxtを配置してSSRしているエックスサーバー上に「Node-Media-Server」を利用して設置しました。すごく簡単にできました。

RTMPプッシュを使用する

メディアサーバができたのでポート開放する必要はありません。IPカメラからメディアサーバにプッシュすれば良いのです。これでポート開放の問題も解決です。

5fpsに抑える

ネットワークの強度の問題は、動画を5fpsに抑えることで対応しました。現代のネットワークであれば、5fpsにしてビットレートを抑えれば、まずまず動くと思われます。

じゃあ配信もそのサーバにさせたらいっか

これは副次的なものですが、これまでNuxtのSSRとして、バックエンドで行っていたJSMpegによるWebSocket経由の動画配信も、バックエンドを切り出してエックスサーバで動作させることにしました。これにより、フロントエンドのアプリが静的でよくなり、適当なレンタルサーバにも配置できるようになりました。
当然、これは前回時点でも可能だったことですが、前回は前提としてVPSは使用したくない、SSRで何とかしたい、という部分があったので、結局諦めたこの段で切り離しました。

ところが問題発生

これで問題解決ですね、めでたしめでたし、とはならないのがプログラマの暗黒面というか、世の中は甘くないというか。
新たに以下の問題が発生

  • 映像がカクカクする:動画が1秒ごとに不安定にカクカクする
  • 中国のライブラリって怖かったりする?:前回も登場した「Node-Media-Server」、はっきりとは書いてありませんが、中国の方が開発しているっぽい?説明が求められると少し面倒かな、と思う節はありました。

映像カクカク問題

今回の構成にした結果、何故か動画が止まったり、高速再生されたり、を1秒ごとにそれを繰り返すようになりました。
恐らく、5fpsを30fpsとして再生しようとして、1/6秒で1秒分再生、5/6秒停止、を繰り返しているのだと思います。

この問題、原因不明です。

まず、今回の構成は次の形です。
IPカメラ -RTMP(プッシュ)→ 「Node-Media-Server」 -RTMP(プル)→ 「rtsp-relay」 -JSMpeg→ WEBアプリ

そこでIPカメラに直接つないで「rtsp-relay」経由で再生してみると、問題ありません。
IPカメラ -RTMP(プル)→ 「rtsp-relay」 -JSMpeg→ WEBアプリ

次に、Node-Media-Serverの配信する映像をVLCで確認しても、問題ありません。
IPカメラ -RTMP(プッシュ)→ 「Node-Media-Server」 -RTMP→ VLC

つまり、使用しているのは「Node-Media-Server」と「rtsp-relay」で、それぞれ単体では問題なく動いています。

配信形式を変えてみる

原因不明の為、映像配信形式を変えてみました。「Node-Media-Server」はHLSやDASHに対応しているため、それぞれに変換してWEBで再生してみました。

が、駄目!!
映像カクカク問題は解消されましたが、調整しても遅延が10秒~1分以上というかなりひどい状態になり、この方法は断念しました。

FFmpegの設定を変えてみる

「rtsp-relay」ではFFmpegのフラグを設定できます。そこで、動画のfpsを変更する「-r 30」や「-vf fps=30」などで30fpsに変更して送信させる方法を試しましたが、これも理由不明ですがうまくいきませんでした。

まさかの解決「nginx-rtmp-module」

この構成では万策尽きたため、「Node-Media-Server」や「rtsp-relay」などのライブラリに頼るのを辞め、直接FFmpegで動画を変換するべく、まず「Node-Media-Server」を「nginx-rtmp-module」に置き換えました。
これも非常に簡単で、インストールして設定ファイルを少しいじるだけで実装できました。

なお、実験時映像配信にOBSを使用しましたが、エンコーダが特殊なものになっており、音声は届くのに映像が再生されない事象に悩まされました。普通にx264とか使用してください。

参考:https://symfoware.blog.fc2.com/blog-entry-2472.html

さて、「Node-Media-Server」が置き換わったので、次はFFmpegの設定と「rtsp-relay」の置き換えだなあ、この辺は大変そうだなあ、、、
取り合えず今の状態で試してみるか、と思い駄目元で試したところ、、、なんと上手く動くではありませんか。

変なところであっけなく解決してしまったのです。どうやら原因は「Node-Media-Server」にあったようだ。
とはいえ、「Node-Media-Server」が駄目、というわけではないところは補足しておきます。「Node-Media-Server」は「映像の安定した配信」を目指すライブラリであって、「低遅延」を目指すライブラリではないのです。現にDASHやHLS、及びバッファを取って再生するVLCでは正常に再生されています。これは相性の問題である、と思います。

最終的な構成

最終的な構成ですが

IPカメラ -RTMP→ 「nginx-rtmp-module」 -RTMP→ 「rtsp-relay」 -JSMpeg→ WEBアプリ

「物体検知」側は「nginx-rtmp-module」の多重化したRTMPを受け取って処理することになります。

続続・IPカメラの動画をWEBに表示しようとしたら地獄だった話

ここから2024年2月22日追記、
さて、これまでちゃんと動いていたこの構成ですが、実環境で動かすと以下のエラーが、、、

Uncaught RuntimeError: memory access out of bounds

意味は書いてある通りなのですが、つまるところ動画表示するにはメモリが足らんということです。
メモリといっても、ブラウザが確保してるメモリやPCのメモリの話ではなく、「JSMpeg」が使用する「WebGL」、が使用できるメモリに制限がかけてあり、それを超えているという話です。ですから、良いパソコンを使ったら、という話ではないです。

問題は、テスト時1280×720pで実行しており動いていたのですが、昼間に実環境で動かすと上記のエラー。夜に動かすとエラーは出ない。
つまり、色や動画の内容でメモリ使用量が変わり、1280×720pあたりにまさにちょうどメモリの制限があるということですね。

解決策

今回対策しなければならないのはWEBアプリ側のみで、WEBアプリ側は正直720pとかいらないので、画面サイズを640×480pくらいに圧縮することにしました。
640×480p"くらい"というのは、「scale=640:-1」と記述し、横幅が640で縦横比が変わらないようにリサイズするので、高さは480とは限らない、ということです。

具体的には、「rtsp-relay」を使用してウェブソケットで動画を配信している部分のプログラムを以下のようにしました。

app.ws(`/api/openStream/:IP`, (ws, req) => {
        proxy({
            url: decodeURIComponent(req.params.IP),
            verbose: true,
            additionalFlags: ['-q', '1', "-vf", "scale=640:-1"],
        })(ws, req);
    }
);

重要なのは「additionalFlags」の部分で、これまで「'-q', '1'」としていた部分に「"-vf", "scale=640:-1"」を追加しました。
これは、「rtsp-relay」の実行する「FFmpeg」コマンドに「-vf scale=640:-1」フラグを追加するという意味です。当然、「'-q', '1'」の1部分を下げることで画質を下げ、メモリ使用を下げることは可能ですが、いくら画質を下げても元動画が大きければメモリを食うので、今回は画面サイズを下げることで対応しています(厳密にはすっごい縦に細長い動画を送り付けられたらこれではダメですが、そこまでは面倒見切れません)。

本当の終わり

今回は一つの記事にするほどの変更ではなかったので(ここにたどり着くまでは紆余曲折ありましたが)、追記する形にしました。
本当にストリームをWEBで扱うのは面倒なものです。
また何かあれば追記します。

Discussion