Skip to content

Commit 8fce1b1

Browse files
frankrousseauclaude
andcommitted
[players] Pre-seek next decoder to handle-in to avoid slate flash
With handles set on a playlist, the hidden decoder preloading the next entity sat at t=0 until playNext seeked it at switch time, so the first frame (the slate) was painted for an instant before the seek completed. Park the preloaded decoder on the next entity's handle-in frame as soon as it is preloaded (loadEntity, switchPlayers, and on nextHandleIn prop updates), fed by a new nextEntityHandleIn computed in PlaylistPlayer. Fixes #1019 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent d931dfe commit 8fce1b1

3 files changed

Lines changed: 64 additions & 2 deletions

File tree

src/components/players/players/PlaylistPlayer.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@
135135
:is-hd="isHd"
136136
:is-repeating="isRepeating"
137137
:muted="true"
138+
:next-handle-in="nextEntityHandleIn"
138139
:panzoom="true"
139140
:handle-in="
140141
['shot', 'edit', 'episode'].includes(playlist.for_entity)
@@ -258,6 +259,7 @@
258259
:is-repeating="isRepeating"
259260
:current-preview-index="currentPreviewIndex"
260261
:muted="isMuted"
262+
:next-handle-in="nextEntityHandleIn"
261263
:panzoom="true"
262264
@entity-change="onPlayerPlayingEntityChange"
263265
@frame-update="onRawPlayerFrameUpdate"
@@ -1304,6 +1306,12 @@ const nextEntityIndex = computed(() => {
13041306
return index
13051307
})
13061308
1309+
// Next entity's handle-in, so the viewers can park their preloaded decoder
1310+
// past the slate before the players switch (#1019).
1311+
const nextEntityHandleIn = computed(
1312+
() => getEntityHandles(entityList.value[nextEntityIndex.value]).handleIn
1313+
)
1314+
13071315
const picturePreviews = computed(() =>
13081316
entityList.value.flatMap(e => [
13091317
{

src/components/players/viewers/MultiVideoViewer.vue

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ const props = defineProps({
106106
type: String,
107107
default: 'main'
108108
},
109+
nextHandleIn: {
110+
// Handle-in frame of the next entity, used to pre-seek its decoder
111+
type: Number,
112+
default: 0
113+
},
109114
panzoom: {
110115
type: Boolean,
111116
default: false
@@ -401,6 +406,19 @@ const reloadCurrentEntity = (silentReload = false) => {
401406
loadEntity(currentIndex.value, currentPlayer.value.currentTime, silentReload)
402407
}
403408
409+
// Park the preloaded decoder on the next entity's handle-in frame so its
410+
// first frame (slate) is never the one painted when players switch.
411+
const preseekNextPlayer = () => {
412+
const player = nextPlayer.value
413+
if (!player || !player.getAttribute('src') || props.nextHandleIn <= 0) return
414+
const nextEntity = props.entities[getNextIndex(currentIndex.value)]
415+
const nextFps =
416+
parseFloat(nextEntity?.fps) ||
417+
parseFloat(currentProduction.value?.fps) ||
418+
DEFAULT_FPS
419+
player.currentTime = props.nextHandleIn / nextFps
420+
}
421+
404422
const loadEntity = (index = 0, currentTime = 0, silentLoad = false) => {
405423
if (index < props.entities.length) {
406424
const nextIndex = getNextIndex(index)
@@ -436,6 +454,7 @@ const loadEntity = (index = 0, currentTime = 0, silentLoad = false) => {
436454
if (!nextPlayer.value.src.endsWith(nextMoviePath)) {
437455
nextPlayer.value.src = nextMoviePath
438456
}
457+
preseekNextPlayer()
439458
} else if (nextPlayer.value) {
440459
nextPlayer.value.src = ''
441460
}
@@ -596,6 +615,7 @@ const switchPlayers = () => {
596615
nextPlayer.value = tmpPlayer
597616
if (nextEntity) {
598617
nextPlayer.value.src = getMoviePath(nextEntity)
618+
preseekNextPlayer()
599619
}
600620
resetHeight()
601621
setSpeed(rate)
@@ -710,6 +730,15 @@ watch(
710730
}
711731
)
712732
733+
// At switch time the prop still holds the previous "next" value: re-apply
734+
// the pre-seek once the parent pushes the new next entity's handle-in.
735+
watch(
736+
() => props.nextHandleIn,
737+
() => {
738+
preseekNextPlayer()
739+
}
740+
)
741+
713742
watch(
714743
() => props.entities,
715744
() => {

tests/unit/players/multivideoviewer.spec.js

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const removeRvfcMock = () => {
2727
delete HTMLVideoElement.prototype.cancelVideoFrameCallback
2828
}
2929

30-
const mountViewer = () => {
30+
const mountViewer = (props = {}) => {
3131
const store = createStore({
3232
getters: { currentProduction: () => ({ fps: '25' }) }
3333
})
@@ -37,7 +37,8 @@ const mountViewer = () => {
3737
{ id: 'e1', preview_file_id: 'p1', preview_file_extension: 'mp4', fps: 25 },
3838
{ id: 'e2', preview_file_id: 'p2', preview_file_extension: 'mp4', fps: 25 }
3939
],
40-
name: 'main'
40+
name: 'main',
41+
...props
4142
},
4243
global: {
4344
mocks: { $t: key => key },
@@ -185,6 +186,30 @@ describe('players/MultiVideoViewer (canvas pipeline)', () => {
185186
wrapper.unmount()
186187
})
187188

189+
it('pre-seeks the preloaded decoder to the next entity handle-in (#1019)', async () => {
190+
const wrapper = mountViewer({ nextHandleIn: 100 })
191+
192+
wrapper.vm.loadEntity(0)
193+
await wrapper.vm.$nextTick()
194+
195+
const current = wrapper.vm.currentPlayer
196+
const next = wrapper
197+
.findAll('video.playlist-movie-decoder')
198+
.map(w => w.element)
199+
.find(el => el !== current)
200+
201+
// The decoder preloading e2 must already be parked on its handle-in
202+
// frame so the slate at frame 0 never flashes when players switch
203+
expect(next.src).toContain('/movies/low/preview-files/p2.mp4')
204+
expect(next.currentTime).toBeCloseTo(100 / 25)
205+
206+
// A handle change on the next entity re-parks the decoder
207+
await wrapper.setProps({ nextHandleIn: 50 })
208+
expect(next.currentTime).toBeCloseTo(50 / 25)
209+
210+
wrapper.unmount()
211+
})
212+
188213
it('does not emit frame-update from ticks while paused', async () => {
189214
const wrapper = mountViewer()
190215
wrapper.vm.loadEntity(0)

0 commit comments

Comments
 (0)