Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(example): Universal App #86

Merged
merged 4 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

172 changes: 172 additions & 0 deletions Example/SwiftAudio/PlayerView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
//
// PlayerView.swift
// SwiftAudio
//
// Created by Brandon Sneed on 3/30/24.
//

import SwiftUI
import SwiftAudioEx

struct PlayerView: View {
@ObservedObject var viewModel: ViewModel
@State private var showingQueue = false

let controller = AudioController.shared

init(viewModel: PlayerView.ViewModel = ViewModel()) {
self.viewModel = viewModel
}

var body: some View {
VStack(spacing: 0) {
HStack(alignment: .center) {
Spacer()
Button(action: { showingQueue.toggle() }, label: {
Text("Queue")
.fontWeight(.bold)
})
}

if let image = viewModel.artwork {
#if os(macOS)
Image(nsImage: image)
.resizable()
.scaledToFit()
.frame(width: 240, height: 240)
.padding(.top, 30)
#elseif os(iOS)
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(width: 240, height: 240)
.padding(.top, 30)
#endif
} else {
AsyncImage(url: nil)
.frame(width: 240, height: 240)
.padding(.top, 30)
}

VStack(spacing: 4) {
Text(viewModel.title)
.fontWeight(.semibold)
.font(.system(size: 18))
Text(viewModel.artist)
.fontWeight(.thin)
}
.padding(.top, 30)

if viewModel.maxTime > 0 {
VStack {
Slider(value: $viewModel.position, in: 0...viewModel.maxTime) { editing in
viewModel.isScrubbing = editing
print("scrubbing = \(viewModel.isScrubbing)")
if viewModel.isScrubbing == false {
controller.player.seek(to: viewModel.position)
}
}
HStack {
Text(viewModel.elapsedTime)
.font(.system(size: 14))
Spacer()
Text(viewModel.remainingTime)
.font(.system(size: 14))
}
}
.padding(.top, 25)
} else {
Text("Live Stream")
.padding(.top, 35)
}

HStack {
Button(action: controller.player.previous, label: {
Text("Prev")
.font(.system(size: 14))
})
.frame(maxWidth: .infinity)

Button(action: {
if viewModel.playing {
controller.player.pause()
} else {
controller.player.play()
}
}, label: {
Text(!viewModel.playWhenReady || viewModel.playbackState == .failed ? "Play" : "Pause")
.font(.system(size: 18))
.fontWeight(.semibold)
})

.frame(maxWidth: .infinity)
Button(action: controller.player.next, label: {
Text("Next")
.font(.system(size: 14))
})
.frame(maxWidth: .infinity)
}
.padding(.top, 80)

VStack {
if viewModel.playbackState == .failed {
Text("Playback failed.")
.font(.system(size: 14))
.foregroundStyle(.red)
.padding(.top, 20)
} else if (viewModel.playbackState == .loading || viewModel.playbackState == .buffering) && viewModel.playWhenReady {
ProgressView()
.progressViewStyle(.circular)
.controlSize(.small)
.padding(.top, 20)
}
}

Spacer()
}
.sheet(isPresented: $showingQueue) {
QueueView()
#if os(macOS)
.frame(width: 300, height: 400)
#endif
}
.padding(.horizontal, 16)
.padding(.top)
}
}

#Preview("Standard") {
let viewModel = PlayerView.ViewModel()
viewModel.title = "Longing"
viewModel.artist = "David Chavez"

return PlayerView(viewModel: viewModel)
}

#Preview("Error") {
let viewModel = PlayerView.ViewModel()
viewModel.title = "Longing"
viewModel.artist = "David Chavez"
viewModel.playbackState = .failed

return PlayerView(viewModel: viewModel)
}

#Preview("Buffering") {
let viewModel = PlayerView.ViewModel()
viewModel.title = "Longing"
viewModel.artist = "David Chavez"
viewModel.playbackState = .buffering
viewModel.playWhenReady = true

return PlayerView(viewModel: viewModel)
}

#Preview("Live Stream") {
let viewModel = PlayerView.ViewModel()
viewModel.title = "Longing"
viewModel.artist = "David Chavez"
viewModel.maxTime = 0

return PlayerView(viewModel: viewModel)
}
120 changes: 120 additions & 0 deletions Example/SwiftAudio/PlayerViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
//
// PlayerViewModel.swift
// SwiftAudio
//
// Created by David Chavez on 4/12/24.
//

import SwiftAudioEx

#if os(macOS)
import AppKit
public typealias NativeImage = NSImage
#elseif os(iOS)
import UIKit
public typealias NativeImage = UIImage
#endif

extension PlayerView {
final class ViewModel: ObservableObject {
// MARK: - Observables

@Published var playing: Bool = false
@Published var position: Double = 0
@Published var artwork: NativeImage? = nil
@Published var title: String = ""
@Published var artist: String = ""
@Published var maxTime: TimeInterval = 100
@Published var isScrubbing: Bool = false
@Published var elapsedTime: String = "00:00"
@Published var remainingTime: String = "00:00"

@Published var playWhenReady: Bool = false
@Published var playbackState: AudioPlayerState = .idle

// MARK: - Properties

let controller = AudioController.shared

// MARK: - Initializer

init() {
controller.player.event.playWhenReadyChange.addListener(self, handlePlayWhenReadyChange)
controller.player.event.stateChange.addListener(self, handleAudioPlayerStateChange)
controller.player.event.playbackEnd.addListener(self, handleAudioPlayerPlaybackEnd(data:))
controller.player.event.secondElapse.addListener(self, handleAudioPlayerSecondElapsed)
controller.player.event.seek.addListener(self, handleAudioPlayerDidSeek)
controller.player.event.updateDuration.addListener(self, handleAudioPlayerUpdateDuration)
controller.player.event.didRecreateAVPlayer.addListener(self, handleAVPlayerRecreated)
}

// MARK: - Updates

private func render() {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
playing = (controller.player.playerState == .playing)
playbackState = controller.player.playerState
playWhenReady = controller.player.playWhenReady
position = controller.player.currentTime
maxTime = controller.player.duration
artist = controller.player.currentItem?.getArtist() ?? ""
title = controller.player.currentItem?.getTitle() ?? ""
elapsedTime = controller.player.currentTime.secondsToString()
remainingTime = (controller.player.duration - controller.player.currentTime).secondsToString()
if let item = controller.player.currentItem as? DefaultAudioItem {
artwork = item.artwork
} else {
artwork = nil
}
}
}

private func renderTimes() {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
position = controller.player.currentTime
maxTime = controller.player.duration
elapsedTime = controller.player.currentTime.secondsToString()
remainingTime = (controller.player.duration - controller.player.currentTime).secondsToString()
print(elapsedTime)
}
}

// MARK: - AudioPlayer Event Handlers

func handleAudioPlayerStateChange(data: AudioPlayer.StateChangeEventData) {
print("state=\(data)")
render()
}

func handlePlayWhenReadyChange(data: AudioPlayer.PlayWhenReadyChangeData) {
print("playWhenReady=\(data)")
render()
}

func handleAudioPlayerPlaybackEnd(data: AudioPlayer.PlaybackEndEventData) {
print("playEndReason=\(data)")
}

func handleAudioPlayerSecondElapsed(data: AudioPlayer.SecondElapseEventData) {
if !isScrubbing {
renderTimes()
}
}

func handleAudioPlayerDidSeek(data: AudioPlayer.SeekEventData) {
// .. don't need this
}

func handleAudioPlayerUpdateDuration(data: AudioPlayer.UpdateDurationEventData) {
if !isScrubbing {
renderTimes()
}
}

func handleAVPlayerRecreated() {
// .. don't need this
}
}
}
65 changes: 65 additions & 0 deletions Example/SwiftAudio/QueueView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// QueueView.swift
// SwiftAudio
//
// Created by David Chavez on 4/12/24.
//

import SwiftUI
import SwiftAudioEx

struct QueueView: View {
let controller = AudioController.shared
@Environment(\.dismiss) var dismiss

var body: some View {
NavigationStack {
VStack {
List {
if controller.player.currentItem != nil {
Section(header: Text("Playing Now")) {
QueueItemView(
title: controller.player.currentItem?.getTitle() ?? "",
artist: controller.player.currentItem?.getArtist() ?? ""
)
}
}
Section(header: Text("Up Next")) {
ForEach(controller.player.nextItems as! [DefaultAudioItem]) { item in
QueueItemView(
title: item.getTitle() ?? "",
artist: item.getArtist() ?? ""
)
}
}
}
}
.navigationTitle("Queue")
.toolbar {
Button("Close") {
dismiss()
}
}
}
}
}

struct QueueItemView: View {
let title: String
let artist: String

var body: some View {
VStack(alignment: .leading) {
Text(title)
.fontWeight(.semibold)
Text(artist)
.fontWeight(.light)
}
}
}


#Preview {
QueueView()
}

17 changes: 17 additions & 0 deletions Example/SwiftAudio/SwiftAudioApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// SwiftAudioApp.swift
// SwiftAudio
//
// Created by Brandon Sneed on 3/30/24.
//

import SwiftUI

@main
struct SwiftAudioApp: App {
var body: some Scene {
WindowGroup {
PlayerView()
}
}
}
Loading
Loading