diff --git a/src/components/players/bars/PlayerPlaybackBar.vue b/src/components/players/bars/PlayerPlaybackBar.vue index 9add72ca63..f51ccd7b40 100644 --- a/src/components/players/bars/PlayerPlaybackBar.vue +++ b/src/components/players/bars/PlayerPlaybackBar.vue @@ -100,6 +100,8 @@ diff --git a/src/components/players/players/PlaylistPlayer.vue b/src/components/players/players/PlaylistPlayer.vue index 6ddd09d811..3cb2f32929 100644 --- a/src/components/players/players/PlaylistPlayer.vue +++ b/src/components/players/players/PlaylistPlayer.vue @@ -434,6 +434,7 @@ :comparison-annotations="comparisonAnnotations" :empty="!isCurrentPreviewMovie" :frame-duration="frameDuration" + :frame-start="frameStart" :is-full-mode="isFullMode" :is-full-screen="fullScreen || isEntitiesHidden" :movie-dimensions="movieDimensions" @@ -505,6 +506,7 @@ :compact="isFullMode" :current-frame-label="currentFrame" :current-time="currentTime" + :frame-start="frameStart" :full-screen="fullScreen" :is-3-d-animation="objectModel.isAnimation" :is-3-d-model="isCurrentPreviewModel" @@ -951,6 +953,7 @@ import { floorToFrame, formatFrame, formatTime, + getEntityFrameStart, roundToFrame } from '@/lib/video' @@ -1386,6 +1389,13 @@ const frameNumber = computed(() => { return Math.round(n) - 1 }) +// Production start frame of the playing shot (data.frame_in, e.g. 1001). +// Forwarded to the playback bar / progress bar as a display-only offset — +// currentFrame stays 1-based since annotation seeks round-trip on it. +const frameStart = computed(() => + getEntityFrameStart(shotMap.value.get(currentEntity.value?.id)) +) + const currentFrame = computed(() => formatFrame(frameNumber.value + 2)) const currentFrameMovieOrPicture = computed(() => { diff --git a/src/components/players/players/PreviewPlayer.vue b/src/components/players/players/PreviewPlayer.vue index 85482f7dd6..9a18e35227 100644 --- a/src/components/players/players/PreviewPlayer.vue +++ b/src/components/players/players/PreviewPlayer.vue @@ -153,6 +153,7 @@ :annotations="annotations" :comparison-annotations="comparisonAnnotations" :frame-duration="frameDuration" + :frame-start="frameStart" :is-full-screen="fullScreen" :movie-dimensions="movieDimensions" :nb-frames="nbFrames" @@ -171,6 +172,7 @@ :available-3-d-animations="available3DAnimations" :current-frame-label="currentFrameLabel" :current-time="currentTime" + :frame-start="frameStart" :full-screen="fullScreen" :is-3-d-animation="is3DAnimation" :is-3-d-model="is3DModel" @@ -468,6 +470,7 @@ import { floorToFrame, formatFrame, formatTime, + getEntityFrameStart, roundToFrame } from '@/lib/video' @@ -642,6 +645,7 @@ const isTVShow = computed(() => store.getters.isTVShow) const organisation = computed(() => store.getters.organisation) const productionMap = computed(() => store.getters.productionMap) const selectedConcepts = computed(() => store.getters.selectedConcepts) +const shotMap = computed(() => store.getters.shotMap) const userId = computed(() => store.getters.user?.id) // Panzoom transform sync — the main viewer drives both the @@ -851,6 +855,12 @@ const frameDuration = computed( () => Math.round((1 / fps.value) * 10000) / 10000 ) +// Production start frame of the displayed shot (data.frame_in, e.g. 1001). +// Forwarded to the playback bar / progress bar as a display-only offset. +const frameStart = computed(() => + getEntityFrameStart(shotMap.value.get(props.task?.entity_id)) +) + const extension = computed(() => currentPreview.value ? currentPreview.value.extension : '' ) diff --git a/src/components/players/progress/VideoProgress.vue b/src/components/players/progress/VideoProgress.vue index 0b71d11050..b99e1193ef 100644 --- a/src/components/players/progress/VideoProgress.vue +++ b/src/components/players/progress/VideoProgress.vue @@ -40,7 +40,7 @@ v-if="handleIn >= 0 && !isFullMode && !empty" > - {{ handleIn + 1 }} + {{ handleIn + 1 + frameStartOffset }} @@ -53,7 +53,7 @@ v-if="handleOut >= 0 && !isFullMode && !empty" > - {{ handleOut + 1 }} + {{ handleOut + 1 + frameStartOffset }} @@ -109,7 +109,7 @@ isFrameNumberVisible && hoverFrame > 0 && !empty && !progressDragging " > - {{ hoverFrame }} + {{ hoverFrame + frameStartOffset }} const videoDuration = computed(() => props.nbFrames * props.frameDuration) +// Display-only shift so the first frame reads as `frameStart` (e.g. a shot +// with data.frame_in = 1001) instead of 1. Internal frame math is untouched. +const frameStartOffset = computed(() => + props.frameStart > 0 ? props.frameStart - 1 : 0 +) + const backgroundSize = computed(() => { if (videoDuration.value) { return 200 / props.nbFrames + '% 100%' diff --git a/src/lib/video.js b/src/lib/video.js index c5c03c9386..f7ae8a355e 100644 --- a/src/lib/video.js +++ b/src/lib/video.js @@ -87,3 +87,13 @@ export const formatFrame = frame => { if (frame < 0) frame = 0 return `${frame}`.padStart(3, '0') } + +/* + * Get the production start frame of an entity (a shot numbered from + * data.frame_in, e.g. 1001). Returns undefined when the entity carries no + * usable value. + */ +export const getEntityFrameStart = entity => { + const frameIn = parseInt(entity?.data?.frame_in) + return frameIn > 0 ? frameIn : undefined +} diff --git a/tests/unit/lib/video.spec.js b/tests/unit/lib/video.spec.js index 07a0399f60..19476122fb 100644 --- a/tests/unit/lib/video.spec.js +++ b/tests/unit/lib/video.spec.js @@ -5,6 +5,7 @@ import { formatTime, formatToTimecode, frameToSeconds, + getEntityFrameStart, roundToFrame } from '@/lib/video' @@ -80,4 +81,16 @@ describe('video', () => { it('formatTime with negative time', () => { expect(formatTime(-1, 25)).toBe('00:00:00:00') }) + + it('getEntityFrameStart', () => { + expect(getEntityFrameStart({ data: { frame_in: 1001 } })).toBe(1001) + expect(getEntityFrameStart({ data: { frame_in: '1001' } })).toBe(1001) + expect(getEntityFrameStart({ data: { frame_in: 1 } })).toBe(1) + expect(getEntityFrameStart({ data: { frame_in: 0 } })).toBeUndefined() + expect(getEntityFrameStart({ data: { frame_in: -5 } })).toBeUndefined() + expect(getEntityFrameStart({ data: { frame_in: 'abc' } })).toBeUndefined() + expect(getEntityFrameStart({ data: {} })).toBeUndefined() + expect(getEntityFrameStart({})).toBeUndefined() + expect(getEntityFrameStart(undefined)).toBeUndefined() + }) }) diff --git a/tests/unit/players/playerplaybackbar.spec.js b/tests/unit/players/playerplaybackbar.spec.js new file mode 100644 index 0000000000..22b9d67bfe --- /dev/null +++ b/tests/unit/players/playerplaybackbar.spec.js @@ -0,0 +1,47 @@ +import { mount } from '@vue/test-utils' + +import PlayerPlaybackBar from '@/components/players/bars/PlayerPlaybackBar.vue' + +const mountBar = (props = {}) => + mount(PlayerPlaybackBar, { + props: { + currentFrameLabel: '005', + isMovie: true, + nbFrames: 120, + ...props + }, + global: { + mocks: { $t: key => key }, + stubs: { + ButtonSimple: true, + ButtonSound: true, + ComboboxStyled: true, + SpeedButton: true + } + } + }) + +describe('players/PlayerPlaybackBar', () => { + describe('frame indicator', () => { + it('keeps the 1-based numbering without a frame start', () => { + const wrapper = mountBar() + expect(wrapper.text()).toContain('(005 / 120)') + }) + + it('offsets the numbering so the first frame reads as frameStart', () => { + const wrapper = mountBar({ frameStart: 1001 }) + expect(wrapper.text()).toContain('(1005 / 1120)') + }) + + it('ignores a frame start of zero or less', () => { + expect(mountBar({ frameStart: 0 }).text()).toContain('(005 / 120)') + expect(mountBar({ frameStart: -10 }).text()).toContain('(005 / 120)') + }) + + it('offsets the compact light indicator too', () => { + const wrapper = mountBar({ frameStart: 1001, light: true }) + expect(wrapper.text()).toContain('(1005)') + expect(wrapper.text()).not.toContain('1120') + }) + }) +})