Open manodory opened 2 days ago
To make sure I understand: the catalog build worked and playback started, but after about 30 minutes of play it produced the above error? I assume it had gotten through some commercials and bumps at that point? Its saturday morning, so cartoons are on - so I'll do a little testing and see if I can tell whats going on.
This part of the code is called when a new show comes on. Since you were able to start playing a show, I assume that it worked one time and that made it through the entire play loop for that show and this happened when it tries to start the next show. I did some 'testing' and everything seems to be working well in the nominal cases, just watched through an episode of shirt-tales, snorks and then alvin and the chipmunks - all the transitions worked fine. I was hoping for something obvious :)
Looking at the piece of code that fails, from the function it is calling it is very clear that there is an error condition that can happen:
def _find_index_at_offset(self, offset):
abs_start = 0
abs_end = 0
index = 0
for _entry in self.playlist:
abs_start = abs_end
abs_end = abs_start + _entry['duration']
if offset > abs_start and offset <= abs_end:
d2 = offset - abs_start
return(index, d2)
index += 1
if no condition is met that satisfies offset > abs_start and offset <= abs_end
then the function will indeed return None
- which means that in every condition tested, offset <= abs_start or offset > abs_end
When I look at how the values being tested are constructed here:
abs_start = abs_end
abs_end = abs_start + _entry['duration']
if _entry['duration'] == 0
then abs_start
and abs_duration
so offset > abs_start and offset <= abs_end
will never be true unless offset is also the exact same value. I suspect you have a video that FFMEG reported had (zero length or close to) duration.
I don't check for this condition when building the catalog. I'll add a 'catalog checker' functionality to station_42.py to check for this condition - give me an hour or so. I almost think the catalog build should fail in this case since there isn't a valid use case for zero duration videos.
Thanks for your update! Just to tell something... I'm using your project as a nostalgic cable box (: I emptied an old cable box from it's content, connected a seven segment display and an IR reciever to the Pi 5, taught it all the buttons of the cable box old remote, as for now I've added only four channels. As I'm not from the US and it's actually a cable box emulation, each channel has it's own shows. The two first channels are the videoflex channel and the schedue board, so I just run a single video in them (It would be great to have a looped channel funtion). Channel 3 is for some TV series, and channel 4 brodcasts only movies. I haven't built the rest of the channels, but 5 would be the sports channel, 6 is the kids channel, 7 is MTV and so on. That error was indeed showing after an epiode was finished in channel 3.
BTW, if you're interested I can give you my code for driving the remote and the seven segment display.
That's amazing! Would love to learn more - this is exactly the type of things I hoped people would do with it.
This should help: I am adding a 'cable-mode' switch in the next couple of days that will make the channel transition act more like cable. I'm also going to add a 'preview channel' for it.
Is there anyway I can communicate with you directly?
My email is manodory
Thanks for your update! Just to tell something... I'm using your project as a nostalgic cable box (: I emptied an old cable box from it's content, connected a seven segment display and an IR reciever to the Pi 5 [...] BTW, if you're interested I can give you my code for driving the remote and the seven segment display.
I'd like this myself. I rescued a small TV that uses seven segment displays that was easily disconnected from its host PCB, and I've been working on interfacing it (and a few other buttons, like TV/VCR) to a Pi's GPIO.
Thank you for the great idea :)
I'll email you @manodory - I just committed a change that will let you test out your catalogs to make sure you don' have any zero-length videos, which is what FFMPEG will report if the video is corrupted. Run it like this:
python3 station_42.py -c
or
python3 station_42.py --check_catalogs
it will report on any videos it finds that report duration of less than one second. Run that and see if you have any bad files in there.
One additional update: I restricted the candidate search in Catalog.find_candidate
so that it wont return videos under 1 second in length. Let me know if the above new command finds any issues in your content.
Thanks for your update! Just to tell something... I'm using your project as a nostalgic cable box (: I emptied an old cable box from it's content, connected a seven segment display and an IR reciever to the Pi 5 [...] BTW, if you're interested I can give you my code for driving the remote and the seven segment display.
I'd like this myself. I rescued a small TV that uses seven segment displays that was easily disconnected from its host PCB, and I've been working on interfacing it (and a few other buttons, like TV/VCR) to a Pi's GPIO.
Thank you for the great idea :)
Here is my code to operate the hardware part of my cablebox - let me know if you need some explanations:
from gpiozero import LED, Button from signal import pause import subprocess import time from threading import Thread
segments = { '0': [0, 0, 0, 0, 0, 0, 1], '1': [1, 0, 0, 1, 1, 1, 1], '2': [0, 0, 1, 0, 0, 1, 0], '3': [0, 0, 0, 0, 1, 1, 0], '4': [1, 0, 0, 1, 1, 0, 0], '5': [0, 1, 0, 0, 1, 0, 0], '6': [0, 1, 0, 0, 0, 0, 0], '7': [0, 0, 0, 1, 1, 1, 1], '8': [0, 0, 0, 0, 0, 0, 0], '9': [0, 0, 0, 0, 1, 0, 0], '-': [1, 1, 1, 1, 1, 1, 0], }
left_segment_pins = [17, 18, 27, 22, 23, 24, 25] right_segment_pins = [8, 6, 13, 19, 26, 21, 20]
left_leds = [LED(pin) for pin in left_segment_pins] right_leds = [LED(pin) for pin in right_segment_pins]
decimal_dots = [LED(12), LED(16)]
button_ch_plus = Button(2, bounce_time=0.1) button_ch_minus = Button(3, bounce_time=0.1) button_pwr = Button(4, bounce_time=0.1)
count = 1 onoff_mode = 'cablebox_on' first_digit = None digit_timeout = 4 # Seconds to wait for the second digit debounce_delay = 0.3 # Debounce delay in seconds last_channel = None # Track the last selected channel
def sevensegmentdisplay_display_number(leds, num): seg = segments[num] for i in range(7): if seg[i] == 1: leds[i].on() else: leds[i].off()
def sevensegmentdisplay_clear(leds): for led in leds: led.on() sevensegmentdisplay_turn_off_decimal_dots()
def sevensegmentdisplay_turn_on_left_decimal_dot(): decimal_dots[0].off() # Turn on only the left decimal dot decimal_dots[1].on() # Ensure the right decimal dot is off
def sevensegmentdisplay_turn_off_decimal_dots(): for dot in decimal_dots: dot.on() # Turn off both decimal dots
def update_cablebox_status(): global count, first_digit, last_channel if onoff_mode == 'cablebox_off': sevensegmentdisplay_clear(left_leds) sevensegmentdisplay_clear(right_leds) sevensegmentdisplay_turn_on_left_decimal_dot() channel_off() else: sevensegmentdisplay_turn_off_decimal_dots() left_digit = str(count // 10) if count >= 10 else '0' right_digit = str(count % 10)
if left_digit == '0':
sevensegmentdisplay_clear(left_leds)
sevensegmentdisplay_display_number(right_leds, right_digit)
else:
sevensegmentdisplay_display_number(left_leds, left_digit)
sevensegmentdisplay_display_number(right_leds, right_digit)
# Only call the channel function if the new channel is different from the last channel
if count != last_channel:
channel_function = channel_functions.get(count)
if channel_function:
channel_function()
last_channel = count # Update the last_channel to the current one
first_digit = None
def increment_channels(): global count count = 1 if count == 24 else count + 1 update_cablebox_status()
def decrement_channels(): global count count = 24 if count == 1 else count - 1 update_cablebox_status()
def toggle_on_off(): global onoff_mode onoff_mode = 'cablebox_off' if onoff_mode == 'cablebox_on' else 'cablebox_on' update_cablebox_status()
def handle_digit_input(digit): global first_digit, count if first_digit is None:
first_digit = digit
sevensegmentdisplay_display_number(left_leds, str(first_digit))
sevensegmentdisplay_display_number(right_leds, '-')
# Start a timer to wait for the second digit
Thread(target=wait_for_second_digit).start()
else:
# Second digit input
channel_number = int(f"{first_digit}{digit}")
if 1 <= channel_number <= 24:
count = channel_number
update_cablebox_status()
def wait_for_second_digit(): global first_digit, count time.sleep(digit_timeout) if first_digit is not None: if first_digit == 0: first_digit = None update_cablebox_status() else: count = int(first_digit) update_cablebox_status()
def on_remote_channel_up(): increment_channels()
def on_remote_channel_down(): decrement_channels()
def on_remote_power_button(): toggle_on_off()
def on_remote_numpad(digit): handle_digit_input(digit)
def listen_for_remote_input(): process = subprocess.Popen(['irw'], stdout=subprocess.PIPE, text=True) print("Listening for remote input...") last_press_time = {} # Track the last press time for each key power_button_debounce = 1 # 1-second debounce for power button
for line in process.stdout:
# Parse the key press
if "KEY_" in line:
key_name = line.strip().split()[2] # Extract key name
current_time = time.time()
# Set debounce delay based on key
if key_name == "KEY_POWER":
debounce_time = power_button_debounce
else:
debounce_time = debounce_delay
# Check if enough time has passed since the last press of this specific key
if key_name in last_press_time and current_time - last_press_time[key_name] < debounce_time:
continue # Skip this press if within debounce delay
# Update last press time for this key
last_press_time[key_name] = current_time
# Channel up, down, and power buttons
if key_name == "KEY_CHANNELUP":
on_remote_channel_up()
elif key_name == "KEY_CHANNELDOWN":
on_remote_channel_down()
elif key_name == "KEY_POWER":
on_remote_power_button()
# Number buttons from 0 to 9
elif key_name == "KEY_1":
on_remote_numpad(1)
elif key_name == "KEY_2":
on_remote_numpad(2)
elif key_name == "KEY_3":
on_remote_numpad(3)
elif key_name == "KEY_4":
on_remote_numpad(4)
elif key_name == "KEY_5":
on_remote_numpad(5)
elif key_name == "KEY_6":
on_remote_numpad(6)
elif key_name == "KEY_7":
on_remote_numpad(7)
elif key_name == "KEY_8":
on_remote_numpad(8)
elif key_name == "KEY_9":
on_remote_numpad(9)
elif key_name == "KEY_0":
on_remote_numpad(0)
def channel_off(): print("cablebox Is Off")
def select_channel(channel): with open("/mnt/nvme/code/FieldStation42/runtime/channel.socket", "w") as file: file.write(channel)
def channel_1(): print("Displaying content for Channel 1") select_channel("channel_1")
def channel_2(): print("Displaying content for Channel 2") select_channel("channel_2")
def channel_3(): print("Displaying content for Channel 3") select_channel("channel_3")
def channel_4(): print("Displaying content for Channel 4") select_channel("channel_4")
def channel_5(): print("Displaying content for Channel 5")
def channel_6(): print("Displaying content for Channel 6")
def channel_7(): print("Displaying content for Channel 7")
def channel_8(): print("Displaying content for Channel 8")
def channel_9(): print("Displaying content for Channel 9")
def channel_10(): print("Displaying content for Channel 10")
def channel_11(): print("Displaying content for Channel 11")
def channel_12(): print("Displaying content for Channel 12")
def channel_13(): print("Displaying content for Channel 13")
def channel_14(): print("Displaying content for Channel 14")
def channel_15(): print("Displaying content for Channel 15")
def channel_16(): print("Displaying content for Channel 16")
def channel_17(): print("Displaying content for Channel 17")
def channel_18(): print("Displaying content for Channel 18")
def channel_19(): print("Displaying content for Channel 19")
def channel_20(): print("Displaying content for Channel 20")
def channel_21(): print("Displaying content for Channel 21")
def channel_22(): print("Displaying content for Channel 22")
def channel_23(): print("Displaying content for Channel 23")
def channel_24(): print("Displaying content for Channel 24")
channel_functions = { 1: channel_1, 2: channel_2, 3: channel_3, 4: channel_4, 5: channel_5, 6: channel_6, 7: channel_7, 8: channel_8, 9: channel_9, 10: channel_10, 11: channel_11, 12: channel_12, 13: channel_13, 14: channel_14, 15: channel_15, 16: channel_16, 17: channel_17, 18: channel_18, 19: channel_19, 20: channel_20, 21: channel_21, 22: channel_22, 23: channel_23, 24: channel_24, }
remote_thread = Thread(target=listen_for_remote_input) remote_thread.daemon = True remote_thread.start()
button_ch_plus.when_pressed = increment_channels button_ch_minus.when_pressed = decrement_channels button_pwr.when_pressed = toggle_on_off
print("Cablebox Script is Running...") update_cablebox_status()
pause()
The script is failing during playback. I has tons of videos of TV series, bumpers and commercials in different lengths. After running for a 30 minutes or so, I get this error and the script crashes:
Traceback (most recent call last): File "/mnt/nvme/code/FieldStation42/field_player.py", line 268, in main_loop() File "/mnt/nvme/code/FieldStation42/field_player.py", line 232, in main_loop outcome = player.play_slot(week_day, hour, skip, runtime_path=station_runtimes[channel]) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/mnt/nvme/code/FieldStation42/field_player.py", line 90, in play_slot return self.start_playing(offset) ^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/mnt/nvme/code/FieldStation42/field_player.py", line 103, in start_playing (index, offset) = self._find_index_at_offset(block_offset) ^^^^^^^^^^^^^^^ TypeError: cannot unpack non-iterable NoneType object