1# -*- coding: utf-8 -*- 2# Pitivi video editor 3# Copyright (c) 2011, Pier Carteri <pier.carteri@gmail.com> 4# Copyright (c) 2012, Thibault Saunier <tsaunier@gnome.org> 5# 6# This program is free software; you can redistribute it and/or 7# modify it under the terms of the GNU Lesser General Public 8# License as published by the Free Software Foundation; either 9# version 2.1 of the License, or (at your option) any later version. 10# 11# This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14# Lesser General Public License for more details. 15# 16# You should have received a copy of the GNU Lesser General Public 17# License along with this program; if not, write to the 18# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, 19# Boston, MA 02110-1301, USA. 20import html 21from gettext import gettext as _ 22 23from gi.repository import Gdk 24from gi.repository import GdkPixbuf 25from gi.repository import GES 26from gi.repository import GLib 27from gi.repository import GObject 28from gi.repository import Gst 29from gi.repository import Gtk 30from gi.repository import Pango 31 32from pitivi.settings import GlobalSettings 33from pitivi.utils.loggable import Loggable 34from pitivi.utils.misc import uri_is_valid 35from pitivi.utils.pipeline import AssetPipeline 36from pitivi.utils.ui import beautify_length 37from pitivi.utils.ui import beautify_stream 38from pitivi.utils.ui import SPACING 39from pitivi.viewer.viewer import ViewerWidget 40 41PREVIEW_WIDTH = 250 42PREVIEW_HEIGHT = 100 43 44GlobalSettings.addConfigSection('filechooser-preview') 45GlobalSettings.addConfigOption('FCEnablePreview', 46 section='filechooser-preview', 47 key='do-preview-on-clip-import', 48 default=True) 49GlobalSettings.addConfigOption('FCpreviewWidth', 50 section='filechooser-preview', 51 key='video-preview-width', 52 default=PREVIEW_WIDTH) 53GlobalSettings.addConfigOption('FCpreviewHeight', 54 section='filechooser-preview', 55 key='video-preview-height', 56 default=PREVIEW_HEIGHT) 57 58ACCEPTABLE_TAGS = [ 59 Gst.TAG_ALBUM_ARTIST, 60 Gst.TAG_ARTIST, 61 Gst.TAG_TITLE, 62 Gst.TAG_ALBUM, 63 Gst.TAG_BITRATE, 64 Gst.TAG_COMPOSER, 65 Gst.TAG_GENRE, 66 Gst.TAG_PERFORMER, 67 Gst.TAG_DATE, 68 Gst.TAG_COPYRIGHT] 69 70 71class PreviewWidget(Gtk.Grid, Loggable): 72 """Widget for displaying a GStreamer sink with playback controls. 73 74 Args: 75 settings (GlobalSettings): The settings of the app. 76 """ 77 78 def __init__(self, settings, minimal=False, discover_sync=False): 79 Gtk.Grid.__init__(self) 80 Loggable.__init__(self) 81 82 self.log("Init PreviewWidget") 83 self.settings = settings 84 self.error_message = None 85 86 # playbin for play pics 87 self.player = AssetPipeline(clip=None, name="preview-player") 88 self.player.connect('eos', self._pipelineEosCb) 89 self.player.connect('error', self._pipelineErrorCb) 90 self.player._bus.connect('message::tag', self._tag_found_cb) 91 92 # some global variables for preview handling 93 self.is_playing = False 94 self.at_eos = False 95 self.original_dims = (PREVIEW_WIDTH, PREVIEW_HEIGHT) 96 self.countinuous_seek = False 97 self.slider_being_used = False 98 self.current_selected_uri = "" 99 self.current_preview_type = "" 100 self.play_on_discover = False 101 self.description = "" 102 103 # Gui elements: 104 # Drawing area for video output 105 self.preview_video = ViewerWidget(self.player.sink_widget) 106 self.preview_video.props.hexpand = minimal 107 self.preview_video.props.vexpand = minimal 108 self.preview_video.show_all() 109 self.attach(self.preview_video, 0, 0, 1, 1) 110 111 # An image for images and audio 112 self.preview_image = Gtk.Image() 113 self.preview_image.set_size_request( 114 self.settings.FCpreviewWidth, self.settings.FCpreviewHeight) 115 self.preview_image.show() 116 self.attach(self.preview_image, 0, 1, 1, 1) 117 118 # Play button 119 self.bbox = Gtk.Box() 120 self.bbox.set_orientation(Gtk.Orientation.HORIZONTAL) 121 self.play_button = Gtk.ToolButton() 122 self.play_button.set_icon_name("media-playback-start") 123 self.play_button.connect("clicked", self._on_start_stop_clicked_cb) 124 self.bbox.pack_start(self.play_button, False, False, 0) 125 126 # Scale for position handling 127 self.pos_adj = Gtk.Adjustment() 128 self.seeker = Gtk.Scale.new(Gtk.Orientation.HORIZONTAL, self.pos_adj) 129 self.seeker.connect('button-press-event', self._on_seeker_press_cb) 130 self.seeker.connect('button-release-event', self._on_seeker_press_cb) 131 self.seeker.connect('motion-notify-event', self._on_motion_notify_cb) 132 self.seeker.set_draw_value(False) 133 self.seeker.show() 134 self.bbox.pack_start(self.seeker, True, True, 0) 135 136 # Zoom buttons 137 self.b_zoom_in = Gtk.ToolButton() 138 self.b_zoom_in.set_icon_name("zoom-in") 139 self.b_zoom_in.connect("clicked", self._on_zoom_clicked_cb, 1) 140 self.b_zoom_out = Gtk.ToolButton() 141 self.b_zoom_out.set_icon_name("zoom-out") 142 self.b_zoom_out.connect("clicked", self._on_zoom_clicked_cb, -1) 143 self.bbox.pack_start(self.b_zoom_in, False, False, 0) 144 self.bbox.pack_start(self.b_zoom_out, False, False, 0) 145 self.bbox.show_all() 146 self.attach(self.bbox, 0, 2, 1, 1) 147 148 # Label for metadata tags 149 self.l_tags = Gtk.Label() 150 self.l_tags.set_justify(Gtk.Justification.LEFT) 151 self.l_tags.set_ellipsize(Pango.EllipsizeMode.END) 152 self.l_tags.show() 153 self.attach(self.l_tags, 0, 3, 1, 1) 154 155 # Error handling 156 vbox = Gtk.Box() 157 vbox.set_orientation(Gtk.Orientation.VERTICAL) 158 vbox.set_spacing(SPACING) 159 self.l_error = Gtk.Label(label=_("Pitivi can not preview this file.")) 160 self.b_details = Gtk.Button.new_with_label(_("More info")) 161 self.b_details.connect('clicked', self._on_b_details_clicked_cb) 162 vbox.pack_start(self.l_error, True, True, 0) 163 vbox.pack_start(self.b_details, False, False, 0) 164 vbox.show() 165 self.attach(vbox, 0, 4, 1, 1) 166 167 if minimal: 168 self.remove(self.l_tags) 169 self.bbox.remove(self.b_zoom_in) 170 self.bbox.remove(self.b_zoom_out) 171 172 self.clear_preview() 173 self._discover_sync = discover_sync 174 175 def update_preview_cb(self, file_chooser): 176 """Previews the URI of the specified file chooser. 177 178 Args: 179 file_chooser (Gtk.FileChooser): The file chooser providing the URI. 180 """ 181 uri = file_chooser.get_preview_uri() 182 previewable = uri and uri_is_valid(uri) 183 if not previewable: 184 self.clear_preview() 185 return 186 self.preview_uri(uri) 187 188 def preview_uri(self, uri): 189 self.log("Preview request for %s", uri) 190 self.clear_preview() 191 self.current_selected_uri = uri 192 193 if not self._discover_sync: 194 GES.UriClipAsset.new(uri, None, self.__asset_loaded_cb) 195 else: 196 self._handle_new_asset(uri=uri) 197 198 def _handle_new_asset(self, async_result=None, uri=None): 199 try: 200 if uri: 201 asset = GES.UriClipAsset.request_sync(uri) 202 else: 203 asset = GES.Asset.request_finish(async_result) 204 uri = asset.get_id() 205 except GLib.Error as error: 206 self.log("Failed discovering %s: %s", uri, error.message) 207 self._show_error(error.message) 208 return 209 210 self.log("Discovered %s", uri) 211 if not self._show_preview(uri, asset.get_info()): 212 return 213 if self.play_on_discover: 214 self.play_on_discover = False 215 self.play() 216 217 def __asset_loaded_cb(self, source, res): 218 self._handle_new_asset(async_result=res) 219 220 def _show_preview(self, uri, info): 221 self.log("Show preview for %s", uri) 222 duration = info.get_duration() 223 pretty_duration = beautify_length(duration) 224 225 videos = info.get_video_streams() 226 if videos: 227 video = videos[0] 228 if video.is_image(): 229 self.current_preview_type = 'image' 230 self.preview_video.hide() 231 path = Gst.uri_get_location(uri) 232 try: 233 pixbuf = GdkPixbuf.Pixbuf.new_from_file(path) 234 except GLib.Error as error: 235 self.debug("Failed loading image because: %s", error) 236 self._show_error(error.message) 237 return False 238 pixbuf_w = pixbuf.get_width() 239 pixbuf_h = pixbuf.get_height() 240 w, h = self.__get_best_size(pixbuf_w, pixbuf_h) 241 pixbuf = pixbuf.scale_simple( 242 w, h, GdkPixbuf.InterpType.NEAREST) 243 self.preview_image.set_from_pixbuf(pixbuf) 244 self.preview_image.set_size_request( 245 self.settings.FCpreviewWidth, self.settings.FCpreviewHeight) 246 self.preview_image.show() 247 self.bbox.show() 248 self.play_button.hide() 249 self.seeker.hide() 250 self.b_zoom_in.show() 251 self.b_zoom_out.show() 252 else: 253 self.current_preview_type = 'video' 254 self.preview_image.hide() 255 self.player.setClipUri(self.current_selected_uri) 256 self.player.setState(Gst.State.PAUSED) 257 self.pos_adj.props.upper = duration 258 video_width = video.get_square_width() 259 video_height = video.get_height() 260 w, h = self.__get_best_size(video_width, video_height) 261 self.preview_video.set_size_request(w, h) 262 aspect_ratio = video_width / video_height 263 self.preview_video.setDisplayAspectRatio(aspect_ratio) 264 self.preview_video.show() 265 self.bbox.show() 266 self.play_button.show() 267 self.seeker.show() 268 self.b_zoom_in.show() 269 self.b_zoom_out.show() 270 self.description = "\n".join([ 271 _("<b>Resolution</b>: %d×%d") % ( 272 video_width, video_height), 273 _("<b>Duration</b>: %s") % pretty_duration]) 274 else: 275 self.current_preview_type = 'audio' 276 self.preview_video.hide() 277 audio = info.get_audio_streams() 278 if not audio: 279 self.debug("Audio has no streams") 280 return False 281 282 audio = audio[0] 283 self.pos_adj.props.upper = duration 284 self.preview_image.set_from_icon_name( 285 "audio-x-generic", Gtk.IconSize.DIALOG) 286 self.preview_image.show() 287 self.preview_image.set_size_request(PREVIEW_WIDTH, PREVIEW_HEIGHT) 288 self.description = "\n".join([ 289 beautify_stream(audio), 290 _("<b>Duration</b>: %s") % pretty_duration]) 291 self.player.setState(Gst.State.NULL) 292 self.player.setClipUri(self.current_selected_uri) 293 self.player.setState(Gst.State.PAUSED) 294 self.play_button.show() 295 self.seeker.show() 296 self.b_zoom_in.hide() 297 self.b_zoom_out.hide() 298 self.bbox.show() 299 return True 300 301 def _show_error(self, error_message): 302 self.error_message = error_message 303 self.l_error.show() 304 self.b_details.show() 305 306 def play(self): 307 if not self.current_preview_type: 308 self.play_on_discover = True 309 return 310 if self.at_eos: 311 # The content played once already and the pipeline is at the end. 312 self.at_eos = False 313 self.player.simple_seek(0) 314 self.player.setState(Gst.State.PLAYING) 315 self.is_playing = True 316 self.play_button.set_stock_id(Gtk.STOCK_MEDIA_PAUSE) 317 GLib.timeout_add(250, self._update_position) 318 self.debug("Preview started") 319 320 def pause(self, state=Gst.State.PAUSED): 321 if state is not None: 322 self.player.setState(state) 323 self.is_playing = False 324 self.play_button.set_stock_id(Gtk.STOCK_MEDIA_PLAY) 325 self.log("Preview paused") 326 327 def togglePlayback(self): 328 if self.is_playing: 329 self.pause() 330 else: 331 self.play() 332 333 def clear_preview(self): 334 self.log("Reset PreviewWidget") 335 self.seeker.set_value(0) 336 self.bbox.hide() 337 self.l_error.hide() 338 self.b_details.hide() 339 self.description = "" 340 self.l_tags.set_markup("") 341 self.pause(state=Gst.State.NULL) 342 self.current_selected_uri = "" 343 self.current_preview_type = "" 344 self.preview_image.hide() 345 self.preview_video.hide() 346 347 def _on_seeker_press_cb(self, widget, event): 348 self.slider_being_used = True 349 if event.type == Gdk.EventType.BUTTON_PRESS: 350 self.countinuous_seek = True 351 if self.is_playing: 352 self.player.setState(Gst.State.PAUSED) 353 elif event.type == Gdk.EventType.BUTTON_RELEASE: 354 self.countinuous_seek = False 355 value = int(widget.get_value()) 356 self.player.simple_seek(value) 357 self.at_eos = False 358 if self.is_playing: 359 self.player.setState(Gst.State.PLAYING) 360 # Now, allow gobject timeout to continue updating the slider pos: 361 self.slider_being_used = False 362 363 def _on_motion_notify_cb(self, widget, event): 364 if self.countinuous_seek: 365 value = int(widget.get_value()) 366 self.player.simple_seek(value) 367 self.at_eos = False 368 369 def _pipelineEosCb(self, unused_pipeline): 370 self._update_position() 371 self.pause() 372 # The pipeline is at the end. Leave it like that so the last frame 373 # is displayed. 374 self.at_eos = True 375 376 def _pipelineErrorCb(self, unused_pipeline, unused_message, unused_detail): 377 self.pause(state=Gst.State.NULL) 378 379 def _update_position(self, *unused_args): 380 if self.is_playing and not self.slider_being_used: 381 curr_pos = self.player.getPosition() 382 self.pos_adj.set_value(int(curr_pos)) 383 return self.is_playing 384 385 def _on_start_stop_clicked_cb(self, button): 386 self.togglePlayback() 387 388 def _on_zoom_clicked_cb(self, button, increment): 389 if self.current_preview_type == 'video': 390 w, h = self.preview_video.get_size_request() 391 if increment > 0: 392 w *= 1.2 393 h *= 1.2 394 else: 395 w *= 0.8 396 h *= 0.8 397 if (w, h) < self.original_dims: 398 (w, h) = self.original_dims 399 self.preview_video.set_size_request(int(w), int(h)) 400 self.settings.FCpreviewWidth = int(w) 401 self.settings.FCpreviewHeight = int(h) 402 elif self.current_preview_type == 'image': 403 pixbuf = self.preview_image.get_pixbuf() 404 w = pixbuf.get_width() 405 h = pixbuf.get_height() 406 if increment > 0: 407 w *= 1.2 408 h *= 1.2 409 else: 410 w *= 0.8 411 h *= 0.8 412 if (w, h) < self.original_dims: 413 (w, h) = self.original_dims 414 pixbuf = GdkPixbuf.Pixbuf.new_from_file( 415 Gst.uri_get_location(self.current_selected_uri)) 416 pixbuf = pixbuf.scale_simple( 417 int(w), int(h), GdkPixbuf.InterpType.BILINEAR) 418 419 self.preview_image.set_size_request(int(w), int(h)) 420 self.preview_image.set_from_pixbuf(pixbuf) 421 self.preview_image.show() 422 self.settings.FCpreviewWidth = int(w) 423 self.settings.FCpreviewHeight = int(h) 424 425 def _append_tag(self, taglist, tag, tags): 426 if tag in ACCEPTABLE_TAGS: 427 name = Gst.tag_get_nick(tag) 428 type = Gst.tag_get_type(tag) 429 type_getters = {GObject.TYPE_STRING: 'get_string', 430 GObject.TYPE_DOUBLE: 'get_double', 431 GObject.TYPE_FLOAT: 'get_float', 432 GObject.TYPE_INT: 'get_int', 433 GObject.TYPE_UINT: 'get_uint'} 434 if type in type_getters: 435 res, value = getattr(taglist, type_getters[type])(tag) 436 assert res 437 if not type == GObject.TYPE_STRING: 438 value = str(value) 439 tags[name] = value 440 441 def _tag_found_cb(self, unused_bus, message): 442 tag_list = message.parse_tag() 443 tags = {} 444 tag_list.foreach(self._append_tag, tags) 445 items = list(tags.items()) 446 items.sort() 447 text = self.description + "\n\n" 448 for key, value in items: 449 capitalized = key.capitalize() 450 escaped = html.escape(value) 451 text = text + "<b>%s</b>: %s\n" % (key, escaped) 452 self.l_tags.set_markup(text) 453 454 def _on_b_details_clicked_cb(self, unused_button): 455 if not self.error_message: 456 return 457 458 dialog = Gtk.MessageDialog(transient_for=None, 459 modal=True, 460 message_type=Gtk.MessageType.WARNING, 461 buttons=Gtk.ButtonsType.OK, 462 text=self.error_message) 463 dialog.set_icon_name("pitivi") 464 dialog.set_title(_("Error while analyzing a file")) 465 dialog.run() 466 dialog.destroy() 467 468 def do_destroy(self): 469 """Handles the destruction of the widget.""" 470 self.player.release() 471 self.is_playing = False 472 473 def __get_best_size(self, width_in, height_in): 474 if width_in > height_in: 475 if self.settings.FCpreviewWidth < width_in: 476 w = self.settings.FCpreviewWidth 477 h = height_in * w / width_in 478 return (w, h) 479 else: 480 if self.settings.FCpreviewHeight < height_in: 481 h = self.settings.FCpreviewHeight 482 w = width_in * h / height_in 483 return (w, h) 484 return (width_in, height_in) 485