#!/usr/bin/python3 ''' SVPlayer - simple video and TV player (c) 2011-2024 Jan ONDREJ (SAL) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Usage: svplayer [options] [URL] Options: -h|--help show this help message -f|--fullscreen start fullscreen -p|--profile X start with profile X -c|--channel X switch to channel X after startup ''' import sys, os, re, pidfile, time, getopt, atexit, json import dbus from dbus.mainloop.glib import DBusGMainLoop from datetime import datetime from config import * from sgtk import * import players from browser import file_browser, set_menu, iptv_date_menu, settings_menu, \ guide_menu import mobile.server, select try: import liblirc except ImportError: liblirc = None try: import tvguide except ImportError: pass reg_geometry = re.compile(r'^([0-9]*)x?([0-9]*)([+-][0-9]*)?([+-][0-9]*)?$') class ProgressBar(Gtk.DrawingArea): __gtype_name__ = "ProgressBar" def __init__(self): super().__init__() self.timeout = None self.sy = 0 self.set_draw_func(self.redraw, None) def set_value(self, name, value): self.label = name self.value = value if self.timeout is not None: GObject.source_remove(self.timeout) self.sy = 8 # refresh self.set_visible(False) self.set_visible(True) def clear(self, cr): # Fill the background with black# cr.set_source_rgb(0, 0, 0) cr.rectangle(0, 0, self.get_width(), self.get_height()) cr.fill() def draw(self, cr): self.clear(cr) sx = self.get_width() sy = self.sy self.set_size_request(sx, sy) self.windowed_size = (sx, sy) if sy>4: cr.set_source_rgb(0, 0.9, 0) x0 = (sx - sx/6*6)/2 + 1 w = sx*int(self.value)/100 for x in range(min(int(w/6+1), int(sx/6))): xx = x0 + x*6 cr.rectangle(xx, 2, 3, sy-4) cr.fill() cr.set_source_rgb(0, 0, 0) cr.rectangle( x0 + w, 0, sx - x0 - w, sy-1 ) cr.fill() def redraw(self, area, cr, x, y, data): self.draw(cr) def ahide(self): '''Animated hide.''' # Animated hide blinks screen if player width is smaller than # window width. Disabled until it will be fixed. return self.set_visible(False) self.sy -= 2 wx = self.get_root().get_size()[0] px = self._parent.player.get_size_request()[0] if self.sy>0: self.timeout = GLib.timeout_add(40, self.ahide) else: self.timeout = None self.set_visible(False) SVPLAYER_CSS_DATA = """\ #label_center { color: yellow; font-family: sans; font-size: 20pt; } #label_right, #label_left { color: yellow; font-family: sans; font-size: 24pt; } #picture { /* margin-top: auto; margin-bottom: auto; vertical-align: middle; margin-top: 50px; margin-bottom: 50px; */ } #browser { } /* file browser */ GtkTreeView { background-color: green; } GtkTreeView row:nth-child(even) { background-color: #ffffff; } GtkTreeView row:nth-child(odd) { background-color: #f0f0f0; } GtkTreeView row:selected { background-color: #4a90d9; } """ ALLOW_DELETE = False APP_TEMPLATE = '''\ File app.submenu_action Open app.file_open Browse app.file_browser About app.about Quit app.quit Profile Source Audio channel Subtitles Engine GStreamer VAAPI app.engine_gstreamer_vaapi GStreamer app.engine_gstreamer VLC app.engine_vlc ''' @Gtk.Template(string=APP_TEMPLATE) class MainWindow(Gtk.ApplicationWindow): __gtype_name__ = 'MainWindow' header_bar = Gtk.Template.Child() file_menu = Gtk.Template.Child() file_menu_model = Gtk.Template.Child() channel_menu_model = Gtk.Template.Child() profile_submenu = Gtk.Template.Child() source_submenu = Gtk.Template.Child() audio_submenu = Gtk.Template.Child() subtitles_submenu = Gtk.Template.Child() vbox = Gtk.Template.Child() hbox = Gtk.Template.Child() label_left = Gtk.Template.Child() label_center = Gtk.Template.Child() entry = Gtk.Template.Child() label_right = Gtk.Template.Child() progress_bar = Gtk.Template.Child() browser = Gtk.Template.Child() scrolled_window = Gtk.Template.Child() treeview = Gtk.Template.Child() cell0 = Gtk.Template.Child() cell1 = Gtk.Template.Child() def wait_for_refresh(self): ctx = GLib.main_context_default() while ctx.pending(): ctx.iteration(False) for i in range(5000): ctx.iteration(False) #@Gtk.Template.Callback() #def file_menu_update(self, *args): # print("FILE MENU CLICK") class SVPlayerApplication(Adw.Application): changing_channel = 0 channel = 0 last_channel = None last_motion = 0 last_cursor_position = (-10, -10) last_url = None status_timeout = None commit_channel_timeout = None geometries = geometries status_next_update = 0 last_message = "" last_pos = None content_length = None next_screensaver_poke = 0 file_browser = None pressed_keys = [] def __init__(self, urls, default_channel='', default_volume=50, default_size=None, default_position=None, fullscreen=False, profile=0, web_remote_control=None): app_id = "sk.salstar.SVPlayer" if pidfile.multi: app_id = app_id+"-multi-"+pidfile.pid super().__init__(application_id=app_id) self.profile = profile if profile>0: self.urls = PROFILES[profile] else: self.urls = urls self.source = 0 self.autoswitch_source = True self.volume = default_volume self.fullscreen = fullscreen self.default_channel = default_channel self.windowed_size = default_size if web_remote_control: #mobile.server.handler_class.CHANNELS = dict(enumerate([ # x.name # for x in urls #])) mobile.server.handler_class.player = self try: self.web_remote = mobile.server.server_class( web_remote_control, mobile.server.handler_class ) except socket.error as error: print("Unable to start web remote server", str(error)) self.web_remote = None else: self.web_remote = None self.connect("activate", self.on_activate) def configure_action(self, name, fx, *args): action = Gio.SimpleAction(name=name) action.connect("activate", fx, *args) self.add_action(action) def add_menu_item(self, menu_model, name, action_name, fx, *args): menu_model.append_item( Gio.MenuItem.new(name, "app."+action_name) ) self.configure_action(action_name, fx, *args) #print("MENU:", action_name, name) def update_menu(self, *args): # remove all entries self.window.profile_submenu.remove_all() self.window.channel_menu_model.remove_all() self.window.source_submenu.remove_all() self.window.audio_submenu.remove_all() self.window.subtitles_submenu.remove_all() # add new entries for profile_id, profile_name in enumerate(PROFILES.keys(), start=1): self.add_menu_item( self.window.profile_submenu, f"{profile_id}. {profile_name}", f"set_profile_{profile_id}", lambda action, _, profile: self.change_profile(profile_id), profile_id ) for channel, url in enumerate(self.urls, start=1): self.add_menu_item( self.window.channel_menu_model, f"{channel}. {url.name}", f"set_channel_{channel}", lambda action, _, channel: self.change_channel(channel=channel), channel ) url = self.urls[self.channel-1] for src_id in range(url.source_count()): if not url(src_id).archive_only and not url(src_id).guide_only: self.add_menu_item( self.window.source_submenu, f"{src_id}. {url(src_id).source} {url(src_id).name}", f"set_source_{src_id}", self.change_source, src_id ) tracks = sorted( enumerate(self.player.audio_tracks), key=lambda x: x[1][0] ) for i, (n, track) in tracks: if n==-1: continue self.add_menu_item( self.window.audio_submenu, f"{i}. {n} {track}", f"set_audio_{i}", lambda action, _, track: self.change_track(value=track), i ) for i, (subid, subdesc) in enumerate(self.player.sub_list()): self.add_menu_item( self.window.subtitles_submenu, f"{i}. {subid} {subdesc}", f"set_subtitle_{i}", lambda action, _, subtitle: self.change_subtitle(subtitle) ) def on_activate(self, app): self.window = MainWindow(application=app) css_provider = Gtk.CssProvider() css_provider.load_from_data(SVPLAYER_CSS_DATA) Gtk.StyleContext.add_provider_for_display( Gdk.Display.get_default(), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ) self.window.connect("destroy", self.destroy) event_key = Gtk.EventControllerKey.new() event_key.connect("key-pressed", self.keyboard_press) event_key.connect("key-released", self.keyboard) #event_key.connect("modifiers", self.keyboard_modifier) self.window.add_controller(event_key) event_focus = Gtk.EventControllerFocus.new() event_focus.connect("enter", self.focus_event) event_focus.connect("leave", self.focus_event) self.window.add_controller(event_focus) event_click = Gtk.GestureClick.new() event_click.set_button(0) event_click.connect("pressed", self.mouse_press) event_click.connect("released", self.mouse_release) self.window.add_controller(event_click) event_scroll = Gtk.EventControllerScroll.new( Gtk.EventControllerScrollFlags.VERTICAL ) event_scroll.connect("scroll", self.mouse_scroll) self.window.add_controller(event_scroll) event_motion = Gtk.EventControllerMotion.new() event_motion.connect("motion", self.mouse_motion) self.window.add_controller(event_motion) ###self.drag_init(self.window) self.last_motion = time.time() # info row self.window.progress_bar._parent = self # Video player widget self.player = players.BasePlayerWidget() self.audio_player = players.BasePlayerWidget() self.button_press = None self.window.vbox.set_visible(True) self.window.set_visible(True) # setup menus self.window.set_titlebar(self.window.header_bar) #self.window.file_menu.set_label("File") self.update_menu() self.window.file_menu.get_popover().connect("show", self.update_menu) self.configure_action("file_open", self.file_open) self.configure_action("file_browser", lambda *args: self.keyboard(key="b")) self.configure_action("about", self.show_about) self.configure_action("quit", self.destroy) self.configure_action("engine_gstreamer_vaapi", self.engine_gstreamer_vaapi) self.configure_action("engine_gstreamer", self.engine_gstreamer_no_vaapi) self.configure_action("engine_vlc", self.engine_vlc) # screensaver if screensaver: print("Screensaver configuration:", screensaver) screensaver.startup(self.window) # change fullscreen after screensaver was started self.change_fullscreen(0) # mute on startup self.mute = bool(os.environ.get("MUTE", 0)) self.change_channel( channel=int(self.default_channel), player=players.default ) self.refresh_status() # start status refreshing self.check_lirc() # automatic mute when screen locked DBusGMainLoop(set_as_default=True) bus = dbus.SystemBus() bus.add_signal_receiver( self.screen_locked, 'PropertiesChanged', 'org.freedesktop.DBus.Properties', 'org.freedesktop.login1' # bus name ) def screen_locked(self, bus, msg, *args): if "LockedHint" in msg: if msg["LockedHint"]: self.change_volume(mute=True) else: self.change_volume(mute=False) def destroy(self, *args): if config_file: # save config cdir = os.path.dirname(config_file) if not os.path.isdir(cdir): os.makedirs(cdir) f = open(config_file, "wt") save_parameters = dict([ (x, getattr(self, x)) for x in [ 'channel', 'volume' ] ]) f.write(json.dumps(save_parameters)) f.close() self.player.stop() self.player.destroy() if self.last_url: self.last_url.stop() sys.exit() def file_open(self, *args): def open_done(dialog, result): try: f = dialog.open_finish(result) except GLib.Error as error: print(f"Error opening file: {error.message}") if f is not None: file_path = f.get_path() print("Open file:", file_path) self.play_files([file_path], [file_path], player=players.default) dialog = Gtk.FileDialog.new() dialog.open(self.window, None, open_done) def change_profile(self, profile): if profile<=len(PROFILES): if PROFILES[profile]: self.profile = profile self.urls = PROFILES[profile] self.profile_name = PROFILES.name(profile) before = self.channel self.channel = sys.maxsize if self.urls[0](0).url.lower().endswith('.jpg'): player = players.ImagePlayerWidget else: player = players.default self.change_channel(channel=before, player=player) self.set_status("Profile: %d. %s" % (profile, self.profile_name)) self.update_menu() else: print("Empty profile:", PROFILES.name(profile)) def change_channel(self, direction=0, channel=None, player=None): before = self.channel if channel is not None: self.channel = channel self.channel += direction if self.channel>len(self.urls): self.channel = 1 if self.channel<1: self.channel = len(self.urls) url = self.urls[self.channel-1] print("Channel:", self.channel, url.name) # reset source if channel changed if before!=self.channel: self.source = 0 while True: if self.source<0: self.source = url.source_count()-1 if self.source>=url.source_count(): self.source = 0 break # skip all archive and guide sources if url(self.source).archive_only or url(self.source).guide_only: self.source += 1 else: break if player: surl = url(self.source, player().name) else: surl = url(self.source, self.player.name) print("URL:", surl.url, surl.options) while self.changing_channel>0: time.sleep(0.1) # do nothing if it's same channel as before if before!=self.channel: self.last_channel = before if self.last_url==surl and not url.background_audio: print("Same channel:", self.channel, self.source, surl) else: self.last_url = surl self.last_pos = None self.content_length = None self.last_message = "" # stop player, change media and start again self.changing_channel += 1 self.player.stop() self.audio_player.stop() if self.last_url: self.last_url.stop() if player is not None: print("Player change: %s -> %s" % (self.player, player)) try: self.window.vbox.remove(self.player) self.player.destroy() except TypeError: pass self.player = player( surl.url, subtitles=surl.subtitles, options=surl.options ) self.window.vbox.append(self.player) else: self.player.set_media(surl.url, surl.options) #self.player.show_all() self.player.play() self.changing_channel -= 1 # audio player if url.background_audio: self.audio_player = players.GSTPlayer( url.background_audio, options={'audio_only': True} ) self.audio_player.play() else: self.audio_player = players.BasePlayerWidget() # set parameters self.change_volume() if surl.deinterlace=="DEFAULT": # DEFAULT self.player.set_deinterlace("yadif2x") # replaced bob else: print("Deinterlace:", surl.deinterlace) self.player.set_deinterlace(surl.deinterlace) self.error_counter = error_counter() # new channel set self.channel_req = "" self.onscreen = self.player self.refresh_status(all=True, timeout=0) self.mouse_motion(None, -10, -10) # set language self.player.language_priority(surl.lang_priority) self.audio_track = 1 # track 1, will be changed automatically later self.mute_volume = 0 # hide ui menu def refresh_status(self, all=False, timeout=1000): url = self.urls[self.channel-1] surl = url(self.source) if all or time.time()>self.status_next_update: self.status_next_update = sys.maxsize # disable self.window.label_left.set_text(str(self.channel)) if url==surl: xname = url.name else: xname = (url.name + ' ' + surl.name).strip() self.window.label_center.set_text( f"{xname} [{surl.source}]" ) self.window.set_title( f"{self.channel}. {xname} [{surl.source}]" ) self.window.progress_bar.ahide() self.window.label_right.set_text(time.strftime("%H:%M:%S")) pos = self.player.get_position() if pos[0]>=1 and pos[0]<=99: self.last_pos = pos[0] self.content_length = pos[2] self.window.label_left.set_text("%4.2f" % (self.channel+pos[0]/100.0)) elif url.source=="multi": source = url.find_guide() if source: prog = tvguide.tv_program.current(source.name) if prog and 0.0<=prog.progress()<1.0: self.last_pos = prog.progress()*100 self.content_length = prog.seconds self.window.label_left.set_text("%4.2f" % (self.channel+prog.progress())) # poke (disable) screensaver activation if self.fullscreen and screensaver: if self.next_screensaver_poke5) if self.fullscreen and self.player.handle_events and \ self.last_motion>0 and time.time()-self.last_motion>5: self.window.set_cursor(Gdk.Cursor.new_from_name("none")) # is video still playing? if self.player.was_ended(): if self.urls[self.channel-1].restart: print("Stream ended, restarting ...") self.player.stop() url = self.urls[self.channel-1] surl = url(self.source, self.player.name) self.player.set_media(surl.url, surl.options) self.player.play() elif self.channel0: self.status_timeout = GLib.timeout_add(timeout, self.refresh_status) def set_status(self, text, delay=1.0, large=False): print(text) self.window.set_title(text) self.window.label_center.set_text(text) self.status_next_update = time.time()+delay def change_volume(self, direction=0, volume=None, mute=None): if volume is not None: self.volume = volume if mute is not None: self.mute = mute self.volume += direction if self.volume>self.player.max_volume: self.volume = self.player.max_volume if self.volume<0: self.volume = 0 surl = self.urls[self.channel-1](self.source) self.player.set_volume(int(self.volume*surl.volume)) self.player.set_mute(self.mute) self.audio_player.set_volume(int(self.volume*surl.volume)) self.audio_player.set_mute(self.mute) if self.mute: self.set_status("Mute: on") else: self.set_status("Volume: %d%%" % self.player.get_volume()) if direction!=0 or volume is not None: self.window.progress_bar.set_value( 'Volume', self.player.get_volume()*100/self.player.max_volume ) def change_source(self, action, _, source): self.source = source self.change_channel() def change_track(self, increment=0, value=None): # reload audio tracks if not present if not self.player.audio_tracks: surl = self.urls[self.channel-1](self.source) self.player.get_tracks(surl.lang_priority) if value is None: self.audio_track += increment else: self.audio_track = value if self.audio_track>=len(self.player.audio_tracks): self.audio_track = 0 self.player.set_track(self.player.audio_tracks[self.audio_track][0]) self.set_status( "Language: %s" % self.player.audio_tracks[self.audio_track][1] ) def change_subtitle(self): self.player.next_sub() try: self.set_status( "Sub: %s" % self.player.sub_list()[self.player.get_sub()][1] ) except IndexError: pass def change_position(self, forward=0, seek=None): if seek is None: #print("Forward:", forward) if forward!=0: self.player.forward(forward) else: self.player.seek(seek) # unpause self.player.pause(False) self.audio_player.pause(False) pos = self.player.get_position() # display position only if this stream has non zero length if pos[2]: surl = self.urls[self.channel-1](self.source) if surl.file_size: fsize = f", ({surl.file_size})" else: fsize = "" self.set_status( f"Position: {totime(pos[1])}/{totime(pos[2])}," f" {pos[0]:3.1f}%{fsize}" ) self.window.progress_bar.set_value('Position', int(pos[0])) def change_fullscreen(self, value=None): if value==1: self.fullscreen = not self.fullscreen elif value==None or value==0: pass # no change else: self.fullscreen = value #print("fullscreen=", self.fullscreen) if self.fullscreen: self.window.progress_bar.set_visible(False) self.windowed_size = (self.window.get_width(), self.window.get_height()) self.window.fullscreen() self.window.hbox.set_visible(True) self.window.label_left.set_visible(True) self.window.label_center.set_visible(True) self.window.label_right.set_visible(True) self.window.entry.set_visible(False) if screensaver: screensaver.inhibit() else: self.window.progress_bar.set_visible(False) self.window.hbox.set_visible(False) self.window.wait_for_refresh() self.window.unfullscreen() #if self.windowed_size is not None: # def resizewin(): # ''' Delay window resize due to GNOME 3 problem ''' # print("Resize:", self.windowed_size) # self.window.resize(*self.windowed_size) # GLib.timeout_add(50, resizewin) self.window.set_size_request(*self.windowed_size) if screensaver: screensaver.uninhibit() def change_aspect(self): try: next_aspect = self.geometries.index( (self.player.get_aspect(), self.player.get_crop()))+1 except ValueError as e: print("ValueError:", e) next_aspect = 0 if next_aspect>=len(self.geometries): next_aspect = 0 self.player.set_aspect(self.geometries[next_aspect][0]) self.player.set_crop(self.geometries[next_aspect][1]) self.set_status( "Aspect ratio: %s, Crop: %s" % self.geometries[next_aspect] ) def change_engine(self): if players.default==players.GSTWidget: players.default = players.VLCWidget self.set_status("Engine: VLC") elif players.default==players.VLCWidget: players.default = players.GSTWidget self.set_status("Engine: GStreamer") print("Current engine:", players.default) self.last_url = None self.change_channel(0, player=players.default) def play_files(self, names, filenames, subtitles=[], media=media, player=None, options={}): for name, filename in zip(names, filenames): self.urls.append(media(name, filename)) self.player.stop() if player: try: self.window.vbox.remove(self.player) except TypeError: pass self.player.destroy() if type(filenames[0])==int: self.player = player( options=options or self.urls[-1](0, player().name).options ) else: self.player = player( subtitles=subtitles, options=options or self.urls[-1](0, player().name).options, events=not filenames[0].startswith("dvd://") ) self.window.vbox.append(self.player) self.change_channel(channel=len(self.urls)-len(filenames)+1) # add subtitles for subtitle in subtitles: self.player.add_subtitle(subtitle) if self.file_browser: ###self.window.vbox.remove(self.file_browser.mainbox) self.file_browser.hide() self.onscreen = self.player #self.player.show_all() # hide hbox if not fullscreen if not self.fullscreen: self.window.hbox.set_visible(False) def check_lirc(self): if lirc is not None: events = lirc() else: events = [] if events: for event in events: print("LIRC:", event) if self.keyboard_press(key=event)==False: self.keyboard(key=event) # remove all pressed keys after window lost focus if not self.window.has_focus: self.pressed_keys = [] # hide menu after timeout if there is no mouse motion if self.onscreen==self.player: # check for decode errors self.error_counter.add(self.player.decode_errors()) if self.autoswitch_source \ and self.urls[self.channel-1].source_count()>1: if self.error_counter.sum()>switch_source_errors: print("TOO MANY ERRORS [%d], switching source..." \ % self.error_counter.sum()) self.error_counter.reset() self.source += 1 self.change_channel() elif self.player.read_bytes()==0 and self.player.run_time()>switch_source_no_signal: print("ERROR: NO INPUT, switching source...") print(self.urls[self.channel-1].source_count()) self.source += 1 self.change_channel() # display messages from player or guide msg = self.player.get_message() if msg is None: guide = self.urls[self.channel-1].find_guide() if guide: msg = tvguide.tv_program.current_name(guide.name) if msg and self.last_message!=msg: self.set_status(msg, 2.0, large=True) self.last_message = msg if self.web_remote: try: self.web_remote.handle_request() except select.error: pass GLib.timeout_add(100, self.check_lirc) def commit_channel(self): if not self.channel_req.isdigit(): self.channel_req = "" self.last_message = "" self.change_position() return self.change_channel( channel=int(self.channel_req), player=players.default ) if self.commit_channel_timeout is not None: #GObject.source_remove(self.commit_channel_timeout) GLib.source_remove(self.commit_channel_timeout) self.commit_channel_timeout = None def hide_browser(self): ###self.window.vbox.remove(self.file_browser.mainbox) self.file_browser.hide() self.player.set_visible(True) self.onscreen = self.player def focus_event(self, event=None, *args): print("FOCUS EVENT") self.pressed_keys = [] return True def keyboard_press(self, event=None, keyval=None, keycode=None, state=None, key=None): #print("KEY PRESS:", event, keyval, keycode, state) if key is None: key = Gdk.keyval_name(keyval) if not key in self.pressed_keys: self.pressed_keys.append(key) if self.onscreen==self.player: if key in ["+", "KP_Add", "plus", "equal"]: if "Control_L" in self.pressed_keys or "Control_R" in self.pressed_keys: self.change_volume(1, mute=False) else: self.change_volume(2, mute=False) return True elif key in ["-", "KP_Subtract", "minus"]: if "Control_L" in self.pressed_keys or "Control_R" in self.pressed_keys: self.change_volume(-1, mute=False) else: self.change_volume(-2, mute=False) return True elif key=='bracketleft' or key=="Left": self.change_position(-10) return True elif key=='bracketright' or key=="Right": self.change_position(10) return True elif key=='apostrophe' or key=="Down": if "Shift_L" in self.pressed_keys or "Shift_R" in self.pressed_keys: self.change_position(-300) else: self.change_position(-60) return True elif key=='backslash' or key=="Up": if "Shift_L" in self.pressed_keys or "Shift_R" in self.pressed_keys: self.change_position(300) else: self.change_position(60) return True elif key=='End': self.change_position(seek=99.9) return True elif key=='Home': self.change_position(seek=0) return True elif self.onscreen==self.file_browser: if key=="Escape": return False if key=='b' or key=='n': self.hide_browser() return True # send all keys to browser for GTK3 return self.file_browser.pushkey(key) return False def keyboard(self, event=None, keyval=None, keycode=None, state=None, key=None): if key is None: key = Gdk.keyval_name(keyval) print("KEY:", key, keyval, state, self.onscreen) print(state.ALT_MASK, state.CONTROL_MASK) if key in self.pressed_keys: self.pressed_keys.remove(key) # ignore some special keys pressed mostly as mistakes #if "Alt_L" in self.pressed_keys: if state and 'alt-mask' in state.value_nicks: return False # remap keys if key in key_remap: key = key_remap[key] if key.startswith("KP_"): key = key[3:] if self.window.entry.get_visible() and len(key)==1 and key!="\n": # ignore characters from text input box return False if self.onscreen==self.file_browser: if key=="Escape": self.hide_browser() return True return False # Profiles if key.startswith('F') and key[1:].isdigit(): self.change_profile(int(key[1:])) return True # Channels elif key=="Page_Down": self.change_channel(-1, player=players.default) return True elif key=="Page_Up": self.change_channel(1, player=players.default) return True elif key in '1234567890': self.channel_req += key print("CHANNEL REQ:", self.channel_req) self.window.label_left.set_text(self.channel_req+"_") self.commit_channel_timeout = \ GLib.timeout_add(2000, self.commit_channel) return True elif key in ["Return", "Enter", "\n"]: tvguide.tv_program.reload_current() self.commit_channel() return True elif key=='BackSpace' and (self.last_channel is not None): self.change_channel(channel=self.last_channel, player=players.default) return True # Source (DVB-T, Multicast, ...) elif key=='s': self.source += 1 self.change_channel() return True # lock on this source, disable autoswitch elif key=='L': self.autoswitch_source = not self.autoswitch_source print("Auto source switch:", self.autoswitch_source) return True # Subtitle elif key=='t': self.change_subtitle() return True # Engine elif key=='E': self.change_engine() return True # Pause, FF, REW elif key=='p': print("Player state:", self.player.get_state()) self.player.pause() self.audio_player.pause() pos = self.player.get_position() self.set_status( "%s: %s/%s, %3.1f%%" % (self.player.get_state(), totime(pos[1]), totime(pos[2]), pos[0]) ) if pos[0]: self.window.progress_bar.set_value('Position', int(pos[0])) time.sleep(0.1) return True elif key=="REW_s": self.change_position(-10) return True elif key=="FF_s": self.change_position(10) return True elif key=="REW_m": self.change_position(-60) return True elif key=="FF_m": self.change_position(60) return True # Volume return True elif key=="VOL_UP": self.change_volume(2, mute=False) return True elif key=="VOL_DOWN": self.change_volume(-2, mute=False) return True elif key=='m': # switch volumes self.volume, self.mute_volume = self.mute_volume, self.volume if self.volume==0: self.player.toggle_mute() self.mute = self.player.get_mute() self.set_status("Mute: %s" % ["off", "on"][self.mute]) else: self.change_volume(volume=self.volume, mute=False) return True elif key=='a': self.change_track(1) # +1 return True # Fullscreen elif key=='f': self.change_fullscreen(1) return True elif key=='h': self.window.hbox.set_visible(not self.window.hbox.get_visible()) elif key=='W' and self.onscreen==self.player and not self.fullscreen: # resize x, y = self.player.get_size() if x>0 and y>0: self.window.resize(x, y) self.set_status("Window size: %d x %d" % (x, y)) return True elif key=='w' and self.onscreen==self.player and not self.fullscreen: vx, vy = self.player.get_size() wx, wy = self.window.get_size() if vy==0 or wy==0: return True # do not divide by zero vaspect = float(vx)/float(vy) waspect = float(wx)/float(wy) if vaspect' or key=='greater': self.player.set_rate(inc=2.0) # snapshot elif key=='S': fn = datetime.now().strftime("snapshot-%Y%m%d-%H%M%S.%f.png") self.player.snapshot(snapshot_path, fn) # debug elif key=="Delete": # remove current file if ALLOW_DELETE: print("Deleting %s ..." % self.last_url.url) os.unlink(self.last_url.url) self.change_channel(1, player=players.default) return True else: print("Delete disabled. Please use --delete option.") elif key=="D": #import IPython;IPython.embed() import e;e() return True return False def mouse_press(self, event, clicks, pos_x, pos_y): #print("Mouse event:", event, clicks, pos_x, pos_y) button = event.get_current_button() if button==1 or button==3: if clicks==2: self.keyboard(key='f') return True return False elif button==8: self.change_channel(1, player=players.default) return True elif button==9: self.change_channel(-1, player=players.default) return True self.button_press = button return False def mouse_release(self, event, *args): self.button_press = None return False def mouse_scroll(self, event, dir_x, dir_y): #if event.direction==Gtk.gdk.SCROLL_UP: if dir_y<0: if self.button_press==2: self.change_channel(1, player=players.default) elif "Shift_L" in self.pressed_keys or "Shift_R" in self.pressed_keys: self.change_position(10) elif "Control_L" in self.pressed_keys or "Control_R" in self.pressed_keys: self.change_volume(2, mute=False) else: self.change_volume(1, mute=False) return True #elif event.direction==Gtk.gdk.SCROLL_DOWN: elif dir_y>0: if self.button_press==2: self.change_channel(-1, player=players.default) elif "Shift_L" in self.pressed_keys or "Shift_R" in self.pressed_keys: self.change_position(-10) elif "Control_L" in self.pressed_keys or "Control_R" in self.pressed_keys: self.change_volume(-2, mute=False) else: self.change_volume(-1, mute=False) return True return False def mouse_motion(self, event, pos_x, pos_y): def delta2(a, b): return (a[0]-b[0])**2 + (a[1]-b[1])**2 if delta2((pos_x, pos_y), self.last_cursor_position)>=50: self.window.set_cursor(None) self.last_motion = time.time() self.last_cursor_position = (pos_x, pos_y) return False def drag_init(self, widget): drags = [ ('text/plain', 0, 0), ('TEXT', 0, 1), ('STRING', 0, 2), ('text/uri-list', 0, 3) ] widget.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) widget.drag_dest_add_text_targets() widget.connect("drag_data_received", self.drag_data) def drag_data(self, widget, context, x, y, data, info, time): url = data.get_text() if url.startswith("http://") or url.startswith("ftp://"): self.play_files([url], [url]) context.finish(True, False, time) def engine_gstreamer_vaapi(self, *args): players.gstp.GST_cmd.prefer_vaapi = "decode" players.default = players.GSTWidget self.last_url = None self.change_channel(0, player=players.default) def engine_gstreamer_no_vaapi(self, *args): players.gstp.GST_cmd.prefer_vaapi = "" players.default = players.GSTWidget self.last_url = None self.change_channel(0, player=players.default) def engine_vlc(self, *args): players.default = players.VLCWidget self.last_url = None self.change_channel(0, player=players.default) def show_about(self, *args): from version import VERSION, RELEASE, LICENSE about = Gtk.AboutDialog() about.set_program_name("SVPlayer") about.set_version(f"{VERSION}-{RELEASE}") about.set_copyright(f"(c) 2011-2023 Ján ONDREJ (SAL), {LICENSE}") about.set_license_type(Gtk.License.GPL_2_0) about.set_website("http://www.salstar.sk/svplayer") about.set_modal(self.window) about.set_visible(True) if __name__ == "__main__": # load config try: f = open(config_file, "rt") defaults = json.loads(f.read()) f.close() except (IOError, ValueError) as e: print("Config error:", e) defaults = dict( volume = 50, channel = 0 ) # parse options try: opts, files = getopt.gnu_getopt(sys.argv[1:], 'hfby:p:c:g:m0', [ 'help', 'fullscreen', 'browser', 'profile=', 'channel=', 'geometry=', 'multi', 'movetopleft', 'delete' ]) except getopt.GetoptError as error: (msg, opt) = error.args print("Error:",msg) sys.exit(1) # start player profile = 0 for key, value in opts: if key in ['-h', '--help']: print(__doc__.strip()) sys.exit() elif key in ['-f', '--fullscreen']: defaults["fullscreen"] = True elif key in ['-p', '--profile']: if value.isdigit(): profile = int(value) elif key in ['-c', '--channel']: defaults['channel'] = int(value) elif key in ['-g', '--geometry']: value = reg_geometry.search(value).groups() print("Geometry:", value) if value[0] and value[1]: defaults["windowed_size"] = (int(value[0]), int(value[1])) if value[2] and value[3]: defaults["window_position"] = (int(value[2]), int(value[3])) elif key=="--delete": ALLOW_DELETE = True if files: CHANNELS = [] defaults['channel'] = 1 for fn in files: subtitles = [ os.path.abspath(fn.rsplit(".", 1)[0] + ".srt") ] CHANNELS.append(media(fn, fn, subtitles=subtitles)) # initialize lirc if liblirc is not None: try: lirc = liblirc.lirc('svplayer') except Exception as e: print("LIRC init error:", e) lirc = None # run remote thread #mobile_thread = mobile.server.thread( # ('', 9008), # dict(enumerate([ # x.name # for x in CHANNELS # ])), # player #) # run main program app = SVPlayerApplication( CHANNELS, defaults.get('channel', 0), defaults.get('volume', 50), defaults.get('windowed_size', (640, 480)), defaults.get('window_position', None), defaults.get('fullscreen', False), profile=profile, web_remote_control=web_remote_control ) app.run() #time.sleep(10)