import os import sys import json import socket import threading import argparse import time import numpy as np import av class PlayerState: def __init__(self): self.paused = True self.volume = 100.0 self.time_pos = 0.0 self.seek_request = None self.running = True self.lock = threading.Lock() def playback_thread(file_path, state): fifo_path = '/tmp/snapfifo' if not os.path.exists(fifo_path): os.mkfifo(fifo_path) container = av.open(file_path) stream = container.streams.audio[0] resampler = av.AudioResampler(format='s16', layout='stereo', rate=48000) with open(fifo_path, 'wb') as fifo: iterator = container.decode(stream) while state.running: with state.lock: paused = state.paused seek_req = state.seek_request vol = state.volume if seek_req is not None: # PyAV seek takes time in AV_TIME_BASE (microseconds) target_timestamp = int(seek_req * av.time_base) container.seek(target_timestamp, any_frame=False, backward=True, stream=stream) with state.lock: state.seek_request = None iterator = container.decode(stream) continue if paused: time.sleep(0.05) continue try: frame = next(iterator) except StopIteration: break except av.AVError: continue with state.lock: state.time_pos = float(frame.pts * stream.time_base) resampled_frames = resampler.resample(frame) for r_frame in resampled_frames: arr = r_frame.to_ndarray() # Apply volume if vol != 100.0: multiplier = max(0.0, vol / 100.0) arr = arr.astype(np.float32) * multiplier arr = np.clip(arr, -32768, 32767).astype(np.int16) fifo.write(arr.tobytes()) def handle_ipc_client(conn, state): buffer = "" while state.running: try: data = conn.recv(4096).decode('utf-8') if not data: break buffer += data while '\n' in buffer: line, buffer = buffer.split('\n', 1) line = line.strip() if not line: continue try: req = json.loads(line) cmd = req.get("command", []) req_id = req.get("request_id", 0) resp = {"error": "success", "data": None, "request_id": req_id} if not cmd: resp["error"] = "invalid command" elif cmd[0] == "get_property": prop = cmd[1] with state.lock: if prop == "pause": resp["data"] = state.paused elif prop == "volume": resp["data"] = state.volume elif prop == "time-pos": resp["data"] = state.time_pos else: resp["error"] = "property not found" elif cmd[0] == "set_property": prop = cmd[1] val = cmd[2] with state.lock: if prop == "pause": state.paused = bool(val) elif prop == "volume": state.volume = float(val) elif prop == "time-pos": state.seek_request = float(val) else: resp["error"] = "property not found" else: resp["error"] = "unknown command" conn.sendall((json.dumps(resp) + '\n').encode('utf-8')) except Exception as e: err_resp = {"error": str(e)} conn.sendall((json.dumps(err_resp) + '\n').encode('utf-8')) except Exception: break conn.close() def ipc_server_thread(socket_path, state): if os.path.exists(socket_path): os.remove(socket_path) server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) server.bind(socket_path) server.listen(5) while state.running: try: server.settimeout(1.0) conn, _ = server.accept() t = threading.Thread(target=handle_ipc_client, args=(conn, state)) t.daemon = True t.start() except socket.timeout: continue except Exception: break server.close() if os.path.exists(socket_path): os.remove(socket_path) def main(): parser = argparse.ArgumentParser(description="Headless audio player") parser.add_argument("--input-ipc-server", required=True, help="Path to IPC socket") parser.add_argument("file", help="Audio file to play") args = parser.parse_args() state = PlayerState() ipc_thread = threading.Thread(target=ipc_server_thread, args=(args.input_ipc_server, state)) ipc_thread.daemon = True ipc_thread.start() try: playback_thread(args.file, state) except KeyboardInterrupt: pass finally: state.running = False ipc_thread.join(timeout=2.0) if __name__ == "__main__": main()