emmental/audio/bass.py
Anna Schumaker 4cdf4c528a audio: Implement our own about-to-finish handling
The playbin's about-to-finish signal triggers in a different thread,
which Gtk is very much not happy about, and often results in both the
about-to-finish and eos handlers getting called (and therefore multiple
tracks getting picked from the queue).

Fix this by checking how much time is left during the regular position
changed timeout function and triggering about-to-finish manually

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2021-12-09 17:51:02 -05:00

164 lines
4.7 KiB
Python

# Copyright 2021 (c) Anna Schumaker.
import gi
gi.require_version("Gst", "1.0")
import lib
import sys
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gst
Gst.init(sys.argv)
from . import replaygain
TIMEOUT = 100
class BassPlayer(GObject.GObject):
def __init__(self):
GObject.GObject.__init__(self)
lib.settings.initialize("audio.replaygain", "disabled")
lib.settings.initialize("audio.volume", 1.0)
self.audio = replaygain.ReplayGainSink()
self.video = Gst.ElementFactory.make("fakesink")
self.playbin = Gst.ElementFactory.make("playbin")
self.playbin.set_property("audio-sink", self.audio)
self.playbin.set_property("video-sink", self.video)
self.playbin.set_property("volume", lib.settings.get_float("audio.volume"))
self.playbin.set_state(Gst.State.READY)
self.set_property("replaygain", lib.settings.get("audio.replaygain"))
self.bus.add_signal_watch()
self.bus.connect("message::eos", self.__eos__)
self.bus.connect("message::state-changed", self.state_changed)
self.bus.connect("message::stream-start", self.stream_start)
self.bus.connect("message::state-changed", self.state_changed)
self.bus.connect("message::tag", self.tag)
self.timeout = None
def __eos__(self, bus, message):
self.emit("eos")
@GObject.Property
def bus(self):
return self.playbin.get_bus()
@GObject.Property
def duration(self):
(res, dur) = self.playbin.query_duration(Gst.Format.TIME)
return dur if res == True else 0
@GObject.Property
def playing(self):
(ret, state, pending) = self.playbin.get_state(Gst.CLOCK_TIME_NONE)
return state == Gst.State.PLAYING
@playing.setter
def playing(self, playing):
state = Gst.State.PLAYING if playing else Gst.State.PAUSED
self.playbin.set_state(state)
@GObject.Property
def play_percent(self):
if self.playbin.clock == None or self.duration == 0:
return 0
runtime = self.playbin.clock.get_time() - self.playbin.base_time
return runtime / self.duration
@GObject.Property
def position(self):
(res, pos) = self.playbin.query_position(Gst.Format.TIME)
return pos if res == True else 0
@position.setter
def position(self, pos):
self.playbin.seek_simple(Gst.Format.TIME,
Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
pos)
@GObject.Property
def replaygain(self):
return self.audio.get_property("mode")
@replaygain.setter
def replaygain(self, mode):
lib.settings.set("audio.replaygain", mode)
self.audio.set_property("mode", mode)
@GObject.Property
def uri(self):
return self.playbin.get_property("uri")
@uri.setter
def uri(self, uri):
if uri:
self.playbin.set_property("uri", uri)
else:
self.playbin.set_state(Gst.State.READY)
@GObject.Property
def volume(self):
return self.playbin.get_property("volume")
@volume.setter
def volume(self, vol):
self.playbin.set_property("volume", vol)
lib.settings.set("audio.volume", vol)
def state_changed(self, bus, message):
if message.src == self.playbin:
(old, new, pending) = message.parse_state_changed()
if new == Gst.State.PLAYING:
self.emit("playback-start")
else:
self.emit("playback-paused")
def stream_start(self, bus, message):
self.emit("duration-changed")
def tag(self, bus, message):
(res, sample) = message.parse_tag().get_sample("image")
if res:
self.emit("artwork", sample)
def timeout_function(self):
self.emit("position-changed")
return GLib.SOURCE_CONTINUE
@GObject.Signal
def about_to_finish(self):
pass
@GObject.Signal(arg_types=(Gst.Sample,))
def artwork(self, sample):
pass
@GObject.Signal
def duration_changed(self):
pass
@GObject.Signal
def eos(self):
pass
@GObject.Signal
def playback_start(self):
if not self.timeout:
self.timeout = GLib.timeout_add(TIMEOUT, self.timeout_function)
@GObject.Signal
def playback_paused(self):
if self.timeout:
GLib.source_remove(self.timeout)
self.timeout = None
@GObject.Signal
def position_changed(self):
remaining = self.duration - self.position
if remaining < 2 * Gst.SECOND:
if remaining + (TIMEOUT * Gst.MSECOND) >= 2 * Gst.SECOND:
self.emit("about-to-finish")