Commit f94218a7 by ojw28 Committed by GitHub

Merge pull request #1905 from google/dev-v2

r2.0.2
parents f8a8302f 6c12ec62
Showing with 693 additions and 246 deletions
......@@ -20,8 +20,6 @@ and extend, and can be updated through Play Store application updates.
## Using ExoPlayer ##
#### Via jCenter ####
The easiest way to get started using ExoPlayer is to add it as a gradle
dependency. You need to make sure you have the jcenter repository included in
the `build.gradle` file in the root of your project:
......@@ -44,31 +42,6 @@ project's [Releases][]. For more details, see the project on [Bintray][].
[Releases]: https://github.com/google/ExoPlayer/releases
[Bintray]: https://bintray.com/google/exoplayer/exoplayer/view
#### As source ####
ExoPlayer can also be built from source using Gradle. You can include it as a
dependent project like so:
```gradle
// settings.gradle
include ':app', ':..:ExoPlayer:library'
// app/build.gradle
dependencies {
compile project(':..:ExoPlayer:library')
}
```
#### As a jar ####
If you want to use ExoPlayer as a jar, run:
```sh
./gradlew jarRelease
```
and copy `library.jar` to the libs folder of your new project.
## Developing ExoPlayer ##
#### Project branches ####
......
# Release notes #
### r2.0.2 ###
* Fixes for MergingMediaSource and sideloaded subtitles.
([#1882](https://github.com/google/ExoPlayer/issues/1882),
[#1854](https://github.com/google/ExoPlayer/issues/1854),
[#1900](https://github.com/google/ExoPlayer/issues/1900)).
* Reduced effect of application code leaking player references
([#1855](https://github.com/google/ExoPlayer/issues/1855)).
* Initial support for fragmented MP4 in HLS.
* Misc bug fixes and minor features.
### r2.0.1 ###
* Fix playback of short duration content
......
......@@ -16,7 +16,6 @@
buildscript {
repositories {
mavenCentral()
jcenter()
}
dependencies {
......@@ -27,11 +26,16 @@ buildscript {
allprojects {
repositories {
mavenCentral()
jcenter()
}
project.ext {
compileSdkVersion=24
targetSdkVersion=24
buildToolsVersion='23.0.3'
releaseRepoName = 'exoplayer'
releaseUserOrg = 'google'
releaseGroupId = 'com.google.android.exoplayer'
releaseVersion = 'r2.0.2'
releaseWebsite = 'https://github.com/google/ExoPlayer'
}
}
......@@ -16,8 +16,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.demo"
android:versionCode="2001"
android:versionName="2.0.1">
android:versionCode="2002"
android:versionName="2.0.2">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
......
......@@ -303,10 +303,14 @@
"uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8"
},
{
"name": "Apple master playlist advanced",
"name": "Apple master playlist advanced (TS)",
"uri": "https://tungsten.aaplimg.com/VOD/bipbop_adv_example_v2/master.m3u8"
},
{
"name": "Apple master playlist advanced (fMP4)",
"uri": "https://tungsten.aaplimg.com/VOD/bipbop_adv_fmp4_example/master.m3u8"
},
{
"name": "Apple TS media playlist",
"uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8"
},
......
......@@ -2,8 +2,29 @@
## Description ##
The OkHttp Extension is an [HttpDataSource][] implementation using Square's [OkHttp][].
The OkHttp Extension is an [HttpDataSource][] implementation using Square's
[OkHttp][].
## Using the extension ##
The easiest way to use the extension is to add it as a gradle dependency. You
need to make sure you have the jcenter repository included in the `build.gradle`
file in the root of your project:
```gradle
repositories {
jcenter()
}
```
Next, include the following in your module's `build.gradle` file:
```gradle
compile 'com.google.android.exoplayer:extension-okhttp:rX.X.X'
```
where `rX.X.X` is the version, which must match the version of the ExoPlayer
library being used.
[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
[OkHttp]: https://square.github.io/okhttp/
......@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
apply plugin: 'com.android.library'
apply plugin: 'bintray-release'
android {
compileSdkVersion project.ext.compileSdkVersion
......@@ -40,3 +41,13 @@ dependencies {
exclude group: 'org.json'
}
}
publish {
artifactId = 'extension-okhttp'
description = 'An OkHttp extension for ExoPlayer.'
repoName = releaseRepoName
userOrg = releaseUserOrg
groupId = releaseGroupId
version = releaseVersion
website = releaseWebsite
}
......@@ -338,17 +338,21 @@ public class OkHttpDataSource implements HttpDataSource {
* @throws IOException If an error occurs reading from the source.
*/
private int readInternal(byte[] buffer, int offset, int readLength) throws IOException {
readLength = bytesToRead == C.LENGTH_UNSET ? readLength
: (int) Math.min(readLength, bytesToRead - bytesRead);
if (readLength == 0) {
// We've read all of the requested data.
return C.RESULT_END_OF_INPUT;
return 0;
}
if (bytesToRead != C.LENGTH_UNSET) {
long bytesRemaining = bytesToRead - bytesRead;
if (bytesRemaining == 0) {
return C.RESULT_END_OF_INPUT;
}
readLength = (int) Math.min(readLength, bytesRemaining);
}
int read = responseByteStream.read(buffer, offset, readLength);
if (read == -1) {
if (bytesToRead != C.LENGTH_UNSET && bytesToRead != bytesRead) {
// The server closed the connection having not sent sufficient data.
if (bytesToRead != C.LENGTH_UNSET) {
// End of stream reached having not read sufficient data.
throw new EOFException();
}
return C.RESULT_END_OF_INPUT;
......
......@@ -92,11 +92,11 @@ android.libraryVariants.all { variant ->
}
publish {
repoName = 'exoplayer'
userOrg = 'google'
groupId = 'com.google.android.exoplayer'
artifactId = 'exoplayer'
version = 'r2.0.1'
description = 'The ExoPlayer library.'
website = 'https://github.com/google/ExoPlayer'
repoName = releaseRepoName
userOrg = releaseUserOrg
groupId = releaseGroupId
version = releaseVersion
website = releaseWebsite
}
......@@ -20,6 +20,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
......
......@@ -68,7 +68,7 @@ public final class DefaultLoadControl implements LoadControl {
* Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class.
*/
public DefaultLoadControl() {
this(new DefaultAllocator(C.DEFAULT_BUFFER_SEGMENT_SIZE));
this(new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE));
}
/**
......@@ -105,6 +105,11 @@ public final class DefaultLoadControl implements LoadControl {
}
@Override
public void onPrepared() {
reset(false);
}
@Override
public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,
TrackSelections<?> trackSelections) {
targetBufferSize = 0;
......@@ -117,9 +122,13 @@ public final class DefaultLoadControl implements LoadControl {
}
@Override
public void onTracksDisabled() {
targetBufferSize = 0;
isBuffering = false;
public void onStopped() {
reset(true);
}
@Override
public void onReleased() {
reset(true);
}
@Override
......@@ -147,4 +156,12 @@ public final class DefaultLoadControl implements LoadControl {
: (bufferedDurationUs < minBufferUs ? BELOW_LOW_WATERMARK : BETWEEN_WATERMARKS);
}
private void reset(boolean resetAllocator) {
targetBufferSize = 0;
isBuffering = false;
if (resetAllocator) {
allocator.reset();
}
}
}
......@@ -106,7 +106,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public void prepare(MediaSource mediaSource, boolean resetPosition) {
timeline = null;
internalPlayer.setMediaSource(mediaSource, resetPosition);
internalPlayer.prepare(mediaSource, resetPosition);
}
@Override
......
......@@ -75,7 +75,7 @@ import java.io.IOException;
public static final int MSG_ERROR = 6;
// Internal messages
private static final int MSG_SET_MEDIA_SOURCE = 0;
private static final int MSG_PREPARE = 0;
private static final int MSG_SET_PLAY_WHEN_READY = 1;
private static final int MSG_DO_SOME_WORK = 2;
private static final int MSG_SEEK_TO = 3;
......@@ -164,8 +164,8 @@ import java.io.IOException;
handler = new Handler(internalPlaybackThread.getLooper(), this);
}
public void setMediaSource(MediaSource mediaSource, boolean resetPosition) {
handler.obtainMessage(MSG_SET_MEDIA_SOURCE, resetPosition ? 1 : 0, 0, mediaSource)
public void prepare(MediaSource mediaSource, boolean resetPosition) {
handler.obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, 0, mediaSource)
.sendToTarget();
}
......@@ -253,8 +253,8 @@ import java.io.IOException;
public boolean handleMessage(Message msg) {
try {
switch (msg.what) {
case MSG_SET_MEDIA_SOURCE: {
setMediaSourceInternal((MediaSource) msg.obj, msg.arg1 != 0);
case MSG_PREPARE: {
prepareInternal((MediaSource) msg.obj, msg.arg1 != 0);
return true;
}
case MSG_SET_PLAY_WHEN_READY: {
......@@ -335,9 +335,10 @@ import java.io.IOException;
}
}
private void setMediaSourceInternal(MediaSource mediaSource, boolean resetPosition)
private void prepareInternal(MediaSource mediaSource, boolean resetPosition)
throws ExoPlaybackException {
resetInternal();
loadControl.onPrepared();
if (resetPosition) {
playbackInfo = new PlaybackInfo(0, C.TIME_UNSET);
}
......@@ -597,11 +598,13 @@ import java.io.IOException;
private void stopInternal() {
resetInternal();
loadControl.onStopped();
setState(ExoPlayer.STATE_IDLE);
}
private void releaseInternal() {
resetInternal();
loadControl.onReleased();
setState(ExoPlayer.STATE_IDLE);
synchronized (this) {
released = true;
......@@ -638,7 +641,6 @@ import java.io.IOException;
loadingPeriodHolder = null;
timeline = null;
bufferAheadPeriodCount = 0;
loadControl.onTracksDisabled();
setIsLoading(false);
}
......@@ -1262,11 +1264,14 @@ import java.io.IOException;
sampleStreams, streamResetFlags, positionUs);
periodTrackSelections = trackSelections;
// Update whether we have enabled tracks and sanity check the expected streams are non-null.
hasEnabledTracks = false;
for (int i = 0; i < sampleStreams.length; i++) {
if (sampleStreams[i] != null) {
Assertions.checkState(trackSelections.get(i) != null);
hasEnabledTracks = true;
break;
} else {
Assertions.checkState(trackSelections.get(i) == null);
}
}
......
......@@ -23,7 +23,7 @@ public interface ExoPlayerLibraryInfo {
/**
* The version of the library, expressed as a string.
*/
String VERSION = "2.0.1";
String VERSION = "2.0.2";
/**
* The version of the library, expressed as an integer.
......@@ -32,7 +32,7 @@ public interface ExoPlayerLibraryInfo {
* corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding
* integer version 123045006 (123-045-006).
*/
int VERSION_INT = 2000001;
int VERSION_INT = 2000002;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
......
......@@ -26,6 +26,11 @@ import com.google.android.exoplayer2.upstream.Allocator;
public interface LoadControl {
/**
* Called by the player when prepared with a new source.
*/
void onPrepared();
/**
* Called by the player when a track selection occurs.
*
* @param renderers The renderers.
......@@ -36,9 +41,14 @@ public interface LoadControl {
TrackSelections<?> trackSelections);
/**
* Called by the player when all tracks are disabled.
* Called by the player when stopped.
*/
void onStopped();
/**
* Called by the player when released.
*/
void onTracksDisabled();
void onReleased();
/**
* Returns the {@link Allocator} that should be used to obtain media buffer allocations.
......
......@@ -184,6 +184,14 @@ public final class SimpleExoPlayer implements ExoPlayer {
}
/**
* Clears any {@link Surface}, {@link SurfaceHolder}, {@link SurfaceView} or {@link TextureView}
* currently set on the player.
*/
public void clearVideoSurface() {
setVideoSurface(null);
}
/**
* Sets the {@link Surface} onto which video will be rendered. The caller is responsible for
* tracking the lifecycle of the surface, and must clear the surface by calling
* {@code setVideoSurface(null)} if the surface is destroyed.
......@@ -240,6 +248,9 @@ public final class SimpleExoPlayer implements ExoPlayer {
if (textureView == null) {
setVideoSurfaceInternal(null);
} else {
if (textureView.getSurfaceTextureListener() != null) {
Log.w(TAG, "Replacing existing SurfaceTextureListener.");
}
SurfaceTexture surfaceTexture = textureView.getSurfaceTexture();
setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture));
textureView.setSurfaceTextureListener(componentListener);
......@@ -456,6 +467,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
@Override
public void release() {
player.release();
removeSurfaceCallbacks();
}
@Override
......@@ -592,13 +604,17 @@ public final class SimpleExoPlayer implements ExoPlayer {
}
private void removeSurfaceCallbacks() {
if (this.textureView != null) {
this.textureView.setSurfaceTextureListener(null);
this.textureView = null;
if (textureView != null) {
if (textureView.getSurfaceTextureListener() != componentListener) {
Log.w(TAG, "SurfaceTextureListener already unset or replaced.");
} else {
textureView.setSurfaceTextureListener(null);
}
textureView = null;
}
if (this.surfaceHolder != null) {
this.surfaceHolder.removeCallback(componentListener);
this.surfaceHolder = null;
if (surfaceHolder != null) {
surfaceHolder.removeCallback(componentListener);
surfaceHolder = null;
}
}
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.audio;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.util.MimeTypes;
......@@ -175,9 +176,12 @@ public final class Ac3Util {
* Returns the size in bytes of the given AC-3 syncframe.
*
* @param data The syncframe to parse.
* @return The syncframe size in bytes.
* @return The syncframe size in bytes. {@link C#LENGTH_UNSET} if the input is invalid.
*/
public static int parseAc3SyncframeSize(byte[] data) {
if (data.length < 5) {
return C.LENGTH_UNSET;
}
int fscod = (data[4] & 0xC0) >> 6;
int frmsizecod = data[4] & 0x3F;
return getAc3SyncframeSize(fscod, frmsizecod);
......@@ -227,11 +231,17 @@ public final class Ac3Util {
}
private static int getAc3SyncframeSize(int fscod, int frmsizecod) {
int halfFrmsizecod = frmsizecod / 2;
if (fscod < 0 || fscod >= SAMPLE_RATE_BY_FSCOD.length || frmsizecod < 0
|| halfFrmsizecod >= SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1.length) {
// Invalid values provided.
return C.LENGTH_UNSET;
}
int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod];
if (sampleRate == 44100) {
return 2 * (SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1[frmsizecod / 2] + (frmsizecod % 2));
return 2 * (SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1[halfFrmsizecod] + (frmsizecod % 2));
}
int bitrate = BITRATE_BY_HALF_FRMSIZECOD[frmsizecod / 2];
int bitrate = BITRATE_BY_HALF_FRMSIZECOD[halfFrmsizecod];
if (sampleRate == 32000) {
return 6 * bitrate;
} else { // sampleRate == 48000
......
......@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.extractor.ts;
package com.google.android.exoplayer2.extractor;
import com.google.android.exoplayer2.C;
......
......@@ -30,6 +30,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom;
import com.google.android.exoplayer2.extractor.mp4.Atom.LeafAtom;
......@@ -115,6 +116,9 @@ public final class FragmentedMp4Extractor implements Extractor {
private final ParsableByteArray nalLength;
private final ParsableByteArray encryptionSignalByte;
// Adjusts sample timestamps.
private final TimestampAdjuster timestampAdjuster;
// Parser state.
private final ParsableByteArray atomHeader;
private final byte[] extendedTypeScratch;
......@@ -140,24 +144,28 @@ public final class FragmentedMp4Extractor implements Extractor {
private boolean haveOutputSeekMap;
public FragmentedMp4Extractor() {
this(0);
this(0, null);
}
/**
* @param flags Flags that control the extractor's behavior.
* @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
*/
public FragmentedMp4Extractor(@Flags int flags) {
this(flags, null);
public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster) {
this(flags, null, timestampAdjuster);
}
/**
* @param flags Flags that control the extractor's behavior.
* @param sideloadedTrack Sideloaded track information, in the case that the extractor
* will not receive a moov box in the input data.
* @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
*/
public FragmentedMp4Extractor(@Flags int flags, Track sideloadedTrack) {
public FragmentedMp4Extractor(@Flags int flags, Track sideloadedTrack,
TimestampAdjuster timestampAdjuster) {
this.sideloadedTrack = sideloadedTrack;
this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0);
this.timestampAdjuster = timestampAdjuster;
atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
nalLength = new ParsableByteArray(4);
......@@ -1012,6 +1020,9 @@ public final class FragmentedMp4Extractor implements Extractor {
? fragment.trackEncryptionBox.keyId
: track.sampleDescriptionEncryptionBoxes[sampleDescriptionIndex].keyId;
}
if (timestampAdjuster != null) {
sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs);
}
output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, encryptionKey);
currentTrackBundle.currentSampleIndex++;
......
......@@ -107,6 +107,9 @@ public final class Ac3Extractor implements Extractor {
return true;
}
int frameSize = Ac3Util.parseAc3SyncframeSize(scratch.data);
if (frameSize == C.LENGTH_UNSET) {
return false;
}
input.advancePeekPosition(frameSize - 5);
}
}
......
......@@ -23,6 +23,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException;
......
......@@ -25,6 +25,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray;
......
......@@ -456,7 +456,7 @@ import java.util.Arrays;
lastSeekPositionUs = 0;
notifyReset = prepared;
for (int i = 0; i < sampleQueues.length; i++) {
sampleQueues[i].reset(trackEnabledStates[i]);
sampleQueues[i].reset(!prepared || trackEnabledStates[i]);
}
loadable.setLoadPosition(0);
}
......
......@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
import java.util.ArrayList;
import java.util.IdentityHashMap;
......@@ -84,7 +85,8 @@ import java.util.IdentityHashMap;
}
}
streamPeriodIndices.clear();
// Select tracks for each child, copying the resulting streams back into the streams array.
// Select tracks for each child, copying the resulting streams back into a new streams array.
SampleStream[] newStreams = new SampleStream[selections.length];
SampleStream[] childStreams = new SampleStream[selections.length];
TrackSelection[] childSelections = new TrackSelection[selections.length];
ArrayList<MediaPeriod> enabledPeriodsList = new ArrayList<>(periods.length);
......@@ -103,17 +105,22 @@ import java.util.IdentityHashMap;
boolean periodEnabled = false;
for (int j = 0; j < selections.length; j++) {
if (selectionChildIndices[j] == i) {
streams[j] = childStreams[j];
if (childStreams[j] != null) {
periodEnabled = true;
streamPeriodIndices.put(childStreams[j], i);
}
// Assert that the child provided a stream for the selection.
Assertions.checkState(childStreams[j] != null);
newStreams[j] = childStreams[j];
periodEnabled = true;
streamPeriodIndices.put(childStreams[j], i);
} else if (streamChildIndices[j] == i) {
// Assert that the child cleared any previous stream.
Assertions.checkState(childStreams[j] == null);
}
}
if (periodEnabled) {
enabledPeriodsList.add(periods[i]);
}
}
// Copy the new streams back into the streams array.
System.arraycopy(newStreams, 0, streams, 0, newStreams.length);
// Update the local state.
enabledPeriods = new MediaPeriod[enabledPeriodsList.size()];
enabledPeriodsList.toArray(enabledPeriods);
......@@ -133,21 +140,22 @@ import java.util.IdentityHashMap;
@Override
public long readDiscontinuity() {
long positionUs = enabledPeriods[0].readDiscontinuity();
long positionUs = periods[0].readDiscontinuity();
// Periods other than the first one are not allowed to report discontinuities.
for (int i = 1; i < periods.length; i++) {
if (periods[i].readDiscontinuity() != C.TIME_UNSET) {
throw new IllegalStateException("Child reported discontinuity");
}
}
// It must be possible to seek enabled periods to the new position, if there is one.
if (positionUs != C.TIME_UNSET) {
// It must be possible to seek additional periods to the new position.
for (int i = 1; i < enabledPeriods.length; i++) {
if (enabledPeriods[i].seekToUs(positionUs) != positionUs) {
for (int i = 0; i < enabledPeriods.length; i++) {
if (enabledPeriods[i] != periods[0]
&& enabledPeriods[i].seekToUs(positionUs) != positionUs) {
throw new IllegalStateException("Children seeked to different positions");
}
}
}
// Additional periods are not allowed to report discontinuities.
for (int i = 1; i < enabledPeriods.length; i++) {
if (enabledPeriods[i].readDiscontinuity() != C.TIME_UNSET) {
throw new IllegalStateException("Child reported discontinuity");
}
}
return positionUs;
}
......
......@@ -41,7 +41,7 @@ import java.util.Arrays;
/**
* The initial size of the allocation used to hold the sample data.
*/
private static final int INITIAL_SAMPLE_SIZE = 1;
private static final int INITIAL_SAMPLE_SIZE = 1024;
private final Uri uri;
private final DataSource.Factory dataSourceFactory;
......@@ -71,7 +71,6 @@ import java.util.Arrays;
tracks = new TrackGroupArray(new TrackGroup(format));
sampleStreams = new ArrayList<>();
loader = new Loader("Loader:SingleSampleMediaPeriod");
sampleData = new byte[INITIAL_SAMPLE_SIZE];
}
public void release() {
......@@ -269,7 +268,9 @@ import java.util.Arrays;
int result = 0;
while (result != C.RESULT_END_OF_INPUT) {
sampleSize += result;
if (sampleSize == sampleData.length) {
if (sampleData == null) {
sampleData = new byte[INITIAL_SAMPLE_SIZE];
} else if (sampleSize == sampleData.length) {
sampleData = Arrays.copyOf(sampleData, sampleData.length * 2);
}
result = dataSource.read(sampleData, sampleSize, sampleData.length - sampleSize);
......
......@@ -654,10 +654,10 @@ public class DashManifestParser extends DefaultHandler
} else if (MimeTypes.isVideo(containerMimeType)) {
return MimeTypes.getVideoMediaMimeType(codecs);
} else if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) {
// We currently only support CEA-608 through RawCC
if (codecs != null
&& (codecs.contains("eia608") || codecs.contains("cea608"))) {
return MimeTypes.APPLICATION_CEA608;
if (codecs != null) {
if (codecs.contains("eia608") || codecs.contains("cea608")) {
return MimeTypes.APPLICATION_CEA608;
}
}
return null;
} else if (mimeTypeIsRawText(containerMimeType)) {
......
......@@ -21,11 +21,12 @@ import android.text.TextUtils;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer2.extractor.ts.Ac3Extractor;
import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
import com.google.android.exoplayer2.extractor.ts.DefaultStreamReaderFactory;
import com.google.android.exoplayer2.extractor.ts.TimestampAdjuster;
import com.google.android.exoplayer2.extractor.ts.TsExtractor;
import com.google.android.exoplayer2.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.TrackGroup;
......@@ -34,6 +35,7 @@ import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil;
import com.google.android.exoplayer2.source.chunk.DataChunk;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
import com.google.android.exoplayer2.trackselection.BaseTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection;
......@@ -101,6 +103,7 @@ import java.util.Locale;
private static final String AC3_FILE_EXTENSION = ".ac3";
private static final String EC3_FILE_EXTENSION = ".ec3";
private static final String MP3_FILE_EXTENSION = ".mp3";
private static final String MP4_FILE_EXTENSION = ".mp4";
private static final String VTT_FILE_EXTENSION = ".vtt";
private static final String WEBVTT_FILE_EXTENSION = ".webvtt";
......@@ -118,6 +121,7 @@ import java.util.Locale;
private long durationUs;
private IOException fatalError;
private HlsInitializationChunk lastLoadedInitializationChunk;
private Uri encryptionKeyUri;
private byte[] encryptionKey;
private String encryptionIvString;
......@@ -289,7 +293,6 @@ import java.util.Locale;
}
HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex);
Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url);
// Check if encryption is specified.
if (segment.isEncrypted) {
......@@ -307,10 +310,6 @@ import java.util.Locale;
clearEncryptionData();
}
// Configure the data source and spec for the chunk.
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) {
......@@ -327,8 +326,15 @@ import java.util.Locale;
long endTimeUs = startTimeUs + (long) (segment.durationSecs * C.MICROS_PER_SECOND);
Format format = variants[newVariantIndex].format;
Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url);
// Configure the extractor that will read the chunk.
Extractor extractor;
boolean useInitializedExtractor = lastLoadedInitializationChunk != null
&& lastLoadedInitializationChunk.format == format;
boolean needNewExtractor = previous == null
|| previous.discontinuitySequenceNumber != segment.discontinuitySequenceNumber
|| format != previous.trackFormat;
boolean extractorNeedsInit = true;
boolean isTimestampMaster = false;
TimestampAdjuster timestampAdjuster = null;
......@@ -348,13 +354,21 @@ import java.util.Locale;
timestampAdjuster = timestampAdjusterProvider.getAdjuster(segment.discontinuitySequenceNumber,
startTimeUs);
extractor = new WebvttExtractor(format.language, timestampAdjuster);
} else if (previous == null
|| previous.discontinuitySequenceNumber != segment.discontinuitySequenceNumber
|| format != previous.trackFormat) {
// MPEG-2 TS segments, but we need a new extractor.
} else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION)) {
isTimestampMaster = true;
timestampAdjuster = timestampAdjusterProvider.getAdjuster(segment.discontinuitySequenceNumber,
startTimeUs);
if (needNewExtractor) {
if (useInitializedExtractor) {
extractor = lastLoadedInitializationChunk.extractor;
} else {
timestampAdjuster = timestampAdjusterProvider.getAdjuster(
segment.discontinuitySequenceNumber, startTimeUs);
extractor = new FragmentedMp4Extractor(0, timestampAdjuster);
}
} else {
extractor = previous.extractor;
}
} else if (needNewExtractor) {
// MPEG-2 TS segments, but we need a new extractor.
// This flag ensures the change of pid between streams does not affect the sample queues.
@DefaultStreamReaderFactory.WorkaroundFlags
int workaroundFlags = DefaultStreamReaderFactory.WORKAROUND_MAP_BY_TYPE;
......@@ -370,14 +384,31 @@ import java.util.Locale;
workaroundFlags |= DefaultStreamReaderFactory.WORKAROUND_IGNORE_H264_STREAM;
}
}
extractor = new TsExtractor(timestampAdjuster,
new DefaultStreamReaderFactory(workaroundFlags));
isTimestampMaster = true;
if (useInitializedExtractor) {
extractor = lastLoadedInitializationChunk.extractor;
} else {
timestampAdjuster = timestampAdjusterProvider.getAdjuster(
segment.discontinuitySequenceNumber, startTimeUs);
extractor = new TsExtractor(timestampAdjuster,
new DefaultStreamReaderFactory(workaroundFlags));
}
} else {
// MPEG-2 TS segments, and we need to continue using the same extractor.
extractor = previous.extractor;
extractorNeedsInit = false;
}
if (needNewExtractor && mediaPlaylist.initializationSegment != null
&& !useInitializedExtractor) {
out.chunk = buildInitializationChunk(mediaPlaylist, extractor, format);
return;
}
lastLoadedInitializationChunk = null;
// Configure the data source and spec for the chunk.
DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength,
null);
out.chunk = new HlsMediaChunk(dataSource, dataSpec, format,
trackSelection.getSelectionReason(), trackSelection.getSelectionData(),
startTimeUs, endTimeUs, chunkMediaSequence, segment.discontinuitySequenceNumber,
......@@ -434,7 +465,9 @@ import java.util.Locale;
* @param chunk The chunk whose load has been completed.
*/
public void onChunkLoadCompleted(Chunk chunk) {
if (chunk instanceof MediaPlaylistChunk) {
if (chunk instanceof HlsInitializationChunk) {
lastLoadedInitializationChunk = (HlsInitializationChunk) chunk;
} else if (chunk instanceof MediaPlaylistChunk) {
MediaPlaylistChunk mediaPlaylistChunk = (MediaPlaylistChunk) chunk;
scratchSpace = mediaPlaylistChunk.getDataHolder();
setMediaPlaylist(mediaPlaylistChunk.variantIndex, mediaPlaylistChunk.getResult());
......@@ -462,6 +495,18 @@ import java.util.Locale;
// Private methods.
private HlsInitializationChunk buildInitializationChunk(HlsMediaPlaylist mediaPlaylist,
Extractor extractor, Format format) {
Segment initSegment = mediaPlaylist.initializationSegment;
// The initialization segment is required before the actual media chunk.
Uri initSegmentUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, initSegment.url);
DataSpec initDataSpec = new DataSpec(initSegmentUri, initSegment.byterangeOffset,
initSegment.byterangeLength, null);
return new HlsInitializationChunk(dataSource, initDataSpec,
trackSelection.getSelectionReason(), trackSelection.getSelectionData(), extractor,
format);
}
private long msToRerequestLiveMediaPlaylist(int variantIndex) {
HlsMediaPlaylist mediaPlaylist = variantPlaylists[variantIndex];
long timeSinceLastMediaPlaylistLoadMs =
......
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source.hls;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.source.chunk.Chunk;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
/**
* An HLS initialization chunk. Provides the extractor with information required for extracting the
* samples.
*/
/* package */ final class HlsInitializationChunk extends Chunk {
public final Format format;
public final Extractor extractor;
private int bytesLoaded;
private volatile boolean loadCanceled;
public HlsInitializationChunk(DataSource dataSource, DataSpec dataSpec, int trackSelectionReason,
Object trackSelectionData, Extractor extractor, Format format) {
super(dataSource, dataSpec, C.TRACK_TYPE_DEFAULT, null, trackSelectionReason,
trackSelectionData, C.TIME_UNSET, C.TIME_UNSET);
this.extractor = extractor;
this.format = format;
}
/**
* Sets the {@link HlsSampleStreamWrapper} that will receive the sample format information from
* the initialization chunk.
*
* @param output The output that will receive the format information.
*/
public void init(HlsSampleStreamWrapper output) {
extractor.init(output);
}
@Override
public long bytesLoaded() {
return bytesLoaded;
}
@Override
public void cancelLoad() {
loadCanceled = true;
}
@Override
public boolean isLoadCanceled() {
return loadCanceled;
}
@Override
public void load() throws IOException, InterruptedException {
DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
try {
ExtractorInput input = new DefaultExtractorInput(dataSource,
loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec));
try {
int result = Extractor.RESULT_CONTINUE;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
result = extractor.read(input, null);
}
} finally {
bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
}
} finally {
dataSource.close();
}
}
}
......@@ -19,7 +19,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ts.TimestampAdjuster;
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
......
......@@ -39,6 +39,7 @@ import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.Loader;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
import java.util.ArrayList;
import java.util.IdentityHashMap;
......@@ -154,7 +155,8 @@ import java.util.List;
}
boolean selectedNewTracks = false;
streamWrapperIndices.clear();
// Select tracks for each child, copying the resulting streams back into the streams array.
// Select tracks for each child, copying the resulting streams back into a new streams array.
SampleStream[] newStreams = new SampleStream[selections.length];
SampleStream[] childStreams = new SampleStream[selections.length];
TrackSelection[] childSelections = new TrackSelection[selections.length];
ArrayList<HlsSampleStreamWrapper> enabledSampleStreamWrapperList = new ArrayList<>(
......@@ -168,19 +170,23 @@ import java.util.List;
mayRetainStreamFlags, childStreams, streamResetFlags, !seenFirstTrackSelection);
boolean wrapperEnabled = false;
for (int j = 0; j < selections.length; j++) {
if (selectionChildIndices[j] == i
|| (selectionChildIndices[j] == C.INDEX_UNSET && streamChildIndices[j] == i)) {
streams[j] = childStreams[j];
if (childStreams[j] != null) {
wrapperEnabled = true;
streamWrapperIndices.put(childStreams[j], i);
}
if (selectionChildIndices[j] == i) {
// Assert that the child provided a stream for the selection.
Assertions.checkState(childStreams[j] != null);
newStreams[j] = childStreams[j];
wrapperEnabled = true;
streamWrapperIndices.put(childStreams[j], i);
} else if (streamChildIndices[j] == i) {
// Assert that the child cleared any previous stream.
Assertions.checkState(childStreams[j] == null);
}
}
if (wrapperEnabled) {
enabledSampleStreamWrapperList.add(sampleStreamWrappers[i]);
}
}
// Copy the new streams back into the streams array.
System.arraycopy(newStreams, 0, streams, 0, newStreams.length);
// Update the local state.
enabledSampleStreamWrappers = new HlsSampleStreamWrapper[enabledSampleStreamWrapperList.size()];
enabledSampleStreamWrapperList.toArray(enabledSampleStreamWrappers);
......
......@@ -358,6 +358,8 @@ import java.util.LinkedList;
HlsMediaChunk mediaChunk = (HlsMediaChunk) loadable;
mediaChunk.init(this);
mediaChunks.add(mediaChunk);
} else if (loadable instanceof HlsInitializationChunk) {
((HlsInitializationChunk) loadable).init(this);
}
long elapsedRealtimeMs = loader.startLoading(loadable, this, minLoadableRetryCount);
eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat,
......
......@@ -16,7 +16,7 @@
package com.google.android.exoplayer2.source.hls;
import android.util.SparseArray;
import com.google.android.exoplayer2.extractor.ts.TimestampAdjuster;
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
/**
* Provides {@link TimestampAdjuster} instances for use during HLS playbacks.
......
......@@ -24,8 +24,8 @@ import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.ts.TimestampAdjuster;
import com.google.android.exoplayer2.text.SubtitleDecoderException;
import com.google.android.exoplayer2.text.webvtt.WebvttParserUtil;
import com.google.android.exoplayer2.util.MimeTypes;
......
......@@ -38,6 +38,10 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
public final long byterangeOffset;
public final long byterangeLength;
public Segment(String uri, long byterangeOffset, long byterangeLength) {
this(uri, 0, -1, C.TIME_UNSET, false, null, null, byterangeOffset, byterangeLength);
}
public Segment(String uri, double durationSecs, int discontinuitySequenceNumber,
long startTimeUs, boolean isEncrypted, String encryptionKeyUri, String encryptionIV,
long byterangeOffset, long byterangeLength) {
......@@ -64,17 +68,19 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
public final int mediaSequence;
public final int targetDurationSecs;
public final int version;
public final Segment initializationSegment;
public final List<Segment> segments;
public final boolean live;
public final long durationUs;
public HlsMediaPlaylist(String baseUri, int mediaSequence, int targetDurationSecs, int version,
boolean live, List<Segment> segments) {
boolean live, Segment initializationSegment, List<Segment> segments) {
super(baseUri, HlsPlaylist.TYPE_MEDIA);
this.mediaSequence = mediaSequence;
this.targetDurationSecs = targetDurationSecs;
this.version = version;
this.live = live;
this.initializationSegment = initializationSegment;
this.segments = segments;
if (!segments.isEmpty()) {
......
......@@ -44,6 +44,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static final String TAG_MEDIA = "#EXT-X-MEDIA";
private static final String TAG_DISCONTINUITY = "#EXT-X-DISCONTINUITY";
private static final String TAG_DISCONTINUITY_SEQUENCE = "#EXT-X-DISCONTINUITY-SEQUENCE";
private static final String TAG_INIT_SEGMENT = "#EXT-X-MAP";
private static final String TAG_MEDIA_DURATION = "#EXTINF";
private static final String TAG_MEDIA_SEQUENCE = "#EXT-X-MEDIA-SEQUENCE";
private static final String TAG_TARGET_DURATION = "#EXT-X-TARGETDURATION";
......@@ -79,6 +80,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
+ ":([\\d\\.]+)\\b");
private static final Pattern REGEX_BYTERANGE = Pattern.compile(TAG_BYTERANGE
+ ":(\\d+(?:@\\d+)?)\\b");
private static final Pattern REGEX_ATTR_BYTERANGE =
Pattern.compile("BYTERANGE=\"(\\d+(?:@\\d+)?)\\b\"");
private static final Pattern REGEX_METHOD = Pattern.compile("METHOD=(" + METHOD_NONE + "|"
+ METHOD_AES128 + ")");
private static final Pattern REGEX_URI = Pattern.compile("URI=\"(.+?)\"");
......@@ -212,13 +215,14 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
int targetDurationSecs = 0;
int version = 1; // Default version == 1.
boolean live = true;
Segment initializationSegment = null;
List<Segment> segments = new ArrayList<>();
double segmentDurationSecs = 0.0;
int discontinuitySequenceNumber = 0;
long segmentStartTimeUs = 0;
long segmentByterangeOffset = 0;
long segmentByterangeLength = C.LENGTH_UNSET;
long segmentByteRangeOffset = 0;
long segmentByteRangeLength = C.LENGTH_UNSET;
int segmentMediaSequence = 0;
boolean isEncrypted = false;
......@@ -228,7 +232,20 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
String line;
while (iterator.hasNext()) {
line = iterator.next();
if (line.startsWith(TAG_TARGET_DURATION)) {
if (line.startsWith(TAG_INIT_SEGMENT)) {
String uri = parseStringAttr(line, REGEX_URI);
String byteRange = parseOptionalStringAttr(line, REGEX_ATTR_BYTERANGE);
if (byteRange != null) {
String[] splitByteRange = byteRange.split("@");
segmentByteRangeLength = Long.parseLong(splitByteRange[0]);
if (splitByteRange.length > 1) {
segmentByteRangeOffset = Long.parseLong(splitByteRange[1]);
}
}
initializationSegment = new Segment(uri, segmentByteRangeOffset, segmentByteRangeLength);
segmentByteRangeOffset = 0;
segmentByteRangeLength = C.LENGTH_UNSET;
} else if (line.startsWith(TAG_TARGET_DURATION)) {
targetDurationSecs = parseIntAttr(line, REGEX_TARGET_DURATION);
} else if (line.startsWith(TAG_MEDIA_SEQUENCE)) {
mediaSequence = parseIntAttr(line, REGEX_MEDIA_SEQUENCE);
......@@ -250,9 +267,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
} else if (line.startsWith(TAG_BYTERANGE)) {
String byteRange = parseStringAttr(line, REGEX_BYTERANGE);
String[] splitByteRange = byteRange.split("@");
segmentByterangeLength = Long.parseLong(splitByteRange[0]);
segmentByteRangeLength = Long.parseLong(splitByteRange[0]);
if (splitByteRange.length > 1) {
segmentByterangeOffset = Long.parseLong(splitByteRange[1]);
segmentByteRangeOffset = Long.parseLong(splitByteRange[1]);
}
} else if (line.startsWith(TAG_DISCONTINUITY_SEQUENCE)) {
discontinuitySequenceNumber = Integer.parseInt(line.substring(line.indexOf(':') + 1));
......@@ -268,24 +285,24 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
segmentEncryptionIV = Integer.toHexString(segmentMediaSequence);
}
segmentMediaSequence++;
if (segmentByterangeLength == C.LENGTH_UNSET) {
segmentByterangeOffset = 0;
if (segmentByteRangeLength == C.LENGTH_UNSET) {
segmentByteRangeOffset = 0;
}
segments.add(new Segment(line, segmentDurationSecs, discontinuitySequenceNumber,
segmentStartTimeUs, isEncrypted, encryptionKeyUri, segmentEncryptionIV,
segmentByterangeOffset, segmentByterangeLength));
segmentByteRangeOffset, segmentByteRangeLength));
segmentStartTimeUs += (long) (segmentDurationSecs * C.MICROS_PER_SECOND);
segmentDurationSecs = 0.0;
if (segmentByterangeLength != C.LENGTH_UNSET) {
segmentByterangeOffset += segmentByterangeLength;
if (segmentByteRangeLength != C.LENGTH_UNSET) {
segmentByteRangeOffset += segmentByteRangeLength;
}
segmentByterangeLength = C.LENGTH_UNSET;
segmentByteRangeLength = C.LENGTH_UNSET;
} else if (line.equals(TAG_ENDLIST)) {
live = false;
}
}
return new HlsMediaPlaylist(baseUri, mediaSequence, targetDurationSecs, version, live,
Collections.unmodifiableList(segments));
initializationSegment, Collections.unmodifiableList(segments));
}
private static String parseStringAttr(String line, Pattern pattern) throws ParserException {
......
......@@ -101,7 +101,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
trackEncryptionBoxes, nalUnitLengthFieldLength, null, null);
FragmentedMp4Extractor extractor = new FragmentedMp4Extractor(
FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME
| FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, track);
| FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, track, null);
extractorWrappers[i] = new ChunkExtractorWrapper(extractor, format, false, false);
}
}
......
......@@ -160,21 +160,27 @@ public final class TextRenderer extends BaseRenderer implements Callback {
}
}
if (nextSubtitle != null && nextSubtitle.timeUs <= positionUs) {
// Advance to the next subtitle. Sync the next event index and trigger an update.
if (subtitle != null) {
subtitle.release();
}
subtitle = nextSubtitle;
nextSubtitle = null;
if (subtitle.isEndOfStream()) {
outputStreamEnded = true;
subtitle.release();
subtitle = null;
return;
if (nextSubtitle != null) {
if (nextSubtitle.isEndOfStream()) {
if (!textRendererNeedsUpdate && getNextEventTime() == Long.MAX_VALUE) {
if (subtitle != null) {
subtitle.release();
subtitle = null;
}
nextSubtitle.release();
nextSubtitle = null;
outputStreamEnded = true;
}
} else if (nextSubtitle.timeUs <= positionUs) {
// Advance to the next subtitle. Sync the next event index and trigger an update.
if (subtitle != null) {
subtitle.release();
}
subtitle = nextSubtitle;
nextSubtitle = null;
nextSubtitleEventIndex = subtitle.getNextEventTimeIndex(positionUs);
textRendererNeedsUpdate = true;
}
nextSubtitleEventIndex = subtitle.getNextEventTimeIndex(positionUs);
textRendererNeedsUpdate = true;
}
if (textRendererNeedsUpdate) {
......
......@@ -16,15 +16,31 @@
package com.google.android.exoplayer2.ui;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import com.google.android.exoplayer2.R;
/**
* A {@link FrameLayout} that resizes itself to match a specified aspect ratio.
*/
public final class AspectRatioFrameLayout extends FrameLayout {
/**
* Either the width or height is decreased to obtain the desired aspect ratio.
*/
public static final int RESIZE_MODE_FIT = 0;
/**
* The width is fixed and the height is increased or decreased to obtain the desired aspect ratio.
*/
public static final int RESIZE_MODE_FIXED_WIDTH = 1;
/**
* The height is fixed and the width is increased or decreased to obtain the desired aspect ratio.
*/
public static final int RESIZE_MODE_FIXED_HEIGHT = 2;
/**
* The {@link FrameLayout} will not resize itself if the fractional difference between its natural
* aspect ratio and the requested aspect ratio falls below this threshold.
* <p>
......@@ -36,13 +52,24 @@ public final class AspectRatioFrameLayout extends FrameLayout {
private static final float MAX_ASPECT_RATIO_DEFORMATION_FRACTION = 0.01f;
private float videoAspectRatio;
private int resizeMode;
public AspectRatioFrameLayout(Context context) {
super(context);
this(context, null);
}
public AspectRatioFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
resizeMode = RESIZE_MODE_FIT;
if (attrs != null) {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
R.styleable.AspectRatioFrameLayout, 0, 0);
try {
resizeMode = a.getInt(R.styleable.AspectRatioFrameLayout_resize_mode, RESIZE_MODE_FIT);
} finally {
a.recycle();
}
}
}
/**
......@@ -57,6 +84,19 @@ public final class AspectRatioFrameLayout extends FrameLayout {
}
}
/**
* Sets the resize mode which can be of value {@link #RESIZE_MODE_FIT},
* {@link #RESIZE_MODE_FIXED_HEIGHT} or {@link #RESIZE_MODE_FIXED_WIDTH}.
*
* @param resizeMode The resize mode.
*/
public void setResizeMode(int resizeMode) {
if (this.resizeMode != resizeMode) {
this.resizeMode = resizeMode;
requestLayout();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
......@@ -74,10 +114,20 @@ public final class AspectRatioFrameLayout extends FrameLayout {
return;
}
if (aspectDeformation > 0) {
height = (int) (width / videoAspectRatio);
} else {
width = (int) (height * videoAspectRatio);
switch (resizeMode) {
case RESIZE_MODE_FIXED_WIDTH:
height = (int) (width / videoAspectRatio);
break;
case RESIZE_MODE_FIXED_HEIGHT:
width = (int) (height * videoAspectRatio);
break;
default:
if (aspectDeformation > 0) {
height = (int) (width / videoAspectRatio);
} else {
width = (int) (height * videoAspectRatio);
}
break;
}
super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
......
......@@ -128,11 +128,21 @@ public class PlaybackControlView extends FrameLayout {
}
/**
* Returns the player currently being controlled by this view, or null if no player is set.
*/
public ExoPlayer getPlayer() {
return player;
}
/**
* Sets the {@link ExoPlayer} to control.
*
* @param player the {@code ExoPlayer} to control.
*/
public void setPlayer(ExoPlayer player) {
if (this.player == player) {
return;
}
if (this.player != null) {
this.player.removeListener(componentListener);
}
......
......@@ -63,6 +63,7 @@ public final class SimpleExoPlayerView extends FrameLayout {
super(context, attrs, defStyleAttr);
boolean useTextureView = false;
int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
if (attrs != null) {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
R.styleable.SimpleExoPlayerView, 0, 0);
......@@ -70,6 +71,8 @@ public final class SimpleExoPlayerView extends FrameLayout {
useController = a.getBoolean(R.styleable.SimpleExoPlayerView_use_controller, useController);
useTextureView = a.getBoolean(R.styleable.SimpleExoPlayerView_use_texture_view,
useTextureView);
resizeMode = a.getInt(R.styleable.SimpleExoPlayerView_resize_mode,
AspectRatioFrameLayout.RESIZE_MODE_FIT);
} finally {
a.recycle();
}
......@@ -78,6 +81,7 @@ public final class SimpleExoPlayerView extends FrameLayout {
LayoutInflater.from(context).inflate(R.layout.exo_simple_player_view, this);
componentListener = new ComponentListener();
layout = (AspectRatioFrameLayout) findViewById(R.id.video_frame);
layout.setResizeMode(resizeMode);
controller = (PlaybackControlView) findViewById(R.id.control);
shutterView = findViewById(R.id.shutter);
subtitleLayout = (SubtitleView) findViewById(R.id.subtitles);
......@@ -94,6 +98,13 @@ public final class SimpleExoPlayerView extends FrameLayout {
}
/**
* Returns the player currently set on this view, or null if no player is set.
*/
public SimpleExoPlayer getPlayer() {
return player;
}
/**
* Set the {@link SimpleExoPlayer} to use. The {@link SimpleExoPlayer#setTextOutput} and
* {@link SimpleExoPlayer#setVideoListener} method of the player will be called and previous
* assignments are overridden.
......@@ -101,6 +112,9 @@ public final class SimpleExoPlayerView extends FrameLayout {
* @param player The {@link SimpleExoPlayer} to use.
*/
public void setPlayer(SimpleExoPlayer player) {
if (this.player == player) {
return;
}
if (this.player != null) {
this.player.setTextOutput(null);
this.player.setVideoListener(null);
......@@ -120,7 +134,9 @@ public final class SimpleExoPlayerView extends FrameLayout {
} else {
shutterView.setVisibility(VISIBLE);
}
setUseController(useController);
if (useController) {
controller.setPlayer(player);
}
}
/**
......@@ -131,6 +147,9 @@ public final class SimpleExoPlayerView extends FrameLayout {
* @param useController If {@code false} the playback control is never used.
*/
public void setUseController(boolean useController) {
if (this.useController == useController) {
return;
}
this.useController = useController;
if (useController) {
controller.setPlayer(player);
......@@ -141,6 +160,17 @@ public final class SimpleExoPlayerView extends FrameLayout {
}
/**
* Sets the resize mode which can be of value {@link AspectRatioFrameLayout#RESIZE_MODE_FIT},
* {@link AspectRatioFrameLayout#RESIZE_MODE_FIXED_HEIGHT} or
* {@link AspectRatioFrameLayout#RESIZE_MODE_FIXED_WIDTH}.
*
* @param resizeMode The resize mode.
*/
public void setResizeMode(int resizeMode) {
layout.setResizeMode(resizeMode);
}
/**
* Set the {@link PlaybackControlView.VisibilityListener}.
*
* @param listener The listener to be notified about visibility changes.
......
......@@ -104,29 +104,35 @@ public final class AssetDataSource implements DataSource {
@Override
public int read(byte[] buffer, int offset, int readLength) throws AssetDataSourceException {
if (bytesRemaining == 0) {
if (readLength == 0) {
return 0;
} else if (bytesRemaining == 0) {
return C.RESULT_END_OF_INPUT;
} else {
int bytesRead;
try {
int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength
: (int) Math.min(bytesRemaining, readLength);
bytesRead = inputStream.read(buffer, offset, bytesToRead);
} catch (IOException e) {
throw new AssetDataSourceException(e);
}
}
if (bytesRead > 0) {
if (bytesRemaining != C.LENGTH_UNSET) {
bytesRemaining -= bytesRead;
}
if (listener != null) {
listener.onBytesTransferred(this, bytesRead);
}
}
int bytesRead;
try {
int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength
: (int) Math.min(bytesRemaining, readLength);
bytesRead = inputStream.read(buffer, offset, bytesToRead);
} catch (IOException e) {
throw new AssetDataSourceException(e);
}
return bytesRead;
if (bytesRead == -1) {
if (bytesRemaining != C.LENGTH_UNSET) {
// End of stream reached having not read sufficient data.
throw new AssetDataSourceException(new EOFException());
}
return C.RESULT_END_OF_INPUT;
}
if (bytesRemaining != C.LENGTH_UNSET) {
bytesRemaining -= bytesRead;
}
if (listener != null) {
listener.onBytesTransferred(this, bytesRead);
}
return bytesRead;
}
@Override
......
......@@ -54,15 +54,18 @@ public final class ByteArrayDataSource implements DataSource {
}
@Override
public int read(byte[] buffer, int offset, int length) throws IOException {
if (bytesRemaining == 0) {
public int read(byte[] buffer, int offset, int readLength) throws IOException {
if (readLength == 0) {
return 0;
} else if (bytesRemaining == 0) {
return C.RESULT_END_OF_INPUT;
}
length = Math.min(length, bytesRemaining);
System.arraycopy(data, readPosition, buffer, offset, length);
readPosition += length;
bytesRemaining -= length;
return length;
readLength = Math.min(readLength, bytesRemaining);
System.arraycopy(data, readPosition, buffer, offset, readLength);
readPosition += readLength;
bytesRemaining -= readLength;
return readLength;
}
@Override
......
......@@ -103,29 +103,35 @@ public final class ContentDataSource implements DataSource {
@Override
public int read(byte[] buffer, int offset, int readLength) throws ContentDataSourceException {
if (bytesRemaining == 0) {
if (readLength == 0) {
return 0;
} else if (bytesRemaining == 0) {
return C.RESULT_END_OF_INPUT;
} else {
int bytesRead;
try {
int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength
: (int) Math.min(bytesRemaining, readLength);
bytesRead = inputStream.read(buffer, offset, bytesToRead);
} catch (IOException e) {
throw new ContentDataSourceException(e);
}
}
if (bytesRead > 0) {
if (bytesRemaining != C.LENGTH_UNSET) {
bytesRemaining -= bytesRead;
}
if (listener != null) {
listener.onBytesTransferred(this, bytesRead);
}
}
int bytesRead;
try {
int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength
: (int) Math.min(bytesRemaining, readLength);
bytesRead = inputStream.read(buffer, offset, bytesToRead);
} catch (IOException e) {
throw new ContentDataSourceException(e);
}
return bytesRead;
if (bytesRead == -1) {
if (bytesRemaining != C.LENGTH_UNSET) {
// End of stream reached having not read sufficient data.
throw new ContentDataSourceException(new EOFException());
}
return C.RESULT_END_OF_INPUT;
}
if (bytesRemaining != C.LENGTH_UNSET) {
bytesRemaining -= bytesRead;
}
if (listener != null) {
listener.onBytesTransferred(this, bytesRead);
}
return bytesRead;
}
@Override
......
......@@ -55,14 +55,18 @@ public interface DataSource {
/**
* Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at
* index {@code offset}. Blocks until at least one byte of data can be read, the end of the opened
* range is detected, or an exception is thrown.
* index {@code offset}.
* <p>
* If {@code length} is zero then 0 is returned. Otherwise, if no data is available because the
* end of the opened range has been reached, then {@link C#RESULT_END_OF_INPUT} is returned.
* Otherwise, the call will block until at least one byte of data has been read and the number of
* bytes read is returned.
*
* @param buffer The buffer into which the read data should be stored.
* @param offset The start offset into {@code buffer} at which data should be written.
* @param readLength The maximum number of bytes to read.
* @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the end of the opened
* range is reached.
* @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if no data is avaliable
* because the end of the opened range has been reached.
* @throws IOException If an error occurs reading from the source.
*/
int read(byte[] buffer, int offset, int readLength) throws IOException;
......
......@@ -26,6 +26,7 @@ public final class DefaultAllocator implements Allocator {
private static final int AVAILABLE_EXTRA_CAPACITY = 100;
private final boolean trimOnReset;
private final int individualAllocationSize;
private final byte[] initialAllocationBlock;
private final Allocation[] singleAllocationReleaseHolder;
......@@ -38,10 +39,12 @@ public final class DefaultAllocator implements Allocator {
/**
* Constructs an instance without creating any {@link Allocation}s up front.
*
* @param trimOnReset Whether memory is freed when the allocator is reset. Should be true unless
* the allocator will be re-used by multiple player instances.
* @param individualAllocationSize The length of each individual {@link Allocation}.
*/
public DefaultAllocator(int individualAllocationSize) {
this(individualAllocationSize, 0);
public DefaultAllocator(boolean trimOnReset, int individualAllocationSize) {
this(trimOnReset, individualAllocationSize, 0);
}
/**
......@@ -49,12 +52,16 @@ public final class DefaultAllocator implements Allocator {
* <p>
* Note: {@link Allocation}s created up front will never be discarded by {@link #trim()}.
*
* @param trimOnReset Whether memory is freed when the allocator is reset. Should be true unless
* the allocator will be re-used by multiple player instances.
* @param individualAllocationSize The length of each individual {@link Allocation}.
* @param initialAllocationCount The number of allocations to create up front.
*/
public DefaultAllocator(int individualAllocationSize, int initialAllocationCount) {
public DefaultAllocator(boolean trimOnReset, int individualAllocationSize,
int initialAllocationCount) {
Assertions.checkArgument(individualAllocationSize > 0);
Assertions.checkArgument(initialAllocationCount >= 0);
this.trimOnReset = trimOnReset;
this.individualAllocationSize = individualAllocationSize;
this.availableCount = initialAllocationCount;
this.availableAllocations = new Allocation[initialAllocationCount + AVAILABLE_EXTRA_CAPACITY];
......@@ -70,6 +77,12 @@ public final class DefaultAllocator implements Allocator {
singleAllocationReleaseHolder = new Allocation[1];
}
public synchronized void reset() {
if (trimOnReset) {
setTargetBufferSize(0);
}
}
public synchronized void setTargetBufferSize(int targetBufferSize) {
boolean targetBufferSizeReduced = targetBufferSize < this.targetBufferSize;
this.targetBufferSize = targetBufferSize;
......
......@@ -550,11 +550,18 @@ public class DefaultHttpDataSource implements HttpDataSource {
if (readLength == 0) {
return 0;
}
if (bytesToRead != C.LENGTH_UNSET) {
long bytesRemaining = bytesToRead - bytesRead;
if (bytesRemaining == 0) {
return C.RESULT_END_OF_INPUT;
}
readLength = (int) Math.min(readLength, bytesRemaining);
}
int read = inputStream.read(buffer, offset, readLength);
if (read == -1) {
if (bytesToRead != C.LENGTH_UNSET && bytesToRead != bytesRead) {
// The server closed the connection having not sent sufficient data.
if (bytesToRead != C.LENGTH_UNSET) {
// End of stream reached having not read sufficient data.
throw new EOFException();
}
return C.RESULT_END_OF_INPUT;
......
......@@ -80,7 +80,9 @@ public final class FileDataSource implements DataSource {
@Override
public int read(byte[] buffer, int offset, int readLength) throws FileDataSourceException {
if (bytesRemaining == 0) {
if (readLength == 0) {
return 0;
} else if (bytesRemaining == 0) {
return C.RESULT_END_OF_INPUT;
} else {
int bytesRead;
......
......@@ -132,29 +132,35 @@ public final class RawResourceDataSource implements DataSource {
@Override
public int read(byte[] buffer, int offset, int readLength) throws RawResourceDataSourceException {
if (bytesRemaining == 0) {
if (readLength == 0) {
return 0;
} else if (bytesRemaining == 0) {
return C.RESULT_END_OF_INPUT;
} else {
int bytesRead;
try {
int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength
: (int) Math.min(bytesRemaining, readLength);
bytesRead = inputStream.read(buffer, offset, bytesToRead);
} catch (IOException e) {
throw new RawResourceDataSourceException(e);
}
}
if (bytesRead > 0) {
if (bytesRemaining != C.LENGTH_UNSET) {
bytesRemaining -= bytesRead;
}
if (listener != null) {
listener.onBytesTransferred(this, bytesRead);
}
}
int bytesRead;
try {
int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength
: (int) Math.min(bytesRemaining, readLength);
bytesRead = inputStream.read(buffer, offset, bytesToRead);
} catch (IOException e) {
throw new RawResourceDataSourceException(e);
}
return bytesRead;
if (bytesRead == -1) {
if (bytesRemaining != C.LENGTH_UNSET) {
// End of stream reached having not read sufficient data.
throw new RawResourceDataSourceException(new EOFException());
}
return C.RESULT_END_OF_INPUT;
}
if (bytesRemaining != C.LENGTH_UNSET) {
bytesRemaining -= bytesRead;
}
if (listener != null) {
listener.onBytesTransferred(this, bytesRead);
}
return bytesRead;
}
@Override
......
......@@ -129,6 +129,10 @@ public final class UdpDataSource implements DataSource {
@Override
public int read(byte[] buffer, int offset, int readLength) throws UdpDataSourceException {
if (readLength == 0) {
return 0;
}
if (packetRemaining == 0) {
// We've read all of the data from the current packet. Get another.
try {
......@@ -136,7 +140,6 @@ public final class UdpDataSource implements DataSource {
} catch (IOException e) {
throw new UdpDataSourceException(e);
}
packetRemaining = packet.getLength();
if (listener != null) {
listener.onBytesTransferred(this, packetRemaining);
......
......@@ -194,12 +194,15 @@ public final class CacheDataSource implements DataSource {
}
@Override
public int read(byte[] buffer, int offset, int max) throws IOException {
public int read(byte[] buffer, int offset, int readLength) throws IOException {
if (readLength == 0) {
return 0;
}
if (bytesRemaining == 0) {
return C.RESULT_END_OF_INPUT;
}
try {
int bytesRead = currentDataSource.read(buffer, offset, max);
int bytesRead = currentDataSource.read(buffer, offset, readLength);
if (bytesRead >= 0) {
if (currentDataSource == cacheReadDataSource) {
totalCachedBytesRead += bytesRead;
......@@ -218,7 +221,7 @@ public final class CacheDataSource implements DataSource {
closeCurrentSource();
if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) {
if (openNextSource(false)) {
return read(buffer, offset, max);
return read(buffer, offset, readLength);
}
}
}
......
......@@ -22,17 +22,17 @@
android:layout_height="match_parent"
android:layout_gravity="center">
<View android:id="@+id/shutter"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"/>
<com.google.android.exoplayer2.ui.SubtitleView android:id="@+id/subtitles"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</com.google.android.exoplayer2.ui.AspectRatioFrameLayout>
<View android:id="@+id/shutter"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"/>
<com.google.android.exoplayer2.ui.PlaybackControlView android:id="@+id/control"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
......
......@@ -14,8 +14,21 @@
limitations under the License.
-->
<resources>
<attr name="resize_mode" format="enum">
<enum name="fit" value="0"/>
<enum name="fixed_width" value="1"/>
<enum name="fixed_height" value="2"/>
</attr>
<declare-styleable name="SimpleExoPlayerView">
<attr name="use_controller" format="boolean"/>
<attr name="use_texture_view" format="boolean"/>
<attr name="resize_mode"/>
</declare-styleable>
<declare-styleable name="AspectRatioFrameLayout">
<attr name="resize_mode"/>
</declare-styleable>
</resources>
......@@ -17,8 +17,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.playbacktests"
android:versionCode="2001"
android:versionName="2.0.1">
android:versionCode="2002"
android:versionName="2.0.2">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
......
......@@ -19,6 +19,8 @@ import android.annotation.TargetApi;
import android.media.MediaDrm;
import android.media.UnsupportedSchemeException;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.test.ActivityInstrumentationTestCase2;
import android.util.Log;
import com.google.android.exoplayer2.C;
......@@ -801,7 +803,7 @@ public final class DashTest extends ActivityInstrumentationTestCase2<HostActivit
private DashTestTrackSelector(String audioFormatId, String[] videoFormatIds,
boolean canIncludeAdditionalVideoFormats) {
super(null);
super(new Handler(Looper.getMainLooper()));
this.audioFormatId = audioFormatId;
this.videoFormatIds = videoFormatIds;
this.canIncludeAdditionalVideoFormats = canIncludeAdditionalVideoFormats;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment