diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 36e189de9f..00c5084227 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.drm.StreamingDrmSessionManager; import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; @@ -56,6 +57,8 @@ import com.google.android.exoplayer2.ui.PlayerControl; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; +import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.util.Util; import android.Manifest.permission; @@ -131,6 +134,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, private SubtitleLayout subtitleLayout; private Button retryButton; + private String userAgent; private DataSource.Factory manifestDataSourceFactory; private DataSource.Factory mediaDataSourceFactory; private FormatEvaluator.Factory formatEvaluatorFactory; @@ -148,7 +152,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - String userAgent = Util.getUserAgent(this, "ExoPlayerDemo"); + userAgent = Util.getUserAgent(this, "ExoPlayerDemo"); manifestDataSourceFactory = new DefaultDataSourceFactory(this, userAgent); DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); mediaDataSourceFactory = new DefaultDataSourceFactory(this, userAgent, bandwidthMeter); @@ -371,15 +375,14 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, if (Util.SDK_INT < 18) { return null; } - if (C.PLAYREADY_UUID.equals(uuid)) { - return StreamingDrmSessionManager.newPlayReadyInstance( - TestMediaDrmCallback.newPlayReadyInstance(licenseUrl), null, mainHandler, eventLogger); - } else if (C.WIDEVINE_UUID.equals(uuid)) { - return StreamingDrmSessionManager.newWidevineInstance( - TestMediaDrmCallback.newWidevineInstance(licenseUrl), null, mainHandler, eventLogger); - } else { - throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME); - } + HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, + new HttpDataSource.Factory() { + @Override + public HttpDataSource createDataSource() { + return new DefaultHttpDataSource(userAgent, null); + } + }); + return new StreamingDrmSessionManager(uuid, drmCallback, null, mainHandler, eventLogger); } private void onUnsupportedDrmError(UnsupportedDrmException e) { diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/TestMediaDrmCallback.java b/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java similarity index 50% rename from demo/src/main/java/com/google/android/exoplayer2/demo/TestMediaDrmCallback.java rename to library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java index dff33c9958..161bc4282e 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/TestMediaDrmCallback.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java @@ -13,30 +13,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.demo; +package com.google.android.exoplayer2.drm; -import com.google.android.exoplayer2.drm.MediaDrmCallback; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.DataSourceInputStream; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.util.Util; import android.annotation.TargetApi; import android.media.MediaDrm.KeyRequest; import android.media.MediaDrm.ProvisionRequest; +import android.net.Uri; import android.text.TextUtils; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.UUID; /** - * A {@link MediaDrmCallback} for test content. + * A {@link MediaDrmCallback} that makes requests using {@link HttpDataSource} instances. */ @TargetApi(18) -/* package */ final class TestMediaDrmCallback implements MediaDrmCallback { +public final class HttpMediaDrmCallback implements MediaDrmCallback { private static final Map PLAYREADY_KEY_REQUEST_PROPERTIES; static { @@ -47,20 +47,16 @@ import java.util.UUID; PLAYREADY_KEY_REQUEST_PROPERTIES = keyRequestProperties; } + private final HttpDataSource.Factory dataSourceFactory; private final String defaultUrl; - private final Map keyRequestProperties; - public static TestMediaDrmCallback newWidevineInstance(String defaultUrl) { - return new TestMediaDrmCallback(defaultUrl, null); - } - - public static TestMediaDrmCallback newPlayReadyInstance(String defaultUrl) { - return new TestMediaDrmCallback(defaultUrl, PLAYREADY_KEY_REQUEST_PROPERTIES); - } - - private TestMediaDrmCallback(String defaultUrl, Map keyRequestProperties) { + /** + * @param defaultUrl The default license URL. + * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. + */ + public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory) { + this.dataSourceFactory = dataSourceFactory; this.defaultUrl = defaultUrl; - this.keyRequestProperties = keyRequestProperties; } @Override @@ -75,42 +71,26 @@ import java.util.UUID; if (TextUtils.isEmpty(url)) { url = defaultUrl; } + Map keyRequestProperties = C.PLAYREADY_UUID.equals(uuid) + ? PLAYREADY_KEY_REQUEST_PROPERTIES : null; return executePost(url, request.getData(), keyRequestProperties); } - private static byte[] executePost(String url, byte[] data, Map requestProperties) + private byte[] executePost(String url, byte[] data, Map requestProperties) throws IOException { - HttpURLConnection urlConnection = null; + HttpDataSource dataSource = dataSourceFactory.createDataSource(); + if (requestProperties != null) { + for (Map.Entry requestProperty : requestProperties.entrySet()) { + dataSource.setRequestProperty(requestProperty.getKey(), requestProperty.getValue()); + } + } + DataSpec dataSpec = new DataSpec(Uri.parse(url), data, 0, 0, C.LENGTH_UNBOUNDED, null, + DataSpec.FLAG_ALLOW_GZIP); + DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec); try { - urlConnection = (HttpURLConnection) new URL(url).openConnection(); - urlConnection.setRequestMethod("POST"); - urlConnection.setDoOutput(data != null); - urlConnection.setDoInput(true); - if (requestProperties != null) { - for (Map.Entry requestProperty : requestProperties.entrySet()) { - urlConnection.setRequestProperty(requestProperty.getKey(), requestProperty.getValue()); - } - } - // Write the request body, if there is one. - if (data != null) { - OutputStream out = urlConnection.getOutputStream(); - try { - out.write(data); - } finally { - out.close(); - } - } - // Read and return the response body. - InputStream inputStream = urlConnection.getInputStream(); - try { - return Util.toByteArray(inputStream); - } finally { - inputStream.close(); - } + return Util.toByteArray(inputStream); } finally { - if (urlConnection != null) { - urlConnection.disconnect(); - } + inputStream.close(); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index ead6888424..bfec990f35 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -189,7 +189,6 @@ public class ChunkSampleStream implements SampleStream, S currentChunk.startTimeUs); } downstreamFormat = format; - return sampleQueue.readData(formatHolder, buffer, loadingFinished, lastSeekPositionUs); } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index ca85ba4c30..93c403e0dc 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; import java.io.OutputStream; +import java.lang.reflect.Method; import java.net.HttpURLConnection; import java.net.NoRouteToHostException; import java.net.ProtocolException; @@ -59,8 +60,9 @@ public class DefaultHttpDataSource implements HttpDataSource { */ 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 = "DefaultHttpDataSource"; + private static final int MAX_REDIRECTS = 20; // Same limit as okhttp. + private static final long MAX_BYTES_TO_DRAIN = 2048; private static final Pattern CONTENT_RANGE_HEADER = Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$"); private static final AtomicReference skipBufferReference = new AtomicReference<>(); @@ -266,7 +268,7 @@ public class DefaultHttpDataSource implements HttpDataSource { public void close() throws HttpDataSourceException { try { if (inputStream != null) { - Util.maybeTerminateInputStream(connection, bytesRemaining()); + maybeTerminateInputStream(connection, bytesRemaining()); try { inputStream.close(); } catch (IOException e) { @@ -564,6 +566,51 @@ public class DefaultHttpDataSource implements HttpDataSource { return read; } + /** + * On platform API levels 19 and 20, okhttp's implementation of {@link InputStream#close} can + * block for a long time if the stream has a lot of data remaining. Call this method before + * closing the input stream to make a best effort to cause the input stream to encounter an + * unexpected end of input, working around this issue. On other platform API levels, the method + * does nothing. + * + * @param connection The connection whose {@link InputStream} should be terminated. + * @param bytesRemaining The number of bytes remaining to be read from the input stream if its + * length is known. {@link C#LENGTH_UNBOUNDED} otherwise. + */ + private static void maybeTerminateInputStream(HttpURLConnection connection, long bytesRemaining) { + if (Util.SDK_INT != 19 && Util.SDK_INT != 20) { + return; + } + + try { + InputStream inputStream = connection.getInputStream(); + if (bytesRemaining == C.LENGTH_UNBOUNDED) { + // If the input stream has already ended, do nothing. The socket may be re-used. + if (inputStream.read() == -1) { + return; + } + } else if (bytesRemaining <= MAX_BYTES_TO_DRAIN) { + // There isn't much data left. Prefer to allow it to drain, which may allow the socket to be + // re-used. + return; + } + String className = inputStream.getClass().getName(); + if (className.equals("com.android.okhttp.internal.http.HttpTransport$ChunkedInputStream") + || className.equals( + "com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream")) { + Class superclass = inputStream.getClass().getSuperclass(); + Method unexpectedEndOfInput = superclass.getDeclaredMethod("unexpectedEndOfInput"); + unexpectedEndOfInput.setAccessible(true); + unexpectedEndOfInput.invoke(inputStream); + } + } catch (Exception e) { + // If an IOException then the connection didn't ever have an input stream, or it was closed + // already. If another type of exception then something went wrong, most likely the device + // isn't using okhttp. + } + } + + /** * Closes the current connection quietly, if there is one. */ diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java index 99f4ad88a8..83ae41429a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java @@ -29,6 +29,16 @@ import java.util.Map; */ public interface HttpDataSource extends DataSource { + /** + * A factory for {@link HttpDataSource} instances. + */ + interface Factory extends DataSource.Factory { + + @Override + HttpDataSource createDataSource(); + + } + /** * A {@link Predicate} that rejects content types often used for pay-walls. */ diff --git a/library/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/src/main/java/com/google/android/exoplayer2/util/Util.java index 8c29d0c934..15d0613f76 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -31,9 +31,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.lang.reflect.Method; import java.math.BigDecimal; -import java.net.HttpURLConnection; import java.text.ParseException; import java.util.Arrays; import java.util.Calendar; @@ -111,8 +109,6 @@ public final class Util { private static final Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile("%([A-Fa-f0-9]{2})"); - private static final long MAX_BYTES_TO_DRAIN = 2048; - private Util() {} /** @@ -522,50 +518,6 @@ public final class Util { return intArray; } - /** - * On platform API levels 19 and 20, okhttp's implementation of {@link InputStream#close} can - * block for a long time if the stream has a lot of data remaining. Call this method before - * closing the input stream to make a best effort to cause the input stream to encounter an - * unexpected end of input, working around this issue. On other platform API levels, the method - * does nothing. - * - * @param connection The connection whose {@link InputStream} should be terminated. - * @param bytesRemaining The number of bytes remaining to be read from the input stream if its - * length is known. {@link C#LENGTH_UNBOUNDED} otherwise. - */ - public static void maybeTerminateInputStream(HttpURLConnection connection, long bytesRemaining) { - if (SDK_INT != 19 && SDK_INT != 20) { - return; - } - - try { - InputStream inputStream = connection.getInputStream(); - if (bytesRemaining == C.LENGTH_UNBOUNDED) { - // If the input stream has already ended, do nothing. The socket may be re-used. - if (inputStream.read() == -1) { - return; - } - } else if (bytesRemaining <= MAX_BYTES_TO_DRAIN) { - // There isn't much data left. Prefer to allow it to drain, which may allow the socket to be - // re-used. - return; - } - String className = inputStream.getClass().getName(); - if (className.equals("com.android.okhttp.internal.http.HttpTransport$ChunkedInputStream") - || className.equals( - "com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream")) { - Class superclass = inputStream.getClass().getSuperclass(); - Method unexpectedEndOfInput = superclass.getDeclaredMethod("unexpectedEndOfInput"); - unexpectedEndOfInput.setAccessible(true); - unexpectedEndOfInput.invoke(inputStream); - } - } catch (Exception e) { - // If an IOException then the connection didn't ever have an input stream, or it was closed - // already. If another type of exception then something went wrong, most likely the device - // isn't using okhttp. - } - } - /** * Given a {@link DataSpec} and a number of bytes already loaded, returns a {@link DataSpec} * that represents the remainder of the data. diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java index 34542d004f..82b8e82b5d 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java +++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.drm.StreamingDrmSessionManager; import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; @@ -30,7 +31,6 @@ import com.google.android.exoplayer2.playbacktests.util.DecoderCountersUtil; import com.google.android.exoplayer2.playbacktests.util.ExoHostedTest; import com.google.android.exoplayer2.playbacktests.util.HostActivity; import com.google.android.exoplayer2.playbacktests.util.MetricsLogger; -import com.google.android.exoplayer2.playbacktests.util.TestMediaDrmCallback; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -43,6 +43,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; +import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -195,7 +197,8 @@ public final class DashTest extends ActivityInstrumentationTestCase2 keyRequestProperties; - - public static TestMediaDrmCallback newWidevineInstance(String contentId, String provider) { - String defaultUrl = WIDEVINE_BASE_URL + "?video_id=" + contentId + "&provider=" + provider; - return new TestMediaDrmCallback(defaultUrl, null); - } - - private TestMediaDrmCallback(String defaultUrl, Map keyRequestProperties) { - this.defaultUrl = defaultUrl; - this.keyRequestProperties = keyRequestProperties; - } - - @Override - public byte[] executeProvisionRequest(UUID uuid, MediaDrm.ProvisionRequest request) - throws IOException { - String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData(), - Charset.defaultCharset()); - return executePost(url, null, null); - } - - @Override - public byte[] executeKeyRequest(UUID uuid, MediaDrm.KeyRequest request) throws Exception { - String url = request.getDefaultUrl(); - if (TextUtils.isEmpty(url)) { - url = defaultUrl; - } - return executePost(url, request.getData(), keyRequestProperties); - } - - private static byte[] executePost(String url, byte[] data, Map requestProperties) - throws IOException { - HttpURLConnection urlConnection = null; - try { - urlConnection = (HttpURLConnection) new URL(url).openConnection(); - urlConnection.setRequestMethod("POST"); - urlConnection.setDoOutput(data != null); - urlConnection.setDoInput(true); - if (requestProperties != null) { - for (Map.Entry requestProperty : requestProperties.entrySet()) { - urlConnection.setRequestProperty(requestProperty.getKey(), requestProperty.getValue()); - } - } - // Write the request body, if there is one. - if (data != null) { - OutputStream out = urlConnection.getOutputStream(); - try { - out.write(data); - } finally { - out.close(); - } - } - // Read and return the response body. - InputStream inputStream = urlConnection.getInputStream(); - try { - return Util.toByteArray(inputStream); - } finally { - inputStream.close(); - } - } finally { - if (urlConnection != null) { - urlConnection.disconnect(); - } - } - } -}