Skip to content
C.W.K.
Lesson 03 of 05 · published

One Root, Many Devices

~14 min · oop, polymorphism, root-class, registry, test-pattern

Level 0Closed Lens
0 XP0/45 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

Consumers Know Only the Root

Oriel watches very different things. A capture card behind a game console. A USB webcam with a motorized gimbal. A phone used as a camera body. A network camera that another family app, Haven, can reach. A synthetic test pattern. If every consumer (sessions, looks, the page, the gate) had to know which of these it was talking to, every new kind of device would mean editing every consumer. The family has a standing rule against exactly that shape: Dad calls a non-object-oriented architecture a sin, and the fix he keeps pointing at is a canonical root class whose children pay their own tax.

In Oriel that root is WatchSource, in backend/sources.py. Its docstring opens with the whole contract: consumers know only WatchSource; a child says how its device is opened, recorded and peeked at. The root declares six abstract methods (modes, present, start, stop, state, peek) and one shared one, describe, which the page reads.

Four Children, Each Paying Its Own Tax

ChildKindWhat it isIts tax
UvcCaptureSourceuvcany camera-class device AVFoundation shows: capture cards, webcams, a Continuity Cameraasks the recorder through the file channel; modes come from the device
TestPatternSourcetestffmpeg's test patternrecords itself with ffmpeg in exactly the same segment and sidecar layout; needs no grant
CordeliaSourcecordeliaa camera of a device body on a phone or tabletstills on request through the body's own wire; refuses to record
HavenCameraSourceHaven's kinda camera Haven reaches over the networkstills on request through Haven, which asks Oriel's gate itself; refuses to record

Look at the last two. A phone body and a network camera cannot keep a continuous recording on Oriel's disk, and they say so in the only honest way: their start raises "takes stills on request; it does not record". The root did not grow a can_record flag for them. The children refuse what they cannot do, and the consumers handle the refusal the way they handle any other.

Notice also where those two children live: not in sources.py, but in cordelia.py and haven_cameras.py, next to the code that knows phones and Haven. The root registers every child automatically through __init_subclass__, so adding a kind is a new class in its own module, and the root file is not edited.

The Test Pattern Is a Real Child

The humblest child is the most useful. TestPatternSource renders ffmpeg's testsrc2 and records it into the same fragmented MP4 segments, with the same JSON sidecars, that the recorder writes for a real card. Because it is a full child, not a mock, everything downstream runs against it unchanged: the frame index, every look, every budget test, a session's start and stop, an engine restart mid-session. The suite exercises the real recording format without a single camera, and when a device is missing in development, the test pattern stands in for it.

That is the payoff of a root class done properly. A test double that obeys the same contract is not a special path through the code; it is just another child.

Devices Are Rows; Classes Are Kinds

One more design choice keeps this clean. An input in Oriel's database is a row: an id, a kind, a unique ID, a name, a role. WatchSource.for_row looks the kind up in the registry and builds the right child for that row. So the code never names a device. Two identical capture cards are two rows of kind uvc; a camera that moves to another Mac is a row whose unique ID carries that Mac's name. The class says how a kind of device behaves. The row says which device this is.

Code

The root and its self-registering children (trimmed excerpt, backend/sources.py and backend/cordelia.py)·python
class WatchSource(ABC):
    kind: ClassVar[str]
    KINDS: ClassVar[dict[str, type["WatchSource"]]] = {}

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        WatchSource.KINDS[cls.kind] = cls          # a new child registers itself

    @classmethod
    def for_row(cls, row, link, ffmpeg) -> "WatchSource":
        kind = cls.KINDS.get(row["kind"])
        if kind is None:
            raise SourceError(f"no watch source of kind {row['kind']}")
        return kind(row, link, ffmpeg)

    @abstractmethod
    def start(self, *, session_id, out_dir, mode, bitrate, gop, segment_seconds,
              latest_every, audio=None) -> None:
        """Begin recording into ``out_dir``: segments, sidecars, the latest frame."""


# backend/cordelia.py: a child in its own module
class CordeliaSource(WatchSource):
    """A camera of a Cordelia body: stills on request, never a recording."""

    kind = "cordelia"

    def start(self, **_):
        raise SourceError(f"{self.row['name']} takes stills on request; it does not record")
Build your own self-registering root and add a child without touching it (runnable)·python
from abc import ABC, abstractmethod


class Source(ABC):
    KINDS = {}

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        Source.KINDS[cls.kind] = cls

    def __init__(self, row):
        self.row = row

    @classmethod
    def for_row(cls, row):
        return cls.KINDS[row["kind"]](row)

    @abstractmethod
    def start(self): ...


class Webcam(Source):
    kind = "uvc"

    def start(self):
        return f"recording {self.row['name']}"


class StillsOnly(Source):           # imagine this lives in another module
    kind = "stills"

    def start(self):
        raise RuntimeError(f"{self.row['name']} takes stills on request; it does not record")


rows = [{"kind": "uvc", "name": "desk webcam"}, {"kind": "stills", "name": "phone body"}]
for row in rows:
    source = Source.for_row(row)
    try:
        print(source.start())
    except RuntimeError as exc:
        print("refused:", exc)

External links

Exercise

Extend the runnable sketch with a third child, TestPattern, whose start writes three fake segment files and a JSON sidecar for each into a temporary folder. Then write one consumer function, record_all(rows), that starts every source and reports which recorded, which refused and why, without an if kind == anywhere in it.
Hint
If you need to know the kind inside the consumer, the contract is missing a method. Add it to the root as abstract, implement it in every child, and the consumer goes back to knowing only the root.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.