From 6b8b150e7dbbea1eb92f4368a363ebbef456d501 Mon Sep 17 00:00:00 2001 From: Vic Wong Date: Wed, 25 Dec 2024 13:23:35 -0800 Subject: [PATCH] refactor: reorganize tmpdir handling, fix buffering issues --- pikaraoke/app.py | 3 +- pikaraoke/karaoke.py | 91 ++++++++++++++++------------------ pikaraoke/lib/file_resolver.py | 22 +++++++- 3 files changed, 66 insertions(+), 50 deletions(-) diff --git a/pikaraoke/app.py b/pikaraoke/app.py index 3cdca25..9174f4b 100644 --- a/pikaraoke/app.py +++ b/pikaraoke/app.py @@ -40,7 +40,7 @@ from pikaraoke import VERSION, karaoke from pikaraoke.constants import LANGUAGES -from pikaraoke.lib.file_resolver import get_tmp_dir +from pikaraoke.lib.file_resolver import delete_tmp_dir, get_tmp_dir from pikaraoke.lib.get_platform import get_platform, is_raspberry_pi try: @@ -1046,6 +1046,7 @@ def main(): driver.close() cherrypy.engine.exit() + delete_tmp_dir() sys.exit() diff --git a/pikaraoke/karaoke.py b/pikaraoke/karaoke.py index bf4391a..ffc5241 100644 --- a/pikaraoke/karaoke.py +++ b/pikaraoke/karaoke.py @@ -3,7 +3,6 @@ import logging import os import random -import shutil import socket import subprocess import time @@ -17,7 +16,7 @@ import qrcode from unidecode import unidecode -from pikaraoke.lib.file_resolver import FileResolver, get_tmp_dir +from pikaraoke.lib.file_resolver import FileResolver, delete_tmp_dir from pikaraoke.lib.get_platform import ( get_ffmpeg_version, get_os_version, @@ -418,11 +417,6 @@ def log_ffmpeg_output(self): def play_file(self, file_path, semitones=0): logging.info(f"Playing file: {file_path} transposed {semitones} semitones") - stream_uid = int(time.time()) - output_file = f"{get_tmp_dir()}/{stream_uid}.mp4" - stream_url = f"{self.url}/stream/{stream_uid}" - # pass a 0.0.0.0 IP to ffmpeg which will work for both hostnames and direct IP access - ffmpeg_url = f"http://0.0.0.0:{self.ffmpeg_port}/{stream_uid}" pitch = 2 ** ( semitones / 12 @@ -468,7 +462,7 @@ def play_file(self, file_path, semitones=0): output = ffmpeg.output( audio, video, - output_file, + fr.output_file, vcodec="libx264", acodec="aac", preset="ultrafast", @@ -483,7 +477,7 @@ def play_file(self, file_path, semitones=0): output = ffmpeg.output( audio, video, - output_file, + fr.output_file, vcodec=vcodec, acodec=acodec, preset="ultrafast", @@ -508,46 +502,50 @@ def play_file(self, file_path, semitones=0): t.start() output_file_size = 0 + buffering_threshold = 4000000 # raise this if pi3 struggles to keep up with transcoding while self.ffmpeg_process.poll() is None: + is_transcoding_complete = False + is_buffering_complete = False try: - # Add a loop to check the size of output_file output = self.ffmpeg_log.get_nowait() - output_file_size = os.path.getsize(output_file) logging.debug("[FFMPEG] " + decode_ignore(output)) - logging.debug(f"Output file size: {output_file_size}") - except (FileNotFoundError, Empty, AttributeError): - # Handle the case where the file might not exist yet - time.sleep(0.1) - pass - else: - # Check if the stream is ready to play - # Determined by completed transcode stream_ready_string match - # or the file size being greater than a threshold - if (stream_ready_string in decode_ignore(output)) or (output_file_size > 4048576): - logging.debug(f"Stream ready! File size: {output_file_size}") - self.now_playing = self.filename_from_path(file_path) - self.now_playing_filename = file_path - self.now_playing_transpose = semitones - self.now_playing_url = stream_url - self.now_playing_user = self.queue[0]["user"] - self.is_paused = False - self.queue.pop(0) - - # Pause until the stream is playing - max_retries = 100 - while self.is_playing == False and max_retries > 0: - time.sleep(0.1) # prevents loop from trying to replay track - max_retries -= 1 - if self.is_playing: - logging.debug("Stream is playing") - break - else: - logging.error( - "Stream was not playable! Run with debug logging to see output. Skipping track" - ) - self.end_song() - break - time.sleep(0.1) + is_transcoding_complete = stream_ready_string in decode_ignore(output) + if is_transcoding_complete: + logging.debug(f"Transcoding complete. File size: {output_file_size}") + except Empty: + try: + output_file_size = os.path.getsize(fr.output_file) + is_buffering_complete = output_file_size > buffering_threshold + if is_buffering_complete: + logging.debug(f"Buffering complete. File size: {output_file_size}") + except (FileNotFoundError, AttributeError): + pass + # Check if the stream is ready to play + # Determined by completed transcode stream_ready_string match + # or the buffered file size being greater than a threshold + if is_transcoding_complete or is_buffering_complete: + logging.debug(f"Stream ready!") + self.now_playing = self.filename_from_path(file_path) + self.now_playing_filename = file_path + self.now_playing_transpose = semitones + self.now_playing_url = f"{self.url}/{fr.stream_url_path}" + self.now_playing_user = self.queue[0]["user"] + self.is_paused = False + self.queue.pop(0) + # Pause until the stream is playing + max_retries = 100 + while self.is_playing == False and max_retries > 0: + time.sleep(0.1) # prevents loop from trying to replay track + max_retries -= 1 + if self.is_playing: + logging.debug("Stream is playing") + break + else: + logging.error( + "Stream was not playable! Run with debug logging to see output. Skipping track" + ) + self.end_song() + break def kill_ffmpeg(self): logging.debug("Killing ffmpeg process") @@ -562,8 +560,7 @@ def end_song(self): logging.info(f"Song ending: {self.now_playing}") self.reset_now_playing() self.kill_ffmpeg() - # delete the tmp dir - shutil.rmtree(get_tmp_dir()) + delete_tmp_dir() logging.debug("ffmpeg process killed") def transpose_current(self, semitones): diff --git a/pikaraoke/lib/file_resolver.py b/pikaraoke/lib/file_resolver.py index 9ab3824..237abd8 100644 --- a/pikaraoke/lib/file_resolver.py +++ b/pikaraoke/lib/file_resolver.py @@ -2,6 +2,7 @@ import re import shutil import zipfile +from sys import maxsize from pikaraoke.lib.get_platform import get_platform @@ -13,10 +14,24 @@ def get_tmp_dir(): tmp_dir = os.path.expanduser(r"~\\AppData\\Local\\Temp\\pikaraoke\\" + str(pid) + r"\\") else: tmp_dir = f"/tmp/pikaraoke/{pid}" + return tmp_dir + + +def create_tmp_dir(): + tmp_dir = get_tmp_dir() # create tmp_dir if it doesn't exist if not os.path.exists(tmp_dir): os.makedirs(tmp_dir) - return tmp_dir + + +def delete_tmp_dir(): + tmp_dir = get_tmp_dir() + if os.path.exists(tmp_dir): + shutil.rmtree(tmp_dir) + + +def string_to_hash(s): + return hash(s) % ((maxsize + 1) * 2) # Processes a given file path and determines the file format and file path, extracting zips into cdg + mp3 if necessary. @@ -24,11 +39,14 @@ class FileResolver: file_path = None cdg_file_path = None file_extension = None - pid = os.getpid() # for scoping tmp directories to this process def __init__(self, file_path): + create_tmp_dir() self.tmp_dir = get_tmp_dir() self.resolved_file_path = self.process_file(file_path) + self.stream_uid = string_to_hash(file_path) + self.output_file = f"{self.tmp_dir}/{self.stream_uid}.mp4" + self.stream_url_path = f"stream/{self.stream_uid}" # Extract zipped cdg + mp3 files into a temporary directory, and set the paths to both files. def handle_zipped_cdg(self, file_path):