SwiftUI Video player how to set url - swift

I am trying to create a view that allows me to send a URL of the video I want to play. My view looks like this:
import SwiftUI
import AVKit
struct PlayerView: View {
var videoURL : String
private let player = AVPlayer(url: URL(string: "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8")!)
var body: some View {
VideoPlayer(player: player)
.onAppear() {
// Start the player going, otherwise controls don't appear
player.play()
}
.onDisappear() {
// Stop the player when the view disappears
player.pause()
}
}
}
If try to set the URL to the passed parameter I get this error:
Cannot use instance member 'videoURL' within property initializer; property initializers run before 'self' is available
How do I pass aURL to the View to play different movies?
I changed my code as suggested and the video does not play:
import SwiftUI
import AVKit
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
}
print(url)
let player = AVPlayer(url: url)
self.player = player
self.player?.seek(to: CMTime.zero)
self.player!.play()
}
.onDisappear() {
// Stop the player when the view disappears
player?.pause()
}
}
}
I am trying to change the video on the timer. I tried this:
} else {
VideoPlayer(player: viewModel.player)
.onAppear {
viewModel.urlString = "https://testsite.neumont.edu/images/green.mp4"
}
.onReceive(movieSwitchTimer) { _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
viewModel.urlString = "https://testsite.neumont.edu/images/earth.mp4"
}
self.activeImageIndex = (self.activeImageIndex + 1) % slideStore.images.count
}
}

There are a number of ways to achieve this, but since you're already using onAppear, that seems like a good place to accomplish this:
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()
}
}
}
You'll want player to be #State because it should persist between renders of the view.
Update, based on comments, handling a scenario where the URL may change:
class VideoViewModel : ObservableObject {
#Published var urlString : String? {
didSet {
guard let urlString = urlString, let url = URL(string: urlString) else {
return
}
player = AVPlayer(url: url)
player.seek(to: .zero)
player.play()
}
}
var player = AVPlayer()
}
struct ContentView : View {
#StateObject var viewModel = VideoViewModel()
var body: some View {
VideoPlayer(player: viewModel.player)
.onAppear {
viewModel.urlString = "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8"
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
viewModel.urlString = Bundle.main.url(forResource: "IMG_0226", withExtension: "mp4")!.absoluteString
}
}
}
}
In the update, you can see that I store the player in an ObservableObject. Any time the url string gets changed, it reloads the player. The onAppear is not necessary in your code -- it's just a way to show loading different videos at different times (the first being the URL you provided, the second being a URL from my bundle).

Related

SwiftUI AVPlayer Error Reporting/Handling

I've created a media app, which plays music via HTTP live stream in SwiftUI. The code (snippet) looks like following:
struct PlayerView: View {
#State var audioPlayer: AVPlayer!
#State var buttonSymbol: String = "pause.circle.fill"
let url: URL!
var body: some View {
VStack {
Text("Sound")
HStack {
Button(action: {
if self.audioPlayer.rate != 0.0 {
self.buttonSymbol = "play.circle.fill"
self.audioPlayer.pause()
} else {
self.buttonSymbol = "pause.circle.fill"
self.audioPlayer.play()
}
}) {
Image(systemName: self.buttonSymbol)
}
.buttonStyle(PlainButtonStyle())
}
}
.onAppear {let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(AVAudioSession.Category.playback, mode: .default, policy: AVAudioSession.RouteSharingPolicy.longFormAudio, options: [])
} catch let error {
fatalError("*** Unable to set up the audio session: \(error.localizedDescription)")
}
let item = AVPlayerItem(url: self.url)
self.audioPlayer = AVPlayer(playerItem: item)
audioSession.activate(options: []) { (success, error) in
guard error == nil else {
fatalError("An error occurred: \(error!.localizedDescription)")
return
}
self.audioPlayer.play()
}
}
}
}
My lack of understanding is, how do I receive the underlying AVPlayer/AVPlayerItem errors (for instance the HTTP stream is interrupted) in SwiftUI in order to show them in the UI? Which SwiftUI action I have to use and how?
According to the documentation AVPlayerItem has a notification called AVPlayerItemFailedToPlayToEndTime.
One possible implementation in a Viewmodel would be:
import Combine
....
playerItemFailedToPlayToEndTimeObserver = NotificationCenter
.default
.publisher(for: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime)
.sink(receiveValue: {[weak self] notification in
// handle notification here
})
You would need to hold a reference to playerItemFailedToPlayToEndTimeObserver else it will go out of scope as soon as the method calling this finished.

VideoPlayer in SwiftUI stops playing when parent-View updates

Using Swift5.3.2, iOS14.4.1, Xcode 12.4,
I am successfully running a VideoPlayer in SwiftUI.
I am calling the Player view with this code: VideoPlayer(player: AVPlayer(url: url)).
The problem is that the video stops playing whenever a parent-View of the VideoPlayer updates (i.e. re-renders).
Since in SwiftUI I don't have any control over when such a re-render moment takes place, I don't know how to overcome this problem.
Any ideas ?
Here is the entire Code:
The VideoPlayer View is called as such:
struct MediaTabView: View {
#State private var url: URL
var body: some View {
// CALL TO VIDEOPLAYER IS HERE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
VideoPlayer(player: AVPlayer(url: url))
}
}
The MediaTabView is called as such:
import SwiftUI
struct PageViewiOS: View {
var body: some View {
ZStack {
Color.black
// CALL TO MEDIATABVIEW IS HERE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!d
MediaTabView(url: URL(string: "https://someurel.com"))
CloseButtonView()
}
}
}
The PageViewiOS View is called as such:
struct MainView: View {
#EnvironmentObject var someState: AppStateService
var body: some View {
NavigationView {
Group {
if someState = .stateOne {
// CALL TO PAGEVIEWIOS IS HERE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
PageViewiOS()
} else {
Text("hello")
}
}
}
}
}
This is in response to our comment thread on the other answer:
class PlayerViewModel: ObservableObject {
#Published var avPlayer: AVPlayer?
func loadFromUrl(url: URL) {
avPlayer = AVPlayer(url: url)
}
}
struct CustomPlayerView: View {
var url : URL
#StateObject private var playerViewModel = PlayerViewModel()
var body: some View {
ZStack {
if let avPlayer = playerViewModel.avPlayer {
VideoPlayer(player: avPlayer)
}
}.onAppear {
playerViewModel.loadFromUrl(url: url)
}
}
}
I'm not sure that this is definitively better, so it's worth testing. But, it does control when AVPlayer gets created and avoids re-creating PlayerViewModel on every render of the parent as well.
With the solution from #jnpdx, everything works now.
Here is the final solution (full credit to #jnpdx):
import SwiftUI
import AVKit
class PlayerViewModel: ObservableObject {
#Published var avPlayer: AVPlayer?
func loadFromUrl(url: URL) {
avPlayer = AVPlayer(url: url)
}
}
struct CustomPlayerView: View {
var url : URL
#StateObject private var playerViewModel = PlayerViewModel()
var body: some View {
ZStack {
if let avPlayer = playerViewModel.avPlayer {
VideoPlayer(player: avPlayer)
}
}.onAppear {
playerViewModel.loadFromUrl(url: url)
}
}
}
With that in hand, it is enough to call the CustomPlayerVideo like that:
CustomPlayerView(url: url)
Remark: I needed to use ZStack instead of Group in my CustomPlayerView in order for it to work.
I have found a solution.
Call the following :
CustomPlayerView(url: url)
...instead of :
VideoPlayer(player: AVPlayer(url: url))
Not sure why this works, tough.
Maybe somebody can explain further ?
Here is the CustomVideoPlayer code:
struct CustomPlayerView: View {
private let url: URL
init(url: URL) {
self.url = url
}
var body: some View {
VideoPlayer(player: AVPlayer(url: url))
}
}
With this minor change, the Video keeps on playing even tough the parent-View gets re-rendered. Still, not sure why ???
----------- Answer with the hint of #jnpdx --------
I changed the CustomVideoPlayer even more :
CustomPlayerView(playerViewModel: PlayerViewModel(avPlayer: AVPlayer(url: url)))
import SwiftUI
import AVKit
class PlayerViewModel: ObservableObject {
#Published var avPlayer: AVPlayer
init(avPlayer: AVPlayer) {
self.avPlayer = avPlayer
}
}
struct CustomPlayerView: View {
#StateObject var playerViewModel: PlayerViewModel
var body: some View {
VideoPlayer(player: playerViewModel.avPlayer)
}
}

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.

Can't pause AVPlayer with custom button

I am trying to make a detailed view in swift, but I just can't figure out a way to pause the video with a custom button. And also when I go back to my list I can still hear the video playing in the background. Here is my code for the AVPlayer and for the button.
import SwiftUI
import AVKit
struct Workdetail: View {
var work: WorkoutDe
#State var player = AVPlayer()
#State var isplaying = true
var body: some View {
VStack {
ZStack {
VideoPlayer(player: $player, work: work)
.frame(height: UIScreen.main.bounds.height / 3.5)
Butto(player: $player, isplaying: $isplaying)
}
Spacer()
}
}
}
struct Butto : View {
#Binding var player : AVPlayer
#Binding var isplaying : Bool
var body : some View {
Button(action: {
if self.isplaying {
self.player.pause()
self.isplaying = false
} else {
self.player.play()
self.isplaying = true
}
}) {
Image(systemName: self.isplaying ? "pause.fill" : "play.fill")
.font(.title)
.foregroundColor(.white)
.padding(20)
}
}
}
struct VideoPlayer : UIViewControllerRepresentable {
var work : WorkoutDe
#Binding var player : AVPlayer
var playerLayer = AVPlayerLayer()
public func makeUIViewController(context: Context) -> AVPlayerViewController {
player = AVPlayer(url: URL(fileURLWithPath: String(work.url)))
let controller = AVPlayerViewController()
controller.player = player
controller.videoGravity = .resizeAspectFill
player.actionAtItemEnd = .none
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: .main) { _ in
player.seek(to: CMTime.zero)
player.play()
}
player.play()
return controller
}
func rewindVideo(notification: Notification) {
playerLayer.player?.seek(to: .zero)
}
public func updateUIViewController(_ uiViewController: AVPlayerViewController, context: UIViewControllerRepresentableContext<VideoPlayer>) {
}
}
The AVPlayer works but when I press the button nothing happens. The image for the button changes but the video won't stop playing. Can someone please explain to me how I can bind the button, because I can't figure it out
You work with different players in your sub views, try
public func makeUIViewController(context: Context) -> AVPlayerViewController {
let player = AVPlayer(url: URL(fileURLWithPath: String(work.url)))
let controller = AVPlayerViewController()
DispatchQueue.main.async {
self.player = player
}
binding should update parent state, which will update Butto, so it should work.

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?