diff --git a/Telegram/SourceFiles/media/player/media_player.style b/Telegram/SourceFiles/media/player/media_player.style index 5e5a3ec8b..b4af94564 100644 --- a/Telegram/SourceFiles/media/player/media_player.style +++ b/Telegram/SourceFiles/media/player/media_player.style @@ -29,6 +29,7 @@ MediaPlayerButton { MediaSpeedMenu { dropdown: DropdownMenu; + qualityMenu: Menu; activeCheck: icon; activeCheckSkip: pixels; sliderStyle: TextStyle; @@ -165,16 +166,20 @@ mediaPlayerMenu: DropdownMenu(defaultDropdownMenu) { } mediaPlayerMenuCheck: icon {{ "player/player_check", mediaPlayerActiveFg }}; +mediaPlayerSpeedMenuInner: Menu(menuWithIcons) { + separator: MenuSeparator(defaultMenuSeparator) { + padding: margins(0px, 4px, 0px, 4px); + width: 6px; + } + itemPadding: margins(54px, 7px, 54px, 9px); + itemFgDisabled: mediaPlayerActiveFg; +} mediaPlayerSpeedMenu: MediaSpeedMenu { dropdown: DropdownMenu(mediaPlayerMenu) { - menu: Menu(menuWithIcons) { - separator: MenuSeparator(defaultMenuSeparator) { - padding: margins(0px, 4px, 0px, 4px); - width: 6px; - } - itemPadding: margins(54px, 7px, 54px, 9px); - itemFgDisabled: mediaPlayerActiveFg; - } + menu: mediaPlayerSpeedMenuInner; + } + qualityMenu: Menu(mediaPlayerSpeedMenuInner) { + itemPadding: margins(17px, 7px, 54px, 9px); } activeCheck: mediaPlayerMenuCheck; activeCheckSkip: 8px; diff --git a/Telegram/SourceFiles/media/player/media_player_dropdown.cpp b/Telegram/SourceFiles/media/player/media_player_dropdown.cpp index 0a8b181fe..843ce1553 100644 --- a/Telegram/SourceFiles/media/player/media_player_dropdown.cpp +++ b/Telegram/SourceFiles/media/player/media_player_dropdown.cpp @@ -177,7 +177,8 @@ void FillSpeedMenu( not_null menu, const style::MediaSpeedMenu &st, rpl::producer value, - Fn callback) { + Fn callback, + bool onlySlider) { auto slider = base::make_unique_q( menu, st, @@ -198,6 +199,11 @@ void FillSpeedMenu( )); menu->addAction(std::move(slider)); + + if (onlySlider) { + return; + } + menu->addSeparator(&st.dropdown.menu.separator); struct SpeedPoint { @@ -693,7 +699,10 @@ SpeedController::SpeedController( not_null menuParent, Fn menuOverCallback, Fn value, - Fn change) + Fn change, + std::vector qualities, + Fn quality, + Fn changeQuality) : WithDropdownController( button, menuParent, @@ -702,7 +711,12 @@ SpeedController::SpeedController( std::move(menuOverCallback)) , _st(button->st()) , _lookup(std::move(value)) -, _change(std::move(change)) { +, _change(std::move(change)) +, _qualities(std::move(qualities)) +, _lookupQuality(std::move(quality)) +, _changeQuality(std::move(changeQuality)) { + Expects(_qualities.empty() || (_lookupQuality && _changeQuality)); + button->setClickedCallback([=] { toggleDefault(); save(); @@ -756,12 +770,63 @@ void SpeedController::save() { _saved.fire({}); } +void SpeedController::setQuality(int quality) { + _quality = quality; + _changeQuality(quality); +} + void SpeedController::fillMenu(not_null menu) { FillSpeedMenu( menu->menu(), _st.menu, _speedChanged.events_starting_with(speed()), - [=](float64 speed) { setSpeed(speed); save(); }); + [=](float64 speed) { setSpeed(speed); save(); }, + !_qualities.empty()); + if (_qualities.empty()) { + return; + } + _quality = _lookupQuality(); + const auto raw = menu->menu(); + const auto &st = _st.menu; + raw->addSeparator(&st.dropdown.menu.separator); + + const auto add = [&](int quality) { + const auto text = quality ? u"%1p"_q.arg(quality) : u"Original"_q; + auto action = base::make_unique_q( + raw, + st.qualityMenu, + Ui::Menu::CreateAction(raw, text, [=] { _changeQuality(quality); }), + nullptr, + nullptr); + const auto raw = action.get(); + const auto check = Ui::CreateChild(raw); + check->resize(st.activeCheck.size()); + check->paintRequest( + ) | rpl::start_with_next([check, icon = &st.activeCheck] { + auto p = QPainter(check); + icon->paint(p, 0, 0, check->width()); + }, check->lifetime()); + raw->sizeValue( + ) | rpl::start_with_next([=, skip = st.activeCheckSkip](QSize size) { + check->moveToRight( + skip, + (size.height() - check->height()) / 2, + size.width()); + }, check->lifetime()); + check->setAttribute(Qt::WA_TransparentForMouseEvents); + _quality.value( + ) | rpl::start_with_next([=](int now) { + const auto chosen = (now == quality); + raw->action()->setEnabled(!chosen); + check->setVisible(chosen); + }, raw->lifetime()); + menu->addAction(std::move(action)); + }; + + add(0); + for (const auto quality : _qualities) { + add(quality); + } } } // namespace Media::Player diff --git a/Telegram/SourceFiles/media/player/media_player_dropdown.h b/Telegram/SourceFiles/media/player/media_player_dropdown.h index 63197dbb8..7fcdc1f29 100644 --- a/Telegram/SourceFiles/media/player/media_player_dropdown.h +++ b/Telegram/SourceFiles/media/player/media_player_dropdown.h @@ -129,7 +129,10 @@ public: not_null menuParent, Fn menuOverCallback, Fn value, - Fn change); + Fn change, + std::vector qualities = {}, + Fn quality = nullptr, + Fn changeQuality = nullptr); [[nodiscard]] rpl::producer<> saved() const; @@ -141,6 +144,7 @@ private: [[nodiscard]] float64 lastNonDefaultSpeed() const; void toggleDefault(); void setSpeed(float64 newSpeed); + void setQuality(int quality); void save(); const style::MediaSpeedButton &_st; @@ -151,6 +155,11 @@ private: rpl::event_stream _speedChanged; rpl::event_stream<> _saved; + std::vector _qualities; + Fn _lookupQuality; + Fn _changeQuality; + rpl::variable _quality; + }; } // namespace Media::Player diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_common.h b/Telegram/SourceFiles/media/streaming/media_streaming_common.h index f91afead2..86f76fea1 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_common.h +++ b/Telegram/SourceFiles/media/streaming/media_streaming_common.h @@ -17,6 +17,9 @@ inline constexpr auto kTimeUnknown = std::numeric_limits::min(); inline constexpr auto kDurationMax = crl::time(std::numeric_limits::max()); inline constexpr auto kDurationUnavailable = std::numeric_limits::max(); +inline constexpr auto kOriginalQuality = 0; +inline constexpr auto kAutoQuality = -1; + namespace Audio { bool SupportsSpeedControl(); } // namespace Audio diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index c235b7d59..3c3ae5fbf 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -307,17 +307,21 @@ mediaviewTitleMaximizeMacPadding: margins(0px, 4px, 8px, 4px); mediaviewShadowTop: icon{{ "mediaview/shadow_top", windowShadowFg }}; mediaviewShadowBottom: icon{{ "mediaview/shadow_bottom", windowShadowFg }}; +mediaviewSpeedMenuInner: Menu(mediaviewMenu) { + separator: MenuSeparator(mediaviewMenuSeparator) { + fg: groupCallMenuBgOver; + padding: margins(0px, 4px, 0px, 4px); + width: 6px; + } + itemPadding: margins(54px, 7px, 54px, 9px); + itemFgDisabled: mediaviewTextLinkFg; +} mediaviewSpeedMenu: MediaSpeedMenu(mediaPlayerSpeedMenu) { dropdown: DropdownMenu(mediaviewDropdownMenu) { - menu: Menu(mediaviewMenu) { - separator: MenuSeparator(mediaviewMenuSeparator) { - fg: groupCallMenuBgOver; - padding: margins(0px, 4px, 0px, 4px); - width: 6px; - } - itemPadding: margins(54px, 7px, 54px, 9px); - itemFgDisabled: mediaviewTextLinkFg; - } + menu: mediaviewSpeedMenuInner; + } + qualityMenu: Menu(mediaviewSpeedMenuInner) { + itemPadding: margins(17px, 7px, 54px, 9px); } activeCheck: icon {{ "player/player_check", mediaviewTextLinkFg }}; slider: MediaSlider(defaultContinuousSlider) { diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 96dcdc445..580d5e120 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -1112,7 +1112,10 @@ bool OverlayWidget::videoShown() const { QSize OverlayWidget::videoSize() const { Expects(videoShown()); - return flipSizeByRotation(_streamed->instance.info().video.size); + const auto use = (_document && _chosenQuality != _document) + ? _document->dimensions + : _streamed->instance.info().video.size; + return flipSizeByRotation(use); } bool OverlayWidget::streamingRequiresControls() const { @@ -2257,16 +2260,37 @@ OverlayWidget::~OverlayWidget() { _dropdown.destroy(); } +not_null OverlayWidget::chooseQuality() const { + Expects(_document != nullptr); + + const auto video = _document->video(); + if (!video || video->qualities.empty() || _quality == kOriginalQuality) { + return _document; + } + auto closest = _document; + auto closestAbs = std::abs(_quality - _document->resolveVideoQuality()); + for (const auto &quality : video->qualities) { + const auto abs = std::abs(_quality - quality->resolveVideoQuality()); + if (abs < closestAbs) { + closestAbs = abs; + closest = quality; + } + } + return closest; +} + void OverlayWidget::assignMediaPointer(DocumentData *document) { _savePhotoVideoWhenLoaded = SavePhotoVideo::None; _photo = nullptr; _photoMedia = nullptr; if (_document != document) { if ((_document = document)) { + _chosenQuality = chooseQuality(); _documentMedia = _document->createMediaView(); _documentMedia->goodThumbnailWanted(); _documentMedia->thumbnailWanted(fileOrigin()); } else { + _chosenQuality = nullptr; _documentMedia = nullptr; } _documentLoadingTo = QString(); @@ -2275,6 +2299,7 @@ void OverlayWidget::assignMediaPointer(DocumentData *document) { void OverlayWidget::assignMediaPointer(not_null photo) { _savePhotoVideoWhenLoaded = SavePhotoVideo::None; + _chosenQuality = nullptr; _document = nullptr; _documentMedia = nullptr; _documentLoadingTo = QString(); @@ -3848,12 +3873,12 @@ void OverlayWidget::startStreamingPlayer( return; } - const auto position = _document + _streamedPosition = _document ? startStreaming.startTime : _photo ? _photo->videoStartPosition() : 0; - restartAtSeekPosition(position); + restartAtSeekPosition(_streamedPosition); } void OverlayWidget::initStreamingThumbnail() { @@ -3892,9 +3917,15 @@ void OverlayWidget::initStreamingThumbnail() { : good ? good->size() : _document->dimensions; - if (!good && !thumbnail && !blurred) { + if (size.isEmpty()) { return; - } else if (size.isEmpty()) { + } else if (!_streamedQualityChangeFrame.isNull()) { + setStaticContent(_streamedQualityChangeFrame.scaled( + size, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation)); + return; + } else if (!good && !thumbnail && !blurred) { return; } const auto options = VideoThumbOptions(_document); @@ -3917,6 +3948,7 @@ void OverlayWidget::initStreamingThumbnail() { void OverlayWidget::streamingReady(Streaming::Information &&info) { if (videoShown()) { applyVideoSize(); + _streamedQualityChangeFrame = QImage(); } else { updateContentRect(); } @@ -3938,8 +3970,9 @@ bool OverlayWidget::createStreamingObjects() { const auto origin = fileOrigin(); const auto callback = [=] { waitingAnimationCallback(); }; - if (_document) { - _streamed = std::make_unique(_document, origin, callback); + const auto video = _chosenQuality ? _chosenQuality : _document; + if (video) { + _streamed = std::make_unique(video, origin, callback); } else { _streamed = std::make_unique(_photo, origin, callback); } @@ -3950,8 +3983,8 @@ bool OverlayWidget::createStreamingObjects() { ++_streamedCreated; _streamed->instance.setPriority(kOverlayLoaderPriority); _streamed->instance.lockPlayer(); - _streamed->withSound = _document - && !_document->isSilentVideo() + _streamed->withSound = video + && !video->isSilentVideo() && (_document->isAudioFile() || _document->isVideoFile() || _document->isVoiceMessage() @@ -4018,6 +4051,7 @@ void OverlayWidget::handleStreamingUpdate(Streaming::Update &&update) { updateContentRect(); Core::App().updateNonIdle(); updatePlaybackState(); + _streamedPosition = update.position; }, [&](const PreloadedAudio &update) { updatePlaybackState(); }, [&](const UpdateAudio &update) { @@ -4381,6 +4415,45 @@ float64 OverlayWidget::playbackControlsCurrentSpeed(bool lastNonDefault) { return Core::App().settings().videoPlaybackSpeed(lastNonDefault); } +std::vector OverlayWidget::playbackControlsQualities() { + const auto video = _document ? _document->video() : nullptr; + if (!video || video->qualities.empty()) { + return {}; + } + auto result = std::vector(); + result.reserve(video->qualities.size()); + for (const auto &quality : video->qualities) { + result.push_back(quality->resolveVideoQuality()); + } + return result; +} + +int OverlayWidget::playbackControlsCurrentQuality() { + return _quality; +} + +void OverlayWidget::playbackControlsQualityChanged(int quality) { + const auto now = _chosenQuality; + if (_quality != quality) { + _quality = quality; + if (_document) { + _chosenQuality = chooseQuality(); + if (_chosenQuality != now) { + if (_streamed && _streamed->instance.ready()) { + _streamedQualityChangeFrame = currentVideoFrameImage(); + } + clearStreaming(); + _streamingStartPaused = false; + const auto time = _streamedPosition; + const auto startStreaming = StartStreaming(false, time); + if (!canInitStreaming() || !initStreaming(startStreaming)) { + redisplayContent(); + } + } + } + } +} + void OverlayWidget::switchToPip() { Expects(_streamed != nullptr); Expects(_document != nullptr); @@ -4628,6 +4701,7 @@ void OverlayWidget::updatePlaybackState() { } const auto state = _streamed->instance.player().prepareLegacyState(); if (state.position != kTimeUnknown && state.length != kTimeUnknown) { + _streamedPosition = state.position; if (_streamed->controls) { _streamed->controls->updatePlayback(state); _touchbarTrackState.fire_copy(state); diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index 5114b169c..8396ddd4d 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -236,6 +236,9 @@ private: void playbackControlsVolumeChangeFinished() override; void playbackControlsSpeedChanged(float64 speed) override; float64 playbackControlsCurrentSpeed(bool lastNonDefault) override; + std::vector playbackControlsQualities() override; + int playbackControlsCurrentQuality() override; + void playbackControlsQualityChanged(int quality) override; void playbackControlsToFullScreen() override; void playbackControlsFromFullScreen() override; void playbackControlsToPictureInPicture() override; @@ -315,11 +318,11 @@ private: void checkForSaveLoaded(); void showPremiumDownloadPromo(); - Entity entityForUserPhotos(int index) const; - Entity entityForSharedMedia(int index) const; - Entity entityForCollage(int index) const; - Entity entityByIndex(int index) const; - Entity entityForItemId(const FullMsgId &itemId) const; + [[nodiscard]] Entity entityForUserPhotos(int index) const; + [[nodiscard]] Entity entityForSharedMedia(int index) const; + [[nodiscard]] Entity entityForCollage(int index) const; + [[nodiscard]] Entity entityByIndex(int index) const; + [[nodiscard]] Entity entityForItemId(const FullMsgId &itemId) const; bool moveToEntity(const Entity &entity, int preloadDelta = 0); void setContext(std::variant< @@ -335,23 +338,23 @@ private: struct SharedMedia; using SharedMediaType = SharedMediaWithLastSlice::Type; using SharedMediaKey = SharedMediaWithLastSlice::Key; - std::optional sharedMediaType() const; - std::optional sharedMediaKey() const; - std::optional computeOverviewType() const; + [[nodiscard]] std::optional sharedMediaType() const; + [[nodiscard]] std::optional sharedMediaKey() const; + [[nodiscard]] std::optional computeOverviewType() const; bool validSharedMedia() const; void validateSharedMedia(); void handleSharedMediaUpdate(SharedMediaWithLastSlice &&update); struct UserPhotos; using UserPhotosKey = UserPhotosSlice::Key; - std::optional userPhotosKey() const; + [[nodiscard]] std::optional userPhotosKey() const; bool validUserPhotos() const; void validateUserPhotos(); void handleUserPhotosUpdate(UserPhotosSlice &&update); struct Collage; using CollageKey = WebPageCollage::Item; - std::optional collageKey() const; + [[nodiscard]] std::optional collageKey() const; bool validCollage() const; void validateCollage(); @@ -430,11 +433,11 @@ private: void contentSizeChanged(); // Radial animation interface. - float64 radialProgress() const; - bool radialLoading() const; - QRect radialRect() const; + [[nodiscard]] float64 radialProgress() const; + [[nodiscard]] bool radialLoading() const; + [[nodiscard]] QRect radialRect() const; void radialStart(); - crl::time radialTimeShift() const; + [[nodiscard]] crl::time radialTimeShift() const; void updateHeader(); void snapXY(); @@ -524,6 +527,7 @@ private: void clearStreaming(bool savePosition = true); [[nodiscard]] bool canInitStreaming() const; [[nodiscard]] bool saveControlLocked() const; + [[nodiscard]] not_null chooseQuality() const; [[nodiscard]] bool topShadowOnTheRight() const; void applyHideWindowWorkaround(); @@ -551,6 +555,8 @@ private: rpl::lifetime _sessionLifetime; PhotoData *_photo = nullptr; DocumentData *_document = nullptr; + DocumentData *_chosenQuality = nullptr; + int _quality = {}; QString _documentLoadingTo; std::shared_ptr _photoMedia; std::shared_ptr _documentMedia; @@ -625,6 +631,8 @@ private: std::unique_ptr _streamed; std::unique_ptr _pip; + QImage _streamedQualityChangeFrame; + crl::time _streamedPosition = 0; int _streamedCreated = 0; bool _showAsPip = false; diff --git a/Telegram/SourceFiles/media/view/media_view_playback_controls.cpp b/Telegram/SourceFiles/media/view/media_view_playback_controls.cpp index 896a66ec7..16ca4b8c4 100644 --- a/Telegram/SourceFiles/media/view/media_view_playback_controls.cpp +++ b/Telegram/SourceFiles/media/view/media_view_playback_controls.cpp @@ -47,7 +47,10 @@ PlaybackControls::PlaybackControls( parent, [=](bool) {}, [=](bool lastNonDefault) { return speedLookup(lastNonDefault); }, - [=](float64 speed) { saveSpeed(speed); }) + [=](float64 speed) { saveSpeed(speed); }, + _delegate->playbackControlsQualities(), + [=] { return _delegate->playbackControlsCurrentQuality(); }, + [=](int quality) { saveQuality(quality); }) : nullptr) , _fadeAnimation(std::make_unique(this)) { _fadeAnimation->show(); @@ -192,6 +195,10 @@ void PlaybackControls::saveSpeed(float64 speed) { _delegate->playbackControlsSpeedChanged(speed); } +void PlaybackControls::saveQuality(int quality) { + _delegate->playbackControlsQualityChanged(quality); +} + void PlaybackControls::updatePlaybackSpeed(float64 speed) { DEBUG_LOG(("Media playback speed: update to %1.").arg(speed)); _delegate->playbackControlsSpeedChanged(speed); diff --git a/Telegram/SourceFiles/media/view/media_view_playback_controls.h b/Telegram/SourceFiles/media/view/media_view_playback_controls.h index ac5e15baa..0970808c8 100644 --- a/Telegram/SourceFiles/media/view/media_view_playback_controls.h +++ b/Telegram/SourceFiles/media/view/media_view_playback_controls.h @@ -44,6 +44,10 @@ public: virtual void playbackControlsSpeedChanged(float64 speed) = 0; [[nodiscard]] virtual float64 playbackControlsCurrentSpeed( bool lastNonDefault) = 0; + [[nodiscard]] virtual auto playbackControlsQualities() + -> std::vector = 0; + [[nodiscard]] virtual int playbackControlsCurrentQuality() = 0; + virtual void playbackControlsQualityChanged(int quality) = 0; virtual void playbackControlsToFullScreen() = 0; virtual void playbackControlsFromFullScreen() = 0; virtual void playbackControlsToPictureInPicture() = 0; @@ -90,6 +94,8 @@ private: [[nodiscard]] float64 speedLookup(bool lastNonDefault) const; void saveSpeed(float64 speed); + void saveQuality(int quality); + const not_null _delegate; bool _inFullScreen = false;