Linux ( Java )

DingRTC的基本功能包含初始化SDK、加入频道、本地发布、订阅远端和离开频道等。通过阅读本文,您可以了解DingRTC的基本功能。

操作步骤

说明 本文中的实现方法仅供参考,您可以根据实际业务需求进行开发。
  1. 初始化SDK。

    您需要创建RtcEngine实例,并注册回调。

    // 设置日志路径
    DingRtcEngine.setLogDirPath(logDir);
    
    // 实现并创建listener
    // 可以按需实现关注的回调,例如:
    // 入离会、推拉流是否成功可以根据对应回调判断
    class MyListener extends DingRtcEngineListener {
      ....
    }
    MyListener listener = new MyListener();
    
    // 创建engine
    DingRtcEngine engine = DingRtcEngine.createInstance(listener, "");
    
    // 关闭订阅(无订阅需求时,减少资源消耗)
    engine.subscribeAllRemoteAudioStreams(false);
    engine.subscribeAllRemoteVideoStreams(false);
    
    // 开启推音视频流
    // note:也可以在入会后调用
    engine.publishLocalAudioStream(true);
    engine.publishLocalVideoStream(true);
  2. 加入频道。

    入会需获取鉴权Token,通常由业务方服务端生成,具体方法请参考“使用Token鉴权

    public static class DingRtcAuthInfo {
        /*! 频道ID。 */
        public String channelId;
        /*! 用户ID。 */
        public String userId;
        /*! 应用ID。 */
        public String appId;
        /*! 令牌。 */
        public String token;
        /*! GSLB地址(建议传空串 - "")。 */
        public String gslb;
    }
    
    // 获取鉴权信息,getFromAppServer由客户侧实现
    DingRtcEngine.DingRtcAuthInfo authInfo = getAuthInfoFromAppServer();
    
    // 加入频道
    // 入会是否成功参见 listener 的 onJoinChannelResult 回调
    engine.joinChannel(authInfo, "张三");

    参数

    描述

    appId

    应用ID,在控制台应用管理页面创建和查看。

    channelId

    频道ID。1~64位,由大小写字母、数字、下划线(_)、短划线(-)组成。

    userId

    用户ID。1~64位,由大小写字母、数字、下划线(_)、短划线(-)组成。

    说明

    同一个用户ID在其他端登录,先入会的端会被后入会的端踢出频道。

    token

    频道鉴权令牌。

    gslbServer

    服务地址,可为空,默认值为:"https://gslb.dingrtc.com",请您通过业务服务器下发到客户端SDK,不建议您将该地址固化在客户端代码。

  3. 发布本地音频流。

    入会之后,SDK默认不推流,需主动调用API开启推流(可在入会前调用)。

    • 开启音频推流

      // 开启推音频,前面调用过可不用再调用(支持入会前调用)
      engine.publishLocalAudioStream(true);
    • 外部音频推流

      • 如果是Linux服务器或没有麦克风声音采集设备,可使用外部音频源接口来推送音频数据。

      // 设置推外部音频文件和音频参数
      // pcmSampleRate:音频文件采样率,例如:48000(48k采样率)
      // pcmChannels:   音频文件通道数,例如:1或者2 (单声道或双声道)
      engine.setExternalAudioSource(true, pcmSampleRate, pcmChannels);
      
      // 生成Frame数据
      DingRtcEngine.DingRtcAudioFrame frame = new DingRtcEngine.DingRtcAudioFrame();
      frame.bytesPerSample = 2;
      frame.samplesPerSec = pcmSampleRate;
      frame.numChannels = pcmChannels;
      frame.buffer = buffer;
      frame.numSamples = numChannels;
      frame.timestamp = timestamp;
      
      // 推送Frame数据
      engine.pushExternalAudioFrame(frame);
      • 通常需要一个单独的线程来循环推送音频数据,同时控制数据保持一定的时间间隔均匀输入给SDK。可类比为麦克风设备,按固定周期采集音频数据输入给SDK。

      static class RtcContext {
        // audio config
        String pcmFilePath;
        int pcmSampleRate = 16000;
        int pcmChannels = 1;
        int pcmReadFreq = 40;
        volatile boolean isPushAudioStarted = false;
        volatile boolean externalAudioFinished = false;
        Thread externalAudioThread;
      }
      
      private static void startPushAudio(RtcContext context) {
        if (context.isPushAudioStarted) {
            return;
        }
        context.isPushAudioStarted = true;
        if (context.engine != null) {
            // 开启推流
            context.engine.publishLocalAudioStream(true);
            // 设置音频PCM推流
            context.engine.setExternalAudioSource(true, context.pcmSampleRate, context.pcmChannels);
        }
        context.externalAudioThread = new Thread(() -> {
            DingRtcEngine.DingRtcAudioFrame frame = new DingRtcEngine.DingRtcAudioFrame();
            try (RandomAccessFile file = new RandomAccessFile(context.pcmFilePath, "r")) {
                long sampleCount = 0;
                // 16-bit pcm, 2byte
                int bytesPerSample = 2;
                int samplesToRead = context.pcmSampleRate / (1000 / context.pcmReadFreq);
                int bufferSize = samplesToRead * context.pcmChannels * bytesPerSample;
                byte[] buffer = new byte[bufferSize];
                long delay = 0;
                long startClock = System.currentTimeMillis();
                long lastStatsClock = startClock;
                while (!context.quit) {
                    int readBytes = file.read(buffer);
                    if (readBytes != bufferSize) {
                        // 读完文件,直接退出
                        // break;
        
                        // 如果想循环读
                        file.seek(0);
                        continue;
                    }
                    frame.bytesPerSample = bytesPerSample;
                    frame.samplesPerSec = context.pcmSampleRate;
                    frame.numChannels = context.pcmChannels;
                    frame.buffer = buffer;
                    frame.numSamples = readBytes / frame.bytesPerSample / frame.numChannels;
                    frame.timestamp = sampleCount * 1000 / context.pcmSampleRate;
        
                    delay = frame.timestamp;
                    long elapsed = System.currentTimeMillis() - startClock;
                    if (delay - elapsed > 5) {
                        sleep(delay - elapsed);
                    }
        
                    if (frame.numSamples > 0) {
                        context.engine.pushExternalAudioFrame(frame);
                        sampleCount += frame.numSamples;
                    }
        
                    long elpasedStats = System.currentTimeMillis() - lastStatsClock;
                    if (elpasedStats >= 2000) {
                        log("[Demo] audioPushThread pushExternalAudioFrame sampleCount:", sampleCount);
                        lastStatsClock = System.currentTimeMillis();
                    }
                }
            } catch (Exception e) {
                log("[Demo] pushAudio error:", e.toString());
            } finally {
                context.externalAudioFinished = true;
            }
        }, "pub-audio");
        context.externalAudioThread.start();
        }
    • 停止音频推流

      // 关闭外部音频源
      engine.setExternalAudioSource(false, pcmSampleRate, pcmChannels);
      
      // 关闭音频推流
      engine.publishLocalAudioStream(false);
  4. 发布本地视频流。

    入会之后,SDK默认不推流,需主动调用API开启推流(可在入会前调用)。

    • 开启视频推流

      // 开启推视频,前面调用过可不用再调用(支持入会前调用)
      engine.publishLocalVideoStream(true);
      
    • 外部视频推流

      • 如果是Linux服务器或没有摄像头视频采集设备,可使用外部视频源接口来推送视频数据。

      // 设置推外部视频文件和流类型
      // track 仅支持 DingRtcVideoTrackCamera  和 DingRtcVideoTrackScreen
      // 外部推流不支持 DingRtcVideoTrackBoth
      engine.setExternalVideoSource(true, DingRtcEngine.DingRtcVideoTrack.DingRtcVideoTrackCamera);
      
      // 生成Frame数据
      DingRtcEngine.DingRtcVideoFrame frame = new DingRtcEngine.DingRtcVideoFrame();
      frame.format = context.pixelFormat;
      frame.width = context.videoWidth;
      frame.height = context.videoHeight;
      frame.lineSize[0] = context.videoWidth;
      frame.lineSize[1] = context.videoWidth >> 1;
      frame.lineSize[2] = context.videoWidth >> 1;
      frame.rotation = context.videoRotation;
      frame.videoFrameLength = bufferSize;
      frame.buffer = buffer;
      
      // bufferSize
      // YUV(I420): buffer_size = width * height + half_width * half_height * 2;
      // RGBA:      buffer_size = width * height * 4;
      
      // 推送Frame数据
      engine.pushExternalAudioFrame(frame);
      • 通常需要一个单独的线程来循环推送视频数据,同时控制数据保持一定的时间间隔均匀输入给SDK。可类比为摄像头设备,按固定周期采集音频数据输入给SDK。

      static class RtcContext {
        // video config
        String videoYUVFilePath;
        DingRtcEngine.DingRtcVideoFormat videoPixelFormat = DingRtcEngine.DingRtcVideoFormat.DingRtcVideoI420;
        int videoWidth = 0;
        int videoHeight = 0;
        int videoRotation = 0;
        int videoFps = 25;
        volatile boolean isPushVideoStarted = false;
        volatile boolean externalVideoFinished = false;
        Thread externalVideoThread;
      }
      
      private static void startPushVideo(RtcContext context) {
          if (context.isPushVideoStarted) {
              return;
          }
          context.isPushVideoStarted = true;
          if (context.engine != null) {
              // 开启推流
              context.engine.publishLocalVideoStream(true);
              // 设置音频PCM推流
              context.engine.setExternalVideoSource(true, DingRtcEngine.DingRtcVideoTrack.DingRtcVideoTrackCamera);
          }
          context.externalVideoThread = new Thread(() -> {
              try (RandomAccessFile file = new RandomAccessFile(context.videoYUVFilePath, "r")) {
                  long frameCount = 0;
                  DingRtcEngine.DingRtcVideoFormat pixelFormat = context.videoPixelFormat;
                  int bufferSize = calcBufferSize(pixelFormat, context.videoWidth, context.videoHeight);
      
                  long delay = 0;
                  long startClock = System.currentTimeMillis();
                  long lastStatsClock = startClock;
                  byte[] buffer = new byte[bufferSize];
                  DingRtcEngine.DingRtcVideoFrame frame = new DingRtcEngine.DingRtcVideoFrame();
                  while (!context.quit) {
                      long readStartClock = System.currentTimeMillis();
                      int readBytes = file.read(buffer);
                      if (readBytes != bufferSize) {
                          // 读完文件,直接退出
                          // break;
      
                          // 如果想循环读
                          file.seek(0);
                          continue;
                      }
                      frame.format = pixelFormat;
                      frame.width = context.videoWidth;
                      frame.height = context.videoHeight;
                      frame.lineSize[0] = context.videoWidth;
                      frame.lineSize[1] = context.videoWidth >> 1;
                      frame.lineSize[2] = context.videoWidth >> 1;
                      frame.rotation = context.videoRotation;
                      frame.videoFrameLength = bufferSize;
                      frame.buffer = buffer;
                      frame.timestamp = frameCount * 1000 / context.videoFps;
      
                      delay = frame.timestamp;
                      long elapsed = System.currentTimeMillis() - startClock;
                      if (delay - elapsed > 5) {
                          sleep(delay - elapsed);
                      }
      
                      context.engine.pushExternalVideoFrame(frame, DingRtcEngine.DingRtcVideoTrack.DingRtcVideoTrackCamera);
                      frameCount++;
      
                      long elpasedStats = System.currentTimeMillis() - lastStatsClock;
                      if (elpasedStats >= 5000) {
                          log("[Demo] videoPushThread pushExternalVideoFrame frameCount:", frameCount);
                          lastStatsClock = System.currentTimeMillis();
                      }
                  }
              } catch (Exception e) {
                  log("[Demo] pushVideo error:", e.toString());
              } finally {
                  context.externalVideoFinished = true;
              }
          }, "pub-video");
          context.externalVideoThread.start();
      }
    • 停止视频推流

      // 关闭外部视频源
      engine.setExternalVideoSource(false, DingRtcEngine.DingRtcVideoTrack.DingRtcVideoTrackCamera);
      
      // 关闭音频推流
      engine.publishLocalVideoStream(false);

  5. 订阅远端音频流。

    SDK默认自动订阅远端音视频并拉流,如需关闭拉流请主动调用API关闭拉流(可在入会前调用)。

    • 订阅音频流

      SDK当前不支持按指定用户订阅音频流,仅支持订阅音频合流(即所有远端用户的声音合流)。

      // 订阅音频流,可在JoinChannel前调用
      engine.subscribeAllRemoteAudioStreams(true);
      
      // 取消订阅音频流,可在JoinChannel前调用
      engine.subscribeAllRemoteAudioStreams(false);
    • 监听音频帧数据

      class MyAudioFrameObserver extends DingRtcAudioFrameObserver {
        ...
      
        public boolean onPlaybackAudioFrame(DingRtcAudioFrame frame) {
          // 远端用户混音后待播放PCM数据
          return false;
        }
      }
      
      // 注册音频数据回调对象
      engine.registerAudioFrameObserver(new MyAudioFrameObserver());
      // 打开播放数据回调(远端用户声音)
      engine.enableAudioFrameObserver(true, DingRtcEngine.DingRtcAudioSource.DingRtcAudioSourcePlayback);
      
      // 取消音频数据回调
      engine.enableAudioFrameObserver(false, DingRtcEngine.DingRtcAudioSource.DingRtcAudioSourcePlayback);
      engine.registerAudioFrameObserver(null);
    • 监听发言人音量

      class MyRtcListener extends DingRtcEngineListener {
        ...
      
        @Override
        public void onAudioVolumeIndication(DingRtcEngine.DingRtcAudioVolumeInfo[] speakers, int speakerNumber) {
            // remote user volume info
        }
        ...
      }
      
      // 开启音量回调
      engine.enableAudioVolumeIndication(300, 3, 1);
      
      // 关闭音量回调
      engine.enableAudioVolumeIndication(0, 3, 1);

  6. 订阅远端视频流。

    • 订阅视频流

      // 方式一:订阅音频流(支持在joinChannel前就调用)
      engine.subscribeAllRemoteAudioStreams(true);
      
      // 方式二:订阅某一个人视频流
      engine.subscribeRemoteVideoStream(uid, track, true);
    • 监听视频帧数据

      class MyVideoFrameObserver extends DingRtcVideoFrameObserver {
        ...
      
        public boolean onCaptureVideoFrame(DingRtcVideoSample dingRtcVideoSample) {
          // 本地采集视频数据
          return false;
        }
      
        public boolean onRemoteVideoFrame(String str, DingRtcVideoTrack dingRtcVideoTrack, DingRtcVideoSample dingRtcVideoSample) {
          // 远端用户视频数据
          return false;
        }
      
        public boolean onPreEncodeVideoFrame(DingRtcVideoTrack dingRtcVideoTrack, DingRtcVideoSample dingRtcVideoSample) {
          // 本地编码前视频数据(对比本地采集视频数据,此处的数据经过了前处理)
          return false;
        }
      
        public DingRtcVideoFormat getVideoFormatPreference() {
          // 目前仅支持I420
          return DingRtcVideoFormat.DingRtcVideoI420;
        }
      }
      
      // 注册视频数据回调对象
      engine.registerVideoFrameObserver(new MyVideoFrameObserver());
      // 打开视频数据回调(远端用户视频)
      engine.enableVideoFrameObserver(true, DingRtcEngine.DingRtcVideoObservePosition.DingRtcPositionPreRender);
      
      // 取消视频数据回调
      engine.enableVideoFrameObserver(false, DingRtcEngine.DingRtcVideoObservePosition.DingRtcPositionPreRender);
      engine.registerVideoFrameObserver(null);

  7. 离开频道。

    // 离会
    engine.leaveChannel();

  8. 销毁引擎。

// 销毁SDK
engine.destroy();