-
-
Save rameshvoltella/2966a132916311153b9a5ddd4c799885 to your computer and use it in GitHub Desktop.
ExoPlayer playing AES/GCM/NoPadding encrypted video
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 com.sharifin.lang.storage; | |
import android.net.Uri ; | |
import com.google.android.exoplayer2.C ; | |
import com.google.android.exoplayer2.upstream.DataSource ; | |
import com.google.android.exoplayer2.upstream.DataSpec ; | |
import com.google.android.exoplayer2.upstream.TransferListener ; | |
import java.io.EOFException ; | |
import java.io.File ; | |
import java.io.FileInputStream ; | |
import java.io.FileNotFoundException ; | |
import java.io.IOException ; | |
import java.io.InputStream ; | |
import android.util.Log; | |
import javax.crypto.Cipher ; | |
import javax.crypto.CipherInputStream ; | |
/** | |
* Created by Parsoa on 5/2/17. | |
*/ | |
public class AESDataSource implements DataSource { | |
private static final String DEBUG_TAG = "AESDataSource" ; | |
private StreamingCipherInputStream cipherInputStream ; | |
private Uri uri ; | |
private long bytesRemaining ; | |
private boolean opened ; | |
public Cipher cipher ; | |
public AESDataSource(Cipher cipher) { | |
this.cipher = cipher ; | |
} | |
@Override | |
public long open(DataSpec dataSpec) throws IOException { | |
// if we're open, we shouldn't need to open again, fast-fail | |
if (opened) { | |
return bytesRemaining ; | |
} | |
Log.e(DEBUG_TAG, "open") ; | |
uri = dataSpec.uri; | |
try { | |
setupInputStream() ; | |
Log.e(DEBUG_TAG, "Dataspec: " + dataSpec.position + " , " + dataSpec.length + " , " + cipherInputStream.available()) ; | |
cipherInputStream.forceSkip(dataSpec.position) ; | |
computeBytesRemaining(dataSpec) ; | |
} catch (IOException e) { | |
throw new EncryptedFileDataSourceException(e) ; | |
} | |
opened = true ; | |
return bytesRemaining ; | |
} | |
private void setupInputStream() throws FileNotFoundException { | |
File encryptedFile = new File(uri.getPath()); | |
FileInputStream fileInputStream = new FileInputStream(encryptedFile); | |
cipherInputStream = new StreamingCipherInputStream(fileInputStream, cipher); | |
} | |
private void computeBytesRemaining(DataSpec dataSpec) throws IOException { | |
if (dataSpec.length != C.LENGTH_UNSET) { | |
bytesRemaining = dataSpec.length; | |
} else { | |
bytesRemaining = cipherInputStream.available(); | |
if (bytesRemaining == Integer.MAX_VALUE) { | |
bytesRemaining = C.LENGTH_UNSET; | |
} | |
} | |
} | |
@Override | |
public int read(byte[] buffer, int offset, int readLength) throws EncryptedFileDataSourceException { | |
// fast-fail if there's 0 quantity requested or we think we've already processed everything | |
Log.e(DEBUG_TAG, "read") ; | |
if (readLength == 0) { | |
return 0 ; | |
} else if (bytesRemaining == 0) { | |
return C.RESULT_END_OF_INPUT ; | |
} | |
// constrain the read length and try to read from the cipher input stream | |
int bytesToRead = getBytesToRead(readLength); | |
int bytesRead; | |
try { | |
bytesRead = cipherInputStream.read(buffer, offset, bytesToRead); | |
} catch (IOException e) { | |
throw new EncryptedFileDataSourceException(e); | |
} | |
// if we get a -1 that means we failed to read - we're either going to EOF error or broadcast EOF | |
if (bytesRead == -1) { | |
if (bytesRemaining != C.LENGTH_UNSET) { | |
throw new EncryptedFileDataSourceException(new EOFException()); | |
} | |
return C.RESULT_END_OF_INPUT; | |
} | |
// we can't decrement bytes remaining if it's just a flag representation (as opposed to a mutable numeric quantity) | |
if (bytesRemaining != C.LENGTH_UNSET) { | |
bytesRemaining -= bytesRead ; | |
} | |
return bytesRead ; | |
} | |
private int getBytesToRead(int bytesToRead) { | |
if (bytesRemaining == C.LENGTH_UNSET) { | |
return bytesToRead ; | |
} | |
return (int) Math.min(bytesRemaining, bytesToRead) ; | |
} | |
@Override | |
public void close() throws EncryptedFileDataSourceException { | |
uri = null; | |
try { | |
if (cipherInputStream != null) { | |
cipherInputStream.close(); | |
} | |
} catch (IOException e) { | |
throw new EncryptedFileDataSourceException(e); | |
} finally { | |
cipherInputStream = null; | |
if (opened) { | |
opened = false; | |
} | |
} | |
} | |
@Override | |
public Uri getUri() { | |
return uri ; | |
} | |
// ================================================================================================================ \\ | |
// ================================================================================================================ \\ | |
// ================================================================================================================ \\ | |
public static class StreamingCipherInputStream extends CipherInputStream { | |
private int bytesAvailable ; | |
public StreamingCipherInputStream(InputStream is, Cipher c) { | |
super(is, c); | |
try { | |
bytesAvailable = is.available(); | |
} catch (IOException e) { | |
// let it be 0 | |
} | |
} | |
// if the CipherInputStream has returns 0 from #skip, #read out enough bytes to get where we need to be | |
public long forceSkip(long bytesToSkip) throws IOException { | |
Log.e(DEBUG_TAG, "skipping " + bytesToSkip) ; | |
long processedBytes = 0; | |
while (processedBytes < bytesToSkip) { | |
Log.e(DEBUG_TAG, "processed " + processedBytes + ", skipping " + (8) + " bytes") ; | |
long bytesSkipped = skip(8) ; | |
if (bytesSkipped == 0) { | |
Log.e(DEBUG_TAG, "Couldn't skip, reading") ; | |
if (read() == -1) { | |
throw new EOFException() ; | |
} | |
bytesSkipped = 1 ; | |
} | |
processedBytes += bytesSkipped ; | |
} | |
Log.e(DEBUG_TAG, "done skipping " + bytesToSkip) ; | |
return processedBytes ; | |
} | |
@Override | |
public int available() throws IOException { | |
return bytesAvailable; | |
} | |
} | |
// ================================================================================================================ \\ | |
// ================================================================================================================ \\ | |
// ================================================================================================================ \\ | |
public static final class EncryptedFileDataSourceException extends IOException { | |
public EncryptedFileDataSourceException(IOException cause) { | |
super(cause); | |
} | |
} | |
} |
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
private void initializePlayer() { | |
if (!shouldInitialize || videoUrl == null || videoUrl.isEmpty()) { | |
// fail-fast | |
return ; | |
} | |
if (player == null) { | |
Log.e(DEBUG_TAG, "initializePlayer") ; | |
TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveVideoTrackSelection.Factory(BANDWIDTH_METER); | |
trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); | |
player = ExoPlayerFactory.newSimpleInstance(mContext, trackSelector, new DefaultLoadControl()); | |
player.addListener(this); | |
eventLogger = new EventLogger(trackSelector); | |
player.addListener(eventLogger); | |
player.setAudioDebugListener(eventLogger); | |
player.setVideoDebugListener(eventLogger); | |
player.setMetadataOutput(eventLogger); | |
simpleExoPlayerView.setPlayer(player); | |
player.setPlayWhenReady(true); | |
myPlayBackControlView.setPlayer(player); | |
simpleExoPlayerView.setControllerVisibilityListener(new PlaybackControlView.VisibilityListener() { | |
@Override | |
public void onVisibilityChange(int visibility) { | |
if (player == null) | |
return; | |
if (player.getPlayWhenReady()) { | |
btnPause.setVisibility(visibility) ; | |
} | |
} | |
}); | |
playerInitialized = true ; | |
Log.e(DEBUG_TAG, "playerInitialized!") ; | |
} | |
initializeMediaSource() ; | |
} | |
private void initializeMediaSource() { | |
mediaSource = buildLocalMediaSource(Uri.parse(videoUrl)) ; | |
continuePlaying() ; | |
} | |
private void continuePlaying() { | |
final boolean haveResumePosition = resumeWindow != C.INDEX_UNSET ; | |
if (haveResumePosition) { | |
player.seekTo(resumeWindow, resumePosition) ; | |
} | |
player.prepare(mediaSource, !haveResumePosition, false) ; | |
updateButtonVisibilities() ; | |
} | |
private MediaSource buildLocalDecryptedMediaSource() { | |
try { | |
File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), getIV()) ; | |
DataSpec dataSpec = new DataSpec(Uri.parse(file.getCanonicalPath())) ; | |
final FileDataSource fileDataSource = new FileDataSource(); | |
try { | |
fileDataSource.open(dataSpec); | |
} catch (FileDataSource.FileDataSourceException e) { | |
e.printStackTrace(); | |
} | |
DataSource.Factory factory = new DataSource.Factory() { | |
@Override | |
public DataSource createDataSource() { | |
return fileDataSource; | |
} | |
}; | |
return new ExtractorMediaSource(fileDataSource.getUri(), factory, new DefaultExtractorsFactory(), null, null); | |
} catch (IOException e) { | |
e.printStackTrace() ; | |
} | |
Log.e(DEBUG_TAG, "Decryption failed") ; | |
return null ; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment