diff --git a/SodaLive/Sources/App/SodaLiveApp.swift b/SodaLive/Sources/App/SodaLiveApp.swift index 4fc9c53..fc42f66 100644 --- a/SodaLive/Sources/App/SodaLiveApp.swift +++ b/SodaLive/Sources/App/SodaLiveApp.swift @@ -57,6 +57,9 @@ struct SodaLiveApp: App { var body: some Scene { WindowGroup { ContentView() + .onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in + CreatorCommunityMediaPlayerManager.shared.pauseContent() + } .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in UIApplication.shared.applicationIconBadgeNumber = 0 diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllItemView.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllItemView.swift index cb7e7fd..9a7cfaf 100644 --- a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllItemView.swift +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllItemView.swift @@ -21,6 +21,9 @@ struct CreatorCommunityAllItemView: View { @State var likeCount = 0 @State private var textHeight: CGFloat = .zero + @StateObject var playManager = CreatorCommunityMediaPlayerManager.shared + @StateObject var contentPlayManager = ContentPlayManager.shared + init( item: GetCommunityPostListResponse, onClickLike: @escaping () -> Void, @@ -88,10 +91,20 @@ struct CreatorCommunityAllItemView: View { if item.price <= 0 || item.existOrdered { if let imageUrl = item.imageUrl { - KFImage(URL(string: imageUrl)) - .resizable() - .frame(maxWidth: .infinity) - .scaledToFit() + ZStack { + KFImage(URL(string: imageUrl)) + .resizable() + .frame(maxWidth: .infinity) + .scaledToFit() + + if let audioUrl = item.audioUrl { + Image(playManager.isPlaying && playManager.currentPlayingContentId == item.postId ? "btn_audio_content_pause" : "btn_audio_content_play") + .onTapGesture { + contentPlayManager.pauseAudio() + playManager.toggleContent(item: CreatorCommunityContentItem(contentId: item.postId, url: audioUrl)) + } + } + } } HStack(spacing: 8) { @@ -155,6 +168,7 @@ struct CreatorCommunityAllItemView_Previews: PreviewProvider { creatorNickname: "민하나", creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png", imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", + audioUrl: nil, content: "너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!", price: 10, date: "3일전", diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift index 5fb361e..36ba893 100644 --- a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift @@ -12,6 +12,7 @@ struct CreatorCommunityAllView: View { let creatorId: Int @StateObject var viewModel = CreatorCommunityAllViewModel() + @StateObject var playerManager = CreatorCommunityMediaPlayerManager.shared var body: some View { GeometryReader { proxy in @@ -135,6 +136,10 @@ struct CreatorCommunityAllView: View { viewModel.purchaseCommunityPost() } } + + if playerManager.isLoading { + LoadingView() + } } } .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { @@ -145,7 +150,24 @@ struct CreatorCommunityAllView: View { .padding(.vertical, 13.3) .frame(width: screenSize().width - 66.7, alignment: .center) .font(.custom(Font.medium.rawValue, size: 12)) - .background(Color(hex: "9970ff")) + .background(Color.button) + .foregroundColor(Color.white) + .multilineTextAlignment(.center) + .cornerRadius(20) + .padding(.top, 66.7) + Spacer() + } + } + } + .popup(isPresented: $playerManager.isShowPopup, type: .toast, position: .top, autohideIn: 2) { + GeometryReader { geo in + HStack { + Spacer() + Text(playerManager.errorMessage) + .padding(.vertical, 13.3) + .frame(width: screenSize().width - 66.7, alignment: .center) + .font(.custom(Font.medium.rawValue, size: 12)) + .background(Color.button) .foregroundColor(Color.white) .multilineTextAlignment(.center) .cornerRadius(20) @@ -158,6 +180,9 @@ struct CreatorCommunityAllView: View { viewModel.creatorId = creatorId viewModel.getCommunityPostList() } + .onDisappear { + CreatorCommunityMediaPlayerManager.shared.stopContent() + } } } diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/Player/CreatorCommunityMediaPlayerManager.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/Player/CreatorCommunityMediaPlayerManager.swift new file mode 100644 index 0000000..af7f128 --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/Player/CreatorCommunityMediaPlayerManager.swift @@ -0,0 +1,130 @@ +// +// CreatorCommunityMediaPlayerManager.swift +// SodaLive +// +// Created by klaus on 8/7/24. +// + +import Foundation +import AVKit +import MediaPlayer + +final class CreatorCommunityMediaPlayerManager: NSObject, ObservableObject { + static let shared = CreatorCommunityMediaPlayerManager() + + @Published private (set) var currentPlayingContentId: Int = 0 + @Published private (set) var isPlaying = false + + @Published var isLoading = false + @Published var errorMessage = "" + @Published var isShowPopup = false + + private var player: AVAudioPlayer! + + private func playContent(item: CreatorCommunityContentItem) { + if item.contentId <= 0 { + return + } + + if (currentPlayingContentId == item.contentId && !isPlaying) { + resumeContent() + return + } + + stopContent() + currentPlayingContentId = item.contentId + + guard let url = URL(string: item.url) else { + showError() + return + } + + isLoading = true + URLSession.shared.dataTask(with: url) { [unowned self] data, response, error in + guard let audioData = data else { + self.isLoading = false + return + } + + do { + let audioSession = AVAudioSession.sharedInstance() + try audioSession.setCategory(.playback, mode: .default) + try audioSession.setActive(true) + + self.player = try AVAudioPlayer(data: audioData) + + DispatchQueue.main.async { + self.player?.volume = 1 + self.player?.delegate = self + self.player?.prepareToPlay() + + self.player?.play() + self.isPlaying = self.player.isPlaying + } + } catch { + DispatchQueue.main.async { + self.showError() + } + } + + DispatchQueue.main.async { + self.isLoading = false + } + }.resume() + } + + private func resumeContent() { + if let player = player { + player.play() + isPlaying = player.isPlaying + } + } + + private func showError() { + self.errorMessage = "오류가 발생했습니다. 다시 시도해 주세요." + self.isShowPopup = true + } +} + +extension CreatorCommunityMediaPlayerManager { + func toggleContent(item: CreatorCommunityContentItem) { + if currentPlayingContentId == item.contentId { + if let player = player, player.isPlaying { + pauseContent() + } else { + resumeContent() + } + } else { + playContent(item: item) + } + } + + func pauseContent() { + if let player = player { + player.pause() + isPlaying = player.isPlaying + } + } + + func stopContent() { + if let player = player { + player.stop() + player.currentTime = 0 + } + + isPlaying = false + currentPlayingContentId = 0 + } +} + +extension CreatorCommunityMediaPlayerManager: AVAudioPlayerDelegate { + func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + stopContent() + } +} + + +struct CreatorCommunityContentItem { + let contentId: Int + let url: String +} diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityItemView.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityItemView.swift index be99c2f..9afad2d 100644 --- a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityItemView.swift +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityItemView.swift @@ -91,6 +91,7 @@ struct CreatorCommunityItemView_Previews: PreviewProvider { creatorNickname: "민하나", creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png", imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", + audioUrl: nil, content: "안녕하세요", price: 10, date: "3일전", diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/GetCommunityPostListResponse.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/GetCommunityPostListResponse.swift index 359262e..6443ac7 100644 --- a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/GetCommunityPostListResponse.swift +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/GetCommunityPostListResponse.swift @@ -11,6 +11,7 @@ struct GetCommunityPostListResponse: Decodable { let creatorNickname: String let creatorProfileUrl: String let imageUrl: String? + let audioUrl: String? let content: String let price: Int let date: String diff --git a/SodaLive/Sources/Live/SectionCommunityPostView.swift b/SodaLive/Sources/Live/SectionCommunityPostView.swift index e9b1776..d809a7c 100644 --- a/SodaLive/Sources/Live/SectionCommunityPostView.swift +++ b/SodaLive/Sources/Live/SectionCommunityPostView.swift @@ -37,6 +37,7 @@ struct SectionCommunityPostView_Previews: PreviewProvider { creatorNickname: "민하나", creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png", imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", + audioUrl: nil, content: "라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!", price: 10, date: "3일전", @@ -54,6 +55,7 @@ struct SectionCommunityPostView_Previews: PreviewProvider { creatorNickname: "닉네임2", creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png", imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", + audioUrl: nil, content: "너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!", price: 10, date: "3일전",