From f1e0cd6c1dc8585db8d9653f62cd6b2e78723061 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 27 Feb 2019 15:36:19 +0400 Subject: [PATCH] Play streaming video in mediaview. --- Telegram/SourceFiles/app.cpp | 9 +- Telegram/SourceFiles/data/data_document.cpp | 323 ++----- Telegram/SourceFiles/data/data_document.h | 3 + .../admin_log/history_admin_log_inner.cpp | 6 +- .../history/history_inner_widget.cpp | 6 +- .../history/media/history_media_document.cpp | 1 - .../history/media/history_media_gif.cpp | 4 +- .../history/media/history_media_gif.h | 8 +- .../history/media/history_media_video.cpp | 16 +- .../view/history_view_context_menu.cpp | 5 +- .../info/media/info_media_list_widget.cpp | 1 + .../inline_bot_layout_internal.cpp | 1 - .../SourceFiles/media/audio/media_audio.cpp | 16 +- .../media/player/media_player_button.h | 2 +- .../media/player/media_player_cover.cpp | 16 +- .../media/player/media_player_cover.h | 10 +- .../media/player/media_player_float.cpp | 4 +- .../media/player/media_player_float.h | 8 +- .../media/player/media_player_instance.h | 2 +- .../media/player/media_player_panel.h | 2 +- .../player/media_player_round_controller.cpp | 14 +- .../player/media_player_round_controller.h | 10 +- .../player/media_player_volume_controller.h | 2 +- .../media/player/media_player_widget.cpp | 16 +- .../media/player/media_player_widget.h | 10 +- .../media/streaming/media_streaming_common.h | 1 + .../streaming/media_streaming_player.cpp | 8 +- .../media/streaming/media_streaming_player.h | 3 +- .../streaming/media_streaming_utility.cpp | 61 +- .../media/streaming/media_streaming_utility.h | 10 +- .../streaming/media_streaming_video_track.cpp | 137 +-- .../streaming/media_streaming_video_track.h | 26 +- .../media/view/media_view_group_thumbs.cpp | 2 +- .../media/view/media_view_overlay_widget.cpp | 789 ++++++++++-------- .../media/view/media_view_overlay_widget.h | 84 +- ...r.cpp => media_view_playback_controls.cpp} | 113 +-- ...oller.h => media_view_playback_controls.h} | 41 +- ...k.cpp => media_view_playback_progress.cpp} | 20 +- ...yback.h => media_view_playback_progress.h} | 8 +- .../SourceFiles/overview/overview_layout.cpp | 2 - Telegram/SourceFiles/rpl/variable.h | 8 +- Telegram/SourceFiles/settings.h | 4 + Telegram/gyp/telegram_sources.txt | 8 +- 43 files changed, 920 insertions(+), 900 deletions(-) rename Telegram/SourceFiles/media/view/{media_clip_controller.cpp => media_view_playback_controls.cpp} (67%) rename Telegram/SourceFiles/media/view/{media_clip_controller.h => media_view_playback_controls.h} (68%) rename Telegram/SourceFiles/media/view/{media_clip_playback.cpp => media_view_playback_progress.cpp} (80%) rename Telegram/SourceFiles/media/view/{media_clip_playback.h => media_view_playback_progress.h} (93%) diff --git a/Telegram/SourceFiles/app.cpp b/Telegram/SourceFiles/app.cpp index 67191343e..b7c68db7e 100644 --- a/Telegram/SourceFiles/app.cpp +++ b/Telegram/SourceFiles/app.cpp @@ -422,14 +422,11 @@ namespace App { HistoryItem *histItemById(ChannelId channelId, MsgId itemId) { if (!itemId) return nullptr; - auto data = fetchMsgsData(channelId, false); + const auto data = fetchMsgsData(channelId, false); if (!data) return nullptr; - auto i = data->constFind(itemId); - if (i != data->cend()) { - return i.value(); - } - return nullptr; + const auto i = data->constFind(itemId); + return (i != data->cend()) ? i.value() : nullptr; } HistoryItem *histItemById(const ChannelData *channel, MsgId itemId) { diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp index 373155f89..f93c6b3f0 100644 --- a/Telegram/SourceFiles/data/data_document.cpp +++ b/Telegram/SourceFiles/data/data_document.cpp @@ -154,7 +154,7 @@ QString FileNameUnsafe( if (QRegularExpression(qsl("^[a-zA-Z_0-9]+$")).match(ext).hasMatch()) { QStringList filters = filter.split(sep); if (filters.size() > 1) { - QString first = filters.at(0); + const auto &first = filters.at(0); int32 start = first.indexOf(qsl("(*.")); if (start >= 0) { if (!QRegularExpression(qsl("\\(\\*\\.") + ext + qsl("[\\)\\s]"), QRegularExpression::CaseInsensitiveOption).match(first).hasMatch()) { @@ -287,202 +287,6 @@ QString documentSaveFilename(const DocumentData *data, bool forceSavingAs = fals return FileNameForSave(caption, filter, prefix, name, forceSavingAs, dir); } -void StartStreaming( - not_null document, - Data::FileOrigin origin) { - AssertIsDebug(); - - using namespace Media::Streaming; - if (auto loader = document->createStreamingLoader(origin)) { - static auto player = std::unique_ptr(); - static auto pauseOnSeek = false; - static auto position = crl::time(0); - static auto preloadedAudio = crl::time(0); - static auto preloadedVideo = crl::time(0); - static auto duration = crl::time(0); - static auto options = PlaybackOptions(); - static auto speed = 1.; - static auto step = pow(2., 1. / 12); - static auto frame = QImage(); - - class Panel -#if defined Q_OS_MAC && !defined OS_MAC_OLD - : public Ui::RpWidgetWrap { - using Parent = Ui::RpWidgetWrap; -#else // Q_OS_MAC && !OS_MAC_OLD - : public Ui::RpWidget { - using Parent = Ui::RpWidget; -#endif // Q_OS_MAC && !OS_MAC_OLD - - public: - Panel() : Parent(nullptr) { - } - - protected: - void paintEvent(QPaintEvent *e) override { - } - void keyPressEvent(QKeyEvent *e) override { - if (e->key() == Qt::Key_Space) { - if (player->paused()) { - player->resume(); - } else { - player->pause(); - } - } else if (e->key() == Qt::Key_Plus) { - speed = std::min(speed * step, 2.); - player->setSpeed(speed); - } else if (e->key() == Qt::Key_Minus) { - speed = std::max(speed / step, 0.5); - player->setSpeed(speed); - } - } - void mousePressEvent(QMouseEvent *e) override { - pauseOnSeek = player->paused(); - player->pause(); - } - void mouseReleaseEvent(QMouseEvent *e) override { - if (player->ready()) { - frame = player->frame({}); - } - preloadedAudio - = preloadedVideo - = position - = options.position - = std::clamp( - (duration * e->pos().x()) / width(), - crl::time(0), - crl::time(duration)); - player->play(options); - } - - }; - - static auto video = base::unique_qptr(); - - player = std::make_unique( - &document->owner(), - std::move(loader)); - base::take(video) = nullptr; - player->lifetime().add([] { - base::take(video) = nullptr; - }); - document->session().lifetime().add([] { - base::take(player) = nullptr; - }); - - options.speed = speed; - //options.syncVideoByAudio = false; - preloadedAudio = preloadedVideo = position = options.position = 0; - frame = QImage(); - player->play(options); - player->updates( - ) | rpl::start_with_next_error_done([=](Update &&update) { - update.data.match([&](Information &update) { - duration = std::max( - update.video.state.duration, - update.audio.state.duration); - if (video) { - if (update.video.cover.isNull()) { - base::take(video) = nullptr; - } else { - video->update(); - } - } else if (!update.video.cover.isNull()) { - video = base::make_unique_q(); - video->setAttribute(Qt::WA_OpaquePaintEvent); - video->paintRequest( - ) | rpl::start_with_next([=](QRect rect) { - const auto till1 = duration - ? (position * video->width() / duration) - : 0; - const auto till2 = duration - ? (std::min(preloadedAudio, preloadedVideo) - * video->width() - / duration) - : 0; - if (player->ready()) { - Painter(video.get()).drawImage( - video->rect(), - player->frame({})); - } else if (!frame.isNull()) { - Painter(video.get()).drawImage( - video->rect(), - frame); - } else { - Painter(video.get()).fillRect( - rect, - Qt::black); - } - Painter(video.get()).fillRect( - 0, - 0, - till1, - video->height(), - QColor(255, 255, 255, 64)); - if (till2 > till1) { - Painter(video.get()).fillRect( - till1, - 0, - till2 - till1, - video->height(), - QColor(255, 255, 255, 32)); - } - }, video->lifetime()); - const auto size = QSize( - ConvertScale(update.video.size.width()), - ConvertScale(update.video.size.height())); - const auto center = App::wnd()->geometry().center(); - video->setGeometry(QRect( - center - QPoint(size.width(), size.height()) / 2, - size)); - video->show(); - video->shownValue( - ) | rpl::start_with_next([=](bool shown) { - if (!shown) { - base::take(player) = nullptr; - } - }, video->lifetime()); - } - }, [&](PreloadedVideo &update) { - if (preloadedVideo < update.till) { - if (preloadedVideo < preloadedAudio) { - video->update(); - } - preloadedVideo = update.till; - } - }, [&](UpdateVideo &update) { - Expects(video != nullptr); - - if (position < update.position) { - position = update.position; - } - video->update(); - }, [&](PreloadedAudio &update) { - if (preloadedAudio < update.till) { - if (video && preloadedAudio < preloadedVideo) { - video->update(); - } - preloadedAudio = update.till; - } - }, [&](UpdateAudio &update) { - if (position < update.position) { - position = update.position; - if (video) { - video->update(); - } - } - }, [&](WaitingForData) { - }, [&](MutedByOther) { - }, [&](Finished) { - base::take(player) = nullptr; - }); - }, [=](const Error &error) { - base::take(video) = nullptr; - }, [=] { - }, player->lifetime()); - } -} - void DocumentOpenClickHandler::Open( Data::FileOrigin origin, not_null data, @@ -503,8 +307,8 @@ void DocumentOpenClickHandler::Open( return; } } - if (data->isAudioFile() || data->isVideoFile()) { - StartStreaming(data, origin); + if (data->canBePlayed()) { + Core::App().showDocument(data, context); return; } if (!location.isEmpty() || (!data->data().isEmpty() && (playVoice || playMusic || playVideo || playAnimation))) { @@ -536,25 +340,6 @@ void DocumentOpenClickHandler::Open( Media::Player::mixer()->play(song); Media::Player::Updated().notify(song); } - } else if (playVideo) { - if (!data->data().isEmpty()) { - Core::App().showDocument(data, context); - } else if (location.accessEnable()) { - Core::App().showDocument(data, context); - location.accessDisable(); - } else { - const auto filepath = location.name(); - if (Data::IsValidMediaFile(filepath)) { - File::Launch(filepath); - } - } - data->owner().markMediaRead(data); - } else if (data->isVoiceMessage() || data->isAudioFile() || data->isVideoFile()) { - const auto filepath = location.name(); - if (Data::IsValidMediaFile(filepath)) { - File::Launch(filepath); - } - data->owner().markMediaRead(data); } else if (data->size < App::kImageSizeLimit) { if (!data->data().isEmpty() && playAnimation) { if (action == ActionOnLoadPlayInline && context) { @@ -610,14 +395,10 @@ void GifOpenClickHandler::onClickImpl() const { void DocumentSaveClickHandler::Save( Data::FileOrigin origin, not_null data, + HistoryItem *context, bool forceSavingAs) { if (!data->date) return; - if (data->isAudioFile() || data->isVideoFile()) { - StartStreaming(data, origin); - return; - } - auto filepath = data->filepath( DocumentData::FilePathResolveSaveFromDataSilent, forceSavingAs); @@ -635,7 +416,7 @@ void DocumentSaveClickHandler::Save( } void DocumentSaveClickHandler::onClickImpl() const { - Save(context(), document()); + Save(context(), document(), getActionItem()); } void DocumentCancelClickHandler::onClickImpl() const { @@ -683,51 +464,44 @@ AuthSession &DocumentData::session() const { return _owner->session(); } -void DocumentData::setattributes(const QVector &attributes) { +void DocumentData::setattributes( + const QVector &attributes) { _isImage = false; _supportsStreaming = false; - for (int32 i = 0, l = attributes.size(); i < l; ++i) { - switch (attributes[i].type()) { - case mtpc_documentAttributeImageSize: { - auto &d = attributes[i].c_documentAttributeImageSize(); - dimensions = QSize(d.vw.v, d.vh.v); - } break; - case mtpc_documentAttributeAnimated: + for (const auto &attribute : attributes) { + attribute.match([&](const MTPDdocumentAttributeImageSize & data) { + dimensions = QSize(data.vw.v, data.vh.v); + }, [&](const MTPDdocumentAttributeAnimated & data) { if (type == FileDocument || type == StickerDocument || type == VideoDocument) { type = AnimatedDocument; _additional = nullptr; - } break; - case mtpc_documentAttributeSticker: { - auto &d = attributes[i].c_documentAttributeSticker(); + } + }, [&](const MTPDdocumentAttributeSticker & data) { if (type == FileDocument) { type = StickerDocument; _additional = std::make_unique(); } if (sticker()) { - sticker()->alt = qs(d.valt); + sticker()->alt = qs(data.valt); if (sticker()->set.type() != mtpc_inputStickerSetID - || d.vstickerset.type() == mtpc_inputStickerSetID) { - sticker()->set = d.vstickerset; + || data.vstickerset.type() == mtpc_inputStickerSetID) { + sticker()->set = data.vstickerset; } } - } break; - case mtpc_documentAttributeVideo: { - auto &d = attributes[i].c_documentAttributeVideo(); + }, [&](const MTPDdocumentAttributeVideo & data) { if (type == FileDocument) { - type = d.is_round_message() + type = data.is_round_message() ? RoundVideoDocument : VideoDocument; } - _duration = d.vduration.v; - _supportsStreaming = d.is_supports_streaming(); - dimensions = QSize(d.vw.v, d.vh.v); - } break; - case mtpc_documentAttributeAudio: { - auto &d = attributes[i].c_documentAttributeAudio(); + _duration = data.vduration.v; + _supportsStreaming = data.is_supports_streaming(); + dimensions = QSize(data.vw.v, data.vh.v); + }, [&](const MTPDdocumentAttributeAudio & data) { if (type == FileDocument) { - if (d.is_voice()) { + if (data.is_voice()) { type = VoiceDocument; _additional = std::make_unique(); } else { @@ -736,25 +510,19 @@ void DocumentData::setattributes(const QVector &attributes } } if (const auto voiceData = voice()) { - voiceData->duration = d.vduration.v; - VoiceWaveform waveform = documentWaveformDecode(qba(d.vwaveform)); - uchar wavemax = 0; - for (int32 i = 0, l = waveform.size(); i < l; ++i) { - uchar waveat = waveform.at(i); - if (wavemax < waveat) wavemax = waveat; - } - voiceData->waveform = waveform; - voiceData->wavemax = wavemax; + voiceData->duration = data.vduration.v; + voiceData->waveform = documentWaveformDecode( + qba(data.vwaveform)); + voiceData->wavemax = voiceData->waveform.empty() + ? uchar(0) + : *ranges::max_element(voiceData->waveform); } else if (const auto songData = song()) { - songData->duration = d.vduration.v; - songData->title = qs(d.vtitle); - songData->performer = qs(d.vperformer); + songData->duration = data.vduration.v; + songData->title = qs(data.vtitle); + songData->performer = qs(data.vperformer); } - } break; - case mtpc_documentAttributeFilename: { - const auto &attribute = attributes[i]; - _filename = qs( - attribute.c_documentAttributeFilename().vfile_name); + }, [&](const MTPDdocumentAttributeFilename & data) { + _filename = qs(data.vfile_name); // We don't want LTR/RTL mark/embedding/override/isolate chars // in filenames, because they introduce a security issue, when @@ -772,8 +540,8 @@ void DocumentData::setattributes(const QVector &attributes for (const auto ch : controls) { _filename = std::move(_filename).replace(ch, "_"); } - } break; - } + }, [&](const MTPDdocumentAttributeHasStickers &data) { + }); } if (type == StickerDocument) { if (dimensions.width() <= 0 @@ -1495,8 +1263,27 @@ bool DocumentData::hasRemoteLocation() const { return (_dc != 0 && _access != 0); } +bool DocumentData::canBeStreamed() const { + return hasRemoteLocation() + && (isAudioFile() + || ((isAnimation() || isVideoFile()) && supportsStreaming())); +} + +bool DocumentData::canBePlayed() const { + return (isAnimation() || isVideoFile() || isAudioFile()) + && (loaded() || canBeStreamed()); +} + auto DocumentData::createStreamingLoader(Data::FileOrigin origin) const -> std::unique_ptr { + // #TODO streaming create local file loader + //auto &location = this->location(true); + //if (!_doc->data().isEmpty()) { + // initStreaming(); + //} else if (location.accessEnable()) { + // initStreaming(); + // location.accessDisable(); + //} return hasRemoteLocation() ? std::make_unique( &session().api(), diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h index 215fa934d..e04ebe5d7 100644 --- a/Telegram/SourceFiles/data/data_document.h +++ b/Telegram/SourceFiles/data/data_document.h @@ -223,6 +223,8 @@ public: const QString &songPerformer); [[nodiscard]] QString composeNameString() const; + [[nodiscard]] bool canBePlayed() const; + [[nodiscard]] bool canBeStreamed() const; [[nodiscard]] auto createStreamingLoader(Data::FileOrigin origin) const -> std::unique_ptr; @@ -303,6 +305,7 @@ public: static void Save( Data::FileOrigin origin, not_null document, + HistoryItem *context, bool forceSavingAs = false); protected: diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp index 8e62fde75..c6f99ef25 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -1092,7 +1092,11 @@ void InnerWidget::savePhotoToFile(PhotoData *photo) { } void InnerWidget::saveDocumentToFile(DocumentData *document) { - DocumentSaveClickHandler::Save(Data::FileOrigin(), document, true); + DocumentSaveClickHandler::Save( + Data::FileOrigin(), + document, + nullptr, + true); } void InnerWidget::copyContextImage(PhotoData *photo) { diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index b14bc5ea1..9e23d944a 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -1823,7 +1823,11 @@ void HistoryInner::showContextInFolder(not_null document) { void HistoryInner::saveDocumentToFile( FullMsgId contextId, not_null document) { - DocumentSaveClickHandler::Save(contextId, document, true); + DocumentSaveClickHandler::Save( + contextId, + document, + App::histItemById(contextId), + true); } void HistoryInner::openContextGif(FullMsgId itemId) { diff --git a/Telegram/SourceFiles/history/media/history_media_document.cpp b/Telegram/SourceFiles/history/media/history_media_document.cpp index 68d0c5a5e..af318505e 100644 --- a/Telegram/SourceFiles/history/media/history_media_document.cpp +++ b/Telegram/SourceFiles/history/media/history_media_document.cpp @@ -621,7 +621,6 @@ bool HistoryDocument::updateStatusText() const { } else if (_data->loading()) { statusSize = _data->loadOffset(); } else if (_data->loaded()) { - using State = Media::Player::State; statusSize = FileStatusSizeLoaded; if (_data->isVoiceMessage()) { auto state = Media::Player::mixer()->currentState(AudioMsgId::Type::Voice); diff --git a/Telegram/SourceFiles/history/media/history_media_gif.cpp b/Telegram/SourceFiles/history/media/history_media_gif.cpp index 0ca34ffbb..3e4612c18 100644 --- a/Telegram/SourceFiles/history/media/history_media_gif.cpp +++ b/Telegram/SourceFiles/history/media/history_media_gif.cpp @@ -13,7 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/audio/media_audio.h" #include "media/clip/media_clip_reader.h" #include "media/player/media_player_round_controller.h" -#include "media/view/media_clip_playback.h" +#include "media/view/media_view_playback_progress.h" #include "boxes/confirm_box.h" #include "history/history_item_components.h" #include "history/history_item.h" @@ -833,7 +833,7 @@ Media::Clip::Reader *HistoryGif::currentReader() const { return (_gif && _gif->ready()) ? _gif.get() : nullptr; } -Media::Clip::Playback *HistoryGif::videoPlayback() const { +Media::View::PlaybackProgress *HistoryGif::videoPlayback() const { if (const auto video = activeRoundVideo()) { return video->playback(); } diff --git a/Telegram/SourceFiles/history/media/history_media_gif.h b/Telegram/SourceFiles/history/media/history_media_gif.h index 621e91c4d..891ab8c0e 100644 --- a/Telegram/SourceFiles/history/media/history_media_gif.h +++ b/Telegram/SourceFiles/history/media/history_media_gif.h @@ -14,9 +14,9 @@ struct HistoryMessageReply; struct HistoryMessageForwarded; namespace Media { -namespace Clip { -class Playback; -} // namespace Clip +namespace View { +class PlaybackProgress; +} // namespace View namespace Player { class RoundController; @@ -91,7 +91,7 @@ private: Media::Player::RoundController *activeRoundVideo() const; Media::Clip::Reader *activeRoundPlayer() const; Media::Clip::Reader *currentReader() const; - Media::Clip::Playback *videoPlayback() const; + Media::View::PlaybackProgress *videoPlayback() const; void clipCallback(Media::Clip::Notification notification); bool needInfoDisplay() const; diff --git a/Telegram/SourceFiles/history/media/history_media_video.cpp b/Telegram/SourceFiles/history/media/history_media_video.cpp index 1e5a5fd38..af5a82f54 100644 --- a/Telegram/SourceFiles/history/media/history_media_video.cpp +++ b/Telegram/SourceFiles/history/media/history_media_video.cpp @@ -226,8 +226,9 @@ void HistoryVideo::draw(Painter &p, const QRect &r, TextSelection selection, crl p.setOpacity(1); } - auto icon = ([this, radial, selected, loaded]() -> const style::icon * { - if (loaded && !radial) { + const auto canPlay = _data->canBePlayed(); + auto icon = [&]() -> const style::icon * { + if (canPlay && !radial) { return &(selected ? st::historyFileThumbPlaySelected : st::historyFileThumbPlay); } else if (radial || _data->loading()) { if (_parent->data()->id > 0 || _data->uploading()) { @@ -236,7 +237,7 @@ void HistoryVideo::draw(Painter &p, const QRect &r, TextSelection selection, crl return nullptr; } return &(selected ? st::historyFileThumbDownloadSelected : st::historyFileThumbDownload); - })(); + }(); if (icon) { icon->paintInCenter(p, inner); } @@ -275,7 +276,7 @@ TextState HistoryVideo::textState(QPoint point, StateRequest request) const { } auto result = TextState(_parent); - bool loaded = _data->loaded(); + const auto canPlay = _data->canBePlayed(); auto paintx = 0, painty = 0, paintw = width(), painth = height(); bool bubble = _parent->hasBubble(); @@ -300,7 +301,7 @@ TextState HistoryVideo::textState(QPoint point, StateRequest request) const { if (_data->uploading()) { result.link = _cancell; } else { - result.link = loaded ? _openl : (_data->loading() ? _cancell : _savel); + result.link = canPlay ? _openl : (_data->loading() ? _cancell : _savel); } } if (_caption.isEmpty() && _parent->media() == this) { @@ -389,10 +390,11 @@ void HistoryVideo::drawGrouped( p.drawEllipse(inner); } + const auto canPlay = _data->canBePlayed(); auto icon = [&]() -> const style::icon * { if (_data->waitingForAlbum()) { return &(selected ? st::historyFileThumbWaitingSelected : st::historyFileThumbWaiting); - } else if (loaded && !radial) { + } else if (canPlay && !radial) { return &(selected ? st::historyFileThumbPlaySelected : st::historyFileThumbPlay); } else if (radial || _data->loading()) { if (_parent->data()->id > 0 || _data->uploading()) { @@ -436,7 +438,7 @@ TextState HistoryVideo::getStateGrouped( } return TextState(_parent, _data->uploading() ? _cancell - : _data->loaded() + : _data->canBePlayed() ? _openl : _data->loading() ? _cancell diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index d09d1dcc1..4c17541d2 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -122,6 +122,9 @@ void AddSaveDocumentAction( not_null menu, Data::FileOrigin origin, not_null document) { + const auto save = [=] { + DocumentSaveClickHandler::Save(origin, document, nullptr, true); + }; menu->addAction( lang(document->isVideoFile() ? lng_context_save_video @@ -135,7 +138,7 @@ void AddSaveDocumentAction( App::LambdaDelayed( st::defaultDropdownMenu.menu.ripple.hideDuration, &Auth(), - [=] { DocumentSaveClickHandler::Save(origin, document, true); })); + save)); } void AddDocumentActions( diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp index 61b7d762a..d80c8e634 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp @@ -1277,6 +1277,7 @@ void ListWidget::showContextMenu( DocumentSaveClickHandler::Save( itemFullId, document, + App::histItemById(itemFullId), true); }); _contextMenu->addAction( diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp index f881e26c1..3983afb8c 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp @@ -894,7 +894,6 @@ bool File::updateStatusText() const { } else if (_document->loading()) { statusSize = _document->loadOffset(); } else if (_document->loaded()) { - using State = Media::Player::State; if (_document->isVoiceMessage()) { statusSize = FileStatusSizeLoaded; auto state = Media::Player::mixer()->currentState(AudioMsgId::Type::Voice); diff --git a/Telegram/SourceFiles/media/audio/media_audio.cpp b/Telegram/SourceFiles/media/audio/media_audio.cpp index 0683a5995..cd9d6fc9a 100644 --- a/Telegram/SourceFiles/media/audio/media_audio.cpp +++ b/Telegram/SourceFiles/media/audio/media_audio.cpp @@ -729,10 +729,10 @@ void Mixer::resetFadeStartPosition(AudioMsgId::Type type, int positionInBuffered return; } - const auto stoppedAtEnd = (alState == AL_STOPPED) - && (!IsStopped(track->state.state) - || IsStoppedAtEnd(track->state.state)) - || track->state.waitingForData; + const auto stoppedAtEnd = track->state.waitingForData + || ((alState == AL_STOPPED) + && (!IsStopped(track->state.state) + || IsStoppedAtEnd(track->state.state))); positionInBuffered = stoppedAtEnd ? track->bufferedLength : alSampleOffset; @@ -1438,10 +1438,10 @@ int32 Fader::updateOnePlayback(Mixer::Track *track, bool &hasPlaying, bool &hasF } int32 emitSignals = 0; - const auto stoppedAtEnd = (alState == AL_STOPPED) - && (!IsStopped(track->state.state) - || IsStoppedAtEnd(track->state.state)) - || track->state.waitingForData; + const auto stoppedAtEnd = track->state.waitingForData + || ((alState == AL_STOPPED) + && (!IsStopped(track->state.state) + || IsStoppedAtEnd(track->state.state))); const auto positionInBuffered = stoppedAtEnd ? track->bufferedLength : alSampleOffset; diff --git a/Telegram/SourceFiles/media/player/media_player_button.h b/Telegram/SourceFiles/media/player/media_player_button.h index 7c33af2d7..6ad124e8c 100644 --- a/Telegram/SourceFiles/media/player/media_player_button.h +++ b/Telegram/SourceFiles/media/player/media_player_button.h @@ -47,5 +47,5 @@ private: }; -} // namespace Clip +} // namespace Player } // namespace Media diff --git a/Telegram/SourceFiles/media/player/media_player_cover.cpp b/Telegram/SourceFiles/media/player/media_player_cover.cpp index f84156aee..cdd3e6679 100644 --- a/Telegram/SourceFiles/media/player/media_player_cover.cpp +++ b/Telegram/SourceFiles/media/player/media_player_cover.cpp @@ -12,7 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/continuous_sliders.h" #include "ui/widgets/buttons.h" #include "media/audio/media_audio.h" -#include "media/view/media_clip_playback.h" +#include "media/view/media_view_playback_progress.h" #include "media/player/media_player_button.h" #include "media/player/media_player_instance.h" #include "media/player/media_player_volume_controller.h" @@ -62,7 +62,7 @@ CoverWidget::CoverWidget(QWidget *parent) : RpWidget(parent) , _timeLabel(this, st::mediaPlayerTime) , _close(this, st::mediaPlayerPanelClose) , _playbackSlider(this, st::mediaPlayerPanelPlayback) -, _playback(std::make_unique()) +, _playbackProgress(std::make_unique()) , _playPause(this) , _volumeToggle(this, st::mediaPlayerVolumeToggle) , _volumeController(this) @@ -76,18 +76,18 @@ CoverWidget::CoverWidget(QWidget *parent) : RpWidget(parent) _timeLabel->setAttribute(Qt::WA_TransparentForMouseEvents); setMouseTracking(true); - _playback->setInLoadingStateChangedCallback([=](bool loading) { + _playbackProgress->setInLoadingStateChangedCallback([=](bool loading) { _playbackSlider->setDisabled(loading); }); - _playback->setValueChangedCallback([=](float64 value) { + _playbackProgress->setValueChangedCallback([=](float64 value) { _playbackSlider->setValue(value); }); _playbackSlider->setChangeProgressCallback([=](float64 value) { - _playback->setValue(value, false); + _playbackProgress->setValue(value, false); handleSeekProgress(value); }); _playbackSlider->setChangeFinishedCallback([=](float64 value) { - _playback->setValue(value, false); + _playbackProgress->setValue(value, false); handleSeekFinished(value); }); _playPause->setClickedCallback([=] { @@ -240,9 +240,9 @@ void CoverWidget::handleSongUpdate(const TrackState &state) { } if (state.id.audio()->loading()) { - _playback->updateLoadingState(state.id.audio()->progress()); + _playbackProgress->updateLoadingState(state.id.audio()->progress()); } else { - _playback->updateState(state); + _playbackProgress->updateState(state); } auto stopped = IsStoppedOrStopping(state.state); diff --git a/Telegram/SourceFiles/media/player/media_player_cover.h b/Telegram/SourceFiles/media/player/media_player_cover.h index 57ba4e5c6..6cfded681 100644 --- a/Telegram/SourceFiles/media/player/media_player_cover.h +++ b/Telegram/SourceFiles/media/player/media_player_cover.h @@ -19,9 +19,9 @@ class MediaSlider; } // namespace Ui namespace Media { -namespace Clip { -class Playback; -} // namespace Clip +namespace View { +class PlaybackProgress; +} // namespace View namespace Player { @@ -71,7 +71,7 @@ private: object_ptr _timeLabel; object_ptr _close; object_ptr _playbackSlider; - std::unique_ptr _playback; + std::unique_ptr _playbackProgress; object_ptr _previousTrack = { nullptr }; object_ptr _playPause; object_ptr _nextTrack = { nullptr }; @@ -82,5 +82,5 @@ private: }; -} // namespace Clip +} // namespace Player } // namespace Media diff --git a/Telegram/SourceFiles/media/player/media_player_float.cpp b/Telegram/SourceFiles/media/player/media_player_float.cpp index e254f478b..8b0d57bec 100644 --- a/Telegram/SourceFiles/media/player/media_player_float.cpp +++ b/Telegram/SourceFiles/media/player/media_player_float.cpp @@ -16,7 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_element.h" #include "media/audio/media_audio.h" #include "media/clip/media_clip_reader.h" -#include "media/view/media_clip_playback.h" +#include "media/view/media_view_playback_progress.h" #include "media/player/media_player_instance.h" #include "media/player/media_player_round_controller.h" #include "window/window_controller.h" @@ -210,7 +210,7 @@ Clip::Reader *Float::getReader() const { return nullptr; } -Clip::Playback *Float::getPlayback() const { +View::PlaybackProgress *Float::getPlayback() const { if (detached()) { return nullptr; } diff --git a/Telegram/SourceFiles/media/player/media_player_float.h b/Telegram/SourceFiles/media/player/media_player_float.h index f845e9685..8637bd510 100644 --- a/Telegram/SourceFiles/media/player/media_player_float.h +++ b/Telegram/SourceFiles/media/player/media_player_float.h @@ -16,9 +16,9 @@ enum class Column; } // namespace Window namespace Media { -namespace Clip { -class Playback; -} // namespace Clip +namespace View { +class PlaybackProgress; +} // namespace View namespace Player { @@ -70,7 +70,7 @@ protected: private: float64 outRatio() const; Clip::Reader *getReader() const; - Clip::Playback *getPlayback() const; + View::PlaybackProgress *getPlayback() const; void repaintItem(); void prepareShadow(); bool hasFrame() const; diff --git a/Telegram/SourceFiles/media/player/media_player_instance.h b/Telegram/SourceFiles/media/player/media_player_instance.h index e20197833..55150f210 100644 --- a/Telegram/SourceFiles/media/player/media_player_instance.h +++ b/Telegram/SourceFiles/media/player/media_player_instance.h @@ -212,5 +212,5 @@ private: }; -} // namespace Clip +} // namespace Player } // namespace Media diff --git a/Telegram/SourceFiles/media/player/media_player_panel.h b/Telegram/SourceFiles/media/player/media_player_panel.h index 7dc616283..9480da90e 100644 --- a/Telegram/SourceFiles/media/player/media_player_panel.h +++ b/Telegram/SourceFiles/media/player/media_player_panel.h @@ -118,5 +118,5 @@ private: }; -} // namespace Clip +} // namespace Player } // namespace Media diff --git a/Telegram/SourceFiles/media/player/media_player_round_controller.cpp b/Telegram/SourceFiles/media/player/media_player_round_controller.cpp index 14447732f..8549c8edd 100644 --- a/Telegram/SourceFiles/media/player/media_player_round_controller.cpp +++ b/Telegram/SourceFiles/media/player/media_player_round_controller.cpp @@ -10,7 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/audio/media_audio.h" #include "media/clip/media_clip_reader.h" #include "media/player/media_player_instance.h" -#include "media/view/media_clip_playback.h" +#include "media/view/media_view_playback_progress.h" #include "history/history_item.h" #include "window/window_controller.h" #include "data/data_media_types.h" @@ -56,8 +56,8 @@ RoundController::RoundController( _context->fullId(), [=](Clip::Notification notification) { callback(notification); }, Clip::Reader::Mode::Video); - _playback = std::make_unique(); - _playback->setValueChangedCallback([=](float64 value) { + _playbackProgress = std::make_unique(); + _playbackProgress->setValueChangedCallback([=](float64 value) { Auth().data().requestItemRepaint(_context); }); Auth().data().markMediaRead(_data); @@ -95,8 +95,8 @@ Clip::Reader *RoundController::reader() const { return _reader ? _reader.get() : nullptr; } -Clip::Playback *RoundController::playback() const { - return _playback.get(); +View::PlaybackProgress *RoundController::playback() const { + return _playbackProgress.get(); } void RoundController::handleAudioUpdate(const TrackState &state) { @@ -112,8 +112,8 @@ void RoundController::handleAudioUpdate(const TrackState &state) { } else if (another) { return; } - if (_playback) { - _playback->updateState(state); + if (_playbackProgress) { + _playbackProgress->updateState(state); } if (IsPaused(state.state) || state.state == State::Pausing) { if (!_reader->videoPaused()) { diff --git a/Telegram/SourceFiles/media/player/media_player_round_controller.h b/Telegram/SourceFiles/media/player/media_player_round_controller.h index 9328f43ae..3a0ebd860 100644 --- a/Telegram/SourceFiles/media/player/media_player_round_controller.h +++ b/Telegram/SourceFiles/media/player/media_player_round_controller.h @@ -15,9 +15,9 @@ class Controller; } // namespace Window namespace Media { -namespace Clip { -class Playback; -} // namespace Clip +namespace View { +class PlaybackProgress; +} // namespace View } // namespace Media namespace Media { @@ -39,7 +39,7 @@ public: FullMsgId contextId() const; void pauseResume(); Clip::Reader *reader() const; - Clip::Playback *playback() const; + View::PlaybackProgress *playback() const; rpl::lifetime &lifetime(); @@ -59,7 +59,7 @@ private: not_null _data; not_null _context; Clip::ReaderPointer _reader; - std::unique_ptr _playback; + std::unique_ptr _playbackProgress; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/media/player/media_player_volume_controller.h b/Telegram/SourceFiles/media/player/media_player_volume_controller.h index c0b4b5e1d..f5e54285a 100644 --- a/Telegram/SourceFiles/media/player/media_player_volume_controller.h +++ b/Telegram/SourceFiles/media/player/media_player_volume_controller.h @@ -74,5 +74,5 @@ private: }; -} // namespace Clip +} // namespace Player } // namespace Media diff --git a/Telegram/SourceFiles/media/player/media_player_widget.cpp b/Telegram/SourceFiles/media/player/media_player_widget.cpp index d94732481..4bde4e4f0 100644 --- a/Telegram/SourceFiles/media/player/media_player_widget.cpp +++ b/Telegram/SourceFiles/media/player/media_player_widget.cpp @@ -15,7 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/ripple_animation.h" #include "lang/lang_keys.h" #include "media/audio/media_audio.h" -#include "media/view/media_clip_playback.h" +#include "media/view/media_view_playback_progress.h" #include "media/player/media_player_button.h" #include "media/player/media_player_instance.h" #include "media/player/media_player_volume_controller.h" @@ -86,7 +86,7 @@ Widget::Widget(QWidget *parent) : RpWidget(parent) , _close(this, st::mediaPlayerClose) , _shadow(this) , _playbackSlider(this, st::mediaPlayerPlayback) -, _playback(std::make_unique()) { +, _playbackProgress(std::make_unique()) { setAttribute(Qt::WA_OpaquePaintEvent); setMouseTracking(true); resize(width(), st::mediaPlayerHeight + st::lineWidth); @@ -94,24 +94,24 @@ Widget::Widget(QWidget *parent) : RpWidget(parent) _nameLabel->setAttribute(Qt::WA_TransparentForMouseEvents); _timeLabel->setAttribute(Qt::WA_TransparentForMouseEvents); - _playback->setInLoadingStateChangedCallback([this](bool loading) { + _playbackProgress->setInLoadingStateChangedCallback([this](bool loading) { _playbackSlider->setDisabled(loading); }); - _playback->setValueChangedCallback([this](float64 value) { + _playbackProgress->setValueChangedCallback([this](float64 value) { _playbackSlider->setValue(value); }); _playbackSlider->setChangeProgressCallback([this](float64 value) { if (_type != AudioMsgId::Type::Song) { return; // Round video seek is not supported for now :( } - _playback->setValue(value, false); + _playbackProgress->setValue(value, false); handleSeekProgress(value); }); _playbackSlider->setChangeFinishedCallback([this](float64 value) { if (_type != AudioMsgId::Type::Song) { return; // Round video seek is not supported for now :( } - _playback->setValue(value, false); + _playbackProgress->setValue(value, false); handleSeekFinished(value); }); _playPause->setClickedCallback([this] { @@ -430,9 +430,9 @@ void Widget::handleSongUpdate(const TrackState &state) { } if (state.id.audio()->loading()) { - _playback->updateLoadingState(state.id.audio()->progress()); + _playbackProgress->updateLoadingState(state.id.audio()->progress()); } else { - _playback->updateState(state); + _playbackProgress->updateState(state); } auto stopped = IsStoppedOrStopping(state.state); diff --git a/Telegram/SourceFiles/media/player/media_player_widget.h b/Telegram/SourceFiles/media/player/media_player_widget.h index 5804f24f6..b3009c44e 100644 --- a/Telegram/SourceFiles/media/player/media_player_widget.h +++ b/Telegram/SourceFiles/media/player/media_player_widget.h @@ -20,10 +20,12 @@ class FilledSlider; } // namespace Ui namespace Media { -namespace Clip { -class Playback; +namespace View { +class PlaybackProgress; } // namespace Clip +} // namespace Media +namespace Media { namespace Player { class PlayButton; @@ -109,11 +111,11 @@ private: object_ptr _close; object_ptr _shadow = { nullptr }; object_ptr _playbackSlider; - std::unique_ptr _playback; + std::unique_ptr _playbackProgress; rpl::lifetime _playlistChangesLifetime; }; -} // namespace Clip +} // namespace Player } // namespace Media diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_common.h b/Telegram/SourceFiles/media/streaming/media_streaming_common.h index 8e4040b65..a00f50a42 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_common.h +++ b/Telegram/SourceFiles/media/streaming/media_streaming_common.h @@ -36,6 +36,7 @@ struct PlaybackOptions { crl::time position = 0; float64 speed = 1.; // Valid values between 0.5 and 2. bool syncVideoByAudio = true; + bool dropStaleFrames = true; }; struct TrackState { diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp index 493d7507a..259f479fd 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp @@ -513,7 +513,7 @@ bool Player::failed() const { } bool Player::playing() const { - return (_stage == Stage::Started) && !_paused; + return (_stage == Stage::Started) && !_paused && !finished(); } bool Player::buffering() const { @@ -524,6 +524,12 @@ bool Player::paused() const { return _pausedByUser; } +bool Player::finished() const { + return (_stage == Stage::Started) + && (!_audio || _audioFinished) + && (!_video || _videoFinished); +} + void Player::setSpeed(float64 speed) { Expects(valid()); Expects(speed >= 0.5 && speed <= 2.); diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_player.h b/Telegram/SourceFiles/media/streaming/media_streaming_player.h index bac478566..4625c39a0 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_player.h +++ b/Telegram/SourceFiles/media/streaming/media_streaming_player.h @@ -44,10 +44,11 @@ public: float64 speed() const; void setSpeed(float64 speed); // 0.5 <= speed <= 2. - [[nodiscard]] bool failed() const; [[nodiscard]] bool playing() const; [[nodiscard]] bool buffering() const; [[nodiscard]] bool paused() const; + [[nodiscard]] bool failed() const; + [[nodiscard]] bool finished() const; [[nodiscard]] rpl::producer updates() const; diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp index 5285ce629..7f68d6136 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp @@ -8,6 +8,7 @@ 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" extern "C" { #include @@ -20,6 +21,7 @@ namespace { constexpr auto kSkipInvalidDataPackets = 10; constexpr auto kAlignImageBy = 16; constexpr auto kPixelBytesSize = 4; +constexpr auto kImageFormat = QImage::Format_ARGB32_Premultiplied; void AlignedImageBufferCleanupHandler(void* data) { const auto buffer = static_cast(data); @@ -39,8 +41,16 @@ void ClearFrameMemory(AVFrame *frame) { } // namespace +bool GoodStorageForFrame(const QImage &storage, QSize size) { + return !storage.isNull() + && (storage.format() == kImageFormat) + && (storage.size() == size) + && storage.isDetached() + && IsAlignedImage(storage); +} + // Create a QImage of desired size where all the data is properly aligned. -QImage CreateImageForOriginalFrame(QSize size) { +QImage CreateFrameStorage(QSize size) { const auto width = size.width(); const auto height = size.height(); const auto widthAlign = kAlignImageBy / kPixelBytesSize; @@ -59,7 +69,7 @@ QImage CreateImageForOriginalFrame(QSize size) { width, height, perLine, - QImage::Format_ARGB32_Premultiplied, + kImageFormat, AlignedImageBufferCleanupHandler, cleanupData); } @@ -267,18 +277,26 @@ AvErrorWrap ReadNextFrame(Stream &stream) { return error; } -QImage ConvertFrame( - Stream &stream, - QSize resize, - QImage storage) { +bool GoodForRequest(const QImage &image, const FrameRequest &request) { + if (request.resize.isEmpty()) { + return true; + } else if ((request.radius != ImageRoundRadius::None) + && ((request.corners & RectPart::AllCorners) != 0)) { + return false; + } + return (request.resize == request.outer) + && (request.resize == image.size()); +} + +QImage ConvertFrame(Stream &stream, QSize resize, QImage storage) { Expects(stream.frame != nullptr); const auto frame = stream.frame.get(); const auto frameSize = QSize(frame->width, frame->height); if (frameSize.isEmpty()) { LOG(("Streaming Error: Bad frame size %1,%2" - ).arg(resize.width() - ).arg(resize.height())); + ).arg(frameSize.width() + ).arg(frameSize.height())); return QImage(); } else if (!frame->data[0]) { LOG(("Streaming Error: Bad frame data.")); @@ -289,11 +307,9 @@ QImage ConvertFrame( } else if (RotationSwapWidthHeight(stream.rotation)) { resize.transpose(); } - if (storage.isNull() - || storage.size() != resize - || !storage.isDetached() - || !IsAlignedImage(storage)) { - storage = CreateImageForOriginalFrame(resize); + + if (!GoodStorageForFrame(storage, resize)) { + storage = CreateFrameStorage(resize); } const auto format = AV_PIX_FMT_BGRA; const auto hasDesiredFormat = (frame->format == format) @@ -343,7 +359,24 @@ QImage ConvertFrame( return QImage(); } } - ClearFrameMemory(stream.frame.get()); + return storage; +} + +QImage PrepareByRequest( + const QImage &original, + const FrameRequest &request, + QImage storage) { + Expects(!request.outer.isEmpty()); + + if (!GoodStorageForFrame(storage, request.outer)) { + storage = CreateFrameStorage(request.outer); + } + { + Painter p(&storage); + PainterHighQualityEnabler hq(p); + p.drawImage(QRect(QPoint(), request.outer), original); + } + // #TODO streaming later full prepare support. return storage; } diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_utility.h b/Telegram/SourceFiles/media/streaming/media_streaming_utility.h index d769f5d0c..f983d5a68 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_utility.h +++ b/Telegram/SourceFiles/media/streaming/media_streaming_utility.h @@ -173,11 +173,19 @@ void LogError(QLatin1String method, AvErrorWrap error); [[nodiscard]] AvErrorWrap ProcessPacket(Stream &stream, Packet &&packet); [[nodiscard]] AvErrorWrap ReadNextFrame(Stream &stream); -[[nodiscard]] QImage CreateImageForOriginalFrame(QSize size); +[[nodiscard]] bool GoodForRequest( + const QImage &image, + const FrameRequest &request); +[[nodiscard]] bool GoodStorageForFrame(const QImage &storage, QSize size); +[[nodiscard]] QImage CreateFrameStorage(QSize size); [[nodiscard]] QImage ConvertFrame( Stream& stream, QSize resize, QImage storage); +[[nodiscard]] QImage PrepareByRequest( + const QImage &original, + const FrameRequest &request, + QImage storage); } // namespace Streaming } // namespace Media diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_video_track.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_video_track.cpp index 5712b5c7a..fb14ab4b8 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_video_track.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_video_track.cpp @@ -43,6 +43,7 @@ public: void setSpeed(float64 speed); void interrupt(); void frameDisplayed(); + void updateFrameRequest(const FrameRequest &request); private: [[nodiscard]] bool interrupted() const; @@ -79,6 +80,7 @@ private: crl::time _nextFrameDisplayTime = kTimeUnknown; rpl::event_stream _nextFrameTimeUpdates; rpl::event_stream<> _waitingForData; + FrameRequest _request; bool _queued = false; base::ConcurrentTimer _readFramesTimer; @@ -150,10 +152,15 @@ void VideoTrackObject::readFrames() { if (interrupted()) { return; } - const auto state = _shared->prepareState(trackTime().trackTime); + const auto time = trackTime().trackTime; + const auto dropStaleFrames = _options.dropStaleFrames; + const auto state = _shared->prepareState(time, dropStaleFrames); state.match([&](Shared::PrepareFrame frame) { - if (readFrame(frame)) { - presentFrameIfNeeded(); + while (readFrame(frame)) { + if (!dropStaleFrames || !VideoTrack::IsStale(frame, time)) { + presentFrameIfNeeded(); + break; + } } }, [&](Shared::PrepareNextCheck delay) { Expects(delay > 0); @@ -183,17 +190,8 @@ bool VideoTrackObject::readFrame(not_null frame) { _error(); return false; } - frame->original = ConvertFrame( - _stream, - QSize(), - std::move(frame->original)); frame->position = position; frame->displayed = kTimeUnknown; - - // #TODO streaming later prepare frame - //frame->request - //frame->prepared - return true; } @@ -202,7 +200,24 @@ void VideoTrackObject::presentFrameIfNeeded() { return; } const auto time = trackTime(); - const auto presented = _shared->presentFrame(time.trackTime); + const auto prepare = [&](not_null frame) { + frame->request = _request; + frame->original = ConvertFrame( + _stream, + frame->request.resize, + std::move(frame->original)); + if (frame->original.isNull()) { + frame->prepared = QImage(); + interrupt(); + _error(); + return; + } + + VideoTrack::PrepareFrameByRequest(frame); + + Ensures(VideoTrack::IsPrepared(frame)); + }; + const auto presented = _shared->presentFrame(time.trackTime, prepare); if (presented.displayPosition != kTimeUnknown) { const auto trackLeft = presented.displayPosition - time.trackTime; @@ -268,6 +283,10 @@ void VideoTrackObject::frameDisplayed() { queueReadFrames(); } +void VideoTrackObject::updateFrameRequest(const FrameRequest &request) { + _request = request; +} + bool VideoTrackObject::tryReadFirstFrame(Packet &&packet) { if (ProcessPacket(_stream, std::move(packet)).failed()) { return false; @@ -412,31 +431,20 @@ not_null VideoTrack::Shared::getFrame(int index) { return &_frames[index]; } -bool VideoTrack::Shared::IsPrepared(not_null frame) { - return (frame->position != kTimeUnknown) - && (frame->displayed == kTimeUnknown) - && !frame->original.isNull(); -} - -bool VideoTrack::Shared::IsStale( - not_null frame, - crl::time trackTime) { - Expects(IsPrepared(frame)); - - return (frame->position < trackTime); -} - -auto VideoTrack::Shared::prepareState(crl::time trackTime) -> PrepareState { +auto VideoTrack::Shared::prepareState( + crl::time trackTime, + bool dropStaleFrames) +-> PrepareState { const auto prepareNext = [&](int index) -> PrepareState { const auto frame = getFrame(index); const auto next = getFrame((index + 1) % kFramesCount); - if (!IsPrepared(frame)) { + if (!IsDecoded(frame)) { return frame; - } else if (IsStale(frame, trackTime)) { + } else if (dropStaleFrames && IsStale(frame, trackTime)) { std::swap(*frame, *next); next->displayed = kDisplaySkipped; - return IsPrepared(frame) ? next : frame; - } else if (!IsPrepared(next)) { + return IsDecoded(frame) ? next : frame; + } else if (!IsDecoded(next)) { return next; } else { return PrepareNextCheck(frame->position - trackTime + 1); @@ -445,7 +453,7 @@ auto VideoTrack::Shared::prepareState(crl::time trackTime) -> PrepareState { const auto finishPrepare = [&](int index) { const auto frame = getFrame(index); // If player already awaits next frame - we ignore if it's stale. - return IsPrepared(frame) ? std::nullopt : PrepareState(frame); + return IsDecoded(frame) ? std::nullopt : PrepareState(frame); }; switch (counter()) { @@ -461,11 +469,18 @@ auto VideoTrack::Shared::prepareState(crl::time trackTime) -> PrepareState { Unexpected("Counter value in VideoTrack::Shared::prepareState."); } -auto VideoTrack::Shared::presentFrame(crl::time trackTime) -> PresentFrame { +template +auto VideoTrack::Shared::presentFrame( + crl::time trackTime, + PrepareCallback &&prepare) +-> PresentFrame { const auto present = [&](int counter, int index) -> PresentFrame { const auto frame = getFrame(index); - Assert(IsPrepared(frame)); const auto position = frame->position; + prepare(frame); + if (!IsPrepared(frame)) { + return { kTimeUnknown, crl::time(0) }; + } // Release this frame to the main thread for rendering. _counter.store( @@ -476,8 +491,8 @@ auto VideoTrack::Shared::presentFrame(crl::time trackTime) -> PresentFrame { const auto nextCheckDelay = [&](int index) -> PresentFrame { const auto frame = getFrame(index); const auto next = getFrame((index + 1) % kFramesCount); - if (!IsPrepared(frame) - || !IsPrepared(next) + if (!IsDecoded(frame) + || !IsDecoded(next) || IsStale(frame, trackTime)) { return { kTimeUnknown, crl::time(0) }; } @@ -594,24 +609,50 @@ crl::time VideoTrack::markFrameDisplayed(crl::time now) { return position; } -QImage VideoTrack::frame(const FrameRequest &request) const { +QImage VideoTrack::frame(const FrameRequest &request) { const auto frame = _shared->frameForPaint(); - Assert(frame != nullptr); - Assert(!frame->original.isNull()); + const auto changed = (frame->request != request); + if (changed) { + frame->request = request; + _wrapped.with([=](Implementation &unwrapped) { + unwrapped.updateFrameRequest(request); + }); + } + return PrepareFrameByRequest(frame, !changed); +} - if (request.resize.isEmpty()) { +QImage VideoTrack::PrepareFrameByRequest( + not_null frame, + bool useExistingPrepared) { + Expects(!frame->original.isNull()); + + if (GoodForRequest(frame->original, frame->request)) { return frame->original; - } else if (frame->prepared.isNull() || frame->request != request) { - // #TODO streaming later prepare frame - //frame->request = request; - //frame->prepared = PrepareFrame( - // frame->original, - // request, - // std::move(frame->prepared)); + } else if (frame->prepared.isNull() || !useExistingPrepared) { + frame->prepared = PrepareByRequest( + frame->original, + frame->request, + std::move(frame->prepared)); } return frame->prepared; } +bool VideoTrack::IsDecoded(not_null frame) { + return (frame->position != kTimeUnknown) + && (frame->displayed == kTimeUnknown); +} + +bool VideoTrack::IsPrepared(not_null frame) { + return IsDecoded(frame) + && !frame->original.isNull(); +} + +bool VideoTrack::IsStale(not_null frame, crl::time trackTime) { + Expects(IsDecoded(frame)); + + return (frame->position < trackTime); +} + rpl::producer VideoTrack::renderNextFrame() const { return _wrapped.producer_on_main([](const Implementation &unwrapped) { return unwrapped.displayFrameAt(); diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_video_track.h b/Telegram/SourceFiles/media/streaming/media_streaming_video_track.h index 2970888e8..57ef6c47c 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_video_track.h +++ b/Telegram/SourceFiles/media/streaming/media_streaming_video_track.h @@ -46,7 +46,7 @@ public: // Called from the main thread. // Returns the position of the displayed frame. [[nodiscard]] crl::time markFrameDisplayed(crl::time now); - [[nodiscard]] QImage frame(const FrameRequest &request) const; + [[nodiscard]] QImage frame(const FrameRequest &request); [[nodiscard]] rpl::producer renderNextFrame() const; [[nodiscard]] rpl::producer<> waitingForData() const; @@ -81,8 +81,15 @@ private: void init(QImage &&cover, crl::time position); [[nodiscard]] bool initialized() const; - [[nodiscard]] PrepareState prepareState(crl::time trackTime); - [[nodiscard]] PresentFrame presentFrame(crl::time trackTime); + [[nodiscard]] PrepareState prepareState( + crl::time trackTime, + bool dropStaleFrames); + + // PrepareCallback(not_null). + template + [[nodiscard]] PresentFrame presentFrame( + crl::time trackTime, + PrepareCallback &&prepare); // Called from the main thread. // Returns the position of the displayed frame. @@ -91,10 +98,6 @@ private: private: [[nodiscard]] not_null getFrame(int index); - [[nodiscard]] static bool IsPrepared(not_null frame); - [[nodiscard]] static bool IsStale( - not_null frame, - crl::time trackTime); [[nodiscard]] int counter() const; static constexpr auto kCounterUninitialized = -1; @@ -105,6 +108,15 @@ private: }; + static QImage PrepareFrameByRequest( + not_null frame, + bool useExistingPrepared = false); + [[nodiscard]] static bool IsDecoded(not_null frame); + [[nodiscard]] static bool IsPrepared(not_null frame); + [[nodiscard]] static bool IsStale( + not_null frame, + crl::time trackTime); + const int _streamIndex = 0; const AVRational _streamTimeBase; //const int _streamRotation = 0; diff --git a/Telegram/SourceFiles/media/view/media_view_group_thumbs.cpp b/Telegram/SourceFiles/media/view/media_view_group_thumbs.cpp index fcee65543..7a76ddb78 100644 --- a/Telegram/SourceFiles/media/view/media_view_group_thumbs.cpp +++ b/Telegram/SourceFiles/media/view/media_view_group_thumbs.cpp @@ -650,7 +650,7 @@ bool GroupThumbs::hidden() const { void GroupThumbs::checkForAnimationStart() { if (_waitingForAnimationStart) { _waitingForAnimationStart = false; - _animation.start([this] { update(); }, 0., 1., kThumbDuration); + _animation.start([=] { update(); }, 0., 1., kThumbDuration); } } diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index ce540d31c..708ea2e77 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -20,8 +20,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text_options.h" #include "media/audio/media_audio.h" #include "media/clip/media_clip_reader.h" -#include "media/view/media_clip_controller.h" +#include "media/view/media_view_playback_controls.h" #include "media/view/media_view_group_thumbs.h" +#include "media/streaming/media_streaming_player.h" +#include "media/streaming/media_streaming_loader.h" #include "history/history.h" #include "history/history_message.h" #include "data/data_media_types.h" @@ -43,6 +45,7 @@ namespace Media { namespace View { namespace { +constexpr auto kMsFrequency = 1000; // 1000 ms per second. constexpr auto kPreloadCount = 4; // Preload X message ids before and after current. @@ -58,6 +61,62 @@ Images::Options VideoThumbOptions(not_null document) { : result; } +void PaintImageProfile(QPainter &p, const QImage &image, QRect rect, QRect fill) { + const auto argb = image.convertToFormat(QImage::Format_ARGB32_Premultiplied); + const auto rgb = image.convertToFormat(QImage::Format_RGB32); + const auto argbp = QPixmap::fromImage(argb); + const auto rgbp = QPixmap::fromImage(rgb); + const auto width = image.width(); + const auto height = image.height(); + const auto xcopies = (fill.width() + width - 1) / width; + const auto ycopies = (fill.height() + height - 1) / height; + const auto copies = xcopies * ycopies; + auto times = QStringList(); + const auto bench = [&](QString label, auto &&paint) { + const auto single = [&](QString label) { + auto now = crl::now(); + const auto push = [&] { + times.push_back(QString("%1").arg(crl::now() - now, 4, 10, QChar(' '))); + now = crl::now(); + }; + paint(rect); + push(); + { + PainterHighQualityEnabler hq(p); + paint(rect); + } + push(); + for (auto i = 0; i < xcopies; ++i) { + for (auto j = 0; j < ycopies; ++j) { + paint(QRect( + fill.topLeft() + QPoint(i * width, j * height), + QSize(width, height))); + } + } + push(); + LOG(("FRAME (%1): %2 (copies: %3)").arg(label).arg(times.join(' ')).arg(copies)); + times = QStringList(); + now = crl::now(); + }; + p.setCompositionMode(QPainter::CompositionMode_Source); + single(label + " S"); + p.setCompositionMode(QPainter::CompositionMode_SourceOver); + single(label + " O"); + }; + bench("ARGB I", [&](QRect rect) { + p.drawImage(rect, argb); + }); + bench("RGB I", [&](QRect rect) { + p.drawImage(rect, rgb); + }); + bench("ARGB P", [&](QRect rect) { + p.drawPixmap(rect, argbp); + }); + bench("RGB P", [&](QRect rect) { + p.drawPixmap(rect, rgbp); + }); +} + } // namespace struct OverlayWidget::SharedMedia { @@ -83,10 +142,30 @@ struct OverlayWidget::Collage { CollageKey key; }; +struct OverlayWidget::Streamed { + Streamed( + not_null owner, + std::unique_ptr loader, + QWidget *controlsParent, + not_null controlsDelegate); + + Streaming::Player player; + Streaming::Information info; + PlaybackControls controls; +}; + +OverlayWidget::Streamed::Streamed( + not_null owner, + std::unique_ptr loader, + QWidget *controlsParent, + not_null controlsDelegate) +: player(owner, std::move(loader)) +, controls(controlsParent, controlsDelegate) { +} + OverlayWidget::OverlayWidget() : OverlayParent(nullptr) , _transparentBrush(style::transparentPlaceholderBrush()) -, _animStarted(crl::now()) , _docDownload(this, lang(lng_media_download), st::mediaviewFileLink) , _docSaveAs(this, lang(lng_mediaview_save_as), st::mediaviewFileLink) , _docCancel(this, lang(lng_cancel), st::mediaviewFileLink) @@ -117,8 +196,11 @@ OverlayWidget::OverlayWidget() } }); subscribe(Auth().calls().currentCallChanged(), [this](Calls::Call *call) { - if (call && _clipController && !_videoPaused) { - onVideoPauseResume(); + if (call + && _streamed + && !_streamed->player.paused() + && !_streamed->player.finished()) { + playbackPauseResume(); } }); subscribe(Auth().documentUpdated, [this](DocumentData *document) { @@ -213,52 +295,72 @@ void OverlayWidget::moveToScreen() { _saveMsg.moveTo((width() - _saveMsg.width()) / 2, (height() - _saveMsg.height()) / 2); } -bool OverlayWidget::fileShown() const { - return !_current.isNull() || gifShown(); +bool OverlayWidget::videoShown() const { + return _streamed && !_streamed->info.video.cover.isNull(); } -bool OverlayWidget::fileBubbleShown() const { - return (!_photo && !_doc) || (_doc && !fileShown() && !_themePreviewShown); +QSize OverlayWidget::videoSize() const { + Expects(videoShown()); + + return _streamed->info.video.size; } -bool OverlayWidget::gifShown() const { - if (_gif && _gif->ready()) { - if (!_gif->started()) { - const auto streamVideo = _doc - && (_doc->isVideoFile() || _doc->isVideoMessage()); - const auto pauseOnStart = (_autoplayVideoDocument != _doc); - if (streamVideo && pauseOnStart && !_gif->videoPaused()) { - const_cast(this)->toggleVideoPaused(); - } - const auto rounding = (_doc && _doc->isVideoMessage()) - ? ImageRoundRadius::Ellipse - : ImageRoundRadius::None; - _gif->start( - _gif->width() / cIntRetinaFactor(), - _gif->height() / cIntRetinaFactor(), - _gif->width() / cIntRetinaFactor(), - _gif->height() / cIntRetinaFactor(), - rounding, - RectPart::AllCorners); - const_cast(this)->_current = QPixmap(); - updateMixerVideoVolume(); - Global::RefVideoVolumeChanged().notify(); - } - return true;// _gif->state() != Media::Clip::State::Error; +bool OverlayWidget::videoIsGifv() const { + return _streamed && _doc->isAnimation() && !_doc->isVideoMessage(); +} + +QImage OverlayWidget::videoFrame() const { + Expects(videoShown()); + + auto request = Media::Streaming::FrameRequest(); + //request.radius = (_doc && _doc->isVideoMessage()) + // ? ImageRoundRadius::Ellipse + // : ImageRoundRadius::None; + return _streamed->player.ready() + ? _streamed->player.frame(request) + : _streamed->info.video.cover; +} + +crl::time OverlayWidget::streamedPosition() const { + Expects(_streamed != nullptr); + + const auto result = std::max( + _streamed->info.audio.state.position, + _streamed->info.video.state.position); + return (result != kTimeUnknown) ? result : crl::time(0); +} + +crl::time OverlayWidget::streamedDuration() const { + Expects(_streamed != nullptr); + + const auto result = std::max( + _streamed->info.audio.state.duration, + _streamed->info.video.state.duration); + if (result != kTimeUnknown) { + return result; } - return false; + const auto duration = _doc->song() + ? _doc->song()->duration + : _doc->duration(); + return (duration > 0) ? duration * crl::time(1000) : kTimeUnknown; } -void OverlayWidget::stopGif() { - _gif = nullptr; - _videoPaused = _videoStopped = _videoIsSilent = false; +bool OverlayWidget::documentContentShown() const { + return _doc && (!_current.isNull() || videoShown()); +} + +bool OverlayWidget::documentBubbleShown() const { + return (!_photo && !_doc) + || (_doc && !_themePreviewShown && _current.isNull() && !_streamed); +} + +void OverlayWidget::clearStreaming() { _fullScreenVideo = false; - _clipController.destroy(); - disconnect(Media::Player::mixer(), SIGNAL(updated(const AudioMsgId&)), this, SLOT(onVideoPlayProgress(const AudioMsgId&))); + _streamed = nullptr; } void OverlayWidget::documentUpdated(DocumentData *doc) { - if (fileBubbleShown() && _doc && _doc == doc) { + if (documentBubbleShown() && _doc && _doc == doc) { if ((_doc->loading() && _docCancel->isHidden()) || (!_doc->loading() && !_docCancel->isHidden())) { updateControls(); } else if (_doc->loading()) { @@ -276,7 +378,7 @@ void OverlayWidget::changingMsgId(not_null row, MsgId newId) { } void OverlayWidget::updateDocSize() { - if (!_doc || !fileBubbleShown()) return; + if (!_doc || !documentBubbleShown()) return; if (_doc->loading()) { quint64 ready = _doc->loadOffset(), total = _doc->size; @@ -325,7 +427,7 @@ void OverlayWidget::refreshNavVisibility() { } void OverlayWidget::updateControls() { - if (_doc && fileBubbleShown()) { + if (_doc && documentBubbleShown()) { if (_doc->loading()) { _docDownload->hide(); _docSaveAs->hide(); @@ -355,7 +457,9 @@ void OverlayWidget::updateControls() { updateThemePreviewGeometry(); - _saveVisible = ((_photo && _photo->loaded()) || (_doc && (_doc->loaded(DocumentData::FilePathResolveChecked) || (!fileShown() && (_photo || _doc))))); + _saveVisible = (_photo && _photo->loaded()) + || (_doc && (_doc->loaded(DocumentData::FilePathResolveChecked) + || !documentContentShown())); _saveNav = myrtlrect(width() - st::mediaviewIconSize.width() * 2, height() - st::mediaviewIconSize.height(), st::mediaviewIconSize.width(), st::mediaviewIconSize.height()); _saveNavIcon = centerrect(_saveNav, st::mediaviewSave); _moreNav = myrtlrect(width() - st::mediaviewIconSize.width(), height() - st::mediaviewIconSize.height(), st::mediaviewIconSize.width(), st::mediaviewIconSize.height()); @@ -423,8 +527,8 @@ void OverlayWidget::refreshCaptionGeometry() { _groupThumbs = nullptr; _groupThumbsRect = QRect(); } - const auto captionBottom = _clipController - ? (_clipController->y() - st::mediaviewCaptionMargin.height()) + const auto captionBottom = (_streamed && !videoIsGifv()) + ? (_streamed->controls.y() - st::mediaviewCaptionMargin.height()) : _groupThumbs ? _groupThumbsTop : height() - st::mediaviewCaptionMargin.height(); @@ -460,7 +564,7 @@ void OverlayWidget::updateActions() { if (_doc && !_doc->filepath(DocumentData::FilePathResolveChecked).isEmpty()) { _actions.push_back({ lang((cPlatform() == dbipMac || cPlatform() == dbipMacOld) ? lng_context_show_in_finder : lng_context_show_in_folder), SLOT(onShowInFolder()) }); } - if ((_doc && fileShown()) || (_photo && _photo->loaded())) { + if ((_doc && documentContentShown()) || (_photo && _photo->loaded())) { _actions.push_back({ lang(lng_mediaview_copy), SLOT(onCopy()) }); } if (_photo && _photo->hasSticker) { @@ -571,6 +675,10 @@ void OverlayWidget::updateCursor() { : (_over == OverNone ? style::cur_default : style::cur_pointer)); } +QRect OverlayWidget::contentRect() const { + return { _x, _y, _w, _h }; +} + float64 OverlayWidget::radialProgress() const { if (_doc) { return _doc->progress(); @@ -691,7 +799,7 @@ void OverlayWidget::zoomReset() { newZoom = 0; } _x = -_width / 2; - _y = -((gifShown() ? _gif->height() : (_current.height() / cIntRetinaFactor())) / 2); + _y = -_height / 2; float64 z = (_zoom == ZoomToScreenLevel) ? _zoomToScreen : _zoom; if (z >= 0) { _x = qRound(_x * (z + 1)); @@ -724,7 +832,7 @@ void OverlayWidget::clearData() { _a_state.stop(); } if (!_animOpacities.isEmpty()) _animOpacities.clear(); - stopGif(); + clearStreaming(); delete _menu; _menu = nullptr; setContext(std::nullopt); @@ -754,7 +862,7 @@ void OverlayWidget::showSaveMsgFile() { } void OverlayWidget::updateMixerVideoVolume() const { - if (_doc && (_doc->isVideoFile() || _doc->isVideoMessage())) { + if (_streamed) { Media::Player::mixer()->setVideoVolume(Global::VideoVolume()); } } @@ -768,8 +876,8 @@ void OverlayWidget::activateControls() { _controlsHideTimer.start(int(st::mediaviewWaitHide)); } if (_fullScreenVideo) { - if (_clipController) { - _clipController->showAnimated(); + if (_streamed) { + _streamed->controls.showAnimated(); } } if (_controlsState == ControlsHiding || _controlsState == ControlsHidden) { @@ -785,14 +893,14 @@ void OverlayWidget::onHideControls(bool force) { if (!_dropdown->isHidden() || _menu || _mousePressed - || (_fullScreenVideo && _clipController && _clipController->geometry().contains(_lastMouseMovePos))) { + || (_fullScreenVideo + && !videoIsGifv() + && _streamed->controls.geometry().contains(_lastMouseMovePos))) { return; } } if (_fullScreenVideo) { - if (_clipController) { - _clipController->hideAnimated(); - } + _streamed->controls.hideAnimated(); } if (_controlsState == ControlsHiding || _controlsState == ControlsHidden) return; @@ -881,8 +989,8 @@ void OverlayWidget::onSaveAs() { if (_doc->data().isEmpty()) location.accessDisable(); } else { - if (!fileShown()) { - DocumentSaveClickHandler::Save(fileOrigin(), _doc, true); + if (!documentContentShown()) { + DocumentSaveClickHandler::Save(fileOrigin(), _doc, App::histItemById(_msgid), true); updateControls(); } else { _saveVisible = false; @@ -934,51 +1042,6 @@ void OverlayWidget::onDocClick() { } } -void OverlayWidget::clipCallback(Media::Clip::Notification notification) { - using namespace Media::Clip; - - if (!_gif) return; - - switch (notification) { - case NotificationReinit: { - if (auto item = App::histItemById(_msgid)) { - if (_gif->state() == State::Error) { - stopGif(); - updateControls(); - update(); - break; - } else if (_gif->state() == State::Finished) { - _videoPositionMs = _videoDurationMs; - _videoStopped = true; - updateSilentVideoPlaybackState(); - } else { - _videoIsSilent = _doc && (_doc->isVideoFile() || _doc->isVideoMessage()) && !_gif->hasAudio(); - _videoDurationMs = _gif->getDurationMs(); - _videoPositionMs = _gif->getPositionMs(); - if (_videoIsSilent) { - updateSilentVideoPlaybackState(); - } - } - displayDocument(_doc, item); - } else { - stopGif(); - updateControls(); - update(); - } - } break; - - case NotificationRepaint: { - if (!_gif->currentDisplayed()) { - _videoPositionMs = _gif->getPositionMs(); - if (_videoIsSilent) { - updateSilentVideoPlaybackState(); - } - update(_x, _y, _w, _h); - } - } break; - } -} - PeerData *OverlayWidget::ui_getPeerForMouseAction() { return _history ? _history->peer.get() : nullptr; } @@ -1013,8 +1076,11 @@ void OverlayWidget::onDownload() { } location.accessDisable(); } else { - if (!fileShown()) { - DocumentSaveClickHandler::Save(fileOrigin(), _doc); + if (!documentContentShown()) { + DocumentSaveClickHandler::Save( + fileOrigin(), + _doc, + App::histItemById(_msgid)); updateControls(); } else { _saveVisible = false; @@ -1102,8 +1168,8 @@ void OverlayWidget::onCopy() { if (_doc) { if (!_current.isNull()) { QApplication::clipboard()->setPixmap(_current); - } else if (gifShown()) { - QApplication::clipboard()->setPixmap(_gif->frameOriginal()); + } else if (videoShown()) { + QApplication::clipboard()->setImage(videoFrame()); } } else { if (!_photo || !_photo->loaded()) return; @@ -1557,7 +1623,7 @@ void OverlayWidget::displayPhoto(not_null photo, HistoryItem *item) displayDocument(nullptr, item); return; } - stopGif(); + clearStreaming(); destroyThemePreview(); _doc = _autoplayVideoDocument = nullptr; _fullScreenVideo = false; @@ -1593,6 +1659,7 @@ void OverlayWidget::displayPhoto(not_null photo, HistoryItem *item) _x = (width() - _w) / 2; _y = (height() - _h) / 2; _width = _w; + _height = _h; if (_msgid && item) { _from = item->senderOriginal(); } else { @@ -1610,13 +1677,16 @@ void OverlayWidget::destroyThemePreview() { _themeCancel.destroy(); } -void OverlayWidget::displayDocument(DocumentData *doc, HistoryItem *item) { // empty messages shown as docs: doc can be NULL - auto documentChanged = (!doc || doc != _doc || (item && item->fullId() != _msgid)); +// Empty messages shown as docs: doc can be nullptr. +void OverlayWidget::displayDocument(DocumentData *doc, HistoryItem *item) { + const auto documentChanged = !doc + || (doc != _doc) + || (item && item->fullId() != _msgid); if (documentChanged || (!doc->isAnimation() && !doc->isVideoFile())) { _fullScreenVideo = false; _current = QPixmap(); - stopGif(); - } else if (gifShown()) { + clearStreaming(); + } else if (videoShown()) { _current = QPixmap(); } if (documentChanged || !doc->isTheme()) { @@ -1648,8 +1718,8 @@ void OverlayWidget::displayDocument(DocumentData *doc, HistoryItem *item) { // e } else { _doc->automaticLoad(fileOrigin(), item); - if (_doc->isAnimation() || _doc->isVideoFile()) { - initAnimation(); + if (_doc->canBePlayed()) { + initStreaming(); } else if (_doc->isTheme()) { initThemePreview(); } else { @@ -1665,7 +1735,7 @@ void OverlayWidget::displayDocument(DocumentData *doc, HistoryItem *item) { // e } _docIconRect = QRect((width() - st::mediaviewFileIconSize) / 2, (height() - st::mediaviewFileIconSize) / 2, st::mediaviewFileIconSize, st::mediaviewFileIconSize); - if (fileBubbleShown()) { + if (documentBubbleShown()) { if (!_doc || !_doc->hasThumbnail()) { int32 colorIndex = documentColorIndex(_doc, _docExt); _docIconColor = documentColor(colorIndex); @@ -1723,14 +1793,16 @@ void OverlayWidget::displayDocument(DocumentData *doc, HistoryItem *item) { // e _current.setDevicePixelRatio(cRetinaFactor()); _w = ConvertScale(_current.width()); _h = ConvertScale(_current.height()); - } else { - _w = ConvertScale(_gif->width()); - _h = ConvertScale(_gif->height()); + } else if (videoShown()) { + const auto contentSize = ConvertScale(videoSize()); + _w = contentSize.width(); + _h = contentSize.height(); } if (isHidden()) { moveToScreen(); } _width = _w; + _height = _h; if (_w > 0 && _h > 0) { _zoomToScreen = float64(width()) / _w; if (_h * _zoomToScreen > height()) { @@ -1804,61 +1876,88 @@ void OverlayWidget::displayFinished() { } } -void OverlayWidget::initAnimation() { +void OverlayWidget::initStreaming() { Expects(_doc != nullptr); - Expects(_doc->isAnimation() || _doc->isVideoFile()); + Expects(_doc->canBePlayed()); - auto &location = _doc->location(true); - if (!_doc->data().isEmpty()) { - createClipReader(); - } else if (location.accessEnable()) { - createClipReader(); - location.accessDisable(); - } else if (_doc->dimensions.width() && _doc->dimensions.height()) { + if (_streamed) { + return; + } + initStreamingThumbnail(); + createStreamingObjects(); + + Core::App().updateNonIdle(); + _streamed->player.updates( + ) | rpl::start_with_next_error([=](Streaming::Update &&update) { + handleStreamingUpdate(std::move(update)); + }, [=](Streaming::Error &&error) { + handleStreamingError(std::move(error)); + }, _streamed->controls.lifetime()); + + restartAtSeekPosition(0); +} + +void OverlayWidget::initStreamingThumbnail() { + if (!_doc->hasThumbnail()) { + return; + } + if (_doc->dimensions.width() && _doc->dimensions.height()) { auto w = _doc->dimensions.width(); auto h = _doc->dimensions.height(); - _current = (_doc->hasThumbnail() - ? _doc->thumbnail() - : Image::Empty().get())->pixNoCache(fileOrigin(), w, h, VideoThumbOptions(_doc), w / cIntRetinaFactor(), h / cIntRetinaFactor()); + _current = _doc->thumbnail()->pixNoCache(fileOrigin(), w, h, VideoThumbOptions(_doc), w / cIntRetinaFactor(), h / cIntRetinaFactor()); _current.setDevicePixelRatio(cRetinaFactor()); - } else if (_doc->hasThumbnail()) { - _current = _doc->thumbnail()->pixNoCache(fileOrigin(), _doc->thumbnail()->width(), _doc->thumbnail()->height(), VideoThumbOptions(_doc), st::mediaviewFileIconSize, st::mediaviewFileIconSize); } else { - _current = Image::Empty()->pixNoCache({}, Image::Empty()->width(), Image::Empty()->height(), VideoThumbOptions(_doc), st::mediaviewFileIconSize, st::mediaviewFileIconSize); + _current = _doc->thumbnail()->pixNoCache(fileOrigin(), _doc->thumbnail()->width(), _doc->thumbnail()->height(), VideoThumbOptions(_doc), st::mediaviewFileIconSize, st::mediaviewFileIconSize); } } -void OverlayWidget::createClipReader() { - if (_gif) return; +void OverlayWidget::createStreamingObjects() { + _streamed = std::make_unique( + &_doc->owner(), + _doc->createStreamingLoader(fileOrigin()), + this, + static_cast(this)); - Expects(_doc != nullptr); - Expects(_doc->isAnimation() || _doc->isVideoFile()); - - if (_doc->dimensions.width() && _doc->dimensions.height()) { - int w = _doc->dimensions.width(); - int h = _doc->dimensions.height(); - _current = (_doc->hasThumbnail() - ? _doc->thumbnail() - : Image::Empty().get())->pixNoCache(fileOrigin(), w, h, VideoThumbOptions(_doc), w / cIntRetinaFactor(), h / cIntRetinaFactor()); - _current.setDevicePixelRatio(cRetinaFactor()); - } else if (_doc->hasThumbnail()) { - _current = _doc->thumbnail()->pixNoCache(fileOrigin(), _doc->thumbnail()->width(), _doc->thumbnail()->height(), VideoThumbOptions(_doc), st::mediaviewFileIconSize, st::mediaviewFileIconSize); + if (videoIsGifv()) { + _streamed->controls.hide(); } else { - _current = Image::Empty()->pixNoCache({}, Image::Empty()->width(), Image::Empty()->height(), VideoThumbOptions(_doc), st::mediaviewFileIconSize, st::mediaviewFileIconSize); + refreshClipControllerGeometry(); + _streamed->controls.show(); } - auto mode = (_doc->isVideoFile() || _doc->isVideoMessage()) - ? Media::Clip::Reader::Mode::Video - : Media::Clip::Reader::Mode::Gif; - _gif = Media::Clip::MakeReader(_doc, _msgid, [this](Media::Clip::Notification notification) { - clipCallback(notification); - }, mode); +} - // Correct values will be set when gif gets inited. - _videoPaused = _videoIsSilent = _videoStopped = false; - _videoPositionMs = 0ULL; - _videoDurationMs = _doc->duration() * 1000ULL; +void OverlayWidget::handleStreamingUpdate(Streaming::Update &&update) { + using namespace Streaming; - createClipController(); + update.data.match([&](Information &update) { + _streamed->info = std::move(update); + this->update(contentRect()); + }, [&](PreloadedVideo &update) { + _streamed->info.video.state.receivedTill = update.till; + //updatePlaybackState(); + }, [&](UpdateVideo &update) { + _streamed->info.video.state.position = update.position; + this->update(contentRect()); + updatePlaybackState(); + }, [&](PreloadedAudio &update) { + _streamed->info.audio.state.receivedTill = update.till; + //updatePlaybackState(); + }, [&](UpdateAudio & update) { + _streamed->info.audio.state.position = update.position; + updatePlaybackState(); + }, [&](WaitingForData) { + }, [&](MutedByOther) { + }, [&](Finished) { + const auto finishTrack = [](Media::Streaming::TrackState &state) { + state.position = state.receivedTill = state.duration; + }; + finishTrack(_streamed->info.audio.state); + finishTrack(_streamed->info.video.state); + updatePlaybackState(); + }); +} + +void OverlayWidget::handleStreamingError(Streaming::Error &&error) { } void OverlayWidget::initThemePreview() { @@ -1912,27 +2011,8 @@ void OverlayWidget::initThemePreview() { } } -void OverlayWidget::createClipController() { - Expects(_doc != nullptr); - if (!_doc->isVideoFile() && !_doc->isVideoMessage()) return; - - _clipController.create(this); - refreshClipControllerGeometry(); - _clipController->show(); - - connect(_clipController, SIGNAL(playPressed()), this, SLOT(onVideoPauseResume())); - connect(_clipController, SIGNAL(pausePressed()), this, SLOT(onVideoPauseResume())); - connect(_clipController, SIGNAL(seekProgress(crl::time)), this, SLOT(onVideoSeekProgress(crl::time))); - connect(_clipController, SIGNAL(seekFinished(crl::time)), this, SLOT(onVideoSeekFinished(crl::time))); - connect(_clipController, SIGNAL(volumeChanged(float64)), this, SLOT(onVideoVolumeChanged(float64))); - connect(_clipController, SIGNAL(toFullScreenPressed()), this, SLOT(onVideoToggleFullScreen())); - connect(_clipController, SIGNAL(fromFullScreenPressed()), this, SLOT(onVideoToggleFullScreen())); - - connect(Media::Player::mixer(), SIGNAL(updated(const AudioMsgId&)), this, SLOT(onVideoPlayProgress(const AudioMsgId&))); -} - void OverlayWidget::refreshClipControllerGeometry() { - if (!_clipController) { + if (!_streamed || videoIsGifv()) { return; } @@ -1943,144 +2023,131 @@ void OverlayWidget::refreshClipControllerGeometry() { const auto controllerBottom = _groupThumbs ? _groupThumbsTop : height(); - _clipController->setGeometry( - (width() - _clipController->width()) / 2, - controllerBottom - _clipController->height() - st::mediaviewCaptionPadding.bottom() - st::mediaviewCaptionMargin.height(), + _streamed->controls.setGeometry( + (width() - _streamed->controls.width()) / 2, + controllerBottom - _streamed->controls.height() - st::mediaviewCaptionPadding.bottom() - st::mediaviewCaptionMargin.height(), st::mediaviewControllerSize.width(), st::mediaviewControllerSize.height()); - Ui::SendPendingMoveResizeEvents(_clipController); + Ui::SendPendingMoveResizeEvents(&_streamed->controls); } -void OverlayWidget::onVideoPauseResume() { - if (!_gif) return; +void OverlayWidget::playbackControlsPlay() { + playbackPauseResume(); +} - if (auto item = App::histItemById(_msgid)) { - if (_gif->state() == Media::Clip::State::Error) { +void OverlayWidget::playbackControlsPause() { + playbackPauseResume(); +} + +void OverlayWidget::playbackControlsToFullScreen() { + playbackToggleFullScreen(); +} + +void OverlayWidget::playbackControlsFromFullScreen() { + playbackToggleFullScreen(); +} + +void OverlayWidget::playbackPauseResume() { + Expects(_streamed != nullptr); + + if (const auto item = App::histItemById(_msgid)) { + if (_streamed->player.failed()) { displayDocument(_doc, item); - } else if (_gif->state() == Media::Clip::State::Finished) { - restartVideoAtSeekPosition(0); + } else if (_streamed->player.finished()) { + restartAtSeekPosition(0); } else { - toggleVideoPaused(); + togglePauseResume(); } } else { - stopGif(); + clearStreaming(); updateControls(); update(); } } -void OverlayWidget::toggleVideoPaused() { - _gif->pauseResumeVideo(); - _videoPaused = _gif->videoPaused(); - if (_videoIsSilent) { - updateSilentVideoPlaybackState(); +void OverlayWidget::togglePauseResume() { + Expects(_streamed != nullptr); + + if (_streamed->player.paused()) { + _streamed->player.resume(); + } else { + _streamed->player.pause(); } } -void OverlayWidget::restartVideoAtSeekPosition(crl::time positionMs) { - // Seek works bad: it seeks to the next keyframe after positionMs. - // At least let user to seek to the beginning of the video. - if (positionMs < 1000 - && (!_videoDurationMs || (positionMs * 20 < _videoDurationMs))) { - positionMs = 0; - } +void OverlayWidget::restartAtSeekPosition(crl::time position) { + Expects(_streamed != nullptr); _autoplayVideoDocument = _doc; - if (_current.isNull()) { - auto rounding = (_doc && _doc->isVideoMessage()) ? ImageRoundRadius::Ellipse : ImageRoundRadius::None; - _current = _gif->current(_gif->width() / cIntRetinaFactor(), _gif->height() / cIntRetinaFactor(), _gif->width() / cIntRetinaFactor(), _gif->height() / cIntRetinaFactor(), rounding, RectPart::AllCorners, crl::now()); + if (_current.isNull() && videoShown()) { + _current = Images::PixmapFast(videoFrame()); } - _gif = Media::Clip::MakeReader(_doc, _msgid, [this](Media::Clip::Notification notification) { - clipCallback(notification); - }, Media::Clip::Reader::Mode::Video, positionMs); + auto options = Streaming::PlaybackOptions(); + options.position = position; + _streamed->player.play(options); - // Correct values will be set when gif gets inited. - _videoPaused = _videoIsSilent = _videoStopped = false; - _videoPositionMs = positionMs; - - Media::Player::TrackState state; - state.state = Media::Player::State::Playing; - state.position = _videoPositionMs; - state.length = _videoDurationMs; - state.frequency = _videoFrequencyMs; - updateVideoPlaybackState(state); + _streamed->info.audio.state.position + = _streamed->info.video.state.position + = position; + updatePlaybackState(); } -void OverlayWidget::onVideoSeekProgress(crl::time positionMs) { - if (!_videoPaused && !_videoStopped) { - onVideoPauseResume(); +void OverlayWidget::playbackControlsSeekProgress(crl::time position) { + Expects(_streamed != nullptr); + + if (!_streamed->player.paused() && !_streamed->player.finished()) { + playbackControlsPause(); } } -void OverlayWidget::onVideoSeekFinished(crl::time positionMs) { - restartVideoAtSeekPosition(positionMs); +void OverlayWidget::playbackControlsSeekFinished(crl::time position) { + restartAtSeekPosition(position); } -void OverlayWidget::onVideoVolumeChanged(float64 volume) { +void OverlayWidget::playbackControlsVolumeChanged(float64 volume) { Global::SetVideoVolume(volume); updateMixerVideoVolume(); Global::RefVideoVolumeChanged().notify(); } -void OverlayWidget::onVideoToggleFullScreen() { - if (!_clipController) return; +void OverlayWidget::playbackToggleFullScreen() { + Expects(_streamed != nullptr); + if (!videoShown()) { + return; + } _fullScreenVideo = !_fullScreenVideo; if (_fullScreenVideo) { _fullScreenZoomCache = _zoom; setZoomLevel(ZoomToScreenLevel); } else { setZoomLevel(_fullScreenZoomCache); - _clipController->showAnimated(); + _streamed->controls.showAnimated(); } - _clipController->setInFullScreen(_fullScreenVideo); + _streamed->controls.setInFullScreen(_fullScreenVideo); updateControls(); update(); } -void OverlayWidget::onVideoPlayProgress(const AudioMsgId &audioId) { - if (!_gif || _gif->audioMsgId() != audioId) { - return; - } +void OverlayWidget::updatePlaybackState() { + Expects(_streamed != nullptr); - auto state = Media::Player::mixer()->currentState(AudioMsgId::Type::Video); - if (state.id == _gif->audioMsgId()) { - if (state.length) { - updateVideoPlaybackState(state); - } - Core::App().updateNonIdle(); + auto state = Player::TrackState(); + state.state = _streamed->player.finished() + ? Player::State::StoppedAtEnd + : _streamed->player.paused() + ? Player::State::Paused + : Player::State::Playing; + state.position = streamedPosition(); + state.length = streamedDuration(); + state.frequency = kMsFrequency; + if (state.position != kTimeUnknown && state.length != kTimeUnknown) { + _streamed->controls.updatePlayback(state); } } -void OverlayWidget::updateVideoPlaybackState(const Media::Player::TrackState &state) { - if (state.frequency) { - if (Media::Player::IsStopped(state.state)) { - _videoStopped = true; - } - _clipController->updatePlayback(state); - } else { // Audio has stopped already. - _videoIsSilent = true; - updateSilentVideoPlaybackState(); - } -} - -void OverlayWidget::updateSilentVideoPlaybackState() { - Media::Player::TrackState state; - if (_videoPaused) { - state.state = Media::Player::State::Paused; - } else if (_videoPositionMs == _videoDurationMs) { - state.state = Media::Player::State::StoppedAtEnd; - } else { - state.state = Media::Player::State::Playing; - } - state.position = _videoPositionMs; - state.length = _videoDurationMs; - state.frequency = _videoFrequencyMs; - updateVideoPlaybackState(state); -} - void OverlayWidget::validatePhotoImage(Image *image, bool blurred) { if (!image || !image->loaded()) { if (!blurred) { @@ -2091,7 +2158,7 @@ void OverlayWidget::validatePhotoImage(Image *image, bool blurred) { return; } const auto w = _width * cIntRetinaFactor(); - const auto h = int((_photo->height() * (qreal(w) / qreal(_photo->width()))) + 0.9999); + const auto h = _height * cIntRetinaFactor(); _current = image->pixNoCache( fileOrigin(), w, @@ -2114,11 +2181,17 @@ void OverlayWidget::validatePhotoCurrentImage() { } void OverlayWidget::paintEvent(QPaintEvent *e) { - QRect r(e->rect()); - QRegion region(e->region()); - QVector rs(region.rects()); + const auto r = e->rect(); + const auto ®ion = e->region(); + const auto rects = region.rects(); + + const auto contentShown = _photo || documentContentShown(); + const auto bgRects = contentShown + ? (region - contentRect()).rects() + : rects; auto ms = crl::now(); + const auto guard = gsl::finally([&] { LOG(("FULL FRAME: %1").arg(crl::now() - ms)); }); Painter p(this); @@ -2127,41 +2200,44 @@ void OverlayWidget::paintEvent(QPaintEvent *e) { p.setClipRegion(region); // main bg - QPainter::CompositionMode m = p.compositionMode(); + const auto m = p.compositionMode(); p.setCompositionMode(QPainter::CompositionMode_Source); - if (_fullScreenVideo) { - for (int i = 0, l = region.rectCount(); i < l; ++i) { - p.fillRect(rs.at(i), st::mediaviewVideoBg); - } - if (_doc && _doc->isVideoMessage()) { - p.setCompositionMode(m); - } - } else { - for (int i = 0, l = region.rectCount(); i < l; ++i) { - p.fillRect(rs.at(i), st::mediaviewBg); - } - p.setCompositionMode(m); + const auto bgColor = _fullScreenVideo ? st::mediaviewVideoBg : st::mediaviewBg; + for (const auto &rect : bgRects) { + p.fillRect(rect, bgColor); } + p.setCompositionMode(m); // photo if (_photo) { validatePhotoCurrentImage(); } p.setOpacity(1); - if (_photo || fileShown()) { - QRect imgRect(_x, _y, _w, _h); - if (imgRect.intersects(r)) { - const auto rounding = (_doc && _doc->isVideoMessage()) ? ImageRoundRadius::Ellipse : ImageRoundRadius::None; - const auto toDraw = (_current.isNull() && _gif) ? _gif->current(_gif->width() / cIntRetinaFactor(), _gif->height() / cIntRetinaFactor(), _gif->width() / cIntRetinaFactor(), _gif->height() / cIntRetinaFactor(), rounding, RectPart::AllCorners, ms) : _current; - if (!_gif && (!_doc || !_doc->getStickerLarge()) && (toDraw.hasAlpha() || toDraw.isNull())) { - p.fillRect(imgRect, _transparentBrush); - } - if (!toDraw.isNull()) { - if (toDraw.width() != _w * cIntRetinaFactor()) { + if (contentShown) { + const auto rect = contentRect(); + if (rect.intersects(r)) { + if (videoShown()) { + const auto image = videoFrame(); + if (image.width() != _w) { + //if (_fullScreenVideo) { + // const auto fill = rect.intersected(this->rect()); + // PaintImageProfile(p, image, rect, fill); + //} else { PainterHighQualityEnabler hq(p); - p.drawPixmap(QRect(_x, _y, _w, _h), toDraw); + p.drawImage(rect, image); + //} } else { - p.drawPixmap(_x, _y, toDraw); + p.drawImage(rect.topLeft(), image); + } + } else if (!_current.isNull()) { + if ((!_doc || !_doc->getStickerLarge()) && _current.hasAlpha()) { + p.fillRect(rect, _transparentBrush); + } + if (_current.width() != _w * cIntRetinaFactor()) { + PainterHighQualityEnabler hq(p); + p.drawPixmap(rect, _current); + } else { + p.drawPixmap(rect.topLeft(), _current); } } @@ -2192,39 +2268,37 @@ void OverlayWidget::paintEvent(QPaintEvent *e) { } else if (_doc) { paintDocRadialLoading(p, radial, radialOpacity); } - - if (_saveMsgStarted) { - auto ms = crl::now(); - float64 dt = float64(ms) - _saveMsgStarted, hidingDt = dt - st::mediaviewSaveMsgShowing - st::mediaviewSaveMsgShown; - if (dt < st::mediaviewSaveMsgShowing + st::mediaviewSaveMsgShown + st::mediaviewSaveMsgHiding) { - if (hidingDt >= 0 && _saveMsgOpacity.to() > 0.5) { - _saveMsgOpacity.start(0); - } - float64 progress = (hidingDt >= 0) ? (hidingDt / st::mediaviewSaveMsgHiding) : (dt / st::mediaviewSaveMsgShowing); - _saveMsgOpacity.update(qMin(progress, 1.), anim::linear); - if (_saveMsgOpacity.current() > 0) { - p.setOpacity(_saveMsgOpacity.current()); - App::roundRect(p, _saveMsg, st::mediaviewSaveMsgBg, MediaviewSaveCorners); - st::mediaviewSaveMsgCheck.paint(p, _saveMsg.topLeft() + st::mediaviewSaveMsgCheckPos, width()); - - p.setPen(st::mediaviewSaveMsgFg); - p.setTextPalette(st::mediaviewTextPalette); - _saveMsgText.draw(p, _saveMsg.x() + st::mediaviewSaveMsgPadding.left(), _saveMsg.y() + st::mediaviewSaveMsgPadding.top(), _saveMsg.width() - st::mediaviewSaveMsgPadding.left() - st::mediaviewSaveMsgPadding.right()); - p.restoreTextPalette(); - p.setOpacity(1); - } - if (!_blurred) { - auto nextFrame = (dt < st::mediaviewSaveMsgShowing || hidingDt >= 0) ? int(AnimationTimerDelta) : (st::mediaviewSaveMsgShowing + st::mediaviewSaveMsgShown + 1 - dt); - _saveMsgUpdater.start(nextFrame); - } - } else { - _saveMsgStarted = 0; + } + if (_saveMsgStarted && _saveMsg.intersects(r)) { + float64 dt = float64(ms) - _saveMsgStarted, hidingDt = dt - st::mediaviewSaveMsgShowing - st::mediaviewSaveMsgShown; + if (dt < st::mediaviewSaveMsgShowing + st::mediaviewSaveMsgShown + st::mediaviewSaveMsgHiding) { + if (hidingDt >= 0 && _saveMsgOpacity.to() > 0.5) { + _saveMsgOpacity.start(0); } + float64 progress = (hidingDt >= 0) ? (hidingDt / st::mediaviewSaveMsgHiding) : (dt / st::mediaviewSaveMsgShowing); + _saveMsgOpacity.update(qMin(progress, 1.), anim::linear); + if (_saveMsgOpacity.current() > 0) { + p.setOpacity(_saveMsgOpacity.current()); + App::roundRect(p, _saveMsg, st::mediaviewSaveMsgBg, MediaviewSaveCorners); + st::mediaviewSaveMsgCheck.paint(p, _saveMsg.topLeft() + st::mediaviewSaveMsgCheckPos, width()); + + p.setPen(st::mediaviewSaveMsgFg); + p.setTextPalette(st::mediaviewTextPalette); + _saveMsgText.draw(p, _saveMsg.x() + st::mediaviewSaveMsgPadding.left(), _saveMsg.y() + st::mediaviewSaveMsgPadding.top(), _saveMsg.width() - st::mediaviewSaveMsgPadding.left() - st::mediaviewSaveMsgPadding.right()); + p.restoreTextPalette(); + p.setOpacity(1); + } + if (!_blurred) { + auto nextFrame = (dt < st::mediaviewSaveMsgShowing || hidingDt >= 0) ? int(AnimationTimerDelta) : (st::mediaviewSaveMsgShowing + st::mediaviewSaveMsgShown + 1 - dt); + _saveMsgUpdater.start(nextFrame); + } + } else { + _saveMsgStarted = 0; } } } else if (_themePreviewShown) { paintThemePreview(p, r); - } else { + } else if (documentBubbleShown()) { if (_docRect.intersects(r)) { p.fillRect(_docRect, st::mediaviewFileBg); if (_docIconRect.intersects(r)) { @@ -2273,8 +2347,8 @@ void OverlayWidget::paintEvent(QPaintEvent *e) { auto o = overLevel(OverLeftNav); if (o > 0) { p.setOpacity(o * co); - for (int i = 0, l = region.rectCount(); i < l; ++i) { - auto fill = _leftNav.intersected(rs.at(i)); + for (const auto &rect : rects) { + const auto fill = _leftNav.intersected(rect); if (!fill.isEmpty()) p.fillRect(fill, st::mediaviewControlBg); } } @@ -2289,8 +2363,8 @@ void OverlayWidget::paintEvent(QPaintEvent *e) { auto o = overLevel(OverRightNav); if (o > 0) { p.setOpacity(o * co); - for (int i = 0, l = region.rectCount(); i < l; ++i) { - auto fill = _rightNav.intersected(rs.at(i)); + for (const auto &rect : rects) { + const auto fill = _rightNav.intersected(rect); if (!fill.isEmpty()) p.fillRect(fill, st::mediaviewControlBg); } } @@ -2305,8 +2379,8 @@ void OverlayWidget::paintEvent(QPaintEvent *e) { auto o = overLevel(OverClose); if (o > 0) { p.setOpacity(o * co); - for (int i = 0, l = region.rectCount(); i < l; ++i) { - auto fill = _closeNav.intersected(rs.at(i)); + for (const auto &rect : rects) { + const auto fill = _closeNav.intersected(rect); if (!fill.isEmpty()) p.fillRect(fill, st::mediaviewControlBg); } } @@ -2406,14 +2480,14 @@ void OverlayWidget::paintEvent(QPaintEvent *e) { } void OverlayWidget::checkGroupThumbsAnimation() { - if (_groupThumbs && (!_gif || _gif->started())) { + if (_groupThumbs && (!_streamed || _streamed->player.ready())) { _groupThumbs->checkForAnimationStart(); } } void OverlayWidget::paintDocRadialLoading(Painter &p, bool radial, float64 radialOpacity) { float64 o = overLevel(OverIcon); - if (radial || (_doc && !_doc->loaded())) { + if (radial || (_doc && !_doc->loaded() && !_streamed)) { QRect inner(QPoint(_docIconRect.x() + ((_docIconRect.width() - st::radialSize.width()) / 2), _docIconRect.y() + ((_docIconRect.height() - st::radialSize.height()) / 2)), st::radialSize); p.setPen(Qt::NoPen); @@ -2495,18 +2569,17 @@ void OverlayWidget::paintThemePreview(Painter &p, QRect clip) { void OverlayWidget::keyPressEvent(QKeyEvent *e) { const auto ctrl = e->modifiers().testFlag(Qt::ControlModifier); - if (_clipController) { + if (_streamed) { // Ctrl + F for full screen toggle is in eventFilter(). const auto toggleFull = (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) && (e->modifiers().testFlag(Qt::AltModifier) || ctrl); if (toggleFull) { - onVideoToggleFullScreen(); + playbackToggleFullScreen(); return; - } - if (_fullScreenVideo) { + } else if (_fullScreenVideo) { if (e->key() == Qt::Key_Escape) { - onVideoToggleFullScreen(); + playbackToggleFullScreen(); } else if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return || e->key() == Qt::Key_Space) { - onVideoPauseResume(); + playbackPauseResume(); } return; } @@ -2522,10 +2595,10 @@ void OverlayWidget::keyPressEvent(QKeyEvent *e) { } else if (e->key() == Qt::Key_Copy || (e->key() == Qt::Key_C && ctrl)) { onCopy(); } else if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return || e->key() == Qt::Key_Space) { - if (_doc && !_doc->loading() && (fileBubbleShown() || !_doc->loaded())) { + if (_doc && !_doc->loading() && (documentBubbleShown() || !_doc->loaded())) { onDocClick(); - } else if (_doc && (_doc->isVideoFile() || _doc->isVideoMessage())) { - onVideoPauseResume(); + } else if (_streamed) { + playbackPauseResume(); } } else if (e->key() == Qt::Key_Left) { if (_controlsHideTimer.isActive()) { @@ -2587,8 +2660,11 @@ void OverlayWidget::setZoomLevel(int newZoom) { if (_zoom == newZoom) return; float64 nx, ny, z = (_zoom == ZoomToScreenLevel) ? _zoomToScreen : _zoom; - _w = gifShown() ? ConvertScale(_gif->width()) : (ConvertScale(_current.width()) / cIntRetinaFactor()); - _h = gifShown() ? ConvertScale(_gif->height()) : (ConvertScale(_current.height()) / cIntRetinaFactor()); + const auto contentSize = videoShown() + ? ConvertScale(videoSize()) + : ConvertScale(_current.size() / cIntRetinaFactor()); + _w = contentSize.width(); + _h = contentSize.height(); if (z >= 0) { nx = (_x - width() / 2.) / (z + 1); ny = (_y - height() / 2.) / (z + 1); @@ -2733,7 +2809,7 @@ bool OverlayWidget::moveToEntity(const Entity &entity, int preloadDelta) { } else { setContext(std::nullopt); } - stopGif(); + clearStreaming(); if (auto photo = base::get_if>(&entity.data)) { displayPhoto(*photo, entity.item); } else if (auto document = base::get_if>(&entity.data)) { @@ -2826,8 +2902,8 @@ void OverlayWidget::mouseDoubleClickEvent(QMouseEvent *e) { updateOver(e->pos()); if (_over == OverVideo) { - onVideoToggleFullScreen(); - onVideoPauseResume(); + playbackToggleFullScreen(); + playbackPauseResume(); } else { e->ignore(); return OverlayParent::mouseDoubleClickEvent(e); @@ -2968,14 +3044,14 @@ void OverlayWidget::updateOver(QPoint pos) { updateOverState(OverHeader); } else if (_saveVisible && _saveNav.contains(pos)) { updateOverState(OverSave); - } else if (_doc && fileBubbleShown() && _docIconRect.contains(pos)) { + } else if (_doc && documentBubbleShown() && _docIconRect.contains(pos)) { updateOverState(OverIcon); } else if (_moreNav.contains(pos)) { updateOverState(OverMore); } else if (_closeNav.contains(pos)) { updateOverState(OverClose); - } else if (_doc && fileShown() && QRect(_x, _y, _w, _h).contains(pos)) { - if ((_doc->isVideoFile() || _doc->isVideoMessage()) && _gif) { + } else if (documentContentShown() && contentRect().contains(pos)) { + if ((_doc->isVideoFile() || _doc->isVideoMessage()) && _streamed) { updateOverState(OverVideo); } else if (!_doc->loaded()) { updateOverState(OverIcon); @@ -3013,7 +3089,9 @@ void OverlayWidget::mouseReleaseEvent(QMouseEvent *e) { } else if (_over == OverClose && _down == OverClose) { close(); } else if (_over == OverVideo && _down == OverVideo) { - onVideoPauseResume(); + if (_streamed) { + playbackPauseResume(); + } } else if (_pressed) { if (_dragging) { if (_dragging > 0) { @@ -3029,7 +3107,10 @@ void OverlayWidget::mouseReleaseEvent(QMouseEvent *e) { if (!_themePreviewRect.contains(e->pos())) { close(); } - } else if (!_doc || fileShown() || !_docRect.contains(e->pos())) { + } else if (!_doc + || documentContentShown() + || !documentBubbleShown() + || !_docRect.contains(e->pos())) { close(); } } @@ -3150,8 +3231,8 @@ bool OverlayWidget::eventFilter(QObject *obj, QEvent *e) { if (type == QEvent::ShortcutOverride) { const auto keyEvent = static_cast(e); const auto ctrl = keyEvent->modifiers().testFlag(Qt::ControlModifier); - if (keyEvent->key() == Qt::Key_F && ctrl) { - onVideoToggleFullScreen(); + if (keyEvent->key() == Qt::Key_F && ctrl && _streamed) { + playbackToggleFullScreen(); } return true; } @@ -3198,15 +3279,15 @@ void OverlayWidget::setVisibleHook(bool visible) { // QOpenGLWidget can't properly destroy a child widget if // it is hidden exactly after that, so it must be repainted // before it is hidden without the child widget. - if (!isHidden() && _clipController) { - _clipController.destroy(); + if (!isHidden() && _streamed) { + _streamed->controls.hide(); _wasRepainted = false; repaint(); if (!_wasRepainted) { // Qt has some optimization to prevent too frequent repaints. // If the previous repaint was less than 1/60 second it silently // converts repaint() call to an update() call. But we have to - // repaint right now, before hide(), with _clipController destroyed. + // repaint right now, before hide(), with _streamingControls destroyed. auto event = QEvent(QEvent::UpdateRequest); QApplication::sendEvent(this, &event); } @@ -3220,7 +3301,7 @@ void OverlayWidget::setVisibleHook(bool visible) { } else { QCoreApplication::instance()->removeEventFilter(this); - stopGif(); + clearStreaming(); destroyThemePreview(); _radial.stop(); _current = QPixmap(); diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index 7cd9ece42..b9279f839 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_shared_media.h" #include "data/data_user_photos.h" #include "data/data_web_page.h" +#include "media/view/media_view_playback_controls.h" namespace Ui { class PopupMenu; @@ -35,9 +36,13 @@ namespace Media { namespace Player { struct TrackState; } // namespace Player -namespace Clip { -class Controller; -} // namespace Clip +namespace Streaming { +struct Update; +struct Error; +} // namespace Streaming +} // namespace Media + +namespace Media { namespace View { class GroupThumbs; @@ -48,7 +53,11 @@ using OverlayParent = Ui::RpWidgetWrap; using OverlayParent = Ui::RpWidget; #endif // Q_OS_MAC && !OS_MAC_OLD -class OverlayWidget final : public OverlayParent, private base::Subscriber, public ClickHandlerHost { +class OverlayWidget final + : public OverlayParent + , private base::Subscriber + , public ClickHandlerHost + , private PlaybackControls::Delegate { Q_OBJECT public: @@ -70,7 +79,6 @@ public: void activateControls(); void onDocClick(); - void clipCallback(Media::Clip::Notification notification); PeerData *ui_getPeerForMouseAction(); void clearData(); @@ -105,14 +113,9 @@ private slots: void updateImage(); - void onVideoPauseResume(); - void onVideoSeekProgress(crl::time positionMs); - void onVideoSeekFinished(crl::time positionMs); - void onVideoVolumeChanged(float64 volume); - void onVideoToggleFullScreen(); - void onVideoPlayProgress(const AudioMsgId &audioId); - private: + struct Streamed; + enum OverState { OverNone, OverLeftNav, @@ -149,6 +152,16 @@ private: void setVisibleHook(bool visible) override; + void playbackControlsPlay() override; + void playbackControlsPause() override; + void playbackControlsSeekProgress(crl::time position) override; + void playbackControlsSeekFinished(crl::time position) override; + void playbackControlsVolumeChanged(float64 volume) override; + void playbackControlsToFullScreen() override; + void playbackControlsFromFullScreen() override; + void playbackPauseResume(); + void playbackToggleFullScreen(); + void updateOver(QPoint mpos); void moveToScreen(); bool moveToNext(int delta); @@ -212,17 +225,18 @@ private: void updateCursor(); void setZoomLevel(int newZoom); - void updateVideoPlaybackState(const Media::Player::TrackState &state); - void updateSilentVideoPlaybackState(); - void restartVideoAtSeekPosition(crl::time positionMs); - void toggleVideoPaused(); + void updatePlaybackState(); + void restartAtSeekPosition(crl::time position); + void togglePauseResume(); - void createClipController(); void refreshClipControllerGeometry(); void refreshCaptionGeometry(); - void initAnimation(); - void createClipReader(); + void initStreaming(); + void initStreamingThumbnail(); + void createStreamingObjects(); + void handleStreamingUpdate(Streaming::Update &&update); + void handleStreamingError(Streaming::Error &&error); void initThemePreview(); void destroyThemePreview(); @@ -231,6 +245,8 @@ private: void documentUpdated(DocumentData *doc); void changingMsgId(not_null row, MsgId newId); + QRect contentRect() const; + // Radial animation interface. float64 radialProgress() const; bool radialLoading() const; @@ -262,6 +278,16 @@ private: void validatePhotoImage(Image *image, bool blurred); void validatePhotoCurrentImage(); + [[nodiscard]] bool videoShown() const; + [[nodiscard]] QSize videoSize() const; + [[nodiscard]] bool videoIsGifv() const; + [[nodiscard]] QImage videoFrame() const; + [[nodiscard]] crl::time streamedPosition() const; + [[nodiscard]] crl::time streamedDuration() const; + [[nodiscard]] bool documentContentShown() const; + [[nodiscard]] bool documentBubbleShown() const; + void clearStreaming(); + QBrush _transparentBrush; PhotoData *_photo = nullptr; @@ -285,12 +311,11 @@ private: QString _dateText; QString _headerText; - object_ptr _clipController = { nullptr }; DocumentData *_autoplayVideoDocument = nullptr; bool _fullScreenVideo = false; int _fullScreenZoomCache = 0; - std::unique_ptr _groupThumbs; + std::unique_ptr _groupThumbs; QRect _groupThumbsRect; int _groupThumbsAvailableWidth = 0; int _groupThumbsLeft = 0; @@ -298,9 +323,8 @@ private: Text _caption; QRect _captionRect; - crl::time _animStarted; - int _width = 0; + int _height = 0; int _x = 0, _y = 0, _w = 0, _h = 0; int _xStart = 0, _yStart = 0; int _zoom = 0; // < 0 - out, 0 - none, > 0 - in @@ -309,21 +333,9 @@ private: bool _pressed = false; int32 _dragging = 0; QPixmap _current; - Media::Clip::ReaderPointer _gif; bool _blurred = true; - // Video without audio stream playback information. - bool _videoIsSilent = false; - bool _videoPaused = false; - bool _videoStopped = false; - crl::time _videoPositionMs = 0; - crl::time _videoDurationMs = 0; - int32 _videoFrequencyMs = 1000; // 1000 ms per second. - - bool fileShown() const; - bool gifShown() const; - bool fileBubbleShown() const; - void stopGif(); + std::unique_ptr _streamed; const style::icon *_docIcon = nullptr; style::color _docIconColor; diff --git a/Telegram/SourceFiles/media/view/media_clip_controller.cpp b/Telegram/SourceFiles/media/view/media_view_playback_controls.cpp similarity index 67% rename from Telegram/SourceFiles/media/view/media_clip_controller.cpp rename to Telegram/SourceFiles/media/view/media_view_playback_controls.cpp index 6a5da7e5e..525ad40fd 100644 --- a/Telegram/SourceFiles/media/view/media_clip_controller.cpp +++ b/Telegram/SourceFiles/media/view/media_view_playback_controls.cpp @@ -5,10 +5,10 @@ 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/view/media_clip_controller.h" +#include "media/view/media_view_playback_controls.h" #include "media/audio/media_audio.h" -#include "media/view/media_clip_playback.h" +#include "media/view/media_view_playback_progress.h" #include "ui/widgets/labels.h" #include "ui/widgets/continuous_sliders.h" #include "ui/effects/fade_animation.h" @@ -17,69 +17,89 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_mediaview.h" namespace Media { -namespace Clip { +namespace View { -Controller::Controller(QWidget *parent) : TWidget(parent) +PlaybackControls::PlaybackControls(QWidget *parent, not_null delegate) +: RpWidget(parent) +, _delegate(delegate) , _playPauseResume(this, st::mediaviewPlayButton) , _playbackSlider(this, st::mediaviewPlayback) -, _playback(std::make_unique()) +, _playbackProgress(std::make_unique()) , _volumeController(this, st::mediaviewPlayback) , _fullScreenToggle(this, st::mediaviewFullScreenButton) , _playedAlready(this, st::mediaviewPlayProgressLabel) , _toPlayLeft(this, st::mediaviewPlayProgressLabel) , _fadeAnimation(std::make_unique(this)) { _fadeAnimation->show(); - _fadeAnimation->setFinishedCallback([this] { fadeFinished(); }); - _fadeAnimation->setUpdatedCallback([this](float64 opacity) { fadeUpdated(opacity); }); + _fadeAnimation->setFinishedCallback([=] { + fadeFinished(); + }); + _fadeAnimation->setUpdatedCallback([=](float64 opacity) { + fadeUpdated(opacity); + }); _volumeController->setValue(Global::VideoVolume()); _volumeController->setChangeProgressCallback([=](float64 value) { - volumeChanged(value); + _delegate->playbackControlsVolumeChanged(value); }); - //_volumeController->setChangeFinishedCallback(); - connect(_playPauseResume, SIGNAL(clicked()), this, SIGNAL(playPressed())); - connect(_fullScreenToggle, SIGNAL(clicked()), this, SIGNAL(toFullScreenPressed())); - //connect(_volumeController, SIGNAL(volumeChanged(float64)), this, SIGNAL(volumeChanged(float64))); + _playPauseResume->addClickHandler([=] { + if (_showPause) { + _delegate->playbackControlsPause(); + } else { + _delegate->playbackControlsPlay(); + } + }); + _fullScreenToggle->addClickHandler([=] { + if (_inFullScreen) { + _delegate->playbackControlsFromFullScreen(); + } else { + _delegate->playbackControlsToFullScreen(); + } + }); - _playback->setInLoadingStateChangedCallback([this](bool loading) { + _playbackProgress->setInLoadingStateChangedCallback([=](bool loading) { _playbackSlider->setDisabled(loading); }); - _playback->setValueChangedCallback([this](float64 value) { + _playbackProgress->setValueChangedCallback([=](float64 value) { _playbackSlider->setValue(value); }); - _playbackSlider->setChangeProgressCallback([this](float64 value) { - _playback->setValue(value, false); - handleSeekProgress(value); // This may destroy Controller. + _playbackSlider->setChangeProgressCallback([=](float64 value) { + _playbackProgress->setValue(value, false); + + // This may destroy PlaybackControls. + handleSeekProgress(value); }); - _playbackSlider->setChangeFinishedCallback([this](float64 value) { - _playback->setValue(value, false); + _playbackSlider->setChangeFinishedCallback([=](float64 value) { + _playbackProgress->setValue(value, false); handleSeekFinished(value); }); } -void Controller::handleSeekProgress(float64 progress) { +void PlaybackControls::handleSeekProgress(float64 progress) { if (!_lastDurationMs) return; auto positionMs = snap(static_cast(progress * _lastDurationMs), 0LL, _lastDurationMs); if (_seekPositionMs != positionMs) { _seekPositionMs = positionMs; refreshTimeTexts(); - emit seekProgress(positionMs); // This may destroy Controller. + + // This may destroy PlaybackControls. + _delegate->playbackControlsSeekProgress(positionMs); } } -void Controller::handleSeekFinished(float64 progress) { +void PlaybackControls::handleSeekFinished(float64 progress) { if (!_lastDurationMs) return; auto positionMs = snap(static_cast(progress * _lastDurationMs), 0LL, _lastDurationMs); _seekPositionMs = -1; - emit seekFinished(positionMs); + _delegate->playbackControlsSeekFinished(positionMs); refreshTimeTexts(); } template -void Controller::startFading(Callback start) { +void PlaybackControls::startFading(Callback start) { if (!_fadeAnimation->animating()) { showChildren(); _playbackSlider->disablePaint(true); @@ -103,45 +123,42 @@ void Controller::startFading(Callback start) { _volumeController->disablePaint(false); } -void Controller::showAnimated() { +void PlaybackControls::showAnimated() { startFading([this]() { _fadeAnimation->fadeIn(st::mediaviewShowDuration); }); } -void Controller::hideAnimated() { +void PlaybackControls::hideAnimated() { startFading([this]() { _fadeAnimation->fadeOut(st::mediaviewHideDuration); }); } -void Controller::fadeFinished() { +void PlaybackControls::fadeFinished() { fadeUpdated(_fadeAnimation->visible() ? 1. : 0.); } -void Controller::fadeUpdated(float64 opacity) { +void PlaybackControls::fadeUpdated(float64 opacity) { _playbackSlider->setFadeOpacity(opacity); _volumeController->setFadeOpacity(opacity); } -void Controller::updatePlayback(const Player::TrackState &state) { +void PlaybackControls::updatePlayback(const Player::TrackState &state) { updatePlayPauseResumeState(state); - _playback->updateState(state); + _playbackProgress->updateState(state); updateTimeTexts(state); } -void Controller::updatePlayPauseResumeState(const Player::TrackState &state) { +void PlaybackControls::updatePlayPauseResumeState(const Player::TrackState &state) { auto showPause = ShowPauseIcon(state.state) || (_seekPositionMs >= 0); if (showPause != _showPause) { - disconnect(_playPauseResume, SIGNAL(clicked()), this, _showPause ? SIGNAL(pausePressed()) : SIGNAL(playPressed())); _showPause = showPause; - connect(_playPauseResume, SIGNAL(clicked()), this, _showPause ? SIGNAL(pausePressed()) : SIGNAL(playPressed())); - _playPauseResume->setIconOverride(_showPause ? &st::mediaviewPauseIcon : nullptr, _showPause ? &st::mediaviewPauseIconOver : nullptr); } } -void Controller::updateTimeTexts(const Player::TrackState &state) { +void PlaybackControls::updateTimeTexts(const Player::TrackState &state) { qint64 position = 0, length = state.length; if (Player::IsStoppedAtEnd(state.state)) { @@ -166,7 +183,7 @@ void Controller::updateTimeTexts(const Player::TrackState &state) { } } -void Controller::refreshTimeTexts() { +void PlaybackControls::refreshTimeTexts() { auto alreadyChanged = false, leftChanged = false; auto timeAlready = _timeAlready; auto timeLeft = _timeLeft; @@ -189,16 +206,16 @@ void Controller::refreshTimeTexts() { } } -void Controller::setInFullScreen(bool inFullScreen) { - _fullScreenToggle->setIconOverride(inFullScreen ? &st::mediaviewFullScreenOutIcon : nullptr, inFullScreen ? &st::mediaviewFullScreenOutIconOver : nullptr); - disconnect(_fullScreenToggle, SIGNAL(clicked()), this, SIGNAL(toFullScreenPressed())); - disconnect(_fullScreenToggle, SIGNAL(clicked()), this, SIGNAL(fromFullScreenPressed())); - - auto handler = inFullScreen ? SIGNAL(fromFullScreenPressed()) : SIGNAL(toFullScreenPressed()); - connect(_fullScreenToggle, SIGNAL(clicked()), this, handler); +void PlaybackControls::setInFullScreen(bool inFullScreen) { + if (_inFullScreen != inFullScreen) { + _inFullScreen = inFullScreen; + _fullScreenToggle->setIconOverride( + _inFullScreen ? &st::mediaviewFullScreenOutIcon : nullptr, + _inFullScreen ? &st::mediaviewFullScreenOutIconOver : nullptr); + } } -void Controller::resizeEvent(QResizeEvent *e) { +void PlaybackControls::resizeEvent(QResizeEvent *e) { int playTop = (height() - _playPauseResume->height()) / 2; _playPauseResume->moveToLeft(st::mediaviewPlayPauseLeft, playTop); @@ -216,7 +233,7 @@ void Controller::resizeEvent(QResizeEvent *e) { _toPlayLeft->moveToRight(width() - (st::mediaviewPlayPauseLeft + _playPauseResume->width() + playTop) - playbackWidth, st::mediaviewPlayProgressTop); } -void Controller::paintEvent(QPaintEvent *e) { +void PlaybackControls::paintEvent(QPaintEvent *e) { Painter p(this); if (_fadeAnimation->paint(p)) { @@ -231,11 +248,11 @@ void Controller::paintEvent(QPaintEvent *e) { App::roundRect(p, rect(), st::mediaviewSaveMsgBg, MediaviewSaveCorners); } -void Controller::mousePressEvent(QMouseEvent *e) { +void PlaybackControls::mousePressEvent(QMouseEvent *e) { e->accept(); // Don't pass event to the Media::View::OverlayWidget. } -Controller::~Controller() = default; +PlaybackControls::~PlaybackControls() = default; -} // namespace Clip +} // namespace View } // namespace Media diff --git a/Telegram/SourceFiles/media/view/media_clip_controller.h b/Telegram/SourceFiles/media/view/media_view_playback_controls.h similarity index 68% rename from Telegram/SourceFiles/media/view/media_clip_controller.h rename to Telegram/SourceFiles/media/view/media_view_playback_controls.h index 009815475..94c17b24d 100644 --- a/Telegram/SourceFiles/media/view/media_clip_controller.h +++ b/Telegram/SourceFiles/media/view/media_view_playback_controls.h @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "ui/rp_widget.h" + namespace Ui { class LabelSimple; class FadeAnimation; @@ -19,15 +21,24 @@ namespace Player { struct TrackState; } // namespace Player -namespace Clip { +namespace View { -class Playback; - -class Controller : public TWidget { - Q_OBJECT +class PlaybackProgress; +class PlaybackControls : public Ui::RpWidget { public: - Controller(QWidget *parent); + class Delegate { + public: + virtual void playbackControlsPlay() = 0; + virtual void playbackControlsPause() = 0; + virtual void playbackControlsSeekProgress(crl::time position) = 0; + virtual void playbackControlsSeekFinished(crl::time position) = 0; + virtual void playbackControlsVolumeChanged(float64 volume) = 0; + virtual void playbackControlsToFullScreen() = 0; + virtual void playbackControlsFromFullScreen() = 0; + }; + + PlaybackControls(QWidget *parent, not_null delegate); void showAnimated(); void hideAnimated(); @@ -35,16 +46,7 @@ public: void updatePlayback(const Player::TrackState &state); void setInFullScreen(bool inFullScreen); - ~Controller(); - -signals: - void playPressed(); - void pausePressed(); - void seekProgress(crl::time positionMs); - void seekFinished(crl::time positionMs); - void volumeChanged(float64 volume); - void toFullScreenPressed(); - void fromFullScreenPressed(); + ~PlaybackControls(); protected: void resizeEvent(QResizeEvent *e) override; @@ -64,6 +66,9 @@ private: void updateTimeTexts(const Player::TrackState &state); void refreshTimeTexts(); + not_null _delegate; + + bool _inFullScreen = false; bool _showPause = false; bool _childrenHidden = false; QString _timeAlready, _timeLeft; @@ -72,7 +77,7 @@ private: object_ptr _playPauseResume; object_ptr _playbackSlider; - std::unique_ptr _playback; + std::unique_ptr _playbackProgress; object_ptr _volumeController; object_ptr _fullScreenToggle; object_ptr _playedAlready; @@ -82,5 +87,5 @@ private: }; -} // namespace Clip +} // namespace View } // namespace Media diff --git a/Telegram/SourceFiles/media/view/media_clip_playback.cpp b/Telegram/SourceFiles/media/view/media_view_playback_progress.cpp similarity index 80% rename from Telegram/SourceFiles/media/view/media_clip_playback.cpp rename to Telegram/SourceFiles/media/view/media_view_playback_progress.cpp index e9a550ce2..57f7f025d 100644 --- a/Telegram/SourceFiles/media/view/media_clip_playback.cpp +++ b/Telegram/SourceFiles/media/view/media_view_playback_progress.cpp @@ -5,23 +5,23 @@ 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/view/media_clip_playback.h" +#include "media/view/media_view_playback_progress.h" #include "media/audio/media_audio.h" #include "styles/style_mediaview.h" namespace Media { -namespace Clip { +namespace View { namespace { constexpr auto kPlaybackAnimationDurationMs = crl::time(200); } // namespace -Playback::Playback() : _a_value(animation(this, &Playback::step_value)) { +PlaybackProgress::PlaybackProgress() : _a_value(animation(this, &PlaybackProgress::step_value)) { } -void Playback::updateState(const Player::TrackState &state) { +void PlaybackProgress::updateState(const Player::TrackState &state) { qint64 position = 0, length = state.length; auto wasInLoadingState = _inLoadingState; @@ -60,7 +60,7 @@ void Playback::updateState(const Player::TrackState &state) { } } -void Playback::updateLoadingState(float64 progress) { +void PlaybackProgress::updateLoadingState(float64 progress) { if (!_inLoadingState) { _inLoadingState = true; if (_inLoadingStateChanged) { @@ -71,16 +71,16 @@ void Playback::updateLoadingState(float64 progress) { setValue(progress, animated); } -float64 Playback::value() const { +float64 PlaybackProgress::value() const { return qMin(a_value.current(), 1.); } -float64 Playback::value(crl::time ms) { +float64 PlaybackProgress::value(crl::time ms) { _a_value.step(ms); return value(); } -void Playback::setValue(float64 value, bool animated) { +void PlaybackProgress::setValue(float64 value, bool animated) { if (animated) { a_value.start(value); _a_value.start(); @@ -93,7 +93,7 @@ void Playback::setValue(float64 value, bool animated) { } } -void Playback::step_value(float64 ms, bool timer) { +void PlaybackProgress::step_value(float64 ms, bool timer) { auto dt = anim::Disabled() ? 1. : (ms / kPlaybackAnimationDurationMs); if (dt >= 1.) { _a_value.stop(); @@ -106,5 +106,5 @@ void Playback::step_value(float64 ms, bool timer) { } } -} // namespace Clip +} // namespace View } // namespace Media diff --git a/Telegram/SourceFiles/media/view/media_clip_playback.h b/Telegram/SourceFiles/media/view/media_view_playback_progress.h similarity index 93% rename from Telegram/SourceFiles/media/view/media_clip_playback.h rename to Telegram/SourceFiles/media/view/media_view_playback_progress.h index c367a837c..55b577ae5 100644 --- a/Telegram/SourceFiles/media/view/media_clip_playback.h +++ b/Telegram/SourceFiles/media/view/media_view_playback_progress.h @@ -14,11 +14,11 @@ namespace Player { struct TrackState; } // namespace Player -namespace Clip { +namespace View { -class Playback { +class PlaybackProgress { public: - Playback(); + PlaybackProgress(); void setValueChangedCallback(Fn callback) { _valueChanged = std::move(callback); @@ -52,5 +52,5 @@ private: }; -} // namespace Clip +} // namespace View } // namespace Media diff --git a/Telegram/SourceFiles/overview/overview_layout.cpp b/Telegram/SourceFiles/overview/overview_layout.cpp index 7bf16c187..2823f48c2 100644 --- a/Telegram/SourceFiles/overview/overview_layout.cpp +++ b/Telegram/SourceFiles/overview/overview_layout.cpp @@ -849,7 +849,6 @@ bool Voice::updateStatusText() { statusSize = FileStatusSizeFailed; } else if (_data->loaded()) { statusSize = FileStatusSizeLoaded; - using State = Media::Player::State; auto state = Media::Player::mixer()->currentState(AudioMsgId::Type::Voice); if (state.id == AudioMsgId(_data, parent()->fullId(), state.id.playId()) && !Media::Player::IsStoppedOrStopping(state.state)) { statusSize = -1 - (state.position / state.frequency); @@ -1220,7 +1219,6 @@ bool Document::updateStatusText() { } else if (_data->loaded()) { if (_data->isSong()) { statusSize = FileStatusSizeLoaded; - using State = Media::Player::State; auto state = Media::Player::mixer()->currentState(AudioMsgId::Type::Song); if (state.id == AudioMsgId(_data, parent()->fullId()) && !Media::Player::IsStoppedOrStopping(state.state)) { statusSize = -1 - (state.position / state.frequency); diff --git a/Telegram/SourceFiles/rpl/variable.h b/Telegram/SourceFiles/rpl/variable.h index f318ecf7b..06f9e0e74 100644 --- a/Telegram/SourceFiles/rpl/variable.h +++ b/Telegram/SourceFiles/rpl/variable.h @@ -90,11 +90,11 @@ public: template < typename OtherType, - typename Error, + typename OtherError, typename Generator, typename = std::enable_if_t< std::is_assignable_v>> - variable(producer &&stream) { + variable(producer &&stream) { std::move(stream) | start_with_next([=](auto &&data) { assign(std::forward(data)); @@ -103,12 +103,12 @@ public: template < typename OtherType, - typename Error, + typename OtherError, typename Generator, typename = std::enable_if_t< std::is_assignable_v>> variable &operator=( - producer &&stream) { + producer &&stream) { _lifetime.destroy(); std::move(stream) | start_with_next([=](auto &&data) { diff --git a/Telegram/SourceFiles/settings.h b/Telegram/SourceFiles/settings.h index e3c680e7a..baee28c73 100644 --- a/Telegram/SourceFiles/settings.h +++ b/Telegram/SourceFiles/settings.h @@ -200,6 +200,10 @@ inline T ConvertScale(T value) { return ConvertScale(value, cScale()); } +inline QSize ConvertScale(QSize size) { + return QSize(ConvertScale(size.width()), ConvertScale(size.height())); +} + inline void SetScaleChecked(int scale) { const auto checked = (scale == kInterfaceScaleAuto) ? kInterfaceScaleAuto diff --git a/Telegram/gyp/telegram_sources.txt b/Telegram/gyp/telegram_sources.txt index 457882f06..748076f57 100644 --- a/Telegram/gyp/telegram_sources.txt +++ b/Telegram/gyp/telegram_sources.txt @@ -472,10 +472,10 @@ <(src_loc)/media/streaming/media_streaming_utility.h <(src_loc)/media/streaming/media_streaming_video_track.cpp <(src_loc)/media/streaming/media_streaming_video_track.h -<(src_loc)/media/view/media_clip_controller.cpp -<(src_loc)/media/view/media_clip_controller.h -<(src_loc)/media/view/media_clip_playback.cpp -<(src_loc)/media/view/media_clip_playback.h +<(src_loc)/media/view/media_view_playback_controls.cpp +<(src_loc)/media/view/media_view_playback_controls.h +<(src_loc)/media/view/media_view_playback_progress.cpp +<(src_loc)/media/view/media_view_playback_progress.h <(src_loc)/media/view/media_view_group_thumbs.cpp <(src_loc)/media/view/media_view_group_thumbs.h <(src_loc)/media/view/media_view_overlay_widget.cpp