-- -- VDR Streamdev Client -- Version 0.2.0 -- -- A script which turns mpv into a client for VDR with the Streamdev-Plugin -- -- Features: -- * runs on Windows, Linux and Mac Os. (needs bash and netcat installed) -- * easy channel switching a la vdr (no channel groups for now) -- * show current and next epg event if available -- * watch recordings -- -- -- Short instructions: -- 1. Enable the streamdev-server-plugin in vdr -- 2. Modify streamdevhosts.conf to contain the clients IP -- 3. If you want to have channel names and epg info, -- modify svdrphosts.conf to contain the clients IP. -- Also netcat ('nc') and bash needs to be installed and in the path. -- 4. Place this file in one of mpvs script folders -- ( ~/.config/mpv/scripts/) or call mpv with the --script -- command line option. -- For now --script vdr-streamdev-client.lua is prefered. -- 5. start mpv -- mpv vdrstream://[vdr-host][:streamdev-port] [--script vdr-streamdev-client.lua] -- -- When mpv is running in Streamdev client mode, you can use -- the keys UP,DOWN, 0-9 to select channels. -- ENTER will bring up the channel info display. -- The key 'm' will show the menu. -- -- -- Copyright 2017 Martin Wache -- VDR Streamdev Client is free software; you can redistribute it and/or -- modify it under the terms of the GNU General Public License as published -- by the Free Software Foundation Version 2 of the License. -- You can obtain a copy of the License from -- https://www.gnu.org/licenses/gpl2.html -- local options = { host="192.168.55.4", svdrp_port="6419", streamdev_port="3000", previous_channel_time=10, epg_update_time=300, } require 'mp.options' read_options(options,'vdr-streamdev-client') local assdraw = require "mp.assdraw" local channels = { } local startup = 1 local vdruri local utils = require 'mp.utils' local channel_idx=1 local next_channel=0 local last_channel=1 local epgnow = {} local epgnext = {} local epg_timer -- refreshes the epg info regulary local vw=495 local vh=275 -- ************************* state machine stuff ********************** local state={} local state_livetv local state_playback local state_channel_info local state_show_epg function update_osd() local cstate=curr_state() mp.log("info","update_osd "..cstate.name) if (cstate.update_osd) then cstate:update_osd() else clear_osd() end end function state_update_timeout() local cstate=curr_state() if cstate.timeout then if cstate.timer == nil then cstate.timer=mp.add_timeout(cstate.timeout,state_back) else cstate.timer:kill() cstate.timer:resume() end end if cstate.update_osd_timeout then if cstate.osd_timer == nil then cstate.osd_timer=mp.add_periodic_timer(cstate.update_osd_timeout, update_osd) else cstate.osd_timer:kill() cstate.osd_timer:resume() end end end function state_remove_timeouts() local cstate=curr_state() if cstate.timer then cstate.timer:kill() end if cstate.osd_timer then cstate.osd_timer:kill() end end function new_state(nstate) table.insert(state,nstate) mp.log("info","state_new "..nstate.name) state_update_timeout() update_osd() end function state_remove_including(name) local cstate=curr_state() while #state>1 and name ~= cstate.name do state_back() cstate=curr_state() end -- remove the state the name state_back() end function state_back_to(name) local cstate=curr_state() while #state>1 and name ~= cstate.name do state_back() cstate=curr_state() end end function state_back() local cstate=curr_state() state_remove_timeouts() if #state>1 then table.remove(state) end cstate=curr_state() mp.log("info","state_back, new "..cstate.name) update_osd() end function curr_state() return state[#state] end -- *********************** OSD stuff ******************************* function ass_color(ass,bgr) ass:append("{\\1c&H"..bgr.."&}") end function ass_bdcolor(ass,bgr) ass:append("{\\3c&H"..bgr.."&}") end function ass_alpha(ass,alpha) ass:append("{\\1a&H"..alpha.."}") end function ass_bdalpha(ass,alpha) ass:append("{\\3a&H"..alpha.."}") end function ass_scale_font(ass,scale) ass:append("{\\fscx"..scale.."\\fscy"..scale.."}") end function ass_clip(ass,x1,y1,x2,y2) ass:append("{\\clip("..x1..","..y1..","..x2..","..y2.."}") end local function print_time(t) if (t == nil) then return " " end return os.date('%H:%M',t) end local function format_epg(epg_info) if epg_info == nil then return "" end local msg=print_time(epg_info['start']) if (epg_info['title'] ~= nil) then msg = msg.." "..epg_info['title'] end return msg end local function print_duration(t) local h=math.floor(t/3600) local m=math.floor(t%3600/60) local s=t%60 return string.format("%02d:%02d:%02d",h,m,s) end function curr_channel_id() local cinfo = channels[channel_idx] return cinfo and cinfo['id'] or nil end function draw_progressbar(ass,left,top, width, height, part) ass:new_event() ass:pos(left, top) ass_color(ass,"007700") ass_alpha(ass,"10") ass_bdcolor(ass,"007700") ass_bdalpha(ass,"10") ass:draw_start() ass:rect_ccw(0,0,width,height) ass:draw_stop() ass:new_event() ass:pos(left, top) ass_color(ass,"000077") ass_alpha(ass,"10") ass_bdcolor(ass,"000077") ass_bdalpha(ass,"10") ass:draw_start() ass:rect_ccw(0,0,part*width,height) ass:draw_stop() ass:new_event() end function show_playback_info(self) local osd_w=480 local osd_h=80 local top=200 local left=10 local time_pos = mp.get_property_native("time-pos") local max_time = mp.get_property_native("duration") local ass = assdraw.ass_new() -- channel info box ass:new_event() ass:pos(left, top) ass_color(ass,"000000") ass_alpha(ass,"70") ass:draw_start() ass:rect_ccw(0,0,osd_w,osd_h) ass:draw_stop() -- print current playback time ass:new_event() ass:pos(left, top) ass:append(print_duration(time_pos)) -- print playback length ass:new_event() ass:pos(osd_w-80, top) ass:append(print_duration(max_time)) -- print recording name ass:new_event() ass:pos(left+80, top) ass:append(self.rinfo['name']) draw_progressbar(ass,left+10,top+30,osd_w-left-10,20,time_pos/max_time) mp.set_osd_ass(0, 0, ass.text) end function show_channel_info(self) local osd_w=480 local osd_h=80 local top=200 local left=10 local cinfo=channels[channel_idx] local ass = assdraw.ass_new() -- channel info box ass:pos(left, top) ass_color(ass,"000000") ass_alpha(ass,"70") ass:draw_start() ass:rect_ccw(0,0,osd_w,osd_h) ass:draw_stop() -- info time box ass:new_event() ass:pos(left, top) ass_color(ass,"000090") ass:draw_start() ass:rect_ccw(0,0,50,23) ass:draw_stop() -- print time ass:new_event() ass:pos(left, top) ass:append(os.date("%H:%M")) -- channel name ass:new_event() ass:pos(left+70, top) ass:append(channel_idx) if cinfo then ass:append(" "..cinfo['name']) end ass:new_event() local cid = cinfo and cinfo['id'] or nil local einfo=epgnow[cid] if einfo then -- epg progress bar local dwidth=200 local dheight=5 if einfo['start'] ~= nil and einfo['duration'] ~= nil then local part = (os.time()-tonumber(einfo['start']))/ tonumber(einfo['duration']) draw_progressbar(ass,left, top+24,dwidth,dheight,part) end -- epg now info ass:pos(left, top+35) ass_scale_font(ass,80) ass:append(format_epg(einfo)) ass:new_event() end einfo = epgnext[cid] if einfo ~= nil then -- epg next info ass:pos(left, top+55) ass_scale_font(ass,80) ass:append(format_epg(einfo)) ass:new_event() end mp.set_osd_ass(0, 0, ass.text) end local osd_w=480 local osd_h=260 local top=20 local left=10 function create_menu_base(options) local ass = assdraw.ass_new() -- menu box ass:pos(left, top) ass_color(ass,"000000") ass_alpha(ass,"70") ass:draw_start() ass:rect_ccw(0,0,osd_w,osd_h) ass:draw_stop() ass:new_event() -- header box ass:pos(left, top) ass_color(ass,"000090") ass_alpha(ass,"70") ass:draw_start() ass:rect_ccw(0,0,osd_w,20) ass:draw_stop() ass:new_event() -- header ass:pos(left, top) ass:append(os.date("%H:%M")) if options and options.name then ass:append(" "..options.name) end ass:new_event() -- footer ass:pos(left,top+osd_h-20) ass_color(ass,"0000F0") ass_alpha(ass,"70") ass:draw_start() ass:rect_ccw(0,0,osd_w/4,20) ass:draw_stop() ass:new_event() if options and options.red then ass:pos(left,top+osd_h-20) ass:append(options.red) ass:new_event() end ass:pos(left+osd_w/4,top+osd_h-20) ass_color(ass,"00F000") ass_alpha(ass,"70") ass:draw_start() ass:rect_ccw(0,0,osd_w/4,20) ass:draw_stop() ass:new_event() if options and options.green then ass:pos(left+osd_w/4,top+osd_h-20) ass:append(options.green) ass:new_event() end ass:pos(left+osd_w/4*2,top+osd_h-20) ass_color(ass,"00F0F0") ass_alpha(ass,"70") ass:draw_start() ass:rect_ccw(0,0,osd_w/4,20) ass:draw_stop() ass:new_event() if options and options.yellow then ass:pos(left,top+osd_h-20) ass:append(options.yellow) ass:new_event() end ass:pos(left+osd_w/4*3,top+osd_h-20) ass_color(ass,"F00000") ass_alpha(ass,"70") ass:draw_start() ass:rect_ccw(0,0,osd_w/4,20) ass:draw_stop() ass:new_event() if options and options.blue then ass:pos(left+osd_w/4*3,top+osd_h-20) ass:append(options.blue) ass:new_event() end return ass end function draw_scrollbar(ass,left,top,width,height,part) ass:pos(left,top) ass_color(ass,"000090") ass_alpha(ass,"70") ass:draw_start() ass:rect_ccw(0,part*height,width,part*height+10) ass:draw_stop() ass:new_event() end local max_items=10 local item_w=osd_w - 10 local item_h=20 function show_menu(self) local ass = create_menu_base{red=self.red_name,green=self.green_name, yellow=self.yellow_name,blue=self.blue_name} if self.selected_item == nil then self.selected_item = 1 end if self.start_pos == nil then self.start_pos = 1 end if self.selected_item>self.start_pos+max_items then self.start_pos=self.selected_item - max_items end if self.selected_itemself.start_pos+max_items and self.start_pos+max_items or #self.items for i = self.start_pos,maxi do v = self.items[i] local itop= top+(i-self.start_pos+1)*item_h if self.selected_item == i then ass:pos(left, itop) ass_color(ass,"000090") ass_alpha(ass,"70") ass:draw_start() ass:rect_ccw(0,0,item_w,item_h) ass:draw_stop() ass:new_event() end if v.draw == nil then ass:pos(left, itop) ass:append(v.text) else v:draw(ass,left,itop,item_w,item_h) end ass:new_event() end if #self.items>max_items then draw_scrollbar(ass,left+osd_w-10,top+20,10,osd_h-40, self.start_pos/#self.items) end mp.set_osd_ass(0, 0, ass.text) end function menu_handle_key(self,k) if k=="UP" then self.selected_item = self.selected_item - 1 elseif k=="DOWN" then self.selected_item = self.selected_item + 1 elseif k=="LEFT" then self.selected_item = self.selected_item - max_items elseif k=="RIGHT" then self.selected_item = self.selected_item + max_items elseif k=="BS" then state_back() elseif k=="RED" and self.red_action then self:red_action() elseif k=="GREEN" and self.green_action then self:green_action() elseif k=="YELLOW" and self.yellow_action then self:yellow_action() elseif k=="BLUE" and self.blue_action then self:blue_action() elseif k=="ENTER" then local item = self.items[self.selected_item] if item and item.action then item:action() end elseif k=="m" then state_remove_including("main_menu") elseif type(k) == "number" and k>=0 and k<=9 then self.selected_item = k end if self.selected_item > #self.items then self.selected_item = #self.items end if self.selected_item < 1 then self.selected_item = 1 end update_osd() end local margin=20 function split_text(max_len,text) local pos = 1 return function() local npos = text:find("\n",pos) if npos == nil then npos=text:len() end if npos-pos>max_len then -- find space to split the string npos=text:find(" ",pos+max_len-margin>0 and pos+max_len-margin or 0) if npos == nil then npos=text:len() end end if pos>=text:len() then return nil end local ret = text:sub(pos,npos-1) pos = npos + 1 return ret end end function show_epg(self) local ass = create_menu_base{blue="Switch"} local i = 0 local einfo = self:epg() -- time, title ass:pos(left, top+23) ass:append(format_epg(einfo)) ass:new_event() -- subtitle if einfo and einfo['subtitle'] then ass:pos(left, top+48) ass_scale_font(ass,80) ass:append(einfo['subtitle']) ass:new_event() end if einfo and einfo['description'] then for v in split_text(85,einfo['description']:gsub("|","\n")) do i = i + 1 ass:pos(left, top+53+i*13) ass_scale_font(ass,70) ass:append(v) ass:new_event() end end mp.set_osd_ass(0, 0, ass.text) end function clear_osd() mp.set_osd_ass(0, 0, "") end function key(k) return function() local cstate=curr_state() mp.log("info","state name "..cstate.name) mp.log("info","key "..k) if cstate and cstate.handle_key then cstate:handle_key(k) end end end function toArray(i) local array={} for v in i do array[#array+1]=v end return array end function slice(tbl,first,last) local s={} for i = first or 1, last or #tbl do s[#s+1] = tbl[i] end return s end local function send_webrequest(path) ret = utils.subprocess({ args= {'/bin/bash', '-c', '( printf "GET /'..path ..' HTTP/1.0\n\n"; sleep 1)|nc '..options.host..' ' ..options.streamdev_port}, -- args= {'/bin/bash', '-c', 'echo "'..command -- ..'" >/dev/tcp/'..options.host..'/'..options.svdrp_port}, cancellable=false, }) return ret.stdout end local function parse_ext3mu(stdout) mp.log("info","Parsing ext3mu") local state='http_header' local ret={} local title={} for l in string.gmatch(stdout,"[^\r\n]+") do if state=='http_header' then if l~="HTTP/1.0 200 OK" then state="error_http_header" break end state="http_header_content" elseif state=="http_header_content" then if l~="Content-type: audio/x-mpegurl; charset=UTF-8" then state="error_http_content" break end state="content_header" elseif state=="content_header" then if l~="#EXTM3U" then state="error_content_header" break end state="content_line_header" elseif state=="content_line_header" then if l:sub(1,11)~="#EXTINF:-1," then state="error_content_line_header" mp.log("info","'"..l:sub(1,11).."'") break end local info=toArray(string.gmatch(l:sub(12),"[^ ]+")) title['idx']=info[1] title['day']=info[2] title['time']=info[3] title['name']=table.concat(slice(info,4)," ") state = "content_line" elseif state=="content_line" then title['url']=l table.insert(ret,title) title = {} state = "content_line_header" end end mp.log("info",state) return ret end local function send_svdrp(command) ret = utils.subprocess({ args= {'/bin/bash', '-c', 'echo "'..command ..'" |nc '..options.host..' '..options.svdrp_port}, -- args= {'/bin/bash', '-c', 'echo "'..command -- ..'" >/dev/tcp/'..options.host..'/'..options.svdrp_port}, cancellable=false, }) return ret.stdout end local function parse_lstc(stdout) mp.log("info","Getting channel list") for i in string.gmatch(stdout,"[^\r\n]+") do local code = i:sub(1,4) if (code ~= "250-") then mp.log("info","Unknown code '"..code.."'") else local channel_end=i:find(";") if (channel_end ~=nil) then local channel=i:sub(5,channel_end-1) local sp=channel:find(" ") if (sp ~= nil) then local c =tonumber(channel:sub(0,sp-1)) local cinfo={} cinfo['idx']=c cinfo['name']=channel:sub(sp+1) -- S19.2E-1-1079-28011 ZDFinfo (S19.2E) -- ZDFinfo;ZDFvision:11953:HC34M2S0:S19.2E:27500:610=2:620=deu@3,621=mis@3,622=mul@3;625=deu@106:630;631=deu:0:28011:1:1079:0 local para=toArray(i:sub(channel_end+1):gmatch("[^:]+")) if (#para>12) then local cid=para[4].."-"..para[11].."-"..para[12].."-"..para[10] --mp.log("info",cid) cinfo['id']=cid end table.insert(channels,cinfo) end end end end end local function get_channels() parse_lstc(send_svdrp('LSTC')) end local function parse_lste(stdout) local epginfo={} local cid mp.log("info","Updating epg") for i in string.gmatch(stdout,"[^\r\n]+") do local code = i:sub(1,5) if (code == "215-C") then cid=i:sub(7) cid=cid:sub(1,cid:find(" ")-1) -- mp.log("info","Channel '"..cid.."'") if (epginfo[cid] == nil) then epginfo[cid] = {} end elseif (code == "215-T") then --mp.log("info","cid '"..cid.."' Titel '"..i:sub(7).."'") if (cid ~= nil) then epginfo[cid]['title']=i:sub(7) end elseif (code == "215-E") then if (cid ~= nil) then local p=toArray(i:sub(7):gmatch("[^ ]+")) epginfo[cid]['start']=p[2] epginfo[cid]['duration']=p[3] end elseif (code == "215-S") then if (cid ~= nil) then epginfo[cid]['subtitle']=i:sub(7) end elseif (code == "215-D") then if (cid ~= nil) then epginfo[cid]['description']=i:sub(7) end elseif (code =="215-c") then cid = nil end end return epginfo end local function get_epg_now() epgnow=parse_lste(send_svdrp("LSTE now")) end local function get_epg_next() epgnext=parse_lste(send_svdrp("LSTE next")) end local function switch_channel(no) mp.log("info","switch_channel "..no) local sav_channel=channel_idx mp.add_timeout(options.previous_channel_time,function() last_channel=sav_channel end) channel_idx=no mp.commandv("loadfile",vdruri .. channel_idx) --mp.commandv("loadfile","/Users/wache/Downloads/mps/7-Minute Workout.webm") if curr_state().name ~="channel_info" then new_state(state_channel_info) else state_update_timeout() end next_channel=0 update_osd() end local function playback_recording(url) mp.log("info","play_rec "..url) local sav_channel=channel_idx mp.commandv("loadfile",url) --mp.commandv("loadfile","/Users/wache/Downloads/mps/7-Minute Workout.webm") end local function channel_next() mp.log("info","next channel called " .. channel_idx .. " len channels " .. #channels) channel_idx = channel_idx + 1 if #channels>0 and channel_idx > #channels then channel_idx = 1 end switch_channel(channel_idx); end local function channel_prev() mp.log("info","next channel called " .. channel_idx .. " len channels " .. #channels) channel_idx = channel_idx - 1 if (channel_idx < 0) then channel_idx =#channels > 0 and #channels or 0 end switch_channel(channel_idx); end local channel_timer=mp.add_periodic_timer(2,function() if ( next_channel ~= 0 ) then switch_channel(next_channel) next_channel = 0 end if ( channel_timer ~= nil ) then channel_timer:kill() end end) local function playback_handle_key(self,key) mp.log("info","state name "..self.name) if key=="BLUE" then state_back_to("livetv") switch_channel(channel_idx) elseif key=="BS" then state_back() switch_channel(channel_idx) elseif key=="DOWN" or key=="UP" then mp.command("cycle pause") elseif key=="YELLOW" then mp.command("no-osd seek +30") update_osd() elseif key=="GREEN" then mp.command("no-osd seek -30") update_osd() elseif key=="ENTER" then if self.update_osd==nil then self.update_osd=show_playback_info else self.update_osd=nil end update_osd() end end local function livetv_handle_key(self,key) mp.log("info","state name "..self.name) if key=="ENTER" then if self.name=="channel_info" then state_back() else new_state(state_channel_info) end elseif key=="BS" then if self.name=="channel_info" then state_back() end elseif key=="m" then state_back() new_state(state_main_menu) elseif key=="UP" then channel_next() elseif key=="DOWN" then channel_prev() elseif type(key) =="number" then if key == 0 and next_channel == 0 then -- immediatly update last_channel local sav_channel=channel_idx switch_channel(last_channel); last_channel=sav_channel return end next_channel=next_channel*10+key mp.osd_message(next_channel) channel_timer:resume() end end local function on_start() local url = mp.get_property("stream-open-filename") mp.log("info","channels length "..#channels) if (url:find("vdrstream://") == 1) then if ( startup == 1) then local host_port = url:sub(13) if (host_port:len()>0) then local has_port=host_port:find(":") if (has_port) then options.host = host_port:sub(1,has_port-1) options.streamdev_port=host_port:sub(has_port+1) else options.host = host_port end end mp.log("info","VDR host:"..options.host) mp.log("info","VDR svdrp port:"..options.svdrp_port) mp.log("info","VDR streamdev port:"..options.streamdev_port) vdruri="http://"..options.host..":"..options.streamdev_port.."/TS/" -- set parameters to optimize channel switch time mp.set_property("cache-secs",1) mp.set_property("demuxer-lavf-analyzeduration",1) mp.set_property("ytdl","no") get_channels() -- load epg in background mp.add_timeout(1,function() get_epg_now() get_epg_next() mp.log("info","finished epg") end) -- periodically update epg epg_timer = mp.add_periodic_timer(options.epg_update_time,function() get_epg_now() get_epg_next() end) startup = 0 end -- mp.set_property("stream-open-filename",channels[channel_idx]) --mp.set_property("cache-size",1024) switch_channel(1) --playback_recording("/Users/wache/Downloads/mps/7-Minute Workout.webm") --rinfo= { --url="blah", --name="Diese Datei", --} --new_state(create_playback_state(rinfo)) end mp.log("info","Lua version " .. _VERSION) end function new_show_epgs_state(epg_info,channel_info) state_show_epg = { name = "menu_epg", handle_key = function(self,k) if k=="ENTER" or k=="BS" then state_back() elseif k=="m" then state_remove_including("main_menu") elseif k=="BLUE" then state_back_to("livetv") switch_channel(self.cinfo['idx']) end end, update_osd = show_epg, epg = function(self) return epg_info end, cinfo = channel_info, } return state_show_epg end function draw_epg_item(self,ass,left,top,width,height) ass:pos(left,top) ass_clip(ass,left,top,left+95,top+height) ass_scale_font(ass,80) ass:append(self.cinfo['name']) ass:new_event() ass:pos(left+100,top) ass_scale_font(ass,80) ass:append(print_time(self.einfo['start'])) ass:new_event() if self.einfo['title'] then ass:pos(left+150,top) ass_clip(ass,left+150,top,left+width,top+height) ass_scale_font(ass,80) ass:append(self.einfo['title']) ass:new_event() end end function create_epg_show_menu_state(name,epglist) local items={} for i=1,#channels do local c = channels[i] if c['id'] and c['name'] then local e = epglist[c['id']] if e then table.insert(items,{ text=c['name']..format_epg(e), action = function() new_state(new_show_epgs_state(e,c)) end, draw = draw_epg_item, einfo = e, cinfo = c, }) end end end local nstate= new_menu_state(name,items) nstate.blue_name="Switch" nstate.blue_action=function(self) state_back_to("livetv") switch_channel(self.selected_item) end nstate.selected_item=channel_idx return nstate end function create_playback_state(rinfo) local state_playback = { name = "playback", handle_key = playback_handle_key, update_osd = nil, rinfo = rinfo, update_osd_timeout=1, } return state_playback end function create_recordings_show_items() items={} recordings=parse_ext3mu(send_webrequest("/recordings.m3u")) for i,r in pairs(recordings) do table.insert(items,{ text=r['name'], action = function(self) playback_recording(self.rinfo['url']) new_state(create_playback_state(self.rinfo)) end, rinfo = r, }) end return items end function new_menu_state(name,items) local state_menu = { name = name, handle_key = menu_handle_key, update_osd = show_menu, items = items, } return state_menu end state_main_menu = new_menu_state("main_menu", { { text="What's on now", action = function() local nstate=create_epg_show_menu_state("epg_now", epgnow) new_state(nstate) end, }, { text="What's on next", action = function() local nstate=create_epg_show_menu_state("epg_next", epgnext) new_state(nstate) end, }, { text="Recordings", action = function() new_state( new_menu_state("Recordings", create_recordings_show_items() )) end, }, }) state_epg_now = { name = "state_epg_now", handle_key = menu_handle_key, update_osd = show_menu, } state_livetv = { name = "livetv", handle_key = livetv_handle_key, update_osd = nil, } state_channel_info = { name = "channel_info", handle_key = livetv_handle_key, update_osd = show_channel_info, timeout = 5, } new_state( state_livetv ) mp.add_key_binding("F1",'vdrkeyRED',key("RED")) mp.add_key_binding("F2",'vdrkeyGREEN',key("GREEN")) mp.add_key_binding("F3",'vdrkeyYELLOW',key("YELLOW")) mp.add_key_binding("F4",'vdrkeyBLUE',key("BLUE")) mp.add_key_binding("0",'vdrkey0',key(0)) mp.add_key_binding("1",'vdrkey1',key(1)) mp.add_key_binding("2",'vdrkey2',key(2)) mp.add_key_binding("3",'vdrkey3',key(3)) mp.add_key_binding("4",'vdrkey4',key(4)) mp.add_key_binding("5",'vdrkey5',key(5)) mp.add_key_binding("6",'vdrkey6',key(6)) mp.add_key_binding("7",'vdrkey7',key(7)) mp.add_key_binding("8",'vdrkey8',key(8)) mp.add_key_binding("9",'vdrkey9',key(9)) mp.add_key_binding("UP",'vdrkeyUP',key("UP")) mp.add_key_binding("DOWN",'vdrkeyDOWN',key("DOWN")) mp.add_key_binding("LEFT",'vdrkeyLEFT',key("LEFT")) mp.add_key_binding("RIGHT",'vdrkeyRIGHT',key("RIGHT")) mp.add_key_binding("ENTER",'vdrkeyENTER',key("ENTER")) mp.add_key_binding("BS",'vdrkeyBS',key("BS")) mp.add_key_binding("m",'vdrkeym',key("m")) mp.add_key_binding("i",'show_description',show_description) mp.add_hook("on_load", 50, on_start)