169 lines
4.5 KiB
Go
169 lines
4.5 KiB
Go
package media
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os/exec"
|
|
"strconv"
|
|
"sync"
|
|
)
|
|
|
|
// CommandFunc is a function that builds an *exec.Cmd from a context, name and
|
|
// args.
|
|
type CommandFunc func(ctx context.Context, name string, arg ...string) *exec.Cmd
|
|
|
|
// AudioFormat represents an abstract audio format, e.g. MP3 or WAV.
|
|
type AudioFormat int
|
|
|
|
const (
|
|
AudioFormatWAV AudioFormat = iota
|
|
AudioFormatMP3
|
|
)
|
|
|
|
// String implements fmt.Stringer.
|
|
func (f AudioFormat) String() string {
|
|
switch f {
|
|
case AudioFormatWAV:
|
|
return "wav"
|
|
case AudioFormatMP3:
|
|
return "mp3"
|
|
default:
|
|
panic("unknown audio format")
|
|
}
|
|
}
|
|
|
|
// AudioSegmentProgress represents a progress update for an AudioSegmentStream,
|
|
// and contains a byte slice of audio data and indication of the approximate
|
|
// progress.
|
|
type AudioSegmentProgress struct {
|
|
PercentComplete float32
|
|
Data []byte
|
|
}
|
|
|
|
// AudioSegmentStream is a stream of AudioSegmentProgress structs.
|
|
type AudioSegmentStream struct {
|
|
progressChan chan AudioSegmentProgress
|
|
errorChan chan error
|
|
}
|
|
|
|
// send publishes a new partial segment and progress update to the strean.
|
|
func (s *AudioSegmentStream) send(p []byte, percentComplete float32) {
|
|
s.progressChan <- AudioSegmentProgress{
|
|
Data: p,
|
|
PercentComplete: percentComplete,
|
|
}
|
|
}
|
|
|
|
// close signals the successful end of the stream of data.
|
|
func (s *AudioSegmentStream) close() {
|
|
close(s.progressChan)
|
|
}
|
|
|
|
// closeWithError signals the unsuccessful end of a stream of data.
|
|
func (s *AudioSegmentStream) closeWithError(err error) {
|
|
s.errorChan <- err
|
|
}
|
|
|
|
// audioSegmentGetter gets an audio segment and streams it to the caller.
|
|
type audioSegmentGetter struct {
|
|
mu sync.Mutex
|
|
commandFunc CommandFunc
|
|
workerPool *WorkerPool
|
|
rawAudio io.ReadCloser
|
|
channels int32
|
|
outFormat AudioFormat
|
|
stream *AudioSegmentStream
|
|
bytesRead, bytesExpected int64
|
|
}
|
|
|
|
// newAudioSegmentGetter returns a new audioSegmentGetter. The io.ReadCloser
|
|
// will be consumed and closed by the getAudioSegment() function.
|
|
func newAudioSegmentGetter(commandFunc CommandFunc, workerPool *WorkerPool, rawAudio io.ReadCloser, channels int32, bytesExpected int64, outFormat AudioFormat) *audioSegmentGetter {
|
|
return &audioSegmentGetter{
|
|
commandFunc: commandFunc,
|
|
workerPool: workerPool,
|
|
rawAudio: rawAudio,
|
|
channels: channels,
|
|
bytesExpected: bytesExpected,
|
|
outFormat: outFormat,
|
|
stream: &AudioSegmentStream{
|
|
progressChan: make(chan AudioSegmentProgress),
|
|
errorChan: make(chan error, 1),
|
|
},
|
|
}
|
|
}
|
|
|
|
// Read implements io.Reader and is consumed by the stdin of the FFMPEG
|
|
// command. It is called from a separate goroutine to Write().
|
|
func (s *audioSegmentGetter) Read(p []byte) (int, error) {
|
|
n, err := s.rawAudio.Read(p)
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.bytesRead += int64(n)
|
|
|
|
return n, err
|
|
}
|
|
|
|
// Write implements io.Writer and consumes the stdout of the FFMPEG command. It
|
|
// is called from a separate goroutine to Read().
|
|
func (s *audioSegmentGetter) Write(p []byte) (int, error) {
|
|
s.stream.send(p, s.percentComplete())
|
|
return len(p), nil
|
|
}
|
|
|
|
func (s *audioSegmentGetter) percentComplete() float32 {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
return (float32(s.bytesRead) / float32(s.bytesExpected)) * 100
|
|
}
|
|
|
|
// Next implements AudioSegmentStream.
|
|
func (s *AudioSegmentStream) Next(ctx context.Context) (AudioSegmentProgress, error) {
|
|
select {
|
|
case progress, ok := <-s.progressChan:
|
|
if !ok {
|
|
return AudioSegmentProgress{}, io.EOF
|
|
}
|
|
return progress, nil
|
|
case err := <-s.errorChan:
|
|
return AudioSegmentProgress{}, err
|
|
case <-ctx.Done():
|
|
return AudioSegmentProgress{}, ctx.Err()
|
|
}
|
|
}
|
|
|
|
func (s *audioSegmentGetter) getAudioSegment(ctx context.Context) {
|
|
defer s.rawAudio.Close()
|
|
|
|
err := s.workerPool.WaitForTask(ctx, func() error {
|
|
var stdErr bytes.Buffer
|
|
cmd := s.commandFunc(ctx, "ffmpeg", "-hide_banner", "-loglevel", "error", "-f", "s16le", "-ac", itoa(int(s.channels)), "-ar", itoa(rawAudioSampleRate), "-i", "-", "-f", s.outFormat.String(), "-")
|
|
cmd.Stderr = &stdErr
|
|
cmd.Stdin = s
|
|
cmd.Stdout = s
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
return fmt.Errorf("error starting command: %v, output: %s", err, stdErr.String())
|
|
}
|
|
|
|
if err := cmd.Wait(); err != nil {
|
|
return fmt.Errorf("error waiting for ffmpeg: %v, output: %s", err, stdErr.String())
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
s.stream.closeWithError(err)
|
|
return
|
|
}
|
|
|
|
s.stream.close()
|
|
}
|
|
|
|
func itoa(i int) string { return strconv.Itoa(i) }
|