Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions src/components/players/bars/PlayerPlaybackBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@
<script setup>
import { computed } from 'vue'

import { formatFrame } from '@/lib/video'

import ButtonSimple from '@/components/widgets/ButtonSimple.vue'
import ButtonSound from '@/components/widgets/ButtonSound.vue'
import ComboboxStyled from '@/components/widgets/ComboboxStyled.vue'
Expand All @@ -123,6 +125,10 @@ const props = defineProps({
type: String,
default: '00:00:00:00'
},
frameStart: {
type: Number,
default: undefined
},
fullScreen: {
type: Boolean,
default: false
Expand Down Expand Up @@ -188,12 +194,25 @@ const speed = defineModel('speed', { type: Number, default: 3 })
const volume = defineModel('volume', { type: Number, default: 50 })

// Computed

// 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 frameIndicator = computed(() => {
const frameLabel = frameStartOffset.value
? formatFrame(Number(props.currentFrameLabel) + frameStartOffset.value)
: props.currentFrameLabel
if (props.light && !props.fullScreen) {
return `(${props.currentFrameLabel})`
return `(${frameLabel})`
}
const nbFrames = String(props.nbFrames).padStart(3, '0')
return `(${props.currentFrameLabel} / ${nbFrames})`
const nbFrames = String(props.nbFrames + frameStartOffset.value).padStart(
3,
'0'
)
return `(${frameLabel} / ${nbFrames})`
})
</script>

Expand Down
10 changes: 10 additions & 0 deletions src/components/players/players/PlaylistPlayer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -951,6 +953,7 @@ import {
floorToFrame,
formatFrame,
formatTime,
getEntityFrameStart,
roundToFrame
} from '@/lib/video'

Expand Down Expand Up @@ -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(() => {
Expand Down
10 changes: 10 additions & 0 deletions src/components/players/players/PreviewPlayer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -468,6 +470,7 @@ import {
floorToFrame,
formatFrame,
formatTime,
getEntityFrameStart,
roundToFrame
} from '@/lib/video'

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 : ''
)
Expand Down
16 changes: 13 additions & 3 deletions src/components/players/progress/VideoProgress.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
v-if="handleIn >= 0 && !isFullMode && !empty"
>
<span class="handle-frame" v-if="handleIn !== 0">
{{ handleIn + 1 }}
{{ handleIn + 1 + frameStartOffset }}
</span>
</span>

Expand All @@ -53,7 +53,7 @@
v-if="handleOut >= 0 && !isFullMode && !empty"
>
<span class="handle-frame">
{{ handleOut + 1 }}
{{ handleOut + 1 + frameStartOffset }}
</span>
</span>

Expand Down Expand Up @@ -109,7 +109,7 @@
isFrameNumberVisible && hoverFrame > 0 && !empty && !progressDragging
"
>
{{ hoverFrame }}
{{ hoverFrame + frameStartOffset }}
<span
class="frame-tile"
:style="getFrameBackgroundStyle(hoverFrame)"
Expand Down Expand Up @@ -150,6 +150,10 @@ const props = defineProps({
default: 0,
type: Number
},
frameStart: {
default: undefined,
type: Number
},
handleIn: {
default: 3,
type: Number
Expand Down Expand Up @@ -212,6 +216,12 @@ const getClientX = event =>

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%'
Expand Down
10 changes: 10 additions & 0 deletions src/lib/video.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
13 changes: 13 additions & 0 deletions tests/unit/lib/video.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
formatTime,
formatToTimecode,
frameToSeconds,
getEntityFrameStart,
roundToFrame
} from '@/lib/video'

Expand Down Expand Up @@ -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()
})
})
47 changes: 47 additions & 0 deletions tests/unit/players/playerplaybackbar.spec.js
Original file line number Diff line number Diff line change
@@ -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')
})
})
})