Merge pull request #444 from google/dev

dev -> dev-webm-vp9-opus
This commit is contained in:
ojw28 2015-05-11 21:17:17 +01:00
commit dd41d14ac3
45 changed files with 452 additions and 210 deletions

View File

@ -20,7 +20,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.0.0'
classpath 'com.android.tools.build:gradle:1.2.2'
classpath 'com.novoda:bintray-release:0.2.7'
}
}

View File

@ -14,12 +14,12 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 21
buildToolsVersion "21.1.2"
compileSdkVersion 22
buildToolsVersion "22.0.1"
defaultConfig {
minSdkVersion 16
targetSdkVersion 21
targetSdkVersion 22
}
buildTypes {
release {

View File

@ -25,7 +25,7 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="21"/>
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="22"/>
<application
android:label="@string/application_name"

View File

@ -25,7 +25,6 @@ import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
@ -52,7 +51,7 @@ public class DemoUtil {
}
public static byte[] executePost(String url, byte[] data, Map<String, String> requestProperties)
throws MalformedURLException, IOException {
throws IOException {
HttpURLConnection urlConnection = null;
try {
urlConnection = (HttpURLConnection) new URL(url).openConnection();

View File

@ -229,7 +229,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
return new DashRendererBuilder(this, userAgent, contentUri.toString(),
new WidevineTestMediaDrmCallback(contentId), debugTextView, audioCapabilities);
case DemoUtil.TYPE_HLS:
return new HlsRendererBuilder(this, userAgent, contentUri.toString(), debugTextView);
return new HlsRendererBuilder(this, userAgent, contentUri.toString(), debugTextView,
audioCapabilities);
case DemoUtil.TYPE_M4A: // There are no file format differences between M4A and MP4.
case DemoUtil.TYPE_MP4:
return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView,
@ -239,7 +240,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
new Mp3Extractor());
case DemoUtil.TYPE_TS:
return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView,
new TsExtractor());
new TsExtractor(0, audioCapabilities));
case DemoUtil.TYPE_AAC:
return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView,
new AdtsExtractor());

View File

@ -130,8 +130,6 @@ import java.util.Locale;
public static final Sample[] MISC = new Sample[] {
new Sample("Dizzy", "http://html5demos.com/assets/dizzy.mp4",
DemoUtil.TYPE_MP4),
new Sample("Dizzy (https->http redirect)", "https://goo.gl/MtUDEj",
DemoUtil.TYPE_MP4),
new Sample("Apple AAC 10s", "https://devimages.apple.com.edgekey.net/"
+ "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac",
DemoUtil.TYPE_AAC),

View File

@ -22,8 +22,6 @@ import android.media.MediaDrm.KeyRequest;
import android.media.MediaDrm.ProvisionRequest;
import android.text.TextUtils;
import org.apache.http.client.ClientProtocolException;
import java.io.IOException;
import java.util.UUID;
@ -43,8 +41,7 @@ public class WidevineTestMediaDrmCallback implements MediaDrmCallback {
}
@Override
public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request)
throws ClientProtocolException, IOException {
public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException {
String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData());
return DemoUtil.executePost(url, null, null);
}

View File

@ -59,7 +59,6 @@ import com.google.android.exoplayer.util.Util;
import android.annotation.TargetApi;
import android.content.Context;
import android.media.AudioFormat;
import android.media.MediaCodec;
import android.media.UnsupportedSchemeException;
import android.os.Handler;
@ -249,7 +248,6 @@ public class DashRendererBuilder implements RendererBuilder,
// Build the audio chunk sources.
List<ChunkSource> audioChunkSourceList = new ArrayList<ChunkSource>();
List<String> audioTrackNameList = new ArrayList<String>();
int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;
if (audioAdaptationSet != null) {
DataSource audioDataSource = new DefaultUriDataSource(userAgent, bandwidthMeter);
FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator();
@ -275,7 +273,6 @@ public class DashRendererBuilder implements RendererBuilder,
continue;
}
audioEncoding = encoding;
for (int j = audioRepresentations.size() - 1; j >= 0; j--) {
if (!audioRepresentations.get(j).format.codecs.equals(codec)) {
audioTrackNameList.remove(j);
@ -303,7 +300,7 @@ public class DashRendererBuilder implements RendererBuilder,
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
DemoPlayer.TYPE_AUDIO);
audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource, drmSessionManager, true,
mainHandler, player, audioEncoding);
mainHandler, player);
}
// Build the text chunk sources.

View File

@ -57,7 +57,7 @@ import android.widget.TextView;
}
@Override
protected int doPrepare() throws ExoPlaybackException {
protected int doPrepare(long positionUs) throws ExoPlaybackException {
maybeFail();
return STATE_PREPARED;
}

View File

@ -19,6 +19,7 @@ import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException;
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.audio.AudioCapabilities;
import com.google.android.exoplayer.chunk.VideoFormatSelectorUtil;
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder;
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilderCallback;
@ -56,15 +57,18 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<Hls
private final String userAgent;
private final String url;
private final TextView debugTextView;
private final AudioCapabilities audioCapabilities;
private DemoPlayer player;
private RendererBuilderCallback callback;
public HlsRendererBuilder(Context context, String userAgent, String url, TextView debugTextView) {
public HlsRendererBuilder(Context context, String userAgent, String url, TextView debugTextView,
AudioCapabilities audioCapabilities) {
this.context = context;
this.userAgent = userAgent;
this.url = url;
this.debugTextView = debugTextView;
this.audioCapabilities = audioCapabilities;
}
@Override
@ -101,7 +105,7 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<Hls
DataSource dataSource = new DefaultUriDataSource(userAgent, bandwidthMeter);
HlsChunkSource chunkSource = new HlsChunkSource(dataSource, url, manifest, bandwidthMeter,
variantIndices, HlsChunkSource.ADAPTIVE_MODE_SPLICE);
variantIndices, HlsChunkSource.ADAPTIVE_MODE_SPLICE, audioCapabilities);
HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, true, 3, REQUESTED_BUFFER_SIZE,
REQUESTED_BUFFER_DURATION_MS, mainHandler, player, DemoPlayer.TYPE_VIDEO);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,

View File

@ -8,6 +8,6 @@
# project structure.
# Project target.
target=android-21
target=android-22
android.library=false
android.library.reference.1=../../../library/src/main

View File

@ -15,8 +15,8 @@ apply plugin: 'com.android.library'
apply plugin: 'bintray-release'
android {
compileSdkVersion 21
buildToolsVersion "21.1.2"
compileSdkVersion 22
buildToolsVersion "22.0.1"
defaultConfig {
// Important: ExoPlayerLib specifies a minSdkVersion of 9 because
@ -25,7 +25,7 @@ android {
// functionality provided by the library requires API level 16 or
// greater.
minSdkVersion 9
targetSdkVersion 21
targetSdkVersion 22
}
buildTypes {

View File

@ -27,6 +27,6 @@
the library may be of use on older devices. However, please note that the core video playback
functionality provided by the library requires API level 16 or greater.
-->
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="21"/>
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="22"/>
</manifest>

View File

@ -18,14 +18,14 @@ package com.google.android.exoplayer;
/**
* A {@link TrackRenderer} that does nothing.
* <p>
* This renderer returns {@link TrackRenderer#STATE_IGNORE} from {@link #doPrepare()} in order to
* request that it should be ignored. {@link IllegalStateException} is thrown from all methods that
* are documented to indicate that they should not be invoked unless the renderer is prepared.
* This renderer returns {@link TrackRenderer#STATE_IGNORE} from {@link #doPrepare(long)} in order
* to request that it should be ignored. {@link IllegalStateException} is thrown from all methods
* that are documented to indicate that they should not be invoked unless the renderer is prepared.
*/
public class DummyTrackRenderer extends TrackRenderer {
@Override
protected int doPrepare() throws ExoPlaybackException {
protected int doPrepare(long positionUs) throws ExoPlaybackException {
return STATE_IGNORE;
}

View File

@ -101,7 +101,7 @@ public interface ExoPlayer {
* The default minimum duration of data that must be buffered for playback to start or resume
* following a user action such as a seek.
*/
public static final int DEFAULT_MIN_BUFFER_MS = 500;
public static final int DEFAULT_MIN_BUFFER_MS = 2500;
/**
* The default minimum duration of data that must be buffered for playback to resume

View File

@ -264,7 +264,7 @@ import java.util.List;
boolean prepared = true;
for (int i = 0; i < renderers.length; i++) {
if (renderers[i].getState() == TrackRenderer.STATE_UNPREPARED) {
int state = renderers[i].prepare();
int state = renderers[i].prepare(positionUs);
if (state == TrackRenderer.STATE_UNPREPARED) {
prepared = false;
}

View File

@ -128,7 +128,7 @@ public final class FrameworkSampleSource implements SampleSource {
}
@Override
public boolean prepare() throws IOException {
public boolean prepare(long positionUs) throws IOException {
if (!prepared) {
extractor = new MediaExtractor();
if (context != null) {

View File

@ -21,9 +21,7 @@ import com.google.android.exoplayer.drm.DrmSessionManager;
import com.google.android.exoplayer.util.MimeTypes;
import android.annotation.TargetApi;
import android.media.AudioFormat;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.media.audiofx.Virtualizer;
import android.os.Handler;
@ -71,7 +69,6 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
private final EventListener eventListener;
private final AudioTrack audioTrack;
private final int encoding;
private int audioSessionId;
private long currentPositionUs;
@ -124,50 +121,27 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
*/
public MediaCodecAudioTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener) {
this(source, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener,
AudioFormat.ENCODING_PCM_16BIT);
}
/**
* @param source The upstream source from which the renderer obtains samples.
* @param drmSessionManager For use with encrypted content. May be null if support for encrypted
* content is not required.
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
* For example a media file may start with a short clear region so as to allow playback to
* begin in parallel with key acquisision. This parameter specifies whether the renderer is
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
* has obtained the keys necessary to decrypt encrypted regions of the media.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param encoding One of the {@code AudioFormat.ENCODING_*} constants specifying the audio
* encoding.
*/
public MediaCodecAudioTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener,
int encoding) {
super(source, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener);
this.eventListener = eventListener;
this.audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
this.audioTrack = new AudioTrack();
this.encoding = encoding;
}
@Override
protected DecoderInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder)
throws DecoderQueryException {
if (encoding == AudioFormat.ENCODING_AC3 || encoding == AudioFormat.ENCODING_E_AC3) {
if (MimeTypes.isPassthroughAudio(mimeType)) {
return new DecoderInfo(RAW_DECODER_NAME, true);
}
return super.getDecoderInfo(mimeType, requiresSecureDecoder);
}
@Override
protected void configureCodec(MediaCodec codec, String codecName, MediaFormat format,
android.media.MediaCrypto crypto) {
protected void configureCodec(MediaCodec codec, String codecName,
android.media.MediaFormat format, android.media.MediaCrypto crypto) {
if (RAW_DECODER_NAME.equals(codecName)) {
// Override the MIME type used to configure the codec if we are using a passthrough decoder.
String mimeType = format.getString(MediaFormat.KEY_MIME);
String mimeType = format.getString(android.media.MediaFormat.KEY_MIME);
format.setString(android.media.MediaFormat.KEY_MIME, MimeTypes.AUDIO_RAW);
codec.configure(format, null, crypto, 0);
format.setString(android.media.MediaFormat.KEY_MIME, mimeType);
@ -193,8 +167,13 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
}
@Override
protected void onOutputFormatChanged(MediaFormat format) {
audioTrack.reconfigure(format, encoding, 0);
protected void onOutputFormatChanged(MediaFormat inputFormat,
android.media.MediaFormat outputFormat) {
if (MimeTypes.isPassthroughAudio(inputFormat.mimeType)) {
audioTrack.reconfigure(inputFormat.getFrameworkMediaFormatV16());
} else {
audioTrack.reconfigure(outputFormat);
}
}
/**

View File

@ -243,9 +243,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
}
@Override
protected int doPrepare() throws ExoPlaybackException {
protected int doPrepare(long positionUs) throws ExoPlaybackException {
try {
boolean sourcePrepared = source.prepare();
boolean sourcePrepared = source.prepare(positionUs);
if (!sourcePrepared) {
return TrackRenderer.STATE_UNPREPARED;
}
@ -742,9 +742,11 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
* <p>
* The default implementation is a no-op.
*
* @param format The new output format.
* @param inputFormat The format of media input to the codec.
* @param outputFormat The new output format.
*/
protected void onOutputFormatChanged(android.media.MediaFormat format) {
protected void onOutputFormatChanged(MediaFormat inputFormat,
android.media.MediaFormat outputFormat) {
// Do nothing.
}
@ -818,7 +820,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
}
if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
onOutputFormatChanged(codec.getOutputFormat());
onOutputFormatChanged(format, codec.getOutputFormat());
codecCounters.outputFormatChangedCount++;
return true;
} else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {

View File

@ -381,15 +381,17 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
}
@Override
protected void onOutputFormatChanged(android.media.MediaFormat format) {
boolean hasCrop = format.containsKey(KEY_CROP_RIGHT) && format.containsKey(KEY_CROP_LEFT)
&& format.containsKey(KEY_CROP_BOTTOM) && format.containsKey(KEY_CROP_TOP);
protected void onOutputFormatChanged(MediaFormat inputFormat,
android.media.MediaFormat outputFormat) {
boolean hasCrop = outputFormat.containsKey(KEY_CROP_RIGHT)
&& outputFormat.containsKey(KEY_CROP_LEFT) && outputFormat.containsKey(KEY_CROP_BOTTOM)
&& outputFormat.containsKey(KEY_CROP_TOP);
currentWidth = hasCrop
? format.getInteger(KEY_CROP_RIGHT) - format.getInteger(KEY_CROP_LEFT) + 1
: format.getInteger(android.media.MediaFormat.KEY_WIDTH);
? outputFormat.getInteger(KEY_CROP_RIGHT) - outputFormat.getInteger(KEY_CROP_LEFT) + 1
: outputFormat.getInteger(android.media.MediaFormat.KEY_WIDTH);
currentHeight = hasCrop
? format.getInteger(KEY_CROP_BOTTOM) - format.getInteger(KEY_CROP_TOP) + 1
: format.getInteger(android.media.MediaFormat.KEY_HEIGHT);
? outputFormat.getInteger(KEY_CROP_BOTTOM) - outputFormat.getInteger(KEY_CROP_TOP) + 1
: outputFormat.getInteger(android.media.MediaFormat.KEY_HEIGHT);
}
@Override

View File

@ -57,10 +57,11 @@ public interface SampleSource {
* and formats). If insufficient data is available then the call will return {@code false} rather
* than block. The method can be called repeatedly until the return value indicates success.
*
* @param positionUs The player's current playback position.
* @return True if the source was prepared successfully, false otherwise.
* @throws IOException If an error occurred preparing the source.
*/
public boolean prepare() throws IOException;
public boolean prepare(long positionUs) throws IOException;
/**
* Returns the number of tracks exposed by the source.

View File

@ -108,11 +108,12 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
* Prepares the renderer. This method is non-blocking, and hence it may be necessary to call it
* more than once in order to transition the renderer into the prepared state.
*
* @param positionUs The player's current playback position.
* @return The current state (one of the STATE_* constants), for convenience.
*/
/* package */ final int prepare() throws ExoPlaybackException {
/* package */ final int prepare(long positionUs) throws ExoPlaybackException {
Assertions.checkState(state == TrackRenderer.STATE_UNPREPARED);
state = doPrepare();
state = doPrepare(positionUs);
Assertions.checkState(state == TrackRenderer.STATE_UNPREPARED ||
state == TrackRenderer.STATE_PREPARED ||
state == TrackRenderer.STATE_IGNORE);
@ -127,11 +128,12 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
* This method should return quickly, and should not block if the renderer is currently unable to
* make any useful progress.
*
* @param positionUs The player's current playback position.
* @return The new state of the renderer. One of {@link #STATE_UNPREPARED},
* {@link #STATE_PREPARED} and {@link #STATE_IGNORE}.
* @throws ExoPlaybackException If an error occurs.
*/
protected abstract int doPrepare() throws ExoPlaybackException;
protected abstract int doPrepare(long positionUs) throws ExoPlaybackException;
/**
* Enable the renderer.

View File

@ -18,6 +18,7 @@ package com.google.android.exoplayer.audio;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.util.Ac3Util;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util;
import android.annotation.TargetApi;
@ -315,24 +316,21 @@ public final class AudioTrack {
}
/**
* Reconfigures the audio track to play back media in {@code format}. The encoding is assumed to
* be {@link AudioFormat#ENCODING_PCM_16BIT}.
* Reconfigures the audio track to play back media in {@code format}, inferring a buffer size from
* the format.
*/
public void reconfigure(MediaFormat format) {
reconfigure(format, AudioFormat.ENCODING_PCM_16BIT, 0);
reconfigure(format, 0);
}
/**
* Reconfigures the audio track to play back media in {@code format}. Buffers passed to
* {@link #handleBuffer} must use the specified {@code encoding}, which should be a constant from
* {@link AudioFormat}.
* Reconfigures the audio track to play back media in {@code format}.
*
* @param format Specifies the channel count and sample rate to play back.
* @param encoding The format in which audio is represented.
* @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to use a
* size inferred from the format.
*/
public void reconfigure(MediaFormat format, int encoding, int specifiedBufferSize) {
public void reconfigure(MediaFormat format, int specifiedBufferSize) {
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
int channelConfig;
switch (channelCount) {
@ -353,8 +351,10 @@ public final class AudioTrack {
}
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
String mimeType = format.getString(MediaFormat.KEY_MIME);
// TODO: Does channelConfig determine channelCount?
int encoding = MimeTypes.getEncodingForMimeType(mimeType);
boolean isAc3 = encoding == C.ENCODING_AC3 || encoding == C.ENCODING_E_AC3;
if (isInitialized() && this.sampleRate == sampleRate && this.channelConfig == channelConfig
&& !this.isAc3 && !isAc3) {
@ -423,12 +423,23 @@ public final class AudioTrack {
return RESULT_BUFFER_CONSUMED;
}
// As a workaround for an issue where an an AC-3 audio track continues to play data written
// while it is paused, stop writing so its buffer empties. See [Internal: b/18899620].
if (isAc3 && audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED) {
// Workarounds for issues with AC-3 passthrough AudioTracks on API versions 21/22:
if (Util.SDK_INT <= 22 && isAc3) {
// An AC-3 audio track continues to play data written while it is paused. Stop writing so its
// buffer empties. See [Internal: b/18899620].
if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED) {
return 0;
}
// A new AC-3 audio track's playback position continues to increase from the old track's
// position for a short time after is has been released. Avoid writing data until the playback
// head position actually returns to zero.
if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_STOPPED
&& audioTrackUtil.getPlaybackHeadPosition() != 0) {
return 0;
}
}
int result = 0;
if (temporaryBufferSize == 0) {
if (isAc3 && ac3Bitrate == UNKNOWN_AC3_BITRATE) {
@ -637,7 +648,8 @@ public final class AudioTrack {
}
if (systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) {
audioTimestampSet = audioTrackUtil.updateTimestamp();
// Don't use AudioTrack.getTimestamp() on AC-3 tracks, as it gives an incorrect timestamp.
audioTimestampSet = !isAc3 && audioTrackUtil.updateTimestamp();
if (audioTimestampSet) {
// Perform sanity checks on the timestamp.
long audioTimestampUs = audioTrackUtil.getTimestampNanoTime() / 1000;
@ -739,7 +751,7 @@ public final class AudioTrack {
private static class AudioTrackUtil {
protected android.media.AudioTrack audioTrack;
private boolean enablePassthroughWorkaround;
private boolean isPassthrough;
private int sampleRate;
private long lastRawPlaybackHeadPosition;
private long rawPlaybackHeadWrapCount;
@ -749,14 +761,11 @@ public final class AudioTrack {
* Reconfigures the audio track utility helper to use the specified {@code audioTrack}.
*
* @param audioTrack The audio track to wrap.
* @param enablePassthroughWorkaround Whether to work around an issue where the playback head
* position jumps back to zero on a paused passthrough/direct audio track. See
* [Internal: b/19187573].
* @param isPassthrough Whether the audio track is used for passthrough (e.g. AC-3) playback.
*/
public void reconfigure(android.media.AudioTrack audioTrack,
boolean enablePassthroughWorkaround) {
public void reconfigure(android.media.AudioTrack audioTrack, boolean isPassthrough) {
this.audioTrack = audioTrack;
this.enablePassthroughWorkaround = enablePassthroughWorkaround;
this.isPassthrough = isPassthrough;
lastRawPlaybackHeadPosition = 0;
rawPlaybackHeadWrapCount = 0;
passthroughWorkaroundPauseOffset = 0;
@ -767,14 +776,14 @@ public final class AudioTrack {
/**
* Returns whether the audio track should behave as though it has pending data. This is to work
* around an issue where AC-3 audio tracks can't be paused, so we empty their buffers when
* paused. In this case, they should still behave as if they have pending data, otherwise
* writing will never resume.
* around an issue on platform API versions 21/22 where AC-3 audio tracks can't be paused, so we
* empty their buffers when paused. In this case, they should still behave as if they have
* pending data, otherwise writing will never resume.
*
* @see #handleBuffer
*/
public boolean overrideHasPendingData() {
return enablePassthroughWorkaround
return Util.SDK_INT <= 22 && isPassthrough
&& audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED
&& audioTrack.getPlaybackHeadPosition() == 0;
}
@ -790,8 +799,16 @@ public final class AudioTrack {
*/
public long getPlaybackHeadPosition() {
long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition();
if (enablePassthroughWorkaround) {
if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED
if (Util.SDK_INT <= 22 && isPassthrough) {
// Work around issues with passthrough/direct AudioTracks on platform API versions 21/22:
// - After resetting, the new AudioTrack's playback position continues to increase for a
// short time from the old AudioTrack's position, while in the PLAYSTATE_STOPPED state.
// - The playback head position jumps back to zero on paused passthrough/direct audio
// tracks. See [Internal: b/19187573].
if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_STOPPED) {
// Prevent detecting a wrapped position.
lastRawPlaybackHeadPosition = rawPlaybackHeadPosition;
} else if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED
&& rawPlaybackHeadPosition == 0) {
passthroughWorkaroundPauseOffset = lastRawPlaybackHeadPosition;
}
@ -868,9 +885,8 @@ public final class AudioTrack {
}
@Override
public void reconfigure(android.media.AudioTrack audioTrack,
boolean enablePassthroughWorkaround) {
super.reconfigure(audioTrack, enablePassthroughWorkaround);
public void reconfigure(android.media.AudioTrack audioTrack, boolean isPassthrough) {
super.reconfigure(audioTrack, isPassthrough);
rawTimestampFramePositionWrapCount = 0;
lastRawTimestampFramePosition = 0;
lastTimestampFramePosition = 0;

View File

@ -87,7 +87,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
private long currentLoadStartTimeMs;
private MediaFormat downstreamMediaFormat;
private volatile Format downstreamFormat;
private Format downstreamFormat;
public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
int bufferSizeContribution, boolean frameAccurateSeeking) {
@ -120,18 +120,8 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
pendingResetPositionUs = NO_RESET_PENDING;
}
/**
* Exposes the current downstream format for debugging purposes. Can be called from any thread.
*
* @return The current downstream format.
*/
@Deprecated
public Format getFormat() {
return downstreamFormat;
}
@Override
public boolean prepare() {
public boolean prepare(long positionUs) {
Assertions.checkState(state == STATE_UNPREPARED);
loader = new Loader("Loader:" + chunkSource.getTrackInfo().mimeType);
state = STATE_PREPARED;

View File

@ -84,7 +84,7 @@ public interface FormatEvaluator {
/**
* Always selects the first format.
*/
public static class FixedEvaluator implements FormatEvaluator {
public static final class FixedEvaluator implements FormatEvaluator {
@Override
public void enable() {
@ -107,7 +107,7 @@ public interface FormatEvaluator {
/**
* Selects randomly between the available formats.
*/
public static class RandomEvaluator implements FormatEvaluator {
public static final class RandomEvaluator implements FormatEvaluator {
private final Random random;
@ -145,7 +145,7 @@ public interface FormatEvaluator {
* reference implementation only. It is recommended that application developers implement their
* own adaptive evaluator to more precisely suit their use case.
*/
public static class AdaptiveEvaluator implements FormatEvaluator {
public static final class AdaptiveEvaluator implements FormatEvaluator {
public static final int DEFAULT_MAX_INITIAL_BITRATE = 800000;
@ -259,8 +259,9 @@ public interface FormatEvaluator {
/**
* Compute the ideal format ignoring buffer health.
*/
protected Format determineIdealFormat(Format[] formats, long bitrateEstimate) {
long effectiveBitrate = computeEffectiveBitrateEstimate(bitrateEstimate);
private Format determineIdealFormat(Format[] formats, long bitrateEstimate) {
long effectiveBitrate = bitrateEstimate == BandwidthMeter.NO_ESTIMATE
? maxInitialBitrate : (long) (bitrateEstimate * bandwidthFraction);
for (int i = 0; i < formats.length; i++) {
Format format = formats[i];
if (format.bitrate <= effectiveBitrate) {
@ -271,14 +272,6 @@ public interface FormatEvaluator {
return formats[formats.length - 1];
}
/**
* Apply overhead factor, or default value in absence of estimate.
*/
protected long computeEffectiveBitrateEstimate(long bitrateEstimate) {
return bitrateEstimate == BandwidthMeter.NO_ESTIMATE
? maxInitialBitrate : (long) (bitrateEstimate * bandwidthFraction);
}
}
}

View File

@ -419,7 +419,9 @@ public class DashChunkSource implements ChunkSource {
(ChunkIndex) initializationChunk.getSeekMap(),
initializationChunk.dataSpec.uri.toString());
}
if (initializationChunk.hasDrmInitData()) {
// The null check avoids overwriting drmInitData obtained from the manifest with drmInitData
// obtained from the stream, as per DASH IF Interoperability Recommendations V3.0, 7.5.3.
if (drmInitData == null && initializationChunk.hasDrmInitData()) {
drmInitData = initializationChunk.getDrmInitData();
}
}

View File

@ -24,10 +24,12 @@ import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase;
import com.google.android.exoplayer.upstream.UriLoadable;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.UriUtil;
import com.google.android.exoplayer.util.Util;
import android.text.TextUtils;
import android.util.Base64;
import org.xml.sax.helpers.DefaultHandler;
import org.xmlpull.v1.XmlPullParser;
@ -41,6 +43,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -270,11 +273,27 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
protected ContentProtection parseContentProtection(XmlPullParser xpp)
throws XmlPullParserException, IOException {
String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri");
return buildContentProtection(schemeIdUri);
UUID uuid = null;
byte[] data = null;
do {
xpp.next();
// The cenc:pssh element is defined in 23001-7:2015
if (isStartTag(xpp, "cenc:pssh") && xpp.next() == XmlPullParser.TEXT) {
byte[] decodedData = Base64.decode(xpp.getText(), Base64.DEFAULT);
ParsableByteArray psshAtom = new ParsableByteArray(decodedData);
psshAtom.skipBytes(12);
uuid = new UUID(psshAtom.readLong(), psshAtom.readLong());
int dataSize = psshAtom.readInt();
data = new byte[dataSize];
psshAtom.readBytes(data, 0, dataSize);
}
} while (!isEndTag(xpp, "ContentProtection"));
return buildContentProtection(schemeIdUri, uuid, data);
}
protected ContentProtection buildContentProtection(String schemeIdUri) {
return new ContentProtection(schemeIdUri, null, null);
protected ContentProtection buildContentProtection(String schemeIdUri, UUID uuid, byte[] data) {
return new ContentProtection(schemeIdUri, uuid, data);
}
/**

View File

@ -72,6 +72,7 @@ public class ExtractorSampleSource implements SampleSource, ExtractorOutput, Loa
private boolean prepared;
private int enabledTrackCount;
private TrackInfo[] trackInfos;
private long maxTrackDurationUs;
private boolean[] pendingMediaFormat;
private boolean[] pendingDiscontinuities;
private boolean[] trackEnabledStates;
@ -137,7 +138,7 @@ public class ExtractorSampleSource implements SampleSource, ExtractorOutput, Loa
}
@Override
public boolean prepare() throws IOException {
public boolean prepare(long positionUs) throws IOException {
if (prepared) {
return true;
}
@ -156,9 +157,13 @@ public class ExtractorSampleSource implements SampleSource, ExtractorOutput, Loa
pendingDiscontinuities = new boolean[trackCount];
pendingMediaFormat = new boolean[trackCount];
trackInfos = new TrackInfo[trackCount];
maxTrackDurationUs = C.UNKNOWN_TIME_US;
for (int i = 0; i < trackCount; i++) {
MediaFormat format = sampleQueues.valueAt(i).getFormat();
trackInfos[i] = new TrackInfo(format.mimeType, format.durationUs);
if (format.durationUs != C.UNKNOWN_TIME_US && format.durationUs > maxTrackDurationUs) {
maxTrackDurationUs = format.durationUs;
}
}
prepared = true;
return true;
@ -448,6 +453,11 @@ public class ExtractorSampleSource implements SampleSource, ExtractorOutput, Loa
loadable = createLoadableFromStart();
} else {
Assertions.checkState(isPendingReset());
if (maxTrackDurationUs != C.UNKNOWN_TIME_US && pendingResetPositionUs >= maxTrackDurationUs) {
loadingFinished = true;
pendingResetPositionUs = NO_RESET_PENDING;
return;
}
loadable = createLoadableFromPositionUs(pendingResetPositionUs);
pendingResetPositionUs = NO_RESET_PENDING;
}

View File

@ -137,7 +137,7 @@ import com.google.android.exoplayer.util.Util;
}
long position = (long) ((1f / 256) * fx * sizeBytes) + firstFramePosition;
return inputLength != C.LENGTH_UNBOUNDED ? Math.min(position, inputLength) : position;
return inputLength != C.LENGTH_UNBOUNDED ? Math.min(position, inputLength - 1) : position;
}
@Override

View File

@ -534,8 +534,18 @@ import java.util.List;
childPosition += childAtomSize;
}
out.mediaFormat = MediaFormat.createAudioFormat(
MimeTypes.AUDIO_AAC, sampleSize, durationUs, channelCount, sampleRate,
// Set the MIME type for ac-3/ec-3 atoms even if the dac3/dec3 child atom is missing.
String mimeType;
if (atomType == Atom.TYPE_ac_3) {
mimeType = MimeTypes.AUDIO_AC3;
} else if (atomType == Atom.TYPE_ec_3) {
mimeType = MimeTypes.AUDIO_EC3;
} else {
mimeType = MimeTypes.AUDIO_AAC;
}
out.mediaFormat = MediaFormat.createAudioFormat(mimeType, sampleSize, durationUs, channelCount,
sampleRate,
initializationData == null ? null : Collections.singletonList(initializationData));
}

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer.extractor.ts;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.audio.AudioCapabilities;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput;
@ -43,7 +44,7 @@ public final class TsExtractor implements Extractor, SeekMap {
private static final int TS_STREAM_TYPE_AAC = 0x0F;
private static final int TS_STREAM_TYPE_ATSC_AC3 = 0x81;
private static final int TS_STREAM_TYPE_DVB_AC3 = 0x06;
private static final int TS_STREAM_TYPE_ATSC_E_AC3 = 0x87;
private static final int TS_STREAM_TYPE_H264 = 0x1B;
private static final int TS_STREAM_TYPE_ID3 = 0x15;
private static final int TS_STREAM_TYPE_EIA608 = 0x100; // 0xFF + 1
@ -52,6 +53,7 @@ public final class TsExtractor implements Extractor, SeekMap {
private final ParsableByteArray tsPacketBuffer;
private final SparseBooleanArray streamTypes;
private final SparseBooleanArray allowedPassthroughStreamTypes;
private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
private final long firstSampleTimestampUs;
private final ParsableBitArray tsScratch;
@ -62,14 +64,15 @@ public final class TsExtractor implements Extractor, SeekMap {
private long lastPts;
public TsExtractor() {
this(0);
this(0, null);
}
public TsExtractor(long firstSampleTimestampUs) {
public TsExtractor(long firstSampleTimestampUs, AudioCapabilities audioCapabilities) {
this.firstSampleTimestampUs = firstSampleTimestampUs;
tsScratch = new ParsableBitArray(new byte[3]);
tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE);
streamTypes = new SparseBooleanArray();
allowedPassthroughStreamTypes = getPassthroughStreamTypes(audioCapabilities);
tsPayloadReaders = new SparseArray<TsPayloadReader>();
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
lastPts = Long.MIN_VALUE;
@ -174,6 +177,24 @@ public final class TsExtractor implements Extractor, SeekMap {
return timeUs + timestampOffsetUs;
}
/**
* Returns a sparse boolean array of stream types that can be played back based on
* {@code audioCapabilities}.
*/
private static SparseBooleanArray getPassthroughStreamTypes(AudioCapabilities audioCapabilities) {
SparseBooleanArray streamTypes = new SparseBooleanArray();
if (audioCapabilities != null) {
if (audioCapabilities.supportsEncoding(C.ENCODING_AC3)) {
streamTypes.put(TS_STREAM_TYPE_ATSC_AC3, true);
}
if (audioCapabilities.supportsEncoding(C.ENCODING_E_AC3)) {
// TODO: Uncomment when Ac3Reader supports enhanced AC-3.
// streamTypes.put(TS_STREAM_TYPE_ATSC_E_AC3, true);
}
}
return streamTypes;
}
/**
* Parses TS packet payload data.
*/
@ -308,13 +329,17 @@ public final class TsExtractor implements Extractor, SeekMap {
continue;
}
// TODO: Detect and read DVB AC-3 streams with Ac3Reader.
ElementaryStreamReader pesPayloadReader = null;
switch (streamType) {
case TS_STREAM_TYPE_AAC:
pesPayloadReader = new AdtsReader(output.track(TS_STREAM_TYPE_AAC));
break;
case TS_STREAM_TYPE_ATSC_E_AC3:
case TS_STREAM_TYPE_ATSC_AC3:
case TS_STREAM_TYPE_DVB_AC3:
if (!allowedPassthroughStreamTypes.get(streamType)) {
continue;
}
pesPayloadReader = new Ac3Reader(output.track(streamType));
break;
case TS_STREAM_TYPE_H264:

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.audio.AudioCapabilities;
import com.google.android.exoplayer.chunk.BaseChunkSampleSourceEventListener;
import com.google.android.exoplayer.chunk.Chunk;
import com.google.android.exoplayer.chunk.DataChunk;
@ -125,6 +126,7 @@ public class HlsChunkSource {
private final int maxHeight;
private final long minBufferDurationToSwitchUpUs;
private final long maxBufferDurationToSwitchDownUs;
private final AudioCapabilities audioCapabilities;
/* package */ byte[] scratchSpace;
/* package */ final HlsMediaPlaylist[] mediaPlaylists;
@ -140,9 +142,11 @@ public class HlsChunkSource {
private byte[] encryptionIv;
public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist,
BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode) {
BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode,
AudioCapabilities audioCapabilities) {
this(dataSource, playlistUrl, playlist, bandwidthMeter, variantIndices, adaptiveMode,
DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS, DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS);
DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS, DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS,
audioCapabilities);
}
/**
@ -160,13 +164,17 @@ public class HlsChunkSource {
* for a switch to a higher quality variant to be considered.
* @param maxBufferDurationToSwitchDownMs The maximum duration of media that needs to be buffered
* for a switch to a lower quality variant to be considered.
* @param audioCapabilities The audio capabilities for playback on this device, or {@code null} if
* the default capabilities should be assumed.
*/
public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist,
BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode,
long minBufferDurationToSwitchUpMs, long maxBufferDurationToSwitchDownMs) {
long minBufferDurationToSwitchUpMs, long maxBufferDurationToSwitchDownMs,
AudioCapabilities audioCapabilities) {
this.dataSource = dataSource;
this.bandwidthMeter = bandwidthMeter;
this.adaptiveMode = adaptiveMode;
this.audioCapabilities = audioCapabilities;
minBufferDurationToSwitchUpUs = minBufferDurationToSwitchUpMs * 1000;
maxBufferDurationToSwitchDownUs = maxBufferDurationToSwitchDownMs * 1000;
baseUri = playlist.baseUri;
@ -235,16 +243,14 @@ public class HlsChunkSource {
public Chunk getChunkOperation(TsChunk previousTsChunk, long seekPositionUs,
long playbackPositionUs) {
int nextFormatIndex;
boolean switchingVariant;
boolean switchingVariantSpliced;
if (adaptiveMode == ADAPTIVE_MODE_NONE) {
nextFormatIndex = formatIndex;
switchingVariant = false;
switchingVariantSpliced = false;
} else {
nextFormatIndex = getNextFormatIndex(previousTsChunk, playbackPositionUs);
switchingVariant = nextFormatIndex != formatIndex;
switchingVariantSpliced = switchingVariant && adaptiveMode == ADAPTIVE_MODE_SPLICE;
switchingVariantSpliced = nextFormatIndex != formatIndex
&& adaptiveMode == ADAPTIVE_MODE_SPLICE;
}
int variantIndex = getVariantIndex(enabledFormats[nextFormatIndex]);
@ -331,10 +337,11 @@ public class HlsChunkSource {
// Configure the extractor that will read the chunk.
HlsExtractorWrapper extractorWrapper;
if (previousTsChunk == null || segment.discontinuity || switchingVariant || liveDiscontinuity) {
if (previousTsChunk == null || segment.discontinuity || !format.equals(previousTsChunk.format)
|| liveDiscontinuity) {
Extractor extractor = chunkUri.getLastPathSegment().endsWith(AAC_FILE_EXTENSION)
? new AdtsExtractor(startTimeUs)
: new TsExtractor(startTimeUs);
: new TsExtractor(startTimeUs, audioCapabilities);
extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor,
switchingVariantSpliced);
} else {

View File

@ -125,15 +125,12 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
}
@Override
public boolean prepare() throws IOException {
public boolean prepare(long positionUs) throws IOException {
if (prepared) {
return true;
}
if (loader == null) {
loader = new Loader("Loader:HLS");
}
continueBufferingInternal();
if (!extractors.isEmpty()) {
// We're not prepared, but we might have loaded what we need.
HlsExtractorWrapper extractor = extractors.getFirst();
if (extractor.isPrepared()) {
trackCount = extractor.getTrackCount();
@ -146,12 +143,23 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
trackInfos[i] = new TrackInfo(format.mimeType, chunkSource.getDurationUs());
}
prepared = true;
return true;
}
}
if (!prepared) {
// We're not prepared and we haven't loaded what we need.
if (loader == null) {
loader = new Loader("Loader:HLS");
}
if (!loader.isLoading()) {
// We're going to have to start loading a chunk to get what we need for preparation. We should
// attempt to load the chunk at positionUs, so that we'll already be loading the correct chunk
// in the common case where the renderer is subsequently enabled at this position.
pendingResetPositionUs = positionUs;
downstreamPositionUs = positionUs;
}
maybeStartLoading();
maybeThrowLoadableException();
}
return prepared;
return false;
}
@Override
@ -345,7 +353,12 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
if (!currentLoadableExceptionFatal) {
clearCurrentLoadable();
}
if (enabledTrackCount > 0) {
maybeStartLoading();
} else {
clearState();
allocator.trim(0);
}
}
@Override

View File

@ -90,9 +90,9 @@ public class MetadataTrackRenderer<T> extends TrackRenderer implements Callback
}
@Override
protected int doPrepare() throws ExoPlaybackException {
protected int doPrepare(long positionUs) throws ExoPlaybackException {
try {
boolean sourcePrepared = source.prepare();
boolean sourcePrepared = source.prepare(positionUs);
if (!sourcePrepared) {
return TrackRenderer.STATE_UNPREPARED;
}

View File

@ -80,9 +80,9 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
}
@Override
protected int doPrepare() throws ExoPlaybackException {
protected int doPrepare(long positionUs) throws ExoPlaybackException {
try {
boolean sourcePrepared = source.prepare();
boolean sourcePrepared = source.prepare(positionUs);
if (!sourcePrepared) {
return TrackRenderer.STATE_UNPREPARED;
}

View File

@ -90,9 +90,9 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
}
@Override
protected int doPrepare() throws ExoPlaybackException {
protected int doPrepare(long positionUs) throws ExoPlaybackException {
try {
boolean sourcePrepared = source.prepare();
boolean sourcePrepared = source.prepare(positionUs);
if (!sourcePrepared) {
return TrackRenderer.STATE_UNPREPARED;
}

View File

@ -18,7 +18,24 @@ package com.google.android.exoplayer.upstream;
/**
* Provides estimates of the currently available bandwidth.
*/
public interface BandwidthMeter {
public interface BandwidthMeter extends TransferListener {
/**
* Interface definition for a callback to be notified of {@link BandwidthMeter} events.
*/
public interface EventListener {
/**
* Invoked periodically to indicate that bytes have been transferred.
*
* @param elapsedMs The time taken to transfer the bytes, in milliseconds.
* @param bytes The number of bytes transferred.
* @param bitrate The estimated bitrate in bits/sec, or {@link #NO_ESTIMATE} if no estimate
* is available. Note that this estimate is typically derived from more information than
* {@code bytes} and {@code elapsedMs}.
*/
void onBandwidthSample(int elapsedMs, long bytes, long bitrate);
}
/**
* Indicates no bandwidth estimate is available.

View File

@ -26,25 +26,7 @@ import android.os.Handler;
* Counts transferred bytes while transfers are open and creates a bandwidth sample and updated
* bandwidth estimate each time a transfer ends.
*/
public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener {
/**
* Interface definition for a callback to be notified of {@link DefaultBandwidthMeter} events.
*/
public interface EventListener {
/**
* Invoked periodically to indicate that bytes have been transferred.
*
* @param elapsedMs The time taken to transfer the bytes, in milliseconds.
* @param bytes The number of bytes transferred.
* @param bitrate The estimated bitrate in bits/sec, or {@link #NO_ESTIMATE} if no estimate
* is available. Note that this estimate is typically derived from more information than
* {@code bytes} and {@code elapsedMs}.
*/
void onBandwidthSample(int elapsedMs, long bytes, long bitrate);
}
public class DefaultBandwidthMeter implements BandwidthMeter {
private static final int DEFAULT_MAX_WEIGHT = 2000;

View File

@ -28,6 +28,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.net.HttpURLConnection;
import java.net.NoRouteToHostException;
import java.net.ProtocolException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
@ -38,17 +40,30 @@ import java.util.regex.Pattern;
/**
* A {@link HttpDataSource} that uses Android's {@link HttpURLConnection}.
* <p>
* By default this implementation will not follow cross-protocol redirects (i.e. redirects from
* HTTP to HTTPS or vice versa). Cross-protocol redirects can be enabled by using the
* {@link #DefaultHttpDataSource(String, Predicate, TransferListener, int, int, boolean)}
* constructor and passing {@code true} as the final argument.
*/
public class DefaultHttpDataSource implements HttpDataSource {
/**
* The default connection timeout, in milliseconds.
*/
public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000;
/**
* The default read timeout, in milliseconds.
*/
public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000;
private static final int MAX_REDIRECTS = 20; // Same limit as okhttp.
private static final String TAG = "HttpDataSource";
private static final Pattern CONTENT_RANGE_HEADER =
Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<byte[]>();
private final boolean allowCrossProtocolRedirects;
private final int connectTimeoutMillis;
private final int readTimeoutMillis;
private final String userAgent;
@ -103,12 +118,33 @@ public class DefaultHttpDataSource implements HttpDataSource {
*/
public DefaultHttpDataSource(String userAgent, Predicate<String> contentTypePredicate,
TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis) {
this(userAgent, contentTypePredicate, listener, connectTimeoutMillis, readTimeoutMillis, false);
}
/**
* @param userAgent The User-Agent string that should be used.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is
* rejected by the predicate then a {@link HttpDataSource.InvalidContentTypeException} is
* thrown from {@link #open(DataSpec)}.
* @param listener An optional listener.
* @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is
* interpreted as an infinite timeout. Pass {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS} to use
* the default value.
* @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted
* as an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value.
* @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP
* to HTTPS and vice versa) are enabled.
*/
public DefaultHttpDataSource(String userAgent, Predicate<String> contentTypePredicate,
TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis,
boolean allowCrossProtocolRedirects) {
this.userAgent = Assertions.checkNotEmpty(userAgent);
this.contentTypePredicate = contentTypePredicate;
this.listener = listener;
this.requestProperties = new HashMap<String, String>();
this.connectTimeoutMillis = connectTimeoutMillis;
this.readTimeoutMillis = readTimeoutMillis;
this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;
}
@Override
@ -283,8 +319,58 @@ public class DefaultHttpDataSource implements HttpDataSource {
return bytesToRead == C.LENGTH_UNBOUNDED ? bytesToRead : bytesToRead - bytesRead;
}
/**
* Establishes a connection, following redirects to do so where permitted.
*/
private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException {
URL url = new URL(dataSpec.uri.toString());
long position = dataSpec.position;
long length = dataSpec.length;
boolean allowGzip = (dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) != 0;
if (!allowCrossProtocolRedirects) {
// HttpURLConnection disallows cross-protocol redirects, but otherwise performs redirection
// automatically. This is the behavior we want, so use it.
HttpURLConnection connection = configureConnection(url, position, length, allowGzip);
connection.connect();
return connection;
}
// We need to handle redirects ourselves to allow cross-protocol redirects.
int redirectCount = 0;
while (redirectCount++ <= MAX_REDIRECTS) {
HttpURLConnection connection = configureConnection(url, position, length, allowGzip);
connection.setInstanceFollowRedirects(false);
connection.connect();
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_MULT_CHOICE
|| responseCode == HttpURLConnection.HTTP_MOVED_PERM
|| responseCode == HttpURLConnection.HTTP_MOVED_TEMP
|| responseCode == HttpURLConnection.HTTP_SEE_OTHER
|| responseCode == 307 /* HTTP_TEMP_REDIRECT */
|| responseCode == 308 /* HTTP_PERM_REDIRECT */) {
String location = connection.getHeaderField("Location");
connection.disconnect();
url = handleRedirect(url, location);
} else {
return connection;
}
}
// If we get here we've been redirected more times than are permitted.
throw new NoRouteToHostException("Too many redirects: " + redirectCount);
}
/**
* Configures a connection, but does not open it.
*
* @param url The url to connect to.
* @param position The byte offset of the requested data.
* @param length The length of the requested data, or {@link C#LENGTH_UNBOUNDED}.
* @param allowGzip Whether to allow the use of gzip.
*/
private HttpURLConnection configureConnection(URL url, long position, long length,
boolean allowGzip) throws IOException {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(connectTimeoutMillis);
connection.setReadTimeout(readTimeoutMillis);
@ -294,28 +380,56 @@ public class DefaultHttpDataSource implements HttpDataSource {
connection.setRequestProperty(property.getKey(), property.getValue());
}
}
setRangeHeader(connection, dataSpec);
connection.setRequestProperty("User-Agent", userAgent);
if ((dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) == 0) {
connection.setRequestProperty("Accept-Encoding", "identity");
}
connection.connect();
return connection;
}
private void setRangeHeader(HttpURLConnection connection, DataSpec dataSpec) {
if (dataSpec.position == 0 && dataSpec.length == C.LENGTH_UNBOUNDED) {
// Not required.
return;
}
String rangeRequest = "bytes=" + dataSpec.position + "-";
if (dataSpec.length != C.LENGTH_UNBOUNDED) {
rangeRequest += (dataSpec.position + dataSpec.length - 1);
if (!(position == 0 && length == C.LENGTH_UNBOUNDED)) {
String rangeRequest = "bytes=" + position + "-";
if (length != C.LENGTH_UNBOUNDED) {
rangeRequest += (position + length - 1);
}
connection.setRequestProperty("Range", rangeRequest);
}
connection.setRequestProperty("User-Agent", userAgent);
if (!allowGzip) {
connection.setRequestProperty("Accept-Encoding", "identity");
}
return connection;
}
private long getContentLength(HttpURLConnection connection) {
/**
* Handles a redirect.
*
* @param originalUrl The original URL.
* @param location The Location header in the response.
* @return The next URL.
* @throws IOException If redirection isn't possible.
*/
private static URL handleRedirect(URL originalUrl, String location) throws IOException {
if (location == null) {
throw new ProtocolException("Null location redirect");
}
// Form the new url.
URL url = new URL(originalUrl, location);
// Check that the protocol of the new url is supported.
String protocol = url.getProtocol();
if (!"https".equals(protocol) && !"http".equals(protocol)) {
throw new ProtocolException("Unsupported protocol redirect: " + protocol);
}
// Currently this method is only called if allowCrossProtocolRedirects is true, and so the code
// below isn't required. If we ever decide to handle redirects ourselves when cross-protocol
// redirects are disabled, we'll need to uncomment this block of code.
// if (!allowCrossProtocolRedirects && !protocol.equals(originalUrl.getProtocol())) {
// throw new ProtocolException("Disallowed cross-protocol redirect ("
// + originalUrl.getProtocol() + " to " + protocol + ")");
// }
return url;
}
/**
* Attempts to extract the length of the content from the response headers of an open connection.
*
* @param connection The open connection.
* @return The extracted length, or {@link C#LENGTH_UNBOUNDED}.
*/
private static long getContentLength(HttpURLConnection connection) {
long contentLength = C.LENGTH_UNBOUNDED;
String contentLengthHeader = connection.getHeaderField("Content-Length");
if (!TextUtils.isEmpty(contentLengthHeader)) {
@ -429,6 +543,9 @@ public class DefaultHttpDataSource implements HttpDataSource {
return read;
}
/**
* Closes the current connection, if there is one.
*/
private void closeConnection() {
if (connection != null) {
connection.disconnect();

View File

@ -36,15 +36,36 @@ public final class DefaultUriDataSource implements UriDataSource {
private UriDataSource dataSource;
/**
* Constructs a new data source that delegates to a {@link FileDataSource} for file URIs and an
* Constructs a new data source that delegates to a {@link FileDataSource} for file URIs and a
* {@link DefaultHttpDataSource} for other URIs.
* <p>
* The constructed instance will not follow cross-protocol redirects (i.e. redirects from HTTP to
* HTTPS or vice versa) when fetching remote data. Cross-protocol redirects can be enabled by
* using the {@link #DefaultUriDataSource(String, TransferListener, boolean)} constructor and
* passing {@code true} as the final argument.
*
* @param userAgent The User-Agent string that should be used when requesting remote data.
* @param transferListener An optional listener.
*/
public DefaultUriDataSource(String userAgent, TransferListener transferListener) {
this(userAgent, transferListener, false);
}
/**
* Constructs a new data source that delegates to a {@link FileDataSource} for file URIs and a
* {@link DefaultHttpDataSource} for other URIs.
*
* @param userAgent The User-Agent string that should be used when requesting remote data.
* @param transferListener An optional listener.
* @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP
* to HTTPS and vice versa) are enabled when fetching remote data..
*/
public DefaultUriDataSource(String userAgent, TransferListener transferListener,
boolean allowCrossProtocolRedirects) {
this(new FileDataSource(transferListener),
new DefaultHttpDataSource(userAgent, null, transferListener));
new DefaultHttpDataSource(userAgent, null, transferListener,
DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, allowCrossProtocolRedirects));
}
/**

View File

@ -15,6 +15,11 @@
*/
package com.google.android.exoplayer.util;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.audio.AudioCapabilities;
import android.media.AudioFormat;
/**
* Defines common MIME types and helper methods.
*/
@ -119,4 +124,37 @@ public class MimeTypes {
return mimeType.equals(APPLICATION_TTML);
}
/**
* Returns the output audio encoding that will result from processing input in {@code mimeType}.
* For non-passthrough audio formats, this is always {@link AudioFormat#ENCODING_PCM_16BIT}. For
* passthrough formats it will be one of {@link AudioFormat}'s other {@code ENCODING_*} constants.
* For non-audio formats, {@link AudioFormat#ENCODING_INVALID} will be returned.
*
* @param mimeType The MIME type of media that will be decoded (or passed through).
* @return The corresponding {@link AudioFormat} encoding.
*/
public static int getEncodingForMimeType(String mimeType) {
if (AUDIO_AC3.equals(mimeType)) {
return C.ENCODING_AC3;
}
if (AUDIO_EC3.equals(mimeType)) {
return C.ENCODING_E_AC3;
}
// All other audio formats will be decoded to 16-bit PCM.
return isAudio(mimeType) ? AudioFormat.ENCODING_PCM_16BIT : AudioFormat.ENCODING_INVALID;
}
/**
* Returns whether the specified {@code mimeType} represents audio that can be played via
* passthrough if the device supports it.
*
* @param mimeType The MIME type of input media.
* @return Whether the audio can be played via passthrough. If this method returns {@code true},
* it is still necessary to check the {@link AudioCapabilities} for device support.
*/
public static boolean isPassthroughAudio(String mimeType) {
return AUDIO_AC3.equals(mimeType) || AUDIO_EC3.equals(mimeType);
}
}

View File

@ -8,5 +8,5 @@
# project structure.
# Project target.
target=android-21
target=android-22
android.library=true

View File

@ -17,7 +17,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer.tests">
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="21"/>
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="22"/>
<application>
<uses-library android:name="android.test.runner"/>

View File

@ -579,7 +579,7 @@ public class Mp4ExtractorTest extends TestCase {
try {
switch (message.what) {
case MSG_PREPARE:
if (!source.prepare()) {
if (!source.prepare(0)) {
sendEmptyMessage(MSG_PREPARE);
} else {
// Select the video track and get its metadata.

View File

@ -11,4 +11,4 @@
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-21
target=android-22