Skip to content

Instantly share code, notes, and snippets.

@JoeyBodnar
Last active June 1, 2016 11:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JoeyBodnar/d872676184396706f804d196f63ad89c to your computer and use it in GitHub Desktop.
Save JoeyBodnar/d872676184396706f804d196f63ad89c to your computer and use it in GitHub Desktop.
Part1.
First, make a file, subclassing NSObject. Call it Download. In the .h file, put this:
@property (strong, nonatomic)NSString *url;
@property BOOL isDownloading;
@property float progress;
@property (strong, nonatomic)NSURLSessionDownloadTask *downloadTask;
Leave the .m file empty.
Now to the view controller. So basically, for me the trick to getting this done is to move the video playing outside the cell, meaning outside cellForRowAtIndexPath. For my video, i had it auto play once the table view stopped scrolling, but if you want it to auto play while scrolling, I think that is possible as well, and I'll discuss how a little later. But first some code.
First, you will need a preview image--an image of the first frame of the video. This will be set in cellForRowAtIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath (NSIndexPath *)indexPath {
VideoCell *cell = (VideoCell *)[[self.tableView dequeueReusableCellWithIdentifier:@"cellReuseId"]]
// asynchronously download and set image:
YourObject *image = self.yourDataSourceArray[indexPath.row].image
cell.previewimageView.image = image
}
That's all for cellForRowAtindexPath. Almost everything else is done in scroll view delegate methods and NSURLSessionDownloadDelegate methods. You will need a variable declared at the top of your class, isScrolling. The scrollView delegate methods:
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
self.isScrolling = YES;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[self performSelector:@selector(scrollViewDidEndScrollingAnimation:) withObject:nil afterDelay:0.3];
}
-(void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView { [NSObject cancelPreviousPerformRequestsWithTarget:self]; } What those three methods do: scrollViewWillBeginDraging changes self.isScrolling to YES. The other 2 methods combine to tell you when the scrollView has stopped scrolling. Now, as soon as the tableview is done scrolling and you stop, whatever code is inside scrollViewDidEndScrollingAnimation will run.
Now, in scrollViewDidEndScrollingAnimation:
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
[NSObject cancelPreviousPerformRequestsWithTarget:self];
self.isScrolling = NO;
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSArray *visibleIndexPaths = [self.tableView indexPathsForVisibleRows];
// Loop through all visible cells on screen
for (NSIndexPath *indexPath in visibleIndexPaths) {
CGRect totalRect = [self.tableView rectForRowAtIndexPath:indexPath];
CGRect screenRect = CGRectOffset(totalRect, -self.tableView.contentOffset.x, -self.tableView.contentOffset.y);
CGFloat cellY = screenRect.origin.y;
if ([self cellIsVisible:cellY]) {
// the method cellIsVisible will see if at least 90% of the cell is visible onscreen
// now, get a reference to the cell
YourVideoCell *videoCell = (YourVideoCell *)[self.tableView cellforRowAtIndexPath:indexPath];
if (videoCell.playerLayer == nil) {
// get the URL (in nesting for now) that is going to be played in this cell
[self startDownload:@"urlStringGoesHere"];
}
}
}
}
Ok, so that is not quite all for scrollViewDidEndScrollingAnimation, as we will handle the cacheing playback later there. Above, we only play the video in the cell if the playerLayer is nil. That is because it is possible that you scroll, stop scrolling, then start scrolling again, and land on the same cell, Essentially causing this whole process to happen over again. If that happens, then the cell will be playing 2 videos, and cell reuse will cause the top view to get recycled and play in the wrong cell when you scroll
Now, the startDownloadMethod:
- (void)startDownload:(NSString *)URLString {
NSURL *url = [NSURL URLWithString:URLString];
Download *download = [[Download alloc] init];
download.url = URLString;
NSURLSession *downloadsSession = [self setupDownloadSession ];
download.downloadTask = [downloadsSession downloadTaskWithURL:url];
[download.downloadTask resume];
download.isDownloading = YES;
[self.activeDownloads setObject:download forKey:[download url]];
}
And the setupDownloadSession method:
- (NSURLSession *)setupDownloadSession {
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
return session;
}
At the top of your class by the way, you should have these 2 properties:
@property (strong, nonatomic)NSURLSessionDataTask *dataTask;
@property (strong, nonatomic)NSMutableDictionary *activeDownloads;
End part 1
Part 2
Continuing, here is the cellIsVisibleMethod. This will tell if the current cell being examined in the didEndScrollViewAnimation has at least 90% exposed on screen. Note: 294 is the height of the cells in my app:
- (BOOL)cellIsVisible:(CGFloat)cellY {
CGFloat screenHeight = [self screenSpace];
if (cellY == 0) {
return YES;
}
if (cellY > 0) {
CGFloat difference = screenHeight - cellY;
if ((difference / 294 > 0.9)) {
return YES;
} else {
return NO;
}
}
if (cellY < 0) {
CGFloat visibleSpace = 294 + cellY;
if (visibleSpace / 294 > 0.9) {
return YES;
} else {
return NO;
}
}
return NO;
}
The screenSpace method, this tells the total amount of space visible on the screen for the table view. In my app this was basically main screen height minus tab bar and segment control:
- (CGFloat) screenSpace {
CGFloat tabBarHeight = self.tabBarController.tabBar.frame.size.height;
CGFloat segmentControlHeight = 44;
CGFloat extraHeight = tabBarHeight + segmentControlHeight;
CGFloat screenSize = [[UIScreen mainScreen] applicationFrame].size.height;
CGFloat videoHeight = screenSize - extraHeight;
return videoHeight + 4;
}
Now, for the display of video :D. Make sure you conform to the NSURLSessionDownloadDelegate in your view controller. Now, implement 2 delegate methods:
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
I think this one is mandatory, so I put it here. But I don't actually use it. If you want, you can use it to track download progress of each video and maybe put that in a progress bar, but I'm not doing it for now. Here is the one I do use though:
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
}
So this is what is called as soon as the downloader finishes downloading the data. So the idea here is to get the NSData from the NSURL, cache that into the documents directory, and then do the actual playback in the cell from the documents directory.
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
NSLog(@"finished writing data");
NSData *data = [[NSData alloc] initWithContentsOfURL:location];
[self getCellForDownloadTask:downloadTask completion:^(VideoTableViewCell *videoCell, NSString *activityID) {
[DataManager cacheNewSentActivityWithData:data andactivityID:activityID];
dispatch_async(dispatch_get_main_queue(), ^{
[self playVideoInCell:videoCell withId:activityID]
});
}];
}
So basically, once inside, you have to retrieve the cell and play back the video from there. Note the activityID. In my app I'm taking videos from parse. I use the objectId as a way to cache videos (you'll see soon) and play them back. maybe you will have some other unique identifier.
now for the getCellForDownloadTaskMethod:
- (void)getCellForDownloadTask:(NSURLSessionDownloadTask *)downloadTask completion:(void (^)(VideoCell *videoCell, NSString *activityID))completionHandler {
NSString *downloadUrl = [downloadTask.originalRequest.URL absoluteString];
NSArray *visibleIndexPaths = [self.tableView indexPathsForVisibleRows];
// loop through all visible index Paths
for (NSIndexPath *indexPath in visibleIndexPaths) {
VideoCell *videoCell = (VideoCell *)[self.tableView cellForRowAtIndexPath:indexPath];
NSString *myURLString = [videoCell object].url
if ([myURLString isEqualToString: downloadUrl]) {
completion(videoCell, videoCell.object.objectId)
}
}
}
So the idea is basically to loop through all the visible index paths, and see
which one has the original URL which matches the downloadURL form the completed download task,
and match them, and then return the cell and the unique identifier in the completion handler,
and from there play the video.
Part 3. Playing the video
You may have noticed this function in the last part:
[DataManager cacheNewSentActivityWithData:data andactivityID:activityID];
this is what cached the video to the documents directory, taking the unique identifier and the NSData as parameters. Here is that method:
+ (void)cacheNewSentActivityWithData:(NSData *)videoData andactivityID:(NSString *)activityID {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *dataPath = [documentsDirectory stringByAppendingPathComponent:@"BlackberryVideo"];
if (![[NSFileManager defaultManager] fileExistsAtPath:dataPath])
[[NSFileManager defaultManager] createDirectoryAtPath:dataPath withIntermediateDirectories:NO attributes:nil error:nil];
NSString *mov = @".mov";
NSString *finalPath = [NSString stringWithFormat: @"/%@%@", activityID, mov];
NSString *videoPath = [documentsDirectory stringByAppendingString:finalPath];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if (videoData != nil) {
[defaults setObject:videoPath forKey:activityID];
BOOL success = [videoData writeToFile:videoPath atomically:NO];
NSLog(@"Successs:::: %@", success ? @"YES" : @"NO");
NSLog(@"video path --> %@",videoPath);
}
}
Now that the video is cached, you can play it using the aforementioned playVideoInCell method:
- (void)playVideoInCell:(VideoCell *)videoCell withActivityID:(NSString *)activityID {
videoCell.contentView.backgroundColor = [UIColor clearColor];
videoCell.redView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, (videoCell.frame.size.width / 2), 255)];
[videoCell.contentView insertSubview:videoCell.redView aboveSubview:videoCell.blueView];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *mov = @".mov";
NSString *fullComponent = [NSString stringWithFormat: @"%@%@", activityID, mov];
NSString *fullPath = [documentsDirectory stringByAppendingPathComponent:fullComponent];
NSURL *url = [NSURL fileURLWithPath:fullPath];
videoCell.item1 = [AVPlayerItem playerItemWithURL:url];
videoCell.player1 = [[AVPlayer alloc] initWithPlayerItem:videoCell.item1];
[videoCell.player1 setMuted:YES];
videoCell.player1.actionAtItemEnd = AVPlayerActionAtItemEndNone;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(player1Finished:)
name:AVPlayerItemDidPlayToEndTimeNotification object:[videoCell.player1 currentItem]];
videoCell.playerLayer1 = [AVPlayerLayer playerLayerWithPlayer:videoCell.player1];
videoCell.playerLayer1.frame = CGRectMake(0, 0, (videoCell.frame.size.width / 2), 255);
videoCell.playerLayer1.backgroundColor = [UIColor clearColor].CGColor;
videoCell.playerLayer1.videoGravity = AVLayerVideoGravityResize;
[videoCell.redView.layer addSublayer:videoCell.playerLayer1];
[videoCell.player1 play];
}
Ok, so watch out for the frames here--I have not edited this from my own implementation, and they only cover half the cell.
Ok, so the AVPlayerLayer needs its own UIView, because we are going to add the AVPlayerLayer to the layer of that UIView. That UIView that belongs specially to the AVPlayerLayer is the redView. Ok, so you were scrolling, and now you stopped. As soon as you stop, the scrollview delegate methods above go into action. Finally, it comes to the function (void)playVideoInCell(VideoCell *)videoCell withActivityId:(NSString *)activityId. First thing is the redView is added as a subView. But, the background color is kept as clear, so that this is invisible. Then, once the AVPlayer and AVPlayerItem are initialized, the AVPlayerLayer is added as a sublayer to the redView.layer. Here, the AVPlayerLayer background color is also kept to clear, so that this step is invisible as well. If you don't do this: videoCell.playerLayer.backgroundColor = [UIColor clearColor].CGColor; then whatever color you choose will make a small flash on screen before the video starts playing. Keep it clear, and then there will be no change on the screen until the video actually starts playing. Then it will look like the previewImage that was there just started moving and became a video, which is exactly what you want.
View hierarchy is very important also because of cell reuse. If you don't clear the cell of all video stuff (redView, AVPlayerLayer) when it disappears, then when those cells are reused you will see the AVPlayerLayer on another cell playing the wrong video, and things get messy very fast. So last but not least, the didEndDisplayingCell tableView delegate method:
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
if ([cell isKindOfClass:[VideoCell class]]) {
VideoCell *videoCell = (VideoCell *)[self.tableView cellForRowAtIndexPath:indexPath];
if (videoCell.redView != nil) {
[videoCell.redView removeFromSuperview];
[videoCell.playerLayer removeFromSuperlayer];
}
}
}
And lastly--this code plays video when you stop scrolling in the tableview.
If you want them to play as people scroll, my suggestion is to find a way to calculate scroll view speed
(this is possible, I did it originally but later deleted that code and switched to this model),
and then perform these calculations not in scrollViewDidEndScrollingAnimation, but instead in scrollViewDidScroll, when the scrollView is scrolling sufficiently slow (I assume you don't want to play videos when the user is scrolling very fast). There might be some other things to worry about with that, but I'm pretty sure that is a good start.
And the last thing--playing from the cache instead of having to download first. change the scrollviewDidEndScrollingAnimation:
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
..... previous code here....
if ([self cellIsVisible:cellY]) {
// the method cellIsVisible will see if at least 90% of the cell is visible onscreen
// now, get a reference to the cell
YourVideoCell *videoCell = (YourVideoCell *)[self.tableView cellforRowAtIndexPath:indexPath];
if ([defaults objectForKey:myObject.objectId] == nil {
if (videoCell.playerLayer == nil) {
// get the URL (in nesting for now) that is going to be played in this cell
[self startDownload:@"urlStringGoesHere"];
} else {
[self playVideoInCell: videoCell withActivityID: myObject.objectId];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment