mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-19 15:47:11 +02:00
Use native sound support in macOS notifications.
This commit is contained in:
parent
07fd9b3074
commit
d135151477
15 changed files with 689 additions and 351 deletions
|
@ -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
|
||||
|
|
97
Telegram/SourceFiles/ffmpeg/ffmpeg_bytes_io_wrap.h
Normal file
97
Telegram/SourceFiles/ffmpeg/ffmpeg_bytes_io_wrap.h
Normal 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
|
|
@ -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);
|
||||
|
|
343
Telegram/SourceFiles/media/audio/media_audio_local_cache.cpp
Normal file
343
Telegram/SourceFiles/media/audio/media_audio_local_cache.cpp
Normal 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
|
27
Telegram/SourceFiles/media/audio/media_audio_local_cache.h
Normal file
27
Telegram/SourceFiles/media/audio/media_audio_local_cache.h
Normal 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
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ¬ification);
|
||||
[[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() {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 ¬ifySettings = 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 ¬ifySettings = 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 {
|
||||
|
|
|
@ -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 {
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
Loading…
Add table
Reference in a new issue