diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ebbef4b --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Xcode +.DS_Store +*/build/* +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +profile +*.moved-aside +DerivedData +.idea/ +*.hmap + +#CocoaPods +Pods diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bc1def5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# GoogleMediaFramework CHANGELOG + +## 0.1.0 + +Initial release. We are looking for feedback and bug reports, please file an issue in the Github project page. + +Features: +- Basic video playback of iOS system supported video formats +- Demo app integrated with Google IMA ads SDK (optional when using the player) + diff --git a/GoogleMediaFramework.podspec b/GoogleMediaFramework.podspec new file mode 100644 index 0000000..f9fcb52 --- /dev/null +++ b/GoogleMediaFramework.podspec @@ -0,0 +1,26 @@ +# +# Be sure to run `pod spec lint NAME.podspec' to ensure this is a +# valid spec and remove all comments before submitting the spec. +# +# To learn more about the attributes see http://docs.cocoapods.org/specification.html +# +Pod::Spec.new do |s| + s.name = "GoogleMediaFramework" + s.version = "0.1.0" + s.summary = "A video player framework for playing videos. Integrates easily with the Google IMA SDK for including advertising on your videos." + s.homepage = "https://github.com/googleads/Google-Media-Framework-iOS" + s.screenshots = "www.example.com/screenshots_1", "www.example.com/screenshots_2" + s.license = { :type => "Apache License, Version 2.0", :file => "LICENSE" } + s.author = "Google" + s.source = { :git => "https://github.com/googleads/Google-Media-Framework-iOS.git", :tag => s.version.to_s } + + s.platform = :ios, '5.0' + s.requires_arc = true + + s.source_files = 'GoogleMediaFramework' + s.resources = 'Resources/*' + + #s.public_header_files = 'GoogleMediaFramework/GoogleMediaFramework.h' + + # s.frameworks = 'SomeFramework', 'AnotherFramework' +end diff --git a/GoogleMediaFramework/GMFAdService.h b/GoogleMediaFramework/GMFAdService.h new file mode 100644 index 0000000..566e4de --- /dev/null +++ b/GoogleMediaFramework/GMFAdService.h @@ -0,0 +1,25 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "GMFPlayerViewController.h" + +@interface GMFAdService : NSObject + +@property(nonatomic, weak) GMFPlayerViewController *videoPlayerController; + +- (id)initWithGMFVideoPlayer:(GMFPlayerViewController* )videoPlayerController; + +@end diff --git a/GoogleMediaFramework/GMFAdService.m b/GoogleMediaFramework/GMFAdService.m new file mode 100644 index 0000000..35e6f5f --- /dev/null +++ b/GoogleMediaFramework/GMFAdService.m @@ -0,0 +1,67 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "GMFAdService.h" + +@implementation GMFAdService + +- (id)init { + NSAssert(false, @"init not available, use initWithGMFVideoPlayer."); + return nil; +} + +// Designated initializer +- (id)initWithGMFVideoPlayer:(GMFPlayerViewController *)videoPlayerController { + self = [super init]; + if (self) { + _videoPlayerController = videoPlayerController; + + // Listen for playback finished event. See GMFPlayerFinishReason. + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(playbackWillFinish:) + name:kGMFPlayerStateWillChangeToFinishedNotification + object:_videoPlayerController]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(playbackDidFinish:) + name:kGMFPlayerStateDidChangeToFinishedNotification + object:_videoPlayerController]; + } + return self; +} + +- (void)playbackWillFinish:(NSNotification *)notification { + // Override this in your AdService class to play any postrolls or post-content events. +} + +- (void)playbackDidFinish:(NSNotification *)notification { + // After playbackWillFinish +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] + removeObserver:self + name:kGMFPlayerStateWillChangeToFinishedNotification + object:nil]; + [[NSNotificationCenter defaultCenter] + removeObserver:self + name:kGMFPlayerStateDidChangeToFinishedNotification + object:nil]; +} + +@end diff --git a/GoogleMediaFramework/GMFPlayerControlsProtocol.h b/GoogleMediaFramework/GMFPlayerControlsProtocol.h new file mode 100644 index 0000000..0deb2b8 --- /dev/null +++ b/GoogleMediaFramework/GMFPlayerControlsProtocol.h @@ -0,0 +1,32 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +@protocol GMFPlayerControlsProtocol + +// These are all mutually exclusive. E.g. calling showPlayButton hides all the +// other views and shows the play button. Only one can be shown at a time. +- (void)showPlayButton; +- (void)showPauseButton; +- (void)showReplayButton; + +- (void)enableSeekbarInteraction; +- (void)disableSeekbarInteraction; +- (void)setSeekbarTrackColor:(UIColor *)color; + +- (void)setTotalTime:(NSTimeInterval)totalTime; +- (void)setMediaTime:(NSTimeInterval)mediaTime; + +@end diff --git a/GoogleMediaFramework/GMFPlayerControlsView.h b/GoogleMediaFramework/GMFPlayerControlsView.h new file mode 100644 index 0000000..6120f51 --- /dev/null +++ b/GoogleMediaFramework/GMFPlayerControlsView.h @@ -0,0 +1,62 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +@protocol GMFPlayerControlsViewDelegate + +- (void)didPressPlay; +- (void)didPressPause; +- (void)didPressReplay; +- (void)didPressMinimize; + +// User seeked to a given time relative to the start of the video. +- (void)didSeekToTime:(NSTimeInterval)time; +- (void)didStartScrubbing; +- (void)didEndScrubbing; + +@end + +@interface GMFPlayerControlsView : UIView + +// Toggle visibility among play, pause, and replay buttons. +- (void)showPlayButton; +- (void)showPauseButton; +- (void)showReplayButton; + +// Set the total duration of the video. May be NaN or Infinity if the +// total time is unknown. Call updateScrubberAndTime to make the change visible. +- (void)setTotalTime:(NSTimeInterval)totalTime; + +// Set the amount of video downloaded. Call updateScrubberAndTime to make +// the change visible. +- (void)setDownloadedTime:(NSTimeInterval)downloadedTime; + +// Set the current position of the scrubber within the total video duration. +// Call updateScrubberAndTime to make the change visible. +- (void)setMediaTime:(NSTimeInterval)mediaTime; + +- (void)updateScrubberAndTime; + +- (CGFloat)preferredHeight; + +- (void)setDelegate:(id)delegate; + +- (void)setSeekbarTrackColor:(UIColor *)color; + +- (void)disableSeekbarInteraction; +- (void)enableSeekbarInteraction; + +@end + diff --git a/GoogleMediaFramework/GMFPlayerControlsView.m b/GoogleMediaFramework/GMFPlayerControlsView.m new file mode 100644 index 0000000..303b3df --- /dev/null +++ b/GoogleMediaFramework/GMFPlayerControlsView.m @@ -0,0 +1,278 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "GMFPlayerControlsView.h" +#import "GMFResources.h" +#import "UILabel+GMFLabels.h" +#import "UIView+GMFPositioning.m" + +static const CGFloat kBarPaddingX = 4; + +@implementation GMFPlayerControlsView { + UIImageView *_backgroundView; + UIButton *_playButton; + UIButton *_pauseButton; + UIButton *_replayButton; + UIButton *_minimizeButton; + UILabel *_secondsPlayedLabel; + UILabel *_totalSecondsLabel; + UISlider *_scrubber; + NSTimeInterval _totalSeconds; + NSTimeInterval _mediaTime; + NSTimeInterval _downloadedSeconds; + BOOL _userScrubbing; + + __weak id _delegate; +} + +// TODO(tensafefrogs): Add _secondsPlayedLabel / _totalSecondsLabel to controls +- (id)init { + self = [super initWithFrame:CGRectZero]; + if (self) { + _backgroundView = [[UIImageView alloc] + initWithImage:[GMFResources playerBarBackgroundImage]]; + [self addSubview:_backgroundView]; + + _secondsPlayedLabel = [UILabel GMF_clearLabelForPlayerControls]; + [_secondsPlayedLabel setTextAlignment:NSTextAlignmentCenter]; + [_secondsPlayedLabel setIsAccessibilityElement:NO]; + [self addSubview:_secondsPlayedLabel]; + + _totalSecondsLabel = [UILabel GMF_clearLabelForPlayerControls]; + [_totalSecondsLabel setIsAccessibilityElement:NO]; + [self addSubview:_totalSecondsLabel]; + + _playButton = [self playerButtonWithImage:[GMFResources playerBarPlayButtonImage] + action:@selector(didPressPlay:) + accessibilityLabel:@"Play"]; + [self addSubview:_playButton]; + + _pauseButton = [self playerButtonWithImage:[GMFResources playerBarPauseButtonImage] + action:@selector(didPressPause:) + accessibilityLabel:@"Pause"]; + [self addSubview:_pauseButton]; + + _replayButton = [self playerButtonWithImage:[GMFResources playerBarReplayButtonImage] + action:@selector(didPressReplay:) + accessibilityLabel:@"Replay"]; + [self addSubview:_replayButton]; + + // Seekbar + _scrubber = [[UISlider alloc] init]; + [_scrubber setMinimumValue:0.0]; + [_scrubber setAccessibilityLabel:@"Seek bar"]; + [self setSeekbarThumbToDefaultImage]; + [_scrubber setMaximumTrackTintColor:[UIColor colorWithWhite:122/255.0 alpha:1.0]]; + [_scrubber addTarget:self + action:@selector(didScrubbingProgress:) + forControlEvents:UIControlEventValueChanged]; + // Scrubbing starts as soon as the user touches the scrubber. + [_scrubber addTarget:self + action:@selector(didScrubbingStart:) + forControlEvents:UIControlEventTouchDown]; + [_scrubber addTarget:self + action:@selector(didScrubbingEnd:) + forControlEvents:UIControlEventTouchUpInside]; + [_scrubber addTarget:self + action:@selector(didScrubbingEnd:) + forControlEvents:UIControlEventTouchUpOutside]; + [self addSubview:_scrubber]; + + _minimizeButton = [self playerButtonWithImage:[GMFResources playerBarMinimizeButtonImage] + action:@selector(didPressMinimize:) + accessibilityLabel:@"Minimize"]; + [self addSubview:_minimizeButton]; + + [self showPlayButton]; + } + return self; +} + +- (id)initWithFrame:(CGRect)frame { + NSAssert(false, @"Invalid initializer."); + return nil; +} + +- (void)dealloc { + [_playButton removeTarget:self + action:NULL + forControlEvents:UIControlEventTouchUpInside]; + [_pauseButton removeTarget:self + action:NULL + forControlEvents:UIControlEventTouchUpInside]; + [_replayButton removeTarget:self + action:NULL + forControlEvents:UIControlEventTouchUpInside]; + [_scrubber removeTarget:self + action:NULL + forControlEvents:UIControlEventAllEvents]; + [_minimizeButton removeTarget:self + action:NULL + forControlEvents:UIControlEventTouchUpInside]; +} + +- (void)showPlayButton { + [_playButton setHidden:NO]; + [_pauseButton setHidden:YES]; + [_replayButton setHidden:YES]; +} + +- (void)showPauseButton { + [_playButton setHidden:YES]; + [_pauseButton setHidden:NO]; + [_replayButton setHidden:YES]; +} + +- (void)showReplayButton { + [_playButton setHidden:YES]; + [_pauseButton setHidden:YES]; + [_replayButton setHidden:NO]; +} + +- (CGRect)progressBarRect { + CGFloat scrubberX = CGRectGetWidth(_playButton.frame) + kBarPaddingX; + CGFloat scrubberWidth = CGRectGetWidth(self.frame) + - (kBarPaddingX * 3) + - scrubberX + - [_minimizeButton GMF_visibleWidth]; + CGRect scrubberRect = CGRectMake(scrubberX, + 0, + scrubberWidth, + [self preferredHeight]); + return scrubberRect; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + [_backgroundView setFrame:[self bounds]]; + + [_playButton GMF_alignCenterLeftToCenterLeftOfView:self paddingX:kBarPaddingX]; + [_pauseButton GMF_alignCenterLeftToCenterLeftOfView:self paddingX:kBarPaddingX]; + [_replayButton GMF_alignCenterLeftToCenterLeftOfView:self paddingX:kBarPaddingX]; + + // TODO(tensafefrogs): Investigate using NSLayoutConstraint here. + CGFloat xPosition = CGRectGetWidth(self.frame) - (CGRectGetWidth(_minimizeButton.frame) / 2.0); + _minimizeButton.center = [UIView + GMF_pixelAlignedPoint:CGPointMake(xPosition - kBarPaddingX, (self.frame.size.height / 2.0))]; + [_scrubber GMF_setPixelAlignedFrame:[self progressBarRect]]; +} + +- (void)setTotalTime:(NSTimeInterval)totalTime { + _totalSeconds = totalTime; +} + +- (void)setDownloadedTime:(NSTimeInterval)downloadedTime { + _downloadedSeconds = downloadedTime; +} + +- (void)setMediaTime:(NSTimeInterval)mediaTime { + _mediaTime = mediaTime; +} + +- (CGFloat)preferredHeight { + return [[GMFResources playerBarBackgroundImage] size].height; +} + +- (void)setDelegate:(id)delegate { + _delegate = delegate; +} + +- (void)updateScrubberAndTime { + // TODO(tensafefrogs): Handle live streams + [_scrubber setMaximumValue:_totalSeconds]; + if (_userScrubbing) { + [self setMediaTime:[_scrubber value]]; + _userScrubbing = NO; + } else { + // If time is this low, we might be resetting the slider after a video completes, so don't want + // it to slide back to zero animated. + BOOL animated = _mediaTime <= 0.5; + [_scrubber setValue:_mediaTime animated:animated]; + } +} + +#pragma mark Private Methods + +- (void)setSeekbarThumbToDefaultImage { + [_scrubber setThumbImage:[GMFResources playerBarScrubberThumbImage] forState:UIControlStateNormal]; +} + +- (void)didPressPlay:(id)sender { + [_delegate didPressPlay]; +} + +- (void)didPressPause:(id)sender { + [_delegate didPressPause]; +} + +- (void)didPressReplay:(id)sender { + [_delegate didPressReplay]; +} + +- (void)didPressMinimize:(id)sender { + [_delegate didPressMinimize]; +} + +- (void)didScrubbingStart:(id)sender { + _userScrubbing = YES; + [_delegate didStartScrubbing]; +} + +- (void)didScrubbingProgress:(id)sender { + _userScrubbing = YES; + [self updateScrubberAndTime]; +} + +- (void)didScrubbingEnd:(id)sender { + _userScrubbing = YES; + [_delegate didSeekToTime:[_scrubber value]]; + [_delegate didEndScrubbing]; + [self updateScrubberAndTime]; +} + +- (void)setSeekbarTrackColor:(UIColor *)color { + [_scrubber setMinimumTrackTintColor:color]; +} + +- (void)disableSeekbarInteraction { + [_scrubber setThumbImage:[[UIImage alloc] init] forState:UIControlStateNormal]; + [_scrubber setUserInteractionEnabled:NO]; +} + +- (void)enableSeekbarInteraction { + [self setSeekbarThumbToDefaultImage]; + [_scrubber setUserInteractionEnabled:YES]; +} + +- (UIButton *)playerButtonWithImage:(UIImage *)image + action:(SEL)action + accessibilityLabel:(NSString *)accessibilityLabel { + UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; + [button setImage:image forState:UIControlStateNormal]; + [button addTarget:self + action:action + forControlEvents:UIControlEventTouchUpInside]; + [button setAccessibilityLabel:accessibilityLabel]; + [button setExclusiveTouch:YES]; + [button setShowsTouchWhenHighlighted:YES]; + [button sizeToFit]; + return button; +} + +@end + diff --git a/GoogleMediaFramework/GMFPlayerFinishReason.h b/GoogleMediaFramework/GMFPlayerFinishReason.h new file mode 100644 index 0000000..d4a92fb --- /dev/null +++ b/GoogleMediaFramework/GMFPlayerFinishReason.h @@ -0,0 +1,20 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +typedef enum { + GMFPlayerFinishReasonPlaybackEnded = 0, + GMFPlayerFinishReasonPlaybackError, + GMFPlayerFinishReasonUserExited +} GMFPlayerFinishReason; + diff --git a/GoogleMediaFramework/GMFPlayerOverlayView.h b/GoogleMediaFramework/GMFPlayerOverlayView.h new file mode 100644 index 0000000..d3d86be --- /dev/null +++ b/GoogleMediaFramework/GMFPlayerOverlayView.h @@ -0,0 +1,38 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import +#import "GMFPlayerControlsProtocol.h" +#import "GMFPlayerControlsView.h" + +@interface GMFPlayerOverlayView : UIView { + @private + GMFPlayerControlsView *_playerControlsView; +} + +@property(nonatomic, weak) id delegate; + +@property(nonatomic, readonly) GMFPlayerControlsView *playerControlsView; + +// Show/hide the loading spinner +- (void)showSpinner; +- (void)hideSpinner; + +- (void)setPlayerBarVisible:(BOOL)visible; + +- (void)setSeekbarTrackColor:(UIColor *)color; +- (void)setSeekbarTrackColorDefault; + +@end + diff --git a/GoogleMediaFramework/GMFPlayerOverlayView.m b/GoogleMediaFramework/GMFPlayerOverlayView.m new file mode 100644 index 0000000..cdfd023 --- /dev/null +++ b/GoogleMediaFramework/GMFPlayerOverlayView.m @@ -0,0 +1,136 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "GMFPlayerOverlayView.h" +#import "UIView+GMFPositioning.m" + +@implementation GMFPlayerOverlayView { + UIActivityIndicatorView *_spinner; +} + +- (id)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + _spinner = [[UIActivityIndicatorView alloc] + initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; + [_spinner setUserInteractionEnabled:NO]; + [_spinner setIsAccessibilityElement:NO]; + [_spinner sizeToFit]; + [self addSubview:_spinner]; + + // Player control bar + _playerControlsView = [[GMFPlayerControlsView alloc] init]; + [self setSeekbarTrackColorDefault]; + [self addSubview:_playerControlsView]; + } + return self; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + [_spinner setCenter:CGPointMake(self.frame.size.width / 2.0, self.frame.size.height / 2.0)]; + + [_playerControlsView GMF_setSize:CGSizeMake(CGRectGetWidth(self.frame), + [_playerControlsView preferredHeight])]; + // Align to bottom edge + // TODO(tensafefrogs): Use springs/struts here to align UI elements + [_playerControlsView GMF_setOrigin:CGPointMake(0, + self.frame.size.height - [_playerControlsView preferredHeight])]; +} + +- (void)setDelegate:(id)delegate { + [_playerControlsView setDelegate:delegate]; +} + +- (void)showSpinner { + [_spinner startAnimating]; + [_spinner setHidden:NO]; +} + +- (void)hideSpinner { + [_spinner stopAnimating]; + [_spinner setHidden:YES]; +} + +- (void)setPlayerBarVisible:(BOOL)visible { + [_playerControlsView setAlpha:visible ? 1 : 0]; + + [self setNeedsLayout]; + [self layoutIfNeeded]; +} + +- (void)showPlayButton { + [_playerControlsView showPlayButton]; +} + +- (void)showPauseButton { + [_playerControlsView showPauseButton]; +} + +- (void)showReplayButton { + [_playerControlsView showReplayButton]; +} + +- (void)setTotalTime:(NSTimeInterval)totalTime { + [_playerControlsView setTotalTime:totalTime]; + [_playerControlsView updateScrubberAndTime]; +} + +- (void)setDownloadedTime:(NSTimeInterval)downloadedTime { + [_playerControlsView setDownloadedTime:downloadedTime]; + [_playerControlsView updateScrubberAndTime]; +} + +- (void)setMediaTime:(NSTimeInterval)mediaTime { + [_playerControlsView setMediaTime:mediaTime]; + [_playerControlsView updateScrubberAndTime]; +} + +- (void)setSeekbarTrackColor:(UIColor *)color { + [_playerControlsView setSeekbarTrackColor:color]; +} + +- (void)setSeekbarTrackColorDefault { + // Light blue + [_playerControlsView setSeekbarTrackColor:[UIColor colorWithRed:0.08235294117 + green:0.49411764705 + blue:0.98431372549 + alpha:1.0]]; +} + +- (void)disableSeekbarInteraction { + [_playerControlsView disableSeekbarInteraction]; +} + +- (void)enableSeekbarInteraction { + [_playerControlsView enableSeekbarInteraction]; +} + +// Check if the tap is over the subviews of the overlay, else let it go to handle taps +// in the aboveRenderingView +-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { + UIView *hitView = [super hitTest:point withEvent:event]; + if (hitView == self) { + return nil; + } + return hitView; +} + +@end + diff --git a/GoogleMediaFramework/GMFPlayerOverlayViewController.h b/GoogleMediaFramework/GMFPlayerOverlayViewController.h new file mode 100644 index 0000000..8b68440 --- /dev/null +++ b/GoogleMediaFramework/GMFPlayerOverlayViewController.h @@ -0,0 +1,63 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import +#import "GMFPlayerOverlayView.h" +#import "GMFPlayerState.h" +#import "GMFPlayerOverlayView.h" + +@protocol GMFPlayerOverlayViewControllerDelegate +@optional +- (void)playerControlsWillShow; +- (void)playerControlsDidShow; +- (void)playerControlsWillHide; +- (void)playerControlsDidHide; +@end + +@interface GMFPlayerOverlayViewController : UIViewController { + @private + GMFPlayerOverlayView *_playerOverlayView; + __weak NSObject *_delegate; + GMFPlayerState _playerState; + BOOL _autoHideEnabled; + BOOL _playerControlsHidden; +} + +@property(nonatomic, weak) id + videoPlayerOverlayViewControllerDelegate; + +- (void)setDelegate:(NSObject *)delegate; + +- (void)playerStateDidChangeToState:(GMFPlayerState)toState; + +- (void)showPlayerControlsAnimated:(BOOL)animated; +- (void)hidePlayerControlsAnimated:(BOOL)animated; + +- (void)playerControlsDidHide; +- (void)playerControlsWillHide; +- (void)playerControlsDidShow; +- (void)playerControlsWillShow; + +- (void)setTotalTime:(NSTimeInterval)totalTime; +- (void)setMediaTime:(NSTimeInterval)mediaTime; + +- (GMFPlayerOverlayView *)playerOverlayView; + +- (GMFPlayerControlsView *)playerControlsView; + +- (void)togglePlayerControlsVisibility; + +- (void)reset; + +@end diff --git a/GoogleMediaFramework/GMFPlayerOverlayViewController.m b/GoogleMediaFramework/GMFPlayerOverlayViewController.m new file mode 100644 index 0000000..dcba9e0 --- /dev/null +++ b/GoogleMediaFramework/GMFPlayerOverlayViewController.m @@ -0,0 +1,241 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "GMFPlayerOverlayView.h" +#import "GMFPlayerOverlayViewController.h" + +static const NSInteger kPaddingTop = 60; +static const NSTimeInterval kAutoHideUserForcedAnimationDuration = 0.2; +static const NSTimeInterval kAutoHideFadeAnimationDuration = 0.7; +static const NSTimeInterval kAutoHideAnimationDelay = 4.0; + +@interface GMFPlayerOverlayViewController () + +@end + +@implementation GMFPlayerOverlayViewController + +// TODO(tensafefrogs): Figure out a nice way to display playback errors here. +- (id)init { + self = [super init]; + return self; +} + +- (void)loadView { + CGRect frameRect = CGRectMake(0, + kPaddingTop, + _playerOverlayView.frame.size.width, + _playerOverlayView.frame.size.height); + _playerOverlayView = [[GMFPlayerOverlayView alloc] initWithFrame:frameRect]; + [self setView:_playerOverlayView]; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + [_playerOverlayView setDelegate:_delegate]; + [_playerOverlayView showSpinner]; + [self playerStateDidChangeToState:_playerState]; +} + +- (void)setDelegate:(NSObject *)delegate { + // Store delegate in case the view isn't loaded yet. + _delegate = delegate; + [_playerOverlayView setDelegate:delegate]; +} + +- (GMFPlayerControlsView *)playerControlsView { + return _playerOverlayView.playerControlsView; +} + +- (void)playerStateDidChangeToState:(GMFPlayerState)toState { + _playerState = toState; + [self updatePlayerBarViewButtonWithState:toState]; + switch (toState) { + case kGMFPlayerStateEmpty: + break; + case kGMFPlayerStatePaused: + case kGMFPlayerStatePlaying: + case kGMFPlayerStateFinished: + case kGMFPlayerStateError: + [_playerOverlayView hideSpinner]; + break; + case kGMFPlayerStateLoadingContent: + case kGMFPlayerStateReadyToPlay: + case kGMFPlayerStateBuffering: + case kGMFPlayerStateSeeking: + [_playerOverlayView showSpinner]; + break; + } + + if (toState == kGMFPlayerStateReadyToPlay + || toState == kGMFPlayerStateError + || toState == kGMFPlayerStateFinished) { + [self showPlayerControlsAnimated:YES]; + } else { + [self updatePlayerControlsVisibility]; + } +} + +- (void)setTotalTime:(NSTimeInterval)totalTime { + [_playerOverlayView setTotalTime:totalTime]; +} + +- (void)setMediaTime:(NSTimeInterval)mediaTime { + [_playerOverlayView setMediaTime:mediaTime]; +} + +- (void)updatePlayerControlsVisibility { + if (!_playerControlsHidden) { + [self showPlayerControlsAnimated:YES]; + } else { + [self hidePlayerControlsAnimated:YES]; + } +} + +- (void)showPlayerControlsAnimated:(BOOL)animated { + if (animated) { + [self animatePlayerControlsToHidden:NO + animationDuration:kAutoHideUserForcedAnimationDuration + afterDelay:0]; + } else { + [self playerControlsWillShow]; + [self playerControlsDidShow]; + } + if (_autoHideEnabled) { + [self animatePlayerControlsToHidden:YES + animationDuration:kAutoHideFadeAnimationDuration + afterDelay:kAutoHideAnimationDelay]; + } +} + +- (void)hidePlayerControlsAnimated:(BOOL)animated { + [self animatePlayerControlsToHidden:animated + animationDuration:kAutoHideUserForcedAnimationDuration + afterDelay:0]; +} + +- (void)playerControlsDidHide { + // Override in a subclass to be notified when _autoHideView is hidden. + [_videoPlayerOverlayViewControllerDelegate playerControlsDidHide]; +} + +- (void)playerControlsWillHide { + // Override in a subclass to be notified when _autoHideView starts hiding. + [_playerOverlayView setPlayerBarVisible:NO]; + _playerControlsHidden = YES; +} + +- (void)playerControlsDidShow { + // Override in a subclass to be notified when _autoHideView is shown. + [_videoPlayerOverlayViewControllerDelegate playerControlsDidShow]; +} + +- (void)playerControlsWillShow { + // Override in a subclass to be notified when _autoHideView starts showing. + [_playerOverlayView setPlayerBarVisible:YES]; + _playerControlsHidden = NO; +} + +- (void)togglePlayerControlsVisibility { + // Hide/show the autoHideView as appropriate. + if (_playerControlsHidden) { + [self showPlayerControlsAnimated:YES]; + } else { + [self hidePlayerControlsAnimated:YES]; + } +} + +#pragma mark Private Methods + +- (GMFPlayerOverlayView *)playerOverlayView { + return (GMFPlayerOverlayView *)[self view]; +} + +- (void)animatePlayerControlsToHidden:(BOOL)hidden + animationDuration:(NSTimeInterval)duration + afterDelay:(NSTimeInterval)delay { + // If we animate before the view is loaded, + // then the first call to layoutSubviews may be animated. + if (![self isViewLoaded]) { + return; + } + void (^animateAutoHideViewBlock)(void) = ^(void) { + [UIView animateWithDuration:duration + delay:0 + options:UIViewAnimationOptionBeginFromCurrentState | + UIViewAnimationOptionAllowUserInteraction + animations:^{ + if (hidden) { + [self playerControlsWillHide]; + } else { + [self playerControlsWillShow]; + } + } + completion:^(BOOL finished) { + if (finished) { + if (hidden) { + [self playerControlsDidHide]; + } else { + [self playerControlsDidShow]; + } + } + }]; + }; + + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + if (delay) { + [self performSelector:@selector(performVoidBlock:) + withObject:animateAutoHideViewBlock + afterDelay:delay]; + } else { + animateAutoHideViewBlock(); + } +} + +// Simply executes the block passed to it. +// Used to execute a block after a delay using performSelector. +- (void)performVoidBlock:(void (^)(void))block { + block(); +} + +- (void)updatePlayerBarViewButtonWithState:(GMFPlayerState)playerState { + switch (playerState) { + case kGMFPlayerStateEmpty: + case kGMFPlayerStateReadyToPlay: + case kGMFPlayerStatePaused: + [[self playerOverlayView] showPlayButton]; + break; + case kGMFPlayerStatePlaying: + [[self playerOverlayView] showPauseButton]; + break; + case kGMFPlayerStateFinished: + [[self playerOverlayView] showReplayButton]; + break; + default: + break; + } +} + +- (void)reset { + [self setTotalTime:0.0]; + [self setMediaTime:0.0]; + [self playerStateDidChangeToState:kGMFPlayerStateEmpty]; +} + +@end + diff --git a/GoogleMediaFramework/GMFPlayerState.h b/GoogleMediaFramework/GMFPlayerState.h new file mode 100644 index 0000000..364ac76 --- /dev/null +++ b/GoogleMediaFramework/GMFPlayerState.h @@ -0,0 +1,25 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +typedef enum { + kGMFPlayerStateEmpty = 0, + kGMFPlayerStateLoadingContent, + kGMFPlayerStateReadyToPlay, + kGMFPlayerStatePlaying, + kGMFPlayerStatePaused, + kGMFPlayerStateBuffering, + kGMFPlayerStateSeeking, + kGMFPlayerStateFinished, + kGMFPlayerStateError +} GMFPlayerState; diff --git a/GoogleMediaFramework/GMFPlayerView.h b/GoogleMediaFramework/GMFPlayerView.h new file mode 100644 index 0000000..1282b83 --- /dev/null +++ b/GoogleMediaFramework/GMFPlayerView.h @@ -0,0 +1,43 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "GMFPlayerOverlayView.h" + +@interface GMFPlayerView : UIView { + @private + GMFPlayerOverlayView *_overlayView; +} + +@property(nonatomic, weak) UIView *aboveRenderingView; +@property(nonatomic, strong) UIView *renderingView; + +// Handles capturing various gestures (taps, swipes, whatever else) and forwards the events to the +// overlayView. This allows 3rd party views set as the aboveRenderingView to capture tap events +// along side the player controls, and also handle taps on the video surface. +@property(nonatomic, readonly) UIView *gestureCapturingView; + +- (id)init; + +- (void)reset; + +- (void)setVideoRenderingView:(UIView *)renderingView; + +- (void)setAboveRenderingView:(UIView *)aboveRenderingView; + +- (void)setOverlayView:(GMFPlayerOverlayView *)overlayView; + +@end + diff --git a/GoogleMediaFramework/GMFPlayerView.m b/GoogleMediaFramework/GMFPlayerView.m new file mode 100644 index 0000000..c95efc9 --- /dev/null +++ b/GoogleMediaFramework/GMFPlayerView.m @@ -0,0 +1,86 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "GMFPlayerView.h" + +// Contains and manages the layout of the various subviews of the video player. +@implementation GMFPlayerView + +- (id)init { + self = [super initWithFrame:CGRectZero]; + if (self) { + [self createAndAddGestureCapturingView]; + [self setBackgroundColor:[UIColor blackColor]]; + //[self setUserInteractionEnabled:YES]; + } + return self; +} + +- (id)initWithFrame:(CGRect)frame { + NSAssert(false, @"initWithFrame not available, use init."); + return nil; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + CGRect bounds = [self bounds]; + [_gestureCapturingView setFrame:bounds]; + [_renderingView setFrame:bounds]; + [_aboveRenderingView setFrame:bounds]; + [_overlayView setFrame:bounds]; +} + +- (void)createAndAddGestureCapturingView { + _gestureCapturingView = [[UIView alloc] init]; + [self insertSubview:_gestureCapturingView atIndex:0]; +} + +- (void)setVideoRenderingView:(UIView *)renderingView { + [_renderingView removeFromSuperview]; + _renderingView = renderingView; + if (_renderingView) { + // Let taps fall through to |_aboveRenderingView| or |_gestureCapturingView|. + [_renderingView setUserInteractionEnabled:NO]; + [self insertSubview:_renderingView aboveSubview:_gestureCapturingView]; + } + [self setNeedsLayout]; +} + +- (void)setOverlayView:(GMFPlayerOverlayView *)overlayView { + [_overlayView removeFromSuperview]; + _overlayView = overlayView; + if (_overlayView) { + // The _overlayView should always be the top view. + [self addSubview:_overlayView]; + } +} + +- (void)setAboveRenderingView:(UIView *)aboveRenderingView { + [_aboveRenderingView removeFromSuperview]; + _aboveRenderingView = aboveRenderingView; + if (_aboveRenderingView) { + [self insertSubview:_aboveRenderingView belowSubview:_overlayView]; + } +} + +- (void)reset { + [self setVideoRenderingView:nil]; +} + +@end + diff --git a/GoogleMediaFramework/GMFPlayerViewController.h b/GoogleMediaFramework/GMFPlayerViewController.h new file mode 100644 index 0000000..0da57db --- /dev/null +++ b/GoogleMediaFramework/GMFPlayerViewController.h @@ -0,0 +1,72 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "GMFPlayerView.h" +#import "GMFVideoPlayer.h" +#import "GMFPlayerOverlayViewController.h" + +@class GMFAdService; +@class GMFPlayerControlsViewDelegate; + +extern NSString * const kGMFPlayerCurrentMediaTimeDidChangeNotification; +extern NSString * const kGMFPlayerDidMinimizeNotification; +extern NSString * const kGMFPlayerPlaybackStateDidChangeNotification; +extern NSString * const kGMFPlayerStateDidChangeToFinishedNotification; +extern NSString * const kGMFPlayerStateWillChangeToFinishedNotification; + +extern NSString * const kGMFPlayerPlaybackDidFinishReasonUserInfoKey; +extern NSString * const kGMFPlayerPlaybackWillFinishReasonUserInfoKey; + +@interface GMFPlayerViewController : UIViewController { + @private + UITapGestureRecognizer *_tapRecognizer; +} + +@property(nonatomic, readonly) GMFPlayerView *playerView; + +@property(nonatomic, weak) GMFAdService *adService; + +@property(nonatomic, readonly, getter=isVideoFinished) BOOL videoFinished; + +- (id)init; + +- (void)loadStreamWithURL:(NSURL *)URL; + +- (void)play; + +- (void)pause; + +- (GMFPlayerState)playbackState; + +- (NSTimeInterval)currentMediaTime; + +#pragma mark Advanced controls + +- (void)registerAdService:(GMFAdService *)adService; + +- (void)setABoveRenderingView:(UIView *)view; + +- (void)setControlsVisibility:(BOOL)visibile animated:(BOOL)animated; + +- (void)setVideoPlayerOverlayDelegate:(id)delegate; + +- (void)setDefaultVideoPlayerOverlayDelegate; + +- (GMFPlayerOverlayView *)playerOverlayView; + +@end + diff --git a/GoogleMediaFramework/GMFPlayerViewController.m b/GoogleMediaFramework/GMFPlayerViewController.m new file mode 100644 index 0000000..3852a65 --- /dev/null +++ b/GoogleMediaFramework/GMFPlayerViewController.m @@ -0,0 +1,358 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "GMFPlayerFinishReason.h" +#import "GMFPlayerViewController.h" +#import "GMFPlayerOverlayViewController.h" + +NSString * const kGMFPlayerCurrentMediaTimeDidChangeNotification = + @"kGMFPlayerCurrentMediaTimeDidChangeNotification"; +NSString * const kGMFPlayerPlaybackStateDidChangeNotification = + @"kGMFPlayerPlaybackStateDidChangeNotification"; +NSString * const kGMFPlayerStateDidChangeToFinishedNotification = + @"kGMFPlayerStateDidChangeToFinishedNotification"; +NSString * const kGMFPlayerStateWillChangeToFinishedNotification = + @"kGMFPlayerStateWillChangeToFinishedNotification"; + +NSString * const kGMFPlayerPlaybackDidFinishReasonUserInfoKey = + @"kGMFPlayerPlaybackDidFinishReasonUserInfoKey"; +NSString * const kGMFPlayerPlaybackWillFinishReasonUserInfoKey = + @"kGMFPlayerPlaybackWillFinishReasonUserInfoKey"; + +@interface GMFPlayerViewController () + +@property(nonatomic, strong) GMFVideoPlayer *player; + +@end + +@implementation GMFPlayerViewController { + GMFPlayerView *_playerView; + NSURL *_currentMediaURL; + GMFPlayerOverlayViewController *_videoPlayerOverlayViewController; + + BOOL _isUserScrubbing; + BOOL _wasPlayingBeforeSeeking; +} + +// Perhaps you'd like to init a player with no content? +- (id)init { + self = [super init]; + if (self) { + if (!_player) { + _player = [[GMFVideoPlayer alloc] init]; + [_player setDelegate:self]; + } + } + return self; +} + +- (void)setControlsVisibility:(BOOL)visibile animated:(BOOL)animated { + if (visibile) { + [_videoPlayerOverlayViewController showPlayerControlsAnimated:animated]; + } else { + [_videoPlayerOverlayViewController hidePlayerControlsAnimated:animated]; + } +} + +- (void)loadStreamWithURL:(NSURL *)URL { + [_player loadStreamWithURL:URL]; +} + +- (void)play { + [_player play]; +} + +- (void)pause { + [_player pause]; +} + +- (void)setABoveRenderingView:(UIView *)view { + [_playerView setAboveRenderingView:view]; +} + +- (void)registerAdService:(GMFAdService *)adService { + _adService = adService; +} + +// Allows outside classes take over or act as proxies for the video player controls. +- (void)setVideoPlayerOverlayDelegate:(id)delegate { + [_videoPlayerOverlayViewController setDelegate:delegate]; +} + +- (void)setDefaultVideoPlayerOverlayDelegate { + // Duration was probably changed by whatever delegate took over, so reset it here. + [_videoPlayerOverlayViewController setTotalTime:[_player totalMediaTime]]; + [_videoPlayerOverlayViewController setMediaTime:[_player currentMediaTime]]; + [_videoPlayerOverlayViewController setDelegate:self]; +} + +- (void)loadView { + _playerView = [[GMFPlayerView alloc] init]; + [self setView:_playerView]; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + if ([self respondsToSelector:@selector(setNeedsStatusBarAppearanceUpdate)]) { + [self prefersStatusBarHidden]; + [self performSelector:@selector(setNeedsStatusBarAppearanceUpdate)]; + } else { + // iOS 6 + [[UIApplication sharedApplication] + setStatusBarHidden:YES withAnimation:UIStatusBarAnimationSlide]; + } + + // Listen to tap events that fall through the overlay views + _tapRecognizer = [[UITapGestureRecognizer alloc] + initWithTarget:self + action:@selector(didTapGestureCapturingView:)]; + [_tapRecognizer setDelegate:self]; + [_playerView.gestureCapturingView addGestureRecognizer:_tapRecognizer]; + + _videoPlayerOverlayViewController = [[GMFPlayerOverlayViewController alloc] init]; + [_playerView setOverlayView:[_videoPlayerOverlayViewController playerOverlayView]]; + [self setDefaultVideoPlayerOverlayDelegate]; +} + +- (void)didTapGestureCapturingView:(UITapGestureRecognizer *)recognizer { + [_videoPlayerOverlayViewController togglePlayerControlsVisibility]; +} + +- (BOOL)prefersStatusBarHidden { + return YES; +} + +- (void)restartPlayback { + [_player replay]; +} + +- (GMFVideoPlayer *)videoPlayer { + return _player; +} + +- (GMFPlayerOverlayView *)playerOverlayView { + return [_videoPlayerOverlayViewController playerOverlayView]; +} + +- (GMFPlayerState)playbackState { + return _player.state; +} + +- (NSTimeInterval)currentMediaTime { + return _player.currentMediaTime; +} + +#pragma mark Player State Change Handlers + +- (void)playerStateDidChangeToReadyToPlay { + [_playerView setVideoRenderingView:[_player renderingView]]; + // TODO(tensafefrogs): Don't set this if an ad is playing / player doesn't have control delegate + [_videoPlayerOverlayViewController setTotalTime:[_player totalMediaTime]]; +} + +- (void)playerStateDidChangeToPlaying { + NSLog(@"State changed to playing"); +} + +- (void)playerStateDidChangeToPaused { + NSLog(@"State changed to paused"); +} + +// Broadcast just before the player ends to allow any ads or other provider that wants to perform +// an action before playback ends. +- (void)playerStateWillChangeToFinished { + NSDictionary *userInfo = @{ + kGMFPlayerPlaybackWillFinishReasonUserInfoKey: + [NSNumber numberWithInt:GMFPlayerFinishReasonPlaybackEnded] + }; + [[NSNotificationCenter defaultCenter] + postNotificationName:kGMFPlayerStateWillChangeToFinishedNotification + object:self + userInfo:userInfo]; +} + +- (void)playerStateDidChangeToFinished { + NSLog(@"State changed to finished"); + NSDictionary *userInfo = @{ + kGMFPlayerPlaybackDidFinishReasonUserInfoKey: + [NSNumber numberWithInt:GMFPlayerFinishReasonPlaybackEnded] + }; + [[NSNotificationCenter defaultCenter] + postNotificationName:kGMFPlayerStateDidChangeToFinishedNotification + object:self + userInfo:userInfo]; +} + +#pragma mark GMFVideoPlayer protocol handlers + +- (void)videoPlayer:(GMFVideoPlayer *)videoPlayer + stateDidChangeFrom:(GMFPlayerState)fromState + to:(GMFPlayerState)toState { + [_videoPlayerOverlayViewController playerStateDidChangeToState:toState]; + switch (toState) { + case kGMFPlayerStateReadyToPlay: + [self playerStateDidChangeToReadyToPlay]; + break; + case kGMFPlayerStatePlaying: + [self playerStateDidChangeToPlaying]; + break; + case kGMFPlayerStatePaused: + [self playerStateDidChangeToPaused]; + break; + case kGMFPlayerStateFinished: + // Allow any ads provider to play any post rolls before we actually finish + [self playerStateWillChangeToFinished]; + [self playerStateDidChangeToFinished]; + break; + case kGMFPlayerStateBuffering: + NSLog(@"BUFFERING"); + // Video is buffering + break; + case kGMFPlayerStateSeeking: + NSLog(@"SEEKING"); + // Seeking + break; + case kGMFPlayerStateLoadingContent: + NSLog(@"LOADING_CONTENT"); + // Loading content + break; + case kGMFPlayerStateEmpty: + NSLog(@"EMPTY"); + // ??? + break; + case kGMFPlayerStateError: + NSLog(@"ERROR"); + // Do something with error state. + break; + } + [[NSNotificationCenter defaultCenter] + postNotificationName:kGMFPlayerPlaybackStateDidChangeNotification + object:self]; +} + +- (void)videoPlayer:(GMFVideoPlayer *)videoPlayer + currentMediaTimeDidChangeToTime:(NSTimeInterval)time { + [_videoPlayerOverlayViewController setMediaTime:time]; + [self notifyCurrentMediaTimeDidChange]; +} + +- (void)videoPlayer:(GMFVideoPlayer *)videoPlayer + bufferedMediaTimeDidChangeToTime:(NSTimeInterval)time { + NSLog(@"Buffered media time: %f", time); +} + +#pragma mark YTPlayerOverlayViewDelegate + +- (void)didPressPlay { + [_player play]; +} + +- (void)didPressPause { + [_player pause]; +} + +- (void)didSeekToTime:(NSTimeInterval)seekTime { + [_player seekToTime:seekTime]; + if (_wasPlayingBeforeSeeking) { + [_player play]; + } +} + +- (void)didStartScrubbing { + // We don't want to override this flag if we're in the middle of another + // seek. + if ([_player state] != kGMFPlayerStateSeeking) { + GMFPlayerState playerState = [_player state]; + _wasPlayingBeforeSeeking = playerState == kGMFPlayerStatePlaying || + playerState == kGMFPlayerStateBuffering; + } + _isUserScrubbing = YES; + [_player pause]; +} + +- (void)didEndScrubbing { + _isUserScrubbing = NO; +} + +- (void)didPressReplay { + if ([_player state] == kGMFPlayerStateFinished) { + [_player replay]; + } +} + +- (void)didPressMinimize { + // Notify first to give observers a chance to remove themselves. + [self notifyUserWillMinimize]; + [self notifyUserDidMinimize]; + [self resetPlayerAndPlayerView]; +} + +- (void)notifyUserWillMinimize { + NSDictionary *userInfo = @{ + kGMFPlayerPlaybackWillFinishReasonUserInfoKey: + [NSNumber numberWithInt:GMFPlayerFinishReasonUserExited] + }; + [[NSNotificationCenter defaultCenter] + postNotificationName:kGMFPlayerStateWillChangeToFinishedNotification + object:self + userInfo:userInfo]; +} + +// Notifies a listener that the user minimized the video player by tapping the minimize button. +- (void)notifyUserDidMinimize { + NSDictionary *userInfo = @{ + kGMFPlayerPlaybackDidFinishReasonUserInfoKey: + [NSNumber numberWithInt:GMFPlayerFinishReasonUserExited] + }; + [[NSNotificationCenter defaultCenter] + postNotificationName:kGMFPlayerStateDidChangeToFinishedNotification + object:self + userInfo:userInfo]; +} + +// Notifies a listener that the curent media time has changed. The listener is expected to check +// GMFVideoPlayerViewController.currentMediaTime to get the new value. Only dispatches a +// notification when the value changes, not on a set time interval. +// TODO(tensafefrogs): Include the new value in the userInfo dictionary with this notification. +- (void)notifyCurrentMediaTimeDidChange { + [[NSNotificationCenter defaultCenter] + postNotificationName:kGMFPlayerCurrentMediaTimeDidChangeNotification + object:self + userInfo:nil]; +} + +#pragma mark - + +// Reset these together, else playerView might retain a reference to the player's renderingView. +- (void)resetPlayerAndPlayerView { + [_videoPlayerOverlayViewController reset]; + [_playerView reset]; + [_player reset]; +} + +// View was removed, clear player and notify observers. +- (void)dealloc { + // Call this first to give things a chance to remove observers. + [self notifyUserDidMinimize]; + [self resetPlayerAndPlayerView]; + + [_tapRecognizer setDelegate:nil]; + [_tapRecognizer removeTarget:self action:@selector(didTapGestureCapturingView:)]; +} + +@end diff --git a/GoogleMediaFramework/GMFResources.h b/GoogleMediaFramework/GMFResources.h new file mode 100644 index 0000000..eb2924b --- /dev/null +++ b/GoogleMediaFramework/GMFResources.h @@ -0,0 +1,29 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import +#import + +@interface GMFResources : NSObject + ++ (UIImage *)playerBarPlayButtonImage; ++ (UIImage *)playerBarPauseButtonImage; ++ (UIImage *)playerBarReplayButtonImage; ++ (UIImage *)playerBarMinimizeButtonImage; ++ (UIImage *)playerBarMaximizeButtonImage; ++ (UIImage *)playerBarScrubberThumbImage; ++ (UIImage *)playerBarBackgroundImage; + +@end + diff --git a/GoogleMediaFramework/GMFResources.m b/GoogleMediaFramework/GMFResources.m new file mode 100644 index 0000000..1babad2 --- /dev/null +++ b/GoogleMediaFramework/GMFResources.m @@ -0,0 +1,74 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "GMFResources.h" +#import "GMFVideoPlayer.h" + +@implementation GMFResources + ++ (UIImage *)playerBarPlayButtonImage { + return [self imageNamed:@"player_control_play"]; +} + ++ (UIImage *)playerBarPauseButtonImage { + return [self imageNamed:@"player_control_pause"]; +} + ++ (UIImage *)playerBarReplayButtonImage { + return [self imageNamed:@"player_control_replay"]; +} + ++ (UIImage *)playerBarMaximizeButtonImage { + return [self imageNamed:@"player_control_maximize"]; +} + ++ (UIImage *)playerBarMinimizeButtonImage { + return [self imageNamed:@"player_control_minimize"]; +} + ++ (UIImage *)playerBarScrubberThumbImage { + return [self imageNamed:@"player_scrubber_thumb"]; +} + ++ (UIImage *)playerBarBackgroundImage { + return [self imageNamed:@"player_controls_background"]; +} + +#pragma mark Private Methods + ++ (UIImage *)imageNamed:(NSString *)name + stretchable:(BOOL)stretchable { + UIImage *image = [UIImage imageNamed:name]; + + NSAssert(image, @"There is no image called %@", name); + if (stretchable) { + // Stretching the image by using a center cap. + CGSize size = [image size]; + return [image stretchableImageWithLeftCapWidth:size.width / 2.0 + topCapHeight:size.height / 2.0]; + } else { + return image; + } +} + ++ (UIImage *)imageNamed:(NSString *)name { + return [self imageNamed:name stretchable:NO]; +} + +@end + diff --git a/GoogleMediaFramework/GMFVideoPlayer.h b/GoogleMediaFramework/GMFVideoPlayer.h new file mode 100644 index 0000000..81dab23 --- /dev/null +++ b/GoogleMediaFramework/GMFVideoPlayer.h @@ -0,0 +1,72 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import +#import + +#import "GMFPlayerState.h" + +@class GMFVideoPlayer; + +@protocol GMFVideoPlayerDelegate + +- (void)videoPlayer:(GMFVideoPlayer *)videoPlayer + stateDidChangeFrom:(GMFPlayerState)fromState + to:(GMFPlayerState)toState; + +// Called whenever media time changes during playback. +- (void)videoPlayer:(GMFVideoPlayer *)videoPlayer + currentMediaTimeDidChangeToTime:(NSTimeInterval)time; + +@optional +// Called whenever buffered media time changes during playback or while loading or paused. +- (void)videoPlayer:(GMFVideoPlayer *)videoPlayer + bufferedMediaTimeDidChangeToTime:(NSTimeInterval)time; + +@end + +// Handles video playback via AVPlayer classes and AVPlayerItem management. Provides a simple API +// to control playback of media content. +@interface GMFVideoPlayer : NSObject + +@property(nonatomic, weak) id delegate; + +@property(nonatomic, readonly) GMFPlayerState state; + +// |renderingView| will only be set after the player enters the ready to play state. After calling +// |reset|, the player discards any previously set rendering view, so if +// you maintain a separate reference to this rendering view, it will no longer be valid for the +// current playback. +@property(nonatomic, readonly) UIView *renderingView; + +// Public method to play media via url. +- (void)loadStreamWithURL:(NSURL* )url; + +// Reset the playback state to enable playing a new video in an existing player instance. +- (void)reset; + +// Handling playback. +- (void)play; +- (void)pause; +- (void)replay; +- (void)seekToTime:(NSTimeInterval)time; + +// Querying the player. +- (NSTimeInterval)currentMediaTime; +- (NSTimeInterval)totalMediaTime; +- (NSTimeInterval)bufferedMediaTime; + +@end + + diff --git a/GoogleMediaFramework/GMFVideoPlayer.m b/GoogleMediaFramework/GMFVideoPlayer.m new file mode 100644 index 0000000..d43661b --- /dev/null +++ b/GoogleMediaFramework/GMFVideoPlayer.m @@ -0,0 +1,495 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "GMFVideoPlayer.h" + +static const NSTimeInterval kGMFPollingInterval = 0.2; + +static void *kGMFPlayerItemStatusContext = &kGMFPlayerItemStatusContext; +static void *kGMFPlayerRateContext = &kGMFPlayerRateContext; + +static NSString * const kStatusKey = @"status"; +static NSString * const kRateKey = @"rate"; + +// Pause the video if user unplugs their headphones. +void GMFAudioRouteChangeListenerCallback(void *inClientData, + AudioSessionPropertyID inID, + UInt32 inDataSize, + const void *inData) { + NSDictionary *routeChangeDictionary = (__bridge NSDictionary *)inData; + NSString *reasonKey = + [NSString stringWithCString:kAudioSession_AudioRouteChangeKey_Reason + encoding:NSASCIIStringEncoding]; + UInt32 reasonCode = 0; + [[routeChangeDictionary objectForKey:reasonKey] getValue:&reasonCode]; + if (reasonCode == kAudioSessionRouteChangeReason_OldDeviceUnavailable) { + // If the user removed the headphones, pause the playback. + GMFVideoPlayer *_player = (__bridge GMFVideoPlayer *)inClientData; + [_player pause]; + } +} + +#pragma mark - +#pragma mark GMFPlayerLayerView + +// GMFPlayerLayerView is a UIView that uses an AVPlayerLayer instead of CGLayer. +@interface GMFPlayerLayerView : UIView + +// Returns an instance of GMFPlayerLayerView for rendering the video content in. +- (AVPlayerLayer *)playerLayer; + +@end + +@implementation GMFPlayerLayerView + ++ (Class)layerClass { + return [AVPlayerLayer class]; +} + +- (AVPlayerLayer *)playerLayer { + return (AVPlayerLayer *)[self layer]; +} + +@end + +#pragma mark GMFVideoPlayer + +@interface GMFVideoPlayer () { + GMFPlayerLayerView *_renderingView; +} + +@property (nonatomic, strong) AVPlayerItem *playerItem; + +@property (nonatomic, strong) AVPlayer *player; + +// Polling timer for content time updates. +@property (nonatomic, strong) NSTimer *playbackStatusPoller; + +// Track content time updates so we know when playback stalls or is paused/playing. +@property (nonatomic, assign) NSTimeInterval lastReportedPlaybackTime; + +@property (nonatomic, assign) NSTimeInterval lastReportedBufferTime; + +// Allow |[_player play]| to be called before content finishes loading. +@property (nonatomic, assign) BOOL pendingPlay; + +// Set when pause is invoked and cleared when player enters the playing state. +// This is used to determine, when resuming from an audio interruption such as +// a phone call, whether the player should be resumed or it should stay in a +// pause state. +@property (nonatomic, assign) BOOL manuallyPaused; + +// Creates an AVPlayerItem and AVPlayer instance when preparing to play a new content URL. +- (void)handlePlayableAsset:(AVAsset *)asset; + +// Updates the current |playerItem| and |player| and removes and re-adds observers. +- (void)setAndObservePlayerItem:(AVPlayerItem *)playerItem player:(AVPlayer *)player; + +// Updates the internal player state and notifies the delegate. +- (void)setState:(GMFPlayerState)state; + +// Starts a polling timer to track content playback state and time. +- (void)startPlaybackStatusPoller; + +// Resets the above polling timer. +- (void)stopPlaybackStatusPoller; + +// Handles audio session changes, such as when a user unplugs headphones. +- (void)onAudioSessionInterruption:(NSNotification *)notification; + +// Handler for |playerItem| state changes. +- (void)playerItemStatusDidChange; + +// Reset the player state. Readies the player to play a new content URL. +- (void)clearPlayer; + +// Resets the player to its default state. +- (void)reset; + +@end + +@implementation GMFVideoPlayer + +// AVPlayerLayer class for video rendering. +@synthesize renderingView = _renderingView; + +- (instancetype)init { + self = [super init]; + if (self) { + _state = kGMFPlayerStateEmpty; + AudioSessionAddPropertyListener(kAudioSessionProperty_AudioRouteChange, + GMFAudioRouteChangeListenerCallback, + (__bridge void *)self); + + // Handles interruptions to playback, like phone calls and activating Siri. + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(onAudioSessionInterruption:) + name:AVAudioSessionInterruptionNotification + object:[AVAudioSession sharedInstance]]; + } + return self; +} + +#pragma mark Public playback methods + +- (void)play { + _manuallyPaused = NO; + if (_state == kGMFPlayerStateLoadingContent || _state == kGMFPlayerStateSeeking) { + _pendingPlay = YES; + } else if (![_player rate]) { + _pendingPlay = YES; + [_player play]; + } +} + +- (void)pause { + _pendingPlay = NO; + _manuallyPaused = YES; + if (_state == kGMFPlayerStatePlaying || + _state == kGMFPlayerStateBuffering || + _state == kGMFPlayerStateSeeking) { + [_player pause]; + // Setting paused state here rather than KVO observing, since the |rate| + // value can change to 0 because of buffer issues too. + [self setState:kGMFPlayerStatePaused]; + } +} + +- (void)replay { + _pendingPlay = YES; + [self seekToTime:0.0]; +} + +- (void)seekToTime:(NSTimeInterval)time { + if ([_playerItem status] != AVPlayerItemStatusReadyToPlay) { + // Calling [AVPlayerItem seekToTime:] before it is in the "ready to play" state + // causes a crash. + // TODO(tensafefrogs): Dev assert here instead of silent return. + return; + } + if (![self isLive]) { + time = MIN(MAX(time, 0), [self totalMediaTime]); + } else { + time = MAX(time, 0); + } + [self setState:kGMFPlayerStateSeeking]; + __weak GMFVideoPlayer *weakSelf = self; + [_playerItem seekToTime:CMTimeMake(time, 1) + completionHandler:^(BOOL finished) { + GMFVideoPlayer *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + if (finished) { + if ([strongSelf pendingPlay]) { + [strongSelf setPendingPlay:NO]; + [[strongSelf player] play]; + } else { + [strongSelf setState:kGMFPlayerStatePaused]; + } + } + }]; +} + +- (void)loadStreamWithURL:(NSURL *)URL { + NSLog(@"Playing URL %@", [URL absoluteString]); + [self setState:kGMFPlayerStateLoadingContent]; + AVAsset *asset = [AVAsset assetWithURL:URL]; + [self handlePlayableAsset:asset]; +} + +#pragma mark Querying Player for info + +- (NSTimeInterval)currentMediaTime { + return [self isPlayableState] ? + [GMFVideoPlayer secondsWithCMTime:[_playerItem currentTime]] : 0.0; +} + +- (NSTimeInterval)totalMediaTime { + // |_playerItem| duration is 0 if the video is a live stream. + return [self isPlayableState] ? [GMFVideoPlayer secondsWithCMTime:[_playerItem duration]] : 0.0; +} + +- (NSTimeInterval)bufferedMediaTime { + if ([self isPlayableState]) { + // Call |loadedTimeRanges| before storing |currentTime| so that the + // loaded time ranges don't change before we get |currentTime|. + // This can happen while video is playing. + NSArray *timeRanges = [_playerItem loadedTimeRanges]; + CMTime currentTime = [_playerItem currentTime]; + for (NSValue *timeRange in timeRanges) { + CMTimeRange range; + [timeRange getValue:&range]; + if (CMTimeRangeContainsTime(range, currentTime)) { + return [GMFVideoPlayer secondsWithCMTime:CMTimeRangeGetEnd(range)]; + } + } + } + return 0; +} + +- (BOOL)isLive { + // |totalMediaTime| is 0 if the video is a live stream. + // TODO(tensafefrogs): Is there a better way to determine if the video is live? + return [self totalMediaTime] == 0.0; +} + +#pragma mark Private methods + +// Once an asset is playable (i.e. tracks are loaded) hand it off to this method to add observers. +- (void)handlePlayableAsset:(AVAsset *)asset { + AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:asset]; + // Recreating the AVPlayer instance because of issues when playing HLS then non-HLS back to + // back, and vice-versa. + AVPlayer *player = [AVPlayer playerWithPlayerItem:playerItem]; + [self setAndObservePlayerItem:playerItem player:player]; +} + +- (void)setAndObservePlayerItem:(AVPlayerItem *)playerItem player:(AVPlayer *)player { + // Player item observers. + [_playerItem removeObserver:self forKeyPath:kStatusKey]; + [[NSNotificationCenter defaultCenter] removeObserver:self + name:AVPlayerItemDidPlayToEndTimeNotification + object:_playerItem]; + + _playerItem = playerItem; + if (_playerItem) { + [_playerItem addObserver:self + forKeyPath:kStatusKey + options:0 + context:kGMFPlayerItemStatusContext]; + + __weak GMFVideoPlayer *weakSelf = self; + [[NSNotificationCenter defaultCenter] + addObserverForName:AVPlayerItemDidPlayToEndTimeNotification + object:_playerItem + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification *note) { + GMFVideoPlayer *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + [strongSelf playbackDidReachEnd]; + }]; + } + + // Player observers. + [_player removeObserver:self forKeyPath:kRateKey]; + + _player = player; + if (_player) { + [_player addObserver:self + forKeyPath:kRateKey + options:0 + context:kGMFPlayerRateContext]; + _renderingView = [[GMFPlayerLayerView alloc] init]; + [[_renderingView playerLayer] setVideoGravity:AVLayerVideoGravityResizeAspect]; + [[_renderingView playerLayer] setBackgroundColor:[[UIColor blackColor] CGColor]]; + [[_renderingView playerLayer] setPlayer:_player]; + } else { + // It is faster to discard the rendering view and create a new one when + // necessary than to call setPlayer:nil and reuse it for future playbacks. + _renderingView = nil; + } +} + +- (void)setState:(GMFPlayerState)state { + if (state != _state) { + GMFPlayerState prevState = _state; + _state = state; + + // Call this last in case the delegate removes references/destroys self. + [_delegate videoPlayer:self stateDidChangeFrom:prevState to:state]; + } +} + +- (void)startPlaybackStatusPoller { + if (_playbackStatusPoller) { + return; + } + _playbackStatusPoller = [NSTimer + scheduledTimerWithTimeInterval:kGMFPollingInterval + target:self + selector:@selector(updateStateAndReportMediaTimes) + userInfo:nil + repeats:YES]; + // Ensure timer fires during UI events such as scrolling. + [[NSRunLoop currentRunLoop] addTimer:_playbackStatusPoller + forMode:NSRunLoopCommonModes]; +} + +- (void)stopPlaybackStatusPoller { + _lastReportedBufferTime = 0; + [_playbackStatusPoller invalidate]; + _playbackStatusPoller = nil; +} + +#pragma mark AVAudioSession notifications + +- (void)onAudioSessionInterruption:(NSNotification *)notification { + NSDictionary *userInfo = [notification userInfo]; + AVAudioSessionInterruptionType type = + [(NSNumber *)[userInfo valueForKey:AVAudioSessionInterruptionTypeKey] unsignedIntegerValue]; + NSUInteger flags = + [(NSNumber *)[userInfo valueForKey:AVAudioSessionInterruptionOptionKey] unsignedIntegerValue]; + // It seems like we don't receive the InterruptionTypeBegan + // event properly. This might be an iOS bug: + // http://openradar.appspot.com/12412685 + // + // So instead we try to detect if the player was manually paused by invoking + // pause, and only resume if the player was not manually paused. + if (type == AVAudioSessionInterruptionTypeEnded && + flags & AVAudioSessionInterruptionOptionShouldResume && + _state == kGMFPlayerStatePaused && + !_manuallyPaused) { + [self play]; + } +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + AudioSessionRemovePropertyListenerWithUserData(kAudioSessionProperty_AudioRouteChange, + GMFAudioRouteChangeListenerCallback, + (__bridge void *)self); + [self clearPlayer]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if (context == kGMFPlayerItemStatusContext) { + [self playerItemStatusDidChange]; + } else if (context == kGMFPlayerRateContext) { + [self playerRateDidChange]; + } else { + [super observeValueForKeyPath:keyPath + ofObject:object + change:change + context:context]; + } +} + +- (void)playerItemStatusDidChange { + if ([_playerItem status] == AVPlayerItemStatusReadyToPlay && + _state == kGMFPlayerStateLoadingContent) { + // TODO(tensafefrogs): It seems like additional AVPlayerItemStatusReadyToPlay + // events indicate HLS stream switching. Investigate. + [self setState:kGMFPlayerStateReadyToPlay]; + if (_pendingPlay) { + _pendingPlay = NO; + // Let's buffer some more data and let the playback poller start playback. + [self setState:kGMFPlayerStateBuffering]; + [self startPlaybackStatusPoller]; + } else { + [self setState:kGMFPlayerStatePaused]; + } + } else if ([_playerItem status] == AVPlayerItemStatusFailed) { + // TODO(tensafefrogs): Better error handling: [self failWithError:[_playerItem error]]; + } +} + +- (void)playerRateDidChange { + // TODO(tensafefrogs): Abandon rate observing since it's inconsistent between HLS + // and non-HLS videos. Rely on the poller. + if ([_player rate]) { + [self startPlaybackStatusPoller]; + [self setState:kGMFPlayerStatePlaying]; + } +} + +- (void)playbackDidReachEnd { + if ([_playerItem status] != AVPlayerItemStatusReadyToPlay) { + // In some cases, |AVPlayerItemDidPlayToEndTimeNotification| is fired while + // the player is being initialized. Ignore such notifications. + return; + } + + // Make sure we report the final media time if necessary before stopping the poller. + [self updateStateAndReportMediaTimes]; + [self stopPlaybackStatusPoller]; + [self setState:kGMFPlayerStateFinished]; + // For HLS videos, the rate isn't set to 0 on video end, so we have to do it + // explicitly. + if ([_player rate]) { + [_player setRate:0]; + } +} + +- (BOOL)isPlayableState { + // TODO(tensafefrogs): Drop this method and rely on the existence of |_player|. + return _state == kGMFPlayerStatePlaying || + _state == kGMFPlayerStatePaused || + _state == kGMFPlayerStateReadyToPlay || + _state == kGMFPlayerStateBuffering || + _state == kGMFPlayerStateSeeking || + _state == kGMFPlayerStateFinished; +} + +- (void)updateStateAndReportMediaTimes { + NSTimeInterval bufferedMediaTime = [self bufferedMediaTime]; + if (_lastReportedBufferTime != bufferedMediaTime) { + _lastReportedBufferTime = bufferedMediaTime; + [_delegate videoPlayer:self bufferedMediaTimeDidChangeToTime:bufferedMediaTime]; + } + + if (_state != kGMFPlayerStatePlaying && _state != kGMFPlayerStateBuffering) { + return; + } + + NSTimeInterval currentMediaTime = [self currentMediaTime]; + // If the current media time is different from the last reported media time, + // the player is playing. + if (_lastReportedPlaybackTime != currentMediaTime) { + _lastReportedPlaybackTime = currentMediaTime; + if (_state == kGMFPlayerStatePlaying) { + [_delegate videoPlayer:self currentMediaTimeDidChangeToTime:currentMediaTime]; + } else { + // Player resumed playback from buffering state. + [self setState:kGMFPlayerStatePlaying]; + } + } else if (![_player rate]) { + [_player play]; + } +} + +#pragma mark Cleanup + +- (void)clearPlayer { + _pendingPlay = NO; + _manuallyPaused = NO; + [self stopPlaybackStatusPoller]; + [self setAndObservePlayerItem:nil player:nil]; + _lastReportedPlaybackTime = 0; + _lastReportedBufferTime = 0; +} + +- (void)reset { + [self clearPlayer]; + [self setState:kGMFPlayerStateEmpty]; +} + +#pragma mark Utils and Misc. + ++ (NSTimeInterval)secondsWithCMTime:(CMTime)t { + return CMTIME_IS_NUMERIC(t) ? CMTimeGetSeconds(t) : 0; +} + +@end + + diff --git a/GoogleMediaFramework/GVPVideoPlayerSDK-Prefix.pch b/GoogleMediaFramework/GVPVideoPlayerSDK-Prefix.pch new file mode 100644 index 0000000..e084eb5 --- /dev/null +++ b/GoogleMediaFramework/GVPVideoPlayerSDK-Prefix.pch @@ -0,0 +1,21 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#define DEBUG + +#ifdef DEBUG +#define DLog(fmt, ...) NSLog((@"%s [Line %d] " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__); +#else +#define DLog(...) +#endif diff --git a/GoogleMediaFramework/GoogleMediaFramework-Prefix.pch b/GoogleMediaFramework/GoogleMediaFramework-Prefix.pch new file mode 100644 index 0000000..86a4d25 --- /dev/null +++ b/GoogleMediaFramework/GoogleMediaFramework-Prefix.pch @@ -0,0 +1,21 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//#define DEBUG + +#ifdef DEBUG +#define DLog(fmt, ...) NSLog((@"%s [Line %d] " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__); +#else +#define DLog(...) +#endif diff --git a/GoogleMediaFramework/GoogleMediaFramework.h b/GoogleMediaFramework/GoogleMediaFramework.h new file mode 100644 index 0000000..aef0107 --- /dev/null +++ b/GoogleMediaFramework/GoogleMediaFramework.h @@ -0,0 +1,20 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Public header files for use by apps using this framework +#import "GMFAdService.h" +#import "GMFPlayerFinishReason.h" +#import "GMFPlayerState.h" +#import "GMFPlayerViewController.h" +#import "GMFVideoPlayer.h" diff --git a/GoogleMediaFramework/UILabel+GMFLabels.h b/GoogleMediaFramework/UILabel+GMFLabels.h new file mode 100644 index 0000000..325122c --- /dev/null +++ b/GoogleMediaFramework/UILabel+GMFLabels.h @@ -0,0 +1,24 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +@interface UILabel (GMFLabelsAdditions) + +// Provides a non-styled label (just text visible) for displaying media time in the player controls. ++ (UILabel *)GMF_clearLabelForPlayerControls; + +@end + + diff --git a/GoogleMediaFramework/UILabel+GMFLabels.m b/GoogleMediaFramework/UILabel+GMFLabels.m new file mode 100644 index 0000000..b7fca34 --- /dev/null +++ b/GoogleMediaFramework/UILabel+GMFLabels.m @@ -0,0 +1,38 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "UILabel+GMFLabels.h" + +@implementation UILabel (GMFLabelsAdditions) + ++ (UILabel *)GMF_clearLabelForPlayerControls { + UILabel *label = [[UILabel alloc] init]; + UIFont *labelFont = [UIFont fontWithName:@"Helvetica" size:12.0]; + [label GMF_setFont:labelFont andColor:[UIColor whiteColor]]; + return label; +} + +- (void)GMF_setFont:(UIFont *)font andColor:(UIColor *)color { + [self setFont:font]; + if (color) { + [self setTextColor:color]; + } +} + +@end + diff --git a/GoogleMediaFramework/UIView+GMFPositioning.h b/GoogleMediaFramework/UIView+GMFPositioning.h new file mode 100644 index 0000000..14cc3bc --- /dev/null +++ b/GoogleMediaFramework/UIView+GMFPositioning.h @@ -0,0 +1,26 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +// Positioning helper methods for media player controls positioning. +@interface UIView (GMFPositioningAdditions) + +// Convenience methods to save some keystrokes. +@property(nonatomic, assign, setter=GMF_setOrigin:) CGPoint GMF_origin; + +// Returns the width of the view, or 0 if hidden. +@property(nonatomic, readonly) CGFloat GMF_visibleWidth; + +@end diff --git a/GoogleMediaFramework/UIView+GMFPositioning.m b/GoogleMediaFramework/UIView+GMFPositioning.m new file mode 100644 index 0000000..d2e9933 --- /dev/null +++ b/GoogleMediaFramework/UIView+GMFPositioning.m @@ -0,0 +1,92 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "UIView+GMFPositioning.h" + +@implementation UIView (GMFPositioningAdditions) + +// TODO(tensafefrogs): Revisit these helper methods. Some may be unnecessary or could be made more +// specific or useful for the video player UI. +- (void)GMF_setSize:(CGSize)size { + CGRect frame = [self frame]; + [self setFrame:CGRectMake(CGRectGetMinX(frame), + CGRectGetMinY(frame), + size.width, + size.height)]; +} + +- (CGFloat)GMF_visibleWidth { + return [self isHidden] ? 0 : CGRectGetWidth(self.frame); +} + +- (void)GMF_setOrigin:(CGPoint)origin { + CGRect frame = [self frame]; + [self setFrame:CGRectMake(origin.x, + origin.y, + CGRectGetWidth(frame), + CGRectGetHeight(frame))]; +} + +- (CGPoint)GMF_origin { + return [self frame].origin; +} + +- (void)GMF_alignCenterLeftToCenterLeftOfView:(UIView *)view + paddingX:(CGFloat)paddingX { + CGRect rect = [self convertRect:view.bounds fromView:view]; + [self GMF_setPixelAlignedFrame:CGRectMake( + CGRectGetMinX(rect) + paddingX, + CGRectGetMinY(rect) + (CGRectGetHeight(rect) - CGRectGetHeight(self.frame)) / 2, + CGRectGetWidth(self.frame), + CGRectGetHeight(self.frame))]; +} + +- (void)GMF_setPixelAlignedFrame:(CGRect)frameRect { + self.frame = [UIView GMF_pixelAlignedRect:frameRect]; +} + +- (void)GMF_setPixelAlignedOrigin:(CGPoint)origin { + [self GMF_setOrigin:[UIView GMF_pixelAlignedPoint:origin]]; +} + +// Utilities for keeping things aligned to whole pixel values to prevent image distortion. ++ (CGFloat)GMF_pixelAlignedValue:(CGFloat)value { + CGFloat screenScale = [[UIScreen mainScreen] scale]; + return floorf(value * screenScale) / screenScale; +} + ++ (CGPoint)GMF_pixelAlignedPoint:(CGPoint)point { + return CGPointMake([self GMF_pixelAlignedValue:point.x], + [self GMF_pixelAlignedValue:point.y]); +} + ++ (CGSize)GMF_pixelAlignedSize:(CGSize)size { + return CGSizeMake([self GMF_pixelAlignedValue:size.width], + [self GMF_pixelAlignedValue:size.height]); +} + ++ (CGRect)GMF_pixelAlignedRect:(CGRect)rect { + return CGRectMake([self GMF_pixelAlignedValue:rect.origin.x], + [self GMF_pixelAlignedValue:rect.origin.y], + [self GMF_pixelAlignedValue:rect.size.width], + [self GMF_pixelAlignedValue:rect.size.height]); +} + +@end + + diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo.xcodeproj/project.pbxproj b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo.xcodeproj/project.pbxproj new file mode 100644 index 0000000..92fcaa9 --- /dev/null +++ b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo.xcodeproj/project.pbxproj @@ -0,0 +1,596 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 4C5BB61017D13D86003D9E20 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C5BB60F17D13D86003D9E20 /* AVFoundation.framework */; }; + 4C5BB61217D13DAC003D9E20 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C5BB61117D13DAC003D9E20 /* AudioToolbox.framework */; }; + 4C5BB61417D13DC8003D9E20 /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C5BB61317D13DC8003D9E20 /* CoreMedia.framework */; }; + 4C87C1D41814423B0010D32D /* GMFIMASDKAdService.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C00AF3517F2081700167389 /* GMFIMASDKAdService.m */; }; + 4CAD3F6B17BD4703008C6D28 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CAD3F6A17BD4703008C6D28 /* UIKit.framework */; }; + 4CAD3F6D17BD4703008C6D28 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CAD3F6C17BD4703008C6D28 /* Foundation.framework */; }; + 4CAD3F6F17BD4703008C6D28 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CAD3F6E17BD4703008C6D28 /* CoreGraphics.framework */; }; + 4CAD3F7517BD4703008C6D28 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4CAD3F7317BD4703008C6D28 /* InfoPlist.strings */; }; + 4CAD3F7717BD4703008C6D28 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CAD3F7617BD4703008C6D28 /* main.m */; }; + 4CAD3F7B17BD4703008C6D28 /* GMFAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CAD3F7A17BD4703008C6D28 /* GMFAppDelegate.m */; }; + 4CAD3F7D17BD4703008C6D28 /* Default.png in Resources */ = {isa = PBXBuildFile; fileRef = 4CAD3F7C17BD4703008C6D28 /* Default.png */; }; + 4CAD3F7F17BD4703008C6D28 /* Default@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 4CAD3F7E17BD4703008C6D28 /* Default@2x.png */; }; + 4CAD3F8117BD4703008C6D28 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 4CAD3F8017BD4703008C6D28 /* Default-568h@2x.png */; }; + 4CAD3F9217BD4703008C6D28 /* SenTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CAD3F9117BD4703008C6D28 /* SenTestingKit.framework */; }; + 4CAD3F9317BD4704008C6D28 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CAD3F6A17BD4703008C6D28 /* UIKit.framework */; }; + 4CAD3F9417BD4704008C6D28 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CAD3F6C17BD4703008C6D28 /* Foundation.framework */; }; + 4CAD3F9C17BD4704008C6D28 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4CAD3F9A17BD4704008C6D28 /* InfoPlist.strings */; }; + 4CAD3F9F17BD4704008C6D28 /* GoogleMediaFrameworkDemoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CAD3F9E17BD4704008C6D28 /* GoogleMediaFrameworkDemoTests.m */; }; + 4CCC637817E7CC0E00D8F767 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A8A054B917E270A50035D08D /* SystemConfiguration.framework */; }; + 4CD0246E185CCE5300793419 /* GMFContentPlayhead.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CD0246D185CCE5300793419 /* GMFContentPlayhead.m */; }; + 4CD7815217EC9E9B00930F92 /* VideoListViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7815117EC9E9B00930F92 /* VideoListViewController.m */; }; + 4CF77DEA18567DAD00F98F76 /* VideoData.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CF77DE918567DAD00F98F76 /* VideoData.m */; }; + A8A054BA17E270A50035D08D /* AdSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A8A054B517E270A50035D08D /* AdSupport.framework */; }; + A8A054BB17E270A50035D08D /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A8A054B617E270A50035D08D /* CoreFoundation.framework */; }; + A8A054BC17E270A50035D08D /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A8A054B717E270A50035D08D /* MessageUI.framework */; }; + A8A054BD17E270A50035D08D /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A8A054B817E270A50035D08D /* QuartzCore.framework */; }; + E8E83A6A2D044DFF96AF06E1 /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D64E0D2ECC1547E581F50A56 /* libPods.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 4CAD3F9517BD4704008C6D28 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 4CAD3F5F17BD4703008C6D28 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 4CAD3F6617BD4703008C6D28; + remoteInfo = GoogleMediaFrameworkDemo; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 4CAD3FEB17C2B99A008C6D28 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 4B7B8DF517044E339E38FF76 /* Pods.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.xcconfig; path = Pods/Pods.xcconfig; sourceTree = ""; }; + 4C00AF3417F2081700167389 /* GMFIMASDKAdService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = GMFIMASDKAdService.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 4C00AF3517F2081700167389 /* GMFIMASDKAdService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = GMFIMASDKAdService.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 4C5BB60F17D13D86003D9E20 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; + 4C5BB61117D13DAC003D9E20 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; }; + 4C5BB61317D13DC8003D9E20 /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; + 4CAD3F6717BD4703008C6D28 /* GoogleMediaFrameworkDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GoogleMediaFrameworkDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 4CAD3F6A17BD4703008C6D28 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + 4CAD3F6C17BD4703008C6D28 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 4CAD3F6E17BD4703008C6D28 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 4CAD3F7217BD4703008C6D28 /* GoogleMediaFrameworkDemo-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleMediaFrameworkDemo-Info.plist"; sourceTree = ""; }; + 4CAD3F7417BD4703008C6D28 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 4CAD3F7617BD4703008C6D28 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = main.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 4CAD3F7817BD4703008C6D28 /* GMFVideoPlayerSDKExamples-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "GMFVideoPlayerSDKExamples-Prefix.pch"; sourceTree = ""; }; + 4CAD3F7917BD4703008C6D28 /* GMFAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = GMFAppDelegate.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 4CAD3F7A17BD4703008C6D28 /* GMFAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = GMFAppDelegate.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 4CAD3F7C17BD4703008C6D28 /* Default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Default.png; sourceTree = ""; }; + 4CAD3F7E17BD4703008C6D28 /* Default@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default@2x.png"; sourceTree = ""; }; + 4CAD3F8017BD4703008C6D28 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; + 4CAD3F9017BD4703008C6D28 /* GoogleMediaFrameworkDemoTests.octest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GoogleMediaFrameworkDemoTests.octest; sourceTree = BUILT_PRODUCTS_DIR; }; + 4CAD3F9117BD4703008C6D28 /* SenTestingKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SenTestingKit.framework; path = Library/Frameworks/SenTestingKit.framework; sourceTree = DEVELOPER_DIR; }; + 4CAD3F9917BD4704008C6D28 /* GoogleMediaFrameworkTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleMediaFrameworkTests-Info.plist"; sourceTree = ""; }; + 4CAD3F9B17BD4704008C6D28 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 4CAD3F9D17BD4704008C6D28 /* GoogleMediaFrameworkDemoTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GoogleMediaFrameworkDemoTests.h; sourceTree = ""; }; + 4CAD3F9E17BD4704008C6D28 /* GoogleMediaFrameworkDemoTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = GoogleMediaFrameworkDemoTests.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 4CD0246C185CCE5300793419 /* GMFContentPlayhead.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = GMFContentPlayhead.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 4CD0246D185CCE5300793419 /* GMFContentPlayhead.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = GMFContentPlayhead.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 4CD7815017EC9E9B00930F92 /* VideoListViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = VideoListViewController.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 4CD7815117EC9E9B00930F92 /* VideoListViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = VideoListViewController.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 4CF77DE818567DAD00F98F76 /* VideoData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VideoData.h; sourceTree = ""; }; + 4CF77DE918567DAD00F98F76 /* VideoData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VideoData.m; sourceTree = ""; }; + A8A054B517E270A50035D08D /* AdSupport.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdSupport.framework; path = System/Library/Frameworks/AdSupport.framework; sourceTree = SDKROOT; }; + A8A054B617E270A50035D08D /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; + A8A054B717E270A50035D08D /* MessageUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageUI.framework; path = System/Library/Frameworks/MessageUI.framework; sourceTree = SDKROOT; }; + A8A054B817E270A50035D08D /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + A8A054B917E270A50035D08D /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; + D64E0D2ECC1547E581F50A56 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 4CAD3F6417BD4703008C6D28 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4CCC637817E7CC0E00D8F767 /* SystemConfiguration.framework in Frameworks */, + A8A054BA17E270A50035D08D /* AdSupport.framework in Frameworks */, + A8A054BB17E270A50035D08D /* CoreFoundation.framework in Frameworks */, + A8A054BC17E270A50035D08D /* MessageUI.framework in Frameworks */, + A8A054BD17E270A50035D08D /* QuartzCore.framework in Frameworks */, + 4C5BB61017D13D86003D9E20 /* AVFoundation.framework in Frameworks */, + 4C5BB61417D13DC8003D9E20 /* CoreMedia.framework in Frameworks */, + 4C5BB61217D13DAC003D9E20 /* AudioToolbox.framework in Frameworks */, + 4CAD3F6B17BD4703008C6D28 /* UIKit.framework in Frameworks */, + 4CAD3F6D17BD4703008C6D28 /* Foundation.framework in Frameworks */, + 4CAD3F6F17BD4703008C6D28 /* CoreGraphics.framework in Frameworks */, + E8E83A6A2D044DFF96AF06E1 /* libPods.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4CAD3F8C17BD4703008C6D28 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4CAD3F9217BD4703008C6D28 /* SenTestingKit.framework in Frameworks */, + 4CAD3F9317BD4704008C6D28 /* UIKit.framework in Frameworks */, + 4CAD3F9417BD4704008C6D28 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 4CAD3F5E17BD4703008C6D28 = { + isa = PBXGroup; + children = ( + 4CAD3F7017BD4703008C6D28 /* GoogleMediaFrameworkDemo */, + 4CAD3F9717BD4704008C6D28 /* GoogleMediaFrameworkDemoTests */, + 4CAD3F6917BD4703008C6D28 /* Frameworks */, + 4CAD3F6817BD4703008C6D28 /* Products */, + 4B7B8DF517044E339E38FF76 /* Pods.xcconfig */, + ); + sourceTree = ""; + }; + 4CAD3F6817BD4703008C6D28 /* Products */ = { + isa = PBXGroup; + children = ( + 4CAD3F6717BD4703008C6D28 /* GoogleMediaFrameworkDemo.app */, + 4CAD3F9017BD4703008C6D28 /* GoogleMediaFrameworkDemoTests.octest */, + ); + name = Products; + sourceTree = ""; + }; + 4CAD3F6917BD4703008C6D28 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A8A054B517E270A50035D08D /* AdSupport.framework */, + A8A054B617E270A50035D08D /* CoreFoundation.framework */, + A8A054B717E270A50035D08D /* MessageUI.framework */, + A8A054B817E270A50035D08D /* QuartzCore.framework */, + A8A054B917E270A50035D08D /* SystemConfiguration.framework */, + 4C5BB61317D13DC8003D9E20 /* CoreMedia.framework */, + 4C5BB61117D13DAC003D9E20 /* AudioToolbox.framework */, + 4C5BB60F17D13D86003D9E20 /* AVFoundation.framework */, + 4CAD3F6A17BD4703008C6D28 /* UIKit.framework */, + 4CAD3F6C17BD4703008C6D28 /* Foundation.framework */, + 4CAD3F6E17BD4703008C6D28 /* CoreGraphics.framework */, + 4CAD3F9117BD4703008C6D28 /* SenTestingKit.framework */, + D64E0D2ECC1547E581F50A56 /* libPods.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 4CAD3F7017BD4703008C6D28 /* GoogleMediaFrameworkDemo */ = { + isa = PBXGroup; + children = ( + 4CAD3F7917BD4703008C6D28 /* GMFAppDelegate.h */, + 4CAD3F7A17BD4703008C6D28 /* GMFAppDelegate.m */, + 4C00AF3417F2081700167389 /* GMFIMASDKAdService.h */, + 4C00AF3517F2081700167389 /* GMFIMASDKAdService.m */, + 4CAD3F7117BD4703008C6D28 /* Supporting Files */, + 4CD7815017EC9E9B00930F92 /* VideoListViewController.h */, + 4CD7815117EC9E9B00930F92 /* VideoListViewController.m */, + 4CD0246C185CCE5300793419 /* GMFContentPlayhead.h */, + 4CD0246D185CCE5300793419 /* GMFContentPlayhead.m */, + 4CF77DE818567DAD00F98F76 /* VideoData.h */, + 4CF77DE918567DAD00F98F76 /* VideoData.m */, + ); + path = GoogleMediaFrameworkDemo; + sourceTree = ""; + }; + 4CAD3F7117BD4703008C6D28 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 4CAD3F7217BD4703008C6D28 /* GoogleMediaFrameworkDemo-Info.plist */, + 4CAD3F7317BD4703008C6D28 /* InfoPlist.strings */, + 4CAD3F7617BD4703008C6D28 /* main.m */, + 4CAD3F7817BD4703008C6D28 /* GMFVideoPlayerSDKExamples-Prefix.pch */, + 4CAD3F7C17BD4703008C6D28 /* Default.png */, + 4CAD3F7E17BD4703008C6D28 /* Default@2x.png */, + 4CAD3F8017BD4703008C6D28 /* Default-568h@2x.png */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 4CAD3F9717BD4704008C6D28 /* GoogleMediaFrameworkDemoTests */ = { + isa = PBXGroup; + children = ( + 4CAD3F9D17BD4704008C6D28 /* GoogleMediaFrameworkDemoTests.h */, + 4CAD3F9E17BD4704008C6D28 /* GoogleMediaFrameworkDemoTests.m */, + 4CAD3F9817BD4704008C6D28 /* Supporting Files */, + ); + name = GoogleMediaFrameworkDemoTests; + path = GoogleMediaFrameworkTests; + sourceTree = ""; + }; + 4CAD3F9817BD4704008C6D28 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 4CAD3F9917BD4704008C6D28 /* GoogleMediaFrameworkTests-Info.plist */, + 4CAD3F9A17BD4704008C6D28 /* InfoPlist.strings */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 4CAD3F6617BD4703008C6D28 /* GoogleMediaFrameworkDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4CAD3FA217BD4704008C6D28 /* Build configuration list for PBXNativeTarget "GoogleMediaFrameworkDemo" */; + buildPhases = ( + B43CA9D051C344D89EAEDD1C /* Check Pods Manifest.lock */, + 4CAD3F6317BD4703008C6D28 /* Sources */, + 4CAD3F6417BD4703008C6D28 /* Frameworks */, + 4CAD3F6517BD4703008C6D28 /* Resources */, + 4CAD3FEB17C2B99A008C6D28 /* CopyFiles */, + 771FE8B597914A5BB20F7EA9 /* Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = GoogleMediaFrameworkDemo; + productName = GoogleMediaFrameworkDemo; + productReference = 4CAD3F6717BD4703008C6D28 /* GoogleMediaFrameworkDemo.app */; + productType = "com.apple.product-type.application"; + }; + 4CAD3F8F17BD4703008C6D28 /* GoogleMediaFrameworkDemoTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4CAD3FA517BD4704008C6D28 /* Build configuration list for PBXNativeTarget "GoogleMediaFrameworkDemoTests" */; + buildPhases = ( + 4CAD3F8B17BD4703008C6D28 /* Sources */, + 4CAD3F8C17BD4703008C6D28 /* Frameworks */, + 4CAD3F8D17BD4703008C6D28 /* Resources */, + 4CAD3F8E17BD4703008C6D28 /* Run Script */, + ); + buildRules = ( + ); + dependencies = ( + 4CAD3F9617BD4704008C6D28 /* PBXTargetDependency */, + ); + name = GoogleMediaFrameworkDemoTests; + productName = GoogleMediaFrameworkDemoTests; + productReference = 4CAD3F9017BD4703008C6D28 /* GoogleMediaFrameworkDemoTests.octest */; + productType = "com.apple.product-type.bundle"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 4CAD3F5F17BD4703008C6D28 /* Project object */ = { + isa = PBXProject; + attributes = { + CLASSPREFIX = GMF; + LastUpgradeCheck = 0500; + ORGANIZATIONNAME = Google; + }; + buildConfigurationList = 4CAD3F6217BD4703008C6D28 /* Build configuration list for PBXProject "GoogleMediaFrameworkDemo" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 4CAD3F5E17BD4703008C6D28; + productRefGroup = 4CAD3F6817BD4703008C6D28 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 4CAD3F6617BD4703008C6D28 /* GoogleMediaFrameworkDemo */, + 4CAD3F8F17BD4703008C6D28 /* GoogleMediaFrameworkDemoTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 4CAD3F6517BD4703008C6D28 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4CAD3F7517BD4703008C6D28 /* InfoPlist.strings in Resources */, + 4CAD3F7D17BD4703008C6D28 /* Default.png in Resources */, + 4CAD3F7F17BD4703008C6D28 /* Default@2x.png in Resources */, + 4CAD3F8117BD4703008C6D28 /* Default-568h@2x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4CAD3F8D17BD4703008C6D28 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4CAD3F9C17BD4704008C6D28 /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 4CAD3F8E17BD4703008C6D28 /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Run the unit tests in this test bundle.\n\"${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests\"\n"; + }; + 771FE8B597914A5BB20F7EA9 /* Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Pods-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + B43CA9D051C344D89EAEDD1C /* Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 4CAD3F6317BD4703008C6D28 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4C87C1D41814423B0010D32D /* GMFIMASDKAdService.m in Sources */, + 4CAD3F7717BD4703008C6D28 /* main.m in Sources */, + 4CAD3F7B17BD4703008C6D28 /* GMFAppDelegate.m in Sources */, + 4CF77DEA18567DAD00F98F76 /* VideoData.m in Sources */, + 4CD0246E185CCE5300793419 /* GMFContentPlayhead.m in Sources */, + 4CD7815217EC9E9B00930F92 /* VideoListViewController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4CAD3F8B17BD4703008C6D28 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4CAD3F9F17BD4704008C6D28 /* GoogleMediaFrameworkDemoTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 4CAD3F9617BD4704008C6D28 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 4CAD3F6617BD4703008C6D28 /* GoogleMediaFrameworkDemo */; + targetProxy = 4CAD3F9517BD4704008C6D28 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 4CAD3F7317BD4703008C6D28 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 4CAD3F7417BD4703008C6D28 /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 4CAD3F9A17BD4704008C6D28 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 4CAD3F9B17BD4704008C6D28 /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 4CAD3FA017BD4704008C6D28 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREFIX_HEADER = ""; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 6.1; + ONLY_ACTIVE_ARCH = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 4CAD3FA117BD4704008C6D28 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_PREFIX_HEADER = ""; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 6.1; + OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 4CAD3FA317BD4704008C6D28 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4B7B8DF517044E339E38FF76 /* Pods.xcconfig */; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo-Prefix.pch"; + INFOPLIST_FILE = "GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo-Info.plist"; + LIBRARY_SEARCH_PATHS = "$(inherited)"; + ONLY_ACTIVE_ARCH = NO; + OTHER_LDFLAGS = ( + "-ObjC", + "-lGoogleIMA3", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "B218B3A1-E07D-44F2-AA69-6739FC0F6D19"; + WRAPPER_EXTENSION = app; + }; + name = Debug; + }; + 4CAD3FA417BD4704008C6D28 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4B7B8DF517044E339E38FF76 /* Pods.xcconfig */; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo-Prefix.pch"; + INFOPLIST_FILE = "GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo-Info.plist"; + LIBRARY_SEARCH_PATHS = "$(inherited)"; + OTHER_LDFLAGS = ( + "-ObjC", + "-lGoogleIMA3", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "B218B3A1-E07D-44F2-AA69-6739FC0F6D19"; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; + 4CAD3FA617BD4704008C6D28 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4B7B8DF517044E339E38FF76 /* Pods.xcconfig */; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/GoogleMediaFrameworkDemo.app/GoogleMediaFrameworkDemo"; + FRAMEWORK_SEARCH_PATHS = ( + "\"$(SDKROOT)/Developer/Library/Frameworks\"", + "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo-Prefix.pch"; + INFOPLIST_FILE = "GoogleMediaFrameworkTests/GoogleMediaFrameworkTests-Info.plist"; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUNDLE_LOADER)"; + WRAPPER_EXTENSION = octest; + }; + name = Debug; + }; + 4CAD3FA717BD4704008C6D28 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4B7B8DF517044E339E38FF76 /* Pods.xcconfig */; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/GoogleMediaFrameworkDemo.app/GoogleMediaFrameworkDemo"; + FRAMEWORK_SEARCH_PATHS = ( + "\"$(SDKROOT)/Developer/Library/Frameworks\"", + "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo-Prefix.pch"; + INFOPLIST_FILE = "GoogleMediaFrameworkTests/GoogleMediaFrameworkTests-Info.plist"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUNDLE_LOADER)"; + WRAPPER_EXTENSION = octest; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 4CAD3F6217BD4703008C6D28 /* Build configuration list for PBXProject "GoogleMediaFrameworkDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4CAD3FA017BD4704008C6D28 /* Debug */, + 4CAD3FA117BD4704008C6D28 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4CAD3FA217BD4704008C6D28 /* Build configuration list for PBXNativeTarget "GoogleMediaFrameworkDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4CAD3FA317BD4704008C6D28 /* Debug */, + 4CAD3FA417BD4704008C6D28 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4CAD3FA517BD4704008C6D28 /* Build configuration list for PBXNativeTarget "GoogleMediaFrameworkDemoTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4CAD3FA617BD4704008C6D28 /* Debug */, + 4CAD3FA717BD4704008C6D28 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 4CAD3F5F17BD4703008C6D28 /* Project object */; +} diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..b450086 --- /dev/null +++ b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo.xcworkspace/contents.xcworkspacedata b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..0820168 --- /dev/null +++ b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo.xcworkspace/contents.xcworkspacedata @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/Default-568h@2x.png b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/Default-568h@2x.png new file mode 100644 index 0000000..0891b7a Binary files /dev/null and b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/Default-568h@2x.png differ diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/Default.png b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/Default.png new file mode 100644 index 0000000..4c8ca6f Binary files /dev/null and b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/Default.png differ diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/Default@2x.png b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/Default@2x.png new file mode 100644 index 0000000..35b84cf Binary files /dev/null and b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/Default@2x.png differ diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GMFAppDelegate.h b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GMFAppDelegate.h new file mode 100644 index 0000000..8f2a604 --- /dev/null +++ b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GMFAppDelegate.h @@ -0,0 +1,26 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import +#import + +#import "GMFIMASDKAdService.h" + +@interface GMFAppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; +@property(strong, nonatomic) GMFPlayerViewController *videoPlayerViewController; +@property(strong, nonatomic) GMFIMASDKAdService *adService; + +@end diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GMFAppDelegate.m b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GMFAppDelegate.m new file mode 100644 index 0000000..0e37757 --- /dev/null +++ b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GMFAppDelegate.m @@ -0,0 +1,38 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "GMFAppDelegate.h" +#import "VideoListViewController.h" + +@implementation GMFAppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + + // For the purposes of this sample app, we are using a NavigationController to display + // a few video and ad options. + VideoListViewController *viewController = [[VideoListViewController alloc] init]; + UINavigationController *navController = + [[UINavigationController alloc] initWithRootViewController:viewController]; + // Hide the navbar so the video plays fullscreen. + [navController setNavigationBarHidden:YES animated:NO]; + + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.rootViewController = navController; + [self.window makeKeyAndVisible]; + + return YES; +} + +@end + diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GMFContentPlayhead.h b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GMFContentPlayhead.h new file mode 100644 index 0000000..71b216e --- /dev/null +++ b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GMFContentPlayhead.h @@ -0,0 +1,13 @@ + +#import +#import +#import + +// Provides a KVO observable property for the Google Interactive Media Ads SDK (IMA SDK). +@interface GMFContentPlayhead : NSObject + +@property(nonatomic, readonly) NSTimeInterval currentTime; + +- (instancetype)initWithGMFPlayerViewController:(GMFPlayerViewController *)playerViewController; + +@end diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GMFContentPlayhead.m b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GMFContentPlayhead.m new file mode 100644 index 0000000..bf044f8 --- /dev/null +++ b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GMFContentPlayhead.m @@ -0,0 +1,51 @@ + + +#import "GMFContentPlayhead.h" + +@implementation GMFContentPlayhead { + GMFPlayerViewController *_playerViewController; +} + +- (instancetype)init { + NSAssert(false, @"You must initialize GMFContentPlayhead using initWithGMFPlayerViewController"); + return nil; +} + +- (instancetype)initWithGMFPlayerViewController:(GMFPlayerViewController *)playerViewController { + self = [super init]; + if (self) { + _playerViewController = playerViewController; + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(currentMediaTimeDidChange) + name:kGMFPlayerCurrentMediaTimeDidChangeNotification + object:_playerViewController]; + } + return self; +} + +- (void)currentMediaTimeDidChange { + [self willChangeValueForKey:@"currentTime"]; + _currentTime = _playerViewController.currentMediaTime; + [self didChangeValueForKey:@"currentTime"]; +} + +// Tell KVO that we will manually notify when the value of currentTime changes. ++ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey { + BOOL automatic = NO; + if ([theKey isEqualToString:@"currentTime"]) { + automatic = NO; + } else { + automatic = [super automaticallyNotifiesObserversForKey:theKey]; + } + return automatic; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] + removeObserver:self + name:kGMFPlayerCurrentMediaTimeDidChangeNotification + object:nil]; +} + +@end diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GMFIMASDKAdService.h b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GMFIMASDKAdService.h new file mode 100644 index 0000000..55c1477 --- /dev/null +++ b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GMFIMASDKAdService.h @@ -0,0 +1,44 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import +#import +#import + +@interface GMFIMASDKAdService : GMFAdService { + @private + UIView* _adView; +} + +@property(nonatomic, strong) IMAAdsLoader *adsLoader; + +@property(nonatomic, strong) IMAAdsManager *adsManager; + +// Initiate a request to the ads server for ads associated with the given adtag. +- (void)requestAdsWithRequest:(NSString *)request; + +#pragma mark GMFPlayerOverlayViewDelegate + +- (void)didPressPlay; +- (void)didPressPause; +- (void)didPressReplay; +- (void)didPressMinimize; +- (void)didSeekToTime:(NSTimeInterval)time; +- (void)didStartScrubbing; +- (void)didEndScrubbing; + +@end + diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GMFIMASDKAdService.m b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GMFIMASDKAdService.m new file mode 100644 index 0000000..aec81e9 --- /dev/null +++ b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GMFIMASDKAdService.m @@ -0,0 +1,249 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "GMFIMASDKAdService.h" +#import "GMFContentPlayhead.h" + +@class GMFPlayerOverlayView; + +@interface GMFIMASDKAdService () + +@end + +@implementation GMFIMASDKAdService { + BOOL _hasVideoPlayerControl; +} + +// Designated initializer +- (instancetype)initWithGMFVideoPlayer:(GMFPlayerViewController *)videoPlayerController { + return [super initWithGMFVideoPlayer:videoPlayerController]; +} + +- (void)requestAdsWithRequest:(NSString *)request { + [self createAdsLoader]; + IMAAdsRequest *adsRequest = + [[IMAAdsRequest alloc] initWithAdTagUrl:request + companionSlots:nil + userContext:nil]; + + [_adsLoader requestAdsWithRequest:adsRequest]; +} + +- (IMASettings *)createIMASettings { + IMASettings *settings = [[IMASettings alloc] init]; + settings.language = @"en"; + return settings; +} + +- (void)createAdsLoader { + self.adsLoader = [[IMAAdsLoader alloc] initWithSettings:[self createIMASettings]]; + self.adsLoader.delegate = self; +} + +#pragma mark GMFVideoPlayerViewController notification handlers + +// Listen to playbackWill finish in order to play postrolls if needed before the player ends. +// The GMFVideoPlayerSDK subscribes to this notification automatically. +- (void)playbackWillFinish:(NSNotification *)notification { + int finishReason = [[[notification userInfo] + objectForKey:kGMFPlayerPlaybackWillFinishReasonUserInfoKey] intValue]; + if (finishReason == GMFPlayerFinishReasonPlaybackEnded) { + // Playback reached the end of the video, notify AdsManager in case there are postrolls. + [self.adsLoader contentComplete]; + } +} + +// Destroy adsManager when user exits the player +- (void)playbackDidFinish:(NSNotification *)notification { + int finishReason = [[[notification userInfo] + objectForKey:kGMFPlayerPlaybackDidFinishReasonUserInfoKey] intValue]; + if (finishReason == GMFPlayerFinishReasonUserExited) { + [self.adsManager destroy]; + [self relinquishControlToVideoPlayer]; + } +} + +#pragma mark IMAAdsLoaderDelegate + +- (void)adsLoader:(IMAAdsLoader *)loader failedWithErrorData:(IMAAdLoadingErrorData *)adErrorData { + // Loading failed, you probbaly want to log it when this happens. + NSLog(@"Ad loading error: %@", adErrorData.adError.message); +} + +// Ads are loaded, create the AdsManager and set up the ad frame for displaying ads +- (void)adsLoader:(IMAAdsLoader *)loader adsLoadedWithData:(IMAAdsLoadedData *)adsLoadedData { + // Get the ads manager from ads loaded data. + self.adsManager = adsLoadedData.adsManager; + + // GMFContentPlayhead handles listening for time updates from the video player and passing those + // to the AdsManager. + GMFContentPlayhead *contentPlayhead = + [[GMFContentPlayhead alloc] initWithGMFPlayerViewController:self.videoPlayerController]; + + [self.adsManager initializeWithContentPlayhead:contentPlayhead adsRenderingSettings:nil]; + + self.adsManager.adView.frame = _adView.bounds; + self.adsManager.delegate = self; + + // Set the adView to just above the player rendering view, but below the controls. + [self.videoPlayerController setABoveRenderingView:self.adsManager.adView]; + + [self.adsManager start]; +} + +#pragma mark IMAAdsManagerDelegate + +- (void)adsManagerDidRequestContentPause:(IMAAdsManager *)adsManager { + // IMA SDK wants control of the player, so pause and take over delegate from video controls. + [self takeControlOfVideoPlayer]; +} + +- (void)adsManagerDidRequestContentResume:(IMAAdsManager *)adsManager { + // Resume or start (if not started yet) the content. + [self.videoPlayerController setControlsVisibility:YES animated:YES]; + [self relinquishControlToVideoPlayer]; + [self.videoPlayerController play]; +} + +// Process ad events. +- (void)adsManager:(IMAAdsManager *)adsManager didReceiveAdEvent:(IMAAdEvent *)event { + // Perform different actions based on the event type. + NSLog(@"** Ad event **: %@", [self adEventAsString:event.type]); + + switch (event.type) { + case kIMAAdEvent_LOADED: + [self.videoPlayerController.playerOverlayView setTotalTime:event.ad.duration]; + [self.videoPlayerController.playerOverlayView setSeekbarTrackColor:[UIColor yellowColor]]; + break; + case kIMAAdEvent_STARTED: + [self.videoPlayerController.playerOverlayView disableSeekbarInteraction]; + // break ommitted on purpose + case kIMAAdEvent_RESUME: + // When an ad starts, take over control of the video player until the ad completes + [self.videoPlayerController.playerOverlayView showPauseButton]; + break; + case kIMAAdEvent_PAUSE: + [self.videoPlayerController.playerOverlayView showPlayButton]; + break; + case kIMAAdEvent_ALL_ADS_COMPLETED: + // When all ads are done, give control back to the video player. + [self relinquishControlToVideoPlayer]; + NSLog(@"destroying ads manager"); + [self.adsManager destroy]; + // TODO: destroy loader (pending IMA SDK bug) + //[self.adsLoader destroy]; + break; + default: + break; + } +} + +// Process ad playing errors. +- (void)adsManager:(IMAAdsManager *)adsManager didReceiveAdError:(IMAAdError *)error { + // There was an error while playing the ad. + NSLog(@"Error during ad playback: %@", error); + [self relinquishControlToVideoPlayer]; + [self.videoPlayerController play]; +} + +- (void)adDidProgressToTime:(NSTimeInterval)mediaTime totalTime:(NSTimeInterval)totalTime { + [self.videoPlayerController.playerOverlayView setMediaTime:mediaTime]; +} + +- (void)takeControlOfVideoPlayer { + _hasVideoPlayerControl = YES; + [self.videoPlayerController pause]; + [self.videoPlayerController setVideoPlayerOverlayDelegate:self]; +} + +- (void)relinquishControlToVideoPlayer { + [self.videoPlayerController.playerOverlayView enableSeekbarInteraction]; + [self.videoPlayerController setDefaultVideoPlayerOverlayDelegate]; + [self.videoPlayerController.playerOverlayView setSeekbarTrackColorDefault]; + _hasVideoPlayerControl = NO; +} + +#pragma mark GMFPlayerOverlayViewDelegate + +- (void)didPressPlay { + [self.adsManager resume]; +} + +- (void)didPressPause { + [self.adsManager pause]; +} + +- (void)didPressReplay { + // Noop +} + +- (void)didPressMinimize { + [self.videoPlayerController didPressMinimize]; +} + +// Not implemented since ads seek bar is read only. +- (void)didSeekToTime:(NSTimeInterval)time { + // Noop +} + +- (void)didStartScrubbing { + // Noop +} + +- (void)didEndScrubbing { + // Noop +} + +#pragma mark Debug Methods + +// Helper/debug method for displaying ad events in the application log. +- (NSString *)adEventAsString:(IMAAdEventType)adEventType { + switch(adEventType) { + case kIMAAdEvent_ALL_ADS_COMPLETED: + // All ads managed by the ads manager have completed. + return @"All Ads Completed"; + case kIMAAdEvent_CLICKED: + // Ad clicked. + return @"Ad Clicked"; + case kIMAAdEvent_COMPLETE: + // Single ad has finished. + return @"Complete"; + case kIMAAdEvent_FIRST_QUARTILE: + // First quartile of a linear ad was reached. + return @"First quartile reached"; + case kIMAAdEvent_LOADED: + // An ad was loaded. + return @"Loaded"; + case kIMAAdEvent_MIDPOINT: + // Midpoint of a linear ad was reached. + return @"Midpoint reached"; + case kIMAAdEvent_PAUSE: + // Ad paused. + return @"Ad Paused"; + case kIMAAdEvent_RESUME: + // Ad resumed. + return @"Ad Resumed"; + case kIMAAdEvent_THIRD_QUARTILE: + // Third quartile of a linear ad was reached. + return @"Third quartile reached"; + case kIMAAdEvent_STARTED: + // Ad has started. + return @"Ad Started"; + default: + return @"Invalid Error type"; + } +} + +@end + diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo-Info.plist b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo-Info.plist new file mode 100644 index 0000000..0ce305a --- /dev/null +++ b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo-Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + com.google.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.1.0 + CFBundleSignature + ???? + CFBundleVersion + 0.1.0 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarHidden + + UIStatusBarStyle + UIStatusBarStyleDefault + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo-Prefix.pch b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo-Prefix.pch new file mode 100644 index 0000000..fc56c2f --- /dev/null +++ b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo-Prefix.pch @@ -0,0 +1,25 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#ifndef __IPHONE_5_0 +#warning "This project uses features only available in iOS SDK 5.0 and later." +#endif + +#ifdef __OBJC__ + #import + #import +#endif + diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/VideoData.h b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/VideoData.h new file mode 100644 index 0000000..ee64b37 --- /dev/null +++ b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/VideoData.h @@ -0,0 +1,29 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +@interface VideoData : NSObject + +@property(nonatomic, copy) NSString *videoURL; +@property(nonatomic, copy) NSString *title; +@property(nonatomic, copy) NSString *description; +@property(nonatomic, copy) NSString *adTagURL; + +- (id)initWithVideoURL:(NSString *)videoURL + title:(NSString *)title + description:(NSString *)description + adTagURL:(NSString *)adTagURL; + +@end diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/VideoData.m b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/VideoData.m new file mode 100644 index 0000000..d7932b0 --- /dev/null +++ b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/VideoData.m @@ -0,0 +1,33 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "VideoData.h" + +@implementation VideoData + +- (instancetype)initWithVideoURL:(NSString *)videoURL + title:(NSString *)title + description:(NSString *)description + adTagURL:(NSString *)adTagURL { + self = [super init]; + if (self) { + _videoURL = videoURL; + _title = title; + _description = description; + _adTagURL = adTagURL; + } + return self; +} + +@end diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/VideoListViewController.h b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/VideoListViewController.h new file mode 100644 index 0000000..803adee --- /dev/null +++ b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/VideoListViewController.h @@ -0,0 +1,27 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "GMFIMASDKAdService.h" + +#import + +@interface VideoListViewController : UIViewController + +@property(nonatomic, strong) UITableView *tableView; +@property(nonatomic, strong) NSArray *videos; +@property(nonatomic, strong) GMFIMASDKAdService *adService; + +@end + diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/VideoListViewController.m b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/VideoListViewController.m new file mode 100644 index 0000000..a21a53e --- /dev/null +++ b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/VideoListViewController.m @@ -0,0 +1,168 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "VideoData.h" +#import "VideoListViewController.h" + +#import + +@interface VideoListViewController () + +@end + +@implementation VideoListViewController + +- (void)loadView { + self.title = @"Example Videos"; + + self.tableView = [[UITableView alloc] initWithFrame:CGRectZero]; + self.tableView.dataSource = self; + self.tableView.delegate = self; + + [self populateVideosArray]; + + self.view = self.tableView; +} + +#pragma mark GMFVideoPlayer notifications + +- (void)playbackDidFinish:(NSNotification *)notification { + int exitReason = + [[[notification userInfo] objectForKey:kGMFPlayerPlaybackDidFinishReasonUserInfoKey] intValue]; + switch (exitReason) { + case GMFPlayerFinishReasonPlaybackEnded: + NSLog(@"Playback ended, content complete."); + break; + case GMFPlayerFinishReasonUserExited: + NSLog(@"User minimized player"); + // User clicked minimize, go back to the prev screen and remove observers + [self removeVideoPlayerObservers]; + [self.navigationController popViewControllerAnimated:YES]; + break; + } +} + +- (void)removeVideoPlayerObservers { + [[NSNotificationCenter defaultCenter] + removeObserver:self + name:kGMFPlayerStateDidChangeToFinishedNotification + object:nil]; +} + +#pragma mark - UITableViewDataSource + +- (UITableViewCell *)tableView:(UITableView *)tableView + cellForRowAtIndexPath:(NSIndexPath *)indexPath { + static NSString *const kVideoCellReuseIndetifier = @"videoCell"; + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kVideoCellReuseIndetifier]; + if (!cell) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle + reuseIdentifier:kVideoCellReuseIndetifier]; + } + + VideoData *video = [_videos objectAtIndex:indexPath.row]; + // TODO(tensafefrogs): Add thumbnails to the sample videos. + // cell.imageView.image = video.thumbnail; + cell.textLabel.text = video.title; + cell.detailTextLabel.text = video.description; + return cell; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return [_videos count]; +} + +#pragma mark - UITableViewDelegate + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + VideoData *video = [_videos objectAtIndex:indexPath.row]; + + // Init the video player view controller. + GMFPlayerViewController *videoPlayerViewController = [[GMFPlayerViewController alloc] init]; + + // Listen for playback finished event. See GMFPlayerFinishReason. + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(playbackDidFinish:) + name:kGMFPlayerStateDidChangeToFinishedNotification + object:videoPlayerViewController]; + // Set the content URL in the player. + [videoPlayerViewController loadStreamWithURL:[NSURL URLWithString:video.videoURL]]; + + // If there's an ad associated with the player, initialize the AdService using the video player + // and request the ads. + if (video.adTagURL != nil) { + _adService = [[GMFIMASDKAdService alloc] initWithGMFVideoPlayer:videoPlayerViewController]; + + [videoPlayerViewController registerAdService:_adService]; + + [_adService requestAdsWithRequest:video.adTagURL]; + } + + // Show the video player. + [self.navigationController pushViewController:videoPlayerViewController animated:YES]; + // Tell the video player to start playing. + [videoPlayerViewController play]; + + [tableView deselectRowAtIndexPath:indexPath animated:YES]; +} + +#pragma mark Videos Data array + +// Populates the videos array with our sample content.. +- (void)populateVideosArray { + if ([_videos count] == 0) { + _videos = @[ + [[VideoData alloc] initWithVideoURL:@"http://rmcdn.2mdn.net/Demo/html5/output.mp4" + title:@"MP4 Video" + description:@"No ads" + adTagURL:nil], + [[VideoData alloc] initWithVideoURL:@"http://rmcdn.2mdn.net/Demo/html5/output.mp4" + title:@"MP4 Video" + description:@"With Preroll" + adTagURL:@"http://pubads.g.doubleclick.net/gampad/ads?sz=400x300&iu=%2F6062%2Fiab_vast_samples&ciu_szs=300x250%2C728x90&gdfp_req=1&env=vp&output=xml_vast2&unviewed_position_start=1&url=[referrer_url]&correlator=[timestamp]&cust_params=iab_vast_samples%3Dlinear"], + [[VideoData alloc] initWithVideoURL:@"http://rmcdn.2mdn.net/Demo/html5/output.mp4" + title:@"MP4 Video" + description:@"2x Preroll pod w/ bumper, 2x midroll, 2x postroll w/ bumpers." + adTagURL:@"http://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=%2F15018773%2Feverything2&ciu_szs=300x250%2C468x60%2C728x90&impl=s&gdfp_req=1&env=vp&output=xml_vast2&unviewed_position_start=1&url=[referrer_url]&correlator=[timestamp]&cmsid=133&vid=10XWSh7W4so&ad_rule=1"], + [[VideoData alloc] initWithVideoURL:@"http://rmcdn.2mdn.net/Demo/html5/output.mp4" + title:@"MP4 Video" + description:@"2x midroll pod, 2 ads each @ 11s and 20s" + adTagURL:@"http://pubads.g.doubleclick.net/gampad/ads?sz=400x300&iu=%2F15018773%2Fcue_point&ciu_szs=728x90&impl=s&gdfp_req=1&env=vp&output=xml_vast2&unviewed_position_start=1&url=[referrer_url]&correlator=[timestamp]&cmsid=493&vid=932730933&ad_rule=1"], + [[VideoData alloc] initWithVideoURL:@"http://rmcdn.2mdn.net/Demo/html5/output.mp4" + title:@"MP4 Video" + description:@"Skippable preroll" + adTagURL:@"http://pubads.g.doubleclick.net/gampad/ads?sz=400x300&iu=%2F6753%2FSDKregression%2Flinear_hosted_video_with_img_comps&ciu_szs=300x250%2C728x90&impl=s&gdfp_req=1&env=vp&output=xml_vast2&unviewed_position_start=1&url=example.com&correlator=12345"], + [[VideoData alloc] initWithVideoURL:@"http://devimages.apple.com/samplecode/adDemo/ad.m3u8" + title:@"m3u8 Streaming Video" + description:@"No ads" + adTagURL:nil], + [[VideoData alloc] initWithVideoURL:@"http://devimages.apple.com/samplecode/adDemo/ad.m3u8" + title:@"m3u8 Streaming Video" + description:@"With Preroll" + adTagURL:@"http://pubads.g.doubleclick.net/gampad/ads?sz=400x300&iu=%2F6062%2Fiab_vast_samples&ciu_szs=300x250%2C728x90&gdfp_req=1&env=vp&output=xml_vast2&unviewed_position_start=1&url=[referrer_url]&correlator=[timestamp]&cust_params=iab_vast_samples%3Dlinear"], + [[VideoData alloc] initWithVideoURL:@"http://devimages.apple.com/samplecode/adDemo/ad.m3u8" + title:@"m3u8 Streaming Video" + description:@"Pre-roll, pre-roll bumper, mid-roll pods (pods of 4, every 30 seconds), post-roll bumper, and post roll" + adTagURL:@"http://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=%2F3510761%2FadRulesSampleTags&ciu_szs=160x600%2C300x250%2C728x90&cust_params=adrule%3Dpremidpostpodandbumpers&impl=s&gdfp_req=1&env=vp&ad_rule=1&vid=47570401&cmsid=481&output=xml_vast2&unviewed_position_start=1&url=[referrer_url]&correlator=[timestamp]"] + ]; + } +} + + +- (void)dealloc { + [self removeVideoPlayerObservers]; +} + +@end + diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/en.lproj/InfoPlist.strings b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/en.lproj/InfoPlist.strings new file mode 100644 index 0000000..477b28f --- /dev/null +++ b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/main.m b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/main.m new file mode 100644 index 0000000..d630f06 --- /dev/null +++ b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkDemo/main.m @@ -0,0 +1,23 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "GMFAppDelegate.h" + +int main(int argc, char *argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([GMFAppDelegate class])); + } +} diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkTests/GoogleMediaFrameworkDemoTests.h b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkTests/GoogleMediaFrameworkDemoTests.h new file mode 100644 index 0000000..7ea9492 --- /dev/null +++ b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkTests/GoogleMediaFrameworkDemoTests.h @@ -0,0 +1,23 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "GMFVideoPlayer.h" + +@interface GoogleMediaFrameworkDemoTests : SenTestCase + ++ (NSString *)stringWithState:(GMFPlayerState)state; + +@end diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkTests/GoogleMediaFrameworkDemoTests.m b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkTests/GoogleMediaFrameworkDemoTests.m new file mode 100644 index 0000000..8d7cb6e --- /dev/null +++ b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkTests/GoogleMediaFrameworkDemoTests.m @@ -0,0 +1,122 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "GoogleMediaFrameworkDemoTests.h" +#import "GMFPlayerState.h" + +NSString *CONTENT_URL = @"http://rmcdn.2mdn.net/Demo/html5/output.mp4"; + +@implementation GoogleMediaFrameworkDemoTests { + @private + GMFVideoPlayer *_player; + NSMutableArray *_eventList; +} + +- (void)setUp { + [super setUp]; + _player = [[GMFVideoPlayer alloc] init]; + [_player setDelegate:self]; + _eventList = [NSMutableArray array]; +} + +- (void)tearDown { + _player = nil; + _eventList = nil; + + [super tearDown]; +} + +- (void)testPlay { + // Load URL + [_player loadStreamWithURL:[NSURL URLWithString:CONTENT_URL]]; + [self waitForState:kGMFPlayerStateLoadingContent withTimeout:10]; + [self waitForState:kGMFPlayerStateReadyToPlay withTimeout:10]; + [self waitForState:kGMFPlayerStatePaused withTimeout:10]; + + // Play stream + [_player play]; + [self waitForState:kGMFPlayerStatePlaying withTimeout:10]; + + STAssertFalse(_eventList.count, @"Unexpected events %@", _eventList); +} + +- (void) waitForState:(GMFPlayerState)state withTimeout:(NSInteger)timeout { + STAssertTrue(WaitFor(^BOOL { + return (([_eventList count] > 0) && (_eventList[0] == [NSNumber numberWithInt:state])); + }, timeout), @"Failed while waiting for state %@", [GoogleMediaFrameworkDemoTests stringWithState:state]); + [self removeWaitingState:state]; +} + +- (void) removeWaitingState:(GMFPlayerState)state { + [_eventList removeObject:[NSNumber numberWithInt:state]]; +} + +BOOL WaitFor(BOOL (^block)(void), NSTimeInterval seconds) { + NSDate* start = [NSDate date]; + NSDate* end = [[NSDate date] dateByAddingTimeInterval:seconds]; + while (!block() && [GoogleMediaFrameworkDemoTests timeIntervalSince:start] < seconds) { + [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode + beforeDate: end]; + } + return block(); +} + ++ (NSTimeInterval)timeIntervalSince:(NSDate*)date { + return -[date timeIntervalSinceNow]; +} + +- (void) videoPlayer:(GMFVideoPlayer *)videoPlayer + stateDidChangeFrom:(GMFPlayerState)fromState + to:(GMFPlayerState)toState { + // Ignore buffering events - we can't reliable predict when they will be fired + if (toState != kGMFPlayerStateBuffering) { + [_eventList addObject:[NSNumber numberWithInt:toState]]; + } +} + +- (void) videoPlayer:(GMFVideoPlayer *)videoPlayer + currentMediaTimeDidChangeToTime:(NSTimeInterval)time { + // no-op +} + +- (void)videoPlayer:(GMFVideoPlayer *)videoPlayer + bufferedMediaTimeDidChangeToTime:(NSTimeInterval)time { + // no-op +} + ++ (NSString *)stringWithState:(GMFPlayerState)state { + switch (state) { + case kGMFPlayerStateEmpty: + return @"Empty"; + case kGMFPlayerStateBuffering: + return @"Buffering"; + case kGMFPlayerStateLoadingContent: + return @"Loading content"; + case kGMFPlayerStateReadyToPlay: + return @"Ready to play"; + case kGMFPlayerStatePlaying: + return @"Playing"; + case kGMFPlayerStatePaused: + return @"Paused"; + case kGMFPlayerStateFinished: + return @"Finished"; + case kGMFPlayerStateSeeking: + return @"Seeking"; + case kGMFPlayerStateError: + return @"Error"; + } + return nil; +} + +@end diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkTests/GoogleMediaFrameworkTests-Info.plist b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkTests/GoogleMediaFrameworkTests-Info.plist new file mode 100644 index 0000000..22e2a02 --- /dev/null +++ b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkTests/GoogleMediaFrameworkTests-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + com.google.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/GoogleMediaFrameworkDemo/GoogleMediaFrameworkTests/en.lproj/InfoPlist.strings b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkTests/en.lproj/InfoPlist.strings new file mode 100644 index 0000000..477b28f --- /dev/null +++ b/GoogleMediaFrameworkDemo/GoogleMediaFrameworkTests/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/GoogleMediaFrameworkDemo/Podfile b/GoogleMediaFrameworkDemo/Podfile new file mode 100644 index 0000000..56313d0 --- /dev/null +++ b/GoogleMediaFrameworkDemo/Podfile @@ -0,0 +1,5 @@ +platform :ios, "5.0" + +pod "GoogleMediaFramework", :path => "../" +# Demo app shows how to integrate with the Google IMA SDK, but this is optional. +pod "GoogleAds-IMA-iOS-SDK", "~> 3.0.beta.4" diff --git a/GoogleMediaFrameworkDemo/Podfile.lock b/GoogleMediaFrameworkDemo/Podfile.lock new file mode 100644 index 0000000..6bc6dfd --- /dev/null +++ b/GoogleMediaFrameworkDemo/Podfile.lock @@ -0,0 +1,17 @@ +PODS: + - GoogleAds-IMA-iOS-SDK (3.0.beta.4) + - GoogleMediaFramework (0.1.0) + +DEPENDENCIES: + - GoogleAds-IMA-iOS-SDK (~> 3.0.beta.4) + - GoogleMediaFramework (from `../`) + +EXTERNAL SOURCES: + GoogleMediaFramework: + :path: ../ + +SPEC CHECKSUMS: + GoogleAds-IMA-iOS-SDK: e1c3ea14aaa20e58e23a66ff43e9d29b8ade1d31 + GoogleMediaFramework: 77ae270aab43d0a84508079aee8af008fef43fb4 + +COCOAPODS: 0.28.0 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6eb9431 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2013 Google, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..ccd6917 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +#Google Media Framework for iOS + +##Introduction +The Google Media Framework (GMF) is a lightweight media player designed to make video playback and integration with the Google IMA SDK on iOS easier. + +##Features +- A simple video player UI for video playback on iOS. +- Easily integrate the Google IMA SDK to enable advertising on your video content. + +##Getting started +[TODO: How to integrate using Cocoapods] + +##Where do I report issues? +Please report issues on the [issues page](../../issues). + +##Support +If you have questions about the framework, you can ask them at http://groups.google.com/d/forum/google-media-framework + +##How do I contribute? +See [this wiki article](../../wiki/Becoming-a-contributor) for details. + +##Requirements + - iOS 6.1+ + +##Authors + - gms@google.com (Geoff Stearns) Please direct questions or bug reports to the Github issues page. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..3c67d3d --- /dev/null +++ b/Rakefile @@ -0,0 +1,148 @@ +desc "Runs the specs [EMPTY]" +task :spec do + # Provide your own implementation +end + +task :version do + git_remotes = `git remote`.strip.split("\n") + + if git_remotes.count > 0 + puts "-- fetching version number from github" + sh 'git fetch' + + remote_version = remote_spec_version + end + + if remote_version.nil? + puts "There is no current released version. You're about to release a new Pod." + version = "0.1.0" + else + puts "The current released version of your pod is " + remote_spec_version.to_s() + version = suggested_version_number + end + + puts "Enter the version you want to release (" + version + ") " + new_version_number = $stdin.gets.strip + if new_version_number == "" + new_version_number = version + end + + replace_version_number(new_version_number) +end + +desc "Release a new version of the Pod" +task :release do + + puts "* Running version" + sh "rake version" + + unless ENV['SKIP_CHECKS'] + if `git symbolic-ref HEAD 2>/dev/null`.strip.split('/').last != 'master' + $stderr.puts "[!] You need to be on the `master' branch in order to be able to do a release." + exit 1 + end + + if `git tag`.strip.split("\n").include?(spec_version) + $stderr.puts "[!] A tag for version `#{spec_version}' already exists. Change the version in the podspec" + exit 1 + end + + puts "You are about to release `#{spec_version}`, is that correct? [y/n]" + exit if $stdin.gets.strip.downcase != 'y' + end + + puts "* Running specs" + sh "rake spec" + + puts "* Linting the podspec" + sh "pod lib lint" + + # Then release + sh "git commit #{podspec_path} CHANGELOG.md -m 'Release #{spec_version}'" + sh "git tag -a #{spec_version} -m 'Release #{spec_version}'" + sh "git push origin master" + sh "git push origin --tags" + sh "pod push master #{podspec_path}" +end + +# @return [Pod::Version] The version as reported by the Podspec. +# +def spec_version + require 'cocoapods' + spec = Pod::Specification.from_file(podspec_path) + spec.version +end + +# @return [Pod::Version] The version as reported by the Podspec from remote. +# +def remote_spec_version + require 'cocoapods-core' + + if spec_file_exist_on_remote? + remote_spec = eval(`git show origin/master:#{podspec_path}`) + remote_spec.version + else + nil + end +end + +# @return [Bool] If the remote repository has a copy of the podpesc file or not. +# +def spec_file_exist_on_remote? + test_condition = `if git rev-parse --verify --quiet origin/master:#{podspec_path} >/dev/null; + then + echo 'true' + else + echo 'false' + fi` + + 'true' == test_condition.strip +end + +# @return [String] The relative path of the Podspec. +# +def podspec_path + podspecs = Dir.glob('*.podspec') + if podspecs.count == 1 + podspecs.first + else + raise "Could not select a podspec" + end +end + +# @return [String] The suggested version number based on the local and remote version numbers. +# +def suggested_version_number + if spec_version != remote_spec_version + spec_version.to_s() + else + next_version(spec_version).to_s() + end +end + +# @param [Pod::Version] version +# the version for which you need the next version +# +# @note It is computed by bumping the last component of the versino string by 1. +# +# @return [Pod::Version] The version that comes next after the version supplied. +# +def next_version(version) + version_components = version.to_s().split("."); + last = (version_components.last.to_i() + 1).to_s + version_components[-1] = last + Pod::Version.new(version_components.join(".")) +end + +# @param [String] new_version_number +# the new version number +# +# @note This methods replaces the version number in the podspec file with a new version number. +# +# @return void +# +def replace_version_number(new_version_number) + text = File.read(podspec_path) + text.gsub!(/(s.version( )*= ")#{spec_version}(")/, "\\1#{new_version_number}\\3") + File.open(podspec_path, "w") { |file| file.puts text } +end diff --git a/Resources/player_control_close@2x.png b/Resources/player_control_close@2x.png new file mode 100644 index 0000000..2a0716a Binary files /dev/null and b/Resources/player_control_close@2x.png differ diff --git a/Resources/player_control_maximize@2x.png b/Resources/player_control_maximize@2x.png new file mode 100644 index 0000000..b5bb1f8 Binary files /dev/null and b/Resources/player_control_maximize@2x.png differ diff --git a/Resources/player_control_minimize@2x.png b/Resources/player_control_minimize@2x.png new file mode 100644 index 0000000..21d9498 Binary files /dev/null and b/Resources/player_control_minimize@2x.png differ diff --git a/Resources/player_control_pause@2x.png b/Resources/player_control_pause@2x.png new file mode 100644 index 0000000..21326a5 Binary files /dev/null and b/Resources/player_control_pause@2x.png differ diff --git a/Resources/player_control_play@2x.png b/Resources/player_control_play@2x.png new file mode 100644 index 0000000..7103c22 Binary files /dev/null and b/Resources/player_control_play@2x.png differ diff --git a/Resources/player_control_replay@2x.png b/Resources/player_control_replay@2x.png new file mode 100644 index 0000000..47e9203 Binary files /dev/null and b/Resources/player_control_replay@2x.png differ diff --git a/Resources/player_controls_background@2x.png b/Resources/player_controls_background@2x.png new file mode 100644 index 0000000..81c6467 Binary files /dev/null and b/Resources/player_controls_background@2x.png differ diff --git a/Resources/player_scrubber_thumb@2x.png b/Resources/player_scrubber_thumb@2x.png new file mode 100644 index 0000000..c30b918 Binary files /dev/null and b/Resources/player_scrubber_thumb@2x.png differ