Android平台基于RTMP或RTSP的一对一音视频互动技术方案探讨

简介: 随着智能门禁等物联网产品的普及,越来越多的开发者对音视频互动体验提出了更高的要求。目前市面上大多一对一互动都是基于WebRTC,优点不再赘述,我们这里先说说可能需要面临的问题:WebRTC的服务器部署非常复杂,可以私有部署,但是非常复杂。传输基于UDP,很难保证传输质量,由于UDP是不可靠的传输协议,在复杂的公网网络环境下,各种突发流量、偶尔的传输错误、网络抖动、超时等等都会引起丢包异常,都会在一定程度上影响音视频通信的质量,难以应对复杂的互联网环境,如跨区跨运营商、低带宽、高丢包等场景,行话说的好:从demo到实用,中间还差1万个WebRTC。

背景

随着智能门禁等物联网产品的普及,越来越多的开发者对音视频互动体验提出了更高的要求。目前市面上大多一对一互动都是基于WebRTC,优点不再赘述,我们这里先说说可能需要面临的问题:WebRTC的服务器部署非常复杂,可以私有部署,但是非常复杂。传输基于UDP,很难保证传输质量,由于UDP是不可靠的传输协议,在复杂的公网网络环境下,各种突发流量、偶尔的传输错误、网络抖动、超时等等都会引起丢包异常,都会在一定程度上影响音视频通信的质量,难以应对复杂的互联网环境,如跨区跨运营商、低带宽、高丢包等场景,行话说的好:从demo到实用,中间还差1万个WebRTC。

其他技术方案

  1. 内网环境下的RTSP轻量级服务;
  2. 基于RTMP的公网或内网技术方案。


本方案系基于现有RTMP或内置RTSP服务、RTMP/RTSP直播播放模块,产品稳定度高,在保证超低延迟的基础上,加入噪音抑制、回音消除、自动增益控制等特性,确保通话体验(如需更好的消除效果,亦可考虑如麦克风阵列等技术方案),采用通用的RTMP服务器(如nginx、SRS)或自身的轻量级RTSP服务,更有利于私有部署,便于支持H.264的扩展SEI消息发送机制,方便扩展特定机型H.265编码支持。

技术实现

废话不多说,先上图:

e15e0116e10045dc998c32ccd3e6c431.jpg

关键demo代码说明:


拉流播放:

        btnPlaybackStartStopPlayback.setOnClickListener(new Button.OnClickListener() 
        {  
            //  @Override  
              public void onClick(View v) {  
                if(isPlaybackViewStarted)
                {
                    Log.i(PLAY_TAG, "Stop playback stream++");
                  btnPlaybackStartStopPlayback.setText("开始播放 ");
                  //btnPopInputText.setEnabled(true);
                  btnPlaybackPopInputUrl.setEnabled(true);
                  btnPlaybackHardwareDecoder.setEnabled(true);
                  btnPlaybackSetPlayBuffer.setEnabled(true);
                      btnPlaybackFastStartup.setEnabled(true);
            if ( playerHandle != 0 )
            {
              libPlayer.SmartPlayerStopPlay(playerHandle);
              libPlayer.SmartPlayerClose(playerHandle);
              playerHandle = 0;
            }
                  isPlaybackViewStarted = false;
                      Log.i(PLAY_TAG, "Stop playback stream--");
                }
                else
                {
                  Log.i(PLAY_TAG, "Start playback stream++");
                  playerHandle = libPlayer.SmartPlayerOpen(curContext);
                    if(playerHandle == 0)
                    {
                      Log.e(PLAY_TAG, "surfaceHandle with nil..");
                      return;
                    }
            libPlayer.SetSmartPlayerEventCallbackV2(playerHandle,
                new EventHandePlayerV2());
                    libPlayer.SmartPlayerSetSurface(playerHandle, playerSurfaceView);   //if set the second param with null, it means it will playback audio only..
                    // libPlayer.SmartPlayerSetSurface(playerHandle, null); 
            libPlayer.SmartPlayerSetRenderScaleMode(playerHandle, 1);
                    // External Render test
                    //libPlayer.SmartPlayerSetExternalRender(playerHandle, new RGBAExternalRender());
                    //libPlayer.SmartPlayerSetExternalRender(playerHandle, new I420ExternalRender());
                    libPlayer.SmartPlayerSetExternalAudioOutput(playerHandle, new PlayerExternalPcmOutput());
                    libPlayer.SmartPlayerSetAudioOutputType(playerHandle, 1);
                    libPlayer.SmartPlayerSetBuffer(playerHandle, playbackBuffer);
                    libPlayer.SmartPlayerSetFastStartup(playerHandle, isPlaybackFastStartup?1:0);
                    if ( isPlaybackMute )
                    {
                      libPlayer.SmartPlayerSetMute(playerHandle, isPlaybackMute?1:0);
                    }
            if (isPlaybackHardwareDecoder) {
              int isSupportHevcHwDecoder = libPlayer.SetSmartPlayerVideoHevcHWDecoder(playerHandle,1);
              int isSupportH264HwDecoder = libPlayer
                  .SetSmartPlayerVideoHWDecoder(playerHandle,1);
              Log.i(TAG, "isSupportH264HwDecoder: " + isSupportH264HwDecoder + ", isSupportHevcHwDecoder: " + isSupportHevcHwDecoder);
            }
                    if( playbackUrl == null )
                    {
                     Log.e(PLAY_TAG, "playback URL with NULL..."); 
                     return;
                    }
                    libPlayer.SmartPlayerSetAudioVolume(playerHandle, curAudioVolume);
                    libPlayer.SmartPlayerSetUrl(playerHandle, playbackUrl);
                    int iPlaybackRet = libPlayer.SmartPlayerStartPlay(playerHandle);
                    if( iPlaybackRet != 0 )
                    {
              libPlayer.SmartPlayerClose(playerHandle);
              playerHandle = 0;
                     Log.e(PLAY_TAG, "StartPlayback strem failed.."); 
                     return;
                    }
                btnPlaybackStartStopPlayback.setText("停止播放 ");
                btnPlaybackPopInputUrl.setEnabled(false);
                    btnPlaybackHardwareDecoder.setEnabled(false);
                    btnPlaybackSetPlayBuffer.setEnabled(false);
                      btnPlaybackFastStartup.setEnabled(false);
                    isPlaybackViewStarted = true;
                    Log.i(PLAY_TAG, "Start playback stream--");
              }
              }
        });

拉流端实时音量调节:

    audioVolumeBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
      @Override
      public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
      }
      @Override
      public void onStartTrackingTouch(SeekBar seekBar) {
        //Log.i(TAG, "开始拖动");
      }
      @Override
      public void onStopTrackingTouch(SeekBar seekBar) {
        Log.i(TAG, "停止拖动, CurProgress: " + seekBar.getProgress());
        curAudioVolume = seekBar.getProgress();
        audioVolumeText.setText("当前音量: " + curAudioVolume);
        if(playerHandle != 0)
        {
          libPlayer.SmartPlayerSetAudioVolume(playerHandle, curAudioVolume);
        }
      }
    });
  }

回调后的PCM数据,传给推送端,用于音频处理

  class NTAudioRecordV2CallbackImpl implements NTAudioRecordV2Callback
  {
    @Override
    public void onNTAudioRecordV2Frame(ByteBuffer data, int size, int sampleRate, int channel, int per_channel_sample_number)
    {
         /*
         Log.i(TAG, "onNTAudioRecordV2Frame size=" + size + " sampleRate=" + sampleRate + " channel=" + channel
             + " per_channel_sample_number=" + per_channel_sample_number);
         */
      if ( (isPushingRtmp || isRTSPPublisherRunning) && publisherHandle != 0 )
      {
        libPublisher.SmartPublisherOnPCMData(publisherHandle, data, size, sampleRate, channel, per_channel_sample_number);
      }
    }
  }
    class PlayerExternalPcmOutput implements NTExternalAudioOutput
    {     
      private int sample_rate_ = 0;
      private int channel_ = 0;
      private int sample_size = 0;
      private int buffer_size = 0;
      private ByteBuffer pcm_buffer_ = null;
      @Override
      public ByteBuffer getPcmByteBuffer(int size)
      {
        //Log.i("getPcmByteBuffer", "size: " + size);
        if(size < 1)
        {
          return null;
        }
        if(buffer_size != size)
        {
          buffer_size = size;
            pcm_buffer_ = ByteBuffer.allocateDirect(buffer_size);
        }
        return pcm_buffer_;
      }
      public void onGetPcmFrame(int ret, int sampleRate, int channel, int sampleSize, int is_low_latency)
      {
        /*Log.i("onGetPcmFrame", "ret: " + ret + ", sampleRate: " + sampleRate + ", channel: " + channel + ", sampleSize: " + sampleSize +
            ",is_low_latency:" + is_low_latency + " buffer_size:" + buffer_size);*/
        if ( pcm_buffer_ == null)
          return;
        pcm_buffer_.rewind();
        if ( ret == 0 && (isPushingRtmp || isRTSPPublisherRunning))
        {
          libPublisher.SmartPublisherOnFarEndPCMData(publisherHandle, pcm_buffer_, sampleRate, channel, sampleSize, is_low_latency);
          if (is_audio_mix_)
          {
            libPublisher.SmartPublisherOnMixPCMData(publisherHandle, 1, pcm_buffer_, 0, buffer_size, sampleRate, channel, sampleSize);
          }
          /*
          java.nio.ByteOrder old_order = pcm_buffer_.order();
          pcm_buffer_.order(java.nio.ByteOrder.nativeOrder());
          java.nio.ShortBuffer short_buffer = pcm_buffer_.asShortBuffer();
          pcm_buffer_.order(old_order);
          short[] short_array =  new short[short_buffer.remaining()];
          short_buffer.get(short_array);
          libPublisher.SmartPublisherOnMixPCMShortArray(publisherHandle, 1, short_array, 0, short_array.length, sampleRate, channel, sampleSize);
          */
        }
        // test
        /*
        byte[] test_buffer = new byte[16];
        pcm_buffer_.get(test_buffer);
        Log.i(TAG, "onGetPcmFrame data:" + bytesToHexString(test_buffer));
        */
      }
    }

推送端:


RTMP推送:

    class ButtonPushStartListener implements OnClickListener
    {
        public void onClick(View v)
        {    
          if (isPushingRtmp)
          {
            stopPush();
        if (!isRTSPPublisherRunning) {
          ConfigControlEnable(true);
        }
        btnPushStartStop.setText("推送RTMP");
        isPushingRtmp = false;
        return;
          }
      Log.i(PUSH_TAG, "onClick start push rtmp..");
      if (libPublisher == null)
        return;
      if (!isRTSPPublisherRunning) {
        InitPusherAndSetConfig();
      }
      if ( inputPushURL != null && inputPushURL.length() > 1 )
      {
        publishURL = inputPushURL;
        Log.i(PUSH_TAG, "start, input publish url:" + publishURL);
      }
      else
      {
        publishURL = basePushURL + String.valueOf((int)( System.currentTimeMillis() % 1000000));
        Log.i(PUSH_TAG, "start, generate random url:" + publishURL);
      }
      printPushText = "URL:" + publishURL;
      Log.i(PUSH_TAG, printPushText);
      textPushCurURL = (TextView)findViewById(R.id.txt_push_cur_url);
      textPushCurURL.setText(printPushText);
      Log.i(PUSH_TAG, "videoWidth: "+ pushVideoWidth + " videoHeight: " + pushVideoHeight + " pushType:" + pushType);
      if ( libPublisher.SmartPublisherSetURL(publisherHandle, publishURL) != 0 )
      {
        Log.e(PUSH_TAG, "Failed to set rtmp pusher URL..");
      }
      int startRet = libPublisher.SmartPublisherStartPublisher(publisherHandle);
      if (startRet != 0) {
        isPushingRtmp = false;
        Log.e(TAG, "Failed to start push stream..");
        return;
      }
      if ( !isRTSPPublisherRunning ) {
        if (pushType == 0 || pushType == 1) {
          CheckInitAudioRecorder();    //enable pure video publisher..
        }
        ConfigControlEnable(false);
      }
      btnPushStartStop.setText("停止推送 ");
      isPushingRtmp = true;
        }
    };

轻量级RTSP服务模式:

  //启动/停止RTSP服务
  class ButtonRtspServiceListener implements OnClickListener {
    public void onClick(View v) {
      if (isRTSPServiceRunning) {
        stopRtspService();
        btnRtspService.setText("启动RTSP服务");
        btnRtspPublisher.setEnabled(false);
        isRTSPServiceRunning = false;
        return;
      }
      Log.i(TAG, "onClick start rtsp service..");
      rtsp_handle_ = libPublisher.OpenRtspServer(0);
      if (rtsp_handle_ == 0) {
        Log.e(TAG, "创建rtsp server实例失败! 请检查SDK有效性");
      } else {
        int port = 8554;
        if (libPublisher.SetRtspServerPort(rtsp_handle_, port) != 0) {
          libPublisher.CloseRtspServer(rtsp_handle_);
          rtsp_handle_ = 0;
          Log.e(TAG, "创建rtsp server端口失败! 请检查端口是否重复或者端口不在范围内!");
        }
        //String user_name = "admin";
        //String password = "12345";
        //libPublisher.SetRtspServerUserNamePassword(rtsp_handle_, user_name, password);
        if (libPublisher.StartRtspServer(rtsp_handle_, 0) == 0) {
          Log.i(TAG, "启动rtsp server 成功!");
        } else {
          libPublisher.CloseRtspServer(rtsp_handle_);
          rtsp_handle_ = 0;
          Log.e(TAG, "启动rtsp server失败! 请检查设置的端口是否被占用!");
        }
        btnRtspService.setText("停止RTSP服务");
        btnRtspPublisher.setEnabled(true);
        isRTSPServiceRunning = true;
      }
    }
  }
  //发布/停止RTSP流
  class ButtonRtspPublisherListener implements OnClickListener {
    public void onClick(View v) {
      if (isRTSPPublisherRunning) {
        stopRtspPublisher();
        if (!isPushingRtmp) {
          ConfigControlEnable(true);
        }
        btnRtspPublisher.setText("发布RTSP流");
        btnGetRtspSessionNumbers.setEnabled(false);
        btnRtspService.setEnabled(true);
        isRTSPPublisherRunning = false;
        return;
      }
      Log.i(TAG, "onClick start rtsp publisher..");
      if (!isPushingRtmp) {
        InitPusherAndSetConfig();
      }
      if (publisherHandle == 0) {
        Log.e(TAG, "Start rtsp publisher, publisherHandle is null..");
        return;
      }
      String rtsp_stream_name = "stream1";
      libPublisher.SetRtspStreamName(publisherHandle, rtsp_stream_name);
      libPublisher.ClearRtspStreamServer(publisherHandle);
      libPublisher.AddRtspStreamServer(publisherHandle, rtsp_handle_, 0);
      if (libPublisher.StartRtspStream(publisherHandle, 0) != 0) {
        Log.e(TAG, "调用发布rtsp流接口失败!");
        return;
      }
      if (!isPushingRtmp) {
        if (pushType == 0 || pushType == 1) {
          CheckInitAudioRecorder();    //enable pure video publisher..
        }
        ConfigControlEnable(false);
      }
      btnRtspPublisher.setText("停止RTSP流");
      btnGetRtspSessionNumbers.setEnabled(true);
      btnRtspService.setEnabled(false);
      isRTSPPublisherRunning = true;
    }
  }
  ;

RTMP推送和轻量级RTSP服务,可以在一个实例里面处理,所以推送参数的初始化,只需要调用一次即可。

  private void InitPusherAndSetConfig() {
    Log.i(TAG, "videoWidth: " + pushVideoWidth + " videoHeight: " + pushVideoHeight
        + " pushType:" + pushType);
    int audio_opt = 1;
    int video_opt = 1;
    if ( pushType == 1 )
    {
      video_opt = 0;
    }
    else if (pushType == 2 )
    {
      audio_opt = 0;
    }
    publisherHandle = libPublisher.SmartPublisherOpen(curContext, audio_opt, video_opt,
        pushVideoWidth, pushVideoHeight);
    if ( publisherHandle == 0  )
    {
      return;
    }
    if(videoEncodeType == 1)
    {
      int h264HWKbps = setHardwareEncoderKbps(true, pushVideoWidth,
          pushVideoHeight);
      Log.i(TAG, "h264HWKbps: " + h264HWKbps);
      int isSupportH264HWEncoder = libPublisher
          .SetSmartPublisherVideoHWEncoder(publisherHandle, h264HWKbps);
      if (isSupportH264HWEncoder == 0) {
        Log.i(TAG, "Great, it supports h.264 hardware encoder!");
      }
    }
    else if (videoEncodeType == 2)
    {
      int hevcHWKbps = setHardwareEncoderKbps(false, pushVideoWidth,
          pushVideoHeight);
      Log.i(TAG, "hevcHWKbps: " + hevcHWKbps);
      int isSupportHevcHWEncoder = libPublisher
          .SetSmartPublisherVideoHevcHWEncoder(publisherHandle, hevcHWKbps);
      if (isSupportHevcHWEncoder == 0) {
        Log.i(TAG, "Great, it supports hevc hardware encoder!");
      }
    }
    if(is_sw_vbr_mode)
    {
      int is_enable_vbr = 1;
      int video_quality = CalVideoQuality(pushVideoWidth,
          pushVideoHeight, true);
      int vbr_max_bitrate = CalVbrMaxKBitRate(pushVideoWidth,
          pushVideoHeight);
      libPublisher.SmartPublisherSetSwVBRMode(publisherHandle, is_enable_vbr, video_quality, vbr_max_bitrate);
    }
    libPublisher.SetSmartPublisherEventCallbackV2(publisherHandle, new EventHandePublisherV2());
    //如果想和时间显示在同一行,请去掉'\n'
    String watermarkText = "大牛直播(daniulive)\n\n";
    String path = pushLogoPath;
    if( pushWatemarkType == 0 )
    {
      if ( isPushWritelogoFileSuccess )
        libPublisher.SmartPublisherSetPictureWatermark(publisherHandle, path, WATERMARK.WATERMARK_POSITION_TOPRIGHT, 160, 160, 10, 10);
    }
    else if( pushWatemarkType == 1 )
    {
      if ( isPushWritelogoFileSuccess )
        libPublisher.SmartPublisherSetPictureWatermark(publisherHandle, path, WATERMARK.WATERMARK_POSITION_TOPRIGHT, 160, 160, 10, 10);
      libPublisher.SmartPublisherSetTextWatermark(publisherHandle, watermarkText, 1, WATERMARK.WATERMARK_FONTSIZE_BIG, WATERMARK.WATERMARK_POSITION_BOTTOMRIGHT, 10, 10);
      //libPublisher.SmartPublisherSetTextWatermarkFontFileName("/system/fonts/DroidSansFallback.ttf");
      //libPublisher.SmartPublisherSetTextWatermarkFontFileName("/sdcard/DroidSansFallback.ttf");
    }
    else if(pushWatemarkType == 2)
    {
      libPublisher.SmartPublisherSetTextWatermark(publisherHandle, watermarkText, 1, WATERMARK.WATERMARK_FONTSIZE_BIG, WATERMARK.WATERMARK_POSITION_BOTTOMRIGHT, 10, 10);
      //libPublisher.SmartPublisherSetTextWatermarkFontFileName("/system/fonts/DroidSansFallback.ttf");
    }
    else
    {
      Log.i(TAG, "no watermark settings..");
    }
    //end
    if ( !is_push_speex )
    {
      // set AAC encoder
      libPublisher.SmartPublisherSetAudioCodecType(publisherHandle, 1);
    }
    else
    {
      // set Speex encoder
      libPublisher.SmartPublisherSetAudioCodecType(publisherHandle, 2);
      libPublisher.SmartPublisherSetSpeexEncoderQuality(publisherHandle, 8);
    }
    libPublisher.SmartPublisherSetNoiseSuppression(publisherHandle, is_push_noise_suppression?1:0);
    libPublisher.SmartPublisherSetAGC(publisherHandle, is_push_agc?1:0);
    libPublisher.SmartPublisherSetEchoCancellation(publisherHandle, 1, echoCancelDelay);
    libPublisher.SmartPublisherSetAudioMix(publisherHandle, is_audio_mix_?1:0);
    libPublisher.SmartPublisherSetInputAudioVolume(publisherHandle, 0 , mic_audio_volume_);
    if ( is_audio_mix_ )
    {
      libPublisher.SmartPublisherSetInputAudioVolume(publisherHandle, 1 , mix_audio_volume_);
    }
    libPublisher.SmartPublisherSetClippingMode(publisherHandle, 0);
    libPublisher.SmartPublisherSetSWVideoEncoderProfile(publisherHandle, push_sw_video_encoder_profile);
    //libPublisher.SetRtmpPublishingType(0);
    //libPublisher.SmartPublisherSetGopInterval(publisherHandle, 18*3);
    //libPublisher.SmartPublisherSetFPS(publisherHandle, 18);
    libPublisher.SmartPublisherSetSWVideoEncoderSpeed(publisherHandle, sw_video_encoder_speed);
    //libPublisher.SmartPublisherSetSWVideoBitRate(600, 1200);
  }

相关封装:

  //停止rtmp推送
  private void stopPush() {
    if(!isPushingRtmp)
    {
      return;
    }
    if ( !isRTSPPublisherRunning) {
      if (audioRecord_ != null) {
        Log.i(TAG, "stopPush, call audioRecord_.StopRecording..");
        audioRecord_.Stop();
        if (audioRecordCallback_ != null) {
          audioRecord_.RemoveCallback(audioRecordCallback_);
          audioRecordCallback_ = null;
        }
        audioRecord_ = null;
      }
    }
    if (libPublisher != null) {
      libPublisher.SmartPublisherStopPublisher(publisherHandle);
    }
    if (!isRTSPPublisherRunning) {
      if (publisherHandle != 0) {
        if (libPublisher != null) {
          libPublisher.SmartPublisherClose(publisherHandle);
          publisherHandle = 0;
        }
      }
    }
  }
  //停止发布RTSP流
  private void stopRtspPublisher() {
    if(!isRTSPPublisherRunning)
    {
      return;
    }
    if (!isPushingRtmp) {
      if (audioRecord_ != null) {
        Log.i(TAG, "stopRtspPublisher, call audioRecord_.StopRecording..");
        audioRecord_.Stop();
        if (audioRecordCallback_ != null) {
          audioRecord_.RemoveCallback(audioRecordCallback_);
          audioRecordCallback_ = null;
        }
        audioRecord_ = null;
      }
    }
    if (libPublisher != null) {
      libPublisher.StopRtspStream(publisherHandle);
    }
    if (!isPushingRtmp) {
      if (publisherHandle != 0) {
        if (libPublisher != null) {
          libPublisher.SmartPublisherClose(publisherHandle);
          publisherHandle = 0;
        }
      }
    }
  }
  //停止RTSP服务
  private void stopRtspService() {
    if(!isRTSPServiceRunning)
    {
      return;
    }
    if (libPublisher != null && rtsp_handle_ != 0) {
      libPublisher.StopRtspServer(rtsp_handle_);
      libPublisher.CloseRtspServer(rtsp_handle_);
      rtsp_handle_ = 0;
    }
  }

传递采集到的视频数据,摄像头数据采集,也可选用camera2的接口,对焦和体验更好:

  @Override
  public void onPreviewFrame(byte[] data, Camera camera) {
    pushFrameCount++;
    if ( pushFrameCount % 3000 == 0 )
    {
      Log.i("OnPre", "gc+");
      System.gc();
      Log.i("OnPre", "gc-");
    }
    if (data == null) {
      Parameters params = camera.getParameters();
      Size size = params.getPreviewSize();
      int bufferSize = (((size.width|0x1f)+1) * size.height * ImageFormat.getBitsPerPixel(params.getPreviewFormat())) / 8;
      camera.addCallbackBuffer(new byte[bufferSize]);
    } 
    else 
    {
      if(isPushingRtmp || isRTSPPublisherRunning)
      {
        libPublisher.SmartPublisherOnCaptureVideoData(publisherHandle, data, data.length, pushCurrentCameraType, currentPushOrigentation);
      }
      camera.addCallbackBuffer(data);
    }
  } 

如果内网环境下,用轻量级RTSP服务的话,需判断对方有没有播放自己的流数据的话,可以通过获取RTSP会话数来判断是否链接。

  //当前RTSP会话数弹出框
  private void PopRtspSessionNumberDialog(int session_numbers) {
    final EditText inputUrlTxt = new EditText(this);
    inputUrlTxt.setFocusable(true);
    inputUrlTxt.setEnabled(false);
    String session_numbers_tag = "RTSP服务当前客户会话数: " + session_numbers;
    inputUrlTxt.setText(session_numbers_tag);
    AlertDialog.Builder builderUrl = new AlertDialog.Builder(this);
    builderUrl
        .setTitle("内置RTSP服务")
        .setView(inputUrlTxt).setNegativeButton("确定", null);
    builderUrl.show();
  }
  //获取RTSP会话数
  class ButtonGetRtspSessionNumbersListener implements OnClickListener {
    public void onClick(View v) {
      if (libPublisher != null && rtsp_handle_ != 0) {
        int session_numbers = libPublisher.GetRtspServerClientSessionNumbers(rtsp_handle_);
        Log.i(TAG, "GetRtspSessionNumbers: " + session_numbers);
        PopRtspSessionNumberDialog(session_numbers);
      }
    }
  };

总结

Android平台的一对一互动,除了WebRTC外,在保证低延迟的前提下,RTMP或RTSP技术方案也是非常不错的选择。

相关文章
|
4月前
|
Java Android开发 Swift
安卓与iOS开发对比:平台选择对项目成功的影响
【10月更文挑战第4天】在移动应用开发的世界中,选择合适的平台是至关重要的。本文将深入探讨安卓和iOS两大主流平台的开发环境、用户基础、市场份额和开发成本等方面的差异,并分析这些差异如何影响项目的最终成果。通过比较这两个平台的优势与挑战,开发者可以更好地决定哪个平台更适合他们的项目需求。
139 1
|
2月前
|
IDE 开发工具 Android开发
移动应用开发之旅:探索Android和iOS平台
在这篇文章中,我们将深入探讨移动应用开发的两个主要平台——Android和iOS。我们将了解它们的操作系统、开发环境和工具,并通过代码示例展示如何在这两个平台上创建一个简单的“Hello World”应用。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和技巧,帮助你更好地理解和掌握移动应用开发。
81 17
|
4月前
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
135 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
Android开发
Android RTMP直播推流方案选择
1. 技术科普: RTMP是Real Time Messaging Protocol(实时消息传输协议)的首字母缩写。
2541 0
|
3月前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台策略
在移动应用开发的战场上,安卓和iOS两大阵营各据一方。随着技术的演进,跨平台开发框架成为开发者的新宠,旨在实现一次编码、多平台部署的梦想。本文将探讨跨平台开发的优势与挑战,并分享实用的开发技巧,帮助开发者在安卓和iOS的世界中游刃有余。
|
12天前
|
缓存 前端开发 Android开发
【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
|
16天前
|
Dart 前端开发 Android开发
【02】写一个注册页面以及配置打包选项打包安卓apk测试—开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【02】写一个注册页面以及配置打包选项打包安卓apk测试—开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【02】写一个注册页面以及配置打包选项打包安卓apk测试—开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
2月前
|
搜索推荐 前端开发 API
探索安卓开发中的自定义视图:打造个性化用户界面
在安卓应用开发的广阔天地中,自定义视图是一块神奇的画布,让开发者能够突破标准控件的限制,绘制出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战技巧,逐步揭示如何在安卓平台上创建和运用自定义视图来提升用户体验。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开新的视野,让你的应用在众多同质化产品中脱颖而出。
72 19
|
3月前
|
IDE Java 开发工具
移动应用与系统:探索Android开发之旅
在这篇文章中,我们将深入探讨Android开发的各个方面,从基础知识到高级技术。我们将通过代码示例和案例分析,帮助读者更好地理解和掌握Android开发。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和技巧。让我们一起开启Android开发的旅程吧!
|
2月前
|
JSON Java API
探索安卓开发:打造你的首个天气应用
在这篇技术指南中,我们将一起潜入安卓开发的海洋,学习如何从零开始构建一个简单的天气应用。通过这个实践项目,你将掌握安卓开发的核心概念、界面设计、网络编程以及数据解析等技能。无论你是初学者还是有一定基础的开发者,这篇文章都将为你提供一个清晰的路线图和实用的代码示例,帮助你在安卓开发的道路上迈出坚实的一步。让我们一起开始这段旅程,打造属于你自己的第一个安卓应用吧!
82 14

热门文章

最新文章