Android中ExoPlayer播放AES加密视频报错求助
Android视频加密下载+ExoPlayer解密播放问题排查与解决
问题背景
在Android项目中实现视频下载时加密、后续用ExoPlayer解密播放的功能,自定义DataSource后仍出现播放失败报错,项目为Java/Kotlin混合代码。
错误信息
ExoPlayerImplInternal: Playback error com.google.android.exoplayer2.ExoPlaybackException: Source error at com.google.android.exoplayer2.ExoPlayerImplInternal.handleIoException(ExoPlayerImplInternal.java:632) at com.google.android.exoplayer2.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:602) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loopOnce(Looper.java:233) at android.os.Looper.loop(Looper.java:344) at android.os.HandlerThread.run(HandlerThread.java:67) Caused by: com.google.android.exoplayer2.source.UnrecognizedInputFormatException: None of the available extractors (FlvExtractor, FlacExtractor, WavExtractor, FragmentedMp4Extractor, Mp4Extractor, AmrExtractor, PsExtractor, OggExtractor, TsExtractor, MatroskaExtractor, AdtsExtractor, Ac3Extractor, Ac4Extractor, Mp3Extractor, AviExtractor, JpegExtractor) could read the stream. at com.google.android.exoplayer2.source.BundledExtractorsAdapter.init(BundledExtractorsAdapter.java:92) at com.google.android.exoplayer2.source.ProgressiveMediaPeriod$ExtractingLoadable.load(ProgressiveMediaPeriod.java:1017) at com.google.android.exoplayer2.upstream.Loader$LoadTask.run(Loader.java:412) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637) at java.lang.Thread.run(Thread.java:1012)
现有实现代码
自定义DataSource(Kotlin)
class EncryptedDataSource(private val key: String) : DataSource { private var inputStream: CipherInputStream? = null private lateinit var uri: Uri override fun addTransferListener(transferListener: TransferListener) {} override fun open(dataSpec: DataSpec): Long { uri = dataSpec.uri try { val file = File(uri.path) val skeySpec = SecretKeySpec("e9wF6xA6WpTfyjeVrqOnesZD/KGrUDdx".toByteArray(), KeyProperties.KEY_ALGORITHM_AES) val cipher = Cipher.getInstance("AES/ECB/PKCS5Padding") cipher.init(Cipher.DECRYPT_MODE, skeySpec) inputStream = CipherInputStream(file.inputStream(), cipher) if (dataSpec.position != 0L) { inputStream?.forceSkip(dataSpec.position) // Needed for skipping } } catch (e: Exception) { } return dataSpec.length } @Throws(IOException::class) override fun read(buffer: ByteArray, offset: Int, readLength: Int): Int = if (readLength == 0) { 0 } else { inputStream?.read(buffer, offset, readLength) ?: 0 Log.i("HeshamNyla",inputStream.toString()); } override fun getUri(): Uri? = uri @Throws(IOException::class) override fun close() { inputStream?.close() } }
DataSource Factory(Kotlin)
class EncryptedDataSourceFactory( private val key: String ) : DataSource.Factory { override fun createDataSource(): EncryptedDataSource = EncryptedDataSource(key) }
下载加密代码(Java)
private void downloadFile(ResponseBody body,Message msg,boolean type) { try { int count; byte data[] = new byte[1024 * 4]; long fileSize = body.contentLength()+length; InputStream bis = new BufferedInputStream(body.byteStream(), 1024 * 8); File outputFile = new File(new LocalVideos(getApplicationContext()).getFolder()+name); OutputStream output = new FileOutputStream(outputFile,type); Cipher encipher = Cipher.getInstance("AES/CBC/PKCS5PAdding"); SecretKey skey = new SecretKeySpec("e9wF6xA6WpTfyjeVrqOnesZD/KGrUDdx".getBytes(), KeyProperties.KEY_ALGORITHM_AES); encipher.init(Cipher.ENCRYPT_MODE, skey); CipherInputStream cis = new CipherInputStream(bis, encipher); CipherOutputStream cos = new CipherOutputStream(output,encipher); long total = length; long startTime = System.currentTimeMillis(); int timeCount = 1; while ((count = cis.read(data)) != -1) { total += count; totalFileSize = (int) ((int) (fileSize / (Math.pow(1024, 2)))); double current = Math.round(total / (Math.pow(1024, 2))); int progress = (int) ((int) ((total * 100) / fileSize)); long currentTime = System.currentTimeMillis() - startTime; Download download = new Download(); download.setTotalFileSize(totalFileSize); if (currentTime > 1000 * timeCount) { download.setCurrentFileSize((int) current); download.setProgress(progress); sendNotification(download); timeCount++; } cos.write(data, 0, count); } onDownloadComplete(msg); cos.flush(); cos.close(); bis.close(); }catch (Throwable exception){ stopForeground(true); stopSelf(msg.arg1); notificationManager.cancel(NOTFIYID); notificationBuilder.setProgress(0,0,false); notificationBuilder.setContentText("Failed try again"); notificationManager.notify(NOTFIYID, notificationBuilder.build()); new VideosState(getApplicationContext()).updateVideoState(name,VideosState.STATE_STOPED); } }
ExoPlayer使用代码(Java)
mediaItem = new MediaItem.Builder() .setUri(Uri.fromFile(new File(url))) .build(); DataSource.Factory dataSource= new EncryptedDataSourceFactory(""); MediaSource videoSource = new ProgressiveMediaSource.Factory(dataSource) .createMediaSource(mediaItem); playerView.setPlayer(player); playerView.setKeepScreenOn(true); player.setPlaybackParameters(parameters); player.setMediaSource(videoSource); player.prepare();
错误原因分析
- 加密解密算法模式不匹配:下载时用
AES/CBC/PKCS5Padding(代码里拼写错误为PKCS5PAdding),解密时用AES/ECB/PKCS5Padding,两种模式完全不兼容,解密后的数据不是有效视频格式,导致ExoPlayer无法识别。 - CBC模式缺少IV处理:CBC模式必须使用初始化向量(IV),加密时如果不指定会自动生成随机IV,但解密时必须使用同一个IV才能正确解密。当前代码未保存/传递IV,导致解密失败。
- 下载加密逻辑错误:同时使用
CipherInputStream和CipherOutputStream,相当于对数据做了双重加密,完全破坏了原始视频数据结构。 - 异常处理不当:
EncryptedDataSource的open方法捕获异常后直接忽略,未抛出或处理,导致初始化失败时inputStream为null,后续读取返回0,数据完全无效。 - 不支持随机读取/跳过:
CipherInputStream不支持skip操作,ExoPlayer播放时需要对数据源进行seek,当前自定义的forceSkip无法正确实现跳过逻辑,导致解析器无法读取视频头部信息。
解决方案
1. 统一加密解密算法,正确处理CBC模式的IV
加密和解密都使用AES/CBC/PKCS5Padding,修正代码中的拼写错误,同时在加密时生成并保存IV,解密时读取IV。
2. 修复下载加密逻辑
删除多余的CipherInputStream,直接用CipherOutputStream加密写入,并将IV写入文件头部:
private void downloadFile(ResponseBody body, Message msg, boolean type) { try { int count; byte data[] = new byte[1024 * 4]; long fileSize = body.contentLength() + length; InputStream bis = new BufferedInputStream(body.byteStream(), 1024 * 8); File outputFile = new File(new LocalVideos(getApplicationContext()).getFolder() + name); // 生成随机IV(AES CBC模式必须,长度16字节) byte[] iv = new byte[16]; new SecureRandom().nextBytes(iv); // 修正拼写错误:PKCS5PAdding -> PKCS5Padding Cipher encipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); SecretKey skey = new SecretKeySpec("e9wF6xA6WpTfyjeVrqOnesZD/KGrUDdx".getBytes(), KeyProperties.KEY_ALGORITHM_AES); encipher.init(Cipher.ENCRYPT_MODE, skey, new IvParameterSpec(iv)); FileOutputStream output = new FileOutputStream(outputFile, type); // 先写入IV到文件头部,方便解密时读取 output.write(iv); CipherOutputStream cos = new CipherOutputStream(output, encipher); long total = length; long startTime = System.currentTimeMillis(); int timeCount = 1; while ((count = bis.read(data)) != -1) { total += count; totalFileSize = (int) (fileSize / (Math.pow(1024, 2))); double current = Math.round(total / (Math.pow(1024, 2))); int progress = (int) ((total * 100) / fileSize); long currentTime = System.currentTimeMillis() - startTime; Download download = new Download(); download.setTotalFileSize(totalFileSize); if (currentTime > 1000 * timeCount) { download.setCurrentFileSize((int) current); download.setProgress(progress); sendNotification(download); timeCount++; } cos.write(data, 0, count); } onDownloadComplete(msg); cos.flush(); cos.close(); bis.close(); output.close(); } catch (Throwable exception) { stopForeground(true); stopSelf(msg.arg1); notificationManager.cancel(NOTFIYID); notificationBuilder.setProgress(0,0,false); notificationBuilder.setContentText("Failed try again"); notificationManager.notify(NOTFIYID, notificationBuilder.build()); new VideosState(getApplicationContext()).updateVideoState(name,VideosState.STATE_STOPED); } }
3. 修复EncryptedDataSource,支持IV读取和正确的跳过逻辑
class EncryptedDataSource(private val key: String) : DataSource { private var inputStream: CipherInputStream? = null private var fileInputStream: FileInputStream? = null private lateinit var uri: Uri override fun addTransferListener(transferListener: TransferListener) {} override fun open(dataSpec: DataSpec): Long { uri = dataSpec.uri try { val file = File(uri.path ?: throw IOException("Invalid file path")) fileInputStream = FileInputStream(file) // 读取文件头部的IV(16字节) val iv = ByteArray(16) if (fileInputStream!!.read(iv) != 16) { throw IOException("Failed to read IV from encrypted file") } val skeySpec = SecretKeySpec("e9wF6xA6WpTfyjeVrqOnesZD/KGrUDdx".toByteArray(), KeyProperties.KEY_ALGORITHM_AES) val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") cipher.init(Cipher.DECRYPT_MODE, skeySpec, IvParameterSpec(iv)) inputStream = CipherInputStream(fileInputStream, cipher) // 处理跳过逻辑 if (dataSpec.position != 0L) { var skipped = 0L while (skipped < dataSpec.position) { val skip = inputStream!!.skip(dataSpec.position - skipped) if (skip == 0L) break skipped += skip } if (skipped != dataSpec.position) { throw IOException("Failed to skip to required position") } } // 返回实际视频数据长度(减去IV的16字节) return file.length() - 16 } catch (e: Exception) { // 抛出异常,避免静默失败 throw IOException("Failed to open encrypted data source", e) } } @Throws(IOException::class) override fun read(buffer: ByteArray, offset: Int, readLength: Int): Int { if (readLength == 0) { return 0 } val readCount = inputStream?.read(buffer, offset, readLength) ?: -1 return if (readCount == -1) { DataSource.RESULT_END_OF_INPUT } else { readCount } } override fun getUri(): Uri? = uri @Throws(IOException::class) override fun close() { inputStream?.close() fileInputStream?.close() } }
4. 优化ExoPlayer使用代码
保持现有逻辑,注意异常日志的收集,同时后续建议将硬编码的密钥迁移到Android Keystore中存储,提升安全性。
内容的提问来源于stack exchange,提问作者HESHAM MOHAMED
相关产品推荐
相关产品推荐

