一定時間 HLS ストリーミングが再生されなかったら配信止めたくない? with Lua + Python3

# はじめに
## 何の記事なの?
nginx-rtmp-module を使ってストリーミング配信しているとどうしても『観たり聴いたりしてない間は止めてもいいかなー』という｢オフタイマー｣な機能が欲しかったのでふとした思いつきで実装してみることにしました｡
Python はそこそこ､ Lua はほぼ初めてな人ですが完全に｢なんとなく｣で実装しています｡

一応誕生日ネタではあります~~が､後日｢[個人開発 Advent Calendar 2023](https://qiita.com/advent-calendar/2023/personal-developement)｣に枠が残っていたらこの記事を書くことになったきっかけになった話やそのコードを出そうと思います(予定)~~｡

## Q: こんなん誰が得すんのさ?
A: (少なくとも)自分は得しましたw

## 前提設定内容
:::note warn
今回関係ある見せられる部分だけ抜き出して内容を差し替えています｡
また､ファイル名やパラメーターは後日｢個人開発 Advent Calendar(ry｣に載せる(かもしれない)ものとは違うものにしています｡
:::
```nginx:/etc/nginx/rtmp.conf
# configuration file /etc/nginx/conf.d/rtmp.conf:
rtmp_auto_push off;
rtmp {
    server {
        listen              1935;
        access_log          /var/log/nginx/rtmp_access.log;
        chunk_size          4096;
        timeout             1s;
        buflen              1s;
        ping                60s;
        drop_idle_publisher 60s;


        application stream {
            live    on;
            record  off;

            hls                 on;
            hls_type            live;
            hls_nested          on;
            hls_path            /tmp/streamer/;
            hls_continuous      on;
            hls_fragment_naming system;
        }
    }
}
```
```nginx:/etc/nginx/http.conf
server {
    server_name stream;

    location ~ /\.ht {
        deny all;
    }

    location /streamer/ {
        types {
            text/html                       html htm;
            application/vnd.apple.mpegurl   m3u8;
            video/mp2t                      ts;
        }
        alias                                   /tmp/streamer/;
        index                                   index.html index.htm;
        expires                                 -1;
        add_header  Cache-Control               no-cache;
        add_header  Access-Control-Allow-Origin *;
    }
}
```
```console:console
[root@example ~]# tree -f /tmp/streamer/test | head
/tmp/streamer/test
├── /tmp/streamer/test/1700556213256.ts
├── /tmp/streamer/test/1700556214282.ts
├── /tmp/streamer/test/1700556215288.ts
├── /tmp/streamer/test/1700556216261.ts
├── /tmp/streamer/test/1700556217264.ts
├── /tmp/streamer/test/1700556218286.ts
├── /tmp/streamer/test/1700556219319.ts
├── /tmp/streamer/test/1700556220340.ts
├── /tmp/streamer/test/1700556221358.ts
[root@example ~]# tree -f /tmp/streamer/test | tail
├── /tmp/streamer/test/1700561403310.ts
├── /tmp/streamer/test/1700561404342.ts
├── /tmp/streamer/test/1700561405362.ts
├── /tmp/streamer/test/1700561406360.ts
├── /tmp/streamer/test/1700561407393.ts
├── /tmp/streamer/test/1700561408409.ts
├── /tmp/streamer/test/1700561409441.ts
└── /tmp/streamer/test/index.m3u8

1 directory, 5075 files
[root@example ~]# 
```
ffmpeg などで｢rtmp://{server}/stream/test｣という URL で送りつけられるとアプリケーション｢stream｣宛に送りつけられたデータを｢(hls_nested を有効にしているので)hls_path に指定したディレクトリに『test』という名前のサブディレクトリを作って｣そこに ts データとプレイリストファイルが生成されるようにしています｡
~~詳しくは[Directives · arut/nginx-rtmp-module Wiki](https://github.com/arut/nginx-rtmp-module/wiki/Directives)をご覧ください｡｢どうしてそうしているのか?｣については個人開発(ry~~

# ご用意するもの
## nginx-lua-module
中の人が Ubuntu/trixie を使っている関係で apt install ができなかったので Debian/sid からパッケージのソースを引っ張ってビルド･インストールしています｡

# トリガーとなるファイルを生成する
```bash:/opt/streamer/bin/toucher
#!/bin/bash

if [ $# -ne 1 ]; then
    echo "USAGE: $0 stream_name"
    exit 1
fi

stream_name=$1
NGX_HLS_PATH="/tmp/streamer"
if [ ! -f "$NGX_HLS_PATH/$stream_name/index.m3u8" ]; then
    echo "not found index.m3u8"
    exit -1
fi

TOUCH_FILE="$NGX_HLS_PATH/$stream_name/readed_m3u8"
touch -m $TOUCH_FILE
if [ "$USER" != "www-data" ]; then
    chown www-data:www-data $TOUCH_FILE
fi
```
```nginx:/etc/nginx/http.conf
        location ~ /streamer/(.*)/index.m3u8 {
            root                    /tmp/;
            set     $stream_name    $1;
            access_by_lua_block {
                os.execute('/opt/streamer/bin/toucher ' .. ngx.var.stream_name .. '')
            }
        }
```
nginx/http.conf に↑を書き入れるとアラ不思議 index.m3u8 にアクセスし続ける限りトリガーとなるファイルの更新日時をアップデートしながら生成してくれます｡(語彙力
ただし､実行権限でファイルにおさわりできない場合があるので実際にストリーミング配信して視聴しながら確認してみるといいでしょう｡

# トリガーを使ってストリーミング配信を止める
```python:/opt/streamer/bin/auto_stopper
#!/usr/bin/python3

import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))

import shutil
import time
import datetime
import pytz

def main():

    stream_name = "test"
    check_path = '/tmp/streamer/{}/readed_m3u8'.format(stream_name)
    if not os.path.isfile(check_path):
        print(f"[{stream_name}]: readed_m3u8 is not found.")
        exit(0)

    limit_ts = 60
    check_path_ts = int(os.path.getmtime(check_path))
    now = int(time.time())
    if (now - check_path_ts) < limit_ts:
        ts_jst = datetime.datetime.fromtimestamp(check_path_ts, tz=pytz.timezone('Asia/Tokyo')).strftime("%Y-%m-%dT%H:%M:%S")
        del_jst = datetime.datetime.fromtimestamp(check_path_ts + limit_ts + 60, tz=pytz.timezone('Asia/Tokyo')).strftime("%Y-%m-%dT%H:%M")
        print(f"[{stream_name}]: readed_m3u8 is fresh.:[{ts_jst},{del_jst}]")
        exit(0)

# ここでストリーミングの配信を停止させたり後処理をここでする
#   print(f"[{stream_name}]: stream stop")
#   proc = subprocess.Popen(['systemctl', 'stop', stream_name], shell=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    os.remove(check_path)
#   print(f"[{stream_name}]: stream stoped")

    exit(0)

# ========================================================================================================
if __name__ == "__main__": 
    main()
```
```conf:/etc/systemd/system/stream_auto_stoper.service 
[Unit]
Description=stream auto stop service
After=nginx.service

[Service]
Type=simple
ExecStart=/opt/streamer/bin/auto_stoper
#ExecStop=/bin/kill -WINCH ${MAINPID}

[Install]
WantedBy=multi-user.target
```
```conf:/etc/systemd/system/stream_auto_stoper.timer
[Unit]
Description=stream auto stop timer

[Timer]
OnCalendar=*-*-* *:*:00

[Install]
WantedBy=timers.target
```
ここで systemd timer を用いてトリガーとなるファイルの更新日時と現在時刻を指定した秒数分差が開いていたらストリーミングの配信を停止させます｡
~~｢実際にどうやって止めてるの?｣については個(ry~~

# まとめ
最終的に下記のような形になりました｡
```nginx:/etc/nginx/http.conf
server {
    server_name stream;

    location ~ /\.ht {
        deny all;
    }

    location /streamer/ {
        types {
            text/html                       html htm;
            application/vnd.apple.mpegurl   m3u8;
            video/mp2t                      ts;
        }
        alias                                   /tmp/streamer/;
        index                                   index.html index.htm;
        expires                                 -1;
        add_header  Cache-Control               no-cache;
        add_header  Access-Control-Allow-Origin *;
    }
    location ~ /streamer/(.*)/index.m3u8 {
        root                    /tmp/;
        set     $stream_name    $1;
        access_by_lua_block {
            os.execute('/opt/streamer/bin/toucher ' .. ngx.var.stream_name .. '')
        }
    }
}
```
```bash:/opt/streamer/bin/toucher
#!/bin/bash

if [ $# -ne 1 ]; then
    echo "USAGE: $0 stream_name"
    exit 1
fi

stream_name=$1
NGX_HLS_PATH="/tmp/streamer"
if [ ! -f "$NGX_HLS_PATH/$stream_name/index.m3u8" ]; then
    echo "not found index.m3u8"
    exit -1
fi

TOUCH_FILE="$NGX_HLS_PATH/$stream_name/readed_m3u8"
touch -m $TOUCH_FILE
if [ "$USER" != "www-data" ]; then
    chown www-data:www-data $TOUCH_FILE
fi
```
```python:/opt/streamer/bin/auto_stopper
#!/usr/bin/python3

import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))

import shutil
import time
import datetime
import pytz

def main():

    args = sys.argv
    if (len(args) - 1) != 1:
        print("not set stream_name")
        print(f"USAGE: {os.path.basename(__file__)} stream_name")
        exit(1)

    stream_name = args[1]
    check_path = '/tmp/streamer/{}/readed_m3u8'.format(stream_name)
    if not os.path.isfile(check_path):
        print(f"[{stream_name}]: readed_m3u8 is not found.")
        exit(0)

    limit_ts = 60
    check_path_ts = int(os.path.getmtime(check_path))
    now = int(time.time())
    if (now - check_path_ts) < limit_ts:
        ts_jst = datetime.datetime.fromtimestamp(check_path_ts, tz=pytz.timezone('Asia/Tokyo')).strftime("%Y-%m-%dT%H:%M:%S")
        del_jst = datetime.datetime.fromtimestamp(check_path_ts + limit_ts + 60, tz=pytz.timezone('Asia/Tokyo')).strftime("%Y-%m-%dT%H:%M")
        print(f"[{stream_name}]: readed_m3u8 is fresh.:[{ts_jst},{del_jst}]")
        exit(0)

# ここでストリーミングの配信を停止させたり後処理をここでする
#   print(f"[{stream_name}]: stream stop")
#   proc = subprocess.Popen(['systemctl', 'stop', stream_name], shell=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    os.remove(check_path)
#   print(f"[{stream_name}]: stream stoped")

    exit(0)

# ========================================================================================================
if __name__ == "__main__": 
    main()
```
```conf:/etc/systemd/system/stream_auto_stoper.service 
[Unit]
Description=stream auto stop service
After=nginx.service

[Service]
Type=simple
ExecStart=/opt/streamer/bin/auto_stoper
#ExecStop=/bin/kill -WINCH ${MAINPID}

[Install]
WantedBy=multi-user.target
```
```conf:/etc/systemd/system/stream_auto_stoper.timer
[Unit]
Description=stream auto stop timer

[Timer]
OnCalendar=*-*-* *:*:00

[Install]
WantedBy=timers.target
```

そんなわけでこんな~~誰が得する~~感じで実装しましたが Lua を~~触るのがほぼ初めてでしたがなんとなく~~わかっていたら10分ぐらいでできちゃう感じでした｡
今回は Python を用いて｢ストリーミング配信を止める｣ようにしていますが､ファイルの日時を取得できればどんな言語でもストリーミング配信を止めることが出来る(と思う)ので参考に~~なるかわかりませんが~~していただければと思います｡

## 最後に
2023年11月24日の時点で Qiita ではじめてコードブロックに｢nginx｣を使いましたが｢ vi などで『*****_by_lua_block』内のところがうまくハイライトしてくれないのは一体何なんでしょうねー｣って思うのは(ｵﾏｴﾀﾞｹﾀﾞﾖ