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 #
### 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 ###
* New modular library structure. You can read more about depending on individual

View File

@ -16,7 +16,7 @@ buildscript {
jcenter()
}
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'
}
// Workaround for the following test coverage issue. Remove when fixed:
@ -48,7 +48,7 @@ allprojects {
releaseRepoName = getBintrayRepo()
releaseUserOrg = 'google'
releaseGroupId = 'com.google.android.exoplayer'
releaseVersion = 'r2.4.0'
releaseVersion = 'r2.4.1'
releaseWebsite = 'https://github.com/google/ExoPlayer'
}
if (it.hasProperty('externalBuildDir')) {

View File

@ -16,8 +16,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.demo"
android:versionCode="2400"
android:versionName="2.4.0">
android:versionCode="2401"
android:versionName="2.4.1">
<uses-permission android:name="android.permission.INTERNET"/>
<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();
boolean needNewPlayer = player == null;
if (needNewPlayer) {
boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false);
UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA)
? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : 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 =
((DemoApplication) getApplication()).useExtensionRenderers()
? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
@ -261,10 +261,10 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this,
drmSessionManager, extensionRendererMode);
TrackSelection.Factory videoTrackSelectionFactory =
TrackSelection.Factory adaptiveTrackSelectionFactory =
new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
trackSelectionHelper = new TrackSelectionHelper(trackSelector, videoTrackSelectionFactory);
trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory);
trackSelectionHelper = new TrackSelectionHelper(trackSelector, adaptiveTrackSelectionFactory);
lastSeenTrackGroupArray = null;
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 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 joiningDeadlineMs The joining deadline.
* @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"
arch[4]="arm64-v8a"
config[4]="--force-target=armv8-android-gcc --sdk-path=$ndk --disable-neon"
config[4]+=" --disable-neon-asm"
config[4]="--force-target=armv8-android-gcc --sdk-path=$ndk --enable-neon"
arch[5]="x86_64"
config[5]="--force-target=x86_64-android-gcc --sdk-path=$ndk --disable-sse2"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -75,7 +75,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
public void testCustomPesReader() throws Exception {
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);
FakeExtractorInput input = new FakeExtractorInput.Builder()
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts"))
@ -100,7 +100,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
public void testCustomInitialSectionReader() throws Exception {
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);
FakeExtractorInput input = new FakeExtractorInput.Builder()
.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());
Cue ttmlCue = output.get(0);
assertEquals("lorem", ttmlCue.text.toString());
assertEquals(10.f / 100.f, ttmlCue.position);
assertEquals(10.f / 100.f, ttmlCue.line);
assertEquals(20.f / 100.f, ttmlCue.size);
assertEquals(10f / 100f, ttmlCue.position);
assertEquals(10f / 100f, ttmlCue.line);
assertEquals(20f / 100f, ttmlCue.size);
ttmlCue = output.get(1);
assertEquals("amet", ttmlCue.text.toString());
assertEquals(60.f / 100.f, ttmlCue.position);
assertEquals(10.f / 100.f, ttmlCue.line);
assertEquals(20.f / 100.f, ttmlCue.size);
assertEquals(60f / 100f, ttmlCue.position);
assertEquals(10f / 100f, ttmlCue.line);
assertEquals(20f / 100f, ttmlCue.size);
output = subtitle.getCues(5000000);
assertEquals(1, output.size());
ttmlCue = output.get(0);
assertEquals("ipsum", ttmlCue.text.toString());
assertEquals(40.f / 100.f, ttmlCue.position);
assertEquals(40.f / 100.f, ttmlCue.line);
assertEquals(20.f / 100.f, ttmlCue.size);
assertEquals(40f / 100f, ttmlCue.position);
assertEquals(40f / 100f, ttmlCue.line);
assertEquals(20f / 100f, ttmlCue.size);
output = subtitle.getCues(9000000);
assertEquals(1, output.size());
ttmlCue = output.get(0);
assertEquals("dolor", ttmlCue.text.toString());
assertEquals(10.f / 100.f, ttmlCue.position);
assertEquals(80.f / 100.f, ttmlCue.line);
assertEquals(Cue.DIMEN_UNSET, ttmlCue.size);
assertEquals(10f / 100f, ttmlCue.position);
assertEquals(80f / 100f, ttmlCue.line);
assertEquals(1f, ttmlCue.size);
output = subtitle.getCues(21000000);
assertEquals(1, output.size());
ttmlCue = output.get(0);
assertEquals("She first said this", ttmlCue.text.toString());
assertEquals(45.f / 100.f, ttmlCue.position);
assertEquals(45.f / 100.f, ttmlCue.line);
assertEquals(35.f / 100.f, ttmlCue.size);
assertEquals(45f / 100f, ttmlCue.position);
assertEquals(45f / 100f, ttmlCue.line);
assertEquals(35f / 100f, ttmlCue.size);
output = subtitle.getCues(25000000);
ttmlCue = output.get(0);
assertEquals("She first said this\nThen this", ttmlCue.text.toString());
@ -197,8 +197,8 @@ public final class TtmlDecoderTest extends InstrumentationTestCase {
assertEquals(1, output.size());
ttmlCue = output.get(0);
assertEquals("She first said this\nThen this\nFinally this", ttmlCue.text.toString());
assertEquals(45.f / 100.f, ttmlCue.position);
assertEquals(45.f / 100.f, ttmlCue.line);
assertEquals(45f / 100f, ttmlCue.position);
assertEquals(45f / 100f, ttmlCue.line);
}
public void testEmptyStyleAttribute() throws IOException, SubtitleDecoderException {

View File

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

View File

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2;
import android.os.Looper;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
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
* discouraged, however if an application does wish to do this then it may do so provided that it
* 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
* Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this
* thread.</li>
@ -113,8 +117,8 @@ public interface ExoPlayer {
* Called when the timeline and/or manifest has been refreshed.
* <p>
* 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
* removed from the timeline. The will <em>not</em> be reported via a separate call to
* For example, the current period index may have changed as a result of periods being added or
* removed from the timeline. This will <em>not</em> be reported via a separate call to
* {@link #onPositionDiscontinuity()}.
*
* @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
* 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.
*/

View File

@ -16,7 +16,6 @@
package com.google.android.exoplayer2;
import android.content.Context;
import android.os.Looper;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.trackselection.TrackSelector;
@ -29,8 +28,7 @@ public final class ExoPlayerFactory {
private ExoPlayerFactory() {}
/**
* Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
* {@link Looper}.
* Creates a {@link SimpleExoPlayer} instance.
*
* @param context A {@link Context}.
* @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
* {@link Looper}. Available extension renderers are not used.
* Creates a {@link SimpleExoPlayer} instance. Available extension renderers are not used.
*
* @param context A {@link Context}.
* @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
* {@link Looper}.
* Creates a {@link SimpleExoPlayer} instance.
*
* @param context A {@link Context}.
* @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
* {@link Looper}.
* Creates a {@link SimpleExoPlayer} instance.
*
* @param context A {@link Context}.
* @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
* {@link Looper}.
* Creates a {@link SimpleExoPlayer} instance.
*
* @param context A {@link Context}.
* @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
* {@link Looper}.
* Creates a {@link SimpleExoPlayer} 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.
@ -135,8 +128,7 @@ public final class ExoPlayerFactory {
}
/**
* Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
* {@link Looper}.
* Creates a {@link SimpleExoPlayer} 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.
@ -148,8 +140,7 @@ public final class ExoPlayerFactory {
}
/**
* Creates an {@link ExoPlayer} instance. Must be called from a thread that has an associated
* {@link Looper}.
* Creates an {@link ExoPlayer} 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.
@ -159,8 +150,7 @@ public final class ExoPlayerFactory {
}
/**
* Creates an {@link ExoPlayer} instance. Must be called from a thread that has an associated
* {@link Looper}.
* Creates an {@link ExoPlayer} 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.

View File

@ -92,7 +92,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
trackGroups = TrackGroupArray.EMPTY;
trackSelections = emptyTrackSelections;
playbackParameters = PlaybackParameters.DEFAULT;
eventHandler = new Handler() {
Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper();
eventHandler = new Handler(eventLooper) {
@Override
public void handleMessage(Message 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".
*/
// 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}.
*/
// 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.
@ -40,7 +40,7 @@ public interface ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// 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}

View File

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

View File

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

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.decoder;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* Buffer for {@link SimpleDecoder} output.
@ -40,7 +41,7 @@ public class SimpleOutputBuffer extends OutputBuffer {
public ByteBuffer init(long timeUs, int size) {
this.timeUs = timeUs;
if (data == null || data.capacity() < size) {
data = ByteBuffer.allocateDirect(size);
data = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
}
data.position(0);
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.PsExtractor;
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.util.TimestampAdjuster;
import java.lang.reflect.Constructor;
/**
@ -67,8 +69,13 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
private @MatroskaExtractor.Flags int matroskaFlags;
private @FragmentedMp4Extractor.Flags int fragmentedMp4Flags;
private @Mp3Extractor.Flags int mp3Flags;
private @TsExtractor.Mode int tsMode;
private @DefaultTsPayloadReaderFactory.Flags int tsFlags;
public DefaultExtractorsFactory() {
tsMode = TsExtractor.MODE_SINGLE_PMT;
}
/**
* Sets flags for {@link MatroskaExtractor} instances created by the factory.
*
@ -107,6 +114,18 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
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
* created by the factory.
@ -130,7 +149,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
extractors[3] = new Mp3Extractor(mp3Flags);
extractors[4] = new AdtsExtractor();
extractors[5] = new Ac3Extractor();
extractors[6] = new TsExtractor(tsFlags);
extractors[6] = new TsExtractor(tsMode, tsFlags);
extractors[7] = new FlvExtractor();
extractors[8] = new OggExtractor();
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.ParsableByteArray;
import java.io.IOException;
import java.util.Arrays;
/**
* OGG packet class.
@ -27,8 +28,8 @@ import java.io.IOException;
/* package */ final class OggPacket {
private final OggPageHeader pageHeader = new OggPageHeader();
private final ParsableByteArray packetArray =
new ParsableByteArray(new byte[OggPageHeader.MAX_PAGE_PAYLOAD], 0);
private final ParsableByteArray packetArray = new ParsableByteArray(
new byte[OggPageHeader.MAX_PAGE_PAYLOAD], 0);
private int currentSegmentIndex = C.INDEX_UNSET;
private int segmentCount;
@ -85,6 +86,9 @@ import java.io.IOException;
int size = calculatePacketSize(currentSegmentIndex);
int segmentIndex = currentSegmentIndex + segmentCount;
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);
packetArray.setLimit(packetArray.limit() + size);
populated = pageHeader.laces[segmentIndex - 1] != 255;
@ -118,6 +122,17 @@ import java.io.IOException;
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}.
*

View File

@ -103,15 +103,12 @@ import java.io.IOException;
switch (state) {
case STATE_READ_HEADERS:
return readHeaders(input);
case STATE_SKIP_HEADERS:
input.skipFully((int) payloadStartPosition);
state = STATE_READ_PAYLOAD;
return Extractor.RESULT_CONTINUE;
case STATE_READ_PAYLOAD:
return readPayload(input, seekPosition);
default:
// Never happens.
throw new IllegalStateException();
@ -152,6 +149,8 @@ import java.io.IOException;
setupData = null;
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;
}

View File

@ -101,7 +101,7 @@ import java.util.ArrayList;
codecInitialisationData.add(vorbisSetup.setupHeaderData);
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,
codecInitialisationData, null, 0, null);
return true;

View File

@ -65,13 +65,13 @@ public final class TsExtractor implements Extractor {
* Modes for the extractor.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({MODE_NORMAL, MODE_SINGLE_PMT, MODE_HLS})
@IntDef({MODE_MULTI_PMT, MODE_SINGLE_PMT, MODE_HLS})
public @interface Mode {}
/**
* 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.
*/
@ -132,12 +132,23 @@ public final class TsExtractor implements Extractor {
* {@code FLAG_*} values that control the behavior of the payload readers.
*/
public TsExtractor(@Flags int defaultTsPayloadReaderFlags) {
this(MODE_NORMAL, new TimestampAdjuster(0),
new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags));
this(MODE_SINGLE_PMT, 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}.
* @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.
* @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) {
// We don't have a format yet, so try and read one.
buffer.clear();
flagsOnlyBuffer.clear();
int result = readSource(formatHolder, flagsOnlyBuffer, true);
if (result == C.RESULT_FORMAT_READ) {
onInputFormatChanged(formatHolder.format);

View File

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

View File

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

View File

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

View File

@ -17,7 +17,6 @@ package com.google.android.exoplayer2.text.ttml;
import android.text.Layout;
import android.util.Log;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
@ -100,7 +99,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
XmlPullParser xmlParser = xmlParserFactory.newPullParser();
Map<String, TtmlStyle> globalStyles = 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);
xmlParser.setInput(inputStream, null);
TtmlSubtitle ttmlSubtitle = null;
@ -211,9 +210,9 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
globalStyles.put(style.getId(), style);
}
} else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) {
Pair<String, TtmlRegion> ttmlRegionInfo = parseRegionAttributes(xmlParser);
if (ttmlRegionInfo != null) {
globalRegions.put(ttmlRegionInfo.first, ttmlRegionInfo.second);
TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser);
if (ttmlRegion != null) {
globalRegions.put(ttmlRegion.id, ttmlRegion);
}
}
} 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
* terms of percentage of the viewport. Regions that do not correctly declare origin are ignored.
* Parses a region declaration.
* <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 regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN);
String regionExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT);
if (regionOrigin == null || regionId == null) {
if (regionId == null) {
return null;
}
float position = Cue.DIMEN_UNSET;
float line = Cue.DIMEN_UNSET;
Matcher originMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin);
if (originMatcher.matches()) {
try {
position = Float.parseFloat(originMatcher.group(1)) / 100.f;
line = Float.parseFloat(originMatcher.group(2)) / 100.f;
} catch (NumberFormatException e) {
Log.w(TAG, "Ignoring region with malformed origin: '" + regionOrigin + "'", e);
position = Cue.DIMEN_UNSET;
float position;
float line;
String regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN);
if (regionOrigin != null) {
Matcher originMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin);
if (originMatcher.matches()) {
try {
position = Float.parseFloat(originMatcher.group(1)) / 100f;
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) {
Matcher extentMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent);
if (extentMatcher.matches()) {
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) {
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) {
@ -277,7 +319,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
try {
style.setBackgroundColor(ColorParser.parseTtmlColor(attributeValue));
} catch (IllegalArgumentException e) {
Log.w(TAG, "failed parsing background value: '" + attributeValue + "'");
Log.w(TAG, "Failed parsing background value: " + attributeValue);
}
break;
case TtmlNode.ATTR_TTS_COLOR:
@ -285,7 +327,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
try {
style.setFontColor(ColorParser.parseTtmlColor(attributeValue));
} catch (IllegalArgumentException e) {
Log.w(TAG, "failed parsing color value: '" + attributeValue + "'");
Log.w(TAG, "Failed parsing color value: " + attributeValue);
}
break;
case TtmlNode.ATTR_TTS_FONT_FAMILY:
@ -296,7 +338,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
style = createIfNull(style);
parseFontSize(attributeValue, style);
} catch (SubtitleDecoderException e) {
Log.w(TAG, "failed parsing fontSize value: '" + attributeValue + "'");
Log.w(TAG, "Failed parsing fontSize value: " + attributeValue);
}
break;
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 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_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_SIZE = "fontSize";
public static final String ATTR_TTS_FONT_FAMILY = "fontFamily";
public static final String ATTR_TTS_FONT_WEIGHT = "fontWeight";
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_ALIGN = "textAlign";
@ -179,7 +180,7 @@ import java.util.TreeSet;
for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
TtmlRegion region = regionMap.get(entry.getKey());
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;
}

View File

@ -22,20 +22,24 @@ import com.google.android.exoplayer2.text.Cue;
*/
/* package */ final class TtmlRegion {
public final String id;
public final float position;
public final float line;
@Cue.LineType
public final int lineType;
@Cue.LineType public final int lineType;
@Cue.AnchorType public final int lineAnchor;
public final float width;
public TtmlRegion() {
this(Cue.DIMEN_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
public TtmlRegion(String id) {
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.line = line;
this.lineType = lineType;
this.lineAnchor = lineAnchor;
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.source.TrackGroup;
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.Util;
import java.util.ArrayList;
@ -376,10 +377,21 @@ public class DefaultTrackSelector extends MappingTrackSelector {
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() {
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) {
return TextUtils.equals(language, Util.normalizeLanguageCode(format.language));
return language != null
&& TextUtils.equals(language, Util.normalizeLanguageCode(format.language));
}
// Viewport size util methods.

View File

@ -33,11 +33,11 @@ import java.util.concurrent.ExecutorService;
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 UnexpectedLoaderException(Exception cause) {
public UnexpectedLoaderException(Throwable cause) {
super("Unexpected " + cause.getClass().getSimpleName() + ": " + cause.getMessage(), cause);
}
@ -316,6 +316,14 @@ public final class Loader implements LoaderErrorThrower {
if (!released) {
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) {
// 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

View File

@ -54,8 +54,8 @@ public final class CacheDataSource implements DataSource {
FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS})
public @interface Flags {}
/**
* A flag indicating whether we will block reads if the cache key is locked. If this flag is
* set, then we will read from upstream if the cache key is locked.
* A flag indicating whether we will block reads if the cache key is locked. If unset then data is
* 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;
@ -110,7 +110,23 @@ public final class CacheDataSource implements DataSource {
/**
* 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) {
this(cache, upstream, flags, DEFAULT_MAX_CACHE_FILE_SIZE);
@ -123,8 +139,8 @@ public final class CacheDataSource implements DataSource {
*
* @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} and {@link
* #FLAG_IGNORE_CACHE_ON_ERROR} or 0.
* @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.
* @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
* 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 cacheWriteDataSink A {@link DataSink} for writing data to the cache. If null, cache is
* accessed read-only.
* @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE} and {@link
* #FLAG_IGNORE_CACHE_ON_ERROR} or 0.
* @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.
* @param eventListener An optional {@link EventListener} to receive events.
*/
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 EventListener eventListener;
/**
* @see CacheDataSource#CacheDataSource(Cache, DataSource)
*/
public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory) {
this(cache, upstreamFactory, 0);
}
/**
* @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);
}
/**
* @see CacheDataSource#CacheDataSource(Cache, DataSource, int, long)
*/
public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory, int flags,
long maxCacheFileSize) {
public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory,
@CacheDataSource.Flags int flags, long maxCacheFileSize) {
this(cache, upstreamFactory, new FileDataSourceFactory(),
new CacheDataSinkFactory(cache, maxCacheFileSize), flags, null);
}
@ -54,8 +62,8 @@ public final class CacheDataSourceFactory implements DataSource.Factory {
* EventListener)
*/
public CacheDataSourceFactory(Cache cache, Factory upstreamFactory,
Factory cacheReadDataSourceFactory,
DataSink.Factory cacheWriteDataSinkFactory, int flags, EventListener eventListener) {
Factory cacheReadDataSourceFactory, DataSink.Factory cacheWriteDataSinkFactory,
@CacheDataSource.Flags int flags, EventListener eventListener) {
this.cache = cache;
this.upstreamFactory = upstreamFactory;
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}.
*
* @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 {
CachedContent cachedContent = index.get(span.key);
Assertions.checkState(cachedContent.removeSpan(span));
if (cachedContent == null || !cachedContent.removeSpan(span)) {
return;
}
totalSpace -= span.length;
if (removeEmptyCachedContent && cachedContent.isEmpty()) {
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
protected void onOutputFormatChanged(MediaCodec codec, android.media.MediaFormat outputFormat) {
protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) {
boolean hasCrop = outputFormat.containsKey(KEY_CROP_RIGHT)
&& outputFormat.containsKey(KEY_CROP_LEFT) && outputFormat.containsKey(KEY_CROP_BOTTOM)
&& outputFormat.containsKey(KEY_CROP_TOP);
@ -408,11 +408,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
@Override
protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive,
Format oldFormat, Format newFormat) {
return areAdaptationCompatible(oldFormat, newFormat)
return areAdaptationCompatible(codecIsAdaptive, oldFormat, newFormat)
&& newFormat.width <= codecMaxValues.width && newFormat.height <= codecMaxValues.height
&& newFormat.maxInputSize <= codecMaxValues.inputSize
&& (codecIsAdaptive
|| (oldFormat.width == newFormat.width && oldFormat.height == newFormat.height));
&& newFormat.maxInputSize <= codecMaxValues.inputSize;
}
@Override
@ -664,7 +662,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
boolean haveUnknownDimensions = false;
for (Format streamFormat : streamFormats) {
if (areAdaptationCompatible(format, streamFormat)) {
if (areAdaptationCompatible(codecInfo.adaptive, format, streamFormat)) {
haveUnknownDimensions |= (streamFormat.width == Format.NO_VALUE
|| streamFormat.height == Format.NO_VALUE);
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
* between two {@link Format}s.
* Returns whether a codec with suitable {@link CodecMaxValues} will support adaptation between
* two {@link Format}s.
*
* @param codecIsAdaptive Whether the codec supports seamless resolution switches.
* @param first The first format.
* @param second The second format.
* @return Whether an adaptive codec with suitable {@link CodecMaxValues} will support adaptation
* between two {@link Format}s.
* @return Whether the codec will support adaptation between the 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)
&& getRotationDegrees(first) == getRotationDegrees(second);
&& getRotationDegrees(first) == getRotationDegrees(second)
&& (codecIsAdaptive || (first.width == second.width && first.height == second.height));
}
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.IOException;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.List;
import junit.framework.TestCase;
@ -56,16 +57,22 @@ public class HlsMasterPlaylistParserTest extends TestCase {
private static final String MASTER_PLAYLIST_WITH_CC = " #EXTM3U \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"
+ "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{
HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST);
List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants;
assertNotNull(variants);
assertEquals(5, variants.size());
assertNull(masterPlaylist.muxedCaptionFormats);
assertEquals(1280000, variants.get(0).format.bitrate);
assertNotNull(variants.get(0).format.codecs);
@ -117,6 +124,11 @@ public class HlsMasterPlaylistParserTest extends TestCase {
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)
throws IOException {
Uri playlistUri = Uri.parse(uri);

View File

@ -92,6 +92,7 @@ import java.util.Locale;
private boolean isTimestampMaster;
private byte[] scratchSpace;
private IOException fatalError;
private HlsUrl expectedPlaylistUrl;
private Uri encryptionKeyUri;
private byte[] encryptionKey;
@ -111,7 +112,8 @@ import java.util.Locale;
* @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If
* multiple {@link HlsChunkSource}s are used for a single playback, they should all share the
* 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,
HlsDataSourceFactory dataSourceFactory, TimestampAdjusterProvider timestampAdjusterProvider,
@ -142,6 +144,9 @@ import java.util.Locale;
if (fatalError != null) {
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) {
int oldVariantIndex = previous == null ? C.INDEX_UNSET
: trackGroup.indexOf(previous.trackFormat);
expectedPlaylistUrl = null;
// Use start time of the previous chunk rather than its end time because switching format will
// require downloading overlapping segments.
long bufferedDurationUs = previous == null ? 0
@ -207,6 +213,7 @@ import java.util.Locale;
HlsUrl selectedUrl = variants[selectedVariantIndex];
if (!playlistTracker.isSnapshotValid(selectedUrl)) {
out.playlist = selectedUrl;
expectedPlaylistUrl = selectedUrl;
// Retry when playlist is refreshed.
return;
}
@ -246,6 +253,7 @@ import java.util.Locale;
out.endOfStream = true;
} else /* Live */ {
out.playlist = selectedUrl;
expectedPlaylistUrl = selectedUrl;
}
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.Util;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
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 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 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 trackSelectionData See {@link #trackSelectionData}.
* @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.
@DefaultTsPayloadReaderFactory.Flags
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.
esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS;
} else {
closedCaptionFormats = Collections.emptyList();
}
String codecs = trackFormat.codecs;
if (!TextUtils.isEmpty(codecs)) {
@ -373,7 +378,7 @@ import java.util.concurrent.atomic.AtomicInteger;
}
}
extractor = new TsExtractor(TsExtractor.MODE_HLS, timestampAdjuster,
new DefaultTsPayloadReaderFactory(esReaderFactoryFlags, muxedCaptionFormats));
new DefaultTsPayloadReaderFactory(esReaderFactoryFlags, closedCaptionFormats));
}
if (usingNewExtractor) {
extractor.init(extractorOutput);

View File

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

View File

@ -30,15 +30,31 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
*/
public static final class HlsUrl {
/**
* The http url from which the media playlist can be obtained.
*/
public final String url;
/**
* Format information associated with the HLS url.
*/
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.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) {
this.url = url;
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;
/**
* The list of demuxed audios declared by the playlist.
*/
public final List<HlsUrl> audios;
/**
* The list of subtitles declared by the playlist.
*/
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;
/**
* 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;
/**
* @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,
List<HlsUrl> subtitles, Format muxedAudioFormat, List<Format> muxedCaptionFormats) {
super(baseUri);
@ -60,14 +102,20 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
this.audios = Collections.unmodifiableList(audios);
this.subtitles = Collections.unmodifiableList(subtitles);
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();
return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null,
Collections.<Format>emptyList());
return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null, null);
}
}

View File

@ -91,12 +91,14 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
public final boolean hasProgramDateTime;
public final Segment initializationSegment;
public final List<Segment> segments;
public final List<String> dateRanges;
public final long durationUs;
public HlsMediaPlaylist(@PlaylistType int playlistType, String baseUri, long startOffsetUs,
long startTimeUs, boolean hasDiscontinuitySequence, int discontinuitySequence,
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);
this.playlistType = playlistType;
this.startTimeUs = startTimeUs;
@ -117,6 +119,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
}
this.startOffsetUs = startOffsetUs == C.TIME_UNSET ? C.TIME_UNSET
: 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) {
return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs, true,
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,
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.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
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_KEY = "#EXT-X-KEY";
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_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_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_CODECS = Pattern.compile("CODECS=\"(.+?)\"");
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> subtitles = new ArrayList<>();
Format muxedAudioFormat = null;
ArrayList<Format> muxedCaptionFormats = new ArrayList<>();
List<Format> muxedCaptionFormats = null;
boolean noClosedCaptions = false;
String line;
while (iterator.hasNext()) {
@ -209,6 +214,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
mimeType = MimeTypes.APPLICATION_CEA708;
accessibilityChannel = Integer.parseInt(instreamId.substring(7));
}
if (muxedCaptionFormats == null) {
muxedCaptionFormats = new ArrayList<>();
}
muxedCaptionFormats.add(Format.createTextContainerFormat(id, null, mimeType, null,
Format.NO_VALUE, selectionFlags, language, accessibilityChannel));
break;
@ -220,6 +228,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
int bitrate = parseIntAttr(line, REGEX_BANDWIDTH);
String codecs = parseOptionalStringAttr(line, REGEX_CODECS);
String resolutionString = parseOptionalStringAttr(line, REGEX_RESOLUTION);
noClosedCaptions |= line.contains(ATTR_CLOSED_CAPTIONS_NONE);
int width;
int height;
if (resolutionString != null) {
@ -242,6 +251,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
variants.add(new HlsMasterPlaylist.HlsUrl(line, format));
}
}
if (noClosedCaptions) {
muxedCaptionFormats = Collections.emptyList();
}
return new HlsMasterPlaylist(baseUri, variants, audios, subtitles, muxedAudioFormat,
muxedCaptionFormats);
}
@ -263,6 +275,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
boolean hasEndTag = false;
Segment initializationSegment = null;
List<Segment> segments = new ArrayList<>();
List<String> dateRanges = new ArrayList<>();
long segmentDurationUs = 0;
boolean hasDiscontinuitySequence = false;
@ -343,6 +356,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
C.msToUs(Util.parseXsDateTime(line.substring(line.indexOf(':') + 1)));
playlistStartTimeUs = programDatetimeUs - segmentStartTimeUs;
}
} else if (line.startsWith(TAG_DATERANGE)) {
dateRanges.add(line);
} else if (!line.startsWith("#")) {
String segmentEncryptionIV;
if (!isEncrypted) {
@ -371,7 +386,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
}
return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, playlistStartTimeUs,
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 {

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
* playlist, this method throws the underlying error. Otherwise, does nothing.
* If the tracker is having trouble refreshing the master playlist or the primary playlist, this
* method throws the underlying error. Otherwise, does nothing.
*
* @throws IOException The underlying error.
*/
public void maybeThrowPlaylistRefreshError() throws IOException {
public void maybeThrowPrimaryPlaylistRefreshError() throws IOException {
initialPlaylistLoader.maybeThrowError();
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.
*

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_BAR_HEIGHT = 4;
private static final int DEFAULT_TOUCH_TARGET_HEIGHT = 26;
private static final int DEFAULT_PLAYED_COLOR = 0x33FFFFFF;
private static final int DEFAULT_BUFFERED_COLOR = 0xCCFFFFFF;
private static final int DEFAULT_PLAYED_COLOR = 0xFFFFFFFF;
private static final int DEFAULT_AD_MARKER_COLOR = 0xB2FFFF00;
private static final int DEFAULT_AD_MARKER_WIDTH = 4;
private static final int DEFAULT_SCRUBBER_ENABLED_SIZE = 12;
private static final int DEFAULT_SCRUBBER_DISABLED_SIZE = 0;
private static final int DEFAULT_SCRUBBER_DRAGGED_SIZE = 16;
private static final int OPAQUE_COLOR = 0xFF000000;
private final Rect seekBounds;
private final Rect progressBar;
private final Rect bufferedBar;
private final Rect scrubberBar;
private final Paint progressPaint;
private final Paint bufferedPaint;
private final Paint playedPaint;
private final Paint scrubberPaint;
private final Paint bufferedPaint;
private final Paint unplayedPaint;
private final Paint adMarkerPaint;
private final int barHeight;
private final int touchTargetHeight;
@ -115,9 +114,10 @@ public class DefaultTimeBar extends View implements TimeBar {
progressBar = new Rect();
bufferedBar = new Rect();
scrubberBar = new Rect();
progressPaint = new Paint();
bufferedPaint = new Paint();
playedPaint = new Paint();
scrubberPaint = new Paint();
bufferedPaint = new Paint();
unplayedPaint = new Paint();
adMarkerPaint = new Paint();
// Calculate the dimensions and paints for drawn elements.
@ -147,13 +147,18 @@ public class DefaultTimeBar extends View implements TimeBar {
scrubberDraggedSize = a.getDimensionPixelSize(
R.styleable.DefaultTimeBar_scrubber_dragged_size, defaultScrubberDraggedSize);
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,
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,
DEFAULT_AD_MARKER_COLOR);
progressPaint.setColor(playedColor);
scrubberPaint.setColor(OPAQUE_COLOR | playedColor);
playedPaint.setColor(playedColor);
scrubberPaint.setColor(scrubberColor);
bufferedPaint.setColor(bufferedColor);
unplayedPaint.setColor(unplayedColor);
adMarkerPaint.setColor(adMarkerColor);
} finally {
a.recycle();
@ -165,9 +170,10 @@ public class DefaultTimeBar extends View implements TimeBar {
scrubberEnabledSize = defaultScrubberEnabledSize;
scrubberDisabledSize = defaultScrubberDisabledSize;
scrubberDraggedSize = defaultScrubberDraggedSize;
scrubberPaint.setColor(OPAQUE_COLOR | DEFAULT_PLAYED_COLOR);
progressPaint.setColor(DEFAULT_PLAYED_COLOR);
bufferedPaint.setColor(DEFAULT_BUFFERED_COLOR);
playedPaint.setColor(DEFAULT_PLAYED_COLOR);
scrubberPaint.setColor(getDefaultScrubberColor(DEFAULT_PLAYED_COLOR));
bufferedPaint.setColor(getDefaultBufferedColor(DEFAULT_PLAYED_COLOR));
unplayedPaint.setColor(getDefaultUnplayedColor(DEFAULT_PLAYED_COLOR));
adMarkerPaint.setColor(DEFAULT_AD_MARKER_COLOR);
}
formatBuilder = new StringBuilder();
@ -337,16 +343,18 @@ public class DefaultTimeBar extends View implements TimeBar {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(measureWidth, measureHeight);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int height = heightMode == MeasureSpec.UNSPECIFIED ? touchTargetHeight
: heightMode == MeasureSpec.EXACTLY ? heightSize : Math.min(touchTargetHeight, heightSize);
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), height);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int width = right - left;
int height = bottom - top;
int barY = height - touchTargetHeight;
int barY = (height - touchTargetHeight) / 2;
int seekLeft = getPaddingLeft();
int seekRight = width - getPaddingRight();
int progressY = barY + (touchTargetHeight - barHeight) / 2;
@ -457,12 +465,10 @@ public class DefaultTimeBar extends View implements TimeBar {
scrubberBar.set(progressBar);
long newScrubberTime = scrubbing ? scrubPosition : position;
if (duration > 0) {
int bufferedPixelWidth =
(int) ((progressBar.width() * bufferedPosition) / duration);
bufferedBar.right = progressBar.left + bufferedPixelWidth;
int scrubberPixelPosition =
(int) ((progressBar.width() * newScrubberTime) / duration);
scrubberBar.right = progressBar.left + scrubberPixelPosition;
int bufferedPixelWidth = (int) ((progressBar.width() * bufferedPosition) / duration);
bufferedBar.right = Math.min(progressBar.left + bufferedPixelWidth, progressBar.right);
int scrubberPixelPosition = (int) ((progressBar.width() * newScrubberTime) / duration);
scrubberBar.right = Math.min(progressBar.left + scrubberPixelPosition, progressBar.right);
} else {
bufferedBar.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 barBottom = barTop + progressBarHeight;
if (duration <= 0) {
canvas.drawRect(progressBar.left, barTop, progressBar.right, barBottom, progressPaint);
canvas.drawRect(progressBar.left, barTop, progressBar.right, barBottom, unplayedPaint);
return;
}
int bufferedLeft = bufferedBar.left;
int bufferedRight = bufferedBar.right;
int progressLeft = Math.max(Math.max(progressBar.left, bufferedRight), scrubberBar.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);
if (bufferedRight > bufferedLeft) {
canvas.drawRect(bufferedLeft, barTop, bufferedRight, barBottom, bufferedPaint);
}
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;
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);
}
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_dragged_size" format="dimension"/>
<attr name="played_color" format="color"/>
<attr name="scrubber_color" format="color"/>
<attr name="buffered_color" format="color"/>
<attr name="unplayed_color" format="color"/>
<attr name="ad_marker_color" format="color"/>
</declare-styleable>