Added transcoder for video downscaling.

This commit is contained in:
23rd
2026-07-16 17:50:10 +03:00
parent f97e42e8f6
commit 0807483acc
3 changed files with 566 additions and 0 deletions

View File

@@ -1513,6 +1513,8 @@ PRIVATE
media/streaming/media_streaming_video_track.cpp
media/streaming/media_streaming_video_track.h
media/streaming/media_streaming_native_frame_mac.h
media/media_video_encode.cpp
media/media_video_encode.h
media/view/media_view_group_thumbs.cpp
media/view/media_view_group_thumbs.h
media/view/media_view_open_common.cpp

View File

@@ -0,0 +1,543 @@
/*
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/media_video_encode.h"
#include "ffmpeg/ffmpeg_bytes_io_wrap.h"
#include "ffmpeg/ffmpeg_utility.h"
#include <QtCore/QTemporaryFile>
namespace Media::Encode {
namespace {
using namespace FFmpeg;
constexpr auto kVideoTimeBase = AVRational{ 1, 1'000'000 };
constexpr auto kMinBitrate = 600'000;
constexpr auto kMaxBitrate = 6'800'000;
constexpr auto kMaxSourceSize = 1000 * int64(1024) * 1024;
[[nodiscard]] int EvenDown(int value) {
return value & ~1;
}
[[nodiscard]] int TargetBitrate(QSize size, float64 fps) {
const auto pixels = int64(size.width()) * size.height();
const auto useFps = (fps > 1. && fps < 121.) ? fps : 30.;
const auto bits = float64(pixels) * useFps * 0.07;
return int(std::clamp(bits, float64(kMinBitrate), float64(kMaxBitrate)));
}
void CopyDisplayMatrix(not_null<AVStream*> from, not_null<AVStream*> to) {
const auto display = av_packet_side_data_get(
from->codecpar->coded_side_data,
from->codecpar->nb_coded_side_data,
AV_PKT_DATA_DISPLAYMATRIX);
if (!display || !display->size) {
return;
}
const auto copy = av_memdup(display->data, display->size);
if (!copy) {
return;
}
const auto added = av_packet_side_data_add(
&to->codecpar->coded_side_data,
&to->codecpar->nb_coded_side_data,
AV_PKT_DATA_DISPLAYMATRIX,
copy,
display->size,
0);
if (!added) {
av_free(copy);
}
}
[[nodiscard]] QByteArray MoveMoovToFront(const QByteArray &mp4) {
if (mp4.isEmpty()) {
return {};
}
auto inWrap = ReadBytesWrap{
.size = mp4.size(),
.data = reinterpret_cast<const uchar*>(mp4.constData()),
};
auto input = MakeFormatPointer(
&inWrap,
&ReadBytesWrap::Read,
nullptr,
&ReadBytesWrap::Seek);
if (!input
|| AvErrorWrap(avformat_find_stream_info(input.get(), nullptr))) {
return {};
}
auto temp = QTemporaryFile(
QDir::tempPath() + u"/tdtranscode-XXXXXX.mp4"_q);
if (!temp.open()) {
return {};
}
const auto path = temp.fileName();
temp.close();
const auto pathUtf8 = path.toUtf8();
auto output = (AVFormatContext*)nullptr;
if (AvErrorWrap(avformat_alloc_output_context2(
&output,
nullptr,
"mp4",
pathUtf8.constData()))
|| !output) {
return {};
}
const auto cleanup = gsl::finally([&] {
if (output->pb) {
avio_closep(&output->pb);
}
avformat_free_context(output);
});
for (auto i = 0; i != int(input->nb_streams); ++i) {
const auto in = input->streams[i];
const auto out = avformat_new_stream(output, nullptr);
if (!out
|| AvErrorWrap(avcodec_parameters_copy(
out->codecpar,
in->codecpar))) {
return {};
}
out->codecpar->codec_tag = 0;
out->time_base = in->time_base;
}
if (AvErrorWrap(avio_open(
&output->pb,
pathUtf8.constData(),
AVIO_FLAG_WRITE))) {
return {};
}
auto options = (AVDictionary*)nullptr;
av_dict_set(&options, "movflags", "faststart", 0);
const auto header = AvErrorWrap(avformat_write_header(output, &options));
av_dict_free(&options);
if (header) {
return {};
}
auto packet = av_packet_alloc();
const auto guard = gsl::finally([&] {
av_packet_free(&packet);
});
while (av_read_frame(input.get(), packet) >= 0) {
const auto unref = gsl::finally([&] {
av_packet_unref(packet);
});
const auto index = packet->stream_index;
av_packet_rescale_ts(
packet,
input->streams[index]->time_base,
output->streams[index]->time_base);
packet->pos = -1;
if (AvErrorWrap(av_interleaved_write_frame(output, packet))) {
return {};
}
}
if (AvErrorWrap(av_write_trailer(output))) {
return {};
}
avio_closep(&output->pb);
auto file = QFile(path);
return file.open(QIODevice::ReadOnly) ? file.readAll() : QByteArray();
}
} // namespace
QSize DownscaledSize(QSize original, int targetShorterSide) {
const auto width = original.width();
const auto height = original.height();
if (width <= 0 || height <= 0 || targetShorterSide <= 0) {
return QSize();
}
const auto shorter = std::min(width, height);
if (shorter <= targetShorterSide) {
return QSize();
}
const auto scale = targetShorterSide / float64(shorter);
return QSize(
std::max(EvenDown(int(base::SafeRound(width * scale))), 2),
std::max(EvenDown(int(base::SafeRound(height * scale))), 2));
}
int CompressedShorterSide(QSize original, int64 size) {
const auto shorter = std::min(original.width(), original.height());
if (shorter <= 0 || size <= 0 || size >= kMaxSourceSize) {
return 0;
} else if (shorter >= 1080) {
return 1080;
} else if (shorter >= 720) {
return 720;
} else if (shorter >= 480) {
return 480;
}
return shorter;
}
QByteArray TranscodeVideoToMp4(
const QByteArray &source,
int targetShorterSide,
Fn<bool(float64)> progress) {
if (source.isEmpty()
|| source.size() >= kMaxSourceSize
|| targetShorterSide <= 0) {
return {};
}
auto inWrap = ReadBytesWrap{
.size = source.size(),
.data = reinterpret_cast<const uchar*>(source.constData()),
};
auto input = MakeFormatPointer(
&inWrap,
&ReadBytesWrap::Read,
nullptr,
&ReadBytesWrap::Seek);
if (!input) {
return {};
}
auto error = AvErrorWrap(avformat_find_stream_info(input.get(), nullptr));
if (error) {
LogError(u"avformat_find_stream_info"_q, error);
return {};
}
const auto videoId = av_find_best_stream(
input.get(),
AVMEDIA_TYPE_VIDEO,
-1,
-1,
nullptr,
0);
if (videoId < 0) {
return {};
}
const auto audioId = av_find_best_stream(
input.get(),
AVMEDIA_TYPE_AUDIO,
-1,
videoId,
nullptr,
0);
const auto inVideoStream = input->streams[videoId];
const auto original = QSize(
inVideoStream->codecpar->width,
inVideoStream->codecpar->height);
auto target = DownscaledSize(original, targetShorterSide);
if (target.isEmpty()) {
target = QSize(
std::max(EvenDown(original.width()), 2),
std::max(EvenDown(original.height()), 2));
}
const auto guessed = av_guess_frame_rate(
input.get(),
inVideoStream,
nullptr);
const auto fps = (guessed.num > 0 && guessed.den > 0)
? av_q2d(guessed)
: 0.;
const auto totalDuration = (input->duration > 0)
? PtsToTime(input->duration, kUniversalTimeBase)
: crl::time(0);
auto decoder = MakeCodecPointer({ .stream = inVideoStream });
if (!decoder) {
return {};
}
auto result = WriteBytesWrap();
auto output = MakeWriteFormatPointer(
static_cast<void*>(&result),
nullptr,
&WriteBytesWrap::Write,
&WriteBytesWrap::Seek,
"mp4"_q);
if (!output) {
return {};
}
auto encoderCodec = avcodec_find_encoder_by_name("libopenh264");
if (!encoderCodec) {
encoderCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!encoderCodec) {
LogError(u"avcodec_find_encoder"_q, u"H264"_q);
return {};
}
}
const auto outVideoStream = avformat_new_stream(
output.get(),
encoderCodec);
if (!outVideoStream) {
LogError(u"avformat_new_stream"_q, u"video"_q);
return {};
}
auto encoder = CodecPointer(avcodec_alloc_context3(encoderCodec));
if (!encoder) {
LogError(u"avcodec_alloc_context3"_q, u"video"_q);
return {};
}
encoder->codec_id = encoderCodec->id;
encoder->codec_type = AVMEDIA_TYPE_VIDEO;
encoder->width = target.width();
encoder->height = target.height();
encoder->time_base = kVideoTimeBase;
encoder->framerate = AVRational{ 0, 1 };
encoder->pix_fmt = AV_PIX_FMT_YUV420P;
const auto sourceBitrate = int64(inVideoStream->codecpar->bit_rate);
const auto targetBitrate = int64(TargetBitrate(target, fps));
encoder->bit_rate = (sourceBitrate > 0)
? std::min(targetBitrate, sourceBitrate)
: targetBitrate;
encoder->gop_size = int(base::SafeRound(
(fps > 1. && fps < 121.) ? fps : 30.));
if (output->oformat->flags & AVFMT_GLOBALHEADER) {
encoder->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
error = AvErrorWrap(avcodec_open2(encoder.get(), encoderCodec, nullptr));
if (error) {
LogError(u"avcodec_open2"_q, error, u"video"_q);
return {};
}
error = AvErrorWrap(avcodec_parameters_from_context(
outVideoStream->codecpar,
encoder.get()));
if (error) {
LogError(u"avcodec_parameters_from_context"_q, error);
return {};
}
outVideoStream->time_base = encoder->time_base;
CopyDisplayMatrix(inVideoStream, outVideoStream);
const auto inAudioStream = (audioId >= 0)
? input->streams[audioId]
: nullptr;
auto outAudioStream = (AVStream*)nullptr;
if (inAudioStream) {
outAudioStream = avformat_new_stream(output.get(), nullptr);
if (!outAudioStream) {
LogError(u"avformat_new_stream"_q, u"audio"_q);
return {};
}
error = AvErrorWrap(avcodec_parameters_copy(
outAudioStream->codecpar,
inAudioStream->codecpar));
if (error) {
LogError(u"avcodec_parameters_copy"_q, error);
return {};
}
outAudioStream->codecpar->codec_tag = 0;
outAudioStream->time_base = inAudioStream->time_base;
}
error = AvErrorWrap(avformat_write_header(output.get(), nullptr));
if (error) {
LogError(u"avformat_write_header"_q, error);
return {};
}
auto swscale = SwscalePointer();
auto encodeFrame = MakeFramePointer();
auto decodedFrame = MakeFramePointer();
if (!encodeFrame || !decodedFrame) {
return {};
}
encodeFrame->format = AV_PIX_FMT_YUV420P;
encodeFrame->width = target.width();
encodeFrame->height = target.height();
error = AvErrorWrap(av_frame_get_buffer(encodeFrame.get(), 0));
if (error) {
LogError(u"av_frame_get_buffer"_q, error);
return {};
}
auto lastVideoPts = int64(-1);
auto failed = false;
auto packet = av_packet_alloc();
const auto packetGuard = gsl::finally([&] {
av_packet_free(&packet);
});
const auto writeEncoded = [&](AVFrame *frame) {
auto sent = AvErrorWrap(avcodec_send_frame(encoder.get(), frame));
if (sent) {
LogError(u"avcodec_send_frame"_q, sent);
return false;
}
auto encoded = av_packet_alloc();
const auto encodedGuard = gsl::finally([&] {
av_packet_free(&encoded);
});
while (true) {
auto received = AvErrorWrap(avcodec_receive_packet(
encoder.get(),
encoded));
if (received.code() == AVERROR(EAGAIN)
|| received.code() == AVERROR_EOF) {
return true;
} else if (received) {
LogError(u"avcodec_receive_packet"_q, received);
return false;
}
encoded->stream_index = outVideoStream->index;
av_packet_rescale_ts(
encoded,
encoder->time_base,
outVideoStream->time_base);
auto written = AvErrorWrap(av_interleaved_write_frame(
output.get(),
encoded));
if (written) {
LogError(u"av_interleaved_write_frame"_q, written);
return false;
}
}
};
const auto drainDecoder = [&] {
while (true) {
auto got = AvErrorWrap(avcodec_receive_frame(
decoder.get(),
decodedFrame.get()));
if (got.code() == AVERROR(EAGAIN) || got.code() == AVERROR_EOF) {
return true;
} else if (got) {
LogError(u"avcodec_receive_frame"_q, got);
failed = true;
return false;
}
swscale = MakeSwscalePointer(
QSize(decodedFrame->width, decodedFrame->height),
decodedFrame->format,
target,
AV_PIX_FMT_YUV420P,
&swscale);
if (!swscale) {
failed = true;
return false;
}
auto writable = AvErrorWrap(av_frame_make_writable(
encodeFrame.get()));
if (writable) {
LogError(u"av_frame_make_writable"_q, writable);
failed = true;
return false;
}
sws_scale(
swscale.get(),
decodedFrame->data,
decodedFrame->linesize,
0,
decodedFrame->height,
encodeFrame->data,
encodeFrame->linesize);
const auto source = (decodedFrame->best_effort_timestamp
!= AV_NOPTS_VALUE)
? decodedFrame->best_effort_timestamp
: decodedFrame->pts;
auto pts = (source != AV_NOPTS_VALUE)
? av_rescale_q(
source,
inVideoStream->time_base,
encoder->time_base)
: (lastVideoPts + 1);
if (pts <= lastVideoPts) {
pts = lastVideoPts + 1;
}
lastVideoPts = pts;
encodeFrame->pts = pts;
if (!writeEncoded(encodeFrame.get())) {
failed = true;
return false;
}
if (progress && totalDuration > 0) {
const auto done = PtsToTime(pts, encoder->time_base);
const auto value = std::clamp(
done / float64(totalDuration),
0.,
1.);
if (!progress(value)) {
failed = true;
return false;
}
}
}
};
while (true) {
auto read = AvErrorWrap(av_read_frame(input.get(), packet));
if (read.code() == AVERROR_EOF) {
break;
} else if (read) {
LogError(u"av_read_frame"_q, read);
return {};
}
const auto unref = gsl::finally([&] {
av_packet_unref(packet);
});
if (packet->stream_index == videoId) {
auto sent = AvErrorWrap(avcodec_send_packet(
decoder.get(),
packet));
if (sent) {
LogError(u"avcodec_send_packet"_q, sent);
return {};
}
if (!drainDecoder()) {
return {};
}
} else if (outAudioStream && packet->stream_index == audioId) {
av_packet_rescale_ts(
packet,
inAudioStream->time_base,
outAudioStream->time_base);
packet->stream_index = outAudioStream->index;
packet->pos = -1;
auto written = AvErrorWrap(av_interleaved_write_frame(
output.get(),
packet));
if (written) {
LogError(u"av_interleaved_write_frame"_q, written);
return {};
}
}
}
if (avcodec_send_packet(decoder.get(), nullptr) >= 0) {
drainDecoder();
}
if (failed) {
return {};
}
if (!writeEncoded(nullptr)) {
return {};
}
error = AvErrorWrap(av_write_trailer(output.get()));
if (error) {
LogError(u"av_write_trailer"_q, error);
return {};
}
auto produced = std::move(result.content);
auto faststart = MoveMoovToFront(produced);
return faststart.isEmpty() ? produced : faststart;
}
} // namespace Media::Encode

View File

@@ -0,0 +1,21 @@
/*
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
namespace Media::Encode {
[[nodiscard]] int CompressedShorterSide(QSize original, int64 size);
[[nodiscard]] QSize DownscaledSize(QSize original, int targetShorterSide);
[[nodiscard]] QByteArray TranscodeVideoToMp4(
const QByteArray &source,
int targetShorterSide,
Fn<bool(float64)> progress = nullptr);
} // namespace Media::Encode