Created
November 9, 2021 05:29
-
-
Save dasl-/3a6b6abcbd01df2b93668d0e33c190b1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/piwall2/broadcaster/videobroadcaster.py b/piwall2/broadcaster/videobroadcaster.py | |
index 5ad7495..509d433 100644 | |
--- a/piwall2/broadcaster/videobroadcaster.py | |
+++ b/piwall2/broadcaster/videobroadcaster.py | |
@@ -65,91 +65,92 @@ class VideoBroadcaster: | |
attempt = 1 | |
max_attempts = 2 | |
while attempt <= max_attempts: | |
try: | |
self.__broadcast_internal() | |
break | |
except YoutubeDlException as e: | |
if attempt < max_attempts: | |
self.__logger.warning("Caught exception in VideoBroadcaster.__broadcast_internal: " + | |
traceback.format_exc()) | |
self.__logger.warning("Updating youtube-dl and retrying broadcast...") | |
self.__update_youtube_dl() | |
if attempt >= max_attempts: | |
raise e | |
finally: | |
self.__do_housekeeping() | |
attempt += 1 | |
def __broadcast_internal(self): | |
self.__logger.info(f"Starting broadcast for: {self.__video_url}") | |
+ self.__start_receivers() | |
+ | |
""" | |
What's going on here? We invoke youtube-dl (ytdl) three times in the broadcast code: | |
1) To populate video metadata, including dimensions which allow us to know how much to crop the video | |
2) To download the proper video format (which generally does not have sound included) and mux it with (3) | |
3) To download the best audio quality | |
Ytdl takes couple of seconds to be invoked. Luckily, (2) and (3) happen in parallel | |
(see self.____get_ffmpeg_input_clause). But that would still leave us with effectively two groups of ytdl | |
invocations which are happening serially: the group consisting of "1" and the group consisting of "2 and 3". | |
Note that (1) happens in self.__get_video_info. | |
By starting a separate process for "2 and 3", we can actually ensure that all three of these invocations | |
happen in parallel. This separate process is started in self.__start_download_and_convert_video_proc. | |
This shaves 2-3 seconds off of video start up time -- although this time saving is partially canceled out | |
by the `time.sleep(2)` we had to add below. | |
This requires that we break up the original single pipeline into two halves. Originally, a single | |
pipeline was responsible for downloading, converting, and broadcasting the video. Now we have two | |
pipelines that we start separately: | |
1) download_and_convert_video_proc, which downloads and converts the video | |
2) video_broadcast_proc, which broadcasts the converted video | |
We connect the stdout of (1) to the stdin of (2). | |
In order to run all the ytdl invocations in parallel, we had to break up the original single pipeline | |
into these two halves, because broadcasting the video requires having started the receivers first. | |
And starting the receivers requires knowing how much to crop, which requires knowing the video dimensions. | |
Thus, we need to know the video dimensions before broadcasting the video. Without breaking up the pipeline, | |
we wouldn't be able to enforce that we don't start broadcasting the video before knowing the dimensions. | |
""" | |
download_and_convert_video_proc = self.__start_download_and_convert_video_proc() | |
self.__get_video_info(assert_data_not_yet_loaded = True) | |
- self.__start_receivers() | |
""" | |
This `sleep` makes the videos more likely to start in-sync across all the TVs, but I'm not totally | |
sure why. My current theory is that this give the receivers enough time to start before the broadcast | |
command starts sending its data. | |
Another potential solution is making use of delay_buffer in video_broadcast_cmd, although I have | |
abandoned that approach for now: https://gist.github.com/dasl-/9ed9d160384a8dd77382ce6a07c43eb6 | |
Another thing I tried was only sending the data once a few megabytes have been read, in case it was a | |
problem with the first few megabytes of the video being downloaded slowly, but this approach resulted in | |
occasional very brief video artifacts (green screen, etc) within the first 30 seconds or so of playback: | |
https://gist.github.com/dasl-/f3fcc941e276d116320d6fa9e4de25de | |
See data collected on the effectiveness of this sleep: | |
https://gist.github.com/dasl-/e5c05bf89c7a92d43881a2ff978dc889 | |
""" | |
- time.sleep(2) | |
+ # time.sleep(2) | |
video_broadcast_proc = self.__start_video_broadcast_proc(download_and_convert_video_proc) | |
self.__logger.info("Waiting for download_and_convert_video and video_broadcast procs to end...") | |
has_download_and_convert_video_proc_ended = False | |
has_video_broadcast_proc_ended = False | |
while True: # Wait for the download_and_convert_video and video_broadcast procs to end... | |
if not has_download_and_convert_video_proc_ended and download_and_convert_video_proc.poll() is not None: | |
has_download_and_convert_video_proc_ended = True | |
if download_and_convert_video_proc.returncode != 0: | |
raise YoutubeDlException("The download_and_convert_video process exited non-zero: " + | |
f"{download_and_convert_video_proc.returncode}. This could mean an issue with youtube-dl; " + | |
"it may require updating.") | |
self.__logger.info("The download_and_convert_video proc ended.") | |
if not has_video_broadcast_proc_ended and video_broadcast_proc.poll() is not None: | |
has_video_broadcast_proc_ended = True | |
if video_broadcast_proc.returncode != 0: | |
raise Exception(f"The video broadcast process exited non-zero: {video_broadcast_proc.returncode}") | |
self.__logger.info("The video_broadcast proc ended.") | |
@@ -178,70 +179,75 @@ class VideoBroadcaster: | |
audio_clause = '-c:a mp2 -b:a 192k' # TODO: is this necessary? Can we use mp3? | |
if self.__get_video_url_type() == self.__VIDEO_URL_TYPE_LOCAL_FILE: | |
# Don't transcode audio if we don't need to | |
audio_clause = '-c:a copy' | |
# Mix the best audio with the video and send via multicast | |
# See: https://github.com/dasl-/piwall2/blob/main/docs/best_video_container_format_for_streaming.adoc | |
cmd = (f"set -o pipefail && {self.__get_standard_ffmpeg_cmd()} {ffmpeg_input_clause} " + | |
f"-c:v copy {audio_clause} -f mpegts -") | |
self.__logger.info(f"Running download_and_convert_video_proc command: {cmd}") | |
# Info on start_new_session: https://gist.github.com/dasl-/1379cc91fb8739efa5b9414f35101f5f | |
# Allows killing all processes (subshells, children, grandchildren, etc as a group) | |
download_and_convert_video_proc = subprocess.Popen( | |
cmd, shell = True, executable = '/usr/bin/bash', start_new_session = True, stdout = subprocess.PIPE | |
) | |
self.__download_and_convert_video_proc_pgid = os.getpgid(download_and_convert_video_proc.pid) | |
return download_and_convert_video_proc | |
def __start_video_broadcast_proc(self, download_and_convert_video_proc): | |
+ msg = { | |
+ 'video_width': self.__get_video_info()['width'], | |
+ 'video_height': self.__get_video_info()['height'], | |
+ } | |
+ self.__control_message_helper.send_msg(ControlMessageHelper.TYPE_VIDEO_DIMENSIONS, msg) | |
+ self.__logger.info("Sent video_dimensions control message.") | |
+ | |
# See: https://github.com/dasl-/piwall2/blob/main/docs/controlling_video_broadcast_speed.adoc | |
mbuffer_size = round(Receiver.VIDEO_PLAYBACK_MBUFFER_SIZE_BYTES / 2) | |
burst_throttling_clause = (f'HOME=/home/pi mbuffer -q -l /tmp/mbuffer.out -m {mbuffer_size}b | ' + | |
f'{self.__get_standard_ffmpeg_cmd()} -re -i pipe:0 -c:v copy -c:a copy -f mpegts - >/dev/null ; ' + | |
f'touch {self.__VIDEO_PLAYBACK_DONE_FILE}') | |
broadcasting_clause = (f"{DirectoryUtils().root_dir}/bin/msend_video " + | |
f'--log-uuid {shlex.quote(Logger.get_uuid())} ' + | |
f'--end-of-video-magic-bytes {self.END_OF_VIDEO_MAGIC_BYTES.decode()}') | |
# Mix the best audio with the video and send via multicast | |
# See: https://github.com/dasl-/piwall2/blob/main/docs/best_video_container_format_for_streaming.adoc | |
video_broadcast_cmd = ("set -o pipefail && " + | |
f"tee >({burst_throttling_clause}) >({broadcasting_clause}) >/dev/null") | |
self.__logger.info(f"Running broadcast command: {video_broadcast_cmd}") | |
# Info on start_new_session: https://gist.github.com/dasl-/1379cc91fb8739efa5b9414f35101f5f | |
# Allows killing all processes (subshells, children, grandchildren, etc as a group) | |
video_broadcast_proc = subprocess.Popen( | |
video_broadcast_cmd, shell = True, executable = '/usr/bin/bash', start_new_session = True, | |
stdin = download_and_convert_video_proc.stdout | |
) | |
self.__video_broadcast_proc_pgid = os.getpgid(video_broadcast_proc.pid) | |
return video_broadcast_proc | |
def __start_receivers(self): | |
msg = { | |
'log_uuid': Logger.get_uuid(), | |
- 'video_width': self.__get_video_info()['width'], | |
- 'video_height': self.__get_video_info()['height'], | |
- 'video_url_type': self.__get_video_url_type(), | |
+ 'video_url_type': self.__get_video_url_type() | |
} | |
self.__control_message_helper.send_msg(ControlMessageHelper.TYPE_PLAY_VIDEO, msg) | |
self.__logger.info("Sent play_video control message.") | |
def __get_standard_ffmpeg_cmd(self): | |
# 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' | |
return f"ffmpeg -hide_banner {log_opts} " | |
def __get_ffmpeg_input_clause(self): | |
video_url_type = self.__get_video_url_type() | |
if video_url_type == self.__VIDEO_URL_TYPE_YOUTUBE: | |
""" | |
diff --git a/piwall2/controlmessagehelper.py b/piwall2/controlmessagehelper.py | |
index 3be55ef..783227e 100644 | |
--- a/piwall2/controlmessagehelper.py | |
+++ b/piwall2/controlmessagehelper.py | |
@@ -1,38 +1,39 @@ | |
import json | |
from piwall2.logger import Logger | |
from piwall2.multicasthelper import MulticastHelper | |
# Helper for sending "control messages". Control messages are sent from the broadcaster via | |
# UDP multicast to control various aspects of the receivers: | |
# 1) controls volume on the receivers | |
# 2) signalling for starting video playback | |
# 3) signalling for skipping a video | |
# 4) signalling when to apply video effects, like adjusting the video tiling mode | |
# 5) etc | |
class ControlMessageHelper: | |
# Control message types | |
TYPE_VOLUME = 'volume' | |
TYPE_PLAY_VIDEO = 'play_video' | |
TYPE_SKIP_VIDEO = 'skip_video' | |
TYPE_DISPLAY_MODE = 'display_mode' | |
+ TYPE_VIDEO_DIMENSIONS = 'video_dimensions' | |
CTRL_MSG_TYPE_KEY = 'msg_type' | |
CONTENT_KEY = 'content' | |
def __init__(self): | |
self.__logger = Logger().set_namespace(self.__class__.__name__) | |
def setup_for_broadcaster(self): | |
self.__multicast_helper = MulticastHelper().setup_broadcaster_socket() | |
return self | |
def setup_for_receiver(self): | |
self.__multicast_helper = MulticastHelper().setup_receiver_control_socket() | |
return self | |
def send_msg(self, ctrl_msg_type, content): | |
msg = json.dumps({ | |
self.CTRL_MSG_TYPE_KEY: ctrl_msg_type, | |
self.CONTENT_KEY: content | |
}) | |
diff --git a/piwall2/displaymode.py b/piwall2/displaymode.py | |
index 3d73c18..02e552d 100644 | |
--- a/piwall2/displaymode.py | |
+++ b/piwall2/displaymode.py | |
@@ -1,8 +1,9 @@ | |
class DisplayMode: | |
# Tile mode is like this: https://i.imgur.com/BBrA1Cr.png | |
# Repeat mode is like this: https://i.imgur.com/cpS61s8.png | |
DISPLAY_MODE_TILE = 'DISPLAY_MODE_TILE' | |
DISPLAY_MODE_REPEAT = 'DISPLAY_MODE_REPEAT' | |
+ | |
DISPLAY_MODES = (DISPLAY_MODE_TILE, DISPLAY_MODE_REPEAT) | |
diff --git a/piwall2/receiver/receiver.py b/piwall2/receiver/receiver.py | |
index 49c6397..7ccc6fc 100644 | |
--- a/piwall2/receiver/receiver.py | |
+++ b/piwall2/receiver/receiver.py | |
@@ -65,83 +65,89 @@ class Receiver: | |
self.__run_internal() | |
except Exception: | |
self.__logger.error('Caught exception: {}'.format(traceback.format_exc())) | |
def __run_internal(self): | |
ctrl_msg = None | |
ctrl_msg = self.__control_message_helper.receive_msg() # This blocks until a message is received! | |
self.__logger.debug(f"Received control message {ctrl_msg}. " + | |
f"self.__is_video_playback_in_progress: {self.__is_video_playback_in_progress}.") | |
if self.__is_video_playback_in_progress: | |
if self.__receive_and_play_video_proc and self.__receive_and_play_video_proc.poll() is not None: | |
self.__logger.info("Ending video playback because receive_and_play_video_proc is no longer running...") | |
self.__stop_video_playback_if_playing() | |
msg_type = ctrl_msg[ControlMessageHelper.CTRL_MSG_TYPE_KEY] | |
if msg_type == ControlMessageHelper.TYPE_PLAY_VIDEO: | |
self.__stop_video_playback_if_playing() | |
self.__receive_and_play_video_proc = self.__receive_and_play_video(ctrl_msg) | |
self.__receive_and_play_video_proc_pgid = os.getpgid(self.__receive_and_play_video_proc.pid) | |
+ elif msg_type == ControlMessageHelper.TYPE_VIDEO_DIMENSIONS: | |
+ if self.__is_video_playback_in_progress: | |
+ self.__set_video_crop_args(ctrl_msg) | |
elif msg_type == ControlMessageHelper.TYPE_SKIP_VIDEO: | |
if self.__is_video_playback_in_progress: | |
self.__stop_video_playback_if_playing() | |
elif msg_type == ControlMessageHelper.TYPE_VOLUME: | |
self.__video_player_volume_pct = ctrl_msg[ControlMessageHelper.CONTENT_KEY] | |
if self.__is_video_playback_in_progress: | |
self.__omxplayer_controller.set_vol_pct(self.__video_player_volume_pct) | |
elif msg_type == ControlMessageHelper.TYPE_DISPLAY_MODE: | |
display_mode_by_tv_id = ctrl_msg[ControlMessageHelper.CONTENT_KEY] | |
old_display_mode = self.__display_mode | |
old_display_mode2 = self.__display_mode2 | |
for tv_num, tv_id in self.__tv_ids.items(): | |
- if tv_id in display_mode_by_tv_id: | |
display_mode_to_set = display_mode_by_tv_id[tv_id] | |
if display_mode_to_set not in DisplayMode.DISPLAY_MODES: | |
display_mode_to_set = DisplayMode.DISPLAY_MODE_TILE | |
+ if tv_id in display_mode_by_tv_id: | |
if tv_num == 1: | |
self.__display_mode = display_mode_to_set | |
else: | |
self.__display_mode2 = display_mode_to_set | |
if self.__is_video_playback_in_progress and self.__crop_args and old_display_mode != self.__display_mode: | |
self.__omxplayer_controller.set_crop(self.__crop_args[self.__display_mode]) | |
if self.__is_video_playback_in_progress and self.__crop_args2 and old_display_mode2 != self.__display_mode2: | |
pass # TODO display_mode2 with a second dbus interface name | |
def __receive_and_play_video(self, ctrl_msg): | |
ctrl_msg_content = ctrl_msg[ControlMessageHelper.CONTENT_KEY] | |
self.__orig_log_uuid = Logger.get_uuid() | |
Logger.set_uuid(ctrl_msg_content['log_uuid']) | |
- cmd, self.__crop_args, self.__crop_args2 = ( | |
- self.__receiver_command_builder.build_receive_and_play_video_command_and_get_crop_args( | |
- ctrl_msg_content['log_uuid'], ctrl_msg_content['video_width'], | |
- ctrl_msg_content['video_height'], self.__video_player_volume_pct, | |
- self.__display_mode, self.__display_mode2 | |
- ) | |
+ cmd = self.__receiver_command_builder.build_receive_and_play_video_command( | |
+ ctrl_msg_content['log_uuid'], self.__video_player_volume_pct | |
) | |
self.__logger.info(f"Running receive_and_play_video command: {cmd}") | |
self.__is_video_playback_in_progress = True | |
proc = subprocess.Popen( | |
cmd, shell = True, executable = '/usr/bin/bash', start_new_session = True | |
) | |
return proc | |
+ def __set_video_crop_args(self, ctrl_msg): | |
+ ctrl_msg_content = ctrl_msg[ControlMessageHelper.CONTENT_KEY] | |
+ self.__crop_args, self.__crop_args2 = self.__receiver_command_builder.get_crop_dimensions( | |
+ ctrl_msg_content['video_width'], ctrl_msg_content['video_height'] | |
+ ) | |
+ self.__omxplayer_controller.set_crop(self.__crop_args[self.__display_mode]) | |
+ | |
def __stop_video_playback_if_playing(self): | |
if not self.__is_video_playback_in_progress: | |
return | |
if self.__receive_and_play_video_proc_pgid: | |
self.__logger.info("Killing receive_and_play_video proc (if it's still running)...") | |
try: | |
os.killpg(self.__receive_and_play_video_proc_pgid, signal.SIGTERM) | |
except Exception: | |
# might raise: `ProcessLookupError: [Errno 3] No such process` | |
pass | |
Logger.set_uuid(self.__orig_log_uuid) | |
self.__is_video_playback_in_progress = False | |
self.__crop_args = None | |
self.__crop_args2 = None | |
# The first video that is played after a system restart appears to have a lag in starting, | |
# which can affect video synchronization across the receivers. Ensure we have played at | |
# least one video since system startup. This is a short, one-second video. | |
# | |
# Perhaps one thing this warmup video does is start the various dbus processes for the first | |
diff --git a/piwall2/receiver/receivercommandbuilder.py b/piwall2/receiver/receivercommandbuilder.py | |
index 2e00fc8..d4c3f64 100644 | |
--- a/piwall2/receiver/receivercommandbuilder.py | |
+++ b/piwall2/receiver/receivercommandbuilder.py | |
@@ -1,45 +1,40 @@ | |
import math | |
import shlex | |
from piwall2.directoryutils import DirectoryUtils | |
from piwall2.displaymode import DisplayMode | |
from piwall2.logger import Logger | |
import piwall2.receiver.receiver | |
from piwall2.volumecontroller import VolumeController | |
# Helper to build the "receive and play video" command | |
class ReceiverCommandBuilder: | |
def __init__(self, config_loader, receiver_config_stanza): | |
self.__logger = Logger().set_namespace(self.__class__.__name__) | |
self.__config_loader = config_loader | |
self.__receiver_config_stanza = receiver_config_stanza | |
- def build_receive_and_play_video_command_and_get_crop_args( | |
- self, log_uuid, video_width, video_height, volume_pct, display_mode, display_mode2 | |
- ): | |
+ def build_receive_and_play_video_command(self, log_uuid, volume_pct): | |
adev, adev2 = self.__get_video_command_adev_args() | |
display, display2 = self.__get_video_command_display_args() | |
- crop_args, crop_args2 = self.__get_video_command_crop_args(video_width, video_height) | |
- crop = crop_args[display_mode] | |
- crop2 = crop_args2[display_mode2] | |
# See: https://github.com/popcornmix/omxplayer/#volume-rw | |
volume_pct = VolumeController.normalize_vol_pct(volume_pct) | |
if volume_pct == 0: | |
volume_millibels = VolumeController.GLOBAL_MIN_VOL_VAL | |
else: | |
volume_millibels = 2000 * math.log(volume_pct, 10) | |
""" | |
We use mbuffer in the receiver command. The mbuffer is here to solve two problems: | |
1) Sometimes the python receiver process would get blocked writing directly to omxplayer. When this happens, | |
the receiver's writes would occur rather slowly. While the receiver is blocked on writing, it cannot read | |
incoming data from the UDP socket. The kernel's UDP buffers would then fill up, causing UDP packets to be | |
dropped. | |
Unlike python, mbuffer is multithreaded, meaning it can read and write simultaneously in two separate | |
threads. Thus, while mbuffer is writing to omxplayer, it can still read the incoming data from python at | |
full speed. Slow writes will not block reads. | |
@@ -47,112 +42,68 @@ class ReceiverCommandBuilder: | |
% omxplayer --help | |
... | |
--audio_fifo n Size of audio output fifo in seconds | |
--video_fifo n Size of video output fifo in MB | |
--audio_queue n Size of audio input queue in MB | |
--video_queue n Size of video input queue in MB | |
... | |
More info: https://github.com/popcornmix/omxplayer/issues/256#issuecomment-57907940 | |
I am not sure which I would need to adjust to ensure enough buffering is available. By using mbuffer, | |
we effectively have a single buffer that accounts for any possible source of delays, whether it's from audio, | |
video, and no matter where in the pipeline the delay is coming from. Using mbuffer seems simpler, and it is | |
easier to monitor. By checking its logs, we can see how close the mbuffer gets to becoming full. | |
""" | |
mbuffer_cmd = ('HOME=/home/pi mbuffer -q -l /tmp/mbuffer.out -m ' + | |
f'{piwall2.receiver.receiver.Receiver.VIDEO_PLAYBACK_MBUFFER_SIZE_BYTES}b') | |
# See: https://github.com/dasl-/piwall2/blob/main/docs/configuring_omxplayer.adoc | |
- omx_cmd_template = ('omxplayer --crop {0} --adev {1} --display {2} --vol {3} --aspect-mode stretch ' + | |
+ omx_cmd_template = ('omxplayer --adev {0} --display {1} --vol {2} --aspect-mode stretch ' + | |
'--no-keys --timeout 30 --threshold 0.2 --video_fifo 10 --genlog pipe:0') | |
omx_cmd = omx_cmd_template.format( | |
- shlex.quote(crop), shlex.quote(adev), shlex.quote(display), shlex.quote(str(volume_millibels)) | |
+ shlex.quote(adev), shlex.quote(display), shlex.quote(str(volume_millibels)) | |
) | |
cmd = 'set -o pipefail && ' | |
if self.__receiver_config_stanza['is_dual_video_output']: | |
omx_cmd2 = omx_cmd_template.format( | |
- shlex.quote(crop2), shlex.quote(adev2), shlex.quote(display2), shlex.quote(str(volume_millibels)) | |
+ shlex.quote(adev2), shlex.quote(display2), shlex.quote(str(volume_millibels)) | |
) | |
cmd += f'{mbuffer_cmd} | tee >({omx_cmd}) >({omx_cmd2}) >/dev/null' | |
else: | |
cmd += f'{mbuffer_cmd} | {omx_cmd}' | |
receiver_cmd = (f'{DirectoryUtils().root_dir}/bin/receive_and_play_video --command {shlex.quote(cmd)} ' + | |
f'--log-uuid {shlex.quote(log_uuid)}') | |
- return (receiver_cmd, crop_args, crop_args2) | |
- | |
- def __get_video_command_adev_args(self): | |
- receiver_config = self.__receiver_config_stanza | |
- adev = None | |
- if receiver_config['audio'] == 'hdmi' or receiver_config['audio'] == 'hdmi0': | |
- adev = 'hdmi' | |
- elif receiver_config['audio'] == 'headphone': | |
- adev = 'local' | |
- elif receiver_config['audio'] == 'hdmi_alsa' or receiver_config['audio'] == 'hdmi0_alsa': | |
- adev = 'alsa:default:CARD=b1' | |
- else: | |
- raise Exception(f"Unexpected audio config value: {receiver_config['audio']}") | |
- | |
- adev2 = None | |
- if receiver_config['is_dual_video_output']: | |
- if receiver_config['audio2'] == 'hdmi1': | |
- adev2 = 'hdmi1' | |
- elif receiver_config['audio2'] == 'headphone': | |
- adev2 = 'local' | |
- elif receiver_config['audio'] == 'hdmi1_alsa': | |
- adev2 = 'alsa:default:CARD=b2' | |
- else: | |
- raise Exception(f"Unexpected audio2 config value: {receiver_config['audio2']}") | |
- | |
- return (adev, adev2) | |
- | |
- def __get_video_command_display_args(self): | |
- receiver_config = self.__receiver_config_stanza | |
- display = None | |
- if receiver_config['video'] == 'hdmi' or receiver_config['video'] == 'hdmi0': | |
- display = '2' | |
- elif receiver_config['video'] == 'composite': | |
- display = '3' | |
- else: | |
- raise Exception(f"Unexpected video config value: {receiver_config['video']}") | |
- | |
- display2 = None | |
- if receiver_config['is_dual_video_output']: | |
- if receiver_config['video2'] == 'hdmi1': | |
- display2 = '7' | |
- else: | |
- raise Exception(f"Unexpected video2 config value: {receiver_config['video2']}") | |
- | |
- return (display, display2) | |
+ return receiver_cmd | |
""" | |
Returns a set of crop args supporting two display modes: tile mode and repeat mode. | |
Tile mode is like this: https://i.imgur.com/BBrA1Cr.png | |
Repeat mode is like this: https://i.imgur.com/cpS61s8.png | |
We return four crop settings because for each mode, we calculate the crop arguments | |
for each of two TVs (each receiver can have at most two TVs hooked up to it). | |
""" | |
- def __get_video_command_crop_args(self, video_width, video_height): | |
+ def get_crop_dimensions(self, video_width, video_height): | |
receiver_config = self.__receiver_config_stanza | |
##################################################################################### | |
# Do tile mode calculations first ################################################### | |
##################################################################################### | |
wall_width = self.__config_loader.get_wall_width() | |
wall_height = self.__config_loader.get_wall_height() | |
displayable_video_width, displayable_video_height = ( | |
self.__get_displayable_video_dimensions_for_screen( | |
video_width, video_height, wall_width, wall_height | |
) | |
) | |
x_offset = (video_width - displayable_video_width) / 2 | |
y_offset = (video_height - displayable_video_height) / 2 | |
x0 = round(x_offset + ((receiver_config['x'] / wall_width) * displayable_video_width)) | |
y0 = round(y_offset + ((receiver_config['y'] / wall_height) * displayable_video_height)) | |
x1 = round(x_offset + (((receiver_config['x'] + receiver_config['width']) / wall_width) * displayable_video_width)) | |
y1 = round(y_offset + (((receiver_config['y'] + receiver_config['height']) / wall_height) * displayable_video_height)) | |
@@ -212,40 +163,84 @@ class ReceiverCommandBuilder: | |
displayable_video_width, displayable_video_height = ( | |
self.__get_displayable_video_dimensions_for_screen( | |
video_width, video_height, receiver_config['width2'], receiver_config['height2'] | |
) | |
) | |
x_offset = (video_width - displayable_video_width) / 2 | |
y_offset = (video_height - displayable_video_height) / 2 | |
repeat_mode_crop2 = f"{x_offset} {y_offset} {x_offset + displayable_video_width} {y_offset + displayable_video_height}" | |
crop_args = { | |
DisplayMode.DISPLAY_MODE_TILE: tile_mode_crop, | |
DisplayMode.DISPLAY_MODE_REPEAT: repeat_mode_crop, | |
} | |
crop_args2 = { | |
DisplayMode.DISPLAY_MODE_TILE: tile_mode_crop2, | |
DisplayMode.DISPLAY_MODE_REPEAT: repeat_mode_crop2, | |
} | |
return (crop_args, crop_args2) | |
+ def __get_video_command_adev_args(self): | |
+ receiver_config = self.__receiver_config_stanza | |
+ adev = None | |
+ if receiver_config['audio'] == 'hdmi' or receiver_config['audio'] == 'hdmi0': | |
+ adev = 'hdmi' | |
+ elif receiver_config['audio'] == 'headphone': | |
+ adev = 'local' | |
+ elif receiver_config['audio'] == 'hdmi_alsa' or receiver_config['audio'] == 'hdmi0_alsa': | |
+ adev = 'alsa:default:CARD=b1' | |
+ else: | |
+ raise Exception(f"Unexpected audio config value: {receiver_config['audio']}") | |
+ | |
+ adev2 = None | |
+ if receiver_config['is_dual_video_output']: | |
+ if receiver_config['audio2'] == 'hdmi1': | |
+ adev2 = 'hdmi1' | |
+ elif receiver_config['audio2'] == 'headphone': | |
+ adev2 = 'local' | |
+ elif receiver_config['audio'] == 'hdmi1_alsa': | |
+ adev2 = 'alsa:default:CARD=b2' | |
+ else: | |
+ raise Exception(f"Unexpected audio2 config value: {receiver_config['audio2']}") | |
+ | |
+ return (adev, adev2) | |
+ | |
+ def __get_video_command_display_args(self): | |
+ receiver_config = self.__receiver_config_stanza | |
+ display = None | |
+ if receiver_config['video'] == 'hdmi' or receiver_config['video'] == 'hdmi0': | |
+ display = '2' | |
+ elif receiver_config['video'] == 'composite': | |
+ display = '3' | |
+ else: | |
+ raise Exception(f"Unexpected video config value: {receiver_config['video']}") | |
+ | |
+ display2 = None | |
+ if receiver_config['is_dual_video_output']: | |
+ if receiver_config['video2'] == 'hdmi1': | |
+ display2 = '7' | |
+ else: | |
+ raise Exception(f"Unexpected video2 config value: {receiver_config['video2']}") | |
+ | |
+ return (display, display2) | |
+ | |
""" | |
The displayable width and height represents the section of the video that the wall will be | |
displaying. A section of these dimensions will be taken from the center of the original | |
video. | |
Currently, the piwall only supports displaying videos in "fill" mode (as opposed to | |
"letterbox" or "stretch"). This means that every portion of the TVs will be displaying | |
some section of the video (i.e. there will be no letterboxing). Furthermore, there will be | |
no warping of the video's aspect ratio. Instead, regions of the original video will be | |
cropped or stretched if necessary. | |
The units of the width and height arguments are not important. We are just concerned with | |
the aspect ratio. Thus, as long as video_width and video_height are in the same units | |
(probably pixels), and as long as screen_width and screen_height are in the same units | |
(probably inches or centimeters), everything will work. | |
The returned dimensions will be in the units of the inputted video_width and video_height | |
(probably pixels). | |
""" | |
def __get_displayable_video_dimensions_for_screen(self, video_width, video_height, screen_width, screen_height): |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment