Skip to content

Commit

Permalink
feat: add basic custom playback controls
Browse files Browse the repository at this point in the history
feat: double click video to toggle full screen state
  • Loading branch information
godly-devotion committed Mar 26, 2024
1 parent 8668d1d commit 740a583
Show file tree
Hide file tree
Showing 10 changed files with 352 additions and 38 deletions.
8 changes: 8 additions & 0 deletions Front Row.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
032BBB1E2B9FF671003D2FA8 /* WindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032BBB1D2B9FF671003D2FA8 /* WindowController.swift */; };
03407ADD2BA90F1100FB4323 /* WindowCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03407ADC2BA90F1100FB4323 /* WindowCommands.swift */; };
0355CA552BA790E5001AF5EA /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 0355CA542BA790E5001AF5EA /* Localizable.xcstrings */; };
03B2590E2BB242620071FF7C /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B2590D2BB242620071FF7C /* PlayerView.swift */; };
03B259102BB249310071FF7C /* PlayerControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B2590F2BB249310071FF7C /* PlayerControlsView.swift */; };
03B712272B96C40C00C1F753 /* AVKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03B712262B96C40C00C1F753 /* AVKit.framework */; };
03D77E952B9AA13700276A45 /* HelpCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D77E942B9AA13700276A45 /* HelpCommands.swift */; };
03E78C582BA7D2D40063BF06 /* OpenURLView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E78C572BA7D2D40063BF06 /* OpenURLView.swift */; };
Expand All @@ -32,6 +34,8 @@
032BBB1D2B9FF671003D2FA8 /* WindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowController.swift; sourceTree = "<group>"; };
03407ADC2BA90F1100FB4323 /* WindowCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowCommands.swift; sourceTree = "<group>"; };
0355CA542BA790E5001AF5EA /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
03B2590D2BB242620071FF7C /* PlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = "<group>"; };
03B2590F2BB249310071FF7C /* PlayerControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsView.swift; sourceTree = "<group>"; };
03B712262B96C40C00C1F753 /* AVKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVKit.framework; path = System/Library/Frameworks/AVKit.framework; sourceTree = SDKROOT; };
03BACB2D2B96C8A800D24F07 /* FrontRowInfo.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = FrontRowInfo.plist; sourceTree = SOURCE_ROOT; };
03D77E942B9AA13700276A45 /* HelpCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpCommands.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -133,6 +137,8 @@
03EA68522B9630CF003348BE /* ContentView.swift */,
03EE7B0B2BA9F396009F68C5 /* GoToTimeView.swift */,
03E78C572BA7D2D40063BF06 /* OpenURLView.swift */,
03B2590F2BB249310071FF7C /* PlayerControlsView.swift */,
03B2590D2BB242620071FF7C /* PlayerView.swift */,
);
path = Views;
sourceTree = "<group>";
Expand Down Expand Up @@ -271,8 +277,10 @@
buildActionMask = 2147483647;
files = (
03E78C582BA7D2D40063BF06 /* OpenURLView.swift in Sources */,
03B259102BB249310071FF7C /* PlayerControlsView.swift in Sources */,
03407ADD2BA90F1100FB4323 /* WindowCommands.swift in Sources */,
03EA68532B9630CF003348BE /* ContentView.swift in Sources */,
03B2590E2BB242620071FF7C /* PlayerView.swift in Sources */,
03EA68612B96523B003348BE /* FileCommands.swift in Sources */,
03E8F3DD2B9F7B350008CE49 /* AppCommands.swift in Sources */,
03EA68702B96BAD1003348BE /* ViewCommands.swift in Sources */,
Expand Down
84 changes: 84 additions & 0 deletions Front Row.xcodeproj/xcshareddata/xcschemes/Front Row.xcscheme
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1530"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "03EA684C2B9630CF003348BE"
BuildableName = "Front Row.app"
BlueprintName = "Front Row"
ReferencedContainer = "container:Front Row.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "03EA684C2B9630CF003348BE"
BuildableName = "Front Row.app"
BlueprintName = "Front Row"
ReferencedContainer = "container:Front Row.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = " -_NS_4445425547 YES"
isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "03EA684C2B9630CF003348BE"
BuildableName = "Front Row.app"
BlueprintName = "Front Row"
ReferencedContainer = "container:Front Row.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
10 changes: 1 addition & 9 deletions Front Row/FrontRowApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,8 @@ struct FrontRowApp: App {
var body: some Scene {
Window("Front Row", id: "main") {
ContentView()
.environment(\.colorScheme, .dark)
.environment(playEngine)
.onContinuousHover { phase in
switch phase {
case .active:
windowController.resetMouseIdleTimer()
windowController.showTitlebar()
case .ended:
windowController.hideTitlebar()
}
}
.sheet(isPresented: $presentedViewManager.isPresentingOpenURLView) {
OpenURLView()
.frame(minWidth: 600)
Expand Down
3 changes: 1 addition & 2 deletions Front Row/Main Menu/PlaybackCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ struct PlaybackCommands: Commands {
Text("Go Forward 5s")
}
.keyboardShortcut(.rightArrow, modifiers: [])
.disabled(
!playEngine.isLoaded || presentedViewManager.isPresenting)
.disabled(!playEngine.isLoaded || presentedViewManager.isPresenting)

Button {
Task { await playEngine.goBackwards() }
Expand Down
6 changes: 6 additions & 0 deletions Front Row/Support/Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,9 @@ extension NSSize {
}
}
}

extension Double {
var asTimecode: String {
Duration.seconds(self).formatted(.time(pattern: .hourMinuteSecond(padHourToLength: 0)))
}
}
51 changes: 48 additions & 3 deletions Front Row/Support/PlayEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,25 @@ import SwiftUI

private(set) var isLocalFile = false

private var _currentTime: TimeInterval = 0.0

var currentTime: Double {
get {
access(keyPath: \.currentTime)
return _currentTime
}
set {
withMutation(keyPath: \.currentTime) {
let time = CMTimeMakeWithSeconds(newValue, preferredTimescale: 1)
player.seek(to: time)
}
}
}

private(set) var duration: TimeInterval = 0.0

private var _isMuted = false

var isMuted: Bool {
get {
access(keyPath: \.isMuted)
Expand All @@ -43,14 +62,14 @@ import SwiftUI
}
}

private var _isMuted = false

private var videoSize = CGSize.zero

private var subs = Set<AnyCancellable>()

private var currentItemSubs = Set<AnyCancellable>()

private var timeObserver: Any?

init() {
player.preventsDisplaySleepDuringVideoPlayback = true

Expand All @@ -69,6 +88,14 @@ import SwiftUI
self._isMuted = isMuted
}
.store(in: &subs)

addPeriodicTimeObserver()
}

deinit {
for sub in currentItemSubs { sub.cancel() }
currentItemSubs.removeAll()
removePeriodicTimeObserver()
}

func isURLPlayable(url: URL) async -> Bool {
Expand Down Expand Up @@ -98,7 +125,7 @@ import SwiftUI

func openFile(url: URL) {
for sub in currentItemSubs { sub.cancel() }
currentItemSubs = []
currentItemSubs.removeAll()

let playerItem = AVPlayerItem(url: url)

Expand Down Expand Up @@ -215,4 +242,22 @@ import SwiftUI
window.setFrame(newFrame, display: true, animate: true)
window.aspectRatio = videoSize
}

private func addPeriodicTimeObserver() {
let interval = CMTime(seconds: 0.5, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
timeObserver = player.addPeriodicTimeObserver(
forInterval: interval,
queue: .main
) { [weak self] time in
guard let self else { return }
_currentTime = time.seconds
duration = player.currentItem?.duration.seconds ?? 0.0
}
}

private func removePeriodicTimeObserver() {
guard let timeObserver else { return }
player.removeTimeObserver(timeObserver)
self.timeObserver = nil
}
}
5 changes: 2 additions & 3 deletions Front Row/Support/WindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,11 @@ import SwiftUI
var isOnTop: Bool {
get {
access(keyPath: \.isOnTop)
return _isOnTop
return NSApplication.shared.mainWindow?.level == .floating
}
set {
withMutation(keyPath: \.isOnTop) {
_isOnTop = newValue
NSApplication.shared.mainWindow?.level = _isOnTop ? .floating : .normal
NSApplication.shared.mainWindow?.level = newValue ? .floating : .normal
}
}
}
Expand Down
96 changes: 75 additions & 21 deletions Front Row/Views/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,88 @@
// Created by Joshua Park on 3/4/24.
//

import AVKit
import SwiftUI

struct ContentView: View {
@State private var playerControlsShown = true
@State private var mouseIdleTimer: Timer!

var body: some View {
VideoPlayer(player: PlayEngine.shared.player)
.onDrop(
of: [.fileURL],
delegate: AnyDropDelegate(
onValidate: {
$0.hasItemsConforming(to: PlayEngine.supportedFileTypes)
},
onPerform: {
guard let provider = $0.itemProviders(for: [.fileURL]).first else {
return false
}
ZStack(alignment: .bottom) {
PlayerView(player: PlayEngine.shared.player)
.onDrop(
of: [.fileURL],
delegate: AnyDropDelegate(
onValidate: {
$0.hasItemsConforming(to: PlayEngine.supportedFileTypes)
},
onPerform: {
guard let provider = $0.itemProviders(for: [.fileURL]).first else {
return false
}

Task {
guard let url = await provider.getURL() else { return }
PlayEngine.shared.openFile(url: url)
}
Task {
guard let url = await provider.getURL() else { return }
PlayEngine.shared.openFile(url: url)
}

return true
}
return true
}
)
)
)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.ignoresSafeArea()
.onTapGesture(count: 2) {
NSApplication.shared.mainWindow?.toggleFullScreen(nil)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.ignoresSafeArea()

if playerControlsShown {
PlayerControlsView()
.animation(.linear(duration: 0.4), value: playerControlsShown)
}
}
.background {
Color.black.ignoresSafeArea()
}
.onContinuousHover { phase in
switch phase {
case .active:
resetMouseIdleTimer()
showPlayerControls()
WindowController.shared.resetMouseIdleTimer()
WindowController.shared.showTitlebar()
case .ended:
hidePlayerControls()
WindowController.shared.hideTitlebar()
}
}
}

private func resetMouseIdleTimer() {
if mouseIdleTimer != nil {
mouseIdleTimer.invalidate()
mouseIdleTimer = nil
}

mouseIdleTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) {
mouseIdleTimerAction($0)
}
}

private func hidePlayerControls() {
withAnimation {
playerControlsShown = false
}
}

private func showPlayerControls() {
withAnimation {
playerControlsShown = true
}
}

private func mouseIdleTimerAction(_ sender: Timer) {
hidePlayerControls()
}
}

Expand Down
Loading

0 comments on commit 740a583

Please sign in to comment.