Merge pull request #315 from LePips/aspect-fill

Add Aspect Fill
This commit is contained in:
aiden 3 2022-01-20 10:32:16 -05:00 committed by GitHub
commit e8c1963e2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 160 additions and 32 deletions

View File

@ -38,13 +38,7 @@ Thank you for your interest in Swiftfin, please check out the [Contribution Guid
### Intended Behaviors Due to Technical Limitations ### 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 - Pausing playback when app is backgrounded as VLCKit pauses video output at the same time
- Due to VLCKit pausing video output at the same moment - Audio delay when starting playback and un-pausing, may be fixed in VLCKit v4
- 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

View File

@ -13,4 +13,19 @@ extension CGSize {
static func Circle(radius: CGFloat) -> CGSize { static func Circle(radius: CGFloat) -> CGSize {
CGSize(width: radius, height: radius) 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
}
} }

View File

@ -24,8 +24,8 @@
"repositoryURL": "https://github.com/CombineCommunity/CombineExt", "repositoryURL": "https://github.com/CombineCommunity/CombineExt",
"state": { "state": {
"branch": null, "branch": null,
"revision": "8ca006df5e3cc6bb176b70238e2b0014bbc3a235", "revision": "0880829102152185190064fd17847a7c681d2127",
"version": "1.0.0" "version": "1.5.1"
} }
}, },
{ {
@ -42,8 +42,8 @@
"repositoryURL": "https://github.com/sindresorhus/Defaults", "repositoryURL": "https://github.com/sindresorhus/Defaults",
"state": { "state": {
"branch": null, "branch": null,
"revision": "8a6e4a96fd38504a05903d136c85634b65fd7c4d", "revision": "55f3302c3ab30a8760f10042d0ebc0a6907f865a",
"version": "6.0.0" "version": "6.1.0"
} }
}, },
{ {
@ -96,8 +96,8 @@
"repositoryURL": "https://github.com/sushichop/Puppy", "repositoryURL": "https://github.com/sushichop/Puppy",
"state": { "state": {
"branch": null, "branch": null,
"revision": "dc82e65c749cee431ffbb8c0913680b61ccd7e08", "revision": "95ce04b0e778b8d7c351876bc98bbf68328dfc9b",
"version": "0.2.0" "version": "0.3.1"
} }
}, },
{ {
@ -105,8 +105,8 @@
"repositoryURL": "https://github.com/rundfunk47/stinsen", "repositoryURL": "https://github.com/rundfunk47/stinsen",
"state": { "state": {
"branch": null, "branch": null,
"revision": "5e6c714f6f308877c8a988523915f9eb592d7d82", "revision": "36d97964075dc770046ddef9346a29bfa8982d6d",
"version": "2.0.3" "version": "2.0.7"
} }
}, },
{ {

View File

@ -52,14 +52,14 @@ struct VLCPlayerOverlayView: View {
// MARK: Top Bar // MARK: Top Bar
ZStack { ZStack(alignment: .center) {
if viewModel.overlayType == .compact { if viewModel.overlayType == .compact {
LinearGradient(gradient: Gradient(colors: [.black.opacity(0.7), .clear]), LinearGradient(gradient: Gradient(colors: [.black.opacity(0.8), .clear]),
startPoint: .top, startPoint: .top,
endPoint: .bottom) endPoint: .bottom)
.ignoresSafeArea() .ignoresSafeArea()
.frame(height: 80) .frame(height: 70)
} }
VStack(alignment: .EpisodeSeriesAlignmentGuide) { VStack(alignment: .EpisodeSeriesAlignmentGuide) {
@ -78,6 +78,7 @@ struct VLCPlayerOverlayView: View {
Text(viewModel.title) Text(viewModel.title)
.font(.title3) .font(.title3)
.fontWeight(.bold) .fontWeight(.bold)
.lineLimit(1)
.alignmentGuide(.EpisodeSeriesAlignmentGuide) { context in .alignmentGuide(.EpisodeSeriesAlignmentGuide) { context in
context[.leading] context[.leading]
} }
@ -87,6 +88,8 @@ struct VLCPlayerOverlayView: View {
HStack(spacing: 20) { HStack(spacing: 20) {
// MARK: Previous Item
if viewModel.shouldShowPlayPreviousItem { if viewModel.shouldShowPlayPreviousItem {
Button { Button {
viewModel.playerOverlayDelegate?.didSelectPlayPreviousItem() viewModel.playerOverlayDelegate?.didSelectPlayPreviousItem()
@ -97,6 +100,8 @@ struct VLCPlayerOverlayView: View {
.foregroundColor(viewModel.nextItemVideoPlayerViewModel == nil ? .gray : .white) .foregroundColor(viewModel.nextItemVideoPlayerViewModel == nil ? .gray : .white)
} }
// MARK: Next Item
if viewModel.shouldShowPlayNextItem { if viewModel.shouldShowPlayNextItem {
Button { Button {
viewModel.playerOverlayDelegate?.didSelectPlayNextItem() viewModel.playerOverlayDelegate?.didSelectPlayNextItem()
@ -107,6 +112,8 @@ struct VLCPlayerOverlayView: View {
.foregroundColor(viewModel.nextItemVideoPlayerViewModel == nil ? .gray : .white) .foregroundColor(viewModel.nextItemVideoPlayerViewModel == nil ? .gray : .white)
} }
// MARK: Autoplay
if viewModel.shouldShowAutoPlay { if viewModel.shouldShowAutoPlay {
Button { Button {
viewModel.autoplayEnabled.toggle() viewModel.autoplayEnabled.toggle()
@ -119,6 +126,8 @@ struct VLCPlayerOverlayView: View {
} }
} }
// MARK: Subtitle Toggle
if !viewModel.subtitleStreams.isEmpty { if !viewModel.subtitleStreams.isEmpty {
Button { Button {
viewModel.subtitlesEnabled.toggle() viewModel.subtitlesEnabled.toggle()
@ -133,10 +142,32 @@ struct VLCPlayerOverlayView: View {
.foregroundColor(viewModel.selectedSubtitleStreamIndex == -1 ? .gray : .white) .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 // MARK: Settings Menu
Menu { Menu {
// MARK: Audio Streams
Menu { Menu {
ForEach(viewModel.audioStreams, id: \.self) { audioStream in ForEach(viewModel.audioStreams, id: \.self) { audioStream in
Button { Button {
@ -156,6 +187,8 @@ struct VLCPlayerOverlayView: View {
} }
} }
// MARK: Subtitle Streams
Menu { Menu {
ForEach(viewModel.subtitleStreams, id: \.self) { subtitleStream in ForEach(viewModel.subtitleStreams, id: \.self) { subtitleStream in
Button { Button {
@ -175,6 +208,8 @@ struct VLCPlayerOverlayView: View {
} }
} }
// MARK: Playback Speed
Menu { Menu {
ForEach(PlaybackSpeed.allCases, id: \.self) { speed in ForEach(PlaybackSpeed.allCases, id: \.self) { speed in
Button { Button {
@ -194,6 +229,8 @@ struct VLCPlayerOverlayView: View {
} }
} }
// MARK: Chapters
if !viewModel.chapters.isEmpty { if !viewModel.chapters.isEmpty {
Button { Button {
viewModel.playerOverlayDelegate?.didSelectChapters() viewModel.playerOverlayDelegate?.didSelectChapters()
@ -205,6 +242,8 @@ struct VLCPlayerOverlayView: View {
} }
} }
// MARK: Jump Button Lengths
if viewModel.shouldShowJumpButtonsInOverlayMenu { if viewModel.shouldShowJumpButtonsInOverlayMenu {
Menu { Menu {
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { forwardLength in ForEach(VideoPlayerJumpLength.allCases, id: \.self) { forwardLength in
@ -259,12 +298,11 @@ struct VLCPlayerOverlayView: View {
.alignmentGuide(.EpisodeSeriesAlignmentGuide) { context in .alignmentGuide(.EpisodeSeriesAlignmentGuide) { context in
context[.leading] 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 // MARK: Center
@ -298,10 +336,10 @@ struct VLCPlayerOverlayView: View {
// MARK: Bottom Bar // MARK: Bottom Bar
ZStack { ZStack(alignment: .center) {
if viewModel.overlayType == .compact { if viewModel.overlayType == .compact {
LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.7)]), LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.8)]),
startPoint: .top, startPoint: .top,
endPoint: .bottom) endPoint: .bottom)
.ignoresSafeArea() .ignoresSafeArea()
@ -363,12 +401,10 @@ struct VLCPlayerOverlayView: View {
.accessibilityLabel(L10n.remainingTime) .accessibilityLabel(L10n.remainingTime)
.accessibilityValue(viewModel.rightLabelText) .accessibilityValue(viewModel.rightLabelText)
} }
.padding(.horizontal) .padding(.horizontal, UIDevice.current.userInterfaceIdiom == .pad ? 30 : 0)
.frame(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 800 : nil) .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 10 : 0)
} }
.frame(maxHeight: 50)
} }
.ignoresSafeArea(edges: .top)
.tint(Color.white) .tint(Color.white)
.foregroundColor(Color.white) .foregroundColor(Color.white)
} }

View File

@ -32,4 +32,10 @@ protocol PlayerOverlayDelegate {
func didSelectChapters() func didSelectChapters()
func didSelectChapter(_ chapter: ChapterInfo) 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
} }

View File

@ -28,6 +28,8 @@ class VLCPlayerViewController: UIViewController {
private var lastProgressReportTicks: Int64 = 0 private var lastProgressReportTicks: Int64 = 0
private var viewModelListeners = Set<AnyCancellable>() private var viewModelListeners = Set<AnyCancellable>()
private var overlayDismissTimer: Timer? private var overlayDismissTimer: Timer?
private var isScreenFilled: Bool = false
private var pinchScale: CGFloat = 1
private var currentPlayerTicks: Int64 { private var currentPlayerTicks: Int64 {
Int64(vlcMediaPlayer.time.intValue) * 100_000 Int64(vlcMediaPlayer.time.intValue) * 100_000
@ -42,7 +44,7 @@ class VLCPlayerViewController: UIViewController {
} }
private lazy var videoContentView = makeVideoContentView() private lazy var videoContentView = makeVideoContentView()
private lazy var mainGestureView = makeTapGestureView() private lazy var mainGestureView = makeMainGestureView()
private var currentOverlayHostingController: UIHostingController<VLCPlayerOverlayView>? private var currentOverlayHostingController: UIHostingController<VLCPlayerOverlayView>?
private var currentChapterOverlayHostingController: UIHostingController<VLCPlayerChapterOverlayView>? private var currentChapterOverlayHostingController: UIHostingController<VLCPlayerChapterOverlayView>?
private var currentJumpBackwardOverlayView: UIImageView? private var currentJumpBackwardOverlayView: UIImageView?
@ -142,7 +144,14 @@ class VLCPlayerViewController: UIViewController {
startPlayback() 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 { private func makeVideoContentView() -> UIView {
let view = UIView() let view = UIView()
@ -152,7 +161,9 @@ class VLCPlayerViewController: UIViewController {
return view return view
} }
private func makeTapGestureView() -> UIView { // MARK: MainGestureView
private func makeMainGestureView() -> UIView {
let view = UIView() let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false view.translatesAutoresizingMaskIntoConstraints = false
@ -164,7 +175,10 @@ class VLCPlayerViewController: UIViewController {
let leftSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(didLeftSwipe)) let leftSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(didLeftSwipe))
leftSwipeGesture.direction = .left leftSwipeGesture.direction = .left
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(didPinch(_:)))
view.addGestureRecognizer(singleTapGesture) view.addGestureRecognizer(singleTapGesture)
view.addGestureRecognizer(pinchGesture)
if viewModel.jumpGesturesEnabled { if viewModel.jumpGesturesEnabled {
view.addGestureRecognizer(rightSwipeGesture) view.addGestureRecognizer(rightSwipeGesture)
@ -189,6 +203,21 @@ class VLCPlayerViewController: UIViewController {
self.didSelectBackward() 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 // MARK: setupOverlayHostingController
private func setupOverlayHostingController(viewModel: VideoPlayerViewModel) { private func setupOverlayHostingController(viewModel: VideoPlayerViewModel) {
@ -814,4 +843,52 @@ extension VLCPlayerViewController: PlayerOverlayDelegate {
viewModel.sendProgressReport() 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
}
} }