Commit 93b5b947 by olly Committed by kim-vde

Support decode-only metadata buffers

PiperOrigin-RevId: 319798613
parent 7474547e
...@@ -31,7 +31,8 @@ public interface MetadataDecoder { ...@@ -31,7 +31,8 @@ public interface MetadataDecoder {
* ByteBuffer#hasArray()} is true. * ByteBuffer#hasArray()} is true.
* *
* @param inputBuffer The input buffer to decode. * @param inputBuffer The input buffer to decode.
* @return The decoded metadata object, or null if the metadata could not be decoded. * @return The decoded metadata object, or {@code null} if the metadata could not be decoded or if
* {@link MetadataInputBuffer#isDecodeOnly()} was set on the input buffer.
*/ */
@Nullable @Nullable
Metadata decode(MetadataInputBuffer inputBuffer); Metadata decode(MetadataInputBuffer inputBuffer);
......
/*
* Copyright (C) 2020 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.metadata;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions;
import java.nio.ByteBuffer;
/**
* A {@link MetadataDecoder} base class that validates input buffers and discards any for which
* {@link MetadataInputBuffer#isDecodeOnly()} is {@code true}.
*/
public abstract class SimpleMetadataDecoder implements MetadataDecoder {
@Override
@Nullable
public final Metadata decode(MetadataInputBuffer inputBuffer) {
ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data);
Assertions.checkArgument(
buffer.position() == 0 && buffer.hasArray() && buffer.arrayOffset() == 0);
return inputBuffer.isDecodeOnly() ? null : decode(inputBuffer, buffer);
}
/**
* Called by {@link #decode(MetadataInputBuffer)} after input buffer validation has been
* performed, except in the case that {@link MetadataInputBuffer#isDecodeOnly()} is {@code true}.
*
* @param inputBuffer The input buffer to decode.
* @param buffer The input buffer's {@link MetadataInputBuffer#data data buffer}, for convenience.
* Validation by {@link #decode} guarantees that {@link ByteBuffer#hasArray()}, {@link
* ByteBuffer#position()} and {@link ByteBuffer#arrayOffset()} are {@code true}, {@code 0} and
* {@code 0} respectively.
* @return The decoded metadata object, or {@code null} if the metadata could not be decoded.
*/
@Nullable
protected abstract Metadata decode(MetadataInputBuffer inputBuffer, ByteBuffer buffer);
}
...@@ -16,21 +16,19 @@ ...@@ -16,21 +16,19 @@
package com.google.android.exoplayer2.metadata.emsg; package com.google.android.exoplayer2.metadata.emsg;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoder;
import com.google.android.exoplayer2.metadata.MetadataInputBuffer; import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
import com.google.android.exoplayer2.metadata.SimpleMetadataDecoder;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Arrays; import java.util.Arrays;
/** Decodes data encoded by {@link EventMessageEncoder}. */ /** Decodes data encoded by {@link EventMessageEncoder}. */
public final class EventMessageDecoder implements MetadataDecoder { public final class EventMessageDecoder extends SimpleMetadataDecoder {
@Override @Override
public Metadata decode(MetadataInputBuffer inputBuffer) { @SuppressWarnings("ByteBufferBackingArray") // Buffer validated by SimpleMetadataDecoder.decode
ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); protected Metadata decode(MetadataInputBuffer inputBuffer, ByteBuffer buffer) {
Assertions.checkArgument(
buffer.position() == 0 && buffer.hasArray() && buffer.arrayOffset() == 0);
return new Metadata(decode(new ParsableByteArray(buffer.array(), buffer.limit()))); return new Metadata(decode(new ParsableByteArray(buffer.array(), buffer.limit())));
} }
......
...@@ -18,9 +18,8 @@ package com.google.android.exoplayer2.metadata.id3; ...@@ -18,9 +18,8 @@ package com.google.android.exoplayer2.metadata.id3;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoder;
import com.google.android.exoplayer2.metadata.MetadataInputBuffer; import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.metadata.SimpleMetadataDecoder;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
...@@ -32,10 +31,8 @@ import java.util.Arrays; ...@@ -32,10 +31,8 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
/** /** Decodes ID3 tags. */
* Decodes ID3 tags. public final class Id3Decoder extends SimpleMetadataDecoder {
*/
public final class Id3Decoder implements MetadataDecoder {
/** /**
* A predicate for determining whether individual frames should be decoded. * A predicate for determining whether individual frames should be decoded.
...@@ -98,10 +95,8 @@ public final class Id3Decoder implements MetadataDecoder { ...@@ -98,10 +95,8 @@ public final class Id3Decoder implements MetadataDecoder {
@Override @Override
@Nullable @Nullable
public Metadata decode(MetadataInputBuffer inputBuffer) { @SuppressWarnings("ByteBufferBackingArray") // Buffer validated by SimpleMetadataDecoder.decode
ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); protected Metadata decode(MetadataInputBuffer inputBuffer, ByteBuffer buffer) {
Assertions.checkArgument(
buffer.position() == 0 && buffer.hasArray() && buffer.arrayOffset() == 0);
return decode(buffer.array(), buffer.limit()); return decode(buffer.array(), buffer.limit());
} }
...@@ -118,7 +113,7 @@ public final class Id3Decoder implements MetadataDecoder { ...@@ -118,7 +113,7 @@ public final class Id3Decoder implements MetadataDecoder {
List<Id3Frame> id3Frames = new ArrayList<>(); List<Id3Frame> id3Frames = new ArrayList<>();
ParsableByteArray id3Data = new ParsableByteArray(data, size); ParsableByteArray id3Data = new ParsableByteArray(data, size);
Id3Header id3Header = decodeHeader(id3Data); @Nullable Id3Header id3Header = decodeHeader(id3Data);
if (id3Header == null) { if (id3Header == null) {
return null; return null;
} }
...@@ -142,8 +137,14 @@ public final class Id3Decoder implements MetadataDecoder { ...@@ -142,8 +137,14 @@ public final class Id3Decoder implements MetadataDecoder {
} }
while (id3Data.bytesLeft() >= frameHeaderSize) { while (id3Data.bytesLeft() >= frameHeaderSize) {
Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack, @Nullable
frameHeaderSize, framePredicate); Id3Frame frame =
decodeFrame(
id3Header.majorVersion,
id3Data,
unsignedIntFrameSizeHack,
frameHeaderSize,
framePredicate);
if (frame != null) { if (frame != null) {
id3Frames.add(frame); id3Frames.add(frame);
} }
...@@ -660,8 +661,10 @@ public final class Id3Decoder implements MetadataDecoder { ...@@ -660,8 +661,10 @@ public final class Id3Decoder implements MetadataDecoder {
ArrayList<Id3Frame> subFrames = new ArrayList<>(); ArrayList<Id3Frame> subFrames = new ArrayList<>();
int limit = framePosition + frameSize; int limit = framePosition + frameSize;
while (id3Data.getPosition() < limit) { while (id3Data.getPosition() < limit) {
Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack, @Nullable
frameHeaderSize, framePredicate); Id3Frame frame =
decodeFrame(
majorVersion, id3Data, unsignedIntFrameSizeHack, frameHeaderSize, framePredicate);
if (frame != null) { if (frame != null) {
subFrames.add(frame); subFrames.add(frame);
} }
......
/*
* Copyright (C) 2020 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.metadata;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import java.nio.ByteBuffer;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link SimpleMetadataDecoder}. */
@RunWith(AndroidJUnit4.class)
public class SimpleMetadataDecoderTest {
@Test
public void decode_nullDataInputBuffer_throwsNullPointerException() {
TestSimpleMetadataDecoder decoder = new TestSimpleMetadataDecoder();
MetadataInputBuffer nullDataInputBuffer = new MetadataInputBuffer();
nullDataInputBuffer.data = null;
assertThrows(NullPointerException.class, () -> decoder.decode(nullDataInputBuffer));
assertThat(decoder.decodeWasCalled).isFalse();
}
@Test
public void decode_directDataInputBuffer_throwsIllegalArgumentException() {
TestSimpleMetadataDecoder decoder = new TestSimpleMetadataDecoder();
MetadataInputBuffer directDataInputBuffer = new MetadataInputBuffer();
directDataInputBuffer.data = ByteBuffer.allocateDirect(8);
assertThrows(IllegalArgumentException.class, () -> decoder.decode(directDataInputBuffer));
assertThat(decoder.decodeWasCalled).isFalse();
}
@Test
public void decode_nonZeroPositionDataInputBuffer_throwsIllegalArgumentException() {
TestSimpleMetadataDecoder decoder = new TestSimpleMetadataDecoder();
MetadataInputBuffer nonZeroPositionDataInputBuffer = new MetadataInputBuffer();
nonZeroPositionDataInputBuffer.data = ByteBuffer.wrap(new byte[8]);
nonZeroPositionDataInputBuffer.data.position(1);
assertThrows(
IllegalArgumentException.class, () -> decoder.decode(nonZeroPositionDataInputBuffer));
assertThat(decoder.decodeWasCalled).isFalse();
}
@Test
public void decode_nonZeroOffsetDataInputBuffer_throwsIllegalArgumentException() {
TestSimpleMetadataDecoder decoder = new TestSimpleMetadataDecoder();
MetadataInputBuffer directDataInputBuffer = new MetadataInputBuffer();
directDataInputBuffer.data = ByteBuffer.wrap(new byte[8], /* offset= */ 4, /* length= */ 4);
assertThrows(IllegalArgumentException.class, () -> decoder.decode(directDataInputBuffer));
assertThat(decoder.decodeWasCalled).isFalse();
}
@Test
public void decode_decodeOnlyBuffer_notPassedToDecodeInternal() {
TestSimpleMetadataDecoder decoder = new TestSimpleMetadataDecoder();
MetadataInputBuffer decodeOnlyBuffer = new MetadataInputBuffer();
decodeOnlyBuffer.data = ByteBuffer.wrap(new byte[8]);
decodeOnlyBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY);
assertThat(decoder.decode(decodeOnlyBuffer)).isNull();
assertThat(decoder.decodeWasCalled).isFalse();
}
@Test
public void decode_returnsDecodeInternalResult() {
TestSimpleMetadataDecoder decoder = new TestSimpleMetadataDecoder();
MetadataInputBuffer buffer = new MetadataInputBuffer();
buffer.data = ByteBuffer.wrap(new byte[8]);
assertThat(decoder.decode(buffer)).isSameInstanceAs(decoder.result);
assertThat(decoder.decodeWasCalled).isTrue();
}
private static final class TestSimpleMetadataDecoder extends SimpleMetadataDecoder {
public final Metadata result;
public boolean decodeWasCalled;
public TestSimpleMetadataDecoder() {
result = new Metadata();
}
@Nullable
@Override
protected Metadata decode(MetadataInputBuffer inputBuffer, ByteBuffer buffer) {
decodeWasCalled = true;
return result;
}
}
}
...@@ -129,10 +129,6 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { ...@@ -129,10 +129,6 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
if (result == C.RESULT_BUFFER_READ) { if (result == C.RESULT_BUFFER_READ) {
if (buffer.isEndOfStream()) { if (buffer.isEndOfStream()) {
inputStreamEnded = true; inputStreamEnded = true;
} else if (buffer.isDecodeOnly()) {
// Do nothing. Note this assumes that all metadata buffers can be decoded independently.
// If we ever need to support a metadata format where this is not the case, we'll need to
// pass the buffer to the decoder and discard the output.
} else { } else {
buffer.subsampleOffsetUs = subsampleOffsetUs; buffer.subsampleOffsetUs = subsampleOffsetUs;
buffer.flip(); buffer.flip();
......
...@@ -17,9 +17,8 @@ package com.google.android.exoplayer2.metadata.dvbsi; ...@@ -17,9 +17,8 @@ package com.google.android.exoplayer2.metadata.dvbsi;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoder;
import com.google.android.exoplayer2.metadata.MetadataInputBuffer; import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.metadata.SimpleMetadataDecoder;
import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
...@@ -32,7 +31,7 @@ import java.util.ArrayList; ...@@ -32,7 +31,7 @@ import java.util.ArrayList;
* href="https://www.etsi.org/deliver/etsi_ts/102800_102899/102809/01.01.01_60/ts_102809v010101p.pdf"> * href="https://www.etsi.org/deliver/etsi_ts/102800_102899/102809/01.01.01_60/ts_102809v010101p.pdf">
* DVB ETSI TS 102 809 v1.1.1 spec</a>. * DVB ETSI TS 102 809 v1.1.1 spec</a>.
*/ */
public final class AppInfoTableDecoder implements MetadataDecoder { public final class AppInfoTableDecoder extends SimpleMetadataDecoder {
/** See section 5.3.6. */ /** See section 5.3.6. */
private static final int DESCRIPTOR_TRANSPORT_PROTOCOL = 0x02; private static final int DESCRIPTOR_TRANSPORT_PROTOCOL = 0x02;
...@@ -47,10 +46,8 @@ public final class AppInfoTableDecoder implements MetadataDecoder { ...@@ -47,10 +46,8 @@ public final class AppInfoTableDecoder implements MetadataDecoder {
@Override @Override
@Nullable @Nullable
public Metadata decode(MetadataInputBuffer inputBuffer) { @SuppressWarnings("ByteBufferBackingArray") // Buffer validated by SimpleMetadataDecoder.decode
ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); protected Metadata decode(MetadataInputBuffer inputBuffer, ByteBuffer buffer) {
Assertions.checkArgument(
buffer.position() == 0 && buffer.hasArray() && buffer.arrayOffset() == 0);
int tableId = buffer.get(); int tableId = buffer.get();
return tableId == APPLICATION_INFORMATION_TABLE_ID return tableId == APPLICATION_INFORMATION_TABLE_ID
? parseAit(new ParsableBitArray(buffer.array(), buffer.limit())) ? parseAit(new ParsableBitArray(buffer.array(), buffer.limit()))
......
...@@ -17,9 +17,8 @@ package com.google.android.exoplayer2.metadata.icy; ...@@ -17,9 +17,8 @@ package com.google.android.exoplayer2.metadata.icy;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoder;
import com.google.android.exoplayer2.metadata.MetadataInputBuffer; import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.metadata.SimpleMetadataDecoder;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
...@@ -29,7 +28,7 @@ import java.util.regex.Matcher; ...@@ -29,7 +28,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** Decodes ICY stream information. */ /** Decodes ICY stream information. */
public final class IcyDecoder implements MetadataDecoder { public final class IcyDecoder extends SimpleMetadataDecoder {
private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.*?)';", Pattern.DOTALL); private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.*?)';", Pattern.DOTALL);
private static final String STREAM_KEY_NAME = "streamtitle"; private static final String STREAM_KEY_NAME = "streamtitle";
...@@ -44,10 +43,7 @@ public final class IcyDecoder implements MetadataDecoder { ...@@ -44,10 +43,7 @@ public final class IcyDecoder implements MetadataDecoder {
} }
@Override @Override
public Metadata decode(MetadataInputBuffer inputBuffer) { protected Metadata decode(MetadataInputBuffer inputBuffer, ByteBuffer buffer) {
ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data);
Assertions.checkArgument(
buffer.position() == 0 && buffer.hasArray() && buffer.arrayOffset() == 0);
@Nullable String icyString = decodeToString(buffer); @Nullable String icyString = decodeToString(buffer);
byte[] icyBytes = new byte[buffer.limit()]; byte[] icyBytes = new byte[buffer.limit()];
buffer.get(icyBytes); buffer.get(icyBytes);
......
...@@ -17,19 +17,16 @@ package com.google.android.exoplayer2.metadata.scte35; ...@@ -17,19 +17,16 @@ package com.google.android.exoplayer2.metadata.scte35;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoder;
import com.google.android.exoplayer2.metadata.MetadataInputBuffer; import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.metadata.SimpleMetadataDecoder;
import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.TimestampAdjuster;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /** Decodes splice info sections and produces splice commands. */
* Decodes splice info sections and produces splice commands. public final class SpliceInfoDecoder extends SimpleMetadataDecoder {
*/
public final class SpliceInfoDecoder implements MetadataDecoder {
private static final int TYPE_SPLICE_NULL = 0x00; private static final int TYPE_SPLICE_NULL = 0x00;
private static final int TYPE_SPLICE_SCHEDULE = 0x04; private static final int TYPE_SPLICE_SCHEDULE = 0x04;
...@@ -48,11 +45,8 @@ public final class SpliceInfoDecoder implements MetadataDecoder { ...@@ -48,11 +45,8 @@ public final class SpliceInfoDecoder implements MetadataDecoder {
} }
@Override @Override
public Metadata decode(MetadataInputBuffer inputBuffer) { @SuppressWarnings("ByteBufferBackingArray") // Buffer validated by SimpleMetadataDecoder.decode
ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); protected Metadata decode(MetadataInputBuffer inputBuffer, ByteBuffer buffer) {
Assertions.checkArgument(
buffer.position() == 0 && buffer.hasArray() && buffer.arrayOffset() == 0);
// Internal timestamps adjustment. // Internal timestamps adjustment.
if (timestampAdjuster == null if (timestampAdjuster == null
|| inputBuffer.subsampleOffsetUs != timestampAdjuster.getTimestampOffsetUs()) { || inputBuffer.subsampleOffsetUs != timestampAdjuster.getTimestampOffsetUs()) {
......
...@@ -361,12 +361,15 @@ public final class PlayerEmsgHandler implements Handler.Callback { ...@@ -361,12 +361,15 @@ public final class PlayerEmsgHandler implements Handler.Callback {
private void parseAndDiscardSamples() { private void parseAndDiscardSamples() {
while (sampleQueue.isReady(/* loadingFinished= */ false)) { while (sampleQueue.isReady(/* loadingFinished= */ false)) {
MetadataInputBuffer inputBuffer = dequeueSample(); @Nullable MetadataInputBuffer inputBuffer = dequeueSample();
if (inputBuffer == null) { if (inputBuffer == null) {
continue; continue;
} }
long eventTimeUs = inputBuffer.timeUs; long eventTimeUs = inputBuffer.timeUs;
Metadata metadata = decoder.decode(inputBuffer); @Nullable Metadata metadata = decoder.decode(inputBuffer);
if (metadata == null) {
continue;
}
EventMessage eventMessage = (EventMessage) metadata.get(0); EventMessage eventMessage = (EventMessage) metadata.get(0);
if (isPlayerEmsgEvent(eventMessage.schemeIdUri, eventMessage.value)) { if (isPlayerEmsgEvent(eventMessage.schemeIdUri, eventMessage.value)) {
parsePlayerEmsgEvent(eventTimeUs, eventMessage); parsePlayerEmsgEvent(eventTimeUs, eventMessage);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment