def __get_streaming_video_download_cmd(self): # --retries infinite: in case downloading has transient errors youtube_dl_cmd_template = "yt-dlp {0} --retries infinite --format {1} --output - {2} | {3}" log_opts = '--no-progress' if Logger.get_level() <= Logger.DEBUG: log_opts = '' # show video download progress if not sys.stderr.isatty(): log_opts += ' --newline' # 50 MB. Based on one video, 1080p avc1 video consumes about 0.36 MB/s. So this should # be enough buffer for ~139s for a 1080p video, which is a lot higher resolution than we # are ever likely to use. video_buffer_size = 1024 * 1024 * 50 # Choose 'worst' video because we want our pixel ffmpeg video scaler to do less work when we # scale the video down to the LED matrix size. # # But the height should be at least the height of the LED matrix (this probably only matters # if someone made a very large LED matrix such that the worst quality video was lower resolution # than the LED matrix pixel dimensions). Filter on only height so that vertical videos don't # result in a super large resolution being chosen? /shrug ... could consider adding a filter on # width too. # # Use avc1 because this means h264, and the pi has hardware acceleration for this format. # See: https://github.com/dasl-/piwall2/blob/88030a47790e5ae208d2c9fe19f9c623fc736c83/docs/video_formats_and_hardware_acceleration.adoc#youtube--youtube-dl # # Fallback onto 'worst' rather than 'worstvideo', because some videos (live videos) only # have combined video + audio formats. Thus, 'worstvideo' would fail for them. video_format = ( f'worstvideo[vcodec^=avc1][height>={Config.get_or_throw("leds.display_height")}]/' + f'worst[vcodec^=avc1][height>={Config.get_or_throw("leds.display_height")}]' ) youtube_dl_video_cmd = youtube_dl_cmd_template.format( shlex.quote(self.__url), shlex.quote(video_format), log_opts, self.__get_mbuffer_cmd(video_buffer_size)) # Also use a 50MB buffer, because in some cases, the audio stream we download may also contain video. audio_buffer_size = 1024 * 1024 * 50 youtube_dl_audio_cmd = youtube_dl_cmd_template.format( shlex.quote(self.__url), # bestaudio: try to select the best audio-only format # bestaudio*: this is the fallback option -- select the best quality format that contains audio. # It may also contain video, e.g. in the case that there are no audio-only formats available. # Some videos (live videos) only have combined video + audio formats. Thus 'bestaudio' would # fail for them. shlex.quote('bestaudio/bestaudio*'), log_opts, self.__get_mbuffer_cmd(audio_buffer_size)) # Mux video from the first input with audio from the second input: https://stackoverflow.com/a/12943003/627663 # We need to specify, because in some cases, either input could contain both audio and video. But in most # cases, the first input will have only video, and the second input will have only audio. return ( f"{self.get_standard_ffmpeg_cmd()} -i <({youtube_dl_video_cmd}) -i <({youtube_dl_audio_cmd}) " + "-c copy -map 0:v:0 -map 1:a:0 -shortest -f mpegts -")
def get_standard_ffmpeg_cmd(): # unfortunately there's no way to make ffmpeg output its stats progress stuff with line breaks log_opts = '-nostats ' if sys.stderr.isatty(): log_opts = '-stats ' if Logger.get_level() <= Logger.DEBUG: pass # don't change anything, ffmpeg is pretty verbose by default else: log_opts += '-loglevel error' # Note: don't use ffmpeg's `-xerror` flag: # https://gist.github.com/dasl-/1ad012f55f33f14b44393960f66c6b00 return f"ffmpeg -hide_banner {log_opts} "