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
32b40502
authored
Jan 29, 2019
by
tonihei
Committed by
Oliver Woodman
Jan 29, 2019
Browse files
Options
_('Browse Files')
Download
Email Patches
Plain Diff
Add HlsMediaPeriod getStreamKeys implementation and tests.
PiperOrigin-RevId: 231385563
parent
6a52cd74
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
305 additions
and
23 deletions
library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java
library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java
library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java
View file @
32b40502
...
...
@@ -20,6 +20,7 @@ import com.google.android.exoplayer2.C;
import
com.google.android.exoplayer2.Format
;
import
com.google.android.exoplayer2.SeekParameters
;
import
com.google.android.exoplayer2.extractor.Extractor
;
import
com.google.android.exoplayer2.offline.StreamKey
;
import
com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory
;
import
com.google.android.exoplayer2.source.MediaPeriod
;
import
com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher
;
...
...
@@ -68,6 +69,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
private
TrackGroupArray
trackGroups
;
private
HlsSampleStreamWrapper
[]
sampleStreamWrappers
;
private
HlsSampleStreamWrapper
[]
enabledSampleStreamWrappers
;
private
int
[]
selectedVariantIndices
;
private
SequenceableLoader
compositeSequenceableLoader
;
private
boolean
notifiedReadingStarted
;
...
...
@@ -112,6 +114,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
timestampAdjusterProvider
=
new
TimestampAdjusterProvider
();
sampleStreamWrappers
=
new
HlsSampleStreamWrapper
[
0
];
enabledSampleStreamWrappers
=
new
HlsSampleStreamWrapper
[
0
];
selectedVariantIndices
=
new
int
[
0
];
eventDispatcher
.
mediaPeriodCreated
();
}
...
...
@@ -144,6 +147,77 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
}
@Override
public
List
<
StreamKey
>
getStreamKeys
(
List
<
TrackSelection
>
trackSelections
)
{
// See HlsMasterPlaylist.copy for interpretation of StreamKeys.
HlsMasterPlaylist
masterPlaylist
=
Assertions
.
checkNotNull
(
playlistTracker
.
getMasterPlaylist
());
boolean
hasVariants
=
!
masterPlaylist
.
variants
.
isEmpty
();
int
audioWrapperOffset
=
hasVariants
?
1
:
0
;
int
subtitleWrapperOffset
=
audioWrapperOffset
+
masterPlaylist
.
audios
.
size
();
TrackGroupArray
mainWrapperTrackGroups
;
int
mainWrapperPrimaryGroupIndex
;
if
(
hasVariants
)
{
HlsSampleStreamWrapper
mainWrapper
=
sampleStreamWrappers
[
0
];
mainWrapperTrackGroups
=
mainWrapper
.
getTrackGroups
();
mainWrapperPrimaryGroupIndex
=
mainWrapper
.
getPrimaryTrackGroupIndex
();
}
else
{
mainWrapperTrackGroups
=
TrackGroupArray
.
EMPTY
;
mainWrapperPrimaryGroupIndex
=
0
;
}
List
<
StreamKey
>
streamKeys
=
new
ArrayList
<>();
boolean
needsPrimaryTrackGroupSelection
=
false
;
boolean
hasPrimaryTrackGroupSelection
=
false
;
for
(
TrackSelection
trackSelection
:
trackSelections
)
{
TrackGroup
trackSelectionGroup
=
trackSelection
.
getTrackGroup
();
int
mainWrapperTrackGroupIndex
=
mainWrapperTrackGroups
.
indexOf
(
trackSelectionGroup
);
if
(
mainWrapperTrackGroupIndex
!=
C
.
INDEX_UNSET
)
{
if
(
mainWrapperTrackGroupIndex
==
mainWrapperPrimaryGroupIndex
)
{
// Primary group in main wrapper.
hasPrimaryTrackGroupSelection
=
true
;
for
(
int
i
=
0
;
i
<
trackSelection
.
length
();
i
++)
{
int
variantIndex
=
selectedVariantIndices
[
trackSelection
.
getIndexInTrackGroup
(
i
)];
streamKeys
.
add
(
new
StreamKey
(
HlsMasterPlaylist
.
GROUP_INDEX_VARIANT
,
variantIndex
));
}
}
else
{
// Embedded group in main wrapper.
needsPrimaryTrackGroupSelection
=
true
;
}
}
else
{
// Audio or subtitle group.
for
(
int
i
=
audioWrapperOffset
;
i
<
sampleStreamWrappers
.
length
;
i
++)
{
TrackGroupArray
wrapperTrackGroups
=
sampleStreamWrappers
[
i
].
getTrackGroups
();
if
(
wrapperTrackGroups
.
indexOf
(
trackSelectionGroup
)
!=
C
.
INDEX_UNSET
)
{
if
(
i
<
subtitleWrapperOffset
)
{
streamKeys
.
add
(
new
StreamKey
(
HlsMasterPlaylist
.
GROUP_INDEX_AUDIO
,
i
-
audioWrapperOffset
));
}
else
{
streamKeys
.
add
(
new
StreamKey
(
HlsMasterPlaylist
.
GROUP_INDEX_SUBTITLE
,
i
-
subtitleWrapperOffset
));
}
break
;
}
}
}
}
if
(
needsPrimaryTrackGroupSelection
&&
!
hasPrimaryTrackGroupSelection
)
{
// A track selection includes a variant-embedded track, but no variant is added yet. We use
// the valid variant with the lowest bitrate to reduce overhead.
int
lowestBitrateIndex
=
selectedVariantIndices
[
0
];
int
lowestBitrate
=
masterPlaylist
.
variants
.
get
(
selectedVariantIndices
[
0
]).
format
.
bitrate
;
for
(
int
i
=
1
;
i
<
selectedVariantIndices
.
length
;
i
++)
{
int
variantBitrate
=
masterPlaylist
.
variants
.
get
(
selectedVariantIndices
[
i
]).
format
.
bitrate
;
if
(
variantBitrate
<
lowestBitrate
)
{
lowestBitrate
=
variantBitrate
;
lowestBitrateIndex
=
selectedVariantIndices
[
i
];
}
}
streamKeys
.
add
(
new
StreamKey
(
HlsMasterPlaylist
.
GROUP_INDEX_VARIANT
,
lowestBitrateIndex
));
}
return
streamKeys
;
}
@Override
public
long
selectTracks
(
TrackSelection
[]
selections
,
boolean
[]
mayRetainStreamFlags
,
SampleStream
[]
streams
,
boolean
[]
streamResetFlags
,
long
positionUs
)
{
// Map each selection and stream onto a child period index.
...
...
@@ -424,44 +498,64 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
*/
private
void
buildAndPrepareMainSampleStreamWrapper
(
HlsMasterPlaylist
masterPlaylist
,
long
positionUs
)
{
List
<
HlsUrl
>
selectedVariants
=
new
ArrayList
<>(
masterPlaylist
.
variants
)
;
ArrayList
<
HlsUrl
>
definiteVideoVariants
=
new
ArrayList
<>()
;
ArrayList
<
HlsUrl
>
definiteAudioOnlyVariants
=
new
ArrayList
<>()
;
for
(
int
i
=
0
;
i
<
selectedV
ariants
.
size
();
i
++)
{
HlsUrl
variant
=
selectedV
ariants
.
get
(
i
);
int
[]
variantTypes
=
new
int
[
masterPlaylist
.
variants
.
size
()]
;
int
videoVariantCount
=
0
;
int
audioVariantCount
=
0
;
for
(
int
i
=
0
;
i
<
masterPlaylist
.
v
ariants
.
size
();
i
++)
{
HlsUrl
variant
=
masterPlaylist
.
v
ariants
.
get
(
i
);
Format
format
=
variant
.
format
;
if
(
format
.
height
>
0
||
Util
.
getCodecsOfType
(
format
.
codecs
,
C
.
TRACK_TYPE_VIDEO
)
!=
null
)
{
definiteVideoVariants
.
add
(
variant
);
variantTypes
[
i
]
=
C
.
TRACK_TYPE_VIDEO
;
videoVariantCount
++;
}
else
if
(
Util
.
getCodecsOfType
(
format
.
codecs
,
C
.
TRACK_TYPE_AUDIO
)
!=
null
)
{
definiteAudioOnlyVariants
.
add
(
variant
);
variantTypes
[
i
]
=
C
.
TRACK_TYPE_AUDIO
;
audioVariantCount
++;
}
else
{
variantTypes
[
i
]
=
C
.
TRACK_TYPE_UNKNOWN
;
}
}
if
(!
definiteVideoVariants
.
isEmpty
())
{
boolean
useVideoVariantsOnly
=
false
;
boolean
useNonAudioVariantsOnly
=
false
;
int
selectedVariantsCount
=
variantTypes
.
length
;
if
(
videoVariantCount
>
0
)
{
// We've identified some variants as definitely containing video. Assume variants within the
// master playlist are marked consistently, and hence that we have the full set. Filter out
// any other variants, which are likely to be audio only.
selectedVariants
=
definiteVideoVariants
;
}
else
if
(
definiteAudioOnlyVariants
.
size
()
<
selectedVariants
.
size
())
{
useVideoVariantsOnly
=
true
;
selectedVariantsCount
=
videoVariantCount
;
}
else
if
(
audioVariantCount
<
variantTypes
.
length
)
{
// We've identified some variants, but not all, as being audio only. Filter them out to leave
// the remaining variants, which are likely to contain video.
selectedVariants
.
removeAll
(
definiteAudioOnlyVariants
);
}
else
{
// Leave the enabled variants unchanged. They're likely either all video or all audio.
useNonAudioVariantsOnly
=
true
;
selectedVariantsCount
=
variantTypes
.
length
-
audioVariantCount
;
}
HlsUrl
[]
selectedVariants
=
new
HlsUrl
[
selectedVariantsCount
];
selectedVariantIndices
=
new
int
[
selectedVariantsCount
];
int
outIndex
=
0
;
for
(
int
i
=
0
;
i
<
masterPlaylist
.
variants
.
size
();
i
++)
{
if
((!
useVideoVariantsOnly
||
variantTypes
[
i
]
==
C
.
TRACK_TYPE_VIDEO
)
&&
(!
useNonAudioVariantsOnly
||
variantTypes
[
i
]
!=
C
.
TRACK_TYPE_AUDIO
))
{
selectedVariants
[
outIndex
]
=
masterPlaylist
.
variants
.
get
(
i
);
selectedVariantIndices
[
outIndex
++]
=
i
;
}
}
Assertions
.
checkArgument
(!
selectedVariants
.
isEmpty
());
HlsUrl
[]
variants
=
selectedVariants
.
toArray
(
new
HlsUrl
[
0
]);
String
codecs
=
variants
[
0
].
format
.
codecs
;
HlsSampleStreamWrapper
sampleStreamWrapper
=
buildSampleStreamWrapper
(
C
.
TRACK_TYPE_DEFAULT
,
variants
,
masterPlaylist
.
muxedAudioFormat
,
masterPlaylist
.
muxedCaptionFormats
,
positionUs
);
String
codecs
=
selectedVariants
[
0
].
format
.
codecs
;
HlsSampleStreamWrapper
sampleStreamWrapper
=
buildSampleStreamWrapper
(
C
.
TRACK_TYPE_DEFAULT
,
selectedVariants
,
masterPlaylist
.
muxedAudioFormat
,
masterPlaylist
.
muxedCaptionFormats
,
positionUs
);
sampleStreamWrappers
[
0
]
=
sampleStreamWrapper
;
if
(
allowChunklessPreparation
&&
codecs
!=
null
)
{
boolean
variantsContainVideoCodecs
=
Util
.
getCodecsOfType
(
codecs
,
C
.
TRACK_TYPE_VIDEO
)
!=
null
;
boolean
variantsContainAudioCodecs
=
Util
.
getCodecsOfType
(
codecs
,
C
.
TRACK_TYPE_AUDIO
)
!=
null
;
List
<
TrackGroup
>
muxedTrackGroups
=
new
ArrayList
<>();
if
(
variantsContainVideoCodecs
)
{
Format
[]
videoFormats
=
new
Format
[
selectedVariants
.
size
()
];
Format
[]
videoFormats
=
new
Format
[
selectedVariants
Count
];
for
(
int
i
=
0
;
i
<
videoFormats
.
length
;
i
++)
{
videoFormats
[
i
]
=
deriveVideoFormat
(
v
ariants
[
i
].
format
);
videoFormats
[
i
]
=
deriveVideoFormat
(
selectedV
ariants
[
i
].
format
);
}
muxedTrackGroups
.
add
(
new
TrackGroup
(
videoFormats
));
...
...
@@ -470,7 +564,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
muxedTrackGroups
.
add
(
new
TrackGroup
(
deriveAudioFormat
(
v
ariants
[
0
].
format
,
selectedV
ariants
[
0
].
format
,
masterPlaylist
.
muxedAudioFormat
,
/* isPrimaryTrackInVariant= */
false
)));
}
...
...
@@ -482,9 +576,9 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
}
}
else
if
(
variantsContainAudioCodecs
)
{
// Variants only contain audio.
Format
[]
audioFormats
=
new
Format
[
selectedVariants
.
size
()
];
Format
[]
audioFormats
=
new
Format
[
selectedVariants
Count
];
for
(
int
i
=
0
;
i
<
audioFormats
.
length
;
i
++)
{
Format
variantFormat
=
v
ariants
[
i
].
format
;
Format
variantFormat
=
selectedV
ariants
[
i
].
format
;
audioFormats
[
i
]
=
deriveAudioFormat
(
variantFormat
,
...
...
library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
View file @
32b40502
...
...
@@ -214,6 +214,10 @@ import java.util.List;
return
trackGroups
;
}
public
int
getPrimaryTrackGroupIndex
()
{
return
primaryTrackGroupIndex
;
}
public
int
bindSampleQueueToSampleStream
(
int
trackGroupIndex
)
{
int
sampleQueueIndex
=
trackGroupToSampleQueueIndex
[
trackGroupIndex
];
if
(
sampleQueueIndex
==
C
.
INDEX_UNSET
)
{
...
...
library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java
0 → 100644
View file @
32b40502
/*
* Copyright (C) 2018 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
.
source
.
hls
;
import
static
org
.
mockito
.
Matchers
.
anyInt
;
import
static
org
.
mockito
.
Mockito
.
mock
;
import
static
org
.
mockito
.
Mockito
.
when
;
import
com.google.android.exoplayer2.Format
;
import
com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory
;
import
com.google.android.exoplayer2.source.MediaSource.MediaPeriodId
;
import
com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher
;
import
com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist
;
import
com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl
;
import
com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist
;
import
com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker
;
import
com.google.android.exoplayer2.testutil.MediaPeriodAsserts
;
import
com.google.android.exoplayer2.testutil.MediaPeriodAsserts.FilterableManifestMediaPeriodFactory
;
import
com.google.android.exoplayer2.testutil.RobolectricUtil
;
import
com.google.android.exoplayer2.upstream.Allocator
;
import
com.google.android.exoplayer2.upstream.DataSource
;
import
com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy
;
import
com.google.android.exoplayer2.upstream.TransferListener
;
import
com.google.android.exoplayer2.util.MimeTypes
;
import
java.util.Arrays
;
import
java.util.Collections
;
import
java.util.List
;
import
org.junit.Test
;
import
org.junit.runner.RunWith
;
import
org.robolectric.RobolectricTestRunner
;
import
org.robolectric.annotation.Config
;
/** Unit test for {@link HlsMediaPeriod}. */
@RunWith
(
RobolectricTestRunner
.
class
)
@Config
(
shadows
=
{
RobolectricUtil
.
CustomLooper
.
class
,
RobolectricUtil
.
CustomMessageQueue
.
class
})
public
final
class
HlsMediaPeriodTest
{
@Test
public
void
getSteamKeys_isCompatibleWithhHlsMasterPlaylistFilter
()
{
HlsMasterPlaylist
testMasterPlaylist
=
createMasterPlaylist
(
/* variants= */
Arrays
.
asList
(
createAudioOnlyVariantHlsUrl
(
/* bitrate= */
10000
),
createMuxedVideoAudioVariantHlsUrl
(
/* bitrate= */
200000
),
createAudioOnlyVariantHlsUrl
(
/* bitrate= */
300000
),
createMuxedVideoAudioVariantHlsUrl
(
/* bitrate= */
400000
),
createMuxedVideoAudioVariantHlsUrl
(
/* bitrate= */
600000
)),
/* audios= */
Arrays
.
asList
(
createAudioHlsUrl
(
/* language= */
"spa"
),
createAudioHlsUrl
(
/* language= */
"ger"
),
createAudioHlsUrl
(
/* language= */
"tur"
)),
/* subtitles= */
Arrays
.
asList
(
createSubtitleHlsUrl
(
/* language= */
"spa"
),
createSubtitleHlsUrl
(
/* language= */
"ger"
),
createSubtitleHlsUrl
(
/* language= */
"tur"
)),
/* muxedAudioFormat= */
createAudioFormat
(
"eng"
),
/* muxedCaptionFormats= */
Arrays
.
asList
(
createSubtitleFormat
(
"eng"
),
createSubtitleFormat
(
"gsw"
)));
FilterableManifestMediaPeriodFactory
<
HlsPlaylist
>
mediaPeriodFactory
=
(
playlist
,
periodIndex
)
->
{
HlsDataSourceFactory
mockDataSourceFactory
=
mock
(
HlsDataSourceFactory
.
class
);
when
(
mockDataSourceFactory
.
createDataSource
(
anyInt
())).
thenReturn
(
mock
(
DataSource
.
class
));
HlsPlaylistTracker
mockPlaylistTracker
=
mock
(
HlsPlaylistTracker
.
class
);
when
(
mockPlaylistTracker
.
getMasterPlaylist
()).
thenReturn
((
HlsMasterPlaylist
)
playlist
);
return
new
HlsMediaPeriod
(
mock
(
HlsExtractorFactory
.
class
),
mockPlaylistTracker
,
mockDataSourceFactory
,
mock
(
TransferListener
.
class
),
mock
(
LoadErrorHandlingPolicy
.
class
),
new
EventDispatcher
()
.
withParameters
(
/* windowIndex= */
0
,
/* mediaPeriodId= */
new
MediaPeriodId
(
/* periodUid= */
new
Object
()),
/* mediaTimeOffsetMs= */
0
),
mock
(
Allocator
.
class
),
mock
(
CompositeSequenceableLoaderFactory
.
class
),
/* allowChunklessPreparation =*/
true
);
};
MediaPeriodAsserts
.
assertGetStreamKeysAndManifestFilterIntegration
(
mediaPeriodFactory
,
testMasterPlaylist
);
}
private
static
HlsMasterPlaylist
createMasterPlaylist
(
List
<
HlsUrl
>
variants
,
List
<
HlsUrl
>
audios
,
List
<
HlsUrl
>
subtitles
,
Format
muxedAudioFormat
,
List
<
Format
>
muxedCaptionFormats
)
{
return
new
HlsMasterPlaylist
(
"http://baseUri"
,
/* tags= */
Collections
.
emptyList
(),
variants
,
audios
,
subtitles
,
muxedAudioFormat
,
muxedCaptionFormats
,
/* hasIndependentSegments= */
true
,
/* variableDefinitions= */
Collections
.
emptyMap
());
}
private
static
HlsUrl
createMuxedVideoAudioVariantHlsUrl
(
int
bitrate
)
{
return
new
HlsUrl
(
"http://url"
,
Format
.
createVideoContainerFormat
(
/* id= */
null
,
/* label= */
null
,
/* containerMimeType= */
MimeTypes
.
APPLICATION_M3U8
,
/* sampleMimeType= */
null
,
/* codecs= */
"avc1.100.41,mp4a.40.2"
,
bitrate
,
/* width= */
Format
.
NO_VALUE
,
/* height= */
Format
.
NO_VALUE
,
/* frameRate= */
Format
.
NO_VALUE
,
/* initializationData= */
null
,
/* selectionFlags= */
0
));
}
private
static
HlsUrl
createAudioOnlyVariantHlsUrl
(
int
bitrate
)
{
return
new
HlsUrl
(
"http://url"
,
Format
.
createVideoContainerFormat
(
/* id= */
null
,
/* label= */
null
,
/* containerMimeType= */
MimeTypes
.
APPLICATION_M3U8
,
/* sampleMimeType= */
null
,
/* codecs= */
"mp4a.40.2"
,
bitrate
,
/* width= */
Format
.
NO_VALUE
,
/* height= */
Format
.
NO_VALUE
,
/* frameRate= */
Format
.
NO_VALUE
,
/* initializationData= */
null
,
/* selectionFlags= */
0
));
}
private
static
HlsUrl
createAudioHlsUrl
(
String
language
)
{
return
new
HlsUrl
(
"http://url"
,
createAudioFormat
(
language
));
}
private
static
HlsUrl
createSubtitleHlsUrl
(
String
language
)
{
return
new
HlsUrl
(
"http://url"
,
createSubtitleFormat
(
language
));
}
private
static
Format
createAudioFormat
(
String
language
)
{
return
Format
.
createAudioContainerFormat
(
/* id= */
null
,
/* label= */
null
,
/* containerMimeType= */
MimeTypes
.
APPLICATION_M3U8
,
MimeTypes
.
getMediaMimeType
(
"mp4a.40.2"
),
/* codecs= */
"mp4a.40.2"
,
/* bitrate= */
Format
.
NO_VALUE
,
/* channelCount= */
Format
.
NO_VALUE
,
/* sampleRate= */
Format
.
NO_VALUE
,
/* initializationData= */
null
,
/* selectionFlags= */
0
,
language
);
}
private
static
Format
createSubtitleFormat
(
String
language
)
{
return
Format
.
createTextContainerFormat
(
/* id= */
null
,
/* label= */
null
,
/* containerMimeType= */
MimeTypes
.
APPLICATION_M3U8
,
/* sampleMimeType= */
MimeTypes
.
TEXT_VTT
,
/* codecs= */
null
,
/* bitrate= */
Format
.
NO_VALUE
,
/* selectionFlags= */
0
,
language
);
}
}
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