上周回顾
- h264的编解码(软、硬解码)
- 像素格式的变换
本周计划
- rtsp的封装与解封装
本周记录
封装与解封装
#daily/25/10/27
mp4格式
h264编码的sps中存放了如 width height pix_format等信息,不用像播放yuv420p视频一样指定宽高
ffplay -i ./640_360_30.yuv -video_size 640x360
ffplay -i ./640_360_30.h264压缩成h264后,虽然也不用指定宽高就能播放,但是
- h264只有视频流
- h264不能seek
mp4可以视作一个容器
- 可以同时存放视频编码(h264)+音频编码(aac)
- 支持http协议
解封装
libavformat 库
解封装接口
AVFormatContext* context_fmt = nullptr;
// 打开输入封装器,会调用avformat_alloc_context分配空间,需要提供一个指针
avformat_open_input(&context_fmt,PATH_MP4,NULL,NULL);
// 获取媒体信息
avformat_find_stream_info(context_fmt, NULL);
// 打印封装信息
av_dump_format(context_fmt,0,PATH_MP4,0);
// 将封装信息拷贝到上下文
avcodec_parameters_to_context(context_dec, vs->codecpar);
// 读取封装的音视频编码
av_read_frame(context_fmt, pkt);解封装后渲染
具体参考 019test_demux_render
while(true)
{
// 读取到压缩帧后直接就发送就可以了
re = av_read_frame(context_fmt, pkt);
// 处理视频
if (vs && pkt->stream_index == vs->index) {
re = avcodec_send_packet(context_dec,pkt); // 发送编码帧
if (re != 0) break;
while (true)
{
re = avcodec_receive_frame(context_dec,frame); // 接收原始帧
if (re != 0) break;
cout << "frame pts :" << frame->pts << endl;
view->Draw(
frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1],
frame->data[2], frame->linesize[2]
);
this_thread::sleep_for(chrono::milliseconds(10));
}
}
}封装
封装接口
具体参考 020test_h264_remux
avformat_alloc_output_context2 // 上下文
avformat_new_stream // 创建流
avio_open // 打开输出io
avcodec_parameters_copy // 将参数拷贝到封装信息中
avformat_write_header // 写入文件头
av_interleaved_write_frame // 写入压缩帧,会自动释放pkt
av_write_trailer // 写入文件结尾
avformat_close_input // 关闭输入
avio_closep // 关闭io口


pts计算
- pts=这一帧解码的时间点
- dts=这一帧显示的时间点
sec / time_base = pts



练习:重封装mp4,截断后10s
#daily/25/10/28
具体参考 022test_truncate_10ms


#include <iostream>
extern "C"
{
#include "libavformat/avformat.h"
#include "libavutil/avutil.h"
#include "libavcodec/avcodec.h"
}
#include "../xvideoview/xvideoview.h"
#include "../xvideoview/xdecode.h"
#pragma comment(lib,"avformat.lib")
#pragma comment(lib,"avutil.lib")
#pragma comment(lib,"avcodec.lib")
#pragma comment(lib,"../../bin/Win32/Release/xvideoview.lib")
using namespace std;
#define PATH_MP4 "C:\\Users\\Shelton\\Workspaces\\code\\vs\\study_ffmpeg\\assets\\640_360_30.mp4"
#define PATH_OUT "C:\\Users\\Shelton\\Workspaces\\code\\vs\\study_ffmpeg\\assets\\out.mp4"
void PrintErr(int re)
{
char buf[128];
av_strerror(re,buf,sizeof(buf));
cout << buf << endl;
}
#define CERR(re) do{if (re != 0) {PrintErr(re); return -1;}}while(0)
int main()
{
int re = 0;
AVFormatContext* context_fmt_in = nullptr;
AVFormatContext* context_fmt_out = nullptr;
AVStream* vs = nullptr;
AVStream* as = nullptr;
//////////////////////////////////////////////////////////////////////////////
// 解封装 PATH_MP4
//////////////////////////////////////////////////////////////////////////////
re = avformat_open_input(&context_fmt_in,PATH_MP4,NULL,NULL);
CERR(re);
re = avformat_find_stream_info(context_fmt_in,NULL); // 获取流媒体详细信息
CERR(re);
av_dump_format(context_fmt_in,NULL,PATH_MP4,NULL);
for (int i = 0; i < context_fmt_in->nb_streams; i++)
{
if (context_fmt_in->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
vs = context_fmt_in->streams[i];
else if (context_fmt_in->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
as = context_fmt_in->streams[i];
}
//////////////////////////////////////////////////////////////////////////////
// 重封装 PATH_OUT
//////////////////////////////////////////////////////////////////////////////
AVPacket* pkt = av_packet_alloc();
// 创建输出上下文
re = avformat_alloc_output_context2(&context_fmt_out,NULL,NULL, PATH_OUT);
CERR(re);
// 创建音视频流
//auto enc = avcodec_find_encoder(vs->codecpar->codec_id);
AVStream* vs_out = avformat_new_stream(context_fmt_out, NULL);
AVStream* as_out = avformat_new_stream(context_fmt_out, NULL);
// 打开输出io
AVIOContext* context_avio = context_fmt_out->pb;
re = avio_open(&context_fmt_out->pb,PATH_OUT, AVIO_FLAG_WRITE);
CERR(re);
if (vs) {
vs_out->time_base = vs->time_base;
re = avcodec_parameters_copy(vs_out->codecpar,vs->codecpar); // 将输入的mp4文件的信息拷贝到要输入的mp4中
CERR(re);
}
if (as) {
as_out->time_base = as->time_base;
re = avcodec_parameters_copy(as_out->codecpar,as->codecpar); // 将输入的mp4文件的信息拷贝到要输入的mp4中
CERR(re);
}
// 写入输出头信息
re = avformat_write_header(context_fmt_out,NULL);
CERR(re);
int cnt = 0;
while (true)
{
re = av_read_frame(context_fmt_in,pkt); // 读取输入文件的pkt
if (re != 0) break;
cout << "wirte pkt" << cnt++ << endl;
// 写入时音视频流
re = av_interleaved_write_frame(context_fmt_out, pkt);
if (re != 0) break;
}
// 写入输出结束信息
re = av_write_trailer(context_fmt_out);
CERR(re);
cout << "wirte finished!" << endl;
avformat_close_input(&context_fmt_in);
avformat_close_input(&context_fmt_out);
avio_close(context_avio);
}截断10s
#daily/25/10/29
视频开头移动到第10秒
// 截断处理,计算pts
long long sec_begin = 10.0;
long long sec_end = 20.0;
auto sec = sec_end - sec_begin;
int pts_begin_video = 0; // 截断视频的开始时间
int pts_end_video = 0; // 截断视频的结束时间
int pts_begin_audio = 0; // 截断音频的开始时间
int pts_end_audio = 0; // 截断视频的结束时间
if (vs && vs->time_base.den > 0) {
auto time_base_of_1 = vs->time_base.den / vs->time_base.num; // num 分子, den 分母
// pts = sec / time_base
pts_begin_video = sec_begin * time_base_of_1;
pts_end_video = sec_end * time_base_of_1;
}
if (as && as->time_base.den > 0) {
auto time_base_of_1 = as->time_base.den / as->time_base.num; // num 分子, den 分母
// pts = sec / time_base ==> sec = time_base * pts
pts_begin_audio = sec_begin * time_base_of_1;
pts_end_audio = sec_end * time_base_of_1;
}
re = av_seek_frame(context_fmt_in,vs->index,pts_begin_video,AVSEEK_FLAG_FRAME| AVSEEK_FLAG_BACKWARD);视频第20秒以后的不写入
while (true)
{
re = av_read_frame(context_fmt_in,pkt); // 读取输入文件的pkt
if (re != 0) break;
// 视频从超过20秒不写入,退出
if (pkt->pts >= pts_end_video) {
continue;
}
// 写入时音视频流
re = av_interleaved_write_frame(context_fmt_out, pkt);
if (re != 0) break;
}没有声音
#done
- 主要原因是因为,音频的pts和视频的pts不同步,接收的pkt包只有一个pts,但这个pkt包可能是音频的,也可能是视频的。
这里计算出截断部分:音频起始的pts、视频起始的pts
// 截断处理,计算pts
long long sec_begin = 10.0;
long long sec_end = 20.0;
long long pts_begin_audio = 0; // 截断音频流起始pts
long long pts_begin_video = 0; // 截断视频流起始pts
long long pts_end_video = 0; // 截断视频流结束pts,音频以视频为准
if (vs_in && vs_in->time_base.den > 0) {
auto time_base_of_1 = vs_in->time_base.den / vs_in->time_base.num; // num 分子, den 分母
// pts = sec / time_base
pts_begin_video = sec_begin * time_base_of_1;
pts_end_video = sec_end * time_base_of_1;
}
if (as_in && as_in->time_base.den > 0) {
auto time_base_of_1 = as_in->time_base.den / as_in->time_base.num; // num 分子, den 分母
pts_begin_audio = sec_begin * time_base_of_1;
}由于av_seek_frame只能处理一个流,所以这里选择视频
- 音频部分通过pts的偏移量
pkt->pts < pts_begin_audio来判断10s后的音频,20s时会因为视频break一起退出接收pkt包的 - 至于重新计算写入pkt的pts、dts、duration是因为兼容性的需求,其实是可以直接播放的
// 视频流开头移动到第10秒
re = av_seek_frame(context_fmt_in,vs_in->index,pts_begin_video,AVSEEK_FLAG_FRAME| AVSEEK_FLAG_BACKWARD);
CERR(re);
long long pts_offset_video = -1;
long long pts_offset_audio = -1;
while (true)
{
re = av_read_frame(context_fmt_in,pkt); // 读取输入文件的pkt
if (re != 0) break;
// 重新分配数据包的pts、dts、duration
if (vs_in && pkt->stream_index == vs_in->index) // 视频
{
// 视频从超过20秒的部分不写入,退出
if (pkt->pts > pts_end_video) {
av_packet_unref(pkt);
break;
}
if (pts_offset_video == -1) {
pts_offset_video = pts_begin_video; // 更新当前偏移量
cout << "first video pts = " << pts_offset_video << endl;
}
// 重新写入pts、dts、duration(这一步主要是为了兼容性)
pkt->pts = av_rescale_q_rnd(pkt->pts - pts_offset_video, vs_in->time_base, vs_out->time_base, (AVRounding)(AV_ROUND_INF | AV_ROUND_PASS_MINMAX));
pkt->dts = av_rescale_q_rnd(pkt->dts - pts_offset_video, vs_in->time_base, vs_out->time_base, (AVRounding)(AV_ROUND_INF | AV_ROUND_PASS_MINMAX));
pkt->duration = av_rescale_q(pkt->duration, vs_in->time_base, vs_out->time_base);
pkt->pos = -1;
pkt->stream_index = vs_out->index;
}
else if (as_in && pkt->stream_index == as_in->index) // 音频
{
// 接收第一个视频数据包后才接收处理音频数据包
if (pts_offset_video == -1 || pkt->pts < pts_begin_audio) {
continue;
}
if (pts_offset_audio == -1) {
pts_offset_audio = pts_begin_audio;
}
// 重写pts、dts、duration
pkt->pts = av_rescale_q_rnd(pkt->pts - pts_offset_audio, as_in->time_base, as_out->time_base, (AVRounding)(AV_ROUND_INF | AV_ROUND_PASS_MINMAX));
pkt->dts = av_rescale_q_rnd(pkt->dts - pts_offset_audio, as_in->time_base, as_out->time_base, (AVRounding)(AV_ROUND_INF | AV_ROUND_PASS_MINMAX));
pkt->duration = av_rescale_q(pkt->duration, as_in->time_base, as_out->time_base);
pkt->pos = -1;
pkt->stream_index = as_out->index;
}
}封装XFormat、XMux、XRemux
#daily/25/10/30
具体参考 022test_xdemux_xmux

练习:解封装 解码h264 修改尺寸 编码h265 封装
具体查看 023test_encode_h265_xmux

一直出现stream[0] 空指针问题的原因
open 中有nb_stream个数是 2,但调用 set_param 的时候 nb_stream 个数却是0,而且 `streams[0] = nullptr ``
是因为 之前调用了 set_context 这个函数中释放了原有的context,又重新创建了一个context
pkt包写不到输出中
因为 pts 没有处理好
// 截断处理,计算pts
long long pts_begin_audio = 0; // 截断音频流起始pts
long long pts_begin_video = 0; // 截断视频流起始pts
long long pts_end_video = 0; // 截断视频流结束pts,音频以视频为准
long long pts_offset = -1; // pts偏移量
// num 分子, den 分母
// pts = sec / timebase
if (xdemux.video_index() >= 0) {
pts_begin_video = sec_begin * xdemux.timebase_video().den / xdemux.timebase_video().num ;
pts_end_video = sec_end * xdemux.timebase_video().den / xdemux.timebase_video().num;
}
if (xdemux.audio_index() >= 0) {
pts_begin_audio = sec_begin * xdemux.timebase_audio().den / xdemux.timebase_audio().num ;
}音频没有声音
#daily/25/10/31
pts不对,不能和视频用同一个偏移量,音频有自己的offet
// 截断处理,计算pts
long long pts_begin_audio = 0; // 截断音频流起始pts
long long pts_begin_video = 0; // 截断视频流起始pts
long long pts_end_video = 0; // 截断视频流结束pts,音频以视频为准
// num 分子, den 分母
// pts = sec / timebase
if (xdemux.video_index() >= 0) {
pts_begin_video = sec_begin * xdemux.timebase_video().den / xdemux.timebase_video().num ;
pts_end_video = sec_end * xdemux.timebase_video().den / xdemux.timebase_video().num;
}
if (xdemux.audio_index() >= 0) {
pts_begin_audio = sec_begin * xdemux.timebase_audio().den / xdemux.timebase_audio().num ;
}
// ...
// 音频帧
else if (pkt->stream_index == xdemux.audio_index())
{
// 接收第一个视频数据包后才接收处理音频数据包
if (pkt->pts < pts_begin_audio) {
continue;
}
// 重新分配数据包的pts、dts、duration
xmux.UpdatePkt(pkt, pts_begin_audio,xdemux.timebase_audio());
cout << "audio dts = " << pkt->dts << endl;
xmux.Write(pkt);
}bool XMux::UpdatePkt(AVPacket *pkt, long long pts_offset, XRational timebase_in)
{
unique_lock<mutex> lock(mut_); // 线程安全
if (!context_) return false;
AVStream* stream_out = context_->streams[pkt->stream_index];
AVRational timebase_in_av;
timebase_in_av.den = timebase_in.den;
timebase_in_av.num = timebase_in.num;
// 重新写入pts、dts、duration(这一步主要是为了兼容性)
pkt->pts = av_rescale_q_rnd(pkt->pts - pts_offset, timebase_in_av, stream_out->time_base, (AVRounding)(AV_ROUND_INF | AV_ROUND_PASS_MINMAX));
pkt->dts = av_rescale_q_rnd(pkt->dts - pts_offset, timebase_in_av, stream_out->time_base, (AVRounding)(AV_ROUND_INF | AV_ROUND_PASS_MINMAX));
pkt->duration = av_rescale_q(pkt->duration, timebase_in_av, stream_out->time_base);
pkt->pos = -1;
pkt->stream_index = stream_out->index;
return true;
}编码一直send失败
其实是编码器打开失败导致的,这个原因是 context_enc->time_base 没有设置,
编码器至少设置5个参数
context_enc->width
context_enc->height
context_enc->pix_fmt
context_enc->thread_count
context_enc->time_base//////////////////////////////////////////////////////////////////////////////
// 编码初始化
//////////////////////////////////////////////////////////////////////////////
XEncode xenc;
auto context_enc = xenc.Create(AV_CODEC_ID_HEVC, true); // 创建编码器
//context_enc->width = context_fmt_in->streams[xdemux.video_index()]->codecpar->width;
//context_enc->height = context_fmt_in->streams[xdemux.video_index()]->codecpar->height;
context_enc->width = width_dst;
context_enc->height = height_dst;
context_enc->pix_fmt = (AVPixelFormat)context_fmt_in->streams[xdemux.video_index()]->codecpar->format;
context_enc->thread_count = 16;
context_enc->time_base.den = xdemux.timebase_video().den;
context_enc->time_base.num = xdemux.timebase_video().num;
xenc.SetContextOption("preset", "medium");
xenc.SetContextOption("crf", "28");
//avcodec_parameters_from_context(context_fmt_in->streams[xdemux.video_index()]->codecpar, context_enc);
xenc.SetContext(context_enc);
xenc.Open();编码后的视频流还是显示h264
原因是将解封装的视频参数直接拷贝给到了封装中,解封装的格式是h264,但后面转码成h265了,所以要使用avcodec_parameters_from_context这个接口,把编码器中的视频参数拷贝到封装中
AVPacket* pkt = av_packet_alloc();
XMux xmux;
auto context_fmt_out = xmux.Open(argv[2]); // 输出上下文
auto vs_in = context_fmt_in->streams[xdemux.video_index()];
auto as_in = context_fmt_in->streams[xdemux.audio_index()];
auto vs_out = context_fmt_out->streams[xdemux.video_index()];
auto as_out = context_fmt_out->streams[xdemux.audio_index()];
if (xdemux.video_index() >= 0)
{
// 拷贝h265编码的编码参数, 拷贝到封装上下文中
avcodec_parameters_from_context(vs_out->codecpar, context_enc);
//xmux.CopyParams(xmux.video_index(), vs_in->codecpar);
}
if (xdemux.audio_index() >= 0)// 这里只做了视频的转码,所以音频使用源文件的参数就可以了
{
// 拷贝mp4视频解封装的编码参数, 拷贝到封装上下文中
xmux.CopyParams(xmux.audio_index(), as_in->codecpar);
}正确的h265的视频流解码信息显示
变换视频帧大小后pts不一致
因为 frame 和 frame_dst 不是同一个buf,所以需要把frame中的信息同步到frame_dst中,
通过这个接口 av_frame_copy_props(frame_dst, frame);
// scale
av_frame_copy_props(frame_dst, frame);
auto context_sws = sws_getCachedContext(
NULL,
frame->width,frame->height,(AVPixelFormat)frame->format,
width_dst,height_dst, (AVPixelFormat)frame->format,
SWS_BILINEAR,
0,0,0
);
sws_scale(
context_sws,
frame->data,frame->linesize,0,frame->height,
frame_dst->data,frame_dst->linesize
);
auto pkt_recv = xenc.Encode(frame_dst); // 编码h265
if (pkt_recv) {
cout << "video pts = " << pkt_recv->pts << endl;
xmux.Write(pkt_recv);
av_packet_free(&pkt_recv);
}swscale视频大小后,封装后,画面大小还是640x360
因为封装的视频参数中,使用的是编码的视频参数,但编码的参数设置的是解封装获得的宽高,改成目标宽高就可以了
//////////////////////////////////////////////////////////////////////////////
// 编码初始化
//////////////////////////////////////////////////////////////////////////////
XEncode xenc;
auto context_enc = xenc.Create(AV_CODEC_ID_HEVC, true); // 创建编码器
//context_enc->width = context_fmt_in->streams[xdemux.video_index()]->codecpar->width;
//context_enc->height = context_fmt_in->streams[xdemux.video_index()]->codecpar->height;
context_enc->width = width_dst;
context_enc->height = height_dst;
context_enc->pix_fmt = (AVPixelFormat)context_fmt_in->streams[xdemux.video_index()]->codecpar->format;
context_enc->thread_count = 16;
context_enc->time_base.den = xdemux.timebase_video().den;
context_enc->time_base.num = xdemux.timebase_video().num;
xenc.SetContextOption("preset", "medium");
xenc.SetContextOption("crf", "28");
//avcodec_parameters_from_context(context_fmt_in->streams[xdemux.video_index()]->codecpar, context_enc);
xenc.SetContext(context_enc);
xenc.Open();