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
0cb81693
authored
Dec 12, 2014
by
Oliver Woodman
Browse files
Options
_('Browse Files')
Download
Plain Diff
Merge branch 'dev-hls' into dev
parents
5a3340d6
1554db16
Show whitespace changes
Inline
Side-by-side
Showing
30 changed files
with
4410 additions
and
5 deletions
demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java
demo/src/main/java/com/google/android/exoplayer/demo/SampleChooserActivity.java
demo/src/main/java/com/google/android/exoplayer/demo/Samples.java
demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java
demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java
demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java
demo/src/main/java/com/google/android/exoplayer/demo/simple/HlsRendererBuilder.java
demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java
library/src/main/java/com/google/android/exoplayer/MediaFormat.java
library/src/main/java/com/google/android/exoplayer/hls/BitArrayChunk.java
library/src/main/java/com/google/android/exoplayer/hls/HlsChunk.java
library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java
library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java
library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylist.java
library/src/main/java/com/google/android/exoplayer/hls/HlsParserUtil.java
library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylist.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/TsExtractor.java
library/src/main/java/com/google/android/exoplayer/hls/Variant.java
library/src/main/java/com/google/android/exoplayer/metadata/ClosedCaption.java
library/src/main/java/com/google/android/exoplayer/metadata/Eia608Parser.java
library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java
library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java
library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java
library/src/main/java/com/google/android/exoplayer/metadata/TxxxMetadata.java
library/src/main/java/com/google/android/exoplayer/upstream/Aes128DataSource.java
library/src/main/java/com/google/android/exoplayer/util/BitArray.java
library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java
demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java
View file @
0cb81693
...
@@ -50,6 +50,7 @@ public class DemoUtil {
...
@@ -50,6 +50,7 @@ public class DemoUtil {
public
static
final
int
TYPE_DASH
=
0
;
public
static
final
int
TYPE_DASH
=
0
;
public
static
final
int
TYPE_SS
=
1
;
public
static
final
int
TYPE_SS
=
1
;
public
static
final
int
TYPE_OTHER
=
2
;
public
static
final
int
TYPE_OTHER
=
2
;
public
static
final
int
TYPE_HLS
=
3
;
public
static
final
boolean
EXPOSE_EXPERIMENTAL_FEATURES
=
false
;
public
static
final
boolean
EXPOSE_EXPERIMENTAL_FEATURES
=
false
;
...
...
demo/src/main/java/com/google/android/exoplayer/demo/SampleChooserActivity.java
View file @
0cb81693
...
@@ -56,6 +56,8 @@ public class SampleChooserActivity extends Activity {
...
@@ -56,6 +56,8 @@ public class SampleChooserActivity extends Activity {
sampleAdapter
.
addAll
((
Object
[])
Samples
.
SMOOTHSTREAMING
);
sampleAdapter
.
addAll
((
Object
[])
Samples
.
SMOOTHSTREAMING
);
sampleAdapter
.
add
(
new
Header
(
"Misc"
));
sampleAdapter
.
add
(
new
Header
(
"Misc"
));
sampleAdapter
.
addAll
((
Object
[])
Samples
.
MISC
);
sampleAdapter
.
addAll
((
Object
[])
Samples
.
MISC
);
sampleAdapter
.
add
(
new
Header
(
"HLS"
));
sampleAdapter
.
addAll
((
Object
[])
Samples
.
HLS
);
if
(
DemoUtil
.
EXPOSE_EXPERIMENTAL_FEATURES
)
{
if
(
DemoUtil
.
EXPOSE_EXPERIMENTAL_FEATURES
)
{
sampleAdapter
.
add
(
new
Header
(
"YouTube WebM DASH (Experimental)"
));
sampleAdapter
.
add
(
new
Header
(
"YouTube WebM DASH (Experimental)"
));
sampleAdapter
.
addAll
((
Object
[])
Samples
.
YOUTUBE_DASH_WEBM
);
sampleAdapter
.
addAll
((
Object
[])
Samples
.
YOUTUBE_DASH_WEBM
);
...
...
demo/src/main/java/com/google/android/exoplayer/demo/Samples.java
View file @
0cb81693
...
@@ -52,6 +52,9 @@ package com.google.android.exoplayer.demo;
...
@@ -52,6 +52,9 @@ package com.google.android.exoplayer.demo;
new
Sample
(
"Super speed (SmoothStreaming)"
,
"uid:ss:superspeed"
,
new
Sample
(
"Super speed (SmoothStreaming)"
,
"uid:ss:superspeed"
,
"http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism"
,
"http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism"
,
DemoUtil
.
TYPE_SS
,
false
),
DemoUtil
.
TYPE_SS
,
false
),
new
Sample
(
"Apple master playlist (HLS)"
,
"uid:hls:applemaster"
,
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/"
+
"bipbop_4x3_variant.m3u8"
,
DemoUtil
.
TYPE_HLS
,
false
),
new
Sample
(
"Dizzy (Misc)"
,
"uid:misc:dizzy"
,
new
Sample
(
"Dizzy (Misc)"
,
"uid:misc:dizzy"
,
"http://html5demos.com/assets/dizzy.mp4"
,
DemoUtil
.
TYPE_OTHER
,
false
),
"http://html5demos.com/assets/dizzy.mp4"
,
DemoUtil
.
TYPE_OTHER
,
false
),
};
};
...
@@ -124,6 +127,18 @@ package com.google.android.exoplayer.demo;
...
@@ -124,6 +127,18 @@ package com.google.android.exoplayer.demo;
+
"22727BB612D24AA4FACE4EF62726F9461A9BF57A&key=ik0"
,
DemoUtil
.
TYPE_DASH
,
true
),
+
"22727BB612D24AA4FACE4EF62726F9461A9BF57A&key=ik0"
,
DemoUtil
.
TYPE_DASH
,
true
),
};
};
public
static
final
Sample
[]
HLS
=
new
Sample
[]
{
new
Sample
(
"Apple master playlist"
,
"uid:hls:applemaster"
,
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/"
+
"bipbop_4x3_variant.m3u8"
,
DemoUtil
.
TYPE_HLS
,
true
),
new
Sample
(
"Apple master playlist advanced"
,
"uid:hls:applemasteradvanced"
,
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_16x9/"
+
"bipbop_16x9_variant.m3u8"
,
DemoUtil
.
TYPE_HLS
,
true
),
new
Sample
(
"Apple single media playlist"
,
"uid:hls:applesinglemedia"
,
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/"
+
"prog_index.m3u8"
,
DemoUtil
.
TYPE_HLS
,
true
),
};
public
static
final
Sample
[]
MISC
=
new
Sample
[]
{
public
static
final
Sample
[]
MISC
=
new
Sample
[]
{
new
Sample
(
"Dizzy"
,
"uid:misc:dizzy"
,
"http://html5demos.com/assets/dizzy.mp4"
,
new
Sample
(
"Dizzy"
,
"uid:misc:dizzy"
,
"http://html5demos.com/assets/dizzy.mp4"
,
DemoUtil
.
TYPE_OTHER
,
true
),
DemoUtil
.
TYPE_OTHER
,
true
),
...
...
demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java
View file @
0cb81693
...
@@ -25,8 +25,10 @@ import com.google.android.exoplayer.demo.full.player.DashRendererBuilder;
...
@@ -25,8 +25,10 @@ import com.google.android.exoplayer.demo.full.player.DashRendererBuilder;
import
com.google.android.exoplayer.demo.full.player.DefaultRendererBuilder
;
import
com.google.android.exoplayer.demo.full.player.DefaultRendererBuilder
;
import
com.google.android.exoplayer.demo.full.player.DemoPlayer
;
import
com.google.android.exoplayer.demo.full.player.DemoPlayer
;
import
com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder
;
import
com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder
;
import
com.google.android.exoplayer.demo.full.player.HlsRendererBuilder
;
import
com.google.android.exoplayer.demo.full.player.SmoothStreamingRendererBuilder
;
import
com.google.android.exoplayer.demo.full.player.SmoothStreamingRendererBuilder
;
import
com.google.android.exoplayer.demo.full.player.UnsupportedDrmException
;
import
com.google.android.exoplayer.demo.full.player.UnsupportedDrmException
;
import
com.google.android.exoplayer.metadata.TxxxMetadata
;
import
com.google.android.exoplayer.text.CaptionStyleCompat
;
import
com.google.android.exoplayer.text.CaptionStyleCompat
;
import
com.google.android.exoplayer.text.SubtitleView
;
import
com.google.android.exoplayer.text.SubtitleView
;
import
com.google.android.exoplayer.util.Util
;
import
com.google.android.exoplayer.util.Util
;
...
@@ -40,6 +42,7 @@ import android.graphics.Point;
...
@@ -40,6 +42,7 @@ import android.graphics.Point;
import
android.net.Uri
;
import
android.net.Uri
;
import
android.os.Bundle
;
import
android.os.Bundle
;
import
android.text.TextUtils
;
import
android.text.TextUtils
;
import
android.util.Log
;
import
android.view.Display
;
import
android.view.Display
;
import
android.view.Menu
;
import
android.view.Menu
;
import
android.view.MenuItem
;
import
android.view.MenuItem
;
...
@@ -57,11 +60,16 @@ import android.widget.PopupMenu.OnMenuItemClickListener;
...
@@ -57,11 +60,16 @@ import android.widget.PopupMenu.OnMenuItemClickListener;
import
android.widget.TextView
;
import
android.widget.TextView
;
import
android.widget.Toast
;
import
android.widget.Toast
;
import
java.util.Map
;
/**
/**
* An activity that plays media using {@link DemoPlayer}.
* An activity that plays media using {@link DemoPlayer}.
*/
*/
public
class
FullPlayerActivity
extends
Activity
implements
SurfaceHolder
.
Callback
,
OnClickListener
,
public
class
FullPlayerActivity
extends
Activity
implements
SurfaceHolder
.
Callback
,
OnClickListener
,
DemoPlayer
.
Listener
,
DemoPlayer
.
TextListener
,
AudioCapabilitiesReceiver
.
Listener
{
DemoPlayer
.
Listener
,
DemoPlayer
.
TextListener
,
DemoPlayer
.
Id3MetadataListener
,
AudioCapabilitiesReceiver
.
Listener
{
private
static
final
String
TAG
=
"FullPlayerActivity"
;
private
static
final
float
CAPTION_LINE_HEIGHT_RATIO
=
0.0533f
;
private
static
final
float
CAPTION_LINE_HEIGHT_RATIO
=
0.0533f
;
private
static
final
int
MENU_GROUP_TRACKS
=
1
;
private
static
final
int
MENU_GROUP_TRACKS
=
1
;
...
@@ -199,6 +207,8 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
...
@@ -199,6 +207,8 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
case
DemoUtil
.
TYPE_DASH
:
case
DemoUtil
.
TYPE_DASH
:
return
new
DashRendererBuilder
(
userAgent
,
contentUri
.
toString
(),
contentId
,
return
new
DashRendererBuilder
(
userAgent
,
contentUri
.
toString
(),
contentId
,
new
WidevineTestMediaDrmCallback
(
contentId
),
debugTextView
,
audioCapabilities
);
new
WidevineTestMediaDrmCallback
(
contentId
),
debugTextView
,
audioCapabilities
);
case
DemoUtil
.
TYPE_HLS
:
return
new
HlsRendererBuilder
(
userAgent
,
contentUri
.
toString
(),
contentId
);
default
:
default
:
return
new
DefaultRendererBuilder
(
this
,
contentUri
,
debugTextView
);
return
new
DefaultRendererBuilder
(
this
,
contentUri
,
debugTextView
);
}
}
...
@@ -209,6 +219,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
...
@@ -209,6 +219,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
player
=
new
DemoPlayer
(
getRendererBuilder
());
player
=
new
DemoPlayer
(
getRendererBuilder
());
player
.
addListener
(
this
);
player
.
addListener
(
this
);
player
.
setTextListener
(
this
);
player
.
setTextListener
(
this
);
player
.
setMetadataListener
(
this
);
player
.
seekTo
(
playerPosition
);
player
.
seekTo
(
playerPosition
);
playerNeedsPrepare
=
true
;
playerNeedsPrepare
=
true
;
mediaController
.
setMediaPlayer
(
player
.
getPlayerControl
());
mediaController
.
setMediaPlayer
(
player
.
getPlayerControl
());
...
@@ -435,6 +446,19 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
...
@@ -435,6 +446,19 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
}
}
}
}
// DemoPlayer.MetadataListener implementation
@Override
public
void
onId3Metadata
(
Map
<
String
,
Object
>
metadata
)
{
for
(
int
i
=
0
;
i
<
metadata
.
size
();
i
++)
{
if
(
metadata
.
containsKey
(
TxxxMetadata
.
TYPE
))
{
TxxxMetadata
txxxMetadata
=
(
TxxxMetadata
)
metadata
.
get
(
TxxxMetadata
.
TYPE
);
Log
.
i
(
TAG
,
String
.
format
(
"ID3 TimedMetadata: description=%s, value=%s"
,
txxxMetadata
.
description
,
txxxMetadata
.
value
));
}
}
}
// SurfaceHolder.Callback implementation
// SurfaceHolder.Callback implementation
@Override
@Override
...
...
demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java
View file @
0cb81693
...
@@ -27,6 +27,8 @@ import com.google.android.exoplayer.audio.AudioTrack;
...
@@ -27,6 +27,8 @@ import com.google.android.exoplayer.audio.AudioTrack;
import
com.google.android.exoplayer.chunk.ChunkSampleSource
;
import
com.google.android.exoplayer.chunk.ChunkSampleSource
;
import
com.google.android.exoplayer.chunk.MultiTrackChunkSource
;
import
com.google.android.exoplayer.chunk.MultiTrackChunkSource
;
import
com.google.android.exoplayer.drm.StreamingDrmSessionManager
;
import
com.google.android.exoplayer.drm.StreamingDrmSessionManager
;
import
com.google.android.exoplayer.metadata.ClosedCaption
;
import
com.google.android.exoplayer.metadata.MetadataTrackRenderer
;
import
com.google.android.exoplayer.text.TextTrackRenderer
;
import
com.google.android.exoplayer.text.TextTrackRenderer
;
import
com.google.android.exoplayer.upstream.DefaultBandwidthMeter
;
import
com.google.android.exoplayer.upstream.DefaultBandwidthMeter
;
import
com.google.android.exoplayer.util.PlayerControl
;
import
com.google.android.exoplayer.util.PlayerControl
;
...
@@ -37,6 +39,8 @@ import android.os.Looper;
...
@@ -37,6 +39,8 @@ import android.os.Looper;
import
android.view.Surface
;
import
android.view.Surface
;
import
java.io.IOException
;
import
java.io.IOException
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.concurrent.CopyOnWriteArrayList
;
import
java.util.concurrent.CopyOnWriteArrayList
;
/**
/**
...
@@ -135,6 +139,13 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
...
@@ -135,6 +139,13 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
void
onText
(
String
text
);
void
onText
(
String
text
);
}
}
/**
* A listener for receiving ID3 metadata parsed from the media stream.
*/
public
interface
Id3MetadataListener
{
void
onId3Metadata
(
Map
<
String
,
Object
>
metadata
);
}
// Constants pulled into this class for convenience.
// Constants pulled into this class for convenience.
public
static
final
int
STATE_IDLE
=
ExoPlayer
.
STATE_IDLE
;
public
static
final
int
STATE_IDLE
=
ExoPlayer
.
STATE_IDLE
;
public
static
final
int
STATE_PREPARING
=
ExoPlayer
.
STATE_PREPARING
;
public
static
final
int
STATE_PREPARING
=
ExoPlayer
.
STATE_PREPARING
;
...
@@ -145,11 +156,12 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
...
@@ -145,11 +156,12 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
public
static
final
int
DISABLED_TRACK
=
-
1
;
public
static
final
int
DISABLED_TRACK
=
-
1
;
public
static
final
int
PRIMARY_TRACK
=
0
;
public
static
final
int
PRIMARY_TRACK
=
0
;
public
static
final
int
RENDERER_COUNT
=
4
;
public
static
final
int
RENDERER_COUNT
=
5
;
public
static
final
int
TYPE_VIDEO
=
0
;
public
static
final
int
TYPE_VIDEO
=
0
;
public
static
final
int
TYPE_AUDIO
=
1
;
public
static
final
int
TYPE_AUDIO
=
1
;
public
static
final
int
TYPE_TEXT
=
2
;
public
static
final
int
TYPE_TEXT
=
2
;
public
static
final
int
TYPE_DEBUG
=
3
;
public
static
final
int
TYPE_TIMED_METADATA
=
3
;
public
static
final
int
TYPE_DEBUG
=
4
;
private
static
final
int
RENDERER_BUILDING_STATE_IDLE
=
1
;
private
static
final
int
RENDERER_BUILDING_STATE_IDLE
=
1
;
private
static
final
int
RENDERER_BUILDING_STATE_BUILDING
=
2
;
private
static
final
int
RENDERER_BUILDING_STATE_BUILDING
=
2
;
...
@@ -160,6 +172,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
...
@@ -160,6 +172,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
private
final
PlayerControl
playerControl
;
private
final
PlayerControl
playerControl
;
private
final
Handler
mainHandler
;
private
final
Handler
mainHandler
;
private
final
CopyOnWriteArrayList
<
Listener
>
listeners
;
private
final
CopyOnWriteArrayList
<
Listener
>
listeners
;
private
final
StringBuilder
closedCaptionStringBuilder
;
private
int
rendererBuildingState
;
private
int
rendererBuildingState
;
private
int
lastReportedPlaybackState
;
private
int
lastReportedPlaybackState
;
...
@@ -174,6 +187,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
...
@@ -174,6 +187,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
private
int
[]
selectedTracks
;
private
int
[]
selectedTracks
;
private
TextListener
textListener
;
private
TextListener
textListener
;
private
Id3MetadataListener
id3MetadataListener
;
private
InternalErrorListener
internalErrorListener
;
private
InternalErrorListener
internalErrorListener
;
private
InfoListener
infoListener
;
private
InfoListener
infoListener
;
...
@@ -189,6 +203,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
...
@@ -189,6 +203,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
selectedTracks
=
new
int
[
RENDERER_COUNT
];
selectedTracks
=
new
int
[
RENDERER_COUNT
];
// Disable text initially.
// Disable text initially.
selectedTracks
[
TYPE_TEXT
]
=
DISABLED_TRACK
;
selectedTracks
[
TYPE_TEXT
]
=
DISABLED_TRACK
;
closedCaptionStringBuilder
=
new
StringBuilder
();
}
}
public
PlayerControl
getPlayerControl
()
{
public
PlayerControl
getPlayerControl
()
{
...
@@ -215,6 +230,10 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
...
@@ -215,6 +230,10 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
textListener
=
listener
;
textListener
=
listener
;
}
}
public
void
setMetadataListener
(
Id3MetadataListener
listener
)
{
id3MetadataListener
=
listener
;
}
public
void
setSurface
(
Surface
surface
)
{
public
void
setSurface
(
Surface
surface
)
{
this
.
surface
=
surface
;
this
.
surface
=
surface
;
pushSurfaceAndVideoTrack
(
false
);
pushSurfaceAndVideoTrack
(
false
);
...
@@ -246,6 +265,9 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
...
@@ -246,6 +265,9 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
pushSurfaceAndVideoTrack
(
false
);
pushSurfaceAndVideoTrack
(
false
);
}
else
{
}
else
{
pushTrackSelection
(
type
,
true
);
pushTrackSelection
(
type
,
true
);
if
(
type
==
TYPE_TEXT
&&
index
==
DISABLED_TRACK
&&
textListener
!=
null
)
{
textListener
.
onText
(
null
);
}
}
}
}
}
...
@@ -454,9 +476,29 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
...
@@ -454,9 +476,29 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
@Override
@Override
public
void
onText
(
String
text
)
{
public
void
onText
(
String
text
)
{
if
(
textListener
!=
null
)
{
processText
(
text
);
textListener
.
onText
(
text
);
}
}
/* package */
MetadataTrackRenderer
.
MetadataRenderer
<
Map
<
String
,
Object
>>
getId3MetadataRenderer
()
{
return
new
MetadataTrackRenderer
.
MetadataRenderer
<
Map
<
String
,
Object
>>()
{
@Override
public
void
onMetadata
(
Map
<
String
,
Object
>
metadata
)
{
if
(
id3MetadataListener
!=
null
)
{
id3MetadataListener
.
onId3Metadata
(
metadata
);
}
}
};
}
/* package */
MetadataTrackRenderer
.
MetadataRenderer
<
List
<
ClosedCaption
>>
getClosedCaptionMetadataRenderer
()
{
return
new
MetadataTrackRenderer
.
MetadataRenderer
<
List
<
ClosedCaption
>>()
{
@Override
public
void
onMetadata
(
List
<
ClosedCaption
>
metadata
)
{
processClosedCaption
(
metadata
);
}
};
}
}
@Override
@Override
...
@@ -550,6 +592,36 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
...
@@ -550,6 +592,36 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
}
}
}
}
/* package */
void
processText
(
String
text
)
{
if
(
textListener
==
null
||
selectedTracks
[
TYPE_TEXT
]
==
DISABLED_TRACK
)
{
return
;
}
textListener
.
onText
(
text
);
}
/* package */
void
processClosedCaption
(
List
<
ClosedCaption
>
metadata
)
{
if
(
textListener
==
null
||
selectedTracks
[
TYPE_TEXT
]
==
DISABLED_TRACK
)
{
return
;
}
closedCaptionStringBuilder
.
setLength
(
0
);
for
(
ClosedCaption
caption
:
metadata
)
{
// Ignore control characters and just insert a new line in between words.
if
(
caption
.
type
==
ClosedCaption
.
TYPE_CTRL
)
{
if
(
closedCaptionStringBuilder
.
length
()
>
0
&&
closedCaptionStringBuilder
.
charAt
(
closedCaptionStringBuilder
.
length
()
-
1
)
!=
'\n'
)
{
closedCaptionStringBuilder
.
append
(
'\n'
);
}
}
else
if
(
caption
.
type
==
ClosedCaption
.
TYPE_TEXT
)
{
closedCaptionStringBuilder
.
append
(
caption
.
text
);
}
}
if
(
closedCaptionStringBuilder
.
length
()
>
0
&&
closedCaptionStringBuilder
.
charAt
(
closedCaptionStringBuilder
.
length
()
-
1
)
==
'\n'
)
{
closedCaptionStringBuilder
.
deleteCharAt
(
closedCaptionStringBuilder
.
length
()
-
1
);
}
textListener
.
onText
(
closedCaptionStringBuilder
.
toString
());
}
private
class
InternalRendererBuilderCallback
implements
RendererBuilderCallback
{
private
class
InternalRendererBuilderCallback
implements
RendererBuilderCallback
{
private
boolean
canceled
;
private
boolean
canceled
;
...
...
demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java
0 → 100644
View file @
0cb81693
/*
* 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
.
demo
.
full
.
player
;
import
com.google.android.exoplayer.MediaCodecAudioTrackRenderer
;
import
com.google.android.exoplayer.MediaCodecUtil
;
import
com.google.android.exoplayer.MediaCodecVideoTrackRenderer
;
import
com.google.android.exoplayer.TrackRenderer
;
import
com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder
;
import
com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilderCallback
;
import
com.google.android.exoplayer.hls.HlsChunkSource
;
import
com.google.android.exoplayer.hls.HlsPlaylist
;
import
com.google.android.exoplayer.hls.HlsPlaylistParser
;
import
com.google.android.exoplayer.hls.HlsSampleSource
;
import
com.google.android.exoplayer.metadata.ClosedCaption
;
import
com.google.android.exoplayer.metadata.Eia608Parser
;
import
com.google.android.exoplayer.metadata.Id3Parser
;
import
com.google.android.exoplayer.metadata.MetadataTrackRenderer
;
import
com.google.android.exoplayer.upstream.DataSource
;
import
com.google.android.exoplayer.upstream.DefaultBandwidthMeter
;
import
com.google.android.exoplayer.upstream.UriDataSource
;
import
com.google.android.exoplayer.util.ManifestFetcher
;
import
com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback
;
import
com.google.android.exoplayer.util.MimeTypes
;
import
android.media.MediaCodec
;
import
java.io.IOException
;
import
java.util.List
;
import
java.util.Map
;
/**
* A {@link RendererBuilder} for HLS.
*/
public
class
HlsRendererBuilder
implements
RendererBuilder
,
ManifestCallback
<
HlsPlaylist
>
{
private
final
String
userAgent
;
private
final
String
url
;
private
final
String
contentId
;
private
DemoPlayer
player
;
private
RendererBuilderCallback
callback
;
public
HlsRendererBuilder
(
String
userAgent
,
String
url
,
String
contentId
)
{
this
.
userAgent
=
userAgent
;
this
.
url
=
url
;
this
.
contentId
=
contentId
;
}
@Override
public
void
buildRenderers
(
DemoPlayer
player
,
RendererBuilderCallback
callback
)
{
this
.
player
=
player
;
this
.
callback
=
callback
;
HlsPlaylistParser
parser
=
new
HlsPlaylistParser
();
ManifestFetcher
<
HlsPlaylist
>
playlistFetcher
=
new
ManifestFetcher
<
HlsPlaylist
>(
parser
,
contentId
,
url
,
userAgent
);
playlistFetcher
.
singleLoad
(
player
.
getMainHandler
().
getLooper
(),
this
);
}
@Override
public
void
onManifestError
(
String
contentId
,
IOException
e
)
{
callback
.
onRenderersError
(
e
);
}
@Override
public
void
onManifest
(
String
contentId
,
HlsPlaylist
manifest
)
{
DefaultBandwidthMeter
bandwidthMeter
=
new
DefaultBandwidthMeter
();
DataSource
dataSource
=
new
UriDataSource
(
userAgent
,
bandwidthMeter
);
boolean
adaptiveDecoder
=
MediaCodecUtil
.
getDecoderInfo
(
MimeTypes
.
VIDEO_H264
,
false
).
adaptive
;
HlsChunkSource
chunkSource
=
new
HlsChunkSource
(
dataSource
,
url
,
manifest
,
bandwidthMeter
,
null
,
adaptiveDecoder
?
HlsChunkSource
.
ADAPTIVE_MODE_SPLICE
:
HlsChunkSource
.
ADAPTIVE_MODE_NONE
);
HlsSampleSource
sampleSource
=
new
HlsSampleSource
(
chunkSource
,
true
,
3
);
MediaCodecVideoTrackRenderer
videoRenderer
=
new
MediaCodecVideoTrackRenderer
(
sampleSource
,
MediaCodec
.
VIDEO_SCALING_MODE_SCALE_TO_FIT
,
0
,
player
.
getMainHandler
(),
player
,
50
);
MediaCodecAudioTrackRenderer
audioRenderer
=
new
MediaCodecAudioTrackRenderer
(
sampleSource
);
MetadataTrackRenderer
<
Map
<
String
,
Object
>>
id3Renderer
=
new
MetadataTrackRenderer
<
Map
<
String
,
Object
>>(
sampleSource
,
new
Id3Parser
(),
player
.
getId3MetadataRenderer
(),
player
.
getMainHandler
().
getLooper
());
MetadataTrackRenderer
<
List
<
ClosedCaption
>>
closedCaptionRenderer
=
new
MetadataTrackRenderer
<
List
<
ClosedCaption
>>(
sampleSource
,
new
Eia608Parser
(),
player
.
getClosedCaptionMetadataRenderer
(),
player
.
getMainHandler
().
getLooper
());
TrackRenderer
[]
renderers
=
new
TrackRenderer
[
DemoPlayer
.
RENDERER_COUNT
];
renderers
[
DemoPlayer
.
TYPE_VIDEO
]
=
videoRenderer
;
renderers
[
DemoPlayer
.
TYPE_AUDIO
]
=
audioRenderer
;
renderers
[
DemoPlayer
.
TYPE_TIMED_METADATA
]
=
id3Renderer
;
renderers
[
DemoPlayer
.
TYPE_TEXT
]
=
closedCaptionRenderer
;
callback
.
onRenderers
(
null
,
null
,
renderers
);
}
}
demo/src/main/java/com/google/android/exoplayer/demo/simple/HlsRendererBuilder.java
0 → 100644
View file @
0cb81693
/*
* 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
.
demo
.
simple
;
import
com.google.android.exoplayer.MediaCodecAudioTrackRenderer
;
import
com.google.android.exoplayer.MediaCodecUtil
;
import
com.google.android.exoplayer.MediaCodecVideoTrackRenderer
;
import
com.google.android.exoplayer.TrackRenderer
;
import
com.google.android.exoplayer.demo.full.player.DemoPlayer
;
import
com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilder
;
import
com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilderCallback
;
import
com.google.android.exoplayer.hls.HlsChunkSource
;
import
com.google.android.exoplayer.hls.HlsPlaylist
;
import
com.google.android.exoplayer.hls.HlsPlaylistParser
;
import
com.google.android.exoplayer.hls.HlsSampleSource
;
import
com.google.android.exoplayer.upstream.DataSource
;
import
com.google.android.exoplayer.upstream.DefaultBandwidthMeter
;
import
com.google.android.exoplayer.upstream.UriDataSource
;
import
com.google.android.exoplayer.util.ManifestFetcher
;
import
com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback
;
import
com.google.android.exoplayer.util.MimeTypes
;
import
android.media.MediaCodec
;
import
java.io.IOException
;
/**
* A {@link RendererBuilder} for HLS.
*/
/* package */
class
HlsRendererBuilder
implements
RendererBuilder
,
ManifestCallback
<
HlsPlaylist
>
{
private
final
SimplePlayerActivity
playerActivity
;
private
final
String
userAgent
;
private
final
String
url
;
private
final
String
contentId
;
private
RendererBuilderCallback
callback
;
public
HlsRendererBuilder
(
SimplePlayerActivity
playerActivity
,
String
userAgent
,
String
url
,
String
contentId
)
{
this
.
playerActivity
=
playerActivity
;
this
.
userAgent
=
userAgent
;
this
.
url
=
url
;
this
.
contentId
=
contentId
;
}
@Override
public
void
buildRenderers
(
RendererBuilderCallback
callback
)
{
this
.
callback
=
callback
;
HlsPlaylistParser
parser
=
new
HlsPlaylistParser
();
ManifestFetcher
<
HlsPlaylist
>
playlistFetcher
=
new
ManifestFetcher
<
HlsPlaylist
>(
parser
,
contentId
,
url
,
userAgent
);
playlistFetcher
.
singleLoad
(
playerActivity
.
getMainLooper
(),
this
);
}
@Override
public
void
onManifestError
(
String
contentId
,
IOException
e
)
{
callback
.
onRenderersError
(
e
);
}
@Override
public
void
onManifest
(
String
contentId
,
HlsPlaylist
manifest
)
{
DefaultBandwidthMeter
bandwidthMeter
=
new
DefaultBandwidthMeter
();
DataSource
dataSource
=
new
UriDataSource
(
userAgent
,
bandwidthMeter
);
boolean
adaptiveDecoder
=
MediaCodecUtil
.
getDecoderInfo
(
MimeTypes
.
VIDEO_H264
,
false
).
adaptive
;
HlsChunkSource
chunkSource
=
new
HlsChunkSource
(
dataSource
,
url
,
manifest
,
bandwidthMeter
,
null
,
adaptiveDecoder
?
HlsChunkSource
.
ADAPTIVE_MODE_SPLICE
:
HlsChunkSource
.
ADAPTIVE_MODE_NONE
);
HlsSampleSource
sampleSource
=
new
HlsSampleSource
(
chunkSource
,
true
,
2
);
MediaCodecVideoTrackRenderer
videoRenderer
=
new
MediaCodecVideoTrackRenderer
(
sampleSource
,
MediaCodec
.
VIDEO_SCALING_MODE_SCALE_TO_FIT
,
0
,
playerActivity
.
getMainHandler
(),
playerActivity
,
50
);
MediaCodecAudioTrackRenderer
audioRenderer
=
new
MediaCodecAudioTrackRenderer
(
sampleSource
);
TrackRenderer
[]
renderers
=
new
TrackRenderer
[
DemoPlayer
.
RENDERER_COUNT
];
renderers
[
DemoPlayer
.
TYPE_VIDEO
]
=
videoRenderer
;
renderers
[
DemoPlayer
.
TYPE_AUDIO
]
=
audioRenderer
;
callback
.
onRenderers
(
videoRenderer
,
audioRenderer
);
}
}
demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java
View file @
0cb81693
...
@@ -166,6 +166,8 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call
...
@@ -166,6 +166,8 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call
contentId
);
contentId
);
case
DemoUtil
.
TYPE_DASH
:
case
DemoUtil
.
TYPE_DASH
:
return
new
DashRendererBuilder
(
this
,
userAgent
,
contentUri
.
toString
(),
contentId
);
return
new
DashRendererBuilder
(
this
,
userAgent
,
contentUri
.
toString
(),
contentId
);
case
DemoUtil
.
TYPE_HLS
:
return
new
HlsRendererBuilder
(
this
,
userAgent
,
contentUri
.
toString
(),
contentId
);
default
:
default
:
return
new
DefaultRendererBuilder
(
this
,
contentUri
);
return
new
DefaultRendererBuilder
(
this
,
contentUri
);
}
}
...
...
library/src/main/java/com/google/android/exoplayer/MediaFormat.java
View file @
0cb81693
...
@@ -87,6 +87,14 @@ public class MediaFormat {
...
@@ -87,6 +87,14 @@ public class MediaFormat {
sampleRate
,
bitrate
,
initializationData
);
sampleRate
,
bitrate
,
initializationData
);
}
}
public
static
MediaFormat
createId3Format
()
{
return
createFormatForMimeType
(
MimeTypes
.
APPLICATION_ID3
);
}
public
static
MediaFormat
createEia608Format
()
{
return
createFormatForMimeType
(
MimeTypes
.
APPLICATION_EIA608
);
}
public
static
MediaFormat
createTtmlFormat
()
{
public
static
MediaFormat
createTtmlFormat
()
{
return
createFormatForMimeType
(
MimeTypes
.
APPLICATION_TTML
);
return
createFormatForMimeType
(
MimeTypes
.
APPLICATION_TTML
);
}
}
...
...
library/src/main/java/com/google/android/exoplayer/hls/BitArrayChunk.java
0 → 100644
View file @
0cb81693
/*
* 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
;
import
com.google.android.exoplayer.upstream.DataSource
;
import
com.google.android.exoplayer.upstream.DataSpec
;
import
com.google.android.exoplayer.util.BitArray
;
import
java.io.IOException
;
/**
* An abstract base class for {@link HlsChunk} implementations where the data should be loaded into
* a {@link BitArray} and subsequently consumed.
*/
public
abstract
class
BitArrayChunk
extends
HlsChunk
{
private
static
final
int
READ_GRANULARITY
=
16
*
1024
;
private
final
BitArray
bitArray
;
private
volatile
boolean
loadFinished
;
private
volatile
boolean
loadCanceled
;
/**
* @param dataSource The source from which the data should be loaded.
* @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed
* {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then
* the length resolved by {@code dataSource.open(dataSpec)} must not exceed
* {@link Integer#MAX_VALUE}.
* @param bitArray The {@link BitArray} into which the data should be loaded.
*/
public
BitArrayChunk
(
DataSource
dataSource
,
DataSpec
dataSpec
,
BitArray
bitArray
)
{
super
(
dataSource
,
dataSpec
);
this
.
bitArray
=
bitArray
;
}
@Override
public
void
consume
()
throws
IOException
{
consume
(
bitArray
);
}
/**
* Invoked by {@link #consume()}. Implementations should override this method to consume the
* loaded data.
*
* @param bitArray The {@link BitArray} containing the loaded data.
* @throws IOException If an error occurs consuming the loaded data.
*/
protected
abstract
void
consume
(
BitArray
bitArray
)
throws
IOException
;
/**
* Whether the whole of the chunk has been loaded.
*
* @return True if the whole of the chunk has been loaded. False otherwise.
*/
@Override
public
boolean
isLoadFinished
()
{
return
loadFinished
;
}
// Loadable implementation
@Override
public
final
void
cancelLoad
()
{
loadCanceled
=
true
;
}
@Override
public
final
boolean
isLoadCanceled
()
{
return
loadCanceled
;
}
@Override
public
final
void
load
()
throws
IOException
,
InterruptedException
{
try
{
bitArray
.
reset
();
dataSource
.
open
(
dataSpec
);
int
bytesRead
=
0
;
while
(
bytesRead
!=
-
1
&&
!
loadCanceled
)
{
bytesRead
=
bitArray
.
append
(
dataSource
,
READ_GRANULARITY
);
}
loadFinished
=
!
loadCanceled
;
}
finally
{
dataSource
.
close
();
}
}
}
library/src/main/java/com/google/android/exoplayer/hls/HlsChunk.java
0 → 100644
View file @
0cb81693
/*
* 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
;
import
com.google.android.exoplayer.upstream.DataSource
;
import
com.google.android.exoplayer.upstream.DataSpec
;
import
com.google.android.exoplayer.upstream.Loader.Loadable
;
import
com.google.android.exoplayer.util.Assertions
;
import
java.io.IOException
;
/**
* An abstract base class for {@link Loadable} implementations that load chunks of data required
* for the playback of HLS streams.
*/
public
abstract
class
HlsChunk
implements
Loadable
{
protected
final
DataSource
dataSource
;
protected
final
DataSpec
dataSpec
;
/**
* @param dataSource The source from which the data should be loaded.
* @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed
* {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then
* the length resolved by {@code dataSource.open(dataSpec)} must not exceed
* {@link Integer#MAX_VALUE}.
*/
public
HlsChunk
(
DataSource
dataSource
,
DataSpec
dataSpec
)
{
Assertions
.
checkState
(
dataSpec
.
length
<=
Integer
.
MAX_VALUE
);
this
.
dataSource
=
Assertions
.
checkNotNull
(
dataSource
);
this
.
dataSpec
=
Assertions
.
checkNotNull
(
dataSpec
);
}
public
abstract
void
consume
()
throws
IOException
;
public
abstract
boolean
isLoadFinished
();
}
library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java
0 → 100644
View file @
0cb81693
/*
* 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
;
import
com.google.android.exoplayer.C
;
import
com.google.android.exoplayer.MediaFormat
;
import
com.google.android.exoplayer.hls.TsExtractor.SamplePool
;
import
com.google.android.exoplayer.upstream.Aes128DataSource
;
import
com.google.android.exoplayer.upstream.BandwidthMeter
;
import
com.google.android.exoplayer.upstream.DataSource
;
import
com.google.android.exoplayer.upstream.DataSpec
;
import
com.google.android.exoplayer.util.Assertions
;
import
com.google.android.exoplayer.util.BitArray
;
import
com.google.android.exoplayer.util.Util
;
import
android.net.Uri
;
import
android.os.SystemClock
;
import
java.io.ByteArrayInputStream
;
import
java.io.IOException
;
import
java.math.BigInteger
;
import
java.util.ArrayList
;
import
java.util.Collections
;
import
java.util.List
;
import
java.util.Locale
;
/**
* A temporary test source of HLS chunks.
* <p>
* TODO: Figure out whether this should merge with the chunk package, or whether the hls
* implementation is going to naturally diverge.
*/
public
class
HlsChunkSource
{
/**
* Adaptive switching is disabled.
* <p>
* The initially selected variant will be used throughout playback.
*/
public
static
final
int
ADAPTIVE_MODE_NONE
=
0
;
/**
* Adaptive switches splice overlapping segments of the old and new variants.
* <p>
* When performing a switch from one variant to another, overlapping segments will be requested
* from both the old and new variants. These segments will then be spliced together, allowing
* a seamless switch from one variant to another even if keyframes are misaligned or if keyframes
* are not positioned at the start of each segment.
* <p>
* Note that where it can be guaranteed that the source content has keyframes positioned at the
* start of each segment, {@link #ADAPTIVE_MODE_ABRUPT} should always be used in preference to
* this mode.
*/
public
static
final
int
ADAPTIVE_MODE_SPLICE
=
1
;
/**
* Adaptive switches are performed at segment boundaries.
* <p>
* For this mode to perform seamless switches, the source content is required to have keyframes
* positioned at the start of each segment. If this is not the case a visual discontinuity may
* be experienced when switching from one variant to another.
* <p>
* Note that where it can be guaranteed that the source content does have keyframes positioned at
* the start of each segment, this mode should always be used in preference to
* {@link #ADAPTIVE_MODE_SPLICE} because it requires fetching less data.
*/
public
static
final
int
ADAPTIVE_MODE_ABRUPT
=
3
;
/**
* The default target buffer duration in milliseconds.
*/
public
static
final
long
DEFAULT_TARGET_BUFFER_DURATION_MS
=
40000
;
/**
* The default minimum duration of media that needs to be buffered for a switch to a higher
* quality variant to be considered.
*/
public
static
final
long
DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS
=
5000
;
/**
* The default maximum duration of media that needs to be buffered for a switch to a lower
* quality variant to be considered.
*/
public
static
final
long
DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS
=
20000
;
private
static
final
float
BANDWIDTH_FRACTION
=
0.8f
;
private
final
SamplePool
samplePool
=
new
TsExtractor
.
SamplePool
();
private
final
DataSource
upstreamDataSource
;
private
final
HlsPlaylistParser
playlistParser
;
private
final
Variant
[]
enabledVariants
;
private
final
BandwidthMeter
bandwidthMeter
;
private
final
BitArray
bitArray
;
private
final
int
adaptiveMode
;
private
final
Uri
baseUri
;
private
final
int
maxWidth
;
private
final
int
maxHeight
;
private
final
long
targetBufferDurationUs
;
private
final
long
minBufferDurationToSwitchUpUs
;
private
final
long
maxBufferDurationToSwitchDownUs
;
/* package */
final
HlsMediaPlaylist
[]
mediaPlaylists
;
/* package */
final
long
[]
lastMediaPlaylistLoadTimesMs
;
/* package */
boolean
live
;
/* package */
long
durationUs
;
private
int
variantIndex
;
private
DataSource
encryptedDataSource
;
private
Uri
encryptionKeyUri
;
private
String
encryptedDataSourceIv
;
private
byte
[]
encryptedDataSourceSecretKey
;
public
HlsChunkSource
(
DataSource
dataSource
,
String
playlistUrl
,
HlsPlaylist
playlist
,
BandwidthMeter
bandwidthMeter
,
int
[]
variantIndices
,
int
adaptiveMode
)
{
this
(
dataSource
,
playlistUrl
,
playlist
,
bandwidthMeter
,
variantIndices
,
adaptiveMode
,
DEFAULT_TARGET_BUFFER_DURATION_MS
,
DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS
,
DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS
);
}
/**
* @param dataSource A {@link DataSource} suitable for loading the media data.
* @param playlistUrl The playlist URL.
* @param playlist The hls playlist.
* @param bandwidthMeter provides an estimate of the currently available bandwidth.
* @param variantIndices A subset of variant indices to consider, or null to consider all of the
* variants in the master playlist.
* @param adaptiveMode The mode for switching from one variant to another. One of
* {@link #ADAPTIVE_MODE_NONE}, {@link #ADAPTIVE_MODE_ABRUPT} and
* {@link #ADAPTIVE_MODE_SPLICE}.
* @param targetBufferDurationMs The targeted duration of media to buffer ahead of the current
* playback position. Note that the greater this value, the greater the amount of memory
* that will be consumed.
* @param minBufferDurationToSwitchUpMs The minimum duration of media that needs to be buffered
* for a switch to a higher quality variant to be considered.
* @param maxBufferDurationToSwitchDownMs The maximum duration of media that needs to be buffered
* for a switch to a lower quality variant to be considered.
*/
public
HlsChunkSource
(
DataSource
dataSource
,
String
playlistUrl
,
HlsPlaylist
playlist
,
BandwidthMeter
bandwidthMeter
,
int
[]
variantIndices
,
int
adaptiveMode
,
long
targetBufferDurationMs
,
long
minBufferDurationToSwitchUpMs
,
long
maxBufferDurationToSwitchDownMs
)
{
this
.
upstreamDataSource
=
dataSource
;
this
.
bandwidthMeter
=
bandwidthMeter
;
this
.
adaptiveMode
=
adaptiveMode
;
targetBufferDurationUs
=
targetBufferDurationMs
*
1000
;
minBufferDurationToSwitchUpUs
=
minBufferDurationToSwitchUpMs
*
1000
;
maxBufferDurationToSwitchDownUs
=
maxBufferDurationToSwitchDownMs
*
1000
;
baseUri
=
playlist
.
baseUri
;
bitArray
=
new
BitArray
();
playlistParser
=
new
HlsPlaylistParser
();
if
(
playlist
.
type
==
HlsPlaylist
.
TYPE_MEDIA
)
{
enabledVariants
=
new
Variant
[]
{
new
Variant
(
0
,
playlistUrl
,
0
,
null
,
-
1
,
-
1
)};
mediaPlaylists
=
new
HlsMediaPlaylist
[
1
];
lastMediaPlaylistLoadTimesMs
=
new
long
[
1
];
setMediaPlaylist
(
0
,
(
HlsMediaPlaylist
)
playlist
);
}
else
{
Assertions
.
checkState
(
playlist
.
type
==
HlsPlaylist
.
TYPE_MASTER
);
enabledVariants
=
filterVariants
((
HlsMasterPlaylist
)
playlist
,
variantIndices
);
mediaPlaylists
=
new
HlsMediaPlaylist
[
enabledVariants
.
length
];
lastMediaPlaylistLoadTimesMs
=
new
long
[
enabledVariants
.
length
];
}
int
maxWidth
=
-
1
;
int
maxHeight
=
-
1
;
// Select the first variant from the master playlist that's enabled.
long
minOriginalVariantIndex
=
Integer
.
MAX_VALUE
;
for
(
int
i
=
0
;
i
<
enabledVariants
.
length
;
i
++)
{
if
(
enabledVariants
[
i
].
index
<
minOriginalVariantIndex
)
{
minOriginalVariantIndex
=
enabledVariants
[
i
].
index
;
variantIndex
=
i
;
}
maxWidth
=
Math
.
max
(
enabledVariants
[
i
].
width
,
maxWidth
);
maxHeight
=
Math
.
max
(
enabledVariants
[
i
].
width
,
maxHeight
);
}
// TODO: We should allow the default values to be passed through the constructor.
this
.
maxWidth
=
maxWidth
>
0
?
maxWidth
:
1920
;
this
.
maxHeight
=
maxHeight
>
0
?
maxHeight
:
1080
;
}
public
long
getDurationUs
()
{
return
live
?
C
.
UNKNOWN_TIME_US
:
durationUs
;
}
/**
* Adaptive implementations must set the maximum video dimensions on the supplied
* {@link MediaFormat}. Other implementations do nothing.
* <p>
* Only called when the source is enabled.
*
* @param out The {@link MediaFormat} on which the maximum video dimensions should be set.
*/
public
void
getMaxVideoDimensions
(
MediaFormat
out
)
{
out
.
setMaxVideoDimensions
(
maxWidth
,
maxHeight
);
}
/**
* Returns the next {@link HlsChunk} that should be loaded.
*
* @param previousTsChunk The previously loaded chunk that the next chunk should follow.
* @param seekPositionUs If there is no previous chunk, this parameter must specify the seek
* position. If there is a previous chunk then this parameter is ignored.
* @param playbackPositionUs The current playback position.
* @return The next chunk to load.
*/
public
HlsChunk
getChunkOperation
(
TsChunk
previousTsChunk
,
long
seekPositionUs
,
long
playbackPositionUs
)
{
if
(
previousTsChunk
!=
null
&&
(
previousTsChunk
.
isLastChunk
||
previousTsChunk
.
endTimeUs
-
playbackPositionUs
>=
targetBufferDurationUs
))
{
// We're either finished, or we have the target amount of data buffered.
return
null
;
}
int
nextVariantIndex
=
variantIndex
;
boolean
switchingVariant
=
false
;
boolean
switchingVariantSpliced
=
false
;
if
(
adaptiveMode
==
ADAPTIVE_MODE_NONE
)
{
// Do nothing.
}
else
{
nextVariantIndex
=
getNextVariantIndex
(
previousTsChunk
,
playbackPositionUs
);
switchingVariant
=
nextVariantIndex
!=
variantIndex
;
switchingVariantSpliced
=
switchingVariant
&&
adaptiveMode
==
ADAPTIVE_MODE_SPLICE
;
}
HlsMediaPlaylist
mediaPlaylist
=
mediaPlaylists
[
nextVariantIndex
];
if
(
mediaPlaylist
==
null
)
{
// We don't have the media playlist for the next variant. Request it now.
return
newMediaPlaylistChunk
(
nextVariantIndex
);
}
variantIndex
=
nextVariantIndex
;
int
chunkMediaSequence
=
0
;
boolean
liveDiscontinuity
=
false
;
if
(
live
)
{
if
(
previousTsChunk
==
null
)
{
chunkMediaSequence
=
getLiveStartChunkMediaSequence
(
variantIndex
);
}
else
{
chunkMediaSequence
=
switchingVariantSpliced
?
previousTsChunk
.
chunkIndex
:
previousTsChunk
.
chunkIndex
+
1
;
if
(
chunkMediaSequence
<
mediaPlaylist
.
mediaSequence
)
{
// If the chunk is no longer in the playlist. Skip ahead and start again.
chunkMediaSequence
=
getLiveStartChunkMediaSequence
(
variantIndex
);
liveDiscontinuity
=
true
;
}
}
}
else
{
// Not live.
if
(
previousTsChunk
==
null
)
{
chunkMediaSequence
=
Util
.
binarySearchFloor
(
mediaPlaylist
.
segments
,
seekPositionUs
,
true
,
true
)
+
mediaPlaylist
.
mediaSequence
;
}
else
{
chunkMediaSequence
=
switchingVariantSpliced
?
previousTsChunk
.
chunkIndex
:
previousTsChunk
.
chunkIndex
+
1
;
}
}
int
chunkIndex
=
chunkMediaSequence
-
mediaPlaylist
.
mediaSequence
;
if
(
chunkIndex
>=
mediaPlaylist
.
segments
.
size
())
{
if
(
mediaPlaylist
.
live
&&
shouldRerequestMediaPlaylist
(
variantIndex
))
{
return
newMediaPlaylistChunk
(
variantIndex
);
}
else
{
return
null
;
}
}
HlsMediaPlaylist
.
Segment
segment
=
mediaPlaylist
.
segments
.
get
(
chunkIndex
);
Uri
chunkUri
=
Util
.
getMergedUri
(
mediaPlaylist
.
baseUri
,
segment
.
url
);
// Check if encryption is specified.
if
(
HlsMediaPlaylist
.
ENCRYPTION_METHOD_AES_128
.
equals
(
segment
.
encryptionMethod
))
{
Uri
keyUri
=
Util
.
getMergedUri
(
mediaPlaylist
.
baseUri
,
segment
.
encryptionKeyUri
);
if
(!
keyUri
.
equals
(
encryptionKeyUri
))
{
// Encryption is specified and the key has changed.
HlsChunk
toReturn
=
newEncryptionKeyChunk
(
keyUri
,
segment
.
encryptionIV
);
return
toReturn
;
}
if
(!
Util
.
areEqual
(
segment
.
encryptionIV
,
encryptedDataSourceIv
))
{
initEncryptedDataSource
(
keyUri
,
segment
.
encryptionIV
,
encryptedDataSourceSecretKey
);
}
}
else
{
clearEncryptedDataSource
();
}
// Configure the data source and spec for the chunk.
DataSource
dataSource
=
encryptedDataSource
!=
null
?
encryptedDataSource
:
upstreamDataSource
;
DataSpec
dataSpec
=
new
DataSpec
(
chunkUri
,
segment
.
byterangeOffset
,
segment
.
byterangeLength
,
null
);
// Compute start and end times, and the sequence number of the next chunk.
long
startTimeUs
;
if
(
live
)
{
if
(
previousTsChunk
==
null
)
{
startTimeUs
=
0
;
}
else
if
(
switchingVariantSpliced
)
{
startTimeUs
=
previousTsChunk
.
startTimeUs
;
}
else
{
startTimeUs
=
previousTsChunk
.
endTimeUs
;
}
}
else
/* Not live */
{
startTimeUs
=
segment
.
startTimeUs
;
}
long
endTimeUs
=
startTimeUs
+
(
long
)
(
segment
.
durationSecs
*
C
.
MICROS_PER_SECOND
);
boolean
isLastChunk
=
!
mediaPlaylist
.
live
&&
chunkIndex
==
mediaPlaylist
.
segments
.
size
()
-
1
;
// Configure the extractor that will read the chunk.
TsExtractor
extractor
;
if
(
previousTsChunk
==
null
||
segment
.
discontinuity
||
switchingVariant
||
liveDiscontinuity
)
{
extractor
=
new
TsExtractor
(
startTimeUs
,
samplePool
,
switchingVariantSpliced
);
}
else
{
extractor
=
previousTsChunk
.
extractor
;
}
return
new
TsChunk
(
dataSource
,
dataSpec
,
extractor
,
enabledVariants
[
variantIndex
].
index
,
startTimeUs
,
endTimeUs
,
chunkMediaSequence
,
isLastChunk
);
}
private
int
getNextVariantIndex
(
TsChunk
previousTsChunk
,
long
playbackPositionUs
)
{
int
idealVariantIndex
=
getVariantIndexForBandwdith
(
(
int
)
(
bandwidthMeter
.
getBitrateEstimate
()
*
BANDWIDTH_FRACTION
));
if
(
idealVariantIndex
==
variantIndex
)
{
// We're already using the ideal variant.
return
variantIndex
;
}
// We're not using the ideal variant for the available bandwidth, but only switch if the
// conditions are appropriate.
long
bufferedPositionUs
=
previousTsChunk
==
null
?
playbackPositionUs
:
adaptiveMode
==
ADAPTIVE_MODE_SPLICE
?
previousTsChunk
.
startTimeUs
:
previousTsChunk
.
endTimeUs
;
long
bufferedUs
=
bufferedPositionUs
-
playbackPositionUs
;
if
((
idealVariantIndex
>
variantIndex
&&
bufferedUs
<
maxBufferDurationToSwitchDownUs
)
||
(
idealVariantIndex
<
variantIndex
&&
bufferedUs
>
minBufferDurationToSwitchUpUs
))
{
// Switch variant.
return
idealVariantIndex
;
}
// Stick with the current variant for now.
return
variantIndex
;
}
private
int
getVariantIndexForBandwdith
(
int
bandwidth
)
{
for
(
int
i
=
0
;
i
<
enabledVariants
.
length
-
1
;
i
++)
{
if
(
enabledVariants
[
i
].
bandwidth
<=
bandwidth
)
{
return
i
;
}
}
return
enabledVariants
.
length
-
1
;
}
private
boolean
shouldRerequestMediaPlaylist
(
int
variantIndex
)
{
// Don't re-request media playlist more often than one-half of the target duration.
HlsMediaPlaylist
mediaPlaylist
=
mediaPlaylists
[
variantIndex
];
long
timeSinceLastMediaPlaylistLoadMs
=
SystemClock
.
elapsedRealtime
()
-
lastMediaPlaylistLoadTimesMs
[
variantIndex
];
return
timeSinceLastMediaPlaylistLoadMs
>=
(
mediaPlaylist
.
targetDurationSecs
*
1000
)
/
2
;
}
private
int
getLiveStartChunkMediaSequence
(
int
variantIndex
)
{
// For live start playback from the third chunk from the end.
HlsMediaPlaylist
mediaPlaylist
=
mediaPlaylists
[
variantIndex
];
int
chunkIndex
=
mediaPlaylist
.
segments
.
size
()
>
3
?
mediaPlaylist
.
segments
.
size
()
-
3
:
0
;
return
chunkIndex
+
mediaPlaylist
.
mediaSequence
;
}
private
MediaPlaylistChunk
newMediaPlaylistChunk
(
int
variantIndex
)
{
Uri
mediaPlaylistUri
=
Util
.
getMergedUri
(
baseUri
,
enabledVariants
[
variantIndex
].
url
);
DataSpec
dataSpec
=
new
DataSpec
(
mediaPlaylistUri
,
0
,
C
.
LENGTH_UNBOUNDED
,
null
);
Uri
baseUri
=
Util
.
parseBaseUri
(
mediaPlaylistUri
.
toString
());
return
new
MediaPlaylistChunk
(
variantIndex
,
upstreamDataSource
,
dataSpec
,
baseUri
);
}
private
EncryptionKeyChunk
newEncryptionKeyChunk
(
Uri
keyUri
,
String
iv
)
{
DataSpec
dataSpec
=
new
DataSpec
(
keyUri
,
0
,
C
.
LENGTH_UNBOUNDED
,
null
);
return
new
EncryptionKeyChunk
(
upstreamDataSource
,
dataSpec
,
iv
);
}
/* package */
void
initEncryptedDataSource
(
Uri
keyUri
,
String
iv
,
byte
[]
secretKey
)
{
String
trimmedIv
;
if
(
iv
.
toLowerCase
(
Locale
.
getDefault
()).
startsWith
(
"0x"
))
{
trimmedIv
=
iv
.
substring
(
2
);
}
else
{
trimmedIv
=
iv
;
}
byte
[]
ivData
=
new
BigInteger
(
trimmedIv
,
16
).
toByteArray
();
byte
[]
ivDataWithPadding
=
new
byte
[
16
];
int
offset
=
ivData
.
length
>
16
?
ivData
.
length
-
16
:
0
;
System
.
arraycopy
(
ivData
,
offset
,
ivDataWithPadding
,
ivDataWithPadding
.
length
-
ivData
.
length
+
offset
,
ivData
.
length
-
offset
);
encryptedDataSource
=
new
Aes128DataSource
(
secretKey
,
ivDataWithPadding
,
upstreamDataSource
);
encryptionKeyUri
=
keyUri
;
encryptedDataSourceIv
=
iv
;
encryptedDataSourceSecretKey
=
secretKey
;
}
private
void
clearEncryptedDataSource
()
{
encryptionKeyUri
=
null
;
encryptedDataSource
=
null
;
encryptedDataSourceIv
=
null
;
encryptedDataSourceSecretKey
=
null
;
}
/* package */
void
setMediaPlaylist
(
int
variantIndex
,
HlsMediaPlaylist
mediaPlaylist
)
{
lastMediaPlaylistLoadTimesMs
[
variantIndex
]
=
SystemClock
.
elapsedRealtime
();
mediaPlaylists
[
variantIndex
]
=
mediaPlaylist
;
live
|=
mediaPlaylist
.
live
;
durationUs
=
mediaPlaylist
.
durationUs
;
}
private
static
Variant
[]
filterVariants
(
HlsMasterPlaylist
masterPlaylist
,
int
[]
variantIndices
)
{
List
<
Variant
>
masterVariants
=
masterPlaylist
.
variants
;
ArrayList
<
Variant
>
enabledVariants
=
new
ArrayList
<
Variant
>();
if
(
variantIndices
!=
null
)
{
for
(
int
i
=
0
;
i
<
variantIndices
.
length
;
i
++)
{
enabledVariants
.
add
(
masterVariants
.
get
(
variantIndices
[
i
]));
}
}
else
{
// If variantIndices is null then all variants are initially considered.
enabledVariants
.
addAll
(
masterVariants
);
}
ArrayList
<
Variant
>
definiteVideoVariants
=
new
ArrayList
<
Variant
>();
ArrayList
<
Variant
>
definiteAudioOnlyVariants
=
new
ArrayList
<
Variant
>();
for
(
int
i
=
0
;
i
<
enabledVariants
.
size
();
i
++)
{
Variant
variant
=
enabledVariants
.
get
(
i
);
if
(
variant
.
height
>
0
||
variantHasExplicitCodecWithPrefix
(
variant
,
"avc"
))
{
definiteVideoVariants
.
add
(
variant
);
}
else
if
(
variantHasExplicitCodecWithPrefix
(
variant
,
"mp4a"
))
{
definiteAudioOnlyVariants
.
add
(
variant
);
}
}
if
(!
definiteVideoVariants
.
isEmpty
())
{
// 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.
enabledVariants
=
definiteVideoVariants
;
}
else
if
(
definiteAudioOnlyVariants
.
size
()
<
enabledVariants
.
size
())
{
// 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.
enabledVariants
.
removeAll
(
definiteAudioOnlyVariants
);
}
else
{
// Leave the enabled variants unchanged. They're likely either all video or all audio.
}
Collections
.
sort
(
enabledVariants
,
new
Variant
.
DecreasingBandwidthComparator
());
Variant
[]
enabledVariantsArray
=
new
Variant
[
enabledVariants
.
size
()];
enabledVariants
.
toArray
(
enabledVariantsArray
);
return
enabledVariantsArray
;
}
private
static
boolean
variantHasExplicitCodecWithPrefix
(
Variant
variant
,
String
prefix
)
{
String
[]
codecs
=
variant
.
codecs
;
if
(
codecs
==
null
)
{
return
false
;
}
for
(
int
i
=
0
;
i
<
codecs
.
length
;
i
++)
{
if
(
codecs
[
i
].
startsWith
(
prefix
))
{
return
true
;
}
}
return
false
;
}
private
class
MediaPlaylistChunk
extends
BitArrayChunk
{
@SuppressWarnings
(
"hiding"
)
private
final
int
variantIndex
;
private
final
Uri
playlistBaseUri
;
public
MediaPlaylistChunk
(
int
variantIndex
,
DataSource
dataSource
,
DataSpec
dataSpec
,
Uri
playlistBaseUri
)
{
super
(
dataSource
,
dataSpec
,
bitArray
);
this
.
variantIndex
=
variantIndex
;
this
.
playlistBaseUri
=
playlistBaseUri
;
}
@Override
protected
void
consume
(
BitArray
data
)
throws
IOException
{
HlsPlaylist
playlist
=
playlistParser
.
parse
(
new
ByteArrayInputStream
(
data
.
getData
(),
0
,
data
.
bytesLeft
()),
null
,
null
,
playlistBaseUri
);
Assertions
.
checkState
(
playlist
.
type
==
HlsPlaylist
.
TYPE_MEDIA
);
HlsMediaPlaylist
mediaPlaylist
=
(
HlsMediaPlaylist
)
playlist
;
setMediaPlaylist
(
variantIndex
,
mediaPlaylist
);
}
}
private
class
EncryptionKeyChunk
extends
BitArrayChunk
{
private
final
String
iv
;
public
EncryptionKeyChunk
(
DataSource
dataSource
,
DataSpec
dataSpec
,
String
iv
)
{
super
(
dataSource
,
dataSpec
,
bitArray
);
this
.
iv
=
iv
;
}
@Override
protected
void
consume
(
BitArray
data
)
throws
IOException
{
byte
[]
secretKey
=
new
byte
[
data
.
bytesLeft
()];
data
.
readBytes
(
secretKey
,
0
,
secretKey
.
length
);
initEncryptedDataSource
(
dataSpec
.
uri
,
iv
,
secretKey
);
}
}
}
library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java
0 → 100644
View file @
0cb81693
/*
* 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
;
import
android.net.Uri
;
import
java.util.List
;
/**
* Represents an HLS master playlist.
*/
public
final
class
HlsMasterPlaylist
extends
HlsPlaylist
{
public
final
List
<
Variant
>
variants
;
public
HlsMasterPlaylist
(
Uri
baseUri
,
List
<
Variant
>
variants
)
{
super
(
baseUri
,
HlsPlaylist
.
TYPE_MASTER
);
this
.
variants
=
variants
;
}
}
library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylist.java
0 → 100644
View file @
0cb81693
/*
* 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
;
import
com.google.android.exoplayer.C
;
import
android.net.Uri
;
import
java.util.List
;
/**
* Represents an HLS media playlist.
*/
public
final
class
HlsMediaPlaylist
extends
HlsPlaylist
{
/**
* Media segment reference.
*/
public
static
final
class
Segment
implements
Comparable
<
Long
>
{
public
final
boolean
discontinuity
;
public
final
double
durationSecs
;
public
final
String
url
;
public
final
long
startTimeUs
;
public
final
String
encryptionMethod
;
public
final
String
encryptionKeyUri
;
public
final
String
encryptionIV
;
public
final
int
byterangeOffset
;
public
final
int
byterangeLength
;
public
Segment
(
String
uri
,
double
durationSecs
,
boolean
discontinuity
,
long
startTimeUs
,
String
encryptionMethod
,
String
encryptionKeyUri
,
String
encryptionIV
,
int
byterangeOffset
,
int
byterangeLength
)
{
this
.
url
=
uri
;
this
.
durationSecs
=
durationSecs
;
this
.
discontinuity
=
discontinuity
;
this
.
startTimeUs
=
startTimeUs
;
this
.
encryptionMethod
=
encryptionMethod
;
this
.
encryptionKeyUri
=
encryptionKeyUri
;
this
.
encryptionIV
=
encryptionIV
;
this
.
byterangeOffset
=
byterangeOffset
;
this
.
byterangeLength
=
byterangeLength
;
}
@Override
public
int
compareTo
(
Long
startTimeUs
)
{
return
this
.
startTimeUs
>
startTimeUs
?
1
:
(
this
.
startTimeUs
<
startTimeUs
?
-
1
:
0
);
}
}
public
static
final
String
ENCRYPTION_METHOD_NONE
=
"NONE"
;
public
static
final
String
ENCRYPTION_METHOD_AES_128
=
"AES-128"
;
public
final
int
mediaSequence
;
public
final
int
targetDurationSecs
;
public
final
int
version
;
public
final
List
<
Segment
>
segments
;
public
final
boolean
live
;
public
final
long
durationUs
;
public
HlsMediaPlaylist
(
Uri
baseUri
,
int
mediaSequence
,
int
targetDurationSecs
,
int
version
,
boolean
live
,
List
<
Segment
>
segments
)
{
super
(
baseUri
,
HlsPlaylist
.
TYPE_MEDIA
);
this
.
mediaSequence
=
mediaSequence
;
this
.
targetDurationSecs
=
targetDurationSecs
;
this
.
version
=
version
;
this
.
live
=
live
;
this
.
segments
=
segments
;
if
(!
segments
.
isEmpty
())
{
Segment
last
=
segments
.
get
(
segments
.
size
()
-
1
);
durationUs
=
last
.
startTimeUs
+
(
long
)
(
last
.
durationSecs
*
C
.
MICROS_PER_SECOND
);
}
else
{
durationUs
=
0
;
}
}
}
library/src/main/java/com/google/android/exoplayer/hls/HlsParserUtil.java
0 → 100644
View file @
0cb81693
/*
* 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
;
import
com.google.android.exoplayer.ParserException
;
import
java.util.regex.Matcher
;
import
java.util.regex.Pattern
;
/**
* Utility methods for HLS manifest parsing.
*/
/* package */
class
HlsParserUtil
{
private
HlsParserUtil
()
{}
public
static
String
parseStringAttr
(
String
line
,
Pattern
pattern
,
String
tag
)
throws
ParserException
{
Matcher
matcher
=
pattern
.
matcher
(
line
);
if
(
matcher
.
find
()
&&
matcher
.
groupCount
()
==
1
)
{
return
matcher
.
group
(
1
);
}
throw
new
ParserException
(
String
.
format
(
"Couldn't match %s tag in %s"
,
tag
,
line
));
}
public
static
String
parseOptionalStringAttr
(
String
line
,
Pattern
pattern
)
{
Matcher
matcher
=
pattern
.
matcher
(
line
);
if
(
matcher
.
find
()
&&
matcher
.
groupCount
()
==
1
)
{
return
matcher
.
group
(
1
);
}
return
null
;
}
public
static
int
parseIntAttr
(
String
line
,
Pattern
pattern
,
String
tag
)
throws
ParserException
{
return
Integer
.
parseInt
(
parseStringAttr
(
line
,
pattern
,
tag
));
}
public
static
double
parseDoubleAttr
(
String
line
,
Pattern
pattern
,
String
tag
)
throws
ParserException
{
return
Double
.
parseDouble
(
parseStringAttr
(
line
,
pattern
,
tag
));
}
}
library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylist.java
0 → 100644
View file @
0cb81693
/*
* 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
;
import
android.net.Uri
;
/**
* Represents an HLS playlist.
*/
public
abstract
class
HlsPlaylist
{
public
final
static
int
TYPE_MASTER
=
0
;
public
final
static
int
TYPE_MEDIA
=
1
;
public
final
Uri
baseUri
;
public
final
int
type
;
protected
HlsPlaylist
(
Uri
baseUri
,
int
type
)
{
this
.
baseUri
=
baseUri
;
this
.
type
=
type
;
}
}
library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java
0 → 100644
View file @
0cb81693
/*
* 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
;
import
com.google.android.exoplayer.C
;
import
com.google.android.exoplayer.ParserException
;
import
com.google.android.exoplayer.hls.HlsMediaPlaylist.Segment
;
import
com.google.android.exoplayer.util.ManifestParser
;
import
android.net.Uri
;
import
java.io.BufferedReader
;
import
java.io.IOException
;
import
java.io.InputStream
;
import
java.io.InputStreamReader
;
import
java.util.ArrayList
;
import
java.util.Collections
;
import
java.util.LinkedList
;
import
java.util.List
;
import
java.util.Queue
;
import
java.util.regex.Pattern
;
/**
* HLS playlists parsing logic.
*/
public
final
class
HlsPlaylistParser
implements
ManifestParser
<
HlsPlaylist
>
{
private
static
final
String
VERSION_TAG
=
"#EXT-X-VERSION"
;
private
static
final
String
STREAM_INF_TAG
=
"#EXT-X-STREAM-INF"
;
private
static
final
String
BANDWIDTH_ATTR
=
"BANDWIDTH"
;
private
static
final
String
CODECS_ATTR
=
"CODECS"
;
private
static
final
String
RESOLUTION_ATTR
=
"RESOLUTION"
;
private
static
final
String
DISCONTINUITY_TAG
=
"#EXT-X-DISCONTINUITY"
;
private
static
final
String
MEDIA_DURATION_TAG
=
"#EXTINF"
;
private
static
final
String
MEDIA_SEQUENCE_TAG
=
"#EXT-X-MEDIA-SEQUENCE"
;
private
static
final
String
TARGET_DURATION_TAG
=
"#EXT-X-TARGETDURATION"
;
private
static
final
String
ENDLIST_TAG
=
"#EXT-X-ENDLIST"
;
private
static
final
String
KEY_TAG
=
"#EXT-X-KEY"
;
private
static
final
String
BYTERANGE_TAG
=
"#EXT-X-BYTERANGE"
;
private
static
final
String
METHOD_ATTR
=
"METHOD"
;
private
static
final
String
URI_ATTR
=
"URI"
;
private
static
final
String
IV_ATTR
=
"IV"
;
private
static
final
Pattern
BANDWIDTH_ATTR_REGEX
=
Pattern
.
compile
(
BANDWIDTH_ATTR
+
"=(\\d+)\\b"
);
private
static
final
Pattern
CODECS_ATTR_REGEX
=
Pattern
.
compile
(
CODECS_ATTR
+
"=\"(.+)\""
);
private
static
final
Pattern
RESOLUTION_ATTR_REGEX
=
Pattern
.
compile
(
RESOLUTION_ATTR
+
"=(\\d+x\\d+)"
);
private
static
final
Pattern
MEDIA_DURATION_REGEX
=
Pattern
.
compile
(
MEDIA_DURATION_TAG
+
":([\\d.]+),"
);
private
static
final
Pattern
MEDIA_SEQUENCE_REGEX
=
Pattern
.
compile
(
MEDIA_SEQUENCE_TAG
+
":(\\d+)\\b"
);
private
static
final
Pattern
TARGET_DURATION_REGEX
=
Pattern
.
compile
(
TARGET_DURATION_TAG
+
":(\\d+)\\b"
);
private
static
final
Pattern
VERSION_REGEX
=
Pattern
.
compile
(
VERSION_TAG
+
":(\\d+)\\b"
);
private
static
final
Pattern
BYTERANGE_REGEX
=
Pattern
.
compile
(
BYTERANGE_TAG
+
":(\\d+(?:@\\d+)?)\\b"
);
private
static
final
Pattern
METHOD_ATTR_REGEX
=
Pattern
.
compile
(
METHOD_ATTR
+
"=([^,.*]+)"
);
private
static
final
Pattern
URI_ATTR_REGEX
=
Pattern
.
compile
(
URI_ATTR
+
"=\"(.+)\""
);
private
static
final
Pattern
IV_ATTR_REGEX
=
Pattern
.
compile
(
IV_ATTR
+
"=([^,.*]+)"
);
@Override
public
HlsPlaylist
parse
(
InputStream
inputStream
,
String
inputEncoding
,
String
contentId
,
Uri
baseUri
)
throws
IOException
{
BufferedReader
reader
=
new
BufferedReader
((
inputEncoding
==
null
)
?
new
InputStreamReader
(
inputStream
)
:
new
InputStreamReader
(
inputStream
,
inputEncoding
));
Queue
<
String
>
extraLines
=
new
LinkedList
<
String
>();
String
line
;
try
{
while
((
line
=
reader
.
readLine
())
!=
null
)
{
line
=
line
.
trim
();
if
(
line
.
isEmpty
())
{
// Do nothing.
}
else
if
(
line
.
startsWith
(
STREAM_INF_TAG
))
{
extraLines
.
add
(
line
);
return
parseMasterPlaylist
(
new
LineIterator
(
extraLines
,
reader
),
baseUri
);
}
else
if
(
line
.
startsWith
(
TARGET_DURATION_TAG
)
||
line
.
startsWith
(
MEDIA_SEQUENCE_TAG
)
||
line
.
startsWith
(
MEDIA_DURATION_TAG
)
||
line
.
startsWith
(
KEY_TAG
)
||
line
.
startsWith
(
BYTERANGE_TAG
)
||
line
.
equals
(
DISCONTINUITY_TAG
)
||
line
.
equals
(
ENDLIST_TAG
))
{
extraLines
.
add
(
line
);
return
parseMediaPlaylist
(
new
LineIterator
(
extraLines
,
reader
),
baseUri
);
}
else
if
(
line
.
startsWith
(
VERSION_TAG
))
{
extraLines
.
add
(
line
);
}
else
if
(!
line
.
startsWith
(
"#"
))
{
throw
new
ParserException
(
"Missing a tag before URL."
);
}
}
}
finally
{
reader
.
close
();
}
throw
new
ParserException
(
"Failed to parse the playlist, could not identify any tags."
);
}
private
static
HlsMasterPlaylist
parseMasterPlaylist
(
LineIterator
iterator
,
Uri
baseUri
)
throws
IOException
{
List
<
Variant
>
variants
=
new
ArrayList
<
Variant
>();
int
bandwidth
=
0
;
String
[]
codecs
=
null
;
int
width
=
-
1
;
int
height
=
-
1
;
int
variantIndex
=
0
;
String
line
;
while
(
iterator
.
hasNext
())
{
line
=
iterator
.
next
();
if
(
line
.
startsWith
(
STREAM_INF_TAG
))
{
bandwidth
=
HlsParserUtil
.
parseIntAttr
(
line
,
BANDWIDTH_ATTR_REGEX
,
BANDWIDTH_ATTR
);
String
codecsString
=
HlsParserUtil
.
parseOptionalStringAttr
(
line
,
CODECS_ATTR_REGEX
);
if
(
codecsString
!=
null
)
{
codecs
=
codecsString
.
split
(
"(\\s*,\\s*)|(\\s*$)"
);
}
else
{
codecs
=
null
;
}
String
resolutionString
=
HlsParserUtil
.
parseOptionalStringAttr
(
line
,
RESOLUTION_ATTR_REGEX
);
if
(
resolutionString
!=
null
)
{
String
[]
widthAndHeight
=
resolutionString
.
split
(
"x"
);
width
=
Integer
.
parseInt
(
widthAndHeight
[
0
]);
height
=
Integer
.
parseInt
(
widthAndHeight
[
1
]);
}
else
{
width
=
-
1
;
height
=
-
1
;
}
}
else
if
(!
line
.
startsWith
(
"#"
))
{
variants
.
add
(
new
Variant
(
variantIndex
++,
line
,
bandwidth
,
codecs
,
width
,
height
));
bandwidth
=
0
;
codecs
=
null
;
width
=
-
1
;
height
=
-
1
;
}
}
return
new
HlsMasterPlaylist
(
baseUri
,
Collections
.
unmodifiableList
(
variants
));
}
private
static
HlsMediaPlaylist
parseMediaPlaylist
(
LineIterator
iterator
,
Uri
baseUri
)
throws
IOException
{
int
mediaSequence
=
0
;
int
targetDurationSecs
=
0
;
int
version
=
1
;
// Default version == 1.
boolean
live
=
true
;
List
<
Segment
>
segments
=
new
ArrayList
<
Segment
>();
double
segmentDurationSecs
=
0.0
;
boolean
segmentDiscontinuity
=
false
;
long
segmentStartTimeUs
=
0
;
String
segmentEncryptionMethod
=
null
;
String
segmentEncryptionKeyUri
=
null
;
String
segmentEncryptionIV
=
null
;
int
segmentByterangeOffset
=
0
;
int
segmentByterangeLength
=
C
.
LENGTH_UNBOUNDED
;
int
segmentMediaSequence
=
0
;
String
line
;
while
(
iterator
.
hasNext
())
{
line
=
iterator
.
next
();
if
(
line
.
startsWith
(
TARGET_DURATION_TAG
))
{
targetDurationSecs
=
HlsParserUtil
.
parseIntAttr
(
line
,
TARGET_DURATION_REGEX
,
TARGET_DURATION_TAG
);
}
else
if
(
line
.
startsWith
(
MEDIA_SEQUENCE_TAG
))
{
mediaSequence
=
HlsParserUtil
.
parseIntAttr
(
line
,
MEDIA_SEQUENCE_REGEX
,
MEDIA_SEQUENCE_TAG
);
segmentMediaSequence
=
mediaSequence
;
}
else
if
(
line
.
startsWith
(
VERSION_TAG
))
{
version
=
HlsParserUtil
.
parseIntAttr
(
line
,
VERSION_REGEX
,
VERSION_TAG
);
}
else
if
(
line
.
startsWith
(
MEDIA_DURATION_TAG
))
{
segmentDurationSecs
=
HlsParserUtil
.
parseDoubleAttr
(
line
,
MEDIA_DURATION_REGEX
,
MEDIA_DURATION_TAG
);
}
else
if
(
line
.
startsWith
(
KEY_TAG
))
{
segmentEncryptionMethod
=
HlsParserUtil
.
parseStringAttr
(
line
,
METHOD_ATTR_REGEX
,
METHOD_ATTR
);
if
(
segmentEncryptionMethod
.
equals
(
HlsMediaPlaylist
.
ENCRYPTION_METHOD_NONE
))
{
segmentEncryptionKeyUri
=
null
;
segmentEncryptionIV
=
null
;
}
else
{
segmentEncryptionKeyUri
=
HlsParserUtil
.
parseStringAttr
(
line
,
URI_ATTR_REGEX
,
URI_ATTR
);
segmentEncryptionIV
=
HlsParserUtil
.
parseOptionalStringAttr
(
line
,
IV_ATTR_REGEX
);
if
(
segmentEncryptionIV
==
null
)
{
segmentEncryptionIV
=
Integer
.
toHexString
(
segmentMediaSequence
);
}
}
}
else
if
(
line
.
startsWith
(
BYTERANGE_TAG
))
{
String
byteRange
=
HlsParserUtil
.
parseStringAttr
(
line
,
BYTERANGE_REGEX
,
BYTERANGE_TAG
);
String
[]
splitByteRange
=
byteRange
.
split
(
"@"
);
segmentByterangeLength
=
Integer
.
parseInt
(
splitByteRange
[
0
]);
if
(
splitByteRange
.
length
>
1
)
{
segmentByterangeOffset
=
Integer
.
parseInt
(
splitByteRange
[
1
]);
}
}
else
if
(
line
.
equals
(
DISCONTINUITY_TAG
))
{
segmentDiscontinuity
=
true
;
}
else
if
(!
line
.
startsWith
(
"#"
))
{
segmentMediaSequence
++;
if
(
segmentByterangeLength
==
C
.
LENGTH_UNBOUNDED
)
{
segmentByterangeOffset
=
0
;
}
segments
.
add
(
new
Segment
(
line
,
segmentDurationSecs
,
segmentDiscontinuity
,
segmentStartTimeUs
,
segmentEncryptionMethod
,
segmentEncryptionKeyUri
,
segmentEncryptionIV
,
segmentByterangeOffset
,
segmentByterangeLength
));
segmentStartTimeUs
+=
(
long
)
(
segmentDurationSecs
*
C
.
MICROS_PER_SECOND
);
segmentDiscontinuity
=
false
;
segmentDurationSecs
=
0.0
;
if
(
segmentByterangeLength
!=
C
.
LENGTH_UNBOUNDED
)
{
segmentByterangeOffset
+=
segmentByterangeLength
;
}
segmentByterangeLength
=
C
.
LENGTH_UNBOUNDED
;
}
else
if
(
line
.
equals
(
ENDLIST_TAG
))
{
live
=
false
;
break
;
}
}
return
new
HlsMediaPlaylist
(
baseUri
,
mediaSequence
,
targetDurationSecs
,
version
,
live
,
Collections
.
unmodifiableList
(
segments
));
}
private
static
class
LineIterator
{
private
final
BufferedReader
reader
;
private
final
Queue
<
String
>
extraLines
;
private
String
next
;
public
LineIterator
(
Queue
<
String
>
extraLines
,
BufferedReader
reader
)
{
this
.
extraLines
=
extraLines
;
this
.
reader
=
reader
;
}
public
boolean
hasNext
()
throws
IOException
{
if
(
next
!=
null
)
{
return
true
;
}
if
(!
extraLines
.
isEmpty
())
{
next
=
extraLines
.
poll
();
return
true
;
}
while
((
next
=
reader
.
readLine
())
!=
null
)
{
next
=
next
.
trim
();
if
(!
next
.
isEmpty
())
{
return
true
;
}
}
return
false
;
}
public
String
next
()
throws
IOException
{
String
result
=
null
;
if
(
hasNext
())
{
result
=
next
;
next
=
null
;
}
return
result
;
}
}
}
library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java
0 → 100644
View file @
0cb81693
/*
* 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
;
import
com.google.android.exoplayer.MediaFormat
;
import
com.google.android.exoplayer.MediaFormatHolder
;
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.upstream.Loader
;
import
com.google.android.exoplayer.upstream.Loader.Loadable
;
import
com.google.android.exoplayer.util.Assertions
;
import
android.os.SystemClock
;
import
java.io.IOException
;
import
java.util.LinkedList
;
/**
* A {@link SampleSource} for HLS streams.
*/
public
class
HlsSampleSource
implements
SampleSource
,
Loader
.
Callback
{
/**
* The default minimum number of times to retry loading data prior to failing.
*/
public
static
final
int
DEFAULT_MIN_LOADABLE_RETRY_COUNT
=
1
;
private
static
final
int
NO_RESET_PENDING
=
-
1
;
private
final
HlsChunkSource
chunkSource
;
private
final
LinkedList
<
TsExtractor
>
extractors
;
private
final
boolean
frameAccurateSeeking
;
private
final
int
minLoadableRetryCount
;
private
int
remainingReleaseCount
;
private
boolean
prepared
;
private
int
trackCount
;
private
int
enabledTrackCount
;
private
boolean
[]
trackEnabledStates
;
private
boolean
[]
pendingDiscontinuities
;
private
TrackInfo
[]
trackInfos
;
private
MediaFormat
[]
downstreamMediaFormats
;
private
long
downstreamPositionUs
;
private
long
lastSeekPositionUs
;
private
long
pendingResetPositionUs
;
private
TsChunk
previousTsLoadable
;
private
HlsChunk
currentLoadable
;
private
boolean
loadingFinished
;
private
Loader
loader
;
private
IOException
currentLoadableException
;
private
boolean
currentLoadableExceptionFatal
;
private
int
currentLoadableExceptionCount
;
private
long
currentLoadableExceptionTimestamp
;
public
HlsSampleSource
(
HlsChunkSource
chunkSource
,
boolean
frameAccurateSeeking
,
int
downstreamRendererCount
)
{
this
(
chunkSource
,
frameAccurateSeeking
,
downstreamRendererCount
,
DEFAULT_MIN_LOADABLE_RETRY_COUNT
);
}
public
HlsSampleSource
(
HlsChunkSource
chunkSource
,
boolean
frameAccurateSeeking
,
int
downstreamRendererCount
,
int
minLoadableRetryCount
)
{
this
.
chunkSource
=
chunkSource
;
this
.
frameAccurateSeeking
=
frameAccurateSeeking
;
this
.
remainingReleaseCount
=
downstreamRendererCount
;
this
.
minLoadableRetryCount
=
minLoadableRetryCount
;
extractors
=
new
LinkedList
<
TsExtractor
>();
}
@Override
public
boolean
prepare
()
throws
IOException
{
if
(
prepared
)
{
return
true
;
}
if
(
loader
==
null
)
{
loader
=
new
Loader
(
"Loader:HLS"
);
}
continueBufferingInternal
();
if
(!
extractors
.
isEmpty
())
{
TsExtractor
extractor
=
extractors
.
getFirst
();
if
(
extractor
.
isPrepared
())
{
trackCount
=
extractor
.
getTrackCount
();
trackEnabledStates
=
new
boolean
[
trackCount
];
pendingDiscontinuities
=
new
boolean
[
trackCount
];
downstreamMediaFormats
=
new
MediaFormat
[
trackCount
];
trackInfos
=
new
TrackInfo
[
trackCount
];
for
(
int
i
=
0
;
i
<
trackCount
;
i
++)
{
MediaFormat
format
=
extractor
.
getFormat
(
i
);
trackInfos
[
i
]
=
new
TrackInfo
(
format
.
mimeType
,
chunkSource
.
getDurationUs
());
}
prepared
=
true
;
}
}
if
(!
prepared
)
{
maybeThrowLoadableException
();
}
return
prepared
;
}
@Override
public
int
getTrackCount
()
{
Assertions
.
checkState
(
prepared
);
return
trackCount
;
}
@Override
public
TrackInfo
getTrackInfo
(
int
track
)
{
Assertions
.
checkState
(
prepared
);
return
trackInfos
[
track
];
}
@Override
public
void
enable
(
int
track
,
long
positionUs
)
{
Assertions
.
checkState
(
prepared
);
Assertions
.
checkState
(!
trackEnabledStates
[
track
]);
enabledTrackCount
++;
trackEnabledStates
[
track
]
=
true
;
downstreamMediaFormats
[
track
]
=
null
;
if
(
enabledTrackCount
==
1
)
{
seekToUs
(
positionUs
);
}
}
@Override
public
void
disable
(
int
track
)
{
Assertions
.
checkState
(
prepared
);
Assertions
.
checkState
(
trackEnabledStates
[
track
]);
enabledTrackCount
--;
trackEnabledStates
[
track
]
=
false
;
pendingDiscontinuities
[
track
]
=
false
;
if
(
enabledTrackCount
==
0
)
{
if
(
loader
.
isLoading
())
{
loader
.
cancelLoading
();
}
else
{
clearState
();
}
}
}
@Override
public
boolean
continueBuffering
(
long
playbackPositionUs
)
throws
IOException
{
Assertions
.
checkState
(
prepared
);
Assertions
.
checkState
(
enabledTrackCount
>
0
);
downstreamPositionUs
=
playbackPositionUs
;
return
continueBufferingInternal
();
}
private
boolean
continueBufferingInternal
()
throws
IOException
{
maybeStartLoading
();
if
(
isPendingReset
()
||
extractors
.
isEmpty
())
{
return
false
;
}
boolean
haveSamples
=
extractors
.
getFirst
().
hasSamples
();
if
(!
haveSamples
)
{
maybeThrowLoadableException
();
}
return
haveSamples
;
}
@Override
public
int
readData
(
int
track
,
long
playbackPositionUs
,
MediaFormatHolder
formatHolder
,
SampleHolder
sampleHolder
,
boolean
onlyReadDiscontinuity
)
throws
IOException
{
Assertions
.
checkState
(
prepared
);
downstreamPositionUs
=
playbackPositionUs
;
if
(
pendingDiscontinuities
[
track
])
{
pendingDiscontinuities
[
track
]
=
false
;
return
DISCONTINUITY_READ
;
}
if
(
onlyReadDiscontinuity
||
isPendingReset
()
||
extractors
.
isEmpty
())
{
maybeThrowLoadableException
();
return
NOTHING_READ
;
}
TsExtractor
extractor
=
extractors
.
getFirst
();
while
(
extractors
.
size
()
>
1
&&
!
extractor
.
hasSamples
())
{
// We're finished reading from the extractor for all tracks, and so can discard it.
extractors
.
removeFirst
().
release
();
extractor
=
extractors
.
getFirst
();
}
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.
extractor
.
configureSpliceTo
(
extractors
.
get
(
1
));
}
int
extractorIndex
=
0
;
while
(
extractors
.
size
()
>
extractorIndex
+
1
&&
!
extractor
.
hasSamples
(
track
))
{
// We're finished reading from the extractor for this particular track, so advance to the
// next one for the current read.
extractor
=
extractors
.
get
(++
extractorIndex
);
}
if
(!
extractor
.
isPrepared
())
{
maybeThrowLoadableException
();
return
NOTHING_READ
;
}
MediaFormat
mediaFormat
=
extractor
.
getFormat
(
track
);
if
(
mediaFormat
!=
null
&&
!
mediaFormat
.
equals
(
downstreamMediaFormats
[
track
],
true
))
{
chunkSource
.
getMaxVideoDimensions
(
mediaFormat
);
formatHolder
.
format
=
mediaFormat
;
downstreamMediaFormats
[
track
]
=
mediaFormat
;
return
FORMAT_READ
;
}
if
(
extractor
.
getSample
(
track
,
sampleHolder
))
{
sampleHolder
.
decodeOnly
=
frameAccurateSeeking
&&
sampleHolder
.
timeUs
<
lastSeekPositionUs
;
return
SAMPLE_READ
;
}
if
(
loadingFinished
)
{
return
END_OF_STREAM
;
}
maybeThrowLoadableException
();
return
NOTHING_READ
;
}
@Override
public
void
seekToUs
(
long
positionUs
)
{
Assertions
.
checkState
(
prepared
);
Assertions
.
checkState
(
enabledTrackCount
>
0
);
lastSeekPositionUs
=
positionUs
;
if
(
pendingResetPositionUs
==
positionUs
||
downstreamPositionUs
==
positionUs
)
{
downstreamPositionUs
=
positionUs
;
return
;
}
downstreamPositionUs
=
positionUs
;
for
(
int
i
=
0
;
i
<
pendingDiscontinuities
.
length
;
i
++)
{
pendingDiscontinuities
[
i
]
=
true
;
}
restartFrom
(
positionUs
);
}
@Override
public
long
getBufferedPositionUs
()
{
Assertions
.
checkState
(
prepared
);
Assertions
.
checkState
(
enabledTrackCount
>
0
);
if
(
isPendingReset
())
{
return
pendingResetPositionUs
;
}
else
if
(
loadingFinished
)
{
return
TrackRenderer
.
END_OF_TRACK_US
;
}
else
{
long
largestSampleTimestamp
=
extractors
.
getLast
().
getLargestSampleTimestamp
();
return
largestSampleTimestamp
==
Long
.
MIN_VALUE
?
downstreamPositionUs
:
largestSampleTimestamp
;
}
}
@Override
public
void
release
()
{
Assertions
.
checkState
(
remainingReleaseCount
>
0
);
if
(--
remainingReleaseCount
==
0
&&
loader
!=
null
)
{
loader
.
release
();
loader
=
null
;
}
}
@Override
public
void
onLoadCompleted
(
Loadable
loadable
)
{
try
{
currentLoadable
.
consume
();
}
catch
(
IOException
e
)
{
currentLoadableException
=
e
;
currentLoadableExceptionCount
++;
currentLoadableExceptionTimestamp
=
SystemClock
.
elapsedRealtime
();
currentLoadableExceptionFatal
=
true
;
}
finally
{
if
(
isTsChunk
(
currentLoadable
))
{
TsChunk
tsChunk
=
(
TsChunk
)
loadable
;
loadingFinished
=
tsChunk
.
isLastChunk
;
}
if
(!
currentLoadableExceptionFatal
)
{
clearCurrentLoadable
();
}
maybeStartLoading
();
}
}
@Override
public
void
onLoadCanceled
(
Loadable
loadable
)
{
if
(
enabledTrackCount
>
0
)
{
restartFrom
(
pendingResetPositionUs
);
}
else
{
clearState
();
}
}
@Override
public
void
onLoadError
(
Loadable
loadable
,
IOException
e
)
{
currentLoadableException
=
e
;
currentLoadableExceptionCount
++;
currentLoadableExceptionTimestamp
=
SystemClock
.
elapsedRealtime
();
maybeStartLoading
();
}
private
void
maybeThrowLoadableException
()
throws
IOException
{
if
(
currentLoadableException
!=
null
&&
currentLoadableExceptionCount
>
minLoadableRetryCount
)
{
throw
currentLoadableException
;
}
}
private
void
restartFrom
(
long
positionUs
)
{
pendingResetPositionUs
=
positionUs
;
loadingFinished
=
false
;
if
(
loader
.
isLoading
())
{
loader
.
cancelLoading
();
}
else
{
clearState
();
maybeStartLoading
();
}
}
private
void
clearState
()
{
for
(
int
i
=
0
;
i
<
extractors
.
size
();
i
++)
{
extractors
.
get
(
i
).
release
();
}
extractors
.
clear
();
clearCurrentLoadable
();
previousTsLoadable
=
null
;
}
private
void
clearCurrentLoadable
()
{
currentLoadable
=
null
;
currentLoadableException
=
null
;
currentLoadableExceptionCount
=
0
;
currentLoadableExceptionFatal
=
false
;
}
private
void
maybeStartLoading
()
{
if
(
currentLoadableExceptionFatal
||
loadingFinished
||
loader
.
isLoading
())
{
return
;
}
boolean
isBackedOff
=
currentLoadableException
!=
null
;
if
(
isBackedOff
)
{
long
elapsedMillis
=
SystemClock
.
elapsedRealtime
()
-
currentLoadableExceptionTimestamp
;
if
(
elapsedMillis
>=
getRetryDelayMillis
(
currentLoadableExceptionCount
))
{
currentLoadableException
=
null
;
loader
.
startLoading
(
currentLoadable
,
this
);
}
return
;
}
HlsChunk
nextLoadable
=
chunkSource
.
getChunkOperation
(
previousTsLoadable
,
pendingResetPositionUs
,
downstreamPositionUs
);
if
(
nextLoadable
==
null
)
{
return
;
}
currentLoadable
=
nextLoadable
;
if
(
isTsChunk
(
currentLoadable
))
{
previousTsLoadable
=
(
TsChunk
)
currentLoadable
;
if
(
isPendingReset
())
{
pendingResetPositionUs
=
NO_RESET_PENDING
;
}
if
(
extractors
.
isEmpty
()
||
extractors
.
getLast
()
!=
previousTsLoadable
.
extractor
)
{
extractors
.
addLast
(
previousTsLoadable
.
extractor
);
}
}
loader
.
startLoading
(
currentLoadable
,
this
);
}
private
boolean
isTsChunk
(
HlsChunk
chunk
)
{
return
chunk
instanceof
TsChunk
;
}
private
boolean
isPendingReset
()
{
return
pendingResetPositionUs
!=
NO_RESET_PENDING
;
}
private
long
getRetryDelayMillis
(
long
errorCount
)
{
return
Math
.
min
((
errorCount
-
1
)
*
1000
,
5000
);
}
protected
final
int
usToMs
(
long
timeUs
)
{
return
(
int
)
(
timeUs
/
1000
);
}
}
library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java
0 → 100644
View file @
0cb81693
/*
* 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
;
import
com.google.android.exoplayer.upstream.DataSource
;
import
com.google.android.exoplayer.upstream.DataSpec
;
import
java.io.IOException
;
/**
* A MPEG2TS chunk.
*/
public
final
class
TsChunk
extends
HlsChunk
{
private
static
final
byte
[]
SCRATCH_SPACE
=
new
byte
[
4096
];
/**
* The index of the variant in the master playlist.
*/
public
final
int
variantIndex
;
/**
* The start time of the media contained by the chunk.
*/
public
final
long
startTimeUs
;
/**
* The end time of the media contained by the chunk.
*/
public
final
long
endTimeUs
;
/**
* The chunk index.
*/
public
final
int
chunkIndex
;
/**
* True if this is the last chunk in the media. False otherwise.
*/
public
final
boolean
isLastChunk
;
/**
* The extractor into which this chunk is being consumed.
*/
public
final
TsExtractor
extractor
;
private
int
loadPosition
;
private
volatile
boolean
loadFinished
;
private
volatile
boolean
loadCanceled
;
/**
* @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded.
* @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
tsExtractor
,
int
variantIndex
,
long
startTimeUs
,
long
endTimeUs
,
int
chunkIndex
,
boolean
isLastChunk
)
{
super
(
dataSource
,
dataSpec
);
this
.
extractor
=
tsExtractor
;
this
.
variantIndex
=
variantIndex
;
this
.
startTimeUs
=
startTimeUs
;
this
.
endTimeUs
=
endTimeUs
;
this
.
chunkIndex
=
chunkIndex
;
this
.
isLastChunk
=
isLastChunk
;
}
@Override
public
void
consume
()
throws
IOException
{
// Do nothing.
}
@Override
public
boolean
isLoadFinished
()
{
return
loadFinished
;
}
// Loadable implementation
@Override
public
void
cancelLoad
()
{
loadCanceled
=
true
;
}
@Override
public
boolean
isLoadCanceled
()
{
return
loadCanceled
;
}
@Override
public
void
load
()
throws
IOException
,
InterruptedException
{
try
{
dataSource
.
open
(
dataSpec
);
int
bytesRead
=
0
;
int
bytesSkipped
=
0
;
// If we previously fed part of this chunk to the extractor, skip it this time.
// TODO: Ideally we'd construct a dataSpec that only loads the remainder of the data here,
// rather than loading the whole chunk again and then skipping data we previously loaded. To
// do this is straightforward for non-encrypted content, but more complicated for content
// encrypted with AES, for which we'll need to modify the way that decryption is performed.
while
(
bytesRead
!=
-
1
&&
!
loadCanceled
&&
bytesSkipped
<
loadPosition
)
{
int
skipLength
=
Math
.
min
(
loadPosition
-
bytesSkipped
,
SCRATCH_SPACE
.
length
);
bytesRead
=
dataSource
.
read
(
SCRATCH_SPACE
,
0
,
skipLength
);
if
(
bytesRead
!=
-
1
)
{
bytesSkipped
+=
bytesRead
;
}
}
// Feed the remaining data into the extractor.
while
(
bytesRead
!=
-
1
&&
!
loadCanceled
)
{
bytesRead
=
extractor
.
read
(
dataSource
);
if
(
bytesRead
!=
-
1
)
{
loadPosition
+=
bytesRead
;
}
}
loadFinished
=
!
loadCanceled
;
}
finally
{
dataSource
.
close
();
}
}
}
library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java
0 → 100644
View file @
0cb81693
/*
* 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
;
import
com.google.android.exoplayer.C
;
import
com.google.android.exoplayer.MediaFormat
;
import
com.google.android.exoplayer.SampleHolder
;
import
com.google.android.exoplayer.metadata.Eia608Parser
;
import
com.google.android.exoplayer.upstream.DataSource
;
import
com.google.android.exoplayer.util.Assertions
;
import
com.google.android.exoplayer.util.BitArray
;
import
com.google.android.exoplayer.util.CodecSpecificDataUtil
;
import
com.google.android.exoplayer.util.MimeTypes
;
import
android.annotation.SuppressLint
;
import
android.media.MediaExtractor
;
import
android.util.Log
;
import
android.util.Pair
;
import
android.util.SparseArray
;
import
java.io.IOException
;
import
java.util.ArrayList
;
import
java.util.Collections
;
import
java.util.List
;
import
java.util.concurrent.ConcurrentLinkedQueue
;
/**
* Facilitates the extraction of data from the MPEG-2 TS container format.
*/
public
final
class
TsExtractor
{
private
static
final
String
TAG
=
"TsExtractor"
;
private
static
final
int
TS_PACKET_SIZE
=
188
;
private
static
final
int
TS_SYNC_BYTE
=
0x47
;
// First byte of each TS packet.
private
static
final
int
TS_PAT_PID
=
0
;
private
static
final
int
TS_STREAM_TYPE_AAC
=
0x0F
;
private
static
final
int
TS_STREAM_TYPE_H264
=
0x1B
;
private
static
final
int
TS_STREAM_TYPE_ID3
=
0x15
;
private
static
final
int
TS_STREAM_TYPE_EIA608
=
0x100
;
// 0xFF + 1
private
final
BitArray
tsPacketBuffer
;
private
final
SparseArray
<
SampleQueue
>
sampleQueues
;
// Indexed by streamType
private
final
SparseArray
<
TsPayloadReader
>
tsPayloadReaders
;
// Indexed by pid
private
final
SamplePool
samplePool
;
private
final
boolean
shouldSpliceIn
;
/* package */
final
long
firstSampleTimestamp
;
// Accessed only by the consuming thread.
private
boolean
spliceConfigured
;
// Accessed only by the loading thread.
/* package */
boolean
pendingFirstSampleTimestampAdjustment
;
/* package */
long
sampleTimestampOffsetUs
;
// Accessed by both the loading and consuming threads.
private
volatile
boolean
prepared
;
/* package */
volatile
long
largestParsedTimestampUs
;
public
TsExtractor
(
long
firstSampleTimestamp
,
SamplePool
samplePool
,
boolean
shouldSpliceIn
)
{
this
.
firstSampleTimestamp
=
firstSampleTimestamp
;
this
.
samplePool
=
samplePool
;
this
.
shouldSpliceIn
=
shouldSpliceIn
;
pendingFirstSampleTimestampAdjustment
=
true
;
tsPacketBuffer
=
new
BitArray
();
sampleQueues
=
new
SparseArray
<
SampleQueue
>();
tsPayloadReaders
=
new
SparseArray
<
TsPayloadReader
>();
tsPayloadReaders
.
put
(
TS_PAT_PID
,
new
PatReader
());
largestParsedTimestampUs
=
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
public
long
getLargestSampleTimestamp
()
{
return
largestParsedTimestampUs
;
}
/**
* Gets the next sample for the specified track.
*
* @param track The track from which to read.
* @param out A {@link SampleHolder} into which the next sample should be read.
* @return True if a sample was read. False otherwise.
*/
public
boolean
getSample
(
int
track
,
SampleHolder
out
)
{
Assertions
.
checkState
(
prepared
);
SampleQueue
sampleQueue
=
sampleQueues
.
valueAt
(
track
);
Sample
sample
=
sampleQueue
.
poll
();
if
(
sample
==
null
)
{
return
false
;
}
convert
(
sample
,
out
);
sampleQueue
.
recycle
(
sample
);
return
true
;
}
/**
* Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for any
* track.
*
* @return True if samples are available for reading from {@link #getSample(int, SampleHolder)}
* for any track. False otherwise.
*/
public
boolean
hasSamples
()
{
for
(
int
i
=
0
;
i
<
sampleQueues
.
size
();
i
++)
{
if
(
hasSamples
(
i
))
{
return
true
;
}
}
return
false
;
}
/**
* 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
boolean
hasSamples
(
int
track
)
{
return
sampleQueues
.
valueAt
(
track
).
peek
()
!=
null
;
}
private
boolean
checkPrepared
()
{
int
pesPayloadReaderCount
=
sampleQueues
.
size
();
if
(
pesPayloadReaderCount
==
0
)
{
return
false
;
}
for
(
int
i
=
0
;
i
<
pesPayloadReaderCount
;
i
++)
{
if
(!
sampleQueues
.
valueAt
(
i
).
hasMediaFormat
())
{
return
false
;
}
}
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.
*/
public
int
read
(
DataSource
dataSource
)
throws
IOException
{
int
read
=
tsPacketBuffer
.
append
(
dataSource
,
TS_PACKET_SIZE
-
tsPacketBuffer
.
bytesLeft
());
if
(
read
==
-
1
)
{
return
-
1
;
}
if
(
tsPacketBuffer
.
bytesLeft
()
!=
TS_PACKET_SIZE
)
{
return
read
;
}
// Parse TS header.
// Check sync byte.
int
syncByte
=
tsPacketBuffer
.
readUnsignedByte
();
if
(
syncByte
!=
TS_SYNC_BYTE
)
{
return
read
;
}
// Skip transportErrorIndicator.
tsPacketBuffer
.
skipBits
(
1
);
boolean
payloadUnitStartIndicator
=
tsPacketBuffer
.
readBit
();
// Skip transportPriority.
tsPacketBuffer
.
skipBits
(
1
);
int
pid
=
tsPacketBuffer
.
readBits
(
13
);
// Skip transport_scrambling_control.
tsPacketBuffer
.
skipBits
(
2
);
boolean
adaptationFieldExists
=
tsPacketBuffer
.
readBit
();
boolean
payloadExists
=
tsPacketBuffer
.
readBit
();
// Skip continuityCounter.
tsPacketBuffer
.
skipBits
(
4
);
// Read the adaptation field.
if
(
adaptationFieldExists
)
{
int
adaptationFieldLength
=
tsPacketBuffer
.
readBits
(
8
);
tsPacketBuffer
.
skipBytes
(
adaptationFieldLength
);
}
// Read Payload.
if
(
payloadExists
)
{
TsPayloadReader
payloadReader
=
tsPayloadReaders
.
get
(
pid
);
if
(
payloadReader
!=
null
)
{
payloadReader
.
read
(
tsPacketBuffer
,
payloadUnitStartIndicator
);
}
}
if
(!
prepared
)
{
prepared
=
checkPrepared
();
}
tsPacketBuffer
.
reset
();
return
read
;
}
@SuppressLint
(
"InlinedApi"
)
private
void
convert
(
Sample
in
,
SampleHolder
out
)
{
if
(
out
.
data
==
null
||
out
.
data
.
capacity
()
<
in
.
size
)
{
out
.
replaceBuffer
(
in
.
size
);
}
if
(
out
.
data
!=
null
)
{
out
.
data
.
put
(
in
.
data
,
0
,
in
.
size
);
}
out
.
size
=
in
.
size
;
out
.
flags
=
in
.
isKeyframe
?
MediaExtractor
.
SAMPLE_FLAG_SYNC
:
0
;
out
.
timeUs
=
in
.
timeUs
;
}
/**
* Parses payload data.
*/
private
abstract
static
class
TsPayloadReader
{
public
abstract
void
read
(
BitArray
tsBuffer
,
boolean
payloadUnitStartIndicator
);
}
/**
* Parses Program Association Table data.
*/
private
class
PatReader
extends
TsPayloadReader
{
@Override
public
void
read
(
BitArray
tsBuffer
,
boolean
payloadUnitStartIndicator
)
{
// Skip pointer.
if
(
payloadUnitStartIndicator
)
{
int
pointerField
=
tsBuffer
.
readBits
(
8
);
tsBuffer
.
skipBytes
(
pointerField
);
}
// Skip PAT header.
tsBuffer
.
skipBits
(
64
);
// 8+1+1+2+12+16+2+5+1+8+8
// Only read the first program and take it.
// Skip program_number.
tsBuffer
.
skipBits
(
16
+
3
);
int
pid
=
tsBuffer
.
readBits
(
13
);
// Pick the first program.
if
(
tsPayloadReaders
.
get
(
pid
)
==
null
)
{
tsPayloadReaders
.
put
(
pid
,
new
PmtReader
());
}
// Skip other programs if exist.
// Skip CRC_32.
}
}
/**
* Parses Program Map Table.
*/
private
class
PmtReader
extends
TsPayloadReader
{
@Override
public
void
read
(
BitArray
tsBuffer
,
boolean
payloadUnitStartIndicator
)
{
// Skip pointer.
if
(
payloadUnitStartIndicator
)
{
int
pointerField
=
tsBuffer
.
readBits
(
8
);
tsBuffer
.
skipBytes
(
pointerField
);
}
// Skip table_id, section_syntax_indicator, etc.
tsBuffer
.
skipBits
(
12
);
// 8+1+1+2
int
sectionLength
=
tsBuffer
.
readBits
(
12
);
// Skip the rest of the PMT header.
tsBuffer
.
skipBits
(
60
);
// 16+2+5+1+8+8+3+13+4
int
programInfoLength
=
tsBuffer
.
readBits
(
12
);
// Skip the descriptors.
tsBuffer
.
skipBytes
(
programInfoLength
);
int
entriesSize
=
sectionLength
-
9
/* size of the rest of the fields before descriptors */
-
programInfoLength
-
4
/* CRC size */
;
while
(
entriesSize
>
0
)
{
int
streamType
=
tsBuffer
.
readBits
(
8
);
tsBuffer
.
skipBits
(
3
);
int
elementaryPid
=
tsBuffer
.
readBits
(
13
);
tsBuffer
.
skipBits
(
4
);
int
esInfoLength
=
tsBuffer
.
readBits
(
12
);
// Skip the descriptors.
tsBuffer
.
skipBytes
(
esInfoLength
);
entriesSize
-=
esInfoLength
+
5
;
if
(
sampleQueues
.
get
(
streamType
)
!=
null
)
{
continue
;
}
PesPayloadReader
pesPayloadReader
=
null
;
switch
(
streamType
)
{
case
TS_STREAM_TYPE_AAC:
pesPayloadReader
=
new
AdtsReader
(
samplePool
);
break
;
case
TS_STREAM_TYPE_H264:
SeiReader
seiReader
=
new
SeiReader
(
samplePool
);
sampleQueues
.
put
(
TS_STREAM_TYPE_EIA608
,
seiReader
);
pesPayloadReader
=
new
H264Reader
(
samplePool
,
seiReader
);
break
;
case
TS_STREAM_TYPE_ID3:
pesPayloadReader
=
new
Id3Reader
(
samplePool
);
break
;
}
if
(
pesPayloadReader
!=
null
)
{
sampleQueues
.
put
(
streamType
,
pesPayloadReader
);
tsPayloadReaders
.
put
(
elementaryPid
,
new
PesReader
(
pesPayloadReader
));
}
}
// Skip CRC_32.
}
}
/**
* Parses PES packet data and extracts samples.
*/
private
class
PesReader
extends
TsPayloadReader
{
// Reusable buffer for incomplete PES data.
private
final
BitArray
pesBuffer
;
// Parses PES payload and extracts individual samples.
private
final
PesPayloadReader
pesPayloadReader
;
private
int
packetLength
;
public
PesReader
(
PesPayloadReader
pesPayloadReader
)
{
this
.
pesPayloadReader
=
pesPayloadReader
;
this
.
packetLength
=
-
1
;
pesBuffer
=
new
BitArray
();
}
@Override
public
void
read
(
BitArray
tsBuffer
,
boolean
payloadUnitStartIndicator
)
{
if
(
payloadUnitStartIndicator
&&
!
pesBuffer
.
isEmpty
())
{
// We've encountered the start of the next packet, but haven't yet read the body. Read it.
// Note that this should only happen if the packet length was unspecified.
Assertions
.
checkState
(
packetLength
==
0
);
readPacketBody
();
}
pesBuffer
.
append
(
tsBuffer
,
tsBuffer
.
bytesLeft
());
if
(
packetLength
==
-
1
&&
pesBuffer
.
bytesLeft
()
>=
6
)
{
// We haven't read the start of the packet, but have enough data to do so.
readPacketStart
();
}
if
(
packetLength
>
0
&&
pesBuffer
.
bytesLeft
()
>=
packetLength
)
{
// The packet length was specified and we now have the whole packet. Read it.
readPacketBody
();
}
}
private
void
readPacketStart
()
{
int
startCodePrefix
=
pesBuffer
.
readBits
(
24
);
if
(
startCodePrefix
!=
0x000001
)
{
Log
.
e
(
TAG
,
"Unexpected start code prefix: "
+
startCodePrefix
);
pesBuffer
.
reset
();
packetLength
=
-
1
;
}
else
{
// TODO: Read and use stream_id.
pesBuffer
.
skipBits
(
8
);
// Skip stream_id.
packetLength
=
pesBuffer
.
readBits
(
16
);
}
}
private
void
readPacketBody
()
{
// Skip some fields/flags.
// TODO: might need to use data_alignment_indicator.
pesBuffer
.
skipBits
(
8
);
// 2+2+1+1+1+1
boolean
ptsFlag
=
pesBuffer
.
readBit
();
// Skip DTS flag.
pesBuffer
.
skipBits
(
1
);
// Skip some fields/flags.
pesBuffer
.
skipBits
(
6
);
// 1+1+1+1+1+1
int
headerDataLength
=
pesBuffer
.
readBits
(
8
);
if
(
headerDataLength
==
0
)
{
headerDataLength
=
pesBuffer
.
bytesLeft
();
}
long
timeUs
=
0
;
if
(
ptsFlag
)
{
// Skip prefix.
pesBuffer
.
skipBits
(
4
);
long
pts
=
pesBuffer
.
readBitsLong
(
3
)
<<
30
;
pesBuffer
.
skipBits
(
1
);
pts
|=
pesBuffer
.
readBitsLong
(
15
)
<<
15
;
pesBuffer
.
skipBits
(
1
);
pts
|=
pesBuffer
.
readBitsLong
(
15
);
pesBuffer
.
skipBits
(
1
);
timeUs
=
(
pts
*
C
.
MICROS_PER_SECOND
)
/
90000
;
// Skip the rest of the header.
pesBuffer
.
skipBytes
(
headerDataLength
-
5
);
}
else
{
// Skip the rest of the header.
pesBuffer
.
skipBytes
(
headerDataLength
);
}
int
payloadSize
;
if
(
packetLength
==
0
)
{
// If pesPacketLength is not specified read all available data.
payloadSize
=
pesBuffer
.
bytesLeft
();
}
else
{
payloadSize
=
packetLength
-
headerDataLength
-
3
;
}
pesPayloadReader
.
read
(
pesBuffer
,
payloadSize
,
timeUs
);
pesBuffer
.
reset
();
packetLength
=
-
1
;
}
}
/**
* A queue of extracted samples together with their corresponding {@link MediaFormat}.
*/
private
abstract
class
SampleQueue
{
@SuppressWarnings
(
"hiding"
)
private
final
SamplePool
samplePool
;
private
final
ConcurrentLinkedQueue
<
Sample
>
internalQueue
;
// Accessed only by the consuming thread.
private
boolean
readFirstFrame
;
private
long
lastReadTimeUs
;
private
long
spliceOutTimeUs
;
// Accessed by both the loading and consuming threads.
private
volatile
MediaFormat
mediaFormat
;
protected
SampleQueue
(
SamplePool
samplePool
)
{
this
.
samplePool
=
samplePool
;
internalQueue
=
new
ConcurrentLinkedQueue
<
Sample
>();
spliceOutTimeUs
=
Long
.
MIN_VALUE
;
lastReadTimeUs
=
Long
.
MIN_VALUE
;
}
public
boolean
hasMediaFormat
()
{
return
mediaFormat
!=
null
;
}
public
MediaFormat
getMediaFormat
()
{
return
mediaFormat
;
}
protected
void
setMediaFormat
(
MediaFormat
mediaFormat
)
{
this
.
mediaFormat
=
mediaFormat
;
}
/**
* Removes and returns the next sample from the queue.
* <p>
* The first sample returned is guaranteed to be a keyframe, since any non-keyframe samples
* queued prior to the first keyframe are discarded.
*
* @return The next sample from the queue, or null if a sample isn't available.
*/
public
Sample
poll
()
{
Sample
head
=
peek
();
if
(
head
!=
null
)
{
internalQueue
.
remove
();
readFirstFrame
=
true
;
lastReadTimeUs
=
head
.
timeUs
;
}
return
head
;
}
/**
* Like {@link #poll()}, except the returned sample is not removed from the queue.
*
* @return The next sample from the queue, or null if a sample isn't available.
*/
public
Sample
peek
()
{
Sample
head
=
internalQueue
.
peek
();
if
(!
readFirstFrame
)
{
// Peeking discard of samples until we find a keyframe or run out of available samples.
while
(
head
!=
null
&&
!
head
.
isKeyframe
)
{
recycle
(
head
);
internalQueue
.
remove
();
head
=
internalQueue
.
peek
();
}
}
if
(
head
==
null
)
{
return
null
;
}
if
(
spliceOutTimeUs
!=
Long
.
MIN_VALUE
&&
head
.
timeUs
>=
spliceOutTimeUs
)
{
// The sample is later than the time this queue is spliced out.
recycle
(
head
);
internalQueue
.
remove
();
return
null
;
}
return
head
;
}
/**
* Clears the queue.
*/
public
void
release
()
{
Sample
toRecycle
=
internalQueue
.
poll
();
while
(
toRecycle
!=
null
)
{
recycle
(
toRecycle
);
toRecycle
=
internalQueue
.
poll
();
}
}
/**
* Recycles a sample.
*
* @param sample The sample to recycle.
*/
public
void
recycle
(
Sample
sample
)
{
samplePool
.
recycle
(
sample
);
}
/**
* Attempts to configure a splice from this queue to the next.
*
* @param nextQueue The queue being spliced to.
* @return Whether the splice was configured successfully.
*/
public
boolean
configureSpliceTo
(
SampleQueue
nextQueue
)
{
if
(
spliceOutTimeUs
!=
Long
.
MIN_VALUE
)
{
// We've already configured the splice.
return
true
;
}
long
firstPossibleSpliceTime
;
Sample
nextSample
=
internalQueue
.
peek
();
if
(
nextSample
!=
null
)
{
firstPossibleSpliceTime
=
nextSample
.
timeUs
;
}
else
{
firstPossibleSpliceTime
=
lastReadTimeUs
+
1
;
}
ConcurrentLinkedQueue
<
Sample
>
nextInternalQueue
=
nextQueue
.
internalQueue
;
Sample
nextQueueSample
=
nextInternalQueue
.
peek
();
while
(
nextQueueSample
!=
null
&&
(
nextQueueSample
.
timeUs
<
firstPossibleSpliceTime
||
!
nextQueueSample
.
isKeyframe
))
{
// Discard samples from the next queue for as long as they are before the earliest possible
// splice time, or not keyframes.
nextQueue
.
internalQueue
.
remove
();
nextQueueSample
=
nextQueue
.
internalQueue
.
peek
();
}
if
(
nextQueueSample
!=
null
)
{
// We've found a keyframe in the next queue that can serve as the splice point. Set the
// splice point now.
spliceOutTimeUs
=
nextQueueSample
.
timeUs
;
return
true
;
}
return
false
;
}
/**
* Obtains a Sample object to use.
*
* @param type The type of the sample.
* @return The sample.
*/
protected
Sample
getSample
(
int
type
)
{
return
samplePool
.
get
(
type
);
}
/**
* Creates a new Sample and adds it to the queue.
*
* @param type The type of the sample.
* @param buffer The buffer to read sample data.
* @param sampleSize The size of the sample data.
* @param sampleTimeUs The sample time stamp.
* @param isKeyframe True if the sample is a keyframe. False otherwise.
*/
protected
void
addSample
(
int
type
,
BitArray
buffer
,
int
sampleSize
,
long
sampleTimeUs
,
boolean
isKeyframe
)
{
Sample
sample
=
getSample
(
type
);
addToSample
(
sample
,
buffer
,
sampleSize
);
sample
.
isKeyframe
=
isKeyframe
;
sample
.
timeUs
=
sampleTimeUs
;
addSample
(
sample
);
}
protected
void
addSample
(
Sample
sample
)
{
adjustTimestamp
(
sample
);
largestParsedTimestampUs
=
Math
.
max
(
largestParsedTimestampUs
,
sample
.
timeUs
);
internalQueue
.
add
(
sample
);
}
protected
void
addToSample
(
Sample
sample
,
BitArray
buffer
,
int
size
)
{
if
(
sample
.
data
.
length
-
sample
.
size
<
size
)
{
sample
.
expand
(
size
-
sample
.
data
.
length
+
sample
.
size
);
}
buffer
.
readBytes
(
sample
.
data
,
sample
.
size
,
size
);
sample
.
size
+=
size
;
}
private
void
adjustTimestamp
(
Sample
sample
)
{
if
(
pendingFirstSampleTimestampAdjustment
)
{
sampleTimestampOffsetUs
=
firstSampleTimestamp
-
sample
.
timeUs
;
pendingFirstSampleTimestampAdjustment
=
false
;
}
sample
.
timeUs
+=
sampleTimestampOffsetUs
;
}
}
/**
* Extracts individual samples from continuous byte stream.
*/
private
abstract
class
PesPayloadReader
extends
SampleQueue
{
protected
PesPayloadReader
(
SamplePool
samplePool
)
{
super
(
samplePool
);
}
public
abstract
void
read
(
BitArray
pesBuffer
,
int
pesPayloadSize
,
long
pesTimeUs
);
}
/**
* Parses a continuous H264 byte stream and extracts individual frames.
*/
private
class
H264Reader
extends
PesPayloadReader
{
private
static
final
int
NAL_UNIT_TYPE_IDR
=
5
;
private
static
final
int
NAL_UNIT_TYPE_SPS
=
7
;
private
static
final
int
NAL_UNIT_TYPE_PPS
=
8
;
private
static
final
int
NAL_UNIT_TYPE_AUD
=
9
;
public
final
SeiReader
seiReader
;
// Used to store uncompleted sample data.
private
Sample
currentSample
;
public
H264Reader
(
SamplePool
samplePool
,
SeiReader
seiReader
)
{
super
(
samplePool
);
this
.
seiReader
=
seiReader
;
}
@Override
public
void
release
()
{
super
.
release
();
if
(
currentSample
!=
null
)
{
recycle
(
currentSample
);
currentSample
=
null
;
}
}
@Override
public
void
read
(
BitArray
pesBuffer
,
int
pesPayloadSize
,
long
pesTimeUs
)
{
// Read leftover frame data from previous PES packet.
pesPayloadSize
-=
readOneH264Frame
(
pesBuffer
,
true
);
if
(
pesBuffer
.
bytesLeft
()
<=
0
||
pesPayloadSize
<=
0
)
{
return
;
}
// Single PES packet should contain only one new H.264 frame.
if
(
currentSample
!=
null
)
{
if
(!
hasMediaFormat
()
&&
currentSample
.
isKeyframe
)
{
parseMediaFormat
(
currentSample
);
}
seiReader
.
read
(
currentSample
.
data
,
currentSample
.
size
,
pesTimeUs
);
addSample
(
currentSample
);
}
currentSample
=
getSample
(
Sample
.
TYPE_VIDEO
);
pesPayloadSize
-=
readOneH264Frame
(
pesBuffer
,
false
);
currentSample
.
timeUs
=
pesTimeUs
;
if
(
pesPayloadSize
>
0
)
{
Log
.
e
(
TAG
,
"PES packet contains more frame data than expected"
);
}
}
@SuppressLint
(
"InlinedApi"
)
private
int
readOneH264Frame
(
BitArray
pesBuffer
,
boolean
remainderOnly
)
{
int
offset
=
remainderOnly
?
0
:
3
;
int
audStart
=
pesBuffer
.
findNextNalUnit
(
NAL_UNIT_TYPE_AUD
,
offset
);
if
(
currentSample
!=
null
)
{
int
idrStart
=
pesBuffer
.
findNextNalUnit
(
NAL_UNIT_TYPE_IDR
,
offset
);
if
(
idrStart
<
audStart
)
{
currentSample
.
isKeyframe
=
true
;
}
addToSample
(
currentSample
,
pesBuffer
,
audStart
);
}
else
{
pesBuffer
.
skipBytes
(
audStart
);
}
return
audStart
;
}
private
void
parseMediaFormat
(
Sample
sample
)
{
BitArray
bitArray
=
new
BitArray
(
sample
.
data
,
sample
.
size
);
// Locate the SPS and PPS units.
int
spsOffset
=
bitArray
.
findNextNalUnit
(
NAL_UNIT_TYPE_SPS
,
0
);
int
ppsOffset
=
bitArray
.
findNextNalUnit
(
NAL_UNIT_TYPE_PPS
,
0
);
if
(
spsOffset
==
bitArray
.
bytesLeft
()
||
ppsOffset
==
bitArray
.
bytesLeft
())
{
return
;
}
int
spsLength
=
bitArray
.
findNextNalUnit
(-
1
,
spsOffset
+
3
)
-
spsOffset
;
int
ppsLength
=
bitArray
.
findNextNalUnit
(-
1
,
ppsOffset
+
3
)
-
ppsOffset
;
byte
[]
spsData
=
new
byte
[
spsLength
];
byte
[]
ppsData
=
new
byte
[
ppsLength
];
System
.
arraycopy
(
bitArray
.
getData
(),
spsOffset
,
spsData
,
0
,
spsLength
);
System
.
arraycopy
(
bitArray
.
getData
(),
ppsOffset
,
ppsData
,
0
,
ppsLength
);
List
<
byte
[]>
initializationData
=
new
ArrayList
<
byte
[]>();
initializationData
.
add
(
spsData
);
initializationData
.
add
(
ppsData
);
// Unescape the SPS unit.
byte
[]
unescapedSps
=
unescapeStream
(
spsData
,
0
,
spsLength
);
bitArray
.
reset
(
unescapedSps
,
unescapedSps
.
length
);
// Parse the SPS unit
// Skip the NAL header.
bitArray
.
skipBytes
(
4
);
int
profileIdc
=
bitArray
.
readBits
(
8
);
// Skip 6 constraint bits, 2 reserved bits and level_idc.
bitArray
.
skipBytes
(
2
);
// Skip seq_parameter_set_id.
bitArray
.
readUnsignedExpGolombCodedInt
();
int
chromaFormatIdc
=
1
;
// Default is 4:2:0
if
(
profileIdc
==
100
||
profileIdc
==
110
||
profileIdc
==
122
||
profileIdc
==
244
||
profileIdc
==
44
||
profileIdc
==
83
||
profileIdc
==
86
||
profileIdc
==
118
||
profileIdc
==
128
||
profileIdc
==
138
)
{
chromaFormatIdc
=
bitArray
.
readUnsignedExpGolombCodedInt
();
if
(
chromaFormatIdc
==
3
)
{
// Skip separate_colour_plane_flag
bitArray
.
skipBits
(
1
);
}
// Skip bit_depth_luma_minus8
bitArray
.
readUnsignedExpGolombCodedInt
();
// Skip bit_depth_chroma_minus8
bitArray
.
readUnsignedExpGolombCodedInt
();
// Skip qpprime_y_zero_transform_bypass_flag
bitArray
.
skipBits
(
1
);
boolean
seqScalingMatrixPresentFlag
=
bitArray
.
readBit
();
if
(
seqScalingMatrixPresentFlag
)
{
int
limit
=
(
chromaFormatIdc
!=
3
)
?
8
:
12
;
for
(
int
i
=
0
;
i
<
limit
;
i
++)
{
boolean
seqScalingListPresentFlag
=
bitArray
.
readBit
();
if
(
seqScalingListPresentFlag
)
{
skipScalingList
(
bitArray
,
i
<
6
?
16
:
64
);
}
}
}
}
// Skip log2_max_frame_num_minus4
bitArray
.
readUnsignedExpGolombCodedInt
();
long
picOrderCntType
=
bitArray
.
readUnsignedExpGolombCodedInt
();
if
(
picOrderCntType
==
0
)
{
// Skip log2_max_pic_order_cnt_lsb_minus4
bitArray
.
readUnsignedExpGolombCodedInt
();
}
else
if
(
picOrderCntType
==
1
)
{
// Skip delta_pic_order_always_zero_flag
bitArray
.
skipBits
(
1
);
// Skip offset_for_non_ref_pic
bitArray
.
readSignedExpGolombCodedInt
();
// Skip offset_for_top_to_bottom_field
bitArray
.
readSignedExpGolombCodedInt
();
long
numRefFramesInPicOrderCntCycle
=
bitArray
.
readUnsignedExpGolombCodedInt
();
for
(
int
i
=
0
;
i
<
numRefFramesInPicOrderCntCycle
;
i
++)
{
// Skip offset_for_ref_frame[i]
bitArray
.
readUnsignedExpGolombCodedInt
();
}
}
// Skip max_num_ref_frames
bitArray
.
readUnsignedExpGolombCodedInt
();
// Skip gaps_in_frame_num_value_allowed_flag
bitArray
.
skipBits
(
1
);
int
picWidthInMbs
=
bitArray
.
readUnsignedExpGolombCodedInt
()
+
1
;
int
picHeightInMapUnits
=
bitArray
.
readUnsignedExpGolombCodedInt
()
+
1
;
boolean
frameMbsOnlyFlag
=
bitArray
.
readBit
();
int
frameHeightInMbs
=
(
2
-
(
frameMbsOnlyFlag
?
1
:
0
))
*
picHeightInMapUnits
;
if
(!
frameMbsOnlyFlag
)
{
// Skip mb_adaptive_frame_field_flag
bitArray
.
skipBits
(
1
);
}
// Skip direct_8x8_inference_flag
bitArray
.
skipBits
(
1
);
int
frameWidth
=
picWidthInMbs
*
16
;
int
frameHeight
=
frameHeightInMbs
*
16
;
boolean
frameCroppingFlag
=
bitArray
.
readBit
();
if
(
frameCroppingFlag
)
{
int
frameCropLeftOffset
=
bitArray
.
readUnsignedExpGolombCodedInt
();
int
frameCropRightOffset
=
bitArray
.
readUnsignedExpGolombCodedInt
();
int
frameCropTopOffset
=
bitArray
.
readUnsignedExpGolombCodedInt
();
int
frameCropBottomOffset
=
bitArray
.
readUnsignedExpGolombCodedInt
();
int
cropUnitX
,
cropUnitY
;
if
(
chromaFormatIdc
==
0
)
{
cropUnitX
=
1
;
cropUnitY
=
2
-
(
frameMbsOnlyFlag
?
1
:
0
);
}
else
{
int
subWidthC
=
(
chromaFormatIdc
==
3
)
?
1
:
2
;
int
subHeightC
=
(
chromaFormatIdc
==
1
)
?
2
:
1
;
cropUnitX
=
subWidthC
;
cropUnitY
=
subHeightC
*
(
2
-
(
frameMbsOnlyFlag
?
1
:
0
));
}
frameWidth
-=
(
frameCropLeftOffset
+
frameCropRightOffset
)
*
cropUnitX
;
frameHeight
-=
(
frameCropTopOffset
+
frameCropBottomOffset
)
*
cropUnitY
;
}
// Set the format.
setMediaFormat
(
MediaFormat
.
createVideoFormat
(
MimeTypes
.
VIDEO_H264
,
MediaFormat
.
NO_VALUE
,
frameWidth
,
frameHeight
,
initializationData
));
}
private
void
skipScalingList
(
BitArray
bitArray
,
int
size
)
{
int
lastScale
=
8
;
int
nextScale
=
8
;
for
(
int
i
=
0
;
i
<
size
;
i
++)
{
if
(
nextScale
!=
0
)
{
int
deltaScale
=
bitArray
.
readSignedExpGolombCodedInt
();
nextScale
=
(
lastScale
+
deltaScale
+
256
)
%
256
;
}
lastScale
=
(
nextScale
==
0
)
?
lastScale
:
nextScale
;
}
}
/**
* Replaces occurrences of [0, 0, 3] with [0, 0].
* <p>
* See ISO/IEC 14496-10:2005(E) page 36 for more information.
*/
private
byte
[]
unescapeStream
(
byte
[]
data
,
int
offset
,
int
limit
)
{
int
position
=
offset
;
List
<
Integer
>
escapePositions
=
new
ArrayList
<
Integer
>();
while
(
position
<
limit
)
{
position
=
findNextUnescapeIndex
(
data
,
position
,
limit
);
if
(
position
<
limit
)
{
escapePositions
.
add
(
position
);
position
+=
3
;
}
}
int
escapeCount
=
escapePositions
.
size
();
int
escapedPosition
=
offset
;
// 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
);
int
copyLength
=
nextEscapePosition
-
escapedPosition
;
System
.
arraycopy
(
data
,
escapedPosition
,
unescapedData
,
unescapedPosition
,
copyLength
);
escapedPosition
+=
copyLength
+
3
;
unescapedPosition
+=
copyLength
+
2
;
}
int
remainingLength
=
unescapedData
.
length
-
unescapedPosition
;
System
.
arraycopy
(
data
,
escapedPosition
,
unescapedData
,
unescapedPosition
,
remainingLength
);
return
unescapedData
;
}
private
int
findNextUnescapeIndex
(
byte
[]
bytes
,
int
offset
,
int
limit
)
{
for
(
int
i
=
offset
;
i
<
limit
-
2
;
i
++)
{
if
(
bytes
[
i
]
==
0x00
&&
bytes
[
i
+
1
]
==
0x00
&&
bytes
[
i
+
2
]
==
0x03
)
{
return
i
;
}
}
return
limit
;
}
}
/**
* Parses a SEI data from H.264 frames and extracts samples with closed captions data.
*/
private
class
SeiReader
extends
SampleQueue
{
// SEI data, used for Closed Captions.
private
static
final
int
NAL_UNIT_TYPE_SEI
=
6
;
private
final
BitArray
seiBuffer
;
public
SeiReader
(
SamplePool
samplePool
)
{
super
(
samplePool
);
setMediaFormat
(
MediaFormat
.
createEia608Format
());
seiBuffer
=
new
BitArray
();
}
@SuppressLint
(
"InlinedApi"
)
public
void
read
(
byte
[]
data
,
int
size
,
long
pesTimeUs
)
{
seiBuffer
.
reset
(
data
,
size
);
while
(
seiBuffer
.
bytesLeft
()
>
0
)
{
int
seiStart
=
seiBuffer
.
findNextNalUnit
(
NAL_UNIT_TYPE_SEI
,
0
);
if
(
seiStart
==
seiBuffer
.
bytesLeft
())
{
return
;
}
seiBuffer
.
skipBytes
(
seiStart
+
4
);
int
ccDataSize
=
Eia608Parser
.
parseHeader
(
seiBuffer
);
if
(
ccDataSize
>
0
)
{
addSample
(
Sample
.
TYPE_MISC
,
seiBuffer
,
ccDataSize
,
pesTimeUs
,
true
);
}
}
}
}
/**
* Parses a continuous ADTS byte stream and extracts individual frames.
*/
private
class
AdtsReader
extends
PesPayloadReader
{
private
final
BitArray
adtsBuffer
;
private
long
timeUs
;
private
long
frameDurationUs
;
public
AdtsReader
(
SamplePool
samplePool
)
{
super
(
samplePool
);
adtsBuffer
=
new
BitArray
();
}
@Override
public
void
read
(
BitArray
pesBuffer
,
int
pesPayloadSize
,
long
pesTimeUs
)
{
boolean
needToProcessLeftOvers
=
!
adtsBuffer
.
isEmpty
();
adtsBuffer
.
append
(
pesBuffer
,
pesPayloadSize
);
// If there are leftovers from previous PES packet, process it with last calculated timeUs.
if
(
needToProcessLeftOvers
&&
!
readOneAacFrame
(
timeUs
))
{
return
;
}
int
frameIndex
=
0
;
do
{
timeUs
=
pesTimeUs
+
(
frameDurationUs
*
frameIndex
++);
}
while
(
readOneAacFrame
(
timeUs
));
}
@SuppressLint
(
"InlinedApi"
)
private
boolean
readOneAacFrame
(
long
timeUs
)
{
if
(
adtsBuffer
.
isEmpty
())
{
return
false
;
}
int
offsetToSyncWord
=
adtsBuffer
.
findNextAdtsSyncWord
();
adtsBuffer
.
skipBytes
(
offsetToSyncWord
);
int
adtsStartOffset
=
adtsBuffer
.
getByteOffset
();
if
(
adtsBuffer
.
bytesLeft
()
<
7
)
{
adtsBuffer
.
setByteOffset
(
adtsStartOffset
);
adtsBuffer
.
clearReadData
();
return
false
;
}
adtsBuffer
.
skipBits
(
15
);
boolean
hasCRC
=
!
adtsBuffer
.
readBit
();
if
(!
hasMediaFormat
())
{
int
audioObjectType
=
adtsBuffer
.
readBits
(
2
)
+
1
;
int
sampleRateIndex
=
adtsBuffer
.
readBits
(
4
);
adtsBuffer
.
skipBits
(
1
);
int
channelConfig
=
adtsBuffer
.
readBits
(
3
);
byte
[]
audioSpecificConfig
=
CodecSpecificDataUtil
.
buildAudioSpecificConfig
(
audioObjectType
,
sampleRateIndex
,
channelConfig
);
Pair
<
Integer
,
Integer
>
audioParams
=
CodecSpecificDataUtil
.
parseAudioSpecificConfig
(
audioSpecificConfig
);
MediaFormat
mediaFormat
=
MediaFormat
.
createAudioFormat
(
MimeTypes
.
AUDIO_AAC
,
MediaFormat
.
NO_VALUE
,
audioParams
.
second
,
audioParams
.
first
,
Collections
.
singletonList
(
audioSpecificConfig
));
frameDurationUs
=
(
C
.
MICROS_PER_SECOND
*
1024L
)
/
mediaFormat
.
sampleRate
;
setMediaFormat
(
mediaFormat
);
}
else
{
adtsBuffer
.
skipBits
(
10
);
}
adtsBuffer
.
skipBits
(
4
);
int
frameSize
=
adtsBuffer
.
readBits
(
13
);
adtsBuffer
.
skipBits
(
13
);
// Decrement frame size by ADTS header size and CRC.
if
(
hasCRC
)
{
// Skip CRC.
adtsBuffer
.
skipBytes
(
2
);
frameSize
-=
9
;
}
else
{
frameSize
-=
7
;
}
if
(
frameSize
>
adtsBuffer
.
bytesLeft
())
{
adtsBuffer
.
setByteOffset
(
adtsStartOffset
);
adtsBuffer
.
clearReadData
();
return
false
;
}
addSample
(
Sample
.
TYPE_AUDIO
,
adtsBuffer
,
frameSize
,
timeUs
,
true
);
return
true
;
}
@Override
public
void
release
()
{
super
.
release
();
adtsBuffer
.
reset
();
}
}
/**
* Parses ID3 data and extracts individual text information frames.
*/
private
class
Id3Reader
extends
PesPayloadReader
{
public
Id3Reader
(
SamplePool
samplePool
)
{
super
(
samplePool
);
setMediaFormat
(
MediaFormat
.
createId3Format
());
}
@SuppressLint
(
"InlinedApi"
)
@Override
public
void
read
(
BitArray
pesBuffer
,
int
pesPayloadSize
,
long
pesTimeUs
)
{
addSample
(
Sample
.
TYPE_MISC
,
pesBuffer
,
pesPayloadSize
,
pesTimeUs
,
true
);
}
}
/**
* A pool from which the extractor can obtain sample objects for internal use.
*
* TODO: Over time the average size of a sample in the video pool will become larger, as the
* proportion of samples in the pool that have at some point held a keyframe grows. Currently
* this leads to inefficient memory usage, since samples large enough to hold keyframes end up
* being used to hold non-keyframes. We need to fix this.
*/
public
static
class
SamplePool
{
private
static
final
int
[]
DEFAULT_SAMPLE_SIZES
;
static
{
DEFAULT_SAMPLE_SIZES
=
new
int
[
Sample
.
TYPE_COUNT
];
DEFAULT_SAMPLE_SIZES
[
Sample
.
TYPE_VIDEO
]
=
10
*
1024
;
DEFAULT_SAMPLE_SIZES
[
Sample
.
TYPE_AUDIO
]
=
512
;
DEFAULT_SAMPLE_SIZES
[
Sample
.
TYPE_MISC
]
=
512
;
}
private
final
Sample
[]
pools
;
public
SamplePool
()
{
pools
=
new
Sample
[
Sample
.
TYPE_COUNT
];
}
/* package */
synchronized
Sample
get
(
int
type
)
{
if
(
pools
[
type
]
==
null
)
{
return
new
Sample
(
type
,
DEFAULT_SAMPLE_SIZES
[
type
]);
}
Sample
sample
=
pools
[
type
];
pools
[
type
]
=
sample
.
nextInPool
;
sample
.
nextInPool
=
null
;
return
sample
;
}
/* package */
synchronized
void
recycle
(
Sample
sample
)
{
sample
.
reset
();
sample
.
nextInPool
=
pools
[
sample
.
type
];
pools
[
sample
.
type
]
=
sample
;
}
}
/**
* An internal variant of {@link SampleHolder} for internal pooling and buffering.
*/
private
static
class
Sample
{
public
static
final
int
TYPE_VIDEO
=
0
;
public
static
final
int
TYPE_AUDIO
=
1
;
public
static
final
int
TYPE_MISC
=
2
;
public
static
final
int
TYPE_COUNT
=
3
;
public
final
int
type
;
public
Sample
nextInPool
;
public
byte
[]
data
;
public
boolean
isKeyframe
;
public
int
size
;
public
long
timeUs
;
public
Sample
(
int
type
,
int
length
)
{
this
.
type
=
type
;
data
=
new
byte
[
length
];
}
public
void
expand
(
int
length
)
{
byte
[]
newBuffer
=
new
byte
[
data
.
length
+
length
];
System
.
arraycopy
(
data
,
0
,
newBuffer
,
0
,
size
);
data
=
newBuffer
;
}
public
void
reset
()
{
isKeyframe
=
false
;
size
=
0
;
timeUs
=
0
;
}
}
}
library/src/main/java/com/google/android/exoplayer/hls/Variant.java
0 → 100644
View file @
0cb81693
/*
* 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
;
import
java.util.Comparator
;
/**
* Variant stream reference.
*/
public
final
class
Variant
{
/**
* Sorts {@link Variant} objects in order of decreasing bandwidth.
* <p>
* When two {@link Variant}s have the same bandwidth, the one with the lowest index comes first.
*/
public
static
final
class
DecreasingBandwidthComparator
implements
Comparator
<
Variant
>
{
@Override
public
int
compare
(
Variant
a
,
Variant
b
)
{
int
bandwidthDifference
=
b
.
bandwidth
-
a
.
bandwidth
;
return
bandwidthDifference
!=
0
?
bandwidthDifference
:
a
.
index
-
b
.
index
;
}
}
public
final
int
index
;
public
final
int
bandwidth
;
public
final
String
url
;
public
final
String
[]
codecs
;
public
final
int
width
;
public
final
int
height
;
public
Variant
(
int
index
,
String
url
,
int
bandwidth
,
String
[]
codecs
,
int
width
,
int
height
)
{
this
.
index
=
index
;
this
.
bandwidth
=
bandwidth
;
this
.
url
=
url
;
this
.
codecs
=
codecs
;
this
.
width
=
width
;
this
.
height
=
height
;
}
}
library/src/main/java/com/google/android/exoplayer/metadata/ClosedCaption.java
0 → 100644
View file @
0cb81693
/*
* 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
.
metadata
;
/**
* A Closed Caption that contains textual data associated with time indices.
*/
public
final
class
ClosedCaption
{
/**
* Identifies closed captions with control characters.
*/
public
static
final
int
TYPE_CTRL
=
0
;
/**
* Identifies closed captions with textual information.
*/
public
static
final
int
TYPE_TEXT
=
1
;
/**
* The type of the closed caption data. If equals to {@link #TYPE_TEXT} the {@link #text} field
* has the textual data, if equals to {@link #TYPE_CTRL} the {@link #text} field has two control
* characters (C1, C2).
*/
public
final
int
type
;
/**
* Contains text or two control characters.
*/
public
final
String
text
;
public
ClosedCaption
(
int
type
,
String
text
)
{
this
.
type
=
type
;
this
.
text
=
text
;
}
}
library/src/main/java/com/google/android/exoplayer/metadata/Eia608Parser.java
0 → 100644
View file @
0cb81693
/*
* 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
.
metadata
;
import
com.google.android.exoplayer.util.BitArray
;
import
com.google.android.exoplayer.util.MimeTypes
;
import
java.io.IOException
;
import
java.util.ArrayList
;
import
java.util.Collections
;
import
java.util.List
;
/**
* Facilitates the extraction and parsing of EIA-608 (a.k.a. "line 21 captions" and "CEA-608")
* Closed Captions from the SEI data block from H.264.
*/
public
class
Eia608Parser
implements
MetadataParser
<
List
<
ClosedCaption
>>
{
private
static
final
int
PAYLOAD_TYPE_CC
=
4
;
private
static
final
int
COUNTRY_CODE
=
0xB5
;
private
static
final
int
PROVIDER_CODE
=
0x31
;
private
static
final
int
USER_ID
=
0x47413934
;
// "GA94"
private
static
final
int
USER_DATA_TYPE_CODE
=
0x3
;
private
static
final
int
[]
SPECIAL_CHARACTER_SET
=
new
int
[]
{
0xAE
,
// 30: 174 '®' "Registered Sign" - registered trademark symbol
0xB0
,
// 31: 176 '°' "Degree Sign"
0xBD
,
// 32: 189 '½' "Vulgar Fraction One Half" (1/2 symbol)
0xBF
,
// 33: 191 '¿' "Inverted Question Mark"
0x2122
,
// 34: "Trade Mark Sign" (tm superscript)
0xA2
,
// 35: 162 '¢' "Cent Sign"
0xA3
,
// 36: 163 '£' "Pound Sign" - pounds sterling
0x266A
,
// 37: "Eighth Note" - music note
0xE0
,
// 38: 224 'à' "Latin small letter A with grave"
0x20
,
// 39: TRANSPARENT SPACE - for now use ordinary space
0xE8
,
// 3A: 232 'è' "Latin small letter E with grave"
0xE2
,
// 3B: 226 'â' "Latin small letter A with circumflex"
0xEA
,
// 3C: 234 'ê' "Latin small letter E with circumflex"
0xEE
,
// 3D: 238 'î' "Latin small letter I with circumflex"
0xF4
,
// 3E: 244 'ô' "Latin small letter O with circumflex"
0xFB
// 3F: 251 'û' "Latin small letter U with circumflex"
};
@Override
public
boolean
canParse
(
String
mimeType
)
{
return
mimeType
.
equals
(
MimeTypes
.
APPLICATION_EIA608
);
}
@Override
public
List
<
ClosedCaption
>
parse
(
byte
[]
data
,
int
size
)
throws
IOException
{
if
(
size
<=
0
)
{
return
null
;
}
BitArray
seiBuffer
=
new
BitArray
(
data
,
size
);
seiBuffer
.
skipBits
(
3
);
// reserved + process_cc_data_flag + zero_bit
int
ccCount
=
seiBuffer
.
readBits
(
5
);
seiBuffer
.
skipBytes
(
1
);
List
<
ClosedCaption
>
captions
=
new
ArrayList
<
ClosedCaption
>();
StringBuilder
stringBuilder
=
new
StringBuilder
();
for
(
int
i
=
0
;
i
<
ccCount
;
i
++)
{
seiBuffer
.
skipBits
(
5
);
// one_bit + reserved
boolean
ccValid
=
seiBuffer
.
readBit
();
if
(!
ccValid
)
{
seiBuffer
.
skipBits
(
18
);
continue
;
}
int
ccType
=
seiBuffer
.
readBits
(
2
);
if
(
ccType
!=
0
&&
ccType
!=
1
)
{
// Not EIA-608 captions.
seiBuffer
.
skipBits
(
16
);
continue
;
}
seiBuffer
.
skipBits
(
1
);
byte
ccData1
=
(
byte
)
seiBuffer
.
readBits
(
7
);
seiBuffer
.
skipBits
(
1
);
byte
ccData2
=
(
byte
)
seiBuffer
.
readBits
(
7
);
if
((
ccData1
==
0x11
)
&&
((
ccData2
&
0x70
)
==
0x30
))
{
ccData2
&=
0xF
;
stringBuilder
.
append
((
char
)
SPECIAL_CHARACTER_SET
[
ccData2
]);
continue
;
}
// Control character.
if
(
ccData1
<
0x20
)
{
if
(
stringBuilder
.
length
()
>
0
)
{
captions
.
add
(
new
ClosedCaption
(
ClosedCaption
.
TYPE_TEXT
,
stringBuilder
.
toString
()));
stringBuilder
.
setLength
(
0
);
}
captions
.
add
(
new
ClosedCaption
(
ClosedCaption
.
TYPE_CTRL
,
new
String
(
new
char
[]{(
char
)
ccData1
,
(
char
)
ccData2
})));
continue
;
}
stringBuilder
.
append
((
char
)
ccData1
);
if
(
ccData2
!=
0
)
{
stringBuilder
.
append
((
char
)
ccData2
);
}
}
if
(
stringBuilder
.
length
()
>
0
)
{
captions
.
add
(
new
ClosedCaption
(
ClosedCaption
.
TYPE_TEXT
,
stringBuilder
.
toString
()));
}
return
Collections
.
unmodifiableList
(
captions
);
}
/**
* 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.
*
* @param seiBuffer The buffer to read from.
* @return The size of closed captions data.
*/
public
static
int
parseHeader
(
BitArray
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
.
readBits
(
16
);
if
(
providerCode
!=
PROVIDER_CODE
)
{
return
0
;
}
int
userIdentifier
=
seiBuffer
.
readBits
(
32
);
if
(
userIdentifier
!=
USER_ID
)
{
return
0
;
}
int
userDataTypeCode
=
seiBuffer
.
readUnsignedByte
();
if
(
userDataTypeCode
!=
USER_DATA_TYPE_CODE
)
{
return
0
;
}
return
payloadSize
;
}
}
library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java
0 → 100644
View file @
0cb81693
/*
* 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
.
metadata
;
import
com.google.android.exoplayer.ParserException
;
import
com.google.android.exoplayer.util.BitArray
;
import
com.google.android.exoplayer.util.MimeTypes
;
import
java.io.UnsupportedEncodingException
;
import
java.util.Collections
;
import
java.util.HashMap
;
import
java.util.Map
;
/**
* Extracts individual TXXX text frames from raw ID3 data.
*/
public
class
Id3Parser
implements
MetadataParser
<
Map
<
String
,
Object
>>
{
@Override
public
boolean
canParse
(
String
mimeType
)
{
return
mimeType
.
equals
(
MimeTypes
.
APPLICATION_ID3
);
}
@Override
public
Map
<
String
,
Object
>
parse
(
byte
[]
data
,
int
size
)
throws
UnsupportedEncodingException
,
ParserException
{
BitArray
id3Buffer
=
new
BitArray
(
data
,
size
);
int
id3Size
=
parseId3Header
(
id3Buffer
);
Map
<
String
,
Object
>
metadata
=
new
HashMap
<
String
,
Object
>();
while
(
id3Size
>
0
)
{
int
frameId0
=
id3Buffer
.
readUnsignedByte
();
int
frameId1
=
id3Buffer
.
readUnsignedByte
();
int
frameId2
=
id3Buffer
.
readUnsignedByte
();
int
frameId3
=
id3Buffer
.
readUnsignedByte
();
int
frameSize
=
id3Buffer
.
readSynchSafeInt
();
if
(
frameSize
<=
1
)
{
break
;
}
id3Buffer
.
skipBytes
(
2
);
// Skip frame flags.
// Check Frame ID == TXXX.
if
(
frameId0
==
'T'
&&
frameId1
==
'X'
&&
frameId2
==
'X'
&&
frameId3
==
'X'
)
{
int
encoding
=
id3Buffer
.
readUnsignedByte
();
String
charset
=
getCharsetName
(
encoding
);
byte
[]
frame
=
new
byte
[
frameSize
-
1
];
id3Buffer
.
readBytes
(
frame
,
0
,
frameSize
-
1
);
int
firstZeroIndex
=
indexOf
(
frame
,
0
,
(
byte
)
0
);
String
description
=
new
String
(
frame
,
0
,
firstZeroIndex
,
charset
);
int
valueStartIndex
=
indexOfNot
(
frame
,
firstZeroIndex
,
(
byte
)
0
);
int
valueEndIndex
=
indexOf
(
frame
,
valueStartIndex
,
(
byte
)
0
);
String
value
=
new
String
(
frame
,
valueStartIndex
,
valueEndIndex
-
valueStartIndex
,
charset
);
metadata
.
put
(
TxxxMetadata
.
TYPE
,
new
TxxxMetadata
(
description
,
value
));
}
else
{
String
type
=
String
.
format
(
"%c%c%c%c"
,
frameId0
,
frameId1
,
frameId2
,
frameId3
);
byte
[]
frame
=
new
byte
[
frameSize
];
id3Buffer
.
readBytes
(
frame
,
0
,
frameSize
);
metadata
.
put
(
type
,
frame
);
}
id3Size
-=
frameSize
+
10
/* header size */
;
}
return
Collections
.
unmodifiableMap
(
metadata
);
}
private
static
int
indexOf
(
byte
[]
data
,
int
fromIndex
,
byte
key
)
{
for
(
int
i
=
fromIndex
;
i
<
data
.
length
;
i
++)
{
if
(
data
[
i
]
==
key
)
{
return
i
;
}
}
return
data
.
length
;
}
private
static
int
indexOfNot
(
byte
[]
data
,
int
fromIndex
,
byte
key
)
{
for
(
int
i
=
fromIndex
;
i
<
data
.
length
;
i
++)
{
if
(
data
[
i
]
!=
key
)
{
return
i
;
}
}
return
data
.
length
;
}
/**
* Parses ID3 header.
* @param id3Buffer A {@link BitArray} with raw ID3 data.
* @return The size of data that contains ID3 frames without header and footer.
* @throws ParserException If ID3 file identifier != "ID3".
*/
private
static
int
parseId3Header
(
BitArray
id3Buffer
)
throws
ParserException
{
int
id1
=
id3Buffer
.
readUnsignedByte
();
int
id2
=
id3Buffer
.
readUnsignedByte
();
int
id3
=
id3Buffer
.
readUnsignedByte
();
if
(
id1
!=
'I'
||
id2
!=
'D'
||
id3
!=
'3'
)
{
throw
new
ParserException
(
String
.
format
(
"Unexpected ID3 file identifier, expected \"ID3\", actual \"%c%c%c\"."
,
id1
,
id2
,
id3
));
}
id3Buffer
.
skipBytes
(
2
);
// Skip version.
int
flags
=
id3Buffer
.
readUnsignedByte
();
int
id3Size
=
id3Buffer
.
readSynchSafeInt
();
// Check if extended header presents.
if
((
flags
&
0x2
)
!=
0
)
{
int
extendedHeaderSize
=
id3Buffer
.
readSynchSafeInt
();
if
(
extendedHeaderSize
>
4
)
{
id3Buffer
.
skipBytes
(
extendedHeaderSize
-
4
);
}
id3Size
-=
extendedHeaderSize
;
}
// Check if footer presents.
if
((
flags
&
0x8
)
!=
0
)
{
id3Size
-=
10
;
}
return
id3Size
;
}
/**
* Maps encoding byte from ID3v2 frame to a Charset.
* @param encodingByte The value of encoding byte from ID3v2 frame.
* @return Charset name.
*/
private
static
String
getCharsetName
(
int
encodingByte
)
{
switch
(
encodingByte
)
{
case
0
:
return
"ISO-8859-1"
;
case
1
:
return
"UTF-16"
;
case
2
:
return
"UTF-16BE"
;
case
3
:
return
"UTF-8"
;
default
:
return
"ISO-8859-1"
;
}
}
}
library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java
0 → 100644
View file @
0cb81693
/*
* 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
.
metadata
;
import
java.io.IOException
;
/**
* Parses objects of type <T> from binary data.
*
* @param <T> The type of the metadata.
*/
public
interface
MetadataParser
<
T
>
{
/**
* Checks whether the parser supports a given mime type.
*
* @param mimeType A metadata mime type.
* @return Whether the mime type is supported.
*/
public
boolean
canParse
(
String
mimeType
);
/**
* Parses metadata objects of type <T> from the provided binary data.
*
* @param data The raw binary data from which to parse the metadata.
* @param size The size of the input data.
* @return @return A parsed metadata object of type <T>.
* @throws IOException If a problem occurred parsing the data.
*/
public
T
parse
(
byte
[]
data
,
int
size
)
throws
IOException
;
}
library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java
0 → 100644
View file @
0cb81693
/*
* 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
.
metadata
;
import
com.google.android.exoplayer.ExoPlaybackException
;
import
com.google.android.exoplayer.MediaFormatHolder
;
import
com.google.android.exoplayer.SampleHolder
;
import
com.google.android.exoplayer.SampleSource
;
import
com.google.android.exoplayer.TrackRenderer
;
import
com.google.android.exoplayer.util.Assertions
;
import
android.os.Handler
;
import
android.os.Handler.Callback
;
import
android.os.Looper
;
import
android.os.Message
;
import
java.io.IOException
;
/**
* A {@link TrackRenderer} for metadata embedded in a media stream.
*
* @param <T> The type of the metadata.
*/
public
class
MetadataTrackRenderer
<
T
>
extends
TrackRenderer
implements
Callback
{
/**
* An interface for components that process metadata.
*
* @param <T> The type of the metadata.
*/
public
interface
MetadataRenderer
<
T
>
{
/**
* Invoked each time there is a metadata associated with current playback time.
*
* @param metadata The metadata to process.
*/
void
onMetadata
(
T
metadata
);
}
private
static
final
int
MSG_INVOKE_RENDERER
=
0
;
private
final
SampleSource
source
;
private
final
MetadataParser
<
T
>
metadataParser
;
private
final
MetadataRenderer
<
T
>
metadataRenderer
;
private
final
Handler
metadataHandler
;
private
final
MediaFormatHolder
formatHolder
;
private
final
SampleHolder
sampleHolder
;
private
int
trackIndex
;
private
long
currentPositionUs
;
private
boolean
inputStreamEnded
;
private
long
pendingMetadataTimestamp
;
private
T
pendingMetadata
;
/**
* @param source A source from which samples containing metadata can be read.
* @param metadataParser A parser for parsing the metadata.
* @param metadataRenderer The metadata renderer to receive the parsed metadata.
* @param metadataRendererLooper The looper associated with the thread on which metadataRenderer
* should be invoked. If the renderer makes use of standard Android UI components, then this
* should normally be the looper associated with the applications' main thread, which can be
* obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the
* renderer should be invoked directly on the player's internal rendering thread.
*/
public
MetadataTrackRenderer
(
SampleSource
source
,
MetadataParser
<
T
>
metadataParser
,
MetadataRenderer
<
T
>
metadataRenderer
,
Looper
metadataRendererLooper
)
{
this
.
source
=
Assertions
.
checkNotNull
(
source
);
this
.
metadataParser
=
Assertions
.
checkNotNull
(
metadataParser
);
this
.
metadataRenderer
=
Assertions
.
checkNotNull
(
metadataRenderer
);
this
.
metadataHandler
=
metadataRendererLooper
==
null
?
null
:
new
Handler
(
metadataRendererLooper
,
this
);
formatHolder
=
new
MediaFormatHolder
();
sampleHolder
=
new
SampleHolder
(
SampleHolder
.
BUFFER_REPLACEMENT_MODE_NORMAL
);
}
@Override
protected
int
doPrepare
()
throws
ExoPlaybackException
{
try
{
boolean
sourcePrepared
=
source
.
prepare
();
if
(!
sourcePrepared
)
{
return
TrackRenderer
.
STATE_UNPREPARED
;
}
}
catch
(
IOException
e
)
{
throw
new
ExoPlaybackException
(
e
);
}
for
(
int
i
=
0
;
i
<
source
.
getTrackCount
();
i
++)
{
if
(
metadataParser
.
canParse
(
source
.
getTrackInfo
(
i
).
mimeType
))
{
trackIndex
=
i
;
return
TrackRenderer
.
STATE_PREPARED
;
}
}
return
TrackRenderer
.
STATE_IGNORE
;
}
@Override
protected
void
onEnabled
(
long
positionUs
,
boolean
joining
)
{
source
.
enable
(
trackIndex
,
positionUs
);
seekToInternal
(
positionUs
);
}
@Override
protected
void
seekTo
(
long
positionUs
)
throws
ExoPlaybackException
{
source
.
seekToUs
(
positionUs
);
seekToInternal
(
positionUs
);
}
private
void
seekToInternal
(
long
positionUs
)
{
currentPositionUs
=
positionUs
;
pendingMetadata
=
null
;
inputStreamEnded
=
false
;
}
@Override
protected
void
doSomeWork
(
long
positionUs
,
long
elapsedRealtimeUs
)
throws
ExoPlaybackException
{
currentPositionUs
=
positionUs
;
try
{
source
.
continueBuffering
(
positionUs
);
}
catch
(
IOException
e
)
{
throw
new
ExoPlaybackException
(
e
);
}
if
(!
inputStreamEnded
&&
pendingMetadata
==
null
)
{
try
{
int
result
=
source
.
readData
(
trackIndex
,
positionUs
,
formatHolder
,
sampleHolder
,
false
);
if
(
result
==
SampleSource
.
SAMPLE_READ
)
{
pendingMetadataTimestamp
=
sampleHolder
.
timeUs
;
pendingMetadata
=
metadataParser
.
parse
(
sampleHolder
.
data
.
array
(),
sampleHolder
.
size
);
sampleHolder
.
data
.
clear
();
}
else
if
(
result
==
SampleSource
.
END_OF_STREAM
)
{
inputStreamEnded
=
true
;
}
}
catch
(
IOException
e
)
{
throw
new
ExoPlaybackException
(
e
);
}
}
if
(
pendingMetadata
!=
null
&&
pendingMetadataTimestamp
<=
currentPositionUs
)
{
invokeRenderer
(
pendingMetadata
);
pendingMetadata
=
null
;
}
}
@Override
protected
void
onDisabled
()
{
pendingMetadata
=
null
;
source
.
disable
(
trackIndex
);
}
@Override
protected
long
getDurationUs
()
{
return
source
.
getTrackInfo
(
trackIndex
).
durationUs
;
}
@Override
protected
long
getCurrentPositionUs
()
{
return
currentPositionUs
;
}
@Override
protected
long
getBufferedPositionUs
()
{
return
TrackRenderer
.
END_OF_TRACK_US
;
}
@Override
protected
boolean
isEnded
()
{
return
inputStreamEnded
;
}
@Override
protected
boolean
isReady
()
{
return
true
;
}
private
void
invokeRenderer
(
T
metadata
)
{
if
(
metadataHandler
!=
null
)
{
metadataHandler
.
obtainMessage
(
MSG_INVOKE_RENDERER
,
metadata
).
sendToTarget
();
}
else
{
invokeRendererInternal
(
metadata
);
}
}
@SuppressWarnings
(
"unchecked"
)
@Override
public
boolean
handleMessage
(
Message
msg
)
{
switch
(
msg
.
what
)
{
case
MSG_INVOKE_RENDERER:
invokeRendererInternal
((
T
)
msg
.
obj
);
return
true
;
}
return
false
;
}
private
void
invokeRendererInternal
(
T
metadata
)
{
metadataRenderer
.
onMetadata
(
metadata
);
}
}
library/src/main/java/com/google/android/exoplayer/metadata/TxxxMetadata.java
0 → 100644
View file @
0cb81693
/*
* 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
.
metadata
;
/**
* A metadata that contains parsed ID3 TXXX (User defined text information) frame data associated
* with time indices.
*/
public
class
TxxxMetadata
{
public
static
final
String
TYPE
=
"TXXX"
;
public
final
String
description
;
public
final
String
value
;
public
TxxxMetadata
(
String
description
,
String
value
)
{
this
.
description
=
description
;
this
.
value
=
value
;
}
}
library/src/main/java/com/google/android/exoplayer/upstream/Aes128DataSource.java
0 → 100644
View file @
0cb81693
/*
* 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
.
upstream
;
import
com.google.android.exoplayer.C
;
import
com.google.android.exoplayer.util.Assertions
;
import
java.io.IOException
;
import
java.security.InvalidAlgorithmParameterException
;
import
java.security.InvalidKeyException
;
import
java.security.Key
;
import
java.security.NoSuchAlgorithmException
;
import
java.security.spec.AlgorithmParameterSpec
;
import
javax.crypto.Cipher
;
import
javax.crypto.CipherInputStream
;
import
javax.crypto.NoSuchPaddingException
;
import
javax.crypto.spec.IvParameterSpec
;
import
javax.crypto.spec.SecretKeySpec
;
/**
* A {@link DataSource} that decrypts the data read from an upstream source, encrypted with AES-128
* with a 128-bit key and PKCS7 padding.
*
*/
public
class
Aes128DataSource
implements
DataSource
{
private
final
DataSource
upstream
;
private
final
byte
[]
secretKey
;
private
final
byte
[]
iv
;
private
CipherInputStream
cipherInputStream
;
public
Aes128DataSource
(
byte
[]
secretKey
,
byte
[]
iv
,
DataSource
upstream
)
{
this
.
upstream
=
upstream
;
this
.
secretKey
=
secretKey
;
this
.
iv
=
iv
;
}
@Override
public
long
open
(
DataSpec
dataSpec
)
throws
IOException
{
Cipher
cipher
;
try
{
cipher
=
Cipher
.
getInstance
(
"AES/CBC/PKCS7Padding"
);
}
catch
(
NoSuchAlgorithmException
e
)
{
throw
new
RuntimeException
(
e
);
}
catch
(
NoSuchPaddingException
e
)
{
throw
new
RuntimeException
(
e
);
}
Key
cipherKey
=
new
SecretKeySpec
(
secretKey
,
"AES"
);
AlgorithmParameterSpec
cipherIV
=
new
IvParameterSpec
(
iv
);
try
{
cipher
.
init
(
Cipher
.
DECRYPT_MODE
,
cipherKey
,
cipherIV
);
}
catch
(
InvalidKeyException
e
)
{
throw
new
RuntimeException
(
e
);
}
catch
(
InvalidAlgorithmParameterException
e
)
{
throw
new
RuntimeException
(
e
);
}
cipherInputStream
=
new
CipherInputStream
(
new
DataSourceInputStream
(
upstream
,
dataSpec
),
cipher
);
return
C
.
LENGTH_UNBOUNDED
;
}
@Override
public
void
close
()
throws
IOException
{
cipherInputStream
=
null
;
upstream
.
close
();
}
@Override
public
int
read
(
byte
[]
buffer
,
int
offset
,
int
readLength
)
throws
IOException
{
Assertions
.
checkState
(
cipherInputStream
!=
null
);
int
bytesRead
=
cipherInputStream
.
read
(
buffer
,
offset
,
readLength
);
if
(
bytesRead
<
0
)
{
return
-
1
;
}
return
bytesRead
;
}
}
library/src/main/java/com/google/android/exoplayer/util/BitArray.java
0 → 100644
View file @
0cb81693
/*
* 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
.
util
;
import
com.google.android.exoplayer.upstream.DataSource
;
import
java.io.IOException
;
/**
* Wraps a byte array, providing methods that allow it to be read as a bitstream.
*/
public
final
class
BitArray
{
private
byte
[]
data
;
// The length of the valid data.
private
int
limit
;
// The offset within the data, stored as the current byte offset, and the bit offset within that
// byte (from 0 to 7).
private
int
byteOffset
;
private
int
bitOffset
;
public
BitArray
()
{
}
public
BitArray
(
byte
[]
data
,
int
limit
)
{
this
.
data
=
data
;
this
.
limit
=
limit
;
}
/**
* Clears all data, setting the offset and limit to zero.
*/
public
void
reset
()
{
byteOffset
=
0
;
bitOffset
=
0
;
limit
=
0
;
}
/**
* Resets to wrap the specified data, setting the offset to zero.
*
* @param data The data to wrap.
* @param limit The limit to set.
*/
public
void
reset
(
byte
[]
data
,
int
limit
)
{
this
.
data
=
data
;
this
.
limit
=
limit
;
byteOffset
=
0
;
bitOffset
=
0
;
}
/**
* Gets the backing byte array.
*
* @return The backing byte array.
*/
public
byte
[]
getData
()
{
return
data
;
}
/**
* Gets the current byte offset.
*
* @return The current byte offset.
*/
public
int
getByteOffset
()
{
return
byteOffset
;
}
/**
* Sets the current byte offset.
*
* @param byteOffset The byte offset to set.
*/
public
void
setByteOffset
(
int
byteOffset
)
{
this
.
byteOffset
=
byteOffset
;
}
/**
* Appends data from a {@link DataSource}.
*
* @param dataSource The {@link DataSource} from which to read.
* @param length The maximum number of bytes to read and append.
* @return The number of bytes that were read and appended, or -1 if no more data is available.
* @throws IOException If an error occurs reading from the source.
*/
public
int
append
(
DataSource
dataSource
,
int
length
)
throws
IOException
{
expand
(
length
);
int
bytesRead
=
dataSource
.
read
(
data
,
limit
,
length
);
if
(
bytesRead
==
-
1
)
{
return
-
1
;
}
limit
+=
bytesRead
;
return
bytesRead
;
}
/**
* Appends data from another {@link BitArray}.
*
* @param bitsArray The {@link BitArray} whose data should be appended.
* @param length The number of bytes to read and append.
*/
public
void
append
(
BitArray
bitsArray
,
int
length
)
{
expand
(
length
);
bitsArray
.
readBytes
(
data
,
limit
,
length
);
limit
+=
length
;
}
private
void
expand
(
int
length
)
{
if
(
data
==
null
)
{
data
=
new
byte
[
length
];
return
;
}
if
(
data
.
length
-
limit
<
length
)
{
byte
[]
newBuffer
=
new
byte
[
limit
+
length
];
System
.
arraycopy
(
data
,
0
,
newBuffer
,
0
,
limit
);
data
=
newBuffer
;
}
}
/**
* Clears data that has already been read, moving the remaining data to the start of the buffer.
*/
public
void
clearReadData
()
{
System
.
arraycopy
(
data
,
byteOffset
,
data
,
0
,
limit
-
byteOffset
);
limit
-=
byteOffset
;
byteOffset
=
0
;
}
/**
* Reads a single unsigned byte.
*
* @return The value of the parsed byte.
*/
public
int
readUnsignedByte
()
{
byte
b
;
if
(
bitOffset
!=
0
)
{
b
=
(
byte
)
((
data
[
byteOffset
]
<<
bitOffset
)
|
(
data
[
byteOffset
+
1
]
>>
(
8
-
bitOffset
)));
}
else
{
b
=
data
[
byteOffset
];
}
byteOffset
++;
// Converting a signed byte into unsigned.
return
b
&
0xFF
;
}
/**
* Reads a single bit.
*
* @return True if the bit is set. False otherwise.
*/
public
boolean
readBit
()
{
return
readBits
(
1
)
==
1
;
}
/**
* Reads up to 32 bits.
*
* @param n The number of bits to read.
* @return An integer whose bottom n bits hold the read data.
*/
public
int
readBits
(
int
n
)
{
return
(
int
)
readBitsLong
(
n
);
}
/**
* Reads up to 64 bits.
*
* @param n The number of bits to read.
* @return A long whose bottom n bits hold the read data.
*/
public
long
readBitsLong
(
int
n
)
{
if
(
n
==
0
)
{
return
0
;
}
long
retval
=
0
;
// While n >= 8, read whole bytes.
while
(
n
>=
8
)
{
n
-=
8
;
retval
|=
(
readUnsignedByte
()
<<
n
);
}
if
(
n
>
0
)
{
int
nextBit
=
bitOffset
+
n
;
byte
writeMask
=
(
byte
)
(
0xFF
>>
(
8
-
n
));
if
(
nextBit
>
8
)
{
// Combine bits from current byte and next byte.
retval
|=
(((
getUnsignedByte
(
byteOffset
)
<<
(
nextBit
-
8
)
|
(
getUnsignedByte
(
byteOffset
+
1
)
>>
(
16
-
nextBit
)))
&
writeMask
));
byteOffset
++;
}
else
{
// Bits to be read only within current byte.
retval
|=
((
getUnsignedByte
(
byteOffset
)
>>
(
8
-
nextBit
))
&
writeMask
);
if
(
nextBit
==
8
)
{
byteOffset
++;
}
}
bitOffset
=
nextBit
%
8
;
}
return
retval
;
}
private
int
getUnsignedByte
(
int
offset
)
{
return
data
[
offset
]
&
0xFF
;
}
/**
* Skips bits and moves current reading position forward.
*
* @param n The number of bits to skip.
*/
public
void
skipBits
(
int
n
)
{
byteOffset
+=
(
n
/
8
);
bitOffset
+=
(
n
%
8
);
if
(
bitOffset
>
7
)
{
byteOffset
++;
bitOffset
-=
8
;
}
}
/**
* Skips bytes and moves current reading position forward.
*
* @param n The number of bytes to skip.
*/
public
void
skipBytes
(
int
n
)
{
byteOffset
+=
n
;
}
/**
* Reads multiple bytes and copies them into provided byte array.
* <p>
* The read position must be at a whole byte boundary for this method to be called.
*
* @param out The byte array to copy read data.
* @param offset The offset in the out byte array.
* @param length The length of the data to read
* @throws IllegalStateException If the method is called with the read position not at a whole
* byte boundary.
*/
public
void
readBytes
(
byte
[]
out
,
int
offset
,
int
length
)
{
Assertions
.
checkState
(
bitOffset
==
0
);
System
.
arraycopy
(
data
,
byteOffset
,
out
,
offset
,
length
);
byteOffset
+=
length
;
}
/**
* @return The number of whole bytes that are available to read.
*/
public
int
bytesLeft
()
{
return
limit
-
byteOffset
;
}
/**
* @return Whether or not there is any data available.
*/
public
boolean
isEmpty
()
{
return
limit
==
0
;
}
/**
* Reads an unsigned Exp-Golomb-coded format integer.
*
* @return The value of the parsed Exp-Golomb-coded integer.
*/
public
int
readUnsignedExpGolombCodedInt
()
{
return
readExpGolombCodeNum
();
}
/**
* Reads an signed Exp-Golomb-coded format integer.
*
* @return The value of the parsed Exp-Golomb-coded integer.
*/
public
int
readSignedExpGolombCodedInt
()
{
int
codeNum
=
readExpGolombCodeNum
();
return
((
codeNum
%
2
)
==
0
?
-
1
:
1
)
*
((
codeNum
+
1
)
/
2
);
}
private
int
readExpGolombCodeNum
()
{
int
leadingZeros
=
0
;
while
(!
readBit
())
{
leadingZeros
++;
}
return
(
1
<<
leadingZeros
)
-
1
+
(
leadingZeros
>
0
?
readBits
(
leadingZeros
)
:
0
);
}
/**
* Reads a Synchsafe integer.
* Synchsafe integers are integers that keep the highest bit of every byte zeroed.
* A 32 bit synchsafe integer can store 28 bits of information.
*
* @return The value of the parsed Synchsafe integer.
*/
public
int
readSynchSafeInt
()
{
int
b1
=
readUnsignedByte
();
int
b2
=
readUnsignedByte
();
int
b3
=
readUnsignedByte
();
int
b4
=
readUnsignedByte
();
return
(
b1
<<
21
)
|
(
b2
<<
14
)
|
(
b3
<<
7
)
|
b4
;
}
// TODO: Find a better place for this method.
/**
* Finds the next Adts sync word.
*
* @return The offset from the current position to the start of the next Adts sync word. If an
* Adts sync word is not found, then the offset to the end of the data is returned.
*/
public
int
findNextAdtsSyncWord
()
{
for
(
int
i
=
byteOffset
;
i
<
limit
-
1
;
i
++)
{
int
syncBits
=
(
getUnsignedByte
(
i
)
<<
8
)
|
getUnsignedByte
(
i
+
1
);
if
((
syncBits
&
0xFFF0
)
==
0xFFF0
&&
syncBits
!=
0xFFFF
)
{
return
i
-
byteOffset
;
}
}
return
limit
-
byteOffset
;
}
//TODO: Find a better place for this method.
/**
* Finds the next NAL unit.
*
* @param nalUnitType The type of the NAL unit to search for, or -1 for any NAL unit.
* @param offset The additional offset in the data to start the search from.
* @return The offset from the current position to the start of the NAL unit. If a NAL unit is
* not found, then the offset to the end of the data is returned.
*/
public
int
findNextNalUnit
(
int
nalUnitType
,
int
offset
)
{
for
(
int
i
=
byteOffset
+
offset
;
i
<
limit
-
3
;
i
++)
{
// Check for NAL unit start code prefix == 0x000001.
if
((
data
[
i
]
==
0
&&
data
[
i
+
1
]
==
0
&&
data
[
i
+
2
]
==
1
)
&&
(
nalUnitType
==
-
1
||
(
nalUnitType
==
(
data
[
i
+
3
]
&
0x1F
))))
{
return
i
-
byteOffset
;
}
}
return
limit
-
byteOffset
;
}
}
library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java
View file @
0cb81693
...
@@ -39,6 +39,8 @@ public class MimeTypes {
...
@@ -39,6 +39,8 @@ public class MimeTypes {
public
static
final
String
TEXT_VTT
=
BASE_TYPE_TEXT
+
"/vtt"
;
public
static
final
String
TEXT_VTT
=
BASE_TYPE_TEXT
+
"/vtt"
;
public
static
final
String
APPLICATION_ID3
=
BASE_TYPE_APPLICATION
+
"/id3"
;
public
static
final
String
APPLICATION_EIA608
=
BASE_TYPE_APPLICATION
+
"/eia-608"
;
public
static
final
String
APPLICATION_TTML
=
BASE_TYPE_APPLICATION
+
"/ttml+xml"
;
public
static
final
String
APPLICATION_TTML
=
BASE_TYPE_APPLICATION
+
"/ttml+xml"
;
private
MimeTypes
()
{}
private
MimeTypes
()
{}
...
...
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