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