24時間以上ストリーミングさせるとトラブルばかりで苦労した

![Qiitaタイトル用.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140663/9ef5c90c-6740-f505-3176-d4f36187d64e.jpeg)

# サマリー
**・ストリーミング映像を表示させるのは意外と簡単で難しい
・ストリーミング映像を長時間表示させたときに見舞われたトラブル
・トラブルの解消
・まとめ**

ここではIPカメラからの道路の映像をストリーミングで垂れ流し、それをYOLOX+motopy（トラッキング）で車両をリアルタイムで検出して渋滞情報を表示する、というシステム構築の苦労話を書きます。
同じシリーズですでにいろいろ書いており、「YOLOXの映像解析で車両の速度をAIで算出して渋滞を判定するのに苦労した～その３：ストリーミング対応編」となるのですが、今回はYOLOXとか渋滞とかはあまり関係がなく、同じようにストリーミングの長時間対応で苦労する人がいるんじゃないかな？と思い、ストリーミング特化のタイトルにしました。

そもそも開発したシステムは道路に設置したIPカメラをターゲットにしているので、ストリーミングは24時間365日に対応することを想定しています。逆を言うと、こんな膨大な映像データはいちいちムービーファイルに落としているとキリがないので、ストリーミングで垂れ流してバックアップは取らず、その都度リアルタイム処理するのが現実的なわけです。

ただし1時間や2時間のストリーミングでは問題なかったのですが、8時間を超えると画面のひきつりや突然のアプリダウンなど様々な問題が発生しました。ここでは長時間ストリーミングで発生した問題とその対応をメインに説明します。

なおYOLOX+motpyによるリアルタイム処理についてはこちらで[別途記事にしております](https://qiita.com/ComputerVision/items/5283a0ebbb59d5a4cb8f)のでよろしければご覧ください。
※リアルタイム処理については、inputがムービーファイルでもライブストリーミングでも特に処理性能上の違いはないです。

# ストリーミング映像を表示させるのは意外と簡単で難しい

### ライブストリームのプレビュー表示コードはとても簡単。
コードから書きますと、例えば[こちら](https://stackoverflow.com/questions/52594300/videocapture-works-properly-but-i-always-get-ret-false-for-the-first-few-fram)にPythonでムービーをプレビューウインドウで再生するとてもシンプルなコードがあります。こんな感じです。
~~~
import cv2
cap = cv2.VideoCapture(VIDEOFILENAME.mp4)
while True:
    retval, image = cap.read()
    if retval:
        cv2.imshow("Video", image)
    if cv2.waitKey(1) == 27:
        break
~~~
アプリを止める場合はプレビュー画面がアクティブな状態で、ESCキーを押せば止まります。
これはもともとムービー（VIDEOFILENAME.mp4）を再生するものですが、ここの部分を例えば
~~~
import cv2
cap = cv2.VideoCapture('rtsp://cameraID:cameraPW@192.168.10.11:554/live/ch0')
while True:
    retval, image = cap.read()
    if retval:
        cv2.imshow("Video", image)
    if cv2.waitKey(1) == 27:
        break
~~~
などrtspから始まるURLに書き換えると、RTSPプロトコルに対応したIPカメラからのストリーミング映像をプレビュー表示してくれます。

rtsp://cameraID:cameraPW@192.168.10.11:554/live/ch0
の部分については、カメラに接続するためのローカルIP（ここでは192.168.10.11）とID（cameraID)・パスワード（cameraPW）があらかじめ分かっている必要があります。
簡単ですね。

### rtspのパラメータが分からない
そして簡単ではない部分ですが、なぜか日本製IPカメラの多くはこのrtspから始まるURL？を公開していません。取扱説明書にも記載がありません（私が見たせまい範囲ですがそんな印象を受けました）。中華製のIPカメラはこの辺ゆるゆるらしく、アプリに記載があったりします。上記のURLはWansviewというメーカーのIPカメラで、ご丁寧に本体アプリ内にRTSPというメニューがあり、「FHDで見たいときはrtsp://～/ch0にしてね。SDで見たいときは、rtsp://～/ch1にしてね。カメラと同じネットワークね」という表示がありました（554はポート番号）。

また「これだ！」と思うrtspのURLが分かったら、いちいちPythonでコードを書かなくてもRTSPプレイヤーで表示を確認できます。有名どころはVLC media playerですね。
起動してメディアメニューから「セットワークストリームを開く」メニューを選ぶとこんなダイアログが開くので、上記のrtspを入力すれば再生されます。私もストリーミングの実装をするまで、VLC media playerにこんな機能があるの知りませんでした。
![VLC mediaplayer設定.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140663/21197bcf-f1a4-f955-5ad9-f84d9b787d52.jpeg)

とにかくライブストリームで物体検知させたい場合は、rtspが分かっているIPカメラを使うしかありません。メーカーに質問したら教えてくれるかもしれません（その場合はぜひ共有をお願いします）。

### こちらに都合の良いライブストリームはない

ここでは24時間渋滞が置きまくるライブストリームで物体検知部分をテストしたかったのですが、残念ながら都心でもそんな地獄のような道路は存在しないし、そんなところにIPカメラを設置するのは（許可も含めて）大変です。仕方がないので、適当に編集した交通量の多い道路の映像をノートPCでループ再生し、それをIPカメラで撮影して疑似的なライブストリーミングするという荒業に出ました（渋滞の閾値も下げました）。
![車両検出サンプル3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140663/b6fa2998-a85a-4fcb-d768-b9796d33fb8c.jpeg)

正直、画質は激悪です。静止画で見ると、車っぽい亡霊にしか見えません。しかし意外とYOLOXが頑張って物体検出してくれてビックリ。物体検出に使用した学習済みファイルはyolox_m.pthです。
ただたまに猫が飛び乗ったのか（猫飼ってないけど）、IPカメラの角度がズレて家族が写っていたりしてビックリすることもありました（以下はまだマシなほう）。

![カメラズレてる.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140663/fee56517-1535-2b71-85e1-fbdb4e44cecf.jpeg)

# ストリーミング映像を長時間表示させたときに見舞われたトラブル

ライブストリームをinputに無事設定出来た後、長時間システムを稼働させていくつかの問題点が出てきました。

### １）ストリーミングのエラー

「Unknown C++ exception from OpenCV code」というエラーが発生して、アプリが落ちることがありました。これは調べてみるとストリーミングで何らかのエラーが発生して、その後にカメラからの映像が取れていない状態が発生していたことが分かりました。

### ２）プレビュー映像がひきつる
このシステム(Traffic Jam Blade）では、プレビュー画面に検出結果を表示させます。実際の車両の検出状況や平均速度、台数や渋滞のステータスなども表示させているわけですね。
そのプレビュー画面がひきつると、場合によってはトラッキングがうまくいかなくなります。
プレビューがひきつるのは2パターンあり、まずは物体検知のリアルタイム処理が一時的に遅れてプレビューが遅くなる場合。こちらはトラッキングには影響がないため、見た目だけの問題で済みます（しかし見た目がよくない）。

問題なのは、ライブストリームが部分的に遅れたりコマが大きく飛ばされたりすることで、これはinput不備にあたり物体検知（特にトラッキング）に悪影響となり、同じ車両を2重カウントしたり速度が正しく出ない原因になります。私のWindows環境ではさらに「PC全体が一定時間フリーズしたようになる」という謎の現象に見舞われました。CPUやGPU、メモリなどの負荷が高くないにもかかわらずPCの時計まで数秒間止まってしまいます。

### ３）ffmpegがデコードで出すエラー

[h264 @ 000002b132db5940] error while decoding MB 0 0, bytestream -2
[h264 @ 000002b132db5940] left block unavailable for requested intra mode
[h264 @ 000002b132db5940] error while decoding MB 0 0, bytestream -10
などのエラーがたくさん出ました。
ただこれはアプリ停止にはならなかったので影響度は低いです。

# トラブルの解決

### １）ストリーミングのエラー⇒再接続を試みる

トラブルは複数の原因が絡み合っていたため、1個ずつ対応していきました。
まずは一番大きな「Unknown C++ exception from OpenCV code」エラーでアプリが落ちることへの対応です。これは「カメラからの映像が取れていない状態」と先に書きましたが調べてみると、フレームが空だったとき(sizeがゼロ)もしくはNone(カメラから映像自体返ってきてない)の両方があります。
ちなみにこの場合は、VLC media playerも終了していました。 
とりあえずは「映像が取れていない状態」になっていないか調べ、retvalがfalse(フレームが空と同じ状態)になった時は10回まで再接続するようにしました。

~~~
import cv2
import time

def main():
    """Main."""
    cap = cv2.VideoCapture('rtsp://cameraID:cameraPW@192.168.10.11:554/live/ch0')
    false_count = 0
    while True:
        retval, image = cap.read()
        if retval:
            cv2.imshow("Video", image)
        else:
            print(f'retval: {retval}')
            print(f'image: {image}')
            false_count += 1
            time.sleep(1 / 30)
        if false_count > 10:
            print(f'false_count: {false_count}')
            break
        if cv2.waitKey(1) == 27:
            break
    cap.release()

if __name__ == '__main__':
    main()
~~~

「映像がない」というエラーが発生する根本原因はよく分かりませんでしたが、この対処でアプリが落ちることはなくなりました。

### ２）プレビュー映像がひきつる⇒スレッド処理を追加する

映像がひきつるのはリソースが足りないからでは？と思い、スレッド処理を追加してみました。


~~~
import threading
import queue
import cv2
import time
import datetime


class ThreadingVideoCapture:
    # https://eqseqs.hatenablog.com/entry/2020/09/16/100000
    def __init__(self, src, max_queue_size=256):
        self.video = cv2.VideoCapture(src)
        # fps = self.video.get(cv2.CAP_PROP_FPS)
        # self.wait_sec = 1 / fps
        self.wait_sec = 1 / 500
        self.q = queue.Queue(maxsize=max_queue_size)
        self.stopped = False

    def start(self):
        self.thread = threading.Thread(target=self.update, daemon=True)
        self.thread.start()
        return self

    def update(self):
        while True:
            # print('q_size:', self.q.qsize())

            if self.stopped:
                return

            if not self.q.full():
                try:  # have to remove !!!!!!!!!!!!!!!!!!!!!!!!
                    ok, frame = self.video.read()
                except Exception as e:
                    if 'Unknown C++' in str(e):
                        print(e)
                        time.sleep(self.wait_sec)
                        continue
                    else:
                        raise e
                self.q.put((ok, frame))
                # print('pos:', self.video.get(cv2.CAP_PROP_POS_FRAMES))

                if not ok:
                    # self.stop()
                    # return
                    time.sleep(1)

            else:
                time.sleep(self.wait_sec)

    def read(self):
        # print(f'q_size: {self.q.qsize()}')
        return self.q.get()

    def stop(self):
        self.stopped = True

    def release(self):
        self.stopped = True
        self.video.release()

    def isOpened(self):
        return self.video.isOpened()

    def get(self, i):
        return self.video.get(i)

    def set(self, i, v):
        return self.video.set(i, v)


def main():
    """Main."""
    cap = ThreadingVideoCapture('rtsp://cameraID:cameraPW@192.168.10.11:554/live/ch0')
    cap.start()
    false_count = 0

    while True:
        retval, image = cap.read()
        if retval:
            cv2.imshow("Video", image)
        else:
            print(f'retval: {retval}')
            print(f'image: {image}')
            print(f'q_size: {cap.q.qsize()}')
            print(datetime.datetime.now())
            false_count += 1
            time.sleep(1 / 30)

        if false_count > 10:
            print(f'false_count: {false_count}')
            break

        if cv2.waitKey(1) == 27:
            break

    cap.release()


if __name__ == '__main__':
    main()
~~~

これを実行すると、処理が終わらずにたまっているフレーム数をq_sizeで表示してくれます。
フレームがたまりすぎていて、それを処理しきれずにプレビューの「ひきつり」が起きている、という想定です。q_sizeがあまりにも多い場合は、これは物体検知パートではなくストリーミングの受信に問題があるため、受信サイズをFHDではなくSDにするなどの工夫が必要となります。
スレッド化とq_sizeの「見える化」で、なんとかプレビュー画像のひきつりはほぼ抑え込むことができました。

### ３）ffmpegがデコードで出すエラー⇒コーデックを指定した

これはh264系が多かったので、VideoWriter_fourccにコーデックとしてh264を指定すると軽減できまし
た。しかしそれでもエラーはゼロにはならなかった（3時間に1個くらい出る）。他の問題があるかもしれないが、優先度が低いので放置しています。

# まとめ
よく分からない原因でIPカメラからの映像が途切れたり、プレビューのひきつりが起きたりするので対応は十分考えよう。今回の対応で72時間は問題なく動きました。ただし1か月とか1年とか稼働させると別のレアな問題が発生する可能性があるかも？あとネットワーク帯域が良くない環境ではそもそもストリーミング受信はできないかも？

# 関連記事
・[2023年：裏技公開！ AI映像解析で物体検出精度をあげる簡単テクニック](https://qiita.com/ComputerVision/items/1bb9861d0b55b0161818)
・[2022年：AI映像解析で車両の速度を自動算出すると異常値ばっかりで苦労した](https://qiita.com/ComputerVision/items/fcd39dcb272edf37518c)
・[2022年：YOLOXの映像解析で車両の速度をAIで算出して渋滞を判定するのに苦労した～その１：リアルタイム処理編](https://qiita.com/ComputerVision/items/5283a0ebbb59d5a4cb8f)
・[2022年：YOLOXの映像解析で車両の速度をAIで算出して渋滞を判定するのに苦労した～その２：速度算出手法編](https://qiita.com/ComputerVision/items/a86f20673df6b6536345)