Fix bug of incomplete waveform when playing new track twice in quick succession
This commit is contained in:
+49
-7
@@ -26,8 +26,10 @@ type AudioCache struct {
|
||||
}
|
||||
|
||||
type cacheEntry struct {
|
||||
done bool
|
||||
cancel context.CancelFunc
|
||||
done bool
|
||||
refCount int
|
||||
pendingDeletion bool
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// AudioCacheRequest represents a request to prefetch and cache an audio file.
|
||||
@@ -62,19 +64,52 @@ func (a *AudioCache) PathForCachedFile(id string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// IsFullyDownloaded returns true if the file for the given id is fully downloaded.
|
||||
func (a *AudioCache) IsFullyDownloaded(id string) bool {
|
||||
return a.PathForCachedFile(id) != ""
|
||||
}
|
||||
|
||||
// PathForCachedFile returns the local filesystem path for a cached track,
|
||||
// including one that is in the process of downloading.
|
||||
// If it is not cached or downloading, it returns an empty string.
|
||||
func (a *AudioCache) PathForCachedOrDownloadingFile(id string) string {
|
||||
return a.pathForCachedOrDownloadingFile(id, false)
|
||||
}
|
||||
|
||||
// ObtainReferenceToFile returns the local filesystem path for a cached track,
|
||||
// including one that is in the process of downloading, and obtains a refernce
|
||||
// to it such that it will not be deleted until ReleaseReferenceToFile is called.
|
||||
|
||||
// If it is not cached or downloading, it returns an empty string.
|
||||
func (a *AudioCache) ObtainReferenceToFile(id string) string {
|
||||
return a.pathForCachedOrDownloadingFile(id, true)
|
||||
}
|
||||
|
||||
func (a *AudioCache) pathForCachedOrDownloadingFile(id string, obtainReference bool) string {
|
||||
a.mutex.Lock()
|
||||
defer a.mutex.Unlock()
|
||||
|
||||
if _, ok := a.entries[id]; ok {
|
||||
if entry, ok := a.entries[id]; ok {
|
||||
if obtainReference {
|
||||
entry.refCount++
|
||||
}
|
||||
return a.pathForID(id)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *AudioCache) ReleaseReferenceToFile(id string) {
|
||||
a.mutex.Lock()
|
||||
defer a.mutex.Unlock()
|
||||
|
||||
if e, ok := a.entries[id]; ok {
|
||||
e.refCount--
|
||||
if e.refCount == 0 && e.pendingDeletion {
|
||||
a.deleteEntry(id, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CacheFile begins downloading a file (if not already downloading) and stores it
|
||||
// to the cache directory under its ID as filename. The download is asynchronous.
|
||||
func (a *AudioCache) CacheFile(id, dlURL string) {
|
||||
@@ -119,10 +154,11 @@ func (a *AudioCache) CacheOnly(keep string, fetch []AudioCacheRequest) {
|
||||
if id != keep && !slices.ContainsFunc(fetch, func(a AudioCacheRequest) bool {
|
||||
return a.ID == id
|
||||
}) {
|
||||
_ = e
|
||||
e.cancel()
|
||||
_ = os.Remove(a.pathForID(id))
|
||||
delete(a.entries, id)
|
||||
if e.refCount == 0 {
|
||||
a.deleteEntry(id, e)
|
||||
} else {
|
||||
e.pendingDeletion = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,3 +192,9 @@ func (a *AudioCache) Shutdown() {
|
||||
func (a *AudioCache) pathForID(id string) string {
|
||||
return filepath.Join(a.baseCacheDir, id)
|
||||
}
|
||||
|
||||
func (a *AudioCache) deleteEntry(id string, e *cacheEntry) {
|
||||
e.cancel()
|
||||
_ = os.Remove(a.pathForID(id))
|
||||
delete(a.entries, id)
|
||||
}
|
||||
|
||||
@@ -78,9 +78,9 @@ func NewPlaybackManager(
|
||||
return pm
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) findWfmImageJob(id string) (*WaveformImageJob, bool) {
|
||||
func (p *PlaybackManager) findWfmImageJob(id string, uncanceledOnly bool) (*WaveformImageJob, bool) {
|
||||
for _, j := range p.wfmImageJobs {
|
||||
if j != nil && j.ItemID == id {
|
||||
if j != nil && j.ItemID == id && (!uncanceledOnly || !j.Canceled()) {
|
||||
return j, true
|
||||
}
|
||||
}
|
||||
@@ -106,8 +106,10 @@ func (p *PlaybackManager) addOnTrackChangeHook() {
|
||||
p.engine.onBeforeSongChange = append(p.engine.onBeforeSongChange, func(item mediaprovider.MediaItem) {
|
||||
if p.engine.playbackCfg.UseWaveformSeekbar {
|
||||
if p.wfmGen != nil && item != nil && item.Metadata().Type == mediaprovider.MediaItemTypeTrack {
|
||||
// start generating waveform image for next-up track
|
||||
p.addWfmImageJob(p.wfmGen.StartWaveformGeneration(item.(*mediaprovider.Track)))
|
||||
if _, ok := p.findWfmImageJob(item.Metadata().ID, true); !ok {
|
||||
// start generating waveform image for next-up track
|
||||
p.addWfmImageJob(p.wfmGen.StartWaveformGeneration(item.(*mediaprovider.Track)))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -168,13 +170,15 @@ func (p *PlaybackManager) handleWaveformImageSongChange(item mediaprovider.Media
|
||||
|
||||
if item != nil {
|
||||
// cancel possible waveform generation job for previous track
|
||||
if old, ok := p.findWfmImageJob(p.lastPlayingID); ok {
|
||||
old.Cancel()
|
||||
if p.lastPlayingID != item.Metadata().ID {
|
||||
if old, ok := p.findWfmImageJob(p.lastPlayingID, false); ok {
|
||||
old.Cancel()
|
||||
}
|
||||
p.lastPlayingID = item.Metadata().ID
|
||||
}
|
||||
p.lastPlayingID = item.Metadata().ID
|
||||
|
||||
var job *WaveformImageJob
|
||||
if j, ok := p.findWfmImageJob(item.Metadata().ID); ok {
|
||||
if j, ok := p.findWfmImageJob(item.Metadata().ID, true); ok {
|
||||
job = j
|
||||
} else if tr, ok := item.(*mediaprovider.Track); ok {
|
||||
job = p.wfmGen.StartWaveformGeneration(tr)
|
||||
|
||||
@@ -77,14 +77,25 @@ func (fs *FileStreamerServer) streamHandler(w http.ResponseWriter, _ *http.Reque
|
||||
|
||||
flusher, canFlush := w.(http.Flusher)
|
||||
|
||||
bytesRead := int64(0)
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
complete := fs.IsComplete()
|
||||
if !complete {
|
||||
if s, err := os.Stat(fs.Path); err == nil {
|
||||
// make sure we don't read near EOF until file is complete
|
||||
maxToRead := max(0, s.Size()-bytesRead-1024) /*safety buffer*/
|
||||
buf = buf[:min(int64(cap(buf)), maxToRead)]
|
||||
}
|
||||
} else {
|
||||
buf = buf[:cap(buf)]
|
||||
}
|
||||
n, err := file.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
log.Printf("read error: %v", err)
|
||||
break
|
||||
}
|
||||
bytesRead += int64(n)
|
||||
|
||||
if n > 0 {
|
||||
_, err := w.Write(buf[:n])
|
||||
|
||||
+27
-14
@@ -48,17 +48,22 @@ type WaveformImageJob struct {
|
||||
progress int // first invalid pixel in X direction
|
||||
done bool
|
||||
cancel func()
|
||||
|
||||
step int
|
||||
canceled bool
|
||||
}
|
||||
|
||||
func (w *WaveformImageJob) Cancel() {
|
||||
if w != nil && w.cancel != nil {
|
||||
w.step = -1
|
||||
w.cancel()
|
||||
if w != nil {
|
||||
w.canceled = true
|
||||
if w.cancel != nil {
|
||||
w.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WaveformImageJob) Canceled() bool {
|
||||
return w.canceled
|
||||
}
|
||||
|
||||
func (w *WaveformImageJob) Done() bool {
|
||||
w.lock.Lock()
|
||||
defer w.lock.Unlock()
|
||||
@@ -109,7 +114,7 @@ func (w *WaveformImageGenerator) StartWaveformGeneration(item *mediaprovider.Tra
|
||||
// 3. Begin analyzing the resulting WAV file
|
||||
// 4. Begin generating the image from the analysis data
|
||||
go func() {
|
||||
path := w.audioCache.PathForCachedOrDownloadingFile(job.ItemID)
|
||||
path := w.audioCache.ObtainReferenceToFile(job.ItemID)
|
||||
// wait for file to begin downloading if not already
|
||||
for path == "" {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
@@ -117,7 +122,7 @@ func (w *WaveformImageGenerator) StartWaveformGeneration(item *mediaprovider.Tra
|
||||
job.setError(e)
|
||||
return
|
||||
}
|
||||
path = w.audioCache.PathForCachedOrDownloadingFile(job.ItemID)
|
||||
path = w.audioCache.ObtainReferenceToFile(job.ItemID)
|
||||
}
|
||||
// and wait for content to begin being written
|
||||
for {
|
||||
@@ -130,13 +135,22 @@ func (w *WaveformImageGenerator) StartWaveformGeneration(item *mediaprovider.Tra
|
||||
return
|
||||
}
|
||||
}
|
||||
job.step = 1
|
||||
|
||||
dir := filepath.Dir(path)
|
||||
transcodeFile := filepath.Join(dir, filepath.Base(path)+"_waveform.wav")
|
||||
var transcodeFile string
|
||||
for i := 0; true; i++ {
|
||||
if i > 0 {
|
||||
transcodeFile = filepath.Join(dir, fmt.Sprintf("%s_waveform_%d.wav", filepath.Base(path), i))
|
||||
} else {
|
||||
transcodeFile = filepath.Join(dir, filepath.Base(path)+"_waveform.wav")
|
||||
}
|
||||
if _, err := os.Stat(transcodeFile); os.IsNotExist(err) {
|
||||
break // found a suitable filename that doesn't exist
|
||||
}
|
||||
}
|
||||
|
||||
fileDone := func() bool {
|
||||
return w.audioCache.PathForCachedFile(job.ItemID) != ""
|
||||
return w.audioCache.IsFullyDownloaded(job.ItemID)
|
||||
}
|
||||
|
||||
// If file isn't fully downloaded from server,
|
||||
@@ -158,8 +172,7 @@ func (w *WaveformImageGenerator) StartWaveformGeneration(item *mediaprovider.Tra
|
||||
// Start converting the file to WAV for analysis
|
||||
var wavConvertDone bool
|
||||
go func() {
|
||||
job.step = 2
|
||||
err := convertToWav(ctx, path, transcodeFile)
|
||||
err := w.convertToWav(ctx, job.ItemID, path, transcodeFile)
|
||||
wavConvertDone = true
|
||||
if err != nil {
|
||||
job.setError(err)
|
||||
@@ -177,7 +190,6 @@ func (w *WaveformImageGenerator) StartWaveformGeneration(item *mediaprovider.Tra
|
||||
return
|
||||
}
|
||||
}
|
||||
job.step = 3
|
||||
|
||||
// Start analyzing the converted wav file
|
||||
data := &waveformData{}
|
||||
@@ -392,7 +404,7 @@ func float64ToByte(val float64) byte {
|
||||
return byte(val * 255)
|
||||
}
|
||||
|
||||
func convertToWav(ctx context.Context, inPath, outPath string) error {
|
||||
func (w *WaveformImageGenerator) convertToWav(ctx context.Context, id, inPath, outPath string) error {
|
||||
m := mpv.Create()
|
||||
m.SetOptionString("video", "no")
|
||||
m.SetOptionString("audio-display", "no")
|
||||
@@ -413,6 +425,7 @@ func convertToWav(ctx context.Context, inPath, outPath string) error {
|
||||
defer m.TerminateDestroy()
|
||||
|
||||
m.Command([]string{"loadfile", inPath, "replace"})
|
||||
defer w.audioCache.ReleaseReferenceToFile(id)
|
||||
|
||||
// Wait for MPV idle or ctx expiry
|
||||
for {
|
||||
|
||||
Reference in New Issue
Block a user