Files
tdesktop/Telegram/lib_webrtc/webrtc/webrtc_video_track.cpp
allhaileris afb81b8278
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
init
2026-02-16 15:50:16 +03:00

580 lines
14 KiB
C++

// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "webrtc/webrtc_video_track.h"
#include "ffmpeg/ffmpeg_utility.h"
#include <QtGui/QImage>
#include <QtGui/QPainter>
#include <api/video/video_sink_interface.h>
#include <api/video/video_frame.h>
namespace Webrtc {
namespace {
constexpr auto kDropFramesWhileInactive = 5 * crl::time(1000);
[[nodiscard]] bool GoodForRequest(
const QImage &image,
int rotation,
const FrameRequest &request) {
if (request.resize.isEmpty()) {
return true;
} else if (rotation != 0) {
return false;
//} else if ((request.radius != ImageRoundRadius::None)
// && ((request.corners & RectPart::AllCorners) != 0)) {
// return false;
}
return (request.resize == request.outer)
&& (request.resize == image.size());
}
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(), Qt::black);
}
if (right > 0) {
p.fillRect(
outer.width() - right,
0,
right,
outer.height(),
Qt::black);
}
if (top > 0) {
p.fillRect(left, 0, inner.width(), top, Qt::black);
}
if (bottom > 0) {
p.fillRect(
left,
outer.height() - bottom,
inner.width(),
bottom,
Qt::black);
}
}
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.");
};
const auto hints = {
QPainter::Antialiasing,
QPainter::SmoothPixmapTransform,
QPainter::TextAntialiasing
};
for (const auto hint : hints) {
p.setRenderHint(hint);
}
if (rotation) {
p.rotate(rotation);
}
const auto rect = rotated(to, rotation);
if (alpha) {
p.fillRect(rect, Qt::white);
}
p.drawImage(rect, original);
}
void PaintFrameContent(
QPainter &p,
const QImage &original,
bool alpha,
int rotation,
const FrameRequest &request) {
const auto full = request.outer.isEmpty()
? original.size()
: request.outer;
const auto size = request.resize.isEmpty()
? original.size()
: request.resize;
const auto to = QRect(
(full.width() - size.width()) / 2,
(full.height() - size.height()) / 2,
size.width(),
size.height());
PaintFrameOuter(p, to, full);
PaintFrameInner(p, to, original, alpha, rotation);
}
void ApplyFrameRounding(QImage &storage, const FrameRequest &request) {
//if (!(request.corners & RectPart::AllCorners)
// || (request.radius == ImageRoundRadius::None)) {
// return;
//}
//Images::prepareRound(storage, request.radius, request.corners);
}
QImage PrepareByRequest(
const QImage &original,
bool alpha,
int rotation,
const FrameRequest &request,
QImage storage) {
Expects(!request.outer.isEmpty() || alpha);
const auto outer = request.outer.isEmpty()
? original.size()
: request.outer;
if (!FFmpeg::GoodStorageForFrame(storage, outer)) {
storage = FFmpeg::CreateFrameStorage(outer);
}
QPainter p(&storage);
PaintFrameContent(p, original, alpha, rotation, request);
p.end();
ApplyFrameRounding(storage, request);
return storage;
}
} // namespace
struct VideoTrack::Frame {
int64 mcstimestamp = 0;
QImage original;
QImage prepared;
rtc::scoped_refptr<webrtc::I420BufferInterface> native;
FrameYUV420 yuv420;
FrameRequest request = FrameRequest::NonStrict();
FrameFormat format = FrameFormat::None;
int rotation = 0;
bool displayed = false;
bool alpha = false;
bool requireARGB32 = true;
};
class VideoTrack::Sink final
: public rtc::VideoSinkInterface<webrtc::VideoFrame>
, public std::enable_shared_from_this<Sink> {
public:
explicit Sink(bool requireARGB32);
using PrepareFrame = not_null<Frame*>;
using PrepareState = bool;
struct FrameWithIndex {
not_null<Frame*> frame;
int index = -1;
};
[[nodiscard]] bool firstPresentHappened() const;
// Called from the main thread.
void markFrameShown();
[[nodiscard]] not_null<Frame*> frameForPaint();
[[nodiscard]] FrameWithIndex frameForPaintWithIndex();
[[nodiscard]] rpl::producer<> renderNextFrameOnMain() const;
void destroyFrameForPaint();
void OnFrame(const webrtc::VideoFrame &nativeVideoFrame) override;
private:
struct FrameForDecode {
not_null<Frame*> frame;
int counter = 0;
};
[[nodiscard]] FrameForDecode nextFrameForDecode();
void presentNextFrame(const FrameForDecode &frame);
[[nodiscard]] not_null<Frame*> getFrame(int index);
[[nodiscard]] not_null<const Frame*> getFrame(int index) const;
[[nodiscard]] int counter() const;
bool decodeFrame(
const webrtc::VideoFrame &nativeVideoFrame,
not_null<Frame*> frame);
void notifyFrameDecoded();
FFmpeg::SwscalePointer _decodeContext;
std::atomic<int> _counter = 0;
// Main thread.
int _counterCycle = 0;
static constexpr auto kFramesCount = 3;
std::array<Frame, kFramesCount> _frames;
rpl::event_stream<> _renderNextFrameOnMain;
};
VideoTrack::Sink::Sink(bool requireARGB32) {
for (auto &frame : _frames) {
frame.requireARGB32 = requireARGB32;
}
}
void VideoTrack::Sink::OnFrame(const webrtc::VideoFrame &nativeVideoFrame) {
const auto decode = nextFrameForDecode();
if (decodeFrame(nativeVideoFrame, decode.frame)) {
PrepareFrameByRequests(decode.frame, nativeVideoFrame.rotation());
presentNextFrame(decode);
}
}
auto VideoTrack::Sink::nextFrameForDecode() -> FrameForDecode {
const auto current = counter();
const auto index = ((current + 3) / 2) % kFramesCount;
const auto frame = getFrame(index);
return { frame, current };
}
void VideoTrack::Sink::presentNextFrame(const FrameForDecode &frame) {
// Release this frame to the main thread for rendering.
const auto present = [&](int counter) {
Expects(counter + 1 < 2 * kFramesCount);
_counter.store(counter + 1, std::memory_order_release);
notifyFrameDecoded();
};
switch (frame.counter) {
case 0: present(0);
case 1: return;
case 2: present(2);
case 3: return;
case 4: present(4);
case 5: return;
//case 6: present(6);
//case 7: return;
}
Unexpected("Counter value in VideoTrack::Sink::presentNextFrame.");
}
bool VideoTrack::Sink::decodeFrame(
const webrtc::VideoFrame &nativeVideoFrame,
not_null<Frame*> frame) {
const auto native = nativeVideoFrame.video_frame_buffer()->ToI420();
const auto size = QSize{ native->width(), native->height() };
if (size.isEmpty()) {
frame->format = FrameFormat::None;
return false;
}
frame->mcstimestamp = nativeVideoFrame.timestamp_us();
if (!frame->mcstimestamp) {
frame->mcstimestamp = crl::now() * 1000;
}
if (!frame->requireARGB32) {
if (!frame->original.isNull()) {
frame->original = frame->prepared = QImage();
}
frame->format = FrameFormat::YUV420;
frame->native = native;
frame->yuv420 = FrameYUV420{
.size = size,
.chromaSize = { native->ChromaWidth(), native->ChromaHeight() },
.y = { native->DataY(), native->StrideY() },
.u = { native->DataU(), native->StrideU() },
.v = { native->DataV(), native->StrideV() },
};
return true;
}
frame->format = FrameFormat::ARGB32;
frame->yuv420 = FrameYUV420{
.size = size,
};
if (!FFmpeg::GoodStorageForFrame(frame->original, size)) {
frame->original = FFmpeg::CreateFrameStorage(size);
}
_decodeContext = FFmpeg::MakeSwscalePointer(
size,
AV_PIX_FMT_YUV420P,
size,
AV_PIX_FMT_BGRA,
&_decodeContext);
Assert(_decodeContext != nullptr);
// AV_NUM_DATA_POINTERS defined in AVFrame struct
const uint8_t *src[AV_NUM_DATA_POINTERS] = {
native->DataY(),
native->DataU(),
native->DataV(),
nullptr
};
int srcLineSize[AV_NUM_DATA_POINTERS] = {
native->StrideY(),
native->StrideU(),
native->StrideV(),
0
};
uint8_t *dst[AV_NUM_DATA_POINTERS] = { frame->original.bits(), nullptr };
int dstLineSize[AV_NUM_DATA_POINTERS] = { int(frame->original.bytesPerLine()), 0 };
sws_scale(
_decodeContext.get(),
src,
srcLineSize,
0,
frame->original.height(),
dst,
dstLineSize);
return true;
}
void VideoTrack::Sink::notifyFrameDecoded() {
crl::on_main([weak = weak_from_this()] {
if (const auto strong = weak.lock()) {
strong->_renderNextFrameOnMain.fire({});
}
});
}
int VideoTrack::Sink::counter() const {
return _counter.load(std::memory_order_acquire);
}
not_null<VideoTrack::Frame*> VideoTrack::Sink::getFrame(int index) {
Expects(index >= 0 && index < kFramesCount);
return &_frames[index];
}
not_null<const VideoTrack::Frame*> VideoTrack::Sink::getFrame(
int index) const {
Expects(index >= 0 && index < kFramesCount);
return &_frames[index];
}
// Sometimes main thread subscribes to check frame requests before
// the first frame is ready and presented and sometimes after.
bool VideoTrack::Sink::firstPresentHappened() const {
switch (counter()) {
case 0: return false;
case 1: return true;
}
Unexpected("Counter value in VideoTrack::Sink::firstPresentHappened.");
}
void VideoTrack::Sink::markFrameShown() {
const auto jump = [&](int counter) {
if (counter == 2 * kFramesCount - 1) {
++_counterCycle;
}
const auto next = (counter + 1) % (2 * kFramesCount);
const auto index = next / 2;
const auto frame = getFrame(index);
frame->displayed = true;
_counter.store(
next,
std::memory_order_release);
};
switch (counter()) {
case 0: return;
case 1: return jump(1);
case 2: return;
case 3: return jump(3);
case 4: return;
case 5: return jump(5);
//case 6: return;
//case 7: return jump(7);
}
Unexpected("Counter value in VideoTrack::Sink::markFrameShown.");
}
not_null<VideoTrack::Frame*> VideoTrack::Sink::frameForPaint() {
return frameForPaintWithIndex().frame;
}
VideoTrack::Sink::FrameWithIndex VideoTrack::Sink::frameForPaintWithIndex() {
const auto index = counter() / 2;
return {
.frame = getFrame(index),
.index = (_counterCycle * 2 * kFramesCount) + index,
};
}
void VideoTrack::Sink::destroyFrameForPaint() {
const auto frame = getFrame(counter() / 2);
if (!frame->original.isNull()) {
frame->original = frame->prepared = QImage();
}
if (frame->native) {
frame->native = nullptr;
}
frame->yuv420 = FrameYUV420();
frame->format = FrameFormat::None;
}
rpl::producer<> VideoTrack::Sink::renderNextFrameOnMain() const {
return _renderNextFrameOnMain.events();
}
VideoTrack::VideoTrack(VideoState state, bool requireARGB32)
: _state(state) {
_sink = std::make_shared<Sink>(requireARGB32);
}
VideoTrack::~VideoTrack() {
}
rpl::producer<> VideoTrack::renderNextFrame() const {
return rpl::merge(
_sink->renderNextFrameOnMain(),
_state.changes() | rpl::to_empty);
}
auto VideoTrack::sink()
-> std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> {
return _sink;
}
[[nodiscard]] VideoState VideoTrack::state() const {
return _state.current();
}
[[nodiscard]] rpl::producer<VideoState> VideoTrack::stateValue() const {
return _state.value();
}
[[nodiscard]] rpl::producer<VideoState> VideoTrack::stateChanges() const {
return _state.changes();
}
void VideoTrack::setState(VideoState state) {
if (state == VideoState::Inactive) {
_inactiveFrom = crl::now();
} else {
_inactiveFrom = 0;
}
_state = state;
if (state == VideoState::Inactive) {
// save last frame?..
_sink->destroyFrameForPaint();
}
}
void VideoTrack::markFrameShown() {
_sink->markFrameShown();
}
QImage VideoTrack::frame(const FrameRequest &request) {
if (_inactiveFrom > 0
&& (_inactiveFrom + kDropFramesWhileInactive > crl::now())) {
_sink->destroyFrameForPaint();
return {};
}
const auto frame = _sink->frameForPaint();
const auto preparedFor = frame->request;
const auto changed = !preparedFor.goodFor(request);
const auto useRequest = changed ? request : preparedFor;
if (changed) {
//_wrapped.with([=](Implementation &unwrapped) {
// unwrapped.updateFrameRequest(instance, useRequest);
//});
}
if (!frame->alpha
&& GoodForRequest(frame->original, frame->rotation, useRequest)) {
return frame->original;
} else if (changed || frame->prepared.isNull()) {
if (changed) {
frame->request = useRequest;
}
frame->prepared = PrepareByRequest(
frame->original,
frame->alpha,
frame->rotation,
useRequest,
std::move(frame->prepared));
}
return frame->prepared;
}
FrameWithInfo VideoTrack::frameWithInfo(bool requireARGB32) const {
if (_inactiveFrom > 0
&& (_inactiveFrom + kDropFramesWhileInactive > crl::now())) {
_sink->destroyFrameForPaint();
return {};
}
const auto data = _sink->frameForPaintWithIndex();
Assert(!requireARGB32
|| (data.frame->format == FrameFormat::ARGB32)
|| (data.frame->format == FrameFormat::None));
if (data.frame->requireARGB32 && !requireARGB32) {
data.frame->requireARGB32 = requireARGB32;
}
return {
.mcstimestamp = data.frame->mcstimestamp,
.original = data.frame->original,
.yuv420 = &data.frame->yuv420,
.format = data.frame->format,
.rotation = data.frame->rotation,
.index = data.index,
};
}
QSize VideoTrack::frameSize() const {
if (_inactiveFrom > 0
&& (_inactiveFrom + kDropFramesWhileInactive > crl::now())) {
_sink->destroyFrameForPaint();
return {};
}
const auto frame = _sink->frameForPaint();
const auto size = frame->yuv420.size;
const auto rotation = frame->rotation;
return (rotation == 90 || rotation == 270)
? QSize(size.height(), size.width())
: size;
}
void VideoTrack::PrepareFrameByRequests(
not_null<Frame*> frame,
int rotation) {
Expects(frame->format != FrameFormat::ARGB32
|| !frame->original.isNull());
frame->rotation = rotation;
if (frame->format != FrameFormat::ARGB32) {
return;
}
if (frame->alpha
|| !GoodForRequest(frame->original, rotation, frame->request)) {
frame->prepared = PrepareByRequest(
frame->original,
frame->alpha,
rotation,
frame->request,
std::move(frame->prepared));
}
}
} // namespace Webrtc