shane-mason / FieldStation42

Broadcast TV simulator
Mozilla Public License 2.0
87 stars 7 forks source link

Script is failing during playback #24

Open manodory opened 2 days ago

manodory commented 2 days ago

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

shane-mason commented 1 day 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.

shane-mason commented 1 day ago

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.

manodory commented 1 day ago

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.

shane-mason commented 1 day ago

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.

manodory commented 1 day ago

Is there anyway I can communicate with you directly? My email is manodorygmail.com.

numindast commented 1 day ago

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 :)

shane-mason commented 1 day ago

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.

shane-mason commented 21 hours ago

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.

manodory commented 17 hours ago

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

Define GPIO pins for segments of seven segment display

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]

Initialize LEDs for segments

left_leds = [LED(pin) for pin in left_segment_pins] right_leds = [LED(pin) for pin in right_segment_pins]

Define pins for decimal dots

decimal_dots = [LED(12), LED(16)]

Buttons on device

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()

Function to handle remote input with numpad

def handle_digit_input(digit): global first_digit, count if first_digit is None:

First digit input

    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)

Channel display functions for each channel (1-24)

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)

Define individual functions for each 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")

Map each channel to its specific function

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, }

Start the IR listening in a separate thread

remote_thread = Thread(target=listen_for_remote_input) remote_thread.daemon = True remote_thread.start()

Button press events

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()