我在Android中使用ExoPlayer,而我正在尝试重现本地存储的加密视频。
ExoPlayer的模块化允许创建可以在ExoPlayer中注入的自定义组件,这似乎就是这种情况。事实上,经过一些研究后,我意识到,为了实现这项任务,我可以创建一个自定义数据源并覆盖open()
,read()
和close()
。
我还找到了this solution,但实际上这里整个文件只需一步解密并存储在一个清晰的输入流中。在许多情况下这可能是好的。但是,如果我需要重现大文件怎么办?
所以问题是:如何在ExoPlayer中重现加密视频,解密内容" on-fly" (没有解密整个文件)?这可能吗?
我尝试创建一个具有open()方法的自定义DataSource:
@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保存到文件中。
答案 0 :(得分:3)
最终我找到了解决方案。
我使用无填充加密算法,这样:
cipher = Cipher.getInstance("AES/CTR/NoPadding", "BC");
以便加密文件的大小和清除文件大小保持不变。所以现在我创建了流:
cipherInputStream = new CipherInputStream(inputStream, cipher) {
@Override
public int available() throws IOException {
return in.available();
}
};
这是因为Java文档说明了ChiperInputStream.available()
实际上我认为它更像是必须的,因为从该方法中检索的值通常很奇怪。
就是这样!现在它完美无缺。
答案 1 :(得分:2)
我不相信具有开放/读取/关闭功能的自定义DataSource可以满足您的需求。对于“即时”解密(对于大文件而言不仅有价值),您必须设计流式架构。
已有与您类似的帖子。要找到它们,不要寻找'exoplayer',而是'videoview'或'mediaplayer'。答案应该是兼容的。
答案 2 :(得分:1)
检查您的代理,给定以下配置。
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有一个方法&#39; filterHdContent&#39;如果设备不是1级(假设在这里它的L3),这总是返回true。这会导致玩家在解析它时忽略mpd中的HD AdaptionSet。
如果您想播放HD,可以将filterHdContent设置为始终返回false,但内容所有者通常要求使用L1 Widevine实现高清内容。
点击此链接了解更多https://github.com/google/ExoPlayer/issues/1116 https://github.com/google/ExoPlayer/issues/1523
答案 3 :(得分:1)
示例如何播放加密的音频文件,希望对您有所帮助。 我在这里使用Kotlin
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()
}
}
}
在上面的函数中,您需要获取用于加密的密码并初始化:类似这样的
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)
}
答案 4 :(得分:0)
这个问题使我无法梳理头发,所以我终于屈服并实现了AES / CBC的流密码,可让您跳过。从理论上讲,CBC允许随机读取,您需要使用前一个块的密文作为初始化矢量来初始化该密码,然后向前读取直到您需要的位置。完整实施的示例项目here.以下是关键类:
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)
}
}