3
# This file is part of Checkbox.
5
# Copyright 2008-2012 Canonical Ltd.
7
# The v4l2 ioctl code comes from the Python bindings for the v4l2
8
# userspace api (http://pypi.python.org/pypi/v4l2):
9
# Copyright (C) 1999-2009 the contributors
11
# The JPEG metadata parser is a part of bfg-pages:
12
# http://code.google.com/p/bfg-pages/source/browse/trunk/pages/getimageinfo.py
13
# Copyright (C) Tim Hoffman
15
# Checkbox is free software: you can redistribute it and/or modify
16
# it under the terms of the GNU General Public License version 3,
17
# as published by the Free Software Foundation.
20
# Checkbox is distributed in the hope that it will be useful,
21
# but WITHOUT ANY WARRANTY; without even the implied warranty of
22
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23
# GNU General Public License for more details.
25
# You should have received a copy of the GNU General Public License
26
# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
41
from gi.repository import GObject
43
from subprocess import check_call, CalledProcessError, STDOUT
44
from tempfile import NamedTemporaryFile
52
_IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS
53
_IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS
54
_IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS
60
def _IOC(dir_, type_, nr, size):
62
ctypes.c_int32(dir_ << _IOC_DIRSHIFT).value |
63
ctypes.c_int32(ord(type_) << _IOC_TYPESHIFT).value |
64
ctypes.c_int32(nr << _IOC_NRSHIFT).value |
65
ctypes.c_int32(size << _IOC_SIZESHIFT).value)
68
def _IOC_TYPECHECK(t):
69
return ctypes.sizeof(t)
72
def _IOR(type_, nr, size):
73
return _IOC(_IOC_READ, type_, nr, ctypes.sizeof(size))
76
def _IOWR(type_, nr, size):
77
return _IOC(_IOC_READ | _IOC_WRITE, type_, nr, _IOC_TYPECHECK(size))
80
class v4l2_capability(ctypes.Structure):
85
('driver', ctypes.c_char * 16),
86
('card', ctypes.c_char * 32),
87
('bus_info', ctypes.c_char * 32),
88
('version', ctypes.c_uint32),
89
('capabilities', ctypes.c_uint32),
90
('reserved', ctypes.c_uint32 * 4),
94
# Values for 'capabilities' field
95
V4L2_CAP_VIDEO_CAPTURE = 0x00000001
96
V4L2_CAP_VIDEO_OVERLAY = 0x00000004
97
V4L2_CAP_READWRITE = 0x01000000
98
V4L2_CAP_STREAMING = 0x04000000
100
v4l2_frmsizetypes = ctypes.c_uint
102
V4L2_FRMSIZE_TYPE_DISCRETE,
103
V4L2_FRMSIZE_TYPE_CONTINUOUS,
104
V4L2_FRMSIZE_TYPE_STEPWISE,
108
class v4l2_frmsize_discrete(ctypes.Structure):
110
('width', ctypes.c_uint32),
111
('height', ctypes.c_uint32),
115
class v4l2_frmsize_stepwise(ctypes.Structure):
117
('min_width', ctypes.c_uint32),
118
('min_height', ctypes.c_uint32),
119
('step_width', ctypes.c_uint32),
120
('min_height', ctypes.c_uint32),
121
('max_height', ctypes.c_uint32),
122
('step_height', ctypes.c_uint32),
126
class v4l2_frmsizeenum(ctypes.Structure):
127
class _u(ctypes.Union):
129
('discrete', v4l2_frmsize_discrete),
130
('stepwise', v4l2_frmsize_stepwise),
134
('index', ctypes.c_uint32),
135
('pixel_format', ctypes.c_uint32),
136
('type', ctypes.c_uint32),
138
('reserved', ctypes.c_uint32 * 2)
141
_anonymous_ = ('_u',)
144
class v4l2_fmtdesc(ctypes.Structure):
146
('index', ctypes.c_uint32),
147
('type', ctypes.c_int),
148
('flags', ctypes.c_uint32),
149
('description', ctypes.c_char * 32),
150
('pixelformat', ctypes.c_uint32),
151
('reserved', ctypes.c_uint32 * 4),
154
V4L2_FMT_FLAG_COMPRESSED = 0x0001
155
V4L2_FMT_FLAG_EMULATED = 0x0002
157
# ioctl code for video devices
158
VIDIOC_QUERYCAP = _IOR('V', 0, v4l2_capability)
159
VIDIOC_ENUM_FRAMESIZES = _IOWR('V', 74, v4l2_frmsizeenum)
160
VIDIOC_ENUM_FMT = _IOWR('V', 2, v4l2_fmtdesc)
165
A simple class that displays a test image via GStreamer.
167
def __init__(self, args, gst_plugin=None, gst_video_type=None):
169
self._mainloop = GObject.MainLoop()
172
self._gst_plugin = gst_plugin
173
self._gst_video_type = gst_video_type
177
Display information regarding webcam hardware
179
cap_status = dev_status = 1
181
cp = v4l2_capability()
182
device = '/dev/video%d' % i
184
with open(device, 'r') as vd:
185
fcntl.ioctl(vd, VIDIOC_QUERYCAP, cp)
189
print("%s: OK" % device)
190
print(" name : %s" % cp.card.decode('UTF-8'))
191
print(" driver : %s" % cp.driver.decode('UTF-8'))
192
print(" version: %s.%s.%s"
194
(cp.version >> 8) & 0xff,
196
print(" flags : 0x%x [" % cp.capabilities,
197
' CAPTURE' if cp.capabilities & V4L2_CAP_VIDEO_CAPTURE
199
' OVERLAY' if cp.capabilities & V4L2_CAP_VIDEO_OVERLAY
201
' READWRITE' if cp.capabilities & V4L2_CAP_READWRITE
203
' STREAMING' if cp.capabilities & V4L2_CAP_STREAMING
207
resolutions = self._get_supported_resolutions(device)
209
self._supported_resolutions_to_string(resolutions).replace(
213
if cp.capabilities & V4L2_CAP_VIDEO_CAPTURE:
215
return dev_status | cap_status
219
Activate camera (switch on led), but don't display any output
221
pipespec = ("v4l2src device=%(device)s "
225
% {'device': self.args.device,
226
'type': self._gst_video_type,
227
'plugin': self._gst_plugin})
228
logging.debug("LED test with pipeline %s", pipespec)
229
self._pipeline = Gst.parse_launch(pipespec)
230
self._pipeline.set_state(Gst.State.PLAYING)
232
self._pipeline.set_state(Gst.State.NULL)
236
Displays the preview window
238
pipespec = ("v4l2src device=%(device)s "
239
"! %(type)s,width=%(width)d,height=%(height)d "
242
% {'device': self.args.device,
243
'type': self._gst_video_type,
244
'width': self._width,
245
'height': self._height,
246
'plugin': self._gst_plugin})
247
logging.debug("display test with pipeline %s", pipespec)
248
self._pipeline = Gst.parse_launch(pipespec)
249
self._pipeline.set_state(Gst.State.PLAYING)
251
self._pipeline.set_state(Gst.State.NULL)
255
Captures an image to a file
257
if self.args.filename:
258
self._still_helper(self.args.filename, self._width, self._height,
261
with NamedTemporaryFile(prefix='camera_test_', suffix='.jpg') as f:
262
self._still_helper(f.name, self._width, self._height,
265
def _still_helper(self, filename, width, height, quiet, pixelformat=None):
267
Captures an image to a given filename. width and height specify the
268
image size and quiet controls whether the image is displayed to the
269
user (quiet = True means do not display image).
271
command = ["fswebcam", "-D 1", "-S 50", "--no-banner",
272
"-d", self.args.device,
274
% (width, height), filename]
275
use_gstreamer = False
277
if 'MJPG' == pixelformat: # special tweak for fswebcam
278
pixelformat = 'MJPEG'
279
command.extend(["-p", pixelformat])
282
check_call(command, stdout=open(os.devnull, 'w'), stderr=STDOUT)
283
except (CalledProcessError, OSError):
287
pipespec = ("v4l2src device=%(device)s "
288
"! %(type)s,width=%(width)d,height=%(height)d "
291
"! filesink location=%(filename)s"
292
% {'device': self.args.device,
293
'type': self._gst_video_type,
296
'plugin': self._gst_plugin,
297
'filename': filename})
298
logging.debug("still test with gstreamer and "
299
"pipeline %s", pipespec)
300
self._pipeline = Gst.parse_launch(pipespec)
301
self._pipeline.set_state(Gst.State.PLAYING)
303
self._pipeline.set_state(Gst.State.NULL)
307
image_type = imghdr.what(filename)
308
pipespec = ("filesrc location=%(filename)s ! "
311
"imagefreeze ! autovideosink"
312
% {'filename': filename,
314
self._pipeline = Gst.parse_launch(pipespec)
315
self._pipeline.set_state(Gst.State.PLAYING)
317
self._pipeline.set_state(Gst.State.NULL)
319
def _supported_resolutions_to_string(self, supported_resolutions):
321
Return a printable string representing a list of supported resolutions
324
for resolution in supported_resolutions:
325
ret += "Format: %s (%s)\n" % (resolution['pixelformat'],
326
resolution['description'])
327
ret += "Resolutions: "
328
for res in resolution['resolutions']:
329
ret += "%sx%s," % (res[0], res[1])
330
# truncate the extra comma with :-1
331
ret = ret[:-1] + "\n"
334
def resolutions(self):
336
After querying the webcam for supported formats and resolutions,
337
take multiple images using the first format returned by the driver,
338
and see if they are valid
340
resolutions = self._get_supported_resolutions(self.args.device)
341
# print supported formats and resolutions for the logs
342
print(self._supported_resolutions_to_string(resolutions))
344
# pick the first format, which seems to be what the driver wants for a
345
# default. This also matches the logic that fswebcam uses to select
347
resolution = resolutions[0]
349
print("Taking multiple images using the %s format"
350
% resolution['pixelformat'])
351
for res in resolution['resolutions']:
354
f = NamedTemporaryFile(prefix='camera_test_%s%sx%s' %
355
(resolution['pixelformat'], w, h),
356
suffix='.jpg', delete=False)
357
print("Taking a picture at %sx%s" % (w, h))
358
self._still_helper(f.name, w, h, True,
359
pixelformat=resolution['pixelformat'])
360
if self._validate_image(f.name, w, h):
361
print("Validated image %s" % f.name)
364
print("Failed to validate image %s" % f.name,
370
def _get_pixel_formats(self, device, maxformats=5):
372
Query the camera to see what pixel formats it supports. A list of
373
dicts is returned consisting of format and description. The caller
374
should check whether this camera supports VIDEO_CAPTURE before
375
calling this function.
377
supported_formats = []
380
fmt.type = V4L2_CAP_VIDEO_CAPTURE
382
while fmt.index < maxformats:
383
with open(device, 'r') as vd:
384
if fcntl.ioctl(vd, VIDIOC_ENUM_FMT, fmt) == 0:
386
# save the int type for re-use later
387
pixelformat['pixelformat_int'] = fmt.pixelformat
388
pixelformat['pixelformat'] = "%s%s%s%s" % \
389
(chr(fmt.pixelformat & 0xFF),
390
chr((fmt.pixelformat >> 8) & 0xFF),
391
chr((fmt.pixelformat >> 16) & 0xFF),
392
chr((fmt.pixelformat >> 24) & 0xFF))
393
pixelformat['description'] = fmt.description.decode()
394
supported_formats.append(pixelformat)
395
fmt.index = fmt.index + 1
397
# EINVAL is the ioctl's way of telling us that there are no
398
# more formats, so we ignore it
399
if e.errno != errno.EINVAL:
400
print("Unable to determine Pixel Formats, this may be a "
402
return supported_formats
403
return supported_formats
405
def _get_supported_resolutions(self, device):
407
Query the camera for supported resolutions for a given pixel_format.
408
Data is returned in a list of dictionaries with supported pixel
409
formats as the following example shows:
410
resolution['pixelformat'] = "YUYV"
411
resolution['description'] = "(YUV 4:2:2 (YUYV))"
412
resolution['resolutions'] = [[width, height], [640, 480], [1280, 720] ]
414
If we are unable to gather any information from the driver, then we
415
return YUYV and 640x480 which seems to be a safe default.
416
Per the v4l2 spec the ioctl used here is experimental
417
but seems to be well supported.
419
supported_formats = self._get_pixel_formats(device)
420
if not supported_formats:
422
resolution['description'] = "YUYV"
423
resolution['pixelformat'] = "YUYV"
424
resolution['resolutions'] = [[640, 480]]
425
supported_formats.append(resolution)
426
return supported_formats
428
for supported_format in supported_formats:
430
framesize = v4l2_frmsizeenum()
432
framesize.pixel_format = supported_format['pixelformat_int']
433
with open(device, 'r') as vd:
435
while fcntl.ioctl(vd,
436
VIDIOC_ENUM_FRAMESIZES,
438
if framesize.type == V4L2_FRMSIZE_TYPE_DISCRETE:
439
resolutions.append([framesize.discrete.width,
440
framesize.discrete.height])
441
# for continuous and stepwise, let's just use min and
442
# max they use the same structure and only return
444
elif (framesize.type in (V4L2_FRMSIZE_TYPE_CONTINUOUS,
445
V4L2_FRMSIZE_TYPE_STEPWISE)):
446
resolutions.append([framesize.stepwise.min_width,
447
framesize.stepwise.min_height]
449
resolutions.append([framesize.stepwise.max_width,
450
framesize.stepwise.max_height]
453
framesize.index = framesize.index + 1
455
# EINVAL is the ioctl's way of telling us that there are no
456
# more formats, so we ignore it
457
if e.errno != errno.EINVAL:
458
print("Unable to determine supported framesizes "
459
"(resolutions), this may be a driver issue.")
460
supported_format['resolutions'] = resolutions
461
return supported_formats
463
def _validate_image(self, filename, width, height):
465
Given a filename, ensure that the image is the width and height
466
specified and is a valid image file.
468
if imghdr.what(filename) != 'jpeg':
472
with open(filename, mode='rb') as jpeg:
476
while (b and ord(b) != 0xDA):
477
while (ord(b) != 0xFF):
479
while (ord(b) == 0xFF):
481
if (ord(b) >= 0xC0 and ord(b) <= 0xC3):
483
h, w = struct.unpack(">HH", jpeg.read(4))
486
outw, outh = int(w), int(h)
487
except (struct.error, ValueError):
491
print("Image width does not match, was %s should be %s" %
492
(outw, width), file=sys.stderr)
495
print("Image width does not match, was %s should be %s" %
496
(outh, height), file=sys.stderr)
504
def parse_arguments(argv):
506
Parse command line arguments
508
parser = argparse.ArgumentParser(description="Run a camera-related test")
509
subparsers = parser.add_subparsers(dest='test',
511
description='Available camera tests')
513
parser.add_argument('--debug', dest='log_level',
514
action="store_const", const=logging.DEBUG,
515
default=logging.INFO, help="Show debugging messages")
517
def add_device_parameter(parser):
518
group = parser.add_mutually_exclusive_group()
519
group.add_argument("-d", "--device", default="/dev/video0",
520
help="Device for the webcam to use")
521
group.add_argument("--highest-device", action="store_true",
522
help=("Use the /dev/videoN "
523
"where N is the highest value available"))
524
group.add_argument("--lowest-device", action="store_true",
525
help=("Use the /dev/videoN "
526
"where N is the lowest value available"))
527
subparsers.add_parser('detect')
528
led_parser = subparsers.add_parser('led')
529
add_device_parameter(led_parser)
530
display_parser = subparsers.add_parser('display')
531
add_device_parameter(display_parser)
532
still_parser = subparsers.add_parser('still')
533
add_device_parameter(still_parser)
534
still_parser.add_argument("-f", "--filename",
535
help="Filename to store the picture")
536
still_parser.add_argument("-q", "--quiet", action="store_true",
537
help=("Don't display picture, "
538
"just write the picture to a file"))
539
resolutions_parser = subparsers.add_parser('resolutions')
540
add_device_parameter(resolutions_parser)
541
args = parser.parse_args(argv)
543
def get_video_devices():
544
devices = sorted(glob('/dev/video[0-9]'),
545
key=lambda d: re.search(r'\d', d).group(0))
546
assert len(devices) > 0, "No video devices found"
549
if hasattr(args, 'highest_device') and args.highest_device:
550
args.device = get_video_devices()[-1]
551
elif hasattr(args, 'lowest_device') and args.lowest_device:
552
args.device = get_video_devices()[0]
556
if __name__ == "__main__":
557
args = parse_arguments(sys.argv[1:])
562
logging.basicConfig(level=args.log_level)
564
# Import Gst only for the test cases that will need it
565
if args.test in ['display', 'still', 'led', 'resolutions']:
566
from gi.repository import Gst
567
if Gst.version()[0] > 0:
568
gst_plugin = 'videoconvert'
569
gst_video_type = 'video/x-raw'
571
gst_plugin = 'ffmpegcolorspace'
572
gst_video_type = 'video/x-raw-yuv'
574
camera = CameraTest(args, gst_plugin, gst_video_type)
576
camera = CameraTest(args)
578
sys.exit(getattr(camera, args.test)())