在 Java 中使用 WebRTC 传输视频——准备工作

引言

最近一段时间的主要工作内容是开发一个远程控制手机的功能,其中音视频传输的部分是采用WebRTC技术来进行的,而我们的手机都是通过与其直接连接的Agent服务器进行管理,Agent服务是Java写的,现在市面上又没有合适的Java版WebRTC库,所以我就基于Google开源代码,写了一个JNI调用WebRTC Native的库。之前的一篇文章,我主要讲了讲我是怎么编译WebRTC的。这篇文章,我就来分享一下我是怎么在Java中使用WebRTC的,以及我根据业务需要对WebRTC的一些改动。本文源代码可通过扫描文章下方的公众号获取或付费下载。更多相关文章和其他文章均收录于贝贝猫的文章目录

Native APIs介绍

如果您也要进行和我类似的工作,我觉得最主要的还是要先熟悉整个Native APIs的使用流程,梳理一下,你就会发现整个使用过程其实非常简单,也就八个大步骤。接下来我会先简单介绍这八个主要步骤,然后再针对每一个步骤,详细的介绍我是怎么做的。
Native APIs
Native APIs使用流程:

  1. 通过Native APIs创建三个WebRTC工作的线程:Worker Thread,Network Thread,Signaling Thread
    • 如果您像我一样需要自定义的音频采集模块以及自定义的编解码实现的话,也需要在这一步将其初始化。
  2. 创建PeerConnectionFactory,这个工厂是所有后续工作的源头,无论是连接,还是音视频采集都需要由它来创建。
  3. 创建PeerConnection,在这个过程中您可以设置连接的一些参数,比如ICE Server用哪个,网络TCP/UDP策略是怎样的。
    • 如果您像我一样需要对端口的使用进行一些限制的话,需要指定自定义PortAllocator
  4. 创建Audio/VideoSource,创建AudioSource时可以指定一些采集参数,VideoSource需要一个VideoCapturer对象作为参数。
    • 如果您想我一样需要自己提供视频图像的话,就要实现一个自定义的VideoCapturer
  5. 以上一步创建的Audio/VideoSource作为参数,创建AudioTrackInterface,这个对象代表了Audio/Video的采集过程
  6. 创建MediaStreamInterface并将前一步创建的Audio/VideoTrack添加进去,这个对象代表了传输通道
  7. 将上一步创建的MediaStream添加到第三步创建的PeerConnection中
  8. PeerConnection通过Observer以回调的形式通知使用者,当前的连接状态等。我们需要通过各类回调以及PeerConnection的API,来完成与另一个连接者之间的SDP和ICE Candidate的交换。

这八个步骤中,前两个是Native APIs这里特有的内容,其后的这些步骤基本上和Web中对WebRTC的使用流程相似。我当时就是在这些Native特有的内容上遇到了很多坑,接下来就让我详细的介绍一下我是如何在Java服务中通过Native APIs和其他客户端建立起连接吧。

JNI Vs JNA

大家应该都知道,要想在Java中调用C++的代码,需要使用JNI或者JNA技术,那么它们两个有什么不同呢?在我们这个场景中应该使用哪一个呢?
JNI-Usage
上图就是JNI的使用方式,从图中可以看到使用步骤非常多,很繁琐。我们先要在Java代码里定义好接口,然后通过工具生成对应的C语言头文件,接着再用C语言实现这些接口并编译成共享库,最终在JVM中Load该库,从而达到调用C语言代码的目的。
JNA-Usage
而JNA相对来说就简单了许多,我们不需要重写我们的动态链接库文件,而是有直接调用的API,大大简化了我们的工作量。看似JNA好像完胜JNI,这部分工作非JNA莫属了。但是在我的这个场景中,JNA有几个致命的问题,以至于我只能用JNI。
为什么不用JNA

  1. JNA只能实现Java访问C函数,而我们在使用PeerConnection相关的APIs时,很多都是以Observer的形式回调的,这就需要C代码回调Java的ObserverWrapper。
  2. JNA技术比使用JNI技术调用动态链接库会有些微的性能损失,虽然我不确定这个损失有多大,但是考虑到我们需要从Java传输每帧的图像给C,这个过程我们希望是越快越好。

好了,既然我们已经确定要使用JNI技术了,就让我来介绍一下我具体是怎么做的吧。

代码结构

Java代码结构

JNI-Structure

  1. script/build-header-files.sh: 根据我写的Java接口,生成对应C语言头文件的脚本。

        #!/usr/bin/env bash
        ls -l ../path/to/rtc4j/core| grep ^- | awk '{print $9}' |
        sed 's/.class//g'|
        sed 's/^/package.name.of.core.&/g'|
        xargs javah -classpath ../target/classes -d ../../cpp/src/jni/
    
  2. src/XXX/core/: 这个包下就是这个库的核心部分,主要包含了音频采集器,视频采集器,连接过程中需要用到的各种回调接口,WebRTC核心类的Wrapper:

    • RTC -> webrtc::PeerConnectionFactoryInterface
    • PeerConnection -> webrtc::PeerConnectionInterface
    • DataChannel -> webrtc::DataChannelInterface
  3. src/XXX/model/: 定义了核心类中使用到的POJO对象

  4. src/XXX/utils/: 实现了不同平台下在Java端加载Shared Lib的过程

C++代码结构

C++这边的代码结构也比较简单,基本上和Java的接口是一一对应的。
JNI-Structure

  1. src/jni/: 由Java接口自动生成出来的C语言头文件,和Java相关的类型工具包
  2. src/media/: 音视频采集相关类,自定义编码相关类
    • 音频部分实现了一个自定义的AudioDeviceModule,在创建PeerConnectionFactory的时候将其注入
    • 视频部分实现了一个自定义的VideoCapturer,在创建VideoSource的时候将其注入
    • H264的视频编解码使用了FFMPEG中提供的libx264以及h264_nvenc(英伟达加速),这部分代码在创建PeerConnectionFactory的时候将其注入
  3. src/rtc/: 各个Java Wrapper接口的实现类
  4. src/rtc/network: 这里面定义了我自己的SocketFactory,通过它达到了限制端口的目的,这部分在创建PeerConnection的时候将其注入

Java代码相对来说都比较简单,就是给Native APIs做个壳儿,C++也有不少代码就是对更下层WebRTC lib的简单封装,这些部分我就一笔带过了,着重来讲一下这里比较难啃的骨头。

在C++中引入需要的库

整个C++项目我是基于CMake搭建的,其中使用到了libwebrtcFFMPEG(用于视频编码),libjpeg-turbo(用于将JavaVideoCapturer中获取的图片转码成YUV), CMake文件如下:

   cmake_minimum_required(VERSION 3.8)
   project(rtc)
   set(CMAKE_CXX_STANDARD 11)

   if (APPLE)
       set(CMAKE_CXX_FLAGS "-fno-rtti -pthread") #WebRTC库用到的FLAGS
   elseif (UNIX)
       #除了前两个-fno-rtti -pthread,其他都是FFMPEG需要使用到的FLAGS
       set(CMAKE_CXX_FLAGS "-fno-rtti -pthread -lva -lva-drm -lva-x11 -llzma -lX11 -lz -ldl -ltheoraenc -ltheoradec")
       set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-Bsymbolic")
   endif()

   include(./CMakeModules/FindFFMPEG.cmake) #引入FFMPEG
   include(./CMakeModules/FindLibJpegTurbo.cmake) #引入Jpeg-Turbo

   if (CMAKE_SYSTEM_NAME MATCHES "Linux") #C++代码中用于区分系统环境使用到属性
       set_property(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS WEBRTC_LINUX)
   elseif(CMAKE_SYSTEM_NAME MATCHES "Darwin")
       set_property(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS WEBRTC_MAC)
   endif()

   find_package(LibWebRTC REQUIRED) #引入WebRTC
   find_package(JNI REQUIRED) #引入JNI
   include_directories(${Java_INCLUDE_PATH}) #JNI头文件
   include_directories(${Java_INCLUDE_PATH2}) #JNI头文件
   include(${LIBWEBRTC_USE_FILE}) #WebRTC头文件
   include_directories("src")
   include_directories(${CMAKE_CURRENT_BINARY_DIR})
   include_directories(${TURBO_INCLUDE_DIRS}) #Jpeg-Turbo头文件

   file(GLOB_RECURSE SOURCES *.cpp) #需要编译的内容
   file(GLOB_RECURSE HEADERS *.h) #需要编译的内容头文件

   add_library(rtc SHARED ${SOURCES} ${HEADERS}) #编译共享库
   target_include_directories(rtc PRIVATE ${TURBO_INCLUDE_DIRS} ${FFMPEG_INCLUDE_DIRS})
   target_link_libraries(rtc PRIVATE ${TURBO_LIBRARIES} ${FFMPEG_LIBRARIES} ${LIBWEBRTC_LIBRARIES}) #链接共享库

引入这些库的时候也踩了不少坑,尤其是使用FFMPEG的时候,下面简单分享一下。

编译FFMPEG

  1. 在Linux下编译FFMPEG,我主要参考了官方Guide, 但是我们这里需要有一些改动
    a. 如果有enable-shared开关一定要打开,官方Guide中都是disable的
    b. 编译的时候一定要加上**"-fPIC"**,否则在Linux下链接时会有错误提示。共享对象可能会被不同的进程加载到不同的位置上,如果共享对象中的指令使用了绝对地址、外部模块地址,那么在共享对象被加载时就必须根据相关模块的加载位置对这个地址做调整,也就是修改这些地址,让它在对应进程中能正确访问,而被修改到的段就不能实现多进程共享一份物理内存,它们在每个进程中都必须有一份物理内存的拷贝。fPIC指令就是为了让使用到同一个共享对象的多个进程能尽可能多的共享物理内存,它背后把那些涉及到绝对地址、外部模块地址访问的地方都抽离出来,保证代码段的内容可以多进程相同,实现共享。

       /usr/bin/ld: test.o: relocation R_X86_64_32 against `a local symbol' can not be used when making a shared object; recompile with -fPIC
       test.o: could not read symbols: Bad value
       collect2: ld returned 1 exit status
    

    c. 如果您也需要Nvidia的支持的话,请参考官方Guide
    d. 最后分享一下我最终编译FFMPEG时使用到的命令

       PATH="$HOME/bin:$PATH" PKG_CONFIG_PATH="$HOME/ffmpeg_build/lib/pkgconfig" ./configure \
         --prefix="$HOME/ffmpeg_build" \
         --pkg-config-flags="--static" \
         --extra-cflags="-I$HOME/ffmpeg_build/include" \
         --extra-ldflags="-L$HOME/ffmpeg_build/lib" \
         --extra-libs=-lpthread \
         --extra-libs=-lm \
         --bindir="$HOME/bin" \
         --enable-gpl \
         --enable-libfdk_aac \
         --enable-libfreetype \
         --enable-libmp3lame \
         --enable-libopus \
         --enable-libvorbis \
         --enable-libvpx \
         --enable-libx264 \
         --enable-libx265 \
         --enable-nonfree \
         --extra-cflags=-I/usr/local/cuda/include/ \
         --extra-ldflags=-L/usr/local/cuda/lib64 \
         --enable-shared \
         --cc="gcc -m64 -fPIC” \
         --enable-nvenc \
         --enable-cuda \
         --enable-cuvid \
         --enable-libnpp
    
  2. Mac上安装FFMPEG就比较简单粗暴, 一键安装带所有参数的版本

        brew install ffmpeg $(brew options ffmpeg | grep -vE '\s' | grep -- '--with-' | tr '\n' ' ')
    

安装libjpeg-turbo

因为这个库比简单,我就直接下载了别人编译的版本

引入Turbo和FFMPEG

引入这两个库的方式非常类似,这里我就选取比较简单的FindLibJpegTurbo.cmake作为例子,FFMPEG与其相比就是寻找的下层依赖更多罢了。

   # Try to find the libjpeg-turbo libraries and headers
   #
   # TURBO_INCLUDE_DIRS
   # TURBO_LIBRARIES
   # TURBO_FOUND

   # Find header files
   FIND_PATH(
       TURBO_INCLUDE_DIRS turbojpeg.h
       /opt/libjpeg-turbo/include/
   )

   FIND_LIBRARY(
       TURBO_LIBRARY
       NAMES libturbojpeg.a
       PATH /opt/libjpeg-turbo/lib64
   )

   FIND_LIBRARY(
       JPEG_LIBRARY
       NAMES libjpeg.a
       PATH /opt/libjpeg-turbo/lib64
   )


   IF (TURBO_LIBRARY)
       SET(TURBO_FOUND TRUE)
   ENDIF ()

   IF (FFMPEG_FOUND AND TURBO_INCLUDE_DIRS)
       SET(TURBO_FOUND TRUE)
       SET(TURBO_LIBRARIES ${TURBO_LIBRARY} ${JPEG_LIBRARY})
       MESSAGE(STATUS "Found Turbo library: ${TURBO_LIBRARIES}, ${TURBO_INCLUDE_DIRS}")
   ELSE (FFMPEG_FOUND AND TURBO_INCLUDE_DIRS)
       MESSAGE(STATUS "Not found Turbo library")
   ENDIF ()

至此,所有准备工作总算是完了,让我们来看看到底是怎么调用Native APIs的吧。

参考内容

[1] JNI的替代者—使用JNA访问Java外部功能接口
[2] Linux共享对象之编译参数fPIC
[3] Android JNI 使用总结
[4] FFmpeg 仓库

本文源代码可通过扫描文章下方的公众号获取或付费下载
stun

  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Java项目集成WebRTC播放视频可以通过以下步骤完成: 1. 添加WebRTC的依赖:首先,在项目的pom.xml文件添加WebRTC的依赖。你可以使用Maven或者Gradle来管理项目依赖,从官方的仓库获取相应的WebRTC依赖。 2. 初始化WebRTC组件:在项目的启动代码,初始化WebRTC相关的组件。这包括创建PeerConnectionFactory,设置视频和音频的参数,以及初始化网络连接等。 3. 连接到视频源:通过WebRTC的API,连接到视频源。你可以通过传入视频源的URL或其他标识来连接到指定的视频源。这可以是一个录制好的视频文件,也可以是一个实时的视频流。 4. 创建视频播放器:使用Java视频播放器组件(如MediaPlayer或JavaFX的Media组件)创建一个视频播放器。将WebRTC接收到的视频数据传输给播放器进行解码和渲染。 5. 控制播放流程:实现视频播放的控制流程,包括开始播放、暂停、停止、快进/快退等功能。你可以根据需要自定义播放器的控制界面或者使用现有的控制组件。 6. 处理错误和异常:在播放视频的过程,可能会遇到网络不稳定、视频源不可用等异常情况。在代码合适的位置添加错误处理逻辑,保证程序的稳定性和容错性。 7. 释放资源:在项目关闭或退出之前,释放WebRTC和播放器相关的资源。这包括关闭网络连接、释放WebRTC组件、停止播放器等操作,以避免资源泄漏。 总结:通过以上步骤,我们可以在Java项目集成WebRTC实现视频的播放。WebRTC提供了强大的实时通信能力,结合Java的多媒体处理能力,可以快速搭建一个高效稳定的视频播放系统。当然,具体实现细节还需要根据项目需求进行相应的调整和完善。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

贝克街的流浪猫

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值