NDK RTMP直播客户端二

news2024/11/17 7:26:02

在之前完成的实战项目【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,bitrate
	mNdkPusher.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) {
    // 初始化 VideoChannel
    videoChannel = 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_in
    if (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,  就是交错模式VUVUVUVU
    param.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/s
    param.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层视频推流编码

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 -- delete
    strcpy(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的 uuuuvvvv
    for (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; // 开始封包RTMPPacket
    RTMPPacket_Alloc(packet, body_size); // 堆区实例化 RTMPPacket

    int i = 0;
    packet->m_body[i++] = 0x17; // 十六进制转换成二进制,二进制查表 就懂了

    packet->m_body[i++] = 0x00;   // 重点是此字节 如果是1 帧类型(关键帧 非关键帧), 如果是0一定是 sps pps
    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;
    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.html
    packet->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 01
    if (pPayload[2] == 0x00){ // 00 00 00 01
        pPayload += 4; // 例如:共10个,挪动4个后,还剩6个
        // 保证 我们的长度是和上的数据对应,也要是6个,所以-= 4
        payload -= 4;
    }else if(pPayload[2] == 0x01){ // 00 00 01
        pPayload +=3; // 例如:共10个,挪动3个后,还剩7个
        // 保证 我们的长度是和上的数据对应,也要是7个,所以-= 3
        payload -= 3;
    }

    // 根据协议设置压缩包数据长度
    int body_size = 5 + 4 + payload;

    RTMPPacket *packet = new RTMPPacket; // 开始封包RTMPPacket
    RTMPPacket_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 pps
    packet->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.html
    packet->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实现推流,直播客户端。

视频推流完成,下一节开始音频推流工作。。。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/419793.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

在国内pmp证书有什么含金量?

关于PMP的含金量&#xff0c;很多回答的说法都差不多&#xff0c;但那也只是字面上的含金量&#xff0c;真正的含金量还是得自己考过了之后能够给自己带来的帮助才方可对PMP含金量下定义&#xff0c;但能一眼就能看到的含金量是在一些招聘信息上关于PMP证书的要求&#xff0c;下…

【Axure教程】日期时间下拉列表

在系统中&#xff0c;我们经常会用到日期时间选择器&#xff0c;它同时包含了日历日期的选择和时间的选择&#xff0c;一般是下拉列表的形式进行选择。 今天作者就教大家如何在Axure中用中继器制作真实日期时间效果的下拉列表选。 一、效果展示 1、点击控件&#xff0c;可以…

游戏开发学习路线图(2023最新版)建议收藏

游戏开发是一个高度技术化的领域&#xff0c;需要掌握编程语言和编程技能。你可以从学习基本的编程概念和语法开始&#xff0c;如C、C#、Python等常用的游戏编程语言。掌握编程的基础知识是游戏开发的基石。很多小伙伴不知道怎么学习游戏开发&#xff0c;那么今天&#xff0c;就…

c/c++:windows平台下依赖的动态库,c底层是汇编语言,程序断点调试,反汇编,vs快捷键

c/c&#xff1a;windows平台下依赖的动态库&#xff0c;c底层是汇编语言&#xff0c;程序断点调试&#xff0c;反汇编&#xff0c;vs快捷键 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;此时学会c的话&#xff0c; 我所知…

【多媒体】多媒体架构

多媒体架构 首先,多媒体架构层通常由三个主要层次组成: 应用层:负责媒体的展示、控制和交互等功能,如播放器、视频编辑器等。应用层一般是用户最直接接触到的界面。 中间件层:提供了各种媒体操作的基本服务,如编解码、音频合成、图像处理、网络传输、数据存储等。中间件…

【图像分割】Meta分割一切(SAM)模型环境配置和使用教程

注意&#xff1a;python>3.8, pytorch>1.7,torchvision>0.8 Feel free to ask any question. 遇到问题欢迎评论区讨论. 官方教程&#xff1a; https://github.com/facebookresearch/segment-anything 1 环境配置 1.1 安装主要库&#xff1a; &#xff08;1&…

day08_数组

今日内容 零、 复习昨日 一、作业 二、数组 零、 复习昨日 方法/函数是什么? 方法就是完成某个功能的一段代码的集合例如: 方法是 做饭 方法的作用是什么? 封装的思想方便维护方便复用 方法的参数列表是什么意思? 参数:方法执行所需的数据参数列表: 方法执行可以需要多个数据…

Linux下C/C++ SNTP网络时间协议实现

对于许多应用程序&#xff0c;特别是在小型计算机和微控制器上&#xff0c;不需要NTP的最终性能。便开发了简单网络时间协议&#xff08;SNTP&#xff09;&#xff0c;为功能较弱的计算机提供时钟同步&#xff0c;而这些计算机不需要NTP的复杂性。 而简单网络时间协议&#xf…

简易糖尿病胰岛素注射量推荐系统运行记录(github项目)

前言 在github上找案例推理相关实现代码&#xff0c;找到这个项目&#xff0c;记录一下运行过程。项目地址&#xff1a;https://github.com/jcf-junior/Diabetes-CBR 运行记录 运行项目的前提是已经装好的所有request的包&#xff0c;电脑里已经安装过mongodb数据库。 原项目…

Linux XFS文件系统的备份与还原

文章目录Linux XFS文件系统的备份与还原XFS文件系统备份xfsdump语法xfsdump备份完整的文件系统用xfsdump进行增量备份XFS文件系统还原xfsrestore语法用xfsrestore观察和xfsdump后的备份数据内容简单恢复level 0 的文件系统恢复增量备份数据仅还原部分文件到xfsrestore交互模式L…

深入剖析Android视图层次结构,为什么UI界面如此多样化?

简述 在Android Framework中&#xff0c;渲染机制是指如何为应用程序的用户界面绘制和布局视图&#xff08;View&#xff09;。Android的视图层次结构&#xff08;View Hierarchy&#xff09;是由视图树中的每个节点表示的 。当更新视图树时&#xff0c;Android会执行以下流程…

记一次内存泄漏问题的排查

阶段一&#xff1a; 前段时间&#xff0c;突然发现服务在毫无征兆的情况下发生了重启。去看了一下容器退出的日志&#xff0c;发现内存利用率超过了100%&#xff0c;导致容器重启&#xff0c;进一步看了skyWalking&#xff0c;发现heap内存超了&#xff0c;当时只是简单的以为是…

HTML2.1列表标签

列表标签种类 无序列表 有序列表 自定义列表 使用场景&#xff1a;在列表中按照行展示关联性内容。 特点&#xff1a;按照行的形式&#xff0c;整齐显示内容。 一、无序列表 标签名说明ul无序列表整体&#xff0c;用于包裹li标签li表示无序列表的每一项&#xff0c;用于包…

【iOS】—— 消息传递和消息转发

消息传递和消息转发 文章目录消息传递和消息转发消息传递&#xff08;方法调用&#xff09;IMP指针IMP与SEL的区别与联系SEL是通过表取对应关系的IMP&#xff0c;进行方法的调用快速查找imp过程汇编代码查找过程总结消息发送快速查找imp(汇编):方法缓存慢速查找总结慢速查找消息…

全链路日志追踪

背景 最近线上的日志全局追踪 traceId 不好使了&#xff0c;不同请求经常出现重复的 traceId&#xff0c;或者通过某个请求的 traceId 追踪搜索&#xff0c;检索出了与该请求完全不相干的日志。我领导叫我去排查解决这个问题&#xff0c;这里我把我排查的过程思路以及如何解决…

真题详解(单元测试)-软件设计(五十)

真题详解(0/1背包)-软件设计&#xff08;四十九)https://blog.csdn.net/ke1ying/article/details/130163955 单元测试 五个特征&#xff1a;模块接口、局部数据结构、重要执行路径、出错处理、边界条件。 模块接口&#xff0c;保证测试模块数据流正确的流入和流出。 测试模块用…

大数据相关知识

1、大数据整体简介 1、1 简介 百度百科这样写道 大数据&#xff08;big data&#xff09;&#xff0c;IT行业术语&#xff0c;是指无法在一定时间范围内用常规软件工具进行捕捉、管理和处理的数据集合&#xff0c;是需要新处理模式才能具有更强的决策力、洞察发现力和流程优…

C++类的学习1

类的定义一般包括两部分&#xff0c;一是类的属性&#xff0c;二是他所拥有的方法。类的实例化是指给类的加载并初始化过程&#xff0c;比如一个people类&#xff0c;我们具体到每一个人就是类的实例化&#xff0c;此外一个类可以在此类上进行扩展。比如people类&#xff0c;我…

vue项目使用luckyexcel预览excel表格

场景 最近工作中在开发文档上传并能在新窗口打开预览的功能。在此记录下心路历程。 方法 我总共尝试了2种方法预览excel&#xff0c;均可实现。还发现一种方法可以实现&#xff0c;需要后端配合&#xff0c;叫做KKfileview。 1.使用luckyexcel插件实现xlsx的预览 2.使用fi…

组合式升降压PFC的分析方法

1. 组合式升降压PFC的基本原理 组合式升降压PFC采用两组储能元件&#xff0c;基本单元为Cuk&#xff0c;Sepic和Zeta。参考论文《New Efficient Bridgeless Cuk Rectifiers for PFC Applications》中的三种拓扑进行分析。   Cuk型PFC的TypeI如下图所示&#xff0c;正半周Dp一…