Improve checks for video frame sizes.

This commit is contained in:
John Preston
2026-04-30 23:52:45 +07:00
parent b1f8021847
commit 9eda44ca31
13 changed files with 184 additions and 79 deletions
+6 -4
View File
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/data_types.h"
#include "media/media_common.h"
#include "ui/widgets/fields/input_field.h"
#include "storage/cache/storage_cache_types.h"
#include "base/openssl_help.h"
@@ -157,8 +158,9 @@ BusinessShortcutId BusinessShortcutIdFromMessage(
bool GoodStickerDimensions(int width, int height) {
// Show all .webp (except very large ones) as stickers,
// allow to open them in media viewer to see details.
constexpr auto kLargetsStickerSide = 2560;
return (width > 0)
&& (height > 0)
&& (width * height <= kLargetsStickerSide * kLargetsStickerSide);
constexpr auto kLargestStickerSide = 2560;
return ::Media::ValidFrameSize(
width,
height,
kLargestStickerSide * kLargestStickerSide);
}
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ffmpeg/ffmpeg_frame_generator.h"
#include "ffmpeg/ffmpeg_utility.h"
#include "media/media_common.h"
#include "base/debug_log.h"
namespace FFmpeg {
@@ -15,6 +16,8 @@ namespace {
constexpr auto kMaxArea = 1920 * 1080 * 4;
using ::Media::ValidFrameSize;
} // namespace
class FrameGenerator::Impl final {
@@ -103,7 +106,11 @@ FrameGenerator::Impl::Impl(const QByteArray &bytes)
const auto info = _format->streams[_streamId];
_rotation = ReadRotationFromMetadata(info);
//_aspect = ValidateAspectRatio(info->sample_aspect_ratio);
_codec = MakeCodecPointer({ .stream = info });
_codec = MakeCodecPointer({
.stream = info,
.hwAllowed = false,
.videoMaxArea = kMaxArea,
});
}
int FrameGenerator::Impl::Read(void *opaque, uint8_t *buf, int buf_size) {
@@ -157,7 +164,7 @@ FrameGenerator::Frame FrameGenerator::Impl::renderCurrent(
const auto width = frame->width;
const auto height = frame->height;
if (!width || !height) {
LOG(("Webm Error: Bad frame size: %1x%2 ").arg(width).arg(height));
LOG(("Webm Error: Bad frame size %1x%2").arg(width).arg(height));
return {};
}
@@ -167,6 +174,10 @@ FrameGenerator::Frame FrameGenerator::Impl::renderCurrent(
}
if (!GoodStorageForFrame(storage, size)) {
storage = CreateFrameStorage(size);
if (storage.isNull()) {
LOG(("Webm Error: Bad frame size %1x%2").arg(width).arg(height));
return {};
}
}
const auto dx = (size.width() - scaled.width()) / 2;
const auto dy = (size.height() - scaled.height()) / 2;
@@ -317,7 +328,7 @@ void FrameGenerator::Impl::readNextFrame() {
while (true) {
auto result = avcodec_receive_frame(_codec.get(), frame.get());
if (result >= 0) {
if (frame->width * frame->height > kMaxArea) {
if (!ValidFrameSize(frame->width, frame->height, kMaxArea)) {
return;
}
_next.frame = std::move(frame);
+78 -18
View File
@@ -16,6 +16,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#endif // !Q_OS_WIN && !Q_OS_MAC
#include <QImage>
#include <limits>
#include <new>
#ifdef LIB_FFMPEG_USE_QT_PRIVATE_API
#include <private/qdrawhelper_p.h>
@@ -44,6 +46,7 @@ constexpr auto kAlignImageBy = 64;
constexpr auto kImageFormat = QImage::Format_ARGB32_Premultiplied;
constexpr auto kMaxScaleByAspectRatio = 16;
constexpr auto kAvioBlockSize = 4096;
constexpr auto kMaxFrameStorageBytes = 64 * 1024 * 1024;
constexpr auto kTimeUnknown = std::numeric_limits<crl::time>::min();
constexpr auto kDurationMax = crl::time(std::numeric_limits<int>::max());
@@ -56,11 +59,47 @@ struct HwAccelDescriptor {
AVPixelFormat format = AV_PIX_FMT_NONE;
};
void AlignedImageBufferCleanupHandler(void* data) {
struct AlignedFrameStorageLayout {
int width = 0;
int height = 0;
int bytesPerLine = 0;
int totalBytes = 0;
};
void AlignedImageBufferCleanupHandler(void *data) {
const auto buffer = static_cast<uchar*>(data);
delete[] buffer;
}
[[nodiscard]] bool ComputeAlignedFrameStorageLayout(
QSize size,
AlignedFrameStorageLayout *out) {
const auto width = size.width();
const auto height = size.height();
if (width <= 0 || height <= 0) {
return false;
}
const auto widthAlign = kAlignImageBy / kPixelBytesSize;
const auto widthRemainder = width % widthAlign;
const auto widthPadding = widthRemainder
? (widthAlign - widthRemainder)
: 0;
const auto alignedWidth = int64_t(width) + widthPadding;
const auto bytesPerLine = int64_t(alignedWidth) * kPixelBytesSize;
if (bytesPerLine > kMaxFrameStorageBytes) {
return false;
}
const auto totalBytes = int64_t(bytesPerLine) * height + kAlignImageBy;
if (totalBytes > kMaxFrameStorageBytes) {
return false;
}
out->width = width;
out->height = height;
out->bytesPerLine = int(bytesPerLine);
out->totalBytes = int(totalBytes);
return true;
}
[[nodiscard]] bool IsValidAspectRatio(AVRational aspect) {
return (aspect.num > 0)
&& (aspect.den > 0)
@@ -394,6 +433,10 @@ CodecPointer MakeCodecPointer(CodecDescriptor descriptor) {
return {};
}
context->pkt_timebase = stream->time_base;
if ((descriptor.videoMaxArea > 0)
&& (context->codec_type == AVMEDIA_TYPE_VIDEO)) {
context->max_pixels = descriptor.videoMaxArea;
}
av_opt_set(context, "threads", "auto", 0);
av_opt_set_int(context, "refcounted_frames", 1, 0);
@@ -684,27 +727,26 @@ bool GoodStorageForFrame(const QImage &storage, QSize size) {
// Create a QImage of desired size where all the data is properly aligned.
QImage CreateFrameStorage(QSize size) {
const auto width = size.width();
const auto height = size.height();
const auto widthAlign = kAlignImageBy / kPixelBytesSize;
const auto neededWidth = width + ((width % widthAlign)
? (widthAlign - (width % widthAlign))
: 0);
const auto perLine = neededWidth * kPixelBytesSize;
const auto buffer = new uchar[size_t(perLine) * height + kAlignImageBy];
const auto cleanupData = static_cast<void *>(buffer);
auto layout = AlignedFrameStorageLayout();
if (!ComputeAlignedFrameStorageLayout(size, &layout)) {
return {};
}
const auto buffer = new (std::nothrow) uchar[layout.totalBytes];
if (!buffer) {
return {};
}
const auto address = reinterpret_cast<uintptr_t>(buffer);
const auto alignedBuffer = buffer + ((address % kAlignImageBy)
? (kAlignImageBy - (address % kAlignImageBy))
: 0);
return QImage(
alignedBuffer,
width,
height,
perLine,
layout.width,
layout.height,
layout.bytesPerLine,
kImageFormat,
AlignedImageBufferCleanupHandler,
cleanupData);
buffer);
}
void UnPremultiply(QImage &dst, const QImage &src) {
@@ -712,21 +754,32 @@ void UnPremultiply(QImage &dst, const QImage &src) {
// as an image in QImage::Format_ARGB32 format.
if (!GoodStorageForFrame(dst, src.size())) {
dst = CreateFrameStorage(src.size());
if (dst.isNull()) {
return;
}
}
const auto srcPerLine = src.bytesPerLine();
const auto dstPerLine = dst.bytesPerLine();
const auto width = src.width();
const auto height = src.height();
if (width <= 0 || height <= 0) {
return;
}
const auto packedLine = int64_t(width) * kPixelBytesSize;
const auto packedCount = int64_t(width) * height;
const auto fast = (srcPerLine == packedLine)
&& (dstPerLine == packedLine)
&& (packedCount <= kMaxFrameStorageBytes);
auto srcBytes = src.bits();
auto dstBytes = dst.bits();
if (srcPerLine != width * 4 || dstPerLine != width * 4) {
if (!fast) {
for (auto i = 0; i != height; ++i) {
UnPremultiplyLine(dstBytes, srcBytes, width);
srcBytes += srcPerLine;
dstBytes += dstPerLine;
}
} else {
UnPremultiplyLine(dstBytes, srcBytes, width * height);
UnPremultiplyLine(dstBytes, srcBytes, int(packedCount));
}
}
@@ -734,14 +787,21 @@ void PremultiplyInplace(QImage &image) {
const auto perLine = image.bytesPerLine();
const auto width = image.width();
const auto height = image.height();
if (width <= 0 || height <= 0) {
return;
}
const auto packedLine = int64_t(width) * kPixelBytesSize;
const auto packedCount = int64_t(width) * height;
const auto fast = (perLine == packedLine)
&& (packedCount <= kMaxFrameStorageBytes);
auto bytes = image.bits();
if (perLine != width * 4) {
if (!fast) {
for (auto i = 0; i != height; ++i) {
PremultiplyLine(bytes, bytes, width);
bytes += perLine;
}
} else {
PremultiplyLine(bytes, bytes, width * height);
PremultiplyLine(bytes, bytes, int(packedCount));
}
}
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/bytes.h"
#include "base/algorithm.h"
#include <cstdint>
#include <crl/crl_time.h>
#include <QSize>
@@ -153,6 +154,7 @@ using CodecPointer = std::unique_ptr<AVCodecContext, CodecDeleter>;
struct CodecDescriptor {
not_null<AVStream*> stream;
bool hwAllowed = false;
int64_t videoMaxArea = 0;
};
[[nodiscard]] CodecPointer MakeCodecPointer(CodecDescriptor descriptor);
@@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_session_settings.h"
#include "media/audio/media_audio.h"
#include "media/clip/media_clip_reader.h"
#include "media/media_common.h"
#include "media/player/media_player_instance.h"
#include "media/streaming/media_streaming_instance.h"
#include "media/streaming/media_streaming_player.h"
@@ -72,6 +73,8 @@ constexpr auto kMaxInlineArea = 1920 * 1080;
constexpr auto kSeekAnimationDuration = crl::time(200);
constexpr auto kSeekTrackOpacity = 0.2;
using ::Media::ValidFrameSize;
[[nodiscard]] int GifMaxStatusWidth(not_null<DocumentData*> document) {
auto result = st::normalFont->width(
Ui::FormatDownloadText(document->size, document->size));
@@ -244,8 +247,7 @@ Gif::~Gif() {
}
bool Gif::CanPlayInline(not_null<DocumentData*> document) {
const auto dimensions = document->dimensions;
return dimensions.width() * dimensions.height() <= kMaxInlineArea;
return ValidFrameSize(document->dimensions, kMaxInlineArea);
}
QSize Gif::sizeForAspectRatio() const {
@@ -2292,9 +2294,10 @@ void Gif::repaintStreamedContent() {
}
void Gif::streamingReady(::Media::Streaming::Information &&info) {
if (info.video.size.width() * info.video.size.height()
> kMaxInlineArea) {
_data->dimensions = info.video.size;
if (!ValidFrameSize(info.video.size, kMaxInlineArea)) {
if (!info.video.size.isEmpty()) {
_data->dimensions = info.video.size;
}
stopAnimation();
} else {
history()->owner().requestViewResize(_parent);
@@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lottie/lottie_single_player.h"
#include "media/audio/media_audio.h"
#include "media/clip/media_clip_reader.h"
#include "media/media_common.h"
#include "media/player/media_player_instance.h"
#include "history/history_location_manager.h"
#include "history/view/history_view_cursor_state.h"
@@ -39,11 +40,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace InlineBots {
namespace Layout {
namespace internal {
namespace {
using TextState = HistoryView::TextState;
constexpr auto kMaxInlineArea = 1280 * 720;
using ::Media::ValidFrameSize;
[[nodiscard]] QSize ScaleDown(int w, int h, int maxW, int maxH) {
if (w * maxH > h * maxW) {
if (maxH < h) {
@@ -60,10 +64,11 @@ constexpr auto kMaxInlineArea = 1280 * 720;
}
[[nodiscard]] bool CanPlayInline(not_null<DocumentData*> document) {
const auto dimensions = document->dimensions;
return dimensions.width() * dimensions.height() <= kMaxInlineArea;
return ValidFrameSize(document->dimensions, kMaxInlineArea);
}
} // namespace
FileBase::FileBase(not_null<Context*> context, std::shared_ptr<Result> result)
: ItemBase(context, std::move(result)) {
}
@@ -430,10 +435,11 @@ void Gif::clipCallback(Media::Clip::Notification notification) {
if (_gif->state() == State::Error) {
_gif.setBad();
} else if (_gif->ready() && !_gif->started()) {
if (_gif->width() * _gif->height() > kMaxInlineArea) {
getShownDocument()->dimensions = QSize(
_gif->width(),
_gif->height());
const auto size = QSize(_gif->width(), _gif->height());
if (!ValidFrameSize(size, kMaxInlineArea)) {
if (!size.isEmpty()) {
getShownDocument()->dimensions = size;
}
_gif.reset();
} else {
_gif->start({
@@ -1915,10 +1921,11 @@ void Game::clipCallback(Media::Clip::Notification notification) {
if (_gif->state() == State::Error) {
_gif.setBad();
} else if (_gif->ready() && !_gif->started()) {
if (_gif->width() * _gif->height() > kMaxInlineArea) {
getResultDocument()->dimensions = QSize(
_gif->width(),
_gif->height());
const auto size = QSize(_gif->width(), _gif->height());
if (!ValidFrameSize(size, kMaxInlineArea)) {
if (!size.isEmpty()) {
getResultDocument()->dimensions = size;
}
_gif.reset();
} else {
_gif->start({
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "media/clip/media_clip_ffmpeg.h"
#include "core/file_location.h"
#include "media/media_common.h"
#include "logs.h"
namespace Media {
@@ -19,30 +20,10 @@ constexpr auto kSkipInvalidDataPackets = 10;
constexpr auto kMaxInlineArea = 1280 * 720;
constexpr auto kMaxSendingArea = 3840 * 2160; // usual 4K
// See https://github.com/telegramdesktop/tdesktop/issues/7225
constexpr auto kAlignImageBy = 64;
void alignedImageBufferCleanupHandler(void *data) {
auto buffer = static_cast<uchar*>(data);
delete[] buffer;
}
// Create a QImage of desired size where all the data is aligned to 16 bytes.
QImage createAlignedImage(QSize size) {
auto width = size.width();
auto height = size.height();
auto widthalign = kAlignImageBy / 4;
auto neededwidth = width + ((width % widthalign) ? (widthalign - (width % widthalign)) : 0);
auto bytesperline = neededwidth * 4;
auto buffer = new uchar[bytesperline * height + kAlignImageBy];
auto cleanupdata = static_cast<void*>(buffer);
auto bufferval = reinterpret_cast<uintptr_t>(buffer);
auto alignedbuffer = buffer + ((bufferval % kAlignImageBy) ? (kAlignImageBy - (bufferval % kAlignImageBy)) : 0);
return QImage(alignedbuffer, width, height, bytesperline, QImage::Format_ARGB32_Premultiplied, alignedImageBufferCleanupHandler, cleanupdata);
}
bool isAlignedImage(const QImage &image) {
return !(reinterpret_cast<uintptr_t>(image.constBits()) % kAlignImageBy) && !(image.bytesPerLine() % kAlignImageBy);
[[nodiscard]] auto MaxAreaForMode(ReaderImplementation::Mode mode) {
return (mode == ReaderImplementation::Mode::Inspecting)
? kMaxSendingArea
: kMaxInlineArea;
}
} // namespace
@@ -58,10 +39,8 @@ ReaderImplementation::ReadResult FFMpegReaderImplementation::readNextFrame() {
do {
int res = avcodec_receive_frame(_codecContext, _frame.get());
if (res >= 0) {
const auto limit = (_mode == Mode::Inspecting)
? kMaxSendingArea
: kMaxInlineArea;
if (_frame->width * _frame->height > limit) {
const auto limit = MaxAreaForMode(_mode);
if (!::Media::ValidFrameSize(_frame->width, _frame->height, limit)) {
return ReadResult::Error;
}
processReadFrame();
@@ -223,8 +202,12 @@ bool FFMpegReaderImplementation::renderFrame(
if (!size.isEmpty() && rotationSwapWidthHeight()) {
toSize.transpose();
}
if (to.isNull() || to.size() != toSize || !to.isDetached() || !isAlignedImage(to)) {
to = createAlignedImage(toSize);
if (!FFmpeg::GoodStorageForFrame(to, toSize)) {
to = FFmpeg::CreateFrameStorage(toSize);
if (to.isNull()) {
LOG(("Gif Error: Bad storage size %1").arg(logData()));
return false;
}
}
const auto format = (_frame->format == AV_PIX_FMT_NONE)
? _codecContext->pix_fmt
@@ -346,6 +329,7 @@ bool FFMpegReaderImplementation::start(Mode mode, crl::time &positionMs) {
const auto audioStreamId = av_find_best_stream(_fmtContext, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);
_hasAudioStream = (audioStreamId >= 0);
}
_codecContext->max_pixels = MaxAreaForMode(_mode);
if ((res = avcodec_open2(_codecContext, codec, nullptr)) < 0) {
LOG(("Gif Error: Unable to avcodec_open2 %1, error %2, %3").arg(logData()).arg(res).arg(av_make_error_string(err, sizeof(err), res)));
+11
View File
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/algorithm.h"
#include <compare>
#include <cstdint>
namespace Media {
@@ -42,6 +43,16 @@ inline constexpr auto kSpeedMin = 0.5;
inline constexpr auto kSpeedMax = 2.5;
inline constexpr auto kSpedUpDefault = 1.7;
[[nodiscard]] inline bool ValidFrameSize(int w, int h, int maxArea) {
return (w > 0)
&& (h > 0)
&& (int64_t(w) * h <= int64_t(maxArea));
}
[[nodiscard]] inline bool ValidFrameSize(QSize size, int maxArea) {
return ValidFrameSize(size.width(), size.height(), maxArea);
}
[[nodiscard]] inline bool EqualSpeeds(float64 a, float64 b) {
return int(base::SafeRound(a * 10.)) == int(base::SafeRound(b * 10.));
}
@@ -23,6 +23,8 @@ bool SupportsSpeedControl();
namespace Streaming {
inline constexpr auto kMaxFrameArea = 3840 * 2160;
inline bool SupportsSpeedControl() {
return Media::Audio::SupportsSpeedControl();
}
@@ -172,6 +172,7 @@ Stream File::Context::initStream(
result.codec = FFmpeg::MakeCodecPointer({
.stream = info,
.hwAllowed = options.hwAllow,
.videoMaxArea = kMaxFrameArea,
});
if (!result.codec) {
return result;
@@ -154,6 +154,9 @@ QImage ConvertFrame(
if (!FFmpeg::GoodStorageForFrame(storage, resize)) {
storage = FFmpeg::CreateFrameStorage(resize);
if (storage.isNull()) {
return QImage();
}
}
const auto format = AV_PIX_FMT_BGRA;
@@ -429,6 +432,9 @@ QImage PrepareByRequest(
: request.outer;
if (!FFmpeg::GoodStorageForFrame(storage, outer)) {
storage = FFmpeg::CreateFrameStorage(outer);
if (storage.isNull()) {
return QImage();
}
}
if (hasAlpha && request.keepAlpha) {
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ffmpeg/ffmpeg_utility.h"
#include "media/audio/media_audio.h"
#include "media/media_common.h"
#include "base/concurrent_timer.h"
#include "core/crash_reports.h"
#include "base/debug_log.h"
@@ -17,11 +18,12 @@ namespace Media {
namespace Streaming {
namespace {
constexpr auto kMaxFrameArea = 3840 * 2160; // usual 4K
constexpr auto kDisplaySkipped = crl::time(-1);
constexpr auto kFinishedPosition = std::numeric_limits<crl::time>::max();
static_assert(kDisplaySkipped != kTimeUnknown);
using ::Media::ValidFrameSize;
[[nodiscard]] QImage ConvertToARGB32(
FrameFormat format,
const FrameYUV &data) {
@@ -35,6 +37,9 @@ static_assert(kDisplaySkipped != kTimeUnknown);
//}
auto result = FFmpeg::CreateFrameStorage(data.size);
if (result.isNull()) {
return QImage();
}
const auto swscale = FFmpeg::MakeSwscalePointer(
data.size,
(format == FrameFormat::YUV420
@@ -374,7 +379,11 @@ auto VideoTrackObject::readFrame(not_null<Frame*> frame) -> FrameResult {
return FrameResult::Waiting;
}
const auto decodedFrame = _stream.decodedFrame.get();
if (int64(decodedFrame->width) * decodedFrame->height > kMaxFrameArea) {
const auto valid = ValidFrameSize(
decodedFrame->width,
decodedFrame->height,
kMaxFrameArea);
if (!valid) {
fail(Error::InvalidData);
return FrameResult::Error;
}
@@ -654,7 +663,11 @@ bool VideoTrackObject::tryReadFirstFrame(FFmpeg::Packet &&packet) {
bool VideoTrackObject::processFirstFrame() {
const auto decodedFrame = _stream.decodedFrame.get();
if (int64(decodedFrame->width) * decodedFrame->height > kMaxFrameArea) {
const auto valid = ValidFrameSize(
decodedFrame->width,
decodedFrame->height,
kMaxFrameArea);
if (!valid) {
return false;
} else if (decodedFrame->hw_frames_ctx) {
if (!_stream.transferredFrame) {
@@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "storage/file_upload.h"
#include "main/main_session.h"
#include "media/audio/media_audio.h"
#include "media/media_common.h"
#include "media/player/media_player_instance.h"
#include "storage/localstorage.h"
#include "history/history.h"
@@ -65,9 +66,10 @@ TextParseOptions _documentNameOptions = {
constexpr auto kMaxInlineArea = 1280 * 720;
constexpr auto kStoryRatio = 1.46;
using ::Media::ValidFrameSize;
[[nodiscard]] bool CanPlayInline(not_null<DocumentData*> document) {
const auto dimensions = document->dimensions;
return dimensions.width() * dimensions.height() <= kMaxInlineArea;
return ValidFrameSize(document->dimensions, kMaxInlineArea);
}
[[nodiscard]] QImage CropMediaFrame(QImage image, int width, int height) {
@@ -2175,10 +2177,11 @@ void Gif::clipCallback(Media::Clip::Notification notification) {
if (_gif->state() == State::Error) {
_gif.setBad();
} else if (_gif->ready() && !_gif->started()) {
if (_gif->width() * _gif->height() > kMaxInlineArea) {
_data->dimensions = QSize(
_gif->width(),
_gif->height());
const auto size = QSize(_gif->width(), _gif->height());
if (!ValidFrameSize(size, kMaxInlineArea)) {
if (!size.isEmpty()) {
_data->dimensions = size;
}
_gif.reset();
} else {
_gif->start({