NDK 直播推流与引流

简介: 流媒体服务器测试

作者:字节流动

来源:https://blog.csdn.net/Kennethdroid/article/details/86418725


流媒体服务器测试

首先利用快直播 app(其他支持 RTMP 推流与引流的 app 亦可)和 ffplay.exe 对流媒体服务器进行测试。

快直播的推流界面和引流界面

image.png

image.png

Windows 下利用 ffplay 进行引流,命令行执行:

ffplay rtmp://192.168.0.0/live/test  
# ip 地址换成流媒体服务器的地址, test 表示直播房间号

测试结果:

image.png

推流

本文直播推流步骤:

  • 使用 AudioRecord 采集音频,使用 Camera API 采集视频数据
  • 分别使用 faac 和 xh264 第三方库在 Native 层对音频和视频进行编码
  • 利用 rtmp-dump 第三方库进行打包和推流

工程目录:

image.png

主要的 JNI 方法:

public class NativePush {
    public native void startPush(String url);
    public native void stopPush();
    public native void release();
    /**
     * 设置视频参数
     * @param width
     * @param height
     * @param bitrate
     * @param fps
     */
    public native void setVideoOptions(int width, int height, int bitrate, int fps);
    /**
     * 设置音频参数
     * @param sampleRateInHz
     * @param channel
     */
    public native void setAudioOptions(int sampleRateInHz, int channel);
    /**
     * 发送视频数据
     * @param data
     */
    public native void fireVideo(byte[] data);
    /**
     * 发送音频数据
     * @param data
     * @param len
     */
    public native void fireAudio(byte[] data, int len);
}

视频采集

视频采集主要基于 Camera 相关 API ,利用 SurfaceView 进行预览,通过 PreviewCallback 获取相机预览数据。

视频预览主要代码实现:

   public void startPreview(){
        try {
            mCamera = Camera.open(mVideoParams.getCameraId());
            Camera.Parameters param = mCamera.getParameters();
            List<Camera.Size> previewSizes = param.getSupportedPreviewSizes();
            int length = previewSizes.size();
            for (int i = 0; i < length; i++) {
                Log.i(TAG, "SupportedPreviewSizes : " + previewSizes.get(i).width + "x" + previewSizes.get(i).height);
            }
            mVideoParams.setWidth(previewSizes.get(0).width);
            mVideoParams.setHeight(previewSizes.get(0).height);
            param.setPreviewFormat(ImageFormat.NV21);
            param.setPreviewSize(mVideoParams.getWidth(), mVideoParams.getHeight());
            mCamera.setParameters(param);
            //mCamera.setDisplayOrientation(90); // 竖屏
            mCamera.setPreviewDisplay(mSurfaceHolder);
            buffer = new byte[mVideoParams.getWidth() * mVideoParams.getHeight() * 4];
            mCamera.addCallbackBuffer(buffer);
            mCamera.setPreviewCallbackWithBuffer(this);
            mCamera.startPreview();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

利用 FrameCallback 获取预览数据传入 Native 层,然后进行编码:

    @Override
    public void onPreviewFrame(byte[] bytes, Camera camera) {
        if (mCamera != null) {
            mCamera.addCallbackBuffer(buffer);
        }
        if (mIsPushing) {
            mNativePush.fireVideo(bytes);
        }
    }

音频采集

音频采集基于 AudioRecord 实现,在一个子线程采集音频 PCM 数据,并将数据不断传入 Native 层进行编码。

    private class AudioRecordRunnable implements Runnable {
        @Override
        public void run() {
            mAudioRecord.startRecording();
            while (mIsPushing) {
                //通过AudioRecord不断读取音频数据
                byte[] buffer = new byte[mMinBufferSize];
                int length = mAudioRecord.read(buffer, 0, buffer.length);
                if (length > 0) {
                    //传递给 Native 代码,进行音频编码
                    mNativePush.fireAudio(buffer, length);
                }
            }
        }
    }

编码和推流

音视频数据编码和推流在 Native 层实现,首先添加 faac , x264 , librtmp 第三方库到 AS 工程,然后初始化相关设置,基于生产者与消费者模式,将编码后的音视频数据,在生产者线程中打包 RTMPPacket 放入双向链表,在消费者线程中从链表中取 RTMPPacket ,通过 RTMP_SendPacket 方法发送给服务器。

x264 初始化:

JNIEXPORT void JNICALL
Java_com_haohao_live_jni_NativePush_setVideoOptions(JNIEnv *env, jobject instance, jint width,
                                                    jint height, jint bitRate, jint fps) {
    x264_param_t param;
    //x264_param_default_preset 设置
    x264_param_default_preset(&param, "ultrafast", "zerolatency");
    //编码输入的像素格式YUV420P
    param.i_csp = X264_CSP_I420;
    param.i_width = width;
    param.i_height = height;
    y_len = width * height;
    u_len = y_len / 4;
    v_len = u_len;
    //参数i_rc_method表示码率控制,CQP(恒定质量),CRF(恒定码率),ABR(平均码率)
    //恒定码率,会尽量控制在固定码率
    param.rc.i_rc_method = X264_RC_CRF;
    param.rc.i_bitrate = bitRate / 1000; //* 码率(比特率,单位Kbps)
    param.rc.i_vbv_max_bitrate = bitRate / 1000 * 1.2; //瞬时最大码率
    //码率控制不通过timebase和timestamp,而是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;
    param.i_threads = 1;//并行编码线程数量,0默认为多线程
    //是否把SPS和PPS放入每一个关键帧
    //SPS Sequence Parameter Set 序列参数集,PPS Picture Parameter Set 图像参数集
    //为了提高图像的纠错能力
    param.b_repeat_headers = 1;
    //设置Level级别
    param.i_level_idc = 51;
    //设置Profile档次
    //baseline级别,没有B帧,只有 I 帧和 P 帧
    x264_param_apply_profile(&param, "baseline");
    //x264_picture_t(输入图像)初始化
    x264_picture_alloc(&pic_in, param.i_csp, param.i_width, param.i_height);
    pic_in.i_pts = 0;
    //打开编码器
    video_encode_handle = x264_encoder_open(&param);
    if (video_encode_handle) {
        LOGI("打开视频编码器成功");
    } else {
        throwNativeError(env, INIT_FAILED);
    }
}

faac 初始化:

JNIEXPORT void JNICALL
Java_com_haohao_live_jni_NativePush_setAudioOptions(JNIEnv *env, jobject instance,
                                                    jint sampleRateInHz, jint channel) {
    audio_encode_handle = faacEncOpen(sampleRateInHz, channel, &nInputSamples,
                                      &nMaxOutputBytes);
    if (!audio_encode_handle) {
        LOGE("音频编码器打开失败");
        return;
    }
    //设置音频编码参数
    faacEncConfigurationPtr p_config = faacEncGetCurrentConfiguration(audio_encode_handle);
    p_config->mpegVersion = MPEG4;
    p_config->allowMidside = 1;
    p_config->aacObjectType = LOW;
    p_config->outputFormat = 0; //输出是否包含ADTS头
    p_config->useTns = 1; //时域噪音控制,大概就是消爆音
    p_config->useLfe = 0;
//  p_config->inputFormat = FAAC_INPUT_16BIT;
    p_config->quantqual = 100;
    p_config->bandWidth = 0; //频宽
    p_config->shortctl = SHORTCTL_NORMAL;
    if (!faacEncSetConfiguration(audio_encode_handle, p_config)) {
        LOGE("%s", "音频编码器配置失败..");
        throwNativeError(env, INIT_FAILED);
        return;
    }
    LOGI("%s", "音频编码器配置成功");
}

对视频数据进行编码打包,通过 add_rtmp_packet 放入链表:

JNIEXPORT void JNICALL
Java_com_haohao_live_jni_NativePush_fireVideo(JNIEnv *env, jobject instance, jbyteArray buffer_) {
    //视频数据转为YUV420P
    //NV21->YUV420P
    jbyte *nv21_buffer = (*env)->GetByteArrayElements(env, buffer_, NULL);
    jbyte *u = pic_in.img.plane[1];
    jbyte *v = pic_in.img.plane[2];
    //nv21 4:2:0 Formats, 12 Bits per Pixel
    //nv21与yuv420p,y个数一致,uv位置对调
    //nv21转yuv420p  y = w*h,u/v=w*h/4
    //nv21 = yvu yuv420p=yuv y=y u=y+1+1 v=y+1
    //如果要进行图像处理(美颜),可以再转换为RGB
    //还可以结合OpenCV识别人脸等等
    memcpy(pic_in.img.plane[0], nv21_buffer, y_len);
    int i;
    for (i = 0; i < u_len; i++) {
        *(u + i) = *(nv21_buffer + y_len + i * 2 + 1);
        *(v + i) = *(nv21_buffer + y_len + i * 2);
    }
    //h264编码得到NALU数组
    x264_nal_t *nal = NULL; //NAL
    int n_nal = -1; //NALU的个数
    //进行h264编码
    if (x264_encoder_encode(video_encode_handle, &nal, &n_nal, &pic_in, &pic_out) < 0) {
        LOGE("%s", "编码失败");
        return;
    }
    //使用rtmp协议将h264编码的视频数据发送给流媒体服务器
    //帧分为关键帧和普通帧,为了提高画面的纠错率,关键帧应包含SPS和PPS数据
    int sps_len, pps_len;
    unsigned char sps[100];
    unsigned char pps[100];
    memset(sps, 0, 100);
    memset(pps, 0, 100);
    pic_in.i_pts += 1; //顺序累加
    //遍历NALU数组,根据NALU的类型判断
    for (i = 0; i < n_nal; i++) {
        if (nal[i].i_type == NAL_SPS) {
            //复制SPS数据,序列参数集(Sequence parameter set)
            sps_len = nal[i].i_payload - 4;
            memcpy(sps, nal[i].p_payload + 4, sps_len); //不复制四字节起始码
        } else if (nal[i].i_type == NAL_PPS) {
            //复制PPS数据,图像参数集(Picture parameter set)
            pps_len = nal[i].i_payload - 4;
            memcpy(pps, nal[i].p_payload + 4, pps_len); //不复制四字节起始码
            //发送序列信息
            //h264关键帧会包含SPS和PPS数据
            add_264_sequence_header(pps, sps, pps_len, sps_len);
        } else {
            //发送帧信息
            add_264_body(nal[i].p_payload, nal[i].i_payload);
        }
    }
    (*env)->ReleaseByteArrayElements(env, buffer_, nv21_buffer, 0);
}

同样,对音频数据进行编码打包放入链表:

JNIEXPORT void JNICALL
Java_com_haohao_live_jni_NativePush_fireAudio(JNIEnv *env, jobject instance, jbyteArray buffer_,
                                              jint length) {
    int *pcmbuf;
    unsigned char *bitbuf;
    jbyte *b_buffer = (*env)->GetByteArrayElements(env, buffer_, 0);
    pcmbuf = (short *) malloc(nInputSamples * sizeof(int));
    bitbuf = (unsigned char *) malloc(nMaxOutputBytes * sizeof(unsigned char));
    int nByteCount = 0;
    unsigned int nBufferSize = (unsigned int) length / 2;
    unsigned short *buf = (unsigned short *) b_buffer;
    while (nByteCount < nBufferSize) {
        int audioLength = nInputSamples;
        if ((nByteCount + nInputSamples) >= nBufferSize) {
            audioLength = nBufferSize - nByteCount;
        }
        int i;
        for (i = 0; i < audioLength; i++) {//每次从实时的pcm音频队列中读出量化位数为8的pcm数据。
            int s = ((int16_t *) buf + nByteCount)[i];
            pcmbuf[i] = s << 8;//用8个二进制位来表示一个采样量化点(模数转换)
        }
        nByteCount += nInputSamples;
        //利用FAAC进行编码,pcmbuf为转换后的pcm流数据,audioLength为调用faacEncOpen时得到的输入采样数,bitbuf为编码后的数据buff,nMaxOutputBytes为调用faacEncOpen时得到的最大输出字节数
        int byteslen = faacEncEncode(audio_encode_handle, pcmbuf, audioLength,
                                     bitbuf, nMaxOutputBytes);
        if (byteslen < 1) {
            continue;
        }
        add_aac_body(bitbuf, byteslen);//从bitbuf中得到编码后的aac数据流,放到数据队列
    }
    if (bitbuf)
        free(bitbuf);
    if (pcmbuf)
        free(pcmbuf);
    (*env)->ReleaseByteArrayElements(env, buffer_, b_buffer, 0);
}

消费者线程不断从链表中取 RTMPPacket 发送给服务器:

void *push_thread(void *arg) {
    JNIEnv *env;//获取当前线程JNIEnv
    (*javaVM)->AttachCurrentThread(javaVM, &env, NULL);
    //建立RTMP连接
    RTMP *rtmp = RTMP_Alloc();
    if (!rtmp) {
        LOGE("rtmp初始化失败");
        goto end;
    }
    RTMP_Init(rtmp);
    rtmp->Link.timeout = 5; //连接超时的时间
    //设置流媒体地址
    RTMP_SetupURL(rtmp, rtmp_path);
    //发布rtmp数据流
    RTMP_EnableWrite(rtmp);
    //建立连接
    if (!RTMP_Connect(rtmp, NULL)) {
        LOGE("%s", "RTMP 连接失败");
        throwNativeError(env, CONNECT_FAILED);
        goto end;
    }
    //计时
    start_time = RTMP_GetTime();
    if (!RTMP_ConnectStream(rtmp, 0)) { //连接流
        LOGE("%s", "RTMP ConnectStream failed");
        throwNativeError(env, CONNECT_FAILED);
        goto end;
    }
    is_pushing = TRUE;
    //发送AAC头信息
    add_aac_sequence_header();
    while (is_pushing) {
        //发送
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond, &mutex);
        //取出队列中的RTMPPacket
        RTMPPacket *packet = queue_get_first();
        if (packet) {
            queue_delete_first(); //移除
            packet->m_nInfoField2 = rtmp->m_stream_id; //RTMP协议,stream_id数据
            int i = RTMP_SendPacket(rtmp, packet, TRUE); //TRUE放入librtmp队列中,并不是立即发送
            if (!i) {
                LOGE("RTMP 断开");
                RTMPPacket_Free(packet);
                pthread_mutex_unlock(&mutex);
                goto end;
            } else {
                LOGI("%s", "rtmp send packet");
            }
            RTMPPacket_Free(packet);
        }
        pthread_mutex_unlock(&mutex);
    }
    end:
    LOGI("%s", "释放资源");
    free(rtmp_path);
    RTMP_Close(rtmp);
    RTMP_Free(rtmp);
    (*javaVM)->DetachCurrentThread(javaVM);
    return 0;
}

引流

这里引流就不做展开讲,可以通过 QLive 的 SDK 或者 vitamio (小楠总)等第三方库实现。

基于 vitamio 实现引流:

    private void init(){
        mVideoView = (VideoView) findViewById(R.id.live_player_view);
        mVideoView.setVideoPath(SPUtils.getInstance(this).getString(SPUtils.KEY_NGINX_SER_URI));
        mVideoView.setMediaController(new MediaController(this));
        mVideoView.requestFocus();
        mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mp) {
                mp.setPlaybackSpeed(1.0f);
            }
        });
    }

PS:源码地址:https://github.com/githubhaohao/NDKLive


NDK 开发系列文章:


「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。

阿里云社区.png

相关文章
|
存储 SQL Oracle
数据库概述 | 学习笔记
快速学习数据库概述。
142 0
 数据库概述 | 学习笔记
|
3天前
|
人工智能 自然语言处理 Shell
深度评测 | 仅用3分钟,百炼调用满血版 Deepseek-r1 API,百万Token免费用,简直不要太爽。
仅用3分钟,百炼调用满血版Deepseek-r1 API,享受百万免费Token。阿里云提供零门槛、快速部署的解决方案,支持云控制台和Cloud Shell两种方式,操作简便。Deepseek-r1满血版在推理能力上表现出色,尤其擅长数学、代码和自然语言处理任务,使用过程中无卡顿,体验丝滑。结合Chatbox工具,用户可轻松掌控模型,提升工作效率。阿里云大模型服务平台百炼不仅速度快,还确保数据安全,值得信赖。
157353 24
深度评测 | 仅用3分钟,百炼调用满血版 Deepseek-r1 API,百万Token免费用,简直不要太爽。
|
5天前
|
人工智能 API 网络安全
用DeepSeek,就在阿里云!四种方式助您快速使用 DeepSeek-R1 满血版!更有内部实战指导!
DeepSeek自发布以来,凭借卓越的技术性能和开源策略迅速吸引了全球关注。DeepSeek-R1作为系列中的佼佼者,在多个基准测试中超越现有顶尖模型,展现了强大的推理能力。然而,由于其爆火及受到黑客攻击,官网使用受限,影响用户体验。为解决这一问题,阿里云提供了多种解决方案。
17020 37
|
13天前
|
机器学习/深度学习 人工智能 自然语言处理
PAI Model Gallery 支持云上一键部署 DeepSeek-V3、DeepSeek-R1 系列模型
DeepSeek 系列模型以其卓越性能在全球范围内备受瞩目,多次评测中表现优异,性能接近甚至超越国际顶尖闭源模型(如OpenAI的GPT-4、Claude-3.5-Sonnet等)。企业用户和开发者可使用 PAI 平台一键部署 DeepSeek 系列模型,实现 DeepSeek 系列模型与现有业务的高效融合。
|
5天前
|
并行计算 PyTorch 算法框架/工具
本地部署DeepSeek模型
要在本地部署DeepSeek模型,需准备Linux(推荐Ubuntu 20.04+)或兼容的Windows/macOS环境,配备NVIDIA GPU(建议RTX 3060+)。安装Python 3.8+、PyTorch/TensorFlow等依赖,并通过官方渠道下载模型文件。配置模型后,编写推理脚本进行测试,可选使用FastAPI服务化部署或Docker容器化。注意资源监控和许可协议。
1311 8
|
13天前
|
人工智能 搜索推荐 Docker
手把手教你使用 Ollama 和 LobeChat 快速本地部署 DeepSeek R1 模型,创建个性化 AI 助手
DeepSeek R1 + LobeChat + Ollama:快速本地部署模型,创建个性化 AI 助手
3416 117
手把手教你使用 Ollama 和 LobeChat 快速本地部署 DeepSeek R1 模型,创建个性化 AI 助手
|
8天前
|
人工智能 自然语言处理 API
DeepSeek全尺寸模型上线阿里云百炼!
阿里云百炼平台近日上线了DeepSeek-V3、DeepSeek-R1及其蒸馏版本等六款全尺寸AI模型,参数量达671B,提供高达100万免费tokens。这些模型在数学、代码、自然语言推理等任务上表现出色,支持灵活调用和经济高效的解决方案,助力开发者和企业加速创新与数字化转型。示例代码展示了如何通过API使用DeepSeek-R1模型进行推理,用户可轻松获取思考过程和最终答案。
|
5天前
|
人工智能 自然语言处理 程序员
如何在通义灵码里用上DeepSeek-V3 和 DeepSeek-R1 满血版671B模型?
除了 AI 程序员的重磅上线外,近期通义灵码能力再升级全新上线模型选择功能,目前已经支持 Qwen2.5、DeepSeek-V3 和 R1系列模型,用户可以在 VSCode 和 JetBrains 里搜索并下载最新通义灵码插件,在输入框里选择模型,即可轻松切换模型。
934 14
|
12天前
|
API 开发工具 Python
阿里云PAI部署DeepSeek及调用
本文介绍如何在阿里云PAI EAS上部署DeepSeek模型,涵盖7B模型的部署、SDK和API调用。7B模型只需一张A10显卡,部署时间约10分钟。文章详细展示了模型信息查看、在线调试及通过OpenAI SDK和Python Requests进行调用的步骤,并附有测试结果和参考文档链接。
1938 9
阿里云PAI部署DeepSeek及调用
|
9天前
|
人工智能 数据可视化 Linux
【保姆级教程】3步搞定DeepSeek本地部署
DeepSeek在2025年春节期间突然爆火出圈。在目前DeepSeek的网站中,极不稳定,总是服务器繁忙,这时候本地部署就可以有效规避问题。本文以最浅显易懂的方式带读者一起完成DeepSeek-r1大模型的本地部署。

热门文章

最新文章