Kotlin中视频帧时间戳与旋转向量传感器数据同步问题
问题背景
功能实现
- 视频采集:保存视频并将每帧时间戳存入.csv文件
- 旋转向量传感器数据采集:将yaw、pitch、roll值及时间戳存入另一.csv文件
配置参数
- 视频采集帧率:30Hz
- 旋转向量更新频率:250Hz(线程写入文件频率约80Hz)
核心问题
尝试关联视频帧与对应传感器数据时,时间戳无法直接对齐,需给视频时间戳手动添加约160ms偏移才能同步。两者时间戳均通过System.currentTimeMillis()捕获,理论上应具备可比性,但延迟来源不明。
需要解答:
VideoRecordEvent.Status返回的帧采集时间是否不准确?- 视频与传感器时间戳存在160ms差异的原因是什么?
- 是否有更精准的延迟校正或补偿方法?
代码片段
视频及时间戳采集代码
val enableAudio = false recording = videoCapture.output .prepareRecording(this, mediaStoreOutput) .apply { if (enableAudio && ContextCompat.checkSelfPermission( this@MainActivity, Manifest.permission.RECORD_AUDIO ) == PackageManager.PERMISSION_GRANTED ) { withAudioEnabled() } } .start(ContextCompat.getMainExecutor(this)) { recordEvent -> when (recordEvent) { is VideoRecordEvent.Start -> { base_time = System.currentTimeMillis() isRecordingVideo = true } is VideoRecordEvent.Status -> { val time = recordEvent.recordingStats.recordedDurationNanos / 1_000_000 val uriTimestampCopy = uriTimestamp if(uriTimestampCopy != null){ addTimestampDataToCsv(uriTimestampCopy, base_time+time) } } is VideoRecordEvent.Finalize -> { isRecordingVideo = false } } }
旋转向量数据及时间戳采集代码
private fun CSV_capture(){ val executor = Executors.newSingleThreadExecutor() executor.execute { val uriRotVectorCopy = uriRotVector while(isRecord && (uriRotVectorCopy != null)) { addRotationDataToCsv(uriRotVectorCopy) } } } fun addRotationDataToCsv(uri: Uri) { val Lyaw = myaw val Lpitch = mpitch val Lroll = mroll val timestamp = System.currentTimeMillis() val resolver = contentResolver try { val outputStream: OutputStream? = resolver.openOutputStream(uri, "wa") outputStream?.let { stream -> val data = "$Lyaw,$Lpitch,$Lroll,$timestamp\n" stream.write(data.toByteArray()) stream.flush() stream.close() } } catch (e: Exception) { e.printStackTrace() } }
已尝试方法
- 捕获每帧视频后立即保存旋转向量数据
- 降低相机分辨率并禁用音频采集以减少延迟
以上尝试均未改善,视频帧时间戳仍与实际帧时间不符。
问题解答
1. VideoRecordEvent.Status返回的帧采集时间是否不准确?
是,这个时间戳并非实际帧捕获的硬件时间。recordedDurationNanos是从录制开始到当前累计的编码输出时长,代表的是视频文件中已写入内容的时长,而非传感器数据那种“事件发生瞬间”的时间戳。另外,VideoRecordEvent.Status在主线程回调,从帧被硬件捕获、编码,到回调触发,中间经过了处理和队列等待,这个过程的时间差会导致它和实际帧捕获时间存在偏差。
2. 视频与传感器时间戳存在160ms差异的原因是什么?
主要来自几个叠加的延迟环节:
- 视频处理流水线延迟:从相机传感器捕获帧,到ISP处理(降噪、色彩校正)、编码器编码,再到
VideoRecordEvent.Status回调触发,整个流水线会产生几十到上百毫秒的延迟,不同设备硬件性能差异会导致这个值波动。 - 线程调度延迟:传感器数据在后台线程写入文件,但视频回调在主线程执行,主线程可能被UI操作阻塞,进一步拉大时间差。
- 时间戳基准不一致:你用
System.currentTimeMillis()作为视频时间的基准,但recordedDurationNanos是相机内部计时,和系统时钟可能存在微小偏移,叠加后就会出现明显差值。 - 文件写入异步性:视频帧写入媒体库是异步操作,
Status回调触发时,帧可能还没真正写入磁盘,但你已经记录了时间戳;而传感器数据是立即写入,两者的“记录时间”对应的实际事件时间本来就不对齐。
3. 是否有更精准的延迟校正或补偿方法?
可以尝试以下几种方案:
(1)使用相机帧的硬件时间戳
放弃VideoRecordEvent.Status的时长计算,改用ImageAnalysis获取相机帧的原始时间戳。通过ImageProxy.getTimestamp()可以拿到传感器捕获帧的硬件时间(纳秒级,基于系统单调时钟),这个时间戳和传感器事件的event.timestamp基准一致,可直接对齐。
示例思路:
val imageAnalyzer = ImageAnalysis.Builder() .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() .also { it.setAnalyzer(executor) { imageProxy -> val frameTimestamp = imageProxy.timestamp // 硬件捕获时间,纳秒 // 将时间戳转换为毫秒存入CSV addTimestampDataToCsv(uriTimestamp, frameTimestamp / 1_000_000) imageProxy.close() } } // 将imageAnalyzer绑定到相机会话 cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalyzer, videoCapture)
(2)统一时间戳基准
传感器数据不要用System.currentTimeMillis(),改用传感器事件自带的event.timestamp(单调时钟,纳秒),视频帧也用ImageProxy.getTimestamp(),两者基于同一时钟源,避免系统时钟的偏移问题。
(3)离线校准延迟
录制一段包含明确同步信号的视频(比如快速闪烁的LED,同时触发传感器记录),离线分析视频帧中LED亮起的时间点和传感器中对应信号的时间点,计算出固定延迟值,后续录制时直接套用这个校准后的偏移。
(4)优化线程调度
传感器数据写入不要每次都打开/关闭输出流,保持流打开状态以减少IO操作延迟;同时用更高优先级的线程处理传感器数据,避免被其他任务阻塞。
内容的提问来源于stack exchange,提问作者Lucas Reis

