Skip to content

Commit

Permalink
Fix lag when scrolling horizontally to seek, #4153
Browse files Browse the repository at this point in the history
This commit will:
- Add methods displayActive and displayIdle to VideoView
- Add use of a Timer to delay stopping the display link
- Change MPVController to ensure display link is running while seeking
- Change PlayerCore to ensure display link is running while stepping
- Change MainWindowController to ensure display link is running when
  entering and leaving full screen mode

Rebased with develop and corrected merge conflicts.
  • Loading branch information
low-batt authored and uiryuu committed Mar 28, 2023
1 parent 2820682 commit 66fa745
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 4 deletions.
18 changes: 16 additions & 2 deletions iina/MPVController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,12 @@ class MPVController: NSObject {

case MPV_EVENT_SEEK:
player.info.isSeeking = true
DispatchQueue.main.sync {
// When playback is paused the display link may be shutdown in order to not waste energy.
// It must be running when seeking to avoid slowdowns caused by mpv waiting for IINA to call
// mpv_render_report_swap.
player.mainWindow.videoView.displayActive()
}
if needRecordSeekTime {
recordedSeekStartTime = CACurrentMediaTime()
}
Expand All @@ -882,6 +888,14 @@ class MPVController: NSObject {
case MPV_EVENT_PLAYBACK_RESTART:
player.info.isIdle = false
player.info.isSeeking = false
DispatchQueue.main.sync {
// When playback is paused the display link may be shutdown in order to not waste energy.
// The display link will be restarted while seeking. If playback is paused shut it down
// again.
if player.info.isPaused {
player.mainWindow.videoView.displayIdle()
}
}
if needRecordSeekTime {
recordedSeekTimeListener?(CACurrentMediaTime() - recordedSeekStartTime)
recordedSeekTimeListener = nil
Expand Down Expand Up @@ -1051,9 +1065,9 @@ class MPVController: NSObject {
// the timer that synchronizes the UI and the high priority display link thread.
if paused {
player.invalidateTimer()
player.mainWindow.videoView.stopDisplayLink()
player.mainWindow.videoView.displayIdle()
} else {
player.mainWindow.videoView.startDisplayLink()
player.mainWindow.videoView.displayActive()
player.createSyncUITimer()
}
}
Expand Down
24 changes: 22 additions & 2 deletions iina/MainWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1173,6 +1173,9 @@ class MainWindowController: PlayerWindowController {
}

func windowWillEnterFullScreen(_ notification: Notification) {
// When playback is paused the display link is stopped in order to avoid wasting energy on
// needless processing. It must be running while transitioning to full screen mode.
videoView.displayActive()
if isInInteractiveMode {
exitInteractiveMode(immediately: true)
}
Expand Down Expand Up @@ -1234,8 +1237,15 @@ class MainWindowController: PlayerWindowController {
fadeableViews.append(additionalInfoView)
}

if Preference.bool(for: .playWhenEnteringFullScreen) && player.info.isPaused {
player.resume()
if player.info.isPaused {
if Preference.bool(for: .playWhenEnteringFullScreen) {
player.resume()
} else {
// When playback is paused the display link is stopped in order to avoid wasting energy on
// needless processing. It must be running while transitioning to full screen mode. Now that
// the transition has completed it can be stopped.
videoView.displayIdle()
}
}

if #available(macOS 10.12.2, *) {
Expand All @@ -1254,6 +1264,9 @@ class MainWindowController: PlayerWindowController {
}

func windowWillExitFullScreen(_ notification: Notification) {
// When playback is paused the display link is stopped in order to avoid wasting energy on
// needless processing. It must be running while transitioning from full screen mode.
videoView.displayActive()
if isInInteractiveMode {
exitInteractiveMode(immediately: true)
}
Expand Down Expand Up @@ -1304,6 +1317,13 @@ class MainWindowController: PlayerWindowController {
removeBlackWindow()
}

if player.info.isPaused {
// When playback is paused the display link is stopped in order to avoid wasting energy on
// needless processing. It must be running while transitioning from full screen mode. Now that
// the transition has completed it can be stopped.
videoView.displayIdle()
}

if #available(macOS 10.12.2, *) {
player.touchBarSupport.toggleTouchBarEsc(enteringFullScr: false)
}
Expand Down
7 changes: 7 additions & 0 deletions iina/PlayerCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -669,11 +669,18 @@ class PlayerCore: NSObject {
}

func frameStep(backwards: Bool) {
// When playback is paused the display link is stopped in order to avoid wasting energy on
// It must be running when stepping to avoid slowdowns caused by mpv waiting for IINA to call
// mpv_render_report_swap.
mainWindow.videoView.displayActive()
if backwards {
mpv.command(.frameBackStep)
} else {
mpv.command(.frameStep)
}
if info.isPaused {
mainWindow.videoView.displayIdle()
}
}

func screenshot() {
Expand Down
35 changes: 35 additions & 0 deletions iina/VideoView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class VideoView: NSView {

var pendingRedrawsAfterEnteringPIP = 0;

private var displayIdleTimer: Timer?

lazy var hdrSubsystem = Logger.Subsystem(rawValue: "hdr")

// MARK: - Attributes
Expand Down Expand Up @@ -203,6 +205,7 @@ class VideoView: NSView {
guard let link = link else {
Logger.fatal("Cannot Create display link!")
}
guard !CVDisplayLinkIsRunning(link) else { return }
updateDisplayLink()
CVDisplayLinkSetOutputCallback(link, displayLinkCallback, mutableRawPointerOf(obj: player.mpv))
CVDisplayLinkStart(link)
Expand Down Expand Up @@ -251,6 +254,38 @@ class VideoView: NSView {
}
}

// MARK: - Reducing Energy Use

/// Starts the display link if it has been stopped in order to save energy.
func displayActive() {
displayIdleTimer?.invalidate()
startDisplayLink()
}

/// Reduces energy consumption when the display link does not need to be running.
///
/// Adherence to energy efficiency best practices requires that IINA be absolutely idle when there is no reason to be performing any
/// processing, such as when playback is paused. The [CVDisplayLink](https://developer.apple.com/documentation/corevideo/cvdisplaylink-k0k)
/// is a high-priority thread that runs at the refresh rate of a display. If the display is not being updated it is desirable to stop the
/// display link in order to not waste energy on needless processing.
///
/// However, IINA will pause playback for short intervals when performing certain operations. In such cases it does not make sense to
/// shutdown the display link only to have to immediately start it again. To avoid this a `Timer` is used to delay shutting down the
/// display link. If playback becomes active again before the timer has fired then the `Timer` will be invalidated and the display link
/// will not be shutdown.
///
/// - Note: In addition to playback the display link must be running for operations such seeking, stepping and entering and leaving
/// full screen mode.
func displayIdle() {
displayIdleTimer?.invalidate()
// The time of 3 seconds is somewhat arbitrary. As mpv does not provide an event indicating a
// frame step has completed it must not be too short or will catch mpv still drawing when
// stepping.
displayIdleTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in
self.stopDisplayLink()
}
}

func setICCProfile(_ displayId: UInt32) {
if !Preference.bool(for: .loadIccProfile) {
Logger.log("Not using ICC due to user preference", subsystem: hdrSubsystem)
Expand Down

0 comments on commit 66fa745

Please sign in to comment.