为了保证视频文件的安全性,有时候需要对保存的视频文件加密,然后播放的时候解密出来再播放,只有加密解密的秘钥一致时才能正常播放,用ffmpeg做视频文件的加密保存和解密播放比较简单,基于ffmpeg强大的字典参数设计,在avformat_write_header写入头部数据的时候,可以通过万能的av_dict_set来给options设置encryption_scheme/encryption_key/encryption_kid三键值,这样保存后的文件是加密过的,第三方播放器都无法正常播放,需要用ffmpeg打开并设置一样的秘钥才能解密播放,打开这边也是通过av_dict_set来给options设置decryption_key键值,在调用avformat_open_input的时候传入options即可。这种加密解密的应用场景一般在对视频文件要求安全性很高的地方,相当于需要定制播放器才能正常播放,做的好一点的话,还可以自定义格式,文件格式关联到定制的播放器,如果默认的MP4文件格式,可能用户不自觉的还会去双击播放,打不开就会很纳闷,所以针对这种加密过的视频文件一般定义一个自定义的格式保存比较好,拓展名仅仅是个名字,和里面的内容无关,内容都是二进制数据。
查阅资料得知,加密和未加密的文件,就是在头部数据中stbl下新增加了3个box。
bool FFmpegSave::initStream()
{
AVDictionary *options = NULL;
QByteArray fileData = fileName.toUtf8();
const char *url = fileData.data();
//既可以是保存到文件也可以是推流(对应格式要区分)
const char *format = "mp4";
if (videoIndexIn < 0 && audioCodecName == "mp3") {
format = "mp3";
}
if (fileName.startsWith("rtmp://")) {
format = "flv";
} else if (fileName.startsWith("rtsp://")) {
format = "rtsp";
av_dict_set(&options, "stimeout", "3000000", 0);
av_dict_set(&options, "rtsp_transport", "tcp", 0);
} else if (fileName.startsWith("udp://")) {
format = "mpegts";
}
//如果存在秘钥则启用加密
QByteArray cryptoKey = this->property("cryptoKey").toByteArray();
if (!cryptoKey.isEmpty()) {
av_dict_set(&options, "encryption_scheme", "cenc-aes-ctr", 0);
av_dict_set(&options, "encryption_key", cryptoKey.constData(), 0);
av_dict_set(&options, "encryption_kid", cryptoKey.constData(), 0);
}
//开辟一个格式上下文用来处理视频流输出(末尾url不填则rtsp推流失败)
int result = avformat_alloc_output_context2(&formatCtx, NULL, format, url);
if (result < 0) {
debug(result, "创建格式", "");
return false;
}
//创建输出视频流
if (!this->initVideoStream()) {
goto end;
}
//创建输出音频流
if (!this->initAudioStream()) {
goto end;
}
//打开输出文件
if (!(formatCtx->oformat->flags & AVFMT_NOFILE)) {
result = avio_open(&formatCtx->pb, url, AVIO_FLAG_WRITE);
if (result < 0) {
debug(result, "打开输出", "");
goto end;
}
}
//写入文件开始符
result = avformat_write_header(formatCtx, &options);
if (result < 0) {
debug(result, "写文件头", "");
goto end;
}
return true;
end:
//关闭释放并清理文件
this->close();
this->deleteFile(fileName);
return false;
}
void FFmpegThread::initOption()
{
//设置缓存大小(1080p可将值调大/现在很多摄像机是2k可能需要调大/一般2k是1080p的四倍)
av_dict_set(&options, "buffer_size", "8192000", 0);
//设置超时断开连接时间(单位微秒/3000000表示3秒)
av_dict_set(&options, "stimeout", "3000000", 0);
//设置最大时延(单位微秒/1000000表示1秒)
av_dict_set(&options, "max_delay", "1000000", 0);
//自动开启线程数
av_dict_set(&options, "threads", "auto", 0);
//通信协议采用tcp还是udp(udp优点是无连接/在网线拔掉以后十几秒钟重新插上还能继续接收/缺点是网络不好的情况下会丢包花屏)
if (transport != "auto") {
av_dict_set(&options, "rtsp_transport", transport.toUtf8().constData(), 0);
}
//开启无缓存(rtmp等视频流不建议开启)
//av_dict_set(&options, "fflags", "nobuffer", 0);
//av_dict_set(&options, "fflags", "discardcorrupt", 0);
//有些视频网站根据这个头部消息过滤不让ffmpeg访问需要模拟成其他的
//av_dict_set(&options, "user_agent", "libmpv", 0);
av_dict_set(&options, "user_agent", "Mozilla", 0);
//增加rtp/sdp支持(后面发现不要加)
if (videoUrl.endsWith(".sdp")) {
//av_dict_set(&options, "protocol_whitelist", "file,rtp,udp", 0);
}
//设置分辨率帧率等参数
if (videoType == VideoType_Camera || videoType == VideoType_Desktop) {
//设置分辨率
if (bufferSize != "0x0") {
av_dict_set(&options, "video_size", bufferSize.toUtf8().constData(), 0);
}
//设置帧率
if (frameRate > 0) {
av_dict_set(&options, "framerate", QString::number(frameRate).toUtf8().constData(), 0);
}
//设置坐标XY以及是否采集鼠标指针
if (videoType == VideoType_Desktop) {
//av_dict_set(&options, "draw_mouse", "0", 0);
//av_dict_set(&options, "show_region", "1", 0);
int offsetX = this->property("offsetX").toInt();
int offsetY = this->property("offsetY").toInt();
#ifdef Q_OS_WIN
//windows上通过设置offset_x/offset_y来设置偏移值
av_dict_set(&options, "offset_x", QString::number(offsetX).toUtf8().constData(), 0);
av_dict_set(&options, "offset_y", QString::number(offsetY).toUtf8().constData(), 0);
#else
//linux系统通过设备名称带上偏移值传入
videoUrl += QString("+%1,%2").arg(offsetX).arg(offsetY);
#endif
}
//设置输入格式(具体要看设置是否支持)
//后面改成了统一转成标准yuv420所以不用设置也没有任何影响
//av_dict_set(&options, "input_format", "mjpeg", 0);
}
//如果设置了秘钥则需要启用解密
if (!secretKey.isEmpty()) {
QByteArray cryptoKey = this->property("cryptoKey").toByteArray();
av_dict_set(&options, "decryption_key", cryptoKey.constData(), 0);
}
}