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
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:
99
Telegram/SourceFiles/ffmpeg/ffmpeg_bytes_io_wrap.h
Normal file
99
Telegram/SourceFiles/ffmpeg/ffmpeg_bytes_io_wrap.h
Normal 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
|
||||
414
Telegram/SourceFiles/ffmpeg/ffmpeg_frame_generator.cpp
Normal file
414
Telegram/SourceFiles/ffmpeg/ffmpeg_frame_generator.cpp
Normal 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
|
||||
41
Telegram/SourceFiles/ffmpeg/ffmpeg_frame_generator.h
Normal file
41
Telegram/SourceFiles/ffmpeg/ffmpeg_frame_generator.h
Normal 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
|
||||
748
Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp
Normal file
748
Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp
Normal 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
|
||||
240
Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h
Normal file
240
Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h
Normal 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
|
||||
Reference in New Issue
Block a user