Use native sound support in macOS notifications.

This commit is contained in:
John Preston 2025-01-17 11:04:12 +04:00
parent 07fd9b3074
commit d135151477
15 changed files with 689 additions and 351 deletions

View file

@ -1135,6 +1135,8 @@ PRIVATE
media/audio/media_audio_loader.h
media/audio/media_audio_loaders.cpp
media/audio/media_audio_loaders.h
media/audio/media_audio_local_cache.cpp
media/audio/media_audio_local_cache.h
media/audio/media_audio_track.cpp
media/audio/media_audio_track.h
media/audio/media_child_ffmpeg_loader.cpp

View file

@ -0,0 +1,97 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ffmpeg/ffmpeg_utility.h"
namespace FFmpeg {
struct ReadBytesWrap {
int64 size = 0;
int64 offset = 0;
const uchar *data = nullptr;
static int Read(void *opaque, uint8_t *buf, int buf_size) {
auto wrap = static_cast<ReadBytesWrap*>(opaque);
const auto toRead = std::min(
int64(buf_size),
wrap->size - wrap->offset);
if (toRead > 0) {
memcpy(buf, wrap->data + wrap->offset, toRead);
wrap->offset += toRead;
}
return toRead;
}
static int64_t Seek(void *opaque, int64_t offset, int whence) {
auto wrap = static_cast<ReadBytesWrap*>(opaque);
auto updated = int64(-1);
switch (whence) {
case SEEK_SET: updated = offset; break;
case SEEK_CUR: updated = wrap->offset + offset; break;
case SEEK_END: updated = wrap->size + offset; break;
case AVSEEK_SIZE: return wrap->size; break;
}
if (updated < 0 || updated > wrap->size) {
return -1;
}
wrap->offset = updated;
return updated;
}
};
struct WriteBytesWrap {
QByteArray content;
int64 offset = 0;
#if DA_FFMPEG_CONST_WRITE_CALLBACK
static int Write(void *opaque, const uint8_t *_buf, int buf_size) {
uint8_t *buf = const_cast<uint8_t *>(_buf);
#else
static int Write(void *opaque, uint8_t *buf, int buf_size) {
#endif
auto wrap = static_cast<WriteBytesWrap*>(opaque);
if (const auto total = wrap->offset + int64(buf_size)) {
const auto size = int64(wrap->content.size());
constexpr auto kReserve = 1024 * 1024;
wrap->content.reserve((total / kReserve) * kReserve);
const auto overwrite = std::min(
size - wrap->offset,
int64(buf_size));
if (overwrite) {
memcpy(wrap->content.data() + wrap->offset, buf, overwrite);
}
if (const auto append = buf_size - overwrite) {
wrap->content.append(
reinterpret_cast<const char*>(buf) + overwrite,
append);
}
wrap->offset += buf_size;
}
return buf_size;
}
static int64_t Seek(void *opaque, int64_t offset, int whence) {
auto wrap = static_cast<WriteBytesWrap*>(opaque);
const auto &content = wrap->content;
const auto checkedSeek = [&](int64_t offset) {
if (offset < 0 || offset > int64(content.size())) {
return int64_t(-1);
}
return int64_t(wrap->offset = offset);
};
switch (whence) {
case SEEK_SET: return checkedSeek(offset);
case SEEK_CUR: return checkedSeek(wrap->offset + offset);
case SEEK_END: return checkedSeek(int64(content.size()) + offset);
case AVSEEK_SIZE: return int64(content.size());
}
return -1;
}
};
} // namespace FFmpeg

View file

@ -1115,7 +1115,7 @@ void DraftOptionsBox(
? tr::lng_settings_save()
: tr::lng_reply_quote_selected();
}) | rpl::flatten_latest();
box->addButton(std::move(save), [=] {
const auto submit = [=] {
if (state->quote.current().overflown) {
show->showToast({
.title = tr::lng_reply_quote_long_title(tr::now),
@ -1125,12 +1125,22 @@ void DraftOptionsBox(
const auto options = state->forward.options;
finish(resolveReply(), state->webpage, options);
}
});
};
box->addButton(std::move(save), submit);
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
box->events() | rpl::start_with_next([=](not_null<QEvent*> e) {
if (e->type() == QEvent::KeyPress) {
const auto key = static_cast<QKeyEvent*>(e.get())->key();
if (key == Qt::Key_Enter || key == Qt::Key_Return) {
submit();
}
}
}, box->lifetime());
args.show->session().data().itemRemoved(
) | rpl::start_with_next([=](not_null<const HistoryItem*> removed) {
const auto inReply = (state->quote.current().item == removed);

View file

@ -0,0 +1,343 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "media/audio/media_audio_local_cache.h"
#include "ffmpeg/ffmpeg_bytes_io_wrap.h"
#include "ffmpeg/ffmpeg_utility.h"
namespace Media::Audio {
namespace {
constexpr auto kMaxDuration = 10 * crl::time(1000);
constexpr auto kMaxStreams = 2;
constexpr auto kFrameSize = 4096;
[[nodiscard]] QByteArray ConvertAndCut(const QByteArray &bytes) {
using namespace FFmpeg;
auto wrap = ReadBytesWrap{
.size = bytes.size(),
.data = reinterpret_cast<const uchar*>(bytes.constData()),
};
auto input = MakeFormatPointer(
&wrap,
&ReadBytesWrap::Read,
nullptr,
&ReadBytesWrap::Seek);
if (!input) {
return {};
}
auto error = AvErrorWrap(avformat_find_stream_info(input.get(), 0));
if (error) {
LogError(u"avformat_find_stream_info"_q, error);
return {};
}
auto inCodec = (const AVCodec*)nullptr;
const auto streamId = av_find_best_stream(
input.get(),
AVMEDIA_TYPE_AUDIO,
-1,
-1,
&inCodec,
0);
if (streamId < 0) {
LogError(u"av_find_best_stream"_q, AvErrorWrap(streamId));
return {};
}
auto inStream = input->streams[streamId];
auto inCodecPar = inStream->codecpar;
auto inCodecContext = CodecPointer(avcodec_alloc_context3(nullptr));
if (!inCodecContext) {
return {};
}
if (avcodec_parameters_to_context(inCodecContext.get(), inCodecPar) < 0) {
return {};
}
if (avcodec_open2(inCodecContext.get(), inCodec, nullptr) < 0) {
return {};
}
auto result = WriteBytesWrap();
auto outFormat = MakeWriteFormatPointer(
static_cast<void*>(&result),
nullptr,
&WriteBytesWrap::Write,
&WriteBytesWrap::Seek,
"wav"_q);
if (!outFormat) {
return {};
}
// Find and open output codec
auto outCodec = avcodec_find_encoder(AV_CODEC_ID_PCM_S16LE);
if (!outCodec) {
return {};
}
auto outStream = avformat_new_stream(outFormat.get(), outCodec);
if (!outStream) {
return {};
}
auto outCodecContext = CodecPointer(
avcodec_alloc_context3(outCodec));
if (!outCodecContext) {
return {};
}
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
auto mono = AVChannelLayout(AV_CHANNEL_LAYOUT_MONO);
auto stereo = AVChannelLayout(AV_CHANNEL_LAYOUT_STEREO);
const auto in = &inCodecContext->ch_layout;
if (!av_channel_layout_compare(in, &mono)
|| !av_channel_layout_compare(in, &stereo)) {
av_channel_layout_copy(&outCodecContext->ch_layout, in);
} else {
outCodecContext->ch_layout = AV_CHANNEL_LAYOUT_STEREO;
}
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
const auto in = inCodecContext->channels;
if (in == 1 || in == 2) {
outCodecContext->channels = in;
outCodecContext->channel_layout = inCodecContext->channel_layout;
} else {
outCodecContext->channels = 2;
outCodecContext->channel_layout = AV_CH_LAYOUT_STEREO;
}
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
const auto inrate = inCodecContext->sample_rate;
const auto rate = (inrate == 44'100 || inrate == 48'000)
? inrate
: 44'100;
outCodecContext->sample_fmt = AV_SAMPLE_FMT_S16;
outCodecContext->time_base = AVRational{ 1, rate };
outCodecContext->bit_rate = 64 * 1024;
outCodecContext->sample_rate = rate;
error = avcodec_open2(outCodecContext.get(), outCodec, nullptr);
if (error) {
LogError("avcodec_open2", error);
return {};
}
error = avcodec_parameters_from_context(
outStream->codecpar,
outCodecContext.get());
if (error) {
LogError("avcodec_parameters_from_context", error);
return {};
}
error = avformat_write_header(outFormat.get(), nullptr);
if (error) {
LogError("avformat_write_header", error);
return {};
}
auto swrContext = MakeSwresamplePointer(
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
&inCodecContext->ch_layout,
inCodecContext->sample_fmt,
inCodecContext->sample_rate,
&outCodecContext->ch_layout,
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
&inCodecContext->channel_layout,
inCodecContext->sample_fmt,
inCodecContext->sample_rate,
&outCodecContext->channel_layout,
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
outCodecContext->sample_fmt,
outCodecContext->sample_rate);
if (!swrContext) {
return {};
}
auto packet = av_packet_alloc();
const auto guard = gsl::finally([&] {
av_packet_free(&packet);
});
auto frame = MakeFramePointer();
if (!frame) {
return {};
}
auto outFrame = MakeFramePointer();
if (!outFrame) {
return {};
}
outFrame->nb_samples = kFrameSize;
outFrame->format = outCodecContext->sample_fmt;
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
av_channel_layout_copy(
&outFrame->ch_layout,
&outCodecContext->ch_layout);
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
outFrame->channel_layout = outCodecContext->channel_layout;
outFrame->channels = outCodecContext->channels;
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
outFrame->sample_rate = outCodecContext->sample_rate;
error = av_frame_get_buffer(outFrame.get(), 0);
if (error) {
LogError("av_frame_get_buffer", error);
return {};
}
auto pts = int64_t(0);
auto maxPts = int64_t(kMaxDuration) * rate / 1000;
const auto writeFrame = [&](AVFrame *frame) { // nullptr to flush
error = avcodec_send_frame(outCodecContext.get(), frame);
if (error) {
LogError("avcodec_send_frame", error);
return error;
}
auto pkt = av_packet_alloc();
const auto guard = gsl::finally([&] {
av_packet_free(&pkt);
});
while (true) {
error = avcodec_receive_packet(outCodecContext.get(), pkt);
if (error) {
if (error.code() != AVERROR(EAGAIN)
&& error.code() != AVERROR_EOF) {
LogError("avcodec_receive_packet", error);
}
return error;
}
pkt->stream_index = outStream->index;
av_packet_rescale_ts(
pkt,
outCodecContext->time_base,
outStream->time_base);
error = av_interleaved_write_frame(outFormat.get(), pkt);
if (error) {
LogError("av_interleaved_write_frame", error);
return error;
}
}
};
while (pts < maxPts) {
error = av_read_frame(input.get(), packet);
const auto finished = (error.code() == AVERROR_EOF);
if (!finished) {
if (error) {
LogError("av_read_frame", error);
return {};
}
auto guard = gsl::finally([&] {
av_packet_unref(packet);
});
if (packet->stream_index != streamId) {
continue;
}
error = avcodec_send_packet(inCodecContext.get(), packet);
if (error) {
LogError("avcodec_send_packet", error);
return {};
}
}
while (true) {
error = avcodec_receive_frame(inCodecContext.get(), frame.get());
if (error) {
if (error.code() == AVERROR(EAGAIN)
|| error.code() == AVERROR_EOF) {
break;
} else {
LogError("avcodec_receive_frame", error);
return {};
}
}
error = swr_convert(
swrContext.get(),
outFrame->data,
kFrameSize,
(const uint8_t**)frame->data,
frame->nb_samples);
if (error) {
LogError("swr_convert", error);
return {};
}
const auto samples = error.code();
if (!samples) {
continue;
}
outFrame->nb_samples = samples;
outFrame->pts = pts;
pts += samples;
if (pts > maxPts) {
break;
}
error = writeFrame(outFrame.get());
if (error && error.code() != AVERROR(EAGAIN)) {
return {};
}
}
if (finished) {
break;
}
}
error = writeFrame(nullptr);
if (error && error.code() != AVERROR_EOF) {
return {};
}
error = av_write_trailer(outFormat.get());
if (error) {
LogError("av_write_trailer", error);
return {};
}
return result.content;
}
} // namespace
LocalCache::~LocalCache() {
for (const auto &[id, path] : _cache) {
QFile::remove(path);
}
}
QString LocalCache::path(
DocumentId id,
Fn<QByteArray()> resolveBytes,
Fn<QByteArray()> fallbackBytes) {
auto &result = _cache[id];
if (!result.isEmpty()) {
return result;
}
const auto bytes = resolveBytes();
if (bytes.isEmpty()) {
return fallbackBytes ? path(0, fallbackBytes, nullptr) : QString();
}
const auto prefix = cWorkingDir() + u"tdata/audio_cache"_q;
QDir().mkpath(prefix);
const auto name = QString::number(id, 16).toUpper();
result = u"%1/%2.wav"_q.arg(prefix, name);
auto file = QFile(result);
if (!file.open(QIODevice::WriteOnly)) {
return fallbackBytes ? path(0, fallbackBytes, nullptr) : QString();
}
file.write(ConvertAndCut(bytes));
file.close();
return result;
}
} // namespace Media::Audio

View file

@ -0,0 +1,27 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Media::Audio {
class LocalCache final {
public:
LocalCache() = default;
~LocalCache();
[[nodiscard]] QString path(
DocumentId id,
Fn<QByteArray()> resolveBytes,
Fn<QByteArray()> fallbackBytes);
private:
base::flat_map<DocumentId, QString> _cache;
};
} // namespace Media::Audio

View file

@ -167,17 +167,14 @@ GLib::Variant AnyVectorToVariant(const std::vector<std::any> &value) {
class NotificationData final : public base::has_weak_ptr {
public:
using NotificationId = Window::Notifications::Manager::NotificationId;
using Info = Window::Notifications::NativeManager::NotificationInfo;
NotificationData(
not_null<Manager*> manager,
XdgNotifications::NotificationsProxy proxy,
NotificationId id);
[[nodiscard]] bool init(
const QString &title,
const QString &subtitle,
const QString &msg,
Window::Notifications::Manager::DisplayOptions options);
[[nodiscard]] bool init(const Info &info);
NotificationData(const NotificationData &other) = delete;
NotificationData &operator=(const NotificationData &other) = delete;
@ -232,18 +229,17 @@ NotificationData::NotificationData(
, _imageKey(GetImageKey()) {
}
bool NotificationData::init(
const QString &title,
const QString &subtitle,
const QString &msg,
Window::Notifications::Manager::DisplayOptions options) {
bool NotificationData::init(const Info &info) {
const auto &title = info.title;
const auto &subtitle = info.subtitle;
//const auto sound = info.soundPath ? info.soundPath() : QString();
if (_application) {
_notification = Gio::Notification::new_(
subtitle.isEmpty()
? title.toStdString()
: subtitle.toStdString() + " (" + title.toStdString() + ')');
_notification.set_body(msg.toStdString());
_notification.set_body(info.message.toStdString());
_notification.set_icon(
Gio::ThemedIcon::new_(base::IconName().toStdString()));
@ -270,7 +266,7 @@ bool NotificationData::init(
"app.notification-activate",
idVariant);
if (!options.hideMarkAsRead) {
if (!info.options.hideMarkAsRead) {
_notification.add_button_with_target(
tr::lng_context_mark_read(tr::now).toStdString(),
"app.notification-mark-as-read",
@ -284,27 +280,28 @@ bool NotificationData::init(
return false;
}
const auto &text = info.message;
if (HasCapability("body-markup")) {
_title = title.toStdString();
_body = subtitle.isEmpty()
? msg.toHtmlEscaped().toStdString()
? text.toHtmlEscaped().toStdString()
: u"<b>%1</b>\n%2"_q.arg(
subtitle.toHtmlEscaped(),
msg.toHtmlEscaped()).toStdString();
text.toHtmlEscaped()).toStdString();
} else {
_title = subtitle.isEmpty()
? title.toStdString()
: subtitle.toStdString() + " (" + title.toStdString() + ')';
_body = msg.toStdString();
_body = text.toStdString();
}
if (HasCapability("actions")) {
_actions.push_back("default");
_actions.push_back(tr::lng_open_link(tr::now).toStdString());
if (!options.hideMarkAsRead) {
if (!info.options.hideMarkAsRead) {
// icon name according to https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html
_actions.push_back("mail-mark-read");
_actions.push_back(
@ -312,7 +309,7 @@ bool NotificationData::init(
}
if (HasCapability("inline-reply")
&& !options.hideReplyButton) {
&& !info.options.hideReplyButton) {
_actions.push_back("inline-reply");
_actions.push_back(
tr::lng_notification_reply(tr::now).toStdString());
@ -555,14 +552,8 @@ public:
void init(XdgNotifications::NotificationsProxy proxy);
void showNotification(
not_null<PeerData*> peer,
MsgId topicRootId,
Ui::PeerUserpicView &userpicView,
MsgId msgId,
const QString &title,
const QString &subtitle,
const QString &msg,
DisplayOptions options);
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView);
void clearAll();
void clearFromItem(not_null<HistoryItem*> item);
void clearFromTopic(not_null<Data::ForumTopic*> topic);
@ -778,32 +769,23 @@ void Manager::Private::init(XdgNotifications::NotificationsProxy proxy) {
}
void Manager::Private::showNotification(
not_null<PeerData*> peer,
MsgId topicRootId,
Ui::PeerUserpicView &userpicView,
MsgId msgId,
const QString &title,
const QString &subtitle,
const QString &msg,
DisplayOptions options) {
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView) {
const auto peer = info.peer;
const auto key = ContextId{
.sessionId = peer->session().uniqueId(),
.peerId = peer->id,
.topicRootId = topicRootId,
.topicRootId = info.topicRootId,
};
const auto notificationId = NotificationId{
.contextId = key,
.msgId = msgId,
.msgId = info.itemId,
};
auto notification = std::make_unique<NotificationData>(
_manager,
_proxy,
notificationId);
const auto inited = notification->init(
title,
subtitle,
msg,
options);
const auto inited = notification->init(info);
if (!inited) {
return;
}
@ -945,23 +927,9 @@ void Manager::clearNotification(NotificationId id) {
Manager::~Manager() = default;
void Manager::doShowNativeNotification(
not_null<PeerData*> peer,
MsgId topicRootId,
Ui::PeerUserpicView &userpicView,
MsgId msgId,
const QString &title,
const QString &subtitle,
const QString &msg,
DisplayOptions options) {
_private->showNotification(
peer,
topicRootId,
userpicView,
msgId,
title,
subtitle,
msg,
options);
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView) {
_private->showNotification(std::move(info), userpicView);
}
void Manager::doClearAllFast() {

View file

@ -20,14 +20,8 @@ public:
protected:
void doShowNativeNotification(
not_null<PeerData*> peer,
MsgId topicRootId,
Ui::PeerUserpicView &userpicView,
MsgId msgId,
const QString &title,
const QString &subtitle,
const QString &msg,
DisplayOptions options) override;
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView) override;
void doClearAllFast() override;
void doClearFromItem(not_null<HistoryItem*> item) override;
void doClearFromTopic(not_null<Data::ForumTopic*> topic) override;

View file

@ -20,14 +20,8 @@ public:
protected:
void doShowNativeNotification(
not_null<PeerData*> peer,
MsgId topicRootId,
Ui::PeerUserpicView &userpicView,
MsgId msgId,
const QString &title,
const QString &subtitle,
const QString &msg,
DisplayOptions options) override;
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView) override;
void doClearAllFast() override;
void doClearFromItem(not_null<HistoryItem*> item) override;
void doClearFromTopic(not_null<Data::ForumTopic*> topic) override;

View file

@ -209,14 +209,8 @@ public:
Private(Manager *manager);
void showNotification(
not_null<PeerData*> peer,
MsgId topicRootId,
Ui::PeerUserpicView &userpicView,
MsgId msgId,
const QString &title,
const QString &subtitle,
const QString &msg,
DisplayOptions options);
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView);
void clearAll();
void clearFromItem(not_null<HistoryItem*> item);
void clearFromTopic(not_null<Data::ForumTopic*> topic);
@ -296,23 +290,18 @@ Manager::Private::Private(Manager *manager)
}
void Manager::Private::showNotification(
not_null<PeerData*> peer,
MsgId topicRootId,
Ui::PeerUserpicView &userpicView,
MsgId msgId,
const QString &title,
const QString &subtitle,
const QString &msg,
DisplayOptions options) {
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView) {
@autoreleasepool {
const auto peer = info.peer;
NSUserNotification *notification = [[[NSUserNotification alloc] init] autorelease];
if ([notification respondsToSelector:@selector(setIdentifier:)]) {
auto identifier = _managerIdString
+ '_'
+ QString::number(peer->id.value)
+ '_'
+ QString::number(msgId.bare);
+ QString::number(info.itemId.bare);
auto identifierValue = Q2NSString(identifier);
[notification setIdentifier:identifierValue];
}
@ -322,30 +311,35 @@ void Manager::Private::showNotification(
@"session",
[NSNumber numberWithUnsignedLongLong:peer->id.value],
@"peer",
[NSNumber numberWithLongLong:topicRootId.bare],
[NSNumber numberWithLongLong:info.topicRootId.bare],
@"topic",
[NSNumber numberWithLongLong:msgId.bare],
[NSNumber numberWithLongLong:info.itemId.bare],
@"msgid",
[NSNumber numberWithUnsignedLongLong:_managerId],
@"manager",
nil]];
[notification setTitle:Q2NSString(title)];
[notification setSubtitle:Q2NSString(subtitle)];
[notification setInformativeText:Q2NSString(msg)];
if (!options.hideNameAndPhoto
[notification setTitle:Q2NSString(info.title)];
[notification setSubtitle:Q2NSString(info.subtitle)];
[notification setInformativeText:Q2NSString(info.message)];
if (!info.options.hideNameAndPhoto
&& [notification respondsToSelector:@selector(setContentImage:)]) {
NSImage *img = Q2NSImage(
Window::Notifications::GenerateUserpic(peer, userpicView));
[notification setContentImage:img];
}
if (!options.hideReplyButton
if (!info.options.hideReplyButton
&& [notification respondsToSelector:@selector(setHasReplyButton:)]) {
[notification setHasReplyButton:YES];
}
[notification setSoundName:nil];
const auto sound = info.soundPath ? info.soundPath() : QString();
if (!sound.isEmpty()) {
[notification setSoundName:Q2NSString(sound)];
} else {
[notification setSoundName:nil];
}
NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
[center deliverNotification:notification];
@ -572,23 +566,9 @@ Manager::Manager(Window::Notifications::System *system) : NativeManager(system)
Manager::~Manager() = default;
void Manager::doShowNativeNotification(
not_null<PeerData*> peer,
MsgId topicRootId,
Ui::PeerUserpicView &userpicView,
MsgId msgId,
const QString &title,
const QString &subtitle,
const QString &msg,
DisplayOptions options) {
_private->showNotification(
peer,
topicRootId,
userpicView,
msgId,
title,
subtitle,
msg,
options);
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView) {
_private->showNotification(std::move(info), userpicView);
}
void Manager::doClearAllFast() {
@ -620,11 +600,11 @@ bool Manager::doSkipToast() const {
}
void Manager::doMaybePlaySound(Fn<void()> playSound) {
_private->invokeIfNotFocused(std::move(playSound));
// Play through native notification system.
}
void Manager::doMaybeFlashBounce(Fn<void()> flashBounce) {
_private->invokeIfNotFocused(std::move(flashBounce));
flashBounce();
}
} // namespace Notifications

View file

@ -428,18 +428,12 @@ void Create(Window::Notifications::System *system) {
class Manager::Private {
public:
using Info = Window::Notifications::NativeManager::NotificationInfo;
explicit Private(Manager *instance);
bool init();
bool showNotification(
not_null<PeerData*> peer,
MsgId topicRootId,
Ui::PeerUserpicView &userpicView,
MsgId msgId,
const QString &title,
const QString &subtitle,
const QString &msg,
DisplayOptions options);
bool showNotification(Info &&info, Ui::PeerUserpicView &userpicView);
void clearAll();
void clearFromItem(not_null<HistoryItem*> item);
void clearFromTopic(not_null<Data::ForumTopic*> topic);
@ -457,14 +451,8 @@ public:
private:
bool showNotificationInTryCatch(
not_null<PeerData*> peer,
MsgId topicRootId,
Ui::PeerUserpicView &userpicView,
MsgId msgId,
const QString &title,
const QString &subtitle,
const QString &msg,
DisplayOptions options);
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView);
void tryHide(const ToastNotification &notification);
[[nodiscard]] std::wstring ensureSendButtonIcon();
@ -677,28 +665,14 @@ void Manager::Private::handleActivation(const ToastActivation &activation) {
}
bool Manager::Private::showNotification(
not_null<PeerData*> peer,
MsgId topicRootId,
Ui::PeerUserpicView &userpicView,
MsgId msgId,
const QString &title,
const QString &subtitle,
const QString &msg,
DisplayOptions options) {
Info &&info,
Ui::PeerUserpicView &userpicView) {
if (!_notifier) {
return false;
}
return base::WinRT::Try([&] {
return showNotificationInTryCatch(
peer,
topicRootId,
userpicView,
msgId,
title,
subtitle,
msg,
options);
return showNotificationInTryCatch(std::move(info), userpicView);
}).value_or(false);
}
@ -712,36 +686,31 @@ std::wstring Manager::Private::ensureSendButtonIcon() {
}
bool Manager::Private::showNotificationInTryCatch(
not_null<PeerData*> peer,
MsgId topicRootId,
Ui::PeerUserpicView &userpicView,
MsgId msgId,
const QString &title,
const QString &subtitle,
const QString &msg,
DisplayOptions options) {
const auto withSubtitle = !subtitle.isEmpty();
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView) {
const auto withSubtitle = !info.subtitle.isEmpty();
const auto peer = info.peer;
auto toastXml = XmlDocument();
const auto key = ContextId{
.sessionId = peer->session().uniqueId(),
.peerId = peer->id,
.topicRootId = topicRootId,
.topicRootId = info.topicRootId,
};
const auto notificationId = NotificationId{
.contextId = key,
.msgId = msgId
.msgId = info.itemId,
};
const auto idString = u"pid=%1&session=%2&peer=%3&topic=%4&msg=%5"_q
.arg(GetCurrentProcessId())
.arg(key.sessionId)
.arg(key.peerId.value)
.arg(topicRootId.bare)
.arg(msgId.bare);
.arg(info.topicRootId.bare)
.arg(info.itemId.bare);
const auto modern = Platform::IsWindows10OrGreater();
if (modern) {
toastXml.LoadXml(NotificationTemplate(idString, options));
toastXml.LoadXml(NotificationTemplate(idString, info.options));
} else {
toastXml = ToastNotificationManager::GetTemplateContent(
(withSubtitle
@ -751,7 +720,7 @@ bool Manager::Private::showNotificationInTryCatch(
SetAction(toastXml, idString);
}
const auto userpicKey = options.hideNameAndPhoto
const auto userpicKey = info.options.hideNameAndPhoto
? InMemoryKey()
: peer->userpicUniqueKey(userpicView);
const auto userpicPath = _cachedUserpics.get(
@ -760,13 +729,13 @@ bool Manager::Private::showNotificationInTryCatch(
userpicView);
const auto userpicPathWide = QDir::toNativeSeparators(
userpicPath).toStdWString();
if (modern && !options.hideReplyButton) {
if (modern && !info.options.hideReplyButton) {
SetReplyIconSrc(toastXml, ensureSendButtonIcon());
SetReplyPlaceholder(
toastXml,
tr::lng_message_ph(tr::now).toStdWString());
}
if (modern && !options.hideMarkAsRead) {
if (modern && !info.options.hideMarkAsRead) {
SetMarkAsReadText(
toastXml,
tr::lng_context_mark_read(tr::now).toStdWString());
@ -779,17 +748,20 @@ bool Manager::Private::showNotificationInTryCatch(
return false;
}
SetNodeValueString(toastXml, nodeList.Item(0), title.toStdWString());
SetNodeValueString(
toastXml,
nodeList.Item(0),
info.title.toStdWString());
if (withSubtitle) {
SetNodeValueString(
toastXml,
nodeList.Item(1),
subtitle.toStdWString());
info.subtitle.toStdWString());
}
SetNodeValueString(
toastXml,
nodeList.Item(withSubtitle ? 2 : 1),
msg.toStdWString());
info.message.toStdWString());
const auto weak = std::weak_ptr(_guarded);
const auto performOnMainQueue = [=](FnMut<void(Manager *manager)> task) {
@ -860,7 +832,7 @@ bool Manager::Private::showNotificationInTryCatch(
auto i = _notifications.find(key);
if (i != _notifications.cend()) {
auto j = i->second.find(msgId);
auto j = i->second.find(info.itemId);
if (j != i->second.end()) {
const auto existing = j->second;
i->second.erase(j);
@ -880,7 +852,7 @@ bool Manager::Private::showNotificationInTryCatch(
}
return false;
}
i->second.emplace(msgId, toast);
i->second.emplace(info.itemId, toast);
return true;
}
@ -910,23 +882,9 @@ void Manager::handleActivation(const ToastActivation &activation) {
Manager::~Manager() = default;
void Manager::doShowNativeNotification(
not_null<PeerData*> peer,
MsgId topicRootId,
Ui::PeerUserpicView &userpicView,
MsgId msgId,
const QString &title,
const QString &subtitle,
const QString &msg,
DisplayOptions options) {
_private->showNotification(
peer,
topicRootId,
userpicView,
msgId,
title,
subtitle,
msg,
options);
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView) {
_private->showNotification(std::move(info), userpicView);
}
void Manager::doClearAllFast() {

View file

@ -26,14 +26,8 @@ public:
protected:
void doShowNativeNotification(
not_null<PeerData*> peer,
MsgId topicRootId,
Ui::PeerUserpicView &userpicView,
MsgId msgId,
const QString &title,
const QString &subtitle,
const QString &msg,
DisplayOptions options) override;
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView) override;
void doClearAllFast() override;
void doClearFromItem(not_null<HistoryItem*> item) override;
void doClearFromTopic(not_null<Data::ForumTopic*> topic) override;

View file

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/concurrent_timer.h"
#include "base/debug_log.h"
#include "ffmpeg/ffmpeg_bytes_io_wrap.h"
#include "ffmpeg/ffmpeg_utility.h"
#include "media/audio/media_audio_capture.h"
#include "ui/image/image_prepare.h"
@ -40,39 +41,6 @@ constexpr auto kMinScale = 0.7;
using namespace FFmpeg;
struct ReadBytesWrap {
int64 size = 0;
int64 offset = 0;
const uchar *data = nullptr;
static int Read(void *opaque, uint8_t *buf, int buf_size) {
auto wrap = static_cast<ReadBytesWrap*>(opaque);
const auto toRead = std::min(
int64(buf_size),
wrap->size - wrap->offset);
if (toRead > 0) {
memcpy(buf, wrap->data + wrap->offset, toRead);
wrap->offset += toRead;
}
return toRead;
};
static int64_t Seek(void *opaque, int64_t offset, int whence) {
auto wrap = static_cast<ReadBytesWrap*>(opaque);
auto updated = int64(-1);
switch (whence) {
case SEEK_SET: updated = offset; break;
case SEEK_CUR: updated = wrap->offset + offset; break;
case SEEK_END: updated = wrap->size + offset; break;
case AVSEEK_SIZE: return wrap->size; break;
}
if (updated < 0 || updated > wrap->size) {
return -1;
}
wrap->offset = updated;
return updated;
};
};
[[nodiscard]] int MinithumbSize() {
const auto full = st::historySendSize.height();
const auto margin = st::historyRecordWaveformBgMargins;
@ -107,22 +75,6 @@ private:
std::array<int64, kMaxStreams> lastDts = { 0 };
};
#if DA_FFMPEG_CONST_WRITE_CALLBACK
static int Write(void *opaque, const uint8_t *_buf, int buf_size) {
uint8_t *buf = const_cast<uint8_t *>(_buf);
#else
static int Write(void *opaque, uint8_t *buf, int buf_size) {
#endif
return static_cast<Private*>(opaque)->write(buf, buf_size);
}
static int64_t Seek(void *opaque, int64_t offset, int whence) {
return static_cast<Private*>(opaque)->seek(offset, whence);
}
int write(uint8_t *buf, int buf_size);
int64_t seek(int64_t offset, int whence);
void initEncoding();
void initCircleMask();
void initMinithumbsCanvas();
@ -188,8 +140,7 @@ private:
crl::time _firstAudioChunkFinished = 0;
crl::time _firstVideoFrameTime = 0;
QByteArray _result;
int64_t _resultOffset = 0;
WriteBytesWrap _result;
crl::time _resultDuration = 0;
bool _finished = false;
@ -236,49 +187,12 @@ RoundVideoRecorder::Private::~Private() {
finishEncoding();
}
int RoundVideoRecorder::Private::write(uint8_t *buf, int buf_size) {
if (const auto total = _resultOffset + int64(buf_size)) {
const auto size = int64(_result.size());
constexpr auto kReserve = 1024 * 1024;
_result.reserve((total / kReserve) * kReserve);
const auto overwrite = std::min(
size - _resultOffset,
int64(buf_size));
if (overwrite) {
memcpy(_result.data() + _resultOffset, buf, overwrite);
}
if (const auto append = buf_size - overwrite) {
_result.append(
reinterpret_cast<const char*>(buf) + overwrite,
append);
}
_resultOffset += buf_size;
}
return buf_size;
}
int64_t RoundVideoRecorder::Private::seek(int64_t offset, int whence) {
const auto checkedSeek = [&](int64_t offset) {
if (offset < 0 || offset > int64(_result.size())) {
return int64_t(-1);
}
return int64_t(_resultOffset = offset);
};
switch (whence) {
case SEEK_SET: return checkedSeek(offset);
case SEEK_CUR: return checkedSeek(_resultOffset + offset);
case SEEK_END: return checkedSeek(int64(_result.size()) + offset);
case AVSEEK_SIZE: return int64(_result.size());
}
return -1;
}
void RoundVideoRecorder::Private::initEncoding() {
_format = MakeWriteFormatPointer(
static_cast<void*>(this),
static_cast<void*>(&_result),
nullptr,
&Private::Write,
&Private::Seek,
&WriteBytesWrap::Write,
&WriteBytesWrap::Seek,
"mp4"_q);
if (!initVideo()) {
@ -427,10 +341,10 @@ bool RoundVideoRecorder::Private::initAudio() {
&_swrContext);
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
_swrContext = MakeSwresamplePointer(
&_audioCodec->ch_layout,
&_audioCodec->channel_layout,
AV_SAMPLE_FMT_S16,
_audioCodec->sample_rate,
&_audioCodec->ch_layout,
&_audioCodec->channel_layout,
_audioCodec->sample_fmt,
_audioCodec->sample_rate,
&_swrContext);
@ -487,7 +401,7 @@ RoundVideoResult RoundVideoRecorder::Private::finish() {
}
finishEncoding();
auto result = appendToPrevious({
.content = base::take(_result),
.content = base::take(_result.content),
.duration = base::take(_resultDuration),
//.waveform = {},
.minithumbs = base::take(_minithumbs),
@ -518,10 +432,10 @@ RoundVideoResult RoundVideoRecorder::Private::appendToPrevious(
}
auto output = MakeWriteFormatPointer(
static_cast<void*>(this),
static_cast<void*>(&_result),
nullptr,
&Private::Write,
&Private::Seek,
&WriteBytesWrap::Write,
&WriteBytesWrap::Seek,
"mp4"_q);
for (auto i = 0; i != input1->nb_streams; ++i) {
@ -562,7 +476,7 @@ RoundVideoResult RoundVideoRecorder::Private::appendToPrevious(
fail(Error::Encoding);
return {};
}
video.content = base::take(_result);
video.content = base::take(_result.content);
video.duration += _previous.duration;
return video;
}
@ -685,7 +599,7 @@ void RoundVideoRecorder::Private::deinitEncoding() {
_firstAudioChunkFinished = 0;
_firstVideoFrameTime = 0;
_resultOffset = 0;
_result.offset = 0;
_maxLevelSinceLastUpdate = 0;
_lastUpdateDuration = 0;

View file

@ -107,6 +107,22 @@ constexpr auto kSystemAlertDuration = crl::time(0);
return {};
}
[[nodiscard]] std::optional<DocumentId> MaybeSoundFor(
not_null<Data::Thread*> thread,
PeerData *from) {
const auto notifySettings = &thread->owner().notifySettings();
const auto threadUnknown = notifySettings->muteUnknown(thread);
const auto threadAlert = !threadUnknown
&& !notifySettings->isMuted(thread);
const auto fromUnknown = (!from
|| notifySettings->muteUnknown(from));
const auto fromAlert = !fromUnknown
&& !notifySettings->isMuted(from);
return (threadAlert || fromAlert)
? notifySettings->sound(thread).id
: std::optional<DocumentId>();
}
} // namespace
const char kOptionGNotification[] = "gnotification";
@ -538,10 +554,12 @@ void System::showGrouped() {
_manager->showNotification({
.item = lastItem,
.forwardedCount = _lastForwardedCount,
.soundId = _lastSoundId,
});
_lastForwardedCount = 0;
_lastHistoryItemId = FullMsgId();
_lastHistorySessionId = 0;
_lastSoundId = {};
}
}
}
@ -568,23 +586,16 @@ void System::showNext() {
}
return false;
};
auto ms = crl::now(), nextAlert = crl::time(0);
auto alertThread = (Data::Thread*)nullptr;
auto alertSoundId = std::optional<DocumentId>();
for (auto i = _whenAlerts.begin(); i != _whenAlerts.end();) {
while (!i->second.empty() && i->second.begin()->first <= ms) {
const auto thread = i->first;
const auto notifySettings = &thread->owner().notifySettings();
const auto threadUnknown = notifySettings->muteUnknown(thread);
const auto threadAlert = !threadUnknown
&& !notifySettings->isMuted(thread);
const auto from = i->second.begin()->second;
const auto fromUnknown = (!from
|| notifySettings->muteUnknown(from));
const auto fromAlert = !fromUnknown
&& !notifySettings->isMuted(from);
if (threadAlert || fromAlert) {
if (const auto soundId = MaybeSoundFor(thread, from)) {
alertThread = thread;
alertSoundId = soundId;
}
while (!i->second.empty()
&& i->second.begin()->first <= ms + kMinimalAlertDelay) {
@ -627,7 +638,9 @@ void System::showNext() {
}
}
if (_waiters.empty() || !settings.desktopNotify() || _manager->skipToast()) {
if (_waiters.empty()
|| !settings.desktopNotify()
|| _manager->skipToast()) {
if (nextAlert) {
_waitTimer.callOnce(nextAlert - ms);
}
@ -759,6 +772,9 @@ void System::showNext() {
if (!_lastHistoryItemId && groupedItem) {
_lastHistorySessionId = groupedItem->history()->session().uniqueId();
_lastHistoryItemId = groupedItem->fullId();
_lastSoundId = MaybeSoundFor(
notifyThread,
groupedItem->specialNotificationPeer());
}
// If the current notification is grouped.
@ -777,6 +793,9 @@ void System::showNext() {
_lastForwardedCount += forwardedCount;
_lastHistorySessionId = groupedItem->history()->session().uniqueId();
_lastHistoryItemId = groupedItem->fullId();
_lastSoundId = MaybeSoundFor(
notifyThread,
groupedItem->specialNotificationPeer());
_waitForAllGroupedTimer.callOnce(kWaitingForAllGroupedDelay);
} else {
// If the current notification is not grouped
@ -788,12 +807,16 @@ void System::showNext() {
const auto reaction = reactionNotification
? notify->item->lookupUnreadReaction(notify->reactionSender)
: Data::ReactionId();
const auto soundFrom = reactionNotification
? notify->reactionSender
: notify->item->specialNotificationPeer();
if (!reactionNotification || !reaction.empty()) {
_manager->showNotification({
.item = notify->item,
.forwardedCount = forwardedCount,
.reactionFrom = notify->reactionSender,
.reactionId = reaction,
.soundId = MaybeSoundFor(notifyThread, soundFrom),
});
}
}
@ -808,6 +831,25 @@ void System::showNext() {
}
}
QByteArray System::lookupSoundBytes(
not_null<Data::Session*> owner,
DocumentId id) {
if (id) {
const auto &notifySettings = owner->notifySettings();
const auto custom = notifySettings.lookupRingtone(id);
return custom ? ReadRingtoneBytes(custom) : QByteArray();
}
auto f = QFile(Core::App().settings().getSoundPath(u"msg_incoming"_q));
if (f.open(QIODevice::ReadOnly)) {
return f.readAll();
}
auto fallback = QFile(u":/sounds/msg_incoming.mp3"_q);
if (fallback.open(QIODevice::ReadOnly)) {
return fallback.readAll();
}
Unexpected("Embedded sound not found!");
}
not_null<Media::Audio::Track*> System::lookupSound(
not_null<Data::Session*> owner,
DocumentId id) {
@ -819,17 +861,14 @@ not_null<Media::Audio::Track*> System::lookupSound(
if (i != end(_customSoundTracks)) {
return i->second.get();
}
const auto &notifySettings = owner->notifySettings();
if (const auto custom = notifySettings.lookupRingtone(id)) {
const auto bytes = ReadRingtoneBytes(custom);
if (!bytes.isEmpty()) {
const auto j = _customSoundTracks.emplace(
id,
Media::Audio::Current().createTrack()
).first;
j->second->fillFromData(bytes::make_vector(bytes));
return j->second.get();
}
const auto bytes = lookupSoundBytes(owner, id);
if (!bytes.isEmpty()) {
const auto j = _customSoundTracks.emplace(
id,
Media::Audio::Current().createTrack()
).first;
j->second->fillFromData(bytes::make_vector(bytes));
return j->second.get();
}
ensureSoundCreated();
return _soundTrack.get();
@ -1212,15 +1251,24 @@ void NativeManager::doShowNotification(NotificationFields &&fields) {
// #TODO optimize
auto userpicView = item->history()->peer->createUserpicView();
doShowNativeNotification(
item->history()->peer,
item->topicRootId(),
userpicView,
item->id,
scheduled ? WrapFromScheduled(fullTitle) : fullTitle,
subtitle,
text,
options);
const auto owner = &item->history()->owner();
const auto soundPath = fields.soundId ? [=, id = *fields.soundId] {
return _localSoundCache.path(id, [=] {
return Core::App().notifications().lookupSoundBytes(owner, id);
}, [=] {
return Core::App().notifications().lookupSoundBytes(owner, 0);
});
} : Fn<QString()>();
doShowNativeNotification({
.peer = item->history()->peer,
.topicRootId = item->topicRootId(),
.itemId = item->id,
.title = scheduled ? WrapFromScheduled(fullTitle) : fullTitle,
.subtitle = subtitle,
.message = text,
.soundPath = soundPath,
.options = options,
}, userpicView);
}
bool NativeManager::forceHideDetails() const {

View file

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_message_reaction_id.h"
#include "base/timer.h"
#include "base/type_traits.h"
#include "media/audio/media_audio_local_cache.h"
class History;
@ -117,6 +118,9 @@ public:
void notifySettingsChanged(ChangeType type);
void playSound(not_null<Main::Session*> session, DocumentId id);
[[nodiscard]] QByteArray lookupSoundBytes(
not_null<Data::Session*> owner,
DocumentId id);
[[nodiscard]] rpl::lifetime &lifetime() {
return _lifetime;
@ -217,6 +221,7 @@ private:
int _lastForwardedCount = 0;
uint64 _lastHistorySessionId = 0;
FullMsgId _lastHistoryItemId;
std::optional<DocumentId> _lastSoundId;
rpl::lifetime _lifetime;
@ -277,6 +282,7 @@ public:
int forwardedCount = 0;
PeerData *reactionFrom = nullptr;
Data::ReactionId reactionId;
std::optional<DocumentId> soundId;
};
explicit Manager(not_null<System*> system) : _system(system) {
@ -313,11 +319,11 @@ public:
void notificationReplied(NotificationId id, const TextWithTags &reply);
struct DisplayOptions {
bool hideNameAndPhoto = false;
bool hideMessageText = false;
bool hideMarkAsRead = false;
bool hideReplyButton = false;
bool spoilerLoginCode = false;
bool hideNameAndPhoto : 1 = false;
bool hideMessageText : 1 = false;
bool hideMarkAsRead : 1 = false;
bool hideReplyButton : 1 = false;
bool spoilerLoginCode : 1 = false;
};
[[nodiscard]] DisplayOptions getNotificationOptions(
HistoryItem *item,
@ -393,6 +399,17 @@ public:
return ManagerType::Native;
}
struct NotificationInfo {
not_null<PeerData*> peer;
MsgId topicRootId = 0;
MsgId itemId = 0;
QString title;
QString subtitle;
QString message;
Fn<QString()> soundPath;
DisplayOptions options;
};
protected:
using Manager::Manager;
@ -407,14 +424,11 @@ protected:
bool forceHideDetails() const override;
virtual void doShowNativeNotification(
not_null<PeerData*> peer,
MsgId topicRootId,
Ui::PeerUserpicView &userpicView,
MsgId msgId,
const QString &title,
const QString &subtitle,
const QString &msg,
DisplayOptions options) = 0;
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView) = 0;
private:
Media::Audio::LocalCache _localSoundCache;
};
@ -428,14 +442,8 @@ public:
protected:
void doShowNativeNotification(
not_null<PeerData*> peer,
MsgId topicRootId,
Ui::PeerUserpicView &userpicView,
MsgId msgId,
const QString &title,
const QString &subtitle,
const QString &msg,
DisplayOptions options) override {
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView) override {
}
void doClearAllFast() override {
}

View file

@ -12,6 +12,7 @@ nice_target_sources(lib_ffmpeg ${src_loc}
PRIVATE
ffmpeg/ffmpeg_frame_generator.cpp
ffmpeg/ffmpeg_frame_generator.h
ffmpeg/ffmpeg_bytes_io_wrap.h
ffmpeg/ffmpeg_utility.cpp
ffmpeg/ffmpeg_utility.h
)