view blender/time_sync/time_from_graph.py @ 2457:d94480bfb179

more work on blender time sync. Might be working, aside from blender play-button
author drewp@bigasterisk.com
date Tue, 20 May 2025 13:48:07 -0700
parents 2d454737a916
children 0e27ba33118c
line wrap: on
line source

import asyncio
import threading
import time
from typing import Coroutine

from attr import dataclass
from rdfdb.patch import Patch
from rdfdb.syncedgraph.syncedgraph import SyncedGraph
from rdflib import Literal

from light9 import networking, showconfig
from light9.ascoltami.graph_context import ascoltamiContext
from light9.ascoltami.play_state import AscoPlayState
from light9.namespaces import L9
from light9.newtypes import decimalLiteral
from light9.run_local import log
from light9.typedgraph import typedValue

from ..asyncio_thread import startLoopInThread
from .blender_time import (
    BlenderTime,
    PausedGotoTime,
    PlayingGotoTime,
    SceneLoaded,
    _TimeEvent,
)


class Sync:
    """asco is the authority on playback status. Sync maintains a copy of the state"""
    lock = threading.Lock()

    # this is edited ONLY by bg thread
    ascoPlayState: AscoPlayState

    def __init__(self):
        # main thread
        self.ascoPlayState = AscoPlayState(None, None, False, False, 1.0)
        self.blenderTime = BlenderTime(self.onBlenderEvent, self.ascoPlayState)
        self.blenderTime.start()

        self.ctx = ascoltamiContext(showconfig.showUri())
        log.debug('🚋3 startLoopInThread')
        self._loop = startLoopInThread(self.connectGraph())
        log.info('🚋10 Sync initd')

    def onBlenderEvent(self, event: _TimeEvent):
        # main thread
        match event:
            case SceneLoaded():
                self.blenderTime.setRange(self.ascoPlayState.duration)
                self.blenderTime.setCurrentTime(self.ascoPlayState.getCurrentSongTime() or 0.0)
            case PausedGotoTime(t):
                self.runInBackgroundLoop(self.setInGraph(t, False))
            case PlayingGotoTime(t):
                self.runInBackgroundLoop(self.setInGraph(t, True))

    def runInBackgroundLoop(self, f: Coroutine):
        # main thread
        asyncio.run_coroutine_threadsafe(f, self._loop)

    async def connectGraph(self):
        # bg thread
        log.info('🚋11 start SyncedGraph')
        self.graph = SyncedGraph(networking.rdfdb.url, "time_sync")
        self.graph.addHandler(self.syncFromGraph)

    def syncFromGraph(self):
        # bg thread
        with self.lock:
            asco = L9['ascoltami']
            
            self.ascoPlayState.wallStartTime = typedValue(float | None, self.graph, asco, L9['wallStartTime'])
            self.ascoPlayState.pausedSongTime = typedValue(float | None, self.graph, asco, L9['pausedSongTime'])
            self.ascoPlayState.duration = typedValue(float | None, self.graph, asco, L9['duration']) or 1.0
            self.blenderTime.durationDirty = True # todo: called too often
            self.ascoPlayState.playing = typedValue(bool | None, self.graph, asco, L9['playing']) or False
            self.ascoPlayState.endOfSong = typedValue(bool | None, self.graph, asco, L9['endOfSong']) or False
            log.info(f'🍇 syncFromGraph {self.ascoPlayState=}')
            self.blenderTime.curFrameDirty = True

    async def setGraphPlaying(self, isBlenderPlaying: bool):
        return
        # bg thread
        log.info(f'set graph playing to {isBlenderPlaying}')
        self.graph.patchObject(self.ctx, L9['ascoltami'], L9['playing'], Literal(isBlenderPlaying))

    async def setInGraph(self, t: float, isBlenderPlaying: bool):
        # bg thread
        log.info(f'set graph time to {t:.2f} {isBlenderPlaying=}')
        p = Patch()
        if isBlenderPlaying:
            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['playing'], Literal(True)))
            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['wallStartTime'], decimalLiteral(round(time.time() - t, 1))))
            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['pausedSongTime'], None))
        else:
            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['playing'], Literal(False)))
            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['wallStartTime'], None))
            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['pausedSongTime'], decimalLiteral(round(t, 1))))

        if p.isEmpty():
            return
        log.info(f'setInGraph {p.shortSummary()}')
        await self.graph.patch(p)