RTP实战-----RTP封装h264码流
编译中虽然已经添加了jrtp库和jthread库,但是运行还是需要在本运行终端添加环境(临时运行方法,如果不想这么干的可能放到系统环境中)如下INC1、INC2、LIB1、LIB2变量都是自己安装jthread以及jrtp相对应的头文件和库(一定要对应的,特别是头文件)可以看出来库路径都已找到,然后执行文件是在out目录下built-in.o。安装jthread以及jrtp参考我的另外一篇。在工程
·
错误以及崩溃已修改,其中最大问题是DEFAULT_BUFF_MAX_SIZE ,默认读取一个NALU可能超过350000(我自测的mp4视频文件,可能还更大)
1.整体目录结构
root@ubuntu:/home/sulier/work/AudioVedio/RTPZip/testRTP# tree
.
├── base.h
├── h264.cpp
├── h264.h
├── main.cpp
├── makefile
├── rtp.h
└── script
└── makefile.build
2. 代码段
base.h
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <iostream>
#include <string>
using namespace std;
rtp.h
#ifndef __RTP_H__
#define __RTP_H__
#define MTU_SIZE 1500
#define MAX_RTP_PACKET_LENGTH 1360
#endif
h264.h
#ifndef __H264_H__
#define __H264_H__
#include"base.h"
#define D_H264_SAMPLE_RATE 90000.0
#define I_H264_SAMPLE_RATE 90000
#define H264_PAYLOAD_TYPE 96
#define H264_FRAME_RATE 25
#define D_H264_FRAME_RATE 25.0
#define DEFAULT_BUFF_MAX_SIZE 350000
//定义整装NALU时NALU头结构体
typedef struct stNaluHead_t
{
uint8_t uiForbidden:1; //h264要求规范为0
uint8_t uiNRI:2; //NALU的重要性 0~3 值越大越重要
uint8_t uiType:5; //1~23 下图中
}stNaluHead;
#define TRANFER_NALU_HEAD(head) \
(head.uiForbidden<<7 | head.uiNRI<<5 | head.uiType)
/******************uiType数值含义******************
uiType 含义
0 未指定
1 非 IDR 图像的片
5 IDR 图像的片
6 SEI(辅助增强信息)
7 SPS(序列参数集)
8 PPS(图像参数集)
24 STAP-A(单一时间组合包模式 A,用于一个 RTP 包荷载多个 NALU)
25 STAP-B(单一时间组合包模式 B)
26 MTAP16(多个时间的组合包模式 A)
27 MTAP24(多个时间的组合包模式 B)
28 FU-A(分片模式 A,用于将单个 NALU 分到多个 RTP 包)
29 FU-B(分片模式 B)
***************************************************/
//定义NALU分片时FU indicator 结构体,分片时位于FU header前
typedef struct stFuIndicator_t
{
uint8_t uiForbidden:1; //h264要求规范为0
uint8_t uiNRI:2; //NALU的重要性 0~3 值越大越重要
uint8_t uiType:5; //28-29 上图中
}stFuIndicator;
#define TRANFER_FU_INDICATOR(head) \
(head.uiForbidden<<7 | head.uiNRI<<5 | head.uiType)
//定义NALU分片时FU header结构体
typedef struct stFuHead_t
{
uint8_t uiStart:1; //1代表开始的第一个分片
uint8_t uiEnd:1; //1代表最后一个分片
uint8_t uiReverse:1; //保留位
uint8_t uiType:5; //1~23 上图中
}stFuHead;
#define TRANFER_FU_HEAD(head) \
(head.uiStart<<7 | head.uiEnd<<6 | head.uiReverse<<5 | head.uiType)
/******************NALU包******************
***************************************************
1.startcode有两种,这是包开始
0x00 0x00 0x00 0x01
0x00 0x00 0x01
2.接下来就是上面的NALU头
uiForbidden规范为0,下面都不做解释
《A》可能是整装:
头结构只有一个NALU头结构体比如0x67就是
二进制0 1 1 0 0 1 1 1
uiForbidden uiNRI uiType
代表重要性值为3(非常重要) 类型为7(SPS序列参数集)
《B》可能是分片:
头结构第一个FU indicator 结构体比如0x5c就是
二进制0 1 0 1 1 1 0 0
uiForbidden uiNRI uiType
代表重要性值为2(重要) 类型为28(FU-A)
头结构第二个就是FU header结构体
3.最后是payload数据
***************************************************
*******************************************/
typedef struct stNalu_t{
int iStartCodePreLen;
uint8_t uiForbidden;
uint8_t uiNRI;
uint8_t uiType;
int iMaxSize;
uint8_t *pBuff;
int iNaluHeadLen; //h264码流中nalu头长度,固定为1
int iPayloadLen; //payload长度
}stNalu;
stNalu *AllocNalu_T(int iMaxSize);
stNalu *AllocNalu();
void FreeNalu(stNalu *pNalu);
bool isStartCode3(uint8_t *pBuff);
bool isStartCode4(uint8_t *pBuff);
int GetAnnexbNalu(FILE *F, stNalu *pNalu);
#endif
h264.cpp
#include"h264.h"
stNalu *AllocNalu_T(int iMaxSize)
{
stNalu *pNalu = (stNalu *)malloc(sizeof(stNalu));
if (pNalu == NULL)
{
return NULL;
}
pNalu->pBuff = (uint8_t *)malloc(iMaxSize);
if (pNalu->pBuff == NULL)
{
free(pNalu);
pNalu = NULL;
return NULL;
}
pNalu->iMaxSize = iMaxSize;
return pNalu;
}
stNalu *AllocNalu()
{
return AllocNalu_T(DEFAULT_BUFF_MAX_SIZE);
}
void FreeNalu(stNalu *pNalu)
{
if (pNalu != NULL)
{
if (pNalu->pBuff != NULL)
{
free(pNalu->pBuff);
pNalu->pBuff = NULL;
}
free(pNalu);
pNalu = NULL;
}
return;
}
bool isStartCode3(uint8_t *pBuff)
{
if (pBuff[0] == 0x00 && pBuff[1] == 0x00 && pBuff[2] == 0x01)
{
return true;
}
return false;
}
bool isStartCode4(uint8_t *pBuff)
{
if (pBuff[0] == 0x00 && pBuff[1] == 0x00 && pBuff[2] == 0x00 && pBuff[3] == 0x01)
{
return true;
}
return false;
}
int GetAnnexbNalu(FILE *F, stNalu *pNalu)
{
memset(pNalu->pBuff, 0, DEFAULT_BUFF_MAX_SIZE);
if (F == NULL || pNalu == NULL || feof(F))
{
return -1;
}
//一次读取3个字节
int iRead = fread(pNalu->pBuff, 1, 3, F);
if (iRead != 3)
{
return -1;
}
if (isStartCode3(pNalu->pBuff) == true)
{
pNalu->iStartCodePreLen = 3;
}
else
{
iRead = fread(pNalu->pBuff + 3, 1, 1, F);
if (iRead != 1)
{
return -1;
}
if (isStartCode4(pNalu->pBuff) == true)
{
pNalu->iStartCodePreLen = 4;
}
else
{
return 0;
}
}
bool bNextStartCode = false;
int pos = pNalu->iStartCodePreLen;
int iNextStartCodeLen = 0;
while (bNextStartCode == false)
{
if (pos > DEFAULT_BUFF_MAX_SIZE) cout<<"malloc not enough"<<endl;
if (feof(F) || pos > DEFAULT_BUFF_MAX_SIZE)
{
break;
}
pNalu->pBuff[pos] = fgetc(F);
//获取下一个startcode的位置,当然有可能是3个字节也有可能4个字节
if (pNalu->pBuff[pos] == 0x01 && pNalu->pBuff[pos - 1] == 0x00 && pNalu->pBuff[pos -2] == 0x00)
{
bNextStartCode = true;
iNextStartCodeLen = 3;
if (pNalu->pBuff[pos -3] == 0x00)
{
iNextStartCodeLen = 4;
}
}
pos++;
}
pos = pos - iNextStartCodeLen;
//已经读取了下一个包的startcode,所以需要回退iNextStartCodeLen字节
if (bNextStartCode == true)
{
if (fseek(F, -iNextStartCodeLen, SEEK_CUR) != 0)
{
return -1;
}
}
pNalu->uiForbidden = pNalu->pBuff[pNalu->iStartCodePreLen] & 0x80;
pNalu->uiNRI = pNalu->pBuff[pNalu->iStartCodePreLen] & 0x60;
pNalu->uiType = pNalu->pBuff[pNalu->iStartCodePreLen] & 0x1f;
pNalu->iNaluHeadLen = 1; //固定1个字节的头
pNalu->iPayloadLen = pos - pNalu->iStartCodePreLen - pNalu->iNaluHeadLen;
return pNalu->iPayloadLen;
}
main.cpp
#include "rtpsession.h"
#include "rtpudpv4transmitter.h"
#include "rtpipv4address.h"
#include "rtpsessionparams.h"
#include "rtperrors.h"
#include "rtplibraryversion.h"
#include "base.h"
#include "h264.h"
#include "rtp.h"
using namespace jrtplib;
void checkerror(int rtperr)
{
if (rtperr < 0)
{
cout << "ERROR: " << RTPGetErrorString(rtperr) << endl;
exit(-1);
}
}
int main()
{
RTPSession cSess;
uint16_t uiPortBase,uiDestPort;
uint32_t uiDestIp;
string strIp, strH264File;
int iStatus,i,num;
cout << "Using version " << RTPLibraryVersion::GetVersion().GetVersionString() << endl;
//本地输出端口
cout << "Enter local portbase:" << endl;
cin >> uiPortBase;
cout << endl;
//本地输入h264视频流文件
cout << "Enter h264 file name include path:" << endl;
cin >> strH264File;
cout << endl;
//输出至远程IP地址
cout << "Enter the destination IP address" << endl;
cin >> strIp;
uiDestIp = inet_addr(strIp.c_str());
if (uiDestIp == INADDR_NONE)
{
cerr << "Bad IP address specified" << endl;
return -1;
}
uiDestIp = ntohl(uiDestIp);
//输出至远程端口
cout << "Enter the destination port" << endl;
cin >> uiDestPort;
//传输协议以及会话参数设置
RTPUDPv4TransmissionParams transparams;
RTPSessionParams sessparams;
//设置自己的时间戳单元,1/sampleRate--也就是采样率
sessparams.SetOwnTimestampUnit(1.0/D_H264_SAMPLE_RATE);
transparams.SetPortbase(uiPortBase);
iStatus = cSess.Create(sessparams, &transparams);
checkerror(iStatus);
RTPIPv4Address addr(uiDestIp,uiDestPort);
iStatus = cSess.AddDestination(addr);
checkerror(iStatus);
cSess.SetDefaultPayloadType(H264_PAYLOAD_TYPE);
cSess.SetDefaultMark(false);
cSess.SetDefaultTimestampIncrement(I_H264_SAMPLE_RATE/H264_FRAME_RATE);
FILE *fH264 = fopen(strH264File.c_str(), "rb");
if (fH264 == NULL)
{
cerr<<"bad file"<<endl;
return -1;
}
stNalu *pNalu = AllocNalu();
uint8_t SendBuff[MTU_SIZE] = {0};
RTPTime delay(1.0/D_H264_FRAME_RATE);
while(true)
{
if(feof(fH264))
{
break;
//fseek(fH264, 0, SEEK_SET);
}
//返回的是payload的长度
int iSize = GetAnnexbNalu(fH264, pNalu);
if (iSize == 0)
{
break;
}
else if (iSize < 0)
{
FreeNalu(pNalu);
exit(0);
}
else
{
if (iSize <= MAX_RTP_PACKET_LENGTH - 1) //预留一个字节存储stNaluHead
{
memset(SendBuff, 0, MTU_SIZE);
stNaluHead Head;
//为了确保强行赋值时位相同
Head.uiForbidden = pNalu->uiForbidden>>7 & 0x1;
Head.uiNRI = pNalu->uiNRI>>5 & 0x3;
Head.uiType = pNalu->uiType & 0x1f;
SendBuff[0] = TRANFER_NALU_HEAD(Head);
uint32_t uiTimeStampInc = 0;
if(Head.uiType == 1 || Head.uiType == 5)// 如果是I帧,需要时间戳增加率
{
uiTimeStampInc = I_H264_SAMPLE_RATE/H264_FRAME_RATE;
}
//payload填充
memcpy(&SendBuff[1], pNalu->pBuff + pNalu->iStartCodePreLen + pNalu->iNaluHeadLen, pNalu->iPayloadLen);
iStatus = cSess.SendPacket((void *)SendBuff, pNalu->iNaluHeadLen + pNalu->iPayloadLen,H264_PAYLOAD_TYPE, true, uiTimeStampInc);
if (uiTimeStampInc != 0)
{
RTPTime::Wait(delay);
}
}
else
{
//拆成分片时需要每个包增加stFuIndicator和stFuHead,所以应该以最大包的值减去2个字节
int iNum = iSize/(MAX_RTP_PACKET_LENGTH - 2);
int iLast = iSize%(MAX_RTP_PACKET_LENGTH - 2);
if (iLast != 0) iNum++;
//第一分片包含了H264码流startcode还有nalu头,payload初始位置需要偏移
int iSendSucLen = pNalu->iStartCodePreLen + pNalu->iNaluHeadLen;
uint8_t uiType = pNalu->uiType & 0x1f;
for(int iSendNum = 0; iSendNum < iNum; iSendNum++)
{
memset(SendBuff, 0, MTU_SIZE);
stFuIndicator Head0;
//为了确保强行赋值时位相同
Head0.uiForbidden = pNalu->uiForbidden>>7 & 0x1;
Head0.uiNRI = pNalu->uiNRI>>5 & 0x3;
Head0.uiType = 0x1c; //Fu-A分片
SendBuff[0] = TRANFER_FU_INDICATOR(Head0);
stFuHead Head1;
Head1.uiStart = ((iSendNum == 0) ? 1: 0); //是否是第一分片
Head1.uiReverse = 0;
Head1.uiEnd = ((iSendNum == iNum -1) ? 1: 0); //是否是最后一分片
Head1.uiType = uiType;
SendBuff[1] = TRANFER_FU_HEAD(Head1);
uint32_t uiTimeStampInc = 0;
if(Head1.uiEnd == 1)
{
memcpy(&SendBuff[2], pNalu->pBuff + iSendSucLen, iLast);
uiTimeStampInc = I_H264_SAMPLE_RATE/H264_FRAME_RATE;
iStatus = cSess.SendPacket((void *)SendBuff, iLast + 2, H264_PAYLOAD_TYPE, true, uiTimeStampInc);
}
else
{
memcpy(&SendBuff[2], pNalu->pBuff + iSendSucLen, MAX_RTP_PACKET_LENGTH - 2);
iSendSucLen = iSendSucLen + MAX_RTP_PACKET_LENGTH - 2;
iStatus = cSess.SendPacket((void *)SendBuff, MAX_RTP_PACKET_LENGTH, H264_PAYLOAD_TYPE, false, uiTimeStampInc);
}
}
if (uiType == 1 || uiType == 5)
{
RTPTime::Wait(delay);
}
}
}
}
FreeNalu(pNalu);
cSess.BYEDestroy(RTPTime(10,0), 0, 0);
return 0;
}
3.编译脚本
makefile
如下INC1、INC2、LIB1、LIB2变量都是自己安装jthread以及jrtp相对应的头文件和库(一定要对应的,特别是头文件)
安装jthread以及jrtp参考我的另外一篇
RTP实战-----RTP开源库安装
.PHONY=clean
CC=g++
RM=rm
INC1=-I/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jrtplib/include/jrtplib3
INC2=-I/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jthread/include
LIB1=-L/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jrtplib/lib
L1=-ljrtp
LIB2=-L/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jthread/lib
L2=-ljthread
CUR_PATH=$(shell pwd)
SRC_PATH=$(CUR_PATH)
MAKEFILE_PATH=$(CUR_PATH)/script
MAKEFILE_BUILD=$(MAKEFILE_PATH)/makefile.build
OUT_PATH=$(CUR_PATH)/out
OUT_PATH := $(shell mkdir -p $(OUT_PATH) && cd $(OUT_PATH) && pwd)
build:
$(MAKE) -f $(MAKEFILE_BUILD) M=$(SRC_PATH) O=$(OUT_PATH) INC1=$(INC1) INC2=$(INC2) LIB1=$(LIB1) L1=$(L1) LIB2=$(LIB2) L2=$(L2)
clean:
$(MAKE) -f $(MAKEFILE_BUILD) M=$(SRC_PATH) O=$(OUT_PATH) clean
script目录下的makefile.build
src-list := $(filter %.cpp,$(shell ls $(M)))
obj-list := $(patsubst %.cpp,%.o,$(src-list))
dep-list := $(patsubst %.cpp,.%.d,$(src-list))
src-list := $(addprefix $(M)/,$(src-list))
obj-list := $(addprefix $(O)/,$(obj-list))
dep-list := $(addprefix $(M)/,$(dep-list))
PHONY :=
target := $(O)/built-in.o
CC=g++
PHONY += __build
__build: $(target)
$(target): $(obj-list)
$(CC) $^ -o $@ -std=c++11 $(INC1) $(INC2) $(LIB1) $(L1) $(LIB2) $(L2)
# 生成依赖文件.d
$(dep-list): $(M)/.%.d: $(M)/%.cpp
$(CC) -MM $< > $@ $(INC1) $(INC2)
@echo "$(O)/`cat $@`\n\t\$$(CC) -c -o \$$@ \$$(filter %.cpp,$$^) $(INC1) $(INC2)" > $@
-include $(dep-list)
PHONY += clean
clean:
@echo "$(O)"
rm -f $(target) $(obj-list) $(dep-list)
.PHONY: $(PHONY)
假如要调试加入gdb的话,如下:
src-list := $(filter %.cpp,$(shell ls $(M)))
obj-list := $(patsubst %.cpp,%.o,$(src-list))
dep-list := $(patsubst %.cpp,.%.d,$(src-list))
src-list := $(addprefix $(M)/,$(src-list))
obj-list := $(addprefix $(O)/,$(obj-list))
dep-list := $(addprefix $(M)/,$(dep-list))
PHONY :=
target := $(O)/built-in.o
CC=g++
PHONY += __build
__build: $(target)
$(target): $(obj-list)
$(CC) $^ -o $@ -std=c++11 $(INC1) $(INC2) $(LIB1) $(L1) $(LIB2) $(L2) -g
# 生成依赖文件.d
$(dep-list): $(M)/.%.d: $(M)/%.cpp
$(CC) -MM $< > $@ $(INC1) $(INC2) -g
@echo "$(O)/`cat $@`\n\t\$$(CC) -c -o \$$@ \$$(filter %.cpp,$$^) $(INC1) $(INC2) -g" > $@
-include $(dep-list)
PHONY += clean
clean:
@echo "$(O)"
rm -f $(target) $(obj-list) $(dep-list)
.PHONY: $(PHONY)
4.编译
在工程目录下make build即可
root@ubuntu:/home/sulier/work/AudioVedio/RTPZip/testRTP# make build
make -f /home/sulier/work/AudioVedio/RTPZip/testRTP/script/makefile.build M=/home/sulier/work/AudioVedio/RTPZip/testRTP O=/home/sulier/work/AudioVedio/RTPZip/testRTP/out INC1=-I/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jrtplib/include/jrtplib3 INC2=-I/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jthread/include LIB1=-L/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jrtplib/lib L1=-ljrtp LIB2=-L/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jthread/lib L2=-ljthread
make[1]: Entering directory '/home/sulier/work/AudioVedio/RTPZip/testRTP'
g++ -MM /home/sulier/work/AudioVedio/RTPZip/testRTP/main.cpp > /home/sulier/work/AudioVedio/RTPZip/testRTP/.main.d -I/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jrtplib/include/jrtplib3 -I/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jthread/include
g++ -MM /home/sulier/work/AudioVedio/RTPZip/testRTP/h264.cpp > /home/sulier/work/AudioVedio/RTPZip/testRTP/.h264.d -I/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jrtplib/include/jrtplib3 -I/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jthread/include
g++ -c -o /home/sulier/work/AudioVedio/RTPZip/testRTP/out/h264.o /home/sulier/work/AudioVedio/RTPZip/testRTP/h264.cpp -I/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jrtplib/include/jrtplib3 -I/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jthread/include
g++ -c -o /home/sulier/work/AudioVedio/RTPZip/testRTP/out/main.o /home/sulier/work/AudioVedio/RTPZip/testRTP/main.cpp -I/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jrtplib/include/jrtplib3 -I/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jthread/include
g++ /home/sulier/work/AudioVedio/RTPZip/testRTP/out/h264.o /home/sulier/work/AudioVedio/RTPZip/testRTP/out/main.o -o /home/sulier/work/AudioVedio/RTPZip/testRTP/out/built-in.o -std=c++11 -I/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jrtplib/include/jrtplib3 -I/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jthread/include -L/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jrtplib/lib -ljrtp -L/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jthread/lib -ljthread
make[1]: Leaving directory '/home/sulier/work/AudioVedio/RTPZip/testRTP'
5.运行
编译中虽然已经添加了jrtp库和jthread库,但是运行还是需要在本运行终端添加环境(临时运行方法,如果不想这么干的可能放到系统环境中)
export LD_LIBRARY_PATH=/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jrtplib/lib
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jthread/lib
可以通过echo命令查看下是否已添加
root@ubuntu:/home/sulier/work/AudioVedio/RTPZip/testRTP# echo $LD_LIBRARY_PATH
/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jrtplib/lib:/home/sulier/work/AudioVedio/RTPZip/TestProject/export/jthread/lib
root@ubuntu:/home/sulier/work/AudioVedio/RTPZip/testRTP# ldd out/built-in.o
linux-vdso.so.1 => (0x00007ffc62ed1000)
libjrtp.so.3.11.2 => /home/sulier/work/AudioVedio/RTPZip/TestProject/export/jrtplib/lib/libjrtp.so.3.11.2 (0x00007f760924c000)
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f7608eca000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f7608cb4000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f76088ea000)
libjthread.so.1.3.3 => /home/sulier/work/AudioVedio/RTPZip/TestProject/export/jthread/lib/libjthread.so.1.3.3 (0x00007f76086e6000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f76084c9000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f76081c0000)
/lib64/ld-linux-x86-64.so.2 (0x00007f7609509000)
可以看出来库路径都已找到,然后执行文件是在out目录下built-in.o
root@ubuntu:/home/sulier/work/AudioVedio/RTPZip/testRTP# ./out/built-in.o
Using version 3.11.2
Enter local portbase:
库路径都已找到,然后可以windows端编辑一个sdp文件
比如show.sdp,其中9004是接收端端口,25是帧率(代码中也是,可以自己对应修改),192.168.36.1是接收端IP
m=video 9004 RTP/AVP 96
a=rtpmap:96 H264/90000;
a=framerate:25
c=IN IP4 192.168.36.1
之后用VLC打开就行了
6.可能存在播放不了问题
关闭下防火墙
更多推荐
所有评论(0)