视频传输过程中,我们可以对采集到的视频数据进行前处理和后处理,获取想要的播放效果。
对于有自行处理视频数据需求的场景,声网提供原始数据功能,你可以在将数据发送给编码器前进行前处理,对捕捉到的视频帧进行修改;也可以在将数据发送给解码器后进行后处理,对接收到的视频帧进行修改。
我们在 GitHub 上提供一个开源的示例项目,你可以前往下载,或查看其中的源代码。
在使用原始数据功能前,请确保你已在项目中完成基本的实时音视频功能,详见一对一通话或互动直播。
IVideoFrameObserver
类,实现采集、修改原始视频数据功能。因此,你必须通过 JNI (Java Native Interface) 使用 Java 调用声网的 C++ 接口。由于 RTC Java SDK 封装了 RTC C++ SDK,所以可以直接通过 include SDK 中 .h
文件的方式调用 C++ 方法。参考如下步骤,在你的项目中实现原始视频数据功能:
registerVideoFrameObserver
方法注册视频观测器,并在该方法中实现一个 IVideoFrameObserver
类。onCaptureVideoFrame
、onPreEncodeVideoFrame
或 onRenderVideoFrame
回调发送获取到的原始视频数据。下图展示了在 Java 项目中调用声网 C++ API 的基本流程。
.cpp
文件)编译成的 .so
库。javac -h -jni
命令生成 .h
文件。C++ 接口文件需要导入此文件。.so
库的 C++ 方法。下图展示使用原始视频数据的 API 调用时序:
registerVideoFrameObserver
、onCaptureVideoFrame
、onPreEncodeVideoFrame
和 onRenderVideoFrame
均为 C++ 方法和回调。通过 JNI 接口分别创建 Java 和 C++ 的接口文件,并将 C++ 接口文件构建为 .so
库。
MediaPreProcessing.java
文件。// Java 接口文件,声明需要调用 C++ 的相关 Java 方法
package io.agora.advancedvideo.rawdata;
import java.nio.ByteBuffer;
public class MediaPreProcessing {
static {
// 加载 C++ .so 库。.so 库通过编译 C++ 接口文件生成。
// .so 库的名称取决于编译 C++ 接口文件生成的库名
System.loadLibrary("apm-plugin-raw-data");
}
// 定义与 C++ API 相对应的 Java 方法
public interface ProgressCallback {
// 获取采集的视频帧
void onCaptureVideoFrame(int videoFrameType, int width, int height, int bufferLength, int yStride, int uStride, int vStride, int rotation, long renderTimeMs);
// 获取编码前的视频帧
void onPreEncodeVideoFrame(int videoFrameType, int width, int height, int bufferLength, int yStride, int uStride, int vStride, int rotation, long renderTimeMs);
// 获取 SDK 渲染的视频帧
void onRenderVideoFrame(int uid, int videoFrameType, int width, int height, int bufferLength, int yStride, int uStride, int vStride, int rotation, long renderTimeMs);
// 获取录制的音频帧
void onRecordAudioFrame(int audioFrameType, int samples, int bytesPerSample, int channels, int samplesPerSec, long renderTimeMs, int bufferLength);
// 获取播放的音频帧
void onPlaybackAudioFrame(int audioFrameType, int samples, int bytesPerSample, int channels, int samplesPerSec, long renderTimeMs, int bufferLength);
// 获取混音前播放的音频帧
void onPlaybackAudioFrameBeforeMixing(int uid, int audioFrameType, int samples, int bytesPerSample, int channels, int samplesPerSec, long renderTimeMs, int bufferLength);
// 获取混音后的音频帧
void onMixedAudioFrame(int audioFrameType, int samples, int bytesPerSample, int channels, int samplesPerSec, long renderTimeMs, int bufferLength);
}
public static native void setCallback(ProgressCallback callback);
public static native void setVideoCaptureByteBuffer(ByteBuffer byteBuffer);
public static native void setAudioRecordByteBuffer(ByteBuffer byteBuffer);
public static native void setAudioPlayByteBuffer(ByteBuffer byteBuffer);
public static native void setBeforeAudioMixByteBuffer(ByteBuffer byteBuffer);
public static native void setAudioMixByteBuffer(ByteBuffer byteBuffer);
public static native void setVideoDecodeByteBuffer(int uid, ByteBuffer byteBuffer);
public static native void releasePoint();
}
.h
文件。# JDK 10 或更高版本
javac -h -jni MediaPreProcessing.java
# JDK 9 或更早版本
javac MediaPreProcessing.java
javah -jni MediaPreProcessing.class
创建 C++ 接口文件,用于被 Java 调用。C++ 接口文件需要根据生成的 .h
文件,从 C++ SDK export 相应的方法。具体实现可参考示例项目中的 io_agora_advancedvideo_rawdata_MediaPreProcessing.cpp
文件。
// 全局变量
jobject gCallBack = nullptr;
jclass gCallbackClass = nullptr;
// Java 层的 method ID
jmethodID captureVideoMethodId = nullptr;
jmethodID renderVideoMethodId = nullptr;
void *_javaDirectPlayBufferCapture = nullptr;
map<int, void *> decodeBufferMap;
static JavaVM *gJVM = nullptr;
// 实现 IVideoFrameObserver 类及相关回调
class AgoraVideoFrameObserver : public agora::media::IVideoFrameObserver
{
public:
AgoraVideoFrameObserver()
{
}
~AgoraVideoFrameObserver()
{
}
// 从 VideoFrame 对象获取视频帧数据,复制到 ByteBuffer,并通过 method ID 调用 Java 方法
void getVideoFrame(VideoFrame &videoFrame, _jmethodID *jmethodID, void *_byteBufferObject,
unsigned int uid)
{
if (_byteBufferObject == nullptr)
{
return;
}
int width = videoFrame.width;
int height = videoFrame.height;
size_t widthAndHeight = (size_t) videoFrame.yStride * height;
size_t length = widthAndHeight * 3 / 2;
AttachThreadScoped ats(gJVM);
JNIEnv *env = ats.env();
memcpy(_byteBufferObject, videoFrame.yBuffer, widthAndHeight);
memcpy((uint8_t *) _byteBufferObject + widthAndHeight, videoFrame.uBuffer,
widthAndHeight / 4);
memcpy((uint8_t *) _byteBufferObject + widthAndHeight * 5 / 4, videoFrame.vBuffer,
widthAndHeight / 4);
if (uid == 0)
{
env->CallVoidMethod(gCallBack, jmethodID, videoFrame.type, width, height, length,
videoFrame.yStride, videoFrame.uStride,
videoFrame.vStride, videoFrame.rotation,
videoFrame.renderTimeMs);
} else
{
env->CallVoidMethod(gCallBack, jmethodID, uid, videoFrame.type, width, height,
length,
videoFrame.yStride, videoFrame.uStride,
videoFrame.vStride, videoFrame.rotation,
videoFrame.renderTimeMs);
}
}
// 将视频帧数据从 ByteBuffer 复制到 VideoFrame 对象
void writebackVideoFrame(VideoFrame &videoFrame, void *byteBuffer)
{
if (byteBuffer == nullptr)
{
return;
}
int width = videoFrame.width;
int height = videoFrame.height;
size_t widthAndHeight = (size_t) videoFrame.yStride * height;
memcpy(videoFrame.yBuffer, byteBuffer, widthAndHeight);
memcpy(videoFrame.uBuffer, (uint8_t *) byteBuffer + widthAndHeight, widthAndHeight / 4);
memcpy(videoFrame.vBuffer, (uint8_t *) byteBuffer + widthAndHeight * 5 / 4,
widthAndHeight / 4);
}
public:
// 实现 onCaptureVideoFrame 回调
virtual bool onCaptureVideoFrame(VideoFrame &videoFrame) override
{
// 获取采集的视频帧
getVideoFrame(videoFrame, captureVideoMethodId, _javaDirectPlayBufferCapture, 0);
__android_log_print(ANDROID_LOG_DEBUG, "AgoraVideoFrameObserver", "onCaptureVideoFrame");
// 将视频帧发送回 SDK
writebackVideoFrame(videoFrame, _javaDirectPlayBufferCapture);
return true;
}
// 实现 onRenderVideoFrame 回调
virtual bool onRenderVideoFrame(unsigned int uid, VideoFrame &videoFrame) override
{
__android_log_print(ANDROID_LOG_DEBUG, "AgoraVideoFrameObserver", "onRenderVideoFrame");
map<int, void *>::iterator it_find;
it_find = decodeBufferMap.find(uid);
if (it_find != decodeBufferMap.end())
{
if (it_find->second != nullptr)
{
// 获取 SDK 渲染的视频帧
getVideoFrame(videoFrame, renderVideoMethodId, it_find->second, uid);
// 将视频帧发送回 SDK
writebackVideoFrame(videoFrame, it_find->second);
}
}
return true;
}
// 实现 onPreEncodeVideoFrame 回调
virtual bool onPreEncodeVideoFrame(VideoFrame& videoFrame) override {
// 获取编码前的视频帧
getVideoFrame(videoFrame, preEncodeVideoMethodId, _javaDirectPlayBufferCapture, 0);
__android_log_print(ANDROID_LOG_DEBUG, "AgoraVideoFrameObserver", "onPreEncodeVideoFrame");
// 将视频帧发送回 SDK
writebackVideoFrame(videoFrame, _javaDirectPlayBufferCapture);
return true;
}
};
...
// AgoraVideoFrameObserver 对象
static AgoraVideoFrameObserver s_videoFrameObserver;
// rtcEngine 对象
static agora::rtc::IRtcEngine *rtcEngine = nullptr;
// 设置 C++ 接口
#ifdef __cplusplus
extern "C" {
#endif
int __attribute__((visibility("default")))
loadAgoraRtcEnginePlugin(agora::rtc::IRtcEngine *engine)
{
__android_log_print(ANDROID_LOG_DEBUG, "agora-raw-data-plugin", "loadAgoraRtcEnginePlugin");
rtcEngine = engine;
return 0;
}
void __attribute__((visibility("default")))
unloadAgoraRtcEnginePlugin(agora::rtc::IRtcEngine *engine)
{
__android_log_print(ANDROID_LOG_DEBUG, "agora-raw-data-plugin", "unloadAgoraRtcEnginePlugin");
rtcEngine = nullptr;
}
...
// 针对 Java 接口文件,通过 JNI 导出相应的 C++ 实现。
// Java_io_agora_advancedvideo_rawdata_MediaPreProcessing_setCallback 方法
// 对应 Java 接口文件的 setCallback 方法。
JNIEXPORT void JNICALL Java_io_agora_advancedvideo_rawdata_MediaPreProcessing_setCallback
(JNIEnv *env, jclass, jobject callback)
{
if (!rtcEngine) return;
env->GetJavaVM(&gJVM);
// 创建使用 IMediaEngine 类为 template 的 AutoPtr 实例
agora::util::AutoPtr<agora::media::IMediaEngine> mediaEngine;
// AutoPtr 实例调用 queryInterface 方法,通过 IID 获取 IMediaEngine 实例的指针。
// AutoPtr 实例会通过箭头操作符访问 IMediaEngine 实例的指针并通过 IMediaEngine 实例调用 registerVideoFrameObserver
mediaEngine.queryInterface(rtcEngine, agora::INTERFACE_ID_TYPE::AGORA_IID_MEDIA_ENGINE);
if (mediaEngine)
{
// 注册视频帧观测器
int code = mediaEngine->registerVideoFrameObserver(&s_videoFrameObserver);
...
}
if (gCallBack == nullptr)
{
gCallBack = env->NewGlobalRef(callback);
gCallbackClass = env->GetObjectClass(gCallBack);
// 获取回调函数的 method ID
captureVideoMethodId = env->GetMethodID(gCallbackClass, "onCaptureVideoFrame",
"(IIIIIIIIJ)V");
renderVideoMethodId = env->GetMethodID(gCallbackClass, "onRenderVideoFrame",
"(IIIIIIIIIJ)V");
__android_log_print(ANDROID_LOG_DEBUG, "setCallback", "setCallback done successfully");
}
...
// Java 接口文件中 setVideoCaptureByteBuffer 方法的 C++ 实现
JNIEXPORT void JNICALL
Java_io_agora_advancedvideo_rawdata_MediaPreProcessing_setVideoCaptureByteBuffer
(JNIEnv *env, jclass, jobject bytebuffer)
{
_javaDirectPlayBufferCapture = env->GetDirectBufferAddress(bytebuffer);
}
// Java 接口文件中 setVideoDecodeByteBuffer 方法的 C++ 实现
JNIEXPORT void JNICALL
Java_io_agora_advancedvideo_rawdata_MediaPreProcessing_setVideoDecodeByteBuffer
(JNIEnv *env, jclass, jint uid, jobject byteBuffer)
{
if (byteBuffer == nullptr)
{
decodeBufferMap.erase(uid);
} else
{
void *_javaDirectDecodeBuffer = env->GetDirectBufferAddress(byteBuffer);
decodeBufferMap.insert(make_pair(uid, _javaDirectDecodeBuffer));
__android_log_print(ANDROID_LOG_DEBUG, "agora-raw-data-plugin",
"setVideoDecodeByteBuffer uid: %u, _javaDirectDecodeBuffer: %p",
uid, _javaDirectDecodeBuffer);
}
}
}
}
...
#ifdef __cplusplus
}
#endif
.so
库。CMake 示例内容如下。所生成的 .so
库在 Java 接口文件中通过 System.loadLibrary()
的方式加载。cmake_minimum_required(VERSION 3.4.1)
add_library( # Sets the name of the library.
apm-plugin-raw-data
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/io_agora_advancedvideo_rawdata_MediaPreProcessing.cpp)
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log)
target_link_libraries( # Specifies the target library.
apm-plugin-raw-data
# Links the target library to the log library
# included in the NDK.
${log-lib})
// 在 Java 中实现 ProgressCallback 接口
public class MediaDataObserverPlugin implements MediaPreProcessing.ProgressCallback {
...
// 获取采集的视频帧
@Override
public void onCaptureVideoFrame(int videoFrameType, int width, int height, int bufferLength, int yStride, int uStride, int vStride, int rotation, long renderTimeMs) {
byte[] buf = new byte[bufferLength];
byteBufferCapture.limit(bufferLength);
byteBufferCapture.get(buf);
byteBufferCapture.flip();
for (MediaDataVideoObserver observer : videoObserverList) {
observer.onCaptureVideoFrame(buf, videoFrameType, width, height, bufferLength, yStride, uStride, vStride, rotation, renderTimeMs);
}
byteBufferCapture.put(buf);
byteBufferCapture.flip();
if (beCaptureVideoShot) {
beCaptureVideoShot = false;
getVideoSnapshot(width, height, rotation, bufferLength, buf, captureFilePath, yStride, uStride, vStride);
}
}
// 获取编码前的视频帧
@Override
public void onPreEncodeVideoFrame(int videoFrameType, int width, int height, int bufferLength, int yStride, int uStride, int vStride, int rotation, long renderTimeMs) {
byte[] buf = new byte[bufferLength];
byteBufferCapture.limit(bufferLength);
byteBufferCapture.get(buf);
byteBufferCapture.flip();
for (MediaDataVideoObserver observer : videoObserverList) {
observer.onPreEncodeVideoFrame(buf, videoFrameType, width, height, bufferLength, yStride, uStride, vStride, rotation, renderTimeMs);
}
byteBufferCapture.put(buf);
byteBufferCapture.flip();
if (beCaptureVideoShot) {
beCaptureVideoShot = false;
getVideoSnapshot(width, height, rotation, bufferLength, buf, captureFilePath, yStride, uStride, vStride);
}
}
// 获取 SDK 渲染的视频帧
@Override
public void onRenderVideoFrame(int uid, int videoFrameType, int width, int height, int bufferLength, int yStride, int uStride, int vStride, int rotation, long renderTimeMs) {
for (MediaDataVideoObserver observer : videoObserverList) {
ByteBuffer tmp = decodeBufferList.get(uid);
if (tmp != null) {
byte[] buf = new byte[bufferLength];
tmp.limit(bufferLength);
tmp.get(buf);
tmp.flip();
observer.onRenderVideoFrame(uid, buf, videoFrameType, width, height, bufferLength, yStride, uStride, vStride, rotation, renderTimeMs);
tmp.put(buf);
tmp.flip();
if (beRenderVideoShot) {
if (uid == renderVideoShotUid) {
beRenderVideoShot = false;
getVideoSnapshot(width, height, rotation, bufferLength, buf, renderFilePath, yStride, uStride, vStride);
}
}
}
}
}
setCallback
方法。setCallback
方法通过 JNI 调用 C++ API 的 registerVideoFrameObserver
方法,注册视频帧观测器。@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// 实现 MediaDataObserverPlugin 实例
mediaDataObserverPlugin = MediaDataObserverPlugin.the();
// 注册视频帧观测器
MediaPreProcessing.setCallback(mediaDataObserverPlugin);
MediaPreProcessing.setVideoCaptureByteBuffer(mediaDataObserverPlugin.byteBufferCapture);
mediaDataObserverPlugin.addVideoObserver(this);
}
onCaptureVideoFrame
回调,onRenderVideoFrame
回调,和 onPreEncodeVideoFrame
回调。从回调中获取视频帧并进行处理。// 获取采集到的视频帧
@Override
public void onCaptureVideoFrame(byte[] data, int frameType, int width, int height, int bufferLength, int yStride, int uStride, int vStride, int rotation, long renderTimeMs) {
Log.e(TAG, "onCaptureVideoFrame0");
if (blur) {
return;
}
Bitmap bitmap = YUVUtils.i420ToBitmap(width, height, rotation, bufferLength, data, yStride, uStride, vStride);
Bitmap bmp = YUVUtils.blur(getContext(), bitmap, 4);
System.arraycopy(YUVUtils.bitmapToI420(width, height, bmp), 0, data, 0, bufferLength);
}
// 获取渲染前的视频帧
@Override
public void onRenderVideoFrame(int uid, byte[] data, int frameType, int width, int height, int bufferLength, int yStride, int uStride, int vStride, int rotation, long renderTimeMs) {
if (blur) {
return;
}
Bitmap bmp = YUVUtils.blur(getContext(), YUVUtils.i420ToBitmap(width, height, rotation, bufferLength, data, yStride, uStride, vStride), 4);
System.arraycopy(YUVUtils.bitmapToI420(width, height, bmp), 0, data, 0, bufferLength);
}
// 获取编码前的视频帧
@Override
public void onPreEncodeVideoFrame(byte[] data, int frameType, int width, int height, int bufferLength, int yStride, int uStride, int vStride, int rotation, long renderTimeMs) {
Log.e(TAG, "onPreEncodeVideoFrame0");
}
使用 3.0.1 或之后版本的 SDK 时,请注意如下:
IVideoFrameObserver
中的回调函数在同一个线程中报告。SDK 只会保证这些回调的顺序性。IVideoFrameObserver
中的回调函数中主动切换 OpenGL 的上下文,否则美颜可能失效。如果你还想在项目中实现原始音频数据功能,请参考原始音频数据。