3 -- Copyright (C) 2011, 2012 quvi project
5 -- This file is part of quvi <http://quvi.sourceforge.net/>.
7 -- This library is free software; you can redistribute it and/or
8 -- modify it under the terms of the GNU Lesser General Public
9 -- License as published by the Free Software Foundation; either
10 -- version 2.1 of the License, or (at your option) any later version.
12 -- This library is distributed in the hope that it will be useful,
13 -- but WITHOUT ANY WARRANTY; without even the implied warranty of
14 -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 -- Lesser General Public License for more details.
17 -- You should have received a copy of the GNU Lesser General Public
18 -- License along with this library; if not, write to the Free Software
19 -- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
24 -- - Add support for Radio programmes
25 -- - Add support for live streaming
26 -- - Better error messages for geolocation errors
27 -- - Offer the subtitles for download somehow
29 -- Obtained with grep -oP '(?<=service=")[^"]+(?=")' on config
30 local fmt_id_lookup
= {
31 high
= 'iplayer_streaming_h264_flv_high',
32 standard
= 'iplayer_streaming_h264_flv',
33 low
= 'iplayer_streaming_h264_flv_lo',
34 vlow
= 'iplayer_streaming_h264_flv_vlo'
35 -- iplayer_streaming_n95_3g
36 -- iplayer_streaming_n95_wifi
39 -- Identify the script.
41 package
.path
= self
.script_dir
.. '/?.lua'
42 local C
= require
'quvi/const'
44 r
.domain
= "www.bbc.co.uk"
45 r
.formats
= "default|best"
46 for k
,_
in pairs(fmt_id_lookup
) do
47 r
.formats
= r
.formats
.."|".. k
49 r
.categories
= C
.proto_rtmp
50 local U
= require
'quvi/util'
51 r
.handles
= U
.handles(self
.page_url
, {r
.domain
}, {"/iplayer/"})
58 function needs_new_authString(params
)
59 if not params
['authString'] then
63 for _
,kind
in pairs
{'limelight', 'akamai', 'level3', 'sis', 'iplayertok'} do
64 if kind
== params
['kind'] then
69 if not found
then return false end
70 -- We don't need to check for the mode, we already know it's what we want
74 function create_uri_for_limelight_level3_iplayertok(params
)
75 params
.uri
= params
.tcurl
.. '/' .. params
.playpath
78 function process_akamai(params
)
79 params
.playpath
= params
.identifier
80 params
.application
= params
.application
or 'ondemand'
81 params
.application
= params
.application
.. '?_fcs_vhost=' .. params
.server
.. '&undefined'
82 params
.uri
= 'rtmp://' .. params
.server
.. ':80/' .. params
.application
83 if not params
.authString
:find("&aifp=") then
84 params
.authString
= params
.authString
.. '&aifp=v001'
86 if not params
.authString
:find("&slist=") then
87 params
.identifier
= params
.identifier
:gsub('^mp[34]:', '')
88 params
.authString
= params
.authString
.. '&slist=' .. params
.identifier
90 params
.playpath
= params
.playpath
.. '?' .. params
.authString
91 params
.uri
= params
.uri
.. '&' .. params
.authString
92 params
.application
= params
.application
.. '&' .. params
.authString
93 params
.tcurl
= 'rtmp://' .. params
.server
.. ':80/' .. params
.application
96 function process_limelight_level3(params
)
97 params
.application
= params
.application
.. '?' .. params
.authString
98 params
.tcurl
= 'rtmp://' .. params
.server
.. ':1935/' .. params
.application
99 params
.playpath
= params
.identifier
100 create_uri_for_limelight_level3_iplayertok(params
)
103 function process_iplayertok(params
)
104 params
.identifier
= params
.identifier
.. '?' .. params
.authString
105 params
.playpath
= params
.identifier
:gsub('^mp[34]:', '')
106 params
.tcurl
= 'rtmp://' .. params
.server
.. ':1935/' .. params
.application
107 create_uri_for_limelight_level3_iplayertok(params
)
112 local _
,_
,s
= self
.page_url
:find('episode/(.-)/')
113 local episode_id
= s
or error('no match: episode id')
117 'http://www.bbc.co.uk/iplayer/playlist/' .. episode_id
118 local playlist
= quvi
.fetch(playlist_uri
, {fetch_type
= 'playlist'})
120 local pl_item_p
,_
,s
= playlist
:find('<item kind="programme".-identifier="(.-)"')
122 pl_item_p
,_
,s
= playlist
:find('<item kind="radioProgramme".-identifier="(.-)"')
123 -- TODO: Implement radio support
125 error('No support for radio yet')
128 local media_id
= s
or error('no match: media id')
130 local _
,_
,s
= playlist
:find('duration="(%d+)"', pl_item_p
)
131 self
.duration
= tonumber(s
) or 0
133 local _
,_
,s
= playlist
:find('<title>(.-)</title>')
134 self
.title
= s
or error('no match: video title')
136 local _
,_
,s
= playlist
:find('<link rel="holding" href="(.-)"')
137 self
.media_thumbnail_url
= s
or ""
139 -- stolen from http://lua-users.org/wiki/MathLibraryTutorial
140 math
.randomseed(os
.time()) math
.random() math
.random() math
.random()
142 'http://www.bbc.co.uk/mediaselector/4/mtis/stream/' ..
143 media_id
.. "?cb=" .. math
.random(10000)
145 local config
= quvi
.fetch(config_uri
, {fetch_type
= 'config'})
147 available_formats
= {}
148 for fmt_id
in config
:gmatch("iplayer_streaming_[%w_]+") do
149 available_formats
[fmt_id
] = true
151 -- Create the list of acceptable formats, ordered by preference
152 local r
= self
.requested_format
153 local preferred
= ((r
== 'best') and {'high', 'standard', 'low', 'vlow'})
154 or ((r
== 'default') and {'standard', 'low', 'vlow', 'high'})
157 -- Pick the first acceptable format available
159 for _
, cur_format
in ipairs(preferred
) do
160 if available_formats
[fmt_id_lookup
[cur_format]]
then
165 if not format then error('format not available') end
167 -- Iterate over <media/>s
169 for section
in config
:gmatch('<media .-</media>') do
170 if section
:find('service="' .. fmt_id_lookup
[format] .. '"') then
175 if not media
then error("Couldn't parse the config") end
179 -- Initialise with the default values from the media
181 for _
,mparam
in pairs
{'kind', 'service'} do
182 _
,_
,mparams
[mparam
] = media
:find(mparam
.. '="(.-)"')
183 -- print ("MEDIA: mparams[" .. mparam .. "] = " .. mparams[mparam])
186 for connection
in media
:gmatch('<connection .-/>') do
187 local params
, complete_uri
= {}, ''
189 for _
,param
in pairs
{'supplier', 'server', 'application', 'identifier', 'authString', 'kind'} do
190 _
,_
,params
[param
] = connection
:find(param
.. '="(.-)"')
191 -- print ("CONNECTION: params[" .. param .. "] = " .. (params[param] or "(null)"))
194 -- Get authstring from more specific mediaselector if
195 -- this mode is specified - fails sometimes otherwise
196 if needs_new_authString(params
) then
199 'http://www.bbc.co.uk/mediaselector/4/mtis/stream/' ..
200 media_id
.. '/' .. mparams
['service'] .. '/' .. params
['kind'] ..
201 "?cb=" .. math
.random(10000)
202 xml
= quvi
.fetch(xml_uri
, {fetch_type
= 'config'})
203 local _
,_
,new_authString
= xml
:find('authString="(.-)"')
204 if new_authString
then
205 params
['authString'] = new_authString
:gsub('&', '&')
208 -- Unescape the authString
209 if params
['authString'] then
210 params
['authString'] = params
['authString']:gsub('&', '&')
214 -- in 'application', mp has a value containing one or more entries separated by strings.
215 -- We only keep the first entry.
216 if params
.application
then
217 params
.application
= params
.application
:gsub("&mp=([^,&]+),?.-&", "&mp=%1&")
220 if params
.supplier
== 'akamai' then
221 process_akamai(params
)
224 if (params
.supplier
== 'limelight' or params
.supplier
== 'level3') then
225 process_limelight_level3(params
)
228 params
.uri
= params
.uri
or error('Could not create RTMP URL')
230 complete_uri
= params
.uri
231 .. ' app=' .. params
.application
232 .. ' playpath=' .. params
.playpath
233 .. ' swfUrl=http://www.bbc.co.uk/emp/revisions/18269_21576_10player.swf?revision=18269_21576 swfVfy=1'
234 .. ' tcUrl=' .. params
.tcurl
235 .. ' pageurl=' .. self
.page_url
237 self
.url
[#(self
.url
) + 1] = complete_uri