Reproducing encrypted video using ExoPlayer

2019-02-01 00:56发布

问题:

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 open(), read() and close().

I have also found 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:

@Override
    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) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        opened = true;
        if (listener != null) {
            listener.onTransferStart();
        }

        return bytesRemaining;
    }

And this is the read() method:

@Override
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) {
                e.printStackTrace();
            }

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

            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)

EDIT:

the encrypted video is generated in this way:

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

outputStream = new CipherOutputStream(output_stream, cipher);

Then the outputStream is saved into a File.

回答1:

I don't believe a custom DataSource, with open/read/close, is a solution to your need. For an 'on-the-fly' decryption (valuable for big files but not only), you must design a streaming architecture.

There are already posts similar to yours. To find them, don't look for 'exoplayer', but 'videoview' or 'mediaplayer' instead. The answers should be compatible.

For instance, Playing encrypted video files using VideoView



回答2:

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) {
    @Override
    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.



回答3:

check your proxy, given the following configuration.

ALLOWED_TRACK_TYPES = "SD_HD"
content_key_specs = [{ "track_type": "HD",
                       "security_level": 1,
                       "required_output_protection": {"hdcp": "HDCP_NONE" }
                     },
                     { "track_type": "SD",
                       "security_level": 1,
                       "required_output_protection": {"cgms_flags": "COPY_FREE" }
                     },
                     { "track_type": "AUDIO"}]
request = json.dumps({"payload": payload,
                      "content_id": content_id,
                      "provider": self.provider,
                      "allowed_track_types": ALLOWED_TRACK_TYPES,
                      "use_policy_overrides_exclusively": True,
                      "policy_overrides": policy_overrides,
                      "content_key_specs": content_key_specs
                     ?

In the ExoPlayer demo app - DashRenderBuilder.java has a method 'filterHdContent' this always returns true if device is not level 1 (Assuming here it's L3). This causes the player to disregard the HD AdaptionSet in the mpd whilst parsing it.

You can set the filterHdContent to always return false if you want to play HD, however it is typical of content owners to require a L1 Widevine implementation for HD content.

check this link for more https://github.com/google/ExoPlayer/issues/1116 https://github.com/google/ExoPlayer/issues/1523