Repository navigation
Communication DLNA
DLNA (UPnP AV) is how media players, NAS systems, smart TVs and phone apps find each other in the home network and play media on each other. With arduino-dlna (install it) your microcontroller can become a DLNA media renderer: it appears as a speaker in DLNA control apps (e.g. BubbleUPnP, foobar2000 or the media server of your NAS), which tell it which URL to play, and arduino-audio-tools plays it. It can also be a DLNA media server that provides a list of internet radios or the files of its SD card to the players in your network. See Communication for the general considerations.
From dlna-audio-renderer.ino: arduino-dlna handles the DLNA protocol, and reports what the controller wants as events; the sketch implements them with an AudioPlayer that plays URLs:
#include "WiFi.h"
#include "dlna.h" // https://github.com/pschatzmann/arduino-dlna
#include "AudioTools.h"
#include "AudioTools/AudioLibs/AudioBoardStream.h"
#include "AudioTools/Communication/AudioHttp.h"
#include "AudioTools/Disk/AudioSourceURL.h"
#include "AudioTools/AudioCodecs/CodecHelix.h"
#include "AudioTools/Concurrency/RTOS.h"
#include "AudioTools/Concurrency/AudioPlayerThreadSafe.h"
// DLNA
const int port = 9000;
WiFiServer wifi(port);
HttpServer<WiFiClient, WiFiServer> server(wifi);
UDPService<WiFiUDP> udp;
DLNAMediaRenderer<WiFiClient> media_renderer(server, udp);
// AudioPlayer that plays the URL it gets from the controller
URLStream url;
AudioSourceDynamicURL source(url);
AudioBoardStream i2s(AudioKitEs8388V1);
MultiDecoder multi_decoder;
AACDecoderHelix dec_aac;
MP3DecoderHelix dec_mp3;
WAVDecoder dec_wav;
AudioPlayer player(source, i2s, multi_decoder);
// the DLNA events come from another task: use a thread-safe player
QueueRTOS<AudioPlayerCommand> queue(20, portMAX_DELAY, 5);
AudioPlayerThreadSafe<QueueRTOS> player_save(player, queue);
Task dlna_task("dlna", 8000, 10, 0);
void handleMediaEvent(MediaEvent ev, DLNAMediaRenderer<WiFiClient>& mr) {
switch (ev) {
case MediaEvent::SET_URI:
source.clear();
player_save.setPath(mr.getCurrentUri());
player_save.begin(0, media_renderer.isActive());
break;
case MediaEvent::PLAY: player_save.setActive(true); break;
case MediaEvent::PAUSE: player_save.setActive(false); break;
case MediaEvent::STOP: player_save.end(); url.end(); break;
case MediaEvent::SET_VOLUME: player_save.setVolume(mr.getVolume() / 100.0); break;
case MediaEvent::SET_MUTE: player_save.setMuted(mr.isMuted()); break;
default: break;
}
}In setup() the decoders are registered with the MultiDecoder (audio/aac, audio/mpeg, audio/wav), the renderer is started with media_renderer.setBaseURL(WiFi.localIP(), port), setMediaEventHandler(handleMediaEvent) and begin(), and a task runs media_renderer.loop(). When a file has finished, the example reports it with media_renderer.setPlaybackCompleted() (see the full example).
A DLNAMediaServer announces itself in the network, and DLNA players and control apps (e.g. VLC, BubbleUPnP, Kodi, smart TVs - or an Arduino renderer from above) can browse its content and play it. The server only provides the directory: each item has a URL, which the player fetches directly. The media server is part of arduino-dlna and does not need arduino-audio-tools.
You provide the content with two callbacks:
-
setPrepareDataCallback(): called for each Browse or Search request. Select the matching items: forBrowseChildrenthe children ofobjectID, forBrowseMetadatathe itemobjectIDitself, forSearchthe items that match the filter. Report how many there are innumberReturnedandtotalMatches. -
setGetDataCallback(): returns the selected items one by one.
From device-media-server.ino, which provides a folder of internet radios:
#include "WiFi.h"
#include "dlna.h" // https://github.com/pschatzmann/arduino-dlna
const int port = 9000;
WiFiServer wifi(port);
HttpServer<WiFiClient, WiFiServer> server(wifi);
UDPService<WiFiUDP> udp;
DLNAMediaServer<WiFiClient> mediaServer(server, udp);
// id, parent id, restricted, title, URL, mime type, class
const MediaItem items[] = {
{"1", "0", true, "Radios", "", "", MediaItemClass::Folder},
{"2", "1", true, "Blues on Radio (MP3)", "https://0n-blues.radionetz.de/0n-blues.mp3", "audio/mpeg", MediaItemClass::Radio},
{"3", "1", true, "Blues on Radio (AAC)", "https://0n-blues.radionetz.de/0n-blues.aac", "audio/aac", MediaItemClass::Radio},
};
std::vector<MediaItem> result_items;
void prepareData(const char* objectID, ContentQueryType queryType,
const char* filter, int startingIndex, int requestedCount,
const char* sortCriteria, int& numberReturned,
int& totalMatches, int& updateID, void* reference) {
result_items.clear();
for (auto& item : items) {
switch (queryType) {
case ContentQueryType::Search:
if (item.itemClass != MediaItemClass::Folder) result_items.push_back(item);
break;
case ContentQueryType::BrowseMetadata:
if (StrView(item.id).equals(objectID)) result_items.push_back(item);
break;
case ContentQueryType::BrowseChildren:
if (StrView(item.parentID).equals(objectID)) result_items.push_back(item);
break;
}
}
numberReturned = result_items.size();
totalMatches = result_items.size();
}
bool getData(int index, MediaItem& item, void* reference) {
if (index < 0 || index >= result_items.size()) return false;
item = result_items[index];
return true;
}
void setup() {
// ... connect WiFi
mediaServer.setFriendlyName("ArduinoMediaServer"); // the name the players show
mediaServer.setBaseURL(WiFi.localIP(), port);
mediaServer.setPrepareDataCallback(prepareData);
mediaServer.setGetDataCallback(getData);
mediaServer.begin();
}
void loop() { mediaServer.loop(); }The root folder has the id "0". MediaItemClass tells the player what an item is: Folder, Radio, Music ...
To provide the files of an SD card, the device-media-server-sdfat example uses the SdFatContentProvider (tools/SdFatContentProvider.h): it scans the file system and implements both callbacks. The URLs of the items point back to the ESP32, so the sketch also needs an HTTP handler that sends the file:
SdFs sd;
FsFile file;
SdFatContentProvider<SdFs> contentProvider;
// GET /files?ID=<id>: send the file with this id
void handleFileRequest(IHttpServer* server_ptr, const char* requestPath, HttpRequestHandlerLine* hl) {
StrView path{requestPath};
int id = StrView(path.c_str() + path.indexOf("ID=") + 3).toInt();
TreeNode& node = contentProvider.getTreeNodeById(id);
file.close();
file = sd.open(node.path().c_str(), O_READ);
if (!file) { server_ptr->replyNotFound(); return; }
server_ptr->reply(node.getMime().c_str(), file, file.size(), 200);
}
void setup() {
// ... connect WiFi, sd.begin(...)
mediaServer.setBaseURL(WiFi.localIP(), port);
mediaServer.setFriendlyName("SdFat Media Server");
contentProvider.begin(sd, "/");
contentProvider.setPath("files");
server.on("/files*", T_GET, "text/html", handleFileRequest);
mediaServer.setPrepareDataCallback([](const char* objectID, ContentQueryType queryType, const char* filter,
int startingIndex, int requestedCount, const char* sortCriteria, int& numberReturned,
int& totalMatches, int& updateID, void* reference) {
contentProvider.prepareData(objectID, queryType, filter, startingIndex, requestedCount,
sortCriteria, numberReturned, totalMatches, updateID, reference);
});
mediaServer.setGetDataCallback([](int index, MediaItem& item, void* reference) -> bool {
return contentProvider.getData(index, item, reference);
});
mediaServer.begin();
}There are also examples for Ethernet and for running the server in a separate task.
-
Formats: the controller may send any format the media server provides - register a decoder for each format you want to support; a
MultiDecoderselects it from the content type. -
Threads: DLNA requests arrive in their own task while the audio is played in
loop(): change the player only viaAudioPlayerThreadSafe. - Media server and renderer together: a media server serves the files, a renderer plays them, and a control point (e.g. a phone app) connects the two. arduino-dlna also provides control points, see arduino-dlna/examples.
- Memory: a media server with a big SD card keeps the directory tree in memory: use PSRAM or keep the number of files small.