#!/usr/bin/env python3
"""Показать ролик на весь экран и повторять его без стыка.

Почему это отдельная программа, а не строчка с `gst-launch-1.0`.

Повтор раньше делался циклом оболочки: процесс доигрывал файл, выходил, цикл
запускал его заново. Работало, но на экране это видно. Завершаясь, процесс
уносит с собой окно Wayland, композитор показывает то, что под ним — дашборд, —
и следующий запуск накрывает его обратно примерно через секунду. Семисекундный
ролик на тридцатисекундном слайде даёт четыре таких мигания подряд.

Здесь конвейер живёт от начала слайда до конца, а повтор делается перемоткой
внутри него. Окно не пересоздаётся ни разу, композитору нечего показать под
ним, и стык не виден вовсе.

Перемотка — сегментная (`SEEK_FLAG_SEGMENT`): вместо конца потока конвейер
присылает `SEGMENT_DONE`, и следующий круг ставится в очередь ещё до того, как
кончился текущий. Это и есть бесшовность. Не все декодеры такое переживают;
если сегментная перемотка не поддержана, придёт обычный конец потока, и тогда
мы перематываем с промывкой — это стоит нескольких кадров, но окно всё равно
остаётся на месте, то есть даже худший случай здесь лучше прежнего лучшего.

Конвейер тот же, что выяснился измерениями на плате: между декодером и выводом
не должно быть ничего. Подробности — в OS/README.md.
"""

import os
import signal
import sys

FAST = (
    "filesrc name=src ! qtdemux ! h264parse ! v4l2slh264dec ! waylandsink name=sink"
)
# Запасной: разбирает что угодно, но кадр идёт через процессор. Нужен ради
# ролика в кодеке, которого видеодвижок платы не знает, — экран не должен
# чернеть из-за одного неудачного файла.
SAFE = "filesrc name=src ! decodebin ! videoconvert ! waylandsink name=sink"


def _shell_fallback(path: str) -> int:
    """Без PyGObject — старое поведение: цикл оболочки.

    Мигание на стыке вернётся, но это несравнимо лучше, чем чёрный экран.
    """
    import shlex

    quoted = shlex.quote(path)
    script = (
        "while :; do "
        f"gst-launch-1.0 -q filesrc location={quoted} "
        "! qtdemux ! h264parse ! v4l2slh264dec ! waylandsink "
        f"|| gst-launch-1.0 -q filesrc location={quoted} "
        "! decodebin ! videoconvert ! waylandsink "
        "|| exit 0; done"
    )
    os.execvp("sh", ["sh", "-c", script])
    return 1  # недостижимо: execvp не возвращается


def main(argv: list[str]) -> int:
    if len(argv) != 2:
        print("использование: dashdash-video <файл>", file=sys.stderr)
        return 2
    path = argv[1]
    if not os.path.isfile(path):
        print(f"нет файла: {path}", file=sys.stderr)
        return 1

    try:
        import gi

        gi.require_version("Gst", "1.0")
        from gi.repository import GLib, Gst
    except (ImportError, ValueError):
        return _shell_fallback(path)

    Gst.init(None)
    loop = GLib.MainLoop()
    state = {"pipeline": None, "description": FAST, "segment_ok": True}

    def build(description: str):
        pipeline = Gst.parse_launch(description)
        pipeline.get_by_name("src").set_property("location", path)
        return pipeline

    def seek_to_start(flush: bool) -> bool:
        flags = Gst.SeekFlags.FLUSH if flush else Gst.SeekFlags.SEGMENT
        return state["pipeline"].seek(
            1.0,
            Gst.Format.TIME,
            flags | Gst.SeekFlags.ACCURATE,
            Gst.SeekType.SET,
            0,
            Gst.SeekType.NONE,
            -1,
        )

    def on_message(_bus, message):
        kind = message.type
        if kind == Gst.MessageType.SEGMENT_DONE:
            # Круг кончился, следующий ставим в очередь без промывки: кадры
            # нового круга уже едут по конвейеру, пока показываются последние
            # кадры старого.
            seek_to_start(flush=False)
        elif kind == Gst.MessageType.EOS:
            # Сегментная перемотка не поддержана — перематываем с промывкой.
            # Окно при этом остаётся, теряются только кадры на стык.
            state["segment_ok"] = False
            if not seek_to_start(flush=True):
                loop.quit()
        elif kind == Gst.MessageType.ERROR:
            error, debug = message.parse_error()
            if state["description"] is FAST:
                # Скорее всего кодек, которого не знает видеодвижок платы.
                # Пересобираемся на универсальный конвейер и пробуем ещё раз.
                print(f"быстрый конвейер отказал ({error.message}), беру запасной",
                      file=sys.stderr)
                start(SAFE)
                return
            print(f"проигрывание не удалось: {error.message} / {debug}", file=sys.stderr)
            loop.quit()
        elif kind == Gst.MessageType.ASYNC_DONE and state["pipeline"] is not None:
            # Первая сегментная перемотка ставится только когда конвейер уже
            # готов: раньше этого момента позиция не определена, и перемотка
            # просто не проходит.
            if state["segment_ok"] and not state.get("armed"):
                state["armed"] = True
                if not seek_to_start(flush=False):
                    state["segment_ok"] = False

    def start(description: str) -> None:
        old = state["pipeline"]
        if old is not None:
            old.set_state(Gst.State.NULL)
        state["description"] = description
        state["armed"] = False
        pipeline = build(description)
        bus = pipeline.get_bus()
        bus.add_signal_watch()
        bus.connect("message", on_message)
        state["pipeline"] = pipeline
        pipeline.set_state(Gst.State.PLAYING)

    def stop(*_args) -> bool:
        if state["pipeline"] is not None:
            state["pipeline"].set_state(Gst.State.NULL)
        loop.quit()
        return True

    # Останов по сигналу, а не по умиранию: конвейер надо довести до NULL, иначе
    # окно и буферы декодера остаются висеть, и следующий ролик не получает
    # /dev/video0.
    GLib.unix_signal_add(GLib.PRIORITY_HIGH, signal.SIGTERM, stop)
    GLib.unix_signal_add(GLib.PRIORITY_HIGH, signal.SIGINT, stop)

    start(FAST)
    try:
        loop.run()
    finally:
        if state["pipeline"] is not None:
            state["pipeline"].set_state(Gst.State.NULL)
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))
