android encryption android-mediaplayer exoplayer

android - Reproducción de video encriptado usando ExoPlayer



encryption android-mediaplayer (4)

Estoy usando ExoPlayer , en Android, y estoy tratando de reproducir un video encriptado almacenado localmente.

La modularidad de ExoPlayer permite crear componentes personalizados que se pueden inyectar en ExoPlayer, y este parece ser el caso. De hecho, después de algunas investigaciones, me di cuenta de que para lograr esa tarea podría crear un DataSource personalizado y anular open() , read() y close() .

También encontré esta solución , pero en realidad aquí el archivo completo se descifra en un solo paso y se almacena en una clara entrada de datos. Esto puede ser bueno en muchas situaciones. ¿Pero qué pasa si necesito reproducir un archivo grande?

Entonces, la pregunta es: ¿cómo puedo reproducir video encriptado en ExoPlayer, descifrando el contenido "sobre la marcha" (sin descifrar todo el archivo)? ¿Es esto posible?

Intenté crear un DataSource personalizado que tiene el método 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; }

Y este es el método de lectura ():

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

Si en lugar de un archivo codificado paso un archivo claro y simplemente elimino la parte CipherInputStream, entonces funciona bien, en lugar de un archivo cifrado obtengo este 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)

EDITAR :

El video encriptado se genera de esta manera:

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

Entonces el outputStream se guarda en un archivo.


Ejemplo de cómo reproducir un archivo de audio encriptado, espero que esto ayude a alguien. Estoy usando Kotlin aquí

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

En la función anterior, necesita obtener el cifrado que se usó para el cifrado e iniciarlo : algo así como

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 }

El siguiente paso es crear DataSource.Factory para DataSource que hemos implementado anteriormente

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

Y el último paso es la inicialización de los jugadores.

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


Finalmente encontré la solución.

Usé un no-relleno para el algoritmo de encriptación, de esta manera:

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

de modo que el tamaño del archivo cifrado y el tamaño del archivo claro permanezcan iguales. Así que ahora he creado la secuencia:

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

Esto se debe a que la documentación de Java dice acerca de ChiperInputStream.available() que

Este método debe ser anulado

y en realidad creo que es más como un MUST, porque los valores recuperados de ese método a menudo son realmente extraños.

¡Y eso es todo! Ahora funciona perfectamente.


No creo que un DataSource personalizado, con abrir / leer / cerrar, sea una solución a su necesidad. Para un descifrado "sobre la marcha" (valioso para archivos grandes, pero no solo), debe diseñar una arquitectura de transmisión.

Ya hay publicaciones similares a la tuya. Para encontrarlos, no busque "exoplayer", sino "videoview" o "mediaplayer" en su lugar. Las respuestas deben ser compatibles.

Por ejemplo, Reproducción de archivos de video encriptados usando VideoView


Revise su proxy, dada la siguiente configuración.

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 ?

En la aplicación de demostración de ExoPlayer, DashRenderBuilder.java tiene un método ''filterHdContent'', esto siempre devuelve verdadero si el dispositivo no está en el nivel 1 (Suponiendo que aquí es L3). Esto hace que el reproductor ignore el HD AdaptionSet en el mpd mientras lo analiza.

Puede configurar filterHdContent para que siempre devuelva falso si desea reproducir HD, sin embargo, es típico de los propietarios de contenido que requieran una implementación de L1 Widevine para contenido HD.

consulte este enlace para obtener más https://github.com/google/ExoPlayer/issues/1116 https://github.com/google/ExoPlayer/issues/1523