from __future__ import annotations
import os
import time
import threading
import logging
import traceback
import shutil
import re
import io
import socket
from datetime import datetime
from datetime import timezone
from zeroconf import ServiceBrowser, ServiceStateChange
import numpy as np
import iot_devices.device as devices
from icemedia import iceflow
from scullery import workers
import cv2
from NVRChannel.onvif import ONVIFCamera


logger = logging.Logger("plugins.nvr")


path = os.path.dirname(os.path.abspath(__file__))


def get_rtsp_from_onvif(c: ONVIFCamera):
    "Choose a URI to read from the discovered camera"
    c.create_devicemgmt_service()
    c.create_media_service()

    selection = None
    cw = 0
    for p in c.media.GetProfiles():
        # We want to find a profile that has H264/AAC
        if "VideoEncoderConfiguration" not in p:
            continue
        if "Encoding" not in p["VideoEncoderConfiguration"]:
            continue

        if not p["VideoEncoderConfiguration"]["Encoding"] == "H264":
            continue

        if "AudioEncoderConfiguration" in p:
            if not p["AudioEncoderConfiguration"]["Encoding"] == "AAC":
                continue

        # We want the best available quality so we are going to look for the widest.
        if "Resolution" in p["VideoEncoderConfiguration"]:
            if p["VideoEncoderConfiguration"]["Resolution"]["Width"] < cw:
                continue

            cw = p["VideoEncoderConfiguration"]["Resolution"]["Width"]
        selection = p

    if not selection:
        raise RuntimeError("Could not select profile from discovered camera")

    # Only do the net request after we know what we want to connect with.
    resp = c.media.GetStreamUri(
        {
            "StreamSetup": {
                "Stream": "RTP-Unicast",
                "Transport": {"Protocol": "RTSP"},
            },
            "ProfileToken": selection.token,
        }
    )

    return resp.Uri


def toImgOpenCV(imgPIL):  # Conver imgPIL to imgOpenCV
    i = np.array(imgPIL)  # After mapping from PIL to np : [R,G,B,A]
    # np Image Channel system: [B,G,R,A]
    red = i[:, :, 0].copy()
    i[:, :, 0] = i[:, :, 2].copy()
    i[:, :, 2] = red
    return i


def letterbox_image(image, size):
    """resize image with unchanged aspect ratio using padding"""
    iw, ih = image.shape[0:2][::-1]
    w, h = size
    scale = min(w / iw, h / ih)
    nw = int(iw * scale)
    nh = int(ih * scale)
    image = cv2.resize(image, (nw, nh), interpolation=cv2.INTER_CUBIC)
    new_image = np.zeros((size[1], size[0], 3), np.uint8)
    new_image.fill(0)
    dx = (w - nw) // 2
    dy = (h - nh) // 2
    new_image[dy : dy + nh, dx : dx + nw, :] = image
    return new_image


automated_record_uuid = "76241b9c-5b08-4828-9358-37c6a25dd823"


# very much not thread safe, doesn't matter, it's only for one UI page
httpservices = []
httplock = threading.Lock()


onvifCams = {}


def fixAddr(a):
    return a.split(".")[0] + ".local"


def on_service_state_change(zeroconf, service_type, name, state_change):
    with httplock:
        info = zeroconf.get_service_info(service_type, name)
        if not info:
            return
        if state_change is ServiceStateChange.Added:
            httpservices.append(
                (
                    tuple(
                        sorted([socket.inet_ntoa(i) for i in info.addresses])
                    ),
                    service_type,
                    name,
                    info.port,
                )
            )
            if len(httpservices) > 2048:
                httpservices.pop(0)

            try:
                if name.startswith("AMC"):
                    # No username/pw yet, we cannot actually fill this in.
                    onvifCams[fixAddr(name)] = None
            except Exception:
                pass
        elif state_change is ServiceStateChange.Removed:
            try:
                httpservices.remove(
                    (
                        tuple(
                            sorted(
                                [socket.inet_ntoa(i) for i in info.addresses]
                            )
                        ),
                        service_type,
                        name,
                        info.port,
                    )
                )

                if name.startswith("AMC"):
                    del onvifCams[fixAddr(name)]
            except Exception:
                logging.exception("???")


# Not common enough to waste CPU all the time on
# browser = ServiceBrowser(util.zeroconf, "_https._tcp.local.", handlers=[ on_service_state_change])
try:
    from kaithem.src.util import zeroconf as zcinstance
except Exception:
    import zeroconf

    zcinstance = zeroconf.Zeroconf()

browser2 = ServiceBrowser(
    zcinstance, "_http._tcp.local.", handlers=[on_service_state_change]
)


class Pipeline(iceflow.GstreamerPipeline):
    def __init__(self, *a, **k):
        super().__init__(*a, **k)
        self.dev: NVRChannel | None = None

    def on_motion_begin(self, *a, **k):
        self.mcb(True)

    def on_motion_end(self, *a, **k):
        self.mcb(False)

    def on_presence_value(self, v):
        self.presenceval(v)

    def on_video_analyze(self, *a, **k):
        self.acb(*a)

    def on_barcode(self, *a, **k):
        self.bcb(*a, **k)

    def getGstreamerSourceData(self, s, cfg, un, pw, doJackAudio=False):
        assert self.dev

        self.config = cfg
        self.h264source = self.mp3src = False
        self.syncFile = False

        # The source is an HLS stream
        if s.endswith(".m3u8") and s.startswith("http"):
            self.add_element("souphttpsrc", location=s)
            self.add_element("hlsdemux")
            self.add_element("tsdemux")
            self.add_element("parsebin")
            self.h264source = self.add_element("tee")

        elif s.startswith("file://"):
            if s.endswith(".jpg"):
                self.add_element(
                    "multifilesrc",
                    location=s.split("://")[-1],
                    loop=True,
                    caps="image/jpeg,framerate="
                    + str((self.config.get("fps", 1) or 1))
                    + "/1",
                    do_timestamp=True,
                )
                self.add_element("jpegdec")
                self.add_element("videoconvert")
                self.add_element("videorate")
                self.add_element("queue", max_size_time=10000000)
                self.add_element(
                    "x264enc",
                    tune="zerolatency",
                    rc_lookahead=0,
                    bitrate=int(self.dev.config["bitrate"]),
                    key_int_max=int((self.config.get("fps", "4") or "4")) * 2,
                )
                self.add_element(
                    "capsfilter", caps="video/x-h264, profile=main"
                )
                self.add_element("h264parse")
                self.add_element("queue")

                self.h264source = self.add_element("tee")

            else:
                if not os.path.exists(s[len("file://") :]):
                    raise RuntimeError("Bad file: " + s)
                self.add_element(
                    "multifilesrc", location=s[len("file://") :], loop=True
                )
                if s.endswith(".mkv"):
                    dm = self.add_element("matroskademux")
                else:
                    dm = self.add_element("qtdemux")
                self.add_element(
                    "h264parse", connect_when_available="video/x-h264"
                )
                # self.add_element('identity', sync=True)
                self.syncFile = True
                self.add_element("queue", max_size_time=10000000)

                self.h264source = self.add_element("tee")
                self.add_element(
                    "decodebin3",
                    connectToOutput=dm,
                    connect_when_available="audio",
                )
                self.add_element("audioconvert", connect_when_available="audio")

                self.add_element("audiorate")
                self.add_element("queue", max_size_time=10000000)
                self.add_element("voaacenc")
                self.add_element("aacparse")

                self.mp3src = self.add_element("queue", max_size_time=10000000)

        # Make a video test src just for this purpose
        elif not s:
            self.add_element("videotestsrc", is_live=True)
            self.add_element("videorate")
            self.add_element(
                "capsfilter",
                caps="video/x-raw,framerate="
                + str(self.config.get("fps", 4) or 4)
                + "/1",
            )
            self.add_element(
                "capsfilter",
                caps="video/x-raw, format=I420, width=320, height=240",
            )

            self.add_element("videoconvert")
            self.add_element(
                "x264enc", tune="zerolatency", byte_stream=True, rc_lookahead=0
            )
            self.add_element("h264parse")
            self.h264source = self.add_element("tee")

        # Make a video test src just for this purpose
        elif s == "test":
            self.add_element("videotestsrc", is_live=True)
            self.add_element(
                "capsfilter",
                caps="video/x-raw,framerate="
                + str(self.config.get("fps", 4) or 4)
                + "/1",
            )

            self.add_element(
                "capsfilter",
                caps="video/x-raw, format=I420, width=320, height=240",
            )
            self.add_element("videoconvert")
            self.add_element(
                "x264enc",
                tune="zerolatency",
                key_int_max=int((self.config.get("fps", 4) or 4)) * 2,
            )
            self.add_element("h264parse")
            self.h264source = self.add_element("tee")

        elif s in ("webcam", "webcam_pipewire", "webcam_audio"):
            self.add_element("v4l2src")
            self.add_element("videorate", drop_only=True)
            self.add_element(
                "capsfilter",
                caps="video/x-raw,framerate="
                + str(self.config.get("fps", 4) or 4)
                + "/1",
            )
            self.add_element("videoconvert")
            self.add_element("queue", max_size_time=10000000)
            self.add_element(
                "x264enc",
                tune="zerolatency",
                rc_lookahead=0,
                bitrate=int(self.dev.config["bitrate"]),
                key_int_max=int(self.config.get("fps", 4) or 4) * 2,
            )
            self.add_element("capsfilter", caps="video/x-h264, profile=main")
            self.add_element("h264parse", config_interval=1)
            self.h264source = self.add_element("tee")

            if s == "webcam_pipewire":
                self.add_element("pipewiresrc", connectToOutput=False)
            if s == "webcam_audio":
                self.add_element("autoaudiosrc", connectToOutput=False)
            else:
                self.add_element("audiotestsrc", wave=4)
            self.add_element("queue")
            self.add_element("audioconvert")

            self.add_element("voaacenc")
            self.add_element("aacparse")

            self.mp3src = self.add_element("queue", max_size_time=10000000)

        elif s.startswith("rtsp://") or self.dev.onvif:
            if self.dev.onvif:
                s = get_rtsp_from_onvif(self.dev.onvif)
                self.dev.metadata["discovered_rtsp_url"] = s

            rtsp = self.add_element(
                "rtspsrc",
                location=s,
                latency=100,
                async_handling=True,
                user_id=un or None,
                user_pw=pw or None,
            )
            self.add_element("rtph264depay", connect_when_available="video")

            self.add_element("h264parse", config_interval=1)
            self.add_element(
                "capsfilter", caps="video/x-h264,stream-format=byte-stream"
            )
            self.h264source = self.add_element("tee")

            self.add_element(
                "decodebin",
                connectToOutput=rtsp,
                connect_when_available="audio",
                async_handling=True,
            )

            rawaudiotee = None
            if doJackAudio:
                rawaudiotee = self.add_element(
                    "tee", connect_when_available="audio"
                )

            self.add_element("audioconvert")
            self.add_element("audiorate")
            self.add_element("voaacenc")
            self.add_element("aacparse")

            self.mp3src = self.add_element("queue", max_size_time=10000000)

            if doJackAudio:
                assert rawaudiotee
                self.add_element(
                    "queue",
                    max_size_time=100_000_000,
                    leaky=2,
                    connect_when_available="audio",
                    connectToOutput=rawaudiotee,
                )
                self.sink = self.add_element(
                    "jackaudiosink",
                    buffer_time=10,
                    latency_time=10,
                    sync=False,
                    provide_clock=False,
                    slave_method=0,
                    port_pattern="ghjkcsrc",
                    client_name=self.dev.name + "_out",
                    connect=0,
                    blocksize=512,
                )

        elif s.startswith("srt://"):
            rtsp = self.add_element(
                "srtsrc", mode=1, uri=s, passphrase=pw or ""
            )

            demux = self.add_element("tsdemux")
            self.add_element(
                "h264parse", config_interval=2, connect_when_available="video"
            )
            self.add_element(
                "capsfilter", caps="video/x-h264,stream-format=byte-stream"
            )
            self.add_element("queue", max_size_time=100_000_000, leaky=2)

            self.h264source = self.add_element("tee")

            self.add_element(
                "aacparse",
                connectToOutput=demux,
                connect_when_available="audio",
            )
            self.mp3src = self.add_element(
                "queue", max_size_time=100_000_000, leaky=2
            )

        elif s == "screen":
            self.add_element("ximagesrc")
            self.add_element(
                "capsfilter",
                caps="video/x-raw,framerate="
                + str(self.config.get("fps", 4) or 4)
                + "/1",
            )
            self.add_element("videoconvert")
            self.add_element("queue", max_size_time=10000000)
            self.add_element(
                "x264enc",
                tune="zerolatency",
                rc_lookahead=0,
                bitrate=int(self.dev.config["bitrate"]),
                key_int_max=int((self.config.get("fps", "4") or "4")) * 2,
            )
            self.add_element("capsfilter", caps="video/x-h264, profile=main")
            self.add_element("h264parse")
            self.add_element(
                "capsfilter", caps="video/x-h264,stream-format=byte-stream"
            )
            self.h264source = self.add_element("tee")

        return s


class NVRChannel(devices.Device):
    device_type = "NVRChannel"
    readme = os.path.join(os.path.dirname(__file__), "README.md")

    config_schema = {
        "type": "object",
        "properties": {
            "source": {
                "type": "string",
                "default": "",
                "description": "The source of the video stream",
                "propertyOrder": 1,
            },
            "username": {
                "type": "string",
                "default": "",
                "propertyOrder": 2,
            },
            "password": {
                "type": "string",
                "default": "",
                "secret": True,
                "propertyOrder": 3,
            },
            "loop_record_length": {"type": "number", "default": 5},
            "storage_dir": {"type": "string", "default": "~/NVR"},
            "fps": {"type": "number", "default": 4},
            "detect_barcodes": {"type": "boolean", "default": False},
            "motion_threshold": {"type": "number", "default": 0.08},
            "bitrate": {"type": "number", "default": 386},
            "retain_days": {"type": "number", "default": 90},
        },
    }

    def putTrashInBuffer(self):
        "Force a wake up of a thread sitting around waiting for the pipe"
        if os.path.exists(self.rawFeedPipe):
            import select

            try:
                f = os.open(self.rawFeedPipe, flags=os.O_NONBLOCK | os.O_APPEND)
                s = 0
                for i in range(188):
                    r, w, x = select.select([], [f], [], 0.2)
                    if w:
                        os.write(f, b"b" * 42)
                    else:
                        s += 1
                        if s > 15:
                            return

            except Exception:
                print(traceback.format_exc())

    def thread(self):
        # Has to be at top othherwise other threads wait and get same val.... and we have multiple...
        initialValue = self.runWidgetThread
        self.threadStarted = True
        self.threadExited = False

        b = b""
        while not os.path.exists(self.rawFeedPipe):
            time.sleep(1)

        f = open(self.rawFeedPipe, "rb")
        lp = time.time()

        while self.runWidgetThread and (self.runWidgetThread == initialValue):
            try:
                x = f.read(188 * 32)
                if x is None:
                    return
                b += x
            except OSError:
                time.sleep(0.2)
            except TypeError:
                time.sleep(1)
                try:
                    f = open(self.rawFeedPipe, "rb")
                except Exception:
                    print(traceback.format_exc())

            except Exception:
                time.sleep(0.5)
                print(traceback.format_exc())

            if self.runWidgetThread:
                if len(b) > (188 * 256) or (lp < (time.time() - 0.2) and b):
                    if self.runWidgetThread and (
                        self.runWidgetThread == initialValue
                    ):
                        lp = time.time()
                        self.push_bytes("raw_feed", b)
                        self.lastPushedWSData = time.time()
                    b = b""
        self.threadExited = True

    def checkThread(self):
        # Has to be at top othherwise other threads wait and get same val.... and we have multiple...
        initialValue = self.runCheckThread

        while self.runCheckThread and (self.runCheckThread == initialValue):
            try:
                self.check()
            except Exception:
                self.handle_exception()
            time.sleep(3)

    def on_before_close(self):
        self.closed = True
        try:
            if self.process:
                self.process.stop()
        except Exception:
            print(traceback.format_exc())
        self.runCheckThread = False
        self.runWidgetThread = False
        try:
            self.putTrashInBuffer()
        except Exception:
            print(traceback.format_exc())

        try:
            if os.path.exists(self.rawFeedPipe):
                os.remove(self.rawFeedPipe)
        except Exception:
            print(traceback.format_exc())

        s = 10
        while s:
            s -= 1
            if self.threadExited:
                break
            time.sleep(0.1)

        devices.Device.close(self)

        try:
            shutil.rmtree("/dev/shm/knvr_buffer/" + self.name)
        except Exception:
            pass

    def __del__(self):
        self.close()

    def onRawTSData(self, data):
        pass

    def getSnapshot(self):
        if hasattr(self, "snapshotter") and self.snapshotter:
            with open("/dev/shm/knvr_buffer/" + self.name + ".bmp", "w") as f:
                os.chmod("/dev/shm/knvr_buffer/" + self.name + ".bmp", 0o700)
            if self.datapoints["running"]:
                try:
                    # Use a temp file to make it an atomic operation
                    fn = "/dev/shm/knvr_buffer/" + self.name + ".bmp"
                    tmpfn = (
                        "/dev/shm/knvr_buffer/"
                        + self.name
                        + str(time.time())
                        + ".bmp"
                    )

                    x = self.snapshotter.pull_to_file(tmpfn)

                    st = time.monotonic()
                    while not os.path.exists(tmpfn):
                        time.sleep(0.01)
                        if time.monotonic() - st > 0.2:
                            break

                    shutil.move(tmpfn, fn)

                except Exception:
                    self.set_data_point("running", 0)
                    if self.process:
                        try:
                            self.process.stop()
                        except Exception:
                            print(traceback.format_exc())
                    raise

                if x:
                    with open(
                        "/dev/shm/knvr_buffer/" + self.name + ".bmp", "rb"
                    ) as f:
                        x = f.read()
                    os.remove("/dev/shm/knvr_buffer/" + self.name + ".bmp")

                return x

    def connect(self):
        if self.closed:
            return
        # Close the old thread
        self.runWidgetThread = time.time()

        if time.time() - self.lastStart < 15:
            return

        # When we reconnect we stop the recording and motion
        self.set_data_point("record", False, None, automated_record_uuid)
        self.set_data_point("raw_motion_value", 0)
        self.set_data_point("motion_detected", 0)
        self.activeSegmentDir = self.segmentDir = None

        self.lastStart = time.time()

        if self.process:
            try:
                self.process.stop()
            except Exception:
                print(traceback.format_exc())

        # Used to check that things are actually still working.
        # Set them to prevent a loop.
        self.lastSegment = time.time()
        self.lastPushedWSData = time.time()

        # Can't stop as soon as they push stop, still need to capture
        # the currently being recorded segment
        self.stoprecordingafternextsegment = 0

        if os.path.exists("/dev/shm/knvr_buffer/" + self.name):
            # Race condition retry
            try:
                shutil.rmtree("/dev/shm/knvr_buffer/" + self.name)
            except Exception:
                shutil.rmtree("/dev/shm/knvr_buffer/" + self.name)

        os.makedirs("/dev/shm/knvr_buffer/" + self.name)

        try:
            # Make it so nobody else can read the files
            os.chmod("/dev/shm/knvr_buffer/" + self.name, 0o700)
        except Exception:
            pass

        # Close the old thread
        self.runWidgetThread = time.time()
        self.putTrashInBuffer()
        s = 100
        while s:
            s -= 1
            if self.threadExited:
                break
            time.sleep(0.1)
        else:
            self.print("COULD NOT STOP OLD THREAD")

        self.process = Pipeline()
        self.process.dev = self

        j = False
        self.process.getGstreamerSourceData(
            self.config.get("source", ""),
            self.config,
            self.config.get("username", ""),
            self.config.get("password", ""),
            doJackAudio=j,
        )

        x = self.process.add_element(
            "queue",
            connectToOutput=self.process.h264source,
            max_size_time=10000000,
        )

        self.process.add_element(
            "mpegtsmux", connectToOutput=(x, self.process.mp3src)
        )

        self.process.add_element("tsparse", set_timestamps=True)

        self.mpegtssrc = self.process.add_element("tee")

        # Path to be created
        path = self.rawFeedPipe

        # Get rid of the old one, it could be clogged
        try:
            os.remove(path)
        except OSError:
            pass

        try:
            os.mkfifo(path)
        except OSError:
            print("Failed to create FIFO")

        os.chmod(path, 0o700)

        self.process.add_element("queue", max_size_time=10000000)
        self.process.add_element(
            "filesink", location=path, buffer_mode=2, sync=self.process.syncFile
        )

        # # Motion detection part of the graph

        # # This flag discards every unit that cannot be handled individually
        self.process.add_element(
            "identity",
            drop_buffer_flags=8192,
            connectToOutput=self.process.h264source,
        )
        self.process.add_element("queue", max_size_time=20000000, leaky=2)
        self.process.add_element("capsfilter", caps="video/x-h264")

        self.process.add_element("avdec_h264")
        # self.process.add_element("videorate",drop_only=True)
        # self.process.add_element("capsfilter", caps="video/x-raw,framerate=1/1")

        rawtee = self.process.add_element("tee")
        self.process.add_element("queue", max_size_buffers=1, leaky=2)

        self.snapshotter = self.process.add_pil_capture()

        self.process.add_element("videoanalyse", connectToOutput=rawtee)

        if self.config.get("barcodes", "").lower() in (
            "yes",
            "true",
            "detect",
            "enable",
            "on",
        ):
            self.process.add_element("zbar")

        # Not a real GST element. The iceflow backend hardcodes this motion/presense detection
        self.process.add_presence_detector((640, 480), regions="")

        self.process.mcb = self.motion
        self.process.bcb = self.barcode
        self.process.acb = self.analysis

        self.process.presenceval = self.presencevalue

        self.process.add_element(
            "hlssink",
            connectToOutput=self.mpegtssrc,
            message_forward=True,
            async_handling=True,
            max_files=0,
            location=os.path.join(
                "/dev/shm/knvr_buffer/", self.name, r"segment%08d.ts"
            ),
            playlist_root=os.path.join("/dev/shm/knvr_buffer/", self.name),
            playlist_location=os.path.join(
                "/dev/shm/knvr_buffer/", self.name, "playlist.m3u8"
            ),
            target_duration=5,
        )

        self.threadStarted = False

        self.datapusher = threading.Thread(
            target=self.thread, daemon=True, name="NVR " + self.name
        )
        self.datapusher.start()

        s = 25000
        while not self.threadStarted:
            time.sleep(0.001)
            s -= 1
        else:
            if not self.threadStarted:
                self.print("Thread not started within 25 seconds")

        self.process.start()
        # Used to check that things are actually still working.
        # Set them to prevent a loop.
        self.lastSegment = time.time()
        self.lastPushedWSData = time.time()

    def onRecordingChange(self, v, t, a):
        with self.recordlock:
            d = os.path.join(self.storageDir, self.name, "recordings")
            if os.path.exists(d):
                for i in os.listdir(d):
                    i2 = os.path.join(d, i)
                    try:
                        dt = datetime.fromisoformat(i)
                    except Exception:
                        continue

                    now = datetime.utcnow().replace(tzinfo=timezone.utc)

                    if dt < now:
                        dt = now - dt
                        # Sanity check
                        if dt.days > self.retainDays and dt.days < 10000:
                            shutil.rmtree(i2)

            if a == automated_record_uuid:
                self.canAutoStopRecord = True
            else:
                self.canAutoStopRecord = False

            if v:
                self.stoprecordingafternextsegment = 0
                if not self.segmentDir:
                    self.setsegmentDir()
            else:
                self.stoprecordingafternextsegment = 1

    def setsegmentDir(self, manual=False):
        with self.recordlock:
            # Manually triggered recordings should go in a different folder

            my_date = datetime.utcnow()
            date = (
                my_date.replace(
                    hour=0, minute=0, second=0, microsecond=0
                ).isoformat()
                + "+00:00"
            )
            t = my_date.isoformat() + "+00:00"

            d = os.path.join(self.storageDir, self.name, "recordings", date, t)
            os.makedirs(d)
            self.segmentDir = d

            with open(
                os.path.join(self.segmentDir, "playlist.m3u8"), "w"
            ) as plfile:
                plfile.write("#EXTM3U\r\n")
                plfile.write("#EXT-X-START:	TIME-OFFSET=0\r\n")
                plfile.write("#EXT-X-PLAYLIST-TYPE: VOD\r\n")
                plfile.write("#EXT-X-VERSION:3\r\n")
                plfile.write("#EXT-X-ALLOW-CACHE:NO\r\n")
                plfile.write("#EXT-X-TARGETDURATION:5\r\n")

        # Capture a tiny preview snapshot
        import PIL
        import PIL.Image
        import PIL.ImageOps

        def f():
            try:
                self.request_data_point("bmp_snapshot")

                st = time.monotonic()
                while not self.datapoints["bmp_snapshot"].get():
                    time.sleep(0.01)
                    if time.monotonic() - st > 5:
                        break

                x = PIL.Image.open(
                    io.BytesIO(self.datapoints["bmp_snapshot"].get()[0])
                )
                x.thumbnail((320, 240))
                x = PIL.ImageOps.autocontrast(x, cutoff=0.1)
                with open(
                    os.path.join(self.segmentDir, "thumbnail.jpg"), "wb"
                ) as f:
                    x.save(f, "jpeg")
            except Exception:
                print(traceback.format_exc())

        workers.do(f)

    def on_multi_file_sink(self, fn, *a, **k):
        with self.recordlock:
            self.moveSegments()
            d = os.path.join("/dev/shm/knvr_buffer/", self.name)
            ls = os.listdir(d)
            ls = list(sorted([i for i in ls if i.endswith(".ts")]))

            n = max(
                1,
                int(
                    (float(self.config.get("loop_record_length", 5)) + 2.5) / 5
                ),
            )

            s = 100
            while len(ls) > n:
                if s < 1:
                    break
                s -= 1
                os.remove(os.path.join(d, ls[0]))
                self.lastSegment = time.time()
                self.set_data_point("running", 1)

                ls = os.listdir(d)
                ls = list(sorted([i for i in ls if i.endswith(".ts")]))
                n = max(
                    1,
                    int(
                        (float(self.config.get("loop_record_length", 5)) + 2.5)
                        / 5
                    ),
                )

    def moveSegments(self):
        with self.recordlock:
            d = os.path.join("/dev/shm/knvr_buffer/", self.name)
            ls = os.listdir(d)
            ls = list(sorted([i for i in ls if i.endswith(".ts")]))

            if self.activeSegmentDir or self.segmentDir:
                # Ignore latest, that could still be recording
                for i in ls[:-1]:
                    self.lastSegment = time.time()
                    self.set_data_point("running", 1)

                    # Someone could delete a segment dir while it is being written to.
                    # Prevent that from locking everything up.
                    if os.path.exists(self.activeSegmentDir or self.segmentDir):
                        # Find the duration of the segment from the hlssink playlist file
                        with open(os.path.join(d, "playlist.m3u8")) as f:
                            x = f.read()
                        if i not in x:
                            return

                        x = x.split(i)[0]
                        x = float(re.findall(r"EXTINF:\s*([\d\.]*)", x)[-1])

                        # Assume the start time is mod time minus length
                        my_date = datetime.utcfromtimestamp(
                            os.stat(os.path.join(d, i)).st_mtime - x
                        )
                        t = my_date.isoformat() + "+00:00"

                        shutil.move(
                            os.path.join(d, i),
                            self.activeSegmentDir or self.segmentDir,
                        )
                        with open(
                            os.path.join(
                                self.activeSegmentDir or self.segmentDir,
                                "playlist.m3u8",
                            ),
                            "a+",
                        ) as f:
                            f.write("\r\n")
                            f.write("#EXTINF:" + str(x) + ",\r\n")
                            f.write("#EXT-X-PROGRAM-DATE-TIME:" + t + "\r\n")
                            f.write(i + "\r\n")

                        self.directorySegments += 1

                    if self.stoprecordingafternextsegment:
                        x = self.segmentDir
                        self.segmentDir = None
                        self.activeSegmentDir = None

                        with open(os.path.join(x, "playlist.m3u8"), "a+") as f:
                            f.write("\r\n#EXT-X-ENDLIST\r\n")

                        break
                    else:
                        # Don't make single directories with more than an hour of video.
                        if self.directorySegments > (3600 / 5):
                            self.setsegmentDir()

                # Now we can transition to the new one!
                self.activeSegmentDir = self.segmentDir
                self.directorySegments = 0

    def check(self):
        "Pretty mush all periodic tasks go here"

        # Make sure we are actually getting video frames. Otherwise we reconnect.
        if not self.lastSegment > (time.time() - 15):
            self.set_data_point("running", False)
            if self.datapoints["switch"].get()[0]:
                self.connect()
                return

        if not self.lastPushedWSData > (time.time() - 15):
            self.set_data_point("running", False)
            if self.datapoints["switch"].get()[0]:
                self.connect()
                return

        d = os.path.join("/dev/shm/knvr_buffer/", self.name)
        ls = os.listdir(d)

        # If there is a ton of files run the poller anyway, if could have stalled because it ran out of memory
        # because something caused things to block long enough for it all to fill up.
        if (not ls == self.lastshm) or len(ls) > 16:
            self.on_multi_file_sink("")
        self.lastshm = ls

    def commandState(self, v, t, a):
        with self.streamLock:
            if not v:
                if self.process:
                    self.process.stop()
                self.runWidgetThread = False
                try:
                    self.putTrashInBuffer()
                except Exception:
                    print(traceback.format_exc())

                s = 10
                while s:
                    s -= 1
                    if self.threadExited:
                        break
                    time.sleep(0.1)
            else:
                self.check()

    def motion(self, v):
        self.doMotionRecordControl(v)
        self.set_data_point("motion_detected", v)

    def doMotionRecordControl(self, v, forceMotionOnly=False):
        "forceMotionOnly records even if there is no object detection, for when the CPU can't keep up with how many motion requests there are"
        if self.config.get("motion_recording", "no").lower() in (
            "true",
            "yes",
            "on",
            "enable",
            "enabled",
        ):
            if v:
                self.lastRecordTrigger = time.time()
                if (self.datapoints["auto_record"].get()[0] or 1) > 0.5:
                    self.set_data_point(
                        "record", True, None, automated_record_uuid
                    )

            elif not v and self.canAutoStopRecord:
                if self.lastRecordTrigger < (time.time() - 12):
                    if self.lastRecordTrigger < (time.time() - 60):
                        self.set_data_point(
                            "record", False, None, automated_record_uuid
                        )

        self.lastDidMotionRecordControl = time.time()

    def presencevalue(self, v):
        "Takes a raw presence value. Unfortunately it seems we need to do our own motion detection."

        if isinstance(v, dict):
            for i in v:
                # Empty string is entire image
                if i and i in self.subdevices:
                    self.subdevices[i].onMotionValue(v[i])

            # Get the overall motion number
            v = v[""]

        self.set_data_point("raw_motion_value", v)

        self.motion(v > float(self.config.get("motion_threshold", 0.08)))
        self.doMotionRecordControl(self.datapoints["motion_detected"], True)

    def analysis(self, v):
        self.set_data_point("luma_average", v["luma_average"])
        self.set_data_point("luma_variance", v["luma_variance"])

    def barcode(self, t, d, q):
        self.set_data_point(
            "barcode",
            {
                "barcode_type": t,
                "barcode_data": d,
                "wallclock": time.time(),
                "quality": q,
            },
        )

    def __init__(self, data, **kw):
        devices.Device.__init__(self, data, **kw)
        try:
            self.runWidgetThread = True
            self.runCheckThread = time.time()

            self.lastInferenceTime = 0.0
            self.threadExited = True
            self.closed = False

            # Support ONVIF URLs
            self.onvif = None
            if self.config["username"] and self.config["password"]:
                try:
                    if (
                        self.config["source"]
                        and not self.config["source"].startswith("rtsp://")
                        and not self.config["source"].startswith("file://")
                        and self.config["source"] not in ("webcam", "screen")
                    ):
                        if not self.config["source"].startswith("srt://"):
                            p = self.config["source"].split("://")[-1]
                            if ":" in p:
                                port = int(p.split(":")[1])
                                p = p.split(":")[0]
                            else:
                                port = 80

                            self.onvif = ONVIFCamera(
                                p,
                                port,
                                self.config["username"],
                                self.config["password"],
                            )
                except Exception:
                    self.print(traceback.format_exc())

            self.process = None

            self.lastInferenceTime = 1

            self.lastDidObjectRecognition = 0

            # So we can tell if there is new object recogintion data since we last checked.
            self.lastDidMotionRecordControl = 0

            # We don't want to stop till a few seconds after an event that would cause motion
            self.lastRecordTrigger = 0

            # We also DO want to stop if we are in object record mode and have not seen the object in a long time

            self.lastObjectDetectionHit = 0

            # If this is true, record when there is motion
            self.set_config_default("motion_recording", "no")

            self.storageDir = os.path.expanduser(
                self.config["storage_dir"] or "~/NVR"
            )

            self.segmentDir = None

            # When changing segment dir, we can't do it instantly, we instead wait to be done with the current file.
            self.activeSegmentDir = None

            # How many segments in this dir. Must track so we can switch to a new directory if we need to.
            self.directorySegments = 0

            self.lastshm = None

            self.canAutoStopRecord = False

            if not os.path.exists(self.storageDir):
                os.makedirs(self.storageDir)
                # Secure it!
                os.chmod(self.storageDir, 0o700)

            self.tsQueue = b""

            self.recordlock = threading.RLock()

            self.rawFeedPipe = (
                "/dev/shm/knvr_buffer/"
                + self.name
                + "/"
                + str(time.time())
                + ".raw_feed.tspipe"
            )

            self.bytestream_data_point(
                "raw_feed", subtype="mpegts", writable=False
            )

            # Give this a little bit of caching
            self.bytestream_data_point(
                "bmp_snapshot",
                subtype="bmp",
                writable=False,
                interval=0.3,
                on_request=self.getSnapshot,
            )

            self.numeric_data_point(
                "switch",
                min=0,
                max=1,
                subtype="bool",
                default=1,
                handler=self.commandState,
                dashboard=False,
            )

            self.numeric_data_point(
                "auto_record",
                min=0,
                max=1,
                subtype="bool",
                default=1,
                handler=self.onRecordingChange,
                description="Set to 0 to disable automatic new recordings.",
                dashboard=False,
            )

            self.numeric_data_point(
                "record",
                min=0,
                max=1,
                subtype="bool",
                default=0,
                handler=self.onRecordingChange,
            )

            self.numeric_data_point(
                "running", min=0, max=1, subtype="bool", writable=False
            )

            self.numeric_data_point(
                "motion_detected", min=0, max=1, subtype="bool", writable=False
            )

            self.numeric_data_point(
                "raw_motion_value",
                min=0,
                max=10,
                writable=False,
                dashboard=False,
            )

            self.numeric_data_point(
                "luma_average", min=0, max=1, writable=False
            )

            self.numeric_data_point(
                "luma_variance", min=0, max=1, writable=False, dashboard=False
            )

            self.set_alarm(
                "Camera dark",
                "luma_average",
                "value < 0.095",
                trip_delay=10,
                auto_ack=True,
            )
            self.set_alarm(
                "Camera low varience",
                "luma_variance",
                "value < 0.004",
                release_condition="value > 0.008",
                trip_delay=60,
                auto_ack=True,
            )
            self.set_alarm(
                "Long_recording",
                "record",
                "value > 0.5",
                trip_delay=800,
                auto_ack=True,
                priority="debug",
            )

            self.set_alarm(
                "Not Running",
                "running",
                "value < 0.5",
                trip_delay=90,
                auto_ack=False,
                priority="warning",
            )

            self.retainDays = int(self.config["retain_days"])

            if self.config["detect_barcodes"]:
                self.object_data_point("barcode", writable=False)

            self.streamLock = threading.RLock()
            self.lastStart = 0

            try:
                self.connect()
            except Exception:
                self.handle_exception()
                self.set_data_point("running", 0)

            self.set_data_point("switch", 1)

            # Used to check that things are actually still working.
            self.lastSegment = time.time()
            self.lastPushedWSData = time.time()

            self.check()
            self.checkthreadobj = threading.Thread(
                target=self.checkThread,
                daemon=True,
                name="NVR checker" + self.name,
            )
            self.checkthreadobj.start()

        except Exception:
            self.handle_exception()

    @classmethod
    def discover_devices(
        cls, config={}, current_device=None, intent=None, **kw
    ):
        # Discover based on the ONVIF cameras.  Let the user fill in username/password.
        retval = {}
        for i in onvifCams:
            config2 = config.copy()

            config2.update({"type": cls.device_type, "source": i})

            config2["username"] = "admin"
            config2["password"] = ""
            retval[i] = config2

        for i in os.listdir("/dev/"):
            if i.startswith("video"):
                config2 = config.copy()

                config2.update({"type": cls.device_type, "source": "/dev/" + i})

                config2["username"] = ""
                config2["password"] = ""
                retval["Webcam " + i] = config2

        config2 = config.copy()
        config2.update({"type": cls.device_type, "source": "screen"})

        config2["username"] = ""
        config2["password"] = ""
        config2["fps"] = "4"
        retval["Screen Recording"] = config2

        return retval
