Skip to content

Commit

Permalink
Let play webcam from PipeWire
Browse files Browse the repository at this point in the history
  • Loading branch information
hongquan committed Mar 6, 2021
1 parent 2a7a616 commit 96134df
Show file tree
Hide file tree
Showing 6 changed files with 59 additions and 19 deletions.
52 changes: 39 additions & 13 deletions cobang/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,10 @@ def setup_actions(self):
action_about.connect('activate', self.show_about_dialog)
self.add_action(action_about)

def build_gstreamer_pipeline(self):
def build_gstreamer_pipeline(self, src_type: str = 'v4l2src'):
# https://gstreamer.freedesktop.org/documentation/application-development/advanced/pipeline-manipulation.html?gi-language=c#grabbing-data-with-appsink
# Try GL backend first
command = (f'v4l2src name={self.GST_SOURCE_NAME} ! tee name=t ! '
command = (f'{src_type} name={self.GST_SOURCE_NAME} ! tee name=t ! '
f'queue ! glsinkbin sink="gtkglsink name={self.SINK_NAME}" name=sink_bin '
't. ! queue leaky=2 max-size-buffers=2 ! videoconvert ! video/x-raw,format=GRAY8 ! '
f'appsink name={self.APPSINK_NAME} max_buffers=2 drop=1')
Expand All @@ -137,7 +137,7 @@ def build_gstreamer_pipeline(self):
if not pipeline:
logger.info('OpenGL is not available, fallback to normal GtkSink')
# Fallback to non-GL
command = (f'v4l2src name={self.GST_SOURCE_NAME} ! videoconvert ! tee name=t ! '
command = (f'{src_type} name={self.GST_SOURCE_NAME} ! videoconvert ! tee name=t ! '
f'queue ! gtksink name={self.SINK_NAME} '
't. ! queue leaky=1 max-size-buffers=2 ! video/x-raw,format=GRAY8 ! '
f'appsink name={self.APPSINK_NAME}')
Expand Down Expand Up @@ -216,8 +216,11 @@ def discover_webcam(self):
# Device is of private type GstV4l2Device or GstPipeWireDevice
logger.debug('Found device {}', d.get_path_string())
cam_name = d.get_display_name()
cam_path = get_device_path(d)
self.webcam_store.append((cam_path, cam_name))
cam_path, src_type = get_device_path(d)
if not cam_name:
logger.error('Not recognize this device.')
continue
self.webcam_store.append((cam_path, cam_name, src_type))
logger.debug('Start device monitoring')
self.devmonitor.start()

Expand Down Expand Up @@ -255,6 +258,16 @@ def replace_webcam_placeholder_with_gstreamer_sink(self):
self.cont_webcam.add(area)
area.show()

def detach_gstreamer_sink_from_window(self):
old_area = self.cont_webcam.get_child()
self.cont_webcam.remove(old_area)

def attach_gstreamer_sink_to_window(self):
sink = self.gst_pipeline.get_by_name(self.SINK_NAME)
area = sink.get_property('widget')
self.cont_webcam.add(area)
area.show()

def grab_focus_on_event_box(self):
event_box: Gtk.EventBox = self.frame_image.get_child()
event_box.grab_focus()
Expand Down Expand Up @@ -354,23 +367,23 @@ def on_device_monitor_message(self, bus: Gst.Bus, message: Gst.Message, user_dat
if not added_dev:
return True
logger.debug('Added: {}', added_dev)
cam_path = get_device_path(added_dev)
cam_path, src_type = get_device_path(added_dev)
cam_name = added_dev.get_display_name()
# Check if this cam already in the list, add to list if not.
for row in self.webcam_store:
if row[0] == cam_path:
break
else:
self.webcam_store.append((cam_path, cam_name))
self.webcam_store.append((cam_path, cam_name, src_type))
return True
elif message.type == Gst.MessageType.DEVICE_REMOVED:
removed_dev: Optional[Gst.Device] = message.parse_device_removed()
if not removed_dev:
return True
logger.debug('Removed: {}', removed_dev)
cam_path = get_device_path(removed_dev)
cam_path, src_type = get_device_path(removed_dev)
ppl_source = self.gst_pipeline.get_by_name(self.GST_SOURCE_NAME)
if cam_path == ppl_source.get_property('device'):
if cam_path == ppl_source.get_property('device') or cam_path == ppl_source.get_property('path'):
self.gst_pipeline.set_state(Gst.State.NULL)
# Find the entry of just-removed in the list and remove it.
itr: Optional[Gtk.TreeIter] = None
Expand All @@ -391,11 +404,24 @@ def on_webcam_combobox_changed(self, combo: Gtk.ComboBox):
if not liter:
return
model = combo.get_model()
path, name = model[liter]
logger.debug('Picked {} {}', path, name)
path, name, source_type = model[liter]
logger.debug('Picked {} {} ({})', path, name, source_type)
app_sink = self.gst_pipeline.get_by_name(self.APPSINK_NAME)
app_sink.set_emit_signals(False)
self.detach_gstreamer_sink_from_window()
self.gst_pipeline.remove(app_sink)
ppl_source = self.gst_pipeline.get_by_name(self.GST_SOURCE_NAME)
self.gst_pipeline.set_state(Gst.State.NULL)
ppl_source.set_property('device', path)
ppl_source.set_state(Gst.State.NULL)
self.gst_pipeline.remove(ppl_source)
self.build_gstreamer_pipeline(source_type)
self.attach_gstreamer_sink_to_window()
ppl_source = self.gst_pipeline.get_by_name(self.GST_SOURCE_NAME)
if source_type == 'pipewiresrc':
logger.debug('Change pipewiresrc path to {}', path)
ppl_source.set_property('path', path)
else:
logger.debug('Change v4l2src device to {}', path)
ppl_source.set_property('device', path)
self.gst_pipeline.set_state(Gst.State.PLAYING)
app_sink = self.gst_pipeline.get_by_name(self.APPSINK_NAME)
app_sink.set_emit_signals(True)
Expand Down
15 changes: 11 additions & 4 deletions cobang/prep.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import io
from fractions import Fraction
from typing import Sequence, Optional
from typing import Sequence, Optional, Tuple

import gi
from logbook import Logger

gi.require_version('Gio', '2.0')
gi.require_version('GdkPixbuf', '2.0')
Expand All @@ -13,6 +14,9 @@
from .resources import is_local_real_image, maybe_remote_image


logger = Logger(__name__)


def choose_first_image(uris: Sequence[str]) -> Optional[Gio.File]:
for u in uris:
gfile: Gio.File = Gio.file_new_for_uri(u)
Expand All @@ -26,15 +30,18 @@ def choose_first_image(uris: Sequence[str]) -> Optional[Gio.File]:
return gfile


def get_device_path(device: Gst.Device):
def get_device_path(device: Gst.Device) -> Tuple[str, str]:
type_name = device.__class__.__name__
# GstPipeWireDevice doesn't have dedicated GIR binding yet,
# so we have to access its "device.path" in general GStreamer way
if type_name == 'GstPipeWireDevice':
properties = device.get_properties()
return properties['device.path']
path = properties['device.path']
if not path:
path = properties['api.v4l2.path']
return path, 'pipewiresrc'
# Assume GstV4l2Device
return device.get_property('device_path')
return device.get_property('device_path'), 'v4l2src'


def scale_pixbuf(pixbuf: GdkPixbuf.Pixbuf, outer_width: int, outer_height):
Expand Down
2 changes: 2 additions & 0 deletions data/main.glade
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@
<column type="gchararray"/>
<!-- column-name name -->
<column type="gchararray"/>
<!-- column-name source_type -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkApplicationWindow" id="main-window">
Expand Down
5 changes: 5 additions & 0 deletions data/vn.hoabinh.quan.CoBang.appdata.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
</kudos>

<releases>
<release version="0.8.0" date="2021-03-07" urgency="medium">
<description>
<p>Let play webcam from PipeWire.</p>
</description>
</release>
<release version="0.7.0" date="2021-03-06" urgency="medium">
<description>
<p>Add button to copy raw result.</p>
Expand Down
2 changes: 1 addition & 1 deletion meson.build
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
project('cobang',
version: '0.7.0',
version: '0.8.0',
meson_version: '>= 0.54',
default_options: [
'warning_level=2',
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "cobang"
version = "0.7.0"
version = "0.8.0"
description = "QR code scanner for Linux desktop"
authors = ["Nguyễn Hồng Quân <[email protected]>"]
license = "GPL-3.0-or-later"
Expand Down

0 comments on commit 96134df

Please sign in to comment.