Replace old camera with new Clutter-based camera, resolves numerous issues including LP: #1414443

This commit is contained in:
Sean Davis 2015-08-29 22:24:42 -04:00
parent addbc918cf
commit df455213dd
3 changed files with 149 additions and 256 deletions

View File

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.18.3 -->
<interface>
<!-- interface-requires gtk+ 3.0 -->
<!-- interface-requires camera_mugshot_dialog 1.0 -->
<requires lib="gtk+" version="3.0"/>
<requires lib="camera_mugshot_dialog" version="1.0"/>
<object class="CameraMugshotDialog" id="camera_mugshot_dialog">
<property name="width_request">230</property>
<property name="height_request">300</property>
@ -12,10 +13,10 @@
<property name="default_height">300</property>
<property name="icon_name">mugshot</property>
<property name="type_hint">normal</property>
<signal name="destroy" handler="on_camera_mugshot_dialog_destroy" swapped="no"/>
<signal name="show" handler="on_camera_mugshot_dialog_show" swapped="no"/>
<signal name="delete-event" handler="on_camera_mugshot_dialog_delete_event" swapped="no"/>
<signal name="destroy" handler="on_camera_mugshot_dialog_destroy" swapped="no"/>
<signal name="hide" handler="on_camera_mugshot_dialog_hide" swapped="no"/>
<signal name="show" handler="on_camera_mugshot_dialog_show" swapped="no"/>
<child internal-child="vbox">
<object class="GtkBox" id="dialog-vbox1">
<property name="visible">True</property>

View File

@ -25,54 +25,136 @@ import logging
logger = logging.getLogger('mugshot')
from gi.repository import Gtk, GObject, Gst, GdkPixbuf
from gi.repository import GdkX11, GstVideo # lint:ok
import cairo
from gi.repository import Cheese, Clutter, GtkClutter
import os
from mugshot_lib import helpers
from mugshot_lib.CameraDialog import CameraDialog
Clutter.init(None)
class CameraBox(GtkClutter.Embed):
__gsignals__ = {
'photo-saved': (GObject.SIGNAL_RUN_LAST,
GObject.TYPE_NONE,
(GObject.TYPE_STRING,)),
'gst-state-changed': (GObject.SIGNAL_RUN_LAST,
GObject.TYPE_NONE,
(GObject.TYPE_INT,))
}
def draw_message(widget, message, ctx):
"""Draw a message (including newlines) vertically centered on a cairo
context."""
split_msg = message.split('\n')
def __init__(self, parent):
GtkClutter.Embed.__init__(self)
self.state = Gst.State.NULL
self.parent = parent
# Get the height and width of the drawing area.
alloc = widget.get_allocation()
height = alloc.height
self.stage = self.get_stage()
self.layout_manager = Clutter.BoxLayout()
# Make the background black.
ctx.set_source_rgb(0, 0, 0)
ctx.paint()
self.scroll = Clutter.ScrollActor.new()
self.scroll.set_scroll_mode(Clutter.ScrollMode.HORIZONTALLY)
# Set the font details.
font_size = 20
font_color = (255, 255, 255)
font_name = "Sans"
row_spacing = 6
left_spacing = 10
self.textures_box = Clutter.Actor(layout_manager=self.layout_manager)
self.textures_box.set_x_align(Clutter.ActorAlign.CENTER)
# Get start position
message_height = (len(split_msg) * font_size) + len(split_msg) - 15
current_pos = (height - message_height) / 2
self.scroll.add_actor(self.textures_box)
self.stage.add_actor(self.scroll)
# Draw the message to the drawing area.
ctx.set_source_rgb(*font_color)
ctx.select_font_face(font_name, cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_NORMAL)
ctx.set_font_size(font_size)
self.video_texture = Clutter.Texture.new()
for line in split_msg:
ctx.move_to(left_spacing, current_pos)
ctx.show_text(line)
current_pos = current_pos + font_size + row_spacing
self.layout_manager.pack(self.video_texture, expand=True, x_fill=False, y_fill=False, x_align=Clutter.BoxAlignment.CENTER, y_align=Clutter.BoxAlignment.CENTER)
self.camera = Cheese.Camera.new(self.video_texture, None, 100, 100)
Cheese.Camera.setup(self.camera, None)
Cheese.Camera.play(self.camera)
self.state = Gst.State.PLAYING
def added(signal, data):
node = data.get_device_node()
self.camera.set_device_by_device_node(node)
self.camera.switch_camera_device()
device_monitor=Cheese.CameraDeviceMonitor.new()
device_monitor.connect("added", added)
device_monitor.coldplug()
self.connect("size-allocate", self.on_size_allocate)
self.camera.connect("photo-taken", self.on_photo_taken)
self.camera.connect("state-flags-changed", self.on_state_flags_changed)
self._save_filename = ""
def on_state_flags_changed(self, camera, state):
self.state = state
self.emit("gst-state-changed", self.state)
def play(self):
if self.state != Gst.State.PLAYING:
Cheese.Camera.play(self.camera)
def pause(self):
if self.state == Gst.State.PLAYING:
Cheese.Camera.play(self.camera)
def stop(self):
Cheese.Camera.stop(self.camera)
def on_size_allocate(self, widget, allocation):
vheight = self.video_texture.get_height()
vwidth = self.video_texture.get_width()
if vheight == 0 or vwidth == 0:
vformat = self.camera.get_current_video_format()
vheight = vformat.height
vwidth = vformat.width
height = allocation.height
mult = vheight / height
width = round(vwidth / mult,1)
self.video_texture.set_height(height)
self.video_texture.set_width(width)
point = Clutter.Point()
point.x = (self.video_texture.get_width() - allocation.width) / 2
point.y = 0
self.scroll.scroll_to_point(point)
def take_photo(self, target_filename):
self._save_filename = target_filename
return self.camera.take_photo_pixbuf()
def on_photo_taken(self, camera, pixbuf):
# Get the image dimensions.
height = pixbuf.get_height()
width = pixbuf.get_width()
start_x = 0
start_y = 0
# Calculate a balanced center.
if width > height:
start_x = (width - height) / 2
width = height
else:
start_y = (height - width) / 2
height = width
# Create a new cropped pixbuf.
new_pixbuf = pixbuf.new_subpixbuf(start_x, start_y, width, height)
# Overwrite the temporary file with our new cropped image.
new_pixbuf.savev(self._save_filename, "png", [], [])
self.emit("photo-saved", self._save_filename)
class CameraMugshotDialog(CameraDialog):
"""Camera Capturing Dialog"""
__gtype_name__ = "CameraMugshotDialog"
__gsignals__ = {'apply': (GObject.SIGNAL_RUN_LAST,
GObject.TYPE_NONE,
(GObject.TYPE_STRING,))
}
def finish_initializing(self, builder): # pylint: disable=E1002
"""Set up the camera dialog"""
@ -81,34 +163,15 @@ class CameraMugshotDialog(CameraDialog):
# Initialize Gst or nothing will work.
Gst.init(None)
self.camera = CameraBox(self)
self.camera.show()
self.camera.connect("gst-state-changed", self.on_camera_state_changed)
self.camera.connect("photo-saved", self.on_camera_photo_saved)
# Pack the video widget into the dialog.
vbox = builder.get_object('camera_box')
self.video_window = Gtk.DrawingArea()
self.video_window.connect("realize", self.__on_video_window_realized)
vbox.pack_start(self.video_window, True, True, 0)
self.video_window.show()
# Prepare the camerabin element.
self.camerabin = Gst.ElementFactory.make("camerabin", "camera-source")
if self.camerabin:
bus = self.camerabin.get_bus()
bus.add_signal_watch()
bus.enable_sync_message_emission()
bus.connect("message", self._on_message)
bus.connect("sync-message::element", self._on_sync_message)
self.realized = False
self.draw_handler = self.video_window.connect('draw', self.on_draw)
# If the camera fails to load, show an error on the screen.
else:
devices = []
for device in os.listdir('/dev/'):
if device.startswith('video'):
devices.append(device)
logger.error(_('Camera failed to load. Devices: %s') %
'; '.join(devices))
self.draw_handler = self.video_window.connect('draw',
self.on_failed_draw)
self.realized = True
vbox.pack_start(self.camera, True, True, 0)
# Essential widgets
self.record_button = builder.get_object('camera_record')
@ -119,138 +182,28 @@ class CameraMugshotDialog(CameraDialog):
self.show_all()
def on_failed_draw(self, widget, ctx):
"""Display a message that the camera failed to load."""
# Translators: Please include newlines, as required to fit the message.
message = _("Sorry, but your camera\nfailed to initialize.")
draw_message(widget, message, ctx)
def on_camera_state_changed(self, widget, state):
if state == Gst.State.PLAYING or self.apply_button.get_sensitive():
self.record_button.set_sensitive(True)
else:
self.record_button.set_sensitive(False)
def on_draw(self, widget, ctx):
"""Display a message that the camera is initializing on first draw.
Afterwards, blank the drawing area to clear the message."""
# Translators: Please include newlines, as required to fit the message.
message = _("Please wait while your\ncamera is initialized.")
draw_message(widget, message, ctx)
# Redefine on_draw to blank the drawing area next time.
def on_draw(self, widget, ctx):
"""Redefinition of on_draw to blank the drawing area next time."""
ctx.set_source_rgb(0, 0, 0)
ctx.paint()
# Redefine on_draw once more to do nothing else.
def on_draw(self, widget, ctx):
"""Redefinition of on_draw no longer do anything."""
pass
def on_camera_photo_saved(self, widget, filename):
self.filename = filename
self.apply_button.set_sensitive(True)
self.camera.pause()
def play(self):
"""Start the camera streaming and display the output. It is necessary
to start the camera playing before using most other functions."""
if not self.realized:
self._set_video_window_id()
if not self.realized:
logger.error(_("Cannot display camera output. "
"Ignoring play command"))
else:
if self.camerabin:
self.camerabin.set_state(Gst.State.PLAYING)
self.camera.play()
def pause(self):
"""Pause the camera output. It will cause the image to "freeze".
Use play() to start the camera playing again. Note that calling pause
before play may cause errors on certain camera."""
if self.camerabin:
self.camerabin.set_state(Gst.State.PAUSED)
def take_picture(self, filename):
"""take_picture - grab a frame from the webcam and save it to
'filename.
If play is not called before take_picture,
an error may occur. If take_picture is called immediately after play,
the camera may not be fully initialized, and an error may occur.
Connect to the signal "image-captured" to be alerted when the picture
is saved."""
self.camerabin.set_property("location", filename)
self.camerabin.emit("start-capture")
self.camera.pause()
def stop(self):
"""Stop the camera streaming and display the output."""
self.camerabin.set_state(Gst.State.NULL)
self.camera.stop()
def _on_message(self, bus, message):
"""Internal signal handler for bus messages.
May be useful to extend in a base class to handle messages
produced from custom behaviors.
arguments -
bus: the bus from which the message was sent, typically self.bux
message: the message sent"""
# Ignore if there is no message.
if message is None:
return
# Get the message type.
t = message.type
# Initial load, wait until camera is ready before enabling capture.
if t == Gst.MessageType.ASYNC_DONE:
self.record_button.set_sensitive(True)
if t == Gst.MessageType.ELEMENT:
# Keep the camera working after several pictures are taken.
if message.get_structure().get_name() == "image-captured":
self.camerabin.set_state(Gst.Sate.NULL)
self.camerabin.set_state(Gst.State.PLAYING)
self.emit("image-captured", self.filename)
# Enable interface elements once the images are finished saving.
elif message.get_structure().get_name() == "image-done":
self.apply_button.set_sensitive(True)
self.record_button.set_sensitive(True)
self.pause()
# Stop the stream if the EOS (end of stream) message is received.
if t == Gst.MessageType.EOS:
self.camerabin.set_state(Gst.State.NULL)
# Capture and report any error received.
elif t == Gst.MessageType.ERROR:
err, debug = message.parse_error()
logger.error("%s" % err, debug)
def _on_sync_message(self, bus, message):
""" _on_sync_message - internal signal handler for bus messages.
May be useful to extend in a base class to handle messages
produced from custom behaviors.
arguments -
bus: the bus from which the message was sent, typically self.bux
message: the message sent
"""
# Ignore empty messages.
if message.get_structure() is None:
return
message_name = message.get_structure().get_name()
# Embed the gstreamer element into our window.
if message_name == "prepare-window-handle":
imagesink = message.src
imagesink.set_property("force-aspect-ratio", True)
imagesink.set_window_handle(self.video_window.get_window()
.get_xid())
def __on_video_window_realized(self, widget, data=None):
"""Internal signal handler, used to set up the xid for the drawing area
in a thread safe manner. Do not call directly."""
self._set_video_window_id()
def _set_video_window_id(self):
"""Set the window ID only if not previously configured."""
if not self.realized and self.video_window.get_window() is not None:
self.video_window.get_window().get_xid()
self.realized = True
def take_picture(self, filename):
self.camera.take_photo(filename)
def on_camera_record_clicked(self, widget):
"""When the camera record/retry button is clicked:
@ -283,7 +236,6 @@ class CameraMugshotDialog(CameraDialog):
"""When the camera Apply button is clicked, crop the current photo and
emit a signal to let the main application know there is a new file
available. Then close the camera dialog."""
self.center_crop(self.filename)
self.emit("apply", self.filename)
self.hide()
@ -298,7 +250,7 @@ class CameraMugshotDialog(CameraDialog):
if self.filename and os.path.isfile(self.filename):
os.remove(self.filename)
# Clean up the camera before exiting
self.camerabin.set_state(Gst.State.NULL)
self.camera.stop()
def on_camera_mugshot_dialog_hide(self, widget, data=None):
"""When the dialog is hidden, pause the camera recording."""
@ -312,44 +264,7 @@ class CameraMugshotDialog(CameraDialog):
self.show_all()
self.play()
def center_crop(self, filename):
"""Crop the specified file to square dimensions."""
# Load the image into a Pixbuf.
pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename)
# Get the image dimensions.
height = pixbuf.get_height()
width = pixbuf.get_width()
start_x = 0
start_y = 0
# Calculate a balanced center.
if width > height:
start_x = (width - height) / 2
width = height
else:
start_y = (height - width) / 2
height = width
# Create a new cropped pixbuf.
new_pixbuf = pixbuf.new_subpixbuf(start_x, start_y, width, height)
# Overwrite the temporary file with our new cropped image.
new_pixbuf.savev(filename, "png", [], [])
def on_camera_mugshot_dialog_delete_event(self, widget, data=None):
"""Override the dialog delete event to just hide the window."""
self.hide()
return True
# Signals used by CameraMugshotDialog:
# image-captured: emitted when the camera is done capturing an image.
# apply: emitted when the apply button has been pressed and there is a
# new file saved for use.
__gsignals__ = {'image-captured': (GObject.SIGNAL_RUN_LAST,
GObject.TYPE_NONE,
(GObject.TYPE_PYOBJECT,)),
'apply': (GObject.SIGNAL_RUN_LAST,
GObject.TYPE_NONE,
(GObject.TYPE_STRING,))
}

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-08-31 21:40-0400\n"
"POT-Creation-Date: 2015-08-29 22:19-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -101,32 +101,9 @@ msgstr ""
msgid "Show debug messages (-vv debugs mugshot_lib also)"
msgstr ""
#: ../mugshot/CameraMugshotDialog.py:107
#, python-format
msgid "Camera failed to load. Devices: %s"
msgstr ""
#. Translators: Please include newlines, as required to fit the message.
#: ../mugshot/CameraMugshotDialog.py:125
msgid ""
"Sorry, but your camera\n"
"failed to initialize."
msgstr ""
#. Translators: Please include newlines, as required to fit the message.
#: ../mugshot/CameraMugshotDialog.py:132
msgid ""
"Please wait while your\n"
"camera is initialized."
msgstr ""
#: ../mugshot/CameraMugshotDialog.py:152
msgid "Cannot display camera output. Ignoring play command"
msgstr ""
#. Set the record button to retry, and disable it until the capture
#. finishes.
#: ../mugshot/CameraMugshotDialog.py:279
#: ../mugshot/CameraMugshotDialog.py:232
msgid "Retry"
msgstr ""