OpenShot Video Editor  2.0.0
export.py
Go to the documentation of this file.
1 ##
2 #
3 # @file
4 # @brief This file loads the Video Export dialog (i.e where is all preferences)
5 # @author Jonathan Thomas <jonathan@openshot.org>
6 #
7 # @section LICENSE
8 #
9 # Copyright (c) 2008-2018 OpenShot Studios, LLC
10 # (http://www.openshotstudios.com). This file is part of
11 # OpenShot Video Editor (http://www.openshot.org), an open-source project
12 # dedicated to delivering high quality video editing and animation solutions
13 # to the world.
14 #
15 # OpenShot Video Editor is free software: you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License as published by
17 # the Free Software Foundation, either version 3 of the License, or
18 # (at your option) any later version.
19 #
20 # OpenShot Video Editor is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
24 #
25 # You should have received a copy of the GNU General Public License
26 # along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
27 #
28 import time
29 import os
30 import locale
31 import xml.dom.minidom as xml
32 import functools
33 
34 from PyQt5.QtCore import *
35 from PyQt5.QtWidgets import *
36 import openshot # Python module for libopenshot (required video editing module installed separately)
37 
38 from classes import info, ui_util, settings
39 from classes.app import get_app
40 from classes.query import File
41 from classes.logger import log
42 from classes.metrics import *
43 
44 try:
45  import json
46 except ImportError:
47  import simplejson as json
48 
49 
50 ##
51 # Export Dialog
52 class Export(QDialog):
53 
54  # Path to ui file
55  ui_path = os.path.join(info.PATH, 'windows', 'ui', 'export.ui')
56 
57  def __init__(self):
58 
59  # Create dialog class
60  QDialog.__init__(self)
61 
62  # Load UI from designer
63  ui_util.load_ui(self, self.ui_path)
64 
65  # Init UI
66  ui_util.init_ui(self)
67 
68  # get translations
69  app = get_app()
70  _ = app._tr
71 
72  # Get settings
74 
75  # Track metrics
76  track_metric_screen("export-screen")
77 
78  # Dynamically load tabs from settings data
79  self.settings_data = settings.get_settings().get_all_settings()
80 
81  # Add buttons to interface
82  self.export_button = QPushButton(_('Export Video'))
83  self.buttonBox.addButton(self.export_button, QDialogButtonBox.AcceptRole)
84  self.buttonBox.addButton(QPushButton(_('Cancel')), QDialogButtonBox.RejectRole)
85  self.exporting = False
86 
87  # Update FPS / Profile timer
88  # Timer to use a delay before applying new profile/fps data (so we don't spam libopenshot)
89  self.delayed_fps_timer = None
90  self.delayed_fps_timer = QTimer()
91  self.delayed_fps_timer.setInterval(200)
92  self.delayed_fps_timer.timeout.connect(self.delayed_fps_callback)
93  self.delayed_fps_timer.stop()
94 
95  # Pause playback (to prevent crash since we are fixing to change the timeline's max size)
96  get_app().window.actionPlay_trigger(None, force="pause")
97 
98  # Clear timeline preview cache (to get more available memory)
99  get_app().window.timeline_sync.timeline.ClearAllCache()
100 
101  # Hide audio channels
102  self.lblChannels.setVisible(False)
103  self.txtChannels.setVisible(False)
104 
105  # Get the original timeline settings
106  width = get_app().window.timeline_sync.timeline.info.width
107  height = get_app().window.timeline_sync.timeline.info.height
108  fps = get_app().window.timeline_sync.timeline.info.fps
109  sample_rate = get_app().window.timeline_sync.timeline.info.sample_rate
110  channels = get_app().window.timeline_sync.timeline.info.channels
111  channel_layout = get_app().window.timeline_sync.timeline.info.channel_layout
112 
113  # Create new "export" openshot.Timeline object
114  self.timeline = openshot.Timeline(width, height, openshot.Fraction(fps.num, fps.den),
115  sample_rate, channels, channel_layout)
116  # Init various properties
117  self.timeline.info.channel_layout = get_app().window.timeline_sync.timeline.info.channel_layout
118  self.timeline.info.has_audio = get_app().window.timeline_sync.timeline.info.has_audio
119  self.timeline.info.has_video = get_app().window.timeline_sync.timeline.info.has_video
120  self.timeline.info.video_length = get_app().window.timeline_sync.timeline.info.video_length
121  self.timeline.info.duration = get_app().window.timeline_sync.timeline.info.duration
122  self.timeline.info.sample_rate = get_app().window.timeline_sync.timeline.info.sample_rate
123  self.timeline.info.channels = get_app().window.timeline_sync.timeline.info.channels
124 
125  # Load the "export" Timeline reader with the JSON from the real timeline
126  json_timeline = json.dumps(get_app().project._data)
127  self.timeline.SetJson(json_timeline)
128 
129  # Open the "export" Timeline reader
130  self.timeline.Open()
131 
132  # Default export path
133  recommended_path = recommended_path = os.path.join(info.HOME_PATH)
134  if app.project.current_filepath:
135  recommended_path = os.path.dirname(app.project.current_filepath)
136 
137  export_path = get_app().project.get(["export_path"])
138  if os.path.exists(export_path):
139  # Use last selected export path
140  self.txtExportFolder.setText(export_path)
141  else:
142  # Default to home dir
143  self.txtExportFolder.setText(recommended_path)
144 
145  # Is this a saved project?
146  if not get_app().project.current_filepath:
147  # Not saved yet
148  self.txtFileName.setText(_("Untitled Project"))
149  else:
150  # Yes, project is saved
151  # Get just the filename
152  parent_path, filename = os.path.split(get_app().project.current_filepath)
153  filename, ext = os.path.splitext(filename)
154  self.txtFileName.setText(filename.replace("_", " ").replace("-", " ").capitalize())
155 
156  # Default image type
157  self.txtImageFormat.setText("-%05d.png")
158 
159  # Loop through Export To options
160  export_options = [_("Video & Audio"), _("Video Only"), _("Audio Only"), _("Image Sequence")]
161  for option in export_options:
162  # append profile to list
163  self.cboExportTo.addItem(option)
164 
165  # Add channel layouts
167  for layout in [(openshot.LAYOUT_MONO, _("Mono (1 Channel)")),
168  (openshot.LAYOUT_STEREO, _("Stereo (2 Channel)")),
169  (openshot.LAYOUT_SURROUND, _("Surround (3 Channel)")),
170  (openshot.LAYOUT_5POINT1, _("Surround (5.1 Channel)")),
171  (openshot.LAYOUT_7POINT1, _("Surround (7.1 Channel)"))]:
172  log.info(layout)
173  self.channel_layout_choices.append(layout[0])
174  self.cboChannelLayout.addItem(layout[1], layout[0])
175 
176  # Connect signals
177  self.btnBrowse.clicked.connect(functools.partial(self.btnBrowse_clicked))
178  self.cboSimpleProjectType.currentIndexChanged.connect(
179  functools.partial(self.cboSimpleProjectType_index_changed, self.cboSimpleProjectType))
180  self.cboProfile.currentIndexChanged.connect(functools.partial(self.cboProfile_index_changed, self.cboProfile))
181  self.cboSimpleTarget.currentIndexChanged.connect(
182  functools.partial(self.cboSimpleTarget_index_changed, self.cboSimpleTarget))
183  self.cboSimpleVideoProfile.currentIndexChanged.connect(
184  functools.partial(self.cboSimpleVideoProfile_index_changed, self.cboSimpleVideoProfile))
185  self.cboSimpleQuality.currentIndexChanged.connect(
186  functools.partial(self.cboSimpleQuality_index_changed, self.cboSimpleQuality))
187  self.cboChannelLayout.currentIndexChanged.connect(self.updateChannels)
188  get_app().window.ExportFrame.connect(self.updateProgressBar)
189 
190  # ********* Advanced Profile List **********
191  # Loop through profiles
192  self.profile_names = []
193  self.profile_paths = {}
194  for profile_folder in [info.USER_PROFILES_PATH, info.PROFILES_PATH]:
195  for file in os.listdir(profile_folder):
196  # Load Profile
197  profile_path = os.path.join(profile_folder, file)
198  profile = openshot.Profile(profile_path)
199 
200  # Add description of Profile to list
201  profile_name = "%s (%sx%s)" % (profile.info.description, profile.info.width, profile.info.height)
202  self.profile_names.append(profile_name)
203  self.profile_paths[profile_name] = profile_path
204 
205  # Sort list
206  self.profile_names.sort()
207 
208  # Loop through sorted profiles
209  box_index = 0
211  for profile_name in self.profile_names:
212 
213  # Add to dropdown
214  self.cboProfile.addItem(self.getProfileName(self.getProfilePath(profile_name)), self.getProfilePath(profile_name))
215 
216  # Set default (if it matches the project)
217  if app.project.get(['profile']) in profile_name:
218  self.selected_profile_index = box_index
219 
220  # increment item counter
221  box_index += 1
222 
223 
224  # ********* Simple Project Type **********
225  # load the simple project type dropdown
226  presets = []
227  for file in os.listdir(info.EXPORT_PRESETS_DIR):
228  xmldoc = xml.parse(os.path.join(info.EXPORT_PRESETS_DIR, file))
229  type = xmldoc.getElementsByTagName("type")
230  presets.append(_(type[0].childNodes[0].data))
231 
232  # Exclude duplicates
233  type_index = 0
234  selected_type = 0
235  presets = list(set(presets))
236  for item in sorted(presets):
237  self.cboSimpleProjectType.addItem(item, item)
238  if item == _("All Formats"):
239  selected_type = type_index
240  type_index += 1
241 
242  # Always select 'All Formats' option
243  self.cboSimpleProjectType.setCurrentIndex(selected_type)
244 
245 
246  # Populate all profiles
247  self.populateAllProfiles(app.project.get(['profile']))
248 
249  # Connect framerate signals
250  self.txtFrameRateNum.valueChanged.connect(self.updateFrameRate)
251  self.txtFrameRateDen.valueChanged.connect(self.updateFrameRate)
252  self.txtWidth.valueChanged.connect(self.updateFrameRate)
253  self.txtHeight.valueChanged.connect(self.updateFrameRate)
254  self.txtSampleRate.valueChanged.connect(self.updateFrameRate)
255  self.txtChannels.valueChanged.connect(self.updateFrameRate)
256  self.cboChannelLayout.currentIndexChanged.connect(self.updateFrameRate)
257 
258  # Determine the length of the timeline (in frames)
259  self.updateFrameRate()
260 
261  ##
262  # Callback for fps/profile changed event timer (to delay the timeline mapping so we don't spam libopenshot)
264  # Stop timer
265  self.delayed_fps_timer.stop()
266 
267  # Calculate fps
268  fps_double = self.timeline.info.fps.ToDouble()
269 
270  # Apply mapping if valid fps detected (anything larger than 300 fps is considered invalid)
271  if self.timeline and fps_double <= 300.0:
272  log.info("Valid framerate detected, sending to libopenshot: %s" % fps_double)
273  self.timeline.ApplyMapperToClips()
274  else:
275  log.warning("Invalid framerate detected, not sending it to libopenshot: %s" % fps_double)
276 
277  ##
278  # Get the profile path that matches the name
279  def getProfilePath(self, profile_name):
280  for profile, path in self.profile_paths.items():
281  if profile_name in profile:
282  return path
283 
284  ##
285  # Get the profile name that matches the name
286  def getProfileName(self, profile_path):
287  for profile, path in self.profile_paths.items():
288  if profile_path == path:
289  return profile
290 
291  ##
292  # Update progress bar during exporting
293  def updateProgressBar(self, path, start_frame, end_frame, current_frame):
294  percentage_string = "%4.1f%% " % (( current_frame - start_frame ) / ( end_frame - start_frame ) * 100)
295  self.progressExportVideo.setValue(current_frame)
296  self.progressExportVideo.setFormat(percentage_string)
297  self.setWindowTitle("%s %s" % (percentage_string, path))
298 
299  ##
300  # Update the # of channels to match the channel layout
301  def updateChannels(self):
302  log.info("updateChannels")
303  channels = self.txtChannels.value()
304  channel_layout = self.cboChannelLayout.currentData()
305 
306  if channel_layout == openshot.LAYOUT_MONO:
307  channels = 1
308  elif channel_layout == openshot.LAYOUT_STEREO:
309  channels = 2
310  elif channel_layout == openshot.LAYOUT_SURROUND:
311  channels = 3
312  elif channel_layout == openshot.LAYOUT_5POINT1:
313  channels = 6
314  elif channel_layout == openshot.LAYOUT_7POINT1:
315  channels = 8
316 
317  # Update channels to match layout
318  self.txtChannels.setValue(channels)
319 
320  ##
321  # Callback for changing the frame rate
322  def updateFrameRate(self):
323  # Adjust the main timeline reader
324  self.timeline.info.width = self.txtWidth.value()
325  self.timeline.info.height = self.txtHeight.value()
326  self.timeline.info.fps.num = self.txtFrameRateNum.value()
327  self.timeline.info.fps.den = self.txtFrameRateDen.value()
328  self.timeline.info.sample_rate = self.txtSampleRate.value()
329  self.timeline.info.channels = self.txtChannels.value()
330  self.timeline.info.channel_layout = self.cboChannelLayout.currentData()
331 
332  # Send changes to libopenshot (apply mappings to all framemappers)... after a small delay
333  self.delayed_fps_timer.start()
334 
335  # Determine max frame (based on clips)
336  timeline_length = 0.0
337  fps = self.timeline.info.fps.ToFloat()
338  clips = self.timeline.Clips()
339  for clip in clips:
340  clip_last_frame = clip.Position() + clip.Duration()
341  if clip_last_frame > timeline_length:
342  # Set max length of timeline
343  timeline_length = clip_last_frame
344 
345  # Convert to int and round
346  self.timeline_length_int = round(timeline_length * fps) + 1
347 
348  # Set the min and max frame numbers for this project
349  self.txtStartFrame.setValue(1)
350  self.txtEndFrame.setValue(self.timeline_length_int)
351 
352  # Init progress bar
353  self.progressExportVideo.setMinimum(self.txtStartFrame.value())
354  self.progressExportVideo.setMaximum(self.txtEndFrame.value())
355  self.progressExportVideo.setValue(self.txtStartFrame.value())
356 
357  def cboSimpleProjectType_index_changed(self, widget, index):
358  selected_project = widget.itemData(index)
359 
360  # set the target dropdown based on the selected project type
361  # first clear the combo
362  self.cboSimpleTarget.clear()
363 
364  # get translations
365  app = get_app()
366  _ = app._tr
367 
368  # parse the xml files and get targets that match the project type
369  project_types = []
370  for file in os.listdir(info.EXPORT_PRESETS_DIR):
371  xmldoc = xml.parse(os.path.join(info.EXPORT_PRESETS_DIR, file))
372  type = xmldoc.getElementsByTagName("type")
373 
374  if _(type[0].childNodes[0].data) == selected_project:
375  titles = xmldoc.getElementsByTagName("title")
376  for title in titles:
377  project_types.append(_(title.childNodes[0].data))
378 
379  # Add all targets for selected project type
380  preset_index = 0
381  selected_preset = 0
382  for item in sorted(project_types):
383  self.cboSimpleTarget.addItem(item, item)
384 
385  # Find index of MP4/H.264
386  if item == _("MP4 (h.264)"):
387  selected_preset = preset_index
388 
389  preset_index += 1
390 
391  # Select MP4/H.264 as default
392  self.cboSimpleTarget.setCurrentIndex(selected_preset)
393 
394  def cboProfile_index_changed(self, widget, index):
395  selected_profile_path = widget.itemData(index)
396  log.info(selected_profile_path)
397 
398  # get translations
399  app = get_app()
400  _ = app._tr
401 
402  # Load profile
403  profile = openshot.Profile(selected_profile_path)
404 
405  # Load profile settings into advanced editor
406  self.txtWidth.setValue(profile.info.width)
407  self.txtHeight.setValue(profile.info.height)
408  self.txtFrameRateDen.setValue(profile.info.fps.den)
409  self.txtFrameRateNum.setValue(profile.info.fps.num)
410  self.txtAspectRatioNum.setValue(profile.info.display_ratio.num)
411  self.txtAspectRatioDen.setValue(profile.info.display_ratio.den)
412  self.txtPixelRatioNum.setValue(profile.info.pixel_ratio.num)
413  self.txtPixelRatioDen.setValue(profile.info.pixel_ratio.den)
414 
415  # Load the interlaced options
416  self.cboInterlaced.clear()
417  self.cboInterlaced.addItem(_("Yes"), "Yes")
418  self.cboInterlaced.addItem(_("No"), "No")
419  if profile.info.interlaced_frame:
420  self.cboInterlaced.setCurrentIndex(0)
421  else:
422  self.cboInterlaced.setCurrentIndex(1)
423 
424  def cboSimpleTarget_index_changed(self, widget, index):
425  selected_target = widget.itemData(index)
426  log.info(selected_target)
427 
428  # get translations
429  app = get_app()
430  _ = app._tr
431 
432  # don't do anything if the combo has been cleared
433  if selected_target:
434  profiles_list = []
435 
436  # Clear the following options (and remember current settings)
437  previous_quality = self.cboSimpleQuality.currentIndex()
438  if previous_quality < 0:
439  previous_quality = self.cboSimpleQuality.count() - 1
440  previous_profile = self.cboSimpleVideoProfile.currentIndex()
441  if previous_profile < 0:
442  previous_profile = self.selected_profile_index
443  self.cboSimpleVideoProfile.clear()
444  self.cboSimpleQuality.clear()
445 
446  # parse the xml to return suggested profiles
447  profile_index = 0
448  all_profiles = False
449  for file in os.listdir(info.EXPORT_PRESETS_DIR):
450  xmldoc = xml.parse(os.path.join(info.EXPORT_PRESETS_DIR, file))
451  title = xmldoc.getElementsByTagName("title")
452  if _(title[0].childNodes[0].data) == selected_target:
453  profiles = xmldoc.getElementsByTagName("projectprofile")
454 
455  # get the basic profile
456  all_profiles = False
457  if profiles:
458  # if profiles are defined, show them
459  for profile in profiles:
460  profiles_list.append(_(profile.childNodes[0].data))
461  else:
462  # show all profiles
463  all_profiles = True
464  for profile_name in self.profile_names:
465  profiles_list.append(profile_name)
466 
467  # get the video bit rate(s)
468  videobitrate = xmldoc.getElementsByTagName("videobitrate")
469  for rate in videobitrate:
470  v_l = rate.attributes["low"].value
471  v_m = rate.attributes["med"].value
472  v_h = rate.attributes["high"].value
473  self.vbr = {_("Low"): v_l, _("Med"): v_m, _("High"): v_h}
474 
475  # get the audio bit rates
476  audiobitrate = xmldoc.getElementsByTagName("audiobitrate")
477  for audiorate in audiobitrate:
478  a_l = audiorate.attributes["low"].value
479  a_m = audiorate.attributes["med"].value
480  a_h = audiorate.attributes["high"].value
481  self.abr = {_("Low"): a_l, _("Med"): a_m, _("High"): a_h}
482 
483  # get the remaining values
484  vf = xmldoc.getElementsByTagName("videoformat")
485  self.txtVideoFormat.setText(vf[0].childNodes[0].data)
486  vc = xmldoc.getElementsByTagName("videocodec")
487  self.txtVideoCodec.setText(vc[0].childNodes[0].data)
488  sr = xmldoc.getElementsByTagName("samplerate")
489  self.txtSampleRate.setValue(int(sr[0].childNodes[0].data))
490  c = xmldoc.getElementsByTagName("audiochannels")
491  self.txtChannels.setValue(int(c[0].childNodes[0].data))
492  c = xmldoc.getElementsByTagName("audiochannellayout")
493 
494  # check for compatible audio codec
495  ac = xmldoc.getElementsByTagName("audiocodec")
496  audio_codec_name = ac[0].childNodes[0].data
497  if audio_codec_name == "aac":
498  # Determine which version of AAC encoder is available
499  if openshot.FFmpegWriter.IsValidCodec("libfaac"):
500  self.txtAudioCodec.setText("libfaac")
501  elif openshot.FFmpegWriter.IsValidCodec("libvo_aacenc"):
502  self.txtAudioCodec.setText("libvo_aacenc")
503  elif openshot.FFmpegWriter.IsValidCodec("aac"):
504  self.txtAudioCodec.setText("aac")
505  else:
506  # fallback audio codec
507  self.txtAudioCodec.setText("ac3")
508  else:
509  # fallback audio codec
510  self.txtAudioCodec.setText(audio_codec_name)
511 
512  layout_index = 0
513  for layout in self.channel_layout_choices:
514  if layout == int(c[0].childNodes[0].data):
515  self.cboChannelLayout.setCurrentIndex(layout_index)
516  break
517  layout_index += 1
518 
519  # init the profiles combo
520  for item in sorted(profiles_list):
521  self.cboSimpleVideoProfile.addItem(self.getProfileName(self.getProfilePath(item)), self.getProfilePath(item))
522 
523  if all_profiles:
524  # select the project's current profile
525  self.cboSimpleVideoProfile.setCurrentIndex(previous_profile)
526 
527  # set the quality combo
528  # only populate with quality settings that exist
529  if v_l or a_l:
530  self.cboSimpleQuality.addItem(_("Low"), "Low")
531  if v_m or a_m:
532  self.cboSimpleQuality.addItem(_("Med"), "Med")
533  if v_h or a_h:
534  self.cboSimpleQuality.addItem(_("High"), "High")
535 
536  # Default to the highest quality setting
537  self.cboSimpleQuality.setCurrentIndex(previous_quality)
538 
539  def cboSimpleVideoProfile_index_changed(self, widget, index):
540  selected_profile_path = widget.itemData(index)
541  log.info(selected_profile_path)
542 
543  # Populate the advanced profile list
544  self.populateAllProfiles(selected_profile_path)
545 
546  ##
547  # Populate the full list of profiles
548  def populateAllProfiles(self, selected_profile_path):
549  # Look for matching profile in advanced options
550  profile_index = 0
551  for profile_name in self.profile_names:
552  # Check for matching profile
553  if self.getProfilePath(profile_name) == selected_profile_path:
554  # Matched!
555  self.cboProfile.setCurrentIndex(profile_index)
556  break
557 
558  # increment index
559  profile_index += 1
560 
561  def cboSimpleQuality_index_changed(self, widget, index):
562  selected_quality = widget.itemData(index)
563  log.info(selected_quality)
564 
565  # get translations
566  app = get_app()
567  _ = app._tr
568 
569  # Set the video and audio bitrates
570  if selected_quality:
571  self.txtVideoBitRate.setText(_(self.vbr[_(selected_quality)]))
572  self.txtAudioBitrate.setText(_(self.abr[_(selected_quality)]))
573 
574  def btnBrowse_clicked(self):
575  log.info("btnBrowse_clicked")
576 
577  # get translations
578  app = get_app()
579  _ = app._tr
580 
581  # update export folder path
582  file_path = QFileDialog.getExistingDirectory(self, _("Choose a Folder..."), self.txtExportFolder.text())
583  if os.path.exists(file_path):
584  self.txtExportFolder.setText(file_path)
585 
586  # update export folder path in project file
587  get_app().updates.update(["export_path"], file_path)
588 
589  def convert_to_bytes(self, BitRateString):
590  bit_rate_bytes = 0
591 
592  # split the string into pieces
593  s = BitRateString.lower().split(" ")
594  measurement = "kb"
595 
596  try:
597  # Get Bit Rate
598  if len(s) >= 2:
599  raw_number_string = s[0]
600  raw_measurement = s[1]
601 
602  # convert string number to float (based on locale settings)
603  raw_number = locale.atof(raw_number_string)
604 
605  if "kb" in raw_measurement:
606  measurement = "kb"
607  bit_rate_bytes = raw_number * 1000.0
608 
609  elif "mb" in raw_measurement:
610  measurement = "mb"
611  bit_rate_bytes = raw_number * 1000.0 * 1000.0
612 
613  except:
614  pass
615 
616  # return the bit rate in bytes
617  return str(int(bit_rate_bytes))
618 
619  ##
620  # Start exporting video
621  def accept(self):
622 
623  # get translations
624  app = get_app()
625  _ = app._tr
626 
627  # Disable controls
628  self.txtFileName.setEnabled(False)
629  self.txtExportFolder.setEnabled(False)
630  self.tabWidget.setEnabled(False)
631  self.export_button.setEnabled(False)
632  self.exporting = True
633 
634  # Determine type of export (video+audio, video, audio, image sequences)
635  # _("Video & Audio"), _("Video Only"), _("Audio Only"), _("Image Sequence")
636  export_type = self.cboExportTo.currentText()
637 
638  # Determine final exported file path
639  if export_type != _("Image Sequence"):
640  file_name_with_ext = "%s.%s" % (self.txtFileName.text().strip(), self.txtVideoFormat.text().strip())
641  else:
642  file_name_with_ext = "%s%s" % (self.txtFileName.text().strip(), self.txtImageFormat.text().strip())
643  export_file_path = os.path.join(self.txtExportFolder.text().strip(), file_name_with_ext)
644  log.info(export_file_path)
645 
646  # Translate object
647  _ = get_app()._tr
648 
649  file = File.get(path=export_file_path)
650  if file:
651  ret = QMessageBox.question(self, _("Export Video"), _("%s is an input file.\nPlease choose a different name.") % file_name_with_ext,
652  QMessageBox.Ok)
653  self.txtFileName.setEnabled(True)
654  self.txtExportFolder.setEnabled(True)
655  self.tabWidget.setEnabled(True)
656  self.export_button.setEnabled(True)
657  self.exporting = False
658  return
659 
660  # Handle exception
661  if os.path.exists(export_file_path) and export_type in [_("Video & Audio"), _("Video Only"), _("Audio Only")]:
662  # File already exists! Prompt user
663  ret = QMessageBox.question(self, _("Export Video"), _("%s already exists.\nDo you want to replace it?") % file_name_with_ext,
664  QMessageBox.No | QMessageBox.Yes)
665  if ret == QMessageBox.No:
666  # Stop and don't do anything
667  # Re-enable controls
668  self.txtFileName.setEnabled(True)
669  self.txtExportFolder.setEnabled(True)
670  self.tabWidget.setEnabled(True)
671  self.export_button.setEnabled(True)
672  self.exporting = False
673  return
674 
675  # Init export settings
676  video_settings = { "vformat": self.txtVideoFormat.text(),
677  "vcodec": self.txtVideoCodec.text(),
678  "fps": { "num" : self.txtFrameRateNum.value(), "den": self.txtFrameRateDen.value()},
679  "width": self.txtWidth.value(),
680  "height": self.txtHeight.value(),
681  "pixel_ratio": {"num": self.txtPixelRatioNum.value(), "den": self.txtPixelRatioDen.value()},
682  "video_bitrate": int(self.convert_to_bytes(self.txtVideoBitRate.text())),
683  "start_frame": self.txtStartFrame.value(),
684  "end_frame": self.txtEndFrame.value() + 1
685  }
686 
687  audio_settings = {"acodec": self.txtAudioCodec.text(),
688  "sample_rate": self.txtSampleRate.value(),
689  "channels": self.txtChannels.value(),
690  "channel_layout": self.cboChannelLayout.currentData(),
691  "audio_bitrate": int(self.convert_to_bytes(self.txtAudioBitrate.text()))
692  }
693 
694  # Override vcodec and format for Image Sequences
695  if export_type == _("Image Sequence"):
696  image_ext = os.path.splitext(self.txtImageFormat.text().strip())[1].replace(".", "")
697  video_settings["vformat"] = image_ext
698  if image_ext in ["jpg", "jpeg"]:
699  video_settings["vcodec"] = "mjpeg"
700  else:
701  video_settings["vcodec"] = image_ext
702 
703  # Set MaxSize (so we don't have any downsampling)
704  self.timeline.SetMaxSize(video_settings.get("width"), video_settings.get("height"))
705 
706  # Set lossless cache settings (temporarily)
707  export_cache_object = openshot.CacheMemory(250)
708  self.timeline.SetCache(export_cache_object)
709 
710  # Create FFmpegWriter
711  try:
712  w = openshot.FFmpegWriter(export_file_path)
713 
714  # Set video options
715  if export_type in [_("Video & Audio"), _("Video Only"), _("Image Sequence")]:
716  w.SetVideoOptions(True,
717  video_settings.get("vcodec"),
718  openshot.Fraction(video_settings.get("fps").get("num"),
719  video_settings.get("fps").get("den")),
720  video_settings.get("width"),
721  video_settings.get("height"),
722  openshot.Fraction(video_settings.get("pixel_ratio").get("num"),
723  video_settings.get("pixel_ratio").get("den")),
724  False,
725  False,
726  video_settings.get("video_bitrate"))
727 
728  # Set audio options
729  if export_type in [_("Video & Audio"), _("Audio Only")]:
730  w.SetAudioOptions(True,
731  audio_settings.get("acodec"),
732  audio_settings.get("sample_rate"),
733  audio_settings.get("channels"),
734  audio_settings.get("channel_layout"),
735  audio_settings.get("audio_bitrate"))
736 
737  # Open the writer
738  w.Open()
739 
740  # Notify window of export started
741  export_file_path = ""
742  get_app().window.ExportStarted.emit(export_file_path, video_settings.get("start_frame"), video_settings.get("end_frame"))
743 
744  progressstep = max(1 , round(( video_settings.get("end_frame") - video_settings.get("start_frame") ) / 1000))
745  start_time_export = time.time()
746  start_frame_export = video_settings.get("start_frame")
747  end_frame_export = video_settings.get("end_frame")
748  # Write each frame in the selected range
749  for frame in range(video_settings.get("start_frame"), video_settings.get("end_frame")):
750  # Update progress bar (emit signal to main window)
751  if (frame % progressstep) == 0:
752  end_time_export = time.time()
753  if ((( frame - start_frame_export ) != 0) & (( end_time_export - start_time_export ) != 0)):
754  seconds_left = round(( start_time_export - end_time_export )*( frame - end_frame_export )/( frame - start_frame_export ))
755  fps_encode = ((frame - start_frame_export)/(end_time_export-start_time_export))
756  export_file_path = _("%(hours)d:%(minutes)02d:%(seconds)02d Remaining (%(fps)5.2f FPS)") % { 'hours' : seconds_left / 3600,
757  'minutes': (seconds_left / 60) % 60,
758  'seconds': seconds_left % 60,
759  'fps': fps_encode }
760  get_app().window.ExportFrame.emit(export_file_path, video_settings.get("start_frame"), video_settings.get("end_frame"), frame)
761 
762  # Process events (to show the progress bar moving)
763  QCoreApplication.processEvents()
764 
765  # Write the frame object to the video
766  w.WriteFrame(self.timeline.GetFrame(frame))
767 
768  # Check if we need to bail out
769  if not self.exporting:
770  break
771 
772  # Close writer
773  w.Close()
774 
775 
776  except Exception as e:
777  # TODO: Find a better way to catch the error. This is the only way I have found that
778  # does not throw an error
779  error_type_str = str(e)
780  log.info("Error type string: %s" % error_type_str)
781 
782  if "InvalidChannels" in error_type_str:
783  log.info("Error setting invalid # of channels (%s)" % (audio_settings.get("channels")))
784  track_metric_error("invalid-channels-%s-%s-%s-%s" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec"), audio_settings.get("channels")))
785 
786  elif "InvalidSampleRate" in error_type_str:
787  log.info("Error setting invalid sample rate (%s)" % (audio_settings.get("sample_rate")))
788  track_metric_error("invalid-sample-rate-%s-%s-%s-%s" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec"), audio_settings.get("sample_rate")))
789 
790  elif "InvalidFormat" in error_type_str:
791  log.info("Error setting invalid format (%s)" % (video_settings.get("vformat")))
792  track_metric_error("invalid-format-%s" % (video_settings.get("vformat")))
793 
794  elif "InvalidCodec" in error_type_str:
795  log.info("Error setting invalid codec (%s/%s/%s)" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec")))
796  track_metric_error("invalid-codec-%s-%s-%s" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec")))
797 
798  elif "ErrorEncodingVideo" in error_type_str:
799  log.info("Error encoding video frame (%s/%s/%s)" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec")))
800  track_metric_error("video-encode-%s-%s-%s" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec")))
801 
802  # Show friendly error
803  friendly_error = error_type_str.split("> ")[0].replace("<", "")
804 
805  # Prompt error message
806  msg = QMessageBox()
807  _ = get_app()._tr
808  msg.setWindowTitle(_("Export Error"))
809  msg.setText(_("Sorry, there was an error exporting your video: \n%s") % friendly_error)
810  msg.exec_()
811 
812  # Notify window of export started
813  get_app().window.ExportEnded.emit(export_file_path)
814 
815  # Close timeline object
816  self.timeline.Close()
817 
818  # Clear all cache
819  self.timeline.ClearAllCache()
820 
821  # Accept dialog
822  super(Export, self).accept()
823 
824  def reject(self):
825  # Cancel dialog
826  self.exporting = False
827  super(Export, self).reject()
def cboSimpleQuality_index_changed(self, widget, index)
Definition: export.py:561
def getProfileName(self, profile_path)
Get the profile name that matches the name.
Definition: export.py:286
def reject(self)
Definition: export.py:824
def __init__(self)
Definition: export.py:57
def updateChannels(self)
Update the # of channels to match the channel layout.
Definition: export.py:301
Export Dialog.
Definition: export.py:52
def get_app()
Returns the current QApplication instance of OpenShot.
Definition: app.py:55
def delayed_fps_callback(self)
Callback for fps/profile changed event timer (to delay the timeline mapping so we don&#39;t spam libopens...
Definition: export.py:263
def cboSimpleProjectType_index_changed(self, widget, index)
Definition: export.py:357
def btnBrowse_clicked(self)
Definition: export.py:574
channel_layout_choices
Definition: export.py:166
def updateFrameRate(self)
Callback for changing the frame rate.
Definition: export.py:322
def cboProfile_index_changed(self, widget, index)
Definition: export.py:394
def cboSimpleTarget_index_changed(self, widget, index)
Definition: export.py:424
def populateAllProfiles(self, selected_profile_path)
Populate the full list of profiles.
Definition: export.py:548
selected_profile_index
Definition: export.py:210
timeline_length_int
Definition: export.py:346
def get_settings()
Get the current QApplication&#39;s settings instance.
Definition: settings.py:44
delayed_fps_timer
Definition: export.py:89
def init_ui(window)
Initialize all child widgets and action of a window or dialog.
Definition: ui_util.py:220
def updateProgressBar(self, path, start_frame, end_frame, current_frame)
Update progress bar during exporting.
Definition: export.py:293
def track_metric_screen(screen_name)
Track a GUI screen being shown.
Definition: metrics.py:96
def track_metric_error(error_name, is_fatal=False)
Track an error has occurred.
Definition: metrics.py:121
def cboSimpleVideoProfile_index_changed(self, widget, index)
Definition: export.py:539
def load_ui(window, path)
Load a Qt *.ui file, and also load an XML parsed version.
Definition: ui_util.py:66
def getProfilePath(self, profile_name)
Get the profile path that matches the name.
Definition: export.py:279
def convert_to_bytes(self, BitRateString)
Definition: export.py:589
def accept(self)
Start exporting video.
Definition: export.py:621