Timeline Progress bar for AVPlayer - iphone

AVPlayer is fully customizable, unfortunately there are convenient methods in AVPlayer for showing the time line progress bar.
AVPlayer *player = [AVPlayer playerWithURL:URL];
AVPlayerLayer *playerLayer = [[AVPlayerLayer playerLayerWithPlayer:avPlayer] retain];[self.view.layer addSubLayer:playerLayer];
I have an progress bar that indicates the how video has been played, and how much remained just as like MPMoviePlayer.
So how to get the timeline of video from AVPlayer and how to update the progress bar
Suggest me.

Please use the below code which is from apple example code "AVPlayerDemo".
double interval = .1f;
CMTime playerDuration = [self playerItemDuration]; // return player duration.
if (CMTIME_IS_INVALID(playerDuration))
{
return;
}
double duration = CMTimeGetSeconds(playerDuration);
if (isfinite(duration))
{
CGFloat width = CGRectGetWidth([yourSlider bounds]);
interval = 0.5f * duration / width;
}
/* Update the scrubber during normal playback. */
timeObserver = [[player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(interval, NSEC_PER_SEC)
queue:NULL
usingBlock:
^(CMTime time)
{
[self syncScrubber];
}] retain];
- (CMTime)playerItemDuration
{
AVPlayerItem *thePlayerItem = [player currentItem];
if (thePlayerItem.status == AVPlayerItemStatusReadyToPlay)
{
return([playerItem duration]);
}
return(kCMTimeInvalid);
}
And in syncScrubber method update the UISlider or UIProgressBar value.
- (void)syncScrubber
{
CMTime playerDuration = [self playerItemDuration];
if (CMTIME_IS_INVALID(playerDuration))
{
yourSlider.minimumValue = 0.0;
return;
}
double duration = CMTimeGetSeconds(playerDuration);
if (isfinite(duration) && (duration > 0))
{
float minValue = [ yourSlider minimumValue];
float maxValue = [ yourSlider maximumValue];
double time = CMTimeGetSeconds([player currentTime]);
[yourSlider setValue:(maxValue - minValue) * time / duration + minValue];
}
}

Thanks to iOSPawan for the code!
I simplified the code to the necessary lines. This might be more clear to understand the concept. Basically I have implemented it like this and it works fine.
Before starting the video:
__weak NSObject *weakSelf = self;
[_player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(1.0 / 60.0, NSEC_PER_SEC)
queue:NULL
usingBlock:^(CMTime time){
[weakSelf updateProgressBar];
}];
[_player play];
Then you need to have a method to update your progress bar:
- (void)updateProgressBar
{
double duration = CMTimeGetSeconds(_playerItem.duration);
double time = CMTimeGetSeconds(_player.currentTime);
_progressView.progress = (CGFloat) (time / duration);
}

let progressView = UIProgressView(progressViewStyle: UIProgressViewStyle.Bar)
self.view.addSubview(progressView)
progressView.constrainHeight("\(1.0/UIScreen.mainScreen().scale)")
progressView.alignLeading("", trailing: "", toView: self.view)
progressView.alignBottomEdgeWithView(self.view, predicate: "")
player.addPeriodicTimeObserverForInterval(CMTimeMakeWithSeconds(1/30.0, Int32(NSEC_PER_SEC)), queue: nil) { time in
let duration = CMTimeGetSeconds(playerItem.duration)
progressView.progress = Float((CMTimeGetSeconds(time) / duration))
}

I know it's an old question, but someone may find it useful. It's only Swift version:
//set the timer, which will update your progress bar. You can use whatever time interval you want
private func setupProgressTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 0.02, repeats: true, block: { [weak self] (completion) in
guard let self = self else { return }
self.updateProgress()
})
}
//update progression of video, based on it's own data
private func updateProgress() {
guard let duration = player?.currentItem?.duration.seconds,
let currentMoment = player?.currentItem?.currentTime().seconds else { return }
progressBar.progress = Float(currentMoment / duration)
}

Swifty answer to get progress:
private func addPeriodicTimeObserver() {
// Invoke callback every half second
let interval = CMTime(seconds: 0.5,
preferredTimescale: CMTimeScale(NSEC_PER_SEC))
// Queue on which to invoke the callback
let mainQueue = DispatchQueue.main
// Add time observer
self.playerController?.player?.addPeriodicTimeObserver(forInterval: interval, queue: mainQueue) { [weak self] time in
let currentSeconds = CMTimeGetSeconds(time)
guard let duration = self?.playerController?.player?.currentItem?.duration else { return }
let totalSeconds = CMTimeGetSeconds(duration)
let progress: Float = Float(currentSeconds/totalSeconds)
print(progress)
}
}
Ref

In my case, the following code works Swift 3:
var timeObserver: Any?
override func viewDidLoad() {
........
let interval = CMTime(seconds: 0.05, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
timeObserver = avPlayer.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { elapsedTime in
self.updateSlider(elapsedTime: elapsedTime)
})
}
func updateSlider(elapsedTime: CMTime) {
let playerDuration = playerItemDuration()
if CMTIME_IS_INVALID(playerDuration) {
seekSlider.minimumValue = 0.0
return
}
let duration = Float(CMTimeGetSeconds(playerDuration))
if duration.isFinite && duration > 0 {
seekSlider.minimumValue = 0.0
seekSlider.maximumValue = duration
let time = Float(CMTimeGetSeconds(elapsedTime))
seekSlider.setValue(time, animated: true)
}
}
private func playerItemDuration() -> CMTime {
let thePlayerItem = avPlayer.currentItem
if thePlayerItem?.status == .readyToPlay {
return thePlayerItem!.duration
}
return kCMTimeInvalid
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
avPlayer.removeTimeObserver(timeObserver!)
}

for timeline i do this
-(void)changeSliderValue {
double duration = CMTimeGetSeconds(self.player.currentItem.duration);
[lengthSlider setMaximumValue:(float)duration];
lengthSlider.value = CMTimeGetSeconds([self.player currentTime]);
int seconds = lengthSlider.value,minutes = seconds/60,hours = minutes/60;
int secondsRemain = lengthSlider.maximumValue - seconds,minutesRemain = secondsRemain/60,hoursRemain = minutesRemain/60;
seconds = seconds-minutes*60;
minutes = minutes-hours*60;
secondsRemain = secondsRemain - minutesRemain*60;
minutesRemain = minutesRemain - hoursRemain*60;
NSString *hourStr,*minuteStr,*secondStr,*hourStrRemain,*minuteStrRemain,*secondStrRemain;
hourStr = hours > 9 ? [NSString stringWithFormat:#"%d",hours] : [NSString stringWithFormat:#"0%d",hours];
minuteStr = minutes > 9 ? [NSString stringWithFormat:#"%d",minutes] : [NSString stringWithFormat:#"0%d",minutes];
secondStr = seconds > 9 ? [NSString stringWithFormat:#"%d",seconds] : [NSString stringWithFormat:#"0%d",seconds];
hourStrRemain = hoursRemain > 9 ? [NSString stringWithFormat:#"%d",hoursRemain] : [NSString stringWithFormat:#"0%d",hoursRemain];
minuteStrRemain = minutesRemain > 9 ? [NSString stringWithFormat:#"%d",minutesRemain] : [NSString stringWithFormat:#"0%d",minutesRemain];
secondStrRemain = secondsRemain > 9 ? [NSString stringWithFormat:#"%d",secondsRemain] : [NSString stringWithFormat:#"0%d",secondsRemain];
timePlayed.text = [NSString stringWithFormat:#"%#:%#:%#",hourStr,minuteStr,secondStr];
timeRemain.text = [NSString stringWithFormat:#"-%#:%#:%#",hourStrRemain,minuteStrRemain,secondStrRemain];
And import CoreMedia framework
lengthSlider is UISlider

I took the answers from the iOSPawan and Raphael and then adapted to my needs.
So I have music and UIProgressView which is always in loop and when you go to the next screen and come back the the song and the bar continued where they were left.
Code:
#interface YourClassViewController (){
NSObject * periodicPlayerTimeObserverHandle;
}
#property (nonatomic, strong) AVPlayer *player;
#property (nonatomic, strong) UIProgressView *progressView;
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
if(_player != nil && ![self isPlaying])
{
[self musicPlay];
}
}
-(void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
if (_player != nil) {
[self stopPlaying];
}
}
// ----------
// PLAYER
// ----------
-(BOOL) isPlaying
{
return ([_player rate] > 0);
}
-(void) musicPlay
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(playerItemDidReachEnd:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:[_player currentItem]];
__weak typeof(self) weakSelf = self;
periodicPlayerTimeObserverHandle = [_player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(1.0 / 60.0, NSEC_PER_SEC)
queue:NULL
usingBlock:^(CMTime time){
[weakSelf updateProgressBar];
}];
[_player play];
}
-(void) stopPlaying
{
#try {
if(periodicPlayerTimeObserverHandle != nil)
{
[_player removeTimeObserver:periodicPlayerTimeObserverHandle];
periodicPlayerTimeObserverHandle = nil;
}
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
[_player pause];
}
#catch (NSException * __unused exception) {}
}
-(void) playPreviewSong:(NSURL *) previewSongURL
{
[self configureAVPlayerAndPlay:previewSongURL];
}
-(void) configureAVPlayerAndPlay: (NSURL*) url {
if(_player)
[self stopPlaying];
AVAsset *audioFileAsset = [AVURLAsset URLAssetWithURL:url options:nil];
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:audioFileAsset];
_player = [AVPlayer playerWithPlayerItem:playerItem];
[_player addObserver:self forKeyPath:#"status" options:0 context:nil];
CRLPerformBlockOnMainThreadAfterDelay(^{
NSError *loadErr;
if([audioFileAsset statusOfValueForKey:#"playable" error:&loadErr] == AVKeyValueStatusLoading)
{
[audioFileAsset cancelLoading];
[self stopPlaying];
[self showNetworkError:NSLocalizedString(#"Could not play file",nil)];
}
}, NETWORK_REQUEST_TIMEOUT);
}
- (void)updateProgressBar
{
double currentTime = CMTimeGetSeconds(_player.currentTime);
if(currentTime <= 0.05){
[_progressView setProgress:(float)(0.0) animated:NO];
return;
}
if (isfinite(currentTime) && (currentTime > 0))
{
float maxValue = CMTimeGetSeconds(_player.currentItem.asset.duration);
[_progressView setProgress:(float)(currentTime/maxValue) animated:YES];
}
}
-(void) showNetworkError:(NSString*)errorMessage
{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(#"No connection", nil) message:errorMessage preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(#"OK", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
// do nothing
}]];
[self presentViewController:alert animated:YES completion:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (object == _player && [keyPath isEqualToString:#"status"]) {
if (_player.status == AVPlayerStatusFailed) {
[self showNetworkError:NSLocalizedString(#"Could not play file", nil)];
} else if (_player.status == AVPlayerStatusReadyToPlay) {
NSLog(#"AVPlayerStatusReadyToPlay");
[TLAppAudioAccess setAudioAccess:TLAppAudioAccessType_Playback];
[self musicPlay];
} else if (_player.status == AVPlayerItemStatusUnknown) {
NSLog(#"AVPlayerItemStatusUnknown");
}
}
}
- (void)playerItemDidReachEnd:(NSNotification *)notification {
if ([notification.object isEqual:self.player.currentItem])
{
[self.player seekToTime:kCMTimeZero];
[self.player play];
}
}
-(void) dealloc{
#try {
[_player removeObserver:self forKeyPath:#"status"];
}
#catch (NSException * __unused exception) {}
[self stopPlaying];
_player = nil;
}

technically you don't need a timer for this one.
Just add property
private var isUserDragingSlider: Bool = false
Then you need to set up a slider target.
STEP 1
self.timeSlider.addTarget(self, action: #selector(handleSliderChangeValue(slider:event:)), for: .allEvents)
and in func handleSliderChangeValue you need to add this:
STEP 2
#objc
func handleSliderChangeValue(slider: UISlider, event: UIEvent) {
if let touchEvent = event.allTouches?.first {
switch touchEvent.phase {
case .began:
self.isUserDragingSlider = true
case .ended:
self.isUserDragingSlider = false
self.updateplayerWithSliderChangeValue()
default:
break
}
}
}
and in the end, you need to update your player with the selected time from a slider.
STEP 3
func updateplayerWithSliderChangeValue() {
if let duration = player?.currentItem?.duration {
let totalSeconds = CMTimeGetSeconds(duration)
if !(totalSeconds.isNaN || totalSeconds.isInfinite) {
let newCurrentTime: TimeInterval = Double(self.timeSlider.value) * CMTimeGetSeconds(duration)
let seekToTime: CMTime = CMTimeMakeWithSeconds(newCurrentTime, preferredTimescale: 600)
self.player?.seek(to: seekToTime)
self.isUserDragingSlider.toggle()
}
}
}
And last thing, You need to update your code. Video is updating your slider.
STEP 4
func setupSliderValue(_ seconds: Float64) {
guard !(seconds.isNaN || seconds.isInfinite) else {
return
}
if !isUserDragingSlider {
if let duration = self.player?.currentItem?.duration {
let durationInSeconds = CMTimeGetSeconds(duration)
self.timeSlider.value = Float(seconds / durationInSeconds)
}
}
}
So main problem here is that when we move the slider we have a conflict between updating the slider ( from video ) and updating the video with our slider change.
That is why the slider is not working well. When you block updates from video time to slider with isUserDragingSlider all is working fine.

Related

AVPlayer - Fast Backward / Forward Stream

This is my code in the viewDidLoad :
AVPlayerItem* playerItem = [AVPlayerItem playerItemWithURL:[NSURL URLWithString:#"http://groove.wavestreamer.com:7321/listen.pls?sid=1"]];
[playerItem addObserver:self forKeyPath:#"timedMetadata" options:NSKeyValueObservingOptionNew context:nil];
music = [[AVPlayer playerWithPlayerItem:playerItem] retain];
[music play];
My question:
How can I create a button, that fast-forwards / fast-backwards the stream 5 seconds, when it´s pressed?
Thank you for your answers... :)
EDIT: How can I add to my current time...
CMTime currentTime = music.currentTime;
...the 5 seconds?
In Swift,
fileprivate let seekDuration: Float64 = 5
#IBAction func doForwardJump(_ sender: Any) {
guard let duration = player.currentItem?.duration else{
return
}
let playerCurrentTime = CMTimeGetSeconds(player.currentTime())
let newTime = playerCurrentTime + seekDuration
if newTime < CMTimeGetSeconds(duration) {
let time2: CMTime = CMTimeMake(Int64(newTime * 1000 as Float64), 1000)
player.seek(to: time2)
}
}
#IBAction func doBackwardJump(_ sender: Any) {
let playerCurrentTime = CMTimeGetSeconds(player.currentTime())
var newTime = playerCurrentTime - seekDuration
if newTime < 0 {
newTime = 0
}
let time2: CMTime = CMTimeMake(Int64(newTime * 1000 as Float64), 1000)
player.seek(to: time2)
}
In Objective-C,
#define seekDuration (float)5
- (IBAction)backwardButtonAction:(UIButton *)sender {
float playerCurrentTime = [self getCurrentTime];
float newTime = playerCurrentTime - seekDuration;
if (newTime < 0) {
newTime = 0;
}
CMTime time = CMTimeMake(newTime*1000, 1000);
[self.player seekToTime:time completionHandler:^(BOOL finished) {
dispatch_async(dispatch_get_main_queue(), ^{
playerSliderisScrubbing = NO;
});
}];
}
- (IBAction)forwardButtonAction:(UIButton *)sender {
float duration = [self getPlayerDuration];
float playerCurrentTime = [self getCurrentTime];
float newTime = playerCurrentTime + seekDuration;
if (newTime < duration) {
CMTime time = CMTimeMake(newTime*1000, 1000);
[self.player seekToTime:time completionHandler:^(BOOL finished) {
dispatch_async(dispatch_get_main_queue(), ^{
playerSliderisScrubbing = NO;
});
}];
}
}
- (float)getCurrentTime {
float seconds = 0;
if (_player) {
seconds = CMTimeGetSeconds([_player currentTime]);
}
return seconds;
}
- (float)getPlayerDuration {
float seconds = 0;
if (_player) {
seconds = CMTimeGetSeconds([[_player currentItem] duration]);
}
return seconds;
}
Swift 4, 4.2 & 5
var player : AVPlayer!
#IBAction func fastForwardBtn(_ sender: UIButton) {
let moveForword : Float64 = 5
if player == nil { return }
if let duration = player!.currentItem?.duration {
let playerCurrentTime = CMTimeGetSeconds(player!.currentTime())
let newTime = playerCurrentTime + moveForword
if newTime < CMTimeGetSeconds(duration)
{
let selectedTime: CMTime = CMTimeMake(value: Int64(newTime * 1000 as Float64), timescale: 1000)
player!.seek(to: selectedTime)
}
player?.pause()
player?.play()
}
}
#IBAction func rewindBtn(_ sender: UIButton) {
let moveBackword: Float64 = 5
if player == nil
{
return
}
let playerCurrenTime = CMTimeGetSeconds(player!.currentTime())
var newTime = playerCurrenTime - moveBackword
if newTime < 0
{
newTime = 0
}
player?.pause()
let selectedTime: CMTime = CMTimeMake(value: Int64(newTime * 1000 as Float64), timescale: 1000)
player?.seek(to: selectedTime)
player?.play()
}
Use AVPlayer method seekToTime
AVPlayer *player=..;
[player seekToTime:time toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
here is a reference
Hope it helps
You can use avPlayer.rate to fast backward/forward
Rates: 1.0 normal
0.0 pause
But first, you should check if avPlayerItem can do one of your action
For more info: https://developer.apple.com/LIBRARY/ios/documentation/AVFoundation/Reference/AVPlayer_Class/index.html#//apple_ref/occ/instp/AVPlayer/rate
Swift 5 function to seek forwards or backwards
private let seekDuration: Float64 = 15
private func seekPlayback(isForward: Bool) {
guard
let player = player,
let duration = player.currentItem?.duration
else {
return
}
let currentElapsedTime = player.currentTime().seconds
var destinationTime = isForward ? (currentElapsedTime + seekDuration) : (currentElapsedTime - seekDuration)
if destinationTime < 0 {
destinationTime = 0
}
if destinationTime < duration.seconds {
let newTime = CMTime(value: Int64(destinationTime * 1000 as Float64), timescale: 1000)
player.seek(to: newTime)
}
}

Play audio if headset is plugged-in [duplicate]

Is it possible to detect that the user has an external headset plugged into the iPhone's 3.5mm connector or the 30-pin connector? I want to output audio only to an external audio device, and keep silent if nothing is connected.
The answer is very similar to the answer to this question, but you'll want to get the kAudioSessionProperty_AudioRoute property instead.
Call this method to find out the bluetooth headset is connected or not.
First import this framework #import <AVFoundation/AVFoundation.h>
- (BOOL) isBluetoothHeadsetConnected
{
AVAudioSession *session = [AVAudioSession sharedInstance];
AVAudioSessionRouteDescription *routeDescription = [session currentRoute];
NSLog(#"Current Routes : %#", routeDescription);
if (routeDescription)
{
NSArray *outputs = [routeDescription outputs];
if (outputs && [outputs count] > 0)
{
AVAudioSessionPortDescription *portDescription = [outputs objectAtIndex:0];
NSString *portType = [portDescription portType];
NSLog(#"dataSourceName : %#", portType);
if (portType && [portType isEqualToString:#"BluetoothA2DPOutput"])
{
return YES;
}
}
}
return NO;
}
There is nice article about this in Apple documentation:
https://developer.apple.com/documentation/avfoundation/avaudiosession/responding_to_audio_session_route_changes
Only you have to verify if portType == AVAudioSessionPortBluetoothA2DP
func setupNotifications() {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self,
selector: #selector(handleRouteChange),
name: .AVAudioSessionRouteChange,
object: nil)
}
#objc func handleRouteChange(notification: Notification) {
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSessionRouteChangeReason(rawValue:reasonValue) else {
return
}
switch reason {
case .newDeviceAvailable:
let session = AVAudioSession.sharedInstance()
for output in session.currentRoute.outputs where output.portType == AVAudioSessionPortBluetoothA2DP {
headsetConnected = true
break
}
case .oldDeviceUnavailable:
if let previousRoute =
userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
for output in previousRoute.outputs where output.portType == AVAudioSessionPortBluetoothA2DP {
headsetConnected = false
break
}
}
default: ()
}
}
func isBluetoothHeadsetConnected() -> Bool {
var result = false
let session = AVAudioSession.sharedInstance()
for output in session.currentRoute.outputs where output.portType == AVAudioSessionPortBluetoothA2DP {
result = true
}
return result
}

AVPlayer streaming progress

I'm successfully using AVPlayer to stream audio from a server and what I want to do now is to show a custom UISlider who shows the progress of the buffering.
Something like this:
With AVPlayer there doesn't seem to be a way to get the total download size or the current downloaded amount for the audio file, only the current playing time and total play time.
There's any workarounds for this?
I am just working on this, and so far have the following:
- (NSTimeInterval) availableDuration;
{
NSArray *loadedTimeRanges = [[self.player currentItem] loadedTimeRanges];
CMTimeRange timeRange = [[loadedTimeRanges objectAtIndex:0] CMTimeRangeValue];
Float64 startSeconds = CMTimeGetSeconds(timeRange.start);
Float64 durationSeconds = CMTimeGetSeconds(timeRange.duration);
NSTimeInterval result = startSeconds + durationSeconds;
return result;
}
It should work well:
Objective-C:
- (CMTime)availableDuration
{
NSValue *range = self.player.currentItem.loadedTimeRanges.firstObject;
if (range != nil){
return CMTimeRangeGetEnd(range.CMTimeRangeValue);
}
return kCMTimeZero;
}
Swift version:
func availableDuration() -> CMTime
{
if let range = self.player?.currentItem?.loadedTimeRanges.first {
return CMTimeRangeGetEnd(range.timeRangeValue)
}
return .zero
}
To watch current time value you can use:
CMTimeShow([self availableDuration]);
or CMTimeShow(availableDuration()) (for swift)
Personally I do not agree that the timeRanges value will always have a count of 1.
According to the documentation
The array contains NSValue objects containing a CMTimeRange value indicating the times ranges for which the player item has media data readily available. The time ranges returned may be discontinuous.
So this may have values similar to:
[(start1, end1), (start2, end2)]
From my experience with the hls.js framework within the desktop web world, the holes between these time ranges could be very small or large depending on a multitude of factors, ex: seeking, discontinuities, etc.
So to correctly get the total buffer length you would need to loop through the array and get the duration of each item and concat.
If you are looking for a buffer value from current play head you would need to filter the time ranges for a start time that's greater than the current time and an end time that's less than current time.
public extension AVPlayerItem {
public func totalBuffer() -> Double {
return self.loadedTimeRanges
.map({ $0.timeRangeValue })
.reduce(0, { acc, cur in
return acc + CMTimeGetSeconds(cur.start) + CMTimeGetSeconds(cur.duration)
})
}
public func currentBuffer() -> Double {
let currentTime = self.currentTime()
guard let timeRange = self.loadedTimeRanges.map({ $0.timeRangeValue })
.first(where: { $0.containsTime(currentTime) }) else { return -1 }
return CMTimeGetSeconds(timeRange.end) - currentTime.seconds
}
}
This method will return buffer time interval for your UISlider
public var bufferAvail: NSTimeInterval {
// Check if there is a player instance
if ((player.currentItem) != nil) {
// Get current AVPlayerItem
var item: AVPlayerItem = player.currentItem
if (item.status == AVPlayerItemStatus.ReadyToPlay) {
var timeRangeArray: NSArray = item.loadedTimeRanges
var aTimeRange: CMTimeRange = timeRangeArray.objectAtIndex(0).CMTimeRangeValue
var startTime = CMTimeGetSeconds(aTimeRange.start)
var loadedDuration = CMTimeGetSeconds(aTimeRange.duration)
return (NSTimeInterval)(startTime + loadedDuration);
}
else {
return(CMTimeGetSeconds(kCMTimeInvalid))
}
}
else {
return(CMTimeGetSeconds(kCMTimeInvalid))
}
}
Selected answer may cause you problems if returned array is empty. Here's a fixed function:
- (NSTimeInterval) availableDuration
{
NSArray *loadedTimeRanges = [[_player currentItem] loadedTimeRanges];
if ([loadedTimeRanges count])
{
CMTimeRange timeRange = [[loadedTimeRanges objectAtIndex:0] CMTimeRangeValue];
Float64 startSeconds = CMTimeGetSeconds(timeRange.start);
Float64 durationSeconds = CMTimeGetSeconds(timeRange.duration);
NSTimeInterval result = startSeconds + durationSeconds;
return result;
}
return 0;
}
The code from Suresh Kansujiya in Objective C
NSTimeInterval bufferAvail;
if (player.currentItem != nil) {
AVPlayerItem *item = player.currentItem;
if (item.status == AVPlayerStatusReadyToPlay) {
NSArray *timeRangeArray = item.loadedTimeRanges;
CMTimeRange aTimeRange = [[timeRangeArray objectAtIndex:0] CMTimeRangeValue];
Float64 startTime = CMTimeGetSeconds(aTimeRange.start);
Float64 loadedDuration = CMTimeGetSeconds(aTimeRange.duration);
bufferAvail = startTime + loadedDuration;
NSLog(#"%# - %f", [self class], bufferAvail);
} else {
NSLog(#"%# - %f", [self class], CMTimeGetSeconds(kCMTimeInvalid)); }
}
else {
NSLog(#"%# - %f", [self class], CMTimeGetSeconds(kCMTimeInvalid));
}

UISlider to control AVAudioPlayer

I'm trying to implement a little function in my app. I am currently playing sounds as AVAudioPlayers and that works fine. What I would like to add is to control the sound's position (currentTime) with an UISlider: is there a simple way to do it ?
I looked at an Apple project but it was quite messy....have you got samples or suggestions ?
Thanks to everyone in advance
Shouldn't be a problem - just set the slider to continuous and set the max value to your player's duration after loading your sound file.
Edit
I just did this and it works for me...
- (IBAction)slide {
player.currentTime = slider.value;
}
- (void)updateTime:(NSTimer *)timer {
slider.value = player.currentTime;
}
- (IBAction)play:(id)sender {
NSURL *url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:#"sound.caf" ofType:nil]];
NSError *error;
player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
if (!player) NSLog(#"Error: %#", error);
[player prepareToPlay];
slider.maximumValue = [player duration];
slider.value = 0.0;
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(updateTime:) userInfo:nil repeats:YES];
[player play];
}
The slider is configured in IB, as is a button to start playing.
Swift 3.0 Update:
var player: AVAudioPlayer!
var sliderr: UISlider!
#IBAction func play(_ sender: Any) {
var url = URL(fileURLWithPath: Bundle.main.path(forResource: "sound.caf", ofType: nil)!)
var error: Error?
do {
player = try AVAudioPlayer(contentsOf: url)
}
catch let error {
}
if player == nil {
print("Error: \(error)")
}
player.prepareToPlay()
sliderr.maximumValue = Float(player.duration)
sliderr.value = 0.0
Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.updateTime), userInfo: nil, repeats: true)
player.play()
}
func updateTime(_ timer: Timer) {
sliderr.value = Float(player.currentTime)
}
#IBAction func slide(_ slider: UISlider) {
player.currentTime = TimeInterval(slider.value)
}
To extend on paull's answer, you'd set the slider to be continuous with a maximum value of your audio player's duration, then add some object of yours (probably the view controller) as a target for the slider's UIControlEventValueChanged event; when you receive the action message, you'd then set the AVAudioPlayer's currentTime property to the slider's value.
You might also want to use an NSTimer to update the slider's value as the audio player plays; +scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: is the easiest way to do that.
I needed to adapt the above answer a bit to get it to work. The issue is that using
slider.maximumValue = [player duration];
slider.value = player.currentTime;
player.currentTime = slider.value;
Do not work because the slider expects a float and the player currentTime and dration return CMTime. To make these work, I adapted them to read:
slider.maximumValue = CMTimeGetSeconds([player duration]);
slider.value = CMTimeGetSeconds(player.currentTime);
player.currentTime = CMTimeMakeWithSeconds((int)slider.value,1);
If you don't need any data in between drag, then you should simply set:
mySlider.isContinuous = false
Otherwise, try below code to controller each phase of touch.
// audio slider bar
private lazy var slider: UISlider = {
let slider = UISlider()
slider.translatesAutoresizingMaskIntoConstraints = false
slider.minimumTrackTintColor = .red
slider.maximumTrackTintColor = .white
slider.setThumbImage(UIImage(named: "sliderThumb"), for: .normal)
slider.addTarget(self, action: #selector(onSliderValChanged(slider:event:)), for: .valueChanged)
// slider.isContinuous = false
return slider
}()
#objc func onSliderValChanged(slider: UISlider, event: UIEvent) {
guard let player = AudioPlayer.shared.player else { return }
if let touchEvent = event.allTouches?.first {
switch touchEvent.phase {
case .began:
// handle drag began
// I would stop the timer when drag begin
timer.invalidate()
case .moved:
// handle drag moved
// Update label's text for current playing time
case .ended:
// update the player's currTime and re-create the timer when drag is done.
player.currentTime = TimeInterval(slider.value)
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTime(_:)), userInfo: nil, repeats: true)
default:
break
}
}
}
Problems that I've faced during playing an audio file and show start/end time and controlling the song with the UISlider.
Not playing audio directly without downloading it in temp folder.
UISlider got crashed on main thread in lower iOS version i.e 12.4/13.1
Smooth Scrolling of UISlider.
Calculating and updating the start/end time of the song.
This answer needs some editing, but it will work without any doubt.
//UISlider init
lazy var slider: UISlider = {
let progress = UISlider()
progress.minimumValue = 0.0
progress.maximumValue = 100.0
progress.tintColor = UIColor.init(named: "ApplicationColor")
return progress
}()
var audioPlayer : AVAudioPlayer?
//First I've downloaded the audio and then playing it.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(trackAudio), userInfo: nil, repeats: true)
if let audioURLString = audioURL{
let urlstring = URL(string: audioURLString)!
downloadFromURL(url: urlstring) { (localURL, response, error) in
if let localURL = localURL{
self.playAudioFile(url: localURL)
}
}
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
stopTimer()
}
// Stop TimeInterval After View disappear
func stopTimer() {
if timer != nil {
timer?.invalidate()
audioPlayer?.stop()
audioPlayer = nil
timer = nil
}
}
#objc func sliderSelected(_ sender : UISlider){
if audioPlayer != nil{
if !isPlaying{
self.audioPlayer?.play()
playButton.setImage(UIImage.init(named: "AudioPause"), for: .normal)
isPlaying = true
}else{
self.audioPlayer?.currentTime = TimeInterval(Float(sender.value) * Float(self.audioPlayer!.duration) / 100.0)
if (sender.value / 100.0 == 1.0){
//Do something if audio ends while dragging the UISlider.
}
}
}
}
func downloadFromURL(url:URL,completion: #escaping((_ downladedURL: URL?,_ response :URLResponse?,_ error: Error?) -> Void)){
var downloadTask:URLSessionDownloadTask
downloadTask = URLSession.shared.downloadTask(with: url) {(URL, response, error) in
if let url = URL{
completion(url,nil,nil)
}else if let response = response{
completion(nil,response,nil)
}
if let error = error{
completion(nil,nil,error)
}
}
downloadTask.resume()
}
func playAudioFile(url:URL){
do{
self.audioPlayer = try AVAudioPlayer(contentsOf: url)
self.audioPlayer?.prepareToPlay()
self.audioPlayer?.delegate = self
self.audioPlayer?.play()
let audioDuration = audioPlayer?.duration
let audioDurationSeconds = audioDuration
minutes = Int(audioDurationSeconds!/60);
seconds = Int(audioDurationSeconds!.truncatingRemainder(dividingBy: 60))
} catch{
print("AVAudioPlayer init failed")
}
}
#objc func trackAudio() {
if audioPlayer != nil{
DispatchQueue.main.async {
print("HI")
let normalizedTime = Float(self.audioPlayer!.currentTime * 100.0 / self.audioPlayer!.duration)
self.slider.setValue(normalizedTime, animated: true)
let currentTime = self.audioPlayer?.currentTime
self.currentMinutes = Int(currentTime!/60);
self.currentSeconds = Int(currentTime!.truncatingRemainder(dividingBy: 60))
self.startTimeLabel.text = String(format: "%02i:%02i", self.currentMinutes, self.currentSeconds)
self.endTimeLabel.text = String(format: "%02i:%02i", self.minutes, self.seconds)
}
}
}
If anyone was looking for a simple TouchDown and TouchUp on UI slider then this turns out to be as simple as :
slider.addTarget(self, action: #selector(changeVlaue(_:)), for: .valueChanged)
slider.addTarget(self, action: #selector(sliderTapped), for: .touchDown)
slider.addTarget(self, action: #selector(sliderUntouched), for: .touchUpInside)
Here's the entire setup for an AVAudioPlayer. Some of the code in handleScrubbing() and fingerLiftedFromSlider() is duplicated but whatever...
This will let you show what's on the currentTimeLabel (usually on the left) and the totalDurationLabel (usually on the right) with the scrubber/slider in the the middle of them. When you slide the slider the currentTime will update to show wherever the slider is.
There is something to be aware about. If the the player was playing before you touch the slider, while you slide the slider, the player is still playing. In .began you need to check if the player was playing and if so pause it and set a variable like wasAudioPlayerPlayingBeforeSliderWasTouched to true so that when your finger is lifted it will continue playing from wherever you lift your finger. If you don't pause the player then the slider isn't going to slide smoothly.
When you lift your finger there is a check in onFingerLiftedStartAudioPlayerIfItWasPlayingBeforeTouchesBegan() to see if the slider is at its endTime. If it is instead of playing it'll run the code in audioEndTimeStopEverything().
In the startAudioPlayer method, I used an AVURLAsset get a set the actual url's duration. I got it from this answer which has a great explanation.
I used this code with a local url, not sure how this will work with a remote url.
import UIKit
import AVFoundation
class MyAudioController: UIViewController {
lazy var currentTimeLabel ... { ... }()
lazy var totalDurationLabel ... { ... }()
lazy vay pausePlayButton ... { ... }()
lazy var fastForwardButton ... { ... }()
lazy var rewindButton ... { ... }()
lazy var slider: UISlider = {
let s = UISlider()
s.translatesAutoresizingMaskIntoConstraints = false
s.isContinuous = true
s.minimumTrackTintColor = UIColor.red
s.maximumTrackTintColor = UIColor.blue
s.setThumbImage(UIImage(named: "circleIcon"), for: .normal)
s.addTarget(self, action: #selector(sliderValChanged(slider:event:)), for: .valueChanged)
return s
}()
weak var timer: Timer? // *** MAKE SURE THIS IS WEAK ***
var audioPlayer: AVAudioPlayer?
var wasAudioPlayerPlayingBeforeSliderWasTouched = false
override func viewDidLoad() {
super.viewDidLoad()
guard let myAudioUrl = URL(string: "...") else { return }
setAudio(with: myAudioUrl)
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
} catch {
print(error)
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
stopAudioPlayer()
}
// 1. init your AVAudioPlayer here
func setAudioPlayer(with audioTrack: URL) {
do {
stopAudioPlayer() // if something was previously playing
audioPlayer = try AVAudioPlayer(contentsOf: audioTrack)
audioPlayer?.delegate = self
audioPlayer?.prepareToPlay()
audioPlayer?.volume = audioVolume
startAudioPlayer()
} catch let err as NSError {
print(err.localizedDescription)
}
}
// 2. Audio PLAYER - start / stop funcs
stopAudioPlayer() {
stopAudioTimer()
audioPlayer?.pause()
audioPlayer?.stop()
}
func startAudioPlayer() {
if let audioPlayer = audioPlayer, audioPlayer.isPlaying {
audioPlayer.pause()
}
audioPlayer?.currentTime = 0
audioPlayer?.play()
pausePlayButton.setImage(UIImage(named: "pauseIcon"), for: .normal)
startAudioTimer()
}
func startAudioTimer() {
stopAudioTimer()
slider.value = 0
currentTimeLabel.text = "00:00"
totalDurationLabel.text = "00:00"
guard let url = audioPlayer?.url else { return }
let assetOpts = [AVURLAssetPreferPreciseDurationAndTimingKey: true]
let asset = AVURLAsset(url: url, options: assetOpts)
let assetDuration: CMTime = asset.duration
let assetDurationInSecs: Float64 = CMTimeGetSeconds(assetDuration)
slider.maximumValue = Float(assetDurationInSecs)
totalDurationLabel.text = strFromTimeInterval(interval: TimeInterval(assetDurationInSecs))
runAudioTimer()
}
// 3. TIMER funcs
func runAudioTimer() {
if timer == nil {
timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true, block: { [weak self](_) in
self?.audioTimerIsRunning()
})
}
}
func audioTimerIsRunning() {
guard let audioPlayer = audioPlayer else { return }
let currentTime = audioPlayer.currentTime
if Float(currentTime) >= Float(slider.maximumValue) {
stopAudioTimer()
}
currentTimeLabel.text = strFromTimeInterval(interval: currentTime)
slider.value = Float(currentTime)
}
func stopAudioTimer() {
if timer != nil {
timer?.invalidate()
timer = nil
}
}
// slider funcs
#objc func sliderValChanged(slider: UISlider, event: UIEvent) {
if let touchEvent = event.allTouches?.first {
switch touchEvent.phase {
case .began:
checkIfAudioPlayerWasPlayingWhenSliderIsFirstTouched()
stopAudioTimer()
print("Finger Touched")
case .moved:
handleScrubbing()
print("Finger is Moving Scrubber")
case .ended:
print("Finger Lifted")
onFingerLiftedStartAudioPlayerIfItWasPlayingBeforeTouchesBegan()
fingerLiftedFromSlider()
default:
print("Something Else Happened In Slider")
}
}
}
func checkIfAudioPlayerWasPlayingWhenSliderIsFirstTouched() {
guard let audioPlayer = audioPlayer else { return }
if audioPlayer.isPlaying {
audioPlayer.pause()
wasAudioPlayerPlayingBeforeSliderWasTouched = true
}
}
func handleScrubbing() {
guard let audioPlayer = audioPlayer else { return }
let sliderValue = TimeInterval(slider.value)
currentTimeLabel.text = strFromTimeInterval(interval: sliderValue)
audioPlayer.currentTime = sliderValue
if audioPlayer.currentTime >= audioPlayer.duration {
audioEndTimeStopEverything()
}
}
func onFingerLiftedStartAudioPlayerIfItWasPlayingBeforeTouchesBegan() {
if wasAudioPlayerPlayingBeforeSliderWasTouched {
wasAudioPlayerPlayingBeforeSliderWasTouched = false
guard let audioPlayer = audioPlayer else { return }
if slider.value >= slider.maximumValue {
audioEndTimeStopEverything()
} else {
audioPlayer.play()
}
}
}
func fingerLiftedFromSlider() {
guard let audioPlayer = audioPlayer else { return }
if !audioPlayer.isPlaying { // this check is necessary because if you paused the audioPlayer, then started sliding, it should still be paused when you lift you finger up. It it's paused there is no need for the timer function to run.
let sliderValue = TimeInterval(slider.value)
currentTimeLabel.text = strFromTimeInterval(interval: sliderValue)
audioPlayer.currentTime = sliderValue
return
}
runAudioTimer()
}
func audioEndTimeStopEverything() {
stopAudioPlayer()
pausePlayButton.setImage(UIImage("named: playIcon"), for: .normal)
guard let audioPlayer = audioPlayer else { return }
// for some reason when the audioPlayer would reach its end time it kept resetting its currentTime property to zero. I don't know if that's meant to happen or a bug but the currentTime would be zero and the slider would be at the end. To rectify the issue I set them both to their end times
audioPlayer.currentTime = audioPlayer.duration
slider.value = slider.maximumValue
}
}
extension MyAudioController: AVAudioPlayerDelegate {
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
audioEndTimeStopEverything()
}
func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {
if let error = error {
print(error.localizedDescription)
}
}
}
Here is the strFromTimeInterval(interval: ) function that I got from here. I only used it because I didn't want to bother with milliseconds. The code above was ran using audio files with minutes and seconds, not hours. If you have any problems with hours you can also swap this function out for this answer
extension MyAudioController {
func strFromTimeInterval(interval: TimeInterval) -> String {
let time = NSInteger(interval)
let seconds = time % 60
let minutes = (time / 60) % 60
let hours = (time / 3600)
var formatString = ""
if hours == 0 {
if (minutes < 10) {
formatString = "%2d:%0.2d"
} else {
formatString = "%0.2d:%0.2d"
}
return String(format: formatString,minutes,seconds)
} else {
formatString = "%2d:%0.2d:%0.2d"
return String(format: formatString,hours,minutes,seconds)
}
}
}

How to find out if an external headset is connected to an iPhone?

Is it possible to detect that the user has an external headset plugged into the iPhone's 3.5mm connector or the 30-pin connector? I want to output audio only to an external audio device, and keep silent if nothing is connected.
The answer is very similar to the answer to this question, but you'll want to get the kAudioSessionProperty_AudioRoute property instead.
Call this method to find out the bluetooth headset is connected or not.
First import this framework #import <AVFoundation/AVFoundation.h>
- (BOOL) isBluetoothHeadsetConnected
{
AVAudioSession *session = [AVAudioSession sharedInstance];
AVAudioSessionRouteDescription *routeDescription = [session currentRoute];
NSLog(#"Current Routes : %#", routeDescription);
if (routeDescription)
{
NSArray *outputs = [routeDescription outputs];
if (outputs && [outputs count] > 0)
{
AVAudioSessionPortDescription *portDescription = [outputs objectAtIndex:0];
NSString *portType = [portDescription portType];
NSLog(#"dataSourceName : %#", portType);
if (portType && [portType isEqualToString:#"BluetoothA2DPOutput"])
{
return YES;
}
}
}
return NO;
}
There is nice article about this in Apple documentation:
https://developer.apple.com/documentation/avfoundation/avaudiosession/responding_to_audio_session_route_changes
Only you have to verify if portType == AVAudioSessionPortBluetoothA2DP
func setupNotifications() {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self,
selector: #selector(handleRouteChange),
name: .AVAudioSessionRouteChange,
object: nil)
}
#objc func handleRouteChange(notification: Notification) {
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSessionRouteChangeReason(rawValue:reasonValue) else {
return
}
switch reason {
case .newDeviceAvailable:
let session = AVAudioSession.sharedInstance()
for output in session.currentRoute.outputs where output.portType == AVAudioSessionPortBluetoothA2DP {
headsetConnected = true
break
}
case .oldDeviceUnavailable:
if let previousRoute =
userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
for output in previousRoute.outputs where output.portType == AVAudioSessionPortBluetoothA2DP {
headsetConnected = false
break
}
}
default: ()
}
}
func isBluetoothHeadsetConnected() -> Bool {
var result = false
let session = AVAudioSession.sharedInstance()
for output in session.currentRoute.outputs where output.portType == AVAudioSessionPortBluetoothA2DP {
result = true
}
return result
}