1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20*/
21#include "SDL_internal.h"
22
23#ifdef SDL_CAMERA_DRIVER_V4L2
24
25#include <dirent.h>
26#include <errno.h>
27#include <fcntl.h> // low-level i/o
28#include <stddef.h>
29#include <sys/ioctl.h>
30#include <sys/mman.h>
31#include <sys/stat.h>
32#include <unistd.h>
33#include <linux/videodev2.h>
34
35#ifndef V4L2_CAP_DEVICE_CAPS
36// device_caps was added to struct v4l2_capability as of kernel 3.4.
37#define device_caps reserved[0]
38SDL_COMPILE_TIME_ASSERT(v4l2devicecaps, offsetof(struct v4l2_capability,device_caps) == offsetof(struct v4l2_capability,capabilities) + 4);
39#endif
40
41#include "../SDL_syscamera.h"
42#include "../SDL_camera_c.h"
43#include "../../video/SDL_pixels_c.h"
44#include "../../video/SDL_surface_c.h"
45#include "../../thread/SDL_systhread.h"
46#include "../../core/linux/SDL_evdev_capabilities.h"
47#include "../../core/linux/SDL_udev.h"
48
49#ifndef SDL_USE_LIBUDEV
50#include <dirent.h>
51#endif
52
53typedef struct V4L2DeviceHandle
54{
55 char *bus_info;
56 char *path;
57} V4L2DeviceHandle;
58
59
60typedef enum io_method {
61 IO_METHOD_INVALID,
62 IO_METHOD_READ,
63 IO_METHOD_MMAP,
64 IO_METHOD_USERPTR
65} io_method;
66
67struct buffer {
68 void *start;
69 size_t length;
70 int available; // Is available in userspace
71};
72
73struct SDL_PrivateCameraData
74{
75 int fd;
76 io_method io;
77 int nb_buffers;
78 struct buffer *buffers;
79 int driver_pitch;
80};
81
82static int xioctl(int fh, int request, void *arg)
83{
84 int r;
85
86 do {
87 r = ioctl(fh, request, arg);
88 } while ((r == -1) && (errno == EINTR));
89
90 return r;
91}
92
93static bool V4L2_WaitDevice(SDL_Camera *device)
94{
95 const int fd = device->hidden->fd;
96
97 int rc;
98
99 do {
100 fd_set fds;
101 FD_ZERO(&fds);
102 FD_SET(fd, &fds);
103
104 struct timeval tv;
105 tv.tv_sec = 0;
106 tv.tv_usec = 100 * 1000;
107
108 rc = select(fd + 1, &fds, NULL, NULL, &tv);
109 if ((rc == -1) && (errno == EINTR)) {
110 rc = 0; // pretend it was a timeout, keep looping.
111 } else if (rc > 0) {
112 return true;
113 }
114
115 // Thread is requested to shut down
116 if (SDL_GetAtomicInt(&device->shutdown)) {
117 return true;
118 }
119
120 } while (rc == 0);
121
122 return false;
123}
124
125static SDL_CameraFrameResult V4L2_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS)
126{
127 const int fd = device->hidden->fd;
128 const io_method io = device->hidden->io;
129 size_t size = device->hidden->buffers[0].length;
130 struct v4l2_buffer buf;
131 ssize_t amount;
132
133 switch (io) {
134 case IO_METHOD_READ:
135 if ((amount = read(fd, device->hidden->buffers[0].start, size)) == -1) {
136 switch (errno) {
137 case EAGAIN:
138 return SDL_CAMERA_FRAME_SKIP;
139
140 case EIO:
141 // Could ignore EIO, see spec.
142 // fall through
143
144 default:
145 SDL_SetError("read");
146 return SDL_CAMERA_FRAME_ERROR;
147 }
148 }
149
150 *timestampNS = SDL_GetTicksNS(); // oh well, close enough.
151 frame->pixels = device->hidden->buffers[0].start;
152 if (device->hidden->driver_pitch) {
153 frame->pitch = device->hidden->driver_pitch;
154 } else {
155 frame->pitch = (int)amount;
156 }
157 break;
158
159 case IO_METHOD_MMAP:
160 SDL_zero(buf);
161
162 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
163 buf.memory = V4L2_MEMORY_MMAP;
164
165 if (xioctl(fd, VIDIOC_DQBUF, &buf) == -1) {
166 switch (errno) {
167 case EAGAIN:
168 return SDL_CAMERA_FRAME_SKIP;
169
170 case EIO:
171 // Could ignore EIO, see spec.
172 // fall through
173
174 default:
175 SDL_SetError("VIDIOC_DQBUF: %d", errno);
176 return SDL_CAMERA_FRAME_ERROR;
177 }
178 }
179
180 if ((int)buf.index < 0 || (int)buf.index >= device->hidden->nb_buffers) {
181 SDL_SetError("invalid buffer index");
182 return SDL_CAMERA_FRAME_ERROR;
183 }
184
185 frame->pixels = device->hidden->buffers[buf.index].start;
186 if (device->hidden->driver_pitch) {
187 frame->pitch = device->hidden->driver_pitch;
188 } else {
189 frame->pitch = buf.bytesused;
190 }
191 device->hidden->buffers[buf.index].available = 1;
192
193 *timestampNS = (((Uint64) buf.timestamp.tv_sec) * SDL_NS_PER_SECOND) + SDL_US_TO_NS(buf.timestamp.tv_usec);
194
195 #if DEBUG_CAMERA
196 SDL_Log("CAMERA: debug mmap: image %d/%d data[0]=%p", buf.index, device->hidden->nb_buffers, (void*)frame->pixels);
197 #endif
198 break;
199
200 case IO_METHOD_USERPTR:
201 SDL_zero(buf);
202
203 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
204 buf.memory = V4L2_MEMORY_USERPTR;
205
206 if (xioctl(fd, VIDIOC_DQBUF, &buf) == -1) {
207 switch (errno) {
208 case EAGAIN:
209 return SDL_CAMERA_FRAME_SKIP;
210
211 case EIO:
212 // Could ignore EIO, see spec.
213 // fall through
214
215 default:
216 SDL_SetError("VIDIOC_DQBUF");
217 return SDL_CAMERA_FRAME_ERROR;
218 }
219 }
220
221 int i;
222 for (i = 0; i < device->hidden->nb_buffers; ++i) {
223 if (buf.m.userptr == (unsigned long)device->hidden->buffers[i].start && buf.length == size) {
224 break;
225 }
226 }
227
228 if (i >= device->hidden->nb_buffers) {
229 SDL_SetError("invalid buffer index");
230 return SDL_CAMERA_FRAME_ERROR;
231 }
232
233 frame->pixels = (void*)buf.m.userptr;
234 if (device->hidden->driver_pitch) {
235 frame->pitch = device->hidden->driver_pitch;
236 } else {
237 frame->pitch = buf.bytesused;
238 }
239 device->hidden->buffers[i].available = 1;
240
241 *timestampNS = (((Uint64) buf.timestamp.tv_sec) * SDL_NS_PER_SECOND) + SDL_US_TO_NS(buf.timestamp.tv_usec);
242
243 #if DEBUG_CAMERA
244 SDL_Log("CAMERA: debug userptr: image %d/%d data[0]=%p", buf.index, device->hidden->nb_buffers, (void*)frame->pixels);
245 #endif
246 break;
247
248 case IO_METHOD_INVALID:
249 SDL_assert(!"Shouldn't have hit this");
250 break;
251 }
252
253 return SDL_CAMERA_FRAME_READY;
254}
255
256static void V4L2_ReleaseFrame(SDL_Camera *device, SDL_Surface *frame)
257{
258 struct v4l2_buffer buf;
259 const int fd = device->hidden->fd;
260 const io_method io = device->hidden->io;
261 int i;
262
263 for (i = 0; i < device->hidden->nb_buffers; ++i) {
264 if (frame->pixels == device->hidden->buffers[i].start) {
265 break;
266 }
267 }
268
269 if (i >= device->hidden->nb_buffers) {
270 return; // oh well, we didn't own this.
271 }
272
273 switch (io) {
274 case IO_METHOD_READ:
275 break;
276
277 case IO_METHOD_MMAP:
278 SDL_zero(buf);
279
280 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
281 buf.memory = V4L2_MEMORY_MMAP;
282 buf.index = i;
283
284 if (xioctl(fd, VIDIOC_QBUF, &buf) == -1) {
285 // !!! FIXME: disconnect the device.
286 return; //SDL_SetError("VIDIOC_QBUF");
287 }
288 device->hidden->buffers[i].available = 0;
289 break;
290
291 case IO_METHOD_USERPTR:
292 SDL_zero(buf);
293
294 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
295 buf.memory = V4L2_MEMORY_USERPTR;
296 buf.index = i;
297 buf.m.userptr = (unsigned long)frame->pixels;
298 buf.length = (int) device->hidden->buffers[i].length;
299
300 if (xioctl(fd, VIDIOC_QBUF, &buf) == -1) {
301 // !!! FIXME: disconnect the device.
302 return; //SDL_SetError("VIDIOC_QBUF");
303 }
304 device->hidden->buffers[i].available = 0;
305 break;
306
307 case IO_METHOD_INVALID:
308 SDL_assert(!"Shouldn't have hit this");
309 break;
310 }
311}
312
313static bool EnqueueBuffers(SDL_Camera *device)
314{
315 const int fd = device->hidden->fd;
316 const io_method io = device->hidden->io;
317 switch (io) {
318 case IO_METHOD_READ:
319 break;
320
321 case IO_METHOD_MMAP:
322 for (int i = 0; i < device->hidden->nb_buffers; ++i) {
323 if (device->hidden->buffers[i].available == 0) {
324 struct v4l2_buffer buf;
325
326 SDL_zero(buf);
327 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
328 buf.memory = V4L2_MEMORY_MMAP;
329 buf.index = i;
330
331 if (xioctl(fd, VIDIOC_QBUF, &buf) == -1) {
332 return SDL_SetError("VIDIOC_QBUF");
333 }
334 }
335 }
336 break;
337
338 case IO_METHOD_USERPTR:
339 for (int i = 0; i < device->hidden->nb_buffers; ++i) {
340 if (device->hidden->buffers[i].available == 0) {
341 struct v4l2_buffer buf;
342
343 SDL_zero(buf);
344 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
345 buf.memory = V4L2_MEMORY_USERPTR;
346 buf.index = i;
347 buf.m.userptr = (unsigned long)device->hidden->buffers[i].start;
348 buf.length = (int) device->hidden->buffers[i].length;
349
350 if (xioctl(fd, VIDIOC_QBUF, &buf) == -1) {
351 return SDL_SetError("VIDIOC_QBUF");
352 }
353 }
354 }
355 break;
356
357 case IO_METHOD_INVALID: SDL_assert(!"Shouldn't have hit this"); break;
358 }
359 return true;
360}
361
362static bool AllocBufferRead(SDL_Camera *device, size_t buffer_size)
363{
364 device->hidden->buffers[0].length = buffer_size;
365 device->hidden->buffers[0].start = SDL_calloc(1, buffer_size);
366 return (device->hidden->buffers[0].start != NULL);
367}
368
369static bool AllocBufferMmap(SDL_Camera *device)
370{
371 const int fd = device->hidden->fd;
372 int i;
373 for (i = 0; i < device->hidden->nb_buffers; ++i) {
374 struct v4l2_buffer buf;
375
376 SDL_zero(buf);
377
378 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
379 buf.memory = V4L2_MEMORY_MMAP;
380 buf.index = i;
381
382 if (xioctl(fd, VIDIOC_QUERYBUF, &buf) == -1) {
383 return SDL_SetError("VIDIOC_QUERYBUF");
384 }
385
386 device->hidden->buffers[i].length = buf.length;
387 device->hidden->buffers[i].start =
388 mmap(NULL /* start anywhere */,
389 buf.length,
390 PROT_READ | PROT_WRITE /* required */,
391 MAP_SHARED /* recommended */,
392 fd, buf.m.offset);
393
394 if (MAP_FAILED == device->hidden->buffers[i].start) {
395 return SDL_SetError("mmap");
396 }
397 }
398 return true;
399}
400
401static bool AllocBufferUserPtr(SDL_Camera *device, size_t buffer_size)
402{
403 int i;
404 for (i = 0; i < device->hidden->nb_buffers; ++i) {
405 device->hidden->buffers[i].length = buffer_size;
406 device->hidden->buffers[i].start = SDL_calloc(1, buffer_size);
407
408 if (!device->hidden->buffers[i].start) {
409 return false;
410 }
411 }
412 return true;
413}
414
415static void format_v4l2_to_sdl(Uint32 fmt, SDL_PixelFormat *format, SDL_Colorspace *colorspace)
416{
417 switch (fmt) {
418 #define CASE(x, y, z) case x: *format = y; *colorspace = z; return
419 CASE(V4L2_PIX_FMT_YUYV, SDL_PIXELFORMAT_YUY2, SDL_COLORSPACE_BT709_LIMITED);
420 CASE(V4L2_PIX_FMT_MJPEG, SDL_PIXELFORMAT_MJPG, SDL_COLORSPACE_SRGB);
421 #undef CASE
422 default:
423 #if DEBUG_CAMERA
424 SDL_Log("CAMERA: Unknown format V4L2_PIX_FORMAT '%c%c%c%c' (0x%x)",
425 (char)(Uint8)(fmt >> 0),
426 (char)(Uint8)(fmt >> 8),
427 (char)(Uint8)(fmt >> 16),
428 (char)(Uint8)(fmt >> 24), fmt);
429 #endif
430 break;
431 }
432 *format = SDL_PIXELFORMAT_UNKNOWN;
433 *colorspace = SDL_COLORSPACE_UNKNOWN;
434}
435
436static Uint32 format_sdl_to_v4l2(SDL_PixelFormat fmt)
437{
438 switch (fmt) {
439 #define CASE(y, x) case x: return y
440 CASE(V4L2_PIX_FMT_YUYV, SDL_PIXELFORMAT_YUY2);
441 CASE(V4L2_PIX_FMT_MJPEG, SDL_PIXELFORMAT_MJPG);
442 #undef CASE
443 default:
444 return 0;
445 }
446}
447
448static void V4L2_CloseDevice(SDL_Camera *device)
449{
450 if (!device) {
451 return;
452 }
453
454 if (device->hidden) {
455 const io_method io = device->hidden->io;
456 const int fd = device->hidden->fd;
457
458 if ((io == IO_METHOD_MMAP) || (io == IO_METHOD_USERPTR)) {
459 enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
460 xioctl(fd, VIDIOC_STREAMOFF, &type);
461 }
462
463 if (device->hidden->buffers) {
464 switch (io) {
465 case IO_METHOD_INVALID:
466 break;
467
468 case IO_METHOD_READ:
469 SDL_free(device->hidden->buffers[0].start);
470 break;
471
472 case IO_METHOD_MMAP:
473 for (int i = 0; i < device->hidden->nb_buffers; ++i) {
474 if (munmap(device->hidden->buffers[i].start, device->hidden->buffers[i].length) == -1) {
475 SDL_SetError("munmap");
476 }
477 }
478 break;
479
480 case IO_METHOD_USERPTR:
481 for (int i = 0; i < device->hidden->nb_buffers; ++i) {
482 SDL_free(device->hidden->buffers[i].start);
483 }
484 break;
485 }
486
487 SDL_free(device->hidden->buffers);
488 }
489
490 if (fd != -1) {
491 close(fd);
492 }
493 SDL_free(device->hidden);
494
495 device->hidden = NULL;
496 }
497}
498
499static bool V4L2_OpenDevice(SDL_Camera *device, const SDL_CameraSpec *spec)
500{
501 const V4L2DeviceHandle *handle = (const V4L2DeviceHandle *) device->handle;
502 struct stat st;
503 struct v4l2_capability cap;
504 const int fd = open(handle->path, O_RDWR /* required */ | O_NONBLOCK, 0);
505
506 // most of this probably shouldn't fail unless the filesystem node changed out from under us since MaybeAddDevice().
507 if (fd == -1) {
508 return SDL_SetError("Cannot open '%s': %d, %s", handle->path, errno, strerror(errno));
509 } else if (fstat(fd, &st) == -1) {
510 close(fd);
511 return SDL_SetError("Cannot identify '%s': %d, %s", handle->path, errno, strerror(errno));
512 } else if (!S_ISCHR(st.st_mode)) {
513 close(fd);
514 return SDL_SetError("%s is not a character device", handle->path);
515 } else if (xioctl(fd, VIDIOC_QUERYCAP, &cap) == -1) {
516 const int err = errno;
517 close(fd);
518 if (err == EINVAL) {
519 return SDL_SetError("%s is unexpectedly not a V4L2 device", handle->path);
520 }
521 return SDL_SetError("Error VIDIOC_QUERYCAP errno=%d device%s is no V4L2 device", err, handle->path);
522 } else if ((cap.device_caps & V4L2_CAP_VIDEO_CAPTURE) == 0) {
523 close(fd);
524 return SDL_SetError("%s is unexpectedly not a video capture device", handle->path);
525 }
526
527 device->hidden = (struct SDL_PrivateCameraData *) SDL_calloc(1, sizeof (struct SDL_PrivateCameraData));
528 if (device->hidden == NULL) {
529 close(fd);
530 return false;
531 }
532
533 device->hidden->fd = fd;
534 device->hidden->io = IO_METHOD_INVALID;
535
536 // Select video input, video standard and tune here.
537 // errors in the crop code are not fatal.
538 struct v4l2_cropcap cropcap;
539 SDL_zero(cropcap);
540 cropcap.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
541 if (xioctl(fd, VIDIOC_CROPCAP, &cropcap) == 0) {
542 struct v4l2_crop crop;
543 SDL_zero(crop);
544 crop.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
545 crop.c = cropcap.defrect; // reset to default
546 xioctl(fd, VIDIOC_S_CROP, &crop);
547 }
548
549 struct v4l2_format fmt;
550 SDL_zero(fmt);
551 fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
552 fmt.fmt.pix.width = spec->width;
553 fmt.fmt.pix.height = spec->height;
554 fmt.fmt.pix.pixelformat = format_sdl_to_v4l2(spec->format);
555 //fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
556 fmt.fmt.pix.field = V4L2_FIELD_ANY;
557
558 #if DEBUG_CAMERA
559 SDL_Log("CAMERA: set SDL format %s", SDL_GetPixelFormatName(spec->format));
560 { const Uint32 f = fmt.fmt.pix.pixelformat; SDL_Log("CAMERA: set format V4L2_format=%d %c%c%c%c", f, (f >> 0) & 0xff, (f >> 8) & 0xff, (f >> 16) & 0xff, (f >> 24) & 0xff); }
561 #endif
562
563 if (xioctl(fd, VIDIOC_S_FMT, &fmt) == -1) {
564 return SDL_SetError("Error VIDIOC_S_FMT");
565 }
566
567 if (spec->framerate_numerator && spec->framerate_denominator) {
568 struct v4l2_streamparm setfps;
569 SDL_zero(setfps);
570 setfps.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
571 if (xioctl(fd, VIDIOC_G_PARM, &setfps) == 0) {
572 if ( (setfps.parm.capture.timeperframe.denominator != spec->framerate_numerator) ||
573 (setfps.parm.capture.timeperframe.numerator = spec->framerate_denominator) ) {
574 setfps.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
575 setfps.parm.capture.timeperframe.numerator = spec->framerate_denominator;
576 setfps.parm.capture.timeperframe.denominator = spec->framerate_numerator;
577 if (xioctl(fd, VIDIOC_S_PARM, &setfps) == -1) {
578 return SDL_SetError("Error VIDIOC_S_PARM");
579 }
580 }
581 }
582 }
583
584 SDL_zero(fmt);
585 fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
586 if (xioctl(fd, VIDIOC_G_FMT, &fmt) == -1) {
587 return SDL_SetError("Error VIDIOC_G_FMT");
588 }
589 device->hidden->driver_pitch = fmt.fmt.pix.bytesperline;
590
591 io_method io = IO_METHOD_INVALID;
592 if ((io == IO_METHOD_INVALID) && (cap.device_caps & V4L2_CAP_STREAMING)) {
593 struct v4l2_requestbuffers req;
594 SDL_zero(req);
595 req.count = 8;
596 req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
597 req.memory = V4L2_MEMORY_MMAP;
598 if ((xioctl(fd, VIDIOC_REQBUFS, &req) == 0) && (req.count >= 2)) {
599 io = IO_METHOD_MMAP;
600 device->hidden->nb_buffers = req.count;
601 } else { // mmap didn't work out? Try USERPTR.
602 SDL_zero(req);
603 req.count = 8;
604 req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
605 req.memory = V4L2_MEMORY_USERPTR;
606 if (xioctl(fd, VIDIOC_REQBUFS, &req) == 0) {
607 io = IO_METHOD_USERPTR;
608 device->hidden->nb_buffers = 8;
609 }
610 }
611 }
612
613 if ((io == IO_METHOD_INVALID) && (cap.device_caps & V4L2_CAP_READWRITE)) {
614 io = IO_METHOD_READ;
615 device->hidden->nb_buffers = 1;
616 }
617
618 if (io == IO_METHOD_INVALID) {
619 return SDL_SetError("Don't have a way to talk to this device");
620 }
621
622 device->hidden->io = io;
623
624 device->hidden->buffers = SDL_calloc(device->hidden->nb_buffers, sizeof(*device->hidden->buffers));
625 if (!device->hidden->buffers) {
626 return false;
627 }
628
629 size_t size, pitch;
630 if (!SDL_CalculateSurfaceSize(device->spec.format, device->spec.width, device->spec.height, &size, &pitch, false)) {
631 return false;
632 }
633
634 bool rc = true;
635 switch (io) {
636 case IO_METHOD_READ:
637 rc = AllocBufferRead(device, size);
638 break;
639
640 case IO_METHOD_MMAP:
641 rc = AllocBufferMmap(device);
642 break;
643
644 case IO_METHOD_USERPTR:
645 rc = AllocBufferUserPtr(device, size);
646 break;
647
648 case IO_METHOD_INVALID:
649 SDL_assert(!"Shouldn't have hit this");
650 break;
651 }
652
653 if (!rc) {
654 return false;
655 } else if (!EnqueueBuffers(device)) {
656 return false;
657 } else if (io != IO_METHOD_READ) {
658 enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
659 if (xioctl(fd, VIDIOC_STREAMON, &type) == -1) {
660 return SDL_SetError("VIDIOC_STREAMON");
661 }
662 }
663
664 // Currently there is no user permission prompt for camera access, but maybe there will be a D-Bus portal interface at some point.
665 SDL_CameraPermissionOutcome(device, true);
666
667 return true;
668}
669
670static bool FindV4L2CameraByBusInfoCallback(SDL_Camera *device, void *userdata)
671{
672 const V4L2DeviceHandle *handle = (const V4L2DeviceHandle *) device->handle;
673 return (SDL_strcmp(handle->bus_info, (const char *) userdata) == 0);
674}
675
676static bool AddCameraFormat(const int fd, CameraFormatAddData *data, SDL_PixelFormat sdlfmt, SDL_Colorspace colorspace, Uint32 v4l2fmt, int w, int h)
677{
678 struct v4l2_frmivalenum frmivalenum;
679 SDL_zero(frmivalenum);
680 frmivalenum.pixel_format = v4l2fmt;
681 frmivalenum.width = (Uint32) w;
682 frmivalenum.height = (Uint32) h;
683
684 while (ioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmivalenum) == 0) {
685 if (frmivalenum.type == V4L2_FRMIVAL_TYPE_DISCRETE) {
686 const int numerator = (int) frmivalenum.discrete.numerator;
687 const int denominator = (int) frmivalenum.discrete.denominator;
688 #if DEBUG_CAMERA
689 const float fps = (float) denominator / (float) numerator;
690 SDL_Log("CAMERA: * Has discrete frame interval (%d / %d), fps=%f", numerator, denominator, fps);
691 #endif
692 if (!SDL_AddCameraFormat(data, sdlfmt, colorspace, w, h, denominator, numerator)) {
693 return false; // Probably out of memory; we'll go with what we have, if anything.
694 }
695 frmivalenum.index++; // set up for the next one.
696 } else if ((frmivalenum.type == V4L2_FRMIVAL_TYPE_STEPWISE) || (frmivalenum.type == V4L2_FRMIVAL_TYPE_CONTINUOUS)) {
697 int d = frmivalenum.stepwise.min.denominator;
698 // !!! FIXME: should we step by the numerator...?
699 for (int n = (int) frmivalenum.stepwise.min.numerator; n <= (int) frmivalenum.stepwise.max.numerator; n += (int) frmivalenum.stepwise.step.numerator) {
700 #if DEBUG_CAMERA
701 const float fps = (float) d / (float) n;
702 SDL_Log("CAMERA: * Has %s frame interval (%d / %d), fps=%f", (frmivalenum.type == V4L2_FRMIVAL_TYPE_STEPWISE) ? "stepwise" : "continuous", n, d, fps);
703 #endif
704 // SDL expects framerate, V4L2 provides interval
705 if (!SDL_AddCameraFormat(data, sdlfmt, colorspace, w, h, d, n)) {
706 return false; // Probably out of memory; we'll go with what we have, if anything.
707 }
708 d += (int) frmivalenum.stepwise.step.denominator;
709 }
710 break;
711 }
712 }
713
714 return true;
715}
716
717
718static void MaybeAddDevice(const char *path)
719{
720 if (!path) {
721 return;
722 }
723
724 struct stat st;
725 const int fd = open(path, O_RDWR /* required */ | O_NONBLOCK, 0);
726 if (fd == -1) {
727 return; // can't open it? skip it.
728 } else if (fstat(fd, &st) == -1) {
729 close(fd);
730 return; // can't stat it? skip it.
731 } else if (!S_ISCHR(st.st_mode)) {
732 close(fd);
733 return; // not a character device.
734 }
735
736 struct v4l2_capability vcap;
737 const int rc = ioctl(fd, VIDIOC_QUERYCAP, &vcap);
738 if (rc != 0) {
739 close(fd);
740 return; // probably not a v4l2 device at all.
741 } else if ((vcap.device_caps & V4L2_CAP_VIDEO_CAPTURE) == 0) {
742 close(fd);
743 return; // not a video capture device.
744 } else if (SDL_FindPhysicalCameraByCallback(FindV4L2CameraByBusInfoCallback, vcap.bus_info)) {
745 close(fd);
746 return; // already have it.
747 }
748
749 #if DEBUG_CAMERA
750 SDL_Log("CAMERA: V4L2 camera path='%s' bus_info='%s' name='%s'", path, (const char *) vcap.bus_info, vcap.card);
751 #endif
752
753 CameraFormatAddData add_data;
754 SDL_zero(add_data);
755
756 struct v4l2_fmtdesc fmtdesc;
757 SDL_zero(fmtdesc);
758 fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
759 while (ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) == 0) {
760 SDL_PixelFormat sdlfmt = SDL_PIXELFORMAT_UNKNOWN;
761 SDL_Colorspace colorspace = SDL_COLORSPACE_UNKNOWN;
762 format_v4l2_to_sdl(fmtdesc.pixelformat, &sdlfmt, &colorspace);
763
764 #if DEBUG_CAMERA
765 SDL_Log("CAMERA: - Has format '%s'%s%s", SDL_GetPixelFormatName(sdlfmt),
766 (fmtdesc.flags & V4L2_FMT_FLAG_EMULATED) ? " [EMULATED]" : "",
767 (fmtdesc.flags & V4L2_FMT_FLAG_COMPRESSED) ? " [COMPRESSED]" : "");
768 #endif
769
770 fmtdesc.index++; // prepare for next iteration.
771
772 if (sdlfmt == SDL_PIXELFORMAT_UNKNOWN) {
773 continue; // unsupported by SDL atm.
774 }
775
776 struct v4l2_frmsizeenum frmsizeenum;
777 SDL_zero(frmsizeenum);
778 frmsizeenum.pixel_format = fmtdesc.pixelformat;
779
780 while (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsizeenum) == 0) {
781 if (frmsizeenum.type == V4L2_FRMSIZE_TYPE_DISCRETE) {
782 const int w = (int) frmsizeenum.discrete.width;
783 const int h = (int) frmsizeenum.discrete.height;
784 #if DEBUG_CAMERA
785 SDL_Log("CAMERA: * Has discrete size %dx%d", w, h);
786 #endif
787 if (!AddCameraFormat(fd, &add_data, sdlfmt, colorspace, fmtdesc.pixelformat, w, h)) {
788 break; // Probably out of memory; we'll go with what we have, if anything.
789 }
790 frmsizeenum.index++; // set up for the next one.
791 } else if ((frmsizeenum.type == V4L2_FRMSIZE_TYPE_STEPWISE) || (frmsizeenum.type == V4L2_FRMSIZE_TYPE_CONTINUOUS)) {
792 const int minw = (int) frmsizeenum.stepwise.min_width;
793 const int minh = (int) frmsizeenum.stepwise.min_height;
794 const int maxw = (int) frmsizeenum.stepwise.max_width;
795 const int maxh = (int) frmsizeenum.stepwise.max_height;
796 const int stepw = (int) frmsizeenum.stepwise.step_width;
797 const int steph = (int) frmsizeenum.stepwise.step_height;
798 for (int w = minw; w <= maxw; w += stepw) {
799 for (int h = minh; w <= maxh; w += steph) {
800 #if DEBUG_CAMERA
801 SDL_Log("CAMERA: * Has %s size %dx%d", (frmsizeenum.type == V4L2_FRMSIZE_TYPE_STEPWISE) ? "stepwise" : "continuous", w, h);
802 #endif
803 if (!AddCameraFormat(fd, &add_data, sdlfmt, colorspace, fmtdesc.pixelformat, w, h)) {
804 break; // Probably out of memory; we'll go with what we have, if anything.
805 }
806 }
807 }
808 break;
809 }
810 }
811 }
812
813 close(fd);
814
815 #if DEBUG_CAMERA
816 SDL_Log("CAMERA: (total specs: %d)", add_data.num_specs);
817 #endif
818
819 if (add_data.num_specs > 0) {
820 V4L2DeviceHandle *handle = (V4L2DeviceHandle *) SDL_calloc(1, sizeof (V4L2DeviceHandle));
821 if (handle) {
822 handle->path = SDL_strdup(path);
823 if (handle->path) {
824 handle->bus_info = SDL_strdup((char *)vcap.bus_info);
825 if (handle->bus_info) {
826 if (SDL_AddCamera((const char *) vcap.card, SDL_CAMERA_POSITION_UNKNOWN, add_data.num_specs, add_data.specs, handle)) {
827 SDL_free(add_data.specs);
828 return; // good to go.
829 }
830 SDL_free(handle->bus_info);
831 }
832 SDL_free(handle->path);
833 }
834 SDL_free(handle);
835 }
836 }
837 SDL_free(add_data.specs);
838}
839
840static void V4L2_FreeDeviceHandle(SDL_Camera *device)
841{
842 if (device) {
843 V4L2DeviceHandle *handle = (V4L2DeviceHandle *) device->handle;
844 SDL_free(handle->path);
845 SDL_free(handle->bus_info);
846 SDL_free(handle);
847 }
848}
849
850#ifdef SDL_USE_LIBUDEV
851static bool FindV4L2CameraByPathCallback(SDL_Camera *device, void *userdata)
852{
853 const V4L2DeviceHandle *handle = (const V4L2DeviceHandle *) device->handle;
854 return (SDL_strcmp(handle->path, (const char *) userdata) == 0);
855}
856
857static void MaybeRemoveDevice(const char *path)
858{
859 if (path) {
860 SDL_CameraDisconnected(SDL_FindPhysicalCameraByCallback(FindV4L2CameraByPathCallback, (void *) path));
861 }
862}
863
864static void CameraUdevCallback(SDL_UDEV_deviceevent udev_type, int udev_class, const char *devpath)
865{
866 if (devpath && (udev_class & SDL_UDEV_DEVICE_VIDEO_CAPTURE)) {
867 if (udev_type == SDL_UDEV_DEVICEADDED) {
868 MaybeAddDevice(devpath);
869 } else if (udev_type == SDL_UDEV_DEVICEREMOVED) {
870 MaybeRemoveDevice(devpath);
871 }
872 }
873}
874#endif // SDL_USE_LIBUDEV
875
876static void V4L2_Deinitialize(void)
877{
878#ifdef SDL_USE_LIBUDEV
879 SDL_UDEV_DelCallback(CameraUdevCallback);
880 SDL_UDEV_Quit();
881#endif // SDL_USE_LIBUDEV
882}
883
884static void V4L2_DetectDevices(void)
885{
886#ifdef SDL_USE_LIBUDEV
887 if (SDL_UDEV_Init()) {
888 if (SDL_UDEV_AddCallback(CameraUdevCallback)) {
889 SDL_UDEV_Scan(); // Force a scan to build the initial device list
890 }
891 return;
892 }
893#endif // SDL_USE_LIBUDEV
894
895 DIR *dirp = opendir("/dev");
896 if (dirp) {
897 struct dirent *dent;
898 while ((dent = readdir(dirp)) != NULL) {
899 int num = 0;
900 if (SDL_sscanf(dent->d_name, "video%d", &num) == 1) {
901 char fullpath[64];
902 SDL_snprintf(fullpath, sizeof (fullpath), "/dev/video%d", num);
903 MaybeAddDevice(fullpath);
904 }
905 }
906 closedir(dirp);
907 }
908}
909
910static bool V4L2_Init(SDL_CameraDriverImpl *impl)
911{
912 impl->DetectDevices = V4L2_DetectDevices;
913 impl->OpenDevice = V4L2_OpenDevice;
914 impl->CloseDevice = V4L2_CloseDevice;
915 impl->WaitDevice = V4L2_WaitDevice;
916 impl->AcquireFrame = V4L2_AcquireFrame;
917 impl->ReleaseFrame = V4L2_ReleaseFrame;
918 impl->FreeDeviceHandle = V4L2_FreeDeviceHandle;
919 impl->Deinitialize = V4L2_Deinitialize;
920
921 return true;
922}
923
924CameraBootStrap V4L2_bootstrap = {
925 "v4l2", "SDL Video4Linux2 camera driver", V4L2_Init, false
926};
927
928#endif // SDL_CAMERA_DRIVER_V4L2
929
930