在 ExoPlayer 中离线播放 AES 加密视频

Play AES encrypted video in ExoPlayer offline

我正在尝试使用 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();
    }

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

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

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

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

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

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

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

  1. Detection

For a stream determined to be in the ISO Base Media File Format [ISOBMFF], this ISO Common Encryption ('cenc') Protection Scheme may be detected as follows.

Protection scheme signaling conforms with [ISOBMFF]. When protection has been applied, the stream type will be transformed to 'encv' for video or 'enca' for audio, with a Protection Scheme Information Box ('sinf') added to the sample entry in the Sample Description Box ('stsd'). The Protection Scheme Information Box ('sinf') will contain a Scheme Type Box ('schm') with a scheme_type field set to a value of '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 月)不读取 MP4 的 PSSH 框,仅对于零散的 MP4。来自该线程:

After looking deeper into this, I found that ExoPlayer's Mp4Extractor actually doesn't read pssh boxes. Currently we only read this info in fragmented MP4 files (using FragmentedMp4Extractor). This means even when playing the file with the pssh box, the drmInitData still ends up null - meaning playback fails. I didn't realise this limitation when you initially filed the issue, otherwise I would have flagged it earlier.

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

那么视频如何加密并可靠播放呢?

您可以在 android 上使用 ffplay,但根据过去在 Android.

上使用 ffmpeg 的经验,我怀疑这不会太简单

还有一些利用 ExoPlayer 的更简单(看似)的示例也值得一看 - 例如:

您还可以考虑利用 DASH。现在流式传输到移动设备的大多数媒体都使用像 DASH 或 HLS 这样的流式传输协议——这些格式几乎总是将加密数据包含在 'manifest' 或 'index' 文件中,这肯定会被识别外部播放器。有在线教程和免费工具可让您将视频打包到 DASH 中,包括添加加密。 ExoPlayer 团队提供有关下载和播放此类流的信息(link 在撰写本文时正确):

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