ptstream / QtUPnP

QtUPnP framework is a C++ framework, based on QT5, to build easily an UPnP control point. It focuses on the UPnP/AV standards.
GNU General Public License v3.0
48 stars 10 forks source link

Faster audio contents discovery #20

Closed MarkoSan closed 4 years ago

MarkoSan commented 4 years ago

Dear Sirs and Madams!

I've managed to run Qt/QML based project with your library included on phyWega board from Phytec and audio contents discovery works fine, however, my Synology DS918+ and its audio contents (for test, about 10 mp3 files) discovery takes way too much time - about 5 minutes. Is there any way to optimize situation?

ptstream commented 4 years ago

Hi Marko, I'm sorry for the problem you encountered. I have never tested this project on this type of machine and especially with so little memory. The smallest computer tested is my Raspberry PI3 under Raspbian Stretch. My tests were done on:

  1. Computer • Raspberry PI3 standard • Memory 1Gbs • CPU Broadcom BCM2837B0, Cortex-A53 (ARMv8) 64-bit SoC @ 1.4GHz • Link Wifi

  2. Server • Synology DS214 • Synology Multimedia server in standard configuration. • CPU MARVELL Armada XP MV78230 • Memory 512Gbs • System DSM 6.2.2-24922 Update 4

The results are: • For 31 audio flac files the time is less than 1s. • For the folder "All music" (12868 files) the time is less than 40s without the cover images.

The only problem is for this server, the download of images in the audio files is very slow.

For your problem I can see some research tracks.

  1. It would seem that the memory installed on phyBOARD-Wega is 256 MB DDR3 RAM. May be it is to small for QML/QT. For example, the application AIVCtrl available at the same place on GitHub take more of 200Mo with the graphic interface. Qt is very powerful but is not really memory efficient and especially with QML.
  2. Try to change the Multimedia server parameters. For example, disable the cover image and/or download in low resolution.

I am sorry for the poor help. Patrice

MarkoSan commented 4 years ago

Hmm, I have done several project using this board with Qt/QML and it worked fine. The exact problem is that my slot for discovering new devices takes too long to execute and maybe the way I search for mp3 files is wrong:

void AudioInputsManager::slotNewDevice(const QString& deviceUUID)
{
    QtUPnP::CDevice foundDevice=this->controlPoint->device(deviceUUID);
    const QMap<QString, QtUPnP::CService>& services=foundDevice.services();
    QString text=QString("New device (%1) found: %2").arg(deviceUUID)
                                                     .arg(foundDevice.name());
    QMapIterator<QString, QtUPnP::CService> servicesIterator(services);

    while(servicesIterator.hasNext())
    {
        servicesIterator.next();

        const QtUPnP::TMActions& actions=servicesIterator.value().actions();

        if(actions.contains("Browse"))
        {
            QtUPnP::CDidlItem didlItem;
            QtUPnP::CDidlElem didlElement;
            QString id=didlItem.isEmpty()?"0":didlItem.value("container",
                                                             "id");
            QtUPnP::CBrowseReply browseReply;
            QtUPnP::CContentDirectory cd(this->controlPoint);
            QList<QtUPnP::CDidlItem> didlItems;

            if(!id.isEmpty())
            {
                quint16 browseReplyIndex=0;

                browseReply=cd.browse(deviceUUID,
                                        id);
                didlItems=browseReply.items();

                for(browseReplyIndex=0; browseReplyIndex< didlItems.size(); browseReplyIndex++)
                {
                    didlItem=didlItems.at(browseReplyIndex);
                    didlElement=didlItem.value("dc:title");

                    if(didlElement.value()=="Music")
                    {
                        id=didlItem.value("container",
                                          "id");

                        browseReply=cd.browse(deviceUUID,
                                              id);
                        didlItems=browseReply.items();

                        for(browseReplyIndex=0; browseReplyIndex<didlItems.size(); browseReplyIndex++)
                        {
                            didlItem=didlItems.at(browseReplyIndex);
                            didlElement=didlItem.value("dc:title");

                            if(didlElement.value()=="All music")
                            {
                                id=didlItem.value("container",
                                                  "id");

                                browseReply=cd.browse(deviceUUID,
                                                      id);
                                didlItems=browseReply.items();

                                for(quint16 audioContentsIndex=0; audioContentsIndex<didlItems.size(); audioContentsIndex++)
                                {
                                    QtUPnP::CDidlElem didlCreator;
                                    QtUPnP::CDidlElem didlTitle;

                                    didlItem=didlItems.at(audioContentsIndex);
                                    didlCreator=didlItem.value("dc:creator");
                                    didlTitle=didlItem.value("dc:title");

                                    qDebug() << COLOR_DEBUG
                                             << Q_FUNC_INFO
                                             << didlItem.dump()
                                             << " "
                                             << didlCreator.value()
                                             << " "
                                             << didlTitle.value();

                                    AudioContentsRecord audioContentsRecord(didlCreator.value(),
                                                                            didlTitle.value());

                                    this->audioContentsModel->slotAddAudioContents(audioContentsRecord);
                                }   // for

                                break;
                            }   // if
                        }   // for
                    }   // if
                }   // for
            }   // if

            this->audioDevicesModel->slotAddAudioDevice(foundDevice.uuid(),
                                                        foundDevice.name());
        }   // if
    }   // while

    qDebug() << COLOR_DEBUG
             << Q_FUNC_INFO
             << text
             << COLOR_RESET;
}   // slotNewDevice

It might also be some settings to tweak regarding network settings of board, they are suspicous ... checking ... will report ...

ptstream commented 4 years ago

I don't understand the link between new device and browse.

This two thinks are totally independent.

  1. CControlPoint:: discover or CControlPoint::avdiscover must be call first. These two functions initialize the discovery of the UPnP devices on the network. From this moment, everything is asynchronous. The software must wait for the desired device to launch action (e.g. browse). It is not possible to know when they will respond (it is the parts unicast and multicast of the UPnP). If you have connected CControlPoint::newDevice signal, your slot can retrieve the new device properties (server, renderer…). In the chupnp example, CControlPoint:: discover is call once at startup.

  2. When a device is correctly detected and if the device is not lost, all launched actions are synchronous. You can call CContentDirectory::browse to browse the content of the audio server. At the return of this function the stucture containing the server data is valid.

ptstream commented 4 years ago

I suggest to make tests, to separate discovery and browse. During discovery, the network can be congested. For example, when you have the good device (your function slotNewDevice), use a timer to launch the brower action. The timer events have low priority. The current network communications may end.

MarkoSan commented 4 years ago

I don't understand the link between new device and browse.

This two thinks are totally independent.

  1. CControlPoint:: discover or CControlPoint::avdiscover must be call first. These two functions initialize the discovery of the UPnP devices on the network. From this moment, everything is asynchronous. The software must wait for the desired device to launch action (e.g. browse). It is not possible to know when they will respond (it is the parts unicast and multicast of the UPnP). If you have connected CControlPoint::newDevice signal, your slot can retrieve the new device properties (server, renderer…). In the chupnp example, CControlPoint:: discover is call once at startup.
  2. When a device is correctly detected and if the device is not lost, all launched actions are synchronous. You can call CContentDirectory::browse to browse the content of the audio server. At the return of this function the stucture containing the server data is valid.

Hmm, thank you for your reply!

The idea of mine is to detect all devices on networks and add audio contetns from each of them into single QList, which is in a function as model to QML ListView, that is why I've added the code into slotNewDevice(). Will separate them. So do I need to call discover, avDiscover or both of them, like in my code:

void AudioInputsManager::startDiscoveryOfDevices()
{
    this->controlPoint->discover();
    this->controlPoint->avDiscover();
}   // startDiscoveryOfDevices
ptstream commented 4 years ago

Hi Marko, It is unnecessary to call discovery and avdiscovery. In fact both will give probably the same results. The differences are:

It is important to understand that when one of these two functions is launched, several UPnP discoveries are sent (4 for discovery and 12 for avdicovery). These discovery requests generate trafiic on the network. During or after these requests, the devices will respond, It is not possible to know when. The devices will respond propably more than one time (generally between 1 and 4). If you have several devices on the network the traffic can then be very important. That's why it's important to uncouple the discovery and the actions (e.g. browse).

MarkoSan commented 4 years ago

Ok, I've managed to dig deeper into problem: The problem is not in discovery() call, becuase, on Wega board, for followning code:


void AudioInputsManager::startDiscoveryOfDevices()
{
    QTime elapsedTime;

    elapsedTime.start();
    bool discoverSuccess=this->controlPoint->discover();
    int ms=elapsedTime.elapsed();

    qDebug() << COLOR_DEBUG
             << "Discovery elapsed time:"
             << " "
             << ms
             << " "
             << "[ms]"
             << " "
             << "with success flag:"
             << " "
             << discoverSuccess
             << COLOR_RESET;
}   // startDiscoveryOfDevices

```, I get very satisfiying result with _qDebug()_:

>  void AudioInputsManager::slotDiscoveryLaunched(const char*, const int&, const int&) uPnp devices discovery started   nt:   upnp:rootdevice   index:   1   count:   4 
>  void AudioInputsManager::slotDiscoveryLaunched(const char*, const int&, const int&) uPnp devices discovery started   nt:   upnp:rootdevice   index:   2   count:   4 
>  void AudioInputsManager::slotDiscoveryLaunched(const char*, const int&, const int&) uPnp devices discovery started   nt:   upnp:rootdevice   index:   3   count:   4 
>  void AudioInputsManager::slotDiscoveryLaunched(const char*, const int&, const int&) uPnp devices discovery started   nt:   upnp:rootdevice   index:   4   count:   4 
>  Discovery elapsed time:   5   [ms]   with success flag:   true
> 

The problem is with _slotNewDevice()_ - slot connected to new device discovered signal takes too long to be called. Do you maybe have some idea why?
ptstream commented 4 years ago

I think there is no simple solution to this problem. This is due to the UPnP protocol. On my own network, the discovery of 5 devices mostly takes less than 10s but sometimes it can take 3 to 4 minutes. It is not possible to force a device to respond.

To improve this, it is possible to store discovered devices. The chupnp test program gives an example. At the exit of chupnp, the peripherals are stored in a file (uuid, IP and port). This is the function void CMainWindow :: saveDevices (). The next time the function void CMainWindow :: restoreDevices () tries to restore the devices. In this case, the peripherals are questioned directly. If all the devices have responded favorably, there is no need to start the discovery function. This method does not completely solve the problem because if your router is configured in DHCP the device exists but its IP has surely changed. We can then give it a fixed IP. In this case, the port may have changed.

Another solution is to search for specific devices. For example launch CControlPoint :: discovery with the no-default argument. The documentation explains the possible arguments. If the device has implemented this type of discovery, this may limit traffic on the network.

Discovery is the most fragile part of the UPnP protocol because UDP is used to discover the devices.

MarkoSan commented 4 years ago

Ok, which signal is emitted when discovery ends?

MarkoSan commented 4 years ago

I have following network configuration for board's eth0 adapter:

3: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000 link/ether 4c:3f:d3:1c:38:ee brd ff:ff:ff:ff:ff:ff inet 192.168.3.11/24 brd 192.168.3.255 scope global eth0 valid_lft forever preferred_lft forever inet 192.168.100.214/24 brd 192.168.100.255 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::4e3f:d3ff:fe1c:38ee/64 scope link valid_lft forever preferred_lft forever

Now, for this setup the discovery takes too long, but the method itself returns true. Now, if I flush all ip addresses of eth0 and add just one (IPv4, valid for my network):

ip addr flush dev eth0 ip addr add 192.168.100.214/24 dev eth0

and list eth0 addresses:

3: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000 link/ether 4c:3f:d3:1c:38:ee brd ff:ff:ff:ff:ff:ff inet 192.168.100.214/24 scope global eth0 valid_lft forever preferred_lft forever

and then run app, I get following output:

CMulticastSocket::initialize (joinMulticast): "239.255.255.250" CMulticastSocket::~CMulticastSocket (leaveMulticastGroup): "" void AudioInputsManager::slotDiscoveryLaunched(const char, const int&, const int&) uPnp devices discovery started nt: upnp:rootdevice index: 1 count: 4 void AudioInputsManager::slotDiscoveryLaunched(const char, const int&, const int&) uPnp devices discovery started nt: upnp:rootdevice index: 2 count: 4 void AudioInputsManager::slotDiscoveryLaunched(const char, const int&, const int&) uPnp devices discovery started nt: upnp:rootdevice index: 3 count: 4 void AudioInputsManager::slotDiscoveryLaunched(const char, const int&, const int&) uPnp devices discovery started nt: upnp:rootdevice index: 4 count: 4 Discovery elapsed time: 3 [ms] with success flag: false Setting file "/opt/UserConsole/bin/noordungmki.conf" exists, parsing it ... void ConfigurationManager::slotLoadConfiguration() Settings loaded. How do I disable IPv6 in library?

ptstream commented 4 years ago

You can comment the line 86 of controlpoint.cpp //m_multicastSocket6 = initializeMulticast (QHostAddress::AnyIPv6, CMulticastSocket::upnpMulticastAddr6, "MulticastSocket6"); I have never tested this configuration but I think it's work.

About the over question, the discovery is asynchronious (see UPnP dcomentation). Emit a signal at the end of the function CControlPoint::discover has no signification. A metaphorical equivalent is I throw a bottle with a message into the sea (CControlPoint::discover) and I hope someone will find it (the signal CControlPoint::newDevice). When, I don't know and maybe never. This is the reason why several discoveries are sent. It is the reason also that the control points all have a command to start the search manually if the initial discovery fails.

MarkoSan commented 4 years ago

Thanks for the hint, but, do you agree, that I as first step investigate current LAN network setup? Because this is weird if I have to change library code ...

ptstream commented 4 years ago

Yes of course. I do not understand why the ipv6 is a problem. If IPV6 is not activated the bind function (multicastsocket.cpp line 26) will fail but as long as IPV4 is working, this should have no consequences. I already had the problem with a MacOS whose IPV6 was disabled. The library works perfectly.

MarkoSan commented 4 years ago

Ok, just one more thing: How do I know, how many devices are being discovered, i.e.: is there any signal from your library which is emmited when all devices on network are probed?

ptstream commented 4 years ago

With the UPnP protocol, it is not possible to know how many devices are present on the network. Remember that an UPnP control point does not know the IP address of devices on the network. These IPs are defined when the device is powered up (see DHCP protocol). Discovery is defined for that. To my knowledge the only method to know the devices on the network is to assign them a fixed IP. However, there is one unknown, the port used. For most UPnP compatible devices it is not possible to force the port.

I would also like to remind you that QtUPnP is completely free and is available to almost everyone but in return it is released under the GNU GENERAL PUBLIC LICENSE version 3 license. This does not allow its use for commercial purposes.