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
6a3e2a64
authored
Mar 28, 2023
by
Googler
Committed by
Tianyi Feng
Mar 30, 2023
Browse files
Options
_('Browse Files')
Download
Email Patches
Plain Diff
Allow associating LoadControl methods with the relevant MediaPeriod.
PiperOrigin-RevId: 520037412
parent
e44e3377
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
123 additions
and
13 deletions
library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java
library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java
library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java
library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java
View file @
6a3e2a64
...
@@ -20,6 +20,7 @@ import static java.lang.Math.max;
...
@@ -20,6 +20,7 @@ import static java.lang.Math.max;
import
static
java
.
lang
.
Math
.
min
;
import
static
java
.
lang
.
Math
.
min
;
import
androidx.annotation.Nullable
;
import
androidx.annotation.Nullable
;
import
com.google.android.exoplayer2.source.MediaPeriodId
;
import
com.google.android.exoplayer2.source.TrackGroupArray
;
import
com.google.android.exoplayer2.source.TrackGroupArray
;
import
com.google.android.exoplayer2.trackselection.ExoTrackSelection
;
import
com.google.android.exoplayer2.trackselection.ExoTrackSelection
;
import
com.google.android.exoplayer2.upstream.Allocator
;
import
com.google.android.exoplayer2.upstream.Allocator
;
...
@@ -327,7 +328,11 @@ public class DefaultLoadControl implements LoadControl {
...
@@ -327,7 +328,11 @@ public class DefaultLoadControl implements LoadControl {
@Override
@Override
public
void
onTracksSelected
(
public
void
onTracksSelected
(
Renderer
[]
renderers
,
TrackGroupArray
trackGroups
,
ExoTrackSelection
[]
trackSelections
)
{
Timeline
timeline
,
MediaPeriodId
mediaPeriodId
,
Renderer
[]
renderers
,
TrackGroupArray
trackGroups
,
ExoTrackSelection
[]
trackSelections
)
{
targetBufferBytes
=
targetBufferBytes
=
targetBufferBytesOverwrite
==
C
.
LENGTH_UNSET
targetBufferBytesOverwrite
==
C
.
LENGTH_UNSET
?
calculateTargetBufferBytes
(
renderers
,
trackSelections
)
?
calculateTargetBufferBytes
(
renderers
,
trackSelections
)
...
@@ -389,7 +394,12 @@ public class DefaultLoadControl implements LoadControl {
...
@@ -389,7 +394,12 @@ public class DefaultLoadControl implements LoadControl {
@Override
@Override
public
boolean
shouldStartPlayback
(
public
boolean
shouldStartPlayback
(
long
bufferedDurationUs
,
float
playbackSpeed
,
boolean
rebuffering
,
long
targetLiveOffsetUs
)
{
Timeline
timeline
,
MediaPeriodId
mediaPeriodId
,
long
bufferedDurationUs
,
float
playbackSpeed
,
boolean
rebuffering
,
long
targetLiveOffsetUs
)
{
bufferedDurationUs
=
Util
.
getPlayoutDurationForMediaDuration
(
bufferedDurationUs
,
playbackSpeed
);
bufferedDurationUs
=
Util
.
getPlayoutDurationForMediaDuration
(
bufferedDurationUs
,
playbackSpeed
);
long
minBufferDurationUs
=
rebuffering
?
bufferForPlaybackAfterRebufferUs
:
bufferForPlaybackUs
;
long
minBufferDurationUs
=
rebuffering
?
bufferForPlaybackAfterRebufferUs
:
bufferForPlaybackUs
;
if
(
targetLiveOffsetUs
!=
C
.
TIME_UNSET
)
{
if
(
targetLiveOffsetUs
!=
C
.
TIME_UNSET
)
{
...
...
library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
View file @
6a3e2a64
...
@@ -1815,8 +1815,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
...
@@ -1815,8 +1815,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
return
true
;
return
true
;
}
}
// Renderers are ready and we're loading. Ask the LoadControl whether to transition.
// Renderers are ready and we're loading. Ask the LoadControl whether to transition.
MediaPeriodHolder
playingPeriodHolder
=
queue
.
getPlayingPeriod
();
long
targetLiveOffsetUs
=
long
targetLiveOffsetUs
=
shouldUseLivePlaybackSpeedControl
(
playbackInfo
.
timeline
,
queue
.
getPlayingPeriod
()
.
info
.
id
)
shouldUseLivePlaybackSpeedControl
(
playbackInfo
.
timeline
,
playingPeriodHolder
.
info
.
id
)
?
livePlaybackSpeedControl
.
getTargetLiveOffsetUs
()
?
livePlaybackSpeedControl
.
getTargetLiveOffsetUs
()
:
C
.
TIME_UNSET
;
:
C
.
TIME_UNSET
;
MediaPeriodHolder
loadingHolder
=
queue
.
getLoadingPeriod
();
MediaPeriodHolder
loadingHolder
=
queue
.
getLoadingPeriod
();
...
@@ -1828,6 +1829,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
...
@@ -1828,6 +1829,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
return
isBufferedToEnd
return
isBufferedToEnd
||
isAdPendingPreparation
||
isAdPendingPreparation
||
loadControl
.
shouldStartPlayback
(
||
loadControl
.
shouldStartPlayback
(
playbackInfo
.
timeline
,
playingPeriodHolder
.
info
.
id
,
getTotalBufferedDurationUs
(),
getTotalBufferedDurationUs
(),
mediaClock
.
getPlaybackParameters
().
speed
,
mediaClock
.
getPlaybackParameters
().
speed
,
isRebuffering
,
isRebuffering
,
...
@@ -2283,7 +2286,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
...
@@ -2283,7 +2286,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
loadingPeriodHolder
.
handlePrepared
(
loadingPeriodHolder
.
handlePrepared
(
mediaClock
.
getPlaybackParameters
().
speed
,
playbackInfo
.
timeline
);
mediaClock
.
getPlaybackParameters
().
speed
,
playbackInfo
.
timeline
);
updateLoadControlTrackSelection
(
updateLoadControlTrackSelection
(
loadingPeriodHolder
.
getTrackGroups
(),
loadingPeriodHolder
.
getTrackSelectorResult
());
loadingPeriodHolder
.
info
.
id
,
loadingPeriodHolder
.
getTrackGroups
(),
loadingPeriodHolder
.
getTrackSelectorResult
());
if
(
loadingPeriodHolder
==
queue
.
getPlayingPeriod
())
{
if
(
loadingPeriodHolder
==
queue
.
getPlayingPeriod
())
{
// This is the first prepared period, so update the position and the renderers.
// This is the first prepared period, so update the position and the renderers.
resetRendererPosition
(
loadingPeriodHolder
.
info
.
startPositionUs
);
resetRendererPosition
(
loadingPeriodHolder
.
info
.
startPositionUs
);
...
@@ -2568,6 +2573,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
...
@@ -2568,6 +2573,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
&&
loadingMediaPeriodHolder
!=
null
&&
loadingMediaPeriodHolder
!=
null
&&
loadingMediaPeriodHolder
.
prepared
)
{
&&
loadingMediaPeriodHolder
.
prepared
)
{
updateLoadControlTrackSelection
(
updateLoadControlTrackSelection
(
loadingMediaPeriodHolder
.
info
.
id
,
loadingMediaPeriodHolder
.
getTrackGroups
(),
loadingMediaPeriodHolder
.
getTrackGroups
(),
loadingMediaPeriodHolder
.
getTrackSelectorResult
());
loadingMediaPeriodHolder
.
getTrackSelectorResult
());
}
}
...
@@ -2588,8 +2594,15 @@ import java.util.concurrent.atomic.AtomicBoolean;
...
@@ -2588,8 +2594,15 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
}
private
void
updateLoadControlTrackSelection
(
private
void
updateLoadControlTrackSelection
(
TrackGroupArray
trackGroups
,
TrackSelectorResult
trackSelectorResult
)
{
MediaPeriodId
mediaPeriodId
,
loadControl
.
onTracksSelected
(
renderers
,
trackGroups
,
trackSelectorResult
.
selections
);
TrackGroupArray
trackGroups
,
TrackSelectorResult
trackSelectorResult
)
{
loadControl
.
onTracksSelected
(
playbackInfo
.
timeline
,
mediaPeriodId
,
renderers
,
trackGroups
,
trackSelectorResult
.
selections
);
}
}
private
boolean
shouldPlayWhenReady
()
{
private
boolean
shouldPlayWhenReady
()
{
...
...
library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java
View file @
6a3e2a64
...
@@ -15,6 +15,8 @@
...
@@ -15,6 +15,8 @@
*/
*/
package
com
.
google
.
android
.
exoplayer2
;
package
com
.
google
.
android
.
exoplayer2
;
import
com.google.android.exoplayer2.source.MediaPeriod
;
import
com.google.android.exoplayer2.source.MediaPeriodId
;
import
com.google.android.exoplayer2.source.TrackGroup
;
import
com.google.android.exoplayer2.source.TrackGroup
;
import
com.google.android.exoplayer2.source.TrackGroupArray
;
import
com.google.android.exoplayer2.source.TrackGroupArray
;
import
com.google.android.exoplayer2.trackselection.ExoTrackSelection
;
import
com.google.android.exoplayer2.trackselection.ExoTrackSelection
;
...
@@ -23,18 +25,49 @@ import com.google.android.exoplayer2.upstream.Allocator;
...
@@ -23,18 +25,49 @@ import com.google.android.exoplayer2.upstream.Allocator;
/** Controls buffering of media. */
/** Controls buffering of media. */
public
interface
LoadControl
{
public
interface
LoadControl
{
/**
* @deprecated Used as a placeholder when MediaPeriodId is unknown. Only used when the deprecated
* methods {@link #onTracksSelected(Renderer[], TrackGroupArray, ExoTrackSelection[])} or
* {@link #shouldStartPlayback(long, float, boolean, long)} are called.
*/
@Deprecated
MediaPeriodId
EMPTY_MEDIA_PERIOD_ID
=
new
MediaPeriodId
(
/* periodUid= */
new
Object
());
/** Called by the player when prepared with a new source. */
/** Called by the player when prepared with a new source. */
void
onPrepared
();
void
onPrepared
();
/**
/**
* Called by the player when a track selection occurs.
* Called by the player when a track selection occurs.
*
*
* @param timeline The current {@link Timeline} in ExoPlayer. Can be {@link Timeline#EMPTY} only
* when the deprecated {@link #onTracksSelected(Renderer[], TrackGroupArray,
* ExoTrackSelection[])} was called.
* @param mediaPeriodId Identifies (in the current timeline) the {@link MediaPeriod} for which the
* selection was made. Will be {@link #EMPTY_MEDIA_PERIOD_ID} when {@code timeline} is empty.
* @param renderers The renderers.
* @param renderers The renderers.
* @param trackGroups The {@link TrackGroup}s from which the selection was made.
* @param trackGroups The {@link TrackGroup}s from which the selection was made.
* @param trackSelections The track selections that were made.
* @param trackSelections The track selections that were made.
*/
*/
void
onTracksSelected
(
@SuppressWarnings
(
"deprecation"
)
// Calling deprecated version of this method.
Renderer
[]
renderers
,
TrackGroupArray
trackGroups
,
ExoTrackSelection
[]
trackSelections
);
default
void
onTracksSelected
(
Timeline
timeline
,
MediaPeriodId
mediaPeriodId
,
Renderer
[]
renderers
,
TrackGroupArray
trackGroups
,
ExoTrackSelection
[]
trackSelections
)
{
onTracksSelected
(
renderers
,
trackGroups
,
trackSelections
);
}
/**
* @deprecated Implement {@link #onTracksSelected(Timeline, MediaPeriodId, Renderer[],
* TrackGroupArray, ExoTrackSelection[])} instead.
*/
@Deprecated
default
void
onTracksSelected
(
Renderer
[]
renderers
,
TrackGroupArray
trackGroups
,
ExoTrackSelection
[]
trackSelections
)
{
onTracksSelected
(
Timeline
.
EMPTY
,
EMPTY_MEDIA_PERIOD_ID
,
renderers
,
trackGroups
,
trackSelections
);
}
/** Called by the player when stopped. */
/** Called by the player when stopped. */
void
onStopped
();
void
onStopped
();
...
@@ -80,7 +113,9 @@ public interface LoadControl {
...
@@ -80,7 +113,9 @@ public interface LoadControl {
boolean
retainBackBufferFromKeyframe
();
boolean
retainBackBufferFromKeyframe
();
/**
/**
* Called by the player to determine whether it should continue to load the source.
* Called by the player to determine whether it should continue to load the source. If this method
* returns true, the {@link MediaPeriod} identified in the most recent {@link #onTracksSelected}
* call will continue being loaded.
*
*
* @param playbackPositionUs The current playback position in microseconds, relative to the start
* @param playbackPositionUs The current playback position in microseconds, relative to the start
* of the {@link Timeline.Period period} that will continue to be loaded if this method
* of the {@link Timeline.Period period} that will continue to be loaded if this method
...
@@ -100,6 +135,10 @@ public interface LoadControl {
...
@@ -100,6 +135,10 @@ public interface LoadControl {
* determines whether playback is actually started. The load control may opt to return {@code
* determines whether playback is actually started. The load control may opt to return {@code
* false} until some condition has been met (e.g. a certain amount of media is buffered).
* false} until some condition has been met (e.g. a certain amount of media is buffered).
*
*
* @param timeline The current {@link Timeline} in ExoPlayer. Can be {@link Timeline#EMPTY} only
* when the deprecated {@link #shouldStartPlayback(long, float, boolean, long)} was called.
* @param mediaPeriodId Identifies (in the current timeline) the {@link MediaPeriod} for which
* playback will start. Will be {@link #EMPTY_MEDIA_PERIOD_ID} when {@code timeline} is empty.
* @param bufferedDurationUs The duration of media that's currently buffered.
* @param bufferedDurationUs The duration of media that's currently buffered.
* @param playbackSpeed The current factor by which playback is sped up.
* @param playbackSpeed The current factor by which playback is sped up.
* @param rebuffering Whether the player is rebuffering. A rebuffer is defined to be caused by
* @param rebuffering Whether the player is rebuffering. A rebuffer is defined to be caused by
...
@@ -110,6 +149,30 @@ public interface LoadControl {
...
@@ -110,6 +149,30 @@ public interface LoadControl {
* configured.
* configured.
* @return Whether playback should be allowed to start or resume.
* @return Whether playback should be allowed to start or resume.
*/
*/
boolean
shouldStartPlayback
(
@SuppressWarnings
(
"deprecation"
)
// Calling deprecated version of this method.
long
bufferedDurationUs
,
float
playbackSpeed
,
boolean
rebuffering
,
long
targetLiveOffsetUs
);
default
boolean
shouldStartPlayback
(
Timeline
timeline
,
MediaPeriodId
mediaPeriodId
,
long
bufferedDurationUs
,
float
playbackSpeed
,
boolean
rebuffering
,
long
targetLiveOffsetUs
)
{
return
shouldStartPlayback
(
bufferedDurationUs
,
playbackSpeed
,
rebuffering
,
targetLiveOffsetUs
);
}
/**
* @deprecated Implement {@link #shouldStartPlayback(Timeline, MediaPeriodId, long, float,
* boolean, long)} instead.
*/
@Deprecated
default
boolean
shouldStartPlayback
(
long
bufferedDurationUs
,
float
playbackSpeed
,
boolean
rebuffering
,
long
targetLiveOffsetUs
)
{
return
shouldStartPlayback
(
Timeline
.
EMPTY
,
EMPTY_MEDIA_PERIOD_ID
,
bufferedDurationUs
,
playbackSpeed
,
rebuffering
,
targetLiveOffsetUs
);
}
}
}
library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java
View file @
6a3e2a64
...
@@ -178,7 +178,12 @@ public class DefaultLoadControlTest {
...
@@ -178,7 +178,12 @@ public class DefaultLoadControlTest {
@Test
@Test
public
void
shouldContinueLoading_withNoSelectedTracks_returnsTrue
()
{
public
void
shouldContinueLoading_withNoSelectedTracks_returnsTrue
()
{
loadControl
=
builder
.
build
();
loadControl
=
builder
.
build
();
loadControl
.
onTracksSelected
(
new
Renderer
[
0
],
TrackGroupArray
.
EMPTY
,
new
ExoTrackSelection
[
0
]);
loadControl
.
onTracksSelected
(
Timeline
.
EMPTY
,
LoadControl
.
EMPTY_MEDIA_PERIOD_ID
,
new
Renderer
[
0
],
TrackGroupArray
.
EMPTY
,
new
ExoTrackSelection
[
0
]);
assertThat
(
assertThat
(
loadControl
.
shouldContinueLoading
(
loadControl
.
shouldContinueLoading
(
...
@@ -202,6 +207,8 @@ public class DefaultLoadControlTest {
...
@@ -202,6 +207,8 @@ public class DefaultLoadControlTest {
assertThat
(
assertThat
(
loadControl
.
shouldStartPlayback
(
loadControl
.
shouldStartPlayback
(
Timeline
.
EMPTY
,
LoadControl
.
EMPTY_MEDIA_PERIOD_ID
,
MIN_BUFFER_US
,
MIN_BUFFER_US
,
SPEED
,
SPEED
,
/* rebuffering= */
false
,
/* rebuffering= */
false
,
...
@@ -221,6 +228,8 @@ public class DefaultLoadControlTest {
...
@@ -221,6 +228,8 @@ public class DefaultLoadControlTest {
assertThat
(
assertThat
(
loadControl
.
shouldStartPlayback
(
loadControl
.
shouldStartPlayback
(
Timeline
.
EMPTY
,
LoadControl
.
EMPTY_MEDIA_PERIOD_ID
,
/* bufferedDurationUs= */
2_999_999
,
/* bufferedDurationUs= */
2_999_999
,
SPEED
,
SPEED
,
/* rebuffering= */
false
,
/* rebuffering= */
false
,
...
@@ -228,6 +237,8 @@ public class DefaultLoadControlTest {
...
@@ -228,6 +237,8 @@ public class DefaultLoadControlTest {
.
isFalse
();
.
isFalse
();
assertThat
(
assertThat
(
loadControl
.
shouldStartPlayback
(
loadControl
.
shouldStartPlayback
(
Timeline
.
EMPTY
,
LoadControl
.
EMPTY_MEDIA_PERIOD_ID
,
/* bufferedDurationUs= */
3_000_000
,
/* bufferedDurationUs= */
3_000_000
,
SPEED
,
SPEED
,
/* rebuffering= */
false
,
/* rebuffering= */
false
,
...
@@ -246,6 +257,8 @@ public class DefaultLoadControlTest {
...
@@ -246,6 +257,8 @@ public class DefaultLoadControlTest {
assertThat
(
assertThat
(
loadControl
.
shouldStartPlayback
(
loadControl
.
shouldStartPlayback
(
Timeline
.
EMPTY
,
LoadControl
.
EMPTY_MEDIA_PERIOD_ID
,
/* bufferedDurationUs= */
499_999
,
/* bufferedDurationUs= */
499_999
,
SPEED
,
SPEED
,
/* rebuffering= */
true
,
/* rebuffering= */
true
,
...
@@ -253,6 +266,8 @@ public class DefaultLoadControlTest {
...
@@ -253,6 +266,8 @@ public class DefaultLoadControlTest {
.
isFalse
();
.
isFalse
();
assertThat
(
assertThat
(
loadControl
.
shouldStartPlayback
(
loadControl
.
shouldStartPlayback
(
Timeline
.
EMPTY
,
LoadControl
.
EMPTY_MEDIA_PERIOD_ID
,
/* bufferedDurationUs= */
500_000
,
/* bufferedDurationUs= */
500_000
,
SPEED
,
SPEED
,
/* rebuffering= */
true
,
/* rebuffering= */
true
,
...
@@ -272,6 +287,8 @@ public class DefaultLoadControlTest {
...
@@ -272,6 +287,8 @@ public class DefaultLoadControlTest {
assertThat
(
assertThat
(
loadControl
.
shouldStartPlayback
(
loadControl
.
shouldStartPlayback
(
Timeline
.
EMPTY
,
LoadControl
.
EMPTY_MEDIA_PERIOD_ID
,
/* bufferedDurationUs= */
3_999_999
,
/* bufferedDurationUs= */
3_999_999
,
SPEED
,
SPEED
,
/* rebuffering= */
true
,
/* rebuffering= */
true
,
...
@@ -279,6 +296,8 @@ public class DefaultLoadControlTest {
...
@@ -279,6 +296,8 @@ public class DefaultLoadControlTest {
.
isFalse
();
.
isFalse
();
assertThat
(
assertThat
(
loadControl
.
shouldStartPlayback
(
loadControl
.
shouldStartPlayback
(
Timeline
.
EMPTY
,
LoadControl
.
EMPTY_MEDIA_PERIOD_ID
,
/* bufferedDurationUs= */
4_000_000
,
/* bufferedDurationUs= */
4_000_000
,
SPEED
,
SPEED
,
/* rebuffering= */
true
,
/* rebuffering= */
true
,
...
@@ -297,6 +316,8 @@ public class DefaultLoadControlTest {
...
@@ -297,6 +316,8 @@ public class DefaultLoadControlTest {
assertThat
(
assertThat
(
loadControl
.
shouldStartPlayback
(
loadControl
.
shouldStartPlayback
(
Timeline
.
EMPTY
,
LoadControl
.
EMPTY_MEDIA_PERIOD_ID
,
/* bufferedDurationUs= */
499_999
,
/* bufferedDurationUs= */
499_999
,
SPEED
,
SPEED
,
/* rebuffering= */
true
,
/* rebuffering= */
true
,
...
@@ -304,6 +325,8 @@ public class DefaultLoadControlTest {
...
@@ -304,6 +325,8 @@ public class DefaultLoadControlTest {
.
isFalse
();
.
isFalse
();
assertThat
(
assertThat
(
loadControl
.
shouldStartPlayback
(
loadControl
.
shouldStartPlayback
(
Timeline
.
EMPTY
,
LoadControl
.
EMPTY_MEDIA_PERIOD_ID
,
/* bufferedDurationUs= */
500_000
,
/* bufferedDurationUs= */
500_000
,
SPEED
,
SPEED
,
/* rebuffering= */
true
,
/* rebuffering= */
true
,
...
@@ -314,7 +337,8 @@ public class DefaultLoadControlTest {
...
@@ -314,7 +337,8 @@ public class DefaultLoadControlTest {
private
void
build
()
{
private
void
build
()
{
builder
.
setAllocator
(
allocator
).
setTargetBufferBytes
(
TARGET_BUFFER_BYTES
);
builder
.
setAllocator
(
allocator
).
setTargetBufferBytes
(
TARGET_BUFFER_BYTES
);
loadControl
=
builder
.
build
();
loadControl
=
builder
.
build
();
loadControl
.
onTracksSelected
(
new
Renderer
[
0
],
null
,
null
);
loadControl
.
onTracksSelected
(
Timeline
.
EMPTY
,
LoadControl
.
EMPTY_MEDIA_PERIOD_ID
,
new
Renderer
[
0
],
null
,
null
);
}
}
private
void
makeSureTargetBufferBytesReached
()
{
private
void
makeSureTargetBufferBytesReached
()
{
...
...
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