You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cast/scripts/cast

497 lines
18 KiB

#!/usr/bin/env python3
import sys
import threading
import time
import os
from typing import Callable
import pychromecast
import wx
import wx.lib.scrolledpanel as scrolled
import wx.media
from vlc import Instance
from Channel import SVT
from ChannelProvider import ChannelProvider
from Utils import (get_all_svt_categories, make_bitmap_from_file,
make_sized_button, BASEPATH)
WIDTH = int(720 / 2)
HEIGHT = int(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
FLAGS = wx.SizerFlags()
FLAGS.Center()
class Cast(wx.Frame):
def __init__(self, *args, **kw):
"""__init__.
:param args:
:param kw:
"""
self.m_mode = 'normal'
self.asset_path: str = '/usr/share/cast'
url = None
if "url" in kw:
self.m_mode = 'single'
url = kw['url']
del kw['url']
super().__init__(*args, **kw)
if not os.path.isdir(BASEPATH):
os.mkdir(BASEPATH)
self.m_selected_chromecast = None
self.SetSizeHints(WIDTH, HEIGHT, maxW=WIDTH)
self.m_style = self.GetWindowStyle()
self.m_chromecast_thr = threading.Thread(target=self.get_chromecasts,
args=(),
kwargs={})
self.m_vlc = Instance()
self.m_vlc_listplayer = self.m_vlc.media_list_player_new()
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_panel.SetupScrolling(rate_y=SCROLL_RATE, scrollToTop=True)
self.m_panel.SetSizer(self.m_sizer)
self.m_providers: list[ChannelProvider] = self.get_providers()
if url:
self.show_player(None, url, 0)
elif len(self.m_providers) > 1:
self.show_provider_list(None)
else:
self.show_channel_list(None, 0)
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 get_player_controls(self, channel_index: int, uri: str) -> wx.BoxSizer:
outer_sizer = wx.BoxSizer(wx.VERTICAL)
inner_sizer = wx.BoxSizer(wx.HORIZONTAL)
play_button = wx.Button(self.m_panel,
-1,
"Play",
size=(WIDTH / 4, BTN_HEIGHT))
pause_button = wx.Button(self.m_panel,
-1,
"Pause",
size=(WIDTH / 4, BTN_HEIGHT))
stop_button = wx.Button(self.m_panel,
-1,
"Stop",
size=(WIDTH / 4, BTN_HEIGHT))
if self.m_mode == 'normal':
back_button = wx.Button(self.m_panel,
-1,
"Go Back",
size=(WIDTH, BTN_HEIGHT))
back_button.Bind(
wx.EVT_BUTTON,
lambda event, cindex=channel_index: self.show_video_list(
event, cindex),
)
else:
back_button = wx.Button(self.m_panel,
-1,
label="Close",
size=(WIDTH, BTN_HEIGHT))
back_button.Bind(wx.EVT_BUTTON, lambda event: self.Destroy())
outer_sizer.Add(back_button, FLAGS)
inner_sizer.Add(play_button, FLAGS)
inner_sizer.Add(pause_button, FLAGS)
inner_sizer.Add(stop_button, FLAGS)
if self.has_usable_chromecasts():
btm = make_bitmap_from_file(
'{}/assets/Cast.png'.format(self.asset_path), wx.Size(24, 24))
cast_button = wx.BitmapButton(self.m_panel,
-1,
bitmap=btm,
size=(WIDTH / 4, BTN_HEIGHT))
cast_button.Bind(
wx.EVT_BUTTON,
lambda event, muri=uri, cindex=channel_index: self.
select_chromecast(event, muri, cindex),
)
inner_sizer.Add(cast_button, FLAGS)
if self.m_selected_chromecast:
self.cast(wx.media.EVT_MEDIA_LOADED, uri),
play_button.Bind(wx.EVT_BUTTON, self.play_cast)
pause_button.Bind(wx.EVT_BUTTON, self.pause_cast)
stop_button.Bind(
wx.EVT_BUTTON,
lambda event, muri=uri, cindex=channel_index: self.
stop_callback(event, muri, cindex),
)
else:
self.Bind(wx.media.EVT_MEDIA_LOADED,
lambda event: wx.Frame.SetFocus(self))
play_button.Bind(wx.EVT_BUTTON, self.play)
pause_button.Bind(wx.EVT_BUTTON, self.pause)
stop_button.Bind(wx.EVT_BUTTON, self.stop)
outer_sizer.Add(inner_sizer, FLAGS)
inner_sizer.Fit(self)
outer_sizer.Fit(self)
outer_sizer.Layout()
return outer_sizer
def get_providers(self) -> list[ChannelProvider]:
providers = list()
chandict = {
"kanaler": {
"channels": [
"ch-svt1", "ch-svt2", "ch-svt24", "ch-barnkanalen",
"ch-kunskapskanalen"
],
"displayname":
"SVT Channel Streams"
}
}
chandict["program"] = {
"channels": ["feed", "allprograms"],
"displayname": "SVT Shows - Latest and A-Ö"
}
categories = list()
for category in get_all_svt_categories():
categories.append(category['id'])
chandict["kategorier"] = {
"channels": categories,
"displayname": "SVT All Categories"
}
for provider in ["kanaler", "program", "kategorier"]:
channels = list()
for id in chandict[provider]["channels"]:
channels.append(SVT.SVT(id))
svt = ChannelProvider(chandict[provider]["displayname"],
channels=channels)
providers.append(svt)
return providers
def has_usable_chromecasts(self) -> bool:
if self.m_chromecast_thr.is_alive():
return False
if self.m_selected_chromecast:
return False
result = False
for cast in self.m_chromecasts:
if cast.cast_type != 'audio':
result = True
break
return result
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)
if len(self.m_providers) > 1:
back_callback = lambda event: self.show_provider_list(event)
self.add_back_button(back_callback)
else:
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, 0, wx.ALL, 1)
channel_index = 0
for channel in self.m_selected_provider.get_channels():
bitmap = channel.get_logo_as_bitmap()
callback = lambda event, cindex=channel_index: self.show_video_list(
event, cindex)
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_provider_list(self, _) -> None:
self.m_sizer.Clear(delete_windows=True)
self.m_sizer = wx.BoxSizer(wx.VERTICAL)
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, 0, wx.ALL, 1)
provider_index = 0
for provider in self.m_providers:
bitmap = provider.get_logo_as_bitmap()
callback = lambda event, pindex=provider_index: self.show_channel_list(
event, pindex)
btn_sizer: wx.BoxSizer = make_sized_button(self.m_panel, bitmap,
provider.get_name(),
callback)
self.m_sizer.Add(btn_sizer, 0, wx.ALL, 1)
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_video_list(self, _, channel_index) -> None:
self.SetWindowStyle(self.m_style)
self.m_selected_channel = self.m_selected_provider.get_channel_by_index(
channel_index)
self.m_sizer.Clear(delete_windows=True)
self.m_sizer = wx.BoxSizer(wx.VERTICAL)
back_callback = lambda event: self.show_channel_list(
event, self.m_selected_provider_index)
self.add_back_button(back_callback)
if self.m_selected_provider.get_name().startswith(
'SVT') and self.m_selected_channel.get_id() not in [
"ch-svt1", "ch-svt2", "ch-svt24", "ch-barnkanalen",
"ch-kunskapskanalen"
]:
def refresh_callback(event):
self.m_selected_channel.refresh()
self.show_video_list(event, channel_index)
refreshbtn = wx.Button(self.m_panel,
-1,
label="Refresh",
size=(WIDTH, BTN_HEIGHT))
refreshbtn.Bind(wx.EVT_BUTTON, refresh_callback)
self.m_sizer.Add(refreshbtn)
if self.m_selected_channel.wait():
with wx.BusyInfo("Please wait, working..."):
number_of_waits = 0
while self.m_selected_channel.wait():
number_of_waits += 1
time.sleep(1)
wx.GetApp().Yield()
if number_of_waits > 10:
break
btnindex = 0
for item in self.m_selected_channel.get_items(): # type: ignore
inner_sizer = wx.BoxSizer(wx.VERTICAL)
pane_sizer = wx.BoxSizer(wx.VERTICAL)
title = wx.StaticText(self.m_panel, -1)
title.SetLabelMarkup("<span weight='bold' >{}</span>".format(
item["title"]))
bitmap = item["thumbnail"]
btn = wx.BitmapButton(self.m_panel,
id=btnindex,
bitmap=bitmap,
style=BM_BTN_STYLE,
size=bitmap.GetSize())
btn.Bind(
wx.EVT_BUTTON,
lambda event, link=item["link"], cindex=channel_index: self.
show_player(event, link, cindex),
)
inner_sizer.Add(title)
inner_sizer.Add(btn)
collapsable_pane = wx.CollapsiblePane(self.m_panel,
wx.ID_ANY,
"Details:",
size=(WIDTH, 20))
inner_sizer.Add(collapsable_pane, 0, wx.GROW | wx.ALL, 5)
pane_win = collapsable_pane.GetPane()
description = wx.StaticText(pane_win, -1, item["description"])
description.Wrap(WIDTH - 2)
pane_sizer.Add(description, wx.GROW | wx.ALL, 2)
pane_win.SetSizer(pane_sizer)
pane_sizer.SetSizeHints(pane_win)
def fit_and_layout(_):
pane_sizer.Layout()
pane_sizer.Fit(pane_win)
inner_sizer.Layout()
inner_sizer.Fit(self)
self.m_sizer.Fit(self)
self.m_sizer.Layout()
collapsable_pane.Bind(wx.EVT_COLLAPSIBLEPANE_CHANGED,
lambda event: fit_and_layout(event))
#inner_sizer.Add(description)
self.m_sizer.Add(inner_sizer)
self.m_sizer.AddSpacer(SPACER_HEIGHT)
btnindex = btnindex + 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: str, channel_index: int):
"""
Show the video player
:param _ event: unused
:param uri str: the link to the video stream
"""
media = self.m_vlc.media_new(uri)
medialist = self.m_vlc.media_list_new()
medialist.add_media(media)
self.m_vlc_listplayer.set_media_list(medialist)
self.m_sizer.Clear(delete_windows=True)
self.m_sizer = wx.BoxSizer(wx.VERTICAL)
self.m_sizer.Add(self.get_player_controls(channel_index, uri), FLAGS)
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 select_chromecast(self, _, uri, channel_index):
self.m_sizer.Clear(delete_windows=True)
self.m_sizer = wx.BoxSizer(wx.VERTICAL)
cancel_btn = wx.Button(self.m_panel,
-1,
"Cancel",
size=(WIDTH, BTN_HEIGHT))
cancel_btn.Bind(
wx.EVT_BUTTON,
lambda event, cindex=channel_index: self.show_video_list(
event, cindex),
)
self.m_sizer.Add(cancel_btn)
for cast in self.m_chromecasts:
if cast.cast_type == 'audio':
continue
friendly_name = "Unknown Chromecast"
try:
friendly_name = cast.device.friendly_name
except AttributeError:
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, cindex=channel_index:
self.set_chromecast(event, chromecast, muri, cindex),
)
self.m_sizer.Add(btn)
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, channel_index):
self.m_selected_chromecast = chromecast
self.show_player(event, uri, channel_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
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.wait()
cast.media_controller.pause()
def play_cast(self, event):
cast = self.m_selected_chromecast
cast.wait()
cast.media_controller.play()
def stop_cast(self, event):
cast = self.m_selected_chromecast
cast.wait()
cast.media_controller.pause()
time.sleep(2)
cast.media_controller.stop()
self.m_selected_chromecast = None
def stop_callback(self, event, uri: str, channel_index: int):
self.stop_cast(event)
self.m_sizer.Clear(delete_windows=True)
self.m_sizer = wx.BoxSizer(wx.VERTICAL)
self.m_sizer.Add(self.get_player_controls(channel_index, 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()
def play(self, _):
self.m_vlc_listplayer.play()
def pause(self, _):
self.m_vlc_listplayer.pause()
def stop(self, _):
self.m_vlc_listplayer.stop()
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.
url = None
app: wx.App = wx.App()
if len(sys.argv) > 1:
url = sys.argv[1]
frm: Cast = Cast(None, title="Cast", url=url)
else:
frm: Cast = Cast(None, title="Cast")
frm.Show()
app.MainLoop()