首页 > 解决方案 > 离线播放 ExoPlayer 中的 AES 加密视频

问题描述

我正在尝试使用 ExoPlayer 从本地存储播放加密视频。使用 FFMPEG 加密视频的命令如下:

-i /storage/emulated/0/Download/20210125_193031.mp4 -vcodec copy -acodec copy -c:v libx264 -encryption_scheme cenc-aes-ctr -encryption_key b42ca3172ee4e69bf51848a59db9cd13 -encryption_kid 09e367028f33436ca5dd60ffe6671e70 /storage/emulated/0/Download/out_enc.mp4

这是我的播放器的源代码:

public class PlayerActivity extends AppCompatActivity {
    private SimpleExoPlayer player;
    private DefaultDrmSessionManager drmSessionManager;

    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_player);
        // Build the media item.
        PlayerView playerView = findViewById(R.id.video_view);
        player = new SimpleExoPlayer.Builder(this).build();
        playerView.setPlayer(player);
        //player.prepare();
        //FFMPEG command: -i /storage/emulated/0/Download/20210125_193031.mp4 -vf scale=-1:720 -c:v libx264 -encryption_scheme cenc-aes-ctr -encryption_key b42ca3172ee4e69bf51848a59db9cd13 -encryption_kid 09e367028f33436ca5dd60ffe6671e70 /storage/emulated/0/Download/out_enc.mp4
        //base 64 keys generated from: https://www.base64encode.org/
        //playVideo("/storage/emulated/0/Download/out_enc.mp4", "MDllMzY3MDI4ZjMzNDM2Y2E1ZGQ2MGZmZTY2NzFlNzA=", "YjQyY2EzMTcyZWU0ZTY5YmY1MTg0OGE1OWRiOWNkMTM=");
        playVideo("/storage/emulated/0/Download/out_enc.mp4", "CeNnAo8zQ2yl3WD/5mcecA", "tCyjFy7k5pv1GEilnbnNEw");
    }

    private void playVideo(String url, String keyID, String keyValue) {
        try {
            drmSessionManager = buildDrmSessionManager(Util.getDrmUuid(C.CLEARKEY_UUID.toString()), true, keyID, keyValue
            );
        } catch (Exception e) {
            e.printStackTrace();
        }
        player.setMediaSource(buildDashMediaSource(Uri.parse(url)));
        player.prepare();
        player.setPlayWhenReady(true);
    }

    private MediaSource buildDashMediaSource(Uri uri) {
        DefaultDataSourceFactory dashChunkSourceFactory = new DefaultDataSourceFactory(this, "agent");
        return new ProgressiveMediaSource.Factory(dashChunkSourceFactory)
                .setDrmSessionManager(drmSessionManager)
                .createMediaSource(uri);
    }

    private DefaultDrmSessionManager buildDrmSessionManager(UUID uuid, Boolean multiSession, String id, String value) {
/*        String base64Id = Base64.encodeToString(id.getBytes(), Base64.DEFAULT);
        String base64Value = Base64.encodeToString(value.getBytes(), Base64.DEFAULT);*/
        String keyString = "{\"keys\":[{\"kty\":\"oct\",\"k\":\""+value+"\",\"kid\":\""+id+"\"}],\"type\":\"temporary\"}";;
        LocalMediaDrmCallback drmCallback = new LocalMediaDrmCallback(keyString.getBytes());
        FrameworkMediaDrm mediaDrm = null;
        try {
            mediaDrm = FrameworkMediaDrm.newInstance(uuid);
        } catch (UnsupportedDrmException e) {
            e.printStackTrace();
        }
        return new DefaultDrmSessionManager(uuid, mediaDrm, drmCallback, null, multiSession);
    }

    @Override
    protected void onDestroy() {
        player.release();
        super.onDestroy();
    }

是加密视频的链接。主要问题:视频正在播放但未解密。我错过了什么?

标签: encryptionaesexoplayer

解决方案


查看 logcat 输出,似乎没有任何 DRM、AES 或 clearkey 错误。

然后查看视频文件本身,它似乎报告了一些问题:

在此处输入图像描述

但是,检查使用您使用的相同 ffmpeg 方法加密的其他示例文件时,它们会显示类似的问题,因此这似乎是 ffprobe 对于以这种方式加密的文件的典型输出。

然后使用 MP4 解析器查看视频文件结构本身以查看单个原子或标题块,似乎没有 PSSH 框。

PSSH 框是一个标头区域,其中包含有关 ISOBMFF mp4 文件加密的数据 - 这实际上是 CENC 规范中的一个可选字段,因此即使没有这个,您的视频也是有效的。

那么显而易见的问题是,播放器如何知道视频已编码?根据 CENC 规范,答案是:

  1. 检测

对于确定为 ISO 基本媒体文件格式 [ISOBMFF] 的流,该 ISO 通用加密 ('cenc') 保护方案可如下检测。

保护方案信令符合[ISOBMFF]。应用保护后,流类型将转换为视频的“encv”或音频的“enca”,并在样本描述框(“stsd”的样本条目中添加保护方案信息框(“sinf”) )。保护方案信息框 ('sinf') 将包含一个方案类型框 ('schm'),其中 scheme_type 字段的值设置为 'cenc'

使用 MP4 分析器(见下文)查看您的视频表明它确实具有在 stud 框中显示为“encv”的流类型(下面的检查工具的输出):

在此处输入图像描述

使用相同的加密密钥测试 ffplay 本身的播放,表明视频确实成功播放:

ffplay out_enc.mp4 -decryption_key b42ca3172ee4e69bf51848a59db9cd13

但是,除非您提供解密密钥,否则普通玩家将无法播放它。在这种情况下,期望播放器标记错误是合理的,但我检查过的一些常见播放器(包括 VLC)似乎不会发生这种情况,因此 Android 上的 ExoPlayer 很可能也没有标记此错误。

具体来看 ExoPlayer,正如 Duna 在下面的评论中指出并在此 GIT 线程https://github.com/google/ExoPlayer/issues/8532#issuecomment-771811707中概述的那样,ExoPlayer 目前(2021 年 2 月)不读取 PSSH用于 MP4 的框,仅适用于零碎的 MP4。从那个线程:

在深入研究之后,我发现 ExoPlayer 的 Mp4Extractor 实际上不读取 pssh 框。目前我们只在碎片化的 MP4 文件中读取此信息(使用 FragmentedMp4Extractor)。这意味着即使在使用 pssh 框播放文件时,drmInitData 仍然以 null 结束——这意味着播放失败。当您最初提交问题时,我没有意识到这个限制,否则我会更早地标记它。

但是,查看 Mp4Extractor 代码,它确实会检查“encv”,并且还会检查默认的 key_id,这两者都存在于检查时生成的视频文件中。同样,如果 ExoPlayer 没有以可以理解的格式找到这些错误,或者如果找到它们但没有提供相应的密钥来播放文件,则 ExoPlayer 标记错误是合理的。

那么如何才能对视频进行加密并可靠播放呢?

您可以在 android 上使用 ffplay,尽管根据过去在 Android 上使用 ffmpeg 的经验,我怀疑这不会太简单。

还有一些更容易(出现)的例子利用 ExoPlayer 也值得一看 - 例如:

您还可以考虑利用 DASH。如今,流式传输到移动设备的大多数媒体都使用 DASH 或 HLS 等流式传输协议——这些格式几乎总是将加密数据包含在“清单”或“索引”文件中,ExoPlayer 肯定会识别这一点。有在线教程和免费工具可让您将视频打包到 DASH 中,包括添加加密。ExoPlayer 团队提供了有关下载和播放这些流的信息(在撰写本文时链接正确):

如果您想自己更详细地检查 mp4 文件,可以使用各种免费工具,例如:


推荐阅读