示例#1
0
def api_solve(**kwargs):
    id_item = kwargs.get("id_item", False)
    solver_name = kwargs.get("solver", False)
    items = kwargs.get("items", [])
    user = kwargs.get("user", anonymous)

    if not user.has_right("rundown_edit", anyval=True):
        return NebulaResponse(403)

    if id_item:
        items.append(id_item)

    if not (items and solver_name):
        return NebulaResponse(
            400, "You must specify placeholder item ID and a solver name"
        )

    Solver = get_solver(solver_name)
    if Solver is None:
        return NebulaResponse(500, "Unable to load the solver. Check logs for details")

    db = DB()
    for id_item in items:
        solver = Solver(Item(id_item, db=db), db=db)
        response = solver.main()
        if response.is_error:
            return response

    return response
示例#2
0
    def channel_recover(self):
        logging.warning("Performing recovery")

        db = DB()
        db.query(
            """
            SELECT id_item, start FROM asrun
            WHERE id_channel = %s ORDER BY id DESC LIMIT 1
            """,
            [self.id_channel],
        )
        try:
            last_id_item, last_start = db.fetchall()[0]
        except IndexError:
            logging.error("Unable to perform recovery.")
        last_item = Item(last_id_item, db=db)
        last_item.asset

        self.controller.current_item = last_item
        self.controller.cued_item = False
        self.controller.cued_fname = False

        if last_start + last_item.duration <= time.time():
            logging.info(f"Last {last_item} has been broadcasted.")
            new_item = self.cue_next(item=last_item, db=db, play=True)
        else:
            logging.info(f"Last {last_item} has not been fully broadcasted.")
            new_item = self.cue_next(item=last_item, db=db)

        if not new_item:
            logging.error("Recovery failed. Unable to cue")
            return

        self.on_change()
示例#3
0
    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)
示例#4
0
    def block_split(self, tc):
        if tc <= self.event["start"] or tc >= self.next_event["start"]:
            logging.error(
                "Timecode of block split must be between "
                "the current and next event start times"
            )
            return False

        logging.info(f"Splitting {self.event} at {format_time(tc)}")
        logging.info(
            "Next event is {} at {}".format(
                self.next_event, self.next_event.show("start")
            )
        )
        new_bin = Bin(db=self.db)
        new_bin.save(notify=False)

        new_placeholder = Item(db=self.db)
        new_placeholder["id_bin"] = new_bin.id
        new_placeholder["position"] = 0

        for key in self.placeholder.meta.keys():
            if key not in ["id_bin", "position", "id_asset", "id"]:
                new_placeholder[key] = self.placeholder[key]

        new_placeholder.save(notify=False)
        new_bin.append(new_placeholder)
        new_bin.save(notify=False)

        new_event = Event(db=self.db)
        new_event["id_channel"] = self.event["id_channel"]
        new_event["title"] = "Split block"
        new_event["start"] = tc
        new_event["id_magic"] = new_bin.id

        new_event.save(notify=False)

        self._needed_duration = None
        self._next_event = None
        self.solve_next = new_placeholder

        if new_bin.id not in self.affected_bins:
            self.affected_bins.append(new_bin.id)

        return True
示例#5
0
    def cue(self, fname, item, **kwargs):
        auto = kwargs.get("auto", True)
        layer = kwargs.get("layer", self.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.caspar_channel, layer, fname, marks)
        else:
            q = "LOADBG {}-{} {} {} {}".format(
                self.caspar_channel, layer, fname, ["", "AUTO"][auto], marks
            )

        self.cueing = fname
        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
            message = "Cued item {} ({})".format(self.cued_item, fname)
        return NebulaResponse(result.response, message)
示例#6
0
    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()
示例#7
0
def get_bin_first_item(id_bin, db=False):
    if not db:
        db = DB()
    db.query(
        """
        SELECT id, meta FROM items
        WHERE id_bin=%s
        ORDER BY position LIMIT 1
        """,
        [id_bin],
    )
    for _, meta in db.fetchall():
        return Item(meta=meta, db=db)
    return False
示例#8
0
def get_next_item(item, **kwargs):
    db = kwargs.get("db", DB())
    force = kwargs.get("force", False)
    if type(item) == int and item > 0:
        current_item = Item(item, db=db)
    elif isinstance(item, Item):
        current_item = item
    else:
        logging.error(f"Unexpected get_next_item argument {item}")
        return False

    logging.debug(f"Looking for an item following {current_item}")
    current_bin = Bin(current_item["id_bin"], db=db)

    items = current_bin.items
    if force == "prev":
        items.reverse()

    for item in items:
        if (force == "prev" and item["position"] < current_item["position"]
            ) or (force != "prev"
                  and item["position"] > current_item["position"]):
            if item["item_role"] == "lead_out" and not force:
                logging.info("Cueing Lead In")
                for i, r in enumerate(current_bin.items):
                    if r["item_role"] == "lead_in":
                        return r
                else:
                    next_item = current_bin.items[0]
                    next_item.asset
                    return next_item
            if item["run_mode"] == RunMode.RUN_SKIP:
                continue
            item.asset
            return item
    else:
        current_event = get_item_event(item.id, db=db)
        direction = ">"
        order = "ASC"
        if force == "prev":
            direction = "<"
            order = "DESC"
        db.query(
            f"""
            SELECT meta FROM events
            WHERE id_channel = %s and start {direction} %s
            ORDER BY start {order} LIMIT 1
            """,
            [current_event["id_channel"], current_event["start"]],
        )
        try:
            next_event = Event(meta=db.fetchall()[0][0], db=db)
            if not next_event.bin.items:
                logging.debug("Next playlist is empty")
                raise Exception
            if next_event["run_mode"] and not kwargs.get(
                    "force_next_event", False):
                logging.debug("Next playlist run mode is not auto")
                raise Exception
            if force == "prev":
                next_item = next_event.bin.items[-1]
            else:
                next_item = next_event.bin.items[0]
            next_item.asset
            return next_item
        except Exception:
            logging.info("Looping current playlist")
            next_item = current_bin.items[0]
            next_item.asset
            return next_item
示例#9
0
class CasparController(object):
    time_unit = "f"

    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_channel = int(parent.channel_config.get("caspar_channel", 1))
        self.caspar_feed_layer = int(parent.channel_config.get("caspar_feed_layer", 10))

        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.loop = 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.caspar_channel)

        thread.start_new_thread(self.work, ())

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

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

    @property
    def port(self):
        return self.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(0.01)
            except Exception:
                log_traceback()
            time.sleep(0.3)

    def main(self):
        info = self.parser.get_info(self.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(0.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.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.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.caspar_channel, layer, fname, marks)
        else:
            q = "LOADBG {}-{} {} {} {}".format(
                self.caspar_channel, layer, fname, ["", "AUTO"][auto], marks
            )

        self.cueing = fname
        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
            message = "Cued item {} ({})".format(self.cued_item, fname)
        return NebulaResponse(result.response, message)

    def clear(self, **kwargs):
        layer = kwargs.get("layer", self.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.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.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
        else:
            message = "Take command 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")
        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.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.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 not self.paused:
            q = "PAUSE {}-{}".format(self.caspar_channel, layer)
            message = "Playback paused"
            new_val = True
        else:
            if self.parser.protocol >= 2.07:
                q = "RESUME {}-{}".format(self.caspar_channel, layer)
            else:
                length = "LENGTH {}".format(
                    int((self.current_out or self.fdur) - self.fpos)
                )
                q = "PLAY {}-{} {} SEEK {} {}".format(
                    self.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.caspar_feed_layer)
        if not self.cued_item:
            return NebulaResponse(400, "Unable to abort. No item is cued.")
        q = "LOAD {}-{} {}".format(self.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)
示例#10
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)
示例#11
0
    def cue(self, **kwargs):
        db = kwargs.get("db", DB())
        lcache = kwargs.get("cache", cache)

        if "item" in kwargs and isinstance(kwargs["item"], Item):
            item = kwargs["item"]
            del kwargs["item"]
        elif "id_item" in kwargs:
            item = Item(int(kwargs["id_item"]), db=db, cache=lcache)
            item.asset
            del kwargs["id_item"]
        else:
            return NebulaResponse(400, "Unable to cue. No item specified")

        if not item:
            return NebulaResponse(404, f"Unable to cue. {item} does not exist")

        if item["item_role"] == "live":
            logging.info("Next is item is live")
            fname = self.channel_config.get("live_source", "EMPTY")
            response = self.controller.cue(fname, item, **kwargs)
            if response.is_success:
                self.cued_live = True
            return response

        if not item["id_asset"]:
            return NebulaResponse(400, f"Unable to cue virtual {item}")

        asset = item.asset
        playout_status = asset.get(self.status_key, DEFAULT_STATUS)["status"]

        kwargs["fname"] = kwargs["full_path"] = None
        if playout_status in [
                ObjectStatus.ONLINE,
                ObjectStatus.CREATING,
                ObjectStatus.UNKNOWN,
        ]:
            kwargs["fname"] = asset.get_playout_name(self.id_channel)
            kwargs["full_path"] = asset.get_playout_full_path(self.id_channel)

        if (not kwargs["full_path"] and self.channel_config.get("allow_remote")
                and asset["status"]
                in (ObjectStatus.ONLINE, ObjectStatus.CREATING)):
            kwargs["fname"] = kwargs["full_path"] = asset.file_path
            kwargs["remote"] = True

        if not kwargs["full_path"]:
            state = ObjectStatus(playout_status).name
            return NebulaResponse(404, f"Unable to cue {state} playout file")

        kwargs["mark_in"] = item["mark_in"]
        kwargs["mark_out"] = item["mark_out"]

        if item["run_mode"] == 1:
            kwargs["auto"] = False
        else:
            kwargs["auto"] = True

        kwargs["loop"] = bool(item["loop"])

        self.cued_live = False
        return self.controller.cue(item=item, **kwargs)
示例#12
0
def get_rundown(id_channel, start_time=False, end_time=False, db=False):
    """Get a rundown."""
    db = db or DB()
    channel_config = config["playout_channels"][id_channel]
    if not start_time:
        # default today
        sh, sm = channel_config.get("day_start", [6, 0])
        rundown_date = time.strftime("%Y-%m-%d", time.localtime(time.time()))
        start_time = datestr2ts(rundown_date, hh=sh, mm=sm)

    end_time = end_time or start_time + (3600 * 24)

    item_runs = get_item_runs(id_channel, start_time, end_time, db=db)

    if channel_config.get("send_action", False):
        db.query(
            """SELECT id_asset FROM jobs
            WHERE id_action=%s AND status in (0, 5)
            """,
            [channel_config["send_action"]],
        )
        pending_assets = [r[0] for r in db.fetchall()]
    else:
        pending_assets = []

    db.query(
        """
        SELECT
            e.id,
            e.meta,
            i.meta,
            a.meta
        FROM
            events AS e

        LEFT JOIN
            items AS i
        ON
            e.id_magic = i.id_bin

        LEFT JOIN
            assets AS a
        ON
            i.id_asset = a.id

        WHERE
            e.id_channel = %s AND e.start >= %s AND e.start < %s

        ORDER BY
            e.start ASC,
            i.position ASC,
            i.id ASC
        """,
        (id_channel, start_time, end_time),
    )

    current_event_id = None
    event = None

    ts_broadcast = ts_scheduled = 0
    pskey = "playout_status/{}".format(id_channel)
    for id_event, emeta, imeta, ameta in db.fetchall() + [
        (-1, None, None, None)
    ]:
        if id_event != current_event_id:
            if event:
                yield event
                if not event.items:
                    ts_broadcast = 0
            if id_event == -1:
                break

            event = Event(meta=emeta)
            event.items = []
            current_event_id = id_event
            rundown_event_asset = event.meta.get("id_asset", False)

            if event["run_mode"]:
                ts_broadcast = 0
            event.meta["rundown_scheduled"] = ts_scheduled = event["start"]
            event.meta["rundown_broadcast"] = ts_broadcast = (ts_broadcast
                                                              or ts_scheduled)

        if imeta:
            item = Item(meta=imeta, db=db)
            if ameta:
                asset = Asset(meta=ameta, db=db) if ameta else False
                item._asset = asset
            else:
                asset = False

            as_start, as_stop = item_runs.get(item.id, (0, 0))
            airstatus = 0
            if as_start:
                ts_broadcast = as_start
                if as_stop:
                    airstatus = ObjectStatus.AIRED
                else:
                    airstatus = ObjectStatus.ONAIR

            item.meta["asset_mtime"] = asset["mtime"] if asset else 0
            item.meta["rundown_scheduled"] = ts_scheduled
            item.meta["rundown_broadcast"] = ts_broadcast
            item.meta["rundown_difference"] = ts_broadcast - ts_scheduled
            if rundown_event_asset:
                item.meta["rundown_event_asset"] = rundown_event_asset

            istatus = 0
            if not asset:
                istatus = ObjectStatus.ONLINE
            elif airstatus:
                istatus = airstatus
            elif asset["status"] == ObjectStatus.OFFLINE:
                istatus = ObjectStatus.OFFLINE
            elif pskey not in asset.meta:
                istatus = ObjectStatus.REMOTE
            elif asset[pskey]["status"] == ObjectStatus.OFFLINE:
                istatus = ObjectStatus.REMOTE
            elif asset[pskey]["status"] == ObjectStatus.ONLINE:
                istatus = ObjectStatus.ONLINE
            elif asset[pskey]["status"] == ObjectStatus.CORRUPTED:
                istatus = ObjectStatus.CORRUPTED
            else:
                istatus = ObjectStatus.UNKNOWN

            item.meta["status"] = istatus
            if asset and asset.id in pending_assets:
                item.meta["transfer_progress"] = -1

            if item["run_mode"] != RunMode.RUN_SKIP:
                ts_scheduled += item.duration
                ts_broadcast += item.duration

            event.items.append(item)
示例#13
0
def api_order(**kwargs):
    """
    Changes order of items in bin/rundown, creates new items from assets
    """

    id_channel = kwargs.get("id_channel", 0)
    id_bin = kwargs.get("id_bin", False)
    order = kwargs.get("order", [])
    db = kwargs.get("db", DB())
    user = kwargs.get("user", anonymous)
    initiator = kwargs.get("initiator", None)

    if not user:
        return NebulaResponse(401)

    if not id_channel in config["playout_channels"]:
        return NebulaResponse(400, f"No such channel ID {id_channel}")

    playout_config = config["playout_channels"][id_channel]
    append_cond = playout_config.get("rundown_accepts", "True")

    if id_channel and not user.has_right("rundown_edit", id_channel):
        return NebulaResponse(403, "You are not allowed to edit this rundown")

    if not (id_bin and order):
        return NebulaResponse(
            400, f'Bad "order" request<br>id_bin: {id_bin}<br>order: {order}')

    logging.info(f"{user} executes bin_order method")
    affected_bins = [id_bin]
    pos = 1
    rlen = float(len(order))
    for i, obj in enumerate(order):
        object_type = obj["object_type"]
        id_object = obj["id_object"]
        meta = obj["meta"]

        if object_type == "item":
            if not id_object:
                item = Item(db=db)
                item["id_asset"] = obj.get("id_asset", 0)
                item.meta.update(meta)
            else:
                item = Item(id_object, db=db)
                if not item["id_bin"]:
                    logging.error(
                        f"Attempted asset data insertion ({object_type} ID {id_object} {meta}) to item. This should never happen"
                    )
                    continue

                if not item:
                    logging.debug(f"Skipping {item}")
                    continue

            if not item["id_bin"] in affected_bins:
                if item["id_bin"]:
                    affected_bins.append(item["id_bin"])

        elif object_type == "asset":
            asset = Asset(id_object, db=db)
            if not asset:
                logging.error(
                    f"Unable to append {object_type} ID {id_object}. Asset does not exist"
                )
                continue
            try:
                can_append = eval(append_cond)
            except Exception:
                log_traceback(
                    "Unable to evaluate rundown accept condition: {append_cond}"
                )
                continue
            if not can_append:
                logging.error(
                    f"Unable to append {asset}. Does not match conditions.")
                continue
            item = Item(db=db)
            for key in meta:
                if key in ["id", "id_bin", "id_asset"]:
                    continue
                item[key] = meta[key]
            item["id_asset"] = asset.id
            item.meta.update(meta)
        else:
            logging.error(
                f"Unable to append {object_type} ID {id_object} {meta}. Unexpected object"
            )
            continue

        if not item or item["position"] != pos or item["id_bin"] != id_bin:
            item["position"] = pos
            item["id_bin"] = id_bin
            # bin_refresh called later should be enough to trigger rundown reload
            item.save(notify=False)
        pos += 1

    # Update bin duration
    bin_refresh(affected_bins, db=db, initiator=initiator)

    return NebulaResponse(200)
示例#14
0
def api_schedule(**kwargs):
    id_channel = kwargs.get("id_channel", 0)
    start_time = kwargs.get("start_time", 0)
    end_time = kwargs.get("end_time", 0)
    events = kwargs.get("events", [])  # Events to add/update
    delete = kwargs.get("delete", [])  # Event ids to delete
    db = kwargs.get("db", DB())
    user = kwargs.get("user", anonymous)
    initiator = kwargs.get("initiator", None)

    try:
        id_channel = int(id_channel)
    except ValueError:
        return NebulaResponse(400, "id_channel must be an integer")

    try:
        start_time = int(start_time)
    except ValueError:
        return NebulaResponse(400, "start_time must be an integer")

    try:
        end_time = int(end_time)
    except ValueError:
        return NebulaResponse(400, "end_time must be an integer")

    if not id_channel or id_channel not in config["playout_channels"]:
        return NebulaResponse(400, f"Unknown playout channel ID {id_channel}")

    changed_event_ids = []

    #
    # Delete events
    #

    for id_event in delete:
        if not user.has_right("scheduler_edit", id_channel):
            return NebulaResponse(403, "You are not allowed to edit this channel")
        event = Event(id_event, db=db)
        if not event:
            logging.warning(f"Unable to delete non existent event ID {id_event}")
            continue
        try:
            event.bin.delete()
        except psycopg2.IntegrityError:
            return NebulaResponse(423, f"Unable to delete {event}. Already aired.")
        else:
            event.delete()
        changed_event_ids.append(event.id)

    #
    # Create / update events
    #

    for event_data in events:
        if not user.has_right("scheduler_edit", id_channel):
            return NebulaResponse(423, "You are not allowed to edit this channel")
        id_event = event_data.get("id", False)

        db.query(
            "SELECT meta FROM events WHERE id_channel=%s and start=%s",
            [id_channel, event_data["start"]],
        )
        try:
            event_at_pos_meta = db.fetchall()[0][0]
            event_at_pos = Event(meta=event_at_pos_meta, db=db)
        except IndexError:
            event_at_pos = False

        if id_event:
            logging.debug(f"Updating event ID {id_event}")
            event = Event(id_event, db=db)
            if not event:
                logging.warning(f"No such event ID {id_event}")
                continue
            pbin = event.bin
        elif event_at_pos:
            event = event_at_pos
            pbin = event.bin
        else:
            logging.debug("Creating new event")
            event = Event(db=db)
            pbin = Bin(db=db)
            pbin.save()
            logging.debug("Saved", pbin)
            event["id_magic"] = pbin.id
            event["id_channel"] = id_channel

        id_asset = event_data.get("id_asset", False)
        if id_asset and id_asset != event["id_asset"]:
            asset = Asset(id_asset, db=db)
            if asset:
                logging.info(f"Replacing event primary asset with {asset}")
                pbin.delete_children()
                pbin.items = []

                item = Item(db=db)
                item["id_asset"] = asset.id
                item["position"] = 0
                item["id_bin"] = pbin.id
                item._asset = asset
                item.save()
                pbin.append(item)
                pbin.save()

                event["id_asset"] = asset.id
                for key in meta_types:
                    if meta_types[key]["ns"] != "m":
                        continue
                    if key in asset.meta:
                        event[key] = asset[key]

        for key in event_data:
            if key == "id_magic" and not event_data[key]:
                continue

            if key == "_items":
                for item_data in event_data["_items"]:
                    if not pbin.items:
                        start_pos = 0
                    else:
                        start_pos = pbin.items[-1]["position"]
                    try:
                        pos = int(item_data["position"])
                    except KeyError:
                        pos = 0
                    item = Item(meta=item_data, db=db)
                    item["position"] = start_pos + pos
                    item["id_bin"] = pbin.id
                    item.save()
                continue
            event[key] = event_data[key]

        changed_event_ids.append(event.id)
        event.save(notify=False)

    if changed_event_ids:
        messaging.send(
            "objects_changed",
            objects=changed_event_ids,
            object_type="event",
            initiator=initiator,
        )

    #
    # Return existing events
    #

    # TODO: ACL scheduler view

    result = []
    if start_time and end_time:
        logging.debug(
            f"Requested events of channel {id_channel} "
            f"from {format_time(start_time)} to {format_time(end_time)}"
        )

        db.query(
            """
                SELECT e.meta, o.meta FROM events AS e, bins AS o
                WHERE
                    e.id_channel=%s
                    AND e.start > %s
                    AND e.start < %s
                    AND e.id_magic = o.id
                ORDER BY start ASC""",
            [id_channel, start_time, end_time],
        )
        res = db.fetchall()
        db.query(
            """
                SELECT e.meta, o.meta FROM events AS e, bins AS o
                WHERE
                    e.id_channel=%s
                    AND start <= %s
                    AND e.id_magic = o.id
                ORDER BY start DESC LIMIT 1""",
            [id_channel, start_time],
        )
        res = db.fetchall() + res

        for event_meta, alt_meta in res:
            ebin = Bin(meta=alt_meta, db=db)
            if "duration" in alt_meta.keys():
                event_meta["duration"] = ebin.duration
            result.append(event_meta)

    return NebulaResponse(200, data=result)