mirror of
https://github.com/androidx/media.git
synced 2025-05-08 16:10:38 +08:00
commit
dd41d14ac3
@ -20,7 +20,7 @@ buildscript {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
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'
|
classpath 'com.novoda:bintray-release:0.2.7'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,12 +14,12 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 21
|
compileSdkVersion 22
|
||||||
buildToolsVersion "21.1.2"
|
buildToolsVersion "22.0.1"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 21
|
targetSdkVersion 22
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<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
|
<application
|
||||||
android:label="@string/application_name"
|
android:label="@string/application_name"
|
||||||
|
@ -25,7 +25,6 @@ import java.net.CookieHandler;
|
|||||||
import java.net.CookieManager;
|
import java.net.CookieManager;
|
||||||
import java.net.CookiePolicy;
|
import java.net.CookiePolicy;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.MalformedURLException;
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@ -52,7 +51,7 @@ public class DemoUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] executePost(String url, byte[] data, Map<String, String> requestProperties)
|
public static byte[] executePost(String url, byte[] data, Map<String, String> requestProperties)
|
||||||
throws MalformedURLException, IOException {
|
throws IOException {
|
||||||
HttpURLConnection urlConnection = null;
|
HttpURLConnection urlConnection = null;
|
||||||
try {
|
try {
|
||||||
urlConnection = (HttpURLConnection) new URL(url).openConnection();
|
urlConnection = (HttpURLConnection) new URL(url).openConnection();
|
||||||
|
@ -229,7 +229,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
|
|||||||
return new DashRendererBuilder(this, userAgent, contentUri.toString(),
|
return new DashRendererBuilder(this, userAgent, contentUri.toString(),
|
||||||
new WidevineTestMediaDrmCallback(contentId), debugTextView, audioCapabilities);
|
new WidevineTestMediaDrmCallback(contentId), debugTextView, audioCapabilities);
|
||||||
case DemoUtil.TYPE_HLS:
|
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_M4A: // There are no file format differences between M4A and MP4.
|
||||||
case DemoUtil.TYPE_MP4:
|
case DemoUtil.TYPE_MP4:
|
||||||
return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView,
|
return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView,
|
||||||
@ -239,7 +240,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
|
|||||||
new Mp3Extractor());
|
new Mp3Extractor());
|
||||||
case DemoUtil.TYPE_TS:
|
case DemoUtil.TYPE_TS:
|
||||||
return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView,
|
return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView,
|
||||||
new TsExtractor());
|
new TsExtractor(0, audioCapabilities));
|
||||||
case DemoUtil.TYPE_AAC:
|
case DemoUtil.TYPE_AAC:
|
||||||
return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView,
|
return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView,
|
||||||
new AdtsExtractor());
|
new AdtsExtractor());
|
||||||
|
@ -130,8 +130,6 @@ import java.util.Locale;
|
|||||||
public static final Sample[] MISC = new Sample[] {
|
public static final Sample[] MISC = new Sample[] {
|
||||||
new Sample("Dizzy", "http://html5demos.com/assets/dizzy.mp4",
|
new Sample("Dizzy", "http://html5demos.com/assets/dizzy.mp4",
|
||||||
DemoUtil.TYPE_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/"
|
new Sample("Apple AAC 10s", "https://devimages.apple.com.edgekey.net/"
|
||||||
+ "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac",
|
+ "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac",
|
||||||
DemoUtil.TYPE_AAC),
|
DemoUtil.TYPE_AAC),
|
||||||
|
@ -22,8 +22,6 @@ import android.media.MediaDrm.KeyRequest;
|
|||||||
import android.media.MediaDrm.ProvisionRequest;
|
import android.media.MediaDrm.ProvisionRequest;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import org.apache.http.client.ClientProtocolException;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@ -43,8 +41,7 @@ public class WidevineTestMediaDrmCallback implements MediaDrmCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request)
|
public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException {
|
||||||
throws ClientProtocolException, IOException {
|
|
||||||
String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData());
|
String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData());
|
||||||
return DemoUtil.executePost(url, null, null);
|
return DemoUtil.executePost(url, null, null);
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,6 @@ import com.google.android.exoplayer.util.Util;
|
|||||||
|
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.media.AudioFormat;
|
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
import android.media.UnsupportedSchemeException;
|
import android.media.UnsupportedSchemeException;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
@ -249,7 +248,6 @@ public class DashRendererBuilder implements RendererBuilder,
|
|||||||
// Build the audio chunk sources.
|
// Build the audio chunk sources.
|
||||||
List<ChunkSource> audioChunkSourceList = new ArrayList<ChunkSource>();
|
List<ChunkSource> audioChunkSourceList = new ArrayList<ChunkSource>();
|
||||||
List<String> audioTrackNameList = new ArrayList<String>();
|
List<String> audioTrackNameList = new ArrayList<String>();
|
||||||
int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;
|
|
||||||
if (audioAdaptationSet != null) {
|
if (audioAdaptationSet != null) {
|
||||||
DataSource audioDataSource = new DefaultUriDataSource(userAgent, bandwidthMeter);
|
DataSource audioDataSource = new DefaultUriDataSource(userAgent, bandwidthMeter);
|
||||||
FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator();
|
FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator();
|
||||||
@ -275,7 +273,6 @@ public class DashRendererBuilder implements RendererBuilder,
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
audioEncoding = encoding;
|
|
||||||
for (int j = audioRepresentations.size() - 1; j >= 0; j--) {
|
for (int j = audioRepresentations.size() - 1; j >= 0; j--) {
|
||||||
if (!audioRepresentations.get(j).format.codecs.equals(codec)) {
|
if (!audioRepresentations.get(j).format.codecs.equals(codec)) {
|
||||||
audioTrackNameList.remove(j);
|
audioTrackNameList.remove(j);
|
||||||
@ -303,7 +300,7 @@ public class DashRendererBuilder implements RendererBuilder,
|
|||||||
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
|
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
|
||||||
DemoPlayer.TYPE_AUDIO);
|
DemoPlayer.TYPE_AUDIO);
|
||||||
audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource, drmSessionManager, true,
|
audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource, drmSessionManager, true,
|
||||||
mainHandler, player, audioEncoding);
|
mainHandler, player);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the text chunk sources.
|
// Build the text chunk sources.
|
||||||
|
@ -57,7 +57,7 @@ import android.widget.TextView;
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int doPrepare() throws ExoPlaybackException {
|
protected int doPrepare(long positionUs) throws ExoPlaybackException {
|
||||||
maybeFail();
|
maybeFail();
|
||||||
return STATE_PREPARED;
|
return STATE_PREPARED;
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
|
|||||||
import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException;
|
import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException;
|
||||||
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
||||||
import com.google.android.exoplayer.TrackRenderer;
|
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.chunk.VideoFormatSelectorUtil;
|
||||||
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder;
|
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder;
|
||||||
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilderCallback;
|
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 userAgent;
|
||||||
private final String url;
|
private final String url;
|
||||||
private final TextView debugTextView;
|
private final TextView debugTextView;
|
||||||
|
private final AudioCapabilities audioCapabilities;
|
||||||
|
|
||||||
private DemoPlayer player;
|
private DemoPlayer player;
|
||||||
private RendererBuilderCallback callback;
|
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.context = context;
|
||||||
this.userAgent = userAgent;
|
this.userAgent = userAgent;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.debugTextView = debugTextView;
|
this.debugTextView = debugTextView;
|
||||||
|
this.audioCapabilities = audioCapabilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -101,7 +105,7 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<Hls
|
|||||||
|
|
||||||
DataSource dataSource = new DefaultUriDataSource(userAgent, bandwidthMeter);
|
DataSource dataSource = new DefaultUriDataSource(userAgent, bandwidthMeter);
|
||||||
HlsChunkSource chunkSource = new HlsChunkSource(dataSource, url, manifest, 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,
|
HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, true, 3, REQUESTED_BUFFER_SIZE,
|
||||||
REQUESTED_BUFFER_DURATION_MS, mainHandler, player, DemoPlayer.TYPE_VIDEO);
|
REQUESTED_BUFFER_DURATION_MS, mainHandler, player, DemoPlayer.TYPE_VIDEO);
|
||||||
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
|
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
|
||||||
|
@ -8,6 +8,6 @@
|
|||||||
# project structure.
|
# project structure.
|
||||||
|
|
||||||
# Project target.
|
# Project target.
|
||||||
target=android-21
|
target=android-22
|
||||||
android.library=false
|
android.library=false
|
||||||
android.library.reference.1=../../../library/src/main
|
android.library.reference.1=../../../library/src/main
|
||||||
|
@ -15,8 +15,8 @@ apply plugin: 'com.android.library'
|
|||||||
apply plugin: 'bintray-release'
|
apply plugin: 'bintray-release'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 21
|
compileSdkVersion 22
|
||||||
buildToolsVersion "21.1.2"
|
buildToolsVersion "22.0.1"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// Important: ExoPlayerLib specifies a minSdkVersion of 9 because
|
// Important: ExoPlayerLib specifies a minSdkVersion of 9 because
|
||||||
@ -25,7 +25,7 @@ android {
|
|||||||
// functionality provided by the library requires API level 16 or
|
// functionality provided by the library requires API level 16 or
|
||||||
// greater.
|
// greater.
|
||||||
minSdkVersion 9
|
minSdkVersion 9
|
||||||
targetSdkVersion 21
|
targetSdkVersion 22
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
@ -27,6 +27,6 @@
|
|||||||
the library may be of use on older devices. However, please note that the core video playback
|
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.
|
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>
|
</manifest>
|
||||||
|
@ -18,14 +18,14 @@ package com.google.android.exoplayer;
|
|||||||
/**
|
/**
|
||||||
* A {@link TrackRenderer} that does nothing.
|
* A {@link TrackRenderer} that does nothing.
|
||||||
* <p>
|
* <p>
|
||||||
* This renderer returns {@link TrackRenderer#STATE_IGNORE} from {@link #doPrepare()} in order to
|
* This renderer returns {@link TrackRenderer#STATE_IGNORE} from {@link #doPrepare(long)} in order
|
||||||
* request that it should be ignored. {@link IllegalStateException} is thrown from all methods that
|
* to request that it should be ignored. {@link IllegalStateException} is thrown from all methods
|
||||||
* are documented to indicate that they should not be invoked unless the renderer is prepared.
|
* that are documented to indicate that they should not be invoked unless the renderer is prepared.
|
||||||
*/
|
*/
|
||||||
public class DummyTrackRenderer extends TrackRenderer {
|
public class DummyTrackRenderer extends TrackRenderer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int doPrepare() throws ExoPlaybackException {
|
protected int doPrepare(long positionUs) throws ExoPlaybackException {
|
||||||
return STATE_IGNORE;
|
return STATE_IGNORE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@ public interface ExoPlayer {
|
|||||||
* The default minimum duration of data that must be buffered for playback to start or resume
|
* The default minimum duration of data that must be buffered for playback to start or resume
|
||||||
* following a user action such as a seek.
|
* 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
|
* The default minimum duration of data that must be buffered for playback to resume
|
||||||
|
@ -264,7 +264,7 @@ import java.util.List;
|
|||||||
boolean prepared = true;
|
boolean prepared = true;
|
||||||
for (int i = 0; i < renderers.length; i++) {
|
for (int i = 0; i < renderers.length; i++) {
|
||||||
if (renderers[i].getState() == TrackRenderer.STATE_UNPREPARED) {
|
if (renderers[i].getState() == TrackRenderer.STATE_UNPREPARED) {
|
||||||
int state = renderers[i].prepare();
|
int state = renderers[i].prepare(positionUs);
|
||||||
if (state == TrackRenderer.STATE_UNPREPARED) {
|
if (state == TrackRenderer.STATE_UNPREPARED) {
|
||||||
prepared = false;
|
prepared = false;
|
||||||
}
|
}
|
||||||
|
@ -128,7 +128,7 @@ public final class FrameworkSampleSource implements SampleSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean prepare() throws IOException {
|
public boolean prepare(long positionUs) throws IOException {
|
||||||
if (!prepared) {
|
if (!prepared) {
|
||||||
extractor = new MediaExtractor();
|
extractor = new MediaExtractor();
|
||||||
if (context != null) {
|
if (context != null) {
|
||||||
|
@ -21,9 +21,7 @@ import com.google.android.exoplayer.drm.DrmSessionManager;
|
|||||||
import com.google.android.exoplayer.util.MimeTypes;
|
import com.google.android.exoplayer.util.MimeTypes;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.media.AudioFormat;
|
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
import android.media.MediaFormat;
|
|
||||||
import android.media.audiofx.Virtualizer;
|
import android.media.audiofx.Virtualizer;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
|
||||||
@ -71,7 +69,6 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
|||||||
|
|
||||||
private final EventListener eventListener;
|
private final EventListener eventListener;
|
||||||
private final AudioTrack audioTrack;
|
private final AudioTrack audioTrack;
|
||||||
private final int encoding;
|
|
||||||
|
|
||||||
private int audioSessionId;
|
private int audioSessionId;
|
||||||
private long currentPositionUs;
|
private long currentPositionUs;
|
||||||
@ -124,50 +121,27 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
|||||||
*/
|
*/
|
||||||
public MediaCodecAudioTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
|
public MediaCodecAudioTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
|
||||||
boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener) {
|
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);
|
super(source, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener);
|
||||||
this.eventListener = eventListener;
|
this.eventListener = eventListener;
|
||||||
this.audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
|
this.audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
|
||||||
this.audioTrack = new AudioTrack();
|
this.audioTrack = new AudioTrack();
|
||||||
this.encoding = encoding;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected DecoderInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder)
|
protected DecoderInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder)
|
||||||
throws DecoderQueryException {
|
throws DecoderQueryException {
|
||||||
if (encoding == AudioFormat.ENCODING_AC3 || encoding == AudioFormat.ENCODING_E_AC3) {
|
if (MimeTypes.isPassthroughAudio(mimeType)) {
|
||||||
return new DecoderInfo(RAW_DECODER_NAME, true);
|
return new DecoderInfo(RAW_DECODER_NAME, true);
|
||||||
}
|
}
|
||||||
return super.getDecoderInfo(mimeType, requiresSecureDecoder);
|
return super.getDecoderInfo(mimeType, requiresSecureDecoder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void configureCodec(MediaCodec codec, String codecName, MediaFormat format,
|
protected void configureCodec(MediaCodec codec, String codecName,
|
||||||
android.media.MediaCrypto crypto) {
|
android.media.MediaFormat format, android.media.MediaCrypto crypto) {
|
||||||
if (RAW_DECODER_NAME.equals(codecName)) {
|
if (RAW_DECODER_NAME.equals(codecName)) {
|
||||||
// Override the MIME type used to configure the codec if we are using a passthrough decoder.
|
// 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);
|
format.setString(android.media.MediaFormat.KEY_MIME, MimeTypes.AUDIO_RAW);
|
||||||
codec.configure(format, null, crypto, 0);
|
codec.configure(format, null, crypto, 0);
|
||||||
format.setString(android.media.MediaFormat.KEY_MIME, mimeType);
|
format.setString(android.media.MediaFormat.KEY_MIME, mimeType);
|
||||||
@ -193,8 +167,13 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onOutputFormatChanged(MediaFormat format) {
|
protected void onOutputFormatChanged(MediaFormat inputFormat,
|
||||||
audioTrack.reconfigure(format, encoding, 0);
|
android.media.MediaFormat outputFormat) {
|
||||||
|
if (MimeTypes.isPassthroughAudio(inputFormat.mimeType)) {
|
||||||
|
audioTrack.reconfigure(inputFormat.getFrameworkMediaFormatV16());
|
||||||
|
} else {
|
||||||
|
audioTrack.reconfigure(outputFormat);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -243,9 +243,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int doPrepare() throws ExoPlaybackException {
|
protected int doPrepare(long positionUs) throws ExoPlaybackException {
|
||||||
try {
|
try {
|
||||||
boolean sourcePrepared = source.prepare();
|
boolean sourcePrepared = source.prepare(positionUs);
|
||||||
if (!sourcePrepared) {
|
if (!sourcePrepared) {
|
||||||
return TrackRenderer.STATE_UNPREPARED;
|
return TrackRenderer.STATE_UNPREPARED;
|
||||||
}
|
}
|
||||||
@ -742,9 +742,11 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
|||||||
* <p>
|
* <p>
|
||||||
* The default implementation is a no-op.
|
* 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.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -818,7 +820,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
||||||
onOutputFormatChanged(codec.getOutputFormat());
|
onOutputFormatChanged(format, codec.getOutputFormat());
|
||||||
codecCounters.outputFormatChangedCount++;
|
codecCounters.outputFormatChangedCount++;
|
||||||
return true;
|
return true;
|
||||||
} else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
|
} else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
|
||||||
|
@ -381,15 +381,17 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onOutputFormatChanged(android.media.MediaFormat format) {
|
protected void onOutputFormatChanged(MediaFormat inputFormat,
|
||||||
boolean hasCrop = format.containsKey(KEY_CROP_RIGHT) && format.containsKey(KEY_CROP_LEFT)
|
android.media.MediaFormat outputFormat) {
|
||||||
&& format.containsKey(KEY_CROP_BOTTOM) && format.containsKey(KEY_CROP_TOP);
|
boolean hasCrop = outputFormat.containsKey(KEY_CROP_RIGHT)
|
||||||
|
&& outputFormat.containsKey(KEY_CROP_LEFT) && outputFormat.containsKey(KEY_CROP_BOTTOM)
|
||||||
|
&& outputFormat.containsKey(KEY_CROP_TOP);
|
||||||
currentWidth = hasCrop
|
currentWidth = hasCrop
|
||||||
? format.getInteger(KEY_CROP_RIGHT) - format.getInteger(KEY_CROP_LEFT) + 1
|
? outputFormat.getInteger(KEY_CROP_RIGHT) - outputFormat.getInteger(KEY_CROP_LEFT) + 1
|
||||||
: format.getInteger(android.media.MediaFormat.KEY_WIDTH);
|
: outputFormat.getInteger(android.media.MediaFormat.KEY_WIDTH);
|
||||||
currentHeight = hasCrop
|
currentHeight = hasCrop
|
||||||
? format.getInteger(KEY_CROP_BOTTOM) - format.getInteger(KEY_CROP_TOP) + 1
|
? outputFormat.getInteger(KEY_CROP_BOTTOM) - outputFormat.getInteger(KEY_CROP_TOP) + 1
|
||||||
: format.getInteger(android.media.MediaFormat.KEY_HEIGHT);
|
: outputFormat.getInteger(android.media.MediaFormat.KEY_HEIGHT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -57,10 +57,11 @@ public interface SampleSource {
|
|||||||
* and formats). If insufficient data is available then the call will return {@code false} rather
|
* 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.
|
* 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.
|
* @return True if the source was prepared successfully, false otherwise.
|
||||||
* @throws IOException If an error occurred preparing the source.
|
* @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.
|
* Returns the number of tracks exposed by the source.
|
||||||
|
@ -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
|
* 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.
|
* 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.
|
* @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);
|
Assertions.checkState(state == TrackRenderer.STATE_UNPREPARED);
|
||||||
state = doPrepare();
|
state = doPrepare(positionUs);
|
||||||
Assertions.checkState(state == TrackRenderer.STATE_UNPREPARED ||
|
Assertions.checkState(state == TrackRenderer.STATE_UNPREPARED ||
|
||||||
state == TrackRenderer.STATE_PREPARED ||
|
state == TrackRenderer.STATE_PREPARED ||
|
||||||
state == TrackRenderer.STATE_IGNORE);
|
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
|
* This method should return quickly, and should not block if the renderer is currently unable to
|
||||||
* make any useful progress.
|
* make any useful progress.
|
||||||
*
|
*
|
||||||
|
* @param positionUs The player's current playback position.
|
||||||
* @return The new state of the renderer. One of {@link #STATE_UNPREPARED},
|
* @return The new state of the renderer. One of {@link #STATE_UNPREPARED},
|
||||||
* {@link #STATE_PREPARED} and {@link #STATE_IGNORE}.
|
* {@link #STATE_PREPARED} and {@link #STATE_IGNORE}.
|
||||||
* @throws ExoPlaybackException If an error occurs.
|
* @throws ExoPlaybackException If an error occurs.
|
||||||
*/
|
*/
|
||||||
protected abstract int doPrepare() throws ExoPlaybackException;
|
protected abstract int doPrepare(long positionUs) throws ExoPlaybackException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable the renderer.
|
* Enable the renderer.
|
||||||
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer.audio;
|
|||||||
import com.google.android.exoplayer.C;
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.util.Ac3Util;
|
import com.google.android.exoplayer.util.Ac3Util;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
import com.google.android.exoplayer.util.MimeTypes;
|
||||||
import com.google.android.exoplayer.util.Util;
|
import com.google.android.exoplayer.util.Util;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
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
|
* Reconfigures the audio track to play back media in {@code format}, inferring a buffer size from
|
||||||
* be {@link AudioFormat#ENCODING_PCM_16BIT}.
|
* the format.
|
||||||
*/
|
*/
|
||||||
public void reconfigure(MediaFormat 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
|
* Reconfigures the audio track to play back media in {@code format}.
|
||||||
* {@link #handleBuffer} must use the specified {@code encoding}, which should be a constant from
|
|
||||||
* {@link AudioFormat}.
|
|
||||||
*
|
*
|
||||||
* @param format Specifies the channel count and sample rate to play back.
|
* @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
|
* @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to use a
|
||||||
* size inferred from the format.
|
* 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 channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
|
||||||
int channelConfig;
|
int channelConfig;
|
||||||
switch (channelCount) {
|
switch (channelCount) {
|
||||||
@ -353,8 +351,10 @@ public final class AudioTrack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
|
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
|
||||||
|
String mimeType = format.getString(MediaFormat.KEY_MIME);
|
||||||
|
|
||||||
// TODO: Does channelConfig determine channelCount?
|
// TODO: Does channelConfig determine channelCount?
|
||||||
|
int encoding = MimeTypes.getEncodingForMimeType(mimeType);
|
||||||
boolean isAc3 = encoding == C.ENCODING_AC3 || encoding == C.ENCODING_E_AC3;
|
boolean isAc3 = encoding == C.ENCODING_AC3 || encoding == C.ENCODING_E_AC3;
|
||||||
if (isInitialized() && this.sampleRate == sampleRate && this.channelConfig == channelConfig
|
if (isInitialized() && this.sampleRate == sampleRate && this.channelConfig == channelConfig
|
||||||
&& !this.isAc3 && !isAc3) {
|
&& !this.isAc3 && !isAc3) {
|
||||||
@ -423,10 +423,21 @@ public final class AudioTrack {
|
|||||||
return RESULT_BUFFER_CONSUMED;
|
return RESULT_BUFFER_CONSUMED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// As a workaround for an issue where an an AC-3 audio track continues to play data written
|
// Workarounds for issues with AC-3 passthrough AudioTracks on API versions 21/22:
|
||||||
// while it is paused, stop writing so its buffer empties. See [Internal: b/18899620].
|
if (Util.SDK_INT <= 22 && isAc3) {
|
||||||
if (isAc3 && audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED) {
|
// An AC-3 audio track continues to play data written while it is paused. Stop writing so its
|
||||||
return 0;
|
// 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;
|
int result = 0;
|
||||||
@ -637,7 +648,8 @@ public final class AudioTrack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) {
|
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) {
|
if (audioTimestampSet) {
|
||||||
// Perform sanity checks on the timestamp.
|
// Perform sanity checks on the timestamp.
|
||||||
long audioTimestampUs = audioTrackUtil.getTimestampNanoTime() / 1000;
|
long audioTimestampUs = audioTrackUtil.getTimestampNanoTime() / 1000;
|
||||||
@ -739,7 +751,7 @@ public final class AudioTrack {
|
|||||||
private static class AudioTrackUtil {
|
private static class AudioTrackUtil {
|
||||||
|
|
||||||
protected android.media.AudioTrack audioTrack;
|
protected android.media.AudioTrack audioTrack;
|
||||||
private boolean enablePassthroughWorkaround;
|
private boolean isPassthrough;
|
||||||
private int sampleRate;
|
private int sampleRate;
|
||||||
private long lastRawPlaybackHeadPosition;
|
private long lastRawPlaybackHeadPosition;
|
||||||
private long rawPlaybackHeadWrapCount;
|
private long rawPlaybackHeadWrapCount;
|
||||||
@ -749,14 +761,11 @@ public final class AudioTrack {
|
|||||||
* Reconfigures the audio track utility helper to use the specified {@code audioTrack}.
|
* Reconfigures the audio track utility helper to use the specified {@code audioTrack}.
|
||||||
*
|
*
|
||||||
* @param audioTrack The audio track to wrap.
|
* @param audioTrack The audio track to wrap.
|
||||||
* @param enablePassthroughWorkaround Whether to work around an issue where the playback head
|
* @param isPassthrough Whether the audio track is used for passthrough (e.g. AC-3) playback.
|
||||||
* position jumps back to zero on a paused passthrough/direct audio track. See
|
|
||||||
* [Internal: b/19187573].
|
|
||||||
*/
|
*/
|
||||||
public void reconfigure(android.media.AudioTrack audioTrack,
|
public void reconfigure(android.media.AudioTrack audioTrack, boolean isPassthrough) {
|
||||||
boolean enablePassthroughWorkaround) {
|
|
||||||
this.audioTrack = audioTrack;
|
this.audioTrack = audioTrack;
|
||||||
this.enablePassthroughWorkaround = enablePassthroughWorkaround;
|
this.isPassthrough = isPassthrough;
|
||||||
lastRawPlaybackHeadPosition = 0;
|
lastRawPlaybackHeadPosition = 0;
|
||||||
rawPlaybackHeadWrapCount = 0;
|
rawPlaybackHeadWrapCount = 0;
|
||||||
passthroughWorkaroundPauseOffset = 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
|
* 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
|
* around an issue on platform API versions 21/22 where AC-3 audio tracks can't be paused, so we
|
||||||
* paused. In this case, they should still behave as if they have pending data, otherwise
|
* empty their buffers when paused. In this case, they should still behave as if they have
|
||||||
* writing will never resume.
|
* pending data, otherwise writing will never resume.
|
||||||
*
|
*
|
||||||
* @see #handleBuffer
|
* @see #handleBuffer
|
||||||
*/
|
*/
|
||||||
public boolean overrideHasPendingData() {
|
public boolean overrideHasPendingData() {
|
||||||
return enablePassthroughWorkaround
|
return Util.SDK_INT <= 22 && isPassthrough
|
||||||
&& audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED
|
&& audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED
|
||||||
&& audioTrack.getPlaybackHeadPosition() == 0;
|
&& audioTrack.getPlaybackHeadPosition() == 0;
|
||||||
}
|
}
|
||||||
@ -790,8 +799,16 @@ public final class AudioTrack {
|
|||||||
*/
|
*/
|
||||||
public long getPlaybackHeadPosition() {
|
public long getPlaybackHeadPosition() {
|
||||||
long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition();
|
long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition();
|
||||||
if (enablePassthroughWorkaround) {
|
if (Util.SDK_INT <= 22 && isPassthrough) {
|
||||||
if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED
|
// 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) {
|
&& rawPlaybackHeadPosition == 0) {
|
||||||
passthroughWorkaroundPauseOffset = lastRawPlaybackHeadPosition;
|
passthroughWorkaroundPauseOffset = lastRawPlaybackHeadPosition;
|
||||||
}
|
}
|
||||||
@ -868,9 +885,8 @@ public final class AudioTrack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reconfigure(android.media.AudioTrack audioTrack,
|
public void reconfigure(android.media.AudioTrack audioTrack, boolean isPassthrough) {
|
||||||
boolean enablePassthroughWorkaround) {
|
super.reconfigure(audioTrack, isPassthrough);
|
||||||
super.reconfigure(audioTrack, enablePassthroughWorkaround);
|
|
||||||
rawTimestampFramePositionWrapCount = 0;
|
rawTimestampFramePositionWrapCount = 0;
|
||||||
lastRawTimestampFramePosition = 0;
|
lastRawTimestampFramePosition = 0;
|
||||||
lastTimestampFramePosition = 0;
|
lastTimestampFramePosition = 0;
|
||||||
|
@ -87,7 +87,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
|||||||
private long currentLoadStartTimeMs;
|
private long currentLoadStartTimeMs;
|
||||||
|
|
||||||
private MediaFormat downstreamMediaFormat;
|
private MediaFormat downstreamMediaFormat;
|
||||||
private volatile Format downstreamFormat;
|
private Format downstreamFormat;
|
||||||
|
|
||||||
public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
|
public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
|
||||||
int bufferSizeContribution, boolean frameAccurateSeeking) {
|
int bufferSizeContribution, boolean frameAccurateSeeking) {
|
||||||
@ -120,18 +120,8 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
|||||||
pendingResetPositionUs = NO_RESET_PENDING;
|
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
|
@Override
|
||||||
public boolean prepare() {
|
public boolean prepare(long positionUs) {
|
||||||
Assertions.checkState(state == STATE_UNPREPARED);
|
Assertions.checkState(state == STATE_UNPREPARED);
|
||||||
loader = new Loader("Loader:" + chunkSource.getTrackInfo().mimeType);
|
loader = new Loader("Loader:" + chunkSource.getTrackInfo().mimeType);
|
||||||
state = STATE_PREPARED;
|
state = STATE_PREPARED;
|
||||||
|
@ -84,7 +84,7 @@ public interface FormatEvaluator {
|
|||||||
/**
|
/**
|
||||||
* Always selects the first format.
|
* Always selects the first format.
|
||||||
*/
|
*/
|
||||||
public static class FixedEvaluator implements FormatEvaluator {
|
public static final class FixedEvaluator implements FormatEvaluator {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void enable() {
|
public void enable() {
|
||||||
@ -107,7 +107,7 @@ public interface FormatEvaluator {
|
|||||||
/**
|
/**
|
||||||
* Selects randomly between the available formats.
|
* Selects randomly between the available formats.
|
||||||
*/
|
*/
|
||||||
public static class RandomEvaluator implements FormatEvaluator {
|
public static final class RandomEvaluator implements FormatEvaluator {
|
||||||
|
|
||||||
private final Random random;
|
private final Random random;
|
||||||
|
|
||||||
@ -145,7 +145,7 @@ public interface FormatEvaluator {
|
|||||||
* reference implementation only. It is recommended that application developers implement their
|
* reference implementation only. It is recommended that application developers implement their
|
||||||
* own adaptive evaluator to more precisely suit their use case.
|
* 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;
|
public static final int DEFAULT_MAX_INITIAL_BITRATE = 800000;
|
||||||
|
|
||||||
@ -259,8 +259,9 @@ public interface FormatEvaluator {
|
|||||||
/**
|
/**
|
||||||
* Compute the ideal format ignoring buffer health.
|
* Compute the ideal format ignoring buffer health.
|
||||||
*/
|
*/
|
||||||
protected Format determineIdealFormat(Format[] formats, long bitrateEstimate) {
|
private Format determineIdealFormat(Format[] formats, long bitrateEstimate) {
|
||||||
long effectiveBitrate = computeEffectiveBitrateEstimate(bitrateEstimate);
|
long effectiveBitrate = bitrateEstimate == BandwidthMeter.NO_ESTIMATE
|
||||||
|
? maxInitialBitrate : (long) (bitrateEstimate * bandwidthFraction);
|
||||||
for (int i = 0; i < formats.length; i++) {
|
for (int i = 0; i < formats.length; i++) {
|
||||||
Format format = formats[i];
|
Format format = formats[i];
|
||||||
if (format.bitrate <= effectiveBitrate) {
|
if (format.bitrate <= effectiveBitrate) {
|
||||||
@ -271,14 +272,6 @@ public interface FormatEvaluator {
|
|||||||
return formats[formats.length - 1];
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -419,7 +419,9 @@ public class DashChunkSource implements ChunkSource {
|
|||||||
(ChunkIndex) initializationChunk.getSeekMap(),
|
(ChunkIndex) initializationChunk.getSeekMap(),
|
||||||
initializationChunk.dataSpec.uri.toString());
|
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();
|
drmInitData = initializationChunk.getDrmInitData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.upstream.UriLoadable;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
import com.google.android.exoplayer.util.MimeTypes;
|
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.UriUtil;
|
||||||
import com.google.android.exoplayer.util.Util;
|
import com.google.android.exoplayer.util.Util;
|
||||||
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.util.Base64;
|
||||||
|
|
||||||
import org.xml.sax.helpers.DefaultHandler;
|
import org.xml.sax.helpers.DefaultHandler;
|
||||||
import org.xmlpull.v1.XmlPullParser;
|
import org.xmlpull.v1.XmlPullParser;
|
||||||
@ -41,6 +43,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@ -270,11 +273,27 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
|
|||||||
protected ContentProtection parseContentProtection(XmlPullParser xpp)
|
protected ContentProtection parseContentProtection(XmlPullParser xpp)
|
||||||
throws XmlPullParserException, IOException {
|
throws XmlPullParserException, IOException {
|
||||||
String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri");
|
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) {
|
protected ContentProtection buildContentProtection(String schemeIdUri, UUID uuid, byte[] data) {
|
||||||
return new ContentProtection(schemeIdUri, null, null);
|
return new ContentProtection(schemeIdUri, uuid, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -72,6 +72,7 @@ public class ExtractorSampleSource implements SampleSource, ExtractorOutput, Loa
|
|||||||
private boolean prepared;
|
private boolean prepared;
|
||||||
private int enabledTrackCount;
|
private int enabledTrackCount;
|
||||||
private TrackInfo[] trackInfos;
|
private TrackInfo[] trackInfos;
|
||||||
|
private long maxTrackDurationUs;
|
||||||
private boolean[] pendingMediaFormat;
|
private boolean[] pendingMediaFormat;
|
||||||
private boolean[] pendingDiscontinuities;
|
private boolean[] pendingDiscontinuities;
|
||||||
private boolean[] trackEnabledStates;
|
private boolean[] trackEnabledStates;
|
||||||
@ -137,7 +138,7 @@ public class ExtractorSampleSource implements SampleSource, ExtractorOutput, Loa
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean prepare() throws IOException {
|
public boolean prepare(long positionUs) throws IOException {
|
||||||
if (prepared) {
|
if (prepared) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -156,9 +157,13 @@ public class ExtractorSampleSource implements SampleSource, ExtractorOutput, Loa
|
|||||||
pendingDiscontinuities = new boolean[trackCount];
|
pendingDiscontinuities = new boolean[trackCount];
|
||||||
pendingMediaFormat = new boolean[trackCount];
|
pendingMediaFormat = new boolean[trackCount];
|
||||||
trackInfos = new TrackInfo[trackCount];
|
trackInfos = new TrackInfo[trackCount];
|
||||||
|
maxTrackDurationUs = C.UNKNOWN_TIME_US;
|
||||||
for (int i = 0; i < trackCount; i++) {
|
for (int i = 0; i < trackCount; i++) {
|
||||||
MediaFormat format = sampleQueues.valueAt(i).getFormat();
|
MediaFormat format = sampleQueues.valueAt(i).getFormat();
|
||||||
trackInfos[i] = new TrackInfo(format.mimeType, format.durationUs);
|
trackInfos[i] = new TrackInfo(format.mimeType, format.durationUs);
|
||||||
|
if (format.durationUs != C.UNKNOWN_TIME_US && format.durationUs > maxTrackDurationUs) {
|
||||||
|
maxTrackDurationUs = format.durationUs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
prepared = true;
|
prepared = true;
|
||||||
return true;
|
return true;
|
||||||
@ -448,6 +453,11 @@ public class ExtractorSampleSource implements SampleSource, ExtractorOutput, Loa
|
|||||||
loadable = createLoadableFromStart();
|
loadable = createLoadableFromStart();
|
||||||
} else {
|
} else {
|
||||||
Assertions.checkState(isPendingReset());
|
Assertions.checkState(isPendingReset());
|
||||||
|
if (maxTrackDurationUs != C.UNKNOWN_TIME_US && pendingResetPositionUs >= maxTrackDurationUs) {
|
||||||
|
loadingFinished = true;
|
||||||
|
pendingResetPositionUs = NO_RESET_PENDING;
|
||||||
|
return;
|
||||||
|
}
|
||||||
loadable = createLoadableFromPositionUs(pendingResetPositionUs);
|
loadable = createLoadableFromPositionUs(pendingResetPositionUs);
|
||||||
pendingResetPositionUs = NO_RESET_PENDING;
|
pendingResetPositionUs = NO_RESET_PENDING;
|
||||||
}
|
}
|
||||||
|
@ -137,7 +137,7 @@ import com.google.android.exoplayer.util.Util;
|
|||||||
}
|
}
|
||||||
|
|
||||||
long position = (long) ((1f / 256) * fx * sizeBytes) + firstFramePosition;
|
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
|
@Override
|
||||||
|
@ -534,8 +534,18 @@ import java.util.List;
|
|||||||
childPosition += childAtomSize;
|
childPosition += childAtomSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
out.mediaFormat = MediaFormat.createAudioFormat(
|
// Set the MIME type for ac-3/ec-3 atoms even if the dac3/dec3 child atom is missing.
|
||||||
MimeTypes.AUDIO_AAC, sampleSize, durationUs, channelCount, sampleRate,
|
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));
|
initializationData == null ? null : Collections.singletonList(initializationData));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
package com.google.android.exoplayer.extractor.ts;
|
package com.google.android.exoplayer.extractor.ts;
|
||||||
|
|
||||||
import com.google.android.exoplayer.C;
|
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.Extractor;
|
||||||
import com.google.android.exoplayer.extractor.ExtractorInput;
|
import com.google.android.exoplayer.extractor.ExtractorInput;
|
||||||
import com.google.android.exoplayer.extractor.ExtractorOutput;
|
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_AAC = 0x0F;
|
||||||
private static final int TS_STREAM_TYPE_ATSC_AC3 = 0x81;
|
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_H264 = 0x1B;
|
||||||
private static final int TS_STREAM_TYPE_ID3 = 0x15;
|
private static final int TS_STREAM_TYPE_ID3 = 0x15;
|
||||||
private static final int TS_STREAM_TYPE_EIA608 = 0x100; // 0xFF + 1
|
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 ParsableByteArray tsPacketBuffer;
|
||||||
private final SparseBooleanArray streamTypes;
|
private final SparseBooleanArray streamTypes;
|
||||||
|
private final SparseBooleanArray allowedPassthroughStreamTypes;
|
||||||
private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
|
private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
|
||||||
private final long firstSampleTimestampUs;
|
private final long firstSampleTimestampUs;
|
||||||
private final ParsableBitArray tsScratch;
|
private final ParsableBitArray tsScratch;
|
||||||
@ -62,14 +64,15 @@ public final class TsExtractor implements Extractor, SeekMap {
|
|||||||
private long lastPts;
|
private long lastPts;
|
||||||
|
|
||||||
public TsExtractor() {
|
public TsExtractor() {
|
||||||
this(0);
|
this(0, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public TsExtractor(long firstSampleTimestampUs) {
|
public TsExtractor(long firstSampleTimestampUs, AudioCapabilities audioCapabilities) {
|
||||||
this.firstSampleTimestampUs = firstSampleTimestampUs;
|
this.firstSampleTimestampUs = firstSampleTimestampUs;
|
||||||
tsScratch = new ParsableBitArray(new byte[3]);
|
tsScratch = new ParsableBitArray(new byte[3]);
|
||||||
tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE);
|
tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE);
|
||||||
streamTypes = new SparseBooleanArray();
|
streamTypes = new SparseBooleanArray();
|
||||||
|
allowedPassthroughStreamTypes = getPassthroughStreamTypes(audioCapabilities);
|
||||||
tsPayloadReaders = new SparseArray<TsPayloadReader>();
|
tsPayloadReaders = new SparseArray<TsPayloadReader>();
|
||||||
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
|
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
|
||||||
lastPts = Long.MIN_VALUE;
|
lastPts = Long.MIN_VALUE;
|
||||||
@ -174,6 +177,24 @@ public final class TsExtractor implements Extractor, SeekMap {
|
|||||||
return timeUs + timestampOffsetUs;
|
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.
|
* Parses TS packet payload data.
|
||||||
*/
|
*/
|
||||||
@ -308,13 +329,17 @@ public final class TsExtractor implements Extractor, SeekMap {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Detect and read DVB AC-3 streams with Ac3Reader.
|
||||||
ElementaryStreamReader pesPayloadReader = null;
|
ElementaryStreamReader pesPayloadReader = null;
|
||||||
switch (streamType) {
|
switch (streamType) {
|
||||||
case TS_STREAM_TYPE_AAC:
|
case TS_STREAM_TYPE_AAC:
|
||||||
pesPayloadReader = new AdtsReader(output.track(TS_STREAM_TYPE_AAC));
|
pesPayloadReader = new AdtsReader(output.track(TS_STREAM_TYPE_AAC));
|
||||||
break;
|
break;
|
||||||
|
case TS_STREAM_TYPE_ATSC_E_AC3:
|
||||||
case TS_STREAM_TYPE_ATSC_AC3:
|
case TS_STREAM_TYPE_ATSC_AC3:
|
||||||
case TS_STREAM_TYPE_DVB_AC3:
|
if (!allowedPassthroughStreamTypes.get(streamType)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
pesPayloadReader = new Ac3Reader(output.track(streamType));
|
pesPayloadReader = new Ac3Reader(output.track(streamType));
|
||||||
break;
|
break;
|
||||||
case TS_STREAM_TYPE_H264:
|
case TS_STREAM_TYPE_H264:
|
||||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer.hls;
|
|||||||
|
|
||||||
import com.google.android.exoplayer.C;
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.MediaFormat;
|
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.BaseChunkSampleSourceEventListener;
|
||||||
import com.google.android.exoplayer.chunk.Chunk;
|
import com.google.android.exoplayer.chunk.Chunk;
|
||||||
import com.google.android.exoplayer.chunk.DataChunk;
|
import com.google.android.exoplayer.chunk.DataChunk;
|
||||||
@ -125,6 +126,7 @@ public class HlsChunkSource {
|
|||||||
private final int maxHeight;
|
private final int maxHeight;
|
||||||
private final long minBufferDurationToSwitchUpUs;
|
private final long minBufferDurationToSwitchUpUs;
|
||||||
private final long maxBufferDurationToSwitchDownUs;
|
private final long maxBufferDurationToSwitchDownUs;
|
||||||
|
private final AudioCapabilities audioCapabilities;
|
||||||
|
|
||||||
/* package */ byte[] scratchSpace;
|
/* package */ byte[] scratchSpace;
|
||||||
/* package */ final HlsMediaPlaylist[] mediaPlaylists;
|
/* package */ final HlsMediaPlaylist[] mediaPlaylists;
|
||||||
@ -140,9 +142,11 @@ public class HlsChunkSource {
|
|||||||
private byte[] encryptionIv;
|
private byte[] encryptionIv;
|
||||||
|
|
||||||
public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist,
|
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,
|
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.
|
* for a switch to a higher quality variant to be considered.
|
||||||
* @param maxBufferDurationToSwitchDownMs The maximum duration of media that needs to be buffered
|
* @param maxBufferDurationToSwitchDownMs The maximum duration of media that needs to be buffered
|
||||||
* for a switch to a lower quality variant to be considered.
|
* 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,
|
public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist,
|
||||||
BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode,
|
BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode,
|
||||||
long minBufferDurationToSwitchUpMs, long maxBufferDurationToSwitchDownMs) {
|
long minBufferDurationToSwitchUpMs, long maxBufferDurationToSwitchDownMs,
|
||||||
|
AudioCapabilities audioCapabilities) {
|
||||||
this.dataSource = dataSource;
|
this.dataSource = dataSource;
|
||||||
this.bandwidthMeter = bandwidthMeter;
|
this.bandwidthMeter = bandwidthMeter;
|
||||||
this.adaptiveMode = adaptiveMode;
|
this.adaptiveMode = adaptiveMode;
|
||||||
|
this.audioCapabilities = audioCapabilities;
|
||||||
minBufferDurationToSwitchUpUs = minBufferDurationToSwitchUpMs * 1000;
|
minBufferDurationToSwitchUpUs = minBufferDurationToSwitchUpMs * 1000;
|
||||||
maxBufferDurationToSwitchDownUs = maxBufferDurationToSwitchDownMs * 1000;
|
maxBufferDurationToSwitchDownUs = maxBufferDurationToSwitchDownMs * 1000;
|
||||||
baseUri = playlist.baseUri;
|
baseUri = playlist.baseUri;
|
||||||
@ -235,16 +243,14 @@ public class HlsChunkSource {
|
|||||||
public Chunk getChunkOperation(TsChunk previousTsChunk, long seekPositionUs,
|
public Chunk getChunkOperation(TsChunk previousTsChunk, long seekPositionUs,
|
||||||
long playbackPositionUs) {
|
long playbackPositionUs) {
|
||||||
int nextFormatIndex;
|
int nextFormatIndex;
|
||||||
boolean switchingVariant;
|
|
||||||
boolean switchingVariantSpliced;
|
boolean switchingVariantSpliced;
|
||||||
if (adaptiveMode == ADAPTIVE_MODE_NONE) {
|
if (adaptiveMode == ADAPTIVE_MODE_NONE) {
|
||||||
nextFormatIndex = formatIndex;
|
nextFormatIndex = formatIndex;
|
||||||
switchingVariant = false;
|
|
||||||
switchingVariantSpliced = false;
|
switchingVariantSpliced = false;
|
||||||
} else {
|
} else {
|
||||||
nextFormatIndex = getNextFormatIndex(previousTsChunk, playbackPositionUs);
|
nextFormatIndex = getNextFormatIndex(previousTsChunk, playbackPositionUs);
|
||||||
switchingVariant = nextFormatIndex != formatIndex;
|
switchingVariantSpliced = nextFormatIndex != formatIndex
|
||||||
switchingVariantSpliced = switchingVariant && adaptiveMode == ADAPTIVE_MODE_SPLICE;
|
&& adaptiveMode == ADAPTIVE_MODE_SPLICE;
|
||||||
}
|
}
|
||||||
|
|
||||||
int variantIndex = getVariantIndex(enabledFormats[nextFormatIndex]);
|
int variantIndex = getVariantIndex(enabledFormats[nextFormatIndex]);
|
||||||
@ -331,10 +337,11 @@ public class HlsChunkSource {
|
|||||||
|
|
||||||
// Configure the extractor that will read the chunk.
|
// Configure the extractor that will read the chunk.
|
||||||
HlsExtractorWrapper extractorWrapper;
|
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)
|
Extractor extractor = chunkUri.getLastPathSegment().endsWith(AAC_FILE_EXTENSION)
|
||||||
? new AdtsExtractor(startTimeUs)
|
? new AdtsExtractor(startTimeUs)
|
||||||
: new TsExtractor(startTimeUs);
|
: new TsExtractor(startTimeUs, audioCapabilities);
|
||||||
extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor,
|
extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor,
|
||||||
switchingVariantSpliced);
|
switchingVariantSpliced);
|
||||||
} else {
|
} else {
|
||||||
|
@ -125,15 +125,12 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean prepare() throws IOException {
|
public boolean prepare(long positionUs) throws IOException {
|
||||||
if (prepared) {
|
if (prepared) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (loader == null) {
|
|
||||||
loader = new Loader("Loader:HLS");
|
|
||||||
}
|
|
||||||
continueBufferingInternal();
|
|
||||||
if (!extractors.isEmpty()) {
|
if (!extractors.isEmpty()) {
|
||||||
|
// We're not prepared, but we might have loaded what we need.
|
||||||
HlsExtractorWrapper extractor = extractors.getFirst();
|
HlsExtractorWrapper extractor = extractors.getFirst();
|
||||||
if (extractor.isPrepared()) {
|
if (extractor.isPrepared()) {
|
||||||
trackCount = extractor.getTrackCount();
|
trackCount = extractor.getTrackCount();
|
||||||
@ -146,12 +143,23 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
|||||||
trackInfos[i] = new TrackInfo(format.mimeType, chunkSource.getDurationUs());
|
trackInfos[i] = new TrackInfo(format.mimeType, chunkSource.getDurationUs());
|
||||||
}
|
}
|
||||||
prepared = true;
|
prepared = true;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!prepared) {
|
// We're not prepared and we haven't loaded what we need.
|
||||||
maybeThrowLoadableException();
|
if (loader == null) {
|
||||||
|
loader = new Loader("Loader:HLS");
|
||||||
}
|
}
|
||||||
return prepared;
|
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 false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -345,7 +353,12 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
|||||||
if (!currentLoadableExceptionFatal) {
|
if (!currentLoadableExceptionFatal) {
|
||||||
clearCurrentLoadable();
|
clearCurrentLoadable();
|
||||||
}
|
}
|
||||||
maybeStartLoading();
|
if (enabledTrackCount > 0) {
|
||||||
|
maybeStartLoading();
|
||||||
|
} else {
|
||||||
|
clearState();
|
||||||
|
allocator.trim(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -90,9 +90,9 @@ public class MetadataTrackRenderer<T> extends TrackRenderer implements Callback
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int doPrepare() throws ExoPlaybackException {
|
protected int doPrepare(long positionUs) throws ExoPlaybackException {
|
||||||
try {
|
try {
|
||||||
boolean sourcePrepared = source.prepare();
|
boolean sourcePrepared = source.prepare(positionUs);
|
||||||
if (!sourcePrepared) {
|
if (!sourcePrepared) {
|
||||||
return TrackRenderer.STATE_UNPREPARED;
|
return TrackRenderer.STATE_UNPREPARED;
|
||||||
}
|
}
|
||||||
|
@ -80,9 +80,9 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int doPrepare() throws ExoPlaybackException {
|
protected int doPrepare(long positionUs) throws ExoPlaybackException {
|
||||||
try {
|
try {
|
||||||
boolean sourcePrepared = source.prepare();
|
boolean sourcePrepared = source.prepare(positionUs);
|
||||||
if (!sourcePrepared) {
|
if (!sourcePrepared) {
|
||||||
return TrackRenderer.STATE_UNPREPARED;
|
return TrackRenderer.STATE_UNPREPARED;
|
||||||
}
|
}
|
||||||
|
@ -90,9 +90,9 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int doPrepare() throws ExoPlaybackException {
|
protected int doPrepare(long positionUs) throws ExoPlaybackException {
|
||||||
try {
|
try {
|
||||||
boolean sourcePrepared = source.prepare();
|
boolean sourcePrepared = source.prepare(positionUs);
|
||||||
if (!sourcePrepared) {
|
if (!sourcePrepared) {
|
||||||
return TrackRenderer.STATE_UNPREPARED;
|
return TrackRenderer.STATE_UNPREPARED;
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,24 @@ package com.google.android.exoplayer.upstream;
|
|||||||
/**
|
/**
|
||||||
* Provides estimates of the currently available bandwidth.
|
* 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.
|
* Indicates no bandwidth estimate is available.
|
||||||
|
@ -26,25 +26,7 @@ import android.os.Handler;
|
|||||||
* Counts transferred bytes while transfers are open and creates a bandwidth sample and updated
|
* Counts transferred bytes while transfers are open and creates a bandwidth sample and updated
|
||||||
* bandwidth estimate each time a transfer ends.
|
* bandwidth estimate each time a transfer ends.
|
||||||
*/
|
*/
|
||||||
public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener {
|
public class DefaultBandwidthMeter implements BandwidthMeter {
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final int DEFAULT_MAX_WEIGHT = 2000;
|
private static final int DEFAULT_MAX_WEIGHT = 2000;
|
||||||
|
|
||||||
|
@ -28,6 +28,8 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InterruptedIOException;
|
import java.io.InterruptedIOException;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.NoRouteToHostException;
|
||||||
|
import java.net.ProtocolException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -38,17 +40,30 @@ import java.util.regex.Pattern;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link HttpDataSource} that uses Android's {@link HttpURLConnection}.
|
* 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 {
|
public class DefaultHttpDataSource implements HttpDataSource {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default connection timeout, in milliseconds.
|
||||||
|
*/
|
||||||
public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000;
|
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;
|
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 String TAG = "HttpDataSource";
|
||||||
private static final Pattern CONTENT_RANGE_HEADER =
|
private static final Pattern CONTENT_RANGE_HEADER =
|
||||||
Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
|
Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
|
||||||
private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<byte[]>();
|
private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<byte[]>();
|
||||||
|
|
||||||
|
private final boolean allowCrossProtocolRedirects;
|
||||||
private final int connectTimeoutMillis;
|
private final int connectTimeoutMillis;
|
||||||
private final int readTimeoutMillis;
|
private final int readTimeoutMillis;
|
||||||
private final String userAgent;
|
private final String userAgent;
|
||||||
@ -103,12 +118,33 @@ public class DefaultHttpDataSource implements HttpDataSource {
|
|||||||
*/
|
*/
|
||||||
public DefaultHttpDataSource(String userAgent, Predicate<String> contentTypePredicate,
|
public DefaultHttpDataSource(String userAgent, Predicate<String> contentTypePredicate,
|
||||||
TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis) {
|
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.userAgent = Assertions.checkNotEmpty(userAgent);
|
||||||
this.contentTypePredicate = contentTypePredicate;
|
this.contentTypePredicate = contentTypePredicate;
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
this.requestProperties = new HashMap<String, String>();
|
this.requestProperties = new HashMap<String, String>();
|
||||||
this.connectTimeoutMillis = connectTimeoutMillis;
|
this.connectTimeoutMillis = connectTimeoutMillis;
|
||||||
this.readTimeoutMillis = readTimeoutMillis;
|
this.readTimeoutMillis = readTimeoutMillis;
|
||||||
|
this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -283,8 +319,58 @@ public class DefaultHttpDataSource implements HttpDataSource {
|
|||||||
return bytesToRead == C.LENGTH_UNBOUNDED ? bytesToRead : bytesToRead - bytesRead;
|
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 {
|
private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException {
|
||||||
URL url = new URL(dataSpec.uri.toString());
|
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();
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
connection.setConnectTimeout(connectTimeoutMillis);
|
connection.setConnectTimeout(connectTimeoutMillis);
|
||||||
connection.setReadTimeout(readTimeoutMillis);
|
connection.setReadTimeout(readTimeoutMillis);
|
||||||
@ -294,28 +380,56 @@ public class DefaultHttpDataSource implements HttpDataSource {
|
|||||||
connection.setRequestProperty(property.getKey(), property.getValue());
|
connection.setRequestProperty(property.getKey(), property.getValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setRangeHeader(connection, dataSpec);
|
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);
|
connection.setRequestProperty("User-Agent", userAgent);
|
||||||
if ((dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) == 0) {
|
if (!allowGzip) {
|
||||||
connection.setRequestProperty("Accept-Encoding", "identity");
|
connection.setRequestProperty("Accept-Encoding", "identity");
|
||||||
}
|
}
|
||||||
connection.connect();
|
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setRangeHeader(HttpURLConnection connection, DataSpec dataSpec) {
|
/**
|
||||||
if (dataSpec.position == 0 && dataSpec.length == C.LENGTH_UNBOUNDED) {
|
* Handles a redirect.
|
||||||
// Not required.
|
*
|
||||||
return;
|
* @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");
|
||||||
}
|
}
|
||||||
String rangeRequest = "bytes=" + dataSpec.position + "-";
|
// Form the new url.
|
||||||
if (dataSpec.length != C.LENGTH_UNBOUNDED) {
|
URL url = new URL(originalUrl, location);
|
||||||
rangeRequest += (dataSpec.position + dataSpec.length - 1);
|
// 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);
|
||||||
}
|
}
|
||||||
connection.setRequestProperty("Range", rangeRequest);
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long getContentLength(HttpURLConnection connection) {
|
/**
|
||||||
|
* 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;
|
long contentLength = C.LENGTH_UNBOUNDED;
|
||||||
String contentLengthHeader = connection.getHeaderField("Content-Length");
|
String contentLengthHeader = connection.getHeaderField("Content-Length");
|
||||||
if (!TextUtils.isEmpty(contentLengthHeader)) {
|
if (!TextUtils.isEmpty(contentLengthHeader)) {
|
||||||
@ -429,6 +543,9 @@ public class DefaultHttpDataSource implements HttpDataSource {
|
|||||||
return read;
|
return read;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the current connection, if there is one.
|
||||||
|
*/
|
||||||
private void closeConnection() {
|
private void closeConnection() {
|
||||||
if (connection != null) {
|
if (connection != null) {
|
||||||
connection.disconnect();
|
connection.disconnect();
|
||||||
|
@ -36,15 +36,36 @@ public final class DefaultUriDataSource implements UriDataSource {
|
|||||||
private UriDataSource dataSource;
|
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.
|
* {@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 userAgent The User-Agent string that should be used when requesting remote data.
|
||||||
* @param transferListener An optional listener.
|
* @param transferListener An optional listener.
|
||||||
*/
|
*/
|
||||||
public DefaultUriDataSource(String userAgent, TransferListener transferListener) {
|
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),
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -15,6 +15,11 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.util;
|
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.
|
* Defines common MIME types and helper methods.
|
||||||
*/
|
*/
|
||||||
@ -119,4 +124,37 @@ public class MimeTypes {
|
|||||||
return mimeType.equals(APPLICATION_TTML);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -8,5 +8,5 @@
|
|||||||
# project structure.
|
# project structure.
|
||||||
|
|
||||||
# Project target.
|
# Project target.
|
||||||
target=android-21
|
target=android-22
|
||||||
android.library=true
|
android.library=true
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.google.android.exoplayer.tests">
|
package="com.google.android.exoplayer.tests">
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="21"/>
|
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="22"/>
|
||||||
|
|
||||||
<application>
|
<application>
|
||||||
<uses-library android:name="android.test.runner"/>
|
<uses-library android:name="android.test.runner"/>
|
||||||
|
@ -579,7 +579,7 @@ public class Mp4ExtractorTest extends TestCase {
|
|||||||
try {
|
try {
|
||||||
switch (message.what) {
|
switch (message.what) {
|
||||||
case MSG_PREPARE:
|
case MSG_PREPARE:
|
||||||
if (!source.prepare()) {
|
if (!source.prepare(0)) {
|
||||||
sendEmptyMessage(MSG_PREPARE);
|
sendEmptyMessage(MSG_PREPARE);
|
||||||
} else {
|
} else {
|
||||||
// Select the video track and get its metadata.
|
// Select the video track and get its metadata.
|
||||||
|
@ -11,4 +11,4 @@
|
|||||||
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
|
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
|
||||||
|
|
||||||
# Project target.
|
# Project target.
|
||||||
target=android-21
|
target=android-22
|
||||||
|
Loading…
x
Reference in New Issue
Block a user