Changeset - 8d87a3528369
[Not reviewed]
default
0 4 1
drewp@bigasterisk.com - 13 years ago 2012-06-18 01:13:18
drewp@bigasterisk.com
go button support
Ignore-this: e32b66cb39bf05a2fec5c91fdb7a7093
5 files changed with 48 insertions and 27 deletions:
0 comments (0 inline, 0 general)
bin/ascoltami2
Show inline comments
 
#!bin/python
 
import web, thread, gobject, sys, optparse, logging
 
from rdflib import URIRef
 
sys.path.append(".")
 
from light9.ascoltami.player import Player
 
from light9.ascoltami.playlist import Playlist, NoSuchSong
 
from light9.ascoltami.webapp import makeWebApp
 
from light9.ascoltami.webapp import makeWebApp, songUri, songLocation
 
from light9 import networking, showconfig
 

	
 

	
 
import BaseHTTPServer
 
BaseHTTPServer.BaseHTTPRequestHandler.log_message = \
 
                     lambda self, format, *args: None
 

	
 
class App:
 
    def __init__(self, graph, show):
 
        self.graph = graph
 
        self.player = Player(onEOS=self.onEOS)
 
        self.show = show
 
        self.playlist = Playlist.fromShow(graph, show)
 

	
 
    def run(self, musicPort):
 
        # the cherrypy server would wedge when vidref pounds on it; this
 
        # one seems to run
 
        thread.start_new(web.httpserver.runbasic,
 
                         (makeWebApp(self).wsgifunc(),
 
                          ('0.0.0.0', musicPort)))
 

	
 
        mainloop = gobject.MainLoop()
 
        mainloop.run()
 

	
 
    def onEOS(self, song):
 
        self.player.pause()
 
        self.player.seek(0)
 

	
 
        # stop here for now- no go-button behavior
 
        return
 
        thisSongUri = songUri(graph, URIRef(song))
 

	
 
        try:
 
            nextSong = self.playlist.nextSong(song)
 
            nextSong = self.playlist.nextSong(thisSongUri)
 
        except NoSuchSong: # we're at the end of the playlist
 
            return
 

	
 
        self.player.setSong(nextSong, play=False)
 
        self.player.setSong(songLocation(graph, nextSong), play=False)
 

	
 
if __name__ == "__main__":
 
    logging.basicConfig()
 
    log = logging.getLogger()
 
    gobject.threads_init()
 

	
 
    parser = optparse.OptionParser()
 
    parser.add_option('--show',
 
        help='show URI, like http://light9.bigasterisk.com/show/dance2008', default=showconfig.showUri())
 
    parser.add_option("-v", "--verbose", action="store_true",
 
                      help="logging.DEBUG")
 
    (options, args) = parser.parse_args()
 

	
 
    log.setLevel(logging.DEBUG if options.verbose else logging.INFO)
 

	
 
    if not options.show:
 
        raise ValueError("missing --show http://...")
 
            
 
    graph = showconfig.getGraph()
 
    app = App(graph, URIRef(options.show))
 
    app.run(networking.musicPlayer.port)
bin/gobutton
Show inline comments
 
new file 100644
 
#!/bin/sh
 
# uri should be set from $LIGHT9_SHOW/config.n3
 
exec curl --silent -d '' http://localhost:8040/go
light9/ascoltami/index.html
Show inline comments
 
@@ -98,65 +98,65 @@
 
    });
 

	
 
    var tojs = JSON.stringify;
 

	
 
    $(document).keypress(function (ev) {
 

	
 
	if (ev.which == 115) { $("#cmd-stop").click(); return false; }
 
	if (ev.which == 112) { $("#cmd-play").click(); return false; }
 
	if (ev.which == 105) { $("#cmd-intro").click(); return false; }
 
	if (ev.which == 116) { $("#cmd-post").click(); return false; }
 

	
 
	if (ev.which == 32) { $("#cmd-go").click(); return false; }
 
	return true;
 
    });
 

	
 
    $("#cmd-stop").click(function () { $.post("time", tojs({pause: true})); });
 
    $("#cmd-play").click(function () { $.post("time", tojs({resume: true})); });
 
    $("#cmd-intro").click(function () { 
 
	$.post("time", tojs({t: times.intro, resume: true}))
 
    });
 
    $("#cmd-post").click(function () { 
 
	$.post("time", tojs({t: currentDuration - times.post, resume: true}))
 
    });
 
    $("#cmd-go").click(function () {
 
	// todo
 
	$.post("go");
 
    });
 
    $("#cmd-out0").click(function () { $.post("output", tojs({sink: "0"})); })
 
    $("#cmd-out1").click(function () { $.post("output", tojs({sink: "1"})); })
 

	
 
    var pendingSlide = false;
 
    $("#timeSlider").slider({
 
	step: .01,
 
	slide: function (event, ui) {
 
	    if (pendingSlide) {
 
		return;
 
	    }
 
	    pendingSlide = true;
 
	    $.post("time", '{"t" : '+ui.value+'}', 
 
		   function (data, status, xhr) {
 
		       pendingSlide = false;
 
		   });
 
	},
 
    });
 
    
 
    var raf = window.requestAnimationFrame ||
 
	window.mozRequestAnimationFrame || 
 
	window.webkitRequestAnimationFrame;
 

	
 
    function updateLoop() {
 
	var whenDone = function () {
 
	    setTimeout(function () { 
 
		raf(updateLoop);
 
	    }, 50);
 
	};
 
	updateCurrent(whenDone);
 
    }
 
    updateLoop();
 

	
 
});
 
// ]]>
 
</script>
 

	
 

	
 
  </body>
 
</html>
 
\ No newline at end of file
 
</html>
light9/ascoltami/player.py
Show inline comments
 
#!/usr/bin/python
 

	
 
"""
 
alternate to the mpd music player, for ascoltami
 
"""
 
from __future__ import division
 
import time, logging, traceback
 
import gst, gobject
 
log = logging.getLogger()
 

	
 
class Player(object):
 
    def __init__(self, autoStopOffset=4, onEOS=None):
 
        """autoStopOffset is the number of seconds before the end of
 
        song before automatically stopping (which is really pausing).
 
        onEOS is an optional function to be called when we reach the
 
        end of a stream (for example, can be used to advance the song).
 
        It is called with one argument which is the URI of the song that
 
        just finished."""
 
        self.autoStopOffset = autoStopOffset
 
        self.playbin = self.pipeline = gst.parse_launch("playbin2")
 
        self.playStartTime = 0
 
        self.lastWatchTime = 0
 
        self.autoStopTime = 0
 
        self.lastSetSongUri = None
 
        self.onEOS = onEOS
 
        
 
        # before playbin2:
 
        #self.pipeline = gst.parse_launch("filesrc name=file location=%s ! wavparse name=src ! audioconvert ! alsasink name=out" % songFile)
 

	
 
        gobject.timeout_add(50, self.watchTime)
 

	
 
        bus = self.pipeline.get_bus()
 
        bus.add_signal_watch()
 

	
 
        def on_any(bus, msg):
 
            #print bus, msg, msg.type
 
            if msg.type == gst.MESSAGE_EOS:
 
                if self.onEOS is not None:
 
                    self.onEOS(self.getSong())
 
        bus.connect('message', on_any)
 

	
 
        def onStreamStatus(bus, message):
 
            print "streamstatus", bus, message
 
            (statusType, _elem) = message.parse_stream_status()
 
            if statusType == gst.STREAM_STATUS_TYPE_ENTER:
 
                self.setupAutostop()
 
        bus.connect('message::stream-status', onStreamStatus)
 

	
 
    def watchTime(self):
 
        try:
 
            try:
 
                t = self.currentTime()
 
            except gst.QueryError:
 
                return True
 
            log.debug("watch %s < %s < %s",
 
                      self.lastWatchTime, self.autoStopTime, t)
 
            if self.lastWatchTime < self.autoStopTime < t:
 
                log.info("autostop")
 
                self.pause()
 

	
 
                # new EOS logic above should be better
 
            ## if not self.onEOS:
 
            ##     if self.isPlaying() and t >= self.duration() - .2:
 
            ##         # i don't expect to hit dur exactly with this
 
            ##         # polling. What would be better would be to watch for
 
            ##         # the EOS signal and react to that
 
            ##         self.onEOS(self.getSong())
 

	
 
            self.lastWatchTime = t
 
        except:
 
            traceback.print_exc()
 
        return True
 

	
 
    def seek(self, t):
 
        assert self.playbin.seek_simple(
 
            gst.FORMAT_TIME,
 
            gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_ACCURATE | gst.SEEK_FLAG_SKIP,
 
            t * gst.SECOND)
 
        self.playStartTime = time.time()
 

	
 
    def setSong(self, songLoc, play=True):
 
        """
 
        uri like file:///my/proj/light9/show/dance2010/music/07.wav
 
        """
 
        log.info("set song to %r" % songLoc)
 
        self.pipeline.set_state(gst.STATE_READY)
 
        self.preload(songLoc)
 
        self.pipeline.set_property("uri", songLoc)
 
        self.lastSetSongUri = songLoc
 
        # todo: don't have any error report yet if the uri can't be read
 
        if play:
 
            self.pipeline.set_state(gst.STATE_PLAYING)
 
            self.playStartTime = time.time()
 

	
 
    def getSong(self):
 
        """Returns the URI of the current song."""
 
        return self.playbin.get_property("uri")
 
        # even the 'uri' that I just set isn't readable yet
 
        return self.playbin.get_property("uri") or self.lastSetSongUri
 

	
 
    def preload(self, songPath):
 
        """
 
        to avoid disk seek stutters, which happened sometimes (in 2007) with the
 
        non-gst version of this program, we read the whole file to get
 
        more OS caching.
 

	
 
        i don't care that it's blocking.
 
        """
 
        log.info("preloading %s", songPath)
 
        assert songPath.startswith("file://"), songPath
 
        open(songPath[len("file://"):]).read()
 

	
 
    def currentTime(self):
 
        try:
 
            cur, _format = self.playbin.query_position(gst.FORMAT_TIME)
 
        except gst.QueryError:
 
            return 0
 
        return cur / gst.SECOND
 

	
 
    def duration(self):
 
        try:
 
            return self.playbin.query_duration(gst.FORMAT_TIME)[0] / gst.SECOND
 
        except gst.QueryError:
 
            return 0
 

	
 
    def states(self):
 
        """json-friendly object describing the interesting states of
 
        the player nodes"""
 
        success, state, pending = self.playbin.get_state(timeout=0)
 
        return {"current": {"name":state.value_nick},
 
                "pending": {"name":state.value_nick}}
 
        
 
    def pause(self):
 
        self.pipeline.set_state(gst.STATE_PAUSED)
 

	
 
    def isAutostopped(self):
 
        """
 
        are we stopped at the autostop time?
 
        """
 
        pos = self.currentTime()
 
        autoStop = self.duration() - self.autoStopOffset
 
        return not self.isPlaying() and abs(pos - autoStop) < 1 # i've seen .4 difference here
 

	
 
    def resume(self):
 
        self.pipeline.set_state(gst.STATE_PLAYING)
 
        pos = self.currentTime()
 
        autoStop = self.duration() - self.autoStopOffset
 
        if abs(pos - autoStop) < .01:
 
            self.releaseAutostop()
 

	
 
    def setupAutostop(self):
 
        dur = self.duration()
 
        self.autoStopTime = (dur - self.autoStopOffset)
 
        log.info("autostop will be at %s", self.autoStopTime)
 
        # pipeline.seek can take a stop time, but using that wasn't
 
        # working out well. I'd get pauses at other times that were
 
        # hard to remove.
 

	
 
    def isPlaying(self):
 
        _, state, _ = self.pipeline.get_state()
 
        return state == gst.STATE_PLAYING
light9/ascoltami/webapp.py
Show inline comments
 
@@ -3,61 +3,64 @@ from twisted.python.util import sibpath
 
from light9.namespaces import L9
 
from light9.showconfig import getSongsFromShow, songOnDisk
 
from rdflib import URIRef
 
from web.contrib.template import render_genshi
 
render = render_genshi([sibpath(__file__, ".")])
 
app = None
 

	
 

	
 
_songUris = {} # locationUri : song
 
def songLocation(graph, songUri):
 
    loc = URIRef("file://%s" % songOnDisk(songUri))
 
    _songUris[loc] = songUri
 
    return loc
 
    
 
def songUri(graph, locationUri):
 
    return _songUris[locationUri]
 

	
 
class root(object):
 
    def GET(self):
 
        web.header("Content-type", "application/xhtml+xml")
 
        # todo: use a template; embed the show name and the intro/post
 
        # times into the page
 
        return render.index(host=socket.gethostname())
 

	
 
def playerSongUri(graph, player):
 
    """or None"""
 
    
 
    playingLocation = player.getSong()
 
    if playingLocation:
 
        return songUri(graph, URIRef(playingLocation))
 
    else:
 
        return None
 

	
 
class timeResource(object):
 
    def GET(self):
 
        player = app.player
 
        graph = app.graph
 

	
 
        playingLocation = player.getSong()
 
        if playingLocation:
 
            song = songUri(graph, URIRef(playingLocation))
 
        else:
 
            song = None
 
        web.header("content-type", "application/json")
 
        return json.dumps({
 
            "song" : song,
 
            "song" : playerSongUri(graph, player),
 
            "started" : player.playStartTime,
 
            "duration" : player.duration(),
 
            "playing" : player.isPlaying(),
 
            "t" : player.currentTime(),
 
            "state" : player.states(),
 
            })
 

	
 
    def POST(self):
 
        """
 
        post a json object with {pause: true} or {resume: true} if you
 
        want those actions. Use {t: <seconds>} to seek, optionally
 
        with a pause/resume command too.
 
        """
 
        params = json.loads(web.data())
 
        player = app.player
 
        if params.get('pause', False):
 
            player.pause()
 
        if params.get('resume', False):
 
            player.resume()
 
        if 't' in params:
 
            player.seek(params['t'])
 
        web.header("content-type", "text/plain")
 
        return "ok"
 

	
 
@@ -77,37 +80,53 @@ class songResource(object):
 
    def POST(self):
 
        """post a uri of song to switch to (and start playing)"""
 
        graph = app.graph
 

	
 
        app.player.setSong(songLocation(graph, URIRef(web.data())))
 
        web.header("content-type", "text/plain")
 
        return "ok"
 
    
 
class seekPlayOrPause(object):
 
    def POST(self):
 
        player = app.player
 

	
 
        data = json.loads(web.data())
 
        if player.isPlaying():
 
            player.pause()
 
        else:
 
            player.seek(data['t'])
 
            player.resume()
 

	
 
class output(object):
 
    def POST(self):
 
        d = json.loads(web.data())
 
        subprocess.check_call(["bin/movesinks", str(d['sink'])])
 

	
 
class goButton(object):
 
    def POST(self):
 
        """
 
        if music is playing, this silently does nothing.
 
        """
 
        graph, player = app.graph, app.player
 

	
 
        if player.isPlaying():
 
            pass
 
        else:
 
            player.resume()
 
            
 
        web.header("content-type", "text/plain")
 
        return "ok"
 

	
 
def makeWebApp(theApp):
 
    global app
 
    app = theApp
 

	
 
    urls = (r"/", "root",
 
            r"/time", "timeResource",
 
            r"/song", "songResource",
 
            r"/songs", "songs",
 
            r"/seekPlayOrPause", "seekPlayOrPause",
 
            r"/output", "output",
 
            r"/go", "goButton",
 
            )
 

	
 
    return web.application(urls, globals(), autoreload=False)
0 comments (0 inline, 0 general)