ta da 💥 (#64)
Some checks are pending
Build and Release IPA / Build IPA (push) Waiting to run

This commit is contained in:
cranci 2025-04-03 17:19:49 +02:00 committed by GitHub
commit 2711cafc93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 312 additions and 160 deletions

View file

@ -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<T: BinaryFloatingPoint>: View {
@Binding var value: T
let inRange: ClosedRange<T>
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<T: BinaryFloatingPoint>: View {
@Binding var value: T
let inRange: ClosedRange<T>
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<T: BinaryFloatingPoint>: 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))
}
}

View file

@ -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..<resolutionEndRange.lowerBound])
if let heightStr = resolutionPart.components(separatedBy: "x").last,
@ -961,7 +1029,8 @@ class CustomMediaPlayerViewController: UIViewController {
if !nextLine.hasPrefix("http") && nextLine.contains(".m3u8") {
if let baseURL = self.baseM3U8URL {
let baseURLString = baseURL.deletingLastPathComponent().absoluteString
qualityURL = URL(string: nextLine, relativeTo: baseURL)?.absoluteString ?? baseURLString + "/" + nextLine
qualityURL = URL(string: nextLine, relativeTo: baseURL)?.absoluteString
?? baseURLString + "/" + nextLine
}
}
@ -992,7 +1061,8 @@ class CustomMediaPlayerViewController: UIViewController {
}
private func switchToQuality(urlString: String) {
guard let url = URL(string: urlString), currentQualityURL?.absoluteString != urlString else { return }
guard let url = URL(string: urlString),
currentQualityURL?.absoluteString != urlString else { return }
let currentTime = player.currentTime()
let wasPlaying = player.rate > 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