Skip to content

Commit

Permalink
#1013 initial webcam support
Browse files Browse the repository at this point in the history
* win32 packaging
* client webcam frame capture via opencv
* server virtual video device injection via cython module
* linux video device detection (so we don't try to use virtual devices for capture)
* configuration option and command line handling
* config file updates
* man page updates

git-svn-id: https://xpra.org/svn/Xpra/trunk@11808 3bb7dfac-3a0b-4e04-842a-767bc560f471
  • Loading branch information
totaam committed Feb 1, 2016
1 parent edcabce commit a408bcf
Show file tree
Hide file tree
Showing 12 changed files with 644 additions and 2 deletions.
8 changes: 8 additions & 0 deletions src/etc/xpra/xpra.conf.in
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,14 @@ auto-refresh-delay = 0.15
av-sync = on


################################################################################
# Webcam forwarding
# webcam = auto
# webcam = no
# webcam = /dev/video0
webcam = auto


################################################################################
# Network Connection

Expand Down
10 changes: 10 additions & 0 deletions src/man/xpra.1
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ xpra \- viewer for remote, persistent X applications
[\fB\-\-xsettings\fP=\fIyes\fP|\fIno\fP]
[\fB\-\-system\-tray\fP=\fIyes\fP|\fIno\fP]
[\fB\-\-bell\fP=\fIyes\fP|\fIno\fP]
[\fB\-\-webcam\fP=\fIyes\fP|\fIno\fP]
[\fB\-\-remote\-logging\fP=\fIyes\fP|\fIno\fP]
[\fB\-\-sound\-source\fP=\fIPLUGIN\fP]
[\fB\-\-speaker\fP=\fIyes\fP|\fIno\fP]
Expand Down Expand Up @@ -73,6 +74,7 @@ xpra \- viewer for remote, persistent X applications
[\fB\-\-xsettings\fP=\fIyes\fP|\fIno\fP]
[\fB\-\-system\-tray\fP=\fIyes\fP|\fIno\fP]
[\fB\-\-bell\fP=\fIyes\fP|\fIno\fP]
[\fB\-\-webcam\fP=\fIyes\fP|\fIno\fP|\fI/dev/deviceXXX\fP|\fIDEVICEID\fP]
[\fB\-\-remote\-logging\fP=\fIyes\fP|\fIno\fP]
[\fB\-\-keyboard\-sync\fP=\fIyes\fP|\fIno\fP]
[\fB\-\-tray\fP=\fIyes\fP|\fIno\fP]
Expand Down Expand Up @@ -122,6 +124,7 @@ xpra \- viewer for remote, persistent X applications
[\fB\-\-clipboard\fP=\fIyes\fP|\fIno\fP]
[\fB\-\-notifications\fP=\fIyes\fP|\fIno\fP]
[\fB\-\-bell\fP=\fIyes\fP|\fIno\fP]
[\fB\-\-webcam\fP=\fIyes\fP|\fIno\fP]
[\fB\-\-sound\-source\fP=\fIPLUGIN\fP]
[\fB\-\-speaker\fP=\fIon\fP|\fIoff\fP|\fIdisabled\fP]
[\fB\-\-speaker\-codec\fP=\fICODEC\fP]
Expand Down Expand Up @@ -851,6 +854,8 @@ platforms.
\fB\-\-bell\fP=\fIyes\fP|\fIno\fP
Enable or disable forwarding of the system bell.
.TP
\fB\-\-webcam\fP=\fIyes\fP|\fIno\fP
Enable or disable webcam forwarding.
\fB\-\-remote\-logging\fP=\fIyes\fP|\fIno\fP
Allow the client to forward its log output to the server.

Expand All @@ -862,6 +867,11 @@ The default is to detect if the graphics card and drivers are
supported (\fIauto\fP mode), but one can also disable OpenGL (\fIno\fP)
or force it enabled (\fIyes\fP).
.TP
\fB\-\-webcam\fP=\fIyes\fP|\fIno\fP|\fI/dev/deviceXXX\fP|\fIDEVICEID\fP
Enable or disable webcam forwarding.
The webcam device to use can also be specified.
.TP

\fB-z\fP\fILEVEL\fP, \fB\-\-compress\fP=\fILEVEL\fP
Select the level of compression xpra will use when transmitting data
over the network.
Expand Down
11 changes: 11 additions & 0 deletions src/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ def is_msvc():
pillow_ENABLED = DEFAULT
webp_ENABLED = DEFAULT and pkg_config_ok("--atleast-version=0.4", "libwebp", fallback=WIN32)
vpx_ENABLED = DEFAULT and pkg_config_ok("--atleast-version=1.3", "vpx", fallback=WIN32)
webcam_ENABLED = DEFAULT and not OSX
v4l2_ENABLED = DEFAULT and (not WIN32 and not OSX)
#ffmpeg 2 onwards:
dec_avcodec2_ENABLED = DEFAULT and pkg_config_ok("--atleast-version=56", "libavcodec", fallback=WIN32)
# some version strings I found:
Expand Down Expand Up @@ -960,6 +962,7 @@ def pkgconfig(*pkgs_options, **ekw):
"xpra/codecs/enc_x264/encoder.c",
"xpra/codecs/enc_x265/encoder.c",
"xpra/codecs/libav_common/av_log.c",
"xpra/codecs/v4l/pusher.c",
"xpra/codecs/webp/encode.c",
"xpra/codecs/webp/decode.c",
"xpra/codecs/dec_avcodec2/decoder.c",
Expand Down Expand Up @@ -1701,6 +1704,9 @@ def add_keywords(path_dirs=[], inc_dirs=[], lib_dirs=[], libs=[], noref=True, no
#this is a mac osx thing:
"ctypes.macholib")

if webcam_ENABLED:
external_includes.append("cv2")

if opengl_ENABLED:
#we need numpy for opengl or as a fallback for the Cython xor module
external_includes.append("numpy")
Expand Down Expand Up @@ -2281,6 +2287,11 @@ def cython_add(*args, **kwargs):
["xpra/codecs/vpx/decoder.pyx"]+membuffers_c,
**vpx_pkgconfig))

toggle_packages(v4l2_ENABLED, "xpra.codecs.v4l2")
if v4l2_ENABLED:
cython_add(Extension("xpra.codecs.v4l2.pusher",
["xpra/codecs/v4l2/pusher.pyx"]+membuffers_c))


toggle_packages(bencode_ENABLED, "xpra.net.bencode")
if cython_bencode_ENABLED:
Expand Down
140 changes: 139 additions & 1 deletion src/xpra/client/ui_client_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
avsynclog = Logger("av-sync")
clipboardlog = Logger("clipboard")
scalinglog = Logger("scaling")
webcamlog = Logger("webcam")


from xpra import __version__ as XPRA_VERSION
Expand All @@ -53,7 +54,7 @@
from xpra.net import compression, packet_encoding
from xpra.child_reaper import reaper_cleanup
from xpra.make_thread import make_thread
from xpra.os_util import Queue, platform_name, get_machine_id, get_user_uuid, bytestostr
from xpra.os_util import BytesIOClass, Queue, platform_name, get_machine_id, get_user_uuid, bytestostr
from xpra.util import nonl, std, iround, AtomicInteger, log_screen_sizes, typedict, updict, csv, engs, CLIENT_EXIT
from xpra.version_util import get_version_info_full, get_platform_info
try:
Expand Down Expand Up @@ -166,6 +167,14 @@ def __init__(self):
self.core_encodings = None
self.encoding = None

#webcam:
self.webcam_option = False
self.webcam_forwarding = False
self.webcam_device = None
self.webcam_device_no = -1
self.server_supports_webcam = False
self.server_virtual_video_devices = 0

#sound:
self.sound_source_plugin = None
self.speaker_allowed = False
Expand Down Expand Up @@ -298,6 +307,22 @@ def init(self, opts):
self.mmap_group = opts.mmap_group
self.shadow_fullscreen = opts.shadow_fullscreen

self.webcam_option = opts.webcam.lower()
self.webcam_forwarding = self.webcam_option not in FALSE_OPTIONS
self.server_supports_webcam = False
self.server_virtual_video_devices = 0
if self.webcam_forwarding:
try:
import cv2
from PIL import Image
assert cv2 and Image
except ImportError as e:
webcamlog.warn("Warning: failed to import opencv:")
webcamlog.warn(" %s", e)
webcamlog.warn(" webcam forwarding is disabled")
self.webcam_forwarding = False
webcamlog("webcam forwarding: %s", self.webcam_option)

self.sound_properties = typedict()
self.speaker_allowed = sound_option(opts.speaker) in ("on", "off")
self.microphone_allowed = sound_option(opts.microphone) in ("on", "off")
Expand Down Expand Up @@ -545,6 +570,7 @@ def cleanup(self):
log.error("error on %s cleanup", type(x), exc_info=True)
#the protocol has been closed, it is now safe to close all the windows:
#(cleaner and needed when we run embedded in the client launcher)
self.stop_sending_webcam()
self.destroy_all_windows()
self.clean_mmap()
reaper_cleanup()
Expand Down Expand Up @@ -1766,6 +1792,13 @@ def process_ui_capabilities(self):
if modifier_keycodes:
self.keyboard_helper.set_modifier_mappings(modifier_keycodes)

#webcam
self.server_supports_webcam = c.boolget("webcam")
self.server_virtual_video_devices = c.intget("virtual-video-devices")
webcamlog("webcam server support: %s (%i devices)", self.server_supports_webcam, self.server_virtual_video_devices)
if self.webcam_forwarding and self.webcam_option=="on" and self.server_supports_webcam and self.server_virtual_video_devices>0:
self.start_sending_webcam()

#sound:
self.server_pulseaudio_id = c.strget("sound.pulseaudio.id")
self.server_pulseaudio_server = c.strget("sound.pulseaudio.server")
Expand Down Expand Up @@ -1940,6 +1973,109 @@ def _process_control(self, packet):
log.warn("received invalid control command from server: %s", command)


def start_sending_webcam(self):
self.do_start_sending_webcam(self.webcam_option)

def do_start_sending_webcam(self, device_str):
webcamlog("start_sending_webcam(%s)", device_str)
assert self.server_supports_webcam
device = 0
if device_str=="auto":
if os.name=="posix":
try:
from xpra.platform.xposix.webcam_util import get_virtual_video_devices, get_all_video_devices
virt_devices = get_virtual_video_devices()
non_virtual = [x.replace("/dev/video", "") for x in get_all_video_devices() if x not in virt_devices]
webcamlog("found %s non-virtual video devices: %s", len(non_virtual), non_virtual)
for x in non_virtual:
try:
device = int(x)
break
except:
pass
except ImportError as e:
webcamlog("no webcam_util: %s", e)
else:
try:
device = int(device_str)
except:
p = device_str.find("video")
if p>=0:
try:
device = int(device_str[p:])
except:
device = 0
import cv2
webcamlog("start_sending_webcam(%s) device=%i", device_str, device)
self.webcam_frame_no = 0
try:
#test capture:
webcam_device = cv2.VideoCapture(device) #0 -> /dev/video0
ret, frame = webcam_device.read()
webcamlog("test capture using %s: %s, %s", webcam_device, ret, frame is not None)
assert ret, "no device or permission"
assert frame is not None, "no data"
assert frame.ndim==3, "unexpected number of dimensions: %s" % frame.ndim
w, h, Bpp = frame.shape
assert Bpp==3, "unexpected number of bytes per pixel: %s" % Bpp
assert frame.size==w*h*Bpp
self.webcam_device_no = device
self.webcam_device = webcam_device
self.send("webcam-start", device, w, h)
#FIXME: add timer to stop if we don't get an ack
except Exception as e:
webcamlog.warn("webcam test capture failed: %s", e)

def stop_sending_webcam(self):
wd = self.webcam_device
webcamlog("stop_sending_webcam() device=%s", wd)
if not wd:
return
self.send("webcam-stop", self.webcam_device_no)
assert self.server_supports_webcam
self.webcam_device = None
self.webcam_device_no = -1
self.webcam_frame_no = 0
try:
wd.release()
except Exception as e:
webcamlog.error("Error closing webcam device %s: %s", wd, e)

def _process_webcam_stop(self, packet):
device_no = packet[1]
if device_no!=self.webcam_device_no:
return
self.stop_sending_webcam()

def _process_webcam_ack(self, packet):
webcamlog("process_webcam_ack: %s", packet)
self.send_webcam_frame()

def send_webcam_frame(self):
assert self.webcam_device_no>=0 and self.webcam_device
try:
import cv2
ret, frame = self.webcam_device.read()
assert ret and frame.ndim==3
w, h, Bpp = frame.shape
assert Bpp==3 and frame.size==w*h*Bpp
webcamlog("send_webcam_frame strides=%s", frame.strides)
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
from PIL import Image
image = Image.fromarray(rgb)
FORMAT = "jpeg" #png
buf = BytesIOClass()
image.save(buf, format=FORMAT)
data = buf.getvalue()
buf.close()
frame_no = self.webcam_frame_no
self.webcam_frame_no += 1
self.send("webcam-frame", self.webcam_device_no, frame_no, FORMAT, w, h, compression.Compressed(FORMAT, data))
except Exception as e:
webcamlog.error("Error sending webcam frame: %s", e)
self.stop_sending_webcam()


def start_sending_sound(self):
""" (re)start a sound source and emit client signal """
soundlog("start_sending_sound()")
Expand Down Expand Up @@ -2779,6 +2915,8 @@ def init_authenticated_packet_handlers(self):
"rpc-reply": self._process_rpc_reply,
"control" : self._process_control,
"draw": self._process_draw,
"webcam-stop": self._process_webcam_stop,
"webcam-ack": self._process_webcam_ack,
# "clipboard-*" packets are handled by a special case below.
})
#these handlers can run directly from the network thread:
Expand Down
4 changes: 4 additions & 0 deletions src/xpra/codecs/v4l2/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# This file is part of Xpra.
# Copyright (C) 2016 Antoine Martin <[email protected]>
# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
# later version. See the file COPYING for details.
Loading

0 comments on commit a408bcf

Please sign in to comment.