●mpv下調べ
先に作成したmpd playerのユーザー・インターフェースに合わせるようにmpv内部コマンドを調べた。
- ~/.config/mpv/でIPCソケット通信
- loadfileコマンドで再生ファイル等のアドレスをplaylistに追加してからplaylist-play-indexコマンドで再生する
- 'stop keep-playlist'コマンドで停止
- metadata プロパティ コマンドで再生中曲情報を得る
- playlist-pos プロパティ コマンドで再生状況を得る
- defaultのmpvのバージョンは0.32で'stop'コマンドの機能が不十分なので最新版にアップする必要が有る
まずは、mpv最新版をサイト記載の手順で導入する。
●CGI Pythonの有効化
pythonをCGIとして利用する方法が分かったのでApacheの設定をする。これでPHPを使用しなくても良くなった。
※CGI有効化と拡張子.pyのCGIが動作出来るように設定
$ sudo nano /etc/apache2/mods-available/mime.conf
219行目の #を削除し、最後に .pyを追加して上書き保存
変更前:
#AddHandler cgi-script .cgi
変更後:
AddHandler cgi-script .cgi .py
※CGIを動かすディレクトリの設定
$ sudo nano /etc/apache2/conf-available/serve-cgi-bin.conf
11行目の /usr/lib/cgi-bin/ を /home/pi/public_html/cgi-bin に変更
12行目の /usr/lib/cgi-bin を /home/pi/public_html/cgi-bin に変更
上書き保存
変更前:
ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
<Directory "/usr/lib/cgi-bin">
変更後:
ScriptAlias /cgi-bin/ /home/pi/public_html/
<Directory "/home/pi/public_html">
※CGI モジュールを有効にする
$ sudo a2enmod cgid *無効にする場合 $ sudo a2dismod cgid
$ sudo systemctl restart apache2
python プログラム側のCGI対応
※pythonプログラム(mpv_player.py)側のCGI対応抜粋
:
import re
import cgi
:
if __name__ == '__main__':
form = cgi.FieldStorage()
recv = form.getvalue('cmd','') # cmdの値が無い場合は2nd パラムで''(null)になるようにしているが、無くても良さそう
re_quotation = re.compile('".*?"|\'.*?\'') # duble quotかsingle quotで囲まれている
argv_pre = re_quotation.sub('', recv).split() + re_quotation.findall(recv) # quot無しと有りを分ける
re_quot_esc = re.compile(r'"|\\')
argv = []
for a in argv_pre:
argv.append(eval(re_quot_esc.sub('',repr(a)))) # duble quotとesc(\)を削除する
argv.insert(0,'cmd') # 内部処理用に追加しているが本来無くても良い
# argc = len(argv)
st = main(argv)
print('Content-Type:application/json\n\n') # browserへの返信用
print(json.dumps(st)) # browserへの返信データ
#※上記2つのprint()以外にコーディング内で使用するとブラウザからのアクセスでサーバエラーになるので注意!
※ コマンドプロンプトより単体テストするには
$ ./mpv_player.py cmd=play%200 # play listの0番を再生させる例で%20はスペースを意味する
$ ./mpv_player-test.py cmd=lsinfo%20'"/mnt/music/2L%20sample"' # '"..."'の様にsingle/duble quotで二重にしてparamを渡す
※ ブラウザからのCGIアクセスURL
上記CGI設定で行ったディレクトリ(/home/pi//public_html)配下のアドレスで指定する
例: /home/pi/public_html/test/test.pyの場合
http://server_name/cgi-bin/test/test.py
●mpv起動コマンド
mpvを起動してアイドル状態でIPC待機させるpython作成(参考:1番参照)
※python プログラム
import subprocess as proc
ipc_server = "/home/pi/.config/mpv/mpvsocket" # プロセス間通信用フォルダInter Process Communication(IPC)
mpv_argv = [
"mpv",
"--idle=yes",
"--no-video",
"--cache=no",
"--really-quiet",
"--no-input-default-bindings",
"--no-input-terminal",
f"--input-ipc-server={ipc_server}",
]
openmpv = ''
openmpv = proc.Popen(mpv_argv) # mpvを起動してidle状態にする
●mpvとコマンドやデータの受け渡し
mpvへコマンドの送信やプロパティの読み込みや書き込みはIPC通信で行う。
コマンドの種類は参考:2番,3番参照のこと。
※リストの一部
def _compose_message(message):
"""辞書型メッセージをjson形式に変換して返す。
"""
data = json.dumps(message, separators=",:")
return data.encode("utf8", "strict") + b"\n"
def _parse_message(data):
"""json形式を辞書型メッセージに変換して返す。
"""
data = data.decode("utf8", "ignore") # (結果となる Unicode から単に文字を除く)
return json.loads(data)
def _send_command(command, timeout = 10):
"""mpvと通信を行う。
"""
cmd = _compose_message(command)
sock = socket.socket(socket.AF_UNIX)
sock.settimeout(timeout)
try:
sock.connect(ipc_server)
except FileNotFoundError as e:
rcv = e
except Exception as e:
t = traceback.format_exception_only(type(e), e)
t1 ='{"error":"' + t[0].replace('\n','') + '"}\n'
rcv = t1.encode()
else:
try:
sock.send(cmd)
rcv = sock.recv(512*10)
except Exception as e:
rcv = e
sock.close()
return rcv
def command(*args):
global TIMEOUT
"""mpvプロセスに対して1つのコマンドを実行し、その結果を返す。
"""
return _send_command({"command": list(args)}, TIMEOUT)
def get_property(name):
"""プロパティ 'name' の値を返す。
"""
return _parse_message(command("get_property", name))
def set_property(name, value):
"""プロパティ 'name' の値を設定する。
"""
return command("set_property", name, value)
●DLNA操作
netの情報を参考にDLNAのファイルをアクセスできるようにした。
詳細は参考:4番参照のこと。
※DLNA通信特有の部分
# ブロードキャストでDLNA Media Serverを探す
def searchServer():
global timeout
msearch_request_lines = (
'M-SEARCH * HTTP/1.1',
'HOST: 239.255.255.250:1900',
'MAN: "ssdp:discover"',
'MX: 1', # seconds to delay response
'ST:ssdp:all', # search target
'',
''
)
svr_list = []
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(1)
msearch_request_body = '\r\n'.join(msearch_request_lines) # cr/lfでないとだめだった
sock.sendto(msearch_request_body.encode('utf-8'), ('239.255.255.250', 1900))
while True:
try:
res, device = sock.recvfrom(512)
response = res.decode('utf-8')
except socket.timeout:
break
if 'ContentDirectory' in response: # DLNAサーバの判別
for line in response.split('\n'):
if 'LOCATION:' in line:
line = line.replace('LOCATION: http://','').replace('\r','')
srv = {'server':'', 'port':'', 'desc':''}
srv['server'] = line[:line.find(':')]
srv['port'] = line[line.find(':')+1:line.find('/')]
srv['desc'] = line[line.find('/'):]
svr_list.append(srv)
sock.close()
return svr_list
# DLNA Media Serverと通信
def sockStream(svr, msg):
global timeout
s = socket.socket(
socket.AF_INET,
socket.SOCK_STREAM)
s.settimeout(timeout)
s.connect( (
svr['server'],
int(svr['port'])) )
s.send(bytes(msg, 'utf-8'))
chunk = ''
msg_length = 512
try:
while len(chunk) < msg_length:
tmp = s.recv(512*10)
if b'content-length:' in tmp.lower(): # message lengthを得る
leng = re.search('content-length: (.+)\\r\\n', tmp.decode(), re.IGNORECASE).group(1)
msg_length = int(leng) + len(tmp.decode())
if not tmp: break # 最後まで受け取ったら受信終了
chunk += tmp.decode()
except socket.timeout as e:
if chunk == '':
print(e)
s.close()
return chunk
# DLNA Media Serverの情報を得る
def getServerInf():
svr_list = searchServer()
if len(svr_list) == 0: return None
# server_listに情報追加
for svr in svr_list:
msg_base = [
f'GET {svr["desc"]} HTTP/1.1',
'Content-Type: text/html; charset="UTF-8"',
'Content-Length: 0',
f'Host: {svr["server"]}:{svr["port"]}',
'Connection: close',
'',
''
] # 最後にダミー2つは必須(EOFを意味する?)
msg = '\r\n'.join(msg_base) # cr/lfでないとだめだった
ret = sockStream(svr,msg)
svr['friendlyName'] = xmlExtraction('friendlyName', ret)
# 全てのserviceを抽出する
ret = ''.join(ret.splitlines()) # WG2200HPの改行コードを削除
services = re.findall(r'<service>(.+?)</service>', ret)
for s in services:
if 'ContentDirectory' in s:
svr['serviceType'] = xmlExtraction('serviceType', s)
svr['controlURL'] = xmlExtraction('controlURL', s)
break # 'ContentDirectory'以外のサービスは不使用
svr['id'] = 0 # default 大親フォルダID
return svr_list
# DLNA Media Serverからフォルダやファイル情報を得る
def soapRequest(svr, flag='BrowseDirectChildren'):
# *** flag: BrowseDirectChildren(default)/BrowseMetadata ***
global timeout
req_base = [
'<?xml version="1.0" Encoding="UTF-8" ?>',
'<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">',
'<s:Body>',
f'<u:Browse xmlns:u="{svr["serviceType"]}">',
f'<ObjectID>{svr["id"]}</ObjectID>',
f'<BrowseFlag>{flag}</BrowseFlag>',
'<Filter>*</Filter>',
'<StartingIndex>0</StartingIndex>',
'<RequestedCount>0</RequestedCount>',
'<SortCriteria></SortCriteria>',
'</u:Browse>',
'</s:Body>',
'</s:Envelope>',
'',
''
] # 最後にダミー2つは必須(EOFを意味する?)
req = '\r\n'.join(req_base) # cr/lfでないとだめだった
req_len = len(req)
message = [
f'POST {svr["controlURL"]} HTTP/1.1',
'Content-Type: text/xml; charset="UTF-8"',
f'Content-Length: {req_len}',
f'Host: {svr["server"]}:{svr["port"]}',
f'SOAPAction: \"{svr["serviceType"]}#Browse\"'
'Connection: close',
'',
f'{req}'
]
msg = '\r\n'.join(message) # cr/lfでないとだめだった
return sockStream(svr, msg)
●ブラウザの操作
ブラウザ側はjavascriptでサーバ側とやり取りする。主な画面操作は以下の通り。
- mpvのplaylistに登録済みのアルバムを再生
- 下図2内の写真下に有るリストから選んで’選択’ボタン押下
- 下図3で再生したい曲を選んで’開始’ボタン押下
- サーバ側csvファイルに登録したyoutube一覧を読み込んで選曲再生
- 下図2右下に有る'src list'ボタン押下
- 下図4右下の'Youtube'ボタン押下
- 下図4下表より再生したい曲を選んで’音楽一覧に追加’ボタン押下
- 下図4上表より追加された曲を選んで’開始’ボタン押下
- サーバ側のradio一覧を読み込んで選局再生
- 下図2右下に有る'src list'ボタン押下
- 下図5右下の'Radio'ボタン押下
- 下図5下表より再生したい局を選んで'音楽一覧に追加'ボタン押下
- 下図5上表より追加された局を選んで'開始'ボタン押下
- DLNA内ファイルの再生
- 下図2右下の'src list'ボタン押下
- 下図6右下の'DLNA'ボタン押下
- DLNA Media Serverを選択し'フォルダ開く'ボタン押下
- 聞きたい曲のフォルダまで'フォルダ開く'ボタン押下で掘り下げていく
- 聞きたい曲を選択し'音楽一覧に追加'ボタン押下
- 音楽一覧に表示されたら選択し'開始'ボタン押下
- ボタンの無いmpvコマンドの送信
- 図2下方左の入力欄に送信するコマンドを入力し’コマンド送信’ボタン押下
- コマンドの返り値は入力欄上の'command status'に表示される
●所感
CGIでpythonが使えてPHPを介さず直にできるのでスッキリした。DLNAはMedia Serverの種類により応答データの形式が異なるので注意が必要だった。
音に関しては、やはりMPDの方が若干明瞭な感じで良いように思う。
また、今回の結果を踏まえて、CGIとDLNAの機能、及びブラウザ画面は先に作成したMPD Playerにも適用し統一させた。