4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / WVUVCDevice.m M
//
// Copyright © 2022 osy. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#import "WVUVCDevice.h"
#import <libusb.h>
#import <libuvc.h>

extern uvc_context_t *uvc_ctx;

@interface WVUVCDevice ()

@property uvc_device_t *device;
@property uvc_device_handle_t *handle;
@property (readwrite) NSString *name;

- (nullable instancetype)initWithDevice:(uvc_device_t *)device;

@end

@implementation WVUVCDevice

+ (NSArray<WVUVCDevice *> *)allDevices {
    NSMutableArray<WVUVCDevice *> *devices = [NSMutableArray array];
    uvc_device_t **devs;
    
    if (uvc_get_device_list(uvc_ctx, &devs) < 0) {
        return devices;
    }
    for (ssize_t i = 0; devs[i] != NULL; i++) {
        WVUVCDevice *device = [[WVUVCDevice alloc] initWithDevice:devs[i]];
        if (device) {
            [devices addObject:device];
        }
    }
    uvc_free_device_list(devs, 1);
    return devices;
}

- (instancetype)initWithDevice:(uvc_device_t *)device {
    if (self = [super init]) {
        self.device = device;
        self.name = [self createDescription];
        if (!self.name) {
            return nil;
        }
        uvc_ref_device(device);
    }
    return self;
}

- (void)dealloc {
    [self stopStream];
    uvc_unref_device(self.device);
}

- (nullable NSString *)createDescription {
    uvc_device_descriptor_t *desc;
    NSString *product;
    NSString *manufacturer;
    NSString *path;
    if (uvc_get_device_descriptor(self.device, &desc) < 0) {
        return nil;
    }
    path = [NSString stringWithFormat:@"%04X:%04X (bus %u, device %u)", desc->idVendor, desc->idProduct, uvc_get_bus_number(self.device), uvc_get_device_address(self.device)];
    if (desc->product) {
        product = [NSString stringWithCString:desc->product encoding:NSASCIIStringEncoding];
    }
    if (desc->manufacturer) {
        manufacturer = [NSString stringWithCString:desc->manufacturer encoding:NSASCIIStringEncoding];
    }
    uvc_free_device_descriptor(desc);
    if (product && manufacturer) {
        return [NSString stringWithFormat:@"%@ - %@ - %@", manufacturer, product, path];
    } else if (product) {
        return [NSString stringWithFormat:@"%@ - %@", product, path];
    } else if (manufacturer) {
        return [NSString stringWithFormat:@"%@ - %@", manufacturer, path];
    } else {
        return path;
    }
}

static NSError *uvcErrorToNSError(uvc_error_t res) {
    const char *errstr = uvc_strerror(res);
    if (errstr) {
        return [NSError errorWithDomain:@"com.osy86.WebcamViewer" code:-1 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedString(@"libuvc error: %s", "WVUVCDevice"), errstr]}];
    } else {
        return [NSError errorWithDomain:@"com.osy86.WebcamViewer" code:-1 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Unknown error.", "WVUVCDevice")}];
    }
}

static void streamCallback(struct uvc_frame *frame, void *user_ptr) {
    WVUVCDevice *self = (__bridge WVUVCDevice *)user_ptr;
    uvc_frame_t *rgb;
    uvc_error_t ret;
    
    /* We'll convert the image from YUV/JPEG to BGR, so allocate space */
    rgb = uvc_allocate_frame(frame->width * frame->height * 3);
    if (!rgb) {
        return;
    }
    
    /* Do the BGR conversion */
    // TODO: use CoreVideo to process the frame
    ret = uvc_any2rgb(frame, rgb);
    if (ret) {
        uvc_perror(ret, "uvc_any2rgb");
        uvc_free_frame(rgb);
        return;
    }
    CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, rgb->data, rgb->data_bytes, NULL);
    CGColorSpaceRef colorspace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
    CGImageRef cgImage = CGImageCreate(/* width              */ rgb->width,
                                       /* height             */ rgb->height,
                                       /* bitsPerComponent   */ 8,
                                       /* bitsPerPixel       */ 24,
                                       /* bytesPerRow        */ rgb->width * 3,
                                       /* colorspace         */ colorspace,
                                       /* bitmapInfo         */ kCGBitmapByteOrderDefault,
                                       /* provider           */ provider,
                                       /* decode             */ NULL,
                                       /* shouldInterpolate  */ YES,
                                       /* intent             */ kCGRenderingIntentDefault);
    CGColorSpaceRelease(colorspace);
    CGDataProviderRelease(provider);
    CGImageRef copy = CGImageCreateCopy(cgImage);
    CGImageRelease(cgImage);
    [self.delegate uvcDevice:self didRecieveFrame:copy];
    CGImageRelease(copy);
    
    uvc_free_frame(rgb);
}

- (BOOL)startStreamWithError:(NSError *__autoreleasing  _Nullable *)error {
    uvc_error_t res = UVC_SUCCESS;
    uvc_device_handle_t *devh = NULL;
    uvc_stream_ctrl_t ctrl;
    NSLog(@"Opening device (%@)...", self.name);
    if ((res = uvc_open(self.device, &devh)) < 0) {
        goto err;
    }
    NSLog(@"Device opened.");
    uvc_print_diag(devh, stderr);
    enum uvc_frame_format frame_format = UVC_FRAME_FORMAT_ANY;
    int width = 640;
    int height = 480;
    int fps = 30;
    for (const uvc_format_desc_t *format_desc = uvc_get_format_descs(devh);
         format_desc;
         format_desc = format_desc->next) {
        if (format_desc->bDescriptorSubtype != UVC_VS_FORMAT_UNCOMPRESSED) {
            continue;
        }
        for (const uvc_frame_desc_t *frame_desc = format_desc->frame_descs;
             frame_desc;
             frame_desc = frame_desc->next) {
            if (frame_desc->wWidth > 640) {
                continue;
            }
            if (frame_desc->wHeight > 480) {
                continue;
            }
            frame_format = UVC_FRAME_FORMAT_UNCOMPRESSED;
            width = frame_desc->wWidth;
            height = frame_desc->wHeight;
            fps = 10000000 / frame_desc->dwDefaultFrameInterval;
            break;
        }
        if (frame_format != UVC_FRAME_FORMAT_ANY) {
            break;
        }
    }
    if (frame_format == UVC_FRAME_FORMAT_ANY) {
        NSLog(@"Cannot find supported format");
        res = UVC_ERROR_NOT_SUPPORTED;
        goto err;
    }
    NSLog(@"Found format: (%x) %dx%d %dfps", frame_format, width, height, fps);
    NSLog(@"Negotiate stream profile...");
    if ((res = uvc_get_stream_ctrl_format_size(devh, &ctrl, frame_format, width, height, fps)) < 0) {
        goto err;
    }
    uvc_print_stream_ctrl(&ctrl, stderr);
    NSLog(@"Starting stream...");
    libusb_set_option(NULL, LIBUSB_OPTION_LOG_LEVEL, LIBUSB_LOG_LEVEL_ERROR);
    if ((res = uvc_start_streaming(devh, &ctrl, streamCallback, (__bridge void *)self, 0)) < 0) {
        goto err;
    }
    NSLog(@"Stream started.");
    self.handle = devh;
    [self setDefaultOptions];
    return YES;
err:
    if (error) {
        *error = uvcErrorToNSError(res);
    }
    if (devh) {
        uvc_close(devh);
    }
    return NO;
}

- (void)stopStream {
    if (self.handle) {
        NSLog(@"Stopping stream...");
        uvc_stop_streaming(self.handle);
        NSLog(@"Stream stopped.");
        libusb_set_option(NULL, LIBUSB_OPTION_LOG_LEVEL, LIBUSB_LOG_LEVEL_DEBUG);
        uvc_close(self.handle);
        self.handle = NULL;
    }
}

- (void)setDefaultOptions {
    uvc_device_handle_t *devh = self.handle;
    uvc_error_t res = UVC_SUCCESS;
    /* enable auto exposure - see uvc_set_ae_mode documentation */
    NSLog(@"Enabling auto exposure ...");
    const uint8_t UVC_AUTO_EXPOSURE_MODE_AUTO = 2;
    res = uvc_set_ae_mode(devh, UVC_AUTO_EXPOSURE_MODE_AUTO);
    if (res == UVC_SUCCESS) {
        NSLog(@" ... enabled auto exposure");
    } else if (res == UVC_ERROR_PIPE) {
        /* this error indicates that the camera does not support the full AE mode;
         * try again, using aperture priority mode (fixed aperture, variable exposure time) */
        NSLog(@" ... full AE not supported, trying aperture priority mode");
        const uint8_t UVC_AUTO_EXPOSURE_MODE_APERTURE_PRIORITY = 8;
        res = uvc_set_ae_mode(devh, UVC_AUTO_EXPOSURE_MODE_APERTURE_PRIORITY);
        if (res < 0) {
            uvc_perror(res, " ... uvc_set_ae_mode failed to enable aperture priority mode");
        } else {
            NSLog(@" ... enabled aperture priority auto exposure mode");
        }
    } else {
        uvc_perror(res, " ... uvc_set_ae_mode failed to enable auto exposure mode");
    }
}

@end