import subprocess
import time
import os
import signal
import logging
import threading
import atexit
import sys
try: # python 3
from pathlib import Path
except ImportError: # python2
from pathlib2 import Path
from decorator import decorator
from dbus import DBusException, Int64, String, ObjectPath
import dbus.types
from omxplayer.bus_finder import BusFinder
from omxplayer.dbus_connection import DBusConnection, \
DBusConnectionError
from evento import Event
# CONSTANTS
RETRY_DELAY = 0.05
# FILE GLOBAL OBJECTS
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
def _check_player_is_active(fn):
# wraps is a decorator that improves debugging wrapped methods
def wrapped(fn, self, *args, **kwargs):
logger.debug('Checking if process is still alive')
# poll determines whether the process has terminated,
# if it hasn't it returns None.
if self._process.poll() is None:
logger.debug('OMXPlayer is running, so execute %s' %
fn.__name__)
return fn(self, *args, **kwargs)
else:
raise OMXPlayerDeadError('Process is no longer alive, can\'t run command')
return decorator(wrapped, fn)
def _from_dbus_type(fn):
def from_dbus_type(dbusVal):
def from_dbus_dict(dbusDict):
d = dict()
for dbusKey, dbusVal in dbusDict.items():
d[from_dbus_type(dbusKey)] = from_dbus_type(dbusVal)
return d
typeUnwrapper = {
dbus.types.Dictionary: from_dbus_dict,
dbus.types.Array: lambda x: list(map(from_dbus_type, x)),
dbus.types.Double: float,
dbus.types.Boolean: bool,
dbus.types.Byte: int,
dbus.types.Int16: int,
dbus.types.Int32: int,
dbus.types.Int64: int,
dbus.types.UInt32: int,
dbus.types.UInt64: int,
dbus.types.ByteArray: str,
dbus.types.ObjectPath: str,
dbus.types.Signature: str,
dbus.types.String: str
}
try:
return typeUnwrapper[type(dbusVal)](dbusVal)
except KeyError:
return dbusVal
def wrapped(fn, self, *args, **kwargs):
return from_dbus_type(fn(self, *args, **kwargs))
return decorator(wrapped, fn)
# CLASSES
[docs]class FileNotFoundError(Exception):
pass
[docs]class OMXPlayerDeadError(Exception):
pass
[docs]class OMXPlayer(object):
"""
OMXPlayer controller
This works by speaking to OMXPlayer over DBus sending messages.
Args:
source (str): Path to the file (as ~/Videos/my-video.mp4) or URL you wish to play
args (list/str): used to pass option parameters to omxplayer. see: https://github.com/popcornmix/omxplayer#synopsis
Multiple argument example:
>>> OMXPlayer('path.mp4', args=['--no-osd', '--no-keys', '-b'])
>>> OMXPlayer('path.mp4', args='--no-osd --no-keys -b')
>>> OMXPlayer('path.mp4', dbus_name='org.mpris.MediaPlayer2.omxplayer2')
"""
def __init__(self, source,
args=None,
bus_address_finder=None,
Connection=None,
dbus_name=None,
pause=False):
logger.debug('Instantiating OMXPlayer')
if args is None:
self.args = []
elif isinstance(args, str):
import shlex
self.args = shlex.split(args)
else:
self.args = list(map(str, args))
self._is_playing = True
self._source = Path(source)
self._dbus_name = dbus_name
self._Connection = Connection if Connection else DBusConnection
self._bus_address_finder = bus_address_finder if bus_address_finder else BusFinder()
#: Event called on pause ``callback(player)``
self.pauseEvent = Event()
#: Event called on play ``callback(player)``
self.playEvent = Event()
#: Event called on stop ``callback(player)``
self.stopEvent = Event()
#: Event called on exit ``callback(player, exit_status)``
self.exitEvent = Event()
#: Event called on seek ``callback(player, relative_position)``
self.seekEvent = Event()
#: Event called on setting position ``callback(player, absolute_position)``
self.positionEvent = Event()
self._process = None
self._connection = None
self.load(source, pause=pause)
def _load_source(self, source):
if self._process:
self.quit()
self._process = self._setup_omxplayer_process(source)
self._rate = 1.0
self._is_muted = False
self._connection = self._setup_dbus_connection(self._Connection, self._bus_address_finder)
def _run_omxplayer(self, source, devnull):
def on_exit(self, exit_status):
logger.info("OMXPlayer process is dead, all DBus calls from here "
"will fail")
self.exitEvent(self, exit_status)
def monitor(self, process, on_exit):
process.wait()
on_exit(self, process.returncode)
try:
source = str(source.resolve())
except AttributeError:
pass
command = ['omxplayer'] + self.args + [source]
if self._dbus_name:
command += ['--dbus_name', self._dbus_name]
logger.debug("Opening omxplayer with the command: %s" % command)
# By running os.setsid in the fork-ed process we create a process group
# which is used to kill the subprocesses the `omxplayer` script
# (it is a bash script itself that calls omxplayer.bin) creates. Without
# doing this we end up in a scenario where we kill the shell script, but not
# the forked children of the shell script.
# See https://pymotw.com/2/subprocess/#process-groups-sessions for examples on this
process = subprocess.Popen(command,
stdin=devnull,
stdout=devnull,
preexec_fn=os.setsid)
self._process_monitor = threading.Thread(target=monitor,
args=(self, process, on_exit))
self._process_monitor.start()
return process
def _setup_omxplayer_process(self, source):
logger.debug('Setting up OMXPlayer process')
with open(os.devnull, 'w') as devnull:
process = self._run_omxplayer(source, devnull)
logger.debug('Process opened with PID %s' % process.pid)
atexit.register(self.quit)
return process
def _setup_dbus_connection(self, Connection, bus_address_finder):
logger.debug('Trying to connect to OMXPlayer via DBus')
tries = 0
while tries < 50:
logger.debug('DBus connect attempt: {}'.format(tries))
try:
connection = Connection(bus_address_finder.get_address(), self._dbus_name)
logger.debug(
'Connected to OMXPlayer at DBus address: %s' % connection)
return connection
except (DBusConnectionError, IOError):
logger.debug('Failed to connect to OMXPlayer DBus address')
tries += 1
time.sleep(RETRY_DELAY)
raise SystemError('DBus cannot connect to the OMXPlayer process')
""" Utilities """
[docs] def load(self, source, pause=False):
"""
Loads a new source (as a file) from ``source`` (a file path or URL)
by killing the current ``omxplayer`` process and forking a new one.
Args:
source (string): Path to the file to play or URL
"""
self._source = source
self._load_source(source)
if pause:
time.sleep(0.5) # Wait for the DBus interface to be initialised
self.pause()
""" ROOT INTERFACE PROPERTIES """
[docs] @_check_player_is_active
@_from_dbus_type
def can_quit(self):
"""
Returns:
bool: whether the player can quit or not """
return self._root_interface_property('CanQuit')
[docs] @_check_player_is_active
@_from_dbus_type
def fullscreen(self):
"""
Returns:
bool: whether the player is fullscreen or not """
return self._root_interface_property('Fullscreen')
[docs] @_check_player_is_active
@_from_dbus_type
def can_set_fullscreen(self):
"""
Returns:
bool: whether the player can go fullscreen """
return self._root_interface_property('CanSetFullscreen')
[docs] @_check_player_is_active
@_from_dbus_type
def can_raise(self):
"""
Returns:
bool: whether the player can raise the display window atop of all other windows"""
return self._root_interface_property('CanRaise')
[docs] @_check_player_is_active
@_from_dbus_type
def has_track_list(self):
"""
Returns:
bool: whether the player has a track list or not"""
return self._root_interface_property('HasTrackList')
[docs] @_check_player_is_active
@_from_dbus_type
def identity(self):
"""
Returns:
str: Returns `omxplayer`, the name of the player
"""
return self._root_interface_property('Identity')
[docs] @_check_player_is_active
@_from_dbus_type
def supported_uri_schemes(self):
"""
Returns:
str: list of supported URI schemes
Examples:
>>> player.supported_uri_schemes()
["file", "http", "rtsp", "rtmp"]
"""
return self._root_interface_property('SupportedUriSchemes')
""" ROOT INTERFACE METHODS """
""" PLAYER INTERFACE PROPERTIES """
[docs] @_check_player_is_active
@_from_dbus_type
def can_go_next(self):
"""
Returns:
bool: whether the player can move to the next item in the playlist
"""
return self._player_interface_property('CanGoNext')
[docs] @_check_player_is_active
@_from_dbus_type
def can_go_previous(self):
"""
Returns:
bool: whether the player can move to the previous item in the
playlist
"""
return self._player_interface_property('CanGoPrevious')
[docs] @_check_player_is_active
@_from_dbus_type
def can_seek(self):
"""
Returns:
bool: whether the player can seek """
return self._player_interface_property('CanSeek')
[docs] @_check_player_is_active
@_from_dbus_type
def can_control(self):
"""
Returns:
bool: whether the player can be controlled"""
return self._player_interface_property('CanControl')
[docs] @_check_player_is_active
@_from_dbus_type
def can_play(self):
"""
Returns:
bool: whether the player can play"""
return self._player_interface_property('CanPlay')
[docs] @_check_player_is_active
@_from_dbus_type
def can_pause(self):
"""
Returns:
bool: whether the player can pause"""
return self._player_interface_property('CanPause')
[docs] @_check_player_is_active
@_from_dbus_type
def playback_status(self):
"""
Returns:
str: one of ("Playing" | "Paused" | "Stopped")
"""
return self._player_interface_property('PlaybackStatus')
[docs] @_check_player_is_active
@_from_dbus_type
def volume(self):
"""
Returns:
float: current player volume
"""
if self._is_muted:
return 0
return self._player_interface_property('Volume')
[docs] @_check_player_is_active
@_from_dbus_type
def set_volume(self, volume):
"""
Args:
float: volume in the interval [0, 10]
"""
# 0 isn't handled correctly so we have to set it to a very small value to achieve the same purpose
if volume == 0:
volume = 1e-10
return self._player_interface_property('Volume', dbus.Double(volume))
@_check_player_is_active
@_from_dbus_type
def _position_us(self):
"""
Returns:
int: position in microseconds
"""
return self._player_interface_property('Position')
[docs] def position(self):
"""
Returns:
int: position in seconds
"""
return self._position_us() / (1000.0 * 1000.0)
[docs] @_check_player_is_active
@_from_dbus_type
def minimum_rate(self):
"""
Returns:
float: minimum playback rate (as proportion of normal rate)
"""
return self._player_interface_property('MinimumRate')
[docs] @_check_player_is_active
@_from_dbus_type
def maximum_rate(self):
"""
Returns:
float: maximum playback rate (as proportion of normal rate)
"""
return self._player_interface_property('MaximumRate')
[docs] @_check_player_is_active
@_from_dbus_type
def rate(self):
"""
Returns:
float: playback rate, 1 is the normal rate, 2 would be double speed.
"""
return self._rate
[docs] @_check_player_is_active
@_from_dbus_type
def set_rate(self, rate):
"""
Set the playback rate of the video as a multiple of the default playback speed
Examples:
>>> player.set_rate(2)
# Will play twice as fast as normal speed
>>> player.set_rate(0.5)
# Will play half speed
"""
self._rate = self._player_interface_property('Rate', dbus.Double(rate))
return self._rate
""" PLAYER INTERFACE NON-STANDARD PROPERTIES """
[docs] @_check_player_is_active
@_from_dbus_type
def aspect_ratio(self):
"""
Returns:
float: aspect ratio
"""
return self._player_interface_property('Aspect')
[docs] @_check_player_is_active
@_from_dbus_type
def video_stream_count(self):
"""
Returns:
int: number of video streams
"""
return self._player_interface_property('VideoStreamCount')
[docs] @_check_player_is_active
@_from_dbus_type
def width(self):
"""
Returns:
int: video width in px
"""
return self._player_interface_property('ResWidth')
[docs] @_check_player_is_active
@_from_dbus_type
def height(self):
"""
Returns:
int: video height in px
"""
return self._player_interface_property('ResHeight')
@_check_player_is_active
@_from_dbus_type
def _duration_us(self):
"""
Returns:
int: total length in microseconds
"""
return self._player_interface_property('Duration')
[docs] @_check_player_is_active
def duration(self):
"""
Returns:
float: duration in seconds
"""
return self._duration_us() / (1000.0 * 1000.0)
""" PLAYER INTERFACE METHODS """
[docs] @_check_player_is_active
def pause(self):
"""
Pause playback
"""
self._player_interface.Pause()
self._is_playing = False
self.pauseEvent(self)
[docs] @_check_player_is_active
def play_pause(self):
"""
Pause playback if currently playing, otherwise start playing if currently paused.
"""
self._player_interface.PlayPause()
self._is_playing = not self._is_playing
if self._is_playing:
self.playEvent(self)
else:
self.pauseEvent(self)
[docs] @_check_player_is_active
@_from_dbus_type
def stop(self):
"""
Stop the player, causing it to quit
"""
self._player_interface.Stop()
self.stopEvent(self)
[docs] @_check_player_is_active
@_from_dbus_type
def seek(self, relative_position):
"""
Seek the video by `relative_position` seconds
Args:
relative_position (float): The position in seconds to seek to.
"""
self._player_interface.Seek(Int64(1000.0 * 1000 * relative_position))
self.seekEvent(self, relative_position)
[docs] @_check_player_is_active
@_from_dbus_type
def set_position(self, position):
"""
Set the video to playback position to `position` seconds from the start of the video
Args:
position (float): The position in seconds.
"""
self._player_interface.SetPosition(ObjectPath("/not/used"), Int64(position * 1000.0 * 1000))
self.positionEvent(self, position)
[docs] @_check_player_is_active
@_from_dbus_type
def set_alpha(self, alpha):
"""
Set the transparency of the video overlay
Args:
alpha (float): The transparency (0..255)
"""
self._player_interface.SetAlpha(ObjectPath('/not/used'), Int64(alpha))
[docs] @_check_player_is_active
def mute(self):
"""
Mute audio. If already muted, then this does not do anything
"""
self._is_muted = True
self._player_interface.Mute()
[docs] @_check_player_is_active
def unmute(self):
"""
Unmutes the video. If already unmuted, then this does not do anything
"""
self._is_muted = False
self._player_interface.Unmute()
[docs] @_check_player_is_active
@_from_dbus_type
def set_aspect_mode(self, mode):
"""
Set the aspect mode of the video
Args:
mode (str): One of ("letterbox" | "fill" | "stretch")
"""
self._player_interface.SetAspectMode(ObjectPath('/not/used'), String(mode))
[docs] @_check_player_is_active
@_from_dbus_type
def set_video_pos(self, x1, y1, x2, y2):
"""
Set the video position on the screen
Args:
x1 (int): Top left x coordinate (px)
y1 (int): Top left y coordinate (px)
x2 (int): Bottom right x coordinate (px)
y2 (int): Bottom right y coordinate (px)
"""
position = "%s %s %s %s" % (str(x1),str(y1),str(x2),str(y2))
self._player_interface.VideoPos(ObjectPath('/not/used'), String(position))
[docs] @_check_player_is_active
def video_pos(self):
"""
Returns:
(int, int, int, int): Video spatial position (x1, y1, x2, y2) where (x1, y1) is top left,
and (x2, y2) is bottom right. All values in px.
"""
position_string = self._player_interface.VideoPos(ObjectPath('/not/used'))
return list(map(int, position_string.split(" ")))
[docs] @_check_player_is_active
@_from_dbus_type
def set_video_crop(self, x1, y1, x2, y2):
"""
Args:
x1 (int): Top left x coordinate (px)
y1 (int): Top left y coordinate (px)
x2 (int): Bottom right x coordinate (px)
y2 (int): Bottom right y coordinate (px)
"""
crop = "%s %s %s %s" % (str(x1),str(y1),str(x2),str(y2))
self._player_interface.SetVideoCropPos(ObjectPath('/not/used'), String(crop))
[docs] @_check_player_is_active
def hide_video(self):
"""
Hides the video overlays
"""
self._player_interface.HideVideo()
[docs] @_check_player_is_active
def show_video(self):
"""
Shows the video (to undo a `hide_video`)
"""
self._player_interface.UnHideVideo()
[docs] @_check_player_is_active
@_from_dbus_type
def list_audio(self):
"""
Returns:
[str]: A list of all known audio streams, each item is in the
format: ``<index>:<language>:<name>:<codec>:<active>``
"""
return self._player_interface.ListAudio()
[docs] @_check_player_is_active
@_from_dbus_type
def list_video(self):
"""
Returns:
[str]: A list of all known video streams, each item is in the
format: ``<index>:<language>:<name>:<codec>:<active>``
"""
return self._player_interface.ListVideo()
[docs] @_check_player_is_active
@_from_dbus_type
def list_subtitles(self):
"""
Returns:
[str]: A list of all known subtitles, each item is in the
format: ``<index>:<language>:<name>:<codec>:<active>``
"""
return self._player_interface.ListSubtitles()
[docs] @_check_player_is_active
def select_subtitle(self, index):
"""
Enable a subtitle specified by the index it is listed in :class:`list_subtitles`
Args:
index (int): index of subtitle listing returned by :class:`list_subtitles`
"""
return self._player_interface.SelectSubtitle(dbus.Int32(index))
[docs] @_check_player_is_active
def select_audio(self, index):
"""
Select audio stream specified by the index of the stream in :class:`list_audio`
Args:
index (int): index of audio stream returned by :class:`list_audio`
"""
return self._player_interface.SelectAudio(dbus.Int32(index))
[docs] @_check_player_is_active
def show_subtitles(self):
"""
Shows subtitles after :class:`hide_subtitles`
"""
return self._player_interface.ShowSubtitles()
[docs] @_check_player_is_active
def hide_subtitles(self):
"""
Hide subtitles
"""
return self._player_interface.HideSubtitles()
[docs] @_check_player_is_active
@_from_dbus_type
def action(self, code):
"""
Executes a keyboard command via a code
Args:
code (int): The key code you wish to emulate
refer to ``keys.py`` for the possible keys
"""
self._player_interface.Action(code)
[docs] @_check_player_is_active
@_from_dbus_type
def is_playing(self):
"""
Returns:
bool: Whether the player is playing
"""
self._is_playing = (self.playback_status() == "Playing")
logger.info("Playing?: %s" % self._is_playing)
return self._is_playing
[docs] @_check_player_is_active
@_from_dbus_type
def play_sync(self):
"""
Play the video and block whilst the video is playing
"""
self.play()
logger.info("Playing synchronously")
try:
time.sleep(0.05)
logger.debug("Wait for playing to start")
while self.is_playing():
time.sleep(0.05)
except DBusException:
logger.error(
"Cannot play synchronously any longer as DBus calls timed out."
)
[docs] @_check_player_is_active
@_from_dbus_type
def play(self):
"""
Play the video asynchronously returning control immediately to the calling code
"""
if not self.is_playing():
self.play_pause()
self._is_playing = True
self.playEvent(self)
[docs] @_check_player_is_active
@_from_dbus_type
def next(self):
"""
Skip to the next chapter
Returns:
bool: Whether the player skipped to the next chapter
"""
return self._player_interface.Next()
[docs] @_check_player_is_active
@_from_dbus_type
def previous(self):
"""
Skip to the previous chapter
Returns:
bool: Whether the player skipped to the previous chapter
"""
return self._player_interface.Previous()
@property
def _root_interface(self):
return self._connection.root_interface
@property
def _player_interface(self):
return self._connection.player_interface
@property
def _properties_interface(self):
return self._connection.properties_interface
def _interface_property(self, interface, prop, val):
if val:
return self._properties_interface.Set(interface, prop, val)
else:
return self._properties_interface.Get(interface, prop)
def _root_interface_property(self, prop, val=None):
return self._interface_property(self._root_interface.dbus_interface, prop, val)
def _player_interface_property(self, prop, val=None):
return self._interface_property(self._player_interface.dbus_interface, prop, val)
[docs] def quit(self):
"""
Quit the player, blocking until the process has died
"""
if self._process is None:
logger.debug('Quit was called after self._process had already been released')
return
try:
logger.debug('Quitting OMXPlayer')
process_group_id = os.getpgid(self._process.pid)
os.killpg(process_group_id, signal.SIGTERM)
logger.debug('SIGTERM Sent to pid: %s' % process_group_id)
self._process_monitor.join()
except OSError:
logger.error('Could not find the process to kill')
self._process = None
[docs] @_check_player_is_active
@_from_dbus_type
def get_source(self):
"""
Get the source URI of the currently playing media
Returns:
str: source currently playing
"""
return self._source
# For backward compatibility
[docs] @_check_player_is_active
@_from_dbus_type
def get_filename(self):
"""
Returns:
str: source currently playing
.. deprecated:: 0.2.0
Use: :func:`get_source` instead.
"""
return self.get_source()
# MediaPlayer2.Player types:
# Track_Id: DBus ID of track
# Plaback_Rate: Multiplier for playback speed (1 = normal speed)
# Volume: 0--1, 0 is muted and 1 is full volume
# Time_In_Us: Time in microseconds
# Playback_Status: Playing|Paused|Stopped
# Loop_Status: None|Track|Playlist