Cannot assign to property: 'self' is immutable in UIViewControllerRepresentable - swift

I have the following code snippet:
struct player : UIViewControllerRepresentable {
var url : String
var player1: AVPlayer
func makeUIViewController(context: UIViewControllerRepresentableContext<player>) -> AVPlayerViewController {
let controller = AVPlayerViewController()
player1 = AVPlayer(url: URL(string: url)!)
controller.player = player1
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: UIViewControllerRepresentableContext<player>) {
}
func pause() {
player1.pause()
}
}
This gives the error:
'Cannot assign to property: 'self' is immutable'
I need to have the AVPlayer outside of the makeUIViewController function because I need to access it from the pause function. How can I do this?

The error you are seeing appears due to the fact that structs are value types and any method on them that changes their properties needs to be marked as mutating. Unfortunately you cannot mark makeUIViewController because it is defined in UIViewControllerRepresentable protocol, but there is a fairly easy solution.
You only actually use the url to construct an AVPlayer - there is no need to hold on to it. Write and initializer for your struct that takes a url string and constructs an AVPlayer. I have made the AVPlayer optional as URL(string: String) returns an Optional URL (as you can imagine not all strings are valid urls). The below code works as expected:
struct Player: UIViewControllerRepresentable {
var player1: AVPlayer?
public init(url string: String) {
guard let url = URL(string: string) else {
self.player1 = nil
return
}
self.player1 = AVPlayer(url: url)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<Player>) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = player1
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController,
context: UIViewControllerRepresentableContext<Player>) {}
func pause() {
player1?.pause()
}
}
A side note: all type names in Swift (classes, structs, enums) are by convention upper-cased: your struct should be called Player not player.
You should also look into Cordinator for UIViewControllerRepresentable - you need something to act as your AVPlayerViewControllerDelegate.

There are several possibilities to choose from (actually it is not a complete list of options, but hope it would be helpful)
Option1: Do it in initailizer
struct player : UIViewControllerRepresentable {
private var url : String
private var player1: AVPlayer
init(url: String) {
self.url = url
self.player1 = AVPlayer(url: URL(string: url)!)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<player>) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = player1
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: UIViewControllerRepresentableContext<player>) {
}
}
Option2: Do it on first request to update controller
struct player : UIViewControllerRepresentable {
var url : String
var player1: AVPlayer
func makeUIViewController(context: UIViewControllerRepresentableContext<player>) -> AVPlayerViewController {
return AVPlayerViewController()
}
func updateUIViewController(_ playerController: AVPlayerViewController, context: UIViewControllerRepresentableContext<player>) {
if playerController.player == nil {
playerController.player = AVPlayer(url: URL(string: url)!)
}
}
}
Option3: If you need to modify player1 during lifetime, then you need to keep it in some external entity, say ObservableObject, because your player is View struct and cannot modify itself, actually what is said in compiler error.

Related

AVPlayer audio only works when ringer is on

With SwiftUI, I have a custom avplayer that auto plays and loops the video. The problem is whether or not I specifically tell avplayer to unmute, it is still muted. The physical volume buttons have no effect. The only way to toggle mute on/off is to physically switch the ringer to silent (muted) or not silent (unmute).
Here is the parent view:
struct VideoCacheView: View {
#State private var avPlayer: AVPlayer? = nil
public let url: String
public let thumbnailURL: String
var body: some View {
if self.avPlayer != nil {
CustomVideoPlayer(player: Binding(self.$avPlayer)!)
.onAppear {
self.avPlayer?.isMuted = false
self.avPlayer?.play()
}
}
}
}
and the child:
struct CustomVideoPlayer: UIViewControllerRepresentable {
#EnvironmentObject var cvm: CameraViewModel
#Binding var player: AVPlayer
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIViewController(context: Context) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = self.player
controller.showsPlaybackControls = false
controller.videoGravity = self.cvm.videoGravity
player.actionAtItemEnd = .none
NotificationCenter.default.addObserver(context.coordinator, selector: #selector(context.coordinator.restartPlayback), name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) { }
class Coordinator: NSObject {
public var parent: CustomVideoPlayer
init(_ parent: CustomVideoPlayer) {
self.parent = parent
}
#objc func restartPlayback () {
self.parent.player.seek(to: .zero)
}
}
}
Why is the only volume control my avplayer has is with the physicaly silent switch?
https://developer.apple.com/documentation/avfoundation/avplayer/1390127-volume
Turns out that the volume is set to 0.0 when ringer is in silent mode. By setting the volume to 1.0 by default, there is volume all the time.
Added this:
self.player?.volume = 1.0
inside of the child view below the videoGravity line

Swiftui avplayer handle stop video

i am making avplayer with swiftui. With uiviewcontrollerrepresenta what I need is whether the video being played is finished or not, if it is done I need to take action accordingly.
struct VideoPlayerSwiftUI : UIViewControllerRepresentable {
var playerURL : String
#State var status : AVPlayer.Status
var videoGravity : AVLayerVideoGravity
let avPlayerController = AVPlayerViewController()
func makeUIViewController(context: Context) -> AVPlayerViewController {
let player = AVPlayer(url: URL(string: playerURL)!)
avPlayerController.player = player
avPlayerController.showsPlaybackControls = false
avPlayerController.player?.play()
avPlayerController.player?.actionAtItemEnd = .pause
avPlayerController.videoGravity = videoGravity
self.addPeriodicTimeObserver()
return avPlayerController
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
}
}

how to capture the keydown event of the AVPlayer?

I'm writing a video player using the AVkit under macOS, the AVPlayer can response to the keyboard event, for example, space key down to pause/play the video, left arrow key down to move back the video.
I want to capture the keydown event, so that I can do more controls to the AVPlayer, how to do it?
the following is my sample code:
import SwiftUI
import AVKit
import AVFoundation
public class VideoItem: ObservableObject {
#Published var player: AVPlayer = AVPlayer()
#Published var playerItem: AVPlayerItem?
func open(_ url: URL) {
let asset = AVAsset(url: url)
let playerItem = AVPlayerItem(asset: asset)
self.playerItem = playerItem
player.replaceCurrentItem(with: playerItem)
}
}
public struct PlayerView: NSViewRepresentable {
#Binding var player: AVPlayer
var titles:[Title] = []
public init(player:Binding<AVPlayer>,currentSeconds:Binding<Double>,subtitleFile:Binding<String>,currentTitle:Binding<Title?>)
{
self._player = player
}
public func updateNSView(_ NSView: NSView, context: NSViewRepresentableContext<PlayerView>) {
guard let view = NSView as? AVPlayerView else {
debugPrint("unexpected view")
return
}
// status = player.timeControlStatus.rawValue
view.player = player
}
public func makeNSView(context: Context) -> NSView {
let av = AVPlayerView(frame: .zero)
return av
}
}
I'v got a solution, to capture the keydown event, We can create a subclass from AVPlayerView and override it's keydown event.

swiftui dealloc and realloc AVplayer with many videos

I have many simultaneous videos. Through a Int var (var test1 and var test2) I would like to be able to add only a certain video and remove all the others so as not to have memory problems
As soon as the view is loaded, the value "nil" is assigned to each player and when test1 == test 2 it should load the video in a certain player and in the other "nil"
The problem is that despite being the testing variable in binding (struct VideoPlayer #Binding var testing) it does not update the state of the player which always remains in Nil
Below the best solution I have obtained so far
Some idea? Thank you all
struct CustomPlayer: View {
#Binding var test1:Int
#Binding var test2:Int
#State var path:String
#State var testing:AVPlayer? = nil
var body: some View {
if(test1 == test2 ) {
self.testing? = AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "\(path)", ofType: "mp4")!) )
self.testing?.play()
} else {
self.testing?.replaceCurrentItem(with: nil)
}
return ZStack{
VideoPlayer(testing: self.$testing)
}
struct VideoPlayer : UIViewControllerRepresentable {
#Binding var testing : AVPlayer?
func makeUIViewController(context: UIViewControllerRepresentableContext<VideoPlayer>) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = testing
controller.showsPlaybackControls = false
controller.view.backgroundColor = UIColor.white
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: UIViewControllerRepresentableContext<VideoPlayer>) {
}
}
Here an example how to load multiple videos simultaneous (i added an autoplay feuature), and remove all other, but not the selected one.
To remove videos tap on video you want to save
And i'm not sure that i solve your initial problem, but this can give you a clue where to look next
Code can be copy/pasted, as i declare it in one file to convenient stackoverflow use
import SwiftUI
import AVKit
struct VideoModel: Identifiable {
let id: Int
let name: String
let type: String = ".mp4"
}
final class VideoData: ObservableObject {
#Published var videos = [VideoModel(id: 100, name: "video"),
VideoModel(id: 101, name: "wow"),
VideoModel(id: 102, name: "okay")]
}
//Multiple item player
struct MultipleVideoPlayer: View {
#EnvironmentObject var userData: VideoData
var body: some View {
VStack(alignment: .center, spacing: 8) {
ForEach(userData.videos) { video in
VideoPlayer(video: .constant(video))
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 250, maxHeight: 250, alignment: .center)
}
}
}
}
//Single item player
struct VideoPlayer : UIViewControllerRepresentable {
#EnvironmentObject var userData: VideoData
#Binding var video: VideoModel
func makeUIViewController(context: Context) -> AVPlayerViewController {
guard let path = Bundle.main.path(forResource: video.name, ofType: video.type) else {
fatalError("\(video.name)\(video.type) not found")
}
let url = URL(fileURLWithPath: path)
let playerItem = AVPlayerItem(url: url)
context.coordinator.player = AVPlayer(playerItem: playerItem)
context.coordinator.player?.isMuted = true
context.coordinator.player?.actionAtItemEnd = .none
NotificationCenter.default.addObserver(context.coordinator,
selector: #selector(Coordinator.playerItemDidReachEnd(notification:)),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: context.coordinator.player?.currentItem)
let controller = AVPlayerViewController()
controller.player = context.coordinator.player
controller.showsPlaybackControls = false
controller.view.backgroundColor = UIColor.white
controller.delegate = context.coordinator
let tapRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.playerDidTap))
controller.view.addGestureRecognizer(tapRecognizer)
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
uiViewController.player?.play()
}
func makeCoordinator() -> Coordinator {
let coord = Coordinator(self)
return coord
}
class Coordinator: NSObject, AVPlayerViewControllerDelegate {
var parent: VideoPlayer
var player: AVPlayer?
init(_ playerViewController: VideoPlayer) {
self.parent = playerViewController
}
#objc func playerItemDidReachEnd(notification: NSNotification) {
if let playerItem: AVPlayerItem = notification.object as? AVPlayerItem {
playerItem.seek(to: CMTime.zero, completionHandler: nil)
}
}
#objc func playerDidTap(){
parent.userData.videos = parent.userData.videos.filter { videoItem in
return videoItem.id == parent.video.id
}
}
}
}
//preview
struct AnotherEntry_Previews: PreviewProvider {
static var previews: some View {
MultipleVideoPlayer()
}
}
And in 'SceneDelegate.swift' replace app entry point with
window.rootViewController = UIHostingController(rootView: MultipleVideoPlayer().environmentObject(VideoData()))
The key thing of this move is to have "VideoData" resource, you can achieve it with EnvironmentObject, or with some other shared data example
Videos below i added to project, including them to Target Membership of a project
#Published var videos = [VideoModel(id: 100, name: "video"),
VideoModel(id: 101, name: "wow"),
VideoModel(id: 102, name: "okay")]
Your code seems to be ok. Are you sure the AVPlayer(...) path is correct
and testing is not nil. Put
.....
self.testing?.play()
print("-----> testing: \(testing.debugDescription)")
is testing = nil at that point?

Embedding videos in a SwiftUI view

My main goal is to embed a GIF/MP4 file in a view in a HStack/VStack using SwiftUI. I understand the code needs to conform to the 'Representable' protocols. My first attempt at doing this is below:
import SwiftUI
import AVKit
struct GIF : UIViewControllerRepresentable {
let url_name: String
func makeUIViewController(context: UIViewControllerRepresentableContext<GIF>) -> AVPlayerViewController {
return AVPlayerViewController()
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: UIViewControllerRepresentableContext<GIF>) {
let url = URL(string: url_name)!
let player = AVPlayer(url: url)
let vc = AVPlayerViewController()
vc.player = player
//PROBLEM
self.present(vc, animated: true){
vc.player?.play()
}
}
}
The problem is that I don't think 'self' makes any sense here and we need to refer to a view. How would we do this? We can't just create a view in a story board as I want to integrate the view controller with SwiftUI.
Any help would be appreciated.
P.S. This is my first iOS app so if I'm missing something very obvious here please try to be kind!
it's easy to wrap old AVPlayerViewController
import AVKit
import SwiftUI
struct AVPlayerView: UIViewControllerRepresentable {
#Binding var videoURL: URL?
private var player: AVPlayer? {
guard let url = videoURL else {
return nil
}
return AVPlayer(url: url)
}
func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) {
playerController.player = player
playerController.player?.play()
}
func makeUIViewController(context: Context) -> AVPlayerViewController {
AVPlayerViewController()
}
}
but if ur target is equal or higher than 14 u can use VideoPlayer component from swiftui without any wrappers
import SwiftUI
import AVKit
struct UrView: View {
var body: some View {
VideoPlayer(
player: AVPlayer(url: URL(string: "{ur url}"))
)
}
}