xpra icon
Bug tracker and wiki

Ticket #1030: webcam-forwarding.patch

File webcam-forwarding.patch, 35.4 KB (added by Antoine Martin, 3 years ago)

almost working patch using opencv, png encoding only for now

  • etc/xpra/xpra.conf.in

     
    205205
    206206
    207207################################################################################
     208# Webcam forwarding
     209# webcam = auto
     210# webcam = no
     211# webcam = /dev/video0
     212webcam = auto
     213
     214
     215################################################################################
    208216# Network Connection
    209217
    210218# Enable shared memory transfers:
  • setup.py

     
    144144pillow_ENABLED          = DEFAULT
    145145webp_ENABLED            = DEFAULT and pkg_config_ok("--atleast-version=0.4", "libwebp", fallback=WIN32)
    146146vpx_ENABLED             = DEFAULT and pkg_config_ok("--atleast-version=1.3", "vpx", fallback=WIN32)
     147v4l2_ENABLED            = DEFAULT and (not WIN32 and not OSX)
    147148#ffmpeg 2 onwards:
    148149dec_avcodec2_ENABLED    = DEFAULT and pkg_config_ok("--atleast-version=56", "libavcodec", fallback=WIN32)
    149150# some version strings I found:
     
    960961                   "xpra/codecs/enc_x264/encoder.c",
    961962                   "xpra/codecs/enc_x265/encoder.c",
    962963                   "xpra/codecs/libav_common/av_log.c",
     964                   "xpra/codecs/v4l/pusher.c",
    963965                   "xpra/codecs/webp/encode.c",
    964966                   "xpra/codecs/webp/decode.c",
    965967                   "xpra/codecs/dec_avcodec2/decoder.c",
     
    22812283                ["xpra/codecs/vpx/decoder.pyx"]+membuffers_c,
    22822284                **vpx_pkgconfig))
    22832285
     2286toggle_packages(v4l2_ENABLED, "xpra.codecs.v4l2")
     2287if v4l2_ENABLED:
     2288    cython_add(Extension("xpra.codecs.v4l2.pusher",
     2289                ["xpra/codecs/v4l2/pusher.pyx"]+membuffers_c))
    22842290
     2291
    22852292toggle_packages(bencode_ENABLED, "xpra.net.bencode")
    22862293if cython_bencode_ENABLED:
    22872294    bencode_pkgconfig = pkgconfig(optimize=not debug_ENABLED)
  • xpra/client/ui_client_base.py

     
    3434avsynclog = Logger("av-sync")
    3535clipboardlog = Logger("clipboard")
    3636scalinglog = Logger("scaling")
     37webcamlog = Logger("webcam")
    3738
    3839
    3940from xpra import __version__ as XPRA_VERSION
     
    5354from xpra.net import compression, packet_encoding
    5455from xpra.child_reaper import reaper_cleanup
    5556from xpra.make_thread import make_thread
    56 from xpra.os_util import Queue, platform_name, get_machine_id, get_user_uuid, bytestostr
     57from xpra.os_util import BytesIOClass, Queue, platform_name, get_machine_id, get_user_uuid, bytestostr
    5758from xpra.util import nonl, std, iround, AtomicInteger, log_screen_sizes, typedict, updict, csv, engs, CLIENT_EXIT
    5859from xpra.version_util import get_version_info_full, get_platform_info
    5960try:
     
    166167        self.core_encodings = None
    167168        self.encoding = None
    168169
     170        #webcam:
     171        self.webcam_option = False
     172        self.webcam_forwarding = False
     173        self.webcam_device = None
     174        self.webcam_device_no = -1
     175        self.server_supports_webcam = False
     176        self.server_virtual_video_devices = 0
     177
    169178        #sound:
    170179        self.sound_source_plugin = None
    171180        self.speaker_allowed = False
     
    298307        self.mmap_group = opts.mmap_group
    299308        self.shadow_fullscreen = opts.shadow_fullscreen
    300309
     310        self.webcam_option = opts.webcam.lower()
     311        self.webcam_forwarding = self.webcam_option not in FALSE_OPTIONS
     312        self.server_supports_webcam = False
     313        self.server_virtual_video_devices = 0
     314        if self.webcam_forwarding:
     315            try:
     316                import cv2
     317                from PIL import Image
     318                assert cv2 and Image
     319            except ImportError as e:
     320                webcamlog.warn("Warning: failed to import opencv:")
     321                webcamlog.warn(" %s", e)
     322                webcamlog.warn(" webcam forwarding is disabled")
     323                self.webcam_forwarding = False
     324        webcamlog("webcam forwarding: %s", self.webcam_option)
     325
    301326        self.sound_properties = typedict()
    302327        self.speaker_allowed = sound_option(opts.speaker) in ("on", "off")
    303328        self.microphone_allowed = sound_option(opts.microphone) in ("on", "off")
     
    545570                log.error("error on %s cleanup", type(x), exc_info=True)
    546571        #the protocol has been closed, it is now safe to close all the windows:
    547572        #(cleaner and needed when we run embedded in the client launcher)
     573        self.stop_sending_webcam()
    548574        self.destroy_all_windows()
    549575        self.clean_mmap()
    550576        reaper_cleanup()
     
    17661792            if modifier_keycodes:
    17671793                self.keyboard_helper.set_modifier_mappings(modifier_keycodes)
    17681794
     1795        #webcam
     1796        self.server_supports_webcam = c.boolget("webcam")
     1797        self.server_virtual_video_devices = c.intget("virtual-video-devices")
     1798        webcamlog("webcam server support: %s (%i devices)", self.server_supports_webcam, self.server_virtual_video_devices)
     1799        if self.webcam_forwarding and self.webcam_option=="on" and self.server_supports_webcam and self.server_virtual_video_devices>0:
     1800            self.start_sending_webcam()
     1801
    17691802        #sound:
    17701803        self.server_pulseaudio_id = c.strget("sound.pulseaudio.id")
    17711804        self.server_pulseaudio_server = c.strget("sound.pulseaudio.server")
     
    19401973            log.warn("received invalid control command from server: %s", command)
    19411974
    19421975
     1976    def start_sending_webcam(self):
     1977        self.do_start_sending_webcam(self.webcam_option)
     1978
     1979    def do_start_sending_webcam(self, device_str):
     1980        webcamlog("start_sending_webcam(%s)", device_str)
     1981        assert self.server_supports_webcam
     1982        device = 0
     1983        if device_str=="auto":
     1984            if os.name=="posix":
     1985                try:
     1986                    from xpra.platform.xposix.webcam_util import get_virtual_video_devices, get_all_video_devices
     1987                    virt_devices = get_virtual_video_devices()
     1988                    non_virtual = [x.replace("/dev/video", "") for x in get_all_video_devices() if x not in virt_devices]
     1989                    webcamlog("found %s non-virtual video devices: %s", len(non_virtual), non_virtual)
     1990                    for x in non_virtual:
     1991                        try:
     1992                            device = int(x)
     1993                            break
     1994                        except:
     1995                            pass
     1996                except ImportError as e:
     1997                    webcamlog("no webcam_util: %s", e)
     1998        else:
     1999            try:
     2000                device = int(device_str)
     2001            except:
     2002                p = device_str.find("video")
     2003                if p>=0:
     2004                    try:
     2005                        device = int(device_str[p:])
     2006                    except:
     2007                        device = 0
     2008        import cv2
     2009        webcamlog("start_sending_webcam(%s) device=%i", device_str, device)
     2010        self.webcam_frame_no = 0
     2011        self.webcam_device_no = device
     2012        self.webcam_device = cv2.VideoCapture(device)        #0 -> /dev/video0
     2013        try:
     2014            #test capture:
     2015            ret, frame = self.webcam_device.read()
     2016            webcamlog("test capture using %s: %s, %s", self.webcam_device, ret, frame is not None)
     2017            assert ret and frame is not None, "no data"
     2018            assert frame.ndim==3, "unexpected  number of dimensions: %s" % frame.ndim
     2019            w, h, Bpp = frame.shape
     2020            assert Bpp==3, "unexpected number of bytes per pixel: %s" % Bpp
     2021            assert frame.size==w*h*Bpp
     2022            self.send("webcam-start", device, w, h)
     2023            #FIXME: add timer to stop if we don't get an ack
     2024        except Exception as e:
     2025            webcamlog.warn("webcam test capture failed: %s", e)
     2026            self.stop_sending_webcam()
     2027
     2028    def stop_sending_webcam(self):
     2029        wd = self.webcam_device
     2030        webcamlog("stop_sending_webcam() device=%s", wd)
     2031        if not wd:
     2032            return
     2033        self.send("webcam-stop", self.webcam_device_no)
     2034        assert self.server_supports_webcam
     2035        self.webcam_device = None
     2036        self.webcam_device_no = -1
     2037        self.webcam_frame_no = 0
     2038        try:
     2039            wd.release()
     2040        except Exception as e:
     2041            webcamlog.error("Error closing webcam device %s: %s", wd, e)
     2042
     2043    def _process_webcam_stop(self, packet):
     2044        device_no = packet[1]
     2045        if device_no!=self.webcam_device_no:
     2046            return
     2047        self.stop_sending_webcam()
     2048
     2049    def _process_webcam_ack(self, packet):
     2050        webcamlog("process_webcam_ack: %s", packet)
     2051        self.send_webcam_frame()
     2052
     2053    def send_webcam_frame(self):
     2054        assert self.webcam_device_no>=0 and self.webcam_device
     2055        try:
     2056            import cv2
     2057            ret, frame = self.webcam_device.read()
     2058            assert ret and frame.ndim==3
     2059            w, h, Bpp = frame.shape
     2060            assert Bpp==3 and frame.size==w*h*Bpp
     2061            webcamlog("send_webcam_frame strides=%s", frame.strides)
     2062            rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
     2063            from PIL import Image
     2064            image = Image.fromarray(rgb)
     2065            png = BytesIOClass()
     2066            image.save(png, format="PNG")
     2067            data = png.getvalue()
     2068            png.close()
     2069            frame_no = self.webcam_frame_no
     2070            self.webcam_frame_no += 1
     2071            self.send("webcam-frame", self.webcam_device_no, frame_no, "png", w, h, compression.Compressed("png", data))
     2072        except Exception as e:
     2073            webcamlog.error("Error sending webcam frame: %s", e)
     2074            self.stop_sending_webcam()
     2075
     2076
    19432077    def start_sending_sound(self):
    19442078        """ (re)start a sound source and emit client signal """
    19452079        soundlog("start_sending_sound()")
     
    27792913            "rpc-reply":            self._process_rpc_reply,
    27802914            "control" :             self._process_control,
    27812915            "draw":                 self._process_draw,
     2916            "webcam-stop":          self._process_webcam_stop,
     2917            "webcam-ack":           self._process_webcam_ack,
    27822918            # "clipboard-*" packets are handled by a special case below.
    27832919            })
    27842920        #these handlers can run directly from the network thread:
  • xpra/codecs/v4l2/__init__.py

     
     1# This file is part of Xpra.
     2# Copyright (C) 2016 Antoine Martin <antoine@devloop.org.uk>
     3# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
     4# later version. See the file COPYING for details.
  • xpra/codecs/v4l2/pusher.pyx

     
     1# This file is part of Xpra.
     2# Copyright (C) 2012-2015 Antoine Martin <antoine@devloop.org.uk>
     3# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
     4# later version. See the file COPYING for details.
     5
     6import time
     7import os
     8
     9from xpra.log import Logger
     10from _dbus_bindings import UInt32
     11log = Logger("webcam")
     12
     13from xpra.codecs.image_wrapper import ImageWrapper
     14
     15
     16from libc.stdint cimport uint32_t, uint8_t
     17
     18cdef extern from "../../buffers/memalign.h":
     19    void *xmemalign(size_t size) nogil
     20    void *memset(void * ptr, int value, size_t num)
     21
     22cdef extern from "stdlib.h":
     23    void free(void* ptr)
     24
     25cdef extern from "string.h":
     26    void * memcpy ( void * destination, void * source, size_t num )
     27
     28
     29cdef extern from "../../buffers/buffers.h":
     30    int object_as_buffer(object obj, const void ** buffer, Py_ssize_t * buffer_len)
     31    int get_buffer_api_version()
     32
     33
     34cdef extern from *:
     35    ctypedef unsigned long size_t
     36
     37cdef extern from "sys/ioctl.h":
     38    int ioctl(int fd, unsigned long request, ...)
     39
     40cdef extern from "linux/videodev2.h":
     41    int VIDIOC_QUERYCAP
     42    int VIDIOC_G_FMT
     43    int VIDIOC_S_FMT
     44    int V4L2_COLORSPACE_SRGB
     45    int V4L2_FIELD_NONE
     46    int V4L2_PIX_FMT_YUV420
     47    int V4L2_PIX_FMT_YVU420
     48    int V4L2_BUF_TYPE_VIDEO_OUTPUT
     49
     50    cdef struct v4l2_capability:
     51        uint8_t driver[16]
     52        uint8_t card[32]
     53        uint8_t bus_info[32]
     54        uint32_t version
     55        uint32_t capabilities
     56        uint32_t device_caps
     57        uint32_t reserved[3]
     58
     59    cdef struct v4l2_pix_format:
     60        uint32_t width
     61        uint32_t height
     62        uint32_t pixelformat
     63        uint32_t field          # enum v4l2_field */
     64        uint32_t bytesperline   # for padding, zero if unused */
     65        uint32_t sizeimage
     66        uint32_t colorspace     # enum v4l2_colorspace */
     67        uint32_t priv           # private data, depends on pixelformat */
     68        uint32_t flags          # format flags (V4L2_PIX_FMT_FLAG_*) */
     69        uint32_t ycbcr_enc      # enum v4l2_ycbcr_encoding */
     70        uint32_t quantization   # enum v4l2_quantization */
     71        uint32_t xfer_func      # enum v4l2_xfer_func */
     72
     73        pass
     74    cdef struct v4l2_pix_format_mplane:
     75        pass
     76    cdef struct v4l2_window:
     77        pass
     78    cdef struct v4l2_vbi_format:
     79        pass
     80    cdef struct v4l2_sliced_vbi_format:
     81        pass
     82    cdef struct v4l2_sdr_format:
     83        pass
     84
     85    cdef union v4l2_format_fmt:
     86        v4l2_pix_format          pix        #V4L2_BUF_TYPE_VIDEO_CAPTURE
     87        v4l2_pix_format_mplane   pix_mp     #V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE
     88        v4l2_window              win        #V4L2_BUF_TYPE_VIDEO_OVERLAY
     89        v4l2_vbi_format          vbi        #V4L2_BUF_TYPE_VBI_CAPTURE
     90        v4l2_sliced_vbi_format   sliced     #V4L2_BUF_TYPE_SLICED_VBI_CAPTURE
     91        v4l2_sdr_format          sdr        #V4L2_BUF_TYPE_SDR_CAPTURE
     92        uint8_t raw_data[200]               #user-defined
     93
     94    cdef struct v4l2_format:
     95        uint32_t type
     96        v4l2_format_fmt fmt
     97
     98def init_module():
     99    log("v4l2.pusher.init_module()")
     100
     101def cleanup_module():
     102    log("v4l2pusher.cleanup_module()")
     103
     104def get_version():
     105    return 0
     106
     107def get_type():
     108    return "v4l2"
     109
     110def get_info():
     111    global COLORSPACES, MAX_WIDTH, MAX_HEIGHT
     112    return {"version"   : get_version(),
     113            "buffer_api": get_buffer_api_version()}
     114
     115def get_input_colorspaces():
     116    return  ["YUV420P"]
     117
     118
     119cdef class Pusher:
     120    cdef unsigned long frames
     121    cdef int width
     122    cdef int height
     123    cdef int rowstride
     124    cdef size_t framesize
     125    cdef object src_format
     126    cdef object device
     127    cdef object device_name
     128
     129    cdef object __weakref__
     130
     131    def init_context(self, int width, int height, int rowstride, src_format, device):    #@DuplicatedSignature
     132        assert src_format=="YUV420P"
     133        self.width = width
     134        self.height = height
     135        self.rowstride = rowstride
     136        self.src_format = src_format
     137        self.frames = 0
     138        self.framesize = self.height*self.rowstride*3//2
     139        self.init_device(device)
     140   
     141    cdef init_device(self, device):
     142        cdef v4l2_capability vid_caps
     143        cdef v4l2_format vid_format
     144        self.device_name = device or os.environ.get("XPRA_VIDEO_DEVICE", "/dev/video1")
     145        log("v4l2 using device %s", self.device_name)
     146        self.device = open(self.device_name, "wb")
     147        r = ioctl(self.device.fileno(), VIDIOC_QUERYCAP, &vid_caps)
     148        log("ioctl(%s, VIDIOC_QUERYCAP, %#x)=%s", self.device_name, <unsigned long> &vid_caps, r)
     149        assert r!=-1, "VIDIOC_QUERYCAP ioctl failed on %s" % self.device_name
     150        memset(&vid_format, 0, sizeof(vid_format))
     151        r = ioctl(self.device.fileno(), VIDIOC_G_FMT, &vid_format)
     152        log("ioctl(%s, VIDIOC_G_FMT, %#x)=%s", self.device_name, <unsigned long> &vid_format, r)
     153        #assert r!=-1, "VIDIOC_G_FMT ioctl failed on %s" % self.device_name
     154        vid_format.type = V4L2_BUF_TYPE_VIDEO_OUTPUT
     155        vid_format.fmt.pix.width = self.width
     156        vid_format.fmt.pix.height = self.height
     157        vid_format.fmt.pix.pixelformat = V4L2_PIX_FMT_YUV420        #V4L2_PIX_FMT_YVU420
     158        vid_format.fmt.pix.sizeimage = self.framesize
     159        vid_format.fmt.pix.field = V4L2_FIELD_NONE
     160        vid_format.fmt.pix.bytesperline = self.rowstride
     161        vid_format.fmt.pix.colorspace = V4L2_COLORSPACE_SRGB
     162        r = ioctl(self.device.fileno(), VIDIOC_S_FMT, &vid_format)
     163        log("vid_format.type                 = %i", vid_format.type)
     164        log("vid_format.fmt.pix.width        = %i", vid_format.fmt.pix.width)
     165        log("vid_format.fmt.pix.height       = %i", vid_format.fmt.pix.height)
     166        log("vid_format.fmt.pix.pixelformat  = %i", vid_format.fmt.pix.pixelformat)
     167        log("vid_format.fmt.pix.sizeimage    = %i", vid_format.fmt.pix.sizeimage)
     168        log("vid_format.fmt.pix.field        = %i", vid_format.fmt.pix.field)
     169        log("vid_format.fmt.pix.bytesperline = %i", vid_format.fmt.pix.bytesperline)
     170        log("vid_format.fmt.pix.colorspace   = %i", vid_format.fmt.pix.colorspace)
     171        log("ioctl(%s, VIDIOC_S_FMT, %#x)=%s", self.device_name, <unsigned long> &vid_format, r)
     172        assert r!=-1, "VIDIOC_S_FMT ioctl failed on %s" % self.device_name
     173
     174    def clean(self):                        #@DuplicatedSignature
     175        self.width = 0
     176        self.height = 0
     177        self.rowstride = 0
     178        self.src_format = ""
     179        self.frames = 0
     180        self.framesize = 0
     181
     182    def get_info(self):             #@DuplicatedSignature
     183        info = get_info()
     184        info.update({"frames"    : self.frames,
     185                     "width"     : self.width,
     186                     "height"    : self.height,
     187                     "src_format": self.src_format})
     188        return info
     189
     190    def __repr__(self):
     191        if self.src_format is None:
     192            return "v4l2.Pusher(uninitialized)"
     193        return "v4l2.Pusher(%s - %sx%s)" % (self.src_format, self.width, self.height)
     194
     195    def is_closed(self):
     196        return not bool(self.src_format)
     197
     198    def __dealloc__(self):
     199        self.clean()
     200
     201    def get_width(self):
     202        return self.width
     203
     204    def get_height(self):
     205        return self.height
     206
     207    def get_type(self):                     #@DuplicatedSignature
     208        return  "v4l2"
     209
     210    def get_src_format(self):
     211        return self.src_format
     212
     213
     214    def push_image(self, image):
     215        cdef unsigned char *Ybuf
     216        cdef unsigned char *Ubuf
     217        cdef unsigned char *Vbuf
     218        cdef unsigned int Ystride, Ustride, Vstride
     219        cdef Py_ssize_t buf_len = 0
     220        cdef uint8_t* buf
     221
     222        iplanes = image.get_planes()
     223        assert iplanes==ImageWrapper._3_PLANES, "invalid input format: %s planes" % iplanes
     224        assert image.get_width()>=self.width, "invalid image width: %s (minimum is %s)" % (image.get_width(), self.width)
     225        assert image.get_height()>=self.height, "invalid image height: %s (minimum is %s)" % (image.get_height(), self.height)
     226        planes = image.get_pixels()
     227        assert planes, "failed to get pixels from %s" % image
     228        input_strides = image.get_rowstride()
     229        log("push_image(%s) strides=%s", (image), input_strides)
     230        Ystride = input_strides[0]
     231        Ustride = input_strides[1]
     232        Vstride = input_strides[2]
     233        assert Ystride==self.rowstride
     234        assert Ustride==self.rowstride//2
     235        assert Vstride==self.rowstride//2
     236        assert object_as_buffer(planes[0], <const void**> &Ybuf, &buf_len)==0, "failed to convert %s to a buffer" % type(planes[0])
     237        assert buf_len>=Ystride*image.get_height(), "buffer for Y plane is too small: %s bytes, expected at least %s" % (buf_len, Ystride*image.get_height())
     238        assert object_as_buffer(planes[1], <const void**> &Ubuf, &buf_len)==0, "failed to convert %s to a buffer" % type(planes[1])
     239        assert buf_len>=Ustride*image.get_height()//2, "buffer for U plane is too small: %s bytes, expected at least %s" % (buf_len, Ustride*image.get_height()/2)
     240        assert object_as_buffer(planes[2], <const void**> &Vbuf, &buf_len)==0, "failed to convert %s to a buffer" % type(planes[2])
     241        assert buf_len>=Vstride*image.get_height()//2, "buffer for V plane is too small: %s bytes, expected at least %s" % (buf_len, Vstride*image.get_height()/2)
     242
     243        buf = <uint8_t*> xmemalign(self.framesize)
     244        assert buf!=NULL
     245        memcpy(buf, Ybuf, Ystride*self.height)
     246        memcpy(buf+Ystride*self.height, Ubuf, Ustride*self.height//2)
     247        memcpy(buf+Ystride*self.height+Ustride*self.height//2, Vbuf, Vstride*self.height//2)
     248        #memset(buf, 128, self.framesize)
     249        self.device.write(buf[:self.framesize])
     250        self.device.flush()
     251        free(buf)
     252
     253
     254def selftest(full=False):
     255    pass
  • xpra/codecs/v4l2

  • xpra/log.py

    Property changes on: xpra/codecs/v4l2
    ___________________________________________________________________
    Added: svn:ignore
    ## -0,0 +1,2 ##
    +pusher.c
    +pusher.html
     
    198198                ("x264"         , "libx264 encoder"),
    199199                ("x265"         , "libx265 encoder"),
    200200                ("webp"         , "libwebp encoder and decoder"),
     201                ("webcam"       , "webcam access"),
    201202                ])),
    202203    ("Pointer", OrderedDict([
    203204                ("mouse"        , "Mouse motion"),
  • xpra/platform/xposix/webcam_util.py

     
     1# This file is part of Xpra.
     2# Copyright (C) 2016 Antoine Martin <antoine@devloop.org.uk>
     3# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
     4# later version. See the file COPYING for details.
     5
     6import os
     7
     8from xpra.log import Logger
     9log = Logger("client")
     10from xpra.util import engs
     11
     12
     13def get_virtual_video_devices():
     14    log("get_virtual_video_devices")
     15    v4l2_virtual_dir = "/sys/devices/virtual/video4linux"
     16    if not os.path.exists(v4l2_virtual_dir) or not os.path.isdir(v4l2_virtual_dir):
     17        log.warn("Warning: virtual video directory %s not found", v4l2_virtual_dir)
     18        log.warn(" webcam forwarding disabled")
     19        return []
     20    contents = os.listdir(v4l2_virtual_dir)
     21    devices = []
     22    for f in contents:
     23        if not f.startswith("video"):
     24            continue
     25        try:
     26            no = int(f[len("video"):])
     27            assert no>=0
     28        except:
     29            continue
     30        dev_dir = os.path.join(v4l2_virtual_dir, f)
     31        if not os.path.isdir(dev_dir):
     32            continue
     33        dev_name = os.path.join(dev_dir, "name")
     34        try:
     35            name = open(dev_name).read().replace("\n", "")
     36            log("found %s: %s", f, name)
     37        except:
     38            continue
     39        dev_file = "/dev/%s" % f
     40        devices.append(dev_file)
     41    log("devices: %s", devices)
     42    log("found %i virtual video device%s", len(devices), engs(devices))
     43    return devices
     44
     45def get_all_video_devices():
     46    contents = os.listdir("/dev")
     47    devices = []
     48    for f in contents:
     49        if not f.startswith("video"):
     50            continue
     51        dev_file = "/dev/%s" % f
     52        if not os.path.isfile(dev_file):
     53            continue
     54        try:
     55            no = int(f[len("video"):])
     56            assert no>=0
     57        except:
     58            continue
     59        devices.append(f)
     60    return devices
     61
     62
     63def main():
     64    import sys
     65    if "-v" in sys.argv or "--verbose" in sys.argv:
     66        from xpra.log import add_debug_category
     67        add_debug_category("webcam")
     68
     69    from xpra.platform import program_context
     70    with program_context("Webcam Info", "Webcam Info"):
     71        devices = get_virtual_video_devices()
     72        log.info("Found %i virtual video device%s:", len(devices), engs(devices))
     73        for d in devices:
     74            log.info("%s", d)
     75
     76if __name__ == "__main__":
     77    main()
  • xpra/scripts/config.py

     
    316316                    "socket-permissions": str,
    317317                    "exec-wrapper"      : str,
    318318                    "dbus-launch"       : str,
     319                    "webcam"            : str,
    319320                    #int options:
    320321                    "quality"           : int,
    321322                    "min-quality"       : int,
     
    460461                    "socket-permissions": "600",
    461462                    "exec-wrapper"      : "",
    462463                    "dbus-launch"       : "dbus-launch --close-stderr",
     464                    "webcam"            : "auto",
    463465                    "quality"           : 0,
    464466                    "min-quality"       : 30,
    465467                    "speed"             : 0,
  • xpra/scripts/main.py

     
    423423    group.add_option("--bell", action="store",
    424424                      dest="bell", default=defaults.bell, metavar="yes|no",
    425425                      help="Forward the system bell. Default: %s." % enabled_str(defaults.bell))
     426    group.add_option("--webcam", action="store",
     427                      dest="webcam", default=defaults.webcam,
     428                      help="Webcam forwarding, can be used to specify a device. Default: %s." % defaults.webcam)
    426429    legacy_bool_parse("global-menus")
    427430    group.add_option("--global-menus", action="store",
    428431                      dest="global_menus", default=defaults.global_menus, metavar="yes|no",
  • xpra/server/server_base.py

     
    2626clipboardlog = Logger("clipboard")
    2727rpclog = Logger("rpc")
    2828dbuslog = Logger("dbus")
     29webcamlog = Logger("webcam")
    2930
    3031from xpra.keyboard.mask import DEFAULT_MODIFIER_MEANINGS
    3132from xpra.server.server_core import ServerCore, get_thread_info
     
    3233from xpra.server.control_command import ArgsControlCommand, ControlError
    3334from xpra.simple_stats import to_std_unit
    3435from xpra.child_reaper import getChildReaper
    35 from xpra.os_util import thread, get_hex_uuid, livefds, load_binary_file
     36from xpra.os_util import BytesIOClass, thread, get_hex_uuid, livefds, load_binary_file
    3637from xpra.util import typedict, updict, log_screen_sizes, engs, repr_ellipsized, csv, iround, \
    3738    SERVER_EXIT, SERVER_ERROR, SERVER_SHUTDOWN, DETACH_REQUEST, NEW_CLIENT, DONE, IDLE_TIMEOUT
    3839from xpra.net.bytestreams import set_socket_timeout
     
    138139        self.send_pings = False
    139140        self.scaling_control = False
    140141        self.rpc_handlers = {}
     142        self.webcam_forwarding = False
     143        self.webcam_forwarding_device = None
     144        self.virtual_video_devices = 0
    141145
    142146        #sound:
    143147        self.pulseaudio = False
     
    231235        self.notifications_forwarder = None
    232236        self.notifications = opts.notifications
    233237        self.scaling_control = parse_bool_or_int("video-scaling", opts.video_scaling)
     238        self.webcam_forwarding = opts.webcam.lower() not in FALSE_OPTIONS
     239        if self.webcam_forwarding:
     240            self.virtual_video_devices = self.init_virtual_video_devices()
     241            if self.virtual_video_devices==0:
     242                self.webcam_forwarding = False
    234243
    235244        #sound:
    236245        self.pulseaudio = opts.pulseaudio
     
    343352        printlog("init_printing() printing=%s", self.printing)
    344353
    345354
     355    def init_virtual_video_devices(self):
     356        webcamlog("init_virtual_video_devices")
     357        if os.name!="posix":
     358            return 0
     359        try:
     360            from xpra.codecs.v4l2.pusher import Pusher
     361            assert Pusher
     362        except ImportError as e:
     363            webcamlog.error("Error: failed to import the virtual video module:")
     364            webcamlog.error(" %s", e)
     365            return 0
     366        try:
     367            from xpra.platform.xposix.webcam_util import get_virtual_video_devices
     368        except ImportError as e:
     369            webcamlog.warn("Warning: cannot load webcam components")
     370            webcamlog.warn(" %s", e)
     371            webcamlog.warn(" webcam forwarding disabled")
     372            return 0
     373        devices = get_virtual_video_devices()
     374        webcamlog.info("found %i virtual video device%s", len(devices), engs(devices))
     375        return len(devices)
     376
    346377    def init_uuid(self):
    347378        # Define a server UUID if needed:
    348379        self.uuid = self.get_uuid()
     
    564595            "command_request":                      self._process_command_request,
    565596            "printers":                             self._process_printers,
    566597            "send-file":                            self._process_send_file,
     598            "webcam-start":                         self._process_webcam_start,
     599            "webcam-stop":                          self._process_webcam_stop,
     600            "webcam-frame":                         self._process_webcam_frame,
    567601          }
    568602        self._authenticated_ui_packet_handlers = self._default_packet_handlers.copy()
    569603        self._authenticated_ui_packet_handlers.update({
     
    731765        getVideoHelper().cleanup()
    732766        reaper_cleanup()
    733767        self.cleanup_pulseaudio()
     768        self.stop_virtual_webcam()
    734769        ds = self.dbus_server
    735770        if ds:
    736771            ds.cleanup()
     
    11551190                 "start-new-commands"           : self.start_new_commands,
    11561191                 "exit-with-children"           : self.exit_with_children,
    11571192                 "av-sync.enabled"              : self.av_sync,
     1193                 "webcam"                       : self.webcam_forwarding,
     1194                 "virtual-video-devices"        : self.virtual_video_devices,
    11581195                 })
    11591196            capabilities.update(self.get_file_transfer_features())
    11601197            for x in self.get_server_features():
     
    17381775            return {}
    17391776        return self._clipboard_helper.get_info()
    17401777
     1778    def get_webcam_info(self):
     1779        return {""                          : self.webcam_forwarding,
     1780                "virtual-video-devices"     : self.virtual_video_devices}
     1781
    17411782    def do_get_info(self, proto, server_sources=None, window_ids=None):
    17421783        info = {"server.python.version" : python_platform.python_version()}
    17431784
     
    17441785        def up(prefix, d, suffix=""):
    17451786            updict(info, prefix, d, suffix)
    17461787
     1788        up("webcam",    self.get_webcam_info())
    17471789        up("file",      self.get_file_transfer_info())
    17481790        up("printing",  self.get_printing_info())
    17491791        up("features",  self.get_features_info())
     
    24502492        log.info("_process_configure_window(%s, %s)", proto, packet)
    24512493
    24522494
     2495    def _process_webcam_start(self, proto, packet):
     2496        assert self.webcam_forwarding
     2497        ss = self._server_sources.get(proto)
     2498        if not ss:
     2499            webcamlog.warn("Warning: invalid client source for webcam start")
     2500            return
     2501        device, w, h = packet[1:4]
     2502        self.start_virtual_webcam(ss, device, w, h)
     2503   
     2504    def start_virtual_webcam(self, ss, device, w, h):
     2505        assert w>0 and h>0
     2506        from xpra.platform.xposix.webcam_util import get_virtual_video_devices
     2507        devices = get_virtual_video_devices()
     2508        if len(devices)==0:
     2509            webcamlog.warn("Warning: cannot start webcam forwarding, no virtual devices found")
     2510            ss.send_webcam_stop(device)
     2511            return
     2512        if self.webcam_forwarding_device:
     2513            self.stop_virtual_webcam()
     2514        device_str = devices[0]
     2515        try:
     2516            from xpra.codecs.v4l2.pusher import Pusher    #@UnresolvedImport
     2517            p = Pusher()
     2518            p.init_context(w, h, w, "YUV420P", device_str)
     2519            self.webcam_forwarding_device = p
     2520            webcamlog.info("webcam forwarding using %s", device_str)
     2521            #this tell the client to start sending:
     2522            ss.send_webcam_ack(device, 0)
     2523        except Exception as e:
     2524            webcamlog.error("Error setting up webcam forwarding:")
     2525            webcamlog.error(" %s", e)
     2526            ss.send_webcam_stop(device, str(e))
     2527
     2528    def _process_webcam_stop(self, proto, packet):
     2529        device, message = packet[1, 2]
     2530        webcamlog("stopping webcam device %s: %s", device, message)
     2531        if not self.webcam_forwarding_device:
     2532            webcamlog.warn("Warning: cannot stop webcam device %s: no such context!", device, message)
     2533            return
     2534        self.stop_virtual_webcam()
     2535
     2536    def stop_virtual_webcam(self):
     2537        webcamlog("stop_virtual_webcam() webcam_forwarding_device=%s", self.webcam_forwarding_device)
     2538        vfd = self.webcam_forwarding_device
     2539        if vfd:
     2540            self.webcam_forwarding_device = None
     2541            vfd.clean()
     2542
     2543    def _process_webcam_frame(self, proto, packet):
     2544        device, frame_no, encoding, w, h, data = packet[1:7]
     2545        assert encoding and w and h and data
     2546        ss = self._server_sources.get(proto)
     2547        if not ss:
     2548            webcamlog.warn("Warning: invalid client source for webcam frame")
     2549            return
     2550        vfd = self.webcam_forwarding_device
     2551        if not self.webcam_forwarding_device:
     2552            webcamlog.warn("Warning: webcam forwarding is not active, dropping frame")
     2553            ss.send_webcam_stop(device, "not started")
     2554            return           
     2555        try:
     2556            #only png supported for now!
     2557            assert encoding=="png"
     2558            from PIL import Image
     2559            buf = BytesIOClass(data)
     2560            img = Image.open(buf)
     2561            pixels = img.tobytes('raw', "BGRX")
     2562            from xpra.codecs.image_wrapper import ImageWrapper
     2563            #now convert it to YUV:
     2564            bgrx_image = ImageWrapper(0, 0, w, h, pixels, "BGRX", 24, w*4, planes=ImageWrapper.PACKED)
     2565            from xpra.codecs.csc_cython.colorspace_converter import ColorspaceConverter        #@UnresolvedImport
     2566            csc = ColorspaceConverter()
     2567            csc.init_context(w, h, "BGRX", w, h, "YUV420P")
     2568            image = csc.convert_image(bgrx_image)
     2569            vfd.push_image(image)
     2570            #tell the client all is good:
     2571            ss.send_webcam_ack(device, frame_no)
     2572        except Exception as e:
     2573            webcamlog.error("Error processing webcam frame:")
     2574            webcamlog.error(" %s", e)
     2575            ss.send_webcam_stop(device, str(e))
     2576            self.stop_virtual_webcam()
     2577
     2578
    24532579    def process_clipboard_packet(self, ss, packet):
    24542580        if not ss:
    24552581            #protocol has been dropped!
  • xpra/server/source.py

     
    15801580        self.send("set_deflate", level)
    15811581
    15821582
     1583    def send_webcam_ack(self, device, frame=0):
     1584        self.send("webcam-ack", device, frame)
     1585
     1586    def send_webcam_stop(self, device, message):
     1587        self.send("webcam-stop", device, message)
     1588
     1589
    15831590    def set_printers(self, printers, password_file, encryption, encryption_keyfile):
    15841591        printlog("set_printers(%s, %s, %s, %s) for %s", printers, password_file, encryption, encryption_keyfile, self)
    15851592        if self.machine_id==get_machine_id() and not ADD_LOCAL_PRINTERS: