Merge pull request #88 from stephenb10/nextUp

iOS player features
This commit is contained in:
aiden vigue 2021-06-27 11:12:34 -04:00 committed by GitHub
commit c5b2a3ce0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 278 additions and 14 deletions

View File

@ -14,6 +14,7 @@
09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */; };
09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09389CC626819B4500AE350E /* VideoPlayerModel.swift */; };
09389CC826819B4600AE350E /* VideoPlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09389CC626819B4500AE350E /* VideoPlayerModel.swift */; };
0959A5FD2686D29800C7C9A9 /* VideoUpNextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0959A5FC2686D29800C7C9A9 /* VideoUpNextView.swift */; };
531069572684E7EE00CFFDBA /* InfoTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531069502684E7EE00CFFDBA /* InfoTabBarViewController.swift */; };
531069582684E7EE00CFFDBA /* MediaInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531069512684E7EE00CFFDBA /* MediaInfoView.swift */; };
531069592684E7EE00CFFDBA /* SubtitlesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531069522684E7EE00CFFDBA /* SubtitlesView.swift */; };
@ -204,6 +205,7 @@
091B5A872683142E00D78B61 /* ServerDiscovery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerDiscovery.swift; sourceTree = "<group>"; };
091B5A882683142E00D78B61 /* UDPBroadCastConnection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UDPBroadCastConnection.swift; sourceTree = "<group>"; };
09389CC626819B4500AE350E /* VideoPlayerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerModel.swift; sourceTree = "<group>"; };
0959A5FC2686D29800C7C9A9 /* VideoUpNextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoUpNextView.swift; sourceTree = "<group>"; };
3773C07648173CE7FEC083D5 /* Pods-JellyfinPlayer iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer iOS.debug.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS.debug.xcconfig"; sourceTree = "<group>"; };
3F905C1D3D3A0C9E13E7A0BC /* Pods_JellyfinPlayer_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_JellyfinPlayer_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
531069502684E7EE00CFFDBA /* InfoTabBarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfoTabBarViewController.swift; sourceTree = "<group>"; };
@ -392,11 +394,11 @@
5310694F2684E7EE00CFFDBA /* VideoPlayer */ = {
isa = PBXGroup;
children = (
531069502684E7EE00CFFDBA /* InfoTabBarViewController.swift */,
531069512684E7EE00CFFDBA /* MediaInfoView.swift */,
531069522684E7EE00CFFDBA /* SubtitlesView.swift */,
531069532684E7EE00CFFDBA /* VideoPlayer.swift */,
531069542684E7EE00CFFDBA /* AudioView.swift */,
531069502684E7EE00CFFDBA /* InfoTabBarViewController.swift */,
531069532684E7EE00CFFDBA /* VideoPlayer.swift */,
531069552684E7EE00CFFDBA /* VideoPlayerViewController.swift */,
531069562684E7EE00CFFDBA /* VideoPlayerStoryboard.storyboard */,
);
@ -539,6 +541,7 @@
53987CA526572F0700E7EA70 /* SeriesItemView.swift */,
539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */,
535BAEA4264A151C005FA86D /* VideoPlayer.swift */,
0959A5FC2686D29800C7C9A9 /* VideoUpNextView.swift */,
53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */,
532E68CE267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift */,
53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */,
@ -1014,6 +1017,7 @@
53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */,
53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */,
62133890265F83A900A81A2A /* LibraryListView.swift in Sources */,
0959A5FD2686D29800C7C9A9 /* VideoUpNextView.swift in Sources */,
62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */,
625CB56F2678C23300530A6E /* HomeView.swift in Sources */,
53892770263C25230035E14B /* NextUpView.swift in Sources */,

View File

@ -1,9 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19115.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_5" orientation="landscape" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19107.4"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<capability name="Image references" minToolsVersion="12.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
@ -170,6 +169,11 @@
<outletCollection property="gestureRecognizers" destination="iQW-fW-KWT" appends="YES" id="H09-88-nzQ"/>
</connections>
</view>
<view hidden="YES" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="CY9-gw-dv8" userLabel="UpNextView">
<rect key="frame" x="675" y="254" width="224" height="161"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" red="0.34509803921568627" green="0.33725490196078434" blue="0.83921568627450982" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
</view>
</subviews>
<viewLayoutGuide key="safeArea" id="zud-b9-RyD"/>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@ -194,6 +198,7 @@
<outlet property="seekSlider" destination="e9f-8l-RdN" id="b3H-tn-TPG"/>
<outlet property="timeText" destination="qft-iu-f1z" id="pAX-J3-I53"/>
<outlet property="titleLabel" destination="o8N-R1-DhT" id="E7D-iU-bMi"/>
<outlet property="upNextView" destination="CY9-gw-dv8" id="BP6-bc-6Vk"/>
<outlet property="videoContentView" destination="Tsh-rC-BwO" id="5uR-No-wLy"/>
<outlet property="videoControlsView" destination="Qcb-Fb-qZl" id="Z1U-Qr-8ND"/>
</connections>

View File

@ -32,6 +32,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
var cancellables = Set<AnyCancellable>()
var mediaPlayer = VLCMediaPlayer()
@IBOutlet weak var upNextView: UIView!
@IBOutlet weak var timeText: UILabel!
@IBOutlet weak var videoContentView: UIView!
@IBOutlet weak var videoControlsView: UIView!
@ -67,17 +68,23 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
}
var hasSentRemoteSeek: Bool = false
var selectedPlaybackSpeedIndex : Int = 3
var selectedAudioTrack: Int32 = -1
var selectedCaptionTrack: Int32 = -1
var playSessionId: String = ""
var lastProgressReportTime: Double = 0
var subtitleTrackArray: [Subtitle] = []
var audioTrackArray: [AudioTrack] = []
let playbackSpeeds : [Float] = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0]
var manifest: BaseItemDto = BaseItemDto()
var playbackItem = PlaybackItem()
var remoteTimeUpdateTimer: Timer?
var smallView : CGRect = .zero
var largeView : CGRect = .zero
var upNextViewModel: UpNextViewModel = UpNextViewModel()
// MARK: IBActions
@IBAction func seekSliderStart(_ sender: Any) {
if playerDestination == .local {
@ -140,6 +147,9 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
@IBAction func controlViewTapped(_ sender: Any) {
if playerDestination == .local {
videoControlsView.isHidden = true
if manifest.type == "Episode" {
smallNextUpView()
}
}
}
@ -147,6 +157,9 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
if playerDestination == .local {
videoControlsView.isHidden = false
controlsAppearTime = CACurrentMediaTime()
if manifest.type == "Episode" {
largeNextUpView()
}
}
}
@ -340,9 +353,34 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
}
}
var nowPlayingInfo = [String: Any]()
nowPlayingInfo[MPMediaItemPropertyTitle] = manifest.name ?? ""
var nowPlayingInfo = [String : Any]()
var runTicks = 0
var playbackTicks = 0
if let ticks = manifest.runTimeTicks {
runTicks = Int(ticks / 10_000_000)
}
if let ticks = manifest.userData?.playbackPositionTicks {
playbackTicks = Int(ticks / 10_000_000)
}
nowPlayingInfo[MPMediaItemPropertyTitle] = manifest.name ?? "Jellyfin Video"
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1.0
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = AVMediaType.video
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = runTicks
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = playbackTicks
if let imageData = NSData(contentsOf: manifest.getPrimaryImage(maxWidth: 200)) {
if let artworkImage = UIImage(data: imageData as Data) {
let artwork = MPMediaItemArtwork.init(boundsSize: artworkImage.size, requestHandler: { (size) -> UIImage in
return artworkImage
})
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
}
}
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
UIApplication.shared.beginReceivingRemoteControlEvents()
@ -354,6 +392,9 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
titleLabel.text = manifest.name ?? ""
} else {
titleLabel.text = "S\(String(manifest.parentIndexNumber ?? 0)):E\(String(manifest.indexNumber ?? 0))\(manifest.name ?? "")"
setupNextUpView()
upNextViewModel.delegate = self
}
if !UIDevice.current.orientation.isLandscape || UIDevice.current.orientation.isFlat {
@ -414,7 +455,13 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
mediaPlayer.delegate = self
mediaPlayer.drawable = videoContentView
setupMediaPlayer()
}
func setupMediaPlayer() {
// Fetch max bitrate from UserDefaults depending on current connection mode
let maxBitrate = Defaults[.inNetworkBandwidth]
@ -534,6 +581,9 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
self.sendPlayReport()
playbackItem = item
self.setupNowPlayingCC()
}
startLocalPlaybackEngine(true)
@ -630,6 +680,97 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
selectedAudioTrack = newTrackID
mediaPlayer.currentAudioTrackIndex = newTrackID
}
func playbackSpeedChanged(index: Int) {
selectedPlaybackSpeedIndex = index
mediaPlayer.rate = playbackSpeeds[index]
}
func smallNextUpView() {
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseIn) { [self] in
upNextViewModel.largeView = false
upNextView.frame = smallView
}
}
func largeNextUpView() {
UIView.animate(withDuration: 0.1, delay: 0, options: .curveEaseOut) { [self] in
upNextViewModel.largeView = true
upNextView.frame = largeView
}
}
func setupNextUpView() {
getNextEpisode()
// Create the swiftUI view
let contentView = UIHostingController(rootView: VideoUpNextView(viewModel: upNextViewModel))
self.upNextView.addSubview(contentView.view)
contentView.view.backgroundColor = .clear
contentView.view.translatesAutoresizingMaskIntoConstraints = false
contentView.view.topAnchor.constraint(equalTo: upNextView.topAnchor).isActive = true
contentView.view.bottomAnchor.constraint(equalTo: upNextView.bottomAnchor).isActive = true
contentView.view.leftAnchor.constraint(equalTo: upNextView.leftAnchor).isActive = true
contentView.view.rightAnchor.constraint(equalTo: upNextView.rightAnchor).isActive = true
// Frame sizes depend on if controls are hidden or shown
smallView = upNextView.frame
largeView = CGRect(x: 460, y: 90, width: 400, height: 270)
}
func getNextEpisode() {
TvShowsAPI.getEpisodes(seriesId: manifest.seriesId!, userId: SessionManager.current.user.user_id!, startItemId: manifest.id, limit: 2)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { [self] response in
// Returns 2 items, the first is the current episode
// The second is the next episode
if let item = response.items?.last {
self.upNextViewModel.item = item
}
})
.store(in: &cancellables)
}
func setPlayerToNextUp() {
mediaPlayer.stop()
ssTargetValueOffset = 0
ssStartValue = 0
paused = true
lastTime = 0.0
startTime = 0
controlsAppearTime = 0
isSeeking = false
remotePositionTicks = 0
selectedPlaybackSpeedIndex = 3
selectedAudioTrack = -1
selectedCaptionTrack = -1
playSessionId = ""
lastProgressReportTime = 0
subtitleTrackArray = []
audioTrackArray = []
manifest = upNextViewModel.item!
playbackItem = PlaybackItem()
upNextViewModel.item = nil
upNextView.isHidden = true
shouldShowLoadingScreen = true
videoControlsView.isHidden = true
titleLabel.text = "S\(String(manifest.parentIndexNumber ?? 0)):E\(String(manifest.indexNumber ?? 0))\(manifest.name ?? "")"
setupMediaPlayer()
getNextEpisode()
}
}
// MARK: - GCKGenericChannelDelegate
@ -794,7 +935,6 @@ extension PlayerViewController: VLCMediaPlayerDelegate {
break
case .playing :
print("Video is playing")
self.setupNowPlayingCC()
sendProgressReport(eventName: "unpause")
delegate?.hideLoadingView(self)
paused = false
@ -826,10 +966,21 @@ extension PlayerViewController: VLCMediaPlayerDelegate {
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
seekSlider.setValue(mediaPlayer.position, animated: true)
delegate?.hideLoadingView(self)
if manifest.type == "Episode" && upNextViewModel.item != nil{
if time > 0.96 {
upNextView.isHidden = false
self.jumpForwardButton.isHidden = true
} else {
upNextView.isHidden = true
self.jumpForwardButton.isHidden = false
}
}
timeText.text = String(mediaPlayer.remainingTime.stringValue.dropFirst())
if CACurrentMediaTime() - controlsAppearTime > 5 {
self.smallNextUpView()
UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseOut, animations: {
self.videoControlsView.alpha = 0.0
}, completion: { (_: Bool) in

View File

@ -37,7 +37,8 @@ struct VideoPlayerSettings: View {
weak var delegate: PlayerViewController!
@State var captionTrack: Int32 = -99
@State var audioTrack: Int32 = -99
@State var playbackSpeedSelection : Int = 3
init(delegate: PlayerViewController) {
self.delegate = delegate
}
@ -60,6 +61,20 @@ struct VideoPlayerSettings: View {
}.onChange(of: audioTrack) { track in
self.delegate.audioTrackChanged(newTrackID: track)
}
Picker("Playback Speed", selection: $playbackSpeedSelection) {
ForEach(delegate.playbackSpeeds.indices, id: \.self) { speedIndex in
let speed = delegate.playbackSpeeds[speedIndex]
if floor(speed) == speed {
Text(String(format: "%.0fx", speed)).tag(speedIndex)
}
else {
Text(String(format: "%.2fx", speed)).tag(speedIndex)
}
}
}
.onChange(of: playbackSpeedSelection, perform: { index in
self.delegate.playbackSpeedChanged(index: index)
})
}.navigationBarTitleDisplayMode(.inline)
.navigationTitle("Audio & Captions")
.toolbar {
@ -78,8 +93,9 @@ struct VideoPlayerSettings: View {
}
}.offset(y: UIDevice.current.userInterfaceIdiom == .pad ? 14 : 0)
.onAppear(perform: {
_captionTrack.wrappedValue = self.delegate.selectedCaptionTrack
_audioTrack.wrappedValue = self.delegate.selectedAudioTrack
captionTrack = self.delegate.selectedCaptionTrack
audioTrack = self.delegate.selectedAudioTrack
playbackSpeedSelection = self.delegate.selectedPlaybackSpeedIndex
})
}
}

View File

@ -0,0 +1,88 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import SwiftUI
import JellyfinAPI
class UpNextViewModel: ObservableObject {
@Published var largeView: Bool = false
@Published var item: BaseItemDto? = nil
var delegate: PlayerViewController?
func episodeAndSeasonNumber() -> String {
if let pID = item?.parentIndexNumber, let id = item?.indexNumber {
return "S\(pID):E\(id)"
}
return ""
}
func episodeName() -> String {
if let name = item?.name {
return name
}
return ""
}
func nextUp() {
if delegate != nil {
delegate?.setPlayerToNextUp()
}
}
}
struct VideoUpNextView: View {
@ObservedObject var viewModel: UpNextViewModel
var body: some View {
VStack(alignment: viewModel.largeView ? .leading : .center) {
Text("Up Next")
.foregroundColor(.white)
.font(viewModel.largeView ? .title : .body)
Button(action: viewModel.nextUp, label: {image})
if viewModel.largeView {
Text(viewModel.episodeName())
.padding(.trailing, 50)
.foregroundColor(.white)
.minimumScaleFactor(0.1)
}
}
.shadow(color: .black, radius: 20)
}
var image : some View {
if let url = viewModel.item?.getPrimaryImage(maxWidth: 100) {
return AnyView(
ImageView(src: url)
.frame(maxWidth: .infinity)
.aspectRatio(CGSize(width: 16, height: 9), contentMode: .fit)
.overlay(overlayIndicator, alignment: .topTrailing)
.cornerRadius(5)
)
}
else {
return AnyView(EmptyView())
}
}
var overlayIndicator : some View {
Text(viewModel.episodeAndSeasonNumber())
.font(viewModel.largeView ? .title3 : .body)
.foregroundColor(.white)
.padding(.horizontal, 5)
.background(Color.black.opacity(0.6))
.cornerRadius(5)
.padding(5)
}
}