腾讯云自助建站,客户都不愿意做网站,怎么创建网站,sae 网站备案信息在之前完成的实战项目【FFmpeg音视频播放器】属于拉流范畴#xff0c;接下来将完成推流工作#xff0c;通过RTMP实现推流#xff0c;即直播客户端。简单的说#xff0c;就是将手机采集的音频数据和视频数据#xff0c;推到服务器端。 接下来的RTMP直播客户端系列#xff…在之前完成的实战项目【FFmpeg音视频播放器】属于拉流范畴接下来将完成推流工作通过RTMP实现推流即直播客户端。简单的说就是将手机采集的音频数据和视频数据推到服务器端。 接下来的RTMP直播客户端系列主要实现红框和紫色部分 本节主要内容
1.Java层视频编码工作。
2.Native层视频编码器工作。
3.Native层视频推流编码工作。
源码
NdkPush: 通过RTMP实现推流直播客户端。
一、Java层视频编码
1MainActivity
MainActivity只与中转站NdkPusher打交道用户操作页面相关功能是调用NdkPusher分发下去
初始化NdkPusher.java
mNdkPusher new NdkPusher(this, Camera.CameraInfo.CAMERA_FACING_BACK, 640, 480, 25, 800000);
首次点击【切换摄像头】时设置Camera与Surface绑定
/*** 切换摄像头** param view*/
public void switchCamera(View view) {if (initPermission()) {if (!isBind) {mNdkPusher.setPreviewDisplay(mSurfaceHolder);isBind true;}mNdkPusher.switchCamera();}
}点击【开始直播】时开始直播并设置rtmp服务器地址
/*** 开始直播** param view*/
public void startLive(View view) {mNdkPusher.startLive(rtmp://139.224.136.101/myapp);
}
点击【停止直播】时停止直播
/*** 停止直播** param view*/
public void stopLive(View view) {mNdkPusher.stopLive();
}
页面关闭释放资源
/*** 释放工作*/
Override
protected void onDestroy() {super.onDestroy();mNdkPusher.release();
}
2NdkPusher
中转站分发MainActivity事件和和Native层打交道
NdkPusher初始化时主要是的三件事
①:初始化native层需要的加载 ②:实例化视频通道并传递基本参数(宽高,fps,码率等) ③:实例化音频通道下一节内容
public NdkPusher(Activity activity, int cameraId, int width, int height, int fps, int bitrate) {native_init();// 将this传递给VideoChannel方便VideoChannel操控native层mVideoChannel new VideoChannel(this, activity, cameraId, width, height, fps, bitrate);
}
分发给视频通道VideoChannel--SurfaceView与中转站里面的Camera绑定
public void setPreviewDisplay(SurfaceHolder surfaceHolder) {mVideoChannel.setPreviewDisplay(surfaceHolder);
}
分发给视频通道VideoChannel--切换摄像头
public void switchCamera() {mVideoChannel.switchCamera();
}
开始直播调用native层开始直播工作分发给视频通道VideoChannel开始直播
public void startLive(String path) {native_start(path);mVideoChannel.startLive();
}
停止直播调用native层停止直播工作分发给视频通道VideoChannel停止直播
public void stopLive() {mVideoChannel.stopLive();native_stop();
}
释放工作释放native层数据和视频通道VideoChannel
public void release() {mVideoChannel.release();native_release();
}
与native层通讯函数
// 音频 视频 公用的
private native void native_init(); // 初始化
private native void native_start(String path); // 开始直播start(音频视频通用一套代码) path:rtmp推流地址
private native void native_stop(); // 停止直播
private native void native_release(); // onDestroy---release释放工作// 下面是视频独有
public native void native_initVideoEncoder(int width, int height, int mFps, int bitrate); // 初始化x264编码器
public native void native_pushVideo(byte[] data); // 相机画面的数据 byte[] 推给 native层
3VideoChannel
视频通道处理NdkPusher分发下来的事件和将CameraHelper的Camera画面数据推送到native层。
初始化CameraHelper设置Camera相机预览帮助类onPreviewFrame(nv21)数据的回调监听和宽高发送改变的监听
public VideoChannel(NdkPusher ndkPusher, Activity activity, int cameraId, int width, int height, int fps, int bitrate) {this.mNdkPusher ndkPusher; // 回调给中转站this.mFps fps; // fps 每秒钟多少帧this.bitrate bitrate; // 码率mCameraHelper new CameraHelper(activity, cameraId, width, height);mCameraHelper.setPreviewCallback(this); // 设置Camera相机预览帮助类onPreviewFrame(nv21)数据的回调监听mCameraHelper.setOnChangedSizeListener(this); // 宽高发送改变的监听回调设置
}
调用帮助类与Surface绑定
public void setPreviewDisplay(SurfaceHolder surfaceHolder) {mCameraHelper.setPreviewDisplay(surfaceHolder);
}
调用帮助类--切换摄像头
public void switchCamera() {mCameraHelper.switchCamera();
}
开始直播只修改标记 让其可以进入if 完成图像数据推送
public void startLive() {isLive true;
}
停止直播只修改标记 让其可以不要进入if 就不会再数据推送了
public void stopLive() {isLive false;
}
释放调用帮助类--停止预览
public void release() {mCameraHelper.stopPreview();
}
Camera预览画面的数据回调到这里再通过mNdkPusher将数据推送到native层
Override
public void onPreviewFrame(byte[] data, Camera camera) {// data nv21 数据if (isLive) {// 图像数据推送mNdkPusher.native_pushVideo(data);}
}
Camera发送宽高改变回调到这里再通过mNdkPusher将数据推送到native层
Override
public void onChanged(int width, int height) {// 视频编码器的初始化有关widthheightfpsbitratemNdkPusher.native_initVideoEncoder(width, height, mFps, bitrate); // 初始化x264编码器
}
4CameraHelper第一节已完成。
二、Native层视频编码器
1native-lib.cpp:
处理Java层NdkPusher调用的native函数
native层初始化工作
NdkPusher构造函数调用到这里初始化native层VideoChannel设置 Camera预览画面的数据推送到native层videoChannel编码后数据通过callback回调到native-lib.cpp加入队列。
extern C
JNIEXPORT void JNICALL
Java_com_ndk_push_NdkPusher_native_1init(JNIEnv *env, jobject thiz) {// 初始化 VideoChannelvideoChannel new VideoChannel();// 设置 Camera预览画面的数据推送到native层videoChannel编码后数据通过callback回调到native-lib.cpp加入队列videoChannel-setVideoCallback(callback);// 设置 队列的释放工作 回调packets.setReleaseCallback(releasePackets);
}
videoCallback 函数指针的实现将编码后数据存放packet到队列
void callback(RTMPPacket *packet) {if (packet) {if (packet-m_nTimeStamp -1) {packet-m_nTimeStamp RTMP_GetTime() - start_time; // 如果是spspps 没有时间搓如果是I帧就需要有时间搓}packets.push(packet); // 存入队列里面}
}
释放RTMPPacket * 包的函数指针实现T无法释放 让外界释放
void releasePackets(RTMPPacket **packet) {if (packet) {RTMPPacket_Free(*packet);delete packet;packet nullptr;}
} 初始化x264编码器Camera宽高改变回调到这里首次设置预览时触发分发到VideoChannel视频通道初始化编码器。
extern C
JNIEXPORT void JNICALL
Java_com_ndk_push_NdkPusher_native_1initVideoEncoder(JNIEnv *env, jobject thiz, jint width,jint height, jint fps, jint bitrate) {if (videoChannel) {videoChannel-initVideoEncoder(width, height, fps, bitrate);}
}
2VideoChannel.cpp native层视频通道初始化x264编码器和处理相机原始数据编码再回到给native-lib.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_len width * height;uv_len y_len / 4;// 防止重复初始化x264编码器if (videoEncoder) {x264_encoder_close(videoEncoder);videoEncoder nullptr;}// 防止重复初始化pic_inif (pic_in) {x264_picture_clean(pic_in);DELETE(pic_in);}// TODO 初始化x264编码器x264_param_t param;// x264的参数集// 设置编码器属性// ultrafast 最快 直播必须快// zerolatency 零延迟直播必须快x264_param_default_preset(param, ultrafast, zerolatency);// 编码规格https://wikipedia.tw.wjbk.site/wiki/H.264 看图片param.i_level_idc 32; // 3.2 中等偏上的规格 自动用 码率模糊程度分辨率// 输入数据格式是 YUV420P 平面模式VVVVVUUUU如果没有P 就是交错模式VUVUVUVUparam.i_csp X264_CSP_I420;param.i_width width;param.i_height height;// 不能有B帧如果有B帧会影响编码、解码效率快param.i_bframe 0;// 码率控制方式。CQP(恒定质量)CRF(恒定码率)ABR(平均码率)param.rc.i_rc_method X264_RC_CRF;// 设置码率param.rc.i_bitrate bitrate / 1000;// 瞬时最大码率 网络波动导致的param.rc.i_vbv_max_bitrate bitrate / 1000 * 1.2;// 设置了i_vbv_max_bitrate就必须设置buffer大小码率控制区大小单位Kb/sparam.rc.i_vbv_buffer_size bitrate / 1000;// 码率控制不是通过 timebase 和 timestamp码率的控制完全不用时间搓 而是通过 fps 来控制 码率根据你的fps来自动控制param.b_vfr_input 0;// 分子 分母// 帧率分子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;// 告诉人家到底是什么时候来一个I帧 计算关键帧的距离// 帧距离(关键帧) 2s一个关键帧 就是把两秒钟一个关键帧告诉人家param.i_keyint_max fps * 2;// sps序列参数 pps图像参数集所以需要设置header(sps pps)// 是否复制sps和pps放在每个关键帧的前面 该参数设置是让每个关键帧(I帧)都附带sps/pps。param.b_repeat_headers 1;// 并行编码线程数param.i_threads 1;// profile级别baseline级别 (把我们上面的参数进行提交)x264_param_apply_profile(param, baseline);// 输入图像初始化pic_in new x264_picture_t(); // 本身空间的初始化x264_picture_alloc(pic_in, param.i_csp, param.i_width, param.i_height); // pic_in内部成员初始化等// 打开编码器 一旦打开成功我们的编码器就拿到了videoEncoder x264_encoder_open(param);if (videoEncoder) {LOGE(x264编码器打开成功);}pthread_mutex_unlock(mutex);
}
三、Native层视频推流编码
1native-lib.cpp:
开始直播 --- 启动工作
创建子线程实现 1.连接流媒体服务器 2.发包
extern C
JNIEXPORT void JNICALL
Java_com_ndk_push_NdkPusher_native_1start(JNIEnv *env, jobject thiz, jstring path_) {/*** 创建子线程* 1.连接流媒体服务器* 2.发包*/if (isStart) {return;}isStart true;const char *path env-GetStringUTFChars(path_, nullptr);// 深拷贝char *url new char(strlen(path) 1); // C的堆区开辟 new -- deletestrcpy(url, path);// 创建线程来进行直播pthread_create(pid_start, nullptr, task_start, url);env-ReleaseStringUTFChars(path_, path); // 你随意释放我已经深拷贝了
}
连接RTMP服务器遍历压缩包队列将数据发送到RTMP服务器
void *task_start(void *args) {char *url static_castchar *(args);// RTMPDump API 九部曲RTMP *rtmp nullptr;int result; // 返回值判断成功失败do {// 1.1rtmp 初始化rtmp RTMP_Alloc();if (!rtmp) {LOGE(rtmp 初始化失败);break;}// 1.2rtmp 初始化RTMP_Init(rtmp);rtmp-Link.timeout 5; // 设置连接的超时时间以秒为单位的连接超时// 2rtmp 设置流媒体地址result RTMP_SetupURL(rtmp, url);if (!result) { // result 0 和 ffmpeg不同0代表失败LOGE(rtmp 设置流媒体地址失败);break;}// 3开启输出模式RTMP_EnableWrite(rtmp);// 4建立连接result RTMP_Connect(rtmp, nullptr);if (!result) { // result 0 和 ffmpeg不同0代表失败LOGE(rtmp 建立连接失败:%d, url: %s, result, url);break;}// 5连接流result RTMP_ConnectStream(rtmp, 0);if (!result) { // result 0 和 ffmpeg不同0代表失败LOGE(rtmp 连接流失败);break;}start_time RTMP_GetTime();// 准备好了可以开始向服务器推流了readyPushing true;// 队列开始工作packets.setWork(1);RTMPPacket *packet nullptr;// 从队列里面获取压缩包直接发给服务器while (readyPushing) {packets.pop(packet); // 阻塞式if (!readyPushing) {break;}// 取不到数据重新取可能还没生产出来if (!packet) {continue;}// 到这里就是成功的获取队列的ptk了可以发送给流媒体服务器packet-m_nInfoField2 rtmp-m_stream_id;// 给rtmp的流id// 成功取出数据包发送result RTMP_SendPacket(rtmp, packet, 1); // 1true 开启内部缓冲// packet 你都发给服务器了可以大胆释放releasePackets(packet);if (!result) { // result 0 和 ffmpeg不同0代表失败LOGE(rtmp 失败 自动断开服务器);break;}}releasePackets(packet); // 只要跳出循环就释放} while (false);// 本次一系列释放工作isStart false;readyPushing false;packets.setWork(0);packets.clear();if (rtmp) {RTMP_Close(rtmp);RTMP_Free(rtmp);}delete url;return nullptr;
}
Camera预览画面的数据回调到这里将原始数据进行x264编码后得到的RTMPPkt(压缩数据)加入队列里面
extern C
JNIEXPORT void JNICALL
Java_com_ndk_push_NdkPusher_native_1pushVideo(JNIEnv *env, jobject thiz, jbyteArray data_) {if (!videoChannel || !readyPushing) { return; }// 把jni --- C语言的jbyte *data env-GetByteArrayElements(data_, nullptr);// data nv21数据编码加入队列videoChannel-encodeData(data);env-ReleaseByteArrayElements(data_, data, 0); // 释放byte[]
}
2VideoChannel.cpp
视频原始数据编码工作
void VideoChannel::encodeData(signed char *data) {pthread_mutex_lock(mutex);// 把nv21的y分量 Copy i420的y分量memcpy(pic_in-img.plane[0], data, y_len);// 把nv21的vuvuvuvu 转化成 i420的 uuuuvvvvfor (int i 0; i uv_len; i) {// u 数据// data y_len i * 2 1 : 移动指针取 data(nv21) 中 u 的数据*(pic_in-img.plane[1] i) *(data y_len i * 2 1);// v 数据// data y_len i * 2 移动指针取 data(nv21) 中 v 的数据*(pic_in-img.plane[2] i) *(data y_len i * 2);}x264_nal_t *nal nullptr; // 通过H.264编码得到NAL数组理解int pi_nal; // pi_nal是nal中输出的NAL单元的数量x264_picture_t pic_out; // 输出编码后图片 编码后的图片// 1.视频编码器 2.nal 3.pi_nal是nal中输出的NAL单元的数量 4.输入原始的图片 5.输出编码后图片int ret x264_encoder_encode(videoEncoder, nal, pi_nal, pic_in,pic_out); // 进行编码本质的理解是编码一张图片if (ret 0) { // 返回值x264_encoder_encode函数 返回返回的 NAL 中的字节数。如果没有返回 NAL 单元则在错误时返回负数和零。LOGE(x264编码失败);pthread_mutex_unlock(mutex); // 注意一旦编码失败了一定要解锁否则有概率性造成死锁了return;}// 发送 Packets 入队queue// sps(序列参数集) pps(图像参数集) 说白了就是告诉我们如何解码图像数据int sps_len, pps_len; // sps 和 pps 的长度uint8_t sps[100]; // 用于接收 sps 的数组定义uint8_t pps[100]; // 用于接收 pps 的数组定义pic_in-i_pts 1; // pts显示的时间1 目的是每次都累加下去 dts编码的时间// 遍历nal中输出的NAL单元组件压缩包数据加入队列for (int i 0; i pi_nal; i) {if (nal[i].i_type NAL_SPS) {sps_len nal[i].i_payload - 4; // 去掉起始码之前我们学过的内容00 00 00 01memcpy(sps, nal[i].p_payload 4, sps_len); // 由于上面减了4所以4挪动这里的位置开始} else if (nal[i].i_type NAL_PPS) {pps_len nal[i].i_payload - 4; // 去掉起始码 之前我们学过的内容00 00 00 01memcpy(pps, nal[i].p_payload 4, pps_len); // 由于上面减了4所以4挪动这里的位置开始// sps pps 1个压缩包数据sendSpsPps(sps, pps, sps_len, pps_len); // pps是跟在sps后面的这里拿到的pps表示前面的sps肯定拿到了} else {// 发送 I帧 P帧sendFrame(nal[i].i_type, nal[i].i_payload, nal[i].p_payload);}}
}
组装sps pps 1个压缩包数据存入队列
void VideoChannel::sendSpsPps(uint8_t *sps, uint8_t *pps, int sps_len, int pps_len) {// 根据协议设置压缩包数据长度int body_size 5 8 sps_len 3 pps_len;RTMPPacket *packet new RTMPPacket; // 开始封包RTMPPacketRTMPPacket_Alloc(packet, body_size); // 堆区实例化 RTMPPacketint i 0;packet-m_body[i] 0x17; // 十六进制转换成二进制二进制查表 就懂了packet-m_body[i] 0x00; // 重点是此字节 如果是1 帧类型关键帧 非关键帧 如果是0一定是 sps ppspacket-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;packet-m_body[i] 0xE1;// 两个字节表达一个长度需要位移// 用两个字节来表达 sps的长度所以就需要位运算取出sps_len高8位 再取出sps_len低8位//位运算https://blog.csdn.net/qq_31622345/article/details/98070787// https://www.cnblogs.com/zhu520/p/8143688.htmlpacket-m_body[i] (sps_len 8) 0xFF; // 取高8位packet-m_body[i] sps_len 0xFF; // 去低8位memcpy(packet-m_body[i], sps, sps_len); // sps拷贝进去了i sps_len; // 拷贝完sps数据 i移位下面才能准确移位packet-m_body[i] 0x01; // pps个数用一个字节表示packet-m_body[i] (pps_len 8) 0xFF; // 取高8位packet-m_body[i] pps_len 0xFF; // 去低8位memcpy(packet-m_body[i], pps, pps_len); // pps拷贝进去了i pps_len; // 拷贝完pps数据 i移位下面才能准确移位// 封包处理packet-m_packetType RTMP_PACKET_TYPE_VIDEO; // 包类型 视频包packet-m_nBodySize body_size; // 设置好 spspps的总大小packet-m_nChannel 10; // 通道ID随便写一个注意不要写的和rtmp.c(里面的m_nChannel有冲突 4301行)packet-m_nTimeStamp 0; // sps pps 包 没有时间戳packet-m_hasAbsTimestamp 0; // 时间戳绝对或相对 也没有时间搓packet-m_headerType RTMP_PACKET_SIZE_MEDIUM; // 包的类型数据量比较少不像帧数据(那就很大了)所以设置中等大小的包// packet 存入队列videoCallback(packet);
}
发送帧信息把帧类型 RTMPPacket 存入队列
void VideoChannel::sendFrame(int type, int payload, uint8_t *pPayload) {// 去掉起始码 00 00 00 01 或者 00 00 01if (pPayload[2] 0x00){ // 00 00 00 01pPayload 4; // 例如共10个挪动4个后还剩6个// 保证 我们的长度是和上的数据对应也要是6个所以- 4payload - 4;}else if(pPayload[2] 0x01){ // 00 00 01pPayload 3; // 例如共10个挪动3个后还剩7个// 保证 我们的长度是和上的数据对应也要是7个所以- 3payload - 3;}// 根据协议设置压缩包数据长度int body_size 5 4 payload;RTMPPacket *packet new RTMPPacket; // 开始封包RTMPPacketRTMPPacket_Alloc(packet, body_size); // 堆区实例化 RTMPPacket// 区分关键帧 和 非关键帧packet-m_body[0] 0x27; // 普通帧 非关键帧if(type NAL_SLICE_IDR){packet-m_body[0] 0x17; // 关键帧}packet-m_body[1] 0x01; // 重点是此字节 如果是1 帧类型关键帧或非关键帧 如果是0一定是 sps ppspacket-m_body[2] 0x00;packet-m_body[3] 0x00;packet-m_body[4] 0x00;// 四个字节表达一个长度需要位移// 用四个字节来表达 payload帧数据的长度所以就需要位运算//位运算https://blog.csdn.net/qq_31622345/article/details/98070787// https://www.cnblogs.com/zhu520/p/8143688.htmlpacket-m_body[5] (payload 24) 0xFF;packet-m_body[6] (payload 16) 0xFF;packet-m_body[7] (payload 8) 0xFF;packet-m_body[8] payload 0xFF;memcpy(packet-m_body[9], pPayload, payload); // 拷贝H264的裸数据packet-m_packetType RTMP_PACKET_TYPE_VIDEO; // 包类型是视频类型packet-m_nBodySize body_size; // 设置好 关键帧 或 普通帧 的总大小packet-m_nChannel 10; // 通道ID随便写一个注意不要写的和rtmp.c(里面的m_nChannel有冲突 4301行)packet-m_nTimeStamp -1; // 帧数据有时间戳packet-m_hasAbsTimestamp 0; // 时间戳绝对或相对 用不到不需要packet-m_headerType RTMP_PACKET_SIZE_LARGE ; // 包的类型若是关键帧的话数据量比较大所以设置大包// 把最终的 帧类型 RTMPPacket 存入队列videoCallback(packet);
}
当压缩数据加入队列后开启直播创建的子线程将会获取队列的压缩数据发送到RTMP服务器。
源码
NdkPush: 通过RTMP实现推流直播客户端。
视频推流完成下一节开始音频推流工作。。。