Created
November 22, 2018 12:01
-
-
Save akhil7687/9ca0cc716268edc6b842a8d582037df1 to your computer and use it in GitHub Desktop.
Exoplayer stream AES Encrypted video from URL
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package player; | |
import java.math.BigInteger; | |
import java.security.InvalidAlgorithmParameterException; | |
import java.security.InvalidKeyException; | |
import java.security.Key; | |
import java.util.Arrays; | |
import javax.crypto.Cipher; | |
import javax.crypto.ShortBufferException; | |
import javax.crypto.spec.IvParameterSpec; | |
/** | |
* Created by wangye on 16-10-19. | |
*/ | |
public class AesHelper { | |
private static final int BLOCK_SIZE = 16; | |
private static final int AES_BLOCK_SIZE = 16; | |
private AesHelper() {} | |
public static final void jumpToOffset(final Cipher c, | |
final Key aesKey, final IvParameterSpec iv, final long offset) { | |
if (!c.getAlgorithm().toUpperCase().startsWith("AES/CTR")) { | |
throw new IllegalArgumentException( | |
"Invalid algorithm, only AES/CTR mode supported"); | |
} | |
if (offset < 0) { | |
throw new IllegalArgumentException("Invalid offset"); | |
} | |
final int skip = (int) (offset % AES_BLOCK_SIZE); | |
final IvParameterSpec calculatedIVForOffset = calculateIVForOffset(iv, | |
offset - skip); | |
try { | |
c.init(Cipher.ENCRYPT_MODE, aesKey, calculatedIVForOffset); | |
final byte[] skipBuffer = new byte[skip]; | |
c.update(skipBuffer, 0, skip, skipBuffer); | |
Arrays.fill(skipBuffer, (byte) 0); | |
} catch (ShortBufferException | InvalidKeyException | |
| InvalidAlgorithmParameterException e) { | |
throw new IllegalStateException(e); | |
} | |
} | |
private static IvParameterSpec calculateIVForOffset(final IvParameterSpec iv, | |
final long blockOffset) { | |
final BigInteger ivBI = new BigInteger(1, iv.getIV()); | |
final BigInteger ivForOffsetBI = ivBI.add(BigInteger.valueOf(blockOffset | |
/ AES_BLOCK_SIZE)); | |
final byte[] ivForOffsetBA = ivForOffsetBI.toByteArray(); | |
final IvParameterSpec ivForOffset; | |
if (ivForOffsetBA.length >= AES_BLOCK_SIZE) { | |
ivForOffset = new IvParameterSpec(ivForOffsetBA, ivForOffsetBA.length - AES_BLOCK_SIZE, | |
AES_BLOCK_SIZE); | |
} else { | |
final byte[] ivForOffsetBASized = new byte[AES_BLOCK_SIZE]; | |
System.arraycopy(ivForOffsetBA, 0, ivForOffsetBASized, AES_BLOCK_SIZE | |
- ivForOffsetBA.length, ivForOffsetBA.length); | |
ivForOffset = new IvParameterSpec(ivForOffsetBASized); | |
} | |
return ivForOffset; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
mSimpleExoPlayerView = (SimpleExoPlayerView) findViewById(R.id.simpleexoplayerview); | |
key = "<YOURKEY>".getBytes(); | |
iv = "<YOURIV>".getBytes(); | |
mSecretKeySpec = new SecretKeySpec(key, AES_ALGORITHM); | |
mIvParameterSpec = new IvParameterSpec(iv); | |
try { | |
mCipher = Cipher.getInstance(AES_TRANSFORMATION); | |
mCipher.init(Cipher.DECRYPT_MODE, mSecretKeySpec, mIvParameterSpec); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
MediaSource sampleSource = new ExtractorMediaSource( | |
Uri.parse(<ENCRYPTED_FILE_URL>), | |
new TestHttpFactory(mCipher, mSecretKeySpec, mIvParameterSpec, | |
new OkHttpClient(), | |
"Android.ExoPlayer", | |
null), | |
new DefaultExtractorsFactory(), null, null); | |
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); | |
TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveVideoTrackSelection.Factory(bandwidthMeter); | |
TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); | |
LoadControl loadControl = new DefaultLoadControl(); | |
player = ExoPlayerFactory.newSimpleInstance(this, trackSelector, loadControl); | |
player.prepare(videoSource); | |
player.setPlayWhenReady(true); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package player; | |
import android.net.Uri; | |
import android.util.Log; | |
import com.google.android.exoplayer2.C; | |
import com.google.android.exoplayer2.upstream.DataSourceException; | |
import com.google.android.exoplayer2.upstream.DataSpec; | |
import com.google.android.exoplayer2.upstream.HttpDataSource; | |
import com.google.android.exoplayer2.upstream.TransferListener; | |
import com.google.android.exoplayer2.util.Assertions; | |
import com.google.android.exoplayer2.util.Predicate; | |
import java.io.EOFException; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.io.InterruptedIOException; | |
import java.math.BigInteger; | |
import java.security.InvalidAlgorithmParameterException; | |
import java.security.InvalidKeyException; | |
import java.security.NoSuchAlgorithmException; | |
import java.util.Arrays; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.concurrent.atomic.AtomicReference; | |
import javax.crypto.Cipher; | |
import javax.crypto.CipherInputStream; | |
import javax.crypto.NoSuchPaddingException; | |
import javax.crypto.spec.IvParameterSpec; | |
import javax.crypto.spec.SecretKeySpec; | |
import cakart.in.cafoundationflashcards.CaLib; | |
import okhttp3.CacheControl; | |
import okhttp3.HttpUrl; | |
import okhttp3.MediaType; | |
import okhttp3.OkHttpClient; | |
import okhttp3.Request; | |
import okhttp3.RequestBody; | |
import okhttp3.Response; | |
/** | |
* An {@link HttpDataSource} that delegates to Square's {@link OkHttpClient}. | |
*/ | |
public class OkHttpDataSource implements HttpDataSource { | |
private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<>(); | |
private static final String TAG = "OkHttpDataSource"; | |
private final OkHttpClient okHttpClient; | |
private final String userAgent; | |
private final Predicate<String> contentTypePredicate; | |
private final TransferListener listener; | |
private final CacheControl cacheControl; | |
private final HashMap<String, String> requestProperties; | |
private final boolean needEncrypty; | |
private DataSpec dataSpec; | |
private Response response; | |
private CipherInputStream mCipherInputStream; | |
private InputStream responseByteStream; | |
private boolean opened; | |
private long bytesToSkip; | |
private long bytesToRead; | |
private long bytesSkipped; | |
private long bytesRead; | |
IvParameterSpec mIvParameterSpec; | |
SecretKeySpec mSecretKeySpec; | |
/** | |
* @param client An {@link OkHttpClient} for use by the source. | |
* @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 com.google.android.exoplayer2.upstream.HttpDataSource.InvalidContentTypeException} is | |
* thrown from {@link #open(DataSpec)}. | |
*/ | |
public OkHttpDataSource(OkHttpClient client, String userAgent, | |
Predicate<String> contentTypePredicate) { | |
this(client, userAgent, contentTypePredicate, null); | |
} | |
/** | |
* @param client An {@link OkHttpClient} for use by the source. | |
* @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 com.google.android.exoplayer2.upstream.HttpDataSource.InvalidContentTypeException} is | |
* thrown from {@link #open(DataSpec)}. | |
* @param listener An optional listener. | |
*/ | |
public OkHttpDataSource(OkHttpClient client, String userAgent, | |
Predicate<String> contentTypePredicate, TransferListener listener) { | |
this(client, userAgent, contentTypePredicate, listener, true, null); | |
} | |
/** | |
* @param client An {@link OkHttpClient} for use by the source. | |
* @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 com.google.android.exoplayer2.upstream.HttpDataSource.InvalidContentTypeException} is | |
* thrown from {@link #open(DataSpec)}. | |
* @param listener An optional listener. | |
* @param cacheControl An optional {@link CacheControl} which sets all requests' Cache-Control | |
* header. For example, you could force the network response for all requests. | |
* | |
*/ | |
public OkHttpDataSource(OkHttpClient client, String userAgent, | |
Predicate<String> contentTypePredicate, TransferListener listener, boolean needEncrypty, | |
CacheControl cacheControl) { | |
this.okHttpClient = Assertions.checkNotNull(client); | |
this.userAgent = Assertions.checkNotEmpty(userAgent); | |
this.contentTypePredicate = contentTypePredicate; | |
this.listener = listener; | |
this.cacheControl = cacheControl; | |
this.requestProperties = new HashMap<>(); | |
this.needEncrypty = needEncrypty; | |
} | |
@Override | |
public Uri getUri() { | |
return response == null ? null : Uri.parse(response.request().url().toString()); | |
} | |
@Override | |
public Map<String, List<String>> getResponseHeaders() { | |
return response == null ? null : response.headers().toMultimap(); | |
} | |
@Override | |
public void setRequestProperty(String name, String value) { | |
Assertions.checkNotNull(name); | |
Assertions.checkNotNull(value); | |
synchronized (requestProperties) { | |
requestProperties.put(name, value); | |
} | |
} | |
private void reconnect() throws HttpDataSourceException { | |
close(); | |
open(dataSpec); | |
Log.i("Akhil", "Reconnected successfully!"); | |
} | |
@Override | |
public void clearRequestProperty(String name) { | |
Assertions.checkNotNull(name); | |
synchronized (requestProperties) { | |
requestProperties.remove(name); | |
} | |
} | |
@Override | |
public void clearAllRequestProperties() { | |
synchronized (requestProperties) { | |
requestProperties.clear(); | |
} | |
} | |
Cipher cipher; | |
@Override | |
public long open(DataSpec dataSpec) throws HttpDataSourceException { | |
Log.i(TAG, "open dataSpec: " + dataSpec.length+" / "+dataSpec.position); | |
this.dataSpec = dataSpec; | |
this.bytesRead = 0; | |
this.bytesSkipped = 0; | |
Request request = makeRequest(dataSpec); | |
try { | |
response = okHttpClient.newCall(request).execute(); | |
} catch (IOException e) { | |
Log.d("Akhil","Reconnecting...."); | |
try { | |
Thread.sleep(8000); | |
}catch (Exception ex){ | |
} | |
reconnect(); | |
//throw new HttpDataSourceException(dataSpec,HttpDataSourceException.TYPE_OPEN); | |
} | |
try{ | |
responseByteStream = response.body().byteStream(); | |
if (needEncrypty) { | |
byte AES_KEY[] = "<YOURKEY>".getBytes(); | |
byte AES_IV[] = "<YOURIV>".getBytes(); | |
mSecretKeySpec = new SecretKeySpec(AES_KEY, "AES"); | |
mIvParameterSpec = new IvParameterSpec(AES_IV); | |
cipher = Cipher.getInstance("AES/CTR/NoPadding"); | |
cipher.init(Cipher.DECRYPT_MODE, mSecretKeySpec, mIvParameterSpec); | |
mCipherInputStream = new CipherInputStream(responseByteStream, cipher); | |
AesHelper.jumpToOffset(cipher, mSecretKeySpec, mIvParameterSpec, dataSpec.position); | |
} | |
} catch (NoSuchPaddingException e) { | |
e.printStackTrace(); | |
} catch (InvalidAlgorithmParameterException e) { | |
e.printStackTrace(); | |
} catch (NoSuchAlgorithmException e) { | |
e.printStackTrace(); | |
} catch (InvalidKeyException e) { | |
e.printStackTrace(); | |
} | |
int responseCode = response.code(); | |
// Check for a valid response code. | |
if (!response.isSuccessful()) { | |
Map<String, List<String>> headers = response.headers().toMultimap(); | |
closeConnectionQuietly(); | |
InvalidResponseCodeException exception = new InvalidResponseCodeException( | |
responseCode, headers, dataSpec); | |
if (responseCode == 416) { | |
exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE)); | |
} | |
try { | |
throw exception; | |
} catch (InvalidResponseCodeException e) { | |
e.printStackTrace(); | |
} | |
} | |
// Check for a valid content type. | |
MediaType mediaType = response.body().contentType(); | |
String contentType = mediaType != null ? mediaType.toString() : null; | |
if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) { | |
Log.d("Akhil","Closing connect"); | |
closeConnectionQuietly(); | |
try { | |
throw new InvalidContentTypeException(contentType, dataSpec); | |
} catch (InvalidContentTypeException e) { | |
e.printStackTrace(); | |
} | |
} | |
// If we requested a range starting from a non-zero position and received a 200 rather than a | |
// 206, then the server does not support partial requests. We'll need to manually skip to the | |
// requested position. | |
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0; | |
// Determine the length of the data to be read, after skipping. | |
long contentLength = response.body().contentLength(); | |
bytesToRead = dataSpec.length != C.LENGTH_UNSET ? dataSpec.length | |
: contentLength != -1 ? contentLength - bytesToSkip | |
: C.LENGTH_UNSET; | |
opened = true; | |
if (listener != null) { | |
listener.onTransferStart(this,dataSpec); | |
} | |
return bytesToRead; | |
} | |
@Override | |
public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException { | |
try { | |
skipInternal(); | |
return readInternal(buffer, offset, readLength); | |
} catch (IOException e) { | |
throw new HttpDataSourceException(dataSpec,HttpDataSourceException.TYPE_READ); | |
} | |
} | |
@Override | |
public void close() throws HttpDataSourceException { | |
if (opened) { | |
opened = false; | |
if (listener != null) { | |
listener.onTransferEnd(this); | |
} | |
closeConnectionQuietly(); | |
} | |
} | |
/** | |
* Returns the number of bytes that have been skipped since the most recent call to | |
* {@link #open(DataSpec)}. | |
* | |
* @return The number of bytes skipped. | |
*/ | |
protected final long bytesSkipped() { | |
return bytesSkipped; | |
} | |
/** | |
* Returns the number of bytes that have been read since the most recent call to | |
* {@link #open(DataSpec)}. | |
* | |
* @return The number of bytes read. | |
*/ | |
protected final long bytesRead() { | |
return bytesRead; | |
} | |
/** | |
* Returns the number of bytes that are still to be read for the current {@link DataSpec}. | |
* <p> | |
* If the total length of the data being read is known, then this length minus {@code bytesRead()} | |
* is returned. If the total length is unknown, {@link C#LENGTH_UNSET} is returned. | |
* | |
* @return The remaining length, or {@link C#LENGTH_UNSET}. | |
*/ | |
protected final long bytesRemaining() { | |
return bytesToRead == C.LENGTH_UNSET ? bytesToRead : bytesToRead - bytesRead; | |
} | |
/** | |
* Establishes a connection. | |
*/ | |
private Request makeRequest(DataSpec dataSpec) { | |
long position = dataSpec.position; | |
long length = dataSpec.length; | |
boolean allowGzip = (dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) != 0; | |
HttpUrl url = HttpUrl.parse(dataSpec.uri.toString()); | |
Request.Builder builder = new Request.Builder().url(url); | |
if (cacheControl != null) { | |
builder.cacheControl(cacheControl); | |
} | |
synchronized (requestProperties) { | |
for (Map.Entry<String, String> property : requestProperties.entrySet()) { | |
builder.addHeader(property.getKey(), property.getValue()); | |
} | |
} | |
if (!(position == 0 && length == C.LENGTH_UNSET)) { | |
String rangeRequest = "bytes=" + position + "-"; | |
if (length != C.LENGTH_UNSET) { | |
rangeRequest += (position + length - 1); | |
} | |
builder.addHeader("Range", rangeRequest); | |
} | |
builder.addHeader("User-Agent", userAgent); | |
if (!allowGzip) { | |
builder.addHeader("Accept-Encoding", "identity"); | |
} | |
if (dataSpec.postBody != null) { | |
builder.post(RequestBody.create(null, dataSpec.postBody)); | |
} | |
return builder.build(); | |
} | |
/** | |
* Skips any bytes that need skipping. Else does nothing. | |
* <p> | |
* This implementation is based roughly on {@code libcore.io.Streams.skipByReading()}. | |
* | |
* @throws InterruptedIOException If the thread is interrupted during the operation. | |
* @throws EOFException If the end of the input stream is reached before the bytes are skipped. | |
*/ | |
private void skipInternal() throws IOException { | |
if (bytesSkipped == bytesToSkip) { | |
return; | |
} | |
InputStream toRead = needEncrypty ? mCipherInputStream : responseByteStream; | |
// Acquire the shared skip buffer. | |
byte[] skipBuffer = skipBufferReference.getAndSet(null); | |
if (skipBuffer == null) { | |
skipBuffer = new byte[4096]; | |
} | |
while (bytesSkipped != bytesToSkip) { | |
int readLength = (int) Math.min(bytesToSkip - bytesSkipped, skipBuffer.length); | |
// int read = responseByteStream.read(skipBuffer, 0, readLength); | |
int read = toRead.read(skipBuffer, 0, readLength); | |
if (Thread.interrupted()) { | |
throw new InterruptedIOException(); | |
} | |
if (read == -1) { | |
throw new EOFException(); | |
} | |
bytesSkipped += read; | |
if (listener != null) { | |
listener.onBytesTransferred(this,read); | |
} | |
} | |
// Release the shared skip buffer. | |
skipBufferReference.set(skipBuffer); | |
} | |
/** | |
* Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at | |
* index {@code offset}. | |
* <p> | |
* This method blocks until at least one byte of data can be read, the end of the opened range is | |
* detected, or an exception is thrown. | |
* | |
* @param buffer The buffer into which the read data should be stored. | |
* @param offset The start offset into {@code buffer} at which data should be written. | |
* @param readLength The maximum number of bytes to read. | |
* @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the end of the opened | |
* range is reached. | |
* @throws IOException If an error occurs reading from the source. | |
*/ | |
private int readInternal(byte[] buffer, int offset, int readLength) throws IOException { | |
InputStream toRead = needEncrypty ? mCipherInputStream : responseByteStream; | |
readLength = bytesToRead == C.LENGTH_UNSET ? readLength | |
: (int) Math.min(readLength, bytesToRead - bytesRead); | |
if (readLength == 0) { | |
// We've read all of the requested data. | |
return C.RESULT_END_OF_INPUT; | |
} | |
int read = toRead.read(buffer, offset, readLength); | |
if (read == -1) { | |
if (bytesToRead != C.LENGTH_UNSET && bytesToRead != bytesRead) { | |
// The server closed the connection having not sent sufficient data. | |
throw new EOFException(); | |
} | |
return C.RESULT_END_OF_INPUT; | |
} | |
bytesRead += read; | |
if (listener != null) { | |
listener.onBytesTransferred(this,read); | |
} | |
return read; | |
} | |
/** | |
* Closes the current connection quietly, if there is one. | |
*/ | |
private void closeConnectionQuietly() { | |
response.body().close(); | |
response = null; | |
responseByteStream = null; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package player; | |
import android.support.annotation.NonNull; | |
import android.support.annotation.Nullable; | |
import com.google.android.exoplayer2.upstream.DataSource; | |
import com.google.android.exoplayer2.upstream.HttpDataSource; | |
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; | |
import com.google.android.exoplayer2.upstream.TransferListener; | |
import javax.crypto.Cipher; | |
import javax.crypto.spec.IvParameterSpec; | |
import javax.crypto.spec.SecretKeySpec; | |
import okhttp3.CacheControl; | |
import okhttp3.Call; | |
import okhttp3.OkHttpClient; | |
public class TestHttpFactory extends BaseFactory { | |
@NonNull | |
private final Call.Factory callFactory; | |
@Nullable | |
private final String userAgent; | |
@Nullable | |
private final TransferListener listener; | |
@Nullable | |
private final CacheControl cacheControl; | |
private Cipher mCipher; | |
private SecretKeySpec mSecretKeySpec; | |
private IvParameterSpec mIvParameterSpec; | |
/** | |
* @param callFactory A {@link Call.Factory} (typically an {@link OkHttpClient}) for use | |
* by the sources created by the factory. | |
* @param userAgent An optional User-Agent string. | |
* @param listener An optional listener. | |
*/ | |
public TestHttpFactory( | |
Cipher cipher, | |
SecretKeySpec secretKeySpec, | |
IvParameterSpec ivParameterSpec, | |
@NonNull Call.Factory callFactory, | |
@Nullable String userAgent, | |
@Nullable TransferListener listener) { | |
this(cipher,secretKeySpec,ivParameterSpec,callFactory, userAgent, listener, null); | |
} | |
/** | |
* @param callFactory A {@link Call.Factory} (typically an {@link OkHttpClient}) for use | |
* by the sources created by the factory. | |
* @param userAgent An optional User-Agent string. | |
* @param listener An optional listener. | |
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header. | |
*/ | |
public TestHttpFactory( | |
Cipher cipher, SecretKeySpec secretKeySpec, IvParameterSpec ivParameterSpec, | |
@NonNull Call.Factory callFactory, | |
@Nullable String userAgent, | |
@Nullable TransferListener listener, | |
@Nullable CacheControl cacheControl) { | |
this.mCipher = cipher; | |
this.mSecretKeySpec = secretKeySpec; | |
this.mIvParameterSpec = ivParameterSpec; | |
this.callFactory = callFactory; | |
this.userAgent = userAgent; | |
this.listener = listener; | |
this.cacheControl = cacheControl; | |
} | |
@Override | |
protected OkHttpDataSource createDataSourceInternal() { | |
return new OkHttpDataSource(new OkHttpClient(),userAgent,null,listener,true,cacheControl); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment