BlinkenArea - GitList
Repositories
Blog
Wiki
stage_director
Code
Commits
Branches
Tags
Search
Tree:
8f3c324
Branches
Tags
master
stage_director
sync_gui.py
implement UDP output and destination config dialog
Stefan Schuermans
commited
8f3c324
at 2013-11-23 22:31:06
sync_gui.py
Blame
History
Raw
#! /usr/bin/env python # BlinkenArea Sync GUI # version 0.1.0 date 2013-11-23 # Copyright 2013 Stefan Schuermans <stefan@blinkenarea.org> # Copyleft: GNU public license - http://www.gnu.org/copyleft/gpl.html # a blinkenarea.org project - https://www.blinkenarea.org/ import os from gi.repository import Gtk import gobject import pango import socket import struct import sys import time import playlist import time_fmt scriptdir = os.path.dirname(os.path.abspath(__file__)) class SyncGui: def __init__(self): """construct a SyncGui object""" self.builder = Gtk.Builder() self.builder.add_from_file(scriptdir + "/sync_gui.glade") self.widMainWindow = self.builder.get_object("MainWindow") self.widPlaylistView = self.builder.get_object("PlaylistView") self.widPlaylistStore = self.builder.get_object("PlaylistStore") self.widPosition = self.builder.get_object("Position") self.widPositionScale = self.builder.get_object("PositionScale") self.widPositionAt = self.builder.get_object("PositionAt") self.widPositionRemaining = self.builder.get_object("PositionRemaining") self.widBtnPause = self.builder.get_object("Pause") self.widBtnPlay = self.builder.get_object("Play") self.widLogoO = self.builder.get_object("LogoO") self.widLogoG = self.builder.get_object("LogoG") self.widStatus = self.builder.get_object("Status") handlers = { "onDestroy": self.onDestroy, "onFileOpen": self.onFileOpen, "onFileExit": self.onFileExit, "onExtrasDestination": self.onExtrasDestination, "onPlaylistDblClick": self.onPlaylistDblClick, "onNewPosition": self.onNewPosition, "onPrevious": self.onPrevious, "onStop": self.onStop, "onPause": self.onPause, "onPlay": self.onPlay, "onNext": self.onNext, } self.builder.connect_signals(handlers) self.playlist = playlist.Playlist() if len(sys.argv) >= 2: # load initial playlist from command line self.playlist.read(sys.argv[1]) self.playlist.update(self.widPlaylistStore) self.sock = None self.stEntryIdx = -1 # no entry selected self.stName = "" # no current entry name self.stDuration = 0 # current entry has zero size self.stPosition = 0 # at begin of current entry self.stPlaying = False # not playing self.stDestination = "255.255.255.255" # local LAN broadcast by default self.setupSock() self.updateEntry() self.updateButtonVisibility() gobject.timeout_add(10, self.onTimer10ms) gobject.timeout_add(100, self.onTimer100ms) def showPosition(self): """update the position texts next to the position slider""" # format current time and remaining time posAt = time_fmt.sec2str(self.stPosition) posRemaining = time_fmt.sec2str(self.stDuration - self.stPosition) self.widPositionAt.set_text(posAt) self.widPositionRemaining.set_text(posRemaining) def updatePositionState(self): """update the position in the state, but not the slider""" # calculate (virtual) start time of playing # i.e. the time the playing would have had started to arrive at the # current position now if it had played continuosly self.stPlayStart = time.time() - self.stPosition # update position texts self.showPosition() def updatePosition(self): """update the position including the position slider""" # update GUI slider self.widPositionScale.set_value(self.stPosition) # update position state self.updatePositionState() def updateDuration(self): """update the duration (i.e. range for the slider) based on the current playlist entry""" # get duration of new playlist entry self.stDuration = 0 if self.stEntryIdx >= 0: entry = self.playlist.entries[self.stEntryIdx] if entry["type"] == "normal": self.stDuration = entry["duration"] # set position to begin self.stPosition = 0 # update value range self.widPosition.set_upper(self.stDuration) # update position of slider self.updatePosition() def updateEntry(self): """update current entry of playlist and duration, position, ...""" # clear selection of playlist sel = self.widPlaylistView.get_selection() if sel: sel.unselect_all() # sanity check for entry index if self.stEntryIdx < -1 or self.stEntryIdx >= len(self.playlist.entries): self.stEntryIdx = -1 # get name of current entry self.stName = "" if self.stEntryIdx >= 0 and \ self.playlist.entries[self.stEntryIdx]["type"] == "normal": self.stName = self.playlist.entries[self.stEntryIdx]["name"] # make current entry bold, all others non-bold def update(model, path, it, user_data): (idx,) = model.get(it, 0) if idx == self.stEntryIdx: weight = pango.WEIGHT_BOLD else: weight = pango.WEIGHT_NORMAL model.set(it, 1, weight) self.widPlaylistStore.foreach(update, None) # playing and (no entry or stop entry) # -> stop playing and update button visibility if self.stPlaying and (self.stEntryIdx < 0 or \ self.playlist.entries[self.stEntryIdx]["type"] == "stop"): self.stPlaying = False self.updateButtonVisibility() # update duration, position, ... self.updateDuration() def updateButtonVisibility(self): """update the visibility of the buttons based on if playing or not""" self.widBtnPause.set_visible(self.stPlaying) self.widBtnPlay.set_visible(not self.stPlaying) self.widLogoO.set_visible(not self.stPlaying) self.widLogoG.set_visible(self.stPlaying) def closeSock(self): """close UDP socket""" self.widStatus.remove_all(0) self.widStatus.push(0, "UDP output ERROR") if self.sock is not None: self.sock.close() self.sock = None def setupSock(self): """create a new UDP socket and "connect" it to the destination address""" self.closeSock() try: self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) self.sock.connect((self.stDestination, 5740)) self.widStatus.remove_all(0) self.widStatus.push(0, "UDP output to \"" + self.stDestination + "\" port 5740") except: self.closeSock() def onDestroy(self, widget): """window will be destroyed""" Gtk.main_quit() def onFileOpen(self, widget): """File Open clicked in menu""" #print("DEBUG sync_gui File Open") # create and run file chooser dialog dialog = Gtk.FileChooserDialog( "BlinkenArea Sync GUI - File Open...", self.widMainWindow, Gtk.FileChooserAction.OPEN, (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)) dialog.set_default_response(Gtk.ResponseType.OK) filt = Gtk.FileFilter() filt.set_name("All files") filt.add_pattern("*") dialog.add_filter(filt) response = dialog.run() if response == Gtk.ResponseType.OK: # dialog closed with OK -> load new playlist filename = dialog.get_filename() self.playlist.read(filename) self.playlist.update(self.widPlaylistStore) self.stEntryIdx = -1 # no entry selected self.stPlaying = False # not playing self.updateEntry() self.updateButtonVisibility() # clean up dialog.destroy() def onFileExit(self, widget): """File Exit clicked in menu""" #print("DEBUG sync_gui File Exit") Gtk.main_quit() def onExtrasDestination(self, widget): """Extras Destination Address clicked in menu""" #print("DEBUG sync_gui Extras Destination") # run input dialog to ask for new destination dialog = self.builder.get_object("DialogDestination") cur = self.builder.get_object("DiaDestCur") new = self.builder.get_object("DiaDestNew") cur.set_text(self.stDestination) new.set_text(self.stDestination) response = dialog.run() if response == 1: self.stDestination = new.get_text() # hide input dialog dialog.hide() # re-create UDP socket self.setupSock() def onPlaylistDblClick(self, widget, row, col): """playlist entry has been double-clicked""" # get index of selected entry idx = -1 sel = self.widPlaylistView.get_selection() if sel is not None: (model, it) = sel.get_selected() if it is not None: (idx,) = model.get(it, 0) #print("DEBUG sync_gui playlist double-click idx=%d" % (idx)) # update playlist entry self.stEntryIdx = idx # set position to zero if playing if self.stPlaying: self.stPosition = 0 # update entry self.updateEntry() def onNewPosition(self, widget, scroll, value): """slider has been moved to a new position""" #print("DEBUG sync_gui new position " + str(value)); # clamp position to valid range if value < 0: value = 0 if value > self.stDuration: value = self.stDuration # update current position - and play start time if playing self.stPosition = value # update position state (do not touch the slider) self.updatePositionState() def onPrevious(self, widget): """previous button as been pressed""" #print("DEBUG sync_gui previous") # go to begin of previous entry (with wrap around) self.stPosition = 0 self.stEntryIdx = self.stEntryIdx - 1 if self.stEntryIdx < 0: self.stEntryIdx = len(self.playlist.entries) - 1 self.updateEntry() def onStop(self, widget): """stop button has been pressed""" #print("DEBUG sync_gui stop") self.stPlaying = False self.stPosition = 0 # stop goes back to begin self.updatePosition() self.updateButtonVisibility() def onPause(self, widget): """pause button has been pressed""" #print("DEBUG sync_gui pause") self.stPlaying = False self.updateButtonVisibility() def onPlay(self, widget): """play button has been pressed""" #print("DEBUG sync_gui play") self.stPlaying = True self.updatePosition() self.updateButtonVisibility() def onNext(self, widget): """next button has been pressed""" #print("DEBUG sync_gui next") # go to begin of next entry (with wrap around) self.stPosition = 0 self.stEntryIdx = self.stEntryIdx + 1 if self.stEntryIdx >= len(self.playlist.entries): self.stEntryIdx = 0 self.updateEntry() def onTimer10ms(self): """timer callback, every 10ms""" # update position if playing if self.stPlaying: self.stPosition = time.time() - self.stPlayStart if self.stPosition >= self.stDuration: # end of entry reached --> go to begin of next entry (with wrap around) self.stPosition = 0 self.stEntryIdx = self.stEntryIdx + 1 if self.stEntryIdx >= len(self.playlist.entries): self.stEntryIdx = 0 self.updateEntry() else: self.updatePosition() # request being called again return True def onTimer100ms(self): """timer callback, every 100ms""" # send sync packet if self.sock is not None: flags = 0 if self.stPlaying: flags = flags | 1 name = self.stName pos_ms = round(self.stPosition * 1000) data = "PoSy" + struct.pack("!I64sI", flags, name, pos_ms) try: self.sock.send(data) except: self.closeSock() # request being called again return True # main application entry point if __name__ == "__main__": app = SyncGui() Gtk.main()