Stream live in C++/Qt

di Andrea Zani, in Qt,

Dopo il mio post precedente riguardante una scommessa sulla realizzazione di un tool per lo streaming live, ecco questo in cui scriverò lo stesso codice utilizzando C++ con Qt. Sul perché di questa conversione tralascerò i dettagli visto che è un'idea nata da un'altra discussione personale e la voglia di realizzarla è dovuta al piacere che ho quando, ogni tanto e senza fretta, posso scrivere in C++ e utilizzare il framework Qt di cui ho già parlato in passato - premettendo che non sono un esperto in nessuno dei due.

Accettando l'idea di questa conversione mi sono ritrovato subito di fronte agli anni che passano e ai problemi di ricordare dettagli che non affrontavo/approfondivo da anni. Due i punti principali che dovevo affrontare per la realizzazione dello stesso tool creato in net core:

  • Creazione di un'immagine (orologio analogico e digitale).
  • Gestione delle connessioni TCP.

La gestione dei socket e delle connessioni TCP l'avevo già affrontata in passato con Qt e mi sono presto ricordato dove mettere le mani. La gestione della creazione delle immagini è tutto un altro paio di maniche e mi sono dovuto leggere un po' di documentazione. Proprio su questo punto, grazie alle classe QImage e QPixmap, sembra tutto abbastanza semplice. In effetti per creare il tutto mi basta questa funzione:

QByteArray ImageCreator::createImage()
{
    QImage image(MAXX, MAXY, QImage::Format_RGB32);
    image.fill(QColor::fromRgb(0x00, 0xbb, 0x55)); // A green rectangle.
    QPainter p;
    p.begin(&image);
    QPen pen(Qt::white, 3);
    p.setPen(pen);
    QFont font(p.font());
    font.setPixelSize(FONT_SIZE);
    font.setFamily("Tahoma");
    p.setFont(font);
    const QRect rectangle(0, 0, MAXX, MAXY);

    // Draw clock
    p.drawEllipse(CLOCK_X, CLOCK_Y, CLOCK_DIAMETER, CLOCK_DIAMETER);

    auto dt = QDateTime::currentDateTimeUtc();
    //draw seconds hand
    std::pair<int, int> cord = MsCoord<int>(dt.time().second(), SECHAND);
    p.drawLine(CLOCK_CENTER_X, CLOCK_CENTER_Y, CLOCK_X + cord.first, CLOCK_Y + cord.second);

    //draw minutes hand
    cord = MsCoord<int>(dt.time().minute(), MINHAND);
    p.drawLine(CLOCK_CENTER_X, CLOCK_CENTER_Y, CLOCK_X + cord.first, CLOCK_Y + cord.second);

    //draw hours hand
    cord = HrCoord<int>(dt.time().hour() % 12, dt.time().minute(), HRHAND);
    p.drawLine(CLOCK_CENTER_X, CLOCK_CENTER_Y, CLOCK_X + cord.first, CLOCK_Y + cord.second);

    // Draw text
    p.drawText(rectangle, Qt::AlignBottom | Qt::AlignHCenter, dt.toString("dd/MM/yyyy hh:mm:ss UTC"));
    p.end();

    QByteArray ba;
    QBuffer buffer(&ba);
    buffer.open(QIODevice::WriteOnly);
    image.save(&buffer, "JPEG", 80);
    return ba;
}

Molto simile a quella già vista per la versione net core con alcune correzioni dovuti alla diversa gestione delle coordinate. Sembra tutto sistemato, e prima di proseguire provo a creare un'immagine da una console application (con la struttura che avevo già mostrato parecchio tempo fa). Per il disegno dell'orologio analogico tutto sembra funzionare correttamente, ma quando cerco di inserire il testo ottengo degli strani errori su Windows. Indagando scopro che con la tipologia di progetto console application (utilizzando la classe QCoreApplication) non è possibile gestire tutte le potenzialità grafiche di Qt (non è permesso, per esempio, l'accesso ai Font). Il problema si risolve momentaneamente con Windows utilizzando la classe QApplication. Momentaneamente perché appena sposto il codice su Linux, lo compilo e lo avvio, un messaggio di errore mi fa tornare con i piedi per terra:

~ $ qtcreator
qt.qpa.plugin: Could not load the Qt platform plugin "xcb" in "" even
though it was found.
This application failed to start because no Qt platform plugin could
be initialized. Reinstalling the application may fix this problem.

Available platform plugins are: eglfs, linuxfb, minimal, minimalegl,
offscreen, vnc, xcb.

Aborted (core dumped)

Indagando scopro che su Linux il limite dell'uso degli oggetti grafici è ancora maggiore; scopro che è possibile utilizzare ugualmente QApplication ma devo attivare un plugin apposito per comunicare a questo oggetto che NON voglio utilizzare l'interfaccia grafica del sistema operativo, e la cosa si ottiene lanciando l'eseguibile con i parametri -platform offscreen. E in effetti, grazie ad essi, il codice funziona.

Io per evitare questa banale aggiunta ho fatto in modo che sotto Linux tali parametri siano passati in automatico con questo codice:

#ifdef __linux__
    //linux code goes here
    char** argv2 = new char*[3];
    size_t length = strlen(argv[0])+1;
    argv2[0] = new char[length];
    strncpy(argv2[0], argv[0], length);
    argv2[1] = const_cast<char*>("-platform");
    argv2[2] = const_cast<char*>("offscreen");
    argc = 3;
    QApplication a(argc, argv2);
#else
    //windows code goes here
    QApplication a(argc, argv);
#endif

Ho differenziato il codice eseguito per le due piattaforme utilizzate nel mio caso, e faccio in modo che tali parametri aggiuntivi siano utilizzati solo sotto Linux. E il problema è risolto e posso passare all'altro problema.

Per capire come utilizzare i socket tcp con Qt è abbastanza semplice anche perché è facile trovare codice di esempio che fa al caso mio. Importante nel mondo Qt è conoscere la programmazione per eventi grazie alla funzionalità nativa dei signal e slot. Anche la programmazione del TcpSocket di Qt si basa su questi eventi come si può vedere nel codice successivo che rimane in attesa di connessioni TCP alla porta 7474:

server = new QTcpServer();                                                       
connect(server, &QTcpServer::newConnection, this, &TcpServer::slotNewConnection);
                                                                                 
if(!server->listen(QHostAddress::Any, PORT))                                     
{                                                                                
    qDebug()<< "Server Could Not be Started";                                    
    return;                                                                      
}                                                                                
else                                                                             
{                                                                                
    qDebug()<< "Server Started";                                                 
}

In caso di nuova connessione viene richiamata la funzione slotNewConnection:

    QTcpSocket* socket = server->nextPendingConnection();
    NLTcpSocket* customSocket = new NLTcpSocket(socket);

    QString text = "HTTP/1.1 200 OK\r\nContent-Type: multipart/x-mixed-replace; boundary=--boundary\r\n";
    socket->write(text.toStdString().c_str());

    QObject::connect(this, &TcpServer::sendMessage, customSocket, &NLTcpSocket::writeMessage);
    QObject::connect(this, &TcpServer::sendMessageBinary, customSocket, &NLTcpSocket::writeMessageBinary);
    connect(customSocket, &NLTcpSocket::dataReady,this, &TcpServer::slotReceive);
    connect(customSocket, &NLTcpSocket::socketDisconnected,this, &TcpServer::slotDisconnectSocket);

Ogni nuova connessione la memorizzo in un mio oggetto interno QList, in modo che, alla chiusura del programma, posso chiudere tutte le connessioni ancora attive e liberare le risorse utilizzate (qui non ho il garbage collector che fa il lavoro sporco). Anche qui vengono utilizzati i signal/slot, i primi due, SendMessage e SendMessageBinary sono creati da me e saranno utilizzati da altre funzioni per mandare al client del testo o un array di byte (come quello creato dalla funzione createImage vista prima). Il signal dataReady viene avviato quando il client invia dei dati a questo socket, mentre socketDisconnected viene eseguito quando il client si disconnette.

Si può notare che per ogni client non viene avviato un nuovo thread. Il trucco sta nel creare una nuova istanza della classe QTcpSocket con il new per ogni client, collegarlo con i signal e slot agli eventi interessati, e lasciarlo istanziato fino alla chiusura della connessione da parte del client (o della chiusura di questo programmino), e infine distruggerlo con il delete alla vecchia maniera brutale di C++ (avevo provato ad utilizzare altri trucchi in merito, come gli smart pointer, ma senza successo, ed ho preferito lasciare questo vecchio approccio dell'uso dei puntatori).

Così come il codice visto in C#, anche qui ho bisogno di un timer che crei l'immagine ad ogni intervallo di tempo stabilito da me. Ho usato un'altra classe ereditata da QThread con un QTimer che ogni secondo richiamerà la funzione per la creazione dell'immagine:

MyTimer::MyTimer(QObject *parent) : QThread(parent)
{}

void MyTimer::run()
{
    _clientConnected = false;
    QTimer timer;
    QObject::connect(&timer, &QTimer::timeout, this, &MyTimer::writeInfo);
    timer.start(1000);
    exec();
}

E la funzione writeInfo:

void MyTimer::writeInfo()
{
    if (_clientConnected) {
        std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();
        auto blob = _imgCreator.createImage();
        std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
        QString memo_text="\r\n--boundary\r\nContent-Type: image/jpeg\r\n"
                "Content-Length: %1\r\n\r\n";
        auto textToSend = memo_text.arg(blob.size()).toStdString();
        QByteArray textTemp(textToSend.c_str(), static_cast<long>(textToSend.length()));
        textTemp.append(blob);
        std::cout << "Send " << textTemp.size() << " "
                  << std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count()
                  << "ms\n";
        emit sendMessageBinary(textTemp);
    }
    else {
#ifdef QT_DEBUG
        std::cout << "No client connected\n";
#endif
    }

Oltre alla creazione dell'immagine controllo anche in quanti ms viene eseguita questa operazioni, quindi preparo il contenuto da inviare al client per la visualizzazione del filmato in formato MJPEG (non approfondisco oltre perché nello scorso post ne ho parlato fin troppo).

Infine ecco il comando per l'invio del contenuto:

emit sendMessageBinary(textTemp);

emit fa partire effettivamente la comunicazione signal/slot di Qt. sendMessageBinary, essendo un signal, è una funziona senza effettivo contenuto, viene definita la sua signature e nient'altro in questa classe:

signals:
    void sendMessage(const QString& msg);
    void sendMessageBinary(const QByteArray& msg);

Lo slot a cui si potrà collegare dovrà avere la stessa signature. Inoltre, oltre agli slot, posso collegare un signal ad un altro signal, in modo da rinviare lo stesso ad un'altra classe, come ho fatto io nel mio codice. In main(), per esempio, ho creato le due classi:

TcpServer tcpServer;
MyTimer mt;

In TcpServer ho definito ancora due signal:

signals:
    void sendMessage(const QString& msg);
    void sendMessageBinary(const QByteArray& msg);

E in main li ho collegati:

QObject::connect(&mt, &MyTimer::sendMessage, &tcpServer, &TcpServer::sendMessage, Qt::ConnectionType::QueuedConnection);
QObject::connect(&mt, &MyTimer::sendMessageBinary, &tcpServer, &TcpServer::sendMessageBinary, Qt::ConnectionType::QueuedConnection);

In questo modo, nella classe TcpClient, posso riutilizzare il suo signal per collegarli alle singole istanze di QTcpSocket viste prima. Lo so, la prima volta il tutto sembra molto confuso e difficile, ma una volta capita questa gestione degli eventi di Qt, si scopre che è la sua forza.

E' il momento di provare il tutto:

Alcune MIE considerazioni finali. Ho fatto dei test comparativi per le prestazioni, e sotto Windows la creazione dell'immagine con net core e Qt non si differenziano di molto (avrei sicuramente avuto prestazioni migliori sotto Qt se avessi utilizzato le librerie OpenGL o Vulkan, ma avendo conoscenze limitate ed avendo già avuto dei problemi con le classi standard di Qt per la gestione delle immagini, non ho voluto nemmeno provarci). In ambiente Linux, invece, il tempo di creazione dell'immagine è circa del 50% inferiore a quella di net core. Per il resto - reattività di risposta tcp - la versione in C++ vince tranquillamente (visibile il tempo di risposta nettamente inferiore). Sempre per mio parere personale, la versione in C++ e Qt mi è sembrato di più facile realizzazione(e io mi ritengo più abile in C# che in C++/Qt) e molto più leggibile come codice (sempre se si ha chiaro il funzionamento degli eventi Signal/Slot.

Infine, qui il codice sorgente scritto con QtCreator 5.0.

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Nella stessa categoria
I più letti del mese