Refactored mplayer and omxplayer under the same media player

This commit is contained in:
Fabio Manganiello 2019-02-02 00:06:28 +01:00
parent 56b6e6a899
commit 63b423cf75
13 changed files with 400 additions and 611 deletions

View file

@ -0,0 +1,6 @@
``platypush.plugins.media.omxplayer``
=====================================
.. automodule:: platypush.plugins.media.omxplayer
:members:

View file

@ -1,6 +0,0 @@
``platypush.plugins.video.omxplayer``
=====================================
.. automodule:: platypush.plugins.video.omxplayer
:members:

View file

@ -33,6 +33,7 @@ Plugins
platypush/plugins/light.hue.rst platypush/plugins/light.hue.rst
platypush/plugins/light.rst platypush/plugins/light.rst
platypush/plugins/media.kodi.rst platypush/plugins/media.kodi.rst
platypush/plugins/media.omxplayer.rst
platypush/plugins/midi.rst platypush/plugins/midi.rst
platypush/plugins/mqtt.rst platypush/plugins/mqtt.rst
platypush/plugins/music.mpd.rst platypush/plugins/music.mpd.rst
@ -51,7 +52,6 @@ Plugins
platypush/plugins/tts.rst platypush/plugins/tts.rst
platypush/plugins/utils.rst platypush/plugins/utils.rst
platypush/plugins/variable.rst platypush/plugins/variable.rst
platypush/plugins/video.omxplayer.rst
platypush/plugins/weather.forecast.rst platypush/plugins/weather.forecast.rst
platypush/plugins/websocket.rst platypush/plugins/websocket.rst
platypush/plugins/wiimote.rst platypush/plugins/wiimote.rst

View file

@ -59,13 +59,13 @@ $(document).ready(function() {
request = { request = {
type: 'request', type: 'request',
action: 'video.omxplayer.play', action: 'media.play',
args: { resource: resource } args: { resource: resource }
}; };
} else { } else {
request = { request = {
type: 'request', type: 'request',
action: 'video.omxplayer.search', action: 'media.search',
args: { query: resource } args: { query: resource }
}; };
@ -87,7 +87,7 @@ $(document).ready(function() {
execute( execute(
{ {
type: 'request', type: 'request',
action: 'video.omxplayer.' + action, action: 'media.' + action,
} }
); );
}); });
@ -100,7 +100,7 @@ $(document).ready(function() {
execute( execute(
{ {
type: 'request', type: 'request',
action: 'video.omxplayer.set_volume', action: 'media.set_volume',
args: { volume: $(this).val() } args: { volume: $(this).val() }
}, },
@ -124,7 +124,7 @@ $(document).ready(function() {
execute( execute(
{ {
type: 'request', type: 'request',
action: 'video.omxplayer.play', action: 'media.play',
args: { resource: $item.data('url') }, args: { resource: $item.data('url') },
}, },

View file

@ -1,5 +1,5 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/video.omxplayer.js') }}"></script> <script type="text/javascript" src="{{ url_for('static', filename='js/media.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/video.omxplayer.css') }}"></script> <link rel="stylesheet" href="{{ url_for('static', filename='css/media.css') }}"></script>
<div class="row" id="video-container"> <div class="row" id="video-container">
<form action="#" id="video-search"> <form action="#" id="video-search">

View file

@ -0,0 +1 @@
media.html

View file

@ -0,0 +1 @@
media.html

View file

@ -6,6 +6,7 @@ import subprocess
import urllib.request import urllib.request
import urllib.parse import urllib.parse
from platypush.config import Config
from platypush.context import get_plugin from platypush.context import get_plugin
from platypush.plugins import Plugin, action from platypush.plugins import Plugin, action
@ -18,8 +19,17 @@ class PlayerState(enum.Enum):
class MediaPlugin(Plugin): class MediaPlugin(Plugin):
""" """
Generic plugin to interact with a media player. Generic plugin to interact with a media player.
Requires:
* A media player installed (supported so far: mplayer, omxplayer)
* **python-libtorrent** (``pip install python-libtorrent``), optional for Torrent support
* **youtube-dl** installed on your system (see your distro instructions), optional for YouTube support
""" """
_NOT_IMPLEMENTED_ERR = NotImplementedError(
'This method must be implemented in a derived class')
# Supported audio extensions # Supported audio extensions
audio_extensions = { audio_extensions = {
'3gp', 'aa', 'aac', 'aax', 'act', 'aiff', 'amr', 'ape', 'au', '3gp', 'aa', 'aac', 'aax', 'act', 'aiff', 'amr', 'ape', 'au',
@ -39,13 +49,10 @@ class MediaPlugin(Plugin):
'f4b', 'f4b',
} }
def __init__(self, player, media_dirs=[], download_dir=None, *args, **kwargs): _supported_media_plugins = { 'media.mplayer', 'media.omxplayer' }
"""
:param player: Name of the player plugin to be used as a backend.
Example: 'media.mplayer', 'media.vlc' or 'media.omxplayer'.
The plugin needs to be configured as well if required.
:type player: str
def __init__(self, media_dirs=[], download_dir=None, *args, **kwargs):
"""
:param media_dirs: Directories that will be scanned for media files when :param media_dirs: Directories that will be scanned for media files when
a search is performed (default: none) a search is performed (default: none)
:type media_dirs: list :type media_dirs: list
@ -57,7 +64,26 @@ class MediaPlugin(Plugin):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._player_name = player player = None
player_config = {}
for plugin in Config.get_plugins().keys():
if plugin in self._supported_media_plugins:
player = plugin
break
if not player:
raise AttributeError('No media plugin configured')
media_dirs = media_dirs or player_config.get('media_dirs', [])
download_dir = download_dir or player_config.get('download_dir')
if self.__class__.__name__ == 'MediaPlugin':
# Populate this plugin with the actions of the configured player
plugin = get_plugin(player)
for action in plugin.registered_actions:
setattr(self, action, getattr(plugin, action))
self.registered_actions.add(action)
self.media_dirs = set( self.media_dirs = set(
filter( filter(
lambda _: os.path.isdir(_), lambda _: os.path.isdir(_),
@ -108,31 +134,31 @@ class MediaPlugin(Plugin):
@action @action
def play(self, resource, *args, **kwargs): def play(self, resource, *args, **kwargs):
return get_plugin(self._player_name).play(resource, *args, **kwargs) raise self._NOT_IMPLEMENTED_ERR
@action @action
def pause(self, *args, **kwargs): def pause(self, *args, **kwargs):
return get_plugin(self._player_name).pause(*args, **kwargs) raise self._NOT_IMPLEMENTED_ERR
@action @action
def stop(self, *args, **kwargs): def stop(self, *args, **kwargs):
return get_plugin(self._player_name).stop(*args, **kwargs) raise self._NOT_IMPLEMENTED_ERR
@action @action
def voldown(self, *args, **kwargs): def voldown(self, *args, **kwargs):
return get_plugin(self._player_name).voldown(*args, **kwargs) raise self._NOT_IMPLEMENTED_ERR
@action @action
def volup(self, *args, **kwargs): def volup(self, *args, **kwargs):
return get_plugin(self._player_name).volup(*args, **kwargs) raise self._NOT_IMPLEMENTED_ERR
@action @action
def back(self, *args, **kwargs): def back(self, *args, **kwargs):
return get_plugin(self._player_name).back(*args, **kwargs) raise self._NOT_IMPLEMENTED_ERR
@action @action
def forward(self, *args, **kwargs): def forward(self, *args, **kwargs):
return get_plugin(self._player_name).forward(*args, **kwargs) raise self._NOT_IMPLEMENTED_ERR
@action @action
def next(self): def next(self):
@ -146,31 +172,31 @@ class MediaPlugin(Plugin):
@action @action
def toggle_subtitles(self, *args, **kwargs): def toggle_subtitles(self, *args, **kwargs):
return get_plugin(self._player_name).toggle_subtitles(*args, **kwargs) raise self._NOT_IMPLEMENTED_ERR
@action @action
def is_playing(self, *args, **kwargs): def is_playing(self, *args, **kwargs):
return get_plugin(self._player_name).is_playing(*args, **kwargs) raise self._NOT_IMPLEMENTED_ERR
@action @action
def load(self, resource, *args, **kwargs): def load(self, resource, *args, **kwargs):
return get_plugin(self._player_name).load(resource, *args, **kwargs) raise self._NOT_IMPLEMENTED_ERR
@action @action
def mute(self, *args, **kwargs): def mute(self, *args, **kwargs):
return get_plugin(self._player_name).mute(*args, **kwargs) raise self._NOT_IMPLEMENTED_ERR
@action @action
def seek(self, *args, **kwargs): def seek(self, *args, **kwargs):
return get_plugin(self._player_name).seek(*args, **kwargs) raise self._NOT_IMPLEMENTED_ERR
@action @action
def set_position(self, *args, **kwargs): def set_position(self, *args, **kwargs):
return get_plugin(self._player_name).set_position(*args, **kwargs) raise self._NOT_IMPLEMENTED_ERR
@action @action
def set_volume(self, volume, *args, **kwargs): def set_volume(self, volume, *args, **kwargs):
return get_plugin(self._player_name).set_volume(volume, *args, **kwargs) raise self._NOT_IMPLEMENTED_ERR
@action @action
def search(self, query, types=None, queue_results=False, autoplay=False): def search(self, query, types=None, queue_results=False, autoplay=False):
@ -219,11 +245,11 @@ class MediaPlugin(Plugin):
@classmethod @classmethod
def _is_video_file(cls, filename): def _is_video_file(cls, filename):
return filename.lower().split('.') in cls.video_extensions return filename.lower().split('.')[-1] in cls.video_extensions
@classmethod @classmethod
def _is_audio_file(cls, filename): def _is_audio_file(cls, filename):
return filename.lower().split('.') in cls.audio_extensions return filename.lower().split('.')[-1] in cls.audio_extensions
@action @action
def file_search(self, query): def file_search(self, query):

View file

@ -10,14 +10,14 @@ class MediaCtrlPlugin(Plugin):
""" """
Wrapper plugin to control audio and video media. Wrapper plugin to control audio and video media.
Examples of supported URL types: Examples of supported URL types:
- file:///media/movies/Movie.mp4 [requires omxplayer Python support] - file:///media/movies/Movie.mp4 [requires media plugin enabled]
- youtube:video:poAk9XgK7Cs [requires omxplayer+youtube-dl] - youtube:video:poAk9XgK7Cs [requires media plugin+youtube-dl]
- magnet:?torrent_magnet [requires torrentcast] - magnet:?torrent_magnet [requires torrentcast]
- spotify:track:track_id [leverages plugins.music.mpd] - spotify:track:track_id [leverages plugins.music.mpd]
""" """
_supported_plugins = { _supported_plugins = {
'music.mpd', 'video.omxplayer', 'video.torrentcast' 'music.mpd', 'media', 'video.torrentcast'
} }
def __init__(self, torrentcast_port=9090, *args, **kwargs): def __init__(self, torrentcast_port=9090, *args, **kwargs):
@ -84,7 +84,7 @@ class MediaCtrlPlugin(Plugin):
if type == 'mpd': if type == 'mpd':
plugin_name = 'music.mpd' plugin_name = 'music.mpd'
elif type == 'youtube:video' or type == 'file': elif type == 'youtube:video' or type == 'file':
plugin_name = 'video.omxplayer' plugin_name = 'media'
elif type == 'torrent': elif type == 'torrent':
plugin_name = 'video.torrentcast' plugin_name = 'video.torrentcast'

View file

@ -65,14 +65,13 @@ class MediaMplayerPlugin(MediaPlugin):
:type args: list :type args: list
""" """
super().__init__(player='media.mplayer', *argv, **kwargs) super().__init__(*argv, **kwargs)
self.args = args or [] self.args = args or []
self._init_mplayer_bin() self._init_mplayer_bin()
self._build_actions() self._build_actions()
self._mplayer = None self._mplayer = None
self._mplayer_timeout = mplayer_timeout self._mplayer_timeout = mplayer_timeout
self._videos_queue = []
def _init_mplayer_bin(self, mplayer_bin=None): def _init_mplayer_bin(self, mplayer_bin=None):
@ -229,6 +228,7 @@ class MediaMplayerPlugin(MediaPlugin):
MPlayer executable MPlayer executable
:type mplayer_args: list[str] :type mplayer_args: list[str]
""" """
resource = self._get_resource(resource)
return self._exec('loadfile', resource, mplayer_args=mplayer_args) return self._exec('loadfile', resource, mplayer_args=mplayer_args)
@action @action
@ -249,63 +249,22 @@ class MediaMplayerPlugin(MediaPlugin):
@action @action
def voldown(self, step=10.0): def voldown(self, step=10.0):
""" Volume down by (default: 10)% """ """ Volume down by (default: 10)% """
volume = self.get_property('volume').output.get('volume') return self.step_property('volume', -step)
if volume is None:
self.logger.warning('Unable to read volume property')
return
new_volume = max(0, volume-step)
return self.set_property('volume', new_volume)
@action @action
def volup(self, step=10.0): def volup(self, step=10.0):
""" Volume up by (default: 10)% """ """ Volume up by (default: 10)% """
volume = self.get_property('volume').output.get('volume') return self.step_property('volume', step)
if volume is None:
self.logger.warning('Unable to read volume property')
return
new_volume = min(100, volume+step)
return self.set_property('volume', new_volume)
@action @action
def back(self, offset=60.0): def back(self, offset=60.0):
""" Back by (default: 60) seconds """ """ Back by (default: 60) seconds """
pos = self.get_property('time_pos').output.get('time_pos') return self.step_property('time_pos', -offset)
if pos is None:
self.logger.warning('Unable to read time_pos property')
return
new_pos = max(0, pos-offset)
return self.set_property('time_pos', new_pos)
@action @action
def forward(self, offset=60.0): def forward(self, offset=60.0):
""" Forward by (default: 60) seconds """ """ Forward by (default: 60) seconds """
pos = self.get_property('time_pos').output.get('time_pos') return self.step_property('time_pos', offset)
if pos is None:
self.logger.warning('Unable to read time_pos property')
return
length = self.get_property('length').output.get('length')
if length is None:
self.logger.warning('Unable to read length property')
return
new_pos = min(length, pos+offset)
return self.set_property('time_pos', new_pos)
@action
def next(self):
""" Play the next item in the queue """
if self._mplayer:
self.quit()
if self._videos_queue:
video = self._videos_queue.pop(0)
return self.play(video)
@action @action
def toggle_subtitles(self): def toggle_subtitles(self):
@ -325,17 +284,12 @@ class MediaMplayerPlugin(MediaPlugin):
""" """
Load a resource/video in the player. Load a resource/video in the player.
""" """
return self._exec('loadfile', resource) return self.play('loadfile', resource)
@action @action
def mute(self): def mute(self):
""" Toggle mute state """ """ Toggle mute state """
mute = self.get_property('mute').output.get('mute') return self._exec('mute')
if mute is None:
self.logger.warning('Unable to read mute property')
return
return self._exec('mute', int(not mute))
@action @action
def seek(self, relative_position): def seek(self, relative_position):
@ -345,19 +299,7 @@ class MediaMplayerPlugin(MediaPlugin):
:param relative_position: Number of seconds relative to the current cursor :param relative_position: Number of seconds relative to the current cursor
:type relative_position: int :type relative_position: int
""" """
return self.step_property('time_pos', offset)
pos = self.get_property('time_pos').output.get('time_pos')
if pos is None:
self.logger.warning('Unable to read time_pos property')
return
length = self.get_property('length').output.get('length')
if length is None:
self.logger.warning('Unable to read length property')
return
new_pos = max(0, min(length, pos+offset))
return self.set_property('time_pos', new_pos)
@action @action
def set_position(self, position): def set_position(self, position):
@ -367,13 +309,7 @@ class MediaMplayerPlugin(MediaPlugin):
:param position: Number of seconds from the start :param position: Number of seconds from the start
:type position: int :type position: int
""" """
return self.set_property('time_pos', position)
length = self.get_property('length').output.get('length')
if length is None:
self.logger.warning('Unable to read length property')
return
return self.set_property('time_pos', max(0, min(length, position)))
@action @action
def get_property(self, property, args=None): def get_property(self, property, args=None):
@ -409,6 +345,30 @@ class MediaMplayerPlugin(MediaPlugin):
response = Response(output={}) response = Response(output={})
result = self._exec('set_property', property, value, result = self._exec('set_property', property, value,
prefix='pausing_keep_force' if property != 'pause'
else None, wait_for_response=True, *args) or {}
for k, v in result.items():
if k == 'ERROR' and v not in response.errors:
response.errors.append('{} {}{}: {}'.format(property, value,
args, v))
else:
response.output[k] = v
return response
@action
def step_property(self, property, value, args=None):
"""
Step a player property (e.g. volume, time_pos etc.). See
http://www.mplayerhq.hu/DOCS/tech/slave.txt for a full list of the
available steppable properties
"""
args = args or []
response = Response(output={})
result = self._exec('step_property', property, value,
prefix='pausing_keep_force', prefix='pausing_keep_force',
wait_for_response=True, *args) or {} wait_for_response=True, *args) or {}

View file

@ -0,0 +1,297 @@
import os
import re
import subprocess
import urllib.request
import urllib.parse
from platypush.context import get_backend, get_plugin
from platypush.plugins.media import MediaPlugin, PlayerState
from platypush.message.event.video import VideoPlayEvent, VideoPauseEvent, \
VideoStopEvent, NewPlayingVideoEvent
from platypush.plugins import action
class MediaOmxplayerPlugin(MediaPlugin):
"""
Plugin to control video and media playback using OMXPlayer.
Requires:
* **omxplayer** installed on your system (see your distro instructions)
* **omxplayer-wrapper** (``pip install omxplayer-wrapper``)
"""
def __init__(self, args=[], *argv, **kwargs):
"""
:param args: Arguments that will be passed to the OMXPlayer constructor
(e.g. subtitles, volume, start position, window size etc.) see
https://github.com/popcornmix/omxplayer#synopsis and
http://python-omxplayer-wrapper.readthedocs.io/en/latest/omxplayer/#omxplayer.player.OMXPlayer
:type args: list
"""
super().__init__(*argv, **kwargs)
self.args = args
self._player = None
@action
def play(self, resource):
"""
Play a resource.
:param resource: Resource to play. Supported types:
* Local files (format: ``file://<path>/<file>``)
* Remote videos (format: ``https://<url>/<resource>``)
* YouTube videos (format: ``https://www.youtube.com/watch?v=<id>``)
* Torrents (format: Magnet links, Torrent URLs or local Torrent files)
"""
resource = self._get_resource(resource)
if self._player:
try:
self._player.stop()
self._player = None
except Exception as e:
self.logger.exception(e)
self.logger.warning('Unable to stop a previously running instance ' +
'of OMXPlayer, trying to play anyway')
try:
from omxplayer import OMXPlayer
self._player = OMXPlayer(resource, args=self.args)
self._init_player_handlers()
except DBusException as e:
self.logger.warning('DBus connection failed: you will probably not ' +
'be able to control the media')
self.logger.exception(e)
return self.status()
@action
def pause(self):
""" Pause the playback """
if self._player: self._player.play_pause()
@action
def stop(self):
""" Stop the playback """
if self._player:
self._player.stop()
self._player.quit()
self._player = None
return {'status':'stop'}
@action
def voldown(self):
""" Volume down by 10% """
if self._player:
self._player.set_volume(max(-6000, self._player.volume()-1000))
return self.status()
@action
def volup(self):
""" Volume up by 10% """
if self._player:
self._player.set_volume(min(0, self._player.volume()+1000))
return self.status()
@action
def back(self, offset=60):
""" Back by (default: 60) seconds """
if self._player:
self._player.seek(-offset)
return self.status()
@action
def forward(self, offset=60):
""" Forward by (default: 60) seconds """
if self._player:
self._player.seek(+offset)
return self.status()
@action
def next(self):
""" Play the next track/video """
if self._player:
self._player.stop()
if self.videos_queue:
video = self.videos_queue.pop(0)
return self.play(video)
@action
def hide_subtitles(self):
""" Hide the subtitles """
if self._player: self._player.hide_subtitles()
return self.status()
@action
def hide_video(self):
""" Hide the video """
if self._player: self._player.hide_video()
return self.status()
@action
def is_playing(self):
"""
:returns: True if it's playing, False otherwise
"""
if self._player: return self._player.is_playing()
else: return False
@action
def load(self, resource, pause=False):
"""
Load a resource/video in the player.
:param pause: If set, load the video in paused mode (default: False)
:type pause: bool
"""
if self._player: self._player.load(resource, pause)
return self.status()
@action
def metadata(self):
""" Get the metadata of the current video """
if self._player:
return self._player.metadata()
return self.status()
@action
def mute(self):
""" Mute the player """
if self._player: self._player.mute()
return self.status()
@action
def unmute(self):
""" Unmute the player """
if self._player: self._player.unmute()
return self.status()
@action
def seek(self, relative_position):
"""
Seek backward/forward by the specified number of seconds
:param relative_position: Number of seconds relative to the current cursor
:type relative_position: int
"""
if self._player: self._player.seek(relative_position)
return self.status()
@action
def set_position(self, position):
"""
Seek backward/forward to the specified absolute position
:param position: Number of seconds from the start
:type position: int
"""
if self._player: self._player.set_seek(position)
return self.status()
@action
def set_volume(self, volume):
"""
Set the volume
:param volume: Volume value between 0 and 100
:type volume: int
"""
# Transform a [0,100] value to an OMXPlayer volume in [-6000,0]
volume = 60.0*volume - 6000
if self._player: self._player.set_volume(volume)
return self.status()
@action
def status(self):
"""
Get the current player state.
:returns: A dictionary containing the current state.
Example::
output = {
"source": "https://www.youtube.com/watch?v=7L9KkZoNZkA",
"state": "play",
"volume": 80,
"elapsed": 123,
"duration": 300,
"width": 800,
"height": 600
}
"""
state = PlayerState.STOP.value
if self._player:
state = self._player.playback_status().lower()
if state == 'playing': state = PlayerState.PLAY.value
elif state == 'stopped': state = PlayerState.STOP.value
elif state == 'paused': state = PlayerState.PAUSE.value
return {
'source': self._player.get_source(),
'state': state,
'volume': self._player.volume(),
'elapsed': self._player.position(),
'duration': self._player.duration(),
'width': self._player.width(),
'height': self._player.height(),
}
else:
return {
'state': PlayerState.STOP.value
}
@action
def send_message(self, msg):
try:
redis = get_backend('redis')
if not redis:
raise KeyError()
except KeyError:
self.logger.warning("Backend {} does not implement send_message " +
"and the fallback Redis backend isn't configured")
return
redis.send_message(msg)
def on_play(self):
def _f(player):
self.send_message(VideoPlayEvent(video=self._player.get_source()))
return _f
def on_pause(self):
def _f(player):
self.send_message(VideoPauseEvent(video=self._player.get_source()))
return _f
def on_stop(self):
def _f(player):
self.send_message(VideoStopEvent())
return _f
def _init_player_handlers(self):
if not self._player:
return
self._player.playEvent += self.on_play()
self._player.pauseEvent += self.on_pause()
self._player.stopEvent += self.on_stop()
# vim:sw=4:ts=4:et:

View file

@ -1,496 +0,0 @@
import os
import re
import subprocess
import urllib.request
import urllib.parse
from platypush.context import get_backend, get_plugin
from platypush.plugins.media import PlayerState
from platypush.message.event.video import VideoPlayEvent, VideoPauseEvent, \
VideoStopEvent, NewPlayingVideoEvent
from platypush.plugins import Plugin, action
class VideoOmxplayerPlugin(Plugin):
"""
Plugin to control video and media playback on your Raspberry Pi or
ARM-compatible device using OMXPlayer.
It can play local files, remote URLs, YouTube URLs and it supports torrents
search, download and play.
Requires:
* **omxplayer** installed on your system (see your distro instructions)
* **omxplayer-wrapper** (``pip install omxplayer-wrapper``)
* **python-libtorrent** (``pip install python-libtorrent``), optional for Torrent support
* **youtube-dl** installed on your system (see your distro instructions), optional for YouTube support
"""
# Supported video extensions
video_extensions = {
'.avi', '.flv', '.wmv', '.mov', '.mp4', '.m4v', '.mpg', '.mpeg',
'.rm', '.swf', '.vob', '.mkv'
}
def __init__(self, args=[], media_dirs=[], download_dir=None, *argv, **kwargs):
"""
:param args: Arguments that will be passed to the OMXPlayer constructor (e.g. subtitles, volume, start position, window size etc.) see https://github.com/popcornmix/omxplayer#synopsis and http://python-omxplayer-wrapper.readthedocs.io/en/latest/omxplayer/#omxplayer.player.OMXPlayer
:type args: list
:param media_dirs: Directories that will be scanned for media files when a search is performed (default: none)
:type media_dirs: list
:param download_dir: Directory where the videos/torrents will be downloaded (default: none)
:type download_dir: str
"""
super().__init__(*argv, **kwargs)
self.args = args
self.media_dirs = set(
filter(
lambda _: os.path.isdir(_),
map(
lambda _: os.path.abspath(os.path.expanduser(_)),
media_dirs
)
)
)
if download_dir:
self.download_dir = os.path.abspath(os.path.expanduser(download_dir))
if not os.path.isdir(self.download_dir):
raise RuntimeError('download_dir [{}] is not a valid directory'
.format(self.download_dir))
self.media_dirs.add(self.download_dir)
self.player = None
self.videos_queue = []
@action
def play(self, resource):
"""
Play a resource.
:param resource: Resource to play. Supported types:
* Local files (format: ``file://<path>/<file>``)
* Remote videos (format: ``https://<url>/<resource>``)
* YouTube videos (format: ``https://www.youtube.com/watch?v=<id>``)
* Torrents (format: Magnet links, Torrent URLs or local Torrent files)
"""
from dbus.exceptions import DBusException
if resource.startswith('youtube:') \
or resource.startswith('https://www.youtube.com/watch?v='):
resource = self._get_youtube_content(resource)
elif resource.startswith('magnet:?'):
torrents = get_plugin('torrent')
response = torrents.download(resource, download_dir=self.download_dir)
resources = [f for f in response.output if self._is_video_file(f)]
if resources:
self.videos_queue = sorted(resources)
resource = self.videos_queue.pop(0)
else:
raise RuntimeError('Unable to download torrent {}'.format(resource))
self.logger.info('Playing {}'.format(resource))
if self.player:
try:
self.player.stop()
self.player = None
except Exception as e:
self.logger.exception(e)
self.logger.warning('Unable to stop a previously running instance ' +
'of OMXPlayer, trying to play anyway')
try:
from omxplayer import OMXPlayer
self.player = OMXPlayer(resource, args=self.args)
self._init_player_handlers()
except DBusException as e:
self.logger.warning('DBus connection failed: you will probably not ' +
'be able to control the media')
self.logger.exception(e)
return self.status()
@action
def pause(self):
""" Pause the playback """
if self.player: self.player.play_pause()
@action
def stop(self):
""" Stop the playback """
if self.player:
self.player.stop()
self.player.quit()
self.player = None
return {'status':'stop'}
@action
def voldown(self):
""" Volume down by 10% """
if self.player:
self.player.set_volume(max(-6000, self.player.volume()-1000))
return self.status()
@action
def volup(self):
""" Volume up by 10% """
if self.player:
self.player.set_volume(min(0, self.player.volume()+1000))
return self.status()
@action
def back(self):
""" Back by 30 seconds """
if self.player:
self.player.seek(-30)
return self.status()
@action
def forward(self):
""" Forward by 30 seconds """
if self.player:
self.player.seek(+30)
return self.status()
@action
def next(self):
""" Play the next track/video """
if self.player:
self.player.stop()
if self.videos_queue:
video = self.videos_queue.pop(0)
return self.play(video)
@action
def hide_subtitles(self):
""" Hide the subtitles """
if self.player: self.player.hide_subtitles()
return self.status()
@action
def hide_video(self):
""" Hide the video """
if self.player: self.player.hide_video()
return self.status()
@action
def is_playing(self):
"""
:returns: True if it's playing, False otherwise
"""
if self.player: return self.player.is_playing()
else: return False
@action
def load(self, resource, pause=False):
"""
Load a resource/video in the player.
:param pause: If set, load the video in paused mode (default: False)
:type pause: bool
"""
if self.player: self.player.load(resource, pause)
return self.status()
@action
def metadata(self):
""" Get the metadata of the current video """
if self.player:
return self.player.metadata()
return self.status()
@action
def mute(self):
""" Mute the player """
if self.player: self.player.mute()
return self.status()
@action
def unmute(self):
""" Unmute the player """
if self.player: self.player.unmute()
return self.status()
@action
def seek(self, relative_position):
"""
Seek backward/forward by the specified number of seconds
:param relative_position: Number of seconds relative to the current cursor
:type relative_position: int
"""
if self.player: self.player.seek(relative_position)
return self.status()
@action
def set_position(self, position):
"""
Seek backward/forward to the specified absolute position
:param position: Number of seconds from the start
:type position: int
"""
if self.player: self.player.set_seek(position)
return self.status()
@action
def set_volume(self, volume):
"""
Set the volume
:param volume: Volume value between 0 and 100
:type volume: int
"""
# Transform a [0,100] value to an OMXPlayer volume in [-6000,0]
volume = 60.0*volume - 6000
if self.player: self.player.set_volume(volume)
return self.status()
@action
def status(self):
"""
Get the current player state.
:returns: A dictionary containing the current state.
Example::
output = {
"source": "https://www.youtube.com/watch?v=7L9KkZoNZkA",
"state": "play",
"volume": 80,
"elapsed": 123,
"duration": 300,
"width": 800,
"height": 600
}
"""
state = PlayerState.STOP.value
if self.player:
state = self.player.playback_status().lower()
if state == 'playing': state = PlayerState.PLAY.value
elif state == 'stopped': state = PlayerState.STOP.value
elif state == 'paused': state = PlayerState.PAUSE.value
return {
'source': self.player.get_source(),
'state': state,
'volume': self.player.volume(),
'elapsed': self.player.position(),
'duration': self.player.duration(),
'width': self.player.width(),
'height': self.player.height(),
}
else:
return {
'state': PlayerState.STOP.value
}
@action
def send_message(self, msg):
try:
redis = get_backend('redis')
if not redis:
raise KeyError()
except KeyError:
self.logger.warning("Backend {} does not implement send_message " +
"and the fallback Redis backend isn't configured")
return
redis.send_message(msg)
def on_play(self):
def _f(player):
self.send_message(VideoPlayEvent(video=self.player.get_source()))
return _f
def on_pause(self):
def _f(player):
self.send_message(VideoPauseEvent(video=self.player.get_source()))
return _f
def on_stop(self):
def _f(player):
self.send_message(VideoStopEvent())
return _f
def _init_player_handlers(self):
if not self.player:
return
self.player.playEvent += self.on_play()
self.player.pauseEvent += self.on_pause()
self.player.stopEvent += self.on_stop()
@action
def search(self, query, types=None, queue_results=False, autoplay=False):
"""
Perform a video search.
:param query: Query string, video name or partial name
:type query: str
:param types: Video types to search (default: ``["youtube", "file", "torrent"]``)
:type types: list
:param queue_results: Append the results to the current playing queue (default: False)
:type queue_results: bool
:param autoplay: Play the first result of the search (default: False)
:type autoplay: bool
"""
results = []
if types is None:
types = { 'youtube', 'file', 'torrent' }
if 'file' in types:
file_results = self.file_search(query).output
results.extend(file_results)
if 'torrent' in types:
torrents = get_plugin('torrent')
torrent_results = torrents.search(query).output
results.extend(torrent_results)
if 'youtube' in types:
yt_results = self.youtube_search(query).output
results.extend(yt_results)
if results:
if queue_results:
self.videos_queue = [_['url'] for _ in results]
if autoplay:
self.play(self.videos_queue.pop(0))
elif autoplay:
self.play(results[0]['url'])
return results
@classmethod
def _is_video_file(cls, filename):
is_video = False
for ext in cls.video_extensions:
if filename.lower().endswith(ext):
is_video = True
break
return is_video
@action
def file_search(self, query):
results = []
query_tokens = [_.lower() for _ in re.split('\s+', query.strip())]
for media_dir in self.media_dirs:
self.logger.info('Scanning {} for "{}"'.format(media_dir, query))
for path, dirs, files in os.walk(media_dir):
for f in files:
if not self._is_video_file(f):
continue
matches_query = True
for token in query_tokens:
if token not in f.lower():
matches_query = False
break
if not matches_query:
continue
results.append({
'url': 'file://' + path + os.sep + f,
'title': f,
})
return results
@action
def youtube_search(self, query):
"""
Performs a YouTube search either using the YouTube API (faster and
recommended, it requires the :mod:`platypush.plugins.google.youtube`
plugin to be configured) or parsing the HTML search results (fallback
slower method)
"""
self.logger.info('Searching YouTube for "{}"'.format(query))
try:
return self._youtube_search_api(query=query)
except Exception as e:
self.logger.warning('Unable to load the YouTube plugin, falling ' +
'back to HTML parse method: {}'.format(str(e)))
return self._youtube_search_html_parse(query=query)
def _youtube_search_api(self, query):
return [
{
'url': 'https://www.youtube.com/watch?v=' + item['id']['videoId'],
'title': item.get('snippet', {}).get('title', '<No Title>'),
}
for item in get_plugin('google.youtube').search(query=query).output
if item.get('id', {}).get('kind') == 'youtube#video'
]
def _youtube_search_html_parse(self, query):
query = urllib.parse.quote(query)
url = "https://www.youtube.com/results?search_query=" + query
response = urllib.request.urlopen(url)
html = response.read().decode('utf-8')
results = []
while html:
m = re.search('(<a href="(/watch\?v=.+?)".+?yt-uix-tile-link.+?title="(.+?)".+?>)', html)
if m:
results.append({
'url': 'https://www.youtube.com' + m.group(2),
'title': m.group(3)
})
html = html.split(m.group(1))[1]
else:
html = ''
self.logger.info('{} YouTube video results for the search query "{}"'
.format(len(results), query))
return results
@classmethod
def _get_youtube_content(cls, url):
m = re.match('youtube:video:(.*)', url)
if m: url = 'https://www.youtube.com/watch?v={}'.format(m.group(1))
proc = subprocess.Popen(['youtube-dl','-f','best', '-g', url],
stdout=subprocess.PIPE)
return proc.stdout.read().decode("utf-8", "strict")[:-1]
# vim:sw=4:ts=4:et: