Skip to content

Instantly share code, notes, and snippets.

@Eggers-CGI
Created May 7, 2025 08:42
Show Gist options
  • Save Eggers-CGI/447c2931eea694de5ba12fc6e9efd2c4 to your computer and use it in GitHub Desktop.
Save Eggers-CGI/447c2931eea694de5ba12fc6e9efd2c4 to your computer and use it in GitHub Desktop.
BUG: Apple tvOS - flickering chapters when using artwork image in LIVE-Streams
import SwiftUI
import AVKit
struct Chapter: Identifiable {
let id = UUID()
let title: String
let seconds: Double
var artworkData: Data?
}
class ChaptersLoader: ObservableObject {
@Published var chapters: [Chapter] = []
@Published var ready = false
let baseChapters = [
("Intro", 0.0),
("Chapter 1", 60.0),
("Chapter 2", 120.0),
("Chapter 3", 180.0)
]
func load() {
var loaded: [Chapter] = []
let group = DispatchGroup()
for (title, seconds) in baseChapters {
group.enter()
let url = URL(string: "https://cataas.com/cat")!
URLSession.shared.dataTask(with: url) { data, _, _ in
var chapter = Chapter(title: title, seconds: seconds, artworkData: nil)
if let data = data {
chapter.artworkData = data
}
loaded.append(chapter)
group.leave()
}.resume()
}
group.notify(queue: .main) {
loaded.sort { $0.seconds < $1.seconds }
self.chapters = loaded
self.ready = true
}
}
}
struct AVPlayerViewControllerRepresentable: UIViewControllerRepresentable {
let streamURL: URL
let chapters: [Chapter]
func makeUIViewController(context: Context) -> AVPlayerViewController {
let item = AVPlayerItem(url: streamURL)
let groups = chapters.map { chapter -> AVTimedMetadataGroup in
let titleItem = AVMutableMetadataItem()
titleItem.identifier = .commonIdentifierTitle
titleItem.value = chapter.title as NSString
titleItem.extendedLanguageTag = "und"
var items: [AVMetadataItem] = [titleItem]
if let data = chapter.artworkData {
let artworkItem = AVMutableMetadataItem()
artworkItem.identifier = .commonIdentifierArtwork
artworkItem.dataType = kCMMetadataBaseDataType_PNG as String
artworkItem.value = data as NSData
artworkItem.extendedLanguageTag = "und"
items.append(artworkItem)
}
let time = CMTime(seconds: chapter.seconds, preferredTimescale: 1)
let range = CMTimeRange(start: time, duration: CMTime(seconds: 10, preferredTimescale: 1))
return AVTimedMetadataGroup(items: items, timeRange: range)
}
let navGroup = AVNavigationMarkersGroup(title: "Chapters", timedNavigationMarkers: groups)
item.navigationMarkerGroups = [navGroup]
let player = AVPlayer(playerItem: item)
let controller = AVPlayerViewController()
controller.player = player
player.play()
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {}
}
struct ContentView: View {
@StateObject var loader = ChaptersLoader()
let streamURL = URL(string: "https://ireplay.tv/test/blender.m3u8")!
var body: some View {
Group {
if loader.ready {
AVPlayerViewControllerRepresentable(streamURL: streamURL, chapters: loader.chapters)
.edgesIgnoringSafeArea(.all)
} else {
ProgressView("Loading cat artwork...")
.onAppear { loader.load() }
}
}
}
}
@main
struct YourProjectNameApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment