forked from GNOME/rhythmbox
-
Notifications
You must be signed in to change notification settings - Fork 0
/
README.daap
268 lines (217 loc) · 13 KB
/
README.daap
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
How to build and use Rhythmbox with DAAP music sharing support
* configure Rhythmbox with --enable-daap
Requires libsoup & either Howl or Avahi
* Start Howl's mDNSResponder or Avahi's avahi-daemon
* Start Rhythmbox, watch as remote music shares appear in your source list
* Click on a remote source, play their music.
* Check Edit->Preferences->Sharing to share your music library.
How Music Sharing Works
0. Definitions
1. mDNS/DNS-SD support
2. Discovering new music shares
3. Connecting to a music share
4. Playing remote music
5. Publishing your music
6. RBDAAPStructure - structure parsing & building
0. Definitions / Fundamentals
DAAP stands for Digital Audio Access Protocol. It is the communications
protocol that music sharing uses. It is a structured (like xml) query/response
language that works over HTTP 1.1. A DAAP server is nothing more than an
HTTP server that responds to certain URI requests with a structured DAAP
response. DAAP is the same music sharing protocol used by Apple iTunes. So,
a copy of Rhythmbox that was compiled with DAAP will be able to browse iTunes
shares, and iTunes will be able to browse Rhythmbox's music.
mDNS is multicast DNS. DNS Service Discovery (DNS-SD) sits on top of it.
mDNS/DNS-SD is what Apple calls Bonjour or Rendezvous. The fundamentals aren't especially important, suffice it to say it allows for automatic discovery of
services on your local network. You send out an mDNS/DNS-SD request for DAAP
servers, it responds with all the servers that have published their information
on your network. mDNS/DNS-SD services have types. The DAAP service type is
_daap. It is transported over tcp, so in the code you'll often see _daap._tcp,
which is the full name for a DAAP service.
iTunes hashing is an "authentication" method that appeared in later versions
of Apple iTunes. It is a modified md5 hash of the URI being requested from a
DAAP server. The hash is sent along in the HTTP headers to show an iTunes
server that the connecting client is "authenticated" (i.e. "is an iTunes
client"). The hashing code was borrowed with many thanks from libopendaap.
1. mDNS/DNS-SD support
For DAAP music sharing you'll need an mDNS/DNS-SD implementation.
Howl (http://www.porchdogsoft.com/products/howl/)
or
Avahi (http://www.freedesktop.org/Software/Avahi)
are supported. Either way, the mDNS/DNS-SD routines live in
daapsharing/rb-daap-mdns.c/h.
There are three routines in the mDNS/DNS-SD libraries that we need. Browsing to
discover remote shares, Resolving to figure out those shares' IP addresses, and
Publishing to make our own share known on the network. There are three opaque
datatypes, RBDAAPmDNSBrowser, RBDAAPmDNSResolver, RBDAAPmDNSPublisher. If
Rhythmbox has been compiled with Howl support, these will will be used as
sw_discovery_oid structures, which are used to identify the specific mDNS/DNS-SD
call. If Rhythmbox is using Avahi, these datatypes represent
AvahiServiceBrowser, AvahiServiceResolver and AvahiEntryGroup variables,
which, again, are used to uniquely identify the specific mDNS/DNS-SD call.
Browsing for remote shares is started via rb_daap_mdns_browse() which takes a
callback that is run anytime a share is discovered or goes away. Browsing will
continue until rb_daap_mdns_browse_cancel() is called.
Resolving is done asychronously via rb_daap_mdns_resolve() and a callback which
runs when the host is resolved or a timeout reached. Either way, when the
the callback is run, the resolving stops. If you wish to stop resolving early,
you can use rb_daap_mdns_resolve_cancel().
Publishing is done via rb_daap_mdns_publish(). The callback for this function
is used to notify you of success or failure due to name collision. Two servers
cannot have the same name. Publishing will continue until
rb_daap_mdns_publish_cancel() is called.
3. Discovering new music shares
Discovery is started in shell/rb-shell.c:rb_shell_constructor().
rb_daap_sources_init() is called, which starts the discovery. When new shares
are discovered, an RBDAAPSource is created and added to the source list. When
shares disappear from the network, the source is removed from the list. No
attempt is made to resolve or connect to the share until the user clicks on the
source. This reduces unneccessary computations and network traffic.
4. Connecting to a music share
When a user clicks on a music share source in the list,
rb-shell.c:rb_shell_select_source() runs the virtual function
rb_source_activate(). This tells the RBDAAPSource to resolve the server via
RBDAAPmDNSResolver. In the resolver's callback function, if the resolve was
successful, an RBDAAPConnection is created to the remote share.
RBDAAPConnection takes care of all the dirty work establishing a connection and
recieving a list of the share's music & playlists. The HTTP communication is
done via libsoup. There are 5 steps to establishing a new connection. In each
several HTTP requests are made, and a DAAP structure returned. The structure
is parsed (see RBDAAPStructure) and the appropriate information read out of
the parsed structure.
1. Get the server's version (/server-info)
Here we request /server-info to figure out what version of DAAP the
server is using. Different versions requiring different iTunes hashing
techniques.
2. Login (/login & /update)
Here we request /login to recieve a session-id. All subsequent requests
must include the session-id as part of the URI (?session-id=X). We also request
the initial revision-number. Revision numbers are used to track changes in
the server's music library. RHYTHMBOX DOES NOT YET SUPPORT TRACKING THESE
CHANGES. The session-id is used by the server to identify your connection.
3. Get the database information (/databases)
We request /databases to get a list of databases on the server. A list
is returned, but it is always only 1 element long, containing the ID number
of the music library on the server.
4. Get the song listing (/databases/ID/items)
We request /database/ID/items with a list of the metadata (artist,
album, title, etc) that we're interested in. A lengthy list of all the songs in
the database and their metadata is returned. Each song has a unique ITEM-ID
number in the database. The URI for requesting the actual song is
/databases/ID/items/ITEM-ID.ITEM-FORMAT. We create a RhythmDBEntry for each
song, and add it to the RhythmDB. A local hash table translating ITEM-ID to
uri is also populated (this will be used in step 5).
5. Get the playlists (/databases/ID/containers &
/databases/ID/containers/CONTAINER-ID/items)
We request /databases/ID/containers to recieve a list of playlists. Each
playlist has a name and a unique CONTAINER-ID number. For each palylist, a
request of /databases/ID/containers/CONTAINER-ID/items is made, asking for the
ITEM-IDs of all the songs in the playlist. Each of these ITEM-IDs is resolved
into the uri using the hash table populated in step 4, and the uri add to the
RBDAAPPlaylist.
After the connection is made and all the data transfered, a new
RBDAAPPlaylistSource is created for each RBDAAPPlaylist in the connection.
These are added underneath the original RBDAAPSource.
4. Playing remote music
A URI for a DAAP music file looks like
daap://HOST:PORT/databases/DATABASE-ID/items/ITEM-ID.ITEM-FORMAT?session-id=SESSION-ID
A new GStreamer source, RBDAAPSrc (existing in daapsharing/rb-daap-src.c/h)
handles playing daap:// URIs. It uses simple TCP sockets to open a connection
to the host, request the file (with the appropriate headers) and push the
recieved data through the pipeline. The data recieved (the song) is in the
original format (be it mp3, ogg, aac, wav, whatever). So GStreamer needs to
figure out how to decode the file before it pushes it the rest of the way
through the pipeline. To do this, GStreamer uses its decodebin & typefind
elements.
Every request for a song (even re-requests) must include several special
headers, including the iTunes hash and a DAAP-Request-ID. The Request-ID is
simply a running tally of the number of songs we've requested. It starts at 1,
and every time there is a new request it is incremented. To determine the
proper headers to send, RBDAAPsrc uses rb_daap_source_find_for_uri() and
rb_daap_source_get_headers() which find the RBDAAPSource that contains a
specific URI and fetch the headers needed for that URI.
rb_daap_source_get_headers() hands the work off to
rb_daap_connection_get_headers(), which keeps track of the Request-ID and
determines the iTunes hash.
Seeking in a DAAP stream is done by closing the existing connection and
establishing a new one, sending a Range: HTTP header with the appropriate
starting point. However, we do not support standard GStreamer seeking.
This is because, if we were to, GStreamer's decodebin & typefind elements
abuse it, seeking several times in succession to determine the type of data
coming through the pipeline. These requests will overwhelm an iTunes server,
prompting it to return an error after several of them. To work around this,
RBPlayer handles seeking in DAAP streams via rb_daap_src_[set/get]_time().
This is a WORKAROUND until GStreamer is fixed or proper seeking can be figured
out.
5. Publishing your music
Publishing is started in shell/rb-shell.c:rb_shell_constructor() via the
rb_daap_sharing_init() call. This creates an RBDAAPShare structure, which, in
turn, creates a SoupServer structure to handle HTTP requests. The server binds
on an available port, and this port along with the machine's IP address are
published to mDNS/DNS-SD via RBDAAPmDNSPublisher.
RBDAAPShare does several things. It assigns unique ID numbers to every song
in the RhythmDB, and keeps an up to date mapping of RhythmDBEntry to ID and ID
to RhythmDBEntry via entry-added & entry-deleted signals from the RhythmDB.
THIS WILL EVENTUALLY BE USED TO SUPPORT SENDING MUSIC LIBRARY UPDATES.
For each request on the SoupServer, an appropriate DAAP response is built (see
RBDAAPStructure) and returned. If the request is for a song file itself, the
file is opened via GnomeVFS and its contents returned. Rhythmbox's DAAP server
implementation does not require iTunes hashing headers nor does it require
unique session-ids.
6. RBDAAPStructure - structure parsing & building
RBDAAPStructure is a data structure that handles parsing and building DAAP
responses. It lives in daapsharing/rb-daap-structure.c/h.
Useful reading:
http://www.deleet.de/projekte/daap/ The section titled 'DAAPD technicals'
http://molelog.molehill.org/blox/Computers/Macintosh/DAAP3.writeback (#1&2 also)
A DAAP response is of the form:
Byte: 1234 | 5678 | length bytes
Data: content code | content length | content
Codes are 4 byte character strings like 'msrv', 'mstt', 'mpro', and so on.
A list of codes is available in the RBDAAPContentCode enumeration in
rb-daap-structure.h. They are defined in the RBDAAPContentCodeDefintion array
in rb-daap-structure.c. There are several DAAP types. You can ask the
DAAP server for /content-codes to retrieve a dictionary mapping codes to types,
or you can "just know."
Type | C data type | Size
------------------------------
Byte | gchar | 1
Signed Int | gchar | 1
Short | gint16 | 2
Int | gint32 | 4
Date | gint32 | 4
Int64 | gint64 | 8
Version | gdouble | 4
String | gchar * | Whatever the DAAP response says
Container | GNode * | Whatever the DAAP response says
Containers hold other data. For example, the response from /server-info is:
dmap.serverinforesponse Container
dmap.status Int
dmap.protocolversion Version
daap.protocolversion Version
daap.itemname String
dmap.loginrequired Int
...
dmap.databasescoutn Int
The serverinforesponse is a Container holding the other content.
The bulk of the parsing work is done in
rb_daap_structure_parse_container_buffer() The data & the length are passed in,
as well as a GNode * for the parent container. The first pass is provided an
empty container to add all the "toplevel" content to. The 4 byte content code
is read and the daap type looked up accordingly. The 4 byte data length
(code size) is read. The data is read into a GValue, with its type set
accordingly. The GValue & content code are added to the GNode * as new
children. If type is a container, rb_daap_structure_parse_container_buffer() is
called recursively.
There are find functions rb_daap_structure_find_item() &
rb_daap_structure_find_node() which take a GNode * structure and content code
to be located. The structure is walked until a node matching the content code
is found. The node or the item is returned, depending on the function being
used.
To build a DAAP structure you use rb_daap_structure_add(). It creates a new
node containing the data you add and appends it to the parent node you pass.
You pass NULL for the parent to create a toplevel node. The new node is
returned. The data you add is passed in as a content code and then the data.
If the content type is Container, you don't need to pass any data. Once the
DAAP structure is build, use rb_daap_structure_serialize() to convert it to
a string appropriate for sending over a socket.