前言
随着自媒体的短视频和直播带货的流行,再加上这几年疫情的肆虐,直接把实体店给干趴下了。叫苦连天的实体店老板们,原来是不懂这些东西,而被割了韭菜。当然这只是开个玩笑,哈哈。他们没必要懂技术。今天我就来剖析直播的前端和后端的实现流程,干货满满,不要忘了先点个小赞,谢谢了。那么就开始我们今天要讲的内容。
Android端
Android端的推流我们需要使用到NDK,而拉流播放就简单了,使用google官方的ExoPlayer播放器进行播放即可。ExoPlayer官方中文文档[developer.android.com/media/media…] 。
Java层
在Java层,我们做一些编码推流的流程控制。
代码实现
CameraHelper.kt
package site.doramusic.app.live
import android.app.Activity
import android.graphics.ImageFormat
import android.hardware.Camera
import android.hardware.Camera.CameraInfo
import android.hardware.Camera.PreviewCallback
import android.util.Log
import android.view.Surface
import android.view.SurfaceHolder
class CameraHelper(
private val activity: Activity,
private var cameraId: Int,
private var width: Int,
private var height: Int
) :
SurfaceHolder.Callback, PreviewCallback {
private var camera: Camera? = null
private var buffer: ByteArray? = null
private var surfaceHolder: SurfaceHolder? = null
private var previewCallback: PreviewCallback? = null
private var rotation = 0
private var onChangedSizeListener: OnChangedSizeListener? = null
var bytes: ByteArray? = null
fun switchCamera() {
cameraId = if (cameraId == CameraInfo.CAMERA_FACING_BACK) {
CameraInfo.CAMERA_FACING_FRONT
} else {
CameraInfo.CAMERA_FACING_BACK
}
stopPreview()
startPreview()
}
private fun stopPreview() {
// 预览数据回调接口
camera?.setPreviewCallback(null)
// 停止预览
camera?.stopPreview()
// 释放摄像头
camera?.release()
camera = null
}
private fun startPreview() {
try {
// 获得camera对象
camera = Camera.open(cameraId)
// 配置camera的属性
val parameters = camera!!.getParameters()
// 设置预览数据格式为nv21
parameters.previewFormat = ImageFormat.NV21
// 这是摄像头宽、高
setPreviewSize(parameters)
// 设置摄像头 图像传感器的角度、方向
setPreviewOrientation(parameters)
camera!!.setParameters(parameters)
buffer = ByteArray(width * height * 3 / 2)
bytes = ByteArray(buffer!!.size)
// 数据缓存区
camera!!.addCallbackBuffer(buffer)
camera!!.setPreviewCallbackWithBuffer(this)
// 设置预览画面
camera!!.setPreviewDisplay(surfaceHolder)
camera!!.startPreview()
} catch (ex: Exception) {
ex.printStackTrace()
}
}
private fun setPreviewOrientation(parameters: Camera.Parameters) {
val info = CameraInfo()
Camera.getCameraInfo(cameraId, info)
rotation = activity.windowManager.defaultDisplay.rotation
var degrees = 0
when (rotation) {
Surface.ROTATION_0 -> {
degrees = 0
onChangedSizeListener!!.onChanged(height, width)
}
Surface.ROTATION_90 -> {
degrees = 90
onChangedSizeListener!!.onChanged(width, height)
}
Surface.ROTATION_270 -> {
degrees = 270
onChangedSizeListener!!.onChanged(width, height)
}
}
var result: Int
if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360
result = (360 - result) % 360 // compensate the mirror
} else { // back-facing
result = (info.orientation - degrees + 360) % 360
}
// 设置角度
camera!!.setDisplayOrientation(result)
}
private fun setPreviewSize(parameters: Camera.Parameters) {
// 获取摄像头支持的宽、高
val supportedPreviewSizes =
parameters.supportedPreviewSizes
var size = supportedPreviewSizes[0]
Log.d(TAG, "支持 " + size.width + "x" + size.height)
// 选择一个与设置的差距最小的支持分辨率
// 10x10 20x20 30x30
// 12x12
var m = Math.abs(size.height * size.width - width * height)
supportedPreviewSizes.removeAt(0)
val iterator: Iterator<Camera.Size> =
supportedPreviewSizes.iterator()
// 遍历
while (iterator.hasNext()) {
val next = iterator.next()
Log.d(TAG, "支持 " + next.width + "x" + next.height)
val n = Math.abs(next.height * next.width - width * height)
if (n < m) {
m = n
size = next
}
}
width = size.width
height = size.height
parameters.setPreviewSize(width, height)
Log.d(
TAG,
"设置预览分辨率 width:" + size.width + " height:" + size.height
)
}
fun setPreviewDisplay(surfaceHolder: SurfaceHolder) {
this.surfaceHolder = surfaceHolder
this.surfaceHolder!!.addCallback(this)
}
fun setPreviewCallback(previewCallback: PreviewCallback) {
this.previewCallback = previewCallback
}
override fun surfaceCreated(holder: SurfaceHolder) {}
override fun surfaceChanged(
holder: SurfaceHolder,
format: Int,
width: Int,
height: Int
) {
// 释放摄像头
stopPreview()
// 开启摄像头
startPreview()
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
stopPreview()
}
override fun onPreviewFrame(
data: ByteArray,
camera: Camera
) {
when (rotation) {
Surface.ROTATION_0 -> rotation90(data)
Surface.ROTATION_90 -> {
}
Surface.ROTATION_270 -> {
}
}
// data数据依然是倒的
previewCallback!!.onPreviewFrame(bytes, camera)
camera.addCallbackBuffer(buffer)
}
private fun rotation90(data: ByteArray) {
var index = 0
val ySize = width * height
// u和v
val uvHeight = height / 2
// 后置摄像头顺时针旋转90度
if (cameraId == CameraInfo.CAMERA_FACING_BACK) { // 将y的数据旋转之后 放入新的byte数组
for (i in 0 until width) {
for (j in height - 1 downTo 0) {
bytes!![index++] = data[width * j + i]
}
}
// 每次处理两个数据
var i = 0
while (i < width) {
for (j in uvHeight - 1 downTo 0) { // v
bytes!![index++] = data[ySize + width * j + i]
// u
bytes!![index++] = data[ySize + width * j + i + 1]
}
i += 2
}
} else { // 逆时针旋转90度
for (i in 0 until width) {
var nPos = width - 1
for (j in 0 until height) {
bytes!![index++] = data[nPos - i]
nPos += width
}
}
// u v
var i = 0
while (i < width) {
var nPos = ySize + width - 1
for (j in 0 until uvHeight) {
bytes!![index++] = data[nPos - i - 1]
bytes!![index++] = data[nPos - i]
nPos += width
}
i += 2
}
}
}
fun setOnChangedSizeListener(listener: OnChangedSizeListener) {
onChangedSizeListener = listener
}
fun release() {
surfaceHolder!!.removeCallback(this)
stopPreview()
}
interface OnChangedSizeListener {
fun onChanged(w: Int, h: Int)
}
companion object {
private const val TAG = "CameraHelper"
}
}
这个类实现的是使用Android系统相机预览画面的功能,包括了切换前置后置摄像头以及旋转画面。
AudioChannel.kt
package site.doramusic.app.live
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class AudioChannel(private val livePusher: LivePusher) {
private val inputSamples: Int
private val executor: ExecutorService
private val audioRecord: AudioRecord
private val channels = 1
private var isLiving = false
fun startLive() {
isLiving = true
executor.submit(AudioTask())
}
fun stopLive() {
isLiving = false
}
fun release() {
audioRecord.release()
}
internal inner class AudioTask : Runnable {
override fun run() {
// 启动录音机
audioRecord.startRecording()
val bytes = ByteArray(inputSamples)
while (isLiving) {
val len = audioRecord.read(bytes, 0, bytes.size)
if (len > 0) {
// 送去编码
livePusher.native_pushAudio(bytes)
}
}
// 停止录音机
audioRecord.stop()
}
}
init {
executor = Executors.newSingleThreadExecutor()
// 准备录音机,采集pcm数据
val channelConfig: Int
channelConfig = if (channels == 2) {
AudioFormat.CHANNEL_IN_STEREO
} else {
AudioFormat.CHANNEL_IN_MONO
}
livePusher.native_setAudioEncodeInfo(44100, channels)
// 16位,2个字节
inputSamples = livePusher.inputSamples * 2
// 最小需要的缓冲区
val minBufferSize = AudioRecord.getMinBufferSize(
44100,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT
) * 2
// 1、麦克风 2、采样率 3、声道数 4、采样位
audioRecord = AudioRecord(
MediaRecorder.AudioSource.MIC,
44100,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
if (minBufferSize > inputSamples) minBufferSize else inputSamples
)
}
}
这个类中,我们实现了音频采样,即录音的功能。
VideoChannel.kt
package site.doramusic.app.live
import android.app.Activity
import android.hardware.Camera
import android.hardware.Camera.PreviewCallback
import android.view.SurfaceHolder
import site.doramusic.app.live.CameraHelper.OnChangedSizeListener
class VideoChannel(
private val livePusher: LivePusher,
activity: Activity,
width: Int,
height: Int,
private val bitrate: Int,
private val fps: Int,
cameraId: Int
) : PreviewCallback, OnChangedSizeListener {
private val cameraHelper: CameraHelper
private var isLiving = false
fun setPreviewDisplay(surfaceHolder: SurfaceHolder) {
cameraHelper.setPreviewDisplay(surfaceHolder)
}
init {
cameraHelper = CameraHelper(activity, cameraId, width, height)
cameraHelper.setPreviewCallback(this)
cameraHelper.setOnChangedSizeListener(this)
}
/**
* 得到nv21数据,已经旋转好的。
*
* @param data
* @param camera
*/
override fun onPreviewFrame(
data: ByteArray,
camera: Camera
) {
if (isLiving) {
livePusher.native_pushVideo(data)
}
}
fun switchCamera() {
cameraHelper.switchCamera()
}
/**
* 真实摄像头数据的宽、高。
*
* @param w
* @param h
*/
override fun onChanged(w: Int, h: Int) {
// 初始化编码器
livePusher.native_setVideoEncInfo(w, h, fps, bitrate)
}
fun startLive() {
isLiving = true
}
fun stopLive() {
isLiving = false
}
fun release() {
cameraHelper.release()
}
}
这个类中我们实现了视频编码的流程控制。
LivePusher.kt
package site.doramusic.app.live
import android.app.Activity
import android.view.SurfaceHolder
/**
* 直播推流。
*/
class LivePusher(
activity: Activity, width: Int, height: Int, bitrate: Int,
fps: Int, cameraId: Int) {
/**
* 音频编码通道。
*/
private val audioChannel: AudioChannel
/**
* 视频编码通道。
*/
private val videoChannel: VideoChannel
val inputSamples: Int external get
companion object {
init {
System.loadLibrary("doralive")
}
}
init {
native_init()
videoChannel = VideoChannel(this, activity, width, height, bitrate, fps, cameraId)
audioChannel = AudioChannel(this)
}
fun setPreviewDisplay(surfaceHolder: SurfaceHolder?) {
videoChannel.setPreviewDisplay(surfaceHolder)
}
/**
* 切换前后摄像头。
*/
fun switchCamera() {
videoChannel.switchCamera()
}
fun startLive(path: String?) {
native_start(path)
videoChannel.startLive()
audioChannel.startLive()
}
fun stopLive() {
videoChannel.stopLive()
audioChannel.stopLive()
native_stop()
}
fun release() {
videoChannel.release()
audioChannel.release()
native_release()
}
external fun native_init()
external fun native_start(path: String?)
external fun native_setVideoEncInfo(
width: Int,
height: Int,
fps: Int,
bitrate: Int
)
external fun native_setAudioEncodeInfo(sampleRateInHz: Int, channels: Int)
external fun native_pushVideo(data: ByteArray?)
external fun native_stop()
external fun native_release()
external fun native_pushAudio(data: ByteArray?)
}
最后,我们使用LivePusher这个类组合音频轨道和视频轨道数据的整合。
Native层
先看一下大致的目录结构,后文附有native层的完整代码。
NDK的目录结构
主要代码
AudioChannel.h
#ifndef PUSHER_AUDIO_CHANNEL_H
#define PUSHER_AUDIO_CHANNEL_H
#include "librtmp/rtmp.h"
#include "faac/faac.h"
#include <sys/types.h>
class AudioChannel {
typedef void (*AudioCallback)(RTMPPacket *packet);
public:
AudioChannel();
~AudioChannel();
void setAudioEncInfo(int samplesInHZ, int channels);
void setAudioCallback(AudioCallback audioCallback);
int getInputSamples();
void encodeData(int8_t *data);
RTMPPacket* getAudioTag();
private:
AudioCallback m_audioCallback;
int m_channels;
faacEncHandle m_audioCodec = 0;
u_long m_inputSamples;
u_long m_maxOutputBytes;
u_char *m_buffer = 0;
};
#endif //PUSHER_AUDIO_CHANNEL_H
AudioChannel.cpp
#include <cstring>
#include "AudioChannel.h"
#include "macro.h"
AudioChannel::AudioChannel() {
}
AudioChannel::~AudioChannel() {
DELETE(m_buffer);
// 释放编码器
if (m_audioCodec) {
faacEncClose(m_audioCodec);
m_audioCodec = 0;
}
}
void AudioChannel::setAudioCallback(AudioCallback audioCallback) {
this->m_audioCallback = audioCallback;
}
void AudioChannel::setAudioEncInfo(int samplesInHZ, int channels) {
// 打开编码器
m_channels = channels;
// 参数3:一次最大能输入编码器的样本数量,也编码的数据的个数 (一个样本是16位 2字节)
// 参数4:最大可能的输出数据,编码后的最大字节数
m_audioCodec = faacEncOpen(samplesInHZ, channels, &m_inputSamples, &m_maxOutputBytes);
// 设置编码器参数
faacEncConfigurationPtr config = faacEncGetCurrentConfiguration(m_audioCodec);
// 指定为 mpeg4 标准
config->mpegVersion = MPEG4;
// lc 标准
config->aacObjectType = LOW;
// 16位
config->inputFormat = FAAC_INPUT_16BIT;
// 编码出原始数据,既不是adts也不是adif
config->outputFormat = 0;
faacEncSetConfiguration(m_audioCodec, config);
// 输出缓冲区,编码后的数据用这个缓冲区来保存
m_buffer = new u_char[m_maxOutputBytes];
}
int AudioChannel::getInputSamples() {
return m_inputSamples;
}
RTMPPacket *AudioChannel::getAudioTag() {
u_char *buf;
u_long len;
faacEncGetDecoderSpecificInfo(m_audioCodec, &buf, &len);
int bodySize = 2 + len;
RTMPPacket *packet = new RTMPPacket;
RTMPPacket_Alloc(packet, bodySize);
// 双声道
packet->m_body[0] = 0xAF;
if (m_channels == 1) {
packet->m_body[0] = 0xAE;
}
packet->m_body[1] = 0x00;
// 图片数据
memcpy(&packet->m_body[2], buf, len);
packet->m_hasAbsTimestamp = 0;
packet->m_nBodySize = bodySize;
packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;
packet->m_nChannel = 0x11;
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
return packet;
}
void AudioChannel::encodeData(int8_t *data) {
// 返回编码后数据字节的长度
int bytelen = faacEncEncode(m_audioCodec, reinterpret_cast<int32_t *>(data), m_inputSamples, m_buffer,
m_maxOutputBytes);
if (bytelen > 0) {
int bodySize = 2 + bytelen;
RTMPPacket *packet = new RTMPPacket;
RTMPPacket_Alloc(packet, bodySize);
// 双声道
packet->m_body[0] = 0xAF;
if (m_channels == 1) {
packet->m_body[0] = 0xAE;
}
// 编码出的声音,都是0x01
packet->m_body[1] = 0x01;
// 图片数据
memcpy(&packet->m_body[2], m_buffer, bytelen);
packet->m_hasAbsTimestamp = 0;
packet->m_nBodySize = bodySize;
packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;
packet->m_nChannel = 0x11;
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
m_audioCallback(packet);
}
}
以上为音频编码的实现。无论是音频编码还是视频编码,最终都会使用到RTMPPacket这个结构体保存数据。音频编码我们采用的是faac的库。
VideoChannel.h
#ifndef PUSHER_VIDEO_CHANNEL_H
#define PUSHER_VIDEO_CHANNEL_H
#include <inttypes.h>
#include "x264/x264.h"
#include <pthread.h>
#include "librtmp/rtmp.h"
class VideoChannel {
typedef void (*VideoCallback)(RTMPPacket* packet);
public:
VideoChannel();
~VideoChannel();
// 创建x264编码器
void setVideoEncInfo(int width, int height, int fps, int bitrate);
void encodeData(int8_t *data);
void setVideoCallback(VideoCallback videoCallback);
private:
pthread_mutex_t m_mutex;
int m_width;
int m_height;
int m_fps;
int m_bitrate;
x264_t *m_videoCodec = 0;
x264_picture_t *m_picIn = 0;
int m_ySize;
int m_uvSize;
VideoCallback m_videoCallback;
void sendSpsPps(uint8_t *sps, uint8_t *pps, int sps_len, int pps_len);
void sendFrame(int type, uint8_t *payload, int i_payload);
};
#endif //PUSHER_VIDEO_CHANNEL_H
VideoChannel.cpp
#include "VideoChannel.h"
#include "librtmp/rtmp.h"
#include "macro.h"
#include "string.h"
VideoChannel::VideoChannel() {
pthread_mutex_init(&m_mutex, 0);
}
VideoChannel::~VideoChannel() {
pthread_mutex_destroy(&m_mutex);
if (m_videoCodec) {
x264_encoder_close(m_videoCodec);
m_videoCodec = 0;
}
if (m_picIn) {
x264_picture_clean(m_picIn);
DELETE(m_picIn);
}
}
void VideoChannel::setVideoEncInfo(int width, int height, int fps, int bitrate) {
pthread_mutex_lock(&m_mutex);
m_width = width;
m_height = height;
m_fps = fps;
m_bitrate = bitrate;
m_ySize = width * height;
m_uvSize = m_ySize / 4;
if (m_videoCodec) {
x264_encoder_close(m_videoCodec);
m_videoCodec = 0;
}
if (m_picIn) {
x264_picture_clean(m_picIn);
DELETE(m_picIn);
}
// 打开x264编码器
// x264编码器的属性
x264_param_t param;
// 参数2:最快
// 参数3:无延迟编码
x264_param_default_preset(¶m, "ultrafast", "zerolatency");
// base_line 3.2 编码规格
param.i_level_idc = 32;
// 输入数据格式
param.i_csp = X264_CSP_I420;
param.i_width = width;
param.i_height = height;
// 无b帧
param.i_bframe = 0;
// 参数i_rc_method表示码率控制,CQP(恒定质量),CRF(恒定码率),ABR(平均码率)
param.rc.i_rc_method = X264_RC_ABR;
// 码率(比特率,单位Kbps)
param.rc.i_bitrate = bitrate / 1000;
// 瞬时最大码率
param.rc.i_vbv_max_bitrate = bitrate / 1000 * 1.2;
// 设置了i_vbv_max_bitrate必须设置此参数,码率控制区大小,单位kbps
param.rc.i_vbv_buffer_size = bitrate / 1000;
// 帧率
param.i_fps_num = fps;
param.i_fps_den = 1;
param.i_timebase_den = param.i_fps_num;
param.i_timebase_num = param.i_fps_den;
// param.pf_log = x264_log_default2;
// 用fps而不是时间戳来计算帧间距离
param.b_vfr_input = 0;
// 帧距离(关键帧) 2s一个关键帧
param.i_keyint_max = fps * 2;
// 是否复制sps和pps放在每个关键帧的前面 该参数设置是让每个关键帧(I帧)都附带sps/pps。
param.b_repeat_headers = 1;
// 多线程
param.i_threads = 1;
x264_param_apply_profile(¶m, "baseline");
// 打开编码器
m_videoCodec = x264_encoder_open(¶m);
m_picIn = new x264_picture_t;
x264_picture_alloc(m_picIn, X264_CSP_I420, width, height);
pthread_mutex_unlock(&m_mutex);
}
void VideoChannel::setVideoCallback(VideoCallback videoCallback) {
this->m_videoCallback = videoCallback;
}
void VideoChannel::encodeData(int8_t *data) {
pthread_mutex_lock(&m_mutex);
// y数据
memcpy(m_picIn->img.plane[0], data, m_ySize);
for (int i = 0; i < m_uvSize; ++i) {
// u数据
*(m_picIn->img.plane[1] + i) = *(data + m_ySize + i * 2 + 1);
*(m_picIn->img.plane[2] + i) = *(data + m_ySize + i * 2);
}
// 编码出来的数据(帧数据)
x264_nal_t *pp_nal;
// 编码出来有几个数据(多少帧)
int pi_nal;
x264_picture_t pic_out;
x264_encoder_encode(m_videoCodec, &pp_nal, &pi_nal, m_picIn, &pic_out);
// 如果是关键帧 3
int sps_len;
int pps_len;
uint8_t sps[100];
uint8_t pps[100];
for (int i = 0; i < pi_nal; ++i) {
if (pp_nal[i].i_type == NAL_SPS) {
// 排除掉 h264的间隔 00 00 00 01
sps_len = pp_nal[i].i_payload - 4;
memcpy(sps, pp_nal[i].p_payload + 4, sps_len);
} else if (pp_nal[i].i_type == NAL_PPS) {
pps_len = pp_nal[i].i_payload - 4;
memcpy(pps, pp_nal[i].p_payload + 4, pps_len);
// pps肯定是跟着sps的
sendSpsPps(sps, pps, sps_len, pps_len);
} else {
sendFrame(pp_nal[i].i_type, pp_nal[i].p_payload, pp_nal[i].i_payload);
}
}
pthread_mutex_unlock(&m_mutex);
}
void VideoChannel::sendSpsPps(uint8_t *sps, uint8_t *pps, int sps_len, int pps_len) {
int bodySize = 13 + sps_len + 3 + pps_len;
RTMPPacket *packet = new RTMPPacket;
RTMPPacket_Alloc(packet, bodySize);
int i = 0;
// 固定头
packet->m_body[i++] = 0x17;
// 类型
packet->m_body[i++] = 0x00;
// composition time 0x000000
packet->m_body[i++] = 0x00;
packet->m_body[i++] = 0x00;
packet->m_body[i++] = 0x00;
// 版本
packet->m_body[i++] = 0x01;
// 编码规格
packet->m_body[i++] = sps[1];
packet->m_body[i++] = sps[2];
packet->m_body[i++] = sps[3];
packet->m_body[i++] = 0xFF;
// 整个sps
packet->m_body[i++] = 0xE1;
// sps长度
packet->m_body[i++] = (sps_len >> 8) & 0xff;
packet->m_body[i++] = sps_len & 0xff;
memcpy(&packet->m_body[i], sps, sps_len);
i += sps_len;
// pps
packet->m_body[i++] = 0x01;
packet->m_body[i++] = (pps_len >> 8) & 0xff;
packet->m_body[i++] = (pps_len) & 0xff;
memcpy(&packet->m_body[i], pps, pps_len);
// 视频
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
packet->m_nBodySize = bodySize;
// 随意分配一个管道(尽量避开rtmp.c中使用的)
packet->m_nChannel = 10;
// sps pps没有时间戳
packet->m_nTimeStamp = 0;
// 不使用绝对时间
packet->m_hasAbsTimestamp = 0;
packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM;
m_videoCallback(packet);
}
void VideoChannel::sendFrame(int type, uint8_t *payload, int i_payload) {
if (payload[2] == 0x00) {
i_payload -= 4;
payload += 4;
} else {
i_payload -= 3;
payload += 3;
}
int bodySize = 9 + i_payload;
RTMPPacket *packet = new RTMPPacket;
RTMPPacket_Alloc(packet, bodySize);
packet->m_body[0] = 0x27;
if(type == NAL_SLICE_IDR){
packet->m_body[0] = 0x17;
LOGE("关键帧");
}
// 类型
packet->m_body[1] = 0x01;
// 时间戳
packet->m_body[2] = 0x00;
packet->m_body[3] = 0x00;
packet->m_body[4] = 0x00;
// 数据长度 int 4个字节
packet->m_body[5] = (i_payload >> 24) & 0xff;
packet->m_body[6] = (i_payload >> 16) & 0xff;
packet->m_body[7] = (i_payload >> 8) & 0xff;
packet->m_body[8] = (i_payload) & 0xff;
// 图片数据
memcpy(&packet->m_body[9], payload, i_payload);
packet->m_hasAbsTimestamp = 0;
packet->m_nBodySize = bodySize;
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
packet->m_nChannel = 0x10;
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
m_videoCallback(packet);
}
以上为视频编码的实现。视频编码我们采用的是x264的库。这里牵扯到了IBP帧的概念,我来简单解释下。
“IPB帧”是视频编码中的一个概念,通常用于描述视频压缩中的帧类型。在IPB帧结构中,I代表“Intra-frame”,P代表“Predictive frame”,B代表“Bidirectional frame”。
- Intra-frame (I帧) :I帧是视频序列中的关键帧,也称为帧内编码帧。每个I帧都是独立编码的,不依赖于其他帧。它们包含了图像的完整信息,没有依赖其他帧的数据。I帧通常用于视频的起始点或者画面发生较大变化的地方。
- Predictive frame (P帧) :P帧是通过参考前面的帧进行编码的,也称为帧间预测帧。P帧存储了相对于前面的I帧或者P帧的差异信息,以便在解码时进行恢复。它们依赖于前面的帧来提供预测信息。
- Bidirectional frame (B帧) :B帧是通过参考前后的帧进行编码的,也称为双向预测帧。B帧存储了相对于前后帧的差异信息,可以最大程度地减小数据量。它们依赖于前后的帧来提供预测信息。
IPB帧结构的使用可以显著减小视频数据量,从而实现更高效的视频压缩和传输。
这里还有另外一个概念就是SPS和PPS。
SPS (Sequence Parameter Set) 和 PPS (Picture Parameter Set) 是 H.264/AVC(高级视频编码)标准中的两个重要概念,用于描述视频序列的参数和图像的参数。
-
SPS (Sequence Parameter Set) :
- SPS 包含了描述视频序列的参数信息,如图像尺寸、帧率、颜色空间等。
- SPS 是对整个视频序列共享的参数的描述,一个视频序列通常只有一个 SPS。
- SPS 信息在视频编码开始时发送,并在整个视频序列中保持不变,直到视频编码器发送新的 SPS。
-
PPS (Picture Parameter Set) :
- PPS 包含了描述图像的参数信息,如编码类型、量化参数等。
- PPS 是对图像级别的参数的描述,每个图像都有一个对应的 PPS。
- PPS 提供了一些特定于图像的参数,允许编码器在编码不同图像时进行灵活的设置。
这两个参数集合在 H.264/AVC 中的引入,使得视频编码器可以更加灵活地进行视频压缩和解码,并且可以提高编解码器的性能和效率。SPS 和 PPS 的发送通常会随着视频流的传输一起发送,以确保解码器能够正确地解析和解码视频数据。
这些都属于音视频编解码的范畴了。
doralive.cpp
#include <jni.h>
#include <string>
#include "librtmp/rtmp.h"
#include "safe_queue.h"
#include "macro.h"
#include "VideoChannel.h"
#include "AudioChannel.h"
SafeQueue<RTMPPacket *> packets;
VideoChannel *videoChannel = 0;
int isStart = 0;
pthread_t pid;
int readyPushing = 0;
uint32_t start_time;
AudioChannel *audioChannel = 0;
void releasePackets(RTMPPacket *&packet) {
if (packet) {
RTMPPacket_Free(packet);
delete packet;
packet = 0;
}
}
void callback(RTMPPacket *packet) {
if (packet) {
// 设置时间戳
packet->m_nTimeStamp = RTMP_GetTime() - start_time;
packets.push(packet);
}
}
extern "C"
JNIEXPORT void JNICALL
Java_site_doramusic_app_live_LivePusher_native_1init(JNIEnv *env, jobject instance) {
// 准备一个Video编码器的工具类,进行编码
videoChannel = new VideoChannel;
videoChannel->setVideoCallback(callback);
audioChannel = new AudioChannel;
audioChannel->setAudioCallback(callback);
// 准备一个队列,打包好的数据放入队列,在线程中统一的取出数据再发送给服务器
packets.setReleaseCallback(releasePackets);
}
extern "C"
JNIEXPORT void JNICALL
Java_site_doramusic_app_live_LivePusher_native_1setVideoEncInfo(JNIEnv *env, jobject instance,
jint width, jint height, jint fps,
jint bitrate) {
if (videoChannel) {
videoChannel->setVideoEncInfo(width, height, fps, bitrate);
}
}
void *start(void *args) {
char *url = static_cast<char *>(args);
RTMP *rtmp = 0;
do {
rtmp = RTMP_Alloc();
if (!rtmp) {
LOGE("alloc rtmp失败");
break;
}
RTMP_Init(rtmp);
int ret = RTMP_SetupURL(rtmp, url);
if (!ret) {
LOGE("设置地址失败:%s", url);
break;
}
// 5s超时时间
rtmp->Link.timeout = 5;
RTMP_EnableWrite(rtmp);
ret = RTMP_Connect(rtmp, 0);
if (!ret) {
LOGE("连接服务器:%s", url);
break;
}
ret = RTMP_ConnectStream(rtmp, 0);
if (!ret) {
LOGE("连接流:%s", url);
break;
}
// 记录一个开始时间
start_time = RTMP_GetTime();
// 表示可以开始推流了
readyPushing = 1;
packets.setWork(1);
// 保证第一个数据是aac解码数据包
callback(audioChannel->getAudioTag());
RTMPPacket *packet = 0;
while (readyPushing) {
packets.pop(packet);
if (!readyPushing) {
break;
}
if (!packet) {
continue;
}
packet->m_nInfoField2 = rtmp->m_stream_id;
// 发送rtmp包 1:队列
// 意外断网?发送失败,rtmpdump 内部会调用RTMP_Close
// RTMP_Close 又会调用 RTMP_SendPacket
// RTMP_SendPacket 又会调用 RTMP_Close
// 将rtmp.c 里面WriteN方法的 Rtmp_Close注释掉
ret = RTMP_SendPacket(rtmp, packet, 1);
releasePackets(packet);
if (!ret) {
LOGE("发送失败");
break;
}
}
releasePackets(packet);
} while (0);
isStart = 0;
readyPushing = 0;
packets.setWork(0);
packets.clear();
if (rtmp) {
RTMP_Close(rtmp);
RTMP_Free(rtmp);
}
delete (url);
return 0;
}
extern "C"
JNIEXPORT void JNICALL
Java_site_doramusic_app_live_LivePusher_native_1start(JNIEnv *env, jobject instance,
jstring path_) {
if (isStart) {
return;
}
isStart = 1;
const char *path = env->GetStringUTFChars(path_, 0);
char *url = new char[strlen(path) + 1];
strcpy(url, path);
pthread_create(&pid, 0, start, url);
env->ReleaseStringUTFChars(path_, path);
}
extern "C"
JNIEXPORT void JNICALL
Java_site_doramusic_app_live_LivePusher_native_1pushVideo(JNIEnv *env, jobject instance,
jbyteArray data_) {
if (!videoChannel || !readyPushing) {
return;
}
jbyte *data = env->GetByteArrayElements(data_, NULL);
videoChannel->encodeData(data);
env->ReleaseByteArrayElements(data_, data, 0);
}
extern "C"
JNIEXPORT void JNICALL
Java_site_doramusic_app_live_LivePusher_native_1stop(JNIEnv *env, jobject instance) {
readyPushing = 0;
// 关闭队列工作
packets.setWork(0);
pthread_join(pid, 0);
}
extern "C"
JNIEXPORT void JNICALL
Java_site_doramusic_app_live_LivePusher_native_1release(JNIEnv *env, jobject instance) {
DELETE(videoChannel);
DELETE(audioChannel);
}
extern "C"
JNIEXPORT void JNICALL
Java_site_doramusic_app_live_LivePusher_native_1setAudioEncodeInfo(JNIEnv *env, jobject instance,
jint sampleRateInHz,
jint channels) {
if (audioChannel) {
audioChannel->setAudioEncInfo(sampleRateInHz, channels);
}
}
extern "C"
JNIEXPORT jint JNICALL
Java_site_doramusic_app_live_LivePusher_getInputSamples(JNIEnv *env, jobject instance) {
if (audioChannel) {
return audioChannel->getInputSamples();
}
return -1;
}
extern "C"
JNIEXPORT void JNICALL
Java_site_doramusic_app_live_LivePusher_native_1pushAudio(JNIEnv *env, jobject instance,
jbyteArray data_) {
if (!audioChannel || !readyPushing) {
return;
}
jbyte *data = env->GetByteArrayElements(data_, NULL);
audioChannel->encodeData(data);
env->ReleaseByteArrayElements(data_, data, 0);
}
这里的_1
是对Java方法名称中_
的转义。最后使用可移植操作系统的POSIX线程pthread发送rtmp包推流到服务器就完成了。
CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
# 引入指定目录下的CMakeLists.txt
add_subdirectory(src/main/cpp/librtmp)
add_library(
doralive
SHARED
src/main/cpp/doralive.cpp
src/main/cpp/VideoChannel.cpp
src/main/cpp/AudioChannel.cpp)
include_directories(src/main/cpp/faac
src/main/cpp/x264)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/src/main/cpp/libs/${ANDROID_ABI}")
target_link_libraries(
doralive
rtmp
x264
faac
log)
总结
一套完整的直播推拉流的解决方案到此就结束了。云里雾里的同学,可以系统学习下Linux操作系统的使用。对于native层代码看得头晕的同学,建议先学习cpp、jni以及音视频编解码的相关内容。
如果你看到了这里,觉得文章写得不错就给个赞呗?
更多Android进阶指南 可以扫码 解锁更多Android进阶资料
敲代码不易,关注一下吧。ღ( ´・ᴗ・` )