IBM / video-streaming-web-player-api

This is the public Web Player API for controlling embedded players created at video.ibm.com.
Apache License 2.0
5 stars 6 forks source link

Is there a way to detect the automatic "seek" on initial load, or the playback position that the player will seek to after the user presses play? #3

Closed AndrewFeeney closed 3 years ago

AndrewFeeney commented 3 years ago

I need to perform some operations on playback start which depend on the progress of the video content. To do this I have defined a listener for the 'playing' event. The listener function calls 'getProperty('progress', callback)` and the callback I pass in does what I need to do with the progress when fired.


this.player = PlayerAPI('my-player');

this.player.addListener('playing', (eventKey, isPlaying) => {
    if (!isPlaying) {
        return
    }

    this.player.getProperty('progress', (progress) => {
        // Do stuff with progress value
    })
 })

This is fine when the content is starting from zero, as the progress value passed to my playing listener is 0 immediately after playback is started. However if the user has previously started the stream, and is continuing again after reloading the page, the progress automatically "seeks" to the previous position after the user presses play.

If I'm understanding this correctly, there appears to be a race condition here where the playing listener fires and then requests the progress value. By the time the progress callback is fired with the progress value, the video has not yet "seeked" to the previous playback position, and the playback is still 0. However, shortly after that the video automatically "seeks" and subsequent calls to getProperty('progress', callback) result in the actual playback position which the player starts playing at after the user presses play.

I'm getting around this by introducing an arbitrary delay before fetching the progress with setTimeout()


this.player = PlayerAPI('my-player');

this.player.addListener('playing', (eventKey, isPlaying) => {
    if (!isPlaying) {
        return
    }

    setTimeout(() => this.player.getProperty('progress', (progress) => {
        // Do stuff with progress value
    }, 1000)
 })

This seems to work, but feels like a pretty brittle, since I have no way of knowing how long the video will take to automatically seek to the previous playback position. Another problem is that this is reducing the accuracy of the playback position, since I'm estimating the delay of the initial seek.

Am I missing something? Is there a better way to do this?

For example, could I detect that "seek" event somehow and only fetch progress after that. Alternatively could I somehow determine the initial playback time on page load?

AndrewFeeney commented 3 years ago

After playing with the source a bit, it looks as though there is an event called seekCompleted which I can add a listener for which will do what I need. This event is not documented, so I'm just wondering if it's safe to depend on it?

this.player = PlayerAPI('my-player');

this.player.addListener('seekCompleted', (eventKey, isPlaying) => {
    if (!isPlaying) {
        return
    }

    this.player.getProperty('progress', (progress) => {
        // Do stuff with progress value
    })
 })
jungdaniel commented 3 years ago

Hi @AndrewFeeney,

The initial seek that you described is the result of the "Continue watching" feature we introduced last year. Current implementation uses our internal seek api, but we might change that later. If you could disable this feature with a param would that solve your use-case?

The seekCompleted event was only for internal use so far, but I don't see why we couldn't add it to the documentation. In cases when we don't have saved positions from previous sessions, the initial seekCompleted event won't fire, so I think the best solution would be to create a dedicated event for detecting this initial seek. Something like initialSeekCompleted or initialPositionChange. We'll discuss this and I'll let you know.

AndrewFeeney commented 3 years ago

@jungdaniel Thanks so much for your response!

I like the continue watching feature, and would certainly prefer to keep it. In my latest iteration I'm treating any seek event as a discrete pause, and start (from a new location) and this simplifies things a little.

However, I am still having difficulty detecting the playback position at which the seek was initiated, and the new playback position after the seek is completed, as these values aren't passed with the seek event payload, and aren't necessarily updated chronologically before and after the seek events are fired as you might expect. This is important for my purposes as we are aiming to detect the watched portions of the content.

I guess in an ideal world there would be a seekStarted event which passes an isPlaying boolean as well as a playbackPosition parameter to it's callback, and the same for seekCompleted. Alternatively, you could pass the seekFromPosition and seekToPosition as parameters to the seekCompleted callback (as well as the isPlaying parameter).

Idea 1: Add new 'seekStarted' event, passing the playbackPosition into the callbacks of both 'seekStarted' and 'seekCompleted'


this.player.addListener('seekStarted', (eventKey, isPlaying, playbackPosition) => {
    // Now we don't need to get the 'progress' property, because we have an assuredly accurate playbackPosition for the last progress value before the seek began
    handleSeekStarted(playbackPosition, isPlaying);
})

 this.player.addListener('seekCompleted', (eventKey, isPlaying, playbackPosition) => {
    // Now we don't need to get the 'progress' property, because we have an assuredly accurate playbackPosition for the new progress value after the seek completed
    handleSeekCompleted(playbackPosition, isPlaying);
})

Idea 2: Pass the seekStartedPosition and seekCompletedPosition into the callback of 'seekCompleted'


this.player.addListener('seekStarted', (eventKey, isPlaying, seekStartedPosition, seekCompletedPosition) => {
    // Now we don't need to use any fancy tricks to estimate the playback position before and after the seek, because we have an assuredly accurate playbackPosition for both the last progress value before the seek began and the new progress value after the seek completed.
    handleSeek(seekStartedPosition, seekCompletedPosition, isPlaying);
})

I don't know if either of these are feasible to implement, because I don't have access to the actual player source. If there's any way I can help, I'd be happy to though.

Regarding initialSeekCompleted

For my purposes, if one of the above changes were to be implemented, I could probably get by without having a differentiation between the player initiated seek at the beginning and any subsequent user initiated seek events.

That said, I still think it would be a good idea to provide a discrete initialSeekCompleted event to differentiate between this player originated seek and the user originated seek. I've been doing a few of these player integrations lately, and differentiating between user initiated events, and system initiated events is a constant struggle which player libraries often don't consider.

jungdaniel commented 3 years ago

Thank you for the explanation and also for the suggestions. I really like the idea of passing seekStartedPosition and seekCompletedPosition. This issue is on our radar. Hopefully we'll be able to work on this this week.

AndrewFeeney commented 3 years ago

@jungdaniel I really appreciate the response! I look forward to seeing any changes you are able to come up with.

jungdaniel commented 3 years ago

@AndrewFeeney We introduced the seekStarted event as documented here: https://developers.video.ibm.com/player-api-usage#seekstarted Hopefully you will be able to filter out the initial seek based on the initiator property taking the value system. Let me know if that helped.

We also documented the seekCompleted event: https://developers.video.ibm.com/player-api-usage#seekcompleted

AndrewFeeney commented 3 years ago

Thanks for all your help @jungdaniel! I meant to come back and close this, but you beat me to it. Cheers!