WEBカメラのURLがわかればコマンド一つで録画できるアプリ作ってみた

# 動機
監視カメラで録画した動画を差分検知をする必要があったんですがネット上には作ってみた、やってみた系しかなく、簡単に解析、録画できるアプリが欲しいと思い作成しました。

# 前提条件
* docker composeがインストールされていること
* もしくは[poetry](https://python-poetry.org/)が実行できる環境であること

dockerの内容はpoetry環境をラップしているだけです。

# アプリ
[webcamrecorder](https://github.com/kwhrkzk/webcamrecorder/tree/main)

# 使い方
[README.md](https://github.com/kwhrkzk/webcamrecorder/blob/main/README.md)に書いてみましたが日本語でも書いてみます。

1. git cloneする
1. .env.exampleを.envにリネーム
1. .env内のWEBCAMERA_URLを録画したいWEBカメラのものにする
1. 以下のコマンドをディレクトリ内で実行

```bash
docker compose run webcamrecorder record
```

これでstoreディレクトリに動画が保存されます。

# 設計図

## コンテキストモデル

```mermaid
graph TD

subgraph InputSource
  WebcameraSource
  VideoSource
end

VideoSource --> webcamrecorder
WebcameraSource --> webcamrecorder
webcamrecorder-- email --> SMTP_Server
webcamrecorder --> Motion-detected_video
webcamrecorder --> video
OS -- Periodic execution --> webcamrecorder
user -- Manual execution --> webcamrecorder
```

入力源として動画とWEBカメラを考えています。
出力としては録画した動画と差分検知した部分を抜き出した動画を考えています。
差分検知した際に通知する機能としてとりあえずメールを用意しました。

定期実行はosの仕組みに任せます。
24時間動かし続ける場合はdocker-compose.ymlにてrestart: alwaysとかにすれば不活監視しなくても良い気がします。

## クラス図

```mermaid
classDiagram
 class VideoSource {
   +createFrame()
   +flash(path: str)
 }
 class WebcameraSource {
   +createFrame()
   +flash(path: str)
   +record(path: str)
 }

 class Frame {
   +encode(): bytes
 }
 VideoSource --> Frame
 WebcameraSource --> Frame
 VideoSource -- FrameQueue
 WebcameraSource -- FrameQueue
 FrameQueue -- Frame
 class MailSender {
   +send(frame: Frame, subject: str, body: str)
 }
 MailSender -- Frame
 class FrameQueue {
   -queue: dequeue<Frame>
   +enqueue(frame: Fraem): None
   +dequeue(): Frame
 }
 class IAnalyzeVideo {
  <<interface>>
  +analyze()
 }
 IAnalyzeVideo -- VideoSource
 class IContinuousRecording {
  <<interface>>
  +record()
 }
 IContinuousRecording -- WebcameraSource 
 class ITimerRecording {
  <<interface>>
  +record()
 }
 ITimerRecording -- WebcameraSource 
 class IDetectMotion {
  <<interface>>
  +detect()
 }
 class IEventRecording {
  <<interface>>
  +record()
 }
 IEventRecording -- WebcameraSource 
 IEventRecording -- ISendMail
 IEventRecording -- IDetectMotion 
 class ISendMail {
  <<interface>>
  +send(frame: Frame)
 }
 ISendMail -- Frame
 ISendMail -- MailSender
 class ITimerEventRecording {
  <<interface>>
  +record()
 }
 ITimerEventRecording -- WebcameraSource 
 ITimerEventRecording -- ISendMail
 ITimerEventRecording -- IDetectMotion 
```

・集約
* Frame(動画やWEBカメラの１フレーム)
* WebcameraSource(WEBカメラの入力源)
* VideoSource(動画の入力源)
* MailSender(SMTPメーラー)

・ファーストクラスコレクション
* FrameQueue(フレームのリスト管理)

・ユースケース
* IContinuousRecording(常時録画)
* IEventRecording(差分検知録画)
* ITimerRecording(常時録画。指定した分数でアプリ終了)
* ITimerEventRecording(差分検知録画。指定した分数でアプリ終了)
* ISendMail(メール送る)
* IDetectMotion(差分検知する)
* IAnalyzeVideo(動画を解析する)

で考えました。ユースケースはDIしてるので差替え容易です。
IDetectMotion(差分検知する)は使うカメラや検知したい内容によって大きく変わるので各々が調整する必要がありそうです。

# 詳しい使い方
コマンドは以下の５つを用意しました。

## record
常時録画です。以下のコマンドで起動します。

```bash
docker compose run webcamrecorder record
```

もしくはdocker-compose.ymlを以下のように書き換えて

```yml:docker-compose.yml
version: '3.7'
services:
  webcamrecorder:
    build: .
    container_name: "webcamrecorder"
    user: root
    restart: always
    volumes:
      - .:/webcamrecorder
      - ${COMPOSE_SAVE_PATH}:/webcamrecorder/store
    working_dir: /webcamrecorder
    entrypoint: poetry run python3 -m webcamrecorder
    command: record
```

```bash
docker compose up -d
```

を実行するとパソコンの電源が落ちたりアプリが異常終了しても録画を再開します。

.envで関係する変数は以下の通りです。

 | name | explanation | default value |
 | ---- | ----------- | ------------- |
 | WEBCAMERA_URL | webcameraのurl. | rtsp://user:password@ip:port/xxx.sdp |
 | WEBCAMERA_FPS | webcameraのfps | 24 |
 | WEBCAMERA_OUTPUTFILENAME | 出力するファイル名のサフィックス。./storeに出力されます。ファイル名は${WEBCAMERA_OUTPUTFILENAME}_yyyymmddhhmmss.mp4 になります。| webcam1 |
 | WEBCAMERA_RECORD_MINUTES | 分単位で録画する分数を設定します。 | 10 |

## event
WEBカメラで差分を検知したら一定フレーム録画します。以下のコマンドで起動します。

```bash
docker compose run webcamrecorder event
```

常時監視するならば*record*と同じようにすれば良いでしょう。

.envで関係する変数は以下の通りです。

 | name | explanation | default value |
 | ---- | ----------- | ------------- |
 | WEBCAMERA_URL | webcameraのurl. | rtsp://user:password@ip:port/xxx.sdp |
 | WEBCAMERA_FPS | webcameraのfps | 24 |
 | WEBCAMERA_OUTPUTFILENAME | 出力するファイル名のサフィックス。./storeに出力されます。ファイル名は${WEBCAMERA_OUTPUTFILENAME}_yyyymmddhhmmss.mp4 になります。| webcam1 |
 | N_FRAME_BEFORE_DETECTED | 検知して何フレーム前から録画を開始するか設定します。遡れば遡るほどメモリを食うので機器と相談して値を決めます。 | 100 |
 | N_FRAME_AFTER_DETECTED | 検知して何フレーム目まで録画するかを設定します。指定した範囲内でまた検知したらその時点から数え直します。 | 300 |
 

メールを送るなら以下の設定も必要です。 

 | name | explanation | default value |
 | ---- | ----------- | ------------- |
 | SMTP_SERVER_ADDRESS | smtp serverのaddress | smtp.gmail.com |
 | SMTP_PORT | smtpのport | 465 |
 | SMTP_ACCOUNT | smtpのaccount | user@gmail.com | 
 | SMTP_PASSWORD | smtpのpassword | password |
 | MAIL_INTERVAL_MINUTES | メールを送ってから次を送るまでの間隔を指定します。たくさん検知してたくさんメールが送られてくるのを防ぎます。 | 1 |
 | MAIL_SUBJECT | mailの題名 | subject |
 | MAIL_BODY | mailの本文 | body |

## timer
指定した時間だけ録画してアプリを終了します。タスクスケジューラなどで定期実行する際に使用するイメージです。

```
docker compose run webcamrecorder timer
```

.envは*record*と同じです。*WEBCAMERA_RECORD_MINUTES*分録画したら終了します。

## timerevent
指定した時間だけ監視してアプリを終了します。タスクスケジューラなどで定期実行する際に使用するイメージです。

```
docker compose run webcamrecorder timerevent
```

.envは*event*に加えて以下の設定を使います。

 | name | explanation | default value |
 | ---- | ----------- | ------------- |
 | WEBCAMERA_RECORD_MINUTES | 分単位で録画する分数を設定します。 | 10 |

## video
録画済みの動画から差分検知して動画を抜き出すために使用します。

```
docker compose run webcamrecorder video --path store/***.mp4
```

*--path*で動画ファイルを指定します。解析したい動画はstoreに格納してから実行したほうが良いでしょう。


 | name | explanation | default value |
 | ---- | ----------- | ------------- |
 | N_FRAME_BEFORE_DETECTED | 検知して何フレーム前から録画を開始するか設定します。遡れば遡るほどメモリを食うので機器と相談して値を決めます。 | 100 |
 | N_FRAME_AFTER_DETECTED | 検知して何フレーム目まで録画するかを設定します。指定した範囲内でまた検知したらその時点から数え直します。 | 300 |

# 終わりに
とりあえず自分が所持しているWEBカメラ(rtsp)では動作しました。
元はopencvなのでhttp指定でも動作するような気はしてます。
自分は今後は日中の時間だけ差分検知するために使用しようと思います。

しかし録画アプリって以下の変数で動作が大きく変わるため一般化は難しいとも思いました。
* 使用するカメラ
* 動画の解像度。fps
* アプリを動作させる機器のマシンパワー
* カメラと機器の間のネットワーク環境
* 検知する方法とか画面のどこをマスクするかとか

pythonも今回本格的に触ってみたのですがコマンドアプリを作るのがとても楽でした。
インタプリタ言語でアプリを作る際は採用しようと思います。