Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an option to match refresh rate in fullscreen #3527

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions iina/Base.lproj/PrefCodecViewController.xib
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
</customView>
<userDefaultsController representsSharedInstance="YES" id="pxc-7C-SGP"/>
<customView id="gZf-gF-XoY">
<rect key="frame" x="0.0" y="0.0" width="444" height="202"/>
<rect key="frame" x="0.0" y="0.0" width="444" height="278"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<textField identifier="SectionTitleVideo" horizontalHuggingPriority="251" verticalHuggingPriority="750" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2BH-mP-kfr">
Expand Down Expand Up @@ -148,20 +148,43 @@
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ota-uw-6BI">
<rect key="frame" x="118" y="53" width="215" height="18"/>
<buttonCell key="cell" type="check" title="Match refresh rate in fullscreen" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="OvP-TR-cOd">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<binding destination="pxc-7C-SGP" name="value" keyPath="values.matchRefreshRate" id="I7K-w8-zNh"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="TSW-aG-h9J">
<rect key="frame" x="118" y="8" width="328" height="42"/>
<textFieldCell key="cell" controlSize="small" sendsActionOnEndEditing="YES" id="Lfo-JP-9et">
<font key="font" metaFont="label" size="11"/>
<string key="title">Switch to a matching refresh rate (if there is any) when the player goes fullscreen. This can eliminate stuttering, and, on some external displays, enable frame interpolation.</string>
<color key="textColor" name="disabledControlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="4rB-d2-drE" firstAttribute="baseline" secondItem="EWs-Ib-pVZ" secondAttribute="baseline" id="0Wl-YU-BME"/>
<constraint firstItem="2BH-mP-kfr" firstAttribute="top" secondItem="gZf-gF-XoY" secondAttribute="top" constant="8" id="0jh-iZ-prT"/>
<constraint firstItem="40Z-9R-jw0" firstAttribute="leading" secondItem="xXS-nP-Chk" secondAttribute="leading" id="1of-sZ-BN6"/>
<constraint firstItem="0Re-PY-hmC" firstAttribute="top" secondItem="4rB-d2-drE" secondAttribute="bottom" constant="4" id="5Mm-wm-VFG"/>
<constraint firstAttribute="bottom" secondItem="IQ8-K4-5CX" secondAttribute="bottom" constant="12" id="6dx-e8-7Ck"/>
<constraint firstItem="Ota-uw-6BI" firstAttribute="leading" secondItem="IQ8-K4-5CX" secondAttribute="leading" id="6XV-jm-sBT"/>
<constraint firstItem="40Z-9R-jw0" firstAttribute="top" secondItem="xXS-nP-Chk" secondAttribute="bottom" constant="4" id="87O-GK-HSl"/>
<constraint firstAttribute="trailing" secondItem="IQ8-K4-5CX" secondAttribute="trailing" id="ASv-Wn-ieW"/>
<constraint firstItem="4rB-d2-drE" firstAttribute="leading" secondItem="paR-ax-h5M" secondAttribute="leading" id="Can-oM-yLm"/>
<constraint firstItem="H9a-N4-xDt" firstAttribute="top" secondItem="40Z-9R-jw0" secondAttribute="bottom" constant="12" id="EeP-q5-sCA"/>
<constraint firstItem="EWs-Ib-pVZ" firstAttribute="leading" secondItem="4rB-d2-drE" secondAttribute="trailing" constant="8" id="Fj7-lr-KGg"/>
<constraint firstAttribute="trailing" secondItem="0Re-PY-hmC" secondAttribute="trailing" id="HJH-5L-Dd4"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="Ota-uw-6BI" secondAttribute="trailing" constant="12" id="Ije-hx-B8b"/>
<constraint firstItem="2BH-mP-kfr" firstAttribute="trailing" relation="lessThanOrEqual" secondItem="gZf-gF-XoY" secondAttribute="leading" constant="120" id="KBf-6v-pL9"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="xXS-nP-Chk" secondAttribute="trailing" constant="12" id="QNu-hr-TdO"/>
<constraint firstItem="TSW-aG-h9J" firstAttribute="leading" secondItem="Ota-uw-6BI" secondAttribute="leading" id="TQY-eJ-m7f"/>
<constraint firstAttribute="trailing" secondItem="TSW-aG-h9J" secondAttribute="trailing" id="TkD-AY-QZP"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="H9a-N4-xDt" secondAttribute="trailing" constant="12" id="TxZ-c7-rVn"/>
<constraint firstItem="IQ8-K4-5CX" firstAttribute="top" secondItem="H9a-N4-xDt" secondAttribute="bottom" constant="4" id="W29-Gj-v7X"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="kas-7q-TbK" secondAttribute="trailing" constant="20" symbolic="YES" id="XnT-jM-uWw"/>
Expand All @@ -174,13 +197,15 @@
<constraint firstItem="NZA-64-tXh" firstAttribute="baseline" secondItem="paR-ax-h5M" secondAttribute="baseline" id="hly-ga-NBA"/>
<constraint firstItem="EWs-Ib-pVZ" firstAttribute="leading" secondItem="NZA-64-tXh" secondAttribute="leading" id="k0a-vZ-1e2"/>
<constraint firstItem="NZA-64-tXh" firstAttribute="leading" secondItem="paR-ax-h5M" secondAttribute="trailing" constant="8" id="mRg-ce-YRy"/>
<constraint firstItem="Ota-uw-6BI" firstAttribute="top" secondItem="IQ8-K4-5CX" secondAttribute="bottom" constant="12" id="o64-sw-FsN"/>
<constraint firstItem="2BH-mP-kfr" firstAttribute="leading" secondItem="gZf-gF-XoY" secondAttribute="leading" id="pDP-Rm-tDg"/>
<constraint firstItem="0Re-PY-hmC" firstAttribute="leading" secondItem="4rB-d2-drE" secondAttribute="leading" id="q9a-bj-3IP"/>
<constraint firstItem="TSW-aG-h9J" firstAttribute="top" secondItem="Ota-uw-6BI" secondAttribute="bottom" constant="4" id="qAU-0r-e5f"/>
<constraint firstItem="4rB-d2-drE" firstAttribute="top" secondItem="paR-ax-h5M" secondAttribute="bottom" constant="16" id="rCv-37-pHD"/>
<constraint firstItem="kas-7q-TbK" firstAttribute="baseline" secondItem="paR-ax-h5M" secondAttribute="baseline" id="rWF-ZF-Tlb"/>
<constraint firstAttribute="trailing" secondItem="40Z-9R-jw0" secondAttribute="trailing" id="u8t-cI-LR5"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="EWs-Ib-pVZ" secondAttribute="trailing" constant="20" symbolic="YES" id="uSA-lp-egl"/>
<constraint firstItem="H9a-N4-xDt" firstAttribute="top" secondItem="40Z-9R-jw0" secondAttribute="bottom" constant="8" id="w3M-iD-Ho3"/>
<constraint firstAttribute="bottom" secondItem="TSW-aG-h9J" secondAttribute="bottom" constant="8" id="w87-4D-FL9"/>
<constraint firstItem="H9a-N4-xDt" firstAttribute="leading" secondItem="40Z-9R-jw0" secondAttribute="leading" id="ybt-SY-BgR"/>
</constraints>
<point key="canvasLocation" x="783" y="394.5"/>
Expand Down
72 changes: 69 additions & 3 deletions iina/MainWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -576,18 +576,29 @@ class MainWindowController: PlayerWindowController {

addObserver(to: .default, forName: .iinaFileLoaded, object: player) { [unowned self] _ in
self.quickSettingView.reload()

if self.fsState.isFullscreen {
matchRefreshRate()
}
}

addObserver(to: .default, forName: NSApplication.didChangeScreenParametersNotification) { [unowned self] _ in
// This observer handles a situation that the user connected a new screen or removed a screen
let screenCount = NSScreen.screens.count
if self.fsState.isFullscreen && Preference.bool(for: .blackOutMonitor) && self.cachedScreenCount != screenCount {
self.removeBlackWindow()
self.blackOutOtherMonitors()
if self.fsState.isFullscreen && self.cachedScreenCount != screenCount {
if Preference.bool(for: .blackOutMonitor) {
self.removeBlackWindow()
self.blackOutOtherMonitors()
}

restoreRefreshRate()
matchRefreshRate()
}

// Update the cached value
self.cachedScreenCount = screenCount
self.videoView.updateDisplayLink()

// In normal full screen mode AppKit will automatically adjust the window frame if the window
// is moved to a new screen such as when the window is on an external display and that display
// is disconnected. In legacy full screen mode IINA is responsible for adjusting the window's
Expand Down Expand Up @@ -1144,11 +1155,63 @@ class MainWindowController: PlayerWindowController {
NSMenu.setMenuBarVisible(true)
}

private var userDisplay: UInt32?
private var userDisplayMode: CGDisplayMode?

func matchRefreshRate() {
if Preference.bool(for: .matchRefreshRate) {
// [23.976, 47.952, 24, 48], [29.97, 59.94, 30, 60]
// [24, 48], [25, 50], [30, 60]
let videoFps = player.mpv.getDouble(MPVProperty.containerFps)
let refreshRates = [videoFps, videoFps * 2, videoFps.rounded(), videoFps.rounded() * 2]

if let curDisplay = videoView.currentDisplay, let curDisplayMode = CGDisplayCopyDisplayMode(curDisplay) {
let displayModes = CGDisplayCopyAllDisplayModes(curDisplay, [kCGDisplayShowDuplicateLowResolutionModes: true] as CFDictionary) as! [CGDisplayMode]
matching: for refreshRate in refreshRates {
for displayMode in displayModes {
if (displayMode.height != curDisplayMode.height
|| displayMode.width != curDisplayMode.width
|| displayMode.pixelHeight != curDisplayMode.pixelHeight
|| displayMode.pixelWidth != curDisplayMode.pixelWidth) {
continue
}

// 24 - 23.976 = 0.024, avoid matching 23.976 to 24 when 23.976 is available
// or vice versa on first pass, prefer 47.952 than 24 for 23.976
if abs(displayMode.refreshRate - refreshRate) < 0.02 {
CGDisplaySetDisplayMode(curDisplay, displayMode, nil)
player.mpv.setDouble(MPVOption.Video.overrideDisplayFps, displayMode.refreshRate)

if userDisplay == nil && userDisplayMode == nil {
userDisplay = curDisplay
userDisplayMode = curDisplayMode
}

break matching
}
}
}
}
}
}

func restoreRefreshRate() {
if let userDisplay = userDisplay, let userDisplayMode = userDisplayMode {
CGDisplaySetDisplayMode(userDisplay, userDisplayMode, nil)
player.mpv.setDouble(MPVOption.Video.overrideDisplayFps, 0)
}

userDisplay = nil; userDisplayMode = nil
}

func windowWillEnterFullScreen(_ notification: Notification) {
if isInInteractiveMode {
exitInteractiveMode(immediately: true)
}

// Match refresh rate
matchRefreshRate()

// Set the appearance to match the theme so the titlebar matches the theme
let iinaTheme = Preference.enum(for: .themeMaterial) as Preference.Theme
if #available(macOS 10.14, *) {
Expand Down Expand Up @@ -1228,6 +1291,9 @@ class MainWindowController: PlayerWindowController {
exitInteractiveMode(immediately: true)
}

// Restore refresh rate
restoreRefreshRate()

// show titleBarView
if oscPosition == .top {
oscTopMainViewTopConstraint.constant = OSCTopMainViewMarginTop
Expand Down
2 changes: 2 additions & 0 deletions iina/Preference.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ struct Preference {
static let hardwareDecoder = Key("hardwareDecoder")
static let forceDedicatedGPU = Key("forceDedicatedGPU")
static let loadIccProfile = Key("loadIccProfile")
static let matchRefreshRate = Key("matchRefreshRate")

static let audioThreads = Key("audioThreads")
static let audioLanguage = Key("audioLanguage")
Expand Down Expand Up @@ -725,6 +726,7 @@ struct Preference {
.hardwareDecoder: HardwareDecoderOption.auto.rawValue,
.forceDedicatedGPU: false,
.loadIccProfile: true,
.matchRefreshRate: false,
.audioThreads: 0,
.audioLanguage: "",
.maxVolume: 100,
Expand Down