Skip to content

Commit b0f9cb7

Browse files
Merge pull request #2104 from frankrousseau/fix-1019-playlist-handles-slate
[players] Pre-seek next decoder to handle-in to avoid slate flash
2 parents 3afe345 + 8fce1b1 commit b0f9cb7

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"
@@ -1288,6 +1290,12 @@ const nextEntityIndex = computed(() => {
12881290
return index
12891291
})
12901292
1293+
// Next entity's handle-in, so the viewers can park their preloaded decoder
1294+
// past the slate before the players switch (#1019).
1295+
const nextEntityHandleIn = computed(
1296+
() => getEntityHandles(entityList.value[nextEntityIndex.value]).handleIn
1297+
)
1298+
12911299
const picturePreviews = computed(() =>
12921300
entityList.value.flatMap(e => [
12931301
{

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)