mirror of
https://github.com/ossrs/srs.git
synced 2025-03-09 15:49:59 +00:00
RTC: Remove cache for RTP packet
This commit is contained in:
parent
7b0a9fe95f
commit
5e2a3572eb
6 changed files with 37 additions and 148 deletions
|
@ -469,86 +469,23 @@ srs_error_t SrsRtcDtls::unprotect_rtcp(char* out_buf, const char* in_buf, int& n
|
||||||
return srs_error_new(ERROR_RTC_SRTP_UNPROTECT, "rtcp unprotect failed");
|
return srs_error_new(ERROR_RTC_SRTP_UNPROTECT, "rtcp unprotect failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
SrsRtcOutgoingPackets::SrsRtcOutgoingPackets(int nn_cache_max)
|
SrsRtcOutgoingPackets::SrsRtcOutgoingPackets()
|
||||||
{
|
{
|
||||||
#if defined(SRS_DEBUG)
|
#if defined(SRS_DEBUG)
|
||||||
debug_id = 0;
|
debug_id = 0;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
use_gso = false;
|
use_gso = false;
|
||||||
should_merge_nalus = false;
|
|
||||||
|
|
||||||
nn_rtp_pkts = 0;
|
nn_rtp_pkts = 0;
|
||||||
nn_audios = nn_extras = 0;
|
nn_audios = nn_extras = 0;
|
||||||
nn_videos = nn_samples = 0;
|
nn_videos = nn_samples = 0;
|
||||||
nn_bytes = nn_rtp_bytes = 0;
|
nn_bytes = nn_rtp_bytes = 0;
|
||||||
nn_padding_bytes = nn_paddings = 0;
|
nn_padding_bytes = nn_paddings = 0;
|
||||||
nn_dropped = 0;
|
nn_dropped = 0;
|
||||||
|
|
||||||
cursor = 0;
|
|
||||||
nn_cache = nn_cache_max;
|
|
||||||
// TODO: FIXME: We should allocate a smaller cache, and increase it when exhausted.
|
|
||||||
cache = new SrsRtpPacket2[nn_cache];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SrsRtcOutgoingPackets::~SrsRtcOutgoingPackets()
|
SrsRtcOutgoingPackets::~SrsRtcOutgoingPackets()
|
||||||
{
|
{
|
||||||
srs_freepa(cache);
|
|
||||||
nn_cache = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SrsRtcOutgoingPackets::reset(bool gso, bool merge_nalus)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < cursor; i++) {
|
|
||||||
SrsRtpPacket2* packet = cache + i;
|
|
||||||
packet->reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
#if defined(SRS_DEBUG)
|
|
||||||
debug_id++;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
use_gso = gso;
|
|
||||||
should_merge_nalus = merge_nalus;
|
|
||||||
|
|
||||||
nn_rtp_pkts = 0;
|
|
||||||
nn_audios = nn_extras = 0;
|
|
||||||
nn_videos = nn_samples = 0;
|
|
||||||
nn_bytes = nn_rtp_bytes = 0;
|
|
||||||
nn_padding_bytes = nn_paddings = 0;
|
|
||||||
nn_dropped = 0;
|
|
||||||
|
|
||||||
cursor = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
SrsRtpPacket2* SrsRtcOutgoingPackets::fetch()
|
|
||||||
{
|
|
||||||
if (cursor >= nn_cache) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
return cache + (cursor++);
|
|
||||||
}
|
|
||||||
|
|
||||||
SrsRtpPacket2* SrsRtcOutgoingPackets::back()
|
|
||||||
{
|
|
||||||
srs_assert(cursor > 0);
|
|
||||||
return cache + cursor - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int SrsRtcOutgoingPackets::size()
|
|
||||||
{
|
|
||||||
return cursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
int SrsRtcOutgoingPackets::capacity()
|
|
||||||
{
|
|
||||||
return nn_cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
SrsRtpPacket2* SrsRtcOutgoingPackets::at(int index)
|
|
||||||
{
|
|
||||||
srs_assert(index < cursor);
|
|
||||||
return cache + index;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SrsRtcPlayer::SrsRtcPlayer(SrsRtcSession* s, int parent_cid)
|
SrsRtcPlayer::SrsRtcPlayer(SrsRtcSession* s, int parent_cid)
|
||||||
|
@ -752,7 +689,7 @@ srs_error_t SrsRtcPlayer::cycle()
|
||||||
int nn_rtc_packets = srs_max(info.nn_audios, info.nn_extras) + info.nn_videos;
|
int nn_rtc_packets = srs_max(info.nn_audios, info.nn_extras) + info.nn_videos;
|
||||||
stat->perf_on_rtc_packets(nn_rtc_packets);
|
stat->perf_on_rtc_packets(nn_rtc_packets);
|
||||||
// Stat the RAW RTP packets, which maybe group by GSO.
|
// Stat the RAW RTP packets, which maybe group by GSO.
|
||||||
stat->perf_on_rtp_packets(info.size());
|
stat->perf_on_rtp_packets(msg_count);
|
||||||
// Stat the RTP packets going into kernel.
|
// Stat the RTP packets going into kernel.
|
||||||
stat->perf_on_gso_packets(info.nn_rtp_pkts);
|
stat->perf_on_gso_packets(info.nn_rtp_pkts);
|
||||||
// Stat the bytes and paddings.
|
// Stat the bytes and paddings.
|
||||||
|
@ -764,8 +701,8 @@ srs_error_t SrsRtcPlayer::cycle()
|
||||||
if (pprint->can_print()) {
|
if (pprint->can_print()) {
|
||||||
// TODO: FIXME: Print stat like frame/s, packet/s, loss_packets.
|
// TODO: FIXME: Print stat like frame/s, packet/s, loss_packets.
|
||||||
srs_trace("-> RTC PLAY %d/%d msgs, %d/%d packets, %d audios, %d extras, %d videos, %d samples, %d/%d/%d bytes, %d pad, %d/%d cache",
|
srs_trace("-> RTC PLAY %d/%d msgs, %d/%d packets, %d audios, %d extras, %d videos, %d samples, %d/%d/%d bytes, %d pad, %d/%d cache",
|
||||||
msg_count, info.nn_dropped, info.size(), info.nn_rtp_pkts, info.nn_audios, info.nn_extras, info.nn_videos, info.nn_samples, info.nn_bytes,
|
msg_count, info.nn_dropped, msg_count, info.nn_rtp_pkts, info.nn_audios, info.nn_extras, info.nn_videos, info.nn_samples, info.nn_bytes,
|
||||||
info.nn_rtp_bytes, info.nn_padding_bytes, info.nn_paddings, info.size(), info.capacity());
|
info.nn_rtp_bytes, info.nn_padding_bytes, info.nn_paddings, msg_count, msg_count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -926,7 +863,9 @@ srs_error_t SrsRtcPlayer::send_packets(std::vector<SrsRtpPacket2*>& pkts, SrsRtc
|
||||||
nack->padding = pkt->padding;
|
nack->padding = pkt->padding;
|
||||||
|
|
||||||
// TODO: FIXME: Should avoid memory copying.
|
// TODO: FIXME: Should avoid memory copying.
|
||||||
SrsRtpRawPayload* payload = nack->reuse_raw();
|
SrsRtpRawPayload* payload = new SrsRtpRawPayload();
|
||||||
|
nack->payload = payload;
|
||||||
|
|
||||||
payload->nn_payload = (int)iov->iov_len;
|
payload->nn_payload = (int)iov->iov_len;
|
||||||
payload->payload = new char[payload->nn_payload];
|
payload->payload = new char[payload->nn_payload];
|
||||||
memcpy((void*)payload->payload, iov->iov_base, iov->iov_len);
|
memcpy((void*)payload->payload, iov->iov_base, iov->iov_len);
|
||||||
|
@ -1099,7 +1038,9 @@ srs_error_t SrsRtcPlayer::send_packets_gso(vector<SrsRtpPacket2*>& pkts, SrsRtcO
|
||||||
nack->padding = packet->padding;
|
nack->padding = packet->padding;
|
||||||
|
|
||||||
// TODO: FIXME: Should avoid memory copying.
|
// TODO: FIXME: Should avoid memory copying.
|
||||||
SrsRtpRawPayload* payload = nack->reuse_raw();
|
SrsRtpRawPayload* payload = new SrsRtpRawPayload();
|
||||||
|
nack->payload = payload;
|
||||||
|
|
||||||
payload->nn_payload = (int)iov->iov_len;
|
payload->nn_payload = (int)iov->iov_len;
|
||||||
payload->payload = new char[payload->nn_payload];
|
payload->payload = new char[payload->nn_payload];
|
||||||
memcpy((void*)payload->payload, iov->iov_base, iov->iov_len);
|
memcpy((void*)payload->payload, iov->iov_base, iov->iov_len);
|
||||||
|
@ -1747,15 +1688,15 @@ void SrsRtcPublisher::on_before_decode_payload(SrsRtpPacket2* pkt, SrsBuffer* bu
|
||||||
|
|
||||||
uint32_t ssrc = pkt->rtp_header.get_ssrc();
|
uint32_t ssrc = pkt->rtp_header.get_ssrc();
|
||||||
if (ssrc == audio_ssrc) {
|
if (ssrc == audio_ssrc) {
|
||||||
*ppayload = pkt->reuse_raw();
|
*ppayload = new SrsRtpRawPayload();
|
||||||
} else if (ssrc == video_ssrc) {
|
} else if (ssrc == video_ssrc) {
|
||||||
uint8_t v = (uint8_t)pkt->nalu_type;
|
uint8_t v = (uint8_t)pkt->nalu_type;
|
||||||
if (v == kStapA) {
|
if (v == kStapA) {
|
||||||
*ppayload = new SrsRtpSTAPPayload();
|
*ppayload = new SrsRtpSTAPPayload();
|
||||||
} else if (v == kFuA) {
|
} else if (v == kFuA) {
|
||||||
*ppayload = pkt->reuse_fua();
|
*ppayload = new SrsRtpFUAPayload2();
|
||||||
} else {
|
} else {
|
||||||
*ppayload = pkt->reuse_raw();
|
*ppayload = new SrsRtpRawPayload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -154,8 +154,6 @@ class SrsRtcOutgoingPackets
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
bool use_gso;
|
bool use_gso;
|
||||||
// TODO: FIXME: Remove it.
|
|
||||||
bool should_merge_nalus;
|
|
||||||
public:
|
public:
|
||||||
#if defined(SRS_DEBUG)
|
#if defined(SRS_DEBUG)
|
||||||
// Debug id.
|
// Debug id.
|
||||||
|
@ -187,22 +185,9 @@ public:
|
||||||
int nn_paddings;
|
int nn_paddings;
|
||||||
// The number of dropped messages.
|
// The number of dropped messages.
|
||||||
int nn_dropped;
|
int nn_dropped;
|
||||||
private:
|
|
||||||
// TODO: FIXME: Remove the cache.
|
|
||||||
int cursor;
|
|
||||||
int nn_cache;
|
|
||||||
SrsRtpPacket2* cache;
|
|
||||||
public:
|
public:
|
||||||
// TODO: FIXME: Remove the cache.
|
SrsRtcOutgoingPackets();
|
||||||
SrsRtcOutgoingPackets(int nn_cache_max = 8);
|
|
||||||
virtual ~SrsRtcOutgoingPackets();
|
virtual ~SrsRtcOutgoingPackets();
|
||||||
public:
|
|
||||||
void reset(bool gso, bool merge_nalus);
|
|
||||||
SrsRtpPacket2* fetch();
|
|
||||||
SrsRtpPacket2* back();
|
|
||||||
int size();
|
|
||||||
int capacity();
|
|
||||||
SrsRtpPacket2* at(int index);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class SrsRtcPlayer : virtual public ISrsCoroutineHandler, virtual public ISrsReloadHandler
|
class SrsRtcPlayer : virtual public ISrsCoroutineHandler, virtual public ISrsReloadHandler
|
||||||
|
|
|
@ -625,7 +625,9 @@ void SrsRtpVideoQueue::covert_frame(std::vector<SrsRtpVideoPacket*>& frame, SrsR
|
||||||
SrsRtpFUAPayload2* head_payload = dynamic_cast<SrsRtpFUAPayload2*>(head->payload);
|
SrsRtpFUAPayload2* head_payload = dynamic_cast<SrsRtpFUAPayload2*>(head->payload);
|
||||||
pkt->nalu_type = head_payload->nalu_type;
|
pkt->nalu_type = head_payload->nalu_type;
|
||||||
|
|
||||||
SrsRtpRawPayload* payload = pkt->reuse_raw();
|
SrsRtpRawPayload* payload = new SrsRtpRawPayload();
|
||||||
|
pkt->payload = payload;
|
||||||
|
|
||||||
payload->nn_payload = nn_nalus + 1;
|
payload->nn_payload = nn_nalus + 1;
|
||||||
payload->payload = new char[payload->nn_payload];
|
payload->payload = new char[payload->nn_payload];
|
||||||
|
|
||||||
|
|
|
@ -699,7 +699,9 @@ srs_error_t SrsRtcFromRtmpBridger::package_opus(char* data, int size, SrsRtpPack
|
||||||
pkt->frame_type = SrsFrameTypeAudio;
|
pkt->frame_type = SrsFrameTypeAudio;
|
||||||
pkt->rtp_header.set_marker(true);
|
pkt->rtp_header.set_marker(true);
|
||||||
|
|
||||||
SrsRtpRawPayload* raw = pkt->reuse_raw();
|
SrsRtpRawPayload* raw = new SrsRtpRawPayload();
|
||||||
|
pkt->payload = raw;
|
||||||
|
|
||||||
raw->payload = new char[size];
|
raw->payload = new char[size];
|
||||||
raw->nn_payload = size;
|
raw->nn_payload = size;
|
||||||
memcpy(raw->payload, data, size);
|
memcpy(raw->payload, data, size);
|
||||||
|
@ -964,7 +966,9 @@ srs_error_t SrsRtcFromRtmpBridger::package_single_nalu(SrsSharedPtrMessage* msg,
|
||||||
pkt->frame_type = SrsFrameTypeVideo;
|
pkt->frame_type = SrsFrameTypeVideo;
|
||||||
pkt->rtp_header.set_timestamp(msg->timestamp * 90);
|
pkt->rtp_header.set_timestamp(msg->timestamp * 90);
|
||||||
|
|
||||||
SrsRtpRawPayload* raw = pkt->reuse_raw();
|
SrsRtpRawPayload* raw = new SrsRtpRawPayload();
|
||||||
|
pkt->payload = raw;
|
||||||
|
|
||||||
raw->payload = sample->bytes;
|
raw->payload = sample->bytes;
|
||||||
raw->nn_payload = sample->size;
|
raw->nn_payload = sample->size;
|
||||||
|
|
||||||
|
@ -991,7 +995,8 @@ srs_error_t SrsRtcFromRtmpBridger::package_fu_a(SrsSharedPtrMessage* msg, SrsSam
|
||||||
pkt->frame_type = SrsFrameTypeVideo;
|
pkt->frame_type = SrsFrameTypeVideo;
|
||||||
pkt->rtp_header.set_timestamp(msg->timestamp * 90);
|
pkt->rtp_header.set_timestamp(msg->timestamp * 90);
|
||||||
|
|
||||||
SrsRtpFUAPayload2* fua = pkt->reuse_fua();
|
SrsRtpFUAPayload2* fua = new SrsRtpFUAPayload2();
|
||||||
|
pkt->payload = fua;
|
||||||
|
|
||||||
fua->nri = (SrsAvcNaluType)header;
|
fua->nri = (SrsAvcNaluType)header;
|
||||||
fua->nalu_type = (SrsAvcNaluType)nal_type;
|
fua->nalu_type = (SrsAvcNaluType)nal_type;
|
||||||
|
|
|
@ -290,23 +290,12 @@ SrsRtpPacket2::SrsRtpPacket2()
|
||||||
nalu_type = SrsAvcNaluTypeReserved;
|
nalu_type = SrsAvcNaluTypeReserved;
|
||||||
shared_msg = NULL;
|
shared_msg = NULL;
|
||||||
frame_type = SrsFrameTypeReserved;
|
frame_type = SrsFrameTypeReserved;
|
||||||
|
cached_payload_size = 0;
|
||||||
cache_raw = new SrsRtpRawPayload();
|
|
||||||
cache_fua = new SrsRtpFUAPayload2();
|
|
||||||
cache_payload = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SrsRtpPacket2::~SrsRtpPacket2()
|
SrsRtpPacket2::~SrsRtpPacket2()
|
||||||
{
|
{
|
||||||
// We may use the cache as payload.
|
|
||||||
if (payload == cache_raw || payload == cache_fua) {
|
|
||||||
payload = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
srs_freep(payload);
|
srs_freep(payload);
|
||||||
srs_freep(cache_raw);
|
|
||||||
srs_freep(cache_fua);
|
|
||||||
|
|
||||||
srs_freep(shared_msg);
|
srs_freep(shared_msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -314,8 +303,8 @@ void SrsRtpPacket2::set_padding(int size)
|
||||||
{
|
{
|
||||||
rtp_header.set_padding(size > 0);
|
rtp_header.set_padding(size > 0);
|
||||||
rtp_header.set_padding_length(size);
|
rtp_header.set_padding_length(size);
|
||||||
if (cache_payload) {
|
if (cached_payload_size) {
|
||||||
cache_payload += size - padding;
|
cached_payload_size += size - padding;
|
||||||
}
|
}
|
||||||
padding = size;
|
padding = size;
|
||||||
}
|
}
|
||||||
|
@ -324,37 +313,12 @@ void SrsRtpPacket2::add_padding(int size)
|
||||||
{
|
{
|
||||||
rtp_header.set_padding(padding + size > 0);
|
rtp_header.set_padding(padding + size > 0);
|
||||||
rtp_header.set_padding_length(rtp_header.get_padding_length() + size);
|
rtp_header.set_padding_length(rtp_header.get_padding_length() + size);
|
||||||
if (cache_payload) {
|
if (cached_payload_size) {
|
||||||
cache_payload += size;
|
cached_payload_size += size;
|
||||||
}
|
}
|
||||||
padding += size;
|
padding += size;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SrsRtpPacket2::reset()
|
|
||||||
{
|
|
||||||
rtp_header.reset();
|
|
||||||
padding = 0;
|
|
||||||
cache_payload = 0;
|
|
||||||
|
|
||||||
// We may use the cache as payload.
|
|
||||||
if (payload == cache_raw || payload == cache_fua) {
|
|
||||||
payload = NULL;
|
|
||||||
}
|
|
||||||
srs_freep(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
SrsRtpRawPayload* SrsRtpPacket2::reuse_raw()
|
|
||||||
{
|
|
||||||
payload = cache_raw;
|
|
||||||
return cache_raw;
|
|
||||||
}
|
|
||||||
|
|
||||||
SrsRtpFUAPayload2* SrsRtpPacket2::reuse_fua()
|
|
||||||
{
|
|
||||||
payload = cache_fua;
|
|
||||||
return cache_fua;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SrsRtpPacket2::set_decode_handler(ISrsRtpPacketDecodeHandler* h)
|
void SrsRtpPacket2::set_decode_handler(ISrsRtpPacketDecodeHandler* h)
|
||||||
{
|
{
|
||||||
decode_handler = h;
|
decode_handler = h;
|
||||||
|
@ -377,7 +341,7 @@ SrsRtpPacket2* SrsRtpPacket2::copy()
|
||||||
cp->shared_msg = shared_msg? shared_msg->copy():NULL;
|
cp->shared_msg = shared_msg? shared_msg->copy():NULL;
|
||||||
cp->frame_type = frame_type;
|
cp->frame_type = frame_type;
|
||||||
|
|
||||||
cp->cache_payload = cache_payload;
|
cp->cached_payload_size = cached_payload_size;
|
||||||
cp->decode_handler = decode_handler;
|
cp->decode_handler = decode_handler;
|
||||||
|
|
||||||
return cp;
|
return cp;
|
||||||
|
@ -385,10 +349,10 @@ SrsRtpPacket2* SrsRtpPacket2::copy()
|
||||||
|
|
||||||
int SrsRtpPacket2::nb_bytes()
|
int SrsRtpPacket2::nb_bytes()
|
||||||
{
|
{
|
||||||
if (!cache_payload) {
|
if (!cached_payload_size) {
|
||||||
cache_payload = rtp_header.nb_bytes() + (payload? payload->nb_bytes():0) + padding;
|
cached_payload_size = rtp_header.nb_bytes() + (payload? payload->nb_bytes():0) + padding;
|
||||||
}
|
}
|
||||||
return cache_payload;
|
return cached_payload_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
srs_error_t SrsRtpPacket2::encode(SrsBuffer* buf)
|
srs_error_t SrsRtpPacket2::encode(SrsBuffer* buf)
|
||||||
|
@ -441,7 +405,7 @@ srs_error_t SrsRtpPacket2::decode(SrsBuffer* buf)
|
||||||
|
|
||||||
// By default, we always use the RAW payload.
|
// By default, we always use the RAW payload.
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
payload = reuse_raw();
|
payload = new SrsRtpRawPayload();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((err = payload->decode(buf)) != srs_success) {
|
if ((err = payload->decode(buf)) != srs_success) {
|
||||||
|
|
|
@ -135,10 +135,8 @@ public:
|
||||||
SrsFrameType frame_type;
|
SrsFrameType frame_type;
|
||||||
// Fast cache for performance.
|
// Fast cache for performance.
|
||||||
private:
|
private:
|
||||||
// Cache frequently used payload for performance.
|
// The cached payload size for packet.
|
||||||
SrsRtpRawPayload* cache_raw;
|
int cached_payload_size;
|
||||||
SrsRtpFUAPayload2* cache_fua;
|
|
||||||
int cache_payload;
|
|
||||||
// The helper handler for decoder, use RAW payload if NULL.
|
// The helper handler for decoder, use RAW payload if NULL.
|
||||||
ISrsRtpPacketDecodeHandler* decode_handler;
|
ISrsRtpPacketDecodeHandler* decode_handler;
|
||||||
public:
|
public:
|
||||||
|
@ -149,12 +147,6 @@ public:
|
||||||
void set_padding(int size);
|
void set_padding(int size);
|
||||||
// Increase the padding of RTP packet.
|
// Increase the padding of RTP packet.
|
||||||
void add_padding(int size);
|
void add_padding(int size);
|
||||||
// Reset RTP packet.
|
|
||||||
void reset();
|
|
||||||
// Reuse the cached raw message as payload.
|
|
||||||
SrsRtpRawPayload* reuse_raw();
|
|
||||||
// Reuse the cached fua message as payload.
|
|
||||||
SrsRtpFUAPayload2* reuse_fua();
|
|
||||||
// Set the decode handler.
|
// Set the decode handler.
|
||||||
void set_decode_handler(ISrsRtpPacketDecodeHandler* h);
|
void set_decode_handler(ISrsRtpPacketDecodeHandler* h);
|
||||||
// Whether the packet is Audio packet.
|
// Whether the packet is Audio packet.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue