Reproducing encrypted video using ExoPlayer

前端 未结 5 1911
心在旅途
心在旅途 2020-12-07 22:51

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 tha

相关标签:
5条回答
  • 2020-12-07 23:32

    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

    0 讨论(0)
  • 2020-12-07 23:40

    This problem had me tearing my hair out, so I finally caved and implemented a streaming cipher for AES/CBC that lets you skip ahead. CBC theoretically allows for random reads, you need to initialize the cipher with the previous block's ciphertext as the initialization vector and then read ahead until the spot you needed. Sample project with full implementation here. Here's the key classes:

    import android.net.Uri
    import android.util.Log
    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 ar.cryptotest.exoplayer2.MainActivity.Companion.AES_TRANSFORMATION
    import java.io.EOFException
    import java.io.File
    import java.io.IOException
    import java.io.InputStream
    import java.lang.RuntimeException
    import javax.crypto.Cipher
    import javax.crypto.CipherInputStream
    import javax.crypto.spec.IvParameterSpec
    import javax.crypto.spec.SecretKeySpec
    
    const val TAG = "ENCRYPTING PROCESS"
    
    class BlockCipherEncryptedDataSource(
        private val secretKeySpec: SecretKeySpec,
        private val uri: Uri,
        cipherTransformation: String = "AES/CBC/PKCS7Padding"
    ) : DataSource {
        private val cipher: Cipher = Cipher.getInstance(cipherTransformation)
        private lateinit var streamingCipherInputStream: StreamingCipherInputStream
        private var bytesRemaining: Long = 0
        private var isOpen = false
        private val transferListeners = mutableListOf<TransferListener>()
        private var dataSpec: DataSpec? = null
    
        @Throws(EncryptedFileDataSourceException::class)
        override fun open(dataSpec: DataSpec): Long {
            this.dataSpec = dataSpec
    
            if (isOpen) return bytesRemaining
    
            try {
                setupInputStream()
                streamingCipherInputStream.forceSkip(dataSpec.position)
                computeBytesRemaining(dataSpec)
            } catch (e: IOException) {
                throw EncryptedFileDataSourceException(e)
            }
    
            isOpen = true
            transferListeners.forEach { it.onTransferStart(this, dataSpec, false) }
    
            return C.LENGTH_UNSET.toLong()
        }
    
        private fun setupInputStream() {
            val path = uri.path ?: throw RuntimeException("Tried decrypting uri with no path: $uri")
            val encryptedFileStream = File(path).inputStream()
            val initializationVector = ByteArray(cipher.blockSize)
            encryptedFileStream.read(initializationVector)
            streamingCipherInputStream =
                StreamingCipherInputStream(
                    encryptedFileStream,
                    cipher,
                    IvParameterSpec(initializationVector),
                    secretKeySpec
                )
        }
    
        @Throws(IOException::class)
        private fun computeBytesRemaining(dataSpec: DataSpec) {
            if (dataSpec.length != C.LENGTH_UNSET.toLong()) {
                bytesRemaining = dataSpec.length
                return
            }
    
            if (bytesRemaining == Int.MAX_VALUE.toLong()) {
                bytesRemaining = C.LENGTH_UNSET.toLong()
                return
            }
    
            bytesRemaining = streamingCipherInputStream.available().toLong()
        }
    
        @Throws(EncryptedFileDataSourceException::class)
        override fun read(buffer: ByteArray, offset: Int, readLength: Int): Int {
            if (bytesRemaining == 0L) {
                Log.e(TAG, "End - No bytes remaining")
                return C.RESULT_END_OF_INPUT
            }
    
            val bytesRead = try {
                streamingCipherInputStream.read(buffer, offset, readLength)
            } catch (e: IOException) {
                throw EncryptedFileDataSourceException(e)
            }
    
            // Reading -1 means an error occurred
            if (bytesRead < 0) {
                if (bytesRemaining != C.LENGTH_UNSET.toLong())
                    throw EncryptedFileDataSourceException(EOFException())
                return C.RESULT_END_OF_INPUT
            }
    
            // Bytes remaining will be unset if file is too large for an int
            if (bytesRemaining != C.LENGTH_UNSET.toLong())
                bytesRemaining -= bytesRead.toLong()
    
            dataSpec?.let { nonNullDataSpec ->
                transferListeners.forEach {
                    it.onBytesTransferred(this, nonNullDataSpec, false, bytesRead)
                }
            }
            return bytesRead
        }
    
        override fun addTransferListener(transferListener: TransferListener) {
            transferListeners.add(transferListener)
        }
    
        override fun getUri(): Uri = uri
    
        @Throws(EncryptedFileDataSourceException::class)
        override fun close() {
            Log.e(TAG, "Closing stream")
            try {
                streamingCipherInputStream.close()
            } catch (e: IOException) {
                throw EncryptedFileDataSourceException(e)
            } finally {
                if (isOpen) {
                    isOpen = false
                    dataSpec?.let { nonNullDataSpec ->
                        transferListeners.forEach { it.onTransferEnd(this, nonNullDataSpec, false) }
                    }
                }
            }
        }
    
        class EncryptedFileDataSourceException(cause: IOException?) : IOException(cause)
        class StreamingCipherInputStream(
            private val sourceStream: InputStream,
            private var cipher: Cipher,
            private val initialIvParameterSpec: IvParameterSpec,
            private val secretKeySpec: SecretKeySpec
        ) : CipherInputStream(
            sourceStream, cipher
        ) {
            private val cipherBlockSize: Int = cipher.blockSize
    
            @Throws(IOException::class)
            override fun read(b: ByteArray, off: Int, len: Int): Int = super.read(b, off, len)
    
            fun forceSkip(bytesToSkip: Long) {
                val bytesSinceStartOfCurrentBlock = bytesToSkip % cipherBlockSize
    
                val bytesUntilPreviousBlockStart =
                    bytesToSkip - bytesSinceStartOfCurrentBlock - cipherBlockSize
    
                try {
                    if (bytesUntilPreviousBlockStart <= 0) {
                        cipher.init(
                            Cipher.DECRYPT_MODE,
                            secretKeySpec,
                            initialIvParameterSpec
                        )
                        return
                    }
    
                    var skipped = sourceStream.skip(bytesUntilPreviousBlockStart)
                    while (skipped < bytesUntilPreviousBlockStart) {
                        sourceStream.read()
                        skipped++
                    }
    
                    val previousEncryptedBlock = ByteArray(cipherBlockSize)
    
                    sourceStream.read(previousEncryptedBlock)
    
                    cipher.init(
                        Cipher.DECRYPT_MODE,
                        secretKeySpec,
                        IvParameterSpec(previousEncryptedBlock)
                    )
                    skip(bytesUntilPreviousBlockStart + cipherBlockSize)
    
                    val discardableByteArray = ByteArray(bytesSinceStartOfCurrentBlock.toInt())
                    read(discardableByteArray)
                } catch (e: Exception) {
                    Log.e(TAG, "Encrypted video skipping error", e)
                    throw e
                }
            }
    
            // 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
            @Throws(IOException::class)
            override fun available(): Int {
                return sourceStream.available()
            }
        }
    }
    
    class BlockCipherEncryptedDataSourceFactory(
        private val secretKeySpec: SecretKeySpec,
        private val uri: Uri,
        private val cipherTransformation: String = "AES/CBC/PKCS7Padding"
    ) : DataSource.Factory {
        override fun createDataSource(): BlockCipherEncryptedDataSource {
            return BlockCipherEncryptedDataSource(secretKeySpec, uri, cipherTransformation)
        }
    }
    
    0 讨论(0)
  • 2020-12-07 23:47

    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.

    0 讨论(0)
  • 2020-12-07 23:50

    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

    0 讨论(0)
  • 2020-12-07 23:55

    Example how to play encrypted audio file, hope this will help to someone. I'm using Kotlin here

    import android.net.Uri
    import com.google.android.exoplayer2.C
    import com.google.android.exoplayer2.upstream.DataSource
    import com.google.android.exoplayer2.upstream.DataSourceInputStream
    import com.google.android.exoplayer2.upstream.DataSpec
    import com.google.android.exoplayer2.util.Assertions
    import java.io.IOException
    import javax.crypto.CipherInputStream
    
    class EncryptedDataSource(upstream: DataSource) : DataSource {
    
        private var upstream: DataSource? = upstream
        private var cipherInputStream: CipherInputStream? = null
    
        override fun open(dataSpec: DataSpec?): Long {
            val cipher = getCipherInitDecrypt()
            val inputStream = DataSourceInputStream(upstream, dataSpec)
            cipherInputStream = CipherInputStream(inputStream, cipher)
            inputStream.open()
            return C.LENGTH_UNSET.toLong()
    
        }
    
        override fun read(buffer: ByteArray?, offset: Int, readLength: Int): Int {
            Assertions.checkNotNull<Any>(cipherInputStream)
            val bytesRead = cipherInputStream!!.read(buffer, offset, readLength)
            return if (bytesRead < 0) {
                C.RESULT_END_OF_INPUT
            } else bytesRead
        }
    
        override fun getUri(): Uri {
            return upstream!!.uri
        }
    
        @Throws(IOException::class)
        override fun close() {
            if (cipherInputStream != null) {
                cipherInputStream = null
                upstream!!.close()
            }
        }
    }
    

    In function above you need to get Cipher which was used for encryption and init it: smth like this

    fun getCipherInitDecrypt(): Cipher {
        val cipher = Cipher.getInstance("AES/CTR/NoPadding", "BC");
        val iv = IvParameterSpec(initVector.toByteArray(charset("UTF-8")))
        val skeySpec = SecretKeySpec(key, TYPE_RSA)
        cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv)
        return cipher
    }
    

    Next step is creating DataSource.Factory for DataSource we've implemented earlier

    import com.google.android.exoplayer2.upstream.DataSource
    
    class EncryptedFileDataSourceFactory(var dataSource: DataSource) : DataSource.Factory {
    
        override fun createDataSource(): DataSource {
            return EncryptedDataSource(dataSource)
        }
    }
    

    And last step is players initialization

        private fun prepareExoPlayerFromFileUri(uri: Uri) {
            val player = ExoPlayerFactory.newSimpleInstance(
                        DefaultRenderersFactory(this),
                        DefaultTrackSelector(),
                        DefaultLoadControl())
    
            val playerView = findViewById<PlayerView>(R.id.player_view)
            playerView.player = player
    
            val dsf = DefaultDataSourceFactory(this, Util.getUserAgent(this, "ExoPlayerInfo"))
            //This line do the thing
            val mediaSource = ExtractorMediaSource.Factory(EncryptedFileDataSourceFactory(dsf.createDataSource())).createMediaSource(uri)
            player.prepare(mediaSource)
        }
    
    0 讨论(0)
提交回复
热议问题