diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp index 3c156814f2..6ce4b5b745 100644 --- a/Telegram/SourceFiles/core/core_settings.cpp +++ b/Telegram/SourceFiles/core/core_settings.cpp @@ -115,7 +115,13 @@ void LogPosition(const WindowPosition &position, const QString &name) { auto result = Media::VideoQuality(); const auto data = static_cast(&result); memcpy(data, &value, sizeof(result)); - return (result.height <= 4320) ? result : Media::VideoQuality(); + + const auto height = result.height; + const auto offset = Media::kVideoQualityOriginalOffset; + const auto max = 4320; + return (height <= max || (height >= offset && height <= offset + max)) + ? result + : Media::VideoQuality(); } } // namespace diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp index f5e88a0dc3..7522dc1bc0 100644 --- a/Telegram/SourceFiles/data/data_document.cpp +++ b/Telegram/SourceFiles/data/data_document.cpp @@ -544,11 +544,12 @@ void DocumentData::setVideoQualities( return; } const auto good = [&](not_null document) { + // Transcodes in alt_documents are always streamable, + // even if the supports_streaming flag is missing return document->isVideoFile() && !document->dimensions.isEmpty() && !document->inappPlaybackFailed() - && document->useStreamingLoader() - && document->canBeStreamed(); + && document->useStreamingLoader(); }; ranges::sort( qualities, @@ -578,18 +579,67 @@ void DocumentData::setVideoQualities( } qualities.erase(qualities.begin() + count, qualities.end()); if (!qualities.empty()) { - if (const auto mine = resolveVideoQuality()) { - if (mine > qualities.front()->resolveVideoQuality()) { - qualities.insert(begin(qualities), this); + auto transcodeMax = 0; + for (const auto &quality : qualities) { + const auto qres = quality->resolveVideoQuality(); + if (qres > transcodeMax) { + transcodeMax = qres; } } + const auto attributesSize = isVideoFile() ? dimensions : QSize(); + const auto attributesQuality = attributesSize.isEmpty() + ? 0 + : std::min(attributesSize.width(), attributesSize.height()); + + // Heuristic: Trust attributes resolution unless it blatantly + // contradicts server-side transcodes (>1.5x delta) + auto mine = (transcodeMax > 0 + && (attributesQuality < transcodeMax + || attributesQuality > transcodeMax * 1.5)) + ? transcodeMax + : attributesQuality; + if (mine) { + qualities.insert(begin(qualities), this); + } } data->qualities = std::move(qualities); } int DocumentData::resolveVideoQuality() const { - const auto size = isVideoFile() ? dimensions : QSize(); - return size.isEmpty() ? 0 : std::min(size.width(), size.height()); + if (const auto data = video()) { + if (!data->realVideoSize.isEmpty()) { + // Always trust FFmpeg-parsed physical resolution + const auto size = data->realVideoSize; + return std::min(size.width(), size.height()); + } + const auto attributesSize = isVideoFile() ? dimensions : QSize(); + const auto attributesQuality = attributesSize.isEmpty() + ? 0 + : std::min(attributesSize.width(), attributesSize.height()); + if (!data->qualities.empty()) { + auto transcodeMax = 0; + for (const auto &quality : data->qualities) { + if (quality != this) { + const auto qres = quality->resolveVideoQuality(); + if (qres > transcodeMax) { + transcodeMax = qres; + } + } + } + if (transcodeMax > 0) { + // Trust transcodes if attributes appear fake + if (attributesQuality < transcodeMax + || attributesQuality > transcodeMax * 1.5) { + return transcodeMax; + } + return attributesQuality; + } + } + } + const auto attributesSize = isVideoFile() ? dimensions : QSize(); + return attributesSize.isEmpty() + ? 0 + : std::min(attributesSize.width(), attributesSize.height()); } auto DocumentData::resolveQualities(HistoryItem *context) const @@ -611,19 +661,30 @@ not_null DocumentData::chooseQuality( return this; } const auto height = int(request.height); - auto closest = this; - auto closestAbs = std::abs(height - resolveVideoQuality()); - auto closestSize = size; + if (height >= Media::kVideoQualityOriginalOffset) { + return this; + } + + auto closest = (DocumentData*)nullptr; + auto closestAbs = -1; + auto closestSize = -1; + for (const auto &quality : list) { - const auto abs = std::abs(height - quality->resolveVideoQuality()); - if (abs < closestAbs - || (abs == closestAbs && quality->size < closestSize)) { + const auto qres = quality->resolveVideoQuality(); + const auto abs = std::abs(height - qres); + // Prefer Original if it fits target resolution best, + // falling back to transcode only on exact matches + if (!closest + || abs < closestAbs + || (abs == closestAbs && (quality->size < closestSize + || (closest == this && quality != this)))) { closest = quality; closestAbs = abs; closestSize = quality->size; } } - return closest; + + return closest ? closest : this; } void DocumentData::validateLottieSticker() { diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h index ec0a47921d..47c9bccfd2 100644 --- a/Telegram/SourceFiles/data/data_document.h +++ b/Telegram/SourceFiles/data/data_document.h @@ -99,6 +99,7 @@ struct VoiceData : public DocumentAdditionalData { struct VideoData : public DocumentAdditionalData { QString codec; std::vector> qualities; + QSize realVideoSize; }; using RoundData = VoiceData; diff --git a/Telegram/SourceFiles/media/media_common.h b/Telegram/SourceFiles/media/media_common.h index 1f757e581f..42d6e4e4d8 100644 --- a/Telegram/SourceFiles/media/media_common.h +++ b/Telegram/SourceFiles/media/media_common.h @@ -41,6 +41,8 @@ inline constexpr auto kSpeedMin = 0.5; inline constexpr auto kSpeedMax = 2.5; inline constexpr auto kSpedUpDefault = 1.7; +inline constexpr auto kVideoQualityOriginalOffset = 1000000; + [[nodiscard]] inline bool EqualSpeeds(float64 a, float64 b) { return int(base::SafeRound(a * 10.)) == int(base::SafeRound(b * 10.)); } diff --git a/Telegram/SourceFiles/media/player/media_player_button.cpp b/Telegram/SourceFiles/media/player/media_player_button.cpp index 2fc6b8877f..8e043be964 100644 --- a/Telegram/SourceFiles/media/player/media_player_button.cpp +++ b/Telegram/SourceFiles/media/player/media_player_button.cpp @@ -437,13 +437,16 @@ void SettingsButton::prepareFrame() { : u"%1X"_q.arg(rounded / 10); paintBadge(p, text, RectPart::TopLeft, color); } - const auto text = (!_quality) + const auto displayQuality = (_quality >= Media::kVideoQualityOriginalOffset) + ? (_quality - Media::kVideoQualityOriginalOffset) + : _quality; + const auto text = (!displayQuality) ? QString() - : (_quality > 2000) + : (displayQuality > 2000) ? u"4K"_q - : (_quality > 1000) + : (displayQuality > 1000) ? u"FHD"_q - : (_quality > 700) + : (displayQuality > 700) ? u"HD"_q : u"SD"_q; if (!text.isEmpty()) { diff --git a/Telegram/SourceFiles/media/player/media_player_dropdown.cpp b/Telegram/SourceFiles/media/player/media_player_dropdown.cpp index d8086faf8b..00950631e2 100644 --- a/Telegram/SourceFiles/media/player/media_player_dropdown.cpp +++ b/Telegram/SourceFiles/media/player/media_player_dropdown.cpp @@ -826,7 +826,13 @@ void SpeedController::fillMenu(not_null menu) { const auto add = [&](int quality) { const auto automatic = tr::lng_mediaview_quality_auto(tr::now); - const auto text = quality ? u"%1p"_q.arg(quality) : automatic; + const auto offset = Media::kVideoQualityOriginalOffset; + // Quality is height-based, except for Original which uses offset + const auto text = !quality + ? automatic + : (quality >= offset) + ? u"Original (%1p)"_q.arg(std::clamp(quality - offset, 0, 4320)) + : u"%1p"_q.arg(quality); auto action = base::make_unique_q( raw, st.qualityMenu, @@ -836,15 +842,15 @@ void SpeedController::fillMenu(not_null menu) { [=] { _changeQuality(quality); }), nullptr, nullptr); - const auto raw = action.get(); - const auto check = Ui::CreateChild(raw); + const auto rawAction = action.get(); + const auto check = Ui::CreateChild(rawAction); check->resize(st.activeCheck.size()); check->paintRequest( ) | rpl::on_next([check, icon = &st.activeCheck] { auto p = QPainter(check); icon->paint(p, 0, 0, check->width()); }, check->lifetime()); - raw->sizeValue( + rawAction->sizeValue( ) | rpl::on_next([=, skip = st.activeCheckSkip](QSize size) { check->moveToRight( skip, @@ -857,13 +863,19 @@ void SpeedController::fillMenu(not_null menu) { const auto chosen = now.manual ? (now.height == quality) : !quality; - raw->action()->setEnabled(!chosen); + rawAction->action()->setEnabled(!chosen); if (!quality) { - raw->action()->setText(automatic - + (now.manual ? QString() : u"\t%1p"_q.arg(now.height))); + const auto offset = Media::kVideoQualityOriginalOffset; + const auto displayHeight = (now.height >= offset) + ? std::clamp(int(now.height - offset), 0, 4320) + : now.height; + const auto suffix = now.manual + ? QString() + : u"\t%1p"_q.arg(displayHeight); + rawAction->action()->setText(automatic + suffix); } check->setVisible(chosen); - }, raw->lifetime()); + }, rawAction->lifetime()); menu->addAction(std::move(action)); }; diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_common.h b/Telegram/SourceFiles/media/streaming/media_streaming_common.h index 79af0e4e0e..505f665a02 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_common.h +++ b/Telegram/SourceFiles/media/streaming/media_streaming_common.h @@ -59,6 +59,7 @@ struct TrackState { struct VideoInformation { TrackState state; QSize size; + QSize realSize; QImage cover; int rotation = 0; float64 fps = 0.; diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp index 4e7979fc2e..7a61e74aaf 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp @@ -62,6 +62,8 @@ void SaveValidVideoInformation( SaveValidStateInformation(to.state, std::move(from.state)); to.size = from.size; + // Propagate physical resolution parsed from the stream + to.realSize = from.realSize; to.cover = std::move(from.cover); to.rotation = from.rotation; to.fps = from.fps; diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_video_track.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_video_track.cpp index 5e63d08182..7c554a7a1c 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_video_track.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_video_track.cpp @@ -721,6 +721,10 @@ void VideoTrackObject::callReady() { const auto frame = _shared->frameForPaint(); ++_frameIndex; + const auto frameSize = frame->original.isNull() + ? frame->yuv.size + : frame->original.size(); + base::take(_ready)({ VideoInformation{ .state = { .position = _syncTimePoint.trackTime, @@ -730,7 +734,11 @@ void VideoTrackObject::callReady() { .duration = _stream.duration, }, .size = FFmpeg::TransposeSizeByRotation( - FFmpeg::CorrectByAspect(frame->original.size(), _stream.aspect), + FFmpeg::CorrectByAspect(frameSize, _stream.aspect), + _stream.rotation), + // realSize captures physical resolution before SAR correction + .realSize = FFmpeg::TransposeSizeByRotation( + frameSize, _stream.rotation), .cover = frame->original, .rotation = _stream.rotation, diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 38827b4963..1c18cbfb37 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -1246,7 +1246,8 @@ QSize OverlayWidget::videoSize() const { Expects(videoShown()); const auto use = (_document && _chosenQuality != _document) - ? _document->dimensions + // Use chosen quality dimensions instead of original + ? _chosenQuality->dimensions : _streamed->instance.info().video.size; return flipSizeByRotation(use); } @@ -4714,8 +4715,21 @@ void OverlayWidget::initStreamingThumbnail() { void OverlayWidget::streamingReady(Streaming::Information &&info) { markStreamedReady(); if (videoShown()) { + if (_document && _streamed && _streamed->ready) { + const auto targetDocument = _chosenQuality ? _chosenQuality : _document; + if (const auto video = targetDocument->video()) { + video->realVideoSize = info.video.realSize; + } + } applyVideoSize(); _streamedQualityChangeFrame = QImage(); + if (_streamed && _streamed->controls) { + crl::on_main(_widget, [=] { + if (_streamed && _streamed->controls) { + _streamed->controls->updateSpeedToggleQuality(); + } + }); + } } else { updateContentRect(); } @@ -5121,12 +5135,16 @@ void OverlayWidget::restartAtSeekPosition(crl::time position) { } const auto overrideDuration = _stories || (_chosenQuality && _chosenQuality != _document); + const auto durationDocument = (_chosenQuality && _chosenQuality != _document) + ? _chosenQuality + : _document; + auto options = Streaming::PlaybackOptions{ .position = position, .durationOverride = ((overrideDuration - && _document - && _document->hasDuration()) - ? _document->duration() + && durationDocument + && durationDocument->hasDuration()) + ? durationDocument->duration() : crl::time(0)), .hwAllowed = Core::App().settings().hardwareAcceleratedVideo(), .seekable = !_stories, @@ -5231,21 +5249,30 @@ std::vector OverlayWidget::playbackControlsQualities() { } auto result = std::vector(); result.reserve(list.size()); + auto seen = std::vector(); for (const auto &quality : list) { - result.push_back(quality->resolveVideoQuality()); + const auto res = quality->resolveVideoQuality(); + const auto value = (quality == _document) + ? (res + Media::kVideoQualityOriginalOffset) + : res; + if (!ranges::contains(seen, value)) { + result.push_back(value); + seen.push_back(value); + } } return result; } VideoQuality OverlayWidget::playbackControlsCurrentQuality() { - return _chosenQuality - ? VideoQuality{ - .manual = _quality.manual, - .height = uint32(_chosenQuality->resolveVideoQuality()), - } - : _quality; + if (!_chosenQuality) { + return _quality; + } + auto height = uint32(_chosenQuality->resolveVideoQuality()); + if (_chosenQuality == _document) { + height += Media::kVideoQualityOriginalOffset; + } + return { .manual = _quality.manual, .height = height }; } - void OverlayWidget::playbackControlsQualityChanged(int quality) { applyVideoQuality({ .manual = (quality > 0), @@ -5556,6 +5583,7 @@ void OverlayWidget::updatePlaybackState() { _streamedPosition = state.position; if (_streamed->controls) { _streamed->controls->updatePlayback(state); + _streamed->controls->updateSpeedToggleQuality(); _touchbarTrackState.fire_copy(state); updatePowerSaveBlocker(state); } diff --git a/Telegram/SourceFiles/media/view/media_view_playback_controls.cpp b/Telegram/SourceFiles/media/view/media_view_playback_controls.cpp index e680d3a909..6c2e83bd23 100644 --- a/Telegram/SourceFiles/media/view/media_view_playback_controls.cpp +++ b/Telegram/SourceFiles/media/view/media_view_playback_controls.cpp @@ -225,13 +225,13 @@ void PlaybackControls::saveSpeed(float64 speed) { } void PlaybackControls::saveQuality(int quality) { - _speedToggle->setQuality(_qualitiesList.empty() ? 0 : quality); + _speedToggle->setQuality(quality); _delegate->playbackControlsQualityChanged(quality); } void PlaybackControls::updateSpeedToggleQuality() { const auto quality = _delegate->playbackControlsCurrentQuality(); - _speedToggle->setQuality(_qualitiesList.empty() ? 0 : quality.height); + _speedToggle->setQuality(quality.height); } void PlaybackControls::updatePlaybackSpeed(float64 speed) { diff --git a/Telegram/SourceFiles/media/view/media_view_playback_controls.h b/Telegram/SourceFiles/media/view/media_view_playback_controls.h index 881c109a4e..0cd4f3b551 100644 --- a/Telegram/SourceFiles/media/view/media_view_playback_controls.h +++ b/Telegram/SourceFiles/media/view/media_view_playback_controls.h @@ -71,6 +71,8 @@ public: void setLoadingProgress(int64 ready, int64 total); void setTimestamps(std::vector timestamps); void setInFullScreen(bool inFullScreen); + void updatePlaybackSpeed(float64 speed); + void updateSpeedToggleQuality(); [[nodiscard]] bool hasTimestamps() const; [[nodiscard]] std::optional nextTimestamp( float64 progress) const; @@ -96,7 +98,6 @@ private: [[nodiscard]] float64 countDownloadedTillPercent( const Player::TrackState &state) const; - void updatePlaybackSpeed(float64 speed); void updateVolumeToggleIcon(); void updateDownloadProgressPosition(); @@ -108,7 +109,6 @@ private: void saveSpeed(float64 speed); void saveQuality(int quality); - void updateSpeedToggleQuality(); void updateTimestampLabel(); const not_null _delegate;