Files
qotplayer/main.py
T
Tanner 9272dddb2f fix: remove invalid self reference in playback_thread
Co-authored-by: aider (gemini/gemini-3.1-pro-preview) <aider@aider.chat>
2026-07-28 16:04:18 -06:00

175 lines
5.8 KiB
Python

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()