basic implementation of the play mode

This commit is contained in:
DYefremov
2020-03-28 17:56:39 +03:00
parent ebcf0a90b5
commit f53f483dce
4 changed files with 191 additions and 34 deletions

View File

@@ -1,12 +1,17 @@
import os import os
import subprocess
import sys import sys
from datetime import datetime from datetime import datetime
from enum import Enum
from urllib.request import urlopen
from app.commons import run_task, log, _DATE_FORMAT from app.commons import run_task, log, _DATE_FORMAT
from app.settings import PlayStreamsMode
class Player: class Player:
__VLC_INSTANCE = None __VLC_INSTANCE = None
__PLAY_STREAMS_MODE = PlayStreamsMode.BUILT_IN
def __init__(self, rewind_callback, position_callback, error_callback, playing_callback): def __init__(self, rewind_callback, position_callback, error_callback, playing_callback):
try: try:
@@ -46,6 +51,10 @@ class Player:
cls.__VLC_INSTANCE = Player(rewind_callback, position_callback, error_callback, playing_callback) cls.__VLC_INSTANCE = Player(rewind_callback, position_callback, error_callback, playing_callback)
return cls.__VLC_INSTANCE return cls.__VLC_INSTANCE
@staticmethod
def get_play_mode():
return Player.__PLAY_STREAMS_MODE
@run_task @run_task
def play(self, mrl=None): def play(self, mrl=None):
if mrl: if mrl:
@@ -105,6 +114,90 @@ class Player:
self._player.set_fullscreen(full) self._player.set_fullscreen(full)
class HttpPlayer:
""" Simple wrapper for VLC media player to interact over http. """
__VLC_INSTANCE = None
__PLAY_STREAMS_MODE = PlayStreamsMode.VLC
class Commands(Enum):
STATUS = "http://127.0.0.1:{}/requests/status.xml"
PLAY = "http://127.0.0.1:{}/requests/status.xml?command=in_play&input={}"
STOP = "http://127.0.0.1:{}/requests/status.xml?command=pl_stop"
CLEAR = "http://127.0.0.1:{}/requests/status.xml?command=pl_empty"
def __init__(self, exe, port):
from concurrent.futures import ThreadPoolExecutor as PoolExecutor
self._executor = PoolExecutor(max_workers=1)
self._cmd = [exe, "--no-stats", "--verbose=-1", "--extraintf", "http", "--http-port", port,
"--no-playlist-skip-ads", "--one-instance", "--quiet"]
self._p = None
self._state = None
self._port = port
@classmethod
def get_instance(cls, settings):
if not cls.__VLC_INSTANCE:
import shutil
is_darwin = settings.is_darwin
# TODO Add options[vlc_exe and port] to the settings!
exe = "/Applications/VLC.app/Contents/MacOS/VLC" if is_darwin else "/usr/bin/vlc"
if shutil.which(exe) is None:
raise ImportError
cls.__VLC_INSTANCE = HttpPlayer(exe=exe, port=str(9090))
return cls.__VLC_INSTANCE
@staticmethod
def get_play_mode():
return HttpPlayer.__PLAY_STREAMS_MODE
@run_task
def play(self, mrl=None):
if not self._p or self._p and self._p.poll() is not None:
self._p = subprocess.Popen(self._cmd + [mrl], preexec_fn=os.setsid)
self._p.communicate()
else:
self._executor.submit(self.open_command, self.Commands.CLEAR)
self._executor.submit(self.open_command, self.Commands.PLAY, mrl)
def open_command(self, command, url=None):
if command is self.Commands.PLAY:
url = self.Commands.PLAY.value.format(self._port, url)
else:
url = command.value.format(self._port)
try:
with urlopen(url, timeout=5) as f:
self._state = command
except Exception as e:
log("{}[open_command, {}] error: {}".format(__class__.__name__, command, e))
def stop(self):
if self._state is self.Commands.PLAY:
self._executor.submit(self.open_command, self.Commands.STOP)
def pause(self):
pass
def set_time(self, time):
pass
@run_task
def release(self):
if self._p and self._p.poll() is None:
import signal
# Good explanation here: https://stackoverflow.com/a/4791612
os.killpg(os.getpgid(self._p.pid), signal.SIGTERM)
def is_playing(self):
return self._state is self.Commands.PLAY
def set_full_screen(self, full):
pass
class Recorder: class Recorder:
__VLC_REC_INSTANCE = None __VLC_REC_INSTANCE = None

View File

@@ -8,16 +8,16 @@ from itertools import chain
from gi.repository import GLib, Gio from gi.repository import GLib, Gio
from app.commons import run_idle, log, run_task, run_with_delay, init_logger from app.commons import run_idle, log, run_task, run_with_delay, init_logger
from app.connections import HttpAPI, HttpRequestType, download_data, DownloadType, upload_data, test_http, \ from app.connections import (HttpAPI, HttpRequestType, download_data, DownloadType, upload_data, test_http,
TestException, HttpApiException TestException, HttpApiException)
from app.eparser import get_blacklist, write_blacklist, parse_m3u from app.eparser import get_blacklist, write_blacklist, parse_m3u
from app.eparser import get_services, get_bouquets, write_bouquets, write_services, Bouquets, Bouquet, Service from app.eparser import get_services, get_bouquets, write_bouquets, write_services, Bouquets, Bouquet, Service
from app.eparser.ecommons import CAS, Flag, BouquetService from app.eparser.ecommons import CAS, Flag, BouquetService
from app.eparser.enigma.bouquets import BqServiceType from app.eparser.enigma.bouquets import BqServiceType
from app.eparser.iptv import export_to_m3u from app.eparser.iptv import export_to_m3u
from app.eparser.neutrino.bouquets import BqType from app.eparser.neutrino.bouquets import BqType
from app.settings import SettingsType, Settings, SettingsException from app.settings import SettingsType, Settings, SettingsException, PlayStreamsMode
from app.tools.media import Player, Recorder from app.tools.media import Player, Recorder, HttpPlayer
from app.ui.epg_dialog import EpgDialog from app.ui.epg_dialog import EpgDialog
from app.ui.transmitter import LinksTransmitter from app.ui.transmitter import LinksTransmitter
from .backup import BackupDialog, backup_data, clear_data_path from .backup import BackupDialog, backup_data, clear_data_path
@@ -25,17 +25,17 @@ from .dialogs import show_dialog, DialogType, get_chooser_dialog, WaitDialog, ge
from .download_dialog import DownloadDialog from .download_dialog import DownloadDialog
from .imports import ImportDialog, import_bouquet from .imports import ImportDialog, import_bouquet
from .iptv import IptvDialog, SearchUnavailableDialog, IptvListConfigurationDialog, YtListImportDialog from .iptv import IptvDialog, SearchUnavailableDialog, IptvListConfigurationDialog, YtListImportDialog
from .main_helper import insert_marker, move_items, rename, ViewTarget, set_flags, locate_in_services, \ 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_picon, remove_picon, \ scroll_to, get_base_model, update_picons_data, copy_picon_reference, assign_picon,
is_only_one_item_selected, gen_bouquets, BqGenType, get_iptv_url, append_picons, get_selection, get_model_data, \ remove_picon, is_only_one_item_selected, gen_bouquets, BqGenType, get_iptv_url, append_picons,
remove_all_unused_picons get_selection, get_model_data, remove_all_unused_picons)
from .picons_downloader import PiconsDialog from .picons_downloader import PiconsDialog
from .satellites_dialog import show_satellites_dialog from .satellites_dialog import show_satellites_dialog
from .search import SearchProvider from .search import SearchProvider
from .service_details_dialog import ServiceDetailsDialog, Action from .service_details_dialog import ServiceDetailsDialog, Action
from .settings_dialog import show_settings_dialog from .settings_dialog import show_settings_dialog
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, LOCKED_ICON, HIDE_ICON, IPTV_ICON, MOVE_KEYS, KeyboardKey, Column, \ from .uicommons import (Gtk, Gdk, UI_RESOURCES_PATH, LOCKED_ICON, HIDE_ICON, IPTV_ICON, MOVE_KEYS, KeyboardKey, Column,
FavClickMode FavClickMode)
class Application(Gtk.Application): class Application(Gtk.Application):
@@ -172,7 +172,7 @@ class Application(Gtk.Application):
# Player # Player
self._player = None self._player = None
self._full_screen = False self._full_screen = False
self._drawing_area_xid = None self._current_mrl = None
# Record # Record
self._recorder = None self._recorder = None
# http api # http api
@@ -1000,7 +1000,8 @@ class Application(Gtk.Application):
self.show_error_dialog(str(e)) self.show_error_dialog(str(e))
return return
except Exception as e: except Exception as e:
log("Append services error: " + str(e)) from traceback import format_exc
log("Reading data error: {}".format(format_exc()))
self.show_error_dialog(get_message("Reading data error!") + "\n" + str(e)) self.show_error_dialog(get_message("Reading data error!") + "\n" + str(e))
return return
else: else:
@@ -1525,7 +1526,10 @@ class Application(Gtk.Application):
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS: if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS:
if self._fav_click_mode is FavClickMode.DISABLED: if self._fav_click_mode is FavClickMode.DISABLED:
return return
elif self._fav_click_mode is FavClickMode.STREAM:
self._fav_view.set_sensitive(False)
if self._fav_click_mode is FavClickMode.STREAM:
self.on_play_stream() self.on_play_stream()
elif self._fav_click_mode is FavClickMode.ZAP_PLAY: elif self._fav_click_mode is FavClickMode.ZAP_PLAY:
self.on_zap(self.on_watch) self.on_zap(self.on_watch)
@@ -1700,6 +1704,7 @@ class Application(Gtk.Application):
row = self._fav_model[path][:] row = self._fav_model[path][:]
if row[Column.FAV_TYPE] != BqServiceType.IPTV.name: if row[Column.FAV_TYPE] != BqServiceType.IPTV.name:
self.show_error_dialog("Not allowed in this context!") self.show_error_dialog("Not allowed in this context!")
self.set_playback_elms_active()
return return
url = get_iptv_url(row, self._s_type) url = get_iptv_url(row, self._s_type)
@@ -1709,24 +1714,36 @@ class Application(Gtk.Application):
self.play(url) self.play(url)
def play(self, url): def play(self, url):
if not self._player: mode = self._settings.play_streams_mode
try: if mode is PlayStreamsMode.M3U:
self._player = Player.get_instance(rewind_callback=self.on_player_duration_changed, self.save_stream_to_m3u(url)
position_callback=self.on_player_time_changed,
error_callback=self.on_player_error,
playing_callback=self.set_playback_elms_active)
except (ImportError, NameError, AttributeError):
self.show_error_dialog("No VLC is found. Check that it is installed!")
return return
if mode is PlayStreamsMode.VLC:
try:
if self._player and self._player.get_play_mode() is not mode:
self._player.release()
self._player = None
if not self._player:
self._player = HttpPlayer.get_instance(self._settings)
except ImportError:
self.show_error_dialog("No VLC is found. Check that it is installed!")
else: else:
if self._drawing_area_xid: self._player.play(url)
self._player.set_xwindow(self._drawing_area_xid) finally:
self.set_playback_elms_active()
else:
if not self._player_box.get_visible():
w, h = self._main_window.get_size() w, h = self._main_window.get_size()
self._player_box.set_size_request(w * 0.6, -1) self._player_box.set_size_request(w * 0.6, -1)
self._current_mrl = url
self._player_box.set_visible(True) self._player_box.set_visible(True)
self._fav_view.set_sensitive(False)
if self._player and self._player.get_play_mode() is PlayStreamsMode.BUILT_IN:
GLib.idle_add(self._player.play, url, priority=GLib.PRIORITY_LOW) GLib.idle_add(self._player.play, url, priority=GLib.PRIORITY_LOW)
elif self._player:
self.show_error_dialog("Play mode has been changed!\nRestart the program to apply the settings.")
self.set_playback_elms_active()
def on_player_stop(self, item=None): def on_player_stop(self, item=None):
if self._player: if self._player:
@@ -1794,11 +1811,26 @@ class Application(Gtk.Application):
return "{}{:02d}:{:02d}".format(str(h) + ":" if h else "", m, s) return "{}{:02d}:{:02d}".format(str(h) + ":" if h else "", m, s)
def on_drawing_area_realize(self, widget): def on_drawing_area_realize(self, widget):
if sys.platform == "darwin": w, h = self._main_window.get_size()
widget.set_size_request(w * 0.6, -1)
if not self._player:
try:
self._player = Player.get_instance(rewind_callback=self.on_player_duration_changed,
position_callback=self.on_player_time_changed,
error_callback=self.on_player_error,
playing_callback=self.set_playback_elms_active)
except (ImportError, NameError, AttributeError):
self.show_error_dialog("No VLC is found. Check that it is installed!")
return True
else:
if self._settings.is_darwin:
self._player.set_nso(widget) self._player.set_nso(widget)
else: else:
self._drawing_area_xid = widget.get_window().get_xid() self._player.set_xwindow(widget.get_window().get_xid())
self._player.set_xwindow(self._drawing_area_xid) self._player.play(self._current_mrl)
finally:
self.set_playback_elms_active()
def on_player_drawing_area_draw(self, widget, cr): def on_player_drawing_area_draw(self, widget, cr):
""" Used for black background drawing in the player drawing area. """ Used for black background drawing in the player drawing area.
@@ -1932,17 +1964,37 @@ class Application(Gtk.Application):
error_code = data.get("error_code", 0) error_code = data.get("error_code", 0)
if error_code or self._http_status_image.get_visible(): if error_code or self._http_status_image.get_visible():
self.show_error_dialog("No connection to the receiver!") self.show_error_dialog("No connection to the receiver!")
self.set_playback_elms_active()
return return
m3u = data.get("m3u", None) m3u = data.get("m3u", None)
if m3u: if m3u:
return [s for s in m3u.split("\n") if not s.startswith("#")][0] return [s for s in m3u.split("\n") if not s.startswith("#")][0]
def save_stream_to_m3u(self, url):
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"
try:
response = show_dialog(DialogType.CHOOSER, self._main_window, settings=self._settings)
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
with open("{}{}.m3u".format(response, s_name), "w", encoding="utf-8") as file:
file.writelines("#EXTM3U\n#EXTVLCOPT--http-reconnect=true\n#EXTINF:-1,{}\n{}\n".format(s_name, url))
except IOError as e:
self.show_error_dialog(str(e))
else:
show_dialog(DialogType.INFO, self._main_window, "Done!")
finally:
GLib.idle_add(self._fav_view.set_sensitive, True)
@run_idle @run_idle
def on_zap(self, callback=None): def on_zap(self, callback=None):
""" Switch(zap) the channel """ """ Switch(zap) the channel """
path, column = self._fav_view.get_cursor() path, column = self._fav_view.get_cursor()
if not path or not self._http_api: if not path or not self._http_api:
self.set_playback_elms_active()
return return
ref = self.get_service_ref(path) ref = self.get_service_ref(path)
@@ -1957,6 +2009,9 @@ class Application(Gtk.Application):
GLib.idle_add(scroll_to, path, self._fav_view) GLib.idle_add(scroll_to, path, self._fav_view)
if callback: if callback:
callback() callback()
else:
self.show_error_dialog("No connection to the receiver!")
self.set_playback_elms_active()
self._http_api.send(HttpRequestType.ZAP, ref, zap) self._http_api.send(HttpRequestType.ZAP, ref, zap)
@@ -1966,6 +2021,7 @@ class Application(Gtk.Application):
if srv_type == BqServiceType.IPTV.name or srv_type == BqServiceType.MARKER.name: if srv_type == BqServiceType.IPTV.name or srv_type == BqServiceType.MARKER.name:
self.show_error_dialog("Not allowed in this context!") self.show_error_dialog("Not allowed in this context!")
self.set_playback_elms_active()
return return
srv = self._services.get(fav_id, None) srv = self._services.get(fav_id, None)

View File

@@ -1953,6 +1953,7 @@ Author: Dmitriy Yefremov
<property name="active">True</property> <property name="active">True</property>
<property name="draw_indicator">True</property> <property name="draw_indicator">True</property>
<property name="group">get_m3u_radio_button</property> <property name="group">get_m3u_radio_button</property>
<signal name="toggled" handler="on_play_mode_changed" swapped="no"/>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@@ -1984,7 +1985,6 @@ Author: Dmitriy Yefremov
<property name="receives_default">False</property> <property name="receives_default">False</property>
<property name="active">True</property> <property name="active">True</property>
<property name="draw_indicator">True</property> <property name="draw_indicator">True</property>
<property name="group">play_in_built_radio_button</property>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@@ -1998,7 +1998,7 @@ Author: Dmitriy Yefremov
<object class="GtkLabel" id="play_streams_label"> <object class="GtkLabel" id="play_streams_label">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="label" translatable="yes">Play streams in:</property> <property name="label" translatable="yes">Play streams mode:</property>
</object> </object>
</child> </child>
</object> </object>

View File

@@ -5,7 +5,7 @@ from enum import Enum
from app.commons import run_task, run_idle from app.commons import run_task, run_idle
from app.connections import test_telnet, test_ftp, TestException, test_http, HttpApiException from app.connections import test_telnet, test_ftp, TestException, test_http, HttpApiException
from app.settings import SettingsType, Settings, PlayStreamsMode from app.settings import SettingsType, Settings, PlayStreamsMode
from app.ui.dialogs import show_dialog, DialogType from app.ui.dialogs import show_dialog, DialogType, get_message
from .main_helper import update_entry_data, scroll_to from .main_helper import update_entry_data, scroll_to
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON
@@ -51,6 +51,7 @@ class SettingsDialog:
"on_network_settings_visible": self.on_network_settings_visible, "on_network_settings_visible": self.on_network_settings_visible,
"on_http_use_ssl_toggled": self.on_http_use_ssl_toggled, "on_http_use_ssl_toggled": self.on_http_use_ssl_toggled,
"on_click_mode_togged": self.on_click_mode_togged, "on_click_mode_togged": self.on_click_mode_togged,
"on_play_mode_changed": self.on_play_mode_changed,
"on_transcoding_preset_changed": self.on_transcoding_preset_changed, "on_transcoding_preset_changed": self.on_transcoding_preset_changed,
"on_apply_presets": self.on_apply_presets, "on_apply_presets": self.on_apply_presets,
"on_digit_entry_changed": self.on_digit_entry_changed, "on_digit_entry_changed": self.on_digit_entry_changed,
@@ -381,7 +382,7 @@ class SettingsDialog:
def show_info_message(self, text, message_type): def show_info_message(self, text, message_type):
self._info_bar.set_visible(True) self._info_bar.set_visible(True)
self._info_bar.set_message_type(message_type) self._info_bar.set_message_type(message_type)
self._message_label.set_text(text) self._message_label.set_text(get_message(text))
@run_idle @run_idle
def show_spinner(self, show): def show_spinner(self, show):
@@ -547,6 +548,13 @@ class SettingsDialog:
return FavClickMode.DISABLED return FavClickMode.DISABLED
def on_play_mode_changed(self, button):
if self._main_stack.get_visible_child_name() != "streaming":
return
if button.get_active():
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
@run_idle @run_idle
def set_play_stream_mode(self, mode): def set_play_stream_mode(self, mode):
self._play_in_built_radio_button.set_sensitive(not self._settings.is_darwin) self._play_in_built_radio_button.set_sensitive(not self._settings.is_darwin)