mirror of
https://github.com/cranci1/Sora.git
synced 2026-04-13 21:10:23 +00:00
custom player start
This commit is contained in:
parent
c35b6c60b5
commit
5ccbbf1c1b
8 changed files with 428 additions and 7 deletions
|
|
@ -7,6 +7,9 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1308CFBC2D19844A004CD38C /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1308CFBB2D19844A004CD38C /* Double+Extension.swift */; };
|
||||
1308CFBE2D19844D004CD38C /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1308CFBD2D19844D004CD38C /* MusicProgressSlider.swift */; };
|
||||
1308CFC12D198466004CD38C /* CustomPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1308CFC02D198466004CD38C /* CustomPlayer.swift */; };
|
||||
132417842D13198000B4F2D2 /* SoraApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417832D13198000B4F2D2 /* SoraApp.swift */; };
|
||||
132417862D13198000B4F2D2 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417852D13198000B4F2D2 /* ContentView.swift */; };
|
||||
132417882D13198200B4F2D2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 132417872D13198200B4F2D2 /* Assets.xcassets */; };
|
||||
|
|
@ -39,6 +42,9 @@
|
|||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1308CFBB2D19844A004CD38C /* Double+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = "<group>"; };
|
||||
1308CFBD2D19844D004CD38C /* MusicProgressSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = "<group>"; };
|
||||
1308CFC02D198466004CD38C /* CustomPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPlayer.swift; sourceTree = "<group>"; };
|
||||
132417802D13198000B4F2D2 /* Sora.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sora.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
132417832D13198000B4F2D2 /* SoraApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoraApp.swift; sourceTree = "<group>"; };
|
||||
132417852D13198000B4F2D2 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -83,6 +89,24 @@
|
|||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
1308CFBA2D19843E004CD38C /* CustomPlayer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1308CFC02D198466004CD38C /* CustomPlayer.swift */,
|
||||
1308CFBF2D198450004CD38C /* Components */,
|
||||
);
|
||||
path = CustomPlayer;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1308CFBF2D198450004CD38C /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1308CFBD2D19844D004CD38C /* MusicProgressSlider.swift */,
|
||||
1308CFBB2D19844A004CD38C /* Double+Extension.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
132417772D13198000B4F2D2 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -124,6 +148,7 @@
|
|||
132417912D1319E800B4F2D2 /* Utils */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1308CFBA2D19843E004CD38C /* CustomPlayer */,
|
||||
132417922D1319E800B4F2D2 /* Miru */,
|
||||
132417942D1319E800B4F2D2 /* Extensions */,
|
||||
132417962D1319E800B4F2D2 /* History */,
|
||||
|
|
@ -328,10 +353,12 @@
|
|||
132417BB2D131A0600B4F2D2 /* SettingsIUView.swift in Sources */,
|
||||
132417C42D131A0600B4F2D2 /* AnimeInfoExtraction.swift in Sources */,
|
||||
132417B82D131A0600B4F2D2 /* SearchView.swift in Sources */,
|
||||
1308CFBC2D19844A004CD38C /* Double+Extension.swift in Sources */,
|
||||
132417D92D1328B900B4F2D2 /* VideoPlayerView.swift in Sources */,
|
||||
1324179F2D1319E800B4F2D2 /* Notification.swift in Sources */,
|
||||
132417BD2D131A0600B4F2D2 /* SettingsModuleView.swift in Sources */,
|
||||
132417BC2D131A0600B4F2D2 /* SettingsLogsView.swift in Sources */,
|
||||
1308CFC12D198466004CD38C /* CustomPlayer.swift in Sources */,
|
||||
132417A22D1319E800B4F2D2 /* ModulesManager.swift in Sources */,
|
||||
132417862D13198000B4F2D2 /* ContentView.swift in Sources */,
|
||||
132417C22D131A0600B4F2D2 /* LibraryView.swift in Sources */,
|
||||
|
|
@ -347,6 +374,7 @@
|
|||
132417C12D131A0600B4F2D2 /* LibraryManager.swift in Sources */,
|
||||
132417BA2D131A0600B4F2D2 /* SettingsAboutView.swift in Sources */,
|
||||
1324179E2D1319E800B4F2D2 /* MiruDataStruct.swift in Sources */,
|
||||
1308CFBE2D19844D004CD38C /* MusicProgressSlider.swift in Sources */,
|
||||
132417D52D13240200B4F2D2 /* EpisodeCell.swift in Sources */,
|
||||
132417A02D1319E800B4F2D2 /* HistoryManager.swift in Sources */,
|
||||
);
|
||||
|
|
|
|||
Binary file not shown.
30
Sora/Utils/CustomPlayer/Components/Double+Extension.swift
Normal file
30
Sora/Utils/CustomPlayer/Components/Double+Extension.swift
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// Double+Extension.swift
|
||||
// AppleMusicSlider
|
||||
//
|
||||
// Created by Pratik on 14/01/23.
|
||||
//
|
||||
// Thanks to pratikg29 for this code inside his open source project "https://github.com/pratikg29/Custom-Slider-Control?ref=iosexample.com"
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Double {
|
||||
func asTimeString(style: DateComponentsFormatter.UnitsStyle) -> String {
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.allowedUnits = [.minute, .second]
|
||||
formatter.unitsStyle = style
|
||||
formatter.zeroFormattingBehavior = .pad
|
||||
return formatter.string(from: self) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
extension BinaryFloatingPoint {
|
||||
func asTimeString(style: DateComponentsFormatter.UnitsStyle) -> String {
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.allowedUnits = [.minute, .second]
|
||||
formatter.unitsStyle = style
|
||||
formatter.zeroFormattingBehavior = .pad
|
||||
return formatter.string(from: TimeInterval(self)) ?? ""
|
||||
}
|
||||
}
|
||||
101
Sora/Utils/CustomPlayer/Components/MusicProgressSlider.swift
Normal file
101
Sora/Utils/CustomPlayer/Components/MusicProgressSlider.swift
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
//
|
||||
// MusicProgressSlider.swift
|
||||
// Custom Seekbar
|
||||
//
|
||||
// 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"
|
||||
//
|
||||
|
||||
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)), 0), alignment: .leading)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text(value.asTimeString(style: .positional))
|
||||
Spacer(minLength: 0)
|
||||
Text("-" + (inRange.upperBound - value).asTimeString(style: .positional))
|
||||
}
|
||||
.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: bounds.size.width, height: bounds.size.height, alignment: .center)
|
||||
.contentShape(Rectangle())
|
||||
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local)
|
||||
.updating($isActive) { value, state, transaction in
|
||||
state = true
|
||||
}
|
||||
.onChanged { gesture in
|
||||
localTempProgress = T(gesture.translation.width / bounds.size.width)
|
||||
value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound)
|
||||
}.onEnded { value 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)
|
||||
}
|
||||
|
||||
private var animation: Animation {
|
||||
if isActive {
|
||||
return .spring()
|
||||
} else {
|
||||
return .spring(response: 0.5, dampingFraction: 0.5, blendDuration: 0.6)
|
||||
}
|
||||
}
|
||||
|
||||
private func getPrgPercentage(_ value: T) -> T {
|
||||
let range = inRange.upperBound - inRange.lowerBound
|
||||
let correctedStartValue = value - inRange.lowerBound
|
||||
let percentage = correctedStartValue / range
|
||||
return percentage
|
||||
}
|
||||
|
||||
private func getPrgValue() -> T {
|
||||
return ((localRealProgress + localTempProgress) * (inRange.upperBound - inRange.lowerBound)) + inRange.lowerBound
|
||||
}
|
||||
}
|
||||
233
Sora/Utils/CustomPlayer/CustomPlayer.swift
Normal file
233
Sora/Utils/CustomPlayer/CustomPlayer.swift
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
//
|
||||
// ContentView.swift
|
||||
// test2
|
||||
//
|
||||
// Created by Francesco on 20/12/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
|
||||
struct CustomVideoPlayer: UIViewControllerRepresentable {
|
||||
let player: AVPlayer
|
||||
|
||||
func makeUIViewController(context: Context) -> AVPlayerViewController {
|
||||
let controller = CustomAVPlayerViewController()
|
||||
controller.player = player
|
||||
controller.showsPlaybackControls = false
|
||||
player.play()
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
|
||||
// yes? Like the plural of the famous american rapper ye? -IBHRAD
|
||||
}
|
||||
}
|
||||
|
||||
class CustomAVPlayerViewController: AVPlayerViewController {
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
if UserDefaults.standard.bool(forKey: "alwaysLandscape") {
|
||||
return .landscape
|
||||
} else {
|
||||
return .all
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomMediaPlayer: View {
|
||||
@State private var player: AVPlayer
|
||||
@State private var isPlaying = true
|
||||
@State private var currentTime: Double = 0.0
|
||||
@State private var duration: Double = 0.0
|
||||
@State private var showControls = false
|
||||
@State private var inactivityTimer: Timer?
|
||||
|
||||
init(urlString: String) {
|
||||
guard let url = URL(string: urlString) else {
|
||||
fatalError("Invalid URL string")
|
||||
}
|
||||
_player = State(initialValue: AVPlayer(url: url))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ZStack {
|
||||
CustomVideoPlayer(player: player)
|
||||
.onAppear {
|
||||
player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 600), queue: .main) { time in
|
||||
currentTime = time.seconds
|
||||
if let itemDuration = player.currentItem?.duration.seconds, itemDuration.isFinite && !itemDuration.isNaN {
|
||||
duration = itemDuration
|
||||
}
|
||||
}
|
||||
startUpdatingCurrentTime()
|
||||
}
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.overlay(
|
||||
Group {
|
||||
if showControls {
|
||||
Color.black.opacity(0.5)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
HStack(spacing: 20) {
|
||||
Button(action: {
|
||||
currentTime = max(currentTime - 10, 0)
|
||||
player.seek(to: CMTime(seconds: currentTime, preferredTimescale: 600))
|
||||
}) {
|
||||
Image(systemName: "gobackward.10")
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 25))
|
||||
.contentShape(Rectangle())
|
||||
.frame(width: 60, height: 60)
|
||||
|
||||
Button(action: {
|
||||
if isPlaying {
|
||||
player.pause()
|
||||
} else {
|
||||
player.play()
|
||||
}
|
||||
isPlaying.toggle()
|
||||
}) {
|
||||
Image(systemName: isPlaying ? "pause.fill" : "play.fill")
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 45))
|
||||
.contentShape(Rectangle())
|
||||
.frame(width: 80, height: 80)
|
||||
|
||||
Button(action: {
|
||||
currentTime = min(currentTime + 10, duration)
|
||||
player.seek(to: CMTime(seconds: currentTime, preferredTimescale: 600))
|
||||
}) {
|
||||
Image(systemName: "goforward.10")
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 25))
|
||||
.contentShape(Rectangle())
|
||||
.frame(width: 60, height: 60)
|
||||
}
|
||||
}
|
||||
},
|
||||
alignment: .center
|
||||
)
|
||||
.onTapGesture {
|
||||
showControls.toggle()
|
||||
}
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
if showControls {
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
Spacer()
|
||||
Menu {
|
||||
Menu("Playback Speed") {
|
||||
Button(action: {
|
||||
player.rate = 0.25
|
||||
if player.timeControlStatus != .playing {
|
||||
player.pause()
|
||||
}
|
||||
}) {
|
||||
Label("0.25", systemImage: "tortoise")
|
||||
}
|
||||
Button(action: {
|
||||
player.rate = 0.5
|
||||
if player.timeControlStatus != .playing {
|
||||
player.pause()
|
||||
}
|
||||
}) {
|
||||
Label("0.5", systemImage: "tortoise.fill")
|
||||
}
|
||||
Button(action: {
|
||||
player.rate = 0.75
|
||||
if player.timeControlStatus != .playing {
|
||||
player.pause()
|
||||
}
|
||||
}) {
|
||||
Label("0.75", systemImage: "hare")
|
||||
}
|
||||
Button(action: {
|
||||
player.rate = 1.0
|
||||
if player.timeControlStatus != .playing {
|
||||
player.pause()
|
||||
}
|
||||
}) {
|
||||
Label("1.0", systemImage: "hare.fill")
|
||||
}
|
||||
Button(action: {
|
||||
player.rate = 1.25
|
||||
if player.timeControlStatus != .playing {
|
||||
player.pause()
|
||||
}
|
||||
}) {
|
||||
Label("1.25", systemImage: "speedometer")
|
||||
}
|
||||
Button(action: {
|
||||
player.rate = 1.5
|
||||
if player.timeControlStatus != .playing {
|
||||
player.pause()
|
||||
}
|
||||
}) {
|
||||
Label("1.5", systemImage: "speedometer")
|
||||
}
|
||||
Button(action: {
|
||||
player.rate = 1.75
|
||||
if player.timeControlStatus != .playing {
|
||||
player.pause()
|
||||
}
|
||||
}) {
|
||||
Label("1.75", systemImage: "speedometer")
|
||||
}
|
||||
Button(action: {
|
||||
player.rate = 2.0
|
||||
if player.timeControlStatus != .playing {
|
||||
player.pause()
|
||||
}
|
||||
}) {
|
||||
Label("2.0", systemImage: "speedometer")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 15))
|
||||
}
|
||||
}
|
||||
.padding(.trailing, 10)
|
||||
|
||||
MusicProgressSlider(
|
||||
value: $currentTime,
|
||||
inRange: 0...duration,
|
||||
activeFillColor: .white,
|
||||
fillColor: .white.opacity(0.5),
|
||||
emptyColor: .white.opacity(0.3),
|
||||
height: 28,
|
||||
onEditingChanged: { editing in
|
||||
if !editing {
|
||||
player.seek(to: CMTime(seconds: currentTime, preferredTimescale: 600))
|
||||
}
|
||||
}
|
||||
)
|
||||
.frame(height: 45)
|
||||
.padding(.bottom, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
startUpdatingCurrentTime()
|
||||
}
|
||||
.onDisappear {
|
||||
player.pause()
|
||||
inactivityTimer?.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startUpdatingCurrentTime() {
|
||||
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
|
||||
currentTime = player.currentTime().seconds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ class VideoPlayerViewController: UIViewController {
|
|||
}
|
||||
|
||||
player = AVPlayer(url: url)
|
||||
playerViewController = AVPlayerViewController()
|
||||
playerViewController = NormalPlayer()
|
||||
playerViewController?.player = player
|
||||
addPeriodicTimeObserver(fullURL: fullUrl)
|
||||
|
||||
|
|
|
|||
|
|
@ -183,6 +183,19 @@ struct AnimeInfoView: View {
|
|||
openInExternalPlayer(scheme: scheme, url: streamUrl)
|
||||
Logger.shared.log("Opening external app with scheme: \(scheme)")
|
||||
return
|
||||
} else if externalPlayer == "Custom" {
|
||||
DispatchQueue.main.async {
|
||||
let customMediaPlayer = CustomMediaPlayer(urlString: streamUrl)
|
||||
let hostingController = UIHostingController(rootView: customMediaPlayer)
|
||||
hostingController.modalPresentationStyle = .fullScreen
|
||||
Logger.shared.log("Opening custom media player with url: \(streamUrl)")
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootVC = windowScene.windows.first?.rootViewController {
|
||||
rootVC.present(hostingController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
|
|
|
|||
|
|
@ -14,30 +14,46 @@ struct SettingsPlayerView: View {
|
|||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(header: Text("Player"), footer: Text("The ForceLandscape and HoldSpeed only work inside the default iOS player.")) {
|
||||
Section(header: Text("Player"), footer: Text("The ForceLandscape and HoldSpeed only work inside the default iOS player and custom player.")) {
|
||||
HStack {
|
||||
Text("Media Player")
|
||||
Spacer()
|
||||
Menu(externalPlayer) {
|
||||
Button("Default") {
|
||||
Button(action: {
|
||||
externalPlayer = "Default"
|
||||
}) {
|
||||
Label("Default", systemImage: externalPlayer == "Default" ? "checkmark" : "")
|
||||
}
|
||||
Button("VLC") {
|
||||
Button(action: {
|
||||
externalPlayer = "VLC"
|
||||
}) {
|
||||
Label("VLC", systemImage: externalPlayer == "VLC" ? "checkmark" : "")
|
||||
}
|
||||
Button("OutPlayer") {
|
||||
Button(action: {
|
||||
externalPlayer = "OutPlayer"
|
||||
}) {
|
||||
Label("OutPlayer", systemImage: externalPlayer == "OutPlayer" ? "checkmark" : "")
|
||||
}
|
||||
Button("Infuse") {
|
||||
Button(action: {
|
||||
externalPlayer = "Infuse"
|
||||
}) {
|
||||
Label("Infuse", systemImage: externalPlayer == "Infuse" ? "checkmark" : "")
|
||||
}
|
||||
Button("nPlayer") {
|
||||
Button(action: {
|
||||
externalPlayer = "nPlayer"
|
||||
}) {
|
||||
Label("nPlayer", systemImage: externalPlayer == "nPlayer" ? "checkmark" : "")
|
||||
}
|
||||
Button(action: {
|
||||
externalPlayer = "Custom"
|
||||
}) {
|
||||
Label("Custom", systemImage: externalPlayer == "Custom" ? "checkmark" : "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Toggle("Force Landscape", isOn: $isAlwaysLandscape)
|
||||
.tint(.accentColor)
|
||||
|
||||
HStack {
|
||||
Text("Hold Speed:")
|
||||
|
|
|
|||
Loading…
Reference in a new issue