Avplayer item pre buffering while scrolling on UITableView - ios

I have created an UITableView with custom UITableViewCells. UITableView consists of images and videos that load via internet. While user is scrolling in one of the UITableViewCell i load AVPlayer to play a hls video. I set URL in hls format to the avplayer item in order to play the item.
self.playerController = [[AVPlayerViewController alloc] init];
NSURL *videoURL = #"https://playeritemvideourl";
self.player = [AVPlayer playerWithURL:url];
self.playerController.player = self.player;
[self.player play];
The video plays but there is a delay of about 3 to 5 seconds from the moment [self.player play] is triggered. How do i pre buffer the video to the currentitem of avplayer so when i scroll to the specific UITableViewCell the video starts playing instantly? I looked at preferredForwardBufferDuration property on AVPlayerItem but does not seem to make any difference. Any help or suggestions appreciated!

AVPlayer begins streaming m3u8 when it is instantiated. (I noticed this by monitoring the Network graph in the Debug navigator in Xcode. I instantiated an AVPlayer without calling -[play] and the network was under load.) Instead of loading the AVPlayer once the cell becomes visible, instantiate the AVPlayer before the cell is visible and play the content when it becomes visible.
This can be implemented by first having some data structure hold the AVPlayer items. I would recommend a NSMutableDictionary, as we can set the key to the video's URL we want and the object can be the AVPlayer already loaded with the URL. There are two ways to populate the structure. You could load it all at once in a method like -viewDidLoad or load items in dynamically in -scrollViewDidScroll: to determine if we are close to a cell with a video. I would use the former if there is not a lot of content and the user is almost guaranteed to watch all the videos. I would use the latter if we have a lot of content to load or if the user might not watch all the videos. Here is an example with a UITableViewController.
MyTableViewController.h
#import <UIKit/UIKit.h>
#import <AVKit/AVKit.h>
#interface MyTableViewController : UITableViewController
{
NSMutableDictionary *mediaPlayers; // stores the media players, key = NSString with URL, value = AVPlayer
// you don't need mediaKeyIndexPaths or mediaKeyURLs if you are taking the load-all-at-once approach
NSMutableSet *mediaKeyIndexPaths; // set that stores the key NSIndexPaths that trigger loading a media player
NSDictionary *mediaKeyURLs; // dictionary that maps a key NSIndexPath to a URL (NSString), key = NSIndexPath, value = NSString
}
#property (strong, nonatomic) AVPlayerViewController *playerController;
#end
MyTableViewController.m
#import "MyTableViewController.h"
#implementation MyTableViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// if you want to load all items at once, you can forget mediaKeyIndexPaths and mediaKeyURLs
// and instead just load all of your media here
mediaPlayers = [[NSMutableDictionary alloc] init];
// if taking the load-all-at-once approach, load mediaPlayers like this
// NSString *videoURLString = #"https://playeritemvideourl";
// AVPlayer *player = [AVPlayer playerWithURL:videoURLString];
// [mediaPlayers setObject:player forKey:#"https://playeritemvideourl"];
// lets say that the cell with the media in it is defined here
NSIndexPath *dummyMediaIndexPath = [NSIndexPath indexPathForRow:40 inSection:0];
// calculate an index path that, when visible, will trigger loading the media at dummyMediaIndexPath
NSIndexPath *dummyKeyIndexPath = [NSIndexPath indexPathForRow:dummyMediaIndexPath.row-10
inSection:dummyMediaIndexPath.section];
// add the key index path to the set of key index paths
mediaKeyIndexPaths = [[NSMutableSet alloc] initWithObjects:dummyKeyIndexPath, nil];
// define mediaKeyURLs mapping the key dummyKeyIndexPath to the value of the URL string we want
mediaKeyURLs = [[NSDictionary alloc] initWithObjectsAndKeys:
#"https://playeritemvideourl", dummyKeyIndexPath,
nil];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 100;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"TextCell" forIndexPath:indexPath];
// this is the row of dummyMediaIndexPath defined in -viewDidLoad
if (indexPath.row == 40)
{
self.playerController = [[AVPlayerViewController alloc] init];
NSString *videoURLString = #"https://playeritemvideourl";
AVPlayer *player = [mediaPlayers objectForKey:videoURLString]; // load player with URL
if (!player)
{
player = [AVPlayer playerWithURL:[NSURL URLWithString:videoURLString]];
NSLog(#"Video with URL: %# was not preloaded. Loading now.", videoURLString);
}
self.playerController.player = player;
// [player play];
// present your playerController here
[cell setText:#"Player cell"];
}
else
{
[cell setText:[NSString stringWithFormat:#"Cell #%li", (long)indexPath.row]];
}
return cell;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// get visible rows
NSArray *visibleRows = [self.tableView indexPathsForVisibleRows];
// traverse through rows
for (NSIndexPath *i in visibleRows)
{
// we use an NSSet to quickly determine if i is contained in mediaKeyIndexPaths
if ([mediaKeyIndexPaths containsObject:i])
{
[mediaKeyIndexPaths removeObject:i]; // we only need to load a player once
NSString *videoURLString = [mediaKeyURLs objectForKey:i];
NSLog(#"Preloading URL: %#", videoURLString); // for information purposes only
AVPlayer *player = [AVPlayer playerWithURL:[NSURL URLWithString:videoURLString]];
[mediaPlayers setObject:player forKey:videoURLString];
}
}
}
#end
This is about as specific as I can make with the amount of code and information you provided. Hope this helps!

Related

Xcode Objective-C IOS - How to play two videos next to each other?

I want to play two videos next to each other.
When building Mac apps I did this by using a AVPlayer object.
It is a view that you could place inside of your ViewController.
But when I tried to find the AVPlayer object in the object library in an IOS project it didn't show up. Only the AVKit Player View Controller did.
But the AVKit Player View Controller always plays videos full screen in a separate ViewController. I have searched the internet to find a way to display two videos next to each other but so far, I have not found anything useful.
So my question is: Is there any way that I can display two videos next to each other in an IOS application?
// ViewController.h
#import AVFoundation;
#import AVKit;
#property(nonatomic, readonly) AVPlayerItem *currentItem;
// ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
i=0;
[self videoPlay:[_passdataArr valueForKey:#"videoUrl"]];
}
-(void)videoPlay:(NSArray*)links
{
NSLog(#"1");
//create an AVPlayer
_detailView.hidden=YES;
// NSMutableArray *videolinksArr=[[NSMutableArray alloc] init];
// videolinksArr=_videoLinkArr;
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:#"%#",links]];
// create a player view controller
player = [AVPlayer playerWithURL:url];
AVPlayerViewController *controller = [[AVPlayerViewController alloc] init];
[self addChildViewController:controller];
[self.view addSubview:controller.view];
controller.view.frame = self.bgimg.frame;
controller.player = player;
controller.showsPlaybackControls = YES;
player.closedCaptionDisplayEnabled = NO;
[player pause];
[player play];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(AllVideoPlay:) name:AVPlayerItemDidPlayToEndTimeNotification object:_currentItem];
}
-(void)AllVideoPlay:(NSNotification*)aNotification
{
NSLog(#"2");
NSArray *temp = _videoLinkArr[i];
[self videoPlay:temp];
i++;
}
But the AVKit Player View Controller always plays videos full screen in a separate ViewController
That's false. If you want the movie view to appear as a subview, make the AVPlayerViewController a child view controller. Start with the Container View Controller in the storyboard and make an Embed segue to an AVPlayerViewController.
Or, you could use AVPlayer without an AVPlayerViewController (in code).
AVPlayer has been available on iOS since iOS 4. AVPlayer is a controller object that manages the playback of media assets with no UI elements directly tied to it, which is why it is not located in the IB object library. Rather you can build your own UI using AVPlayerLayer our use Apple's standard AVPlayerViewController, which includes default controls and standard playback behavior. The fastest way to get two videos playing side-by-side is to embed two instances of AVPlayerViewController inside another view controller. If you are using storyboards your code could be as short as:
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.destinationViewController isKindOfClass:[AVPlayerViewController class]] && [segue.identifier isEqualToString:#"First Player"])
{
AVPlayerViewController *avPlayerViewController = (AVPlayerViewController *)segue.destinationViewController;
avPlayerViewController.player = [AVPlayer playerWithURL: [NSURL URLWithString:#"http://devstreaming.apple.com/videos/wwdc/2016/703rx8zlfedjfom6l93/703/hls_vod_mvp.m3u8"]];
[avPlayerViewController.player play];
} else if ([segue.destinationViewController isKindOfClass:[AVPlayerViewController class]] && [segue.identifier isEqualToString:#"Second Player"])
{
AVPlayerViewController *avPlayerViewController = (AVPlayerViewController *)segue.destinationViewController;
avPlayerViewController.player = [AVPlayer playerWithURL: [NSURL URLWithString:#"http://devstreaming.apple.com/videos/wwdc/2016/703rx8zlfedjfom6l93/704/hls_vod_mvp.m3u8"]];
[avPlayerViewController.player play];
}
}
#end
Or something similar. Where most of the work is done by two container views and embed segues to two AVPlayerViewControllers If you are looking to have custom video UI, you can look at Apple's PIP sample code where they show how you can use key value observing to a video player with custom UI.

How to open url from table cells in webview iOS

I have just figured out how to make a tabbed navigation controller open in two table views. I would like to be able to click on one of the table cells (6 on a page), and for this to open the url in a webview.
This is my code:
#import "FlashTopicsEViewController.h"
#import "ADVTheme.h"
#implementation FlashTopicsEViewController
#synthesize webView;
NSArray *flashtopicsEth;
NSArray *urlLinks;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.;
self.view.backgroundColor = [ADVTheme viewBackgroundColor];
self.view.tintColor = [UIColor whiteColor];
flashtopicsEth = [NSArray arrayWithObjects:#"Topic1",#"Topic2", nil];
urlLinks = [[NSArray alloc] initwithObjects:#"http://url1.com", #"http://url2.com",nil];
webView.delegate = self;
NSURL *URL = [NSURL URLWithString: urlLinks[indexPath.row]];
NSURLRequest *requestObj = [NSURLRequest requestWithURL:URL];
[webView loadRequest:requestObj];
}
and this is the code below it:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *simpleTableIdentifier = #"FlashTopicECell";
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:simpleTableIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc]
initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:simpleTableIdentifier];
}
UIImage *image1 = [UIImage imageNamed:#"flashImage.png"];
cell.imageView.image = image1;
cell.textLabel.text = [flashtopicsEth objectAtIndex:indexPath.row];
return cell;
}
and this is what I have in my .h file
#import <UIKit/UIKit.h>
#interface FlashTopicsEViewController : UIViewController
#property NSArray *urlLinks;
#property NSArray *flashtopicsEth;
#property (nonatomic,strong) IBOutlet UIWebView *webView;
#end
#matt is right, this is beyond trivial to do lol :) But anyway, what you need to do is to make use of one of UITableView's delegate methods called "didSelectRowAtIndexPath". This will detect when a cell is tapped:
Your question is very basic and not detailed, so for the purposes of this basic answer, I am going to declare a UIWebView in the header file and then use that. You will need to add it to your ViewController and link it up. I also do NOT know how you are storing your URLs, I presume you are using an array? Well for the purposes of this answer, I will too.
IBOutlet UIWebView *websiteView;
NSArray *urlLinks;
Now on to the implementation code:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// Get the cell number that was tapped and then
// use that to access your array of URLs.
// Then pass that URL to a UIWebView.
NSURL *URL = [NSURL URLWithString:urlLinks[indexPath.row]];
NSURLRequest *requestObj = [NSURLRequest requestWithURL:URL];
[websiteView loadRequest:requestObj];
}
Also in the viewDidLoad, set the webview delegate like so:
websiteView.delegate = self;
Update - In answer to your comments
Ok so in my answer I just assumed you are using an array for the list of URLs. But there are other ways too. You don't have to use an array if you don't want to.
As to how you populate the array? Well that depends on quite a few things:
1) Are you downloading a list of URLs from a server?
2) Are you setting the list of URLs in your code?
If you are doing option 1, then you are probably parsing something like a JSON file which you are downloading from a server. In this case you may want to use something a long the lines of:
urlLinks = [[feed objectForKey:#"items"] valueForKey:#"url"];
The above code will put all the url links into your array. (This is just a very tiny snippet - not the whole code - just to give you an idea).
If you are going with option 2, so you already know the list of URLs and you want to define them in your app, then just do this:
NSArray *urlLinks = [[NSArray alloc] initwithObjects:#"http://url1.com", #"http://url2.com", ...etc.... , nil];
Right so to then access the URLs, you need to basically get the string from the correct array object and then turn that into an NSURL and then pass that to your webview like so:
NSURL *URL = [NSURL URLWithString:urlLinks[indexPath.row]];
NSURLRequest *requestObj = [NSURLRequest requestWithURL:URL];
[websiteView loadRequest:requestObj];
Lastly, how you present your UIWebView is up to you. It doesn't have to be in a seperate ViewController if you dont want it to. Thats up to you. But if you do, then yes, the next bit is to present the next view controller on screen. Just remember you will have to pass your URL to it, if you want the UIWebView in that view controller to present your website.
Update 2 - in answer to your question/code
Ok there are a few things wrong with your code:
Get rid of the "etc...." bit in your array declaration.... Lol I only added that to help you understand how to populate the array. So it should really be:
flashtopicsEth = [NSArray arrayWithObjects:#"Topic1",#"Topic2", nil];
You have just randomly inserted your array code in your implementation (.m) file.... that's not where you put it. You can insert it in methods such as the viewDidLoad or other methods that you make yourself. You can't just randomly add it wherever you feel like. So for the purposes of keeping things simple, lets start with just assigning the arrays in the viewDidLoad like so:
-(void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
flashtopicsEth = [NSArray arrayWithObjects:#"Topic1", #"Topic2", nil];
urlLinks = [[NSArray alloc] initwithObjects:#"http://url1.com",#"http://url2.com", nil];
webView.delegate = self;
NSURL *URL = [NSURL URLWithString: urlLinks[indexPath.row]];
NSURLRequest *requestObj = [NSURLRequest requestWithURL:URL];
[webView loadRequest:requestObj];
}
Declare your NSArray's in your header file and assign them in the implementation (.m) file. Don't declare them in the viewDidLoad because they will then be local to the viewDidLoad and the other UITableView methods will not be able to access them unless your pass the array to them. For the purposes of keeping things simple (as you are a beginner), just declare them like this in your header (.h) file:
#import <UIKit/UIKit.h>
#interface FlashTopicsEViewController : UIViewController {
NSArray *flashtopicsEth;
NSArray *urlLinks;
}
#property (nonatomic,strong) IBOutlet UIWebView *webView;
#end

Next Song in MediaItemCollection?

How do I find out the name of the next item to be played in an MPMediaItem collection? I would prefer to store this as an MPMediaItem.
I have a songsViewController that has a tableView of all the songs. This is my code for didSelectRowAtIndexPath:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
MPMediaItem *selectedItem;
selectedItem = [[songs objectAtIndex:indexPath.row] representativeItem];
MPMusicPlayerController *musicPlayer = [MPMusicPlayerController iPodMusicPlayer];
[musicPlayer setQueueWithItemCollection:[MPMediaItemCollection collectionWithItems:[songsQuery items]]];
[musicPlayer setNowPlayingItem:selectedItem];
nowPlayingViewController *nowPlaying = [[rightSideMenuViewController alloc]
initWithNibName:#"nowPlayingViewController" bundle:nil];
[self presentViewController:nowPlaying animated:YES completion:nil];
[musicPlayer play];
}
I have a UILabel on my nowPlayingViewController which pops up when the user has selected a song. I would like to store the title of the next item in the MediaItemCollection/queue to be in that UILabel - so it is a preview of what the next song is.
Any help would be much appreciated, thanks! :)
Keep your list (since your can't access at musicplayer queue).
#property (nonatomic, strong) NSArray *playlist;
When you do:
[musicPlayer setQueueWithItemCollection:[MPMediaItemCollection collectionWithItems:[songsQuery items]]];
Add:
playlist = [songsQuery items];
To fetch your previous/next:
-(MPMediaItem *)nextItem
{
int currentIndex = [musicPlayer indexOfNowPlayingItem];
MPMediaItem *nextItem = [playlist objectAtIndex:currentIndex+1];
return nextItem
}
-(MPMediaItem *)previousItem
{
int currentIndex = [musicPlayer indexOfNowPlayingItem];
MPMediaItem *previousItem = [playlist objectAtIndex:currentIndex-1];
return previousItem;
}
Important note:
I didn't check if the current item was the first/last (according if you want previous/next) item in playlist. So be careful with bounds of the NSArray, or you'll get a:
NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index Z beyond bounds [X .. Y]
So does previousItem "exists"? Does nextItem "exists"?
You may also have to look at:
#property (nonatomic) MPMusicRepeatMode repeatMode
In case that the nextItem may be the first item, or the previous item the last one.
MPMusicPlayerController provides the following instance methods to quickly skip to your next or previous song:
[musicPlayer skipToNextItem];
[musicPlayer skipToPreviousItem];

DTMF sounds for custom Dialer iOS

I am trying to add dtmf sounds to my custom dialer pad.
I managed reproducing a sound for each number on touch down. Now I am trying to achieve the natural phone behaviour for when the person who is dialing does a "longpress" on a number.
The problem is that the sound ends. How could I make it lasting as long as the touch ends?
In my code for now I am trying to add a long sound even if I am using the touch down event, just to test if it works. If it does I will add the gesture recognizer for long press.
Here is my code:
#interface ViewController : UIViewController<AVAudioPlayerDelegate>{
//SystemSoundID toneSSIDs[12];
AVAudioPlayer *toneSSIDs[12];
}
#end
#implementation ViewController
-(id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if(self)
{
for(int count = 0; count < 12; count++){
NSString *toneFilename = [NSString stringWithFormat:#"dtmf-%d", count];
NSURL *toneURLRef = [[NSBundle mainBundle] URLForResource:toneFilename
withExtension:#"mp3"];
player = [[AVAudioPlayer alloc]initWithContentsOfURL:toneURLRef error:NULL ];
player.volume = 0.4f;
toneSSIDs[count] = player;
}
}
return self;
}
-(void)playSoundForKey:(int)key {
_sound = toneSSIDs[key];
_sound.delegate = self;
_sound.numberOfLoops = -1;
[_sound prepareToPlay];
[_sound play];
}
- (IBAction)touchNum:(id)sender {
[self playSoundForKey:[sender tag]];
}
I tried using both .wav and .mp3 sounds and also looping the sound but I get multiple sounds, not a long one as I need. Somebody help, please!

Switching views without dismissing View

I am developing a small music application in which first view comprises of all the List of songs in Tableview and when user taps on any one row (song) it takes them to Second view & Plays the song their(comprising of play/Pause Buttons). Now I go back in First View from Second View using back button to select another song. And their is one button in First View for just switching views(i.e it takes to current playing song)but it plays that same song from start but not from its currentplaying . Its similar to now Playing button in Stock Music app in iOS.
Im using storyboards & Segues
-(void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
temp = indexPath.row;
[tableView deselectRowAtIndexPath:indexPath animated:NO];
[self performSegueWithIdentifier:#"Player" sender:self];
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([[segue identifier] isEqualToString:#"Player"])
{
MPMediaItem *item = [itemCollections objectAtIndex:temp];
NSString *trackInfo = [[NSString alloc] initWithFormat:#"%#- %#",[item valueForProperty:MPMediaItemPropertyTitle],[item valueForProperty:MPMediaItemPropertyArtist]];
MPMediaItemArtwork *artwork = [item valueForProperty:MPMediaItemPropertyArtwork];
NSURL *tempURL = [item valueForProperty:MPMediaItemPropertyAssetURL];
playerController = [segue destinationViewController];
playerController.fileUrl1 = tempURL;
playerController.globalVar = [NSNumber numberWithInteger:temp];
playerController.labelTitleString = trackInfo;
playerController.img = [artwork imageWithSize:CGSizeMake(100, 100)];
}
}
You could go about this in many ways -- I would suggest implementing a singleton to handle the media playback. Rather than constantly retaining that view, this singleton manager (part of your model) should provide any controllers with the needed information about the current song, album, author, etc.
Ex. In your TableViewController:
- (void)viewDidLoad
{
[self setSong:[[PlaybackManager sharedInstance] currentSong]];
if (self.song) {
//Add navigation item
}
}

Resources