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
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

View File

@ -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
}
}

View File

@ -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"
}
},
{

View File

@ -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)
}

View File

@ -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
}

View File

@ -28,6 +28,8 @@ class VLCPlayerViewController: UIViewController {
private var lastProgressReportTicks: Int64 = 0
private var viewModelListeners = Set<AnyCancellable>()
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<VLCPlayerOverlayView>?
private var currentChapterOverlayHostingController: UIHostingController<VLCPlayerChapterOverlayView>?
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
}
}