mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-06-05 06:33:57 +02:00
Wait for both audio and video to start.
This commit is contained in:
parent
e59e4afd3e
commit
20a4c7f9f4
4 changed files with 115 additions and 39 deletions
|
@ -144,7 +144,7 @@ void Instance::start(Fn<void(Chunk)> externalProcessing) {
|
||||||
});
|
});
|
||||||
}, [=] {
|
}, [=] {
|
||||||
crl::on_main(this, [=] {
|
crl::on_main(this, [=] {
|
||||||
_updates.fire_error({});
|
_updates.fire_error(Error::Other);
|
||||||
});
|
});
|
||||||
}, externalProcessing);
|
}, externalProcessing);
|
||||||
crl::on_main(this, [=] {
|
crl::on_main(this, [=] {
|
||||||
|
|
|
@ -20,6 +20,15 @@ struct Update {
|
||||||
bool finished = false;
|
bool finished = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class Error : uchar {
|
||||||
|
Other,
|
||||||
|
AudioInit,
|
||||||
|
VideoInit,
|
||||||
|
AudioTimeout,
|
||||||
|
VideoTimeout,
|
||||||
|
Encoding,
|
||||||
|
};
|
||||||
|
|
||||||
struct Chunk {
|
struct Chunk {
|
||||||
crl::time finished = 0;
|
crl::time finished = 0;
|
||||||
QByteArray samples;
|
QByteArray samples;
|
||||||
|
@ -41,7 +50,7 @@ public:
|
||||||
return _available;
|
return _available;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] rpl::producer<Update, rpl::empty_error> updated() const {
|
[[nodiscard]] rpl::producer<Update, Error> updated() const {
|
||||||
return _updates.events();
|
return _updates.events();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,7 +71,7 @@ private:
|
||||||
|
|
||||||
bool _available = false;
|
bool _available = false;
|
||||||
rpl::variable<bool> _started = false;
|
rpl::variable<bool> _started = false;
|
||||||
rpl::event_stream<Update, rpl::empty_error> _updates;
|
rpl::event_stream<Update, Error> _updates;
|
||||||
QThread _thread;
|
QThread _thread;
|
||||||
std::unique_ptr<Inner> _inner;
|
std::unique_ptr<Inner> _inner;
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
*/
|
*/
|
||||||
#include "ui/controls/round_video_recorder.h"
|
#include "ui/controls/round_video_recorder.h"
|
||||||
|
|
||||||
|
#include "base/concurrent_timer.h"
|
||||||
#include "base/debug_log.h"
|
#include "base/debug_log.h"
|
||||||
#include "ffmpeg/ffmpeg_utility.h"
|
#include "ffmpeg/ffmpeg_utility.h"
|
||||||
#include "media/audio/media_audio_capture.h"
|
#include "media/audio/media_audio_capture.h"
|
||||||
|
@ -25,7 +26,8 @@ constexpr auto kUpdateEach = crl::time(100);
|
||||||
constexpr auto kAudioFrequency = 48'000;
|
constexpr auto kAudioFrequency = 48'000;
|
||||||
constexpr auto kAudioBitRate = 32'000;
|
constexpr auto kAudioBitRate = 32'000;
|
||||||
constexpr auto kVideoBitRate = 3 * 1024 * 1024;
|
constexpr auto kVideoBitRate = 3 * 1024 * 1024;
|
||||||
constexpr auto kMaxDuration = 10 * crl::time(1000); AssertIsDebug();
|
constexpr auto kMaxDuration = 10 * crl::time(1000);
|
||||||
|
constexpr auto kInitTimeout = 5 * crl::time(1000);
|
||||||
|
|
||||||
using namespace FFmpeg;
|
using namespace FFmpeg;
|
||||||
|
|
||||||
|
@ -40,7 +42,7 @@ public:
|
||||||
void push(const Media::Capture::Chunk &chunk);
|
void push(const Media::Capture::Chunk &chunk);
|
||||||
|
|
||||||
using Update = Media::Capture::Update;
|
using Update = Media::Capture::Update;
|
||||||
[[nodiscard]] rpl::producer<Update, rpl::empty_error> updated() const;
|
[[nodiscard]] rpl::producer<Update, Error> updated() const;
|
||||||
|
|
||||||
[[nodiscard]] RoundVideoResult finish();
|
[[nodiscard]] RoundVideoResult finish();
|
||||||
|
|
||||||
|
@ -57,7 +59,8 @@ private:
|
||||||
void notifyFinished();
|
void notifyFinished();
|
||||||
void deinitEncoding();
|
void deinitEncoding();
|
||||||
void finishEncoding();
|
void finishEncoding();
|
||||||
void fail();
|
void fail(Error error);
|
||||||
|
void timeout();
|
||||||
|
|
||||||
void encodeVideoFrame(int64 mcstimestamp, const QImage &frame);
|
void encodeVideoFrame(int64 mcstimestamp, const QImage &frame);
|
||||||
void encodeAudioFrame(const Media::Capture::Chunk &chunk);
|
void encodeAudioFrame(const Media::Capture::Chunk &chunk);
|
||||||
|
@ -105,16 +108,21 @@ private:
|
||||||
|
|
||||||
ushort _maxLevelSinceLastUpdate = 0;
|
ushort _maxLevelSinceLastUpdate = 0;
|
||||||
crl::time _lastUpdateDuration = 0;
|
crl::time _lastUpdateDuration = 0;
|
||||||
rpl::event_stream<Update, rpl::empty_error> _updates;
|
rpl::event_stream<Update, Error> _updates;
|
||||||
|
|
||||||
std::vector<bool> _circleMask; // Always nice to use vector<bool>! :D
|
std::vector<bool> _circleMask; // Always nice to use vector<bool>! :D
|
||||||
|
|
||||||
|
base::ConcurrentTimer _timeoutTimer;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
RoundVideoRecorder::Private::Private(crl::weak_on_queue<Private> weak)
|
RoundVideoRecorder::Private::Private(crl::weak_on_queue<Private> weak)
|
||||||
: _weak(std::move(weak)) {
|
: _weak(std::move(weak))
|
||||||
|
, _timeoutTimer(_weak, [=] { timeout(); }) {
|
||||||
initEncoding();
|
initEncoding();
|
||||||
initCircleMask();
|
initCircleMask();
|
||||||
|
|
||||||
|
_timeoutTimer.callOnce(kInitTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
RoundVideoRecorder::Private::~Private() {
|
RoundVideoRecorder::Private::~Private() {
|
||||||
|
@ -174,8 +182,11 @@ void RoundVideoRecorder::Private::initEncoding() {
|
||||||
&Private::Seek,
|
&Private::Seek,
|
||||||
"mp4"_q);
|
"mp4"_q);
|
||||||
|
|
||||||
if (!initVideo() || !initAudio()) {
|
if (!initVideo()) {
|
||||||
fail();
|
fail(Error::VideoInit);
|
||||||
|
return;
|
||||||
|
} else if (!initAudio()) {
|
||||||
|
fail(Error::AudioInit);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,7 +195,7 @@ void RoundVideoRecorder::Private::initEncoding() {
|
||||||
nullptr));
|
nullptr));
|
||||||
if (error) {
|
if (error) {
|
||||||
LogError("avformat_write_header", error);
|
LogError("avformat_write_header", error);
|
||||||
fail();
|
fail(Error::Encoding);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -364,7 +375,7 @@ void RoundVideoRecorder::Private::finishEncoding() {
|
||||||
}
|
}
|
||||||
|
|
||||||
auto RoundVideoRecorder::Private::updated() const
|
auto RoundVideoRecorder::Private::updated() const
|
||||||
-> rpl::producer<Update, rpl::empty_error> {
|
-> rpl::producer<Update, Error> {
|
||||||
return _updates.events();
|
return _updates.events();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -380,11 +391,19 @@ RoundVideoResult RoundVideoRecorder::Private::finish() {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
void RoundVideoRecorder::Private::fail() {
|
void RoundVideoRecorder::Private::fail(Error error) {
|
||||||
deinitEncoding();
|
deinitEncoding();
|
||||||
_updates.fire_error({});
|
_updates.fire_error({});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RoundVideoRecorder::Private::timeout() {
|
||||||
|
if (!_firstAudioChunkFinished) {
|
||||||
|
fail(Error::AudioTimeout);
|
||||||
|
} else if (!_firstVideoFrameTime) {
|
||||||
|
fail(Error::VideoTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void RoundVideoRecorder::Private::deinitEncoding() {
|
void RoundVideoRecorder::Private::deinitEncoding() {
|
||||||
_swsContext = nullptr;
|
_swsContext = nullptr;
|
||||||
_videoCodec = nullptr;
|
_videoCodec = nullptr;
|
||||||
|
@ -448,7 +467,7 @@ void RoundVideoRecorder::Private::encodeVideoFrame(
|
||||||
AV_PIX_FMT_YUV420P,
|
AV_PIX_FMT_YUV420P,
|
||||||
&_swsContext);
|
&_swsContext);
|
||||||
if (!_swsContext) {
|
if (!_swsContext) {
|
||||||
fail();
|
fail(Error::Encoding);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -579,7 +598,7 @@ void RoundVideoRecorder::Private::encodeAudioFrame(
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
LogError("swr_convert", error);
|
LogError("swr_convert", error);
|
||||||
fail();
|
fail(Error::Encoding);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -619,6 +638,8 @@ bool RoundVideoRecorder::Private::writeFrame(
|
||||||
const FramePointer &frame,
|
const FramePointer &frame,
|
||||||
const CodecPointer &codec,
|
const CodecPointer &codec,
|
||||||
AVStream *stream) {
|
AVStream *stream) {
|
||||||
|
_timeoutTimer.cancel();
|
||||||
|
|
||||||
if (frame) {
|
if (frame) {
|
||||||
updateResultDuration(frame->pts, codec->time_base);
|
updateResultDuration(frame->pts, codec->time_base);
|
||||||
}
|
}
|
||||||
|
@ -626,7 +647,7 @@ bool RoundVideoRecorder::Private::writeFrame(
|
||||||
auto error = AvErrorWrap(avcodec_send_frame(codec.get(), frame.get()));
|
auto error = AvErrorWrap(avcodec_send_frame(codec.get(), frame.get()));
|
||||||
if (error) {
|
if (error) {
|
||||||
LogError("avcodec_send_frame", error);
|
LogError("avcodec_send_frame", error);
|
||||||
fail();
|
fail(Error::Encoding);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -642,7 +663,7 @@ bool RoundVideoRecorder::Private::writeFrame(
|
||||||
return true; // Encoding finished
|
return true; // Encoding finished
|
||||||
} else if (error) {
|
} else if (error) {
|
||||||
LogError("avcodec_receive_packet", error);
|
LogError("avcodec_receive_packet", error);
|
||||||
fail();
|
fail(Error::Encoding);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -654,7 +675,7 @@ bool RoundVideoRecorder::Private::writeFrame(
|
||||||
error = AvErrorWrap(av_interleaved_write_frame(_format.get(), pkt));
|
error = AvErrorWrap(av_interleaved_write_frame(_format.get(), pkt));
|
||||||
if (error) {
|
if (error) {
|
||||||
LogError("av_interleaved_write_frame", error);
|
LogError("av_interleaved_write_frame", error);
|
||||||
fail();
|
fail(Error::Encoding);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -677,7 +698,11 @@ void RoundVideoRecorder::Private::updateResultDuration(
|
||||||
AVRational timeBase) {
|
AVRational timeBase) {
|
||||||
accumulate_max(_resultDuration, PtsToTimeCeil(pts, timeBase));
|
accumulate_max(_resultDuration, PtsToTimeCeil(pts, timeBase));
|
||||||
|
|
||||||
if (_lastUpdateDuration + kUpdateEach >= _resultDuration) {
|
const auto initial = !_lastUpdateDuration;
|
||||||
|
if (initial) {
|
||||||
|
accumulate_max(_resultDuration, crl::time(1));
|
||||||
|
}
|
||||||
|
if (initial || (_lastUpdateDuration + kUpdateEach < _resultDuration)) {
|
||||||
_lastUpdateDuration = _resultDuration;
|
_lastUpdateDuration = _resultDuration;
|
||||||
_updates.fire({
|
_updates.fire({
|
||||||
.samples = int(_resultDuration * 48),
|
.samples = int(_resultDuration * 48),
|
||||||
|
@ -704,15 +729,14 @@ Fn<void(Media::Capture::Chunk)> RoundVideoRecorder::audioChunkProcessor() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto RoundVideoRecorder::updated()
|
auto RoundVideoRecorder::updated() -> rpl::producer<Update, Error> {
|
||||||
-> rpl::producer<Update, rpl::empty_error> {
|
|
||||||
return _private.producer_on_main([](const Private &that) {
|
return _private.producer_on_main([](const Private &that) {
|
||||||
return that.updated();
|
return that.updated();
|
||||||
}) | rpl::before_next([=](const Update &update) {
|
}) | rpl::before_next(crl::guard(this, [=](const Update &update) {
|
||||||
const auto duration = (update.samples * crl::time(1000))
|
const auto progress = (update.samples * crl::time(1000))
|
||||||
/ kAudioFrequency;
|
/ float64(kAudioFrequency * kMaxDuration);
|
||||||
progressTo(duration / (1. * kMaxDuration));
|
progressTo(progress);
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
void RoundVideoRecorder::hide(Fn<void(RoundVideoResult)> done) {
|
void RoundVideoRecorder::hide(Fn<void(RoundVideoResult)> done) {
|
||||||
|
@ -733,12 +757,20 @@ void RoundVideoRecorder::hide(Fn<void(RoundVideoResult)> done) {
|
||||||
void RoundVideoRecorder::progressTo(float64 progress) {
|
void RoundVideoRecorder::progressTo(float64 progress) {
|
||||||
if (_progress == progress) {
|
if (_progress == progress) {
|
||||||
return;
|
return;
|
||||||
|
} else if (_progress > 0.001) {
|
||||||
|
_progressAnimation.start(
|
||||||
|
[=] { _preview->update(); },
|
||||||
|
_progress,
|
||||||
|
progress,
|
||||||
|
kUpdateEach * 1.1);
|
||||||
|
}
|
||||||
|
if (!_progress) {
|
||||||
|
_fadeContentAnimation.start(
|
||||||
|
[=] { _preview->update(); },
|
||||||
|
0.,
|
||||||
|
1.,
|
||||||
|
crl::time(200));
|
||||||
}
|
}
|
||||||
_progressAnimation.start(
|
|
||||||
[=] { _preview->update(); },
|
|
||||||
progress,
|
|
||||||
_progress,
|
|
||||||
kUpdateEach);
|
|
||||||
_progress = progress;
|
_progress = progress;
|
||||||
_preview->update();
|
_preview->update();
|
||||||
}
|
}
|
||||||
|
@ -775,12 +807,12 @@ void RoundVideoRecorder::prepareFrame() {
|
||||||
|
|
||||||
void RoundVideoRecorder::createImages() {
|
void RoundVideoRecorder::createImages() {
|
||||||
const auto ratio = style::DevicePixelRatio();
|
const auto ratio = style::DevicePixelRatio();
|
||||||
_framePrepared = QImage(
|
_framePlaceholder = QImage(
|
||||||
QSize(_side, _side) * ratio,
|
QSize(_side, _side) * ratio,
|
||||||
QImage::Format_ARGB32_Premultiplied);
|
QImage::Format_ARGB32_Premultiplied);
|
||||||
_framePrepared.fill(Qt::transparent);
|
_framePlaceholder.fill(Qt::transparent);
|
||||||
_framePrepared.setDevicePixelRatio(ratio);
|
_framePlaceholder.setDevicePixelRatio(ratio);
|
||||||
auto p = QPainter(&_framePrepared);
|
auto p = QPainter(&_framePlaceholder);
|
||||||
auto hq = PainterHighQualityEnabler(p);
|
auto hq = PainterHighQualityEnabler(p);
|
||||||
|
|
||||||
p.setPen(Qt::NoPen);
|
p.setPen(Qt::NoPen);
|
||||||
|
@ -830,10 +862,23 @@ void RoundVideoRecorder::setup() {
|
||||||
prepareFrame();
|
prepareFrame();
|
||||||
|
|
||||||
auto p = QPainter(raw);
|
auto p = QPainter(raw);
|
||||||
|
const auto opacity = _fadeAnimation.value(_visible ? 1. : 0.);
|
||||||
|
if (_fadeAnimation.animating()) {
|
||||||
|
p.setOpacity(opacity);
|
||||||
|
} else if (!_visible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
p.drawImage(raw->rect(), _shadow);
|
p.drawImage(raw->rect(), _shadow);
|
||||||
const auto inner = QRect(_extent, _extent, _side, _side);
|
const auto inner = QRect(_extent, _extent, _side, _side);
|
||||||
p.drawImage(inner, _framePrepared);
|
if (!_progress) {
|
||||||
if (_progress > 0.) {
|
p.drawImage(inner, _framePlaceholder);
|
||||||
|
} else {
|
||||||
|
if (_fadeContentAnimation.animating()) {
|
||||||
|
p.drawImage(inner, _framePlaceholder);
|
||||||
|
p.setOpacity(opacity * _fadeContentAnimation.value(1.));
|
||||||
|
}
|
||||||
|
p.drawImage(inner, _framePrepared);
|
||||||
|
|
||||||
auto hq = PainterHighQualityEnabler(p);
|
auto hq = PainterHighQualityEnabler(p);
|
||||||
p.setPen(QPen(
|
p.setPen(QPen(
|
||||||
Qt::white,
|
Qt::white,
|
||||||
|
@ -867,10 +912,24 @@ void RoundVideoRecorder::setup() {
|
||||||
}, raw->lifetime());
|
}, raw->lifetime());
|
||||||
_descriptor.track->markFrameShown();
|
_descriptor.track->markFrameShown();
|
||||||
|
|
||||||
|
fade(true);
|
||||||
|
|
||||||
raw->show();
|
raw->show();
|
||||||
raw->raise();
|
raw->raise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RoundVideoRecorder::fade(bool visible) {
|
||||||
|
if (_visible == visible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_visible = visible;
|
||||||
|
_fadeAnimation.start(
|
||||||
|
[=] { _preview->update(); },
|
||||||
|
visible ? 0. : 1.,
|
||||||
|
visible ? 1. : 0.,
|
||||||
|
crl::time(200));
|
||||||
|
}
|
||||||
|
|
||||||
void RoundVideoRecorder::setPaused(bool paused) {
|
void RoundVideoRecorder::setPaused(bool paused) {
|
||||||
if (_paused == paused) {
|
if (_paused == paused) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "base/weak_ptr.h"
|
||||||
#include "ui/effects/animations.h"
|
#include "ui/effects/animations.h"
|
||||||
|
|
||||||
#include <crl/crl_object_on_queue.h>
|
#include <crl/crl_object_on_queue.h>
|
||||||
|
@ -14,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
namespace Media::Capture {
|
namespace Media::Capture {
|
||||||
struct Chunk;
|
struct Chunk;
|
||||||
struct Update;
|
struct Update;
|
||||||
|
enum class Error : uchar;
|
||||||
} // namespace Media::Capture
|
} // namespace Media::Capture
|
||||||
|
|
||||||
namespace tgcalls {
|
namespace tgcalls {
|
||||||
|
@ -42,7 +44,7 @@ struct RoundVideoResult {
|
||||||
crl::time duration = 0;
|
crl::time duration = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class RoundVideoRecorder final {
|
class RoundVideoRecorder final : public base::has_weak_ptr {
|
||||||
public:
|
public:
|
||||||
explicit RoundVideoRecorder(RoundVideoRecorderDescriptor &&descriptor);
|
explicit RoundVideoRecorder(RoundVideoRecorderDescriptor &&descriptor);
|
||||||
~RoundVideoRecorder();
|
~RoundVideoRecorder();
|
||||||
|
@ -53,7 +55,8 @@ public:
|
||||||
void hide(Fn<void(RoundVideoResult)> done = nullptr);
|
void hide(Fn<void(RoundVideoResult)> done = nullptr);
|
||||||
|
|
||||||
using Update = Media::Capture::Update;
|
using Update = Media::Capture::Update;
|
||||||
[[nodiscard]] rpl::producer<Update, rpl::empty_error> updated();
|
using Error = Media::Capture::Error;
|
||||||
|
[[nodiscard]] rpl::producer<Update, Error> updated();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
class Private;
|
class Private;
|
||||||
|
@ -62,13 +65,17 @@ private:
|
||||||
void prepareFrame();
|
void prepareFrame();
|
||||||
void createImages();
|
void createImages();
|
||||||
void progressTo(float64 progress);
|
void progressTo(float64 progress);
|
||||||
|
void fade(bool visible);
|
||||||
|
|
||||||
const RoundVideoRecorderDescriptor _descriptor;
|
const RoundVideoRecorderDescriptor _descriptor;
|
||||||
std::unique_ptr<RpWidget> _preview;
|
std::unique_ptr<RpWidget> _preview;
|
||||||
crl::object_on_queue<Private> _private;
|
crl::object_on_queue<Private> _private;
|
||||||
Ui::Animations::Simple _progressAnimation;
|
Ui::Animations::Simple _progressAnimation;
|
||||||
|
Ui::Animations::Simple _fadeAnimation;
|
||||||
|
Ui::Animations::Simple _fadeContentAnimation;
|
||||||
float64 _progress = 0.;
|
float64 _progress = 0.;
|
||||||
QImage _frameOriginal;
|
QImage _frameOriginal;
|
||||||
|
QImage _framePlaceholder;
|
||||||
QImage _framePrepared;
|
QImage _framePrepared;
|
||||||
QImage _shadow;
|
QImage _shadow;
|
||||||
int _lastAddedIndex = 0;
|
int _lastAddedIndex = 0;
|
||||||
|
@ -76,6 +83,7 @@ private:
|
||||||
int _side = 0;
|
int _side = 0;
|
||||||
int _progressStroke = 0;
|
int _progressStroke = 0;
|
||||||
int _extent = 0;
|
int _extent = 0;
|
||||||
|
bool _visible = false;
|
||||||
bool _paused = false;
|
bool _paused = false;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Reference in a new issue