火山引擎云手机支持虚拟摄像头、虚拟麦克风功能,能够实现将音视频裸数据注入到云机实例的虚拟摄像头/麦克风中,供实例上运行的应用获取和使用。
本文介绍如何使用火山引擎云手机虚拟摄像头 & 麦克风 SDK(Proxy SDK),将获取到的音视频数据注入云机实例。内部采集音视频注入功能请参考内部采集音视频注入;外部音视频数据注入功能请参考外部音视频源注入(客户端注入)。
使用 Proxy SDK 注入音视频适用的场景如下:

下载 SDK 并拷贝至你的项目。
语言 | SDK 文件 |
|---|---|
Java | |
C++ | armeabi-v7a: arm64-v8a: |
专为 Unity 场景开发的用于采集 Unity Texture 的插件。需配合 Proxy SDK 一起使用。
将解压后的 SDK 文件夹复制到项目的 libs 目录中。
配置项目依赖。打开项目的 build.gradle 文件,在 dependencies 代码块中添加以下配置:
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar','*.aar']) }
同步项目。保存build.gradle文件的更改后,执行 gradle sync 即可。
配置 CMakeLists.txt 以引入 SDK 库和头文件。
# 添加 SDK 的 so 库 add_library(vdevice SHARED IMPORTED) set_target_properties(vdevice PROPERTIES IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libvdevice.so) # 指定头文件路径 include_directories(src/main/cpp/include)
在 CMakeLists.txt 中链接 NDK 日志库。
target_link_libraries( # Specifies the target library. native_test # Links the target library to the log library # included in the NDK. vdevice ${log-lib} )
在目标库中使用 SDK。
using namespace android; static void onAudioRequest(int start) { LOGI("onAudioRequest: %d", start); } static void onCameraRequest(int start, int cameraId) { LOGI("onCameraRequest start = %d, cameraId = %d", start, cameraId); } bool testRegisterAudioCblk() { LOGI("call testRegisterAudioCblk"); static AudioProxy *audioProxy = AudioProxy::getInstance(); return audioProxy->registerCallback(onAudioRequest); } bool testRegisterCameraCblk() { LOGI("call testRegisterAudioCblk"); static CameraProxy *cameraProxy = CameraProxy::getInstance(); return cameraProxy->registerCallback(onCameraRequest); }
使用 Proxy SDK 实现音视频裸数据注入功能时,可能会使用到以下三方库,请自行引入:
注意
使用 Proxy SDK 注入音频前,如正在使用云手机 Android SDK 注入外部采集音频,请先调用 setAudioInjectionState(false) 接口停止从客户端注入音频,否则会导致注入失败。
// step1. 获取DeviceProxyContext DeviceProxyContext deviceProxyContext = DeviceProxyContext.getInstance(context); // step2. 获取AudioProxyManager AudioProxyManager proxy = deviceProxyContext.getAudioProxyManager(); // step3. 设置注入参数(采样率、声道数、采样深度,采样周期。默认 48kHz, 双声道、16 位采样深度) proxy.setAudioSampleConfigs(new AudioSampleConfig(48000, 2, 2, 10)); // step4. 指定音频文件路径和注入类型 String path = "/vendor/res/sound/48000-16-2.pcm"; proxy.recordByFile(path, FileRecordType.TYPE_CIRCLE);
// step1. 获取DeviceProxyContext DeviceProxyContext deviceProxyContext = DeviceProxyContext.getInstance(context); // step2. 获取AudioProxyManager AudioProxyManager proxy = deviceProxyContext.getAudioProxyManager(); // step3(可选). 设置注入参数(采样率、声道数、采样深度,默认48kHz, 双声道、16位采样深度) proxy.setAudioSampleConfigs(new AudioSampleConfig(48000, 2, 2)); //设置每帧时长或者设置每帧 buffer 大小 proxy.setSamplePeriodInMs(10); //proxy.setAudioBufferSize(size); AudioTask task = new AudioTask(proxy); AudioCallback callback = new AudioProxyManager.AudioCallback() { @Override public void onStart() { // 开始注入音频数据 task.startAudioRecord(); } @Override public void onResume() {} @Override public void onPause() {} @Override public void onStop() { // 停止注入音频数据 task.stopAudioRecord(); } @Override public void onRequest() {} }; // step4. 注册AudioCallback,监听系统打开/关闭Mic proxy.registerCallback(callback); class AudioTask { private AudioProxyManager mProxy; public AudioTask(AudioProxyManager proxy) { mProxy = proxy; } public void startAudioRecord() { ... // step5. 调用putAudioFrame注入音频帧 mProxy.putAudioFrame(buffer); ... } public void stopAudioRecord() { // step6. 结束注入音频帧 ... } }
// step1. 获取 DeviceProxyContext DeviceProxyContext deviceProxyContext = DeviceProxyContext.getInstance(context); // step2. 获取 AudioProxyManager AudioProxyManager manager = deviceProxyContext.getAudioProxyManager(); // step3. 指定 App 包名,确定音频源。若注入源为自主开发应用,请保证音频格式为 AudioAttributes.USAGE_MEDIA 或AudioAttributes.USAGE_GAME,否则无法录入 manager.registerAppAsAudioSource("com.dimowner.audiorecorder");
注意
getCameraProxyManager 后,程序应暂停 200 毫秒(ms),以确保 CameraProxyManager 实例已正确初始化,然后再注册 CameraCallback 回调。ProxyContext。CameraProxyManager。CameraCallback,监听 onStart() 和 onStop() 接口。putVideoFrame 注入视频帧。示例代码参考:
public class CameraDemoActivity extends AppCompatActivity { private DeviceProxyContext proxyContext; private CameraProxyManager cameraProxyManager; private static final int WIDTH = 1280; // Frame width private static final int HEIGHT = 720; // Frame height private static final int FORMAT = VideoFrameFormat.I420; // 帧格式 private VideoFrameConfig videoFrameConfig = new VideoFrameConfig(WIDTH, HEIGHT, FORMAT, 0, 0); private byte[] demoBuffer; private CameraProxyManager.CameraCallback cameraCallback = new CameraProxyManager.CameraCallback() { @Override public void onStart(int id) { handler.post(renderThread); } @Override public void onResume(int id) { } @Override public void onPause(int id) { } @Override public void onStop(int id) { handler.removeCallbacks(renderThread); } @Override public void onRequest(int id) { } }; private Handler handler; private Runnable renderThread = new Runnable() { @Override public void run() { if(cameraProxyManager != null) { Arrays.fill(demoBuffer, (byte) 0x00); //TODO fill image data cameraProxyManager.putVideoFrame(demoBuffer, demoBuffer.length, videoFrameConfig); handler.postDelayed(this, 33); // 30fps } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_camera_demo); handler = new Handler(Looper.myLooper()); proxyContext = DeviceProxyContext.getInstance(this); try { Thread.sleep(200); } catch (InterruptedException e) { throw new RuntimeException(e); } if(proxyContext != null){ cameraProxyManager = proxyContext.getCameraProxyManager(); if(cameraProxyManager != null){ cameraProxyManager.unregisterCallback(cameraCallback); boolean ret = cameraProxyManager.registerCallback(cameraCallback); demoBuffer = new byte[videoFrameConfig.width * videoFrameConfig.height * 3 / 2]; // I420 buffer } } } }
可参考以下示例,实现将解码视频文件后获取到的视频流数据注入至云机实例。
public interface CameraProxyManager extends ProxyInterface { /** * 注册Camera打开/关闭事件回调监听接口,注册成功返回true, * 如果返回false,可能是优先级低,或者正在使用 ,或者重复注册 * @param cameraCallback 回调 * @return true 成功,false 失败 */ boolean registerCallback(CameraCallback cameraCallback); /** * 取消Camera打开/关闭事件回调接口,取消成功返回true, * 如果返回false,可能是底层服务异常,或者binder服务连接异常 * @param cameraCallback 回调,需要和register保持一致 * @return true 成功,false 失败 */ boolean unregisterCallback(CameraCallback cameraCallback); /** * 向虚拟摄像头注入一帧画面,如果成功返回true, * 如果返回false,可能是CameraCallback没有注入,就调用了put方法,也可能是camera没有打开,或者camera服务异常。 * @param buffer 图像数据的ByteBuffer对象 * @param config 图像数据的大小,旋转方向等。 * @return true 成功,false 失败 */ boolean putVideoFrame(@NonNull ByteBuffer buffer, @NonNull VideoFrameConfig config); /** * 向虚拟摄像头注入一帧画面,如果成功返回true, * 如果返回false,可能是CameraCallback没有注入,就调用了put方法,也可能是camera没有打开,或者camera服务异常。 * @param buffer 图像数据的byte array对象,不建议使用,大概率触发频繁GC动作 * @param config 图像数据的大小,旋转方向等。 * @return true 成功,false 失败 */ boolean putVideoFrame(@NonNull byte[] buffer, int size, @NonNull VideoFrameConfig config); @Deprecated void setPreviewFile(String filepath, VideoFrameConfig config); interface CameraCallback { /** * 三方应用调用系统Camera API打开Camera时候回调 * @param id camera id,0:Back后置,1:Front前置 */ void onStart(int id); /** * 暂不使用,忽略 * @param id */ void onResume(int id); /** * 暂不使用,忽略 * @param id */ void onPause(int id); /** * 三方应用调用系统Camera API关闭Camera时候回调 * @param id camera id,0:Back后置,1:Front前置 */ void onStop(int id); /** * 暂不使用,忽略 * @param id */ void onRequest(int id); } }
public class VideoFrameConfig implements Parcelable { public int width; // Frame width public int height; // Frame height public int format; // Frame format,current only support VideoFrameFormat.I420 public int rotation; // Frame rotation public int cameraId; // Frame source, default 0 }
public class VideoFrameFormat implements Parcelable { public static final int RGBA_8888 = PixelFormat.RGBA_8888; public static final int RGBX_8888 = PixelFormat.RGBX_8888; public static final int I420 = ImageFormat.YUV_420_888; //当前只支持这个格式 public static final int NV21 = ImageFormat.NV21; public static final int JPEG = ImageFormat.JPEG; public static final int RGB_888 = ImageFormat.FLEX_RGB_888; }
AudioProxy.h
// 每一帧pcm数据大小(目前支持48000Hz, 双声道、16位采样深度pcm音频数据) static const int FRAME_SIZE = 48000 * 2 * 2 /100; static AudioProxy *getInstance(); // 监听麦克风打开关闭回调(0-关闭,1-打开) typedef void (*Callback)(int); bool registerCallback(Callback cb); // 注入每一帧数据 size_t pushAudioFrame(const void* data);
CameraProxy.h
// 帧格式,目前只支持YUV I420 格式 static const int FRAME_FORMAT = 35; static CameraProxy *getInstance(); // 收到camera打开callback后,开始注入前,需要调用 bool prepare(); // 收到camera关闭callback后,需要调用 bool release(); // 监听Camera打开和关闭的回调,第一个参数代表开/关(0-关闭,1-打开),第二个参数是Camera的ID(0-后置摄像头,1-前置摄像头) typedef void (*CameraCallback)(int, int); bool registerCallback(CameraCallback cb); // 注入的每一帧数据(cameraid用回调里面的cameraid) size_t pushVideoFrame(const uint8_t* data, int size, int width, int height, int format, int rotation, int cameraId);