Audio keeps playing after AVPlayer is removed from VStack (SwiftUI) - ios

I have the following VStack that contains an AVPlayer (in PlayerView):
struct ContentView: View {
#State var url: URL?
private let openFile = NotificationCenter.default.publisher(for: .openFile)
var body: some View {
VStack {
if isVideo(self.url!) {
PlayerView(url: self.url!)
} else {
Image(nsImage: NSImage(contentsOf: self.url!)!).resizable().aspectRatio(contentMode: .fit)
}
}.onReceive(openFile) { notification in
self.url = notification.object as? URL
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
And this is the PlayerView:
struct PlayerView: NSViewRepresentable {
private var url: URL
init(url: URL) {
self.url = url
}
func updateNSView(_ nsView: PlayerNSView, context _: NSViewRepresentableContext<PlayerView>) {
nsView.play(url: url)
}
func makeNSView(context _: Context) -> PlayerNSView {
PlayerNSView(frame: .zero)
}
func dismantleNSView(coordinator _: Coordinator) {
// not called
}
}
After updating from a video to a image the audio of the video keeps playing for a few seconds.
Where can I tell the AVPlayer to pause? Does the VStack notify VideoPlayer?
Updated with code from iUrii:
struct ContentView: View {
#State var url: URL?
#State var playerView: PlayerView?
private let openFile = NotificationCenter.default.publisher(for: .openFile)
var body: some View {
VStack {
if let view = playerView {
view
} else {
Image(nsImage: NSImage(contentsOf: self.url!)!).resizable().aspectRatio(contentMode: .fit)
}
}.onReceive(openFile) { notification in
url = notification.object as? URL
if playerView != nil {
playerView!.player.pause()
playerView = nil
}
if videoExtensions.contains(url!.pathExtension.lowercased()) {
playerView = PlayerView(url: url!)
}
}
}
}
This works great when going video to image to video.
When I go video to video, the first video will be paused and audio from the second video starts playing, it is not shown though (the first video is still visible).
I solved it by updating the player with playerView.player.replaceCurrentItem(with: playerItem) instead of replacing the view.

You should manage your PlayerNSView with AVPlayer manually if you want to control its behaviour e.g.:
struct PlayerView: NSViewRepresentable {
let player: AVPlayer
init(url: URL) {
self.player = AVPlayer(url: url)
}
func updateNSView(_ nsView: AVPlayerView, context: NSViewRepresentableContext<Self>) {
}
func makeNSView(context: NSViewRepresentableContext<Self>) -> AVPlayerView {
let playerView = AVPlayerView(frame: .zero)
playerView.player = player
return playerView
}
}
struct ContentView: View {
#State var playerView: PlayerView?
var body: some View {
VStack {
if let view = playerView {
view
} else {
Image(nsImage: NSImage(named: "iphone12")!)
}
Button("Toogle") {
if playerView == nil {
let url = URL(string: "https://www.apple.com/105/media/us/iphone-12-pro/2020/e70ffbd8-50f1-40f3-ac36-0f03a15ac314/films/product/iphone-12-pro-product-tpl-us-2020_16x9.m3u8")!
playerView = PlayerView(url: url)
playerView?.player.play()
}
else {
playerView?.player.pause()
playerView = nil
}
}
}
}
}

Related

How to control AVPlayer playback in SwiftUI

I am trying to play music on my app and manage to play/stop the music from the app's settings.
First I am creating an ObservableObject class called MusicPlayer:
class MusicPlayer: ObservableObject {
#Published var isPlaying = AppDefaults.shared.isMusicPlaying()
#Published var music : AVAudioPlayer? = nil
func playMusic() {
guard let strFilePath = Bundle.main.path(forResource: "music", ofType: "mp3") else { return }
do {
music = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: strFilePath))
} catch {
print(error)
}
music?.volume = 0.60
music?.numberOfLoops = -1
if isPlaying {
music?.play()
} else {
music?.stop()
}
}
}
and then play the music in main app file:
#main
struct AppName: App {
#StateObject private var player = MusicPlayer()
var body: some Scene {
WindowGroup {
ContentsView()
.onAppear {
player.playMusic()
}
}
}
}
and then trying to stop/play the music using toggle from settings:
struct SettingsView: View {
#StateObject private var player = MusicPlayer()
var body: some View {
Toggle("Music", isOn: $player.isPlaying)
.onChange(of: player.isPlaying, perform: { _ in
AppDefaults.shared.setMusic(player.isPlaying)
if player.isPlaying {
player.music?.stop()
} else {
player.music?.play()
}
})
}
}
now the problem is switching to on or off doesn't change the state of playing. How can I fix this issue?
The issue here is you are initializing your Viewmodel twice. So you have 2 different sources of truth. So there are 2 different AVAudioPlayer.
Solution: Create one single instance in the top View and pass this on to the views that need this.
As you decided to omit how SettingsView correlate with the other Views I can only give a more general solution.
Let´s asume the SettingsView is used in AppName:
#StateObject private var player = MusicPlayer()
WindowGroup {
....(ContentView stuff)
SettingsView()
// pass the observableObject on to the SettingsView and its children
.environmentObject(player)
}
Then in SettingsView:
struct SettingsView: View {
// get the observableObject from the environment
#EnvironmentObject private var player: MusicPlayer
var body: some View {
Toggle("Music", isOn: $player.isPlaying)
.onChange(of: player.isPlaying, perform: { _ in
AppDefaults.shared.setMusic(player.isPlaying)
if player.isPlaying {
player.music?.stop()
} else {
player.music?.play()
}
})
}
}

Show activity indicator and error while loading from url AVPlayer swiftui

I was trying to load short videos from a URL and I found the best way is to use AVPlayer, I found the code below but it didn't show the activity indicator also if the user doesn't have an internet it won't play the video. Thanks in advance.
import SwiftUI
import AVKit
struct ContentView: View {
#State var player = AVPlayer(url: URL(string: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4")!)
#State var isplaying = false
#State var showcontrols = false
#State var value : Float = 0
var body: some View {
VStack{
ZStack{
VideoPlayer(player: $player)
if self.showcontrols{
Controls(player: self.$player, isplaying: self.$isplaying, pannel: self.$showcontrols,value: self.$value)
}
}
.frame(height: UIScreen.main.bounds.height / 3.5)
.onTapGesture {
self.showcontrols = true
}
GeometryReader{_ in
VStack{
Text("Custom Video Player").foregroundColor(.white)
}
}
}
.background(Color.black.edgesIgnoringSafeArea(.all))
.onAppear {
self.player.play()
self.isplaying = true
}
}
}
This is the main screen which is ContentView
struct Controls : View {
#Binding var player : AVPlayer
#Binding var isplaying : Bool
#Binding var pannel : Bool
#Binding var value : Float
var body : some View{
VStack{
Spacer()
HStack{
Button(action: {
if self.isplaying{
self.player.pause()
self.isplaying = false
}
else{
self.player.play()
self.isplaying = true
}
if self.player.currentItem?.duration.seconds == self.player.currentTime().seconds{
print("did it")
self.player.seek(to: CMTime.zero)
self.player.play()
self.isplaying = true
}
}) {
Image(systemName: self.isplaying ? "pause.fill" : "play.fill")
.font(.title)
.foregroundColor(.white)
.padding(20)
}
}
}.padding()
.onTapGesture {
self.pannel = false
}
}
func getSliderValue()->Float{
return Float(self.player.currentTime().seconds / (self.player.currentItem?.duration.seconds)!)
}
func getSeconds()->Double{
return Double(Double(self.value) * (self.player.currentItem?.duration.seconds)!)
}
}
The button for control the video.
class Host : UIHostingController<ContentView>{
override var preferredStatusBarStyle: UIStatusBarStyle{
return .lightContent
}
}
struct VideoPlayer : UIViewControllerRepresentable {
#Binding var player : AVPlayer
func makeUIViewController(context: UIViewControllerRepresentableContext<VideoPlayer>) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = player
controller.showsPlaybackControls = false
controller.videoGravity = .resize
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: UIViewControllerRepresentableContext<VideoPlayer>) {
}
}

Downloading video with alamofire not working

I have a list of URLMedia items, which can be a video or Image from and URL in the internet. I want to show to the user, and for improving the user experience, I want to catch the media locally, so the user can see the next media item immediately without needed to download it.
The interface is ready for testing the functionality. You have the media viewer, and down 2 buttons: 1 for go to next media, one for catch the data. There are 4 items as example, 2 videos 2 images, and you can go in loop over the 4 items over and over.
So far the functionality for the photos works fine, you can perceive the speed difference between going to next media before catching the data and after catching it, is immediate. The problem comes with the video, it does not work, and I can't figure out why. There are some limitations about the tempo folder I am unaware of?
This is the main view
struct ContentView: View {
#StateObject private var AS: AppState = AppState.singleton
func downloadUsingAlamofire(urlInput: URL, index: Int) {
AF.download(urlInput).responseURL { response in
// Read file from provided file URL.
AS.models[index].URLCachedMedia = response.fileURL!.absoluteString
print("Done downloading: \(AS.getCurrentModel().URLCachedMedia)")
print("In task: \(index)")
}
}
var body: some View {
VStack {
switch AS.currentType {
case .image:
AsyncImage(url: URL(string: AS.getCurrentModel().URLCachedMedia))
case .video:
PlayerView(videoURL: AS.getCurrentModel().URLCachedMedia)
}
HStack (spacing: 30) {
Button("Next model") {
AS.nextModel()
print("The URL of the model is: \(AS.getCurrentModel().URLCachedMedia)")
}
Button("Cach media") {
for (index, model) in AS.models.enumerated() {
downloadUsingAlamofire(urlInput: URL(string: model.URLMedia)!, index: index)
}
}
}
}
}
}
Video player view, optimise to auto-play when the video is ready:
struct PlayerView: View {
var videoURL : String
#State private var player : AVPlayer?
var body: some View {
VideoPlayer(player: player)
.onAppear() {
// Start the player going, otherwise controls don't appear
guard let url = URL(string: videoURL) else {
return
}
let player = AVPlayer(url: url)
self.player = player
player.play()
}
.onDisappear() {
// Stop the player when the view disappears
player?.pause()
}
}
}
This is the State of the app
class AppState: ObservableObject {
static let singleton = AppState()
private init() {}
#Published var models: [Model] = getModels()
#Published var currentModel: Int = 0
#Published var currentType: TypeMedia = getModels()[0].type
func nextModel() {
if currentModel < models.count - 1 {
currentModel += 1
} else {
currentModel = 0
}
currentType = getCurrentModel().type
}
func getCurrentModel() -> Model {
return models[currentModel]
}
}
And this is the model, with the demo data to test
enum TypeMedia {
case image, video
}
class Model: ObservableObject {
let URLMedia: String
#Published var URLCachedMedia: String
let type: TypeMedia
init(URLMedia: String, type: TypeMedia) {
self.URLMedia = URLMedia
self.type = type
self.URLCachedMedia = self.URLMedia
}
}
func getModels() -> [Model] {
let model1 = Model(URLMedia: "https://storage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", type: .video)
let model2 = Model(URLMedia: "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4", type: .video)
let model3 = Model(URLMedia: "https://i0.wp.com/www.wikiwiki.in/wp-content/uploads/2021/09/Palki-Sharma-Upadhyay-Journalist-4.jpg?resize=761.25%2C428&ssl=1", type: .image)
let model4 = Model(URLMedia: "https://static.dw.com/image/60451375_403.jpg", type: .image)
return [model1, model3, model2, model4]
}

SwiftUI: Play video only if it's in the centre of the screen

Hi am making app which plays video only If it's in the centre of the view. I've already managed to get the position in the scroll view, but I can't combine that with playing the video.
This is my main view:
struct MainView: View {
#State var position = 0.0
var body: some View {
ScrollView {
ForEach(videos){ video in
VideoView(player: video.player)
.onChange(of: position) { pos in
if pos > -50 && pos < 400 {
print("Play video")
}else {
print("Stop video")
}
}
}
.background(GeometryReader {
Color.clear.preference(key: ViewOffsetKey.self, value: -$0.frame(in: .named("scroll")).origin.y)
})
}.onPreferenceChange(ViewOffsetKey.self) {
position = $0
}
.coordinateSpace(name: "scroll")
.padding()
}
}
This is my video model:
struct VideoModel: Identifiable {
var id = UUID()
var number: Int
var player: AVPlayer
}
This is video array:
let videos = [
VideoModel(number: 1, player: AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "video", ofType: "mp4")!))),
VideoModel(number: 3, player: AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "video", ofType: "mp4")!))),
VideoModel(number: 4, player: AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "video", ofType: "mp4")!))),
VideoModel(number: 5, player: AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "video", ofType: "mp4")!)))
]
And those are structures handling the video player and preference key:
struct ViewOffsetKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
struct VideoView: View {
var player: AVPlayer
var body: some View {
AVPlayerControllerRepresented(player: player)
.frame(height: height)
}
}
struct AVPlayerControllerRepresented : UIViewControllerRepresentable {
var player : AVPlayer
func makeUIViewController(context: Context) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = player
controller.showsPlaybackControls = false
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
}
}
Please help, I will be so thankful.
First You need to make the AVPlayer a Binding for the changes in VideoView & AVPlayerControllerRepresented to take effect, then add those pieces of code accordingly
player.play() // to play
player.stop() // to stop

How to manage AVPlayer state in SwiftUI

I have a list of URLs in SwiftUI. When I tap an item, I present a full screen video player. I have an #EnvironmentObject that handles some viewer options (for example, whether to show a timecode). I also have a toggle that shows and hides the timecode (I've only included the toggle in this example as the timecode view doesn't matter) but every time I change the toggle the view is created again, which re-sets the AVPlayer. This makes sense since I'm creating the player in the view's initialiser.
I thought about creating my own ObserveredObject class to contain an AVPlayer but I'm not sure how or where I'd initialise it since I need to give it a URL, which I only know from the initialiser of CustomPlayerView. I also thought about setting the player as an #EnvironmentObject but it seems weird to initialise something I might not need (if the user doesn't tap on a URL to start the player).
What is the correct way to create an AVPlayer to hand to AVKit's VideoPlayer please? Here's my example code:
class ViewerOptions: ObservableObject {
#Published var showTimecode = false
}
struct CustomPlayerView: View {
#EnvironmentObject var viewerOptions: ViewerOptions
private let avPlayer: AVPlayer
init(url: URL) {
avPlayer = AVPlayer(url: url)
}
var body: some View {
HStack {
VideoPlayer(player: avPlayer)
Toggle(isOn: $viewerOptions.showTimecode) { Text("Show Timecode") }
}
}
}
There are a couple of approaches you can take here. You can try them out and see which one suits best for you.
Option 1: As you said you can wrap avPlayer in a new ObserveredObject class
class PlayerViewModel: ObservableObject {
#Published var avPlayer: AVPlayer? = nil
}
class ViewerOptions: ObservableObject {
#Published var showTimecode = false
}
#main
struct DemoApp: App {
var playerViewModel = PlayerViewModel()
var viewerOptions = ViewerOptions()
var body: some Scene {
WindowGroup {
CustomPlayerView(url: URL(string: "Your URL here")!)
.environmentObject(playerViewModel)
.environmentObject(viewerOptions)
}
}
}
struct CustomPlayerView: View {
#EnvironmentObject var viewerOptions: ViewerOptions
#EnvironmentObject var playerViewModel: PlayerViewModel
init(url: URL) {
if playerViewModel.avPlayer == nil {
playerViewModel.avPlayer = AVPlayer(url: url)
} else {
playerViewModel.avPlayer?.pause()
playerViewModel.avPlayer?.replaceCurrentItem(with: AVPlayerItem(url: url))
}
}
var body: some View {
HStack {
VideoPlayer(player: playerViewModel.avPlayer)
Toggle(isOn: $viewerOptions.showTimecode) { Text("Show Timecode") }
}
}
}
Option 2: You can add avPlayer to your already existing class ViewerOptions as an optional property and then initialise it when you need it
class ViewerOptions: ObservableObject {
#Published var showTimecode = false
#Published var avPlayer: AVPlayer? = nil
}
struct CustomPlayerView: View {
#EnvironmentObject var viewerOptions: ViewerOptions
init(url: URL) {
if viewerOptions.avPlayer == nil {
viewerOptions.avPlayer = AVPlayer(url: url)
} else {
viewerOptions.avPlayer?.pause()
viewerOptions.avPlayer?.replaceCurrentItem(with: AVPlayerItem(url: url))
}
}
var body: some View {
HStack {
VideoPlayer(player: viewerOptions.avPlayer)
Toggle(isOn: $viewerOptions.showTimecode) { Text("Show Timecode") }
}
}
}
Option 3: Make your avPlayer a state object this way its memory will be managed by the system and it will not re-set it and keep it alive for you until your view exists.
class ViewerOptions: ObservableObject {
#Published var showTimecode = false
}
struct CustomPlayerView: View {
#EnvironmentObject var viewerOptions: ViewerOptions
#State private var avPlayer: AVPlayer
init(url: URL) {
_avPlayer = .init(wrappedValue: AVPlayer(url: url))
}
var body: some View {
HStack {
VideoPlayer(player: avPlayer)
Toggle(isOn: $viewerOptions.showTimecode) { Text("Show Timecode") }
}
}
}
Option 4: Create your avPlayer object when you need it and forget it (Not sure this is the best approach for you but if you do not need your player object to perform custom actions then you can use this option)
class ViewerOptions: ObservableObject {
#Published var showTimecode = false
}
struct CustomPlayerView: View {
#EnvironmentObject var viewerOptions: ViewerOptions
private let url: URL
init(url: URL) {
self.url = url
}
var body: some View {
HStack {
VideoPlayer(player: AVPlayer(url: url))
Toggle(isOn: $viewerOptions.showTimecode) { Text("Show Timecode") }
}
}
}

Resources