Skip to content

Instantly share code, notes, and snippets.

@akhil7687
Created November 22, 2018 12:01
Show Gist options
  • Save akhil7687/9ca0cc716268edc6b842a8d582037df1 to your computer and use it in GitHub Desktop.
Save akhil7687/9ca0cc716268edc6b842a8d582037df1 to your computer and use it in GitHub Desktop.
Exoplayer stream AES Encrypted video from URL
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;
}
}
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);
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;
}
}
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