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

本文涉及的产品
视觉智能开放平台,视频资源包5000点
视觉智能开放平台,分割抠图1万点
视觉智能开放平台,图像资源包5000点
简介: 一个好的无纸化同屏系统,需要考虑的有整体组网、分辨率、码率、实时延迟、音视频同步和连续性等各个指标,做容易,做好难

技术背景

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

image.gif

技术考量指标

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

image.gif

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平台亦可,比如采集原始屏幕,或者缩放后的屏幕,具体见下图:

/* 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);
    }

image.gif

5. Android横竖屏自动适配

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

4. 为什么要考虑补帧

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

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

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

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);
                      }
                  }
            });

image.gif

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

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

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();
    }

image.gif

7. 文字、图片水印

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

//水印效果选择++++++++++
        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) {
            }
        });

image.gif

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

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;
    }

image.gif

9. 同屏延迟,能不能做到毫秒级


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

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

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

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

总结

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

相关文章
|
1天前
|
IDE 开发工具 Android开发
安卓与iOS开发对比:平台选择对项目成功的影响
【9月更文挑战第10天】在移动应用开发的世界中,选择正确的平台是至关重要的。本文将深入探讨安卓和iOS这两大主要移动操作系统的开发环境,通过比较它们的市场份额、开发工具、编程语言和用户群体等方面,为开发者提供一个清晰的指南。我们将分析这两个平台的优势和劣势,并讨论如何根据项目需求和目标受众来做出最佳选择。无论你是初学者还是有经验的开发者,这篇文章都将帮助你更好地理解每个平台的特性,并指导你做出明智的决策。
|
1天前
|
开发工具 Android开发 iOS开发
安卓与iOS开发:平台选择的艺术与科学
在移动应用开发的广阔天地中,安卓与iOS两大平台如同东西方哲学的碰撞,既有共通之处又各具特色。本文将深入探讨这两个平台的设计理念、开发工具和市场定位,旨在为开发者提供一份简明扼要的指南,帮助他们在这场技术与商业的博弈中找到自己的道路。通过比较分析,我们将揭示每个平台的优势与局限,以及它们如何影响应用的性能、用户体验和市场接受度。无论你是初涉江湖的新手,还是经验丰富的老手,这篇文章都将为你的选择提供新的视角和思考。
11 5
|
23小时前
|
JavaScript 前端开发 Android开发
让Vite+Vue3项目在Android端离线打开(不需要起服务)
让Vite+Vue3项目在Android端离线打开(不需要起服务)
|
1天前
|
Linux Android开发 iOS开发
探索Android与iOS开发:平台之战还是互补共生?
在移动应用开发的浩瀚宇宙中,Android和iOS这两大星系始终吸引着无数开发者的目光。它们各自拥有独特的引力场,引领着技术潮流的方向。本文将穿梭于这两个平台的星际空间,揭示它们背后的力量对比,以及如何在这两者之间找到平衡点,共同推动移动应用开发的进步。
8 1
|
1天前
|
移动开发 开发框架 Android开发
安卓与iOS开发:平台之战的新篇章
在移动应用开发的广阔天地中,安卓和iOS始终占据着主导地位。本文通过比较这两个平台的发展历程、技术特点及未来趋势,探讨了它们之间的竞争与合作。文章旨在为开发者提供一个清晰的平台选择指南,并预测未来移动开发的可能走向。
5 1
|
8天前
|
存储 Android开发 开发者
探索安卓开发之旅:从新手到专家的必经之路
【9月更文挑战第3天】在这篇文章中,我们将踏上一场激动人心的旅程,深入探索安卓开发的广阔天地。无论你是初涉编程世界的新手,还是期望提升技能的开发者,这里都有你需要的知识与技巧。我们将从基础概念讲起,逐步引导你了解安卓应用的核心组件,并分享实用的开发建议。准备好了吗?让我们一起开启这段成长之旅吧!
|
Android开发
Android平台设计规范整理(尺寸+组成元素+字体+滑块)
转自:http://www.ui.cn/project.php?id=12394
710 0
|
2天前
|
安全 Android开发 开发者
探索安卓开发的未来:Kotlin的崛起与Flutter的挑战
在移动开发的广阔天地中,安卓平台始终占据着举足轻重的地位。随着技术的不断进步和开发者需求的多样化,Kotlin和Flutter成为了改变游戏规则的新玩家。本文将深入探讨Kotlin如何以其现代化的特性赢得开发者的青睐,以及Flutter凭借跨平台的能力如何挑战传统的安卓开发模式。通过实际案例分析,我们将揭示这两种技术如何塑造未来的安卓应用开发。
17 6