Skip to content

Latest commit

ย 

History

History
458 lines (380 loc) ยท 12.4 KB

File metadata and controls

458 lines (380 loc) ยท 12.4 KB

SharePlay AI Reference

FaceTime ํ•จ๊ป˜ ๋ณด๊ธฐ ๊ฒฝํ—˜ ๊ตฌํ˜„ ๊ฐ€์ด๋“œ. ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ณ  SharePlay ์ฝ”๋“œ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฐœ์š”

SharePlay๋Š” FaceTime ํ†ตํ™” ์ค‘ ์ฝ˜ํ…์ธ ๋ฅผ ํ•จ๊ป˜ ๋ณด๊ณ  ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. GroupActivities ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ํ†ตํ•ด ์•ฑ ์ƒํƒœ๋ฅผ ์‹ค์‹œ๊ฐ„ ๋™๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.

ํ•„์ˆ˜ Import

import GroupActivities

ํ”„๋กœ์ ํŠธ ์„ค์ •

  1. Capabilities: Group Activities ์ถ”๊ฐ€
  2. Info.plist:
<key>NSSupportsLiveActivities</key>
<true/>

ํ•ต์‹ฌ ๊ตฌ์„ฑ์š”์†Œ

1. GroupActivity ์ •์˜

struct WatchTogetherActivity: GroupActivity {
    // ์ฝ˜ํ…์ธ  ์ •๋ณด
    let movie: Movie
    
    // ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ
    var metadata: GroupActivityMetadata {
        var metadata = GroupActivityMetadata()
        metadata.title = movie.title
        metadata.subtitle = "ํ•จ๊ป˜ ๋ณด๊ธฐ"
        metadata.previewImage = movie.thumbnailImage
        metadata.type = .watchTogether
        return metadata
    }
}

struct Movie: Codable, Hashable {
    let id: String
    let title: String
    let url: URL
    var thumbnailImage: CGImage? { nil }
}

2. ํ™œ๋™ ์‹œ์ž‘

func startSharePlay(movie: Movie) async {
    let activity = WatchTogetherActivity(movie: movie)
    
    switch await activity.prepareForActivation() {
    case .activationPreferred:
        do {
            _ = try await activity.activate()
        } catch {
            print("ํ™œ์„ฑํ™” ์‹คํŒจ: \(error)")
        }
    case .activationDisabled:
        // SharePlay ๋น„ํ™œ์„ฑํ™”๋จ
        print("SharePlay๊ฐ€ ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค")
    case .cancelled:
        // ์‚ฌ์šฉ์ž ์ทจ์†Œ
        break
    @unknown default:
        break
    }
}

3. ์„ธ์…˜ ๊ด€๋ฆฌ

@Observable
class SharePlayManager {
    var session: GroupSession<WatchTogetherActivity>?
    var messenger: GroupSessionMessenger?
    var isSharePlayActive = false
    
    func configureSession() async {
        for await session in WatchTogetherActivity.sessions() {
            self.session = session
            self.isSharePlayActive = true
            
            // ๋ฉ”์‹ ์ € ์„ค์ •
            messenger = GroupSessionMessenger(session: session)
            
            // ์„ธ์…˜ ์ƒํƒœ ๊ด€์ฐฐ
            Task {
                for await state in session.$state.values {
                    if case .invalidated = state {
                        self.isSharePlayActive = false
                        self.session = nil
                    }
                }
            }
            
            // ์„ธ์…˜ ์ฐธ๊ฐ€
            session.join()
        }
    }
}

์ „์ฒด ์ž‘๋™ ์˜ˆ์ œ

import SwiftUI
import GroupActivities
import AVKit

// MARK: - Activity ์ •์˜
struct MovieWatchActivity: GroupActivity {
    let movieID: String
    let movieTitle: String
    
    var metadata: GroupActivityMetadata {
        var metadata = GroupActivityMetadata()
        metadata.title = movieTitle
        metadata.subtitle = "ํ•จ๊ป˜ ์˜ํ™” ๋ณด๊ธฐ"
        metadata.type = .watchTogether
        return metadata
    }
}

// ๋™๊ธฐํ™”ํ•  ๋ฉ”์‹œ์ง€
struct PlaybackState: Codable {
    let isPlaying: Bool
    let currentTime: TimeInterval
}

// MARK: - SharePlay Manager
@Observable
class MovieSharePlayManager {
    var session: GroupSession<MovieWatchActivity>?
    var messenger: GroupSessionMessenger?
    var isSharePlayActive = false
    var participants: Set<Participant> = []
    
    private var tasks = Set<Task<Void, Never>>()
    
    init() {
        Task {
            await observeSessions()
        }
    }
    
    private func observeSessions() async {
        for await session in MovieWatchActivity.sessions() {
            cleanUp()
            
            self.session = session
            
            // ๋ฉ”์‹ ์ € ์„ค์ •
            let messenger = GroupSessionMessenger(session: session)
            self.messenger = messenger
            
            // ์ฐธ๊ฐ€์ž ๊ด€์ฐฐ
            let participantTask = Task {
                for await participants in session.$activeParticipants.values {
                    await MainActor.run {
                        self.participants = participants
                    }
                }
            }
            tasks.insert(participantTask)
            
            // ์„ธ์…˜ ์ƒํƒœ ๊ด€์ฐฐ
            let stateTask = Task {
                for await state in session.$state.values {
                    await MainActor.run {
                        switch state {
                        case .joined:
                            self.isSharePlayActive = true
                        case .invalidated:
                            self.isSharePlayActive = false
                            self.cleanUp()
                        default:
                            break
                        }
                    }
                }
            }
            tasks.insert(stateTask)
            
            // ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ 
            let messageTask = Task {
                for await (message, _) in messenger.messages(of: PlaybackState.self) {
                    await handlePlaybackState(message)
                }
            }
            tasks.insert(messageTask)
            
            // ์„ธ์…˜ ์ฐธ๊ฐ€
            session.join()
        }
    }
    
    func startSharePlay(movieID: String, title: String) async {
        let activity = MovieWatchActivity(movieID: movieID, movieTitle: title)
        
        switch await activity.prepareForActivation() {
        case .activationPreferred:
            do {
                _ = try await activity.activate()
            } catch {
                print("SharePlay ํ™œ์„ฑํ™” ์‹คํŒจ: \(error)")
            }
        case .activationDisabled:
            print("SharePlay๊ฐ€ ๋น„ํ™œ์„ฑํ™”๋จ")
        case .cancelled:
            break
        @unknown default:
            break
        }
    }
    
    func sendPlaybackState(isPlaying: Bool, currentTime: TimeInterval) {
        guard let messenger else { return }
        
        let state = PlaybackState(isPlaying: isPlaying, currentTime: currentTime)
        
        Task {
            do {
                try await messenger.send(state)
            } catch {
                print("๋ฉ”์‹œ์ง€ ์ „์†ก ์‹คํŒจ: \(error)")
            }
        }
    }
    
    @MainActor
    private func handlePlaybackState(_ state: PlaybackState) async {
        // ViewModel์—์„œ ์žฌ์ƒ ์ƒํƒœ ๋™๊ธฐํ™”
        NotificationCenter.default.post(
            name: .sharePlayStateReceived,
            object: state
        )
    }
    
    func endSession() {
        session?.end()
        cleanUp()
    }
    
    private func cleanUp() {
        tasks.forEach { $0.cancel() }
        tasks.removeAll()
        session = nil
        messenger = nil
        participants = []
    }
}

extension Notification.Name {
    static let sharePlayStateReceived = Notification.Name("sharePlayStateReceived")
}

// MARK: - Video Player ViewModel
@Observable
class VideoPlayerViewModel {
    let movie: Movie
    var isPlaying = false
    var currentTime: TimeInterval = 0
    var sharePlayManager: MovieSharePlayManager
    
    init(movie: Movie, sharePlayManager: MovieSharePlayManager) {
        self.movie = movie
        self.sharePlayManager = sharePlayManager
        
        observeSharePlay()
    }
    
    private func observeSharePlay() {
        NotificationCenter.default.addObserver(
            forName: .sharePlayStateReceived,
            object: nil,
            queue: .main
        ) { [weak self] notification in
            guard let state = notification.object as? PlaybackState else { return }
            self?.syncPlayback(state)
        }
    }
    
    private func syncPlayback(_ state: PlaybackState) {
        isPlaying = state.isPlaying
        currentTime = state.currentTime
    }
    
    func togglePlayPause() {
        isPlaying.toggle()
        
        if sharePlayManager.isSharePlayActive {
            sharePlayManager.sendPlaybackState(isPlaying: isPlaying, currentTime: currentTime)
        }
    }
    
    func seek(to time: TimeInterval) {
        currentTime = time
        
        if sharePlayManager.isSharePlayActive {
            sharePlayManager.sendPlaybackState(isPlaying: isPlaying, currentTime: currentTime)
        }
    }
}

struct Movie: Identifiable {
    let id: String
    let title: String
    let url: URL
}

// MARK: - Views
struct MoviePlayerView: View {
    let movie: Movie
    @State private var sharePlayManager = MovieSharePlayManager()
    @State private var viewModel: VideoPlayerViewModel?
    
    var body: some View {
        VStack {
            // ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด (์‹ค์ œ๋กœ๋Š” AVPlayer ์‚ฌ์šฉ)
            Rectangle()
                .fill(.black)
                .aspectRatio(16/9, contentMode: .fit)
                .overlay {
                    Image(systemName: viewModel?.isPlaying == true ? "pause.fill" : "play.fill")
                        .font(.system(size: 50))
                        .foregroundStyle(.white.opacity(0.8))
                }
                .onTapGesture {
                    viewModel?.togglePlayPause()
                }
            
            // ์ปจํŠธ๋กค
            HStack(spacing: 20) {
                // ์žฌ์ƒ/์ผ์‹œ์ •์ง€
                Button {
                    viewModel?.togglePlayPause()
                } label: {
                    Image(systemName: viewModel?.isPlaying == true ? "pause.circle.fill" : "play.circle.fill")
                        .font(.system(size: 44))
                }
                
                Spacer()
                
                // SharePlay ์ƒํƒœ
                if sharePlayManager.isSharePlayActive {
                    HStack {
                        Image(systemName: "shareplay")
                        Text("\(sharePlayManager.participants.count)๋ช… ์‹œ์ฒญ ์ค‘")
                    }
                    .font(.caption)
                    .foregroundStyle(.green)
                }
                
                // SharePlay ๋ฒ„ํŠผ
                ShareLink(
                    item: movie.url,
                    preview: SharePreview(movie.title)
                ) {
                    Image(systemName: "shareplay")
                        .font(.title2)
                }
            }
            .padding()
            
            // SharePlay ์‹œ์ž‘ ๋ฒ„ํŠผ
            if !sharePlayManager.isSharePlayActive {
                Button {
                    Task {
                        await sharePlayManager.startSharePlay(
                            movieID: movie.id,
                            title: movie.title
                        )
                    }
                } label: {
                    Label("SharePlay ์‹œ์ž‘", systemImage: "shareplay")
                }
                .buttonStyle(.borderedProminent)
            }
        }
        .onAppear {
            viewModel = VideoPlayerViewModel(movie: movie, sharePlayManager: sharePlayManager)
        }
        .onDisappear {
            sharePlayManager.endSession()
        }
    }
}

๊ณ ๊ธ‰ ํŒจํ„ด

1. AVPlayer ๋™๊ธฐํ™”

// CoordinationManager ์‚ฌ์šฉ (iOS 15+)
func configureAVPlayerSync() {
    guard let session else { return }
    
    let coordinator = AVPlaybackCoordinator()
    session.coordinator = coordinator
    
    // AVPlayer์™€ ์—ฐ๊ฒฐ
    player.playbackCoordinator.coordinateWithSession(session)
}

2. ์ปค์Šคํ…€ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”

// ๊ฒŒ์ž„ ์ƒํƒœ ๋™๊ธฐํ™”
struct GameState: Codable {
    let playerPositions: [String: CGPoint]
    let score: [String: Int]
    let currentTurn: String
}

// ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ์ „์†ก (์ˆœ์„œ ๋ณด์žฅ)
try await messenger.send(gameState, to: .all, deliveryMode: .reliable)

// ๋น ๋ฅธ ์ „์†ก (์‹ค์‹œ๊ฐ„, ์ˆœ์„œ ๋ฏธ๋ณด์žฅ)
try await messenger.send(position, to: .all, deliveryMode: .unreliable)

3. ์ฐธ๊ฐ€์ž๋ณ„ ๋ฉ”์‹œ์ง€

// ํŠน์ • ์ฐธ๊ฐ€์ž์—๊ฒŒ๋งŒ ์ „์†ก
if let host = participants.first(where: { $0.isLocal == false }) {
    try await messenger.send(message, to: .only(host))
}

์ฃผ์˜์‚ฌํ•ญ

  1. FaceTime ํ•„์š”

    • SharePlay๋Š” FaceTime ํ†ตํ™” ์ค‘์—๋งŒ ๋™์ž‘
    • ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์—์„œ ์ œํ•œ์  ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ
  2. ๋„คํŠธ์›Œํฌ ์ง€์—ฐ

    • ์ƒํƒœ ๋™๊ธฐํ™”์— ์ง€์—ฐ ๋ฐœ์ƒ ๊ฐ€๋Šฅ
    • UI์— ๋ฒ„ํผ๋ง ํ‘œ์‹œ ๊ถŒ์žฅ
  3. ์„ธ์…˜ ์ •๋ฆฌ

    • ํ™”๋ฉด ์ดํƒˆ ์‹œ session.leave() ๋˜๋Š” session.end() ํ˜ธ์ถœ
    • ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€
  4. ์ฐธ๊ฐ€์ž ์ œํ•œ

    • FaceTime ๊ทธ๋ฃน ํ†ตํ™” ์ตœ๋Œ€ 32๋ช…
    • ์•ฑ๋ณ„๋กœ ์ ์ ˆํ•œ ์ œํ•œ ์„ค์ • ๊ถŒ์žฅ