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

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,247 @@
/*
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_audio_track.h"
#include "media/streaming/media_streaming_utility.h"
#include "media/audio/media_audio.h"
#include "media/audio/media_child_ffmpeg_loader.h"
#include "media/player/media_player_instance.h"
namespace Media {
namespace Streaming {
AudioTrack::AudioTrack(
const PlaybackOptions &options,
Stream &&stream,
AudioMsgId audioId,
FnMut<void(const Information &)> ready,
Fn<void(Error)> error)
: _options(options)
, _stream(std::move(stream))
, _audioId(audioId)
, _ready(std::move(ready))
, _error(std::move(error))
, _playPosition(options.position) {
Expects(_stream.duration > 1);
Expects(_stream.duration != kDurationUnavailable); // Not supported.
Expects(_ready != nullptr);
Expects(_error != nullptr);
Expects(_audioId.externalPlayId() != 0);
}
int AudioTrack::streamIndex() const {
// Thread-safe, because _stream.index is immutable.
return _stream.index;
}
AVRational AudioTrack::streamTimeBase() const {
return _stream.timeBase;
}
crl::time AudioTrack::streamDuration() const {
return _stream.duration;
}
void AudioTrack::process(std::vector<FFmpeg::Packet> &&packets) {
if (packets.empty()) {
return;
} else if (packets.front().empty()) {
Assert(packets.size() == 1);
_readTillEnd = true;
}
for (auto i = begin(packets), e = end(packets); i != e; ++i) {
if (initialized()) {
mixerEnqueue(gsl::make_span(&*i, (e - i)));
break;
} else if (!tryReadFirstFrame(std::move(*i))) {
_error(Error::InvalidData);
break;
}
}
}
void AudioTrack::waitForData() {
if (initialized()) {
mixerForceToBuffer();
}
}
bool AudioTrack::initialized() const {
return !_ready;
}
bool AudioTrack::tryReadFirstFrame(FFmpeg::Packet &&packet) {
if (ProcessPacket(_stream, std::move(packet)).failed()) {
return false;
}
while (true) {
if (const auto error = ReadNextFrame(_stream)) {
if (error.code() == AVERROR_EOF) {
if (!_initialSkippingFrame) {
return false;
}
// Return the last valid frame if we seek too far.
_stream.decodedFrame = std::move(_initialSkippingFrame);
return processFirstFrame();
} else if (error.code() != AVERROR(EAGAIN) || _readTillEnd) {
return false;
} else {
// Waiting for more packets.
return true;
}
} else if (!fillStateFromFrame()) {
return false;
} else if (_startedPosition >= _options.position) {
return processFirstFrame();
}
// Seek was with AVSEEK_FLAG_BACKWARD so first we get old frames.
// Try skipping frames until one is after the requested position.
std::swap(_initialSkippingFrame, _stream.decodedFrame);
if (!_stream.decodedFrame) {
_stream.decodedFrame = FFmpeg::MakeFramePointer();
}
}
}
bool AudioTrack::processFirstFrame() {
if (!FFmpeg::FrameHasData(_stream.decodedFrame.get())) {
return false;
}
mixerInit();
callReady();
return true;
}
bool AudioTrack::fillStateFromFrame() {
const auto position = FramePosition(_stream);
if (position == kTimeUnknown) {
return false;
}
_startedPosition = position;
return true;
}
void AudioTrack::mixerInit() {
Expects(!initialized());
auto data = std::make_unique<ExternalSoundData>();
data->frame = std::move(_stream.decodedFrame);
data->codec = std::move(_stream.codec);
data->duration = _stream.duration;
data->speed = _options.speed;
Media::Player::mixer()->play(
_audioId,
std::move(data),
_startedPosition);
}
void AudioTrack::callReady() {
Expects(_ready != nullptr);
auto data = AudioInformation();
data.state.duration = _stream.duration;
data.state.position = _startedPosition;
data.state.receivedTill = _readTillEnd
? _stream.duration
: _startedPosition;
base::take(_ready)({ VideoInformation(), data });
}
void AudioTrack::mixerEnqueue(gsl::span<FFmpeg::Packet> packets) {
Media::Player::mixer()->feedFromExternal({
_audioId,
packets
});
}
void AudioTrack::mixerForceToBuffer() {
Media::Player::mixer()->forceToBufferExternal(_audioId);
}
void AudioTrack::pause(crl::time time) {
Expects(initialized());
Media::Player::mixer()->pause(_audioId, true);
}
void AudioTrack::resume(crl::time time) {
Expects(initialized());
Media::Player::mixer()->resume(_audioId, true);
}
void AudioTrack::stop() {
if (_audioId.externalPlayId()) {
Media::Player::mixer()->stop(_audioId);
}
}
void AudioTrack::setSpeed(float64 speed) {
_options.speed = speed;
Media::Player::mixer()->setSpeedFromExternal(_audioId, speed);
}
rpl::producer<> AudioTrack::waitingForData() const {
return _waitingForData.events();
}
rpl::producer<crl::time> AudioTrack::playPosition() {
Expects(_ready == nullptr);
if (!_subscription) {
_subscription = Media::Player::Updated(
) | rpl::on_next([=](const AudioMsgId &id) {
using State = Media::Player::State;
if (id != _audioId) {
return;
}
const auto state = Media::Player::mixer()->currentState(
_audioId.type());
if (state.id != _audioId) {
// #TODO streaming later muted by other
return;
} else switch (state.state) {
case State::Stopped:
case State::StoppedAtEnd:
case State::PausedAtEnd:
_playPosition.reset();
return;
case State::StoppedAtError:
case State::StoppedAtStart:
_error(Error::InvalidData);
return;
case State::Starting:
case State::Playing:
case State::Stopping:
case State::Pausing:
case State::Resuming:
if (state.waitingForData) {
_waitingForData.fire({});
}
_playPosition = std::clamp(
crl::time((state.position * 1000 + (state.frequency / 2))
/ state.frequency),
crl::time(0),
_stream.duration - 1);
return;
case State::Paused:
return;
}
});
}
return _playPosition.value();
}
AudioTrack::~AudioTrack() {
stop();
}
} // namespace Streaming
} // namespace Media

View File

@@ -0,0 +1,94 @@
/*
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 "media/streaming/media_streaming_utility.h"
namespace Media {
namespace Streaming {
class AudioTrack final {
public:
// Called from some unspecified thread.
// Callbacks are assumed to be thread-safe.
AudioTrack(
const PlaybackOptions &options,
Stream &&stream,
AudioMsgId audioId,
FnMut<void(const Information &)> ready,
Fn<void(Error)> error);
// Called from the main thread.
// Must be called after 'ready' was invoked.
void pause(crl::time time);
void resume(crl::time time);
// Allow to irreversibly stop only audio track.
void stop();
// Called from the main thread.
void setSpeed(float64 speed);
[[nodiscard]] rpl::producer<> waitingForData() const;
// Called from the main thread.
// Non-const, because we subscribe to changes on the first call.
// Must be called after 'ready' was invoked.
[[nodiscard]] rpl::producer<crl::time> playPosition();
// Thread-safe.
[[nodiscard]] int streamIndex() const;
[[nodiscard]] AVRational streamTimeBase() const;
[[nodiscard]] crl::time streamDuration() const;
// Called from the same unspecified thread.
void process(std::vector<FFmpeg::Packet> &&packets);
void waitForData();
// Called from the main thread.
~AudioTrack();
private:
// Called from the same unspecified thread.
[[nodiscard]] bool initialized() const;
[[nodiscard]] bool tryReadFirstFrame(FFmpeg::Packet &&packet);
[[nodiscard]] bool fillStateFromFrame();
[[nodiscard]] bool processFirstFrame();
void mixerInit();
void mixerEnqueue(gsl::span<FFmpeg::Packet> packets);
void mixerForceToBuffer();
void callReady();
PlaybackOptions _options;
// Accessed from the same unspecified thread.
Stream _stream;
const AudioMsgId _audioId;
bool _readTillEnd = false;
// Assumed to be thread-safe.
FnMut<void(const Information &)> _ready;
const Fn<void(Error)> _error;
// First set from the same unspecified thread before _ready is called.
// After that is immutable.
crl::time _startedPosition = kTimeUnknown;
// Accessed from the main thread.
rpl::lifetime _subscription;
rpl::event_stream<> _waitingForData;
// First set from the same unspecified thread before _ready is called.
// After that accessed from the main thread.
rpl::variable<crl::time> _playPosition;
// For initial frame skipping for an exact seek.
FFmpeg::FramePointer _initialSkippingFrame;
};
} // namespace Streaming
} // namespace Media

View File

@@ -0,0 +1,200 @@
/*
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 "data/data_audio_msg_id.h"
#include "ui/image/image_prepare.h"
#include "ui/rect_part.h"
namespace Media {
inline constexpr auto kTimeUnknown = std::numeric_limits<crl::time>::min();
inline constexpr auto kDurationMax = crl::time(std::numeric_limits<int>::max());
inline constexpr auto kDurationUnavailable = std::numeric_limits<crl::time>::max();
namespace Audio {
bool SupportsSpeedControl();
} // namespace Audio
namespace Streaming {
inline bool SupportsSpeedControl() {
return Media::Audio::SupportsSpeedControl();
}
class VideoTrack;
class AudioTrack;
enum class Mode {
Both,
Audio,
Video,
Inspection,
};
struct PlaybackOptions {
Mode mode = Mode::Both;
crl::time position = 0;
crl::time durationOverride = 0;
float64 speed = 1.; // Valid values between 0.5 and 2.
AudioMsgId audioId;
bool syncVideoByAudio = true;
bool waitForMarkAsShown = false;
bool hwAllowed = false;
bool seekable = true;
bool loop = false;
};
struct TrackState {
crl::time position = kTimeUnknown;
crl::time receivedTill = kTimeUnknown;
crl::time duration = kTimeUnknown;
};
struct VideoInformation {
TrackState state;
QSize size;
QImage cover;
int rotation = 0;
bool alpha = false;
};
struct AudioInformation {
TrackState state;
};
struct Information {
VideoInformation video;
AudioInformation audio;
int headerSize = 0;
};
template <typename Track>
struct PreloadedUpdate {
crl::time till = kTimeUnknown;
};
template <typename Track>
struct PlaybackUpdate {
crl::time position = kTimeUnknown;
};
using PreloadedVideo = PreloadedUpdate<VideoTrack>;
using UpdateVideo = PlaybackUpdate<VideoTrack>;
using PreloadedAudio = PreloadedUpdate<AudioTrack>;
using UpdateAudio = PlaybackUpdate<AudioTrack>;
struct WaitingForData {
bool waiting = false;
};
struct SpeedEstimate {
int bytesPerSecond = 0;
bool unreliable = false;
};
struct MutedByOther {
};
struct Finished {
};
struct Update {
std::variant<
Information,
PreloadedVideo,
UpdateVideo,
PreloadedAudio,
UpdateAudio,
WaitingForData,
SpeedEstimate,
MutedByOther,
Finished> data;
};
enum class Error {
OpenFailed,
LoadFailed,
InvalidData,
NotStreamable,
};
struct FrameRequest {
QSize resize;
QSize outer;
Images::CornersMaskRef rounding;
QImage mask;
QColor colored = QColor(0, 0, 0, 0);
bool blurredBackground = false;
bool requireARGB32 = true;
bool keepAlpha = false;
bool strict = true;
static FrameRequest NonStrict() {
auto result = FrameRequest();
result.strict = false;
return result;
}
[[nodiscard]] bool empty() const {
return blurredBackground ? outer.isEmpty() : resize.isEmpty();
}
[[nodiscard]] bool operator==(const FrameRequest &other) const {
return (resize == other.resize)
&& (outer == other.outer)
&& (rounding == other.rounding)
&& (mask.constBits() == other.mask.constBits())
&& (colored == other.colored)
&& (keepAlpha == other.keepAlpha)
&& (requireARGB32 == other.requireARGB32)
&& (blurredBackground == other.blurredBackground);
}
[[nodiscard]] bool operator!=(const FrameRequest &other) const {
return !(*this == other);
}
[[nodiscard]] bool goodFor(const FrameRequest &other) const {
return (blurredBackground == other.blurredBackground)
&& (requireARGB32 == other.requireARGB32)
&& (keepAlpha == other.keepAlpha)
&& (colored == other.colored)
&& ((strict && !other.strict) || (*this == other));
}
};
enum class FrameFormat {
None,
ARGB32,
YUV420,
NV12,
};
struct FrameChannel {
const void *data = nullptr;
int stride = 0;
};
struct FrameYUV {
QSize size;
QSize chromaSize;
FrameChannel y;
FrameChannel u;
FrameChannel v;
};
struct FrameWithInfo {
QImage image;
FrameYUV *yuv = nullptr;
FrameFormat format = FrameFormat::None;
int index = -1;
bool alpha = false;
};
} // namespace Streaming
} // namespace Media

View File

@@ -0,0 +1,368 @@
/*
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_document.h"
#include "media/streaming/media_streaming_instance.h"
#include "media/streaming/media_streaming_loader.h"
#include "media/streaming/media_streaming_reader.h"
#include "data/data_session.h"
#include "data/data_document.h"
#include "data/data_photo.h"
#include "data/data_document_media.h"
#include "data/data_file_origin.h"
#include "main/main_session.h"
#include "storage/file_download.h" // Storage::kMaxFileInMemory.
#include "styles/style_widgets.h"
#include <QtCore/QBuffer>
namespace Media {
namespace Streaming {
namespace {
constexpr auto kWaitingFastDuration = crl::time(200);
constexpr auto kWaitingShowDuration = crl::time(500);
constexpr auto kWaitingShowDelay = crl::time(500);
constexpr auto kGoodThumbQuality = 87;
constexpr auto kSwitchQualityUpPreloadedThreshold = 4 * crl::time(1000);
constexpr auto kSwitchQualityUpSpeedMultiplier = 1.2;
} // namespace
Document::Document(
not_null<DocumentData*> document,
std::shared_ptr<Reader> reader,
std::vector<QualityDescriptor> otherQualities)
: Document(std::move(reader), document, {}, std::move(otherQualities)) {
_player.fullInCache(
) | rpl::on_next([=](bool fullInCache) {
_document->setLoadedInMediaCache(fullInCache);
}, _player.lifetime());
}
Document::Document(
not_null<PhotoData*> photo,
std::shared_ptr<Reader> reader,
std::vector<QualityDescriptor> otherQualities)
: Document(std::move(reader), {}, photo, {}) {
}
Document::Document(std::unique_ptr<Loader> loader)
: Document(std::make_shared<Reader>(std::move(loader)), {}, {}, {}) {
}
Document::Document(
std::shared_ptr<Reader> reader,
DocumentData *document,
PhotoData *photo,
std::vector<QualityDescriptor> otherQualities)
: _document(document)
, _photo(photo)
, _player(std::move(reader))
, _radial(
[=] { waitingCallback(); },
st::defaultInfiniteRadialAnimation)
, _otherQualities(std::move(otherQualities)) {
resubscribe();
}
void Document::resubscribe() {
_subscription = _player.updates(
) | rpl::on_next_error([=](Update &&update) {
handleUpdate(std::move(update));
}, [=](Streaming::Error &&error) {
handleError(std::move(error));
resubscribe();
});
}
Player &Document::player() {
return _player;
}
const Player &Document::player() const {
return _player;
}
const Information &Document::info() const {
return _info;
}
void Document::play(const PlaybackOptions &options) {
_player.play(options);
_info.audio.state.position
= _info.video.state.position
= options.position;
waitingChange(true);
}
void Document::saveFrameToCover() {
_info.video.cover = _player.ready()
? _player.currentFrameImage()
: _info.video.cover;
}
void Document::registerInstance(not_null<Instance*> instance) {
_instances.emplace(instance);
}
void Document::unregisterInstance(not_null<Instance*> instance) {
_instances.remove(instance);
_player.unregisterInstance(instance);
refreshPlayerPriority();
}
void Document::refreshPlayerPriority() {
if (_instances.empty()) {
return;
}
const auto max = ranges::max_element(
_instances,
ranges::less(),
&Instance::priority);
_player.setLoaderPriority((*max)->priority());
}
bool Document::waitingShown() const {
if (!_fading.animating() && !_waiting) {
_radial.stop(anim::type::instant);
return false;
}
return _radial.animating();
}
float64 Document::waitingOpacity() const {
return _fading.value(_waiting ? 1. : 0.);
}
Ui::RadialState Document::waitingState() const {
return _radial.computeState();
}
rpl::producer<int> Document::switchQualityRequests() const {
return _switchQualityRequests.events();
}
void Document::handleUpdate(Update &&update) {
v::match(update.data, [&](Information &update) {
ready(std::move(update));
}, [&](PreloadedVideo update) {
_info.video.state.receivedTill = update.till;
checkSwitchToHigherQuality();
}, [&](UpdateVideo update) {
_info.video.state.position = update.position;
}, [&](PreloadedAudio update) {
_info.audio.state.receivedTill = update.till;
}, [&](UpdateAudio update) {
_info.audio.state.position = update.position;
}, [&](WaitingForData update) {
waitingChange(update.waiting);
}, [&](SpeedEstimate update) {
checkForQualitySwitch(update);
}, [](MutedByOther) {
}, [&](Finished) {
const auto finishTrack = [](TrackState &state) {
state.position = state.receivedTill = state.duration;
};
finishTrack(_info.audio.state);
finishTrack(_info.video.state);
});
}
void Document::setOtherQualities(std::vector<QualityDescriptor> value) {
_otherQualities = std::move(value);
checkForQualitySwitch(_lastSpeedEstimate);
}
void Document::checkForQualitySwitch(SpeedEstimate estimate) {
_lastSpeedEstimate = estimate;
if (!checkSwitchToHigherQuality()) {
checkSwitchToLowerQuality();
}
}
bool Document::checkSwitchToHigherQuality() {
if (_otherQualities.empty()
|| (_info.video.state.duration == kTimeUnknown)
|| (_info.video.state.duration == kDurationUnavailable)
|| (_info.video.state.position == kTimeUnknown)
|| (_info.video.state.receivedTill == kTimeUnknown)
|| !_lastSpeedEstimate.bytesPerSecond
|| _lastSpeedEstimate.unreliable
|| (_info.video.state.receivedTill
< std::min(
_info.video.state.duration,
(_info.video.state.position
+ kSwitchQualityUpPreloadedThreshold)))) {
return false;
}
const auto size = _player.fileSize();
Assert(size >= 0 && size <= std::numeric_limits<uint32>::max());
auto to = QualityDescriptor{ .sizeInBytes = uint32(size) };
const auto duration = _info.video.state.duration / 1000.;
const auto speed = _player.speed();
const auto multiplier = speed * kSwitchQualityUpSpeedMultiplier;
for (const auto &descriptor : _otherQualities) {
const auto perSecond = descriptor.sizeInBytes / duration;
if (descriptor.sizeInBytes > to.sizeInBytes
&& _lastSpeedEstimate.bytesPerSecond >= perSecond * multiplier) {
to = descriptor;
}
}
if (!to.height) {
return false;
}
_switchQualityRequests.fire_copy(to.height);
return true;
}
bool Document::checkSwitchToLowerQuality() {
if (_otherQualities.empty()
|| !_waiting
|| !_radial.animating()
|| !_lastSpeedEstimate.bytesPerSecond) {
return false;
}
const auto size = _player.fileSize();
Assert(size >= 0 && size <= std::numeric_limits<uint32>::max());
auto to = QualityDescriptor();
for (const auto &descriptor : _otherQualities) {
if (descriptor.sizeInBytes < size
&& descriptor.sizeInBytes > to.sizeInBytes) {
to = descriptor;
}
}
if (!to.height) {
return false;
}
_switchQualityRequests.fire_copy(to.height);
return true;
}
void Document::handleError(Error &&error) {
if (_document) {
if (error == Error::NotStreamable) {
_document->setNotSupportsStreaming();
} else if (error == Error::OpenFailed) {
_document->setInappPlaybackFailed();
}
} else if (_photo) {
if (error == Error::NotStreamable || error == Error::OpenFailed) {
_photo->setVideoPlaybackFailed();
}
}
waitingChange(false);
}
void Document::ready(Information &&info) {
_info = std::move(info);
validateGoodThumbnail();
waitingChange(false);
}
void Document::waitingChange(bool waiting) {
if (_waiting == waiting) {
return;
}
_waiting = waiting;
const auto fade = [=](crl::time duration) {
if (!_radial.animating()) {
_radial.start(
st::defaultInfiniteRadialAnimation.sineDuration);
}
_fading.start([=] {
waitingCallback();
}, _waiting ? 0. : 1., _waiting ? 1. : 0., duration);
checkSwitchToLowerQuality();
};
if (waiting) {
if (_radial.animating()) {
_timer.cancel();
fade(kWaitingFastDuration);
} else {
_timer.callOnce(kWaitingShowDelay);
_timer.setCallback([=] {
fade(kWaitingShowDuration);
});
}
} else {
_timer.cancel();
if (_radial.animating()) {
fade(kWaitingFastDuration);
}
}
}
void Document::validateGoodThumbnail() {
if (_info.video.cover.isNull()
|| !_document
|| _document->goodThumbnailChecked()) {
return;
}
const auto sticker = (_document->sticker() != nullptr);
const auto document = _document;
const auto information = _info.video;
const auto key = document->goodThumbnailCacheKey();
const auto guard = base::make_weak(&document->session());
document->owner().cache().get(key, [=](QByteArray value) {
if (!value.isEmpty()) {
return;
}
const auto image = [&] {
auto result = information.cover;
if (information.rotation != 0) {
auto transform = QTransform();
transform.rotate(information.rotation);
result = result.transformed(transform);
}
if (result.size() != information.size) {
result = result.scaled(
information.size,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
if (!sticker && information.alpha) {
result = Images::Opaque(std::move(result));
}
return result;
}();
auto bytes = QByteArray();
{
auto buffer = QBuffer(&bytes);
image.save(&buffer, sticker ? "WEBP" : "JPG", kGoodThumbQuality);
}
const auto length = bytes.size();
if (!length || length > Storage::kMaxFileInMemory) {
LOG(("App Error: Bad thumbnail data for saving to cache."));
bytes = "(failed)"_q;
}
crl::on_main(guard, [=] {
if (const auto active = document->activeMediaView()) {
active->setGoodThumbnail(image);
}
if (bytes != "(failed)"_q) {
document->setGoodThumbnailChecked(true);
}
document->owner().cache().putIfEmpty(
document->goodThumbnailCacheKey(),
Storage::Cache::Database::TaggedValue(
base::duplicate(bytes),
Data::kImageCacheTag));
});
});
}
void Document::waitingCallback() {
for (const auto &instance : _instances) {
instance->callWaitingCallback();
}
}
} // namespace Streaming
} // namespace Media

View File

@@ -0,0 +1,98 @@
/*
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 "media/streaming/media_streaming_player.h"
#include "ui/effects/radial_animation.h"
#include "ui/effects/animations.h"
#include "base/timer.h"
class DocumentData;
namespace Media::Streaming {
class Instance;
class Loader;
struct QualityDescriptor {
uint32 sizeInBytes = 0;
uint32 height = 0;
};
class Document {
public:
Document(
not_null<DocumentData*> document,
std::shared_ptr<Reader> reader,
std::vector<QualityDescriptor> otherQualities = {});
Document(
not_null<PhotoData*> photo,
std::shared_ptr<Reader> reader,
std::vector<QualityDescriptor> otherQualities = {});
explicit Document(std::unique_ptr<Loader> loader);
void play(const PlaybackOptions &options);
void saveFrameToCover();
[[nodiscard]] Player &player();
[[nodiscard]] const Player &player() const;
[[nodiscard]] const Information &info() const;
[[nodiscard]] bool waitingShown() const;
[[nodiscard]] float64 waitingOpacity() const;
[[nodiscard]] Ui::RadialState waitingState() const;
void setOtherQualities(std::vector<QualityDescriptor> value);
[[nodiscard]] rpl::producer<int> switchQualityRequests() const;
private:
Document(
std::shared_ptr<Reader> reader,
DocumentData *document,
PhotoData *photo,
std::vector<QualityDescriptor> otherQualities);
friend class Instance;
void registerInstance(not_null<Instance*> instance);
void unregisterInstance(not_null<Instance*> instance);
void refreshPlayerPriority();
void waitingCallback();
void checkForQualitySwitch(SpeedEstimate estimate);
bool checkSwitchToHigherQuality();
bool checkSwitchToLowerQuality();
void handleUpdate(Update &&update);
void handleError(Error &&error);
void ready(Information &&info);
void waitingChange(bool waiting);
void validateGoodThumbnail();
void resubscribe();
DocumentData *_document = nullptr;
PhotoData *_photo = nullptr;
Player _player;
Information _info;
rpl::lifetime _subscription;
mutable Ui::InfiniteRadialAnimation _radial;
Ui::Animations::Simple _fading;
base::Timer _timer;
base::flat_set<not_null<Instance*>> _instances;
std::vector<QualityDescriptor> _otherQualities;
rpl::event_stream<int> _switchQualityRequests;
SpeedEstimate _lastSpeedEstimate;
bool _waiting = false;
};
} // namespace Media::Streaming

View File

@@ -0,0 +1,506 @@
/*
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_file.h"
#include "media/streaming/media_streaming_loader.h"
#include "media/streaming/media_streaming_file_delegate.h"
#include "ffmpeg/ffmpeg_utility.h"
namespace Media {
namespace Streaming {
namespace {
constexpr auto kMaxSingleReadAmount = 8 * 1024 * 1024;
constexpr auto kMaxQueuedPackets = 1024;
[[nodiscard]] bool UnreliableFormatDuration(
not_null<AVFormatContext*> format,
not_null<AVStream*> stream,
Mode mode) {
return (mode == Mode::Video || mode == Mode::Inspection)
&& stream->codecpar
&& (stream->codecpar->codec_id == AV_CODEC_ID_VP9)
&& format->iformat
&& format->iformat->name
&& QString::fromLatin1(
format->iformat->name
).split(QChar(',')).contains(u"webm");
}
} // namespace
File::Context::Context(
not_null<FileDelegate*> delegate,
not_null<Reader*> reader)
: _delegate(delegate)
, _reader(reader)
, _size(reader->size()) {
}
File::Context::~Context() = default;
int File::Context::Read(void *opaque, uint8_t *buffer, int bufferSize) {
return static_cast<Context*>(opaque)->read(
bytes::make_span(buffer, bufferSize));
}
int64_t File::Context::Seek(void *opaque, int64_t offset, int whence) {
return static_cast<Context*>(opaque)->seek(offset, whence);
}
int File::Context::read(bytes::span buffer) {
Expects(_size >= _offset);
const auto amount = std::min(_size - _offset, int64(buffer.size()));
if (unroll()) {
return AVERROR_EXTERNAL;
} else if (amount > kMaxSingleReadAmount) {
LOG(("Streaming Error: Read callback asked for too much data: %1"
).arg(amount));
return AVERROR_EXTERNAL;
} else if (!amount) {
return AVERROR_EOF;
}
buffer = buffer.subspan(0, amount);
while (true) {
const auto result = _reader->fill(_offset, buffer, &_semaphore);
if (result == Reader::FillState::Success) {
break;
} else if (result == Reader::FillState::WaitingRemote) {
// Perhaps for the correct sleeping in case of enough packets
// being read already we require SleepPolicy::Allowed here.
// Otherwise if we wait for the remote frequently and
// _queuedPackets never get to kMaxQueuedPackets and we don't call
// processQueuedPackets(SleepPolicy::Allowed) ever.
//
// But right now we can't simply pass SleepPolicy::Allowed here,
// it freezes because of two _semaphore.acquire one after another.
processQueuedPackets(SleepPolicy::Disallowed);
_delegate->fileWaitingForData();
}
_semaphore.acquire();
if (_interrupted) {
return AVERROR_EXTERNAL;
} else if (const auto error = _reader->streamingError()) {
fail(*error);
return AVERROR_EXTERNAL;
}
}
sendFullInCache();
_offset += amount;
return amount;
}
int64_t File::Context::seek(int64_t offset, int whence) {
const auto checkedSeek = [&](int64_t offset) {
if (_failed || offset < 0 || offset > _size) {
return int64(-1);
}
return (_offset = offset);
};
switch (whence) {
case SEEK_SET: return checkedSeek(offset);
case SEEK_CUR: return checkedSeek(_offset + offset);
case SEEK_END: return checkedSeek(_size + offset);
case AVSEEK_SIZE: return _size;
}
return -1;
}
void File::Context::logError(QLatin1String method) {
if (!unroll()) {
FFmpeg::LogError(method);
}
}
void File::Context::logError(
QLatin1String method,
FFmpeg::AvErrorWrap error) {
if (!unroll()) {
FFmpeg::LogError(method, error);
}
}
void File::Context::logFatal(QLatin1String method) {
if (!unroll()) {
FFmpeg::LogError(method);
fail(_format ? Error::InvalidData : Error::OpenFailed);
}
}
void File::Context::logFatal(
QLatin1String method,
FFmpeg::AvErrorWrap error) {
if (!unroll()) {
FFmpeg::LogError(method, error);
fail(_format ? Error::InvalidData : Error::OpenFailed);
}
}
Stream File::Context::initStream(
not_null<AVFormatContext*> format,
AVMediaType type,
Mode mode,
StartOptions options) {
auto result = Stream();
const auto index = result.index = av_find_best_stream(
format,
type,
-1,
-1,
nullptr,
0);
if (index < 0) {
return {};
}
const auto info = format->streams[index];
if (type == AVMEDIA_TYPE_VIDEO) {
if (info->disposition & AV_DISPOSITION_ATTACHED_PIC) {
// ignore cover streams
return Stream();
}
result.codec = FFmpeg::MakeCodecPointer({
.stream = info,
.hwAllowed = options.hwAllow,
});
if (!result.codec) {
return result;
}
result.rotation = FFmpeg::ReadRotationFromMetadata(info);
result.aspect = FFmpeg::ValidateAspectRatio(
info->sample_aspect_ratio);
} else if (type == AVMEDIA_TYPE_AUDIO) {
result.frequency = info->codecpar->sample_rate;
if (!result.frequency) {
return result;
}
result.codec = FFmpeg::MakeCodecPointer({ .stream = info });
if (!result.codec) {
return result;
}
}
result.decodedFrame = FFmpeg::MakeFramePointer();
if (!result.decodedFrame) {
result.codec = nullptr;
return result;
}
result.timeBase = info->time_base;
result.duration = options.durationOverride
? options.durationOverride
: (info->duration != AV_NOPTS_VALUE)
? FFmpeg::PtsToTime(info->duration, result.timeBase)
: UnreliableFormatDuration(format, info, mode)
? kTimeUnknown
: FFmpeg::PtsToTime(format->duration, FFmpeg::kUniversalTimeBase);
if (result.duration == kTimeUnknown) {
result.duration = kDurationUnavailable;
} else if (result.duration <= 0) {
result.codec = nullptr;
} else {
++result.duration;
if (result.duration > kDurationMax) {
result.duration = 0;
result.codec = nullptr;
}
}
return result;
}
void File::Context::seekToPosition(
not_null<AVFormatContext*> format,
const Stream &stream,
crl::time position) {
auto error = FFmpeg::AvErrorWrap();
if (!position) {
return;
} else if (stream.duration == kDurationUnavailable) {
// Seek in files with unknown duration is not supported.
return;
}
//
// Non backward search reads the whole file if the position is after
// the last keyframe inside the index. So we search only backward.
//
//const auto seekFlags = 0;
//error = av_seek_frame(
// format,
// streamIndex,
// TimeToPts(position, kUniversalTimeBase),
// seekFlags);
//if (!error) {
// return;
//}
//
error = av_seek_frame(
format,
stream.index,
FFmpeg::TimeToPts(
std::clamp(position, crl::time(0), stream.duration - 1),
stream.timeBase),
AVSEEK_FLAG_BACKWARD);
if (!error) {
return;
}
return logFatal(qstr("av_seek_frame"), error);
}
std::variant<FFmpeg::Packet, FFmpeg::AvErrorWrap> File::Context::readPacket() {
auto error = FFmpeg::AvErrorWrap();
auto result = FFmpeg::Packet();
error = av_read_frame(_format.get(), &result.fields());
if (unroll()) {
return FFmpeg::AvErrorWrap();
} else if (!error) {
return result;
} else if (error.code() != AVERROR_EOF) {
logFatal(qstr("av_read_frame"), error);
}
return error;
}
void File::Context::start(StartOptions options) {
Expects(options.seekable || !options.position);
auto error = FFmpeg::AvErrorWrap();
if (unroll()) {
return;
}
auto format = FFmpeg::MakeFormatPointer(
static_cast<void*>(this),
&Context::Read,
nullptr,
options.seekable ? &Context::Seek : nullptr);
if (!format) {
return fail(Error::OpenFailed);
}
if ((error = avformat_find_stream_info(format.get(), nullptr))) {
return logFatal(qstr("avformat_find_stream_info"), error);
}
const auto mode = _delegate->fileOpenMode();
auto video = initStream(
format.get(),
AVMEDIA_TYPE_VIDEO,
mode,
options);
if (unroll()) {
return;
}
auto audio = initStream(
format.get(),
AVMEDIA_TYPE_AUDIO,
mode,
options);
if (unroll()) {
return;
}
_reader->headerDone();
if (_reader->isRemoteLoader()) {
sendFullInCache(true);
}
if (options.seekable && (video.codec || audio.codec)) {
seekToPosition(
format.get(),
video.codec ? video : audio,
options.position);
}
if (unroll()) {
return;
}
if (video.codec) {
_queuedPackets[video.index].reserve(kMaxQueuedPackets);
}
if (audio.codec) {
_queuedPackets[audio.index].reserve(kMaxQueuedPackets);
}
const auto header = _reader->headerSize();
if (!_delegate->fileReady(header, std::move(video), std::move(audio))) {
return fail(Error::OpenFailed);
}
_format = std::move(format);
}
void File::Context::sendFullInCache(bool force) {
const auto started = _fullInCache.has_value();
if (force || started) {
const auto nowFullInCache = _reader->fullInCache();
if (!started || *_fullInCache != nowFullInCache) {
_fullInCache = nowFullInCache;
_delegate->fileFullInCache(nowFullInCache);
}
}
}
void File::Context::readNextPacket() {
auto result = readPacket();
if (unroll()) {
return;
} else if (const auto packet = std::get_if<FFmpeg::Packet>(&result)) {
const auto index = packet->fields().stream_index;
const auto i = _queuedPackets.find(index);
if (i == end(_queuedPackets)) {
return;
}
i->second.push_back(std::move(*packet));
if (i->second.size() == kMaxQueuedPackets) {
processQueuedPackets(SleepPolicy::Allowed);
}
Assert(i->second.size() < kMaxQueuedPackets);
} else {
// Still trying to read by drain.
Assert(v::is<FFmpeg::AvErrorWrap>(result));
Assert(v::get<FFmpeg::AvErrorWrap>(result).code() == AVERROR_EOF);
processQueuedPackets(SleepPolicy::Allowed);
if (!finished()) {
handleEndOfFile();
}
}
}
void File::Context::handleEndOfFile() {
_delegate->fileProcessEndOfFile();
if (_delegate->fileReadMore()) {
_readTillEnd = false;
auto error = FFmpeg::AvErrorWrap(av_seek_frame(
_format.get(),
-1, // stream_index
0, // timestamp
AVSEEK_FLAG_BACKWARD));
if (error) {
logFatal(qstr("av_seek_frame"));
}
// If we loaded a file till the end then we think it is fully cached,
// assume we finished loading and don't want to keep all other
// download tasks throttled because of an active streaming.
_reader->tryRemoveLoaderAsync();
} else {
_readTillEnd = true;
}
}
void File::Context::processQueuedPackets(SleepPolicy policy) {
const auto more = _delegate->fileProcessPackets(_queuedPackets);
if (!more && policy == SleepPolicy::Allowed) {
do {
_reader->startSleep(&_semaphore);
_semaphore.acquire();
_reader->stopSleep();
} while (!unroll() && !_delegate->fileReadMore());
}
}
void File::Context::interrupt() {
_interrupted = true;
_semaphore.release();
}
void File::Context::wake() {
_semaphore.release();
}
bool File::Context::interrupted() const {
return _interrupted;
}
bool File::Context::failed() const {
return _failed;
}
bool File::Context::unroll() const {
return failed() || interrupted();
}
void File::Context::fail(Error error) {
_failed = true;
_delegate->fileError(error);
}
bool File::Context::finished() const {
return unroll() || _readTillEnd;
}
void File::Context::stopStreamingAsync() {
// If we finished loading we don't want to keep all other
// download tasks throttled because of an active streaming.
_reader->stopStreamingAsync();
}
File::File(std::shared_ptr<Reader> reader)
: _reader(std::move(reader)) {
}
void File::start(not_null<FileDelegate*> delegate, StartOptions options) {
stop(true);
_reader->startStreaming();
_context.emplace(delegate, _reader.get());
_thread = std::thread([=, context = &*_context] {
crl::toggle_fp_exceptions(true);
context->start(options);
while (!context->finished()) {
context->readNextPacket();
}
if (!context->interrupted()) {
context->stopStreamingAsync();
}
});
}
void File::wake() {
Expects(_context.has_value());
_context->wake();
}
void File::stop(bool stillActive) {
if (_thread.joinable()) {
_context->interrupt();
_thread.join();
}
_reader->stopStreaming(stillActive);
_context.reset();
}
bool File::isRemoteLoader() const {
return _reader->isRemoteLoader();
}
void File::setLoaderPriority(int priority) {
_reader->setLoaderPriority(priority);
}
int64 File::size() const {
return _reader->size();
}
rpl::producer<SpeedEstimate> File::speedEstimate() const {
return _reader->speedEstimate();
}
File::~File() {
stop();
}
} // namespace Streaming
} // namespace Media

View File

@@ -0,0 +1,126 @@
/*
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 "media/streaming/media_streaming_common.h"
#include "media/streaming/media_streaming_utility.h"
#include "media/streaming/media_streaming_reader.h"
#include "ffmpeg/ffmpeg_utility.h"
#include "base/bytes.h"
#include "base/weak_ptr.h"
#include <thread>
namespace Media {
namespace Streaming {
class FileDelegate;
struct StartOptions {
crl::time position = 0;
crl::time durationOverride = 0;
bool seekable = true;
bool hwAllow = false;
};
class File final {
public:
explicit File(std::shared_ptr<Reader> reader);
File(const File &other) = delete;
File &operator=(const File &other) = delete;
void start(not_null<FileDelegate*> delegate, StartOptions options);
void wake();
void stop(bool stillActive = false);
[[nodiscard]] bool isRemoteLoader() const;
void setLoaderPriority(int priority);
[[nodiscard]] int64 size() const;
[[nodiscard]] rpl::producer<SpeedEstimate> speedEstimate() const;
~File();
private:
class Context final : public base::has_weak_ptr {
public:
Context(not_null<FileDelegate*> delegate, not_null<Reader*> reader);
~Context();
void start(StartOptions options);
void readNextPacket();
void interrupt();
void wake();
[[nodiscard]] bool interrupted() const;
[[nodiscard]] bool failed() const;
[[nodiscard]] bool finished() const;
void stopStreamingAsync();
private:
enum class SleepPolicy {
Allowed,
Disallowed,
};
static int Read(void *opaque, uint8_t *buffer, int bufferSize);
static int64_t Seek(void *opaque, int64_t offset, int whence);
[[nodiscard]] int read(bytes::span buffer);
[[nodiscard]] int64_t seek(int64_t offset, int whence);
[[nodiscard]] bool unroll() const;
void logError(QLatin1String method);
void logError(QLatin1String method, FFmpeg::AvErrorWrap error);
void logFatal(QLatin1String method);
void logFatal(QLatin1String method, FFmpeg::AvErrorWrap error);
void fail(Error error);
[[nodiscard]] Stream initStream(
not_null<AVFormatContext *> format,
AVMediaType type,
Mode mode,
StartOptions options);
void seekToPosition(
not_null<AVFormatContext *> format,
const Stream &stream,
crl::time position);
// TODO base::expected.
[[nodiscard]] auto readPacket()
-> std::variant<FFmpeg::Packet, FFmpeg::AvErrorWrap>;
void processQueuedPackets(SleepPolicy policy);
void handleEndOfFile();
void sendFullInCache(bool force = false);
const not_null<FileDelegate*> _delegate;
const not_null<Reader*> _reader;
base::flat_map<int, std::vector<FFmpeg::Packet>> _queuedPackets;
int64 _offset = 0;
int64 _size = 0;
bool _failed = false;
bool _readTillEnd = false;
std::optional<bool> _fullInCache;
crl::semaphore _semaphore;
std::atomic<bool> _interrupted = false;
FFmpeg::FormatPointer _format;
};
std::optional<Context> _context;
std::shared_ptr<Reader> _reader;
std::thread _thread;
};
} // namespace Streaming
} // namespace Media

View 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
namespace FFmpeg {
class Packet;
} // namespace FFmpeg
namespace Media {
namespace Streaming {
struct Stream;
enum class Error;
class FileDelegate {
public:
[[nodiscard]] virtual Mode fileOpenMode() = 0;
[[nodiscard]] virtual bool fileReady(
int headerSize,
Stream &&video,
Stream &&audio) = 0;
virtual void fileError(Error error) = 0;
virtual void fileWaitingForData() = 0;
virtual void fileFullInCache(bool fullInCache) = 0;
virtual void fileProcessEndOfFile() = 0;
// Return true if reading and processing more packets is desired.
// Return false if sleeping until 'wake()' is called is desired.
[[nodiscard]] virtual bool fileProcessPackets(
base::flat_map<int, std::vector<FFmpeg::Packet>> &packets) = 0;
// Also returns true after fileProcessEndOfFile() if looping is desired.
[[nodiscard]] virtual bool fileReadMore() = 0;
};
} // namespace Streaming
} // namespace Media

View File

@@ -0,0 +1,264 @@
/*
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_instance.h"
#include "media/streaming/media_streaming_document.h"
#include "data/data_file_origin.h"
#include "data/data_photo.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "data/data_streaming.h"
namespace Media {
namespace Streaming {
Instance::Instance(const Instance &other)
: _shared(other._shared)
, _waitingCallback(other._waitingCallback)
, _priority(other._priority)
, _playerLocked(other._playerLocked) {
if (_shared) {
_shared->registerInstance(this);
if (_playerLocked) {
_shared->player().lock();
}
}
}
Instance::Instance(
std::shared_ptr<Document> shared,
Fn<void()> waitingCallback)
: _shared(std::move(shared))
, _waitingCallback(std::move(waitingCallback)) {
if (_shared) {
_shared->registerInstance(this);
}
}
Instance::Instance(
not_null<DocumentData*> document,
Data::FileOrigin origin,
Fn<void()> waitingCallback)
: Instance(
document->owner().streaming().sharedDocument(document, origin),
std::move(waitingCallback)) {
}
Instance::Instance(
not_null<DocumentData*> quality,
not_null<DocumentData*> original,
HistoryItem *context,
Data::FileOrigin origin,
Fn<void()> waitingCallback)
: Instance(
quality->owner().streaming().sharedDocument(
quality,
original,
context,
origin),
std::move(waitingCallback)) {
}
Instance::Instance(
not_null<PhotoData*> photo,
Data::FileOrigin origin,
Fn<void()> waitingCallback)
: Instance(
photo->owner().streaming().sharedDocument(photo, origin),
std::move(waitingCallback)) {
}
Instance::~Instance() {
if (_shared) {
unlockPlayer();
_shared->unregisterInstance(this);
}
}
bool Instance::valid() const {
return (_shared != nullptr);
}
std::shared_ptr<Document> Instance::shared() const {
return _shared;
}
const Player &Instance::player() const {
Expects(_shared != nullptr);
return _shared->player();
}
const Information &Instance::info() const {
Expects(_shared != nullptr);
return _shared->info();
}
rpl::producer<int> Instance::switchQualityRequests() const {
return _shared->switchQualityRequests();
}
void Instance::play(const PlaybackOptions &options) {
Expects(_shared != nullptr);
_shared->play(options);
}
void Instance::pause() {
Expects(_shared != nullptr);
_shared->player().pause();
}
void Instance::resume() {
Expects(_shared != nullptr);
_shared->player().resume();
}
void Instance::stop() {
Expects(_shared != nullptr);
_shared->player().stop();
}
void Instance::stopAudio() {
Expects(_shared != nullptr);
_shared->player().stopAudio();
}
void Instance::saveFrameToCover() {
Expects(_shared != nullptr);
_shared->saveFrameToCover();
}
bool Instance::active() const {
Expects(_shared != nullptr);
return _shared->player().active();
}
bool Instance::ready() const {
Expects(_shared != nullptr);
return _shared->player().ready();
}
std::optional<Error> Instance::failed() const {
Expects(_shared != nullptr);
return _shared->player().failed();
}
bool Instance::paused() const {
Expects(_shared != nullptr);
return _shared->player().paused();
}
float64 Instance::speed() const {
Expects(_shared != nullptr);
return _shared->player().speed();
}
void Instance::setSpeed(float64 speed) {
Expects(_shared != nullptr);
_shared->player().setSpeed(speed);
}
bool Instance::waitingShown() const {
Expects(_shared != nullptr);
return _shared->waitingShown();
}
float64 Instance::waitingOpacity() const {
Expects(_shared != nullptr);
return _shared->waitingOpacity();
}
Ui::RadialState Instance::waitingState() const {
Expects(_shared != nullptr);
return _shared->waitingState();
}
void Instance::callWaitingCallback() {
if (_waitingCallback) {
_waitingCallback();
}
}
QImage Instance::frame(const FrameRequest &request) const {
return player().frame(request, this);
}
FrameWithInfo Instance::frameWithInfo(const FrameRequest &request) const {
return player().frameWithInfo(request, this);
}
FrameWithInfo Instance::frameWithInfo() const {
return player().frameWithInfo(this);
}
bool Instance::markFrameShown() const {
Expects(_shared != nullptr);
return _shared->player().markFrameShown();
}
void Instance::lockPlayer() {
Expects(_shared != nullptr);
if (!_playerLocked) {
_playerLocked = true;
_shared->player().lock();
}
}
void Instance::unlockPlayer() {
Expects(_shared != nullptr);
if (_playerLocked) {
_playerLocked = false;
_shared->player().unlock();
}
}
bool Instance::playerLocked() const {
Expects(_shared != nullptr);
return _shared->player().locked();
}
void Instance::setPriority(int priority) {
Expects(_shared != nullptr);
if (_priority == priority) {
return;
}
_priority = priority;
_shared->refreshPlayerPriority();
}
int Instance::priority() const {
return _priority;
}
rpl::lifetime &Instance::lifetime() {
return _lifetime;
}
} // namespace Streaming
} // namespace Media

View File

@@ -0,0 +1,104 @@
/*
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 "media/streaming/media_streaming_common.h"
class DocumentData;
namespace Ui {
struct RadialState;
} // namespace Ui
namespace Data {
struct FileOrigin;
} // namespace Data
namespace Media {
namespace Streaming {
class Document;
class Player;
class Instance {
public:
Instance(const Instance &other);
Instance(
std::shared_ptr<Document> shared,
Fn<void()> waitingCallback);
Instance(
not_null<DocumentData*> document,
Data::FileOrigin origin,
Fn<void()> waitingCallback);
Instance(
not_null<DocumentData*> quality,
not_null<DocumentData*> original,
HistoryItem *context,
Data::FileOrigin origin,
Fn<void()> waitingCallback);
Instance(
not_null<PhotoData*> photo,
Data::FileOrigin origin,
Fn<void()> waitingCallback);
~Instance();
[[nodiscard]] bool valid() const;
[[nodiscard]] std::shared_ptr<Document> shared() const;
[[nodiscard]] const Player &player() const;
[[nodiscard]] const Information &info() const;
[[nodiscard]] rpl::producer<int> switchQualityRequests() const;
void play(const PlaybackOptions &options);
void pause();
void resume();
void stop();
void stopAudio();
void saveFrameToCover();
[[nodiscard]] bool active() const;
[[nodiscard]] bool ready() const;
[[nodiscard]] std::optional<Error> failed() const;
[[nodiscard]] bool paused() const;
[[nodiscard]] float64 speed() const;
void setSpeed(float64 speed);
[[nodiscard]] bool waitingShown() const;
[[nodiscard]] float64 waitingOpacity() const;
[[nodiscard]] Ui::RadialState waitingState() const;
void callWaitingCallback();
[[nodiscard]] QImage frame(const FrameRequest &request) const;
[[nodiscard]] FrameWithInfo frameWithInfo(
const FrameRequest &request) const;
[[nodiscard]] FrameWithInfo frameWithInfo() const;
bool markFrameShown() const;
void lockPlayer();
void unlockPlayer();
[[nodiscard]] bool playerLocked() const;
void setPriority(int priority);
[[nodiscard]] int priority() const;
[[nodiscard]] rpl::lifetime &lifetime();
private:
const std::shared_ptr<Document> _shared;
Fn<void()> _waitingCallback;
int _priority = 1;
bool _playerLocked = false;
rpl::lifetime _lifetime;
};
} // namespace Streaming
} // namespace Media

View File

@@ -0,0 +1,94 @@
/*
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_loader.h"
namespace Media {
namespace Streaming {
bool LoadedPart::valid(int64 size) const {
return (offset != kFailedOffset)
&& ((bytes.size() == Loader::kPartSize)
|| (offset + bytes.size() == size));
}
bool operator<(
const PriorityQueue::Entry &a,
const PriorityQueue::Entry &b) {
if (a.priority > b.priority) {
return true;
} else if (a.priority < b.priority) {
return false;
} else {
return a.value < b.value;
}
}
bool PriorityQueue::add(int64 value) {
const auto i = ranges::find(_data, value, &Entry::value);
if (i == end(_data)) {
_data.insert({ value, _priority });
return true;
} else if (i->priority != _priority) {
_data.erase(i);
_data.insert({ value, _priority });
return true;
}
return false;
}
bool PriorityQueue::remove(int64 value) {
const auto i = ranges::find(_data, value, &Entry::value);
if (i == end(_data)) {
return false;
}
_data.erase(i);
return true;
}
bool PriorityQueue::empty() const {
return _data.empty();
}
std::optional<int64> PriorityQueue::front() const {
return _data.empty()
? std::nullopt
: std::make_optional(_data.front().value);
}
std::optional<int64> PriorityQueue::take() {
if (_data.empty()) {
return std::nullopt;
}
const auto result = _data.front().value;
_data.erase(_data.begin());
return result;
}
base::flat_set<int64> PriorityQueue::takeInRange(int64 from, int64 till) {
auto result = base::flat_set<int64>();
for (auto i = _data.begin(); i != _data.end();) {
if (i->value >= from && i->value < till) {
result.emplace(i->value);
i = _data.erase(i);
} else {
++i;
}
}
return result;
}
void PriorityQueue::clear() {
_data.clear();
}
void PriorityQueue::resetPriorities() {
++_priority;
}
} // namespace Streaming
} // namespace Media

View File

@@ -0,0 +1,80 @@
/*
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 "media/streaming/media_streaming_common.h"
namespace Storage {
class StreamedFileDownloader;
} // namespace Storage
namespace Media::Streaming {
struct LoadedPart {
int64 offset = 0;
QByteArray bytes;
static constexpr auto kFailedOffset = int64(-1);
[[nodiscard]] bool valid(int64 size) const;
};
class Loader {
public:
static constexpr auto kPartSize = int64(128 * 1024);
[[nodiscard]] virtual Storage::Cache::Key baseCacheKey() const = 0;
[[nodiscard]] virtual int64 size() const = 0;
virtual void load(int64 offset) = 0;
virtual void cancel(int64 offset) = 0;
virtual void resetPriorities() = 0;
virtual void setPriority(int priority) = 0;
virtual void stop() = 0;
// Remove from queue if no requests are in progress.
virtual void tryRemoveFromQueue() = 0;
// Parts will be sent from the main thread.
[[nodiscard]] virtual rpl::producer<LoadedPart> parts() const = 0;
[[nodiscard]] virtual auto speedEstimate() const
-> rpl::producer<SpeedEstimate> = 0;
virtual void attachDownloader(
not_null<Storage::StreamedFileDownloader*> downloader) = 0;
virtual void clearAttachedDownloader() = 0;
virtual ~Loader() = default;
};
class PriorityQueue {
public:
bool add(int64 value);
bool remove(int64 value);
void resetPriorities();
[[nodiscard]] bool empty() const;
[[nodiscard]] std::optional<int64> front() const;
[[nodiscard]] std::optional<int64> take();
[[nodiscard]] base::flat_set<int64> takeInRange(int64 from, int64 till);
void clear();
private:
struct Entry {
int64 value = 0;
int priority = 0;
};
friend bool operator<(const Entry &a, const Entry &b);
base::flat_set<Entry> _data;
int _priority = 0;
};
} // namespace Media::Streaming

View File

@@ -0,0 +1,115 @@
/*
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_loader_local.h"
#include "storage/cache/storage_cache_types.h"
#include <QtCore/QBuffer>
namespace Media {
namespace Streaming {
namespace {
// This is the maximum file size in Telegram API.
constexpr auto kMaxFileSize = 8000 * int64(512 * 1024);
[[nodiscard]] int64 ValidateLocalSize(int64 size) {
return (size > 0 && size <= kMaxFileSize) ? size : 0;
}
} // namespace
LoaderLocal::LoaderLocal(std::unique_ptr<QIODevice> device)
: _device(std::move(device))
, _size(ValidateLocalSize(_device->size())) {
Expects(_device != nullptr);
if (!_size || !_device->open(QIODevice::ReadOnly)) {
fail();
}
}
Storage::Cache::Key LoaderLocal::baseCacheKey() const {
return {};
}
int64 LoaderLocal::size() const {
return _size;
}
void LoaderLocal::load(int64 offset) {
if (_device->pos() != offset && !_device->seek(offset)) {
fail();
return;
}
auto result = _device->read(kPartSize);
if (result.isEmpty()
|| ((result.size() != kPartSize)
&& (offset + result.size() != size()))) {
fail();
return;
}
crl::on_main(this, [=, result = std::move(result)]() mutable {
_parts.fire({ offset, std::move(result) });
});
}
void LoaderLocal::fail() {
crl::on_main(this, [=] {
_parts.fire({ LoadedPart::kFailedOffset });
});
}
void LoaderLocal::cancel(int64 offset) {
}
void LoaderLocal::resetPriorities() {
}
void LoaderLocal::setPriority(int priority) {
}
void LoaderLocal::stop() {
}
void LoaderLocal::tryRemoveFromQueue() {
}
rpl::producer<LoadedPart> LoaderLocal::parts() const {
return _parts.events();
}
rpl::producer<SpeedEstimate> LoaderLocal::speedEstimate() const {
return rpl::never<SpeedEstimate>();
}
void LoaderLocal::attachDownloader(
not_null<Storage::StreamedFileDownloader*> downloader) {
Unexpected("Downloader attached to a local streaming loader.");
}
void LoaderLocal::clearAttachedDownloader() {
Unexpected("Downloader detached from a local streaming loader.");
}
std::unique_ptr<LoaderLocal> MakeFileLoader(const QString &path) {
return std::make_unique<LoaderLocal>(std::make_unique<QFile>(path));
}
std::unique_ptr<LoaderLocal> MakeBytesLoader(const QByteArray &bytes) {
auto device = std::make_unique<QBuffer>();
auto copy = new QByteArray(bytes);
QObject::connect(device.get(), &QBuffer::destroyed, [=] {
delete copy;
});
device->setBuffer(copy);
return std::make_unique<LoaderLocal>(std::move(device));
}
} // namespace Streaming
} // namespace Media

View File

@@ -0,0 +1,55 @@
/*
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 "media/streaming/media_streaming_loader.h"
#include "mtproto/sender.h"
#include "data/data_file_origin.h"
class ApiWrap;
namespace Media {
namespace Streaming {
class LoaderLocal : public Loader, public base::has_weak_ptr {
public:
LoaderLocal(std::unique_ptr<QIODevice> device);
[[nodiscard]] Storage::Cache::Key baseCacheKey() const override;
[[nodiscard]] int64 size() const override;
void load(int64 offset) override;
void cancel(int64 offset) override;
void resetPriorities() override;
void setPriority(int priority) override;
void stop() override;
void tryRemoveFromQueue() override;
// Parts will be sent from the main thread.
[[nodiscard]] rpl::producer<LoadedPart> parts() const override;
[[nodiscard]] rpl::producer<SpeedEstimate> speedEstimate() const override;
void attachDownloader(
not_null<Storage::StreamedFileDownloader*> downloader) override;
void clearAttachedDownloader() override;
private:
void fail();
const std::unique_ptr<QIODevice> _device;
const int64 _size = 0;
rpl::event_stream<LoadedPart> _parts;
};
std::unique_ptr<LoaderLocal> MakeFileLoader(const QString &path);
std::unique_ptr<LoaderLocal> MakeBytesLoader(const QByteArray &bytes);
} // namespace Streaming
} // namespace Media

View File

@@ -0,0 +1,221 @@
/*
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_loader_mtproto.h"
#include "apiwrap.h"
#include "main/main_session.h"
#include "storage/streamed_file_downloader.h"
#include "storage/cache/storage_cache_types.h"
namespace Media {
namespace Streaming {
namespace {
constexpr auto kCheckStatsInterval = crl::time(1000);
constexpr auto kInitialStatsWait = 5 * crl::time(1000);
} // namespace
LoaderMtproto::LoaderMtproto(
not_null<Storage::DownloadManagerMtproto*> owner,
const StorageFileLocation &location,
int64 size,
Data::FileOrigin origin)
: DownloadMtprotoTask(owner, location, origin)
, _size(size)
, _api(&api().instance())
, _statsTimer([=] { checkStats(); }) {
}
Storage::Cache::Key LoaderMtproto::baseCacheKey() const {
return v::get<StorageFileLocation>(
location().data
).bigFileBaseCacheKey();
}
int64 LoaderMtproto::size() const {
return _size;
}
void LoaderMtproto::load(int64 offset) {
crl::on_main(this, [=] {
if (_downloader) {
auto bytes = _downloader->readLoadedPart(offset);
if (!bytes.isEmpty()) {
cancelForOffset(offset);
_parts.fire({ offset, std::move(bytes) });
return;
}
}
if (haveSentRequestForOffset(offset)) {
return;
} else if (_requested.add(offset)) {
addToQueueWithPriority();
}
});
}
void LoaderMtproto::addToQueueWithPriority() {
addToQueue(_priority);
}
void LoaderMtproto::stop() {
crl::on_main(this, [=] {
cancelAllRequests();
_requested.clear();
removeFromQueue();
});
}
void LoaderMtproto::tryRemoveFromQueue() {
crl::on_main(this, [=] {
if (_requested.empty() && !haveSentRequests()) {
removeFromQueue();
}
});
}
void LoaderMtproto::cancel(int64 offset) {
crl::on_main(this, [=] {
cancelForOffset(offset);
});
}
void LoaderMtproto::cancelForOffset(int64 offset) {
if (haveSentRequestForOffset(offset)) {
cancelRequestForOffset(offset);
if (!_requested.empty()) {
addToQueueWithPriority();
}
} else {
_requested.remove(offset);
}
}
void LoaderMtproto::attachDownloader(
not_null<Storage::StreamedFileDownloader*> downloader) {
_downloader = downloader;
}
void LoaderMtproto::clearAttachedDownloader() {
_downloader = nullptr;
}
void LoaderMtproto::resetPriorities() {
crl::on_main(this, [=] {
_requested.resetPriorities();
});
}
void LoaderMtproto::setPriority(int priority) {
if (_priority == priority) {
return;
}
_priority = priority;
if (haveSentRequests()) {
addToQueueWithPriority();
}
}
bool LoaderMtproto::readyToRequest() const {
return !_requested.empty();
}
int64 LoaderMtproto::takeNextRequestOffset() {
const auto offset = _requested.take();
Assert(offset.has_value());
const auto time = crl::now();
if (!_firstRequestStart) {
_firstRequestStart = time;
}
_stats.push_back({ .start = crl::now(), .offset = *offset });
Ensures(offset.has_value());
return *offset;
}
bool LoaderMtproto::feedPart(int64 offset, const QByteArray &bytes) {
const auto time = crl::now();
for (auto &entry : _stats) {
if (entry.offset == offset && entry.start < time) {
entry.end = time;
if (!_statsTimer.isActive()) {
const auto checkAt = std::max(
time + kCheckStatsInterval,
_firstRequestStart + kInitialStatsWait);
_statsTimer.callOnce(checkAt - time);
}
break;
}
}
_parts.fire({ offset, bytes });
return true;
}
void LoaderMtproto::cancelOnFail() {
_parts.fire({ LoadedPart::kFailedOffset });
}
rpl::producer<LoadedPart> LoaderMtproto::parts() const {
return _parts.events();
}
rpl::producer<SpeedEstimate> LoaderMtproto::speedEstimate() const {
return _speedEstimate.events();
}
void LoaderMtproto::checkStats() {
const auto time = crl::now();
const auto from = time - kInitialStatsWait;
{ // Erase all stats entries that are too old.
for (auto i = begin(_stats); i != end(_stats);) {
if (i->start >= from) {
break;
} else if (i->end && i->end < from) {
i = _stats.erase(i);
} else {
++i;
}
}
}
if (_stats.empty()) {
return;
}
// Count duration for which at least one request was in progress.
// This is the time we should consider for download speed.
// We don't count time when no requests were in progress.
auto durationCountedTill = _stats.front().start;
auto duration = crl::time(0);
auto received = int64(0);
for (const auto &entry : _stats) {
if (entry.start > durationCountedTill) {
durationCountedTill = entry.start;
}
const auto till = entry.end ? entry.end : time;
if (till > durationCountedTill) {
duration += (till - durationCountedTill);
durationCountedTill = till;
}
if (entry.end) {
received += Storage::kDownloadPartSize;
}
}
if (duration) {
_speedEstimate.fire({
.bytesPerSecond = int(std::clamp(
int64(received * 1000 / duration),
int64(0),
int64(64 * 1024 * 1024))),
.unreliable = (received < 3 * Storage::kDownloadPartSize),
});
}
}
} // namespace Streaming
} // namespace Media

View File

@@ -0,0 +1,81 @@
/*
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/timer.h"
#include "media/streaming/media_streaming_loader.h"
#include "mtproto/sender.h"
#include "data/data_file_origin.h"
#include "storage/download_manager_mtproto.h"
namespace Media {
namespace Streaming {
class LoaderMtproto : public Loader, public Storage::DownloadMtprotoTask {
public:
LoaderMtproto(
not_null<Storage::DownloadManagerMtproto*> owner,
const StorageFileLocation &location,
int64 size,
Data::FileOrigin origin);
[[nodiscard]] Storage::Cache::Key baseCacheKey() const override;
[[nodiscard]] int64 size() const override;
void load(int64 offset) override;
void cancel(int64 offset) override;
void resetPriorities() override;
void setPriority(int priority) override;
void stop() override;
void tryRemoveFromQueue() override;
// Parts will be sent from the main thread.
[[nodiscard]] rpl::producer<LoadedPart> parts() const override;
[[nodiscard]] rpl::producer<SpeedEstimate> speedEstimate() const override;
void attachDownloader(
not_null<Storage::StreamedFileDownloader*> downloader) override;
void clearAttachedDownloader() override;
private:
struct StatsEntry {
crl::time start = 0;
crl::time end = 0;
int64 offset = 0;
};
bool readyToRequest() const override;
int64 takeNextRequestOffset() override;
bool feedPart(int64 offset, const QByteArray &bytes) override;
void cancelOnFail() override;
void cancelForOffset(int64 offset);
void addToQueueWithPriority();
void checkStats();
const int64 _size = 0;
int _priority = 0;
MTP::Sender _api;
PriorityQueue _requested;
rpl::event_stream<LoadedPart> _parts;
rpl::event_stream<SpeedEstimate> _speedEstimate;
std::vector<StatsEntry> _stats;
crl::time _firstRequestStart = 0;
base::Timer _statsTimer;
Storage::StreamedFileDownloader *_downloader = nullptr;
};
} // namespace Streaming
} // namespace Media

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,223 @@
/*
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 "media/streaming/media_streaming_common.h"
#include "media/streaming/media_streaming_file_delegate.h"
#include "base/weak_ptr.h"
#include "base/timer.h"
namespace Media {
namespace Player {
struct TrackState;
} // namespace Player
} // namespace Media
namespace Media {
namespace Streaming {
class Reader;
class File;
class AudioTrack;
class VideoTrack;
class Instance;
class Player final : private FileDelegate {
public:
// Public interfaces is used from the main thread.
explicit Player(std::shared_ptr<Reader> reader);
// Because we remember 'this' in calls to crl::on_main.
Player(const Player &other) = delete;
Player &operator=(const Player &other) = delete;
void play(const PlaybackOptions &options);
void pause();
void resume();
void stop();
// Allow to irreversibly stop only audio track.
void stopAudio();
[[nodiscard]] bool active() const;
[[nodiscard]] bool ready() const;
[[nodiscard]] float64 speed() const;
void setSpeed(float64 speed);
void setWaitForMarkAsShown(bool wait);
[[nodiscard]] bool playing() const;
[[nodiscard]] bool buffering() const;
[[nodiscard]] bool paused() const;
[[nodiscard]] std::optional<Error> failed() const;
[[nodiscard]] bool finished() const;
[[nodiscard]] rpl::producer<Update, Error> updates() const;
[[nodiscard]] rpl::producer<bool> fullInCache() const;
[[nodiscard]] int64 fileSize() const;
[[nodiscard]] QSize videoSize() const;
[[nodiscard]] QImage frame(
const FrameRequest &request,
const Instance *instance = nullptr) const;
[[nodiscard]] FrameWithInfo frameWithInfo(
const FrameRequest &request,
const Instance *instance = nullptr) const;
[[nodiscard]] FrameWithInfo frameWithInfo(
const Instance *instance = nullptr) const; // !requireARGB32
[[nodiscard]] QImage currentFrameImage() const; // Converts if needed.
void unregisterInstance(not_null<const Instance*> instance);
bool markFrameShown();
void setLoaderPriority(int priority);
[[nodiscard]] Media::Player::TrackState prepareLegacyState() const;
void lock();
void unlock();
[[nodiscard]] bool locked() const;
[[nodiscard]] rpl::lifetime &lifetime();
~Player();
private:
enum class Stage {
Uninitialized,
Initializing,
Ready,
Started,
};
// Thread-safe.
not_null<FileDelegate*> delegate();
// FileDelegate methods are called only from the File thread.
Mode fileOpenMode() override;
bool fileReady(int headerSize, Stream &&video, Stream &&audio) override;
void fileError(Error error) override;
void fileWaitingForData() override;
void fileFullInCache(bool fullInCache) override;
bool fileProcessPackets(
base::flat_map<int, std::vector<FFmpeg::Packet>> &packets) override;
void fileProcessEndOfFile() override;
bool fileReadMore() override;
// Called from the main thread.
void streamReady(Information &&information);
void streamFailed(Error error);
void start();
void stop(bool stillActive);
void provideStartInformation();
void fail(Error error);
void checkVideoStep();
void checkNextFrameRender();
void checkNextFrameAvailability();
void renderFrame(crl::time now);
void audioReceivedTill(crl::time position);
void audioPlayedTill(crl::time position);
void videoReceivedTill(crl::time position);
void videoPlayedTill(crl::time position);
void updatePausedState();
[[nodiscard]] bool trackReceivedEnough(
const TrackState &state,
crl::time amount) const;
[[nodiscard]] bool bothReceivedEnough(crl::time amount) const;
[[nodiscard]] bool receivedTillEnd() const;
void checkResumeFromWaitingForData();
[[nodiscard]] crl::time getCurrentReceivedTill(crl::time duration) const;
void savePreviousReceivedTill(
const PlaybackOptions &options,
crl::time previousReceivedTill);
[[nodiscard]] crl::time loadInAdvanceFor() const;
template <typename Track>
int durationByPacket(const Track &track, const FFmpeg::Packet &packet);
// Valid after fileReady call ends. Thread-safe.
[[nodiscard]] crl::time computeAudioDuration() const;
[[nodiscard]] crl::time computeVideoDuration() const;
[[nodiscard]] crl::time computeTotalDuration() const;
void setDurationByPackets();
template <typename Track>
void trackReceivedTill(
const Track &track,
TrackState &state,
crl::time position);
template <typename Track>
void trackSendReceivedTill(
const Track &track,
TrackState &state);
template <typename Track>
void trackPlayedTill(
const Track &track,
TrackState &state,
crl::time position);
const std::unique_ptr<File> _file;
// Immutable while File is active after it is ready.
AudioMsgId _audioId;
std::unique_ptr<AudioTrack> _audio;
std::unique_ptr<VideoTrack> _video;
// Immutable while File is active.
base::has_weak_ptr _sessionGuard;
// Immutable while File is active except '.speed'.
// '.speed' is changed from the main thread.
PlaybackOptions _options;
// Belongs to the File thread while File is active.
bool _readTillEnd = false;
bool _waitingForData = false;
std::atomic<bool> _pauseReading = false;
// Belongs to the main thread.
Information _information;
Stage _stage = Stage::Uninitialized;
std::optional<Error> _lastFailure;
bool _pausedByUser = false;
bool _pausedByWaitingForData = false;
bool _paused = false;
bool _audioFinished = false;
bool _videoFinished = false;
bool _remoteLoader = false;
crl::time _startedTime = kTimeUnknown;
crl::time _pausedTime = kTimeUnknown;
crl::time _currentFrameTime = kTimeUnknown;
crl::time _nextFrameTime = kTimeUnknown;
base::Timer _renderFrameTimer;
rpl::event_stream<Update, Error> _updates;
rpl::event_stream<bool> _fullInCache;
std::optional<bool> _fullInCacheSinceStart;
crl::time _totalDuration = kTimeUnknown;
crl::time _loopingShift = 0;
crl::time _previousReceivedTill = kTimeUnknown;
std::atomic<int> _durationByPackets = 0;
int _durationByLastAudioPacket = 0;
int _durationByLastVideoPacket = 0;
int _locks = 0;
rpl::lifetime _lifetime;
rpl::lifetime _sessionLifetime;
};
} // namespace Streaming
} // namespace Media

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,286 @@
/*
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 "media/streaming/media_streaming_common.h"
#include "media/streaming/media_streaming_loader.h"
#include "base/bytes.h"
#include "base/weak_ptr.h"
#include "base/thread_safe_wrap.h"
namespace Storage {
class StreamedFileDownloader;
} // namespace Storage
namespace Storage {
namespace Cache {
struct Key;
class Database;
} // namespace Cache
} // namespace Storage
namespace Media {
namespace Streaming {
class Loader;
struct LoadedPart;
enum class Error;
class Reader final : public base::has_weak_ptr {
public:
enum class FillState : uchar {
Success,
WaitingCache,
WaitingRemote,
Failed,
};
// Main thread.
explicit Reader(
std::unique_ptr<Loader> loader,
Storage::Cache::Database *cache = nullptr);
void setLoaderPriority(int priority);
// Any thread.
[[nodiscard]] int64 size() const;
[[nodiscard]] bool isRemoteLoader() const;
// Single thread.
[[nodiscard]] FillState fill(
int64 offset,
bytes::span buffer,
not_null<crl::semaphore*> notify);
[[nodiscard]] std::optional<Error> streamingError() const;
void headerDone();
[[nodiscard]] int headerSize() const;
[[nodiscard]] bool fullInCache() const;
// Thread safe.
void startSleep(not_null<crl::semaphore*> wake);
void wakeFromSleep();
void stopSleep();
void stopStreamingAsync();
void tryRemoveLoaderAsync();
// Main thread.
void startStreaming();
void stopStreaming(bool stillActive = false);
[[nodiscard]] rpl::producer<LoadedPart> partsForDownloader() const;
void loadForDownloader(
not_null<Storage::StreamedFileDownloader*> downloader,
int64 offset);
void doneForDownloader(int64 offset);
void cancelForDownloader(
not_null<Storage::StreamedFileDownloader*> downloader);
void continueDownloaderFromMainThread();
[[nodiscard]] rpl::producer<SpeedEstimate> speedEstimate() const;
~Reader();
private:
static constexpr auto kLoadFromRemoteMax = 8;
struct CacheHelper;
// FileSize: Right now any file size fits 32 bit.
using PartsMap = base::flat_map<uint32, QByteArray>;
template <int Size>
class StackIntVector {
public:
bool add(uint32 value);
auto values() const;
private:
std::array<uint32, Size> _storage = { uint32(-1) };
};
struct SerializedSlice {
int number = -1;
QByteArray data;
};
struct FillResult {
static constexpr auto kReadFromCacheMax = 2;
StackIntVector<kReadFromCacheMax> sliceNumbersFromCache;
StackIntVector<kLoadFromRemoteMax> offsetsFromLoader;
SerializedSlice toCache;
FillState state = FillState::WaitingRemote;
};
struct Slice {
enum class Flag : uchar {
LoadingFromCache = 0x01,
LoadedFromCache = 0x02,
ChangedSinceCache = 0x04,
FullInCache = 0x08,
};
friend constexpr inline bool is_flag_type(Flag) { return true; }
using Flags = base::flags<Flag>;
struct PrepareFillResult {
StackIntVector<kLoadFromRemoteMax> offsetsFromLoader;
PartsMap::const_iterator start;
PartsMap::const_iterator finish;
bool ready = true;
};
void processCacheData(PartsMap &&data);
void addPart(uint32 offset, QByteArray bytes);
PrepareFillResult prepareFill(uint32 from, uint32 till);
// Get up to kLoadFromRemoteMax not loaded parts in from-till range.
StackIntVector<kLoadFromRemoteMax> offsetsFromLoader(
uint32 from,
uint32 till) const;
PartsMap parts;
Flags flags;
};
class Slices {
public:
Slices(uint32 size, bool useCache);
void headerDone(bool fromCache);
[[nodiscard]] int headerSize() const;
[[nodiscard]] bool fullInCache() const;
[[nodiscard]] bool headerWontBeFilled() const;
[[nodiscard]] bool headerModeUnknown() const;
[[nodiscard]] bool isFullInHeader() const;
[[nodiscard]] bool isGoodHeader() const;
[[nodiscard]] bool waitingForHeaderCache() const;
[[nodiscard]] int requestSliceSizesCount() const;
void processCacheResult(int sliceNumber, PartsMap &&result);
void processCachedSizes(const std::vector<int> &sizes);
void processPart(uint32 offset, QByteArray &&bytes);
[[nodiscard]] FillResult fill(uint32 offset, bytes::span buffer);
[[nodiscard]] SerializedSlice unloadToCache();
[[nodiscard]] QByteArray partForDownloader(uint32 offset) const;
[[nodiscard]] bool readCacheForDownloaderRequired(uint32 offset);
private:
enum class HeaderMode {
Unknown,
Small,
Good,
Full,
NoCache,
};
void applyHeaderCacheData();
[[nodiscard]] int maxSliceSize(int sliceNumber) const;
[[nodiscard]] SerializedSlice serializeAndUnloadSlice(
int sliceNumber);
[[nodiscard]] SerializedSlice serializeAndUnloadUnused();
[[nodiscard]] QByteArray serializeComplexSlice(
const Slice &slice) const;
[[nodiscard]] QByteArray serializeAndUnloadFirstSliceNoHeader();
void markSliceUsed(int sliceIndex);
[[nodiscard]] bool computeIsGoodHeader() const;
[[nodiscard]] FillResult fillFromHeader(
uint32 offset,
bytes::span buffer);
void unloadSlice(Slice &slice) const;
void checkSliceFullLoaded(int sliceNumber);
[[nodiscard]] bool checkFullInCache() const;
std::vector<Slice> _data;
Slice _header;
std::deque<int> _usedSlices;
uint32 _size = 0;
HeaderMode _headerMode = HeaderMode::Unknown;
bool _fullInCache = false;
};
// 0 is for headerData, slice index = sliceNumber - 1.
// returns false if asked for a known-empty downloader slice cache.
void readFromCache(int sliceNumber);
[[nodiscard]] bool readFromCacheForDownloader(int sliceNumber);
bool processCacheResults();
void putToCache(SerializedSlice &&data);
void cancelLoadInRange(uint32 from, uint32 till);
void loadAtOffset(uint32 offset);
void checkLoadWillBeFirst(uint32 offset);
bool processLoadedParts();
bool checkForSomethingMoreReceived();
FillState fillFromSlices(uint32 offset, bytes::span buffer);
void finalizeCache();
void processDownloaderRequests();
void checkCacheResultsForDownloader();
void pruneDownloaderCache(uint32 minimalOffset);
void pruneDoneDownloaderRequests();
void sendDownloaderRequests();
[[nodiscard]] bool downloaderWaitForCachedSlice(uint32 offset);
void enqueueDownloaderOffsets();
void checkForDownloaderChange(int checkItemsCount);
void checkForDownloaderReadyOffsets();
void refreshLoaderPriority();
static std::shared_ptr<CacheHelper> InitCacheHelper(
Storage::Cache::Key baseKey);
const std::unique_ptr<Loader> _loader;
Storage::Cache::Database * const _cache = nullptr;
// shared_ptr is used to be able to have weak_ptr.
const std::shared_ptr<CacheHelper> _cacheHelper;
base::thread_safe_queue<LoadedPart, std::vector> _loadedParts;
std::atomic<crl::semaphore*> _waiting = nullptr;
std::atomic<crl::semaphore*> _sleeping = nullptr;
std::atomic<bool> _stopStreamingAsync = false;
PriorityQueue _loadingOffsets;
Slices _slices;
// Even if streaming had failed, the Reader can work for the downloader.
std::optional<Error> _streamingError;
// In case streaming is active both main and streaming threads have work.
// In case only downloader is active, all work is done on main thread.
// Main thread.
Storage::StreamedFileDownloader *_attachedDownloader = nullptr;
rpl::event_stream<LoadedPart> _partsForDownloader;
int _realPriority = 1;
bool _streamingActive = false;
// Streaming thread.
std::deque<uint32> _offsetsForDownloader;
base::flat_set<uint32> _downloaderOffsetsRequested;
base::flat_map<uint32, std::optional<PartsMap>> _downloaderReadCache;
// Communication from main thread to streaming thread.
// Streaming thread to main thread communicates using crl::on_main.
base::thread_safe_queue<uint32> _downloaderOffsetRequests;
base::thread_safe_queue<uint32> _downloaderOffsetAcks;
rpl::lifetime _lifetime;
};
[[nodiscard]] QByteArray SerializeComplexPartsMap(
const base::flat_map<uint32, QByteArray> &parts);
} // namespace Streaming
} // namespace Media

View File

@@ -0,0 +1,62 @@
/*
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_round_preview.h"
namespace Media::Streaming {
RoundPreview::RoundPreview(const QByteArray &bytes, int size)
: _bytes(bytes)
, _reader(
Clip::MakeReader(_bytes, [=](Clip::Notification update) {
clipCallback(update);
}))
, _size(size) {
}
std::shared_ptr<Ui::DynamicImage> RoundPreview::clone() {
Unexpected("RoundPreview::clone.");
}
QImage RoundPreview::image(int size) {
if (!_reader || !_reader->started()) {
return QImage();
}
return _reader->current({
.frame = QSize(_size, _size),
.factor = style::DevicePixelRatio(),
.radius = ImageRoundRadius::Ellipse,
}, crl::now());
}
void RoundPreview::subscribeToUpdates(Fn<void()> callback) {
_repaint = std::move(callback);
}
void RoundPreview::clipCallback(Clip::Notification notification) {
switch (notification) {
case Clip::Notification::Reinit: {
if (_reader->state() == ::Media::Clip::State::Error) {
_reader.setBad();
} else if (_reader->ready() && !_reader->started()) {
_reader->start({
.frame = QSize(_size, _size),
.factor = style::DevicePixelRatio(),
.radius = ImageRoundRadius::Ellipse,
});
}
} break;
case Clip::Notification::Repaint: break;
}
if (const auto onstack = _repaint) {
onstack();
}
}
} // namespace Media::Streaming

View File

@@ -0,0 +1,35 @@
/*
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/dynamic_image.h"
#include "media/clip/media_clip_reader.h"
namespace Media::Streaming {
class RoundPreview final : public Ui::DynamicImage {
public:
RoundPreview(const QByteArray &bytes, int size);
std::shared_ptr<DynamicImage> clone() override;
QImage image(int size) override;
void subscribeToUpdates(Fn<void()> callback) override;
private:
void clipCallback(Clip::Notification notification);
const QByteArray _bytes;
Clip::ReaderPointer _reader;
Fn<void()> _repaint;
int _size = 0;
};
} // namespace Media::Streaming

View File

@@ -0,0 +1,448 @@
/*
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

View File

@@ -0,0 +1,98 @@
/*
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 "media/streaming/media_streaming_common.h"
#include "ffmpeg/ffmpeg_utility.h"
namespace Media {
namespace Streaming {
struct TimePoint {
crl::time trackTime = kTimeUnknown;
crl::time worldTime = kTimeUnknown;
bool valid() const {
return (trackTime != kTimeUnknown) && (worldTime != kTimeUnknown);
}
explicit operator bool() const {
return valid();
}
};
struct Stream {
int index = -1;
crl::time duration = kTimeUnknown;
AVRational timeBase = FFmpeg::kUniversalTimeBase;
FFmpeg::CodecPointer codec;
FFmpeg::FramePointer decodedFrame;
FFmpeg::FramePointer transferredFrame;
std::deque<FFmpeg::Packet> queue;
int invalidDataPackets = 0;
// Audio only.
int frequency = 0;
// Video only.
int rotation = 0;
AVRational aspect = FFmpeg::kNormalAspect;
FFmpeg::SwscalePointer swscale;
};
[[nodiscard]] crl::time FramePosition(const Stream &stream);
[[nodiscard]] FFmpeg::AvErrorWrap ProcessPacket(
Stream &stream,
FFmpeg::Packet &&packet);
[[nodiscard]] FFmpeg::AvErrorWrap ReadNextFrame(Stream &stream);
[[nodiscard]] bool GoodForRequest(
const QImage &image,
bool hasAlpha,
int rotation,
const FrameRequest &request);
[[nodiscard]] bool TransferFrame(
Stream &stream,
not_null<AVFrame*> decodedFrame,
not_null<AVFrame*> transferredFrame);
[[nodiscard]] QImage ConvertFrame(
Stream &stream,
not_null<AVFrame*> frame,
QSize resize,
QImage storage);
[[nodiscard]] FrameYUV ExtractYUV(Stream &stream, AVFrame *frame);
struct ExpandDecision {
QSize result;
bool expanding = false;
};
[[nodiscard]] ExpandDecision DecideFrameResize(
QSize outer,
QSize original,
int minVisibleNominator = 3, // If we cut out no more than 0.25 of
int minVisibleDenominator = 4); // the original, let's expand.
[[nodiscard]] bool FrameResizeMayExpand(
QSize outer,
QSize original,
int minVisibleNominator = 3,
int minVisibleDenominator = 4);
[[nodiscard]] ExpandDecision DecideVideoFrameResize(
QSize outer,
QSize original);
[[nodiscard]] QSize CalculateResizeFromOuter(QSize outer, QSize original);
[[nodiscard]] QImage PrepareBlurredBackground(QSize outer, QImage frame);
void FillBlurredBackground(QPainter &p, QSize outer, QImage bg);
[[nodiscard]] QImage PrepareByRequest(
const QImage &original,
bool hasAlpha,
const AVRational &aspect,
int rotation,
const FrameRequest &request,
QImage storage);
} // namespace Streaming
} // namespace Media

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,186 @@
/*
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 "media/streaming/media_streaming_utility.h"
#include <crl/crl_object_on_queue.h>
namespace Media {
namespace Streaming {
constexpr auto kFrameDisplayTimeAlreadyDone
= std::numeric_limits<crl::time>::max();
class VideoTrackObject;
class Instance;
class VideoTrack final {
public:
// Called from some unspecified thread.
// Callbacks are assumed to be thread-safe.
VideoTrack(
const PlaybackOptions &options,
Stream &&stream,
const AudioMsgId &audioId,
FnMut<void(const Information &)> ready,
Fn<void(Error)> error);
// Thread-safe.
[[nodiscard]] int streamIndex() const;
[[nodiscard]] AVRational streamTimeBase() const;
[[nodiscard]] crl::time streamDuration() const;
// Called from the same unspecified thread.
void process(std::vector<FFmpeg::Packet> &&packets);
void waitForData();
// Called from the main thread.
// Must be called after 'ready' was invoked.
void pause(crl::time time);
void resume(crl::time time);
// Called from the main thread.
void setSpeed(float64 speed);
void setWaitForMarkAsShown(bool wait);
// Called from the main thread.
// Returns the position of the displayed frame.
[[nodiscard]] crl::time markFrameDisplayed(crl::time now);
void addTimelineDelay(crl::time delayed);
bool markFrameShown();
[[nodiscard]] crl::time nextFrameDisplayTime() const;
[[nodiscard]] QImage frame(
const FrameRequest &request,
const Instance *instance);
[[nodiscard]] FrameWithInfo frameWithInfo(
const FrameRequest &request,
const Instance *instance);
[[nodiscard]] FrameWithInfo frameWithInfo(const Instance *instance);
[[nodiscard]] QImage currentFrameImage();
void unregisterInstance(not_null<const Instance*> instance);
[[nodiscard]] rpl::producer<> checkNextFrame() const;
[[nodiscard]] rpl::producer<> waitingForData() const;
// Called from the main thread.
~VideoTrack();
private:
friend class VideoTrackObject;
struct Prepared {
Prepared(const FrameRequest &request) : request(request) {
}
FrameRequest request = FrameRequest::NonStrict();
QImage image;
};
struct Frame {
FFmpeg::FramePointer decoded = FFmpeg::MakeFramePointer();
FFmpeg::FramePointer transferred;
QImage original;
FrameYUV yuv;
crl::time position = kTimeUnknown;
crl::time displayed = kTimeUnknown;
crl::time display = kTimeUnknown;
FrameFormat format = FrameFormat::None;
base::flat_map<const Instance*, Prepared> prepared;
int index = 0;
bool alpha = false;
};
struct FrameWithIndex {
not_null<Frame*> frame;
int index = -1;
};
class Shared {
public:
using PrepareFrame = not_null<Frame*>;
using PrepareNextCheck = crl::time;
using PrepareState = std::variant<
v::null_t,
PrepareFrame,
PrepareNextCheck>;
struct PresentFrame {
crl::time displayPosition = kTimeUnknown;
crl::time nextCheckDelay = 0;
crl::time addedWorldTimeDelay = 0;
};
// Called from the wrapped object queue.
void init(QImage &&cover, bool hasAlpha, crl::time position);
[[nodiscard]] bool initialized() const;
[[nodiscard]] PrepareState prepareState(
crl::time trackTime,
bool dropStaleFrames);
[[nodiscard]] PresentFrame presentFrame(
not_null<VideoTrackObject*> object,
TimePoint trackTime,
float64 playbackSpeed,
bool dropStaleFrames);
[[nodiscard]] bool firstPresentHappened() const;
// Called from the main thread.
// Returns the position of the displayed frame.
[[nodiscard]] crl::time markFrameDisplayed(crl::time now);
void addTimelineDelay(crl::time delayed);
bool markFrameShown();
[[nodiscard]] crl::time nextFrameDisplayTime() const;
[[nodiscard]] not_null<Frame*> frameForPaint();
[[nodiscard]] FrameWithIndex frameForPaintWithIndex();
private:
[[nodiscard]] not_null<Frame*> getFrame(int index);
[[nodiscard]] not_null<const Frame*> getFrame(int index) const;
[[nodiscard]] int counter() const;
static constexpr auto kCounterUninitialized = -1;
std::atomic<int> _counter = kCounterUninitialized;
static constexpr auto kFramesCount = 4;
std::array<Frame, kFramesCount> _frames;
// (_counter % 2) == 1 main thread can write _delay.
// (_counter % 2) == 0 crl::queue can read _delay.
crl::time _delay = kTimeUnknown;
};
static void PrepareFrameByRequests(
not_null<Frame*> frame,
const AVRational &aspect,
int rotation);
[[nodiscard]] static bool IsDecoded(not_null<const Frame*> frame);
[[nodiscard]] static bool IsRasterized(not_null<const Frame*> frame);
[[nodiscard]] static bool IsStale(
not_null<const Frame*> frame,
crl::time trackTime);
[[nodiscard]] QImage frameImage(
not_null<Frame*> frame,
const FrameRequest &request,
const Instance *instance);
const int _streamIndex = 0;
const AVRational _streamTimeBase;
const crl::time _streamDuration = 0;
const int _streamRotation = 0;
const AVRational _streamAspect = FFmpeg::kNormalAspect;
std::unique_ptr<Shared> _shared;
using Implementation = VideoTrackObject;
crl::object_on_queue<Implementation> _wrapped;
};
} // namespace Streaming
} // namespace Media