PDA

View Full Version : Qt 5.6.1 / Linux Streaming MJPEG in plug browser


fano
14-11-2018, 10:21
Ho un problema che mi sta davvero facendo impazzire... ho una telecamera tipo "video sorveglianza" che mi invia via HTTP uno stream mjpeg che qualsiasi browser del mondo sa leggere semplicemente aprendo la pagina, ma ovviamente QWebkit no! Il meglio che ho ottenuto è che lo stampasse come testo :D

La cosa più ovvia visto che sto "browser" a stento supporta HTML4 era di usare un plugin, ma Flash no perché la gente ha paura di lui (?), così avevo pensato al plugin di VLC, ma tutto quello che ho ottenuto era un cubetto con un bel '?' dentro!

Ora sto provando a fare il mio plugin ed ero tutto contento perché questo funziona pure: http://doc.qt.io/archives/qt-4.8/qt-webkit-simplewebplugin-example.html!

Così ho pensato di usare QMediaplayer, ma sembra la solita cosa non finita... come plugin va addirittura in SIGABORT (!), l'applicazione standalone mi mostrava uno schermo nero :eek:

I codec credo di averceli ho messo gstreamer-ffmpeg che dovrebbe supportare qualunque cosa, ma nulla!

Come ultimo tentativo ho deciso di farlo "a mano", in fondo uno stream MJPEG è piuttosto semplice da gestire sono tante JPEG una dietro l'altra e infatti l'applicazione standalone funziona!

Così stamane mi appropinquo a trasformarla in un plugin e... NON appare nulla!
Peggio ancora il segnale readyRead di QNetworkReply manco avviene!

Questo è il plugin factory, molto simile all'esempio di fatto:


QObject *mediaPlayerFactory::create(const QString &mimeType, const QUrl &url,
const QStringList &argumentNames,
const QStringList &argumentValues) const
{
if (mimeType != "video/mjpeg")
return 0;

mediaPlayerView *view = new mediaPlayerView(url);

QNetworkRequest request(url);
QNetworkReply *reply = manager->get(request);
connect(reply, SIGNAL(readyRead()), view, SLOT(onReadyRead(reply)));
connect(reply,SIGNAL(error(QNetworkReply::NetworkError)),
view,SLOT(onError(QNetworkReply::NetworkError)));

//connect(reply, SIGNAL(finished()), reply, SLOT(deleteLater()));

return view;
}


E questo è il costruttore di mediaPlayerView:


mediaPlayerView::mediaPlayerView(const QUrl &url, QWidget *parent)
: QWidget(parent)
{
this->url = url;

Log("Getting MJPEG stream from url: " + url.toString());

label = new QLabel(this);

label->setWindowFlags(label->windowFlags() | Qt::FramelessWindowHint);
label->show();

Log("Ready to stream!");
}


onReadyRead() è relativamente banale legge tutto il pacchetto vede se è un'immagine completa e lo disegna... sono sicuro al 100% che se venisse chiamato funzionerebbe pure!

Ho settato ovviamente che i plugin siano abilitati e che possano accedere alla rete:


QWebSettings::globalSettings()->setAttribute(QWebSettings::PluginsEnabled,
true);

QWebSettings::globalSettings()->setAttribute(QWebSettings::LocalContentCanAccessRemoteUrls, true);



Qualche idea?

Il prossimo tentativo sarebbe qualcosa di davvero troppo anche per Linux: far partire VLC frameless ed embeddare la sua finestra dentro la pagina web :muro: :muro: :muro:

fano
14-11-2018, 11:25
L'ho trovato! ... ed è imbarazzante quello che Qt fa...

Per chi non conosce come funziona l'animale Qt per nascondere l'indubbio casino che sono i thread in C++ usa un meccanismo di segnali / slot così - per esempio - quando il thread che si occupa di leggere dal socket ha il buffer pieno mi notifica con readyRead() che ha qualcosa da leggere ed io mi "aggancio" ad esso con il mio slot onReadyRead().

Peccato che per far questo - in realtà - non si usa "realmente" C++ ma una sorta di dialetto, infatti file .cpp Qt è compilato in un'equivalente .moc in cui le "orrende" macchine a stati / switch appaiono robe orride tipo:


void mediaPlayerView::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
if (_c == QMetaObject::InvokeMetaMethod) {
mediaPlayerView *_t = static_cast<mediaPlayerView *>(_o);
Q_UNUSED(_t)
switch (_id) {
case 0: _t->notifyLog((*reinterpret_cast< BrowserApp::LogLevel(*)>(_a[1])),(*reinterpret_cast< QString(*)>(_a[2]))); break;
case 1: _t->onReadyRead(); break;
case 2: _t->onError((*reinterpret_cast< QNetworkReply::NetworkError(*)>(_a[1]))); break;
case 3: _t->onFinished(); break;
default: ;


Bello vero?

Peccato che io avevo fatto un piccolo errorino... gli slot devono essere definiti esattamente con lo stesso prototipo dei segnali, ma ci volevo passare "reply"!!!
Tutto compilava perfettamente, i moc erano creati e compilati e l'applicazione girava perfettamente peccato che non si vedeva una favonza...

Finché non ho letto cosa era scritto sullo stderr e sorpresona:


QObject::connect: No such slot mediaPlayerView:onReadyRead(reply) in src/mediaPlayerFactory.cpp:25


vedete? Come fosse javascript un errore di "sintassi" e ce ne accorgiamo a runtime :muro:

Cambiato onReadyRead() per non prendere argomenti e fatto un'orrido "dynamic cast" con oggetto che esiste per "miracolo" chiamato sender e...


void mediaPlayerView::onReadyRead()
{
QNetworkReply *reply = static_cast<QNetworkReply *>(sender());

if (reply->error() != QNetworkReply::NoError) {
Log(BrowserApp::Error, "reply is an Error!");
return;
}

[...]


Mah... a me pare che async di C# sia un po' meglio!
Anche se devo dire che se qualcuno ha mai programmato in BeOS la loro soluzione al problema dei "thread" era migliore avevano inventato un tipo BMessage e semplicemente per ricevere i messaggi uno doveva implementare un metodo MessageReceived() e poi
se li "puppava" con il classico switch e via...

A volte quando le cose sono troppo "magiche" è molto difficile ragionarci sopra :D

Voi cosa ne pensate?

carsco
15-11-2018, 09:22
Nel 99% dei casi quella camera invia anche uno stream RTP o RTSP: perche' non lo apri con VLC direttamente conoscendo indirizzo, nome stream e porta?

Perche' scomodare uno strato in piu'?

fano
16-11-2018, 08:57
Perché lo devo mostrare dentro la pagina web... sono ancora in HTML4 (QWebKit non supporta HTML5 o almeno non supporta tutti i tag) e comunque il tag video non supporta MJPEG.

Avevo provato - infatti - ad usare VLC plugin, ma non c'è stato verso... ottenevo un cubetto con dentro '?', la cosa strana è che Flash funzionava! Credo che alla fine supporti solo i "suoi" e Flash (c'è un bel hardcoding da qualche parte?)...

Comunque ora sto impazzendo con sto meccanismo dei segnali... si sconnette mi arriva il segnale "finished()", fai abort() (close del socket?) e poi mi arriva di nuovo il segnale finished()? Ed io mi inlooppo a stecca...

carsco
16-11-2018, 23:58
Credo che alla fine supporti solo i "suoi" e FlashMa secondo te, dentro lo stream che ti manda sul browser, cosa c'e'? C'e' lo stream MJPEG (o H264 se la camera e' piu' recente)!

La camera potrebbe anche mettere a disposizione un indirizzo con una JPG che si aggiorna svariate volte al secondo (quindi, M-Jpeg, ma che lo dico a fare?).

Sappiamo marca e modello della camera?
Non andiamoci a caso.
Ti posso aiutare.

pabloski
17-11-2018, 15:34
Comunque, siccome si sta parlando di Qt 5, faccio notare che QtWebkit è deprecato ed è stato sostituito da QtWebEngine.

Riguardo il meccanismo degli slot/signals ( che vista l'epoca era rivoluzionario e ha dato vita ai framework basati su bindings ), in qml sono stati sostituiti dai bindings.

fano
20-11-2018, 12:01
Purtroppo il lavoro che sto facendo deve anche funzionare su un Centos 5.4 (!) quindi Qt 4.6 (o 4.8) dove sicuramente QtWebEngine non esiste... e sinceramente visto che l'applicazione esiste da più di 10 anni ho paura a cambiare Engine chissà quante incompatibilità ci saranno :D

La telecamera è una HIK e ha 2 modi per fornire lo streaming MJPEG:

1. Via HTPP...funziona benissimo con Firefox... il nostro browser basato su QWebKit ovviamente non è in grado. Stranamente anche VLC non è in grado di aprirlo
2. Via RSTP questo lo apre VLC, ma non Firefox... bah!

Il mio mediaplayer "fatto a mano" funziona pure, il problema è che il modo per disegnare 30 immagini / s su una QLabel, X mi occupa il 95% di CPU (+25% imputabili al mio software)!

vlc chiamato da terminale ovviamente occupa solo il 14% :eek:

Le performance sono proprie pietose... non credo se l'avessi potuto scrivere in C# sarebbe stato così lento.

Questo è il codice che uso per disegnare:


QImage image;
bool isValid;

isValid = image.loadFromData(data, "JPG");

if (!isValid) {
Log(BrowserApp::Error, "Error image is not valid");
return;
}

QPixmap pix = QPixmap();
pix = pix.fromImage(image);
pix = pix.scaled(this->size(), Qt::KeepAspectRatio);
label->setPixmap(pix);


La mia impressione è che questo codice faccia una marea di memcpy(), ma credo sia il modo "consigliato" in Qt...

C'è un modo più efficiente per disegnare?

pabloski
20-11-2018, 12:20
Il metodo consigliato e' usare Phonon. In alternativa e' possibile sfruttare FFMPEG che e' pure cross-platform. Oppure piu' semplicemente si puo' usare OpenCV, trasformando i frame contenuti in una matrice OpenCV in immagine QImage. Si taglia la testa al toro e si usa pure la GPU se presente.

fano
28-11-2018, 14:17
Phonon non sembra più supportato da Qt5, le altre soluzioni sembrano davvero troppo complesse per risolvere un problema così banale come visualizzare uno stream MJPEG.

Alla fine visto che VLC era in grado di farlo (usando solo il 10% di CPU) ho usato la libvlc, l'API rigorosamente in C permette abbastanza semplicemente di farsi il "proprio" VLC:

https://wiki.videolan.org/LibVLC_Tutorial/#Sample_LibVLC_Code

da quell'esempio è "banale" ottenere il famoso plugin VLC / QT che finalmente piace a QWebkit :eek:

Attenzione perché non è così banale ci sono 2 trappole:

1. Il pacchetto rpm VLC-Core della Centos 6 è - ovviamente - toppato e non include tra i video "plugins" quello che permette di sbattere VLC dentro una finestra X (ho passato 2 giorni prima di capirlo!). La "menata" è che si deve installare l'intero VLC che si porta dietro anche Qt4 ed altra bratta... dovrò fare un "RPM" a mano per mettere solo i plugins
2. Mentre il vero "VLC" ritenta se lo stream s'interrompe (banalmente si può simularlo staccando il cavo di rete dalla TLC), libVLC è "bacata" e semplicemente scrive che lo stream è finito! Per farlo correttamente si deve usare una callback per esempio nel posto dove viene gestito il "play":


libvlc_event_manager_t *event_manager = libvlc_media_player_event_manager (vlcPlayer);
libvlc_event_attach(event_manager, libvlc_MediaPlayerEndReached, (libvlc_callback_t )&media_is_ended, this);
libvlc_event_attach(event_manager, libvlc_MediaPlayerEncounteredError, (libvlc_callback_t )&media_is_ended, this);


In "media_is_ended()" fai semplicemente stop() / play(), se si vede qualcosa siete a cavallo altrimenti media_is_ended() sarà richiamato. Non preoccupatevi di non "stressare" la rete, vlc genera l'evento "libvlc_MediaPlayerEndReached" già a timeout.

Usando VLC ottengo altri 2 vantaggi:


Se volessi potrei anche usare lo stream H264 in full HD!
In caso di microinterruzioni lo stream continua ininterrotto perché - ovviamente - VLC fa buffering, con l'altra soluzione fatta "a mano" dovevo gestirmi anche questo caso...


Spero questo sarà utile a qualche altro povero sfigato che si trovi in questa situazione... imbarazzante :D