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
Related
From the question in this If I assign a sound in Reality Composer, can I stop it programmatically in RealityKit?, I would like to use method to resume playback after Play Music.
Can I do that?
Now, I use this command in stopAudio function to stop the music.
func stopAudio() {
if arView.scene.anchors.count > 0 {
if arView.scene.anchors[0].isAnchored {
arView.scene.anchors[0].children[0].stopAllAudio()
}
}
}
If I want arView.scene.anchors[0] to replay the music again, which command should I use?
Audio Playback Controller
Since RealityKit 2.0 isn't able to control parameters of Reality Composer's behaviors, the best strategy for controlling audio is to create a programmatic AudioPlaybackController. To feed your audio file to the controller, export .rcproject scene to .usdz format and use unzipping trick to extract the .aiff, .caf or .mp3 sound file. When loading audio for playback, you can choose between spatial and non-spatial audio experience.
UIKit version
import UIKit
import RealityKit
extension ViewController {
private func loadAudio() {
do {
let resource = try AudioFileResource.load(
named: "planetarium07.caf",
in: nil,
inputMode: .spatial,
loadingStrategy: .preload,
shouldLoop: true)
self.controller = entity.prepareAudio(resource)
self.controller?.speed = 0.9
self.controller?.fade(to: .infinity, duration: 2)
} catch {
print(error.localizedDescription)
}
}
}
ViewController.
class ViewController : UIViewController {
#IBOutlet var uiView: UIView! // when using #IBAction buttons
#IBOutlet var arView: ARView!
private var entity = Entity()
private var controller: AudioPlaybackController? = nil
override func viewDidLoad() {
super.viewDidLoad()
uiView.backgroundColor = .systemCyan
let boxScene = try! Experience.loadBox()
arView.scene.anchors.append(boxScene)
let anchor = boxScene.anchor
anchor?.addChild(entity)
self.loadAudio()
}
#IBAction func playMusic(_ sender: UIButton) {
self.controller?.play()
}
#IBAction func stopMusic(_ sender: UIButton) {
self.controller?.pause()
// self.controller?.stop()
}
}
SwiftUI version
import SwiftUI
import RealityKit
struct ContentView : View {
#State var arView = ARView(frame: .zero)
#State var controller: AudioPlaybackController? = nil
#State var entity = Entity()
var body: some View {
ZStack {
ARViewContainer(arView: $arView,
entity: $entity).ignoresSafeArea()
VStack {
Spacer()
Button("Play") { loadSound(); controller?.play() }
Button("Stop") { controller?.stop() }
}
}
}
func loadSound() {
do {
let resource = try AudioFileResource.load(
named: "planetarium07.caf",
in: nil,
inputMode: .spatial,
loadingStrategy: .preload,
shouldLoop: true)
self.controller = entity.prepareAudio(resource)
} catch {
print(error.localizedDescription)
}
}
}
ARViewContainer.
struct ARViewContainer: UIViewRepresentable {
#Binding var arView: ARView
#Binding var entity: Entity
func makeUIView(context: Context) -> ARView {
let boxScene = try! Experience.loadBox()
arView.scene.anchors.append(boxScene)
let anchor = boxScene.anchor
anchor?.addChild(entity)
return arView
}
func updateUIView(_ view: ARView, context: Context) { }
}
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?
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.
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?
I am able to present an AVPlayerViewController from SwiftUI but there is some padding around the video and I would like for it to be full-screen.
From the SwiftUI portion there is the following:
var body: some View {
NavigationView {
List {
ForEach(topicsArray) { topic in
Section(header: Text(topic.title)) {
ForEach(0..<topic.shows.count) { index in
NavigationLink(destination: PlayerView(showID: topic.shows[index])) {
ShowCell(showID: topic.shows[index])
}
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
}
}
.listStyle(GroupedListStyle())
.padding()
}.onAppear(perform: initialDataLoad)
}
The code being called from the NavigationLink that shows the player is:
struct PlayerView: UIViewControllerRepresentable {
var showID:Int
func makeUIViewController(context: Context) -> AVPlayerViewController {
let pv = PlayerViewController()
pv.showID = showID
return pv
}
func updateUIViewController(_ viewController: AVPlayerViewController, context: Context) {
}
}
class PlayerViewController: AVPlayerViewController {
var showID:Int! {
didSet {
setup()
}
}
private var videoLaunch:VideoLaunch!
private func setup() {
videoLaunch = VideoLaunch(showID: showID,
season: nil,
episodeID: nil,
selectedIndex: IndexPath(row: 0, section: 0),
showType: .single,
dataStructure: topics as Any,
screenType: .live)
playVideo()
}
private func playVideo() {
guard let videoURL = self.videoLaunch.getMediaURL() else {print("Problem getting media URL");return}
self.player = AVPlayer(url: videoURL)
self.videoGravity = .resizeAspectFill
self.player?.play()
}
I have tried setting the bounds and using the modalpresentationstyle for fullscreen, but none have had any impact. There is still what looks like a 10 point border around the video.
I was able to solve the issue by inserting the following within the PlayerViewController class.
override func viewDidLayoutSubviews() {
self.view.bounds = UIScreen.main.bounds
}