传输AAC的组播RTSP服务
参考:从零开始写一个RTSP服务器(八)一个多播的RTSP服务器 aac文件:test.aac文件地址
和java实现传输AAC的RTSP服务区别
- 服务端往组播ip+port发送AACRtp数据,循环发送
- rtsp的响应:DESCRIBE和SETUP修改
代码
RtspTcpServer.java
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;
import java.util.Date;
// Linux内核对TCP连接的识别是通过四元组来区分:源ip,源port,目标ip,目标port
public class RtspTcpServer {
public static void main(String[] args) throws IOException, InterruptedException {
int rtcpPort = MulticastServer.start();//开启往组播组发送数据
ServerSocket serverSocket = new ServerSocket(8888);//1.创建服务端对象
System.out.println("TCP服务端端启动===>"+serverSocket.getLocalSocketAddress());
while (true){
Socket socket = serverSocket.accept(); //阻塞式,2.获取连接过来的客户端对象
//获取到连接,则开启一个线程处理当前连接
new Thread(new Runnable() {
@Override
public void run() {
InputStream inputStream = null;
OutputStream outputStream = null;
try {
System.out.println("TCP已连接===>"+socket.getRemoteSocketAddress());
inputStream = socket.getInputStream();//3.通过socket对象获取输入流,要读取客户端发来的数据
outputStream = socket.getOutputStream();//3.通过socket对象获取输入流,要读取客户端发来的数据
byte[] buffer = new byte[1024*1024];
int readNum = 0;
while((readNum=inputStream.read(buffer))!=-1){
if(readNum>0){
byte[] receive = Arrays.copyOfRange(buffer,0,readNum);
System.out.println("读取的字节数:"+readNum);
System.out.println("读取的字节数:"+receive.length);
System.out.println("缓冲区大小:"+buffer.length);
handlerReceiveData(outputStream,receive,rtcpPort);
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
System.out.println("断开连接");
if(inputStream!=null){
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(outputStream!=null){
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}).start();
}
}
public static void handlerReceiveData(OutputStream outputStream, byte[] buffer,int rtcpPort){
String receiveStr=new String(buffer);
System.out.println("TCP-----------------接收receiveStr----------------------");
System.out.println(receiveStr);
System.out.println("TCP-----------------接收receiveStr----------------------");
String lines[] = receiveStr.split("\\r?\\n");//按行分割
int cseq=0;
int clientRtpPort=0;
int clientRtcpPort=0;
String url=null;
String localIp=null;
{
for(String line:lines){
if(line.indexOf("rtsp://")>-1){
url = line.split("\\s+")[1];
String[] split = line.split(":");
localIp = split[1].substring(2);
}
if(line.startsWith("CSeq:")){
String[] split = line.split(": ");
cseq = Integer.parseInt(split[1].trim());
}
if(line.startsWith("Transport:")){
String[] split = line.split(";");
for(String i : split){
if(i.startsWith("client_port=")){
String substring = i.substring(12);
String[] split1 = substring.split("-");
clientRtpPort = Integer.parseInt(split1[0].trim());
clientRtcpPort = Integer.parseInt(split1[1].trim());
}
}
}
}
}//获取cseq
String responseStr=null;
if (receiveStr.startsWith("OPTIONS")){
//OPTIONS 请求服务端支持的RTSP方法列表;也可以定时发送这个请求来保活RTSP会话。
responseStr=String.format("RTSP/1.0 200 OK\r\n"+
"CSeq: %d\r\n"+
"Public: OPTIONS, DESCRIBE, SETUP, PLAY\r\n"+
"\r\n",cseq);
}else if(receiveStr.startsWith("SETUP")){
//SETUP:用于配置数据交互的方法(比如制定音视频的传输方式TCP或UDP)。
responseStr=String.format("RTSP/1.0 200 OK\r\n"+
"CSeq: %d\r\n"+
"Transport: RTP/AVP;multicast;destination=%s;source=%s;port=%d-%d;ttl=255\r\n"+
"Session: 66334873\r\n"+
"\r\n",
cseq,
MulticastServer.MULTICAST_ADDRESS,
url,
MulticastServer.PORT,
rtcpPort);
}else if(receiveStr.startsWith("DESCRIBE")){
//DESCRIBE:请求指定的媒体流的SDP描述信息(详细包括音视频流的帧率、编码类型等媒体信息)。
String sdp=String.format("v=0\r\n"+
"o=- 9%d 1 IN IP4 %s\r\n"+
"t=0 0\r\n"+
"a=control:*\r\n"+
"a=type:broadcast\r\n"+
"a=rtcp-unicast: reflection\r\n"+
"m=audio %d RTP/AVP 97\r\n"+
"c=IN IP4 %s/255\r\n"+
"a=rtpmap:97 mpeg4-generic/44100/2\r\n"+
"a=fmtp:97 SizeLength=13;\r\n"+
"a=control:track0\r\n",
new Date().getTime(),
localIp,
MulticastServer.PORT,
MulticastServer.MULTICAST_ADDRESS);
//sdp指定多播地址和端口
responseStr=String.format("RTSP/1.0 200 OK\r\nCSeq: %d\r\n"+
"Content-Base: %s\r\n"+
"Content-type: application/sdp\r\n"+
"Content-length: %d\r\n\r\n"+
"%s",
cseq,
url,
sdp.length(),
sdp);
}else if(receiveStr.startsWith("PLAY")){
//PLAY:用于启动(当暂停时重启)交付数据给客户端。
responseStr=String.format("RTSP/1.0 200 OK\r\n"+
"CSeq: %d\r\n"+
"Range: npt=0.000-\r\n"+
"Session: 66334873; timeout=60\r\n" +
"\r\n",
cseq);
}else if(receiveStr.startsWith("PAUSE")){
//PAUSE:用于临时停止服务端的数据的交互(使用PLAY来重新启动数据交互)。
responseStr=String.format("RTSP/1.0 200 OK\r\n" +
"CSeq: %d\r\n" +
"\r\n",cseq);
}else if(receiveStr.startsWith("TEARDOWN")){
//TEARDOWN:请求终止来自服务端的数据的传输。
responseStr=String.format("RTSP/1.0 200 OK\r\n" +
"CSeq: %d\r\n" +
"\r\n",cseq);
}
try {
outputStream.write(responseStr.getBytes());
outputStream.flush();
System.out.println("TCP-----------------响应responseStr----------------------");
System.out.println(responseStr);
System.out.println("TCP-----------------响应responseStr----------------------");
} catch (IOException e) {
e.printStackTrace();
}
}
}
MulticastServer.java
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.MulticastSocket;
/*
* IP组播:
* 组播IP地址:用于标识一个IP组播组,范围是从224.0.0.0到239.255.255.255。
* 组播组可以是永久的也可以是临时的:
* - 组播组地址中,有一部分由官方分配的,称为永久组播组。
* - 永久组播组保持不变的是它的ip地址,组中的成员构成可以发生变化。
* - 永久组播组中成员的数量都可以是任意的,甚至可以为零。
* - 那些没有保留下来供永久组播组使用的ip组播地址,可以被临时组播组利用。
* - 224.0.0.0~224.0.0.255为预留的组播地址(永久组地址)。
* - 224.0.1.0~238.255.255.255为用户可用的组播地址(临时组地址),全网范围内有效。
* - 239.0.0.0~239.255.255.255为本地管理组播地址,仅在特定的本地范围内有效。
* 组播源:信息的发送者,组播源不一定属于组播组,它向组播组发送数据,自己不一定是接收者。多个组播源可以同时向一个组播组发送报文。
* 组播组:信息接收者,加入同一组播组的接收者成员可以广泛分布在网络中的任何地方,没有地域限制。
* 组播路由器:支持组播信息传输的所有路由器
*
*
* 1.IP协议规定组播地址的范围是224.0.0.0 ~ 239.255.255.255,协议软件底层设定好的
* 2.java提供了
* - socket=new MulticastSocket(组播port);创建组播成员(port端口)
* - socket.joinGroup(InetAddress.getByName(组播ip));加入组播地址
* - 该socket可以接收发往组播地址的包
* 3.往组播地址发送udp包,所有加入该组播ip+port的成员都能收到
* - byte[] data="哈哈哈".getBytes();
* - DatagramPacket packet=new DatagramPacket(data,data.length,InetAddress.getByName(组播ip),组播port);
* - DatagramSocket datagramSocket=new DatagramSocket();//创建DatagramSocket对象
* - datagramSocket.send(packet);//向目标ip+port端发送数据报
* - datagramSocket.close();
* */
public class MulticastServer {
public static String MULTICAST_ADDRESS = "225.0.0.1";//组播ip
public static int PORT = 5555;//组播端口
private static MulticastSocket multicastSocket;
public static int start() throws IOException {
/*往组播组添加一个成员MulticastSocket,用于发送数据(当然也可以是普通的DatagramSocket)*/
multicastSocket = new MulticastSocket(PORT);
multicastSocket.joinGroup(InetAddress.getByName(MULTICAST_ADDRESS));
/*开启一个专门接收数据的线程(有可能收到谁(-_-)往组播组发的消息)*/
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
byte[] buf = new byte[1024*1024];//创建数据包
DatagramPacket datagramPacket = new DatagramPacket(buf,buf.length);
multicastSocket.receive(datagramPacket);
handlerReceiveData(0,datagramPacket);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}).start();
/*开启一个线程专门往组播组发送h264数据rtp包*/
RTPAACServer rtph264Server = new RTPAACServer(multicastSocket, InetAddress.getByName(MULTICAST_ADDRESS), PORT);
new Thread(new Runnable() {
@Override
public void run() {
try {
rtph264Server.startSendRtpPackage();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
/*创建一个rtcp单播Socket*/
DatagramSocket rtcpUdpSocket = new DatagramSocket();//建立socket服务
new Thread(new Runnable() {
@Override
public void run() {
byte[] buf = new byte[1024*1024];//创建数据包
DatagramPacket datagramPacket = new DatagramPacket(buf,buf.length);
while (true){
try {
rtcpUdpSocket.receive(datagramPacket); //阻塞式,3.使用接收方法将数据存储到数据包中
System.out.println("rtp UDP接收包===>"+datagramPacket.getSocketAddress());
handlerReceiveData(1,datagramPacket);
} catch (IOException e) {
e.printStackTrace();
break;
}
}
}
});
return rtcpUdpSocket.getLocalPort();
}
public static void handlerReceiveData(int type,DatagramPacket datagramPacket){
if(type==0){
// System.out.println("组播");
// System.out.println("接收到参数:"+datagramPacket.getSocketAddress());
// System.out.println(datagramPacket.getData());
// System.out.println(datagramPacket.getLength());
}else{
System.out.println("rtcp");
// System.out.println("接收到参数:"+datagramPacket.getSocketAddress());
// System.out.println(datagramPacket.getData());
// System.out.println(datagramPacket.getLength());
}
}
}
RTPAACServer.java
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.List;
public class RTPAACServer {
private RandomAccessFile in;
private List<ADTSHeader> ADTSIndexs = new ArrayList<>() ;//用来记录每个ADTS帧信息以及起始位置
DatagramSocket rtpUdpSocket;//发送数据的socket
InetAddress clientAddress;//目标地址
int clientRtpPort;//目标端口
public RTPAACServer(DatagramSocket socket, InetAddress address, int port) throws SocketException {
rtpUdpSocket = socket;
clientAddress = address;
clientRtpPort = port;
}
public void close(){
if(rtpUdpSocket!=null){
rtpUdpSocket.close();
}
}
public void startSendRtpPackage() throws Exception {
String fileName = RTPAACServer.class.getResource("test.aac").getPath();
in = new RandomAccessFile(fileName, "r");
parseIndexs();//获取所有起始下标
sendADTSRtpPackage();
in.close();
}
/*
* AAC的音频文件格式有ADIF & ADTS:
* - ADIF:Audio Data Interchange Format音频数据交换格式,ADIF只有一个统一的头,所以必须得到所有的数据后解码。
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | ADIF header | AAC data |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* - ADTS:Audio Data Transport Stream是AAC音频的传输流格式,ADTS每一帧都有头信息,可以在任意帧解码。
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | ADTS header | AAC data | ADTS header | AAC data |......
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |fixed28bits|variable28bits|
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
public void parseIndexs() throws Exception {
boolean isEnd=false;
while(true) {
if(in.length()>0) {
Long index = in.getFilePointer();
byte[] header=new byte[7];
in.read(header);
if(((header[0]&0xFF)==255)&&((header[1] & 0xF0)==240)){
ADTSHeader adtsHeader = new ADTSHeader();
adtsHeader.index = index;
adtsHeader.syncword = 0xFFF;//同步头12bit总是0xFFF,代表着一个ADTS帧的开始
adtsHeader.id = (header[1] & 0x08)>>3;//1 bit MPEG 标示符:0=MPEG-4;1=MPEG-2;
adtsHeader.layer = (header[1] & 0x06)>>1; //2 bit 总是00
adtsHeader.protectionAbsent = header[1] & 0x01; //1 bit 1表示没有crc,0表示有crc
adtsHeader.profile = (header[2] & 0xc0)>>6; //2 bit 表示使用哪个级别的AAC
adtsHeader.samplingFreqIndex = (header[2] & 0x3c)>>2; //4 bit 表示使用的采样频率
adtsHeader.privateBit = (header[2] & 0x02)>>1; //1 bit
adtsHeader.channelCfg = (((header[2] & 0x01)<<2) | ((header[3] & 0xc0)>>6)); //3 bit 表示声道数
adtsHeader.originalCopy = (header[3] & 0x20)>>5; //1 bit
adtsHeader.home = (header[3] & 0x10)>>4; //1 bit
/*可变头信息28bits:帧与帧之间可变*/
adtsHeader.copyrightIdentificationBit = (header[3] & 0x08)>>3; //1 bit
adtsHeader.copyrightIdentificationStart = (header[3] & 0x04)>>2; //1 bit
adtsHeader.aacFrameLength = (((header[3] & 0x03)<<11) | (header[4]<< 3) | ((header[5]&0xe0)>> 5));//13 bit 一个ADTS帧的长度包括ADTS头和AAC原始流
adtsHeader.adtsBufferFullness = (((header[5] & 0x1f)<<6) | ((header[6] & 0xfc)>>2)); //11 bit 0x7FF 说明是码率可变的码流
adtsHeader.numberOfRawDataBlockInFrame = header[6] & 0x03; //2 bit
ADTSIndexs.add(adtsHeader);//getFilePointer()返回此文件中的当前偏移量。
Long nextStart = in.getFilePointer() + adtsHeader.aacFrameLength-7;
if(in.length()-1>=nextStart){
in.seek(nextStart);
}else{
isEnd=true;
}
}else{
throw new Exception("failed to parse adts header");
}
}
if(isEnd) {
//读到文件尾部,跳出
break;
}
}
}
/*
* 获取每一帧ADTS
*/
public void sendADTSRtpPackage() throws IOException, InterruptedException {
/*
* 如果采样频率是44100
* 一般AAC每个1024个采样为一帧
* 所以一秒就有 44100 / 1024 = 43帧
* 时间增量就是 44100 / 43 = 1025
* 一帧的时间为 1 / 43 = 23ms
*/
int timestamp_increse = 44100/(44100/1024);//时间增量
int PT = 97;//负载类型号97:AAC
int packageSize = 1400;//最大负载长度
int seqNum = 1;//序列号
int ts_current = 1;//当前时间戳
for(int i=0;i<ADTSIndexs.size();) {
in.seek(ADTSIndexs.get(i).index+7);//设置文件指针偏移
int len = ADTSIndexs.get(i).aacFrameLength-7;//读取长度
byte[] aacADTSDataArr=new byte[len];//正真的数据包
in.read(aacADTSDataArr);
byte[] bytes = aacDataToRtp(aacADTSDataArr, PT, seqNum, ts_current, 0x88923423);
//2.创建数据报,包含响应的数据信息
// System.out.println(bytes.length+"="+len+"("+in.getFilePointer()+"="+ADTSIndexs.get(i).aacFrameLength+"-7)+"+16);
DatagramPacket packet2=new DatagramPacket(bytes,bytes.length,clientAddress,clientRtpPort);
try {
rtpUdpSocket.send(packet2);//3.响应客户端
} catch (IOException e) {
e.printStackTrace();
break;
}
ts_current+=timestamp_increse;
seqNum ++;
Thread.sleep(22);
/*循环播放*/
if(i==ADTSIndexs.size()-1){
i=0;
}else{
i++;
}
}
}
/*
* 一个ADTS帧数据一个rtp包
* AAC的RTP打包方式:
* Rtp头+4byte(0x00+0x10+aac_data_size13bit)+AAC_Data
* */
public byte[] aacDataToRtp(byte[] aacADTSDataArr,int PT,int seq,int timestamp,int ssrc){
byte[] rtpHeader = initRTPHeader(PT, seq, timestamp, ssrc);//12个字节的rtpHeader
byte[] rtpPackage=new byte[12+4+aacADTSDataArr.length];
System.arraycopy(rtpHeader,0,rtpPackage,0,12);//从源数组的第几位,复制到目标数组开始下标,n位
rtpPackage[12] = 0;
rtpPackage[13] = 0x10;
rtpPackage[14] = (byte) ((aacADTSDataArr.length & 0xFF)>>5);//& 0x1FE0)>>5);//aac data大小高8位
rtpPackage[15] = (byte) ((aacADTSDataArr.length & 0x1F)<<3);//aac data大小底5位,放在字节高位
System.arraycopy(aacADTSDataArr,0,rtpPackage,16,aacADTSDataArr.length);//从源数组的第几位,复制到目标数组开始下标,n位
return rtpPackage;
}
/*
* RTP报文格式:
* |===============================================================|
* | 0 | 1 | 2 | 3 |
* |===============|===============|===============|===============|
* |7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|
* |===============|===============|===============================|
* |V2|P1|X1|CC4 |M1| PT7 | sequence number16 |
* |===============================================================|
* | timestamp时间戳 |
* |===============================================================|
* |同步信源(SSRC)标识符synchronization source (SSRC) identifier |
* |===============================================================|
* |特约信源(CSRC)标识符contributing source (CSRC) identifiers |
* | .... |
* |===============================================================|
*/
public byte[] initRTPHeader(int PT,int seq,int timestamp,int ssrc){
byte[] headerArr = new byte[12];//rtp固定头部有12个字节
//1.清空headerArr
for (int i = 0; i < headerArr.length; i++) { headerArr[i] = (byte) 0; }
//2.填充数据
//字节1:|V2|P1|X1|CC4 |
headerArr[0] = (byte) 0x80;//10000000==>V=1.0,P=0,X=0,CC=0000
//字节2:|M1| PT7 |
headerArr[1] = (byte)(PT & 0xff | 0x80);//11100001==>M=1,PT=1100001
//字节3-4:|sequence number16|:headerArr[2],headerArr[3]
System.arraycopy(intToBytes(seq,2),0,headerArr,2,2);//从源数组的第几位,复制到目标数组开始下标,n位
//字节5-8|timestamp时间戳|:headerArr[4]~headerArr[7]
System.arraycopy(intToBytes(timestamp,4),0,headerArr,4,4);//从源数组的第几位,复制到目标数组开始下标,n位
//字节9-12|同步信源(SSRC)标识符|:headerArr[8]~headerArr[11]
System.arraycopy(intToBytes(ssrc,4),0,headerArr,8,4);//从源数组的第几位,复制到目标数组开始下标,n位
return headerArr;
}
/**
* 将32位长度转换为n字节。(大端字节序:高位在前,低位在后)
* @param ldata 将从中构造n字节数组的int。
* @param n 要将长文件转换为的所需字节数。
* @return 用长值填充的所需字节数组。
*/
public byte[] intToBytes(int ldata, int n) {
byte[] buff = new byte[n];
for (int i=n-1;i>=0;i--) {
// 保持将最右边的8位分配给字节数组,同时在每次迭代中移位8位
buff[i] = (byte)ldata;
ldata = ldata>>8;
}
return buff;
}
/**
* - ADTS:Audio Data Transport Stream是AAC音频的传输流格式,ADTS每一帧都有头信息,可以在任意帧解码。
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | ADTS header | AAC data | ADTS header | AAC data |......
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |fixed28bits|variable28bits|
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
static class ADTSHeader{
/*ADTS帧的起始位置*/
Long index;
/*固定头信息28bits(每个帧都一样)*/
int syncword; //12 bit 同步字1111 1111 1111=0xFFF,说明一个ADTS帧的开始
int id; //1 bit MPEG 标示符:0=MPEG-4;1=MPEG-2;
int layer; //2 bit 总是00
int protectionAbsent; //1 bit 表示是否误码校验1-没有crc,0-有crc
int profile; //2 bit 表示使用哪个级别的AAC
int samplingFreqIndex; //4 bit 表示使用的采样率下标
int privateBit; //1 bit
int channelCfg; //3 bit 表示声道数
int originalCopy; //1 bit
int home; //1 bit
/*可变头信息28bits:帧与帧之间可变*/
int copyrightIdentificationBit; //1 bit
int copyrightIdentificationStart; //1 bit
int aacFrameLength; //13 bit 一个ADTS帧的长度包括ADTS头和AAC原始流
int adtsBufferFullness; //11 bit 0x7FF 说明是码率可变的码流
int numberOfRawDataBlockInFrame; //2 bit
public int formatSamplingFreqIndex(int samplingFreqIndex){
if((samplingFreqIndex & 0x0f)==0x0){
return 96000;
}else if((samplingFreqIndex & 0x0f)==0x1){
return 88200;
}else if((samplingFreqIndex & 0x0f)==0x2){
return 64000;
}else if((samplingFreqIndex & 0x0f)==0x3){
return 48000;
}else if((samplingFreqIndex & 0x0f)==0x4){
return 44100;
}else if((samplingFreqIndex & 0x0f)==0x5){
return 32000;
}else if((samplingFreqIndex & 0x0f)==0x6){
return 24000;
}else if((samplingFreqIndex & 0x0f)==0x7){
return 22050;
}else if((samplingFreqIndex & 0x0f)==0x8){
return 16000;
}else if((samplingFreqIndex & 0x0f)==0x9){
return 12000;
}else if((samplingFreqIndex & 0x0f)==0xa){
return 11025;
}else if((samplingFreqIndex & 0x0f)==0xb){
return 8000;
}else if((samplingFreqIndex & 0x0f)==0xc){
return 7350;
}
return 0;
}
}
}
启动
- 运行
RtspTcpServer.java
的mian()
就跑起来了 - 使用vlc播放器播放网络流(文件=>打开网络=>输入URL)
rtsp://127.0.0.1:8888
VLC播放器 | 打开URl播放 | 结果 |