feat: enforce 100% frame-based positioning (no time)
Core Principle: Frame is the ONLY standard Changes: - VideoPlayer: Remove startTime/endTime props - VideoPlayer: Add mandatory startFrame/endFrame check - VideoPlayer: UI shows F123 (51.2s) format - All Views: Only set startFrame/endFrame - All Views: Only pass :start-frame/:end-frame - Remove all startTime/endTime from currentVideo refs Time is only used for display (auxiliary). Frame is the source of truth for all positioning.
This commit is contained in:
@@ -139,7 +139,7 @@
|
|||||||
<span class="ms-video-seg-info2">{{ currentItemIdx + 1 }} / {{ totalItems }}</span>
|
<span class="ms-video-seg-info2">{{ currentItemIdx + 1 }} / {{ totalItems }}</span>
|
||||||
<button class="ms-fm-btn ms-video-nav-btn" @click="nextItem" :disabled="!canNext">下一個 →</button>
|
<button class="ms-fm-btn ms-video-nav-btn" @click="nextItem" :disabled="!canNext">下一個 →</button>
|
||||||
</div>
|
</div>
|
||||||
<span v-else class="ms-video-seg-info">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</span>
|
<span v-else class="ms-video-seg-info">F{{ currentFrame }} / F{{ totalFrames }} <span class="ms-video-time-aux">({{ formatTime(currentTime) }} / {{ formatTime(duration) }})</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,10 +159,8 @@ const zIndexValue = ref(getNextZIndex())
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
fileUuid: string
|
fileUuid: string
|
||||||
startTime?: number
|
startFrame: number
|
||||||
endTime?: number
|
endFrame: number
|
||||||
startFrame?: number
|
|
||||||
endFrame?: number
|
|
||||||
allTraces?: any[]
|
allTraces?: any[]
|
||||||
mergedSegments?: any[]
|
mergedSegments?: any[]
|
||||||
initialTraceIdx?: number
|
initialTraceIdx?: number
|
||||||
@@ -171,6 +169,8 @@ const props = withDefaults(defineProps<{
|
|||||||
simple?: boolean
|
simple?: boolean
|
||||||
useOriginal?: boolean
|
useOriginal?: boolean
|
||||||
}>(), {
|
}>(), {
|
||||||
|
startFrame: 0,
|
||||||
|
endFrame: 0,
|
||||||
allTraces: () => [],
|
allTraces: () => [],
|
||||||
mergedSegments: () => [],
|
mergedSegments: () => [],
|
||||||
initialTraceIdx: 0,
|
initialTraceIdx: 0,
|
||||||
@@ -888,17 +888,20 @@ onMounted(async () => {
|
|||||||
emit('trace-change', currentTraceIdx.value)
|
emit('trace-change', currentTraceIdx.value)
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const params: any = {
|
if (props.startFrame == null || props.endFrame == null) {
|
||||||
|
videoError.value = 'ERROR: startFrame and endFrame are required'
|
||||||
|
videoLoading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await apiCall('get_video_stream', {
|
||||||
uuid: props.fileUuid,
|
uuid: props.fileUuid,
|
||||||
|
startTime: null,
|
||||||
|
endTime: null,
|
||||||
|
startFrame: props.startFrame,
|
||||||
|
endFrame: props.endFrame,
|
||||||
original: props.useOriginal,
|
original: props.useOriginal,
|
||||||
}
|
})
|
||||||
|
|
||||||
if (props.startFrame != null && props.endFrame != null) {
|
|
||||||
params.startFrame = props.startFrame
|
|
||||||
params.endFrame = props.endFrame
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await apiCall('get_video_stream', params)
|
|
||||||
if (typeof data === 'string') {
|
if (typeof data === 'string') {
|
||||||
videoSrc.value = data
|
videoSrc.value = data
|
||||||
} else {
|
} else {
|
||||||
@@ -970,7 +973,8 @@ onUnmounted(() => {
|
|||||||
.ms-video-tl-pos-tip { display: none; position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); background: #1a56db; color: #fff; font-size: 10px; padding: 2px 6px; border-radius: 4px; white-space: nowrap; pointer-events: none; z-index: 10; }
|
.ms-video-tl-pos-tip { display: none; position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); background: #1a56db; color: #fff; font-size: 10px; padding: 2px 6px; border-radius: 4px; white-space: nowrap; pointer-events: none; z-index: 10; }
|
||||||
.ms-video-tl-position:hover .ms-video-tl-pos-tip { display: block; }
|
.ms-video-tl-position:hover .ms-video-tl-pos-tip { display: block; }
|
||||||
.ms-video-nav { display: flex; align-items: center; justify-content: space-between; margin-top: 14px; gap: 10px; flex-wrap: wrap; }
|
.ms-video-nav { display: flex; align-items: center; justify-content: space-between; margin-top: 14px; gap: 10px; flex-wrap: wrap; }
|
||||||
.ms-video-seg-info { font-size: 12px; color: rgba(255,255,255,0.6); flex-shrink: 0; }
|
.ms-video-seg-info { font-size: 12px; color: rgba(255,255,255,0.9); flex-shrink: 0; font-weight: 500; }
|
||||||
|
.ms-video-time-aux { font-size: 11px; color: rgba(255,255,255,0.5); margin-left: 6px; }
|
||||||
.ms-video-seg-info2 { font-size: 12px; color: #9aa0a6; flex: 1; text-align: center; }
|
.ms-video-seg-info2 { font-size: 12px; color: #9aa0a6; flex: 1; text-align: center; }
|
||||||
|
|
||||||
/* Frame seek controls */
|
/* Frame seek controls */
|
||||||
|
|||||||
@@ -563,7 +563,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VideoPlayer v-if="playing" simple :file-uuid="currentVideo.fileUuid" :start-time="currentVideo.startTime" :end-time="currentVideo.endTime" :title="currentVideo.title" @close="playing = false" />
|
<VideoPlayer v-if="playing" simple :file-uuid="currentVideo.fileUuid" :start-frame="currentVideo.startFrame" :end-frame="currentVideo.endFrame" :title="currentVideo.title" @close="playing = false" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -588,7 +588,7 @@ const displayFilter = ref('all')
|
|||||||
const showFilter = ref(false)
|
const showFilter = ref(false)
|
||||||
const selectedFiles = ref<string[]>([])
|
const selectedFiles = ref<string[]>([])
|
||||||
const playing = ref(false)
|
const playing = ref(false)
|
||||||
const currentVideo = ref({ fileUuid: '', startTime: 0, endTime: 0, title: '' })
|
const currentVideo = ref({ fileUuid: '', startFrame: 0, endFrame: 0, title: '' })
|
||||||
const processorJsonDialog = ref(false)
|
const processorJsonDialog = ref(false)
|
||||||
const processorJsonContent = ref('')
|
const processorJsonContent = ref('')
|
||||||
const processorJsonTitle = ref('')
|
const processorJsonTitle = ref('')
|
||||||
@@ -842,7 +842,7 @@ function toggleSelect(f: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function playVideo(f: any) {
|
function playVideo(f: any) {
|
||||||
currentVideo.value = { fileUuid: f.file_uuid, startTime: 0, endTime: 99999, title: f.file_name }
|
currentVideo.value = { fileUuid: f.file_uuid, startFrame: 0, endFrame: 99999, title: `F0-F99999 (${f.file_name})` }
|
||||||
playing.value = true
|
playing.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -274,7 +274,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VideoPlayer v-if="playing" :file-uuid="currentVideo.fileUuid" :start-time="currentVideo.startTime" :end-time="currentVideo.endTime" :all-traces="clusterVideoTraces" :merged-segments="clusterVideoSegments" :title="currentVideo.title" @close="playing = false; clusterVideoTraces = []; clusterVideoSegments = []" />
|
<VideoPlayer v-if="playing" :file-uuid="currentVideo.fileUuid" :start-frame="currentVideo.startFrame" :end-frame="currentVideo.endFrame" :all-traces="clusterVideoTraces" :merged-segments="clusterVideoSegments" :title="currentVideo.title" @close="playing = false; clusterVideoTraces = []; clusterVideoSegments = []" />
|
||||||
|
|
||||||
<!-- Merge Target Selection Modal -->
|
<!-- Merge Target Selection Modal -->
|
||||||
<div v-if="showMergeTargetModal" class="ms-modal-overlay show" @click.self="showMergeTargetModal = false">
|
<div v-if="showMergeTargetModal" class="ms-modal-overlay show" @click.self="showMergeTargetModal = false">
|
||||||
@@ -488,7 +488,7 @@ const loading = computed(() => refreshing.value)
|
|||||||
const faces = ref<any[]>([])
|
const faces = ref<any[]>([])
|
||||||
const traces = ref<any[]>([])
|
const traces = ref<any[]>([])
|
||||||
const playing = ref(false)
|
const playing = ref(false)
|
||||||
const currentVideo = ref({ fileUuid: '', startTime: 0, endTime: 0, title: '' })
|
const currentVideo = ref({ fileUuid: '', startFrame: 0, endFrame: 0, title: '' })
|
||||||
const candidateThumbs = faceThumbsCache
|
const candidateThumbs = faceThumbsCache
|
||||||
const activeFrameCard = ref('')
|
const activeFrameCard = ref('')
|
||||||
const activeFrameMode = ref('')
|
const activeFrameMode = ref('')
|
||||||
@@ -1118,7 +1118,12 @@ async function assignClusterTrace(trace: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function playTrace(t: any) {
|
function playTrace(t: any) {
|
||||||
currentVideo.value = { fileUuid: t.file_uuid, startTime: t.first_sec, endTime: t.last_sec, title: `Trace F${t.trace_id} - ${formatTime(t.first_sec)}-${formatTime(t.last_sec)}` }
|
currentVideo.value = {
|
||||||
|
fileUuid: t.file_uuid,
|
||||||
|
startFrame: t.first_frame || 0,
|
||||||
|
endFrame: t.last_frame || (t.first_frame || 0) + 30,
|
||||||
|
title: `F${t.first_frame || 0}-F${t.last_frame || 0} (Trace ${t.trace_id})`
|
||||||
|
}
|
||||||
playing.value = true
|
playing.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1354,21 +1359,20 @@ async function playFaceDetailVideo() {
|
|||||||
const frameTime = frame / fps
|
const frameTime = frame / fps
|
||||||
const startTime = Math.max(0, frameTime - 5)
|
const startTime = Math.max(0, frameTime - 5)
|
||||||
const endTime = frameTime + 5
|
const endTime = frameTime + 5
|
||||||
currentVideo.value = {
|
currentVideo.value = {
|
||||||
fileUuid: c.file_uuid,
|
fileUuid: c.file_uuid,
|
||||||
startTime,
|
startFrame: startTime * fps,
|
||||||
endTime,
|
endFrame: endTime * fps,
|
||||||
title: `Face #${frame} (${Math.round(frameTime)}s)`
|
title: `F${Math.round(startTime * fps)}-F${Math.round(endTime * fps)} (${c.name})`
|
||||||
}
|
}
|
||||||
playing.value = true
|
playing.value = true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to get file info:', e)
|
console.error('Failed to get file info:', e)
|
||||||
const frameTime = frame / 30
|
|
||||||
currentVideo.value = {
|
currentVideo.value = {
|
||||||
fileUuid: c.file_uuid,
|
fileUuid: c.file_uuid,
|
||||||
startTime: Math.max(0, frameTime - 5),
|
startFrame: Math.round(Math.max(0, frameTime - 5) * 30),
|
||||||
endTime: frameTime + 5,
|
endFrame: Math.round((frameTime + 5) * 30),
|
||||||
title: `Face #${frame}`
|
title: `F${Math.round(Math.max(0, frameTime - 5) * 30)}-F${Math.round((frameTime + 5) * 30)}`
|
||||||
}
|
}
|
||||||
playing.value = true
|
playing.value = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -270,7 +270,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VideoPlayer v-if="playing" :file-uuid="currentVideo.fileUuid" :start-time="currentVideo.startTime" :end-time="currentVideo.endTime" :all-traces="allTraces" :merged-segments="mergedSegments" :initial-trace-idx="currentVideo.traceIdx" :title="currentVideo.title" @close="playing = false" />
|
<VideoPlayer v-if="playing" :file-uuid="currentVideo.fileUuid" :start-frame="currentVideo.startFrame" :end-frame="currentVideo.endFrame" :all-traces="allTraces" :merged-segments="mergedSegments" :initial-trace-idx="currentVideo.traceIdx" :title="currentVideo.title" @close="playing = false" />
|
||||||
|
|
||||||
<!-- Trace strip context menu -->
|
<!-- Trace strip context menu -->
|
||||||
<div v-if="traceCtxMenu.show" class="ms-ctx-menu" :style="{ left: traceCtxMenu.x + 'px', top: traceCtxMenu.y + 'px', display: 'block' }" @click.stop @mousedown.stop @pointerdown.stop>
|
<div v-if="traceCtxMenu.show" class="ms-ctx-menu" :style="{ left: traceCtxMenu.x + 'px', top: traceCtxMenu.y + 'px', display: 'block' }" @click.stop @mousedown.stop @pointerdown.stop>
|
||||||
@@ -300,7 +300,7 @@ const allPeople = ref<any[]>([])
|
|||||||
const allTraces = ref<any[]>([])
|
const allTraces = ref<any[]>([])
|
||||||
const mergedSegments = ref<any[]>([])
|
const mergedSegments = ref<any[]>([])
|
||||||
const playing = ref(false)
|
const playing = ref(false)
|
||||||
const currentVideo = ref({ fileUuid: '', startTime: 0, endTime: 0, traceIdx: 0, title: '' })
|
const currentVideo = ref({ fileUuid: '', startFrame: 0, endFrame: 0, traceIdx: 0, title: '' })
|
||||||
const showCandidates = ref(false)
|
const showCandidates = ref(false)
|
||||||
const showMerge = ref(false)
|
const showMerge = ref(false)
|
||||||
const mergeSearchQuery = ref('')
|
const mergeSearchQuery = ref('')
|
||||||
@@ -734,10 +734,10 @@ function playAllSegments() {
|
|||||||
const t = allTraces.value[0]
|
const t = allTraces.value[0]
|
||||||
currentVideo.value = {
|
currentVideo.value = {
|
||||||
fileUuid: t.file_uuid || '',
|
fileUuid: t.file_uuid || '',
|
||||||
startTime: t.first_sec || t.start_time || 0,
|
startFrame: t.first_frame || 0,
|
||||||
endTime: t.last_sec || t.end_time || 0,
|
endFrame: t.last_frame || (t.first_frame || 0) + 30,
|
||||||
traceIdx: 0,
|
traceIdx: 0,
|
||||||
title: `${person.value?.name} · ${card.count} segments`,
|
title: `F${t.first_frame || 0}-F${t.last_frame || 0} (${person.value?.name})`,
|
||||||
}
|
}
|
||||||
playing.value = true
|
playing.value = true
|
||||||
}
|
}
|
||||||
@@ -747,10 +747,10 @@ function playMerged(m: any) {
|
|||||||
if (!t) return
|
if (!t) return
|
||||||
currentVideo.value = {
|
currentVideo.value = {
|
||||||
fileUuid: t.file_uuid || '',
|
fileUuid: t.file_uuid || '',
|
||||||
startTime: t.first_sec || t.start_time || 0,
|
startFrame: t.first_frame || 0,
|
||||||
endTime: t.last_sec || t.end_time || 0,
|
endFrame: t.last_frame || (t.first_frame || 0) + 30,
|
||||||
traceIdx: m._startIdx,
|
traceIdx: m._startIdx,
|
||||||
title: `${person.value?.name} · #${m.start_frame}–#${m.end_frame}`,
|
title: `F${t.first_frame || 0}-F${t.last_frame || 0} (${person.value?.name})`,
|
||||||
}
|
}
|
||||||
playing.value = true
|
playing.value = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,8 +190,8 @@
|
|||||||
v-if="playing"
|
v-if="playing"
|
||||||
simple
|
simple
|
||||||
:file-uuid="currentVideo.fileUuid"
|
:file-uuid="currentVideo.fileUuid"
|
||||||
:start-time="currentVideo.startTime"
|
:start-frame="currentVideo.startFrame"
|
||||||
:end-time="currentVideo.endTime"
|
:end-frame="currentVideo.endFrame"
|
||||||
:title="currentVideo.title"
|
:title="currentVideo.title"
|
||||||
@close="playing = false"
|
@close="playing = false"
|
||||||
/>
|
/>
|
||||||
@@ -423,7 +423,7 @@ function ensureMinDuration(r: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const playing = ref(false)
|
const playing = ref(false)
|
||||||
const currentVideo = ref({ fileUuid: '', startTime: 0, endTime: 0, startFrame: null as number | null, endFrame: null as number | null, title: '' })
|
const currentVideo = ref({ fileUuid: '', startFrame: 0, endFrame: 0, title: '' })
|
||||||
|
|
||||||
const currentModeLabel = computed(() => modes.find(m => m.value === mode.value)?.label || 'Keyword')
|
const currentModeLabel = computed(() => modes.find(m => m.value === mode.value)?.label || 'Keyword')
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user