# Copyright (c) 2021 by xfangfang. All Rights Reserved.
#
# Using IINA as DLNA media renderer
#
# Macast Metadata
# IINA Renderer
# IINARenderer
# darwin
# 0.31
# 0.7
# xfangfang
# IINA support for Macast. Because iina is developed based on MPV, this plugin's experience is similar to the built-in MPV renderer.
import os
import json
import time
import socket
import threading
import cherrypy
import subprocess
import gettext
import logging
from queue import Queue
from macast import RendererSetting, Setting, gui
from macast_renderer.mpv import MPVRenderer
IINA_PATH = '/Applications/IINA.app/Contents/MacOS/iina-cli'
logger = logging.getLogger("IINARenderer")
logger.setLevel(logging.DEBUG)
class IINARenderer(MPVRenderer):
def __init__(self):
super(IINARenderer, self).__init__(gettext.gettext, IINA_PATH)
self.renderer_setting = RendererSetting()
self.commond_queue = Queue()
self.mpv_thread = None
self.ipc_thread = None
self.iina = None
self.is_iina_start = False
def command_send_thread(self):
print("command_send_thread start {}".format(self.running))
while self.running:
print("check command")
while not self.commond_queue.empty():
if not self.running:
return
if not self.is_iina_start:
break
command = self.commond_queue.get()
error_time = 10
while error_time > 0:
error_time -= 1
print("send command: " + str(command))
msg = json.dumps({"command": command}) + '\n'
try:
self.ipc_sock.sendall(msg.encode())
self.commond_queue.task_done()
time.sleep(0.05)
break
except Exception as e:
logger.error('error sendCommand: ' + str(e))
time.sleep(1)
else:
cherrypy.engine.publish("app_notify", "Macast", "Cannot sending msg to iina.")
logger.error("iina cannot start")
threading.Thread(target=lambda: Setting.stop_service(), name="IINA_STOP_SERVICE").start()
time.sleep(1)
def set_media_stop(self):
try:
if self.iina is not None:
self.iina.terminate()
os.waitpid(-1, 1)
except Exception as e:
print(str(e))
self.iina = None
self.is_iina_start = False
self.ipc_running = False
if self.ipc_thread is not None and self.ipc_thread.is_alive():
self.ipc_thread.join()
self.ipc_thread = None
cherrypy.engine.publish('renderer_av_stop')
def set_media_url(self, data, start='0'):
""" data : string
"""
def position_to_second(position: str) -> int:
pos = position.split(':')
if len(pos) < 3:
return 0
return int(pos[0]) * 3600 + int(pos[1]) * 60 + int(pos[2])
try:
start = int(start)
except:
start = position_to_second(start)
if not self.is_iina_start:
self.set_media_stop()
self.start_iina(data, start)
self.ipc_thread = threading.Thread(target=self.start_ipc, name="IINA_IPC_THREAD")
self.ipc_thread.start()
else:
self.send_command(['loadfile', data, 'replace', f'start={start}'])
cherrypy.engine.publish('renderer_av_uri', data)
def send_command(self, command):
"""Sending command to iina
"""
if self.is_iina_start:
print("put command to queue: {}".format(command))
self.commond_queue.put(command)
def set_observe(self):
super(IINARenderer, self).set_observe()
self.set_media_volume(100)
def start_iina(self, url, start=0):
"""Start iina thread
"""
self.is_iina_start = True
params = [
self.path,
'--keep-running',
f'--mpv-input-ipc-server={self.mpv_sock}',
f'--mpv-start={start}',
url,
]
# start iina
print("iina starting")
cherrypy.engine.publish('mpv_start')
self.iina = subprocess.Popen(
params,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=subprocess.PIPE,
env=Setting.get_system_env())
def start_ipc(self):
"""Start ipc thread
Communicating with mpv
"""
if self.ipc_running:
logger.error("mpv ipc is already runing")
return
self.ipc_running = True
error_time = 0
internal = 0.5
while self.ipc_running and self.running and self.mpv_thread.is_alive():
try:
time.sleep(internal)
logger.error("mpv ipc socket start connect")
self.ipc_sock = socket.socket(socket.AF_UNIX,
socket.SOCK_STREAM)
self.ipc_sock.connect(self.mpv_sock)
cherrypy.engine.publish('mpvipc_start')
cherrypy.engine.publish('renderer_start')
self.ipc_once_connected = True
internal = 0.5
self.set_observe()
except Exception as e:
error_time += 1
if error_time > 20:
internal = 2
if self.iina is not None and self.iina.poll() is not None:
self.is_iina_start = False
self.ipc_running = False
self.set_state_stop()
logger.error("mpv ipc socket reconnecting: {}".format(str(e)))
continue
res = b''
msgs = None
while self.ipc_running:
try:
data = self.ipc_sock.recv(1048576)
if data == b'':
break
res += data
if data[-1] != 10:
continue
except Exception as e:
logger.debug(e)
break
try:
msgs = res.decode().strip().split('\n')
for msg in msgs:
self.update_state(msg)
except Exception as e:
logger.error("decode error: {}".format(e))
logger.error(f"decode error data: {msg}")
logger.error(f"decode error data list: {msgs}")
finally:
res = b''
self.ipc_sock.close()
logger.error("mpv ipc stopped")
def start(self):
super(MPVRenderer, self).start()
logger.info("starting IINARenderer")
self.mpv_thread = threading.Thread(target=self.command_send_thread, daemon=True, name="COMMAND_SEND")
self.mpv_thread.start()
def stop(self):
super(MPVRenderer, self).stop()
logger.info("stoping IINARenderer")
self.set_media_stop()
if __name__ == '__main__':
Setting.load()
gui(IINARenderer())