{"id":83790,"date":"2026-10-27T12:06:13","date_gmt":"2026-10-27T06:36:13","guid":{"rendered":"https:\/\/2thenew.today\/blog\/?p=83790"},"modified":"2026-10-27T22:56:29","modified_gmt":"2026-10-27T17:26:29","slug":"swift-concurrency-for-avplayer-actors-and-async-await","status":"publish","type":"post","link":"https:\/\/2thenew.today\/blog\/swift-concurrency-for-avplayer-actors-and-async-await\/","title":{"rendered":"Swift Concurrency for AVPlayer: Actors and async\/await"},"content":{"rendered":"<h2>Introduction<\/h2>\n<p>Here\u2019s a bug you might recognise. A user taps \u201cNext episode\u201d while the current one is still loading, and a second later an error screen pops up over the new episode. The error actually belongs to the previous one, which failed late.<\/p>\n<p>Bugs like this are easy to blame on AVFoundation, but they\u2019re really ordering problems: events arriving after the thing they belong to is gone. Working on OTT apps for iPhone and Apple TV, I\u2019ve settled on a player structure that makes most of them go away, and Swift Concurrency is what makes it practical. Let me walk you through it.<\/p>\n<h2>Prerequisites<\/h2>\n<ol>\n<li>Xcode 15 or later (Swift 5.9+).<\/li>\n<li>An iOS 16 \/ tvOS 16 deployment target.<\/li>\n<li>You\u2019re comfortable with <a href=\"https:\/\/developer.apple.com\/documentation\/avfoundation\/avplayer\" target=\"_blank\" rel=\"noopener\">AVPlayer<\/a> and know the basics of <a href=\"https:\/\/docs.swift.org\/swift-book\/documentation\/the-swift-programming-language\/concurrency\/\" target=\"_blank\" rel=\"noopener\">Swift Concurrency<\/a>.<\/li>\n<\/ol>\n<h2>Why is a video player so hard to get right?<\/h2>\n<p>Because a lot is happening at the same time. During a single playback session, your code is hearing from:<\/p>\n<ul>\n<li>KVO on <code>AVPlayerItem.status<\/code> and <code>AVPlayer.timeControlStatus<\/code><\/li>\n<li>the periodic time observer<\/li>\n<li>asset loading, plus network calls for entitlements, DRM and resume positions<\/li>\n<li>the user (play, pause, seek, next episode) and the app lifecycle<\/li>\n<\/ul>\n<p>None of these wait for each other. That\u2019s how you end up with late errors from the previous item, resume positions saved twice, or a buffering spinner that just never disappears. Which one has bitten you?<\/p>\n<h2>How I split the responsibilities<\/h2>\n<p>The idea is simple. <code>AVPlayer<\/code> stays on the main actor. Playback state lives in actors. Loading happens off the main thread. Each piece has one isolation domain, so the compiler checks every hop between them for you.<\/p>\n<table>\n<thead>\n<tr>\n<th>Component<\/th>\n<th>Isolation<\/th>\n<th>Owns<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>PlayerController<\/code><\/td>\n<td><code>@MainActor<\/code> class<\/td>\n<td><code>AVPlayer<\/code> and observer tasks<\/td>\n<\/tr>\n<tr>\n<td><code>AssetLoader<\/code><\/td>\n<td>nonisolated struct<\/td>\n<td>Nothing; returns Sendable results<\/td>\n<\/tr>\n<tr>\n<td><code>PlaybackStateStore<\/code><\/td>\n<td><code>actor<\/code><\/td>\n<td>The playback state machine<\/td>\n<\/tr>\n<tr>\n<td><code>ResumePositionStore<\/code><\/td>\n<td><code>actor<\/code><\/td>\n<td>Pending resume positions<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Why not put <code>AVPlayer<\/code> in an actor too? It isn\u2019t <code>Sendable<\/code>, <code>AVPlayerLayer<\/code> wants the main thread anyway, and every <code>play()<\/code> would become an <code>await<\/code>.<\/p>\n<h2>1. A state actor that ignores stale events<\/h2>\n<p>This is the piece that fixes the \u201clate error from the previous episode\u201d bug. All state changes go through one actor, and every event carries the ID of the item it belongs to. If the ID doesn\u2019t match the item that\u2019s currently active, the event is simply dropped.<\/p>\n<pre>enum PlaybackState: Equatable, Sendable {\r\n    case idle\r\n    case loading(itemID: String), playing(itemID: String), paused(itemID: String)\r\n    case buffering(itemID: String), ended(itemID: String)\r\n    case ready(itemID: String, isLive: Bool)\r\n    case failed(itemID: String, message: String)\r\n}\r\n\r\nactor PlaybackStateStore {\r\n    private(set) var state: PlaybackState = .idle\r\n    private var activeItemID: String?\r\n    private var continuations: [UUID: AsyncStream&lt;PlaybackState&gt;.Continuation] = [:]\r\n\r\n    func begin(itemID: String) {\r\n        activeItemID = itemID\r\n        publish(.loading(itemID: itemID))\r\n    }\r\n\r\n    func apply(_ newState: PlaybackState, for itemID: String) {\r\n        guard itemID == activeItemID else { return }   \/\/ stale event, drop it\r\n        guard newState != state else { return }        \/\/ de-duplicate KVO noise\r\n        publish(newState)\r\n    }\r\n\r\n    func updates() -&gt; AsyncStream&lt;PlaybackState&gt; {\r\n        let id = UUID()\r\n        let (stream, continuation) = AsyncStream.makeStream(\r\n            of: PlaybackState.self, bufferingPolicy: .bufferingNewest(1))\r\n        continuation.yield(state)\r\n        continuations[id] = continuation\r\n        continuation.onTermination = { [weak self] _ in\r\n            Task { await self?.removeSubscriber(id) }\r\n        }\r\n        return stream\r\n    }\r\n\r\n    private func publish(_ newState: PlaybackState) {\r\n        state = newState\r\n        continuations.values.forEach { $0.yield(newState) }\r\n    }\r\n\r\n    private func removeSubscriber(_ id: UUID) { continuations[id] = nil }\r\n}<\/pre>\n<p>I use <code>.bufferingNewest(1)<\/code> because the UI only ever cares about the latest state, not a backlog. And there\u2019s no <code>await<\/code> anywhere inside the actor, so a state change can\u2019t be interrupted halfway through.<\/p>\n<h2>2. Loading assets (and actually cancelling them)<\/h2>\n<p>If you\u2019re still using <code>loadValuesAsynchronously(forKeys:)<\/code>, the async <code>AVAsset.load(_:)<\/code> API that arrived in iOS 15 is much nicer. You can load several properties in one call, and you get real errors back.<\/p>\n<pre>struct LoadedAsset: @unchecked Sendable {\r\n    let asset: AVURLAsset   \/\/ not mutated after loading\r\n    let isLive: Bool\r\n}\r\n\r\nenum AssetLoadError: Error { case notPlayable }\r\n\r\nstruct AssetLoader {\r\n    func load(url: URL) async throws -&gt; LoadedAsset {\r\n        let asset = AVURLAsset(url: url)\r\n        return try await withTaskCancellationHandler {\r\n            let (isPlayable, duration) = try await asset.load(.isPlayable, .duration)\r\n            try Task.checkCancellation()\r\n            guard isPlayable else { throw AssetLoadError.notPlayable }\r\n            return LoadedAsset(asset: asset, isLive: duration.isIndefinite)\r\n        } onCancel: {\r\n            asset.cancelLoading()\r\n        }\r\n    }\r\n}<\/pre>\n<p>The part I like most is the cancellation handler. When the user switches episodes, the task gets cancelled, <code>cancelLoading()<\/code> runs, and the old playlist stops downloading in the background. Also, an indefinite <code>duration<\/code> means the stream is live, so you know early whether to show a seek bar or a LIVE badge.<\/p>\n<h2>3. Putting it together in the controller<\/h2>\n<p>The controller is a <code>@MainActor<\/code> class that owns the player. Every call to <code>load<\/code> cancels whatever was loading before it.<\/p>\n<pre>@MainActor\r\nfinal class PlayerController {\r\n    let player = AVPlayer()\r\n    private let loader = AssetLoader()\r\n    private let stateStore: PlaybackStateStore\r\n    private var loadTask: Task&lt;Void, Never&gt;?\r\n    private var observers: [Task&lt;Void, Never&gt;] = []\r\n\r\n    init(stateStore: PlaybackStateStore) { self.stateStore = stateStore }\r\n\r\n    func load(itemID: String, url: URL) {\r\n        loadTask?.cancel()\r\n        observers.forEach { $0.cancel() }\r\n        observers.removeAll()\r\n\r\n        loadTask = Task {\r\n            await stateStore.begin(itemID: itemID)\r\n            do {\r\n                let loaded = try await loader.load(url: url)\r\n                try Task.checkCancellation()   \/\/ a newer load() may have started\r\n                player.replaceCurrentItem(with: AVPlayerItem(asset: loaded.asset))\r\n                observeTimeControlStatus(itemID: itemID)\r\n                await stateStore.apply(.ready(itemID: itemID, isLive: loaded.isLive), for: itemID)\r\n                player.play()\r\n            } catch is CancellationError {\r\n                \/\/ Superseded by a newer load. Nothing to report.\r\n            } catch {\r\n                await stateStore.apply(.failed(itemID: itemID,\r\n                    message: error.localizedDescription), for: itemID)\r\n            }\r\n        }\r\n    }\r\n\r\n    private func observeTimeControlStatus(itemID: String) {\r\n        let store = stateStore\r\n        observers.append(Task { [player] in\r\n            for await status in player.publisher(for: .timeControlStatus).values {\r\n                let state: PlaybackState = switch status {\r\n                case .playing: .playing(itemID: itemID)\r\n                case .waitingToPlayAtSpecifiedRate: .buffering(itemID: itemID)\r\n                default: .paused(itemID: itemID)\r\n                }\r\n                await store.apply(state, for: itemID)\r\n            }\r\n        })\r\n    }\r\n}<\/pre>\n<p>Because the <code>Task<\/code> inherits main-actor isolation from the class, I can touch <code>player<\/code> after each <code>await<\/code> without any <code>DispatchQueue.main.async<\/code>. The <code>checkCancellation()<\/code> after loading stops an outdated load from replacing the item the user actually wants. If you\u2019re using SwiftUI, call <code>load<\/code> from <code>.task(id: episode.id)<\/code> and the \u201cnext episode while loading\u201d case is handled for you.<\/p>\n<h2>4. The bug actors don\u2019t protect you from<\/h2>\n<p>This one took me a while to properly understand. An actor makes sure only one call touches its state at a time, but it can\u2019t make sure your assumptions still hold after an <code>await<\/code>. Every <code>await<\/code> inside an actor is a point where other calls can sneak in. Here\u2019s a flush function for saving resume positions that looks perfectly fine:<\/p>\n<pre>func flush() async {   \/\/ BUGGY\r\n    guard !pending.isEmpty else { return }\r\n    try? await api.save(pending)   \/\/ suspends here\r\n    pending.removeAll()            \/\/ wipes positions recorded during the await\r\n}<\/pre>\n<p>The trouble starts while <code>api.save<\/code> is waiting on the network. New positions keep coming in, and a second flush can start and send the same batch again. Then, when the first save finally returns, <code>removeAll()<\/code> wipes positions that were never sent. On office Wi-Fi you\u2019ll never see it, which is exactly why it gets past QA.<\/p>\n<p>The fix is to finish changing state <em>before<\/em> you suspend, and be careful about what you assume <em>after<\/em>:<\/p>\n<pre>actor ResumePositionStore {\r\n    private var pending: [String: Double] = [:]\r\n    private var isFlushing = false\r\n    private let api: ProgressAPI\r\n\r\n    init(api: ProgressAPI) { self.api = api }\r\n\r\n    func record(_ seconds: Double, for itemID: String) {\r\n        pending[itemID] = seconds\r\n    }\r\n\r\n    func flush() async {\r\n        guard !isFlushing, !pending.isEmpty else { return }\r\n        let batch = pending            \/\/ 1. update state before suspending\r\n        pending.removeAll()\r\n        isFlushing = true\r\n        defer { isFlushing = false }\r\n        do {\r\n            try await api.save(batch)  \/\/ 2. suspend\r\n        } catch {\r\n            pending.merge(batch) { newer, _ in newer }   \/\/ 3. keep newer positions\r\n        }\r\n    }\r\n}<\/pre>\n<p>These days I read every <code>await<\/code> inside an actor as \u201cstate might be different after this line\u201d. Take a snapshot before, and don\u2019t assume anything after.<\/p>\n<h2>A few mistakes to avoid<\/h2>\n<ul>\n<li>Don\u2019t hop into an actor on every frame. Half a second to a second is plenty for a scrubber, and 5\u201310 seconds is fine for resume positions.<\/li>\n<li>Swift won\u2019t stop a cancelled task for you. After an <code>await<\/code>, check for cancellation yourself before you touch the player.<\/li>\n<li>Cancel your observer tasks when the item changes. Waiting for <code>deinit<\/code> usually means waiting forever.<\/li>\n<li>If you move to Swift 6.2 with <code>NonisolatedNonsendingByDefault<\/code> on, mark <code>AssetLoader.load<\/code> as <code>@concurrent<\/code> (<a href=\"https:\/\/github.com\/swiftlang\/swift-evolution\/blob\/main\/proposals\/0461-async-function-isolation.md\" target=\"_blank\" rel=\"noopener\">SE-0461<\/a>), otherwise loading will quietly start running on the main actor.<\/li>\n<\/ul>\n<h2>Conclusion<\/h2>\n<p>Swift Concurrency won\u2019t make a video player simple. What it does is make the rules something the compiler can check instead of something you have to remember. If you take only three things from this post: keep <code>AVPlayer<\/code> on the main actor, tag every event with its item ID, and be suspicious of every <code>await<\/code> inside an actor. That alone gets rid of most \u201cthis should never happen\u201d bugs.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Introduction Here\u2019s a bug you might recognise. A user taps \u201cNext episode\u201d while the current one is still loading, and a second later an error screen pops up over the new episode. The error actually belongs to the previous one, which failed late. Bugs like this are easy to blame on AVFoundation, but they\u2019re really [&hellip;]<\/p>\n","protected":false},"author":2138,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":2,"footnotes":""},"categories":[3477],"tags":[9078,9074,9076,7521,9077,7964,5963,3116,2715,9075,9073,5460,3531,5534],"class_list":["post-83790","post","type-post","status-publish","format-standard","hentry","category-media-entertainment","tag-actorreentrancy","tag-asyncawait","tag-avfoundation","tag-avplayer","tag-hlsstreaming","tag-iosdevelopment","tag-mobileappdevelopment","tag-ott","tag-swift","tag-swiftactors","tag-swiftconcurrency","tag-swiftui","tag-tvos","tag-videostreaming"],"aioseo_notices":[],"aioseo_head":"\n\t\t<!-- All in One SEO 5.0.0.1 - aioseo.com -->\n\t<meta name=\"description\" content=\"Introduction Here\u2019s a bug you might recognise. A user taps \u201cNext episode\u201d while the current one is still loading, and a second later an error screen pops up over the new episode. The error actually belongs to the previous one, which failed late. Bugs like this are easy to blame on AVFoundation, but they\u2019re really\" \/>\n\t<meta name=\"robots\" content=\"max-image-preview:large\" \/>\n\t<meta name=\"author\" content=\"Mukul Sharma\"\/>\n\t<link rel=\"canonical\" href=\"https:\/\/2thenew.today\/blog\/swift-concurrency-for-avplayer-actors-and-async-await\/\" \/>\n\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.0.1\" \/>\n\t\t<meta property=\"og:locale\" content=\"en_US\" \/>\n\t\t<meta property=\"og:site_name\" content=\"TO THE NEW BLOG\" \/>\n\t\t<meta property=\"og:type\" content=\"blog\" \/>\n\t\t<meta property=\"og:title\" content=\"Swift Concurrency for AVPlayer: Actors and async\/await | TO THE NEW Blog\" \/>\n\t\t<meta property=\"og:description\" content=\"Introduction Here\u2019s a bug you might recognise. A user taps \u201cNext episode\u201d while the current one is still loading, and a second later an error screen pops up over the new episode. The error actually belongs to the previous one, which failed late. Bugs like this are easy to blame on AVFoundation, but they\u2019re really\" \/>\n\t\t<meta property=\"og:url\" content=\"https:\/\/2thenew.today\/blog\/swift-concurrency-for-avplayer-actors-and-async-await\/\" \/>\n\t\t<meta property=\"og:image\" content=\"https:\/\/2thenew.today\/blog\/wp-content\/themes\/ttn\/images\/social-logo.png\" \/>\n\t\t<meta property=\"og:image:secure_url\" content=\"https:\/\/2thenew.today\/blog\/wp-content\/themes\/ttn\/images\/social-logo.png\" \/>\n\t\t<meta name=\"twitter:card\" content=\"summary\" \/>\n\t\t<meta name=\"twitter:site\" content=\"@tothenew\" \/>\n\t\t<meta name=\"twitter:title\" content=\"Swift Concurrency for AVPlayer: Actors and async\/await | TO THE NEW Blog\" \/>\n\t\t<meta name=\"twitter:description\" content=\"Introduction Here\u2019s a bug you might recognise. A user taps \u201cNext episode\u201d while the current one is still loading, and a second later an error screen pops up over the new episode. The error actually belongs to the previous one, which failed late. Bugs like this are easy to blame on AVFoundation, but they\u2019re really\" \/>\n\t\t<meta name=\"twitter:image\" content=\"https:\/\/2thenew.today\/blog\/wp-content\/themes\/ttn\/images\/social-logo.png\" \/>\n\t\t<script type=\"application\/ld+json\" class=\"aioseo-schema\">\n\t\t\t{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/swift-concurrency-for-avplayer-actors-and-async-await\\\/#article\",\"name\":\"Swift Concurrency for AVPlayer: Actors and async\\\/await | TO THE NEW Blog\",\"headline\":\"Swift Concurrency for AVPlayer: Actors and async\\\/await\",\"author\":{\"@id\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/author\\\/mukul-sharma1\\\/#author\"},\"publisher\":{\"@id\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/#organization\"},\"datePublished\":\"2026-10-27T12:06:13+05:30\",\"dateModified\":\"2026-10-27T22:56:29+05:30\",\"inLanguage\":\"en-US\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/swift-concurrency-for-avplayer-actors-and-async-await\\\/#webpage\"},\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/swift-concurrency-for-avplayer-actors-and-async-await\\\/#webpage\"},\"articleSection\":\"Media &amp; Entertainment, ActorReentrancy, AsyncAwait, AVFoundation, AVPlayer, HLSStreaming, iOSdevelopment, MobileAppDevelopment, ott, swift, SwiftActors, SwiftConcurrency, SwiftUI, tvOS, VideoStreaming\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/swift-concurrency-for-avplayer-actors-and-async-await\\\/#breadcrumblist\",\"itemListElement\":[{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/www.tothenew.com\\\/blog#listItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.tothenew.com\\\/blog\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/category\\\/media-entertainment\\\/#listItem\",\"name\":\"Media &amp; Entertainment\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/category\\\/media-entertainment\\\/#listItem\",\"position\":2,\"name\":\"Media &amp; Entertainment\",\"item\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/category\\\/media-entertainment\\\/\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/swift-concurrency-for-avplayer-actors-and-async-await\\\/#listItem\",\"name\":\"Swift Concurrency for AVPlayer: Actors and async\\\/await\"},\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/www.tothenew.com\\\/blog#listItem\",\"name\":\"Home\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/swift-concurrency-for-avplayer-actors-and-async-await\\\/#listItem\",\"position\":3,\"name\":\"Swift Concurrency for AVPlayer: Actors and async\\\/await\",\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/category\\\/media-entertainment\\\/#listItem\",\"name\":\"Media &amp; Entertainment\"}}]},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/#organization\",\"name\":\"TO THE NEW Blog\",\"url\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/\"},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/author\\\/mukul-sharma1\\\/#author\",\"url\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/author\\\/mukul-sharma1\\\/\",\"name\":\"Mukul Sharma\",\"image\":{\"@type\":\"ImageObject\",\"@id\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/swift-concurrency-for-avplayer-actors-and-async-await\\\/#authorImage\",\"url\":\"https:\\\/\\\/newersworld-sf-static.tothenew.net\\\/prod\\\/profilePicFolder\\\/f000d40e-a20f-4c13-9993-e1e81cb48167_Mukul-Sharma-Profile-Pitcure.jpeg\",\"width\":96,\"height\":96,\"caption\":\"Mukul Sharma\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/swift-concurrency-for-avplayer-actors-and-async-await\\\/#webpage\",\"url\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/swift-concurrency-for-avplayer-actors-and-async-await\\\/\",\"name\":\"Swift Concurrency for AVPlayer: Actors and async\\\/await | TO THE NEW Blog\",\"description\":\"Introduction Here\\u2019s a bug you might recognise. A user taps \\u201cNext episode\\u201d while the current one is still loading, and a second later an error screen pops up over the new episode. The error actually belongs to the previous one, which failed late. Bugs like this are easy to blame on AVFoundation, but they\\u2019re really\",\"inLanguage\":\"en-US\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/#website\"},\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/swift-concurrency-for-avplayer-actors-and-async-await\\\/#breadcrumblist\"},\"author\":{\"@id\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/author\\\/mukul-sharma1\\\/#author\"},\"creator\":{\"@id\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/author\\\/mukul-sharma1\\\/#author\"},\"datePublished\":\"2026-10-27T12:06:13+05:30\",\"dateModified\":\"2026-10-27T22:56:29+05:30\"},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/\",\"name\":\"TO THE NEW Blog\",\"inLanguage\":\"en-US\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.tothenew.com\\\/blog\\\/#organization\"}}]}\n\t\t<\/script>\n\t\t<!-- All in One SEO -->\n\n","aioseo_head_json":{"title":"Swift Concurrency for AVPlayer: Actors and async\/await | TO THE NEW Blog","description":"Introduction Here\u2019s a bug you might recognise. A user taps \u201cNext episode\u201d while the current one is still loading, and a second later an error screen pops up over the new episode. The error actually belongs to the previous one, which failed late. Bugs like this are easy to blame on AVFoundation, but they\u2019re really","canonical_url":"https:\/\/2thenew.today\/blog\/swift-concurrency-for-avplayer-actors-and-async-await\/","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/2thenew.today\/blog\/swift-concurrency-for-avplayer-actors-and-async-await\/#article","name":"Swift Concurrency for AVPlayer: Actors and async\/await | TO THE NEW Blog","headline":"Swift Concurrency for AVPlayer: Actors and async\/await","author":{"@id":"https:\/\/2thenew.today\/blog\/author\/mukul-sharma1\/#author"},"publisher":{"@id":"https:\/\/2thenew.today\/blog\/#organization"},"datePublished":"2026-10-27T12:06:13+05:30","dateModified":"2026-10-27T22:56:29+05:30","inLanguage":"en-US","mainEntityOfPage":{"@id":"https:\/\/2thenew.today\/blog\/swift-concurrency-for-avplayer-actors-and-async-await\/#webpage"},"isPartOf":{"@id":"https:\/\/2thenew.today\/blog\/swift-concurrency-for-avplayer-actors-and-async-await\/#webpage"},"articleSection":"Media &amp; Entertainment, ActorReentrancy, AsyncAwait, AVFoundation, AVPlayer, HLSStreaming, iOSdevelopment, MobileAppDevelopment, ott, swift, SwiftActors, SwiftConcurrency, SwiftUI, tvOS, VideoStreaming"},{"@type":"BreadcrumbList","@id":"https:\/\/2thenew.today\/blog\/swift-concurrency-for-avplayer-actors-and-async-await\/#breadcrumblist","itemListElement":[{"@type":"ListItem","@id":"https:\/\/2thenew.today\/blog#listItem","position":1,"name":"Home","item":"https:\/\/2thenew.today\/blog","nextItem":{"@type":"ListItem","@id":"https:\/\/2thenew.today\/blog\/category\/media-entertainment\/#listItem","name":"Media &amp; Entertainment"}},{"@type":"ListItem","@id":"https:\/\/2thenew.today\/blog\/category\/media-entertainment\/#listItem","position":2,"name":"Media &amp; Entertainment","item":"https:\/\/2thenew.today\/blog\/category\/media-entertainment\/","nextItem":{"@type":"ListItem","@id":"https:\/\/2thenew.today\/blog\/swift-concurrency-for-avplayer-actors-and-async-await\/#listItem","name":"Swift Concurrency for AVPlayer: Actors and async\/await"},"previousItem":{"@type":"ListItem","@id":"https:\/\/2thenew.today\/blog#listItem","name":"Home"}},{"@type":"ListItem","@id":"https:\/\/2thenew.today\/blog\/swift-concurrency-for-avplayer-actors-and-async-await\/#listItem","position":3,"name":"Swift Concurrency for AVPlayer: Actors and async\/await","previousItem":{"@type":"ListItem","@id":"https:\/\/2thenew.today\/blog\/category\/media-entertainment\/#listItem","name":"Media &amp; Entertainment"}}]},{"@type":"Organization","@id":"https:\/\/2thenew.today\/blog\/#organization","name":"TO THE NEW Blog","url":"https:\/\/2thenew.today\/blog\/"},{"@type":"Person","@id":"https:\/\/2thenew.today\/blog\/author\/mukul-sharma1\/#author","url":"https:\/\/2thenew.today\/blog\/author\/mukul-sharma1\/","name":"Mukul Sharma","image":{"@type":"ImageObject","@id":"https:\/\/2thenew.today\/blog\/swift-concurrency-for-avplayer-actors-and-async-await\/#authorImage","url":"https:\/\/newersworld-sf-static.tothenew.net\/prod\/profilePicFolder\/f000d40e-a20f-4c13-9993-e1e81cb48167_Mukul-Sharma-Profile-Pitcure.jpeg","width":96,"height":96,"caption":"Mukul Sharma"}},{"@type":"WebPage","@id":"https:\/\/2thenew.today\/blog\/swift-concurrency-for-avplayer-actors-and-async-await\/#webpage","url":"https:\/\/2thenew.today\/blog\/swift-concurrency-for-avplayer-actors-and-async-await\/","name":"Swift Concurrency for AVPlayer: Actors and async\/await | TO THE NEW Blog","description":"Introduction Here\u2019s a bug you might recognise. A user taps \u201cNext episode\u201d while the current one is still loading, and a second later an error screen pops up over the new episode. The error actually belongs to the previous one, which failed late. Bugs like this are easy to blame on AVFoundation, but they\u2019re really","inLanguage":"en-US","isPartOf":{"@id":"https:\/\/2thenew.today\/blog\/#website"},"breadcrumb":{"@id":"https:\/\/2thenew.today\/blog\/swift-concurrency-for-avplayer-actors-and-async-await\/#breadcrumblist"},"author":{"@id":"https:\/\/2thenew.today\/blog\/author\/mukul-sharma1\/#author"},"creator":{"@id":"https:\/\/2thenew.today\/blog\/author\/mukul-sharma1\/#author"},"datePublished":"2026-10-27T12:06:13+05:30","dateModified":"2026-10-27T22:56:29+05:30"},{"@type":"WebSite","@id":"https:\/\/2thenew.today\/blog\/#website","url":"https:\/\/2thenew.today\/blog\/","name":"TO THE NEW Blog","inLanguage":"en-US","publisher":{"@id":"https:\/\/2thenew.today\/blog\/#organization"}}]},"og:locale":"en_US","og:site_name":"TO THE NEW BLOG","og:type":"blog","og:title":"Swift Concurrency for AVPlayer: Actors and async\/await | TO THE NEW Blog","og:description":"Introduction Here\u2019s a bug you might recognise. A user taps \u201cNext episode\u201d while the current one is still loading, and a second later an error screen pops up over the new episode. The error actually belongs to the previous one, which failed late. Bugs like this are easy to blame on AVFoundation, but they\u2019re really","og:url":"https:\/\/2thenew.today\/blog\/swift-concurrency-for-avplayer-actors-and-async-await\/","og:image":"https:\/\/2thenew.today\/blog\/wp-content\/themes\/ttn\/images\/social-logo.png","og:image:secure_url":"https:\/\/2thenew.today\/blog\/wp-content\/themes\/ttn\/images\/social-logo.png","twitter:card":"summary","twitter:site":"@tothenew","twitter:title":"Swift Concurrency for AVPlayer: Actors and async\/await | TO THE NEW Blog","twitter:description":"Introduction Here\u2019s a bug you might recognise. A user taps \u201cNext episode\u201d while the current one is still loading, and a second later an error screen pops up over the new episode. The error actually belongs to the previous one, which failed late. Bugs like this are easy to blame on AVFoundation, but they\u2019re really","twitter:image":"https:\/\/2thenew.today\/blog\/wp-content\/themes\/ttn\/images\/social-logo.png"},"aioseo_meta_data":{"post_id":"83790","title":null,"description":null,"keywords":null,"keyphrases":{"focus":{"keyphrase":"","score":0,"analysis":{"keyphraseInTitle":{"score":0,"maxScore":9,"error":1}}},"additional":[]},"primary_term":null,"canonical_url":null,"og_title":null,"og_description":null,"og_object_type":"default","og_image_type":"default","og_image_url":null,"og_image_width":null,"og_image_height":null,"og_image_custom_url":null,"og_image_custom_fields":null,"og_video":"","og_custom_url":null,"og_article_section":null,"og_article_tags":null,"twitter_use_og":false,"twitter_card":"default","twitter_image_type":"default","twitter_image_url":null,"twitter_image_custom_url":null,"twitter_image_custom_fields":null,"twitter_title":null,"twitter_description":null,"schema":{"blockGraphs":[],"customGraphs":[],"default":{"data":{"Article":[],"Course":[],"Dataset":[],"FAQPage":[],"Movie":[],"Person":[],"Product":[],"ProductReview":[],"Car":[],"Recipe":[],"Service":[],"SoftwareApplication":[],"WebPage":[]},"graphName":"Article","isEnabled":true},"graphs":[]},"schema_type":"default","schema_type_options":null,"pillar_content":false,"robots_default":true,"robots_noindex":false,"robots_noarchive":false,"robots_nosnippet":false,"robots_nofollow":false,"robots_noimageindex":false,"robots_noodp":false,"robots_notranslate":false,"robots_max_snippet":"-1","robots_max_videopreview":"-1","robots_max_imagepreview":"large","priority":null,"frequency":"default","local_seo":null,"limit_modified_date":false,"created":"2026-10-01 05:18:30","updated":"2026-10-27 17:26:31","focus_keyword":null,"additional_keywords":null,"truseo_locale":null,"ai":{"faqs":[],"keyPoints":[],"schemas":[],"titles":[],"descriptions":[],"socialPosts":{"email":{"subject":"","preview":"","content":""},"linkedin":[],"twitter":[],"facebook":[],"instagram":[]}},"breadcrumb_settings":null,"seo_analyzer_scan_date":null},"aioseo_breadcrumb":"<div class=\"aioseo-breadcrumbs\"><span class=\"aioseo-breadcrumb\">\n\t\t\t<a href=\"https:\/\/2thenew.today\/blog\" title=\"Home\">Home<\/a>\n\t\t<\/span><span class=\"aioseo-breadcrumb-separator\">&raquo;<\/span><span class=\"aioseo-breadcrumb\">\n\t\t\t<a href=\"https:\/\/2thenew.today\/blog\/category\/media-entertainment\/\" title=\"Media &amp; Entertainment\">Media &amp; Entertainment<\/a>\n\t\t<\/span><span class=\"aioseo-breadcrumb-separator\">&raquo;<\/span><span class=\"aioseo-breadcrumb\">\n\t\t\tSwift Concurrency for AVPlayer: Actors and async\/await\n\t\t<\/span><\/div>","aioseo_breadcrumb_json":[{"label":"Home","link":"https:\/\/2thenew.today\/blog"},{"label":"Media &amp; Entertainment","link":"https:\/\/2thenew.today\/blog\/category\/media-entertainment\/"},{"label":"Swift Concurrency for AVPlayer: Actors and async\/await","link":"https:\/\/2thenew.today\/blog\/swift-concurrency-for-avplayer-actors-and-async-await\/"}],"_links":{"self":[{"href":"https:\/\/2thenew.today\/blog\/wp-json\/wp\/v2\/posts\/83790","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/2thenew.today\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/2thenew.today\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/2thenew.today\/blog\/wp-json\/wp\/v2\/users\/2138"}],"replies":[{"embeddable":true,"href":"https:\/\/2thenew.today\/blog\/wp-json\/wp\/v2\/comments?post=83790"}],"version-history":[{"count":3,"href":"https:\/\/2thenew.today\/blog\/wp-json\/wp\/v2\/posts\/83790\/revisions"}],"predecessor-version":[{"id":84060,"href":"https:\/\/2thenew.today\/blog\/wp-json\/wp\/v2\/posts\/83790\/revisions\/84060"}],"wp:attachment":[{"href":"https:\/\/2thenew.today\/blog\/wp-json\/wp\/v2\/media?parent=83790"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/2thenew.today\/blog\/wp-json\/wp\/v2\/categories?post=83790"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/2thenew.today\/blog\/wp-json\/wp\/v2\/tags?post=83790"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}