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