diff --git a/README.md b/README.md index 7be46656..599f14d0 100644 --- a/README.md +++ b/README.md @@ -55,9 +55,9 @@ Raspberry Pi 3 and above. Anything else will likely be too slow. Other pi considerations: - Should be running Raspberry pi desktop OS if running headed, since it requires a browser -- 32-bit version of the Bullseye OS is recommended. 64-bit seemed slower in my testing, but pi4 and above can probably handle it. Bookworm seems to install an incompatible version of ffmpeg at the moment. Please avoid if possible ( see #323 ) -- Disable "screen blanking" in raspi-config if you want to prevent the display from turning off when idle +- 32-bit version of the Bullseye OS is recommended for pi 3. 64-bit bookworm is fine for pi 4 and above. - Pi3 might struggle a bit with high-res video playback. Overclocking seems to help +- Disable "screen blanking" in raspi-config if you want to prevent the display from turning off when idle Works fine on modern Mac, PCs, and Linux! @@ -190,6 +190,10 @@ options: ## Troubleshooting +### How do I update pikaraoke to the latest version? + +Simply run the setup script again. CD the pikaraoke directory and run: `./setup.sh` (linux/osx/rpi) `setup-windows.bat` (windows) + ### I'm not hearing audio out of the headphone jack By default the raspbian outputs to HDMI audio when it's available. Pikaraoke tries to output to both HDMI and headphone, but if it doesn't work you may need to to force it to the headphone jack. This is definitely the case when using VLC. To do so, change following setting on the pi: @@ -269,6 +273,12 @@ You'll need to add them manually by copying them to the root of your download fo CDG files must have an mp3 file with a exact matching file name. They can also be bundled together in a single zip file, but the filenames in the zip must still match. They must also be placed in the root of the download directory and not stashed away in sub-directories. +### I'm getting this ChromeDriver error on launch: "session not created: DevToolsActivePort file doesn't exist" + +Are you trying to launch over SSH? That probably indicates that chromedriver doesn't know which display to launch the browser on. If so, you may need to specify the native display of the remote device using this command: `DISPLAY=:0.0 ./pikaraoke.sh`. Note that Pikaraoke 1.2.1 and newer should do this for you. + +You can alternately run headless if you launch the splash screen manually on a separate machine: `./pikaraoke.sh --headless` + ### How do I dismiss the Splash confirmation screen on an in-TV browser? (like a Samsung TV with web browsing) The splash confirmation screen is an unfortunate necessity due to modern browser permissions disabling video autoplay. A single interaction will enable it, and the confirmation screen serves as this interaction. Hopefully your TV has a way to click the button on the screen with the remote or otherwise. diff --git a/app.py b/app.py index b8a9f166..e8edccc1 100644 --- a/app.py +++ b/app.py @@ -27,7 +27,7 @@ import karaoke from constants import LANGUAGES, VERSION -from lib.get_platform import get_platform +from lib.get_platform import get_platform, is_raspberry_pi try: from urllib.parse import quote, unquote @@ -45,7 +45,8 @@ babel = Babel(app) site_name = "PiKaraoke" admin_password = None -is_raspberry_pi = get_platform() == "raspberry_pi" +raspberry_pi = is_raspberry_pi() +linux = get_platform() == "linux" def filename_from_path(file_path, remove_youtube_id=True): rc = os.path.basename(file_path) @@ -461,7 +462,7 @@ def edit_file(): @app.route("/splash") def splash(): # Only do this on Raspberry Pis - if is_raspberry_pi: + if raspberry_pi: status = subprocess.run(['iwconfig', 'wlan0'], stdout=subprocess.PIPE).stdout.decode('utf-8') text = "" if "Mode:Master" in status: @@ -546,8 +547,12 @@ def info(): memory=memory, cpu=cpu, disk=disk, + ffmpeg_version=k.ffmpeg_version, youtubedl_version=youtubedl_version, - is_pi=is_raspberry_pi, + platform=k.platform, + os_version=k.os_version, + is_pi=raspberry_pi, + is_linux=linux, pikaraoke_version=VERSION, admin=is_admin(), admin_enabled=admin_password != None @@ -631,11 +636,11 @@ def reboot(): @app.route("/expand_fs") def expand_fs(): - if (is_admin() and is_raspberry_pi): + if (is_admin() and raspberry_pi): flash("Expanding filesystem and rebooting system now!", "is-danger") th = threading.Thread(target=delayed_halt, args=[3]) th.start() - elif (platform != "raspberry_pi"): + elif (not raspberry_pi): flash("Cannot expand fs on non-raspberry pi devices!", "is-danger") else: flash("You don't have permission to resize the filesystem", "is-danger") @@ -652,7 +657,7 @@ def get_default_youtube_dl_path(platform): def get_default_dl_dir(platform): - if is_raspberry_pi: + if raspberry_pi: return "~/pikaraoke-songs" elif platform == "windows": legacy_directory = os.path.expanduser("~\pikaraoke\songs") @@ -883,7 +888,7 @@ def get_default_dl_dir(platform): # Start the splash screen using selenium if not args.hide_splash_screen: - if platform == "raspberry_pi": + if raspberry_pi: service = Service(executable_path='/usr/bin/chromedriver') else: service = None diff --git a/constants.py b/constants.py index 49bb2b2a..1eb38bf0 100644 --- a/constants.py +++ b/constants.py @@ -1,4 +1,4 @@ -VERSION = "1.2" +VERSION = "1.2.1" LANGUAGES = { "en": "English", "zh_CN": "Chinese", diff --git a/karaoke.py b/karaoke.py index 7816c333..8fd0cd38 100644 --- a/karaoke.py +++ b/karaoke.py @@ -17,7 +17,8 @@ from unidecode import unidecode from lib.file_resolver import FileResolver -from lib.get_platform import get_platform +from lib.get_platform import (get_ffmpeg_version, get_os_version, get_platform, + is_raspberry_pi, supports_hardware_h264_encoding) # Support function for reading lines from ffmpeg stderr without blocking @@ -57,6 +58,12 @@ class Karaoke: screensaver_timeout = 300 # in seconds ffmpeg_process = None + ffmpeg_log = None + ffmpeg_version = get_ffmpeg_version() + supports_hardware_h264_encoding = supports_hardware_h264_encoding() + + raspberry_pi = is_raspberry_pi() + os_version = get_os_version() def __init__( self, @@ -124,9 +131,15 @@ def __init__( logo path: {self.logo_path} log_level: {log_level} hide overlay: {self.hide_overlay} + + platform: {self.platform} + os version: {self.os_version} + ffmpeg version: {self.ffmpeg_version} + hardware h264 encoding: {self.supports_hardware_h264_encoding} + youtubedl-version: {self.get_youtubedl_version()} """) # Generate connection URL and QR code, - if self.platform == "raspberry_pi": + if self.raspberry_pi: #retry in case pi is still starting up # and doesn't have an IP yet (occurs when launched from /etc/rc.local) end_time = int(time.time()) + 30 @@ -361,6 +374,12 @@ def get_youtube_id_from_url(self, url): else: logging.error("Error parsing youtube id from url: " + url) return None + + def log_ffmpeg_output(self): + if self.ffmpeg_log != None and self.ffmpeg_log.qsize() > 0: + while self.ffmpeg_log.qsize() > 0: + output = self.ffmpeg_log.get_nowait() + logging.debug("[FFMPEG] " + decode_ignore(output)) def play_file(self, file_path, semitones=0): logging.info(f"Playing file: {file_path} transposed {semitones} semitones") @@ -379,7 +398,7 @@ def play_file(self, file_path, semitones=0): return False # use h/w acceleration on pi - default_vcodec = "h264_v4l2m2m" if self.platform == "raspberry_pi" else "libx264" + default_vcodec = "h264_v4l2m2m" if self.supports_hardware_h264_encoding else "libx264" # just copy the video stream if it's an mp4 or webm file, since they are supported natively in html5 # otherwise use the default h264 codec vcodec = "copy" if fr.file_extension == ".mp4" or fr.file_extension == ".webm" else default_vcodec @@ -390,21 +409,27 @@ def play_file(self, file_path, semitones=0): acodec = "aac" if is_transposed else "copy" input = ffmpeg.input(fr.file_path) audio = input.audio.filter("rubberband", pitch=pitch) if is_transposed else input.audio + # Ffmpeg outputs "Stream #0" when the stream is ready to consume + stream_ready_string = "Stream #" if (fr.cdg_file_path != None): #handle CDG files logging.info("Playing CDG/MP3 file: " + file_path) + # Ffmpeg outputs "Video: cdgraphics" when the stream is ready to consume + stream_ready_string = "Video: cdgraphics" # copyts helps with sync issues, fps=25 prevents ffmpeg from needlessly encoding cdg at 300fps cdg_input = ffmpeg.input(fr.cdg_file_path, copyts=None) video = cdg_input.video.filter("fps", fps=25) - #cdg is very fussy about these flags. pi needs to encode to aac and cant just copy the mp3 stream + #cdg is very fussy about these flags. + # pi ffmpeg needs to encode to aac and cant just copy the mp3 stream + # It alse appears to have memory issues with hardware acceleration h264_v4l2m2m output = ffmpeg.output(audio, video, ffmpeg_url, - vcodec=vcodec, acodec="aac", - pix_fmt="yuv420p", listen=1, f="mp4", video_bitrate=vbitrate, + vcodec="libx264", acodec="aac", preset="ultrafast", + pix_fmt="yuv420p", listen=1, f="mp4", video_bitrate="500k", movflags="frag_keyframe+default_base_moof") else: video = input.video output = ffmpeg.output(audio, video, ffmpeg_url, - vcodec=vcodec, acodec=acodec, + vcodec=vcodec, acodec=acodec, preset="ultrafast", listen=1, f="mp4", video_bitrate=vbitrate, movflags="frag_keyframe+default_base_moof") @@ -417,21 +442,20 @@ def play_file(self, file_path, semitones=0): # ffmpeg outputs everything useful to stderr for some insane reason! # prevent reading stderr from being a blocking action - q = Queue() - t = Thread(target=enqueue_output, args=(self.ffmpeg_process.stderr, q)) + self.ffmpeg_log = Queue() + t = Thread(target=enqueue_output, args=(self.ffmpeg_process.stderr, self.ffmpeg_log)) t.daemon = True t.start() while self.ffmpeg_process.poll() is None: try: - output = q.get_nowait() + output = self.ffmpeg_log.get_nowait() logging.debug("[FFMPEG] " + decode_ignore(output)) except Empty: pass else: - if "Stream #" in decode_ignore(output): + if stream_ready_string in decode_ignore(output): logging.debug("Stream ready!") - # Ffmpeg outputs "Stream #0" when the stream is ready to consume self.now_playing = self.filename_from_path(file_path) self.now_playing_filename = file_path self.now_playing_transpose = semitones @@ -440,15 +464,10 @@ def play_file(self, file_path, semitones=0): self.is_paused = False self.queue.pop(0) - # Keep logging output until the splash screen reports back that the stream is playing + # 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 - try: - output = q.get_nowait() - logging.debug("[FFMPEG] " + decode_ignore(output)) - except Empty: - pass max_retries -= 1 if self.is_playing: logging.debug("Stream is playing") @@ -635,6 +654,7 @@ def reset_now_playing(self): self.is_paused = True self.is_playing = False self.now_playing_transpose = 0 + self.ffmpeg_log = None def run(self): logging.info("Starting PiKaraoke!") @@ -652,6 +672,7 @@ def run(self): self.handle_run_loop() i += self.loop_interval self.play_file(self.queue[0]["file"], self.queue[0]["semitones"]) + self.log_ffmpeg_output() self.handle_run_loop() except KeyboardInterrupt: logging.warn("Keyboard interrupt: Exiting pikaraoke...") diff --git a/lib/get_platform.py b/lib/get_platform.py index 6bfde8a3..1cb40469 100644 --- a/lib/get_platform.py +++ b/lib/get_platform.py @@ -1,7 +1,23 @@ import os +import platform +import re +import subprocess import sys +def get_ffmpeg_version(): + try: + # Execute the command 'ffmpeg -version' + result = subprocess.run(['ffmpeg', '-version'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + # Parse the first line to get the version + first_line = result.stdout.split('\n')[0] + version_info = first_line.split(' ')[2] # Assumes the version info is the third element + return version_info + except FileNotFoundError: + return "FFmpeg is not installed" + except IndexError: + return "Unable to parse FFmpeg version" + def is_raspberry_pi(): try: return ( @@ -15,10 +31,33 @@ def get_platform(): if sys.platform == "darwin": return "osx" elif is_raspberry_pi(): - return "raspberry_pi" + try: + with open('/proc/device-tree/model', 'r') as file: + model = file.read().strip() + if "Raspberry Pi" in model: + return model # Returns something like "Raspberry Pi 4 Model B Rev 1.2" + except FileNotFoundError: + return "Rasperry Pi - unrecognized" elif sys.platform.startswith("linux"): return "linux" elif sys.platform.startswith("win"): return "windows" else: return "unknown" + +def get_os_version(): + return platform.version() + +def supports_hardware_h264_encoding(): + if is_raspberry_pi(): + platform = get_platform() + + # Raspberry Pi >= 5 no longer has hardware GPU decoding + match = re.search(r"Raspberry Pi (\d+)", platform) + if match: + model_number = int(match.group(1)) + if model_number >= 5: + return False + return True + else: + return False \ No newline at end of file diff --git a/lib/vlcclient.py b/lib/vlcclient.py index 9750c231..aecdab8d 100644 --- a/lib/vlcclient.py +++ b/lib/vlcclient.py @@ -1,7 +1,7 @@ import logging import os -import re import random +import re import shutil import string import subprocess @@ -13,7 +13,7 @@ import requests -from lib.get_platform import get_platform +from lib.get_platform import get_platform, is_raspberry_pi def get_default_vlc_path(platform): @@ -49,6 +49,7 @@ def __init__(self, port=5002, path=None, qrcode=None, url=None): self.path = get_default_vlc_path(self.platform) else: self.path = path + self.raspberry_pi = is_raspberry_pi() # Determine tmp directories (for things like extracted cdg files) if self.platform == "windows": @@ -173,8 +174,8 @@ def play_file_transpose(self, file_path, semitones): # Sample rate converter type # Different resampling algorithms are supported. The best one is slower, while the fast one exhibits # low quality. - - if self.platform == "raspberry_pi": + + if self.raspberry_pi: # pi sounds bad on hightest quality setting (CPU not sufficient) speex_quality = 10 src_type = 1 diff --git a/pikaraoke.sh b/pikaraoke.sh index 27253c4e..5b395c1a 100755 --- a/pikaraoke.sh +++ b/pikaraoke.sh @@ -1,4 +1,14 @@ #!/bin/bash cd "$(dirname "$0")" source .venv/bin/activate + +if [[ -n "$SSH_CLIENT" || -n "$SSH_TTY" ]]; then + if [[ "$DISPLAY" != ":0.0" ]]; then + echo "Warning: Running remotely via SSH. Setting DISPLAY=:0.0 to run on host display" + export DISPLAY=:0.0 + else + echo "DISPLAY is correctly set for SSH session." + fi +fi + python3 app.py $@ diff --git a/setup-windows.bat b/setup-windows.bat index 559af3e5..d8d51c15 100755 --- a/setup-windows.bat +++ b/setup-windows.bat @@ -9,6 +9,10 @@ if /i "%confirm%" == "n" goto end :setup +echo +echo "*** PULLING LATEST PIKARAOKE CODE ***." +git pull + echo echo "*** CREATING PYTHON VIRTUAL ENVIRONMENT ***" python3 -m venv .venv diff --git a/setup.sh b/setup.sh index f85042a9..d376904a 100755 --- a/setup.sh +++ b/setup.sh @@ -5,8 +5,13 @@ if [ $REPLY = "y" ] then ## setup stuff +echo +echo "*** PULLING LATEST PIKARAOKE CODE ***." +git pull +if [ $? -ne 0 ]; then echo "ERROR: 'git pull' failed with error code: $?"; exit 1; fi +echo -if [[ $(cat /etc/os-release | grep ^ID= | grep -i 'debian\|raspbian') != "" ]]; then +if [[ $(cat /etc/os-release | grep ^ID= | grep -i 'debian\|raspbian') != "" ]] || [[ $(cat /etc/os-release | grep ^ID_LIKE= | grep -i 'debian') != "" ]]; then echo "Client is a Debian-based system. Installing binaries"; echo echo "*** RUNNING APT-GET UPDATE ***" diff --git a/templates/info.html b/templates/info.html index 79e7ce82..9b1a6855 100644 --- a/templates/info.html +++ b/templates/info.html @@ -76,17 +76,26 @@

{% block title %} {# MSG: Header of the information section about the computer running Pikaraoke. #}

{% trans %}System Info{% endtrans %}

+ +

{% trans %}System stats{% endtrans %}


@@ -139,7 +148,7 @@

{% trans %}Shutdown{% endtrans %}