mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Simplify AudioTrack.handleBuffer's return value.
Position discontinuities are notified via AudioTrack.Listener. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=144202048
This commit is contained in:
parent
444811c010
commit
2906a2ea09
3 changed files with 87 additions and 110 deletions
|
|
@ -63,6 +63,19 @@ public final class AudioTrack {
|
||||||
*/
|
*/
|
||||||
public interface Listener {
|
public interface Listener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the audio track has been initialized with the specified {@code audioSessionId}.
|
||||||
|
*
|
||||||
|
* @param audioSessionId The audio session id.
|
||||||
|
*/
|
||||||
|
void onAudioSessionId(int audioSessionId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the audio track handles a buffer whose timestamp is discontinuous with the last
|
||||||
|
* buffer handled since it was reset.
|
||||||
|
*/
|
||||||
|
void onPositionDiscontinuity();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the audio track underruns.
|
* Called when the audio track underruns.
|
||||||
*
|
*
|
||||||
|
|
@ -74,13 +87,6 @@ public final class AudioTrack {
|
||||||
*/
|
*/
|
||||||
void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs);
|
void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs);
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the audio track has been initialized with the specified {@code audioSessionId}.
|
|
||||||
*
|
|
||||||
* @param audioSessionId The audio session id.
|
|
||||||
*/
|
|
||||||
void onAudioSessionId(int audioSessionId);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -144,15 +150,6 @@ public final class AudioTrack {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returned in the result of {@link #handleBuffer} if the buffer was discontinuous.
|
|
||||||
*/
|
|
||||||
public static final int RESULT_POSITION_DISCONTINUITY = 1;
|
|
||||||
/**
|
|
||||||
* Returned in the result of {@link #handleBuffer} if the buffer can be released.
|
|
||||||
*/
|
|
||||||
public static final int RESULT_BUFFER_CONSUMED = 2;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returned by {@link #getCurrentPositionUs} when the position is not set.
|
* Returned by {@link #getCurrentPositionUs} when the position is not set.
|
||||||
*/
|
*/
|
||||||
|
|
@ -591,24 +588,21 @@ public final class AudioTrack {
|
||||||
* Attempts to write data from a {@link ByteBuffer} to the audio track, starting from its current
|
* Attempts to write data from a {@link ByteBuffer} to the audio track, starting from its current
|
||||||
* position and ending at its limit (exclusive). The position of the {@link ByteBuffer} is
|
* position and ending at its limit (exclusive). The position of the {@link ByteBuffer} is
|
||||||
* advanced by the number of bytes that were successfully written.
|
* advanced by the number of bytes that were successfully written.
|
||||||
|
* {@link Listener#onPositionDiscontinuity()} will be called if {@code presentationTimeUs} is
|
||||||
|
* discontinuous with the last buffer handled since the track was reset.
|
||||||
* <p>
|
* <p>
|
||||||
* Returns a bit field containing {@link #RESULT_BUFFER_CONSUMED} if the data was written in full,
|
* Returns whether the data was written in full. If the data was not written in full then the same
|
||||||
* and {@link #RESULT_POSITION_DISCONTINUITY} if the buffer was discontinuous with previously
|
* {@link ByteBuffer} must be provided to subsequent calls until it has been fully consumed,
|
||||||
* written data.
|
* except in the case of an interleaving call to {@link #reset()} (or an interleaving call to
|
||||||
* <p>
|
* {@link #configure(String, int, int, int, int)} that caused the track to be reset).
|
||||||
* If the data was not written in full then the same {@link ByteBuffer} must be provided to
|
|
||||||
* subsequent calls until it has been fully consumed, except in the case of an interleaving call
|
|
||||||
* to {@link #configure} or {@link #reset}.
|
|
||||||
*
|
*
|
||||||
* @param buffer The buffer containing audio data to play back.
|
* @param buffer The buffer containing audio data to play back.
|
||||||
* @param presentationTimeUs Presentation timestamp of the next buffer in microseconds.
|
* @param presentationTimeUs Presentation timestamp of the next buffer in microseconds.
|
||||||
* @return A bit field with {@link #RESULT_BUFFER_CONSUMED} if the buffer can be released, and
|
* @return Whether the buffer was consumed fully.
|
||||||
* {@link #RESULT_POSITION_DISCONTINUITY} if the buffer was not contiguous with previously
|
|
||||||
* written data.
|
|
||||||
* @throws InitializationException If an error occurs initializing the track.
|
* @throws InitializationException If an error occurs initializing the track.
|
||||||
* @throws WriteException If an error occurs writing the audio data.
|
* @throws WriteException If an error occurs writing the audio data.
|
||||||
*/
|
*/
|
||||||
public int handleBuffer(ByteBuffer buffer, long presentationTimeUs)
|
public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs)
|
||||||
throws InitializationException, WriteException {
|
throws InitializationException, WriteException {
|
||||||
if (!isInitialized()) {
|
if (!isInitialized()) {
|
||||||
initialize();
|
initialize();
|
||||||
|
|
@ -623,12 +617,12 @@ public final class AudioTrack {
|
||||||
long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs;
|
long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs;
|
||||||
listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs);
|
listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs);
|
||||||
}
|
}
|
||||||
int result = writeBuffer(buffer, presentationTimeUs);
|
boolean result = writeBuffer(buffer, presentationTimeUs);
|
||||||
lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime();
|
lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int writeBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException {
|
private boolean writeBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException {
|
||||||
boolean isNewSourceBuffer = currentSourceBuffer == null;
|
boolean isNewSourceBuffer = currentSourceBuffer == null;
|
||||||
Assertions.checkState(isNewSourceBuffer || currentSourceBuffer == buffer);
|
Assertions.checkState(isNewSourceBuffer || currentSourceBuffer == buffer);
|
||||||
currentSourceBuffer = buffer;
|
currentSourceBuffer = buffer;
|
||||||
|
|
@ -637,7 +631,7 @@ public final class AudioTrack {
|
||||||
// An AC-3 audio track continues to play data written while it is paused. Stop writing so its
|
// An AC-3 audio track continues to play data written while it is paused. Stop writing so its
|
||||||
// buffer empties. See [Internal: b/18899620].
|
// buffer empties. See [Internal: b/18899620].
|
||||||
if (audioTrack.getPlayState() == PLAYSTATE_PAUSED) {
|
if (audioTrack.getPlayState() == PLAYSTATE_PAUSED) {
|
||||||
return 0;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A new AC-3 audio track's playback position continues to increase from the old track's
|
// A new AC-3 audio track's playback position continues to increase from the old track's
|
||||||
|
|
@ -645,7 +639,7 @@ public final class AudioTrack {
|
||||||
// head position actually returns to zero.
|
// head position actually returns to zero.
|
||||||
if (audioTrack.getPlayState() == PLAYSTATE_STOPPED
|
if (audioTrack.getPlayState() == PLAYSTATE_STOPPED
|
||||||
&& audioTrackUtil.getPlaybackHeadPosition() != 0) {
|
&& audioTrackUtil.getPlaybackHeadPosition() != 0) {
|
||||||
return 0;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -656,7 +650,7 @@ public final class AudioTrack {
|
||||||
if (!currentSourceBuffer.hasRemaining()) {
|
if (!currentSourceBuffer.hasRemaining()) {
|
||||||
// The buffer is empty.
|
// The buffer is empty.
|
||||||
currentSourceBuffer = null;
|
currentSourceBuffer = null;
|
||||||
return RESULT_BUFFER_CONSUMED;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
useResampledBuffer = targetEncoding != sourceEncoding;
|
useResampledBuffer = targetEncoding != sourceEncoding;
|
||||||
|
|
@ -689,7 +683,7 @@ public final class AudioTrack {
|
||||||
// number of bytes submitted.
|
// number of bytes submitted.
|
||||||
startMediaTimeUs += (presentationTimeUs - expectedPresentationTimeUs);
|
startMediaTimeUs += (presentationTimeUs - expectedPresentationTimeUs);
|
||||||
startMediaTimeState = START_IN_SYNC;
|
startMediaTimeState = START_IN_SYNC;
|
||||||
result |= RESULT_POSITION_DISCONTINUITY;
|
listener.onPositionDiscontinuity();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Util.SDK_INT < 21) {
|
if (Util.SDK_INT < 21) {
|
||||||
|
|
@ -739,9 +733,9 @@ public final class AudioTrack {
|
||||||
submittedEncodedFrames += framesPerEncodedSample;
|
submittedEncodedFrames += framesPerEncodedSample;
|
||||||
}
|
}
|
||||||
currentSourceBuffer = null;
|
currentSourceBuffer = null;
|
||||||
result |= RESULT_BUFFER_CONSUMED;
|
return true;
|
||||||
}
|
}
|
||||||
return result;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -228,29 +228,26 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the audio session id becomes known. Once the id is known it will not change (and
|
* Called when the audio session id becomes known. The default implementation is a no-op. One
|
||||||
* hence this method will not be called again) unless the renderer is disabled and then
|
* reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in
|
||||||
* subsequently re-enabled.
|
* order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances
|
||||||
* <p>
|
* should be released in {@link #onDisabled()} (if not before).
|
||||||
* The default implementation is a no-op. One reason for overriding this method would be to
|
|
||||||
* instantiate and enable a {@link Virtualizer} in order to spatialize the audio channels. For
|
|
||||||
* this use case, any {@link Virtualizer} instances should be released in {@link #onDisabled()}
|
|
||||||
* (if not before).
|
|
||||||
*
|
*
|
||||||
* @param audioSessionId The audio session id.
|
* @see AudioTrack.Listener#onAudioSessionId(int)
|
||||||
*/
|
*/
|
||||||
protected void onAudioSessionId(int audioSessionId) {
|
protected void onAudioSessionId(int audioSessionId) {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when an {@link AudioTrack} underrun occurs.
|
* @see AudioTrack.Listener#onPositionDiscontinuity()
|
||||||
*
|
*/
|
||||||
* @param bufferSize The size of the {@link AudioTrack}'s buffer, in bytes.
|
protected void onAudioTrackPositionDiscontinuity() {
|
||||||
* @param bufferSizeMs The size of the {@link AudioTrack}'s buffer, in milliseconds, if it is
|
// Do nothing.
|
||||||
* configured for PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output,
|
}
|
||||||
* as the buffered media can have a variable bitrate so the duration may be unknown.
|
|
||||||
* @param elapsedSinceLastFeedMs The time since the {@link AudioTrack} was last fed data.
|
/**
|
||||||
|
* @see AudioTrack.Listener#onUnderrun(int, long, long)
|
||||||
*/
|
*/
|
||||||
protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs,
|
protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs,
|
||||||
long elapsedSinceLastFeedMs) {
|
long elapsedSinceLastFeedMs) {
|
||||||
|
|
@ -335,26 +332,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
int handleBufferResult;
|
|
||||||
try {
|
try {
|
||||||
handleBufferResult = audioTrack.handleBuffer(buffer, bufferPresentationTimeUs);
|
if (audioTrack.handleBuffer(buffer, bufferPresentationTimeUs)) {
|
||||||
} catch (AudioTrack.InitializationException | AudioTrack.WriteException e) {
|
|
||||||
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we are out of sync, allow currentPositionUs to jump backwards.
|
|
||||||
if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) {
|
|
||||||
handleAudioTrackDiscontinuity();
|
|
||||||
allowPositionDiscontinuity = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Release the buffer if it was consumed.
|
|
||||||
if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) {
|
|
||||||
codec.releaseOutputBuffer(bufferIndex, false);
|
codec.releaseOutputBuffer(bufferIndex, false);
|
||||||
decoderCounters.renderedOutputBufferCount++;
|
decoderCounters.renderedOutputBufferCount++;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
} catch (AudioTrack.InitializationException | AudioTrack.WriteException e) {
|
||||||
|
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -363,10 +349,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||||
audioTrack.handleEndOfStream();
|
audioTrack.handleEndOfStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void handleAudioTrackDiscontinuity() {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
|
public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
|
||||||
switch (messageType) {
|
switch (messageType) {
|
||||||
|
|
@ -388,19 +370,25 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||||
|
|
||||||
private final class AudioTrackListener implements AudioTrack.Listener {
|
private final class AudioTrackListener implements AudioTrack.Listener {
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
|
||||||
eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
|
||||||
MediaCodecAudioRenderer.this.onAudioTrackUnderrun(bufferSize, bufferSizeMs,
|
|
||||||
elapsedSinceLastFeedMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAudioSessionId(int audioSessionId) {
|
public void onAudioSessionId(int audioSessionId) {
|
||||||
eventDispatcher.audioSessionId(audioSessionId);
|
eventDispatcher.audioSessionId(audioSessionId);
|
||||||
MediaCodecAudioRenderer.this.onAudioSessionId(audioSessionId);
|
MediaCodecAudioRenderer.this.onAudioSessionId(audioSessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPositionDiscontinuity() {
|
||||||
|
onAudioTrackPositionDiscontinuity();
|
||||||
|
// We are out of sync so allow currentPositionUs to jump backwards.
|
||||||
|
MediaCodecAudioRenderer.this.allowPositionDiscontinuity = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||||
|
eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||||
|
onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -183,29 +183,26 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the audio session id becomes known. Once the id is known it will not change (and
|
* Called when the audio session id becomes known. The default implementation is a no-op. One
|
||||||
* hence this method will not be called again) unless the renderer is disabled and then
|
* reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in
|
||||||
* subsequently re-enabled.
|
* order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances
|
||||||
* <p>
|
* should be released in {@link #onDisabled()} (if not before).
|
||||||
* The default implementation is a no-op. One reason for overriding this method would be to
|
|
||||||
* instantiate and enable a {@link Virtualizer} in order to spatialize the audio channels. For
|
|
||||||
* this use case, any {@link Virtualizer} instances should be released in {@link #onDisabled()}
|
|
||||||
* (if not before).
|
|
||||||
*
|
*
|
||||||
* @param audioSessionId The audio session id.
|
* @see AudioTrack.Listener#onAudioSessionId(int)
|
||||||
*/
|
*/
|
||||||
protected void onAudioSessionId(int audioSessionId) {
|
protected void onAudioSessionId(int audioSessionId) {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when an {@link AudioTrack} underrun occurs.
|
* @see AudioTrack.Listener#onPositionDiscontinuity()
|
||||||
*
|
*/
|
||||||
* @param bufferSize The size of the {@link AudioTrack}'s buffer, in bytes.
|
protected void onAudioTrackPositionDiscontinuity() {
|
||||||
* @param bufferSizeMs The size of the {@link AudioTrack}'s buffer, in milliseconds, if it is
|
// Do nothing.
|
||||||
* configured for PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output,
|
}
|
||||||
* as the buffered media can have a variable bitrate so the duration may be unknown.
|
|
||||||
* @param elapsedSinceLastFeedMs The time since the {@link AudioTrack} was last fed data.
|
/**
|
||||||
|
* @see AudioTrack.Listener#onUnderrun(int, long, long)
|
||||||
*/
|
*/
|
||||||
protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs,
|
protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs,
|
||||||
long elapsedSinceLastFeedMs) {
|
long elapsedSinceLastFeedMs) {
|
||||||
|
|
@ -271,15 +268,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||||
audioTrackNeedsConfigure = false;
|
audioTrackNeedsConfigure = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int handleBufferResult = audioTrack.handleBuffer(outputBuffer.data, outputBuffer.timeUs);
|
if (audioTrack.handleBuffer(outputBuffer.data, outputBuffer.timeUs)) {
|
||||||
|
|
||||||
// If we are out of sync, allow currentPositionUs to jump backwards.
|
|
||||||
if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) {
|
|
||||||
allowPositionDiscontinuity = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Release the buffer if it was consumed.
|
|
||||||
if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) {
|
|
||||||
decoderCounters.renderedOutputBufferCount++;
|
decoderCounters.renderedOutputBufferCount++;
|
||||||
outputBuffer.release();
|
outputBuffer.release();
|
||||||
outputBuffer = null;
|
outputBuffer = null;
|
||||||
|
|
@ -563,19 +552,25 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||||
|
|
||||||
private final class AudioTrackListener implements AudioTrack.Listener {
|
private final class AudioTrackListener implements AudioTrack.Listener {
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
|
||||||
eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
|
||||||
SimpleDecoderAudioRenderer.this.onAudioTrackUnderrun(bufferSize, bufferSizeMs,
|
|
||||||
elapsedSinceLastFeedMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAudioSessionId(int audioSessionId) {
|
public void onAudioSessionId(int audioSessionId) {
|
||||||
eventDispatcher.audioSessionId(audioSessionId);
|
eventDispatcher.audioSessionId(audioSessionId);
|
||||||
SimpleDecoderAudioRenderer.this.onAudioSessionId(audioSessionId);
|
SimpleDecoderAudioRenderer.this.onAudioSessionId(audioSessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPositionDiscontinuity() {
|
||||||
|
onAudioTrackPositionDiscontinuity();
|
||||||
|
// We are out of sync so allow currentPositionUs to jump backwards.
|
||||||
|
SimpleDecoderAudioRenderer.this.allowPositionDiscontinuity = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||||
|
eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||||
|
onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue