2022-12-20 18:25:47 +01:00
|
|
|
package ffmpeg
|
2020-02-19 20:53:35 +01:00
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2022-12-18 18:22:12 +01:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
2020-02-19 20:53:35 +01:00
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
2023-02-07 19:08:25 +01:00
|
|
|
"sync"
|
2020-02-19 20:53:35 +01:00
|
|
|
|
2023-02-07 19:08:25 +01:00
|
|
|
"github.com/navidrome/navidrome/conf"
|
2020-02-19 20:53:35 +01:00
|
|
|
"github.com/navidrome/navidrome/log"
|
|
|
|
)
|
|
|
|
|
2022-12-20 18:25:47 +01:00
|
|
|
type FFmpeg interface {
|
2023-12-02 19:10:25 +01:00
|
|
|
Transcode(ctx context.Context, command, path string, maxBitRate, offset int) (io.ReadCloser, error)
|
2022-12-20 18:25:47 +01:00
|
|
|
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)
|
2023-02-07 19:08:25 +01:00
|
|
|
Probe(ctx context.Context, files []string) (string, error)
|
|
|
|
CmdPath() (string, error)
|
2023-12-28 02:20:29 +01:00
|
|
|
IsAvailable() bool
|
2024-02-17 18:06:01 +01:00
|
|
|
Version() string
|
2020-02-19 20:53:35 +01:00
|
|
|
}
|
|
|
|
|
2022-12-20 18:25:47 +01:00
|
|
|
func New() FFmpeg {
|
|
|
|
return &ffmpeg{}
|
2020-02-19 20:53:35 +01:00
|
|
|
}
|
|
|
|
|
2023-02-07 19:08:25 +01:00
|
|
|
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 -"
|
2023-02-07 19:08:25 +01:00
|
|
|
)
|
2020-02-19 20:53:35 +01:00
|
|
|
|
2022-12-20 18:25:47 +01:00
|
|
|
type ffmpeg struct{}
|
|
|
|
|
2023-12-02 19:10:25 +01:00
|
|
|
func (e *ffmpeg) Transcode(ctx context.Context, command, path string, maxBitRate, offset int) (io.ReadCloser, error) {
|
2023-02-07 19:08:25 +01:00
|
|
|
if _, err := ffmpegCmd(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-12-02 19:10:25 +01:00
|
|
|
args := createFFmpegCommand(command, path, maxBitRate, offset)
|
2022-12-20 18:25:47 +01:00
|
|
|
return e.start(ctx, args)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *ffmpeg) ExtractImage(ctx context.Context, path string) (io.ReadCloser, error) {
|
2023-02-07 19:08:25 +01:00
|
|
|
if _, err := ffmpegCmd(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-12-02 19:10:25 +01:00
|
|
|
args := createFFmpegCommand(extractImageCmd, path, 0, 0)
|
2022-12-20 18:25:47 +01:00
|
|
|
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) {
|
2023-12-02 19:10:25 +01:00
|
|
|
args := createFFmpegCommand(createWavCmd, path, 0, 0)
|
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
|
|
|
return e.start(ctx, args)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *ffmpeg) ConvertToFLAC(ctx context.Context, path string) (io.ReadCloser, error) {
|
2023-12-02 19:10:25 +01:00
|
|
|
args := createFFmpegCommand(createFLACCmd, path, 0, 0)
|
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
|
|
|
return e.start(ctx, args)
|
|
|
|
}
|
|
|
|
|
2023-02-07 19:08:25 +01:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2023-12-28 02:20:29 +01:00
|
|
|
func (e *ffmpeg) IsAvailable() bool {
|
|
|
|
_, err := ffmpegCmd()
|
|
|
|
return err == nil
|
|
|
|
}
|
|
|
|
|
2024-02-17 18:06:01 +01:00
|
|
|
// Version executes ffmpeg -version and extracts the version from the output.
|
|
|
|
// Sample output: ffmpeg version 6.0 Copyright (c) 2000-2023 the FFmpeg developers
|
|
|
|
func (e *ffmpeg) Version() string {
|
|
|
|
cmd, err := ffmpegCmd()
|
|
|
|
if err != nil {
|
|
|
|
return "N/A"
|
|
|
|
}
|
|
|
|
out, err := exec.Command(cmd, "-version").CombinedOutput() // #nosec
|
|
|
|
if err != nil {
|
|
|
|
return "N/A"
|
|
|
|
}
|
|
|
|
parts := strings.Split(string(out), " ")
|
|
|
|
if len(parts) < 3 {
|
|
|
|
return "N/A"
|
|
|
|
}
|
|
|
|
return parts[2]
|
|
|
|
}
|
|
|
|
|
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)
|
2023-02-06 21:37:12 +01:00
|
|
|
j := &ffCmd{args: args}
|
2022-12-18 18:22:12 +01:00
|
|
|
j.PipeReader, j.out = io.Pipe()
|
|
|
|
err := j.start()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
go j.wait()
|
|
|
|
return j, nil
|
|
|
|
}
|
|
|
|
|
2023-02-06 21:37:12 +01:00
|
|
|
type ffCmd struct {
|
2022-12-18 18:22:12 +01:00
|
|
|
*io.PipeReader
|
|
|
|
out *io.PipeWriter
|
|
|
|
args []string
|
|
|
|
cmd *exec.Cmd
|
|
|
|
}
|
|
|
|
|
2023-02-06 21:37:12 +01:00
|
|
|
func (j *ffCmd) start() error {
|
|
|
|
cmd := exec.Command(j.args[0], j.args[1:]...) // #nosec
|
2022-12-18 18:22:12 +01:00
|
|
|
cmd.Stdout = j.out
|
2023-12-25 22:29:59 +01:00
|
|
|
if log.IsGreaterOrEqualTo(log.LevelTrace) {
|
2022-12-20 18:25:47 +01:00
|
|
|
cmd.Stderr = os.Stderr
|
|
|
|
} else {
|
|
|
|
cmd.Stderr = io.Discard
|
|
|
|
}
|
2022-12-18 18:22:12 +01:00
|
|
|
j.cmd = cmd
|
|
|
|
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
|
|
return fmt.Errorf("starting cmd: %w", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-02-06 21:37:12 +01:00
|
|
|
func (j *ffCmd) wait() {
|
2022-12-18 18:22:12 +01:00
|
|
|
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
|
2020-02-19 20:53:35 +01:00
|
|
|
}
|
2022-12-18 18:22:12 +01:00
|
|
|
_ = j.out.Close()
|
2020-02-19 20:53:35 +01:00
|
|
|
}
|
|
|
|
|
2020-04-03 20:48:44 +02:00
|
|
|
// Path will always be an absolute path
|
2023-12-02 19:10:25 +01:00
|
|
|
func createFFmpegCommand(cmd, path string, maxBitRate, offset int) []string {
|
2023-02-07 19:08:25 +01:00
|
|
|
split := strings.Split(fixCmd(cmd), " ")
|
2023-12-02 19:10:25 +01:00
|
|
|
var parts []string
|
|
|
|
|
|
|
|
for _, s := range split {
|
|
|
|
if strings.Contains(s, "%s") {
|
|
|
|
s = strings.ReplaceAll(s, "%s", path)
|
|
|
|
parts = append(parts, s)
|
|
|
|
if offset > 0 && !strings.Contains(cmd, "%t") {
|
|
|
|
parts = append(parts, "-ss", strconv.Itoa(offset))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
s = strings.ReplaceAll(s, "%t", strconv.Itoa(offset))
|
|
|
|
s = strings.ReplaceAll(s, "%b", strconv.Itoa(maxBitRate))
|
|
|
|
parts = append(parts, s)
|
|
|
|
}
|
2020-02-19 20:53:35 +01:00
|
|
|
}
|
|
|
|
|
2023-12-02 19:10:25 +01:00
|
|
|
return parts
|
2020-02-19 20:53:35 +01:00
|
|
|
}
|
2023-02-07 19:08:25 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
)
|