diff --git a/app/tools/media.py b/app/tools/media.py index f455b5a6..9fad7984 100644 --- a/app/tools/media.py +++ b/app/tools/media.py @@ -1,32 +1,224 @@ import os import sys +from abc import ABC, abstractmethod from datetime import datetime from app.commons import run_task, log, _DATE_FORMAT -class Player: +class Player(ABC): + """ Base player class. Also used as a factory. """ + + @abstractmethod + def get_play_mode(self): + pass + + @abstractmethod + def play(self, mrl=None): + pass + + @abstractmethod + def stop(self): + pass + + @abstractmethod + def pause(self): + pass + + @abstractmethod + def set_time(self, time): + pass + + @abstractmethod + def release(self): + pass + + @abstractmethod + def is_playing(self): + pass + + @abstractmethod + def get_instance(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb): + pass + + @staticmethod + def make(name, mode, widget, buf_cb=None, position_cb=None, error_cb=None, playing_cb=None): + """ Factory method. We will not use a separate factory to return a specific implementation. + + @param name: implementation name. + @param mode: current player mode [Built-in or windowed]. + @param widget: parent of video widget. + @param buf_cb: buffering callback. + @param position_cb: time (position) callback. + @param error_cb: error callback. + @param playing_cb: playing state callback. + + Throws a NameError if there is no implementation for the given name. + """ + if name == "gst": + return GstPlayer.get_instance(mode, widget, buf_cb, position_cb, error_cb, playing_cb) + elif name == "vlc": + return VlcPlayer.get_instance(mode, widget, buf_cb, position_cb, error_cb, playing_cb) + else: + raise NameError("There is no such [{}] implementation.".format(name)) + + +class GstPlayer(Player): + """ Simple wrapper for GStreamer playbin. """ + + __INSTANCE = None + + def __init__(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb): + try: + import gi + + gi.require_version("Gst", "1.0") + gi.require_version("GstVideo", "1.0") + from gi.repository import Gst, GstVideo + # Initialization of GStreamer. + Gst.init(sys.argv) + gtk_sink = Gst.ElementFactory.make("gtksink") + if not gtk_sink: + msg = "GStreamer error: gtksink plugin not installed!" + log(msg) + raise ImportError(msg) + except OSError as e: + log("{}: Load library error: {}".format(__class__.__name__, e)) + raise ImportError("No GStreamer is found. Check that it is installed!") + else: + self._error_cb = error_cb + self._playing_cb = playing_cb + + self.STATE = Gst.State + self.STAT_RETURN = Gst.StateChangeReturn + + self._mode = mode + self._is_playing = False + self._player = Gst.ElementFactory.make("playbin", "player") + # Initialization of the playback widget. + self._player.set_property("video-sink", gtk_sink) + vid_widget = gtk_sink.props.widget + widget.add(vid_widget) + vid_widget.show() + + bus = self._player.get_bus() + bus.add_signal_watch() + bus.connect("message::error", self.on_error) + bus.connect("message::state-changed", self.on_state_changed) + bus.connect("message::eos", self.on_eos) + + @classmethod + def get_instance(cls, mode, widget, buf_cb=None, position_cb=None, error_cb=None, playing_cb=None): + if not cls.__INSTANCE: + cls.__INSTANCE = GstPlayer(mode, widget, buf_cb, position_cb, error_cb, playing_cb) + return cls.__INSTANCE + + def get_play_mode(self): + return self._mode + + def play(self, mrl=None): + self._player.set_state(self.STATE.READY) + if not mrl: + return + + self._player.set_property("uri", mrl) + + log("Setting the URL for playback: {}".format(mrl)) + ret = self._player.set_state(self.STATE.PLAYING) + + if ret == self.STAT_RETURN.FAILURE: + log("ERROR: Unable to set the 'PLAYING' state for '{}'.".format(mrl)) + else: + self._is_playing = True + + def stop(self): + log("Stop playback...") + self._player.set_state(self.STATE.READY) + self._is_playing = False + + def pause(self): + self._player.set_state(self.STATE.PAUSED) + + def set_time(self, time): + pass + + @run_task + def release(self): + self._is_playing = False + self._player.set_state(self.STATE.NULL) + self.__INSTANCE = None + + def set_mrl(self, mrl): + self._player.set_property("uri", mrl) + + def is_playing(self): + return self._is_playing + + def on_error(self, bus, msg): + err, dbg = msg.parse_error() + log(err) + self._error_cb() + + def on_state_changed(self, bus, msg): + if not msg.src == self._player: + # Not from the player. + return + + old_state, new_state, pending = msg.parse_state_changed() + if new_state is self.STATE.PLAYING: + log("Starting playback...") + self._playing_cb() + self.get_stream_info() + + def on_eos(self, bus, msg): + """ Called when an end-of-stream message appears. """ + self._player.set_state(self.STATE.READY) + self._is_playing = False + + def get_stream_info(self): + log("Getting stream info...") + nr_video = self._player.get_property("n-video") + for i in range(nr_video): + # Retrieve the stream's video tags. + tags = self._player.emit("get-video-tags", i) + if tags: + _, cod = tags.get_string("video-codec") + log("Video codec: {}".format(cod or "unknown")) + + nr_audio = self._player.get_property("n-audio") + for i in range(nr_audio): + # Retrieve the stream's video tags. + tags = self._player.emit("get-audio-tags", i) + if tags: + _, cod = tags.get_string("audio-codec") + log("Audio codec: {}".format(cod or "unknown")) + + +class VlcPlayer(Player): """ Simple wrapper for VLC media player. """ + __VLC_INSTANCE = None - def __init__(self, mode, rewind_cb, position_cb, error_cb, playing_cb): + def __init__(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb): try: from app.tools import vlc from app.tools.vlc import EventType - except OSError as e: + + args = "--quiet {}".format("" if sys.platform == "darwin" else "--no-xlib") + self._player = vlc.Instance(args).media_player_new() + except (OSError, AttributeError) as e: log("{}: Load library error: {}".format(__class__.__name__, e)) - raise ImportError + raise ImportError("No VLC is found. Check that it is installed!") else: self._mode = mode self._is_playing = False - args = "--quiet {}".format("" if sys.platform == "darwin" else "--no-xlib") - self._player = vlc.Instance(args).media_player_new() + ev_mgr = self._player.event_manager() - if rewind_cb: + if buf_cb: # TODO look other EventType options ev_mgr.event_attach(EventType.MediaPlayerBuffering, - lambda et, p: rewind_cb(p.get_media().get_duration()), + lambda et, p: buf_cb(p.get_media().get_duration()), self._player) if position_cb: ev_mgr.event_attach(EventType.MediaPlayerTimeChanged, @@ -42,10 +234,12 @@ class Player: lambda et, p: playing_cb(), self._player) + self.init_video_widget(widget) + @classmethod - def get_instance(cls, mode, rewind_cb=None, position_cb=None, error_cb=None, playing_cb=None): + def get_instance(cls, mode, widget, buf_cb=None, position_cb=None, error_cb=None, playing_cb=None): if not cls.__VLC_INSTANCE: - cls.__VLC_INSTANCE = Player(mode, rewind_cb, position_cb, error_cb, playing_cb) + cls.__VLC_INSTANCE = VlcPlayer(mode, widget, buf_cb, position_cb, error_cb, playing_cb) return cls.__VLC_INSTANCE def get_play_mode(self): @@ -78,12 +272,24 @@ class Player: self._player.release() self.__VLC_INSTANCE = None - def set_xwindow(self, xid): - self._player.set_xwindow(xid) + def set_mrl(self, mrl): + self._player.set_mrl(mrl) + + def is_playing(self): + return self._is_playing + + def init_video_widget(self, widget): + from gi.repository import Gtk, Gdk + + area = Gtk.DrawingArea(visible=True) + area.connect("draw", self.on_drawing_area_draw) + area.set_events(Gdk.ModifierType.BUTTON1_MASK) + widget.add(area) + if sys.platform == "linux": + self._player.set_xwindow(area.get_window().get_xid()) def set_nso(self, widget): """ Used on MacOS to set NSObject. - Based on gtkvlc.py[get_window_pointer] example from here: https://github.com/oaubert/python-vlc/tree/master/examples """ @@ -101,14 +307,14 @@ class Player: pointer = ctypes.pythonapi.PyCapsule_GetPointer(widget.get_window().__gpointer__, None) self._player.set_nsobject(get_nsview(pointer)) - def set_mrl(self, mrl): - self._player.set_mrl(mrl) + def on_drawing_area_draw(self, widget, cr): + """ Used for black background drawing in the player drawing area. """ + allocation = widget.get_allocation() + cr.set_source_rgb(0, 0, 0) + cr.rectangle(0, 0, allocation.width, allocation.height) + cr.fill() - def is_playing(self): - return self._is_playing - - def set_full_screen(self, full): - self._player.set_fullscreen(full) + return False class Recorder: diff --git a/app/ui/main_app_window.py b/app/ui/main_app_window.py index cd53f504..eb5b3c72 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -157,10 +157,8 @@ class Application(Gtk.Application): "on_player_press": self.on_player_press, "on_full_screen": self.on_full_screen, "on_http_status_visible": self.on_http_status_visible, - "on_drawing_area_realize": self.on_drawing_area_realize, - "on_player_drawing_area_draw": self.on_player_drawing_area_draw, + "on_player_box_realize": self.on_player_box_realize, "on_ftp_realize": self.on_ftp_realize, - "on_main_window_state": self.on_main_window_state, "on_record": self.on_record, "on_remove_all_unavailable": self.on_remove_all_unavailable, "on_new_bouquet": self.on_new_bouquet, @@ -296,11 +294,11 @@ class Application(Gtk.Application): self._filter_bar.bind_property("search-mode-enabled", self._filter_bar, "visible") # Player self._player_box = builder.get_object("player_box") + self._player_event_box = builder.get_object("player_event_box") self._player_scale = builder.get_object("player_scale") self._player_full_time_label = builder.get_object("player_full_time_label") self._player_current_time_label = builder.get_object("player_current_time_label") self._player_rewind_box = builder.get_object("player_rewind_box") - self._player_drawing_area = builder.get_object("player_drawing_area") self._player_tool_bar = builder.get_object("player_tool_bar") self._player_prev_button = builder.get_object("player_prev_button") self._player_next_button = builder.get_object("player_next_button") @@ -318,9 +316,6 @@ class Application(Gtk.Application): self._fav_view.bind_property("sensitive", self._player_next_button, "sensitive") # Record self._record_image = builder.get_object("record_button_image") - # Enabling events for the drawing area - self._player_drawing_area.set_events(Gdk.ModifierType.BUTTON1_MASK) - self._player_frame = builder.get_object("player_frame") # Search self._search_bar = builder.get_object("search_bar") self._search_bar.bind_property("search-mode-enabled", self._search_bar, "visible") @@ -2293,18 +2288,17 @@ class Application(Gtk.Application): yield True self._wait_dialog.hide() - # ***************** Backup ********************# + # ***************** Backup ******************** # def on_backup_tool_show(self, action, value=None): """ Shows backup tool dialog """ BackupDialog(self._main_window, self._settings, self.open_data).show() - # ***************** Player *********************# + # ***************** Player ********************* # def on_play_stream(self, item=None): self.on_player_play() - @run_idle def on_player_play(self, item=None): path, column = self._fav_view.get_cursor() if path: @@ -2340,9 +2334,9 @@ class Application(Gtk.Application): self.show_playback_window() elif self._playback_window: title = self.get_playback_title() - GLib.idle_add(self._playback_window.set_title, title) - GLib.idle_add(self._player.play, url, priority=GLib.PRIORITY_LOW) - GLib.idle_add(self._playback_window.show) + self._playback_window.set_title(title) + self._playback_window.show() + GLib.idle_add(self._player.play, url) else: self.show_error_dialog("Init player error!") finally: @@ -2354,7 +2348,8 @@ class Application(Gtk.Application): if not self._player_box.get_visible(): self.set_player_area_size(self._player_box) - GLib.idle_add(self._player.play, url, priority=GLib.PRIORITY_LOW) + GLib.idle_add(self._player.play, url) + self._player_box.set_visible(True) def on_player_stop(self, item=None): @@ -2371,6 +2366,7 @@ class Application(Gtk.Application): @run_with_delay(1) def set_player_action(self): + self._fav_view.set_sensitive(False) if self._fav_click_mode is FavClickMode.PLAY: self.on_stream() elif self._fav_click_mode is FavClickMode.ZAP_PLAY: @@ -2406,12 +2402,15 @@ class Application(Gtk.Application): self._player_scale.get_adjustment().set_upper(duration) GLib.idle_add(self._player_rewind_box.set_visible, duration > 0, priority=GLib.PRIORITY_LOW) GLib.idle_add(self._player_current_time_label.set_text, "0", priority=GLib.PRIORITY_LOW) - GLib.idle_add(self._player_full_time_label.set_text, self.get_time_str(duration), priority=GLib.PRIORITY_LOW) + GLib.idle_add(self._player_full_time_label.set_text, self.get_time_str(duration), + priority=GLib.PRIORITY_LOW) def on_player_time_changed(self, t): if not self._full_screen and self._player_rewind_box.get_visible(): - GLib.idle_add(self._player_current_time_label.set_text, self.get_time_str(t), priority=GLib.PRIORITY_LOW) + GLib.idle_add(self._player_current_time_label.set_text, self.get_time_str(t), + priority=GLib.PRIORITY_LOW) + @run_with_delay(2) def on_player_error(self): self.set_playback_elms_active() self.show_error_dialog("Can't Playback!") @@ -2427,49 +2426,31 @@ class Application(Gtk.Application): h, m = divmod(m, 60) return "{}{:02d}:{:02d}".format(str(h) + ":" if h else "", m, s) - def on_drawing_area_realize(self, widget): + def on_player_box_realize(self, widget): if not self._player: try: - self._player = Player.get_instance(mode=self._settings.play_streams_mode, - rewind_cb=self.on_player_duration_changed, - position_cb=self.on_player_time_changed, - error_cb=self.on_player_error, - playing_cb=self.set_playback_elms_active) - except (ImportError, NameError, AttributeError): - self.show_error_dialog("No VLC is found. Check that it is installed!") + self._player = Player.make(name="gst", + mode=self._settings.play_streams_mode, + widget=widget, + buf_cb=self.on_player_duration_changed, + position_cb=self.on_player_time_changed, + error_cb=self.on_player_error, + playing_cb=self.set_playback_elms_active) + except (ImportError, NameError) as e: + self.show_error_dialog(str(e)) return True else: - if self._settings.is_darwin: - self._player.set_nso(widget) - else: - self._player.set_xwindow(widget.get_window().get_xid()) self._player.play(self._current_mrl) finally: - self.set_playback_elms_active() if self._settings.play_streams_mode is PlayStreamsMode.BUILT_IN: self.set_player_area_size(widget) + self._fav_view.do_grab_focus(self._fav_view) @run_idle def set_player_area_size(self, widget): w, h = self._main_window.get_size() widget.set_size_request(w * 0.6, -1) - def on_player_drawing_area_draw(self, widget, cr): - """ Used for black background drawing in the player drawing area. - - Required for Gtk >= 3.20. - More info: https://developer.gnome.org/gtk3/stable/ch32s10.html, - https://developer.gnome.org/gtk3/stable/GtkStyleContext.html#gtk-render-background - """ - context = widget.get_style_context() - width = widget.get_allocated_width() - height = widget.get_allocated_height() - Gtk.render_background(context, cr, 0, 0, width, height) - r, g, b, a = 0, 0, 0, 1 # black color - cr.set_source_rgba(r, g, b, a) - cr.rectangle(0, 0, width, height) - cr.fill() - def on_player_press(self, area, event): if event.button == Gdk.BUTTON_PRIMARY: if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS: @@ -2478,26 +2459,19 @@ class Application(Gtk.Application): def on_full_screen(self, item=None): self._full_screen = not self._full_screen if self._settings.play_streams_mode is PlayStreamsMode.BUILT_IN: + self.update_state_on_full_screen(not self._full_screen) self._main_window.fullscreen() if self._full_screen else self._main_window.unfullscreen() elif self._playback_window: self._player_tool_bar.set_visible(not self._full_screen) self._playback_window.fullscreen() if self._full_screen else self._playback_window.unfullscreen() - def on_main_window_state(self, window, event): - state = event.new_window_state - full = not state & Gdk.WindowState.FULLSCREEN - self._main_data_box.set_visible(full) - self._player_tool_bar.set_visible(full) - self._status_bar_box.set_visible(full) - if not state & Gdk.WindowState.ICONIFIED and self._links_transmitter: - self._links_transmitter.hide() + def update_state_on_full_screen(self, visible): + self._main_data_box.set_visible(visible) + self._player_tool_bar.set_visible(visible) + self._status_bar_box.set_visible(visible and not self._app_info_box.get_visible()) @run_idle def show_playback_window(self): - self._player_prev_button.set_visible(False) - self._player_next_button.set_visible(False) - self._player_play_button.set_margin_left(5) - width, height = 480, 240 size = self._settings.get("playback_window_size") if size: @@ -2509,11 +2483,15 @@ class Application(Gtk.Application): icon_name="demon-editor") self._playback_window.resize(width, height) self._playback_window.connect("delete-event", self.on_player_close) + + self._player_prev_button.set_visible(False) + self._player_next_button.set_visible(False) box = Gtk.HBox(visible=True, orientation="vertical") - self._player_drawing_area.reparent(box) + self._player_event_box.reparent(box) self._player_box.remove(self._player_tool_bar) box.pack_end(self._player_tool_bar, False, False, 0) self._playback_window.add(box) + self._playback_window.set_application(self) self._playback_window.show() @@ -2612,7 +2590,7 @@ class Application(Gtk.Application): """ Switch to the channel and watch in the player """ if not self._app_info_box.get_visible() and self._settings.play_streams_mode is PlayStreamsMode.BUILT_IN: self.set_player_area_size(self._player_box) - self._player_box.set_visible(True) + GLib.idle_add(self._player_box.set_visible, True) GLib.idle_add(self._app_info_box.set_visible, False) self._http_api.send(HttpAPI.Request.STREAM_CURRENT, None, self.watch) diff --git a/app/ui/main_window.glade b/app/ui/main_window.glade index c09d60df..7f2c8822 100644 --- a/app/ui/main_window.glade +++ b/app/ui/main_window.glade @@ -1049,7 +1049,6 @@ Author: Dmitriy Yefremov center DemonEditor - True @@ -1492,12 +1491,14 @@ Author: Dmitriy Yefremov False vertical - + True False - - + + + + True