frame interpolation Demo1
This commit is contained in:
1 parent
9c9ef4a13f
commit
fc2b1689d0
14 files changed
+652
-67
No files matched your search
@@ -394,12 +394,13 @@
|
|||||||
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</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"/>
|
<rect key="frame" x="0.0" y="25.5" width="380" height="32"/>
|
||||||
<segments>
|
<segments>
|
||||||
<segment title="Off"/>
|
<segment title="Off"/>
|
||||||
<segment title="Legacy"/>
|
<segment title="Legacy"/>
|
||||||
<segment title="Queue Buffering"/>
|
<segment title="Queue Buffering"/>
|
||||||
|
<segment title="×2 Interpolation"/>
|
||||||
</segments>
|
</segments>
|
||||||
<color key="tintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="tintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
<color key="selectedSegmentTintColor" systemColor="linkColor"/>
|
<color key="selectedSegmentTintColor" systemColor="linkColor"/>
|
||||||
@@ -3937,19 +3938,19 @@
|
|||||||
<image name="tray.full.fill" catalog="system" width="128" height="88"/>
|
<image name="tray.full.fill" catalog="system" width="128" height="88"/>
|
||||||
<image name="video.and.waveform" catalog="system" width="128" height="94"/>
|
<image name="video.and.waveform" catalog="system" width="128" height="94"/>
|
||||||
<systemColor name="linkColor">
|
<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>
|
||||||
<systemColor name="systemBackgroundColor">
|
<systemColor name="systemBackgroundColor">
|
||||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
</systemColor>
|
</systemColor>
|
||||||
<systemColor name="systemBrownColor">
|
<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>
|
||||||
<systemColor name="systemGray3Color">
|
<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>
|
||||||
<systemColor name="systemTealColor">
|
<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>
|
</systemColor>
|
||||||
</resources>
|
</resources>
|
||||||
</document>
|
</document>
|
||||||
@@ -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">
|
<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"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24504"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24504"/>
|
||||||
<capability name="Image references" minToolsVersion="12.0"/>
|
<capability name="Image references" minToolsVersion="12.0"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
@@ -14,7 +15,7 @@
|
|||||||
<objects>
|
<objects>
|
||||||
<viewController id="dgh-JZ-Q7z" customClass="MainFrameViewController" sceneMemberID="viewController">
|
<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">
|
<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"/>
|
<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"/>
|
<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">
|
<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"/>
|
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</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"/>
|
<rect key="frame" x="0.0" y="25.5" width="380" height="32"/>
|
||||||
<segments>
|
<segments>
|
||||||
<segment title="Off"/>
|
<segment title="Off"/>
|
||||||
<segment title="Legacy"/>
|
<segment title="Legacy"/>
|
||||||
<segment title="Queue Buffering"/>
|
<segment title="Queue Buffering"/>
|
||||||
|
<segment title="×2 Interpolation"/>
|
||||||
</segments>
|
</segments>
|
||||||
<color key="tintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="tintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
<color key="selectedSegmentTintColor" systemColor="linkColor"/>
|
<color key="selectedSegmentTintColor" systemColor="linkColor"/>
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ typedef NS_ENUM(NSInteger, ControllerMouseStick) {
|
|||||||
typedef NS_ENUM(NSInteger, FramePacingMode) {
|
typedef NS_ENUM(NSInteger, FramePacingMode) {
|
||||||
FramePacingModeOff,
|
FramePacingModeOff,
|
||||||
FramePacingModeLegacy,
|
FramePacingModeLegacy,
|
||||||
FramePacingModeQueue
|
FramePacingModeQueue,
|
||||||
|
FramePacingModeInterpolation
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef NS_ENUM(NSInteger, SettingsMenuMode) {
|
typedef NS_ENUM(NSInteger, SettingsMenuMode) {
|
||||||
|
|||||||
@@ -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" : {
|
"framePacingStackTip" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
//
|
||||||
|
// FrameInterpolator.swift
|
||||||
|
// VoidLink
|
||||||
|
//
|
||||||
|
// Created by True砖家 on 2026/8/6.
|
||||||
|
// Copyright © 2026 True砖家 @ Bilibili. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import CoreVideo
|
import CoreVideo
|
||||||
import ObjectiveC.runtime
|
import ObjectiveC.runtime
|
||||||
@@ -19,12 +27,14 @@ final class FrameInterpolator: NSObject {
|
|||||||
private var previousFrame: Frame?
|
private var previousFrame: Frame?
|
||||||
private var pendingFrames: [PendingFrame] = []
|
private var pendingFrames: [PendingFrame] = []
|
||||||
private var isProcessing = false
|
private var isProcessing = false
|
||||||
|
private var resetRequested = false
|
||||||
private var width = 0
|
private var width = 0
|
||||||
private var height = 0
|
private var height = 0
|
||||||
private var slowUntil = CFTimeInterval(0)
|
private var slowUntil = CFTimeInterval(0)
|
||||||
private var completedProcessingCount = 0
|
private var completedProcessingCount = 0
|
||||||
private var consecutiveSlowFrames = 0
|
private var consecutiveSlowFrames = 0
|
||||||
private var loggedEvents = Set<String>()
|
private var loggedEvents = Set<String>()
|
||||||
|
private var overlayGeneration = 0
|
||||||
|
|
||||||
var isEnabled = false {
|
var isEnabled = false {
|
||||||
didSet {
|
didSet {
|
||||||
@@ -46,13 +56,167 @@ final class FrameInterpolator: NSObject {
|
|||||||
#endif
|
#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 {
|
var isAvailable: Bool {
|
||||||
isEnabled && Self.deviceSupportsInterpolation
|
isEnabled && Self.deviceSupportsInterpolation
|
||||||
}
|
}
|
||||||
|
|
||||||
func reset() {
|
func reset() {
|
||||||
queue.async { [weak self] in
|
queue.async {
|
||||||
self?.resetLocked()
|
self.requestResetLocked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +227,11 @@ final class FrameInterpolator: NSObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard !self.resetRequested else {
|
||||||
|
completion([frame])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
self.pendingFrames.append(PendingFrame(frame: frame, completion: completion))
|
self.pendingFrames.append(PendingFrame(frame: frame, completion: completion))
|
||||||
self.drainPendingFrames()
|
self.drainPendingFrames()
|
||||||
}
|
}
|
||||||
@@ -181,14 +350,16 @@ final class FrameInterpolator: NSObject {
|
|||||||
|
|
||||||
isProcessing = true
|
isProcessing = true
|
||||||
let startedAt = CACurrentMediaTime()
|
let startedAt = CACurrentMediaTime()
|
||||||
processor.process(parameters: parameters) { [weak self] _, error in
|
processor.process(parameters: parameters) { [self] _, error in
|
||||||
guard let self else {
|
|
||||||
completion([frame])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.queue.async {
|
self.queue.async {
|
||||||
self.isProcessing = false
|
self.isProcessing = false
|
||||||
|
|
||||||
|
if self.resetRequested {
|
||||||
|
self.resetLocked()
|
||||||
|
completion([frame])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
self.previousFrame = frame
|
self.previousFrame = frame
|
||||||
|
|
||||||
let elapsed = CACurrentMediaTime() - startedAt
|
let elapsed = CACurrentMediaTime() - startedAt
|
||||||
@@ -201,15 +372,24 @@ final class FrameInterpolator: NSObject {
|
|||||||
)
|
)
|
||||||
else {
|
else {
|
||||||
if let error {
|
if let error {
|
||||||
self.logOnce("process-error-\((error as NSError).code)", "processing failed: \(error)")
|
self.logOnce("process-error", error: error)
|
||||||
} else {
|
} 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])
|
completion([frame])
|
||||||
self.drainPendingFrames()
|
self.drainPendingFrames()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.logOnce(
|
||||||
|
"first-interpolated-frame",
|
||||||
|
"produced the first interpolated frame",
|
||||||
|
overlayText: "Interpolator started.".localized
|
||||||
|
)
|
||||||
self.completedProcessingCount += 1
|
self.completedProcessingCount += 1
|
||||||
if sourceFrameInterval.isFinite, sourceFrameInterval > 0, elapsed >= sourceFrameInterval {
|
if sourceFrameInterval.isFinite, sourceFrameInterval > 0, elapsed >= sourceFrameInterval {
|
||||||
self.consecutiveSlowFrames += 1
|
self.consecutiveSlowFrames += 1
|
||||||
@@ -221,7 +401,8 @@ final class FrameInterpolator: NSObject {
|
|||||||
self.consecutiveSlowFrames = 0
|
self.consecutiveSlowFrames = 0
|
||||||
self.logOnce(
|
self.logOnce(
|
||||||
"sustained-overrun",
|
"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)
|
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) {
|
if !configuration.supportedPixelFormats.contains(inputPixelFormat) {
|
||||||
let supportedFormats = configuration.supportedPixelFormats
|
let supportedFormats = configuration.supportedPixelFormats
|
||||||
.map(pixelFormatName)
|
.map(pixelFormatName)
|
||||||
@@ -289,6 +504,7 @@ final class FrameInterpolator: NSObject {
|
|||||||
&pool
|
&pool
|
||||||
)
|
)
|
||||||
guard status == kCVReturnSuccess, let pool else {
|
guard status == kCVReturnSuccess, let pool else {
|
||||||
|
processor.endSession()
|
||||||
logOnce("pool-error-\(status)", "failed to create destination pixel buffer pool: \(status)")
|
logOnce("pool-error-\(status)", "failed to create destination pixel buffer pool: \(status)")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -366,11 +582,51 @@ final class FrameInterpolator: NSObject {
|
|||||||
CMTimeAdd(first, CMTimeMultiplyByFloat64(CMTimeSubtract(second, first), multiplier: 0.5))
|
CMTimeAdd(first, CMTimeMultiplyByFloat64(CMTimeSubtract(second, first), multiplier: 0.5))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func logOnce(_ event: String, _ message: String) {
|
private func logOnce(_ event: String, _ message: String, overlayText: String? = nil) {
|
||||||
guard loggedEvents.insert(event).inserted else {
|
guard loggedEvents.insert(event).inserted || event == "sustained-overrun" else {
|
||||||
return
|
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 {
|
private func pixelFormatName(_ pixelFormat: OSType) -> String {
|
||||||
@@ -384,12 +640,30 @@ final class FrameInterpolator: NSObject {
|
|||||||
return "\(fourCC) (\(pixelFormat))"
|
return "\(fourCC) (\(pixelFormat))"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func requestResetLocked() {
|
||||||
|
pendingFrames.removeAll()
|
||||||
|
guard !isProcessing else {
|
||||||
|
resetRequested = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resetLocked()
|
||||||
|
}
|
||||||
|
|
||||||
private func resetLocked(keepPendingFrames: Bool = false) {
|
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
|
processor = nil
|
||||||
pixelBufferPool = nil
|
pixelBufferPool = nil
|
||||||
formatDescription = nil
|
formatDescription = nil
|
||||||
previousFrame = nil
|
previousFrame = nil
|
||||||
isProcessing = false
|
isProcessing = false
|
||||||
|
resetRequested = false
|
||||||
width = 0
|
width = 0
|
||||||
height = 0
|
height = 0
|
||||||
slowUntil = 0
|
slowUntil = 0
|
||||||
@@ -485,6 +759,5 @@ extension VTLowLatencyFrameInterpolationConfiguration {
|
|||||||
let supported = limits.supports(width: 1920, height: 1080)
|
let supported = limits.supports(width: 1920, height: 1080)
|
||||||
print("1920x1080 supported:", supported)
|
print("1920x1080 supported:", supported)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -174,7 +174,7 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
|
|||||||
_asyncFrameDequeue = tempSettings.asyncFrameDequeue;
|
_asyncFrameDequeue = tempSettings.asyncFrameDequeue;
|
||||||
NSLog(@"_asyncFrameDequeue %d", _asyncFrameDequeue);
|
NSLog(@"_asyncFrameDequeue %d", _asyncFrameDequeue);
|
||||||
_enableTimebase = false;
|
_enableTimebase = false;
|
||||||
_queueSize = tempSettings.frameQueueSize.intValue;
|
_queueSize = kEnableFrameInterpolation ? 4 : tempSettings.frameQueueSize.intValue;
|
||||||
_needRequeuing = _queueSize>0;
|
_needRequeuing = _queueSize>0;
|
||||||
|
|
||||||
_frameQueue = [FrameQueue sharedInstance];
|
_frameQueue = [FrameQueue sharedInstance];
|
||||||
@@ -205,7 +205,8 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
|
|||||||
[[ImGuiPlots sharedInstance] clearData];
|
[[ImGuiPlots sharedInstance] clearData];
|
||||||
|
|
||||||
DataManager* dataMan = [[DataManager alloc] init];
|
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;
|
_renderingBackend = RENDER_AVSB;
|
||||||
|
|
||||||
if (kEnableFrameInterpolation) {
|
if (kEnableFrameInterpolation) {
|
||||||
@@ -297,6 +298,55 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
|
|||||||
} mutableCopy];
|
} mutableCopy];
|
||||||
#endif
|
#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, *)) {
|
if (@available(iOS 17.0, tvOS 17.0, *)) {
|
||||||
destinationPixelBufferAttributes[(id)kVTVideoDecoderSpecification_RequireHardwareAcceleratedVideoDecoder] = @YES;
|
destinationPixelBufferAttributes[(id)kVTVideoDecoderSpecification_RequireHardwareAcceleratedVideoDecoder] = @YES;
|
||||||
destinationPixelBufferAttributes[(id)kVTDecompressionPropertyKey_GeneratePerFrameHDRDisplayMetadata] = @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);
|
Log(LOG_W, @"Falling back from 420v frame-interpolation decode output to native pixel format %@", nativePixelFormat);
|
||||||
[self->_frameInterpolator reset];
|
[self->_frameInterpolator reset];
|
||||||
self->_frameInterpolator = nil;
|
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];
|
[self setupDecompressionSessionWithAttributes:destinationPixelBufferAttributes];
|
||||||
|
|
||||||
NSInteger targetFrameRate = MIN(self->_frameRate, self->_maxRefreshRate);
|
NSInteger targetFrameRate = MIN(self->_frameRate, self->_maxRefreshRate);
|
||||||
|
|||||||
@@ -533,11 +533,14 @@ import UIKit
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc public static func handleLegacyFramePacingTip(in vc: UIViewController?, with selector: UISegmentedControl, passAlert: Bool = false, uiAction: (() -> Void)? = nil) {
|
@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?()
|
uiAction?()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if selector.previousSelectedSegmentIndex != FramePacingMode.queue.rawValue {
|
if selector.previousSelectedSegmentIndex != FramePacingMode.queue.rawValue
|
||||||
|
&& selector.previousSelectedSegmentIndex != FramePacingMode.interpolation.rawValue {
|
||||||
uiAction?()
|
uiAction?()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
|
#import <CoreMedia/CoreMedia.h>
|
||||||
#import "DiscoveryManager.h"
|
#import "DiscoveryManager.h"
|
||||||
#import "PairManager.h"
|
#import "PairManager.h"
|
||||||
#import "StreamConfiguration.h"
|
#import "StreamConfiguration.h"
|
||||||
@@ -49,7 +50,7 @@
|
|||||||
|
|
||||||
- (NSInteger)requestForBitrate:(NSInteger)bitrateKbps;
|
- (NSInteger)requestForBitrate:(NSInteger)bitrateKbps;
|
||||||
#endif
|
#endif
|
||||||
- (void)fillResolutionTable:(CGSize*)resolutionTable externalDisplayMode:(NSInteger)externalDisplayMode;
|
- (void)fillResolutionTable:(CMVideoDimensions *)resolutionTable externalDisplayMode:(NSInteger)externalDisplayMode;
|
||||||
- (void)setNeedsUpdateAllowedOrientation;
|
- (void)setNeedsUpdateAllowedOrientation;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -837,7 +837,7 @@ static NSMutableSet* hostList;
|
|||||||
- (void) reloadStreamConfig {
|
- (void) reloadStreamConfig {
|
||||||
DataManager* dataMan = [[DataManager alloc] init];
|
DataManager* dataMan = [[DataManager alloc] init];
|
||||||
TemporarySettings* streamSettings = [dataMan getSettings];
|
TemporarySettings* streamSettings = [dataMan getSettings];
|
||||||
|
[VideoDecoderRenderer setFrameInterpolationEnabled: streamSettings.framePacingMode.intValue == FramePacingModeInterpolation];
|
||||||
_streamConfig.frameRate = [streamSettings.framerate intValue];
|
_streamConfig.frameRate = [streamSettings.framerate intValue];
|
||||||
if (@available(iOS 10.3, *)) {
|
if (@available(iOS 10.3, *)) {
|
||||||
UIWindow *window = UIApplication.sharedApplication.windows.firstObject;
|
UIWindow *window = UIApplication.sharedApplication.windows.firstObject;
|
||||||
@@ -854,6 +854,8 @@ static NSMutableSet* hostList;
|
|||||||
|
|
||||||
_streamConfig.height = [streamSettings.height intValue];
|
_streamConfig.height = [streamSettings.height intValue];
|
||||||
_streamConfig.width = [streamSettings.width intValue];
|
_streamConfig.width = [streamSettings.width intValue];
|
||||||
|
|
||||||
|
NSLog(@"saveSettings.width %d, %d", _streamConfig.width, _streamConfig.height);
|
||||||
#if TARGET_OS_TV
|
#if TARGET_OS_TV
|
||||||
// Don't allow streaming 4K on the Apple TV HD
|
// Don't allow streaming 4K on the Apple TV HD
|
||||||
struct utsname systemInfo;
|
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;
|
UIWindow *window = self.view.window;
|
||||||
NSLog(@" window %@", window);
|
NSLog(@" window %@", window);
|
||||||
|
|
||||||
@@ -2117,35 +2119,46 @@ static NSMutableSet* hostList;
|
|||||||
|
|
||||||
bool needSwapWidthAndHeight = appWindowWidth < appWindowHeight;
|
bool needSwapWidthAndHeight = appWindowWidth < appWindowHeight;
|
||||||
|
|
||||||
resolutionTable[0] = CGSizeMake(1280, 720);
|
resolutionTable[0] = (CMVideoDimensions){ .width = 1280, .height = 720 };
|
||||||
resolutionTable[1] = CGSizeMake(1920, 1080);
|
resolutionTable[1] = (CMVideoDimensions){ .width = 1920, .height = 1080 };
|
||||||
resolutionTable[2] = CGSizeMake(3840, 2160);
|
resolutionTable[2] = (CMVideoDimensions){ .width = 3840, .height = 2160 };
|
||||||
|
|
||||||
for(uint8_t i=0;i<6;i++){
|
for(uint8_t i=0;i<6;i++){
|
||||||
CGFloat longSideLen = resolutionTable[i].height > resolutionTable[i].width ? resolutionTable[i].height : resolutionTable[i].width;
|
int32_t longSideLen = MAX(resolutionTable[i].width, resolutionTable[i].height);
|
||||||
CGFloat shortSideLen = resolutionTable[i].height < resolutionTable[i].width ? resolutionTable[i].height : resolutionTable[i].width;
|
int32_t shortSideLen = MIN(resolutionTable[i].width, resolutionTable[i].height);
|
||||||
if(needSwapWidthAndHeight) resolutionTable[i] = CGSizeMake(shortSideLen, longSideLen);
|
if(needSwapWidthAndHeight) resolutionTable[i] = (CMVideoDimensions){ .width = shortSideLen, .height = longSideLen };
|
||||||
else resolutionTable[i] = CGSizeMake(longSideLen, shortSideLen);
|
else resolutionTable[i] = (CMVideoDimensions){ .width = longSideLen, .height = shortSideLen };
|
||||||
}
|
}
|
||||||
|
|
||||||
// add app window resolution and not swap width and height
|
// add app window resolution and not swap width and height
|
||||||
resolutionTable[3] = CGSizeMake(safeAreaWidth, appWindowHeight);
|
resolutionTable[3] = (CMVideoDimensions){ .width = (int32_t)safeAreaWidth, .height = (int32_t)appWindowHeight };
|
||||||
resolutionTable[4] = CGSizeMake(appWindowWidth, appWindowHeight);
|
resolutionTable[4] = (CMVideoDimensions){ .width = (int32_t)appWindowWidth, .height = (int32_t)appWindowHeight };
|
||||||
}
|
}
|
||||||
|
|
||||||
-(void) updateResolutionAccordingly {
|
-(void) updateResolutionAccordingly {
|
||||||
DataManager* dataMan = [[DataManager alloc] init];
|
DataManager* dataMan = [[DataManager alloc] init];
|
||||||
Settings *currentSettings = [dataMan retrieveSettings];
|
Settings *currentSettings = [dataMan retrieveSettings];
|
||||||
|
|
||||||
CGSize tempResolutionTable[6] = {0};
|
CMVideoDimensions tempResolutionTable[6] = {0};
|
||||||
tempResolutionTable[5] = CGSizeMake(currentSettings.width.intValue, currentSettings.height.intValue);
|
tempResolutionTable[5] = (CMVideoDimensions){
|
||||||
|
.width = (int32_t)currentSettings.width.intValue,
|
||||||
|
.height = (int32_t)currentSettings.height.intValue,
|
||||||
|
};
|
||||||
[self fillResolutionTable:tempResolutionTable externalDisplayMode:currentSettings.externalDisplayMode.intValue];
|
[self fillResolutionTable:tempResolutionTable externalDisplayMode:currentSettings.externalDisplayMode.intValue];
|
||||||
|
|
||||||
int selectedIndex = currentSettings.resolutionSelected.intValue;
|
int selectedIndex = currentSettings.resolutionSelected.intValue;
|
||||||
if (selectedIndex >= 0 && selectedIndex < 6) {
|
if (selectedIndex >= 0 && selectedIndex < 6) {
|
||||||
CGSize selectedSize = tempResolutionTable[selectedIndex];
|
CMVideoDimensions originalDimensions = tempResolutionTable[selectedIndex];
|
||||||
currentSettings.width = @(selectedSize.width);
|
|
||||||
currentSettings.height = @(selectedSize.height);
|
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);
|
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_SIZE = 6;
|
||||||
const int RESOLUTION_TABLE_CUSTOM_INDEX = RESOLUTION_TABLE_SIZE - 1;
|
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)controllerTypeToSegmentIndex:(uint16_t)type{
|
||||||
uint16_t index;
|
uint16_t index;
|
||||||
@@ -882,6 +882,7 @@ BOOL isCustomResolution(int resolutionSelected) {
|
|||||||
|
|
||||||
self.framePacingStack.hasInfoTag = YES;
|
self.framePacingStack.hasInfoTag = YES;
|
||||||
[self addSetting:self.framePacingStack ofId:@"framePacingStack" to:videoSection];
|
[self addSetting:self.framePacingStack ofId:@"framePacingStack" to:videoSection];
|
||||||
|
if(!FrameInterpolator.deviceSupportsInterpolation) [self.framerateSelector setEnabled:false forSegmentAtIndex:FramePacingModeInterpolation];
|
||||||
|
|
||||||
self.frameQueueSizeStack.hasDynamicLabel = YES;
|
self.frameQueueSizeStack.hasDynamicLabel = YES;
|
||||||
[self addSetting:self.frameQueueSizeStack ofId:@"frameQueueSizeStack" to:videoSection];
|
[self addSetting:self.frameQueueSizeStack ofId:@"frameQueueSizeStack" to:videoSection];
|
||||||
@@ -2129,7 +2130,10 @@ BOOL isCustomResolution(int resolutionSelected) {
|
|||||||
self.resolutionSelector.selectedSegmentIndex = 3;
|
self.resolutionSelector.selectedSegmentIndex = 3;
|
||||||
[self.resolutionSelector setNeedsLayout];
|
[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];
|
[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 == FramePacingModeOff || sender.selectedSegmentIndex == FramePacingModeLegacy) forStack:self.frameQueueSizeStack];
|
||||||
// [self setHidden:(sender.selectedSegmentIndex != FramePacingModeQueue) forStack:self.frameTimebaseStack];
|
// [self setHidden:(sender.selectedSegmentIndex != FramePacingModeQueue) forStack:self.frameTimebaseStack];
|
||||||
[self setHidden:(sender.selectedSegmentIndex != FramePacingModeQueue
|
[self setHidden:(sender.selectedSegmentIndex != FramePacingModeQueue
|
||||||
|
|| sender.selectedSegmentIndex != FramePacingModeInterpolation
|
||||||
|| self.renderingBackendSelector.selectedSegmentIndex != 0) forStack:self.asyncFrameDequeueStack];
|
|| self.renderingBackendSelector.selectedSegmentIndex != 0) forStack:self.asyncFrameDequeueStack];
|
||||||
|
|
||||||
if(sender.selectedSegmentIndex == FramePacingModeOff || sender.selectedSegmentIndex == FramePacingModeLegacy){
|
if(sender.selectedSegmentIndex == FramePacingModeOff || sender.selectedSegmentIndex == FramePacingModeLegacy){
|
||||||
@@ -2698,6 +2703,16 @@ BOOL isCustomResolution(int resolutionSelected) {
|
|||||||
[self.enableGraphsSwitch setEnabled:sender.selectedSegmentIndex == FramePacingModeQueue];
|
[self.enableGraphsSwitch setEnabled:sender.selectedSegmentIndex == FramePacingModeQueue];
|
||||||
[self.graphOpacityStepper setEnabled:self.enableGraphsSwitch.isOn];
|
[self.graphOpacityStepper setEnabled:self.enableGraphsSwitch.isOn];
|
||||||
[self setHidden:(sender.selectedSegmentIndex == FramePacingModeOff || sender.selectedSegmentIndex == FramePacingModeLegacy) forStack:self.performanceGraphStack];
|
[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] setBool:YES forKey:key];
|
||||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||||
|
|
||||||
|
if(self.framerateSelector.selectedSegmentIndex == 2
|
||||||
|
&& self.framePacingModeSelector.selectedSegmentIndex == FramePacingModeInterpolation) self.framerateSelector.selectedSegmentIndex = self.framerateSelector.previousSelectedSegmentIndex;
|
||||||
|
|
||||||
// NSInteger fps = [self getChosenFrameRate];
|
// NSInteger fps = [self getChosenFrameRate];
|
||||||
// [self touchMoveEventIntervalSliderMoved:self.touchMoveEventIntervalSlider];
|
// [self touchMoveEventIntervalSliderMoved:self.touchMoveEventIntervalSlider];
|
||||||
[self updateBitrate];
|
[self updateBitrate];
|
||||||
@@ -3655,8 +3673,9 @@ BOOL isCustomResolution(int resolutionSelected) {
|
|||||||
|
|
||||||
- (void) updateBitrate {
|
- (void) updateBitrate {
|
||||||
NSInteger fps = [self getChosenFrameRate];
|
NSInteger fps = [self getChosenFrameRate];
|
||||||
NSInteger width = [self getChosenStreamWidth];
|
CMVideoDimensions dimensions = [self getChosenStreamDimensions];
|
||||||
NSInteger height = [self getChosenStreamHeight];
|
NSInteger width = dimensions.width;
|
||||||
|
NSInteger height = dimensions.height;
|
||||||
NSInteger defaultBitrate;
|
NSInteger defaultBitrate;
|
||||||
|
|
||||||
// This logic is shamelessly stolen from Moonlight Qt:
|
// This logic is shamelessly stolen from Moonlight Qt:
|
||||||
@@ -3792,7 +3811,10 @@ BOOL isCustomResolution(int resolutionSelected) {
|
|||||||
width = MAX(width, 256);
|
width = MAX(width, 256);
|
||||||
height = MAX(height, 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 updateBitrate];
|
||||||
[self updateResolutionDisplayLabel];
|
[self updateResolutionDisplayLabel];
|
||||||
|
|
||||||
@@ -3814,8 +3836,9 @@ BOOL isCustomResolution(int resolutionSelected) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void) updateResolutionDisplayLabel {
|
- (void) updateResolutionDisplayLabel {
|
||||||
NSInteger width = [self getChosenStreamWidth];
|
CMVideoDimensions dimensions = [self getChosenStreamDimensions];
|
||||||
NSInteger height = [self getChosenStreamHeight];
|
NSInteger width = dimensions.width;
|
||||||
|
NSInteger height = dimensions.height;
|
||||||
|
|
||||||
[self findDynamicLabelFromStack:_resolutionSelectorStack].text = [NSString stringWithFormat:@"%ld × %ld", (long)width, (long)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
|
// because the 4k resolution can be removed
|
||||||
|
CMVideoDimensions originalDimensions;
|
||||||
if (self.customResolutionSwitch.isOn) {
|
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;
|
CMVideoDimensions targetDimensions = self.framePacingModeSelector.selectedSegmentIndex == FramePacingModeInterpolation
|
||||||
}
|
? [FrameInterpolator interpolatableDimensionsBy:originalDimensions]
|
||||||
|
: originalDimensions;
|
||||||
|
|
||||||
- (NSInteger) getChosenStreamWidth {
|
if(targetDimensions.width == 0 || targetDimensions.height == 0) {
|
||||||
// because the 4k resolution can be removed
|
targetDimensions = [FrameInterpolator interpolatableDimensionsBy720p:originalDimensions];
|
||||||
if (self.customResolutionSwitch.isOn) {
|
|
||||||
return resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].width;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolutionTable[self.resolutionSelector.selectedSegmentIndex].width;
|
NSLog(@"dimensions: %d, %d", targetDimensions.width, targetDimensions.height);
|
||||||
|
|
||||||
|
return targetDimensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (UIStackView *)findFlatStackViewFrom:(UIView *)view {
|
- (UIStackView *)findFlatStackViewFrom:(UIView *)view {
|
||||||
@@ -4250,10 +4277,17 @@ BOOL isCustomResolution(int resolutionSelected) {
|
|||||||
|
|
||||||
CGFloat settingsMenuOffset = _rememberFoldStateSwitch.isOn ? _scrollView.contentOffset.y : 0;
|
CGFloat settingsMenuOffset = _rememberFoldStateSwitch.isOn ? _scrollView.contentOffset.y : 0;
|
||||||
|
|
||||||
NSInteger height = self.mainFrameViewController.isStreaming ? currentSettings.height.intValue : [self getChosenStreamHeight];
|
CMVideoDimensions dimensions;
|
||||||
NSInteger width = self.mainFrameViewController.isStreaming ? currentSettings.width.intValue : [self getChosenStreamWidth];
|
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];
|
NSInteger framerate = [self getChosenFrameRate];
|
||||||
|
|
||||||
|
|||||||
@@ -67,5 +67,7 @@
|
|||||||
- (void)returnToMainFrame;
|
- (void)returnToMainFrame;
|
||||||
- (void)bringUpToolboxMenu;
|
- (void)bringUpToolboxMenu;
|
||||||
- (void)bringUpToolboxMenuWithoutWidgetLayoutTool;
|
- (void)bringUpToolboxMenuWithoutWidgetLayoutTool;
|
||||||
|
- (void)updateOverlayText:(NSString*)text;
|
||||||
|
- (void)updateTransientHUDText:(NSString*)text;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -89,6 +89,7 @@ static __weak StreamFrameViewController *VLSharedStreamFrameViewController = nil
|
|||||||
NSTimer *_inactivityTimer;
|
NSTimer *_inactivityTimer;
|
||||||
NSTimer *_statsUpdateTimer;
|
NSTimer *_statsUpdateTimer;
|
||||||
PaddedLabel *_overlayView;
|
PaddedLabel *_overlayView;
|
||||||
|
PaddedLabel *_transientHUDView;
|
||||||
UITapGestureRecognizer *_menuTapGestureRecognizer;
|
UITapGestureRecognizer *_menuTapGestureRecognizer;
|
||||||
UITapGestureRecognizer *_menuDoubleTapGestureRecognizer;
|
UITapGestureRecognizer *_menuDoubleTapGestureRecognizer;
|
||||||
UITapGestureRecognizer *_playPauseTapGestureRecognizer;
|
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
|
// 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
|
// 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.
|
// 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 setBounds:CGRectMake(self.view.frame.origin.x,
|
||||||
_overlayView.frame.origin.y,
|
_overlayView.frame.origin.y,
|
||||||
self.view.frame.size.width,
|
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 {
|
- (void)returnToMainFrame {
|
||||||
if (@available(iOS 13.0, *)) {
|
if (@available(iOS 13.0, *)) {
|
||||||
// [ControllerNavigator setUINavigationDelegate:[_mainFrameViewcontroller isInAppView] ? _mainFrameViewcontroller : _mainFrameViewcontroller.hostCollectionVC];
|
// [ControllerNavigator setUINavigationDelegate:[_mainFrameViewcontroller isInAppView] ? _mainFrameViewcontroller : _mainFrameViewcontroller.hostCollectionVC];
|
||||||
|
|||||||
@@ -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" : {
|
"2TR-Kd-ro4.normalTitle" : {
|
||||||
"comment" : "Class = \"UIButton\"; normalTitle = \"Button\"; ObjectID = \"2TR-Kd-ro4\";",
|
"comment" : "Class = \"UIButton\"; normalTitle = \"Button\"; ObjectID = \"2TR-Kd-ro4\";",
|
||||||
"extractionState" : "extracted_with_value",
|
"extractionState" : "extracted_with_value",
|
||||||
@@ -4598,8 +4628,8 @@
|
|||||||
},
|
},
|
||||||
"zh-Hans" : {
|
"zh-Hans" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "帧步进控制"
|
"value" : "帧步调"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-Hant" : {
|
"zh-Hant" : {
|
||||||
|
|||||||
@@ -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" : {
|
"CEd-Wx-Gml.text" : {
|
||||||
"comment" : "Class = \"UILabel\"; text = \"Duck Other Apps\"; ObjectID = \"CEd-Wx-Gml\";",
|
"comment" : "Class = \"UILabel\"; text = \"Duck Other Apps\"; ObjectID = \"CEd-Wx-Gml\";",
|
||||||
"extractionState" : "extracted_with_value",
|
"extractionState" : "extracted_with_value",
|
||||||
@@ -2060,7 +2090,7 @@
|
|||||||
"zh-Hans" : {
|
"zh-Hans" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "needs_review",
|
"state" : "needs_review",
|
||||||
"value" : "帧步进控制"
|
"value" : "帧步调"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-Hant" : {
|
"zh-Hant" : {
|
||||||
|
|||||||
Reference in new issue
Block a user