From fc457e4c1bf6d372852cbc0e3d8bfe86a235608f Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 19 Jul 2016 09:47:37 -0700 Subject: [PATCH] Move default DRM callback implementation to library This also removes direct use of HttpURLConnection and allows use of any HttpDataSource for license requests, which means those using OkHttp can finally use the same network stack for license requests as they're using for everything else, without having to implement their own callback. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=127841045 --- .../exoplayer2/demo/PlayerActivity.java | 23 ++-- .../exoplayer2/drm/HttpMediaDrmCallback.java | 78 +++++-------- .../source/chunk/ChunkSampleStream.java | 1 - .../upstream/DefaultHttpDataSource.java | 51 ++++++++- .../exoplayer2/upstream/HttpDataSource.java | 10 ++ .../google/android/exoplayer2/util/Util.java | 48 -------- .../playbacktests/gts/DashTest.java | 19 +++- .../playbacktests/util/ExoHostedTest.java | 15 +-- .../util/TestMediaDrmCallback.java | 107 ------------------ 9 files changed, 123 insertions(+), 229 deletions(-) rename demo/src/main/java/com/google/android/exoplayer2/demo/TestMediaDrmCallback.java => library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java (50%) delete mode 100644 playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/TestMediaDrmCallback.java 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(); - } - } - } -}