#!/usr/bin/env python3 import threading import time import pychromecast import wx import wx.lib.scrolledpanel as scrolled import wx.media from typing import Callable from Channel import SVT, YouTube from ChannelProvider import ChannelProvider from Utils import make_sized_button WIDTH = 720 / 2 HEIGHT = 1440 / 2 BTN_HEIGHT = 40 SPACER_HEIGHT = 10 SCROLL_RATE = 5 BM_BTN_STYLE = wx.BOTTOM | wx.EXPAND | wx.LEFT | wx.TOP BTN_STYLE = wx.BORDER_NONE | wx.BU_AUTODRAW | wx.BU_EXACTFIT | wx.BU_NOTEXT class Cast(wx.Frame): def __init__(self, *args, **kw): """__init__. :param args: :param kw: """ super().__init__(*args, **kw) self.m_selected_chromecast = None self.SetSizeHints(WIDTH, HEIGHT, maxW=WIDTH) self.m_index = 0 self.m_chromecast_thr = threading.Thread( target=self.get_chromecasts, args=(), kwargs={} ) self.m_chromecast_thr.start() self.m_sizer: wx.Sizer = wx.BoxSizer(wx.VERTICAL) self.m_panel: wx.lib.scrolledpanel.ScrolledPanel = scrolled.ScrolledPanel( # type: ignore self, -1, style=wx.VSCROLL ) self.m_control = None self.m_panel.SetupScrolling(rate_y=SCROLL_RATE,scrollToTop=True) self.m_panel.SetSizer(self.m_sizer) self.m_providers: list[ChannelProvider] = [ ChannelProvider( "SVT", channels=[ SVT.SVT("feed"), SVT.SVT("ch-svt1"), SVT.SVT("ch-svt2"), SVT.SVT("ch-svt24"), ], ), ChannelProvider( "YouTube", channels=[ YouTube.YouTube("UCtESv1e7ntJaLJYKIO1FoYw"), YouTube.YouTube("UC9-y-6csu5WGm29I7JiwpnA"), YouTube.YouTube("UCoxcjq-8xIDTYp3uz647V5A"), YouTube.YouTube("UCu6mSoMNzHQiBIOCkHUa2Aw"), ], ), ] self.m_selected_channel = None self.m_selected_provider_index = None self, self.show_provider_list(None) def add_back_button(self, callback: Callable) -> None: backbtn = wx.Button(self.m_panel, -1, label="Go back", size=(WIDTH, BTN_HEIGHT)) backbtn.Bind( wx.EVT_BUTTON, callback ) self.m_sizer.Add(backbtn) def get_chromecasts(self) -> None: """ [TODO:description] :rtype None: [TODO:description] """ self.m_chromecasts, self.m_browser = pychromecast.get_chromecasts() def show_provider_list(self, _) -> None: self.m_sizer.Clear(delete_windows=True) self.m_sizer = wx.BoxSizer(wx.VERTICAL) self.m_sizer.AddSpacer(SPACER_HEIGHT * 4) closebtn = wx.Button(self.m_panel, -1, label="Close", size=(WIDTH, BTN_HEIGHT)) closebtn.Bind(wx.EVT_BUTTON, lambda event: self.Destroy()) self.m_sizer.Add(closebtn) provider_index = 0 for provider in self.m_providers: bitmap = provider.get_logo_as_bitmap() callback = lambda event, index=provider_index: self.show_channel_list(event, index) btn_sizer: wx.BoxSizer = make_sized_button(self.m_panel,bitmap,provider.get_name(),callback) self.m_sizer.Add(btn_sizer) provider_index += 1 self.m_panel.SetupScrolling(rate_y=SCROLL_RATE,scrollToTop=True) self.m_panel.SetSizer(self.m_sizer) self.m_sizer.Fit(self) self.m_sizer.Layout() def show_channel_list(self, _, provider_index) -> None: self.m_selected_provider_index = provider_index self.m_selected_provider = self.m_providers[provider_index] self.m_sizer.Clear(delete_windows=True) self.m_sizer = wx.BoxSizer(wx.VERTICAL) self.m_sizer.AddSpacer(SPACER_HEIGHT * 4) bck_callback = lambda event: self.show_provider_list(event) self.add_back_button(bck_callback) channel_index = 0 for channel in self.m_selected_provider.get_channels(): bitmap = channel.get_logo_as_bitmap() callback = lambda event, index=channel_index: self.show_video_list(event, index) btn_sizer: wx.BoxSizer = make_sized_button(self.m_panel,bitmap,channel.get_name(),callback) self.m_sizer.Add(btn_sizer) channel_index += 1 self.m_panel.SetupScrolling(rate_y=SCROLL_RATE,scrollToTop=True) self.m_panel.SetSizer(self.m_sizer) self.m_sizer.Fit(self) self.m_sizer.Layout() def show_video_list(self, _, index=0) -> None: self.m_sizer.Clear(delete_windows=True) self.m_sizer = wx.BoxSizer(wx.VERTICAL) self.m_sizer.AddSpacer(SPACER_HEIGHT * 4) channel = self.m_selected_provider.get_channel_by_index(index) if channel.wait(): with wx.BusyInfo("Please wait, working..."): index = 0 while channel.wait(): time.sleep(1) wx.GetApp().Yield() callback = lambda event: self.show_channel_list(event, self.m_selected_provider_index) self.add_back_button(callback) for item in channel.get_items(): # type: ignore inner_sizer = wx.BoxSizer(wx.VERTICAL) title = wx.StaticText(self.m_panel, -1) title.SetLabelMarkup("{}".format(item["title"])) description = wx.StaticText(self.m_panel, -1, item["description"]) description.Wrap(WIDTH -2) bitmap = item["thumbnail"] btn = wx.BitmapButton(self.m_panel, id=self.m_index, bitmap=bitmap, style= BM_BTN_STYLE) btn.Bind( wx.EVT_BUTTON, lambda event, link=item["link"], provider_index=index: self.show_player( event, link, provider_index ), ) inner_sizer.Add(title) inner_sizer.Add(btn) inner_sizer.Add(description) self.m_sizer.Add(inner_sizer) self.m_sizer.AddSpacer(SPACER_HEIGHT) self.m_index = self.m_index + 1 self.m_panel.SetupScrolling(rate_y=SCROLL_RATE,scrollToTop=True) self.m_panel.SetSizer(self.m_sizer) self.m_sizer.Fit(self) self.m_sizer.Layout() def show_player(self, _, uri, provider_index: int): """ Show the video player :param _ event: unused :param uri str: the link to the video stream """ self.m_sizer.Clear(delete_windows=True) self.m_sizer = wx.BoxSizer(wx.VERTICAL) self.m_sizer.AddSpacer(SPACER_HEIGHT * 4) inner_sizer = wx.GridBagSizer() self.m_control = wx.media.MediaCtrl( self.m_panel, # size=(WIDTH, HEIGHT/2), # style=wx.SIMPLE_BORDER, szBackend=wx.media.MEDIABACKEND_GSTREAMER, ) play_button = wx.Button(self.m_panel, -1, "Play") pause_button = wx.Button(self.m_panel, -1, "Pause") back_button = wx.Button(self.m_panel, -1, "Back") back_button.Bind( wx.EVT_BUTTON, lambda event, index=provider_index: self.show_video_list(event, index), ) inner_sizer.Add(self.m_control, (0, 0)) inner_sizer.SetItemSpan(0, (0, 6)) inner_sizer.Add(play_button, (1, 1)) inner_sizer.Add(pause_button, (1, 2)) inner_sizer.Add(back_button, (1, 3)) if not self.m_chromecast_thr.is_alive() and not self.m_selected_chromecast: chromecast_button = wx.Button(self.m_panel, -1, "Cast") chromecast_button.Bind( wx.EVT_BUTTON, lambda event, muri=uri, index=provider_index: self.select_chromecast( event, muri, index ), ) inner_sizer.Add(chromecast_button, (2, 2)) self.m_sizer.Add(inner_sizer) if self.m_selected_chromecast: self.Bind( wx.media.EVT_MEDIA_LOADED, lambda event, muri=uri: self.cast(event, muri), ) play_button.Bind(wx.EVT_BUTTON, self.play_cast) pause_button.Bind(wx.EVT_BUTTON, self.pause_cast) else: self.Bind(wx.media.EVT_MEDIA_LOADED, self.play) play_button.Bind(wx.EVT_BUTTON, self.play) pause_button.Bind(wx.EVT_BUTTON, self.pause) self.Bind(wx.media.EVT_MEDIA_FINISHED, self.show_video_list) self.load_uri(uri) self.m_panel.SetupScrolling(rate_y=SCROLL_RATE,scrollToTop=True) self.m_panel.SetSizer(self.m_sizer) self.m_sizer.Fit(self) self.m_sizer.Layout() self.m_panel.ScrollChildIntoView(self.m_control) def select_chromecast(self, _, uri, provider_index): self.m_sizer.Clear(delete_windows=True) self.m_sizer = wx.BoxSizer(wx.VERTICAL) self.m_sizer.AddSpacer(SPACER_HEIGHT * 4) cancel_btn = wx.Button(self.m_panel, -1, "Cancel", size=(WIDTH, BTN_HEIGHT), style=wx.BU_EXACTFIT) cancel_btn.Bind( wx.EVT_BUTTON, lambda event, index=provider_index: self.show_video_list(event, index), ) self.m_sizer.Add(cancel_btn) #, wx.ALIGN_CENTER_VERTICAL) for cast in self.m_chromecasts: friendly_name = cast.cast_info.friendly_name btn = wx.Button(self.m_panel, id=-1, label=friendly_name, size=(WIDTH, BTN_HEIGHT)) btn.Bind( wx.EVT_BUTTON, lambda event, chromecast=cast, muri=uri, index=provider_index: self.set_chromecast( event, chromecast, muri, index ), ) self.m_sizer.Add(btn)#, wx.ALIGN_CENTER_VERTICAL) self.m_panel.SetupScrolling(rate_y=SCROLL_RATE,scrollToTop=True) self.m_panel.SetSizer(self.m_sizer) self.m_sizer.Fit(self) self.m_sizer.Layout() def set_chromecast(self, event, chromecast, uri, provider_index): self.m_selected_chromecast = chromecast self.show_player(event, uri, provider_index) def cast(self, _, uri): mimetype = "video/mp4" cast = self.m_selected_chromecast cast.wait() cast.media_controller.play_media(uri, mimetype) # Wait for player_state PLAYING player_state = None has_played = False while True: if player_state != cast.media_controller.status.player_state: player_state = cast.media_controller.status.player_state print("Player state:", player_state) if player_state == "PLAYING": has_played = True break if ( cast.socket_client.is_connected and has_played and player_state != "PLAYING" ): has_played = False cast.media_controller.play_media(uri, mimetype) time.sleep(0.1) self.m_browser.stop_discovery() def pause_cast(self, event): cast = self.m_selected_chromecast cast.media_controller.pause() def play_cast(self, event): cast = self.m_selected_chromecast cast.media_controller.play() def load_uri(self, uri): self.m_control.LoadURI(uri) def play(self, _): self.m_control.Play() def pause(self, _): self.m_control.Pause() def quit(self, _): self.Destroy() if __name__ == "__main__": # When this module is run (not imported) then create the app, the # frame, show it, and start the event loop. app: wx.App = wx.App() frm: Cast = Cast(None, title="Cast") frm.Show() app.MainLoop()