RTMP協(xié)議介紹
為了替代RMTP協(xié)議姐呐,蘋果出了一個HLS協(xié)議,Adobe已經(jīng)決定不再維護RTMP協(xié)議邢锯;目前國內(nèi)大部分還是使用的RTMP協(xié)議爹梁,它在傳輸和實時性方面都要強于HLS;
作用:用于娛樂直播的或者點播
通信步驟:
先基于socket進行TCP連接急侥;
-
tcp連接之后再進行握手砌滞;
客戶端先發(fā)個c0+c1,服務器收到后往客戶端發(fā)送s0+s1+s2坏怪,客戶端最后發(fā)送個c2贝润,就結束了握手流程;
握手流程 -
握手完成后再確定建立rtmp連接铝宵;
客戶端向服務端發(fā)送個連接到消息打掘;
服務端回可變窗口控制的大小、帶寬和數(shù)據(jù)塊的大小鹏秋,以及連接成功的消息尊蚁;
客戶端也會放一個傳輸塊的最大大小侣夷;
連接流程 -
創(chuàng)建RTMP流横朋,連接之后數(shù)據(jù)以stream的方式進行數(shù)據(jù)交互;
創(chuàng)建rtmp流 -
推流流程
其metaData就是音視頻流的基本信息惜纸,如采樣率陈惰、采樣大小溉仑、通道數(shù)、幀率、分辨率等贮乳;
推流 -
播流流程
播流流程 -
RTMP消息的結構
消息有頭部和body組成
消息格式
basic header:是必須要有的,占用一個字節(jié)爸黄,前兩位赢织,表示格式,后六位表示chunk stream id腺阳;
message header:中有時間戳落君、消息長度、數(shù)據(jù)類型ID亭引、流ID绎速。當因為數(shù)據(jù)量太大而被拆分成多個chunk 的時候,根據(jù)消息是否屬于同一個流焙蚓、同一個類型數(shù)據(jù)纹冤、消息長度相同、時間戳相同购公,決定是否需要這幾個字段是否需要省略萌京;
Extended timestamp:擴展時間戳,當message header中的時間戳3個字節(jié)不足以表示的時候宏浩,就需要這個擴展知残,也就是timestamp=0xFFFFFF的時候;
1.message header和Extended timestamp根據(jù)basic header頭部中的信息決定的比庄;
2.當chunk stream id為0時求妹,basic header占用2個字節(jié),多出字節(jié)用來表示更多的chunk stream id印蔗;
3.當chunk stream id為1時扒最,basic header占用4個字節(jié),多出字節(jié)用來表示更多的chunk stream id华嘹;
4.basic header中的前兩位決定message header的長度吧趣,當fmt == 00,表示message header全有耙厚;當fmt = 01强挫,消息頭部占用7個字節(jié),當fmt = 10薛躬,消息頭部只占用3個字節(jié)俯渤;當fmt = 11,沒有消息頭部型宝;
消息的類型:有三種:控制消息八匠、音視頻數(shù)據(jù)絮爷、命令消息;
set chunk size : 設置chunk包的大小梨树,默認是128字節(jié)坑夯;
abort message:當某個流結束的時候,告訴服務端就不需要接受這個流了抡四;
acknowledgement:協(xié)商從那個起始點開始確認消息柜蜈;
window acknowledgement size :設置滑動窗口的大小
set peer bandwidth :告訴對方本機的最大一次可傳輸?shù)臄?shù)據(jù)量,也就是帶寬指巡;
data message : 就是音視頻數(shù)據(jù)的元數(shù)據(jù)淑履,比如推流前的metaData,AMF0和AMF1是flash數(shù)據(jù)編碼的一種格式藻雪;
shared object message : 共享消息
command message : 命令消息
FLV協(xié)議
FLV是一種文件秘噪,在將視頻文件進行推流時,會先生成flv文件阔涉。所有的rtmp數(shù)據(jù)在flv中都被加了個頭部缆娃,
- FLV文件結構
- 9個字節(jié)頭部:1=F、2=L 瑰排、3=V贯要、4=版本、5=類型椭住、6789=表示頭部的大小崇渗,固定是9 ;
- 其中頭部的第5個字節(jié)中的前五位和第七位是保留位京郑,第6位表示是否有音頻tag宅广,第8位表示是否有視頻tag;
- 后面所有的內(nèi)容就是由pre_tag_size些举、tag組成跟狱,其中pre_tag_size表示前一個tag的大小,占用4字節(jié)户魏;
-
每個tag又由tag_header驶臊、tag_data組成,
tag_header是對tag_data的描述包括:TT(標簽類型)是音頻還是視頻叼丑, datasize(data的長度)关翎、timesta(時間戳)、E(前面時間戳的擴展)鸠信、SID(流id)纵寝;
tag_data分為音頻和視頻數(shù)據(jù):
其中音頻數(shù)據(jù)audio data又由頭部和 aac data組成,頭部是音頻的采樣信息星立,aac data又由音頻配置信息和adts包裝的音頻數(shù)據(jù)爽茴,這個aac data是rtmp協(xié)議真正需要的葬凳;
其中的視頻數(shù)據(jù)video data由頭部和AVCVideoPacket組成,頭部是表示編碼器id和編碼器類型室奏,AVCVideoPacket前面也有一個類型和時間戳沮明,AVCVideoPacket里面就是sps、pps和NAL組成窍奋;
FLV
- FLV 文件分析器
Diffindo下載
- 編譯:在下載文件的gcc目錄下執(zhí)行 ./flv_compile_clang.sh,成功后生成gcc_flv文件夾酱畅;
- 開始分析:FLVParser flv文件路徑 輸出文件路徑琳袄;
使用ffmpeg根據(jù)視頻文件生成flv文件:ffmpeg -i 視頻文件 -f flv 文件路徑;
推流實踐
安裝librtmp:
1.使用brew rtmpdump 安裝librtmp
2.openssl 我使用的源碼的方式直接在文件下面執(zhí)行./Configure && make && make install
- 推流步驟
- 生成獲取FLV文件
二進制讀取方式打開FLV文件纺酸,并且跳過flv的頭部和第一個pre_tag_size窖逗,使用fseek; - 獲取FLV中的音視頻數(shù)據(jù)餐蔬,讀取到RTMPPacket中
- tag的12個字節(jié)是tag的頭部碎紊,從頭部中讀取關鍵信息,再根據(jù)頭部信息的size獲取flv文件里面的頻數(shù)據(jù)到packet->mbody中樊诺;由于FLV是大端存儲仗考,再因為intel處理器是小端讀取,所以在頭部的信息需要將大端轉換成小端進行存儲词爬;
- 設置rtmp頭部類型m_headerType為RTMP_PACKET_SIZE_LARGE秃嗜,用最長的消息長度方式;
- 設置時間戳m_nTimestamp顿膨,音視頻同步使用
- 設置數(shù)據(jù)類型m_packetType
- 設置數(shù)據(jù)大小m_nBodySize
- 初始化librtmp對象: RTMP_Alloc锅锨、RTMP_Init
設置超時時間:rtmp->Link->timeout
設置推流地址 : RTMP_SetupURL
設置是否是推流:RTMP_EnableWrite 設置了就是推流 未設置就是播流
連接流媒體服務器:RTMP_Connect
創(chuàng)建流:RTMP_ConnectStream(); 從0開始,創(chuàng)建流可能會失敗 - 利用librtmp傳輸
rtmp傳輸?shù)臄?shù)據(jù)需要被包裝到RTMPPacket中恋沃,循環(huán)讀取flv文件發(fā)送必搞;
- 初始化RTMPPacket:malloc 分配空間、RTMPPacket_Alloc分配緩沖區(qū)最大傳輸 = 64 x 1024囊咏、RTMPPacket_Reset重置緩沖區(qū)恕洲、m_hasAbsTimestmp不要絕對時間戳、m_nChannel = 0x4;
- 從flv中讀取音視頻數(shù)據(jù)(看第二步)匆笤;
- 判斷rtmp連接是否正常 RMTP_IsConnected
- 發(fā)送數(shù)據(jù)RTMP_Send_Packet研侣,隊列大小等于0就好;
- 由于服務端緩沖區(qū)有限炮捧,所以應該在每發(fā)送一段數(shù)據(jù)后庶诡,就延遲數(shù)據(jù)的播放時長后再發(fā)送下一段數(shù)據(jù),利用當前tag的時間戳減去上一個tag的時間戳 = 需要休眠的時間咆课。調用usleep后末誓,再發(fā)送數(shù)據(jù)扯俱;
- 生成獲取FLV文件
代碼實戰(zhàn)
- 打開flv文件,跳過flv頭部和第一個pre_tag_size喇澡;
static FILE* open_flv(const char *path) {
FILE *file = fopen(path, "rb");
if (!file) {
printf("flv文件打開失敗\n");
return NULL;
}
//跳過flv文件的頭部 9個字節(jié)
fseek(file, 9, SEEK_SET);
// 跳過第一個presize
fseek(file, 4, SEEK_CUR);
return file;
}
- 初始化librtmp對象迅栅,并建立連接
static RTMP* connect_rtmp(char *rtmp_url) {
RTMP *rtmp = NULL;
int result = -1;
rtmp = RTMP_Alloc();
if (rtmp == NULL) {
printf("初始化rtmp 失敗\n");
goto __ERROR;
}
RTMP_Init(rtmp);
rtmp->Link.timeout = 10;
RTMP_SetupURL(rtmp, rtmp_url);
// 確認是推流
RTMP_EnableWrite(rtmp);
result = RTMP_Connect(rtmp, NULL);
if (result < 0) {
printf("rtmp 連接失敗:%s\n", av_err2str(result));
goto __ERROR;
}
result = RTMP_ConnectStream(rtmp, 0);
if (result < 0) {
printf("rtmp 創(chuàng)建流失斍缇痢:%s\n", av_err2str(result));
}
return rtmp;
__ERROR:
if (rtmp) {
RTMP_Close(rtmp);
RTMP_Free(rtmp);
}
return rtmp;
}
- 初始化RTMPPacket對象
static RTMPPacket* init_packet() {
RTMPPacket *packet = NULL;
packet = malloc(sizeof(RTMPPacket));
// 最大傳輸64kb
if (RTMPPacket_Alloc(packet, 64 * 1024) < 0) {
printf("packet緩沖區(qū)分配失敗\n");
RTMPPacket_Free(packet);
return NULL;
}
RTMPPacket_Reset(packet);
packet->m_hasAbsTimestamp = 0;
packet->m_nChannel = 0x4;
return packet;
}
- 從flv文件中讀取tag的數(shù)據(jù)
//讀取文件數(shù)據(jù)
static int read_data_unsigned8(FILE *file,unsigned char *data) {
if(fread(data, 1, 1, file) != 1) {
return -1;
}
return 0;
}
static int read_data_unsigned24(FILE *file,uint32_t *data) {
if (fread(data, 1, 3, file) != 3) {
return -1;
}
// 因為FLV中是大端存儲的 又因為Intel處理器是小端存儲的 所以需要將大端轉換成小端存儲
*data = (*data >> 16 & 0x000000FF) | (*data << 16 & 0x00FF0000) | (*data & 0x0000ff00);
return 0;
}
static int read_timestamp(FILE *file, uint32_t *data) {
if (fread(data, 1, 4, file) != 4) {
return -1;
}
// 因為FLV中是大端存儲的 又因為Intel處理器是小端存儲的 所以需要將大端轉換成小端存儲
// 擴展時間戳解析時放在高8位
*data = (*data >> 16 & 0x000000FF) | (*data << 16 & 0x00FF0000) | (*data & 0x0000ff00) | (*data & 0xff000000);
return 0;
}
static int read_data_to_packet(FILE *file, RTMPPacket **packet) {
// 數(shù)據(jù)類型
uint8_t type;
// tag的大小
uint32_t data_size;
// 時間戳
uint32_t timestamp;
// 流id
uint32_t stream_id;
if (read_data_unsigned8(file, &type) < 0 ||
read_data_unsigned24(file, &data_size) < 0 ||
read_timestamp(file, ×tamp) < 0 ||
read_data_unsigned24(file, &stream_id) < 0) {
printf("讀取flv tag 頭部信息失敹链妗!\n");
goto __ERROR;
}
size_t read_size = fread((*packet)->m_body, 1, data_size, file);
if (read_size != data_size) {
printf("讀取flv 中的數(shù)據(jù)出錯呕屎!\n");
goto __ERROR;
}
// 相當于message的頭部全開啟 占用11個字節(jié)
(*packet)->m_headerType = RTMP_PACKET_SIZE_LARGE;
(*packet)->m_packetType = type;
(*packet)->m_nBodySize = data_size;
(*packet)->m_nTimeStamp = timestamp;
// 跳過4個字節(jié)的pre tag size
fseek(file, 4, SEEK_CUR);
return 0;
__ERROR:
return -1;
}
- 推送數(shù)據(jù)流到rtmp服務
static void push_data(FILE *file, RTMP *rtmp) {
// 延遲時間戳
useconds_t delay_timestamp = 0;
RTMPPacket *packet = init_packet();
packet->m_nInfoField2 = rtmp->m_stream_id;
while (is_living) {
if (read_data_to_packet(file, &packet) < 0) {
printf("從flv文件中讀取信息失敗或者讀取完畢让簿!\n");
RTMPPacket_Free(packet);
break;
}
if (!RTMP_IsConnected(rtmp)) {
printf("連接已斷開!");
break;
}
printf("等待時間 == %d\n", (packet->m_nTimeStamp - delay_timestamp) * 1000);
// usleep 使用的是納秒
usleep((packet->m_nTimeStamp - delay_timestamp) * 1000);
// 開始發(fā)送
RTMP_SendPacket(rtmp, packet, 0);
delay_timestamp = packet->m_nTimeStamp;
}
__ERROR:
RTMPPacket_Free(packet);
}
- 推流總體調用
void start_push(void) {
is_living = 1;
// 1. 打開flv文件
char *path = "/Users/cunw/Desktop/learning/音視頻學習/音視頻文件/iphone.flv";
FILE *file = open_flv(path);
// 2.連接rtmp服務器 本地nginx服務
char *url = "rtmp://localhost/live/1026238004";
RTMP *rtmp = connect_rtmp(url);
// 3.推送數(shù)據(jù)
push_data(file, rtmp);
is_living = 0;
// 4. 釋放資源
if (rtmp) {
RTMP_Close(rtmp);
RTMP_Free(rtmp);
}
}