구간반복 기능 추가
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
|
import Kingfisher
|
||||||
|
|
||||||
final class ContentPlayerPlayManager: NSObject, ObservableObject {
|
final class ContentPlayerPlayManager: NSObject, ObservableObject {
|
||||||
|
enum LoopState {
|
||||||
|
case idle
|
||||||
|
case waitingForEnd(start: CMTime)
|
||||||
|
case looping(start: CMTime, end: CMTime)
|
||||||
|
}
|
||||||
|
|
||||||
static let shared = ContentPlayerPlayManager()
|
static let shared = ContentPlayerPlayManager()
|
||||||
|
|
||||||
private let repository = ContentGenerateUrlRepository()
|
private let repository = ContentGenerateUrlRepository()
|
||||||
|
@ -35,6 +41,9 @@ final class ContentPlayerPlayManager: NSObject, ObservableObject {
|
||||||
@Published private(set) var duration: Double = 0.0
|
@Published private(set) var duration: Double = 0.0
|
||||||
@Published var isEditing = false
|
@Published var isEditing = false
|
||||||
|
|
||||||
|
@Published private(set) var loopState: LoopState = .idle
|
||||||
|
@Published private var timeObserver: Any?
|
||||||
|
|
||||||
var player: AVPlayer?
|
var player: AVPlayer?
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
@ -70,6 +79,8 @@ final class ContentPlayerPlayManager: NSObject, ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupPlayer(with url: URL) {
|
private func setupPlayer(with url: URL) {
|
||||||
|
stopLooping()
|
||||||
|
loopState = .idle
|
||||||
// 기존 playerItem 관련 Combine 구독 해제
|
// 기존 playerItem 관련 Combine 구독 해제
|
||||||
cancellables.removeAll()
|
cancellables.removeAll()
|
||||||
|
|
||||||
|
@ -152,6 +163,8 @@ final class ContentPlayerPlayManager: NSObject, ObservableObject {
|
||||||
|
|
||||||
func playNextContent() {
|
func playNextContent() {
|
||||||
stop()
|
stop()
|
||||||
|
stopLooping()
|
||||||
|
loopState = .idle
|
||||||
|
|
||||||
if let content = playlistManager?.moveToNext() {
|
if let content = playlistManager?.moveToNext() {
|
||||||
generateUrl(contentId: content.id) { [unowned self] url in
|
generateUrl(contentId: content.id) { [unowned self] url in
|
||||||
|
@ -164,6 +177,8 @@ final class ContentPlayerPlayManager: NSObject, ObservableObject {
|
||||||
|
|
||||||
func playPreviousContent() {
|
func playPreviousContent() {
|
||||||
stop()
|
stop()
|
||||||
|
stopLooping()
|
||||||
|
loopState = .idle
|
||||||
|
|
||||||
if let content = playlistManager?.moveToPrevious() {
|
if let content = playlistManager?.moveToPrevious() {
|
||||||
generateUrl(contentId: content.id) { [unowned self] url in
|
generateUrl(contentId: content.id) { [unowned self] url in
|
||||||
|
@ -213,6 +228,8 @@ final class ContentPlayerPlayManager: NSObject, ObservableObject {
|
||||||
|
|
||||||
func resetPlayer() {
|
func resetPlayer() {
|
||||||
stop()
|
stop()
|
||||||
|
stopLooping()
|
||||||
|
loopState = .idle
|
||||||
duration = 0
|
duration = 0
|
||||||
isShowingMiniPlayer = false
|
isShowingMiniPlayer = false
|
||||||
|
|
||||||
|
@ -227,6 +244,8 @@ final class ContentPlayerPlayManager: NSObject, ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
func seek(to time: Double) {
|
func seek(to time: Double) {
|
||||||
|
stopLooping()
|
||||||
|
loopState = .idle
|
||||||
let cmTime = CMTime(seconds: time, preferredTimescale: 600)
|
let cmTime = CMTime(seconds: time, preferredTimescale: 600)
|
||||||
player?.seek(to: cmTime)
|
player?.seek(to: cmTime)
|
||||||
}
|
}
|
||||||
|
@ -244,6 +263,41 @@ final class ContentPlayerPlayManager: NSObject, ObservableObject {
|
||||||
seek(to: min(newTimeInSeconds, durationInSeconds))
|
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) {
|
private func generateUrl(contentId: Int, onSuccess: @escaping (String) -> Void, onFailure: @escaping () -> Void) {
|
||||||
if contentId < 0 {
|
if contentId < 0 {
|
||||||
onFailure()
|
onFailure()
|
||||||
|
|
|
@ -163,6 +163,21 @@ struct ContentPlayerView: View {
|
||||||
.padding(.vertical, 21)
|
.padding(.vertical, 21)
|
||||||
|
|
||||||
HStack(spacing: 0) {
|
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()
|
Spacer()
|
||||||
|
|
||||||
Image("ic_playlist")
|
Image("ic_playlist")
|
||||||
|
|
Loading…
Reference in New Issue