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 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, rawAudio io.ReadCloser, channels int32, bytesExpected int64, outFormat AudioFormat) *audioSegmentGetter { return &audioSegmentGetter{ commandFunc: commandFunc, 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() 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 { s.stream.closeWithError(fmt.Errorf("error starting command: %v, output: %s", err, stdErr.String())) return } if err := cmd.Wait(); err != nil { s.stream.closeWithError(fmt.Errorf("error waiting for ffmpeg: %v, output: %s", err, stdErr.String())) return } s.stream.close() } func itoa(i int) string { return strconv.Itoa(i) }