frame interpolation Demo1

This commit is contained in:
True Zhuanjia
2026-08-08 03:45:50 +08:00
parent 9c9ef4a13f
commit fc2b1689d0
14 changed files with 656 additions and 71 deletions
+6 -5
View File
@@ -394,12 +394,13 @@
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="1" translatesAutoresizingMaskIntoConstraints="NO" id="2Dp-Qg-chi" userLabel="Frame Pacing Mode Selector">
<segmentedControl verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" apportionsSegmentWidthsByContent="YES" segmentControlStyle="plain" selectedSegmentIndex="1" translatesAutoresizingMaskIntoConstraints="NO" id="2Dp-Qg-chi" userLabel="Frame Pacing Mode Selector">
<rect key="frame" x="0.0" y="25.5" width="380" height="32"/>
<segments>
<segment title="Off"/>
<segment title="Legacy"/>
<segment title="Queue Buffering"/>
<segment title="×2 Interpolation"/>
</segments>
<color key="tintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="selectedSegmentTintColor" systemColor="linkColor"/>
@@ -3937,19 +3938,19 @@
<image name="tray.full.fill" catalog="system" width="128" height="88"/>
<image name="video.and.waveform" catalog="system" width="128" height="94"/>
<systemColor name="linkColor">
<color red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="systemBrownColor">
<color red="0.63529411759999999" green="0.51764705879999995" blue="0.36862745099999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.67450980392156867" green="0.49803921568627452" blue="0.36862745098039218" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemGray3Color">
<color red="0.78039215689999997" green="0.78039215689999997" blue="0.80000000000000004" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.7803921568627451" green="0.7803921568627451" blue="0.80000000000000004" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemTealColor">
<color red="0.18823529410000001" green="0.69019607839999997" blue="0.78039215689999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.0" green="0.76470588235294112" blue="0.81568627450980391" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>
+4 -2
View File
@@ -2,6 +2,7 @@
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="DL0-L5-LOv">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24504"/>
<capability name="Image references" minToolsVersion="12.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
@@ -14,7 +15,7 @@
<objects>
<viewController id="dgh-JZ-Q7z" customClass="MainFrameViewController" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="Rtu-AT-Alw" customClass="AppCollectionView">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="746"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.3333333432674408" green="0.3333333432674408" blue="0.3333333432674408" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="15" minimumInteritemSpacing="15" id="YcZ-cR-3tK">
@@ -2530,12 +2531,13 @@
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="1" translatesAutoresizingMaskIntoConstraints="NO" id="CC6-pu-Uoh" userLabel="Frame Pacing Mode Selector">
<segmentedControl verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" apportionsSegmentWidthsByContent="YES" segmentControlStyle="plain" selectedSegmentIndex="1" translatesAutoresizingMaskIntoConstraints="NO" id="CC6-pu-Uoh" userLabel="Frame Pacing Mode Selector">
<rect key="frame" x="0.0" y="25.5" width="380" height="32"/>
<segments>
<segment title="Off"/>
<segment title="Legacy"/>
<segment title="Queue Buffering"/>
<segment title="×2 Interpolation"/>
</segments>
<color key="tintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="selectedSegmentTintColor" systemColor="linkColor"/>
+2 -1
View File
@@ -59,7 +59,8 @@ typedef NS_ENUM(NSInteger, ControllerMouseStick) {
typedef NS_ENUM(NSInteger, FramePacingMode) {
FramePacingModeOff,
FramePacingModeLegacy,
FramePacingModeQueue
FramePacingModeQueue,
FramePacingModeInterpolation
};
typedef NS_ENUM(NSInteger, SettingsMenuMode) {
+93 -1
View File
@@ -9304,6 +9304,98 @@
}
}
},
"Interpolation failed for an unknown reason." : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "未知原因导致的插帧失败"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "未知原因導致插幀失敗"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "未知原因導致插幀失敗"
}
}
}
},
"Interpolation overhead is too high. Falling back to normal mode." : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "插帧性能开销过大,回退普通模式"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "插幀效能開銷過大,回退至一般模式"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "插幀效能開銷過大,回退至一般模式"
}
}
}
},
"Interpolator error:" : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "插帧器错误:"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "插幀器錯誤:"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "插幀器錯誤:"
}
}
}
},
"Interpolator started." : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "插帧器已启动。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "插幀器已啟動。"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "插幀器已啟動。"
}
}
}
},
"framePacingStackTip" : {
"extractionState" : "manual",
"localizations" : {
@@ -19823,4 +19915,4 @@
}
},
"version" : "1.1"
}
}
+288 -15
View File
@@ -1,3 +1,11 @@
//
// FrameInterpolator.swift
// VoidLink
//
// Created by True砖家 on 2026/8/6.
// Copyright © 2026 True砖家 @ Bilibili. All rights reserved.
//
import AVFoundation
import CoreVideo
import ObjectiveC.runtime
@@ -19,12 +27,14 @@ final class FrameInterpolator: NSObject {
private var previousFrame: Frame?
private var pendingFrames: [PendingFrame] = []
private var isProcessing = false
private var resetRequested = false
private var width = 0
private var height = 0
private var slowUntil = CFTimeInterval(0)
private var completedProcessingCount = 0
private var consecutiveSlowFrames = 0
private var loggedEvents = Set<String>()
private var overlayGeneration = 0
var isEnabled = false {
didSet {
@@ -45,14 +55,168 @@ final class FrameInterpolator: NSObject {
return false
#endif
}()
static var dimensionLimitsAreKnown: Bool = {
#if targetEnvironment(simulator)
return false
#else
if #available(iOS 26.0, tvOS 26.0, *) {
let limits = VTLowLatencyFrameInterpolationConfiguration
.runtimeDimensionLimits(spatialScaleFactor: 1)
guard
let maximumDimension = limits.maximumDimension,
let maximumPixelCount = limits.maximumPixelCount,
maximumDimension > 0,
maximumPixelCount > 0
else {return false}
return true
}
else {
return false
}
#endif
}()
static func interpolatableDimensionsBy(_ dimensions: CMVideoDimensions) -> CMVideoDimensions {
guard deviceSupportsInterpolation else {
return CMVideoDimensions(width: 0, height: 0)
}
#if targetEnvironment(simulator)
return CMVideoDimensions(width: 0, height: 0)
#else
guard #available(iOS 26.0, tvOS 26.0, *) else {
return CMVideoDimensions(width: 0, height: 0)
}
let limits = VTLowLatencyFrameInterpolationConfiguration
.runtimeDimensionLimits(spatialScaleFactor: 1)
guard
let maximumDimension = limits.maximumDimension,
let maximumPixelCount = limits.maximumPixelCount,
maximumDimension > 0,
maximumPixelCount > 0
else {
return CMVideoDimensions(width: 0, height: 0)
}
return constrainedDimensions(
dimensions,
maximumDimension: maximumDimension,
maximumPixelCount: maximumPixelCount
)
#endif
}
static func interpolatableDimensionsBy1080p(_ dimensions: CMVideoDimensions) -> CMVideoDimensions {
guard deviceSupportsInterpolation else {
return CMVideoDimensions(width: 0, height: 0)
}
return constrainedDimensions(
dimensions,
maximumDimension: 1920,
maximumPixelCount: 1920 * 1080
)
}
static func interpolatableDimensionsBy720p(_ dimensions: CMVideoDimensions) -> CMVideoDimensions {
guard deviceSupportsInterpolation else {
return CMVideoDimensions(width: 0, height: 0)
}
return constrainedDimensions(
dimensions,
maximumDimension: 1280,
maximumPixelCount: 1280 * 720
)
}
@objc(sourcePixelBufferAttributesForDimensions:)
static func sourcePixelBufferAttributes(for dimensions: CMVideoDimensions) -> NSDictionary? {
#if targetEnvironment(simulator)
return nil
#else
guard
#available(iOS 26.0, tvOS 26.0, *),
dimensions.width > 0,
dimensions.height > 0,
VTLowLatencyFrameInterpolationConfiguration.isSupported,
let configuration = VTLowLatencyFrameInterpolationConfiguration(
frameWidth: Int(dimensions.width),
frameHeight: Int(dimensions.height),
numberOfInterpolatedFrames: 1
)
else {
return nil
}
return configuration.sourcePixelBufferAttributes as NSDictionary
#endif
}
private static func constrainedDimensions(
_ dimensions: CMVideoDimensions,
maximumDimension: Int,
maximumPixelCount: Int
) -> CMVideoDimensions {
guard
dimensions.width > 0,
dimensions.height > 0,
maximumDimension > 0,
maximumPixelCount > 0
else {
return CMVideoDimensions(width: 0, height: 0)
}
let width = Int(dimensions.width)
let height = Int(dimensions.height)
let (sourcePixelCount, pixelCountOverflow) = width.multipliedReportingOverflow(by: height)
let fullHDWidth = 1920
let fullHDHeight = 1080
let fullHDPixelCount = fullHDWidth * fullHDHeight
let isSixteenByNine = width * 9 == height * 16
if !pixelCountOverflow,
isSixteenByNine,
sourcePixelCount > fullHDPixelCount,
maximumDimension >= fullHDWidth,
maximumPixelCount >= fullHDPixelCount {
return CMVideoDimensions(width: Int32(fullHDWidth), height: Int32(fullHDHeight))
}
if !pixelCountOverflow,
max(width, height) <= maximumDimension,
sourcePixelCount <= maximumPixelCount {
return dimensions
}
let sourceWidth = Double(width)
let sourceHeight = Double(height)
let dimensionScale = Double(maximumDimension) / max(sourceWidth, sourceHeight)
let pixelCountScale = sqrt(Double(maximumPixelCount) / (sourceWidth * sourceHeight))
let scale = min(dimensionScale, pixelCountScale)
let scaledWidth = Int((sourceWidth * scale).rounded(.down)) & ~(31)
let scaledHeight = Int((sourceHeight * scale).rounded(.down)) & ~(31)
guard
scaledWidth > 0,
scaledHeight > 0,
scaledWidth <= Int(Int32.max),
scaledHeight <= Int(Int32.max)
else {
return CMVideoDimensions(width: 0, height: 0)
}
return CMVideoDimensions(width: Int32(scaledWidth), height: Int32(scaledHeight))
}
var isAvailable: Bool {
isEnabled && Self.deviceSupportsInterpolation
}
func reset() {
queue.async { [weak self] in
self?.resetLocked()
queue.async {
self.requestResetLocked()
}
}
@@ -63,6 +227,11 @@ final class FrameInterpolator: NSObject {
return
}
guard !self.resetRequested else {
completion([frame])
return
}
self.pendingFrames.append(PendingFrame(frame: frame, completion: completion))
self.drainPendingFrames()
}
@@ -181,14 +350,16 @@ final class FrameInterpolator: NSObject {
isProcessing = true
let startedAt = CACurrentMediaTime()
processor.process(parameters: parameters) { [weak self] _, error in
guard let self else {
completion([frame])
return
}
processor.process(parameters: parameters) { [self] _, error in
self.queue.async {
self.isProcessing = false
if self.resetRequested {
self.resetLocked()
completion([frame])
return
}
self.previousFrame = frame
let elapsed = CACurrentMediaTime() - startedAt
@@ -201,15 +372,24 @@ final class FrameInterpolator: NSObject {
)
else {
if let error {
self.logOnce("process-error-\((error as NSError).code)", "processing failed: \(error)")
self.logOnce("process-error", error: error)
} else {
self.logOnce("sample-buffer-failed", "processing completed but the interpolated CMSampleBuffer could not be created")
self.logOnce(
"sample-buffer-failed",
"processing completed but the interpolated CMSampleBuffer could not be created",
overlayText: "Interpolation failed for an unknown reason.".localized
)
}
completion([frame])
self.drainPendingFrames()
return
}
self.logOnce(
"first-interpolated-frame",
"produced the first interpolated frame",
overlayText: "Interpolator started.".localized
)
self.completedProcessingCount += 1
if sourceFrameInterval.isFinite, sourceFrameInterval > 0, elapsed >= sourceFrameInterval {
self.consecutiveSlowFrames += 1
@@ -221,7 +401,8 @@ final class FrameInterpolator: NSObject {
self.consecutiveSlowFrames = 0
self.logOnce(
"sustained-overrun",
"processing repeatedly exceeded the source-frame interval; temporarily falling back to original frames"
"processing repeatedly exceeded the source-frame interval; temporarily falling back to original frames",
overlayText: "Interpolation overhead is too high. Falling back to normal mode.".localized
)
}
@@ -262,6 +443,40 @@ final class FrameInterpolator: NSObject {
}
let inputPixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer)
var extendedLeft = 0
var extendedRight = 0
var extendedTop = 0
var extendedBottom = 0
CVPixelBufferGetExtendedPixels(
pixelBuffer,
&extendedLeft,
&extendedRight,
&extendedTop,
&extendedBottom
)
logOnce(
"input-layout-\(width)x\(height)",
"input pixel buffer layout for \(width)x\(height): " +
"extended left=\(extendedLeft), right=\(extendedRight), " +
"top=\(extendedTop), bottom=\(extendedBottom)"
)
let runtimeLimits = VTLowLatencyFrameInterpolationConfiguration
.runtimeDimensionLimits(spatialScaleFactor: 1)
logOnce(
"runtime-limits-\(width)x\(height)",
"runtime limits for spatial scale 1: minimum=\(String(describing: runtimeLimits.minimumDimensions)), " +
"legacyMaximum=\(String(describing: runtimeLimits.maximumDimensions)), " +
"maximumDimension=\(String(describing: runtimeLimits.maximumDimension)), " +
"maximumPixelCount=\(String(describing: runtimeLimits.maximumPixelCount))"
)
logOnce(
"source-attributes-\(width)x\(height)",
"source pixel buffer attributes for \(width)x\(height): \(configuration.sourcePixelBufferAttributes)"
)
logOnce(
"destination-attributes-\(width)x\(height)",
"destination pixel buffer attributes for \(width)x\(height): \(configuration.destinationPixelBufferAttributes)"
)
if !configuration.supportedPixelFormats.contains(inputPixelFormat) {
let supportedFormats = configuration.supportedPixelFormats
.map(pixelFormatName)
@@ -289,6 +504,7 @@ final class FrameInterpolator: NSObject {
&pool
)
guard status == kCVReturnSuccess, let pool else {
processor.endSession()
logOnce("pool-error-\(status)", "failed to create destination pixel buffer pool: \(status)")
return false
}
@@ -366,11 +582,51 @@ final class FrameInterpolator: NSObject {
CMTimeAdd(first, CMTimeMultiplyByFloat64(CMTimeSubtract(second, first), multiplier: 0.5))
}
private func logOnce(_ event: String, _ message: String) {
guard loggedEvents.insert(event).inserted else {
private func logOnce(_ event: String, _ message: String, overlayText: String? = nil) {
guard loggedEvents.insert(event).inserted || event == "sustained-overrun" else {
return
}
NSLog("[FrameInterpolator] %@", message)
if let overlayText {
showTransientHUDText(overlayText)
NSLog("[FrameInterpolator] %@", message)
}
}
private func logOnce(_ event: String, error: Error) {
let nsError = error as NSError
logOnce(
event,
"processing failed: \(error)",
overlayText: "\("Interpolator error:".localized) \(nsError.localizedDescription)"
)
}
private func showTransientHUDText(_ text: String) {
overlayGeneration &+= 1
let generation = overlayGeneration
DispatchQueue.main.async {
guard let streamFrameVC = StreamFrameViewController.sharedInstance() else {
return
}
streamFrameVC.updateTransientHUDText(text)
DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self, weak streamFrameVC] in
guard let self, let streamFrameVC else {
return
}
self.queue.async {
guard self.overlayGeneration == generation else {
return
}
DispatchQueue.main.async {
streamFrameVC.updateTransientHUDText(nil)
}
}
}
}
}
private func pixelFormatName(_ pixelFormat: OSType) -> String {
@@ -384,12 +640,30 @@ final class FrameInterpolator: NSObject {
return "\(fourCC) (\(pixelFormat))"
}
private func requestResetLocked() {
pendingFrames.removeAll()
guard !isProcessing else {
resetRequested = true
return
}
resetLocked()
}
private func resetLocked(keepPendingFrames: Bool = false) {
#if !targetEnvironment(simulator)
if #available(iOS 26.0, tvOS 26.0, *),
let processor = processor as? VTFrameProcessor {
processor.endSession()
}
#endif
processor = nil
pixelBufferPool = nil
formatDescription = nil
previousFrame = nil
isProcessing = false
resetRequested = false
width = 0
height = 0
slowUntil = 0
@@ -485,6 +759,5 @@ extension VTLowLatencyFrameInterpolationConfiguration {
let supported = limits.supports(width: 1920, height: 1080)
print("1920x1080 supported:", supported)
}
}
#endif
+60 -4
View File
@@ -174,7 +174,7 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
_asyncFrameDequeue = tempSettings.asyncFrameDequeue;
NSLog(@"_asyncFrameDequeue %d", _asyncFrameDequeue);
_enableTimebase = false;
_queueSize = tempSettings.frameQueueSize.intValue;
_queueSize = kEnableFrameInterpolation ? 4 : tempSettings.frameQueueSize.intValue;
_needRequeuing = _queueSize>0;
_frameQueue = [FrameQueue sharedInstance];
@@ -205,9 +205,10 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
[[ImGuiPlots sharedInstance] clearData];
DataManager* dataMan = [[DataManager alloc] init];
if ([[dataMan getSettings].renderingBackend integerValue] == RENDER_AVSB) {
TemporarySettings* settings = [dataMan getSettings];
if ([settings.renderingBackend integerValue] == RENDER_AVSB) {
_renderingBackend = RENDER_AVSB;
if (kEnableFrameInterpolation) {
_frameInterpolator = [[FrameInterpolator alloc] init];
_frameInterpolator.isEnabled = YES;
@@ -297,6 +298,55 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
} mutableCopy];
#endif
#if !TARGET_OS_SIMULATOR
if (interpolationRequested) {
CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(_formatDesc);
NSDictionary *interpolationSourceAttributes =
[FrameInterpolator sourcePixelBufferAttributesForDimensions:dimensions];
CFDictionaryRef resolvedAttributes = NULL;
CVReturn resolveStatus = kCVReturnInvalidArgument;
if (interpolationSourceAttributes != nil) {
NSArray *attributeRequirements = @[
destinationPixelBufferAttributes,
interpolationSourceAttributes
];
resolveStatus = CVPixelBufferCreateResolvedAttributesDictionary(
kCFAllocatorDefault,
(__bridge CFArrayRef)attributeRequirements,
&resolvedAttributes);
}
if (resolveStatus == kCVReturnSuccess && resolvedAttributes != NULL) {
destinationPixelBufferAttributes =
[(__bridge NSDictionary *)resolvedAttributes mutableCopy];
CFRelease(resolvedAttributes);
} else {
if (resolvedAttributes != NULL) {
CFRelease(resolvedAttributes);
}
Log(LOG_W,
@"Unable to resolve frame-interpolation source attributes for %dx%d (status %d); using native decode output",
dimensions.width,
dimensions.height,
resolveStatus);
[self->_frameInterpolator reset];
self->_frameInterpolator = nil;
interpolationRequested = NO;
destinationPixelBufferAttributes = [@{
(id)kCVPixelBufferPixelFormatTypeKey : nativePixelFormat
} mutableCopy];
NSInteger targetFrameRate = MIN(self->_frameRate, self->_maxRefreshRate);
if (@available(iOS 15.0, tvOS 15.0, *)) {
self->_displayLink.preferredFrameRateRange = CAFrameRateRangeMake(targetFrameRate, targetFrameRate, targetFrameRate);
} else {
self->_displayLink.preferredFramesPerSecond = targetFrameRate;
}
}
}
#endif
if (@available(iOS 17.0, tvOS 17.0, *)) {
destinationPixelBufferAttributes[(id)kVTVideoDecoderSpecification_RequireHardwareAcceleratedVideoDecoder] = @YES;
destinationPixelBufferAttributes[(id)kVTDecompressionPropertyKey_GeneratePerFrameHDRDisplayMetadata] = @YES;
@@ -313,7 +363,13 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
Log(LOG_W, @"Falling back from 420v frame-interpolation decode output to native pixel format %@", nativePixelFormat);
[self->_frameInterpolator reset];
self->_frameInterpolator = nil;
destinationPixelBufferAttributes[(id)kCVPixelBufferPixelFormatTypeKey] = nativePixelFormat;
destinationPixelBufferAttributes = [@{
(id)kCVPixelBufferPixelFormatTypeKey : nativePixelFormat
} mutableCopy];
if (@available(iOS 17.0, tvOS 17.0, *)) {
destinationPixelBufferAttributes[(id)kVTVideoDecoderSpecification_RequireHardwareAcceleratedVideoDecoder] = @YES;
destinationPixelBufferAttributes[(id)kVTDecompressionPropertyKey_GeneratePerFrameHDRDisplayMetadata] = @YES;
}
[self setupDecompressionSessionWithAttributes:destinationPixelBufferAttributes];
NSInteger targetFrameRate = MIN(self->_frameRate, self->_maxRefreshRate);
+5 -2
View File
@@ -533,11 +533,14 @@ import UIKit
}
@objc public static func handleLegacyFramePacingTip(in vc: UIViewController?, with selector: UISegmentedControl, passAlert: Bool = false, uiAction: (() -> Void)? = nil) {
if passAlert || selector.selectedSegmentIndex == FramePacingMode.queue.rawValue {
if passAlert
|| selector.selectedSegmentIndex == FramePacingMode.queue.rawValue
|| selector.selectedSegmentIndex == FramePacingMode.interpolation.rawValue{
uiAction?()
return
}
if selector.previousSelectedSegmentIndex != FramePacingMode.queue.rawValue {
if selector.previousSelectedSegmentIndex != FramePacingMode.queue.rawValue
&& selector.previousSelectedSegmentIndex != FramePacingMode.interpolation.rawValue {
uiAction?()
return
}
@@ -10,6 +10,7 @@
//
#import <UIKit/UIKit.h>
#import <CoreMedia/CoreMedia.h>
#import "DiscoveryManager.h"
#import "PairManager.h"
#import "StreamConfiguration.h"
@@ -49,7 +50,7 @@
- (NSInteger)requestForBitrate:(NSInteger)bitrateKbps;
#endif
- (void)fillResolutionTable:(CGSize*)resolutionTable externalDisplayMode:(NSInteger)externalDisplayMode;
- (void)fillResolutionTable:(CMVideoDimensions *)resolutionTable externalDisplayMode:(NSInteger)externalDisplayMode;
- (void)setNeedsUpdateAllowedOrientation;
@end
@@ -837,7 +837,7 @@ static NSMutableSet* hostList;
- (void) reloadStreamConfig {
DataManager* dataMan = [[DataManager alloc] init];
TemporarySettings* streamSettings = [dataMan getSettings];
[VideoDecoderRenderer setFrameInterpolationEnabled: streamSettings.framePacingMode.intValue == FramePacingModeInterpolation];
_streamConfig.frameRate = [streamSettings.framerate intValue];
if (@available(iOS 10.3, *)) {
UIWindow *window = UIApplication.sharedApplication.windows.firstObject;
@@ -854,6 +854,8 @@ static NSMutableSet* hostList;
_streamConfig.height = [streamSettings.height intValue];
_streamConfig.width = [streamSettings.width intValue];
NSLog(@"saveSettings.width %d, %d", _streamConfig.width, _streamConfig.height);
#if TARGET_OS_TV
// Don't allow streaming 4K on the Apple TV HD
struct utsname systemInfo;
@@ -2099,7 +2101,7 @@ static NSMutableSet* hostList;
// });
//}
-(void) fillResolutionTable:(CGSize*)resolutionTable externalDisplayMode:(NSInteger)externalDisplayMode{
-(void) fillResolutionTable:(CMVideoDimensions *)resolutionTable externalDisplayMode:(NSInteger)externalDisplayMode{
UIWindow *window = self.view.window;
NSLog(@" window %@", window);
@@ -2117,35 +2119,46 @@ static NSMutableSet* hostList;
bool needSwapWidthAndHeight = appWindowWidth < appWindowHeight;
resolutionTable[0] = CGSizeMake(1280, 720);
resolutionTable[1] = CGSizeMake(1920, 1080);
resolutionTable[2] = CGSizeMake(3840, 2160);
resolutionTable[0] = (CMVideoDimensions){ .width = 1280, .height = 720 };
resolutionTable[1] = (CMVideoDimensions){ .width = 1920, .height = 1080 };
resolutionTable[2] = (CMVideoDimensions){ .width = 3840, .height = 2160 };
for(uint8_t i=0;i<6;i++){
CGFloat longSideLen = resolutionTable[i].height > resolutionTable[i].width ? resolutionTable[i].height : resolutionTable[i].width;
CGFloat shortSideLen = resolutionTable[i].height < resolutionTable[i].width ? resolutionTable[i].height : resolutionTable[i].width;
if(needSwapWidthAndHeight) resolutionTable[i] = CGSizeMake(shortSideLen, longSideLen);
else resolutionTable[i] = CGSizeMake(longSideLen, shortSideLen);
int32_t longSideLen = MAX(resolutionTable[i].width, resolutionTable[i].height);
int32_t shortSideLen = MIN(resolutionTable[i].width, resolutionTable[i].height);
if(needSwapWidthAndHeight) resolutionTable[i] = (CMVideoDimensions){ .width = shortSideLen, .height = longSideLen };
else resolutionTable[i] = (CMVideoDimensions){ .width = longSideLen, .height = shortSideLen };
}
// add app window resolution and not swap width and height
resolutionTable[3] = CGSizeMake(safeAreaWidth, appWindowHeight);
resolutionTable[4] = CGSizeMake(appWindowWidth, appWindowHeight);
resolutionTable[3] = (CMVideoDimensions){ .width = (int32_t)safeAreaWidth, .height = (int32_t)appWindowHeight };
resolutionTable[4] = (CMVideoDimensions){ .width = (int32_t)appWindowWidth, .height = (int32_t)appWindowHeight };
}
-(void) updateResolutionAccordingly {
DataManager* dataMan = [[DataManager alloc] init];
Settings *currentSettings = [dataMan retrieveSettings];
CGSize tempResolutionTable[6] = {0};
tempResolutionTable[5] = CGSizeMake(currentSettings.width.intValue, currentSettings.height.intValue);
CMVideoDimensions tempResolutionTable[6] = {0};
tempResolutionTable[5] = (CMVideoDimensions){
.width = (int32_t)currentSettings.width.intValue,
.height = (int32_t)currentSettings.height.intValue,
};
[self fillResolutionTable:tempResolutionTable externalDisplayMode:currentSettings.externalDisplayMode.intValue];
int selectedIndex = currentSettings.resolutionSelected.intValue;
if (selectedIndex >= 0 && selectedIndex < 6) {
CGSize selectedSize = tempResolutionTable[selectedIndex];
currentSettings.width = @(selectedSize.width);
currentSettings.height = @(selectedSize.height);
CMVideoDimensions originalDimensions = tempResolutionTable[selectedIndex];
CMVideoDimensions targetDimensions;
if(currentSettings.framePacingMode.intValue == FramePacingModeInterpolation) {
targetDimensions = [FrameInterpolator interpolatableDimensionsBy:originalDimensions];
if(!(targetDimensions.width>0 && targetDimensions.height>0)) targetDimensions = [FrameInterpolator interpolatableDimensionsBy720p:originalDimensions];
}
else targetDimensions = originalDimensions;
currentSettings.width = @(targetDimensions.width);
currentSettings.height = @(targetDimensions.height);
NSLog(@"Updated resolution to: %@ x %@", currentSettings.width, currentSettings.height);
}
@@ -206,7 +206,7 @@ static const int bitrateTable[] = {
const int RESOLUTION_TABLE_SIZE = 6;
const int RESOLUTION_TABLE_CUSTOM_INDEX = RESOLUTION_TABLE_SIZE - 1;
CGSize resolutionTable[RESOLUTION_TABLE_SIZE];
CMVideoDimensions resolutionTable[RESOLUTION_TABLE_SIZE];
-(uint16_t)controllerTypeToSegmentIndex:(uint16_t)type{
uint16_t index;
@@ -882,6 +882,7 @@ BOOL isCustomResolution(int resolutionSelected) {
self.framePacingStack.hasInfoTag = YES;
[self addSetting:self.framePacingStack ofId:@"framePacingStack" to:videoSection];
if(!FrameInterpolator.deviceSupportsInterpolation) [self.framerateSelector setEnabled:false forSegmentAtIndex:FramePacingModeInterpolation];
self.frameQueueSizeStack.hasDynamicLabel = YES;
[self addSetting:self.frameQueueSizeStack ofId:@"frameQueueSizeStack" to:videoSection];
@@ -2129,7 +2130,10 @@ BOOL isCustomResolution(int resolutionSelected) {
self.resolutionSelector.selectedSegmentIndex = 3;
[self.resolutionSelector setNeedsLayout];
resolutionTable[5] = CGSizeMake([self->tempSettings.width integerValue], [self->tempSettings.height integerValue]); // custom initial value
resolutionTable[5] = (CMVideoDimensions){
.width = (int32_t)[self->tempSettings.width integerValue],
.height = (int32_t)[self->tempSettings.height integerValue],
}; // custom initial value
[self updateResolutionTable];
@@ -2689,6 +2693,7 @@ BOOL isCustomResolution(int resolutionSelected) {
[self setHidden:(sender.selectedSegmentIndex == FramePacingModeOff || sender.selectedSegmentIndex == FramePacingModeLegacy) forStack:self.frameQueueSizeStack];
// [self setHidden:(sender.selectedSegmentIndex != FramePacingModeQueue) forStack:self.frameTimebaseStack];
[self setHidden:(sender.selectedSegmentIndex != FramePacingModeQueue
|| sender.selectedSegmentIndex != FramePacingModeInterpolation
|| self.renderingBackendSelector.selectedSegmentIndex != 0) forStack:self.asyncFrameDequeueStack];
if(sender.selectedSegmentIndex == FramePacingModeOff || sender.selectedSegmentIndex == FramePacingModeLegacy){
@@ -2698,6 +2703,16 @@ BOOL isCustomResolution(int resolutionSelected) {
[self.enableGraphsSwitch setEnabled:sender.selectedSegmentIndex == FramePacingModeQueue];
[self.graphOpacityStepper setEnabled:self.enableGraphsSwitch.isOn];
[self setHidden:(sender.selectedSegmentIndex == FramePacingModeOff || sender.selectedSegmentIndex == FramePacingModeLegacy) forStack:self.performanceGraphStack];
bool enableInterpolation = sender.selectedSegmentIndex == FramePacingModeInterpolation;
[self widget:self.frameQueueSizeSlider setEnabled:!enableInterpolation];
[self updateResolutionDisplayLabel];
if(enableInterpolation){
if(self.framerateSelector.selectedSegmentIndex == 2) self.framerateSelector.selectedSegmentIndex = 1;
if(!FrameInterpolator.dimensionLimitsAreKnown){
NSLog(@"FrameInterpolator dimensionLimitsUnknown");
}
}
}];
/*
@@ -3648,6 +3663,9 @@ BOOL isCustomResolution(int resolutionSelected) {
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:key];
[[NSUserDefaults standardUserDefaults] synchronize];
if(self.framerateSelector.selectedSegmentIndex == 2
&& self.framePacingModeSelector.selectedSegmentIndex == FramePacingModeInterpolation) self.framerateSelector.selectedSegmentIndex = self.framerateSelector.previousSelectedSegmentIndex;
// NSInteger fps = [self getChosenFrameRate];
// [self touchMoveEventIntervalSliderMoved:self.touchMoveEventIntervalSlider];
[self updateBitrate];
@@ -3655,8 +3673,9 @@ BOOL isCustomResolution(int resolutionSelected) {
- (void) updateBitrate {
NSInteger fps = [self getChosenFrameRate];
NSInteger width = [self getChosenStreamWidth];
NSInteger height = [self getChosenStreamHeight];
CMVideoDimensions dimensions = [self getChosenStreamDimensions];
NSInteger width = dimensions.width;
NSInteger height = dimensions.height;
NSInteger defaultBitrate;
// This logic is shamelessly stolen from Moonlight Qt:
@@ -3792,7 +3811,10 @@ BOOL isCustomResolution(int resolutionSelected) {
width = MAX(width, 256);
height = MAX(height, 256);
resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX] = CGSizeMake(width, height);
resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX] = (CMVideoDimensions){
.width = (int32_t)width,
.height = (int32_t)height,
};
[self updateBitrate];
[self updateResolutionDisplayLabel];
@@ -3814,8 +3836,9 @@ BOOL isCustomResolution(int resolutionSelected) {
}
- (void) updateResolutionDisplayLabel {
NSInteger width = [self getChosenStreamWidth];
NSInteger height = [self getChosenStreamHeight];
CMVideoDimensions dimensions = [self getChosenStreamDimensions];
NSInteger width = dimensions.width;
NSInteger height = dimensions.height;
[self findDynamicLabelFromStack:_resolutionSelectorStack].text = [NSString stringWithFormat:@"%ld × %ld", (long)width, (long)height];
}
@@ -3888,22 +3911,26 @@ BOOL isCustomResolution(int resolutionSelected) {
}
}
- (NSInteger) getChosenStreamHeight {
- (CMVideoDimensions)getChosenStreamDimensions {
// because the 4k resolution can be removed
CMVideoDimensions originalDimensions;
if (self.customResolutionSwitch.isOn) {
return resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].height;
originalDimensions = resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX];
} else {
originalDimensions = resolutionTable[self.resolutionSelector.selectedSegmentIndex];
}
return resolutionTable[self.resolutionSelector.selectedSegmentIndex].height;
}
- (NSInteger) getChosenStreamWidth {
// because the 4k resolution can be removed
if (self.customResolutionSwitch.isOn) {
return resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].width;
CMVideoDimensions targetDimensions = self.framePacingModeSelector.selectedSegmentIndex == FramePacingModeInterpolation
? [FrameInterpolator interpolatableDimensionsBy:originalDimensions]
: originalDimensions;
if(targetDimensions.width == 0 || targetDimensions.height == 0) {
targetDimensions = [FrameInterpolator interpolatableDimensionsBy720p:originalDimensions];
}
NSLog(@"dimensions: %d, %d", targetDimensions.width, targetDimensions.height);
return resolutionTable[self.resolutionSelector.selectedSegmentIndex].width;
return targetDimensions;
}
- (UIStackView *)findFlatStackViewFrom:(UIView *)view {
@@ -4250,10 +4277,17 @@ BOOL isCustomResolution(int resolutionSelected) {
CGFloat settingsMenuOffset = _rememberFoldStateSwitch.isOn ? _scrollView.contentOffset.y : 0;
NSInteger height = self.mainFrameViewController.isStreaming ? currentSettings.height.intValue : [self getChosenStreamHeight];
NSInteger width = self.mainFrameViewController.isStreaming ? currentSettings.width.intValue : [self getChosenStreamWidth];
CMVideoDimensions dimensions;
if (self.mainFrameViewController.isStreaming) {
dimensions.width = (int32_t)currentSettings.width.intValue;
dimensions.height = (int32_t)currentSettings.height.intValue;
} else {
dimensions = [self getChosenStreamDimensions];
}
NSInteger height = dimensions.height;
NSInteger width = dimensions.width;
// NSLog(@"saveSettings aspectRatio %d, %ld, %ld",self.mainFrameViewController.isStreaming, width, height);
NSLog(@"saveSettings %d, %ld, %ld",self.mainFrameViewController.isStreaming, width, height);
NSInteger framerate = [self getChosenFrameRate];
@@ -67,5 +67,7 @@
- (void)returnToMainFrame;
- (void)bringUpToolboxMenu;
- (void)bringUpToolboxMenuWithoutWidgetLayoutTool;
- (void)updateOverlayText:(NSString*)text;
- (void)updateTransientHUDText:(NSString*)text;
@end
@@ -89,6 +89,7 @@ static __weak StreamFrameViewController *VLSharedStreamFrameViewController = nil
NSTimer *_inactivityTimer;
NSTimer *_statsUpdateTimer;
PaddedLabel *_overlayView;
PaddedLabel *_transientHUDView;
UITapGestureRecognizer *_menuTapGestureRecognizer;
UITapGestureRecognizer *_menuDoubleTapGestureRecognizer;
UITapGestureRecognizer *_playPauseTapGestureRecognizer;
@@ -1353,6 +1354,11 @@ static __weak StreamFrameViewController *VLSharedStreamFrameViewController = nil
// We set our bounds to the maximum width in order to work around a bug where
// sizeToFit interacts badly with the UITextView's line breaks, causing the
// width to get smaller and smaller each time as more line breaks are inserted.
//sdfdsf;
if (_overlayView.superview == nil) {
[self.view addSubview:_overlayView];
}
[_overlayView setBounds:CGRectMake(self.view.frame.origin.x,
_overlayView.frame.origin.y,
self.view.frame.size.width,
@@ -1367,6 +1373,47 @@ static __weak StreamFrameViewController *VLSharedStreamFrameViewController = nil
}
}
- (void)updateTransientHUDText:(NSString*)text {
if (![NSThread isMainThread]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self updateTransientHUDText:text];
});
return;
}
if (text == nil) {
_transientHUDView.hidden = YES;
return;
}
if (_transientHUDView == nil) {
_transientHUDView = [[PaddedLabel alloc] initWithFrame:CGRectZero];
_transientHUDView.translatesAutoresizingMaskIntoConstraints = NO;
_transientHUDView.textInsets = UIEdgeInsetsMake(10, 14, 10, 14);
_transientHUDView.numberOfLines = 0;
_transientHUDView.textAlignment = NSTextAlignmentLeft;
_transientHUDView.textColor = [UIColor.whiteColor colorWithAlphaComponent:0.7];
_transientHUDView.backgroundColor = [UIColor.blackColor colorWithAlphaComponent:0.5];
_transientHUDView.font = [UIFont systemFontOfSize:PublicUtils.isIPhone ? 18 : 22
weight:UIFontWeightSemibold];
_transientHUDView.layer.cornerRadius = 8;
_transientHUDView.layer.masksToBounds = YES;
_transientHUDView.userInteractionEnabled = NO;
[self.view addSubview:_transientHUDView];
UILayoutGuide *safeArea = self.view.safeAreaLayoutGuide;
[NSLayoutConstraint activateConstraints:@[
[_transientHUDView.leadingAnchor constraintEqualToAnchor:safeArea.leadingAnchor constant:35],
[_transientHUDView.topAnchor constraintEqualToAnchor:safeArea.topAnchor constant:12],
[_transientHUDView.widthAnchor constraintLessThanOrEqualToAnchor:safeArea.widthAnchor multiplier:0.8]
]];
}
_transientHUDView.text = text;
_transientHUDView.hidden = NO;
[self.view bringSubviewToFront:_transientHUDView];
}
- (void)returnToMainFrame {
if (@available(iOS 13.0, *)) {
// [ControllerNavigator setUINavigationDelegate:[_mainFrameViewcontroller isInAppView] ? _mainFrameViewcontroller : _mainFrameViewcontroller.hostCollectionVC];
+32 -2
View File
@@ -163,6 +163,36 @@
}
}
},
"2Dp-Qg-chi.segmentTitles[3]" : {
"comment" : "Class = \"UISegmentedControl\"; 2Dp-Qg-chi.segmentTitles[3] = \"×2 Interpolation\"; ObjectID = \"2Dp-Qg-chi\";",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "×2 Interpolation"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "×2 插帧"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "x2 影格插補"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "x2 影格插補"
}
}
}
},
"2TR-Kd-ro4.normalTitle" : {
"comment" : "Class = \"UIButton\"; normalTitle = \"Button\"; ObjectID = \"2TR-Kd-ro4\";",
"extractionState" : "extracted_with_value",
@@ -4598,8 +4628,8 @@
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "帧步进控制"
"state" : "needs_review",
"value" : "帧步调"
}
},
"zh-Hant" : {
+31 -1
View File
@@ -1891,6 +1891,36 @@
}
}
},
"CC6-pu-Uoh.segmentTitles[3]" : {
"comment" : "Class = \"UISegmentedControl\"; CC6-pu-Uoh.segmentTitles[3] = \"×2 Interpolation\"; ObjectID = \"CC6-pu-Uoh\";",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "×2 Interpolation"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "×2 插帧"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "x2 影格插補"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "x2 影格插補"
}
}
}
},
"CEd-Wx-Gml.text" : {
"comment" : "Class = \"UILabel\"; text = \"Duck Other Apps\"; ObjectID = \"CEd-Wx-Gml\";",
"extractionState" : "extracted_with_value",
@@ -2060,7 +2090,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "帧步进控制"
"value" : "帧步调"
}
},
"zh-Hant" : {