[vlc-commits] [Git][videolan/vlc][master] 6 commits: player: allow pause from STARTED state for deferred pause

Steve Lhomme (@robUx4) gitlab at videolan.org
Thu Jun 18 09:35:42 UTC 2026



Steve Lhomme pushed to branch master at VideoLAN / VLC


Commits:
d4f20629 by Alexandre Janniaux at 2026-06-18T09:23:43+00:00
player: allow pause from STARTED state for deferred pause

The player state machine assertion in vlc_player_input_HandleState()
only allowed pause transitions from PLAYING state. This might causes
an assertion failure when --start-paused is used with slow streams, but
with future changes it also triggers it more systematically.

In future commits, when --start-paused defers pause until after
buffering completes:
 - Input starts and enters STARTED state during buffering
 - When buffering completes, the deferred pause is applied
 - Input transitions to PAUSE_S and notifies the player
 - Player receives PAUSE_S while still in STARTED state
 - Assertion fails: assert(player->global_state == VLC_PLAYER_STATE_PLAYING)

This commit relaxes the assertion to allow pause from both PLAYING and
STARTED states, which is a valid transition when applying deferred pause
after buffering.

Refs #29435

- - - - -
2191d729 by Alexandre Janniaux at 2026-06-18T09:23:43+00:00
player: ensure we don't signal pause twice

With deferred --start-paused, i.e. pause applied after buffering and
clock initialization via EsOutDecodersStopBuffering, the player timer's
best_source can receive two pause notifications for a single pause:

  1. The input PAUSE_S state event (es_source = NULL) notifies every
     timer source whose source->es is NULL, i.e. the best_source before
     any ES has delivered a point.

  2. Once the clock has a valid reference (SetFirstPcr has run), the
     clock emits a startup paused point. That point claims
     best_source.es = audio. The subsequent output-clock pause event
     (es_source = audio) then also matches and notifies the same
     best_source.

Before the deferred-pause feature, ControlPause() ran before
SetFirstPcr, so no startup point was produced and best_source.es
stayed NULL throughout the paused startup. Only the first case happened
and we didn't get duplicated event.

With the deferred pause, input pause and output pause are racing against
the startup point. If 1. fires first (es still NULL), both channels
match: best_source receives PAUSED, POINT, PAUSED instead of PAUSED,
POINT. That extra PAUSED typically makes the test wait_timer_report
fails and doesn't provide any additional information.

This commit prevent the duplication of this pause signal by checking the
currently signalled state.

Refs #29435

- - - - -
73ca279b by Alexandre Janniaux at 2026-06-18T09:23:43+00:00
input: allow ControlPause to get a synchronous parameter

With the following commits, a pause from the user can be asynchronous if
it is triggered before buffering is done. However, we currently only
handle frame-by-frame synchronously.

This commit allows adding the frame-by-frame exception for the deferred
pause handling. This should probably be implemented properly later and
this commit should then be reverted.

The implementation for this parameter will follow in subsequent input.c
commit.

- - - - -
8f6579e8 by Alexandre Janniaux at 2026-06-18T09:23:43+00:00
input: es_out: allow defer pause/unpause during buffering

This commit prepares a fix for a playback glitch that occurs when
resuming from the --start-paused option. Previously, ControlPause() was
called immediately at the start of MainLoop(), before any PCR/PTS
reference points were established in the clock system. The pause_date
was stored during initialization and buffering, so when the user would
unpause, the accumulated pause duration included the entire startup and
buffering time. This caused the content to skip forward by an amount
matching the buffering duration.

    Old behavior (--start-paused):

    MainLoop        Buffering                       User
    start           completes                       unpauses
      |                |                               |
      v                v                               v
    --+--[PAUSED]------+----------------//-------------+-->
      ^                                                |
      |<------------- pause_duration ----------------->|
      includes buffering time → content skips on resume

    New behavior (deferred pause):

    MainLoop        Buffering                       User
    start           completes                       unpauses
      |                |                               |
      v                v                               v
    --+--[PLAYING]-----+---[PAUSED]------------//------+-->
      flag set         ^
                       |<------ pause_duration ------->|
                   Deferred        
                   Pause 

The fix requires deferring the pause operation until after buffering
completes and clock references are established, so that pause_duration
only accounts for actual pause time.

This commit introduces a b_pause_after_buffering flag in the input
private structure and adds the handling code in EsOutDecodersStopBuffering()
to apply the deferred pause. The pause is applied right after
vlc_clock_main_SetFirstPcr() establishes the first PCR reference point,
at which point both the input_clock and main_clock have valid references
and the pause operation can correctly account for timing when resuming.

The implementation uses EsOutChangePause() to properly pause both
decoders and clocks, and updates the input thread state to PAUSE_S with
an event so the player UI correctly reflects the paused state.

The flag is initialized to false and is not set by this commit. A
subsequent commit will wire up the --start-paused option to set this
flag, completing the fix.

Refs #29435

- - - - -
9f80ae52 by Alexandre Janniaux at 2026-06-18T09:23:43+00:00
input: wire up --start-paused and pause/unpause deferral during buffering

Wire up the deferred pause mechanism introduced in the previous commit:

1. --start-paused now sets b_pause_after_buffering flag instead of calling
   ControlPause() directly, avoiding pause during buffering.

2. ControlPause() checks for buffering and defers by setting the flag and
   updating input state without calling demux_Control or es_out_SetPauseState.
   The es_out will switch to pause state at the end of the buffering.

3. ControlUnpause() checks for buffering and cancels deferred pause by
   clearing the flag. It still unset the pause on the demux.

This allows users to pause/unpause during buffering without interfering
with clock reference establishment and fixes the delay after resuming
from --start-paused. The actual pause/unpause is applied when buffering
completes in EsOutDecodersStopBuffering() as per previous commit.

Fixes #29435

- - - - -
e81f0ab5 by Alexandre Janniaux at 2026-06-18T09:23:43+00:00
input: es_out: clean up pause state

Now that we handled deferred pause, we cannot be in the pause state if
the deferred pause mechanism is enabled.

- - - - -


6 changed files:

- src/input/es_out.c
- src/input/input.c
- src/input/input_internal.h
- src/player/input.c
- src/player/player.h
- src/player/timer.c


Changes:

=====================================
src/input/es_out.c
=====================================
@@ -1254,6 +1254,27 @@ static void EsOutDecodersStopBuffering(es_out_sys_t *p_sys, bool b_forced)
                                i_stream_start);
     vlc_clock_main_Unlock(p_sys->p_pgrm->clocks.main);
 
+    /* If pause was requested during buffering previously, set pause state now
+     * that buffering is complete and clock references are established. Use
+     * EsOutChangePause to properly pause both decoders and clocks. */
+    if (input_priv(p_sys->p_input)->b_pause_after_buffering)
+    {
+        input_priv(p_sys->p_input)->b_pause_after_buffering = false;
+
+        /* ES out should not be paused yet: a deferred pause is never applied to
+         * es_out before this point, and all synchronous pauses clear the
+         * b_pause_after_buffering flag. */
+        assert(!p_sys->b_paused);
+        assert(input_priv(p_sys->p_input)->i_state != PAUSE_S);
+
+        const vlc_tick_t i_pause_date = vlc_tick_now();
+        EsOutChangePause(p_sys, true, i_pause_date);
+
+         /* Update input state since pause was not yet applied. */
+        input_priv(p_sys->p_input)->i_state = PAUSE_S;
+        input_SendEventState(p_sys->p_input, PAUSE_S, i_pause_date);
+    }
+
     foreach_es_then_es_slaves(p_es)
     {
         if( !p_es->p_dec )


=====================================
src/input/input.c
=====================================
@@ -75,7 +75,7 @@ static int ControlPopEarly(input_thread_t *input, int *type,
 static void       ControlRelease( int i_type, const input_control_param_t *p_param );
 static bool       ControlIsSeekRequest( int i_type );
 static bool       Control( input_thread_t *, int, input_control_param_t );
-static void       ControlPause( input_thread_t *, vlc_tick_t );
+static void       ControlPause(input_thread_t *, vlc_tick_t, bool);
 
 static int  UpdateTitleSeekpointFromDemux( input_thread_t * );
 static void UpdateGenericFromDemux( input_thread_t * );
@@ -275,6 +275,7 @@ input_thread_t * input_Create( vlc_object_t *p_parent, input_item_t *p_item,
     priv->is_running = false;
     priv->is_stopped = false;
     priv->b_recording = false;
+    priv->b_pause_after_buffering = false;
     priv->rate = 1.f;
     TAB_INIT( priv->i_attachment, priv->attachment );
     priv->p_sout   = NULL;
@@ -644,7 +645,7 @@ static void MainLoop( input_thread_t *p_input, bool b_interactive )
     vlc_tick_t i_last_seek_mdate = 0;
 
     if( b_interactive && var_InheritBool( p_input, "start-paused" ) )
-        ControlPause( p_input, vlc_tick_now() );
+        ControlPause(p_input, vlc_tick_now(), true);
 
     bool eof_signaled = false;
 
@@ -1716,8 +1717,22 @@ static void ControlRelease( int i_type, const input_control_param_t *p_param )
     }
 }
 
-/* Pause input */
-static void ControlPause( input_thread_t *p_input, vlc_tick_t i_control_date )
+/* Pause input.
+ *
+ * can_defer tells whether the pause may be deferred until buffering
+ * completes. It must be false for any caller that really needs es_out
+ * to be paused right away.
+ *
+ * The only such caller is the frame-by-frame handling. A frame step is
+ * implemented as "pause the output, then release exactly one picture at a
+ * time".
+ *
+ * TODO: at some point, we must also be able to remove this special case
+ *       for frame-by-frame, but the deferred pause implementation would
+ *       break frame-by-frame without this.
+ */
+static void ControlPause(input_thread_t *p_input, vlc_tick_t i_control_date,
+                         bool can_defer)
 {
     int i_state = PAUSE_S;
 
@@ -1732,6 +1747,23 @@ static void ControlPause( input_thread_t *p_input, vlc_tick_t i_control_date )
         }
     }
 
+    /* Check if in buffering state, and if so, deferred the pasue
+     * instead of trying to pause the media playback right away, since
+     * the playback and the clocks are not even configured yet.
+     *
+     * We must avoid switching to PAUSE_S here, so that we keep the
+     * property that PAUSE_S implies es_out is paused. We'll handle the
+     * pause as soon as buffering is done. */
+    if (can_defer &&
+        input_priv(p_input)->master->b_can_pause &&
+        es_out_GetBuffering(input_priv(p_input)->p_es_out))
+    {
+        input_priv(p_input)->b_pause_after_buffering = true;
+        return;
+    }
+
+    input_priv(p_input)->b_pause_after_buffering = false;
+
     /* */
     if( es_out_SetPauseState( input_priv(p_input)->p_es_out,
                               input_priv(p_input)->master->b_can_pause,
@@ -1759,6 +1791,8 @@ static void ControlUnpause( input_thread_t *p_input, vlc_tick_t i_control_date )
         }
     }
 
+    input_priv(p_input)->b_pause_after_buffering = false;
+
     /* Switch to play */
     input_ChangeState( p_input, PLAYING_S, i_control_date );
     es_out_SetPauseState( input_priv(p_input)->p_es_out, false, false, i_control_date );
@@ -2122,7 +2156,7 @@ static bool Control( input_thread_t *p_input,
                 case PAUSE_S:
                     if( priv->i_state == PLAYING_S )
                     {
-                        ControlPause( p_input, i_control_date );
+                        ControlPause(p_input, i_control_date, true);
                         b_force_update = true;
                     }
                     break;
@@ -2441,7 +2475,7 @@ static bool Control( input_thread_t *p_input,
             }
             else if( priv->i_state == PLAYING_S )
             {
-                ControlPause( p_input, i_control_date );
+                ControlPause(p_input, i_control_date, false);
                 input_SendEvent(p_input, &(struct vlc_input_event) {
                     .type = INPUT_EVENT_FRAME_NEXT_STATUS,
                     .frame_next_status = -EAGAIN,
@@ -2482,7 +2516,7 @@ static bool Control( input_thread_t *p_input,
             }
             else if( priv->i_state == PLAYING_S )
             {
-                ControlPause( p_input, i_control_date );
+                ControlPause(p_input, i_control_date, false);
                 input_SendEvent(p_input, &(struct vlc_input_event) {
                     .type = INPUT_EVENT_FRAME_PREVIOUS_STATUS,
                     .frame_next_status = -EAGAIN,


=====================================
src/input/input_internal.h
=====================================
@@ -506,6 +506,7 @@ typedef struct input_thread_private_t
     bool        is_running;
     bool        is_stopped;
     bool        b_recording;
+    bool        b_pause_after_buffering; /* Defer pause until after buffering */
     float       rate;
 
     /* Playtime configuration and state */


=====================================
src/player/input.c
=====================================
@@ -308,7 +308,9 @@ vlc_player_input_HandleState(struct vlc_player_input *input,
             break;
 
         case VLC_PLAYER_STATE_PAUSED:
-            assert(player->global_state == VLC_PLAYER_STATE_PLAYING);
+            /* Can pause from PLAYING or STARTED (deferred pause during buffering) */
+            assert(player->global_state == VLC_PLAYER_STATE_PLAYING
+                   || player->global_state == VLC_PLAYER_STATE_STARTED);
             assert(state_date != VLC_TICK_INVALID);
             input->pause_date = state_date;
 


=====================================
src/player/player.h
=====================================
@@ -195,6 +195,12 @@ struct vlc_player_timer_source
     vlc_es_id_t *es; /* weak reference */
     struct vlc_player_timer_point point;
     bool seeking;
+
+    /* Set once a pause has been reported to this source's listener.
+     * The input might signal paused clock and then output
+     * would signal it. This flag filters this case here. */
+    bool pause_reported;
+
     union
     {
         struct {


=====================================
src/player/timer.c
=====================================
@@ -270,6 +270,12 @@ vlc_player_UpdateTimerEvent(vlc_player_t *player, vlc_es_id_t *es_source,
                 struct vlc_player_timer_source *source = &player->timer.sources[i];
                 if (source->es != es_source)
                     continue;
+
+                /* The input might signal paused clock and then output
+                 * would signal it. This flag filters this case here. */
+                if (source->pause_reported)
+                    continue;
+                source->pause_reported = true;
                 vlc_player_SendTimerPause(player, source, system_date,
                                           i == VLC_PLAYER_TIMER_TYPE_SMPTE);
             }
@@ -279,6 +285,8 @@ vlc_player_UpdateTimerEvent(vlc_player_t *player, vlc_es_id_t *es_source,
         case VLC_PLAYER_TIMER_EVENT_PLAYING:
             assert(!player->timer.stopping);
             player->timer.update_state = UPDATE_STATE_RESUMING;
+            for (size_t i = 0; i < VLC_PLAYER_TIMER_TYPE_COUNT; ++i)
+                player->timer.sources[i].pause_reported = false;
             break;
 
         case VLC_PLAYER_TIMER_EVENT_STOPPING:
@@ -755,6 +763,7 @@ vlc_player_InitTimer(vlc_player_t *player)
         player->timer.sources[i].point.system_date = VLC_TICK_INVALID;
         player->timer.sources[i].es = NULL;
         player->timer.sources[i].seeking = false;
+        player->timer.sources[i].pause_reported = false;
     }
     vlc_player_ResetTimer(player);
 }



View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/053b329b1e22e45868310e9e02f24c13b4731ba5...e81f0ab5cc7ff3efe3899a9c1af23599ba95d7ce

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/053b329b1e22e45868310e9e02f24c13b4731ba5...e81f0ab5cc7ff3efe3899a9c1af23599ba95d7ce
You're receiving this email because of your account on code.videolan.org. Manage all notifications: https://code.videolan.org/-/profile/notifications | Help: https://code.videolan.org/help




More information about the vlc-commits mailing list