Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
SDK
/
exoplayer
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Settings
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
a0d99a6a
authored
Sep 30, 2020
by
christosts
Committed by
kim-vde
Oct 06, 2020
Browse files
Options
_('Browse Files')
Download
Email Patches
Plain Diff
Fix flaky unit tests
PiperOrigin-RevId: 334580007
parent
c35787a0
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
50 additions
and
48 deletions
library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java
library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuer.java
library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java
library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java
View file @
a0d99a6a
...
@@ -83,19 +83,24 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
...
@@ -83,19 +83,24 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* labelling the internal thread accordingly.
* labelling the internal thread accordingly.
*/
*/
/* package */
AsynchronousMediaCodecAdapter
(
MediaCodec
codec
,
int
trackType
)
{
/* package */
AsynchronousMediaCodecAdapter
(
MediaCodec
codec
,
int
trackType
)
{
this
(
codec
,
trackType
,
new
HandlerThread
(
createThreadLabel
(
trackType
)));
this
(
codec
,
trackType
,
new
HandlerThread
(
createCallbackThreadLabel
(
trackType
)),
new
HandlerThread
(
createQueueingThreadLabel
(
trackType
)));
}
}
@VisibleForTesting
@VisibleForTesting
/* package */
AsynchronousMediaCodecAdapter
(
/* package */
AsynchronousMediaCodecAdapter
(
MediaCodec
codec
,
MediaCodec
codec
,
int
trackType
,
int
trackType
,
HandlerThread
handlerThread
)
{
HandlerThread
callbackThread
,
HandlerThread
enqueueingThread
)
{
this
.
lock
=
new
Object
();
this
.
lock
=
new
Object
();
this
.
mediaCodecAsyncCallback
=
new
MediaCodecAsyncCallback
();
this
.
mediaCodecAsyncCallback
=
new
MediaCodecAsyncCallback
();
this
.
codec
=
codec
;
this
.
codec
=
codec
;
this
.
handlerThread
=
handler
Thread
;
this
.
handlerThread
=
callback
Thread
;
this
.
bufferEnqueuer
=
new
AsynchronousMediaCodecBufferEnqueuer
(
codec
,
trackType
);
this
.
bufferEnqueuer
=
new
AsynchronousMediaCodecBufferEnqueuer
(
codec
,
enqueueingThread
);
this
.
state
=
STATE_CREATED
;
this
.
state
=
STATE_CREATED
;
}
}
...
@@ -276,8 +281,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
...
@@ -276,8 +281,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
}
}
}
private
static
String
createThreadLabel
(
int
trackType
)
{
private
static
String
createCallbackThreadLabel
(
int
trackType
)
{
StringBuilder
labelBuilder
=
new
StringBuilder
(
"ExoPlayer:MediaCodecAsyncAdapter:"
);
return
createThreadLabel
(
trackType
,
/* prefix= */
"ExoPlayer:MediaCodecAsyncAdapter:"
);
}
private
static
String
createQueueingThreadLabel
(
int
trackType
)
{
return
createThreadLabel
(
trackType
,
/* prefix= */
"ExoPlayer:MediaCodecQueueingThread:"
);
}
private
static
String
createThreadLabel
(
int
trackType
,
String
prefix
)
{
StringBuilder
labelBuilder
=
new
StringBuilder
(
prefix
);
if
(
trackType
==
C
.
TRACK_TYPE_AUDIO
)
{
if
(
trackType
==
C
.
TRACK_TYPE_AUDIO
)
{
labelBuilder
.
append
(
"Audio"
);
labelBuilder
.
append
(
"Audio"
);
}
else
if
(
trackType
==
C
.
TRACK_TYPE_VIDEO
)
{
}
else
if
(
trackType
==
C
.
TRACK_TYPE_VIDEO
)
{
...
...
library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuer.java
View file @
a0d99a6a
...
@@ -26,7 +26,6 @@ import androidx.annotation.GuardedBy;
...
@@ -26,7 +26,6 @@ import androidx.annotation.GuardedBy;
import
androidx.annotation.Nullable
;
import
androidx.annotation.Nullable
;
import
androidx.annotation.RequiresApi
;
import
androidx.annotation.RequiresApi
;
import
androidx.annotation.VisibleForTesting
;
import
androidx.annotation.VisibleForTesting
;
import
com.google.android.exoplayer2.C
;
import
com.google.android.exoplayer2.decoder.CryptoInfo
;
import
com.google.android.exoplayer2.decoder.CryptoInfo
;
import
com.google.android.exoplayer2.util.ConditionVariable
;
import
com.google.android.exoplayer2.util.ConditionVariable
;
import
com.google.android.exoplayer2.util.Util
;
import
com.google.android.exoplayer2.util.Util
;
...
@@ -66,13 +65,10 @@ class AsynchronousMediaCodecBufferEnqueuer {
...
@@ -66,13 +65,10 @@ class AsynchronousMediaCodecBufferEnqueuer {
* Creates a new instance that submits input buffers on the specified {@link MediaCodec}.
* Creates a new instance that submits input buffers on the specified {@link MediaCodec}.
*
*
* @param codec The {@link MediaCodec} to submit input buffers to.
* @param codec The {@link MediaCodec} to submit input buffers to.
* @param
trackType The type of stream (used for debug logs)
.
* @param
queueingThread The {@link HandlerThread} to use for queueing buffers
.
*/
*/
public
AsynchronousMediaCodecBufferEnqueuer
(
MediaCodec
codec
,
int
trackType
)
{
public
AsynchronousMediaCodecBufferEnqueuer
(
MediaCodec
codec
,
HandlerThread
queueingThread
)
{
this
(
this
(
codec
,
queueingThread
,
/* conditionVariable= */
new
ConditionVariable
());
codec
,
new
HandlerThread
(
createThreadLabel
(
trackType
)),
/* conditionVariable= */
new
ConditionVariable
());
}
}
@VisibleForTesting
@VisibleForTesting
...
@@ -291,18 +287,6 @@ class AsynchronousMediaCodecBufferEnqueuer {
...
@@ -291,18 +287,6 @@ class AsynchronousMediaCodecBufferEnqueuer {
return
manufacturer
.
contains
(
"samsung"
)
||
manufacturer
.
contains
(
"motorola"
);
return
manufacturer
.
contains
(
"samsung"
)
||
manufacturer
.
contains
(
"motorola"
);
}
}
private
static
String
createThreadLabel
(
int
trackType
)
{
StringBuilder
labelBuilder
=
new
StringBuilder
(
"ExoPlayer:MediaCodecBufferEnqueuer:"
);
if
(
trackType
==
C
.
TRACK_TYPE_AUDIO
)
{
labelBuilder
.
append
(
"Audio"
);
}
else
if
(
trackType
==
C
.
TRACK_TYPE_VIDEO
)
{
labelBuilder
.
append
(
"Video"
);
}
else
{
labelBuilder
.
append
(
"Unknown("
).
append
(
trackType
).
append
(
")"
);
}
return
labelBuilder
.
toString
();
}
/** Performs a deep copy of {@code cryptoInfo} to {@code frameworkCryptoInfo}. */
/** Performs a deep copy of {@code cryptoInfo} to {@code frameworkCryptoInfo}. */
private
static
void
copy
(
private
static
void
copy
(
CryptoInfo
cryptoInfo
,
android
.
media
.
MediaCodec
.
CryptoInfo
frameworkCryptoInfo
)
{
CryptoInfo
cryptoInfo
,
android
.
media
.
MediaCodec
.
CryptoInfo
frameworkCryptoInfo
)
{
...
...
library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java
View file @
a0d99a6a
...
@@ -38,26 +38,27 @@ import org.robolectric.shadows.ShadowLooper;
...
@@ -38,26 +38,27 @@ import org.robolectric.shadows.ShadowLooper;
public
class
AsynchronousMediaCodecAdapterTest
{
public
class
AsynchronousMediaCodecAdapterTest
{
private
AsynchronousMediaCodecAdapter
adapter
;
private
AsynchronousMediaCodecAdapter
adapter
;
private
MediaCodec
codec
;
private
MediaCodec
codec
;
private
TestHandlerThread
handlerThread
;
private
TestHandlerThread
callbackThread
;
private
HandlerThread
queueingThread
;
private
MediaCodec
.
BufferInfo
bufferInfo
;
private
MediaCodec
.
BufferInfo
bufferInfo
;
@Before
@Before
public
void
setUp
()
throws
IOException
{
public
void
setUp
()
throws
IOException
{
codec
=
MediaCodec
.
createByCodecName
(
"h264"
);
codec
=
MediaCodec
.
createByCodecName
(
"h264"
);
handlerThread
=
new
TestHandlerThread
(
"TestHandlerThread"
);
callbackThread
=
new
TestHandlerThread
(
"TestCallbackThread"
);
queueingThread
=
new
HandlerThread
(
"TestQueueingThread"
);
adapter
=
adapter
=
new
AsynchronousMediaCodecAdapter
(
new
AsynchronousMediaCodecAdapter
(
codec
,
codec
,
/* trackType= */
C
.
TRACK_TYPE_VIDEO
,
callbackThread
,
queueingThread
);
/* trackType= */
C
.
TRACK_TYPE_VIDEO
,
handlerThread
);
bufferInfo
=
new
MediaCodec
.
BufferInfo
();
bufferInfo
=
new
MediaCodec
.
BufferInfo
();
}
}
@After
@After
public
void
tearDown
()
{
public
void
tearDown
()
{
adapter
.
shutdown
();
adapter
.
shutdown
();
codec
.
release
();
assertThat
(
handler
Thread
.
hasQuit
()).
isTrue
();
assertThat
(
callback
Thread
.
hasQuit
()).
isTrue
();
}
}
@Test
@Test
...
@@ -66,7 +67,7 @@ public class AsynchronousMediaCodecAdapterTest {
...
@@ -66,7 +67,7 @@ public class AsynchronousMediaCodecAdapterTest {
createMediaFormat
(
"foo"
),
/* surface= */
null
,
/* crypto= */
null
,
/* flags= */
0
);
createMediaFormat
(
"foo"
),
/* surface= */
null
,
/* crypto= */
null
,
/* flags= */
0
);
// After adapter.start(), the ShadowMediaCodec offers one input buffer. We pause the looper so
// After adapter.start(), the ShadowMediaCodec offers one input buffer. We pause the looper so
// that the buffer is not propagated to the adapter.
// that the buffer is not propagated to the adapter.
shadowOf
(
handler
Thread
.
getLooper
()).
pause
();
shadowOf
(
callback
Thread
.
getLooper
()).
pause
();
adapter
.
start
();
adapter
.
start
();
assertThat
(
adapter
.
dequeueInputBufferIndex
()).
isEqualTo
(
MediaCodec
.
INFO_TRY_AGAIN_LATER
);
assertThat
(
adapter
.
dequeueInputBufferIndex
()).
isEqualTo
(
MediaCodec
.
INFO_TRY_AGAIN_LATER
);
...
@@ -79,7 +80,7 @@ public class AsynchronousMediaCodecAdapterTest {
...
@@ -79,7 +80,7 @@ public class AsynchronousMediaCodecAdapterTest {
adapter
.
start
();
adapter
.
start
();
// After start(), the ShadowMediaCodec offers input buffer 0. We advance the looper to make sure
// After start(), the ShadowMediaCodec offers input buffer 0. We advance the looper to make sure
// and messages have been propagated to the adapter.
// and messages have been propagated to the adapter.
shadowOf
(
handler
Thread
.
getLooper
()).
idle
();
shadowOf
(
callback
Thread
.
getLooper
()).
idle
();
assertThat
(
adapter
.
dequeueInputBufferIndex
()).
isEqualTo
(
0
);
assertThat
(
adapter
.
dequeueInputBufferIndex
()).
isEqualTo
(
0
);
}
}
...
@@ -92,7 +93,7 @@ public class AsynchronousMediaCodecAdapterTest {
...
@@ -92,7 +93,7 @@ public class AsynchronousMediaCodecAdapterTest {
// After adapter.start(), the ShadowMediaCodec offers input buffer 0. We run all currently
// After adapter.start(), the ShadowMediaCodec offers input buffer 0. We run all currently
// enqueued messages and pause the looper so that flush is not completed.
// enqueued messages and pause the looper so that flush is not completed.
ShadowLooper
shadowLooper
=
shadowOf
(
handler
Thread
.
getLooper
());
ShadowLooper
shadowLooper
=
shadowOf
(
callback
Thread
.
getLooper
());
shadowLooper
.
idle
();
shadowLooper
.
idle
();
shadowLooper
.
pause
();
shadowLooper
.
pause
();
adapter
.
flush
();
adapter
.
flush
();
...
@@ -107,7 +108,7 @@ public class AsynchronousMediaCodecAdapterTest {
...
@@ -107,7 +108,7 @@ public class AsynchronousMediaCodecAdapterTest {
adapter
.
start
();
adapter
.
start
();
// After adapter.start(), the ShadowMediaCodec offers input buffer 0. We advance the looper to
// After adapter.start(), the ShadowMediaCodec offers input buffer 0. We advance the looper to
// make sure all messages have been propagated to the adapter.
// make sure all messages have been propagated to the adapter.
ShadowLooper
shadowLooper
=
shadowOf
(
handler
Thread
.
getLooper
());
ShadowLooper
shadowLooper
=
shadowOf
(
callback
Thread
.
getLooper
());
shadowLooper
.
idle
();
shadowLooper
.
idle
();
adapter
.
flush
();
adapter
.
flush
();
...
@@ -123,7 +124,7 @@ public class AsynchronousMediaCodecAdapterTest {
...
@@ -123,7 +124,7 @@ public class AsynchronousMediaCodecAdapterTest {
adapter
.
configure
(
adapter
.
configure
(
createMediaFormat
(
"foo"
),
/* surface= */
null
,
/* crypto= */
null
,
/* flags= */
0
);
createMediaFormat
(
"foo"
),
/* surface= */
null
,
/* crypto= */
null
,
/* flags= */
0
);
// Pause the looper so that we interact with the adapter from this thread only.
// Pause the looper so that we interact with the adapter from this thread only.
shadowOf
(
handler
Thread
.
getLooper
()).
pause
();
shadowOf
(
callback
Thread
.
getLooper
()).
pause
();
adapter
.
start
();
adapter
.
start
();
// Set an error directly on the adapter (not through the looper).
// Set an error directly on the adapter (not through the looper).
...
@@ -140,7 +141,7 @@ public class AsynchronousMediaCodecAdapterTest {
...
@@ -140,7 +141,7 @@ public class AsynchronousMediaCodecAdapterTest {
// After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we
// After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we
// progress the adapter's looper. We progress the looper so that we call shutdown() on a
// progress the adapter's looper. We progress the looper so that we call shutdown() on a
// non-empty adapter.
// non-empty adapter.
shadowOf
(
handler
Thread
.
getLooper
()).
idle
();
shadowOf
(
callback
Thread
.
getLooper
()).
idle
();
adapter
.
shutdown
();
adapter
.
shutdown
();
...
@@ -155,7 +156,7 @@ public class AsynchronousMediaCodecAdapterTest {
...
@@ -155,7 +156,7 @@ public class AsynchronousMediaCodecAdapterTest {
adapter
.
start
();
adapter
.
start
();
// After start(), the ShadowMediaCodec offers an output format change. We progress the looper
// After start(), the ShadowMediaCodec offers an output format change. We progress the looper
// so that the format change is propagated to the adapter.
// so that the format change is propagated to the adapter.
shadowOf
(
handler
Thread
.
getLooper
()).
idle
();
shadowOf
(
callback
Thread
.
getLooper
()).
idle
();
assertThat
(
adapter
.
dequeueOutputBufferIndex
(
bufferInfo
))
assertThat
(
adapter
.
dequeueOutputBufferIndex
(
bufferInfo
))
.
isEqualTo
(
MediaCodec
.
INFO_OUTPUT_FORMAT_CHANGED
);
.
isEqualTo
(
MediaCodec
.
INFO_OUTPUT_FORMAT_CHANGED
);
...
@@ -171,13 +172,17 @@ public class AsynchronousMediaCodecAdapterTest {
...
@@ -171,13 +172,17 @@ public class AsynchronousMediaCodecAdapterTest {
adapter
.
start
();
adapter
.
start
();
// After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we
// After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we
// progress the adapter's looper.
// progress the adapter's looper.
ShadowLooper
shadowLooper
=
shadowOf
(
handler
Thread
.
getLooper
());
ShadowLooper
callbackShadowLooper
=
shadowOf
(
callback
Thread
.
getLooper
());
s
hadowLooper
.
idle
();
callbackS
hadowLooper
.
idle
();
int
index
=
adapter
.
dequeueInputBufferIndex
();
int
index
=
adapter
.
dequeueInputBufferIndex
();
adapter
.
queueInputBuffer
(
index
,
0
,
0
,
0
,
0
);
adapter
.
queueInputBuffer
(
index
,
0
,
0
,
0
,
0
);
// Progress the looper so that the ShadowMediaCodec processes the input buffer.
// Progress the queueuing looper first so the asynchronous enqueuer submits the input buffer,
shadowLooper
.
idle
();
// the ShadowMediaCodec processes the input buffer and produces an output buffer. Then, progress
// the callback looper so that the available output buffer callback is handled and the output
// buffer reaches the adapter.
shadowOf
(
queueingThread
.
getLooper
()).
idle
();
callbackShadowLooper
.
idle
();
// The ShadowMediaCodec will first offer an output format and then the output buffer.
// The ShadowMediaCodec will first offer an output format and then the output buffer.
assertThat
(
adapter
.
dequeueOutputBufferIndex
(
bufferInfo
))
assertThat
(
adapter
.
dequeueOutputBufferIndex
(
bufferInfo
))
...
@@ -194,7 +199,7 @@ public class AsynchronousMediaCodecAdapterTest {
...
@@ -194,7 +199,7 @@ public class AsynchronousMediaCodecAdapterTest {
adapter
.
start
();
adapter
.
start
();
// After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we
// After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we
// progress the adapter's looper.
// progress the adapter's looper.
ShadowLooper
shadowLooper
=
shadowOf
(
handler
Thread
.
getLooper
());
ShadowLooper
shadowLooper
=
shadowOf
(
callback
Thread
.
getLooper
());
shadowLooper
.
idle
();
shadowLooper
.
idle
();
// Flush enqueues a task in the looper, but we will pause the looper to leave flush()
// Flush enqueues a task in the looper, but we will pause the looper to leave flush()
...
@@ -211,7 +216,7 @@ public class AsynchronousMediaCodecAdapterTest {
...
@@ -211,7 +216,7 @@ public class AsynchronousMediaCodecAdapterTest {
// Pause the looper so that we interact with the adapter from this thread only.
// Pause the looper so that we interact with the adapter from this thread only.
adapter
.
configure
(
adapter
.
configure
(
createMediaFormat
(
"foo"
),
/* surface= */
null
,
/* crypto= */
null
,
/* flags= */
0
);
createMediaFormat
(
"foo"
),
/* surface= */
null
,
/* crypto= */
null
,
/* flags= */
0
);
shadowOf
(
handler
Thread
.
getLooper
()).
pause
();
shadowOf
(
callback
Thread
.
getLooper
()).
pause
();
adapter
.
start
();
adapter
.
start
();
// Set an error directly on the adapter.
// Set an error directly on the adapter.
...
@@ -227,7 +232,7 @@ public class AsynchronousMediaCodecAdapterTest {
...
@@ -227,7 +232,7 @@ public class AsynchronousMediaCodecAdapterTest {
adapter
.
start
();
adapter
.
start
();
// After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we
// After start(), the ShadowMediaCodec offers input buffer 0, which is available only if we
// progress the adapter's looper.
// progress the adapter's looper.
ShadowLooper
shadowLooper
=
shadowOf
(
handler
Thread
.
getLooper
());
ShadowLooper
shadowLooper
=
shadowOf
(
callback
Thread
.
getLooper
());
shadowLooper
.
idle
();
shadowLooper
.
idle
();
int
index
=
adapter
.
dequeueInputBufferIndex
();
int
index
=
adapter
.
dequeueInputBufferIndex
();
...
@@ -246,7 +251,7 @@ public class AsynchronousMediaCodecAdapterTest {
...
@@ -246,7 +251,7 @@ public class AsynchronousMediaCodecAdapterTest {
createMediaFormat
(
"foo"
),
/* surface= */
null
,
/* crypto= */
null
,
/* flags= */
0
);
createMediaFormat
(
"foo"
),
/* surface= */
null
,
/* crypto= */
null
,
/* flags= */
0
);
// After start() the ShadowMediaCodec offers an output format change. Pause the looper so that
// After start() the ShadowMediaCodec offers an output format change. Pause the looper so that
// the format change is not propagated to the adapter.
// the format change is not propagated to the adapter.
shadowOf
(
handler
Thread
.
getLooper
()).
pause
();
shadowOf
(
callback
Thread
.
getLooper
()).
pause
();
adapter
.
start
();
adapter
.
start
();
assertThrows
(
IllegalStateException
.
class
,
()
->
adapter
.
getOutputFormat
());
assertThrows
(
IllegalStateException
.
class
,
()
->
adapter
.
getOutputFormat
());
...
@@ -259,7 +264,7 @@ public class AsynchronousMediaCodecAdapterTest {
...
@@ -259,7 +264,7 @@ public class AsynchronousMediaCodecAdapterTest {
adapter
.
start
();
adapter
.
start
();
// After start(), the ShadowMediaCodec offers an output format, which is available only if we
// After start(), the ShadowMediaCodec offers an output format, which is available only if we
// progress the adapter's looper.
// progress the adapter's looper.
shadowOf
(
handler
Thread
.
getLooper
()).
idle
();
shadowOf
(
callback
Thread
.
getLooper
()).
idle
();
// Add another format directly on the adapter.
// Add another format directly on the adapter.
adapter
.
onOutputFormatChanged
(
codec
,
createMediaFormat
(
"format2"
));
adapter
.
onOutputFormatChanged
(
codec
,
createMediaFormat
(
"format2"
));
...
@@ -283,7 +288,7 @@ public class AsynchronousMediaCodecAdapterTest {
...
@@ -283,7 +288,7 @@ public class AsynchronousMediaCodecAdapterTest {
adapter
.
start
();
adapter
.
start
();
// After start(), the ShadowMediaCodec offers an output format, which is available only if we
// After start(), the ShadowMediaCodec offers an output format, which is available only if we
// progress the adapter's looper.
// progress the adapter's looper.
ShadowLooper
shadowLooper
=
shadowOf
(
handler
Thread
.
getLooper
());
ShadowLooper
shadowLooper
=
shadowOf
(
callback
Thread
.
getLooper
());
shadowLooper
.
idle
();
shadowLooper
.
idle
();
adapter
.
dequeueOutputBufferIndex
(
bufferInfo
);
adapter
.
dequeueOutputBufferIndex
(
bufferInfo
);
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment