view light9/ascoltami/musictime_client.py @ 2183:081f36506ad3

address a bunch of type errors and loose types
author drewp@bigasterisk.com
date Fri, 19 May 2023 17:28:03 -0700
parents 5db8e7698d6a
children ccdfdc8183ad
line wrap: on
line source

import time, json, logging
from typing import Dict, cast
from twisted.internet.interfaces import IReactorTime

from twisted.internet import reactor
from twisted.internet.defer import inlineCallbacks
import treq

from light9 import networking

log = logging.getLogger()


class MusicTime(object):
    """
    fetch times from ascoltami in a background thread; return times
    upon request, adjusted to be more precise with the system clock
    """

    def __init__(self, period=.2, onChange=lambda position: None, pollCurvecalc='ignored'):
        """period is the seconds between
        http time requests.

        We call onChange with the time in seconds and the total time

        The choice of period doesn't need to be tied to framerate,
        it's more the size of the error you can tolerate (since we
        make up times between the samples, and we'll just run off the
        end of a song)
        """
        self.positionFetchTime = 0
        self.period = period
        self.hoverPeriod = .05
        self.onChange = onChange

        self.position: Dict[str, float] = {}
        # driven by our pollCurvecalcTime and also by Gui.incomingTime
        self.lastHoverTime = None  # None means "no recent value"
        self.pollMusicTime()

    def getLatest(self, frameTime=None) -> Dict:
        """
        dict with 't' and 'song', etc.

        frameTime is the timestamp from the camera, which will be used
        instead of now.

        Note that this may be called in a gst camera capture thread. Very often.
        """
        if not hasattr(self, 'position'):
            return {'t': 0, 'song': None}
        pos = self.position.copy()
        now = frameTime or time.time()
        if pos.get('playing'):
            pos['t'] = pos['t'] + (now - self.positionFetchTime)
        else:
            if self.lastHoverTime is not None:
                pos['hoverTime'] = self.lastHoverTime
        return pos

    def pollMusicTime(self):

        @inlineCallbacks
        def cb(response):

            if response.code != 200:
                raise ValueError("%s %s", response.code, (yield response.content()))

            position = yield response.json()

            # this is meant to be the time when the server gave me its
            # report, and I don't know if that's closer to the
            # beginning of my request or the end of it (or some
            # fraction of the way through)
            self.positionFetchTime = time.time()

            self.position = position
            self.onChange(position)

            cast(IReactorTime, reactor).callLater(self.period, self.pollMusicTime) # type: ignore

        def eb(err):
            log.warn("talking to ascoltami: %s", err.getErrorMessage())
            cast(IReactorTime, reactor).callLater(2, self.pollMusicTime) # type: ignore

        d = treq.get(networking.musicPlayer.path("time").toPython())
        d.addCallback(cb)
        d.addErrback(eb)  # note this includes errors in cb()

    def sendTime(self, t):
        """request that the player go to this time"""
        treq.post(
            networking.musicPlayer.path('time'),
            data=json.dumps({
                "t": time
            }).encode('utf8'),
            headers={b"content-type": [b"application/json"]},
        )