观看一些户外直播时,我们观众端看到的是主播摄像头的内容,这是如何实现的呢?这篇将手写一个直播Demo。
在上一篇中,可以拍摄camera的数据,并加上背景音乐,其实只要解决了如何推流到服务器就可以了。我们使用 Rtmp 来传输 Rtmp Packet 数据,需要用到 NDK 开发。
基本流程
- 获取camera和录音数据(byte[])
- 对数据进行 h264 编码
- 封装Rtmp 数据包
-
上传到直播服务器推流地址
一.前期准备
因为要用到推流服务器,所以需要自己自行搭建流媒体服务器,可以参照这篇,使用Nginx+rtmp搭建流媒体服务器 - 简书 (jianshu.com),需要对linux懂一些常识。
至于服务器,最开始想用vmware的网络转发来对外,然后手机连接使用,但是发现电脑上可以ping通,但是手机上ping不了,还是得买个带公网的云服务器,翻了下,腾讯云有轻量云服务,首年几十块钱挺合适,自己搭建做些探索性的工作够了。
3.配置cmakeList
需要加入rtmp包
然后配置配置cmakeList
# 添加 define -DNO_CRYPTO,在c文件可使用,
#1. CMAKE_C_FLAGS介绍:https://cloud.tencent.com/developer/article/1433578
#2. define介绍:https://blog.csdn.net/chouhuan1877/article/details/100808689
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DNO_CRYPTO")
#或者使用 add_definitions(-DTEST_DEBUG),这样在cxx_flags,c_flags都有
AUX_SOURCE_DIRECTORY(${CMAKE_SOURCE_DIR} SRC_LIST)
AUX_SOURCE_DIRECTORY(${CMAKE_SOURCE_DIR}/librtmp RTMP_LIST)
add_library( # Sets the name of the library.
live-push
# Sets the library as a shared library.
SHARED
${RTMP_LIST}
${SRC_LIST}
# Provides a relative path to your source file(s).
native-lib.cpp
DZPacketQueue.cpp
DZJNICall.cpp
DZLivePush.cpp
)
要注意,-DNO_CRYPTO得加上,要不然编译不通过,这是一个变量,和define的全局变量有点像,NO_CRYPTO = ture。
二.代码
视频推流
在上一篇基础上,VideoEncoderThread中加入打印代码,就可以看到视频的数据,可以先行打印看下,然后对照着sps、pps、I、P等帧的类型,推的流其实就是遵守一定协议的,一串二进制码。
// 返回有效数据填充的输出缓冲区的索引
int outputBufferIndex = mVideoCodec.dequeueOutputBuffer(bufferInfo,0);
if(outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
//TODO
ByteBuffer byteBuffer = mVideoCodec.getOutputFormat().getByteBuffer("csd-0");
mVideoSps = new byte[byteBuffer.remaining()];
byteBuffer.get(mVideoSps,0,mVideoSps.length);
String videoData = parseByte2HexStr(mVideoPps);
Log.e(TAG+" sps",videoData);
byteBuffer = mVideoCodec.getOutputFormat().getByteBuffer("csd-1");
mVideoPps = new byte[byteBuffer.remaining()];
byteBuffer.get(mVideoPps,0,mVideoPps.length);
videoData = parseByte2HexStr(mVideoSps);
Log.e(TAG+" pps",videoData);
}
// 获取数据
ByteBuffer outBuffer = mVideoCodec.getOutputBuffers()[outputBufferIndex];
outBuffer.position(bufferInfo.offset);
outBuffer.limit(bufferInfo.offset+bufferInfo.size);
// 修改视频的 pts,基准时间戳
if(videoPts ==0)
videoPts = bufferInfo.presentationTimeUs;
bufferInfo.presentationTimeUs -= videoPts;
byte[] mVideoBytes = new byte[outBuffer.remaining()];
outBuffer.get(mVideoBytes,0,mVideoBytes.length);
String v1 = parseByte2HexStr(mVideoBytes);
Log.e(TAG+":",v1);
可以看到结果:
都是以00000001开头,然后跟着类型码及数据。最开始的为sps、pps打头。
41十六进制转为二进制为1000001,对照下表6-7位,可以看到为P帧。
65十六进制转为二进制为1100101,为I帧。
每隔30个P帧,为一个I帧。
VideoEncoderThread整体改造后如下:
private class VideoEncoderThread extends Thread{
WeakReference<BaseVideoPush> videoRecorderWf;
private boolean shouldExit =false;
private MediaCodec mVideoCodec;
MediaCodec.BufferInfo bufferInfo;
CyclicBarrier stopCb;
long videoPts = 0;
/**
* 视频轨道
*/
private int mVideoTrackIndex = -1;
byte[] mVideoPps;
byte[] mVideoSps;
public VideoEncoderThread(WeakReference<BaseVideoPush> videoRecorderWf){
this.videoRecorderWf = videoRecorderWf;
this.mVideoCodec = videoRecorderWf.get().mVideoCodec;
this.stopCb = videoRecorderWf.get().stopCb;
bufferInfo = new MediaCodec.BufferInfo();
}
@Override
public void run() {
mVideoCodec.start();
while (true){
try {
if(shouldExit){
onDestroy();
return;
}
// 返回有效数据填充的输出缓冲区的索引
int outputBufferIndex = mVideoCodec.dequeueOutputBuffer(bufferInfo,0);
if(outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
//TODO
ByteBuffer byteBuffer = mVideoCodec.getOutputFormat().getByteBuffer("csd-0");
mVideoSps = new byte[byteBuffer.remaining()];
byteBuffer.get(mVideoSps,0,mVideoSps.length);
String videoData = parseByte2HexStr(mVideoPps);
Log.e(TAG+" pps",videoData);
byteBuffer = mVideoCodec.getOutputFormat().getByteBuffer("csd-1");
mVideoPps = new byte[byteBuffer.remaining()];
byteBuffer.get(mVideoPps,0,mVideoPps.length);
videoData = parseByte2HexStr(mVideoSps);
Log.e(TAG+" sps",videoData);
}else {
while (outputBufferIndex >= 0){
if(bufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME){
videoRecorderWf.get().livePush.pushSpsPps(mVideoSps,mVideoSps.length,mVideoPps,mVideoPps.length);
String v1 = parseByte2HexStr(mVideoSps);
Log.e(TAG+"sps data:",v1);
String v2 = parseByte2HexStr(mVideoPps);
Log.e(TAG+"pps data:",v2);
}
// 获取数据
ByteBuffer outBuffer = mVideoCodec.getOutputBuffers()[outputBufferIndex];
outBuffer.position(bufferInfo.offset);
outBuffer.limit(bufferInfo.offset+bufferInfo.size);
// 修改视频的 pts,基准时间戳
if(videoPts ==0)
videoPts = bufferInfo.presentationTimeUs;
bufferInfo.presentationTimeUs -= videoPts;
byte[] mVideoBytes = new byte[outBuffer.remaining()];
outBuffer.get(mVideoBytes,0,mVideoBytes.length);
String v1 = parseByte2HexStr(mVideoBytes);
Log.e(TAG+":",v1);
videoRecorderWf.get().livePush.pushVideo(mVideoBytes,mVideoBytes.length,
bufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME);
if(videoRecorderWf.get().recordInfoListener != null){
// us,需要除以1000转为 ms
videoRecorderWf.get().recordInfoListener.onTime( bufferInfo.presentationTimeUs / 1000);
}
// 释放 outBuffer
mVideoCodec.releaseOutputBuffer(outputBufferIndex,false);
outputBufferIndex = mVideoCodec.dequeueOutputBuffer(bufferInfo,0);
}
}
} catch (Exception e){
e.printStackTrace();
}
}
}
private void onDestroy() {
try {
if (mVideoCodec != null){
mVideoCodec.stop();
mVideoCodec.release();
mVideoCodec = null;
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void requestExit() {
shouldExit = true;
}
}
视频的数据分为sps、pps,还有i帧及p帧,需要sps、pps的数据先推上去
videoRecorderWf.get().livePush.pushSpsPps(mVideoSps,mVideoSps.length,mVideoPps,mVideoPps.length)
这里调用JNI的代码
先把几个数据帧的结构发一下
更细一点的结构如下,也是在代码里要用到的:
// frame type : 1关键帧,2 非关键帧 (4bit)
// CodecID : 7表示 AVC (4bit) , 与 frame type 组合起来刚好是 1 个字节 0x17
// fixed : 0x00 0x00 0x00 0x00 (4byte)
// configurationVersion (1byte) 0x01版本
// AVCProfileIndication (1byte) sps[1] profile
// profile_compatibility (1byte) sps[2] compatibility
// AVCLevelIndication (1byte) sps[3] Profile level
// lengthSizeMinusOne (1byte) 0xff 包长数据所使用的字节数
// sps + pps 的数据
// sps number (1byte) 0xe1 sps 个数
// sps data length (2byte) sps 长度
// sps data sps 的内容
// pps number (1byte) 0x01 pps 个数
// pps data length (2byte) pps 长度
// pps data pps 的内容
因此,可以编写
void DZLivePush::pushSpsPps(jbyte* sps_data, jint sps_length, jbyte* pps_data, jint pps_length) {
// frame type : 1关键帧,2 非关键帧 (4bit)
// CodecID : 7表示 AVC (4bit) , 与 frame type 组合起来刚好是 1 个字节 0x17
// fixed : 0x00 0x00 0x00 0x00 (4byte)
// configurationVersion (1byte) 0x01版本
// AVCProfileIndication (1byte) sps[1] profile
// profile_compatibility (1byte) sps[2] compatibility
// AVCLevelIndication (1byte) sps[3] Profile level
// lengthSizeMinusOne (1byte) 0xff 包长数据所使用的字节数
// sps + pps 的数据
// sps number (1byte) 0xe1 sps 个数
// sps data length (2byte) sps 长度
// sps data sps 的内容
// pps number (1byte) 0x01 pps 个数
// pps data length (2byte) pps 长度
// pps data pps 的内容
int bodySize = sps_length + pps_length + 16;
RTMPPacket* rtmpPacket = (RTMPPacket *) malloc(sizeof(RTMPPacket));
RTMPPacket_Alloc(rtmpPacket,bodySize);
RTMPPacket_Reset(rtmpPacket);
int index = 0;
char* body = rtmpPacket->m_body;
//标识位 sps pps,AVC sequence header 与IDR一样
body[index++] = 0x17;
//跟着的补齐
body[index++] = 0x00;
body[index++] = 0x00;
body[index++] = 0x00;
body[index++] = 0x00;
//版本
body[index++] = 0x01;
//编码规格
body[index++] = sps_data[1];
body[index++] = sps_data[2];
body[index++] = sps_data[3];
// reserved(111111) + lengthSizeMinusOne(2位 nal 长度) 总是0xff
body[index++] = 0xff;
// reserved(111) + lengthSizeMinusOne(5位 sps 个数) 总是0xe1
body[index++] = 0xe1;
//sps length 2字节
body[index++] = (sps_length >> 8) & 0xff; //第0个字节
body[index++] = sps_length & 0xff; //第1个字节
// sps data
memcpy(&body[index], sps_data, sps_length);
index += sps_length;
//pps
body[index++] = 0x01;
body[index++] = (pps_length >> 8) & 0XFF;
body[index++] = pps_length & 0xFF;
memcpy(&body[index], pps_data, pps_length);
rtmpPacket->m_hasAbsTimestamp = 0;
rtmpPacket->m_nTimeStamp = 0;
rtmpPacket->m_nBodySize = bodySize;
rtmpPacket->m_packetType = RTMP_PACKET_TYPE_VIDEO;
rtmpPacket->m_headerType = RTMP_PACKET_SIZE_LARGE;
rtmpPacket->m_nChannel = 0x04;
rtmpPacket->m_nInfoField2 = rtmp->m_stream_id;
// LOGE("sps pps 发送到dzPacketQueue");
dzPacketQueue->push(rtmpPacket);
}
推送video的I帧、P帧如下
void DZLivePush::pushVideo(jbyte *videoByte, jint length,jboolean isKeyFrame) {
// frame type : 1关键帧,2 非关键帧 (4bit)
// CodecID : 7表示 AVC (4bit) , 与 frame type 组合起来刚好是 1 个字节 0x17
// fixed : 0x01 0x00 0x00 0x00 (4byte) 0x01 表示 NALU 单元
// video data length (4byte) video 长度
// video data
// 数据的长度(大小) = dataLen + 9
int bodySize = 9+length;
RTMPPacket* packet = (RTMPPacket *)(malloc(sizeof(RTMPPacket)));
RTMPPacket_Alloc(packet,bodySize);
RTMPPacket_Reset(packet);
int index = 0;
char* body = packet->m_body;
// frame type : 1关键帧,2 非关键帧 (4bit)
// CodecID : 7表示 AVC (4bit) , 与 frame type 组合起来刚好是 1 个字节 0x17
if(isKeyFrame)
body[index++] =0x17;
else
body[index++] =0x27;
body[index++] =0x01;
body[index++] =0x00;
body[index++] =0x00;
body[index++] =0x00;
body[index++] =(length >> 24) & 0xFF;
body[index++] =(length >> 16) & 0xFF;
body[index++] =(length >> 8) & 0xFF;
body[index++] =length & 0xFF;
memcpy(&body[index],videoByte,length);
packet->m_nBodySize = bodySize;
packet->m_nChannel = 0x04;
packet->m_nTimeStamp = RTMP_GetTime() - startTime;
packet->m_hasAbsTimestamp = 0;
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
packet->m_nInfoField2 = rtmp->m_stream_id;
// LOGE("I P 发送到dzPacketQueue");
dzPacketQueue->push(packet);
}
这些都是固定写法,packet->m_nChannel=0x04,音频推送时也需要04,要不音频推送写个05,会发现无法正常播放声音。
音频推流
现在推音频流,使用AudioRecord 采集音频数据
bufferSizeInBytes = AudioRecord.getMinBufferSize(
AUDIO_SAMPLE_RATE,
AudioFormat.CHANNEL_IN_STEREO,
AudioFormat.ENCODING_PCM_16BIT);
audioRecord = new AudioRecord(
MediaRecorder.AudioSource.MIC,
AUDIO_SAMPLE_RATE,
AudioFormat.CHANNEL_IN_STEREO,
AudioFormat.ENCODING_PCM_16BIT,
bufferSizeInBytes);
mAdudioData = new byte[bufferSizeInBytes];
//开始采集
audioRecord.startRecording();
//录取录制的数据
audioRecord.read(mAdudioData,0,bufferSizeInBytes);
因为原始的音频文件如采样率之类的,可能不是我们规定的,这里推到mediacodec中,再拿到数据推流
audioRecord.read(mAdudioData,0,bufferSizeInBytes);
int inputBufferTrack = mAudioCodec.dequeueInputBuffer(0);
if(inputBufferTrack >= 0){
ByteBuffer inputBuffer = mAudioCodec.getInputBuffers()[inputBufferTrack];
inputBuffer.clear();
inputBuffer.put(mAdudioData);
//0.41795918 *1000 000
audioPts += 1000000 * bufferSizeInBytes * 1.0f / AUDIO_SAMPLE_RATE * AUDIO_CHANNELS * 2;
//数据放入mAudioCodec的队列中
mAudioCodec.queueInputBuffer(inputBufferTrack,0,bufferSizeInBytes,audioPts,0);
}
和视频一样的取数据
mAudioCodec.start();
while (true){
try {
if(shouldExit){
onDestroy();
return;
}
// 返回有效数据填充的输出缓冲区的索引
int outputBufferIndex = mAudioCodec.dequeueOutputBuffer(bufferInfo,0);
while (outputBufferIndex >= 0){
// Log.e(TAG,"outputBufferIndex:"+outputBufferIndex+" count:"+index);
// 获取数据
ByteBuffer outBuffer = mAudioCodec.getOutputBuffers()[outputBufferIndex];
outBuffer.position(bufferInfo.offset);
outBuffer.limit(bufferInfo.offset+bufferInfo.size);
// 修改视频的 pts,基准时间戳
if(audioPts ==0)
audioPts = bufferInfo.presentationTimeUs;
bufferInfo.presentationTimeUs -= audioPts;
byte[] audioData = new byte[outBuffer.remaining()];
outBuffer.get(audioData,0,audioData.length);
recorderReference.get().livePush.pushAudio(audioData,audioData.length);
// 释放 outBuffer
mAudioCodec.releaseOutputBuffer(outputBufferIndex,false);
outputBufferIndex = mAudioCodec.dequeueOutputBuffer(bufferInfo,0);
}
} catch (Exception e){
e.printStackTrace();
}
}
RTMP 包中封装的音视频数据流,其实和FLV/tag封装音频和视频数据的方式是相同的,所以我们只需要按照FLV格式封装音视频即可。
void DZLivePush::pushAudio(jbyte *audioData, jint audioLen) {
// 2 字节头信息
// 前四位表示音频数据格式 AAC 10(A)
// 五六位表示采样率 0 = 5.5k 1 = 11k 2 = 22k 3(11) = 44k
// 七位表示采样采样的精度 0 = 8bits 1 = 16bits
// 八位表示音频类型 0 = mono 1 = stereo
// 组合起来:1010 1111 -,算出来第一个字节是 0xAF
// 0x01 代表 aac 原始数据
int bodySize = audioLen+2;
RTMPPacket* packet = (RTMPPacket *)(malloc(sizeof(RTMPPacket)));
RTMPPacket_Alloc(packet,bodySize);
RTMPPacket_Reset(packet);
char * body = packet->m_body;
//上面推算出
body[0] = 0xaf;
body[1] = 0x01;
memcpy(&body[2],audioData,audioLen);
packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
packet->m_nInfoField2 = rtmp->m_stream_id;
packet->m_hasAbsTimestamp = 0;
packet->m_nTimeStamp = RTMP_GetTime()-startTime;
packet->m_nChannel = 0x04;
packet->m_nBodySize = bodySize;
LOGE("AAC 发送到dzPacketQueue");
dzPacketQueue->push(packet);
}
这样就可以了。
停止
因为是开启的线程推流到服务器
void DZLivePush::initConnect() {
pthread_create(&initConnectTid,NULL, initConnectRun,this);
}
void *initConnectRun(void * context){
//不断循环取数据上传到服务器
...
while (pLivePush->isPushing){
RTMPPacket* packet = pLivePush->dzPacketQueue->pop();
if(packet != NULL){
int send_result = RTMP_SendPacket(pLivePush->rtmp,packet,1);
LOGE("send_result: %d",send_result);
RTMPPacket_Free(packet);
free(packet);
packet = NULL;
}
}
...
}
所以加一个退出标识,然后pthread_join等待线程完成退出。
void DZLivePush::stop() {
isPushing = false;
pthread_join(initConnectTid,NULL);
LOGE("等待停止");
}
这样代码就写完了。
验证
可以使用下载的
Builds - CODEX FFMPEG @ gyan.dev
ffmpeg for windows,使用ffplay rtmp://自己的流媒体IP:1935/cctvf/mystream来播放了。
代码在这里:livepush at github
参考: