I have an app on the store for at least six months that comes with a target for iOS and another for Apple Watch. Both targets play videos using AVFoundation
.
The code I was using for playing the videos was giving me problems. I found some code on the web, modified it and managed to make it work for iOS. Now I am trying to do it for the watch.
This is the code:
#CustomVideoPlayer
import SwiftUI
import Combine
import AVKit
public struct CustomVideoPlayer: UIViewRepresentable {
@ObservedObject private var playerVM: PlayerViewModel
public init(_ playerVM: PlayerViewModel) {
self.playerVM = playerVM
}
public func makeUIView(context: Context) -> PlayerView {
let view = PlayerView()
view.player = playerVM.player
context.coordinator.setController(view.playerLayer)
return view
}
public func updateUIView(_ uiView: PlayerView, context: Context) { }
public func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
public class Coordinator: NSObject, AVPictureInPictureControllerDelegate {
private let parent: CustomVideoPlayer
private var controller: AVPictureInPictureController?
private var cancellable: AnyCancellable?
init(_ parent: CustomVideoPlayer) {
self.parent = parent
super.init()
cancellable = parent.playerVM.$isInPipMode
.sink { [weak self] in
guard let self = self,
let controller = self.controller else { return }
if $0 {
if controller.isPictureInPictureActive == false {
controller.startPictureInPicture()
}
} else if controller.isPictureInPictureActive {
controller.stopPictureInPicture()
}
}
}
public func setController(_ playerLayer: AVPlayerLayer) {
controller = AVPictureInPictureController(playerLayer: playerLayer)
controller?.canStartPictureInPictureAutomaticallyFromInline = true
controller?.delegate = self
}
public func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
parent.playerVM.isInPipMode = true
}
public func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
parent.playerVM.isInPipMode = false
}
}
}
#CustomControlsView
import SwiftUI
public struct CustomControlsView: View {
@ObservedObject private var playerVM: PlayerViewModel
public init(playerVM: PlayerViewModel) {
self.playerVM = playerVM
}
public var body: some View {
HStack {
if playerVM.isPlaying == false {
Button(action: {
playerVM.player.play()
}, label: {
Image(systemName: "play.circle")
.renderingMode(.template)
.font(.system(size: 25))
.foregroundColor(.black)
})
} else {
Button(action: {
playerVM.player.pause()
}, label: {
Image(systemName: "pause.circle")
.renderingMode(.template)
.font(.system(size: 25))
.foregroundColor(.black)
})
}
if let duration = playerVM.duration {
Slider(value: $playerVM.currentTime, in: 0...duration, onEditingChanged: { isEditing in
playerVM.isEditingCurrentTime = isEditing
})
} else {
Spacer()
}
}
.padding()
.background(.thinMaterial)
}
}
#PlayerViewModel
import AVFoundation
import Combine
final public class PlayerViewModel: ObservableObject {
public let player = AVPlayer()
@Published var isInPipMode: Bool = false
@Published var isPlaying = false
@Published var isEditingCurrentTime = false
@Published var currentTime: Double = .zero
@Published var duration: Double?
private var subscriptions: Set<AnyCancellable> = []
private var timeObserver: Any?
deinit {
if let timeObserver = timeObserver {
player.removeTimeObserver(timeObserver)
}
}
public init() {
$isEditingCurrentTime
.dropFirst()
.filter({ $0 == false })
.sink(receiveValue: { [weak self] _ in
guard let self = self else { return }
self.player.seek(to: CMTime(seconds: self.currentTime, preferredTimescale: 1), toleranceBefore: .zero, toleranceAfter: .zero)
if self.player.rate != 0 {
self.player.play()
}
})
.store(in: &subscriptions)
player.publisher(for: \.timeControlStatus)
.sink { [weak self] status in
switch status {
case .playing:
self?.isPlaying = true
case .paused:
self?.isPlaying = false
case .waitingToPlayAtSpecifiedRate:
break
@unknown default:
break
}
}
.store(in: &subscriptions)
timeObserver = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 600), queue: .main) { [weak self] time in
guard let self = self else { return }
if self.isEditingCurrentTime == false {
self.currentTime = time.seconds
}
}
}
private var videoName = ""
private func videoURL(_ filename:String) -> URL? {
guard let fileURL = Bundle.main.url(forResource:filename,
withExtension: "mp4")
else { return nil }
return fileURL
}
public func setCurrentItem(_ filename:String){
currentTime = .zero
duration = nil
guard let videoURL = videoURL(filename) else { return }
let avPlayerItem = AVPlayerItem(url:videoURL)
player.replaceCurrentItem(with: avPlayerItem)
avPlayerItem.publisher(for: \.status)
.filter({ $0 == .readyToPlay })
.sink(receiveValue: { [weak self] _ in
self?.duration = avPlayerItem.asset.duration.seconds
})
.store(in: &subscriptions)
}
}
#PlayerView
import AVFoundation
import UIKit
final public class PlayerView: UIView {
public override static var layerClass: AnyClass {
return AVPlayerLayer.self
}
var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer }
var player: AVPlayer? {
get {
playerLayer.player
}
set {
playerLayer.videoGravity = .resizeAspectFill
playerLayer.player = newValue
}
}
}
How to use it:
CustomVideoPlayer(playerVM)
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
.onAppear {
if let filename = item.filename {
playerVM.setCurrentItem("beach") // load 'beach.mp4' from the bundle
playerVM.player.play()
}
}
My problem is that PlayerView
uses UIKit
, and Apple Watch does not have that framework.
How can I modify this code to work with Apple Watch?
Thanks in advance?
You're trying to use views and types on watchOS that aren't available on watchOS.
Just to name a few:
You can't do that. You either need to find equivalents for them which are available on watchOS or make some of their provided functionality iOS-only (such as Picture in Picture - that doesn't really make sense on watchOS).
If you want to be able to keep your existing video player features on iOS, you'll have to use a different view on watchOS for playing videos - this approach makes sense anyways as the completely different screen sizes mean users don't expect the same video features on both platforms.
You can keep using the current, iOS only component on iOS and use a separate video player view on watchOS. You have multiple options:
UIViewRepresentable
and wrap a native WatchKit
interface object for playing the video, such as WKInterfaceMovie