diff --git a/README.md b/README.md index 3d51fc32..42c86e76 100644 --- a/README.md +++ b/README.md @@ -38,13 +38,7 @@ Thank you for your interest in Swiftfin, please check out the [Contribution Guid ### Intended Behaviors Due to Technical Limitations -The following behaviors are intended due to technical limitations: +The following behaviors are intended due to technical limitations with VLCKit: -- Pausing playback when app is backgrounded - - Due to VLCKit pausing video output at the same moment - -- Audio delay after un-pausing - - Due to VLCKit, may be fixed in VLCKit v4 - -- No aspect fill - - VLCKit doesn't have the ability to aspect fill the view that the video output occupies +- Pausing playback when app is backgrounded as VLCKit pauses video output at the same time +- Audio delay when starting playback and un-pausing, may be fixed in VLCKit v4 diff --git a/Shared/Extensions/CGSizeExtensions.swift b/Shared/Extensions/CGSizeExtensions.swift index 08026424..664eb5d0 100644 --- a/Shared/Extensions/CGSizeExtensions.swift +++ b/Shared/Extensions/CGSizeExtensions.swift @@ -13,4 +13,19 @@ extension CGSize { static func Circle(radius: CGFloat) -> CGSize { CGSize(width: radius, height: radius) } + + // From https://gist.github.com/jkosoy/c835fea2c03e76720c77 + static func aspectFill(aspectRatio: CGSize, minimumSize: CGSize) -> CGSize { + var minimumSize = minimumSize + let mW = minimumSize.width / aspectRatio.width + let mH = minimumSize.height / aspectRatio.height + + if mH > mW { + minimumSize.width = minimumSize.height / aspectRatio.height * aspectRatio.width + } else if mW > mH { + minimumSize.height = minimumSize.width / aspectRatio.width * aspectRatio.height + } + + return minimumSize + } } diff --git a/Swiftfin.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Swiftfin.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8e52e799..74055c43 100644 --- a/Swiftfin.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Swiftfin.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/CombineCommunity/CombineExt", "state": { "branch": null, - "revision": "8ca006df5e3cc6bb176b70238e2b0014bbc3a235", - "version": "1.0.0" + "revision": "0880829102152185190064fd17847a7c681d2127", + "version": "1.5.1" } }, { @@ -42,8 +42,8 @@ "repositoryURL": "https://github.com/sindresorhus/Defaults", "state": { "branch": null, - "revision": "8a6e4a96fd38504a05903d136c85634b65fd7c4d", - "version": "6.0.0" + "revision": "55f3302c3ab30a8760f10042d0ebc0a6907f865a", + "version": "6.1.0" } }, { @@ -96,8 +96,8 @@ "repositoryURL": "https://github.com/sushichop/Puppy", "state": { "branch": null, - "revision": "dc82e65c749cee431ffbb8c0913680b61ccd7e08", - "version": "0.2.0" + "revision": "95ce04b0e778b8d7c351876bc98bbf68328dfc9b", + "version": "0.3.1" } }, { @@ -105,8 +105,8 @@ "repositoryURL": "https://github.com/rundfunk47/stinsen", "state": { "branch": null, - "revision": "5e6c714f6f308877c8a988523915f9eb592d7d82", - "version": "2.0.3" + "revision": "36d97964075dc770046ddef9346a29bfa8982d6d", + "version": "2.0.7" } }, { diff --git a/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift b/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift index ce9979f0..08133616 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift @@ -52,14 +52,14 @@ struct VLCPlayerOverlayView: View { // MARK: Top Bar - ZStack { + ZStack(alignment: .center) { if viewModel.overlayType == .compact { - LinearGradient(gradient: Gradient(colors: [.black.opacity(0.7), .clear]), + LinearGradient(gradient: Gradient(colors: [.black.opacity(0.8), .clear]), startPoint: .top, endPoint: .bottom) .ignoresSafeArea() - .frame(height: 80) + .frame(height: 70) } VStack(alignment: .EpisodeSeriesAlignmentGuide) { @@ -78,6 +78,7 @@ struct VLCPlayerOverlayView: View { Text(viewModel.title) .font(.title3) .fontWeight(.bold) + .lineLimit(1) .alignmentGuide(.EpisodeSeriesAlignmentGuide) { context in context[.leading] } @@ -87,6 +88,8 @@ struct VLCPlayerOverlayView: View { HStack(spacing: 20) { + // MARK: Previous Item + if viewModel.shouldShowPlayPreviousItem { Button { viewModel.playerOverlayDelegate?.didSelectPlayPreviousItem() @@ -97,6 +100,8 @@ struct VLCPlayerOverlayView: View { .foregroundColor(viewModel.nextItemVideoPlayerViewModel == nil ? .gray : .white) } + // MARK: Next Item + if viewModel.shouldShowPlayNextItem { Button { viewModel.playerOverlayDelegate?.didSelectPlayNextItem() @@ -107,6 +112,8 @@ struct VLCPlayerOverlayView: View { .foregroundColor(viewModel.nextItemVideoPlayerViewModel == nil ? .gray : .white) } + // MARK: Autoplay + if viewModel.shouldShowAutoPlay { Button { viewModel.autoplayEnabled.toggle() @@ -119,6 +126,8 @@ struct VLCPlayerOverlayView: View { } } + // MARK: Subtitle Toggle + if !viewModel.subtitleStreams.isEmpty { Button { viewModel.subtitlesEnabled.toggle() @@ -133,10 +142,32 @@ struct VLCPlayerOverlayView: View { .foregroundColor(viewModel.selectedSubtitleStreamIndex == -1 ? .gray : .white) } + // MARK: Screen Fill + + Button { + viewModel.playerOverlayDelegate?.didSelectScreenFill() + } label: { + if viewModel.playerOverlayDelegate?.getScreenFilled() ?? true { + if viewModel.playerOverlayDelegate?.isVideoAspectRatioGreater() ?? true { + Image(systemName: "rectangle.arrowtriangle.2.inward") + } else { + Image(systemName: "rectangle.portrait.arrowtriangle.2.inward") + } + } else { + if viewModel.playerOverlayDelegate?.isVideoAspectRatioGreater() ?? true { + Image(systemName: "rectangle.arrowtriangle.2.outward") + } else { + Image(systemName: "rectangle.portrait.arrowtriangle.2.outward") + } + } + } + // MARK: Settings Menu Menu { + // MARK: Audio Streams + Menu { ForEach(viewModel.audioStreams, id: \.self) { audioStream in Button { @@ -156,6 +187,8 @@ struct VLCPlayerOverlayView: View { } } + // MARK: Subtitle Streams + Menu { ForEach(viewModel.subtitleStreams, id: \.self) { subtitleStream in Button { @@ -175,6 +208,8 @@ struct VLCPlayerOverlayView: View { } } + // MARK: Playback Speed + Menu { ForEach(PlaybackSpeed.allCases, id: \.self) { speed in Button { @@ -194,6 +229,8 @@ struct VLCPlayerOverlayView: View { } } + // MARK: Chapters + if !viewModel.chapters.isEmpty { Button { viewModel.playerOverlayDelegate?.didSelectChapters() @@ -205,6 +242,8 @@ struct VLCPlayerOverlayView: View { } } + // MARK: Jump Button Lengths + if viewModel.shouldShowJumpButtonsInOverlayMenu { Menu { ForEach(VideoPlayerJumpLength.allCases, id: \.self) { forwardLength in @@ -259,12 +298,11 @@ struct VLCPlayerOverlayView: View { .alignmentGuide(.EpisodeSeriesAlignmentGuide) { context in context[.leading] } - .offset(y: -20) + .offset(y: -18) } } + .padding(.horizontal, UIDevice.current.userInterfaceIdiom == .pad ? 30 : 0) } - .padding(.horizontal, UIDevice.current.userInterfaceIdiom == .pad ? 50 : 0) - .padding(.top, UIDevice.current.userInterfaceIdiom == .pad ? 10 : 0) // MARK: Center @@ -298,10 +336,10 @@ struct VLCPlayerOverlayView: View { // MARK: Bottom Bar - ZStack { + ZStack(alignment: .center) { if viewModel.overlayType == .compact { - LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.7)]), + LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.8)]), startPoint: .top, endPoint: .bottom) .ignoresSafeArea() @@ -363,12 +401,10 @@ struct VLCPlayerOverlayView: View { .accessibilityLabel(L10n.remainingTime) .accessibilityValue(viewModel.rightLabelText) } - .padding(.horizontal) - .frame(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 800 : nil) + .padding(.horizontal, UIDevice.current.userInterfaceIdiom == .pad ? 30 : 0) + .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 10 : 0) } - .frame(maxHeight: 50) } - .ignoresSafeArea(edges: .top) .tint(Color.white) .foregroundColor(Color.white) } diff --git a/Swiftfin/Views/VideoPlayer/PlayerOverlayDelegate.swift b/Swiftfin/Views/VideoPlayer/PlayerOverlayDelegate.swift index f0e49c37..4977b3a4 100644 --- a/Swiftfin/Views/VideoPlayer/PlayerOverlayDelegate.swift +++ b/Swiftfin/Views/VideoPlayer/PlayerOverlayDelegate.swift @@ -32,4 +32,10 @@ protocol PlayerOverlayDelegate { func didSelectChapters() func didSelectChapter(_ chapter: ChapterInfo) + + func didSelectScreenFill() + func getScreenFilled() -> Bool + // Returns whether the aspect ratio of the video + // is greater than the aspect ratio of the screen + func isVideoAspectRatioGreater() -> Bool } diff --git a/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift b/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift index a22a5212..e46f8f78 100644 --- a/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift +++ b/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift @@ -28,6 +28,8 @@ class VLCPlayerViewController: UIViewController { private var lastProgressReportTicks: Int64 = 0 private var viewModelListeners = Set() private var overlayDismissTimer: Timer? + private var isScreenFilled: Bool = false + private var pinchScale: CGFloat = 1 private var currentPlayerTicks: Int64 { Int64(vlcMediaPlayer.time.intValue) * 100_000 @@ -42,7 +44,7 @@ class VLCPlayerViewController: UIViewController { } private lazy var videoContentView = makeVideoContentView() - private lazy var mainGestureView = makeTapGestureView() + private lazy var mainGestureView = makeMainGestureView() private var currentOverlayHostingController: UIHostingController? private var currentChapterOverlayHostingController: UIHostingController? private var currentJumpBackwardOverlayView: UIImageView? @@ -142,7 +144,14 @@ class VLCPlayerViewController: UIViewController { startPlayback() } - // MARK: subviews + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + if isScreenFilled { + fillScreen(screenSize: size) + } + super.viewWillTransition(to: size, with: coordinator) + } + + // MARK: VideoContentView private func makeVideoContentView() -> UIView { let view = UIView() @@ -152,7 +161,9 @@ class VLCPlayerViewController: UIViewController { return view } - private func makeTapGestureView() -> UIView { + // MARK: MainGestureView + + private func makeMainGestureView() -> UIView { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false @@ -164,7 +175,10 @@ class VLCPlayerViewController: UIViewController { let leftSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(didLeftSwipe)) leftSwipeGesture.direction = .left + let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(didPinch(_:))) + view.addGestureRecognizer(singleTapGesture) + view.addGestureRecognizer(pinchGesture) if viewModel.jumpGesturesEnabled { view.addGestureRecognizer(rightSwipeGesture) @@ -189,6 +203,21 @@ class VLCPlayerViewController: UIViewController { self.didSelectBackward() } + @objc + private func didPinch(_ gestureRecognizer: UIPinchGestureRecognizer) { + if gestureRecognizer.state == .began || gestureRecognizer.state == .changed { + pinchScale = gestureRecognizer.scale + } else { + if pinchScale > 1 && !isScreenFilled { + isScreenFilled.toggle() + fillScreen() + } else if pinchScale < 1 && isScreenFilled { + isScreenFilled.toggle() + shrinkScreen() + } + } + } + // MARK: setupOverlayHostingController private func setupOverlayHostingController(viewModel: VideoPlayerViewModel) { @@ -814,4 +843,52 @@ extension VLCPlayerViewController: PlayerOverlayDelegate { viewModel.sendProgressReport() } + + func didSelectScreenFill() { + + isScreenFilled.toggle() + + if isScreenFilled { + fillScreen() + } else { + shrinkScreen() + } + } + + private func fillScreen(screenSize: CGSize = UIScreen.main.bounds.size) { + let videoSize = vlcMediaPlayer.videoSize + let fillSize = CGSize.aspectFill(aspectRatio: videoSize, minimumSize: screenSize) + + let scale: CGFloat + + if fillSize.height > screenSize.height { + scale = fillSize.height / screenSize.height + } else { + scale = fillSize.width / screenSize.width + } + + UIView.animate(withDuration: 0.2) { + self.videoContentView.transform = CGAffineTransform(scaleX: scale, y: scale) + } + } + + private func shrinkScreen() { + UIView.animate(withDuration: 0.2) { + self.videoContentView.transform = .identity + } + } + + func getScreenFilled() -> Bool { + isScreenFilled + } + + func isVideoAspectRatioGreater() -> Bool { + let screenSize = UIScreen.main.bounds.size + let videoSize = vlcMediaPlayer.videoSize + + let screenAspectRatio = screenSize.width / screenSize.height + let videoAspectRatio = videoSize.width / videoSize.height + + return videoAspectRatio > screenAspectRatio + } }