diff --git a/crates/wzp-video/src/mediacodec.rs b/crates/wzp-video/src/mediacodec.rs index b6e22a5..df87326 100644 --- a/crates/wzp-video/src/mediacodec.rs +++ b/crates/wzp-video/src/mediacodec.rs @@ -65,7 +65,7 @@ impl MediaCodecEncoder { format.set_i32("bitrate", bitrate_bps as i32); format.set_i32("frame-rate", 30); format.set_i32("i-frame-interval", 1); - format.set_i32("color-format", COLOR_FORMAT_YUV420_PLANAR); + format.set_i32("color-format", COLOR_FORMAT_YUV420_SEMIPLANAR); let codec = MediaCodec::from_encoder_type("video/avc").ok_or_else(|| { VideoError::PlatformError("AMediaCodec_createEncoderByType failed".into()) @@ -122,10 +122,11 @@ impl VideoEncoder for MediaCodecEncoder { } else { 0 }; + let input = i420_to_nv12(&frame.data, self.width as usize, self.height as usize)?; let to_copy = { let buf = buffer.buffer_mut(); - let n = frame.data.len().min(buf.len()); - for (d, &s) in buf[..n].iter_mut().zip(frame.data[..n].iter()) { + let n = input.len().min(buf.len()); + for (d, &s) in buf[..n].iter_mut().zip(input[..n].iter()) { d.write(s); } n @@ -1149,6 +1150,31 @@ fn i420_len(width: usize, height: usize) -> Result { }) } +#[cfg(target_os = "android")] +fn i420_to_nv12(src: &[u8], width: usize, height: usize) -> Result, VideoError> { + let y_size = width + .checked_mul(height) + .ok_or_else(|| VideoError::InvalidInput(format!("invalid frame dimensions {width}x{height}")))?; + let uv_size = y_size / 4; + let expected = y_size + uv_size * 2; + if src.len() < expected { + return Err(VideoError::InvalidInput(format!( + "I420 frame too small for NV12 conversion: {} bytes, expected {expected}", + src.len() + ))); + } + + let mut out = vec![0u8; expected]; + out[..y_size].copy_from_slice(&src[..y_size]); + let u = &src[y_size..y_size + uv_size]; + let v = &src[y_size + uv_size..y_size + uv_size * 2]; + for i in 0..uv_size { + out[y_size + i * 2] = u[i]; + out[y_size + i * 2 + 1] = v[i]; + } + Ok(out) +} + #[cfg(target_os = "android")] fn yuv420_planar_to_tight_i420( src: &[u8], diff --git a/desktop/src-tauri/src/engine.rs b/desktop/src-tauri/src/engine.rs index 4c220c3..137d716 100644 --- a/desktop/src-tauri/src/engine.rs +++ b/desktop/src-tauri/src/engine.rs @@ -1958,6 +1958,22 @@ impl CallEngine { }), ); } + if should_log_video_sample(camera_frames, false) { + crate::emit_call_debug( + &vid_app, + "video:camera_frame_sample", + serde_json::json!({ + "t_ms": vid_t0.elapsed().as_millis() as u64, + "codec": format!("{:?}", vid_codec), + "frame_no": camera_frames, + "width": f.width, + "height": f.height, + "data_bytes": f.data.len(), + "platform": "android", + "i420_sample": i420_sample(&f.data, f.width, f.height), + }), + ); + } f } Ok(None) => break, @@ -3099,6 +3115,22 @@ impl CallEngine { }), ); } + if should_log_video_sample(camera_frames, false) { + crate::emit_call_debug( + &vid_app, + "video:camera_frame_sample", + serde_json::json!({ + "t_ms": vid_t0.elapsed().as_millis() as u64, + "codec": format!("{:?}", vid_codec), + "frame_no": camera_frames, + "width": f.width, + "height": f.height, + "data_bytes": f.data.len(), + "platform": "desktop", + "i420_sample": i420_sample(&f.data, f.width, f.height), + }), + ); + } f } Ok(None) => break, // sender dropped