commit
e8c1963e2a
12
README.md
12
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
|
### 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
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue