草庐IT

Android平台实现系统内录(捕获播放的音频)并推送RTMP服务技术方案探究

音视频牛哥 2023-06-11 原文

几年来,我们在做无纸化同屏或在线教育相关场景的时候,总是被一件事情困扰:如何实现Android平台的系统内录,并推送到其他播放端,常用的场景比如做无纸化会议或教育的时候,主讲人或老师需要放一个视频,该怎么办呢?这里我们分析三种可行的技术方案:

方案1:解析视频文件推送

Android终端的话,先利用MediaExtractor,把mp4文件的音视频数据分离,然后调用我们publisher模块,实现编码后的数据对接到RTMP服务器,实例代码如下:

/*
 * SmartPublisherActivity.java
 * Github: https://github.com/daniulive/SmarterStreaming
 */  
private void InitMediaExtractor(){
    File mFile = new File("/storage/emulated/0/","2022.mp4");
  
    if (!mFile.exists()){
      Log.e(TAG, "mp4文件不存在");
      return;
    }
 
    MediaExtractor mediaExtractor = new MediaExtractor();
    try {
      mediaExtractor.setDataSource(mFile.getAbsolutePath());
    } catch (IOException e) {
      e.printStackTrace();
    }
 
    int count = mediaExtractor.getTrackCount();//获取轨道数量
    Log.e(TAG, "轨道数量 = "+count);
 
    for (int i = 0; i < count; i++)
    {
      MediaFormat trackFormat = mediaExtractor.getTrackFormat(i);
      String mineType = trackFormat.getString(MediaFormat.KEY_MIME);
      Log.e(TAG, i + "编号通道格式 = " + mineType);
 
      //视频信道
      if (mineType.startsWith("video/")) {
        video_track_index = i;
        is_has_video = true;
 
        try {
          video_media_extractor.setDataSource(mFile.getAbsolutePath());
        } catch (IOException e) {
          e.printStackTrace();
        }
 
        if(mineType.equals("video/avc"))
        {
          video_codec_id = 1;
        }
        else if(mineType.equals("video/hevc"))
        {
          video_codec_id = 2;
        }
 
        int width = trackFormat.getInteger(MediaFormat.KEY_WIDTH);
        int height = trackFormat.getInteger(MediaFormat.KEY_HEIGHT);
        long duration = trackFormat.getLong(MediaFormat.KEY_DURATION);//总时间
        int video_fps = trackFormat.getInteger(MediaFormat.KEY_FRAME_RATE);//帧率
        max_sample_size = trackFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);//获取视频缓存输出的最大大小
 
        Log.e(TAG, "video width " + width + ", height: " + height + ", duration: " + duration + ", max_sample_size: " + max_sample_size + ", fps: " + video_fps);
      }
 
      //音频信道
      if (mineType.startsWith("audio/")) {
        audio_track_index = i;
        is_has_audio = true;
 
        try {
          audio_media_extractor.setDataSource(mFile.getAbsolutePath());
        } catch (IOException e) {
          e.printStackTrace();
        }
 
 
        if(mineType.equals("audio/mp4a-latm"))
        {
          audio_codec_id = 0x10002;
        }
 
        audio_sample_rate = trackFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);//获取采样率
        int audioTrackBitrate = trackFormat.getInteger(MediaFormat.KEY_BIT_RATE);      //获取比特率
        int channels = trackFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);       //获取声道数量
 
        Log.e(TAG, "mp4 audio_sample_rate " + audio_sample_rate + ", audioTrackBitrate: " + audioTrackBitrate + ", channels: " + channels);
      }
    }
  }

推送video数据:

if(IsVpsSpsPps(video_header_checker_buffer, video_codec_id))
{
  is_key_frame = true;
}
 
if ( isPushing || isRTSPPublisherRunning || isGB28181StreamRunning) {
  libPublisher.SmartPublisherPostVideoEncodedData(publisherHandle, video_codec_id, byteBuffer, video_sample_size, is_key_frame?1:0, cur_sample_time, cur_sample_time);
}

推送audio数据:

int audio_sample_size = audio_media_extractor.readSampleData(byteBuffer, 0);
 
if(audio_sample_size < 0)
{
  Log.i(TAG, "audio reach the end..");
  break;
}
 
long cur_sample_time = audio_media_extractor.getSampleTime()/1000;
 
if ( isPushing || isRTSPPublisherRunning || isGB28181StreamRunning) {
  libPublisher.SmartPublisherPostAudioEncodedData(publisherHandle, audio_codec_id, byteBuffer, audio_sample_size, 0, cur_sample_time, parameter_info, parameter_info_size);
}

上述代码,实现原理很简单,无非就是想把audio video从容器中分离出来,然后打包发出去,我们有做流媒体后视镜相关场景的合作公司,就这么实现过。

方案2:REMOTE_SUBMIX

Android中可以通过使用MediaRecorder.AudioSource.REMOTE_SUBMIX来实现系统声音的录制,这个属性只有系统应用能够使用,而且这个属性会截掉耳机和扬声器的声音,让我们听不到手机中播放音乐或者视频时的声音,而录制结束后会发现播放录制好的文件是有这些声音的。一般来说,做无纸化会议或教育同屏的公司,如果硬件是厂商定制的,可以跟厂商提出来,修改ROM,得到内录audio权限和数据。为此,我们专门设计了个接口,便于有这个权限的厂商使用。

REMOTE_SUBMIX可以实现内录功能,有几点需要注意:需要有系统权限,而且会截走扬声器和耳机的声音,也就是说再录音时本地无法播放声音,对于系统权限,可在AndroidManifest.xml添加 android:sharedUserId="android.uid.system",然后使用系统签名来打包应用。 

private void CheckInitAudioRecorder() {
        if (audioRecord_ == null) {
            //audioRecord_ = new NTAudioRecord(this, 1);

            audioRecord_ = new NTAudioRecordV2(this);
        }

        if (audioRecord_ != null) {
            Log.i(TAG, "CheckInitAudioRecorder call audioRecord_.start()+++...");

            audioRecordCallback_ = new NTAudioRecordV2CallbackImpl();

            //audioRecord_.IsMicSource(true);       //如音频采集声音过小,建议开启

            // audioRecord_.IsRemoteSubmixSource(true);

            audioRecord_.AddCallback(audioRecordCallback_);

            audioRecord_.Start();

            Log.i(TAG, "CheckInitAudioRecorder call audioRecord_.start()---...");
        }
    }

方案3:AudioPlaybackCapture API

也是本文提到的重点,实际上,Android 10 已引入 AudioPlaybackCapture API。应用可以借助此 API 复制其他应用正在播放的音频。此功能类似于屏幕采集,但采集对象是音频。主要用例是视频在线播放应用,这些应用希望捕获游戏正在播放的音频。对于其音频正在被捕获的应用,Capture API 不会影响该应用的延迟时间。

为确保安全性和隐私,“捕获播放的音频”功能会施加一些限制。为了能够捕获音频,应用必须满足以下要求:

捕获和播放音频的应用必须使用同一份用户个人资料。

捕获音频

如要从其他应用中捕获音频,您的应用必须构建 ​​AudioRecord​​​ 对象,并向其添加 ​​AudioPlaybackCaptureConfiguration​​。请按以下步骤操作:

  1. 调用 ​​AudioPlaybackCaptureConfiguration.Builder.build()​​​ 以构建 ​​AudioPlaybackCaptureConfiguration​​。
  2. 通过调用 ​​setAudioPlaybackCaptureConfig​​​ 将配置传递到 ​​AudioRecord​​。

采集的话,10.0以上版本,按照上述设置即可获取到数据。

if(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
{
  CheckInitAudioRecorderSpeaker();    //采集扬声器,需要android 10.0+版本
}

private void CheckInitAudioRecorderSpeaker() {
  if (audioRecordSpeaker_ == null) {
    audioRecordSpeaker_ = new AudioRecordSpeaker(this);
  }

  if (audioRecordSpeaker_ != null) {
    Log.i(TAG, "CheckInitAudioRecorder call audioRecordSpeaker_.start()+++...");

    audioRecordSpeakerCallback_ = new AudioRecordSpeakerCallbackImpl();
    audioRecordSpeaker_.AddCallback(audioRecordSpeakerCallback_);

    audioRecordSpeaker_.Start(mMediaProjection, 44100, 1);
    Log.i(TAG, "CheckInitAudioRecorder call audioRecordSpeaker_.start()---...");
  }
}

class AudioRecordSpeakerCallbackImpl implements AudioRecordSpeakerCallback {
  @Override
  public void onAudioRecordSpeakerFrame(ByteBuffer data, int size, int sampleRate, int channel, int per_channel_sample_number) {

    //Log.i(TAG, "onAudioRecordSpeakerFrame size=" + size + " sampleRate=" + sampleRate + " channel=" + channel
    //        + " per_channel_sample_number=" + per_channel_sample_number);

    if ( publisherHandle != 0 && (isRecording || isPushingRtmp || isRTSPPublisherRunning) )
    {
      libPublisher.SmartPublisherOnMixPCMData(publisherHandle, 1, data, 0, size, sampleRate, channel, per_channel_sample_number);
    }
  }
}

大家注意到,我们数据投递,用的是SmartPublisherOnMixPCMData()这个接口,为什么这么做呢?我们考虑到,在做无纸化同屏或者教育投屏的时候,一般来说,主要还是采集麦克风音频为主,中间如果有视频播放或者类似需求的时候,我们把内录audio的打开即可(也可以做混音模式,或者推送过程中,实时静音麦克风或扬声器数据源,当然也可以实时调节二者的音量),具体在初始化的时候,可以做下设置:

//audio mix模式下, 如果需要切换麦克风和扬声器数据源,针对麦克风或扬声器实时静音即可
//混音模式下,也可以针对麦克风或扬声器,做实时音量调节
boolean is_audio_mix = true;   //是否混音
libPublisher.SmartPublisherSetAudioMix(publisherHandle, is_audio_mix?1:0);

if(is_audio_mix)
{
  int index = 0;  //0: 麦克风音量调节 1: 扬声器音量调节
  libPublisher.SmartPublisherSetInputAudioVolume(publisherHandle, index, 0.0f);
}

无图无真相,Android平台RTMP推送端或者轻量级RTSP服务测,采集到屏幕画面和扬声器声音,打包传输,RTMP或RTSP播放端截到的同屏画面如下(本来想放一段视频的,没找到传视频的地方):


总结

低版本的Android系统,方案1应该是相对可行但局限很大的选择,方案2大多时候,非定制设备,很难满足权限要求,方案3对Android系统版本要求比较高。

通过测试,方案3除了对Android版本要求比较高外,体验式最好的,感兴趣的开发者,可以尝试看看,如果是特定场景下,本身选用的设备,Android的版本就比较高,又有内录audio需求的话,无疑是非常不错的选择。

 

有关Android平台实现系统内录(捕获播放的音频)并推送RTMP服务技术方案探究的更多相关文章

  1. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  2. 电脑0x0000001A蓝屏错误怎么U盘重装系统教学 - 2

      电脑0x0000001A蓝屏错误怎么U盘重装系统教学分享。有用户电脑开机之后遇到了系统蓝屏的情况。系统蓝屏问题很多时候都是系统bug,只有通过重装系统来进行解决。那么蓝屏问题如何通过U盘重装新系统来解决呢?来看看以下的详细操作方法教学吧。  准备工作:  1、U盘一个(尽量使用8G以上的U盘)。  2、一台正常联网可使用的电脑。  3、ghost或ISO系统镜像文件(Win10系统下载_Win10专业版_windows10正式版下载-系统之家)。  4、在本页面下载U盘启动盘制作工具:系统之家U盘启动工具。  U盘启动盘制作步骤:  注意:制作期间,U盘会被格式化,因此U盘中的重要文件请注

  3. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  4. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  5. Unity 热更新技术 | (三) Lua语言基本介绍及下载安装 - 2

    ?博客主页:https://xiaoy.blog.csdn.net?本文由呆呆敲代码的小Y原创,首发于CSDN??学习专栏推荐:Unity系统学习专栏?游戏制作专栏推荐:游戏制作?Unity实战100例专栏推荐:Unity实战100例教程?欢迎点赞?收藏⭐留言?如有错误敬请指正!?未来很长,值得我们全力奔赴更美好的生活✨------------------❤️分割线❤️-------------------------

  6. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  7. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  8. kvm虚拟机安装centos7基于ubuntu20.04系统 - 2

    需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc

  9. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  10. ruby - 如何让Ruby捕获线程中的语法错误 - 2

    我正在尝试使用ruby​​编写一个双线程客户端,一个线程从套接字读取数据并将其打印出来,另一个线程读取本地数据并将其发送到远程服务器。我发现的问题是Ruby似乎无法捕获线程内的错误,这是一个示例:#!/usr/bin/rubyThread.new{loop{$stdout.puts"hi"abc.putsefsleep1}}loop{sleep1}显然,如果我在线程外键入abc.putsef,代码将永远不会运行,因为Ruby将报告“undefinedvariableabc”。但是,如果它在一个线程内,则没有错误报告。我的问题是,如何让Ruby捕获这样的错误?或者至少,报告线程中的错误?

随机推荐