navidrome/core/ffmpeg/piped_header.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

61 lines
1.6 KiB
Go
Raw Normal View History

package ffmpeg
import (
"bytes"
"errors"
"io"
)
// patchPipedHeader repairs the header ffmpeg cannot finish when its output is a pipe,
// since it only learns the sample or frame count once it can no longer rewind.
func patchPipedHeader(r io.ReadCloser, duration float32) io.ReadCloser {
if duration <= 0 {
return r
}
return &headerPatcher{ReadCloser: r, duration: duration}
}
type headerPatcher struct {
io.ReadCloser
duration float32
// Peeking here rather than in the constructor keeps Transcode from blocking
// until ffmpeg has emitted its first bytes.
stream io.Reader
}
func (h *headerPatcher) Read(p []byte) (int, error) {
if h.stream == nil {
prefix, err := h.peek()
if err != nil {
return 0, err
}
h.stream = io.MultiReader(bytes.NewReader(prefix), h.ReadCloser)
}
return h.stream.Read(p)
}
// peek dispatches on the magic bytes rather than on the requested format, which is a
// free-text label on the transcoding profile and need not match what ffmpeg emits.
func (h *headerPatcher) peek() ([]byte, error) {
buf, err := h.fill(nil, 4)
if err != nil || len(buf) < 4 {
return buf, err
}
if string(buf) == "fLaC" {
return h.flacPrefix(buf)
}
return h.mp3Prefix(buf)
}
// fill grows buf to n bytes, stopping short when the stream ends first.
func (h *headerPatcher) fill(buf []byte, n int) ([]byte, error) {
if len(buf) >= n {
return buf, nil
}
more := make([]byte, n-len(buf))
read, err := io.ReadFull(h.ReadCloser, more)
if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) {
return buf, err
}
return append(buf, more[:read]...), nil
}