| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448 |
- /*
- 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 "media/streaming/media_streaming_utility.h"
- #include "media/streaming/media_streaming_common.h"
- #include "ui/image/image_prepare.h"
- #include "ui/painter.h"
- #include "ffmpeg/ffmpeg_utility.h"
- namespace Media {
- namespace Streaming {
- namespace {
- constexpr auto kSkipInvalidDataPackets = 10;
- } // namespace
- crl::time FramePosition(const Stream &stream) {
- const auto pts = !stream.decodedFrame
- ? AV_NOPTS_VALUE
- : (stream.decodedFrame->best_effort_timestamp != AV_NOPTS_VALUE)
- ? stream.decodedFrame->best_effort_timestamp
- : (stream.decodedFrame->pts != AV_NOPTS_VALUE)
- ? stream.decodedFrame->pts
- : stream.decodedFrame->pkt_dts;
- const auto result = FFmpeg::PtsToTime(pts, stream.timeBase);
- // Sometimes the result here may be larger than the stream duration.
- return (stream.duration == kDurationUnavailable)
- ? result
- : std::min(result, stream.duration);
- }
- FFmpeg::AvErrorWrap ProcessPacket(Stream &stream, FFmpeg::Packet &&packet) {
- Expects(stream.codec != nullptr);
- auto error = FFmpeg::AvErrorWrap();
- const auto native = &packet.fields();
- const auto guard = gsl::finally([
- &,
- size = native->size,
- data = native->data
- ] {
- native->size = size;
- native->data = data;
- packet = FFmpeg::Packet();
- });
- error = avcodec_send_packet(
- stream.codec.get(),
- native->data ? native : nullptr); // Drain on eof.
- if (error) {
- LogError(u"avcodec_send_packet"_q, error);
- if (error.code() == AVERROR_INVALIDDATA
- // There is a sample voice message where skipping such packet
- // results in a crash (read_access to nullptr) in swr_convert().
- && stream.codec->codec_id != AV_CODEC_ID_OPUS) {
- if (++stream.invalidDataPackets < kSkipInvalidDataPackets) {
- return FFmpeg::AvErrorWrap(); // Try to skip a bad packet.
- }
- }
- } else {
- stream.invalidDataPackets = 0;
- }
- return error;
- }
- FFmpeg::AvErrorWrap ReadNextFrame(Stream &stream) {
- Expects(stream.decodedFrame != nullptr);
- auto error = FFmpeg::AvErrorWrap();
- do {
- error = avcodec_receive_frame(
- stream.codec.get(),
- stream.decodedFrame.get());
- if (!error
- || error.code() != AVERROR(EAGAIN)
- || stream.queue.empty()) {
- return error;
- }
- error = ProcessPacket(stream, std::move(stream.queue.front()));
- stream.queue.pop_front();
- } while (!error);
- return error;
- }
- bool GoodForRequest(
- const QImage &image,
- bool hasAlpha,
- int rotation,
- const FrameRequest &request) {
- if (image.isNull()
- || (hasAlpha && !request.keepAlpha)
- || request.colored.alpha() != 0) {
- return false;
- } else if (!request.blurredBackground && request.resize.isEmpty()) {
- return true;
- } else if (rotation != 0) {
- return false;
- } else if (!request.rounding.empty() || !request.mask.isNull()) {
- return false;
- }
- const auto size = request.blurredBackground
- ? request.outer
- : request.resize;
- return (size == request.outer) && (size == image.size());
- }
- bool TransferFrame(
- Stream &stream,
- not_null<AVFrame*> decodedFrame,
- not_null<AVFrame*> transferredFrame) {
- Expects(decodedFrame->hw_frames_ctx != nullptr);
- const auto error = FFmpeg::AvErrorWrap(
- av_hwframe_transfer_data(transferredFrame, decodedFrame, 0));
- if (error) {
- LogError(u"av_hwframe_transfer_data"_q, error);
- return false;
- }
- FFmpeg::ClearFrameMemory(decodedFrame);
- return true;
- }
- QImage ConvertFrame(
- Stream &stream,
- not_null<AVFrame*> frame,
- QSize resize,
- QImage storage) {
- const auto frameSize = QSize(frame->width, frame->height);
- if (frameSize.isEmpty()) {
- LOG(("Streaming Error: Bad frame size %1,%2"
- ).arg(frameSize.width()
- ).arg(frameSize.height()));
- return QImage();
- } else if (!FFmpeg::FrameHasData(frame)) {
- LOG(("Streaming Error: Bad frame data."));
- return QImage();
- }
- if (resize.isEmpty()) {
- resize = frameSize;
- } else if (FFmpeg::RotationSwapWidthHeight(stream.rotation)) {
- resize.transpose();
- }
- if (!FFmpeg::GoodStorageForFrame(storage, resize)) {
- storage = FFmpeg::CreateFrameStorage(resize);
- }
- const auto format = AV_PIX_FMT_BGRA;
- const auto hasDesiredFormat = (frame->format == format);
- if (frameSize == storage.size() && hasDesiredFormat) {
- static_assert(sizeof(uint32) == FFmpeg::kPixelBytesSize);
- auto to = reinterpret_cast<uint32*>(storage.bits());
- auto from = reinterpret_cast<const uint32*>(frame->data[0]);
- const auto deltaTo = (storage.bytesPerLine() / sizeof(uint32))
- - storage.width();
- const auto deltaFrom = (frame->linesize[0] / sizeof(uint32))
- - frame->width;
- for ([[maybe_unused]] const auto y : ranges::views::ints(0, frame->height)) {
- for ([[maybe_unused]] const auto x : ranges::views::ints(0, frame->width)) {
- // Wipe out possible alpha values.
- *to++ = 0xFF000000U | *from++;
- }
- to += deltaTo;
- from += deltaFrom;
- }
- } else {
- stream.swscale = MakeSwscalePointer(
- frame,
- resize,
- &stream.swscale);
- if (!stream.swscale) {
- return QImage();
- }
- // AV_NUM_DATA_POINTERS defined in AVFrame struct
- uint8_t *data[AV_NUM_DATA_POINTERS] = { storage.bits(), nullptr };
- int linesize[AV_NUM_DATA_POINTERS] = { int(storage.bytesPerLine()), 0 };
- sws_scale(
- stream.swscale.get(),
- frame->data,
- frame->linesize,
- 0,
- frame->height,
- data,
- linesize);
- if (frame->format == AV_PIX_FMT_YUVA420P) {
- FFmpeg::PremultiplyInplace(storage);
- }
- }
- FFmpeg::ClearFrameMemory(frame);
- return storage;
- }
- FrameYUV ExtractYUV(Stream &stream, AVFrame *frame) {
- return {
- .size = { frame->width, frame->height },
- .chromaSize = {
- AV_CEIL_RSHIFT(frame->width, 1), // SWScale does that.
- AV_CEIL_RSHIFT(frame->height, 1)
- },
- .y = { .data = frame->data[0], .stride = frame->linesize[0] },
- .u = { .data = frame->data[1], .stride = frame->linesize[1] },
- .v = { .data = frame->data[2], .stride = frame->linesize[2] },
- };
- }
- void PaintFrameOuter(QPainter &p, const QRect &inner, QSize outer) {
- const auto left = inner.x();
- const auto right = outer.width() - inner.width() - left;
- const auto top = inner.y();
- const auto bottom = outer.height() - inner.height() - top;
- if (left > 0) {
- p.fillRect(0, 0, left, outer.height(), st::imageBg);
- }
- if (right > 0) {
- p.fillRect(
- outer.width() - right,
- 0,
- right,
- outer.height(),
- st::imageBg);
- }
- if (top > 0) {
- p.fillRect(left, 0, inner.width(), top, st::imageBg);
- }
- if (bottom > 0) {
- p.fillRect(
- left,
- outer.height() - bottom,
- inner.width(),
- bottom,
- st::imageBg);
- }
- }
- void PaintFrameInner(
- QPainter &p,
- QRect to,
- const QImage &original,
- bool alpha,
- int rotation) {
- const auto rotated = [](QRect rect, int rotation) {
- switch (rotation) {
- case 0: return rect;
- case 90: return QRect(
- rect.y(),
- -rect.x() - rect.width(),
- rect.height(),
- rect.width());
- case 180: return QRect(
- -rect.x() - rect.width(),
- -rect.y() - rect.height(),
- rect.width(),
- rect.height());
- case 270: return QRect(
- -rect.y() - rect.height(),
- rect.x(),
- rect.height(),
- rect.width());
- }
- Unexpected("Rotation in PaintFrameInner.");
- };
- PainterHighQualityEnabler hq(p);
- if (rotation) {
- p.rotate(rotation);
- }
- const auto rect = rotated(to, rotation);
- if (alpha) {
- p.fillRect(rect, Qt::white);
- }
- p.drawImage(rect, original);
- }
- QImage PrepareBlurredBackground(QSize outer, QImage frame) {
- const auto bsize = frame.size();
- const auto copyw = std::min(
- bsize.width(),
- std::max(outer.width() * bsize.height() / outer.height(), 1));
- const auto copyh = std::min(
- bsize.height(),
- std::max(outer.height() * bsize.width() / outer.width(), 1));
- auto copy = (bsize == QSize(copyw, copyh))
- ? std::move(frame)
- : frame.copy(
- (bsize.width() - copyw) / 2,
- (bsize.height() - copyh) / 2,
- copyw,
- copyh);
- auto scaled = (copy.width() <= 100 && copy.height() <= 100)
- ? std::move(copy)
- : copy.scaled(40, 40, Qt::KeepAspectRatio, Qt::FastTransformation);
- return Images::Blur(std::move(scaled), true);
- }
- void FillBlurredBackground(QPainter &p, QSize outer, QImage bg) {
- auto hq = PainterHighQualityEnabler(p);
- const auto rect = QRect(QPoint(), outer);
- const auto ratio = p.device()->devicePixelRatio();
- p.drawImage(
- rect,
- PrepareBlurredBackground(outer * ratio, std::move(bg)));
- p.fillRect(rect, QColor(0, 0, 0, 48));
- }
- void PaintFrameContent(
- QPainter &p,
- const QImage &original,
- bool hasAlpha,
- const AVRational &aspect,
- int rotation,
- const FrameRequest &request) {
- const auto outer = request.outer;
- const auto full = request.outer.isEmpty() ? original.size() : outer;
- const auto deAlpha = hasAlpha && !request.keepAlpha;
- const auto resize = request.blurredBackground
- ? DecideVideoFrameResize(
- outer,
- FFmpeg::TransposeSizeByRotation(
- FFmpeg::CorrectByAspect(original.size(), aspect), rotation))
- : ExpandDecision{ request.resize.isEmpty()
- ? original.size()
- : request.resize };
- const auto size = resize.result;
- const auto target = QRect(
- (full.width() - size.width()) / 2,
- (full.height() - size.height()) / 2,
- size.width(),
- size.height());
- if (request.blurredBackground) {
- if (!resize.expanding) {
- FillBlurredBackground(p, full, original);
- }
- } else if (!hasAlpha || !request.keepAlpha) {
- PaintFrameOuter(p, target, full);
- }
- PaintFrameInner(p, target, original, deAlpha, rotation);
- }
- void ApplyFrameRounding(QImage &storage, const FrameRequest &request) {
- if (!request.mask.isNull()) {
- auto p = QPainter(&storage);
- p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
- p.drawImage(
- QRect(QPoint(), storage.size() / storage.devicePixelRatio()),
- request.mask);
- } else if (!request.rounding.empty()) {
- storage = Images::Round(std::move(storage), request.rounding);
- }
- }
- ExpandDecision DecideFrameResize(
- QSize outer,
- QSize original,
- int minVisibleNominator,
- int minVisibleDenominator) {
- if (outer.isEmpty()) {
- // Often "expanding" means that we don't need to fill the background.
- return { .result = original, .expanding = true };
- }
- const auto big = original.scaled(outer, Qt::KeepAspectRatioByExpanding);
- if ((big.width() <= outer.width())
- && (big.height() * minVisibleNominator
- <= outer.height() * minVisibleDenominator)) {
- return { .result = big, .expanding = true };
- }
- return { .result = original.scaled(outer, Qt::KeepAspectRatio) };
- }
- bool FrameResizeMayExpand(
- QSize outer,
- QSize original,
- int minVisibleNominator,
- int minVisibleDenominator) {
- const auto min = std::min({
- outer.width(),
- outer.height(),
- original.width(),
- original.height(),
- });
- // Count for: (nominator / denominator) - (1 / min).
- // In case the result is less than 1 / 2, just return.
- if (2 * minVisibleNominator * min
- < 2 * minVisibleDenominator + minVisibleDenominator * min) {
- return false;
- }
- return DecideFrameResize(
- outer,
- original,
- minVisibleNominator * min - minVisibleDenominator,
- minVisibleDenominator * min).expanding;
- }
- ExpandDecision DecideVideoFrameResize(QSize outer, QSize original) {
- return DecideFrameResize(outer, original, 1, 2);
- }
- QSize CalculateResizeFromOuter(QSize outer, QSize original) {
- return DecideVideoFrameResize(outer, original).result;
- }
- QImage PrepareByRequest(
- const QImage &original,
- bool hasAlpha,
- const AVRational &aspect,
- int rotation,
- const FrameRequest &request,
- QImage storage) {
- Expects(!request.outer.isEmpty() || hasAlpha);
- const auto outer = request.outer.isEmpty()
- ? original.size()
- : request.outer;
- if (!FFmpeg::GoodStorageForFrame(storage, outer)) {
- storage = FFmpeg::CreateFrameStorage(outer);
- }
- if (hasAlpha && request.keepAlpha) {
- storage.fill(Qt::transparent);
- }
- QPainter p(&storage);
- PaintFrameContent(p, original, hasAlpha, aspect, rotation, request);
- p.end();
- ApplyFrameRounding(storage, request);
- if (request.colored.alpha() != 0) {
- storage = Images::Colored(std::move(storage), request.colored);
- }
- return storage;
- }
- } // namespace Streaming
- } // namespace Media
|