diff --git a/InstantReplay.Externals/unienc/crates/unienc_android_mc/src/common.rs b/InstantReplay.Externals/unienc/crates/unienc_android_mc/src/common.rs index cf23690d..26160563 100644 --- a/InstantReplay.Externals/unienc/crates/unienc_android_mc/src/common.rs +++ b/InstantReplay.Externals/unienc/crates/unienc_android_mc/src/common.rs @@ -2,7 +2,7 @@ use bincode::{Decode, Encode}; use jni::{ JNIEnv, objects::{JObject, JString}, - sys::{jint, jlong}, + sys::{jfloat, jint, jlong}, }; use std::future::Future; use std::pin::Pin; @@ -496,6 +496,18 @@ pub fn set_format_integer( Ok(()) } +/// Set float parameter on MediaFormat +pub fn set_format_float( + env: &mut JNIEnv, + format: &JObject, + key: &str, + value: jfloat, +) -> Result<()> { + let key_str = to_java_string(env, key)?; + bindings::MediaFormat::set_float(env, format, &key_str, value)?; + Ok(()) +} + #[derive(Encode, Decode)] pub struct CommonEncodedData { pub content: CommonEncodedDataContent, diff --git a/InstantReplay.Externals/unienc/crates/unienc_android_mc/src/video/mod.rs b/InstantReplay.Externals/unienc/crates/unienc_android_mc/src/video/mod.rs index 0d1e8a7c..f6b9c13f 100644 --- a/InstantReplay.Externals/unienc/crates/unienc_android_mc/src/video/mod.rs +++ b/InstantReplay.Externals/unienc/crates/unienc_android_mc/src/video/mod.rs @@ -1,4 +1,7 @@ -use jni::{JNIEnv, sys::jint}; +use jni::{ + JNIEnv, + sys::{jfloat, jint}, +}; use std::sync::Arc; use std::time::Duration; use unienc_common::{ @@ -35,6 +38,7 @@ struct UninitializedState { tx: tokio::sync::oneshot::Sender<()>, bitrate: u32, fps_hint: u32, + idr_interval_seconds: f32, } enum MediaCodecVideoEncoderInputProcessor { @@ -138,6 +142,7 @@ impl MediaCodecVideoEncoder { tx, bitrate: options.bitrate(), fps_hint: options.fps_hint(), + idr_interval_seconds: options.idr_interval_seconds(), }, ), runtime: runtime.clone(), @@ -186,6 +191,7 @@ async fn push_video_impl( this.padded_height, state.bitrate, state.fps_hint, + state.idr_interval_seconds, false, // use_surface = false for buffer mode )?; this.codec.configure(&format)?; @@ -272,6 +278,7 @@ async fn push_video_impl( this.padded_height, state.bitrate, state.fps_hint, + state.idr_interval_seconds, true, // use_surface = true for hardware buffer mode )?; this.codec.configure(&format)?; @@ -372,6 +379,7 @@ fn create_video_format_raw( padded_height: u32, bitrate: u32, fps_hint: u32, + idr_interval_seconds: f32, use_surface: bool, ) -> Result { let mime = to_java_string(env, MIME_TYPE_VIDEO_AVC)?; @@ -396,7 +404,14 @@ fn create_video_format_raw( set_format_integer(env, &format_obj, KEY_BITRATE, bitrate as jint)?; set_format_integer(env, &format_obj, KEY_FRAME_RATE, fps_hint as jint)?; - set_format_integer(env, &format_obj, KEY_I_FRAME_INTERVAL, 1)?; + // MediaFormat.KEY_I_FRAME_INTERVAL accepts a float since API 25, and `min_api` is 26, so the + // sub-second range is available without a version guard. + set_format_float( + env, + &format_obj, + KEY_I_FRAME_INTERVAL, + idr_interval_seconds as jfloat, + )?; set_format_integer(env, &format_obj, KEY_PRIORITY, 0)?; set_format_integer(env, &format_obj, KEY_OPERATING_RATE, fps_hint as jint)?; diff --git a/InstantReplay.Externals/unienc/crates/unienc_apple_vt/src/video/mod.rs b/InstantReplay.Externals/unienc/crates/unienc_apple_vt/src/video/mod.rs index 116a7e1c..d214e406 100644 --- a/InstantReplay.Externals/unienc/crates/unienc_apple_vt/src/video/mod.rs +++ b/InstantReplay.Externals/unienc/crates/unienc_apple_vt/src/video/mod.rs @@ -14,7 +14,8 @@ use objc2_core_video::{CVPixelBuffer, CVPixelBufferCreateWithBytes, kCVPixelForm use objc2_video_toolbox::{ VTCompressionSession, VTEncodeInfoFlags, VTSessionSetProperty, kVTCompressionPropertyKey_AllowFrameReordering, kVTCompressionPropertyKey_AverageBitRate, - kVTCompressionPropertyKey_RealTime, kVTInvalidSessionErr, + kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, kVTCompressionPropertyKey_RealTime, + kVTInvalidSessionErr, }; use tokio::sync::mpsc; use unienc_common::{ @@ -44,6 +45,7 @@ pub struct VideoToolboxEncoderInput { width: u32, height: u32, bitrate: u32, + idr_interval_seconds: f32, } struct CompressionSession { @@ -279,7 +281,12 @@ impl EncoderInput for VideoToolboxEncoderInput { // VTCompressionSession turns invalid when the app enters background on iOS; retry // once with a fresh session, re-passing the same reserved permit. retry += 1; - match CompressionSession::new(self.width, self.height, self.bitrate) { + match CompressionSession::new( + self.width, + self.height, + self.bitrate, + self.idr_interval_seconds, + ) { Ok(session) => { self.session = session; continue; @@ -335,7 +342,7 @@ impl Drop for VideoToolboxEncoderInput { } impl CompressionSession { - fn new(width: u32, height: u32, bitrate: u32) -> Result { + fn new(width: u32, height: u32, bitrate: u32, idr_interval_seconds: f32) -> Result { let mut session: *mut VTCompressionSession = std::ptr::null_mut(); unsafe { @@ -382,6 +389,14 @@ impl CompressionSession { ) } .to_result()?; + unsafe { + VTSessionSetProperty( + &session, + kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, + Some(&CFNumber::new_f64(idr_interval_seconds as f64)), + ) + } + .to_result()?; Ok(CompressionSession { inner: session }) } @@ -393,14 +408,16 @@ impl VideoToolboxEncoder { let tx = Box::new(tx); let (width, height, bitrate) = (options.width(), options.height(), options.bitrate()); + let idr_interval_seconds = options.idr_interval_seconds(); Ok(VideoToolboxEncoder { input: VideoToolboxEncoderInput { - session: CompressionSession::new(width, height, bitrate)?, + session: CompressionSession::new(width, height, bitrate, idr_interval_seconds)?, tx, width, height, bitrate, + idr_interval_seconds, }, output: VideoToolboxEncoderOutput { rx }, }) diff --git a/InstantReplay.Externals/unienc/crates/unienc_c/src/types.rs b/InstantReplay.Externals/unienc/crates/unienc_c/src/types.rs index 49b1f3db..1e78dd77 100644 --- a/InstantReplay.Externals/unienc/crates/unienc_c/src/types.rs +++ b/InstantReplay.Externals/unienc/crates/unienc_c/src/types.rs @@ -26,6 +26,9 @@ pub struct VideoEncoderOptionsNative { pub height: u32, pub fps_hint: u32, pub bitrate: u32, + /// Maximum interval between IDR (key) frames, in seconds. Required; the managed side rejects + /// non-positive and non-finite values before the struct crosses the boundary. + pub idr_interval_seconds: f32, } #[repr(C)] @@ -52,6 +55,10 @@ impl VideoEncoderOptions for VideoEncoderOptionsNative { fn bitrate(&self) -> u32 { self.bitrate } + + fn idr_interval_seconds(&self) -> f32 { + self.idr_interval_seconds + } } impl AudioEncoderOptions for AudioEncoderOptionsNative { diff --git a/InstantReplay.Externals/unienc/crates/unienc_common/src/lib.rs b/InstantReplay.Externals/unienc/crates/unienc_common/src/lib.rs index 44262da7..ef056e7f 100644 --- a/InstantReplay.Externals/unienc/crates/unienc_common/src/lib.rs +++ b/InstantReplay.Externals/unienc/crates/unienc_common/src/lib.rs @@ -95,6 +95,11 @@ pub trait VideoEncoderOptions: Clone + Copy { fn height(&self) -> u32; fn fps_hint(&self) -> u32; fn bitrate(&self) -> u32; + + /// Maximum interval between IDR (key) frames, in seconds. Must be finite and greater than + /// zero; every platform encoder is configured with it, so there is no "leave it to the + /// platform" case to represent. + fn idr_interval_seconds(&self) -> f32; } pub trait AudioEncoderOptions: Clone + Copy { diff --git a/InstantReplay.Externals/unienc/crates/unienc_ffmpeg/src/video/mod.rs b/InstantReplay.Externals/unienc/crates/unienc_ffmpeg/src/video/mod.rs index a9408c30..0a92a417 100644 --- a/InstantReplay.Externals/unienc/crates/unienc_ffmpeg/src/video/mod.rs +++ b/InstantReplay.Externals/unienc/crates/unienc_ffmpeg/src/video/mod.rs @@ -132,6 +132,8 @@ impl FFmpegVideoEncoder { let height = options.height(); let cfr = options.fps_hint(); + let idr_interval_seconds = options.idr_interval_seconds(); + // encode raw BGRA frames into H.264 stream let mut spawned = ffmpeg::Builder::new() .use_stdin(true) @@ -166,7 +168,7 @@ impl FFmpegVideoEncoder { "-b:v", &format!("{}", options.bitrate()), "-force_key_frames", - "expr:gte(t,n_forced*1)", + &format!("expr:gte(t,n_forced*{idr_interval_seconds})"), ], ffmpeg::Destination::Stdout, )?; diff --git a/InstantReplay.Externals/unienc/crates/unienc_testkit/src/options.rs b/InstantReplay.Externals/unienc/crates/unienc_testkit/src/options.rs index 9a9481ce..68aa0b23 100644 --- a/InstantReplay.Externals/unienc/crates/unienc_testkit/src/options.rs +++ b/InstantReplay.Externals/unienc/crates/unienc_testkit/src/options.rs @@ -1,11 +1,16 @@ use crate::e2e::E2eConfig; +/// Interval the backends applied before it became configurable, kept so that the harness exercises +/// the same key-frame spacing it always has. +const DEFAULT_IDR_INTERVAL_SECONDS: f32 = 1.0; + #[derive(Debug, Clone, Copy)] pub struct TestVideoOptions { pub width: u32, pub height: u32, pub fps_hint: u32, pub bitrate: u32, + pub idr_interval_seconds: f32, } impl From<&E2eConfig> for TestVideoOptions { @@ -15,6 +20,7 @@ impl From<&E2eConfig> for TestVideoOptions { height: config.height, fps_hint: config.fps, bitrate: config.video_bitrate, + idr_interval_seconds: DEFAULT_IDR_INTERVAL_SECONDS, } } } @@ -35,6 +41,10 @@ impl unienc_common::VideoEncoderOptions for TestVideoOptions { fn bitrate(&self) -> u32 { self.bitrate } + + fn idr_interval_seconds(&self) -> f32 { + self.idr_interval_seconds + } } #[derive(Debug, Clone, Copy)] diff --git a/InstantReplay.Externals/unienc/crates/unienc_webcodecs/src/video/mod.rs b/InstantReplay.Externals/unienc/crates/unienc_webcodecs/src/video/mod.rs index e3a48ff4..fe1bf2f8 100644 --- a/InstantReplay.Externals/unienc/crates/unienc_webcodecs/src/video/mod.rs +++ b/InstantReplay.Externals/unienc/crates/unienc_webcodecs/src/video/mod.rs @@ -20,6 +20,7 @@ pub struct WebCodecsVideoEncoderInput { fps_hint: f64, tx: mpsc::Sender, prev_key_timestamp: Option, + idr_interval_seconds: f64, runtime: R, } @@ -49,6 +50,8 @@ impl WebCodecsVideoEncoder { encoder_handle: None, tx, prev_key_timestamp: None, + // WebCodecs has no GOP-length knob, so keyframes are requested per frame. + idr_interval_seconds: options.idr_interval_seconds() as f64, runtime: runtime.clone(), }, output: WebCodecsVideoEncoderOutput { rx }, @@ -108,17 +111,12 @@ impl EncoderInput for WebCodecsVideoEncoderInput { Some(prev) => data.timestamp - prev, None => f64::INFINITY, }; - if since_prev_key >= 1.0 { + let is_key = since_prev_key >= self.idr_interval_seconds; + if is_key { self.prev_key_timestamp = Some(data.timestamp); } encoder_handle - .push_video_frame( - pixels, - frame.width, - frame.height, - data.timestamp, - since_prev_key >= 1.0, - ) + .push_video_frame(pixels, frame.width, frame.height, data.timestamp, is_key) .context("Failed to push video frame to WebCodecs EncoderHandle")?; Ok(()) } diff --git a/InstantReplay.Externals/unienc/crates/unienc_windows_mf/src/audio/mod.rs b/InstantReplay.Externals/unienc/crates/unienc_windows_mf/src/audio/mod.rs index 90b54567..bb6be587 100644 --- a/InstantReplay.Externals/unienc/crates/unienc_windows_mf/src/audio/mod.rs +++ b/InstantReplay.Externals/unienc/crates/unienc_windows_mf/src/audio/mod.rs @@ -53,6 +53,7 @@ impl MediaFoundationAudioEncoder { }, input_type, output_type, + &|_| {}, runtime, )?; diff --git a/InstantReplay.Externals/unienc/crates/unienc_windows_mf/src/mft.rs b/InstantReplay.Externals/unienc/crates/unienc_windows_mf/src/mft.rs index f910182b..c42a5fd1 100644 --- a/InstantReplay.Externals/unienc/crates/unienc_windows_mf/src/mft.rs +++ b/InstantReplay.Externals/unienc/crates/unienc_windows_mf/src/mft.rs @@ -239,12 +239,16 @@ enum Pipeline { } impl Transform { + /// `configure` runs on each candidate MFT after it is activated but before the media types are + /// negotiated. It is for optional codec properties: anything it fails to apply must leave the + /// transform usable, so it cannot report an error. pub fn new( category: windows_core::GUID, input: MFT_REGISTER_TYPE_INFO, output: MFT_REGISTER_TYPE_INFO, input_type: IMFMediaType, output_type: IMFMediaType, + configure: &dyn Fn(&IMFTransform), runtime: &impl Runtime, ) -> Result<(Self, mpsc::Receiver>)> { let mfts = MftIter::new(category, input, output); @@ -259,7 +263,13 @@ impl Transform { println!("Skipping MFT: {}", Self::get_name(&activate)?); continue; } - match Self::try_activate(activate, &mut input_type, &mut output_type, runtime) { + match Self::try_activate( + activate, + &mut input_type, + &mut output_type, + configure, + runtime, + ) { Ok(r) => { result = Some(r); } @@ -290,6 +300,7 @@ impl Transform { activate: IMFActivate, input_type: &mut Option, output_type: &mut Option, + configure: &dyn Fn(&IMFTransform), runtime: &impl Runtime, ) -> Result<(Self, mpsc::Receiver>)> { println!("Trying MFT: {}", Self::get_name(&activate)?); @@ -297,6 +308,8 @@ impl Transform { let is_async = unsafe { activate.GetUINT32(&MF_TRANSFORM_ASYNC) }.unwrap_or(0) != 0; let transform = unsafe { activate.ActivateObject::()? }; + configure(&transform); + if is_async { let attributes = unsafe { transform.GetAttributes()? }; unsafe { attributes.SetUINT32(&MF_TRANSFORM_ASYNC_UNLOCK, 1)? }; diff --git a/InstantReplay.Externals/unienc/crates/unienc_windows_mf/src/video/mod.rs b/InstantReplay.Externals/unienc/crates/unienc_windows_mf/src/video/mod.rs index b33708f1..c36373d8 100644 --- a/InstantReplay.Externals/unienc/crates/unienc_windows_mf/src/video/mod.rs +++ b/InstantReplay.Externals/unienc/crates/unienc_windows_mf/src/video/mod.rs @@ -6,6 +6,8 @@ use unienc_common::{ UnsupportedBlitData, VideoEncoderOptions, VideoFrame, VideoSample, }; use windows::Win32::Media::MediaFoundation::*; +use windows::Win32::System::Variant::{VARIANT, VT_UI4}; +use windows::core::Interface; use crate::common::*; use crate::mft::Transform; @@ -48,6 +50,29 @@ impl MediaFoundationVideoEncoder { output_type }; + // Media Foundation expresses the GOP length in frames, so the interval has to be resolved + // against the frame rate hint. `CODECAPI_AVEncMPVGOPSize` is optional for an MFT to + // implement, so an encoder that rejects it keeps its own GOP length rather than failing to + // activate. + let gop_size = ((options.idr_interval_seconds() as f64 * options.fps_hint() as f64).round() + as u32) + .max(1); + let configure = |transform: &IMFTransform| { + let Ok(codec_api) = transform.cast::() else { + println!("MFT does not expose ICodecAPI; leaving the GOP size at its default"); + return; + }; + let mut value = VARIANT::default(); + unsafe { + let inner = &mut value.Anonymous.Anonymous; + inner.vt = VT_UI4; + inner.Anonymous.ulVal = gop_size; + if let Err(err) = codec_api.SetValue(&CODECAPI_AVEncMPVGOPSize, &value) { + println!("Failed to set CODECAPI_AVEncMPVGOPSize to {gop_size}: {err:?}"); + } + } + }; + let (transform, output_rx) = Transform::new( MFT_CATEGORY_VIDEO_ENCODER, MFT_REGISTER_TYPE_INFO { @@ -60,6 +85,7 @@ impl MediaFoundationVideoEncoder { }, input_type, output_type, + &configure, runtime, )?; diff --git a/Packages/jp.co.cyberagent.instant-replay/Runtime/Pipeline/BoundedEncodedFrameBuffer.cs b/Packages/jp.co.cyberagent.instant-replay/Runtime/Pipeline/BoundedEncodedFrameBuffer.cs index 6c98ef7f..8737ba40 100644 --- a/Packages/jp.co.cyberagent.instant-replay/Runtime/Pipeline/BoundedEncodedFrameBuffer.cs +++ b/Packages/jp.co.cyberagent.instant-replay/Runtime/Pipeline/BoundedEncodedFrameBuffer.cs @@ -108,6 +108,14 @@ public bool TryAddAudioFrame(EncodedFrame frame) /// /// Gets frames for the specified duration, adjusted to start from a keyframe. /// + /// + /// The segment can only begin at a keyframe, so the IDR interval bounds how precisely the requested + /// duration can be honoured, and no frames at all are returned while the buffer holds no keyframe. Raising + /// beyond the retained duration therefore + /// yields an empty export. Eviction also works one frame at a time rather than one group of pictures at a + /// time, so the frames preceding the oldest surviving keyframe are retained but unusable, and a longer IDR + /// interval leaves a larger share of the memory budget unusable. + /// public void GetFramesForDuration(double? durationSeconds, out ReadOnlyMemory videoFrames, out ReadOnlyMemory audioFrames) { diff --git a/Packages/jp.co.cyberagent.instant-replay/Runtime/Pipeline/RealtimeEncodingOptions.cs b/Packages/jp.co.cyberagent.instant-replay/Runtime/Pipeline/RealtimeEncodingOptions.cs index 9fdbed1b..41b65a4f 100644 --- a/Packages/jp.co.cyberagent.instant-replay/Runtime/Pipeline/RealtimeEncodingOptions.cs +++ b/Packages/jp.co.cyberagent.instant-replay/Runtime/Pipeline/RealtimeEncodingOptions.cs @@ -64,7 +64,8 @@ public int AudioInputQueueSize Width = 1280, Height = 720, FpsHint = 30, - Bitrate = 2500000 // 2.5 Mbps + Bitrate = 2500000, // 2.5 Mbps + IdrIntervalSeconds = 1f }, AudioOptions = new AudioEncoderOptions { diff --git a/Packages/jp.co.cyberagent.instant-replay/UniEnc/Runtime/Generated/NativeMethods.g.cs b/Packages/jp.co.cyberagent.instant-replay/UniEnc/Runtime/Generated/NativeMethods.g.cs index 3d87b392..7e5f5295 100644 --- a/Packages/jp.co.cyberagent.instant-replay/UniEnc/Runtime/Generated/NativeMethods.g.cs +++ b/Packages/jp.co.cyberagent.instant-replay/UniEnc/Runtime/Generated/NativeMethods.g.cs @@ -144,6 +144,11 @@ internal unsafe partial struct VideoEncoderOptionsNative public uint height; public uint fps_hint; public uint bitrate; + /// + /// Maximum interval between IDR (key) frames, in seconds. Required; the managed side rejects + /// non-positive and non-finite values before the struct crosses the boundary. + /// + public float idr_interval_seconds; } [StructLayout(LayoutKind.Sequential)] diff --git a/Packages/jp.co.cyberagent.instant-replay/UniEnc/Runtime/VideoEncoderOptions.cs b/Packages/jp.co.cyberagent.instant-replay/UniEnc/Runtime/VideoEncoderOptions.cs index b63587c4..bf3b2759 100644 --- a/Packages/jp.co.cyberagent.instant-replay/UniEnc/Runtime/VideoEncoderOptions.cs +++ b/Packages/jp.co.cyberagent.instant-replay/UniEnc/Runtime/VideoEncoderOptions.cs @@ -8,6 +8,11 @@ namespace UniEnc /// public struct VideoEncoderOptions { + /// + /// Interval between IDR (key) frames applied when is not set, in seconds. + /// + public const float DefaultIdrIntervalSeconds = 1f; + /// /// Width of the video in pixels. /// @@ -28,6 +33,13 @@ public struct VideoEncoderOptions /// public uint Bitrate { get; set; } + /// + /// Maximum interval between IDR (key) frames, in seconds. Must be finite and greater than 0 when set. + /// null applies rather than deferring to the platform + /// encoder, whose own default differs per platform and may be considerably longer. + /// + public float? IdrIntervalSeconds { get; set; } + /// /// Validates the options and throws if invalid. /// @@ -41,6 +53,11 @@ internal void Validate() if (Bitrate == 0) throw new ArgumentException("Bitrate must be greater than 0"); + + if (IdrIntervalSeconds is { } idrIntervalSeconds && + (idrIntervalSeconds <= 0f || float.IsNaN(idrIntervalSeconds) || + float.IsInfinity(idrIntervalSeconds))) + throw new ArgumentException("IDR interval must be a finite value greater than 0"); } /// @@ -54,7 +71,8 @@ internal VideoEncoderOptionsNative ToNative() width = Width, height = Height, fps_hint = FpsHint, - bitrate = Bitrate + bitrate = Bitrate, + idr_interval_seconds = IdrIntervalSeconds ?? DefaultIdrIntervalSeconds }; } } diff --git a/README.ja.md b/README.ja.md index c86e7b54..aef040fc 100644 --- a/README.ja.md +++ b/README.ja.md @@ -151,6 +151,8 @@ File.Move(outputPath, Path.Combine(Application.persistentDataPath, Path.GetFileN 実行時に使用されるメモリとしては、上記のエンコード済みのデータを保持するバッファに加え、エンコード前の生のフレームや音声サンプルがいくつか保持されます。これはエンコーダーが非同期的に動作する関係で、あるフレームをエンコードしている間に次のフレームを受け取るためです。`VideoInputQueueSize` と `AudioInputQueueSizeSeconds` でそれぞれのキューのサイズを指定できるほか、`MaxNumberOfRawFrameBuffers` (オプション) で圧縮前のフレームを保持するバッファの最大数を指定できます。この値を小さくすることでメモリ使用量を削減できる場合がありますが、フレームドロップの可能性が高まります。 +`IdrIntervalSeconds` は IDR (キー) フレームの最大間隔を秒で指定します。書き出しはキーフレームからしか開始できないため、この値は `StopAndExportAsync` が指定した長さにどれだけ近づけられるかの粒度を決めます。`null` を指定した場合は、プラットフォームのエンコーダー自身の既定値ではなく `VideoEncoderOptions.DefaultIdrIntervalSeconds` (1 秒) が適用されます。エンコーダーの既定値はプラットフォームごとに異なり、著しく長い場合があるためです。間隔を長くするとキーフレームに費やされるビットレートが減りますが、保持している時間より長い間隔を指定するとバッファ内にキーフレームが存在しなくなり、書き出しが何も生成しない場合があります。 + ```csharp // デフォルト設定 var options = new RealtimeEncodingOptions @@ -160,7 +162,8 @@ var options = new RealtimeEncodingOptions Width = 1280, Height = 720, FpsHint = 30, - Bitrate = 2500000 // 2.5 Mbps + Bitrate = 2500000, // 2.5 Mbps + IdrIntervalSeconds = 1f // IDR (キー) フレームの最大間隔 (秒)。null の場合は既定値の 1 秒が適用されます。 }, AudioOptions = new AudioEncoderOptions { diff --git a/README.md b/README.md index a510159c..5a08e4fa 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,8 @@ Recording uses memory in two places: buffers for compressed output, and buffers `VideoInputQueueSize`, `AudioInputQueueSizeSeconds`, and `MaxNumberOfRawFrameBuffers` (optional) control how many raw frames and audio samples are queued for encoding. These queues are needed because the encoder runs asynchronously, receiving the next frame while encoding the current one. Reducing these values decreases memory usage but may increase the likelihood of dropped frames. +`IdrIntervalSeconds` sets the maximum interval between IDR (key) frames. An export can only begin at a key frame, so this bounds how closely `StopAndExportAsync` can match the requested duration. `null` applies `VideoEncoderOptions.DefaultIdrIntervalSeconds`, which is one second, rather than deferring to the platform encoder, whose own default differs per platform and may be considerably longer. A longer interval spends less of the bitrate on key frames, but an interval longer than the retained duration can leave the buffer with no key frame to start from, in which case the export produces nothing. + ```csharp // Default settings var options = new RealtimeEncodingOptions @@ -164,7 +166,8 @@ var options = new RealtimeEncodingOptions Width = 1280, Height = 720, FpsHint = 30, - Bitrate = 2500000 // 2.5 Mbps + Bitrate = 2500000, // 2.5 Mbps + IdrIntervalSeconds = 1f // Max interval between IDR (key) frames in seconds. null applies the default of 1 second. }, AudioOptions = new AudioEncoderOptions {