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

Posted 音视频牛哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android平台实现系统内录(捕获播放的音频)并推送RTMP服务技术方案探究相关的知识,希望对你有一定的参考价值。

几年来,我们在做无纸化同屏或在线教育相关场景的时候,总是被一件事情困扰:如何实现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 Q及以上系统音频捕获功能(声音内录)的简单实现

前言

现在越来越多的视频类APP,如抖音、快手、B站等等,都开放了音频捕获配置,也就是android:allowAudioPlaybackCapture="true"。因此学习如何捕获音频实现声音内录,是很有必要的。

第一步:设置allowAudioPlaybackCapture

很简单,在项目AndroidManifest.xml的application中增加一句android:allowAudioPlaybackCapture="true"

第二步:配置Service

        <service
            android:name=".RecordService"
            android:exported="true"
            android:foregroundServiceType="mediaProjection"
            android:enabled="true"/>

注意:本文只适用在Android Q及以上版本,因此必须使用前台服务方式startForegroundService()来启动服务。

请在Service的onCreate()中实现前台服务通知。

第三步:开始使用录制或投射内容

1.向用户询问是否开始使用录制或投射内容,注意:在Activity或Fragment中询问。

    if (currentResultCode != Activity.RESULT_OK || resultData == null) 
        MediaProjectionManager mediaProjectionManager
                = (MediaProjectionManager) this.getSystemService(MEDIA_PROJECTION_SERVICE);
        Intent screenCaptureIntent = mediaProjectionManager.createScreenCaptureIntent();
        startActivityForResult(screenCaptureIntent, REQUEST_SCREEN_CAPTURE_CODE);
        return;
    

2.在onActivityResult()中获取currentResultCode和resultData

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) 
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_SCREEN_CAPTURE_CODE) 
            currentResultCode = resultCode;
            resultData = data;
        
    

3.将currentResultCode和resultData传递给Service

    Intent intent = new Intent(this, RecordService.class);
    intent.putExtra("resultData", resultData);
    intent.putExtra("resultCode", currentResultCode);
    startForegroundService(intent);

4.在Service的onStartCommand()中接收currentResultCode和resultData,获取MediaProjection实例(实例化要在前台Service中进行,这就是为什么要用Service的原因),并配置和启动AudioRecord

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) 
        int currentResultCode = intent.getIntExtra("resultCode", 0);
        Intent resultData = intent.getParcelableExtra("resultData");
        minBufferSize = AudioRecord.getMinBufferSize(16000,
                AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
        MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getBaseContext()
                .getSystemService(MEDIA_PROJECTION_SERVICE);
        MediaProjection mediaProjection = mediaProjectionManager.getMediaProjection(currentResultCode,
                Objects.requireNonNull(resultData));
        AudioRecord.Builder builder = new AudioRecord.Builder();
        builder.setAudioFormat(new AudioFormat.Builder()
                .setSampleRate(16000)
                .setChannelMask(AudioFormat.CHANNEL_IN_MONO)
                .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                .build())
                .setBufferSizeInBytes(minBufferSize);
        AudioPlaybackCaptureConfiguration config =
                new AudioPlaybackCaptureConfiguration.Builder(mediaProjection)
                        .addMatchingUsage(AudioAttributes.USAGE_MEDIA)
                        .addMatchingUsage(AudioAttributes.USAGE_UNKNOWN)
                        .addMatchingUsage(AudioAttributes.USAGE_GAME)
                        .build();
        builder.setAudioPlaybackCaptureConfig(config);
        try 
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
                    == PackageManager.PERMISSION_GRANTED) 
                audioRecord = builder.build();
            
         catch (Exception e) 
            Log.e("录音器错误", "录音器初始化失败");
        
        startRecord();
        return super.onStartCommand(intent, flags, startId);
    

demo下载地址:https://download.csdn.net/download/sinat_39508948/86955519

完毕

以上是关于Android平台实现系统内录(捕获播放的音频)并推送RTMP服务技术方案探究的主要内容,如果未能解决你的问题,请参考以下文章

Android 音频播放

捕获音频/视频后无法播放系统声音

请问,如何用Audacity软件录制正在电脑上播放的音频文件,谢谢。

使用 naudio 捕获音频并使用 javascript 播放

Android平台中关于音频播放

在 android 设备上捕获视频并在 iOS 设备上播放