音视频编解码系列目录:
Android 音视频基础知识
Android 音视频播放器 Demo(一)------ 视频解码与渲染
Android 音视频播放器 Demo(二)------ 音频解码与音视频同步
RTMP 直播推流 Demo(一)------ 项目配置与视频预览
RTMP 直播推流 Demo(二)------ 音频推流与视频推流
上一节我们对项目进行了配置,并且实现了摄像头预览,摄像头采集到的图像数据已经可以通过 LivePusher 传递到 Native 层,接下来就可以开始音视频编码与推流了。
1、视频编码
通过 CameraHelper 可以获取视频的帧数据,我们需要对其进行编码再将数据"塞"到 RTMPPacket 中,然后才能将 RTMPPacket 发送给服务器。
编码之前,需要先对编码器等元素进行初始化。
1.1 初始化
Native 层的初始化是由 LivePusher 触发的:
kotlin
class LivePusher(
activity: Activity,
cameraId: Int,
previewWidth: Int,
previewHeight: Int,
fps: Int,
bitrate: Int
) {
private val mVideoChannel: VideoChannel
private val mAudioChannel: AudioChannel
init {
// 这个 nativeInit() 要在 AudioChannel 之前初始化,因为后者初始化时会用到 Native 方法
nativeInit()
mVideoChannel =
VideoChannel(this, activity, cameraId, previewWidth, previewHeight, fps, bitrate)
mAudioChannel = AudioChannel(this, 44100, 2)
}
}
Native 入口为 native-lib,初始化时要创建 Native 层的 VideoChannel 和 AudioChannel:
cpp
// 视频通道,处理视频编码等工作
VideoChannel *videoChannel = nullptr;
// 音频通道,处理音频编码
AudioChannel *audioChannel = nullptr;
extern "C"
JNIEXPORT void JNICALL
Java_com_rtmp_pusher_LivePusher_nativeInit(JNIEnv *env, jobject thiz) {
videoChannel = new VideoChannel;
videoChannel->setVideoCallback(callback);
audioChannel = new AudioChannel;
audioChannel->setAudioCallback(callback);
// releasePacket 是丢弃 AVPacket 队列中元素的回调函数
packets.setReleaseCallback(releasePacket);
}
我们使用 x264 进行视频编码,在编码前需要对 x264 编码器进行初始化,初始化的时机是 SurfaceView 的尺寸发生变化时(因为编码器初始化需要宽高数据)。在 CameraHelper 的 setPreviewOrientation() 中我们通过 OnSurfaceSizeChangedListener 将宽高变化发送出去了,在 VideoChannel 中接收时做编码器的初始化:
kotlin
// CameraHelper.OnSurfaceSizeChangedListener
override fun onSizeChanged(width: Int, height: Int) {
mLivePusher.nativeInitVideoEncoder(width, height, mFps, mBitrate)
}
在 native-lib 的对应函数中,将初始化的具体工作交给 Native 的 VideoChannel:
cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_rtmp_pusher_LivePusher_nativeInitVideoEncoder(JNIEnv *env, jobject thiz, jint width,
jint height, jint fps, jint bitrate) {
if (videoChannel) {
videoChannel->initVideoEncoder(width, height, fps, bitrate);
}
}
initVideoEncoder() 主要是设置 x264 编码器的参数并打开编码器,此外还需根据宽高数据创建接收视频一帧数据(理解成一张图片)的对象 x264_picture_t:
cpp
/**
* 初始化 x264 编码器,为了防止被多次初始化,需要加互斥锁保证线程安全
*/
void VideoChannel::initVideoEncoder(int width, int height, int fps, int bitrate) {
pthread_mutex_lock(&mutex);
// 初始化编码器所需的参数
mWidth = width;
mHeight = height;
mFps = fps;
mBitrate = bitrate;
// y 的个数为宽高之积,uv 的数量分别都是 y 的 1/4
y_len = width * height;
uv_len = y_len / 4;
// 防止重复初始化编码器
if (videoEncoder) {
x264_encoder_close(videoEncoder);
videoEncoder = nullptr;
}
// 防止重复初始化一帧图片
if (pic_in) {
x264_picture_clean(pic_in);
DELETE(pic_in)
}
// 初始化 x264 编码器
x264_param_t param;
// 超快、零延迟,因为直播要求时效性
x264_param_default_preset(¶m, "ultrafast", "zerolatency");
// 编码级别,代表了编码器能够处理的视频参数和编码限制,如视频分辨率、帧率、比特率等,32 为中等偏上,
// 对应的最大编码码率为 2000,最大清晰度为 1280 * 720,最高帧率为 60 帧
param.i_level_idc = 32;
// 输出格式为 YUV420P,P 是 Planar 表示 UV 平面格式,例如 VVVVUUUU,没有 P 就是交错模式 VUVUVUVU
param.i_csp = X264_CSP_I420;
param.i_width = width;
param.i_height = height;
// 设置两个 picture 之间的 B 帧数量为 0。直播不能有 B 帧,因为影响编解码效率
param.i_bframe = 0;
// 码率控制方式:CQP(恒定质量)、CRF(恒定码率)、ABR(平均码率)
param.rc.i_rc_method = X264_RC_CRF;
// 码率、瞬时最大码率、码率控制区大小(单位 Kb/s,设置了瞬时最大码率就必须设置它)
param.rc.i_bitrate = bitrate / 1000;
param.rc.i_vbv_max_bitrate = bitrate / 1000 * 1.2;
param.rc.i_vbv_buffer_size = bitrate / 1000;
// 0 表示只使用 fps 进行码率控制,1 表示使用时间基和时间戳
param.b_vfr_input = 0;
// 帧率分子与分母
param.i_fps_num = fps;
param.i_fps_den = 1;
// 时间基的分子与分母,二者做除法可以算出帧之间的时间间隔,音视频同步时有用
param.i_timebase_num = param.i_fps_den;
param.i_timebase_den = param.i_fps_num;
// 设置编码器的最大关键帧间隔,编码器在编码视频时会尽量保持每隔 fps * 2 帧生成一个关键帧,即约 2 秒一个关键帧
param.i_keyint_max = fps * 2;
// 是否重复头部,设置为 1,编码时会在每个关键帧之前加入 SPS/PPS
param.b_repeat_headers = 1;
// 指定并行编码线程数,如果设置为 0 则会由 x264 编码库自动决定线程数
param.i_threads = 1;
// 将 param 参数应用到 H.264/AVC 规范定义的编码配置文件上,这些文件包括 baseline、main、high 等
x264_param_apply_profile(¶m, "baseline");
// 为 pic_in 分配内存空间并初始化内部成员
pic_in = new x264_picture_t;
x264_picture_alloc(pic_in, param.i_csp, param.i_width, param.i_height);
// 打开编码器
videoEncoder = x264_encoder_open(¶m);
if (videoEncoder) {
LOGD("成功打开x264编码器");
}
pthread_mutex_unlock(&mutex);
}
说明:
-
参数设置中用到的方法 x264_param_default_preset() 和 x264_param_apply_profile() 都有可选参数,这些参数在源码的函数上面以数组的形式呈现。比如 x264_param_default_preset():
cppstatic const char * const x264_preset_names[] = { "ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow", "placebo", 0 }; static const char * const x264_tune_names[] = { "film", "animation", "grain", "stillimage", "psnr", "ssim", "fastdecode", "zerolatency", 0 }; /* returns 0 on success, negative on failure (e.g. invalid preset/tune name). */ X264_API int x264_param_default_preset( x264_param_t *, const char *preset, const char *tune );
x264_preset_names 就是 preset 的可选参数数组,x264_tune_names 就是 tune 的可选参数数组,如果选 0 表示使用默认配置。其他函数也是类似的套路。
-
参数全部设置完毕后通过 x264_encoder_open() 打开编码器会得到一个 x264_t 类型的对象,也就是编码器对象,我们将其设置为成员变量:
cppclass VideoChannel { ... private: ... // x264 解码器 x264_t *videoEncoder; // 表示 x264 的一帧图片 x264_picture_t *pic_in; };
此外 x264_picture_t 表示 x264 的一帧图片,也保存为成员变量,在通过 x264_picture_alloc() 为其分配了内存之后,编码器的初始化工作就完成了
1.2 将数据传递到 Native 层
初始化完毕可以进行编码了,上层的 VideoChannel 在 CameraHelper.OnPreviewListener 接口的 onPreviewFrame() 中会接收到 CameraHelper 传来的视频数据:
kotlin
// CameraHelper.OnPreviewListener
override fun onPreviewFrame(data: ByteArray) {
if (mIsLiving) {
mLivePusher.nativePushVideo(data)
}
}
mIsLiving 表示是否开始直播推流了,通过如下方法进行控制:
kotlin
fun startLive() {
mIsLiving = true
}
fun stopLive() {
mIsLiving = false
}
具体的编码工作还是交给 Native 的 VideoChannel 执行:
cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_rtmp_pusher_LivePusher_nativePushVideo(JNIEnv *env, jobject thiz, jbyteArray data_) {
if (!videoChannel || !readyPushing) {
return;
}
jbyte *data = env->GetByteArrayElements(data_, nullptr);
videoChannel->encodeData(data);
env->ReleaseByteArrayElements(data_, data, 0);
}
encodeData() 的内容较多,我们放在下一小节中单独讲解。
1.3 编码
编码的工作包括如下内容:
- 将摄像头采集到的 NV21 格式数据转换为 I420 格式数据
- 使用 x264 对一帧图片进行编码
- 将编码后的数据发送到队列中
格式转换
由于安卓摄像头采集的是 NV21 格式,但 Windows 或 IOS 都是 I420 格式,因此需要进行转换。二者的不同之处在于 UV 的排列方式:
Y Y Y Y Y Y Y Y Y Y Y Y
Y Y Y Y Y Y Y Y Y Y Y Y
Y Y Y Y Y Y Y Y Y Y Y Y
Y Y Y Y Y Y Y Y Y Y Y Y
V U V U V U U U U U U U
V U V U V U V V V V V V
- NV21 - - I420 -
因此我们要把 YUV 按照 I420 的排列方式存入 pic_in 中:
cpp
void VideoChannel::encodeData(signed char *data) {
pthread_mutex_lock(&mutex);
/*
* 1.将摄像头采集到的 NV21 格式转换为 I420 格式
*/
// 先将 y 拷贝到 pic_in->img.plane[0]
memcpy(pic_in->img.plane[0], data, y_len);
// 再拷贝 vu,u、v 分别存放在 pic_in->img.plane[1],pic_in->img.plane[2]
// 在 NV21 中,vu 成对出现,v 在前
for (int i = 0; i < uv_len; ++i) {
*(pic_in->img.plane[1] + i) = *(data + y_len + i * 2 + 1);
*(pic_in->img.plane[2] + i) = *(data + y_len + i * 2);
}
...
}
用 x264 编码
使用 x264_encoder_encode() 对刚刚得到的 pic_in 进行编码,得到编码后的 NAL 数组 nal 以及 NALU 的数量 pi_nal,当然还有编码后的图片 pic_out:
cpp
void VideoChannel::encodeData(signed char *data) {
...
/*
* 2.编码
*/
// 编码后得到的 NAL 数组
x264_nal_t *nal = nullptr;
// NAL 中 NALU 的数量
int pi_nal;
// 编码后输出的图片
x264_picture_t pic_out;
// 编码一张图片,如果失败会返回负值
if (x264_encoder_encode(videoEncoder, &nal, &pi_nal, pic_in, &pic_out) < 0) {
LOGE("x264编码失败");
pthread_mutex_unlock(&mutex);
return;
}
...
}
发送到队列中
上一步我们得到了编码后的 NALU 数组,接下来要做的就是将这些 NALU 封装进 RTMPPacket 并发送到队列中。在转换过程中,对于不同的 NALU 类型有不同的操作。这里我们先简单复习一下 NALU 的相关知识。
H.264 原始码流(又称为裸流),是由一个接一个的 NALU 组成的:
每个 NALU 之间都有一个起始码 00 00 00 01(此 NALU 有多片)或 00 00 01(此 NALU 只有一片),而 NALU 本身由 NAL 头和挂载数据 RBSP 组成,其中 NAL 头的低五位表示帧类型,我们需要关注的类型有四种:
- NAL 头的数据为 0x67,低五位为 0x67 & 0x1F = 7,类型是 NAL_SPS,表示这是一个 SPS 帧
- NAL 头的数据为 0x68,低五位为 0x68 & 0x1F = 8,类型是 NAL_PPS,表示这是一个 PPS 帧
- NAL 头的数据为 0x65,低五位为 0x65 & 0x1F = 5,类型是 NAL_SLICE_IDR,表示这是一个 IDR 帧,即关键帧
- NAL 头的数据为 0x61,低五位为 0x61 & 0x1F = 1,类型是 NAL_SLICE,表示这是一个非关键帧
这四种类型的格式如下,其中 SPS 与 PPS 是放在一起的:
这里要解释一下为什么第一个字节是 0x17 或 0x27,下表是第一个字节的含义:
字段 | 占位 | 描述 |
---|---|---|
FrameType | 4 | 帧类型。 1: key frame(for AVC, a seekable frame) 2: inter frame(for AVC, a non-seekable frame) 3: disposable inter frame(H.263 only) 4: generated keyframe(reserved for server use only) 5: video info/command frame |
CodecID | 4 | 编码类型。 1: JPEG(目前未用到) 2: Sorenson H.263 3: Screen video 4: On2 VP6 5: On2 VP6 with alpha channel 6: Screen video version 2 7: AVC(高级视频编码) |
关键帧的 FrameType 是 1,非关键帧的 FrameType 是 2,CodecID 都是 AVC 即 7,因此关键帧的第一个字节是 0x17,非关键帧第一个字节是 0x27。
此外,SPS 与 PPS 的数据需要注意每个字节的内容:
接下来进入代码:
cpp
void VideoChannel::encodeData(signed char *data) {
/*
* 3.将编码后的数据发送到队列中
*/
// SPS 与 PPS 的长度
int sps_len, pps_len;
// SPS 与 PPS 的内容
uint8_t sps[100], pps[100];
// 视频帧的展示时间戳要进行累加
pic_in->i_pts += 1;
for (int i = 0; i < pi_nal; ++i) {
if (nal[i].i_type == NAL_SPS) {
// SPS 的长度和内容都要刨除起始码的 4 个字节
sps_len = nal[i].i_payload - 4;
memcpy(sps, nal[i].p_payload + 4, sps_len);
} else if (nal[i].i_type == NAL_PPS) {
// PPS 的长度和内容也要刨除起始码的 4 个字节
pps_len = nal[i].i_payload - 4;
memcpy(pps, nal[i].p_payload + 4, pps_len);
// PPS 在 SPS 后面,这里既然拿到了 PPS,说明 SPS 也已经拿到了,可以发送二者了
sendSpsPps(sps, pps, sps_len, pps_len);
} else {
// 发送非 SPS、PPS 的数据帧,可能是 I 帧或 P 帧
sendFrame(nal[i].i_type, nal[i].i_payload, nal[i].p_payload);
}
}
pthread_mutex_unlock(&mutex);
}
sendSpsPps() 参照上面的表格填写每个字节的数据:
cpp
/**
* 将 SPS 与 PPS 发送到队列中
*/
void VideoChannel::sendSpsPps(uint8_t *sps, uint8_t *pps, int sps_len, int pps_len) {
/*
* 1.创建一个 RTMPPacket 并分配空间
*/
// RTMPPacket 的数据长度
int body_size = 5 + 8 + sps_len + 3 + pps_len;
auto *packet = new RTMPPacket;
RTMPPacket_Alloc(packet, body_size);
/*
* 2.逐个字节填充 RTMPPacket 的内容
*/
int index = 0;
// SPS 与 PPS 的第一个字节都是 0x17
packet->m_body[index++] = 0x17;
// 第 2 ~ 5 个字节都是 0
packet->m_body[index++] = 0x00;
packet->m_body[index++] = 0x00;
packet->m_body[index++] = 0x00;
packet->m_body[index++] = 0x00;
// 第 6 个字节表示配置版本,这里我们写 0x01
packet->m_body[index++] = 0x01;
// 第 7 ~ 9 个字节分别是 sps[1]、sps[2]、sps[3]
packet->m_body[index++] = sps[1];
packet->m_body[index++] = sps[2];
packet->m_body[index++] = sps[3];
// 第 10 个字节是包长数据所使用的字节数,通常为 0xFF
packet->m_body[index++] = 0xFF;
// 第 11 个字节表示 SPS 个数,通常为 0xE1
packet->m_body[index++] = 0xE1;
// 接下来是 SPS 长度,用两个字节表示,先存高 8 位
packet->m_body[index++] = (sps_len >> 8) & 0xFF;
packet->m_body[index++] = sps_len & 0xFF;
// SPS 数据内容
memcpy(&packet->m_body[index], sps, sps_len);
index += sps_len;
// 最后是 PPS 的个数、长度和内容,与 SPS 类型
packet->m_body[index++] = 0x01;
packet->m_body[index++] = (pps_len >> 8) & 0xFF;
packet->m_body[index++] = pps_len & 0xFF;
memcpy(&packet->m_body[index], pps, pps_len);
/*
* 3.封包处理
*/
// 包的类型是视频包
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
// 包的大小
packet->m_nBodySize = body_size;
// 通道 ID,可随意赋值但不要与 rtmp.c 内的 m_nChannel = 0x04 冲突
packet->m_nChannel = 10;
// SPS 与 PPS 没有时间戳
packet->m_nTimeStamp = 0;
// 不使用绝对时间戳,而是使用相对时间戳
packet->m_hasAbsTimestamp = 0;
// 包的头部类型,像 SPS 与 PPS 这种不是很大的可以设置为中等大小
packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM;
/*
* 4.通过回调将 RTMPPacket 存入队列,因为队列是在 native-lib 中控制的
*/
videoCallback(packet);
}
sendFrame() 就是发送关键帧或非关键帧的数据,要比发送 SPS 和 PPS 简单些:
cpp
/**
* 将数据帧(I 帧或 P 帧)发送到队列中
* @param type 帧的类型
* @param payload_size 帧的数据长度
* @param payload 帧数据
*/
void VideoChannel::sendFrame(int type, int payload_size, uint8_t *payload) {
/*
* 1.先计算 RTMPPacket 的数据长度,该长度由固定的前 5 个字节,加上数据长度的 4 个
* 字节,最后加上 H.264 裸数据构成。真正的数据长度是 payload_size 减去起始码的长度
*/
if (payload[2] == 0x00) {
// 起始码是 00 00 00 01,那么有效的数据大小要减去 4 字节,
// payload 的指针也要相应的跳过起始码的 4 字节
payload_size -= 4;
payload += 4;
} else if (payload[2] == 0x01) {
// 起始码是 00 00 01
payload_size -= 3;
payload += 3;
}
int body_size = 5 + 4 + payload_size;
/*
* 2.创建 RTMPPacket 并填入数据
*/
auto packet = new RTMPPacket;
RTMPPacket_Alloc(packet, body_size);
// 关键帧的第 1 个字节是 0x17,非关键帧是 0x27
if (type == NAL_SLICE_IDR) {
packet->m_body[0] = 0x17;
} else {
packet->m_body[0] = 0x27;
}
// 第 2 个字节为 1 表示是关键帧或非关键帧,为 0 就是 PPS、SPS 包
packet->m_body[1] = 0x01;
packet->m_body[2] = 0x00;
packet->m_body[3] = 0x00;
packet->m_body[4] = 0x00;
// 接下来的 4 个字节是帧长度,先保存最高的 8 位,以此类推
packet->m_body[5] = (payload_size >> 24) & 0xFF;
packet->m_body[6] = (payload_size >> 16) & 0xFF;
packet->m_body[7] = (payload_size >> 8) & 0xFF;
packet->m_body[8] = payload_size & 0xFF;
// 从第 10 个字节开始拷贝帧数据
memcpy(&packet->m_body[9], payload, payload_size);
/*
* 3.封包操作,与 sendSpsPps() 类似,不同之处在于帧数据有时间戳,
* 并且 m_headerType 为 LARGE 包的类型是视频包
*/
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
// 包的大小
packet->m_nBodySize = body_size;
// 通道 ID,可随意赋值但不要与 rtmp.c 内的 m_nChannel 冲突
packet->m_nChannel = 10;
// 帧数据有时间戳
packet->m_nTimeStamp = -1;
// 不需要绝对或相对时间戳
packet->m_hasAbsTimestamp = 0;
// 包的类型,关键帧数据量大,要设置为大包
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
/*
* 4.把 RTMPPacket 回调给 native-lib 存入队列中
*/
videoCallback(packet);
}
两个发送函数最后都是通过 videoCallback 这个回调接口将 RTMPPacket 存入队列的:
cpp
class VideoChannel {
public:
typedef void (*VideoCallback)(RTMPPacket *packet);
void setVideoCallback(VideoCallback videoCallback);
private:
VideoCallback videoCallback;
}
native-lib 实现它:
cpp
SafeQueue<RTMPPacket *> packets;
void callback(RTMPPacket *packet) {
if (packet) {
if (packet->m_nTimeStamp == -1) {
// I/P/B帧需要时间戳,但是 SPS 和 PPS 本身没有时间戳也就不需要
packet->m_nTimeStamp = RTMP_GetTime() - start_time;
}
packets.put(packet);
}
}
如果 packet 的 m_nTimeStamp 之前设置为 -1,表示它需要时间戳,那么就计算出时间戳赋值给它,然后放进线程安全的队列 packets 中。
2、视频推流
用户点击界面的开始直播按钮,会触发 LivePusher 的 startLive(),进而调用 nativeStart(),执行对应的 Native 函数,需要开启一个子线程 pid_start 来连接 RTMP 服务器并发送 RTMPPacket 的工作:
cpp
/**
* 在子线程中开启 task_start 任务,通过 RTMP 连接服务器并推送数据
*/
extern "C"
JNIEXPORT void JNICALL
Java_com_rtmp_pusher_LivePusher_nativeStart(JNIEnv *env, jobject thiz, jstring path_) {
// 避免多次开启
if (isStart) {
return;
}
isStart = true;
const char *path = env->GetStringUTFChars(path_, nullptr);
// 深拷贝,以免在子线程中使用 path 时,path 已经在本函数末尾被回收
char *url = new char[strlen(path) + 1];
strcpy(url, path);
// 子线程开启 task_start 任务
pthread_create(&pid_start, nullptr, task_start, url);
// 回收 path
env->ReleaseStringUTFChars(path_, path);
}
线程任务 task_start 会连接 RTMP 服务器,并且在一个循环中不断取出 RTMPPacket 发送给服务器实现推流。RTMP 的使用步骤如下:
task_start 的内容就是实现如上步骤:
cpp
/**
* 链接 RTMP 服务器,并不断从队列中取出 RTMPPacket 发送至服务器
* 这里有两个问题需要再精进一些:
* 1.考虑是否要将 1 ~ 7 步放在 do-while(false) 循环中?
* 答:如果添加重试功能,可能需要这么做,以更好地进行流程控制
* 2.如果推流速度远远慢于 RTMPPacket 入队的速度怎么办?
* 答:可能会造成内存溢出,需要添加入队的速度控制,当前 Demo 代码可以正常运行
*/
void *task_start(void *args) {
char *url = static_cast<char *>(args);
// 1.申请内存
RTMP *rtmp = RTMP_Alloc();
if (!rtmp) {
LOGE("RTMP_Alloc failed");
return nullptr;
}
// 2.初始化
RTMP_Init(rtmp);
// 设置连接的超时时间,单位为秒
rtmp->Link.timeout = 5;
// 3.设置地址
if (!RTMP_SetupURL(rtmp, url)) {
LOGE("RTMP_SetupURL failed");
return nullptr;
}
// 4.开启输出模式
RTMP_EnableWrite(rtmp);
// 5.连接服务器
if (!RTMP_Connect(rtmp, nullptr)) {
LOGE("RTMP_Connect failed");
return nullptr;
}
// 6.连接流
if (!RTMP_ConnectStream(rtmp, 0)) {
LOGE("RTMP_ConnectStream failed");
return nullptr;
}
// 记录开始时间,准备向服务器推流
start_time = RTMP_GetTime();
readyPushing = true;
packets.setEnable(true);
RTMPPacket *packet = nullptr;
// 获取并将音频头存入队列,实测不用放这个头也是可以成功将音频推流的
callback(audioChannel->getAudioSeqHeader());
// 在循环中将数据发送至服务器
while (readyPushing) {
// 从队列中取出 packet,队列为空时会阻塞
packets.get(packet);
if (!readyPushing) {
break;
}
if (!packet) {
continue;
}
// 成功取出 packet 后为其设置流的 ID
packet->m_nInfoField2 = rtmp->m_stream_id;
// 7.发送数据,1 是开启内部缓冲,在收到结果前,packet 会保存在队列中
if (!RTMP_SendPacket(rtmp, packet, 1)) {
// RTMP 发送失败会自动断开与服务器的连接,因此就跳出循环
LOGE("RTMP_SendPacket failed");
releasePacket(&packet);
break;
}
}
// 考虑到可能从 if (!readyPushing) 跳出循环的情况,也要进行释放
releasePacket(&packet);
// 更新状态
isStart = false;
readyPushing = false;
packets.setEnable(false);
packets.clear();
if (rtmp) {
// 8.关闭连接
RTMP_Close(rtmp);
// 9.释放
RTMP_Free(rtmp);
}
DELETE(url)
return nullptr;
}
代码就是按照图片给出的顺序连接服务器,然后在推流状态下不断从队列中取出 RTMPPacket,通过 RTMP_SendPacket() 发送给服务器,从而实现推流。
3、音频编码与推流
3.1 采集音频
上层的 AudioChannel 控制 AudioRecord 录制音频:
kotlin
class AudioChannel(
private val mLivePusher: LivePusher,
sampleRateInHz: Int,
channels: Int
) {
private val mExecutor: ExecutorService
private var mAudioRecord: AudioRecord?
// 比如单通道样本数是 1024,那么双声道就是 2048,再加上采样位数是 16 位,
// 再乘 2 就是 4096,也就是 mInputSamples 的值了
private val mInputSamples: Int
private var mIsLiving = false
init {
mExecutor = Executors.newSingleThreadExecutor()
// 通道配置
val channelConfig = if (channels == 2)
AudioFormat.CHANNEL_IN_STEREO
else
AudioFormat.CHANNEL_IN_MONO
// AudioRecord 所需最小的缓冲区大小
val minBufferSize = AudioRecord.getMinBufferSize(
sampleRateInHz,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT // 采样位数设置为 16 位
) * 2
// 还需获取 faac 编码器缓冲区的大小,获取前需要先初始化 faac 编码器
mLivePusher.nativeInitAudioEncoder(sampleRateInHz, channels)
// 采样位数为 16 位,需要再乘以 2 个字节
mInputSamples = mLivePusher.getInputSamples() * 2
mAudioRecord = AudioRecord(
MediaRecorder.AudioSource.MIC,
sampleRateInHz,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
max(minBufferSize, mInputSamples) // 缓冲区取大的,如果取小了数据不完整
)
}
fun startLive() {
mIsLiving = true
mExecutor.submit {
// 开始录音
mAudioRecord?.startRecording()
// 录音缓冲区
val bytes = ByteArray(mInputSamples)
var len: Int
while (mIsLiving) {
len = mAudioRecord?.read(bytes, 0, bytes.size) ?: 0
if (len > 0) {
mLivePusher.nativePushAudio(bytes)
}
}
mAudioRecord?.stop()
}
}
fun stopLive() {
mIsLiving = false
}
fun release() {
mAudioRecord?.release()
mAudioRecord = null
}
}
3.2 Native 层
上层的 AudioChannel 在 init 时调用了 LivePusher 的 nativeInitAudioEncoder() 对音频编码器进行初始化,这项工作落到 Native 层的 AudioChannel 上:
cpp
void AudioChannel::initAudioEncoder(int sample_rate_in_hz, int channels) {
this->channels = channels;
// 1.打开 faac 编码器。前两个是入参,后两个是出参
audioEncoder = faacEncOpen(sample_rate_in_hz, channels, &inputSamples, &maxOutputBytes);
if (!audioEncoder) {
LOGE("打开 faac 编码器失败");
return;
}
// 2.配置编码器参数
faacEncConfigurationPtr config = faacEncGetCurrentConfiguration(audioEncoder);
// 使用 MPEG4 版本编码
config->mpegVersion = MPEG4;
// LC 标准,因为要求的质量不高,因此编解码较快
config->aacObjectType = LOW;
// 16 位
config->inputFormat = FAAC_INPUT_16BIT;
// 比特流输出格式为 Raw,设置为 1 的话就是 ADTS
config->outputFormat = 0;
// 开启 Temporal Noise Shaping,该音频编码技术用于减小编码后的音频中的噪声
config->useTns = 1;
// 禁用 Low-Frequency Effect 低频效果
config->useLfe = 0;
// 3.把参数设置给编码器
if (!faacEncSetConfiguration(audioEncoder, config)) {
LOGE("FAAC 音频编码器参数配置失败");
return;
}
LOGD("FAAC 音频编码器初始化成功");
buffer = new u_char(maxOutputBytes);
}
随后,上层的 AudioChannel 采集到一帧音频,就会调用 LivePusher 的 nativePushAudio() 让 Native 层编码:
cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_rtmp_pusher_LivePusher_nativePushAudio(JNIEnv *env, jobject thiz, jbyteArray data_) {
if (!audioChannel || !readyPushing) {
return;
}
jbyte *data = env->GetByteArrayElements(data_, nullptr);
audioChannel->encodeData(data);
env->ReleaseByteArrayElements(data_, data, 0);
}
具体的编码工作还是由 Native 的 AudioChannel 完成:
cpp
void AudioChannel::encodeData(int8_t *data) {
// 1.编码音频数据
int byteLen = faacEncEncode(audioEncoder, reinterpret_cast<int32_t *>(data),
inputSamples, buffer, maxOutputBytes);
if (byteLen > 0) {
// 2.实例化 RTMPPacket 并分配空间
auto packet = new RTMPPacket;
int body_size = 2 + byteLen;
RTMPPacket_Alloc(packet, body_size);
// 3.设置 RTMPPacket 内容
// 声道数
if (channels == 2) {
packet->m_body[0] = 0xAF;
} else {
packet->m_body[0] = 0xAE;
}
// 编码出的音频数据都是 0x01
packet->m_body[1] = 0x01;
// 音频数据
memcpy(&packet->m_body[2], buffer, byteLen);
// 4.封包,与视频封包类似
// 包类型为音频
packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;
packet->m_nBodySize = body_size;
packet->m_nChannel = 11;
// 音频帧数据有时间戳
packet->m_nTimeStamp = -1;
// 一般不用绝对时间戳
packet->m_hasAbsTimestamp = 0;
// 帧数据类型一般是大包,如果是头信息,可以给一个中包或小包
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
// 5.回调加入队列
audioCallback(packet);
}
}
由于 RTMPPacket 本身是不区分音频与视频的,也就是说音视频数据编码完成后都可以存入 RTMPPacket 然后通过回调添加到 native-lib 的 RTMPPacket 队列中。最后的 audioCallback 在讲视频初始化时已经写过,回调函数与视频通道的回调函数相同,都是 native-lib 的 callback():
cpp
void callback(RTMPPacket *packet) {
if (packet) {
if (packet->m_nTimeStamp == -1) {
// I 帧需要时间戳,但是 SPS 和 PPS 本身没有时间戳也就不需要
packet->m_nTimeStamp = RTMP_GetTime() - start_time;
}
packets.put(packet);
}
}
此外,回看视频推流一节中的 task_start(),在 while 循环开始发送 RTMPPacket 前,有通过 callback() 向队列中发送一个音频头:
cpp
// 获取并将音频头存入队列,实测不用放这个头也是可以成功将音频推流的
callback(audioChannel->getAudioSeqHeader());
当然实测时发现没有音频头拉流端也能正常播放音频。音频头具体设置如下:
cpp
RTMPPacket *AudioChannel::getAudioSeqHeader() {
u_char *ppBuffer;
// 这个 len 对于头而言固定为 2
u_long len;
faacEncGetDecoderSpecificInfo(audioEncoder, &ppBuffer, &len);
auto packet = new RTMPPacket;
int body_size = 2 + len;
RTMPPacket_Alloc(packet, body_size);
// 3.设置 RTMPPacket 内容
// 声道数
if (channels == 2) {
packet->m_body[0] = 0xAF;
} else {
packet->m_body[0] = 0xAE;
}
// 序列头为 0x00
packet->m_body[1] = 0x00;
// 音频数据
memcpy(&packet->m_body[2], ppBuffer, len);
// 4.封包,与视频封包类似
// 包类型为音频
packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;
packet->m_nBodySize = body_size;
packet->m_nChannel = 11;
// 音频帧的头一般都是没有时间搓的
packet->m_nTimeStamp = 0;
// 一般不用绝对时间戳
packet->m_hasAbsTimestamp = 0;
// 帧数据类型一般是大包,如果是头信息,可以给一个中包或小包
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
return packet;
}
4、释放与测试结果
释放工作主要是 Native 层的各种成员变量。
首先 native-lib 中要删除两个 Channel 并等待线程 pid_start 执行完毕:
cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_rtmp_pusher_LivePusher_nativeStop(JNIEnv *env, jobject thiz) {
isStart = false;
readyPushing = false;
packets.setEnable(false);
// 等待 pid_start 线程执行完再做后续的善后工作
pthread_join(pid_start, nullptr);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_rtmp_pusher_LivePusher_nativeRelease(JNIEnv *env, jobject thiz) {
DELETE(videoChannel)
DELETE(audioChannel)
}
VideoChannel 的析构函数要销毁同步锁、关闭编码器:
cpp
VideoChannel::~VideoChannel() {
pthread_mutex_destroy(&mutex);
releaseVideoCodec();
videoCallback = nullptr;
}
void VideoChannel::releaseVideoCodec() {
if (videoEncoder) {
// 释放通过 x264_picture_alloc 分配内存的图片的相关资源
x264_picture_clean(pic_in);
// 关闭编码器
x264_encoder_close(videoEncoder);
// 释放编码器结构体,通过该函数将结构体内部的字段置空
x264_encoder_parameters(videoEncoder, nullptr);
videoEncoder = nullptr;
}
}
AudioChannel 需要关闭音频编码器,回收 buffer:
cpp
AudioChannel::~AudioChannel() {
DELETE(buffer)
if (audioEncoder) {
faacEncClose(audioEncoder);
audioEncoder = nullptr;
}
audioCallback = nullptr;
}
最终的演示效果,手机端推流:
拉流端通过 ffplay 拉流:
shell
ffplay -i rtmp://xxx.xx.xx.xx/myapp
效果图: