sheet 中的 AVPlayer:“CALayer 位置包含 NaN:[nan nan]”(包括 MRE)
AVPlayer in sheet: “CALayer position contains NaN: [nan nan]” (includes MRE)
最后,我设法创建了一个适合 1 个文件的最小可重现示例。这个异常要了我的命,整个周末都在试图找到原因。我无法用断点调试它,因为它在它的线程开始时被调用(xcode window 截图:https://d.pr/i/OPmq8Z )
MRE(最小可重现示例)的作用:
单击按钮 3 秒后,hVideoURL 的值设置为远程视频文件路径“https://...”。
设置 hVideoURL 的值会使 .sheet(item: hVideoURL) 出现
Sheet 包含一个播放远程视频的 AVPlayer。在某些情况下(有时在所有情况下,有时我需要重新连接物理设备才能获得此异常)它会触发异常:
2021-04-11 22:38:17.251165+0200 DetailedTesting[44173:3531565] *** Terminating app due to uncaught exception 'CALayerInvalidGeometry', reason: 'CALayer position contains NaN: [nan nan]. Layer: <CALayer:0x2811a80a0; position = CGPoint (2 8); bounds = CGRect (0 0; 455 4); delegate = <UIView: 0x11bf18f60; frame = (2 8; 455 4); anchorPoint = (0, 0); autoresizesSubviews = NO; layer = <CALayer: 0x2811a80a0>>; opaque = YES; allowsGroupOpacity = YES; anchorPoint = CGPoint (0 0); _swiftUI_displayListID = 11; backgroundColor = <CGColor 0x2835f2d60> [<CGColorSpace 0x2835e09c0> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB IEC61966-2.1; extended range)] ( 1 1 1 0.232 )>'
*** First throw call stack:
(0x19e95d86c 0x1b3976c50 0x19e8564a4 0x1a1d85db4 0x1a1d85ce4 0x1a55f5378 0x1a58f1e38 0x1a58efb78 0x1a58eea8c 0x1a56585f0 0x1a53b09d0 0x1a5658158 0x1a5657f0c 0x1a565793c 0x1a5657f2c 0x1a565793c 0x1a56576d0 0x1a5657580 0x1a57167f4 0x1a52ca0ac 0x1a5868434 0x1a585e2c8 0x1a59e8f90 0x1a59e8fc4 0x1a185fec4 0x1a1d7a644 0x1a1d7ab18 0x1a1d8f30c 0x1a1cd4640 0x1a1cffb08 0x1a1d00e98 0x19e8d8358 0x19e8d25c4 0x19e8d2b74 0x19e8d221c 0x1b649c784 0x1a1312ee8 0x1a131875c 0x1a58a2210 0x1a58a219c 0x1a53daf90 0x100b17b48 0x100b17be8 0x19e5926b0)
libc++abi.dylib: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'CALayerInvalidGeometry', reason: 'CALayer position contains NaN: [nan nan]. Layer: <CALayer:0x2811a80a0; position = CGPoint (2 8); bounds = CGRect (0 0; 455 4); delegate = <UIView: 0x11bf18f60; frame = (2 8; 455 4); anchorPoint = (0, 0); autoresizesSubviews = NO; layer = <CALayer: 0x2811a80a0>>; opaque = YES; allowsGroupOpacity = YES; anchorPoint = CGPoint (0 0); _swiftUI_displayListID = 11; backgroundColor = <CGColor 0x2835f2d60> [<CGColorSpace 0x2835e09c0> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB IEC61966-2.1; extended range)] ( 1 1 1 0.232 )>'
terminating with uncaught exception of type NSException
Sheet有视频播放器(出现异常时):
我的想法:
我发现如果我评论“VideoPlayerControlsView()”,错误不会再次发生,所以问题可能是在我的 CustomSlider 对象位于 VideoPlayerControlsView 视图内。
我觉得可能是加载远程视频导致的,因为一开始视频没有加载,app不知道size/bounds 的 AVPlayer 对象,因此无法创建某些父视图(可能是 CustomerSlider),或者计算负数 width/height..
代码(MRE):
import SwiftUI
import AVFoundation
import Foundation
struct ContentView: View {
@State var hVideoURL: String?
@State var isPaused: Bool = false
var body: some View {
Button("Let's Go!") {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
print("settings isPaused to TRUE")
self.hVideoURL = "https://firebasestorage.googleapis.com/v0/b/fitma-e3043.appspot.com/o/flamelink%2Fmedia%2F1-horizontal.mov?alt=media&token=8f7dfc0f-0261-4a78-9eb0-6154ce1d9dfe"
print("[debug] hVideoURL = \(String(describing: hVideoURL))")
self.isPaused = true
}
}
.sheet(item: self.$hVideoURL, onDismiss: {
self.isPaused = false
print("resume playing main video")
}) { hVideoURLItem in
detailedVideoView(url: hVideoURLItem)
}
}
@ViewBuilder
func detailedVideoView(url: String) -> some View {
DetailedVideo(url: URL(string: url)!, isPaused: self.$isPaused)
.onAppear {
AppDelegate.orientationLock = UIInterfaceOrientationMask.landscapeLeft
UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
}
.onDisappear {
DispatchQueue.main.async {
AppDelegate.orientationLock = UIInterfaceOrientationMask.portrait
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
}
}
}
}
extension String: Identifiable {
public var id: String { self }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct DetailedVideo: View {
var url: URL
@Binding var isPaused: Bool
var body: some View {
ZStack {
DetailedPlayerView(hVideoURL: url)
}
}
}
struct DetailedPlayerView: View {
// The progress through the video, as a percentage (from 0 to 1)
@State private var videoPos: Double = 0
// The duration of the video in seconds
@State private var videoDuration: Double = 0
// Whether we're currently interacting with the seek bar or doing a seek
@State private var seeking = false
private var player: AVPlayer = AVPlayer()
init(hVideoURL: URL?) {
if hVideoURL != nil {
player = AVPlayer(url: hVideoURL!)
player.isMuted = true
player.play()
} else {
print("[debug] hVideoURL is nil")
}
}
var body: some View {
ZStack {
VideoPlayerView(videoPos: $videoPos,
videoDuration: $videoDuration,
seeking: $seeking,
player: player)
.ignoresSafeArea(.all)
.frame(width: UIScreen.screenHeight, height: UIScreen.screenWidth)
VStack {
Spacer()
VideoPlayerControlsView(videoPos: $videoPos,
videoDuration: $videoDuration,
seeking: $seeking,
player: player)
.frame(width: UIScreen.screenHeight - 2*70, height: 20)
.padding(.bottom, 20)
}
}
.onDisappear {
// When this View isn't being shown anymore stop the player
self.player.replaceCurrentItem(with: nil)
}
}
}
// This is the SwiftUI view which wraps the UIKit-based PlayerUIView above
struct VideoPlayerView: UIViewRepresentable {
@Binding private(set) var videoPos: Double
@Binding private(set) var videoDuration: Double
@Binding private(set) var seeking: Bool
let player: AVPlayer
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<VideoPlayerView>) {
// This function gets called if the bindings change, which could be useful if
// you need to respond to external changes, but we don't in this example
}
func makeUIView(context: UIViewRepresentableContext<VideoPlayerView>) -> UIView {
let uiView = VideoPlayerUIView(player: player,
videoPos: $videoPos,
videoDuration: $videoDuration,
seeking: $seeking
)
return uiView
}
static func dismantleUIView(_ uiView: UIView, coordinator: ()) {
guard let playerUIView = uiView as? VideoPlayerUIView else {
return
}
playerUIView.cleanUp()
}
}
class VideoPlayerUIView: UIView {
private let player: AVPlayer
private let playerLayer = AVPlayerLayer()
private let videoPos: Binding<Double>
private let videoDuration: Binding<Double>
private let seeking: Binding<Bool>
private var durationObservation: NSKeyValueObservation?
private var timeObservation: Any?
init(player: AVPlayer, videoPos: Binding<Double>, videoDuration: Binding<Double>, seeking: Binding<Bool>) {
self.player = player
self.videoDuration = videoDuration
self.videoPos = videoPos
self.seeking = seeking
super.init(frame: .zero)
backgroundColor = .lightGray
playerLayer.player = player
layer.addSublayer(playerLayer)
// Observe the duration of the player's item so we can display it
// and use it for updating the seek bar's position
durationObservation = player.currentItem?.observe(\.duration, changeHandler: { [weak self] item, change in
guard let self = self else { return }
self.videoDuration.wrappedValue = item.duration.seconds
})
// Observe the player's time periodically so we can update the seek bar's
// position as we progress through playback
timeObservation = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.5, preferredTimescale: 600), queue: nil) { [weak self] time in
guard let self = self else { return }
// If we're not seeking currently (don't want to override the slider
// position if the user is interacting)
guard !self.seeking.wrappedValue else {
return
}
// update videoPos with the new video time (as a percentage)
self.videoPos.wrappedValue = time.seconds / self.videoDuration.wrappedValue
}
}
required init?(coder: NSCoder) {
fatalError("[debug] init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
func cleanUp() {
// Remove observers we setup in init
durationObservation?.invalidate()
durationObservation = nil
if let observation = timeObservation {
player.removeTimeObserver(observation)
timeObservation = nil
}
}
}
// This is the SwiftUI view that contains the controls for the player
struct VideoPlayerControlsView : View {
@Binding private(set) var videoPos: Double
@Binding private(set) var videoDuration: Double
@Binding private(set) var seeking: Bool
@State var shouldStopPlayer: Bool = false
@State var player: AVPlayer
@State private var playerPaused = false
var body: some View {
HStack {
// Play/pause button
Button(action: togglePlayPause) {
Image(systemName: playerPaused ? "arrowtriangle.right.fill" : "pause.fill")
.foregroundColor(Color.red)
.contentShape(Rectangle())
.padding(.trailing, 10)
}
// Current video time
if videoPos.isFinite && videoPos.isCanonical && videoDuration.isFinite && videoDuration.isCanonical {
Text(Utility.formatSecondsToHMS(videoPos * videoDuration))
.foregroundColor(Color.red)
}
// Slider for seeking / showing video progress
CustomSlider(value: $videoPos, shouldStopPlayer: self.$shouldStopPlayer, range: (0, 1), knobWidth: 4) { modifiers in
ZStack {
Group {
Color(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 0.5799999833106995))//Color((red: 0.4, green: 0.3, blue: 1)
.opacity(0.4)
.frame(height: 4)
.modifier(modifiers.barRight)
Color.red//Color(red: 0.4, green: 0.3, blue: 1)
.frame(height: 4)
.modifier(modifiers.barLeft)
}
.cornerRadius(5)
VStack {
Image(systemName: "arrowtriangle.down.fill") // SF Symbol
.foregroundColor(Color.red)
.offset(y: -3)
}
.frame(width: 20, height: 20)
.contentShape(Rectangle())
.modifier(modifiers.knob)
}
}
.onChange(of: shouldStopPlayer) { _ in
if shouldStopPlayer == false {
print("[debug] shouldStopPlayer == false")
sliderEditingChanged(editingStarted: false)
} else {
if seeking == false {
print("[debug] shouldStopPlayer == true")
sliderEditingChanged(editingStarted: true)
}
}
}
.frame(height: 20)
// Video duration
if videoDuration.isCanonical && videoDuration.isFinite {
Text(Utility.formatSecondsToHMS(videoDuration))
.foregroundColor(Color.red)
}
}
.padding(.leading, 40)
.padding(.trailing, 40)
}
private func togglePlayPause() {
pausePlayer(!playerPaused)
}
private func pausePlayer(_ pause: Bool) {
playerPaused = pause
if playerPaused {
player.pause()
}
else {
player.play()
}
}
private func sliderEditingChanged(editingStarted: Bool) {
if editingStarted {
// Set a flag stating that we're seeking so the slider doesn't
// get updated by the periodic time observer on the player
seeking = true
pausePlayer(true)
}
// Do the seek if we're finished
if !editingStarted {
let targetTime = CMTime(seconds: videoPos * videoDuration,
preferredTimescale: 600)
player.seek(to: targetTime) { _ in
// Now the seek is finished, resume normal operation
self.seeking = false
self.pausePlayer(false)
}
}
}
}
class Utility: NSObject {
private static var timeHMSFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .positional
formatter.allowedUnits = [.minute, .second]
formatter.zeroFormattingBehavior = [.pad]
return formatter
}()
static func formatSecondsToHMS(_ seconds: Double) -> String {
return timeHMSFormatter.string(from: seconds) ?? "00:00"
}
}
extension UIScreen {
static let screenWidth = UIScreen.main.bounds.size.width
static let screenHeight = UIScreen.main.bounds.size.height
static let screenSize = UIScreen.main.bounds.size
}
struct CustomSlider<Component: View>: View {
@Binding var value: Double
var range: (Double, Double)
var knobWidth: CGFloat?
let viewBuilder: (CustomSliderComponents) -> Component
@Binding var shouldStopPlayer: Bool
init(value: Binding<Double>, shouldStopPlayer: Binding<Bool>, range: (Double, Double), knobWidth: CGFloat? = nil, _ viewBuilder: @escaping (CustomSliderComponents) -> Component
) {
_value = value
_shouldStopPlayer = shouldStopPlayer
self.range = range
self.viewBuilder = viewBuilder
self.knobWidth = knobWidth
}
var body: some View {
return GeometryReader { geometry in
self.view(geometry: geometry) // function below
}
}
private func view(geometry: GeometryProxy) -> some View {
let frame = geometry.frame(in: .global)
let drag = DragGesture(minimumDistance: 0)
.onChanged { drag in
shouldStopPlayer = true
self.onDragChange(drag, frame)
}
.onEnded { drag in
shouldStopPlayer = false
//self.updatedValue = value
print("[debug] slider drag gesture ended, value = \(value)")
}
let offsetX = self.getOffsetX(frame: frame)
let knobSize = CGSize(width: knobWidth ?? frame.height, height: frame.height)
let barLeftSize = CGSize(width: CGFloat(offsetX + knobSize.width * 0.5), height: frame.height)
let barRightSize = CGSize(width: frame.width - barLeftSize.width, height: frame.height)
let modifiers = CustomSliderComponents(
barLeft: CustomSliderModifier(name: .barLeft, size: barLeftSize, offset: 0),
barRight: CustomSliderModifier(name: .barRight, size: barRightSize, offset: barLeftSize.width),
knob: CustomSliderModifier(name: .knob, size: knobSize, offset: offsetX))
return ZStack { viewBuilder(modifiers).gesture(drag) }
}
private func onDragChange(_ drag: DragGesture.Value,_ frame: CGRect) {
let width = (knob: Double(knobWidth ?? frame.size.height), view: Double(frame.size.width))
let xrange = (min: Double(0), max: Double(width.view - width.knob))
var value = Double(drag.startLocation.x + drag.translation.width) // knob center x
value -= 0.5*width.knob // offset from center to leading edge of knob
value = value > xrange.max ? xrange.max : value // limit to leading edge
value = value < xrange.min ? xrange.min : value // limit to trailing edge
value = value.convert(fromRange: (xrange.min, xrange.max), toRange: range)
//print("[debug] slider drag gesture detected, value = \(value)")
self.value = value
}
private func getOffsetX(frame: CGRect) -> CGFloat {
let width = (knob: knobWidth ?? frame.size.height, view: frame.size.width)
let xrange: (Double, Double) = (0, Double(width.view - width.knob))
let result = self.value.convert(fromRange: range, toRange: xrange)
return CGFloat(result)
}
}
extension Double {
func convert(fromRange: (Double, Double), toRange: (Double, Double)) -> Double {
// Example: if self = 1, fromRange = (0,2), toRange = (10,12) -> solution = 11
var value = self
value -= fromRange.0
value /= Double(fromRange.1 - fromRange.0)
value *= toRange.1 - toRange.0
value += toRange.0
return value
}
}
struct CustomSliderComponents {
let barLeft: CustomSliderModifier
let barRight: CustomSliderModifier
let knob: CustomSliderModifier
}
struct CustomSliderModifier: ViewModifier {
enum Name {
case barLeft
case barRight
case knob
}
let name: Name
let size: CGSize
let offset: CGFloat
func body(content: Content) -> some View {
content
.frame(width: (size.width >= 0) ? size.width : 0)
.position(x: size.width*0.5, y: size.height*0.5)
.offset(x: offset)
}
}
class AppDelegate: UIResponder, UIApplicationDelegate {
static var orientationLock = UIInterfaceOrientationMask.portrait
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return AppDelegate.orientationLock
}
}
非常感谢thoughts/ideas!
当遇到nan
问题时,我的第一步总是寻找除法运算。
在您的代码中,这看起来像您的 convert
函数(您怀疑的滑块的一部分)。在进行计算之前,我会检查有效值的范围:
func convert(fromRange: (Double, Double), toRange: (Double, Double)) -> Double {
//put a guard statement here to check that fromRange.0 isn't equal to fromRange.1 and that you won't get a 0 result. Return a default value otherwise.
var value = self
value -= fromRange.0
value /= Double(fromRange.1 - fromRange.0)
value *= toRange.1 - toRange.0
value += toRange.0
return value
}
最后,我设法创建了一个适合 1 个文件的最小可重现示例。这个异常要了我的命,整个周末都在试图找到原因。我无法用断点调试它,因为它在它的线程开始时被调用(xcode window 截图:https://d.pr/i/OPmq8Z )
MRE(最小可重现示例)的作用:
单击按钮 3 秒后,hVideoURL 的值设置为远程视频文件路径“https://...”。
设置 hVideoURL 的值会使 .sheet(item: hVideoURL) 出现
Sheet 包含一个播放远程视频的 AVPlayer。在某些情况下(有时在所有情况下,有时我需要重新连接物理设备才能获得此异常)它会触发异常:
2021-04-11 22:38:17.251165+0200 DetailedTesting[44173:3531565] *** Terminating app due to uncaught exception 'CALayerInvalidGeometry', reason: 'CALayer position contains NaN: [nan nan]. Layer: <CALayer:0x2811a80a0; position = CGPoint (2 8); bounds = CGRect (0 0; 455 4); delegate = <UIView: 0x11bf18f60; frame = (2 8; 455 4); anchorPoint = (0, 0); autoresizesSubviews = NO; layer = <CALayer: 0x2811a80a0>>; opaque = YES; allowsGroupOpacity = YES; anchorPoint = CGPoint (0 0); _swiftUI_displayListID = 11; backgroundColor = <CGColor 0x2835f2d60> [<CGColorSpace 0x2835e09c0> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB IEC61966-2.1; extended range)] ( 1 1 1 0.232 )>'
*** First throw call stack:
(0x19e95d86c 0x1b3976c50 0x19e8564a4 0x1a1d85db4 0x1a1d85ce4 0x1a55f5378 0x1a58f1e38 0x1a58efb78 0x1a58eea8c 0x1a56585f0 0x1a53b09d0 0x1a5658158 0x1a5657f0c 0x1a565793c 0x1a5657f2c 0x1a565793c 0x1a56576d0 0x1a5657580 0x1a57167f4 0x1a52ca0ac 0x1a5868434 0x1a585e2c8 0x1a59e8f90 0x1a59e8fc4 0x1a185fec4 0x1a1d7a644 0x1a1d7ab18 0x1a1d8f30c 0x1a1cd4640 0x1a1cffb08 0x1a1d00e98 0x19e8d8358 0x19e8d25c4 0x19e8d2b74 0x19e8d221c 0x1b649c784 0x1a1312ee8 0x1a131875c 0x1a58a2210 0x1a58a219c 0x1a53daf90 0x100b17b48 0x100b17be8 0x19e5926b0)
libc++abi.dylib: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'CALayerInvalidGeometry', reason: 'CALayer position contains NaN: [nan nan]. Layer: <CALayer:0x2811a80a0; position = CGPoint (2 8); bounds = CGRect (0 0; 455 4); delegate = <UIView: 0x11bf18f60; frame = (2 8; 455 4); anchorPoint = (0, 0); autoresizesSubviews = NO; layer = <CALayer: 0x2811a80a0>>; opaque = YES; allowsGroupOpacity = YES; anchorPoint = CGPoint (0 0); _swiftUI_displayListID = 11; backgroundColor = <CGColor 0x2835f2d60> [<CGColorSpace 0x2835e09c0> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB IEC61966-2.1; extended range)] ( 1 1 1 0.232 )>'
terminating with uncaught exception of type NSException
Sheet有视频播放器(出现异常时):
我的想法:
我发现如果我评论“VideoPlayerControlsView()”,错误不会再次发生,所以问题可能是在我的 CustomSlider 对象位于 VideoPlayerControlsView 视图内。
我觉得可能是加载远程视频导致的,因为一开始视频没有加载,app不知道size/bounds 的 AVPlayer 对象,因此无法创建某些父视图(可能是 CustomerSlider),或者计算负数 width/height..
代码(MRE):
import SwiftUI
import AVFoundation
import Foundation
struct ContentView: View {
@State var hVideoURL: String?
@State var isPaused: Bool = false
var body: some View {
Button("Let's Go!") {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
print("settings isPaused to TRUE")
self.hVideoURL = "https://firebasestorage.googleapis.com/v0/b/fitma-e3043.appspot.com/o/flamelink%2Fmedia%2F1-horizontal.mov?alt=media&token=8f7dfc0f-0261-4a78-9eb0-6154ce1d9dfe"
print("[debug] hVideoURL = \(String(describing: hVideoURL))")
self.isPaused = true
}
}
.sheet(item: self.$hVideoURL, onDismiss: {
self.isPaused = false
print("resume playing main video")
}) { hVideoURLItem in
detailedVideoView(url: hVideoURLItem)
}
}
@ViewBuilder
func detailedVideoView(url: String) -> some View {
DetailedVideo(url: URL(string: url)!, isPaused: self.$isPaused)
.onAppear {
AppDelegate.orientationLock = UIInterfaceOrientationMask.landscapeLeft
UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
}
.onDisappear {
DispatchQueue.main.async {
AppDelegate.orientationLock = UIInterfaceOrientationMask.portrait
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
}
}
}
}
extension String: Identifiable {
public var id: String { self }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct DetailedVideo: View {
var url: URL
@Binding var isPaused: Bool
var body: some View {
ZStack {
DetailedPlayerView(hVideoURL: url)
}
}
}
struct DetailedPlayerView: View {
// The progress through the video, as a percentage (from 0 to 1)
@State private var videoPos: Double = 0
// The duration of the video in seconds
@State private var videoDuration: Double = 0
// Whether we're currently interacting with the seek bar or doing a seek
@State private var seeking = false
private var player: AVPlayer = AVPlayer()
init(hVideoURL: URL?) {
if hVideoURL != nil {
player = AVPlayer(url: hVideoURL!)
player.isMuted = true
player.play()
} else {
print("[debug] hVideoURL is nil")
}
}
var body: some View {
ZStack {
VideoPlayerView(videoPos: $videoPos,
videoDuration: $videoDuration,
seeking: $seeking,
player: player)
.ignoresSafeArea(.all)
.frame(width: UIScreen.screenHeight, height: UIScreen.screenWidth)
VStack {
Spacer()
VideoPlayerControlsView(videoPos: $videoPos,
videoDuration: $videoDuration,
seeking: $seeking,
player: player)
.frame(width: UIScreen.screenHeight - 2*70, height: 20)
.padding(.bottom, 20)
}
}
.onDisappear {
// When this View isn't being shown anymore stop the player
self.player.replaceCurrentItem(with: nil)
}
}
}
// This is the SwiftUI view which wraps the UIKit-based PlayerUIView above
struct VideoPlayerView: UIViewRepresentable {
@Binding private(set) var videoPos: Double
@Binding private(set) var videoDuration: Double
@Binding private(set) var seeking: Bool
let player: AVPlayer
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<VideoPlayerView>) {
// This function gets called if the bindings change, which could be useful if
// you need to respond to external changes, but we don't in this example
}
func makeUIView(context: UIViewRepresentableContext<VideoPlayerView>) -> UIView {
let uiView = VideoPlayerUIView(player: player,
videoPos: $videoPos,
videoDuration: $videoDuration,
seeking: $seeking
)
return uiView
}
static func dismantleUIView(_ uiView: UIView, coordinator: ()) {
guard let playerUIView = uiView as? VideoPlayerUIView else {
return
}
playerUIView.cleanUp()
}
}
class VideoPlayerUIView: UIView {
private let player: AVPlayer
private let playerLayer = AVPlayerLayer()
private let videoPos: Binding<Double>
private let videoDuration: Binding<Double>
private let seeking: Binding<Bool>
private var durationObservation: NSKeyValueObservation?
private var timeObservation: Any?
init(player: AVPlayer, videoPos: Binding<Double>, videoDuration: Binding<Double>, seeking: Binding<Bool>) {
self.player = player
self.videoDuration = videoDuration
self.videoPos = videoPos
self.seeking = seeking
super.init(frame: .zero)
backgroundColor = .lightGray
playerLayer.player = player
layer.addSublayer(playerLayer)
// Observe the duration of the player's item so we can display it
// and use it for updating the seek bar's position
durationObservation = player.currentItem?.observe(\.duration, changeHandler: { [weak self] item, change in
guard let self = self else { return }
self.videoDuration.wrappedValue = item.duration.seconds
})
// Observe the player's time periodically so we can update the seek bar's
// position as we progress through playback
timeObservation = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.5, preferredTimescale: 600), queue: nil) { [weak self] time in
guard let self = self else { return }
// If we're not seeking currently (don't want to override the slider
// position if the user is interacting)
guard !self.seeking.wrappedValue else {
return
}
// update videoPos with the new video time (as a percentage)
self.videoPos.wrappedValue = time.seconds / self.videoDuration.wrappedValue
}
}
required init?(coder: NSCoder) {
fatalError("[debug] init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
func cleanUp() {
// Remove observers we setup in init
durationObservation?.invalidate()
durationObservation = nil
if let observation = timeObservation {
player.removeTimeObserver(observation)
timeObservation = nil
}
}
}
// This is the SwiftUI view that contains the controls for the player
struct VideoPlayerControlsView : View {
@Binding private(set) var videoPos: Double
@Binding private(set) var videoDuration: Double
@Binding private(set) var seeking: Bool
@State var shouldStopPlayer: Bool = false
@State var player: AVPlayer
@State private var playerPaused = false
var body: some View {
HStack {
// Play/pause button
Button(action: togglePlayPause) {
Image(systemName: playerPaused ? "arrowtriangle.right.fill" : "pause.fill")
.foregroundColor(Color.red)
.contentShape(Rectangle())
.padding(.trailing, 10)
}
// Current video time
if videoPos.isFinite && videoPos.isCanonical && videoDuration.isFinite && videoDuration.isCanonical {
Text(Utility.formatSecondsToHMS(videoPos * videoDuration))
.foregroundColor(Color.red)
}
// Slider for seeking / showing video progress
CustomSlider(value: $videoPos, shouldStopPlayer: self.$shouldStopPlayer, range: (0, 1), knobWidth: 4) { modifiers in
ZStack {
Group {
Color(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 0.5799999833106995))//Color((red: 0.4, green: 0.3, blue: 1)
.opacity(0.4)
.frame(height: 4)
.modifier(modifiers.barRight)
Color.red//Color(red: 0.4, green: 0.3, blue: 1)
.frame(height: 4)
.modifier(modifiers.barLeft)
}
.cornerRadius(5)
VStack {
Image(systemName: "arrowtriangle.down.fill") // SF Symbol
.foregroundColor(Color.red)
.offset(y: -3)
}
.frame(width: 20, height: 20)
.contentShape(Rectangle())
.modifier(modifiers.knob)
}
}
.onChange(of: shouldStopPlayer) { _ in
if shouldStopPlayer == false {
print("[debug] shouldStopPlayer == false")
sliderEditingChanged(editingStarted: false)
} else {
if seeking == false {
print("[debug] shouldStopPlayer == true")
sliderEditingChanged(editingStarted: true)
}
}
}
.frame(height: 20)
// Video duration
if videoDuration.isCanonical && videoDuration.isFinite {
Text(Utility.formatSecondsToHMS(videoDuration))
.foregroundColor(Color.red)
}
}
.padding(.leading, 40)
.padding(.trailing, 40)
}
private func togglePlayPause() {
pausePlayer(!playerPaused)
}
private func pausePlayer(_ pause: Bool) {
playerPaused = pause
if playerPaused {
player.pause()
}
else {
player.play()
}
}
private func sliderEditingChanged(editingStarted: Bool) {
if editingStarted {
// Set a flag stating that we're seeking so the slider doesn't
// get updated by the periodic time observer on the player
seeking = true
pausePlayer(true)
}
// Do the seek if we're finished
if !editingStarted {
let targetTime = CMTime(seconds: videoPos * videoDuration,
preferredTimescale: 600)
player.seek(to: targetTime) { _ in
// Now the seek is finished, resume normal operation
self.seeking = false
self.pausePlayer(false)
}
}
}
}
class Utility: NSObject {
private static var timeHMSFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .positional
formatter.allowedUnits = [.minute, .second]
formatter.zeroFormattingBehavior = [.pad]
return formatter
}()
static func formatSecondsToHMS(_ seconds: Double) -> String {
return timeHMSFormatter.string(from: seconds) ?? "00:00"
}
}
extension UIScreen {
static let screenWidth = UIScreen.main.bounds.size.width
static let screenHeight = UIScreen.main.bounds.size.height
static let screenSize = UIScreen.main.bounds.size
}
struct CustomSlider<Component: View>: View {
@Binding var value: Double
var range: (Double, Double)
var knobWidth: CGFloat?
let viewBuilder: (CustomSliderComponents) -> Component
@Binding var shouldStopPlayer: Bool
init(value: Binding<Double>, shouldStopPlayer: Binding<Bool>, range: (Double, Double), knobWidth: CGFloat? = nil, _ viewBuilder: @escaping (CustomSliderComponents) -> Component
) {
_value = value
_shouldStopPlayer = shouldStopPlayer
self.range = range
self.viewBuilder = viewBuilder
self.knobWidth = knobWidth
}
var body: some View {
return GeometryReader { geometry in
self.view(geometry: geometry) // function below
}
}
private func view(geometry: GeometryProxy) -> some View {
let frame = geometry.frame(in: .global)
let drag = DragGesture(minimumDistance: 0)
.onChanged { drag in
shouldStopPlayer = true
self.onDragChange(drag, frame)
}
.onEnded { drag in
shouldStopPlayer = false
//self.updatedValue = value
print("[debug] slider drag gesture ended, value = \(value)")
}
let offsetX = self.getOffsetX(frame: frame)
let knobSize = CGSize(width: knobWidth ?? frame.height, height: frame.height)
let barLeftSize = CGSize(width: CGFloat(offsetX + knobSize.width * 0.5), height: frame.height)
let barRightSize = CGSize(width: frame.width - barLeftSize.width, height: frame.height)
let modifiers = CustomSliderComponents(
barLeft: CustomSliderModifier(name: .barLeft, size: barLeftSize, offset: 0),
barRight: CustomSliderModifier(name: .barRight, size: barRightSize, offset: barLeftSize.width),
knob: CustomSliderModifier(name: .knob, size: knobSize, offset: offsetX))
return ZStack { viewBuilder(modifiers).gesture(drag) }
}
private func onDragChange(_ drag: DragGesture.Value,_ frame: CGRect) {
let width = (knob: Double(knobWidth ?? frame.size.height), view: Double(frame.size.width))
let xrange = (min: Double(0), max: Double(width.view - width.knob))
var value = Double(drag.startLocation.x + drag.translation.width) // knob center x
value -= 0.5*width.knob // offset from center to leading edge of knob
value = value > xrange.max ? xrange.max : value // limit to leading edge
value = value < xrange.min ? xrange.min : value // limit to trailing edge
value = value.convert(fromRange: (xrange.min, xrange.max), toRange: range)
//print("[debug] slider drag gesture detected, value = \(value)")
self.value = value
}
private func getOffsetX(frame: CGRect) -> CGFloat {
let width = (knob: knobWidth ?? frame.size.height, view: frame.size.width)
let xrange: (Double, Double) = (0, Double(width.view - width.knob))
let result = self.value.convert(fromRange: range, toRange: xrange)
return CGFloat(result)
}
}
extension Double {
func convert(fromRange: (Double, Double), toRange: (Double, Double)) -> Double {
// Example: if self = 1, fromRange = (0,2), toRange = (10,12) -> solution = 11
var value = self
value -= fromRange.0
value /= Double(fromRange.1 - fromRange.0)
value *= toRange.1 - toRange.0
value += toRange.0
return value
}
}
struct CustomSliderComponents {
let barLeft: CustomSliderModifier
let barRight: CustomSliderModifier
let knob: CustomSliderModifier
}
struct CustomSliderModifier: ViewModifier {
enum Name {
case barLeft
case barRight
case knob
}
let name: Name
let size: CGSize
let offset: CGFloat
func body(content: Content) -> some View {
content
.frame(width: (size.width >= 0) ? size.width : 0)
.position(x: size.width*0.5, y: size.height*0.5)
.offset(x: offset)
}
}
class AppDelegate: UIResponder, UIApplicationDelegate {
static var orientationLock = UIInterfaceOrientationMask.portrait
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return AppDelegate.orientationLock
}
}
非常感谢thoughts/ideas!
当遇到nan
问题时,我的第一步总是寻找除法运算。
在您的代码中,这看起来像您的 convert
函数(您怀疑的滑块的一部分)。在进行计算之前,我会检查有效值的范围:
func convert(fromRange: (Double, Double), toRange: (Double, Double)) -> Double {
//put a guard statement here to check that fromRange.0 isn't equal to fromRange.1 and that you won't get a 0 result. Return a default value otherwise.
var value = self
value -= fromRange.0
value /= Double(fromRange.1 - fromRange.0)
value *= toRange.1 - toRange.0
value += toRange.0
return value
}