mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/telegramdesktop/tdesktop
synced 2026-09-21 00:23:56 +08:00
Added transcoder for video downscaling.
This commit is contained in:
@@ -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
|
||||
|
||||
543
Telegram/SourceFiles/media/media_video_encode.cpp
Normal file
543
Telegram/SourceFiles/media/media_video_encode.cpp
Normal 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
|
||||
21
Telegram/SourceFiles/media/media_video_encode.h
Normal file
21
Telegram/SourceFiles/media/media_video_encode.h
Normal 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
|
||||
Reference in New Issue
Block a user