android使用RTMP实现录屏直播推送音视频(已在享学公众号发表)
目录MediaCodec编码音频MediaCodec编码视频H264裸流格式分析FLV封装格式分析利用librtmp封包发送一、MediaCodec编码音频创建音频编码器,指定AAC格式,采样率44100,码率64_000,单声道;//创建编码器MediaFormat mediaFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_
目录
创建音频编码器,指定AAC格式,采样率44100,码率64_000,单声道;
循环从录音器中读取PCM格式的byte数组,放入编码器的输入队列;
循环从编码器的输入队列中读取数据,获得编码好的AAC格式的byte数组,等待后续rtmp封包用。
将编码器的surface传给MediaProjection对象构建虚拟显示
一、MediaCodec编码音频
-
创建音频编码器,指定AAC格式,采样率44100,码率64_000,单声道;
//创建编码器
MediaFormat mediaFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC,
44100, 1);
//编码规格,可以看成质量
mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE,
MediaCodecInfo.CodecProfileLevel.AACObjectLC);
//码率
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 64_000);
mediacodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
mediacodec.configure(mediaFormat,null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
-
创建AudioRecord录音对象,设置参数与编码器对应;
//创建录音对象
minBufferSize = AudioRecord.getMinBufferSize(44100,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT);
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, 44100,
AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, minBufferSize);
-
启动编码器和录音器;
-
循环从录音器中读取PCM格式的byte数组,放入编码器的输入队列;
//从录音器读取数据
readIndex = audioRecord.read(buffer, 0, 1024);
if (readIndex <= 0){
continue;
}
//获取编码器输入队列的可用位置下标
inputBufferIndex = mediacodec.dequeueInputBuffer(0);
if (inputBufferIndex >= 0) {
//获取输入队列
inputBuffer = mediacodec.getInputBuffer(inputBufferIndex);
inputBuffer.clear();//避免缓冲队列有冗余数据,先clear一下
Log.d("zhangdi", "length = " + buffer.length + " readIndex=" + readIndex);
inputBuffer.put(buffer);//将需要编码的数据放入队列缓冲区
//将缓冲队列放回编码器
mediacodec.queueInputBuffer(inputBufferIndex, 0, readIndex,
System.nanoTime() / 1000, 0);
}
-
循环从编码器的输入队列中读取数据,获得编码好的AAC格式的byte数组,等待后续rtmp封包用。
//获取编码器输入队列的可用下标位置,10为等待超时时间
outputBufferIndex = mediacodec.dequeueOutputBuffer(bufferInfo, 10);
while (outputBufferIndex >= 0 && isRecording) {
//获取输出缓冲区
outputBuffer = mediacodec.getOutputBuffer(outputBufferIndex);
outBuffer = new byte[bufferInfo.size];
//获取编码好的AAC数据
outputBuffer.get(outBuffer);
//rtmp封包
//释放缓冲区
mediacodec.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = mediacodec.dequeueOutputBuffer(bufferInfo, 10);
}
二、MediaCodec编码视频
-
申请录屏权限,获取MediaProjection对象
/**
* 开始直播
* @param url 直播服务器地址
* @param context 接收录屏请求回调的activity
*/
public void startLive(String url, Activity context){
this.url = url;
mediaProjectionManager = (MediaProjectionManager) context.
getSystemService(Context.MEDIA_PROJECTION_SERVICE);
Intent intent = mediaProjectionManager.createScreenCaptureIntent();
context.startActivityForResult(intent, 101);
}
/**
* 屏幕录制权限申请后处理,返回结果,成功则开启屏幕推流,否则返回false
* @param requestCode
* @param resultCode
* @param data
* @return true 授权成功开启推流
* false 授权失败
*/
public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent data){
if (requestCode == 101 && resultCode == Activity.RESULT_OK){
mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data);
LiveExecutors.getInstance().excute(this);
return true;
}else {
return false;
}
}
-
创建视频编码器
指定AVC格式(对应H264),宽高使用360*640,码率采用400_000(宽高和码率的设置为经验值,这里参考腾讯云实时音视频给出的参考值),帧率15fps,关键帧帧间间隔2s,本地原始视频格式采用MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface(录屏输出的格式);
-
将编码器的surface传给MediaProjection对象构建虚拟显示
也就是让录屏画面通过编码器的surface进入编码器的输入队列;
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC,360, 640);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 400_000);//设置比特率
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);//帧率
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2);//关键帧间隔
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
try {
mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
mediaCodec.configure(mediaFormat,null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
Surface surface = mediaCodec.createInputSurface();
//将编码器的surface传给mediaProjection
virtualDisplay = mediaProjection.createVirtualDisplay("screen-vd",
360, 640, 1,
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,surface,null,null);
start();//启动线程
} catch (IOException e) {
e.printStackTrace();
}
-
循环从编码器的输出队列读取数据
获得编码好的AVC格式的byte数组,留待后续rtmp封包使用。
while (isEncoding){
//编码器指定的关键帧间隔可能失效,所以需要手动2s刷新一下关键帧
if (lastTimeTamp != 0){
if (System.currentTimeMillis() - lastTimeTamp > 2000) {//每2秒强制刷新关键帧
Bundle bundle = new Bundle();
bundle.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
mediaCodec.setParameters(bundle);
lastTimeTamp = System.currentTimeMillis();
}
} else {
lastTimeTamp = System.currentTimeMillis();
}
//获取输出获取冲可用下标,timeoutUs是从队列中取数据的超时时间,这是个阻塞方法,如果超时则不继续等待
int index = mediaCodec.dequeueOutputBuffer(bufferInfo, 10);
if (index >= 0) {
//获取输出缓冲区队列
ByteBuffer byteBuffer = mediaCodec.getOutputBuffer(index);
byte[] bytes = new byte[bufferInfo.size];
//从输出缓冲区队列读取数据
byteBuffer.get(bytes);
//获得了编码好的数据,封装出rmpt视频包
//释放,让队列中index位置能放新数据
mediaCodec.releaseOutputBuffer(index, false);
}
}
三、H264裸流格式分析
此时我们就获取到了AAC音频裸流和H264视频裸流。H264码流在网络中传输时实际是以NALU形式传输的,NALU就是NAL UNIT,NAL单元。NAL全称是NetWork Abstract Layer,即网络抽象层。我们此时得到的编码数据就是NAL数据。
一段包含了N个图像的H264裸数据,每个NAL之间由:
00 00 00 01 或者 00 00 01进行分割。
在分隔符之后的第一个字节就是表示这个NAL的类型:
0x67 : sps
0x68 : pps
0x65 : IDR首个关键帧
分析这些是因为,我们后续往RTMP包中封装的视频数据就是NAL数据,但是需要去掉每个NAL单元之间的分隔符,且根据NAL类型的不同,封装的数据格式也有区别。
为了更加形象的解释NAL单元我将获取到的AVC视频byte数组转换为16进制打印出来了,大家可以看一下。
四、FLV封装格式分析
FLV(Flash Video)是Adobe公司设计开发的一种流行的流媒体格式,由于其视频文件体积轻巧、封装简单等特点,使其很适合在互联网上进行应用。此外,FLV可以使用Flash Player进行播放,而Flash Player插件已经安装在全世界绝大部分浏览器上,这使得通过网页播放FLV视频十分容易。目前主流的视频网站如优酷网,土豆网,乐视网等网站无一例外地使用了FLV格式。FLV封装格式的文件后缀通常为“.flv”。
FLV文件总体格式:
FLV包括文件头(File Header)和文件体(File Body)两部分,其中文件体由一系列的Tag组成。因此一个FLV文件是如图结构。
我们主要关心Tag就可以,因为我们RTMP封包的数据就是由FLV中的Tag数据构成的。每个Tag前面还包含了Previous Tag Size字段,表示前面一个Tag的大小。Tag的类型可以是视频、音频和Script,每个Tag只能包含以上三种类型的数据中的一种。下图展示了
FLV文件的详细结构。
我们主要就分析音频类型的Tag和视频类型的Tag,不涉及script data类型。两种类的header结构相同,所以下面主要分析他们的Tag data格式。
音频Tag Data格式:
音频Tag开始的第1个字节包含了音频数据的参数信息,从第2个字节开始为音频流数据即我们从编码器获取的音频流数据。
第一个字节参数信息分析:
前四位的音频编码类型如图所示:
2位的采样率的取值如图所示:
1位精度取值如图所示:
1位声道类型,单声道、双声道取值如下图所示,但是这里需要注意AAC格式的值总为1:
所以根据咱们上面代码中配置的编码格式,咱们最后的取值为10101111,转为16进制就是0xAF。
视频Tag Data格式分析:
下面这个图很重要,rtmp封包的时候里面的数据按照下面的格式封装。
注:图中sps[1]为profile sps[2]为兼容性 sps[3]为profile level
接下来用三张flv视频的格式分析图片来对照看看,我把AVC序列头类型的视频Tag分析写了一下,普通NAL单元类型视频和音频自己试试分析吧。
视频类型AVC序列头类型:
视频类型普通NAL单元类型:
音频类型:
五、利用librtmp封包发送
- 利用librtmp连接流;
- 根据数据类型封装分别封装视频类型和音频类型的package;
- 利用librtmp发送package;
librtmp的工作流程:
首先我们Java层需要定义一个类RTMPPacket用来封装编码器读出的数据;
然后补全我们上面两种编码器中缺失的编码数据封装,
视频编码器中添加:
//获得了编码好的数据,封装出rmpt视频包
RTMPPackage rtmpPackage = new RTMPPackage();
rtmpPackage.setBuffer(bytes);
rtmpPackage.setType(RTMPPackage.RTMP_PACKET_TYPE_VIDEO);
if (firstTimeStamp == 0) {//记录第一次获取编码数据的时间
firstTimeStamp = bufferInfo.presentationTimeUs/1000;
}
//tms需要的是一个相对时间
rtmpPackage.setTms(bufferInfo.presentationTimeUs/1000 - firstTimeStamp);
screenLive.addPackage(rtmpPackage);
音频编码器中添加:
需要注意的是在循环发送音频数据前,需要先发送一个音频数据的头部信息,
如果使用的是单声道就是 0x12 ,0x08,
双声道就是 0x12 ,0x10
//rtmp中音频数据封包头部数据,只用发送一次
RTMPPackage rtmpPackage = new RTMPPackage();
rtmpPackage.setType(RTMPPackage.RTMP_PACKET_TYPE_AUDIO_HEAD);
rtmpPackage.setBuffer(new byte[]{0x12, 0x08});
Log.d("RTMP", "AudioCodec addPackage");
screenLive.addPackage(rtmpPackage);
//rtmp音频封包
rtmpPackage = new RTMPPackage();
rtmpPackage.setType(RTMPPackage.RTMP_PACKET_TYPE_AUDIO_DATA);
rtmpPackage.setBuffer(outBuffer);
if (firstTimeStamp == 0) {
firstTimeStamp = bufferInfo.presentationTimeUs / 1000;
}
rtmpPackage.setTms(bufferInfo.presentationTimeUs / 1000 - firstTimeStamp);
screenLive.addPackage(rtmpPackage);
接下来是JNI方法:
利用librtmp连接流:
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_mpass_xxapp_rtmp_ScreenLive_connect(JNIEnv *env, jobject thiz, jstring url_) {
// TODO: implement connect()
const char *url = env->GetStringUTFChars(url_, 0);
int ret = 0;
do{
live = static_cast<Live *>(malloc(sizeof(Live)));
memset(live,0, sizeof(Live));
live->rtmp = RTMP_Alloc();//申请内存
RTMP_Init(live->rtmp);//初始化
//设置URL
if (!(ret = RTMP_SetupURL(live->rtmp, const_cast<char *>(url)))) break;
//开启输出模式
RTMP_EnableWrite(live->rtmp);
//连接服务器
if (!(ret = RTMP_Connect(live->rtmp, 0))) break;
//连接流
if (!(ret = RTMP_ConnectStream(live->rtmp, 0))) break;
LOGI("connect success");
}while (0);
if (!ret && live){
free(live);
live = NULL;
}
env->ReleaseStringUTFChars(url_, url);
return ret;
}
音频封包:
RTMPPacket * buildAudioPackage(int8_t *buffer, int len, int type, long tms, RTMP *rtmp){
RTMPPacket *audioPacket = static_cast<RTMPPacket *>(malloc(sizeof(RTMPPacket)));
RTMPPacket_Alloc(audioPacket, len + 2);//申请内存
audioPacket->m_body[0] = 0xAF;//设置音频参数,编码类型、采样率、精度、类型
audioPacket->m_body[1] = 0x01;//非指定类型,不是头信息
if (type == 1) {
audioPacket->m_body[1] = 0x00;//头信息类型
}
memcpy(&audioPacket->m_body[2], buffer, len);//拷贝音频数据
audioPacket->m_headerType = RTMP_PACKET_SIZE_LARGE;
audioPacket->m_hasAbsTimestamp = 0;//是否使用绝对时间
audioPacket->m_nTimeStamp = tms;//时间戳
audioPacket->m_nBodySize = len + 2;//音频数据长度+头部的参数和类型2字节
audioPacket->m_packetType = RTMP_PACKET_TYPE_AUDIO;
audioPacket->m_nChannel = 0x05;
audioPacket->m_nInfoField2 = rtmp->m_stream_id;
return audioPacket;
}
视频封包:
1.判断视频类型,是否包含SPS和PPS信息:
int sendVideo(int8_t *byte, int len, long tms) {
int ret;
do{
//视频数据的前四个字节是分隔符
if (byte[4] == 0x67) {//如果是sps信息则需要保存到结构体中,随着下一次的AVC序列头发送
if (live && (!live->pps || !live->sps)) {
saveSequenceHeadMsg(byte, len, live);
}
} else{
if (byte[4] == 0x65){//发送AVC序列头
RTMPPacket *packet = buildVideoSequencePackage(live);
if (!(ret = sendPackageData(packet))) {
break;
}
}
//发送普通NAL单元
RTMPPacket *packet = buildVideoPackage(byte, len, tms, live->rtmp);
ret = sendPackageData(packet);
}
}while (0);
return ret;
}
2.获取SPS和PPS信息:
void saveSequenceHeadMsg(int8_t *buffer, int len, Live *live){
for (int i = 0; i < len-4; ++i) {
if (buffer[i] == 0x00 &&
buffer[i+1] == 0x00 &&
buffer[i+2] == 0x00 &&
buffer[i+3] == 0x01) {//H264以NALU单元组成,每个单元之间以0x00 0x00 0x00 0x01分割
if (buffer[i+4] == 0x68) {//67为sps,68为pps
live->sps_len = i-4;
live->sps = static_cast<int8_t *>(malloc(sizeof(int8_t)));
memcpy(live->sps, buffer+4, live->sps_len);
live->pps_len = len-i-4;
live->pps = static_cast<int8_t *>(malloc(sizeof(int8_t)));
memcpy(live->pps, buffer+i+4, live->pps_len);
LOGI("sps:%d pps:%d", live->sps_len, live->pps_len);
break;
}
}
}
}
3.封装AVC序列头信息:
RTMPPacket* buildVideoSequencePackage(Live *live){
RTMPPacket *packet = static_cast<RTMPPacket *>(malloc(sizeof(RTMPPacket)));
//视频数据(0x17)1字节+类型(0x00序列头)1字节+合成时间3个字节全为0+版本(0x01)1字节+
// 编码规格3字节(sps[1]+sps[2]+sps[3])+NALU长度1个字节(0xFF)+sps个数1个字节(0xE1)
// +sps长度2个字节+sps内容+pps个数1个字节(0x01)+pps长度2个字节+pps内容
int bodySize = 16+live->pps_len+live->sps_len;
RTMPPacket_Alloc(packet, bodySize);
int i=0;
packet->m_body[i++] = 0x17;//数据类型 1关键帧 7AVC
packet->m_body[i++] = 0x00;//序列头类型
packet->m_body[i++] = 0x00;//合成时间
packet->m_body[i++] = 0x00;
packet->m_body[i++] = 0x00;
packet->m_body[i++] = 0x01;//版本
packet->m_body[i++] = live->sps[1];//编码规格sps[1]
packet->m_body[i++] = live->sps[2];//编码规格sps[2]
packet->m_body[i++] = live->sps[3];//编码规格sps[3]
packet->m_body[i++] = 0xFF;//NALU长度
packet->m_body[i++] = 0xE1;//sps个数
packet->m_body[i++] = (live->sps_len>>8) & 0xff;//sps长度高位
packet->m_body[i++] = live->sps_len & 0xff;//sps长度低位
memcpy(&packet->m_body[i], live->sps, live->sps_len);//sps内容
i += live->sps_len;
packet->m_body[i++] = 0x01;//pps个数
packet->m_body[i++] = (live->pps_len>>8) & 0xff;//pps长度高位
packet->m_body[i++] = live->pps_len & 0xff;//pps长度低位
memcpy(&packet->m_body[i], live->pps, live->pps_len);//pps内容
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
packet->m_nInfoField2 = live->rtmp->m_stream_id;
packet->m_nChannel =0x04;
packet->m_nBodySize = bodySize;
packet->m_nTimeStamp = 0;
packet->m_hasAbsTimestamp = 0;
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
return packet;
}
4.封装普通NAL单元信息:
RTMPPacket* buildVideoPackage(int8_t *buffer, int len, long tms, RTMP *rtmp){
buffer += 4;//去掉分隔符
len -= 4;
RTMPPacket *videoPacket = static_cast<RTMPPacket *>(malloc(sizeof(RTMPPacket)));
RTMPPacket_Alloc(videoPacket, len+9);//申请空间大小需要加上头部的9个字节
videoPacket->m_body[0] = 0x27;
if (buffer[0] == 0x65) {//关键帧
videoPacket->m_body[0] = 0x17;
LOGI("发送关键帧 data");
}
videoPacket->m_body[1] = 0x01;
videoPacket->m_body[2] = 0x00;
videoPacket->m_body[3] = 0x00;
videoPacket->m_body[4] = 0x00;
//长度
videoPacket->m_body[5] = (len >> 24) & 0xff;
videoPacket->m_body[6] = (len >> 16) & 0xff;
videoPacket->m_body[7] = (len >> 8) & 0xff;
videoPacket->m_body[8] = (len) & 0xff;
memcpy(&videoPacket->m_body[9], buffer, len);
videoPacket->m_packetType = RTMP_PACKET_TYPE_VIDEO;
videoPacket->m_nInfoField2 = rtmp->m_stream_id;
videoPacket->m_nChannel =0x04;
videoPacket->m_nBodySize = len+9;
videoPacket->m_nTimeStamp = tms;
videoPacket->m_hasAbsTimestamp = 0;
videoPacket->m_headerType = RTMP_PACKET_SIZE_LARGE;
return videoPacket;
}
利用librtmp发包:
int sendPackageData(RTMPPacket *packet){
int ret = RTMP_SendPacket(live->rtmp, packet, 1);//1代表加入发包队列
RTMPPacket_Free(packet);
free(packet);
return ret;
}
好了基本思路就是这样的,至于写完如何验证程序正确性,你可以自己搭建Nginx服务器,也可以选择白嫖斗鱼的服务器,我用的后者,申请斗鱼主播,然后开播后会给你一个rtmp推流地址和推流码,拼接后就是librtmp需要的url地址,不过推流码有时间限制,最后看个推流成功的动态图结束吧。
参考文章链接:
Demo下载链接:
更多推荐
所有评论(0)