구간반복 기능 추가

This commit is contained in:
Yu Sung 2025-04-01 11:51:24 +09:00
parent 711ffbcb83
commit 592b014941
8 changed files with 135 additions and 3 deletions

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -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()

View File

@ -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")