--[[ =head1 NAME applets.FramechannelPl.FramechannelPlApplet - A screensaver displaying a Framechannel RSS content =head1 DESCRIPTION This screensaver displays the images received on the framechannel RSS feed. It uses the duration xml filed coupled with each image to determine how long the image will be displayed on the screen, and it uses the xml "ttl" field to determine the interval at which the feed will be re-read from the network. This app is largely based on the Flickr screen saver implementation. The XML Parsing code is a modified version of Roberto Ierusalimschy skeleton, the original of which can be found in: http://lua-users.org/wiki/LuaXml chosen because it was a lua only implementation and thus less obtrussive, the main modification to his code is the ability to apply a tag with a ":" character in it. This allows us to parse the media:content tags to extract the "url" and the "duration" attributes. The get_ttl and build_media_table functions allows us to extract the ttl value form the raw feed, and create a media group specific table. Currently the "height", and "width" attributes are not used but they can be added if later needed to the build_media_table function. Adapted by: lec 06/23/2009 for Framemedia =head1 FUNCTIONS Applet related methods are described in L. FramechannelPlApplet adds the following methods: =head1 FUNCTIONS =head2 parseargs =head3 arguments string =head3 description converts substring into attribute entry =head2 collect =head3 arguments string representing the whole xml file =head3 description This is the main engine for the parser =head2 build_media_table =head3 arguments xml string =head3 description This function invokes the collect function to comvert xml into a lua table then it parses that table looking for media content and when found adds it to a table. =head2 get_ttl =head3 argumets xml string =head3 description a raw patter extractor for the time to live value =head2 get_unique_id =head3 argumets xml string =head3 description a raw patter extractor for the unique_id value =cut --]] -- stuff we use local pairs, ipairs, tostring = pairs, ipairs, tostring local math = require("math") local table = require("table") local string = require("string") local oo = require("loop.simple") local Applet = require("jive.Applet") local Font = require("jive.ui.Font") local Framework = require("jive.ui.Framework") local Icon = require("jive.ui.Icon") local Label = require("jive.ui.Label") local Group = require("jive.ui.Group") local Popup = require("jive.ui.Popup") local RadioButton = require("jive.ui.RadioButton") local RadioGroup = require("jive.ui.RadioGroup") local Textinput = require("jive.ui.Textinput") local Textarea = require("jive.ui.Textarea") local SimpleMenu = require("jive.ui.SimpleMenu") local Surface = require("jive.ui.Surface") local Window = require("jive.ui.Window") local Timer = require("jive.ui.Timer") local SocketHttp = require("jive.net.SocketHttp") local RequestHttp = require("jive.net.RequestHttp") local json = require("json") local debug = require("jive.utils.debug") local log = require("jive.utils.log").logger("applets.screensavers") local FRAME_RATE = jive.ui.FRAME_RATE local LAYER_FRAME = jive.ui.LAYER_FRAME local LAYER_CONTENT = jive.ui.LAYER_CONTENT local jnt = jnt local appletManager = appletManager module(..., Framework.constants) oo.class(_M, Applet) local urlStem = "http://rss.framechannel.com/productId=SqueezeBox/frameId=" local fmIdGenerator = "http://rss.framechannel.com/device/generateId" local transitionBoxOut local transitionTopDown local transitionBottomUp local transitionLeftRight local transitionRightLeft local framechannelplTitleStyle = 'settingstitle' local fake_slide = false function openScreensaver(self, menuItem) self.slideQueue = {} self.unique_id = "" self.rssURL = "" self.ttl = 1 self.nextSlide = 1 self.transitions = { transitionBoxOut, transitionTopDown, transitionBottomUp, transitionLeftRight, transitionRightLeft, Window.transitionFadeIn } local ok, err = self:obtainUniqueId() -- err won't be set in this call local label = nil if ok then -- if here, we may have obtained a unique id, either from the settings or the website -- format an rss feed request label = Label("label", self:string("SCREENSAVER_FRAMECHANNEL_LOADING_PHOTO")) self.window = self:_window(label) -- needed here to set the timer if self.unique_id then self.rssURL = urlStem .. self.unique_id log:info("rss URL: ", self.rssURL ) ok, err = self:displayNextSlide() if ok then label = Label("label", self:string("SCREENSAVER_FRAMECHANNEL_LOADING_PHOTO")) else label = Label("label", self:string("SCREENSAVER_FRAMECHANNEL_ERROR")) end end -- should this be an else with reschedulling logic? else label = Label("label", self:string("SCREENSAVER_FRAMECHANNEL_ERROR")) end self.window = self:_window(label) self.window:show() end function popupMessage(self, title, msg) local popup = Window("window", title) local text = Textarea("textarea", msg) popup:addWidget(text) popup:addListener(EVENT_SCROLL, function() popup:playSound("WINDOWHIDE") popup:hide() end) self:tieAndShowWindow(popup) end function openSettings(self, menuItem) -- we don't do much here, the only thing that is left configurable is the transition mode -- even though uniqueId is kept on the settings it is not user settable local window = Window("window", menuItem.text, framechannelplTitleStyle) window:addWidget(SimpleMenu("menu", { { text = self:string("SCREENS_TRANSITION"), sound = "WINDOWSHOW", callback = function(event, menuItem) self:defineTransition(menuItem) return EVENT_CONSUME end }, { text = self:string("Advanced Settings"), sound = "WINDOWSHOW", callback = function(event, menuItem) self:advancedSettings(menuItem) return EVENT_CONSUME end }, })) self:tieAndShowWindow(window) return window end function advancedSettings(self, menuItem) local group = RadioGroup() local window = Window("window", menuItem.text,framechannelplTitleStyle) window:addWidget(SimpleMenu("menu", { { text = self:string("Reset ID"), icon = RadioButton( "radio", group, function() self:getSettings()["framechannel.uniqueId"] = "" self:storeSettings() end, local_id == "" ), }, })) self:tieAndShowWindow(window) return window end function defineTransition(self, menuItem) local group = RadioGroup() local trans = self:getSettings()["framechannel.transition"] local window = Window("window", menuItem.text, framechannelplTitleStyle) window:addWidget(SimpleMenu("menu", { { text = self:string("SCREENSAVER_FRAMECHANNEL_TRANSITION_RANDOM"), icon = RadioButton( "radio", group, function() self:setTransition("random") end, trans == "random" ), }, { text = self:string("SCREENSAVER_FRAMECHANNEL_TRANSITION_INSIDE_OUT"), icon = RadioButton( "radio", group, function() self:setTransition("boxout") end, trans == "boxout" ), }, { text = self:string("SCREENSAVER_FRAMECHANNELPL_TRANSITION_TOP_DOWN"), icon = RadioButton( "radio", group, function() self:setTransition("topdown") end, trans == "topdown" ), }, { text = self:string("SCREENSAVER_FRAMECHANNELPL_TRANSITION_BOTTOM_UP"), icon = RadioButton( "radio", group, function() self:setTransition("bottomup") end, display == "bottomup" ), }, { text = self:string("SCREENSAVER_FRAMECHANNELPL_TRANSITION_LEFT_RIGHT"), icon = RadioButton( "radio", group, function() self:setTransition("leftright") end, trans == "leftright" ), }, { text = self:string("SCREENSAVER_FRAMECHANNELPL_TRANSITION_RIGHT_LEFT"), icon = RadioButton( "radio", group, function() self:setTransition("rightleft") end, trans == "rightleft" ), }, })) self:tieAndShowWindow(window) return window end function setTransition(self, trans) self:getSettings()["framechannel.transition"] = trans self:storeSettings() end function obtainUniqueId(self) -- Is there an Id stored within the settings? if not try to obtain one from the website local unique_id = self:getSettings()["framechannel.uniqueId"] self.unique_id = unique_id log:info("unique id from setting is: ", self.unique_id) if( self.unique_id == nil or self.unique_id == "" ) then self:_requestId() end return true end function displayNextSlide(self) -- fill slide queue if it's empty log:info("Slide Queue has: ",#self.slideQueue," Elements") if #self.slideQueue == 0 then return self:_requestSlides() end self.nextSlide = self.nextSlide + 1 if self.nextSlide > #self.slideQueue then self.nextSlide = 1 end -- pick the slide following in the sequence local slide = self.slideQueue[self.nextSlide] local url,duration = self:_getSlideUrl(slide) log:info("slide URL: ", url, " duration : ", duration, " index: ", self.nextSlide ) local a,b,host = string.find(url,"http://(%w.-)/"); log:info("using host: ", host ) port = 80 if fake_slide == true then host = "localhost" url = "http://localhost:8080/webcore/IMG_6169.jpg" port = 8080 end -- request slide local socket = SocketHttp(jnt, host, port, "framechannel") local req = RequestHttp(function(chunk, err) if chunk then local srf = Surface:loadImageData(chunk, #chunk) self:_loadedSlide(self.window, slide, srf, duration) end end, 'GET', url) socket:fetch(req) return true end function _requestId(self) log:info("_requestId Fetching from: ",fmIdGenerator) local socket = SocketHttp(jnt, "rss.framechannel.com", 80, "idRequestSocket") local req = RequestHttp ( function (chunk, err ) self:_handleIdRequest(chunk,err) end, 'GET', fmIdGenerator ) socket:fetch(req) return true end function _handleIdRequest(self, chunk, err ) log:info("_handleIdRequest chunk: ", chunk, " err: ", err ) local ret = false if chunk then local page = chunk self.unique_id = self:get_unique_id(page) if self.unique_id == "" then ret = false else self:getSettings()["framechannel.uniqueId"] = self.unique_id self:storeSettings() end end return ret end function _requestSlides(self) -- Ok this function deals with obtaining the slides contained in the RSS feed -- The chunk will contain all the xml frame which will then be converted by the parsing funtion -- into a slide queue log:info("_requestSlides ") local method, args -- always mate unique id with stem here, to eliminate issue where rssURL -- is set before we know the ID and there always fail -- make sure to re-read it from settings as by this time it should have been already set. self.unique_id = self:getSettings()["framechannel.uniqueId"] self.rssURL = urlStem .. self.unique_id local socket = SocketHttp(jnt, "rss.framechannel.com", 80, "framemediRSSsocket") local req = RequestHttp( function(chunk, err) self:_extractMediaSlides(chunk, err) end, 'GET', self.rssURL ) socket:fetch(req) self:setUpTimeToLive( self.ttl * 60000 ) -- ttl expressed in minutes end function setUpTimeToLive( self, timeout) log:info("Starting ttl timer to kick in in ", timeout, " Ms ") self.ttl_timer = Timer(timeout, function() self:_requestSlides() end, true ) self.ttl_timer:start() end function _window(self, ...) local window = Window("framechannelpl") window:setSkin({ framechannelpl = { layout = Window.noLayout, font = Font:load("fonts/FreeSans.ttf", 10) } }) -- black window background window:setShowFrameworkWidgets(false) for i, v in ipairs{...} do window:addWidget(v) end window:addListener(EVENT_WINDOW_RESIZE, function(evt) local icon = self:_makeIcon() self.window:addWidget(icon) end) -- register window as a screensaver local manager = appletManager:getAppletInstance("ScreenSavers") manager:screensaverWindow(window) return window end function _extractMediaSlides(self, chunk, err) log:info("_extractMediaSlides"); if chunk then self.ttl = self:get_ttl(chunk) log:info("Got a new ttl value", self.ttl ); self.slideQueue = self:build_media_table(chunk) if self.timer == nil then -- should be only on the first run through self:displayNextSlide() end else log:info("Endcall to _extractMediaSlides err: ", err ) end end function _makeIcon(self) -- reposition icon local sw, sh = Framework:getScreenSize() local slide = self.slide local srf = self.slideSrf local w,h = srf:getSize() local zoom = sw/w + .1 if w < h then srf = srf:rotozoom(0, sw / w, 1) else srf = srf:rotozoom(-90, zoom, 1) end log:info("Aspect ratio w/h Screen : ", sw/sh, " img: ", w/h, " rz: ", zoom, " oz: ", sw/h ) w,h = srf:getSize() local x, y = (sw - w) / 2, (sh - h) / 2 -- empty image to draw onto local totImg = Surface:newRGBA(sw, sh) totImg:filledRectangle(0, 0, sw, sh, 0x000000FF) -- draw image srf:blit(totImg, x, y) local icon = Icon("image", totImg) icon:setPosition(0, 0) return icon end -- function _showDetailsAction(self) -- self.window:playSound("WINDOWSHOW") -- self:_detailsShow() -- return EVENT_CONSUME -- end function _loadedSlide(self, lastWindow, slide, srf, duration) log:info("slide loaded") -- don't display the photo if the top window has changed if lastWindow ~= Framework.windowStack[1] then return end self.slide = slide self.slideSrf = srf local icon = self:_makeIcon() self.window = self:_window(icon) -- self.window:addActionListener("go", self, _showDetailsAction) local transition local trans = self:getSettings()["framechannel.transition"] if trans == "random" then transition = self.transitions[math.random(#self.transitions)] elseif trans == "boxout" then transition = transitionBoxOut elseif trans == "topdown" then transition = transitionTopDown elseif trans == "bottomup" then transition = transitionBottomUp elseif trans == "leftright" then transition = transitionLeftRight elseif trans == "rightleft" then transition = transitionRightLeft end self.window:showInstead(transition) -- start timer for next slide in timeout seconds local timeout = duration * 1000 if self.timer then log:info("Clearing Slide timer") self.window:removeTimer(self.timer) self.timer = nil end log:info("Setting Slide timer to trigger in ", timeout, " milliseconds") self.timer = self.window:addTimer(timeout, function() self:displayNextSlide() end) end function _getSlideUrl(self, slide) return slide.url, slide.duration end function transitionBoxOut(oldWindow, newWindow) local frames = FRAME_RATE * 2 -- 2 secs local screenWidth, screenHeight = Framework:getScreenSize() local incX = screenWidth / frames / 2 local incY = screenHeight / frames / 2 local x = screenWidth / 2 local y = screenHeight / 2 local i = 0 return function(widget, surface) local adjX = i * incX local adjY = i * incY newWindow:draw(surface, LAYER_FRAME) oldWindow:draw(surface, LAYER_CONTENT) surface:setClip(x - adjX, y - adjY, adjX * 2, adjY * 2) newWindow:draw(surface, LAYER_CONTENT) i = i + 1 if i == frames then Framework:_killTransition() end end end function transitionBottomUp(oldWindow, newWindow) local frames = FRAME_RATE * 2 -- 2 secs local screenWidth, screenHeight = Framework:getScreenSize() local incY = screenHeight / frames local i = 0 return function(widget, surface) local adjY = i * incY newWindow:draw(surface, LAYER_FRAME) oldWindow:draw(surface, LAYER_CONTENT) surface:setClip(0, screenHeight-adjY, screenWidth, screenHeight) newWindow:draw(surface, LAYER_CONTENT) i = i + 1 if i == frames then Framework:_killTransition() end end end function transitionTopDown(oldWindow, newWindow) local frames = FRAME_RATE * 2 -- 2 secs local screenWidth, screenHeight = Framework:getScreenSize() local incY = screenHeight / frames local i = 0 return function(widget, surface) local adjY = i * incY newWindow:draw(surface, LAYER_FRAME) oldWindow:draw(surface, LAYER_CONTENT) surface:setClip(0, 0, screenWidth, adjY) newWindow:draw(surface, LAYER_CONTENT) i = i + 1 if i == frames then Framework:_killTransition() end end end function transitionLeftRight(oldWindow, newWindow) local frames = FRAME_RATE * 2 -- 2 secs local screenWidth, screenHeight = Framework:getScreenSize() local incX = screenWidth / frames local i = 0 return function(widget, surface) local adjX = i * incX newWindow:draw(surface, LAYER_FRAME) oldWindow:draw(surface, LAYER_CONTENT) surface:setClip(0, 0, adjX, screenHeight) newWindow:draw(surface, LAYER_CONTENT) i = i + 1 if i == frames then Framework:_killTransition() end end end function transitionRightLeft(oldWindow, newWindow) local frames = FRAME_RATE * 2 -- 2 secs local screenWidth, screenHeight = Framework:getScreenSize() local incX = screenWidth / frames local i = 0 return function(widget, surface) local adjX = i * incX newWindow:draw(surface, LAYER_FRAME) oldWindow:draw(surface, LAYER_CONTENT) surface:setClip(screenWidth-adjX, 0, screenWidth, screenHeight) newWindow:draw(surface, LAYER_CONTENT) i = i + 1 if i == frames then Framework:_killTransition() end end end function parseargs(s) local arg = {} string.gsub(s, "(%w+)=([\"'])(.-)%2", function (w, _, a) arg[w] = a end) return arg end function collect(s) local stack = {} local top = {} table.insert(stack, top) local ni,c,label,xarg, empty local i, j = 1, 1 while true do ni,j,c,label,xarg, empty = string.find(s, "<(%/?)(%a+%:%a+)(.-)(%/?)>", i) if not ni then break end local text = string.sub(s, i, ni-1) if not string.find(text, "^%s*$") then table.insert(top, text) end if empty == "/" then -- empty element tag table.insert(top, {label=label, xarg=parseargs(xarg), empty=1}) elseif c == "" then -- start tag top = {label=label, xarg=parseargs(xarg)} table.insert(stack, top) -- new level else -- end tag local toclose = table.remove(stack) -- remove top top = stack[#stack] if #stack < 1 then error("nothing to close with "..label) end if toclose.label ~= label then error("trying to close "..toclose.label.." with "..label) end table.insert(top, toclose) end i = j+1 end local text = string.sub(s, i) if not string.find(text, "^%s*$") then table.insert(stack[#stack], text) end if #stack > 1 then error("unclosed "..stack[stack.n].label) end return stack[1] end --[[ This is just a raw pattern extractor no need to spend too many cycles parsing since there is only one ttl per frame --]] function get_ttl(self, s) local a,b,ttl = string.find(s,"%s?(%d+)<%s?/%s?ttl%s?>") return ttl end -- another raw pattern extractor the id field is also only one per xml file function get_unique_id(self,s) local a,b,id = string.find(s,"%s?(%x+%.?%x+)<%s?/%s?frameId%s?>") log:info("I got it! value is: ", id ) return id end function build_media_table(self, s ) local x = collect(s) local mt = {} for i = 1, #x do if x[i].label == "media:content" then if x[i].xarg ~= nil then table.insert(mt,{url= x[i].xarg.url, duration=x[i].xarg.duration}) end end end return mt end --[[ =head1 LICENSE Copyright 2007 Logitech. All Rights Reserved. This file is subject to the Logitech Public Source License Version 1.0. Please see the LICENCE file for details. =cut --]]