Android平台实现无纸化同屏并推送RTMP或轻量级RTSP服务(毫秒级延迟)

​技术背景

在写这篇文章之前,实际上几年之前,我们就有非常稳定的无纸化同屏的模块,本文借demo更新,算是做个新的总结,废话不多说,先看图,本文以Android平台屏幕实时采集推送,Windows播放为例,和大家做个技术分享。

技术考量指标

本文以大牛直播SDK前些年实现的Android同屏采集推送为例,大概介绍下一些技术考量指标。

1. 轻量级RTSP服务还是RTMP?

我们在做无纸化同屏的时候,问的最多的是,能不能不要自建服务,直接主讲人或教师端,直接启动轻量级RTSP服务,其他终端拉流,如果是小并发,比如5人内的小范围的同屏,Windows平台走轻量级RTSP无可厚非,如果是30-60甚至100人的会议室,建议走RTMP。

2. 推送分辨率和码率选择

我们接触到好多设备,性能一般,但是屏幕是高分屏,甚至可以采集到4K的,考虑到实时编码和并发环境下,AP的承载能力,一般建议选择适合自己的分辨率码率即可,不要只追求高分辨率高码率,导致组网困难,单个或双通道AP压力大,一般建议控制在1920*1080分辨率内,码率控制在1-5M。

3. 软编码还是硬编码

Windows平台,一般优先考虑软编,因为大多Windows性能瓶颈不太大,超过1080P可以考虑硬编,Android平台建议直接硬编码。

4. 高分屏采集编码效率低怎么办

高分屏,不管是Windows还是Android,采集后的数据,建议先压缩,再编码,Windows平台我们可以设置压缩比例(scale rate),Android平台亦可,比如采集原始屏幕,或者缩放后的屏幕,具体见下图:

ini 复制代码
  /* BackgroudService.java
   * Author: daniusdk.com
   */ 
  private void createScreenEnvironment() {

        sreenWindowWidth = mWindowManager.getDefaultDisplay().getWidth();
        screenWindowHeight = mWindowManager.getDefaultDisplay().getHeight();

        Log.i(TAG, "screenWindowWidth: " + sreenWindowWidth + ",screenWindowHeight: "
                + screenWindowHeight);

        if (sreenWindowWidth > 800)
        {
            if (screen_resolution_type_ == SCREEN_RESOLUTION_STANDARD)
            {
                scale_rate = SCALE_RATE_HALF;
                sreenWindowWidth = align(sreenWindowWidth / 2, 16);
                screenWindowHeight = align(screenWindowHeight / 2, 16);
            }
            else if(screen_resolution_type_ == SCREEN_RESOLUTION_LOW)
            {
                scale_rate = SCALE_RATE_TWO_FIFTHS;
                sreenWindowWidth = align(sreenWindowWidth * 2 / 5, 16);
                screenWindowHeight = align(screenWindowHeight * 2 / 5, 16);
            }
        }

        Log.i(TAG, "After adjust mWindowWidth: " + sreenWindowWidth + ", mWindowHeight: " + screenWindowHeight);

        int pf = mWindowManager.getDefaultDisplay().getPixelFormat();
        Log.i(TAG, "display format:" + pf);

        DisplayMetrics displayMetrics = new DisplayMetrics();
        mWindowManager.getDefaultDisplay().getMetrics(displayMetrics);
        mScreenDensity = displayMetrics.densityDpi;

        mImageReader = ImageReader.newInstance(sreenWindowWidth,
                screenWindowHeight, 0x1, 6);

        mMediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
    }

5. Android横竖屏自动适配

Android平台,如果是pad采集,基本就是横屏采集,如果手机端,需要确保横竖屏模式下都可以正常采集。

4. 为什么要考虑补帧

Android的时候,一定的采集模式下,屏幕如果没有变化,不会一直有实时屏幕数据回调下来,这时候,为了保持帧率或数据采集的完整性,建议补帧。

5. 异常网络处理、事件回调机制

网络状态,不管是推送端,还是播放端,都是需要有实时的状态回调,确保客户端可以实时感知网络状态。

ini 复制代码
backgroudService.SetEventListener(new EventListener() {
                  @Override
                  public void onPublisherEventCallback(long handle, int id, long param1, long param2, String param3, String param4, Object param5) {
                      String publisher_event = "";

                      switch (id) {
                          case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_STARTED:
                              publisher_event = "开始..";
                              break;
                          case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTING:
                              publisher_event = "连接中..";
                              break;
                          case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTION_FAILED:
                              publisher_event = "连接失败..";
                              break;
                          case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTED:
                              publisher_event = "连接成功..";
                              break;
                          case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_DISCONNECTED:
                              publisher_event = "连接断开..";
                              break;
                          case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_STOP:
                              publisher_event = "关闭..";
                              break;
                          case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_RECORDER_START_NEW_FILE:
                              publisher_event = "开始一个新的录像文件 : " + param3;
                              break;
                          case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_ONE_RECORDER_FILE_FINISHED:
                              publisher_event = "已生成一个录像文件 : " + param3;
                              break;

                          case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_SEND_DELAY:
                              publisher_event = "发送时延: " + param1 + " 帧数:" + param2;
                              break;

                          case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_CAPTURE_IMAGE:
                              publisher_event = "快照: " + param1 + " 路径:" + param3;

                              if (param1 == 0) {
                                  publisher_event = publisher_event + "截取快照成功..";
                              } else {
                                  publisher_event = publisher_event + "截取快照失败..";
                              }
                              break;
                          case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_RTSP_URL:
                              publisher_event = "RTSP服务URL: " + param3;
                              break;
                          case NTSmartEventID.EVENT_DANIULIVE_ERC_PUSH_RTSP_SERVER_RESPONSE_STATUS_CODE:
                              publisher_event = "RTSP status code received, codeID: " + param1 + ", RTSP URL: " + param3;
                              break;
                          case NTSmartEventID.EVENT_DANIULIVE_ERC_PUSH_RTSP_SERVER_NOT_SUPPORT:
                              publisher_event = "服务器不支持RTSP推送, 推送的RTSP URL: " + param3;
                              break;
                      }

                      String str = "当前状态:" + publisher_event;

                      Log.i(TAG, str);

                      if (handler_ != null) {
                          Message message = new Message();
                          message.what = PUBLISHER_EVENT_MSG;
                          message.obj = publisher_event;
                          handler_.sendMessage(message);
                      }
                  }
            });

6. 采集到的数据可以按需录像吗

可以,而且很有必要,同屏的时候,如果需要把开会或教授内容实时保存下来,可以随时启动录像。

scss 复制代码
    public boolean startRecorder()
    {
        Log.i(TAG, "onClick startRecorder..");

        if(!stream_publisher_.is_publishing())
        {
            startCaptureScreen();
        }

        if (layer_post_thread_ != null)
            layer_post_thread_.update_layers();

        if (stream_publisher_.is_recording()) {
            stopRecorder();
            return false;
        }

        InitAndSetConfig();

        ConfigRecorderParam();

        boolean start_ret = stream_publisher_.StartRecorder();
        if (!start_ret) {
            stream_publisher_.try_release();
            Log.e(TAG, "Failed to start recorder.");
            return false;
        }

        startAudioRecorder();
        startLayerPostThread();

        return true;
    }

    //停止录像
    public void stopRecorder() {
        stream_publisher_.StopRecorder();
        stream_publisher_.try_release();

        if (!stream_publisher_.is_publishing())
            stopAudioRecorder();
    }

7. 文字、图片水印

需要而且建议支持,比如实时时间、学校或公司logo等。

ini 复制代码
        //水印效果选择++++++++++
        watermarkSelctor = (Spinner) findViewById(R.id.watermarkSelctor);
        watermarkSelctor.setEnabled(false);
        final String[] watermarks = new String[]{"图片水印", "全部水印", "文字水印", "不加水印"};

        ArrayAdapter<String> adapterWatermark = new ArrayAdapter<String>(this,
                android.R.layout.simple_spinner_item, watermarks);

        adapterWatermark.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);

        watermarkSelctor.setAdapter(adapterWatermark);

        watermarkSelctor.setSelection(3,true);
        watemarkType = 3;   //默认不加水印

        watermarkSelctor.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {

            @Override
            public void onItemSelected(AdapterView<?> parent, View view,
                                       int position, long id) {

                watemarkType = position;

                Log.i(TAG, "[水印类型]Currently choosing: " + watermarks[position] + ", watemarkType: " + watemarkType);

                if(backgroudService !=null) {
                    backgroudService.updateWatermarker(watemarkType);
                }
            }

            @Override
            public void onNothingSelected(AdapterView<?> parent) {

            }
        });

8. 可以同时启动轻量级RTSP服务吗

ini 复制代码
    public boolean startRtspService(int port)
    {
        Log.i(TAG, "startRtspService++");

        rtsp_handle_ = lib_publisher_.OpenRtspServer(0);

        if (rtsp_handle_ == 0) {
            Log.e(TAG, "创建rtsp server实例失败! 请检查SDK有效性");
        } else {
            if (lib_publisher_.SetRtspServerPort(rtsp_handle_, port) != 0) {
                lib_publisher_.CloseRtspServer(rtsp_handle_);
                rtsp_handle_ = 0;
                Log.e(TAG, "创建rtsp server端口失败! 请检查端口是否重复或者端口不在范围内!");
            }

            if (lib_publisher_.StartRtspServer(rtsp_handle_, 0) == 0) {
                Log.i(TAG, "启动rtsp server 成功!");
            } else {
                lib_publisher_.CloseRtspServer(rtsp_handle_);
                rtsp_handle_ = 0;
                Log.e(TAG, "启动rtsp server失败! 请检查设置的端口是否被占用!");
            }

            isRTSPServiceRunning = true;
        }

        return true;
    }

    //停止RTSP服务
    public void stopRtspService() {
        Log.i(TAG, "stopRtspService++");
        if(!isRTSPServiceRunning)
        {
            return;
        }
        if (lib_publisher_ != null && rtsp_handle_ != 0) {
            lib_publisher_.StopRtspServer(rtsp_handle_);
            lib_publisher_.CloseRtspServer(rtsp_handle_);
            rtsp_handle_ = 0;
        }

        isRTSPServiceRunning = false;
    }

    public boolean startRtspPublisher(){
        Log.i(TAG, "startRtspPublisher++");

        if(!stream_publisher_.is_publishing())
        {
            startCaptureScreen();
        }

        InitAndSetConfig();

        String rtsp_stream_name = "stream1";
        stream_publisher_.SetRtspStreamName(rtsp_stream_name);
        stream_publisher_.ClearRtspStreamServer();

        stream_publisher_.AddRtspStreamServer(rtsp_handle_);

        if (!stream_publisher_.StartRtspStream()) {
            stream_publisher_.try_release();
            Log.e(TAG, "调用发布rtsp流接口失败!");
            return false;
        }

        startAudioRecorder();
        startLayerPostThread();

        return true;
    }

    //停止发布RTSP流
    public void stopRtspPublisher() {
        Log.i(TAG, "stopRtspPublisher++");
        stream_publisher_.StopRtspStream();
        stream_publisher_.try_release();

        if (!stream_publisher_.is_publishing())
            stopAudioRecorder();
    }

    public int getRtspSessionNumbers(){
        int session_numbers = 0;
        if (lib_publisher_ != null && rtsp_handle_ != 0) {
            session_numbers = lib_publisher_.GetRtspServerClientSessionNumbers(rtsp_handle_);
            Log.i(TAG, "GetRtspSessionNumbers: " + session_numbers);
        }

        return session_numbers;
    }
  1. 同屏延迟,能不能做到毫秒级

废话不多说,见上图,延迟毫秒级。

10. 能不能采集到扬声器的audio?

Windows不在话下,Android平台需要高版本支持,高版本是可以采集到扬声器数据的,我们也实现了相关的demo,可以同时采集麦克风和扬声器的audio,单独推送或者同时混音输出。

11. 同屏过程中,重点画面可以快照吗?

当然可以,我们同屏采集端,支持采集编码png或jpg格式输出。

总结

其实一个好的无纸化同屏系统,需要考虑的有整体组网、分辨率、码率、实时延迟、音视频同步和连续性等各个指标,做容易,做好难,上述抛砖引玉,未能面面俱到,感兴趣的开发者,可以跟我单独Q我89030985交流。

相关推荐
chenchao_shenzhen2 天前
RK3568嵌入式音视频硬件编解码4K 60帧 rkmpp FFmpeg7.1 音视频开发
ffmpeg·音视频·rk3588·音视频开发·嵌入式开发·瑞芯微rk3568·硬件编解码
码流怪侠4 天前
Google SoundStream音频编解码器技术解析
深度学习·音视频开发
字节跳动视频云技术团队5 天前
基于 DiT 大模型与字体级分割的视频字幕无痕擦除方案,助力短剧出海
aigc·音视频开发·视频编码
音视频牛哥6 天前
跨平台轻量级RTSP服务模块技术详解与内网低延迟直播实践
音视频开发·视频编码·直播
aqi007 天前
FFmpeg开发笔记(八十)使用百变魔音AiSound实现变声特效
android·ffmpeg·音视频·直播·流媒体
aqi008 天前
FFmpeg开发笔记(七十九)专注于视频弹幕功能的国产弹弹播放器
android·ffmpeg·音视频·直播·流媒体
音视频牛哥11 天前
SmartMediaKit 模块化音视频框架实战指南:场景链路 + 能力矩阵全解析
音视频开发·视频编码·直播
子龙_11 天前
JS解析wav音频数据并使用wasm加速
前端·javascript·音视频开发
泉城老铁12 天前
Spring Boot + Vue + ZLMediaKit 实现 RTSP 拉流播放的完整方案
java·vue.js·音视频开发