肇鑫的技术博客

业精于勤,荒于嬉

VideoPlayer Turns Black Screen When Replace a Player Item

I was working on a project that shows videos from the Photo Library. However, each time I replaced the current video, the VideoPlayer turned black screen for a while.

I had no idea why this happened so I looked for what Photos app did for videos. It turned out that Photos showed a preview of the video first and then automatically played the video when the video data was loaded.

I did the same. First I fetched the preview image. When the preview was downloaded, I fetched the video.

The issue was the same. After some investigation, I found that the method of fetching video was called twice, which was not expected. Then I found that was because fetching preview may be called multiple times. Photo Library may provide a low quality image first then provide the full quality image. This issue was solved by read the info dictionary of the fetching preview result. But the black screen issue was still there.

Maybe the AVPlayer or PlayerItem was not ready?

if let video {
    player.replaceCurrentItem(with: video)
    
    Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
        if player.status == .readyToPlay {
            playerItem = video
            timer.invalidate()
        }
    }
}

The result was the same.

Maybe there was still some time between the player was ready and the player was really playing.

if let video {
    player.replaceCurrentItem(with: video)
    
    Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
        if player.status == .readyToPlay {
            player.play()
            
            if player.timeControlStatus == .playing {
                playerItem = video
                timer.invalidate()
            }
        }
    }
}

The issue solved.

Conclusion

  1. Do not create another AVPlayer each time. Just create an empty player and replace the replaceCurrentItem.
  2. There was time gap between player ready and player real playing. So you need to play the player first when player was ready and then updated the UI after the player was really playing.
  3. I was using a timer instead of a notification as the former was more simple to use.

SwiftUI with Preview

Some times, only the preview of SwiftUI view crashes. Both the simulator and the real device don't crash. So we need to do extra works or don't run some methods if the target is a preview.

We can't identify preview from debug, so we add a isPreview variable under debug, and that won't leave the variable in release version.

struct SomeView : View {
    #if DEBUG
    var isPreview = false
    #endif
    var body: some View {
        Text("Hello World!")
            .onAppear {
                #if DEBUG
                if isPreview {
                    return
                }
                #endif
                
                foo()
            }
    }
    
    private func foo() {
        ...
    }
}

struct SomeView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            SomeView(isPreview: true)
                .previewDevice(PreviewDevice(rawValue: "iPhone SE 2"))
                .previewDisplayName("iPhone SE 2")
            
            SomeView(isPreview: true)
                .previewDevice(PreviewDevice(rawValue: "iPhone 14 Pro"))
                .previewDisplayName("iPhone 14 Pro")
        }
    }
}

Get Critical Alert Permission from User instead of Apple

When a user uses "Do not disturb" mode, your notifications no longer get sound and banner, unless your app can post a Critical Alert.

However, to get the entitlement of critical alert, you have to ask Apple to give your permission. Someone complains that their requests to Apple even took months without a response.

Critical Alerts entitlement

I found a new way to get the permission directly from the user, instead of Apple.

Steps

UNUserNotificationCenter.current().requestAuthorization(options: [.sound, .alert, .criticalAlert]) { granted, error in
    guard error == nil else {
        DispatchQueue.main.async {
            NSSound.beep()
            let alert = NSAlert(error: error!)
            alert.runModal()
        }
        
        return
    }
}

Above code will get an error as you don't have the entitlement of critical alert. But we could use code below:

UNUserNotificationCenter.current().requestAuthorization(options: [.sound, .alert]) { granted, error in
    guard error == nil else {
        DispatchQueue.main.async {
            NSSound.beep()
            let alert = NSAlert(error: error!)
            alert.runModal()
        }
        
        return
    }
    
    if granted {
        UNUserNotificationCenter.current().requestAuthorization(options: [.sound, .alert, .criticalAlert]) { granted, error in
            /* You will get an error here as you don't have the critical alert entitlement.
                But since you have already got the permission for normal notifications, you now get a disabled critical alert permission.
                You are just one step to the critical alert. Ask your user to manually enable the permisssion.
                The error should be igonred here.
                */

            UNUserNotificationCenter.current().getNotificationSettings { settings in
                if settings.criticalAlertSetting != .enabled {
                    DispatchQueue.main.async {
                        NSSound.beep()
                        showCriticalAlert = true
                    }
                }
            }
        }
    }
}

We request the permission twice. First, we request the common notification permission. When a user allow that, we request the critical alert permission. Since we have already got the common notification permission the second permission request won't pop up. Also as we don't have critical alert entitlement from Apple, we will get an error. But the error can be ignored. Then when you open the app's notification settings, there is a critical alert permission, with disabled state. So what you only need to do is to show an alert and ask the user to enable the critical alert if the user wants to get notified in "Do not disturb" mode.

Sample App

I have already got an sample app on sale in App Store. You can try it for free.

Stand Reminder