diff --git a/transcoder/src/info.go b/transcoder/src/info.go index 768d89e8..87e4d453 100644 --- a/transcoder/src/info.go +++ b/transcoder/src/info.go @@ -76,6 +76,9 @@ type Video struct { Height uint32 `json:"height"` /// The average bitrate of the video in bytes/s Bitrate uint32 `json:"bitrate"` + + /// Keyframes of this video + Keyframes *Keyframe `json:"-"` } type Audio struct { @@ -91,6 +94,9 @@ type Audio struct { MimeCodec *string `json:"mimeCodec" db:"mime_codec"` /// Is this stream the default one of it's type? IsDefault bool `json:"isDefault" db:"is_default"` + + /// Keyframes of this video + Keyframes *Keyframe `json:"-"` } type Subtitle struct { diff --git a/transcoder/src/keyframes.go b/transcoder/src/keyframes.go index c687a6f6..b81f0ec4 100644 --- a/transcoder/src/keyframes.go +++ b/transcoder/src/keyframes.go @@ -2,6 +2,8 @@ package src import ( "bufio" + "database/sql/driver" + "errors" "fmt" "log" "os/exec" @@ -10,16 +12,15 @@ import ( "sync" ) +const KeyframeVersion = 1 + type Keyframe struct { - Sha string - Keyframes []float64 - CanTransmux bool - IsDone bool - info *KeyframeInfo + Keyframes []float64 + IsDone bool + info *KeyframeInfo } type KeyframeInfo struct { mutex sync.RWMutex - ready sync.WaitGroup listeners []func(keyframes []float64) } @@ -62,37 +63,86 @@ func (kf *Keyframe) AddListener(callback func(keyframes []float64)) { kf.info.listeners = append(kf.info.listeners, callback) } -var keyframes = NewCMap[string, *Keyframe]() - -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 (kf *Keyframe) Value() (driver.Value, error) { + return driver.Value(kf.Keyframes), nil } -func getKeyframes(path string, kf *Keyframe, sha string) error { - defer printExecTime("ffprobe analysis for %s", path)() +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() + } + + 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. // 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 @@ -100,7 +150,7 @@ func getKeyframes(path string, kf *Keyframe, sha string) error { cmd := exec.Command( "ffprobe", "-loglevel", "error", - "-select_streams", "v:0", + "-select_streams", fmt.Sprint("V:%d", video_idx), "-show_entries", "packet=pts_time,flags", "-of", "csv=print_section=0", path, @@ -159,7 +209,7 @@ func getKeyframes(path string, kf *Keyframe, sha string) error { if len(ret) == max { kf.add(ret) if done == 0 { - kf.info.ready.Done() + ready.Done() } else if done >= 500 { max = 500 } @@ -168,32 +218,23 @@ func getKeyframes(path string, kf *Keyframe, sha string) error { 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) if done == 0 { - kf.info.ready.Done() + ready.Done() } kf.IsDone = true return nil } -func getDummyKeyframes(path string, sha string) ([]float64, error) { - dummyKeyframeDuration := float64(2) - info, err := GetInfo(path, sha) - if err != nil { - return nil, err - } +func getAudioKeyframes(info *MediaInfo, audio_idx int, kf *Keyframe, ready *sync.WaitGroup) error { + dummyKeyframeDuration := float64(4) segmentCount := int((float64(info.Duration) / dummyKeyframeDuration) + 1) - ret := make([]float64, segmentCount) + kf.Keyframes = make([]float64, segmentCount) 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 } diff --git a/transcoder/src/metadata.go b/transcoder/src/metadata.go index 13cc7063..32d5bd45 100644 --- a/transcoder/src/metadata.go +++ b/transcoder/src/metadata.go @@ -5,10 +5,11 @@ import ( ) type MetadataService struct { - database *sqlx.DB - lock *RunLock[string, *MediaInfo] - thumbLock *RunLock[string, interface{}] - extractLock *RunLock[string, interface{}] + database *sqlx.DB + lock *RunLock[string, *MediaInfo] + thumbLock *RunLock[string, interface{}] + extractLock *RunLock[string, interface{}] + keyframeLock *RunLock[KeyframeKey, *Keyframe] } 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 { 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 }