init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ffmpeg/ffmpeg_utility.h"
namespace FFmpeg {
struct ReadBytesWrap {
int64 size = 0;
int64 offset = 0;
const uchar *data = nullptr;
static int Read(void *opaque, uint8_t *buf, int buf_size) {
auto wrap = static_cast<ReadBytesWrap*>(opaque);
const auto toRead = std::min(
int64(buf_size),
wrap->size - wrap->offset);
if (!toRead) {
return AVERROR_EOF;
} else if (toRead > 0) {
memcpy(buf, wrap->data + wrap->offset, toRead);
wrap->offset += toRead;
}
return toRead;
}
static int64_t Seek(void *opaque, int64_t offset, int whence) {
auto wrap = static_cast<ReadBytesWrap*>(opaque);
auto updated = int64(-1);
switch (whence) {
case SEEK_SET: updated = offset; break;
case SEEK_CUR: updated = wrap->offset + offset; break;
case SEEK_END: updated = wrap->size + offset; break;
case AVSEEK_SIZE: return wrap->size; break;
}
if (updated < 0 || updated > wrap->size) {
return -1;
}
wrap->offset = updated;
return updated;
}
};
struct WriteBytesWrap {
QByteArray content;
int64 offset = 0;
#if DA_FFMPEG_CONST_WRITE_CALLBACK
static int Write(void *opaque, const uint8_t *_buf, int buf_size) {
uint8_t *buf = const_cast<uint8_t *>(_buf);
#else
static int Write(void *opaque, uint8_t *buf, int buf_size) {
#endif
auto wrap = static_cast<WriteBytesWrap*>(opaque);
if (const auto total = wrap->offset + int64(buf_size)) {
const auto size = int64(wrap->content.size());
constexpr auto kReserve = 1024 * 1024;
wrap->content.reserve((total / kReserve) * kReserve);
const auto overwrite = std::min(
size - wrap->offset,
int64(buf_size));
if (overwrite) {
memcpy(wrap->content.data() + wrap->offset, buf, overwrite);
}
if (const auto append = buf_size - overwrite) {
wrap->content.append(
reinterpret_cast<const char*>(buf) + overwrite,
append);
}
wrap->offset += buf_size;
}
return buf_size;
}
static int64_t Seek(void *opaque, int64_t offset, int whence) {
auto wrap = static_cast<WriteBytesWrap*>(opaque);
const auto &content = wrap->content;
const auto checkedSeek = [&](int64_t offset) {
if (offset < 0 || offset > int64(content.size())) {
return int64_t(-1);
}
return int64_t(wrap->offset = offset);
};
switch (whence) {
case SEEK_SET: return checkedSeek(offset);
case SEEK_CUR: return checkedSeek(wrap->offset + offset);
case SEEK_END: return checkedSeek(int64(content.size()) + offset);
case AVSEEK_SIZE: return int64(content.size());
}
return -1;
}
};
} // namespace FFmpeg

View File

@@ -0,0 +1,414 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ffmpeg/ffmpeg_frame_generator.h"
#include "ffmpeg/ffmpeg_utility.h"
#include "base/debug_log.h"
namespace FFmpeg {
namespace {
constexpr auto kMaxArea = 1920 * 1080 * 4;
} // namespace
class FrameGenerator::Impl final {
public:
explicit Impl(const QByteArray &bytes);
[[nodiscard]] Frame renderNext(
QImage storage,
QSize size,
Qt::AspectRatioMode mode);
[[nodiscard]] Frame renderCurrent(
QImage storage,
QSize size,
Qt::AspectRatioMode mode);
void jumpToStart();
private:
struct ReadFrame {
FramePointer frame;
crl::time position = 0;
crl::time duration = 0;
};
void readNextFrame();
void resolveNextFrameTiming();
[[nodiscard]] QString wrapError(int result) const;
bool rotationSwapWidthHeight() const {
return (_rotation == 90) || (_rotation == 270);
}
[[nodiscard]] static int Read(
void *opaque,
uint8_t *buf,
int buf_size);
[[nodiscard]] static int64_t Seek(
void *opaque,
int64_t offset,
int whence);
[[nodiscard]] int read(uint8_t *buf, int buf_size);
[[nodiscard]] int64_t seek(int64_t offset, int whence);
const QByteArray _bytes;
int _deviceOffset = 0;
FormatPointer _format;
ReadFrame _current;
ReadFrame _next;
CodecPointer _codec;
SwscalePointer _scale;
int _streamId = 0;
int _rotation = 0;
//AVRational _aspect = kNormalAspect;
crl::time _framePosition = 0;
int _nextFrameDelay = 0;
int _currentFrameDelay = 0;
};
FrameGenerator::Impl::Impl(const QByteArray &bytes)
: _bytes(bytes) {
_format = MakeFormatPointer(
static_cast<void*>(this),
&FrameGenerator::Impl::Read,
nullptr,
&FrameGenerator::Impl::Seek);
auto error = 0;
if ((error = avformat_find_stream_info(_format.get(), nullptr))) {
return;
}
_streamId = av_find_best_stream(
_format.get(),
AVMEDIA_TYPE_VIDEO,
-1,
-1,
nullptr,
0);
if (_streamId < 0) {
return;
}
const auto info = _format->streams[_streamId];
_rotation = ReadRotationFromMetadata(info);
//_aspect = ValidateAspectRatio(info->sample_aspect_ratio);
_codec = MakeCodecPointer({ .stream = info });
}
int FrameGenerator::Impl::Read(void *opaque, uint8_t *buf, int buf_size) {
return static_cast<Impl*>(opaque)->read(buf, buf_size);
}
int FrameGenerator::Impl::read(uint8_t *buf, int buf_size) {
const auto available = _bytes.size() - _deviceOffset;
if (available <= 0) {
return AVERROR_EOF;
}
const auto fill = std::min(int(available), buf_size);
memcpy(buf, _bytes.data() + _deviceOffset, fill);
_deviceOffset += fill;
return fill;
}
int64_t FrameGenerator::Impl::Seek(
void *opaque,
int64_t offset,
int whence) {
return static_cast<Impl*>(opaque)->seek(offset, whence);
}
int64_t FrameGenerator::Impl::seek(int64_t offset, int whence) {
if (whence == AVSEEK_SIZE) {
return _bytes.size();
}
const auto now = [&] {
switch (whence) {
case SEEK_SET: return offset;
case SEEK_CUR: return _deviceOffset + offset;
case SEEK_END: return int64_t(_bytes.size()) + offset;
}
return int64_t(-1);
}();
if (now < 0 || now > _bytes.size()) {
return -1;
}
_deviceOffset = now;
return now;
}
FrameGenerator::Frame FrameGenerator::Impl::renderCurrent(
QImage storage,
QSize size,
Qt::AspectRatioMode mode) {
Expects(_current.frame != nullptr);
const auto frame = _current.frame.get();
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));
return {};
}
auto scaled = QSize(width, height).scaled(size, mode);
if (!scaled.isEmpty() && rotationSwapWidthHeight()) {
scaled.transpose();
}
if (!GoodStorageForFrame(storage, size)) {
storage = CreateFrameStorage(size);
}
const auto dx = (size.width() - scaled.width()) / 2;
const auto dy = (size.height() - scaled.height()) / 2;
Assert(dx >= 0 && dy >= 0 && (!dx || !dy));
const auto srcFormat = (frame->format == AV_PIX_FMT_NONE)
? _codec->pix_fmt
: frame->format;
const auto srcSize = QSize(frame->width, frame->height);
const auto dstFormat = AV_PIX_FMT_BGRA;
const auto dstSize = scaled;
const auto bgra = (srcFormat == AV_PIX_FMT_BGRA);
const auto withAlpha = bgra || (srcFormat == AV_PIX_FMT_YUVA420P);
const auto dstPerLine = storage.bytesPerLine();
auto dst = storage.bits() + dx * sizeof(int32) + dy * dstPerLine;
if (srcSize == dstSize && bgra) {
const auto srcPerLine = frame->linesize[0];
const auto perLine = std::min(srcPerLine, int(dstPerLine));
auto src = frame->data[0];
for (auto y = 0, height = srcSize.height(); y != height; ++y) {
memcpy(dst, src, perLine);
src += srcPerLine;
dst += dstPerLine;
}
} else {
_scale = MakeSwscalePointer(
srcSize,
srcFormat,
dstSize,
dstFormat,
&_scale);
Assert(_scale != nullptr);
// AV_NUM_DATA_POINTERS defined in AVFrame struct
uint8_t *dstData[AV_NUM_DATA_POINTERS] = { dst, nullptr };
int dstLinesize[AV_NUM_DATA_POINTERS] = { int(dstPerLine), 0 };
sws_scale(
_scale.get(),
frame->data,
frame->linesize,
0,
frame->height,
dstData,
dstLinesize);
}
if (dx && size.height() > 0) {
auto dst = storage.bits();
const auto line = scaled.width() * sizeof(int32);
memset(dst, 0, dx * sizeof(int32));
dst += dx * sizeof(int32);
for (auto y = 0; y != size.height() - 1; ++y) {
memset(dst + line, 0, (dstPerLine - line));
dst += dstPerLine;
}
dst += line;
memset(dst, 0, (size.width() - scaled.width() - dx) * sizeof(int32));
} else if (dy && size.width() > 0) {
const auto dst = storage.bits();
memset(dst, 0, dstPerLine * dy);
memset(
dst + dstPerLine * (dy + scaled.height()),
0,
dstPerLine * (size.height() - scaled.height() - dy));
}
if (withAlpha) {
PremultiplyInplace(storage);
}
if (_rotation != 0) {
auto transform = QTransform();
transform.rotate(_rotation);
storage = storage.transformed(transform);
}
const auto duration = _next.frame
? (_next.position - _current.position)
: _current.duration;
return {
.duration = duration,
.image = std::move(storage),
.last = !_next.frame,
};
}
FrameGenerator::Frame FrameGenerator::Impl::renderNext(
QImage storage,
QSize size,
Qt::AspectRatioMode mode) {
if (!_codec) {
return {};
} else if (!_current.frame) {
readNextFrame();
}
std::swap(_current, _next);
if (!_current.frame) {
return {};
}
readNextFrame();
return renderCurrent(std::move(storage), size, mode);
}
void FrameGenerator::Impl::jumpToStart() {
if (!_codec) {
return;
}
auto result = 0;
if ((result = avformat_seek_file(_format.get(), _streamId, std::numeric_limits<int64_t>::min(), 0, std::numeric_limits<int64_t>::max(), 0)) < 0) {
if ((result = av_seek_frame(_format.get(), _streamId, 0, AVSEEK_FLAG_BYTE)) < 0) {
if ((result = av_seek_frame(_format.get(), _streamId, 0, AVSEEK_FLAG_FRAME)) < 0) {
if ((result = av_seek_frame(_format.get(), _streamId, 0, 0)) < 0) {
LOG(("Webm Error: Unable to av_seek_frame() to the start, ") + wrapError(result));
return;
}
}
}
}
avcodec_flush_buffers(_codec.get());
_current = ReadFrame();
_next = ReadFrame();
_currentFrameDelay = _nextFrameDelay = 0;
_framePosition = 0;
}
void FrameGenerator::Impl::resolveNextFrameTiming() {
const auto base = _format->streams[_streamId]->time_base;
const auto duration = _next.frame->duration;
const auto framePts = _next.frame->pts;
auto framePosition = (framePts * 1000LL * base.num) / base.den;
_currentFrameDelay = _nextFrameDelay;
if (_framePosition + _currentFrameDelay < framePosition) {
_currentFrameDelay = int32(framePosition - _framePosition);
} else if (framePosition < _framePosition + _currentFrameDelay) {
framePosition = _framePosition + _currentFrameDelay;
}
if (duration == AV_NOPTS_VALUE) {
_nextFrameDelay = 0;
} else {
_nextFrameDelay = (duration * 1000LL * base.num) / base.den;
}
_framePosition = framePosition;
_next.position = _framePosition;
_next.duration = _nextFrameDelay;
}
void FrameGenerator::Impl::readNextFrame() {
auto frame = _next.frame ? base::take(_next.frame) : MakeFramePointer();
while (true) {
auto result = avcodec_receive_frame(_codec.get(), frame.get());
if (result >= 0) {
if (frame->width * frame->height > kMaxArea) {
return;
}
_next.frame = std::move(frame);
resolveNextFrameTiming();
return;
}
if (result == AVERROR_EOF) {
return;
} else if (result != AVERROR(EAGAIN)) {
LOG(("Webm Error: Unable to avcodec_receive_frame(), ")
+ wrapError(result));
return;
}
auto packet = Packet();
auto finished = false;
do {
const auto result = av_read_frame(
_format.get(),
&packet.fields());
if (result == AVERROR_EOF) {
finished = true;
break;
} else if (result < 0) {
LOG(("Webm Error: Unable to av_read_frame(), ")
+ wrapError(result));
return;
}
} while (packet.fields().stream_index != _streamId);
if (finished) {
result = avcodec_send_packet(_codec.get(), nullptr); // Drain.
} else {
const auto native = &packet.fields();
const auto guard = gsl::finally([
&,
size = native->size,
data = native->data
] {
native->size = size;
native->data = data;
packet = Packet();
});
result = avcodec_send_packet(_codec.get(), native);
}
if (result < 0) {
LOG(("Webm Error: Unable to avcodec_send_packet(), ")
+ wrapError(result));
return;
}
}
}
QString FrameGenerator::Impl::wrapError(int result) const {
auto error = std::array<char, AV_ERROR_MAX_STRING_SIZE>{};
return u"error %1, %2"_q
.arg(result)
.arg(av_make_error_string(error.data(), error.size(), result));
}
FrameGenerator::FrameGenerator(const QByteArray &bytes)
: _impl(std::make_unique<Impl>(bytes)) {
}
FrameGenerator::~FrameGenerator() = default;
int FrameGenerator::count() {
return 0;
}
double FrameGenerator::rate() {
return 0.;
}
FrameGenerator::Frame FrameGenerator::renderNext(
QImage storage,
QSize size,
Qt::AspectRatioMode mode) {
return _impl->renderNext(std::move(storage), size, mode);
}
FrameGenerator::Frame FrameGenerator::renderCurrent(
QImage storage,
QSize size,
Qt::AspectRatioMode mode) {
return _impl->renderCurrent(std::move(storage), size, mode);
}
void FrameGenerator::jumpToStart() {
_impl->jumpToStart();
}
} // namespace FFmpeg

View File

@@ -0,0 +1,41 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/effects/frame_generator.h"
#include <QtGui/QImage>
#include <memory>
namespace FFmpeg {
class FrameGenerator final : public Ui::FrameGenerator {
public:
explicit FrameGenerator(const QByteArray &bytes);
~FrameGenerator();
int count() override;
double rate() override;
Frame renderNext(
QImage storage,
QSize size,
Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio) override;
Frame renderCurrent(
QImage storage,
QSize size,
Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio) override;
void jumpToStart() override;
private:
class Impl;
std::unique_ptr<Impl> _impl;
};
} // namespace FFmpeg

View File

@@ -0,0 +1,748 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ffmpeg/ffmpeg_utility.h"
#include "base/algorithm.h"
#include "logs.h"
#if !defined Q_OS_WIN && !defined Q_OS_MAC
#include "base/platform/linux/base_linux_library.h"
#include <deque>
#endif // !Q_OS_WIN && !Q_OS_MAC
#include <QImage>
#ifdef LIB_FFMPEG_USE_QT_PRIVATE_API
#include <private/qdrawhelper_p.h>
#endif // LIB_FFMPEG_USE_QT_PRIVATE_API
extern "C" {
#include <libavutil/opt.h>
#include <libavutil/display.h>
} // extern "C"
#if !defined Q_OS_WIN && !defined Q_OS_MAC
extern "C" {
void _libvdpau_so_tramp_resolve_all(void) __attribute__((weak));
void _libva_drm_so_tramp_resolve_all(void) __attribute__((weak));
void _libva_x11_so_tramp_resolve_all(void) __attribute__((weak));
void _libva_so_tramp_resolve_all(void) __attribute__((weak));
void _libdrm_so_tramp_resolve_all(void) __attribute__((weak));
} // extern "C"
#endif // !Q_OS_WIN && !Q_OS_MAC
namespace FFmpeg {
namespace {
// See https://github.com/telegramdesktop/tdesktop/issues/7225
constexpr auto kAlignImageBy = 64;
constexpr auto kImageFormat = QImage::Format_ARGB32_Premultiplied;
constexpr auto kMaxScaleByAspectRatio = 16;
constexpr auto kAvioBlockSize = 4096;
constexpr auto kTimeUnknown = std::numeric_limits<crl::time>::min();
constexpr auto kDurationMax = crl::time(std::numeric_limits<int>::max());
using GetFormatMethod = enum AVPixelFormat(*)(
struct AVCodecContext *s,
const enum AVPixelFormat *fmt);
struct HwAccelDescriptor {
GetFormatMethod getFormat = nullptr;
AVPixelFormat format = AV_PIX_FMT_NONE;
};
void AlignedImageBufferCleanupHandler(void* data) {
const auto buffer = static_cast<uchar*>(data);
delete[] buffer;
}
[[nodiscard]] bool IsValidAspectRatio(AVRational aspect) {
return (aspect.num > 0)
&& (aspect.den > 0)
&& (aspect.num <= aspect.den * kMaxScaleByAspectRatio)
&& (aspect.den <= aspect.num * kMaxScaleByAspectRatio);
}
[[nodiscard]] bool IsAlignedImage(const QImage &image) {
return !(reinterpret_cast<uintptr_t>(image.bits()) % kAlignImageBy)
&& !(image.bytesPerLine() % kAlignImageBy);
}
void UnPremultiplyLine(uchar *dst, const uchar *src, int intsCount) {
[[maybe_unused]] const auto udst = reinterpret_cast<uint*>(dst);
const auto usrc = reinterpret_cast<const uint*>(src);
#ifndef LIB_FFMPEG_USE_QT_PRIVATE_API
for (auto i = 0; i != intsCount; ++i) {
udst[i] = qUnpremultiply(usrc[i]);
}
#else // !LIB_FFMPEG_USE_QT_PRIVATE_API
static const auto layout = &qPixelLayouts[QImage::Format_ARGB32];
layout->storeFromARGB32PM(dst, usrc, 0, intsCount, nullptr, nullptr);
#endif // LIB_FFMPEG_USE_QT_PRIVATE_API
}
void PremultiplyLine(uchar *dst, const uchar *src, int intsCount) {
const auto udst = reinterpret_cast<uint*>(dst);
[[maybe_unused]] const auto usrc = reinterpret_cast<const uint*>(src);
#ifndef LIB_FFMPEG_USE_QT_PRIVATE_API
for (auto i = 0; i != intsCount; ++i) {
udst[i] = qPremultiply(usrc[i]);
}
#else // !LIB_FFMPEG_USE_QT_PRIVATE_API
static const auto layout = &qPixelLayouts[QImage::Format_ARGB32];
layout->fetchToARGB32PM(udst, src, 0, intsCount, nullptr, nullptr);
#endif // LIB_FFMPEG_USE_QT_PRIVATE_API
}
#if !defined Q_OS_WIN && !defined Q_OS_MAC
[[nodiscard]] auto CheckHwLibs() {
auto list = std::deque{
AV_PIX_FMT_CUDA,
};
if (!_libvdpau_so_tramp_resolve_all
|| base::Platform::LoadLibrary("libvdpau.so.1")) {
list.push_front(AV_PIX_FMT_VDPAU);
}
if ([&] {
const auto list = std::array{
std::make_pair(_libva_drm_so_tramp_resolve_all, "libva-drm.so.2"),
std::make_pair(_libva_x11_so_tramp_resolve_all, "libva-x11.so.2"),
std::make_pair(_libva_so_tramp_resolve_all, "libva.so.2"),
std::make_pair(_libdrm_so_tramp_resolve_all, "libdrm.so.2"),
};
for (const auto &lib : list) {
if (lib.first && !base::Platform::LoadLibrary(lib.second)) {
return false;
}
}
return true;
}()) {
list.push_front(AV_PIX_FMT_VAAPI);
}
return list;
}
#endif // !Q_OS_WIN && !Q_OS_MAC
[[nodiscard]] bool InitHw(AVCodecContext *context, AVHWDeviceType type) {
AVCodecContext *parent = static_cast<AVCodecContext*>(context->opaque);
auto hwDeviceContext = (AVBufferRef*)nullptr;
AvErrorWrap error = av_hwdevice_ctx_create(
&hwDeviceContext,
type,
nullptr,
nullptr,
0);
if (error || !hwDeviceContext) {
LogError(u"av_hwdevice_ctx_create"_q, error);
return false;
}
DEBUG_LOG(("Video Info: "
"Trying \"%1\" hardware acceleration for \"%2\" decoder."
).arg(
av_hwdevice_get_type_name(type),
context->codec->name));
if (parent->hw_device_ctx) {
av_buffer_unref(&parent->hw_device_ctx);
}
parent->hw_device_ctx = av_buffer_ref(hwDeviceContext);
av_buffer_unref(&hwDeviceContext);
context->hw_device_ctx = parent->hw_device_ctx;
return true;
}
[[nodiscard]] enum AVPixelFormat GetHwFormat(
AVCodecContext *context,
const enum AVPixelFormat *formats) {
const auto has = [&](enum AVPixelFormat format) {
const enum AVPixelFormat *p = nullptr;
for (p = formats; *p != AV_PIX_FMT_NONE; p++) {
if (*p == format) {
return true;
}
}
return false;
};
#if defined Q_OS_WIN || defined Q_OS_MAC
const auto list = std::array{
#ifdef Q_OS_WIN
AV_PIX_FMT_D3D11,
AV_PIX_FMT_DXVA2_VLD,
AV_PIX_FMT_CUDA,
#elif defined Q_OS_MAC // Q_OS_WIN
AV_PIX_FMT_VIDEOTOOLBOX,
#endif // Q_OS_WIN || Q_OS_MAC
};
#else // Q_OS_WIN || Q_OS_MAC
static const auto list = CheckHwLibs();
#endif // !Q_OS_WIN && !Q_OS_MAC
for (const auto format : list) {
if (!has(format)) {
continue;
}
const auto type = [&] {
switch (format) {
#ifdef Q_OS_WIN
case AV_PIX_FMT_D3D11: return AV_HWDEVICE_TYPE_D3D11VA;
case AV_PIX_FMT_DXVA2_VLD: return AV_HWDEVICE_TYPE_DXVA2;
case AV_PIX_FMT_CUDA: return AV_HWDEVICE_TYPE_CUDA;
#elif defined Q_OS_MAC // Q_OS_WIN
case AV_PIX_FMT_VIDEOTOOLBOX:
return AV_HWDEVICE_TYPE_VIDEOTOOLBOX;
#else // Q_OS_WIN || Q_OS_MAC
case AV_PIX_FMT_VAAPI: return AV_HWDEVICE_TYPE_VAAPI;
case AV_PIX_FMT_VDPAU: return AV_HWDEVICE_TYPE_VDPAU;
case AV_PIX_FMT_CUDA: return AV_HWDEVICE_TYPE_CUDA;
#endif // Q_OS_WIN || Q_OS_MAC
}
return AV_HWDEVICE_TYPE_NONE;
}();
if (type == AV_HWDEVICE_TYPE_NONE && context->hw_device_ctx) {
av_buffer_unref(&context->hw_device_ctx);
} else if (type != AV_HWDEVICE_TYPE_NONE && !InitHw(context, type)) {
continue;
}
return format;
}
enum AVPixelFormat result = AV_PIX_FMT_NONE;
for (const enum AVPixelFormat *p = formats; *p != AV_PIX_FMT_NONE; p++) {
result = *p;
}
return result;
}
template <AVPixelFormat Required>
enum AVPixelFormat GetFormatImplementation(
AVCodecContext *ctx,
const enum AVPixelFormat *pix_fmts) {
const enum AVPixelFormat *p = nullptr;
for (p = pix_fmts; *p != -1; p++) {
if (*p == Required) {
return *p;
}
}
return AV_PIX_FMT_NONE;
}
} // namespace
IOPointer MakeIOPointer(
void *opaque,
int(*read)(void *opaque, uint8_t *buffer, int bufferSize),
#if DA_FFMPEG_CONST_WRITE_CALLBACK
int(*write)(void *opaque, const uint8_t *buffer, int bufferSize),
#else
int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
#endif
int64_t(*seek)(void *opaque, int64_t offset, int whence)) {
auto buffer = reinterpret_cast<uchar*>(av_malloc(kAvioBlockSize));
if (!buffer) {
LogError(u"av_malloc"_q);
return {};
}
auto result = IOPointer(avio_alloc_context(
buffer,
kAvioBlockSize,
write ? 1 : 0,
opaque,
read,
write,
seek));
if (!result) {
av_freep(&buffer);
LogError(u"avio_alloc_context"_q);
return {};
}
return result;
}
void IODeleter::operator()(AVIOContext *value) {
if (value) {
av_freep(&value->buffer);
avio_context_free(&value);
}
}
FormatPointer MakeFormatPointer(
void *opaque,
int(*read)(void *opaque, uint8_t *buffer, int bufferSize),
#if DA_FFMPEG_CONST_WRITE_CALLBACK
int(*write)(void *opaque, const uint8_t *buffer, int bufferSize),
#else
int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
#endif
int64_t(*seek)(void *opaque, int64_t offset, int whence)) {
auto io = MakeIOPointer(opaque, read, write, seek);
if (!io) {
return {};
}
io->seekable = (seek != nullptr);
auto result = avformat_alloc_context();
if (!result) {
LogError(u"avformat_alloc_context"_q);
return {};
}
result->pb = io.get();
result->flags |= AVFMT_FLAG_CUSTOM_IO;
auto options = (AVDictionary*)nullptr;
const auto guard = gsl::finally([&] { av_dict_free(&options); });
av_dict_set(&options, "usetoc", "1", 0);
const auto error = AvErrorWrap(avformat_open_input(
&result,
nullptr,
nullptr,
&options));
if (error) {
// avformat_open_input freed 'result' in case an error happened.
LogError(u"avformat_open_input"_q, error);
return {};
}
if (seek) {
result->flags |= AVFMT_FLAG_FAST_SEEK;
}
// Now FormatPointer will own and free the IO context.
io.release();
return FormatPointer(result);
}
FormatPointer MakeWriteFormatPointer(
void *opaque,
int(*read)(void *opaque, uint8_t *buffer, int bufferSize),
#if DA_FFMPEG_CONST_WRITE_CALLBACK
int(*write)(void *opaque, const uint8_t *buffer, int bufferSize),
#else
int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
#endif
int64_t(*seek)(void *opaque, int64_t offset, int whence),
const QByteArray &format) {
const AVOutputFormat *found = nullptr;
void *i = nullptr;
while ((found = av_muxer_iterate(&i))) {
if (found->name == format) {
break;
}
}
if (!found) {
LogError(
"av_muxer_iterate",
u"Format %1 not found"_q.arg(QString::fromUtf8(format)));
return {};
}
auto io = MakeIOPointer(opaque, read, write, seek);
if (!io) {
return {};
}
io->seekable = (seek != nullptr);
auto result = (AVFormatContext*)nullptr;
auto error = AvErrorWrap(avformat_alloc_output_context2(
&result,
(AVOutputFormat*)found,
nullptr,
nullptr));
if (!result || error) {
LogError("avformat_alloc_output_context2", error);
return {};
}
result->pb = io.get();
result->flags |= AVFMT_FLAG_CUSTOM_IO;
// Now FormatPointer will own and free the IO context.
io.release();
return FormatPointer(result);
}
void FormatDeleter::operator()(AVFormatContext *value) {
if (value) {
const auto deleter = IOPointer(value->pb);
avformat_close_input(&value);
}
}
const AVCodec *FindDecoder(not_null<AVCodecContext*> context) {
// Force libvpx-vp9, because we need alpha channel support.
return (context->codec_id == AV_CODEC_ID_VP9)
? avcodec_find_decoder_by_name("libvpx-vp9")
: avcodec_find_decoder(context->codec_id);
}
CodecPointer MakeCodecPointer(CodecDescriptor descriptor) {
auto error = AvErrorWrap();
auto result = CodecPointer(avcodec_alloc_context3(nullptr));
const auto context = result.get();
if (!context) {
LogError(u"avcodec_alloc_context3"_q);
return {};
}
const auto stream = descriptor.stream;
error = avcodec_parameters_to_context(context, stream->codecpar);
if (error) {
LogError(u"avcodec_parameters_to_context"_q, error);
return {};
}
context->pkt_timebase = stream->time_base;
av_opt_set(context, "threads", "auto", 0);
av_opt_set_int(context, "refcounted_frames", 1, 0);
const auto codec = FindDecoder(context);
if (!codec) {
LogError(u"avcodec_find_decoder"_q, context->codec_id);
return {};
}
if (descriptor.hwAllowed) {
context->get_format = GetHwFormat;
context->opaque = context;
} else {
DEBUG_LOG(("Video Info: Using software \"%2\" decoder."
).arg(codec->name));
}
if ((error = avcodec_open2(context, codec, nullptr))) {
LogError(u"avcodec_open2"_q, error);
return {};
}
return result;
}
void CodecDeleter::operator()(AVCodecContext *value) {
if (value) {
avcodec_free_context(&value);
}
}
FramePointer MakeFramePointer() {
return FramePointer(av_frame_alloc());
}
FramePointer DuplicateFramePointer(AVFrame *frame) {
return frame
? FramePointer(av_frame_clone(frame))
: FramePointer();
}
bool FrameHasData(AVFrame *frame) {
return (frame && frame->data[0] != nullptr);
}
void ClearFrameMemory(AVFrame *frame) {
if (FrameHasData(frame)) {
av_frame_unref(frame);
}
}
void FrameDeleter::operator()(AVFrame *value) {
av_frame_free(&value);
}
SwscalePointer MakeSwscalePointer(
QSize srcSize,
int srcFormat,
QSize dstSize,
int dstFormat,
SwscalePointer *existing) {
// We have to use custom caching for SwsContext, because
// sws_getCachedContext checks passed flags with existing context flags,
// and re-creates context if they're different, but in the process of
// context creation the passed flags are modified before being written
// to the resulting context, so the caching doesn't work.
if (existing && (*existing) != nullptr) {
const auto &deleter = existing->get_deleter();
if (deleter.srcSize == srcSize
&& deleter.srcFormat == srcFormat
&& deleter.dstSize == dstSize
&& deleter.dstFormat == dstFormat) {
return std::move(*existing);
}
}
if (srcFormat <= AV_PIX_FMT_NONE || srcFormat >= AV_PIX_FMT_NB) {
LogError(u"frame->format"_q);
return SwscalePointer();
}
const auto result = sws_getCachedContext(
existing ? existing->release() : nullptr,
srcSize.width(),
srcSize.height(),
AVPixelFormat(srcFormat),
dstSize.width(),
dstSize.height(),
AVPixelFormat(dstFormat),
0,
nullptr,
nullptr,
nullptr);
if (!result) {
LogError(u"sws_getCachedContext"_q);
}
return SwscalePointer(
result,
{ srcSize, srcFormat, dstSize, dstFormat });
}
SwscalePointer MakeSwscalePointer(
not_null<AVFrame*> frame,
QSize resize,
SwscalePointer *existing) {
return MakeSwscalePointer(
QSize(frame->width, frame->height),
frame->format,
resize,
AV_PIX_FMT_BGRA,
existing);
}
void SwresampleDeleter::operator()(SwrContext *value) {
if (value) {
swr_free(&value);
}
}
SwresamplePointer MakeSwresamplePointer(
AVChannelLayout *srcLayout,
AVSampleFormat srcFormat,
int srcRate,
AVChannelLayout *dstLayout,
AVSampleFormat dstFormat,
int dstRate,
SwresamplePointer *existing) {
// We have to use custom caching for SwsContext, because
// sws_getCachedContext checks passed flags with existing context flags,
// and re-creates context if they're different, but in the process of
// context creation the passed flags are modified before being written
// to the resulting context, so the caching doesn't work.
if (existing && (*existing) != nullptr) {
const auto &deleter = existing->get_deleter();
if (srcLayout->nb_channels == deleter.srcChannels
&& dstLayout->nb_channels == deleter.dstChannels
&& srcFormat == deleter.srcFormat
&& dstFormat == deleter.dstFormat
&& srcRate == deleter.srcRate
&& dstRate == deleter.dstRate) {
return std::move(*existing);
}
}
// Initialize audio resampler
auto result = (SwrContext*)nullptr;
auto error = AvErrorWrap(swr_alloc_set_opts2(
&result,
dstLayout,
dstFormat,
dstRate,
srcLayout,
srcFormat,
srcRate,
0,
nullptr));
if (error || !result) {
LogError(u"swr_alloc_set_opts2"_q, error);
return SwresamplePointer();
}
error = AvErrorWrap(swr_init(result));
if (error) {
LogError(u"swr_init"_q, error);
swr_free(&result);
return SwresamplePointer();
}
return SwresamplePointer(
result,
{
srcFormat,
srcRate,
srcLayout->nb_channels,
dstFormat,
dstRate,
dstLayout->nb_channels,
});
}
void SwscaleDeleter::operator()(SwsContext *value) {
if (value) {
sws_freeContext(value);
}
}
void LogError(const QString &method, const QString &details) {
LOG(("Streaming Error: Error in %1%2."
).arg(method
).arg(details.isEmpty() ? QString() : " - " + details));
}
void LogError(
const QString &method,
AvErrorWrap error,
const QString &details) {
LOG(("Streaming Error: Error in %1 (code: %2, text: %3)%4."
).arg(method
).arg(error.code()
).arg(error.text()
).arg(details.isEmpty() ? QString() : " - " + details));
}
crl::time PtsToTime(int64_t pts, AVRational timeBase) {
return (pts == AV_NOPTS_VALUE || !timeBase.den)
? kTimeUnknown
: ((pts * 1000LL * timeBase.num) / timeBase.den);
}
crl::time PtsToTimeCeil(int64_t pts, AVRational timeBase) {
return (pts == AV_NOPTS_VALUE || !timeBase.den)
? kTimeUnknown
: ((pts * 1000LL * timeBase.num + timeBase.den - 1) / timeBase.den);
}
int64_t TimeToPts(crl::time time, AVRational timeBase) {
return (time == kTimeUnknown || !timeBase.num)
? AV_NOPTS_VALUE
: (time * timeBase.den) / (1000LL * timeBase.num);
}
crl::time PacketPosition(const Packet &packet, AVRational timeBase) {
const auto &native = packet.fields();
return PtsToTime(
(native.pts == AV_NOPTS_VALUE) ? native.dts : native.pts,
timeBase);
}
crl::time PacketDuration(const Packet &packet, AVRational timeBase) {
return PtsToTime(packet.fields().duration, timeBase);
}
int DurationByPacket(const Packet &packet, AVRational timeBase) {
const auto position = PacketPosition(packet, timeBase);
const auto duration = std::max(
PacketDuration(packet, timeBase),
crl::time(1));
const auto bad = [](crl::time time) {
return (time < 0) || (time > kDurationMax);
};
if (bad(position) || bad(duration) || bad(position + duration + 1)) {
LOG(("Streaming Error: Wrong duration by packet: %1 + %2"
).arg(position
).arg(duration));
return -1;
}
return int(position + duration + 1);
}
int ReadRotationFromMetadata(not_null<AVStream*> stream) {
const auto displaymatrix = av_packet_side_data_get(
stream->codecpar->coded_side_data,
stream->codecpar->nb_coded_side_data,
AV_PKT_DATA_DISPLAYMATRIX);
auto theta = 0;
if (displaymatrix) {
const auto matrix = (int32_t*)displaymatrix->data;
theta = -round(av_display_rotation_get(matrix));
}
theta -= 360 * floor(theta / 360 + 0.9 / 360);
const auto result = int(base::SafeRound(theta));
return (result == 90 || result == 180 || result == 270) ? result : 0;
}
AVRational ValidateAspectRatio(AVRational aspect) {
return IsValidAspectRatio(aspect) ? aspect : kNormalAspect;
}
QSize CorrectByAspect(QSize size, AVRational aspect) {
Expects(IsValidAspectRatio(aspect));
return QSize(size.width() * av_q2d(aspect), size.height());
}
bool RotationSwapWidthHeight(int rotation) {
return (rotation == 90 || rotation == 270);
}
QSize TransposeSizeByRotation(QSize size, int rotation) {
return RotationSwapWidthHeight(rotation) ? size.transposed() : size;
}
bool GoodStorageForFrame(const QImage &storage, QSize size) {
return !storage.isNull()
&& (storage.format() == kImageFormat)
&& (storage.size() == size)
&& storage.isDetached()
&& IsAlignedImage(storage);
}
// 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[perLine * height + kAlignImageBy];
const auto cleanupData = static_cast<void *>(buffer);
const auto address = reinterpret_cast<uintptr_t>(buffer);
const auto alignedBuffer = buffer + ((address % kAlignImageBy)
? (kAlignImageBy - (address % kAlignImageBy))
: 0);
return QImage(
alignedBuffer,
width,
height,
perLine,
kImageFormat,
AlignedImageBufferCleanupHandler,
cleanupData);
}
void UnPremultiply(QImage &dst, const QImage &src) {
// This creates QImage::Format_ARGB32_Premultiplied, but we use it
// as an image in QImage::Format_ARGB32 format.
if (!GoodStorageForFrame(dst, src.size())) {
dst = CreateFrameStorage(src.size());
}
const auto srcPerLine = src.bytesPerLine();
const auto dstPerLine = dst.bytesPerLine();
const auto width = src.width();
const auto height = src.height();
auto srcBytes = src.bits();
auto dstBytes = dst.bits();
if (srcPerLine != width * 4 || dstPerLine != width * 4) {
for (auto i = 0; i != height; ++i) {
UnPremultiplyLine(dstBytes, srcBytes, width);
srcBytes += srcPerLine;
dstBytes += dstPerLine;
}
} else {
UnPremultiplyLine(dstBytes, srcBytes, width * height);
}
}
void PremultiplyInplace(QImage &image) {
const auto perLine = image.bytesPerLine();
const auto width = image.width();
const auto height = image.height();
auto bytes = image.bits();
if (perLine != width * 4) {
for (auto i = 0; i != height; ++i) {
PremultiplyLine(bytes, bytes, width);
bytes += perLine;
}
} else {
PremultiplyLine(bytes, bytes, width * height);
}
}
} // namespace FFmpeg

View File

@@ -0,0 +1,240 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/bytes.h"
#include "base/algorithm.h"
#include <crl/crl_time.h>
#include <QSize>
#include <QString>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libswresample/swresample.h>
#include <libavutil/opt.h>
#include <libavutil/version.h>
} // extern "C"
#define DA_FFMPEG_CONST_WRITE_CALLBACK (LIBAVFORMAT_VERSION_INT >= \
AV_VERSION_INT(61, 01, 100))
class QImage;
namespace FFmpeg {
inline constexpr auto kPixelBytesSize = 4;
inline constexpr auto kAVBlockSize = 4096; // 4Kb for ffmpeg blocksize
constexpr auto kUniversalTimeBase = AVRational{ 1, AV_TIME_BASE };
constexpr auto kNormalAspect = AVRational{ 1, 1 };
class AvErrorWrap {
public:
AvErrorWrap(int code = 0) : _code(code) {
}
[[nodiscard]] bool failed() const {
return (_code < 0);
}
[[nodiscard]] explicit operator bool() const {
return failed();
}
[[nodiscard]] int code() const {
return _code;
}
[[nodiscard]] QString text() const {
char string[AV_ERROR_MAX_STRING_SIZE] = { 0 };
return QString::fromUtf8(av_make_error_string(
string,
sizeof(string),
_code));
}
private:
int _code = 0;
};
class Packet {
public:
Packet() = default;
Packet(Packet &&other) : _data(base::take(other._data)) {
}
Packet &operator=(Packet &&other) {
if (this != &other) {
release();
_data = base::take(other._data);
}
return *this;
}
~Packet() {
release();
}
[[nodiscard]] AVPacket &fields() {
if (!_data) {
_data = av_packet_alloc();
}
return *_data;
}
[[nodiscard]] const AVPacket &fields() const {
if (!_data) {
_data = av_packet_alloc();
}
return *_data;
}
[[nodiscard]] bool empty() const {
return !_data || !fields().data;
}
void release() {
av_packet_free(&_data);
}
private:
mutable AVPacket *_data = nullptr;
};
struct IODeleter {
void operator()(AVIOContext *value);
};
using IOPointer = std::unique_ptr<AVIOContext, IODeleter>;
[[nodiscard]] IOPointer MakeIOPointer(
void *opaque,
int(*read)(void *opaque, uint8_t *buffer, int bufferSize),
#if DA_FFMPEG_CONST_WRITE_CALLBACK
int(*write)(void *opaque, const uint8_t *buffer, int bufferSize),
#else
int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
#endif
int64_t(*seek)(void *opaque, int64_t offset, int whence));
struct FormatDeleter {
void operator()(AVFormatContext *value);
};
using FormatPointer = std::unique_ptr<AVFormatContext, FormatDeleter>;
[[nodiscard]] FormatPointer MakeFormatPointer(
void *opaque,
int(*read)(void *opaque, uint8_t *buffer, int bufferSize),
#if DA_FFMPEG_CONST_WRITE_CALLBACK
int(*write)(void *opaque, const uint8_t *buffer, int bufferSize),
#else
int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
#endif
int64_t(*seek)(void *opaque, int64_t offset, int whence));
[[nodiscard]] FormatPointer MakeWriteFormatPointer(
void *opaque,
int(*read)(void *opaque, uint8_t *buffer, int bufferSize),
#if DA_FFMPEG_CONST_WRITE_CALLBACK
int(*write)(void *opaque, const uint8_t *buffer, int bufferSize),
#else
int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
#endif
int64_t(*seek)(void *opaque, int64_t offset, int whence),
const QByteArray &format);
struct CodecDeleter {
void operator()(AVCodecContext *value);
};
using CodecPointer = std::unique_ptr<AVCodecContext, CodecDeleter>;
struct CodecDescriptor {
not_null<AVStream*> stream;
bool hwAllowed = false;
};
[[nodiscard]] CodecPointer MakeCodecPointer(CodecDescriptor descriptor);
struct FrameDeleter {
void operator()(AVFrame *value);
};
using FramePointer = std::unique_ptr<AVFrame, FrameDeleter>;
[[nodiscard]] FramePointer MakeFramePointer();
[[nodiscard]] FramePointer DuplicateFramePointer(AVFrame *frame);
[[nodiscard]] bool FrameHasData(AVFrame *frame);
void ClearFrameMemory(AVFrame *frame);
struct SwscaleDeleter {
QSize srcSize;
int srcFormat = int(AV_PIX_FMT_NONE);
QSize dstSize;
int dstFormat = int(AV_PIX_FMT_NONE);
void operator()(SwsContext *value);
};
using SwscalePointer = std::unique_ptr<SwsContext, SwscaleDeleter>;
[[nodiscard]] SwscalePointer MakeSwscalePointer(
QSize srcSize,
int srcFormat,
QSize dstSize,
int dstFormat, // This field doesn't take part in caching!
SwscalePointer *existing = nullptr);
[[nodiscard]] SwscalePointer MakeSwscalePointer(
not_null<AVFrame*> frame,
QSize resize,
SwscalePointer *existing = nullptr);
struct SwresampleDeleter {
AVSampleFormat srcFormat = AV_SAMPLE_FMT_NONE;
int srcRate = 0;
int srcChannels = 0;
AVSampleFormat dstFormat = AV_SAMPLE_FMT_NONE;
int dstRate = 0;
int dstChannels = 0;
void operator()(SwrContext *value);
};
using SwresamplePointer = std::unique_ptr<SwrContext, SwresampleDeleter>;
[[nodiscard]] SwresamplePointer MakeSwresamplePointer(
AVChannelLayout *srcLayout,
AVSampleFormat srcFormat,
int srcRate,
AVChannelLayout *dstLayout,
AVSampleFormat dstFormat,
int dstRate,
SwresamplePointer *existing = nullptr);
void LogError(const QString &method, const QString &details = {});
void LogError(
const QString &method,
FFmpeg::AvErrorWrap error,
const QString &details = {});
[[nodiscard]] const AVCodec *FindDecoder(not_null<AVCodecContext*> context);
[[nodiscard]] crl::time PtsToTime(int64_t pts, AVRational timeBase);
// Used for full duration conversion.
[[nodiscard]] crl::time PtsToTimeCeil(int64_t pts, AVRational timeBase);
[[nodiscard]] int64_t TimeToPts(crl::time time, AVRational timeBase);
[[nodiscard]] crl::time PacketPosition(
const FFmpeg::Packet &packet,
AVRational timeBase);
[[nodiscard]] crl::time PacketDuration(
const FFmpeg::Packet &packet,
AVRational timeBase);
[[nodiscard]] int DurationByPacket(
const FFmpeg::Packet &packet,
AVRational timeBase);
[[nodiscard]] int ReadRotationFromMetadata(not_null<AVStream*> stream);
[[nodiscard]] AVRational ValidateAspectRatio(AVRational aspect);
[[nodiscard]] bool RotationSwapWidthHeight(int rotation);
[[nodiscard]] QSize TransposeSizeByRotation(QSize size, int rotation);
[[nodiscard]] QSize CorrectByAspect(QSize size, AVRational aspect);
[[nodiscard]] bool GoodStorageForFrame(const QImage &storage, QSize size);
[[nodiscard]] QImage CreateFrameStorage(QSize size);
void UnPremultiply(QImage &to, const QImage &from);
void PremultiplyInplace(QImage &image);
} // namespace FFmpeg