navidrome/core/ffmpeg/ffmpeg.go

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

196 lines
4.6 KiB
Go
Raw Normal View History

2022-12-20 18:25:47 +01:00
package ffmpeg
import (
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"strconv"
"strings"
"sync"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
)
2022-12-20 18:25:47 +01:00
type FFmpeg interface {
Transcode(ctx context.Context, command, path string, maxBitRate int) (io.ReadCloser, error)
ExtractImage(ctx context.Context, path string) (io.ReadCloser, error)
Jukebox mode (#2289) * Adding cache directory to ignore-list * Adding jukebox-related config options * Adding DevEnableJukebox config option pls. dummy server * Adding types and routers * Now without panic * First draft on parsing the action * Some cleanups * Adding playback server * Verify audio device configuration * Adding debug-build target to have full symbol support * Adding beep sound library pls some example code. Not working yet * Play a fixed mp3 on any interface access for testing purposes * Put action code into separate file, adding stringer, more debug output, prepare structs, validation * Put action parameter parser code where it belongs * Have a single Action transporting all information * User fmt.Errorf for error-generation * Adding wide playback interface * Use action map for parsing, stringer instead switch stmt. * Use but only one switch case and direct dispatch, refactoring * Add error handling and pushing to client * send decent errormessage, no internal server error * Adding playback devices slice and load it from config * Combine config-verification and structure init * Return user-specific device * Separate playback server from device * Use dataStore to retrieve mediafile by id * WIP: Playlist and start/stop handling. Doing start/stop the hard way as of now * WIP: set, start and stop work on one single song. More to come * Dont need to wait for the end * Merge jukebox_action.go into jukebox.go * Remove getParameterAsInt64(). Use existing requiredParamInt() instead * Dont need to call newFailure() explicitly * Remove int64, use int instead. * Add and set action now accept multiple ids * Kickout copy of childFromMediaFile(). It is not needed here. * Refactoring devices and playbackServer * Turn (internal) playback.DeviceStatus into subsonic JukeboxStatus when rendering output. Indexes int64 -> int * Now we have a position and playing status * Switching gain to float32, xs:float is defined as 32 bit. Fixing nasty copy/pointer bug * Now with volume control * Start working the queue * Remove user from device interface * Rename function GetDevice -> GetDeviceForUser to make intention clearer * Have a nice stringer for the queue * User Prepared boolean for now to allow pause/unpause * Skipping works, but without offsets * Make ChildFromMediaFile public to be used in jukebox get() implementation * Return position in seconds and implement offset-skip in seconds * Default offset to 0 * Adding a simple setGain implementation * Prepare for transcoding AAC * WIP: transcode to WAV to use beeps wav decoder. Not done yet. * WIP: out of sheer desparation: convert to MP3 (which works) rather than WAV to troubleshoot issue. * Use FLAC as intermediate format to play Apple AAC * A bit of cleanup * Catching the end-of-stream event for further reactions * Have a trackSwitching goroutine waiting on channel when track ends * Move decoder code into own file. Restructure code a bit * Now with going on to play the next song in the playlist * Adding shuffle feature * Implementing remove action * Cleanup code * Remove templates for ffmpeg mp3 generation. Not needed anymore. * Adding some documentation * Check whether offset into track is in range. Fixing potential remove track bug. Documentation * Make golangci-lint happy: handling return values * Adding test suite and example dummy for playback package * Adding some basic queue tests * Only use Jukebox.Enabled config option * Adding stream closing handling * Pass context.Context to all PlaybackDevice methods * Remove unneeded function * Correct spelling * Reduce visibility of ChildFromMediaFile * Decomplicate action-parsing * Adding simple tempfile-based AAC->FLAC transcoding. No parallel reading and writing yet. * Try to optimize pipe-writing, tempfile-handling and reading. Not done yet. * Do a synchronous copy of the tempfile. Racecondition detected * More debugging statements and fixing the play/pause bug. More work needed * Start the trackSwitcher() with each device once. Return JSON position even if its 0. More debug-output * Moving all track-handling code into own module * Fix typo. Do not pass ctx around when not applicable * WIP: More refactoring, debugging output * Fix nil pointer * Repairing MP3 playback by pinning indirect dependencies: hajimehoshi/go-mp3 and hajimehoshi/oto * Do not forget to cleanup after a skip action * Make resync with master easy * Adding missing mocks * Adding missing error-handling found by linter * Updating github.com/hajimehoshi/oto * Removing duplicate function * Move BEEP-related code into own package * Juggle beep-related code around as preparation for interface access * More refactoring for interface separation * Gather CloseDevice() behind Track interface. * Adding skeleton, draft audio-interface using mpv.io * Adding majority of interface commands using messages to mpv socket. * Adding end-of-stream handling * MPV: start/stop are working * postition is given in float in mpv * Unify Close() and CloseDevice(). Using temp filename for controlling socket * Wait until control-socket shows up. Cleanup socket in Close() * Use canceable command. Rename to Executor * Skipping tracks works now * Now with actually setting the position * Fix regain * Add missing error-handling found by linter * Adding retry mode on time-pos property getter * Remove unneeded code on queue * Putting build-tag beep onto beep files * Remove deprecated call to rand.Seed() "As of Go 1.20 there is no reason to call Seed with a random value. Programs that call Seed with a known value to get a specific sequence of results should use New(NewSource(seed)) to obtain a local random generator." * Using int32 to conform to Subsonic API spec * Fix merge error * Minor style changes * Get username from context --------- Co-authored-by: Deluan <deluan@navidrome.org>
2023-09-10 17:25:22 +02:00
ConvertToWAV(ctx context.Context, path string) (io.ReadCloser, error)
ConvertToFLAC(ctx context.Context, path string) (io.ReadCloser, error)
Probe(ctx context.Context, files []string) (string, error)
CmdPath() (string, error)
}
2022-12-20 18:25:47 +01:00
func New() FFmpeg {
return &ffmpeg{}
}
const (
extractImageCmd = "ffmpeg -i %s -an -vcodec copy -f image2pipe -"
probeCmd = "ffmpeg %s -f ffmetadata"
Jukebox mode (#2289) * Adding cache directory to ignore-list * Adding jukebox-related config options * Adding DevEnableJukebox config option pls. dummy server * Adding types and routers * Now without panic * First draft on parsing the action * Some cleanups * Adding playback server * Verify audio device configuration * Adding debug-build target to have full symbol support * Adding beep sound library pls some example code. Not working yet * Play a fixed mp3 on any interface access for testing purposes * Put action code into separate file, adding stringer, more debug output, prepare structs, validation * Put action parameter parser code where it belongs * Have a single Action transporting all information * User fmt.Errorf for error-generation * Adding wide playback interface * Use action map for parsing, stringer instead switch stmt. * Use but only one switch case and direct dispatch, refactoring * Add error handling and pushing to client * send decent errormessage, no internal server error * Adding playback devices slice and load it from config * Combine config-verification and structure init * Return user-specific device * Separate playback server from device * Use dataStore to retrieve mediafile by id * WIP: Playlist and start/stop handling. Doing start/stop the hard way as of now * WIP: set, start and stop work on one single song. More to come * Dont need to wait for the end * Merge jukebox_action.go into jukebox.go * Remove getParameterAsInt64(). Use existing requiredParamInt() instead * Dont need to call newFailure() explicitly * Remove int64, use int instead. * Add and set action now accept multiple ids * Kickout copy of childFromMediaFile(). It is not needed here. * Refactoring devices and playbackServer * Turn (internal) playback.DeviceStatus into subsonic JukeboxStatus when rendering output. Indexes int64 -> int * Now we have a position and playing status * Switching gain to float32, xs:float is defined as 32 bit. Fixing nasty copy/pointer bug * Now with volume control * Start working the queue * Remove user from device interface * Rename function GetDevice -> GetDeviceForUser to make intention clearer * Have a nice stringer for the queue * User Prepared boolean for now to allow pause/unpause * Skipping works, but without offsets * Make ChildFromMediaFile public to be used in jukebox get() implementation * Return position in seconds and implement offset-skip in seconds * Default offset to 0 * Adding a simple setGain implementation * Prepare for transcoding AAC * WIP: transcode to WAV to use beeps wav decoder. Not done yet. * WIP: out of sheer desparation: convert to MP3 (which works) rather than WAV to troubleshoot issue. * Use FLAC as intermediate format to play Apple AAC * A bit of cleanup * Catching the end-of-stream event for further reactions * Have a trackSwitching goroutine waiting on channel when track ends * Move decoder code into own file. Restructure code a bit * Now with going on to play the next song in the playlist * Adding shuffle feature * Implementing remove action * Cleanup code * Remove templates for ffmpeg mp3 generation. Not needed anymore. * Adding some documentation * Check whether offset into track is in range. Fixing potential remove track bug. Documentation * Make golangci-lint happy: handling return values * Adding test suite and example dummy for playback package * Adding some basic queue tests * Only use Jukebox.Enabled config option * Adding stream closing handling * Pass context.Context to all PlaybackDevice methods * Remove unneeded function * Correct spelling * Reduce visibility of ChildFromMediaFile * Decomplicate action-parsing * Adding simple tempfile-based AAC->FLAC transcoding. No parallel reading and writing yet. * Try to optimize pipe-writing, tempfile-handling and reading. Not done yet. * Do a synchronous copy of the tempfile. Racecondition detected * More debugging statements and fixing the play/pause bug. More work needed * Start the trackSwitcher() with each device once. Return JSON position even if its 0. More debug-output * Moving all track-handling code into own module * Fix typo. Do not pass ctx around when not applicable * WIP: More refactoring, debugging output * Fix nil pointer * Repairing MP3 playback by pinning indirect dependencies: hajimehoshi/go-mp3 and hajimehoshi/oto * Do not forget to cleanup after a skip action * Make resync with master easy * Adding missing mocks * Adding missing error-handling found by linter * Updating github.com/hajimehoshi/oto * Removing duplicate function * Move BEEP-related code into own package * Juggle beep-related code around as preparation for interface access * More refactoring for interface separation * Gather CloseDevice() behind Track interface. * Adding skeleton, draft audio-interface using mpv.io * Adding majority of interface commands using messages to mpv socket. * Adding end-of-stream handling * MPV: start/stop are working * postition is given in float in mpv * Unify Close() and CloseDevice(). Using temp filename for controlling socket * Wait until control-socket shows up. Cleanup socket in Close() * Use canceable command. Rename to Executor * Skipping tracks works now * Now with actually setting the position * Fix regain * Add missing error-handling found by linter * Adding retry mode on time-pos property getter * Remove unneeded code on queue * Putting build-tag beep onto beep files * Remove deprecated call to rand.Seed() "As of Go 1.20 there is no reason to call Seed with a random value. Programs that call Seed with a known value to get a specific sequence of results should use New(NewSource(seed)) to obtain a local random generator." * Using int32 to conform to Subsonic API spec * Fix merge error * Minor style changes * Get username from context --------- Co-authored-by: Deluan <deluan@navidrome.org>
2023-09-10 17:25:22 +02:00
createWavCmd = "ffmpeg -i %s -c:a pcm_s16le -f wav -"
createFLACCmd = "ffmpeg -i %s -f flac -"
)
2022-12-20 18:25:47 +01:00
type ffmpeg struct{}
func (e *ffmpeg) Transcode(ctx context.Context, command, path string, maxBitRate int) (io.ReadCloser, error) {
if _, err := ffmpegCmd(); err != nil {
return nil, err
}
2022-12-20 18:25:47 +01:00
args := createFFmpegCommand(command, path, maxBitRate)
return e.start(ctx, args)
}
func (e *ffmpeg) ExtractImage(ctx context.Context, path string) (io.ReadCloser, error) {
if _, err := ffmpegCmd(); err != nil {
return nil, err
}
2022-12-20 18:25:47 +01:00
args := createFFmpegCommand(extractImageCmd, path, 0)
return e.start(ctx, args)
}
Jukebox mode (#2289) * Adding cache directory to ignore-list * Adding jukebox-related config options * Adding DevEnableJukebox config option pls. dummy server * Adding types and routers * Now without panic * First draft on parsing the action * Some cleanups * Adding playback server * Verify audio device configuration * Adding debug-build target to have full symbol support * Adding beep sound library pls some example code. Not working yet * Play a fixed mp3 on any interface access for testing purposes * Put action code into separate file, adding stringer, more debug output, prepare structs, validation * Put action parameter parser code where it belongs * Have a single Action transporting all information * User fmt.Errorf for error-generation * Adding wide playback interface * Use action map for parsing, stringer instead switch stmt. * Use but only one switch case and direct dispatch, refactoring * Add error handling and pushing to client * send decent errormessage, no internal server error * Adding playback devices slice and load it from config * Combine config-verification and structure init * Return user-specific device * Separate playback server from device * Use dataStore to retrieve mediafile by id * WIP: Playlist and start/stop handling. Doing start/stop the hard way as of now * WIP: set, start and stop work on one single song. More to come * Dont need to wait for the end * Merge jukebox_action.go into jukebox.go * Remove getParameterAsInt64(). Use existing requiredParamInt() instead * Dont need to call newFailure() explicitly * Remove int64, use int instead. * Add and set action now accept multiple ids * Kickout copy of childFromMediaFile(). It is not needed here. * Refactoring devices and playbackServer * Turn (internal) playback.DeviceStatus into subsonic JukeboxStatus when rendering output. Indexes int64 -> int * Now we have a position and playing status * Switching gain to float32, xs:float is defined as 32 bit. Fixing nasty copy/pointer bug * Now with volume control * Start working the queue * Remove user from device interface * Rename function GetDevice -> GetDeviceForUser to make intention clearer * Have a nice stringer for the queue * User Prepared boolean for now to allow pause/unpause * Skipping works, but without offsets * Make ChildFromMediaFile public to be used in jukebox get() implementation * Return position in seconds and implement offset-skip in seconds * Default offset to 0 * Adding a simple setGain implementation * Prepare for transcoding AAC * WIP: transcode to WAV to use beeps wav decoder. Not done yet. * WIP: out of sheer desparation: convert to MP3 (which works) rather than WAV to troubleshoot issue. * Use FLAC as intermediate format to play Apple AAC * A bit of cleanup * Catching the end-of-stream event for further reactions * Have a trackSwitching goroutine waiting on channel when track ends * Move decoder code into own file. Restructure code a bit * Now with going on to play the next song in the playlist * Adding shuffle feature * Implementing remove action * Cleanup code * Remove templates for ffmpeg mp3 generation. Not needed anymore. * Adding some documentation * Check whether offset into track is in range. Fixing potential remove track bug. Documentation * Make golangci-lint happy: handling return values * Adding test suite and example dummy for playback package * Adding some basic queue tests * Only use Jukebox.Enabled config option * Adding stream closing handling * Pass context.Context to all PlaybackDevice methods * Remove unneeded function * Correct spelling * Reduce visibility of ChildFromMediaFile * Decomplicate action-parsing * Adding simple tempfile-based AAC->FLAC transcoding. No parallel reading and writing yet. * Try to optimize pipe-writing, tempfile-handling and reading. Not done yet. * Do a synchronous copy of the tempfile. Racecondition detected * More debugging statements and fixing the play/pause bug. More work needed * Start the trackSwitcher() with each device once. Return JSON position even if its 0. More debug-output * Moving all track-handling code into own module * Fix typo. Do not pass ctx around when not applicable * WIP: More refactoring, debugging output * Fix nil pointer * Repairing MP3 playback by pinning indirect dependencies: hajimehoshi/go-mp3 and hajimehoshi/oto * Do not forget to cleanup after a skip action * Make resync with master easy * Adding missing mocks * Adding missing error-handling found by linter * Updating github.com/hajimehoshi/oto * Removing duplicate function * Move BEEP-related code into own package * Juggle beep-related code around as preparation for interface access * More refactoring for interface separation * Gather CloseDevice() behind Track interface. * Adding skeleton, draft audio-interface using mpv.io * Adding majority of interface commands using messages to mpv socket. * Adding end-of-stream handling * MPV: start/stop are working * postition is given in float in mpv * Unify Close() and CloseDevice(). Using temp filename for controlling socket * Wait until control-socket shows up. Cleanup socket in Close() * Use canceable command. Rename to Executor * Skipping tracks works now * Now with actually setting the position * Fix regain * Add missing error-handling found by linter * Adding retry mode on time-pos property getter * Remove unneeded code on queue * Putting build-tag beep onto beep files * Remove deprecated call to rand.Seed() "As of Go 1.20 there is no reason to call Seed with a random value. Programs that call Seed with a known value to get a specific sequence of results should use New(NewSource(seed)) to obtain a local random generator." * Using int32 to conform to Subsonic API spec * Fix merge error * Minor style changes * Get username from context --------- Co-authored-by: Deluan <deluan@navidrome.org>
2023-09-10 17:25:22 +02:00
func (e *ffmpeg) ConvertToWAV(ctx context.Context, path string) (io.ReadCloser, error) {
args := createFFmpegCommand(createWavCmd, path, 0)
return e.start(ctx, args)
}
func (e *ffmpeg) ConvertToFLAC(ctx context.Context, path string) (io.ReadCloser, error) {
args := createFFmpegCommand(createFLACCmd, path, 0)
return e.start(ctx, args)
}
func (e *ffmpeg) Probe(ctx context.Context, files []string) (string, error) {
if _, err := ffmpegCmd(); err != nil {
return "", err
}
args := createProbeCommand(probeCmd, files)
log.Trace(ctx, "Executing ffmpeg command", "args", args)
cmd := exec.CommandContext(ctx, args[0], args[1:]...) // #nosec
output, _ := cmd.CombinedOutput()
return string(output), nil
}
func (e *ffmpeg) CmdPath() (string, error) {
return ffmpegCmd()
}
2022-12-20 18:25:47 +01:00
func (e *ffmpeg) start(ctx context.Context, args []string) (io.ReadCloser, error) {
log.Trace(ctx, "Executing ffmpeg command", "cmd", args)
j := &ffCmd{args: args}
j.PipeReader, j.out = io.Pipe()
err := j.start()
if err != nil {
return nil, err
}
go j.wait()
return j, nil
}
type ffCmd struct {
*io.PipeReader
out *io.PipeWriter
args []string
cmd *exec.Cmd
}
func (j *ffCmd) start() error {
cmd := exec.Command(j.args[0], j.args[1:]...) // #nosec
cmd.Stdout = j.out
2022-12-20 18:25:47 +01:00
if log.CurrentLevel() >= log.LevelTrace {
cmd.Stderr = os.Stderr
} else {
cmd.Stderr = io.Discard
}
j.cmd = cmd
if err := cmd.Start(); err != nil {
return fmt.Errorf("starting cmd: %w", err)
}
return nil
}
func (j *ffCmd) wait() {
if err := j.cmd.Wait(); err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
_ = j.out.CloseWithError(fmt.Errorf("%s exited with non-zero status code: %d", j.args[0], exitErr.ExitCode()))
} else {
_ = j.out.CloseWithError(fmt.Errorf("waiting %s cmd: %w", j.args[0], err))
}
2020-02-25 16:01:39 +01:00
return
}
_ = j.out.Close()
}
// Path will always be an absolute path
2022-12-20 18:25:47 +01:00
func createFFmpegCommand(cmd, path string, maxBitRate int) []string {
split := strings.Split(fixCmd(cmd), " ")
for i, s := range split {
s = strings.ReplaceAll(s, "%s", path)
s = strings.ReplaceAll(s, "%b", strconv.Itoa(maxBitRate))
split[i] = s
}
return split
}
func createProbeCommand(cmd string, inputs []string) []string {
split := strings.Split(fixCmd(cmd), " ")
var args []string
for _, s := range split {
if s == "%s" {
for _, inp := range inputs {
args = append(args, "-i", inp)
}
} else {
args = append(args, s)
}
}
return args
}
func fixCmd(cmd string) string {
split := strings.Split(cmd, " ")
var result []string
cmdPath, _ := ffmpegCmd()
for _, s := range split {
if s == "ffmpeg" || s == "ffmpeg.exe" {
result = append(result, cmdPath)
} else {
result = append(result, s)
}
}
return strings.Join(result, " ")
}
func ffmpegCmd() (string, error) {
ffOnce.Do(func() {
if conf.Server.FFmpegPath != "" {
ffmpegPath = conf.Server.FFmpegPath
ffmpegPath, ffmpegErr = exec.LookPath(ffmpegPath)
} else {
ffmpegPath, ffmpegErr = exec.LookPath("ffmpeg")
if errors.Is(ffmpegErr, exec.ErrDot) {
log.Trace("ffmpeg found in current folder '.'")
ffmpegPath, ffmpegErr = exec.LookPath("./ffmpeg")
}
}
if ffmpegErr == nil {
log.Info("Found ffmpeg", "path", ffmpegPath)
return
}
})
return ffmpegPath, ffmpegErr
}
var (
ffOnce sync.Once
ffmpegPath string
ffmpegErr error
)