question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

problem of ExoPlayer with more than locally 2 GB encrypted files

See original GitHub issue

Hi, At first my deepest thank goes to your team. There is a problem while using ExoPlayer for playing encrypted files. we can play less than 2GB encrypted files but there is a problem for more than 2GB encrypted files and we couldn’t play these files. Is there any solution to solve this problem?

note:

  • we using custom data source! code is at the bottom.
  • we encrypt files ate users’ devices (locally)
  • we do not change format file in an encryption process
  • we secretKey and IvParameterSpec size is 16 byte
  • we have no change in size of files before or after of encryption (for example 2.54GB file not change size in before or after of encryption)

error :

Source error.
com.google.android.exoplayer2.source.UnrecognizedInputFormatException: None of the available extractors (MatroskaExtractor, FragmentedMp4Extractor, Mp4Extractor, Mp3Extractor, AdtsExtractor, Ac3Extractor, TsExtractor, FlvExtractor, OggExtractor, PsExtractor, WavExtractor) could read the stream.

encrypt info :

    public static final String AES_ALGORITHM = "AES";
    public static final String AES_TRANSFORMATION = "AES/CTR/NoPadding";

encrypt code :

            SecretKeySpec secretKey = new SecretKeySpec(keys, AES_ALGORITHM);
            Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION);
            cipher.init(cipherMode, secretKey, iv);

            FileInputStream inputStream = new FileInputStream(inputFile);
            byte[] inputBytes = new byte[1024*1024];

            System.out.println("Start");
            int bytesRead;
            FileOutputStream fileOutputStream = new FileOutputStream(outputFile);

            CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, 
            cipher);
            while ((bytesRead = inputStream.read(inputBytes, 0, bufferSize)) != -1) {
                cipherOutputStream.write(inputBytes , 0  , bytesRead);
            }
            inputStream.close();
            cipherOutputStream.close();
            if (inputFile.exists()) {
                inputFile.delete();
            }
           System.out.println("END");

player :

                   DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
                    dataSourceFactory = new EncryptedFileDataSourceFactory(cipher, key, 
                     iv, bandwidthMeter);
                    ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();
                    videoSource = new ProgressiveMediaSource.Factory(dataSourceFactory , 
                    extractorsFactory).createMediaSource(uri);

EncryptedFileDataSourceFactory :

public class EncryptedFileDataSourceFactory implements DataSource.Factory {

    private Cipher mCipher;
    private SecretKeySpec mSecretKeySpec;
    private IvParameterSpec mIvParameterSpec;
    private TransferListener mTransferListener;

    public EncryptedFileDataSourceFactory(Cipher cipher, SecretKeySpec secretKeySpec, IvParameterSpec ivParameterSpec , TransferListener listener) {
        this.mCipher = cipher;
        this.mSecretKeySpec = secretKeySpec;
        this.mIvParameterSpec = ivParameterSpec;
        this.mTransferListener = listener;
    }

    @Override
    public EncryptedFileDataSource createDataSource() {
        return new EncryptedFileDataSource(mCipher , mSecretKeySpec , mIvParameterSpec , mTransferListener);
    }

}

EncryptedFileDataSource :

public final class EncryptedFileDataSource implements DataSource {

    private final TransferListener mTransferListener;
    private StreamingCipherInputStream mInputStream;
    private Uri mUri;
    private long mBytesRemaining;
    private boolean mOpened;
    private Cipher mCipher;
    private SecretKeySpec mSecretKeySpec;
    private IvParameterSpec mIvParameterSpec;
    DataSpec dataSpec;

    public EncryptedFileDataSource(Cipher cipher, SecretKeySpec secretKeySpec, IvParameterSpec ivParameterSpec, TransferListener listener) {
        mCipher = cipher;
        mSecretKeySpec = secretKeySpec;
        mIvParameterSpec = ivParameterSpec;
        mTransferListener = listener;
    }

    @Override
    public void addTransferListener(TransferListener transferListener) {

    }

    @Override
    public long open(DataSpec dataSpec) throws EncryptedFileDataSourceException {
        // if we're open, we shouldn't need to open again, fast-fail
        if (mOpened) {
            return mBytesRemaining;
        }
        // #getUri is part of the contract...
        this.dataSpec = dataSpec;
        mUri = dataSpec.uri;
        // put all our throwable work in a single block, wrap the error in a custom Exception
        try {
            setupInputStream();
            skipToPosition(dataSpec);
            computeBytesRemaining(dataSpec);
        } catch (IOException e) {
            throw new EncryptedFileDataSourceException(e);
        }
        // if we made it this far, we're open
        mOpened = true;
        // notify
        if (mTransferListener != null) {
            mTransferListener.onTransferStart(this, dataSpec, false);
        }
        // report
        return mBytesRemaining;
    }

    private void setupInputStream() throws FileNotFoundException {
        File encryptedFile = new File(mUri.getPath());
        FileInputStream fileInputStream = new FileInputStream(encryptedFile);
        mInputStream = new StreamingCipherInputStream(fileInputStream, mCipher, mSecretKeySpec, mIvParameterSpec);
    }

    private void skipToPosition(DataSpec dataSpec) throws IOException {
        mInputStream.forceSkip(dataSpec.position);
    }

    private void computeBytesRemaining(DataSpec dataSpec) throws IOException {
        if (dataSpec.length != C.LENGTH_UNSET) {
            mBytesRemaining = dataSpec.length;
        } else {
            mBytesRemaining = mInputStream.available();
            if (mBytesRemaining == Integer.MAX_VALUE) {
                mBytesRemaining = 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
        if (readLength == 0) {
            return 0;
        } else if (mBytesRemaining == 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 = mInputStream.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 (mBytesRemaining != 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 (mBytesRemaining != C.LENGTH_UNSET) {
            mBytesRemaining -= bytesRead;
        }
        // notify
        if (mTransferListener != null) {
            mTransferListener.onBytesTransferred(this, dataSpec, false, bytesRead);
        }
        // report
        return bytesRead;
    }

    private int getBytesToRead(int bytesToRead) {
        if (mBytesRemaining == C.LENGTH_UNSET) {
            return bytesToRead;
        }
        return (int) Math.min(mBytesRemaining, bytesToRead);
    }

    @Override
    public Uri getUri() {
        return mUri;
    }

    @Override
    public void close() throws EncryptedFileDataSourceException {
        mUri = null;
        try {
            if (mInputStream != null) {
                mInputStream.close();
            }
        } catch (IOException e) {
            throw new EncryptedFileDataSourceException(e);
        } finally {
            mInputStream = null;
            if (mOpened) {
                mOpened = false;
                if (mTransferListener != null) {
                    mTransferListener.onTransferEnd(this, dataSpec, false);
                }
            }
        }
    }
}

StreamingCipherInputStream :

class StreamingCipherInputStream extends CipherInputStream {

    private static final int AES_BLOCK_SIZE = 16;

    private InputStream mUpstream;
    private Cipher mCipher;
    private SecretKeySpec mSecretKeySpec;
    private IvParameterSpec mIvParameterSpec;

    public StreamingCipherInputStream(InputStream inputStream, Cipher cipher, SecretKeySpec secretKeySpec, IvParameterSpec ivParameterSpec) {
        super(inputStream, cipher);
        mUpstream = inputStream;
        mCipher = cipher;
        mSecretKeySpec = secretKeySpec;
        mIvParameterSpec = ivParameterSpec;
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        return super.read(b, off, len);
    }

    public long forceSkip(long bytesToSkip) throws IOException {
        long skipped = mUpstream.skip(bytesToSkip);
        try {
            int skip = (int) (bytesToSkip % AES_BLOCK_SIZE);
            long blockOffset = bytesToSkip - skip;
            long numberOfBlocks = blockOffset / AES_BLOCK_SIZE;
            // from here to the next inline comment, i don't understand
            BigInteger ivForOffsetAsBigInteger = new BigInteger(1, mIvParameterSpec.getIV()).add(BigInteger.valueOf(numberOfBlocks));
            byte[] ivForOffsetByteArray = ivForOffsetAsBigInteger.toByteArray();
            IvParameterSpec computedIvParameterSpecForOffset;
            if (ivForOffsetByteArray.length < AES_BLOCK_SIZE) {
                byte[] resizedIvForOffsetByteArray = new byte[AES_BLOCK_SIZE];
                System.arraycopy(ivForOffsetByteArray, 0, resizedIvForOffsetByteArray, AES_BLOCK_SIZE - ivForOffsetByteArray.length, ivForOffsetByteArray.length);
                computedIvParameterSpecForOffset = new IvParameterSpec(resizedIvForOffsetByteArray);
            } else {
                computedIvParameterSpecForOffset = new IvParameterSpec(ivForOffsetByteArray, ivForOffsetByteArray.length - AES_BLOCK_SIZE, AES_BLOCK_SIZE);
            }
            mCipher.init(Cipher.ENCRYPT_MODE, mSecretKeySpec, computedIvParameterSpecForOffset);
            byte[] skipBuffer = new byte[skip];
            // i get that we need to update, but i don't get how we're able to take the shortcut from here to the previous comment
            mCipher.update(skipBuffer, 0, skip, skipBuffer);
            Arrays.fill(skipBuffer, (byte) 0);
        } catch (Exception e) {
            return 0;
        }
        return skipped;
    }

    // We need to return the available bytes from the upstream.
    // In this implementation we're front loading it, but it's possible the value might change during the lifetime
    // of this instance, and reference to the stream should be retained and queried for available bytes instead
    @Override
    public int available() throws IOException {
        return mUpstream.available();
    }

}

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:7 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
ojw28commented, Jan 9, 2020

Glad to hear you solved the issue!

0reactions
Navidhpcommented, Jan 8, 2020

@ojw28 Hi, At first, I appreciate your advice and it was perfectly useful for me to solve the problem. It’s worth mentioning that maximum value of files with more than 2GB was more than maximum value of integer. so it was the reason of the problem in our component.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Having trouble playing encrypted video files in ExoPlayer ...
I am attempting to playback video files, via ExoPlayer, that I have encrypted within my app, but anytime I try to playback a...
Read more >
RELEASENOTES.md - google/ExoPlayer - Sourcegraph
MP3: Fix issue parsing the XING headers belonging to files larger than 2GB. ([#7337](https://github.com/google/ExoPlayer/issues/7337)).
Read more >
RELEASENOTES.md · master · Lahlouh, Ishak / RFC_Player
MP3: Fix issue parsing the XING headers belonging to files larger than 2GB (#7337). MPEG-TS: Fix issue where SEI NAL units were incorrectly ......
Read more >
App resources overview - Android Developers
Table 1. Resource directories supported inside project res/ directory. Directory, Resource Type. animator/, XML files that ...
Read more >
Amazon Kinesis Video Streams FAQs
The video track of each fragment must contain codec private data in the Advanced Video Coding (AVC) for H.264 format and HEVC for...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found