Merge pull request #2860 from google/dev-2.4.1-rc

r2.4.1
This commit is contained in:
ojw28 2017-05-23 18:12:22 +01:00 committed by GitHub
commit 9f81485053
51 changed files with 788 additions and 202 deletions

View File

@ -1,5 +1,28 @@
# Release notes # # Release notes #
### r2.4.1 ###
* Stability: Avoid OutOfMemoryError in extractors when parsing malformed media
([#2780](https://github.com/google/ExoPlayer/issues/2780)).
* Stability: Avoid native crash on Galaxy Nexus. Avoid unnecessarily large codec
input buffer allocations on all devices
([#2607](https://github.com/google/ExoPlayer/issues/2607)).
* Variable speed playback: Fix interpolation for rate/pitch adjustment
([#2774](https://github.com/google/ExoPlayer/issues/2774)).
* HLS: Include EXT-X-DATERANGE tags in HlsMediaPlaylist.
* HLS: Don't expose CEA-608 track if CLOSED-CAPTIONS=NONE
([#2743](https://github.com/google/ExoPlayer/issues/2743)).
* HLS: Correctly propagate errors loading the media playlist
([#2623](https://github.com/google/ExoPlayer/issues/2623)).
* UI: DefaultTimeBar enhancements and bug fixes
([#2740](https://github.com/google/ExoPlayer/issues/2740)).
* Ogg: Fix failure to play some Ogg files
([#2782](https://github.com/google/ExoPlayer/issues/2782)).
* Captions: Don't select text tack with no language by default.
* Captions: TTML positioning fixes
([#2824](https://github.com/google/ExoPlayer/issues/2824)).
* Misc bugfixes.
### r2.4.0 ### ### r2.4.0 ###
* New modular library structure. You can read more about depending on individual * New modular library structure. You can read more about depending on individual

View File

@ -16,7 +16,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:2.3.0' classpath 'com.android.tools.build:gradle:2.3.1'
classpath 'com.novoda:bintray-release:0.4.0' classpath 'com.novoda:bintray-release:0.4.0'
} }
// Workaround for the following test coverage issue. Remove when fixed: // Workaround for the following test coverage issue. Remove when fixed:
@ -48,7 +48,7 @@ allprojects {
releaseRepoName = getBintrayRepo() releaseRepoName = getBintrayRepo()
releaseUserOrg = 'google' releaseUserOrg = 'google'
releaseGroupId = 'com.google.android.exoplayer' releaseGroupId = 'com.google.android.exoplayer'
releaseVersion = 'r2.4.0' releaseVersion = 'r2.4.1'
releaseWebsite = 'https://github.com/google/ExoPlayer' releaseWebsite = 'https://github.com/google/ExoPlayer'
} }
if (it.hasProperty('externalBuildDir')) { if (it.hasProperty('externalBuildDir')) {

View File

@ -16,8 +16,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.demo" package="com.google.android.exoplayer2.demo"
android:versionCode="2400" android:versionCode="2401"
android:versionName="2.4.0"> android:versionName="2.4.1">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

View File

@ -234,7 +234,6 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
Intent intent = getIntent(); Intent intent = getIntent();
boolean needNewPlayer = player == null; boolean needNewPlayer = player == null;
if (needNewPlayer) { if (needNewPlayer) {
boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false);
UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA) UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA)
? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null; ? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null;
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null; DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null;
@ -253,6 +252,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
} }
} }
boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false);
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode = @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode =
((DemoApplication) getApplication()).useExtensionRenderers() ((DemoApplication) getApplication()).useExtensionRenderers()
? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER ? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
@ -261,10 +261,10 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this, DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this,
drmSessionManager, extensionRendererMode); drmSessionManager, extensionRendererMode);
TrackSelection.Factory videoTrackSelectionFactory = TrackSelection.Factory adaptiveTrackSelectionFactory =
new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory);
trackSelectionHelper = new TrackSelectionHelper(trackSelector, videoTrackSelectionFactory); trackSelectionHelper = new TrackSelectionHelper(trackSelector, adaptiveTrackSelectionFactory);
lastSeenTrackGroupArray = null; lastSeenTrackGroupArray = null;
player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);

View File

@ -286,7 +286,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
* *
* @param outputBufferTimeUs The timestamp of the current output buffer. * @param outputBufferTimeUs The timestamp of the current output buffer.
* @param nextOutputBufferTimeUs The timestamp of the next output buffer or * @param nextOutputBufferTimeUs The timestamp of the next output buffer or
* {@link TIME_UNSET} if the next output buffer is unavailable. * {@link C#TIME_UNSET} if the next output buffer is unavailable.
* @param positionUs The current playback position. * @param positionUs The current playback position.
* @param joiningDeadlineMs The joining deadline. * @param joiningDeadlineMs The joining deadline.
* @return Returns whether to drop the current output buffer. * @return Returns whether to drop the current output buffer.

View File

@ -51,8 +51,7 @@ config[3]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx"
config[3]+=" --disable-avx2 --enable-pic" config[3]+=" --disable-avx2 --enable-pic"
arch[4]="arm64-v8a" arch[4]="arm64-v8a"
config[4]="--force-target=armv8-android-gcc --sdk-path=$ndk --disable-neon" config[4]="--force-target=armv8-android-gcc --sdk-path=$ndk --enable-neon"
config[4]+=" --disable-neon-asm"
arch[5]="x86_64" arch[5]="x86_64"
config[5]="--force-target=x86_64-android-gcc --sdk-path=$ndk --disable-sse2" config[5]="--force-target=x86_64-android-gcc --sdk-path=$ndk --disable-sse2"

View File

@ -9,7 +9,7 @@ track 0:
id = null id = null
containerMimeType = null containerMimeType = null
sampleMimeType = audio/vorbis sampleMimeType = audio/vorbis
maxInputSize = 65025 maxInputSize = -1
width = -1 width = -1
height = -1 height = -1
frameRate = -1.0 frameRate = -1.0

View File

@ -9,7 +9,7 @@ track 0:
id = null id = null
containerMimeType = null containerMimeType = null
sampleMimeType = audio/vorbis sampleMimeType = audio/vorbis
maxInputSize = 65025 maxInputSize = -1
width = -1 width = -1
height = -1 height = -1
frameRate = -1.0 frameRate = -1.0

View File

@ -9,7 +9,7 @@ track 0:
id = null id = null
containerMimeType = null containerMimeType = null
sampleMimeType = audio/vorbis sampleMimeType = audio/vorbis
maxInputSize = 65025 maxInputSize = -1
width = -1 width = -1
height = -1 height = -1
frameRate = -1.0 frameRate = -1.0

View File

@ -9,7 +9,7 @@ track 0:
id = null id = null
containerMimeType = null containerMimeType = null
sampleMimeType = audio/vorbis sampleMimeType = audio/vorbis
maxInputSize = 65025 maxInputSize = -1
width = -1 width = -1
height = -1 height = -1
frameRate = -1.0 frameRate = -1.0

View File

@ -9,7 +9,7 @@ track 0:
id = null id = null
containerMimeType = null containerMimeType = null
sampleMimeType = audio/vorbis sampleMimeType = audio/vorbis
maxInputSize = 65025 maxInputSize = -1
width = -1 width = -1
height = -1 height = -1
frameRate = -1.0 frameRate = -1.0

View File

@ -75,7 +75,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
public void testCustomPesReader() throws Exception { public void testCustomPesReader() throws Exception {
CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(true, false); CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(true, false);
TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_NORMAL, new TimestampAdjuster(0), TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_MULTI_PMT, new TimestampAdjuster(0),
factory); factory);
FakeExtractorInput input = new FakeExtractorInput.Builder() FakeExtractorInput input = new FakeExtractorInput.Builder()
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts")) .setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts"))
@ -100,7 +100,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
public void testCustomInitialSectionReader() throws Exception { public void testCustomInitialSectionReader() throws Exception {
CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(false, true); CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(false, true);
TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_NORMAL, new TimestampAdjuster(0), TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_MULTI_PMT, new TimestampAdjuster(0),
factory); factory);
FakeExtractorInput input = new FakeExtractorInput.Builder() FakeExtractorInput input = new FakeExtractorInput.Builder()
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample_with_sdt.ts")) .setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample_with_sdt.ts"))

View File

@ -157,39 +157,39 @@ public final class TtmlDecoderTest extends InstrumentationTestCase {
assertEquals(2, output.size()); assertEquals(2, output.size());
Cue ttmlCue = output.get(0); Cue ttmlCue = output.get(0);
assertEquals("lorem", ttmlCue.text.toString()); assertEquals("lorem", ttmlCue.text.toString());
assertEquals(10.f / 100.f, ttmlCue.position); assertEquals(10f / 100f, ttmlCue.position);
assertEquals(10.f / 100.f, ttmlCue.line); assertEquals(10f / 100f, ttmlCue.line);
assertEquals(20.f / 100.f, ttmlCue.size); assertEquals(20f / 100f, ttmlCue.size);
ttmlCue = output.get(1); ttmlCue = output.get(1);
assertEquals("amet", ttmlCue.text.toString()); assertEquals("amet", ttmlCue.text.toString());
assertEquals(60.f / 100.f, ttmlCue.position); assertEquals(60f / 100f, ttmlCue.position);
assertEquals(10.f / 100.f, ttmlCue.line); assertEquals(10f / 100f, ttmlCue.line);
assertEquals(20.f / 100.f, ttmlCue.size); assertEquals(20f / 100f, ttmlCue.size);
output = subtitle.getCues(5000000); output = subtitle.getCues(5000000);
assertEquals(1, output.size()); assertEquals(1, output.size());
ttmlCue = output.get(0); ttmlCue = output.get(0);
assertEquals("ipsum", ttmlCue.text.toString()); assertEquals("ipsum", ttmlCue.text.toString());
assertEquals(40.f / 100.f, ttmlCue.position); assertEquals(40f / 100f, ttmlCue.position);
assertEquals(40.f / 100.f, ttmlCue.line); assertEquals(40f / 100f, ttmlCue.line);
assertEquals(20.f / 100.f, ttmlCue.size); assertEquals(20f / 100f, ttmlCue.size);
output = subtitle.getCues(9000000); output = subtitle.getCues(9000000);
assertEquals(1, output.size()); assertEquals(1, output.size());
ttmlCue = output.get(0); ttmlCue = output.get(0);
assertEquals("dolor", ttmlCue.text.toString()); assertEquals("dolor", ttmlCue.text.toString());
assertEquals(10.f / 100.f, ttmlCue.position); assertEquals(10f / 100f, ttmlCue.position);
assertEquals(80.f / 100.f, ttmlCue.line); assertEquals(80f / 100f, ttmlCue.line);
assertEquals(Cue.DIMEN_UNSET, ttmlCue.size); assertEquals(1f, ttmlCue.size);
output = subtitle.getCues(21000000); output = subtitle.getCues(21000000);
assertEquals(1, output.size()); assertEquals(1, output.size());
ttmlCue = output.get(0); ttmlCue = output.get(0);
assertEquals("She first said this", ttmlCue.text.toString()); assertEquals("She first said this", ttmlCue.text.toString());
assertEquals(45.f / 100.f, ttmlCue.position); assertEquals(45f / 100f, ttmlCue.position);
assertEquals(45.f / 100.f, ttmlCue.line); assertEquals(45f / 100f, ttmlCue.line);
assertEquals(35.f / 100.f, ttmlCue.size); assertEquals(35f / 100f, ttmlCue.size);
output = subtitle.getCues(25000000); output = subtitle.getCues(25000000);
ttmlCue = output.get(0); ttmlCue = output.get(0);
assertEquals("She first said this\nThen this", ttmlCue.text.toString()); assertEquals("She first said this\nThen this", ttmlCue.text.toString());
@ -197,8 +197,8 @@ public final class TtmlDecoderTest extends InstrumentationTestCase {
assertEquals(1, output.size()); assertEquals(1, output.size());
ttmlCue = output.get(0); ttmlCue = output.get(0);
assertEquals("She first said this\nThen this\nFinally this", ttmlCue.text.toString()); assertEquals("She first said this\nThen this\nFinally this", ttmlCue.text.toString());
assertEquals(45.f / 100.f, ttmlCue.position); assertEquals(45f / 100f, ttmlCue.position);
assertEquals(45.f / 100.f, ttmlCue.line); assertEquals(45f / 100f, ttmlCue.line);
} }
public void testEmptyStyleAttribute() throws IOException, SubtitleDecoderException { public void testEmptyStyleAttribute() throws IOException, SubtitleDecoderException {

View File

@ -142,9 +142,9 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
public final void disable() { public final void disable() {
Assertions.checkState(state == STATE_ENABLED); Assertions.checkState(state == STATE_ENABLED);
state = STATE_DISABLED; state = STATE_DISABLED;
onDisabled();
stream = null; stream = null;
streamIsFinal = false; streamIsFinal = false;
onDisabled();
} }
// RendererCapabilities implementation. // RendererCapabilities implementation.
@ -300,8 +300,6 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
/** /**
* Returns whether the upstream source is ready. * Returns whether the upstream source is ready.
*
* @return Whether the source is ready.
*/ */
protected final boolean isSourceReady() { protected final boolean isSourceReady() {
return readEndOfStream ? streamIsFinal : stream.isReady(); return readEndOfStream ? streamIsFinal : stream.isReady();

View File

@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import android.os.Looper;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.metadata.MetadataRenderer;
@ -88,7 +90,9 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
* thread. The application's main thread is ideal. Accessing an instance from multiple threads is * thread. The application's main thread is ideal. Accessing an instance from multiple threads is
* discouraged, however if an application does wish to do this then it may do so provided that it * discouraged, however if an application does wish to do this then it may do so provided that it
* ensures accesses are synchronized.</li> * ensures accesses are synchronized.</li>
* <li>Registered listeners are called on the thread that created the ExoPlayer instance.</li> * <li>Registered listeners are called on the thread that created the ExoPlayer instance, unless
* the thread that created the ExoPlayer instance does not have a {@link Looper}. In that case,
* registered listeners will be called on the application's main thread.</li>
* <li>An internal playback thread is responsible for playback. Injected player components such as * <li>An internal playback thread is responsible for playback. Injected player components such as
* Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this
* thread.</li> * thread.</li>
@ -113,8 +117,8 @@ public interface ExoPlayer {
* Called when the timeline and/or manifest has been refreshed. * Called when the timeline and/or manifest has been refreshed.
* <p> * <p>
* Note that if the timeline has changed then a position discontinuity may also have occurred. * Note that if the timeline has changed then a position discontinuity may also have occurred.
* For example the current period index may have changed as a result of periods being added or * For example, the current period index may have changed as a result of periods being added or
* removed from the timeline. The will <em>not</em> be reported via a separate call to * removed from the timeline. This will <em>not</em> be reported via a separate call to
* {@link #onPositionDiscontinuity()}. * {@link #onPositionDiscontinuity()}.
* *
* @param timeline The latest timeline. Never null, but may be empty. * @param timeline The latest timeline. Never null, but may be empty.
@ -253,7 +257,8 @@ public interface ExoPlayer {
/** /**
* Register a listener to receive events from the player. The listener's methods will be called on * Register a listener to receive events from the player. The listener's methods will be called on
* the thread that was used to construct the player. * the thread that was used to construct the player. However, if the thread used to construct the
* player does not have a {@link Looper}, then the listener will be called on the main thread.
* *
* @param listener The listener to register. * @param listener The listener to register.
*/ */

View File

@ -16,7 +16,6 @@
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import android.content.Context; import android.content.Context;
import android.os.Looper;
import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelector;
@ -29,8 +28,7 @@ public final class ExoPlayerFactory {
private ExoPlayerFactory() {} private ExoPlayerFactory() {}
/** /**
* Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated * Creates a {@link SimpleExoPlayer} instance.
* {@link Looper}.
* *
* @param context A {@link Context}. * @param context A {@link Context}.
* @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance.
@ -45,8 +43,7 @@ public final class ExoPlayerFactory {
} }
/** /**
* Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated * Creates a {@link SimpleExoPlayer} instance. Available extension renderers are not used.
* {@link Looper}. Available extension renderers are not used.
* *
* @param context A {@link Context}. * @param context A {@link Context}.
* @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance.
@ -63,8 +60,7 @@ public final class ExoPlayerFactory {
} }
/** /**
* Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated * Creates a {@link SimpleExoPlayer} instance.
* {@link Looper}.
* *
* @param context A {@link Context}. * @param context A {@link Context}.
* @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance.
@ -86,8 +82,7 @@ public final class ExoPlayerFactory {
} }
/** /**
* Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated * Creates a {@link SimpleExoPlayer} instance.
* {@link Looper}.
* *
* @param context A {@link Context}. * @param context A {@link Context}.
* @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance.
@ -112,8 +107,7 @@ public final class ExoPlayerFactory {
} }
/** /**
* Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated * Creates a {@link SimpleExoPlayer} instance.
* {@link Looper}.
* *
* @param context A {@link Context}. * @param context A {@link Context}.
* @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance.
@ -123,8 +117,7 @@ public final class ExoPlayerFactory {
} }
/** /**
* Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated * Creates a {@link SimpleExoPlayer} instance.
* {@link Looper}.
* *
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
* @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance.
@ -135,8 +128,7 @@ public final class ExoPlayerFactory {
} }
/** /**
* Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated * Creates a {@link SimpleExoPlayer} instance.
* {@link Looper}.
* *
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
* @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance.
@ -148,8 +140,7 @@ public final class ExoPlayerFactory {
} }
/** /**
* Creates an {@link ExoPlayer} instance. Must be called from a thread that has an associated * Creates an {@link ExoPlayer} instance.
* {@link Looper}.
* *
* @param renderers The {@link Renderer}s that will be used by the instance. * @param renderers The {@link Renderer}s that will be used by the instance.
* @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance.
@ -159,8 +150,7 @@ public final class ExoPlayerFactory {
} }
/** /**
* Creates an {@link ExoPlayer} instance. Must be called from a thread that has an associated * Creates an {@link ExoPlayer} instance.
* {@link Looper}.
* *
* @param renderers The {@link Renderer}s that will be used by the instance. * @param renderers The {@link Renderer}s that will be used by the instance.
* @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance.

View File

@ -92,7 +92,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
trackGroups = TrackGroupArray.EMPTY; trackGroups = TrackGroupArray.EMPTY;
trackSelections = emptyTrackSelections; trackSelections = emptyTrackSelections;
playbackParameters = PlaybackParameters.DEFAULT; playbackParameters = PlaybackParameters.DEFAULT;
eventHandler = new Handler() { Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper();
eventHandler = new Handler(eventLooper) {
@Override @Override
public void handleMessage(Message msg) { public void handleMessage(Message msg) {
ExoPlayerImpl.this.handleEvent(msg); ExoPlayerImpl.this.handleEvent(msg);

View File

@ -24,13 +24,13 @@ public interface ExoPlayerLibraryInfo {
* The version of the library expressed as a string, for example "1.2.3". * The version of the library expressed as a string, for example "1.2.3".
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
String VERSION = "2.4.0"; String VERSION = "2.4.1";
/** /**
* The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}.
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
String VERSION_SLASHY = "ExoPlayerLib/2.4.0"; String VERSION_SLASHY = "ExoPlayerLib/2.4.1";
/** /**
* The version of the library expressed as an integer, for example 1002003. * The version of the library expressed as an integer, for example 1002003.
@ -40,7 +40,7 @@ public interface ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006). * integer version 123045006 (123-045-006).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
int VERSION_INT = 2004000; int VERSION_INT = 2004001;
/** /**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}

View File

@ -20,6 +20,7 @@ import android.graphics.SurfaceTexture;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.PlaybackParams; import android.media.PlaybackParams;
import android.os.Handler; import android.os.Handler;
import android.os.Looper;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
import android.view.Surface; import android.view.Surface;
@ -111,8 +112,10 @@ public class SimpleExoPlayer implements ExoPlayer {
protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector, protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector,
LoadControl loadControl) { LoadControl loadControl) {
componentListener = new ComponentListener(); componentListener = new ComponentListener();
renderers = renderersFactory.createRenderers(new Handler(), componentListener, Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper();
componentListener, componentListener, componentListener); Handler eventHandler = new Handler(eventLooper);
renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener,
componentListener, componentListener);
// Obtain counts of video and audio renderers. // Obtain counts of video and audio renderers.
int videoRendererCount = 0; int videoRendererCount = 0;

View File

@ -374,8 +374,8 @@ import java.util.Arrays;
} }
private short interpolate(short[] in, int inPos, int oldSampleRate, int newSampleRate) { private short interpolate(short[] in, int inPos, int oldSampleRate, int newSampleRate) {
short left = in[inPos * numChannels]; short left = in[inPos];
short right = in[inPos * numChannels + numChannels]; short right = in[inPos + numChannels];
int position = newRatePosition * oldSampleRate; int position = newRatePosition * oldSampleRate;
int leftPosition = oldRatePosition * newSampleRate; int leftPosition = oldRatePosition * newSampleRate;
int rightPosition = (oldRatePosition + 1) * newSampleRate; int rightPosition = (oldRatePosition + 1) * newSampleRate;
@ -402,7 +402,7 @@ import java.util.Arrays;
enlargeOutputBufferIfNeeded(1); enlargeOutputBufferIfNeeded(1);
for (int i = 0; i < numChannels; i++) { for (int i = 0; i < numChannels; i++) {
outputBuffer[numOutputSamples * numChannels + i] = outputBuffer[numOutputSamples * numChannels + i] =
interpolate(pitchBuffer, position + i, oldSampleRate, newSampleRate); interpolate(pitchBuffer, position * numChannels + i, oldSampleRate, newSampleRate);
} }
newRatePosition++; newRatePosition++;
numOutputSamples++; numOutputSamples++;

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.decoder; package com.google.android.exoplayer2.decoder;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/** /**
* Buffer for {@link SimpleDecoder} output. * Buffer for {@link SimpleDecoder} output.
@ -40,7 +41,7 @@ public class SimpleOutputBuffer extends OutputBuffer {
public ByteBuffer init(long timeUs, int size) { public ByteBuffer init(long timeUs, int size) {
this.timeUs = timeUs; this.timeUs = timeUs;
if (data == null || data.capacity() < size) { if (data == null || data.capacity() < size) {
data = ByteBuffer.allocateDirect(size); data = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
} }
data.position(0); data.position(0);
data.limit(size); data.limit(size);

View File

@ -26,7 +26,9 @@ import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory;
import com.google.android.exoplayer2.extractor.ts.PsExtractor; import com.google.android.exoplayer2.extractor.ts.PsExtractor;
import com.google.android.exoplayer2.extractor.ts.TsExtractor; import com.google.android.exoplayer2.extractor.ts.TsExtractor;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader;
import com.google.android.exoplayer2.extractor.wav.WavExtractor; import com.google.android.exoplayer2.extractor.wav.WavExtractor;
import com.google.android.exoplayer2.util.TimestampAdjuster;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
/** /**
@ -67,8 +69,13 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
private @MatroskaExtractor.Flags int matroskaFlags; private @MatroskaExtractor.Flags int matroskaFlags;
private @FragmentedMp4Extractor.Flags int fragmentedMp4Flags; private @FragmentedMp4Extractor.Flags int fragmentedMp4Flags;
private @Mp3Extractor.Flags int mp3Flags; private @Mp3Extractor.Flags int mp3Flags;
private @TsExtractor.Mode int tsMode;
private @DefaultTsPayloadReaderFactory.Flags int tsFlags; private @DefaultTsPayloadReaderFactory.Flags int tsFlags;
public DefaultExtractorsFactory() {
tsMode = TsExtractor.MODE_SINGLE_PMT;
}
/** /**
* Sets flags for {@link MatroskaExtractor} instances created by the factory. * Sets flags for {@link MatroskaExtractor} instances created by the factory.
* *
@ -107,6 +114,18 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
return this; return this;
} }
/**
* Sets the mode for {@link TsExtractor} instances created by the factory.
*
* @see TsExtractor#TsExtractor(int, TimestampAdjuster, TsPayloadReader.Factory).
* @param mode The mode to use.
* @return The factory, for convenience.
*/
public synchronized DefaultExtractorsFactory setTsExtractorMode(@TsExtractor.Mode int mode) {
tsMode = mode;
return this;
}
/** /**
* Sets flags for {@link DefaultTsPayloadReaderFactory}s used by {@link TsExtractor} instances * Sets flags for {@link DefaultTsPayloadReaderFactory}s used by {@link TsExtractor} instances
* created by the factory. * created by the factory.
@ -130,7 +149,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
extractors[3] = new Mp3Extractor(mp3Flags); extractors[3] = new Mp3Extractor(mp3Flags);
extractors[4] = new AdtsExtractor(); extractors[4] = new AdtsExtractor();
extractors[5] = new Ac3Extractor(); extractors[5] = new Ac3Extractor();
extractors[6] = new TsExtractor(tsFlags); extractors[6] = new TsExtractor(tsMode, tsFlags);
extractors[7] = new FlvExtractor(); extractors[7] = new FlvExtractor();
extractors[8] = new OggExtractor(); extractors[8] = new OggExtractor();
extractors[9] = new PsExtractor(); extractors[9] = new PsExtractor();

View File

@ -20,6 +20,7 @@ import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
/** /**
* OGG packet class. * OGG packet class.
@ -27,8 +28,8 @@ import java.io.IOException;
/* package */ final class OggPacket { /* package */ final class OggPacket {
private final OggPageHeader pageHeader = new OggPageHeader(); private final OggPageHeader pageHeader = new OggPageHeader();
private final ParsableByteArray packetArray = private final ParsableByteArray packetArray = new ParsableByteArray(
new ParsableByteArray(new byte[OggPageHeader.MAX_PAGE_PAYLOAD], 0); new byte[OggPageHeader.MAX_PAGE_PAYLOAD], 0);
private int currentSegmentIndex = C.INDEX_UNSET; private int currentSegmentIndex = C.INDEX_UNSET;
private int segmentCount; private int segmentCount;
@ -85,6 +86,9 @@ import java.io.IOException;
int size = calculatePacketSize(currentSegmentIndex); int size = calculatePacketSize(currentSegmentIndex);
int segmentIndex = currentSegmentIndex + segmentCount; int segmentIndex = currentSegmentIndex + segmentCount;
if (size > 0) { if (size > 0) {
if (packetArray.capacity() < packetArray.limit() + size) {
packetArray.data = Arrays.copyOf(packetArray.data, packetArray.limit() + size);
}
input.readFully(packetArray.data, packetArray.limit(), size); input.readFully(packetArray.data, packetArray.limit(), size);
packetArray.setLimit(packetArray.limit() + size); packetArray.setLimit(packetArray.limit() + size);
populated = pageHeader.laces[segmentIndex - 1] != 255; populated = pageHeader.laces[segmentIndex - 1] != 255;
@ -118,6 +122,17 @@ import java.io.IOException;
return packetArray; return packetArray;
} }
/**
* Trims the packet data array.
*/
public void trimPayload() {
if (packetArray.data.length == OggPageHeader.MAX_PAGE_PAYLOAD) {
return;
}
packetArray.data = Arrays.copyOf(packetArray.data, Math.max(OggPageHeader.MAX_PAGE_PAYLOAD,
packetArray.limit()));
}
/** /**
* Calculates the size of the packet starting from {@code startSegmentIndex}. * Calculates the size of the packet starting from {@code startSegmentIndex}.
* *

View File

@ -103,15 +103,12 @@ import java.io.IOException;
switch (state) { switch (state) {
case STATE_READ_HEADERS: case STATE_READ_HEADERS:
return readHeaders(input); return readHeaders(input);
case STATE_SKIP_HEADERS: case STATE_SKIP_HEADERS:
input.skipFully((int) payloadStartPosition); input.skipFully((int) payloadStartPosition);
state = STATE_READ_PAYLOAD; state = STATE_READ_PAYLOAD;
return Extractor.RESULT_CONTINUE; return Extractor.RESULT_CONTINUE;
case STATE_READ_PAYLOAD: case STATE_READ_PAYLOAD:
return readPayload(input, seekPosition); return readPayload(input, seekPosition);
default: default:
// Never happens. // Never happens.
throw new IllegalStateException(); throw new IllegalStateException();
@ -152,6 +149,8 @@ import java.io.IOException;
setupData = null; setupData = null;
state = STATE_READ_PAYLOAD; state = STATE_READ_PAYLOAD;
// First payload packet. Trim the payload array of the ogg packet after headers have been read.
oggPacket.trimPayload();
return Extractor.RESULT_CONTINUE; return Extractor.RESULT_CONTINUE;
} }

View File

@ -101,7 +101,7 @@ import java.util.ArrayList;
codecInitialisationData.add(vorbisSetup.setupHeaderData); codecInitialisationData.add(vorbisSetup.setupHeaderData);
setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS, null, setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS, null,
this.vorbisSetup.idHeader.bitrateNominal, OggPageHeader.MAX_PAGE_PAYLOAD, this.vorbisSetup.idHeader.bitrateNominal, Format.NO_VALUE,
this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate, this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate,
codecInitialisationData, null, 0, null); codecInitialisationData, null, 0, null);
return true; return true;

View File

@ -65,13 +65,13 @@ public final class TsExtractor implements Extractor {
* Modes for the extractor. * Modes for the extractor.
*/ */
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef({MODE_NORMAL, MODE_SINGLE_PMT, MODE_HLS}) @IntDef({MODE_MULTI_PMT, MODE_SINGLE_PMT, MODE_HLS})
public @interface Mode {} public @interface Mode {}
/** /**
* Behave as defined in ISO/IEC 13818-1. * Behave as defined in ISO/IEC 13818-1.
*/ */
public static final int MODE_NORMAL = 0; public static final int MODE_MULTI_PMT = 0;
/** /**
* Assume only one PMT will be contained in the stream, even if more are declared by the PAT. * Assume only one PMT will be contained in the stream, even if more are declared by the PAT.
*/ */
@ -132,12 +132,23 @@ public final class TsExtractor implements Extractor {
* {@code FLAG_*} values that control the behavior of the payload readers. * {@code FLAG_*} values that control the behavior of the payload readers.
*/ */
public TsExtractor(@Flags int defaultTsPayloadReaderFlags) { public TsExtractor(@Flags int defaultTsPayloadReaderFlags) {
this(MODE_NORMAL, new TimestampAdjuster(0), this(MODE_SINGLE_PMT, defaultTsPayloadReaderFlags);
new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags));
} }
/** /**
* @param mode Mode for the extractor. One of {@link #MODE_NORMAL}, {@link #MODE_SINGLE_PMT} * @param mode Mode for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT}
* and {@link #MODE_HLS}.
* @param defaultTsPayloadReaderFlags A combination of {@link DefaultTsPayloadReaderFactory}
* {@code FLAG_*} values that control the behavior of the payload readers.
*/
public TsExtractor(@Mode int mode, @Flags int defaultTsPayloadReaderFlags) {
this(mode, new TimestampAdjuster(0),
new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags));
}
/**
* @param mode Mode for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT}
* and {@link #MODE_HLS}. * and {@link #MODE_HLS}.
* @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.
* @param payloadReaderFactory Factory for injecting a custom set of payload readers. * @param payloadReaderFactory Factory for injecting a custom set of payload readers.

View File

@ -488,7 +488,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
} }
if (format == null) { if (format == null) {
// We don't have a format yet, so try and read one. // We don't have a format yet, so try and read one.
buffer.clear(); flagsOnlyBuffer.clear();
int result = readSource(formatHolder, flagsOnlyBuffer, true); int result = readSource(formatHolder, flagsOnlyBuffer, true);
if (result == C.RESULT_FORMAT_READ) { if (result == C.RESULT_FORMAT_READ) {
onInputFormatChanged(formatHolder.format); onInputFormatChanged(formatHolder.format);

View File

@ -429,6 +429,7 @@ public final class MediaCodecUtil {
case CodecProfileLevel.AVCLevel42: return 8704 * 16 * 16; case CodecProfileLevel.AVCLevel42: return 8704 * 16 * 16;
case CodecProfileLevel.AVCLevel5: return 22080 * 16 * 16; case CodecProfileLevel.AVCLevel5: return 22080 * 16 * 16;
case CodecProfileLevel.AVCLevel51: return 36864 * 16 * 16; case CodecProfileLevel.AVCLevel51: return 36864 * 16 * 16;
case CodecProfileLevel.AVCLevel52: return 36864 * 16 * 16;
default: return -1; default: return -1;
} }
} }

View File

@ -153,7 +153,6 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
protected void onDisabled() { protected void onDisabled() {
flushPendingMetadata(); flushPendingMetadata();
decoder = null; decoder = null;
super.onDisabled();
} }
@Override @Override

View File

@ -254,7 +254,6 @@ public final class TextRenderer extends BaseRenderer implements Callback {
streamFormat = null; streamFormat = null;
clearOutput(); clearOutput();
releaseDecoder(); releaseDecoder();
super.onDisabled();
} }
@Override @Override

View File

@ -17,7 +17,6 @@ package com.google.android.exoplayer2.text.ttml;
import android.text.Layout; import android.text.Layout;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
@ -100,7 +99,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
XmlPullParser xmlParser = xmlParserFactory.newPullParser(); XmlPullParser xmlParser = xmlParserFactory.newPullParser();
Map<String, TtmlStyle> globalStyles = new HashMap<>(); Map<String, TtmlStyle> globalStyles = new HashMap<>();
Map<String, TtmlRegion> regionMap = new HashMap<>(); Map<String, TtmlRegion> regionMap = new HashMap<>();
regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion()); regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion(null));
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length); ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length);
xmlParser.setInput(inputStream, null); xmlParser.setInput(inputStream, null);
TtmlSubtitle ttmlSubtitle = null; TtmlSubtitle ttmlSubtitle = null;
@ -211,9 +210,9 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
globalStyles.put(style.getId(), style); globalStyles.put(style.getId(), style);
} }
} else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) { } else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) {
Pair<String, TtmlRegion> ttmlRegionInfo = parseRegionAttributes(xmlParser); TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser);
if (ttmlRegionInfo != null) { if (ttmlRegion != null) {
globalRegions.put(ttmlRegionInfo.first, ttmlRegionInfo.second); globalRegions.put(ttmlRegion.id, ttmlRegion);
} }
} }
} while (!XmlPullParserUtil.isEndTag(xmlParser, TtmlNode.TAG_HEAD)); } while (!XmlPullParserUtil.isEndTag(xmlParser, TtmlNode.TAG_HEAD));
@ -221,41 +220,84 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
} }
/** /**
* Parses a region declaration. Supports origin and extent definition but only when defined in * Parses a region declaration.
* terms of percentage of the viewport. Regions that do not correctly declare origin are ignored. * <p>
* If the region defines an origin and/or extent, it is required that they're defined as
* percentages of the viewport. Region declarations that define origin and/or extent in other
* formats are unsupported, and null is returned.
*/ */
private Pair<String, TtmlRegion> parseRegionAttributes(XmlPullParser xmlParser) { private TtmlRegion parseRegionAttributes(XmlPullParser xmlParser) {
String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID); String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID);
String regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN); if (regionId == null) {
String regionExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT);
if (regionOrigin == null || regionId == null) {
return null; return null;
} }
float position = Cue.DIMEN_UNSET;
float line = Cue.DIMEN_UNSET; float position;
Matcher originMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin); float line;
if (originMatcher.matches()) { String regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN);
try { if (regionOrigin != null) {
position = Float.parseFloat(originMatcher.group(1)) / 100.f; Matcher originMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin);
line = Float.parseFloat(originMatcher.group(2)) / 100.f; if (originMatcher.matches()) {
} catch (NumberFormatException e) { try {
Log.w(TAG, "Ignoring region with malformed origin: '" + regionOrigin + "'", e); position = Float.parseFloat(originMatcher.group(1)) / 100f;
position = Cue.DIMEN_UNSET; line = Float.parseFloat(originMatcher.group(2)) / 100f;
} catch (NumberFormatException e) {
Log.w(TAG, "Ignoring region with malformed origin: " + regionOrigin);
return null;
}
} else {
Log.w(TAG, "Ignoring region with unsupported origin: " + regionOrigin);
return null;
} }
} else {
// Origin is omitted. Default to top left.
position = 0;
line = 0;
} }
float width = Cue.DIMEN_UNSET;
float width;
float height;
String regionExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT);
if (regionExtent != null) { if (regionExtent != null) {
Matcher extentMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent); Matcher extentMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent);
if (extentMatcher.matches()) { if (extentMatcher.matches()) {
try { try {
width = Float.parseFloat(extentMatcher.group(1)) / 100.f; width = Float.parseFloat(extentMatcher.group(1)) / 100f;
height = Float.parseFloat(extentMatcher.group(2)) / 100f;
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
Log.w(TAG, "Ignoring malformed region extent: '" + regionExtent + "'", e); Log.w(TAG, "Ignoring region with malformed extent: " + regionOrigin);
return null;
} }
} else {
Log.w(TAG, "Ignoring region with unsupported extent: " + regionOrigin);
return null;
}
} else {
// Extent is omitted. Default to extent of parent.
width = 1;
height = 1;
}
@Cue.AnchorType int lineAnchor = Cue.ANCHOR_TYPE_START;
String displayAlign = XmlPullParserUtil.getAttributeValue(xmlParser,
TtmlNode.ATTR_TTS_DISPLAY_ALIGN);
if (displayAlign != null) {
switch (displayAlign.toLowerCase()) {
case "center":
lineAnchor = Cue.ANCHOR_TYPE_MIDDLE;
line += height / 2;
break;
case "after":
lineAnchor = Cue.ANCHOR_TYPE_END;
line += height;
break;
default:
// Default "before" case. Do nothing.
break;
} }
} }
return position != Cue.DIMEN_UNSET ? new Pair<>(regionId, new TtmlRegion(position, line,
Cue.LINE_TYPE_FRACTION, width)) : null; return new TtmlRegion(regionId, position, line, Cue.LINE_TYPE_FRACTION, lineAnchor, width);
} }
private String[] parseStyleIds(String parentStyleIds) { private String[] parseStyleIds(String parentStyleIds) {
@ -277,7 +319,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
try { try {
style.setBackgroundColor(ColorParser.parseTtmlColor(attributeValue)); style.setBackgroundColor(ColorParser.parseTtmlColor(attributeValue));
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
Log.w(TAG, "failed parsing background value: '" + attributeValue + "'"); Log.w(TAG, "Failed parsing background value: " + attributeValue);
} }
break; break;
case TtmlNode.ATTR_TTS_COLOR: case TtmlNode.ATTR_TTS_COLOR:
@ -285,7 +327,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
try { try {
style.setFontColor(ColorParser.parseTtmlColor(attributeValue)); style.setFontColor(ColorParser.parseTtmlColor(attributeValue));
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
Log.w(TAG, "failed parsing color value: '" + attributeValue + "'"); Log.w(TAG, "Failed parsing color value: " + attributeValue);
} }
break; break;
case TtmlNode.ATTR_TTS_FONT_FAMILY: case TtmlNode.ATTR_TTS_FONT_FAMILY:
@ -296,7 +338,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
style = createIfNull(style); style = createIfNull(style);
parseFontSize(attributeValue, style); parseFontSize(attributeValue, style);
} catch (SubtitleDecoderException e) { } catch (SubtitleDecoderException e) {
Log.w(TAG, "failed parsing fontSize value: '" + attributeValue + "'"); Log.w(TAG, "Failed parsing fontSize value: " + attributeValue);
} }
break; break;
case TtmlNode.ATTR_TTS_FONT_WEIGHT: case TtmlNode.ATTR_TTS_FONT_WEIGHT:

View File

@ -50,14 +50,15 @@ import java.util.TreeSet;
public static final String ANONYMOUS_REGION_ID = ""; public static final String ANONYMOUS_REGION_ID = "";
public static final String ATTR_ID = "id"; public static final String ATTR_ID = "id";
public static final String ATTR_TTS_BACKGROUND_COLOR = "backgroundColor"; public static final String ATTR_TTS_ORIGIN = "origin";
public static final String ATTR_TTS_EXTENT = "extent"; public static final String ATTR_TTS_EXTENT = "extent";
public static final String ATTR_TTS_DISPLAY_ALIGN = "displayAlign";
public static final String ATTR_TTS_BACKGROUND_COLOR = "backgroundColor";
public static final String ATTR_TTS_FONT_STYLE = "fontStyle"; public static final String ATTR_TTS_FONT_STYLE = "fontStyle";
public static final String ATTR_TTS_FONT_SIZE = "fontSize"; public static final String ATTR_TTS_FONT_SIZE = "fontSize";
public static final String ATTR_TTS_FONT_FAMILY = "fontFamily"; public static final String ATTR_TTS_FONT_FAMILY = "fontFamily";
public static final String ATTR_TTS_FONT_WEIGHT = "fontWeight"; public static final String ATTR_TTS_FONT_WEIGHT = "fontWeight";
public static final String ATTR_TTS_COLOR = "color"; public static final String ATTR_TTS_COLOR = "color";
public static final String ATTR_TTS_ORIGIN = "origin";
public static final String ATTR_TTS_TEXT_DECORATION = "textDecoration"; public static final String ATTR_TTS_TEXT_DECORATION = "textDecoration";
public static final String ATTR_TTS_TEXT_ALIGN = "textAlign"; public static final String ATTR_TTS_TEXT_ALIGN = "textAlign";
@ -179,7 +180,7 @@ import java.util.TreeSet;
for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) { for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
TtmlRegion region = regionMap.get(entry.getKey()); TtmlRegion region = regionMap.get(entry.getKey());
cues.add(new Cue(cleanUpText(entry.getValue()), null, region.line, region.lineType, cues.add(new Cue(cleanUpText(entry.getValue()), null, region.line, region.lineType,
Cue.TYPE_UNSET, region.position, Cue.TYPE_UNSET, region.width)); region.lineAnchor, region.position, Cue.TYPE_UNSET, region.width));
} }
return cues; return cues;
} }

View File

@ -22,20 +22,24 @@ import com.google.android.exoplayer2.text.Cue;
*/ */
/* package */ final class TtmlRegion { /* package */ final class TtmlRegion {
public final String id;
public final float position; public final float position;
public final float line; public final float line;
@Cue.LineType @Cue.LineType public final int lineType;
public final int lineType; @Cue.AnchorType public final int lineAnchor;
public final float width; public final float width;
public TtmlRegion() { public TtmlRegion(String id) {
this(Cue.DIMEN_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET); this(id, Cue.DIMEN_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
} }
public TtmlRegion(float position, float line, @Cue.LineType int lineType, float width) { public TtmlRegion(String id, float position, float line, @Cue.LineType int lineType,
@Cue.AnchorType int lineAnchor, float width) {
this.id = id;
this.position = position; this.position = position;
this.line = line; this.line = line;
this.lineType = lineType; this.lineType = lineType;
this.lineAnchor = lineAnchor;
this.width = width; this.width = width;
} }

View File

@ -24,6 +24,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList; import java.util.ArrayList;
@ -376,10 +377,21 @@ public class DefaultTrackSelector extends MappingTrackSelector {
private final AtomicReference<Parameters> paramsReference; private final AtomicReference<Parameters> paramsReference;
/** /**
* Constructs an instance that does not support adaptive tracks. * Constructs an instance that does not support adaptive track selection.
*/ */
public DefaultTrackSelector() { public DefaultTrackSelector() {
this(null); this((TrackSelection.Factory) null);
}
/**
* Constructs an instance that supports adaptive track selection. Adaptive track selections use
* the provided {@link BandwidthMeter} to determine which individual track should be used during
* playback.
*
* @param bandwidthMeter The {@link BandwidthMeter}.
*/
public DefaultTrackSelector(BandwidthMeter bandwidthMeter) {
this(new AdaptiveTrackSelection.Factory(bandwidthMeter));
} }
/** /**
@ -867,7 +879,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
} }
protected static boolean formatHasLanguage(Format format, String language) { protected static boolean formatHasLanguage(Format format, String language) {
return TextUtils.equals(language, Util.normalizeLanguageCode(format.language)); return language != null
&& TextUtils.equals(language, Util.normalizeLanguageCode(format.language));
} }
// Viewport size util methods. // Viewport size util methods.

View File

@ -33,11 +33,11 @@ import java.util.concurrent.ExecutorService;
public final class Loader implements LoaderErrorThrower { public final class Loader implements LoaderErrorThrower {
/** /**
* Thrown when an unexpected exception is encountered during loading. * Thrown when an unexpected exception or error is encountered during loading.
*/ */
public static final class UnexpectedLoaderException extends IOException { public static final class UnexpectedLoaderException extends IOException {
public UnexpectedLoaderException(Exception cause) { public UnexpectedLoaderException(Throwable cause) {
super("Unexpected " + cause.getClass().getSimpleName() + ": " + cause.getMessage(), cause); super("Unexpected " + cause.getClass().getSimpleName() + ": " + cause.getMessage(), cause);
} }
@ -316,6 +316,14 @@ public final class Loader implements LoaderErrorThrower {
if (!released) { if (!released) {
obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget(); obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget();
} }
} catch (OutOfMemoryError e) {
// This can occur if a stream is malformed in a way that causes an extractor to think it
// needs to allocate a large amount of memory. We don't want the process to die in this
// case, but we do want the playback to fail.
Log.e(TAG, "OutOfMemory error loading stream", e);
if (!released) {
obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget();
}
} catch (Error e) { } catch (Error e) {
// We'd hope that the platform would kill the process if an Error is thrown here, but the // We'd hope that the platform would kill the process if an Error is thrown here, but the
// executor may catch the error (b/20616433). Throw it here, but also pass and throw it from // executor may catch the error (b/20616433). Throw it here, but also pass and throw it from

View File

@ -54,8 +54,8 @@ public final class CacheDataSource implements DataSource {
FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}) FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS})
public @interface Flags {} public @interface Flags {}
/** /**
* A flag indicating whether we will block reads if the cache key is locked. If this flag is * A flag indicating whether we will block reads if the cache key is locked. If unset then data is
* set, then we will read from upstream if the cache key is locked. * read from upstream if the cache key is locked, regardless of whether the data is cached.
*/ */
public static final int FLAG_BLOCK_ON_CACHE = 1 << 0; public static final int FLAG_BLOCK_ON_CACHE = 1 << 0;
@ -110,7 +110,23 @@ public final class CacheDataSource implements DataSource {
/** /**
* Constructs an instance with default {@link DataSource} and {@link DataSink} instances for * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for
* reading and writing the cache and with {@link #DEFAULT_MAX_CACHE_FILE_SIZE}. * reading and writing the cache.
*
* @param cache The cache.
* @param upstream A {@link DataSource} for reading data not in the cache.
*/
public CacheDataSource(Cache cache, DataSource upstream) {
this(cache, upstream, 0, DEFAULT_MAX_CACHE_FILE_SIZE);
}
/**
* Constructs an instance with default {@link DataSource} and {@link DataSink} instances for
* reading and writing the cache.
*
* @param cache The cache.
* @param upstream A {@link DataSource} for reading data not in the cache.
* @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR}
* and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0.
*/ */
public CacheDataSource(Cache cache, DataSource upstream, @Flags int flags) { public CacheDataSource(Cache cache, DataSource upstream, @Flags int flags) {
this(cache, upstream, flags, DEFAULT_MAX_CACHE_FILE_SIZE); this(cache, upstream, flags, DEFAULT_MAX_CACHE_FILE_SIZE);
@ -123,8 +139,8 @@ public final class CacheDataSource implements DataSource {
* *
* @param cache The cache. * @param cache The cache.
* @param upstream A {@link DataSource} for reading data not in the cache. * @param upstream A {@link DataSource} for reading data not in the cache.
* @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE} and {@link * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR}
* #FLAG_IGNORE_CACHE_ON_ERROR} or 0. * and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0.
* @param maxCacheFileSize The maximum size of a cache file, in bytes. If the cached data size * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the cached data size
* exceeds this value, then the data will be fragmented into multiple cache files. The * exceeds this value, then the data will be fragmented into multiple cache files. The
* finer-grained this is the finer-grained the eviction policy can be. * finer-grained this is the finer-grained the eviction policy can be.
@ -145,8 +161,8 @@ public final class CacheDataSource implements DataSource {
* @param cacheReadDataSource A {@link DataSource} for reading data from the cache. * @param cacheReadDataSource A {@link DataSource} for reading data from the cache.
* @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. If null, cache is * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. If null, cache is
* accessed read-only. * accessed read-only.
* @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE} and {@link * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR}
* #FLAG_IGNORE_CACHE_ON_ERROR} or 0. * and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0.
* @param eventListener An optional {@link EventListener} to receive events. * @param eventListener An optional {@link EventListener} to receive events.
*/ */
public CacheDataSource(Cache cache, DataSource upstream, DataSource cacheReadDataSource, public CacheDataSource(Cache cache, DataSource upstream, DataSource cacheReadDataSource,

View File

@ -33,18 +33,26 @@ public final class CacheDataSourceFactory implements DataSource.Factory {
private final int flags; private final int flags;
private final EventListener eventListener; private final EventListener eventListener;
/**
* @see CacheDataSource#CacheDataSource(Cache, DataSource)
*/
public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory) {
this(cache, upstreamFactory, 0);
}
/** /**
* @see CacheDataSource#CacheDataSource(Cache, DataSource, int) * @see CacheDataSource#CacheDataSource(Cache, DataSource, int)
*/ */
public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory, int flags) { public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory,
@CacheDataSource.Flags int flags) {
this(cache, upstreamFactory, flags, CacheDataSource.DEFAULT_MAX_CACHE_FILE_SIZE); this(cache, upstreamFactory, flags, CacheDataSource.DEFAULT_MAX_CACHE_FILE_SIZE);
} }
/** /**
* @see CacheDataSource#CacheDataSource(Cache, DataSource, int, long) * @see CacheDataSource#CacheDataSource(Cache, DataSource, int, long)
*/ */
public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory, int flags, public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory,
long maxCacheFileSize) { @CacheDataSource.Flags int flags, long maxCacheFileSize) {
this(cache, upstreamFactory, new FileDataSourceFactory(), this(cache, upstreamFactory, new FileDataSourceFactory(),
new CacheDataSinkFactory(cache, maxCacheFileSize), flags, null); new CacheDataSinkFactory(cache, maxCacheFileSize), flags, null);
} }
@ -54,8 +62,8 @@ public final class CacheDataSourceFactory implements DataSource.Factory {
* EventListener) * EventListener)
*/ */
public CacheDataSourceFactory(Cache cache, Factory upstreamFactory, public CacheDataSourceFactory(Cache cache, Factory upstreamFactory,
Factory cacheReadDataSourceFactory, Factory cacheReadDataSourceFactory, DataSink.Factory cacheWriteDataSinkFactory,
DataSink.Factory cacheWriteDataSinkFactory, int flags, EventListener eventListener) { @CacheDataSource.Flags int flags, EventListener eventListener) {
this.cache = cache; this.cache = cache;
this.upstreamFactory = upstreamFactory; this.upstreamFactory = upstreamFactory;
this.cacheReadDataSourceFactory = cacheReadDataSourceFactory; this.cacheReadDataSourceFactory = cacheReadDataSourceFactory;

View File

@ -64,7 +64,7 @@ public final class CacheUtil {
} }
/** /**
* Returns already cached and missing bytes in the {@cache} for the data defined by {@code * Returns already cached and missing bytes in the {@code cache} for the data defined by {@code
* dataSpec}. * dataSpec}.
* *
* @param dataSpec Defines the data to be checked. * @param dataSpec Defines the data to be checked.

View File

@ -286,7 +286,9 @@ public final class SimpleCache implements Cache {
private void removeSpan(CacheSpan span, boolean removeEmptyCachedContent) throws CacheException { private void removeSpan(CacheSpan span, boolean removeEmptyCachedContent) throws CacheException {
CachedContent cachedContent = index.get(span.key); CachedContent cachedContent = index.get(span.key);
Assertions.checkState(cachedContent.removeSpan(span)); if (cachedContent == null || !cachedContent.removeSpan(span)) {
return;
}
totalSpace -= span.length; totalSpace -= span.length;
if (removeEmptyCachedContent && cachedContent.isEmpty()) { if (removeEmptyCachedContent && cachedContent.isEmpty()) {
index.removeEmpty(cachedContent.key); index.removeEmpty(cachedContent.key);

View File

@ -0,0 +1,306 @@
/*
* Copyright (C) 2017 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.video;
import static android.opengl.EGL14.EGL_ALPHA_SIZE;
import static android.opengl.EGL14.EGL_BLUE_SIZE;
import static android.opengl.EGL14.EGL_CONFIG_CAVEAT;
import static android.opengl.EGL14.EGL_CONTEXT_CLIENT_VERSION;
import static android.opengl.EGL14.EGL_DEFAULT_DISPLAY;
import static android.opengl.EGL14.EGL_DEPTH_SIZE;
import static android.opengl.EGL14.EGL_GREEN_SIZE;
import static android.opengl.EGL14.EGL_HEIGHT;
import static android.opengl.EGL14.EGL_NONE;
import static android.opengl.EGL14.EGL_OPENGL_ES2_BIT;
import static android.opengl.EGL14.EGL_RED_SIZE;
import static android.opengl.EGL14.EGL_RENDERABLE_TYPE;
import static android.opengl.EGL14.EGL_SURFACE_TYPE;
import static android.opengl.EGL14.EGL_TRUE;
import static android.opengl.EGL14.EGL_WIDTH;
import static android.opengl.EGL14.EGL_WINDOW_BIT;
import static android.opengl.EGL14.eglChooseConfig;
import static android.opengl.EGL14.eglCreateContext;
import static android.opengl.EGL14.eglCreatePbufferSurface;
import static android.opengl.EGL14.eglGetDisplay;
import static android.opengl.EGL14.eglInitialize;
import static android.opengl.EGL14.eglMakeCurrent;
import static android.opengl.GLES20.glDeleteTextures;
import static android.opengl.GLES20.glGenTextures;
import android.annotation.TargetApi;
import android.graphics.SurfaceTexture;
import android.graphics.SurfaceTexture.OnFrameAvailableListener;
import android.opengl.EGL14;
import android.opengl.EGLConfig;
import android.opengl.EGLContext;
import android.opengl.EGLDisplay;
import android.opengl.EGLSurface;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.HandlerThread;
import android.os.Message;
import android.util.Log;
import android.view.Surface;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import javax.microedition.khronos.egl.EGL10;
/**
* A dummy {@link Surface}.
*/
@TargetApi(17)
public final class DummySurface extends Surface {
private static final String TAG = "DummySurface";
private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;
/**
* Whether the device supports secure dummy surfaces.
*/
public static final boolean SECURE_SUPPORTED;
static {
if (Util.SDK_INT >= 17) {
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
String extensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS);
SECURE_SUPPORTED = extensions.contains("EGL_EXT_protected_content");
} else {
SECURE_SUPPORTED = false;
}
}
/**
* Whether the surface is secure.
*/
public final boolean secure;
private final DummySurfaceThread thread;
private boolean threadReleased;
/**
* Returns a newly created dummy surface. The surface must be released by calling {@link #release}
* when it's no longer required.
* <p>
* Must only be called if {@link Util#SDK_INT} is 17 or higher.
*
* @param secure Whether a secure surface is required. Must only be requested if
* {@link #SECURE_SUPPORTED} is {@code true}.
*/
public static DummySurface newInstanceV17(boolean secure) {
assertApiLevel17OrHigher();
Assertions.checkState(!secure || SECURE_SUPPORTED);
DummySurfaceThread thread = new DummySurfaceThread();
return thread.init(secure);
}
private DummySurface(DummySurfaceThread thread, SurfaceTexture surfaceTexture, boolean secure) {
super(surfaceTexture);
this.thread = thread;
this.secure = secure;
}
@Override
public void release() {
super.release();
// The Surface may be released multiple times (explicitly and by Surface.finalize()). The
// implementation of super.release() has its own deduplication logic. Below we need to
// deduplicate ourselves. Synchronization is required as we don't control the thread on which
// Surface.finalize() is called.
synchronized (thread) {
if (!threadReleased) {
thread.release();
threadReleased = true;
}
}
}
private static void assertApiLevel17OrHigher() {
if (Util.SDK_INT < 17) {
throw new UnsupportedOperationException("Unsupported prior to API level 17");
}
}
private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener,
Callback {
private static final int MSG_INIT = 1;
private static final int MSG_UPDATE_TEXTURE = 2;
private static final int MSG_RELEASE = 3;
private final int[] textureIdHolder;
private Handler handler;
private SurfaceTexture surfaceTexture;
private Error initError;
private RuntimeException initException;
private DummySurface surface;
public DummySurfaceThread() {
super("dummySurface");
textureIdHolder = new int[1];
}
public DummySurface init(boolean secure) {
start();
handler = new Handler(getLooper(), this);
boolean wasInterrupted = false;
synchronized (this) {
handler.obtainMessage(MSG_INIT, secure ? 1 : 0, 0).sendToTarget();
while (surface == null && initException == null && initError == null) {
try {
wait();
} catch (InterruptedException e) {
wasInterrupted = true;
}
}
}
if (wasInterrupted) {
// Restore the interrupted status.
Thread.currentThread().interrupt();
}
if (initException != null) {
throw initException;
} else if (initError != null) {
throw initError;
} else {
return surface;
}
}
public void release() {
handler.sendEmptyMessage(MSG_RELEASE);
}
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
handler.sendEmptyMessage(MSG_UPDATE_TEXTURE);
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_INIT:
try {
initInternal(msg.arg1 != 0);
} catch (RuntimeException e) {
Log.e(TAG, "Failed to initialize dummy surface", e);
initException = e;
} catch (Error e) {
Log.e(TAG, "Failed to initialize dummy surface", e);
initError = e;
} finally {
synchronized (this) {
notify();
}
}
return true;
case MSG_UPDATE_TEXTURE:
surfaceTexture.updateTexImage();
return true;
case MSG_RELEASE:
try {
releaseInternal();
} catch (Throwable e) {
Log.e(TAG, "Failed to release dummy surface", e);
} finally {
quit();
}
return true;
default:
return true;
}
}
private void initInternal(boolean secure) {
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
Assertions.checkState(display != null, "eglGetDisplay failed");
int[] version = new int[2];
boolean eglInitialized = eglInitialize(display, version, 0, version, 1);
Assertions.checkState(eglInitialized, "eglInitialize failed");
int[] eglAttributes = new int[] {
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, 8,
EGL_DEPTH_SIZE, 0,
EGL_CONFIG_CAVEAT, EGL_NONE,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_NONE
};
EGLConfig[] configs = new EGLConfig[1];
int[] numConfigs = new int[1];
boolean eglChooseConfigSuccess = eglChooseConfig(display, eglAttributes, 0, configs, 0, 1,
numConfigs, 0);
Assertions.checkState(eglChooseConfigSuccess && numConfigs[0] > 0 && configs[0] != null,
"eglChooseConfig failed");
EGLConfig config = configs[0];
int[] glAttributes;
if (secure) {
glAttributes = new int[] {
EGL_CONTEXT_CLIENT_VERSION, 2,
EGL_PROTECTED_CONTENT_EXT,
EGL_TRUE, EGL_NONE};
} else {
glAttributes = new int[] {
EGL_CONTEXT_CLIENT_VERSION, 2,
EGL_NONE};
}
EGLContext context = eglCreateContext(display, config, android.opengl.EGL14.EGL_NO_CONTEXT,
glAttributes, 0);
Assertions.checkState(context != null, "eglCreateContext failed");
int[] pbufferAttributes;
if (secure) {
pbufferAttributes = new int[] {
EGL_WIDTH, 1,
EGL_HEIGHT, 1,
EGL_PROTECTED_CONTENT_EXT, EGL_TRUE,
EGL_NONE};
} else {
pbufferAttributes = new int[] {
EGL_WIDTH, 1,
EGL_HEIGHT, 1,
EGL_NONE};
}
EGLSurface pbuffer = eglCreatePbufferSurface(display, config, pbufferAttributes, 0);
Assertions.checkState(pbuffer != null, "eglCreatePbufferSurface failed");
boolean eglMadeCurrent = eglMakeCurrent(display, pbuffer, pbuffer, context);
Assertions.checkState(eglMadeCurrent, "eglMakeCurrent failed");
glGenTextures(1, textureIdHolder, 0);
surfaceTexture = new SurfaceTexture(textureIdHolder[0]);
surfaceTexture.setOnFrameAvailableListener(this);
surface = new DummySurface(this, surfaceTexture, secure);
}
private void releaseInternal() {
try {
surfaceTexture.release();
} finally {
surface = null;
surfaceTexture = null;
glDeleteTextures(1, textureIdHolder, 0);
}
}
}
}

View File

@ -376,7 +376,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
} }
@Override @Override
protected void onOutputFormatChanged(MediaCodec codec, android.media.MediaFormat outputFormat) { protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) {
boolean hasCrop = outputFormat.containsKey(KEY_CROP_RIGHT) boolean hasCrop = outputFormat.containsKey(KEY_CROP_RIGHT)
&& outputFormat.containsKey(KEY_CROP_LEFT) && outputFormat.containsKey(KEY_CROP_BOTTOM) && outputFormat.containsKey(KEY_CROP_LEFT) && outputFormat.containsKey(KEY_CROP_BOTTOM)
&& outputFormat.containsKey(KEY_CROP_TOP); && outputFormat.containsKey(KEY_CROP_TOP);
@ -408,11 +408,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
@Override @Override
protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive, protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive,
Format oldFormat, Format newFormat) { Format oldFormat, Format newFormat) {
return areAdaptationCompatible(oldFormat, newFormat) return areAdaptationCompatible(codecIsAdaptive, oldFormat, newFormat)
&& newFormat.width <= codecMaxValues.width && newFormat.height <= codecMaxValues.height && newFormat.width <= codecMaxValues.width && newFormat.height <= codecMaxValues.height
&& newFormat.maxInputSize <= codecMaxValues.inputSize && newFormat.maxInputSize <= codecMaxValues.inputSize;
&& (codecIsAdaptive
|| (oldFormat.width == newFormat.width && oldFormat.height == newFormat.height));
} }
@Override @Override
@ -664,7 +662,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
} }
boolean haveUnknownDimensions = false; boolean haveUnknownDimensions = false;
for (Format streamFormat : streamFormats) { for (Format streamFormat : streamFormats) {
if (areAdaptationCompatible(format, streamFormat)) { if (areAdaptationCompatible(codecInfo.adaptive, format, streamFormat)) {
haveUnknownDimensions |= (streamFormat.width == Format.NO_VALUE haveUnknownDimensions |= (streamFormat.width == Format.NO_VALUE
|| streamFormat.height == Format.NO_VALUE); || streamFormat.height == Format.NO_VALUE);
maxWidth = Math.max(maxWidth, streamFormat.width); maxWidth = Math.max(maxWidth, streamFormat.width);
@ -817,17 +815,19 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
} }
/** /**
* Returns whether an adaptive codec with suitable {@link CodecMaxValues} will support adaptation * Returns whether a codec with suitable {@link CodecMaxValues} will support adaptation between
* between two {@link Format}s. * two {@link Format}s.
* *
* @param codecIsAdaptive Whether the codec supports seamless resolution switches.
* @param first The first format. * @param first The first format.
* @param second The second format. * @param second The second format.
* @return Whether an adaptive codec with suitable {@link CodecMaxValues} will support adaptation * @return Whether the codec will support adaptation between the two {@link Format}s.
* between two {@link Format}s.
*/ */
private static boolean areAdaptationCompatible(Format first, Format second) { private static boolean areAdaptationCompatible(boolean codecIsAdaptive, Format first,
Format second) {
return first.sampleMimeType.equals(second.sampleMimeType) return first.sampleMimeType.equals(second.sampleMimeType)
&& getRotationDegrees(first) == getRotationDegrees(second); && getRotationDegrees(first) == getRotationDegrees(second)
&& (codecIsAdaptive || (first.width == second.width && first.height == second.height));
} }
private static float getPixelWidthHeightRatio(Format format) { private static float getPixelWidthHeightRatio(Format format) {

View File

@ -23,6 +23,7 @@ import com.google.android.exoplayer2.util.MimeTypes;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.Collections;
import java.util.List; import java.util.List;
import junit.framework.TestCase; import junit.framework.TestCase;
@ -56,16 +57,22 @@ public class HlsMasterPlaylistParserTest extends TestCase {
private static final String MASTER_PLAYLIST_WITH_CC = " #EXTM3U \n" private static final String MASTER_PLAYLIST_WITH_CC = " #EXTM3U \n"
+ "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n" + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n"
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
+ "http://example.com/low.m3u8\n"; + "http://example.com/low.m3u8\n";
private static final String MASTER_PLAYLIST_WITHOUT_CC = " #EXTM3U \n"
+ "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128,"
+ "CLOSED-CAPTIONS=NONE\n"
+ "http://example.com/low.m3u8\n";
public void testParseMasterPlaylist() throws IOException{ public void testParseMasterPlaylist() throws IOException{
HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST); HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST);
List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants; List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants;
assertNotNull(variants); assertNotNull(variants);
assertEquals(5, variants.size()); assertEquals(5, variants.size());
assertNull(masterPlaylist.muxedCaptionFormats);
assertEquals(1280000, variants.get(0).format.bitrate); assertEquals(1280000, variants.get(0).format.bitrate);
assertNotNull(variants.get(0).format.codecs); assertNotNull(variants.get(0).format.codecs);
@ -117,6 +124,11 @@ public class HlsMasterPlaylistParserTest extends TestCase {
assertEquals("es", closedCaptionFormat.language); assertEquals("es", closedCaptionFormat.language);
} }
public void testPlaylistWithoutClosedCaptions() throws IOException {
HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST_WITHOUT_CC);
assertEquals(Collections.emptyList(), playlist.muxedCaptionFormats);
}
private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString) private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString)
throws IOException { throws IOException {
Uri playlistUri = Uri.parse(uri); Uri playlistUri = Uri.parse(uri);

View File

@ -92,6 +92,7 @@ import java.util.Locale;
private boolean isTimestampMaster; private boolean isTimestampMaster;
private byte[] scratchSpace; private byte[] scratchSpace;
private IOException fatalError; private IOException fatalError;
private HlsUrl expectedPlaylistUrl;
private Uri encryptionKeyUri; private Uri encryptionKeyUri;
private byte[] encryptionKey; private byte[] encryptionKey;
@ -111,7 +112,8 @@ import java.util.Locale;
* @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If * @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If
* multiple {@link HlsChunkSource}s are used for a single playback, they should all share the * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the
* same provider. * same provider.
* @param muxedCaptionFormats List of muxed caption {@link Format}s. * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption
* information is available in the master playlist.
*/ */
public HlsChunkSource(HlsPlaylistTracker playlistTracker, HlsUrl[] variants, public HlsChunkSource(HlsPlaylistTracker playlistTracker, HlsUrl[] variants,
HlsDataSourceFactory dataSourceFactory, TimestampAdjusterProvider timestampAdjusterProvider, HlsDataSourceFactory dataSourceFactory, TimestampAdjusterProvider timestampAdjusterProvider,
@ -142,6 +144,9 @@ import java.util.Locale;
if (fatalError != null) { if (fatalError != null) {
throw fatalError; throw fatalError;
} }
if (expectedPlaylistUrl != null) {
playlistTracker.maybeThrowPlaylistRefreshError(expectedPlaylistUrl);
}
} }
/** /**
@ -194,6 +199,7 @@ import java.util.Locale;
public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChunkHolder out) { public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChunkHolder out) {
int oldVariantIndex = previous == null ? C.INDEX_UNSET int oldVariantIndex = previous == null ? C.INDEX_UNSET
: trackGroup.indexOf(previous.trackFormat); : trackGroup.indexOf(previous.trackFormat);
expectedPlaylistUrl = null;
// Use start time of the previous chunk rather than its end time because switching format will // Use start time of the previous chunk rather than its end time because switching format will
// require downloading overlapping segments. // require downloading overlapping segments.
long bufferedDurationUs = previous == null ? 0 long bufferedDurationUs = previous == null ? 0
@ -207,6 +213,7 @@ import java.util.Locale;
HlsUrl selectedUrl = variants[selectedVariantIndex]; HlsUrl selectedUrl = variants[selectedVariantIndex];
if (!playlistTracker.isSnapshotValid(selectedUrl)) { if (!playlistTracker.isSnapshotValid(selectedUrl)) {
out.playlist = selectedUrl; out.playlist = selectedUrl;
expectedPlaylistUrl = selectedUrl;
// Retry when playlist is refreshed. // Retry when playlist is refreshed.
return; return;
} }
@ -246,6 +253,7 @@ import java.util.Locale;
out.endOfStream = true; out.endOfStream = true;
} else /* Live */ { } else /* Live */ {
out.playlist = selectedUrl; out.playlist = selectedUrl;
expectedPlaylistUrl = selectedUrl;
} }
return; return;
} }

View File

@ -39,6 +39,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.TimestampAdjuster;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -104,7 +105,8 @@ import java.util.concurrent.atomic.AtomicInteger;
* @param dataSpec Defines the data to be loaded. * @param dataSpec Defines the data to be loaded.
* @param initDataSpec Defines the initialization data to be fed to new extractors. May be null. * @param initDataSpec Defines the initialization data to be fed to new extractors. May be null.
* @param hlsUrl The url of the playlist from which this chunk was obtained. * @param hlsUrl The url of the playlist from which this chunk was obtained.
* @param muxedCaptionFormats List of muxed caption {@link Format}s. * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption
* information is available in the master playlist.
* @param trackSelectionReason See {@link #trackSelectionReason}. * @param trackSelectionReason See {@link #trackSelectionReason}.
* @param trackSelectionData See {@link #trackSelectionData}. * @param trackSelectionData See {@link #trackSelectionData}.
* @param startTimeUs The start time of the chunk in microseconds. * @param startTimeUs The start time of the chunk in microseconds.
@ -356,9 +358,12 @@ import java.util.concurrent.atomic.AtomicInteger;
// This flag ensures the change of pid between streams does not affect the sample queues. // This flag ensures the change of pid between streams does not affect the sample queues.
@DefaultTsPayloadReaderFactory.Flags @DefaultTsPayloadReaderFactory.Flags
int esReaderFactoryFlags = DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM; int esReaderFactoryFlags = DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM;
if (!muxedCaptionFormats.isEmpty()) { List<Format> closedCaptionFormats = muxedCaptionFormats;
if (closedCaptionFormats != null) {
// The playlist declares closed caption renditions, we should ignore descriptors. // The playlist declares closed caption renditions, we should ignore descriptors.
esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS; esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS;
} else {
closedCaptionFormats = Collections.emptyList();
} }
String codecs = trackFormat.codecs; String codecs = trackFormat.codecs;
if (!TextUtils.isEmpty(codecs)) { if (!TextUtils.isEmpty(codecs)) {
@ -373,7 +378,7 @@ import java.util.concurrent.atomic.AtomicInteger;
} }
} }
extractor = new TsExtractor(TsExtractor.MODE_HLS, timestampAdjuster, extractor = new TsExtractor(TsExtractor.MODE_HLS, timestampAdjuster,
new DefaultTsPayloadReaderFactory(esReaderFactoryFlags, muxedCaptionFormats)); new DefaultTsPayloadReaderFactory(esReaderFactoryFlags, closedCaptionFormats));
} }
if (usingNewExtractor) { if (usingNewExtractor) {
extractor.init(extractorOutput); extractor.init(extractorOutput);

View File

@ -84,7 +84,7 @@ public final class HlsMediaSource implements MediaSource,
@Override @Override
public void maybeThrowSourceInfoRefreshError() throws IOException { public void maybeThrowSourceInfoRefreshError() throws IOException {
playlistTracker.maybeThrowPlaylistRefreshError(); playlistTracker.maybeThrowPrimaryPlaylistRefreshError();
} }
@Override @Override

View File

@ -30,15 +30,31 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
*/ */
public static final class HlsUrl { public static final class HlsUrl {
/**
* The http url from which the media playlist can be obtained.
*/
public final String url; public final String url;
/**
* Format information associated with the HLS url.
*/
public final Format format; public final Format format;
public static HlsUrl createMediaPlaylistHlsUrl(String baseUri) { /**
* Creates an HLS url from a given http url.
*
* @param url The url.
* @return An HLS url.
*/
public static HlsUrl createMediaPlaylistHlsUrl(String url) {
Format format = Format.createContainerFormat("0", MimeTypes.APPLICATION_M3U8, null, null, Format format = Format.createContainerFormat("0", MimeTypes.APPLICATION_M3U8, null, null,
Format.NO_VALUE, 0, null); Format.NO_VALUE, 0, null);
return new HlsUrl(baseUri, format); return new HlsUrl(url, format);
} }
/**
* @param url See {@link #url}.
* @param format See {@link #format}.
*/
public HlsUrl(String url, Format format) { public HlsUrl(String url, Format format) {
this.url = url; this.url = url;
this.format = format; this.format = format;
@ -46,13 +62,39 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
} }
/**
* The list of variants declared by the playlist.
*/
public final List<HlsUrl> variants; public final List<HlsUrl> variants;
/**
* The list of demuxed audios declared by the playlist.
*/
public final List<HlsUrl> audios; public final List<HlsUrl> audios;
/**
* The list of subtitles declared by the playlist.
*/
public final List<HlsUrl> subtitles; public final List<HlsUrl> subtitles;
/**
* The format of the audio muxed in the variants. May be null if the playlist does not declare any
* muxed audio.
*/
public final Format muxedAudioFormat; public final Format muxedAudioFormat;
/**
* The format of the closed captions declared by the playlist. May be empty if the playlist
* explicitly declares no captions are available, or null if the playlist does not declare any
* captions information.
*/
public final List<Format> muxedCaptionFormats; public final List<Format> muxedCaptionFormats;
/**
* @param baseUri The base uri. Used to resolve relative paths.
* @param variants See {@link #variants}.
* @param audios See {@link #audios}.
* @param subtitles See {@link #subtitles}.
* @param muxedAudioFormat See {@link #muxedAudioFormat}.
* @param muxedCaptionFormats See {@link #muxedCaptionFormats}.
*/
public HlsMasterPlaylist(String baseUri, List<HlsUrl> variants, List<HlsUrl> audios, public HlsMasterPlaylist(String baseUri, List<HlsUrl> variants, List<HlsUrl> audios,
List<HlsUrl> subtitles, Format muxedAudioFormat, List<Format> muxedCaptionFormats) { List<HlsUrl> subtitles, Format muxedAudioFormat, List<Format> muxedCaptionFormats) {
super(baseUri); super(baseUri);
@ -60,14 +102,20 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
this.audios = Collections.unmodifiableList(audios); this.audios = Collections.unmodifiableList(audios);
this.subtitles = Collections.unmodifiableList(subtitles); this.subtitles = Collections.unmodifiableList(subtitles);
this.muxedAudioFormat = muxedAudioFormat; this.muxedAudioFormat = muxedAudioFormat;
this.muxedCaptionFormats = Collections.unmodifiableList(muxedCaptionFormats); this.muxedCaptionFormats = muxedCaptionFormats != null
? Collections.unmodifiableList(muxedCaptionFormats) : null;
} }
public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUri) { /**
List<HlsUrl> variant = Collections.singletonList(HlsUrl.createMediaPlaylistHlsUrl(variantUri)); * Creates a playlist with a single variant.
*
* @param variantUrl The url of the single variant.
* @return A master playlist with a single variant for the provided url.
*/
public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUrl) {
List<HlsUrl> variant = Collections.singletonList(HlsUrl.createMediaPlaylistHlsUrl(variantUrl));
List<HlsUrl> emptyList = Collections.emptyList(); List<HlsUrl> emptyList = Collections.emptyList();
return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null, return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null, null);
Collections.<Format>emptyList());
} }
} }

View File

@ -91,12 +91,14 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
public final boolean hasProgramDateTime; public final boolean hasProgramDateTime;
public final Segment initializationSegment; public final Segment initializationSegment;
public final List<Segment> segments; public final List<Segment> segments;
public final List<String> dateRanges;
public final long durationUs; public final long durationUs;
public HlsMediaPlaylist(@PlaylistType int playlistType, String baseUri, long startOffsetUs, public HlsMediaPlaylist(@PlaylistType int playlistType, String baseUri, long startOffsetUs,
long startTimeUs, boolean hasDiscontinuitySequence, int discontinuitySequence, long startTimeUs, boolean hasDiscontinuitySequence, int discontinuitySequence,
int mediaSequence, int version, long targetDurationUs, boolean hasEndTag, int mediaSequence, int version, long targetDurationUs, boolean hasEndTag,
boolean hasProgramDateTime, Segment initializationSegment, List<Segment> segments) { boolean hasProgramDateTime, Segment initializationSegment, List<Segment> segments,
List<String> dateRanges) {
super(baseUri); super(baseUri);
this.playlistType = playlistType; this.playlistType = playlistType;
this.startTimeUs = startTimeUs; this.startTimeUs = startTimeUs;
@ -117,6 +119,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
} }
this.startOffsetUs = startOffsetUs == C.TIME_UNSET ? C.TIME_UNSET this.startOffsetUs = startOffsetUs == C.TIME_UNSET ? C.TIME_UNSET
: startOffsetUs >= 0 ? startOffsetUs : durationUs + startOffsetUs; : startOffsetUs >= 0 ? startOffsetUs : durationUs + startOffsetUs;
this.dateRanges = Collections.unmodifiableList(dateRanges);
} }
/** /**
@ -155,7 +158,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) { public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) {
return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs, true, return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs, true,
discontinuitySequence, mediaSequence, version, targetDurationUs, hasEndTag, discontinuitySequence, mediaSequence, version, targetDurationUs, hasEndTag,
hasProgramDateTime, initializationSegment, segments); hasProgramDateTime, initializationSegment, segments, dateRanges);
} }
/** /**
@ -170,7 +173,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
} }
return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs, return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs,
hasDiscontinuitySequence, discontinuitySequence, mediaSequence, version, targetDurationUs, hasDiscontinuitySequence, discontinuitySequence, mediaSequence, version, targetDurationUs,
true, hasProgramDateTime, initializationSegment, segments); true, hasProgramDateTime, initializationSegment, segments, dateRanges);
} }
} }

View File

@ -29,6 +29,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Queue; import java.util.Queue;
@ -57,6 +58,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static final String TAG_ENDLIST = "#EXT-X-ENDLIST"; private static final String TAG_ENDLIST = "#EXT-X-ENDLIST";
private static final String TAG_KEY = "#EXT-X-KEY"; private static final String TAG_KEY = "#EXT-X-KEY";
private static final String TAG_BYTERANGE = "#EXT-X-BYTERANGE"; private static final String TAG_BYTERANGE = "#EXT-X-BYTERANGE";
private static final String TAG_DATERANGE = "#EXT-X-DATERANGE";
private static final String TYPE_AUDIO = "AUDIO"; private static final String TYPE_AUDIO = "AUDIO";
private static final String TYPE_VIDEO = "VIDEO"; private static final String TYPE_VIDEO = "VIDEO";
@ -69,6 +71,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static final String BOOLEAN_TRUE = "YES"; private static final String BOOLEAN_TRUE = "YES";
private static final String BOOLEAN_FALSE = "NO"; private static final String BOOLEAN_FALSE = "NO";
private static final String ATTR_CLOSED_CAPTIONS_NONE = "CLOSED-CAPTIONS=NONE";
private static final Pattern REGEX_BANDWIDTH = Pattern.compile("BANDWIDTH=(\\d+)\\b"); private static final Pattern REGEX_BANDWIDTH = Pattern.compile("BANDWIDTH=(\\d+)\\b");
private static final Pattern REGEX_CODECS = Pattern.compile("CODECS=\"(.+?)\""); private static final Pattern REGEX_CODECS = Pattern.compile("CODECS=\"(.+?)\"");
private static final Pattern REGEX_RESOLUTION = Pattern.compile("RESOLUTION=(\\d+x\\d+)"); private static final Pattern REGEX_RESOLUTION = Pattern.compile("RESOLUTION=(\\d+x\\d+)");
@ -172,7 +176,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
ArrayList<HlsMasterPlaylist.HlsUrl> audios = new ArrayList<>(); ArrayList<HlsMasterPlaylist.HlsUrl> audios = new ArrayList<>();
ArrayList<HlsMasterPlaylist.HlsUrl> subtitles = new ArrayList<>(); ArrayList<HlsMasterPlaylist.HlsUrl> subtitles = new ArrayList<>();
Format muxedAudioFormat = null; Format muxedAudioFormat = null;
ArrayList<Format> muxedCaptionFormats = new ArrayList<>(); List<Format> muxedCaptionFormats = null;
boolean noClosedCaptions = false;
String line; String line;
while (iterator.hasNext()) { while (iterator.hasNext()) {
@ -209,6 +214,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
mimeType = MimeTypes.APPLICATION_CEA708; mimeType = MimeTypes.APPLICATION_CEA708;
accessibilityChannel = Integer.parseInt(instreamId.substring(7)); accessibilityChannel = Integer.parseInt(instreamId.substring(7));
} }
if (muxedCaptionFormats == null) {
muxedCaptionFormats = new ArrayList<>();
}
muxedCaptionFormats.add(Format.createTextContainerFormat(id, null, mimeType, null, muxedCaptionFormats.add(Format.createTextContainerFormat(id, null, mimeType, null,
Format.NO_VALUE, selectionFlags, language, accessibilityChannel)); Format.NO_VALUE, selectionFlags, language, accessibilityChannel));
break; break;
@ -220,6 +228,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
int bitrate = parseIntAttr(line, REGEX_BANDWIDTH); int bitrate = parseIntAttr(line, REGEX_BANDWIDTH);
String codecs = parseOptionalStringAttr(line, REGEX_CODECS); String codecs = parseOptionalStringAttr(line, REGEX_CODECS);
String resolutionString = parseOptionalStringAttr(line, REGEX_RESOLUTION); String resolutionString = parseOptionalStringAttr(line, REGEX_RESOLUTION);
noClosedCaptions |= line.contains(ATTR_CLOSED_CAPTIONS_NONE);
int width; int width;
int height; int height;
if (resolutionString != null) { if (resolutionString != null) {
@ -242,6 +251,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
variants.add(new HlsMasterPlaylist.HlsUrl(line, format)); variants.add(new HlsMasterPlaylist.HlsUrl(line, format));
} }
} }
if (noClosedCaptions) {
muxedCaptionFormats = Collections.emptyList();
}
return new HlsMasterPlaylist(baseUri, variants, audios, subtitles, muxedAudioFormat, return new HlsMasterPlaylist(baseUri, variants, audios, subtitles, muxedAudioFormat,
muxedCaptionFormats); muxedCaptionFormats);
} }
@ -263,6 +275,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
boolean hasEndTag = false; boolean hasEndTag = false;
Segment initializationSegment = null; Segment initializationSegment = null;
List<Segment> segments = new ArrayList<>(); List<Segment> segments = new ArrayList<>();
List<String> dateRanges = new ArrayList<>();
long segmentDurationUs = 0; long segmentDurationUs = 0;
boolean hasDiscontinuitySequence = false; boolean hasDiscontinuitySequence = false;
@ -343,6 +356,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
C.msToUs(Util.parseXsDateTime(line.substring(line.indexOf(':') + 1))); C.msToUs(Util.parseXsDateTime(line.substring(line.indexOf(':') + 1)));
playlistStartTimeUs = programDatetimeUs - segmentStartTimeUs; playlistStartTimeUs = programDatetimeUs - segmentStartTimeUs;
} }
} else if (line.startsWith(TAG_DATERANGE)) {
dateRanges.add(line);
} else if (!line.startsWith("#")) { } else if (!line.startsWith("#")) {
String segmentEncryptionIV; String segmentEncryptionIV;
if (!isEncrypted) { if (!isEncrypted) {
@ -371,7 +386,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
} }
return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, playlistStartTimeUs, return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, playlistStartTimeUs,
hasDiscontinuitySequence, playlistDiscontinuitySequence, mediaSequence, version, hasDiscontinuitySequence, playlistDiscontinuitySequence, mediaSequence, version,
targetDurationUs, hasEndTag, playlistStartTimeUs != 0, initializationSegment, segments); targetDurationUs, hasEndTag, playlistStartTimeUs != 0, initializationSegment, segments,
dateRanges);
} }
private static String parseStringAttr(String line, Pattern pattern) throws ParserException { private static String parseStringAttr(String line, Pattern pattern) throws ParserException {

View File

@ -200,18 +200,29 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
} }
/** /**
* If the tracker is having trouble refreshing the primary playlist or loading an irreplaceable * If the tracker is having trouble refreshing the master playlist or the primary playlist, this
* playlist, this method throws the underlying error. Otherwise, does nothing. * method throws the underlying error. Otherwise, does nothing.
* *
* @throws IOException The underlying error. * @throws IOException The underlying error.
*/ */
public void maybeThrowPlaylistRefreshError() throws IOException { public void maybeThrowPrimaryPlaylistRefreshError() throws IOException {
initialPlaylistLoader.maybeThrowError(); initialPlaylistLoader.maybeThrowError();
if (primaryHlsUrl != null) { if (primaryHlsUrl != null) {
playlistBundles.get(primaryHlsUrl).mediaPlaylistLoader.maybeThrowError(); maybeThrowPlaylistRefreshError(primaryHlsUrl);
} }
} }
/**
* If the playlist is having trouble loading the playlist referenced by the given {@link HlsUrl},
* this method throws the underlying error.
*
* @param url The {@link HlsUrl}.
* @throws IOException The underyling error.
*/
public void maybeThrowPlaylistRefreshError(HlsUrl url) throws IOException {
playlistBundles.get(url).mediaPlaylistLoader.maybeThrowError();
}
/** /**
* Triggers a playlist refresh and whitelists it. * Triggers a playlist refresh and whitelists it.
* *

View File

@ -61,22 +61,21 @@ public class DefaultTimeBar extends View implements TimeBar {
private static final int DEFAULT_INCREMENT_COUNT = 20; private static final int DEFAULT_INCREMENT_COUNT = 20;
private static final int DEFAULT_BAR_HEIGHT = 4; private static final int DEFAULT_BAR_HEIGHT = 4;
private static final int DEFAULT_TOUCH_TARGET_HEIGHT = 26; private static final int DEFAULT_TOUCH_TARGET_HEIGHT = 26;
private static final int DEFAULT_PLAYED_COLOR = 0x33FFFFFF; private static final int DEFAULT_PLAYED_COLOR = 0xFFFFFFFF;
private static final int DEFAULT_BUFFERED_COLOR = 0xCCFFFFFF;
private static final int DEFAULT_AD_MARKER_COLOR = 0xB2FFFF00; private static final int DEFAULT_AD_MARKER_COLOR = 0xB2FFFF00;
private static final int DEFAULT_AD_MARKER_WIDTH = 4; private static final int DEFAULT_AD_MARKER_WIDTH = 4;
private static final int DEFAULT_SCRUBBER_ENABLED_SIZE = 12; private static final int DEFAULT_SCRUBBER_ENABLED_SIZE = 12;
private static final int DEFAULT_SCRUBBER_DISABLED_SIZE = 0; private static final int DEFAULT_SCRUBBER_DISABLED_SIZE = 0;
private static final int DEFAULT_SCRUBBER_DRAGGED_SIZE = 16; private static final int DEFAULT_SCRUBBER_DRAGGED_SIZE = 16;
private static final int OPAQUE_COLOR = 0xFF000000;
private final Rect seekBounds; private final Rect seekBounds;
private final Rect progressBar; private final Rect progressBar;
private final Rect bufferedBar; private final Rect bufferedBar;
private final Rect scrubberBar; private final Rect scrubberBar;
private final Paint progressPaint; private final Paint playedPaint;
private final Paint bufferedPaint;
private final Paint scrubberPaint; private final Paint scrubberPaint;
private final Paint bufferedPaint;
private final Paint unplayedPaint;
private final Paint adMarkerPaint; private final Paint adMarkerPaint;
private final int barHeight; private final int barHeight;
private final int touchTargetHeight; private final int touchTargetHeight;
@ -115,9 +114,10 @@ public class DefaultTimeBar extends View implements TimeBar {
progressBar = new Rect(); progressBar = new Rect();
bufferedBar = new Rect(); bufferedBar = new Rect();
scrubberBar = new Rect(); scrubberBar = new Rect();
progressPaint = new Paint(); playedPaint = new Paint();
bufferedPaint = new Paint();
scrubberPaint = new Paint(); scrubberPaint = new Paint();
bufferedPaint = new Paint();
unplayedPaint = new Paint();
adMarkerPaint = new Paint(); adMarkerPaint = new Paint();
// Calculate the dimensions and paints for drawn elements. // Calculate the dimensions and paints for drawn elements.
@ -147,13 +147,18 @@ public class DefaultTimeBar extends View implements TimeBar {
scrubberDraggedSize = a.getDimensionPixelSize( scrubberDraggedSize = a.getDimensionPixelSize(
R.styleable.DefaultTimeBar_scrubber_dragged_size, defaultScrubberDraggedSize); R.styleable.DefaultTimeBar_scrubber_dragged_size, defaultScrubberDraggedSize);
int playedColor = a.getInt(R.styleable.DefaultTimeBar_played_color, DEFAULT_PLAYED_COLOR); int playedColor = a.getInt(R.styleable.DefaultTimeBar_played_color, DEFAULT_PLAYED_COLOR);
int scrubberColor = a.getInt(R.styleable.DefaultTimeBar_scrubber_color,
getDefaultScrubberColor(playedColor));
int bufferedColor = a.getInt(R.styleable.DefaultTimeBar_buffered_color, int bufferedColor = a.getInt(R.styleable.DefaultTimeBar_buffered_color,
DEFAULT_BUFFERED_COLOR); getDefaultBufferedColor(playedColor));
int unplayedColor = a.getInt(R.styleable.DefaultTimeBar_unplayed_color,
getDefaultUnplayedColor(playedColor));
int adMarkerColor = a.getInt(R.styleable.DefaultTimeBar_ad_marker_color, int adMarkerColor = a.getInt(R.styleable.DefaultTimeBar_ad_marker_color,
DEFAULT_AD_MARKER_COLOR); DEFAULT_AD_MARKER_COLOR);
progressPaint.setColor(playedColor); playedPaint.setColor(playedColor);
scrubberPaint.setColor(OPAQUE_COLOR | playedColor); scrubberPaint.setColor(scrubberColor);
bufferedPaint.setColor(bufferedColor); bufferedPaint.setColor(bufferedColor);
unplayedPaint.setColor(unplayedColor);
adMarkerPaint.setColor(adMarkerColor); adMarkerPaint.setColor(adMarkerColor);
} finally { } finally {
a.recycle(); a.recycle();
@ -165,9 +170,10 @@ public class DefaultTimeBar extends View implements TimeBar {
scrubberEnabledSize = defaultScrubberEnabledSize; scrubberEnabledSize = defaultScrubberEnabledSize;
scrubberDisabledSize = defaultScrubberDisabledSize; scrubberDisabledSize = defaultScrubberDisabledSize;
scrubberDraggedSize = defaultScrubberDraggedSize; scrubberDraggedSize = defaultScrubberDraggedSize;
scrubberPaint.setColor(OPAQUE_COLOR | DEFAULT_PLAYED_COLOR); playedPaint.setColor(DEFAULT_PLAYED_COLOR);
progressPaint.setColor(DEFAULT_PLAYED_COLOR); scrubberPaint.setColor(getDefaultScrubberColor(DEFAULT_PLAYED_COLOR));
bufferedPaint.setColor(DEFAULT_BUFFERED_COLOR); bufferedPaint.setColor(getDefaultBufferedColor(DEFAULT_PLAYED_COLOR));
unplayedPaint.setColor(getDefaultUnplayedColor(DEFAULT_PLAYED_COLOR));
adMarkerPaint.setColor(DEFAULT_AD_MARKER_COLOR); adMarkerPaint.setColor(DEFAULT_AD_MARKER_COLOR);
} }
formatBuilder = new StringBuilder(); formatBuilder = new StringBuilder();
@ -337,16 +343,18 @@ public class DefaultTimeBar extends View implements TimeBar {
@Override @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int measureWidth = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(measureWidth, measureHeight); int height = heightMode == MeasureSpec.UNSPECIFIED ? touchTargetHeight
: heightMode == MeasureSpec.EXACTLY ? heightSize : Math.min(touchTargetHeight, heightSize);
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), height);
} }
@Override @Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int width = right - left; int width = right - left;
int height = bottom - top; int height = bottom - top;
int barY = height - touchTargetHeight; int barY = (height - touchTargetHeight) / 2;
int seekLeft = getPaddingLeft(); int seekLeft = getPaddingLeft();
int seekRight = width - getPaddingRight(); int seekRight = width - getPaddingRight();
int progressY = barY + (touchTargetHeight - barHeight) / 2; int progressY = barY + (touchTargetHeight - barHeight) / 2;
@ -457,12 +465,10 @@ public class DefaultTimeBar extends View implements TimeBar {
scrubberBar.set(progressBar); scrubberBar.set(progressBar);
long newScrubberTime = scrubbing ? scrubPosition : position; long newScrubberTime = scrubbing ? scrubPosition : position;
if (duration > 0) { if (duration > 0) {
int bufferedPixelWidth = int bufferedPixelWidth = (int) ((progressBar.width() * bufferedPosition) / duration);
(int) ((progressBar.width() * bufferedPosition) / duration); bufferedBar.right = Math.min(progressBar.left + bufferedPixelWidth, progressBar.right);
bufferedBar.right = progressBar.left + bufferedPixelWidth; int scrubberPixelPosition = (int) ((progressBar.width() * newScrubberTime) / duration);
int scrubberPixelPosition = scrubberBar.right = Math.min(progressBar.left + scrubberPixelPosition, progressBar.right);
(int) ((progressBar.width() * newScrubberTime) / duration);
scrubberBar.right = progressBar.left + scrubberPixelPosition;
} else { } else {
bufferedBar.right = progressBar.left; bufferedBar.right = progressBar.left;
scrubberBar.right = progressBar.left; scrubberBar.right = progressBar.left;
@ -502,21 +508,21 @@ public class DefaultTimeBar extends View implements TimeBar {
int barTop = progressBar.centerY() - progressBarHeight / 2; int barTop = progressBar.centerY() - progressBarHeight / 2;
int barBottom = barTop + progressBarHeight; int barBottom = barTop + progressBarHeight;
if (duration <= 0) { if (duration <= 0) {
canvas.drawRect(progressBar.left, barTop, progressBar.right, barBottom, progressPaint); canvas.drawRect(progressBar.left, barTop, progressBar.right, barBottom, unplayedPaint);
return; return;
} }
int bufferedLeft = bufferedBar.left; int bufferedLeft = bufferedBar.left;
int bufferedRight = bufferedBar.right; int bufferedRight = bufferedBar.right;
int progressLeft = Math.max(Math.max(progressBar.left, bufferedRight), scrubberBar.right); int progressLeft = Math.max(Math.max(progressBar.left, bufferedRight), scrubberBar.right);
if (progressLeft < progressBar.right) { if (progressLeft < progressBar.right) {
canvas.drawRect(progressLeft, barTop, progressBar.right, barBottom, progressPaint); canvas.drawRect(progressLeft, barTop, progressBar.right, barBottom, unplayedPaint);
} }
bufferedLeft = Math.max(bufferedLeft, scrubberBar.right); bufferedLeft = Math.max(bufferedLeft, scrubberBar.right);
if (bufferedRight > bufferedLeft) { if (bufferedRight > bufferedLeft) {
canvas.drawRect(bufferedLeft, barTop, bufferedRight, barBottom, bufferedPaint); canvas.drawRect(bufferedLeft, barTop, bufferedRight, barBottom, bufferedPaint);
} }
if (scrubberBar.width() > 0) { if (scrubberBar.width() > 0) {
canvas.drawRect(scrubberBar.left, barTop, scrubberBar.right, barBottom, scrubberPaint); canvas.drawRect(scrubberBar.left, barTop, scrubberBar.right, barBottom, playedPaint);
} }
int adMarkerOffset = adMarkerWidth / 2; int adMarkerOffset = adMarkerWidth / 2;
for (int i = 0; i < adBreakCount; i++) { for (int i = 0; i < adBreakCount; i++) {
@ -577,4 +583,16 @@ public class DefaultTimeBar extends View implements TimeBar {
return (int) (dps * displayMetrics.density + 0.5f); return (int) (dps * displayMetrics.density + 0.5f);
} }
private static int getDefaultScrubberColor(int playedColor) {
return 0xFF000000 | playedColor;
}
private static int getDefaultUnplayedColor(int playedColor) {
return 0x33000000 | (playedColor & 0x00FFFFFF);
}
private static int getDefaultBufferedColor(int playedColor) {
return 0xCC000000 | (playedColor & 0x00FFFFFF);
}
} }

View File

@ -68,7 +68,9 @@
<attr name="scrubber_disabled_size" format="dimension"/> <attr name="scrubber_disabled_size" format="dimension"/>
<attr name="scrubber_dragged_size" format="dimension"/> <attr name="scrubber_dragged_size" format="dimension"/>
<attr name="played_color" format="color"/> <attr name="played_color" format="color"/>
<attr name="scrubber_color" format="color"/>
<attr name="buffered_color" format="color"/> <attr name="buffered_color" format="color"/>
<attr name="unplayed_color" format="color"/>
<attr name="ad_marker_color" format="color"/> <attr name="ad_marker_color" format="color"/>
</declare-styleable> </declare-styleable>