Search code examples
iphoneobjective-ciosaudiostreamer

Matt Gallagher's AudioStream: how to handle view controller pop


Well, this is my very first post ever -- between this website, personal blogs, and iPhoneDevSDK forums, I have always been able to figure things out on my own. I've been developing for iOS for about five months and have never asked for help outside of Google. But this time, I'm stuck.

I have successfully implemented Matt Galagher's AudioStream class (had to remove link because SO assumes my post is spam) into my app, and I'm happy to report that it works beautifully. I'm using it in one view controller, which is actually a child to a parent view in a TabBar app. In fact, my implementation isn't much different than that in Matt's example -- I only changed some UI elements.

Up to this point, I'd been using viewDidDisappear to stop the streamer when the user switches to another tab. But recently I decided it would be slick to let the audio stream play as long as the app is running, no matter what view is on top. That was a very simple change, and now my audio stream continues to play no matter where I am in the app.

Here's how the view controller works:

  1. I have a Play button and a Stop button laying in the same spot, with the Stop button hidden.
  2. When the user presses the Play button, the Play button hides, revealing a UIActivityIndicatorView (`[streamer isWaiting]`)
  3. When the stream begins playing (`[streamer isPlaying]`), the `UIActivityIndicatorView` is hidden, and a Stop button appears
  4. Also, while `[streamer isPlaying]`, I'm displaying an elapsed time (mm:ss) inside the Navigation Prompt, which updates each second

All of the above work perfectly every time. If I leave the view (either by popping to the parent view, or by navigating to another tab), the audio will stay playing, which is exactly what I want. But when I return to the view, the UI appears as though I haven't started it yet (Play button visible, Navigation Prompt is hidden). If I press the play button, then I hear a second stream playing simultaneously with the first.

The updateProgress seems to have stopped (as self.navigationItem.prompt is no longer visible), and I'm guessing the playbackStateChanged is "dead" (not sure how to describe it).

I've spent hours sifting through the AudioStreamer class trying to figure out how to maintain control of the stream, but I'm exhausted. I'm hoping someone might be able to tell me what I'm missing.

As I said earlier, my view controller is nearly identical to the example (see hyperlink above, as SO still assumes I'm a spammer), with only a few UI-related changes.

I guess the short question would be this: has anyone been able to implement the AudioSTreamer class, pop its view, then come back and be able to view elapsed time or stop the stream?

EDIT: below is my view controller implementing AudioStreamer

.h

#import <UIKit/UIKit.h>
#import <QuartzCore/CoreAnimation.h>
#import <MediaPlayer/MediaPlayer.h>
#import <CFNetwork/CFNetwork.h>

@class AudioStreamer;


@interface RadioViewController : UIViewController
{
    IBOutlet UIButton *playButton;
    IBOutlet UIButton *stopButton;
    IBOutlet UIActivityIndicatorView *waitIndicator;

    AudioStreamer *streamer;
    NSTimer *progressUpdateTimer;

    BOOL shouldAutoStop;
}

- (IBAction)play;
- (IBAction)stop;

- (void)createStreamer;
- (void)destroyStreamer;
- (void)updateProgress:(NSTimer *)aNotification;
- (void)checkWiFi;

@end

.m

#import "AudioStreamer.h"
#import "ServerCheck.h"
#import "RadioViewController.h"

@implementation RadioViewController


- (void)viewDidLoad
{
    [[self navigationItem] setTitle:@"WXK33 162.550"];

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(checkWiFi) name:UIApplicationDidBecomeActiveNotification object:nil];

    [super viewDidLoad];
}

- (void)viewDidAppear:(BOOL)animated
{   
    [self performSelector:@selector(checkWiFi)];

    [super viewDidAppear:animated];
}

- (void)viewDidDisappear:(BOOL)animated
{
    //[self performSelector:@selector(stop)];

    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];

    [super viewDidDisappear:animated];
}


- (void)createStreamer
{
    if ([ServerCheck serverReachable:@"audiostream.wunderground.com"])
    {
        if (streamer)
        {
            return;
        }

        [self destroyStreamer];

        NSURL *url = [NSURL URLWithString:@"http://audiostream.wunderground.com/MaffooClock/San_Angelo.mp3"];
        streamer = [[AudioStreamer alloc] initWithURL:url];

        progressUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(updateProgress:) userInfo:nil repeats:YES];

        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackStateChanged:) name:ASStatusChangedNotification object:streamer];

    }
    else
    {
        [[self navigationController] popViewControllerAnimated:YES];
    }
}

- (IBAction)play
{
    [self createStreamer];
    [waitIndicator startAnimating];
    [streamer start];

    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
}

- (IBAction)stop
{
    self.navigationItem.prompt = nil;
    [streamer stop];

    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
}


- (void)playbackStateChanged:(NSNotification *)aNotification
{
    if ([streamer isWaiting])
    {
        [playButton setHidden:YES];
        [stopButton setHidden:YES];
        [waitIndicator startAnimating];
    }
    else if ([streamer isPlaying])
    {   
        [playButton setHidden:YES];
        [stopButton setHidden:NO];
        [waitIndicator stopAnimating];

        [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
    }
    else if ([streamer isIdle])
    {
        [self destroyStreamer];

        [playButton setHidden:NO];
        [stopButton setHidden:YES];
        [waitIndicator stopAnimating];
    }
}


- (void)updateProgress:(NSTimer *)updatedTimer
{
    if (streamer.bitRate > 0.0)
    {
        double progress = streamer.progress;

        double minutes = floor(progress/60);
        double seconds = trunc(progress - (minutes * 60));

        self.navigationItem.prompt = [NSString stringWithFormat:@"Elapsed: %02.0f:%02.0f",minutes,seconds];;

        if (shouldAutoStop && progress > 600)
            [self performSelector:@selector(stop)];
    }
    else
    {
        self.navigationItem.prompt = nil;
    }
}

- (void) checkWiFi
{
    if (![ServerCheck wifiAvailable])
    {
        LogInfo(@"No Wi-Fi");

        NSString * messageTitle = @"Notice";
        NSString * messageText = @"It appears that you do not have an active Wi-Fi connection.  Listening to streaming audio via cellular data may incurr additional data charges.  Streaming will automatically stop after 10 minutes.";

        UIAlertView *errorAlert = [[UIAlertView alloc] initWithTitle:messageTitle message:messageText delegate:self cancelButtonTitle:@"Dismiss" otherButtonTitles:nil];

        [errorAlert show];
        [errorAlert release];

        shouldAutoStop = YES;
    }
    else
        shouldAutoStop = NO;    
}


- (void)destroyStreamer
{
    if (streamer)
    {
        [[NSNotificationCenter defaultCenter] removeObserver:self name:ASStatusChangedNotification object:streamer];
        [progressUpdateTimer invalidate];
        progressUpdateTimer = nil;

        [streamer stop];
        [streamer release], streamer = nil;
    }
}


- (void)dealloc
{
    [self destroyStreamer];
    if (progressUpdateTimer)
    {
        [progressUpdateTimer invalidate], progressUpdateTimer = nil;
    }
    [super dealloc];
}

@end

Solution

  • Turns out that I just needed to re-think how I'm allocating and pushing views. The "More" tab displays a table view, and each cell represents a child view that will be pushed when that cell is selected. I changed this view to alloc all child views inside viewDidLoad, then simply perform the push inside didSelectRowAtIndexPath, and release the child views in dealloc.

    This solves my problem perfectly. Thanks to Jason Coco for getting the ball rolling.