GVillani82 GVillani82 - 2 months ago 22x
Android Question

Reproducing encrypted video using ExoPlayer

I'm using ExoPlayer, in Android, and I'm trying to reproduce an encrypted video stored locally.

The modularity of ExoPlayer allows to create custom components that can be injected in the ExoPlayer, and this seems the case. Indeed, after some researches I realized that for achive that task I could create a custom DataSource and overriding


I also find this solution, but actually here the entire file is decrypted in one step and stored in a clear inputstream. This can be good in many situation. But what if I need to reproduce big file?

So the question is: how can I reproduce encrypted video in ExoPlayer, decrypting content "on-fly" (without decrypting the entire file)? Is this possibile?

I tried creating a custom DataSource that has the open() method:

public long open(DataSpec dataSpec) throws FileDataSourceException {
try {
File file = new File(dataSpec.uri.getPath());

clearInputStream = new CipherInputStream(new FileInputStream(file), mCipher);

long skipped = clearInputStream.skip(dataSpec.position);
if (skipped < dataSpec.position) {
throw new EOFException();
if (dataSpec.length != C.LENGTH_UNBOUNDED) {
bytesRemaining = dataSpec.length;
} else {
bytesRemaining = clearInputStream.available();
if (bytesRemaining == 0) {
bytesRemaining = C.LENGTH_UNBOUNDED;
} catch (EOFException e) {
} catch (IOException e) {

opened = true;
if (listener != null) {

return bytesRemaining;

And this is the read() method:

public int read(byte[] buffer, int offset, int readLength) throws FileDataSourceException {
if (bytesRemaining == 0) {
return -1;
} else {
int bytesRead = 0;

int bytesToRead = bytesRemaining == C.LENGTH_UNBOUNDED ? readLength
: (int) Math.min(bytesRemaining, readLength);
try {
bytesRead = clearInputStream.read(buffer, offset, bytesToRead);
} catch (IOException e) {

if (bytesRead > 0) {
if (bytesRemaining != C.LENGTH_UNBOUNDED) {
bytesRemaining -= bytesRead;
if (listener != null) {

return bytesRead;

If instead of an encoded file I pass a clear file, and just remove the CipherInputStream part, then it works fine, instead with encrypted file I obtain this error:

Unexpected exception loading stream
java.lang.IllegalStateException: Top bit not zero: -1195853062
at com.google.android.exoplayer.util.ParsableByteArray.readUnsignedIntToInt(ParsableByteArray.java:240)
at com.google.android.exoplayer.extractor.mp4.Mp4Extractor.readSample(Mp4Extractor.java:331)
at com.google.android.exoplayer.extractor.mp4.Mp4Extractor.read(Mp4Extractor.java:122)
at com.google.android.exoplayer.extractor.ExtractorSampleSource$ExtractingLoadable.load(ExtractorSampleSource.java:745)
at com.google.android.exoplayer.upstream.Loader$LoadTask.run(Loader.java:209)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:423)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)


the encrypted video is generated in this way:

Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec("01234567890abcde".getBytes(), "AES");
IvParameterSpec ivSpec = new IvParameterSpec("fedcba9876543210".getBytes());
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);

outputStream = new CipherOutputStream(output_stream, cipher);

Then the outputStream is saved into a File.


Eventually I found the solution.

I used a no-padding for the encryption algorithm, in this way:

cipher = Cipher.getInstance("AES/CTR/NoPadding", "BC");

so that the size of the encrypted file and the clear file size remain the same. So now I created the stream:

cipherInputStream = new CipherInputStream(inputStream, cipher) {
    public int available() throws IOException {
         return in.available();

This is because the Java documentation says about ChiperInputStream.available() that

This method should be overriden

and actually I think is it more like a MUST, because the values retrieved from that method are often really strange.

And that is it! Now it works perfectly.