You need to enable JavaScript to run this app.
云手机

云手机

复制全文
音视频注入
音视频裸数据注入(Pod 注入)
复制全文
音视频裸数据注入(Pod 注入)

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

适用场景

使用 Proxy SDK 注入音视频适用的场景如下:

  • 视频文件合流注入 用户在后台运行服务,自行解码多个视频文件并将其合为一路视频流后,使用 Proxy SDK 将该路视频流注入到虚拟摄像头中。
  • 网络视频流注入 用户从网络源拉取 RTMP 等格式的在线视频流并完成解码,使用 Proxy SDK 将解码后的音视频数据注入虚拟摄像头和麦克风。
  • Unity 渲染画面注入 用户在后台运行 Unity 渲染引擎,将渲染完成的画面及其生成的音频通过 Proxy SDK 注入到虚拟摄像头和麦克风。
  • 自定义注入方案 Proxy SDK 支持用户根据特定需求,开发自定义的音视频注入场景。

前提条件
  • 具备旗舰型配置的实例资源。高负载场景请使用旗舰型配置的设备,避免因性能不足造成的卡顿、丢帧、杂音、黑屏等问题。
  • 需要音视频注入的应用程序已在控制台完成应用加签(适用于自主开发的应用,第三方应用无需关注)。

注意事项
  • 视频裸数据注入只支持竖屏模式下使用,横屏模式下会出现显示异常等不兼容情况。
  • Proxy SDK 仅接收 PCM 格式的音频帧,和 YUV-I420(默认)或 RGBA 格式的视频帧数据。非上述格式数据,在注入前请先进行解码转换,具体参考 Demo。
  • Proxy SDK 内部已实现音视频自动同步逻辑,仅需保证传入的音频帧与视频帧相匹配。
  • 三种音视频注入模式存在优先级和切换限制:
    • 本功能与客户端内部源音视频注入、客户端外部源音视频注入(Pod 注入)功能互斥,即云机实例同一时间只能接收一种注入源;
    • 当三种模式并存时,云机实例接收注入源的优先级为:客户端外部源音视频注入>音视频裸数据注入(Pod 注入)>客户端内部源音视频注入;
    • 若切换至其他注入源,请务必先关闭摄像头和麦克风,关闭当前正在注入的应用程序,否则会导致切换失败。

功能实现

Image

下载 Proxy SDK

下载 SDK 并拷贝至你的项目。

ProxySDK

语言

SDK 文件

Java

proxysdk-0.2.2.3.2.aar.zip
未知大小

C++

armeabi-v7a:

libvdevice_armeabi-v7a.so
173.45KB

arm64-v8a:
libvdevice_arm64-v8a.so
232.53KB

Unity 插件

专为 Unity 场景开发的用于采集 Unity Texture 的插件。需配合 Proxy SDK 一起使用。

injectplugin-release.aar.zip
745.73KB

集成 Proxy SDK

Proxy Java SDK

  1. 将解压后的 SDK 文件夹复制到项目的 libs 目录中。

  2. 配置项目依赖。打开项目的 build.gradle 文件,在 dependencies 代码块中添加以下配置:

    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar','*.aar'])
    }
    
  3. 同步项目。保存build.gradle文件的更改后,执行 gradle sync 即可。

Proxy C++ SDK

  1. 配置 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)
    
  2. 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} )
    
  3. 在目标库中使用 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 实现音视频裸数据注入功能时,可能会使用到以下三方库,请自行引入:

  • Ffmpeg:用于音视频格式处理
  • libyuv:用于 YUV 图像处理,如裁剪、旋转、格式转换

基础功能示例 Demo

Proxy Java 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);

方式二:自定义 AudioCallback 注入
// 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");

视频注入

注意

  • 使用 Proxy SDK 注入视频前,如正在使用云手机 Android SDK 注入外部采集视频,请先调用 setVideoInjectionState(false) 接口停止从客户端注入视频,否则会导致注入失败。
  • 调用 getCameraProxyManager 后,程序应暂停 200 毫秒(ms),以确保 CameraProxyManager 实例已正确初始化,然后再注册 CameraCallback 回调。
  1. 获取 ProxyContext
  2. 获取 CameraProxyManager
  3. 设置 CameraCallback,监听 onStart()onStop() 接口。
  4. 调用 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
            }
        }
    }
}

Proxy C++ SDK

C++_demo.cpp
4.51KB

高级功能示例 Demo

可参考以下示例,实现将解码视频文件后获取到的视频流数据注入至云机实例。

VirtCamera.zip
3.21MB

API 参考

Proxy Java SDK

CameraProxyManager

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);
    }
}

VideoFrameConfig

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
}

VideoFrameFormat

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;
}

Proxy C++ SDK

Audio 相关接口

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);

Camera 相关接口

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);
最近更新时间:2025.09.26 11:25:08
这个页面对您有帮助吗?
有用
有用
无用
无用