I've been playing around with SwiftUI since a few days and I am currently trying to implement a video player, I am struggling with a size issue.
This is my view code:
struct test: View {
var body: some View {
PlayerView()
.aspectRatio(contentMode: .fit)
.background(Color.blue)
}
}
This is what's inside PlayerView()
class PlayerUIView: UIView {
private let playerLayer = AVPlayerLayer()
override init(frame: CGRect) {
super.init(frame: frame)
let url = URL(string: "http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8")!
let player = AVPlayer(url: url)
player.isMuted = true
player.play()
playerLayer.player = player
playerLayer.videoGravity = AVLayerVideoGravity.resizeAspect
layer.addSublayer(playerLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
}
Using this code I obtain this:
There is this extra border (that we can see in blue) that I want to get rid of and I don't know how to do it.
I've tried to set the playerLayer's videoGravity property to :
resize: I don't have the border anymore but the video is stretched
resizeAspectFill: I don't have the border anymore but the video is cut
How can I get rid of those borders while preserving the original ratio and size provided by the video?
I managed to make it work this way:
View Code
struct test: View {
var body: some View {
PlayerView()
.edgesIgnoringSafeArea(.all)
.scaledToFill()
}
}
PlayerUIView.swift:
class PlayerUIView: UIView {
private let playerLayer = AVPlayerLayer()
override init(frame: CGRect) {
super.init(frame: frame)
let url = URL(string: "http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8")!
let player = AVPlayer(url: url)
player.isMuted = true
player.play()
playerLayer.player = player
playerLayer.videoGravity = AVLayerVideoGravity.resizeAspect
layer.addSublayer(playerLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
}
Related
I have a navigation view from a list of video urls to a destination Videoview and then to a video player. The player uses just the AVPlayer (I don't need controls) to automatically play the video when I navigate to it.
However the video does not stop playing when I hit the 'back' button and navigate away from the PlayerView.
Any thoughts on how to fix this are appreciated.
I've included code from the Navigation view, the pass thru VideoView and the PlayerView as context. TIA.
Navigation view
NavigationView {
List(messages, id: \.self) { message in
NavigationLink(destination: VideoView(videoURL: URL(string:"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4")!, previewLength: 15)) {
Text(message)
}
}.navigationBarTitle("Answers")
VideoView code
struct VideoView: UIViewRepresentable {
var videoURL:URL
var previewLength:Double?
func makeUIView(context: Context) -> UIView {
print("making VideoView")
return PlayerView(frame: .zero, url: videoURL, previewLength: previewLength ?? 30)
}
func updateUIView(_ uiView: UIView, context: Context) {
}
Player Snippet
class PlayerView: UIView {
private let playerLayer = AVPlayerLayer()
private var previewTimer:Timer?
var previewLength:Double
init(frame: CGRect, url: URL, previewLength:Double) {
self.previewLength = previewLength
super.init(frame: frame)
// Create the video player using the URL passed in.
let player = AVPlayer(url: url)
player.volume = 2 // Will play audio if you don't set to zero
// This is the AVPlayer approach to playing video with no controls
player.play() // Set to play once created.
// Add the player to our Player Layer
playerLayer.player = player
playerLayer.videoGravity = .resizeAspectFill // Resizes content to fill whole video layer.
playerLayer.backgroundColor = UIColor.black.cgColor
previewTimer = Timer.scheduledTimer(withTimeInterval: previewLength, repeats: false, block: { (timer) in
player.seek(to: CMTime(seconds: 0, preferredTimescale: CMTimeScale(1)))
})
layer.addSublayer(playerLayer)
Here is possible approach - to use presentation mode on update, because in this scenario it is updated due to navigation back.
struct VideoView: UIViewRepresentable {
#Environment(\.presentationMode) var presentation // << here !!
var videoURL:URL
var previewLength:Double?
func makeUIView(context: Context) -> UIView {
print("making VideoView")
return PlayerView(frame: .zero, url: videoURL, previewLength: previewLength ?? 30)
}
func updateUIView(_ playerView: PlayerView, context: Context) {
// update might be called several times, so PlayerView should
// be safe for repeated calls
if presentation.wrappedValue.isPresented {
playerView.play()
} else {
playerView.stop()
}
}
}
and PlayerView should be updated to have access to player
class PlayerView: UIView {
private let playerLayer = AVPlayerLayer()
private var previewTimer:Timer?
var previewLength:Double
private var player: AVPlayer // << make property
init(frame: CGRect, url: URL, previewLength:Double) {
self.previewLength = previewLength
super.init(frame: frame)
// Create the video player using the URL passed in.
player = AVPlayer(url: url)
player.volume = 2 // Will play audio if you don't set to zero
// don't run .play() here !!!
// ... other code
}
func play() {
player.rate = 1.0
}
func stop() {
player.rate = 0.0
}
}
resizeAspect as the video gravity only works properly for me, when using an iPhone X.
For some reasons, the black aspect bar gets only added to the top and not to the bottom. This is how it looks like when I'm not using an iPhone X (the image is white)
This is how it should look like:
As you can see, on the iPhone X, everything looks clean and balanced as expected.
This is how I play the video:
avPlayerLayer = AVPlayerLayer(player: avPlayer)
avPlayerLayer.frame = PreviewLayer.bounds
avPlayerLayer.videoGravity = .resizeAspect //Will automatically add black bars
PreviewLayer.layer.insertSublayer(avPlayerLayer, at: 0)
let playerItem = AVPlayerItem(url: video)
avPlayer?.replaceCurrentItem(with: playerItem)
avPlayer?.play()
Try this steps
1- Check bottom constraint connect with safe are
2- Try this code
import UIKit
import AVKit
class ViewController: UIViewController {
#IBOutlet weak var playerView: UIView!
private var player: AVPlayer!
override func viewDidLoad() {
super.viewDidLoad()
playVideo(from: "Intro.mp4")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
private func playVideo(from file:String) {
let file = file.components(separatedBy: ".")
guard let path = Bundle.main.path(forResource: file[0], ofType:file[1]) else {
debugPrint( "\(file.joined(separator: ".")) not found")
return
}
player = AVPlayer(url: URL(fileURLWithPath: path))
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: self.player.currentItem, queue: .main) { _ in
self.player?.seek(to: kCMTimeZero)
self.player?.play()
}
let playerLayer = AVPlayerLayer(player: player)
playerLayer.videoGravity = AVLayerVideoGravity.resizeAspect
playerLayer.frame = playerView.layer.bounds
playerView.layer.insertSublayer(playerLayer, at: 0)
player.play()
}
}
Result on iphone X
Result on iphone 7
Note : if you need video full view on all devices try to link Bottom
constraint and Top constraint with superview not with safe area
Make your videoLayer View a subview of:
class VideoContainerView: UIView {
var playerLayer: CALayer?
override func layoutSublayers(of layer: CALayer) {
super.layoutSublayers(of: layer)
playerLayer?.frame = self.bounds
}
}
and add:
self.VideoShareLayer.playerLayer = avPlayerLayer;
before inserting the layer.
I am trying to display a video inside a cell. Instead, it just comes up as a black box. It is possible it is not initializing the player. I do, however see the black box so I know it is initializing the VideoPlayerView. When I print the frame of the playerLayer it prints (0.0, 0.0, 0.0, 0.0), possibly due to how I am initializing the view. Is there any way to extend the playerlayer to the edges of the view without using frame?
class VideoPlayerView : UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.black
let videoURL = URL(string: "https://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4")
let player = AVPlayer(url: videoURL!)
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = self.bounds
//print(playerLayer.frame)
self.layer.addSublayer(playerLayer)
player.play()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Thanks!
I've tried this a couple of different ways and ended up using a custom UIView class approach (like you did) but a few differences. For example, your class could look like this:
class PlayerView: UIView {
var player: AVPlayer? {
get {
return playerLayer.player
}
set {
playerLayer.player = newValue
}
}
var playerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}
// Override UIView property
override static var layerClass: AnyClass {
return AVPlayerLayer.self
}
}
Then, you add a UIView in IB, set the class to PlayerView (instead of default UIView), add your constraints like normal and drag an outlet to your controller like any other control. To use it, you do the following:
playerView.player = AVPlayer(url: url)
playerView.playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
Then trigger the Play command however your application needs it.
You could create this view dynamically using the same method but I tend to use IB most of the time.
Hope this helps!
You can set constraints to playerLayer, for example:
let width = UIScreen.main.bounds.width
let height = 300
let playerLayer = AVPlayerLayer(player: player)
playerLayer.translatesAutoresizingMaskIntoConstraints = false
playerLayer.widthAnchor.constraint(equalToConstant: width).isActive = true
playerLayer.heightAnchor.constraint(equalToConstant: height).isActive = true
I figured out that all you need to do is add this code:
Turns out when you resize it doesn't automatically resize layer sizes.
override func layoutSublayers(of layer: CALayer) {
super.layoutSublayers(of: layer)
playerLayer?.frame = self.bounds
}
I have a WKWebView which loads HTML5 games. The HTML5 games resize perfectly when opening them in safari on my mac, without a problem. However, when I change the orientation of my iPad Pro and thus resize the size of the UICollectionViewCells, the WebView containing the WKWebView also gets resized. Unfortunately, not the game.
In the image below, I gave the WKWebView a backgroundColor of UIColor.red so its easy to distinguish.
This is my code:
import UIKit
import WebKit
final class WebView: UIView {
var webView: WKWebView!
var bounces: Bool = true {
didSet {
webView.scrollView.bounces = bounces
}
}
override var bounds: CGRect {
didSet {
if webView != nil {
var b = bounds
b.origin.x = 0
b.origin.y = 0
webView.bounds = b
webView.frame = b
}
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let webConfiguration = WKWebViewConfiguration()
webView = WKWebView(frame: CGRect(x: 0, y: 0, width: self.bounds.width, height: 100), configuration: webConfiguration)
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
webView.navigationDelegate = self
webView.backgroundColor = UIColor.red
addSubview(webView)
}
override func draw(_ rect: CGRect) {
webView.frame = rect
}
/// Load request
func load(_ request: URLRequest) {
webView.load(request)
}
}
What is it I am doing wrong, or better yet... how can I tell the WKWebView to resize without reloading the webview.
Update:
If I lock the device (on/off button) and unlock the device, resuming, the content resizes properly and looks correct.
Update:
When decreasing the size of the UICollectionViewCell, the contents resizes nicely. But it doesn't when increasing the size.
Also tried resizing subclasses, which didn't help either, using:
override var bounds: CGRect {
didSet {
if webView != nil {
var b = bounds
b.origin.x = 0
b.origin.y = 0
webView.bounds = b
webView.frame = b
for item in webView.subviews {
item.bounds = b
if item.subviews.count > 0 {
setBounds(toSubView: item, toBounds: b)
}
}
}
}
}
func setBounds(toSubView subView: UIView, toBounds bounds: CGRect) {
for item in subView.subviews {
if item.subviews.count > 0 {
setBounds(toSubView: item, toBounds: bounds)
}
if item.classIdentifier == "WKContentView" {
item.bounds = bounds
}
}
}
I am trying to draw on an image from the camera roll using the code from this tutorial. After selecting a photo from the camera roll, imageView is set to the photo. When the user presses the draw button, tempImageView is added over the photo, so the user can draw onto it. However, when I attempt to draw on the image, nothing happens. Why?
Code:
class EditViewController: UIViewController{
var tempImageView: DrawableView!
var imageView: UIImageView = UIImageView(image: UIImage(named: "ClearImage"))
func draw(sender: UIButton!) {
tempImageView = DrawableView(frame: imageView.bounds)
tempImageView.backgroundColor = UIColor.clearColor()
self.view.addSubview(tempImageView)
}
}
class DrawableView: UIView {
let path=UIBezierPath()
var previousPoint:CGPoint
var lineWidth:CGFloat=10.0
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override init(frame: CGRect) {
previousPoint=CGPoint.zero
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
previousPoint=CGPoint.zero
super.init(coder: aDecoder)
let panGestureRecognizer=UIPanGestureRecognizer(target: self, action: "pan:")
panGestureRecognizer.maximumNumberOfTouches=1
self.addGestureRecognizer(panGestureRecognizer)
}
override func drawRect(rect: CGRect) {
// Drawing code
UIColor.greenColor().setStroke()
path.stroke()
path.lineWidth=lineWidth
}
func pan(panGestureRecognizer:UIPanGestureRecognizer)->Void
{
let currentPoint=panGestureRecognizer.locationInView(self)
let midPoint=self.midPoint(previousPoint, p1: currentPoint)
if panGestureRecognizer.state == .Began
{
path.moveToPoint(currentPoint)
}
else if panGestureRecognizer.state == .Changed
{
path.addQuadCurveToPoint(midPoint,controlPoint: previousPoint)
}
previousPoint=currentPoint
self.setNeedsDisplay()
}
func midPoint(p0:CGPoint,p1:CGPoint)->CGPoint
{
let x=(p0.x+p1.x)/2
let y=(p0.y+p1.y)/2
return CGPoint(x: x, y: y)
}
}
make sure that tempImageView.userIteractionEnabled = YES. by default this property for UIImageView is NO.
after put breakpoint to you gestureRecogniser method, see if it's being called.