[libcamera-devel,v2,13/19] py: cam: Convert ctx and state to classes
diff mbox series

Message ID 20220524114610.41848-14-tomi.valkeinen@ideasonboard.com
State Accepted
Headers show
Series
  • More misc Python patches
Related show

Commit Message

Tomi Valkeinen May 24, 2022, 11:46 a.m. UTC
From: Tomi Valkeinen <tomi.valkeinen@iki.fi>

Convert ctx and state dicts to classes. No functional changes.

Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
---
 src/py/cam/cam.py      | 585 +++++++++++++++++++++--------------------
 src/py/cam/cam_kms.py  |  12 +-
 src/py/cam/cam_null.py |   8 +-
 src/py/cam/cam_qt.py   |  16 +-
 src/py/cam/cam_qtgl.py |  22 +-
 5 files changed, 327 insertions(+), 316 deletions(-)

Comments

Laurent Pinchart May 27, 2022, 6:53 a.m. UTC | #1
Hi Tomi,

Thank you for the patch.

On Tue, May 24, 2022 at 02:46:04PM +0300, Tomi Valkeinen wrote:
> From: Tomi Valkeinen <tomi.valkeinen@iki.fi>
> 
> Convert ctx and state dicts to classes. No functional changes.
> 
> Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
> ---
>  src/py/cam/cam.py      | 585 +++++++++++++++++++++--------------------
>  src/py/cam/cam_kms.py  |  12 +-
>  src/py/cam/cam_null.py |   8 +-
>  src/py/cam/cam_qt.py   |  16 +-
>  src/py/cam/cam_qtgl.py |  22 +-
>  5 files changed, 327 insertions(+), 316 deletions(-)
> 
> diff --git a/src/py/cam/cam.py b/src/py/cam/cam.py
> index 64f67e86..f6e8232c 100755
> --- a/src/py/cam/cam.py
> +++ b/src/py/cam/cam.py
> @@ -6,6 +6,7 @@
>  # \todo Convert ctx and state dicts to proper classes, and move relevant
>  #       functions to those classes.
>  
> +from typing import Any
>  import argparse
>  import binascii
>  import libcamera as libcam
> @@ -14,379 +15,400 @@ import sys
>  import traceback
>  
>  
> -class CustomAction(argparse.Action):
> -    def __init__(self, option_strings, dest, **kwargs):
> -        super().__init__(option_strings, dest, default={}, **kwargs)
> +class CameraContext:
> +    camera: libcam.Camera
> +    id: str
> +    idx: int
>  
> -    def __call__(self, parser, namespace, values, option_string=None):
> -        if len(namespace.camera) == 0:
> -            print(f'Option {option_string} requires a --camera context')
> -            sys.exit(-1)
> +    opt_stream: str
> +    opt_strict_formats: bool
> +    opt_crc: bool
> +    opt_metadata: bool
> +    opt_save_frames: bool
> +    opt_capture: int
>  
> -        if self.type == bool:
> -            values = True
> +    stream_names: dict[libcam.Stream, str]
> +    streams: list[libcam.Stream]
> +    allocator: libcam.FrameBufferAllocator
> +    requests: list[libcam.Request]
> +    reqs_queued: int
> +    reqs_completed: int
> +    last: int = 0
> +    fps: float
>  
> -        current = namespace.camera[-1]
> +    def __init__(self, camera, idx):
> +        self.camera = camera
> +        self.idx = idx
> +        self.id = 'cam' + str(idx)
> +        self.reqs_queued = 0
> +        self.reqs_completed = 0
>  
> -        data = getattr(namespace, self.dest)
> +    def do_cmd_list_props(self):
> +        camera = self.camera
>  
> -        if self.nargs == '+':
> -            if current not in data:
> -                data[current] = []
> +        print('Properties for', self.id)
>  
> -            data[current] += values
> -        else:
> -            data[current] = values
> +        for name, prop in camera.properties.items():

You can use self.camera here and drop the local variable. Same in a few
other functions below.

> +            print('\t{}: {}'.format(name, prop))
>  
> +    def do_cmd_list_controls(self):
> +        camera = self.camera
>  
> -def do_cmd_list(cm):
> -    print('Available cameras:')
> -
> -    for idx, c in enumerate(cm.cameras):
> -        print(f'{idx + 1}: {c.id}')
> -
> -
> -def do_cmd_list_props(ctx):
> -    camera = ctx['camera']
> -
> -    print('Properties for', ctx['id'])
> -
> -    for name, prop in camera.properties.items():
> -        print('\t{}: {}'.format(name, prop))
> -
> -
> -def do_cmd_list_controls(ctx):
> -    camera = ctx['camera']
> -
> -    print('Controls for', ctx['id'])
> -
> -    for name, prop in camera.controls.items():
> -        print('\t{}: {}'.format(name, prop))
> +        print('Controls for', self.id)
>  
> +        for name, prop in camera.controls.items():
> +            print('\t{}: {}'.format(name, prop))
>  
> -def do_cmd_info(ctx):
> -    camera = ctx['camera']
> +    def do_cmd_info(self):
> +        camera = self.camera
>  
> -    print('Stream info for', ctx['id'])
> +        print('Stream info for', self.id)
>  
> -    roles = [libcam.StreamRole.Viewfinder]
> +        roles = [libcam.StreamRole.Viewfinder]
>  
> -    camconfig = camera.generate_configuration(roles)
> -    if camconfig is None:
> -        raise Exception('Generating config failed')
> +        camconfig = camera.generate_configuration(roles)
> +        if camconfig is None:
> +            raise Exception('Generating config failed')
>  
> -    for i, stream_config in enumerate(camconfig):
> -        print('\t{}: {}'.format(i, stream_config))
> +        for i, stream_config in enumerate(camconfig):
> +            print('\t{}: {}'.format(i, stream_config))
>  
> -        formats = stream_config.formats
> -        for fmt in formats.pixel_formats:
> -            print('\t * Pixelformat:', fmt, formats.range(fmt))
> +            formats = stream_config.formats
> +            for fmt in formats.pixel_formats:
> +                print('\t * Pixelformat:', fmt, formats.range(fmt))
>  
> -            for size in formats.sizes(fmt):
> -                print('\t  -', size)
> +                for size in formats.sizes(fmt):
> +                    print('\t  -', size)
>  
> +    def acquire(self):
> +        camera = self.camera
>  
> -def acquire(ctx):
> -    camera = ctx['camera']
> +        camera.acquire()
>  
> -    camera.acquire()
> +    def release(self):
> +        camera = self.camera
>  
> +        camera.release()
>  
> -def release(ctx):
> -    camera = ctx['camera']
> +    def __parse_streams(self):
> +        streams = []
>  
> -    camera.release()
> +        for stream_desc in self.opt_stream:
> +            stream_opts: dict[str, Any]
> +            stream_opts = {'role': libcam.StreamRole.Viewfinder}
>  
> +            for stream_opt in stream_desc.split(','):
> +                if stream_opt == 0:
> +                    continue
>  
> -def parse_streams(ctx):
> -    streams = []
> -
> -    for stream_desc in ctx['opt-stream']:
> -        stream_opts = {'role': libcam.StreamRole.Viewfinder}
> -
> -        for stream_opt in stream_desc.split(','):
> -            if stream_opt == 0:
> -                continue
> -
> -            arr = stream_opt.split('=')
> -            if len(arr) != 2:
> -                print('Bad stream option', stream_opt)
> -                sys.exit(-1)
> -
> -            key = arr[0]
> -            value = arr[1]
> -
> -            if key in ['width', 'height']:
> -                value = int(value)
> -            elif key == 'role':
> -                rolemap = {
> -                    'still': libcam.StreamRole.StillCapture,
> -                    'raw': libcam.StreamRole.Raw,
> -                    'video': libcam.StreamRole.VideoRecording,
> -                    'viewfinder': libcam.StreamRole.Viewfinder,
> -                }
> -
> -                role = rolemap.get(value.lower(), None)
> +                arr = stream_opt.split('=')
> +                if len(arr) != 2:
> +                    print('Bad stream option', stream_opt)
> +                    sys.exit(-1)

Raising an exception would be better, but that's a candidate for another
patch as it's a functional change.

>  
> -                if role is None:
> -                    print('Bad stream role', value)
> +                key = arr[0]
> +                value = arr[1]
> +
> +                if key in ['width', 'height']:
> +                    value = int(value)
> +                elif key == 'role':
> +                    rolemap = {
> +                        'still': libcam.StreamRole.StillCapture,
> +                        'raw': libcam.StreamRole.Raw,
> +                        'video': libcam.StreamRole.VideoRecording,
> +                        'viewfinder': libcam.StreamRole.Viewfinder,
> +                    }
> +
> +                    role = rolemap.get(value.lower(), None)
> +
> +                    if role is None:
> +                        print('Bad stream role', value)
> +                        sys.exit(-1)
> +
> +                    value = role
> +                elif key == 'pixelformat':
> +                    pass
> +                else:
> +                    print('Bad stream option key', key)
>                      sys.exit(-1)
>  
> -                value = role
> -            elif key == 'pixelformat':
> -                pass
> -            else:
> -                print('Bad stream option key', key)
> -                sys.exit(-1)
> +                stream_opts[key] = value
>  
> -            stream_opts[key] = value
> +            streams.append(stream_opts)
>  
> -        streams.append(stream_opts)
> +        return streams
>  
> -    return streams
> +    def configure(self):
> +        camera = self.camera
>  
> +        streams = self.__parse_streams()
>  
> -def configure(ctx):
> -    camera = ctx['camera']
> +        roles = [opts['role'] for opts in streams]
>  
> -    streams = parse_streams(ctx)
> +        camconfig = camera.generate_configuration(roles)
> +        if camconfig is None:
> +            raise Exception('Generating config failed')
>  
> -    roles = [opts['role'] for opts in streams]
> +        for idx, stream_opts in enumerate(streams):
> +            stream_config = camconfig.at(idx)
>  
> -    camconfig = camera.generate_configuration(roles)
> -    if camconfig is None:
> -        raise Exception('Generating config failed')
> +            if 'width' in stream_opts:
> +                stream_config.size.width = stream_opts['width']
>  
> -    for idx, stream_opts in enumerate(streams):
> -        stream_config = camconfig.at(idx)
> +            if 'height' in stream_opts:
> +                stream_config.size.height = stream_opts['height']
>  
> -        if 'width' in stream_opts:
> -            stream_config.size.width = stream_opts['width']
> +            if 'pixelformat' in stream_opts:
> +                stream_config.pixel_format = libcam.PixelFormat(stream_opts['pixelformat'])
>  
> -        if 'height' in stream_opts:
> -            stream_config.size.height = stream_opts['height']
> +        stat = camconfig.validate()
>  
> -        if 'pixelformat' in stream_opts:
> -            stream_config.pixel_format = libcam.PixelFormat(stream_opts['pixelformat'])
> +        if stat == libcam.CameraConfiguration.Status.Invalid:
> +            print('Camera configuration invalid')
> +            exit(-1)
> +        elif stat == libcam.CameraConfiguration.Status.Adjusted:
> +            if self.opt_strict_formats:
> +                print('Adjusting camera configuration disallowed by --strict-formats argument')
> +                exit(-1)
>  
> -    stat = camconfig.validate()
> +            print('Camera configuration adjusted')
>  
> -    if stat == libcam.CameraConfiguration.Status.Invalid:
> -        print('Camera configuration invalid')
> -        exit(-1)
> -    elif stat == libcam.CameraConfiguration.Status.Adjusted:
> -        if ctx['opt-strict-formats']:
> -            print('Adjusting camera configuration disallowed by --strict-formats argument')
> -            exit(-1)
> +        r = camera.configure(camconfig)
> +        if r != 0:
> +            raise Exception('Configure failed')
>  
> -        print('Camera configuration adjusted')
> +        self.stream_names = {}
> +        self.streams = []
>  
> -    r = camera.configure(camconfig)
> -    if r != 0:
> -        raise Exception('Configure failed')
> +        for idx, stream_config in enumerate(camconfig):
> +            stream = stream_config.stream
> +            self.streams.append(stream)
> +            self.stream_names[stream] = 'stream' + str(idx)
> +            print('{}-{}: stream config {}'.format(self.id, self.stream_names[stream], stream.configuration))
>  
> -    ctx['stream-names'] = {}
> -    ctx['streams'] = []
> +    def alloc_buffers(self):
> +        camera = self.camera
>  
> -    for idx, stream_config in enumerate(camconfig):
> -        stream = stream_config.stream
> -        ctx['streams'].append(stream)
> -        ctx['stream-names'][stream] = 'stream' + str(idx)
> -        print('{}-{}: stream config {}'.format(ctx['id'], ctx['stream-names'][stream], stream.configuration))
> +        allocator = libcam.FrameBufferAllocator(camera)
>  
> +        for stream in self.streams:
> +            ret = allocator.allocate(stream)
> +            if ret < 0:
> +                print('Cannot allocate buffers')
> +                exit(-1)
>  
> -def alloc_buffers(ctx):
> -    camera = ctx['camera']
> +            allocated = len(allocator.buffers(stream))
>  
> -    allocator = libcam.FrameBufferAllocator(camera)
> +            print('{}-{}: Allocated {} buffers'.format(self.id, self.stream_names[stream], allocated))
>  
> -    for idx, stream in enumerate(ctx['streams']):
> -        ret = allocator.allocate(stream)
> -        if ret < 0:
> -            print('Cannot allocate buffers')
> -            exit(-1)
> +        self.allocator = allocator
>  
> -        allocated = len(allocator.buffers(stream))
> +    def create_requests(self):
> +        camera = self.camera
>  
> -        print('{}-{}: Allocated {} buffers'.format(ctx['id'], ctx['stream-names'][stream], allocated))
> +        self.requests = []
>  
> -    ctx['allocator'] = allocator
> +        # Identify the stream with the least number of buffers
> +        num_bufs = min([len(self.allocator.buffers(stream)) for stream in self.streams])
>  
> +        requests = []
>  
> -def create_requests(ctx):
> -    camera = ctx['camera']
> +        for buf_num in range(num_bufs):
> +            request = camera.create_request(self.idx)
>  
> -    ctx['requests'] = []
> +            if request is None:
> +                print('Can not create request')
> +                exit(-1)
>  
> -    # Identify the stream with the least number of buffers
> -    num_bufs = min([len(ctx['allocator'].buffers(stream)) for stream in ctx['streams']])
> +            for stream in self.streams:
> +                buffers = self.allocator.buffers(stream)
> +                buffer = buffers[buf_num]
>  
> -    requests = []
> +                ret = request.add_buffer(stream, buffer)
> +                if ret < 0:
> +                    print('Can not set buffer for request')
> +                    exit(-1)
>  
> -    for buf_num in range(num_bufs):
> -        request = camera.create_request(ctx['idx'])
> +            requests.append(request)
>  
> -        if request is None:
> -            print('Can not create request')
> -            exit(-1)
> +        self.requests = requests
>  
> -        for stream in ctx['streams']:
> -            buffers = ctx['allocator'].buffers(stream)
> -            buffer = buffers[buf_num]
> +    def start(self):
> +        camera = self.camera
>  
> -            ret = request.add_buffer(stream, buffer)
> -            if ret < 0:
> -                print('Can not set buffer for request')
> -                exit(-1)
> +        camera.start()
>  
> -        requests.append(request)
> +    def stop(self):
> +        camera = self.camera
>  
> -    ctx['requests'] = requests
> +        camera.stop()
>  
> +    def queue_requests(self):
> +        camera = self.camera
>  
> -def start(ctx):
> -    camera = ctx['camera']
> +        for request in self.requests:
> +            camera.queue_request(request)
> +            self.reqs_queued += 1
>  
> -    camera.start()
> +        del self.requests
>  
>  
> -def stop(ctx):
> -    camera = ctx['camera']
> +class CaptureState:
> +    cm: libcam.CameraManager
> +    contexts: list[CameraContext]
> +    renderer: Any
>  
> -    camera.stop()
> +    def __init__(self, cm, contexts):
> +        self.cm = cm
> +        self.contexts = contexts
>  
> +    # Called from renderer when there is a libcamera event
> +    def event_handler(self):
> +        try:
> +            cm = self.cm
> +            contexts = self.contexts

Same here, these local variables could possibly be dropped.

Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>

>  
> -def queue_requests(ctx):
> -    camera = ctx['camera']
> +            cm.read_event()
>  
> -    for request in ctx['requests']:
> -        camera.queue_request(request)
> -        ctx['reqs-queued'] += 1
> +            reqs = cm.get_ready_requests()
>  
> -    del ctx['requests']
> +            for req in reqs:
> +                ctx = next(ctx for ctx in contexts if ctx.idx == req.cookie)
> +                self.__request_handler(ctx, req)
>  
> +            running = any(ctx.reqs_completed < ctx.opt_capture for ctx in contexts)
> +            return running
> +        except Exception:
> +            traceback.print_exc()
> +            return False
>  
> -def capture_init(contexts):
> -    for ctx in contexts:
> -        acquire(ctx)
> +    def __request_handler(self, ctx, req):
> +        if req.status != libcam.Request.Status.Complete:
> +            raise Exception('{}: Request failed: {}'.format(ctx.id, req.status))
>  
> -    for ctx in contexts:
> -        configure(ctx)
> +        buffers = req.buffers
>  
> -    for ctx in contexts:
> -        alloc_buffers(ctx)
> +        # Compute the frame rate. The timestamp is arbitrarily retrieved from
> +        # the first buffer, as all buffers should have matching timestamps.
> +        ts = buffers[next(iter(buffers))].metadata.timestamp
> +        last = ctx.last
> +        fps = 1000000000.0 / (ts - last) if (last != 0 and (ts - last) != 0) else 0
> +        ctx.last = ts
> +        ctx.fps = fps
>  
> -    for ctx in contexts:
> -        create_requests(ctx)
> +        for stream, fb in buffers.items():
> +            stream_name = ctx.stream_names[stream]
>  
> +            crcs = []
> +            if ctx.opt_crc:
> +                with libcamera.utils.MappedFrameBuffer(fb) as mfb:
> +                    plane_crcs = [binascii.crc32(p) for p in mfb.planes]
> +                    crcs.append(plane_crcs)
>  
> -def capture_start(contexts):
> -    for ctx in contexts:
> -        start(ctx)
> +            meta = fb.metadata
>  
> -    for ctx in contexts:
> -        queue_requests(ctx)
> +            print('{:.6f} ({:.2f} fps) {}-{}: seq {}, bytes {}, CRCs {}'
> +                  .format(ts / 1000000000, fps,
> +                          ctx.id, stream_name,
> +                          meta.sequence, meta.bytesused,
> +                          crcs))
>  
> +            if ctx.opt_metadata:
> +                reqmeta = req.metadata
> +                for ctrl, val in reqmeta.items():
> +                    print(f'\t{ctrl} = {val}')
>  
> -# Called from renderer when there is a libcamera event
> -def event_handler(state):
> -    try:
> -        cm = state['cm']
> -        contexts = state['contexts']
> +            if ctx.opt_save_frames:
> +                with libcamera.utils.MappedFrameBuffer(fb) as mfb:
> +                    filename = 'frame-{}-{}-{}.data'.format(ctx.id, stream_name, ctx.reqs_completed)
> +                    with open(filename, 'wb') as f:
> +                        for p in mfb.planes:
> +                            f.write(p)
>  
> -        cm.read_event()
> +        self.renderer.request_handler(ctx, req)
>  
> -        reqs = cm.get_ready_requests()
> +        ctx.reqs_completed += 1
>  
> -        for req in reqs:
> -            ctx = next(ctx for ctx in contexts if ctx['idx'] == req.cookie)
> -            request_handler(state, ctx, req)
> +    # Called from renderer when it has finished with a request
> +    def request_processed(self, ctx, req):
> +        camera = ctx.camera
>  
> -        running = any(ctx['reqs-completed'] < ctx['opt-capture'] for ctx in contexts)
> -        return running
> -    except Exception:
> -        traceback.print_exc()
> -        return False
> +        if ctx.reqs_queued < ctx.opt_capture:
> +            req.reuse()
> +            camera.queue_request(req)
> +            ctx.reqs_queued += 1
>  
> +    def __capture_init(self):
> +        for ctx in self.contexts:
> +            ctx.acquire()
>  
> -def request_handler(state, ctx, req):
> -    if req.status != libcam.Request.Status.Complete:
> -        raise Exception('{}: Request failed: {}'.format(ctx['id'], req.status))
> +        for ctx in self.contexts:
> +            ctx.configure()
>  
> -    buffers = req.buffers
> +        for ctx in self.contexts:
> +            ctx.alloc_buffers()
>  
> -    # Compute the frame rate. The timestamp is arbitrarily retrieved from
> -    # the first buffer, as all buffers should have matching timestamps.
> -    ts = buffers[next(iter(buffers))].metadata.timestamp
> -    last = ctx.get('last', 0)
> -    fps = 1000000000.0 / (ts - last) if (last != 0 and (ts - last) != 0) else 0
> -    ctx['last'] = ts
> -    ctx['fps'] = fps
> +        for ctx in self.contexts:
> +            ctx.create_requests()
>  
> -    for stream, fb in buffers.items():
> -        stream_name = ctx['stream-names'][stream]
> +    def __capture_start(self):
> +        for ctx in self.contexts:
> +            ctx.start()
>  
> -        crcs = []
> -        if ctx['opt-crc']:
> -            with libcamera.utils.MappedFrameBuffer(fb) as mfb:
> -                plane_crcs = [binascii.crc32(p) for p in mfb.planes]
> -                crcs.append(plane_crcs)
> +        for ctx in self.contexts:
> +            ctx.queue_requests()
>  
> -        meta = fb.metadata
> +    def __capture_deinit(self):
> +        for ctx in self.contexts:
> +            ctx.stop()
>  
> -        print('{:.6f} ({:.2f} fps) {}-{}: seq {}, bytes {}, CRCs {}'
> -              .format(ts / 1000000000, fps,
> -                      ctx['id'], stream_name,
> -                      meta.sequence, meta.bytesused,
> -                      crcs))
> +        for ctx in self.contexts:
> +            ctx.release()
>  
> -        if ctx['opt-metadata']:
> -            reqmeta = req.metadata
> -            for ctrl, val in reqmeta.items():
> -                print(f'\t{ctrl} = {val}')
> +    def do_cmd_capture(self):
> +        self.__capture_init()
>  
> -        if ctx['opt-save-frames']:
> -            with libcamera.utils.MappedFrameBuffer(fb) as mfb:
> -                filename = 'frame-{}-{}-{}.data'.format(ctx['id'], stream_name, ctx['reqs-completed'])
> -                with open(filename, 'wb') as f:
> -                    for p in mfb.planes:
> -                        f.write(p)
> +        renderer = self.renderer
>  
> -    state['renderer'].request_handler(ctx, req)
> +        renderer.setup()
>  
> -    ctx['reqs-completed'] += 1
> +        self.__capture_start()
>  
> +        renderer.run()
>  
> -# Called from renderer when it has finished with a request
> -def request_prcessed(ctx, req):
> -    camera = ctx['camera']
> +        self.__capture_deinit()
>  
> -    if ctx['reqs-queued'] < ctx['opt-capture']:
> -        req.reuse()
> -        camera.queue_request(req)
> -        ctx['reqs-queued'] += 1
>  
> +class CustomAction(argparse.Action):
> +    def __init__(self, option_strings, dest, **kwargs):
> +        super().__init__(option_strings, dest, default={}, **kwargs)
>  
> -def capture_deinit(contexts):
> -    for ctx in contexts:
> -        stop(ctx)
> +    def __call__(self, parser, namespace, values, option_string=None):
> +        if len(namespace.camera) == 0:
> +            print(f'Option {option_string} requires a --camera context')
> +            sys.exit(-1)
>  
> -    for ctx in contexts:
> -        release(ctx)
> +        if self.type == bool:
> +            values = True
>  
> +        current = namespace.camera[-1]
>  
> -def do_cmd_capture(state):
> -    capture_init(state['contexts'])
> +        data = getattr(namespace, self.dest)
>  
> -    renderer = state['renderer']
> +        if self.nargs == '+':
> +            if current not in data:
> +                data[current] = []
>  
> -    renderer.setup()
> +            data[current] += values
> +        else:
> +            data[current] = values
>  
> -    capture_start(state['contexts'])
>  
> -    renderer.run()
> +def do_cmd_list(cm):
> +    print('Available cameras:')
>  
> -    capture_deinit(state['contexts'])
> +    for idx, c in enumerate(cm.cameras):
> +        print(f'{idx + 1}: {c.id}')
>  
>  
>  def main():
> @@ -422,39 +444,28 @@ def main():
>              print('Unable to find camera', cam_idx)
>              return -1
>  
> -        contexts.append({
> -                        'camera': camera,
> -                        'idx': cam_idx,
> -                        'id': 'cam' + str(cam_idx),
> -                        'reqs-queued': 0,
> -                        'reqs-completed': 0,
> -                        'opt-capture': args.capture.get(cam_idx, False),
> -                        'opt-crc': args.crc.get(cam_idx, False),
> -                        'opt-save-frames': args.save_frames.get(cam_idx, False),
> -                        'opt-metadata': args.metadata.get(cam_idx, False),
> -                        'opt-strict-formats': args.strict_formats.get(cam_idx, False),
> -                        'opt-stream': args.stream.get(cam_idx, ['role=viewfinder']),
> -                        })
> +        ctx = CameraContext(camera, cam_idx)
> +        ctx.opt_capture = args.capture.get(cam_idx, 0)
> +        ctx.opt_crc = args.crc.get(cam_idx, False)
> +        ctx.opt_save_frames = args.save_frames.get(cam_idx, False)
> +        ctx.opt_metadata = args.metadata.get(cam_idx, False)
> +        ctx.opt_strict_formats = args.strict_formats.get(cam_idx, False)
> +        ctx.opt_stream = args.stream.get(cam_idx, ['role=viewfinder'])
> +        contexts.append(ctx)
>  
>      for ctx in contexts:
> -        print('Using camera {} as {}'.format(ctx['camera'].id, ctx['id']))
> +        print('Using camera {} as {}'.format(ctx.camera.id, ctx.id))
>  
>      for ctx in contexts:
>          if args.list_properties:
> -            do_cmd_list_props(ctx)
> +            ctx.do_cmd_list_props()
>          if args.list_controls:
> -            do_cmd_list_controls(ctx)
> +            ctx.do_cmd_list_controls()
>          if args.info:
> -            do_cmd_info(ctx)
> +            ctx.do_cmd_info()
>  
>      if args.capture:
> -
> -        state = {
> -            'cm': cm,
> -            'contexts': contexts,
> -            'event_handler': event_handler,
> -            'request_prcessed': request_prcessed,
> -        }
> +        state = CaptureState(cm, contexts)
>  
>          if args.renderer == 'null':
>              import cam_null
> @@ -472,9 +483,9 @@ def main():
>              print('Bad renderer', args.renderer)
>              return -1
>  
> -        state['renderer'] = renderer
> +        state.renderer = renderer
>  
> -        do_cmd_capture(state)
> +        state.do_cmd_capture()
>  
>      return 0
>  
> diff --git a/src/py/cam/cam_kms.py b/src/py/cam/cam_kms.py
> index 74cd3b38..213e0b03 100644
> --- a/src/py/cam/cam_kms.py
> +++ b/src/py/cam/cam_kms.py
> @@ -10,8 +10,8 @@ class KMSRenderer:
>      def __init__(self, state):
>          self.state = state
>  
> -        self.cm = state['cm']
> -        self.contexts = state['contexts']
> +        self.cm = state.cm
> +        self.contexts = state.contexts
>          self.running = False
>  
>          card = pykms.Card()
> @@ -92,7 +92,7 @@ class KMSRenderer:
>          if old:
>              req = old['camreq']
>              ctx = old['camctx']
> -            self.state['request_prcessed'](ctx, req)
> +            self.state.request_processed(ctx, req)
>  
>      def queue(self, drmreq):
>          if not self.next:
> @@ -108,7 +108,7 @@ class KMSRenderer:
>  
>          idx = 0
>          for ctx in self.contexts:
> -            for stream in ctx['streams']:
> +            for stream in ctx.streams:
>  
>                  cfg = stream.configuration
>                  fmt = cfg.pixel_format
> @@ -125,7 +125,7 @@ class KMSRenderer:
>                      'size': cfg.size,
>                  })
>  
> -                for fb in ctx['allocator'].buffers(stream):
> +                for fb in ctx.allocator.buffers(stream):
>                      w = cfg.size.width
>                      h = cfg.size.height
>                      fds = []
> @@ -148,7 +148,7 @@ class KMSRenderer:
>                  self.handle_page_flip(ev.seq, ev.time)
>  
>      def readcam(self, fd):
> -        self.running = self.state['event_handler'](self.state)
> +        self.running = self.state.event_handler()
>  
>      def readkey(self, fileobj):
>          sys.stdin.readline()
> diff --git a/src/py/cam/cam_null.py b/src/py/cam/cam_null.py
> index a6da9671..45c5f467 100644
> --- a/src/py/cam/cam_null.py
> +++ b/src/py/cam/cam_null.py
> @@ -9,8 +9,8 @@ class NullRenderer:
>      def __init__(self, state):
>          self.state = state
>  
> -        self.cm = state['cm']
> -        self.contexts = state['contexts']
> +        self.cm = state.cm
> +        self.contexts = state.contexts
>  
>          self.running = False
>  
> @@ -37,11 +37,11 @@ class NullRenderer:
>          print('Exiting...')
>  
>      def readcam(self, fd):
> -        self.running = self.state['event_handler'](self.state)
> +        self.running = self.state.event_handler()
>  
>      def readkey(self, fileobj):
>          sys.stdin.readline()
>          self.running = False
>  
>      def request_handler(self, ctx, req):
> -        self.state['request_prcessed'](ctx, req)
> +        self.state.request_processed(ctx, req)
> diff --git a/src/py/cam/cam_qt.py b/src/py/cam/cam_qt.py
> index 03096920..d638e9cc 100644
> --- a/src/py/cam/cam_qt.py
> +++ b/src/py/cam/cam_qt.py
> @@ -176,8 +176,8 @@ class QtRenderer:
>      def __init__(self, state):
>          self.state = state
>  
> -        self.cm = state['cm']
> -        self.contexts = state['contexts']
> +        self.cm = state.cm
> +        self.contexts = state.contexts
>  
>      def setup(self):
>          self.app = QtWidgets.QApplication([])
> @@ -185,7 +185,7 @@ class QtRenderer:
>          windows = []
>  
>          for ctx in self.contexts:
> -            for stream in ctx['streams']:
> +            for stream in ctx.streams:
>                  window = MainWindow(ctx, stream)
>                  window.show()
>                  windows.append(window)
> @@ -206,7 +206,7 @@ class QtRenderer:
>          print('Exiting...')
>  
>      def readcam(self):
> -        running = self.state['event_handler'](self.state)
> +        running = self.state.event_handler()
>  
>          if not running:
>              self.app.quit()
> @@ -223,7 +223,7 @@ class QtRenderer:
>  
>              wnd.handle_request(stream, fb)
>  
> -        self.state['request_prcessed'](ctx, req)
> +        self.state.request_processed(ctx, req)
>  
>      def cleanup(self):
>          for w in self.windows:
> @@ -254,7 +254,7 @@ class MainWindow(QtWidgets.QWidget):
>          group.setLayout(groupLayout)
>          controlsLayout.addWidget(group)
>  
> -        lab = QtWidgets.QLabel(ctx['id'])
> +        lab = QtWidgets.QLabel(ctx.id)
>          groupLayout.addWidget(lab)
>  
>          self.frameLabel = QtWidgets.QLabel()
> @@ -265,7 +265,7 @@ class MainWindow(QtWidgets.QWidget):
>          group.setLayout(groupLayout)
>          controlsLayout.addWidget(group)
>  
> -        camera = ctx['camera']
> +        camera = ctx.camera
>  
>          for k, v in camera.properties.items():
>              lab = QtWidgets.QLabel()
> @@ -308,4 +308,4 @@ class MainWindow(QtWidgets.QWidget):
>          self.label.setPixmap(pix)
>  
>          self.frameLabel.setText('Queued: {}\nDone: {}\nFps: {:.2f}'
> -                                .format(ctx['reqs-queued'], ctx['reqs-completed'], ctx['fps']))
> +                                .format(ctx.reqs_queued, ctx.reqs_completed, ctx.fps))
> diff --git a/src/py/cam/cam_qtgl.py b/src/py/cam/cam_qtgl.py
> index c9e367a2..5f7ccf1e 100644
> --- a/src/py/cam/cam_qtgl.py
> +++ b/src/py/cam/cam_qtgl.py
> @@ -142,7 +142,7 @@ class QtRenderer:
>          self.window = window
>  
>      def run(self):
> -        camnotif = QtCore.QSocketNotifier(self.state['cm'].efd, QtCore.QSocketNotifier.Read)
> +        camnotif = QtCore.QSocketNotifier(self.state.cm.efd, QtCore.QSocketNotifier.Read)
>          camnotif.activated.connect(lambda _: self.readcam())
>  
>          keynotif = QtCore.QSocketNotifier(sys.stdin.fileno(), QtCore.QSocketNotifier.Read)
> @@ -155,7 +155,7 @@ class QtRenderer:
>          print('Exiting...')
>  
>      def readcam(self):
> -        running = self.state['event_handler'](self.state)
> +        running = self.state.event_handler()
>  
>          if not running:
>              self.app.quit()
> @@ -184,12 +184,12 @@ class MainWindow(QtWidgets.QWidget):
>          self.reqqueue = {}
>          self.current = {}
>  
> -        for ctx in self.state['contexts']:
> +        for ctx in self.state.contexts:
>  
> -            self.reqqueue[ctx['idx']] = []
> -            self.current[ctx['idx']] = []
> +            self.reqqueue[ctx.idx] = []
> +            self.current[ctx.idx] = []
>  
> -            for stream in ctx['streams']:
> +            for stream in ctx.streams:
>                  self.textures[stream] = None
>  
>          num_tiles = len(self.textures)
> @@ -312,12 +312,12 @@ class MainWindow(QtWidgets.QWidget):
>              if len(queue) == 0:
>                  continue
>  
> -            ctx = next(ctx for ctx in self.state['contexts'] if ctx['idx'] == ctx_idx)
> +            ctx = next(ctx for ctx in self.state.contexts if ctx.idx == ctx_idx)
>  
>              if self.current[ctx_idx]:
>                  old = self.current[ctx_idx]
>                  self.current[ctx_idx] = None
> -                self.state['request_prcessed'](ctx, old)
> +                self.state.request_processed(ctx, old)
>  
>              next_req = queue.pop(0)
>              self.current[ctx_idx] = next_req
> @@ -336,8 +336,8 @@ class MainWindow(QtWidgets.QWidget):
>  
>          size = self.size()
>  
> -        for idx, ctx in enumerate(self.state['contexts']):
> -            for stream in ctx['streams']:
> +        for idx, ctx in enumerate(self.state.contexts):
> +            for stream in ctx.streams:
>                  if self.textures[stream] is None:
>                      continue
>  
> @@ -359,5 +359,5 @@ class MainWindow(QtWidgets.QWidget):
>          assert(b)
>  
>      def handle_request(self, ctx, req):
> -        self.reqqueue[ctx['idx']].append(req)
> +        self.reqqueue[ctx.idx].append(req)
>          self.update()

Patch
diff mbox series

diff --git a/src/py/cam/cam.py b/src/py/cam/cam.py
index 64f67e86..f6e8232c 100755
--- a/src/py/cam/cam.py
+++ b/src/py/cam/cam.py
@@ -6,6 +6,7 @@ 
 # \todo Convert ctx and state dicts to proper classes, and move relevant
 #       functions to those classes.
 
+from typing import Any
 import argparse
 import binascii
 import libcamera as libcam
@@ -14,379 +15,400 @@  import sys
 import traceback
 
 
-class CustomAction(argparse.Action):
-    def __init__(self, option_strings, dest, **kwargs):
-        super().__init__(option_strings, dest, default={}, **kwargs)
+class CameraContext:
+    camera: libcam.Camera
+    id: str
+    idx: int
 
-    def __call__(self, parser, namespace, values, option_string=None):
-        if len(namespace.camera) == 0:
-            print(f'Option {option_string} requires a --camera context')
-            sys.exit(-1)
+    opt_stream: str
+    opt_strict_formats: bool
+    opt_crc: bool
+    opt_metadata: bool
+    opt_save_frames: bool
+    opt_capture: int
 
-        if self.type == bool:
-            values = True
+    stream_names: dict[libcam.Stream, str]
+    streams: list[libcam.Stream]
+    allocator: libcam.FrameBufferAllocator
+    requests: list[libcam.Request]
+    reqs_queued: int
+    reqs_completed: int
+    last: int = 0
+    fps: float
 
-        current = namespace.camera[-1]
+    def __init__(self, camera, idx):
+        self.camera = camera
+        self.idx = idx
+        self.id = 'cam' + str(idx)
+        self.reqs_queued = 0
+        self.reqs_completed = 0
 
-        data = getattr(namespace, self.dest)
+    def do_cmd_list_props(self):
+        camera = self.camera
 
-        if self.nargs == '+':
-            if current not in data:
-                data[current] = []
+        print('Properties for', self.id)
 
-            data[current] += values
-        else:
-            data[current] = values
+        for name, prop in camera.properties.items():
+            print('\t{}: {}'.format(name, prop))
 
+    def do_cmd_list_controls(self):
+        camera = self.camera
 
-def do_cmd_list(cm):
-    print('Available cameras:')
-
-    for idx, c in enumerate(cm.cameras):
-        print(f'{idx + 1}: {c.id}')
-
-
-def do_cmd_list_props(ctx):
-    camera = ctx['camera']
-
-    print('Properties for', ctx['id'])
-
-    for name, prop in camera.properties.items():
-        print('\t{}: {}'.format(name, prop))
-
-
-def do_cmd_list_controls(ctx):
-    camera = ctx['camera']
-
-    print('Controls for', ctx['id'])
-
-    for name, prop in camera.controls.items():
-        print('\t{}: {}'.format(name, prop))
+        print('Controls for', self.id)
 
+        for name, prop in camera.controls.items():
+            print('\t{}: {}'.format(name, prop))
 
-def do_cmd_info(ctx):
-    camera = ctx['camera']
+    def do_cmd_info(self):
+        camera = self.camera
 
-    print('Stream info for', ctx['id'])
+        print('Stream info for', self.id)
 
-    roles = [libcam.StreamRole.Viewfinder]
+        roles = [libcam.StreamRole.Viewfinder]
 
-    camconfig = camera.generate_configuration(roles)
-    if camconfig is None:
-        raise Exception('Generating config failed')
+        camconfig = camera.generate_configuration(roles)
+        if camconfig is None:
+            raise Exception('Generating config failed')
 
-    for i, stream_config in enumerate(camconfig):
-        print('\t{}: {}'.format(i, stream_config))
+        for i, stream_config in enumerate(camconfig):
+            print('\t{}: {}'.format(i, stream_config))
 
-        formats = stream_config.formats
-        for fmt in formats.pixel_formats:
-            print('\t * Pixelformat:', fmt, formats.range(fmt))
+            formats = stream_config.formats
+            for fmt in formats.pixel_formats:
+                print('\t * Pixelformat:', fmt, formats.range(fmt))
 
-            for size in formats.sizes(fmt):
-                print('\t  -', size)
+                for size in formats.sizes(fmt):
+                    print('\t  -', size)
 
+    def acquire(self):
+        camera = self.camera
 
-def acquire(ctx):
-    camera = ctx['camera']
+        camera.acquire()
 
-    camera.acquire()
+    def release(self):
+        camera = self.camera
 
+        camera.release()
 
-def release(ctx):
-    camera = ctx['camera']
+    def __parse_streams(self):
+        streams = []
 
-    camera.release()
+        for stream_desc in self.opt_stream:
+            stream_opts: dict[str, Any]
+            stream_opts = {'role': libcam.StreamRole.Viewfinder}
 
+            for stream_opt in stream_desc.split(','):
+                if stream_opt == 0:
+                    continue
 
-def parse_streams(ctx):
-    streams = []
-
-    for stream_desc in ctx['opt-stream']:
-        stream_opts = {'role': libcam.StreamRole.Viewfinder}
-
-        for stream_opt in stream_desc.split(','):
-            if stream_opt == 0:
-                continue
-
-            arr = stream_opt.split('=')
-            if len(arr) != 2:
-                print('Bad stream option', stream_opt)
-                sys.exit(-1)
-
-            key = arr[0]
-            value = arr[1]
-
-            if key in ['width', 'height']:
-                value = int(value)
-            elif key == 'role':
-                rolemap = {
-                    'still': libcam.StreamRole.StillCapture,
-                    'raw': libcam.StreamRole.Raw,
-                    'video': libcam.StreamRole.VideoRecording,
-                    'viewfinder': libcam.StreamRole.Viewfinder,
-                }
-
-                role = rolemap.get(value.lower(), None)
+                arr = stream_opt.split('=')
+                if len(arr) != 2:
+                    print('Bad stream option', stream_opt)
+                    sys.exit(-1)
 
-                if role is None:
-                    print('Bad stream role', value)
+                key = arr[0]
+                value = arr[1]
+
+                if key in ['width', 'height']:
+                    value = int(value)
+                elif key == 'role':
+                    rolemap = {
+                        'still': libcam.StreamRole.StillCapture,
+                        'raw': libcam.StreamRole.Raw,
+                        'video': libcam.StreamRole.VideoRecording,
+                        'viewfinder': libcam.StreamRole.Viewfinder,
+                    }
+
+                    role = rolemap.get(value.lower(), None)
+
+                    if role is None:
+                        print('Bad stream role', value)
+                        sys.exit(-1)
+
+                    value = role
+                elif key == 'pixelformat':
+                    pass
+                else:
+                    print('Bad stream option key', key)
                     sys.exit(-1)
 
-                value = role
-            elif key == 'pixelformat':
-                pass
-            else:
-                print('Bad stream option key', key)
-                sys.exit(-1)
+                stream_opts[key] = value
 
-            stream_opts[key] = value
+            streams.append(stream_opts)
 
-        streams.append(stream_opts)
+        return streams
 
-    return streams
+    def configure(self):
+        camera = self.camera
 
+        streams = self.__parse_streams()
 
-def configure(ctx):
-    camera = ctx['camera']
+        roles = [opts['role'] for opts in streams]
 
-    streams = parse_streams(ctx)
+        camconfig = camera.generate_configuration(roles)
+        if camconfig is None:
+            raise Exception('Generating config failed')
 
-    roles = [opts['role'] for opts in streams]
+        for idx, stream_opts in enumerate(streams):
+            stream_config = camconfig.at(idx)
 
-    camconfig = camera.generate_configuration(roles)
-    if camconfig is None:
-        raise Exception('Generating config failed')
+            if 'width' in stream_opts:
+                stream_config.size.width = stream_opts['width']
 
-    for idx, stream_opts in enumerate(streams):
-        stream_config = camconfig.at(idx)
+            if 'height' in stream_opts:
+                stream_config.size.height = stream_opts['height']
 
-        if 'width' in stream_opts:
-            stream_config.size.width = stream_opts['width']
+            if 'pixelformat' in stream_opts:
+                stream_config.pixel_format = libcam.PixelFormat(stream_opts['pixelformat'])
 
-        if 'height' in stream_opts:
-            stream_config.size.height = stream_opts['height']
+        stat = camconfig.validate()
 
-        if 'pixelformat' in stream_opts:
-            stream_config.pixel_format = libcam.PixelFormat(stream_opts['pixelformat'])
+        if stat == libcam.CameraConfiguration.Status.Invalid:
+            print('Camera configuration invalid')
+            exit(-1)
+        elif stat == libcam.CameraConfiguration.Status.Adjusted:
+            if self.opt_strict_formats:
+                print('Adjusting camera configuration disallowed by --strict-formats argument')
+                exit(-1)
 
-    stat = camconfig.validate()
+            print('Camera configuration adjusted')
 
-    if stat == libcam.CameraConfiguration.Status.Invalid:
-        print('Camera configuration invalid')
-        exit(-1)
-    elif stat == libcam.CameraConfiguration.Status.Adjusted:
-        if ctx['opt-strict-formats']:
-            print('Adjusting camera configuration disallowed by --strict-formats argument')
-            exit(-1)
+        r = camera.configure(camconfig)
+        if r != 0:
+            raise Exception('Configure failed')
 
-        print('Camera configuration adjusted')
+        self.stream_names = {}
+        self.streams = []
 
-    r = camera.configure(camconfig)
-    if r != 0:
-        raise Exception('Configure failed')
+        for idx, stream_config in enumerate(camconfig):
+            stream = stream_config.stream
+            self.streams.append(stream)
+            self.stream_names[stream] = 'stream' + str(idx)
+            print('{}-{}: stream config {}'.format(self.id, self.stream_names[stream], stream.configuration))
 
-    ctx['stream-names'] = {}
-    ctx['streams'] = []
+    def alloc_buffers(self):
+        camera = self.camera
 
-    for idx, stream_config in enumerate(camconfig):
-        stream = stream_config.stream
-        ctx['streams'].append(stream)
-        ctx['stream-names'][stream] = 'stream' + str(idx)
-        print('{}-{}: stream config {}'.format(ctx['id'], ctx['stream-names'][stream], stream.configuration))
+        allocator = libcam.FrameBufferAllocator(camera)
 
+        for stream in self.streams:
+            ret = allocator.allocate(stream)
+            if ret < 0:
+                print('Cannot allocate buffers')
+                exit(-1)
 
-def alloc_buffers(ctx):
-    camera = ctx['camera']
+            allocated = len(allocator.buffers(stream))
 
-    allocator = libcam.FrameBufferAllocator(camera)
+            print('{}-{}: Allocated {} buffers'.format(self.id, self.stream_names[stream], allocated))
 
-    for idx, stream in enumerate(ctx['streams']):
-        ret = allocator.allocate(stream)
-        if ret < 0:
-            print('Cannot allocate buffers')
-            exit(-1)
+        self.allocator = allocator
 
-        allocated = len(allocator.buffers(stream))
+    def create_requests(self):
+        camera = self.camera
 
-        print('{}-{}: Allocated {} buffers'.format(ctx['id'], ctx['stream-names'][stream], allocated))
+        self.requests = []
 
-    ctx['allocator'] = allocator
+        # Identify the stream with the least number of buffers
+        num_bufs = min([len(self.allocator.buffers(stream)) for stream in self.streams])
 
+        requests = []
 
-def create_requests(ctx):
-    camera = ctx['camera']
+        for buf_num in range(num_bufs):
+            request = camera.create_request(self.idx)
 
-    ctx['requests'] = []
+            if request is None:
+                print('Can not create request')
+                exit(-1)
 
-    # Identify the stream with the least number of buffers
-    num_bufs = min([len(ctx['allocator'].buffers(stream)) for stream in ctx['streams']])
+            for stream in self.streams:
+                buffers = self.allocator.buffers(stream)
+                buffer = buffers[buf_num]
 
-    requests = []
+                ret = request.add_buffer(stream, buffer)
+                if ret < 0:
+                    print('Can not set buffer for request')
+                    exit(-1)
 
-    for buf_num in range(num_bufs):
-        request = camera.create_request(ctx['idx'])
+            requests.append(request)
 
-        if request is None:
-            print('Can not create request')
-            exit(-1)
+        self.requests = requests
 
-        for stream in ctx['streams']:
-            buffers = ctx['allocator'].buffers(stream)
-            buffer = buffers[buf_num]
+    def start(self):
+        camera = self.camera
 
-            ret = request.add_buffer(stream, buffer)
-            if ret < 0:
-                print('Can not set buffer for request')
-                exit(-1)
+        camera.start()
 
-        requests.append(request)
+    def stop(self):
+        camera = self.camera
 
-    ctx['requests'] = requests
+        camera.stop()
 
+    def queue_requests(self):
+        camera = self.camera
 
-def start(ctx):
-    camera = ctx['camera']
+        for request in self.requests:
+            camera.queue_request(request)
+            self.reqs_queued += 1
 
-    camera.start()
+        del self.requests
 
 
-def stop(ctx):
-    camera = ctx['camera']
+class CaptureState:
+    cm: libcam.CameraManager
+    contexts: list[CameraContext]
+    renderer: Any
 
-    camera.stop()
+    def __init__(self, cm, contexts):
+        self.cm = cm
+        self.contexts = contexts
 
+    # Called from renderer when there is a libcamera event
+    def event_handler(self):
+        try:
+            cm = self.cm
+            contexts = self.contexts
 
-def queue_requests(ctx):
-    camera = ctx['camera']
+            cm.read_event()
 
-    for request in ctx['requests']:
-        camera.queue_request(request)
-        ctx['reqs-queued'] += 1
+            reqs = cm.get_ready_requests()
 
-    del ctx['requests']
+            for req in reqs:
+                ctx = next(ctx for ctx in contexts if ctx.idx == req.cookie)
+                self.__request_handler(ctx, req)
 
+            running = any(ctx.reqs_completed < ctx.opt_capture for ctx in contexts)
+            return running
+        except Exception:
+            traceback.print_exc()
+            return False
 
-def capture_init(contexts):
-    for ctx in contexts:
-        acquire(ctx)
+    def __request_handler(self, ctx, req):
+        if req.status != libcam.Request.Status.Complete:
+            raise Exception('{}: Request failed: {}'.format(ctx.id, req.status))
 
-    for ctx in contexts:
-        configure(ctx)
+        buffers = req.buffers
 
-    for ctx in contexts:
-        alloc_buffers(ctx)
+        # Compute the frame rate. The timestamp is arbitrarily retrieved from
+        # the first buffer, as all buffers should have matching timestamps.
+        ts = buffers[next(iter(buffers))].metadata.timestamp
+        last = ctx.last
+        fps = 1000000000.0 / (ts - last) if (last != 0 and (ts - last) != 0) else 0
+        ctx.last = ts
+        ctx.fps = fps
 
-    for ctx in contexts:
-        create_requests(ctx)
+        for stream, fb in buffers.items():
+            stream_name = ctx.stream_names[stream]
 
+            crcs = []
+            if ctx.opt_crc:
+                with libcamera.utils.MappedFrameBuffer(fb) as mfb:
+                    plane_crcs = [binascii.crc32(p) for p in mfb.planes]
+                    crcs.append(plane_crcs)
 
-def capture_start(contexts):
-    for ctx in contexts:
-        start(ctx)
+            meta = fb.metadata
 
-    for ctx in contexts:
-        queue_requests(ctx)
+            print('{:.6f} ({:.2f} fps) {}-{}: seq {}, bytes {}, CRCs {}'
+                  .format(ts / 1000000000, fps,
+                          ctx.id, stream_name,
+                          meta.sequence, meta.bytesused,
+                          crcs))
 
+            if ctx.opt_metadata:
+                reqmeta = req.metadata
+                for ctrl, val in reqmeta.items():
+                    print(f'\t{ctrl} = {val}')
 
-# Called from renderer when there is a libcamera event
-def event_handler(state):
-    try:
-        cm = state['cm']
-        contexts = state['contexts']
+            if ctx.opt_save_frames:
+                with libcamera.utils.MappedFrameBuffer(fb) as mfb:
+                    filename = 'frame-{}-{}-{}.data'.format(ctx.id, stream_name, ctx.reqs_completed)
+                    with open(filename, 'wb') as f:
+                        for p in mfb.planes:
+                            f.write(p)
 
-        cm.read_event()
+        self.renderer.request_handler(ctx, req)
 
-        reqs = cm.get_ready_requests()
+        ctx.reqs_completed += 1
 
-        for req in reqs:
-            ctx = next(ctx for ctx in contexts if ctx['idx'] == req.cookie)
-            request_handler(state, ctx, req)
+    # Called from renderer when it has finished with a request
+    def request_processed(self, ctx, req):
+        camera = ctx.camera
 
-        running = any(ctx['reqs-completed'] < ctx['opt-capture'] for ctx in contexts)
-        return running
-    except Exception:
-        traceback.print_exc()
-        return False
+        if ctx.reqs_queued < ctx.opt_capture:
+            req.reuse()
+            camera.queue_request(req)
+            ctx.reqs_queued += 1
 
+    def __capture_init(self):
+        for ctx in self.contexts:
+            ctx.acquire()
 
-def request_handler(state, ctx, req):
-    if req.status != libcam.Request.Status.Complete:
-        raise Exception('{}: Request failed: {}'.format(ctx['id'], req.status))
+        for ctx in self.contexts:
+            ctx.configure()
 
-    buffers = req.buffers
+        for ctx in self.contexts:
+            ctx.alloc_buffers()
 
-    # Compute the frame rate. The timestamp is arbitrarily retrieved from
-    # the first buffer, as all buffers should have matching timestamps.
-    ts = buffers[next(iter(buffers))].metadata.timestamp
-    last = ctx.get('last', 0)
-    fps = 1000000000.0 / (ts - last) if (last != 0 and (ts - last) != 0) else 0
-    ctx['last'] = ts
-    ctx['fps'] = fps
+        for ctx in self.contexts:
+            ctx.create_requests()
 
-    for stream, fb in buffers.items():
-        stream_name = ctx['stream-names'][stream]
+    def __capture_start(self):
+        for ctx in self.contexts:
+            ctx.start()
 
-        crcs = []
-        if ctx['opt-crc']:
-            with libcamera.utils.MappedFrameBuffer(fb) as mfb:
-                plane_crcs = [binascii.crc32(p) for p in mfb.planes]
-                crcs.append(plane_crcs)
+        for ctx in self.contexts:
+            ctx.queue_requests()
 
-        meta = fb.metadata
+    def __capture_deinit(self):
+        for ctx in self.contexts:
+            ctx.stop()
 
-        print('{:.6f} ({:.2f} fps) {}-{}: seq {}, bytes {}, CRCs {}'
-              .format(ts / 1000000000, fps,
-                      ctx['id'], stream_name,
-                      meta.sequence, meta.bytesused,
-                      crcs))
+        for ctx in self.contexts:
+            ctx.release()
 
-        if ctx['opt-metadata']:
-            reqmeta = req.metadata
-            for ctrl, val in reqmeta.items():
-                print(f'\t{ctrl} = {val}')
+    def do_cmd_capture(self):
+        self.__capture_init()
 
-        if ctx['opt-save-frames']:
-            with libcamera.utils.MappedFrameBuffer(fb) as mfb:
-                filename = 'frame-{}-{}-{}.data'.format(ctx['id'], stream_name, ctx['reqs-completed'])
-                with open(filename, 'wb') as f:
-                    for p in mfb.planes:
-                        f.write(p)
+        renderer = self.renderer
 
-    state['renderer'].request_handler(ctx, req)
+        renderer.setup()
 
-    ctx['reqs-completed'] += 1
+        self.__capture_start()
 
+        renderer.run()
 
-# Called from renderer when it has finished with a request
-def request_prcessed(ctx, req):
-    camera = ctx['camera']
+        self.__capture_deinit()
 
-    if ctx['reqs-queued'] < ctx['opt-capture']:
-        req.reuse()
-        camera.queue_request(req)
-        ctx['reqs-queued'] += 1
 
+class CustomAction(argparse.Action):
+    def __init__(self, option_strings, dest, **kwargs):
+        super().__init__(option_strings, dest, default={}, **kwargs)
 
-def capture_deinit(contexts):
-    for ctx in contexts:
-        stop(ctx)
+    def __call__(self, parser, namespace, values, option_string=None):
+        if len(namespace.camera) == 0:
+            print(f'Option {option_string} requires a --camera context')
+            sys.exit(-1)
 
-    for ctx in contexts:
-        release(ctx)
+        if self.type == bool:
+            values = True
 
+        current = namespace.camera[-1]
 
-def do_cmd_capture(state):
-    capture_init(state['contexts'])
+        data = getattr(namespace, self.dest)
 
-    renderer = state['renderer']
+        if self.nargs == '+':
+            if current not in data:
+                data[current] = []
 
-    renderer.setup()
+            data[current] += values
+        else:
+            data[current] = values
 
-    capture_start(state['contexts'])
 
-    renderer.run()
+def do_cmd_list(cm):
+    print('Available cameras:')
 
-    capture_deinit(state['contexts'])
+    for idx, c in enumerate(cm.cameras):
+        print(f'{idx + 1}: {c.id}')
 
 
 def main():
@@ -422,39 +444,28 @@  def main():
             print('Unable to find camera', cam_idx)
             return -1
 
-        contexts.append({
-                        'camera': camera,
-                        'idx': cam_idx,
-                        'id': 'cam' + str(cam_idx),
-                        'reqs-queued': 0,
-                        'reqs-completed': 0,
-                        'opt-capture': args.capture.get(cam_idx, False),
-                        'opt-crc': args.crc.get(cam_idx, False),
-                        'opt-save-frames': args.save_frames.get(cam_idx, False),
-                        'opt-metadata': args.metadata.get(cam_idx, False),
-                        'opt-strict-formats': args.strict_formats.get(cam_idx, False),
-                        'opt-stream': args.stream.get(cam_idx, ['role=viewfinder']),
-                        })
+        ctx = CameraContext(camera, cam_idx)
+        ctx.opt_capture = args.capture.get(cam_idx, 0)
+        ctx.opt_crc = args.crc.get(cam_idx, False)
+        ctx.opt_save_frames = args.save_frames.get(cam_idx, False)
+        ctx.opt_metadata = args.metadata.get(cam_idx, False)
+        ctx.opt_strict_formats = args.strict_formats.get(cam_idx, False)
+        ctx.opt_stream = args.stream.get(cam_idx, ['role=viewfinder'])
+        contexts.append(ctx)
 
     for ctx in contexts:
-        print('Using camera {} as {}'.format(ctx['camera'].id, ctx['id']))
+        print('Using camera {} as {}'.format(ctx.camera.id, ctx.id))
 
     for ctx in contexts:
         if args.list_properties:
-            do_cmd_list_props(ctx)
+            ctx.do_cmd_list_props()
         if args.list_controls:
-            do_cmd_list_controls(ctx)
+            ctx.do_cmd_list_controls()
         if args.info:
-            do_cmd_info(ctx)
+            ctx.do_cmd_info()
 
     if args.capture:
-
-        state = {
-            'cm': cm,
-            'contexts': contexts,
-            'event_handler': event_handler,
-            'request_prcessed': request_prcessed,
-        }
+        state = CaptureState(cm, contexts)
 
         if args.renderer == 'null':
             import cam_null
@@ -472,9 +483,9 @@  def main():
             print('Bad renderer', args.renderer)
             return -1
 
-        state['renderer'] = renderer
+        state.renderer = renderer
 
-        do_cmd_capture(state)
+        state.do_cmd_capture()
 
     return 0
 
diff --git a/src/py/cam/cam_kms.py b/src/py/cam/cam_kms.py
index 74cd3b38..213e0b03 100644
--- a/src/py/cam/cam_kms.py
+++ b/src/py/cam/cam_kms.py
@@ -10,8 +10,8 @@  class KMSRenderer:
     def __init__(self, state):
         self.state = state
 
-        self.cm = state['cm']
-        self.contexts = state['contexts']
+        self.cm = state.cm
+        self.contexts = state.contexts
         self.running = False
 
         card = pykms.Card()
@@ -92,7 +92,7 @@  class KMSRenderer:
         if old:
             req = old['camreq']
             ctx = old['camctx']
-            self.state['request_prcessed'](ctx, req)
+            self.state.request_processed(ctx, req)
 
     def queue(self, drmreq):
         if not self.next:
@@ -108,7 +108,7 @@  class KMSRenderer:
 
         idx = 0
         for ctx in self.contexts:
-            for stream in ctx['streams']:
+            for stream in ctx.streams:
 
                 cfg = stream.configuration
                 fmt = cfg.pixel_format
@@ -125,7 +125,7 @@  class KMSRenderer:
                     'size': cfg.size,
                 })
 
-                for fb in ctx['allocator'].buffers(stream):
+                for fb in ctx.allocator.buffers(stream):
                     w = cfg.size.width
                     h = cfg.size.height
                     fds = []
@@ -148,7 +148,7 @@  class KMSRenderer:
                 self.handle_page_flip(ev.seq, ev.time)
 
     def readcam(self, fd):
-        self.running = self.state['event_handler'](self.state)
+        self.running = self.state.event_handler()
 
     def readkey(self, fileobj):
         sys.stdin.readline()
diff --git a/src/py/cam/cam_null.py b/src/py/cam/cam_null.py
index a6da9671..45c5f467 100644
--- a/src/py/cam/cam_null.py
+++ b/src/py/cam/cam_null.py
@@ -9,8 +9,8 @@  class NullRenderer:
     def __init__(self, state):
         self.state = state
 
-        self.cm = state['cm']
-        self.contexts = state['contexts']
+        self.cm = state.cm
+        self.contexts = state.contexts
 
         self.running = False
 
@@ -37,11 +37,11 @@  class NullRenderer:
         print('Exiting...')
 
     def readcam(self, fd):
-        self.running = self.state['event_handler'](self.state)
+        self.running = self.state.event_handler()
 
     def readkey(self, fileobj):
         sys.stdin.readline()
         self.running = False
 
     def request_handler(self, ctx, req):
-        self.state['request_prcessed'](ctx, req)
+        self.state.request_processed(ctx, req)
diff --git a/src/py/cam/cam_qt.py b/src/py/cam/cam_qt.py
index 03096920..d638e9cc 100644
--- a/src/py/cam/cam_qt.py
+++ b/src/py/cam/cam_qt.py
@@ -176,8 +176,8 @@  class QtRenderer:
     def __init__(self, state):
         self.state = state
 
-        self.cm = state['cm']
-        self.contexts = state['contexts']
+        self.cm = state.cm
+        self.contexts = state.contexts
 
     def setup(self):
         self.app = QtWidgets.QApplication([])
@@ -185,7 +185,7 @@  class QtRenderer:
         windows = []
 
         for ctx in self.contexts:
-            for stream in ctx['streams']:
+            for stream in ctx.streams:
                 window = MainWindow(ctx, stream)
                 window.show()
                 windows.append(window)
@@ -206,7 +206,7 @@  class QtRenderer:
         print('Exiting...')
 
     def readcam(self):
-        running = self.state['event_handler'](self.state)
+        running = self.state.event_handler()
 
         if not running:
             self.app.quit()
@@ -223,7 +223,7 @@  class QtRenderer:
 
             wnd.handle_request(stream, fb)
 
-        self.state['request_prcessed'](ctx, req)
+        self.state.request_processed(ctx, req)
 
     def cleanup(self):
         for w in self.windows:
@@ -254,7 +254,7 @@  class MainWindow(QtWidgets.QWidget):
         group.setLayout(groupLayout)
         controlsLayout.addWidget(group)
 
-        lab = QtWidgets.QLabel(ctx['id'])
+        lab = QtWidgets.QLabel(ctx.id)
         groupLayout.addWidget(lab)
 
         self.frameLabel = QtWidgets.QLabel()
@@ -265,7 +265,7 @@  class MainWindow(QtWidgets.QWidget):
         group.setLayout(groupLayout)
         controlsLayout.addWidget(group)
 
-        camera = ctx['camera']
+        camera = ctx.camera
 
         for k, v in camera.properties.items():
             lab = QtWidgets.QLabel()
@@ -308,4 +308,4 @@  class MainWindow(QtWidgets.QWidget):
         self.label.setPixmap(pix)
 
         self.frameLabel.setText('Queued: {}\nDone: {}\nFps: {:.2f}'
-                                .format(ctx['reqs-queued'], ctx['reqs-completed'], ctx['fps']))
+                                .format(ctx.reqs_queued, ctx.reqs_completed, ctx.fps))
diff --git a/src/py/cam/cam_qtgl.py b/src/py/cam/cam_qtgl.py
index c9e367a2..5f7ccf1e 100644
--- a/src/py/cam/cam_qtgl.py
+++ b/src/py/cam/cam_qtgl.py
@@ -142,7 +142,7 @@  class QtRenderer:
         self.window = window
 
     def run(self):
-        camnotif = QtCore.QSocketNotifier(self.state['cm'].efd, QtCore.QSocketNotifier.Read)
+        camnotif = QtCore.QSocketNotifier(self.state.cm.efd, QtCore.QSocketNotifier.Read)
         camnotif.activated.connect(lambda _: self.readcam())
 
         keynotif = QtCore.QSocketNotifier(sys.stdin.fileno(), QtCore.QSocketNotifier.Read)
@@ -155,7 +155,7 @@  class QtRenderer:
         print('Exiting...')
 
     def readcam(self):
-        running = self.state['event_handler'](self.state)
+        running = self.state.event_handler()
 
         if not running:
             self.app.quit()
@@ -184,12 +184,12 @@  class MainWindow(QtWidgets.QWidget):
         self.reqqueue = {}
         self.current = {}
 
-        for ctx in self.state['contexts']:
+        for ctx in self.state.contexts:
 
-            self.reqqueue[ctx['idx']] = []
-            self.current[ctx['idx']] = []
+            self.reqqueue[ctx.idx] = []
+            self.current[ctx.idx] = []
 
-            for stream in ctx['streams']:
+            for stream in ctx.streams:
                 self.textures[stream] = None
 
         num_tiles = len(self.textures)
@@ -312,12 +312,12 @@  class MainWindow(QtWidgets.QWidget):
             if len(queue) == 0:
                 continue
 
-            ctx = next(ctx for ctx in self.state['contexts'] if ctx['idx'] == ctx_idx)
+            ctx = next(ctx for ctx in self.state.contexts if ctx.idx == ctx_idx)
 
             if self.current[ctx_idx]:
                 old = self.current[ctx_idx]
                 self.current[ctx_idx] = None
-                self.state['request_prcessed'](ctx, old)
+                self.state.request_processed(ctx, old)
 
             next_req = queue.pop(0)
             self.current[ctx_idx] = next_req
@@ -336,8 +336,8 @@  class MainWindow(QtWidgets.QWidget):
 
         size = self.size()
 
-        for idx, ctx in enumerate(self.state['contexts']):
-            for stream in ctx['streams']:
+        for idx, ctx in enumerate(self.state.contexts):
+            for stream in ctx.streams:
                 if self.textures[stream] is None:
                     continue
 
@@ -359,5 +359,5 @@  class MainWindow(QtWidgets.QWidget):
         assert(b)
 
     def handle_request(self, ctx, req):
-        self.reqqueue[ctx['idx']].append(req)
+        self.reqqueue[ctx.idx].append(req)
         self.update()