frame interpolation Demo1
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -59,7 +59,8 @@ typedef NS_ENUM(NSInteger, ControllerMouseStick) {
|
||||
typedef NS_ENUM(NSInteger, FramePacingMode) {
|
||||
FramePacingModeOff,
|
||||
FramePacingModeLegacy,
|
||||
FramePacingModeQueue
|
||||
FramePacingModeQueue,
|
||||
FramePacingModeInterpolation
|
||||
};
|
||||
|
||||
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" : {
|
||||
"extractionState" : "manual",
|
||||
"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 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 {
|
||||
@@ -46,13 +56,167 @@ final class FrameInterpolator: NSObject {
|
||||
#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
|
||||
|
||||
@@ -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,7 +205,8 @@ 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) {
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
CMVideoDimensions targetDimensions = self.framePacingModeSelector.selectedSegmentIndex == FramePacingModeInterpolation
|
||||
? [FrameInterpolator interpolatableDimensionsBy:originalDimensions]
|
||||
: originalDimensions;
|
||||
|
||||
- (NSInteger) getChosenStreamWidth {
|
||||
// because the 4k resolution can be removed
|
||||
if (self.customResolutionSwitch.isOn) {
|
||||
return resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].width;
|
||||
if(targetDimensions.width == 0 || targetDimensions.height == 0) {
|
||||
targetDimensions = [FrameInterpolator interpolatableDimensionsBy720p:originalDimensions];
|
||||
}
|
||||
|
||||
return resolutionTable[self.resolutionSelector.selectedSegmentIndex].width;
|
||||
NSLog(@"dimensions: %d, %d", targetDimensions.width, targetDimensions.height);
|
||||
|
||||
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];
|
||||
|
||||
@@ -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" : {
|
||||
|
||||
@@ -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" : {
|
||||
|
||||
Reference in New Issue
Block a user