Support rendering Webm videos with alpha.

This commit is contained in:
John Preston 2022-01-19 17:45:51 +03:00
parent 1755ead681
commit 8b7d2c880e
25 changed files with 139 additions and 44 deletions

View File

@ -858,8 +858,8 @@ void StickerSetBox::Inner::paintSticker(
const auto &media = element.documentMedia;
media->checkStickerSmall();
const auto isAnimated = document->sticker()->animated;
if (isAnimated
const auto isLottie = document->sticker()->isLottie();
if (isLottie
&& !element.animated
&& media->loaded()) {
const_cast<Inner*>(this)->setupLottie(index);
@ -867,7 +867,7 @@ void StickerSetBox::Inner::paintSticker(
auto w = 1;
auto h = 1;
if (isAnimated && !document->dimensions.isEmpty()) {
if (isLottie && !document->dimensions.isEmpty()) {
const auto request = Lottie::FrameRequest{ boundingBoxSize() * cIntRetinaFactor() };
const auto size = request.size(document->dimensions, true) / cIntRetinaFactor();
w = std::max(size.width(), 1);

View File

@ -827,7 +827,7 @@ void FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) {
const auto &media = sticker.documentMedia;
if (!document->sticker()) continue;
if (document->sticker()->animated
if (document->sticker()->isLottie()
&& !sticker.animated
&& media->loaded()) {
setupLottie(sticker);

View File

@ -141,7 +141,7 @@ void DicePack::generateLocal(int index, const QString &name) {
_map.emplace(index, document);
Ensures(document->sticker());
Ensures(document->sticker()->animated);
Ensures(document->sticker()->isLottie());
}
DicePacks::DicePacks(not_null<Main::Session*> session) : _session(session) {

View File

@ -1916,8 +1916,8 @@ void StickersListWidget::paintSticker(Painter &p, Set &set, int y, int section,
return;
}
const auto isAnimated = document->sticker()->animated;
if (isAnimated
const auto isLottie = document->sticker()->isLottie();
if (isLottie
&& !sticker.animated
&& media->loaded()) {
setupLottie(set, section, index);
@ -1936,7 +1936,7 @@ void StickersListWidget::paintSticker(Painter &p, Set &set, int y, int section,
auto w = 1;
auto h = 1;
if (isAnimated && !document->dimensions.isEmpty()) {
if (isLottie && !document->dimensions.isEmpty()) {
const auto request = Lottie::FrameRequest{ boundingBoxSize() * cIntRetinaFactor() };
const auto size = request.size(document->dimensions, true) / cIntRetinaFactor();
w = std::max(size.width(), 1);

View File

@ -139,7 +139,7 @@ bool HasLottieThumbnail(
}
const auto document = media->owner();
if (const auto info = document->sticker()) {
if (!info->animated) {
if (!info->isLottie()) {
return false;
}
media->automaticLoad(document->stickerSetOrigin(), nullptr);

View File

@ -109,6 +109,14 @@ MimeType MimeTypeForData(const QByteArray &data) {
return MimeType(QMimeDatabase().mimeTypeForData(data));
}
bool IsMimeStickerLottie(const QString &mime) {
return (mime == u"application/x-tgsticker"_q);
}
bool IsMimeStickerWebm(const QString &mime) {
return (mime == u"video/webm"_q);
}
bool IsMimeStickerAnimated(const QString &mime) {
return (mime == u"application/x-tgsticker"_q);
}

View File

@ -52,7 +52,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace {
const auto kAnimatedStickerDimensions = QSize(
const auto kLottieStickerDimensions = QSize(
kStickerSideSize,
kStickerSideSize);
@ -262,6 +262,22 @@ Data::FileOrigin StickerData::setOrigin() const {
: Data::FileOrigin();
}
bool StickerData::isStatic() const {
return (type == StickerType::Webp);
}
bool StickerData::isLottie() const {
return (type == StickerType::Tgs);
}
bool StickerData::isAnimated() const {
return !isStatic();
}
bool StickerData::isWebm() const {
return (type == StickerType::Webm);
}
VoiceData::~VoiceData() {
if (!waveform.isEmpty()
&& waveform[0] == -1
@ -380,12 +396,19 @@ void DocumentData::setattributes(
}
if (type == StickerDocument
&& ((size > Storage::kMaxStickerBytesSize)
|| (!sticker()->animated
|| (!sticker()->isLottie()
&& !GoodStickerDimensions(
dimensions.width(),
dimensions.height())))) {
type = FileDocument;
_additional = nullptr;
} else if (type == FileDocument
&& hasMimeType(qstr("video/webm"))
&& (size < Storage::kMaxStickerBytesSize)
&& GoodStickerDimensions(dimensions.width(), dimensions.height())) {
type = StickerDocument;
_additional = std::make_unique<StickerData>();
sticker()->type = StickerType::Webm;
}
if (isAudioFile() || isAnimation() || isVoiceMessage()) {
setMaybeSupportsStreaming(true);
@ -397,8 +420,8 @@ void DocumentData::validateLottieSticker() {
&& hasMimeType(qstr("application/x-tgsticker"))) {
type = StickerDocument;
_additional = std::make_unique<StickerData>();
sticker()->animated = true;
dimensions = kAnimatedStickerDimensions;
sticker()->type = StickerType::Tgs;
dimensions = kLottieStickerDimensions;
}
}

View File

@ -57,12 +57,22 @@ struct DocumentAdditionalData {
};
struct StickerData : public DocumentAdditionalData {
Data::FileOrigin setOrigin() const;
enum class StickerType : uchar {
Webp,
Tgs,
Webm,
};
struct StickerData : public DocumentAdditionalData {
[[nodiscard]] Data::FileOrigin setOrigin() const;
[[nodiscard]] bool isStatic() const;
[[nodiscard]] bool isLottie() const;
[[nodiscard]] bool isAnimated() const;
[[nodiscard]] bool isWebm() const;
bool animated = false;
QString alt;
StickerSetIdentifier set;
StickerType type = StickerType::Webp;
};
struct SongData : public DocumentAdditionalData {

View File

@ -49,7 +49,7 @@ enum class FileType {
|| owner->isAnimation()
|| owner->isWallPaper()
|| owner->isTheme()
|| (owner->sticker() && owner->sticker()->animated);
|| (owner->sticker() && owner->sticker()->isAnimated());
}
[[nodiscard]] QImage PrepareGoodThumbnail(
@ -260,7 +260,7 @@ void DocumentMedia::checkStickerLarge() {
return;
}
automaticLoad(_owner->stickerSetOrigin(), nullptr);
if (data->animated || !loaded()) {
if (data->isAnimated() || !loaded()) {
return;
}
if (_bytes.isEmpty()) {
@ -366,9 +366,9 @@ bool DocumentMedia::thumbnailEnoughForSticker() const {
void DocumentMedia::checkStickerSmall() {
const auto data = _owner->sticker();
if ((data && data->animated) || thumbnailEnoughForSticker()) {
if ((data && data->isAnimated()) || thumbnailEnoughForSticker()) {
_owner->loadThumbnail(_owner->stickerSetOrigin());
if (data && data->animated) {
if (data && data->isAnimated()) {
automaticLoad(_owner->stickerSetOrigin(), nullptr);
}
} else {
@ -383,7 +383,7 @@ Image *DocumentMedia::getStickerLarge() {
Image *DocumentMedia::getStickerSmall() {
const auto data = _owner->sticker();
if ((data && data->animated) || thumbnailEnoughForSticker()) {
if ((data && data->isAnimated()) || thumbnailEnoughForSticker()) {
return thumbnail();
}
return _sticker.get();

View File

@ -994,7 +994,7 @@ std::vector<not_null<DocumentData*>> Stickers::getListByEmoji(
const auto CreateSortKey = [&](
not_null<DocumentData*> document,
int base) {
if (document->sticker() && document->sticker()->animated) {
if (document->sticker() && document->sticker()->isAnimated()) {
base += kSlice;
}
return TimeId(base + int((document->id ^ seed) % kSlice));
@ -1005,7 +1005,7 @@ std::vector<not_null<DocumentData*>> Stickers::getListByEmoji(
auto myCounter = 0;
const auto CreateMySortKey = [&](not_null<DocumentData*> document) {
auto base = kSlice * 6;
if (!document->sticker() || !document->sticker()->animated) {
if (!document->sticker() || !document->sticker()->isAnimated()) {
base -= kSlice;
}
return (base - (++myCounter));
@ -1019,7 +1019,7 @@ std::vector<not_null<DocumentData*>> Stickers::getListByEmoji(
const auto InstallDateAdjusted = [&](
TimeId date,
not_null<DocumentData*> document) {
return (document->sticker() && document->sticker()->animated)
return (document->sticker() && document->sticker()->isAnimated())
? date
: date / 2;
};

View File

@ -38,7 +38,7 @@ ItemSticker::ItemSticker(
? 1.0
: (_pixmap.height() / float64(_pixmap.width())));
});
if (stickerData->animated) {
if (stickerData->isLottie()) {
_lottie.player = ChatHelpers::LottiePlayerFromDocument(
_mediaView.get(),
ChatHelpers::StickerLottieSize::MessageHistory,

View File

@ -391,6 +391,7 @@ void Gif::draw(Painter &p, const PaintContext &context) const {
auto request = ::Media::Streaming::FrameRequest();
request.outer = QSize(usew, painth) * cIntRetinaFactor();
request.resize = QSize(_thumbw, _thumbh) * cIntRetinaFactor();
request.keepAlpha = true; AssertIsDebug();
request.corners = roundCorners;
request.radius = roundRadius;
if (!activeRoundPlaying && activeOwnPlaying->instance.playerLocked()) {
@ -992,6 +993,7 @@ void Gif::drawGrouped(
{ geometry.width(), geometry.height() });
request.outer = geometry.size() * cIntRetinaFactor();
request.resize = pixSize * cIntRetinaFactor();
request.keepAlpha = true; AssertIsDebug();
request.corners = corners;
request.radius = roundRadius;
if (activeOwnPlaying->instance.playerLocked()) {

View File

@ -123,7 +123,7 @@ bool Sticker::readyToDrawLottie() {
ensureDataMediaCreated();
_dataMedia->checkStickerLarge();
const auto loaded = _dataMedia->loaded();
if (sticker->animated && !_lottie && loaded) {
if (sticker->isLottie() && !_lottie && loaded) {
setupLottie();
}
return (_lottie && _lottie->ready());
@ -147,7 +147,7 @@ void Sticker::draw(
if (readyToDrawLottie()) {
paintLottie(p, context, r);
} else if (!_data->sticker()
|| (_data->sticker()->animated && _replacements)
|| (_data->sticker()->isLottie() && _replacements)
|| !paintPixmap(p, context, r)) {
paintPath(p, context, r);
}

View File

@ -554,7 +554,7 @@ void Sticker::prepareThumbnail() const {
ensureDataMediaCreated(document);
if (!_lottie
&& document->sticker()
&& document->sticker()->animated
&& document->sticker()->isLottie()
&& _dataMedia->loaded()) {
setupLottie();
}

View File

@ -188,8 +188,16 @@ crl::time FFMpegReaderImplementation::framePresentationTime() const {
}
crl::time FFMpegReaderImplementation::durationMs() const {
if (_fmtContext->streams[_streamId]->duration == AV_NOPTS_VALUE) return 0;
return (_fmtContext->streams[_streamId]->duration * 1000LL * _fmtContext->streams[_streamId]->time_base.num) / _fmtContext->streams[_streamId]->time_base.den;
const auto rebase = [](int64_t duration, const AVRational &base) {
return (duration * 1000LL * base.num) / base.den;
};
const auto stream = _fmtContext->streams[_streamId];
if (stream->duration != AV_NOPTS_VALUE) {
return rebase(stream->duration, stream->time_base);
} else if (_fmtContext->duration != AV_NOPTS_VALUE) {
return rebase(_fmtContext->duration, AVRational{ 1, AV_TIME_BASE });
}
return 0;
}
bool FFMpegReaderImplementation::renderFrame(QImage &to, bool &hasAlpha, const QSize &size) {
@ -211,8 +219,12 @@ bool FFMpegReaderImplementation::renderFrame(QImage &to, bool &hasAlpha, const Q
if (to.isNull() || to.size() != toSize || !to.isDetached() || !isAlignedImage(to)) {
to = createAlignedImage(toSize);
}
hasAlpha = (_frame->format == AV_PIX_FMT_BGRA || (_frame->format == -1 && _codecContext->pix_fmt == AV_PIX_FMT_BGRA));
if (_frame->width == toSize.width() && _frame->height == toSize.height() && hasAlpha) {
const auto format = (_frame->format == AV_PIX_FMT_NONE)
? _codecContext->pix_fmt
: _frame->format;
const auto bgra = (format == AV_PIX_FMT_BGRA);
hasAlpha = bgra || (format == AV_PIX_FMT_YUVA420P);
if (_frame->width == toSize.width() && _frame->height == toSize.height() && bgra) {
int32 sbpl = _frame->linesize[0], dbpl = to.bytesPerLine(), bpl = qMin(sbpl, dbpl);
uchar *s = _frame->data[0], *d = to.bits();
for (int32 i = 0, l = _frame->height; i < l; ++i) {
@ -228,7 +240,7 @@ bool FFMpegReaderImplementation::renderFrame(QImage &to, bool &hasAlpha, const Q
int toLinesize[AV_NUM_DATA_POINTERS] = { int(to.bytesPerLine()), 0 };
sws_scale(_swsContext, _frame->data, _frame->linesize, 0, _frame->height, toData, toLinesize);
}
if (hasAlpha) {
if (bgra) {
FFmpeg::PremultiplyInplace(to);
}
if (_rotation != Rotation::None) {
@ -391,6 +403,19 @@ bool FFMpegReaderImplementation::isGifv() const {
return true;
}
bool FFMpegReaderImplementation::isWebmSticker() const {
if (_hasAudioStream) {
return false;
}
if (dataSize() > kMaxInMemory) {
return false;
}
if (_codecContext->codec_id != AV_CODEC_ID_VP9) {
return false;
}
return true;
}
QString FFMpegReaderImplementation::logData() const {
return u"for file '%1', data size '%2'"_q.arg(_location ? _location->name() : QString()).arg(_data->size());
}

View File

@ -43,6 +43,7 @@ public:
QString logData() const;
bool isGifv() const;
bool isWebmSticker() const;
~FFMpegReaderImplementation();

View File

@ -845,6 +845,7 @@ Ui::PreparedFileInformation::Video PrepareForSending(const QString &fname, const
auto durationMs = reader->durationMs();
if (durationMs > 0) {
result.isGifv = reader->isGifv();
result.isWebmSticker = reader->isWebmSticker();
// Use first video frame as a thumbnail.
// All other apps and server do that way.
//if (!result.isGifv) {
@ -857,7 +858,7 @@ Ui::PreparedFileInformation::Video PrepareForSending(const QString &fname, const
auto readResult = reader->readFramesTill(-1, crl::now());
auto readFrame = (readResult == internal::ReaderImplementation::ReadResult::Success);
if (readFrame && reader->renderFrame(result.thumbnail, hasAlpha, QSize())) {
if (hasAlpha) {
if (hasAlpha && !result.isWebmSticker) {
auto cacheForResize = QImage();
auto request = FrameRequest();
request.framew = request.outerw = result.thumbnail.width();

View File

@ -121,6 +121,7 @@ struct FrameRequest {
ImageRoundRadius radius = ImageRoundRadius();
RectParts corners = RectPart::AllCorners;
bool requireARGB32 = true;
bool keepAlpha = false;
bool strict = true;
static FrameRequest NonStrict() {
@ -138,6 +139,7 @@ struct FrameRequest {
&& (outer == other.outer)
&& (radius == other.radius)
&& (corners == other.corners)
&& (keepAlpha == other.keepAlpha)
&& (requireARGB32 == other.requireARGB32);
}
[[nodiscard]] bool operator!=(const FrameRequest &other) const {
@ -146,6 +148,7 @@ struct FrameRequest {
[[nodiscard]] bool goodFor(const FrameRequest &other) const {
return (requireARGB32 == other.requireARGB32)
&& (keepAlpha == other.keepAlpha)
&& ((*this == other) || (strict && !other.strict));
}
};

View File

@ -89,9 +89,10 @@ FFmpeg::AvErrorWrap ReadNextFrame(Stream &stream) {
bool GoodForRequest(
const QImage &image,
bool hasAlpha,
int rotation,
const FrameRequest &request) {
if (image.isNull()) {
if (image.isNull() || (hasAlpha && !request.keepAlpha)) {
return false;
} else if (request.resize.isEmpty()) {
return true;
@ -170,6 +171,10 @@ QImage ConvertFrame(
frame->height,
data,
linesize);
if (frame->format == AV_PIX_FMT_YUVA420P) {
FFmpeg::PremultiplyInplace(storage);
}
}
FFmpeg::ClearFrameMemory(frame);
@ -274,8 +279,11 @@ void PaintFrameContent(
(full.height() - size.height()) / 2,
size.width(),
size.height());
PaintFrameOuter(p, to, full);
PaintFrameInner(p, to, original, alpha, rotation);
if (!alpha || !request.keepAlpha) {
PaintFrameOuter(p, to, full);
}
const auto deAlpha = alpha && !request.keepAlpha;
PaintFrameInner(p, to, original, deAlpha, rotation);
}
void ApplyFrameRounding(QImage &storage, const FrameRequest &request) {
@ -304,6 +312,10 @@ QImage PrepareByRequest(
storage = FFmpeg::CreateFrameStorage(outer);
}
if (alpha && request.keepAlpha) {
storage.fill(Qt::transparent);
}
QPainter p(&storage);
PaintFrameContent(p, original, alpha, rotation, request);
p.end();

View File

@ -51,6 +51,7 @@ struct Stream {
[[nodiscard]] bool GoodForRequest(
const QImage &image,
bool hasAlpha,
int rotation,
const FrameRequest &request);
[[nodiscard]] QImage ConvertFrame(

View File

@ -444,7 +444,8 @@ void VideoTrackObject::rasterizeFrame(not_null<Frame*> frame) {
}
frame->format = FrameFormat::YUV420;
} else {
frame->alpha = (frame->decoded->format == AV_PIX_FMT_BGRA);
frame->alpha = (frame->decoded->format == AV_PIX_FMT_BGRA)
|| (frame->decoded->format == AV_PIX_FMT_YUVA420P);
frame->yuv420.size = {
frame->decoded->width,
frame->decoded->height
@ -1110,8 +1111,11 @@ QImage VideoTrack::frame(
&& frame->format == FrameFormat::YUV420) {
frame->original = ConvertToARGB32(frame->yuv420);
}
if (!frame->alpha
&& GoodForRequest(frame->original, _streamRotation, useRequest)) {
if (GoodForRequest(
frame->original,
frame->alpha,
_streamRotation,
useRequest)) {
return frame->original;
} else if (changed || none || i->second.image.isNull()) {
const auto j = none
@ -1187,8 +1191,11 @@ void VideoTrack::PrepareFrameByRequests(
const auto end = frame->prepared.end();
for (auto i = begin; i != end; ++i) {
auto &prepared = i->second;
if (frame->alpha
|| !GoodForRequest(frame->original, rotation, prepared.request)) {
if (!GoodForRequest(
frame->original,
frame->alpha,
rotation,
prepared.request)) {
auto j = begin;
for (; j != i; ++j) {
if (j->second.request == prepared.request) {

View File

@ -619,6 +619,7 @@ bool FileLoadTask::CheckForVideo(
static const auto extensions = {
qstr(".mp4"),
qstr(".mov"),
qstr(".webm"),
};
if (!CheckMimeOrExtensions(filepath, result->filemime, mimes, extensions)) {
return false;

View File

@ -298,7 +298,7 @@ void PrepareDetails(PreparedFile &file, int previewWidth) {
UpdateImageDetails(file, previewWidth);
file.type = PreparedFile::Type::Photo;
} else if (Core::IsMimeSticker(file.information->filemime)
|| image->animated) {
|| image->animated) {
file.type = PreparedFile::Type::None;
}
} else if (const auto video = std::get_if<Video>(

View File

@ -31,6 +31,7 @@ struct PreparedFileInformation {
};
struct Video {
bool isGifv = false;
bool isWebmSticker = false;
bool supportsStreaming = false;
int duration = -1;
QImage thumbnail;

View File

@ -262,7 +262,7 @@ QPixmap MediaPreviewWidget::currentImage() const {
if (_document) {
if (const auto sticker = _document->sticker()) {
if (_cacheStatus != CacheLoaded) {
if (sticker->animated && !_lottie && _documentMedia->loaded()) {
if (sticker->isLottie() && !_lottie && _documentMedia->loaded()) {
const_cast<MediaPreviewWidget*>(this)->setupLottie();
}
if (_lottie && _lottie->ready()) {