NDK RTMP直播客户端二
在之前完成的实战项目【FFmpeg音视频播放器】属于拉流范畴,接下来将完成推流工作,通过RTMP实现推流,即直播客户端。简单的说,就是将手机采集的音频数据和视频数据,推到服务器端。

接下来的RTMP直播客户端系列,主要实现红框和紫色部分:

本节主要内容:
1.Java层视频编码工作。
2.Native层视频编码器工作。
3.Native层视频推流编码工作。
源码:
NdkPush: 通过RTMP实现推流,直播客户端。
一、Java层视频编码
1)MainActivity:
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();
}
2)NdkPusher:
中转站,分发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层
3)VideoChannel:
视频通道,处理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) {// 视频编码器的初始化有关:width,height,fps,bitratemNdkPusher.native_initVideoEncoder(width, height, mFps, bitrate); // 初始化x264编码器
}
4)CameraHelper第一节已完成。
二、Native层视频编码器
1)native-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; // 如果是sps+pps 没有时间搓,如果是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);}
}
2)VideoChannel.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(¶m, "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(¶m, "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(¶m);if (videoEncoder) {LOGE("x264编码器打开成功");}pthread_mutex_unlock(&mutex);
}
三、Native层视频推流编码
1)native-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_cast<char *>(args);// RTMPDump API 九部曲RTMP *rtmp = nullptr;int result; // 返回值判断成功失败do {// 1.1,rtmp 初始化rtmp = RTMP_Alloc();if (!rtmp) {LOGE("rtmp 初始化失败");break;}// 1.2,rtmp 初始化RTMP_Init(rtmp);rtmp->Link.timeout = 5; // 设置连接的超时时间(以秒为单位的连接超时)// 2,rtmp 设置流媒体地址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); // 1==true 开启内部缓冲// 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[]
}
2)VideoChannel.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 01)memcpy(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 01)memcpy(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; // 设置好 sps+pps的总大小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实现推流,直播客户端。
视频推流完成,下一节开始音频推流工作。。。
相关文章:
NDK RTMP直播客户端二
在之前完成的实战项目【FFmpeg音视频播放器】属于拉流范畴,接下来将完成推流工作,通过RTMP实现推流,即直播客户端。简单的说,就是将手机采集的音频数据和视频数据,推到服务器端。 接下来的RTMP直播客户端系列ÿ…...
Python3--垃圾回收机制
一、概述 Python 内部采用 引用计数法,为每个对象维护引用次数,并据此回收不在需要的垃圾对象。由于引用计数法存在重大缺陷,循环引用时由内存泄露风险,因此Python还采用 标记清除法 来回收在循环引用的垃圾对象。此外,…...
C/C++开发,认识opencv各模块
目录 一、opencv模块总述 二、opencv主要模块 2.1 opencv安装路径及内容 2.2 opencv模块头文件说明 2.3 成熟OpenCV主要模块 2.4 社区支持的opencv_contrib扩展主要模块 2.5 关于库文件的引用 一、opencv模块总述 opencv的主要能力在于图像处理,尤其是针对二维图…...
【WLSM、FDM状态估计】电力系统状态估计研究(Matlab代码实现)
💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…...
准备2023(2024)蓝桥杯
前缀和 一维前缀和 s[i]s[i-1]a[i]二维前缀和(子矩阵的和) s[i][j]s[i-1][j]s[i][j-1]-s[i-1][j-1]a[i][j] 差分 一维数组 //b是差分数组b[i]c;b[j1]-c;例题 #include<iostream> using namespace std; int n,m; int b[100002],a[100002]; vo…...
剑指 Offer 60. n个骰子的点数
剑指 Offer 60. n个骰子的点数 难度:middle\color{orange}{middle}middle 题目描述 把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。 你需要用一个浮点数数组返回答案,其中第 i 个…...
阿里巴巴-淘宝搜索排序算法学习
模型效能:模型结构优化 模型效能:减枝 FLOPS:每秒浮点运算的次数 模型效能:量化 基于统计阈值限定,基于学习阈值限定。 平台效能:一站式DL训练平台 平台效能:搜索模型的系统流程 协同关系…...
〖Python网络爬虫实战⑮〗- pyquery的使用
订阅:新手可以订阅我的其他专栏。免费阶段订阅量1000python项目实战 Python编程基础教程系列(零基础小白搬砖逆袭) 说明:本专栏持续更新中,目前专栏免费订阅,在转为付费专栏前订阅本专栏的,可以免费订阅付费…...
SQL综合查询下
SQL综合查询下 目录SQL综合查询下18、查询所有人都选修了的课程号与课程名题目代码题解19、SQL查询:查询没有参加选课的学生。题目代码20、SQL查询:统计各门课程选修人数,要求输出课程代号,课程名,有成绩人数ÿ…...
全连接层FC
lenet结构: 输入层(Input Layer):接收手写数字的图像数据,通常是28x28的灰度图像。 卷积层1(Convolutional Layer 1):对输入图像进行卷积操作,提取低级别的特征,使用 6 个大小为 5x5 的卷积核进行卷积,得到 6 个输出特征图,激活函数为 Sigmoid。 平均池化层1(Aver…...
图的遍历及连通性
文章目录 图的遍历及连通性程序设计程序分析图的遍历及连通性 【问题描述】 根据输入的图的邻接矩阵A,判断此图的连通分量的个数。 【输入形式】 第一行为图的结点个数n,之后的n行为邻接矩阵的内容,每行n个数表示。其中A[i][j]=1表示两个结点邻接,而A[i][j]=0表示两个结点无…...
DJ3-4 实时调度
目录 3.4.1 实现实时调度的基本条件 1. 提供必要的信息 2. 系统的处理能力强 3. 采用抢占式调度机制 4. 具有快速切换机制 3.4.2 实时调度算法的分类 1. 非抢占式调度算法 2. 抢占式调度算法 3.4.3 常用的几种实时调度算法 1. 最早截止时间优先 EDF(Ea…...
Oracle之PL/SQL游标练习题(三)
游标练习题目1、定义游标:列出每个员工的姓名部门名称并编程显示第10个到第20个记录2、定义游标:从雇员表中显示工资大于3000的记录,只要姓名、部门编号和工资,编程显示其中的奇数记录3、用游标显示所有部门编号与名称,…...
docker运行服务端性能监控系统Prometheus和数据分析系统Grafana
文章目录一、Prometheus的安装和运行1、使用docker拉取镜像2、创建prometheus.yml文件3、启动容器4、查看启动是否成功5、记录安装过程中出现的错误二、Grafana的安装和运行1、使用docker拉取镜像2、创建grafana3、运行grafana4、查看grafana运行日志5、登录grafana一、Prometh…...
【Linux】【应用层】多线程编程
一、线程创建 Linux 中的 pthread_create() 函数用来创建线程,它声明在<pthread.h>头文件中,语法格式如下: int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine) (void *),void *arg);各个参数…...
GameFramework 框架详解之 如何接入热更框架HybridCLR
一.前言 HybridCLR是一个特性完整、零成本、高性能、低内存的近乎完美的c#热更新方案 GameFramework是一个非常出色完整的基于Unity引擎的游戏框架,里面包含了非常多的模块,封装非常完整。 以前市面上的热更大多数都是Lua为主,后来出了一个ILRuntime的C#热更框架,虽然性能…...
全国青少年软件编程(Scratch)等级考试二级考试真题2023年3月——持续更新.....
一、单选题(共25题,共50分) 1. 小猫的程序如图所示,积木块的颜色与球的颜色一致。点击绿旗执行程序后,下列说法正确的是?( ) A.小猫一直在左右移动,嘴里一直说着“抓到了”。 B.小猫会碰到球,然后停止。 C.小猫一直在左右移动,嘴里一直说着“别跑” D.小猫会碰到球,…...
HTML2.1列表标签
列表标签种类 无序列表 有序列表 自定义列表 使用场景:在列表中按照行展示关联性内容。 特点:按照行的形式,整齐显示内容。 一、无序列表 标签名说明ul无序列表整体,用于包裹li标签li表示无序列表的每一项,用于包…...
在 Flutter 多人视频通话中实现虚拟背景、美颜与空间音效
前言 在之前的「基于声网 Flutter SDK 实现多人视频通话」里,我们通过 Flutter 声网 SDK 完美实现了跨平台和多人视频通话的效果,那么本篇我们将在之前例子的基础上进阶介绍一些常用的特效功能,包括虚拟背景、色彩增强、空间音频、基础变声…...
Ambari-web 架构
Ambari-web 使用的前端 Embar.js MVC 框架实现,Embar.js 是一个 TodoMVC 框架,涵盖了单页面应用(single page application)几乎所有的行为 Nodejs 是一个基于 Chrome JavaScript 运行时建立的一个平台,用来方便的搭建…...
Leetcode 3576. Transform Array to All Equal Elements
Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接:3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到…...
以下是对华为 HarmonyOS NETX 5属性动画(ArkTS)文档的结构化整理,通过层级标题、表格和代码块提升可读性:
一、属性动画概述NETX 作用:实现组件通用属性的渐变过渡效果,提升用户体验。支持属性:width、height、backgroundColor、opacity、scale、rotate、translate等。注意事项: 布局类属性(如宽高)变化时&#…...
uni-app学习笔记二十二---使用vite.config.js全局导入常用依赖
在前面的练习中,每个页面需要使用ref,onShow等生命周期钩子函数时都需要像下面这样导入 import {onMounted, ref} from "vue" 如果不想每个页面都导入,需要使用node.js命令npm安装unplugin-auto-import npm install unplugin-au…...
可靠性+灵活性:电力载波技术在楼宇自控中的核心价值
可靠性灵活性:电力载波技术在楼宇自控中的核心价值 在智能楼宇的自动化控制中,电力载波技术(PLC)凭借其独特的优势,正成为构建高效、稳定、灵活系统的核心解决方案。它利用现有电力线路传输数据,无需额外布…...
1688商品列表API与其他数据源的对接思路
将1688商品列表API与其他数据源对接时,需结合业务场景设计数据流转链路,重点关注数据格式兼容性、接口调用频率控制及数据一致性维护。以下是具体对接思路及关键技术点: 一、核心对接场景与目标 商品数据同步 场景:将1688商品信息…...
对WWDC 2025 Keynote 内容的预测
借助我们以往对苹果公司发展路径的深入研究经验,以及大语言模型的分析能力,我们系统梳理了多年来苹果 WWDC 主题演讲的规律。在 WWDC 2025 即将揭幕之际,我们让 ChatGPT 对今年的 Keynote 内容进行了一个初步预测,聊作存档。等到明…...
《通信之道——从微积分到 5G》读书总结
第1章 绪 论 1.1 这是一本什么样的书 通信技术,说到底就是数学。 那些最基础、最本质的部分。 1.2 什么是通信 通信 发送方 接收方 承载信息的信号 解调出其中承载的信息 信息在发送方那里被加工成信号(调制) 把信息从信号中抽取出来&am…...
Qt Http Server模块功能及架构
Qt Http Server 是 Qt 6.0 中引入的一个新模块,它提供了一个轻量级的 HTTP 服务器实现,主要用于构建基于 HTTP 的应用程序和服务。 功能介绍: 主要功能 HTTP服务器功能: 支持 HTTP/1.1 协议 简单的请求/响应处理模型 支持 GET…...
C# SqlSugar:依赖注入与仓储模式实践
C# SqlSugar:依赖注入与仓储模式实践 在 C# 的应用开发中,数据库操作是必不可少的环节。为了让数据访问层更加简洁、高效且易于维护,许多开发者会选择成熟的 ORM(对象关系映射)框架,SqlSugar 就是其中备受…...
多种风格导航菜单 HTML 实现(附源码)
下面我将为您展示 6 种不同风格的导航菜单实现,每种都包含完整 HTML、CSS 和 JavaScript 代码。 1. 简约水平导航栏 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport&qu…...
