diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift b/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift index e8a3ca8..55ae983 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift @@ -5,82 +5,129 @@ // Created by Pratik on 08/01/23. // // Thanks to pratikg29 for this code inside his open source project "https://github.com/pratikg29/Custom-Slider-Control?ref=iosexample.com" -// I did edit just a little bit the code for my liking -// +// I did edit some of the code for my liking (added a buffer indicator, etc.) import SwiftUI -struct MusicProgressSlider: View { - @Binding var value: T - let inRange: ClosedRange - let activeFillColor: Color - let fillColor: Color - let emptyColor: Color - let height: CGFloat - let onEditingChanged: (Bool) -> Void - - @State private var localRealProgress: T = 0 - @State private var localTempProgress: T = 0 - @GestureState private var isActive: Bool = false - - var body: some View { - GeometryReader { bounds in - ZStack { - VStack { - ZStack(alignment: .center) { - Capsule() - .fill(emptyColor) - Capsule() - .fill(isActive ? activeFillColor : fillColor) - .mask({ - HStack { - Rectangle() - .frame(width: max((bounds.size.width * CGFloat(localRealProgress + localTempProgress)).isFinite ? bounds.size.width * CGFloat(localRealProgress + localTempProgress) : 0, 0), alignment: .leading) - Spacer(minLength: 0) - } - }) + struct MusicProgressSlider: View { + @Binding var value: T + let inRange: ClosedRange + + let bufferValue: T + let activeFillColor: Color + let fillColor: Color + let bufferColor: Color + let emptyColor: Color + let height: CGFloat + + let onEditingChanged: (Bool) -> Void + + @State private var localRealProgress: T = 0 + @State private var localTempProgress: T = 0 + @GestureState private var isActive: Bool = false + + var body: some View { + GeometryReader { bounds in + ZStack { + VStack { + ZStack(alignment: .center) { + Capsule() + .fill(emptyColor) + + Capsule() + .fill(bufferColor) + .mask({ + HStack { + Rectangle() + .frame( + width: max( + (bounds.size.width + * CGFloat(getPrgPercentage(bufferValue))) + .isFinite + ? bounds.size.width + * CGFloat(getPrgPercentage(bufferValue)) + : 0, + 0 + ), + alignment: .leading + ) + Spacer(minLength: 0) + } + }) + + Capsule() + .fill(isActive ? activeFillColor : fillColor) + .mask({ + HStack { + Rectangle() + .frame( + width: max( + (bounds.size.width + * CGFloat(localRealProgress + localTempProgress)) + .isFinite + ? bounds.size.width + * CGFloat(localRealProgress + localTempProgress) + : 0, + 0 + ), + alignment: .leading + ) + Spacer(minLength: 0) + } + }) + } + + HStack { + let shouldShowHours = inRange.upperBound >= 3600 + Text(value.asTimeString(style: .positional, showHours: shouldShowHours)) + Spacer(minLength: 0) + Text("-" + (inRange.upperBound - value).asTimeString( + style: .positional, + showHours: shouldShowHours + )) + } + .font(.system(size: 12)) + .foregroundColor(isActive ? fillColor : emptyColor) } - - HStack { - // Determine if we should show hours based on the total duration. - let shouldShowHours = inRange.upperBound >= 3600 - Text(value.asTimeString(style: .positional, showHours: shouldShowHours)) - Spacer(minLength: 0) - Text("-" + (inRange.upperBound - value).asTimeString(style: .positional, showHours: shouldShowHours)) - } - - .font(.system(size: 12)) - .foregroundColor(isActive ? fillColor : emptyColor) + .frame( + width: isActive ? bounds.size.width * 1.04 : bounds.size.width, + alignment: .center + ) + .animation(animation, value: isActive) } - .frame(width: isActive ? bounds.size.width * 1.04 : bounds.size.width, alignment: .center) - .animation(animation, value: isActive) - } - .frame(width: bounds.size.width, height: bounds.size.height, alignment: .center) - .contentShape(Rectangle()) - .gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local) + .frame( + width: bounds.size.width, + height: bounds.size.height, + alignment: .center + ) + .contentShape(Rectangle()) + .gesture( + DragGesture(minimumDistance: 0, coordinateSpace: .local) .updating($isActive) { _, state, _ in - state = true - } + state = true + } .onChanged { gesture in - localTempProgress = T(gesture.translation.width / bounds.size.width) - value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound) - }.onEnded { _ in - localRealProgress = max(min(localRealProgress + localTempProgress, 1), 0) - localTempProgress = 0 - }) - .onChange(of: isActive) { newValue in - value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound) - onEditingChanged(newValue) - } - .onAppear { - localRealProgress = getPrgPercentage(value) - } - .onChange(of: value) { newValue in - if !isActive { - localRealProgress = getPrgPercentage(newValue) + localTempProgress = T(gesture.translation.width / bounds.size.width) + value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound) + } + .onEnded { _ in + localRealProgress = max(min(localRealProgress + localTempProgress, 1), 0) + localTempProgress = 0 + } + ) + .onChange(of: isActive) { newValue in + value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound) + onEditingChanged(newValue) + } + .onAppear { + localRealProgress = getPrgPercentage(value) + } + .onChange(of: value) { newValue in + if !isActive { + localRealProgress = getPrgPercentage(newValue) + } } } - } .frame(height: isActive ? height * 1.25 : height, alignment: .center) } @@ -100,6 +147,36 @@ struct MusicProgressSlider: View { } private func getPrgValue() -> T { - return ((localRealProgress + localTempProgress) * (inRange.upperBound - inRange.lowerBound)) + inRange.lowerBound + return ((localRealProgress + localTempProgress) * (inRange.upperBound - inRange.lowerBound)) + inRange.lowerBound} + // MARK: - Helpers + + private func fraction(of val: T) -> T { + let total = inRange.upperBound - inRange.lowerBound + let normalized = val - inRange.lowerBound + return (total > 0) ? (normalized / total) : 0 + } + + private func clampedFraction(_ f: T) -> T { + max(0, min(f, 1)) + } + + private func getCurrentValue() -> T { + let total = inRange.upperBound - inRange.lowerBound + let frac = clampedFraction(localRealProgress + localTempProgress) + return frac * total + inRange.lowerBound + } + + private func clampedValue(_ raw: T) -> T { + max(inRange.lowerBound, min(raw, inRange.upperBound)) + } + + private func playedWidth(boundsWidth: CGFloat) -> CGFloat { + let frac = fraction(of: value) + return max(0, min(boundsWidth * CGFloat(frac), boundsWidth)) + } + + private func bufferWidth(boundsWidth: CGFloat) -> CGFloat { + let frac = fraction(of: bufferValue) + return max(0, min(boundsWidth * CGFloat(frac), boundsWidth)) } } diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index f30a6f3..c8c198f 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -10,10 +10,15 @@ import AVKit import SwiftUI import AVFoundation +// MARK: - SliderViewModel + class SliderViewModel: ObservableObject { @Published var sliderValue: Double = 0.0 + @Published var bufferValue: Double = 0.0 } +// MARK: - CustomMediaPlayerViewController + class CustomMediaPlayerViewController: UIViewController { let module: ScrapingModule let streamURL: String @@ -82,13 +87,16 @@ class CustomMediaPlayerViewController: UIViewController { var isControlsVisible = false var subtitleBottomConstraint: NSLayoutConstraint? - var subtitleBottomPadding: CGFloat = 10.0 { didSet { updateSubtitleLabelConstraints() } } + private var playerItemKVOContext = 0 + private var loadedTimeRangesObservation: NSKeyValueObservation? + + init(module: ScrapingModule, urlString: String, fullUrl: String, @@ -112,10 +120,12 @@ class CustomMediaPlayerViewController: UIViewController { guard let url = URL(string: urlString) else { fatalError("Invalid URL string") } + var request = URLRequest(url: url) request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer") request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Origin") - request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", forHTTPHeaderField: "User-Agent") + request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", + forHTTPHeaderField: "User-Agent") let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": request.allHTTPHeaderFields ?? [:]]) let playerItem = AVPlayerItem(asset: asset) @@ -145,8 +155,8 @@ class CustomMediaPlayerViewController: UIViewController { addInvisibleControlOverlays() setupSubtitleLabel() setupDismissButton() - setupQualityButton() setupSpeedButton() + setupQualityButton() setupMenuButton() setupSkip85Button() setupWatchNextButton() @@ -154,6 +164,12 @@ class CustomMediaPlayerViewController: UIViewController { startUpdateTimer() setupAudioSession() + if let item = player.currentItem { + loadedTimeRangesObservation = item.observe(\.loadedTimeRanges, options: [.new, .initial]) { [weak self] (playerItem, change) in + self?.updateBufferValue() + } + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in self?.checkForHLSStream() } @@ -175,25 +191,32 @@ class CustomMediaPlayerViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidChange), name: .AVPlayerItemNewAccessLogEntry, object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(playerItemDidChange), + name: .AVPlayerItemNewAccessLogEntry, + object: nil) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - NotificationCenter.default.removeObserver(self, name: .AVPlayerItemNewAccessLogEntry, object: nil) + loadedTimeRangesObservation?.invalidate() + loadedTimeRangesObservation = nil - if let playbackSpeed = player?.rate { - UserDefaults.standard.set(playbackSpeed, forKey: "lastPlaybackSpeed") - } - player.pause() - updateTimer?.invalidate() - inactivityTimer?.invalidate() if let token = timeObserverToken { player.removeTimeObserver(token) timeObserverToken = nil } - UserDefaults.standard.set(player.rate, forKey: "lastPlaybackSpeed") + + updateTimer?.invalidate() + inactivityTimer?.invalidate() + + player.pause() + + if let playbackSpeed = player?.rate { + UserDefaults.standard.set(playbackSpeed, forKey: "lastPlaybackSpeed") + } + if let currentItem = player.currentItem, currentItem.duration.seconds > 0 { let progress = currentTimeVal / currentItem.duration.seconds let item = ContinueWatchingItem( @@ -211,9 +234,36 @@ class CustomMediaPlayerViewController: UIViewController { } } + override func observeValue(forKeyPath keyPath: String?, + of object: Any?, + change: [NSKeyValueChangeKey : Any]?, + context: UnsafeMutableRawPointer?) { + + guard context == &playerItemKVOContext else { + super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) + return + } + + if keyPath == "loadedTimeRanges" { + updateBufferValue() + } + } + + private func updateBufferValue() { + guard let item = player.currentItem else { return } + + if let timeRange = item.loadedTimeRanges.first?.timeRangeValue { + let buffered = CMTimeGetSeconds(timeRange.start) + CMTimeGetSeconds(timeRange.duration) + DispatchQueue.main.async { + self.sliderViewModel.bufferValue = buffered + } + } + } + @objc private func playerItemDidChange() { DispatchQueue.main.async { [weak self] in - if let self = self, self.qualityButton.isHidden && self.isHLSStream { + guard let self = self else { return } + if self.qualityButton.isHidden && self.isHLSStream { self.qualityButton.isHidden = false self.qualityButton.menu = self.qualitySelectionMenu() } @@ -310,8 +360,10 @@ class CustomMediaPlayerViewController: UIViewController { value: Binding(get: { self.sliderViewModel.sliderValue }, set: { self.sliderViewModel.sliderValue = $0 }), inRange: 0...(duration > 0 ? duration : 1.0), + bufferValue: self.sliderViewModel.bufferValue, activeFillColor: .white, fillColor: .white.opacity(0.5), + bufferColor: .white.opacity(0.2), emptyColor: .white.opacity(0.3), height: 30, onEditingChanged: { editing in @@ -353,6 +405,7 @@ class CustomMediaPlayerViewController: UIViewController { ]) } + func addInvisibleControlOverlays() { let playPauseOverlay = UIButton(type: .custom) playPauseOverlay.backgroundColor = .clear @@ -365,30 +418,6 @@ class CustomMediaPlayerViewController: UIViewController { playPauseOverlay.widthAnchor.constraint(equalTo: playPauseButton.widthAnchor, constant: 20), playPauseOverlay.heightAnchor.constraint(equalTo: playPauseButton.heightAnchor, constant: 20) ]) - - let backwardOverlay = UIButton(type: .custom) - backwardOverlay.backgroundColor = .clear - backwardOverlay.addTarget(self, action: #selector(seekBackward), for: .touchUpInside) - view.addSubview(backwardOverlay) - backwardOverlay.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - backwardOverlay.centerXAnchor.constraint(equalTo: backwardButton.centerXAnchor), - backwardOverlay.centerYAnchor.constraint(equalTo: backwardButton.centerYAnchor), - backwardOverlay.widthAnchor.constraint(equalTo: backwardButton.widthAnchor, constant: 20), - backwardOverlay.heightAnchor.constraint(equalTo: backwardButton.heightAnchor, constant: 20) - ]) - - let forwardOverlay = UIButton(type: .custom) - forwardOverlay.backgroundColor = .clear - forwardOverlay.addTarget(self, action: #selector(seekForward), for: .touchUpInside) - view.addSubview(forwardOverlay) - forwardOverlay.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - forwardOverlay.centerXAnchor.constraint(equalTo: forwardButton.centerXAnchor), - forwardOverlay.centerYAnchor.constraint(equalTo: forwardButton.centerYAnchor), - forwardOverlay.widthAnchor.constraint(equalTo: forwardButton.widthAnchor, constant: 20), - forwardOverlay.heightAnchor.constraint(equalTo: forwardButton.heightAnchor, constant: 20) - ]) } func setupSkipAndDismissGestures() { @@ -516,32 +545,49 @@ class CustomMediaPlayerViewController: UIViewController { dismissButton.addTarget(self, action: #selector(dismissTapped), for: .touchUpInside) controlsContainerView.addSubview(dismissButton) dismissButton.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ dismissButton.leadingAnchor.constraint(equalTo: controlsContainerView.leadingAnchor, constant: 16), dismissButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), dismissButton.widthAnchor.constraint(equalToConstant: 40), dismissButton.heightAnchor.constraint(equalToConstant: 40) ]) + + let episodeLabel = UILabel() + episodeLabel.text = "\(titleText) • Ep \(episodeNumber)" + episodeLabel.textColor = .white + episodeLabel.font = UIFont.systemFont(ofSize: 15, weight: .medium) + episodeLabel.numberOfLines = 1 + episodeLabel.lineBreakMode = .byTruncatingTail + + controlsContainerView.addSubview(episodeLabel) + episodeLabel.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + episodeLabel.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor), + episodeLabel.leadingAnchor.constraint(equalTo: dismissButton.trailingAnchor, constant: 12), + episodeLabel.trailingAnchor.constraint(lessThanOrEqualTo: controlsContainerView.trailingAnchor, constant: -16) + ]) } func setupMenuButton() { menuButton = UIButton(type: .system) menuButton.setImage(UIImage(systemName: "text.bubble"), for: .normal) menuButton.tintColor = .white - + if let subtitlesURL = subtitlesURL, !subtitlesURL.isEmpty { menuButton.showsMenuAsPrimaryAction = true menuButton.menu = buildOptionsMenu() } else { menuButton.isHidden = true } - + controlsContainerView.addSubview(menuButton) menuButton.translatesAutoresizingMaskIntoConstraints = false - + NSLayoutConstraint.activate([ menuButton.topAnchor.constraint(equalTo: controlsContainerView.topAnchor, constant: 20), - menuButton.trailingAnchor.constraint(equalTo: speedButton.leadingAnchor, constant: -20), + menuButton.trailingAnchor.constraint(equalTo: qualityButton.leadingAnchor, constant: -20), menuButton.widthAnchor.constraint(equalToConstant: 40), menuButton.heightAnchor.constraint(equalToConstant: 40) ]) @@ -553,23 +599,26 @@ class CustomMediaPlayerViewController: UIViewController { speedButton.tintColor = .white speedButton.showsMenuAsPrimaryAction = true speedButton.menu = speedChangerMenu() - + controlsContainerView.addSubview(speedButton) speedButton.translatesAutoresizingMaskIntoConstraints = false - + NSLayoutConstraint.activate([ - // Middle speedButton.topAnchor.constraint(equalTo: controlsContainerView.topAnchor, constant: 20), - speedButton.trailingAnchor.constraint(equalTo: qualityButton.leadingAnchor, constant: -20), + speedButton.trailingAnchor.constraint(equalTo: controlsContainerView.trailingAnchor, constant: -20), speedButton.widthAnchor.constraint(equalToConstant: 40), speedButton.heightAnchor.constraint(equalToConstant: 40) ]) } func setupWatchNextButton() { + let config = UIImage.SymbolConfiguration(pointSize: 14, weight: .regular) + let image = UIImage(systemName: "forward.fill", withConfiguration: config) + watchNextButton = UIButton(type: .system) - watchNextButton.setTitle("Play Next", for: .normal) - watchNextButton.setImage(UIImage(systemName: "forward.fill"), for: .normal) + watchNextButton.setTitle(" Play Next", for: .normal) + watchNextButton.titleLabel?.font = UIFont.systemFont(ofSize: 14) + watchNextButton.setImage(image, for: .normal) watchNextButton.tintColor = .black watchNextButton.backgroundColor = .white watchNextButton.layer.cornerRadius = 25 @@ -591,17 +640,21 @@ class CustomMediaPlayerViewController: UIViewController { watchNextButtonControlsConstraints = [ watchNextButton.trailingAnchor.constraint(equalTo: sliderHostingController!.view.trailingAnchor), watchNextButton.bottomAnchor.constraint(equalTo: sliderHostingController!.view.topAnchor, constant: -5), - watchNextButton.heightAnchor.constraint(equalToConstant: 50), - watchNextButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 120) + watchNextButton.heightAnchor.constraint(equalToConstant: 47), + watchNextButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 97) ] NSLayoutConstraint.activate(watchNextButtonNormalConstraints) } func setupSkip85Button() { + let config = UIImage.SymbolConfiguration(pointSize: 14, weight: .regular) + let image = UIImage(systemName: "goforward", withConfiguration: config) + skip85Button = UIButton(type: .system) - skip85Button.setTitle("Skip 85s", for: .normal) - skip85Button.setImage(UIImage(systemName: "goforward"), for: .normal) + skip85Button.setTitle(" Skip 85s", for: .normal) + skip85Button.titleLabel?.font = UIFont.systemFont(ofSize: 14) + skip85Button.setImage(image, for: .normal) skip85Button.tintColor = .black skip85Button.backgroundColor = .white skip85Button.layer.cornerRadius = 25 @@ -614,9 +667,9 @@ class CustomMediaPlayerViewController: UIViewController { NSLayoutConstraint.activate([ skip85Button.leadingAnchor.constraint(equalTo: sliderHostingController!.view.leadingAnchor), - skip85Button.bottomAnchor.constraint(equalTo: sliderHostingController!.view.topAnchor, constant: -3), - skip85Button.heightAnchor.constraint(equalToConstant: 50), - skip85Button.widthAnchor.constraint(greaterThanOrEqualToConstant: 120) + skip85Button.bottomAnchor.constraint(equalTo: sliderHostingController!.view.topAnchor, constant: -5), + skip85Button.heightAnchor.constraint(equalToConstant: 47), + skip85Button.widthAnchor.constraint(greaterThanOrEqualToConstant: 97) ]) } @@ -627,18 +680,17 @@ class CustomMediaPlayerViewController: UIViewController { qualityButton.showsMenuAsPrimaryAction = true qualityButton.menu = qualitySelectionMenu() qualityButton.isHidden = true - + controlsContainerView.addSubview(qualityButton) qualityButton.translatesAutoresizingMaskIntoConstraints = false - + NSLayoutConstraint.activate([ qualityButton.topAnchor.constraint(equalTo: controlsContainerView.topAnchor, constant: 20), - qualityButton.trailingAnchor.constraint(equalTo: controlsContainerView.trailingAnchor, constant: -20), + qualityButton.trailingAnchor.constraint(equalTo: speedButton.leadingAnchor, constant: -20), qualityButton.widthAnchor.constraint(equalToConstant: 40), qualityButton.heightAnchor.constraint(equalToConstant: 40) ]) } - func updateSubtitleLabelAppearance() { subtitleLabel.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize)) @@ -666,7 +718,9 @@ class CustomMediaPlayerViewController: UIViewController { func addTimeObserver() { let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) - timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in + timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, + queue: .main) + { [weak self] time in guard let self = self, let currentItem = self.player.currentItem, currentItem.duration.seconds.isFinite else { return } @@ -693,18 +747,24 @@ class CustomMediaPlayerViewController: UIViewController { DispatchQueue.main.async { self.sliderHostingController?.rootView = MusicProgressSlider( - value: Binding(get: { max(0, min(self.sliderViewModel.sliderValue, self.duration)) }, - set: { self.sliderViewModel.sliderValue = max(0, min($0, self.duration)) }), + value: Binding(get: { + max(0, min(self.sliderViewModel.sliderValue, self.duration)) + }, set: { + self.sliderViewModel.sliderValue = max(0, min($0, self.duration)) + }), inRange: 0...(self.duration > 0 ? self.duration : 1.0), + bufferValue: self.sliderViewModel.bufferValue, activeFillColor: .white, - fillColor: .white.opacity(0.5), + fillColor: .white.opacity(0.6), + bufferColor: .white.opacity(0.36), emptyColor: .white.opacity(0.3), height: 30, onEditingChanged: { editing in - self.isSliderEditing = editing if !editing { - let seekTime = CMTime(seconds: self.sliderViewModel.sliderValue, preferredTimescale: 600) - self.player.seek(to: seekTime) + let targetTime = CMTime(seconds: self.sliderViewModel.sliderValue, preferredTimescale: 600) + self.player.seek(to: targetTime) { [weak self] finished in + self?.updateBufferValue() + } } } ) @@ -758,8 +818,6 @@ class CustomMediaPlayerViewController: UIViewController { } } } - - func repositionWatchNextButton() { self.isWatchNextRepositioned = true @@ -774,7 +832,6 @@ class CustomMediaPlayerViewController: UIViewController { self.watchNextButtonTimer?.invalidate() self.watchNextButtonTimer = nil } - func startUpdateTimer() { updateTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in @@ -820,8 +877,11 @@ class CustomMediaPlayerViewController: UIViewController { let holdValue = UserDefaults.standard.double(forKey: "skipIncrementHold") let finalSkip = holdValue > 0 ? holdValue : 30 currentTimeVal = max(currentTimeVal - finalSkip, 0) - player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) - } + player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] finished in + guard let self = self else { return } + self.updateBufferValue() + } + } } @objc func seekForwardLongPress(_ gesture: UILongPressGestureRecognizer) { @@ -829,23 +889,32 @@ class CustomMediaPlayerViewController: UIViewController { let holdValue = UserDefaults.standard.double(forKey: "skipIncrementHold") let finalSkip = holdValue > 0 ? holdValue : 30 currentTimeVal = min(currentTimeVal + finalSkip, duration) - player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) - } + player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] finished in + guard let self = self else { return } + self.updateBufferValue() + } + } } @objc func seekBackward() { let skipValue = UserDefaults.standard.double(forKey: "skipIncrement") let finalSkip = skipValue > 0 ? skipValue : 10 currentTimeVal = max(currentTimeVal - finalSkip, 0) - player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) - } + player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] finished in + guard let self = self else { return } + self.updateBufferValue() + } + } @objc func seekForward() { let skipValue = UserDefaults.standard.double(forKey: "skipIncrement") let finalSkip = skipValue > 0 ? skipValue : 10 currentTimeVal = min(currentTimeVal + finalSkip, duration) - player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) - } + player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] finished in + guard let self = self else { return } + self.updateBufferValue() + } + } @objc func handleDoubleTap(_ gesture: UITapGestureRecognizer) { let tapLocation = gesture.location(in: view) @@ -857,7 +926,7 @@ class CustomMediaPlayerViewController: UIViewController { showSkipFeedback(direction: "forward") } } - + @objc func handleSwipeDown(_ gesture: UISwipeGestureRecognizer) { dismiss(animated: true, completion: nil) } @@ -881,11 +950,6 @@ class CustomMediaPlayerViewController: UIViewController { isPlaying.toggle() } - @objc func sliderEditingEnded() { - let newTime = sliderViewModel.sliderValue - player.seek(to: CMTime(seconds: newTime, preferredTimescale: 600)) - } - @objc func dismissTapped() { dismiss(animated: true, completion: nil) } @@ -919,10 +983,13 @@ class CustomMediaPlayerViewController: UIViewController { var request = URLRequest(url: url) request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer") request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Origin") - request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", forHTTPHeaderField: "User-Agent") + request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", + forHTTPHeaderField: "User-Agent") URLSession.shared.dataTask(with: request) { [weak self] data, response, error in - guard let self = self, let data = data, let content = String(data: data, encoding: .utf8) else { + guard let self = self, + let data = data, + let content = String(data: data, encoding: .utf8) else { print("Failed to load m3u8 file") DispatchQueue.main.async { self?.qualities = [] @@ -948,7 +1015,8 @@ class CustomMediaPlayerViewController: UIViewController { for (index, line) in lines.enumerated() { if line.contains("#EXT-X-STREAM-INF"), index + 1 < lines.count { if let resolutionRange = line.range(of: "RESOLUTION="), - let resolutionEndRange = line[resolutionRange.upperBound...].range(of: ",") ?? line[resolutionRange.upperBound...].range(of: "\n") { + let resolutionEndRange = line[resolutionRange.upperBound...].range(of: ",") + ?? line[resolutionRange.upperBound...].range(of: "\n") { let resolutionPart = String(line[resolutionRange.upperBound.. 0 @@ -1000,13 +1070,13 @@ class CustomMediaPlayerViewController: UIViewController { var request = URLRequest(url: url) request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer") request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Origin") - request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", forHTTPHeaderField: "User-Agent") + request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", + forHTTPHeaderField: "User-Agent") let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": request.allHTTPHeaderFields ?? [:]]) let playerItem = AVPlayerItem(asset: asset) player.replaceCurrentItem(with: playerItem) - player.seek(to: currentTime) if wasPlaying { player.play() @@ -1018,7 +1088,10 @@ class CustomMediaPlayerViewController: UIViewController { qualityButton.menu = qualitySelectionMenu() if let selectedQuality = qualities.first(where: { $0.1 == urlString })?.0 { - DropManager.shared.showDrop(title: "Quality: \(selectedQuality)", subtitle: "", duration: 0.5, icon: UIImage(systemName: "eye")) + DropManager.shared.showDrop(title: "Quality: \(selectedQuality)", + subtitle: "", + duration: 0.5, + icon: UIImage(systemName: "eye")) } } @@ -1207,7 +1280,9 @@ class CustomMediaPlayerViewController: UIViewController { ] let paddingMenu = UIMenu(title: "Bottom Padding", children: paddingActions) - let subtitleOptionsMenu = UIMenu(title: "Subtitle Options", children: [subtitlesToggleAction, colorMenu, fontSizeMenu, shadowMenu, backgroundMenu, paddingMenu]) + let subtitleOptionsMenu = UIMenu(title: "Subtitle Options", children: [ + subtitlesToggleAction, colorMenu, fontSizeMenu, shadowMenu, backgroundMenu, paddingMenu + ]) menuElements = [subtitleOptionsMenu] } @@ -1285,7 +1360,6 @@ class CustomMediaPlayerViewController: UIViewController { let audioSession = AVAudioSession.sharedInstance() try audioSession.setCategory(.playback, mode: .moviePlayback, options: .mixWithOthers) try audioSession.setActive(true) - try audioSession.overrideOutputAudioPort(.speaker) } catch { Logger.shared.log("Failed to set up AVAudioSession: \(error)") @@ -1330,6 +1404,7 @@ class CustomMediaPlayerViewController: UIViewController { } } + // yes? Like the plural of the famous american rapper ye? -IBHRAD // low taper fade the meme is massive -cranci // cranci still doesnt have a job -seiike