Arduino DLNA Server
Loading...
Searching...
No Matches
MediaServer.h
Go to the documentation of this file.
1// Minimal header-only Digital Media Server (MediaServer) device
2#pragma once
3
4#include "MediaItem.h"
6#include "basic/Str.h"
7#include "basic/StrPrint.h"
8#include "dlna/DLNADevice.h"
10#include "dlna/xml/XMLPrinter.h"
11#include "http/HttpServer.h"
12#include "ms_connmgr.h"
13#include "ms_content_dir.h"
14
15namespace tiny_dlna {
16
42 public:
43 // MediaItem is now defined in MediaItem.h
44
45 // PrepareData callback signature:
46 // objectID, browseFlag, filter, startingIndex, requestedCount, sortCriteria,
47 // numberReturned (out), totalMatches (out), updateID (out), reference
48 typedef void (*PrepareDataCallback)(
49 const char* objectID, const char* browseFlag, const char* filter,
50 int startingIndex, int requestedCount, const char* sortCriteria,
51 int& numberReturned, int& totalMatches, int& updateID, void* reference);
52
53 // GetData callback signature:
54 // index, MediaItem (out), reference
55 typedef bool (*GetDataCallback)(int index, MediaItem& item, void* reference);
56
57 // (No default GetDataCallback — user must register one to serve items.)
58
60 DlnaLogger.log(DlnaLogLevel::Info, "MediaServer::MediaServer");
63 setFriendlyName("MediaServer");
64 setManufacturer("TinyDLNA");
65 setModelName("TinyDLNA MediaServer");
66 setModelNumber("1.0");
67 setBaseURL("http://localhost:44757");
68 }
69
71
72 void end() { /* nothing special */ }
73
74 void setupServices(HttpServer& server, IUDPService& udp) {
75 setupServicesImpl(&server);
76 p_server = &server;
77 }
78
81
84
86 void setReference(void* ref) { reference_ = ref; }
87
90
91 protected:
92 const char* st = "urn:schemas-upnp-org:device:MediaServer:1";
93 const char* usn = "uuid:media-server-0000-0000-0000-000000000001";
94 // stored callbacks
97 HttpServer* p_server = nullptr;
98 void* reference_ = nullptr;
99 // Globals used by the streaming callback. These are set immediately
100 // before calling server->reply(...) and cleared afterwards. This is
101 // safe in the single-threaded server model.
102 static inline GetDataCallback g_stream_get_data_cb = nullptr;
103 static inline int g_stream_numberReturned = 0;
104 static inline int g_stream_totalMatches = 0;
105 static inline int g_stream_updateID = 0;
106 static inline void* g_stream_reference = nullptr;
107
109 DlnaLogger.log(DlnaLogLevel::Info, "MediaServer::setupServices");
110
111 auto contentDescCB = [](HttpServer* server, const char* requestPath,
113 server->reply("text/xml",
114 [](Print& out) { ms_content_dir_xml_printer(out); });
115 };
116 auto connDescCB = [](HttpServer* server, const char* requestPath,
118 server->reply("text/xml",
119 [](Print& out) { ms_connmgr_xml_printer(out); });
120 };
121
123 cd.setup("urn:schemas-upnp-org:service:ContentDirectory:1",
124 "urn:upnp-org:serviceId:ContentDirectory", "/CD/service.xml",
125 contentDescCB, "/CD/control", contentDirectoryControlCB,
126 "/CD/event",
127 [](HttpServer* server, const char* requestPath,
128 HttpRequestHandlerLine* hl) { server->replyOK(); });
129
131 cm.setup("urn:schemas-upnp-org:service:ConnectionManager:1",
132 "urn:upnp-org:serviceId:ConnectionManager", "/CM/service.xml",
133 connDescCB, "/CM/control", connmgrControlCB, "/CM/event",
134 [](HttpServer* server, const char* requestPath,
135 HttpRequestHandlerLine* hl) { server->replyOK(); });
136
137 addService(cd);
138 addService(cm);
139 }
140
143 static const char* replyTemplate() {
144 static const char* tpl =
145 "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\""
146 " s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
147 "<s:Body><u:%1 "
148 "xmlns:u=\"urn:schemas-upnp-org:service:%2:1\">%3</u:%1></s:Body></"
149 "s:Envelope>";
150 return tpl;
151 }
152
153 // Control handler for ContentDirectory service
155 const char* requestPath,
157 Str soap = server->contentStr();
158 DlnaLogger.log(DlnaLogLevel::Info, "ContentDirectory Control: %s",
159 soap.c_str());
160 Str reply_str{replyTemplate()};
161 reply_str.replaceAll("%2", "ContentDirectory");
162
163 // Handle only Browse for now
164 if (soap.indexOf("Browse") < 0) {
165 reply_str.replaceAll("%1", "ActionResponse");
166 reply_str.replaceAll("%3", "");
167 server->reply("text/xml", reply_str.c_str());
168 return;
169 }
170
171 // Extract common arguments
172 auto extractArg = [&](const char* tag) -> Str {
173 Str result;
174 int pos = soap.indexOf(tag);
175 if (pos >= 0) {
176 int start = soap.indexOf('>', pos);
177 if (start >= 0) {
178 start += 1;
179 int end = soap.indexOf("</", start);
180 if (end > start) result = soap.substring(start, end);
181 }
182 }
183 return result;
184 };
185
186 Str obj = extractArg("ObjectID");
187 Str flag = extractArg("BrowseFlag");
188 Str filter = extractArg("Filter");
189 Str startIdx = extractArg("StartingIndex");
190 Str reqCount = extractArg("RequestedCount");
191 Str sort = extractArg("SortCriteria");
192
193 int startingIndex = startIdx.isEmpty() ? 0 : startIdx.toInt();
194 int requestedCount = reqCount.isEmpty() ? 0 : reqCount.toInt();
195
196 // collect results using callback or default
197 MediaServer* ms = nullptr;
198 if (hl && hl->context[0]) ms = (MediaServer*)hl->context[0];
199
200 int numberReturned = 0;
201 int totalMatches = 0;
202 int updateID = 1;
203 if (ms && ms->prepare_data_cb) {
204 ms->prepare_data_cb(obj.c_str(), flag.c_str(), filter.c_str(),
205 startingIndex, requestedCount, sort.c_str(),
206 numberReturned, totalMatches, updateID,
207 ms->reference_);
208 } else {
209 numberReturned = 0;
210 totalMatches = 0;
211 updateID = 0;
212 }
213
214 // Stream response via Print-callback and generate DIDL on-the-fly.
215 // Set globals that the callback will read while running synchronously.
216 g_stream_numberReturned = numberReturned;
217 g_stream_totalMatches = totalMatches;
218 g_stream_updateID = updateID;
219 g_stream_get_data_cb = ms ? ms->get_data_cb : nullptr;
220 g_stream_reference = ms ? ms->reference_ : nullptr;
221 server->reply("text/xml", streamReplyCallback);
222 // clear globals after reply returns
226 g_stream_get_data_cb = nullptr;
227 g_stream_reference = nullptr;
228 }
229
230 // helper to write text content with minimal XML-escaping for &, < and >
231 static void writeEscapedText(Print& out, const char* s) {
232 if (!s) return;
233 for (const char* p = s; *p; ++p) {
234 if (*p == '&')
235 out.print("&amp;");
236 else if (*p == '<')
237 out.print("&lt;");
238 else if (*p == '>')
239 out.print("&gt;");
240 else
241 out.write((const uint8_t)*p);
242 }
243 }
244
245 // streaming reply callback: writes the full SOAP envelope and escapes
246 // DIDL tags while streaming the items from g_stream_items.
247 static void streamReplyCallback(Print& out) {
248 XMLPrinter xml(out);
249 // Envelope start with namespace and encoding attribute
250 const char* envAttrs =
251 "xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" "
252 "s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"";
253 xml.printNodeBegin("Envelope", envAttrs, "s");
254 xml.printNodeBeginNl("Body", nullptr, "s");
255
256 // BrowseResponse with its namespace declaration
257 const char* brAttrs =
258 "xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\"";
259 xml.printNodeBeginNl("BrowseResponse", brAttrs, "u");
260
261 // Result node: use callback to stream escaped DIDL
262 xml.printNode("Result", [&]() -> size_t {
264 return 0;
265 });
266
267 // numeric fields
268 xml.printNode("NumberReturned", g_stream_numberReturned);
269 xml.printNode("TotalMatches", g_stream_totalMatches);
270 xml.printNode("UpdateID", g_stream_updateID);
271
272 xml.printNodeEnd("BrowseResponse", "u");
273 xml.printNodeEnd("Body", "s");
274 xml.printNodeEnd("Envelope", "s");
275 }
276
277 // helper: stream DIDL-Lite (escaped) for the provided items to the Print
279 int count) {
280 EscapingPrint esc(out);
281 XMLPrinter didl(esc);
282
283 const char* didlAttrs =
284 "xmlns:dc=\"http://purl.org/dc/elements/1.1/\" "
285 "xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" "
286 "xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\"";
287 didl.printNodeBeginNl("DIDL-Lite", didlAttrs);
288
289 if (get_data_cb) {
290 for (int i = 0; i < count; ++i) {
291 MediaItem item;
292 if (!get_data_cb(i, item, g_stream_reference)) break;
293 char itemAttr[256];
294 snprintf(itemAttr, sizeof(itemAttr),
295 "id=\"%s\" parentID=\"%s\" restricted=\"%d\"",
296 item.id ? item.id : "", item.parentID ? item.parentID : "0",
297 item.restricted ? 1 : 0);
298 didl.printNodeBeginNl("item", itemAttr);
299
300 // title
301 didl.printNode("dc:title", item.title ? item.title : "");
302
303 // res with optional protocolInfo attribute
304 if (item.mimeType) {
305 char resAttr[128];
306 snprintf(resAttr, sizeof(resAttr), "protocolInfo=\"%s\"",
307 item.mimeType);
308 didl.printNode("res", item.res ? item.res : "", resAttr);
309 } else {
310 didl.printNode("res", item.res ? item.res : "");
311 }
312
313 didl.printNodeEnd("item");
314 }
315 } // end if get_data_cb
316 didl.printNodeEnd("DIDL-Lite");
317 }
318
319 // simple connection manager control that replies OK
320 static void connmgrControlCB(HttpServer* server, const char* requestPath,
322 // For simplicity we'll return an empty ActionResponse
323 Str reply_str{replyTemplate()};
324 reply_str.replaceAll("%2", "ConnectionManager");
325 reply_str.replaceAll("%1", "ActionResponse");
326 reply_str.replaceAll("%3", "");
327 server->reply("text/xml", reply_str.c_str());
328 }
329};
330
331} // namespace tiny_dlna
Device Attributes and generation of XML using urn:schemas-upnp-org:device-1-0. We could just return a...
Definition: DLNADeviceInfo.h:27
void setDeviceType(const char *st)
Definition: DLNADeviceInfo.h:48
void addService(DLNAServiceInfo s)
Adds a service definition.
Definition: DLNADeviceInfo.h:138
void setModelName(const char *name)
Definition: DLNADeviceInfo.h:127
void setSerialNumber(const char *sn)
Definition: DLNADeviceInfo.h:131
void setManufacturer(const char *man)
Definition: DLNADeviceInfo.h:121
void setFriendlyName(const char *name)
Definition: DLNADeviceInfo.h:119
XMLPrinter xml
Definition: DLNADeviceInfo.h:195
void setModelNumber(const char *number)
Definition: DLNADeviceInfo.h:129
void setBaseURL(const char *url)
Defines the base url.
Definition: DLNADeviceInfo.h:59
Attributes needed for the DLNA Service Definition.
Definition: DLNAServiceInfo.h:16
void setup(const char *type, const char *id, const char *scp, http_callback cbScp, const char *control, http_callback cbControl, const char *event, http_callback cbEvent)
Definition: DLNAServiceInfo.h:19
Used to register and process callbacks.
Definition: HttpRequestHandlerLine.h:19
void ** context
Definition: HttpRequestHandlerLine.h:39
A Simple Header only implementation of Http Server that allows the registration of callback functions...
Definition: HttpServer.h:24
void replyOK()
write OK reply with 200 SUCCESS
Definition: HttpServer.h:363
Str contentStr()
converts the client content to a string
Definition: HttpServer.h:450
void reply(const char *contentType, Stream &inputStream, int size, int status=200, const char *msg=SUCCESS)
write reply - copies data from input stream with header size
Definition: HttpServer.h:293
Abstract Interface for UDP API.
Definition: IUDPService.h:34
void log(DlnaLogLevel current_level, const char *fmt...)
Print log message.
Definition: Logger.h:40
Minimal Digital Media Server implementation.
Definition: MediaServer.h:41
static GetDataCallback g_stream_get_data_cb
Definition: MediaServer.h:102
void(* PrepareDataCallback)(const char *objectID, const char *browseFlag, const char *filter, int startingIndex, int requestedCount, const char *sortCriteria, int &numberReturned, int &totalMatches, int &updateID, void *reference)
Definition: MediaServer.h:48
void setPrepareDataCallback(PrepareDataCallback cb)
Sets the PrepareData callback.
Definition: MediaServer.h:80
bool(* GetDataCallback)(int index, MediaItem &item, void *reference)
Definition: MediaServer.h:55
HttpServer * getHttpServer()
Provides access to the http server.
Definition: MediaServer.h:89
const char * usn
Definition: MediaServer.h:93
PrepareDataCallback prepare_data_cb
Definition: MediaServer.h:95
~MediaServer()
Definition: MediaServer.h:70
void end()
Definition: MediaServer.h:72
static const char * replyTemplate()
Definition: MediaServer.h:143
HttpServer * p_server
Definition: MediaServer.h:97
static int g_stream_numberReturned
Definition: MediaServer.h:103
MediaServer()
Definition: MediaServer.h:59
static void contentDirectoryControlCB(HttpServer *server, const char *requestPath, HttpRequestHandlerLine *hl)
Definition: MediaServer.h:154
static void * g_stream_reference
Definition: MediaServer.h:106
static void streamReplyCallback(Print &out)
Definition: MediaServer.h:247
void setupServices(HttpServer &server, IUDPService &udp)
to be implemented by subclasses
Definition: MediaServer.h:74
void setGetDataCallback(GetDataCallback cb)
Sets the GetData callback.
Definition: MediaServer.h:83
const char * st
Definition: MediaServer.h:92
static int g_stream_updateID
Definition: MediaServer.h:105
static void connmgrControlCB(HttpServer *server, const char *requestPath, HttpRequestHandlerLine *hl)
Definition: MediaServer.h:320
static int g_stream_totalMatches
Definition: MediaServer.h:104
void * reference_
Definition: MediaServer.h:98
static void writeEscapedText(Print &out, const char *s)
Definition: MediaServer.h:231
void setupServicesImpl(HttpServer *server)
Definition: MediaServer.h:108
GetDataCallback get_data_cb
Definition: MediaServer.h:96
static void buildEscapedDIDL(Print &out, GetDataCallback get_data_cb, int count)
Definition: MediaServer.h:278
void setReference(void *ref)
Sets a user reference pointer, available in callbacks.
Definition: MediaServer.h:86
virtual bool isEmpty()
checks if the string is empty
Definition: StrView.h:383
virtual int indexOf(const char c, int start=0)
Definition: StrView.h:275
int toInt()
Converts the string to an int.
Definition: StrView.h:596
String implementation which keeps the data on the heap. We grow the allocated memory only if the copy...
Definition: Str.h:22
const char * c_str()
provides the string value as const char*
Definition: Str.h:187
Str substring(int start, int end)
copies a substring into the current string
Definition: Str.h:195
Definition: Allocator.h:6
LoggerClass DlnaLogger
Definition: Logger.cpp:5
Print wrapper that escapes & < > while forwarding to an underlying Print.
Definition: EscapingPrint.h:7
Media item description used to build DIDL-Lite entries.
Definition: MediaItem.h:6
const char * parentID
Definition: MediaItem.h:8
const char * res
Definition: MediaItem.h:11
const char * mimeType
Definition: MediaItem.h:12
const char * id
Definition: MediaItem.h:7
bool restricted
Definition: MediaItem.h:9
const char * title
Definition: MediaItem.h:10
Functions to efficiently output XML. XML data contains a lot of redundancy so it is more memory effic...
Definition: XMLPrinter.h:31
size_t printNodeBegin(const char *node, const char *attributes=nullptr, const char *ns=nullptr)
Definition: XMLPrinter.h:97
size_t printNodeBeginNl(const char *node, const char *attributes=nullptr, const char *ns=nullptr)
Definition: XMLPrinter.h:115
size_t printNode(XMLNode node)
Definition: XMLPrinter.h:43
size_t printNodeEnd(const char *node, const char *ns=nullptr)
Definition: XMLPrinter.h:122