2021年4月19日月曜日

Androidで超A&G+録画 2

 前回のコードだとやっぱり録画失敗することが多いので、コードの見直しをする。

録画失敗とは別に、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 件のコメント:

コメントを投稿