Discard extra silent channels on Samsung Galaxy S6/S7.
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148654495
This commit is contained in:
parent
2411d0fc76
commit
b3cfeaa17b
@ -270,6 +270,7 @@ public final class AudioTrack {
|
|||||||
public static boolean failOnSpuriousAudioTimestamp = false;
|
public static boolean failOnSpuriousAudioTimestamp = false;
|
||||||
|
|
||||||
private final AudioCapabilities audioCapabilities;
|
private final AudioCapabilities audioCapabilities;
|
||||||
|
private final ChannelMappingBufferProcessor channelMappingBufferProcessor;
|
||||||
private final BufferProcessor[] availableBufferProcessors;
|
private final BufferProcessor[] availableBufferProcessors;
|
||||||
private final Listener listener;
|
private final Listener listener;
|
||||||
private final ConditionVariable releasingConditionVariable;
|
private final ConditionVariable releasingConditionVariable;
|
||||||
@ -343,9 +344,11 @@ public final class AudioTrack {
|
|||||||
public AudioTrack(AudioCapabilities audioCapabilities, BufferProcessor[] bufferProcessors,
|
public AudioTrack(AudioCapabilities audioCapabilities, BufferProcessor[] bufferProcessors,
|
||||||
Listener listener) {
|
Listener listener) {
|
||||||
this.audioCapabilities = audioCapabilities;
|
this.audioCapabilities = audioCapabilities;
|
||||||
availableBufferProcessors = new BufferProcessor[bufferProcessors.length + 1];
|
channelMappingBufferProcessor = new ChannelMappingBufferProcessor();
|
||||||
|
availableBufferProcessors = new BufferProcessor[bufferProcessors.length + 2];
|
||||||
availableBufferProcessors[0] = new ResamplingBufferProcessor();
|
availableBufferProcessors[0] = new ResamplingBufferProcessor();
|
||||||
System.arraycopy(bufferProcessors, 0, availableBufferProcessors, 1, bufferProcessors.length);
|
availableBufferProcessors[1] = channelMappingBufferProcessor;
|
||||||
|
System.arraycopy(bufferProcessors, 0, availableBufferProcessors, 2, bufferProcessors.length);
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
releasingConditionVariable = new ConditionVariable(true);
|
releasingConditionVariable = new ConditionVariable(true);
|
||||||
if (Util.SDK_INT >= 18) {
|
if (Util.SDK_INT >= 18) {
|
||||||
@ -449,6 +452,30 @@ public final class AudioTrack {
|
|||||||
*/
|
*/
|
||||||
public void configure(String mimeType, int channelCount, int sampleRate,
|
public void configure(String mimeType, int channelCount, int sampleRate,
|
||||||
@C.PcmEncoding int pcmEncoding, int specifiedBufferSize) throws ConfigurationException {
|
@C.PcmEncoding int pcmEncoding, int specifiedBufferSize) throws ConfigurationException {
|
||||||
|
configure(mimeType, channelCount, sampleRate, pcmEncoding, specifiedBufferSize, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures (or reconfigures) the audio track.
|
||||||
|
*
|
||||||
|
* @param mimeType The mime type.
|
||||||
|
* @param channelCount The number of channels.
|
||||||
|
* @param sampleRate The sample rate in Hz.
|
||||||
|
* @param pcmEncoding For PCM formats, the encoding used. One of {@link C#ENCODING_PCM_16BIT},
|
||||||
|
* {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} and
|
||||||
|
* {@link C#ENCODING_PCM_32BIT}.
|
||||||
|
* @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a
|
||||||
|
* suitable buffer size automatically.
|
||||||
|
* @param outputChannels A mapping from input to output channels that is applied to this track's
|
||||||
|
* input as a preprocessing step, if handling PCM input. Specify {@code null} to leave the
|
||||||
|
* input unchanged. Otherwise, the element at index {@code i} specifies index of the input
|
||||||
|
* channel to map to output channel {@code i} when preprocessing input buffers. After the
|
||||||
|
* map is applied the audio data will have {@code outputChannels.length} channels.
|
||||||
|
* @throws ConfigurationException If an error occurs configuring the track.
|
||||||
|
*/
|
||||||
|
public void configure(String mimeType, int channelCount, int sampleRate,
|
||||||
|
@C.PcmEncoding int pcmEncoding, int specifiedBufferSize, int[] outputChannels)
|
||||||
|
throws ConfigurationException {
|
||||||
boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType);
|
boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType);
|
||||||
@C.Encoding int encoding = passthrough ? getEncodingForMimeType(mimeType) : pcmEncoding;
|
@C.Encoding int encoding = passthrough ? getEncodingForMimeType(mimeType) : pcmEncoding;
|
||||||
boolean flush = false;
|
boolean flush = false;
|
||||||
@ -456,17 +483,15 @@ public final class AudioTrack {
|
|||||||
pcmFrameSize = Util.getPcmFrameSize(pcmEncoding, channelCount);
|
pcmFrameSize = Util.getPcmFrameSize(pcmEncoding, channelCount);
|
||||||
|
|
||||||
// Reconfigure the buffer processors.
|
// Reconfigure the buffer processors.
|
||||||
|
channelMappingBufferProcessor.setChannelMap(outputChannels);
|
||||||
ArrayList<BufferProcessor> newBufferProcessors = new ArrayList<>();
|
ArrayList<BufferProcessor> newBufferProcessors = new ArrayList<>();
|
||||||
for (BufferProcessor bufferProcessor : availableBufferProcessors) {
|
for (BufferProcessor bufferProcessor : availableBufferProcessors) {
|
||||||
boolean wasActive = bufferProcessor.isActive();
|
|
||||||
try {
|
try {
|
||||||
flush |= bufferProcessor.configure(sampleRate, channelCount, encoding);
|
flush |= bufferProcessor.configure(sampleRate, channelCount, encoding);
|
||||||
} catch (BufferProcessor.UnhandledFormatException e) {
|
} catch (BufferProcessor.UnhandledFormatException e) {
|
||||||
throw new ConfigurationException(e);
|
throw new ConfigurationException(e);
|
||||||
}
|
}
|
||||||
boolean isActive = bufferProcessor.isActive();
|
if (bufferProcessor.isActive()) {
|
||||||
flush |= isActive != wasActive;
|
|
||||||
if (isActive) {
|
|
||||||
newBufferProcessors.add(bufferProcessor);
|
newBufferProcessors.add(bufferProcessor);
|
||||||
channelCount = bufferProcessor.getOutputChannelCount();
|
channelCount = bufferProcessor.getOutputChannelCount();
|
||||||
encoding = bufferProcessor.getOutputEncoding();
|
encoding = bufferProcessor.getOutputEncoding();
|
||||||
|
@ -42,16 +42,18 @@ public interface BufferProcessor {
|
|||||||
ByteBuffer EMPTY_BUFFER = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder());
|
ByteBuffer EMPTY_BUFFER = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures the processor to process input buffers with the specified format and returns whether
|
* Configures the processor to process input buffers with the specified format. After calling this
|
||||||
* the processor must be flushed. After calling this method, {@link #isActive()} returns whether
|
* method, {@link #isActive()} returns whether the processor needs to handle buffers; if not, the
|
||||||
* the processor needs to handle buffers; if not, the processor will not accept any buffers until
|
* processor will not accept any buffers until it is reconfigured. Returns {@code true} if the
|
||||||
* it is reconfigured. {@link #getOutputChannelCount()} and {@link #getOutputEncoding()} return
|
* processor must be flushed, or if the value returned by {@link #isActive()} has changed as a
|
||||||
* the processor's output format.
|
* result of the call. If it's active, {@link #getOutputChannelCount()} and
|
||||||
|
* {@link #getOutputEncoding()} return the processor's output format.
|
||||||
*
|
*
|
||||||
* @param sampleRateHz The sample rate of input audio in Hz.
|
* @param sampleRateHz The sample rate of input audio in Hz.
|
||||||
* @param channelCount The number of interleaved channels in input audio.
|
* @param channelCount The number of interleaved channels in input audio.
|
||||||
* @param encoding The encoding of input audio.
|
* @param encoding The encoding of input audio.
|
||||||
* @return Whether the processor must be flushed.
|
* @return {@code true} if the processor must be flushed or the value returned by
|
||||||
|
* {@link #isActive()} has changed as a result of the call.
|
||||||
* @throws UnhandledFormatException Thrown if the specified format can't be handled as input.
|
* @throws UnhandledFormatException Thrown if the specified format can't be handled as input.
|
||||||
*/
|
*/
|
||||||
boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding)
|
boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding)
|
||||||
|
@ -0,0 +1,155 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.audio;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.C.Encoding;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buffer processor that applies a mapping from input channels onto specified output channels. This
|
||||||
|
* can be used to reorder, duplicate or discard channels.
|
||||||
|
*/
|
||||||
|
/* package */ final class ChannelMappingBufferProcessor implements BufferProcessor {
|
||||||
|
|
||||||
|
private int channelCount;
|
||||||
|
private int sampleRateHz;
|
||||||
|
private int[] pendingOutputChannels;
|
||||||
|
|
||||||
|
private boolean active;
|
||||||
|
private int[] outputChannels;
|
||||||
|
private ByteBuffer buffer;
|
||||||
|
private ByteBuffer outputBuffer;
|
||||||
|
private boolean inputEnded;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new processor that applies a channel mapping.
|
||||||
|
*/
|
||||||
|
public ChannelMappingBufferProcessor() {
|
||||||
|
buffer = EMPTY_BUFFER;
|
||||||
|
outputBuffer = EMPTY_BUFFER;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)}
|
||||||
|
* to start using the new channel map.
|
||||||
|
*
|
||||||
|
* @see AudioTrack#configure(String, int, int, int, int, int[])
|
||||||
|
*/
|
||||||
|
public void setChannelMap(int[] outputChannels) {
|
||||||
|
pendingOutputChannels = outputChannels;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean configure(int sampleRateHz, int channelCount, @Encoding int encoding)
|
||||||
|
throws UnhandledFormatException {
|
||||||
|
boolean outputChannelsChanged = !Arrays.equals(pendingOutputChannels, outputChannels);
|
||||||
|
outputChannels = pendingOutputChannels;
|
||||||
|
if (outputChannels == null) {
|
||||||
|
active = false;
|
||||||
|
return outputChannelsChanged;
|
||||||
|
}
|
||||||
|
if (encoding != C.ENCODING_PCM_16BIT) {
|
||||||
|
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
|
||||||
|
}
|
||||||
|
if (!outputChannelsChanged && this.sampleRateHz == sampleRateHz
|
||||||
|
&& this.channelCount == channelCount) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.sampleRateHz = sampleRateHz;
|
||||||
|
this.channelCount = channelCount;
|
||||||
|
|
||||||
|
active = channelCount != outputChannels.length;
|
||||||
|
for (int i = 0; i < outputChannels.length; i++) {
|
||||||
|
int channelIndex = outputChannels[i];
|
||||||
|
if (channelIndex >= channelCount) {
|
||||||
|
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
|
||||||
|
}
|
||||||
|
active |= (channelIndex != i);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isActive() {
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOutputChannelCount() {
|
||||||
|
return outputChannels == null ? channelCount : outputChannels.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOutputEncoding() {
|
||||||
|
return C.ENCODING_PCM_16BIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void queueInput(ByteBuffer inputBuffer) {
|
||||||
|
int position = inputBuffer.position();
|
||||||
|
int limit = inputBuffer.limit();
|
||||||
|
int frameCount = (limit - position) / (2 * channelCount);
|
||||||
|
int outputSize = frameCount * outputChannels.length * 2;
|
||||||
|
if (buffer.capacity() < outputSize) {
|
||||||
|
buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder());
|
||||||
|
} else {
|
||||||
|
buffer.clear();
|
||||||
|
}
|
||||||
|
while (position < limit) {
|
||||||
|
for (int channelIndex : outputChannels) {
|
||||||
|
buffer.putShort(inputBuffer.getShort(position + 2 * channelIndex));
|
||||||
|
}
|
||||||
|
position += channelCount * 2;
|
||||||
|
}
|
||||||
|
inputBuffer.position(limit);
|
||||||
|
buffer.flip();
|
||||||
|
outputBuffer = buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void queueEndOfStream() {
|
||||||
|
inputEnded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer getOutput() {
|
||||||
|
ByteBuffer outputBuffer = this.outputBuffer;
|
||||||
|
this.outputBuffer = EMPTY_BUFFER;
|
||||||
|
return outputBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("ReferenceEquality")
|
||||||
|
@Override
|
||||||
|
public boolean isEnded() {
|
||||||
|
return inputEnded && outputBuffer == EMPTY_BUFFER;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void flush() {
|
||||||
|
outputBuffer = EMPTY_BUFFER;
|
||||||
|
inputEnded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void release() {
|
||||||
|
flush();
|
||||||
|
buffer = EMPTY_BUFFER;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -47,8 +47,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||||||
private final AudioTrack audioTrack;
|
private final AudioTrack audioTrack;
|
||||||
|
|
||||||
private boolean passthroughEnabled;
|
private boolean passthroughEnabled;
|
||||||
|
private boolean codecNeedsDiscardChannelsWorkaround;
|
||||||
private android.media.MediaFormat passthroughMediaFormat;
|
private android.media.MediaFormat passthroughMediaFormat;
|
||||||
private int pcmEncoding;
|
private int pcmEncoding;
|
||||||
|
private int channelCount;
|
||||||
private long currentPositionUs;
|
private long currentPositionUs;
|
||||||
private boolean allowPositionDiscontinuity;
|
private boolean allowPositionDiscontinuity;
|
||||||
|
|
||||||
@ -188,6 +190,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||||||
@Override
|
@Override
|
||||||
protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
|
protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
|
||||||
MediaCrypto crypto) {
|
MediaCrypto crypto) {
|
||||||
|
codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name);
|
||||||
if (passthroughEnabled) {
|
if (passthroughEnabled) {
|
||||||
// Override the MIME type used to configure the codec if we are using a passthrough decoder.
|
// Override the MIME type used to configure the codec if we are using a passthrough decoder.
|
||||||
passthroughMediaFormat = format.getFrameworkMediaFormatV16();
|
passthroughMediaFormat = format.getFrameworkMediaFormatV16();
|
||||||
@ -219,6 +222,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||||||
// output 16-bit PCM.
|
// output 16-bit PCM.
|
||||||
pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding
|
pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding
|
||||||
: C.ENCODING_PCM_16BIT;
|
: C.ENCODING_PCM_16BIT;
|
||||||
|
channelCount = newFormat.channelCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -230,8 +234,18 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||||||
MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat;
|
MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat;
|
||||||
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
|
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
|
||||||
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
|
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
|
||||||
|
int[] channelMap;
|
||||||
|
if (codecNeedsDiscardChannelsWorkaround && channelCount == 6 && this.channelCount < 6) {
|
||||||
|
channelMap = new int[this.channelCount];
|
||||||
|
for (int i = 0; i < this.channelCount; i++) {
|
||||||
|
channelMap[i] = i;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channelMap = null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0);
|
audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0, channelMap);
|
||||||
} catch (AudioTrack.ConfigurationException e) {
|
} catch (AudioTrack.ConfigurationException e) {
|
||||||
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
||||||
}
|
}
|
||||||
@ -388,6 +402,20 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the decoder is known to output six audio channels when provided with input with
|
||||||
|
* fewer than six channels.
|
||||||
|
* <p>
|
||||||
|
* See [Internal: b/35655036].
|
||||||
|
*/
|
||||||
|
private static boolean codecNeedsDiscardChannelsWorkaround(String codecName) {
|
||||||
|
// The workaround applies to Samsung Galaxy S6 and Samsung Galaxy S7.
|
||||||
|
return Util.SDK_INT < 24 && "OMX.SEC.aac.dec".equals(codecName)
|
||||||
|
&& "samsung".equals(Util.MANUFACTURER)
|
||||||
|
&& (Util.DEVICE.startsWith("zeroflte") || Util.DEVICE.startsWith("herolte")
|
||||||
|
|| Util.DEVICE.startsWith("heroqlte"));
|
||||||
|
}
|
||||||
|
|
||||||
private final class AudioTrackListener implements AudioTrack.Listener {
|
private final class AudioTrackListener implements AudioTrack.Listener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -25,6 +25,7 @@ import java.nio.ByteOrder;
|
|||||||
*/
|
*/
|
||||||
/* package */ final class ResamplingBufferProcessor implements BufferProcessor {
|
/* package */ final class ResamplingBufferProcessor implements BufferProcessor {
|
||||||
|
|
||||||
|
private int sampleRateHz;
|
||||||
private int channelCount;
|
private int channelCount;
|
||||||
@C.PcmEncoding
|
@C.PcmEncoding
|
||||||
private int encoding;
|
private int encoding;
|
||||||
@ -36,6 +37,8 @@ import java.nio.ByteOrder;
|
|||||||
* Creates a new buffer processor that converts audio data to {@link C#ENCODING_PCM_16BIT}.
|
* Creates a new buffer processor that converts audio data to {@link C#ENCODING_PCM_16BIT}.
|
||||||
*/
|
*/
|
||||||
public ResamplingBufferProcessor() {
|
public ResamplingBufferProcessor() {
|
||||||
|
sampleRateHz = Format.NO_VALUE;
|
||||||
|
channelCount = Format.NO_VALUE;
|
||||||
encoding = C.ENCODING_INVALID;
|
encoding = C.ENCODING_INVALID;
|
||||||
buffer = EMPTY_BUFFER;
|
buffer = EMPTY_BUFFER;
|
||||||
outputBuffer = EMPTY_BUFFER;
|
outputBuffer = EMPTY_BUFFER;
|
||||||
@ -48,11 +51,12 @@ import java.nio.ByteOrder;
|
|||||||
&& encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) {
|
&& encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) {
|
||||||
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
|
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
|
||||||
}
|
}
|
||||||
this.channelCount = channelCount;
|
if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount
|
||||||
if (this.encoding == encoding) {
|
&& this.encoding == encoding) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
this.sampleRateHz = sampleRateHz;
|
||||||
|
this.channelCount = channelCount;
|
||||||
this.encoding = encoding;
|
this.encoding = encoding;
|
||||||
if (encoding == C.ENCODING_PCM_16BIT) {
|
if (encoding == C.ENCODING_PCM_16BIT) {
|
||||||
buffer = EMPTY_BUFFER;
|
buffer = EMPTY_BUFFER;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user