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
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:
@@ -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
|
||||
@@ -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
|
||||
200
Telegram/SourceFiles/media/streaming/media_streaming_common.h
Normal file
200
Telegram/SourceFiles/media/streaming/media_streaming_common.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
506
Telegram/SourceFiles/media/streaming/media_streaming_file.cpp
Normal file
506
Telegram/SourceFiles/media/streaming/media_streaming_file.cpp
Normal 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
|
||||
126
Telegram/SourceFiles/media/streaming/media_streaming_file.h
Normal file
126
Telegram/SourceFiles/media/streaming/media_streaming_file.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
104
Telegram/SourceFiles/media/streaming/media_streaming_instance.h
Normal file
104
Telegram/SourceFiles/media/streaming/media_streaming_instance.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
1029
Telegram/SourceFiles/media/streaming/media_streaming_player.cpp
Normal file
1029
Telegram/SourceFiles/media/streaming/media_streaming_player.cpp
Normal file
File diff suppressed because it is too large
Load Diff
223
Telegram/SourceFiles/media/streaming/media_streaming_player.h
Normal file
223
Telegram/SourceFiles/media/streaming/media_streaming_player.h
Normal 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
|
||||
1435
Telegram/SourceFiles/media/streaming/media_streaming_reader.cpp
Normal file
1435
Telegram/SourceFiles/media/streaming/media_streaming_reader.cpp
Normal file
File diff suppressed because it is too large
Load Diff
286
Telegram/SourceFiles/media/streaming/media_streaming_reader.h
Normal file
286
Telegram/SourceFiles/media/streaming/media_streaming_reader.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
448
Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp
Normal file
448
Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp
Normal 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
|
||||
@@ -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
|
||||
1346
Telegram/SourceFiles/media/streaming/media_streaming_video_track.cpp
Normal file
1346
Telegram/SourceFiles/media/streaming/media_streaming_video_track.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
Reference in New Issue
Block a user