ijkplayer 自定义协议播放加密内容 Android

6,359 阅读6分钟

想对播放的音视频进行加密,防止资源被盗用,该怎么办呢?

这篇文章从自定义协议的角度来提供一中实现思路。在 ijkplayer 的基础上,通过实现自定义协议对文件进行解密。边解边播,以此为基础,还可以实现在线资源边下载边解密边播放。

结合 ijkplayer 源码阅读本文效果最佳。

FFmpeg 文件协议

ffmpeg 中定义了 URLProtocol, 对接入 ffmpeg 中的各种协议进行了统一的抽象,这里可以理解为 C++ 中的抽象基类,并基于此抽象协议实现了 file、http、ftp、cache 等许多不同的具体文件传输协议。
而在 libavformat 模块中的 avio.c aviobuf.c 两个文件则是在 ffmpeg 对文件传输协议抽象的基础上进行的封装,使对文件的操作能够在 ffmpeg 中其他对文件协议细节不关心的地方使用。这就好比是面向对象中的模版模式。
ffmpeg 中的文件协议简单讲就是这样,更深入复杂的我也还没具体研究,本文中也用不到。

URLProtocol 的部分代码如下,这里我只留下了最常用的一部分。URLProtocol 中定义了很多函数指针,在 avio.c 的模板模式代码中用到。

typedef struct URLProtocol {
    const char *name;
    int     (*url_open)( URLContext *h, const char *url, int flags);
    int     (*url_read)( URLContext *h, unsigned char *buf, int size);
    int     (*url_write)(URLContext *h, const unsigned char *buf, int size);
    int64_t (*url_seek)( URLContext *h, int64_t pos, int whence);
    int     (*url_close)(URLContext *h);

    int priv_data_size;
    const AVClass *priv_data_class;

} URLProtocol;

通过实现 URLProtocol 中的函数指针,就完成了一个可以用在 ffmpeg 中的文件协议。比如我找了一番 ffmpeg 中一个比较简单的文件协议,从实现上可以看出这个 bluray 在ffmpeg 中是一个只读协议。

const URLProtocol ff_bluray_protocol = {
    .name            = "bluray",
    .url_close       = bluray_close,
    .url_open        = bluray_open,
    .url_read        = bluray_read,
    .url_seek        = bluray_seek,
    .priv_data_size  = sizeof(BlurayContext),
    .priv_data_class = &bluray_context_class,
};

ffmpeg 中通过一个列表保存了所有实现了的文件协议,进行各种文件操作的第一步,就是先根据 url(filename)找到对应的 URLProtocol。这段代码中通过匹配 url 中的 scheme 字符串,找到并返回对应的 protocol 指针。

static const struct URLProtocol *url_find_protocol(const char *filename)
{
    // ......
    const URLProtocol **protocols = ffurl_get_protocols(NULL, NULL);
    if (!protocols)
        return NULL;
    for (i = 0; protocols[i]; i++) {
            const URLProtocol *up = protocols[i];
        if (!strcmp(proto_str, up->name)) {
            av_freep(&protocols);
            return up;
        }
    }
    // ......
}

如果对 ffmpeg 有一定的了解,可能会知道  ffmpeg 中的 Codec 还有 avcodec_register。FFmpeg 通过接口 avcodec_register 在运行时动态添加编解码器。但是却没有类似的 avformat_register 或者 av_urlprotocol_register 来实现运行时动态注册自定义的 protocol。为什么不实现这个的原因没有研究过,所以不能瞎吹。但是我们可以改 ffmpeg 源代码呀。

ijkplayer 的作者就改了 ffmpeg ,新增了几个 URLProtocol 并加入到了默认的 protocols 数组中,这些新增的 protocols 在 ffmpeg 源码中默认是空的实现。但是在 ffmpeg 中预留了接口可以在 ijkplayer 中使用后期实现的 protocol 替换这个空的 protocol。相当于添加了作用类似于 av_urlprotocol_register 但是有一定限制的接口。

ijkmediadatasource 协议实现

在 ijkplayer 项目的 ijkmediadatasource.c 源文件中,实现了一种 URLProtocol

URLProtocol ijkimp_ff_ijkmediadatasource_protocol = {
    .name                = "ijkmediadatasource",
    .url_open2           = ijkmds_open,
    .url_read            = ijkmds_read,
    .url_seek            = ijkmds_seek,
    .url_close           = ijkmds_close,
    .priv_data_size      = sizeof(Context),
    .priv_data_class     = &ijkmediadatasource_context_class,
};

在每个具体的函数指针实现中,又通过 J4A 生成的胶水代码去调用到 java 层的代码。
进一步解释一下, setDataSource 的时候调用到这一块代码,callback 已经是一个具体的 java 对象了,并且是一个 tv/danmaku/ijk/media/player/misc/IMediaDataSource 接口的具体实现。

IjkMediaPlayer_setDataSourceCallback(JNIEnv *env, jobject thiz, jobject callback) {
    nativeMediaDataSource = jni_set_media_data_source(env, thiz, callback);
    snprintf(uri, sizeof(uri), "ijkmediadatasource:%"PRId64, nativeMediaDataSource);
    retval = ijkmp_set_data_source(mp, uri);
}

jni_set_media_data_source 函数新建一个 NDK 环境对 callback 这个 jobject 的全局引用,并把引用作为 intptr_t 类型保存在 IjkMediaPlayer 的 mNativeMediaDataSource 字段中,方便后面再次使用以及最后需要close 是用到。 通过这样的转换之后,url 进入 ffmpeg  avformat 逻辑中就成了 “ijkmediadatasource:2234234290" 这样的形式。 再通过 url_find_protocol 找到  ijkimp_ff_ijkmediadatasource_protocol ,所有的对这个文件协议的操作又回到 ijkplayer 的代码中了。
ijkmds_open  中,只需要把 intptr_t 的变量转换成 jobject 就行,不必实际去打开某个文件。 ijkmds_read 、 ijkmds_seek  函数通过 J4A 去调用  IMediaDataSource 接口的 readAt 方法。 readAt 方法多了 pos 参数,所以 read 和 seek 都可以通过 readAt 实现, ijkmds_close  调用 IMediaDataSource 接口的 close 方法,并释放 NDK 环境中的 jobject 全局引用。

通过这一层套一层的接口定义实现、struct 函数指针定义实现, java 代码中设置的 dataSource 在 ijkplayer 、ffmpeg 中打了个转之后最终又回到 java 代码中。

jni4android 自动生成代码

jni4android 是 B 站出品的开源项目 github.com/bilibili/jn…。能够根据简单的 java 接口描述代码生成 ndk 胶水代码,方便在 ndk c/c++ 环境中调用 java 代码。
ijkmediadatasource.c 源文件中 JNI 相关调用的代码,都是 j4a 自动生成的。按照 jni4android 中 readme 编译好 j4a 之后,结合 ijkplayer/ijkmedia/j4a  中的 makefile 文件,就可以快速掌握 j4a 在 ijkplayer 中的作用和使用方法了。

加密和解密

前面代码分析中解释了 ijkplayer 中自定义文件协议到底是怎么一回事,以及其中的函数调用过程怎么样的。
分析完了,终于可以开始代码敲起来。我们先对一个视频文件加密,然后实现解密的文件协议并在播放中使用。

这里采用古老的凯撒加解密算法,加密过程就是每个 byte 加个数字,解密过程就是每个 byte 减去个数字。相当简单,很容易暴力破解,这里仅作为示例参考。

加密过程如下(截取部分代码):

func main() {
	ibuf := bufio.NewReader(inputfile)
	obuf := bufio.NewWriter(outputfile)
	buf := make([]byte, 128)
	for {
		n, err := ibuf.Read(buf)
		if err == nil {
			for index := 0; index < n; index++ {
				buf[index] = buf[index] + byte(cck)
			}
			obuf.Write(buf[:n])
		} else if err == io.EOF {
			break
		}
	}
	obuf.Flush()
}

解密的 java IMediaDataSource 实现如下,这里只给出关键部分,其他的和项目中 FileMediaDataSource 一样。

public class CCFileMediaDataSource implements IMediaDataSource {
    @Override
    public int readAt(long position, byte[] buffer, int offset, int size) throws IOException {
        if (mFile.getFilePointer() != position)
            mFile.seek(position);
        if (size == 0) return 0;
        int s = mFile.read(buffer, 0, size);
        for (int i = 0; i < s; i++) {
        	buffer[i] = (byte)(buffer[i] - 10);
        }
    	return s;
    }
}

什么情况下会用到 CCFileMediaDataSource 呢? 简单粗暴,把项目中 IjkVieoView.java 用到 FileMediaDataSource 的地方都改成 CCFileMediaDataSource,其实也没几处,然后别忘了在 demo 中设置选项里选中使用MediaDataSource。

IMediaDataSource 其他实现

RandomAccessFile 实现的 IMediaDataSource, 支持本地保存的文件。


public class FileMediaDataSource implements IMediaDataSource {
    private RandomAccessFile mFile;
    private long mFileSize;

    public FileMediaDataSource(File file) throws IOException {
        mFile = new RandomAccessFile(file, "r");
        mFileSize = mFile.length();
    }

    @Override
    public int readAt(long position, byte[] buffer, int offset, int size) throws IOException {
        if (mFile.getFilePointer() != position)
            mFile.seek(position);
        if (size == 0) return 0;
        return mFile.read(buffer, 0, size);
    }

    @Override
    public long getSize() throws IOException {
        return mFileSize;
    }

    @Override
    public void close() throws IOException {
        mFileSize = 0;
        mFile.close();
        mFile = null;
    }
}

InputStream 实现的 IMediaDataSource,可以用于播放 asset 资源文件

public class StreamDataSource implements IMediaDataSource {
    private InputStream mIs;
    private long mPosition = 0;

    public StreamDataSource(InputStream mIs) {
        this.mIs = mIs;
    }

    @Override
    public int readAt(long position, byte[] buffer, int offset, int size) throws IOException {
        if (size <= 0)
            return size;
        if (mPosition != position) {
            mIs.reset();
            mPosition = mIs.skip(position);
        }
        int length = mIs.read(buffer, offset, size);
        mPosition += length;
        return length;
    }

    @Override
    public long getSize() throws IOException {
        return mIs.available();
    }

    @Override
    public void close() throws IOException {
        if (mIs != null)
            mIs.close();
        mIs = null;
    }
}
  AssetManager assetManager = mContext.getAssets();
  InputStream is = assetManager.open("asset_file_path", AssetManager.ACCESS_RANDOM);
  mIjkMediaPlayer.setDataSource(new StreamDataSource(is));

文中完整代码请在 github.com/befovy/blog… 查看