mirror of
https://github.com/DYefremov/DemonEditor.git
synced 2026-02-08 15:47:58 +01:00
streams playback rework
This commit is contained in:
@@ -28,57 +28,95 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
|
||||
from gi.repository import Gdk, Gtk
|
||||
from gi.repository import Gdk, Gtk, GObject
|
||||
|
||||
from app.commons import run_task, log, _DATE_FORMAT, run_with_delay
|
||||
|
||||
|
||||
class Player(ABC):
|
||||
class Player(Gtk.DrawingArea):
|
||||
""" Base player class. Also used as a factory. """
|
||||
|
||||
@abstractmethod
|
||||
def __init__(self, mode, widget, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
GObject.signal_new("error", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("message", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("position", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("played", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("audio-track", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("subtitle-track", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
|
||||
self.connect("draw", self.on_draw)
|
||||
self.connect("motion-notify-event", self.on_mouse_motion)
|
||||
self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK)
|
||||
widget.add(self)
|
||||
|
||||
parent = widget.get_parent()
|
||||
parent.connect("play", self.on_play)
|
||||
parent.connect("stop", self.on_stop)
|
||||
self.show()
|
||||
|
||||
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):
|
||||
def set_audio_track(self, track):
|
||||
pass
|
||||
|
||||
def get_window_handle(self, widget):
|
||||
def get_audio_track(self):
|
||||
pass
|
||||
|
||||
def set_subtitle_track(self, track):
|
||||
pass
|
||||
|
||||
def set_aspect_ratio(self, ratio):
|
||||
pass
|
||||
|
||||
def get_instance(self, mode, widget):
|
||||
pass
|
||||
|
||||
def on_play(self, widget, url):
|
||||
self.play(url)
|
||||
|
||||
def on_stop(self, widget, state):
|
||||
self.stop()
|
||||
|
||||
def on_release(self, widget, state):
|
||||
self.release()
|
||||
|
||||
def get_window_handle(self):
|
||||
""" Returns the identifier [pointer] for the window.
|
||||
|
||||
Based on gtkvlc.py[get_window_pointer] example from here:
|
||||
https://github.com/oaubert/python-vlc/tree/master/examples
|
||||
"""
|
||||
if sys.platform == "linux":
|
||||
return widget.get_window().get_xid()
|
||||
return self.get_window().get_xid()
|
||||
else:
|
||||
is_darwin = sys.platform == "darwin"
|
||||
try:
|
||||
@@ -91,23 +129,14 @@ class Player(ABC):
|
||||
# https://gitlab.gnome.org/GNOME/pygobject/-/issues/112
|
||||
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
|
||||
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object]
|
||||
gpointer = ctypes.pythonapi.PyCapsule_GetPointer(widget.get_window().__gpointer__, None)
|
||||
gpointer = ctypes.pythonapi.PyCapsule_GetPointer(self.get_window().__gpointer__, None)
|
||||
get_pointer = libgdk.gdk_quartz_window_get_nsview if is_darwin else libgdk.gdk_win32_window_get_handle
|
||||
get_pointer.restype = ctypes.c_void_p
|
||||
get_pointer.argtypes = [ctypes.c_void_p]
|
||||
|
||||
return get_pointer(gpointer)
|
||||
|
||||
def get_video_widget(self, widget):
|
||||
area = Gtk.DrawingArea(visible=True)
|
||||
area.connect("draw", self.on_drawing_area_draw)
|
||||
area.connect("motion-notify-event", self.on_mouse_motion)
|
||||
area.set_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK)
|
||||
widget.add(area)
|
||||
|
||||
return area
|
||||
|
||||
def on_drawing_area_draw(self, widget, cr):
|
||||
def on_draw(self, widget, cr):
|
||||
""" Used for black background drawing in the player drawing area. """
|
||||
cr.set_source_rgb(0, 0, 0)
|
||||
cr.paint()
|
||||
@@ -126,25 +155,21 @@ class Player(ABC):
|
||||
window.set_cursor(cursor)
|
||||
|
||||
@staticmethod
|
||||
def make(name, mode, widget, buf_cb=None, position_cb=None, error_cb=None, playing_cb=None):
|
||||
def make(name, mode, widget):
|
||||
""" 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 == "mpv":
|
||||
return MpvPlayer.get_instance(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
return MpvPlayer.get_instance(mode, widget)
|
||||
elif name == "gst":
|
||||
return GstPlayer.get_instance(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
return GstPlayer.get_instance(mode, widget)
|
||||
elif name == "vlc":
|
||||
return VlcPlayer.get_instance(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
return VlcPlayer.get_instance(mode, widget)
|
||||
else:
|
||||
raise NameError("There is no such [{}] implementation.".format(name))
|
||||
|
||||
@@ -156,11 +181,12 @@ class MpvPlayer(Player):
|
||||
"""
|
||||
__INSTANCE = None
|
||||
|
||||
def __init__(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
|
||||
def __init__(self, mode, widget):
|
||||
super().__init__(mode, widget)
|
||||
try:
|
||||
from app.tools import mpv
|
||||
|
||||
self._player = mpv.MPV(wid=str(self.get_window_handle(self.get_video_widget(widget), )),
|
||||
self._player = mpv.MPV(wid=str(self.get_window_handle()),
|
||||
input_default_bindings=False,
|
||||
input_cursor=False,
|
||||
cursor_autohide="no")
|
||||
@@ -174,25 +200,24 @@ class MpvPlayer(Player):
|
||||
@self._player.event_callback(mpv.MpvEventID.FILE_LOADED)
|
||||
def on_open(event):
|
||||
log("Starting playback...")
|
||||
playing_cb()
|
||||
self.emit("played", 0)
|
||||
|
||||
@self._player.event_callback(mpv.MpvEventID.END_FILE)
|
||||
def on_end(event):
|
||||
event = event.get("event", {})
|
||||
if event.get("reason", mpv.MpvEventEndFile.ERROR) == mpv.MpvEventEndFile.ERROR:
|
||||
log("Stream playback error: {}".format(event.get("error", mpv.ErrorCode.GENERIC)))
|
||||
error_cb()
|
||||
self.emit("error", "Can't Playback!")
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
|
||||
def get_instance(cls, mode, widget):
|
||||
if not cls.__INSTANCE:
|
||||
cls.__INSTANCE = MpvPlayer(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
cls.__INSTANCE = MpvPlayer(mode, widget)
|
||||
return cls.__INSTANCE
|
||||
|
||||
def get_play_mode(self):
|
||||
return self._mode
|
||||
|
||||
@run_task
|
||||
def play(self, mrl=None):
|
||||
if not mrl:
|
||||
return
|
||||
@@ -200,7 +225,6 @@ class MpvPlayer(Player):
|
||||
self._player.play(mrl)
|
||||
self._is_playing = True
|
||||
|
||||
@run_task
|
||||
def stop(self):
|
||||
self._player.stop()
|
||||
self._is_playing = True
|
||||
@@ -225,7 +249,8 @@ class GstPlayer(Player):
|
||||
|
||||
__INSTANCE = None
|
||||
|
||||
def __init__(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
|
||||
def __init__(self, mode, widget):
|
||||
super().__init__(mode, widget)
|
||||
try:
|
||||
import gi
|
||||
|
||||
@@ -234,30 +259,17 @@ class GstPlayer(Player):
|
||||
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, ValueError) 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.get_property("widget")
|
||||
vid_widget.connect("motion-notify-event", self.on_mouse_motion)
|
||||
widget.add(vid_widget)
|
||||
vid_widget.show()
|
||||
self._player.set_window_handle(self.get_window_handle())
|
||||
|
||||
bus = self._player.get_bus()
|
||||
bus.add_signal_watch()
|
||||
@@ -266,9 +278,9 @@ class GstPlayer(Player):
|
||||
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):
|
||||
def get_instance(cls, mode, widget):
|
||||
if not cls.__INSTANCE:
|
||||
cls.__INSTANCE = GstPlayer(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
cls.__INSTANCE = GstPlayer(mode, widget)
|
||||
return cls.__INSTANCE
|
||||
|
||||
def get_play_mode(self):
|
||||
@@ -285,8 +297,11 @@ class GstPlayer(Player):
|
||||
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))
|
||||
msg = "ERROR: Unable to set the 'PLAYING' state for '{}'.".format(mrl)
|
||||
log(msg)
|
||||
self.emit("error", msg)
|
||||
else:
|
||||
self.emit("played", 0)
|
||||
self._is_playing = True
|
||||
|
||||
def stop(self):
|
||||
@@ -315,7 +330,7 @@ class GstPlayer(Player):
|
||||
def on_error(self, bus, msg):
|
||||
err, dbg = msg.parse_error()
|
||||
log(err)
|
||||
self._error_cb()
|
||||
self.emit("error", "Can't Playback!")
|
||||
|
||||
def on_state_changed(self, bus, msg):
|
||||
if not msg.src == self._player:
|
||||
@@ -325,7 +340,7 @@ class GstPlayer(Player):
|
||||
old_state, new_state, pending = msg.parse_state_changed()
|
||||
if new_state is self.STATE.PLAYING:
|
||||
log("Starting playback...")
|
||||
self._playing_cb()
|
||||
self.emit("played", 0)
|
||||
self.get_stream_info()
|
||||
|
||||
def on_eos(self, bus, msg):
|
||||
@@ -360,8 +375,12 @@ class VlcPlayer(Player):
|
||||
|
||||
__VLC_INSTANCE = None
|
||||
|
||||
def __init__(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
|
||||
def __init__(self, mode, widget):
|
||||
super().__init__(mode, widget)
|
||||
try:
|
||||
if sys.platform == "win32":
|
||||
os.add_dll_directory(r"C:\Program Files\VideoLAN\VLC")
|
||||
|
||||
from app.tools import vlc
|
||||
from app.tools.vlc import EventType
|
||||
|
||||
@@ -377,45 +396,28 @@ class VlcPlayer(Player):
|
||||
self._is_playing = False
|
||||
|
||||
ev_mgr = self._player.event_manager()
|
||||
|
||||
if buf_cb:
|
||||
# TODO look other EventType options
|
||||
ev_mgr.event_attach(EventType.MediaPlayerBuffering,
|
||||
lambda et, p: buf_cb(p.get_media().get_duration()),
|
||||
self._player)
|
||||
if position_cb:
|
||||
ev_mgr.event_attach(EventType.MediaPlayerTimeChanged,
|
||||
lambda et, p: position_cb(p.get_time()),
|
||||
self._player)
|
||||
|
||||
if error_cb:
|
||||
ev_mgr.event_attach(EventType.MediaPlayerEncounteredError,
|
||||
lambda et, p: error_cb(),
|
||||
self._player)
|
||||
if playing_cb:
|
||||
ev_mgr.event_attach(EventType.MediaPlayerPlaying,
|
||||
lambda et, p: playing_cb(),
|
||||
self._player)
|
||||
ev_mgr.event_attach(EventType.MediaPlayerVout, self.on_playback_start)
|
||||
ev_mgr.event_attach(EventType.MediaPlayerTimeChanged,
|
||||
lambda et: self.emit("position", self._player.get_time()))
|
||||
ev_mgr.event_attach(EventType.MediaPlayerEncounteredError, lambda et: self.emit("error", "Can't Playback!"))
|
||||
|
||||
self.init_video_widget(widget)
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, mode, widget, buf_cb=None, position_cb=None, error_cb=None, playing_cb=None):
|
||||
def get_instance(cls, mode, widget):
|
||||
if not cls.__VLC_INSTANCE:
|
||||
cls.__VLC_INSTANCE = VlcPlayer(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
cls.__VLC_INSTANCE = VlcPlayer(mode, widget)
|
||||
return cls.__VLC_INSTANCE
|
||||
|
||||
def get_play_mode(self):
|
||||
return self._mode
|
||||
|
||||
@run_task
|
||||
def play(self, mrl=None):
|
||||
if mrl:
|
||||
self._player.set_mrl(mrl)
|
||||
self._player.play()
|
||||
self._is_playing = True
|
||||
|
||||
@run_task
|
||||
def stop(self):
|
||||
if self._is_playing:
|
||||
self._player.stop()
|
||||
@@ -441,14 +443,34 @@ class VlcPlayer(Player):
|
||||
def is_playing(self):
|
||||
return self._is_playing
|
||||
|
||||
def set_audio_track(self, track):
|
||||
self._player.audio_set_track(track)
|
||||
|
||||
def get_audio_track(self):
|
||||
return self._player.audio_get_track()
|
||||
|
||||
def set_subtitle_track(self, track):
|
||||
self._player.video_set_spu(track)
|
||||
|
||||
def set_aspect_ratio(self, ratio):
|
||||
self._player.video_set_aspect_ratio(ratio)
|
||||
|
||||
def on_playback_start(self, event):
|
||||
self.emit("played", self._player.get_media().get_duration())
|
||||
# Audio tracks
|
||||
a_desc = self._player.audio_get_track_description()
|
||||
self.emit("audio-track", [(t[0], t[1].decode(encoding="utf-8", errors="ignore")) for t in a_desc])
|
||||
# Subtitle
|
||||
s_desc = self._player.video_get_spu_description()
|
||||
self.emit("subtitle-track", [(s[0], s[1].decode(encoding="utf-8", errors="ignore")) for s in s_desc])
|
||||
|
||||
def init_video_widget(self, widget):
|
||||
video_widget = self.get_video_widget(widget)
|
||||
if sys.platform == "linux":
|
||||
self._player.set_xwindow(video_widget.get_window().get_xid())
|
||||
self._player.set_xwindow(self.get_window_handle())
|
||||
elif sys.platform == "darwin":
|
||||
self._player.set_nsobject(self.get_window_handle(video_widget))
|
||||
self._player.set_nsobject(self.get_window_handle())
|
||||
else:
|
||||
self._player.set_hwnd(self.get_window_handle(video_widget))
|
||||
self._player.set_hwnd(self.get_window_handle())
|
||||
|
||||
|
||||
class Recorder:
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<menu id="menu_bar">
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Playback</attribute>
|
||||
<attribute name="action">app.hide_media_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Close</attribute>
|
||||
<attribute name="action">app.on_playback_close</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">File</attribute>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
@@ -176,6 +187,17 @@
|
||||
</section>
|
||||
</menu>
|
||||
<menu id="mac_menu_bar">
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Playback</attribute>
|
||||
<attribute name="action">app.hide_media_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Close</attribute>
|
||||
<attribute name="action">app.on_playback_close</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">File</attribute>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
|
||||
@@ -1201,6 +1201,7 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_title_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
@@ -1276,7 +1277,7 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">False</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
|
||||
@@ -35,9 +35,10 @@ from urllib.parse import quote
|
||||
from gi.repository import GLib
|
||||
|
||||
from .dialogs import get_builder, show_dialog, DialogType
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Page
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Page, Column
|
||||
from ..commons import run_task, run_with_delay, log, run_idle
|
||||
from ..connections import HttpAPI, UtfFTP
|
||||
from ..settings import IS_DARWIN, PlayStreamsMode
|
||||
|
||||
|
||||
class EpgBox(Gtk.Box):
|
||||
@@ -131,15 +132,14 @@ class RecordingsBox(Gtk.Box):
|
||||
def __init__(self, app, http_api, settings, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._http_api = http_api
|
||||
self._app = app
|
||||
self._app.connect("profile-changed", self.init)
|
||||
self._settings = settings
|
||||
self._ftp = None
|
||||
# Icon.
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
icon = "folder-symbolic"
|
||||
self._icon = theme.load_icon(icon, 32, 0) if theme.lookup_icon(icon, 32, 0) else None
|
||||
icon = "folder-symbolic" if IS_DARWIN else "folder"
|
||||
self._icon = theme.load_icon(icon, 24, 0) if theme.lookup_icon(icon, 24, 0) else None
|
||||
|
||||
handlers = {"on_path_press": self.on_path_press,
|
||||
"on_path_activated": self.on_path_activated,
|
||||
@@ -150,6 +150,7 @@ class RecordingsBox(Gtk.Box):
|
||||
objects=("recordings_frame", "recordings_model", "rec_paths_model"))
|
||||
self._rec_view = builder.get_object("recordings_view")
|
||||
self._paths_view = builder.get_object("recordings_paths_view")
|
||||
self._paned = builder.get_object("recordings_paned")
|
||||
self.pack_start(builder.get_object("recordings_frame"), True, True, 0)
|
||||
|
||||
self.init()
|
||||
@@ -209,7 +210,7 @@ class RecordingsBox(Gtk.Box):
|
||||
def on_path_activated(self, view, path, column):
|
||||
row = view.get_model()[path][:]
|
||||
path = "{}/{}".format(row[-1], row[1])
|
||||
self._http_api.send(HttpAPI.Request.RECORDINGS, quote(path), self.update_recordings_data)
|
||||
self._app.http_api.send(HttpAPI.Request.RECORDINGS, quote(path), self.update_recordings_data)
|
||||
|
||||
def on_path_press(self, view, event):
|
||||
target = view.get_path_at_pos(event.x, event.y)
|
||||
@@ -237,12 +238,12 @@ class RecordingsBox(Gtk.Box):
|
||||
|
||||
def on_recordings_activated(self, view, path, column):
|
||||
rec = view.get_model()[path][-1]
|
||||
self._http_api.send(HttpAPI.Request.STREAM_TS, rec.get("e2filename", ""), self.on_play_recording)
|
||||
self._app.http_api.send(HttpAPI.Request.STREAM_TS, rec.get("e2filename", ""), self.on_play_recording)
|
||||
|
||||
def on_play_recording(self, m3u):
|
||||
url = self._app.get_url_from_m3u(m3u)
|
||||
if url:
|
||||
self._app.play(url)
|
||||
self._app.emit("play-recording", url)
|
||||
|
||||
def on_recording_remove(self, action, value=None):
|
||||
""" Removes recordings via FTP. """
|
||||
@@ -259,6 +260,21 @@ class RecordingsBox(Gtk.Box):
|
||||
self._app.show_error_message(resp)
|
||||
break
|
||||
|
||||
def on_playback(self, box, state):
|
||||
""" Updates state of the UI elements for playback mode. """
|
||||
if self._settings.play_streams_mode is PlayStreamsMode.BUILT_IN:
|
||||
self._paned.set_orientation(Gtk.Orientation.VERTICAL)
|
||||
self.update_rec_columns_visibility(False)
|
||||
|
||||
def on_playback_close(self, box, state):
|
||||
""" Restores UI elements state after playback mode. """
|
||||
self._paned.set_orientation(Gtk.Orientation.HORIZONTAL)
|
||||
self.update_rec_columns_visibility(True)
|
||||
|
||||
def update_rec_columns_visibility(self, state):
|
||||
for c in (Column.REC_SERVICE, Column.REC_TIME, Column.REC_LEN, Column.REC_FILE, Column.REC_DESC):
|
||||
self._rec_view.get_column(c).set_visible(state)
|
||||
|
||||
|
||||
class ControlBox(Gtk.Box):
|
||||
|
||||
|
||||
3544
app/ui/main.glade
3544
app/ui/main.glade
File diff suppressed because it is too large
Load Diff
406
app/ui/main.py
406
app/ui/main.py
@@ -45,12 +45,13 @@ from app.eparser.ecommons import CAS, Flag, BouquetService
|
||||
from app.eparser.enigma.bouquets import BqServiceType
|
||||
from app.eparser.iptv import export_to_m3u
|
||||
from app.eparser.neutrino.bouquets import BqType
|
||||
from app.settings import (SettingsType, Settings, SettingsException, PlayStreamsMode, SettingsReadException,
|
||||
IS_DARWIN)
|
||||
from app.tools.media import Player, Recorder
|
||||
from app.settings import (SettingsType, Settings, SettingsException, SettingsReadException,
|
||||
IS_DARWIN, PlayStreamsMode)
|
||||
from app.tools.media import Recorder
|
||||
from app.ui.control import ControlBox, EpgBox, TimersBox, RecordingsBox
|
||||
from app.ui.epg_dialog import EpgDialog
|
||||
from app.ui.ftp import FtpClientBox
|
||||
from app.ui.playback import PlayerBox
|
||||
from app.ui.transmitter import LinksTransmitter
|
||||
from .backup import BackupDialog, backup_data, clear_data_path
|
||||
from .dialogs import show_dialog, DialogType, get_chooser_dialog, WaitDialog, get_message, get_builder
|
||||
@@ -59,8 +60,9 @@ from .imports import ImportDialog, import_bouquet
|
||||
from .iptv import IptvDialog, SearchUnavailableDialog, IptvListConfigurationDialog, YtListImportDialog, M3uImportDialog
|
||||
from .main_helper import (insert_marker, move_items, rename, ViewTarget, set_flags, locate_in_services,
|
||||
scroll_to, get_base_model, update_picons_data, copy_picon_reference, assign_picons,
|
||||
remove_picon, is_only_one_item_selected, gen_bouquets, BqGenType, get_iptv_url, append_picons,
|
||||
get_selection, get_model_data, remove_all_unused_picons, get_picon_pixbuf, get_base_itrs)
|
||||
remove_picon, is_only_one_item_selected, gen_bouquets, BqGenType, append_picons,
|
||||
get_selection, get_model_data, remove_all_unused_picons, get_picon_pixbuf, get_base_itrs,
|
||||
get_iptv_url)
|
||||
from .picons import PiconManager
|
||||
from .satellites import SatellitesTool, ServicesUpdateDialog
|
||||
from .search import SearchProvider
|
||||
@@ -174,18 +176,8 @@ class Application(Gtk.Application):
|
||||
"on_epg_list_configuration": self.on_epg_list_configuration,
|
||||
"on_iptv_list_configuration": self.on_iptv_list_configuration,
|
||||
"on_play_stream": self.on_play_stream,
|
||||
"on_watch": self.on_watch,
|
||||
"on_player_play": self.on_player_play,
|
||||
"on_player_stop": self.on_player_stop,
|
||||
"on_player_previous": self.on_player_previous,
|
||||
"on_player_next": self.on_player_next,
|
||||
"on_player_rewind": self.on_player_rewind,
|
||||
"on_player_close": self.on_player_close,
|
||||
"on_player_press": self.on_player_press,
|
||||
"on_full_screen": self.on_full_screen,
|
||||
"on_play_current": self.on_play_current,
|
||||
"on_main_window_state": self.on_main_window_state,
|
||||
"on_player_box_realize": self.on_player_box_realize,
|
||||
"on_player_box_visibility": self.on_player_box_visibility,
|
||||
"on_record": self.on_record,
|
||||
"on_remove_all_unavailable": self.on_remove_all_unavailable,
|
||||
"on_new_bouquet": self.on_new_bouquet,
|
||||
@@ -240,11 +232,6 @@ class Application(Gtk.Application):
|
||||
self._recordings_box = None
|
||||
self._control_box = None
|
||||
self._ftp_client = None
|
||||
# Player
|
||||
self._player = None
|
||||
self._full_screen = False
|
||||
self._current_mrl = None
|
||||
self._playback_window = None
|
||||
# Record
|
||||
self._recorder = None
|
||||
# http api
|
||||
@@ -258,14 +245,20 @@ class Application(Gtk.Application):
|
||||
self._EXTRA_COLOR = None # Color for services with a extra name for the bouquet
|
||||
# Current page.
|
||||
self._page = Page.INFO
|
||||
self._fav_pages = {Page.SERVICES, Page.PICONS, Page.PLAYBACK, Page.EPG, Page.TIMERS}
|
||||
self._fav_pages = {Page.SERVICES, Page.PICONS, Page.EPG, Page.TIMERS}
|
||||
# Signals.
|
||||
GObject.signal_new("profile-changed", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("fav-changed", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("fav-clicked", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("page-changed", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("play-recording", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("play-current", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "main.glade", handlers)
|
||||
self._main_window = builder.get_object("main_window")
|
||||
@@ -288,7 +281,7 @@ class Application(Gtk.Application):
|
||||
self._services_view.get_model().set_sort_func(Column.SRV_POS, self.position_sort_func, Column.SRV_POS)
|
||||
# App info
|
||||
self._app_info_box = builder.get_object("app_info_box")
|
||||
self._app_info_box.bind_property("visible", builder.get_object("main_paned"), "visible", 4)
|
||||
self._app_info_box.bind_property("visible", builder.get_object("data_paned"), "visible", 4)
|
||||
# Info bar.
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._info_label = builder.get_object("info_label")
|
||||
@@ -338,23 +331,20 @@ class Application(Gtk.Application):
|
||||
self._filter_only_free_button = builder.get_object("filter_only_free_button")
|
||||
self._services_load_spinner.bind_property("active", self._filter_header_button, "sensitive", 4)
|
||||
self._services_load_spinner.bind_property("active", self._filter_box, "sensitive", 4)
|
||||
# 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_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")
|
||||
self._player_play_button = builder.get_object("player_play_button")
|
||||
# Playback.
|
||||
self._player_box = PlayerBox(self)
|
||||
paned = builder.get_object("main_paned")
|
||||
data_paned = paned.get_child1()
|
||||
paned.remove(data_paned)
|
||||
self._player_box.bind_property("visible", self._profile_combo_box, "visible", 4)
|
||||
self._player_box.bind_property("visible", self._player_event_box, "visible")
|
||||
self._fav_view.bind_property("sensitive", self._player_prev_button, "sensitive")
|
||||
self._fav_view.bind_property("sensitive", self._player_next_button, "sensitive")
|
||||
self._fav_view.bind_property("sensitive", self._bouquets_view, "sensitive")
|
||||
self._player_tool_bar.bind_property("visible", builder.get_object("fs_box"), "visible")
|
||||
paned.pack1(self._player_box, True, True)
|
||||
paned.pack2(data_paned, True, False)
|
||||
self._player_box.connect("show", self.on_playback_show)
|
||||
self._player_box.connect("playback-close", self.on_playback_close)
|
||||
self._player_box.connect("playback-full-screen", self.on_playback_full_screen)
|
||||
self._data_paned = builder.get_object("data_paned")
|
||||
self._data_paned.bind_property("visible", self._status_bar_box, "visible")
|
||||
self._data_paned.bind_property("visible", builder.get_object("fs_box"), "visible")
|
||||
# Record
|
||||
self._record_image = builder.get_object("record_button_image")
|
||||
# Search
|
||||
@@ -380,27 +370,32 @@ class Application(Gtk.Application):
|
||||
self._stack_ftp_box = builder.get_object("ftp_box")
|
||||
self._stack_control_box = builder.get_object("control_box")
|
||||
# Header bar.
|
||||
profile_box = builder.get_object("profile_combo_box")
|
||||
toolbar_box = builder.get_object("toolbar_main_box")
|
||||
if IS_GNOME_SESSION:
|
||||
header_bar = Gtk.HeaderBar(visible=True, show_close_button=True)
|
||||
header_bar.pack_start(builder.get_object("file_header_button"))
|
||||
header_bar.pack_start(Gtk.Separator(visible=True))
|
||||
header_bar.pack_start(builder.get_object("profile_combo_box"))
|
||||
header_bar.pack_start(builder.get_object("toolbar_main_box"))
|
||||
header_bar.pack_start(profile_box)
|
||||
header_bar.pack_start(toolbar_box)
|
||||
header_bar.set_custom_title(builder.get_object("stack_switcher"))
|
||||
self._player_box.bind_property("visible", builder.get_object("main_popover_menu_box"), "visible", 4)
|
||||
self._player_box.bind_property("visible", builder.get_object("close_player_menu_button"), "visible")
|
||||
self._main_window.set_titlebar(header_bar)
|
||||
else:
|
||||
tool_bar = Gtk.Box(visible=True, spacing=6, margin=6, valign=Gtk.Align.CENTER)
|
||||
tool_bar.add(builder.get_object("profile_combo_box"))
|
||||
tool_bar.add(builder.get_object("toolbar_main_box"))
|
||||
tool_bar.add(profile_box)
|
||||
tool_bar.add(toolbar_box)
|
||||
tool_bar.set_center_widget(builder.get_object("stack_switcher"))
|
||||
|
||||
main_header_box = Gtk.Box(visible=True, spacing=6)
|
||||
main_header_box.get_style_context().add_class(Gtk.STYLE_CLASS_PRIMARY_TOOLBAR)
|
||||
main_header_box.pack_start(tool_bar, True, True, 0)
|
||||
main_box = builder.get_object("main_window_box")
|
||||
main_box.add(main_header_box)
|
||||
main_box.reorder_child(main_header_box, 0)
|
||||
self._player_tool_bar.bind_property("visible", main_header_box, "visible")
|
||||
self._data_paned.bind_property("visible", main_header_box, "visible")
|
||||
self._player_box.bind_property("visible", profile_box, "visible", 4)
|
||||
self._player_box.bind_property("visible", toolbar_box, "visible", 4)
|
||||
# Style
|
||||
style_provider = Gtk.CssProvider()
|
||||
style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
@@ -499,11 +494,17 @@ class Application(Gtk.Application):
|
||||
sa = self.set_state_action("show_control", self.on_page_show, self._settings.get("show_control", True))
|
||||
sa.connect("change-state", lambda a, v: self._stack_control_box.set_visible(v))
|
||||
self.bind_property("is-enigma", sa, "enabled")
|
||||
# Menu bar.
|
||||
# Menu bar and playback.
|
||||
self.set_action("on_playback_close", self._player_box.on_close)
|
||||
if not IS_GNOME_SESSION:
|
||||
# We are working with the "hidden-when" submenu attribute. See 'app_menu_.ui' file.
|
||||
hide_bar_action = Gio.SimpleAction.new("hide_menu_bar", None)
|
||||
self._player_box.bind_property("visible", hide_bar_action, "enabled", 4)
|
||||
self.add_action(hide_bar_action)
|
||||
hide_media_bar = Gio.SimpleAction.new("hide_media_bar", None)
|
||||
hide_media_bar.set_enabled(False)
|
||||
self._player_box.bind_property("visible", hide_media_bar, "enabled")
|
||||
self.add_action(hide_media_bar)
|
||||
|
||||
def set_action(self, name, fun, enabled=True):
|
||||
ac = Gio.SimpleAction.new(name, None)
|
||||
@@ -546,9 +547,6 @@ class Application(Gtk.Application):
|
||||
"last_bouquet": self._current_bq_name})
|
||||
self._settings.save() # storing current settings
|
||||
|
||||
if self._player:
|
||||
self._player.release()
|
||||
|
||||
if self._http_api:
|
||||
self._http_api.close()
|
||||
|
||||
@@ -714,6 +712,11 @@ class Application(Gtk.Application):
|
||||
else:
|
||||
GLib.idle_add(self.quit)
|
||||
|
||||
def on_main_window_state(self, window, event):
|
||||
if event.new_window_state & Gdk.WindowState.FULLSCREEN or event.new_window_state & Gdk.WindowState.MAXIMIZED:
|
||||
# Saving the current size of the application window.
|
||||
self._settings.add("window_size", self._main_window.get_size())
|
||||
|
||||
@run_idle
|
||||
def on_about_app(self, action, value=None):
|
||||
show_dialog(DialogType.ABOUT, self._main_window)
|
||||
@@ -752,6 +755,8 @@ class Application(Gtk.Application):
|
||||
def on_recordings_realize(self, box):
|
||||
self._recordings_box = RecordingsBox(self, self._http_api, self._settings)
|
||||
box.pack_start(self._recordings_box, True, True, 0)
|
||||
self._player_box.connect("play", self._recordings_box.on_playback)
|
||||
self._player_box.connect("playback-close", self._recordings_box.on_playback_close)
|
||||
|
||||
def on_ftp_realize(self, box):
|
||||
self._ftp_client = FtpClientBox(self, self._settings)
|
||||
@@ -2237,9 +2242,9 @@ class Application(Gtk.Application):
|
||||
view.do_unselect_all(view)
|
||||
elif ctrl and model_name == self.FAV_MODEL_NAME:
|
||||
if key is KeyboardKey.P:
|
||||
self.on_play_stream()
|
||||
self.emit("fav-clicked", FavClickMode.STREAM)
|
||||
if key is KeyboardKey.W:
|
||||
self.on_zap(self.on_watch)
|
||||
self.emit("fav-clicked", FavClickMode.ZAP_PLAY)
|
||||
if key is KeyboardKey.Z:
|
||||
self.on_zap()
|
||||
elif key is KeyboardKey.CTRL_L or key is KeyboardKey.CTRL_R:
|
||||
@@ -2347,16 +2352,10 @@ class Application(Gtk.Application):
|
||||
if self._fav_click_mode is FavClickMode.DISABLED:
|
||||
return
|
||||
|
||||
self._fav_view.set_sensitive(False)
|
||||
|
||||
if self._fav_click_mode is FavClickMode.STREAM:
|
||||
self.on_play_stream()
|
||||
elif self._fav_click_mode is FavClickMode.ZAP_PLAY:
|
||||
self.on_zap(self.on_watch)
|
||||
elif self._fav_click_mode is FavClickMode.ZAP:
|
||||
if self._fav_click_mode is FavClickMode.ZAP:
|
||||
self.on_zap()
|
||||
elif self._fav_click_mode is FavClickMode.PLAY:
|
||||
self.on_stream()
|
||||
else:
|
||||
self.emit("fav-clicked", self._fav_click_mode)
|
||||
else:
|
||||
return self.on_view_popup_menu(menu, event)
|
||||
|
||||
@@ -2597,237 +2596,31 @@ class Application(Gtk.Application):
|
||||
""" Shows backup tool dialog """
|
||||
BackupDialog(self._main_window, self._settings, self.open_data).show()
|
||||
|
||||
# ***************** Player ********************* #
|
||||
# ************************* Streams ***************************** #
|
||||
|
||||
def on_play_stream(self, item=None):
|
||||
self.on_player_play()
|
||||
self.emit("fav-clicked", FavClickMode.STREAM)
|
||||
|
||||
def on_player_play(self, item=None):
|
||||
path, column = self._fav_view.get_cursor()
|
||||
if path:
|
||||
row = self._fav_model[path][:]
|
||||
if row[Column.FAV_TYPE] != BqServiceType.IPTV.name:
|
||||
self.show_error_message("Not allowed in this context!")
|
||||
self.set_playback_elms_active()
|
||||
return
|
||||
def on_play_current(self, item=None):
|
||||
""" starts playback of the current channel. """
|
||||
self.emit("play-current", None)
|
||||
|
||||
url = get_iptv_url(row, self._s_type)
|
||||
self.update_player_buttons()
|
||||
if not url:
|
||||
self.show_error_message("No reference is present!")
|
||||
self.set_playback_elms_active()
|
||||
return
|
||||
self.play(url)
|
||||
|
||||
def play(self, url):
|
||||
mode = self._settings.play_streams_mode
|
||||
if mode is PlayStreamsMode.M3U:
|
||||
self.save_stream_to_m3u(url)
|
||||
return
|
||||
|
||||
if self._player and self._player.get_play_mode() is not mode:
|
||||
self.show_error_message("Play mode has been changed!\nRestart the program to apply the settings.")
|
||||
self.set_playback_elms_active()
|
||||
return
|
||||
|
||||
if mode is PlayStreamsMode.WINDOW:
|
||||
try:
|
||||
if not self._player:
|
||||
self._current_mrl = url
|
||||
self.show_playback_window()
|
||||
elif self._playback_window:
|
||||
title = self.get_playback_title()
|
||||
self._playback_window.set_title(title)
|
||||
self._playback_window.show()
|
||||
GLib.idle_add(self._player.play, url)
|
||||
else:
|
||||
self.show_error_message("Init player error!")
|
||||
finally:
|
||||
self.set_playback_elms_active()
|
||||
else:
|
||||
if not self._player:
|
||||
self._current_mrl = url
|
||||
else:
|
||||
if not self._player_box.get_visible():
|
||||
self.set_player_area_size(self._player_box)
|
||||
|
||||
GLib.idle_add(self._player.play, url)
|
||||
|
||||
self._player_box.set_visible(True)
|
||||
self._stack.set_visible_child(self._player_box)
|
||||
|
||||
def on_player_stop(self, item=None):
|
||||
if self._player:
|
||||
self._fav_view.set_sensitive(True)
|
||||
self._player.stop()
|
||||
|
||||
def on_player_previous(self, item):
|
||||
if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, -1):
|
||||
self.set_player_action()
|
||||
|
||||
def on_player_next(self, item):
|
||||
if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, 1):
|
||||
self.set_player_action()
|
||||
|
||||
@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:
|
||||
self.on_zap(self.on_watch)
|
||||
elif self._fav_click_mode is FavClickMode.STREAM:
|
||||
self.on_play_stream()
|
||||
|
||||
def on_player_rewind(self, scale, scroll_type, value):
|
||||
self._player.set_time(int(value))
|
||||
|
||||
def update_player_buttons(self):
|
||||
if self._player:
|
||||
path, column = self._fav_view.get_cursor()
|
||||
current_index = path[0]
|
||||
self._player_prev_button.set_sensitive(current_index != 0)
|
||||
self._player_next_button.set_sensitive(len(self._fav_model) != current_index + 1)
|
||||
|
||||
def on_player_close(self, window=None, event=None):
|
||||
if self._player:
|
||||
GLib.idle_add(self._player.stop)
|
||||
|
||||
self.set_playback_elms_active()
|
||||
if self._playback_window:
|
||||
self._settings.add("playback_window_size", self._playback_window.get_size())
|
||||
self._playback_window.hide()
|
||||
else:
|
||||
GLib.idle_add(self._player_box.set_visible, False, priority=GLib.PRIORITY_LOW)
|
||||
return True
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def on_player_duration_changed(self, duration):
|
||||
self._player_scale.set_value(0)
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
@run_with_delay(2)
|
||||
def on_player_error(self):
|
||||
self.set_playback_elms_active()
|
||||
self.show_error_message("Can't Playback!")
|
||||
|
||||
@run_idle
|
||||
def set_playback_elms_active(self):
|
||||
self._fav_view.set_sensitive(True)
|
||||
self._fav_view.do_grab_focus(self._fav_view)
|
||||
|
||||
def get_time_str(self, duration):
|
||||
""" Returns a string representation of time from duration in milliseconds """
|
||||
m, s = divmod(duration // 1000, 60)
|
||||
h, m = divmod(m, 60)
|
||||
return "{}{:02d}:{:02d}".format(str(h) + ":" if h else "", m, s)
|
||||
|
||||
def on_player_box_realize(self, widget):
|
||||
if not self._player:
|
||||
try:
|
||||
self._player = Player.make(name=self._settings.stream_lib,
|
||||
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_message(str(e))
|
||||
return True
|
||||
else:
|
||||
self._main_window.connect("key-press-event", self.on_player_key_press)
|
||||
self._player.play(self._current_mrl)
|
||||
finally:
|
||||
if self._settings.play_streams_mode is PlayStreamsMode.BUILT_IN:
|
||||
self.set_player_area_size(widget)
|
||||
|
||||
def on_player_box_visibility(self, box):
|
||||
visible = box.get_visible()
|
||||
self._fav_paned.set_orientation(Gtk.Orientation.VERTICAL if visible else Gtk.Orientation.HORIZONTAL)
|
||||
|
||||
@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_press(self, area, event):
|
||||
if event.button == Gdk.BUTTON_PRIMARY:
|
||||
if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS:
|
||||
self.on_full_screen()
|
||||
|
||||
def on_player_key_press(self, widget, event):
|
||||
if self._player and self._player_event_box.get_visible():
|
||||
key = event.keyval
|
||||
if any((key == Gdk.KEY_F11, key == Gdk.KEY_f, self._full_screen and key == Gdk.KEY_Escape)):
|
||||
self.on_full_screen()
|
||||
|
||||
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 update_state_on_full_screen(self, visible):
|
||||
self._player_tool_bar.set_visible(visible)
|
||||
self._fav_paned.set_visible(visible)
|
||||
self._status_bar_box.set_visible(visible and not self._app_info_box.get_visible())
|
||||
def on_playback_full_screen(self, box, state):
|
||||
self._data_paned.set_visible(state)
|
||||
self._main_window.unfullscreen() if state else self._main_window.fullscreen()
|
||||
if not IS_GNOME_SESSION:
|
||||
self._main_window.set_show_menubar(visible)
|
||||
self._main_window.set_show_menubar(state)
|
||||
|
||||
def on_main_window_state(self, window, event):
|
||||
if event.new_window_state & Gdk.WindowState.FULLSCREEN or event.new_window_state & Gdk.WindowState.MAXIMIZED:
|
||||
# Saving the current size of the application window.
|
||||
self._settings.add("window_size", self._main_window.get_size())
|
||||
def on_playback_show(self, box):
|
||||
if self._page is not Page.RECORDINGS and self._settings.play_streams_mode is PlayStreamsMode.BUILT_IN:
|
||||
self._stack.set_visible(False)
|
||||
self._fav_paned.set_orientation(Gtk.Orientation.VERTICAL)
|
||||
|
||||
@run_idle
|
||||
def show_playback_window(self):
|
||||
width, height = 480, 240
|
||||
size = self._settings.get("playback_window_size")
|
||||
if size:
|
||||
width, height = size
|
||||
|
||||
self._playback_window = Gtk.Window(title=self.get_playback_title(),
|
||||
window_position=Gtk.WindowPosition.CENTER,
|
||||
gravity=Gdk.Gravity.CENTER,
|
||||
icon_name="demon-editor")
|
||||
self._playback_window.resize(width, height)
|
||||
self._playback_window.connect("delete-event", self.on_player_close)
|
||||
self._playback_window.connect("key-press-event", self.on_player_key_press)
|
||||
|
||||
box = Gtk.HBox(visible=True, orientation="vertical")
|
||||
self._player_event_box.reparent(box)
|
||||
self._playback_window.bind_property("visible", self._player_event_box, "visible")
|
||||
|
||||
if not self._settings.is_darwin:
|
||||
self._player_prev_button.set_visible(False)
|
||||
self._player_next_button.set_visible(False)
|
||||
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()
|
||||
|
||||
def get_playback_title(self):
|
||||
path, column = self._fav_view.get_cursor()
|
||||
if path:
|
||||
return "DemonEditor [{}]".format(self._fav_model[path][:][Column.FAV_SERVICE])
|
||||
return "DemonEditor [Playback]"
|
||||
|
||||
# ************************* Record ***************************** #
|
||||
def on_playback_close(self, box, state):
|
||||
self._fav_view.set_sensitive(True)
|
||||
self._stack.set_visible(True)
|
||||
self._fav_paned.set_orientation(Gtk.Orientation.HORIZONTAL)
|
||||
|
||||
def on_record(self, button):
|
||||
if show_dialog(DialogType.QUESTION, self._main_window) == Gtk.ResponseType.CANCEL:
|
||||
@@ -2845,7 +2638,7 @@ class Application(Gtk.Application):
|
||||
if is_record:
|
||||
self._recorder.stop()
|
||||
else:
|
||||
self._http_api.send(HttpAPI.Request.STREAM_CURRENT, None, self.record)
|
||||
self._http_api.send(HttpAPI.Request.STREAM_CURRENT, "", self.record)
|
||||
|
||||
def record(self, data):
|
||||
url = self.get_url_from_m3u(data)
|
||||
@@ -2898,39 +2691,10 @@ class Application(Gtk.Application):
|
||||
elif self._links_transmitter:
|
||||
self._links_transmitter.show(enable)
|
||||
|
||||
def on_stream(self, item=None):
|
||||
path, column = self._fav_view.get_cursor()
|
||||
if not path or not self._http_api:
|
||||
return
|
||||
|
||||
ref = self.get_service_ref(path)
|
||||
if not ref:
|
||||
return
|
||||
|
||||
if self._player and self._player.is_playing():
|
||||
self._player.stop()
|
||||
|
||||
self._http_api.send(HttpAPI.Request.STREAM, ref, self.watch)
|
||||
|
||||
def on_watch(self, item=None):
|
||||
""" 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)
|
||||
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, "", self.watch)
|
||||
|
||||
def watch(self, data):
|
||||
url = self.get_url_from_m3u(data)
|
||||
if url:
|
||||
GLib.timeout_add_seconds(1, self.play, url)
|
||||
|
||||
def get_url_from_m3u(self, data):
|
||||
error_code = data.get("error_code", 0)
|
||||
if error_code or self._http_status_image.get_visible():
|
||||
self.show_error_message("No connection to the receiver!")
|
||||
self.set_playback_elms_active()
|
||||
return
|
||||
|
||||
m3u = data.get("m3u", None)
|
||||
@@ -2938,6 +2702,10 @@ class Application(Gtk.Application):
|
||||
return [s for s in m3u.split("\n") if not s.startswith("#")][0]
|
||||
|
||||
def save_stream_to_m3u(self, url):
|
||||
if self._page not in self._fav_pages:
|
||||
self.show_error_message("Not allowed in this context!")
|
||||
return
|
||||
|
||||
path, column = self._fav_view.get_cursor()
|
||||
s_name = self._fav_model.get_value(self._fav_model.get_iter(path), Column.FAV_SERVICE) if path else "stream"
|
||||
|
||||
@@ -2960,20 +2728,18 @@ class Application(Gtk.Application):
|
||||
""" Switch(zap) the channel """
|
||||
path, column = self._fav_view.get_cursor()
|
||||
if not path or not self._http_api:
|
||||
self.set_playback_elms_active()
|
||||
return
|
||||
|
||||
ref = self.get_service_ref(path)
|
||||
if not ref:
|
||||
return
|
||||
|
||||
if self._player and self._player.is_playing():
|
||||
self._player.stop()
|
||||
self._player_box.on_stop()
|
||||
|
||||
# IPTV type checking
|
||||
row = self._fav_model[path][:]
|
||||
if row[Column.FAV_TYPE] == BqServiceType.IPTV.name and callback:
|
||||
callback = self.play(get_iptv_url(row, self._s_type))
|
||||
callback = self._player_box.play(get_iptv_url(row, self._s_type))
|
||||
|
||||
def zap(rq):
|
||||
if rq and rq.get("e2state", False):
|
||||
@@ -2982,7 +2748,6 @@ class Application(Gtk.Application):
|
||||
callback()
|
||||
else:
|
||||
self.show_error_message("No connection to the receiver!")
|
||||
self.set_playback_elms_active()
|
||||
|
||||
self._http_api.send(HttpAPI.Request.ZAP, ref, zap)
|
||||
|
||||
@@ -2992,7 +2757,6 @@ class Application(Gtk.Application):
|
||||
|
||||
if srv_type in self._marker_types:
|
||||
self.show_error_message("Not allowed in this context!")
|
||||
self.set_playback_elms_active()
|
||||
return
|
||||
|
||||
srv = self._services.get(fav_id, None)
|
||||
@@ -3666,6 +3430,10 @@ class Application(Gtk.Application):
|
||||
def app_settings(self):
|
||||
return self._settings
|
||||
|
||||
@property
|
||||
def http_api(self):
|
||||
return self._http_api
|
||||
|
||||
@GObject.Property(type=bool, default=True)
|
||||
def is_enigma(self):
|
||||
return self._is_enigma
|
||||
|
||||
@@ -218,6 +218,9 @@ class PiconManager(Gtk.Box):
|
||||
if index % factor == 0:
|
||||
yield True
|
||||
|
||||
if not os.path.isdir(path):
|
||||
return
|
||||
|
||||
for file in os.listdir(path):
|
||||
if self._terminate:
|
||||
return
|
||||
|
||||
157
app/ui/playback.glade
Normal file
157
app/ui/playback.glade
Normal file
@@ -0,0 +1,157 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<object class="GtkEventBox" id="event_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<signal name="button-press-event" handler="on_press" swapped="no"/>
|
||||
<signal name="realize" handler="on_realize" swapped="no"/>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkToolbar" id="tool_bar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="prev_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Previous stream in the list</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="stock_id">gtk-media-previous</property>
|
||||
<signal name="clicked" handler="on_previous" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="play_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Play</property>
|
||||
<property name="stock_id">gtk-media-play</property>
|
||||
<signal name="clicked" handler="on_play" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="stop_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Stop playback</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="stock_id">gtk-media-stop</property>
|
||||
<signal name="clicked" handler="on_stop" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="next_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Next stream in the list</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="stock_id">gtk-media-next</property>
|
||||
<signal name="clicked" handler="on_next" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolItem" id="player_item">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="rewind_box">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="current_time_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScale" id="scale">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="restrict_to_fill_level">False</property>
|
||||
<property name="fill_level">0</property>
|
||||
<property name="draw_value">False</property>
|
||||
<property name="has_origin">False</property>
|
||||
<signal name="change-value" handler="on_rewind" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="full_time_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="homogeneous">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="full_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Toggle in fullscreen</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="stock_id">gtk-fullscreen</property>
|
||||
<signal name="clicked" handler="on_full_screen" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="close_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Close playback</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="stock_id">gtk-close</property>
|
||||
<signal name="clicked" handler="on_close" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
331
app/ui/playback.py
Normal file
331
app/ui/playback.py
Normal file
@@ -0,0 +1,331 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Additional module for playback. """
|
||||
from functools import lru_cache
|
||||
|
||||
from gi.repository import GLib, GObject
|
||||
|
||||
from app.commons import run_idle, run_with_delay
|
||||
from app.connections import HttpAPI
|
||||
from app.eparser.ecommons import BqServiceType
|
||||
from app.settings import PlayStreamsMode, IS_DARWIN
|
||||
from app.tools.media import Player
|
||||
from app.ui.dialogs import get_builder
|
||||
from app.ui.main_helper import get_iptv_url
|
||||
from app.ui.uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, Column
|
||||
|
||||
|
||||
class PlayerBox(Gtk.Box):
|
||||
|
||||
def __init__(self, app, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Signals.
|
||||
GObject.signal_new("playback-full-screen", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("playback-close", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("play", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("stop", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
|
||||
self._app = app
|
||||
self._app.connect("fav-clicked", self.on_fav_clicked)
|
||||
self._app.connect("page-changed", self.on_page_changed)
|
||||
self._app.connect("play-current", self.on_play_current)
|
||||
self._app.connect("play-recording", self.on_play_recording)
|
||||
self._fav_view = app.fav_view
|
||||
self._player = None
|
||||
self._current_mrl = None
|
||||
self._full_screen = False
|
||||
self._playback_window = None
|
||||
self._play_mode = self._app.app_settings.play_streams_mode
|
||||
|
||||
handlers = {"on_realize": self.on_realize,
|
||||
"on_press": self.on_press,
|
||||
"on_play": self.on_play,
|
||||
"on_stop": self.on_stop,
|
||||
"on_next": self.on_next,
|
||||
"on_previous": self.on_previous,
|
||||
"on_rewind": self.on_rewind,
|
||||
"on_full_screen": self.on_full_screen,
|
||||
"on_close": self.on_close}
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "playback.glade", handlers)
|
||||
self.set_spacing(5)
|
||||
self.set_orientation(Gtk.Orientation.VERTICAL)
|
||||
self._event_box = builder.get_object("event_box")
|
||||
self.pack_start(self._event_box, True, True, 0)
|
||||
self.pack_end(builder.get_object("tool_bar"), False, True, 0)
|
||||
|
||||
self._scale = builder.get_object("scale")
|
||||
self._full_time_label = builder.get_object("full_time_label")
|
||||
self._current_time_label = builder.get_object("current_time_label")
|
||||
self._rewind_box = builder.get_object("rewind_box")
|
||||
self._tool_bar = builder.get_object("tool_bar")
|
||||
self._prev_button = builder.get_object("prev_button")
|
||||
self._next_button = builder.get_object("next_button")
|
||||
self._play_button = builder.get_object("play_button")
|
||||
self._fav_view.bind_property("sensitive", self._prev_button, "sensitive")
|
||||
self._fav_view.bind_property("sensitive", self._next_button, "sensitive")
|
||||
|
||||
self.connect("delete-event", self.on_delete)
|
||||
self.connect("show", self.set_player_area_size)
|
||||
|
||||
def on_fav_clicked(self, app, mode):
|
||||
if mode is not FavClickMode.STREAM and not self._app.http_api:
|
||||
return
|
||||
|
||||
self._fav_view.set_sensitive(False)
|
||||
if mode is FavClickMode.STREAM:
|
||||
self.on_play_stream()
|
||||
elif mode is FavClickMode.ZAP_PLAY:
|
||||
self._app.on_zap(self.on_watch)
|
||||
elif mode is FavClickMode.PLAY:
|
||||
self.on_play_service()
|
||||
|
||||
def on_play_current(self, app, url):
|
||||
self.on_watch()
|
||||
|
||||
def on_play_recording(self, app, url):
|
||||
self.play(url)
|
||||
|
||||
def on_page_changed(self, app, page):
|
||||
self.on_close()
|
||||
self.set_visible(False)
|
||||
|
||||
def on_realize(self, box):
|
||||
if not self._player:
|
||||
settings = self._app.app_settings
|
||||
try:
|
||||
self._player = Player.make(settings.stream_lib, settings.play_streams_mode, self._event_box)
|
||||
self._player.connect("error", self.on_error)
|
||||
self._player.connect("played", self.on_played)
|
||||
self._player.connect("position", self.on_time_changed)
|
||||
except (ImportError, NameError) as e:
|
||||
self._app.show_error_message(str(e))
|
||||
return True
|
||||
else:
|
||||
self._app.app_window.connect("key-press-event", self.on_key_press)
|
||||
self.emit("play", self._current_mrl)
|
||||
finally:
|
||||
if settings.play_streams_mode is PlayStreamsMode.BUILT_IN:
|
||||
self.set_player_area_size(box)
|
||||
|
||||
def on_play(self, button=None):
|
||||
self.emit("play", None)
|
||||
|
||||
def on_stop(self, button=None):
|
||||
self.emit("stop", None)
|
||||
|
||||
def on_next(self, button):
|
||||
if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, 1):
|
||||
self.set_player_action()
|
||||
|
||||
def on_previous(self, button):
|
||||
if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, -1):
|
||||
self.set_player_action()
|
||||
|
||||
def on_rewind(self, scale, scroll_type, value):
|
||||
self._player.set_time(int(value))
|
||||
|
||||
def on_full_screen(self, item=None):
|
||||
self._full_screen = not self._full_screen
|
||||
if self._play_mode is PlayStreamsMode.BUILT_IN:
|
||||
self._tool_bar.set_visible(not self._full_screen)
|
||||
self.emit("playback-full-screen", not self._full_screen)
|
||||
elif self._playback_window:
|
||||
self._tool_bar.set_visible(not self._full_screen)
|
||||
self._playback_window.fullscreen() if self._full_screen else self._playback_window.unfullscreen()
|
||||
|
||||
def on_close(self, action=None, value=None):
|
||||
if self._playback_window:
|
||||
self._app.app_settings.add("playback_window_size", self._playback_window.get_size())
|
||||
self._playback_window.hide()
|
||||
|
||||
self.on_stop()
|
||||
self.hide()
|
||||
self.emit("playback-close", None)
|
||||
|
||||
return True
|
||||
|
||||
def on_press(self, area, event):
|
||||
if event.button == Gdk.BUTTON_PRIMARY:
|
||||
if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS:
|
||||
self.on_full_screen()
|
||||
|
||||
def on_key_press(self, widget, event):
|
||||
if self._player and self.get_visible():
|
||||
key = event.keyval
|
||||
if any((key == Gdk.KEY_F11, key == Gdk.KEY_f, self._full_screen and key == Gdk.KEY_Escape)):
|
||||
self.on_full_screen()
|
||||
|
||||
def on_delete(self, box):
|
||||
if self._player:
|
||||
self._player.release()
|
||||
|
||||
@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_play_service()
|
||||
elif self._fav_click_mode is FavClickMode.ZAP_PLAY:
|
||||
self.on_zap(self.on_watch)
|
||||
elif self._fav_click_mode is FavClickMode.STREAM:
|
||||
self.on_play_stream()
|
||||
|
||||
def update_buttons(self):
|
||||
if self._player:
|
||||
path, column = self._fav_view.get_cursor()
|
||||
current_index = path[0]
|
||||
self._player_prev_button.set_sensitive(current_index != 0)
|
||||
self._player_next_button.set_sensitive(len(self._fav_model) != current_index + 1)
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def on_duration_changed(self, duration):
|
||||
self._scale.set_value(0)
|
||||
self._scale.get_adjustment().set_upper(duration)
|
||||
GLib.idle_add(self._rewind_box.set_visible, duration > 0, priority=GLib.PRIORITY_LOW)
|
||||
GLib.idle_add(self._current_time_label.set_text, "0", priority=GLib.PRIORITY_LOW)
|
||||
GLib.idle_add(self._full_time_label.set_text, self.get_time_str(duration),
|
||||
priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def on_time_changed(self, widget, t):
|
||||
if not self._full_screen and self._rewind_box.get_visible():
|
||||
GLib.idle_add(self._current_time_label.set_text, self.get_time_str(t),
|
||||
priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def get_time_str(self, duration):
|
||||
""" Returns a string representation of time from duration in milliseconds """
|
||||
m, s = divmod(duration // 1000, 60)
|
||||
h, m = divmod(m, 60)
|
||||
return "{}{:02d}:{:02d}".format(str(h) + ":" if h else "", m, s)
|
||||
|
||||
def set_player_area_size(self, widget):
|
||||
w, h = self._app.app_window.get_size()
|
||||
widget.set_size_request(w * 0.6, -1)
|
||||
|
||||
@run_idle
|
||||
def show_playback_window(self):
|
||||
width, height = 480, 240
|
||||
size = self._app.app_settings.get("playback_window_size")
|
||||
if size:
|
||||
width, height = size
|
||||
|
||||
if self._playback_window:
|
||||
self._playback_window.show()
|
||||
else:
|
||||
self._playback_window = Gtk.Window(title=self.get_playback_title(),
|
||||
window_position=Gtk.WindowPosition.CENTER,
|
||||
icon_name="demon-editor")
|
||||
|
||||
self._playback_window.connect("delete-event", self.on_close)
|
||||
self._playback_window.connect("key-press-event", self.on_key_press)
|
||||
self._playback_window.bind_property("visible", self._event_box, "visible")
|
||||
|
||||
if not IS_DARWIN:
|
||||
self._prev_button.set_visible(False)
|
||||
self._next_button.set_visible(False)
|
||||
|
||||
self.reparent(self._playback_window)
|
||||
self._playback_window.set_application(self._app)
|
||||
|
||||
self.show()
|
||||
self._playback_window.resize(width, height)
|
||||
self._playback_window.show()
|
||||
|
||||
def get_playback_title(self):
|
||||
path, column = self._fav_view.get_cursor()
|
||||
if path:
|
||||
return "DemonEditor [{}]".format(self._app.fav_view.get_model()[path][:][Column.FAV_SERVICE])
|
||||
return "DemonEditor [Playback]"
|
||||
|
||||
def on_play_stream(self):
|
||||
path, column = self._fav_view.get_cursor()
|
||||
if path:
|
||||
row = self._fav_view.get_model()[path][:]
|
||||
if row[Column.FAV_TYPE] != BqServiceType.IPTV.name:
|
||||
self.on_error(None, "Not allowed in this context!")
|
||||
return
|
||||
|
||||
url = get_iptv_url(row, self._app.app_settings.setting_type)
|
||||
self.play(url) if url else self.on_error(None, "No reference is present!")
|
||||
|
||||
def on_play_service(self, item=None):
|
||||
path, column = self._fav_view.get_cursor()
|
||||
if not path or not self._app.http_api:
|
||||
return
|
||||
|
||||
ref = self._app.get_service_ref(path)
|
||||
if not ref:
|
||||
return
|
||||
|
||||
if self._player and self._player.is_playing():
|
||||
self.emit("stop", None)
|
||||
|
||||
self._app.http_api.send(HttpAPI.Request.STREAM, ref, self.watch)
|
||||
|
||||
def on_watch(self, item=None):
|
||||
""" Switch to the channel and watch in the player. """
|
||||
self._app.http_api.send(HttpAPI.Request.STREAM_CURRENT, "", self.watch)
|
||||
|
||||
def watch(self, data):
|
||||
url = self._app.get_url_from_m3u(data)
|
||||
GLib.timeout_add_seconds(1, self.play, url) if url else self.on_error(None, "Can't Playback!")
|
||||
|
||||
def play(self, url):
|
||||
if self._play_mode is PlayStreamsMode.M3U:
|
||||
self._app.save_stream_to_m3u(url)
|
||||
return
|
||||
|
||||
if self._play_mode is not self._app.app_settings.play_streams_mode:
|
||||
self.on_error(None, "Play mode has been changed!\nRestart the program to apply the settings.")
|
||||
return
|
||||
|
||||
if self._play_mode is PlayStreamsMode.BUILT_IN:
|
||||
self.show()
|
||||
elif self._play_mode is PlayStreamsMode.WINDOW:
|
||||
self.show_playback_window()
|
||||
|
||||
if self._player:
|
||||
self.emit("play", url)
|
||||
else:
|
||||
self._current_mrl = url
|
||||
|
||||
def on_played(self, player, duration):
|
||||
GLib.idle_add(self._fav_view.set_sensitive, True)
|
||||
self.on_duration_changed(duration)
|
||||
|
||||
def on_error(self, player, msg):
|
||||
self._app.show_error_message(msg)
|
||||
self._fav_view.set_sensitive(True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
@@ -125,7 +125,6 @@ class Page(Enum):
|
||||
SERVICES = "services"
|
||||
SATELLITE = "satellite"
|
||||
PICONS = "picons"
|
||||
PLAYBACK = "playback"
|
||||
EPG = "epg"
|
||||
TIMERS = "timers"
|
||||
RECORDINGS = "recordings"
|
||||
@@ -210,6 +209,13 @@ class Column(IntEnum):
|
||||
ALT_FAV_ID = 5
|
||||
ALT_ID = 6
|
||||
ALT_ITER = 7
|
||||
# Recordings view
|
||||
REC_SERVICE = 0
|
||||
REC_TITLE = 1
|
||||
REC_TIME = 2
|
||||
REC_LEN = 3
|
||||
REC_FILE = 4
|
||||
REC_DESC = 5
|
||||
|
||||
def __index__(self):
|
||||
""" Overridden to get the index in slices directly """
|
||||
|
||||
Reference in New Issue
Block a user