# Copyright (c) 2021 by xfangfang. All Rights Reserved. # # Using IINA as DLNA media renderer # # Macast Metadata # 波澜播放器-IINA # BolanIINARenderer # darwin # 0.32 # 0.7 # xfangfang&小棉袄&Reborn # IINA echanced support for Macast offical IINA Renderer. Echancement: support headers 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 from urllib.parse import unquote IINA_PATH = '/Applications/IINA.app/Contents/MacOS/iina-cli' logger = logging.getLogger("BolanIINARenderer") logger.setLevel(logging.DEBUG) class BolanIINARenderer(MPVRenderer): def __init__(self): super(BolanIINARenderer, 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 parse_header(self, url): header_map = {} data = url.split("##|", 1) if len(data) < 2: return url, header_map url = data[0] headers = data[1].split("&") for h in headers: item = h.split("=", 1) if len(item) < 2: continue header_map[item[0]] = unquote(item[1], 'utf-8') return url, header_map def set_media_url(self, url, 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) url, header_map = self.parse_header(url) agent = "Mozilla/5.0 (Linux; Android 11; Mi 10 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.152 Mobile Safari/537.36" if 'User-Agent' in header_map: agent = header_map['User-Agent'] del header_map['User-Agent'] header = ','.join([f'{i}: {header_map[i]}' for i in header_map]) # UA要放最后,否则不正常 header = f'User-Agent: {agent}' if len(header) == 0 else f'{header},User-Agent: {agent}' header = header.replace('+', ' ') if not self.is_iina_start: self.set_media_stop() self.start_iina(url, header, start) self.ipc_thread = threading.Thread(target=self.start_ipc, name="IINA_IPC_THREAD") self.ipc_thread.start() else: self.send_command(['loadfile', url, 'replace', f'start={start}']) # self.send_command(['set_property', '--mpv-http-header-fields', header]) cherrypy.engine.publish('renderer_av_uri', url) 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(BolanIINARenderer, self).set_observe() self.set_media_volume(100) def start_iina(self, url, header, start=0): """Start iina thread """ print("url: {}".format(url)) print("header: {}".format(header)) self.is_iina_start = True if len(header) == 0: params = [ self.path, '--keep-running', f'--mpv-input-ipc-server={self.mpv_sock}', f'--mpv-start={start}', f'{url}', ] else: params = [ self.path, '--keep-running', f'--mpv-input-ipc-server={self.mpv_sock}', f'--mpv-start={start}', f'--mpv-http-header-fields={header}', f'{url}', ] # start iina logger.info(' '.join([f'{i}' for i in params])) 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 BolanIINARenderer") 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 BolanIINARenderer") self.set_media_stop() if __name__ == '__main__': Setting.load() gui(BolanIINARenderer())