# # AUTHOR: # KesieV # # WHAT IT DOES: # Use mpg123 as backend. Is quite complex, as custom backend, because mpg123 # seems to be tricky to be remote controlled and pls support is coded. # You can reuse some pieces of code for your frontend but I suggest you to # start copying the default Mplayer object, which is simplier and cleaner. # Obviuosly can be MORE complex (mp3 streaming from STDIN) since the plugin # system is raw but flexible. # # New player backends are implemented overlapping the Mplayer objects method class Mplayer # NOTE: put @@LASTFM=nil to disable LastFM features. LastFM stations are plain # MP3 streams and, since mpg123 supports them, LastFM works out of the box. # mpg123 features only the mp3 format, so will be indexed only mp3s alias mpg123_initialize initialize def initialize(lastfmuser="",lastfmpassword="") mpg123_initialize(lastfmuser,lastfmpassword) @features={:formats=>[".mp3"]} end # I/O of mpg123 is quite a mess. Have to be manually synced each # line. Implementig a "tell" function, which syncs pipe with # mpg123 each call. Seems working. def tell(string) # Since we're using the "sequence" structure, is safe to check the pipe # before sending commands - if user hits stop when skipping from a # sequence item to another begin @pipe.flush @pipe.puts(string) @pipe.flush rescue =>error; end end def play(file,metadata={},indexonly=false) # Player object startup, LastFM login etc. startup(file,metadata,indexonly) # mpg123 do not supports pls playlists when in interactive mode. We attemp # to parse it for him, so some Shoutcasts should work. You can reuse this # structure for any tiny media player which do not support playlists. # The trick is to reset the sequence array with the played filename and # substitute with the array of items you want to play when needed. # Then, into the player's thread, we will call the player multiple times # and quit the sequence fetching when user stops the playback by hand. sequence=[file]; if (file[-3..-1]=="pls") then sequence=[] open(file,"r") { |f| f.each { |line| if line[0..3]=="File" then sequence << line[line.index("=")+1..-1] end } } end @thread=Thread.new { # Usually is needed to execute this just once per file. We're trying to # simulate a "single item playlist" so pls are fetched until one of them # works. sequence.each { |file| # Opening a pipe to mpg123 in interactive mode @pipe=IO.popen("mpg123 -R",'r+') # As I said, we have to sync manually with mpg123. Disabling # automatic sync. @pipe.sync=false # With mpg123, sizes of the media are served once so we will store them # and served each time. sz=[-1,-1] @pipe.each { |line| # Waiting for mpg123 to be ready. if line[0..1]=="@R" then # If is ready, it starts playback tell((indexonly ? "LP" : "L")+" "+(@meta[:file][0..5]=="lastfm" && @lastfmdata[:session] && @meta[:title]!=$opt[:unknown] ? @lastfmdata[:url] : file) ); # Playback starts here setmeta(nil,[:play,nil]) end # call this sometime (i.e. each stdout line) for updating LastFM meta # and doing other stuff. backgroundupdate # Update meta. Seems that mpg123 ID3 doesn't tell the track number :( # ID3 tags are formatted using string chunks 30 characters long. if line[0..6]=="@I ID3:" then [:title,:artist,:album].each_with_index { |id,i| setmeta(id,line[7+(30*i)..36+(30*i)].rstrip) if line[7+(30*i)..36+(30*i)].rstrip.length>0 } end # ID3 Track number ships alone... if line[0..12]=="@I ID3.track:" then setmeta(:trackno,line[13..-1].rstrip) end # Since latest versions of mpg123 supports ID3v2 tags, we will also use # them. Track number is supported :) if line[0..8]=="@I ID3v2." then [ { :label=>"title:", :id=>:title }, { :label=>"artist:", :id=>:artist }, { :label=>"album:", :id=>:album }, { :label=>"track:", :id=>:trackno }, { :label=>"year:", :id=>:year } ].each { |i| if line[9..8+i[:label].length]==i[:label] && line[9+i[:label].length..-1].rstrip.length>0 then setmeta(i[:id],line[9+i[:label].length..-1].rstrip) end } end # Icecast tags if line[0..11]=="@I ICY-META:" then title=line[/StreamTitle='([^']*)'/,1] if title.to_s.length>0 then setmeta(:title,title) end end if indexonly then # if this object was called for indexing music, quit ASAP if line[0..1]=="@P" then tell("quit") end else # else update player status # If playback ended, quits the mpg123 player (is not automatic) if line[0..3]=="@P 0" then tell("quit") end # Updates playback position if line[0..1]=="@F" then data=line.split(" "); if sz[0]==-1 then sz[0]=data[4] sz[1]=data[2].to_f end @runtime.call(data[3],sz[0],data[1].to_f,sz[1]) end end } # if playback was ended by user, stops the sequence fetching. Else # the player quitted for end of stream (i.e. stream error) or end # of single MP3 (but should be alone into the sequence array) if @state[0]==:stop && @state[1]==:byhand then break end } # If thread exited, puts the player in stop state. (end of song) setmeta(nil,[:stop,@state[1]]) } end # This is called by the GUI for changing the playback status. def control(action,attr) setmeta(nil,[:stop,attr]) if action == :stop setmeta(nil,[(@state[0] == :play ? :pause : :play),attr]) if action==:pause && @state[0]!=:stop #fullscreen is not supported tell({:stop=>"quit",:pause=>"pause",:forward=>"J +10seconds",:rewind=>"J -10seconds"}[action]) if @thread && @thread.alive? @thread.join if @state[0] == :stop && @thread && @thread.alive? end end