Androidで ExoPlayer を使用していて、ローカルに保存されている暗号化されたビデオを再生しようとしています。
ExoPlayerのモジュール性により、ExoPlayerに挿入できるカスタムコンポーネントを作成できます。これは事実のようです。実際、いくつかの調査の結果、そのタスクを達成するために、カスタムデータソースを作成し、open()
、read()
、およびclose()
をオーバーライドできることに気付きました。
このソリューション も見つかりましたが、実際には、ファイル全体が1つのステップで復号化され、クリアな入力ストリームに保存されます。これは多くの状況で良いことがあります。しかし、大きなファイルを複製する必要がある場合はどうなりますか?
したがって、問題は、暗号化されたビデオを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がファイルに保存されます。
最終的に私は解決策を見つけました。
このように、暗号化アルゴリズムにパディングなしを使用しました。
_cipher = Cipher.getInstance("AES/CTR/NoPadding", "BC");
_
暗号化されたファイルのサイズとクリアファイルのサイズが同じままになるようにします。だから今私はストリームを作成しました:
_cipherInputStream = new CipherInputStream(inputStream, cipher) {
@Override
public int available() throws IOException {
return in.available();
}
};
_
これは、JavaドキュメントにChiperInputStream.available()
について記載されているためです。
実際、そのメソッドから取得された値は非常に奇妙なことが多いため、MUSTのようなものだと思います。
そしてそれだけです!今では完全に機能します。
オープン/読み取り/クローズを備えたカスタムデータソースがあなたのニーズに対する解決策であるとは思いません。 「オンザフライ」復号化(大きなファイルだけでなく価値がある)の場合、ストリーミングアーキテクチャを設計する必要があります。
あなたと同じような投稿がすでにあります。それらを見つけるには、「exoplayer」ではなく、「videoview」または「mediaplayer」を探してください。答えは互換性があるはずです。
暗号化されたオーディオファイルを再生する方法の例。これが誰かに役立つことを願っています。ここでは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()
}
}
}
上記の関数では、暗号化に使用された暗号を取得する必要があります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
}
次のステップは、以前に実装したDataSource
のDataSource.Factory
を作成することです
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)
}
次の構成を前提として、プロキシを確認してください。
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でない場合は常にtrueを返します(ここではL3であると想定しています)。これにより、プレーヤーは、解析中にmpdのHDAdaptionSetを無視します。
HDを再生する場合は、filterHdContentを常にfalseを返すように設定できますが、コンテンツ所有者は通常、HDコンテンツにL1Widevine実装を要求します。
詳細については、このリンクを確認してください https://github.com/google/ExoPlayer/issues/1116https://github.com/google/ExoPlayer/issues/152