使用ffmpeg新增rtsp字幕流 (t140)
如有錯誤請指正,謝謝。
使用ffmpeg新增rtsp字幕流 (t140)
使用ffmpeg推送一個視訊檔案到rtsp非常簡單:
ffmpeg -re -i subtitle.mkv -vcodec copy -acodec copy -rtsp_transport tcp -f rtsp rtsp://127.0.0.1:10554/sVideo
但如果想要把視訊中的字幕流也推送到rtsp伺服器上卻不行:
ffmpeg -re -i subtitle.mkv -vcodec copy -acodec copy -scodec copy -rtsp_transport tcp -f rtsp rtsp://127.0.0.1:10554/sVideo
提示:
[rtp @ 0503fd00] Unsupported codec ass
所以要使用ffmpeg推送rtsp字幕流必須要修改原始碼, 關於rtp載荷文字流的協議描述為RFC4103。
1.使rtp打包支援subtitle形式的編碼
要使rtp打包時支援相關字幕編碼器, 必須修改rtpenc.c中的 is_supported函式, 在最後加入AV_CODEC_ID_ASS(ass字幕), AV_CODEC_ID_TEXT(純文字字幕流)
static int is_supported(enum AVCodecID id) { switch(id) { case AV_CODEC_ID_DIRAC: case AV_CODEC_ID_H261: ... case AV_CODEC_ID_TEXT: case AV_CODEC_ID_ASS: return 1; default: return 0; } }
2.使建立sdp時生成subtitle相關欄位
rtsp對可用媒體流的數量及屬性判斷是通過SDP, 所以有字幕流的rtsp,需要在建立rtsp時一併將字幕流的資訊也建立完成。
修改sdp.c sdp_write_media_attributes函式,指定subtitle編碼器對應的流媒體描述。
指定AV_CODEC_ID_TEXT編碼器的名稱為t140, AV_CODEC_ID_ASS編碼器的名稱為ass。
ffmpeg解析rtsp的sdp時會根據rtpmap中的編碼器名稱去尋找對應的編碼屬性,生成相關的媒體流。
sdp.c:sdp_write_media_attributes 函式修改節選
switch (p->codec_id) {
case AV_CODEC_ID_DIRAC:
av_strlcatf(buff, size, "a=rtpmap:%d VC2/90000\r\n", payload_type);
break;
case AV_CODEC_ID_H264: {
int mode = 1;
if (fmt && fmt->oformat && fmt->oformat->priv_class &&
av_opt_flag_is_set(fmt->priv_data, "rtpflags", "h264_mode0"))
mode = 0;
if (p->extradata_size) {
config = extradata2psets(fmt, p);
}
av_strlcatf(buff, size, "a=rtpmap:%d H264/90000\r\n"
"a=fmtp:%d packetization-mode=%d%s\r\n",
payload_type,
payload_type, mode, config ? config : "");
break;
}
......
case AV_CODEC_ID_TEXT:
av_strlcatf(buff, size, "a=rtpmap:%d t140/1000\r\n", payload_type);
break;
case AV_CODEC_ID_ASS:
av_strlcatf(buff, size, "a=rtpmap:%d ass/1000\r\n", payload_type);
break;
default:
/* Nothing special to do here... */
break;
截止目前,使用ffmpeg推送rtsp字幕流的原始碼修改已經完成。
但若想使用ffmpeg播放非t140字幕編碼的字幕流, 還差最後一步。
3.使sdp中的subtitle可以正確被解析
假設有以下sdp資訊:
v=0
o=- 0 0 IN IP4 127.0.0.1
s=No Name
c=IN IP4 192.168.1.180
t=0 0
a=tool:libavformat 56.40.101
m=video 0 RTP/AVP 96
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1; sprop-parameter-sets=Z24AMqbMpgHgCJ+XARAAGXTwBMS0CPGDGaA=,aOl4/LA=;
profile-level-id=6E0032
a=control:streamid=0
m=audio 0 RTP/AVP 97
a=rtpmap:97 MPEG4-GENERIC/48000/2
a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;
config=119056E500
a=control:streamid=1
m=text 0 RTP/AVP 98
a=rtpmap:98 t140/1000
m=text 0 RTP/AVP 99
a=rtpmap:99 ass/1000
以上文字資訊描述了兩個subtitle流:一個文字流(t140), 一個ass字幕流(ass)。
ffmpeg在解析sdp時會建立兩個AVStream, 該AVStream的codec_type為AVMEDIA_TYPE_SUBTITLE, codec_id會呼叫 ff_rtp_handler_find_by_name 方法進行尋找。
看下ff_rtp_handler_find_by_name的具體實現
const RTPDynamicProtocolHandler *ff_rtp_handler_find_by_name(const char *name,
enum AVMediaType codec_type)
{
void *i = 0;
const RTPDynamicProtocolHandler *handler;
while (handler = ff_rtp_handler_iterate(&i)) {
if (handler->enc_name &&
!av_strcasecmp(name, handler->enc_name) &&
codec_type == handler->codec_type)
return handler;
}
return NULL;
}
ff_rtp_handler_iterate實現:
const RTPDynamicProtocolHandler *ff_rtp_handler_iterate(void **opaque)
{
uintptr_t i = (uintptr_t)*opaque;
const RTPDynamicProtocolHandler *r = rtp_dynamic_protocol_handler_list[i];
if (r)
*opaque = (void*)(i + 1);
return r;
}
即通過遍歷rtpdec.c中的rtp_dynamic_protocol_handler_list去尋找符合要求的RTPDynamicProtocolHandler,所以我們需要在rtpdec.c中新增有關ass編碼器的RTPDynamicProtocolHandler,同時將對應的地址加入到list中.
注意:
或許因為rfc標準的原因,t140對應的動態協議已經被實現了, 所以若是推送的rtsp字幕流的編碼器名稱為t140,則無需修改ffmpeg原始碼也可以進行解析。
static RTPDynamicProtocolHandler t140_dynamic_handler = { /* RFC 4103 */
.enc_name = "t140",
.codec_type = AVMEDIA_TYPE_SUBTITLE,
.codec_id = AV_CODEC_ID_TEXT,
};
static RTPDynamicProtocolHandler ass_dynamic_handler = {
.enc_name = "ass",
.codec_type = AVMEDIA_TYPE_SUBTITLE,
.codec_id = AV_CODEC_ID_ASS,
};
static const RTPDynamicProtocolHandler *rtp_dynamic_protocol_handler_list[] = {
/* rtp */
&ff_ac3_dynamic_handler,
......
&t140_dynamic_handler,
&ass_dynamic_handler,
......
};
至此,修改完畢。
4.程式碼新增text字幕流
一段程式碼節選,新增一個字幕流到rtsp流中:
bool RtspPusher::AddInfoStream(unsigned &streamIndex)
{
if(!m_outputCtx) //AVFormatContext
return false;
AVStream * outStream = avformat_new_stream(m_outputCtx, NULL);
AVCodec * encoder = avcodec_find_encoder(AV_CODEC_ID_TEXT);
if(encoder == NULL)
return false;
AVCodecContext * encoderContext = avcodec_alloc_context3(encoder);
int ret = avcodec_parameters_from_context(outStream->codecpar, encoderContext);
avcodec_free_context(&encoderContext);
if(ret < 0)
return false;
outStream->codecpar->codec_type = AVMEDIA_TYPE_SUBTITLE;
streamIndex = m_outputCtx->nb_streams - 1;
return true;
}