Exemplo n.º 1
0
    def __init__(self, mv_channel, channels, **kwargs):
        self.mv_channel = mv_channel
        self.caspar_host = kwargs.get("caspar_host", "localhost")
        self.caspar_port = kwargs.get("caspar_port", 5250)
        self.osc_host = kwargs.get("osc_host", "localhost")
        self.osc_port = kwargs.get("osc_port", 5253)
        self.osd_host = kwargs.get("osd_host", "")
        self.osd_port = kwargs.get("osd_port", 8082)
        self.osd_layer = kwargs.get("osd_layer", 100)
        self.osd_template = kwargs.get("osd_template", "mvboot")
        self.ws_host = kwargs.get("ws_host", "localhost")
        self.ws_port = kwargs.get("ws_port", 9001)

        assert type(channels) == list
        self.channels = []
        for i, channel in enumerate(channels):
            if not isinstance(channel, MultiViewerChannel):
                logging.warning(
                    "Unexpected type of channel {}. Skipping.".format(i))
            else:
                self.channels.append(channel)

        if len(channels) <= 4:
            self.grid_size = 2
        elif len(channels) <= 9:
            self.grid_size = 3
        else:
            self.grid_size = 4
        self.channel_scale = 1.0 / self.grid_size

        # CasparCG client
        self.caspar = CasparCG(self.caspar_host, self.caspar_port)

        # HTML OSD Layer server
        self.osd = OSDHTTPServer(self, self.osd_host, self.osd_port)
        thread.start_new_thread(self.osd.serve_forever, ())

        # OSC server (caspar -> service)
        self.osc = OSC(self)

        # Websockets server (service -> html)
        self.ws = WebSockets(self)
Exemplo n.º 2
0
 def connect(self):
     if not hasattr(self, "cmdc"):
         self.cmdc = CasparCG(self.host, self.port)
         self.infc = CasparCG(self.host, self.port)
     return self.cmdc.connect() and self.infc.connect()
Exemplo n.º 3
0
class CasparController(object):
    def __init__(self, parent):
        self.parent = parent

        self.current_item = False
        self.current_fname = False
        self.cued_item = False
        self.cued_fname = False
        self.cueing = False
        self.force_cue = False

        self.paused = False
        self.stopped = False
        self.stalled = False

        self.fpos = self.fdur = 0
        self.cued_in = self.cued_out = self.current_in = self.current_out = 0

        self.bad_requests = 0
        self.request_time = self.recovery_time = time.time()

        if not self.connect():
            logging.error("Unable to connect CasparCG Server. Shutting down.")
            self.parent.shutdown()
            return

        Parser = get_info_parser(self.infc)
        self.parser = Parser(self.infc, self.parent.caspar_channel)

        thread.start_new_thread(self.work, ())

    @property
    def id_channel(self):
        return self.parent.id_channel

    @property
    def host(self):
        return self.parent.caspar_host

    @property
    def port(self):
        return self.parent.caspar_port

    def connect(self):
        if not hasattr(self, "cmdc"):
            self.cmdc = CasparCG(self.host, self.port)
            self.infc = CasparCG(self.host, self.port)
        return self.cmdc.connect() and self.infc.connect()

    def query(self, *args, **kwargs):
        return self.cmdc.query(*args, **kwargs)

    @property
    def fps(self):
        return self.parent.fps

    @property
    def position(self):
        return int(self.fpos - self.current_in)

    @property
    def duration(self):
        if self.parent.current_live:
            return 0
        dur = self.fdur
        if self.current_out > 0:
            dur -= dur - self.current_out
        if self.current_in > 0:
            dur -= self.current_in
        return dur

    def work(self):
        while True:
            try:
                self.main()
                time.sleep(.01)
            except Exception:
                log_traceback()
            time.sleep(.3)

    def main(self):
        info = self.parser.get_info(self.parent.caspar_feed_layer)
        if not info:
            logging.warning("Channel {} update stat failed".format(
                self.id_channel))
            self.bad_requests += 1
            if self.bad_requests > 10:
                logging.error("Connection lost. Reconnecting...")
                if self.connect():
                    logging.goodnews("Connection estabilished")
                else:
                    logging.error("Connection call failed")
                    time.sleep(2)
            time.sleep(.3)
            return
        else:
            self.request_time = time.time()
        self.bad_requests = 0

        current_fname = info["current"]
        cued_fname = info["cued"]

        #
        #
        # Auto recovery
        #
        #        if not current_fname and time.time() - self.recovery_time > 20:
        #            self.parent.channel_recover()
        #            return
        #        self.recovery_time = time.time()

        if cued_fname and (not self.paused) and (
                info["pos"] == self.fpos) and (
                    not self.stopped
                ) and not self.parent.current_live and self.cued_item and (
                    not self.cued_item["run_mode"]):
            if self.stalled and self.stalled < time.time() - 5:
                logging.warning("Stalled for a long time")
                logging.warning("Taking stalled clip (pos: {})".format(
                    self.fpos))
                self.take()
            elif not self.stalled:
                logging.debug("Playback is stalled")
                self.stalled = time.time()
        elif self.stalled:
            logging.debug("No longer stalled")
            self.stalled = False

        self.fpos = info["pos"]
        self.fdur = info["dur"]

        #
        # Playlist advancing
        #

        advanced = False

        if self.cueing and self.cueing == current_fname and not cued_fname and not self.parent.cued_live:
            logging.warning("Using short clip workaround")
            self.current_item = self.cued_item
            self.current_fname = current_fname
            self.current_in = self.cued_in
            self.current_out = self.cued_out
            self.cued_in = self.cued_out = 0
            advanced = True
            self.cued_item = False
            self.cueing = False
            self.cued_fname = False

        elif (not cued_fname) and (
                current_fname) and not self.parent.cued_live:
            if current_fname == self.cued_fname:
                self.current_item = self.cued_item
                self.current_fname = self.cued_fname
                self.current_in = self.cued_in
                self.current_out = self.cued_out
                self.cued_in = self.cued_out = 0
                advanced = True
            self.cued_item = False

        elif (not current_fname) and (
                not cued_fname) and self.parent.cued_live:
            self.current_item = self.cued_item
            self.current_fname = "LIVE"
            self.current_in = 0
            self.current_out = 0
            self.cued_in = self.cued_out = 0
            advanced = True
            self.cued_item = False
            self.parent.on_live_enter()

        if advanced:
            try:
                self.parent.on_change()
            except Exception:
                log_traceback("Playout on_change failed")

        if self.current_item and not self.cued_item and not self.cueing:
            self.parent.cue_next()

        elif self.force_cue:
            logging.info("Forcing cue next")
            self.parent.cue_next()
            self.force_cue = False

        if self.cueing:
            if cued_fname == self.cued_fname:
                logging.debug("Cued", self.cueing)
                self.cueing = False
            else:
                logging.debug("Cueing", self.cueing)

        if self.cued_item and cued_fname and cued_fname != self.cued_fname and not self.cueing:
            logging.warning("Cue mismatch: IS: {} SHOULDBE: {}".format(
                cued_fname, self.cued_fname))
            self.cued_item = False

        try:
            self.parent.on_progress()
        except Exception:
            log_traceback("Playout on_main failed")
        self.current_fname = current_fname
        self.cued_fname = cued_fname

    def cue(self, fname, item, **kwargs):
        auto = kwargs.get("auto", True)
        layer = kwargs.get("layer", self.parent.caspar_feed_layer)
        play = kwargs.get("play", False)
        loop = kwargs.get("loop", False)
        mark_in = item.mark_in()
        mark_out = item.mark_out()

        marks = ""
        if loop:
            marks += " LOOP"
        if mark_in:
            marks += " SEEK {}".format(int(mark_in * self.parser.seek_fps))
        if mark_out and mark_out < item["duration"] and mark_out > mark_in:
            marks += " LENGTH {}".format(
                int((mark_out - mark_in) * self.parser.seek_fps))

        if play:
            q = "PLAY {}-{} {}{}".format(self.parent.caspar_channel, layer,
                                         fname, marks)
        else:
            q = "LOADBG {}-{} {} {} {}".format(self.parent.caspar_channel,
                                               layer, fname,
                                               ["", "AUTO"][auto], marks)

        result = self.query(q)

        if result.is_error:
            message = "Unable to cue \"{}\" {} - args: {}".format(
                fname, result.data, str(kwargs))
            self.cued_item = Item()
            self.cued_fname = False
            self.cueing = False
        else:
            self.cued_item = item
            self.cued_fname = fname
            self.cued_in = mark_in * self.fps
            self.cued_out = mark_out * self.fps
            self.cueing = fname
            message = "Cued item {} ({})".format(self.cued_item, fname)

        return NebulaResponse(result.response, message)

    def clear(self, **kwargs):
        layer = layer or self.parent.caspar_feed_layer
        result = self.query("CLEAR {}-{}".format(self.channel, layer))
        return NebulaResponse(result.response, result.data)

    def take(self, **kwargs):
        layer = kwargs.get("layer", self.parent.caspar_feed_layer)
        if not self.cued_item or self.cueing:
            return NebulaResponse(400, "Unable to take. No item is cued.")
        self.paused = False
        result = self.query("PLAY {}-{}".format(self.parent.caspar_channel,
                                                layer))
        if result.is_success:
            if self.parent.current_live:
                self.parent.on_live_leave()
            message = "Take OK"
            self.stalled = False
            self.paused = False
            self.stopped = False
        else:
            message = "Take command failed: " + result.data
        return NebulaResponse(result.response, message)

    def retake(self, **kwargs):
        layer = kwargs.get("layer", self.parent.caspar_feed_layer)
        if self.parent.current_live:
            return NebulaResponse(409, "Unable to retake live item")
        seekparam = str(int(self.current_item.mark_in() * self.fps))
        if self.current_item.mark_out():
            seekparam += " LENGTH {}".format(
                int((self.current_item.mark_out() -
                     self.current_item.mark_in()) * self.parser.seek_fps))
        q = "PLAY {}-{} {} SEEK {}".format(self.parent.caspar_channel, layer,
                                           self.current_fname, seekparam)
        self.paused = False
        result = self.query(q)
        if result.is_success:
            message = "Retake OK"
            self.stalled = False
            self.paused = False
            self.stopped = False
            self.parent.cue_next()
        else:
            message = "Take command failed: " + result.data
        return NebulaResponse(result.response, message)

    def freeze(self, **kwargs):
        layer = kwargs.get("layer", self.parent.caspar_feed_layer)
        if self.parent.current_live:
            return NebulaResponse(409, "Unable to freeze live item")
        if not self.paused:
            q = "PAUSE {}-{}".format(self.parent.caspar_channel, layer)
            message = "Playback paused"
            new_val = True
        else:
            if self.parser.protocol >= 2.07:
                q = "RESUME {}-{}".format(self.parent.caspar_channel, layer)
            else:
                length = "LENGTH {}".format(
                    int((self.current_out or self.fdur) - self.fpos))
                q = "PLAY {}-{} {} SEEK {} {}".format(
                    self.parent.caspar_channel, layer, self.current_fname,
                    self.fpos, length)
            message = "Playback resumed"
            new_val = False

        result = self.query(q)
        if result.is_success:
            self.paused = new_val
            if self.parser.protocol < 2.07 and not new_val:
                self.force_cue = True
        else:
            message = result.data
        return NebulaResponse(result.response, message)

    def abort(self, **kwargs):
        layer = kwargs.get("layer", self.parent.caspar_feed_layer)
        if not self.cued_item:
            return NebulaResponse(400, "Unable to abort. No item is cued.")
        q = "LOAD {}-{} {}".format(self.parent.caspar_channel, layer,
                                   self.cued_fname)
        if self.cued_item.mark_in():
            q += " SEEK {}".format(
                int(self.cued_item.mark_in() * self.parser.seek_fps))
        if self.cued_item.mark_out():
            q += " LENGTH {}".format(
                int((self.cued_item.mark_out() - self.cued_item.mark_in()) *
                    self.parser.seek_fps))
        result = self.query(q)
        if result.is_success:
            self.paused = True
        return NebulaResponse(result.response, result.data)
Exemplo n.º 4
0
class MultiViewer(object):
    def __init__(self, mv_channel, channels, **kwargs):
        self.mv_channel = mv_channel
        self.caspar_host = kwargs.get("caspar_host", "localhost")
        self.caspar_port = kwargs.get("caspar_port", 5250)
        self.osc_host = kwargs.get("osc_host", "localhost")
        self.osc_port = kwargs.get("osc_port", 5253)
        self.osd_host = kwargs.get("osd_host", "")
        self.osd_port = kwargs.get("osd_port", 8082)
        self.osd_layer = kwargs.get("osd_layer", 100)
        self.osd_template = kwargs.get("osd_template", "mvboot")
        self.ws_host = kwargs.get("ws_host", "localhost")
        self.ws_port = kwargs.get("ws_port", 9001)

        assert type(channels) == list
        self.channels = []
        for i, channel in enumerate(channels):
            if not isinstance(channel, MultiViewerChannel):
                logging.warning(
                    "Unexpected type of channel {}. Skipping.".format(i))
            else:
                self.channels.append(channel)

        if len(channels) <= 4:
            self.grid_size = 2
        elif len(channels) <= 9:
            self.grid_size = 3
        else:
            self.grid_size = 4
        self.channel_scale = 1.0 / self.grid_size

        # CasparCG client
        self.caspar = CasparCG(self.caspar_host, self.caspar_port)

        # HTML OSD Layer server
        self.osd = OSDHTTPServer(self, self.osd_host, self.osd_port)
        thread.start_new_thread(self.osd.serve_forever, ())

        # OSC server (caspar -> service)
        self.osc = OSC(self)

        # Websockets server (service -> html)
        self.ws = WebSockets(self)

    def set_vu(self, id_channel, audio_channel, value):
        id = "#channel-{}-vu-{}".format(id_channel, audio_channel)
        self.ws.send(json.dumps({"vuid": id, "value": value}))

    def query(self, query):
        return self.caspar.query(query)

    def reset(self):
        self.query("CLEAR {}".format(self.mv_channel))
        self.query("CHANNEL_GRID")
        self.query("MIXER {} MASTERVOLUME 0".format(self.mv_channels))
        #        self.query("REMOVE 10 SCREEN")
        self.query("PLAY {}-{} {}".format(self.mv_channel, self.osd_layer,
                                          self.osd_template))

        for i, channel in enumerate(self.channels):
            self.query("CLEAR {}".format(channel.caspar_id))
            source = channel.source
            if source:
                self.query("PLAY {}-1 {}".format(channel.caspar_id, source))
Exemplo n.º 5
0
 def connect(self):
     logging.info("CasparCG: connecting to server {}:{}".format(
         self.address, self.port))
     self.caspar = CasparCG(self.address, self.port)
     if self.caspar.connect():
         logging.goodnews("CasparCG: connected")
Exemplo n.º 6
0
class CasparMetricsProvider(object):
    def __init__(self, settings):
        self.stage = {}
        self.fps = {}
        self.profiler = {}
        self.peak_volume = {}

        self.address = settings["caspar_host"]
        self.port = settings["amcp_port"]
        self.osc_address = "0.0.0.0"
        self.osc_port = settings["osc_port"]
        self.last_osc_ts = 0

        if not self.address:
            return

        self.connect()
        response = self.query("VERSION")
        protocols = {
            "2.3": 2.2,
            "2.2": 2.2,
            "2.1": 2.1,
            "2.0.7": 2.07,
            "2.0.6": 2.06
        }
        for p in protocols:
            if response.data.startswith(p):
                logging.info("CasparCG: using parsed protocol {}".format(
                    protocols[p]))
                self.protocol = protocols[p]
                break
        else:
            self.protocol = 2.2
            logging.info("CasparCG: using default protocol 2.06")

        i = 1
        mode_to_fps = {
            "PAL": (25, 1),
            "NTSC": (30000, 1001),
            "1080i5000": (25, 1),
            "1080p5000": (50, 1),
            "1080i5994": (30000, 1001),
            "1080p5994": (50000, 1001),
        }

        while True:
            response = self.query("INFO {}".format(i))
            if not response:
                break
            x = xml(response.data)
            video_mode = x.find("video-mode").text
            fps = mode_to_fps[video_mode]
            logging.info("CasparCG: parsed channel {} FPS: {}".format(i, fps))
            self.fps[i] = fps
            i += 1

        self.start()
        logging.debug("CasparCG: initialization completed")

    def start(self):
        _thread.start_new_thread(self.heartbeat, ())
        _thread.start_new_thread(self.main, ())

    def connect(self):
        logging.info("CasparCG: connecting to server {}:{}".format(
            self.address, self.port))
        self.caspar = CasparCG(self.address, self.port)
        if self.caspar.connect():
            logging.goodnews("CasparCG: connected")

    def query(self, *args, **kwargs):
        if not self.caspar:
            return CasparResponse(500, "Not connected")
        return self.caspar.query(*args, **kwargs)

    def heartbeat(self):
        while True:
            try:
                response = self.query("VERSION", verbose=False)
                if not response:
                    self.connect()
                time.sleep(5)
            except Exception:
                log_traceback()

    def main(self):
        self.dispatcher = dispatcher.Dispatcher()
        self.osc = osc_server.BlockingOSCUDPServer(
            (self.osc_address, self.osc_port), self.dispatcher)

        self.dispatcher.map("/channel/*/stage/layer/*/", self.parse_stage)
        self.dispatcher.map("/channel/*/framerate", self.parse_framerate)
        self.dispatcher.map("/channel/*/mixer/audio/*", self.parse_volume)
        self.dispatcher.map("/channel/*/output/consume_time",
                            self.parse_consume_time)

        self.dispatcher.set_default_handler(self.parse_null)
        self.osc.serve_forever()

    def parse_null(self, *args):
        pass

    def parse_framerate(self, *args):
        address = args[0].split("/")
        id_channel = int(address[2])
        data = args[1:]
        if self.fps.get(id_channel) != data:
            logging.info("CasparCG: channel {} FPS changed to".format(
                id_channel, data))
            self.fps[id_channel] = data

    def parse_volume(self, *args):
        address = args[0].split("/")
        id_channel = int(address[2])
        if len(address) == 7 and address[6] == "pFS":
            self.peak_volume[id_channel] = max(
                args[1], self.peak_volume.get(id_channel, 0))

    def parse_consume_time(self, *args):
        self.last_osc_ts = time.time()
        address = args[0].split("/")
        id_channel = int(address[2])
        data = args[1]

        if data > 1 / self.get_fps(id_channel):
            logging.warning(
                "CasparCG: dropped frame on channel {}".format(id_channel))

    def parse_stage(self, *args):
        address = args[0].split("/")
        data = args[1:]
        id_channel = int(address[2])
        layer = int(address[5])
        key = "/".join(address[6:])
        if not id_channel in self.stage:
            self.stage[id_channel] = {}

        if not layer in self.stage[id_channel]:
            self.stage[id_channel][layer] = {}

        self.stage[id_channel][layer][key] = data

        if key == "profiler/time":
            treal, texp = data
            if treal > texp:
                if not id_channel in self.profiler:
                    self.profiler[id_channel] = {}
                if not layer in self.profiler[id_channel]:
                    self.profiler[id_channel][layer] = 0

                logging.warning(
                    "CasparCG: drop frame detected on channel {} layer {}".
                    format(id_channel, layer))
                self.profiler[id_channel][layer] += 1

    def get_peak_volume(self, id_channel):
        result = self.peak_volume.get(id_channel, 0)
        self.peak_volume[id_channel] = 0
        return result

    def get_fps(self, id_channel):
        fps_n, fps_d = self.fps.get(id_channel, (25, 1))
        return fractions.Fraction(fps_d, fps_n)

    def get_layer(self, id_channel=1, id_layer=10):
        try:
            lsrc = self.stage[id_channel][id_layer]
        except KeyError:
            return default_layer_info

        current = lsrc.get("foreground/file/name")
        current = os.path.splitext(current[0])[0] if current else False

        cued = lsrc.get("background/file/name")
        cued = os.path.splitext(cued[0])[0] if cued else False

        paused = lsrc.get("foreground/paused")
        paused = paused[0] if paused else False

        fg_producer = lsrc.get("foreground/producer")
        if fg_producer and fg_producer[0] == "decklink":
            is_live = True
        else:
            is_live = False

        bkg_producer = lsrc.get("background/producer")
        bkg_producer = bkg_producer[0] if bkg_producer else "empty"
        if bkg_producer == "empty":
            if "background/file/name" in lsrc:
                del (lsrc["background/file/name"])
            cued = False

        poss, durs = lsrc.get("foreground/file/time", (0, 0))

        if is_live:
            pos = dur = 0
        else:
            fps = self.get_fps(id_channel)
            pos = int(poss * fps)
            dur = int(durs * fps)

        return {
            "current": current,
            "cued": cued,
            "paused": paused,
            "pos": pos,
            "dur": dur,
            "live": is_live
        }
Exemplo n.º 7
0
#!/usr/bin/env python3

import time

from vial import Vial

from nxtools import logging
from nxtools.caspar import CasparCG

caspar = CasparCG("127.0.0.1")

while not caspar.query("VERSION"):
    time.sleep(3)


class App(Vial):
    def handle(self, request):
        if request.method == "POST" and request.path == "/amcp":
            r = caspar.query(request.body.text)

            return self.response.text(
                r.data,
                status=r.response,
                headers={"Access-Control-Allow-Origin": "*"})

        return self.response.text(f"Bad request", status=400)


app = App()

if __name__ == "__main__":
Exemplo n.º 8
0
 def connect(self):
     """Connect to a running CasparCG instance using AMCP protocol"""
     self.cmdc = CasparCG(self.caspar_host, self.caspar_port)
     return self.cmdc.connect()
Exemplo n.º 9
0
class CasparController(object):
    time_unit = "s"

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

        self.caspar_host = parent.channel_config.get("caspar_host",
                                                     "localhost")
        self.caspar_port = int(parent.channel_config.get("caspar_port", 5250))
        self.caspar_osc_port = int(
            parent.channel_config.get("caspar_osc_port", 5253))
        self.caspar_channel = int(
            parent.channel_config.get("caspar_channel", 1))
        self.caspar_feed_layer = int(
            parent.channel_config.get("caspar_feed_layer", 10))

        self.should_run = True

        self.current_item = Item()
        self.current_fname = False
        self.cued_item = False
        self.cued_fname = False
        self.cueing = False
        self.cueing_time = 0
        self.cueing_item = False
        self.stalled = False

        # To be updated based on CCG data
        self.channel_fps = self.fps
        self.paused = False
        self.loop = False
        self.pos = self.dur = 0

        if not self.connect():
            logging.error("Unable to connect CasparCG Server. Shutting down.")
            self.parent.shutdown()
            return

        self.caspar_data = CasparOSCServer(self.caspar_osc_port)
        self.lock = threading.Lock()
        self.work_thread = threading.Thread(target=self.work, args=())
        self.work_thread.start()

    def shutdown(self):
        logging.info("Controller shutdown requested")
        self.should_run = False
        self.caspar_data.shutdown()

    def on_main(self):
        if time.time() - self.caspar_data.last_osc > 5:
            logging.warning("Waiting for OSC")

    @property
    def id_channel(self):
        return self.parent.id_channel

    @property
    def request_time(self):
        return time.time()

    @property
    def fps(self):
        return self.parent.fps

    @property
    def position(self) -> float:
        """Time position (seconds) of the clip currently playing"""
        if self.current_item:
            return self.pos - self.current_item.mark_in()
        return self.pos

    @property
    def duration(self) -> float:
        """Duration (seconds) of the clip currently playing"""
        if self.parent.current_live:
            return 0
        return self.dur

    def connect(self):
        """Connect to a running CasparCG instance using AMCP protocol"""
        self.cmdc = CasparCG(self.caspar_host, self.caspar_port)
        return self.cmdc.connect()

    def query(self, *args, **kwargs) -> NebulaResponse:
        """Send an AMCP query to the CasparCG server"""
        return self.cmdc.query(*args, **kwargs)

    def work(self):
        while self.should_run:
            try:
                self.main()
            except Exception:
                log_traceback()
            time.sleep(1 / self.fps)
        logging.info("Controller work thread shutdown")

    def main(self):
        channel = self.caspar_data[self.caspar_channel]
        if not channel:
            return

        layer = channel[self.caspar_feed_layer]
        if not layer:
            return

        foreground = layer["foreground"]
        background = layer["background"]

        current_fname = os.path.splitext(foreground.name)[0]
        cued_fname = os.path.splitext(background.name)[0]
        pos = foreground.position
        dur = foreground.duration

        self.channel_fps = channel.fps
        self.paused = foreground.paused
        self.loop = foreground.loop

        #        if cued_fname and (not self.paused) and (pos == self.pos) and (not self.parent.current_live) and self.cued_item and (not self.cued_item["run_mode"]):
        #            if self.stalled and self.stalled < time.time() - 5:
        #                logging.warning("Stalled for a long time")
        #                logging.warning("Taking stalled clip (pos: {})".format(self.pos))
        #                self.take()
        #            elif not self.stalled:
        #                logging.debug("Playback is stalled")
        #                self.stalled = time.time()
        #        elif self.stalled:
        #            logging.debug("No longer stalled")
        #            self.stalled = False

        self.pos = pos
        self.dur = dur

        #
        # Playlist advancing
        #

        advanced = False
        if self.parent.cued_live:
            if ((background.producer == "empty")
                    and (foreground.producer != "empty") and not self.cueing):
                self.current_item = self.cued_item
                self.current_fname = "LIVE"
                advanced = True
                self.cued_item = False
                self.parent.on_live_enter()

        else:
            if (not cued_fname) and (current_fname):
                if current_fname == self.cued_fname:
                    self.current_item = self.cued_item
                    self.current_fname = self.cued_fname
                    advanced = True
                self.cued_item = False

        if advanced and not self.cueing:
            try:
                self.parent.on_change()
            except Exception:
                log_traceback("Playout on_change failed")

        if self.current_item and not self.cued_item and not self.cueing:
            self.cueing = True
            if not self.parent.cue_next():
                self.cueing = False

        if self.cueing:
            if cued_fname == self.cueing:
                logging.goodnews(f"Cued {self.cueing}")
                self.cued_item = self.cueing_item
                self.cueing_item = False
                self.cueing = False
            elif self.parent.cued_live:
                if background.producer != "empty":
                    logging.goodnews(f"Cued {self.cueing}")
                    self.cued_item = self.cueing_item
                    self.cueing_item = False
                    self.cueing = False

            else:
                logging.debug(
                    f"Waiting for cue {self.cueing} (is {cued_fname})")
                if time.time() - self.cueing_time > 5 and self.current_item:
                    logging.warning("Cueing again")
                    self.cueing = False
                    self.parent.cue_next()

        elif (not self.cueing and self.cued_item and cued_fname
              and cued_fname != self.cued_fname and not self.parent.cued_live):
            logging.error(
                f"Cue mismatch: IS: {cued_fname} SHOULDBE: {self.cued_fname}")
            self.cued_item = False

        self.current_fname = current_fname
        self.cued_fname = cued_fname

        try:
            self.parent.on_progress()
        except Exception:
            log_traceback("Playout on_progress failed")

    def cue(self,
            fname,
            item,
            layer=None,
            play=False,
            auto=True,
            loop=False,
            **kwargs):
        layer = layer or self.caspar_feed_layer

        query_list = ["PLAY" if play else "LOADBG"]
        query_list.append(f"{self.caspar_channel}-{layer}")
        query_list.append(fname)

        if auto:
            query_list.append("AUTO")
        if loop:
            query_list.append("LOOP")
        if item.mark_in():
            query_list.append(f"SEEK {int(item.mark_in() * self.channel_fps)}")
        if item.mark_out():
            query_list.append(
                f"LENGTH {int(item.duration * self.channel_fps)}")

        query = " ".join(query_list)

        self.cueing = fname
        self.cueing_item = item
        self.cueing_time = time.time()

        result = self.query(query)

        if result.is_error:
            message = f'Unable to cue "{fname}" {result.data}'
            self.cued_item = Item()
            self.cued_fname = False
            self.cueing = False
            self.cueing_item = False
            self.cueing_time = 0
            return NebulaResponse(result.response, message)
        if play:
            self.cueing = False
            self.cueing_item = False
            self.cueing_time = 0
            self.current_item = item
            self.current_fname = fname
        return NebulaResponse(200)

    def clear(self, layer=None):
        layer = layer or self.caspar_feed_layer
        result = self.query(f"CLEAR {self.channel}-{layer}")
        return NebulaResponse(result.response, result.data)

    def take(self, **kwargs):
        layer = kwargs.get("layer", self.caspar_feed_layer)
        if not self.cued_item or self.cueing:
            return NebulaResponse(400, "Unable to take. No item is cued.")
        result = self.query(f"PLAY {self.caspar_channel}-{layer}")
        if result.is_success:
            if self.parent.current_live:
                self.parent.on_live_leave()
            message = "Take OK"
            self.stalled = False
        else:
            message = "Take failed: {result.data}"
        return NebulaResponse(result.response, message)

    def retake(self, **kwargs):
        layer = kwargs.get("layer", self.caspar_feed_layer)
        if self.parent.current_live:
            return NebulaResponse(409, "Unable to retake live item")
        seekparams = "SEEK " + str(
            int(self.current_item.mark_in() * self.channel_fps))
        if self.current_item.mark_out():
            seekparams += " LENGTH " + str(
                int((self.current_item.mark_out() -
                     self.current_item.mark_in()) * self.channel_fps))
        query = f"PLAY {self.caspar_channel}-{layer} {self.current_fname} {seekparams}"
        result = self.query(query)
        if result.is_success:
            message = "Retake OK"
            self.stalled = False
            self.parent.cue_next()
        else:
            message = "Take command failed: " + result.data
        return NebulaResponse(result.response, message)

    def freeze(self, **kwargs):
        layer = kwargs.get("layer", self.caspar_feed_layer)
        if self.parent.current_live:
            return NebulaResponse(409, "Unable to freeze live item")
        if self.paused:
            query = f"RESUME {self.caspar_channel}-{layer}"
            message = "Playback resumed"
        else:
            query = f"PAUSE {self.caspar_channel}-{layer}"
            message = "Playback paused"
        result = self.query(query)
        return NebulaResponse(result.response, message)

    def abort(self, **kwargs):
        layer = kwargs.get("layer", self.caspar_feed_layer)
        if not self.cued_item:
            return NebulaResponse(400, "Unable to abort. No item is cued.")
        query = f"LOAD {self.caspar_channel}-{layer} {self.cued_fname}"
        if self.cued_item.mark_in():
            seek = int(self.cued_item.mark_in() * self.channel_fps)
            query += f" SEEK {seek}"
        if self.cued_item.mark_out():
            length = int(
                (self.cued_item.mark_out() - self.cued_item.mark_in()) *
                self.channel_fps)
            query += f" LENGTH {length}"
        result = self.query(query)
        return NebulaResponse(result.response, result.data)

    def set(self, key, value):
        if key == "loop":
            do_loop = int(str(value) in ["1", "True", "true"])
            result = self.query(
                f"CALL {self.caspar_channel}-{self.caspar_feed_layer} LOOP {do_loop}"
            )
            if self.current_item and bool(
                    self.current_item["loop"] != bool(do_loop)):
                self.current_item["loop"] = bool(do_loop)
                self.current_item.save(notify=False)
                bin_refresh([self.current_item["id_bin"]],
                            db=self.current_item.db)
            return NebulaResponse(result.response, f"SET LOOP: {result.data}")
        else:
            return NebulaResponse(400)