forked from LMS-Community/slimserver
-
Notifications
You must be signed in to change notification settings - Fork 0
/
DEVELOPERS.txt
323 lines (259 loc) · 20.5 KB
/
DEVELOPERS.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
------------- How is image/icon/cover passed from the OPML item to a track -------------
When a $url from an OPML item is added to current playlist, a $track object is created and its
image is obtained by calling handlerForUrl($url)->getMetadataFor($url). If this protocol handler
is HTTP, it needs to find the image just by using this $url, there is no other information.
XMLBrowser(s), when adding the url to the playlist, call setRemoteMetadata which caches the OPML
items's image link as "remote_image_$url". It does that automatically for HTTP(S) and otherwise
calls the $url's protocol handler method shouldCacheImage. This method can either return true and
then the "remote_image_$url" will be cached or it can also do its own caching (or not) and return
false. When "remote_image_$url" is cached, it is used getMetadataFor() as one of the image sources.
Now, when playing the $track, if scanUrl() points to Slim::Utils::Scanner::Remote::ScanURL() and if
there are redirections, a $newtrack is created whose $newurl is the redirected one which will then
be used in further getMetadataFor calls. As this $newurl has no image's link entry in the cache,
the "remote_entry_$url" is copied upon each redirection to "remote_image_$newurl" during the scan.
Ultimately, when the actual image is cached (not the link), there will be an artwork cache entry
that getMetadataFor() will use.
Note that some format may have other cover art inside streamed with the track and, for example,
mp3 does grab that image and cache it using "cover_$url". It is the *actual* image there, not a
link to it. Look at Slim::Format::XXX for further details.
--------------------------------- How scanUrl works ------------------------------------
For HTTP(S) protocol handler, the scanUrl calls Slim::Utils::Scanner::Remote::scanURL when a track
starts to play to acquire all necessary informations. The scanUrl is called with the $url to scan
and an $args containing the $song object and a $callback.
That $song contains a $track object that is created from the original $url set when OPML item is
added in the playlist. When scanning $url, the Slim::Utils::Scanner::Remote::scanURL creates a new
$track object everytime the uri returned by the GET is different from the $url argument. This
happens on HTTP redirection and if the $url argument differs from $args->{'song'}->track->url. The
original url is stored as $track->redir (note that it might be empty).
When it returns, scanUrl provides a new $track that replaces the current one in the $song object
*and* in the playlist. This means that the playlist may have new $track->url for same item index.
When playing again that track from the playlist, LMS would normally scan again that redirected
$track->url which might be gone (see below).
Not that scanURL also replaces the $song->streamUrl by the $newTrack->url if streamUrl still equals
to the $url parameter when song is being opened/created. This allows protocol handler to change it
as they want, but still update it when built-in scan has control.See "thin protocol handler" for
more explanations.
-------------------------- Issue of "volatile" redirection -------------------------------
Some url are being redirected to temporary location, which means that the final location is only
valid for as low as a few minutes. This causes a problem when user pauses such a track.
When pauses happens, LMS quite often memorizes the current position and closes the connection (it's
not always the case, depending of course is track is seekable, on the fullness of player's buffer
and a few other parameters). The issue is that upon resume, the redirected location might be gone
and a 403 will happen.
When resuming, LMS sometimes creates a new() song, sometimes it just re-opens it with a song open().
In both cases, the $url inside the track is the one that scanUrl has put after redirection.
To solve that, if Slim::Player::Song::open() fails and the track has been redirected, open() will
set streamUrl to $track->redir (the original url) and then will recurse with direct streaming
disabled. It's not possible to allow direct streaming as if an HTTP -> HTTPS upgrade is requested
during direct streaming redirection, playback will fall.
When using new() on resume, LMS does a getNextSong which is the method calling scanUrl. If it fails,
getNextSong will recurse with $track->redir.
This is more tricky when direct stream is used because the error happens much later, when the
player returns the result of the HTTP request and it is thus not possible to recurse in open(). In
this case, as when redirection happens in direct streaming, we'll set the streamUrl to original url
and give it another try. It's not foolproof, but it solves most cases. Typically, as said above, the
upgraded HTTP -> HTTPS redirection cannot be handled
Note that the original url is only set when Slim::Utils::Misc::Scanner::Remote::scanURL is called,
so this behavior does not impact plugins that do not call their Slim::Player::HTTP::scanUrl
ancestor.
------------------------ Thin Protocol Handler (e.g. podcast) -----------------------------
A thin protocol handler simply (and mostly) encapsulate HTTPS(s) urls into a small wrapper like
"<myph>://http://<$url>". Typically, the scanUrl unwraps the $url to the HTTP(S) one and then
relies on normal HTTP(S) handling, but there are a few catches as we want to benefit from all
HTTP(S) methods but still overload some, making sure our protocol handler is still called after
we have de-encapsulated urls.
When a $track object has changed after scanUrl (due to a redirection), LMS offers to re-evaluate
the song->handler and replace it by what the $handler->songHandler() returns. This is useful when
an HTTP has been upgraded to HTTPS during redirection because you know want HTTPS handle to take
care of this url but in many case it's dangerous. For example, for thin Protocol Handler, you don't
want to loose the control on that song which would happend if the handler is reset to HTTP.
So, when calling the scanUrl ancestor, you should unwrap your $url paramater and then let
Slim::Utils::Scanner::Remote::ScanURL do it's HTTP job. But the callback, which returns the
parsed track, shall be interecepted to recover the redirected url and replace it by your original
url (at least add-again your <myph:://> to was is returned) to make sure that the track, once
replaced in the playlist, will still use your protocol handler. This is also where it's recommended
to set $song->streamUrl to the streamable url, often simply the $newtrack->url. Note that because
$song->streamUrl is updated by the protocol handler, it will not be overwritten by scanURL
Best is to look at Slim::Plugin::Podcast as an example of such thin protocol handler
IMPORTANT NOTE: When doing proxy streaming, LMS calls the protocol handler's new() at each
redirection. This can be problematic for HTTP(S) subclasses if the new() if calling the ancestor's
new() *but* uses it's $song->streamUrl. It will create an infinite loop as the url is constantly
reset to one that will be redirected. To avoid that, use the $args->{redir} in the new arguments to
determine if this new is after a redirection.
------------------------------ Scanning of remote tracks ----------------------------------
One issue with remote tracks is that their headers might be needed for LMS to properly process their
sample rate, size and a few critical parameters that are required for seeking accross. Without
acquiring the header, it can be impossible to seek into various formats like mp4. Sometimes, the
header is at the bottom of the file, requiring to do an offset HTTP request.
The solution was to provide a small framework to allow LMS to acquire the header, get it stored and
analysed, then re-used every time streaming bytes starts to be sent to players.
The core happens in Slim::Utils::Scanner::Remote where the method parseRemoteHeader() can be called
by protocol handlers which want to acquire headers for their remote stream. It is the default for
Slim::Player::Protocol::HTTP::scanUrl.
It relies on helpers for each XXX format from Slim::Format::XXX
- parseStream() is used to parse the stream on-the-fly, acquire track information and store them in
track header so that it maybe be adjusted later when seeking. This method is aimed to be called
everytime a new scanning chunk is received, until a header is successfully parsed. It returns 0 upon
failure, -1 if it needs more bytes, a value > 0 to jump to that offset in the stream and a hash
containing parsed information when done. See Slim::Format::Movie.pm for a most complete example.
- getInitialAudioBlock() is used to get such header that will be sent to the player at the beginning
of playback. It is called only when there is a processor, so see explanation below to handle header
acquisition properly. In most of cases, it's simple and the default function will work.
The handling for every format is spread between Slim::Formats::XXX and Slim::Utils::Scanner::Remote
and this could be refactored a bit. In a nutshell, after parsing stream's header, it is decided if
the remote stream header should be discarded and replaced by a tweaked one when starting to send
audio bytes to the player. Such tweaked headers can be added never, once, every time or only when
seeking (this also influences the possibility for a track to be directly played or proxied)
Upon actual streaming of audio and when header needs to be tweaked, the Slim::Player::HTTP::request
will look for "processors" in the track. Such processors are set upon initial scanning or by a
handler in scanURL. A track can output multiple formats, so that's why there are multiple processors
possible. LMS will pickup one depending on what the scan offered and what the player can accept.
The relevant processor is then called in Slim::Player::HTTP::request and can simply create a tweaked
header that will then be passed to the player but it can also return a structure with a method to be
called for every chunk of audio data received. This is used for adts frames extraction from mp4 file,
when player wants 'aac' and not 'mp4'. This is also used for flac synchronization where some IP3K
players can't resynchronize when seeking in a middle of a flac stream. The more simple case is wav
file that just need a header tweak but then don't need further handling of audio chunks.
Processors must set 'initial_block_type' to either ONCE, ONSEEK or ALWAYS to set when
getInitialAudioBlock() will be called by Slim::Player::HTTP::request and also decide when/if direct
streaming is possible. When there is nothing returned by getInitialAudioBlock(), it will be treated
as a defined-but-empty initial block.
The 'GET' request for the actual audio data uses a 'Range' byte offset to skip the header (if any).
If the $track->audio_offset has been set *and* the initial audio block is defined, then this is the
range. Now, if there is a processor, it can override it by setting sourceStreamOffset but if there
is no processor this cannot be changed.
So understand the implication of setting $track->audio_offset. When it is not set, then the GET
range will be 0 unless a processor has set the sourceStreamOffset. Note that the stored initial block
will only be sent to the player when there is a processor.
In other words, if you want LMS to GET the whole file from 0 and send it to the player but you don't
want/need to set a processor, then DO NOT set $track->audio_offset!
When there is a processor, direct streaming is enable only if 'initial_block_type' is set to ONSEEK
and track starts from zero (no seekdata). Streaming will always be proxied otherwise. When there is
no processor, direct streaming will be attempted according to usual rules but the $track->audio_offset
logic described above will apply and be passed to the player as range.
The logic is the same when seeking, except that the byte seek offset is added.
Look at Slim::Misc::Utils::Scanner::Remote and Slim::Formats::Movie or Slim::Formats::FLAC for
general understanding and at Slim::Plugin::WiMP::ProtocolHandler to see how a plugin can use this
framework to have its tracks scanned.
----------------------------------- Podcast plugin extension --------------------------------------
Since LMS 8.2, the Podcast plugin has the possibility to search for feeds with a choice of search
engines. As it is not possible to add too many engines directly in LMS, a small extension framework
has been built and allow custom plugins to add their own provider and a few simple set of sub-menus.
The "Provider" class is used as the base for any podcast provider and has is expecting to haave the
following methods:
- new (O) => a class/object reference to the newly created provider
- getName => name of the provider
- getMenuItems (O) => array reference of OPML items to add to main menu
- getFeedsIterator => closure to be called to iterate through results
- getSearchParams => array reference to: [ $url for search and array reference to extra headers]
- newsHandler (O) => a classical OPML list builder if the provider supports "what's new"
The default search handler Slim::Plugin::Podcast::searchHandler is doing an HTTP query using a
customizable url and expects a JSON payload in return that it maps to OPML feeds by repeateadtly
calling the iterator gotten from getFeedsIterator (See PodcastIndex example).
The "newsHandler" method, when present, signals to the plugin that the provider is capable of
listing new episodes of subscribed feeds. Now, there is no boilerplate code for how to handle the
search for news and it is left to the provider itself (it can be too much different between
providers). The presence of "newsHandler" methods only guarantees that podcast settings shows a
"since" and "max" options.
The Slim::Plugin::Podcats::registerProvider($classname) is used to add providers. See example here
https://github.com/philippe44/LMS-PodcastExt for adding a new provider or extending an existing one
--------------------------- Some comments on tracks/song data structure ---------------------------
>>> URL's in $song
- streamUrl is the url that will actually be GET, optionally using direct mode. It is set to
$track->url at creation of $song, updated when track is being changed after a scanURL (if the
updated url differs from the original one) and also set by $player->canDirectStream if direct is
possible. Quite often, PH use it to store the url they really want to stream. By default, HTTP
uses it in canDirectStreamSong to pass the argument to canDirectStream
>>> URL's in Slim::Utils::Schema::(Remote)Track
- _url is the url used at the track's creation
- url is a method to get/set it. When set, it changes the cache
- redir is the $url argument of Slim::Utils::Scanner::Remote::scanURL. It is set *only* upon redirection
so it might be empty which is useful to know that redirection happened.
>>> Tracks in Slim::Player::Song
When a track contains a playlist, there is only one $song created (new) but then that same song is opened
multiple times. The first time the playlist is scanned and then all sub-tracks are scanned (note that
visually it looks like one track that can be skipped but will stay on the same track until all sub-tracks
have been skipped). On further open(), the next sub-track is set in _currentTrack.
- _currentTrack (RW) is the current sub-track (if any) in the playlist. it can be empty
- _track (RW) is the master track
- track() is a method that returns the master track
- currentTrack() is a method that returns the actual current track, i.e. _currentTrack || _track
>>> Handlers in Slim::Player::Song and Slim::Player::SongStreamingController
- $song->handler (RO) is set at the creation of the song. it is based on track->url at creation and is
immutable.
- $song->_currentTrackHandler (RW) is set when a single track happens to be a playlist, for each item.
It allows each individual sub-tracks to have their own PH when it relates to some of their management.
It means that it is empty most of the time but PH can update it when $track is changed after scanning
(for example when upgrding from HTTP to HTTPS)
- $song->currentTrackHandler() is a method that returns (_currentTrackHandler || handler) so it tells
what shall be used for all url related to the current (sub) track
- $songStreamingController->urlHandler is RO and set at the creation of the song streaming controller,
based on the streamUrl (in S::P::Song::open)
- $songStreamingController->streamHandler is RO and set at creation of the song streaming controller.
It is the class of the *socket* object created by $song->handler->new.
- $songStreamingController->currentTrackHandler is a shortcut to $song->currentTrackhandler
>>> Protocol Handlers
These files contains a base class but whose methods are called in different contexts (the $self)
1- a $song context means they are called with a $song->currentTrackHandler or $song->handler
2- a $sock context means they are called with a $socket (or the object) that was created by
$song->handler->new
3- a simple $url context means they are called by $song->currentTrackHandler, $song->Handler or a
$songStreamingController->streamHandler
So it's a bit confusing that methods from the same package can be object-oriented called with totally
different type of ancestors/context. In fact, some methods can be called in both context is the
ancestor of the PH is capable of (e.g. HTTP).
- functions like sysread are only called with a $sock context
- functions like getMetadataFor can be called with a $song or an $url context
- functions like requestString can be called in a $song or $sock context
>>> Example of requestString
That function is to create the HTTP headers to be sent with the GET to grab the actual audio. It
can be called in proxied or direct mode (in direct mode, there is no $sock available). Until
version 8.2, it was only called in the context of urlHandler because such PH always derive from
some sort of HTTP/RemoteStream. It could not be a songHandler as these might have a non-HTTP base
so requestString will not exist.
But that caused a problem when a song's PH spits out a url that can be direct *but* still wants to
modify the requestString, he would never see the requestString as the one being called was in the
class of urlHandler (means streamUrl). Since 8.2, the call of requestString is now trying in order
first songHandler then urlHandler
>>> Example of getMetadataFor
That function is called with the handler from handlerForURL most of the time but in many cases the
url can be the playing song. This is an issue if the song has redefined its $track->url (thin
protocol for example). Now it is covered because when calling Slim::Player::Protocols::HTTP::getMetadataFor
(most probably the base class) there we verify that we are not checking the current song in which
case we call currentTrackHandler->getMetadataFor.
>>> example of canDirectStream and canDirectStreamSong
The method canDirectStream is first invoked in a $player context with currentTrack->url and $sock.
Its role is to check with protocol handlers if they will actually spit out a HTTP(s) url than can
be direcly streamed by the player. The protocol handler must return the direct url if it allows
that LMS places it in $song->streamUrl (before the SongStreamingController is created).
In protocol handler's contexts, canDirectStream is invoked with the $client and currentTrack->url.
From there and if available, canDirectStreamSong is invoked with the $client and the $song (it
seems strange to have these 2 methods when one would have probably suffice)
If canDirectStreamSong is missing, canDirectStream is invoked with the streambel url. When using
or suclassing, Slim::Player::Protocol::HTTP(S), protocol handler do not need to offer a canDirecStream
as the base class will invoke it from canDirectStreamSong using the streamUrl
----------------------------------- HTTP methods -----------------------------------------
The constructors of Slim::Networking::SimpleAsyncHTTP and Slim::Networking::Async::HTTP have
two new keys in their hash argument
- 'options': set parameters for underlying socket object. For example, to change SSL
options => {
SSL_cipher_list => 'DEFAULT:!DH',
SSL_verify_mode => Net::SSLeay::VERIFY_NONE }
}
- 'socks': use a socks proxy to tunnel the request (see SOCKS.TXT)
socks => {
ProxyAddr => '192.168.0.1', # can also be a FQDN
ProxyPort => 1080, # optional, 1080 by default
Username => 'user', # only for socks5
Password => 'password', # only for socks5
}
Slim::Networking::Async::HTTP->new( {
options => {
SSL_cipher_list => 'DEFAULT:!DH',
SSL_verify_mode => Net::SSLeay::VERIFY_NONE
},
socks => {
ProxyAddr => '192.168.0.1',
ProxyPort => 1080,
}
} );