render.py 51.7 KB
Newer Older
1
# -*- coding: utf-8 -*-
2
# Pitivi video editor
3 4 5 6 7 8 9 10 11 12 13 14 15 16
# Copyright (c) 2009, Edward Hervey <bilboed@bilboed.com>
#
# 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
"""Rendering-related classes and utilities."""
20
import os
21
import posixpath
22
import time
23
from gettext import gettext as _
24

25
from gi.repository import GES
26
from gi.repository import Gio
27
from gi.repository import GLib
28
from gi.repository import GObject
29
from gi.repository import Gst
30
from gi.repository import Gtk
31

32
from pitivi import configure
33
from pitivi.check import missing_soft_deps
34
from pitivi.preset import EncodingTargetManager
35
from pitivi.utils.loggable import Loggable
36 37
from pitivi.utils.misc import path_from_uri
from pitivi.utils.misc import show_user_manual
38
from pitivi.utils.ripple_update_group import RippleUpdateGroup
39 40 41 42 43 44
from pitivi.utils.ui import audio_channels
from pitivi.utils.ui import audio_rates
from pitivi.utils.ui import beautify_ETA
from pitivi.utils.ui import frame_rates
from pitivi.utils.ui import get_combo_value
from pitivi.utils.ui import set_combo_value
45
from pitivi.utils.widgets import GstElementSettingsDialog
46
from pitivi.utils.widgets import TextWidget
47

48

49
class Encoders(Loggable):
50
    """Registry of avalaible Muxers, Audio encoders and Video encoders.
51

52
    Also keeps the avalaible combinations of those.
53

54
    It is a singleton. Use `Encoders()` to access the instance.
55 56

    Attributes:
57 58 59 60 61 62 63 64 65
        supported_muxers (List[Gst.ElementFactory]): The supported available
            muxers.
        supported_aencoders (List[Gst.ElementFactory]): The supported available
            audio encoders.
        supported_vencoders (List[Gst.ElementFactory]): The supported available
            video encoders.
        muxers (List[Gst.ElementFactory]): The available muxers.
        aencoders (List[Gst.ElementFactory]): The available audio encoders.
        vencoders (List[Gst.ElementFactory]): The available video encoders.
66 67 68 69
        compatible_audio_encoders (dict): Maps each muxer name to a list of
            compatible audio encoders ordered by rank.
        compatible_video_encoders (dict): Maps each muxer name to a list of
            compatible video encoders ordered by rank.
70 71 72 73 74 75 76 77 78 79 80 81 82
        default_muxer (str): The factory name of the default muxer.
        default_audio_encoder (str): The factory name of the default audio
            encoder.
        default_video_encoder (str): The factory name of the default video
            encoder.
    """

    OGG = "oggmux"
    MKV = "matroskamux"
    MP4 = "mp4mux"
    QUICKTIME = "qtmux"
    WEBM = "webmmux"

83 84 85 86
    if Gst.ElementFactory.find("fdkaacenc"):
        AAC = "fdkaacenc"
    else:
        AAC = "voaacenc"
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
    AC3 = "avenc_ac3_fixed"
    OPUS = "opusenc"
    VORBIS = "vorbisenc"

    JPEG = "jpegenc"
    THEORA = "theoraenc"
    VP8 = "vp8enc"
    X264 = "x264enc"

    SUPPORTED_ENCODERS_COMBINATIONS = [
        (OGG, VORBIS, THEORA),
        (OGG, OPUS, THEORA),
        (WEBM, VORBIS, VP8),
        (WEBM, OPUS, VP8),
        (MP4, AAC, X264),
        (MP4, AC3, X264),
        (QUICKTIME, AAC, JPEG),
        (MKV, OPUS, X264),
        (MKV, VORBIS, X264),
        (MKV, OPUS, JPEG),
        (MKV, VORBIS, JPEG)]
    """The combinations of muxers and encoders which are supported.

    Mirror of GES_ENCODING_TARGET_COMBINATIONS from
    https://cgit.freedesktop.org/gstreamer/gst-editing-services/tree/tests/validate/geslaunch.py
112
    """
113 114 115 116

    _instance = None

    def __new__(cls, *args, **kwargs):
117
        """Returns the singleton instance."""
118
        if not cls._instance:
119
            cls._instance = super(Encoders, cls).__new__(cls)
120 121 122
            # We have to initialize the instance here, otherwise
            # __init__ is called every time we use Encoders().
            Loggable.__init__(cls._instance)
123 124
            cls._instance._load_encoders()
            cls._instance._load_combinations()
125 126
        return cls._instance

127
    def _load_encoders(self):
128 129
        self.aencoders = []
        self.vencoders = []
130 131 132
        self.muxers = Gst.ElementFactory.list_get_elements(
            Gst.ELEMENT_FACTORY_TYPE_MUXER,
            Gst.Rank.SECONDARY)
133 134 135 136 137 138 139 140 141

        for fact in Gst.ElementFactory.list_get_elements(
                Gst.ELEMENT_FACTORY_TYPE_ENCODER, Gst.Rank.SECONDARY):
            klist = fact.get_klass().split('/')
            if "Video" in klist or "Image" in klist:
                self.vencoders.append(fact)
            elif "Audio" in klist:
                self.aencoders.append(fact)

142 143 144 145
    def _load_combinations(self):
        self.compatible_audio_encoders = {}
        self.compatible_video_encoders = {}
        useless_muxers = set()
146
        for muxer in self.muxers:
147 148 149 150 151
            aencs = self._find_compatible_encoders(self.aencoders, muxer)
            vencs = self._find_compatible_encoders(self.vencoders, muxer)
            if not aencs or not vencs:
                # The muxer is not compatible with no video encoder or
                # with no audio encoder.
152
                useless_muxers.add(muxer)
153 154 155 156 157
                continue

            muxer_name = muxer.get_name()
            self.compatible_audio_encoders[muxer_name] = aencs
            self.compatible_video_encoders[muxer_name] = vencs
158 159 160 161

        for muxer in useless_muxers:
            self.muxers.remove(muxer)

162 163 164 165 166 167 168 169 170 171 172 173 174 175
        self.factories_by_name = dict([(fact.get_name(), fact)
                                       for fact in self.muxers + self.aencoders + self.vencoders])

        good_muxers, good_aencoders, good_vencoders = zip(*self.SUPPORTED_ENCODERS_COMBINATIONS)
        self.supported_muxers = set([muxer
                                     for muxer in self.muxers
                                     if muxer.get_name() in good_muxers])
        self.supported_aencoders = set([encoder
                                        for encoder in self.aencoders
                                        if encoder.get_name() in good_aencoders])
        self.supported_vencoders = set([encoder
                                        for encoder in self.vencoders
                                        if encoder.get_name() in good_vencoders])

176 177 178 179
        self.default_muxer, \
            self.default_audio_encoder, \
            self.default_video_encoder = self._pick_defaults()

180
    def _find_compatible_encoders(self, encoders, muxer):
181
        """Returns the list of encoders compatible with the specified muxer."""
182
        res = []
183 184 185
        sink_caps = [template.get_caps()
                     for template in muxer.get_static_pad_templates()
                     if template.direction == Gst.PadDirection.SINK]
186
        for encoder in encoders:
187 188 189 190 191 192 193 194 195 196 197 198 199
            for template in encoder.get_static_pad_templates():
                if not template.direction == Gst.PadDirection.SRC:
                    continue
                if self._can_muxer_sink_caps(template.get_caps(), sink_caps):
                    res.append(encoder)
                    break
        return sorted(res, key=lambda encoder: - encoder.get_rank())

    def _can_muxer_sink_caps(self, output_caps, sink_caps):
        """Checks whether the specified caps match the muxer's receptors."""
        for caps in sink_caps:
            if not caps.intersect(output_caps).is_empty():
                return True
200
        return False
201

202 203 204 205 206 207 208
    def _pick_defaults(self):
        """Picks the defaults for new projects.

        Returns:
            (str, str, str): The muxer, audio encoder, video encoder.
        """
        for muxer, audio, video in self.SUPPORTED_ENCODERS_COMBINATIONS:
209 210 211
            if muxer not in self.factories_by_name or \
                    audio not in self.factories_by_name or \
                    video not in self.factories_by_name:
212 213 214 215 216 217
                continue
            self.info("Default encoders: %s, %s, %s", muxer, audio, video)
            return muxer, audio, video
        self.warning("No good combination of container and encoders available.")
        return Encoders.OGG, Encoders.VORBIS, Encoders.THEORA

218 219 220 221 222 223 224 225
    def is_supported(self, factory):
        """Returns whether the specified factory is supported."""
        if type(factory) is str:
            factory = self.factories_by_name[factory]
        return factory in self.supported_muxers or\
            factory in self.supported_aencoders or\
            factory in self.supported_vencoders

226

227
def beautify_factory_name(factory):
228 229 230
    """Returns a nice name for the specified Gst.ElementFactory instance.

    Intended for removing redundant words and shorten the codec names.
231 232 233 234 235 236

    Args:
        factory (Gst.ElementFactory): The factory which needs to be displayed.

    Returns:
        str: Cleaned up name.
237
    """
238 239
    # Only replace lowercase versions of "format", "video", "audio"
    # otherwise they might be part of a trademark name.
240
    words_to_remove = ["Muxer", "muxer", "Encoder", "encoder",
241 242 243
                       "format", "video", "audio", "instead",
                       # Incorrect naming for Sorenson Spark:
                       "Flash Video (FLV) /", ]
244
    words_to_replace = [["version ", "v"], ["Microsoft", "MS"], ]
245 246 247
    name = factory.get_longname()
    for word in words_to_remove:
        name = name.replace(word, "")
248 249
    for match, replacement in words_to_replace:
        name = name.replace(match, replacement)
250 251 252
    return " ".join(word for word in name.split())


253 254 255 256 257 258
def extension_for_muxer(muxer_name):
    """Returns the file extension appropriate for the specified muxer.

    Args:
        muxer_name (str): The name of the muxer factory.
    """
259 260 261
    exts = {
        "asfmux": "asf",
        "avimux": "avi",
262 263 264 265 266 267 268 269 270 271 272 273 274
        "avmux_3g2": "3g2",
        "avmux_avm2": "avm2",
        "avmux_dvd": "vob",
        "avmux_flv": "flv",
        "avmux_ipod": "mp4",
        "avmux_mpeg": "mpeg",
        "avmux_mpegts": "mpeg",
        "avmux_psp": "mp4",
        "avmux_rm": "rm",
        "avmux_svcd": "mpeg",
        "avmux_swf": "swf",
        "avmux_vcd": "mpeg",
        "avmux_vob": "vob",
275 276 277 278 279 280 281 282 283 284 285 286
        "flvmux": "flv",
        "gppmux": "3gp",
        "matroskamux": "mkv",
        "mj2mux": "mj2",
        "mp4mux": "mp4",
        "mpegpsmux": "mpeg",
        "mpegtsmux": "mpeg",
        "mvemux": "mve",
        "mxfmux": "mxf",
        "oggmux": "ogv",
        "qtmux": "mov",
        "webmmux": "webm"}
287
    return exts.get(muxer_name)
288 289


290 291 292 293 294 295 296
# --------------------------------- Public classes -----------------------------#

class RenderingProgressDialog(GObject.Object):

    __gsignals__ = {
        "pause": (GObject.SIGNAL_RUN_LAST, None, ()),
        "cancel": (GObject.SIGNAL_RUN_LAST, None, ()),
297 298 299
    }

    def __init__(self, app, parent):
300 301
        GObject.Object.__init__(self)

302
        self.app = app
303
        self.main_render_dialog = parent
304
        self.builder = Gtk.Builder()
305 306
        self.builder.add_from_file(
            os.path.join(configure.get_ui_dir(), "renderingprogress.ui"))
307 308 309 310 311 312
        self.builder.connect_signals(self)

        self.window = self.builder.get_object("render-progress")
        self.table1 = self.builder.get_object("table1")
        self.progressbar = self.builder.get_object("progressbar")
        self.play_pause_button = self.builder.get_object("play_pause_button")
313 314
        self.play_rendered_file_button = self.builder.get_object(
            "play_rendered_file_button")
315
        self.close_button = self.builder.get_object("close_button")
316 317
        self.show_in_file_manager_button = self.builder.get_object(
            "show_in_file_manager_button")
318
        self.cancel_button = self.builder.get_object("cancel_button")
319 320 321 322
        self._filesize_est_label = self.builder.get_object(
            "estimated_filesize_label")
        self._filesize_est_value_label = self.builder.get_object(
            "estimated_filesize_value_label")
323
        # Parent the dialog with mainwindow, since renderingdialog is hidden.
324
        # It allows this dialog to properly minimize together with mainwindow
325
        self.window.set_transient_for(self.app.gui)
326
        self.window.set_icon_name("system-run-symbolic")
327

328 329 330
        # We will only show the close/play buttons when the render is done:
        self.play_rendered_file_button.hide()
        self.close_button.hide()
331
        self.show_in_file_manager_button.hide()
332

333
    def updatePosition(self, fraction):
334
        self.progressbar.set_fraction(fraction)
335 336
        self.window.set_title(
            _("Rendering — %d%% complete") % int(100 * fraction))
337

338 339 340 341 342 343 344 345
    def updateProgressbarETA(self, time_estimation):
        # Translators: this string indicates the estimated time
        # remaining until an action (such as rendering) completes.
        # The "%s" is an already-localized human-readable duration,
        # such as "31 seconds", "1 minute" or "1 hours, 14 minutes".
        # In some languages, "About %s left" can be expressed roughly as
        # "There remains approximatively %s" (to handle gender and plurals).
        self.progressbar.set_text(_("About %s left") % time_estimation)
346

347
    def setFilesizeEstimate(self, estimated_filesize=None):
348 349 350 351 352 353 354 355
        if not estimated_filesize:
            self._filesize_est_label.hide()
            self._filesize_est_value_label.hide()
        else:
            self._filesize_est_value_label.set_text(estimated_filesize)
            self._filesize_est_label.show()
            self._filesize_est_value_label.show()

356
    def _deleteEventCb(self, unused_dialog_widget, unused_event):
357 358
        """Stops the rendering."""
        # The user closed the window by pressing Escape.
359 360
        self.emit("cancel")

361 362 363 364 365 366
    def _cancelButtonClickedCb(self, unused_button):
        self.emit("cancel")

    def _pauseButtonClickedCb(self, unused_button):
        self.emit("pause")

367 368 369 370 371 372 373
    def _closeButtonClickedCb(self, unused_button):
        self.window.destroy()
        if self.main_render_dialog.notification is not None:
            self.main_render_dialog.notification.close()
        self.main_render_dialog.window.show()

    def _playRenderedFileButtonClickedCb(self, unused_button):
374
        Gio.AppInfo.launch_default_for_uri(self.main_render_dialog.outfile, None)
375

376 377 378 379
    def _show_in_file_manager_button_clicked_cb(self, unused_button):
        directory_uri = posixpath.dirname(self.main_render_dialog.outfile)
        Gio.AppInfo.launch_default_for_uri(directory_uri, None)

380

381
class RenderDialog(Loggable):
382 383
    """Render dialog box.

384 385 386 387 388 389 390
    Args:
        app (Pitivi): The app.
        project (Project): The project to be rendered.

    Attributes:
        preferred_aencoder (str): The last audio encoder selected by the user.
        preferred_vencoder (str): The last video encoder selected by the user.
391
    """
392
    INHIBIT_REASON = _("Currently rendering")
393

394 395
    _factory_formats = {}

396
    def __init__(self, app, project):
397 398 399 400
        Loggable.__init__(self)

        self.app = app
        self.project = project
401
        self._pipeline = self.project.pipeline
402 403

        self.outfile = None
404
        self.notification = None
405 406 407 408 409

        # Variables to keep track of progress indication timers:
        self._filesizeEstimateTimer = self._timeEstimateTimer = None
        self._is_rendering = False
        self._rendering_is_paused = False
410
        self.current_position = None
411 412
        self._time_started = 0
        self._time_spent_paused = 0  # Avoids the ETA being wrong on resume
413 414 415 416 417

        # Various gstreamer signal connection ID's
        # {object: sigId}
        self._gstSigId = {}

418 419
        self.render_presets = EncodingTargetManager(project)
        self.render_presets.connect('profile-selected', self._encoding_profile_selected_cb)
420

421 422
        # Whether encoders changing are a result of changing the muxer.
        self.muxer_combo_changing = False
423
        self._createUi()
424

425 426 427 428 429 430 431
        # Directory and Filename
        self.filebutton.set_current_folder(self.app.settings.lastExportFolder)
        if not self.project.name:
            self.updateFilename(_("Untitled"))
        else:
            self.updateFilename(self.project.name)

432 433
        self._setting_encoding_profile = False

434
        # We store these so that when the user tries various container formats,
435
        # (AKA muxers) we select these a/v encoders, if they are compatible with
436
        # the current container format.
437 438
        self.preferred_vencoder = self.project.vencoder
        self.preferred_aencoder = self.project.aencoder
439
        self.__unproxiedClips = {}
440

441 442 443 444
        self.frame_rate_combo.set_model(frame_rates)
        self.channels_combo.set_model(audio_channels)
        self.sample_rate_combo.set_model(audio_rates)
        self.__initialize_muxers_model()
445 446 447 448
        self._displaySettings()
        self._displayRenderSettings()

        self.window.connect("delete-event", self._deleteEventCb)
449 450
        self.project.connect("rendering-settings-changed",
                             self._rendering_settings_changed_cb)
451 452 453 454 455 456 457

        # Monitor changes

        self.wg = RippleUpdateGroup()
        self.wg.addVertex(self.frame_rate_combo, signal="changed")
        self.wg.addVertex(self.channels_combo, signal="changed")
        self.wg.addVertex(self.sample_rate_combo, signal="changed")
458
        self.wg.addVertex(self.muxer_combo, signal="changed")
459 460
        self.wg.addVertex(self.audio_encoder_combo, signal="changed")
        self.wg.addVertex(self.video_encoder_combo, signal="changed")
461 462
        self.wg.addVertex(self.preset_menubutton,
                          update_func=self._updatePresetMenuButton)
463

464 465 466
        self.wg.addEdge(self.frame_rate_combo, self.preset_menubutton)
        self.wg.addEdge(self.audio_encoder_combo, self.preset_menubutton)
        self.wg.addEdge(self.video_encoder_combo, self.preset_menubutton)
467
        self.wg.addEdge(self.muxer_combo, self.preset_menubutton)
468 469
        self.wg.addEdge(self.channels_combo, self.preset_menubutton)
        self.wg.addEdge(self.sample_rate_combo, self.preset_menubutton)
470

471 472 473 474 475 476
    def _encoding_profile_selected_cb(self, unused_target, encoding_profile):
        self._set_encoding_profile(encoding_profile)

    def _set_encoding_profile(self, encoding_profile, recursing=False):
        old_profile = self.project.container_profile

477
        def rollback():
478 479 480 481 482 483 484 485
            if recursing:
                return

            self._set_encoding_profile(old_profile, True)

        def factory(x):
            return Encoders().factories_by_name.get(getattr(self.project, x))

486
        self.project.set_container_profile(encoding_profile)
487 488 489
        self._setting_encoding_profile = True

        if not set_combo_value(self.muxer_combo, factory('muxer')):
490 491
            rollback()
            return
492 493

        self.updateAvailableEncoders()
494 495
        self._update_valid_audio_restrictions(Gst.ElementFactory.find(self.project.aencoder))
        self._update_valid_video_restrictions(Gst.ElementFactory.find(self.project.vencoder))
496 497 498 499 500 501
        for i, (combo, name, value) in enumerate([
                (self.audio_encoder_combo, "aencoder", factory("aencoder")),
                (self.video_encoder_combo, "vencoder", factory("vencoder")),
                (self.sample_rate_combo, "audiorate", self.project.audiorate),
                (self.channels_combo, "audiochannels", self.project.audiochannels),
                (self.frame_rate_combo, "videorate", self.project.videorate)]):
502
            if value is None:
503 504
                self.error("%d - Got no value for %s (%s)... rolling back",
                           i, name, combo)
505 506
                rollback()
                return
507 508 509 510

            if not set_combo_value(combo, value):
                self.error("%d - Could not set value %s for combo %s... rolling back",
                           i, value, combo)
511 512
                rollback()
                return
513 514

        self.updateResolution()
515
        self.project.add_encoding_profile(self.project.container_profile)
516
        self._setting_encoding_profile = False
517

518 519
    def _updatePresetMenuButton(self, unused_source, unused_target):
        self.render_presets.updateMenuActions()
520

521 522 523
    def muxer_setter(self, widget, muxer_name):
        set_combo_value(widget, Encoders().factories_by_name.get(muxer_name))
        self.project.setEncoders(muxer=muxer_name)
524 525 526 527 528 529

        # Update the extension of the filename.
        basename = os.path.splitext(self.fileentry.get_text())[0]
        self.updateFilename(basename)

        # Update muxer-dependent widgets.
530
        self.updateAvailableEncoders()
531

532 533 534
    def acodec_setter(self, widget, aencoder_name):
        set_combo_value(widget, Encoders().factories_by_name.get(aencoder_name))
        self.project.aencoder = aencoder_name
535
        if not self.muxer_combo_changing:
536
            # The user directly changed the audio encoder combo.
537
            self.preferred_aencoder = aencoder_name
538

539 540 541
    def vcodec_setter(self, widget, vencoder_name):
        set_combo_value(widget, Encoders().factories_by_name.get(vencoder_name))
        self.project.setEncoders(vencoder=vencoder_name)
542
        if not self.muxer_combo_changing:
543
            # The user directly changed the video encoder combo.
544
            self.preferred_vencoder = vencoder_name
545 546

    def sample_rate_setter(self, widget, value):
547 548
        set_combo_value(widget, value)
        self.project.audiorate = value
549 550

    def channels_setter(self, widget, value):
551 552
        set_combo_value(widget, value)
        self.project.audiochannels = value
553 554

    def framerate_setter(self, widget, value):
555 556
        set_combo_value(widget, value)
        self.project.videorate = value
557

558 559
    def _createUi(self):
        builder = Gtk.Builder()
560 561
        builder.add_from_file(
            os.path.join(configure.get_ui_dir(), "renderingdialog.ui"))
562 563 564
        builder.connect_signals(self)

        self.window = builder.get_object("render-dialog")
565 566 567 568
        self.video_output_checkbutton = builder.get_object(
            "video_output_checkbutton")
        self.audio_output_checkbutton = builder.get_object(
            "audio_output_checkbutton")
569
        self.render_button = builder.get_object("render_button")
570 571 572 573
        self.video_settings_button = builder.get_object(
            "video_settings_button")
        self.audio_settings_button = builder.get_object(
            "audio_settings_button")
574
        self.frame_rate_combo = builder.get_object("frame_rate_combo")
575
        self.frame_rate_combo.set_model(frame_rates)
576 577
        self.scale_spinbutton = builder.get_object("scale_spinbutton")
        self.channels_combo = builder.get_object("channels_combo")
578
        self.channels_combo.set_model(audio_channels)
579
        self.sample_rate_combo = builder.get_object("sample_rate_combo")
580
        self.muxer_combo = builder.get_object("muxercombobox")
581 582 583 584 585
        self.audio_encoder_combo = builder.get_object("audio_encoder_combo")
        self.video_encoder_combo = builder.get_object("video_encoder_combo")
        self.filebutton = builder.get_object("filebutton")
        self.fileentry = builder.get_object("fileentry")
        self.resolution_label = builder.get_object("resolution_label")
586 587
        self.preset_menubutton = builder.get_object("preset_menubutton")

588 589 590 591 592 593
        text_widget = TextWidget(matches=r'^[a-z][a-z-0-9-]+$', combobox=True)
        self.presets_combo = text_widget.combo
        preset_table = builder.get_object("preset_table")
        preset_table.attach(text_widget, 1, 0, 1, 1)
        text_widget.show()

594 595 596
        self.video_output_checkbutton.props.active = self.project.video_profile.is_enabled()
        self.audio_output_checkbutton.props.active = self.project.audio_profile.is_enabled()

597 598 599 600 601 602 603 604 605
        self.__automatically_use_proxies = builder.get_object(
            "automatically_use_proxies")

        self.__always_use_proxies = builder.get_object("always_use_proxies")
        self.__always_use_proxies.props.group = self.__automatically_use_proxies

        self.__never_use_proxies = builder.get_object("never_use_proxies")
        self.__never_use_proxies.props.group = self.__automatically_use_proxies

606
        self.render_presets.setupUi(self.presets_combo, self.preset_menubutton)
607
        self.render_presets.loadAll()
608

609
        self.window.set_icon_name("system-run-symbolic")
610 611
        self.window.set_transient_for(self.app.gui)

612 613
    def _rendering_settings_changed_cb(self, unused_project, unused_item):
        """Handles Project metadata changes."""
614 615
        self.updateResolution()

616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645
    def __initialize_muxers_model(self):
        # By default show only supported muxers and encoders.
        model = self.create_combobox_model(Encoders().muxers)
        self.muxer_combo.set_model(model)

    def create_combobox_model(self, factories):
        """Creates a model for a combobox showing factories.

        Args:
            combobox (Gtk.ComboBox): The combobox to setup.
            factories (List[Gst.ElementFactory]): The factories to display.

        Returns:
            Gtk.ListStore: The model with (display name, factory, unsupported).
        """
        model = Gtk.TreeStore(str, object)
        data_supported = []
        data_unsupported = []
        for factory in factories:
            supported = Encoders().is_supported(factory)
            row = (beautify_factory_name(factory), factory)
            if supported:
                data_supported.append(row)
            else:
                data_unsupported.append(row)

        data_supported.sort()
        for row in data_supported:
            model.append(None, row)

646 647 648 649 650 651 652 653
        if data_unsupported:
            # Translators: This item appears in a combobox's popup and
            # contains as children the unsupported (but still available)
            # muxers and encoders.
            unsupported_iter = model.append(None, (_("Unsupported"), None))
            data_unsupported.sort()
            for row in data_unsupported:
                model.append(unsupported_iter, row)
654 655

        return model
656 657

    def _displaySettings(self):
658
        """Displays the settings also in the ProjectSettingsDialog."""
659
        # Video settings
660
        set_combo_value(self.frame_rate_combo, self.project.videorate)
661
        # Audio settings
662 663
        set_combo_value(self.channels_combo, self.project.audiochannels)
        set_combo_value(self.sample_rate_combo, self.project.audiorate)
664 665

    def _displayRenderSettings(self):
666
        """Displays the settings available only in the RenderDialog."""
667
        # Video settings
668
        # This will trigger an update of the video resolution label.
669
        self.scale_spinbutton.set_value(self.project.render_scale)
670
        # Muxer settings
671
        # This will trigger an update of the codec comboboxes.
672 673
        set_combo_value(self.muxer_combo,
                        Encoders().factories_by_name.get(self.project.muxer))
674

675
    def _checkForExistingFile(self, *unused_args):
676
        """Displays a warning if the file path already exists."""
677 678 679 680
        path = self.filebutton.get_current_folder()
        if not path:
            # This happens when the window is initialized.
            return
681
        warning_icon = "dialog-warning"
682 683 684 685 686 687 688 689 690 691
        filename = self.fileentry.get_text()
        if not filename:
            tooltip_text = _("A file name is required.")
        elif filename and os.path.exists(os.path.join(path, filename)):
            tooltip_text = _("This file already exists.\n"
                             "If you don't want to overwrite it, choose a "
                             "different file name or folder.")
        else:
            warning_icon = None
            tooltip_text = None
692
        self.fileentry.set_icon_from_icon_name(1, warning_icon)
693 694
        self.fileentry.set_icon_tooltip_text(1, tooltip_text)

695
    def _getFilesizeEstimate(self):
696
        """Estimates the final file size.
697 698 699 700

        Estimates in megabytes (over 30 MB) are rounded to the nearest 10 MB
        to smooth out small variations. You'd be surprised how imprecision can
        improve perceived accuracy.
701 702 703

        Returns:
            str: A human-readable (ex: "14 MB") estimate for the file size.
704
        """
705
        if not self.current_position:
706 707 708
            return None

        current_filesize = os.stat(path_from_uri(self.outfile)).st_size
709
        length = self.project.ges_timeline.props.duration
710 711
        estimated_size = float(
            current_filesize * float(length) / self.current_position)
712 713 714 715
        # Now let's make it human-readable (instead of octets).
        # If it's in the giga range (10⁹) instead of mega (10⁶), use 2 decimals
        if estimated_size > 10e8:
            gigabytes = estimated_size / (10 ** 9)
716
            return _("%.2f GB") % gigabytes
717 718
        else:
            megabytes = int(estimated_size / (10 ** 6))
719 720
            if megabytes > 30:
                megabytes = int(round(megabytes, -1))  # -1 means round to 10
721
            return _("%d MB") % megabytes
722

723 724
    def updateFilename(self, basename):
        """Updates the filename UI element to show the specified file name."""
725
        extension = extension_for_muxer(self.project.muxer)
726 727 728 729 730 731
        if extension:
            name = "%s%s%s" % (basename, os.path.extsep, extension)
        else:
            name = basename
        self.fileentry.set_text(name)

732
    def _update_valid_restriction_values(self, caps, combo, caps_template,
733
                               model, combo_value,
734 735 736 737 738 739 740 741 742
                               caps_template_expander=None):
        def caps_template_expander_func(caps_template, value):
            return caps_template % value

        if not caps_template_expander:
            caps_template_expander = caps_template_expander_func

        model_headers = [model.get_column_type(i) for i in range(model.get_n_columns())]
        reduced_model = Gtk.ListStore(*model_headers)
743
        reduced = []
744 745 746
        for name, value in dict(model).items():
            ecaps = Gst.Caps(caps_template_expander(caps_template, value))
            if not caps.intersect(ecaps).is_empty():
747
                reduced.append((name, value))
748

749 750
        for v in sorted(reduced, key=lambda v: float(v[1])):
            reduced_model.append(v)
751 752
        combo.set_model(reduced_model)

753
        set_combo_value(combo, combo_value)
754
        if get_combo_value(combo) != combo_value:
755 756
            combo.set_active(len(reduced_model) - 1)
            self.warning("%s in %s not supported, setting: %s",
757
                         combo_value, caps_template, get_combo_value(combo))
758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784

    def _update_valid_audio_restrictions(self, factory):
        template = [t for t in factory.get_static_pad_templates()
                    if t.direction == Gst.PadDirection.SINK][0]

        caps = template.static_caps.get()
        self._update_valid_restriction_values(caps, self.sample_rate_combo,
                                              "audio/x-raw,rate=(int)%d",
                                              audio_rates,
                                              self.project.audiorate)

        self._update_valid_restriction_values(caps, self.channels_combo,
                                              "audio/x-raw,channels=(int)%d",
                                              audio_channels,
                                              self.project.audiochannels)

    def _update_valid_video_restrictions(self, factory):
        def fraction_expander_func(caps_template, value):
            return caps_template % (value.num, value.denom)

        template = [t for t in factory.get_static_pad_templates()
                    if t.direction == Gst.PadDirection.SINK][0]

        caps = template.static_caps.get()
        self._update_valid_restriction_values(
            caps, self.frame_rate_combo,
            "video/x-raw,framerate=(GstFraction)%d/%d", frame_rates,
785
            self.project.videorate,
786 787
            caps_template_expander=fraction_expander_func)

788
    def updateAvailableEncoders(self):
789
        """Updates the encoder comboboxes to show the available encoders."""
790 791 792 793 794 795 796 797 798 799 800 801 802 803 804
        self.muxer_combo_changing = True
        try:
            model = self.create_combobox_model(
                Encoders().compatible_video_encoders[self.project.muxer])
            self.video_encoder_combo.set_model(model)
            self._update_encoder_combo(self.video_encoder_combo,
                                       self.preferred_vencoder)

            model = self.create_combobox_model(
                Encoders().compatible_audio_encoders[self.project.muxer])
            self.audio_encoder_combo.set_model(model)
            self._update_encoder_combo(self.audio_encoder_combo,
                                       self.preferred_aencoder)
        finally:
            self.muxer_combo_changing = False
805

806
    def _update_encoder_combo(self, encoder_combo, preferred_encoder):
807
        """Selects the specified encoder for the specified encoder combo."""
808 809
        if preferred_encoder:
            # A preference exists, pick it if it can be found in
810
            # the current model of the combobox.
811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827
            encoder = Encoders().factories_by_name.get(preferred_encoder)
            set_combo_value(encoder_combo, encoder)
        if not preferred_encoder or not get_combo_value(encoder_combo):
            # No preference exists or it is not available,
            # pick the first encoder from the combobox's model.
            first = encoder_combo.props.model.get_iter_first()
            if not first:
                # Model is empty. Should not happen.
                self.warning("Model is empty")
                return
            if not encoder_combo.props.model.iter_has_child(first):
                # The first item is a supported factory.
                encoder_combo.set_active_iter(first)
            else:
                # The first element is the Unsupported group.
                second = encoder_combo.props.model.iter_nth_child(first, 0)
                encoder_combo.set_active_iter(second)
828

829
    def _elementSettingsDialog(self, factory, media_type):
830
        """Opens a dialog to edit the properties for the specified factory.
831

832 833
        Args:
            factory (Gst.ElementFactory): The factory for editing.
834
            media_type (str): String describing the media type ('audio' or 'video')
835
        """
836 837 838
        # Reconsitute the property name from the media type (vcodecsettings or acodecsettings)
        properties = getattr(self.project, media_type[0] + 'codecsettings')

839
        self.dialog = GstElementSettingsDialog(factory, properties=properties,
840
                                               caps=getattr(self.project, media_type + '_profile').get_format(),
841
                                               parent_window=self.window)
842
        self.dialog.ok_btn.connect(
843
            "clicked", self._okButtonClickedCb, media_type)
844

845 846 847 848 849 850 851 852
    def __additional_debug_info(self, error):
        if self.project.vencoder == 'x264enc':
            if self.project.videowidth % 2 or self.project.videoheight % 2:
                return "\n\n%s\n\n" % _("<b>Make sure your rendering size is even, "
                         "x264enc might not be able to render otherwise.</b>\n\n")

        return ""

853
    def _showRenderErrorDialog(self, error, unused_details):
854
        primary_message = _("Sorry, something didn’t work right.")
855
        secondary_message = _("An error occurred while trying to render your "
856
                              "project.") + self.__additional_debug_info(str(error)) + _(
857 858
            "You might want to check our troubleshooting guide or file a bug report. "
            "The GStreamer error was:") + "\n\n<i>" + str(error) + "</i>"
859

860
        dialog = Gtk.MessageDialog(transient_for=self.window, modal=True,
861 862
                                   message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK,
                                   text=primary_message)
863
        dialog.set_property("secondary-text", secondary_message)
864 865
        dialog.set_property("secondary-use-markup", True)
        dialog.show_all()
866 867 868
        dialog.run()
        dialog.destroy()

869
    def startAction(self):
870
        """Starts the render process."""
871
        self._pipeline.set_state(Gst.State.NULL)
872 873
        # FIXME: https://github.com/pitivi/gst-editing-services/issues/23
        self._pipeline.set_mode(GES.PipelineFlags.RENDER)
874
        encodebin = self._pipeline.get_by_name("internal-encodebin")
875
        self._gstSigId[encodebin] = encodebin.connect(
876
            "element-added", self.__element_added_cb)
877
        for element in encodebin.iterate_recurse():
878
            self.__set_properties(element)
879
        self._pipeline.set_state(Gst.State.PLAYING)
880 881
        self._is_rendering = True
        self._time_started = time.time()
882

883 884
    def _cancelRender(self, *unused_args):
        self.debug("Aborting render")
885 886 887 888
        self._shutDown()
        self._destroyProgressWindow()

    def _shutDown(self):
889
        """Shuts down the pipeline and disconnects from its signals."""
890 891 892
        self._is_rendering = False
        self._rendering_is_paused = False
        self._time_spent_paused = 0
893
        self._pipeline.set_state(Gst.State.NULL)
894
        self.project.set_rendering(False)
895
        self.__useProxyAssets()
896
        self._disconnectFromGst()
897
        self._pipeline.set_mode(GES.PipelineFlags.FULL_PREVIEW)
898
        self._pipeline.set_state(Gst.State.PAUSED)
899

900
    def _pauseRender(self, unused_progress):
901 902
        self._rendering_is_paused = self.progress.play_pause_button.get_active(
        )
903 904 905
        if self._rendering_is_paused:
            self._last_timestamp_when_pausing = time.time()
        else:
906 907 908 909
            self._time_spent_paused += time.time(
            ) - self._last_timestamp_when_pausing
            self.debug(
                "Resuming render after %d seconds in pause", self._time_spent_paused)
910
        self.project.pipeline.togglePlayback()
911 912

    def _destroyProgressWindow(self):
913
        """Handles the completion or the cancellation of the render process."""
914 915
        self.progress.window.destroy()
        self.progress = None
916
        self.window.show()  # Show the rendering dialog again
917 918

    def _disconnectFromGst(self):
919
        for obj, id in self._gstSigId.items():
920 921
            obj.disconnect(id)
        self._gstSigId = {}
922
        try:
923
            self.project.pipeline.disconnect_by_func(self._updatePositionCb)
924 925 926
        except TypeError:
            # The render was successful, so this was already disconnected
            pass
927 928 929 930

    def destroy(self):
        self.window.destroy()

931 932 933
    def _maybe_play_finished_sound(self):
        """Plays a sound to signal the render operation is done."""
        if "GSound" in missing_soft_deps:
934
            return
935 936 937 938 939 940 941
        from gi.repository import GSound
        sound_context = GSound.Context()
        try:
            sound_context.init()
            sound_context.play_simple({GSound.ATTR_EVENT_ID: "complete"})
        except GLib.Error as e:
            self.warning("GSound failed to play: %s", e)
942

943 944 945 946 947
    def __maybeUseSourceAsset(self):
        if self.__always_use_proxies.get_active():
            self.debug("Rendering from proxies, not replacing assets")
            return

948
        for layer in self.app.project_manager.current_project.ges_timeline.get_layers():
949 950 951 952 953 954 955
            for clip in layer.get_clips():
                if not isinstance(clip, GES.UriClip):
                    continue

                asset = clip.get_asset()
                asset_target = asset.get_proxy_target()
                if not asset_target:
956
                    # The asset is not a proxy.
957 958 959
                    continue

                if self.__automatically_use_proxies.get_active():
960
                    if not self.app.proxy_manager.isAssetFormatWellSupported(
961
                            asset_target):
962
                        self.info("Original asset %s format not well supported, "
963 964 965 966
                                  "rendering from proxy.",
                                  asset_target.props.id)
                        continue

967 968 969 970 971 972 973 974 975 976 977 978 979
                    self.info("Original asset %s format well supported, "
                              "rendering from real asset.",
                              asset_target.props.id)

                if asset_target.get_error():
                    # The original asset cannot be used.
                    continue

                clip.set_asset(asset_target)
                self.info("Using original asset %s (instead of proxy %s)",
                          asset_target.get_id(),
                          asset.get_id())
                self.__unproxiedClips[clip] = asset
980 981 982

    def __useProxyAssets(self):
        for clip, asset in self.__unproxiedClips.items():
983
            self.info("Reverting to using proxy asset %s", asset)
984 985 986 987
            clip.set_asset(asset)

        self.__unproxiedClips = {}

988
    # ------------------- Callbacks ------------------------------------------ #
989

990
    # -- UI callbacks
991 992 993 994 995 996 997
    def _okButtonClickedCb(self, unused_button, media_type):
        assert(media_type in ("audio", "video"))
        setattr(self.project, media_type[0] + 'codecsettings', self.dialog.getSettings())

        caps = self.dialog.get_caps()
        if caps:
            getattr(self.project, media_type + '_profile').set_format(caps)
998 999 1000
        self.dialog.window.destroy()

    def _renderButtonClickedCb(self, unused_button):
1001
        """Starts the rendering process."""
1002
        self.__maybeUseSourceAsset()
1003 1004
        self.outfile = os.path.join(self.filebutton.get_uri(),
                                    self.fileentry.get_text())
1005
        self.progress = RenderingProgressDialog(self.app, self)
1006 1007
        # Hide the rendering settings dialog while rendering
        self.window.hide()
1008

1009
        self.app.gui.editor.timeline_ui.timeline.set_best_zoom_ratio(allow_zoom_in=True)
1010
        self.project.set_rendering(True)
1011 1012
        self._pipeline.set_render_settings(
            self.outfile, self.project.container_profile)
1013 1014 1015 1016 1017 1018 1019
        self.startAction()
        self.progress.window.show()
        self.progress.connect("cancel", self._cancelRender)
        self.progress.connect("pause", self._pauseRender)
        bus = self._pipeline.get_bus()
        bus.add_signal_watch()
        self._gstSigId[bus] = bus.connect('message', self._busMessageCb)
1020
        self.project.pipeline.connect("position", self._updatePositionCb)
1021 1022
        # Force writing the config now, or the path will be reset
        # if the user opens the rendering dialog again
1023 1024
        self.app.settings.lastExportFolder = self.filebutton.get_current_folder(
        )
1025
        self.app.settings.storeSettings()
1026 1027 1028

    def _closeButtonClickedCb(self, unused_button):
        self.debug("Render dialog's Close button clicked")
1029
        self.project.disconnect_by_func(self._rendering_settings_changed_cb)
1030 1031
        self.destroy()

1032
    def _deleteEventCb(self, unused_window, unused_event):
1033 1034 1035
        self.debug("Render dialog is being deleted")
        self.destroy()

1036 1037 1038
    def _containerContextHelpClickedCb(self, unused_button):
        show_user_manual("codecscontainers")

1039
    # Periodic (timer) callbacks
1040 1041
    def _updateTimeEstimateCb(self):
        if self._rendering_is_paused:
1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052
            # Do nothing until we resume rendering
            return True
        if self._is_rendering:
            if self.current_position:
                timediff = time.time() - self._time_started - self._time_spent_paused
                length = self.project.ges_timeline.props.duration
                estimated_time = timediff * length / self.current_position
                remaining_time = estimated_time - timediff
                estimate = beautify_ETA(int(remaining_time * Gst.SECOND))
                if estimate:
                    self.progress.updateProgressbarETA(estimate)
1053 1054 1055 1056
            return True
        else:
            self._timeEstimateTimer = None
            self.debug("Stopping the ETA timer")
1057
            return False
1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071

    def _updateFilesizeEstimateCb(self):
        if self._rendering_is_paused:
            return True  # Do nothing until we resume rendering
        elif self._is_rendering:
            est_filesize = self._getFilesizeEstimate()
            if est_filesize:
                self.progress.setFilesizeEstimate(est_filesize)
            return True
        else:
            self.debug("Stopping the filesize estimation timer")
            self._filesizeEstimateTimer = None
            return False  # Stop the timer

1072
    # GStreamer callbacks
1073
    def _busMessageCb(self, unused_bus, message):
1074
        if message.type == Gst.MessageType.EOS:  # Render complete
1075 1076
            self.debug("got EOS message, render complete")
            self._shutDown()
1077
            self.progress.progressbar.set_fraction(1.0)
1078 1079
            self.progress.progressbar.set_text(_("Render complete"))
            self.progress.window.set_title(_("Render complete"))
1080
            self.progress.setFilesizeEstimate(None)
1081
            if not self.progress.window.is_active():
1082
                notification = _(
1083
                    '"%s" has finished rendering.') % self.fileentry.get_text()
1084 1085
                self.notification = self.app.system.desktopMessage(
                    _("Render complete"), notification, "pitivi")
1086
            self._maybe_play_finished_sound()
1087 1088
            self.progress.play_rendered_file_button.show()
            self.progress.close_button.show()
1089
            self.progress.show_in_file_manager_button.show()
1090 1091 1092
            self.progress.cancel_button.hide()
            self.progress.play_pause_button.hide()

1093 1094 1095 1096 1097 1098 1099
        elif message.type == Gst.MessageType.ERROR:
            # Errors in a GStreamer pipeline are fatal. If we encounter one,
            # we should abort and show the error instead of sitting around.
            error, details = message.parse_error()
            self._cancelRender()
            self._showRenderErrorDialog(error, details)

1100
        elif message.type == Gst.MessageType.STATE_CHANGED and self.progress:
1101
            if message.src == self._pipeline:
1102 1103 1104
                prev, state, pending = message.parse_state_changed()
                if pending == Gst.State.VOID_PENDING:
                    # State will not change further.
1105
                    if state == Gst.State.PLAYING:
1106 1107 1108
                        self.debug("Inhibiting sleep when rendering")
                        self.app.simple_inhibit(RenderDialog.INHIBIT_REASON,
                                                Gtk.ApplicationInhibitFlags.SUSPEND)
1109
                    else:
1110
                        self.app.simple_uninhibit(RenderDialog.INHIBIT_REASON)
1111

1112
    def _updatePositionCb(self, unused_pipeline, position):
1113 1114 1115 1116
        """Updates the progress bar and triggers the update of the file size.

        This one occurs every time the pipeline emits a position changed signal,
        which is *very* often.
1117
        """
1118
        self.current_position = position
1119 1120 1121
        if not self.progress or not position:
            return

1122
        length = self.project.ges_timeline.props.duration
1123 1124 1125 1126 1127 1128 1129 1130 1131
        fraction = float(min(position, length)) / float(length)
        self.progress.updatePosition(fraction)

        # In order to have enough averaging, only display the ETA after 5s
        timediff = time.time() - self._time_started
        if not self._timeEstimateTimer:
            if timediff < 6:
                self.progress.progressbar.set_text(_("Estimating..."))
            else:
1132 1133
                self._timeEstimateTimer = GLib.timeout_add_seconds(
                    3, self._updateTimeEstimateCb)
1134

1135
        # Filesize is trickier and needs more time to be meaningful.
1136
        if not self._filesizeEstimateTimer and (fraction > 0.33 or timediff > 180):
1137 1138
            self._filesizeEstimateTimer = GLib.timeout_add_seconds(
                5, self._updateFilesizeEstimateCb)
1139

1140 1141 1142 1143
    def __element_added_cb(self, unused_bin, gst_element):
        self.__set_properties(gst_element)

    def __set_properties(self, gst_element):
1144 1145
        """Sets properties on the specified Gst.Element."""
        factory = gst_element.get_factory()
1146 1147 1148 1149 1150 1151
        settings = {}
        if factory == get_combo_value(self.video_encoder_combo):
            settings = self.project.vcodecsettings
        elif factory == get_combo_value(self.audio_encoder_combo):
            settings = self.project.acodecsettings

1152
        for propname, value in settings.items():
1153
            gst_element.set_property(propname, value)
1154
            self.debug("Setting %s to %s", propname, value)
1155

1156
    # Settings changed callbacks
1157
    def _scaleSpinbuttonChangedCb(self, unused_button):
1158
        render_scale = self.scale_spinbutton.get_value()
1159
        self.project.render_scale = render_scale
1160 1161 1162
        self.updateResolution()

    def updateResolution(self):
1163
        width, height = self.project.getVideoWidthAndHeight(render=True)
1164
        self.resolution_label.set_text("%%d" % (width, height))
1165

1166
    def _projectSettingsButtonClickedCb(self, unused_button):
1167
        from pitivi.project import ProjectSettingsDialog
1168
        dialog = ProjectSettingsDialog(self.window, self.project, self.app)
1169 1170
        dialog.window.run()

1171
    def _audioOutputCheckbuttonToggledCb(self, unused_audio):
1172
        active = self.audio_output_checkbutton.get_active()
1173 1174 1175 1176
        self.channels_combo.set_sensitive(active)
        self.sample_rate_combo.set_sensitive(active)
        self.audio_encoder_combo.set_sensitive(active)
        self.audio_settings_button.set_sensitive(active)
1177
        self.project.audio_profile.set_enabled(active)
1178
        self.__updateRenderButtonSensitivity()
1179

1180
    def _videoOutputCheckbuttonToggledCb(self, unused_video):
1181
        active = self.video_output_checkbutton.get_active()
1182 1183 1184 1185
        self.scale_spinbutton.set_sensitive(active)
        self.frame_rate_combo.set_sensitive(active)
        self.video_encoder_combo.set_sensitive(active)
        self.video_settings_button.set_sensitive(active)
1186
        self.project.video_profile.set_enabled(active)
1187 1188 1189 1190 1191 1192
        self.__updateRenderButtonSensitivity()

    def __updateRenderButtonSensitivity(self):
        video_enabled = self.video_output_checkbutton.get_active()
        audio_enabled = self.audio_output_checkbutton.get_active()
        self.render_button.set_sensitive(video_enabled or audio_enabled)
1193

1194
    def _frameRateComboChangedCb(self, combo):
1195 1196
        if self._setting_encoding_profile:
            return
1197
        framerate = get_combo_value(combo)
1198
        self.project.videorate = framerate
1199 1200

    def _videoEncoderComboChangedCb(self, combo):
1201 1202
        if self._setting_encoding_profile:
            return
1203 1204 1205
        factory = get_combo_value(combo)
        name = factory.get_name()
        self.project.vencoder = name
1206
        if not self.muxer_combo_changing:
1207
            # The user directly changed the video encoder combo.
1208 1209
            self.debug("User chose a video encoder: %s", name)
            self.preferred_vencoder = name
1210
        self._update_valid_video_restrictions(factory)
1211

1212
    def _videoSettingsButtonClickedCb(self, unused_button):
1213 1214
        if self._setting_encoding_profile:
            return
1215
        factory = get_combo_value(self.video_encoder_combo)