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')
+ })
+ })
+})