はじめに
何の記事なの?
nginx-rtmp-module を使ってストリーミング配信しているとどうしても『観たり聴いたりしてない間は止めてもいいかなー』という「オフタイマー」な機能が欲しかったのでふとした思いつきで実装してみることにしました。
Python はそこそこ、 Lua はほぼ初めてな人ですが完全に「なんとなく」で実装しています。
一応誕生日ネタではありますが、後日「個人開発 Advent Calendar 2023」に枠が残っていたらこの記事を書くことになったきっかけになった話やそのコードを出そうと思います(予定)。
Q: こんなん誰が得すんのさ?
A: (少なくとも)自分は得しましたw
前提設定内容
今回関係ある見せられる部分だけ抜き出して内容を差し替えています。
また、ファイル名やパラメーターは後日「個人開発 Advent Calendar(ry」に載せる(かもしれない)ものとは違うものにしています。
# 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;
}
}
}
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 *;
}
}
[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をご覧ください。「どうしてそうしているのか?」については個人開発(ry
ご用意するもの
nginx-lua-module
中の人が Ubuntu/trixie を使っている関係で apt install ができなかったので Debian/sid からパッケージのソースを引っ張ってビルド・インストールしています。
トリガーとなるファイルを生成する
#!/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
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 にアクセスし続ける限りトリガーとなるファイルの更新日時をアップデートしながら生成してくれます。(語彙力
ただし、実行権限でファイルにおさわりできない場合があるので実際にストリーミング配信して視聴しながら確認してみるといいでしょう。
トリガーを使ってストリーミング配信を止める
#!/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()
[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
[Unit]
Description=stream auto stop timer
[Timer]
OnCalendar=*-*-* *:*:00
[Install]
WantedBy=timers.target
ここで systemd timer を用いてトリガーとなるファイルの更新日時と現在時刻を指定した秒数分差が開いていたらストリーミングの配信を停止させます。
「実際にどうやって止めてるの?」については個(ry
まとめ
最終的に下記のような形になりました。
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 .. '')
}
}
}
#!/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
#!/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()
[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
[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』内のところがうまくハイライトしてくれないのは一体何なんでしょうねー」って思うのは(オマエダケダヨ