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
654d37fe
authored
Feb 19, 2015
by
ojw28
Browse files
Options
_('Browse Files')
Download
Plain Diff
Merge pull request #310 from google/dev
dev -> dev-webm-vp9-opus
parents
8f0d576f
b5100886
Show whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
437 additions
and
209 deletions
CONTRIBUTING.md
demo/src/main/AndroidManifest.xml
demo/src/main/java/com/google/android/exoplayer/demo/Samples.java
library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java
library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java
library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java
library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java
library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java
library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java
library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsExtractor.java
library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java
library/src/main/java/com/google/android/exoplayer/hls/parser/PesPayloadReader.java → library/src/main/java/com/google/android/exoplayer/hls/parser/ElementaryStreamReader.java
library/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java
library/src/main/java/com/google/android/exoplayer/hls/parser/HlsExtractor.java
library/src/main/java/com/google/android/exoplayer/hls/parser/Id3Reader.java
library/src/main/java/com/google/android/exoplayer/hls/parser/SeiReader.java
library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java
library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java
library/src/main/java/com/google/android/exoplayer/source/DefaultSampleSource.java
library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java
CONTRIBUTING.md
View file @
654d37fe
...
...
@@ -3,9 +3,11 @@
We'd love to hear your feedback. Please open new issues describing any bugs,
feature requests or suggestions that you have.
We are not actively looking to accept patches to this project at the current
time, however in some cases we may do so. For such cases, please see the
agreement below.
We will also consider high quality pull requests. These should normally merge
into the
[
dev
][]
branch rather than master. To contribute in this way you must
first submit a Contributor License Agreement, as described below.
[
dev
]:
https://github.com/google/ExoPlayer/tree/dev
## Contributor License Agreement ##
...
...
demo/src/main/AndroidManifest.xml
View file @
654d37fe
...
...
@@ -16,8 +16,8 @@
<manifest
xmlns:android=
"http://schemas.android.com/apk/res/android"
package=
"com.google.android.exoplayer.demo"
android:versionCode=
"1
2
00"
android:versionName=
"1.
2
.00"
android:versionCode=
"1
3
00"
android:versionName=
"1.
3
.00"
android:theme=
"@style/RootTheme"
>
<uses-permission
android:name=
"android.permission.INTERNET"
/>
...
...
demo/src/main/java/com/google/android/exoplayer/demo/Samples.java
View file @
654d37fe
...
...
@@ -117,9 +117,12 @@ import java.util.Locale;
new
Sample
(
"Apple master playlist advanced"
,
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_16x9/"
+
"bipbop_16x9_variant.m3u8"
,
DemoUtil
.
TYPE_HLS
),
new
Sample
(
"Apple
single
media playlist"
,
new
Sample
(
"Apple
TS
media playlist"
,
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/"
+
"prog_index.m3u8"
,
DemoUtil
.
TYPE_HLS
),
new
Sample
(
"Apple AAC media playlist"
,
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/"
+
"prog_index.m3u8"
,
DemoUtil
.
TYPE_HLS
),
};
public
static
final
Sample
[]
MISC
=
new
Sample
[]
{
...
...
library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java
View file @
654d37fe
...
...
@@ -26,7 +26,7 @@ public class ExoPlayerLibraryInfo {
/**
* The version of the library, expressed as a string.
*/
public
static
final
String
VERSION
=
"1.
2
.0"
;
public
static
final
String
VERSION
=
"1.
3
.0"
;
/**
* The version of the library, expressed as an integer.
...
...
@@ -34,7 +34,7 @@ public class ExoPlayerLibraryInfo {
* Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the
* corresponding integer version 001002003.
*/
public
static
final
int
VERSION_INT
=
00100
2
000
;
public
static
final
int
VERSION_INT
=
00100
3
000
;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer.util.Assertions}
...
...
library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java
View file @
654d37fe
...
...
@@ -417,7 +417,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
if
(
currentLoadable
!=
null
&&
mediaChunk
==
currentLoadable
)
{
// Linearly interpolate partially-fetched chunk times.
long
chunkLength
=
mediaChunk
.
getLength
();
if
(
chunkLength
!=
C
.
LENGTH_UNBOUNDED
)
{
if
(
chunkLength
!=
C
.
LENGTH_UNBOUNDED
&&
chunkLength
!=
0
)
{
return
mediaChunk
.
startTimeUs
+
((
mediaChunk
.
endTimeUs
-
mediaChunk
.
startTimeUs
)
*
mediaChunk
.
bytesLoaded
())
/
chunkLength
;
}
else
{
...
...
library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java
View file @
654d37fe
...
...
@@ -17,6 +17,8 @@ package com.google.android.exoplayer.hls;
import
com.google.android.exoplayer.C
;
import
com.google.android.exoplayer.MediaFormat
;
import
com.google.android.exoplayer.hls.parser.AdtsExtractor
;
import
com.google.android.exoplayer.hls.parser.HlsExtractor
;
import
com.google.android.exoplayer.hls.parser.TsExtractor
;
import
com.google.android.exoplayer.upstream.Aes128DataSource
;
import
com.google.android.exoplayer.upstream.BandwidthMeter
;
...
...
@@ -105,6 +107,7 @@ public class HlsChunkSource {
public
static
final
long
DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS
=
20000
;
private
static
final
String
TAG
=
"HlsChunkSource"
;
private
static
final
String
AAC_FILE_EXTENSION
=
".aac"
;
private
static
final
float
BANDWIDTH_FRACTION
=
0.8f
;
private
final
BufferPool
bufferPool
;
...
...
@@ -332,9 +335,11 @@ public class HlsChunkSource {
boolean
isLastChunk
=
!
mediaPlaylist
.
live
&&
chunkIndex
==
mediaPlaylist
.
segments
.
size
()
-
1
;
// Configure the extractor that will read the chunk.
T
sExtractor
extractor
;
Hl
sExtractor
extractor
;
if
(
previousTsChunk
==
null
||
segment
.
discontinuity
||
switchingVariant
||
liveDiscontinuity
)
{
extractor
=
new
TsExtractor
(
startTimeUs
,
switchingVariantSpliced
,
bufferPool
);
extractor
=
chunkUri
.
getLastPathSegment
().
endsWith
(
AAC_FILE_EXTENSION
)
?
new
AdtsExtractor
(
switchingVariantSpliced
,
startTimeUs
,
bufferPool
)
:
new
TsExtractor
(
switchingVariantSpliced
,
startTimeUs
,
bufferPool
);
}
else
{
extractor
=
previousTsChunk
.
extractor
;
}
...
...
library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java
View file @
654d37fe
...
...
@@ -60,7 +60,7 @@ public final class HlsPlaylistParser implements ManifestParser<HlsPlaylist> {
private
static
final
Pattern
BANDWIDTH_ATTR_REGEX
=
Pattern
.
compile
(
BANDWIDTH_ATTR
+
"=(\\d+)\\b"
);
private
static
final
Pattern
CODECS_ATTR_REGEX
=
Pattern
.
compile
(
CODECS_ATTR
+
"=\"(.+)\""
);
Pattern
.
compile
(
CODECS_ATTR
+
"=\"(.+
?
)\""
);
private
static
final
Pattern
RESOLUTION_ATTR_REGEX
=
Pattern
.
compile
(
RESOLUTION_ATTR
+
"=(\\d+x\\d+)"
);
...
...
library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java
View file @
654d37fe
...
...
@@ -21,7 +21,7 @@ import com.google.android.exoplayer.SampleHolder;
import
com.google.android.exoplayer.SampleSource
;
import
com.google.android.exoplayer.TrackInfo
;
import
com.google.android.exoplayer.TrackRenderer
;
import
com.google.android.exoplayer.hls.parser.
T
sExtractor
;
import
com.google.android.exoplayer.hls.parser.
Hl
sExtractor
;
import
com.google.android.exoplayer.upstream.Loader
;
import
com.google.android.exoplayer.upstream.Loader.Loadable
;
import
com.google.android.exoplayer.util.Assertions
;
...
...
@@ -44,7 +44,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
private
static
final
int
NO_RESET_PENDING
=
-
1
;
private
final
HlsChunkSource
chunkSource
;
private
final
LinkedList
<
T
sExtractor
>
extractors
;
private
final
LinkedList
<
Hl
sExtractor
>
extractors
;
private
final
boolean
frameAccurateSeeking
;
private
final
int
minLoadableRetryCount
;
...
...
@@ -83,7 +83,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
this
.
frameAccurateSeeking
=
frameAccurateSeeking
;
this
.
remainingReleaseCount
=
downstreamRendererCount
;
this
.
minLoadableRetryCount
=
minLoadableRetryCount
;
extractors
=
new
LinkedList
<
T
sExtractor
>();
extractors
=
new
LinkedList
<
Hl
sExtractor
>();
}
@Override
...
...
@@ -96,7 +96,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
}
continueBufferingInternal
();
if
(!
extractors
.
isEmpty
())
{
T
sExtractor
extractor
=
extractors
.
getFirst
();
Hl
sExtractor
extractor
=
extractors
.
getFirst
();
if
(
extractor
.
isPrepared
())
{
trackCount
=
extractor
.
getTrackCount
();
trackEnabledStates
=
new
boolean
[
trackCount
];
...
...
@@ -195,7 +195,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
return
NOTHING_READ
;
}
T
sExtractor
extractor
=
getCurrentExtractor
();
Hl
sExtractor
extractor
=
getCurrentExtractor
();
if
(
extractors
.
size
()
>
1
)
{
// If there's more than one extractor, attempt to configure a seamless splice from the
// current one to the next one.
...
...
@@ -328,8 +328,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
*
* @return The current extractor from which samples should be read. Guaranteed to be non-null.
*/
private
T
sExtractor
getCurrentExtractor
()
{
T
sExtractor
extractor
=
extractors
.
getFirst
();
private
Hl
sExtractor
getCurrentExtractor
()
{
Hl
sExtractor
extractor
=
extractors
.
getFirst
();
while
(
extractors
.
size
()
>
1
&&
!
haveSamplesForEnabledTracks
(
extractor
))
{
// We're finished reading from the extractor for all tracks, and so can discard it.
extractors
.
removeFirst
().
release
();
...
...
@@ -338,7 +338,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
return
extractor
;
}
private
void
discardSamplesForDisabledTracks
(
T
sExtractor
extractor
,
long
timeUs
)
{
private
void
discardSamplesForDisabledTracks
(
Hl
sExtractor
extractor
,
long
timeUs
)
{
if
(!
extractor
.
isPrepared
())
{
return
;
}
...
...
@@ -349,7 +349,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
}
}
private
boolean
haveSamplesForEnabledTracks
(
T
sExtractor
extractor
)
{
private
boolean
haveSamplesForEnabledTracks
(
Hl
sExtractor
extractor
)
{
if
(!
extractor
.
isPrepared
())
{
return
false
;
}
...
...
library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java
View file @
654d37fe
...
...
@@ -15,7 +15,7 @@
*/
package
com
.
google
.
android
.
exoplayer
.
hls
;
import
com.google.android.exoplayer.hls.parser.
T
sExtractor
;
import
com.google.android.exoplayer.hls.parser.
Hl
sExtractor
;
import
com.google.android.exoplayer.upstream.DataSource
;
import
com.google.android.exoplayer.upstream.DataSpec
;
...
...
@@ -51,7 +51,7 @@ public final class TsChunk extends HlsChunk {
/**
* The extractor into which this chunk is being consumed.
*/
public
final
T
sExtractor
extractor
;
public
final
Hl
sExtractor
extractor
;
private
int
loadPosition
;
private
volatile
boolean
loadFinished
;
...
...
@@ -60,16 +60,17 @@ public final class TsChunk extends HlsChunk {
/**
* @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded.
* @param extractor An extractor to parse samples from the data.
* @param variantIndex The index of the variant in the master playlist.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param chunkIndex The index of the chunk.
* @param isLastChunk True if this is the last chunk in the media. False otherwise.
*/
public
TsChunk
(
DataSource
dataSource
,
DataSpec
dataSpec
,
TsExtractor
tsE
xtractor
,
public
TsChunk
(
DataSource
dataSource
,
DataSpec
dataSpec
,
HlsExtractor
e
xtractor
,
int
variantIndex
,
long
startTimeUs
,
long
endTimeUs
,
int
chunkIndex
,
boolean
isLastChunk
)
{
super
(
dataSource
,
dataSpec
);
this
.
extractor
=
tsE
xtractor
;
this
.
extractor
=
e
xtractor
;
this
.
variantIndex
=
variantIndex
;
this
.
startTimeUs
=
startTimeUs
;
this
.
endTimeUs
=
endTimeUs
;
...
...
library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsExtractor.java
0 → 100644
View file @
654d37fe
/*
* Copyright (C) 2014 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
.
exoplayer
.
hls
.
parser
;
import
com.google.android.exoplayer.MediaFormat
;
import
com.google.android.exoplayer.SampleHolder
;
import
com.google.android.exoplayer.upstream.BufferPool
;
import
com.google.android.exoplayer.upstream.DataSource
;
import
com.google.android.exoplayer.util.Assertions
;
import
com.google.android.exoplayer.util.ParsableByteArray
;
import
java.io.IOException
;
/**
* Facilitates the extraction of AAC samples from elementary audio files formatted as AAC with ADTS
* headers.
*/
public
class
AdtsExtractor
extends
HlsExtractor
{
private
static
final
int
MAX_PACKET_SIZE
=
200
;
private
final
long
firstSampleTimestamp
;
private
final
ParsableByteArray
packetBuffer
;
private
final
AdtsReader
adtsReader
;
// Accessed only by the loading thread.
private
boolean
firstPacket
;
// Accessed by both the loading and consuming threads.
private
volatile
boolean
prepared
;
public
AdtsExtractor
(
boolean
shouldSpliceIn
,
long
firstSampleTimestamp
,
BufferPool
bufferPool
)
{
super
(
shouldSpliceIn
);
this
.
firstSampleTimestamp
=
firstSampleTimestamp
;
packetBuffer
=
new
ParsableByteArray
(
MAX_PACKET_SIZE
);
adtsReader
=
new
AdtsReader
(
bufferPool
);
firstPacket
=
true
;
}
@Override
public
int
getTrackCount
()
{
Assertions
.
checkState
(
prepared
);
return
1
;
}
@Override
public
MediaFormat
getFormat
(
int
track
)
{
Assertions
.
checkState
(
prepared
);
return
adtsReader
.
getMediaFormat
();
}
@Override
public
boolean
isPrepared
()
{
return
prepared
;
}
@Override
public
void
release
()
{
adtsReader
.
release
();
}
@Override
public
long
getLargestSampleTimestamp
()
{
return
adtsReader
.
getLargestParsedTimestampUs
();
}
@Override
public
boolean
getSample
(
int
track
,
SampleHolder
holder
)
{
Assertions
.
checkState
(
prepared
);
Assertions
.
checkState
(
track
==
0
);
return
adtsReader
.
getSample
(
holder
);
}
@Override
public
void
discardUntil
(
int
track
,
long
timeUs
)
{
Assertions
.
checkState
(
prepared
);
Assertions
.
checkState
(
track
==
0
);
adtsReader
.
discardUntil
(
timeUs
);
}
@Override
public
boolean
hasSamples
(
int
track
)
{
Assertions
.
checkState
(
prepared
);
Assertions
.
checkState
(
track
==
0
);
return
!
adtsReader
.
isEmpty
();
}
@Override
public
int
read
(
DataSource
dataSource
)
throws
IOException
{
int
bytesRead
=
dataSource
.
read
(
packetBuffer
.
data
,
0
,
MAX_PACKET_SIZE
);
if
(
bytesRead
==
-
1
)
{
return
-
1
;
}
packetBuffer
.
setPosition
(
0
);
packetBuffer
.
setLimit
(
bytesRead
);
// TODO: Make it possible for adtsReader to consume the dataSource directly, so that it becomes
// unnecessary to copy the data through packetBuffer.
adtsReader
.
consume
(
packetBuffer
,
firstSampleTimestamp
,
firstPacket
);
firstPacket
=
false
;
if
(!
prepared
)
{
prepared
=
adtsReader
.
hasMediaFormat
();
}
return
bytesRead
;
}
@Override
protected
SampleQueue
getSampleQueue
(
int
track
)
{
Assertions
.
checkState
(
track
==
0
);
return
adtsReader
;
}
}
library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java
View file @
654d37fe
...
...
@@ -30,7 +30,7 @@ import java.util.Collections;
/**
* Parses a continuous ADTS byte stream and extracts individual frames.
*/
/* package */
class
AdtsReader
extends
PesPayload
Reader
{
/* package */
class
AdtsReader
extends
ElementaryStream
Reader
{
private
static
final
int
STATE_FINDING_SYNC
=
0
;
private
static
final
int
STATE_READING_HEADER
=
1
;
...
...
@@ -137,6 +137,8 @@ import java.util.Collections;
if
(
found
)
{
hasCrc
=
(
adtsData
[
i
]
&
0x1
)
==
0
;
pesBuffer
.
setPosition
(
i
+
1
);
// Reset lastByteWasFF for next time.
lastByteWasFF
=
false
;
return
true
;
}
}
...
...
library/src/main/java/com/google/android/exoplayer/hls/parser/
PesPayload
Reader.java
→
library/src/main/java/com/google/android/exoplayer/hls/parser/
ElementaryStream
Reader.java
View file @
654d37fe
...
...
@@ -19,11 +19,11 @@ import com.google.android.exoplayer.upstream.BufferPool;
import
com.google.android.exoplayer.util.ParsableByteArray
;
/**
* Extracts individual samples from
continuous byte
stream, preserving original order.
* Extracts individual samples from
an elementary media
stream, preserving original order.
*/
/* package */
abstract
class
PesPayload
Reader
extends
SampleQueue
{
/* package */
abstract
class
ElementaryStream
Reader
extends
SampleQueue
{
protected
PesPayload
Reader
(
BufferPool
bufferPool
)
{
protected
ElementaryStream
Reader
(
BufferPool
bufferPool
)
{
super
(
bufferPool
);
}
...
...
library/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java
View file @
654d37fe
...
...
@@ -30,7 +30,7 @@ import java.util.List;
/**
* Parses a continuous H264 byte stream and extracts individual frames.
*/
/* package */
class
H264Reader
extends
PesPayload
Reader
{
/* package */
class
H264Reader
extends
ElementaryStream
Reader
{
private
static
final
int
NAL_UNIT_TYPE_IDR
=
5
;
private
static
final
int
NAL_UNIT_TYPE_SEI
=
6
;
...
...
@@ -44,6 +44,8 @@ import java.util.List;
private
final
NalUnitTargetBuffer
pps
;
private
final
NalUnitTargetBuffer
sei
;
private
int
scratchEscapeCount
;
private
int
[]
scratchEscapePositions
;
private
boolean
isKeyframe
;
public
H264Reader
(
BufferPool
bufferPool
,
SeiReader
seiReader
)
{
...
...
@@ -53,6 +55,7 @@ import java.util.List;
sps
=
new
NalUnitTargetBuffer
(
NAL_UNIT_TYPE_SPS
,
128
);
pps
=
new
NalUnitTargetBuffer
(
NAL_UNIT_TYPE_PPS
,
128
);
sei
=
new
NalUnitTargetBuffer
(
NAL_UNIT_TYPE_SEI
,
128
);
scratchEscapePositions
=
new
int
[
10
];
}
@Override
...
...
@@ -133,7 +136,8 @@ import java.util.List;
sps
.
endNalUnit
(
discardPadding
);
pps
.
endNalUnit
(
discardPadding
);
if
(
sei
.
endNalUnit
(
discardPadding
))
{
seiReader
.
read
(
sei
.
nalData
,
0
,
pesTimeUs
);
int
unescapedLength
=
unescapeStream
(
sei
.
nalData
,
sei
.
nalLength
);
seiReader
.
read
(
sei
.
nalData
,
0
,
unescapedLength
,
pesTimeUs
);
}
}
...
...
@@ -147,8 +151,8 @@ import java.util.List;
initializationData
.
add
(
ppsData
);
// Unescape and then parse the SPS unit.
byte
[]
unescapedSps
=
unescapeStream
(
spsData
,
0
,
spsData
.
l
ength
);
ParsableBitArray
bitArray
=
new
ParsableBitArray
(
unescapedSps
);
unescapeStream
(
sps
.
nalData
,
sps
.
nalL
ength
);
ParsableBitArray
bitArray
=
new
ParsableBitArray
(
sps
.
nalData
);
bitArray
.
skipBits
(
32
);
// NAL header
int
profileIdc
=
bitArray
.
readBits
(
8
);
bitArray
.
skipBits
(
16
);
// constraint bits (6), reserved (2) and level_idc (8)
...
...
@@ -242,36 +246,45 @@ import java.util.List;
}
/**
* Replaces occurrences of [0, 0, 3] with [0, 0].
* Unescapes {@code data} up to the specified limit, replacing occurrences of [0, 0, 3] with
* [0, 0]. The unescaped data is returned in-place, with the return value indicating its length.
* <p>
* See ISO/IEC 14496-10:2005(E) page 36 for more information.
*
* @param data The data to unescape.
* @param limit The limit (exclusive) of the data to unescape.
* @return The length of the unescaped data.
*/
private
byte
[]
unescapeStream
(
byte
[]
data
,
int
offset
,
int
limit
)
{
int
position
=
offset
;
List
<
Integer
>
escapePositions
=
new
ArrayList
<
Integer
>()
;
private
int
unescapeStream
(
byte
[]
data
,
int
limit
)
{
int
position
=
0
;
scratchEscapeCount
=
0
;
while
(
position
<
limit
)
{
position
=
findNextUnescapeIndex
(
data
,
position
,
limit
);
if
(
position
<
limit
)
{
escapePositions
.
add
(
position
);
if
(
scratchEscapePositions
.
length
<=
scratchEscapeCount
)
{
// Grow scratchEscapePositions to hold a larger number of positions.
scratchEscapePositions
=
Arrays
.
copyOf
(
scratchEscapePositions
,
scratchEscapePositions
.
length
*
2
);
}
scratchEscapePositions
[
scratchEscapeCount
++]
=
position
;
position
+=
3
;
}
}
int
escapeCount
=
escapePositions
.
size
()
;
int
escapedPosition
=
offset
;
// The position being read from.
int
unescapedLength
=
limit
-
scratchEscapeCount
;
int
escapedPosition
=
0
;
// The position being read from.
int
unescapedPosition
=
0
;
// The position being written to.
byte
[]
unescapedData
=
new
byte
[
limit
-
offset
-
escapeCount
];
for
(
int
i
=
0
;
i
<
escapeCount
;
i
++)
{
int
nextEscapePosition
=
escapePositions
.
get
(
i
);
for
(
int
i
=
0
;
i
<
scratchEscapeCount
;
i
++)
{
int
nextEscapePosition
=
scratchEscapePositions
[
i
];
int
copyLength
=
nextEscapePosition
-
escapedPosition
;
System
.
arraycopy
(
data
,
escapedPosition
,
unescapedD
ata
,
unescapedPosition
,
copyLength
);
System
.
arraycopy
(
data
,
escapedPosition
,
d
ata
,
unescapedPosition
,
copyLength
);
escapedPosition
+=
copyLength
+
3
;
unescapedPosition
+=
copyLength
+
2
;
}
int
remainingLength
=
unescaped
Data
.
l
ength
-
unescapedPosition
;
System
.
arraycopy
(
data
,
escapedPosition
,
unescapedD
ata
,
unescapedPosition
,
remainingLength
);
return
unescaped
Data
;
int
remainingLength
=
unescaped
L
ength
-
unescapedPosition
;
System
.
arraycopy
(
data
,
escapedPosition
,
d
ata
,
unescapedPosition
,
remainingLength
);
return
unescaped
Length
;
}
private
int
findNextUnescapeIndex
(
byte
[]
bytes
,
int
offset
,
int
limit
)
{
...
...
library/src/main/java/com/google/android/exoplayer/hls/parser/HlsExtractor.java
0 → 100644
View file @
654d37fe
/*
* Copyright (C) 2014 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
.
exoplayer
.
hls
.
parser
;
import
com.google.android.exoplayer.MediaFormat
;
import
com.google.android.exoplayer.SampleHolder
;
import
com.google.android.exoplayer.upstream.DataSource
;
import
java.io.IOException
;
/**
* Facilitates extraction of media samples for HLS playbacks.
*/
// TODO: Consider consolidating more common logic in this base class.
public
abstract
class
HlsExtractor
{
private
final
boolean
shouldSpliceIn
;
// Accessed only by the consuming thread.
private
boolean
spliceConfigured
;
public
HlsExtractor
(
boolean
shouldSpliceIn
)
{
this
.
shouldSpliceIn
=
shouldSpliceIn
;
}
/**
* Attempts to configure a splice from this extractor to the next.
* <p>
* The splice is performed such that for each track the samples read from the next extractor
* start with a keyframe, and continue from where the samples read from this extractor finish.
* A successful splice may discard samples from either or both extractors.
* <p>
* Splice configuration may fail if the next extractor is not yet in a state that allows the
* splice to be performed. Calling this method is a noop if the splice has already been
* configured. Hence this method should be called repeatedly during the window within which a
* splice can be performed.
*
* @param nextExtractor The extractor being spliced to.
*/
public
final
void
configureSpliceTo
(
HlsExtractor
nextExtractor
)
{
if
(
spliceConfigured
||
!
nextExtractor
.
shouldSpliceIn
||
!
nextExtractor
.
isPrepared
())
{
// The splice is already configured, or the next extractor doesn't want to be spliced in, or
// the next extractor isn't ready to be spliced in.
return
;
}
boolean
spliceConfigured
=
true
;
int
trackCount
=
getTrackCount
();
for
(
int
i
=
0
;
i
<
trackCount
;
i
++)
{
spliceConfigured
&=
getSampleQueue
(
i
).
configureSpliceTo
(
nextExtractor
.
getSampleQueue
(
i
));
}
this
.
spliceConfigured
=
spliceConfigured
;
return
;
}
/**
* Gets the number of available tracks.
* <p>
* This method should only be called after the extractor has been prepared.
*
* @return The number of available tracks.
*/
public
abstract
int
getTrackCount
();
/**
* Gets the format of the specified track.
* <p>
* This method must only be called after the extractor has been prepared.
*
* @param track The track index.
* @return The corresponding format.
*/
public
abstract
MediaFormat
getFormat
(
int
track
);
/**
* Whether the extractor is prepared.
*
* @return True if the extractor is prepared. False otherwise.
*/
public
abstract
boolean
isPrepared
();
/**
* Releases the extractor, recycling any pending or incomplete samples to the sample pool.
* <p>
* This method should not be called whilst {@link #read(DataSource)} is also being invoked.
*/
public
abstract
void
release
();
/**
* Gets the largest timestamp of any sample parsed by the extractor.
*
* @return The largest timestamp, or {@link Long#MIN_VALUE} if no samples have been parsed.
*/
public
abstract
long
getLargestSampleTimestamp
();
/**
* Gets the next sample for the specified track.
*
* @param track The track from which to read.
* @param holder A {@link SampleHolder} into which the sample should be read.
* @return True if a sample was read. False otherwise.
*/
public
abstract
boolean
getSample
(
int
track
,
SampleHolder
holder
);
/**
* Discards samples for the specified track up to the specified time.
*
* @param track The track from which samples should be discarded.
* @param timeUs The time up to which samples should be discarded, in microseconds.
*/
public
abstract
void
discardUntil
(
int
track
,
long
timeUs
);
/**
* Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for the
* specified track.
*
* @return True if samples are available for reading from {@link #getSample(int, SampleHolder)}
* for the specified track. False otherwise.
*/
public
abstract
boolean
hasSamples
(
int
track
);
/**
* Reads up to a single TS packet.
*
* @param dataSource The {@link DataSource} from which to read.
* @throws IOException If an error occurred reading from the source.
* @return The number of bytes read from the source.
*/
public
abstract
int
read
(
DataSource
dataSource
)
throws
IOException
;
/**
* Gets the {@link SampleQueue} for the specified track.
*
* @param track The track index.
* @return The corresponding sample queue.
*/
protected
abstract
SampleQueue
getSampleQueue
(
int
track
);
}
library/src/main/java/com/google/android/exoplayer/hls/parser/Id3Reader.java
View file @
654d37fe
...
...
@@ -22,7 +22,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
/**
* Parses ID3 data and extracts individual text information frames.
*/
/* package */
class
Id3Reader
extends
PesPayload
Reader
{
/* package */
class
Id3Reader
extends
ElementaryStream
Reader
{
public
Id3Reader
(
BufferPool
bufferPool
)
{
super
(
bufferPool
);
...
...
library/src/main/java/com/google/android/exoplayer/hls/parser/SeiReader.java
View file @
654d37fe
...
...
@@ -36,14 +36,33 @@ import com.google.android.exoplayer.util.ParsableByteArray;
seiBuffer
=
new
ParsableByteArray
();
}
public
void
read
(
byte
[]
data
,
int
position
,
long
pesTimeUs
)
{
seiBuffer
.
reset
(
data
,
data
.
length
);
public
void
read
(
byte
[]
data
,
int
position
,
int
limit
,
long
pesTimeUs
)
{
seiBuffer
.
reset
(
data
,
limit
);
// Skip the NAL prefix and type.
seiBuffer
.
setPosition
(
position
+
4
);
int
ccDataSize
=
Eia608Parser
.
parseHeader
(
seiBuffer
);
if
(
ccDataSize
>
0
)
{
int
b
;
while
(
seiBuffer
.
bytesLeft
()
>
1
/* last byte will be rbsp_trailing_bits */
)
{
// Parse payload type.
int
payloadType
=
0
;
do
{
b
=
seiBuffer
.
readUnsignedByte
();
payloadType
+=
b
;
}
while
(
b
==
0xFF
);
// Parse payload size.
int
payloadSize
=
0
;
do
{
b
=
seiBuffer
.
readUnsignedByte
();
payloadSize
+=
b
;
}
while
(
b
==
0xFF
);
// Process the payload. We only support EIA-608 payloads currently.
if
(
Eia608Parser
.
inspectSeiMessage
(
payloadType
,
payloadSize
,
seiBuffer
))
{
startSample
(
pesTimeUs
);
appendData
(
seiBuffer
,
ccData
Size
);
appendData
(
seiBuffer
,
payload
Size
);
commitSample
(
true
);
}
else
{
seiBuffer
.
skip
(
payloadSize
);
}
}
}
...
...
library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java
View file @
654d37fe
...
...
@@ -32,7 +32,7 @@ import java.io.IOException;
/**
* Facilitates the extraction of data from the MPEG-2 TS container format.
*/
public
final
class
TsExtractor
{
public
final
class
TsExtractor
extends
HlsExtractor
{
private
static
final
String
TAG
=
"TsExtractor"
;
...
...
@@ -51,13 +51,9 @@ public final class TsExtractor {
private
final
SparseArray
<
SampleQueue
>
sampleQueues
;
// Indexed by streamType
private
final
SparseArray
<
TsPayloadReader
>
tsPayloadReaders
;
// Indexed by pid
private
final
BufferPool
bufferPool
;
private
final
boolean
shouldSpliceIn
;
private
final
long
firstSampleTimestamp
;
private
final
ParsableBitArray
tsScratch
;
// Accessed only by the consuming thread.
private
boolean
spliceConfigured
;
// Accessed only by the loading thread.
private
int
tsPacketBytesRead
;
private
long
timestampOffsetUs
;
...
...
@@ -66,9 +62,9 @@ public final class TsExtractor {
// Accessed by both the loading and consuming threads.
private
volatile
boolean
prepared
;
public
TsExtractor
(
long
firstSampleTimestamp
,
boolean
shouldSpliceIn
,
BufferPool
bufferPool
)
{
public
TsExtractor
(
boolean
shouldSpliceIn
,
long
firstSampleTimestamp
,
BufferPool
bufferPool
)
{
super
(
shouldSpliceIn
);
this
.
firstSampleTimestamp
=
firstSampleTimestamp
;
this
.
shouldSpliceIn
=
shouldSpliceIn
;
this
.
bufferPool
=
bufferPool
;
tsScratch
=
new
ParsableBitArray
(
new
byte
[
3
]);
tsPacketBuffer
=
new
ParsableByteArray
(
TS_PACKET_SIZE
);
...
...
@@ -78,86 +74,31 @@ public final class TsExtractor {
lastPts
=
Long
.
MIN_VALUE
;
}
/**
* Gets the number of available tracks.
* <p>
* This method should only be called after the extractor has been prepared.
*
* @return The number of available tracks.
*/
@Override
public
int
getTrackCount
()
{
Assertions
.
checkState
(
prepared
);
return
sampleQueues
.
size
();
}
/**
* Gets the format of the specified track.
* <p>
* This method must only be called after the extractor has been prepared.
*
* @param track The track index.
* @return The corresponding format.
*/
@Override
public
MediaFormat
getFormat
(
int
track
)
{
Assertions
.
checkState
(
prepared
);
return
sampleQueues
.
valueAt
(
track
).
getMediaFormat
();
}
/**
* Whether the extractor is prepared.
*
* @return True if the extractor is prepared. False otherwise.
*/
@Override
public
boolean
isPrepared
()
{
return
prepared
;
}
/**
* Releases the extractor, recycling any pending or incomplete samples to the sample pool.
* <p>
* This method should not be called whilst {@link #read(DataSource)} is also being invoked.
*/
@Override
public
void
release
()
{
for
(
int
i
=
0
;
i
<
sampleQueues
.
size
();
i
++)
{
sampleQueues
.
valueAt
(
i
).
release
();
}
}
/**
* Attempts to configure a splice from this extractor to the next.
* <p>
* The splice is performed such that for each track the samples read from the next extractor
* start with a keyframe, and continue from where the samples read from this extractor finish.
* A successful splice may discard samples from either or both extractors.
* <p>
* Splice configuration may fail if the next extractor is not yet in a state that allows the
* splice to be performed. Calling this method is a noop if the splice has already been
* configured. Hence this method should be called repeatedly during the window within which a
* splice can be performed.
*
* @param nextExtractor The extractor being spliced to.
*/
public
void
configureSpliceTo
(
TsExtractor
nextExtractor
)
{
Assertions
.
checkState
(
prepared
);
if
(
spliceConfigured
||
!
nextExtractor
.
shouldSpliceIn
||
!
nextExtractor
.
isPrepared
())
{
// The splice is already configured, or the next extractor doesn't want to be spliced in, or
// the next extractor isn't ready to be spliced in.
return
;
}
boolean
spliceConfigured
=
true
;
for
(
int
i
=
0
;
i
<
sampleQueues
.
size
();
i
++)
{
spliceConfigured
&=
sampleQueues
.
valueAt
(
i
).
configureSpliceTo
(
nextExtractor
.
sampleQueues
.
valueAt
(
i
));
}
this
.
spliceConfigured
=
spliceConfigured
;
return
;
}
/**
* Gets the largest timestamp of any sample parsed by the extractor.
*
* @return The largest timestamp, or {@link Long#MIN_VALUE} if no samples have been parsed.
*/
@Override
public
long
getLargestSampleTimestamp
()
{
long
largestParsedTimestampUs
=
Long
.
MIN_VALUE
;
for
(
int
i
=
0
;
i
<
sampleQueues
.
size
();
i
++)
{
...
...
@@ -167,36 +108,19 @@ public final class TsExtractor {
return
largestParsedTimestampUs
;
}
/**
* Gets the next sample for the specified track.
*
* @param track The track from which to read.
* @param holder A {@link SampleHolder} into which the sample should be read.
* @return True if a sample was read. False otherwise.
*/
@Override
public
boolean
getSample
(
int
track
,
SampleHolder
holder
)
{
Assertions
.
checkState
(
prepared
);
return
sampleQueues
.
valueAt
(
track
).
getSample
(
holder
);
}
/**
* Discards samples for the specified track up to the specified time.
*
* @param track The track from which samples should be discarded.
* @param timeUs The time up to which samples should be discarded, in microseconds.
*/
@Override
public
void
discardUntil
(
int
track
,
long
timeUs
)
{
Assertions
.
checkState
(
prepared
);
sampleQueues
.
valueAt
(
track
).
discardUntil
(
timeUs
);
}
/**
* Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for the
* specified track.
*
* @return True if samples are available for reading from {@link #getSample(int, SampleHolder)}
* for the specified track. False otherwise.
*/
@Override
public
boolean
hasSamples
(
int
track
)
{
Assertions
.
checkState
(
prepared
);
return
!
sampleQueues
.
valueAt
(
track
).
isEmpty
();
...
...
@@ -215,13 +139,7 @@ public final class TsExtractor {
return
true
;
}
/**
* Reads up to a single TS packet.
*
* @param dataSource The {@link DataSource} from which to read.
* @throws IOException If an error occurred reading from the source.
* @return The number of bytes read from the source.
*/
@Override
public
int
read
(
DataSource
dataSource
)
throws
IOException
{
int
bytesRead
=
dataSource
.
read
(
tsPacketBuffer
.
data
,
tsPacketBytesRead
,
TS_PACKET_SIZE
-
tsPacketBytesRead
);
...
...
@@ -276,6 +194,11 @@ public final class TsExtractor {
return
bytesRead
;
}
@Override
protected
SampleQueue
getSampleQueue
(
int
track
)
{
return
sampleQueues
.
valueAt
(
track
);
}
/**
* Adjusts a PTS value to the corresponding time in microseconds, accounting for PTS wraparound.
*
...
...
@@ -404,7 +327,7 @@ public final class TsExtractor {
continue
;
}
PesPayload
Reader
pesPayloadReader
=
null
;
ElementaryStream
Reader
pesPayloadReader
=
null
;
switch
(
streamType
)
{
case
TS_STREAM_TYPE_AAC:
pesPayloadReader
=
new
AdtsReader
(
bufferPool
);
...
...
@@ -444,7 +367,7 @@ public final class TsExtractor {
private
static
final
int
MAX_HEADER_EXTENSION_SIZE
=
5
;
private
final
ParsableBitArray
pesScratch
;
private
final
PesPayload
Reader
pesPayloadReader
;
private
final
ElementaryStream
Reader
pesPayloadReader
;
private
int
state
;
private
int
bytesRead
;
...
...
@@ -457,7 +380,7 @@ public final class TsExtractor {
private
long
timeUs
;
public
PesReader
(
PesPayload
Reader
pesPayloadReader
)
{
public
PesReader
(
ElementaryStream
Reader
pesPayloadReader
)
{
this
.
pesPayloadReader
=
pesPayloadReader
;
pesScratch
=
new
ParsableBitArray
(
new
byte
[
HEADER_SIZE
]);
state
=
STATE_FINDING_HEADER
;
...
...
library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java
View file @
654d37fe
...
...
@@ -161,7 +161,7 @@ public final class Mp4Util {
}
}
int
limit
=
endOffset
-
2
;
int
limit
=
endOffset
-
1
;
// We're looking for the NAL unit start code prefix 0x000001, followed by a byte that matches
// the specified type. The value of i tracks the index of the third byte in the four bytes
// being examined.
...
...
library/src/main/java/com/google/android/exoplayer/source/DefaultSampleSource.java
View file @
654d37fe
...
...
@@ -88,7 +88,7 @@ public final class DefaultSampleSource implements SampleSource {
Assertions
.
checkState
(
trackStates
[
track
]
==
TRACK_STATE_DISABLED
);
trackStates
[
track
]
=
TRACK_STATE_ENABLED
;
sampleExtractor
.
selectTrack
(
track
);
seekToUs
(
positionUs
);
seekToUs
Internal
(
positionUs
,
positionUs
!=
0
);
}
@Override
...
...
@@ -131,17 +131,7 @@ public final class DefaultSampleSource implements SampleSource {
@Override
public
void
seekToUs
(
long
positionUs
)
{
Assertions
.
checkState
(
prepared
);
if
(
seekPositionUs
!=
positionUs
)
{
// Avoid duplicate calls to the underlying extractor's seek method in the case that there
// have been no interleaving calls to readSample.
seekPositionUs
=
positionUs
;
sampleExtractor
.
seekTo
(
positionUs
);
for
(
int
i
=
0
;
i
<
trackStates
.
length
;
++
i
)
{
if
(
trackStates
[
i
]
!=
TRACK_STATE_DISABLED
)
{
pendingDiscontinuities
[
i
]
=
true
;
}
}
}
seekToUsInternal
(
positionUs
,
false
);
}
@Override
...
...
@@ -158,4 +148,18 @@ public final class DefaultSampleSource implements SampleSource {
}
}
private
void
seekToUsInternal
(
long
positionUs
,
boolean
force
)
{
// Unless forced, avoid duplicate calls to the underlying extractor's seek method in the case
// that there have been no interleaving calls to readSample.
if
(
force
||
seekPositionUs
!=
positionUs
)
{
seekPositionUs
=
positionUs
;
sampleExtractor
.
seekTo
(
positionUs
);
for
(
int
i
=
0
;
i
<
trackStates
.
length
;
++
i
)
{
if
(
trackStates
[
i
]
!=
TRACK_STATE_DISABLED
)
{
pendingDiscontinuities
[
i
]
=
true
;
}
}
}
}
}
library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java
View file @
654d37fe
...
...
@@ -97,14 +97,17 @@ public class Eia608Parser {
}
/* package */
ClosedCaptionList
parse
(
SampleHolder
sampleHolder
)
{
if
(
sampleHolder
.
size
<
=
0
)
{
if
(
sampleHolder
.
size
<
1
0
)
{
return
null
;
}
captions
.
clear
();
stringBuilder
.
setLength
(
0
);
seiBuffer
.
reset
(
sampleHolder
.
data
.
array
());
seiBuffer
.
skipBits
(
3
);
// reserved + process_cc_data_flag + zero_bit
// country_code (8) + provider_code (16) + user_identifier (32) + user_data_type_code (8) +
// reserved (1) + process_cc_data_flag (1) + zero_bit (1)
seiBuffer
.
skipBits
(
67
);
int
ccCount
=
seiBuffer
.
readBits
(
5
);
seiBuffer
.
skipBits
(
8
);
...
...
@@ -177,52 +180,28 @@ public class Eia608Parser {
}
/**
* Parses the beginning of SEI data and returns the size of underlying contains closed captions
* data following the header. Returns 0 if the SEI doesn't contain any closed captions data.
* Inspects an sei message to determine whether it contains EIA-608.
* <p>
* The position of {@code payload} is left unchanged.
*
* @param seiBuffer The buffer to read from.
* @return The size of closed captions data.
* @param payloadType The payload type of the message.
* @param payloadLength The length of the payload.
* @param payload A {@link ParsableByteArray} containing the payload.
* @return True if the sei message contains EIA-608. False otherwise.
*/
public
static
int
parseHeader
(
ParsableByteArray
seiBuffer
)
{
int
b
=
0
;
int
payloadType
=
0
;
do
{
b
=
seiBuffer
.
readUnsignedByte
();
payloadType
+=
b
;
}
while
(
b
==
0xFF
);
if
(
payloadType
!=
PAYLOAD_TYPE_CC
)
{
return
0
;
}
int
payloadSize
=
0
;
do
{
b
=
seiBuffer
.
readUnsignedByte
();
payloadSize
+=
b
;
}
while
(
b
==
0xFF
);
if
(
payloadSize
<=
0
)
{
return
0
;
}
int
countryCode
=
seiBuffer
.
readUnsignedByte
();
if
(
countryCode
!=
COUNTRY_CODE
)
{
return
0
;
}
int
providerCode
=
seiBuffer
.
readUnsignedShort
();
if
(
providerCode
!=
PROVIDER_CODE
)
{
return
0
;
}
int
userIdentifier
=
seiBuffer
.
readInt
();
if
(
userIdentifier
!=
USER_ID
)
{
return
0
;
}
int
userDataTypeCode
=
seiBuffer
.
readUnsignedByte
();
if
(
userDataTypeCode
!=
USER_DATA_TYPE_CODE
)
{
return
0
;
}
return
payloadSize
;
public
static
boolean
inspectSeiMessage
(
int
payloadType
,
int
payloadLength
,
ParsableByteArray
payload
)
{
if
(
payloadType
!=
PAYLOAD_TYPE_CC
||
payloadLength
<
8
)
{
return
false
;
}
int
startPosition
=
payload
.
getPosition
();
int
countryCode
=
payload
.
readUnsignedByte
();
int
providerCode
=
payload
.
readUnsignedShort
();
int
userIdentifier
=
payload
.
readInt
();
int
userDataTypeCode
=
payload
.
readUnsignedByte
();
payload
.
setPosition
(
startPosition
);
return
countryCode
==
COUNTRY_CODE
&&
providerCode
==
PROVIDER_CODE
&&
userIdentifier
==
USER_ID
&&
userDataTypeCode
==
USER_DATA_TYPE_CODE
;
}
}
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