上一篇 Android视音频开发初探【一】(clang编译FFmpeg+fdk-aac+x264+openssl)
下一篇 Android视音频开发初探【三】(简单的播放器)
demo地址https://github.com/ColorfulHorse/learnFFmpeg
前言
上一篇博客中我们已经成功编译出了 FFmpeg动态库,现在来使用它实现一个简单的推流器,以此逐渐熟悉一些C/C++知识,NDK以及CMake知识,java层代码为kotlin编写。
简单梳理一下相机推流流程
- 搭建流媒体服务器
- 采集相机数据
- 编码相机数据为H.264流
- 推流到流媒体服务器
搭建流媒体服务器
要推流首先需要一个流媒体服务器来收流,可以使用nginx-rtmp搭建,也可以使用srs
nginx-rtmp可以参照这篇博客 搭建RTMP服务器
srs由于是国人开发的,可以直接去看wiki srs wiki
如果你暂时不想去弄这些,也可以先使用我搭建好的,地址在demo里面有,不过不太稳定,可能容易连接失败
构建项目
配置项目
可以直接创建一个包含native的项目,也可以在现有项目通过添加配置引入,这里我们使用CMake来构建。
app gradle文件如下
android {
.....
ndk {
// app build时检查哪些平台的库
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
}
externalNativeBuild {
cmake {
arguments "-DANDROID_STL=c++_shared"
cppFlags "-std=c++11 -frtti -fexceptions"
// cmake 构建哪些平台的库
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
}
}
}
externalNativeBuild {
cmake {
// 此路径指定了native代码的根目录
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
......
}
项目目录如下
[图片上传失败...(image-ff4c3-1610000472160)]
然后将编译出的库文件和头文件分别放入libs和include目录,src目录则是我们存放.c/.cpp文件的地方
<figure>
<center>
<img src="//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/98cc5393b1664b44b57c78a9d9c21ba2~tplv-k3u1fbpfcp-zoom-1.image" />
<img src="//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/82567d7050564ea6a698822ce1d479f5~tplv-k3u1fbpfcp-zoom-1.image" />
</center>
</figure>
编写CMakeLists构建文件
关于cmake这里只需要知道一些简单的用法,这里无非就是做了下面几件事
- 通过file指令将src文件夹下面的所有.cpp文件抽取赋值给SRC_LIST变量,将它添加为动态库
- include_directories指定头文件路径
- 指定库文件路径,依次添加所有需要依赖的动态库,同时添加log库(这个库android系统内部自带)
- 链接动态库
CMake文件写完以后直接make project就可以生成动态库了,它将会输出到你指定的路径,要注意的是src目录下至少需要一个源文件
cmake_minimum_required(VERSION 3.4)
project(lyjplayer)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -frtti -fexceptions")
#获取上级目录
get_filename_component(PARENT_DIR "${PROJECT_SOURCE_DIR}" PATH)
get_filename_component(SRC_DIR "${PARENT_DIR}" PATH)
get_filename_component(APP_DIR "${SRC_DIR}" PATH)
# GLOB将所有匹配的文件生成一个list赋值给SRC_LIST
file(GLOB SRC_LIST "${PROJECT_SOURCE_DIR}/src/*.cpp")
# 输出.so库位置
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY
${APP_DIR}/output/${CMAKE_ANDROID_ARCH_ABI})
# 第三方库目录
set(LIBS_DIR ${APP_DIR}/libs/${CMAKE_ANDROID_ARCH_ABI})
# 头文件目录
include_directories(
${PROJECT_SOURCE_DIR}/include
)
# 生成动态库
add_library(lyjplayer SHARED ${SRC_LIST})
# 编解码(最重要的库)
add_library(avcodec SHARED IMPORTED)
# 设备信息
add_library(avdevice SHARED IMPORTED)
# 滤镜特效处理库
add_library(avfilter SHARED IMPORTED)
# 封装格式处理库
add_library(avformat SHARED IMPORTED)
add_library(avutil SHARED IMPORTED)
# 音频采样数据格式转换库
add_library(swresample SHARED IMPORTED)
# 视频像素数据格式转换
add_library(swscale SHARED IMPORTED)
# 后处理
add_library(postproc SHARED IMPORTED)
add_library(yuv SHARED IMPORTED)
find_library(log-lib log)
set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION ${LIBS_DIR}/libavcodec.so)
set_target_properties(avdevice PROPERTIES IMPORTED_LOCATION ${LIBS_DIR}/libavdevice.so)
set_target_properties(avfilter PROPERTIES IMPORTED_LOCATION ${LIBS_DIR}/libavfilter.so)
set_target_properties(avformat PROPERTIES IMPORTED_LOCATION ${LIBS_DIR}/libavformat.so)
set_target_properties(avutil PROPERTIES IMPORTED_LOCATION ${LIBS_DIR}/libavutil.so)
set_target_properties(swresample PROPERTIES IMPORTED_LOCATION ${LIBS_DIR}/libswresample.so)
set_target_properties(swscale PROPERTIES IMPORTED_LOCATION ${LIBS_DIR}/libswscale.so)
set_target_properties(postproc PROPERTIES IMPORTED_LOCATION ${LIBS_DIR}/libpostproc.so)
set_target_properties(yuv PROPERTIES IMPORTED_LOCATION ${LIBS_DIR}/libyuv.so)
# 指定生成版本号,VERSION指代动态库版本,SOVERSION指代API版本
set_target_properties(lyjplayer PROPERTIES VERSION 1.2 SOVERSION 1)
target_link_libraries(
lyjplayer#目标库
# 依赖库,可以写多个
log
android
avcodec
avdevice
avfilter
avformat
avutil
swresample
swscale
postproc
yuv
)
获取相机预览数据
支持格式
相机这部分我使用的是Camera2的API,不得不说比较难用,不太建议用。如果对Camera2没兴趣可以跳过这一节直接用Camera API实现,毕竟最终只需要拿到预览数据就行了。要注意的是由于要使用x264进行编码,相机的预览数据的格式最终要转换成为x264支持的格式,x264支持格式如下。
[图片上传失败...(image-d34c72-1610000472160)]
开启相机预览并捕获数据
- 启动一个handlerThread用来操作camera(官方建议),准备一个TextureView(使用surfaceView也可以)作为预览载体
private fun setup() {
// 对于camera
startBackgroundThread()
if (preview.isAvailable) {
mSurfaceTexture = preview.surfaceTexture
openBackCamera()
} else {
preview.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
mSurfaceTexture = surface
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
mSurfaceTexture = surface
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
mSurfaceTexture = null
return true
}
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
mSurfaceTexture = surface
openBackCamera()
}
}
}
}
- 打开相机,Camera2要通过getSystemService拿到CameraManager服务,然后遍历过滤拿到需要的摄像头。这里需要一个支持YUV_420_888的后置摄像头;ImageFormat.YUV_420_888是一个特殊的格式,后面我们组装yuv420的时候再讲。
private fun openBackCamera() {
val manager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
var cameraId = ""
// 遍历所有摄像头,找到支持YUV_420_888输出格式的后置摄像头
for (id in manager.cameraIdList) {
// 获取摄像头特征
val cameraInfo = manager.getCameraCharacteristics(id)
// 支持的硬件等级
val level = cameraInfo[CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL]
val previewFormat = ImageFormat.YUV_420_888
//if (level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL || level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3) {
// 是否后置
if (cameraInfo[CameraCharacteristics.LENS_FACING] == CameraCharacteristics.LENS_FACING_BACK) {
val map = cameraInfo.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
) ?: continue
if (!map.isOutputSupportedFor(previewFormat)) {
continue
}
cameraId = id
mCameraInfo = cameraInfo
break
}
//}
}
// 通过cameraId打开摄像头
if (cameraId.isNotBlank()) {
manager.openCamera(cameraId, object : CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
mCamera = camera
// 创建Session
createSession()
}
override fun onDisconnected(camera: CameraDevice) {
}
override fun onError(camera: CameraDevice, error: Int) {
camera.close()
}
}, backgroundHandler)
}
}
- 创建输出,camera2无论是预览还是拍照还是接收数据都需要传入surface作为输出对象,这边我们创建两个输出对象,一个用于预览画面(分辨率较高),一个用于接收数据(分辨率较低),预览画面用的surface通过textureView的surfaceTexture创建,接收数据的通过ImageReader创建,ImageReader是一个专门用来接收图像数据的类。
private fun createOutputs() {
mCameraInfo?.let { info ->
val map = info[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]
map?.let { config ->
// 获取预览分辨率,这里预览画面是全屏的,通过屏幕尺寸计算获取最接近的相机支持分辨率
val previewSize =
getOptimalSize(
config.getOutputSizes(SurfaceTexture::class.java),
previewWidth,
previewHeight
)
// 获取预览数据分辨率,获取最接近480x640的
val previewDataSize =
getOptimalSize(config.getOutputSizes(SurfaceTexture::class.java), 480, 640)
this.previewDataSize = previewDataSize;
// 创建接收预览数据的imageReader
val previewReader =
ImageReader.newInstance(
previewDataSize.width,
previewDataSize.height,
ImageFormat.YUV_420_888,
3
)
mPreviewReader = previewReader
// 设置imageReader回调,相机的帧画面数据将会会掉到imageListener中
previewReader.setOnImageAvailableListener(imageListener, backgroundHandler)
// 接收预览数据用的surface
previewDataSurface = previewReader.surface
mSurfaceTexture?.run {
// 设置surfaceTexture的实际尺寸
setDefaultBufferSize(previewSize.width, previewSize.height)
// 预览画面用的surface
val surface = Surface(this)
previewSurface = surface
}
}
}
}
- 创建CaptureSession,CaptureSession是实际用来操作相机的类,你可以把它当场camera的代理。
private fun createSession() {
mCamera?.let { camera ->
createOutputs()
// 输出对象
val outputs = listOf(previewSurface, previewDataSurface)
camera.createCaptureSession(
outputs,
object : CameraCaptureSession.StateCallback() {
override fun onConfigureFailed(session: CameraCaptureSession) {
}
override fun onConfigured(session: CameraCaptureSession) {
// 创建session成功,开始预览
mSession = session
startPreview()
}
override fun onClosed(session: CameraCaptureSession) {
super.onClosed(session)
}
},
backgroundHandler
)
}
}
- 开始预览;通过CameraCaptureSession发起预览,session对相机进任何行操作都以发送request的方式进行,比如拍照、连拍、预览,都使用createCaptureRequest创建,只是类型不同。
private fun startPreview() {
mCamera?.let { camera ->
mSession?.let { session ->
// 创建一个适用于预览的request
val builder = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
// 绑定预览画面和数据的surface
previewSurface?.let { builder.addTarget(it) }
previewDataSurface?.let { builder.addTarget(it) }
// 自动对焦
builder[CaptureRequest.CONTROL_AF_MODE] =
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
builder[CaptureRequest.CONTROL_AE_MODE] =
CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH
val request = builder.build()
mSession = session
// 请求预览
session.setRepeatingRequest(
request, object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureStarted(session: CameraCaptureSession, request: CaptureRequest, timestamp: Long, frameNumber: Long) {
// 预览成功
super.onCaptureStarted(session, request, timestamp, frameNumber)
}
override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
}
},
backgroundHandler
)
}
}
}
- 获取相机每一帧的YUV数据并组合成YUV420p。
由于设置的接收类型为YUV_420_888,我们需要做一些转换,YUV_420_888格式将Y U V三个分量分别存在三个通道的ByteBuffer里面,y通道buffer的大小为 plane.rowStride*height(rowStride为每一行的实际数据长度),u v 分量的buffer size是y的一半。
u v 的buffer中其实都存有完整的u分量和v分量,在u通道里下标0、2、4...存有u分量,1、3、5...存有v分量;而在v通道里0、2、4...存有v分量,1、3、5...存有u分量。我们这里将Y U
V分别取出来按顺序装到一个byte[]中组成yuvi420格式。
有关YUV_420_888更详细的解释可以看Android: Image类浅析(结合YUV_420_888) 。
另外这里还有一个内存对齐的问题,比如说摄像头内存对齐规定为16byte,输出分辨率宽度为500,那么rowStride因为内存对齐会变成512,就是16的倍数,但是实际上多出来的数据我们并不需要,所以需要处理一下,关于此格式内存对齐更详细的解释可以看这里Android的YUV_420_888图片转换Bitmap时的rowStride问题。
private val imageListener = { reader: ImageReader ->
val image = reader.acquireNextImage()
if (image != null) {
// y u v三通道
val yBuffer = image.planes[0].buffer
val uBuffer = image.planes[1].buffer
// 两像素之间u分量的间隔,yuv420中uv为1
val uStride = image.planes[1].pixelStride
val vBuffer = image.planes[2].buffer
val uvSize = image.width * image.height / 4
// yuvi420: Y:U:V = 4:1:1 = YYYYUV
val buffer = ByteArray(image.width * image.height * 3 / 2)
// 每一行的实际数据长度,可能因为内存对齐大于图像width
val rowStride = image.planes[0].rowStride
// 内存对齐导致每行末多出来的长度
val padding = rowStride - image.width
var pos = 0
// 将y buffer拼进去
if (padding == 0) {
pos = yBuffer.remaining()
yBuffer.get(buffer, 0, pos)
} else {
var yBufferPos = 0
for (row in 0 until image.height) {
yBuffer.position(yBufferPos)
yBuffer.get(buffer, pos, image.width)
// 忽略行末冗余数据,偏移到下一行的位置
yBufferPos += rowStride
pos += image.width
}
}
var i = 0
val uRemaining = uBuffer.remaining()
while (i < uRemaining) {
// 循环u v buffer,隔一个取一个
buffer[pos] = uBuffer[i]
buffer[pos+uvSize] = vBuffer[i]
pos++
i += uStride
if (padding == 0) continue
// 并跳过每一行冗余数据
val rowLen = i % rowStride
if (rowLen >= image.width) {
i += padding
}
}
// 调用native方法将byte[]丢到推流器队列,方法实现看后面native层部分
publisher.publishData(buffer)
image.close()
}
}
定义JNI接口
JNI简单介绍
JNI是java与native层交互的桥梁,通过在java代码中定义native方法,然后在c代码中定义相应的方法建立一个映射关系,达到互相调用的目的;建立映射关系的方法有静态注册和动态注册两种。由于Java层和Native层享有不同的内存空间,在编写native代码的时候要注意内存管理。
静态注册
静态注册比较简单,规定了native层方法必须写成特定的格式,方法名需要带上java类的包名
java层
package com.lyj.learnffmpeg
class LyjPlayer {
......
external fun initPlayer()
}
c层
extern "C"
JNIEXPORT void JNICALL
Java_com_lyj_learnffmpeg_LyjPlayer_initPlayer(JNIEnv *env, jobject thiz) {}
动态注册
动态注册比较灵活,方法名并没有格式规定,但是需要在写一些代码手动注册,本文就是用的动态注册。
java文件
class Publisher {
......
external fun startPublish(path: String, width: Int, height: Int, orientation: Int): Int
}
cpp文件, 这里只需要看JNI_OnLoad中具体是如何注册的
template<class T>
int arrayLen(T &array) {
return (sizeof(array) / sizeof(array[0]));
}
#ifdef __cplusplus
extern "C" {
#endif
const char *cls_publish = "com/lyj/learnffmpeg/Publisher";
Publisher *publisher = nullptr;
......
// 开始推流
int publisher_start_publish(JNIEnv *env, jobject thiz, jstring path, jint width, jint height,
jint orientation) {
const char *p_path = nullptr;
p_path = env->GetStringUTFChars(path, nullptr);
if (publisher) {
publisher->startPublish(p_path, width, height, orientation);
}
env->ReleaseStringUTFChars(path, p_path);
return 0;
}
// 方法映射,(Ljava/lang/String;)I代表入参为String类型,返回值为int
JNINativeMethod player_methods[] = {
{
......
{"startPlay", "(Ljava/lang/String;)I", (void *) player_start_play}
}
};
int jniRegisterNativeMethods(JNIEnv *env, const char *className, const JNINativeMethod *methods,
int count) {
int res = -1;
jclass cls = env->FindClass(className);
if (cls != nullptr) {
int ret = env->RegisterNatives(cls, methods, count);
if (ret > 0) {
res = 0;
}
}
env->DeleteLocalRef(cls);
return res;
}
// 在此方法中进行动态注册
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = nullptr;
jint result = -1;
if (vm->GetEnv((void **) (&env), JNI_VERSION_1_6) != JNI_OK) {
return result;
}
// 动态注册,绑定java方法
jniRegisterNativeMethods(env, cls_publish, publisher_methods, arrayLen(publisher_methods));
return JNI_VERSION_1_6;
}
JNIEXPORT void JNI_OnUnload(JavaVM *jvm, void *reserved) {
}
#ifdef __cplusplus
}
#endif
可以看到动态注册方法映射的时候需要知道java方法的参数和返回值的类型签名,这点比较麻烦。
静态注册和动态注册各有优劣,静态注册在AndroidStudio中有更好的支持,可以通过点击java/c方法前的图标直接跳转到映射方法定义处,非常方便;而动态注册能让你更自由地管理代码。
编写推流逻辑
简单流程
- 创建一个队列,从java层接收相机数据,放入阻塞队列
- 开启另一个线程,不断从队列取帧数据编码推给流媒体服务器
代码实现
Publisher.kt,定义java层类
class Publisher {
private val handler = Handler()
companion object {
// 状态码
const val STATE_CONNECTED = 0
const val STATE_START = STATE_CONNECTED + 1
const val STATE_STOP = STATE_START + 1
// 错误码
const val CONNECT_ERROR = 0
const val UNKNOW = CONNECT_ERROR + 1
init {
// 加载so库
LibLoader.loadLib("lyjplayer")
}
}
init {
// 初始化
initPublish()
}
// 初始化
external fun initPublish()
// 设置回调
external fun setCallBack(callback: PublishCallBack)
// 开始推流
external fun startPublish(path: String, width: Int, height: Int, orientation: Int): Int
external fun stopPublish(): Int
// 推相机帧数据
external fun publishData(data: ByteArray): Int
external fun release()
interface PublishCallBack {
fun onState(state: Int)
fun onError(code: Int)
}
fun setPublishListener(callback: PublishCallBack) {
setCallBack(object : PublishCallBack {
override fun onState(state: Int) {
handler.post { callback.onState(state) }
}
override fun onError(code: Int) {
handler.post { callback.onError(code) }
}
})
}
}
register_jni.cpp, 主要用作jni动态注册,绑定java方法和native方法
#include <publisher.h>
#include <logger.h>
template<class T>
int arrayLen(T &array) {
return (sizeof(array) / sizeof(array[0]));
}
#ifdef __cplusplus
extern "C" {
#endif
// java类包名
const char *cls_publish = "com/lyj/learnffmpeg/Publisher";
Publisher *publisher = nullptr;
void publisher_init(JNIEnv *env, jobject thiz) {
if (publisher == nullptr) {
publisher = new Publisher();
// 将jvm实例赋值进去,以便回调java方法
env->GetJavaVM(&publisher->vm);
}
}
// 设置回调
void publisher_set_callback(JNIEnv *env, jobject thiz, jobject callback) {
if (publisher) {
if (publisher->callback) {
env->DeleteGlobalRef(publisher->callback);
}
publisher->callback = env->NewGlobalRef(callback);
}
}
void publisher_release(JNIEnv *env, jobject thiz) {
if (publisher != nullptr) {
env->DeleteGlobalRef(publisher->callback);
publisher->release();
delete publisher;
publisher = nullptr;
}
}
// 初始化推流
int publisher_start_publish(JNIEnv *env, jobject thiz, jstring path, jint width, jint height, jint orientation) {
const char *p_path = nullptr;
p_path = env->GetStringUTFChars(path, nullptr);
if (publisher) {
publisher->startPublish(p_path, width, height, orientation);
}
env->ReleaseStringUTFChars(path, p_path);
return 0;
}
// 接收相机帧数据放入队列
int publisher_publish_data(JNIEnv *env, jobject thiz, jbyteArray data) {
if (publisher && publisher->isPublish()) {
int len = env->GetArrayLength(data);
jbyte *buffer = new jbyte[len];
// 这里复制一份数据给native层用,因为相机本身的数据需要回收
env->GetByteArrayRegion(data, 0, len, buffer);
if (publisher) {
publisher->pushData((unsigned char *) (buffer));
}
}
return 0;
}
int publisher_stop_publish(JNIEnv *env, jobject thiz) {
if (publisher) {
publisher->stopPublish();
}
return 0;
}
JNINativeMethod publisher_methods[] = {
{"initPublish", "()V", (void *) publisher_init},
{"setCallBack", "(Lcom/lyj/learnffmpeg/PublishCallBack;)V", (void *) publisher_set_callback},
{"release", "()V", (void *) publisher_release},
{"startPublish", "(Ljava/lang/String;III)I", (void *) publisher_start_publish},
{"stopPublish", "()I", (void *) publisher_stop_publish},
{"publishData", "([B)I", (void *) publisher_publish_data}
};
// jni注册
int jniRegisterNativeMethods(JNIEnv *env, const char *className, const JNINativeMethod *methods,
int count) {
int res = -1;
jclass cls = env->FindClass(className);
if (cls != nullptr) {
int ret = env->RegisterNatives(cls, methods, count);
if (ret > 0) {
res = 0;
}
}
env->DeleteLocalRef(cls);
return res;
}
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = nullptr;
jint result = -1;
if (vm->GetEnv((void **) (&env), JNI_VERSION_1_6) != JNI_OK) {
return result;
}
jniRegisterNativeMethods(env, cls_publish, publisher_methods, arrayLen(publisher_methods));
return JNI_VERSION_1_6;
}
JNIEXPORT void JNI_OnUnload(JavaVM *jvm, void *reserved) {
}
#ifdef __cplusplus
}
#endif
ub
publisher.h定义了一些函数和变量,注释都比较详细,LinkedBlockingQueue是自己定义的一个阻塞队列,这里就不贴代码了,可以去demo中看
// 由于是FFmpeg是纯c编写,所以需要在extern "C"{}中引入
#ifdef __cplusplus
extern "C" {
#endif
#include <libavformat/avformat.h>
#ifdef __cplusplus
}
#endif
using namespace std;
class Publisher {
private:
mutex pool_mutex;
const char *path;
int width = 0;
int height = 0;
atomic_bool is_publish = {false};
int fps = 25;
int64_t index = 0;
uint8_t *pic_buf = nullptr;
// AVFormatContext用于封装/解封装 flv,avi,rmvb,mp4
AVFormatContext *formatContext = nullptr;
// 用于编解码
AVCodecContext *codecContext = nullptr;
AVDictionary *codec_dict = nullptr;
AVStream *stream = nullptr;
// AVPacket是存储压缩编码数据相关信息的结构体
AVPacket *packet = nullptr;
// 解码后/压缩前的数据
AVFrame *frame = nullptr;
// 从队列取数据编码线程
thread worker;
// 初始化推流
int initPublish(JNIEnv *env);
int destroyPublish();
// 编码一帧
int encodeFrame(AVFrame *frame);
void callbackState(JNIEnv *env, PublishState state);
void callbackError(JNIEnv *env, PublishError error);
public:
JavaVM *vm = nullptr;
// 回调实例
jobject callback = nullptr;
// 阻塞队列
LinkedBlockingQueue<unsigned char *> dataPool;
// 线程控制
atomic_bool running = {false};
atomic_bool initing = {false};
Publisher();
int startPublish(const char *path, int width, int height, int orientation);
int stopPublish();
int pushData(unsigned char *buffer);
int release();
bool isPublish();
};
#endif
主要推流流程
publisher.cpp,主要逻辑实现都放在这里,基本函数调用可分为下面几步
- avformat_network_init() 初始化网络
- avformat_alloc_output_context2(&formatContext, nullptr, "flv", 推流地址) 根据文件名创建AVFormatContext,用于格式封装
- avcodec_find_encoder(AV_CODEC_ID_H264) 获取h.264编码器
- avcodec_alloc_context3(编码器) 根据编码器创建AVCodecContext,并给它设置一些参数,用于编码
- avcodec_open2(codecContext, codec, &codec_dict) 初始化编码器
- stream = avformat_new_stream(formatContext, nullptr) 创建一个视频流一个流并设置给formatContext
- avcodec_parameters_from_context(stream->codecpar, codecContext) 将编码器配置复制到流
- avio_open(&formatContext->pb, 推流路径, AVIO_FLAG_WRITE) 打开输入流,准备推流
- avformat_write_header(formatContext, nullptr) 写视频文件头
- frame = av_frame_alloc() 创建帧数据
- avcodec_send_frame(codecContext, frame) 将帧数据送入编码器
- packet = av_packet_alloc(),av_new_packet(packet, pic_size) 创建packet用于接收编码后的帧数据
- avcodec_receive_packet(codecContext, packet) 接收编码后的帧数据,然后设置packet的pts,dts
- av_interleaved_write_frame(formatContext, packet) 写入packet到码流
- av_write_trailer(formatContext) 写文件尾
准备推流
int Publisher::startPublish(const char *path, int width, int height, int orientation) {
if (initing.load() || running.load()) {
LOGE("已经在推流");
return -1;
}
this->path = path;
// 如果摄像头旋转角度为90/270度则宽高对换
this->width = orientation % 180 == 0 ? width:height;
this->height = orientation % 180 == 0 ? height:width;
LOGE("publish width:%d, height:%d", this->width, this->height);
this->orientation = orientation;
running = true;
initing = true;
if (worker.joinable()) {
worker.join();
}
// 开启编码线程,循环队列编码
worker = thread([=]() {
encodeRun();
});
return 0;
}
初始化推流
Publisher::initPublish(JNIEnv *env) {
int ret = avformat_network_init();
AVCodec *codec = nullptr;
// 根据输出封装格式创建AVFormatContext
avformat_alloc_output_context2(&formatContext, nullptr, "flv", path);
// 获取编码器
codec = avcodec_find_encoder(AV_CODEC_ID_H264);
codecContext = avcodec_alloc_context3(codec);
codecContext->codec_id = codec->id;
codecContext->codec_type = AVMEDIA_TYPE_VIDEO;
codecContext->pix_fmt = AV_PIX_FMT_YUV420P;
codecContext->width = width;
codecContext->height = height;
// 码率
codecContext->bit_rate = 144 * 1024;
// i帧间隔
codecContext->gop_size = 20;
// 量化
codecContext->qmin = 10;
codecContext->qmax = 51;
// 两个非B帧之间的最大B帧数
codecContext->max_b_frames = 3;
// 时间基 1/25 秒
codecContext->time_base = AVRational{1, fps};
codecContext->framerate = AVRational{fps, 1};
// codecContext->thread_count = 4;
if (formatContext->oformat->flags & AVFMT_GLOBALHEADER) {
codecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
if (codecContext->codec_id == AV_CODEC_ID_H264) {
// 编码速度和质量的平衡
// "ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow", "placebo"
av_dict_set(&codec_dict, "preset", "veryfast", 0);
av_dict_set(&codec_dict, "tune", "zerolatency", 0);
}
// 打印封装格式信息
av_dump_format(formatContext, 0, path, 1);
// 初始化编码器
if (avcodec_open2(codecContext, codec, &codec_dict) < 0) {
return -1;
}
// new一个流并挂到fmt_ctx名下,调用avformat_free_context时会释放该流
stream = avformat_new_stream(formatContext, nullptr);
if (stream == nullptr) {
return -1;
}
// 获取源图像字节大小,1byte内存对齐 yuv420P YYYYUV
int pic_size = av_image_get_buffer_size(codecContext->pix_fmt,
codecContext->width,
codecContext->height, 1);
// 创建缓冲区
pic_buf = (uint8_t *) (av_malloc(static_cast<size_t>(pic_size)));
// 创建编码数据
frame = av_frame_alloc();
frame->format = codecContext->pix_fmt;
frame->width = codecContext->width;
frame->height = codecContext->height;
// 格式化缓冲区内存
// dst_data: 格式化通道如rgb三通道
av_image_fill_arrays(frame->data, frame->linesize, pic_buf,
codecContext->pix_fmt,
codecContext->width, codecContext->height, 1);
// 创建packet存储编码后的数据
packet = av_packet_alloc();
av_new_packet(packet, pic_size);
// 复制编码配置到码流配置
avcodec_parameters_from_context(stream->codecpar, codecContext);
// 打开输出流
ret = avio_open(&formatContext->pb, path, AVIO_FLAG_WRITE);
if (ret < 0) {
char *err = av_err2str(ret);
LOGE("打开输出流失败, err:%s", err);
// 回调错误到java层
callbackError(env, PublishError::CONNECT_ERROR);
return -1;
}
// 回调状态到java层
callbackState(env, PublishState::CONNECTED);
LOGE("打开输出流成功");
// 写视频文件头
ret = avformat_write_header(formatContext, nullptr);
if (ret == 0) {
callbackState(env, PublishState::START);
} else {
callbackError(env, PublishError::UNKNOW);
return ret;
}
is_publish = true;
return ret;
}
编码线程
void Publisher::encodeRun() {
JNIEnv *env = nullptr;
// 绑定当前线程的jni实例,用于回调
int ret = vm->AttachCurrentThread(&env, nullptr);
ret = initPublish(env);
initing = false;
if (ret == 0) {
while (running.load()) {
unsigned char *buffer = nullptr;
buffer = dataPool.pop();
if (buffer) {
int32_t ysize = width * height;
int32_t usize = (width / 2) * (height / 2);
const uint8_t *sy = buffer;
const uint8_t *su = buffer + ysize;
const uint8_t *sv = buffer + ysize + usize;
uint8_t *ty = pic_buf;
uint8_t *tu = pic_buf + ysize;
uint8_t *tv = pic_buf + ysize + usize;
// 旋转,如果摄像头画面不是正确朝上的,要根据旋转角度将画面数据旋转
libyuv::I420Rotate(sy, height, su, height >> 1, sv, height >> 1,
ty, width, tu, width >> 1, tv, width>> 1,
height, width, (libyuv::RotationMode) orientation);
frame->data[0] = ty;
frame->data[1] = tu;
frame->data[2] = tv;
chrono::system_clock::time_point start = chrono::system_clock::now();
// 编码一帧
encodeFrame(frame);
chrono::system_clock::time_point finish = chrono::system_clock::now();
LOGE("encode time: %lf",
chrono::duration_cast<chrono::duration<double, ratio<1, 1000>>>(
finish - start).count());
delete[] buffer;
}
}
}
vm->DetachCurrentThread();
destroyPublish();
}
编码每一帧数据然后放入线程池发送
这里要了解一下FFmpeg编解码中时间戳的概念,它定义了pts、dts用来标识视频帧,pts表示解码后的视频帧什么时候被显示出来,dts则表示packet在什么时候开始送入解码器中进行解码。
视频编码中由于B帧的存在,pts和dts并不一定一致,B帧可能需要依赖后一帧的数据来补全自身,此时它的dts < pts,即需要后解码先显示。
详细可以看看这篇文章深入理解pts,dts,time_base
int Publisher::encodeFrame(AVFrame *frame) {
if (frame) {
// frame pts 为帧当前帧数
frame->pts = index;
}
// 发送一帧到编码器
int ret = avcodec_send_frame(codecContext, frame);
if (ret == AVERROR(EAGAIN)) {
ret = 0;
} else if (ret != 0) {
LOGE("encode error code: %d, msg:%s", ret, av_err2str(ret));
return -1;
}
while (ret >= 0) {
// 读编码完成的数据 某些解码器可能会消耗部分数据包而不返回任何输出,因此需要在循环中调用此函数,直到它返回EAGAIN
ret = avcodec_receive_packet(codecContext, packet);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
// 读完一帧
index++;
av_packet_unref(packet);
return 0;
} else if (ret < 0) {
LOGE("ENCODE ERROR CODE %d", ret);
av_packet_unref(packet);
return -1;
}
packet->stream_index = stream->index;
if (frame) {
// flv mp4一般时间基为 1/1000
// 将codec的pts转换为mux层的pts
int64_t pts = av_rescale_q_rnd(packet->pts, codecContext->time_base, stream->time_base, AV_ROUND_NEAR_INF);
int64_t dts = av_rescale_q_rnd(packet->dts, codecContext->time_base, stream->time_base, AV_ROUND_NEAR_INF);
packet->pts = pts;
packet->dts = dts;
// 表示当前帧的持续时间, 25帧 1000/25 = 40
packet->duration = (stream->time_base.den) / ((stream->time_base.num) * fps);
packet->pos = -1;
}
int64_t frame_index = index;
AVRational time_base = formatContext->streams[0]->time_base; //{ 1, 1000 };
LOGI("Send frame index:%lld,pts:%lld,dts:%lld,duration:%lld,time_base:%d,%d,size:%d",
(int64_t) frame_index,
(int64_t) packet->pts,
(int64_t) packet->dts,
(int64_t) packet->duration,
time_base.num, time_base.den,
packet->size);
long start = clock();
LOGE("start write a frame");
// 将解码完的数据包写入输出
int code = av_interleaved_write_frame(formatContext, packet);
long end = clock();
LOGE("send time: %ld", end - start);
if (code != 0) {
LOGE("av_interleaved_write_frame failed");
}
av_packet_unref(packet);
}
return 0;
}
结束时写文件尾,回收资源
int Publisher::destroyPublish() {
LOGE("destroyPublish");
index = 0;
if (is_publish.load()) {
int ret = encodeFrame(nullptr);
if (ret == 0) {
// 写文件尾
av_write_trailer(formatContext);
}
}
is_publish = false;
running = false;
if (pic_buf) {
av_free(pic_buf);
pic_buf = nullptr;
}
if (packet) {
av_packet_free(&packet);
packet = nullptr;
}
if (frame) {
av_frame_free(&frame);
frame = nullptr;
}
if (codec_dict) {
av_dict_free(&codec_dict);
}
if (formatContext) {
avio_close(formatContext->pb);
avformat_free_context(formatContext);
formatContext = nullptr;
}
return 0;
}
自此整个流程已经走完,整体代码并不完整,有一些细节上的东西限于篇幅没有贴出来,可以到demo里面去看。
使用ffplay看看推流效果
推流成功以后可以使用ffplay验证效果,到官网下载编译好的windows平台可执行文件
cmd到bin目录下面执行ffplay xxxx(流媒体地址)即可
下一篇来做一个简单的播放器