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
This commit is contained in:
olly 2016-07-19 09:47:37 -07:00 committed by Oliver Woodman
parent a61828a675
commit fc457e4c1b
9 changed files with 123 additions and 229 deletions

View File

@ -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) {

View File

@ -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<String, String> 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<String, String> 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<String, String> 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,43 +71,27 @@ import java.util.UUID;
if (TextUtils.isEmpty(url)) {
url = defaultUrl;
}
Map<String, String> 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<String, String> requestProperties)
private byte[] executePost(String url, byte[] data, Map<String, String> requestProperties)
throws IOException {
HttpURLConnection urlConnection = null;
try {
urlConnection = (HttpURLConnection) new URL(url).openConnection();
urlConnection.setRequestMethod("POST");
urlConnection.setDoOutput(data != null);
urlConnection.setDoInput(true);
HttpDataSource dataSource = dataSourceFactory.createDataSource();
if (requestProperties != null) {
for (Map.Entry<String, String> requestProperty : requestProperties.entrySet()) {
urlConnection.setRequestProperty(requestProperty.getKey(), requestProperty.getValue());
dataSource.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();
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 {
return Util.toByteArray(inputStream);
} finally {
inputStream.close();
}
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
}
}
}

View File

@ -189,7 +189,6 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
currentChunk.startTimeUs);
}
downstreamFormat = format;
return sampleQueue.readData(formatHolder, buffer, loadingFinished, lastSeekPositionUs);
}

View File

@ -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<byte[]> 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.
*/

View File

@ -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.
*/

View File

@ -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.

View File

@ -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<HostActivit
WIDEVINE_VP9_180P_VIDEO_REPRESENTATION_ID,
WIDEVINE_VP9_360P_VIDEO_REPRESENTATION_ID};
private static final String WIDEVINE_PROVIDER = "widevine_test";
private static final String WIDEVINE_LICENSE_URL =
"https://proxy.uat.widevine.com/proxy?provider=widevine_test&video_id=";
private static final String WIDEVINE_SW_CRYPTO_CONTENT_ID = "exoplayer_test_1";
private static final String WIDEVINE_HW_SECURE_DECODE_CONTENT_ID = "exoplayer_test_2";
private static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL);
@ -692,7 +695,7 @@ public final class DashTest extends ActivityInstrumentationTestCase2<HostActivit
@Override
@TargetApi(18)
protected final StreamingDrmSessionManager buildDrmSessionManager() {
protected final StreamingDrmSessionManager buildDrmSessionManager(final String userAgent) {
StreamingDrmSessionManager drmSessionManager = null;
if (isWidevineEncrypted) {
try {
@ -703,8 +706,14 @@ public final class DashTest extends ActivityInstrumentationTestCase2<HostActivit
String widevineContentId = forceL3Widevine ? WIDEVINE_SW_CRYPTO_CONTENT_ID
: WIDEVINE_SECURITY_LEVEL_1.equals(securityProperty)
? WIDEVINE_HW_SECURE_DECODE_CONTENT_ID : WIDEVINE_SW_CRYPTO_CONTENT_ID;
TestMediaDrmCallback drmCallback = TestMediaDrmCallback.newWidevineInstance(
widevineContentId, WIDEVINE_PROVIDER);
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(
WIDEVINE_LICENSE_URL + widevineContentId,
new HttpDataSource.Factory() {
@Override
public HttpDataSource createDataSource() {
return new DefaultHttpDataSource(userAgent, null);
}
});
drmSessionManager = StreamingDrmSessionManager.newWidevineInstance(drmCallback, null,
null, null);
if (forceL3Widevine && !WIDEVINE_SECURITY_LEVEL_3.equals(securityProperty)) {

View File

@ -15,10 +15,6 @@
*/
package com.google.android.exoplayer2.playbacktests.util;
import android.os.Handler;
import android.os.SystemClock;
import android.util.Log;
import android.view.Surface;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
@ -34,6 +30,10 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.util.Util;
import android.os.Handler;
import android.os.SystemClock;
import android.util.Log;
import android.view.Surface;
import junit.framework.Assert;
@ -123,9 +123,10 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen
public final void onStart(HostActivity host, Surface surface) {
// Build the player.
trackSelector = buildTrackSelector(host);
DrmSessionManager drmSessionManager = buildDrmSessionManager();
String userAgent = "ExoPlayerPlaybackTests";
DrmSessionManager drmSessionManager = buildDrmSessionManager(userAgent);
player = buildExoPlayer(host, surface, trackSelector, drmSessionManager);
player.setMediaSource(buildSource(host, Util.getUserAgent(host, "ExoPlayerPlaybackTests")));
player.setMediaSource(buildSource(host, Util.getUserAgent(host, userAgent)));
player.addListener(this);
player.setDebugListener(this);
player.setPlayWhenReady(true);
@ -275,7 +276,7 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen
// Internal logic
protected DrmSessionManager buildDrmSessionManager() {
protected DrmSessionManager buildDrmSessionManager(String userAgent) {
// Do nothing. Interested subclasses may override.
return null;
}

View File

@ -1,107 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.playbacktests.util;
import android.annotation.TargetApi;
import android.media.MediaDrm;
import android.text.TextUtils;
import com.google.android.exoplayer2.drm.MediaDrmCallback;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.UUID;
/**
* A {@link MediaDrmCallback} for Widevine test content.
*/
@TargetApi(18)
public final class TestMediaDrmCallback implements MediaDrmCallback {
private static final String WIDEVINE_BASE_URL = "https://proxy.uat.widevine.com/proxy";
private final String defaultUrl;
private final Map<String, String> 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<String, String> 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<String, String> 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<String, String> 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();
}
}
}
}