Воспроизведение зашифрованного видео с помощью ExoPlayer

Я использую ExoPlayer в Android и пытаюсь воспроизвести зашифрованное видео, хранящееся локально.

Модульность ExoPlayer позволяет создавать пользовательские компоненты, которые можно вводить в ExoPlayer, и это, кажется, так. Действительно, после некоторых исследований я понял, что для решения этой задачи я могу создать собственный источник данных и переопределить open(), read() и close().

Я также нашел это решение, но на самом деле здесь весь файл расшифровывается за один шаг и сохраняется в чистом входном потоке. Это может быть хорошо во многих ситуациях. Но что, если мне нужно воспроизвести большой файл?

Итак, вопрос: как я могу воспроизвести зашифрованное видео в ExoPlayer, расшифровывая контент «на лету» (без расшифровки всего файла)? Возможно ли это?

Я попытался создать собственный источник данных с методом open ():

@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;
    }

А это метод read ():

@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;
        }
    }

Если вместо закодированного файла я передаю чистый файл и просто удаляю часть CipherInputStream, тогда он работает нормально, вместо этого с зашифрованным файлом я получаю эту ошибку:

    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)

ИЗМЕНИТЬ:

зашифрованное видео создается таким образом:

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);

Затем outputStream сохраняется в файл.


person GVillani82    schedule 02.08.2016    source источник
comment
У вас есть зашифрованное видео? С каким шифрованием он зашифрован?   -  person sergej shafarenka    schedule 04.08.2016
comment
Я отредактировал свой вопрос   -  person GVillani82    schedule 04.08.2016
comment
Я бы попробовал обернуть CipherInputStream DataInputStream и использовать readFully() вместо read().   -  person sergej shafarenka    schedule 05.08.2016
comment
С DataInputStream у меня такая же проблема   -  person GVillani82    schedule 05.08.2016
comment
C.LENGTH_UNBOUNDED это не решено .. устарело или какую версию exoplayer вы использовали?   -  person Emre Kilinc Arslan    schedule 07.04.2021


Ответы (5)


Пример того, как воспроизводить зашифрованный аудиофайл, надеюсь, это кому-то поможет. Я использую здесь Котлин

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()
        }
    }
}

В приведенной выше функции вам нужно получить Cipher, который использовался для шифрования, и запустить его: что-то вроде этого

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
}

Следующим шагом будет создание DataSource.Factory для DataSource, которое мы реализовали ранее

import com.google.android.exoplayer2.upstream.DataSource

class EncryptedFileDataSourceFactory(var dataSource: DataSource) : DataSource.Factory {

    override fun createDataSource(): DataSource {
        return EncryptedDataSource(dataSource)
    }
}

И последний шаг - инициализация игроков.

    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)
    }
person Bender    schedule 15.02.2019
comment
в какой версии exoplayer это работает? - person Emre Kilinc Arslan; 11.04.2021

В конце концов я нашел решение.

Я использовал отсутствие заполнения для алгоритма шифрования следующим образом:

cipher = Cipher.getInstance("AES/CTR/NoPadding", "BC");

так что размер зашифрованного файла и размер незашифрованного файла остаются неизменными. Итак, теперь я создал поток:

cipherInputStream = new CipherInputStream(inputStream, cipher) {
    @Override
    public int available() throws IOException {
         return in.available();
    }
};

Это потому, что в документации Java говорится о ChiperInputStream.available(), что

Этот метод должен быть отвергнутым

и на самом деле я думаю, что это больше похоже на ОБЯЗАТЕЛЬНО, потому что значения, полученные из этого метода, часто действительно странные.

Вот и все! Теперь работает отлично.

person GVillani82    schedule 12.08.2016
comment
Можете ли вы обновить ссылку для кода ... Я застрял в той же проблеме, и указанная выше ссылка теперь мертва. - person SRB Bans; 13.11.2019
comment
Прежде всего, спасибо за ответ .. я говорю о I have also found this solution, but actually here the entire file is decrypted в вашем вопросе .. не в этом ответе. - person SRB Bans; 13.11.2019
comment
Я нашел демонстрационный проект, но он не работает с AES/CBC/PKCS5Padding .. я думаю .. - person SRB Bans; 13.11.2019
comment
Извините, я не знаю, где найти правильную ссылку. Раньше это работало: через 3 с половиной года я не могу вспомнить эту ссылку. - person GVillani82; 13.11.2019
comment
Хорошо .. :( ... у тебя есть рабочий демонстрационный код или что-то в этом роде ... можешь отправить мне на [email protected] .... я буду тебе очень благодарен. :) - person SRB Bans; 13.11.2019

Эта проблема заставила меня вырвать себе волосы, поэтому я, наконец, прогнал и внедрил потоковый шифр для AES / CBC, который позволяет вам пропустить вперед. CBC теоретически допускает случайное чтение, вам нужно инициализировать шифр с зашифрованным текстом предыдущего блока в качестве вектора инициализации, а затем читать вперед до нужного места. Пример проекта с полной реализацией здесь. Вот основные классы:

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)
    }
}
person JaviCasa    schedule 12.11.2020

проверьте свой прокси, учитывая следующую конфигурацию.

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
                     ?

В демонстрационном приложении ExoPlayer - DashRenderBuilder.java имеет метод filterHdContent, который всегда возвращает истину, если устройство не уровня 1 (предполагается, что здесь это L3). Это заставляет игрока игнорировать HD AdaptionSet в mpd во время его анализа.

Вы можете настроить filterHdContent так, чтобы он всегда возвращал false, если вы хотите воспроизводить HD, однако владельцы контента обычно требуют реализации L1 Widevine для HD-контента.

проверьте эту ссылку, чтобы узнать больше https://github.com/google/ExoPlayer/issues/1116 https://github.com/google/ExoPlayer/issues/1523

person youngdero    schedule 04.08.2016
comment
Я действительно не понял твоего ответа. Кажется, это не связано с моей проблемой, так как это была просто проблема с правильным созданием DataSource. - person GVillani82; 12.08.2016

Я не верю, что настраиваемый источник данных с открытием / чтением / закрытием - это решение вашей потребности. Для дешифрования «на лету» (важно для больших файлов, но не только) вы должны разработать потоковую архитектуру.

Уже есть посты, похожие на ваш. Чтобы найти их, ищите не exoplayer, а videoview или mediaplayer. Ответы должны быть совместимы.

Например, Воспроизведение зашифрованных видеофайлов с помощью VideoView

person libeasy    schedule 08.08.2016
comment
Конечно, вы можете использовать DataSource. ExoPlayer разработан по модульному принципу, поэтому DataSource может быть любым, каким вы хотите. Вы можете создать свою собственную реализацию для получения данных из сети, от поставщика контента, из ресурса, из файла (зашифрованного или нет) и т. Д. Так что это прекрасно. Взгляните на мой ответ для подробностей. - person GVillani82; 12.08.2016
comment
используя кастомную платную библиотеку .. правда? .. это какая-то акция? - person SRB Bans; 12.11.2019