@@ -9,6 +9,7 @@ import AVKit
99import Combine
1010import SwiftUI
1111
12+ @MainActor
1213@Observable public final class PlayEngine {
1314
1415 static let shared = PlayEngine ( )
@@ -145,7 +146,7 @@ import SwiftUI
145146
146147 private var timeObserver : Any ?
147148
148- init ( ) {
149+ private init ( ) {
149150 NowPlayable . shared. sessionStart ( )
150151 NowPlayable . shared. setupRemoteCommandHandlers ( playEngine: self )
151152
@@ -154,24 +155,24 @@ import SwiftUI
154155
155156 player. publisher ( for: \. timeControlStatus)
156157 . receive ( on: DispatchQueue . main)
157- . sink { status in
158- self . timeControlStatus = status
159- self . updateNowPlayingInfo ( )
158+ . sink { [ weak self ] status in
159+ self ? . timeControlStatus = status
160+ self ? . updateNowPlayingInfo ( )
160161 }
161162 . store ( in: & subs)
162163
163164 player. publisher ( for: \. rate)
164165 . receive ( on: DispatchQueue . main)
165- . sink { rate in
166- self . updateNowPlayingInfo ( )
166+ . sink { [ weak self ] rate in
167+ self ? . updateNowPlayingInfo ( )
167168 }
168169 . store ( in: & subs)
169170
170171 player. publisher ( for: \. isMuted)
171172 . removeDuplicates ( )
172173 . receive ( on: DispatchQueue . main)
173- . sink { isMuted in
174- self . _isMuted = isMuted
174+ . sink { [ weak self ] isMuted in
175+ self ? . _isMuted = isMuted
175176 }
176177 . store ( in: & subs)
177178
@@ -189,35 +190,27 @@ import SwiftUI
189190 /// Attempts to open file at url. If its not playable, returns false.
190191 /// - Parameter url: A URL to a local, remote, or HTTP Live Streaming media resource.
191192 /// - Returns: A Boolean value that indicates whether an asset contains playable content.
192- @MainActor
193193 @discardableResult func openFile( url: URL ) async -> Bool {
194194 if asset != nil {
195195 asset!. cancelLoading ( )
196196 }
197- asset = AVURLAsset ( url: url)
197+ let newAsset = AVURLAsset ( url: url)
198+ asset = newAsset
199+
198200 do {
199- let isPlayable = try await asset! . load ( . isPlayable)
201+ let isPlayable = try await newAsset . load ( . isPlayable)
200202 guard isPlayable else { return false }
201203
202- if let subtitleGroup = try await asset!. loadMediaSelectionGroup ( for: . legible) {
203- self . subtitleGroup = subtitleGroup
204- } else {
205- self . subtitleGroup = nil
206- }
207-
208- if let audioGroup = try await asset!. loadMediaSelectionGroup ( for: . audible) {
209- self . audioGroup = audioGroup
210- } else {
211- self . audioGroup = nil
212- }
204+ self . subtitleGroup = try ? await newAsset. loadMediaSelectionGroup ( for: . legible)
205+ self . audioGroup = try ? await newAsset. loadMediaSelectionGroup ( for: . audible)
213206 } catch {
214207 return false
215208 }
216209
217210 for sub in currentItemSubs { sub. cancel ( ) }
218211 currentItemSubs. removeAll ( )
219212
220- let playerItem = AVPlayerItem ( asset: asset! )
213+ let playerItem = AVPlayerItem ( asset: newAsset )
221214
222215 playerItem. publisher ( for: \. status)
223216 . removeDuplicates ( )
@@ -226,17 +219,18 @@ import SwiftUI
226219 guard let self else { return }
227220 switch status {
228221 case . readyToPlay:
229- isLoaded = true
230- isLocalFile = FileManager . default. fileExists (
231- atPath: url. path ( percentEncoded: false ) )
222+ self . isLoaded = true
223+ self . isLocalFile = FileManager . default. fileExists ( atPath: url. path)
232224 NowPlayable . shared. setNowPlayingMetadata (
233225 NowPlayableStaticMetadata (
234- assetURL: url, mediaType: videoSize == CGSize . zero ? . audio : . video,
235- title: url. lastPathComponent) )
236- updateNowPlayingInfo ( )
226+ assetURL: url,
227+ mediaType: self . videoSize == . zero ? . audio : . video,
228+ title: url. lastPathComponent
229+ ) )
230+ self . updateNowPlayingInfo ( )
237231 case . failed:
238- isLoaded = false
239- isLocalFile = false
232+ self . isLoaded = false
233+ self . isLocalFile = false
240234 NowPlayable . shared. sessionEnd ( )
241235 default :
242236 break
@@ -249,25 +243,16 @@ import SwiftUI
249243 . receive ( on: DispatchQueue . main)
250244 . sink { [ weak self] size in
251245 guard let self else { return }
252- videoSize = size
253- fitToVideoSize ( skipResize: WindowController . shared. isFullscreen)
246+ self . videoSize = size
247+ self . fitToVideoSize ( skipResize: WindowController . shared. isFullscreen)
254248 }
255249 . store ( in: & currentItemSubs)
256250
257251 player. replaceCurrentItem ( with: playerItem)
258252 player. play ( )
259253
260- if let subtitleGroup {
261- subtitle = subtitleGroup. options. first
262- } else {
263- subtitle = nil
264- }
265-
266- if let audioGroup {
267- audioTrack = audioGroup. options. first
268- } else {
269- audioTrack = nil
270- }
254+ self . subtitle = subtitleGroup? . options. first
255+ self . audioTrack = audioGroup? . options. first
271256
272257 return true
273258 }
@@ -420,17 +405,20 @@ import SwiftUI
420405
421406 private func addPeriodicTimeObserver( ) {
422407 let interval = CMTime ( seconds: 0.5 , preferredTimescale: CMTimeScale ( NSEC_PER_SEC) )
408+
423409 timeObserver = player. addPeriodicTimeObserver (
424410 forInterval: interval,
425411 queue: . main
426412 ) { [ weak self] time in
427- guard let self else { return }
428- _currentTime = time. seconds
413+ Task { @MainActor in
414+ guard let self else { return }
415+ self . _currentTime = time. seconds
429416
430- guard let duration = player. currentItem? . duration. seconds else { return }
431- guard !duration. isNaN && !duration. isInfinite else { return }
432- self . duration = duration
433- timeRemaining = duration - _currentTime
417+ guard let duration = self . player. currentItem? . duration. seconds else { return }
418+ guard !duration. isNaN && !duration. isInfinite else { return }
419+ self . duration = duration
420+ self . timeRemaining = duration - self . _currentTime
421+ }
434422 }
435423 }
436424
0 commit comments