mirror of
https://github.com/cranci1/Sora.git
synced 2026-04-21 08:32:00 +00:00
custom player
This commit is contained in:
parent
71d243dba5
commit
ff6b01c429
6 changed files with 145 additions and 72 deletions
Binary file not shown.
|
|
@ -12,7 +12,7 @@ struct CustomVideoPlayer: UIViewControllerRepresentable {
|
||||||
let player: AVPlayer
|
let player: AVPlayer
|
||||||
|
|
||||||
func makeUIViewController(context: Context) -> AVPlayerViewController {
|
func makeUIViewController(context: Context) -> AVPlayerViewController {
|
||||||
let controller = CustomAVPlayerViewController()
|
let controller = NormalPlayer()
|
||||||
controller.player = player
|
controller.player = player
|
||||||
controller.showsPlaybackControls = false
|
controller.showsPlaybackControls = false
|
||||||
player.play()
|
player.play()
|
||||||
|
|
@ -24,16 +24,6 @@ struct CustomVideoPlayer: UIViewControllerRepresentable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CustomAVPlayerViewController: AVPlayerViewController {
|
|
||||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
|
||||||
if UserDefaults.standard.bool(forKey: "alwaysLandscape") {
|
|
||||||
return .landscape
|
|
||||||
} else {
|
|
||||||
return .all
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CustomMediaPlayer: View {
|
struct CustomMediaPlayer: View {
|
||||||
@State private var player: AVPlayer
|
@State private var player: AVPlayer
|
||||||
@State private var isPlaying = true
|
@State private var isPlaying = true
|
||||||
|
|
@ -41,13 +31,23 @@ struct CustomMediaPlayer: View {
|
||||||
@State private var duration: Double = 0.0
|
@State private var duration: Double = 0.0
|
||||||
@State private var showControls = false
|
@State private var showControls = false
|
||||||
@State private var inactivityTimer: Timer?
|
@State private var inactivityTimer: Timer?
|
||||||
|
@State private var timeObserverToken: Any?
|
||||||
@Environment(\.presentationMode) var presentationMode
|
@Environment(\.presentationMode) var presentationMode
|
||||||
|
|
||||||
init(urlString: String) {
|
let fullUrl: String
|
||||||
|
let title: String
|
||||||
|
let episodeNumber: Int
|
||||||
|
let onWatchNext: () -> Void
|
||||||
|
|
||||||
|
init(urlString: String, fullUrl: String, title: String, episodeNumber: Int, onWatchNext: @escaping () -> Void) {
|
||||||
guard let url = URL(string: urlString) else {
|
guard let url = URL(string: urlString) else {
|
||||||
fatalError("Invalid URL string")
|
fatalError("Invalid URL string")
|
||||||
}
|
}
|
||||||
_player = State(initialValue: AVPlayer(url: url))
|
_player = State(initialValue: AVPlayer(url: url))
|
||||||
|
self.fullUrl = fullUrl
|
||||||
|
self.title = title
|
||||||
|
self.episodeNumber = episodeNumber
|
||||||
|
self.onWatchNext = onWatchNext
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
@ -63,6 +63,7 @@ struct CustomMediaPlayer: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
startUpdatingCurrentTime()
|
startUpdatingCurrentTime()
|
||||||
|
addPeriodicTimeObserver(fullURL: fullUrl)
|
||||||
}
|
}
|
||||||
.edgesIgnoringSafeArea(.all)
|
.edgesIgnoringSafeArea(.all)
|
||||||
.overlay(
|
.overlay(
|
||||||
|
|
@ -118,11 +119,30 @@ struct CustomMediaPlayer: View {
|
||||||
|
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
if showControls {
|
VStack {
|
||||||
VStack {
|
Spacer()
|
||||||
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
HStack {
|
if duration - currentTime <= duration * 0.06 {
|
||||||
Spacer()
|
Button(action: {
|
||||||
|
player.pause()
|
||||||
|
presentationMode.wrappedValue.dismiss()
|
||||||
|
onWatchNext()
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "forward.fill")
|
||||||
|
.foregroundColor(Color.black)
|
||||||
|
Text("Watch Next")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(Color.black)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color.white.opacity(0.8))
|
||||||
|
.cornerRadius(32)
|
||||||
|
}
|
||||||
|
.padding(.trailing, 10)
|
||||||
|
}
|
||||||
|
if showControls {
|
||||||
Menu {
|
Menu {
|
||||||
Menu("Playback Speed") {
|
Menu("Playback Speed") {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
|
|
@ -196,8 +216,10 @@ struct CustomMediaPlayer: View {
|
||||||
.font(.system(size: 15))
|
.font(.system(size: 15))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.trailing, 10)
|
}
|
||||||
|
.padding(.trailing, 10)
|
||||||
|
|
||||||
|
if showControls {
|
||||||
MusicProgressSlider(
|
MusicProgressSlider(
|
||||||
value: $currentTime,
|
value: $currentTime,
|
||||||
inRange: 0...duration,
|
inRange: 0...duration,
|
||||||
|
|
@ -211,8 +233,8 @@ struct CustomMediaPlayer: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.frame(height: 45)
|
.frame(height: 45)
|
||||||
.padding(.bottom, 10)
|
.padding(.bottom, 10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -222,6 +244,10 @@ struct CustomMediaPlayer: View {
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
player.pause()
|
player.pause()
|
||||||
inactivityTimer?.invalidate()
|
inactivityTimer?.invalidate()
|
||||||
|
if let timeObserverToken = timeObserverToken {
|
||||||
|
player.removeTimeObserver(timeObserverToken)
|
||||||
|
self.timeObserverToken = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -235,6 +261,7 @@ struct CustomMediaPlayer: View {
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.font(.system(size: 20))
|
.font(.system(size: 20))
|
||||||
}
|
}
|
||||||
|
.frame(width: 60, height: 60)
|
||||||
.padding()
|
.padding()
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
@ -249,4 +276,20 @@ struct CustomMediaPlayer: View {
|
||||||
currentTime = player.currentTime().seconds
|
currentTime = player.currentTime().seconds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private func addPeriodicTimeObserver(fullURL: String) {
|
||||||
|
let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||||
|
timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in
|
||||||
|
guard let currentItem = player.currentItem,
|
||||||
|
currentItem.duration.seconds.isFinite else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentTime = time.seconds
|
||||||
|
let duration = currentItem.duration.seconds
|
||||||
|
|
||||||
|
UserDefaults.standard.set(currentTime, forKey: "lastPlayedTime_\(fullURL)")
|
||||||
|
UserDefaults.standard.set(duration, forKey: "totalTime_\(fullURL)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,13 +36,13 @@ class VideoPlayerViewController: UIViewController {
|
||||||
|
|
||||||
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(fullUrl)")
|
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(fullUrl)")
|
||||||
if lastPlayedTime > 0 {
|
if lastPlayedTime > 0 {
|
||||||
let seekTime = CMTime(seconds: lastPlayedTime, preferredTimescale: 1)
|
let seekTime = CMTime(seconds: lastPlayedTime, preferredTimescale: 1)
|
||||||
self.player?.seek(to: seekTime) { _ in
|
self.player?.seek(to: seekTime) { _ in
|
||||||
self.player?.play()
|
self.player?.play()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.player?.play()
|
self.player?.play()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
|
@ -51,28 +51,28 @@ class VideoPlayerViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidDisappear(_ animated: Bool) {
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
super.viewDidDisappear(animated)
|
super.viewDidDisappear(animated)
|
||||||
if let timeObserverToken = timeObserverToken {
|
if let timeObserverToken = timeObserverToken {
|
||||||
player?.removeTimeObserver(timeObserverToken)
|
player?.removeTimeObserver(timeObserverToken)
|
||||||
self.timeObserverToken = nil
|
self.timeObserverToken = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addPeriodicTimeObserver(fullURL: String) {
|
func addPeriodicTimeObserver(fullURL: String) {
|
||||||
guard let player = self.player else { return }
|
guard let player = self.player else { return }
|
||||||
|
|
||||||
let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||||
timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in
|
timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in
|
||||||
guard let currentItem = player.currentItem,
|
guard let currentItem = player.currentItem,
|
||||||
currentItem.duration.seconds.isFinite else {
|
currentItem.duration.seconds.isFinite else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentTime = time.seconds
|
let currentTime = time.seconds
|
||||||
let duration = currentItem.duration.seconds
|
let duration = currentItem.duration.seconds
|
||||||
|
|
||||||
UserDefaults.standard.set(currentTime, forKey: "lastPlayedTime_\(fullURL)")
|
UserDefaults.standard.set(currentTime, forKey: "lastPlayedTime_\(fullURL)")
|
||||||
UserDefaults.standard.set(duration, forKey: "totalTime_\(fullURL)")
|
UserDefaults.standard.set(duration, forKey: "totalTime_\(fullURL)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ struct AnimeInfoView: View {
|
||||||
@State var isLoading: Bool = true
|
@State var isLoading: Bool = true
|
||||||
@State var showFullSynopsis: Bool = false
|
@State var showFullSynopsis: Bool = false
|
||||||
@State var animeID: Int?
|
@State var animeID: Int?
|
||||||
|
@State private var selectedEpisode: String = ""
|
||||||
|
@State private var selectedEpisodeNumber: Int = 0
|
||||||
|
|
||||||
@AppStorage("externalPlayer") private var externalPlayer: String = "Default"
|
@AppStorage("externalPlayer") private var externalPlayer: String = "Default"
|
||||||
|
|
||||||
|
|
@ -138,6 +140,8 @@ struct AnimeInfoView: View {
|
||||||
|
|
||||||
EpisodeCell(episode: episodes[index], episodeID: index, imageUrl: anime.imageUrl, progress: progress, animeID: animeID ?? 0)
|
EpisodeCell(episode: episodes[index], episodeID: index, imageUrl: anime.imageUrl, progress: progress, animeID: animeID ?? 0)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
|
selectedEpisode = episodes[index]
|
||||||
|
selectedEpisodeNumber = index + 1
|
||||||
fetchEpisodeStream(urlString: episodeURL)
|
fetchEpisodeStream(urlString: episodeURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -185,7 +189,15 @@ struct AnimeInfoView: View {
|
||||||
return
|
return
|
||||||
} else if externalPlayer == "Custom" {
|
} else if externalPlayer == "Custom" {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let customMediaPlayer = CustomMediaPlayer(urlString: streamUrl)
|
let customMediaPlayer = CustomMediaPlayer(
|
||||||
|
urlString: streamUrl,
|
||||||
|
fullUrl: fullURL,
|
||||||
|
title: anime.name,
|
||||||
|
episodeNumber: selectedEpisodeNumber,
|
||||||
|
onWatchNext: {
|
||||||
|
selectNextEpisode()
|
||||||
|
}
|
||||||
|
)
|
||||||
let hostingController = UIHostingController(rootView: customMediaPlayer)
|
let hostingController = UIHostingController(rootView: customMediaPlayer)
|
||||||
hostingController.modalPresentationStyle = .fullScreen
|
hostingController.modalPresentationStyle = .fullScreen
|
||||||
Logger.shared.log("Opening custom media player with url: \(streamUrl)")
|
Logger.shared.log("Opening custom media player with url: \(streamUrl)")
|
||||||
|
|
@ -212,6 +224,17 @@ struct AnimeInfoView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func selectNextEpisode() {
|
||||||
|
guard let currentEpisodeIndex = episodes.firstIndex(of: selectedEpisode) else { return }
|
||||||
|
let nextEpisodeIndex = currentEpisodeIndex + 1
|
||||||
|
if nextEpisodeIndex < episodes.count {
|
||||||
|
selectedEpisode = episodes[nextEpisodeIndex]
|
||||||
|
selectedEpisodeNumber = nextEpisodeIndex + 1
|
||||||
|
let nextEpisodeURL = "\(module.module[0].details.baseURL)\(episodes[nextEpisodeIndex])"
|
||||||
|
fetchEpisodeStream(urlString: nextEpisodeURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func openSafariViewController(with urlString: String) {
|
private func openSafariViewController(with urlString: String) {
|
||||||
guard let url = URL(string: anime.href.hasPrefix("http") ? anime.href : "\(module.module[0].details.baseURL)\(anime.href)") else {
|
guard let url = URL(string: anime.href.hasPrefix("http") ? anime.href : "\(module.module[0].details.baseURL)\(anime.href)") else {
|
||||||
Logger.shared.log("Unable to open the webpage")
|
Logger.shared.log("Unable to open the webpage")
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,13 @@ struct CircularProgressBar: View {
|
||||||
.rotationEffect(Angle(degrees: 270.0))
|
.rotationEffect(Angle(degrees: 270.0))
|
||||||
.animation(.linear, value: progress)
|
.animation(.linear, value: progress)
|
||||||
|
|
||||||
Text(String(format: "%.0f%%", min(progress, 1.0) * 100.0))
|
if progress >= 0.95 {
|
||||||
.font(.system(size: 12))
|
Image(systemName: "checkmark")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
} else {
|
||||||
|
Text(String(format: "%.0f%%", min(progress, 1.0) * 100.0))
|
||||||
|
.font(.system(size: 12))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -78,21 +78,23 @@ struct EpisodeCell: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
|
let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
|
||||||
let episodes = json["episodes"] as? [String: Any],
|
guard let json = jsonObject as? [String: Any],
|
||||||
let episodeDetails = episodes["\(episodeID + 1)"] as? [String: Any],
|
let episodes = json["episodes"] as? [String: Any],
|
||||||
let title = episodeDetails["title"] as? [String: String],
|
let episodeDetails = episodes["\(episodeID + 1)"] as? [String: Any],
|
||||||
let image = episodeDetails["image"] as? String {
|
let title = episodeDetails["title"] as? [String: String],
|
||||||
DispatchQueue.main.async {
|
let image = episodeDetails["image"] as? String else {
|
||||||
self.episodeTitle = title["en"] ?? ""
|
print("Invalid response format")
|
||||||
self.episodeImageUrl = image
|
DispatchQueue.main.async {
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
}
|
}
|
||||||
} else {
|
return
|
||||||
print("Invalid response")
|
}
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.isLoading = false
|
DispatchQueue.main.async {
|
||||||
}
|
self.episodeTitle = title["en"] ?? ""
|
||||||
|
self.episodeImageUrl = image
|
||||||
|
self.isLoading = false
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to parse JSON: \(error)")
|
print("Failed to parse JSON: \(error)")
|
||||||
|
|
@ -102,4 +104,4 @@ struct EpisodeCell: View {
|
||||||
}
|
}
|
||||||
}.resume()
|
}.resume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue