/UPDATE: the original version of this post is fine for small video files, as it decrypts the entire file and reads the clear stream into memory. For large files though this is probably not feasible; a work-around is provided. The original post is included at the end.

Reference https://github.com/google/ExoPlayer/issues/2467

Playing encrypted video in ExoPlayer turns out to be pretty much the same as playing encrypted video with MediaPlayer. For the latter, you’re limited in how you can provide data to the player, so most people use a local SocketServer to send the stream across HTTP. This is actually must easier than it sounds, and the basic techniques presented here http://stackoverflow.com/a/5432091/6585616 are roughly enough to get it done (IIRC, the script needed some minor tweaking, but all credit to the original author).

I presume this is also the technique used by the commercial streaming server “libeasy” or “libmedia” (the second is a little confusing as that is also the name of an Android internal package used for this video decoding (decoding, not decrypting)): http://libeasy.alwaysdata.net/network/#server

This was the second approach I used to provide streaming encrypted content to ExoPlayer, and it works fine (with both MediaPlayer and ExoPlayer).

However, with ExoPlayer, we’re able to provide a direct datasource, so the extra server layer was bothersome and seemed like it had to be unnecessary. From reading source documents, you’d think that providing a CipherInputStream around your encrypted file would work – and in fact, it does “work” on some ciphers (e.g., RC4), but skipping (“scrubbing”) was really slow to perform (I think because it had to decrypt from the first byte, but am not sure of that) – I’d see as much as 5, 10, or 20 seconds of lag when skipping ahead.

According to http://libeasy.alwaysdata.net/network/#server, it seems as if some encryption algorithms provided random access, notable AES/CTR/NoPadding (as well as some others, possible CBC and CFB). So I encrypted a file with AES/CTR/NoPadding, then try to read it with ExoPlayer using a DataSource that was modeled on the provided AssetDataSource and FileDataSource, but wrapped the FileInputSteam with a CipherInputStream; the same basic approach that did with with RC4 but showed the slow seeking. This did allow playback, but would fail with “Top bit not zero” when seeking – rather than running slowly (as it did with RC4), the app would crash.

Following the chain of errors, it turns out that CipherInputStream has methods that fail when used in this fashion: available() and skip(). The docs state available should be overwritten, so this was less of a surprise, but finding the issue with skip() was a matter of trial and error.

For available(), just return the bytes available from the wrapped stream, so you can either front load it or just pass through:

CipherInputStream.skip would often return 0; I’m not sure if this is a timing issue (since sometimes allowing it to “warm up” for a few seconds would result in a successful skip), but returning 0 would cause a failure in most of the existing DataSource implementations (most DataSources test if the skip() call actually skipped enough bytes to reach the position specified by the player (dataSpec.position), and either report EOF or thrown an EOF error.

The work around is to force it – basically call skip(n) normally, but if you get a 0 return, call read() for as many bytes as needed. It might look like this:

You’ll find that this forceSkip method is doing essentially the same work as the skipFully method found in the streaming server example.

Here’s a working DataSource implementation. The dataSpec should have it’s Uri point to an existing, encrypted file on the local file system. The Cipher instance passed in should be a Cipher in decryption mode with the same key and algorithm as was used to encrypt the file. This will not work with padded algorithms, as the seek position will be different between the clear and encrypted file.

// FROM HERE DOWN CONSIDER DEPRECATED
Ran into this requirement recently, and despite a couple open questions on Stackoverflow and ExoPlayer’s issues page, there wasn’t much guidance.

Turns out this is relatively straightforward if you’re not streaming the video (e.g., you’re playing it from a local file).

The DataSource interface has 3 methods: open, close, and read. When the renderer is trying to play the file, it invokes read to get the bytes it needs. Open and close are basically setup and teardown – open happens when a source is first queried, and close happens when ExoPlayer thinks it no longer needs to read from the source.

If we look at most existing implementations:

https://github.com/google/ExoPlayer/blob/master/library/src/main/java/com/google/android/exoplayer/upstream/AssetDataSource.java
https://github.com/google/ExoPlayer/blob/master/library/src/main/java/com/google/android/exoplayer/upstream/FileDataSource.java
https://github.com/google/ExoPlayer/blob/master/library/src/main/java/com/google/android/exoplayer/upstream/ContentDataSource.java

We can see that in most cases, they’re just grabbing an InputStream on open, and reading from that in read. So with an encrypted file, simply decrypt it on the line before and convert that output (probably a byte array) to a Stream. Here’s an example based of ContentDataSource: