check.py 14.8 KB
Newer Older
1
# -*- coding: utf-8 -*-
2
# Pitivi video editor
3
# Copyright (c) 2014, Mathieu Duponchelle <mduponchelle1@gmail.com>
Edward Hervey's avatar
Edward Hervey committed
4 5 6 7 8 9 10 11 12 13 14 15 16
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program; if not, write to the
Hicham HAOUARI's avatar
Hicham HAOUARI committed
17 18
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
19
"""Logic for ensuring important dependencies satisfy minimum requirements.
20 21

The checks here are supposed to take a negligible amount of time (< 0.2 seconds)
22
and not impact startup.
23 24

Package maintainers should look at the bottom section of this file.
Edward Hervey's avatar
Edward Hervey committed
25
"""
26
import os
27 28
import sys
from gettext import gettext as _
29

30
missing_soft_deps = {}
31
videosink_factory = None
32 33


34 35 36 37 38 39 40 41
def _version_to_string(version):
    return ".".join([str(x) for x in version])


def _string_to_list(version):
    return [int(x) for x in version.split(".")]


42
class Dependency(object):
43 44 45 46 47 48 49 50
    """Represents a module or component requirement.

    Args:
        modulename (str): The name identifying the component.
        version_required_string (Optional[str]): The minimum required version,
            if any, formatted like "X.Y.Z".
        additional_message (Optional[str]): Message displayed to the user to
            further explain the purpose of the missing component.
51
    """
52

53
    def __init__(self, modulename, version_required_string=None, additional_message=None):
54 55 56 57 58 59 60 61
        self.version_required_string = version_required_string
        self.modulename = modulename
        self.satisfied = False
        self.version_installed = None
        self.component = None
        self.additional_message = additional_message

    def check(self):
62 63 64
        """Checks whether the dependency is satisfied.

        Sets the `satisfied` field to True or False.
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
        """
        self.component = self._try_importing_component()

        if not self.component:
            self.satisfied = False
        elif self.version_required_string is None:
            self.satisfied = True
        else:
            formatted_version = self._format_version(self.component)
            self.version_installed = _version_to_string(formatted_version)

            if formatted_version >= _string_to_list(self.version_required_string):
                self.satisfied = True

    def _try_importing_component(self):
80 81 82 83
        """Performs the check.

        Returns:
            The dependent component.
84 85 86 87
        """
        raise NotImplementedError

    def _format_version(self, module):
88 89 90 91 92 93 94 95 96
        """Formats the version of the component.

        Args:
            module: The component returned by _try_importing_component.

        Returns:
            List[int]: The version number of the component.

            For example "1.2.10" should return [1, 2, 10].
97 98 99
        """
        raise NotImplementedError

100
    def __bool__(self):
101 102
        return self.satisfied

103 104 105 106 107
    def __repr__(self):
        if self.satisfied:
            return ""

        if not self.component:
108
            # Translators: %s is a Python module name or another os component
109
            message = _("- %s not found on the system") % self.modulename
110
        else:
111
            # Translators: %s is a Python module name or another os component
112 113
            message = _("- %s version %s is installed but Pitivi requires at least version %s") % (
                self.modulename, self.version_installed, self.version_required_string)
114

115 116
        if self.additional_message is not None:
            message += "\n    -> " + self.additional_message
117

118
        return message
119

Edward Hervey's avatar
Edward Hervey committed
120

121
class GIDependency(Dependency):
122

123 124
    def __init__(self, modulename, apiversion, version_required_string=None, additional_message=None):
        self.__api_version = apiversion
125
        Dependency.__init__(self, modulename, version_required_string, additional_message)
126

127 128
    def _try_importing_component(self):
        try:
129 130 131 132 133 134
            import gi
            try:
                gi.require_version(self.modulename, self.__api_version)
            except ValueError:
                return None

135 136 137 138 139
            __import__("gi.repository." + self.modulename)
            module = sys.modules["gi.repository." + self.modulename]
        except ImportError:
            module = None
        return module
140

141

142
class ClassicDependency(Dependency):
143

144 145 146 147 148 149 150
    def _try_importing_component(self):
        try:
            __import__(self.modulename)
            module = sys.modules[self.modulename]
        except ImportError:
            module = None
        return module
Edward Hervey's avatar
Edward Hervey committed
151 152


153
class GstPluginDependency(Dependency):
154

155 156 157 158
    def __init__(self, *args, **kwargs):
        self.__extra_modulenames = kwargs.pop("extra_modulenames", [])
        super().__init__(*args, **kwargs)

159
    def _try_importing_component(self):
160 161 162 163
        try:
            from gi.repository import Gst
        except ImportError:
            return None
164
        Gst.init(None)
165

166 167
        registry = Gst.Registry.get()
        plugin = registry.find_plugin(self.modulename)
168 169 170 171 172 173
        if not plugin and self.__extra_modulenames:
            for module in self.__extra_modulenames:
                plugin = registry.find_plugin(module)
                if plugin:
                    return plugin

174
        return plugin
Edward Hervey's avatar
Edward Hervey committed
175

176 177
    def _format_version(self, plugin):
        return _string_to_list(plugin.get_version())
178

179

180
class GstDependency(GIDependency):
181

182 183
    def _format_version(self, module):
        return list(module.version())
184

185

186
class GtkDependency(GIDependency):
187

188 189
    def _format_version(self, module):
        return [module.MAJOR_VERSION, module.MINOR_VERSION, module.MICRO_VERSION]
190 191


192
class CairoDependency(ClassicDependency):
193

194 195
    def __init__(self, version_required_string):
        ClassicDependency.__init__(self, "cairo", version_required_string)
196

197 198
    def _format_version(self, module):
        return _string_to_list(module.cairo_version_string())
199

200

201
def _check_audiosinks():
202
    from gi.repository import Gst
203 204
    # Yes, this can still fail, if PulseAudio is non-responsive for example.
    sink = Gst.ElementFactory.make("autoaudiosink", None)
205
    return sink
206 207


208
def _using_broadway_display():
209 210 211
    from gi.repository import Gdk
    from gi.repository import GObject
    try:
212
        gdk_broadway_display_type = GObject.type_from_name("GdkBroadwayDisplay")
213
    except RuntimeError:
214 215 216 217
        return False
    display = Gdk.Display.get_default()
    return GObject.type_is_a(display.__gtype__, gdk_broadway_display_type)

218

219 220 221 222 223 224 225 226 227
def _check_videosink():
    from gi.repository import Gst
    global videosink_factory

    # If using GdkBroadwayDisplay make sure not to try to use gtkglsink
    # as it would segfault right away.
    if not videosink_factory and \
            not _using_broadway_display() and \
            "gtkglsink" in os.environ.get("PITIVI_UNSTABLE_FEATURES", ""):
228
        sink = Gst.ElementFactory.make("gtkglsink", None)
229 230 231 232 233
        if sink:
            res = sink.set_state(Gst.State.READY)
            if res == Gst.StateChangeReturn.SUCCESS:
                videosink_factory = sink.get_factory()
                sink.set_state(Gst.State.NULL)
234

235 236
    if not videosink_factory:
        videosink_factory = Gst.ElementFactory.find("gtksink")
237

238
    return videosink_factory
239 240


241 242 243 244 245 246 247 248 249 250 251 252 253
def _check_vaapi():
    from gi.repository import Gst
    if "vaapi" in os.environ.get("PITIVI_UNSTABLE_FEATURES", ""):
        print("Vaapi decoders enabled.")
        return

    for feature in Gst.Registry.get().get_feature_list_by_plugin("vaapi"):
        if isinstance(feature, Gst.ElementFactory):
            klass = feature.get_klass()
            if "Decoder" in klass and "Video" in klass:
                feature.set_rank(Gst.Rank.MARGINAL)


254 255 256 257 258 259 260 261 262
def _check_gst_python():
    from gi.repository import Gst
    try:
        Gst.Fraction(9001, 1)  # It's over NINE THOUSANDS!
    except TypeError:
        return False  # What, nine thousands?! There's no way that can be right
    return True


263 264 265 266 267 268 269 270
class GICheck(ClassicDependency):
    def __init__(self, version_required_string):
        ClassicDependency.__init__(self, "gi", version_required_string)

    def _format_version(self, module):
        return list(module.version_info)


271
def check_requirements():
272
    """Checks Pitivi's dependencies are satisfied."""
273
    hard_dependencies_satisfied = True
274

275 276
    for dependency in HARD_DEPENDENCIES:
        dependency.check()
277 278 279
        if dependency.satisfied:
            continue
        if hard_dependencies_satisfied:
280
            hard_dependencies_satisfied = False
281 282 283 284
            header = _("ERROR - The following hard dependencies are unmet:")
            print(header)
            print("=" * len(header))
        print(dependency)
285 286 287 288 289

    for dependency in SOFT_DEPENDENCIES:
        dependency.check()
        if not dependency.satisfied:
            missing_soft_deps[dependency.modulename] = dependency
290
            print(_("Missing soft dependency:"))
291
            print(dependency)
292 293 294 295

    if not hard_dependencies_satisfied:
        return False

296
    if not _check_gst_python():
297 298
        print(_("ERROR — Could not create a Gst.Fraction — "
                "this means gst-python is not installed correctly."))
299 300
        return False

301
    if not _check_audiosinks():
302 303
        print(_("Could not create audio output sink. "
                "Make sure you have a valid one (pulsesink, alsasink or osssink)."))
304 305
        return False

306
    if not _check_videosink():
307 308
        print(_("Could not create video output sink. "
                "Make sure you have a gtksink available."))
309 310
        return False

311 312
    _check_vaapi()

313
    return True
314 315


316 317 318 319 320 321
def require_version(modulename, version):
    import gi

    try:
        gi.require_version(modulename, version)
    except ValueError:
322 323
        print(_("Could not import '%s'. Make sure you have it available.")
              % modulename)
324 325 326
        exit(1)


327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342
def get_square_width(video_info):
    """Applies the pixel aspect ratio to the width of the video info.

    Args:
        video_info (GstPbutils.DiscovererVideoInfo): The video info.

    Returns:
        int: The width calculated exactly as GStreamer does.
    """
    width = video_info.get_width()
    par_num = video_info.get_par_num()
    par_denom = video_info.get_par_denom()
    # We observed GStreamer does a simple int(), so we leave it like this.
    return int(width * par_num / par_denom)


343
def initialize_modules():
344
    """Initializes the modules.
345 346 347 348

    This has to be done in a specific order otherwise the app
    crashes on some systems.
    """
349 350 351
    try:
        import gi
    except ImportError:
352 353
        print(_("Could not import 'gi'. "
                "Make sure you have pygobject available."))
354 355 356 357
        exit(1)

    require_version("Gtk", GTK_API_VERSION)
    require_version("Gdk", GTK_API_VERSION)
358 359
    from gi.repository import Gdk
    Gdk.init([])
360 361 362 363 364
    from gi.repository import Gtk

    # Monkey patch deprecated methods to use the new variant by default
    Gtk.Layout.get_vadjustment = Gtk.Scrollable.get_vadjustment
    Gtk.Layout.get_hadjustment = Gtk.Scrollable.get_hadjustment
365 366 367 368 369

    if not gi.version_info >= (3, 11):
        from gi.repository import GObject
        GObject.threads_init()

370 371
    require_version("Gst", GST_API_VERSION)
    require_version("GstController", GST_API_VERSION)
372
    require_version("GstTranscoder", GST_API_VERSION)
373
    from gi.repository import Gst
374
    from pitivi.configure import get_audiopresets_dir, get_videopresets_dir
375
    Gst.init(None)
376

377 378 379
    require_version("GstPbutils", GST_API_VERSION)
    from gi.repository import GstPbutils

380
    # Monky patch a helper method for retrieving the size of a video
381
    # when using square pixels.
382
    GstPbutils.DiscovererVideoInfo.get_square_width = get_square_width
383

384 385 386
    if not os.environ.get("GES_DISCOVERY_TIMEOUT"):
        os.environ["GES_DISCOVERY_TIMEOUT"] = "5"

387
    require_version("GES", GST_API_VERSION)
388
    from gi.repository import GES
389
    res, sys.argv = GES.init_check(sys.argv)
390 391
    # Monkey patch deprecated methods to use the new variant by default
    GES.TrackElement.list_children_properties = GES.TimelineElement.list_children_properties
392

393
    from pitivi.utils import validate
394 395 396 397 398 399 400 401 402
    if validate.init() and "--inspect-action-type" in sys.argv:
        try:
            action_type = [sys.argv[1 + sys.argv.index("--inspect-action-type")]]
        except IndexError:
            action_type = []
        if validate.GstValidate.print_action_types(action_type):
            exit(0)
        else:
            exit(1)
403

404

405 406 407 408 409 410 411 412 413 414 415 416 417 418
# Package maintainers, this is where you can see the list of requirements.
# -----------------------------------------------------------------------------
#
# Those are either:
# - Classic Python modules
# - Dynamic Python bindings through GObject introspection ("GIDependency")
# - Something else. For example, there are various GStreamer plugins/elements
#   for which there is no clear detection method other than trying to instantiate;
#   there are special snowflakes like gst-python that are GI bindings "overrides"
#   for which there is no way to detect the version either.
#
# Some of our dependencies have version numbers requirements; for those without
# a specific version requirement, they have the "None" value.

419
GST_API_VERSION = "1.0"
420
GST_VERSION = "1.14.1"
421 422
GTK_API_VERSION = "3.0"
GLIB_API_VERSION = "2.0"
Thibault Saunier's avatar
Thibault Saunier committed
423
HARD_DEPENDENCIES = [GICheck("3.20.0"),
424
                     CairoDependency("1.10.0"),
425 426
                     GstDependency("Gst", GST_API_VERSION, GST_VERSION),
                     GstDependency("GES", GST_API_VERSION, GST_VERSION),
427
                     GIDependency("GstTranscoder", GST_API_VERSION),
428
                     GIDependency("GstVideo", GST_API_VERSION),
Thibault Saunier's avatar
Thibault Saunier committed
429
                     GtkDependency("Gtk", GTK_API_VERSION, "3.20.0"),
430
                     ClassicDependency("numpy"),
431
                     GIDependency("Gio", "2.0"),
432 433
                     GstPluginDependency("gtk"),
                     GstPluginDependency("gdkpixbuf"),
434
                     ClassicDependency("matplotlib"),
435
                     GIDependency("Peas", "1.0"),
436 437
                     ]

438
SOFT_DEPENDENCIES = (
439 440
    GIDependency("GSound", "1.0", None,
                 _("enables sound notifications when rendering is complete")),
441 442 443 444 445 446 447 448
    GIDependency("Notify", "0.7", None,
                 _("enables visual notifications when rendering is complete")),
    GstPluginDependency("libav", None,
                        _("additional multimedia codecs through the GStreamer Libav library")),
    GstPluginDependency("debugutilsbad", None,
                        _("enables a watchdog in the GStreamer pipeline."
                          " Use to detect errors happening in GStreamer"
                          " and recover from them")))