Skip to content

Commit

Permalink
fix(player): improve looping behaviour
Browse files Browse the repository at this point in the history
  • Loading branch information
mihar-22 committed Dec 20, 2023
1 parent 5c4c359 commit 23fd34f
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 38 deletions.
23 changes: 11 additions & 12 deletions packages/vidstack/src/core/state/media-request-manager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { effect, peek, type ReadSignal } from 'maverick.js';
import { isUndefined } from 'maverick.js/std';
import { DOMEvent, isUndefined } from 'maverick.js/std';

import {
FullscreenController,
Expand Down Expand Up @@ -103,9 +103,9 @@ export class MediaRequestManager extends MediaPlayerController implements MediaR
async _play(trigger?: Event) {
if (__SERVER__) return;

const { canPlay, paused, ended, autoplaying, seekableStart } = this.$state;
const { canPlay, paused, autoplaying } = this.$state;

if (!peek(paused)) return;
if (!peek(paused) && !this._request._looping) return;

if (trigger?.type === 'media-play-request') {
this._request._queue._enqueue('play', trigger as RE.MediaPlayRequestEvent);
Expand All @@ -114,11 +114,6 @@ export class MediaRequestManager extends MediaPlayerController implements MediaR
try {
const provider = peek(this._$provider);
throwIfNotReadyForPlayback(provider, peek(canPlay));

if (peek(ended)) {
provider!.setCurrentTime(seekableStart() + 0.1);
}

return await provider!.play();
} catch (error) {
if (__DEV__) {
Expand Down Expand Up @@ -171,7 +166,8 @@ export class MediaRequestManager extends MediaPlayerController implements MediaR
const provider = peek(this._$provider);
throwIfNotReadyForPlayback(provider, peek(canPlay));

provider!.setCurrentTime(liveSyncPosition() ?? seekableEnd() - 2);
const end = seekableEnd() - 2;
provider!.setCurrentTime(Math.min(end, liveSyncPosition() ?? end));
}

private _wasPIPActive = false;
Expand Down Expand Up @@ -452,7 +448,6 @@ export class MediaRequestManager extends MediaPlayerController implements MediaR
await this._play(event);
} catch (e) {
this._request._looping = false;
this._request._replaying = false;
}
}

Expand Down Expand Up @@ -537,14 +532,18 @@ export class MediaRequestManager extends MediaPlayerController implements MediaR
}

['media-seek-request'](event: RE.MediaSeekRequestEvent) {
const { seekableStart, seekableEnd, ended, canSeek, live, userBehindLiveEdge } = this.$state;
const { seekableStart, seekableEnd, ended, canSeek, live, userBehindLiveEdge, clipStartTime } =
this.$state;

if (ended()) this._request._replaying = true;

this._request._seeking = false;
this._request._queue._delete('seeking');

const boundTime = Math.min(Math.max(seekableStart() + 0.1, event.detail), seekableEnd() - 0.1);
const boundTime = Math.min(
Math.max(seekableStart() + 0.1, event.detail + clipStartTime()),
seekableEnd() - 0.1,
);

if (!Number.isFinite(boundTime) || !canSeek()) return;

Expand Down
100 changes: 74 additions & 26 deletions packages/vidstack/src/core/state/media-state-manager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import debounce from 'just-debounce-it';
import throttle from 'just-throttle';
import { onDispose } from 'maverick.js';
import { onDispose, peek } from 'maverick.js';
import { DOMEvent, listenEvent } from 'maverick.js/std';

import { ListSymbol } from '../../foundation/list/symbols';
Expand Down Expand Up @@ -34,6 +34,7 @@ import { TRACKED_EVENT } from './tracked-media-events';
export class MediaStateManager extends MediaPlayerController {
private readonly _trackedEvents = new Map<string, ME.MediaEvent>();

private _clipEnded = false;
private _firingWaiting = false;
private _waitingTrigger: Event | undefined;

Expand Down Expand Up @@ -92,6 +93,7 @@ export class MediaStateManager extends MediaPlayerController {

private _resetTracking() {
this._stopWaiting();
this._clipEnded = false;
this._request._replaying = false;
this._request._looping = false;
this._firingWaiting = false;
Expand Down Expand Up @@ -358,28 +360,28 @@ export class MediaStateManager extends MediaPlayerController {
}

protected _onCanPlayDetail(detail: ME.MediaCanPlayDetail) {
const { seekable, seekableEnd, buffered, duration, canPlay } = this.$state;
const { seekable, seekableEnd, buffered, intrinsicDuration, canPlay } = this.$state;
canPlay.set(true);
buffered.set(detail.buffered);
seekable.set(detail.seekable);
duration.set(seekableEnd());
intrinsicDuration.set(seekableEnd());
}

['duration-change'](event: ME.MediaDurationChangeEvent) {
const { live, duration } = this.$state,
const { live, intrinsicDuration } = this.$state,
time = event.detail;
if (!live()) duration.set(!Number.isNaN(time) ? time : 0);
if (!live()) intrinsicDuration.set(!Number.isNaN(time) ? time : 0);
}

['progress'](event: ME.MediaProgressEvent) {
const { buffered, seekable, live, duration, seekableEnd } = this.$state,
const { buffered, seekable, live, intrinsicDuration, seekableEnd } = this.$state,
detail = event.detail;

buffered.set(detail.buffered);
seekable.set(detail.seekable);

if (live()) {
duration.set(seekableEnd);
intrinsicDuration.set(seekableEnd);
this.dispatch('duration-change', {
detail: seekableEnd(),
trigger: event,
Expand All @@ -391,13 +393,15 @@ export class MediaStateManager extends MediaPlayerController {
const { paused, autoplayError, ended, autoplaying, playsinline, pointer, muted, viewType } =
this.$state;

event.autoplay = autoplaying();
this._resetPlaybackIfNeeded();

if (this._request._looping || !paused()) {
if (!paused() && !this._request._looping) {
event.stopImmediatePropagation();
return;
}

event.autoplay = autoplaying();

const waitingEvent = this._trackedEvents.get('waiting');
if (waitingEvent) event.triggers.add(waitingEvent);

Expand Down Expand Up @@ -427,6 +431,33 @@ export class MediaStateManager extends MediaPlayerController {
if (!playsinline() && viewType() === 'video' && pointer() === 'coarse') {
this._media.remote.enterFullscreen('prefer-media', event);
}

if (this._request._looping) {
event.stopImmediatePropagation();
}
}

private _resetPlaybackIfNeeded(trigger?: Event) {
const provider = peek(this._media.$provider);
if (!provider) return;

const { ended, seekableStart, clipStartTime, clipEndTime, realCurrentTime, duration } =
this.$state;

const shouldReset =
realCurrentTime() < clipStartTime() ||
(clipEndTime() > 0 && realCurrentTime() >= clipEndTime()) ||
Math.abs(realCurrentTime() - duration()) < 0.1 ||
ended();

if (shouldReset) {
this.dispatch('media-seek-request', {
detail: (clipStartTime() > 0 ? 0 : seekableStart()) + 0.1,
trigger,
});
}

return shouldReset;
}

['play-fail'](event: ME.MediaPlayFailEvent) {
Expand Down Expand Up @@ -512,34 +543,53 @@ export class MediaStateManager extends MediaPlayerController {
this._isPlayingOnDisconnect = true;
}

if (this._request._looping) {
this._satisfyRequest('pause', event);

const seekedEvent = this._trackedEvents.get('seeked');
if (seekedEvent) event.triggers.add(seekedEvent);

if (this._clipEnded) {
event.stopImmediatePropagation();
this._handle(this.createEvent('end', { trigger: event }));
this._clipEnded = false;
return;
}

const seekedEvent = this._trackedEvents.get('seeked');
if (seekedEvent) event.triggers.add(seekedEvent);
if (this._request._looping) {
event.stopImmediatePropagation();
return;
}

this._satisfyRequest('pause', event);
this._resetTracking();

const { paused, playing } = this.$state;
paused.set(true);
playing.set(false);

this._resetTracking();
}

['time-update'](event: ME.MediaTimeUpdateEvent) {
const { currentTime, played, waiting } = this.$state,
if (this._request._looping) {
event.stopImmediatePropagation();
return;
}

const { realCurrentTime, played, waiting, clipEndTime, loop } = this.$state,
endTime = clipEndTime(),
detail = event.detail;

currentTime.set(detail.currentTime);
realCurrentTime.set(detail.currentTime);
played.set(detail.played);
waiting.set(false);

for (const track of this._media.textTracks) {
track[TextTrackSymbol._updateActiveCues](detail.currentTime, event);
}

if (endTime > 0 && detail.currentTime >= endTime) {
if (loop()) this._request._looping = true;
this._clipEnded = true;
this.dispatch('media-pause-request', { trigger: event });
}
}

['volume-change'](event: ME.MediaVolumeChangeEvent) {
Expand All @@ -552,9 +602,9 @@ export class MediaStateManager extends MediaPlayerController {

['seeking'] = throttle(
(event: ME.MediaSeekingEvent) => {
const { seeking, currentTime, paused } = this.$state;
const { seeking, realCurrentTime, paused } = this.$state;
seeking.set(true);
currentTime.set(event.detail);
realCurrentTime.set(event.detail);
this._satisfyRequest('seeking', event);
if (paused()) {
this._waitingTrigger = event;
Expand All @@ -566,7 +616,7 @@ export class MediaStateManager extends MediaPlayerController {
);

['seeked'](event: ME.MediaSeekedEvent) {
const { seeking, currentTime, paused, duration, ended } = this.$state;
const { seeking, realCurrentTime, paused, duration, ended } = this.$state;

if (this._request._seeking) {
seeking.set(true);
Expand All @@ -586,7 +636,7 @@ export class MediaStateManager extends MediaPlayerController {

if (event.detail !== duration()) ended.set(false);

currentTime.set(event.detail);
realCurrentTime.set(event.detail);
this._satisfyRequest('seeked', event);

// Only start if user initiated.
Expand Down Expand Up @@ -621,17 +671,15 @@ export class MediaStateManager extends MediaPlayerController {
this._firingWaiting = false;
}, 300);

['end'](event: ME.MediaEndEvent) {
['end'](event: Event) {
const { loop } = this.$state;

if (loop()) {
setTimeout(() => {
requestAnimationFrame(() => {
this.dispatch('media-loop-request', {
trigger: event,
});
this.dispatch('media-loop-request', { trigger: event });
});
}, 0);
}, 10);

return;
}
Expand Down

0 comments on commit 23fd34f

Please sign in to comment.