Skip to content

Commit

Permalink
feat: add parameter for full buffering approach
Browse files Browse the repository at this point in the history
  • Loading branch information
vicwomg committed Dec 26, 2024
1 parent 9fe00f2 commit 777ecda
Show file tree
Hide file tree
Showing 4 changed files with 36 additions and 15 deletions.
12 changes: 8 additions & 4 deletions pikaraoke/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -733,14 +733,11 @@ def stream_full(id):
with open(file_path, "rb") as file:
file_content = file.read()
return Response(file_content, mimetype="video/mp4")

# Extract range start and end from Range header (e.g., "bytes=0-499")
range_match = re.search(r"bytes=(\d+)-(\d*)", range_header)
start, end = range_match.groups()

start = int(start)
end = int(end) if end else file_size - 1

# Generate response with part of file
with open(file_path, "rb") as file:
file.seek(start)
Expand All @@ -752,7 +749,6 @@ def stream_full(id):
"Content-Range": f"bytes {start}-{end}/{file_size}",
"Content-Length": str(len(data)),
}

return Response(data, status=status_code, headers=headers)
except IOError:
flash("File not found.", "is-danger")
Expand Down Expand Up @@ -896,6 +892,13 @@ def main():
help="Download higher quality video. Note: requires ffmpeg and may cause CPU, download speed, and other performance issues",
required=False,
)
parser.add_argument(
"-b",
"--buffer-fully-before-playback",
action="store_true",
help="Allow ffmpeg transcoding buffer to fully complete before playback begins. This can help with streaming on slower devices and improve browser compatibility (Safari), but will increase the delay before playback begins.",
required=False,
)
parser.add_argument(
"--logo-path",
nargs="+",
Expand Down Expand Up @@ -958,6 +961,7 @@ def main():
log_level=args.log_level,
volume=parsed_volume,
normalize_audio=args.normalize_audio,
buffer_fully_before_playback=args.buffer_fully_before_playback,
hide_url=args.hide_url,
hide_raspiwifi_instructions=args.hide_raspiwifi_instructions,
hide_splash_screen=args.hide_splash_screen,
Expand Down
17 changes: 12 additions & 5 deletions pikaraoke/karaoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ def __init__(
high_quality=False,
volume=0.85,
normalize_audio=False,
buffer_fully_before_playback=False,
log_level=logging.DEBUG,
splash_delay=2,
youtubedl_path="/usr/local/bin/yt-dlp",
Expand All @@ -104,6 +105,7 @@ def __init__(
self.splash_delay = int(splash_delay)
self.volume = volume
self.normalize_audio = normalize_audio
self.buffer_fully_before_playback = buffer_fully_before_playback
self.youtubedl_path = youtubedl_path
self.logo_path = self.default_logo_path if logo_path == None else logo_path
self.hide_overlay = hide_overlay
Expand Down Expand Up @@ -135,6 +137,7 @@ def __init__(
download path: {self.download_path}
default volume: {self.volume}
normalize audio: {self.normalize_audio}
buffer audio fully before playback: {self.buffer_fully_before_playback}
youtube-dl path: {self.youtubedl_path}
logo path: {self.logo_path}
log_level: {log_level}
Expand Down Expand Up @@ -410,15 +413,17 @@ def play_file(self, file_path, semitones=0):
logging.info(f"Playing file: {file_path} transposed {semitones} semitones")

try:
fr = FileResolver(file_path)
fr = FileResolver(file_path, self.buffer_fully_before_playback)
except Exception as e:
logging.error("Error resolving file: " + str(e))
self.queue.pop(0)
return False

self.kill_ffmpeg()

ffmpeg_cmd = build_ffmpeg_cmd(fr, semitones, self.normalize_audio)
ffmpeg_cmd = build_ffmpeg_cmd(
fr, semitones, self.normalize_audio, self.buffer_fully_before_playback
)
self.ffmpeg_process = ffmpeg_cmd.run_async(pipe_stderr=True, pipe_stdin=True)

# ffmpeg outputs everything useful to stderr for some insane reason!
Expand All @@ -445,11 +450,13 @@ def play_file(self, file_path, semitones=0):
except:
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}")
if not self.buffer_fully_before_playback:
is_buffering_complete = output_file_size > buffering_threshold
if is_buffering_complete:
logging.debug(f"Buffering complete. File size: {output_file_size}")
except:
pass

# Check if the stream is ready to play. Determined by:
# - completed transcoding
# - buffered file size being greater than a threshold
Expand Down
12 changes: 8 additions & 4 deletions pikaraoke/lib/ffmpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from pikaraoke.lib.get_platform import supports_hardware_h264_encoding


def build_ffmpeg_cmd(fr, semitones=0, normalize_audio=True):
def build_ffmpeg_cmd(fr, semitones=0, normalize_audio=True, buffer_fully_before_playback=False):
# use h/w acceleration on pi
default_vcodec = "h264_v4l2m2m" if 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
Expand All @@ -28,14 +28,18 @@ def build_ffmpeg_cmd(fr, semitones=0, normalize_audio=True):
# normalize the audio
audio = audio.filter("loudnorm", i=-16, tp=-1.5, lra=11) if normalize_audio else audio

# frag_keyframe+default_base_moof is used to set the correct headers for streaming incomplete files,
# without it, there's better compatibility for streaming on certain browsers like Firefox
movflags = "+faststart" if buffer_fully_before_playback else "frag_keyframe+default_base_moof"

if fr.cdg_file_path != None: # handle CDG files
logging.info("Playing CDG/MP3 file: " + fr.file_path)
# 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 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
# It also appears to have memory issues with hardware acceleration h264_v4l2m2m
output = ffmpeg.output(
audio,
video,
Expand All @@ -47,7 +51,7 @@ def build_ffmpeg_cmd(fr, semitones=0, normalize_audio=True):
listen=1,
f="mp4",
video_bitrate="500k",
movflags="frag_keyframe+default_base_moof",
movflags=movflags,
)
else:
video = input.video
Expand All @@ -61,7 +65,7 @@ def build_ffmpeg_cmd(fr, semitones=0, normalize_audio=True):
listen=1,
f="mp4",
video_bitrate=vbitrate,
movflags="frag_keyframe+default_base_moof",
movflags=movflags,
)

args = output.get_args()
Expand Down
10 changes: 8 additions & 2 deletions pikaraoke/lib/file_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,19 @@ class FileResolver:
cdg_file_path = None
file_extension = None

def __init__(self, file_path):
def __init__(self, file_path, buffer_fully_before_playback=False):
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}"
if buffer_fully_before_playback:
# This route is used for streaming the full video file, and includes more
# accurate headers for safari and other browsers
self.stream_url_path = f"/stream/full/{self.stream_uid}"
else:
# This route is used for streaming the video file in chunks, only works on chrome
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):
Expand Down

0 comments on commit 777ecda

Please sign in to comment.