diff --git a/VoidLink/Base.lproj/iPad.storyboard b/VoidLink/Base.lproj/iPad.storyboard
index 4bc7d608..a86229d8 100644
--- a/VoidLink/Base.lproj/iPad.storyboard
+++ b/VoidLink/Base.lproj/iPad.storyboard
@@ -986,6 +986,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1014,9 +1037,11 @@
+
+
-
+
diff --git a/VoidLink/Base.lproj/iPhone.storyboard b/VoidLink/Base.lproj/iPhone.storyboard
index e1e24874..53088bc5 100644
--- a/VoidLink/Base.lproj/iPhone.storyboard
+++ b/VoidLink/Base.lproj/iPhone.storyboard
@@ -1685,6 +1685,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1713,6 +1736,8 @@
+
+
@@ -1798,19 +1823,19 @@
-
+
-
+
-
+
-
+
diff --git a/VoidLink/Database/DataManager.h b/VoidLink/Database/DataManager.h
index 6111327d..af86acc2 100644
--- a/VoidLink/Database/DataManager.h
+++ b/VoidLink/Database/DataManager.h
@@ -42,6 +42,10 @@ typedef NS_ENUM(NSInteger, GyroMode) {
AlwaysController
};
+typedef NS_ENUM(NSInteger, FramePacingMode) {
+ FramePacingModeLegacy,
+ FramePacingModeQueue
+};
typedef NS_ENUM(NSInteger, SettingsMenuMode) {
AllSettings,
@@ -91,6 +95,7 @@ typedef NS_ENUM(NSInteger, SettingsMenuMode) {
enableGraphs:(BOOL)enableGraphs
graphOpacity:(NSInteger)graphOpacity
renderingBackend:(NSInteger)renderingBackend
+ framePacingMode:(NSInteger)framePacingMode
backgroundSessionTimer:(NSInteger)backgroundSessionTimer;
- (NSArray*) getHosts;
diff --git a/VoidLink/Database/DataManager.m b/VoidLink/Database/DataManager.m
index 488f2904..6140fd17 100644
--- a/VoidLink/Database/DataManager.m
+++ b/VoidLink/Database/DataManager.m
@@ -97,6 +97,7 @@
enableGraphs:(BOOL)enableGraphs
graphOpacity:(NSInteger)graphOpacity
renderingBackend:(NSInteger)renderingBackend
+ framePacingMode:(NSInteger)framePacingMode
backgroundSessionTimer:(NSInteger)backgroundSessionTimer{
[_managedObjectContext performBlockAndWait:^{
@@ -145,6 +146,7 @@
settingsToSave.enableGraphs = enableGraphs;
settingsToSave.graphOpacity = [NSNumber numberWithInteger:graphOpacity];
settingsToSave.renderingBackend = [NSNumber numberWithInteger:renderingBackend];
+ settingsToSave.framePacingMode = [NSNumber numberWithInteger:framePacingMode];
[self saveData];
}];
}
diff --git a/VoidLink/Database/TemporarySettings.h b/VoidLink/Database/TemporarySettings.h
index a44c0e57..bfa557b2 100644
--- a/VoidLink/Database/TemporarySettings.h
+++ b/VoidLink/Database/TemporarySettings.h
@@ -68,6 +68,7 @@
@property (nonatomic, retain) NSNumber * frameQueueSize;
@property (nonatomic, retain) NSNumber * graphOpacity;
@property (nonatomic, retain) NSNumber * renderingBackend;
+@property (nonatomic, retain) NSNumber * framePacingMode;
- (id) initFromSettings:(Settings*)settings;
diff --git a/VoidLink/Database/TemporarySettings.m b/VoidLink/Database/TemporarySettings.m
index 160549ac..499608c0 100644
--- a/VoidLink/Database/TemporarySettings.m
+++ b/VoidLink/Database/TemporarySettings.m
@@ -61,6 +61,7 @@
// iOS < 17 must use Balanced mode (AVSB renderer)
self.renderingBackend = [NSNumber numberWithInteger:RENDER_AVSB];
}
+ self.framePacingMode = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"framePacingMode"]];
NSInteger _screenSize = [[NSUserDefaults standardUserDefaults] integerForKey:@"streamResolution"];
switch (_screenSize) {
@@ -129,6 +130,7 @@
self.enableGraphs = settings.enableGraphs;
self.graphOpacity = settings.graphOpacity;
self.renderingBackend = settings.renderingBackend;
+ self.framePacingMode = settings.framePacingMode;
self.backgroundSessionTimer = settings.backroundSessionTimer;
#endif
self.uniqueId = settings.uniqueId;
diff --git a/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents b/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents
index b582424a..ac857603 100644
--- a/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents
+++ b/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents
@@ -34,6 +34,7 @@
+
diff --git a/VoidLink/Localization/Localizable.xcstrings b/VoidLink/Localization/Localizable.xcstrings
index fbddb762..d8dbd8f1 100644
--- a/VoidLink/Localization/Localizable.xcstrings
+++ b/VoidLink/Localization/Localizable.xcstrings
@@ -306,29 +306,6 @@
}
}
},
- "[ Toggle stats overlay & graphs ]" : {
- "extractionState" : "manual",
- "localizations" : {
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "[ 切换统计覆盖层和图表 ]"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "[ 切換統計覆蓋層和圖表 ]"
- }
- },
- "zh-HK" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "[ 切換統計覆蓋層和圖表 ]"
- }
- }
- }
- },
"[ On-screen widget tool ]" : {
"extractionState" : "manual",
"localizations" : {
@@ -375,6 +352,29 @@
}
}
},
+ "[ Toggle stats overlay & graphs ]" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "[ 切换统计覆盖层和图表 ]"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "[ 切換統計覆蓋層和圖表 ]"
+ }
+ },
+ "zh-HK" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "[ 切換統計覆蓋層和圖表 ]"
+ }
+ }
+ }
+ },
"%@ is currently running" : {
"extractionState" : "manual",
"localizations" : {
@@ -789,6 +789,29 @@
}
}
},
+ "Balanced" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "平衡"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "平衡"
+ }
+ },
+ "zh-HK" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "平衡"
+ }
+ }
+ }
+ },
"Ball: assumed touch point. Cross: indicator location" : {
"extractionState" : "manual",
"localizations" : {
@@ -2010,6 +2033,52 @@
}
}
},
+ "Frames to Buffer" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "缓冲帧数"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "緩衝影格數"
+ }
+ },
+ "zh-HK" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "緩衝影格數"
+ }
+ }
+ }
+ },
+ "Frames to buffer: %ld" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "缓冲帧数:%ld"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "緩衝影格數:%ld"
+ }
+ },
+ "zh-HK" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "緩衝影格數:%ld"
+ }
+ }
+ }
+ },
"From the player community, to the player community." : {
"extractionState" : "manual",
"localizations" : {
@@ -2419,6 +2488,29 @@
}
}
},
+ "Later" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "稍后"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "稍後"
+ }
+ },
+ "zh-HK" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "稍後"
+ }
+ }
+ }
+ },
"Launch" : {
"extractionState" : "manual",
"localizations" : {
@@ -3393,6 +3485,52 @@
}
}
},
+ "Performance" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "性能"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "效能"
+ }
+ },
+ "zh-HK" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "效能"
+ }
+ }
+ }
+ },
+ "Performance Graphs - Opacity: %ld%%" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "性能图表 - 不透明度:%ld%%"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "效能圖表 - 不透明度:%ld%%"
+ }
+ },
+ "zh-HK" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "效能圖表 - 不透明度:%ld%%"
+ }
+ }
+ }
+ },
"Peripherals" : {
"extractionState" : "manual",
"localizations" : {
@@ -3745,6 +3883,29 @@
}
}
},
+ "Quit Now" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "立即退出"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "立即退出"
+ }
+ },
+ "zh-HK" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "立即退出"
+ }
+ }
+ }
+ },
"Quit Running App" : {
"extractionState" : "manual",
"localizations" : {
@@ -3837,98 +3998,6 @@
}
}
},
- "Quit Now" : {
- "extractionState" : "manual",
- "localizations" : {
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "立即退出"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "立即退出"
- }
- },
- "zh-HK" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "立即退出"
- }
- }
- }
- },
- "Rendering mode change requires app restart" : {
- "extractionState" : "manual",
- "localizations" : {
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "更改渲染模式需要重启应用以确保干净启动。您是否要立即退出应用?"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "更改渲染模式需要重新啟動應用程式以確保乾淨啟動。您是否要立即退出應用程式?"
- }
- },
- "zh-HK" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "更改渲染模式需要重新啟動應用程式以確保乾淨啟動。您是否要立即退出應用程式?"
- }
- }
- }
- },
- "Restart Required" : {
- "extractionState" : "manual",
- "localizations" : {
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "需要重启"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "需要重新啟動"
- }
- },
- "zh-HK" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "需要重新啟動"
- }
- }
- }
- },
- "Later" : {
- "extractionState" : "manual",
- "localizations" : {
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "稍后"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "稍後"
- }
- },
- "zh-HK" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "稍後"
- }
- }
- }
- },
"Read Widget Instruction" : {
"extractionState" : "manual",
"localizations" : {
@@ -4044,6 +4113,52 @@
}
}
},
+ "Rendering Mode (Experimental)" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "渲染模式(实验性)"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "渲染模式(實驗性)"
+ }
+ },
+ "zh-HK" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "渲染模式(實驗性)"
+ }
+ }
+ }
+ },
+ "Rendering mode change requires app restart" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "更改渲染模式需要重启应用以确保干净启动。您是否要立即退出应用?"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "更改渲染模式需要重新啟動應用程式以確保乾淨啟動。您是否要立即退出應用程式?"
+ }
+ },
+ "zh-HK" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "更改渲染模式需要重新啟動應用程式以確保乾淨啟動。您是否要立即退出應用程式?"
+ }
+ }
+ }
+ },
"renderingBackendStackTip" : {
"extractionState" : "manual",
"localizations" : {
@@ -4073,6 +4188,29 @@
}
}
},
+ "Restart Required" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "需要重启"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "需要重新啟動"
+ }
+ },
+ "zh-HK" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "需要重新啟動"
+ }
+ }
+ }
+ },
"Resume App" : {
"extractionState" : "manual",
"localizations" : {
@@ -5384,144 +5522,6 @@
}
}
},
- "Rendering Mode (Experimental)" : {
- "extractionState" : "manual",
- "localizations" : {
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "渲染模式(实验性)"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "渲染模式(實驗性)"
- }
- },
- "zh-HK" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "渲染模式(實驗性)"
- }
- }
- }
- },
- "Frames to Buffer" : {
- "extractionState" : "manual",
- "localizations" : {
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "缓冲帧数"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "緩衝影格數"
- }
- },
- "zh-HK" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "緩衝影格數"
- }
- }
- }
- },
- "Frames to buffer: %ld" : {
- "extractionState" : "manual",
- "localizations" : {
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "缓冲帧数:%ld"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "緩衝影格數:%ld"
- }
- },
- "zh-HK" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "緩衝影格數:%ld"
- }
- }
- }
- },
- "Performance Graphs - Opacity: %ld%%" : {
- "extractionState" : "manual",
- "localizations" : {
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "性能图表 - 不透明度:%ld%%"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "效能圖表 - 不透明度:%ld%%"
- }
- },
- "zh-HK" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "效能圖表 - 不透明度:%ld%%"
- }
- }
- }
- },
- "Balanced" : {
- "extractionState" : "manual",
- "localizations" : {
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "平衡"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "平衡"
- }
- },
- "zh-HK" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "平衡"
- }
- }
- }
- },
- "Performance" : {
- "extractionState" : "manual",
- "localizations" : {
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "性能"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "效能"
- }
- },
- "zh-HK" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "效能"
- }
- }
- }
- },
"闽ICP备17012590号-3A\n主办单位:福州创图信息技术有限公司" : {
}
diff --git a/VoidLink/Stream/VideoDecoderRenderer.m b/VoidLink/Stream/VideoDecoderRenderer.m
index 2a71cae6..48384b07 100644
--- a/VoidLink/Stream/VideoDecoderRenderer.m
+++ b/VoidLink/Stream/VideoDecoderRenderer.m
@@ -55,6 +55,13 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
NSInteger _maxRefreshRate;
RenderingBackend _renderingBackend;
+ // Frame pacing mode
+ BOOL _useLegacyPacing;
+
+ // Legacy pacing frame counter and queue management
+ NSInteger _simplePacingFrameCount;
+ NSInteger _enqueuedSampleBuffers;
+
}
- (void)reinitializeDisplayLayer
@@ -129,9 +136,15 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
DataManager* dataMan = [[DataManager alloc] init];
- _frameQueue = [FrameQueue sharedInstance];
- [_frameQueue start];
- [_frameQueue setHighWaterMark:(int)[[dataMan getSettings].frameQueueSize integerValue]];
+ // Check if we should use legacy pacing mode
+ _useLegacyPacing = [[dataMan getSettings].framePacingMode integerValue] == FramePacingModeLegacy;
+
+ if (!_useLegacyPacing) {
+ // Queue pacing mode uses FrameQueue
+ _frameQueue = [FrameQueue sharedInstance];
+ [_frameQueue start];
+ [_frameQueue setHighWaterMark:(int)[[dataMan getSettings].frameQueueSize integerValue]];
+ }
[self reinitializeDisplayLayer];
@@ -155,11 +168,17 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
DataManager* dataMan = [[DataManager alloc] init];
if ([[dataMan getSettings].renderingBackend integerValue] == RENDER_AVSB) {
- // PACING_MODE_VSYNC:
- // Deliver 1 frame at each vsync interval. Ignores server pts timestamps.
- // Drop frames intelligently to maintain chosen queue size.
_renderingBackend = RENDER_AVSB;
- _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(renderModeAVSB:)];
+
+ if (_useLegacyPacing) {
+ // Legacy pacing mode
+ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(renderModeLegacy:)];
+ } else {
+ // PACING_MODE_VSYNC:
+ // Deliver 1 frame at each vsync interval. Ignores server pts timestamps.
+ // Drop frames intelligently to maintain chosen queue size.
+ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(renderModeAVSB:)];
+ }
if (@available(iOS 15.0, tvOS 15.0, *)) {
_displayLink.preferredFrameRateRange = CAFrameRateRangeMake(self->_frameRate, self->_frameRate, self->_frameRate);
@@ -176,7 +195,6 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
- (void)setupDecompressionSessionWithAttributes:(NSDictionary *)destinationPixelBufferAttributes {
- // This method is called from within synchronized block, so no additional sync needed here
if (_decompressionSession != NULL) {
VTDecompressionSessionInvalidate(_decompressionSession);
CFRelease(_decompressionSession);
@@ -242,7 +260,33 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
-#pragma mark DisplayLink - Frame Pacing - Vsync with FrameQueue
+#pragma mark DisplayLink - Legacy Frame Pacing
+
+// Legacy frame pacing method from upstream/Integration - direct polling without FrameQueue
+- (void)renderModeLegacy:(CADisplayLink *)link {
+ VIDEO_FRAME_HANDLE handle;
+ PDECODE_UNIT du;
+
+ while (LiPollNextVideoFrame(&handle, &du)) {
+ LiCompleteVideoFrame(handle, DrSubmitDecodeUnit(du));
+
+ // Calculate the actual display refresh rate
+ double displayRefreshRate = 1 / (link.targetTimestamp - link.timestamp);
+
+ // Only pace frames if the display refresh rate is >= 90% of our stream frame rate.
+ // Battery saver, accessibility settings, or device thermals can cause the actual
+ // refresh rate of the display to drop below the physical maximum.
+ if (displayRefreshRate >= _frameRate * 0.9f) {
+ // Keep one pending frame to smooth out gaps due to
+ // network jitter at the cost of 1 frame of latency
+ if (LiGetPendingVideoFrames() == 1) {
+ break;
+ }
+ }
+ }
+}
+
+#pragma mark DisplayLink - Frame Pacing - Vsync with FrameQueue (Queue Pacing)
// This frame pacing method was inspired by the behavior of moonlight-qt's Pacer class, although it has evolved
// a few additional features. Incoming frames from Sunshine are asynchronously processed into a queue by the VideoRecv thread.
@@ -294,9 +338,11 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
// we missed a callback
// Log(LOG_W, @"*** slow frametime %.3f ms", frametime * 1000.0);
}
- if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground) {
- [[ImGuiPlots sharedInstance] observeFloat:PLOT_FRAMETIME value:frametime * 1000.0];
- }
+ dispatch_async(dispatch_get_main_queue(), ^{
+ if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground) {
+ [[ImGuiPlots sharedInstance] observeFloat:PLOT_FRAMETIME value:frametime * 1000.0];
+ }
+ });
}
lastTargetLocal = targetLocal;
@@ -364,7 +410,9 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
}
- (void)cleanup
{
- [_frameQueue stop];
+ if (_frameQueue) {
+ [_frameQueue stop];
+ }
if (_renderingBackend == RENDER_AVSB) {
[_displayLink invalidate];
@@ -925,34 +973,82 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
// Dispatch onto our higher priority queue
dispatch_async(self->_vtq, ^{
- Frame *frame = nil;
- if (self->_renderingBackend == RENDER_AVSB) {
- frame = [[Frame alloc] initWithSampleBuffer:sampleBufferOut frameNumber:frameNumber frameType:frameType];
+ if (self->_useLegacyPacing && self->_renderingBackend == RENDER_AVSB) {
+ // Legacy pacing: Process all frames normally, but with immediate display timing
+
+ // Set presentation time to current time for immediate display
+ CMTime targetTime = CMTimeMakeWithSeconds(CACurrentMediaTime(), NSEC_PER_SEC);
+ CMSampleBufferSetOutputPresentationTimeStamp(sampleBufferOut, targetTime);
+
+ // Dispatch display layer operations to main thread to avoid CATransaction warnings
+ dispatch_async(dispatch_get_main_queue(), ^{
+ if (frameType == FRAME_TYPE_IDR) {
+ // Ensure the layer is visible now
+ self->_displayLayer.hidden = NO;
+
+ // Tell our parent VC to hide the progress indicator
+ [self->_callbacks videoContentShown];
+ }
+
+ if ([self->_displayLayer controlTimebase] == NULL) {
+ // On first frame, set timebase
+ CMTimebaseRef timebase = NULL;
+ CMTimebaseCreateWithSourceClock(CFAllocatorGetDefault(), CMClockGetHostTimeClock(), &timebase);
+
+ CMTime pts = CMSampleBufferGetOutputPresentationTimeStamp(sampleBufferOut);
+ CMTimebaseSetTime(timebase, pts);
+ CMTimebaseSetRate(timebase, 1.0);
+
+ [self->_displayLayer setControlTimebase:timebase];
+ if (timebase) {
+ CFRelease(timebase);
+ }
+ Log(LOG_I, @"Setting timebase for legacy pacing to %d / %d", pts.value, pts.timescale);
+ }
+
+ [self->_displayLayer enqueueSampleBuffer:sampleBufferOut];
+ CFRelease(sampleBufferOut);
+ });
} else {
- frame = [[Frame alloc] initWithPixelBufffer:pixelBuffer frameNumber:frameNumber frameType:frameType pts:presentationTimestamp];
- [frame setFormatDesc:self->_formatDesc];
- }
- int framesDropped = [self->_frameQueue enqueue:frame withSlackSize:3];
-
- if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground) {
- static PlotMetrics frameQueueMetrics = {};
- [[ImGuiPlots sharedInstance] observeFloatReturnMetrics:PLOT_QUEUED_FRAMES value:[self->_frameQueue count] plotMetrics:&frameQueueMetrics];
- [self safeCopyMetricsTo:&self->_frameQueueMetrics from:&frameQueueMetrics];
-
- [[ImGuiPlots sharedInstance] observeFloat:PLOT_DROPPED value:framesDropped];
-
- // It's important we capture host metrics on the incoming thread, as this frame object
- // may have been dropped by the above enqueue
- static CFTimeInterval lastHostFrame = 0.0f;
- if (lastHostFrame != 0) {
- [[ImGuiPlots sharedInstance] observeFloat:PLOT_HOST_FRAMETIME value:(frame.pts - lastHostFrame) * 1000.0];
+ // Queue pacing: use FrameQueue
+ Frame *frame = nil;
+ if (self->_renderingBackend == RENDER_AVSB) {
+ frame = [[Frame alloc] initWithSampleBuffer:sampleBufferOut frameNumber:frameNumber frameType:frameType];
+ } else {
+ frame = [[Frame alloc] initWithPixelBufffer:pixelBuffer frameNumber:frameNumber frameType:frameType pts:presentationTimestamp];
+ [frame setFormatDesc:self->_formatDesc];
}
- lastHostFrame = frame.pts;
+ int framesDropped = [self->_frameQueue enqueue:frame withSlackSize:3];
- // Decode time is not graphed because it is marked as hidden, but we can use the same mechanism for the value used by stats
- static PlotMetrics decodeMetrics = {};
- [[ImGuiPlots sharedInstance] observeFloatReturnMetrics:PLOT_DECODE value:(CACurrentMediaTime() - decodeStartTime) * 1000.0 plotMetrics:&decodeMetrics];
- [self safeCopyMetricsTo:&self->_decodeMetrics from:&decodeMetrics];
+ // Capture metrics on main thread to avoid UIApplication thread warning
+ int frameQueueCount = [self->_frameQueue count];
+ CFTimeInterval capturedDecodeTime = CACurrentMediaTime() - decodeStartTime;
+ CFTimeInterval hostFramePts = frame.pts;
+
+ dispatch_async(dispatch_get_main_queue(), ^{
+ if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground) {
+ static PlotMetrics frameQueueMetrics = {};
+ [[ImGuiPlots sharedInstance] observeFloatReturnMetrics:PLOT_QUEUED_FRAMES value:frameQueueCount plotMetrics:&frameQueueMetrics];
+ [self safeCopyMetricsTo:&self->_frameQueueMetrics from:&frameQueueMetrics];
+
+ [[ImGuiPlots sharedInstance] observeFloat:PLOT_DROPPED value:framesDropped];
+
+ // It's important we capture host metrics on the incoming thread, as this frame object
+ // may have been dropped by the above enqueue
+ static CFTimeInterval lastHostFrame = 0.0f;
+ if (lastHostFrame != 0) {
+ [[ImGuiPlots sharedInstance] observeFloat:PLOT_HOST_FRAMETIME value:(hostFramePts - lastHostFrame) * 1000.0];
+ }
+ lastHostFrame = hostFramePts;
+
+ // Decode time is not graphed because it is marked as hidden, but we can use the same mechanism for the value used by stats
+ static PlotMetrics decodeMetrics = {};
+ [[ImGuiPlots sharedInstance] observeFloatReturnMetrics:PLOT_DECODE
+ value:capturedDecodeTime * 1000.0
+ plotMetrics:&decodeMetrics];
+ [self safeCopyMetricsTo:&self->_decodeMetrics from:&decodeMetrics];
+ }
+ });
}
});
});
@@ -1051,13 +1147,16 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
if (_renderingBackend == RENDER_METAL) {
stats->renderingBackendString = [NSString stringWithFormat:@"Metal, colorspace: %@", [MetalVideoRenderer currentColorSpace]];
} else {
- stats->renderingBackendString = @"AVSampleBuffer";
+ NSString *pacingMode = _useLegacyPacing ? @"Legacy" : @"Queue";
+ stats->renderingBackendString = [NSString stringWithFormat:@"AVSampleBuffer (%@ pacing)", pacingMode];
}
dispatch_sync(_sq, ^{
memcpy(&stats->decodeMetrics, &_decodeMetrics, sizeof(PlotMetrics));
- memcpy(&stats->frameQueueMetrics, &_frameQueueMetrics, sizeof(PlotMetrics));
- [_frameQueue.frameDropMetrics copyMetrics:&stats->frameDropMetrics];
+ if (_frameQueue) {
+ memcpy(&stats->frameQueueMetrics, &_frameQueueMetrics, sizeof(PlotMetrics));
+ [_frameQueue.frameDropMetrics copyMetrics:&stats->frameDropMetrics];
+ }
});
}
@@ -1069,7 +1168,7 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
static int lastTargetRate = 0;
int targetRate = (int)_maxRefreshRate;
- if (_maxRefreshRate <= 60 || _maxRefreshRate == 90) {
+ if (_maxRefreshRate <= 60 || _maxRefreshRate == 90 || !_frameQueue) {
return;
}
diff --git a/VoidLink/Utility/Plot.h b/VoidLink/Utility/Plot.h
index 2605233a..de78bf03 100644
--- a/VoidLink/Utility/Plot.h
+++ b/VoidLink/Utility/Plot.h
@@ -1,8 +1,3 @@
-typedef enum {
- PACING_MODE_VSYNC,
- PACING_MODE_PTS
-} FramePacingMode;
-
typedef enum {
PLOT_FRAMETIME = 0,
PLOT_HOST_FRAMETIME,
diff --git a/VoidLink/ViewControllers/MainFrameViewController.m b/VoidLink/ViewControllers/MainFrameViewController.m
index 52dbe30c..84488125 100644
--- a/VoidLink/ViewControllers/MainFrameViewController.m
+++ b/VoidLink/ViewControllers/MainFrameViewController.m
@@ -28,6 +28,7 @@
#import "IdManager.h"
#import "ConnectionHelper.h"
#import "LocalizationHelper.h"
+#import "Plot.h"
#import "CustomEdgeSlideGestureRecognizer.h"
#import "DataManager.h"
#import "ThemeManager.h"
@@ -1180,6 +1181,9 @@ static NSMutableSet* hostList;
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.audioConfigStack];
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.pipStack];
[settingsViewController.renderingBackendSelector setEnabled:!_settingsExpandedInStreamView];
+ // Enable frame pacing mode selector only if not in stream view AND not in performance mode
+ BOOL shouldEnableFramePacing = !_settingsExpandedInStreamView && (settingsViewController.renderingBackendSelector.selectedSegmentIndex != RENDER_METAL);
+ [settingsViewController.framePacingModeSelector setEnabled:shouldEnableFramePacing];
}
- (void)revealController:(SWRevealViewController *)revealController didMoveToPosition:(FrontViewPosition)position {
diff --git a/VoidLink/ViewControllers/SettingsViewController.h b/VoidLink/ViewControllers/SettingsViewController.h
index 13860d64..696792e3 100644
--- a/VoidLink/ViewControllers/SettingsViewController.h
+++ b/VoidLink/ViewControllers/SettingsViewController.h
@@ -57,6 +57,7 @@
@property (strong, nonatomic) IBOutlet UIStackView *externalDisplayModeStack;
@property (strong, nonatomic) IBOutlet UIStackView *localMousePointerModeStack;
@property (strong, nonatomic) IBOutlet UIStackView *renderingBackendStack;
+@property (strong, nonatomic) IBOutlet UIStackView *framePacingStack;
@property (strong, nonatomic) IBOutlet UIStackView *frameQueueSizeStack;
@property (strong, nonatomic) IBOutlet UIStackView *performanceGraphStack;
@@ -115,6 +116,7 @@
@property (strong, nonatomic) IBOutlet UISwitch *enableGraphsSwitch;
@property (strong, nonatomic) IBOutlet UIStepper *graphOpacityStepper;
@property (strong, nonatomic) IBOutlet UISegmentedControl *renderingBackendSelector;
+@property (strong, nonatomic) IBOutlet UISegmentedControl *framePacingModeSelector;
@property (strong, nonatomic) IBOutlet UIStackView *backgroundSessionTimerStack;
@property (strong, nonatomic) IBOutlet UISlider *backgroundSessionTimerSlider;
diff --git a/VoidLink/ViewControllers/SettingsViewController.m b/VoidLink/ViewControllers/SettingsViewController.m
index 9262ceef..cb90ab5e 100644
--- a/VoidLink/ViewControllers/SettingsViewController.m
+++ b/VoidLink/ViewControllers/SettingsViewController.m
@@ -560,6 +560,7 @@ BOOL isCustomResolution(int resolutionSelected) {
[self addSetting:self.pipStack ofId:@"pipStack" withInfoTag:YES withDynamicLabel:NO to:videoSection];
[self addSetting:self.pipStack ofId:@"pipStack" withInfoTag:YES withDynamicLabel:NO to:videoSection];
[self addSetting:self.frameQueueSizeStack ofId:@"frameQueueSizeStack" withInfoTag:NO withDynamicLabel:NO to:videoSection];
+ [self addSetting:self.framePacingStack ofId:@"framePacingStack" withInfoTag:YES withDynamicLabel:NO to:videoSection];
// Only show Metal renderer option on iOS 17+ where CAMetalDisplayLink is available
if (@available(iOS 17.0, *)) {
@@ -1431,7 +1432,13 @@ BOOL isCustomResolution(int resolutionSelected) {
NSInteger renderingBackend = [currentSettings.renderingBackend integerValue];
[self.renderingBackendSelector setSelectedSegmentIndex:renderingBackend];
[self.renderingBackendSelector addTarget:self action:@selector(renderingBackendChanged:) forControlEvents:UIControlEventValueChanged];
- [self renderingBackendChanged:self.renderingBackendSelector]; // Update PiP state based on current selection
+
+ NSInteger framePacingMode = [currentSettings.framePacingMode integerValue];
+ [self.framePacingModeSelector setSelectedSegmentIndex:framePacingMode];
+ [self.framePacingModeSelector addTarget:self action:@selector(framePacingModeChanged:) forControlEvents:UIControlEventValueChanged];
+ [self framePacingModeChanged:self.framePacingModeSelector];
+
+ [self renderingBackendChanged:self.renderingBackendSelector]; // Update PiP and frame pacing state based on current selection
[self.citrixX1MouseSwitch setOn:currentSettings.btMouseSupport];
[self.optimizeGamesSwitch setOn: currentSettings.optimizeGames];
@@ -1665,6 +1672,24 @@ BOOL isCustomResolution(int resolutionSelected) {
}
- (void)renderingBackendChanged:(UISegmentedControl *)sender {
+ // Disable PiP toggle when Metal renderer is selected
+ if (sender.selectedSegmentIndex == RENDER_METAL) {
+ // Performance mode (Metal renderer) selected - disable PiP
+ [self.pipSwitch setOn:NO animated:YES];
+ [self.pipSwitch setEnabled:NO];
+ // Set pacing method to Queue and disable selector
+ [self.framePacingModeSelector setSelectedSegmentIndex:FramePacingModeQueue];
+ [self.framePacingModeSelector setEnabled:NO];
+ } else {
+ // Balanced mode (AVSB renderer) - enable PiP toggle if iOS 15+
+ if (@available(iOS 15.0, *)) {
+ [self.pipSwitch setEnabled:YES];
+ } else {
+ [self.pipSwitch setOn:NO];
+ [self.pipSwitch setEnabled:NO];
+ }
+ [self.framePacingModeSelector setEnabled:YES];
+ }
// Get the current settings to compare with the new selection
DataManager* dataMan = [[DataManager alloc] init];
@@ -1706,19 +1731,22 @@ BOOL isCustomResolution(int resolutionSelected) {
[alertController addAction:quitAction];
[self presentViewController:alertController animated:YES completion:nil];
}
-
- // Disable PiP toggle when Metal renderer is selected
- if (sender.selectedSegmentIndex == RENDER_METAL) {
- // Performance mode (Metal renderer) selected - disable PiP
- [self.pipSwitch setOn:NO animated:YES];
- [self.pipSwitch setEnabled:NO];
+}
+
+- (void)framePacingModeChanged:(UISegmentedControl *)sender {
+
+ if (sender.selectedSegmentIndex == FramePacingModeLegacy) {
+ // Legacy mode selected - disable frames to buffer and graph settings
+ [self.frameQueueSizeSlider setEnabled:NO];
+ [self.enableGraphsSwitch setOn:NO animated:YES];
+ [self.enableGraphsSwitch setEnabled:NO];
+ [self.graphOpacityStepper setEnabled:NO];
} else {
- // Balanced mode (AVSB renderer) - enable PiP toggle if iOS 15+
- if (@available(iOS 15.0, *)) {
- [self.pipSwitch setEnabled:YES];
- } else {
- [self.pipSwitch setOn:NO];
- [self.pipSwitch setEnabled:NO];
+ // Queue mode selected - enable frames to buffer and graph settings
+ [self.frameQueueSizeSlider setEnabled:YES];
+ [self.enableGraphsSwitch setEnabled:YES];
+ if (self.enableGraphsSwitch.isOn) {
+ [self.graphOpacityStepper setEnabled:YES];
}
}
}
@@ -2265,6 +2293,7 @@ BOOL isCustomResolution(int resolutionSelected) {
NSInteger audioConfig = [@[@2, @6, @8][[self.audioConfigSelector selectedSegmentIndex]] integerValue];
NSInteger renderingBackend = [self.renderingBackendSelector selectedSegmentIndex];
+ NSInteger framePacingMode = [self.framePacingModeSelector selectedSegmentIndex];
NSInteger onscreenControls = [self.onScreenWidgetSelector selectedSegmentIndex];
NSInteger keyboardToggleFingers = self.softKeyboardGestureSelector.selectedSegmentIndex == 3 ? 20 : self.softKeyboardGestureSelector.selectedSegmentIndex+3;
NSInteger oscLayoutToolFingers = (uint16_t)self->oswLayoutFingers;
@@ -2348,6 +2377,7 @@ BOOL isCustomResolution(int resolutionSelected) {
enableGraphs:enableGraphs
graphOpacity:_graphOpacity
renderingBackend:renderingBackend
+ framePacingMode:framePacingMode
backgroundSessionTimer:backgroundSessionTimer];
}
diff --git a/VoidLink/mul.lproj/iPad.xcstrings b/VoidLink/mul.lproj/iPad.xcstrings
index 3ecdccf7..fe5c2086 100644
--- a/VoidLink/mul.lproj/iPad.xcstrings
+++ b/VoidLink/mul.lproj/iPad.xcstrings
@@ -31,6 +31,66 @@
}
}
},
+ "2Dp-Qg-chi.segmentTitles[0]" : {
+ "comment" : "Class = \"UISegmentedControl\"; 2Dp-Qg-chi.segmentTitles[0] = \"Legacy\"; ObjectID = \"2Dp-Qg-chi\";",
+ "extractionState" : "extracted_with_value",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "new",
+ "value" : "Legacy"
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "旧版"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "舊版"
+ }
+ },
+ "zh-HK" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "舊版"
+ }
+ }
+ }
+ },
+ "2Dp-Qg-chi.segmentTitles[1]" : {
+ "comment" : "Class = \"UISegmentedControl\"; 2Dp-Qg-chi.segmentTitles[1] = \"Queue\"; ObjectID = \"2Dp-Qg-chi\";",
+ "extractionState" : "extracted_with_value",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "new",
+ "value" : "Queue"
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "佇列"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "佇列"
+ }
+ },
+ "zh-HK" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "佇列"
+ }
+ }
+ }
+ },
"2w1-Vw-GVy.text" : {
"comment" : "Class = \"UILabel\"; text = \"HDR\"; ObjectID = \"2w1-Vw-GVy\";",
"extractionState" : "extracted_with_value",
@@ -2311,6 +2371,36 @@
}
}
},
+ "MaN-f7-I3g.text" : {
+ "comment" : "Class = \"UILabel\"; text = \"Frame Pacing Mode\"; ObjectID = \"MaN-f7-I3g\";",
+ "extractionState" : "extracted_with_value",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "new",
+ "value" : "Frame Pacing Mode"
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "帧缓冲模式"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "幀緩衝模式"
+ }
+ },
+ "zh-HK" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "幀緩衝模式"
+ }
+ }
+ }
+ },
"Mm0-xf-9eh.segmentTitles[0]" : {
"comment" : "Class = \"UISegmentedControl\"; Mm0-xf-9eh.segmentTitles[0] = \"Slide From Left Edge\"; ObjectID = \"Mm0-xf-9eh\";",
"extractionState" : "extracted_with_value",
diff --git a/VoidLink/mul.lproj/iPhone.xcstrings b/VoidLink/mul.lproj/iPhone.xcstrings
index a6c78979..9f4fb8d9 100644
--- a/VoidLink/mul.lproj/iPhone.xcstrings
+++ b/VoidLink/mul.lproj/iPhone.xcstrings
@@ -1087,6 +1087,66 @@
}
}
},
+ "CC6-pu-Uoh.segmentTitles[0]" : {
+ "comment" : "Class = \"UISegmentedControl\"; CC6-pu-Uoh.segmentTitles[0] = \"Legacy\"; ObjectID = \"CC6-pu-Uoh\";",
+ "extractionState" : "extracted_with_value",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "new",
+ "value" : "Legacy"
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "旧版"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "舊版"
+ }
+ },
+ "zh-HK" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "舊版"
+ }
+ }
+ }
+ },
+ "CC6-pu-Uoh.segmentTitles[1]" : {
+ "comment" : "Class = \"UISegmentedControl\"; CC6-pu-Uoh.segmentTitles[1] = \"Queue\"; ObjectID = \"CC6-pu-Uoh\";",
+ "extractionState" : "extracted_with_value",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "new",
+ "value" : "Queue"
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "佇列"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "佇列"
+ }
+ },
+ "zh-HK" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "佇列"
+ }
+ }
+ }
+ },
"ci6-ak-aCa.text" : {
"comment" : "Class = \"UILabel\"; text = \"Custom Resolution\"; ObjectID = \"ci6-ak-aCa\";",
"extractionState" : "extracted_with_value",
@@ -1177,6 +1237,36 @@
}
}
},
+ "Cvx-kQ-L3H.text" : {
+ "comment" : "Class = \"UILabel\"; text = \"Frame Pacing Mode\"; ObjectID = \"Cvx-kQ-L3H\";",
+ "extractionState" : "extracted_with_value",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "new",
+ "value" : "Frame Pacing Mode"
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "帧缓冲模式"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "幀緩衝模式"
+ }
+ },
+ "zh-HK" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "幀緩衝模式"
+ }
+ }
+ }
+ },
"D9X-7p-9oc.title" : {
"comment" : "Class = \"UIBarButtonItem\"; title = \"ExportAll\"; ObjectID = \"D9X-7p-9oc\";",
"extractionState" : "extracted_with_value",