How to receive NSNotifications from UIWebView embedded YouTube video playback
Asked Answered
A

9

51

I didn't received any notifications for MPMoviePlayerController. What am I doing wrong?

I use following logic.

I'm begining to play youtube video in UIWebView. UIWebView calls a standard MPMoviePlayerController. I don't control MPMoviePlayerController because I didn't instantiate MPMoviePlayerController.

I run youtube's clip with autoplay (1 second delay):

[self performSelector:@selector(touchInView:) withObject:b afterDelay:1];

My code is:

- (void)viewDidLoad
{
    [super viewDidLoad];

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(loadStateDidChange:) name:MPMoviePlayerLoadStateDidChangeNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackDidFinish:) name:MPMoviePlayerDidExitFullscreenNotification object:nil];

    [self embedYouTube];
}

- (void)loadStateDidChange:(NSNotification*)notification
{
    NSLog(@"________loadStateDidChange");
}

- (void)playbackDidFinish:(NSNotification*)notification
{
    NSLog(@"________DidExitFullscreenNotification");
}

- (void)embedYouTube
{
    CGRect frame = CGRectMake(25, 89, 161, 121);
    NSString *urlString = [NSString stringWithString:@"http://www.youtube.com/watch?v=sh29Pm1Rrc0"];

    NSString *embedHTML = @"<html><head>\
    <body style=\"margin:0\">\
    <embed id=\"yt\" src=\"%@\" type=\"application/x-shockwave-flash\" \
    width=\"%0.0f\" height=\"%0.0f\"></embed>\
    </body></html>";
    NSString *html = [NSString stringWithFormat:embedHTML, urlString, frame.size.width, frame.size.height];
    UIWebView *videoView = [[UIWebView alloc] initWithFrame:frame];
    videoView.delegate = self;

    for (id subview in videoView.subviews)
        if ([[subview class] isSubclassOfClass: [UIScrollView class]])
            ((UIScrollView *)subview).bounces = NO;

            [videoView loadHTMLString:html baseURL:nil];
    [self.view addSubview:videoView];
    [videoView release];
}

- (void)webViewDidFinishLoad:(UIWebView *)_webView 
{
    UIButton *b = [self findButtonInView:_webView];
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(touchInView:) object:b];
    [self performSelector:@selector(touchInView:) withObject:b afterDelay:1];
}

- (UIButton *)findButtonInView:(UIView *)view 
{
    UIButton *button = nil;

    if ([view isMemberOfClass:[UIButton class]]) {
        return (UIButton *)view;
    }

    if (view.subviews && [view.subviews count] > 0) 
    {
        for (UIView *subview in view.subviews) 
        {
            button = [self findButtonInView:subview];
            if (button) return button;
        }
    }
    return button;
}

- (void)touchInView:(UIButton*)b
{
    [b sendActionsForControlEvents:UIControlEventTouchUpInside];
}

UPDATE: I'm creating application that plays youtube's video. You can run playlist and you will see first video. When first video has ended, second video begins play automatically and so on.

I need to support ios 4.1 and above.

UPDATE2: @H2CO3 I'm trying to use your url-scheme, but it don't works. Delegate method didn't called on exit event. I added my html url to log. It is:

<html><head>    <body style="margin:0">    
<script>function endMovie() 
{document.location.href="somefakeurlscheme://video-ended";} 
 </script>      <embed id="yt" src="http://www.youtube.com/watch?v=sh29Pm1Rrc0"        
 onended="endMovie()" type="application/x-shockwave-flash"  
 width="161" height="121"></embed>  
 </body></html>

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
  if ([[[request URL] absoluteString] hasPrefix:@"somefakeurlscheme://video-ended"]) 
  {
    [self someMethodSupposedToDetectVideoEndedEvent];
    return NO; // prevent really loading the URL
   }
  return YES; // else load the URL as desired
}

UPDATE3 @Till, I cann't caught UIMoviePlayerControllerDidExitFullscreenNotification, but I found MPAVControllerItemPlaybackDidEndNotification. MPAVControllerItemPlaybackDidEndNotification appears when playback video is ended.

But I don't understand how do I catch onDone notifications?

Apparitor answered 15/12, 2011 at 10:39 Comment(1)
Your initial assumption is incorrect. UIWebView does not use the standard MPMoviePlayerController for playback.Pollen
P
64

There are no documented notifications sent by the UIWebView embedded movie player.

In fact, the closed implementation used within the UIWebView does differ from the public MPMoviePlayerController in many aspects (e.g. DRM).

The most important classes used for playing video content within that UIWebView are called MPAVController and UIMoviePlayerController. The latter one makes the player appear like the MPMoviePlayerController fullscreen interface.

In case you dare to risk a rejection by Apple, there are actually ways to still achieve what you are looking for.

NOTE This is not documented and is subject to break on each and every new iOS release. It does however work on iOS4.3, 5.0 and 5.01, 5.1 and 6.0 and it may work on other versions as well.

I am not able to test this solution on iOS 4.1 and 4.2, so that is up to you to do. I highly suspect that it will work.


Fullscreen State

If, for example you are intending to react upon the user tapping the DONE button, you may be able to do it this way:

UPDATE The old version of this answer recommended to use UIMoviePlayerControllerDidExitFullscreenNotification whereas this new version (updated for iOS6) recommends using UIMoviePlayerControllerWillExitFullscreenNotification.

C-Language Level:

void PlayerWillExitFullscreen (CFNotificationCenterRef center,
                 void *observer,
                 CFStringRef name,
                 const void *object,
                 CFDictionaryRef userInfo)
{
    //do something...
}

CFNotificationCenterAddObserver(CFNotificationCenterGetLocalCenter(), 
    NULL, 
    PlayerWillExitFullscreen, 
    CFSTR("UIMoviePlayerControllerWillExitFullscreenNotification"), 
    NULL,  
    CFNotificationSuspensionBehaviorDeliverImmediately);

Objective-C Level:

- (void)playerWillExitFullscreen:(NSNotification *)notification
{
    //do something...
}

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(playerWillExitFullscreen:)
                                             name:@"UIMoviePlayerControllerWillExitFullscreenNotification" 
                                           object:nil];

I did draft both, C-Level and Objective-C-Level options because the best way to actually find out about all of this is to use C-Level (CoreFoundation) functions as shown at the end of my answer. If the sender of a notification does not use Objective-C (NSNotifications), you may actually not be able to trap them using the NSNotification-mechanics.


Playback State

For examining the playback state, look out for "MPAVControllerPlaybackStateChangedNotification" (as drafted above) and examine the userInfo which may look like this:

{
    MPAVControllerNewStateParameter = 1;
    MPAVControllerOldStateParameter = 2;
}

Further Reverse Engineering

For reverse engineering and exploring all the notifications sent, use the following snippet.

void MyCallBack (CFNotificationCenterRef center,
                 void *observer,
                 CFStringRef name,
                 const void *object,
                 CFDictionaryRef userInfo)
{
    NSLog(@"name: %@", name);
    NSLog(@"userinfo: %@", userInfo);
}

CFNotificationCenterAddObserver(CFNotificationCenterGetLocalCenter(), 
    NULL, 
    MyCallBack, 
    NULL, 
    NULL,  
    CFNotificationSuspensionBehaviorDeliverImmediately);
Pollen answered 18/12, 2011 at 19:2 Comment(5)
The standard notification syntax also worked for me: [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(movieStartedPlaying:) name:@"UIMoviePlayerControllerDidEnterFullscreenNotification" object:nil]; Also on pre-4.0 iOS, the movie opens in its own modal controller and does not fire these events. On the plus side onViewDidAppear will be called on the underlying controller.Leland
Was this approach used? If yes, was it approved by appstore ?Lobectomy
@Lobectomy it most definitely gets approved as there is no private API being invoked. But then again, they might suddenly change their mind.Pollen
Using undocumented notification very certainly is private API, but nothing Apple would detect easily. For the playback state changes, better use UIMovieViewPlaybackStateDidChangeNotification.Medin
@Pollen Hi if we want to hide the default controls of player and add some custom controls how to do this? any idea ?Gloriagloriana
L
32

In iOS 4.3+ you can use the UIMoviePlayerControllerDidEnterFullscreenNotification and UIMoviePlayerControllerDidExitFullscreenNotification notifications:

-(void)viewDidLoad
{

    ...

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(youTubeStarted:) name:@"UIMoviePlayerControllerDidEnterFullscreenNotification" object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(youTubeFinished:) name:@"UIMoviePlayerControllerDidExitFullscreenNotification" object:nil];
}

-(void)youTubeStarted:(NSNotification *)notification{
    // your code here
}

-(void)youTubeFinished:(NSNotification *)notification{
    // your code here
}
Lorettelorgnette answered 31/1, 2012 at 23:41 Comment(4)
Thank you so much, but do you know where is this documented? I am unable to find anything, and need something similar to a UIMoviePlayerController Will EnterFullscreenNotification.Odoric
It is undocumented. Apparently there are UIMoviePlayerControllerWillEnterFullscreenNotification and WillExit notifications in iOS 6 now but I have not tested these. See the accepted answer to this question (by Till) as he has updated it with a more detailed explanation.Lorettelorgnette
best solution but one question if i want to stop player to play video then how can i do that?Gloriagloriana
So, what is the solution for iOS8 ? @RakeshBhattChancelor
I
16

As far as I know, the implementation details of UIWebView (and all system classes made by Apple) are not to be relied upon when making a Cocoa Touch application. Maybe it's the case that an UIWebView's video player is not a standard MPMoviePlayerController class and it might have a totally different delegation/notification system, which is not supposed to be accessible by the user.

I suggest you to use the HTML5 element and detect the "onended" event of this tag:

<html>
    <body>
        <script>
function endMovie() {
    // detect the event here
    document.location.href="somefakeurlscheme://video-ended";
}
        </script>
        <video src="http://youtube.com/watch?v=aiugvdk755f" onended="endMovie()"></video>
    </body>
</html>

In fact, from the endMovie JavaScript function, you can redirect to a bogus URL which you can catch in your -webView:shouldStartLoadWithRequest: (UIWebViewDelegate) method thus get notified that the video has ended:

- (BOOL) webView:(UIWebView *)wv shouldStartLoadWithRequest:(NSURLRequest *)req {
    if ([[[req URL] absoluteString] hasPrefix:@"somefakeurlscheme://video-ended"]) {
        [self someMethodSupposedToDetectVideoEndedEvent];
        return NO; // prevent really loading the URL
    }
    return YES; // else load the URL as desired
}

Hope this helps.

Impervious answered 18/12, 2011 at 19:7 Comment(6)
+1 I like the idea of using a url-scheme to make this callback work. So now it entirely depends on if UIWebView correctly handles onended.Pollen
@H2CO3, Your method didn't worked. Please see my updated question.Apparitor
@Apparitor because you did it wrong. I told you to use the <video> element, not the <embed> element (which, opposed to <video>, doesn't have the onEnded event associated with it).Impervious
Hello, this idea is fairly brilliant I must say.. however... the Youtube video's are not embedded (not any) through the use of your code, they all end up being an video element with an stripe through the play button. So you cant click it. -- Has anyone found a solution for this?Richly
It will not work for all youtube/Vimeo videos right ? Only HTML supported formats ?Otolith
Your use of the fake url scheme helped a lot. However I couldn't use the video tag for the Youtube videos so I had to use the new iframe API from Youtube to get the on end event.Mercurio
B
6

Based on the @H2CO3 answer but with the iframe API. It was the only way I could make it work.

This doesn't use any private API which makes it more future proof.

Here's the code to embed your Youtube video. Check the API for more ways to customise this.

<html>
  <body>
  <!-- 1. The <iframe> (and video player) will replace this <div> tag. -->
  <div id="player"></div>

  <script>
  // 2. This code loads the IFrame Player API code asynchronously.
    var tag = document.createElement('script');

    tag.src = "https://www.youtube.com/iframe_api";
    var firstScriptTag = document.getElementsByTagName('script')[0];
    firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
    // 3. This function creates an <iframe> (and YouTube player)
    //    after the API code downloads.
    var player;
    function onYouTubeIframeAPIReady() {
      player = new YT.Player('player', {
        height: '480',
        width: '640',
        videoId: 'aiugvdk755f',
        events: {
          'onStateChange': onPlayerStateChange
        }
      });
    }
    // 5. The API calls this function when the player's state changes.
    function onPlayerStateChange(event) {
      if (event.data == YT.PlayerState.ENDED) {
        endedMovie();
      }
    }
    function endedMovie() {
      // detect the event here
      document.location.href="somefakeurlscheme://video-ended";
    }
  </script>
  </body>
</html>

And this is how you get notified that the video ended (UIWebViewDelegate method).

- (BOOL) webView:(UIWebView *)wv shouldStartLoadWithRequest:(NSURLRequest *)req {
    if ([[[req URL] absoluteString] hasPrefix:@"somefakeurlscheme://video-ended"]) {
        [self someMethodSupposedToDetectVideoEndedEvent];
        return NO; // prevent really loading the URL
    }
    return YES; // else load the URL as desired
 }
Briolette answered 24/9, 2013 at 14:39 Comment(0)
B
5

in ViewDidLoad add the following code

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(VideoExitFullScreen:) name:@"UIMoviePlayerControllerDidExitFullscreenNotification" object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(VideoEnterFullScreen:) name:@"UIMoviePlayerControllerDidEnterFullscreenNotification" object:nil];

The following methods are for showing the message/functions for respective process of entering/exiting to/from full screen

- (void)VideoExitFullScreen:(id)sender{
// Your respective content/function for Exit from full screen
}

- (void)VideoEnterFullScreen:(id)sender{
// Your respective content/function for Enter to full screen
}
Bewilder answered 29/10, 2013 at 5:5 Comment(0)
F
4

This works for me in iOS 6.1, it hides/removes other windows when the AVPlayerItemDidPlayToEndTimeNotification is received:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerItemEnded:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];

...

- (void)playerItemEnded:(NSNotification *)notification
{    
    for (UIWindow *window in [[UIApplication sharedApplication] windows]) {
        if (window != self.window) {
            window.hidden = YES;
        }
    }
}
Fug answered 13/2, 2013 at 16:56 Comment(1)
This only works when the user plays the video to the end. If they only play for a few seconds then pressed Done, this will not be called.Employment
I
3
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(youTubeStarted:) name:UIWindowDidBecomeVisibleNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(youTubeFinished:) name:UIWindowDidBecomeHiddenNotification object:nil];


-(void)youTubeStarted:(NSNotification *)notification
 {
   // Entered Fullscreen code goes here..
   AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
   appDelegate.fullScreenVideoIsPlaying = YES;
   NSLog(@"%f %f",webViewForWebSite.frame.origin.x,webViewForWebSite.frame.origin.y);

 }

 -(void)youTubeFinished:(NSNotification *)notification{
   // Left fullscreen code goes here...
   AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
   appDelegate.fullScreenVideoIsPlaying = NO;

   //CODE BELOW FORCES APP BACK TO PORTRAIT ORIENTATION ONCE YOU LEAVE VIDEO.
   [[UIApplication sharedApplication] setStatusBarOrientation:UIInterfaceOrientationPortrait animated:NO];
   //present/dismiss viewcontroller in order to activate rotating.
   UIViewController *mVC = [[UIViewController alloc] init];
   [self presentViewController:mVC animated:NO completion:Nil];
   //  [self presentModalViewController:mVC animated:NO];
   [self dismissViewControllerAnimated:NO completion:Nil];
   //   [self dismissModalViewControllerAnimated:NO];

}
Integration answered 7/10, 2014 at 5:12 Comment(0)
S
1

For iOS8 (Also I have an embedded video that is not a youtube video) the only solution I could get to work was to catch either one of viewWill/DidLayoutSubviews, and as an added bonus you don't need to change the HTML or use any private APIs :

So basically:

@property (nonatomic) BOOL showingVideoFromWebView;
...
...

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
 navigationType:(UIWebViewNavigationType)navigationType {
    if (navigationType == UIWebViewNavigationTypeOther) {
        //Was "other" in my case... Might be UIWebViewNavigationTypeLinkClicked
        self.showingVideoFromWebView = YES;
    }
}

- (void)viewWillLayoutSubviews {
    [super viewWillLayoutSubviews];
    // Do whatever...
    // Note: This will get called both when video is entering fullscreen AND exiting!
    self.showingVideoFromWebView = NO;
}

In my case my web view is inside a UITableViewCell so I had to find a way to communicate between the cell and the view controller, and to also avoid using a BOOL flag I did this:

- (BOOL)webView:(UIWebView *)webView shouldStartLoad.....
... if (opening video check....) {
    [[NSNotificationCenter defaultCenter] addObserverForName:@"webViewEmbedVidChangedState" object:nil queue:nil usingBlock:^(NSNotification *note) {
        // Do whatever need to be done when the video is either 
        // entering fullscreen or exiting fullscreen....
        [[NSNotificationCenter defaultCenter] removeObserver:self name:@"webViewEmbedVidChangedState" object:nil];
    }];
}

- (void)viewWillLayoutSubviews.....
    [[NSNotificationCenter defaultCenter] postNotificationName:@"webViewEmbedVidChangedState" object:nil];
Semiautomatic answered 27/10, 2014 at 14:35 Comment(2)
as far as I remember, all solutions were unsafe, so in order to make it reliable just changed logic in the way that I don't need that.Metheglin
Can this way get a pause notification from the video player?Soothsayer
C
1

Actually for the reverse engineering purposes you can also use Cocoa API like

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

In this case you will receive all notifications

Chemical answered 1/12, 2015 at 12:40 Comment(2)
This is a late answer to an old question. What does your answer provide that the others do not?Laureenlaurel
@Laureenlaurel As mentioned here you can reverse engineer notifications using Core foundation, But I think most of iOS developers likes Foundation more =)Chemical

© 2022 - 2024 — McMap. All rights reserved.