10
votes

I'm using UIWebView to play YouTube videos in an iPad.

How can I detect when a YouTube video is finished playing? I see the play icon in the status bar and I tried to use MPMusicPlayerController notifications to detect playbackStateDidChange, but it didn't work.

Any ideas how to detect this event? Again, I'm talking about iPad not iPhone.

Thanks in advance.

Update:

If you use zero solution to detect the end of playing and also want the Youtube video to start automatically set the UIWebView to :

self.webView.mediaPlaybackRequiresUserAction = NO ;

I just want to clarify about the YouTube frame API:

"Important: This is an experimental feature, which means that it might change unexpectedly" (08/05/2012)

4

4 Answers

15
votes

No, there's no way to directly get web page event from UIWebView. But we can accomplish this by using Javascript.

  • First you use embed Javascript in your custom HTML, to detect video ending play event.
  • Then you try to load a scheme-customized request using JavaScript, and UIWebView could catch the request.

These links may help:

  1. Calling Objective-C from JavaScript in an iPhone UIWebView

  2. Javascript callback in Objective-C

  3. YouTube iFrame API

updated with an example:

in UIWebView's delegate, i put:

#pragma - mark WebView Delegate
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {


if ( [[[request URL] scheme] isEqualToString:@"callback"] ) {

    NSLog(@"get callback");

    return NO;
}

return YES;

}

the web page is load when viewDidLoad:

[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:[[NSBundle       mainBundle] pathForResource:@"youtube" ofType:@"html" ]]]];

and in youtube.html i put:

<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 = "http://www.youtube.com/player_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 onYouTubePlayerAPIReady() {
    player = new YT.Player('player', {
      height: '390',
      width: '640',
      videoId: 'u1zgFlCw8Aw',
      events: {
        'onReady': onPlayerReady,
        'onStateChange': onPlayerStateChange
      }
    });
  }

  // 4. The API will call this function when the video player is ready.
  function onPlayerReady(event) {
    event.target.playVideo();
  }

  // 5. The API calls this function when the player's state changes.
  //    The function indicates that when playing a video (state=1),
  //    the player should play for six seconds and then stop.
  var done = false;
  function onPlayerStateChange(event) {

    if (event.data == YT.PlayerState.PLAYING && !done) {
      setTimeout(stopVideo, 6000);
      done = true;
    }
    if (event.data == YT.PlayerState.ENDED) {
      window.location = "callback:anything"; //here's the key
    };
  }
  function stopVideo() {
    player.stopVideo();
  }
</script>
</body>
</html>

you can see i add

if (event.data == YT.PlayerState.ENDED) {
      window.location = "callback:anything";
    };

to YouTube's iFrame API demo, and it catches the player end playing event and try to load a request with scheme "callback", then the UIWebView Delegate could catch it.

You can use this method to trigger any event using JavaScript;

11
votes

please refere to:

https://developer.apple.com/library/ios/#documentation/AVFoundation/Reference/AVPlayerItem_Class/Reference/Reference.html

there is an notification available iOS 4.0 , which you can use to detect youtube video finish playing

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

Here is @zero's fantastic answer as a full-fledged class for your use:

@interface YouTubeWebView () <UIWebViewDelegate>

@end


@implementation YouTubeWebView 

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self == nil) return nil;

    self.mediaPlaybackRequiresUserAction = NO;
    self.delegate = self;
    self.alpha = 0;

    return self;
}

- (void)loadVideo:(NSString *)videoId
{
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"youtube" ofType:@"html"];
    //    if (filePath == nil)

    NSError *error;
    NSString *string = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:&error];
    // TODO: error check

    string = [NSString stringWithFormat:string, videoId];

    NSData *htmlData = [string dataUsingEncoding:NSUTF8StringEncoding];
    //    if (htmlData == nil)

    NSString *documentsDirectoryPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
    NSString *targetPath = [documentsDirectoryPath stringByAppendingPathComponent:@"youtube.html"];
    [htmlData writeToFile:targetPath atomically:YES];

    [self loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:targetPath]]];
}

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    if ([[[request URL] scheme] isEqualToString:@"callback"]) {
        [self removeFromSuperview];

        NSString *documentsDirectoryPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
        NSString *targetPath = [documentsDirectoryPath stringByAppendingPathComponent:@"youtube.html"];
        NSError *error;
        [[NSFileManager defaultManager] removeItemAtPath:targetPath error:&error];
//        TODO: error check
    }

    return YES;
}

Just use this version of youtube.html, which has a substitution code (%@) in place of the video id:

<html>
<head><style>body{margin:0px 0px 0px 44px;}</style></head>
<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 = "http://www.youtube.com/player_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 onYouTubePlayerAPIReady() {
    player = new YT.Player('player', {
      height: '320',
      width: '480',
      videoId: '%@',
      events: {
        'onReady': onPlayerReady,
        'onStateChange': onPlayerStateChange
      }
    });
  }

  // 4. The API will call this function when the video player is ready.
  function onPlayerReady(event) {
    event.target.playVideo();
  }

  // 5. The API calls this function when the player's state changes.
  //    The function indicates that when playing a video (state=1),
  //    the player should play for six seconds and then stop.
  var done = false;
  function onPlayerStateChange(event) {
    if (event.data == YT.PlayerState.PLAYING && !done) {
      setTimeout(stopVideo, 6000);
      done = true;
    }
    if (event.data == YT.PlayerState.ENDED) {
      window.location = "callback:anything"; 
    };
  }
  function stopVideo() {
    player.stopVideo();
  }
</script>
</body>
</html>

The only major obstacle I had to overcome to implement this was loading the file as a string to make the substitution. Unfortunately it has to be written again as a file for autoplay to work. If thats not necessary for your use case, feel free to load the HTML into the web view directly as a string instead.

0
votes

Here's a Swift 3 answer

   NotificationCenter.default.addObserver(
    self,
    selector: #selector(self.videoEnded),
    name: .AVPlayerItemDidPlayToEndTime,
    object: nil)


}

func videoEnded(){
    print("video ended")
}