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
a03f8a1c
authored
Apr 27, 2020
by
Ian Baker
Browse files
Options
_('Browse Files')
Download
Email Patches
Plain Diff
Merge pull request #7199 from TiVo:p-fix-stuckcaption
PiperOrigin-RevId: 308229206
parent
bf5b52e2
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
76 additions
and
59 deletions
RELEASENOTES.md
library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java
library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java
library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java
library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java
RELEASENOTES.md
View file @
a03f8a1c
...
@@ -101,6 +101,9 @@
...
@@ -101,6 +101,9 @@
used
`start`
,
`middle`
and
`end`
).
used
`start`
,
`middle`
and
`end`
).
*
Use anti-aliasing and bitmap filtering when displaying bitmap subtitles
*
Use anti-aliasing and bitmap filtering when displaying bitmap subtitles
(
[
#6950
](
https://github.com/google/ExoPlayer/pull/6950
)
).
(
[
#6950
](
https://github.com/google/ExoPlayer/pull/6950
)
).
*
Implement timing-out of stuck CEA-608 captions (as permitted by
ANSI/CTA-608-E R-2014 Annex C.9) and set the default timeout to 16
seconds (
[
#7181
](
https://github.com/google/ExoPlayer/issues/7181
)
).
*
DRM:
*
DRM:
*
Add support for attaching DRM sessions to clear content in the demo app.
*
Add support for attaching DRM sessions to clear content in the demo app.
*
Remove
`DrmSessionManager`
references from all renderers.
*
Remove
`DrmSessionManager`
references from all renderers.
...
@@ -111,8 +114,8 @@
...
@@ -111,8 +114,8 @@
(
[
#7078
](
https://github.com/google/ExoPlayer/issues/7078
)
).
(
[
#7078
](
https://github.com/google/ExoPlayer/issues/7078
)
).
*
Remove generics from DRM components.
*
Remove generics from DRM components.
*
Downloads and caching:
*
Downloads and caching:
*
Merge downloads in
`SegmentDownloader`
to improve overall download
*
Merge downloads in
`SegmentDownloader`
to improve overall download
speed
speed
(
[
#5978
](
https://github.com/google/ExoPlayer/issues/5978
)
).
(
[
#5978
](
https://github.com/google/ExoPlayer/issues/5978
)
).
*
Replace
`CacheDataSinkFactory`
and
`CacheDataSourceFactory`
with
*
Replace
`CacheDataSinkFactory`
and
`CacheDataSourceFactory`
with
`CacheDataSink.Factory`
and
`CacheDataSource.Factory`
respectively.
`CacheDataSink.Factory`
and
`CacheDataSource.Factory`
respectively.
*
Remove
`DownloadConstructorHelper`
and use
`CacheDataSource.Factory`
*
Remove
`DownloadConstructorHelper`
and use
`CacheDataSource.Factory`
...
...
library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java
View file @
a03f8a1c
...
@@ -27,7 +27,6 @@ import com.google.android.exoplayer2.text.ttml.TtmlDecoder;
...
@@ -27,7 +27,6 @@ import com.google.android.exoplayer2.text.ttml.TtmlDecoder;
import
com.google.android.exoplayer2.text.tx3g.Tx3gDecoder
;
import
com.google.android.exoplayer2.text.tx3g.Tx3gDecoder
;
import
com.google.android.exoplayer2.text.webvtt.Mp4WebvttDecoder
;
import
com.google.android.exoplayer2.text.webvtt.Mp4WebvttDecoder
;
import
com.google.android.exoplayer2.text.webvtt.WebvttDecoder
;
import
com.google.android.exoplayer2.text.webvtt.WebvttDecoder
;
import
com.google.android.exoplayer2.util.Clock
;
import
com.google.android.exoplayer2.util.MimeTypes
;
import
com.google.android.exoplayer2.util.MimeTypes
;
/**
/**
...
@@ -109,8 +108,10 @@ public interface SubtitleDecoderFactory {
...
@@ -109,8 +108,10 @@ public interface SubtitleDecoderFactory {
return
new
Tx3gDecoder
(
format
.
initializationData
);
return
new
Tx3gDecoder
(
format
.
initializationData
);
case
MimeTypes
.
APPLICATION_CEA608
:
case
MimeTypes
.
APPLICATION_CEA608
:
case
MimeTypes
.
APPLICATION_MP4CEA608
:
case
MimeTypes
.
APPLICATION_MP4CEA608
:
return
new
Cea608Decoder
(
mimeType
,
format
.
accessibilityChannel
,
return
new
Cea608Decoder
(
16000L
,
Clock
.
DEFAULT
);
mimeType
,
format
.
accessibilityChannel
,
Cea608Decoder
.
MIN_DATA_CHANNEL_TIMEOUT_MS
);
case
MimeTypes
.
APPLICATION_CEA708
:
case
MimeTypes
.
APPLICATION_CEA708
:
return
new
Cea708Decoder
(
format
.
accessibilityChannel
,
format
.
initializationData
);
return
new
Cea708Decoder
(
format
.
accessibilityChannel
,
format
.
initializationData
);
case
MimeTypes
.
APPLICATION_DVBSUBS
:
case
MimeTypes
.
APPLICATION_DVBSUBS
:
...
...
library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java
View file @
a03f8a1c
...
@@ -26,12 +26,14 @@ import android.text.style.StyleSpan;
...
@@ -26,12 +26,14 @@ import android.text.style.StyleSpan;
import
android.text.style.UnderlineSpan
;
import
android.text.style.UnderlineSpan
;
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.Format
;
import
com.google.android.exoplayer2.text.Cue
;
import
com.google.android.exoplayer2.text.Cue
;
import
com.google.android.exoplayer2.text.Subtitle
;
import
com.google.android.exoplayer2.text.Subtitle
;
import
com.google.android.exoplayer2.text.SubtitleDecoder
;
import
com.google.android.exoplayer2.text.SubtitleDecoder
;
import
com.google.android.exoplayer2.text.SubtitleDecoderException
;
import
com.google.android.exoplayer2.text.SubtitleInputBuffer
;
import
com.google.android.exoplayer2.text.SubtitleInputBuffer
;
import
com.google.android.exoplayer2.text.SubtitleOutputBuffer
;
import
com.google.android.exoplayer2.util.Assertions
;
import
com.google.android.exoplayer2.util.Assertions
;
import
com.google.android.exoplayer2.util.Clock
;
import
com.google.android.exoplayer2.util.Log
;
import
com.google.android.exoplayer2.util.Log
;
import
com.google.android.exoplayer2.util.MimeTypes
;
import
com.google.android.exoplayer2.util.MimeTypes
;
import
com.google.android.exoplayer2.util.ParsableByteArray
;
import
com.google.android.exoplayer2.util.ParsableByteArray
;
...
@@ -41,11 +43,15 @@ import java.util.Collections;
...
@@ -41,11 +43,15 @@ import java.util.Collections;
import
java.util.List
;
import
java.util.List
;
import
org.checkerframework.checker.nullness.compatqual.NullableType
;
import
org.checkerframework.checker.nullness.compatqual.NullableType
;
/**
/** A {@link SubtitleDecoder} for CEA-608 (also known as "line 21 captions" and "EIA-608"). */
* A {@link SubtitleDecoder} for CEA-608 (also known as "line 21 captions" and "EIA-608").
*/
public
final
class
Cea608Decoder
extends
CeaDecoder
{
public
final
class
Cea608Decoder
extends
CeaDecoder
{
/**
* The minimum value for the {@code validDataChannelTimeoutMs} constructor parameter permitted by
* ANSI/CTA-608-E R-2014 Annex C.9.
*/
public
static
final
long
MIN_DATA_CHANNEL_TIMEOUT_MS
=
16_000
;
private
static
final
String
TAG
=
"Cea608Decoder"
;
private
static
final
String
TAG
=
"Cea608Decoder"
;
private
static
final
int
CC_VALID_FLAG
=
0x04
;
private
static
final
int
CC_VALID_FLAG
=
0x04
;
...
@@ -238,6 +244,7 @@ public final class Cea608Decoder extends CeaDecoder {
...
@@ -238,6 +244,7 @@ public final class Cea608Decoder extends CeaDecoder {
private
final
int
packetLength
;
private
final
int
packetLength
;
private
final
int
selectedField
;
private
final
int
selectedField
;
private
final
int
selectedChannel
;
private
final
int
selectedChannel
;
private
final
long
validDataChannelTimeoutUs
;
private
final
ArrayList
<
CueBuilder
>
cueBuilders
;
private
final
ArrayList
<
CueBuilder
>
cueBuilders
;
private
CueBuilder
currentCueBuilder
;
private
CueBuilder
currentCueBuilder
;
...
@@ -258,26 +265,26 @@ public final class Cea608Decoder extends CeaDecoder {
...
@@ -258,26 +265,26 @@ public final class Cea608Decoder extends CeaDecoder {
// service bytes and drops the rest.
// service bytes and drops the rest.
private
boolean
isInCaptionService
;
private
boolean
isInCaptionService
;
// Static counter to keep track of last CC rendered. This is used to force erase the caption when
private
long
lastCueUpdateUs
;
// the stream does not explicitly send control codes to remove caption as specified by
// CEA-608 Annex C.9
private
long
lastCueUpdateMs
=
C
.
TIME_UNSET
;
private
boolean
captionEraseCommandSeen
=
false
;
// CEA-608 Annex C.9 propose that if no data are received for the selected caption channel within
// a given time, the decoder should automatically erase the caption. The time limit should be no
// less than 16 seconds
// This value is set in the constructor. The automatic erasure is disabled when this value is 0
private
long
validDataChannelTimeoutMs
=
0
;
private
Clock
clock
;
public
Cea608Decoder
(
String
mimeType
,
int
accessibilityChannel
,
long
timeoutMs
,
Clock
clock
)
{
/**
* Constructs an instance.
*
* @param mimeType The MIME type of the CEA-608 data.
* @param accessibilityChannel The Accessibility channel, or {@link
* com.google.android.exoplayer2.Format#NO_VALUE} if unknown.
* @param validDataChannelTimeoutMs The timeout (in milliseconds) permitted by ANSI/CTA-608-E
* R-2014 Annex C.9 to clear "stuck" captions where no removal control code is received. The
* timeout should be at least {@link #MIN_DATA_CHANNEL_TIMEOUT_MS} or {@link C#TIME_UNSET} for
* no timeout.
*/
public
Cea608Decoder
(
String
mimeType
,
int
accessibilityChannel
,
long
validDataChannelTimeoutMs
)
{
ccData
=
new
ParsableByteArray
();
ccData
=
new
ParsableByteArray
();
cueBuilders
=
new
ArrayList
<>();
cueBuilders
=
new
ArrayList
<>();
currentCueBuilder
=
new
CueBuilder
(
CC_MODE_UNKNOWN
,
DEFAULT_CAPTIONS_ROW_COUNT
);
currentCueBuilder
=
new
CueBuilder
(
CC_MODE_UNKNOWN
,
DEFAULT_CAPTIONS_ROW_COUNT
);
currentChannel
=
NTSC_CC_CHANNEL_1
;
currentChannel
=
NTSC_CC_CHANNEL_1
;
validDataChannelTimeoutMs
=
timeoutMs
;
this
.
validDataChannelTimeoutUs
=
this
.
clock
=
clock
;
validDataChannelTimeoutMs
>
0
?
validDataChannelTimeoutMs
*
1000
:
C
.
TIME_UNSET
;
packetLength
=
MimeTypes
.
APPLICATION_MP4CEA608
.
equals
(
mimeType
)
?
2
:
3
;
packetLength
=
MimeTypes
.
APPLICATION_MP4CEA608
.
equals
(
mimeType
)
?
2
:
3
;
switch
(
accessibilityChannel
)
{
switch
(
accessibilityChannel
)
{
case
1
:
case
1
:
...
@@ -305,6 +312,7 @@ public final class Cea608Decoder extends CeaDecoder {
...
@@ -305,6 +312,7 @@ public final class Cea608Decoder extends CeaDecoder {
setCaptionMode
(
CC_MODE_UNKNOWN
);
setCaptionMode
(
CC_MODE_UNKNOWN
);
resetCueBuilders
();
resetCueBuilders
();
isInCaptionService
=
true
;
isInCaptionService
=
true
;
lastCueUpdateUs
=
C
.
TIME_UNSET
;
}
}
@Override
@Override
...
@@ -326,7 +334,7 @@ public final class Cea608Decoder extends CeaDecoder {
...
@@ -326,7 +334,7 @@ public final class Cea608Decoder extends CeaDecoder {
repeatableControlCc2
=
0
;
repeatableControlCc2
=
0
;
currentChannel
=
NTSC_CC_CHANNEL_1
;
currentChannel
=
NTSC_CC_CHANNEL_1
;
isInCaptionService
=
true
;
isInCaptionService
=
true
;
lastCueUpdate
M
s
=
C
.
TIME_UNSET
;
lastCueUpdate
U
s
=
C
.
TIME_UNSET
;
}
}
@Override
@Override
...
@@ -334,6 +342,26 @@ public final class Cea608Decoder extends CeaDecoder {
...
@@ -334,6 +342,26 @@ public final class Cea608Decoder extends CeaDecoder {
// Do nothing
// Do nothing
}
}
@Nullable
@Override
public
SubtitleOutputBuffer
dequeueOutputBuffer
()
throws
SubtitleDecoderException
{
SubtitleOutputBuffer
outputBuffer
=
super
.
dequeueOutputBuffer
();
if
(
outputBuffer
!=
null
)
{
return
outputBuffer
;
}
if
(
shouldClearStuckCaptions
())
{
outputBuffer
=
getAvailableOutputBuffer
();
if
(
outputBuffer
!=
null
)
{
cues
=
Collections
.
emptyList
();
lastCueUpdateUs
=
C
.
TIME_UNSET
;
Subtitle
subtitle
=
createSubtitle
();
outputBuffer
.
setContent
(
getPositionUs
(),
subtitle
,
Format
.
OFFSET_SAMPLE_RELATIVE
);
return
outputBuffer
;
}
}
return
null
;
}
@Override
@Override
protected
boolean
isNewSubtitleDataAvailable
()
{
protected
boolean
isNewSubtitleDataAvailable
()
{
return
cues
!=
lastCues
;
return
cues
!=
lastCues
;
...
@@ -351,7 +379,6 @@ public final class Cea608Decoder extends CeaDecoder {
...
@@ -351,7 +379,6 @@ public final class Cea608Decoder extends CeaDecoder {
ByteBuffer
subtitleData
=
Assertions
.
checkNotNull
(
inputBuffer
.
data
);
ByteBuffer
subtitleData
=
Assertions
.
checkNotNull
(
inputBuffer
.
data
);
ccData
.
reset
(
subtitleData
.
array
(),
subtitleData
.
limit
());
ccData
.
reset
(
subtitleData
.
array
(),
subtitleData
.
limit
());
boolean
captionDataProcessed
=
false
;
boolean
captionDataProcessed
=
false
;
captionEraseCommandSeen
=
false
;
while
(
ccData
.
bytesLeft
()
>=
packetLength
)
{
while
(
ccData
.
bytesLeft
()
>=
packetLength
)
{
byte
ccHeader
=
packetLength
==
2
?
CC_IMPLICIT_DATA_HEADER
byte
ccHeader
=
packetLength
==
2
?
CC_IMPLICIT_DATA_HEADER
:
(
byte
)
ccData
.
readUnsignedByte
();
:
(
byte
)
ccData
.
readUnsignedByte
();
...
@@ -361,6 +388,7 @@ public final class Cea608Decoder extends CeaDecoder {
...
@@ -361,6 +388,7 @@ public final class Cea608Decoder extends CeaDecoder {
// TODO: We're currently ignoring the top 5 marker bits, which should all be 1s according
// TODO: We're currently ignoring the top 5 marker bits, which should all be 1s according
// to the CEA-608 specification. We need to determine if the data should be handled
// to the CEA-608 specification. We need to determine if the data should be handled
// differently when that is not the case.
// differently when that is not the case.
if
((
ccHeader
&
CC_TYPE_FLAG
)
!=
0
)
{
if
((
ccHeader
&
CC_TYPE_FLAG
)
!=
0
)
{
// Do not process anything that is not part of the 608 byte stream.
// Do not process anything that is not part of the 608 byte stream.
continue
;
continue
;
...
@@ -370,6 +398,7 @@ public final class Cea608Decoder extends CeaDecoder {
...
@@ -370,6 +398,7 @@ public final class Cea608Decoder extends CeaDecoder {
// Do not process packets not within the selected field.
// Do not process packets not within the selected field.
continue
;
continue
;
}
}
// Strip the parity bit from each byte to get CC data.
// Strip the parity bit from each byte to get CC data.
byte
ccData1
=
(
byte
)
(
ccByte1
&
0x7F
);
byte
ccData1
=
(
byte
)
(
ccByte1
&
0x7F
);
byte
ccData2
=
(
byte
)
(
ccByte2
&
0x7F
);
byte
ccData2
=
(
byte
)
(
ccByte2
&
0x7F
);
...
@@ -439,9 +468,7 @@ public final class Cea608Decoder extends CeaDecoder {
...
@@ -439,9 +468,7 @@ public final class Cea608Decoder extends CeaDecoder {
if
(
captionDataProcessed
)
{
if
(
captionDataProcessed
)
{
if
(
captionMode
==
CC_MODE_ROLL_UP
||
captionMode
==
CC_MODE_PAINT_ON
)
{
if
(
captionMode
==
CC_MODE_ROLL_UP
||
captionMode
==
CC_MODE_PAINT_ON
)
{
cues
=
getDisplayCues
();
cues
=
getDisplayCues
();
if
((
validDataChannelTimeoutMs
!=
0
)
&&
!
captionEraseCommandSeen
)
{
lastCueUpdateUs
=
getPositionUs
();
lastCueUpdateMs
=
clock
.
elapsedRealtime
();
}
}
}
}
}
}
}
...
@@ -560,17 +587,14 @@ public final class Cea608Decoder extends CeaDecoder {
...
@@ -560,17 +587,14 @@ public final class Cea608Decoder extends CeaDecoder {
cues
=
Collections
.
emptyList
();
cues
=
Collections
.
emptyList
();
if
(
captionMode
==
CC_MODE_ROLL_UP
||
captionMode
==
CC_MODE_PAINT_ON
)
{
if
(
captionMode
==
CC_MODE_ROLL_UP
||
captionMode
==
CC_MODE_PAINT_ON
)
{
resetCueBuilders
();
resetCueBuilders
();
captionEraseCommandSeen
=
true
;
}
}
break
;
break
;
case
CTRL_ERASE_NON_DISPLAYED_MEMORY:
case
CTRL_ERASE_NON_DISPLAYED_MEMORY:
resetCueBuilders
();
resetCueBuilders
();
captionEraseCommandSeen
=
true
;
break
;
break
;
case
CTRL_END_OF_CAPTION:
case
CTRL_END_OF_CAPTION:
cues
=
getDisplayCues
();
cues
=
getDisplayCues
();
resetCueBuilders
();
resetCueBuilders
();
captionEraseCommandSeen
=
true
;
break
;
break
;
case
CTRL_CARRIAGE_RETURN:
case
CTRL_CARRIAGE_RETURN:
// carriage returns only apply to rollup captions; don't bother if we don't have anything
// carriage returns only apply to rollup captions; don't bother if we don't have anything
...
@@ -1040,17 +1064,12 @@ public final class Cea608Decoder extends CeaDecoder {
...
@@ -1040,17 +1064,12 @@ public final class Cea608Decoder extends CeaDecoder {
}
}
protected
void
clearStuckCaptions
()
/** See ANSI/CTA-608-E R-2014 Annex C.9 for Caption Erase Logic. */
{
private
boolean
shouldClearStuckCaptions
()
{
if
((
validDataChannelTimeoutMs
!=
0
)
&&
if
(
validDataChannelTimeoutUs
==
C
.
TIME_UNSET
||
lastCueUpdateUs
==
C
.
TIME_UNSET
)
{
(
lastCueUpdateMs
!=
C
.
TIME_UNSET
))
{
return
false
;
long
timeElapsed
=
clock
.
elapsedRealtime
()
-
lastCueUpdateMs
;
}
if
(
timeElapsed
>=
validDataChannelTimeoutMs
)
{
long
elapsedUs
=
getPositionUs
()
-
lastCueUpdateUs
;
// Force erase captions. There might be stale captions stuck on screen.
return
elapsedUs
>=
validDataChannelTimeoutUs
;
// (CEA-608 Annex C.9)
cues
=
Collections
.
emptyList
();
lastCueUpdateMs
=
C
.
TIME_UNSET
;
}
}
}
}
}
}
library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java
View file @
a03f8a1c
...
@@ -1296,14 +1296,6 @@ public final class Cea708Decoder extends CeaDecoder {
...
@@ -1296,14 +1296,6 @@ public final class Cea708Decoder extends CeaDecoder {
}
}
}
}
protected
void
clearStuckCaptions
()
{
// Do nothing for CEA-708.
// As per spec CEA-708 Caption text sequences shall be terminated by either the start of a new
// DTVCC Command, or with an ASCII ETX (End of Text) (0x03) character when no other DTVCC
// Commands follow.
}
/** A {@link Cue} for CEA-708. */
/** A {@link Cue} for CEA-708. */
private
static
final
class
Cea708CueInfo
{
private
static
final
class
Cea708CueInfo
{
...
...
library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java
View file @
a03f8a1c
...
@@ -97,8 +97,6 @@ import java.util.PriorityQueue;
...
@@ -97,8 +97,6 @@ import java.util.PriorityQueue;
if
(
availableOutputBuffers
.
isEmpty
())
{
if
(
availableOutputBuffers
.
isEmpty
())
{
return
null
;
return
null
;
}
}
// check if 608 decoder needs to clean up the stale caption
clearStuckCaptions
();
// iterate through all available input buffers whose timestamps are less than or equal
// iterate through all available input buffers whose timestamps are less than or equal
// to the current playback position; processing input buffers for future content should
// to the current playback position; processing input buffers for future content should
// be deferred until they would be applicable
// be deferred until they would be applicable
...
@@ -181,6 +179,15 @@ import java.util.PriorityQueue;
...
@@ -181,6 +179,15 @@ import java.util.PriorityQueue;
*/
*/
protected
abstract
void
decode
(
SubtitleInputBuffer
inputBuffer
);
protected
abstract
void
decode
(
SubtitleInputBuffer
inputBuffer
);
@Nullable
protected
final
SubtitleOutputBuffer
getAvailableOutputBuffer
()
{
return
availableOutputBuffers
.
pollFirst
();
}
protected
final
long
getPositionUs
()
{
return
playbackPositionUs
;
}
private
static
final
class
CeaInputBuffer
extends
SubtitleInputBuffer
private
static
final
class
CeaInputBuffer
extends
SubtitleInputBuffer
implements
Comparable
<
CeaInputBuffer
>
{
implements
Comparable
<
CeaInputBuffer
>
{
...
@@ -215,9 +222,4 @@ import java.util.PriorityQueue;
...
@@ -215,9 +222,4 @@ import java.util.PriorityQueue;
owner
.
releaseOutputBuffer
(
this
);
owner
.
releaseOutputBuffer
(
this
);
}
}
}
}
/**
* Implements CEA-608 Annex C.9 automatic Caption Erase Logic
*/
protected
abstract
void
clearStuckCaptions
();
}
}
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