Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions doc/api/Player_Events.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,34 @@ The object emitted as the following properties:
That is the real live position (and not the position as announced by the
video element).

### play

Emitted when the `RxPlayer`'s `videoElement` is no longer considered paused.

This event is generally triggered when and if the
[`play`](./Basic_Methods/play.md) method has succeeded.

Note that this event can be sent even if the [player's state](./Player_States.md)
doesn't currently allow playback, for example when in the `"LOADING"` or
`"BUFFERING"` states, among other.
It shouldn't be sent however when the player's state is `"STOPPED"` which is
when no content is loading nor loaded.

### pause

Emitted when the `RxPlayer`'s `videoElement` is now considered paused.

This event is triggered when and if the [`pause`](./Basic_Methods/play.md) method
has succeeded, when the content has ended or due to other rare occurences: for
example if we could not automatically play after a `"LOADING"` or `"RELOADING"`
state due to [the browser's autoplay policies](https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide).

Note that this event can be sent even if the [player's state](./Player_States.md)
doesn't currently allow playback, for example when in the `"LOADING"` or
`"BUFFERING"` states, among other.
It shouldn't be sent however when the player's state is `"STOPPED"` which is
when no content is loading nor loaded.

### seeking

Emitted when a "seek" operation (to "move"/"skip" to another position) begins
Expand Down
51 changes: 50 additions & 1 deletion src/core/api/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@ import MediaElementTrackChoiceManager from "./tracks_management/media_element_tr
import TrackChoiceManager from "./tracks_management/track_choice_manager";
import {
constructPlayerStateReference,
emitPlayPauseEvents,
emitSeekEvents,
isLoadedState,
// emitSeekEvents,
PLAYER_STATES,
} from "./utils";

Expand Down Expand Up @@ -956,6 +956,53 @@ class Player extends EventEmitter<IPublicAPIEvent> {
}
};

/**
* `TaskCanceller` allowing to stop emitting `"play"` and `"pause"`
* events.
* `null` when such events are not emitted currently.
*/
let playPauseEventsCanceller : TaskCanceller | null = null;

/**
* Callback emitting `"play"` and `"pause`" events once the content is
* loaded, starting from the state indicated in argument.
* @param {boolean} willAutoPlay - If `false`, we're currently paused.
*/
const triggerPlayPauseEventsWhenReady = (willAutoPlay: boolean) => {
if (playPauseEventsCanceller !== null) {
playPauseEventsCanceller.cancel(); // cancel previous logic
playPauseEventsCanceller = null;
}
playerStateRef.onUpdate((val, stopListeningToStateUpdates) => {
if (!isLoadedState(val)) {
return; // content not loaded yet: no event
}
stopListeningToStateUpdates();
if (playPauseEventsCanceller !== null) {
playPauseEventsCanceller.cancel();
}
playPauseEventsCanceller = new TaskCanceller();
playPauseEventsCanceller.linkToSignal(currentContentCanceller.signal);
if (willAutoPlay !== !videoElement.paused) {
Copy link
Collaborator Author

@peaBerberian peaBerberian May 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could also be written willAutoPlay === videoElement.paused but I though it was less understandable and that the double negation was, for once, more readable

// paused status is not at the expected value on load: emit event
if (videoElement.paused) {
this.trigger("pause", null);
} else {
this.trigger("play", null);
}
}
emitPlayPauseEvents(videoElement,
() => this.trigger("play", null),
() => this.trigger("pause", null),
currentContentCanceller.signal);
}, { emitCurrentValue: false, clearSignal: currentContentCanceller.signal });
};

triggerPlayPauseEventsWhenReady(autoPlay);
initializer.addEventListener("reloadingMediaSource", (payload) => {
triggerPlayPauseEventsWhenReady(payload.autoPlay);
});

/**
* `TaskCanceller` allowing to stop emitting `"seeking"` and `"seeked"`
* events.
Expand Down Expand Up @@ -3066,6 +3113,8 @@ interface IPublicAPIEvent {
availableTextTracksChange : IAvailableTextTrack[];
availableVideoTracksChange : IAvailableVideoTrack[];
decipherabilityUpdate : IDecipherabilityUpdateContent[];
play: null;
pause: null;
seeking : null;
seeked : null;
streamEvent : IStreamEvent;
Expand Down
26 changes: 26 additions & 0 deletions src/core/api/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,32 @@ export function emitSeekEvents(
}, { includeLastObservation: true, clearSignal: cancelSignal });
}

/**
* @param {HTMLMediaElement} mediaElement
* @param {function} onPlay - Callback called when a play operation has started
* on `mediaElement`.
* @param {function} onPause - Callback called when a pause operation has
* started on `mediaElement`.
* @param {Object} cancelSignal - When triggered, stop calling callbacks and
* remove all listeners this function has registered.
*/
export function emitPlayPauseEvents(
mediaElement : HTMLMediaElement | null,
onPlay: () => void,
onPause: () => void,
cancelSignal : CancellationSignal
) : void {
if (cancelSignal.isCancelled() || mediaElement === null) {
return ;
}
mediaElement.addEventListener("play", onPlay);
mediaElement.addEventListener("pause", onPause);
cancelSignal.register(() => {
mediaElement.removeEventListener("play", onPlay);
mediaElement.removeEventListener("pause", onPause);
});
}

/** Player state dictionnary. */
export const enum PLAYER_STATES {
STOPPED = "STOPPED",
Expand Down
159 changes: 159 additions & 0 deletions tests/integration/utils/launch_tests_for_content.js
Original file line number Diff line number Diff line change
Expand Up @@ -1375,16 +1375,36 @@ export default function launchTestsForContent(manifestInfos) {
});

describe("play", () => {
let pauseEventsSent = 0;
let playEventsSent = 0;
beforeEach(() => {
player.addEventListener("pause", () => {
pauseEventsSent++;
});
player.addEventListener("play", () => {
playEventsSent++;
});
});

afterEach(() => {
pauseEventsSent = 0;
playEventsSent = 0;
});

it("should begin to play if LOADED", async () => {
player.loadVideo({
url: manifestInfos.url,
transport,
});
await waitForLoadedStateAfterLoadVideo(player);
expect(player.getPlayerState()).to.equal("LOADED");
expect(pauseEventsSent).to.equal(0);
expect(playEventsSent).to.equal(0);
player.play();
await sleep(10);
expect(player.getPlayerState()).to.equal("PLAYING");
expect(pauseEventsSent).to.equal(0);
expect(playEventsSent).to.equal(1);
});

it("should resume if paused", async () => {
Expand All @@ -1396,16 +1416,38 @@ export default function launchTestsForContent(manifestInfos) {
await waitForLoadedStateAfterLoadVideo(player);
await sleep(100);
expect(player.getPlayerState()).to.equal("PLAYING");
expect(pauseEventsSent).to.equal(0);
expect(playEventsSent).to.equal(0);
player.pause();
await sleep(100);
expect(player.getPlayerState()).to.equal("PAUSED");
expect(pauseEventsSent).to.equal(1);
expect(playEventsSent).to.equal(0);
player.play();
await sleep(100);
expect(player.getPlayerState()).to.equal("PLAYING");
expect(pauseEventsSent).to.equal(1);
expect(playEventsSent).to.equal(1);
});
});

describe("pause", () => {
let pauseEventsSent = 0;
let playEventsSent = 0;
beforeEach(() => {
player.addEventListener("pause", () => {
pauseEventsSent++;
});
player.addEventListener("play", () => {
playEventsSent++;
});
});

afterEach(() => {
pauseEventsSent = 0;
playEventsSent = 0;
});

it("should have no effect when LOADED", async () => {
await tryTestMultipleTimes(
async function runTest(cancelTest) {
Expand All @@ -1432,6 +1474,8 @@ export default function launchTestsForContent(manifestInfos) {
await sleep(10);

expect(player.getPlayerState()).to.equal("LOADED");
expect(pauseEventsSent).to.equal(0);
expect(playEventsSent).to.equal(0);
},
3,
function cleanUp() {
Expand All @@ -1448,9 +1492,12 @@ export default function launchTestsForContent(manifestInfos) {
});
await waitForLoadedStateAfterLoadVideo(player);
expect(player.getPlayerState()).to.equal("PLAYING");
expect(pauseEventsSent).to.equal(0);
player.pause();
await sleep(100);
expect(player.getPlayerState()).to.equal("PAUSED");
expect(pauseEventsSent).to.equal(1);
expect(playEventsSent).to.equal(0);
});

it("should do nothing if already paused", async () => {
Expand All @@ -1461,12 +1508,17 @@ export default function launchTestsForContent(manifestInfos) {
});
await waitForLoadedStateAfterLoadVideo(player);
expect(player.getPlayerState()).to.equal("PLAYING");
expect(pauseEventsSent).to.equal(0);
player.pause();
await sleep(100);
expect(player.getPlayerState()).to.equal("PAUSED");
expect(pauseEventsSent).to.equal(1);
expect(playEventsSent).to.equal(0);
player.pause();
await sleep(100);
expect(player.getPlayerState()).to.equal("PAUSED");
expect(pauseEventsSent).to.equal(1);
expect(playEventsSent).to.equal(0);
});
});

Expand All @@ -1483,6 +1535,113 @@ export default function launchTestsForContent(manifestInfos) {
player.seekTo(minimumPosition + 50);
expect(player.getPosition()).to.be.closeTo(minimumPosition + 50, 0.5);
});

it("should conserve pause if previously paused", async () => {
let pauseEventsSent = 0;
let playEventsSent = 0;
player.addEventListener("pause", () => {
pauseEventsSent++;
});
player.addEventListener("play", () => {
playEventsSent++;
});
player.loadVideo({
url: manifestInfos.url,
transport,
});
await waitForLoadedStateAfterLoadVideo(player);
player.seekTo(minimumPosition + 50);
await waitForState(player, "PAUSED");
expect(pauseEventsSent).to.equal(0);
expect(playEventsSent).to.equal(0);
});

it("should still play if previously playing", async () => {
let pauseEventsSent = 0;
let playEventsSent = 0;
let nbPausedStates = 0;
player.addEventListener("pause", () => {
pauseEventsSent++;
});
player.addEventListener("play", () => {
playEventsSent++;
});
player.addEventListener("playerStateChange", (state) => {
if (state === "PAUSED") {
nbPausedStates++;
}
});
player.loadVideo({
url: manifestInfos.url,
transport,
autoPlay: true,
});
await waitForLoadedStateAfterLoadVideo(player);
player.seekTo(minimumPosition + 50);
await waitForState(player, "PLAYING");
expect(pauseEventsSent).to.equal(0);
expect(playEventsSent).to.equal(0);
expect(nbPausedStates).to.equal(0);
});

it("should be able to pause while seeking", async () => {
let pauseEventsSent = 0;
let playEventsSent = 0;
let nbPausedStates = 0;
player.addEventListener("pause", () => {
pauseEventsSent++;
});
player.addEventListener("play", () => {
playEventsSent++;
});
player.addEventListener("playerStateChange", (state) => {
if (state === "PAUSED") {
nbPausedStates++;
}
});
player.loadVideo({
url: manifestInfos.url,
transport,
autoPlay: true,
});
await waitForLoadedStateAfterLoadVideo(player);
player.seekTo(minimumPosition + 50);
await sleep(0);
player.pause();
await waitForState(player, "PAUSED");
expect(pauseEventsSent).to.equal(1);
expect(playEventsSent).to.equal(0);
expect(nbPausedStates).to.equal(1);
});

it("should be able to play while seeking", async () => {
let pauseEventsSent = 0;
let playEventsSent = 0;
let nbPlayingStates = 0;
player.addEventListener("pause", () => {
pauseEventsSent++;
});
player.addEventListener("play", () => {
playEventsSent++;
});
player.addEventListener("playerStateChange", (state) => {
if (state === "PLAYING") {
nbPlayingStates++;
}
});
player.loadVideo({
url: manifestInfos.url,
transport,
});
await waitForLoadedStateAfterLoadVideo(player);
player.seekTo(minimumPosition + 50);
await sleep(0);
player.play();
await waitForState(player, "PLAYING");
expect(pauseEventsSent).to.equal(0);
expect(playEventsSent).to.equal(1);
expect(nbPlayingStates).to.equal(1);
});
});

describe("getVolume", () => {
Expand Down