Feat: Add image sender to webui + Browser screen capture (#611)

* Feat: Add image sender to webui

This PR adds a new image sending feature to the webui and extends the api accordingly. In javascript the processing of images is horrible slow (without WASM), so the solution is at the API side with out-of-the-box power of Qt.

- The image cmd accepts now a raw image that is encoded with base64. Supported are all image formats that qt supports (enough)
- There is no real size limit. It will be automatically scaled down to max 2000px width or height according to aspect ratio
- It's possible to scale down through a new "scale" property if developer wants that.
- Test were successfull until 3MP pictues, 4k+ closes the websocket on browser side, so 2k is a decent value
- Adds a new image streaming feature from browser (tabs/applications/complete desktop as target). This works just if used with HTTPS PR#612 AND with a recent version of Firefox/Chrome
This commit is contained in:
brindosch
2019-08-21 16:10:35 +02:00
committed by GitHub
parent 8e5f3251b5
commit 09ee8f26ee
11 changed files with 245 additions and 28 deletions

View File

@@ -28,17 +28,27 @@
},
"imagewidth": {
"type" : "integer",
"required": true,
"minimum": 0
},
"imageheight": {
"type" : "integer",
"required": true,
"minimum": 0
},
"imagedata": {
"type": "string",
"required": true
},
"format": {
"type": "string",
"enum" : ["auto"]
},
"scale": {
"type": "integer",
"minimum" : 25,
"maximum" : 2000
},
"name": {
"type": "string"
}
},
"additionalProperties": false

View File

@@ -112,7 +112,7 @@ bool JsonAPI::handleInstanceSwitch(const quint8& inst, const bool& forced)
// // imageStream last state
// if(_ledcolorsImageActive)
// connect(_hyperion, &Hyperion::currentImage, this, &JsonAPI::setImage, Qt::UniqueConnection);
//
//
// //ledColor stream last state
// if(_ledcolorsLedsActive)
// connect(_hyperion, &Hyperion::rawLedColors, this, &JsonAPI::streamLedcolorsUpdate, Qt::UniqueConnection);
@@ -172,7 +172,7 @@ void JsonAPI::handleMessage(const QString& messageString, const QString& httpAut
sendErrorReply("No Authorization", command, tan);
return;
}
// switch over all possible commands and handle them
if (command == "color") handleColorCommand (message, command, tan);
else if (command == "image") handleImageCommand (message, command, tan);
@@ -232,20 +232,83 @@ void JsonAPI::handleImageCommand(const QJsonObject& message, const QString& comm
int duration = message["duration"].toInt(-1);
int width = message["imagewidth"].toInt();
int height = message["imageheight"].toInt();
int scale = message["scale"].toInt(-1);
QString format = message["format"].toString();
QString imgName = message["name"].toString("");
QByteArray data = QByteArray::fromBase64(QByteArray(message["imagedata"].toString().toUtf8()));
// check consistency of the size of the received data
if (data.size() != width*height*3)
// truncate name length
imgName.truncate(16);
if(format == "auto")
{
sendErrorReply("Size of image data does not match with the width and height", command, tan);
return;
QImage img = QImage::fromData(data);
if(img.isNull())
{
sendErrorReply("Failed to parse picture, the file might be corrupted", command, tan);
return;
}
// check for requested scale
if(scale > 24)
{
if(img.height() > scale)
{
img = img.scaledToHeight(scale);
}
if(img.width() > scale)
{
img = img.scaledToWidth(scale);
}
}
// check if we need to force a scale
if(img.width() > 2000 || img.height() > 2000)
{
scale = 2000;
if(img.height() > scale)
{
img = img.scaledToHeight(scale);
}
if(img.width() > scale)
{
img = img.scaledToWidth(scale);
}
}
width = img.width();
height = img.height();
// extract image
img = img.convertToFormat(QImage::Format_ARGB32_Premultiplied);
data.clear();
data.reserve(img.width() * img.height() * 3);
for (int i = 0; i < img.height(); ++i)
{
const QRgb * scanline = reinterpret_cast<const QRgb *>(img.scanLine(i));
for (int j = 0; j < img.width(); ++j)
{
data.append((char) qRed(scanline[j]));
data.append((char) qGreen(scanline[j]));
data.append((char) qBlue(scanline[j]));
}
}
}
else
{
// check consistency of the size of the received data
if (data.size() != width*height*3)
{
sendErrorReply("Size of image data does not match with the width and height", command, tan);
return;
}
}
// create ImageRgb
// copy image
Image<ColorRgb> image(width, height);
memcpy(image.memptr(), data.data(), data.size());
_hyperion->registerInput(priority, hyperion::COMP_IMAGE, origin);
_hyperion->registerInput(priority, hyperion::COMP_IMAGE, origin, imgName);
_hyperion->setInputImage(priority, image, duration);
// send reply

View File

@@ -268,7 +268,7 @@ void PriorityMuxer::clearAll(bool forceClearAll)
for(auto key : _activeInputs.keys())
{
const InputInfo info = getInputInfo(key);
if ((info.componentId == hyperion::COMP_COLOR || info.componentId == hyperion::COMP_EFFECT) && key < PriorityMuxer::LOWEST_PRIORITY-1)
if ((info.componentId == hyperion::COMP_COLOR || info.componentId == hyperion::COMP_EFFECT || info.componentId == hyperion::COMP_IMAGE) && key < PriorityMuxer::LOWEST_PRIORITY-1)
{
clearInput(key);
}
@@ -299,7 +299,7 @@ void PriorityMuxer::setCurrentTime(void)
newPriority = qMin(newPriority, infoIt->priority);
// call timeTrigger when effect or color is running with timeout > 0, blacklist prio 255
if(infoIt->priority < 254 && infoIt->timeoutTime_ms > 0 && (infoIt->componentId == hyperion::COMP_EFFECT || infoIt->componentId == hyperion::COMP_COLOR))
if(infoIt->priority < 254 && infoIt->timeoutTime_ms > 0 && (infoIt->componentId == hyperion::COMP_EFFECT || infoIt->componentId == hyperion::COMP_COLOR || infoIt->componentId == hyperion::COMP_IMAGE))
emit signalTimeTrigger(); // as signal to prevent Threading issues
++infoIt;