How can I update what video is playing on AVPlayerLayer in SwiftUI? - swift

I have a #State variable in my main ContentView called url that is the source to an mp4 file. How can I modify either PlayerView or VideoView (both below) so that when something causes url to change in ContentView, the VideoView updates itself to play the new video at url?
I feel like I am on the right track by adding a Coordinator in VideoView, but this is something I saw in Apple's tutorials, and I don't really understand how to use it.
PlayerView.swift
import UIKit
import AVKit
class PlayerView: UIView {
private let playerLayer = AVPlayerLayer()
private var playerLooper: AVPlayerLooper?
init(frame: CGRect, url: URL) {
super.init(frame: frame)
// Obtain asset and create an item from it
let asset = AVAsset(url: url)
let item = AVPlayerItem(asset: asset)
// Create the video player using the URL passed in.
let player = AVQueuePlayer()
// Add the player to our Player Layer
playerLayer.player = player
playerLayer.videoGravity = .resizeAspect // Resizes content to fill whole video layer.
playerLayer.backgroundColor = UIColor.black.cgColor
layer.addSublayer(playerLayer)
// Create new player looper
playerLooper = AVPlayerLooper(player: player, templateItem: item)
// Start the movie
player.volume = 0
player.play()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
}
VideoView
(wrapper)
import SwiftUI
import AVKit
struct VideoView: UIViewRepresentable {
#Binding var videoURL: URL
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UIView {
return PlayerView(frame: .zero, url: videoURL)
}
func updateUIView(_ playerView: UIView, context: Context) {
}
class Coordinator: NSObject {
var parent: VideoView
init(_ videoView: VideoView) {
self.parent = videoView
}
}
}

Related

SwiftUI pass parameter to Class from UIViewRepresentable

I have a question that I think is quite simple and yet I get stuck, so I come to ask for help and maybe it could be other people !
I want to loop a video, but i've problem, i need to pass a var to a struct then a class like :
struct FinalPreview: View {
var url: URL
PlayerView(url: url)
.aspectRatio(contentMode: .fill)
.frame(width: size.width, height: size.height)
.clipShape(RoundedRectangle(cornerRadius: 30, style: .continuous))
.onAppear{
if player.currentItem == nil {
let item = AVPlayerItem(url: url)
player.replaceCurrentItem(with: item)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
player.play()
})
}
}
struct PlayerView: UIViewRepresentable {
var url: URL
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PlayerView>) {
}
func makeUIView(context: Context) -> UIView {
return PlayerUIView(frame: .zero)
}
}
class PlayerUIView: UIView {
private let playerLayer = AVPlayerLayer()
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
super.init(frame: frame)
// Load the resource
let fileUrl = "MY URL VAR FROM PlayerView"
// Setup the player
let player = AVPlayer(url: fileUrl)
playerLayer.player = player
playerLayer.videoGravity = .resizeAspectFill
layer.addSublayer(playerLayer)
// Setup looping
player.actionAtItemEnd = .none
// Start the movie
player.play()
}
#objc
func playerItemDidReachEnd(notification: Notification) {
playerLayer.player?.seek(to: CMTime.zero)
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
}
It's to loop a video.
I'm also looking to hide AVPlayer controls by using :
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) {
}
}
To hide the controls, it works very well, I don't know how to combine both...
Thank you.
You actually don't need 3 different types, just a single UIViewControllerRepresentable with a url:
import AVKit
import SwiftUI
struct PlayerView: UIViewControllerRepresentable {
var url: URL
func makeUIViewController(context: Context) -> AVPlayerViewController {
// create a looping auto-starting video player:
let player = AVPlayer(url: url)
player.actionAtItemEnd = .none
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: .main) { [weak player] _ in
player?.seek(to: .zero)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak player] in
player?.play()
}
// use that player in a view controller:
let controller = AVPlayerViewController()
controller.player = player
controller.videoGravity = .resizeAspectFill
controller.showsPlaybackControls = false
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {}
}
struct PlayerView_Previews: PreviewProvider {
static var previews: some View {
PlayerView(url: someVideoURL) // replace the URL with your own
}
}

SwiftUI AVPlayer and AVPlayerLayer during view re-creation

I'm creating a Mac OS app, using SwiftUI.
I have a NSViewRepresentable that wraps the AVPlayer:
class PlaybackModel {
var player: AVPlayer!
var playerLayer: AVPlayerLayer!
func createPlayerLayer() -> AVPlayerLayer {
let result = AVPlayerLayer(player: self.player)
result.videoGravity = .resizeAspect
result.needsDisplayOnBoundsChange = true
result.backgroundColor = .black
self.playerLayer = result
}
}
struct PlayerViewAdapter: NSViewRepresentable {
let playbackModel: PlaybackModel!
func makeNSView(context: Context) -> BasePlayerView {
return BasePlayerView(playbackModel)
}
func updateNSView(_ nsView: BasePlayerView, context: Context) {
}
}
and the BasePlayerView uses the AVPlayerLayer as a backing layer:
class BasePlayerView: NSView {
private var playbackModel: PlaybackModel!
init(_ playbackModel: PlaybackModel) {
self.playbackModel = playbackModel
super.init(frame: .zero)
wantsLayer = true
}
override func makeBackingLayer() -> CALayer {
return playbackModel.playerLayer
}
}
Initially it works fine and I can see the video playing inside the view.
Alas, there seems to be an issue during SwiftUI re-render process, when the PlayerViewAdapter.makeNSView() method gets called again for the same PlaybackModel (due to state changes), the AVPlayerLayer is not rendered in the newly created BasePlayerView.
Do I need to perform some additional action during this re-creation of the player views?

Implementing UIView in SwiftUI

I am trying to implement a video player in SwiftUI via a UIView following this tutorial: https://medium.com/flawless-app-stories/avplayer-swiftui-b87af6d0553
Unfortunately the video is not displaying, I have AVFoundation imported so I am not sure of the issue. Has anyone found a solution to this?
Here are the classes I created:
class PlayerUIView: UIView {
private let playerLayer = AVPlayerLayer()
init(frame: CGRect, urlString: String) {
super.init(frame: frame)
guard let url = URL(string: urlString) else {
return
}
let player = AVPlayer(url: url)
player.play()
playerLayer.player = player
layer.addSublayer(playerLayer)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
}
struct VideoPlayerUIView: UIViewRepresentable {
var frame: CGRect
var urlString: String
func makeUIView(context: Context) -> some UIView {
return PlayerUIView(frame: self.frame, urlString: self.urlString)
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
}
Here I how I am using the implementation in SwiftUI:
var body: some View {
Text("Videos")
VideoPlayerUIView(frame: CGRect(x: 0, y: 0, width: 400, height: 300), urlString: urlString)
}
This was the output (no video)
enter image description here
You did everything right here.
The only thing that is causing error is how you are process the URL.
Try initialising the player like this instead
let player = AVPlayer(url: Bundle.main.url(forResource: urlString, withExtension: "mp4")!)
Change the extension according to the format you are using. Remember, here I am force wrapping the optional. So, if you give a wrong urlString or have a video extension other than mp4, the app will crash

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.

How can I play a video using AVPlayer?

Apple Swift version 3.0.2 (swiftlang-800.0.63 clang-800.0.42.1)
I would like to play a video from internet using AVPlayer.
But, a error occurred "super.init(frame: frame)" in AVPlayer.swift of below source list.
Thread 1:EXC_BAD_ACCESS (code=2, address=0x16fc07fe0)
How can I play a video using AVPlayer?
If there is wrong point except point of error, let me know it, too.
AVPlayerView.swift
import Foundation
import UIKit
final class AVPlayerView : UIView {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
override init(frame: CGRect) {
super.init(frame: frame)
}
override public class var layerClass: Swift.AnyClass {
get {
return AVPlayerView.self
}
}
}
ViewController.swift
import UIKit
import AVFoundation
import CoreMedia
class ViewController: UIViewController {
var playerItem : AVPlayerItem!
var videoPlayer : AVPlayer!
override func viewDidLoad() {
super.viewDidLoad()
let url:NSURL = NSURL(string: "https://aaa.com/test.m3u8")!
let avAsset = AVURLAsset(url: url as URL, options: nil)
playerItem = AVPlayerItem(asset: avAsset)
videoPlayer = AVPlayer(playerItem: playerItem)
print(self.view.bounds)
let videoPlayerView = AVPlayerView(frame: self.view.bounds)
let layer = videoPlayerView.layer as! AVPlayerLayer
layer.videoGravity = AVLayerVideoGravityResizeAspect
layer.player = videoPlayer
self.view.layer.addSublayer(layer)
videoPlayer.play()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Here is code to play video:
let videoURL = NSURL(string: "PUT_YOUR_PROPER_URL")
let playerAV = AVPlayer(url: videoURL! as URL)
let playerLayerAV = AVPlayerLayer(player: playerAV)
playerLayerAV.frame = self.view.bounds
self.view.layer.addSublayer(playerLayerAV)
playerAV.play()
AVPlayer support below extension:
public.mpeg
public.mpeg-2-video
public.avi
public.aifc-audio
public.aac-audio
public.mpeg-4
public.au-audio
public.aiff-audio
public.mp2
public.3gpp2
public.ac3-audio
public.mp3
public.mpeg-2-transport-stream
public.3gpp
public.mpeg-4-audio
Make sure you can use supported video in AVPlayer.