diff --git a/Telegram/Resources/icons/voice_lock/input_round_s.png b/Telegram/Resources/icons/voice_lock/input_round_s.png new file mode 100644 index 0000000000..549dd1ab18 Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/input_round_s.png differ diff --git a/Telegram/Resources/icons/voice_lock/input_round_s@2x.png b/Telegram/Resources/icons/voice_lock/input_round_s@2x.png new file mode 100644 index 0000000000..4ab801a465 Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/input_round_s@2x.png differ diff --git a/Telegram/Resources/icons/voice_lock/input_round_s@3x.png b/Telegram/Resources/icons/voice_lock/input_round_s@3x.png new file mode 100644 index 0000000000..878aa7e6a1 Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/input_round_s@3x.png differ diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index b1bfa2f780..25e4852a69 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -1210,6 +1210,7 @@ historyRecordLockBody: icon {{ "voice_lock/record_lock_body", historyToDownBg }} historyRecordLockMargin: margins(4px, 4px, 4px, 4px); historyRecordLockArrow: icon {{ "voice_lock/voice_arrow", historyToDownFg }}; historyRecordLockInput: icon {{ "voice_lock/input_mic_s", historyToDownFg }}; +historyRecordLockRound: icon {{ "voice_lock/input_round_s", historyToDownFg }}; historyRecordLockRippleMargin: margins(6px, 6px, 6px, 6px); historyRecordDelete: IconButton(historyAttach) { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp index 436e381675..e1110c770d 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp @@ -497,8 +497,7 @@ ListenWrap::ListenWrap( , _data(data) , _delete(base::make_unique_q(parent, _st.remove)) , _durationFont(font) -, _duration(Ui::FormatDurationText( - float64(_data->samples) / ::Media::Player::kDefaultFrequency)) +, _duration(Ui::FormatDurationText(_data->duration / 1000)) , _durationWidth(_durationFont->width(_duration)) , _playPauseSt(st::mediaPlayerButton) , _playPauseButton(base::make_unique_q(parent)) @@ -634,7 +633,7 @@ void ListenWrap::initPlayButton() { _mediaView->setBytes(_data->bytes); _document->size = _data->bytes.size(); - _document->type = VoiceDocument; + _document->type = _data->video ? RoundVideoDocument : VoiceDocument; const auto &play = _playPauseSt.playOuter; const auto &width = _waveformBgFinalCenterRect.height(); @@ -833,6 +832,7 @@ public: void requestPaintLockToStopProgress(float64 progress); void requestPaintPauseToInputProgress(float64 progress); void setVisibleTopPart(int part); + void setRecordingVideo(bool value); [[nodiscard]] rpl::producer<> locks() const; [[nodiscard]] bool isLocked() const; @@ -861,6 +861,7 @@ private: float64 _pauseToInputProgress = 0.; rpl::variable _progress = 0.; int _visibleTopPart = -1; + bool _recordingVideo = false; }; @@ -884,6 +885,10 @@ void RecordLock::setVisibleTopPart(int part) { _visibleTopPart = part; } +void RecordLock::setRecordingVideo(bool value) { + _recordingVideo = value; +} + void RecordLock::init() { shownValue( ) | rpl::start_with_next([=](bool shown) { @@ -974,9 +979,10 @@ void RecordLock::drawProgress(QPainter &p) { p.setBrush(_st.fg); if (_pauseToInputProgress > 0.) { p.setOpacity(_pauseToInputProgress); - st::historyRecordLockInput.paintInCenter( - p, - blockRect.toRect()); + const auto &icon = _recordingVideo + ? st::historyRecordLockRound + : st::historyRecordLockInput; + icon.paintInCenter(p, blockRect.toRect()); p.setOpacity(1. - _pauseToInputProgress); } p.drawRoundedRect( @@ -1533,6 +1539,7 @@ void VoiceRecordBar::init() { } _recordingTipRequire = crl::now(); _recordingVideo = (_send->type() == Ui::SendButton::Type::Round); + _lock->setRecordingVideo(_recordingVideo); _startTimer.callOnce(st::universalDuration); } else if (e->type() == QEvent::MouseButtonRelease) { checkTipRequired(); @@ -1680,7 +1687,9 @@ void VoiceRecordBar::startRecording() { _paused = false; instance()->pause(false, nullptr); if (_videoRecorder) { - _videoRecorder->setPaused(false); + _videoRecorder->resume({ + .content = _data.bytes, + }); } } else { instance()->start(_videoRecorder @@ -1809,7 +1818,6 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) { using namespace ::Media::Capture; if (type == StopType::Cancel) { if (_videoRecorder) { - _videoRecorder->setPaused(true); _videoRecorder->hide(); } instance()->stop(crl::guard(this, [=](Result &&data) { @@ -1817,29 +1825,54 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) { })); } else if (type == StopType::Listen) { if (_videoRecorder) { - _videoRecorder->setPaused(true); + const auto weak = Ui::MakeWeak(this); + _videoRecorder->pause([=](Ui::RoundVideoResult data) { + crl::on_main([=, data = std::move(data)]() mutable { + if (weak) { + window()->raise(); + window()->activateWindow(); + + _paused = true; + _data = ::Media::Capture::Result{ + .bytes = std::move(data.content), + //.waveform = std::move(data.waveform), + .duration = data.duration, + .video = true, + }; + _listen = std::make_unique( + this, + _st, + &_show->session(), + &_data, + _cancelFont); + _listenChanges.fire({}); + } + }); + }); + instance()->pause(true); + } else { + instance()->pause(true, crl::guard(this, [=](Result &&data) { + if (data.bytes.isEmpty()) { + // Close everything. + stop(false); + return; + } + _paused = true; + _data = std::move(data); + + window()->raise(); + window()->activateWindow(); + _listen = std::make_unique( + this, + _st, + &_show->session(), + &_data, + _cancelFont); + _listenChanges.fire({}); + + // _lockShowing = false; + })); } - instance()->pause(true, crl::guard(this, [=](Result &&data) { - if (data.bytes.isEmpty()) { - // Close everything. - stop(false); - return; - } - _paused = true; - _data = std::move(data); - - window()->raise(); - window()->activateWindow(); - _listen = std::make_unique( - this, - _st, - &_show->session(), - &_data, - _cancelFont); - _listenChanges.fire({}); - - // _lockShowing = false; - })); } else if (type == StopType::Send) { if (_videoRecorder) { const auto weak = Ui::MakeWeak(this); @@ -1882,7 +1915,7 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) { _sendVoiceRequests.fire({ _data.bytes, _data.waveform, - Duration(_data.samples), + _data.duration, options, }); })); @@ -1949,7 +1982,7 @@ void VoiceRecordBar::requestToSendWithOptions(Api::SendOptions options) { _sendVoiceRequests.fire({ _data.bytes, _data.waveform, - Duration(_data.samples), + _data.duration, options, }); } diff --git a/Telegram/SourceFiles/media/audio/media_audio_capture.cpp b/Telegram/SourceFiles/media/audio/media_audio_capture.cpp index 3d6df88c67..d46f8789c5 100644 --- a/Telegram/SourceFiles/media/audio/media_audio_capture.cpp +++ b/Telegram/SourceFiles/media/audio/media_audio_capture.cpp @@ -170,13 +170,15 @@ void Instance::stop(Fn callback) { } void Instance::pause(bool value, Fn callback) { - Expects(callback != nullptr || !value); InvokeQueued(_inner.get(), [=] { - _inner->pause(value, [=](Result &&result) { - crl::on_main([=, result = std::move(result)]() mutable { - callback(std::move(result)); - }); - }); + auto done = callback + ? [=](Result &&result) { + crl::on_main([=, result = std::move(result)]() mutable { + callback(std::move(result)); + }); + } + : std::move(callback); + _inner->pause(value, std::move(done)); }); } @@ -481,11 +483,16 @@ void Instance::Inner::pause(bool value, Fn callback) { if (!_paused) { return; } - callback({ - d->fullSamples ? d->data : QByteArray(), - d->fullSamples ? CollectWaveform(d->waveform) : VoiceWaveform(), - qint32(d->fullSamples), - }); + if (callback) { + callback({ + .bytes = d->fullSamples ? d->data : QByteArray(), + .waveform = (d->fullSamples + ? CollectWaveform(d->waveform) + : VoiceWaveform()), + .duration = ((d->fullSamples * crl::time(1000)) + / int64(kCaptureFrequency)), + }); + } } void Instance::Inner::stop(Fn callback) { @@ -620,7 +627,11 @@ void Instance::Inner::stop(Fn callback) { } if (needResult) { - callback({ result, waveform, samples }); + callback({ + .bytes = result, + .waveform = waveform, + .duration = (samples * crl::time(1000)) / kCaptureFrequency, + }); } } diff --git a/Telegram/SourceFiles/media/audio/media_audio_capture.h b/Telegram/SourceFiles/media/audio/media_audio_capture.h index e588637c1a..fb79b39dde 100644 --- a/Telegram/SourceFiles/media/audio/media_audio_capture.h +++ b/Telegram/SourceFiles/media/audio/media_audio_capture.h @@ -63,7 +63,7 @@ public: void start(Fn externalProcessing = nullptr); void stop(Fn callback = nullptr); - void pause(bool value, Fn callback); + void pause(bool value, Fn callback = nullptr); private: class Inner; diff --git a/Telegram/SourceFiles/media/audio/media_audio_capture_common.h b/Telegram/SourceFiles/media/audio/media_audio_capture_common.h index 370259f583..e276ad4e94 100644 --- a/Telegram/SourceFiles/media/audio/media_audio_capture_common.h +++ b/Telegram/SourceFiles/media/audio/media_audio_capture_common.h @@ -12,7 +12,8 @@ namespace Media::Capture { struct Result { QByteArray bytes; VoiceWaveform waveform; - int samples = 0; + crl::time duration; + bool video = false; }; } // namespace Media::Capture diff --git a/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp b/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp index 12dc6fd93a..3025a84ab6 100644 --- a/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp +++ b/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp @@ -46,6 +46,7 @@ public: [[nodiscard]] rpl::producer updated() const; [[nodiscard]] RoundVideoResult finish(); + void restart(RoundVideoPartial partial); private: static int Write(void *opaque, uint8_t *buf, int buf_size); @@ -111,6 +112,10 @@ private: crl::time _lastUpdateDuration = 0; rpl::event_stream _updates; + crl::time _maxDuration = 0; + crl::time _previousPartsDuration = 0; + QByteArray _previousContent; + std::vector _circleMask; // Always nice to use vector! :D base::ConcurrentTimer _timeoutTimer; @@ -119,6 +124,7 @@ private: RoundVideoRecorder::Private::Private(crl::weak_on_queue weak) : _weak(std::move(weak)) +, _maxDuration(kMaxDuration) , _timeoutTimer(_weak, [=] { timeout(); }) { initEncoding(); initCircleMask(); @@ -385,15 +391,31 @@ RoundVideoResult RoundVideoRecorder::Private::finish() { return {}; } finishEncoding(); - if (_resultDuration < kMinDuration) { + auto result = RoundVideoResult{ + .content = base::take(_result), + .waveform = QByteArray(), + .duration = base::take(_resultDuration), + }; + if (result.duration < kMinDuration) { return {}; } - return { - .content = _result, - .waveform = QByteArray(), - .duration = _resultDuration, - }; -}; + _previousPartsDuration += result.duration; + _maxDuration -= result.duration; + return result; +} + +void RoundVideoRecorder::Private::restart(RoundVideoPartial partial) { + if (_format) { + return; + } else if (_maxDuration <= 0) { + notifyFinished(); + return; + } + _previousContent = std::move(partial.content); + _finished = false; + initEncoding(); + _timeoutTimer.callOnce(kInitTimeout); +} void RoundVideoRecorder::Private::fail(Error error) { deinitEncoding(); @@ -421,7 +443,17 @@ void RoundVideoRecorder::Private::deinitEncoding() { _videoFirstTimestamp = -1; _videoPts = 0; + _audioTail = QByteArray(); _audioPts = 0; + _audioChannels = 0; + + _firstAudioChunkFinished = 0; + _firstVideoFrameTime = 0; + + _resultOffset = 0; + + _maxLevelSinceLastUpdate = 0; + _lastUpdateDuration = 0; } void RoundVideoRecorder::Private::push( @@ -494,7 +526,7 @@ void RoundVideoRecorder::Private::encodeVideoFrame( cutCircleFromYUV420P(_videoFrame.get()); _videoFrame->pts = mcstimestamp - _videoFirstTimestamp; - if (_videoFrame->pts >= kMaxDuration * int64(1000)) { + if (_videoFrame->pts >= _maxDuration * int64(1000)) { notifyFinished(); return; } else if (!writeFrame(_videoFrame, _videoCodec, _videoStream)) { @@ -611,7 +643,7 @@ void RoundVideoRecorder::Private::encodeAudioFrame( _audioFrame->pts = _audioPts; _audioPts += _audioFrame->nb_samples; - if (_audioPts >= kMaxDuration * int64(kAudioFrequency) / 1000) { + if (_audioPts >= _maxDuration * int64(kAudioFrequency) / 1000) { notifyFinished(); return; } else if (!writeFrame(_audioFrame, _audioCodec, _audioStream)) { @@ -632,7 +664,7 @@ void RoundVideoRecorder::Private::encodeAudioFrame( void RoundVideoRecorder::Private::notifyFinished() { _finished = true; _updates.fire({ - .samples = int(_resultDuration * 48), + .samples = int((_previousPartsDuration + _resultDuration) * 48), .level = base::take(_maxLevelSinceLastUpdate), .finished = true, }); @@ -709,7 +741,7 @@ void RoundVideoRecorder::Private::updateResultDuration( if (initial || (_lastUpdateDuration + kUpdateEach < _resultDuration)) { _lastUpdateDuration = _resultDuration; _updates.fire({ - .samples = int(_resultDuration * 48), + .samples = int((_previousPartsDuration + _resultDuration) * 48), .level = base::take(_maxLevelSinceLastUpdate), }); } @@ -744,13 +776,7 @@ auto RoundVideoRecorder::updated() -> rpl::producer { } void RoundVideoRecorder::hide(Fn done) { - if (done) { - _private.with([done = std::move(done)](Private &that) { - done(that.finish()); - }); - } - - setPaused(true); + pause(std::move(done)); _preview->hide(); if (const auto onstack = _descriptor.hidden) { @@ -934,16 +960,29 @@ void RoundVideoRecorder::fade(bool visible) { crl::time(200)); } -void RoundVideoRecorder::setPaused(bool paused) { - if (_paused == paused) { +void RoundVideoRecorder::pause(Fn done) { + if (_paused) { return; + } else if (done) { + _private.with([done = std::move(done)](Private &that) { + done(that.finish()); + }); } - _paused = paused; - _descriptor.track->setState(paused - ? Webrtc::VideoState::Inactive - : Webrtc::VideoState::Active); + _paused = true; + _descriptor.track->setState(Webrtc::VideoState::Inactive); _preview->update(); } +void RoundVideoRecorder::resume(RoundVideoPartial partial) { + if (!_paused) { + return; + } + _private.with([partial = std::move(partial)](Private &that) mutable { + that.restart(std::move(partial)); + }); + _paused = false; + _descriptor.track->setState(Webrtc::VideoState::Active); + _preview->update(); +} } // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/round_video_recorder.h b/Telegram/SourceFiles/ui/controls/round_video_recorder.h index 661314445e..6d7505207d 100644 --- a/Telegram/SourceFiles/ui/controls/round_video_recorder.h +++ b/Telegram/SourceFiles/ui/controls/round_video_recorder.h @@ -44,6 +44,12 @@ struct RoundVideoResult { crl::time duration = 0; }; +struct RoundVideoPartial { + QByteArray content; + crl::time from = 0; + crl::time till = 0; +}; + class RoundVideoRecorder final : public base::has_weak_ptr { public: explicit RoundVideoRecorder(RoundVideoRecorderDescriptor &&descriptor); @@ -51,7 +57,8 @@ public: [[nodiscard]] Fn audioChunkProcessor(); - void setPaused(bool paused); + void pause(Fn done = nullptr); + void resume(RoundVideoPartial partial); void hide(Fn done = nullptr); using Update = Media::Capture::Update;