clapper / src /services /renderer /useRenderer.ts
jbilcke-hf's picture
jbilcke-hf HF staff
add linter and prettifier
c1f12bf
raw
history blame
No virus
9.53 kB
'use client'
import { create } from 'zustand'
import { ClapOutputType, ClapSegmentCategory } from '@aitube/clap'
import { BufferedSegments, RendererStore } from '@aitube/clapper-services'
import { TimelineStore, useTimeline, TimelineSegment } from '@aitube/timeline'
import { getDefaultRendererState } from './getDefaultRendererState'
import { getSegmentCacheKey } from './getSegmentCacheKey'
import { getDefaultBufferedSegments } from './getDefaultBufferedSegments'
export const useRenderer = create<RendererStore>((set, get) => ({
...getDefaultRendererState(),
clear: () => {
set({
...getDefaultRendererState(),
})
},
// this will be called at 60 FPS - and yes, it is expensive
// we could probably improve things by using a temporal tree index
renderLoop: (jumpedSomewhere?: boolean): BufferedSegments => {
const {
computeBufferedSegments,
bufferedSegments,
dataUriBuffer1: previousDataUriBuffer1,
dataUriBuffer2: previousDataUriBuffer2,
dataUriBuffer1Key: previousDataUriBuffer1Key,
dataUriBuffer2Key: previousDataUriBuffer2Key,
activeBufferNumber: previousActiveBufferNumber,
} = get()
// note: although useRequestAnimationFrame is called at 60 FPS,
// computeBufferedSegments has a throttle since it is expensive
const maybeNewBufferedSegments = computeBufferedSegments()
const activeSegmentsChanged =
maybeNewBufferedSegments.activeSegmentsCacheKey !==
bufferedSegments.activeSegmentsCacheKey
const upcomingSegmentsChanged =
maybeNewBufferedSegments.upcomingSegmentsCacheKey !==
bufferedSegments.upcomingSegmentsCacheKey
if (activeSegmentsChanged || upcomingSegmentsChanged) {
const maybeNewCurrentSegment = maybeNewBufferedSegments.activeVideoSegment
?.assetUrl
? maybeNewBufferedSegments.activeVideoSegment
: maybeNewBufferedSegments.activeStoryboardSegment?.assetUrl
? maybeNewBufferedSegments.activeStoryboardSegment
: undefined
// the upcoming asset we want to preload (note: we just want to preload it, not display it just yet)
const maybeNewPreloadSegment = maybeNewBufferedSegments
.upcomingVideoSegment?.assetUrl
? maybeNewBufferedSegments.upcomingVideoSegment
: maybeNewBufferedSegments.upcomingStoryboardSegment?.assetUrl
? maybeNewBufferedSegments.upcomingStoryboardSegment
: undefined
// performance optimization:
// we only look at the first part since it might be huge
// for assets, using a smaller header lookup like 256 or even 512 doesn't seem to be enough
const newCurrentSegmentKey =
`${maybeNewCurrentSegment?.assetUrl || ''}`.slice(0, 1024)
const newPreloadSegmentKey =
`${maybeNewPreloadSegment?.assetUrl || ''}`.slice(0, 1024)
let newDataUriBuffer1 = previousDataUriBuffer1
let newDataUriBuffer2 = previousDataUriBuffer2
let newDataUriBuffer1Key = `${newDataUriBuffer1?.assetUrl || ''}`.slice(
0,
1024
)
let newDataUriBuffer2Key = `${newDataUriBuffer2?.assetUrl || ''}`.slice(
0,
1024
)
if (jumpedSomewhere) {
// if we jumped somewhere we need to change the visible buffer
if (previousActiveBufferNumber == 2) {
// visible buffer is #2
if (newCurrentSegmentKey !== previousDataUriBuffer1Key) {
// we thus write to visible buffer (#1)
newDataUriBuffer1 = maybeNewCurrentSegment
newDataUriBuffer1Key = `${newDataUriBuffer1?.assetUrl || ''}`.slice(
0,
1024
)
}
} else {
// visible buffer is #1
if (newCurrentSegmentKey !== previousDataUriBuffer2Key) {
// we thus write to visible buffer (#2)
newDataUriBuffer2 = maybeNewCurrentSegment
newDataUriBuffer2Key = `${newDataUriBuffer2?.assetUrl || ''}`.slice(
0,
1024
)
}
}
}
// otherwise we do the predictive stuff as usual
set({
bufferedSegments: maybeNewBufferedSegments,
currentSegment: maybeNewCurrentSegment,
preloadSegment: maybeNewPreloadSegment,
currentSegmentKey: newCurrentSegmentKey,
preloadSegmentKey: newPreloadSegmentKey,
dataUriBuffer1Key: newDataUriBuffer1Key,
dataUriBuffer2Key: newDataUriBuffer2Key,
dataUriBuffer1: newDataUriBuffer1,
dataUriBuffer2: newDataUriBuffer2,
})
return maybeNewBufferedSegments
}
return bufferedSegments
},
computeBufferedSegments: (): BufferedSegments => {
const timelineState: TimelineStore = useTimeline.getState()
const { cursorTimestampAtInMs, segments: clapSegments } = timelineState
const segments = clapSegments as TimelineSegment[]
const results: BufferedSegments = getDefaultBufferedSegments()
// we could use a temporal index to keep things efficient here
// thiere is this relatively recent algorithm, the IB+ Tree,
// which seems to be good for what we want to do
// https://www.npmjs.com/package/i2bplustree
// another solution could be to filter the segments into multiple arrays by category,
// although we will have to see when those segments are re-computed / synced
for (const segment of segments) {
const inActiveShot =
segment.startTimeInMs <= cursorTimestampAtInMs &&
cursorTimestampAtInMs < segment.endTimeInMs
if (inActiveShot) {
const isActiveVideo =
segment.category === ClapSegmentCategory.VIDEO && segment.assetUrl
if (isActiveVideo) {
results.activeSegments.push(segment)
results.activeVideoSegment = segment
results.activeSegmentsCacheKey = getSegmentCacheKey(
segment,
results.activeSegmentsCacheKey
)
continue
}
const isActiveAudio =
// IF this is an audio segment
segment.outputType === ClapOutputType.AUDIO &&
// AND there is an actual audio buffer attached to it
segment.audioBuffer
if (isActiveAudio) {
results.activeSegments.push(segment)
results.activeAudioSegments.push(segment)
results.activeSegmentsCacheKey = getSegmentCacheKey(
segment,
results.activeSegmentsCacheKey
)
continue
}
const isActiveStoryboard =
segment.category === ClapSegmentCategory.STORYBOARD &&
segment.assetUrl
if (isActiveStoryboard) {
results.activeSegments.push(segment)
results.activeStoryboardSegment = segment
results.activeSegmentsCacheKey = getSegmentCacheKey(
segment,
results.activeSegmentsCacheKey
)
continue
}
results.activeSegments.push(segment)
results.activeSegmentsCacheKey = getSegmentCacheKey(
segment,
results.activeSegmentsCacheKey
)
continue
}
const existingVideoShot: TimelineSegment | undefined =
results.upcomingVideoSegment
const isUpcoming = segment.startTimeInMs > cursorTimestampAtInMs
if (isUpcoming) {
// for the "upcoming" video we need to find the closest one
// in terms of distance with the current cursor position
// otherwise we populare the upcoming segments,
// if we find the closests candidates
const isVideo =
segment.category === ClapSegmentCategory.VIDEO && segment.assetUrl
// const isUpcomingStoryboard = segment.category === ClapSegmentCategory.STORYBOARD && segment.assetUrl
if (isVideo) {
// if the candidate isn't "more upcoming" than the current result,
// then we continue
if (
existingVideoShot &&
existingVideoShot.startTimeInMs < segment.startTimeInMs
) {
continue
}
results.upcomingVideoSegment = segment
continue
}
const isStoryboard =
segment.category === ClapSegmentCategory.STORYBOARD &&
segment.assetUrl
if (isStoryboard && !results.upcomingStoryboardSegment) {
results.upcomingStoryboardSegment = segment
continue
}
}
}
results.upcomingSegments = []
results.upcomingSegmentsCacheKey = ''
if (results.upcomingVideoSegment) {
results.upcomingSegments.push(results.upcomingVideoSegment)
results.upcomingSegmentsCacheKey = getSegmentCacheKey(
results.upcomingVideoSegment,
''
)
}
if (results.upcomingStoryboardSegment) {
results.upcomingSegments.push(results.upcomingStoryboardSegment)
results.upcomingSegmentsCacheKey = getSegmentCacheKey(
results.upcomingStoryboardSegment,
results.upcomingSegmentsCacheKey
)
}
return results
},
setDataUriBuffer1: (dataUriBuffer1?: TimelineSegment) => {
set({ dataUriBuffer1 })
},
setDataUriBuffer2: (dataUriBuffer2?: TimelineSegment) => {
set({ dataUriBuffer2 })
},
setActiveBufferNumber: (activeBufferNumber: number) => {
set({ activeBufferNumber })
},
syncVideoToCurrentCursorPosition: () => {
const timeline: TimelineStore = useTimeline.getState()
// @TODO julian: make sure we play the video at the correct time
console.log(`syncing Video..`)
},
}))