[{"id":23180,"web_url":"https://patchwork.libcamera.org/comment/23180/","msgid":"<YpB1gO7VlitqsONA@pendragon.ideasonboard.com>","date":"2022-05-27T06:53:52","subject":"Re: [libcamera-devel] [PATCH v2 13/19] py: cam: Convert ctx and\n\tstate to classes","submitter":{"id":2,"url":"https://patchwork.libcamera.org/api/people/2/","name":"Laurent Pinchart","email":"laurent.pinchart@ideasonboard.com"},"content":"Hi Tomi,\n\nThank you for the patch.\n\nOn Tue, May 24, 2022 at 02:46:04PM +0300, Tomi Valkeinen wrote:\n> From: Tomi Valkeinen <tomi.valkeinen@iki.fi>\n> \n> Convert ctx and state dicts to classes. No functional changes.\n> \n> Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n> ---\n>  src/py/cam/cam.py      | 585 +++++++++++++++++++++--------------------\n>  src/py/cam/cam_kms.py  |  12 +-\n>  src/py/cam/cam_null.py |   8 +-\n>  src/py/cam/cam_qt.py   |  16 +-\n>  src/py/cam/cam_qtgl.py |  22 +-\n>  5 files changed, 327 insertions(+), 316 deletions(-)\n> \n> diff --git a/src/py/cam/cam.py b/src/py/cam/cam.py\n> index 64f67e86..f6e8232c 100755\n> --- a/src/py/cam/cam.py\n> +++ b/src/py/cam/cam.py\n> @@ -6,6 +6,7 @@\n>  # \\todo Convert ctx and state dicts to proper classes, and move relevant\n>  #       functions to those classes.\n>  \n> +from typing import Any\n>  import argparse\n>  import binascii\n>  import libcamera as libcam\n> @@ -14,379 +15,400 @@ import sys\n>  import traceback\n>  \n>  \n> -class CustomAction(argparse.Action):\n> -    def __init__(self, option_strings, dest, **kwargs):\n> -        super().__init__(option_strings, dest, default={}, **kwargs)\n> +class CameraContext:\n> +    camera: libcam.Camera\n> +    id: str\n> +    idx: int\n>  \n> -    def __call__(self, parser, namespace, values, option_string=None):\n> -        if len(namespace.camera) == 0:\n> -            print(f'Option {option_string} requires a --camera context')\n> -            sys.exit(-1)\n> +    opt_stream: str\n> +    opt_strict_formats: bool\n> +    opt_crc: bool\n> +    opt_metadata: bool\n> +    opt_save_frames: bool\n> +    opt_capture: int\n>  \n> -        if self.type == bool:\n> -            values = True\n> +    stream_names: dict[libcam.Stream, str]\n> +    streams: list[libcam.Stream]\n> +    allocator: libcam.FrameBufferAllocator\n> +    requests: list[libcam.Request]\n> +    reqs_queued: int\n> +    reqs_completed: int\n> +    last: int = 0\n> +    fps: float\n>  \n> -        current = namespace.camera[-1]\n> +    def __init__(self, camera, idx):\n> +        self.camera = camera\n> +        self.idx = idx\n> +        self.id = 'cam' + str(idx)\n> +        self.reqs_queued = 0\n> +        self.reqs_completed = 0\n>  \n> -        data = getattr(namespace, self.dest)\n> +    def do_cmd_list_props(self):\n> +        camera = self.camera\n>  \n> -        if self.nargs == '+':\n> -            if current not in data:\n> -                data[current] = []\n> +        print('Properties for', self.id)\n>  \n> -            data[current] += values\n> -        else:\n> -            data[current] = values\n> +        for name, prop in camera.properties.items():\n\nYou can use self.camera here and drop the local variable. Same in a few\nother functions below.\n\n> +            print('\\t{}: {}'.format(name, prop))\n>  \n> +    def do_cmd_list_controls(self):\n> +        camera = self.camera\n>  \n> -def do_cmd_list(cm):\n> -    print('Available cameras:')\n> -\n> -    for idx, c in enumerate(cm.cameras):\n> -        print(f'{idx + 1}: {c.id}')\n> -\n> -\n> -def do_cmd_list_props(ctx):\n> -    camera = ctx['camera']\n> -\n> -    print('Properties for', ctx['id'])\n> -\n> -    for name, prop in camera.properties.items():\n> -        print('\\t{}: {}'.format(name, prop))\n> -\n> -\n> -def do_cmd_list_controls(ctx):\n> -    camera = ctx['camera']\n> -\n> -    print('Controls for', ctx['id'])\n> -\n> -    for name, prop in camera.controls.items():\n> -        print('\\t{}: {}'.format(name, prop))\n> +        print('Controls for', self.id)\n>  \n> +        for name, prop in camera.controls.items():\n> +            print('\\t{}: {}'.format(name, prop))\n>  \n> -def do_cmd_info(ctx):\n> -    camera = ctx['camera']\n> +    def do_cmd_info(self):\n> +        camera = self.camera\n>  \n> -    print('Stream info for', ctx['id'])\n> +        print('Stream info for', self.id)\n>  \n> -    roles = [libcam.StreamRole.Viewfinder]\n> +        roles = [libcam.StreamRole.Viewfinder]\n>  \n> -    camconfig = camera.generate_configuration(roles)\n> -    if camconfig is None:\n> -        raise Exception('Generating config failed')\n> +        camconfig = camera.generate_configuration(roles)\n> +        if camconfig is None:\n> +            raise Exception('Generating config failed')\n>  \n> -    for i, stream_config in enumerate(camconfig):\n> -        print('\\t{}: {}'.format(i, stream_config))\n> +        for i, stream_config in enumerate(camconfig):\n> +            print('\\t{}: {}'.format(i, stream_config))\n>  \n> -        formats = stream_config.formats\n> -        for fmt in formats.pixel_formats:\n> -            print('\\t * Pixelformat:', fmt, formats.range(fmt))\n> +            formats = stream_config.formats\n> +            for fmt in formats.pixel_formats:\n> +                print('\\t * Pixelformat:', fmt, formats.range(fmt))\n>  \n> -            for size in formats.sizes(fmt):\n> -                print('\\t  -', size)\n> +                for size in formats.sizes(fmt):\n> +                    print('\\t  -', size)\n>  \n> +    def acquire(self):\n> +        camera = self.camera\n>  \n> -def acquire(ctx):\n> -    camera = ctx['camera']\n> +        camera.acquire()\n>  \n> -    camera.acquire()\n> +    def release(self):\n> +        camera = self.camera\n>  \n> +        camera.release()\n>  \n> -def release(ctx):\n> -    camera = ctx['camera']\n> +    def __parse_streams(self):\n> +        streams = []\n>  \n> -    camera.release()\n> +        for stream_desc in self.opt_stream:\n> +            stream_opts: dict[str, Any]\n> +            stream_opts = {'role': libcam.StreamRole.Viewfinder}\n>  \n> +            for stream_opt in stream_desc.split(','):\n> +                if stream_opt == 0:\n> +                    continue\n>  \n> -def parse_streams(ctx):\n> -    streams = []\n> -\n> -    for stream_desc in ctx['opt-stream']:\n> -        stream_opts = {'role': libcam.StreamRole.Viewfinder}\n> -\n> -        for stream_opt in stream_desc.split(','):\n> -            if stream_opt == 0:\n> -                continue\n> -\n> -            arr = stream_opt.split('=')\n> -            if len(arr) != 2:\n> -                print('Bad stream option', stream_opt)\n> -                sys.exit(-1)\n> -\n> -            key = arr[0]\n> -            value = arr[1]\n> -\n> -            if key in ['width', 'height']:\n> -                value = int(value)\n> -            elif key == 'role':\n> -                rolemap = {\n> -                    'still': libcam.StreamRole.StillCapture,\n> -                    'raw': libcam.StreamRole.Raw,\n> -                    'video': libcam.StreamRole.VideoRecording,\n> -                    'viewfinder': libcam.StreamRole.Viewfinder,\n> -                }\n> -\n> -                role = rolemap.get(value.lower(), None)\n> +                arr = stream_opt.split('=')\n> +                if len(arr) != 2:\n> +                    print('Bad stream option', stream_opt)\n> +                    sys.exit(-1)\n\nRaising an exception would be better, but that's a candidate for another\npatch as it's a functional change.\n\n>  \n> -                if role is None:\n> -                    print('Bad stream role', value)\n> +                key = arr[0]\n> +                value = arr[1]\n> +\n> +                if key in ['width', 'height']:\n> +                    value = int(value)\n> +                elif key == 'role':\n> +                    rolemap = {\n> +                        'still': libcam.StreamRole.StillCapture,\n> +                        'raw': libcam.StreamRole.Raw,\n> +                        'video': libcam.StreamRole.VideoRecording,\n> +                        'viewfinder': libcam.StreamRole.Viewfinder,\n> +                    }\n> +\n> +                    role = rolemap.get(value.lower(), None)\n> +\n> +                    if role is None:\n> +                        print('Bad stream role', value)\n> +                        sys.exit(-1)\n> +\n> +                    value = role\n> +                elif key == 'pixelformat':\n> +                    pass\n> +                else:\n> +                    print('Bad stream option key', key)\n>                      sys.exit(-1)\n>  \n> -                value = role\n> -            elif key == 'pixelformat':\n> -                pass\n> -            else:\n> -                print('Bad stream option key', key)\n> -                sys.exit(-1)\n> +                stream_opts[key] = value\n>  \n> -            stream_opts[key] = value\n> +            streams.append(stream_opts)\n>  \n> -        streams.append(stream_opts)\n> +        return streams\n>  \n> -    return streams\n> +    def configure(self):\n> +        camera = self.camera\n>  \n> +        streams = self.__parse_streams()\n>  \n> -def configure(ctx):\n> -    camera = ctx['camera']\n> +        roles = [opts['role'] for opts in streams]\n>  \n> -    streams = parse_streams(ctx)\n> +        camconfig = camera.generate_configuration(roles)\n> +        if camconfig is None:\n> +            raise Exception('Generating config failed')\n>  \n> -    roles = [opts['role'] for opts in streams]\n> +        for idx, stream_opts in enumerate(streams):\n> +            stream_config = camconfig.at(idx)\n>  \n> -    camconfig = camera.generate_configuration(roles)\n> -    if camconfig is None:\n> -        raise Exception('Generating config failed')\n> +            if 'width' in stream_opts:\n> +                stream_config.size.width = stream_opts['width']\n>  \n> -    for idx, stream_opts in enumerate(streams):\n> -        stream_config = camconfig.at(idx)\n> +            if 'height' in stream_opts:\n> +                stream_config.size.height = stream_opts['height']\n>  \n> -        if 'width' in stream_opts:\n> -            stream_config.size.width = stream_opts['width']\n> +            if 'pixelformat' in stream_opts:\n> +                stream_config.pixel_format = libcam.PixelFormat(stream_opts['pixelformat'])\n>  \n> -        if 'height' in stream_opts:\n> -            stream_config.size.height = stream_opts['height']\n> +        stat = camconfig.validate()\n>  \n> -        if 'pixelformat' in stream_opts:\n> -            stream_config.pixel_format = libcam.PixelFormat(stream_opts['pixelformat'])\n> +        if stat == libcam.CameraConfiguration.Status.Invalid:\n> +            print('Camera configuration invalid')\n> +            exit(-1)\n> +        elif stat == libcam.CameraConfiguration.Status.Adjusted:\n> +            if self.opt_strict_formats:\n> +                print('Adjusting camera configuration disallowed by --strict-formats argument')\n> +                exit(-1)\n>  \n> -    stat = camconfig.validate()\n> +            print('Camera configuration adjusted')\n>  \n> -    if stat == libcam.CameraConfiguration.Status.Invalid:\n> -        print('Camera configuration invalid')\n> -        exit(-1)\n> -    elif stat == libcam.CameraConfiguration.Status.Adjusted:\n> -        if ctx['opt-strict-formats']:\n> -            print('Adjusting camera configuration disallowed by --strict-formats argument')\n> -            exit(-1)\n> +        r = camera.configure(camconfig)\n> +        if r != 0:\n> +            raise Exception('Configure failed')\n>  \n> -        print('Camera configuration adjusted')\n> +        self.stream_names = {}\n> +        self.streams = []\n>  \n> -    r = camera.configure(camconfig)\n> -    if r != 0:\n> -        raise Exception('Configure failed')\n> +        for idx, stream_config in enumerate(camconfig):\n> +            stream = stream_config.stream\n> +            self.streams.append(stream)\n> +            self.stream_names[stream] = 'stream' + str(idx)\n> +            print('{}-{}: stream config {}'.format(self.id, self.stream_names[stream], stream.configuration))\n>  \n> -    ctx['stream-names'] = {}\n> -    ctx['streams'] = []\n> +    def alloc_buffers(self):\n> +        camera = self.camera\n>  \n> -    for idx, stream_config in enumerate(camconfig):\n> -        stream = stream_config.stream\n> -        ctx['streams'].append(stream)\n> -        ctx['stream-names'][stream] = 'stream' + str(idx)\n> -        print('{}-{}: stream config {}'.format(ctx['id'], ctx['stream-names'][stream], stream.configuration))\n> +        allocator = libcam.FrameBufferAllocator(camera)\n>  \n> +        for stream in self.streams:\n> +            ret = allocator.allocate(stream)\n> +            if ret < 0:\n> +                print('Cannot allocate buffers')\n> +                exit(-1)\n>  \n> -def alloc_buffers(ctx):\n> -    camera = ctx['camera']\n> +            allocated = len(allocator.buffers(stream))\n>  \n> -    allocator = libcam.FrameBufferAllocator(camera)\n> +            print('{}-{}: Allocated {} buffers'.format(self.id, self.stream_names[stream], allocated))\n>  \n> -    for idx, stream in enumerate(ctx['streams']):\n> -        ret = allocator.allocate(stream)\n> -        if ret < 0:\n> -            print('Cannot allocate buffers')\n> -            exit(-1)\n> +        self.allocator = allocator\n>  \n> -        allocated = len(allocator.buffers(stream))\n> +    def create_requests(self):\n> +        camera = self.camera\n>  \n> -        print('{}-{}: Allocated {} buffers'.format(ctx['id'], ctx['stream-names'][stream], allocated))\n> +        self.requests = []\n>  \n> -    ctx['allocator'] = allocator\n> +        # Identify the stream with the least number of buffers\n> +        num_bufs = min([len(self.allocator.buffers(stream)) for stream in self.streams])\n>  \n> +        requests = []\n>  \n> -def create_requests(ctx):\n> -    camera = ctx['camera']\n> +        for buf_num in range(num_bufs):\n> +            request = camera.create_request(self.idx)\n>  \n> -    ctx['requests'] = []\n> +            if request is None:\n> +                print('Can not create request')\n> +                exit(-1)\n>  \n> -    # Identify the stream with the least number of buffers\n> -    num_bufs = min([len(ctx['allocator'].buffers(stream)) for stream in ctx['streams']])\n> +            for stream in self.streams:\n> +                buffers = self.allocator.buffers(stream)\n> +                buffer = buffers[buf_num]\n>  \n> -    requests = []\n> +                ret = request.add_buffer(stream, buffer)\n> +                if ret < 0:\n> +                    print('Can not set buffer for request')\n> +                    exit(-1)\n>  \n> -    for buf_num in range(num_bufs):\n> -        request = camera.create_request(ctx['idx'])\n> +            requests.append(request)\n>  \n> -        if request is None:\n> -            print('Can not create request')\n> -            exit(-1)\n> +        self.requests = requests\n>  \n> -        for stream in ctx['streams']:\n> -            buffers = ctx['allocator'].buffers(stream)\n> -            buffer = buffers[buf_num]\n> +    def start(self):\n> +        camera = self.camera\n>  \n> -            ret = request.add_buffer(stream, buffer)\n> -            if ret < 0:\n> -                print('Can not set buffer for request')\n> -                exit(-1)\n> +        camera.start()\n>  \n> -        requests.append(request)\n> +    def stop(self):\n> +        camera = self.camera\n>  \n> -    ctx['requests'] = requests\n> +        camera.stop()\n>  \n> +    def queue_requests(self):\n> +        camera = self.camera\n>  \n> -def start(ctx):\n> -    camera = ctx['camera']\n> +        for request in self.requests:\n> +            camera.queue_request(request)\n> +            self.reqs_queued += 1\n>  \n> -    camera.start()\n> +        del self.requests\n>  \n>  \n> -def stop(ctx):\n> -    camera = ctx['camera']\n> +class CaptureState:\n> +    cm: libcam.CameraManager\n> +    contexts: list[CameraContext]\n> +    renderer: Any\n>  \n> -    camera.stop()\n> +    def __init__(self, cm, contexts):\n> +        self.cm = cm\n> +        self.contexts = contexts\n>  \n> +    # Called from renderer when there is a libcamera event\n> +    def event_handler(self):\n> +        try:\n> +            cm = self.cm\n> +            contexts = self.contexts\n\nSame here, these local variables could possibly be dropped.\n\nReviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n\n>  \n> -def queue_requests(ctx):\n> -    camera = ctx['camera']\n> +            cm.read_event()\n>  \n> -    for request in ctx['requests']:\n> -        camera.queue_request(request)\n> -        ctx['reqs-queued'] += 1\n> +            reqs = cm.get_ready_requests()\n>  \n> -    del ctx['requests']\n> +            for req in reqs:\n> +                ctx = next(ctx for ctx in contexts if ctx.idx == req.cookie)\n> +                self.__request_handler(ctx, req)\n>  \n> +            running = any(ctx.reqs_completed < ctx.opt_capture for ctx in contexts)\n> +            return running\n> +        except Exception:\n> +            traceback.print_exc()\n> +            return False\n>  \n> -def capture_init(contexts):\n> -    for ctx in contexts:\n> -        acquire(ctx)\n> +    def __request_handler(self, ctx, req):\n> +        if req.status != libcam.Request.Status.Complete:\n> +            raise Exception('{}: Request failed: {}'.format(ctx.id, req.status))\n>  \n> -    for ctx in contexts:\n> -        configure(ctx)\n> +        buffers = req.buffers\n>  \n> -    for ctx in contexts:\n> -        alloc_buffers(ctx)\n> +        # Compute the frame rate. The timestamp is arbitrarily retrieved from\n> +        # the first buffer, as all buffers should have matching timestamps.\n> +        ts = buffers[next(iter(buffers))].metadata.timestamp\n> +        last = ctx.last\n> +        fps = 1000000000.0 / (ts - last) if (last != 0 and (ts - last) != 0) else 0\n> +        ctx.last = ts\n> +        ctx.fps = fps\n>  \n> -    for ctx in contexts:\n> -        create_requests(ctx)\n> +        for stream, fb in buffers.items():\n> +            stream_name = ctx.stream_names[stream]\n>  \n> +            crcs = []\n> +            if ctx.opt_crc:\n> +                with libcamera.utils.MappedFrameBuffer(fb) as mfb:\n> +                    plane_crcs = [binascii.crc32(p) for p in mfb.planes]\n> +                    crcs.append(plane_crcs)\n>  \n> -def capture_start(contexts):\n> -    for ctx in contexts:\n> -        start(ctx)\n> +            meta = fb.metadata\n>  \n> -    for ctx in contexts:\n> -        queue_requests(ctx)\n> +            print('{:.6f} ({:.2f} fps) {}-{}: seq {}, bytes {}, CRCs {}'\n> +                  .format(ts / 1000000000, fps,\n> +                          ctx.id, stream_name,\n> +                          meta.sequence, meta.bytesused,\n> +                          crcs))\n>  \n> +            if ctx.opt_metadata:\n> +                reqmeta = req.metadata\n> +                for ctrl, val in reqmeta.items():\n> +                    print(f'\\t{ctrl} = {val}')\n>  \n> -# Called from renderer when there is a libcamera event\n> -def event_handler(state):\n> -    try:\n> -        cm = state['cm']\n> -        contexts = state['contexts']\n> +            if ctx.opt_save_frames:\n> +                with libcamera.utils.MappedFrameBuffer(fb) as mfb:\n> +                    filename = 'frame-{}-{}-{}.data'.format(ctx.id, stream_name, ctx.reqs_completed)\n> +                    with open(filename, 'wb') as f:\n> +                        for p in mfb.planes:\n> +                            f.write(p)\n>  \n> -        cm.read_event()\n> +        self.renderer.request_handler(ctx, req)\n>  \n> -        reqs = cm.get_ready_requests()\n> +        ctx.reqs_completed += 1\n>  \n> -        for req in reqs:\n> -            ctx = next(ctx for ctx in contexts if ctx['idx'] == req.cookie)\n> -            request_handler(state, ctx, req)\n> +    # Called from renderer when it has finished with a request\n> +    def request_processed(self, ctx, req):\n> +        camera = ctx.camera\n>  \n> -        running = any(ctx['reqs-completed'] < ctx['opt-capture'] for ctx in contexts)\n> -        return running\n> -    except Exception:\n> -        traceback.print_exc()\n> -        return False\n> +        if ctx.reqs_queued < ctx.opt_capture:\n> +            req.reuse()\n> +            camera.queue_request(req)\n> +            ctx.reqs_queued += 1\n>  \n> +    def __capture_init(self):\n> +        for ctx in self.contexts:\n> +            ctx.acquire()\n>  \n> -def request_handler(state, ctx, req):\n> -    if req.status != libcam.Request.Status.Complete:\n> -        raise Exception('{}: Request failed: {}'.format(ctx['id'], req.status))\n> +        for ctx in self.contexts:\n> +            ctx.configure()\n>  \n> -    buffers = req.buffers\n> +        for ctx in self.contexts:\n> +            ctx.alloc_buffers()\n>  \n> -    # Compute the frame rate. The timestamp is arbitrarily retrieved from\n> -    # the first buffer, as all buffers should have matching timestamps.\n> -    ts = buffers[next(iter(buffers))].metadata.timestamp\n> -    last = ctx.get('last', 0)\n> -    fps = 1000000000.0 / (ts - last) if (last != 0 and (ts - last) != 0) else 0\n> -    ctx['last'] = ts\n> -    ctx['fps'] = fps\n> +        for ctx in self.contexts:\n> +            ctx.create_requests()\n>  \n> -    for stream, fb in buffers.items():\n> -        stream_name = ctx['stream-names'][stream]\n> +    def __capture_start(self):\n> +        for ctx in self.contexts:\n> +            ctx.start()\n>  \n> -        crcs = []\n> -        if ctx['opt-crc']:\n> -            with libcamera.utils.MappedFrameBuffer(fb) as mfb:\n> -                plane_crcs = [binascii.crc32(p) for p in mfb.planes]\n> -                crcs.append(plane_crcs)\n> +        for ctx in self.contexts:\n> +            ctx.queue_requests()\n>  \n> -        meta = fb.metadata\n> +    def __capture_deinit(self):\n> +        for ctx in self.contexts:\n> +            ctx.stop()\n>  \n> -        print('{:.6f} ({:.2f} fps) {}-{}: seq {}, bytes {}, CRCs {}'\n> -              .format(ts / 1000000000, fps,\n> -                      ctx['id'], stream_name,\n> -                      meta.sequence, meta.bytesused,\n> -                      crcs))\n> +        for ctx in self.contexts:\n> +            ctx.release()\n>  \n> -        if ctx['opt-metadata']:\n> -            reqmeta = req.metadata\n> -            for ctrl, val in reqmeta.items():\n> -                print(f'\\t{ctrl} = {val}')\n> +    def do_cmd_capture(self):\n> +        self.__capture_init()\n>  \n> -        if ctx['opt-save-frames']:\n> -            with libcamera.utils.MappedFrameBuffer(fb) as mfb:\n> -                filename = 'frame-{}-{}-{}.data'.format(ctx['id'], stream_name, ctx['reqs-completed'])\n> -                with open(filename, 'wb') as f:\n> -                    for p in mfb.planes:\n> -                        f.write(p)\n> +        renderer = self.renderer\n>  \n> -    state['renderer'].request_handler(ctx, req)\n> +        renderer.setup()\n>  \n> -    ctx['reqs-completed'] += 1\n> +        self.__capture_start()\n>  \n> +        renderer.run()\n>  \n> -# Called from renderer when it has finished with a request\n> -def request_prcessed(ctx, req):\n> -    camera = ctx['camera']\n> +        self.__capture_deinit()\n>  \n> -    if ctx['reqs-queued'] < ctx['opt-capture']:\n> -        req.reuse()\n> -        camera.queue_request(req)\n> -        ctx['reqs-queued'] += 1\n>  \n> +class CustomAction(argparse.Action):\n> +    def __init__(self, option_strings, dest, **kwargs):\n> +        super().__init__(option_strings, dest, default={}, **kwargs)\n>  \n> -def capture_deinit(contexts):\n> -    for ctx in contexts:\n> -        stop(ctx)\n> +    def __call__(self, parser, namespace, values, option_string=None):\n> +        if len(namespace.camera) == 0:\n> +            print(f'Option {option_string} requires a --camera context')\n> +            sys.exit(-1)\n>  \n> -    for ctx in contexts:\n> -        release(ctx)\n> +        if self.type == bool:\n> +            values = True\n>  \n> +        current = namespace.camera[-1]\n>  \n> -def do_cmd_capture(state):\n> -    capture_init(state['contexts'])\n> +        data = getattr(namespace, self.dest)\n>  \n> -    renderer = state['renderer']\n> +        if self.nargs == '+':\n> +            if current not in data:\n> +                data[current] = []\n>  \n> -    renderer.setup()\n> +            data[current] += values\n> +        else:\n> +            data[current] = values\n>  \n> -    capture_start(state['contexts'])\n>  \n> -    renderer.run()\n> +def do_cmd_list(cm):\n> +    print('Available cameras:')\n>  \n> -    capture_deinit(state['contexts'])\n> +    for idx, c in enumerate(cm.cameras):\n> +        print(f'{idx + 1}: {c.id}')\n>  \n>  \n>  def main():\n> @@ -422,39 +444,28 @@ def main():\n>              print('Unable to find camera', cam_idx)\n>              return -1\n>  \n> -        contexts.append({\n> -                        'camera': camera,\n> -                        'idx': cam_idx,\n> -                        'id': 'cam' + str(cam_idx),\n> -                        'reqs-queued': 0,\n> -                        'reqs-completed': 0,\n> -                        'opt-capture': args.capture.get(cam_idx, False),\n> -                        'opt-crc': args.crc.get(cam_idx, False),\n> -                        'opt-save-frames': args.save_frames.get(cam_idx, False),\n> -                        'opt-metadata': args.metadata.get(cam_idx, False),\n> -                        'opt-strict-formats': args.strict_formats.get(cam_idx, False),\n> -                        'opt-stream': args.stream.get(cam_idx, ['role=viewfinder']),\n> -                        })\n> +        ctx = CameraContext(camera, cam_idx)\n> +        ctx.opt_capture = args.capture.get(cam_idx, 0)\n> +        ctx.opt_crc = args.crc.get(cam_idx, False)\n> +        ctx.opt_save_frames = args.save_frames.get(cam_idx, False)\n> +        ctx.opt_metadata = args.metadata.get(cam_idx, False)\n> +        ctx.opt_strict_formats = args.strict_formats.get(cam_idx, False)\n> +        ctx.opt_stream = args.stream.get(cam_idx, ['role=viewfinder'])\n> +        contexts.append(ctx)\n>  \n>      for ctx in contexts:\n> -        print('Using camera {} as {}'.format(ctx['camera'].id, ctx['id']))\n> +        print('Using camera {} as {}'.format(ctx.camera.id, ctx.id))\n>  \n>      for ctx in contexts:\n>          if args.list_properties:\n> -            do_cmd_list_props(ctx)\n> +            ctx.do_cmd_list_props()\n>          if args.list_controls:\n> -            do_cmd_list_controls(ctx)\n> +            ctx.do_cmd_list_controls()\n>          if args.info:\n> -            do_cmd_info(ctx)\n> +            ctx.do_cmd_info()\n>  \n>      if args.capture:\n> -\n> -        state = {\n> -            'cm': cm,\n> -            'contexts': contexts,\n> -            'event_handler': event_handler,\n> -            'request_prcessed': request_prcessed,\n> -        }\n> +        state = CaptureState(cm, contexts)\n>  \n>          if args.renderer == 'null':\n>              import cam_null\n> @@ -472,9 +483,9 @@ def main():\n>              print('Bad renderer', args.renderer)\n>              return -1\n>  \n> -        state['renderer'] = renderer\n> +        state.renderer = renderer\n>  \n> -        do_cmd_capture(state)\n> +        state.do_cmd_capture()\n>  \n>      return 0\n>  \n> diff --git a/src/py/cam/cam_kms.py b/src/py/cam/cam_kms.py\n> index 74cd3b38..213e0b03 100644\n> --- a/src/py/cam/cam_kms.py\n> +++ b/src/py/cam/cam_kms.py\n> @@ -10,8 +10,8 @@ class KMSRenderer:\n>      def __init__(self, state):\n>          self.state = state\n>  \n> -        self.cm = state['cm']\n> -        self.contexts = state['contexts']\n> +        self.cm = state.cm\n> +        self.contexts = state.contexts\n>          self.running = False\n>  \n>          card = pykms.Card()\n> @@ -92,7 +92,7 @@ class KMSRenderer:\n>          if old:\n>              req = old['camreq']\n>              ctx = old['camctx']\n> -            self.state['request_prcessed'](ctx, req)\n> +            self.state.request_processed(ctx, req)\n>  \n>      def queue(self, drmreq):\n>          if not self.next:\n> @@ -108,7 +108,7 @@ class KMSRenderer:\n>  \n>          idx = 0\n>          for ctx in self.contexts:\n> -            for stream in ctx['streams']:\n> +            for stream in ctx.streams:\n>  \n>                  cfg = stream.configuration\n>                  fmt = cfg.pixel_format\n> @@ -125,7 +125,7 @@ class KMSRenderer:\n>                      'size': cfg.size,\n>                  })\n>  \n> -                for fb in ctx['allocator'].buffers(stream):\n> +                for fb in ctx.allocator.buffers(stream):\n>                      w = cfg.size.width\n>                      h = cfg.size.height\n>                      fds = []\n> @@ -148,7 +148,7 @@ class KMSRenderer:\n>                  self.handle_page_flip(ev.seq, ev.time)\n>  \n>      def readcam(self, fd):\n> -        self.running = self.state['event_handler'](self.state)\n> +        self.running = self.state.event_handler()\n>  \n>      def readkey(self, fileobj):\n>          sys.stdin.readline()\n> diff --git a/src/py/cam/cam_null.py b/src/py/cam/cam_null.py\n> index a6da9671..45c5f467 100644\n> --- a/src/py/cam/cam_null.py\n> +++ b/src/py/cam/cam_null.py\n> @@ -9,8 +9,8 @@ class NullRenderer:\n>      def __init__(self, state):\n>          self.state = state\n>  \n> -        self.cm = state['cm']\n> -        self.contexts = state['contexts']\n> +        self.cm = state.cm\n> +        self.contexts = state.contexts\n>  \n>          self.running = False\n>  \n> @@ -37,11 +37,11 @@ class NullRenderer:\n>          print('Exiting...')\n>  \n>      def readcam(self, fd):\n> -        self.running = self.state['event_handler'](self.state)\n> +        self.running = self.state.event_handler()\n>  \n>      def readkey(self, fileobj):\n>          sys.stdin.readline()\n>          self.running = False\n>  \n>      def request_handler(self, ctx, req):\n> -        self.state['request_prcessed'](ctx, req)\n> +        self.state.request_processed(ctx, req)\n> diff --git a/src/py/cam/cam_qt.py b/src/py/cam/cam_qt.py\n> index 03096920..d638e9cc 100644\n> --- a/src/py/cam/cam_qt.py\n> +++ b/src/py/cam/cam_qt.py\n> @@ -176,8 +176,8 @@ class QtRenderer:\n>      def __init__(self, state):\n>          self.state = state\n>  \n> -        self.cm = state['cm']\n> -        self.contexts = state['contexts']\n> +        self.cm = state.cm\n> +        self.contexts = state.contexts\n>  \n>      def setup(self):\n>          self.app = QtWidgets.QApplication([])\n> @@ -185,7 +185,7 @@ class QtRenderer:\n>          windows = []\n>  \n>          for ctx in self.contexts:\n> -            for stream in ctx['streams']:\n> +            for stream in ctx.streams:\n>                  window = MainWindow(ctx, stream)\n>                  window.show()\n>                  windows.append(window)\n> @@ -206,7 +206,7 @@ class QtRenderer:\n>          print('Exiting...')\n>  \n>      def readcam(self):\n> -        running = self.state['event_handler'](self.state)\n> +        running = self.state.event_handler()\n>  \n>          if not running:\n>              self.app.quit()\n> @@ -223,7 +223,7 @@ class QtRenderer:\n>  \n>              wnd.handle_request(stream, fb)\n>  \n> -        self.state['request_prcessed'](ctx, req)\n> +        self.state.request_processed(ctx, req)\n>  \n>      def cleanup(self):\n>          for w in self.windows:\n> @@ -254,7 +254,7 @@ class MainWindow(QtWidgets.QWidget):\n>          group.setLayout(groupLayout)\n>          controlsLayout.addWidget(group)\n>  \n> -        lab = QtWidgets.QLabel(ctx['id'])\n> +        lab = QtWidgets.QLabel(ctx.id)\n>          groupLayout.addWidget(lab)\n>  \n>          self.frameLabel = QtWidgets.QLabel()\n> @@ -265,7 +265,7 @@ class MainWindow(QtWidgets.QWidget):\n>          group.setLayout(groupLayout)\n>          controlsLayout.addWidget(group)\n>  \n> -        camera = ctx['camera']\n> +        camera = ctx.camera\n>  \n>          for k, v in camera.properties.items():\n>              lab = QtWidgets.QLabel()\n> @@ -308,4 +308,4 @@ class MainWindow(QtWidgets.QWidget):\n>          self.label.setPixmap(pix)\n>  \n>          self.frameLabel.setText('Queued: {}\\nDone: {}\\nFps: {:.2f}'\n> -                                .format(ctx['reqs-queued'], ctx['reqs-completed'], ctx['fps']))\n> +                                .format(ctx.reqs_queued, ctx.reqs_completed, ctx.fps))\n> diff --git a/src/py/cam/cam_qtgl.py b/src/py/cam/cam_qtgl.py\n> index c9e367a2..5f7ccf1e 100644\n> --- a/src/py/cam/cam_qtgl.py\n> +++ b/src/py/cam/cam_qtgl.py\n> @@ -142,7 +142,7 @@ class QtRenderer:\n>          self.window = window\n>  \n>      def run(self):\n> -        camnotif = QtCore.QSocketNotifier(self.state['cm'].efd, QtCore.QSocketNotifier.Read)\n> +        camnotif = QtCore.QSocketNotifier(self.state.cm.efd, QtCore.QSocketNotifier.Read)\n>          camnotif.activated.connect(lambda _: self.readcam())\n>  \n>          keynotif = QtCore.QSocketNotifier(sys.stdin.fileno(), QtCore.QSocketNotifier.Read)\n> @@ -155,7 +155,7 @@ class QtRenderer:\n>          print('Exiting...')\n>  \n>      def readcam(self):\n> -        running = self.state['event_handler'](self.state)\n> +        running = self.state.event_handler()\n>  \n>          if not running:\n>              self.app.quit()\n> @@ -184,12 +184,12 @@ class MainWindow(QtWidgets.QWidget):\n>          self.reqqueue = {}\n>          self.current = {}\n>  \n> -        for ctx in self.state['contexts']:\n> +        for ctx in self.state.contexts:\n>  \n> -            self.reqqueue[ctx['idx']] = []\n> -            self.current[ctx['idx']] = []\n> +            self.reqqueue[ctx.idx] = []\n> +            self.current[ctx.idx] = []\n>  \n> -            for stream in ctx['streams']:\n> +            for stream in ctx.streams:\n>                  self.textures[stream] = None\n>  \n>          num_tiles = len(self.textures)\n> @@ -312,12 +312,12 @@ class MainWindow(QtWidgets.QWidget):\n>              if len(queue) == 0:\n>                  continue\n>  \n> -            ctx = next(ctx for ctx in self.state['contexts'] if ctx['idx'] == ctx_idx)\n> +            ctx = next(ctx for ctx in self.state.contexts if ctx.idx == ctx_idx)\n>  \n>              if self.current[ctx_idx]:\n>                  old = self.current[ctx_idx]\n>                  self.current[ctx_idx] = None\n> -                self.state['request_prcessed'](ctx, old)\n> +                self.state.request_processed(ctx, old)\n>  \n>              next_req = queue.pop(0)\n>              self.current[ctx_idx] = next_req\n> @@ -336,8 +336,8 @@ class MainWindow(QtWidgets.QWidget):\n>  \n>          size = self.size()\n>  \n> -        for idx, ctx in enumerate(self.state['contexts']):\n> -            for stream in ctx['streams']:\n> +        for idx, ctx in enumerate(self.state.contexts):\n> +            for stream in ctx.streams:\n>                  if self.textures[stream] is None:\n>                      continue\n>  \n> @@ -359,5 +359,5 @@ class MainWindow(QtWidgets.QWidget):\n>          assert(b)\n>  \n>      def handle_request(self, ctx, req):\n> -        self.reqqueue[ctx['idx']].append(req)\n> +        self.reqqueue[ctx.idx].append(req)\n>          self.update()","headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id A353EBD161\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri, 27 May 2022 06:53:59 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 0653D633A3;\n\tFri, 27 May 2022 08:53:59 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id E78CB61FB6\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 27 May 2022 08:53:57 +0200 (CEST)","from pendragon.ideasonboard.com (ip-109-40-242-63.web.vodafone.de\n\t[109.40.242.63])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id F2EA831A;\n\tFri, 27 May 2022 08:53:56 +0200 (CEST)"],"DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1653634439;\n\tbh=xmXctt9ynh7NAJB9KRVJLgE6De0FB0z4RzrOHVAPtCQ=;\n\th=Date:To:References:In-Reply-To:Subject:List-Id:List-Unsubscribe:\n\tList-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:Cc:\n\tFrom;\n\tb=3l2h35arFTlOVlqbMHL1osGyBTmTqZbX+xNdauHmZsmS35ccozzdAdjvEVFAkGV0w\n\t3R1Ha1ameOvbO+CeQMmWDKhKRy/K7O5+j5njOzvdqFQbxgovMVoOzp1G6yrVh2Dpmy\n\tMnjyoBDZfULIsP7obAgAetv5NuCELw3ore8J9kgFP87tairAdH6NslmUmB1vfre3KV\n\tQ11QjvwTyGuw4F9hucColttOUdAY/mz1iPWMDyYYTGbinGkt/aWlVhi2pxIvGcsh6K\n\totomMLrRcUtQrpDcwrzxmSaKHdiEvthgIFZdYljSAvPikpg+Q8ohcXUjcIBwTH0kk7\n\tkhAbrqT8Ld2Sw==","v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1653634437;\n\tbh=xmXctt9ynh7NAJB9KRVJLgE6De0FB0z4RzrOHVAPtCQ=;\n\th=Date:From:To:Cc:Subject:References:In-Reply-To:From;\n\tb=Acrzg3ukMuQKPyAQ2LJQ9l/w0Jrk4JqOGgolhBi0qnHPStEnUD6mfcNKIH04ebtec\n\tNdh8E9+tKx57JpNd0v08GPaHmlGkTcRt3lldyQxIqUFpDwkvZ4z8pVoqPam4v4Graa\n\tZjKH8mjGxOIM2WdcTbLBqJP6n0XLxESwk67HExn0="],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key; \n\tunprotected) header.d=ideasonboard.com\n\theader.i=@ideasonboard.com\n\theader.b=\"Acrzg3uk\"; dkim-atps=neutral","Date":"Fri, 27 May 2022 09:53:52 +0300","To":"Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>","Message-ID":"<YpB1gO7VlitqsONA@pendragon.ideasonboard.com>","References":"<20220524114610.41848-1-tomi.valkeinen@ideasonboard.com>\n\t<20220524114610.41848-14-tomi.valkeinen@ideasonboard.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8","Content-Disposition":"inline","In-Reply-To":"<20220524114610.41848-14-tomi.valkeinen@ideasonboard.com>","Subject":"Re: [libcamera-devel] [PATCH v2 13/19] py: cam: Convert ctx and\n\tstate to classes","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","From":"Laurent Pinchart via libcamera-devel\n\t<libcamera-devel@lists.libcamera.org>","Reply-To":"Laurent Pinchart <laurent.pinchart@ideasonboard.com>","Cc":"libcamera-devel@lists.libcamera.org,\n\tTomi Valkeinen <tomi.valkeinen@iki.fi>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}}]