前回のコードだとやっぱり録画失敗することが多いので、コードの見直しをする。
録画失敗とは別に、agr4d.shの中身がおかしかったのでそちらも修正。
2021/05/01追記
なんかちゃんと動かないっぽいのでそのうち修正予定
AgRecdroid.sh
#!/bin/bash
python AgRecdroid.py """$1""" $2 -st """$3"""
ファイル名が変わっているのは特に理由なし。
pythonの方は大幅に変更してみた。
ffmpegでm3u8をダウンロードすると、エラー落ちせずに終了してしまうことが多々あるので、tsファイルを直接保存して-f concat
する方法に変えてみる。m3u8をVLCやMX
Playerで再生すると時々再生が止まることがあるが、ChromeやSafariで見ると問題なく再生できるのでm3u8へのアクセスに何らかの問題があると想定した。
AgRecdroid.py
#-*- coding: utf-8 -*-
import m3u8
import datetime
import argparse
import subprocess
import time
from pathlib import Path
from time import sleep
from datetime import datetime as dt
from datetime import timedelta as tdl
from threading import (Event, Thread)
from urllib.parse import urljoin
parser = argparse.ArgumentParser(description='')
parser.add_argument('title',)
parser.add_argument('dur',type=float)
parser.add_argument('-st', '--start', default=None)
args = parser.parse_args()
class AgRec:
def __init__(self,url):
self.m3u8 = m3u8get(url)
self.rec_sta = False
self.play_sta = False
def rec(self,filename,sec,st):
if not self.rec_sta:
self.rec_states = True
self.filename = file.with_suffix('.ts')
th = Thread(target=self.rec_start,args=(self.filename,sec,st))
th.start()
def rec_start(self,filename,sec,st):
self.rec_sta = True
n_playlist = m3u8.M3U8()
o_playlist = m3u8.M3U8()
start = time.time()
rectime = 0
count = 0
segs = []
o_segs = []
o_segs_len = 0
pl_time2 = 0
if st:
now_time = dt.strptime(dt.now().strftime('%H%M%S%f'), '%H%M%S%f')
try:
sleep((st-now_time).total_seconds())
except:
pass
log_file = filename.stem + '.log'
log_path = log_dir / log_file
while self.rec_sta: #m3u8のリロード
n_segs =[]
try:
playlist = m3u8.load(self.m3u8)
except Exception as e:
now = dt.now().strftime('%Y/%m/%d %H:%M:%S')
t = traceback.format_exception_only(type(e), e)[0].rstrip('\n')
with log_path.open(mode='a') as f:
f.write('-------<{}>-------\n'.format(now))
f.write('\t' + t + '\n')
break
pl_time = 0
start2 = time.time()
segs_id = len(playlist.segments) - 1
for i, seg in enumerate(playlist.segments):
#Segmentごとの処理
if 'http' in seg.uri: #tセグメントのURIが絶対表記と相対表記の分岐
uri =seg.uri
else:
uri =urljoin(seg.base_uri,seg.uri)
if not uri in o_segs: #回収していないセグメント
if count == 0: #最初の1回目だけ一番最後(=新しい)セグメントだけ回収
if i == segs_id:
n_segs.append('file \'{}\''.format(uri))
pl_time = pl_time + seg.duration
rectime = rectime + seg.duration
else:
n_segs.append('file \'{}\''.format(uri))
pl_time = pl_time + seg.duration
rectime = rectime + seg.duration
o_segs.append(uri)
if rectime >= sec: #録画時間と録画ファイルの再生時間のチェック
self.rec_sta = False
break
if n_segs: #追加するセグメント
concat_file = filename.with_suffix('.txt') #生成したファイルと追加するセグメントを記載(-f concat -i TXTで使用)
part_file = filename.with_name(filename.stem + '.part.ts') #生成したファイル
part_file2 = filename.with_name(filename.stem + '.part0.ts') #これから生成するファイル
if count != 0: #初回以降はpart_fileもconcat_fileに追加
n_segs.insert(0,'file \'{}\''.format(str(part_file)).replace('\\',r'\\'))
s = '\n'.join(n_segs)
concat_file.write_text(s)
cmd = ('ffmpeg -protocol_whitelist file,http,https,tcp,tls,crypto -safe 0 -f concat -i "{}" -y -c copy "{}"').format(str(concat_file),str(part_file2))
lp = 0
while True: #セグメントにアクセスできない場合のリトライ
now = dt.now().strftime('%Y/%m/%d %H:%M:%S')
if lp == 0:
now = '-------<{}>-------\n'.format(now)
else:
now = '-------<{}_ReTry>-------\n'.format(now)
self.rec = subprocess.Popen('exec '+cmd,shell=True,stdout=subprocess.PIPE, stderr=subprocess.STDOUT,stdin=subprocess.PIPE)
lines = []
for line in self.rec.stdout:
try:line =line.decode("utf8")
except:line = line.decode("cp932")
lines += ['\t' + ln for ln in line[:-2].split('\r')]
self.rec.communicate()
with log_path.open(mode='a') as f:
f.write(now + '\n'.join(lines[10:])+ '\n')
if self.rec.returncode == 0:
break #正常に終了したら抜ける
else: #終わらなかったら セグメントが消えるギリギリまでリトライする
if time.time() - start2 >= pl_time + seg.duration:
break
else:
lp = 1
sleep(1)
#part_file2→part_fileへリネーム&エラー処理
try:
part_file.unlink()
except:
pass
try:
part_file2.rename(part_file)
except Exception as e:
now = dt.now().strftime('%Y/%m/%d %H:%M:%S')
t = traceback.format_exception_only(type(e), e)[0].rstrip('\n')
with log_path.open(mode='a') as f:
f.write('-------<{}>-------\n'.format(now))
f.write('\t' + t + '\n')
break
if self.rec_sta: #m3u8のリロード間隔調整
pas = time.time() - start2
if pas < seg.duration:
sleep(seg.duration-pas)
if len(o_segs) > 8:
o_segs = o_segs[-8:]
pl_time2 += pl_time
count += 1
#part_file→出力ファイルへ変換
cmd = ('ffmpeg -i "{}" -y -c copy "{}"').format(str(part_file),str(filename.with_suffix('.mp4')))
proc = subprocess.run('exec '+cmd,shell=True,stdout=subprocess.PIPE, stderr=subprocess.STDOUT,stdin=subprocess.PIPE)
now = dt.now().strftime('%Y/%m/%d %H:%M:%S')
if not self.rec_sta:
with log_path.open(mode='a') as f:
f.write('-------<{}_録画完了>-------\n'.format(now))
else:
self.rec_sta = False
#録画時間チェック
cmd = ('ffmpeg -y -i "{}"').format(str(filename.with_suffix('.mp4')))
proc = subprocess.Popen('exec '+cmd,shell=True,stdout=subprocess.PIPE, stderr=subprocess.STDOUT,stdin=subprocess.PIPE)
lines = []
for line in proc.stdout:
try:line =line.decode("utf8")
except:line = line.decode("cp932")
if 'Duration' in line:
dur = line.split(',')[0][12:].split('.')[0]
proc.communicate()
try:
dur = tdl(hours=int(dur.split(':')[0]), minutes=int(dur.split(':')[1]),seconds=int(dur.split(':')[2])).total_seconds()
except:
dur = 0
with log_path.open(mode='a') as f:
f.write('\t' + str(int(dur)-sec) + '\n')
if int(dur)-sec >= 0:
print('正常に終了しました')
else:
print('録画データに欠損がある可能性があります')
try:
part_file.unlink()
except:
pass
concat_file.unlink()
def m3u8get(v_m3u8): #ビットレートの最も大きいm3u8を取得
pl = None
try:
v_pl = m3u8.load(v_m3u8)
except:
pass
else:
if v_pl.is_variant:
urls = []
bands = []
for n in range(len(v_pl.playlists)):
pl = v_pl.playlists[n]
urls.append(pl.uri)
bands.append(pl.stream_info.bandwidth)
pl = urls[bands.index(max(bands))]
else:
pl = v_pl
return pl
def ngword(str): #駄目文字処理
dic={'\¥': '¥', '/': '/', ':': ':', '*': '*', '?': '?', '!': '!', '¥"': '”', '<': '<', '>': '>','|': '|'}
table='\\/:*?!"<>|'
for ch in table:
if ch in str:
rm = dic.pop(ch)
str = str.replace(ch,rm)
try:
str = re.sub('\t','',str)
except:
pass
try:
str = re.sub('\n','',str)
except:
pass
return str
date = dt.now().strftime('%Y%m%d%H%M%S')
title = ngword(args.title)
r_dir = Path('/storage/emulated/0/AgRec/rec') / title
log_dir = Path('/storage/emulated/0/AgRec/log')
r_dir.mkdir(parents=True,exist_ok=True)
log_dir.mkdir(parents=True,exist_ok=True)
filename = date + '_' + title
duration = args.dur + 17
st = args.start
try:
st = dt.strptime(st, '%H:%M')
except:
st = None
file = r_dir / (filename)
ag = AgRec('https://www.uniqueradio.jp/agplayer5/hls/mbr-1.m3u8') #アドレスは高画質用に変更可能
ag.rec(file,duration,st)
以前とはAgRec.rec_start()
が大きく変わっている。単純にm3u8をffmpegに食わせるよりも数倍めんどくさくなっている。多分もっとスマートにできるような気がする。
使用するときは以下のコマンド。
AgRecdroid.sh "番組名" 時間(sec) 開始時刻(HH:MM)
Taskerのプロファイルで、起動時間を番組表時刻の1分前に設定すると、1分間待機したあとに録画開始するようにした。更に、m3u8初回ロード時は一番新しいセグメントだけを読むことでHLSのラグを約1分から最短16秒程度にすることができた。録画マージンにもそれなりの余裕があるので、引数に指定する時間は番組表通りでも問題なく録画できる。
0 件のコメント:
コメントを投稿