Show a patch.

GET /api/patches/16082/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 16082,
    "url": "https://patchwork.libcamera.org/api/patches/16082/?format=api",
    "web_url": "https://patchwork.libcamera.org/patch/16082/",
    "project": {
        "id": 1,
        "url": "https://patchwork.libcamera.org/api/projects/1/?format=api",
        "name": "libcamera",
        "link_name": "libcamera",
        "list_id": "libcamera_core",
        "list_email": "libcamera-devel@lists.libcamera.org",
        "web_url": "",
        "scm_url": "",
        "webscm_url": ""
    },
    "msgid": "<20220527144447.94891-11-tomi.valkeinen@ideasonboard.com>",
    "date": "2022-05-27T14:44:27",
    "name": "[libcamera-devel,v3,10/30] py: cam: Convert ctx and state to classes",
    "commit_ref": null,
    "pull_url": null,
    "state": "accepted",
    "archived": false,
    "hash": "ed521b5a17854f3ffeb54c690f30480958fc79a8",
    "submitter": {
        "id": 109,
        "url": "https://patchwork.libcamera.org/api/people/109/?format=api",
        "name": "Tomi Valkeinen",
        "email": "tomi.valkeinen@ideasonboard.com"
    },
    "delegate": null,
    "mbox": "https://patchwork.libcamera.org/patch/16082/mbox/",
    "series": [
        {
            "id": 3146,
            "url": "https://patchwork.libcamera.org/api/series/3146/?format=api",
            "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=3146",
            "date": "2022-05-27T14:44:17",
            "name": "More misc Python patches",
            "version": 3,
            "mbox": "https://patchwork.libcamera.org/series/3146/mbox/"
        }
    ],
    "comments": "https://patchwork.libcamera.org/api/patches/16082/comments/",
    "check": "pending",
    "checks": "https://patchwork.libcamera.org/api/patches/16082/checks/",
    "tags": {},
    "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 6E646BD161\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri, 27 May 2022 14:45:23 +0000 (UTC)",
            "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id E9DF565634;\n\tFri, 27 May 2022 16:45:22 +0200 (CEST)",
            "from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id B36E665632\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 27 May 2022 16:45:09 +0200 (CEST)",
            "from deskari.lan (91-156-85-209.elisa-laajakaista.fi\n\t[91.156.85.209])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 1D68412F3;\n\tFri, 27 May 2022 16:45:09 +0200 (CEST)"
        ],
        "DKIM-Signature": [
            "v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1653662723;\n\tbh=3XTegwOALyIVfX+/RY3EzWuXBIi8GmuOGgF1U0zPDdM=;\n\th=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe:\n\tList-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:\n\tFrom;\n\tb=QKxe/16iatbShzLa/EigQNFFKfuPGcQqCCravbCGBt9MMkCXa+650Jw42qidQVYvZ\n\tvP4Q98dBDhiiOa5ftNXQ+mh5iplitAeki+IIA+/lhYSfTwB6iLiM/meRTKK6PdtMqr\n\tHXc54s8MtXyQoNDBIaQ85w3HGDqm1oJJLlKSWPQlpDVGetavjcv4ImX787XISCUsYZ\n\tldSy8R+VcJh3fslBJCAofnAlu5LTWK5jOtv1UrTadOQp67IKiee0eD8hopPvtT9OrH\n\tN+o6J7MqzAbSR8RDSyYy7CWEGDrcX1hb5vs0a8i7JX835Z0edxD6C8seyvsgDJfPew\n\tcyxaL8hSyVVxQ==",
            "v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1653662709;\n\tbh=3XTegwOALyIVfX+/RY3EzWuXBIi8GmuOGgF1U0zPDdM=;\n\th=From:To:Cc:Subject:Date:In-Reply-To:References:From;\n\tb=nBmAeiJ8ikwjqxpRRAdu/WJYHol/rxmyV1/3e/1BSSHA2bI5kKmkjcvzRI1f2JGW3\n\tKlhyWGFJCmpvHraFCltAITNRK0GfVjxVXVQf46VC7FcCUJcOosGB3O7pxU2naMPBJy\n\tzXDKPRDE2sLaku4sEZ7XvnmHF3Eq0XzDUSIAjY60="
        ],
        "Authentication-Results": "lancelot.ideasonboard.com; dkim=pass (1024-bit key; \n\tunprotected) header.d=ideasonboard.com\n\theader.i=@ideasonboard.com\n\theader.b=\"nBmAeiJ8\"; dkim-atps=neutral",
        "To": "libcamera-devel@lists.libcamera.org,\n\tDavid Plowman <david.plowman@raspberrypi.com>,\n\tKieran Bingham <kieran.bingham@ideasonboard.com>,\n\tLaurent Pinchart <laurent.pinchart@ideasonboard.com>,\n\tJacopo Mondi <jacopo@jmondi.org>",
        "Date": "Fri, 27 May 2022 17:44:27 +0300",
        "Message-Id": "<20220527144447.94891-11-tomi.valkeinen@ideasonboard.com>",
        "X-Mailer": "git-send-email 2.34.1",
        "In-Reply-To": "<20220527144447.94891-1-tomi.valkeinen@ideasonboard.com>",
        "References": "<20220527144447.94891-1-tomi.valkeinen@ideasonboard.com>",
        "MIME-Version": "1.0",
        "Content-Transfer-Encoding": "8bit",
        "Subject": "[libcamera-devel] [PATCH v3 10/30] py: cam: Convert ctx and state\n\tto 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": "Tomi Valkeinen via libcamera-devel\n\t<libcamera-devel@lists.libcamera.org>",
        "Reply-To": "Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>",
        "Errors-To": "libcamera-devel-bounces@lists.libcamera.org",
        "Sender": "\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"
    },
    "content": "Convert ctx and state dicts to classes. No functional changes.\n\nSigned-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\nReviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n---\n src/py/cam/cam.py      | 580 ++++++++++++++++++++---------------------\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, 310 insertions(+), 328 deletions(-)",
    "diff": "diff --git a/src/py/cam/cam.py b/src/py/cam/cam.py\nindex 64f67e86..c8ffb084 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,371 @@ 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-\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-        if self.type == bool:\n-            values = True\n-\n-        current = namespace.camera[-1]\n-\n-        data = getattr(namespace, self.dest)\n-\n-        if self.nargs == '+':\n-            if current not in data:\n-                data[current] = []\n-\n-            data[current] += values\n-        else:\n-            data[current] = values\n-\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+class CameraContext:\n+    camera: libcam.Camera\n+    id: str\n+    idx: int\n \n-    for name, prop in camera.properties.items():\n-        print('\\t{}: {}'.format(name, prop))\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+    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-def do_cmd_list_controls(ctx):\n-    camera = ctx['camera']\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-    print('Controls for', ctx['id'])\n+    def do_cmd_list_props(self):\n+        print('Properties for', self.id)\n \n-    for name, prop in camera.controls.items():\n-        print('\\t{}: {}'.format(name, prop))\n+        for name, prop in self.camera.properties.items():\n+            print('\\t{}: {}'.format(name, prop))\n \n+    def do_cmd_list_controls(self):\n+        print('Controls for', self.id)\n \n-def do_cmd_info(ctx):\n-    camera = ctx['camera']\n+        for name, prop in self.camera.controls.items():\n+            print('\\t{}: {}'.format(name, prop))\n \n-    print('Stream info for', ctx['id'])\n+    def do_cmd_info(self):\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 = self.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+        self.camera.acquire()\n \n-def acquire(ctx):\n-    camera = ctx['camera']\n+    def release(self):\n+        self.camera.release()\n \n-    camera.acquire()\n+    def __parse_streams(self):\n+        streams = []\n \n+        for stream_desc in self.opt_stream:\n+            stream_opts: dict[str, Any]\n+            stream_opts = {'role': libcam.StreamRole.Viewfinder}\n \n-def release(ctx):\n-    camera = ctx['camera']\n+            for stream_opt in stream_desc.split(','):\n+                if stream_opt == 0:\n+                    continue\n \n-    camera.release()\n-\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-\n-                if role is None:\n-                    print('Bad stream role', value)\n+                arr = stream_opt.split('=')\n+                if len(arr) != 2:\n+                    print('Bad stream option', stream_opt)\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-            stream_opts[key] = value\n-\n-        streams.append(stream_opts)\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-    return streams\n+                stream_opts[key] = value\n \n+            streams.append(stream_opts)\n \n-def configure(ctx):\n-    camera = ctx['camera']\n+        return streams\n \n-    streams = parse_streams(ctx)\n+    def configure(self):\n+        streams = self.__parse_streams()\n \n-    roles = [opts['role'] for opts in streams]\n+        roles = [opts['role'] for opts in streams]\n \n-    camconfig = camera.generate_configuration(roles)\n-    if camconfig is None:\n-        raise Exception('Generating config failed')\n+        camconfig = self.camera.generate_configuration(roles)\n+        if camconfig is None:\n+            raise Exception('Generating config failed')\n \n-    for idx, stream_opts in enumerate(streams):\n-        stream_config = camconfig.at(idx)\n+        for idx, stream_opts in enumerate(streams):\n+            stream_config = camconfig.at(idx)\n \n-        if 'width' in stream_opts:\n-            stream_config.size.width = stream_opts['width']\n+            if 'width' in stream_opts:\n+                stream_config.size.width = stream_opts['width']\n \n-        if 'height' in stream_opts:\n-            stream_config.size.height = stream_opts['height']\n+            if 'height' in stream_opts:\n+                stream_config.size.height = stream_opts['height']\n \n-        if 'pixelformat' in stream_opts:\n-            stream_config.pixel_format = libcam.PixelFormat(stream_opts['pixelformat'])\n+            if 'pixelformat' in stream_opts:\n+                stream_config.pixel_format = libcam.PixelFormat(stream_opts['pixelformat'])\n \n-    stat = camconfig.validate()\n+        stat = camconfig.validate()\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+        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-        print('Camera configuration adjusted')\n-\n-    r = camera.configure(camconfig)\n-    if r != 0:\n-        raise Exception('Configure failed')\n-\n-    ctx['stream-names'] = {}\n-    ctx['streams'] = []\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-\n-\n-def alloc_buffers(ctx):\n-    camera = ctx['camera']\n-\n-    allocator = libcam.FrameBufferAllocator(camera)\n+            print('Camera configuration adjusted')\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+        r = self.camera.configure(camconfig)\n+        if r != 0:\n+            raise Exception('Configure failed')\n \n-        allocated = len(allocator.buffers(stream))\n+        self.stream_names = {}\n+        self.streams = []\n \n-        print('{}-{}: Allocated {} buffers'.format(ctx['id'], ctx['stream-names'][stream], allocated))\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['allocator'] = allocator\n+    def alloc_buffers(self):\n+        allocator = libcam.FrameBufferAllocator(self.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 create_requests(ctx):\n-    camera = ctx['camera']\n+            allocated = len(allocator.buffers(stream))\n \n-    ctx['requests'] = []\n+            print('{}-{}: Allocated {} buffers'.format(self.id, self.stream_names[stream], allocated))\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+        self.allocator = allocator\n \n-    requests = []\n+    def create_requests(self):\n+        self.requests = []\n \n-    for buf_num in range(num_bufs):\n-        request = camera.create_request(ctx['idx'])\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-        if request is None:\n-            print('Can not create request')\n-            exit(-1)\n+        requests = []\n \n-        for stream in ctx['streams']:\n-            buffers = ctx['allocator'].buffers(stream)\n-            buffer = buffers[buf_num]\n+        for buf_num in range(num_bufs):\n+            request = self.camera.create_request(self.idx)\n \n-            ret = request.add_buffer(stream, buffer)\n-            if ret < 0:\n-                print('Can not set buffer for request')\n+            if request is None:\n+                print('Can not create request')\n                 exit(-1)\n \n-        requests.append(request)\n+            for stream in self.streams:\n+                buffers = self.allocator.buffers(stream)\n+                buffer = buffers[buf_num]\n \n-    ctx['requests'] = 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+            requests.append(request)\n \n-def start(ctx):\n-    camera = ctx['camera']\n+        self.requests = requests\n \n-    camera.start()\n+    def start(self):\n+        self.camera.start()\n \n+    def stop(self):\n+        self.camera.stop()\n \n-def stop(ctx):\n-    camera = ctx['camera']\n+    def queue_requests(self):\n+        for request in self.requests:\n+            self.camera.queue_request(request)\n+            self.reqs_queued += 1\n \n-    camera.stop()\n+        del self.requests\n \n \n-def queue_requests(ctx):\n-    camera = ctx['camera']\n+class CaptureState:\n+    cm: libcam.CameraManager\n+    contexts: list[CameraContext]\n+    renderer: Any\n \n-    for request in ctx['requests']:\n-        camera.queue_request(request)\n-        ctx['reqs-queued'] += 1\n+    def __init__(self, cm, contexts):\n+        self.cm = cm\n+        self.contexts = contexts\n \n-    del ctx['requests']\n+    # Called from renderer when there is a libcamera event\n+    def event_handler(self):\n+        try:\n+            self.cm.read_event()\n \n+            reqs = self.cm.get_ready_requests()\n \n-def capture_init(contexts):\n-    for ctx in contexts:\n-        acquire(ctx)\n+            for req in reqs:\n+                ctx = next(ctx for ctx in self.contexts if ctx.idx == req.cookie)\n+                self.__request_handler(ctx, req)\n \n-    for ctx in contexts:\n-        configure(ctx)\n+            running = any(ctx.reqs_completed < ctx.opt_capture for ctx in self.contexts)\n+            return running\n+        except Exception:\n+            traceback.print_exc()\n+            return False\n \n-    for ctx in contexts:\n-        alloc_buffers(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-        create_requests(ctx)\n+        buffers = req.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.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-def capture_start(contexts):\n-    for ctx in contexts:\n-        start(ctx)\n+        for stream, fb in buffers.items():\n+            stream_name = ctx.stream_names[stream]\n \n-    for ctx in contexts:\n-        queue_requests(ctx)\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+            meta = fb.metadata\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+            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-        cm.read_event()\n+            if ctx.opt_metadata:\n+                reqmeta = req.metadata\n+                for ctrl, val in reqmeta.items():\n+                    print(f'\\t{ctrl} = {val}')\n \n-        reqs = cm.get_ready_requests()\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-        for req in reqs:\n-            ctx = next(ctx for ctx in contexts if ctx['idx'] == req.cookie)\n-            request_handler(state, ctx, req)\n+        self.renderer.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+        ctx.reqs_completed += 1\n \n+    # Called from renderer when it has finished with a request\n+    def request_processed(self, ctx, req):\n+        if ctx.reqs_queued < ctx.opt_capture:\n+            req.reuse()\n+            ctx.camera.queue_request(req)\n+            ctx.reqs_queued += 1\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+    def __capture_init(self):\n+        for ctx in self.contexts:\n+            ctx.acquire()\n \n-    buffers = req.buffers\n+        for ctx in self.contexts:\n+            ctx.configure()\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.alloc_buffers()\n \n-    for stream, fb in buffers.items():\n-        stream_name = ctx['stream-names'][stream]\n+        for ctx in self.contexts:\n+            ctx.create_requests()\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+    def __capture_start(self):\n+        for ctx in self.contexts:\n+            ctx.start()\n \n-        meta = fb.metadata\n+        for ctx in self.contexts:\n+            ctx.queue_requests()\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+    def __capture_deinit(self):\n+        for ctx in self.contexts:\n+            ctx.stop()\n \n-        if ctx['opt-metadata']:\n-            reqmeta = req.metadata\n-            for ctrl, val in reqmeta.items():\n-                print(f'\\t{ctrl} = {val}')\n+        for ctx in self.contexts:\n+            ctx.release()\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+    def do_cmd_capture(self):\n+        self.__capture_init()\n \n-    state['renderer'].request_handler(ctx, req)\n+        self.renderer.setup()\n \n-    ctx['reqs-completed'] += 1\n+        self.__capture_start()\n \n+        self.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 +415,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 +454,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 \ndiff --git a/src/py/cam/cam_kms.py b/src/py/cam/cam_kms.py\nindex 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()\ndiff --git a/src/py/cam/cam_null.py b/src/py/cam/cam_null.py\nindex 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)\ndiff --git a/src/py/cam/cam_qt.py b/src/py/cam/cam_qt.py\nindex 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))\ndiff --git a/src/py/cam/cam_qtgl.py b/src/py/cam/cam_qtgl.py\nindex 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()\n",
    "prefixes": [
        "libcamera-devel",
        "v3",
        "10/30"
    ]
}