从此

媒体/直播流、FFMPEG音视频开发、libjitsi通话库

综合/最新

Java编程专题

通用


音视频

综合:
  比特率=采样率 * 位 * 通道数
  RTP强制支持PCMU(μ-law)音频格式 而非PCMA(a-law)。
  FMJ规整URL斜杠:new MediaLocator(URLUtils.createUrlStr(new File("samplemedia/gulp2.wav")))
  音频接口:Windows保底为MME (Microsoft Multimedia Extensions),追求质量上DirectSound或独占模式的WASAPI (Windows Audio Session API)。


libjitsi:
  说明 - 
    支持本地收发;音频走 AudioMediaStreamImpl,视频走 VideoMediaStreamImpl,通过 RTCP Sender Reports 进行音画同步。
    本地网络 mediaStream.setConnector(connector); 远端目标 mediaStream.setTarget(new MediaStreamTarget(isa,isa2));
    正常停止不了就强制关闭:if (mediaStream != null) { try { mediaStream.stop(); } finally { mediaStream.close(); mediaStreams[i] = null; } }
    媒体流收发停止后,不会关闭网络连接的传输或监听,故还应 connector.close();
    若没调用 LibJitsi.stop() 收尾,直接 kill 程序,可能导致远端收不到正常挂断通知而死等超时:
      Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { LibJitsi.stop(); } catch (Exception e) { } }));

    默认配置:var ms = LibJitsi.getMediaService(); var dc = ((MediaServiceImpl) ms).getDeviceConfiguration(); var as = dc.getAudioSystem();
      // 或 AudioSystem.getAudioSystem(LOCATOR_PROTOCOL_PORTAUDIO);
    遍历收音:var cdi2 = as.getDevices(AudioSystem.DataFlow.PLAYBACK).getFirst();
      // 或 LibJitsi.getMediaService().getDefaultDevice(MediaType.AUDIO, MediaUseCase.CALL)).getCaptureDeviceInfo(); // 无设备时也非 null!?
    指定放音:as.setDevice(AudioSystem.DataFlow.PLAYBACK, cdi2, true); // 或 new AudioMediaDeviceImpl(cdi2);
      // 或 new MediaDeviceImpl(((CaptureDevice) Manager.createDataSource(loc)).getCaptureDeviceInfo(), MediaType.AUDIO);
    反查放音:as.getSelectedDevice(AudioSystem.DataFlow.PLAYBACK); // 或收音用 dc.getAudioCaptureDevice()
      // as.getDevice(AudioSystem.DataFlow.PLAYBACK, new MediaLocator(cdi.getLocator().toExternalForm()));
    建媒体流:LibJitsi.getMediaService().createMediaStream(new MediaDeviceImpl(cdi2, MediaType.AUDIO));

    implementation("org.jitsi:libjitsi:1.1-34-gb93ce2ee") // 解压出 win32-x86-64 目录;已包含了fmj库
    //implementation("org.jitsi:jitsi-lgpl-dependencies:1.2-23-g7b49874:win32-x86-64")
    tasks.named("run") { 
        jvmArgs = listOf("-Djava.library.path=D:\\temp\\libjitsi-1.2-23-g7b49874\\win32-x86-64\\")
        args = listOf("--local-port-base=11111","--remote-host=localhost","--remote-port-base=22222")
        //args = listOf("--local-port-base=22222","--remote-host=localhost","--remote-port-base=11111")
        //args +=['--local-port-base=11111', '--remote-host=localhost', '--remote-port-base=22222'] // 接收入参;2分钟自动退出
        //args +=['--local-port-base=22222', '--remote-host=localhost', '--remote-port-base=11111'] // 发送入参;1分钟自动退出
    }
    检查 java.library.path 指定或自动解压原生库:
        try { System.loadLibrary("jnawtrenderer"); System.out.println("libjitsi 原生库加载成功!");
        } catch (UnsatisfiedLinkError e) { e.printStackTrace(System.err); }

  说明:
    注意 - LibJitsi.stop() 一旦被调用,LibJitsi.getMediaService() 就会卡住,故只在程序退出时调一次。
      PortAudioStream用于音频输入、PortAudioRenderer用于音频输出?
      示例 - https://github.com/jitsi/libjitsi/blob/master/src/main/java/org/jitsi/examples/AVTransmit2.java

  通用API:
    LibJitsi.start(); // 属性配置必须在库启动后设置;环境变量估计可以提前。
    LibJitsi.getConfigurationService().setProperty(DISABLE_VIDEO_SUPPORT_PNAME, true);
    //LibJitsi.getConfigurationService().setProperty("net.java.sip.communicator.impl.neomedia.echocancel.filterLengthInMillis", 10000);

    // macOS 系统中 Java_org_jitsi_impl_neomedia_portaudio_Pa_Initialize(...) 调 PortAudio 库 Pa_Initialize() 报 PortAudioException: Insufficient memory: errorCode= -9992;
    // 会自动移出 AudioSystem.getAudioSystems(),不影响功能;或规避下输出:System.setProperty("org.jitsi.impl.neomedia.device.PortAudioSystem.disabled", "true");
    MediaService ms = LibJitsi.getMediaService(); // NeomediaServiceUtils.getMediaServiceImpl();

    var cdi = ((MediaDeviceImpl) ms.getDefaultDevice(MediaType.AUDIO, MediaUseCase.CALL)).getCaptureDeviceInfo();
    if (cdi != null) { // 因getDefaultDevice匹配不到设备会返回一个非null实例,故只能用getCaptureDeviceInfo()来判null。
      var stream = ms.createMediaStream((MediaDevice) cdi); System.out.println("stream - " + stream);
    }

    MediaServiceImpl msi = (MediaServiceImpl)ms;
    if (msi != null) {
        var dc = msi.getDeviceConfiguration();
        if (dc != null) {
            var r = dc.getEchoCancelFilterLengthInMillis();
            System.out.println("r - " + r);
        }
    }

    WASAPISystem感知不到小部分USB声卡“ReSpeaker 4 Mic Array (UAC1.0)”,获取到的MediaDevice是个“inactive”实例,可改用能感知USB声卡的PortAudioSystem:
      //默认 AudioSystem 实现在 DeviceSystem.java 中 OSUtils.IS_WINDOWS ? ".WASAPISystem" : null 和 OSUtils.IS_ANDROID ? null : ".PortAudioSystem"。
      LibJitsi.start(); // 初始化、启动库;并暂时禁用依赖FFmpeg的视频支持。
      LibJitsi.getConfigurationService().setProperty(DISABLE_VIDEO_SUPPORT_PNAME, true);
      LibJitsi.getMediaService(); // 调用后getAudioSystem才不返回null。
      var as = AudioSystem.getAudioSystem(LOCATOR_PROTOCOL_PORTAUDIO); // 换成除 IS_ANDROID 操作系统外均支持的 PortAudioSystem。
      if(as!=null){
       var devices = as.getDevices(AudioSystem.DataFlow.CAPTURE);
       System.out.println(devices);
       if (!devices.isEmpty()) {
        var cdi = (CaptureDevice) Manager.createDataSource(devices.getFirst().getLocator()).getCaptureDeviceInfo();
        if (cdi != null) {
            var stream = ms.createMediaStream(new MediaDeviceImpl(cdi, MediaType.AUDIO));
            System.out.println("stream - " + stream);
        }
       }
      }


  JMF/FMJ:
    说明 - JMF已被JDK删除,FMJ官方成员也说停止维护和不推荐使用,故可换用 Jitsi libjitsi 库。
    Bug:
      Manager.createDataSink(ds, m).open() 中 rtpManager.initialize(new SessionAddress()) 入参未判空被调导致NPE:
        故应改写 DataSink 实现类 net.sf.fmj.media.datasink.rtp.Handler.open() 源码,传入 new SessionAddress(InetAddress.getLocalHost(), parsedRTPUrl.elements[0].port);
        完整用法 - var ds = new Handler(); ds.setSource(processor.getDataOutput()); ds.setOutputLocator(m); ds.open(); ds.start();

    rtp最简实例: 仅需 implementation("org.jitsi:fmj:1.0.2-jitsi")
      VLC media player 媒体 -> 打开网络串流 填入组播(全子网)网址 rtp://224.0.0.1:22222/audio/16
      RTP发送源码 https://github.com/jitsi/fmj/blob/master/src.examples.rtp/SimpleVoiceTransmiter.java  https://github.com/jitsi/fmj/blob/master/src.test/net/sf/fmj/rtp/rtpaudio.java
        String url = "rtp://224.0.0.1:22222/audio/16"; // 224.0.0.1 为组播IP,多个流则通过RTP头的SSRC或CNAME来区分;audio为媒体类型,16为可选的TTL值;无TTL写法 rtp://224.0.0.1:22222/audio。
      解决 Unable to transcode format - Audacity -> 打开文件(示例音频) -> 导出音频 -> 导出到计算机 -> 单声道、采样率 8000 Hz、编码 U-Law -> 导出。
      解决 com.sun.media.processor.unknown.Handler ConcurrentModificationException - 将 if blockingRealize() 块儿换为 processor.realize();,取消注释下方的 while (processor.getState() != Processor.Realized) 块儿。     

其他