Creating a Media Player - iOS App

Goal: Create a native iOS app using the AVPlayer class and then add simple controls, closed captioning support, and timed metadata observation.

Software Prerequisites:

  • Xcode 7 or higher
  • iOS developer environment
  • Apple developer account

Knowledge Prerequisites:

  • Objective-C
  • iOS SDK

Key Steps:

  1. Create and set up a project.
  2. Create and set up the player within the project.
  3. Test playback.
  4. Add playback controls and support for rewinding and closed captioning.
  5. Observe timed metadata embedded within a stream.

Step 1 - Create a Project

Create a new single view application project in Xcode.

Open Xcode.

From the Welcome to Xcode screen, click Create a new Xcode project.

Select "Single View Application" and then click Next.

From the Choose options for your new project: dialog box, define a product name and an organization identifier.

Setting Description Example

Product Name

Identifies the name of the application being created.

iStreamWidgets

Organization Identifier

Defines a unique identifier for your organization. This setting is concatenated with your product name to uniquely identify your application.

com.mycompany.ios

Click Next.

Browse to the location where the project will be stored.

Click Create to create the project.

Step 2 - Set up a Basic Project

Define basic project settings, include the AVFoundation framework, and add a view object to the view controller.

Clear the Portrait option. This setting can be found on the Build Settings tab of the project paneThis pane is located in the middle of the window..

From the Identity and Type section of the File Inspector pane, set the Class Prefix option to the desired class prefix (e.g., VLS).

By default, the specified class prefix will be preprended to each new class created in this project. The purpose of this prefix is to avoid name conflicts with classes in external frameworks and libraries.

Add the AVFoundation framework to the project. This framework is required for media playback.

From the Project Navigator paneThis pane is located on the left-hand side of the window., select Main.storyboard.

From the project paneThis pane is located in the middle of the window., expand the View Controller Scene tree and then select View Controller.

From the pane on the right-hand side, view the Attributes pane by clicking .

Under the Simulated Metrics section, set the Size option to "iPhone 5.5-inch."

Under the Simulated Metrics section, set the Orientation option to "Landscape."

Add a view object to the view controller.

Step 3 - Add a Player Class

Add a class through which the AVPlayer implementation will be managed.

Add an Objective-C Class to the project.

From the Project Navigator, select VLSPlayerView.h and then update it to match the following code:

//
//  VLSPlayerView.h
//

#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>

@interface VLSPlayerView : UIView

@property (nonatomic) AVPlayer *player;

@end		

Select VLSPlayerView.m and then update it to match the following code:

//
//  VLSPlayerView.m
//

#import "VLSPlayerView.h"

@implementation VLSPlayerView

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
    }
    return self;
}

+ (Class)layerClass {
    return [AVPlayerLayer class];
}
- (AVPlayer*)player {
    return [(AVPlayerLayer *)[self layer] player];
}
- (void)setPlayer:(AVPlayer *)player {
    [(AVPlayerLayer *)[self layer] setPlayer:player];
}

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
    // Drawing code
}
*/

@end

Step 4 - Connect the PlayerView

A view object was added to the storyboard in step 2. This object needs to be defined as an instance of VLSPlayerView.

From the Project Navigator paneThis pane is located on the left-hand side of the window., select Main.storyboard.

Select the view object nested under the "View" tree item.

View the Identity Inspector pane by clicking from the pane on the right-hand side.

Set the Class option to "VLSPlayerView."

Step 5 - Set up the View Controller

Code the View Controller to play a VLS assetRefers to media (i.e., audio/video content) associated with either your account or a library shared with your account..

From the Project Navigator paneThis pane is located on the left-hand side of the window., select "ViewController.h" and then add the lines indicated in blue font below.

//
//  ViewController.h
//

#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>

@class VLSPlayerView;
@interface ViewController : UIViewController

@property (nonatomic) AVPlayer *player;
@property (nonatomic) AVPlayerItem *playerItem;
@property (nonatomic, weak) IBOutlet VLSPlayerView *playerView;
@end	

From the Project Navigator paneThis pane is located on the left-hand side of the window., select "ViewController.m" and then add the lines indicated in blue font below.

//
//  ViewController.m
//

#import "ViewController.h"
#import "VLSPlayerView.h"

@interface ViewController ()

@end

@implementation ViewController
@synthesize player, playerView, playerItem;

- (void)viewDidLoad
{
	[super viewDidLoad];
	// Do any additional setup after loading the view, typically from a nib.

	NSURL *content_with_captions = [NSURL URLWithString:@"https://content.uplynk.com/209da4fef4b442f6b8a100d71a9f6a9a.m3u8"];
	player = [AVPlayer playerWithURL:content_with_captions];

	[self.playerView setPlayer:player];
	[player play];
}

 - (void)didReceiveMemoryWarning{
	[super didReceiveMemoryWarning];
	// Dispose of any resources that can be recreated.
}

@end	

Replace the playback URL, indicated in orange font below, with one that corresponds to an asset that does not require a signed playback URL.

NSURL *content_with_captions = [NSURL URLWithString:@"https://content.uplynk.com/209da4fef4b442f6b8a100d71a9f6a9a.m3u8"];	

The asset's Require a token for playback option determines whether a playback URL must be signed.

Set the View Controller to the above view object.

Step 6 - Test the App

The app is now ready to be tested. Upon running the app, the app will load and it should immediately start content playback.

Click to build and run the app.

Verify that the app plays an asset from your library.

Step 7 - Playback Controls

AVPlayer provides advanced functionality such as support for closed captions and observing timed metadata. It also lacks a proper set of playback controls, granting you the freedom to create your own playback controls. Let's create a custom play / pause control for use in the app.

Click on MainStoryboard_iPhone.storyboard in the Project Navigator.

Drag a Button object from the Object Library and place it in the bottom middle of your storyboard app. Double click the button text and change it to say Play.

The View Controller, as hub of our app, needs to know 2 things, which button and what happens when it's clicked. Select UTViewController.h and edit it to match the following:

//
//  UTViewController.h
//

#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>

@class UTPlayerView;
@interface UTViewController : UIViewController

@property (nonatomic) AVPlayer *player;
@property (nonatomic) AVPlayerItem *playerItem;
@property (nonatomic, weak) IBOutlet UTPlayerView *playerView;
@property (nonatomic, weak) IBOutlet UIButton *playButton;

-(IBAction)play:sender;

@end

We've added 1 outlet and 1 action to our app. The outlet tells the view controller which button we want to be our play button. The action is how we tell the view controller what effect we want the play button to have on our app. In our case, it will start playback of content.

Now let's implement the .m changes. Click UTViewController.m and modify it as follows:

//
//  UTViewController.m
//

#import <CoreMedia/CoreMedia.h>
#import "UTViewController.h"
#import "UTPlayerView.h"

@interface UTViewController ()

@end

@implementation UTViewController
@synthesize player, playerView, playerItem, playButton;

- (void)viewDidLoad
{
	[super viewDidLoad];
	// Do any additional setup after loading the view, typically from a nib.

	NSURL *content_with_captions = [NSURL URLWithString:@"https://content.uplynk.com/209da4fef4b442f6b8a100d71a9f6a9a.m3u8"];
	player = [AVPlayer playerWithURL:content_with_captions];

	[self.playerView setPlayer:player];
}

 - (void) play:sender {
	NSLog(@"play toggled");
	if (player.rate == 0.0) {
		[playButton setTitle:@"Pause" forState:UIControlStateNormal];
		[player play];

	} else {
		[playButton setTitle:@"Play" forState:UIControlStateNormal];
		[player pause];
	}
}

- (void)didReceiveMemoryWarning
{
	[super didReceiveMemoryWarning];
	// Dispose of any resources that can be recreated.
}

@end

You'll see we've removed the player call to play from our viewDidLoad method. Our video will not automatically start. The play button will be used to start playback. We implemented our play method and added the extra touch of changing its title to Pause during playback, similar to the play button of many multimedia interfaces.

Time to connect the button in Interface Builder. Click on MainStoryboard_iPhone.storyboard. We need to make 2 connections. First control + click and drag from View Controller to the play button and choose playButton from the pop up menu. Next control + click and drag from the play button to the View Controller and choose Play from the pop up menu.

Test the interface and ensure playback starts, pauses and restarts as you click the play button.

Step 8 - Rewind

You have successfully played content. You can watch it through to the end. It's not very user friendly to force a restart of the application to play the video again, though. Our playerItem provides a notification we can observe and use to "rewind" our video.

In order to implement this observer, we'll modify UTViewController.m again as follows:

//
//  UTViewController.m
//

#import <CoreMedia/CoreMedia.h>
#import "UTViewController.h"
#import "UTPlayerView.h"

@interface UTViewController ()

@end

@implementation UTViewController
@synthesize player, playerView, playerItem, playButton;

- (void)viewDidLoad
{
	[super viewDidLoad];
	// Do any additional setup after loading the view, typically from a nib.

	NSURL *content_with_captions = [NSURL URLWithString:@"https://content.uplynk.com/209da4fef4b442f6b8a100d71a9f6a9a.m3u8"];
	player = [AVPlayer playerWithURL:content_with_captions];

	[self.playerView setPlayer:player];
	[[NSNotificationCenter defaultCenter] addObserver:self
			selector:@selector(playerItemDidReachEnd:)
			name:AVPlayerItemDidPlayToEndTimeNotification
			object:[self.player currentItem]];
}


- (void) play:sender {
	NSLog(@"play toggled");
	if(player.rate == 0.0){
		[playButton setTitle:@"Pause" forState:UIControlStateNormal];
		[player play];

	} else {
		[playButton setTitle:@"Play" forState:UIControlStateNormal];
		[player pause];
	}

}

- (void) playerItemDidReachEnd:(NSNotification *)notification {
	CMTime zero = CMTimeMakeWithSeconds(0.0, 1);
	[player seekToTime:zero];
	[playButton setTitle:@"Play" forState:UIControlStateNormal];
}

- (void)didReceiveMemoryWarning
{
	[super didReceiveMemoryWarning];
	// Dispose of any resources that can be recreated.
}



@end

We've made 3 changes.

First, we've imported the CoreMedia framework. You'll also need to add it to our list of libraries to link with. Click your root app in the Project Navigator. Click the Build Phases tab. Expand Link Binary With Libraries. Click the + button. Find CoreMedia.framework in the list. Click the Add button.

Second, we've added the playerItemDidReachEnd method that will be called when we observe the end of our content.

Third, we have added an observer to the default center to receive our notification and call our method when we get it. We've given it the name of the method (selector) we want called, playerItemDidReachEnd. Save these changes and test it out. When the video ends, you should be able to replay it with a press of the play button.

Step 9 - Closed Captions

One of the advanced benefits of using AVPlayer is its support for closed captions. We'll now add a button to our interface to toggle display of closed captions if our content provides them.

It's a similar drill to adding the play button. We'll start with the storyboard. Click MainStoryboard_iPhone.storyboard, and drag another Round Rect Button from the object library to the interface. Double-click the title and change it to say Captions Off. You may wish to reposition the play button to fit your captions button. Save the storyboard file.

Now let' inform the view controller of the new button. Click on UTViewController.h and make the following modifications:

//
//  UTViewController.h
//

#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>

@class UTPlayerView;
@interface UTViewController : UIViewController

@property (nonatomic) AVPlayer *player;
@property (nonatomic) AVPlayerItem *playerItem;
@property (nonatomic, weak) IBOutlet UTPlayerView *playerView;
@property (nonatomic, weak) IBOutlet UIButton *playButton;
@property (nonatomic, weak) IBOutlet UIButton *captionsButton;

-(IBAction)play:sender;
-(IBAction)toggleCaptions:sender;
@end

Next we'll implement our new action, toggleCaptions, in UTViewController.m. Make these modifications:

//
//  UTViewController.m
//

#import <CoreMedia/CoreMedia.h>
#import "UTViewController.h"
#import "UTPlayerView.h"

@interface UTViewController ()

@end

@implementation UTViewController
@synthesize player, playerView, playerItem, playButton, captionsButton;

- (void)viewDidLoad
{
	[super viewDidLoad];
		// Do any additional setup after loading the view, typically from a nib.

	NSURL *content_with_captions = [NSURL URLWithString:@"https://content.uplynk.com/209da4fef4b442f6b8a100d71a9f6a9a.m3u8"];
	player = [AVPlayer playerWithURL:content_with_captions];

	[self.playerView setPlayer:player];
	[self.player addObserver:self
		forKeyPath:kTimedMetadataKey
		options:0
		context:TimedMetadataObserverContext];
	[[NSNotificationCenter defaultCenter] addObserver:self
		selector:@selector(playerItemDidReachEnd:)
		name:AVPlayerItemDidPlayToEndTimeNotification
		object:[self.player currentItem]];
}

- (void) play:sender {
	NSLog(@"play toggled");
	if(player.rate == 0.0){
		[playButton setTitle:@"Pause" forState:UIControlStateNormal];
		[player play];

	} else {
		[playButton setTitle:@"Play" forState:UIControlStateNormal];
		[player pause];
	}

}

- (void) toggleCaptions:sender {
	NSLog(@"Toggle captions");
	player.closedCaptionDisplayEnabled = !player.closedCaptionDisplayEnabled;
	if(player.closedCaptionDisplayEnabled == NO)
		[captionsButton setTitle:@"Captions Off" forState:UIControlStateNormal];
	else{
		[captionsButton setTitle:@"Captions On" forState:UIControlStateNormal];
	}
}

- (void) playerItemDidReachEnd:(NSNotification *)notification {
	CMTime zero = CMTimeMakeWithSeconds(0.0, 1);
	[player seekToTime:zero];
	[playButton setTitle:@"Play" forState:UIControlStateNormal];
}

- (void)didReceiveMemoryWarning
{
	[super didReceiveMemoryWarning];
	// Dispose of any resources that can be recreated.
}
@end

This is a simple method that toggles display of closed captions, and updates the button's title.

The last thing we need to do is make the outlet and action connections in Interface Builder. Click on MainStoryboard_iPhone.storyboard. Control + click and drag from our View Controller to the captions button. Select captionsButton from the pop up menu. Next control + click and drag from the captions button to the View Controller. Select toggleCaptions from the pop up menu. Save the file.

Naturally you'll need content with closed captions to test this feature. The playback URL used in this tutorial has a few rough captions included.

Step 10 - Observing Stream Embedded Timed Metadata

Timed metadata is included in our streams. Its format is assetID_ray_slicenumber. Ray change observations are useful for determining when the player changes bit rates. Changes in asset ID could indicate your content has entered an ad break and should hide its controls, for example.

We will update our View Controller with an observer and a method that prints the observed data to the debug console.

Click UTViewController.m and edit it to match the following:

//
//  UTViewController.m
//  AVTutorialSample
//
//  Created by tbye on 2/20/13.
//  Copyright (c) 2013 upLynk, LLC. All rights reserved.
//

#import <CoreMedia/CoreMedia.h>
#import "UTViewController.h"
#import "UTPlayerView.h"

static void *TimedMetadataObserverContext = &TimedMetadataObserverContext;
NSString *kTimedMetadataKey = @"currentItem.timedMetadata";
NSArray* tmarray;

@interface UTViewController ()

@end

@implementation UTViewController
@synthesize player, playerView, playerItem, playButton, captionsButton;

- (void)viewDidLoad
{
	[super viewDidLoad];
	// Do any additional setup after loading the view, typically from a nib.

	NSURL *content_with_captions = [NSURL URLWithString:@"https://content.uplynk.com/209da4fef4b442f6b8a100d71a9f6a9a.m3u8"];
	player = [AVPlayer playerWithURL:content_with_captions];

	[self.playerView setPlayer:player];
	[self.player addObserver:self
	forKeyPath:kTimedMetadataKey
	options:0
	context:TimedMetadataObserverContext];
	[[NSNotificationCenter defaultCenter] addObserver:self
	selector:@selector(playerItemDidReachEnd:)
	name:AVPlayerItemDidPlayToEndTimeNotification
	object:[self.player currentItem]];
}

- (void) play:sender {
	NSLog(@"play toggled");
	if(player.rate == 0.0){
		[playButton setTitle:@"Pause" forState:UIControlStateNormal];
		[player play];

	} else {
		[playButton setTitle:@"Play" forState:UIControlStateNormal];
		[player pause];
	}

}

- (void) playerItemDidReachEnd:(NSNotification *)notification {
	CMTime zero = CMTimeMakeWithSeconds(0.0, 1);
	[player seekToTime:zero];
	[playButton setTitle:@"Play" forState:UIControlStateNormal];
}

- (void) toggleCaptions:sender {
	NSLog(@"Toggle captions");
	player.closedCaptionDisplayEnabled = !player.closedCaptionDisplayEnabled;
	if(player.closedCaptionDisplayEnabled == NO)
		[captionsButton setTitle:@"Captions Off" forState:UIControlStateNormal];
	else{
		[captionsButton setTitle:@"Captions On" forState:UIControlStateNormal];
	}
}

- (void)observeValueForKeyPath:(NSString*) path
	ofObject:(id)object
	change:(NSDictionary*)change
	context:(void*)context
{
	if (context == TimedMetadataObserverContext)
	{
		tmarray = [[player currentItem] timedMetadata];
		for (AVMetadataItem *metadataItem in tmarray)
		{
			[self handleTimedMetadata:metadataItem];
		}

	}
}

- (void)handleTimedMetadata:(AVMetadataItem*)timedMetadata
{
	/* Uplynk metadata comes in the format assetid_ray_slicenum.
	These values can be observed to help detect bitrate changes
	or content switches such as ad breaks.*/

	NSDictionary *propertyList = (NSDictionary *)[timedMetadata value];
	NSLog(@"%@", propertyList);
}

- (void)didReceiveMemoryWarning
{
	[super didReceiveMemoryWarning];
	// Dispose of any resources that can be recreated.
}

@end

The method for accessing timed metadata is an example of Key-Value Observing. In the sample we add a context instance, a variable that keeps track of our key name, and an array to hold our metadata. In our viewDidLoad method we add our key-value observer to our player instance, along with the key we're interested in watching for changes, and the context we declared. When a change in that key occurs, the observeValueForKeyPath method is called. We then compare our context to the context of the change. KVO is used everywhere in iOS. We use the context to make sure we're observing the changes on the objects we expect to be observed. Finally, the handleTimedMetadata method is called when a change is observed. In our case it prints out the timed metadata to our debug console.

Finished! Your Next Challenge

You've created the simplest of AVPlayer apps, played our content, created custom controls, and observed timed metadata. From here you can extend the interface to include scrub bars for seeking through content, buttons to share content on Facebook, or integrate with Twitter to allow users to tweet about your content.

For the challenge, try implementing the player view, play and caption buttons on the iPad storyboard, MainStoryboard_iPad.storyboard.