Use new storage system for keyframes

This commit is contained in:
Zoe Roux 2024-07-30 23:31:02 +02:00
parent 5991916227
commit cc45bb4656
3 changed files with 124 additions and 59 deletions

View File

@ -76,6 +76,9 @@ type Video struct {
Height uint32 `json:"height"` Height uint32 `json:"height"`
/// The average bitrate of the video in bytes/s /// The average bitrate of the video in bytes/s
Bitrate uint32 `json:"bitrate"` Bitrate uint32 `json:"bitrate"`
/// Keyframes of this video
Keyframes *Keyframe `json:"-"`
} }
type Audio struct { type Audio struct {
@ -91,6 +94,9 @@ type Audio struct {
MimeCodec *string `json:"mimeCodec" db:"mime_codec"` MimeCodec *string `json:"mimeCodec" db:"mime_codec"`
/// Is this stream the default one of it's type? /// Is this stream the default one of it's type?
IsDefault bool `json:"isDefault" db:"is_default"` IsDefault bool `json:"isDefault" db:"is_default"`
/// Keyframes of this video
Keyframes *Keyframe `json:"-"`
} }
type Subtitle struct { type Subtitle struct {

View File

@ -2,6 +2,8 @@ package src
import ( import (
"bufio" "bufio"
"database/sql/driver"
"errors"
"fmt" "fmt"
"log" "log"
"os/exec" "os/exec"
@ -10,16 +12,15 @@ import (
"sync" "sync"
) )
const KeyframeVersion = 1
type Keyframe struct { type Keyframe struct {
Sha string
Keyframes []float64 Keyframes []float64
CanTransmux bool
IsDone bool IsDone bool
info *KeyframeInfo info *KeyframeInfo
} }
type KeyframeInfo struct { type KeyframeInfo struct {
mutex sync.RWMutex mutex sync.RWMutex
ready sync.WaitGroup
listeners []func(keyframes []float64) listeners []func(keyframes []float64)
} }
@ -62,37 +63,86 @@ func (kf *Keyframe) AddListener(callback func(keyframes []float64)) {
kf.info.listeners = append(kf.info.listeners, callback) kf.info.listeners = append(kf.info.listeners, callback)
} }
var keyframes = NewCMap[string, *Keyframe]() func (kf *Keyframe) Value() (driver.Value, error) {
return driver.Value(kf.Keyframes), nil
}
func (kf *Keyframe) Scan(src interface{}) error {
switch src.(type) {
case []float64:
kf.Keyframes = src.([]float64)
kf.IsDone = true
kf.info = &KeyframeInfo{}
default:
return errors.New("incompatible type for keyframe in database")
}
return nil
}
type KeyframeKey struct {
Sha string
IsVideo bool
Index int
}
func (s *MetadataService) GetKeyframe(info *MediaInfo, isVideo bool, idx int) (*Keyframe, error) {
get_running, set := s.keyframeLock.Start(KeyframeKey{
Sha: info.Sha,
IsVideo: isVideo,
Index: idx,
})
if get_running != nil {
return get_running()
}
func GetKeyframes(sha string, path string) *Keyframe {
ret, _ := keyframes.GetOrCreate(sha, func() *Keyframe {
kf := &Keyframe{ kf := &Keyframe{
Sha: sha,
IsDone: false, IsDone: false,
info: &KeyframeInfo{}, info: &KeyframeInfo{},
} }
kf.info.ready.Add(1)
var ready sync.WaitGroup
var err error
ready.Add(1)
go func() { go func() {
save_path := fmt.Sprintf("%s/%s/keyframes.json", Settings.Metadata, sha) var table string
if err := getSavedInfo(save_path, kf); err == nil { if isVideo {
log.Printf("Using keyframes cache on filesystem for %s", path) table = "videos"
kf.info.ready.Done() err = getVideoKeyframes(info.Path, idx, kf, &ready)
} else {
table = "audios"
err = getAudioKeyframes(info, idx, kf, &ready)
}
if err != nil {
log.Printf("Couldn't retrive keyframes for %s %s %d: %v", info.Path, table, idx, err)
return return
} }
err := getKeyframes(path, kf, sha) _, err = s.database.NamedExec(
if err == nil { fmt.Sprint(
saveInfo(save_path, kf) `update %s set keyframes = :keyframes, ver_keyframes = :version where sha = :sha and idx = :idx`,
table,
),
map[string]interface{}{
"sha": info.Sha,
"idx": idx,
"keyframes": kf.Keyframes,
"version": KeyframeVersion,
},
)
if err != nil {
log.Printf("Couldn't store keyframes on database: %v", err)
} }
}() }()
return kf ready.Wait()
}) return set(kf, err)
ret.info.ready.Wait()
return ret
} }
func getKeyframes(path string, kf *Keyframe, sha string) error { // Retrive video's keyframes and store them inside the kf var.
defer printExecTime("ffprobe analysis for %s", path)() // Returns when all key frames are retrived (or an error occurs)
// ready.Done() is called when more than 100 are retrived (or extraction is done)
func getVideoKeyframes(path string, video_idx int, kf *Keyframe, ready *sync.WaitGroup) error {
defer printExecTime("ffprobe keyframe analysis for %s video n%d", path, video_idx)()
// run ffprobe to return all IFrames, IFrames are points where we can split the video in segments. // run ffprobe to return all IFrames, IFrames are points where we can split the video in segments.
// We ask ffprobe to return the time of each frame and it's flags // We ask ffprobe to return the time of each frame and it's flags
// We could ask it to return only i-frames (keyframes) with the -skip_frame nokey but using it is extremly slow // We could ask it to return only i-frames (keyframes) with the -skip_frame nokey but using it is extremly slow
@ -100,7 +150,7 @@ func getKeyframes(path string, kf *Keyframe, sha string) error {
cmd := exec.Command( cmd := exec.Command(
"ffprobe", "ffprobe",
"-loglevel", "error", "-loglevel", "error",
"-select_streams", "v:0", "-select_streams", fmt.Sprint("V:%d", video_idx),
"-show_entries", "packet=pts_time,flags", "-show_entries", "packet=pts_time,flags",
"-of", "csv=print_section=0", "-of", "csv=print_section=0",
path, path,
@ -159,7 +209,7 @@ func getKeyframes(path string, kf *Keyframe, sha string) error {
if len(ret) == max { if len(ret) == max {
kf.add(ret) kf.add(ret)
if done == 0 { if done == 0 {
kf.info.ready.Done() ready.Done()
} else if done >= 500 { } else if done >= 500 {
max = 500 max = 500
} }
@ -168,32 +218,23 @@ func getKeyframes(path string, kf *Keyframe, sha string) error {
ret = ret[:0] ret = ret[:0]
} }
} }
// If there is less than 2 (i.e. equals 0 or 1 (it happens for audio files with poster))
if len(ret) < 2 {
dummy, err := getDummyKeyframes(path, sha)
if err != nil {
return err
}
ret = dummy
}
kf.add(ret) kf.add(ret)
if done == 0 { if done == 0 {
kf.info.ready.Done() ready.Done()
} }
kf.IsDone = true kf.IsDone = true
return nil return nil
} }
func getDummyKeyframes(path string, sha string) ([]float64, error) { func getAudioKeyframes(info *MediaInfo, audio_idx int, kf *Keyframe, ready *sync.WaitGroup) error {
dummyKeyframeDuration := float64(2) dummyKeyframeDuration := float64(4)
info, err := GetInfo(path, sha)
if err != nil {
return nil, err
}
segmentCount := int((float64(info.Duration) / dummyKeyframeDuration) + 1) segmentCount := int((float64(info.Duration) / dummyKeyframeDuration) + 1)
ret := make([]float64, segmentCount) kf.Keyframes = make([]float64, segmentCount)
for segmentIndex := 0; segmentIndex < segmentCount; segmentIndex += 1 { for segmentIndex := 0; segmentIndex < segmentCount; segmentIndex += 1 {
ret[segmentIndex] = float64(segmentIndex) * dummyKeyframeDuration kf.Keyframes[segmentIndex] = float64(segmentIndex) * dummyKeyframeDuration
} }
return ret, nil
ready.Done()
kf.IsDone = true
return nil
} }

View File

@ -9,6 +9,7 @@ type MetadataService struct {
lock *RunLock[string, *MediaInfo] lock *RunLock[string, *MediaInfo]
thumbLock *RunLock[string, interface{}] thumbLock *RunLock[string, interface{}]
extractLock *RunLock[string, interface{}] extractLock *RunLock[string, interface{}]
keyframeLock *RunLock[KeyframeKey, *Keyframe]
} }
func (s MetadataService) GetMetadata(path string, sha string) (*MediaInfo, error) { func (s MetadataService) GetMetadata(path string, sha string) (*MediaInfo, error) {
@ -23,6 +24,23 @@ func (s MetadataService) GetMetadata(path string, sha string) (*MediaInfo, error
if ret.Versions.Extract < ExtractVersion { if ret.Versions.Extract < ExtractVersion {
go s.ExtractSubs(ret) go s.ExtractSubs(ret)
} }
if ret.Versions.Keyframes < KeyframeVersion && ret.Versions.Keyframes != 0 {
for _, video := range ret.Videos {
video.Keyframes = nil
}
for _, audio := range ret.Audios {
audio.Keyframes = nil
}
s.database.NamedExec(`
update videos set keyframes = nil where sha = :sha;
update audios set keyframes = nil where sha = :sha;
update info set ver_keyframes = 0 where sha = :sha;
`,
map[string]interface{}{
"sha": sha,
},
)
}
return ret, nil return ret, nil
} }