Skip to content

Commit

Permalink
Improve player UI
Browse files Browse the repository at this point in the history
  • Loading branch information
mbernson committed Dec 30, 2023
1 parent c66ad76 commit aa8c02f
Show file tree
Hide file tree
Showing 10 changed files with 290 additions and 173 deletions.
4 changes: 4 additions & 0 deletions CCCTube.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
D5C76ACB2A619BF100B7469C /* TalkThumbnail.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C76ACA2A619BF100B7469C /* TalkThumbnail.swift */; };
D5C76ACD2A61A02700B7469C /* ConferenceThumbnail.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C76ACC2A61A02700B7469C /* ConferenceThumbnail.swift */; };
D5D4B2BC2B3F25E500F33CD3 /* Alert+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D4B2BB2B3F25E500F33CD3 /* Alert+Error.swift */; };
D5D4B2BE2B3F435C00F33CD3 /* TalkViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D4B2BD2B3F435C00F33CD3 /* TalkViewModel.swift */; };
D5D4DF43289859AB00560E4F /* MediaAnalyzer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D4DF42289859AB00560E4F /* MediaAnalyzer.swift */; };
D5E97D4A2B3F11E8005BFF45 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = D5E97D492B3F11E8005BFF45 /* Localizable.xcstrings */; };
D5E97D4C2B3F11F0005BFF45 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = D5E97D4B2B3F11F0005BFF45 /* Localizable.xcstrings */; };
Expand Down Expand Up @@ -104,6 +105,7 @@
D5C76ACA2A619BF100B7469C /* TalkThumbnail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TalkThumbnail.swift; sourceTree = "<group>"; };
D5C76ACC2A61A02700B7469C /* ConferenceThumbnail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConferenceThumbnail.swift; sourceTree = "<group>"; };
D5D4B2BB2B3F25E500F33CD3 /* Alert+Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Alert+Error.swift"; sourceTree = "<group>"; };
D5D4B2BD2B3F435C00F33CD3 /* TalkViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TalkViewModel.swift; sourceTree = "<group>"; };
D5D4DF42289859AB00560E4F /* MediaAnalyzer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaAnalyzer.swift; sourceTree = "<group>"; };
D5E97D492B3F11E8005BFF45 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
D5E97D4B2B3F11F0005BFF45 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
Expand Down Expand Up @@ -278,6 +280,7 @@
D501386A2A4B5ADC00ED68C0 /* TalksGrid.swift */,
D5C76ACA2A619BF100B7469C /* TalkThumbnail.swift */,
D5A156F228947625006989FF /* TalkView.swift */,
D5D4B2BD2B3F435C00F33CD3 /* TalkViewModel.swift */,
);
path = Talk;
sourceTree = "<group>";
Expand Down Expand Up @@ -466,6 +469,7 @@
D501386B2A4B5ADC00ED68C0 /* TalksGrid.swift in Sources */,
D501386D2A4B5AE100ED68C0 /* TalkListItem.swift in Sources */,
D550CA3128953B4000FD8AB2 /* TalkPlayerView.swift in Sources */,
D5D4B2BE2B3F435C00F33CD3 /* TalkViewModel.swift in Sources */,
D5E97D502B3F17B1005BFF45 /* TalkMetadataFactory.swift in Sources */,
D5A156F328947625006989FF /* TalkView.swift in Sources */,
D5F06C5D2A5035A500774C39 /* VideoPlayerView.swift in Sources */,
Expand Down
4 changes: 2 additions & 2 deletions CCCTube/Features/Talk/TalkListItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ struct TalkListItem: View {
var body: some View {
HStack(alignment: .top, spacing: 20) {
let width: CGFloat = 320
if let thumbURL = talk.thumbURL {
AsyncImage(url: thumbURL) { image in
if let imageURL = talk.posterURL ?? talk.thumbURL {
AsyncImage(url: imageURL) { image in
image.resizable().scaledToFit()
} placeholder: {
ProgressView()
Expand Down
19 changes: 15 additions & 4 deletions CCCTube/Features/Talk/TalkMetadataFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,21 @@ struct TalkMetadataFactory {
return item.copy() as! AVMetadataItem
}

func fetchImageData(forPosterImageURL posterURL: URL) async throws -> Data? {
let (imageData, _) = try await URLSession.shared.data(from: posterURL)
func createArtworkMetadataItem(forURL url: URL) async throws -> AVMetadataItem? {
let imageData = try await fetchImagePngData(forURL: url)
guard let imageData else { return nil }
let thumbnailMetadata = AVMutableMetadataItem()
thumbnailMetadata.identifier = .commonIdentifierArtwork
thumbnailMetadata.dataType = kCMMetadataBaseDataType_PNG as String
thumbnailMetadata.value = imageData as NSData
// Specify "und" to indicate an undefined language.
thumbnailMetadata.extendedLanguageTag = "und" as String
return thumbnailMetadata.copy() as? AVMetadataItem
}

private func fetchImagePngData(forURL url: URL) async throws -> Data? {
let (imageData, _) = try await URLSession.shared.data(from: url)
let image = UIImage(data: imageData)
let jpegData = image?.jpegData(compressionQuality: 0.8)
return jpegData
return image?.pngData()
}
}
37 changes: 34 additions & 3 deletions CCCTube/Features/Talk/TalkPlayerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,49 @@ import CCCApi
struct TalkPlayerView: View {
let talk: Talk
let recording: Recording
let automaticallyStartsPlayback: Bool

@State private var isLoading = false
@StateObject private var viewModel = TalkPlayerViewModel()

var body: some View {
VideoPlayerView(player: viewModel.player)
.ignoresSafeArea()
.task {
await viewModel.play(recording: recording, ofTalk: talk)
.task(id: recording) {
guard recording != viewModel.currentRecording else { return }

isLoading = true
await viewModel.prepareForPlayback(recording: recording, talk: talk)
if automaticallyStartsPlayback {
viewModel.play()
} else {
await viewModel.preroll()
}
isLoading = false
}
#if os(iOS)
.overlay {
if isLoading {
VideoProgressIndicator()
}
}
#endif
}
}

#if os(iOS)
private struct VideoProgressIndicator: View {
var body: some View {
ProgressView()
.progressViewStyle(.circular)
.controlSize(.large)
.padding(10)
.background(.regularMaterial)
.clipShape(Circle())
}
}
#endif

#Preview {
TalkPlayerView(talk: .example, recording: .example)
TalkPlayerView(talk: .example, recording: .example, automaticallyStartsPlayback: false)
}
47 changes: 39 additions & 8 deletions CCCTube/Features/Talk/TalkPlayerViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,63 @@ import AVKit
import CCCApi
import os.log

@MainActor class TalkPlayerViewModel: ObservableObject {
class TalkPlayerViewModel: ObservableObject {
var player: AVPlayer?

@Published var currentRecording: Recording?

private let factory = TalkMetadataFactory()
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "TalkPlayerViewModel")

func play(recording: Recording, ofTalk talk: Talk) async {
private var statusObservation: NSKeyValueObservation?

func prepareForPlayback(recording: Recording, talk: Talk) async {
let item = AVPlayerItem(url: recording.recordingURL)
item.externalMetadata = factory.createMetadataItems(for: recording, talk: talk)

let player = AVPlayer(playerItem: item)
self.player = player
objectWillChange.send()
player.play()
logger.info("Starting playback of recording: \(recording.recordingURL.absoluteString, privacy: .public)")
DispatchQueue.main.async {
self.currentRecording = recording
self.objectWillChange.send()
}
logger.info("Preparing playback of recording: \(recording.recordingURL.absoluteString, privacy: .public)")

// Fetch poster image and append it to the metadata
if let imageMetadata = await fetchPosterImage(for: talk) {
item.externalMetadata.append(imageMetadata)
}
}

func preroll() async {
guard let player else { return }
await withCheckedContinuation { continuation in
statusObservation = player.observe(\.status, options: [.initial, .new]) { player, change in
if player.status == .readyToPlay {
continuation.resume(returning: ())
self.statusObservation?.invalidate()
}
}
}
if await player.preroll(atRate: 1.0) {
logger.debug("Preroll success")
} else {
logger.warning("Preroll failed")
}
}

func play() {
player?.play()
}

func pause() {
player?.pause()
}

private func fetchPosterImage(for talk: Talk) async -> AVMetadataItem? {
do {
if let posterURL = talk.posterURL, let imageData = try await factory.fetchImageData(forPosterImageURL: posterURL) {
let imageMetadata = factory.createMetadataItem(for: .commonIdentifierArtwork, value: imageData as NSData, language: nil)
return imageMetadata
if let imageURL = talk.posterURL ?? talk.thumbURL {
return try await factory.createArtworkMetadataItem(forURL: imageURL)
} else {
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion CCCTube/Features/Talk/TalkThumbnail.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ struct TalkThumbnail: View {
let talk: Talk

var body: some View {
AsyncImage(url: talk.thumbURL ?? talk.posterURL) { phase in
AsyncImage(url: talk.posterURL ?? talk.thumbURL) { phase in
if let image = phase.image {
image.resizable().scaledToFit()
} else if phase.error != nil {
Expand Down
Loading

0 comments on commit aa8c02f

Please sign in to comment.