You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 03:18:25