구간반복 기능 추가
This commit is contained in:
parent
711ffbcb83
commit
592b014941
21
SodaLive/Resources/Assets.xcassets/ic_loop_segment_active.imageset/Contents.json
vendored
Normal file
21
SodaLive/Resources/Assets.xcassets/ic_loop_segment_active.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "ic_loop_segment_active.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_loop_segment_active.imageset/ic_loop_segment_active.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/ic_loop_segment_active.imageset/ic_loop_segment_active.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
21
SodaLive/Resources/Assets.xcassets/ic_loop_segment_idle.imageset/Contents.json
vendored
Normal file
21
SodaLive/Resources/Assets.xcassets/ic_loop_segment_idle.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "ic_loop_segment_idle.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_loop_segment_idle.imageset/ic_loop_segment_idle.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/ic_loop_segment_idle.imageset/ic_loop_segment_idle.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
21
SodaLive/Resources/Assets.xcassets/ic_loop_segment_start_set.imageset/Contents.json
vendored
Normal file
21
SodaLive/Resources/Assets.xcassets/ic_loop_segment_start_set.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "ic_loop_segment_start_set.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_loop_segment_start_set.imageset/ic_loop_segment_start_set.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/ic_loop_segment_start_set.imageset/ic_loop_segment_start_set.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
|
@ -13,6 +13,12 @@ import Combine
|
|||
import Kingfisher
|
||||
|
||||
final class ContentPlayerPlayManager: NSObject, ObservableObject {
|
||||
enum LoopState {
|
||||
case idle
|
||||
case waitingForEnd(start: CMTime)
|
||||
case looping(start: CMTime, end: CMTime)
|
||||
}
|
||||
|
||||
static let shared = ContentPlayerPlayManager()
|
||||
|
||||
private let repository = ContentGenerateUrlRepository()
|
||||
|
@ -23,8 +29,8 @@ final class ContentPlayerPlayManager: NSObject, ObservableObject {
|
|||
@Published var coverImageUrl = ""
|
||||
@Published var creatorProfileUrl = ""
|
||||
|
||||
@Published private (set) var isShowingMiniPlayer = false
|
||||
@Published private (set) var isPlaying = false
|
||||
@Published private(set) var isShowingMiniPlayer = false
|
||||
@Published private(set) var isPlaying = false
|
||||
|
||||
@Published var isLoading = false
|
||||
@Published var errorMessage = ""
|
||||
|
@ -32,9 +38,12 @@ final class ContentPlayerPlayManager: NSObject, ObservableObject {
|
|||
@Published var isShowPlaylist = false
|
||||
@Published var playlist: [AudioContentPlaylistContent] = []
|
||||
@Published var currentTime: Double = 0.0
|
||||
@Published private (set) var duration: Double = 0.0
|
||||
@Published private(set) var duration: Double = 0.0
|
||||
@Published var isEditing = false
|
||||
|
||||
@Published private(set) var loopState: LoopState = .idle
|
||||
@Published private var timeObserver: Any?
|
||||
|
||||
var player: AVPlayer?
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
|
@ -70,6 +79,8 @@ final class ContentPlayerPlayManager: NSObject, ObservableObject {
|
|||
}
|
||||
|
||||
private func setupPlayer(with url: URL) {
|
||||
stopLooping()
|
||||
loopState = .idle
|
||||
// 기존 playerItem 관련 Combine 구독 해제
|
||||
cancellables.removeAll()
|
||||
|
||||
|
@ -152,6 +163,8 @@ final class ContentPlayerPlayManager: NSObject, ObservableObject {
|
|||
|
||||
func playNextContent() {
|
||||
stop()
|
||||
stopLooping()
|
||||
loopState = .idle
|
||||
|
||||
if let content = playlistManager?.moveToNext() {
|
||||
generateUrl(contentId: content.id) { [unowned self] url in
|
||||
|
@ -164,6 +177,8 @@ final class ContentPlayerPlayManager: NSObject, ObservableObject {
|
|||
|
||||
func playPreviousContent() {
|
||||
stop()
|
||||
stopLooping()
|
||||
loopState = .idle
|
||||
|
||||
if let content = playlistManager?.moveToPrevious() {
|
||||
generateUrl(contentId: content.id) { [unowned self] url in
|
||||
|
@ -213,6 +228,8 @@ final class ContentPlayerPlayManager: NSObject, ObservableObject {
|
|||
|
||||
func resetPlayer() {
|
||||
stop()
|
||||
stopLooping()
|
||||
loopState = .idle
|
||||
duration = 0
|
||||
isShowingMiniPlayer = false
|
||||
|
||||
|
@ -227,6 +244,8 @@ final class ContentPlayerPlayManager: NSObject, ObservableObject {
|
|||
}
|
||||
|
||||
func seek(to time: Double) {
|
||||
stopLooping()
|
||||
loopState = .idle
|
||||
let cmTime = CMTime(seconds: time, preferredTimescale: 600)
|
||||
player?.seek(to: cmTime)
|
||||
}
|
||||
|
@ -244,6 +263,41 @@ final class ContentPlayerPlayManager: NSObject, ObservableObject {
|
|||
seek(to: min(newTimeInSeconds, durationInSeconds))
|
||||
}
|
||||
|
||||
func toggleLoop() {
|
||||
guard let currentTime = player?.currentTime() else { return }
|
||||
|
||||
switch loopState {
|
||||
case .idle:
|
||||
loopState = .waitingForEnd(start: currentTime)
|
||||
|
||||
case .waitingForEnd(let start):
|
||||
let end = currentTime
|
||||
loopState = .looping(start: start, end: end)
|
||||
startLooping(from: start, to: end)
|
||||
|
||||
case .looping:
|
||||
stopLooping()
|
||||
loopState = .idle
|
||||
}
|
||||
}
|
||||
|
||||
private func startLooping(from start: CMTime, to end: CMTime) {
|
||||
let interval = CMTime(seconds: 0.1, preferredTimescale: 600)
|
||||
|
||||
timeObserver = player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] currentTime in
|
||||
if currentTime >= end {
|
||||
self?.player?.seek(to: start, toleranceBefore: .zero, toleranceAfter: .zero)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func stopLooping() {
|
||||
if let observer = timeObserver {
|
||||
player?.removeTimeObserver(observer)
|
||||
timeObserver = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func generateUrl(contentId: Int, onSuccess: @escaping (String) -> Void, onFailure: @escaping () -> Void) {
|
||||
if contentId < 0 {
|
||||
onFailure()
|
||||
|
|
|
@ -163,6 +163,21 @@ struct ContentPlayerView: View {
|
|||
.padding(.vertical, 21)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Image({
|
||||
switch playerManager.loopState {
|
||||
case .waitingForEnd:
|
||||
"ic_loop_segment_start_set"
|
||||
|
||||
case .looping:
|
||||
"ic_loop_segment_active"
|
||||
|
||||
default:
|
||||
"ic_loop_segment_idle"
|
||||
}
|
||||
}())
|
||||
.padding(5)
|
||||
.onTapGesture { playerManager.toggleLoop() }
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("ic_playlist")
|
||||
|
|
Loading…
Reference in New Issue