Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Mac #107

Draft
wants to merge 25 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@

## Unreleased

### Added

- Mac support.

## 1.8.1 - 2022-12-18

### Fixed
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,21 @@ Installation guidelines are provided over here:
### System requirements

* Python3, for the magic to take place (supported versions: 3.7, 3.8, 3.9, 3.10 and 3.11).
* Tcl-Tk on Mac if you want to use VLC and if Python was installed with Brew (see note bellow).

At least one of there players:

* [VLC](https://www.videolan.org/vlc/) (supported version: 3.0.0 and higher, note that versions 3.0.13 to 3.0.16 cannot be used);
* [mpv](https://mpv.io/) (supported version: 0.27 and higher).

For 64 bits operating systems, you must install the equivalent version of the requirements.
Linux and Windows are supported.
Linux, Mac and Windows are supported.

### Note for Mac users

Tk has to be used to create a window for VLC, as it cannot do it automatically, unlike on the other supported operating systems.
If you have installed Python using Brew, the Tk library may not be installed, so you have to install it manually.
The library should be located automatically, but you can indicate its location with the environment variable `TK_LIBRARY_PATH`.

### Virtual environment

Expand Down Expand Up @@ -83,7 +90,7 @@ dakara-player create-config
python -m dakara_player create-config
```

and complete it with your values. The file is stored in your user space: `~/.config/dakara` on Linux, or `$APPDATA\DakaraProject\dakara` on Windows.
and complete it with your values. The file is stored in your user space: `~/.config/dakara` on Linux, `~/Library/Preferences/dakara` on Mac, or `$APPDATA\DakaraProject\dakara` on Windows.

### Configuration

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ classifiers = [
"Operating System :: OS Independent",
"Environment :: X11 Applications",
"Environment :: Win32 (MS Windows)",
"Environment :: MacOS X",
"Intended Audience :: End Users/Desktop",
]
dependencies = [
Expand Down
41 changes: 41 additions & 0 deletions src/dakara_player/font.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ def get_font_loader_class():
if system == "Linux":
return FontLoaderLinux

if system == "Darwin":
return FontLoaderMac

if system == "Windows":
return FontLoaderWindows

Expand Down Expand Up @@ -238,6 +241,44 @@ def unload_font(self, font_file_name):
)


class FontLoaderMac(FontLoaderLinux):
"""Font loader for Mac.

It copies fonts to load in the user fonts directory and removes them on
exit. Using symbolic links is not safe as the location of the package fonts
may not be permanent (see `importlib.resources.path` for more info).

See:
https://docs.python.org/3/library/importlib.html#importlib.resources.path

Example of use:

>>> with FontLoaderMac() as loader:
... loader.load()
... # do stuff while fonts are loaded
>>> # now fonts are unloaded

Args:
package (str): Package checked for font files.

Attributes:
package (str): Package checked for font files.
font_loaded (dict of path.Path): List of loaded fonts. The key is the
font file name and the value is the path of the installed font in
user directory.
"""

GREETINGS = "Font loader for Mac selected"
FONT_DIR_MACHINE = Path("/") / "Library" / "Fonts"
FONT_DIR_SYSTEM = Path("/") / "System" / "Library" / "Fonts"
FONT_DIR_USER = Path("~") / "Library" / "Fonts"

def get_system_font_path_list(self):
return list(self.FONT_DIR_MACHINE.walkfiles()) + list(
self.FONT_DIR_SYSTEM.walkfiles()
)


class FontLoaderWindows(FontLoader):
"""Font loader for Windows.

Expand Down
114 changes: 114 additions & 0 deletions src/dakara_player/mac.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import os
import sys
import tkinter
from ctypes import c_void_p, cdll
from subprocess import CalledProcessError, run

from path import Path

TK_LIBRARY_PATH = "TK_LIBRARY_PATH"
BREW = "brew"
BREW_PREFIX = "--prefix"
LIB = "lib"


def get_brew_prefix(formula):
"""Get the prefix of a Brew formula.

Args:
formula (str): Brew formula.

Returns:
path.Path: Path of the formula prefix. `None` if the prefix cannot be
obtained.
"""
try:
outcome = run(
[BREW, BREW_PREFIX, str(formula)],
capture_output=True,
check=True,
text=True,
)

except (CalledProcessError, FileNotFoundError):
return None

return Path(outcome.stdout.strip())


def get_tcl_tk_lib_path():
"""Retrieve the location of Tcl Tk library.

The location can be obtained either from the environment variable
`TK_LIBRARY_PATH`, or from Brew.

Returns:
path.Path: Path of Tck Tk library. `None` if it can't be found.
"""
# if the TK_LIBRARY_PATH environment variable exists, use it
if TK_LIBRARY_PATH in os.environ:
return Path(os.environ[TK_LIBRARY_PATH])

# try to obtain the location of Tk from Brew
prefix = get_brew_prefix("tcl-tk")
if prefix is None:
return None

return prefix / LIB


def load_get_ns_view():
"""Load the function to get NSView.

Load Tk library for Mac.

See:
https://github.com/oaubert/python-vlc/blob/38a90baf1d6c1e9a6131433ec3819766f308612c/examples/tkvlc.py#L52

Returns:
tuple: Contains:

- function: Function to get NSView object.
- bool: `True` if the function was found, `False` otherwise.
"""

# libtk = cdll.LoadLibrary(ctypes.util.find_library('tk'))
# returns the tk library /usr/lib/libtk.dylib from macOS,
# but we need the tkX.Y library bundled with Python 3+,
# to match the version number of tkinter, _tkinter, etc.
try:
libtk = "libtk{}.dylib".format(tkinter.TkVersion)

# if Python was installed with Brew, it doesn't have Tk, so Tcl-Tk must be
# installed separately and we have to retrieve its library path
lib_path = get_tcl_tk_lib_path()

if lib_path is None:
# use default locations
prefix = Path(getattr(sys, "base_prefix", sys.prefix))
lib_path = prefix / LIB

dylib = cdll.LoadLibrary(str(lib_path / libtk))

# getNSView = dylib.TkMacOSXDrawableView is the
# proper function to call, but that is non-public
# (in Tk source file macosx/TkMacOSXSubwindows.c)
# and dylib.TkMacOSXGetRootControl happens to call
# dylib.TkMacOSXDrawableView and return the NSView
_GetNSView = dylib.TkMacOSXGetRootControl
# C signature: void *_GetNSView(void *drawable) to get
# the Cocoa/Obj-C NSWindow.contentView attribute, the
# drawable NSView object of the (drawable) NSWindow
_GetNSView.restype = c_void_p
_GetNSView.argtypes = (c_void_p,)

found = True

except (NameError, OSError): # image or symbol not found

def _GetNSView(unused):
return None

found = False

return _GetNSView, found
5 changes: 4 additions & 1 deletion src/dakara_player/media_player/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def is_available():
bool: `True` if the media player is useable.
"""

def init_worker(self, config, tempdir, warn_long_exit=True):
def init_worker(self, window_comm, config, tempdir, warn_long_exit=True):
"""Initialize the base objects of the media player.

Actions performed in this method should not have any side effects
Expand All @@ -94,6 +94,9 @@ def init_worker(self, config, tempdir, warn_long_exit=True):
"""
self.check_is_available()

# window communication queue
self.window_comm = window_comm

# karaoke parameters
self.fullscreen = config.get("fullscreen", False)
self.kara_folder_path = Path(config.get("kara_folder", ""))
Expand Down
29 changes: 16 additions & 13 deletions src/dakara_player/media_player/mpv.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,17 +120,17 @@ def get_version():

@staticmethod
def get_class_from_version(version):
"""Get the mpv media player class according to installed version.
"""Get the mpv media player class according to provided version.

Args:
version (packaging.version.Version): Arbitrary mpv version to use.

Returns:
MediaPlayerMpv: Will return the class adapted to the version of mpv:
type: Will return the class adapted to the version of mpv:

- `MediaPlayerMpvPost0340` if mpv newer than 0.34.0;
- `MediaPlayerMpvPost0330` if mpv newer than 0.33.0;
- `MediaPlayerMpvOld` as default.
- `MediaPlayerMpvPost0340` if mpv newer than 0.34.0;
- `MediaPlayerMpvPost0330` if mpv newer than 0.33.0;
- `MediaPlayerMpvOld` as default.
"""
if version >= Version("0.34.0"):
logger.debug("Using post 0.34.0 API of mpv")
Expand All @@ -144,21 +144,24 @@ def get_class_from_version(version):
return MediaPlayerMpvOld

@staticmethod
def from_version(*args, **kwargs):
"""Instanciate the right mpv media player class.
def get_class(config=None):
"""Get the mpv media player class.

Args:
config (dakara_base.config.Config): Configuration. Used to
determine if mpv version should be automatically detected or
read from configuration.

Returns:
MediaPlayer: Instance of the mpv media player for the correct
version of mpv.
type: Will return the class.
"""
try:
config = kwargs.get("config") or args[2]
if config is not None and "mpv" in config and "force_version" in config["mpv"]:
version = Version(config["mpv"]["force_version"])

except (KeyError, IndexError):
else:
version = MediaPlayerMpv.get_version()

return MediaPlayerMpv.get_class_from_version(version)(*args, **kwargs)
return MediaPlayerMpv.get_class_from_version(version)


class MediaPlayerMpvOld(MediaPlayerMpv):
Expand Down
48 changes: 21 additions & 27 deletions src/dakara_player/media_player/vlc.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
from dakara_base.safe_workers import safe
from packaging.version import parse

from dakara_player.window import DummyWindowManager, WindowManager

try:
import vlc
from vlc import libvlc_get_version
Expand Down Expand Up @@ -106,18 +104,6 @@ def init_player(self, config, tempdir):
config_vlc = config.get("vlc") or {}
self.media_parameters = config_vlc.get("media_parameters") or []

# window for VLC
if config_vlc.get("use_default_window", False):
window_manager_class = DummyWindowManager

else:
window_manager_class = WindowManager

self.window = window_manager_class(
title="Dakara Player VLC",
fullscreen=self.fullscreen,
)

# VLC objects
self.instance = vlc.Instance(config_vlc.get("instance_parameters") or [])
self.player = self.instance.media_player_new()
Expand All @@ -136,8 +122,7 @@ def load_player(self):
self.check_version()

# assign window to VLC
self.window.open()
self.set_window(self.window.get_id())
self.set_window()

# set VLC callbacks
self.set_vlc_default_callbacks()
Expand Down Expand Up @@ -399,9 +384,6 @@ def stop_player(self):
self.player.stop()
logger.debug("Stopped player")

# closing window
self.window.close()

def set_playlist_entry_player(self, playlist_entry, file_path, autoplay):
"""Prepare playlist entry data to be played.

Expand Down Expand Up @@ -713,33 +695,45 @@ def handle_paused(self, event):

logger.debug("Paused")

def set_window(self, id):
def set_window(self):
"""Associate an existing window to VLC

Args:
id (int): ID of the window.

Raises:
NotImplementedError: If the current platform is not supported.
"""
if id is None:
window = self.window_comm.get()
system = platform.system()

if window is None:
logger.debug("Using VLC default window")

if system == "Darwin":
logger.error(
"VLC cannot create a window by itself on Mac, "
"the application may crash"
)

return

system = platform.system()

if system == "Linux":
logger.debug("Associating X window to VLC")
self.player.set_xwindow(id)
self.player.set_xwindow(window)
return

if system == "Darwin":
logger.debug("Associating AppKit window to VLC")
self.player.set_nsobject(window)
return

if system == "Windows":
logger.debug("Associating Win API window to VLC")
self.player.set_hwnd(id)
self.player.set_hwnd(window)
return

raise NotImplementedError(
"This operating system ({}) is not currently supported".format(system)
f"This operating system ({system}) is not currently supported"
)


Expand Down
Loading
Loading