mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-31 04:04:21 -04:00
Use new storage system for keyframes
This commit is contained in:
parent
5991916227
commit
cc45bb4656
@ -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 {
|
||||||
|
@ -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
|
IsDone bool
|
||||||
CanTransmux bool
|
info *KeyframeInfo
|
||||||
IsDone bool
|
|
||||||
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 GetKeyframes(sha string, path string) *Keyframe {
|
|
||||||
ret, _ := keyframes.GetOrCreate(sha, func() *Keyframe {
|
|
||||||
kf := &Keyframe{
|
|
||||||
Sha: sha,
|
|
||||||
IsDone: false,
|
|
||||||
info: &KeyframeInfo{},
|
|
||||||
}
|
|
||||||
kf.info.ready.Add(1)
|
|
||||||
go func() {
|
|
||||||
save_path := fmt.Sprintf("%s/%s/keyframes.json", Settings.Metadata, sha)
|
|
||||||
if err := getSavedInfo(save_path, kf); err == nil {
|
|
||||||
log.Printf("Using keyframes cache on filesystem for %s", path)
|
|
||||||
kf.info.ready.Done()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := getKeyframes(path, kf, sha)
|
|
||||||
if err == nil {
|
|
||||||
saveInfo(save_path, kf)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return kf
|
|
||||||
})
|
|
||||||
ret.info.ready.Wait()
|
|
||||||
return ret
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getKeyframes(path string, kf *Keyframe, sha string) error {
|
func (kf *Keyframe) Scan(src interface{}) error {
|
||||||
defer printExecTime("ffprobe analysis for %s", path)()
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
kf := &Keyframe{
|
||||||
|
IsDone: false,
|
||||||
|
info: &KeyframeInfo{},
|
||||||
|
}
|
||||||
|
|
||||||
|
var ready sync.WaitGroup
|
||||||
|
var err error
|
||||||
|
ready.Add(1)
|
||||||
|
go func() {
|
||||||
|
var table string
|
||||||
|
if isVideo {
|
||||||
|
table = "videos"
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.database.NamedExec(
|
||||||
|
fmt.Sprint(
|
||||||
|
`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)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
ready.Wait()
|
||||||
|
return set(kf, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrive video's keyframes and store them inside the kf var.
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
|
@ -5,10 +5,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type MetadataService struct {
|
type MetadataService struct {
|
||||||
database *sqlx.DB
|
database *sqlx.DB
|
||||||
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
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user