Преглед изворни кода

Video4Linux2 support for WebcamVideo and a few minor improvements to the webcam code.

rdb пре 15 година
родитељ
комит
00ee003d1e

+ 10 - 6
panda/src/vision/Sources.pp

@@ -9,26 +9,30 @@
   #define TARGET p3vision
   #define LOCAL_LIBS \
     display text pgraph gobj linmath putil audio movies
-    
+
   #define COMBINED_SOURCES vision_composite1.cxx
 
   #define SOURCES \
     arToolKit.I arToolKit.h \
     config_vision.h \
     openCVTexture.I openCVTexture.h \
-    webcamVideo.h webcamVideo.I
-    
+    webcamVideo.h webcamVideo.I \
+    webcamVideoCursorV4L.h webcamVideoV4L.h
+
   #define INCLUDED_SOURCES \
     arToolKit.cxx \
     config_vision.cxx \
     openCVTexture.cxx \
     webcamVideo.cxx \
-    webcamVideoDS.cxx
-    
+    webcamVideoDS.cxx \
+    webcamVideoCursorV4L.cxx \
+    webcamVideoV4L.cxx
+
   #define INSTALL_HEADERS \
     arToolKit.I arToolKit.h \
     openCVTexture.I openCVTexture.h \
-    webcamVideo.h webcamVideo.I
+    webcamVideo.h webcamVideo.I \
+    webcamVideoCursorV4L.h webcamVideoV4L.h
 
   #define IGATESCAN all
 

+ 4 - 0
panda/src/vision/config_vision.cxx

@@ -15,6 +15,8 @@
 #include "config_vision.h"
 #include "openCVTexture.h"
 #include "webcamVideo.h"
+#include "webcamVideoCursorV4L.h"
+#include "webcamVideoV4L.h"
 
 #include "pandaSystem.h"
 #include "texturePool.h"
@@ -44,6 +46,8 @@ init_libvision() {
   initialized = true;
 
   WebcamVideo::init_type();
+  WebcamVideoCursorV4L::init_type();
+  WebcamVideoV4L::init_type();
 
 #ifdef HAVE_OPENCV
   OpenCVTexture::init_type();

+ 2 - 0
panda/src/vision/vision_composite1.cxx

@@ -2,5 +2,7 @@
 #include "config_vision.cxx"
 #include "openCVTexture.cxx"
 #include "webcamVideo.cxx"
+#include "webcamVideoCursorV4L.cxx"
 #include "webcamVideoDS.cxx"
+#include "webcamVideoV4L.cxx"
 

+ 18 - 1
panda/src/vision/webcamVideo.I

@@ -37,10 +37,27 @@ get_size_y() const {
 //     Function: WebcamVideo::get_fps
 //       Access: Published
 //  Description: Returns the camera's framerate.  This
-//               is a maximum theoretical: the actual performance 
+//               is a maximum theoretical: the actual performance
 //               will depend on the speed of the hardware.
 ////////////////////////////////////////////////////////////////////
 INLINE int WebcamVideo::
 get_fps() const {
   return _fps;
 }
+
+////////////////////////////////////////////////////////////////////
+//     Function: WebcamVideo::output
+//       Access: Public
+//  Description: Outputs the WebcamVideo.  This function simply
+//               writes the name, size and FPS to the output stream.
+////////////////////////////////////////////////////////////////////
+INLINE void WebcamVideo::
+output(ostream &out) const {
+  out << get_name() << ": " << get_size_x() << "x" << get_size_y() << " @ " << get_fps() << "Hz";
+}
+
+INLINE ostream &operator << (ostream &out, const WebcamVideo &n) {
+  n.output(out);
+  return out;
+}
+

+ 5 - 5
panda/src/vision/webcamVideo.cxx

@@ -23,7 +23,7 @@ TypeHandle WebcamVideo::_type_handle;
 ////////////////////////////////////////////////////////////////////
 //     Function: WebcamVideo::Destructor
 //       Access: Published, Virtual
-//  Description: 
+//  Description:
 ////////////////////////////////////////////////////////////////////
 WebcamVideo::
 ~WebcamVideo() {
@@ -55,7 +55,7 @@ find_all_webcams() {
   find_all_webcams_ds();
 #endif
 
-#ifdef HAVE_VIDEO4LINUX
+#ifdef IS_LINUX
   extern void find_all_webcams_v4l();
   find_all_webcams_v4l();
 #endif
@@ -63,11 +63,11 @@ find_all_webcams() {
 
 ////////////////////////////////////////////////////////////////////
 //     Function: WebcamVideo::get_num_options
-//       Access: Public
+//       Access: Public, Static
 //  Description: Returns the number of webcam options.  An "option"
 //               consists of a device plus a set of configuration
 //               parameters.  For example, "Creative Webcam Live at
-//               640x480, 30 fps" is an option.  
+//               640x480, 30 fps" is an option.
 ////////////////////////////////////////////////////////////////////
 int WebcamVideo::
 get_num_options() {
@@ -77,7 +77,7 @@ get_num_options() {
 
 ////////////////////////////////////////////////////////////////////
 //     Function: WebcamVideo::get_option
-//       Access: Public
+//       Access: Public, Static
 //  Description: Returns the nth webcam option.
 ////////////////////////////////////////////////////////////////////
 PT(WebcamVideo) WebcamVideo::

+ 6 - 2
panda/src/vision/webcamVideo.h

@@ -30,13 +30,15 @@ PUBLISHED:
   static int             get_num_options();
   static PT(WebcamVideo) get_option(int n);
   MAKE_SEQ(get_options, get_num_options, get_option);
-  
+
   INLINE int get_size_x() const;
   INLINE int get_size_y() const;
   INLINE int get_fps() const;
-  
+
   virtual PT(MovieVideoCursor) open() = 0;
 
+  INLINE void output(ostream &out) const;
+
 public:
   static void find_all_webcams();
 
@@ -65,6 +67,8 @@ private:
   static TypeHandle _type_handle;
 };
 
+INLINE ostream &operator << (ostream &out, const WebcamVideo &n);
+
 #include "webcamVideo.I"
 
 #endif

+ 413 - 0
panda/src/vision/webcamVideoCursorV4L.cxx

@@ -0,0 +1,413 @@
+// Filename: webcamVideoCursorV4L.cxx
+// Created by: rdb (11Jun2010)
+//
+////////////////////////////////////////////////////////////////////
+//
+// PANDA 3D SOFTWARE
+// Copyright (c) Carnegie Mellon University.  All rights reserved.
+//
+// All use of this software is subject to the terms of the revised BSD
+// license.  You should have received a copy of this license along
+// with this source code in a file named "LICENSE."
+//
+////////////////////////////////////////////////////////////////////
+
+#ifdef IS_LINUX
+
+#include "webcamVideoV4L.h"
+
+#include <sys/mman.h>
+#include <linux/videodev.h>
+#include <linux/videodev2.h>
+
+#ifdef HAVE_JPEG
+extern "C" {
+  #include <jpeglib.h>
+  #include <jpegint.h>
+  #include <jerror.h>
+}
+
+#include <setjmp.h>
+#endif
+
+TypeHandle WebcamVideoCursorV4L::_type_handle;
+
+#define clamp(x) min(max(x, 0.0), 255.0)
+
+INLINE static void yuv_to_rgb(unsigned char *dest, const unsigned char *src) {
+  double y1 = (255 / 219.0) * (src[0] - 16);
+  double pb = (255 / 224.0) * (src[1] - 128);
+  double pr = (255 / 224.0) * (src[2] - 128);
+  dest[2] = clamp(1.0 * y1 + 0     * pb + 1.402 * pr);
+  dest[1] = clamp(1.0 * y1 - 0.344 * pb - 0.714 * pr);
+  dest[0] = clamp(1.0 * y1 + 1.772 * pb + 0     * pr);
+}
+
+INLINE static void yuyv_to_rgbrgb(unsigned char *dest, const unsigned char *src) {
+  unsigned char yuv[] = {src[0], src[1], src[3]};
+  yuv_to_rgb(dest, yuv);
+  yuv[0] = src[2];
+  yuv_to_rgb(dest + 3, yuv);
+}
+
+INLINE static void yuyv_to_rgbargba(unsigned char *dest, const unsigned char *src) {
+  unsigned char yuv[] = {src[0], src[1], src[3]};
+  yuv_to_rgb(dest, yuv);
+  yuv[0] = src[2];
+  yuv_to_rgb(dest + 4, yuv);
+  dest[3] = (unsigned char) -1;
+  dest[7] = (unsigned char) -1;
+}
+
+#if defined(HAVE_JPEG) && !defined(CPPPARSER)
+
+struct my_error_mgr {
+  struct jpeg_error_mgr pub;
+  jmp_buf setjmp_buffer;
+};
+
+typedef struct my_error_mgr *my_error_ptr;
+
+static void my_error_exit (j_common_ptr cinfo) {
+  // Output the error message
+  char buffer[JMSG_LENGTH_MAX];
+  (*cinfo->err->format_message) (cinfo, buffer);
+  vision_cat.error() << buffer << "\n";
+  // cinfo->err really points to a my_error_mgr struct, so coerce pointer
+  my_error_ptr myerr = (my_error_ptr) cinfo->err;
+  // Return control to the setjmp point
+  longjmp(myerr->setjmp_buffer, 1);
+}
+
+static void my_output_message (j_common_ptr cinfo){
+  char buffer[JMSG_LENGTH_MAX];
+  (*cinfo->err->format_message) (cinfo, buffer);
+  vision_cat.warning() << buffer << "\n";
+}
+
+static void my_init_source(j_decompress_ptr cinfo) {
+}
+
+static boolean my_fill_input_buffer(j_decompress_ptr cinfo) {
+  struct jpeg_source_mgr *src = cinfo->src;
+  static JOCTET FakeEOI[] = {0xFF, JPEG_EOI};
+
+  WARNMS(cinfo, JWRN_JPEG_EOF);
+
+  src->next_input_byte = FakeEOI;
+  src->bytes_in_buffer = 2;
+
+  return TRUE;
+}
+
+static void my_skip_input_data(j_decompress_ptr cinfo, long num_bytes) {
+  struct jpeg_source_mgr *src = cinfo->src;
+
+  if (num_bytes >= (long) src->bytes_in_buffer) {
+    my_fill_input_buffer(cinfo);
+    return;
+  }
+
+  src->bytes_in_buffer -= num_bytes;
+  src->next_input_byte += num_bytes;
+}
+
+static void my_term_source(j_decompress_ptr cinfo) {
+}
+
+#endif
+
+////////////////////////////////////////////////////////////////////
+//     Function: WebcamVideoCursorV4L::Constructor
+//       Access: Published
+//  Description:
+////////////////////////////////////////////////////////////////////
+WebcamVideoCursorV4L::
+WebcamVideoCursorV4L(WebcamVideoV4L *src) : MovieVideoCursor(src) {
+  _size_x = src->_size_x;
+  _size_y = src->_size_y;
+  _num_components = 3;
+  _length = 1.0E10;
+  _can_seek = false;
+  _can_seek_fast = false;
+  _aborted = false;
+  _last_start = -1.0;
+  _next_start = 0.0;
+  _streaming = true;
+  _ready = false;
+  _format = (struct v4l2_format *) malloc(sizeof(struct v4l2_format));
+  memset(_format, 0, sizeof(struct v4l2_format));
+  _cinfo = NULL;
+  _buffers = NULL;
+  _buflens = NULL;
+  _fd = open(src->_device.c_str(), O_RDWR);
+  if (-1 == _fd) {
+    vision_cat.error() << "Failed to open " << src->_device.c_str() << "\n";
+    return;
+  }
+
+  _format->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+  pvector<uint32_t>::iterator it;
+  for (it = src->_pformats.begin(); it != src->_pformats.end(); ++it) {
+#ifdef HAVE_JPEG
+    if (*it == V4L2_PIX_FMT_MJPEG) {
+      _format->fmt.pix.pixelformat = *it;
+      break;
+    } else
+#endif
+    if (*it == V4L2_PIX_FMT_YUYV) {
+      _format->fmt.pix.pixelformat = *it;
+      break;
+    }
+  }
+  if (it == src->_pformats.end()) {
+    vision_cat.error() << "Failed to find a suitable pixel format!\n";
+    _ready = false;
+    close(_fd);
+    _fd = -1;
+    return;
+  }
+
+  // No interlacing
+  _format->fmt.pix.field = V4L2_FIELD_NONE;
+
+  // This really should be there, but for
+  // some reason it causes weird memory corruption.
+
+  _format->fmt.pix.width = _size_x;
+  _format->fmt.pix.height = _size_y;
+
+  // Now politely ask the driver to switch to this format
+  if (-1 == ioctl(_fd, VIDIOC_S_FMT, _format)) {
+    vision_cat.error() << "Driver rejected format!\n";
+    _ready = false;
+    close(_fd);
+    _fd = -1;
+    return;
+  }
+
+  _size_x = _format->fmt.pix.width;
+  _size_y = _format->fmt.pix.height;
+
+  struct v4l2_requestbuffers req;
+  memset(&req, 0, sizeof req);
+  req.count = 4;
+  req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+  req.memory = V4L2_MEMORY_MMAP;
+
+  if (-1 == ioctl (_fd, VIDIOC_REQBUFS, &req)) {
+    vision_cat.error() << "Failed to request buffers from webcam!\n";
+  }
+
+  if (req.count < 2) {
+    vision_cat.error() << "Insufficient buffer memory!\n";
+  }
+
+  _bufcount = req.count;
+  _buffers = (void* *) calloc (req.count, sizeof (void*));
+  _buflens = (size_t*) calloc (req.count, sizeof (size_t));
+
+  if (!_buffers || !_buflens) {
+    vision_cat.error() << "Not enough memory!\n";
+  }
+
+  struct v4l2_buffer buf;
+  for (int i = 0; i < _bufcount; ++i) {
+    memset(&buf, 0, sizeof buf);
+    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    buf.memory = V4L2_MEMORY_MMAP;
+    buf.index = i;
+
+    if (-1 == ioctl(_fd, VIDIOC_QUERYBUF, &buf)) {
+      vision_cat.error() << "Failed to query buffer!\n";
+    }
+
+    _buflens[i] = buf.length;
+    _buffers[i] = mmap (NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, _fd, buf.m.offset);
+
+    if (_buffers[i] == MAP_FAILED) {
+      vision_cat.error() << "Failed to map buffer!\n";
+    }
+
+    if (-1 == ioctl(_fd, VIDIOC_QBUF, &buf)) {
+      vision_cat.error() << "Failed to exchange buffer with driver!\n";
+    }
+  }
+
+  enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+  if (-1 == ioctl(_fd, VIDIOC_STREAMON, &type)) {
+    vision_cat.error() << "Failed to stream from buffer!\n";
+  }
+
+#ifdef HAVE_JPEG
+  // Initialize the JPEG library, if necessary
+  if (_format->fmt.pix.pixelformat == V4L2_PIX_FMT_MJPEG) {
+    _cinfo = (struct jpeg_decompress_struct *) malloc(sizeof(struct jpeg_decompress_struct));
+    jpeg_create_decompress(_cinfo);
+
+    _cinfo->src = (struct jpeg_source_mgr *)
+      (*_cinfo->mem->alloc_small) ((j_common_ptr) _cinfo, JPOOL_PERMANENT,
+                                          sizeof(struct jpeg_source_mgr));
+    // Set up function pointers
+    _cinfo->src->init_source = my_init_source;
+    _cinfo->src->fill_input_buffer = my_fill_input_buffer;
+    _cinfo->src->skip_input_data = my_skip_input_data;
+    _cinfo->src->resync_to_restart = jpeg_resync_to_restart;
+    _cinfo->src->term_source = my_term_source;
+  }
+#endif
+  _ready = true;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: WebcamVideoCursorV4L::Destructor
+//       Access: Published, Virtual
+//  Description:
+////////////////////////////////////////////////////////////////////
+WebcamVideoCursorV4L::
+~WebcamVideoCursorV4L() {
+#ifdef HAVE_JPEG
+  if (_cinfo != NULL) {
+    jpeg_destroy_decompress(_cinfo);
+    free(_cinfo);
+  }
+#endif
+  if (_format != NULL) {
+    free(_format);
+  }
+  if (-1 != _fd) {
+    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    ioctl(_fd, VIDIOC_STREAMOFF, &type);
+    close(_fd);
+  }
+  if (_buffers) {
+    for (int i = 0; i < _bufcount; ++i) {
+      munmap(_buffers[i], _buflens[i]);
+    }
+    free(_buffers);
+  }
+  if (_buflens) {
+    free(_buflens);
+  }
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: WebcamVideoCursorV4L::fetch_into_buffer
+//       Access: Published, Virtual
+//  Description:
+////////////////////////////////////////////////////////////////////
+void WebcamVideoCursorV4L::
+fetch_into_buffer(double time, unsigned char *block, bool bgra) {
+  if (!_ready) {
+    return;
+  }
+
+  struct v4l2_buffer vbuf;
+  memset(&vbuf, 0, sizeof vbuf);
+  vbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+  vbuf.memory = V4L2_MEMORY_MMAP;
+  if (-1 == ioctl(_fd, VIDIOC_DQBUF, &vbuf) && errno != EIO) {
+    vision_cat.error() << "Failed to dequeue buffer!\n";
+    return;
+  }
+  nassertv(vbuf.index < _bufcount);
+  size_t bufsize = _buflens[vbuf.index];
+  size_t old_bpl = _format->fmt.pix.bytesperline;
+  size_t new_bpl = bgra ? _size_x * 4 : _size_x * 3;
+  unsigned char *buf = (unsigned char *) _buffers[vbuf.index];
+
+  if (_format->fmt.pix.pixelformat == V4L2_PIX_FMT_MJPEG) {
+#ifdef HAVE_JPEG
+    nassertv(!bgra);
+    struct my_error_mgr jerr;
+    _cinfo->err = jpeg_std_error(&jerr.pub);
+    jerr.pub.error_exit = my_error_exit;
+    jerr.pub.output_message = my_output_message;
+
+    unsigned char *newbuf = (unsigned char*) malloc(new_bpl * _size_y);
+
+    // Establish the setjmp return context for my_error_exit to use
+    if (setjmp(jerr.setjmp_buffer)) {
+      if (_cinfo->global_state > DSTATE_READY) {
+        jpeg_abort_decompress(_cinfo);
+      }
+    } else {
+      // Set up data pointer
+      _cinfo->src->bytes_in_buffer = bufsize;
+      _cinfo->src->next_input_byte = buf;
+
+      if (jpeg_read_header(_cinfo, TRUE) == JPEG_HEADER_OK) {
+        _cinfo->scale_num = 1;
+        _cinfo->scale_denom = 1;
+        _cinfo->out_color_space = JCS_RGB;
+
+        if (jpeg_start_decompress(_cinfo) && _cinfo->output_components == 3
+          && _size_x == _cinfo->output_width && _size_y == _cinfo->output_height) {
+
+          JSAMPLE *buffer_end = newbuf + new_bpl * _cinfo->output_height;
+          JSAMPLE *rowptr = newbuf;
+          while (_cinfo->output_scanline < _cinfo->output_height) {
+            nassertd(rowptr + new_bpl <= buffer_end) break;
+            jpeg_read_scanlines(_cinfo, &rowptr, 1);
+            rowptr += new_bpl;
+          }
+
+          if (_cinfo->output_scanline < _cinfo->output_height) {
+            jpeg_abort_decompress(_cinfo);
+          } else {
+            jpeg_finish_decompress(_cinfo);
+          }
+        }
+      }
+    }
+
+    // Flip the image vertically
+    for (size_t row = 0; row < _size_y; ++row) {
+      memcpy(block + (_size_y - row - 1) * new_bpl, newbuf + row * new_bpl, new_bpl);
+    }
+    free(newbuf);
+
+    // Swap red / blue
+    unsigned char ex;
+    if (bgra) {
+      for (size_t i = 0; i < new_bpl * _size_y; i += 4) {
+        ex = block[i];
+        block[i] = block[i + 2];
+        block[i + 2] = ex;
+      }
+    } else {
+      for (size_t i = 0; i < new_bpl * _size_y; i += 3) {
+        ex = block[i];
+        block[i] = block[i + 2];
+        block[i + 2] = ex;
+      }
+    }
+#else
+    nassertv(false); // Not compiled with JPEG support
+#endif
+  } else {
+    if (bgra) {
+      for (size_t row = 0; row < _size_y; ++row) {
+        size_t c = 0;
+        for (size_t i = 0; i < old_bpl; i += 4) {
+          yuyv_to_rgbargba(block + (_size_y - row - 1) * new_bpl + c, buf + row * old_bpl + i);
+          c += 8;
+        }
+      }
+    } else {
+      for (size_t row = 0; row < _size_y; ++row) {
+        size_t c = 0;
+        for (size_t i = 0; i < old_bpl; i += 4) {
+          yuyv_to_rgbrgb(block + (_size_y - row - 1) * new_bpl + c, buf + row * old_bpl + i);
+          c += 6;
+        }
+      }
+    }
+  }
+
+  if (-1 == ioctl(_fd, VIDIOC_QBUF, &vbuf)) {
+    vision_cat.error() << "Failed to exchange buffer with driver!\n";
+  }
+}
+
+#endif

+ 69 - 0
panda/src/vision/webcamVideoCursorV4L.h

@@ -0,0 +1,69 @@
+// Filename: webcamVideoCursorV4L.h
+// Created by: rdb (11Jun2010)
+//
+////////////////////////////////////////////////////////////////////
+//
+// PANDA 3D SOFTWARE
+// Copyright (c) Carnegie Mellon University.  All rights reserved.
+//
+// All use of this software is subject to the terms of the revised BSD
+// license.  You should have received a copy of this license along
+// with this source code in a file named "LICENSE."
+//
+////////////////////////////////////////////////////////////////////
+
+#ifndef WEBCAMVIDEOCURSORV4L_H
+#define WEBCAMVIDEOCURSORV4L_H
+
+#ifdef IS_LINUX
+
+#include "webcamVideo.h"
+
+struct v4l2_format;
+#ifdef HAVE_JPEG
+struct jpeg_decompress_struct;
+#endif
+
+class WebcamVideoV4L;
+
+////////////////////////////////////////////////////////////////////
+//       Class : WebcamVideoCursorV4L
+// Description : The Video4Linux implementation of webcams.
+////////////////////////////////////////////////////////////////////
+class WebcamVideoCursorV4L : public MovieVideoCursor {
+public:
+  WebcamVideoCursorV4L(WebcamVideoV4L *src);
+  virtual ~WebcamVideoCursorV4L();
+  virtual void fetch_into_buffer(double time, unsigned char *block, bool rgba);
+
+private:
+  int _fd;
+  void **_buffers;
+  size_t *_buflens;
+  size_t _bufcount;
+  struct v4l2_format *_format;
+#ifdef HAVE_JPEG
+  struct jpeg_decompress_struct *_cinfo;
+#endif
+
+public:
+  static TypeHandle get_class_type() {
+    return _type_handle;
+  }
+  static void init_type() {
+    MovieVideoCursor::init_type();
+    register_type(_type_handle, "WebcamVideoCursorV4L",
+                  MovieVideoCursor::get_class_type());
+  }
+  virtual TypeHandle get_type() const {
+    return get_class_type();
+  }
+  virtual TypeHandle force_init_type() {init_type(); return get_class_type();}
+
+private:
+  static TypeHandle _type_handle;
+};
+
+#endif // IS_LINUX
+
+#endif

+ 60 - 62
panda/src/vision/webcamVideoDS.cxx

@@ -23,7 +23,7 @@
 // This code was created by studying and adapting the VDOGRAB
 // library by Shu-Kai Yang and the videoInput library by Theodore
 // Watson.  We owe both of them a great deal of thanks for
-// figuring all this out.  Both of their libraries have 
+// figuring all this out.  Both of their libraries have
 // informal licenses (the "do whatever you want and don't blame
 // me" sort), so I think there's not a problem using their code.
 //
@@ -31,7 +31,7 @@
 
 #ifdef HAVE_DIRECTCAM
 
-#define WIN32_LEAN_AND_MEAN 
+#define WIN32_LEAN_AND_MEAN
 
 #undef Configure
 
@@ -102,7 +102,7 @@ private:
 
   IMoniker *_moniker;
   AM_MEDIA_TYPE *_media;
-  
+
   friend class WebcamVideoCursorDS;
 
 public:
@@ -141,14 +141,14 @@ public:
 public:
   void cleanup();
 
-  class CSampleGrabberCB : public ISampleGrabberCB 
-  { 
+  class CSampleGrabberCB : public ISampleGrabberCB
+  {
   public:
     WebcamVideoCursorDS *_host;
-    
+
     ULONG __stdcall AddRef() { return 2; }
     ULONG __stdcall Release() { return 1; }
-    
+
     HRESULT __stdcall QueryInterface(REFIID riid, void ** ppv);
     HRESULT __stdcall SampleCB(double SampleTime, IMediaSample *pSample);
     HRESULT __stdcall BufferCB(double dblSampleTime, BYTE *pBuffer, long lBufferSize);
@@ -161,12 +161,12 @@ public:
   ICaptureGraphBuilder2   *_pCaptureBuilder;
   IBaseFilter             *_pSrcFilter;
   IAMStreamConfig         *_pStreamConfig;
-  CComPtr<ISampleGrabber>  _pSampleGrabber; 
+  CComPtr<ISampleGrabber>  _pSampleGrabber;
   IBaseFilter             *_pStreamRenderer;
   IMediaControl           *_pMediaCtrl;
   //  IMemAllocator           *_pAllocator;
   CSampleGrabberCB         _sample_cb;
-  
+
 public:
   static TypeHandle get_class_type() {
     return _type_handle;
@@ -329,30 +329,28 @@ add_device(WebcamVideoList &list, IMoniker *pMoniker, AM_MEDIA_TYPE *media) {
     }
   }
   PT(WebcamVideoDS) wc = new WebcamVideoDS;
-  ostringstream name;
-  name << "DirectShow: " << get_moniker_name(pMoniker) << " @ " << media_x(media) << " x " << media_y(media) << "  FPS:" << media_fps(media);
-  wc->set_name(name.str());
+  wc->set_name(get_moniker_name(pMoniker));
   wc->_size_x = media_x(media);
   wc->_size_y = media_y(media);
   wc->_fps = media_fps(media);
   wc->_moniker = pMoniker;
   wc->_media = media;
   list.push_back(wc);
-  cerr << "Added device: " << wc->get_name() << "\n";
+  cerr << "Added device: DirectShow: " << wc << "\n";
 }
 
 
 ////////////////////////////////////////////////////////////////////
 //     Function: WebcamVideoDS::find_all_webcams_ds
 //       Access: Public, Static
-//  Description: Finds all DirectShow webcams and adds them to 
+//  Description: Finds all DirectShow webcams and adds them to
 //               the global list _all_webcams.
 ////////////////////////////////////////////////////////////////////
 void WebcamVideoDS::
 find_all_webcams_ds() {
 
   pvector <PT(WebcamVideoDS)> list;
-  
+
   ICreateDevEnum *pCreateDevEnum=NULL;
   IEnumMoniker *pEnumMoniker=NULL;
   IGraphBuilder *pGraphBuilder=NULL;
@@ -362,7 +360,7 @@ find_all_webcams_ds() {
   IAMStreamConfig *pStreamConfig=NULL;
   HRESULT hResult;
   ULONG cFetched;
-  
+
   hResult=CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC,
                            IID_IGraphBuilder,(void**)&pGraphBuilder);
   if (hResult != S_OK) goto cleanup;
@@ -377,7 +375,7 @@ find_all_webcams_ds() {
   hResult=pCreateDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory,
                                                 &pEnumMoniker, 0);
   if(hResult != S_OK) goto cleanup;
-  
+
   while(1) {
     if (pMoniker)       { pMoniker->Release();  pMoniker=0; }
     if (pBaseFilter)    { pBaseFilter->Release(); pBaseFilter=0; }
@@ -390,7 +388,7 @@ find_all_webcams_ds() {
     if (hResult != S_OK) continue;
     hResult = pCaptureGraphBuilder2->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, pBaseFilter,
                                                    IID_IAMStreamConfig, (void **)&pStreamConfig);
-    if (hResult != S_OK) continue;  
+    if (hResult != S_OK) continue;
     int iCount, iSize;
     hResult = pStreamConfig->GetNumberOfCapabilities(&iCount, &iSize);
     if (hResult != S_OK || (iSize != sizeof(VIDEO_STREAM_CONFIG_CAPS))) continue;
@@ -405,10 +403,10 @@ find_all_webcams_ds() {
         add_device(list, pMoniker, mtype);
       }
     }
-    
+
     pMoniker = 0;
   }
-  
+
  cleanup:
   if (pCreateDevEnum) { pCreateDevEnum->Release(); pCreateDevEnum=0; }
   if (pEnumMoniker)   { pEnumMoniker->Release();   pEnumMoniker=0; }
@@ -417,7 +415,7 @@ find_all_webcams_ds() {
   if (pMoniker)       { pMoniker->Release();  pMoniker=0; }
   if (pBaseFilter)    { pBaseFilter->Release(); pBaseFilter=0; }
   if (pStreamConfig)  { pStreamConfig->Release(); pStreamConfig=0; }
-  
+
   for (int i=0; i<(int)list.size(); i++) {
     WebcamVideoDS *obj = list[i];
     _all_webcams.push_back(obj);
@@ -443,7 +441,7 @@ open() {
 ////////////////////////////////////////////////////////////////////
 //     Function: WebcamVideoCursorDS::Constructor
 //       Access: Published
-//  Description: 
+//  Description:
 ////////////////////////////////////////////////////////////////////
 WebcamVideoCursorDS::
 WebcamVideoCursorDS(WebcamVideoDS *src) :
@@ -457,39 +455,39 @@ WebcamVideoCursorDS(WebcamVideoDS *src) :
 {
   AM_MEDIA_TYPE mediaType;
   VIDEOINFOHEADER *pVideoInfo;
-  
+
   HRESULT hResult;
-  
+
   hResult=CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC,
                            IID_IGraphBuilder,(void**)&_pGraphBuilder);
   if(hResult != S_OK) { cleanup(); return; }
-  
+
   hResult=CoCreateInstance(CLSID_CaptureGraphBuilder2, NULL, CLSCTX_INPROC,
                            IID_ICaptureGraphBuilder2, (void**)&_pCaptureBuilder);
   if(hResult != S_OK) { cleanup(); return; }
-  
+
   _pCaptureBuilder->SetFiltergraph(_pGraphBuilder);
   cerr << "  IID_IGraphBuilder & IID_ICaptureGraphBuilder2 are established.\n";
-  
+
   hResult=_pGraphBuilder->QueryInterface(IID_IMediaControl, (void **)&_pMediaCtrl);
   if(hResult != S_OK)
     {  cerr << "  Can not get the IID_IMediaControl interface!";
     cleanup(); return;  }
   cerr << "  IID_IMediaControl interface is acquired.\n";
-  
+
   src->_moniker->BindToObject(NULL,NULL,IID_IBaseFilter, (void**)&_pSrcFilter);
   if(_pSrcFilter == NULL)
     {  cerr << "  Such capture device is not found.\n";
     cleanup(); return;  }
   cerr << "  The capture filter is acquired.\n";
-  
+
   hResult=_pGraphBuilder->AddFilter(_pSrcFilter, L"Capture Filter");
   if(hResult != DD_OK)
     {  cerr << "  The capture filter can not be added to the graph.\n";
     cleanup(); return;  }
   cerr << "  The capture filter has been added to the graph.\n";
-  
-  
+
+
   hResult = _pCaptureBuilder->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, _pSrcFilter,
                                             IID_IAMStreamConfig, (void **)&_pStreamConfig);
   if (hResult != S_OK) {
@@ -501,16 +499,16 @@ WebcamVideoCursorDS(WebcamVideoDS *src) :
     cerr << "Could not select desired video resolution\n";
     cleanup(); return;
   }
-  
+
   _pSampleGrabber.CoCreateInstance(CLSID_SampleGrabber);
   if(!_pSampleGrabber)
     {  cerr << "  Can not create the sample grabber, maybe qedit.dll is not registered?";
     cleanup(); return;  }
-  
-  
+
+
   CComQIPtr< IBaseFilter, &IID_IBaseFilter > pGrabberFilter(_pSampleGrabber);
   cerr << "  IID_IBaseFilter of CLSID_SampleGrabber is acquired.\n";
-  
+
   ZeroMemory(&mediaType, sizeof(AM_MEDIA_TYPE));
   mediaType.majortype=MEDIATYPE_Video;
   mediaType.subtype=MEDIASUBTYPE_RGB24;
@@ -519,14 +517,14 @@ WebcamVideoCursorDS(WebcamVideoDS *src) :
     {  cerr << "  Fail to set the media type!";
     cleanup(); return;  }
   cerr << "  The media type of the sample grabber is set 24-bit RGB.\n";
-  
+
   hResult=_pGraphBuilder->AddFilter(pGrabberFilter, L"Sample Grabber");
   if(hResult != S_OK)
     {  cerr << "  Fail to add the sample grabber to the graph.";
     cleanup(); return;  }
   cerr << "  The sample grabber has been added to the graph.\n";
 
-  //used to give the video stream somewhere to go to.  
+  //used to give the video stream somewhere to go to.
   hResult = CoCreateInstance(CLSID_NullRenderer, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (void**)&_pStreamRenderer);
   if(hResult != S_OK)
     {  cerr << "  Can not create the null renderer.";
@@ -538,20 +536,20 @@ WebcamVideoCursorDS(WebcamVideoDS *src) :
     {  cerr << "  Fail to add the Null Renderer to the graph.";
     cleanup(); return;  }
   cerr << "  The Null Renderer has been added to the graph.\n";
-  
+
   hResult=_pCaptureBuilder->RenderStream(&PIN_CATEGORY_CAPTURE,
                                          &MEDIATYPE_Video, _pSrcFilter, pGrabberFilter, _pStreamRenderer);
   if(hResult != S_OK) {
     cerr << "  ICaptureGraphBuilder2::RenderStream() can not connect the pins\n";
     cleanup(); return;
   }
-  
+
   hResult=_pSampleGrabber->GetConnectedMediaType(&mediaType);
   if(hResult != S_OK) {
     cerr << "  Failed to read the connected media type.";
-    cleanup(); return; 
+    cleanup(); return;
   }
-  
+
   //  IPin *iPin;
   //  hResult = FindInputPin(pGrabberFilter, &iPin);
   //  if ((iPin == 0)||(hResult != S_OK)) {
@@ -579,12 +577,12 @@ WebcamVideoCursorDS(WebcamVideoDS *src) :
   //    cerr << "Could not set allocator properties.\n";
   //  }
   //  cerr << "Allocator properties (adjusted): cBuffers=" << aprops.cBuffers << "\n";
-  
+
   pVideoInfo=(VIDEOINFOHEADER*)mediaType.pbFormat;
   _size_x = pVideoInfo->bmiHeader.biWidth;
   _size_y = pVideoInfo->bmiHeader.biHeight;
   cerr << "Connected media type " << _size_x << " x " << _size_y << "\n";
-  
+
   _sample_cb._host = this;
   _num_components = 3;
   _length = 1.0E10;
@@ -602,28 +600,28 @@ WebcamVideoCursorDS(WebcamVideoDS *src) :
     mediaType.cbFormat=0;
     mediaType.pbFormat=NULL;
   }
-  
+
   if(mediaType.pUnk != NULL) {
     mediaType.pUnk->Release();
     mediaType.pUnk=NULL;
   }
-  
+
   _pSampleGrabber->SetBufferSamples(FALSE);
   _pSampleGrabber->SetOneShot(FALSE);
-  
+
   hResult=_pSampleGrabber->SetCallback(&_sample_cb, 0);
   if(hResult != S_OK) {
     cerr << "  Can not set the callback interface!";
     cleanup(); return;
   }
-  
+
   _pMediaCtrl->Run();
 }
 
 ////////////////////////////////////////////////////////////////////
 //     Function: WebcamVideoCursorDS::cleanup
 //       Access: Published
-//  Description: 
+//  Description:
 ////////////////////////////////////////////////////////////////////
 void WebcamVideoCursorDS::
 cleanup() {
@@ -635,7 +633,7 @@ cleanup() {
   if (_pMediaCtrl) {
     _pMediaCtrl->Stop();
   }
-  
+
   if(_pMediaCtrl)       {  _pMediaCtrl->Release();  _pMediaCtrl=NULL;  }
   if(_pCaptureBuilder)  {  _pCaptureBuilder->Release();  _pCaptureBuilder=NULL;  }
   if(_pGraphBuilder)    {  _pGraphBuilder->Release();  _pGraphBuilder=NULL;  }
@@ -648,7 +646,7 @@ cleanup() {
 ////////////////////////////////////////////////////////////////////
 //     Function: WebcamVideoCursorDS::Destructor
 //       Access: Published
-//  Description: 
+//  Description:
 ////////////////////////////////////////////////////////////////////
 WebcamVideoCursorDS::
 ~WebcamVideoCursorDS() {
@@ -658,16 +656,16 @@ WebcamVideoCursorDS::
 ////////////////////////////////////////////////////////////////////
 //     Function: WebcamVideoCursorDS::fetch_into_buffer
 //       Access: Published
-//  Description: 
+//  Description:
 ////////////////////////////////////////////////////////////////////
 void WebcamVideoCursorDS::
 fetch_into_buffer(double time, unsigned char *block, bool bgra) {
-  
+
   if (!_ready) {
     return;
   }
 
-#ifdef LOCKING_MODE  
+#ifdef LOCKING_MODE
   unsigned char *ptr;
   int pixels = _size_x * _size_y;
   HRESULT res = _saved->GetPointer(&ptr);
@@ -700,7 +698,7 @@ fetch_into_buffer(double time, unsigned char *block, bool bgra) {
     memcpy(block, _buffer, pixels * 3);
   }
 #endif
-  
+
   _ready = false;
 }
 
@@ -708,14 +706,14 @@ fetch_into_buffer(double time, unsigned char *block, bool bgra) {
 ////////////////////////////////////////////////////////////////////
 //     Function: WebcamVideoCursorDS::CSampleGrabberCB::QueryInterface
 //       Access: Private
-//  Description: 
+//  Description:
 ////////////////////////////////////////////////////////////////////
 HRESULT __stdcall WebcamVideoCursorDS::CSampleGrabberCB::QueryInterface(REFIID riid, void **ppv)
 {
   if((riid == IID_ISampleGrabberCB) || (riid == IID_IUnknown)) {
     *ppv=(void *)static_cast<ISampleGrabberCB *> (this);
     return NOERROR;
-  } 
+  }
   return E_NOINTERFACE;
 }
 
@@ -723,7 +721,7 @@ HRESULT __stdcall WebcamVideoCursorDS::CSampleGrabberCB::QueryInterface(REFIID r
 ////////////////////////////////////////////////////////////////////
 //     Function: WebcamVideoCursorDS::CSampleGrabberCB::SampleCB
 //       Access: Private
-//  Description: 
+//  Description:
 ////////////////////////////////////////////////////////////////////
 HRESULT __stdcall WebcamVideoCursorDS::CSampleGrabberCB::SampleCB(double SampleTime, IMediaSample *pSample)
 {
@@ -731,7 +729,7 @@ HRESULT __stdcall WebcamVideoCursorDS::CSampleGrabberCB::SampleCB(double SampleT
     return 0;
   }
 
-#ifdef LOCKING_MODE  
+#ifdef LOCKING_MODE
   pSample->AddRef();
   _host->_saved = pSample;
 #else
@@ -753,10 +751,10 @@ HRESULT __stdcall WebcamVideoCursorDS::CSampleGrabberCB::SampleCB(double SampleT
 ////////////////////////////////////////////////////////////////////
 //     Function: WebcamVideoCursorDS::CSampleGrabberCB::BufferCB
 //       Access: Private
-//  Description: 
+//  Description:
 ////////////////////////////////////////////////////////////////////
 HRESULT __stdcall WebcamVideoCursorDS::CSampleGrabberCB::BufferCB(double dblSampleTime, BYTE *pBuffer, long lBufferSize)
-{ 
+{
   // Not used.
   return 0;
 }
@@ -765,7 +763,7 @@ HRESULT __stdcall WebcamVideoCursorDS::CSampleGrabberCB::BufferCB(double dblSamp
 //{
 //  if (!pFilter || ! ppPin)
 //    return E_POINTER;
-//  
+//
 //  *ppPin = 0;
 //  HRESULT hr;
 //  //Find the output pin of the Source Filter
@@ -773,7 +771,7 @@ HRESULT __stdcall WebcamVideoCursorDS::CSampleGrabberCB::BufferCB(double dblSamp
 //  hr = pFilter->EnumPins(&pPinEnum);
 //  if (FAILED(hr))
 //    return E_FAIL;
-//  
+//
 //  IPin *pSearchPin;
 //  while (pPinEnum->Next(1, &pSearchPin, NULL) == S_OK)
 //    {

+ 135 - 0
panda/src/vision/webcamVideoV4L.cxx

@@ -0,0 +1,135 @@
+// Filename: webcamVideoV4L.cxx
+// Created by: rdb (11Jun2010)
+//
+////////////////////////////////////////////////////////////////////
+//
+// PANDA 3D SOFTWARE
+// Copyright (c) Carnegie Mellon University.  All rights reserved.
+//
+// All use of this software is subject to the terms of the revised BSD
+// license.  You should have received a copy of this license along
+// with this source code in a file named "LICENSE."
+//
+////////////////////////////////////////////////////////////////////
+
+#ifdef IS_LINUX
+
+#include "webcamVideoV4L.h"
+#include "webcamVideoCursorV4L.h"
+
+#include <linux/videodev.h>
+#include <linux/videodev2.h>
+
+TypeHandle WebcamVideoV4L::_type_handle;
+
+////////////////////////////////////////////////////////////////////
+//     Function: find_all_webcams_v4l
+//       Access: Public, Static
+//  Description: Finds all Video4Linux webcams and adds them to
+//               the global list _all_webcams.
+////////////////////////////////////////////////////////////////////
+void find_all_webcams_v4l() {
+  struct video_capability cap;
+  struct v4l2_capability cap2;
+
+  vector_string devs;
+  GlobPattern pattern ("/dev/video*");
+  pattern.match_files(devs);
+  for (vector_string::iterator it = devs.begin(); it != devs.end(); ++it) {
+    int fd = open(it->c_str(), O_RDWR);
+    if (fd != -1) {
+      // Check for Video4Linux2 capabilities
+      if (ioctl(fd, VIDIOC_QUERYCAP, &cap2) != -1) {
+        if ((cap2.capabilities & V4L2_CAP_VIDEO_CAPTURE) &&
+            (cap2.capabilities & V4L2_CAP_STREAMING)) {
+          struct v4l2_fmtdesc fmt;
+          for (int i = 0;; i++) {
+            memset(&fmt, 0, sizeof fmt);
+            fmt.index = i;
+            fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+            if (ioctl(fd, VIDIOC_ENUM_FMT, &fmt) == -1) {
+              break;
+            }
+            struct v4l2_frmsizeenum frmsizeenum;
+            for (int j = 0;; j++) {
+              memset(&frmsizeenum, 0, sizeof frmsizeenum);
+              frmsizeenum.index = j;
+              frmsizeenum.pixel_format = fmt.pixelformat;
+              if (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsizeenum) == -1) {
+                break;
+              }
+              if (frmsizeenum.type != V4L2_FRMSIZE_TYPE_DISCRETE) {
+                continue;
+              }
+              struct v4l2_frmivalenum frmivalenum;
+              for (int k = 0;; k++) {
+                memset(&frmivalenum, 0, sizeof frmivalenum);
+                frmivalenum.index = k;
+                frmivalenum.pixel_format = fmt.pixelformat;
+                frmivalenum.width = frmsizeenum.discrete.width;
+                frmivalenum.height = frmsizeenum.discrete.height;
+                if (ioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmivalenum) == -1) {
+                  break;
+                }
+                if (frmivalenum.type != V4L2_FRMIVAL_TYPE_DISCRETE) {
+                  continue;
+                }
+
+                // Create a new webcam video object
+                PT(WebcamVideoV4L) wc = new WebcamVideoV4L;
+                wc->set_name((const char*) cap2.card);
+                wc->_device = *it;
+                wc->_size_x = frmsizeenum.discrete.width;
+                wc->_size_y = frmsizeenum.discrete.height;
+                wc->_fps = ((double) frmivalenum.discrete.denominator) / ((double) frmivalenum.discrete.numerator);
+                wc->_pformats.push_back(fmt.pixelformat);
+
+                // Iterate through the webcams to make sure we don't put any duplicates in there
+                pvector<PT(WebcamVideo)>::iterator wvi;
+                for (wvi = WebcamVideoV4L::_all_webcams.begin(); wvi != WebcamVideoV4L::_all_webcams.end(); ++wvi) {
+                  if ((*wvi)->is_of_type(WebcamVideoV4L::get_class_type())) {
+                    PT(WebcamVideoV4L) wv_v4l = DCAST(WebcamVideoV4L, *wvi);
+                    if (wv_v4l->_device == wc->_device &&
+                        wv_v4l->_size_x == wc->_size_x &&
+                        wv_v4l->_size_y == wc->_size_y &&
+                        wv_v4l->_fps == wc->_fps) {
+                      wv_v4l->_pformats.push_back(fmt.pixelformat);
+                      break;
+                    }
+                  }
+                }
+                // Did the loop finish, meaning that a webcam of these
+                // properties does not exist? Add it.
+                if (wvi == WebcamVideoV4L::_all_webcams.end()) {
+                  WebcamVideoV4L::_all_webcams.push_back(DCAST(WebcamVideo, wc));
+                }
+              }
+            }
+          }
+          continue;
+        }
+      }
+
+      // Check for Video4Linux capabilities
+      if (ioctl(fd, VIDIOCGCAP, &cap) != -1) {
+        if (cap.type & VID_TYPE_CAPTURE) {
+          //TODO: Video4Linux support
+          continue;
+        }
+      }
+    }
+    close(fd);
+  }
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: WebcamVideoV4L::open
+//       Access: Published, Virtual
+//  Description: Open this video, returning a MovieVideoCursor.
+////////////////////////////////////////////////////////////////////
+PT(MovieVideoCursor) WebcamVideoV4L::
+open() {
+  return new WebcamVideoCursorV4L(this);
+}
+
+#endif

+ 58 - 0
panda/src/vision/webcamVideoV4L.h

@@ -0,0 +1,58 @@
+// Filename: webcamVideoV4L.h
+// Created by: rdb (11Jun2010)
+//
+////////////////////////////////////////////////////////////////////
+//
+// PANDA 3D SOFTWARE
+// Copyright (c) Carnegie Mellon University.  All rights reserved.
+//
+// All use of this software is subject to the terms of the revised BSD
+// license.  You should have received a copy of this license along
+// with this source code in a file named "LICENSE."
+//
+////////////////////////////////////////////////////////////////////
+
+#ifndef WEBCAMVIDEOV4L_H
+#define WEBCAMVIDEOV4L_H
+
+#ifdef IS_LINUX
+
+#include "webcamVideo.h"
+
+class WebcamVideoCursorV4L;
+
+////////////////////////////////////////////////////////////////////
+//       Class : WebcamVideoV4L
+// Description : The Video4Linux implementation of webcams.
+////////////////////////////////////////////////////////////////////
+class WebcamVideoV4L : public WebcamVideo {
+private:
+  virtual PT(MovieVideoCursor) open();
+
+  friend class WebcamVideoCursorV4L;
+  friend void find_all_webcams_v4l();
+
+  string _device;
+  pvector<uint32_t> _pformats;
+
+public:
+  static TypeHandle get_class_type() {
+    return _type_handle;
+  }
+  static void init_type() {
+    WebcamVideo::init_type();
+    register_type(_type_handle, "WebcamVideoV4L",
+                  WebcamVideo::get_class_type());
+  }
+  virtual TypeHandle get_type() const {
+    return get_class_type();
+  }
+  virtual TypeHandle force_init_type() {init_type(); return get_class_type();}
+
+private:
+  static TypeHandle _type_handle;
+};
+
+#endif // IS_LINUX
+
+#endif