OpenShot Video Editor  2.0.0
preview_thread.py
Go to the documentation of this file.
1 ##
2 #
3 # @file
4 # @brief This file contains the preview thread, used for displaying previews of the timeline
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 time
31 import sip
32 
33 from PyQt5.QtCore import QObject, QThread, pyqtSlot, pyqtSignal, QCoreApplication
34 import openshot # Python module for libopenshot (required video editing module installed separately)
35 
36 from classes.app import get_app
37 from classes.logger import log
38 from classes import settings
39 
40 try:
41  import json
42 except ImportError:
43  import simplejson as json
44 
45 
46 ##
47 # Class which communicates with the PlayerWorker Class (running on a separate thread)
48 class PreviewParent(QObject):
49 
50  # Signal when the frame position changes in the preview player
51  def onPositionChanged(self, current_frame):
52  self.parent.movePlayhead(current_frame)
53 
54  # Check if we are at the end of the timeline
55  if self.worker.player.Mode() == openshot.PLAYBACK_PLAY and current_frame >= self.worker.timeline_length and self.worker.timeline_length != -1:
56  # Yes, pause the video
57  self.parent.actionPlay.trigger()
58  self.worker.timeline_length = -1
59 
60  # Signal when the playback mode changes in the preview player (i.e PLAY, PAUSE, STOP)
61  def onModeChanged(self, current_mode):
62  log.info('onModeChanged')
63 
64  @pyqtSlot(object, object)
65  def Init(self, parent, timeline, video_widget):
66  # Important vars
67  self.parent = parent
68  self.timeline = timeline
69 
70  # Background Worker Thread (for preview video process)
71  self.background = QThread(self)
72  self.worker = PlayerWorker() # no parent!
73 
74  # Init worker variables
75  self.worker.Init(parent, timeline, video_widget)
76 
77  # Hook up signals to Background Worker
78  self.worker.position_changed.connect(self.onPositionChanged)
79  self.worker.mode_changed.connect(self.onModeChanged)
80  self.background.started.connect(self.worker.Start)
81  self.worker.finished.connect(self.background.quit)
82 
83  # Connect preview thread to main UI signals
84  self.parent.previewFrameSignal.connect(self.worker.previewFrame)
85  self.parent.refreshFrameSignal.connect(self.worker.refreshFrame)
86  self.parent.LoadFileSignal.connect(self.worker.LoadFile)
87  self.parent.PlaySignal.connect(self.worker.Play)
88  self.parent.PauseSignal.connect(self.worker.Pause)
89  self.parent.SeekSignal.connect(self.worker.Seek)
90  self.parent.SpeedSignal.connect(self.worker.Speed)
91  self.parent.StopSignal.connect(self.worker.Stop)
92 
93  # Move Worker to new thread, and Start
94  self.worker.moveToThread(self.background)
95  self.background.start()
96 
97 
98 ##
99 # QT Player Worker Object (to preview video on a separate thread)
100 class PlayerWorker(QObject):
101 
102  position_changed = pyqtSignal(int)
103  mode_changed = pyqtSignal(object)
104  finished = pyqtSignal()
105 
106  @pyqtSlot(object, object)
107  def Init(self, parent, timeline, videoPreview):
108  self.parent = parent
109  self.timeline = timeline
110  self.videoPreview = videoPreview
111  self.clip_path = None
112  self.clip_reader = None
113  self.original_speed = 0
115  self.previous_clips = []
117  self.is_running = True
118  self.number = None
119  self.current_frame = None
120  self.current_mode = None
121  self.timeline_length = -1
122 
123  # Create QtPlayer class from libopenshot
124  self.player = openshot.QtPlayer()
125 
126  @pyqtSlot()
127  ##
128  # This method starts the video player
129  def Start(self):
130  log.info("QThread Start Method Invoked")
131 
132  # Init new player
133  self.initPlayer()
134 
135  # Connect player to timeline reader
136  self.player.Reader(self.timeline)
137  self.player.Play()
138  self.player.Pause()
139 
140  # Main loop, waiting for frames to process
141  while self.is_running:
142 
143  # Emit position changed signal (if needed)
144  if self.current_frame != self.player.Position():
145  self.current_frame = self.player.Position()
146 
147  if not self.clip_path:
148  # Emit position of overall timeline (don't emit this for clip previews)
149  self.position_changed.emit(self.current_frame)
150 
151  # TODO: Remove this hack and really determine what's blocking the main thread
152  # Try and keep things responsive
153  QCoreApplication.processEvents()
154 
155  # Emit mode changed signal (if needed)
156  if self.player.Mode() != self.current_mode:
157  self.current_mode = self.player.Mode()
158  self.mode_changed.emit(self.current_mode)
159 
160  # wait for a small delay
161  time.sleep(0.01)
162  QCoreApplication.processEvents()
163 
164  self.finished.emit()
165  log.info('exiting thread')
166 
167  @pyqtSlot()
168  def initPlayer(self):
169  log.info("initPlayer")
170 
171  # Get the address of the player's renderer (a QObject that emits signals when frames are ready)
172  self.renderer_address = self.player.GetRendererQObject()
173  self.player.SetQWidget(sip.unwrapinstance(self.videoPreview))
174  self.renderer = sip.wrapinstance(self.renderer_address, QObject)
175  self.videoPreview.connectSignals(self.renderer)
176 
177  ##
178  # Kill this thread
179  def kill(self):
180  self.is_running = False
181 
182  ##
183  # Preview a certain frame
184  def previewFrame(self, number):
185  log.info("previewFrame: %s" % number)
186 
187  # Mark frame number for processing
188  self.Seek(number)
189 
190  log.info("self.player.Position(): %s" % self.player.Position())
191 
192  ##
193  # Refresh a certain frame
194  def refreshFrame(self):
195  log.info("refreshFrame")
196 
197  # Always load back in the timeline reader
198  self.parent.LoadFileSignal.emit('')
199 
200  # Mark frame number for processing (if parent is done initializing)
201  self.Seek(self.player.Position())
202 
203  log.info("self.player.Position(): %s" % self.player.Position())
204 
205  ##
206  # Load a media file into the video player
207  def LoadFile(self, path=None):
208  # Check to see if this path is already loaded
209  # TODO: Determine why path is passed in as an empty string instead of None
210  if path == self.clip_path or (not path and not self.clip_path):
211  return
212 
213  log.info("LoadFile %s" % path)
215 
216  # Determine the current frame of the timeline (when switching to a clip)
217  seek_position = 1
218  if path and not self.clip_path:
219  # Track the current frame
220  self.original_position = self.player.Position()
221 
222  # If blank path, switch back to self.timeline reader
223  if not path:
224  # Return to self.timeline reader
225  log.info("Set timeline reader again in player: %s" % self.timeline)
226  self.player.Reader(self.timeline)
227 
228  # Clear clip reader reference
229  self.clip_reader = None
230  self.clip_path = None
231 
232  # Switch back to last timeline position
233  seek_position = self.original_position
234  else:
235  # Get extension of media path
236  ext = os.path.splitext(path)
237 
238  # Create new timeline reader (to preview selected clip)
240  project = get_app().project
241 
242  # Get some settings from the project
243  fps = project.get(["fps"])
244  width = project.get(["width"])
245  height = project.get(["height"])
246  sample_rate = project.get(["sample_rate"])
247  channels = project.get(["channels"])
248  channel_layout = project.get(["channel_layout"])
249 
250  # Create an instance of a libopenshot Timeline object
251  self.clip_reader = openshot.Timeline(width, height, openshot.Fraction(fps["num"], fps["den"]), sample_rate, channels, channel_layout)
252  self.clip_reader.info.channel_layout = channel_layout
253  self.clip_reader.info.has_audio = True
254  self.clip_reader.info.has_video = True
255  self.clip_reader.info.video_length = 999999
256  self.clip_reader.info.duration = 999999
257  self.clip_reader.info.sample_rate = sample_rate
258  self.clip_reader.info.channels = channels
259 
260  try:
261  # Add clip for current preview file
262  new_clip = openshot.Clip(path)
263  self.clip_reader.AddClip(new_clip)
264  except:
265  log.error('Failed to load media file into video player: %s' % path)
266  return
267 
268  # Assign new clip_reader
269  self.clip_path = path
270 
271  # Keep track of previous clip readers (so we can Close it later)
272  self.previous_clips.append(new_clip)
273  self.previous_clip_readers.append(self.clip_reader)
274 
275  # Open and set reader
276  self.clip_reader.Open()
277  self.player.Reader(self.clip_reader)
278 
279  # Close and destroy old clip readers (leaving the 3 most recent)
280  while len(self.previous_clip_readers) > 3:
281  log.info('Removing old clips from preview: %s' % self.previous_clip_readers[0])
282  previous_clip = self.previous_clips.pop(0)
283  previous_clip.Close()
284  previous_reader = self.previous_clip_readers.pop(0)
285  previous_reader.Close()
286 
287  # Seek to frame 1, and resume speed
288  self.Seek(seek_position)
289 
290  ##
291  # Start playing the video player
292  def Play(self, timeline_length):
293 
294  # Set length of timeline in frames
295  self.timeline_length = timeline_length
296 
297  # Start playback
298  if self.parent.initialized:
299  self.player.Play()
300 
301  ##
302  # Pause the video player
303  def Pause(self):
304 
305  # Pause playback
306  if self.parent.initialized:
307  self.player.Pause()
308 
309  ##
310  # Stop the video player and terminate the playback threads
311  def Stop(self):
312 
313  # Stop playback
314  if self.parent.initialized:
315  self.player.Stop()
316 
317  ##
318  # Seek to a specific frame
319  def Seek(self, number):
320 
321  # Seek to frame
322  if self.parent.initialized:
323  self.player.Seek(number)
324 
325  ##
326  # Set the speed of the video player
327  def Speed(self, new_speed):
328 
329  # Set speed
330  if self.parent.initialized:
331  self.player.Speed(new_speed)
def onPositionChanged(self, current_frame)
def Play(self, timeline_length)
Start playing the video player.
def get_app()
Returns the current QApplication instance of OpenShot.
Definition: app.py:55
def onModeChanged(self, current_mode)
def Seek(self, number)
Seek to a specific frame.
def Stop(self)
Stop the video player and terminate the playback threads.
QT Player Worker Object (to preview video on a separate thread)
def LoadFile(self, path=None)
Load a media file into the video player.
def Start(self)
This method starts the video player.
def refreshFrame(self)
Refresh a certain frame.
def Init(self, parent, timeline, video_widget)
def kill(self)
Kill this thread.
def get_settings()
Get the current QApplication&#39;s settings instance.
Definition: settings.py:44
def Init(self, parent, timeline, videoPreview)
def previewFrame(self, number)
Preview a certain frame.
Class which communicates with the PlayerWorker Class (running on a separate thread) ...
def Speed(self, new_speed)
Set the speed of the video player.
def Pause(self)
Pause the video player.