WKWebView stop audio when backgrounding app - ios

I have a WKWebView which is playing HTML5 video. When I click the home button and the app enters the background I can still hear the audio playing. How can I pause the HTML 5 video to stop the background audio from playing. I tried just loading a blank page when the app enters background which works,
let url = URL(string: "about:blank")
let request = URLRequest(url:url!)
self.webView.load(request)
but I want to be able to start the video at the same point when the user returns from the background so it is not a viable solution since it just loads the blank page when I open the app back up.

I was able to achive this by injecting JavaScript when the app entered the background which would pause any video elements that are playing.
NotificationCenter.default.addObserver(self, selector: #selector(enteredBackground(notification:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
#objc func enteredBackground(notification: Notification) {
let script = "var vids = document.getElementsByTagName('video'); for( var i = 0; i < vids.length; i++ ){vids.item(i).pause()}"
self.webView.evaluateJavaScript(script, completionHandler:nil)
}

Related

iOS How to trigger video to keep playing after exiting fullscreen?

I'm building a website that plays video in iOS. I have a fullscreen button working in iOS however the video pauses when exiting fullscreen. Does anyone know a way to either force the video to continue to play upon exiting fullscreen or how to set up a listener that triggers the video to autoplay upon exiting fullscreen?
here is my code:
<script>
var video = document.getElementById('tv'),
play = document.getElementById('fullscreenbutton'),
time;
video.addEventListener('webkitbeginfullscreen', function() {
play.innerText = '';
window.clearInterval(time);
});
video.addEventListener('webkitendfullscreen', function() {
tv.autoplay();
});
play.addEventListener('touchstart', function() {
time = window.setInterval(function() {
try {
video.webkitEnterFullscreen();
}
catch(e) {}
}, 250);
play.innerText = 'loading ...';
tv.play();
});
</script>
'''
You can use WKScriptMessageHandler in your iOS app when a button to exit fullscreen mode is tapped then resume your video player.
Here is a workaround on how I would handle this situation.
Native iOS + JavaScript
1 Step: Website front end part
When exit full screen button is tapped on your website emit an event give it a unique name something like "exitFullScreenEvent"
2 Step: iOS App
import WebKit
ViewController: WKScriptMessageHandler {
// Helper method to set configurations for your webView in iOS
func configureWebView() {
///Add the script message handler into the content controller.
let contentController = WKUserContentController()
/// Give it the same name as on the web front-end part
contentController.add(self, name: "exitFullScreenEvent")
let config = WKWebViewConfiguration()
config.userContentController = contentController
/// Most important, when initilizing your web view pass the configuration above
let webView = WKWebView(frame: .zero, configuration: config)
// Layout/add constraints to your webView
// .....
}
// Delegate method to listen an event emitted from your website
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
// TODO: - Resume/ Play your video
}
}
Pure Website (Javascript only)
For website only which runs in pure Javascript without any iOS related components and on Safari browser you might want to check this webkitjs displayingfullscreen
I Haven't used it before but it worth reading the doc.
But another work around without putting lots of effort is to compare your video player's screen size with the browsers window size ie.. check if browser is full screen if so get the full screen width if it's the same with your video player's size then video is in full screen.

CADisplayLink stutters when built from archive

I have project that is rendering video playback and applying CIFilters to it. I know that I can use video composition to get video with filters, but problems is that filters needs to be swipeable (with preview of next filter so we're using mask for 1st imageview and filtering 2nd one with next filter).
func displayLinkDidRefresh(link: CADisplayLink){
let itemTime = videoOutput.itemTime(forHostTime: CACurrentMediaTime())
if videoOutput.hasNewPixelBuffer(forItemTime: itemTime) {
if let pixelBuffer = videoOutput.copyPixelBuffer(forItemTime: itemTime, itemTimeForDisplay: nil){
unfilteredImage = CIImage(cvImageBuffer: pixelBuffer)
displayFilteredImage(unfilteredImage: unfilteredImage)
}
}
}
This is the code used to create AVPlayer instance and CADisplayLink:
player = AVPlayer(playerItem: item)
player.isMuted = true
displayLink = CADisplayLink(target: self, selector: #selector(displayLinkDidRefresh(link:)))
displayLink!.preferredFramesPerSecond = 24
displayLink!.add(to: RunLoop.main, forMode: RunLoopMode.commonModes)
NotificationCenter.default.addObserver(self, selector: #selector(self.playerItemDidReachEnd(notification:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.player.currentItem)
When running from debugger I experience no stutter (or it is at minimum level), but when running build from archive it is stuttering a lot. What I do to test is deleting app from phone and then instal it on the phone and test, as said debug builds are fine, but archive are creating issues. Any input on this?
EDIT 1:
Managed to get it to work better, but still stuttering after attaching video composition to player item when item and player status is readyToPlay.
After some time playing with this I found the issue, nothing is wrong with the code. Issue was that we have Appsee analytics inside app and we needed to pause it on these screens in order for them to render properly. Reason why we didn't experience these issues in debug mode was the AppDelegate if condition that was preventing Appsee to work in debug environment.

Background Playback Of Audio Stream After Interruption, iOS Swift

I Have an app that uses AVPlayer() to play a music stream from the web. I have everything setup to play in background but when a call comes in (or any other interrupt) the playback will not resume after the interrupt is over. To be specific if the interrupt is dissmised pretty quickly playback will resume but if I have a long phone call for example playback will not resume.
This only happens if my app is in the background as well. If my app is in the foreground when an interrupt comes in everything works.
I have my interrupt notification set up as:
func playInterrupt(notification: NSNotification) {
var info = notification.userInfo!
var intValue: UInt = 0
(info[AVAudioSessionInterruptionTypeKey] as! NSValue).getValue(&intValue)
if let type = AVAudioSessionInterruptionType(rawValue: intValue) {
switch type {
case .began:
print("began")
pause()
case .ended:
print("ended")
play()
}
}
Using
AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: AVAudioSessionCategoryOptions.mixWithOthers )
Fixes the problem but then if I start playing from another audio source, they both overlay each other. Ideally I need my app to stop playback in that situation.
Any suggestions on how to achieve this?
Thanks in advance

Audio playback lock screen controls not displaying on iPhone

I am testing this using iOS 10.2 on my actual iPhone 6s device.
I am playing streamed audio and am able to play/pause audio, skip tracks, etc. I also have enabled background modes and the audio plays in the background and continues through a playlist properly. The only issue I am having is getting the lock screen controls to show up. Nothing displays at all...
In viewDidLoad() of my MainViewController, right when my app launches, I call this...
func setupAudioSession(){
UIApplication.shared.beginReceivingRemoteControlEvents()
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: AVAudioSessionCategoryOptions.mixWithOthers)
self.becomeFirstResponder()
do {
try AVAudioSession.sharedInstance().setActive(true)
print("AVAudioSession is Active")
} catch let error as NSError {
print(error.localizedDescription)
}
} catch let error as NSError {
print(error.localizedDescription)
}
}
and then in my AudioPlayer class after I begin playing audio I call ...
func setupLockScreen(){
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.nextTrackCommand.isEnabled = true
commandCenter.nextTrackCommand.addTarget(self, action:#selector(skipTrack))
MPNowPlayingInfoCenter.default().nowPlayingInfo = [MPMediaItemPropertyTitle: "TESTING"]
}
When I lock my iPhone and then tap the power button again to go to the lock screen, the audio controls are not displayed at all. It is as if no audio is playing, I just see my normal background photo. Also no controls are displayed in the control panel (swiping up on home screen and then swiping left to where the music controls should be).
Is the issue because I am not using AVAudioPlayer or AVPlayer? But then how does, for example, Spotify get the lock screen controls to display using their own custom audio player? Thanks for any advice / help
The issue turned out to be this line...
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: AVAudioSessionCategoryOptions.duckOthers)
Once I changed it to
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: [])
everything worked fine. So it seems that passing in any argument for AVAudioSessionCategoryPlaybackOptions causes the lock screen controls to not display. I also tried passing in .mixWithOthers an that too caused the lock screen controls to not be displayed
In Swift 4. This example is only to show the player on the lock screen and works with iOS 11. To know how to play auidio on the device you can follow this thread https://stackoverflow.com/a/47710809/1283517
import MediaPlayer
import AVFoundation
Declare player
var player : AVPlayer?
Now create a function
func setupLockScreen(){
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.nextTrackCommand.isEnabled = true
commandCenter.togglePlayPauseCommand.addTarget(self, action: #selector(controlPause))
MPNowPlayingInfoCenter.default().nowPlayingInfo = [MPMediaItemPropertyTitle: currentStation]
}
now create a function for control play and pause event. I have created a BOOL "isPlaying" to determine the status of the player.
#objc func controlPause() {
if isPlaying == true {
player?.pause()
isPlaying = false
} else {
player?.play()
isPlaying = true
}
}
And ready. Now the player will be displayed on the lock screen
Yes, for the lock screen to work you need to use iOS APIs to play audio. Not sure how Spotify does it but they may be using a second audio session in parallel for this purpose and use the controls to control both. Your background handler (the singleton in my case) could start playing the second audio with 0 volume when it goes into background and stop it when in foreground. I haven't tested it myself but an option to try.

Why is AVPlayer not continuing to next song while in background?

I have been streaming music from remote source using AVPlayer. I get URLs, use one to create an AVPlayerItem, which i then associate with my instance of AVPlayer. I add an observer to the item that I associate with the player to observe when the item finishes playing ( AVPlayerItemDidPlayToEndTimeNotification ). When the observer notifies me at the item end, I then create a new AVPlayerItem and do it all over again. This works well in the foreground AND in the background on iOS 9.2.
Problem: Since I have updated to iOS 9.3 this does not work in the background. Here is the relevant code:
var portionToBurffer = Double()
var player = AVPlayer()
func prepareAudioPlayer(songNSURL: NSURL, portionOfSongToBuffer: Double){
self.portionToBuffer = portionOfSongToBuffer
//create AVPlayerItem
let createdItem = AVPlayerItem(URL: songNSURL)
//Associate createdItem with AVPlayer
player = AVPlayer(playerItem: createdItem)
//Add item end observer
NSNotificationCenter.defaultCenter().addObserver(self, selector: "playerItemDidReachEnd:", name: AVPlayerItemDidPlayToEndTimeNotification, object: player.currentItem)
//Use KVO to see how much is loaded
player.currentItem?.addObserver(self, forKeyPath: "loadedTimeRanges", options: .New, context: nil)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if keyPath == "loadedTimeRanges" {
if let loadedRangeAsNSValueArray = player.currentItem?.loadedTimeRanges {
let loadedRangeAsCMTimeRange = loadedRangeAsNSValueArray[0].CMTimeRangeValue
let endPointLoaded = CMTimeRangeGetEnd(loadedRangeAsCMTimeRange)
let secondsLoaded = CMTimeGetSeconds(endPointLoaded)
print("the endPointLoaded is \(secondsLoaded) and the duration is \(CMTimeGetSeconds((player.currentItem?.duration)!))")
if secondsLoaded >= portionToBuffer {
player.currentItem?.removeObserver(self, forKeyPath: "loadedTimeRanges")
player.play()
}
}
}
}
func playerItemDidReachEnd(notification: NSNotification){
recievedItemEndNotification()
}
func recievedItemEndNotification() {
//register background task
bgTasker.registerBackgroundTask()
if session.playlistSongIndex == session.playlistSongTitles.count-1 {
session.playlistSongIndex = 0
} else {
session.playlistSongIndex += 1
}
prepareAudioPlayer(songURL: session.songURLs[session.playlistSongIndex], portionOfSongToBuffer: 30.00)
}
I have set breakpoints to see that player.play() IS being called when in the background. When i print player.rate it reads 0.0. I have checked the property playbackLikelyToKeepUp of the AVPlayerItem and it is true. I have confirmed also that the new URL is successfully used to create the new AVPlayerItem and associated with the AVPlayer when the app is in the background. I have turned audio and airplay background capabilities on and I have even opened up a finite length background task (in code above as bgTasker.registerBackgroundTask). No idea what is going on.
I found THIS but i'm not sure it helps. Any advice would be great, thanks
When the observer notifies me at the item end, I then create a new AVPlayerItem and do it all over again
But the problem is that meanwhile play stops, and the rule is that background playing is permitted only so long as you were playing in the foreground and continue to play in the background.
I would suggest using AVQueuePlayer instead of AVPlayer. This will allow you to enqueue the next item while the current item is still playing — and thus, we may hope, this will count as continuing to play.
I encountered the similar problem, and I searched lots of websites on google, but didn't find the answer.
The Phenomenon
The problem of my app is that when I start play an audio, and turn the app to background, it will finish the playing of the current audio, and when playing the second audio, it will load some data, but then it stopped, if I turn the app to foreground, it will play the audio.
Solution
My solution is to add the following call.
UIApplication.shared.beginReceivingRemoteControlEvents()
So enable background audio capabilities is not enough, we need to begin receiving remote controller events.

Resources