You need to enable JavaScript to run this app.
文档中心
云手机

云手机

复制全文
下载 pdf
音视频注入
设备数据注入
复制全文
下载 pdf
设备数据注入
火山引擎云手机支持设备数据注入能力,能够向云手机的麦克风(Mic)、摄像头(Camera)、传感器(Sensor)和定位数据(Location)注入数据,供云手机上运行的应用获取和使用。​
本文介绍如何使用火山引擎云手机设备数据注入 SDK,将获取到的 Mic、Camera、Sensor、Location 数据注入云手机对应的麦克风、摄像头、传感器和定位服务中。​
适用场景​
设备数据注入 SDK 适用于以下场景:​
  • 需要在云手机实例中模拟注入麦克风、摄像头、传感器、定位等设备数据。​
  • 在测试工具、自动化回放工具或系统级进程中执行数据注入,无需以普通应用方式运行。​
  • 调用方无法使用标准 Android 应用上下文,或不希望以应用形式启动相关服务。​
前提条件​
云手机实例需要使用 2026 年 8 月 4 日及之后发布的镜像文件,支持 AOSP 10~13,具体版本请参见 公共镜像发布历史。​
SDK 与 Demo 下载​
SDK​
下载 SDK 并拷贝至您的项目。​
​
​
deviceproxysdk-direct.jar
​
​
Demo​
​
​
app_process_device_proxy_injector.zip
​
​
  • 从 Demo 压缩包中获取编译脚本。请先下载 Demo 压缩包并解压,然后在解压后的根目录执行以下命令编译工具:​
  • ​
    ./build_device_proxy_injector.sh​
    ​
  • 编译完成后,产物路径为:​
  • ​
    ./build/device-proxy-injector.jar​
    ​
  • 使用以下命令进行验证测试:​
  • ​
    # 注入重力传感器数据(间隔20ms,随机生成数据注入,持续10s), demo支持通过参数指定sensor类型、周期、持续时间​
    CLASSPATH=/data/local/tmp/device-proxy-injector.jar app_process /system/bin com.ss.device.tools.injector.DeviceProxyInjectorMain sensor --sensor-type 9 --interval-ms 20 --duration-ms 10000 --verbose​
    ​
    # 给多个传感器持续注入随机数据​
    CLASSPATH=/data/local/tmp/device-proxy-injector.jar app_process /system/bin com.ss.device.tools.injector.DeviceProxyInjectorMain sensor --all-sensors --random --interval-ms 20 --duration-ms 10000 --verbose​
    ​
功能实现​
推荐使用 DeviceProxyManagers 获取四类数据通道,并在注入前检查通道是否连接成功。​
注意​
调用注入接口前,建议先调用 isConnected() 确认通道的连接状态。若返回 false,表示当前镜像版本不支持该功能,请升级至符合要求的镜像版本,请见 前提条件。​
​
import com.ss.device.proxy.direct.DeviceProxyManagers;​
​
boolean audioReady = DeviceProxyManagers.audio().isConnected();​
boolean cameraReady = DeviceProxyManagers.camera().isConnected();​
boolean sensorReady = DeviceProxyManagers.sensor().isConnected();​
boolean locationReady = DeviceProxyManagers.location().isConnected();​
​
​
方法​
返回类型​
用途​
DeviceProxyManagers.audio()​
DirectAudioProxyManager​
获取 Mic 数据通道,用于写入原始音频数据(PCM 格式)。​
DeviceProxyManagers.camera()​
DirectCameraProxyManager​
获取 Camera 数据通道,用于写入视频数据。​
DeviceProxyManagers.sensor()​
SensorProxyManager​
获取 Sensor 数据通道,用于写入结构化传感器数据。​
DeviceProxyManagers.location()​
LocationProxyManager​
获取 Location 数据通道,用于写入结构化定位数据。​
​
麦克风(Mic)数据注入​
数据格式​
Mic 数据注入使用原始音频格式(PCM)。使用前需先配置采样率、声道数和采样精度,再按帧持续写入音频数据。常见配置下,采样精度为 16 位(即每个采样点占 2 字节)。​
说明​
采样配置使用 AudioSampleConfig(sampleRateInHz, channels, formatInBytes)。例如,使用 PCM16 立体声 48 kHz 格式,可配置为 new AudioSampleConfig(48000, 2, 2)。​
​
boolean registerAudioCallback(IAudioCallback callback);​
boolean unregisterAudioCallback(IAudioCallback callback);​
void setAudioSampleConfigs(AudioSampleConfig config);​
boolean putAudioFrame(Bundle bundle);​
​
注意事项​
  • 注册回调和写入音频数据必须在同一个进程中执行,否则音频数据可能写入失败。​
  • 只有当云手机中的应用开始录音并触发 onStart 回调后,音频数据才会被正常接收。建议在收到 onStart 后开始发送数据,收到 onStop 后停止发送。​
  • 若使用 PCM16 立体声 48 kHz 格式(即使用双声道,每秒采样 48000 次、每个采样点占 2 字节的音频格式),每 20 ms 一帧的数据大小为 3840(48000 * 2 * 2 * 20 / 1000 = 3840)字节,具体可参考 SDK Demo 中的音频示例。​
示例代码​
​
/* ​
* Mic PCM 注入示例 ​
*/​
​
import android.os.Bundle;​
import android.os.SharedMemory;​
import java.nio.ByteBuffer;​
import com.ss.device.audio.AudioSampleConfig;​
import com.ss.device.audio.IAudioCallback;​
import com.ss.device.proxy.direct.DeviceProxyManagers;​
import com.ss.device.proxy.direct.DirectAudioProxyManager;​
​
DirectAudioProxyManager audio = DeviceProxyManagers.audio();​
if (!audio.isConnected()) {​
throw new IllegalStateException("device_proxy audio is not connected");​
}​
​
audio.setAudioSampleConfigs(new AudioSampleConfig(48000, 2, 2));​
​
IAudioCallback callback = new IAudioCallback.Stub() {​
@Override public void onStart() {}​
@Override public void onResume() {}​
@Override public void onPause() {}​
@Override public void onStop() {}​
@Override public void onRequest() {}​
};​
​
boolean registered = audio.registerAudioCallback(callback);​
if (!registered) {​
throw new IllegalStateException("register audio callback failed");​
}​
​
byte[] pcmFrame = readNextPcmFrame();​
SharedMemory memory = SharedMemory.create("adk_mic_frame", pcmFrame.length);​
ByteBuffer buffer = memory.mapReadWrite();​
buffer.put(pcmFrame);​
​
Bundle bundle = new Bundle();​
bundle.putParcelable("sharedMemory", memory);​
bundle.putInt("size", pcmFrame.length);​
boolean ok = audio.putAudioFrame(bundle);​
SharedMemory.unmap(buffer);​
memory.close();​
​
摄像头(Camera)数据注入​
数据格式​
Camera 数据注入按照视频帧(视频中的每一张图片)写入。每一帧都需要通过 VideoFrameConfig 说明分辨率、像素格式、旋转角度和目标摄像头。​
​
参数​
说明​
width / height​
视频帧分辨率,需与传入的图像数据大小保持一致。​
format​
视频帧像素格式,取值支持:RGBA_8888、RGBX_8888、I420、NV21、JPEG、RGB_888。​
rotation​
视频帧旋转角度。​
cameraId​
目标虚拟 camera id。​
​
​
VideoFrameConfig config = new VideoFrameConfig(​
1280,​
720,​
VideoFrameFormat.I420,​
0,​
0);​
​
注意事项​
  • 注册回调和写入图像数据必须在同一个进程中执行,否则图像数据可能写入失败。​
  • 只有当云手机中的应用开始使用摄像头并触发 onStart 回调后,才开始按固定帧率持续写入图像数据;收到 onStop 后停止写入。​
  • 传入的图像数据大小必须与像素格式和分辨率匹配,各格式对应的数据大小如下:​
  • ​
    像素格式​
    数据大小​
    I420 / NV21​
    width × height × 3 / 2 字节​
    RGBA / RGBX​
    width × height × 4 字节​
    RGB888​
    width × height × 3 字节​
    ​
示例代码​
​
/* ​
* Camera I420 注入示例​
*/​
​
import com.ss.device.camera.VideoFrameConfig;​
import com.ss.device.camera.VideoFrameFormat;​
import com.ss.device.proxy.camera.CameraProxyManager;​
import com.ss.device.proxy.direct.DeviceProxyManagers;​
​
CameraProxyManager camera = DeviceProxyManagers.camera();​
VideoFrameConfig config = new VideoFrameConfig(1280, 720, VideoFrameFormat.I420, 0, 0);​
​
CameraProxyManager.CameraCallback callback = new CameraProxyManager.CameraCallback() {​
@Override public void onStart(int id) {​
// start pushing frames​
}​
@Override public void onResume(int id) {}​
@Override public void onPause(int id) {}​
@Override public void onStop(int id) {​
// stop pushing frames​
}​
@Override public void onRequest(int id) {}​
};​
​
boolean registered = camera.registerCallback(callback);​
if (!registered) {​
throw new IllegalStateException("register camera callback failed");​
}​
​
byte[] i420 = new byte[1280 * 720 * 3 / 2];​
fillI420Frame(i420);​
boolean ok = camera.putVideoFrame(i420, i420.length, config);​
​
传感器(Sensor)数据注入​
推荐使用接口​
Sensor 推荐使用结构化数据模型 SensorEventData。您可以把它理解为“一条传感器事件”,其中包含传感器类型、传感器数值、精度和时间戳。旧接口 reportSensorData(String) 仍保留,但仅建议历史兼容场景使用。​
​
boolean reportSensorEvent(SensorEventData event);​
boolean reportSensorEvents(List<SensorEventData> events);​
​
参数说明​
系统会校验传感器类型与 values 长度是否匹配。若未传入时间戳或时间戳无效(即 timestamp 小于等于 0),系统将自动补充为当前时间。​
​
Sensor 类型​
最小 values 长度​
说明​
加速度 / 重力 / 线性加速度​
3​
x, y, z。​
磁场 / 方向 / 陀螺仪​
3​
x, y, z 或 azimuth, pitch, roll。​
旋转向量 / 游戏旋转向量 / 地磁旋转向量​
4​
通常为 x, y, z, w,部分类型可带 heading accuracy。​
未校准磁场 / 陀螺仪 / 加速度​
6​
uncalibrated + bias。​
其他类型​
1​
默认至少传入一个值。​
​
示例代码​
单条传感器事件注入示例​
​
import android.hardware.Sensor;​
import android.hardware.SensorManager;​
import android.os.SystemClock;​
import com.ss.device.proxy.direct.DeviceProxyManagers;​
import com.ss.device.proxy.sensor.SensorProxyManager;​
import com.ss.device.sensor.SensorEventData;​
​
SensorProxyManager sensor = DeviceProxyManagers.sensor();​
SensorEventData event = SensorEventData.of(​
Sensor.TYPE_GRAVITY,​
new float[]{0.0f, 0.0f, 7.8f},​
SensorManager.SENSOR_STATUS_ACCURACY_HIGH,​
SystemClock.elapsedRealtimeNanos());​
​
boolean ok = sensor.reportSensorEvent(event);​
​
批量传感器事件注入示例​
​
List<SensorEventData> events = new ArrayList<>();​
events.add(SensorEventData.of(Sensor.TYPE_ACCELEROMETER,​
new float[]{0.0f, 0.0f, 7.8f},​
SensorManager.SENSOR_STATUS_ACCURACY_HIGH,​
SystemClock.elapsedRealtimeNanos()));​
events.add(SensorEventData.of(Sensor.TYPE_GYROSCOPE,​
new float[]{0.01f, 0.02f, 0.03f},​
SensorManager.SENSOR_STATUS_ACCURACY_HIGH,​
SystemClock.elapsedRealtimeNanos()));​
​
boolean ok = DeviceProxyManagers.sensor().reportSensorEvents(events);​
​
定位数据(Location)数据注入​
推荐使用接口​
Location 推荐使用结构化数据模型 LocationEventData。基础定位点可使用 ofSample 快速构造;如果需要精细控制字段是否有效、NMEA 或 GNSS 卫星状态,可使用完整 of(...) 方法。​
​
boolean registerCallback(RequestCallback callback)​
boolean unregisterCallback(RequestCallback callback)​
boolean reportLocationEvent(LocationEventData event);​
boolean reportLocationEvents(List<LocationEventData> events);​
​
参数说明​
ofSample 默认会标记经纬度、海拔、水平精度、速度、航向和 elapsed realtime 均有效。若要表达未知海拔、未知速度或未知航向,不要用 0 表示未知值,应使用完整 LocationEventData.of(...) 并清除对应 flag。​
​
字段​
要求​
latitude​
设置 HAS_LAT_LONG 时,范围必须为 [-90, 90]。​
longitude​
设置 HAS_LAT_LONG 时,范围必须为 [-180, 180]。​
speedMetersPerSecond​
设置 HAS_SPEED 时,必须非负。​
bearingDegrees​
设置 HAS_BEARING 时,必须在 [0, 360) 范围内。​
各类 accuracy​
对应 flag 有效时,必须 finite 且非负。​
timestampMillis / elapsedRealtimeNanos​
必须非负;建议传入单调时间 elapsedRealtimeNanos。​
​
示例代码​
​
/* 注入单个 GPS 点​
* 1. 获取 LocationProxyManager​
* 2. 注册 RequestCallback​
* 3. 收到 onStart() 后开始注入​
* 4. 收到 onStop() 后停止注入​
* 5. 退出时注销 callback​
*/​
​
package com.example.location;​
​
import android.os.SystemClock;​
​
import com.ss.device.location.LocationEventData;​
import com.ss.device.proxy.direct.DeviceProxyManagers;​
import com.ss.device.proxy.location.LocationProxyManager;​
​
import java.util.concurrent.atomic.AtomicBoolean;​
​
public final class LocationInjectionExample {​
private static final long INJECT_INTERVAL_MS = 1000L;​
​
private LocationInjectionExample() {​
}​
​
public static void main(String[] args) {​
LocationProxyManager locationManager = DeviceProxyManagers.location();​
AtomicBoolean shouldInject = new AtomicBoolean(false);​
​
LocationProxyManager.RequestCallback callback =​
new LocationProxyManager.RequestCallback() {​
@Override​
public void onStart(String provider, int type, int repeat) {​
System.out.println("Location injection started"​
+ ", provider=" + provider​
+ ", type=" + type);​
shouldInject.set(true);​
}​
​
@Override​
public void onStop(String provider, int type) {​
System.out.println("Location injection stopped"​
+ ", provider=" + provider​
+ ", type=" + type);​
shouldInject.set(false);​
}​
};​
​
if (!locationManager.registerCallback(callback)) {​
throw new IllegalStateException(​
"Failed to register location request callback");​
}​
​
Runtime.getRuntime().addShutdownHook(new Thread(() -> {​
shouldInject.set(false);​
locationManager.unregisterCallback(callback);​
}));​
​
injectLoop(locationManager, shouldInject);​
}​
​
private static void injectLoop(LocationProxyManager locationManager,​
AtomicBoolean shouldInject) {​
int sequence = 0;​
​
while (true) {​
if (shouldInject.get()) {​
sequence++;​
​
LocationEventData event = LocationEventData.ofSample(​
sequence,​
SystemClock.elapsedRealtimeNanos(),​
31.2304, // latitude​
121.4737, // longitude​
10.0, // altitude meters​
3.0f, // accuracy meters​
0.0f, // speed meters/second​
0.0f // bearing degrees​
);​
​
boolean accepted = locationManager.reportLocationEvent(event);​
System.out.println("Report location event, accepted=" + accepted);​
}​
​
try {​
Thread.sleep(INJECT_INTERVAL_MS);​
} catch (InterruptedException e) {​
Thread.currentThread().interrupt();​
return;​
}​
}​
}​
}​
​
调试与验证​
接入自查清单​
  • 确认镜像版本支持设备数据注入 SDK。​
  • 调用 DeviceProxyManagers.*().isConnected(),并处理 false 返回值。​
  • Mic / Camera 在 callback onStart 后开始推流,在 onStop 后停止。​
  • Camera buffer 大小与 VideoFrameConfig 的 format、width、height 匹配。​
  • Sensor 使用 SensorEventData,并保证 values 长度满足 sensorType 要求。​
  • Location 使用 LocationEventData,并用 flags 表达字段有效性。​
  • 正式接入前,补齐所需系统权限规则。​
处理建议​
​
返回值​
处理建议​
isConnected() == false​
数据通道不可用,调用方不应继续注入。请检查镜像版本和服务状态。​
reportSensorEvent(s) == false​
可能是注入数据不合法、传感器服务不可用或数据写入失败。请检查 sensorType、values 长度和服务状态。​
reportLocationEvent(s) == false​
可能是注入数据不合法、定位服务不可用或数据写入失败。请检查经纬度、速度、航向、精度和时间字段。​
putVideoFrame == false​
可能是摄像头未被应用打开、写入进程不一致、共享内存失败或 buffer / config 不合法。​
putAudioFrame == false​
可能是 Mic 未处于录音状态、写入进程不一致、共享内存失败或 PCM 数据不合法。​
​
常见问题​
Q1:为什么已经调用注入方法,但应用侧没有收到数据?​
A1:Mic 和 Camera 只有在云手机中的应用开始使用对应虚拟设备后才会消费数据。请确认已成功注册 callback,并在 onStart 后开始推流,在 onStop 后停止推流。​
Q2:如何判断 Sensor 或 Location 数据是否合法?​
A2:Sensor 需要保证 values 长度满足 sensorType 要求;Location 需要通过 flags 正确表达字段有效性,并保证经纬度、速度、航向、精度和时间字段满足取值范围。​
最近更新时间:2026.09.03 14:47:16
这个页面对您有帮助吗?
有用
有用
无用
无用