Playback controls in swiftui - ios

Attempting to use AVKit's AVPlayer to play a video without playback controls:
private let player = AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "videoToPlay", ofType: "mp4")!))
player.showsPlaybackControls = false
The above results in an error message that declarations must be separated by ';'.
I've also tried:
VideoPlayer(player: player)
.onAppear {
player.showsPlaybackControls = false
}
Which results in a different error.
Any advice on hiding playback controls with swiftui?

VideoPlayer does not appear to have any methods on it that I can find in the documentation to control whether the playback controls are shown.
showPlaybackControls doesn't work because AVPlayer doesn't have that property either.
It looks like for now you'll have to do something like wrap an AVPlayerViewController:
Note that this is a very limited example and doesn't take into account a lot of scenarios you may need to consider, like what happens when AVPlayerControllerRepresented gets reloaded because it's parent changes -- will you need to use updateUIViewController to update its properties? You will also probably need a more stable solution to storing your AVPlayer than what I used, which will also get recreated any time the parent view changes. But, all of these are relatively easily solved architectural decisions (look into ObservableObject, StateObject, Equatable, etc)
struct ContentView: View {
let player = AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "IMG_0226", ofType: "mp4")!))
var body: some View {
AVPlayerControllerRepresented(player: player)
.onAppear {
player.play()
}
}
}
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) {
}
}

For me, setting allowsHitTesting false hides play back controls in iOS 16:
private let player = AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "videoToPlay", ofType: "mp4")!))
...
// Disable user interaction on the video player to hide playback controls
VideoPlayer(player: player)
.allowsHitTesting(false)
There may be some corner cases I'm missing where the UI could still appear, but I have yet to encounter that.

I was having problems with playing segments with VideoPlayer, so I had to resort to using AVPlayerLayer which does not have the controls.
class PlayerView: UIView {
// Override the property to make AVPlayerLayer the view's backing layer.
override static var layerClass: AnyClass { AVPlayerLayer.self }
// The associated player object.
var player: AVPlayer? {
get { playerLayer.player }
set { playerLayer.player = newValue }
}
private var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer }
}
struct CustomVideoPlayer: UIViewRepresentable {
let player: AVQueuePlayer
func makeUIView(context: Context) -> PlayerView {
let view = PlayerView()
view.player = player
return view
}
func updateUIView(_ uiView: PlayerView, context: Context) { }
}
Usage:
CustomVideoPlayer(player: player)
.onAppear() {
player.play()
}
.onDisappear() {
player.pause()
}
.ignoresSafeArea(.all)

Another great option is to use a video layer instead of an AVPlayerViewController. This is especially useful if the video needs to repeat, for example, since that seems to be the only way to support that use-case.
Since you're just displaying a video layer, there are no controls by default.
How to loop video with AVPlayerLooper

import Foundation
import SwiftUI
import AVKit
import AVFoundation
struct GifyVideoView : View {
let videoName: String
var body: some View {
if let url = Bundle.main.url(forResource: videoName, withExtension: "mp4") {
GifyVideoViewWrapped(url: url)
} else {
Text("No video for some reason")
}
}
}
fileprivate struct GifyVideoViewWrapped: View {
let player : AVPlayer
var pub = NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime)
init(url: URL) {
player = AVPlayer(url: url)
}
var body: some View {
AVPlayerControllerRepresented(player: player)
.onAppear {
player.play()
}
.onReceive(pub) { (output) in
player.play()
}
}
}
fileprivate struct AVPlayerControllerRepresented : NSViewRepresentable {
var player : AVPlayer
func makeNSView(context: Context) -> AVPlayerView {
let view = AVPlayerView()
view.controlsStyle = .none
view.player = player
return view
}
func updateNSView(_ nsView: AVPlayerView, context: Context) { }
}
usage:
GifyVideoView(VideoName: "someMp4VideoName")
.frame(width: 314, height: 196 * 2)
.clipShape( RoundedRectangle(cornerRadius: 15, style: .continuous) )
video will be played looped ant without controls.
Video will be taken from project resources

Related

How to make a custom AVPlayer support SharePlay adjust coordinate or play-pause control

This app was written using SwiftUI and I also consulted Apple's official documentation and some third-party websites. So I wrote something like the following code
This is the Swift code where the player is called and the SharePlay-related logic
import SwiftUI
import AVFoundation
import AVKit
import GroupActivities
import Combine
struct EpisodeDetail: View {
#State var episode: CommonResponse<Episode>
#State var videoPlayer: CustomVideoPlayer
#State private var groupSession: GroupSession<EpisodeActivity>?
#State private var subscriptions = Set<AnyCancellable>()
var body: some View {
VStack {
videoPlayer
.transition(.move(edge: .bottom))
.edgesIgnoringSafeArea(.all)
ScrollView {
VStack(alignment: .center) {
Button {
prepareToPlay(episode.attributes)
} label: {
Label("同播共享", systemImage: "shareplay")
}
.buttonStyle(.bordered)
.cornerRadius(20)
}
VStack(alignment: .leading, spacing: 20) {
Text(episode.attributes.title)
.font(.title2)
.fontWeight(.bold)
Text(episode.attributes.description)
.font(.body)
.foregroundColor(.gray)
}
}
.padding(12)
}
.task {
for await session in EpisodeActivity.sessions() {
configureGroupSession(session)
}
}
}
private func configureGroupSession(_ session: GroupSession<EpisodeActivity>) {
groupSession = session
videoPlayer.player.playbackCoordinator.coordinateWithSession(session)
session.$state
.sink { state in
if case .invalidated = state {
groupSession = nil
subscriptions.removeAll()
}
}
.store(in: &subscriptions)
session.$activity
.sink { activity in
print("Activity Changed: \(activity.metadata.title ?? "No title for this shitty video LOL")")
}
.store(in: &subscriptions)
session.join()
}
private func prepareToPlay(_ playerEpisodeData: Episode) {
let activity = EpisodeActivity(episode: playerEpisodeData)
Task {
switch await activity.prepareForActivation() {
case .activationDisabled:
videoPlayer.player.replaceCurrentItem(with: AVPlayerItem(url: playerEpisodeData.videoUrl))
break
case .activationPreferred:
videoPlayer.player.replaceCurrentItem(with: AVPlayerItem(url: playerEpisodeData.videoUrl))
_ = try await activity.activate()
case .cancelled:
break
#unknown default:
break
}
}
}
}
Then the following code is a custom AVPlayer
import SwiftUI
import AVFoundation
import AVKit
struct CustomVideoPlayer: UIViewControllerRepresentable {
#State var videoUrl: URL
var player: AVPlayer {
return AVPlayer(url: videoUrl)
}
func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) {
playerController.modalPresentationStyle = .fullScreen
playerController.allowsPictureInPicturePlayback = true
playerController.canStartPictureInPictureAutomaticallyFromInline = true
playerController.player = player
}
func makeUIViewController(context: Context) -> AVPlayerViewController {
return AVPlayerViewController()
}
}
The SharePlay prompt pops up properly and shows the current activity correctly
This is a screenshot of the SharePlay pop-up box, I couldn't insert an image, so I had to insert a link
https://i.stack.imgur.com/QNqBE.jpg
But when I do something with the player, like pause, or adjust the playback progress, the other iPhone doesn't sync
So what should I do?
Can't thank you enough :-)
Solved, by adding #State annotation to AVPlayer, thanks guys, this question is closed
struct CustomVideoPlayer: UIViewControllerRepresentable {
#State var player: AVPlayer? = nil
func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) {
playerController.modalPresentationStyle = .fullScreen
playerController.allowsPictureInPicturePlayback = true
playerController.canStartPictureInPictureAutomaticallyFromInline = true
playerController.player = player
}
func makeUIViewController(context: Context) -> AVPlayerViewController {
return AVPlayerViewController()
}
}

Play audio on touch down action of button in swiftui

I want to play a audio when a button is touched down(as soon as it is clicked) not on touch release in SwiftUI. How can I implement this?
My code looks something like this:
struct PressedButtonStyle: ButtonStyle {
let touchDown: () -> ()
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.foregroundColor(configuration.isPressed ? Color.gray : Color.blue)
.background(configuration.isPressed ? self.handlePressed() : Color.clear)
}
private func handlePressed() -> Color {
touchDown()
return Color.clear
}
}
struct DemoPressedButton: View {
#State var audioPlayer: AVAudioPlayer!
var body: some View {
Button("Demo") {
print(">> tap up")
}
.buttonStyle(PressedButtonStyle {
print(">> tap down")
let sound = Bundle.main.path(forResource: "filename", ofType: "wav")
self.audioPlayer = try! AVAudioPlayer(contentsOf: URL(fileURLWithPath: sound!)) // receives warning about changing state while updating view
self.audioPlayer.play() // breaks
})
}
}
The code breaks when calling self.audioPlayer.play() .
The custom touchdown code is from this link: SwiftUI button action as soon as button is clicked not on click release
Here is a demo of possible approach. Tested with Xcode 11.4 / iOS 13.4
Note: you should keep reference to AVAudioPlayer while it is playing and better track its state, so this is more appropriate to do in some helper class (like view model)
class PlayViewModel {
private var audioPlayer: AVAudioPlayer!
func play() {
let sound = Bundle.main.path(forResource: "filename", ofType: "wav")
self.audioPlayer = try! AVAudioPlayer(contentsOf: URL(fileURLWithPath: sound!))
self.audioPlayer.play()
}
}
struct DemoPressedButton: View {
let vm = PlayViewModel()
var body: some View {
Button("Demo") {
print(">> tap up")
}
.buttonStyle(PressedButtonStyle {
print(">> tap down")
self.vm.play()
})
}
}

Handling AVPlayerLayer when wrapping AVPlayer behaviour

I'm trying to wrap AVPlayer in my own class so I can provide a nicer API to use throughout my app and so I can mock player behaviour for testing with other objects (and because the AVPlayer KVOs are quite ugly to use!). Here's a simplified model of what I'm trying to do with just the play and pause functionality:
protocol VideoPlayerProtocol {
func play()
func pause()
}
class AVPlayerWrapped: VideoPlayerProtocol {
private let player = AVPlayer()
init(playerItem: AVPlayerItem) {
self.player.replaceCurrentItem(with: playerItem)
}
func play() {
player.play()
}
func pause() {
player.pause()
}
}
I also have a PlayerView which adds an AVPlayerLayer to a view. From the Apple docs, this is set by providing the view an AVPlayer:
class PlayerView: UIView {
override class var layerClass: AnyClass {
return AVPlayerLayer.self
}
var playerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}
var player: AVPlayer? {
get { playerLayer.player }
set { playerLayer.player = newValue }
}
}
The problem is that when I setup an AVPlayerWrapped object, in order to display the playback in a view I need to reveal the underlying AVPlayer to the player property on PlayerView which defeats the purpose of me wrapping the player.
Is there a way for me to somehow use an AVPlayerLayer without my AVPlayerWrapped having to reveal its underlying player please? Or am I taking the wrong approach?
Any guidance much appreciated!
class AVPlayerWrapped: VideoPlayerProtocol {
fileprivate let player = AVPlayer()
init(playerItem: AVPlayerItem) {
self.player.replaceCurrentItem(with: playerItem)
}
func play() {
player.play()
}
func pause() {
player.pause()
}
}
extension AVPlayerLayer {
func setPlayerWrapper(_ playerWrapped: AVPlayerWrapped) {
player = playerWrapped.player
}
}
And
class PlayerView: UIView {
override class var layerClass: AnyClass {
return AVPlayerLayer.self
}
var playerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}
func setPlayerWrapper(_ playerWrapped: AVPlayerWrapped) {
playerLayer.setPlayerWrapper(playerWrapped)
}
}
I believe you don't need a getter for your view - in my practice I haven't used it. But in case you do, you can do it with an associatedObject, but it's much slower than a real property and I would advise you to use this approach only in special cases.

Avplayer AVPlayerItemDidPlayToEndTime notificaton not sending message to observer

I am using AVQueuePlayer to play a list of remote audio files. I want to implement repeat all by default.
My approach, I am observing AVPlayerItemDidPlayToEndTime notification and I add the playerItem to the back of the Queue when it has finished playing.
The nextAudio(notification: Notification) is not running at all. Need help on this or better still a better way to implement infinite play.
func playAudio(_ items: [AVPlayerItem]) {
let avPlayerVC = AVPlayerViewController()
let player = AVQueuePlayer(items: items)
player.actionAtItemEnd = .pause
avPlayerVC.player = player
for item in items {
NotificationCenter.default.addObserver(self,
selector: #selector(FilesViewController.nextAudio(notification:)),
name: .AVPlayerItemDidPlayToEndTime, object: item)
}
present(avPlayerVC, animated: true) {
self.player.play()
}
}
#objc func nextAudio(notification: Notification) {
debugPrint("nextAudio was called")
guard player != nil else { return }
debugPrint("AVPlayerItemDidPlayToEndTime notif info \(notification.userInfo)")
if let currentItem = notification.userInfo!["object"] as? AVPlayerItem {
currentItem.seek(to: kCMTimeZero)
self.player.advanceToNextItem()
self.player.insert(currentItem, after: nil)
}
}
I am sure you already figured it out but I just encountered myself and decided to answer anyways:
It looks like it is not delivered when you specify an object of notification (which should be a proper way BTW). may be a bug in iOS...
You need to pass nil instead:
NotificationCenter.default.addObserver(self, selector: #selector(videoDidPlayToEnd), name: .AVPlayerItemDidPlayToEndTime, object: nil)
Then in the method you should check in the notification's object is your player item. Documentation is actually not consistent because Apple states that notification's object is a AVplayer but it is a AVPlayerItem:
#objc
private func videoDidPlayToEnd(_ notification: Notification) {
guard let playerItem = notification.object as? AVPlayerItem, let urlAsset = playerItem.asset as? AVURLAsset else { return }
gpLog("Sender urlAsset: \(urlAsset.url.absoluteString)")
// Compare an asset URL.
}
Thank you #Lukasz for your great answer! I was having an issue with multiple videos firing the notification at the wrong time. Your answer helped me fix it.
If anyone is looking for examples of how to use this with SwiftUI here's my code below:
First create a player:
import SwiftUI
import AVKit
struct VideoPlayer : UIViewControllerRepresentable {
func makeCoordinator() -> VideoPlayer.Coordinator {
return VideoPlayer.Coordinator(parent1: self)
}
#Binding var didFinishVideo : Bool
#Binding var player : AVPlayer
var play: Bool
var loop: Bool
var videoName: String
var controller = AVPlayerViewController()
func makeUIViewController(context: UIViewControllerRepresentableContext<VideoPlayer>) -> AVPlayerViewController {
controller.player = player
controller.showsPlaybackControls = false
controller.videoGravity = .resize
NotificationCenter.default.addObserver(context.coordinator, selector: #selector(context.coordinator.playerDidFinishPlaying(_:)), name: .AVPlayerItemDidPlayToEndTime, object: nil)
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: UIViewControllerRepresentableContext<VideoPlayer>) {
if play {
player.play()
}
}
class Coordinator : NSObject{
var parent : VideoPlayer
init(parent1 : VideoPlayer) {
parent = parent1
}
#objc func playerDidFinishPlaying(_ notification: Notification) {
guard let playerItem = notification.object as? AVPlayerItem, let urlAsset = playerItem.asset as? AVURLAsset else { return }
print("Sender urlAsset: \(urlAsset.url.absoluteString)")
if urlAsset.url.absoluteString.contains(parent.videoName) {
if parent.loop {
parent.player.seek(to: CMTime.zero)
parent.player.play()
} else {
parent.didFinishVideo = true
}
}
}
}
}
The you can use it to create multiple videos like this:
import SwiftUI
import AVKit
struct ExampleVideo: View {
#Binding var didFinishVideo : Bool
var play: Bool
#State private var player = AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "exampleVideoFileName", ofType: "mp4")!))
var body: some View {
VideoPlayer(didFinishVideo: $didFinishVideo, player: $player, play: play, loop: false, videoName: "exampleVideoFileName")
}
}
struct ExampleVideo_Previews: PreviewProvider {
static var previews: some View {
CeveraIntroVideo(didFinishVideo: .constant(true), play: true)
}
}
Here's an example of using it in a view after something loads:
struct IntroScreens: View {
#State var loadingComplete = false
#State var didFinishVideo = false
var body: some View {
ZStack{
ExampleVideo(didFinishVideo: $didFinishVideo, play: loadingComplete)
.zIndex(loadingComplete ? 3 : 0)
.animation(Animation.easeInOut(duration: 1))
}
}
}

AVPlayer audio isn't always muted

I want my AVPlayer video to be muted unless the user's phone isn't on silent mode. This generally works by default, however sometimes the sound still plays even when on silent mode. How can I fix this? My code is:
let player = AVPlayer(playerItem: AVPlayerItem(asset: attachmentVideo))
videoView.playerLayer.player = player
player.play()
Where videoView is a PlayerView defined as:
class PlayerView: UIView {
var player: AVPlayer? {
get {
return playerLayer.player
}
set {
playerLayer.player = newValue
}
}
var playerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}
override class var layerClass : AnyClass {
return AVPlayerLayer.self
}
}
I don't specifically specify anywhere that the AVPlayer should be muted...

Resources