OpenShot Video Editor  2.0.0
cutting.py
Go to the documentation of this file.
1 ##
2 #
3 # @file
4 # @brief This file loads the clip cutting interface (quickly cut up a clip into smaller clips)
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 
29 import os
30 import sys
31 import functools
32 import math
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, qt_types, updates
39 from classes.app import get_app
40 from classes.logger import log
41 from classes.metrics import *
42 from windows.preview_thread import PreviewParent
43 from windows.video_widget import VideoWidget
44 
45 try:
46  import json
47 except ImportError:
48  import simplejson as json
49 
50 
51 ##
52 # Cutting Dialog
53 class Cutting(QDialog):
54 
55  # Path to ui file
56  ui_path = os.path.join(info.PATH, 'windows', 'ui', 'cutting.ui')
57 
58  # Signals for preview thread
59  previewFrameSignal = pyqtSignal(int)
60  refreshFrameSignal = pyqtSignal()
61  LoadFileSignal = pyqtSignal(str)
62  PlaySignal = pyqtSignal(int)
63  PauseSignal = pyqtSignal()
64  SeekSignal = pyqtSignal(int)
65  SpeedSignal = pyqtSignal(float)
66  StopSignal = pyqtSignal()
67 
68  def __init__(self, file=None, preview=False):
69  _ = get_app()._tr
70 
71  # Create dialog class
72  QDialog.__init__(self)
73 
74  # Load UI from designer
75  ui_util.load_ui(self, self.ui_path)
76 
77  # Init UI
78  ui_util.init_ui(self)
79 
80  # Track metrics
81  track_metric_screen("cutting-screen")
82 
83  # If preview, hide cutting controls
84  if preview:
85  self.lblInstructions.setVisible(False)
86  self.widgetControls.setVisible(False)
87  self.setWindowTitle(_("Preview"))
88 
89  self.start_frame = 1
90  self.start_image = None
91  self.end_frame = 1
92  self.end_image = None
93 
94  # Keep track of file object
95  self.file = file
96  self.file_path = file.absolute_path()
97  self.video_length = int(file.data['video_length'])
98  self.fps_num = int(file.data['fps']['num'])
99  self.fps_den = int(file.data['fps']['den'])
100  self.fps = float(self.fps_num) / float(self.fps_den)
101  self.width = int(file.data['width'])
102  self.height = int(file.data['height'])
103  self.sample_rate = int(file.data['sample_rate'])
104  self.channels = int(file.data['channels'])
105  self.channel_layout = int(file.data['channel_layout'])
106 
107  # Open video file with Reader
108  log.info(self.file_path)
109 
110  # Create an instance of a libopenshot Timeline object
111  self.r = openshot.Timeline(self.width, self.height, openshot.Fraction(self.fps_num, self.fps_den), self.sample_rate, self.channels, self.channel_layout)
112  self.r.info.channel_layout = self.channel_layout
113 
114  try:
115  # Add clip for current preview file
116  self.clip = openshot.Clip(self.file_path)
117 
118  # Show waveform for audio files
119  if not self.clip.Reader().info.has_video and self.clip.Reader().info.has_audio:
120  self.clip.Waveform(True)
121 
122  # Set has_audio property
123  self.r.info.has_audio = self.clip.Reader().info.has_audio
124 
125  if preview:
126  # Display frame #'s during preview
127  self.clip.display = openshot.FRAME_DISPLAY_CLIP
128 
129  self.r.AddClip(self.clip)
130  except:
131  log.error('Failed to load media file into preview player: %s' % self.file_path)
132  return
133 
134  # Add Video Widget
135  self.videoPreview = VideoWidget()
136  self.videoPreview.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
137  self.verticalLayout.insertWidget(0, self.videoPreview)
138 
139  # Set max size of video preview (for speed)
140  viewport_rect = self.videoPreview.centeredViewport(self.videoPreview.width(), self.videoPreview.height())
141  self.r.SetMaxSize(viewport_rect.width(), viewport_rect.height())
142 
143  # Open reader
144  self.r.Open()
145 
146  # Start the preview thread
147  self.initialized = False
148  self.transforming_clip = False
149  self.preview_parent = PreviewParent()
150  self.preview_parent.Init(self, self.r, self.videoPreview)
151  self.preview_thread = self.preview_parent.worker
152 
153  # Set slider constraints
154  self.sliderIgnoreSignal = False
155  self.sliderVideo.setMinimum(1)
156  self.sliderVideo.setMaximum(self.video_length)
157  self.sliderVideo.setSingleStep(1)
158  self.sliderVideo.setSingleStep(1)
159  self.sliderVideo.setPageStep(24)
160 
161  # Determine if a start or end attribute is in this file
162  start_frame = 1
163  if 'start' in self.file.data.keys():
164  start_frame = (float(self.file.data['start']) * self.fps) + 1
165 
166  # Display start frame (and then the previous frame)
167  QTimer.singleShot(500, functools.partial(self.sliderVideo.setValue, start_frame + 1))
168  QTimer.singleShot(600, functools.partial(self.sliderVideo.setValue, start_frame))
169 
170  # Connect signals
171  self.actionPlay.triggered.connect(self.actionPlay_Triggered)
172  self.btnPlay.clicked.connect(self.btnPlay_clicked)
173  self.sliderVideo.valueChanged.connect(self.sliderVideo_valueChanged)
174  self.btnStart.clicked.connect(self.btnStart_clicked)
175  self.btnEnd.clicked.connect(self.btnEnd_clicked)
176  self.btnClear.clicked.connect(self.btnClear_clicked)
177  self.btnAddClip.clicked.connect(self.btnAddClip_clicked)
178  self.initialized = True
179 
181  # Trigger play button (This action is invoked from the preview thread, so it must exist here)
182  self.btnPlay.click()
183 
184  ##
185  # Update the playhead position
186  def movePlayhead(self, frame_number):
187 
188  # Move slider to correct frame position
189  self.sliderIgnoreSignal = True
190  self.sliderVideo.setValue(frame_number)
191  self.sliderIgnoreSignal = False
192 
193  # Convert frame to seconds
194  seconds = (frame_number-1) / self.fps
195 
196  # Convert seconds to time stamp
197  time_text = self.secondsToTime(seconds, self.fps_num, self.fps_den)
198  timestamp = "%s:%s:%s:%s" % (time_text["hour"], time_text["min"], time_text["sec"], time_text["frame"])
199 
200  # Update label
201  self.lblVideoTime.setText(timestamp)
202 
203  def btnPlay_clicked(self, force=None):
204  log.info("btnPlay_clicked")
205 
206  if force == "pause":
207  self.btnPlay.setChecked(False)
208  elif force == "play":
209  self.btnPlay.setChecked(True)
210 
211  if self.btnPlay.isChecked():
212  log.info('play (icon to pause)')
213  ui_util.setup_icon(self, self.btnPlay, "actionPlay", "media-playback-pause")
214  self.preview_thread.Play(self.video_length)
215  else:
216  log.info('pause (icon to play)')
217  ui_util.setup_icon(self, self.btnPlay, "actionPlay", "media-playback-start") # to default
218  self.preview_thread.Pause()
219 
220  # Send focus back to toolbar
221  self.sliderVideo.setFocus()
222 
223  def sliderVideo_valueChanged(self, new_frame):
224  if self.preview_thread and not self.sliderIgnoreSignal:
225  log.info('sliderVideo_valueChanged')
226 
227  # Pause video
228  self.btnPlay_clicked(force="pause")
229 
230  # Seek to new frame
231  self.preview_thread.previewFrame(new_frame)
232 
233  ##
234  # Start of clip button was clicked
235  def btnStart_clicked(self):
236  _ = get_app()._tr
237 
238  # Pause video
239  self.btnPlay_clicked(force="pause")
240 
241  # Get the current frame
242  current_frame = self.sliderVideo.value()
243 
244  # Check if starting frame less than end frame
245  if self.btnEnd.isEnabled() and current_frame >= self.end_frame:
246  # Handle exception
247  msg = QMessageBox()
248  msg.setText(_("Please choose valid 'start' and 'end' values for your clip."))
249  msg.exec_()
250  return
251 
252  # remember frame #
253  self.start_frame = current_frame
254 
255  # Save thumbnail image
256  self.start_image = os.path.join(info.USER_PATH, 'thumbnail', '%s.png' % self.start_frame)
257  self.r.GetFrame(self.start_frame).Thumbnail(self.start_image, 160, 90, '', '', '#000000', True)
258 
259  # Set CSS on button
260  self.btnStart.setStyleSheet('background-image: url(%s);' % self.start_image.replace('\\', '/'))
261 
262  # Enable end button
263  self.btnEnd.setEnabled(True)
264  self.btnClear.setEnabled(True)
265 
266  # Send focus back to toolbar
267  self.sliderVideo.setFocus()
268 
269  log.info('btnStart_clicked, current frame: %s' % self.start_frame)
270 
271  ##
272  # End of clip button was clicked
273  def btnEnd_clicked(self):
274  _ = get_app()._tr
275 
276  # Pause video
277  self.btnPlay_clicked(force="pause")
278 
279  # Get the current frame
280  current_frame = self.sliderVideo.value()
281 
282  # Check if ending frame greater than start frame
283  if current_frame <= self.start_frame:
284  # Handle exception
285  msg = QMessageBox()
286  msg.setText(_("Please choose valid 'start' and 'end' values for your clip."))
287  msg.exec_()
288  return
289 
290  # remember frame #
291  self.end_frame = current_frame
292 
293  # Save thumbnail image
294  self.end_image = os.path.join(info.USER_PATH, 'thumbnail', '%s.png' % self.end_frame)
295  self.r.GetFrame(self.end_frame).Thumbnail(self.end_image, 160, 90, '', '', '#000000', True)
296 
297  # Set CSS on button
298  self.btnEnd.setStyleSheet('background-image: url(%s);' % self.end_image.replace('\\', '/'))
299 
300  # Enable create button
301  self.btnAddClip.setEnabled(True)
302 
303  # Send focus back to toolbar
304  self.sliderVideo.setFocus()
305 
306  log.info('btnEnd_clicked, current frame: %s' % self.end_frame)
307 
308  ##
309  # Clear the current clip and reset the form
310  def btnClear_clicked(self):
311  log.info('btnClear_clicked')
312 
313  # Reset form
314  self.clearForm()
315 
316  ##
317  # Clear all form controls
318  def clearForm(self):
319  # Clear buttons
320  self.start_frame = 1
321  self.end_frame = 1
322  self.start_image = ''
323  self.end_image = ''
324  self.btnStart.setStyleSheet('background-image: None;')
325  self.btnEnd.setStyleSheet('background-image: None;')
326 
327  # Clear text
328  self.txtName.setText('')
329 
330  # Disable buttons
331  self.btnEnd.setEnabled(False)
332  self.btnAddClip.setEnabled(False)
333  self.btnClear.setEnabled(False)
334 
335  ##
336  # Add the selected clip to the project
338  log.info('btnAddClip_clicked')
339 
340  # Remove unneeded attributes
341  if 'name' in self.file.data.keys():
342  self.file.data.pop('name')
343 
344  # Save new file
345  self.file.id = None
346  self.file.key = None
347  self.file.type = 'insert'
348  self.file.data['start'] = (self.start_frame-1) / self.fps
349  self.file.data['end'] = (self.end_frame-1) / self.fps
350  if self.txtName.text():
351  self.file.data['name'] = self.txtName.text()
352  self.file.save()
353 
354  # Reset form
355  self.clearForm()
356 
357  def padNumber(self, value, pad_length):
358  format_mask = '%%0%sd' % pad_length
359  return format_mask % value
360 
361  def secondsToTime(self, secs, fps_num, fps_den):
362  # calculate time of playhead
363  milliseconds = secs * 1000
364  sec = math.floor(milliseconds/1000)
365  milli = milliseconds % 1000
366  min = math.floor(sec/60)
367  sec = sec % 60
368  hour = math.floor(min/60)
369  min = min % 60
370  day = math.floor(hour/24)
371  hour = hour % 24
372  week = math.floor(day/7)
373  day = day % 7
374 
375  frame = round((milli / 1000.0) * (fps_num / fps_den)) + 1
376  return { "week":self.padNumber(week,2), "day":self.padNumber(day,2), "hour":self.padNumber(hour,2), "min":self.padNumber(min,2), "sec":self.padNumber(sec,2), "milli":self.padNumber(milli,2), "frame":self.padNumber(frame,2) };
377 
378  # TODO: Remove these 4 methods
379  ##
380  # Ok button clicked
381  def accept(self):
382  log.info('accept')
383 
384  ##
385  # Actually close window and accept dialog
386  def close(self):
387  log.info('close')
388 
389  def closeEvent(self, event):
390  log.info('closeEvent')
391 
392  # Stop playback
393  self.preview_parent.worker.Stop()
394 
395  # Stop preview thread (and wait for it to end)
396  self.preview_parent.worker.kill()
397  self.preview_parent.background.exit()
398  self.preview_parent.background.wait(5000)
399 
400  # Close readers
401  self.r.Close()
402  self.clip.Close()
403  self.r.ClearAllCache()
404 
405  def reject(self):
406  log.info('reject')
407 
408 
409 
def movePlayhead
Update the playhead position.
Definition: cutting.py:186
tuple ui_path
Definition: cutting.py:56
Cutting Dialog.
Definition: cutting.py:53
def track_metric_screen
Track a GUI screen being shown.
Definition: metrics.py:96
def get_app
Returns the current QApplication instance of OpenShot.
Definition: app.py:55
def setup_icon
Using the window xml, set the icon on the given element, or if theme_name passed load that icon...
Definition: ui_util.py:146
def secondsToTime
Definition: cutting.py:361
def btnEnd_clicked
End of clip button was clicked.
Definition: cutting.py:273
def close
Actually close window and accept dialog.
Definition: cutting.py:386
def load_ui
Load a Qt *.ui file, and also load an XML parsed version.
Definition: ui_util.py:66
def sliderVideo_valueChanged
Definition: cutting.py:223
def accept
Ok button clicked.
Definition: cutting.py:381
def btnPlay_clicked
Definition: cutting.py:203
def clearForm
Clear all form controls.
Definition: cutting.py:318
def init_ui
Initialize all child widgets and action of a window or dialog.
Definition: ui_util.py:215
def btnClear_clicked
Clear the current clip and reset the form.
Definition: cutting.py:310
def btnStart_clicked
Start of clip button was clicked.
Definition: cutting.py:235
def btnAddClip_clicked
Add the selected clip to the project.
Definition: cutting.py:337
def actionPlay_Triggered
Definition: cutting.py:180