使用 ortp 发送原始 H.264 码流
oRTP 是一個 RTP (Real-time Transport Protocol (RFC 3550)) 協議的庫實現,它完全以 C 語言來實現,因此方便應用于各種不同的平臺。本文分享用 oRTP 發送,以 Android 的 MediaCodec 編碼出來的原始 H.264 碼流,又稱裸流的方法。
H.264 碼流
MediaCode 以 H.264 編碼格式編碼之后的視頻,是由一個一個的NALU組成的。他們的結構如下圖所示。
其中每個 NALU 之間通過 startcode(起始碼)進行分隔。起始碼分成兩種,一種是 0x000001(3Byte),另一種是 0x00000001(4Byte)。NALU 中,起始碼之后,是 NALU 的類型字節,它用于描述這個 NALU 中數據的類型,NALU 的重要性等。H.264 視頻流的 meta 信息等也被封裝為 NALU,并以特定的類型標識 ,如 SPS 和 PPS 等描述視頻流分辨率、碼率等特性的信息。NALU 類型字節格式如下:
+---------------+|0|1|2|3|4|5|6|7|+-+-+-+-+-+-+-+-+|F|NRI| Type |+---------------+NALU 類型字節中各個字段的語義,在 ITU 的 H.264規范 中有清晰地定義,這里給出它們的簡要說明:
F:1 位
forbidden_zero_bit。H.264 規范聲明,值為 1 時表示語法違規。也就是數據包損壞。NRI:2 位
nal_ref_idc。這個字段用于描述該 NALU 的重要性。值為 00 表示 NALU 的內容不被用于重建圖像預測的參考圖像。這種 NALU 可以被丟棄而不危及參考圖像的完整性。大于 00 的值表示需要解碼該 NALU 來維護參考圖像的完整性。Type:5 位
nal_unit_type。這個組件指定 NALU 載荷的類型,在 H.264 的表 7-1 中定義。具體的類型定義如下:
對于 NALU 的起始碼,如果它對應的 Slice 為一幀的開始就用0x00000001,否則就用 0x000001。H.264 碼流解析的步驟就是首先從碼流中搜索 0x000001 和 0x00000001,分離出 NALU;然后再分析NALU的各個字段。在 MediaCodec API 的輸出中,通常都是一幀的圖像被編碼為一個 NALU。不同類型的 Meta 信息也會被編碼為不同的 NALU,如 SPS,PPS 等。
H.264 碼流的分辨率大小不同,NALU 的類型不同等因素,導致 NALU 有著各種不同的大小。RTP 協議通常為了更高的實時性,而會選擇用 UDP 作為傳輸層協議。但 UDP 包的大小受限于 IP 的 MTU,也就是 UDP 包加上 IP 頭不能超過 IP 層的 MTU 值大小。因此在用 RTP 傳輸 H.264 碼流時,需要適配 RTP 包的大小限制。
NALU 適配 RTP 包大小,需要分為多種情況來處理:
1. NALU 非常小,多個 NALU 可以放在一個 RTP 包中傳輸,為了不浪費傳輸能力,通常需要把它們聚合在一個包中傳輸。
2. NALU 大小與 RTP 包大小限制在同一量級,一個 RTP 包中可以放一個 NALU,但不能放多個。
3. NALU 比較大,分辨率較高的視頻,比如 1080P 的視頻,編碼出來的一幀圖像可能在近 10 KB 到一兩百 KB 之間,這種就需要把一個 NALU 放進多個 RTP 包中傳輸。
在用 RTP 傳輸 H.264 碼流時,會為每個載荷加上一到兩個字節的 RTP載荷頭部,用于區分前面提到的這多種不同的情況。關于具體的 H.264 視頻的 RTP 載荷格式,可以參考 H.264 視頻的 RTP 載荷格式 一文,或者 IETF 的 RFC6184。
然后來看使用 ortp 發送原始 H.264 碼流的方法。
oRTP 源碼下載
首先需要下載 oRTP 的源碼,下載地址如下:
http://www.linphone.org/technical-corner/ortp/downloads可以通過 Git 下載最新版本的源碼:
git clone git://git.linphone.org/ortp.git還可以下載不同發布版本打包的 .tar.gz 包:
http://download.savannah.nongnu.org/releases/linphone/ortp/sources/最新版為 ortp-0.27.0.tar.gz。
oRTP 這個項目已經針對 Android 做了移植。得到源碼之后,可以在
ortp/build/android 目錄下找到 Android.mk 文件,可以借助于這個文件,將 oRTP 的代碼集成進自己的 JNI 代碼或 Android 的代碼庫中。
使用 ortp 發送原始 H.264 碼流
使用 ortp 發送原始 H.264 碼流主要需要兩步,首先是初始化 RtpSession:
#define Y_PLOAD_TYPE 96 #define DefaultTimestampIncrement 1500 //(90000 / framerate)static const unsigned char RtpPayloadTypeNaluMin = 1; static const unsigned char RtpPayloadTypeNaluMax = 23; static const unsigned char RtpPayloadTypeStapA = 24; static const unsigned char RtpPayloadTypeStapB = 25; static const unsigned char RtpPayloadTypeMtap16 = 26; static const unsigned char RtpPayloadTypeMtap24 = 27; static const unsigned char RtpPayloadTypeFuA = 28; static const unsigned char RtpPayloadTypeFuB = 29;typedef enum {NALU_TYPE_SLICE = 1,NALU_TYPE_DPA = 2,NALU_TYPE_DPB = 3,NALU_TYPE_DPC = 4,NALU_TYPE_IDR = 5,NALU_TYPE_SEI = 6,NALU_TYPE_SPS = 7,NALU_TYPE_PPS = 8,NALU_TYPE_AUD = 9,NALU_TYPE_EOSEQ = 10,NALU_TYPE_EOSTREAM = 11,NALU_TYPE_FILL = 12, } NaluType;static const unsigned char FUHeaderMaskStart = 0x80; static const unsigned char FUHeaderMaskEnd = 0x40; static const unsigned char FUHeaderMaskType = 0x1F;int cond = 1;void stop_handler(int signum) {if (cond == 1) {cond = 0;} else {exit(1);} }void ssrc_cb(RtpSession *session) {printf("hey, the ssrc has changed !\n"); }static RtpSession * init_rtp_session(int rtp_port) {RtpSession *session = NULL;bool_t adapt = TRUE;int jittcomp = 40;char *ssrc;ortp_init();ortp_scheduler_init();ortp_set_log_level_mask(ORTP_DEBUG | ORTP_MESSAGE | ORTP_WARNING | ORTP_ERROR);session = rtp_session_new(RTP_SESSION_SENDRECV);rtp_session_set_scheduling_mode(session, 1);rtp_session_set_blocking_mode(session, 1);rtp_session_set_remote_addr(session, "10.242.55.30", rtp_port);rtp_session_set_connected_mode(session, TRUE);rtp_session_set_payload_type(session, Y_PLOAD_TYPE);ssrc = getenv("SSRC");if (ssrc != NULL) {printf("using SSRC=%i.\n", atoi(ssrc));// 設置輸出流的SSRC。不做此步的話將會給個隨機值rtp_session_set_ssrc(session, atoi(ssrc));}return session; }RTP 的使用模式,通常是接收者先 listen 在特定的端口上,然后發送者向該端口發送數據。rtp_session_set_remote_addr() 用于設置碼流的接收端地址。
需要特別說明的一點是載荷類型的設置,rtp_session_set_payload_type(session, Y_PLOAD_TYPE);,這里傳入了 96。載荷類型用于描述某種特定載荷的一些特性,如 MimeType、時鐘頻率、比特率等。在 RFC3551 RTP Profile for Audio and Video Conferences with Minimal Control 中定義了為具體的載荷類型分配的載荷類型編號。
在 oRTP 中,預定義了許多載荷類型的描述,如 H.263,PCMU8000,H.264 等(在文件 ortp/src/avprofile.c 中):
PayloadType payload_type_pcmu8000={TYPE(PAYLOAD_AUDIO_CONTINUOUS),CLOCK_RATE(8000),BITS_PER_SAMPLE(8),ZERO_PATTERN( &offset127),PATTERN_LENGTH(1),NORMAL_BITRATE(64000),MIME_TYPE("PCMU"),CHANNELS(1),RECV_FMTP(NULL),SEND_FMTP(NULL),NO_AVPF,FLAGS(0) };PayloadType payload_type_h263={TYPE(PAYLOAD_VIDEO),CLOCK_RATE(90000),BITS_PER_SAMPLE(0),ZERO_PATTERN(NULL),PATTERN_LENGTH(0),NORMAL_BITRATE(256000),MIME_TYPE("H263"),CHANNELS(0),RECV_FMTP(NULL),SEND_FMTP(NULL),NO_AVPF,FLAGS(0) };PayloadType payload_type_h264={TYPE(PAYLOAD_VIDEO),CLOCK_RATE(90000),BITS_PER_SAMPLE(0),ZERO_PATTERN(NULL),PATTERN_LENGTH(0),NORMAL_BITRATE(256000),MIME_TYPE("H264"),CHANNELS(0),RECV_FMTP(NULL),SEND_FMTP(NULL),AVPF(PAYLOAD_TYPE_AVPF_FIR | PAYLOAD_TYPE_AVPF_PLI, RTCP_DEFAULT_REPORT_INTERVAL),FLAGS(PAYLOAD_TYPE_RTCP_FEEDBACK_ENABLED) };此外,還定義了一個表,基于 RFC3551 建立了載荷類型編號與載荷類型之間的映射關系,具體是在文件 ortp/src/avprofile.c 中的 av_profile_init() 函數里:
void av_profile_init(RtpProfile *profile) {rtp_profile_clear_all(profile);profile->name="AV profile";rtp_profile_set_payload(profile,0,&payload_type_pcmu8000);rtp_profile_set_payload(profile,1,&payload_type_lpc1016);rtp_profile_set_payload(profile,3,&payload_type_gsm);rtp_profile_set_payload(profile,7,&payload_type_lpc);rtp_profile_set_payload(profile,4,&payload_type_g7231);rtp_profile_set_payload(profile,8,&payload_type_pcma8000);rtp_profile_set_payload(profile,9,&payload_type_g722);rtp_profile_set_payload(profile,10,&payload_type_l16_stereo);rtp_profile_set_payload(profile,11,&payload_type_l16_mono);rtp_profile_set_payload(profile,13,&payload_type_cn);rtp_profile_set_payload(profile,18,&payload_type_g729);rtp_profile_set_payload(profile,31,&payload_type_h261);rtp_profile_set_payload(profile,32,&payload_type_mpv);rtp_profile_set_payload(profile,34,&payload_type_h263);rtp_profile_set_payload(profile,96,&payload_type_t140);rtp_profile_set_payload(profile,97,&payload_type_t140_red); }av_profile_init() 函數在 ortp 庫初始化時會被調用到:
void ortp_init() {if (ortp_initialized++) return;#ifdef _WIN32win32_init_sockets(); #endifav_profile_init(&av_profile);ortp_global_stats_reset();init_random_number_generator();ortp_message("oRTP-" ORTP_VERSION " initialized."); }為 RtpSession 設置的載荷類型對數據收發的過程有一定的影響。
通過 oRTP 收發數據時,需要為其傳入用戶時間戳,oRTP 會根據為 RtpSession 設置的載荷類型找到描述載荷類型的PayloadType,并根據 PayloadType 的時鐘頻率和用戶時間戳,計算出數據收發的時間間隔。以此實現用戶對數據收發頻率的控制。如(ortp/src/rtpsession.c):
/* function used by the scheduler only:*/ uint32_t rtp_session_ts_to_time (RtpSession * session, uint32_t timestamp) {PayloadType *payload;payload =rtp_profile_get_payload (session->snd.profile,session->snd.pt);if (payload == NULL){ortp_warning("rtp_session_ts_to_t: use of unsupported payload type %d.", session->snd.pt);return 0;}/* the return value is in milisecond */return (uint32_t) (1000.0 *((double) timestamp /(double) payload->clock_rate)); }在 RFC3551 中,載荷類型 96 是動態映射的類型,通常由特定的應用字節決定。如在 oRTP 中,這個類型是被映射為 T140 的,但也常將 96 映射到 H.264。為了讓我們前面設置的載荷類型能夠正常工作,還需要修改 oRTP 的源碼 ortp/src/avprofile.c 中的 av_profile_init() 函數,把如下這一行
rtp_profile_set_payload(profile,96,&payload_type_t140);改為
rtp_profile_set_payload(profile,96,&payload_type_h264);初始化了 RtpSession 之后,就可以發送 H.264 裸流了:
static bool isNalu3Start(unsigned char *buffer) {if (buffer[0] != 0 || buffer[1] != 0 || buffer[2] != 1) {return false;} else {return true;} }static bool isNalu4Start(unsigned char *buffer) {if (buffer[0] != 0 || buffer[1] != 0 || buffer[2] != 0 || buffer[3] != 1) {return false;} else {return true;} }static void forward_frame(RtpSession * session, uint8_t * buffer, int len,uint32_t userts) {unsigned char NALU = buffer[4];uint32_t valid_len = len - 4;if (valid_len <= MAX_RTP_PKT_LENGTH) {int offset = 0;int lastNaluStartPos = -1;while (offset < len) {if (isNalu4Start(buffer + offset)) {if (lastNaluStartPos >= 0) {rtp_session_send_with_ts(session, buffer + lastNaluStartPos,offset - lastNaluStartPos, userts);}lastNaluStartPos = offset + 4;offset += 3;} else if (isNalu3Start(buffer + offset)) {if (lastNaluStartPos >= 0) {rtp_session_send_with_ts(session, buffer + lastNaluStartPos,offset - lastNaluStartPos, userts);}lastNaluStartPos = offset + 3;offset += 2;}++offset;}rtp_session_send_with_ts(session, buffer + lastNaluStartPos,len - lastNaluStartPos, userts);} else {valid_len -= 1;int packetnum = valid_len / MAX_RTP_PKT_LENGTH;if (valid_len % MAX_RTP_PKT_LENGTH != 0) {packetnum += 1;}int i = 0;int pos = 5;while (i < packetnum) {if (i < packetnum - 1) {buffer[pos - 2] = (NALU & 0x60) | 28;buffer[pos - 1] = (NALU & 0x1f);if (0 == i) {buffer[pos - 1] |= 0x80;}rtp_session_send_with_ts(session, &buffer[pos - 2],MAX_RTP_PKT_LENGTH + 2, userts);} else {int iSendLen = len - pos;buffer[pos - 2] = (NALU & 0x60) | 28;buffer[pos - 1] = (NALU & 0x1f);buffer[pos - 1] |= 0x40;rtp_session_send_with_ts(session, &buffer[pos - 2],iSendLen + 2, userts);}pos += MAX_RTP_PKT_LENGTH;++i;}} }這里根據 RTP 載荷格式的規范,將 NALU 轉為 RTP 的載荷,并發送。需要特別說明的是,從 MediaCodec 拿到的第一個 Buffer,其內容通常像下面這樣:
00000000 00 00 00 01 67 42 80 2A DA 01 10 0F 1E 5E 52 0A ....gB.*.....^R. 00000010 0C 0A 0D A1 42 6A 00 00 00 01 68 CE 06 E2其中包含了類型分別為 SPS 和 PPS 的兩個 NALU。要發送這塊 Buffer,可以按照 H.264 的 RTP 載荷格式規范中描述的,單時間聚合包的格式來發送,或者拆分為兩個 RTP 包來發送。
RTP 是一個用于流媒體傳輸的協議,而不是流媒體方案。要想使 RTP 在實際的項目中用起來,當然還是有許多其它工作要做的。
參考文檔
視音頻數據處理入門:H.264視頻碼流解析
ORTP移植到Hi3518e,h.264封包rtp發送
Done。
總結
以上是生活随笔為你收集整理的使用 ortp 发送原始 H.264 码流的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 原始 H.264 码流播放
- 下一篇: live555 源码分析:简介