Skip to content

Commit

Permalink
Merge pull request #351 from vicwomg/show-more-debugging-info
Browse files Browse the repository at this point in the history
Improve diagnostics and handle error conditions
  • Loading branch information
vicwomg authored Jul 19, 2024
2 parents de474a8 + e2c96ac commit d9c8039
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 41 deletions.
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
21 changes: 13 additions & 8 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion constants.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VERSION = "1.2"
VERSION = "1.2.1"
LANGUAGES = {
"en": "English",
"zh_CN": "Chinese",
Expand Down
57 changes: 39 additions & 18 deletions karaoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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
Expand All @@ -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")

Expand All @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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!")
Expand All @@ -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...")
Expand Down
41 changes: 40 additions & 1 deletion lib/get_platform.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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
9 changes: 5 additions & 4 deletions lib/vlcclient.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging
import os
import re
import random
import re
import shutil
import string
import subprocess
Expand All @@ -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):
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions pikaraoke.sh
Original file line number Diff line number Diff line change
@@ -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 $@
4 changes: 4 additions & 0 deletions setup-windows.bat
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit d9c8039

Please sign in to comment.