10

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)

Tagc
  • 8,736
  • 7
  • 61
  • 114
user1105951
  • 2,259
  • 2
  • 34
  • 55

4 Answers4

15

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;

Community
  • 1
  • 1
ZeR0
  • 331
  • 3
  • 6
  • But it's depends if the youtube player support such event, no ? Did you succeed doing that ? – user1105951 May 07 '12 at 12:53
  • @user1105951 yes, youtube player support end event called "onStateChange", and you shall add a listener to it follow YouTube JavaScript API [YouTube JavaScript API - Events](https://developers.google.com/youtube/js_api_reference#Events) – ZeR0 May 07 '12 at 15:46
  • @Zero, I check your link, and I remember alreay testing it. try to run this demo https://developers.google.com/youtube/youtube_player_demo -ON THE IPAD- safari web browser, and you will see : "you need flash player 9+ and javascript enabled to view this video". – user1105951 May 07 '12 at 19:43
  • @user1105951 safari on iOS devices don't support flash, so you need to follow YouTube's iFrame API to load HTML5 player. Sorry to mislead, so i update my answer giving a simple example which i wrote and tested. it worked well. hope it will help. – ZeR0 May 08 '12 at 03:44
  • @Zero, works like a charm ! and I learn also about cool band :D ( metal rules) http://www.youtube.com/watch?v=zi5AE9LKX04 Thanks ! – user1105951 May 08 '12 at 10:23
  • just want to clarify about the youtube frame api : "Important: This is an experimental feature, which means that it might change unexpectedly" – user1105951 May 08 '12 at 12:21
  • @zero, in your sample code did you manage to make the youtube video start play automatically ? – user1105951 May 08 '12 at 12:40
  • @zero got it :self.webView.mediaPlaybackRequiresUserAction = NO ;(for start auto play). good day. – user1105951 May 08 '12 at 12:57
  • @zero - I tried the above code but the WebView does not show the initial video image. I have also tried with the iFrame API . In both cases the javascript code never gets called. IFrame html page - https://developers.google.com/youtube/iframe_api_reference – Salil Dec 04 '12 at 11:11
  • As I person learning Xcode it took me a while that the delegate has to be added to the viewcontroller.h file. Like this @interface MahViewController : UIViewController and lastly the viewController.m needs [self.yourWebView setDelegate:self]; on the ViewDidLoad. – Ives.me Jun 04 '14 at 06:42
11

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];
João Nunes
  • 3,751
  • 31
  • 32
chings228
  • 1,859
  • 24
  • 24
  • it 's good to reload the youtube page when video finish , otherwise , it will show related video provided by youtube – chings228 May 10 '13 at 02:43
  • 3
    Don't waste time trying other solutions. This one works like charm and it's only a line of code. Anyway, this is the observer being called, just for reference - (void)youTubePlayed:(id)sender – Alejandro Luengo Sep 06 '13 at 10:23
  • 2
    This only works if the user plays the video to the end and then pressed the Done button. If they haven't finished playing video to the end, this notification will not be called. – Enrico Susatyo Dec 08 '14 at 05:10
  • Great help !!! UIWebView doesn't even provide a replay button for the embedded video. The only (simple) solution available to avoid the "related videos" at the end, by reloading the video in youTubePlayed:(id); – cirronimbo Sep 07 '15 at 12:37
3

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.

Ilias Karim
  • 4,798
  • 3
  • 38
  • 60
  • why don't you pass the video id as a parameter to your javascript function and call that function to load the video? – João Nunes Jul 21 '15 at 13:27
0

Here's a Swift 3 answer

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


}

func videoEnded(){
    print("video ended")
}
JP Aquino
  • 3,946
  • 1
  • 23
  • 25