mirror of
https://github.com/owncast/owncast.git
synced 2024-11-21 12:18:02 +03:00
Codec selection (#892)
* Query for installed codecs * Start modeling out codecs * Can now specify a codec and get the correct settings returned from the model * Return codecs in admin/serverconfig * Start handling transcoding errors and return messages to user * filter available codecs against a whitelist * Fix merge * Codecs are working * Switching between codecs work * Add apis for setting a custom video codec * Cleanup the logging of transcoder errors * Add v4l codec * Add fetching v4l * Add support for per-codec presets * Use updated nvenc encoding parameters * Update log message * Some more codec WIP * Turn off v4l. It is a mess. * Try to make the lowest latency level a bit more playable * Use a human redable display name in console messages * Turn on transcoder persistent connections * Add more codec-related user-facing error messages * Give the initial offline state transcoder an id * Force a minimum segment count of 3 * Disable qsv for now. set x264 specific params in VariantFlags * Close body in case * Ignore vbv underflow message, it is not actionable * Determine a dynamic gop value based on the length of segments * Add codec-specific tests * Cleanup * Ignore goconst lint warnings in codec file * Troubleshoot omx * Add more codec tests * Remove no longer accurate comment * Bundle admin from codec branch * Revert back to old setting * Cleanup list of codecs a bit * Remove old references to the encoder preset * Commit updated API documentation * Update admin bundle * Commit updated API documentation * Add codec setting to api spec * Commit updated API documentation Co-authored-by: Owncast <owncast@owncast.online>
This commit is contained in:
parent
7dec4fe063
commit
5214d81264
21 changed files with 845 additions and 180 deletions
|
@ -18,6 +18,7 @@ trap shutdown INT TERM ABRT EXIT
|
|||
echo "Cloning owncast admin into $INSTALL_TEMP_DIRECTORY..."
|
||||
git clone https://github.com/owncast/owncast-admin 2> /dev/null
|
||||
cd owncast-admin
|
||||
git checkout gek/codec-selection
|
||||
|
||||
echo "Installing npm modules for the owncast admin..."
|
||||
npm --silent install 2> /dev/null
|
||||
|
|
|
@ -53,8 +53,8 @@ func GetDefaults() Defaults {
|
|||
{
|
||||
IsAudioPassthrough: true,
|
||||
VideoBitrate: 1200,
|
||||
EncoderPreset: "veryfast",
|
||||
Framerate: 24,
|
||||
CPUUsageLevel: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -438,26 +438,6 @@ func SetStreamOutputVariants(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// Temporary: Convert the cpuUsageLevel to a preset. In the future we will have
|
||||
// different codec models that will handle this for us and we won't
|
||||
// be keeping track of presets at all. But for now...
|
||||
presetMapping := []string{
|
||||
"ultrafast",
|
||||
"superfast",
|
||||
"veryfast",
|
||||
"faster",
|
||||
"fast",
|
||||
}
|
||||
|
||||
for i, variant := range videoVariants.Value {
|
||||
preset := "superfast"
|
||||
if variant.CPUUsageLevel > 0 && variant.CPUUsageLevel <= len(presetMapping) {
|
||||
preset = presetMapping[variant.CPUUsageLevel-1]
|
||||
}
|
||||
variant.EncoderPreset = preset
|
||||
videoVariants.Value[i] = variant
|
||||
}
|
||||
|
||||
if err := data.SetStreamOutputVariants(videoVariants.Value); err != nil {
|
||||
controllers.WriteSimpleResponse(w, false, "unable to update video config with provided values "+err.Error())
|
||||
return
|
||||
|
@ -508,6 +488,26 @@ func SetChatDisabled(w http.ResponseWriter, r *http.Request) {
|
|||
controllers.WriteSimpleResponse(w, true, "chat disabled status updated")
|
||||
}
|
||||
|
||||
// SetVideoCodec will change the codec used for video encoding.
|
||||
func SetVideoCodec(w http.ResponseWriter, r *http.Request) {
|
||||
if !requirePOST(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
configValue, success := getValueFromRequest(w, r)
|
||||
if !success {
|
||||
controllers.WriteSimpleResponse(w, false, "unable to change video codec")
|
||||
return
|
||||
}
|
||||
|
||||
if err := data.SetVideoCodec(configValue.Value.(string)); err != nil {
|
||||
controllers.WriteSimpleResponse(w, false, "unable to update codec")
|
||||
return
|
||||
}
|
||||
|
||||
controllers.WriteSimpleResponse(w, true, "video codec updated")
|
||||
}
|
||||
|
||||
// SetExternalActions will set the 3rd party actions for the web interface.
|
||||
func SetExternalActions(w http.ResponseWriter, r *http.Request) {
|
||||
type externalActionsRequest struct {
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/core/transcoder"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
@ -13,6 +14,8 @@ import (
|
|||
|
||||
// GetServerConfig gets the config details of the server.
|
||||
func GetServerConfig(w http.ResponseWriter, r *http.Request) {
|
||||
ffmpeg := utils.ValidatedFfmpegPath(data.GetFfMpegPath())
|
||||
|
||||
var videoQualityVariants = make([]models.StreamOutputVariant, 0)
|
||||
for _, variant := range data.GetStreamOutputVariants() {
|
||||
videoQualityVariants = append(videoQualityVariants, models.StreamOutputVariant{
|
||||
|
@ -20,10 +23,9 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) {
|
|||
IsAudioPassthrough: variant.GetIsAudioPassthrough(),
|
||||
IsVideoPassthrough: variant.IsVideoPassthrough,
|
||||
Framerate: variant.GetFramerate(),
|
||||
EncoderPreset: variant.GetEncoderPreset(),
|
||||
VideoBitrate: variant.VideoBitrate,
|
||||
AudioBitrate: variant.AudioBitrate,
|
||||
CPUUsageLevel: variant.GetCPUUsageLevel(),
|
||||
CPUUsageLevel: variant.CPUUsageLevel,
|
||||
ScaledWidth: variant.ScaledWidth,
|
||||
ScaledHeight: variant.ScaledHeight,
|
||||
})
|
||||
|
@ -41,7 +43,7 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) {
|
|||
NSFW: data.GetNSFW(),
|
||||
CustomStyles: data.GetCustomStyles(),
|
||||
},
|
||||
FFmpegPath: utils.ValidatedFfmpegPath(data.GetFfMpegPath()),
|
||||
FFmpegPath: ffmpeg,
|
||||
StreamKey: data.GetStreamKey(),
|
||||
WebServerPort: config.WebServerPort,
|
||||
RTMPServerPort: data.GetRTMPPortNumber(),
|
||||
|
@ -56,6 +58,8 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) {
|
|||
},
|
||||
S3: data.GetS3Config(),
|
||||
ExternalActions: data.GetExternalActions(),
|
||||
SupportedCodecs: transcoder.GetCodecs(ffmpeg),
|
||||
VideoCodec: data.GetVideoCodec(),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
@ -77,6 +81,8 @@ type serverConfigAdminResponse struct {
|
|||
YP yp `json:"yp"`
|
||||
ChatDisabled bool `json:"chatDisabled"`
|
||||
ExternalActions []models.ExternalAction `json:"externalActions"`
|
||||
SupportedCodecs []string `json:"supportedCodecs"`
|
||||
VideoCodec string `json:"videoCodec"`
|
||||
}
|
||||
|
||||
type videoSettings struct {
|
||||
|
|
|
@ -99,6 +99,7 @@ func transitionToOfflineVideoStreamContent() {
|
|||
offlineFilePath := "static/" + offlineFilename
|
||||
_transcoder := transcoder.NewTranscoder()
|
||||
_transcoder.SetInput(offlineFilePath)
|
||||
_transcoder.SetIdentifier("offline")
|
||||
_transcoder.Start()
|
||||
|
||||
// Copy the logo to be the thumbnail
|
||||
|
|
|
@ -39,6 +39,7 @@ const videoStreamOutputVariantsKey = "video_stream_output_variants"
|
|||
const chatDisabledKey = "chat_disabled"
|
||||
const externalActionsKey = "external_actions"
|
||||
const customStylesKey = "custom_styles"
|
||||
const videoCodecKey = "video_codec"
|
||||
|
||||
// GetExtraPageBodyContent will return the user-supplied body content.
|
||||
func GetExtraPageBodyContent() string {
|
||||
|
@ -481,6 +482,20 @@ func GetCustomStyles() string {
|
|||
return style
|
||||
}
|
||||
|
||||
// SetVideoCodec will set the codec used for video encoding.
|
||||
func SetVideoCodec(codec string) error {
|
||||
return _datastore.SetString(videoCodecKey, codec)
|
||||
}
|
||||
|
||||
func GetVideoCodec() string {
|
||||
codec, err := _datastore.GetString(videoCodecKey)
|
||||
if codec == "" || err != nil {
|
||||
return "libx264" // Default value
|
||||
}
|
||||
|
||||
return codec
|
||||
}
|
||||
|
||||
// VerifySettings will perform a sanity check for specific settings values.
|
||||
func VerifySettings() error {
|
||||
if GetStreamKey() == "" {
|
||||
|
|
373
core/transcoder/codecs.go
Normal file
373
core/transcoder/codecs.go
Normal file
|
@ -0,0 +1,373 @@
|
|||
//nolint:goconst
|
||||
|
||||
package transcoder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Codec represents a supported codec on the system.
|
||||
type Codec interface {
|
||||
Name() string
|
||||
DisplayName() string
|
||||
GlobalFlags() string
|
||||
PixelFormat() string
|
||||
ExtraArguments() string
|
||||
ExtraFilters() string
|
||||
VariantFlags(v *HLSVariant) string
|
||||
GetPresetForLevel(l int) string
|
||||
}
|
||||
|
||||
var supportedCodecs = map[string]string{
|
||||
(&Libx264Codec{}).Name(): "libx264",
|
||||
(&OmxCodec{}).Name(): "omx",
|
||||
(&VaapiCodec{}).Name(): "vaapi",
|
||||
(&NvencCodec{}).Name(): "NVIDIA nvenc",
|
||||
}
|
||||
|
||||
type Libx264Codec struct {
|
||||
}
|
||||
|
||||
func (c *Libx264Codec) Name() string {
|
||||
return "libx264"
|
||||
}
|
||||
|
||||
func (c *Libx264Codec) DisplayName() string {
|
||||
return "x264"
|
||||
}
|
||||
|
||||
func (c *Libx264Codec) GlobalFlags() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Libx264Codec) PixelFormat() string {
|
||||
return "yuv420p"
|
||||
}
|
||||
|
||||
func (c *Libx264Codec) ExtraArguments() string {
|
||||
return strings.Join([]string{
|
||||
"-tune", "zerolatency", // Option used for good for fast encoding and low-latency streaming (always includes iframes in each segment)
|
||||
}, " ")
|
||||
}
|
||||
|
||||
func (c *Libx264Codec) ExtraFilters() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Libx264Codec) VariantFlags(v *HLSVariant) string {
|
||||
bufferSize := int(float64(v.videoBitrate) * 1.2) // How often it checks the bitrate of encoded segments to see if it's too high/low.
|
||||
|
||||
return strings.Join([]string{
|
||||
fmt.Sprintf("-x264-params:v:%d \"scenecut=0:open_gop=0\"", v.index), // How often the encoder checks the bitrate in order to meet average/max values
|
||||
fmt.Sprintf("-bufsize:v:%d %dk", v.index, bufferSize),
|
||||
fmt.Sprintf("-profile:v:%d %s", v.index, "high"), // Encoding profile
|
||||
}, " ")
|
||||
}
|
||||
|
||||
func (c *Libx264Codec) GetPresetForLevel(l int) string {
|
||||
presetMapping := []string{
|
||||
"ultrafast",
|
||||
"superfast",
|
||||
"veryfast",
|
||||
"faster",
|
||||
"fast",
|
||||
}
|
||||
|
||||
if l >= len(presetMapping) {
|
||||
return "superfast"
|
||||
}
|
||||
|
||||
return presetMapping[l]
|
||||
}
|
||||
|
||||
type OmxCodec struct {
|
||||
}
|
||||
|
||||
func (c *OmxCodec) Name() string {
|
||||
return "h264_omx"
|
||||
}
|
||||
|
||||
func (c *OmxCodec) DisplayName() string {
|
||||
return "OpenMAX (omx)"
|
||||
}
|
||||
|
||||
func (c *OmxCodec) GlobalFlags() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *OmxCodec) PixelFormat() string {
|
||||
return "yuv420p"
|
||||
}
|
||||
|
||||
func (c *OmxCodec) ExtraArguments() string {
|
||||
return strings.Join([]string{
|
||||
"-tune", "zerolatency", // Option used for good for fast encoding and low-latency streaming (always includes iframes in each segment)
|
||||
}, " ")
|
||||
}
|
||||
|
||||
func (c *OmxCodec) ExtraFilters() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *OmxCodec) VariantFlags(v *HLSVariant) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *OmxCodec) GetPresetForLevel(l int) string {
|
||||
presetMapping := []string{
|
||||
"ultrafast",
|
||||
"superfast",
|
||||
"veryfast",
|
||||
"faster",
|
||||
"fast",
|
||||
}
|
||||
|
||||
if l >= len(presetMapping) {
|
||||
return "superfast"
|
||||
}
|
||||
|
||||
return presetMapping[l]
|
||||
}
|
||||
|
||||
type VaapiCodec struct {
|
||||
}
|
||||
|
||||
func (c *VaapiCodec) Name() string {
|
||||
return "h264_vaapi"
|
||||
}
|
||||
|
||||
func (c *VaapiCodec) DisplayName() string {
|
||||
return "VA-API"
|
||||
}
|
||||
|
||||
func (c *VaapiCodec) GlobalFlags() string {
|
||||
flags := []string{
|
||||
"-vaapi_device", "/dev/dri/renderD128",
|
||||
}
|
||||
|
||||
return strings.Join(flags, " ")
|
||||
}
|
||||
|
||||
func (c *VaapiCodec) PixelFormat() string {
|
||||
return "vaapi_vld"
|
||||
}
|
||||
|
||||
func (c *VaapiCodec) ExtraFilters() string {
|
||||
return "format=nv12,hwupload"
|
||||
}
|
||||
|
||||
func (c *VaapiCodec) ExtraArguments() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *VaapiCodec) VariantFlags(v *HLSVariant) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *VaapiCodec) GetPresetForLevel(l int) string {
|
||||
presetMapping := []string{
|
||||
"ultrafast",
|
||||
"superfast",
|
||||
"veryfast",
|
||||
"faster",
|
||||
"fast",
|
||||
}
|
||||
|
||||
if l >= len(presetMapping) {
|
||||
return "superfast"
|
||||
}
|
||||
|
||||
return presetMapping[l]
|
||||
}
|
||||
|
||||
type NvencCodec struct {
|
||||
}
|
||||
|
||||
func (c *NvencCodec) Name() string {
|
||||
return "h264_nvenc"
|
||||
}
|
||||
|
||||
func (c *NvencCodec) DisplayName() string {
|
||||
return "nvidia nvenc"
|
||||
}
|
||||
|
||||
func (c *NvencCodec) GlobalFlags() string {
|
||||
flags := []string{
|
||||
"-hwaccel cuda",
|
||||
}
|
||||
|
||||
return strings.Join(flags, " ")
|
||||
}
|
||||
|
||||
func (c *NvencCodec) PixelFormat() string {
|
||||
return "yuv420p"
|
||||
}
|
||||
|
||||
func (c *NvencCodec) ExtraArguments() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *NvencCodec) ExtraFilters() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *NvencCodec) VariantFlags(v *HLSVariant) string {
|
||||
tuning := "ll" // low latency
|
||||
return fmt.Sprintf("-tune:v:%d %s", v.index, tuning)
|
||||
}
|
||||
|
||||
func (c *NvencCodec) GetPresetForLevel(l int) string {
|
||||
presetMapping := []string{
|
||||
"p1",
|
||||
"p2",
|
||||
"p3",
|
||||
"p4",
|
||||
"p5",
|
||||
}
|
||||
|
||||
if l >= len(presetMapping) {
|
||||
return "p3"
|
||||
}
|
||||
|
||||
return presetMapping[l]
|
||||
}
|
||||
|
||||
type QuicksyncCodec struct {
|
||||
}
|
||||
|
||||
func (c *QuicksyncCodec) Name() string {
|
||||
return "h264_qsv"
|
||||
}
|
||||
|
||||
func (c *QuicksyncCodec) DisplayName() string {
|
||||
return "Intel QuickSync"
|
||||
}
|
||||
|
||||
func (c *QuicksyncCodec) GlobalFlags() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *QuicksyncCodec) PixelFormat() string {
|
||||
return "nv12"
|
||||
}
|
||||
|
||||
func (c *QuicksyncCodec) ExtraArguments() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *QuicksyncCodec) ExtraFilters() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *QuicksyncCodec) VariantFlags(v *HLSVariant) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *QuicksyncCodec) GetPresetForLevel(l int) string {
|
||||
presetMapping := []string{
|
||||
"ultrafast",
|
||||
"superfast",
|
||||
"veryfast",
|
||||
"faster",
|
||||
"fast",
|
||||
}
|
||||
|
||||
if l >= len(presetMapping) {
|
||||
return "superfast"
|
||||
}
|
||||
|
||||
return presetMapping[l]
|
||||
}
|
||||
|
||||
type Video4Linux struct{}
|
||||
|
||||
func (c *Video4Linux) Name() string {
|
||||
return "h264_v4l2m2m"
|
||||
}
|
||||
|
||||
func (c *Video4Linux) DisplayName() string {
|
||||
return "Video4Linux"
|
||||
}
|
||||
|
||||
func (c *Video4Linux) GlobalFlags() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Video4Linux) PixelFormat() string {
|
||||
return "nv21"
|
||||
}
|
||||
|
||||
func (c *Video4Linux) ExtraArguments() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Video4Linux) ExtraFilters() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Video4Linux) VariantFlags(v *HLSVariant) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Video4Linux) GetPresetForLevel(l int) string {
|
||||
presetMapping := []string{
|
||||
"ultrafast",
|
||||
"superfast",
|
||||
"veryfast",
|
||||
"faster",
|
||||
"fast",
|
||||
}
|
||||
|
||||
if l >= len(presetMapping) {
|
||||
return "superfast"
|
||||
}
|
||||
|
||||
return presetMapping[l]
|
||||
}
|
||||
|
||||
// GetCodecs will return the supported codecs available on the system.
|
||||
func GetCodecs(ffmpegPath string) []string {
|
||||
codecs := make([]string, 0)
|
||||
|
||||
cmd := exec.Command(ffmpegPath, "-encoders")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return codecs
|
||||
}
|
||||
|
||||
response := string(out)
|
||||
lines := strings.Split(response, "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "H.264") {
|
||||
fields := strings.Fields(line)
|
||||
codec := fields[1]
|
||||
if _, supported := supportedCodecs[codec]; supported {
|
||||
codecs = append(codecs, codec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return codecs
|
||||
}
|
||||
|
||||
func getCodec(name string) Codec {
|
||||
switch name {
|
||||
case (&NvencCodec{}).Name():
|
||||
return &NvencCodec{}
|
||||
case (&VaapiCodec{}).Name():
|
||||
return &VaapiCodec{}
|
||||
case (&QuicksyncCodec{}).Name():
|
||||
return &QuicksyncCodec{}
|
||||
case (&OmxCodec{}).Name():
|
||||
return &OmxCodec{}
|
||||
case (&Video4Linux{}).Name():
|
||||
return &Video4Linux{}
|
||||
default:
|
||||
return &Libx264Codec{}
|
||||
}
|
||||
}
|
|
@ -62,6 +62,8 @@ func (s *FileWriterReceiverService) uploadHandler(w http.ResponseWriter, r *http
|
|||
writePath := filepath.Join(config.PrivateHLSStoragePath, path)
|
||||
|
||||
var buf bytes.Buffer
|
||||
defer r.Body.Close()
|
||||
|
||||
_, _ = io.Copy(&buf, r.Body)
|
||||
data := buf.Bytes()
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package transcoder
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
|
@ -27,6 +28,7 @@ type Transcoder struct {
|
|||
ffmpegPath string
|
||||
segmentIdentifier string
|
||||
internalListenerPort string
|
||||
codec Codec
|
||||
|
||||
currentStreamOutputSettings []models.StreamOutputVariant
|
||||
currentLatencyLevel models.LatencyLevel
|
||||
|
@ -46,7 +48,7 @@ type HLSVariant struct {
|
|||
audioBitrate string // The audio bitrate
|
||||
isAudioPassthrough bool // Override all settings and just copy the audio stream
|
||||
|
||||
encoderPreset string // A collection of automatic settings for the encoder. https://trac.ffmpeg.org/wiki/Encode/H.264#crf
|
||||
cpuUsageLevel int // The amount of hardware to use for encoding a stream
|
||||
}
|
||||
|
||||
// VideoSize is the scaled size of the video output.
|
||||
|
@ -81,25 +83,43 @@ func (t *Transcoder) Stop() {
|
|||
|
||||
// Start will execute the transcoding process with the settings previously set.
|
||||
func (t *Transcoder) Start() {
|
||||
command := t.getString()
|
||||
_lastTranscoderLogMessage = ""
|
||||
|
||||
log.Tracef("Video transcoder started with %d stream variants.", len(t.variants))
|
||||
command := t.getString()
|
||||
log.Infof("Video transcoder started using %s with %d stream variants.", t.codec.DisplayName(), len(t.variants))
|
||||
|
||||
if config.EnableDebugFeatures {
|
||||
log.Println(command)
|
||||
}
|
||||
|
||||
_commandExec = exec.Command("sh", "-c", command)
|
||||
err := _commandExec.Start()
|
||||
stdout, err := _commandExec.StderrPipe()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = _commandExec.Start()
|
||||
if err != nil {
|
||||
log.Errorln("Transcoder error. See transcoder.log for full output to debug.")
|
||||
log.Panicln(err, command)
|
||||
}
|
||||
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
handleTranscoderMessage(line)
|
||||
}
|
||||
}()
|
||||
|
||||
err = _commandExec.Wait()
|
||||
if t.TranscoderCompleted != nil {
|
||||
t.TranscoderCompleted(err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorln("transcoding error. look at transcoder.log to help debug. your copy of ffmpeg may not support your selected codec of", t.codec.Name(), "https://owncast.online/docs/troubleshooting/#codecs")
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transcoder) getString() string {
|
||||
|
@ -121,9 +141,12 @@ func (t *Transcoder) getString() string {
|
|||
hlsOptionsString = "-hls_flags " + strings.Join(hlsOptionFlags, "+")
|
||||
}
|
||||
ffmpegFlags := []string{
|
||||
`FFREPORT=file="transcoder.log":level=32`,
|
||||
t.ffmpegPath,
|
||||
"-hide_banner",
|
||||
"-loglevel warning",
|
||||
t.codec.GlobalFlags(),
|
||||
"-fflags +genpts", // Generate presentation time stamp if missing
|
||||
"-i ", t.input,
|
||||
|
||||
t.getVariantsString(),
|
||||
|
@ -133,12 +156,12 @@ func (t *Transcoder) getString() string {
|
|||
|
||||
"-hls_time", strconv.Itoa(t.currentLatencyLevel.SecondsPerSegment), // Length of each segment
|
||||
"-hls_list_size", strconv.Itoa(t.currentLatencyLevel.SegmentCount), // Max # in variant playlist
|
||||
"-hls_delete_threshold", "10", // Start deleting files after hls_list_size + 10
|
||||
hlsOptionsString,
|
||||
"-segment_format_options", "mpegts_flags=+initial_discontinuity:mpegts_copyts=1",
|
||||
|
||||
// Video settings
|
||||
"-tune", "zerolatency", // Option used for good for fast encoding and low-latency streaming (always includes iframes in each segment)
|
||||
"-pix_fmt", "yuv420p", // Force yuv420p color format
|
||||
t.codec.ExtraArguments(),
|
||||
"-pix_fmt", t.codec.PixelFormat(),
|
||||
"-sc_threshold", "0", // Disable scene change detection for creating segments
|
||||
|
||||
// Filenames
|
||||
|
@ -149,9 +172,7 @@ func (t *Transcoder) getString() string {
|
|||
"-max_muxing_queue_size", "400", // Workaround for Too many packets error: https://trac.ffmpeg.org/ticket/6375?cversion=0
|
||||
|
||||
"-method PUT -http_persistent 0", // HLS results sent back to us will be over PUTs
|
||||
"-fflags +genpts", // Generate presentation time stamp if missing
|
||||
localListenerAddress + "/%v/stream.m3u8", // Send HLS playlists back to us over HTTP
|
||||
"2> transcoder.log", // Log to a file for debugging
|
||||
}
|
||||
|
||||
return strings.Join(ffmpegFlags, " ")
|
||||
|
@ -181,7 +202,7 @@ func getVariantFromConfigQuality(quality models.StreamOutputVariant, index int)
|
|||
// Set a default, reasonable preset if one is not provided.
|
||||
// "superfast" and "ultrafast" are generally not recommended since they look bad.
|
||||
// https://trac.ffmpeg.org/wiki/Encode/H.264
|
||||
variant.encoderPreset = quality.GetEncoderPreset()
|
||||
variant.cpuUsageLevel = quality.CPUUsageLevel
|
||||
|
||||
variant.SetVideoBitrate(quality.VideoBitrate)
|
||||
variant.SetAudioBitrate(strconv.Itoa(quality.AudioBitrate) + "k")
|
||||
|
@ -202,6 +223,7 @@ func NewTranscoder() *Transcoder {
|
|||
|
||||
transcoder.currentStreamOutputSettings = data.GetStreamOutputVariants()
|
||||
transcoder.currentLatencyLevel = data.GetStreamLatencyLevel()
|
||||
transcoder.codec = getCodec(data.GetVideoCodec())
|
||||
|
||||
var outputPath string
|
||||
if data.GetS3Config().Enabled {
|
||||
|
@ -233,12 +255,25 @@ func (v *HLSVariant) getVariantString(t *Transcoder) string {
|
|||
v.getAudioQualityString(),
|
||||
}
|
||||
|
||||
if v.videoSize.Width != 0 || v.videoSize.Height != 0 {
|
||||
variantEncoderCommands = append(variantEncoderCommands, v.getScalingString())
|
||||
if (v.videoSize.Width != 0 || v.videoSize.Height != 0) && !v.isVideoPassthrough {
|
||||
// Order here matters, you must scale before changing hardware formats
|
||||
filters := []string{
|
||||
v.getScalingString(),
|
||||
}
|
||||
if t.codec.ExtraFilters() != "" {
|
||||
filters = append(filters, t.codec.ExtraFilters())
|
||||
}
|
||||
scalingAlgorithm := "bilinear"
|
||||
filterString := fmt.Sprintf("-sws_flags %s -filter:v:%d \"%s\"", scalingAlgorithm, v.index, strings.Join(filters, ","))
|
||||
variantEncoderCommands = append(variantEncoderCommands, filterString)
|
||||
} else if t.codec.ExtraFilters() != "" && !v.isVideoPassthrough {
|
||||
filterString := fmt.Sprintf("-filter:v:%d \"%s\"", v.index, t.codec.ExtraFilters())
|
||||
variantEncoderCommands = append(variantEncoderCommands, filterString)
|
||||
}
|
||||
|
||||
if v.encoderPreset != "" {
|
||||
variantEncoderCommands = append(variantEncoderCommands, fmt.Sprintf("-preset %s", v.encoderPreset))
|
||||
preset := t.codec.GetPresetForLevel(v.cpuUsageLevel)
|
||||
if preset != "" {
|
||||
variantEncoderCommands = append(variantEncoderCommands, fmt.Sprintf("-preset %s", preset))
|
||||
}
|
||||
|
||||
return strings.Join(variantEncoderCommands, " ")
|
||||
|
@ -276,8 +311,7 @@ func (v *HLSVariant) SetVideoScalingHeight(height int) {
|
|||
}
|
||||
|
||||
func (v *HLSVariant) getScalingString() string {
|
||||
scalingAlgorithm := "bilinear"
|
||||
return fmt.Sprintf("-filter:v:%d \"scale=%s\" -sws_flags %s", v.index, v.videoSize.getString(), scalingAlgorithm)
|
||||
return fmt.Sprintf("scale=%s", v.videoSize.getString())
|
||||
}
|
||||
|
||||
// Video Quality
|
||||
|
@ -292,11 +326,14 @@ func (v *HLSVariant) getVideoQualityString(t *Transcoder) string {
|
|||
return fmt.Sprintf("-map v:0 -c:v:%d copy", v.index)
|
||||
}
|
||||
|
||||
encoderCodec := "libx264"
|
||||
|
||||
// -1 to work around segments being generated slightly larger than expected.
|
||||
// https://trac.ffmpeg.org/ticket/6915?replyto=58#comment:57
|
||||
gop := (t.currentLatencyLevel.SecondsPerSegment * v.framerate) - 1
|
||||
// Determine if we should force key frames every 1, 2 or 3 frames.
|
||||
isEven := t.currentLatencyLevel.SecondsPerSegment%2 == 0
|
||||
gop := v.framerate * 2
|
||||
if t.currentLatencyLevel.SecondsPerSegment == 1 {
|
||||
gop = v.framerate
|
||||
} else if !isEven {
|
||||
gop = v.framerate * 3
|
||||
}
|
||||
|
||||
// For limiting the output bitrate
|
||||
// https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
|
||||
|
@ -304,18 +341,16 @@ func (v *HLSVariant) getVideoQualityString(t *Transcoder) string {
|
|||
// Adjust the max & buffer size until the output bitrate doesn't exceed the ~+10% that Apple's media validator
|
||||
// complains about.
|
||||
maxBitrate := int(float64(v.videoBitrate) * 1.06) // Max is a ~+10% over specified bitrate.
|
||||
bufferSize := int(float64(v.videoBitrate) * 1.2) // How often it checks the bitrate of encoded segments to see if it's too high/low.
|
||||
|
||||
cmd := []string{
|
||||
"-map v:0",
|
||||
fmt.Sprintf("-c:v:%d %s", v.index, encoderCodec), // Video codec used for this variant
|
||||
fmt.Sprintf("-c:v:%d %s", v.index, t.codec.Name()), // Video codec used for this variant
|
||||
fmt.Sprintf("-b:v:%d %dk", v.index, v.videoBitrate), // The average bitrate for this variant
|
||||
fmt.Sprintf("-maxrate:v:%d %dk", v.index, maxBitrate), // The max bitrate allowed for this variant
|
||||
fmt.Sprintf("-bufsize:v:%d %dk", v.index, bufferSize), // How often the encoder checks the bitrate in order to meet average/max values
|
||||
fmt.Sprintf("-g:v:%d %d", v.index, gop), // How often i-frames are encoded into the segments
|
||||
fmt.Sprintf("-profile:v:%d %s", v.index, "high"), // Encoding profile
|
||||
fmt.Sprintf("-g:v:%d %d", v.index, gop), // Suggested interval where i-frames are encoded into the segments
|
||||
fmt.Sprintf("-keyint_min:v:%d %d", v.index, gop), // minimum i-keyframe interval
|
||||
fmt.Sprintf("-r:v:%d %d", v.index, v.framerate),
|
||||
fmt.Sprintf("-x264-params:v:%d \"scenecut=0:open_gop=0:min-keyint=%d:keyint=%d\"", v.index, gop, gop), // How often i-frames are encoded into the segments
|
||||
t.codec.VariantFlags(v),
|
||||
}
|
||||
|
||||
return strings.Join(cmd, " ")
|
||||
|
@ -326,9 +361,9 @@ func (v *HLSVariant) SetVideoFramerate(framerate int) {
|
|||
v.framerate = framerate
|
||||
}
|
||||
|
||||
// SetEncoderPreset will set the video encoder preset of this variant.
|
||||
func (v *HLSVariant) SetEncoderPreset(preset string) {
|
||||
v.encoderPreset = preset
|
||||
// SetCPUUsageLevel will set the hardware usage of this variant.
|
||||
func (v *HLSVariant) SetCPUUsageLevel(level int) {
|
||||
v.cpuUsageLevel = level
|
||||
}
|
||||
|
||||
// Audio Quality
|
||||
|
@ -377,3 +412,7 @@ func (t *Transcoder) SetIdentifier(output string) {
|
|||
func (t *Transcoder) SetInternalHTTPPort(port string) {
|
||||
t.internalListenerPort = port
|
||||
}
|
||||
|
||||
func (t *Transcoder) SetCodec(codecName string) {
|
||||
t.codec = getCodec(codecName)
|
||||
}
|
||||
|
|
48
core/transcoder/transcoder_nvenc_test.go
Normal file
48
core/transcoder/transcoder_nvenc_test.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package transcoder
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/owncast/owncast/models"
|
||||
)
|
||||
|
||||
func TestFFmpegNvencCommand(t *testing.T) {
|
||||
latencyLevel := models.GetLatencyLevel(3)
|
||||
codec := NvencCodec{}
|
||||
|
||||
transcoder := new(Transcoder)
|
||||
transcoder.ffmpegPath = "/fake/path/ffmpeg"
|
||||
transcoder.SetInput("fakecontent.flv")
|
||||
transcoder.SetOutputPath("fakeOutput")
|
||||
transcoder.SetIdentifier("jdoieGg")
|
||||
transcoder.SetInternalHTTPPort("8123")
|
||||
transcoder.SetCodec(codec.Name())
|
||||
transcoder.currentLatencyLevel = latencyLevel
|
||||
|
||||
variant := HLSVariant{}
|
||||
variant.videoBitrate = 1200
|
||||
variant.isAudioPassthrough = true
|
||||
variant.SetVideoFramerate(30)
|
||||
variant.SetCPUUsageLevel(2)
|
||||
transcoder.AddVariant(variant)
|
||||
|
||||
variant2 := HLSVariant{}
|
||||
variant2.videoBitrate = 3500
|
||||
variant2.isAudioPassthrough = true
|
||||
variant2.SetVideoFramerate(24)
|
||||
variant2.SetCPUUsageLevel(4)
|
||||
transcoder.AddVariant(variant2)
|
||||
|
||||
variant3 := HLSVariant{}
|
||||
variant3.isAudioPassthrough = true
|
||||
variant3.isVideoPassthrough = true
|
||||
transcoder.AddVariant(variant3)
|
||||
|
||||
cmd := transcoder.getString()
|
||||
|
||||
expected := `FFREPORT=file="transcoder.log":level=32 /fake/path/ffmpeg -hide_banner -loglevel warning -hwaccel cuda -fflags +genpts -i fakecontent.flv -map v:0 -c:v:0 h264_nvenc -b:v:0 1200k -maxrate:v:0 1272k -g:v:0 60 -keyint_min:v:0 60 -r:v:0 30 -tune:v:0 ll -map a:0? -c:a:0 copy -preset p3 -map v:0 -c:v:1 h264_nvenc -b:v:1 3500k -maxrate:v:1 3710k -g:v:1 48 -keyint_min:v:1 48 -r:v:1 24 -tune:v:1 ll -map a:0? -c:a:1 copy -preset p5 -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset p1 -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2 " -f hls -hls_time 2 -hls_list_size 3 -segment_format_options mpegts_flags=+initial_discontinuity:mpegts_copyts=1 -pix_fmt yuv420p -sc_threshold 0 -master_pl_name stream.m3u8 -strftime 1 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdoieGg%s.ts -max_muxing_queue_size 400 -method PUT -http_persistent 0 http://127.0.0.1:8123/%v/stream.m3u8`
|
||||
|
||||
if cmd != expected {
|
||||
t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)
|
||||
}
|
||||
}
|
48
core/transcoder/transcoder_omx_test.go
Normal file
48
core/transcoder/transcoder_omx_test.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package transcoder
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/owncast/owncast/models"
|
||||
)
|
||||
|
||||
func TestFFmpegOmxCommand(t *testing.T) {
|
||||
latencyLevel := models.GetLatencyLevel(3)
|
||||
codec := OmxCodec{}
|
||||
|
||||
transcoder := new(Transcoder)
|
||||
transcoder.ffmpegPath = "/fake/path/ffmpeg"
|
||||
transcoder.SetInput("fakecontent.flv")
|
||||
transcoder.SetOutputPath("fakeOutput")
|
||||
transcoder.SetIdentifier("jdFsdfzGg")
|
||||
transcoder.SetInternalHTTPPort("8123")
|
||||
transcoder.SetCodec(codec.Name())
|
||||
transcoder.currentLatencyLevel = latencyLevel
|
||||
|
||||
variant := HLSVariant{}
|
||||
variant.videoBitrate = 1200
|
||||
variant.isAudioPassthrough = true
|
||||
variant.SetVideoFramerate(30)
|
||||
variant.SetCPUUsageLevel(2)
|
||||
transcoder.AddVariant(variant)
|
||||
|
||||
variant2 := HLSVariant{}
|
||||
variant2.videoBitrate = 3500
|
||||
variant2.isAudioPassthrough = true
|
||||
variant2.SetVideoFramerate(24)
|
||||
variant2.SetCPUUsageLevel(4)
|
||||
transcoder.AddVariant(variant2)
|
||||
|
||||
variant3 := HLSVariant{}
|
||||
variant3.isAudioPassthrough = true
|
||||
variant3.isVideoPassthrough = true
|
||||
transcoder.AddVariant(variant3)
|
||||
|
||||
cmd := transcoder.getString()
|
||||
|
||||
expected := `FFREPORT=file="transcoder.log":level=32 /fake/path/ffmpeg -hide_banner -loglevel warning -fflags +genpts -i fakecontent.flv -map v:0 -c:v:0 h264_omx -b:v:0 1200k -maxrate:v:0 1272k -g:v:0 60 -keyint_min:v:0 60 -r:v:0 30 -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 h264_omx -b:v:1 3500k -maxrate:v:1 3710k -g:v:1 48 -keyint_min:v:1 48 -r:v:1 24 -map a:0? -c:a:1 copy -preset fast -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset ultrafast -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2 " -f hls -hls_time 2 -hls_list_size 3 -segment_format_options mpegts_flags=+initial_discontinuity:mpegts_copyts=1 -tune zerolatency -pix_fmt yuv420p -sc_threshold 0 -master_pl_name stream.m3u8 -strftime 1 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdFsdfzGg%s.ts -max_muxing_queue_size 400 -method PUT -http_persistent 0 http://127.0.0.1:8123/%v/stream.m3u8`
|
||||
|
||||
if cmd != expected {
|
||||
t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
package transcoder
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/owncast/owncast/models"
|
||||
)
|
||||
|
||||
func TestFFmpegCommand(t *testing.T) {
|
||||
latencyLevel := models.GetLatencyLevel(3)
|
||||
|
||||
transcoder := new(Transcoder)
|
||||
transcoder.ffmpegPath = "/fake/path/ffmpeg"
|
||||
transcoder.SetInput("fakecontent.flv")
|
||||
transcoder.SetOutputPath("fakeOutput")
|
||||
transcoder.SetIdentifier("jdofFGg")
|
||||
transcoder.SetInternalHTTPPort("8123")
|
||||
transcoder.currentLatencyLevel = latencyLevel
|
||||
|
||||
variant := HLSVariant{}
|
||||
variant.videoBitrate = 1200
|
||||
variant.isAudioPassthrough = true
|
||||
variant.encoderPreset = "veryfast"
|
||||
variant.SetVideoFramerate(30)
|
||||
transcoder.AddVariant(variant)
|
||||
|
||||
variant2 := HLSVariant{}
|
||||
variant2.videoBitrate = 3500
|
||||
variant2.isAudioPassthrough = true
|
||||
variant2.encoderPreset = "faster"
|
||||
variant2.SetVideoFramerate(24)
|
||||
transcoder.AddVariant(variant2)
|
||||
|
||||
variant3 := HLSVariant{}
|
||||
variant3.isAudioPassthrough = true
|
||||
variant3.isVideoPassthrough = true
|
||||
transcoder.AddVariant(variant3)
|
||||
|
||||
cmd := transcoder.getString()
|
||||
|
||||
expected := `/fake/path/ffmpeg -hide_banner -loglevel warning -i fakecontent.flv -map v:0 -c:v:0 libx264 -b:v:0 1200k -maxrate:v:0 1272k -bufsize:v:0 1440k -g:v:0 89 -profile:v:0 high -r:v:0 30 -x264-params:v:0 "scenecut=0:open_gop=0:min-keyint=89:keyint=89" -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 libx264 -b:v:1 3500k -maxrate:v:1 3710k -bufsize:v:1 4200k -g:v:1 71 -profile:v:1 high -r:v:1 24 -x264-params:v:1 "scenecut=0:open_gop=0:min-keyint=71:keyint=71" -map a:0? -c:a:1 copy -preset faster -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2 " -f hls -hls_time 3 -hls_list_size 3 -hls_delete_threshold 10 -tune zerolatency -pix_fmt yuv420p -sc_threshold 0 -master_pl_name stream.m3u8 -strftime 1 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdofFGg%s.ts -max_muxing_queue_size 400 -method PUT -http_persistent 0 -fflags +genpts http://127.0.0.1:8123/%v/stream.m3u8 2> transcoder.log`
|
||||
|
||||
if cmd != expected {
|
||||
t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)
|
||||
}
|
||||
}
|
48
core/transcoder/transcoder_vaapi_test.go
Normal file
48
core/transcoder/transcoder_vaapi_test.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package transcoder
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/owncast/owncast/models"
|
||||
)
|
||||
|
||||
func TestFFmpegVaapiCommand(t *testing.T) {
|
||||
latencyLevel := models.GetLatencyLevel(3)
|
||||
codec := VaapiCodec{}
|
||||
|
||||
transcoder := new(Transcoder)
|
||||
transcoder.ffmpegPath = "/fake/path/ffmpeg"
|
||||
transcoder.SetInput("fakecontent.flv")
|
||||
transcoder.SetOutputPath("fakeOutput")
|
||||
transcoder.SetIdentifier("jdofFGg")
|
||||
transcoder.SetInternalHTTPPort("8123")
|
||||
transcoder.SetCodec(codec.Name())
|
||||
transcoder.currentLatencyLevel = latencyLevel
|
||||
|
||||
variant := HLSVariant{}
|
||||
variant.videoBitrate = 1200
|
||||
variant.isAudioPassthrough = true
|
||||
variant.SetVideoFramerate(30)
|
||||
variant.SetCPUUsageLevel(2)
|
||||
transcoder.AddVariant(variant)
|
||||
|
||||
variant2 := HLSVariant{}
|
||||
variant2.videoBitrate = 3500
|
||||
variant2.isAudioPassthrough = true
|
||||
variant2.SetVideoFramerate(24)
|
||||
variant2.SetCPUUsageLevel(4)
|
||||
transcoder.AddVariant(variant2)
|
||||
|
||||
variant3 := HLSVariant{}
|
||||
variant3.isAudioPassthrough = true
|
||||
variant3.isVideoPassthrough = true
|
||||
transcoder.AddVariant(variant3)
|
||||
|
||||
cmd := transcoder.getString()
|
||||
|
||||
expected := `FFREPORT=file="transcoder.log":level=32 /fake/path/ffmpeg -hide_banner -loglevel warning -vaapi_device /dev/dri/renderD128 -fflags +genpts -i fakecontent.flv -map v:0 -c:v:0 h264_vaapi -b:v:0 1200k -maxrate:v:0 1272k -g:v:0 60 -keyint_min:v:0 60 -r:v:0 30 -map a:0? -c:a:0 copy -filter:v:0 "format=nv12,hwupload" -preset veryfast -map v:0 -c:v:1 h264_vaapi -b:v:1 3500k -maxrate:v:1 3710k -g:v:1 48 -keyint_min:v:1 48 -r:v:1 24 -map a:0? -c:a:1 copy -filter:v:1 "format=nv12,hwupload" -preset fast -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset ultrafast -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2 " -f hls -hls_time 2 -hls_list_size 3 -segment_format_options mpegts_flags=+initial_discontinuity:mpegts_copyts=1 -pix_fmt vaapi_vld -sc_threshold 0 -master_pl_name stream.m3u8 -strftime 1 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdofFGg%s.ts -max_muxing_queue_size 400 -method PUT -http_persistent 0 http://127.0.0.1:8123/%v/stream.m3u8`
|
||||
|
||||
if cmd != expected {
|
||||
t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)
|
||||
}
|
||||
}
|
48
core/transcoder/transcoder_x264_test.go
Normal file
48
core/transcoder/transcoder_x264_test.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package transcoder
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/owncast/owncast/models"
|
||||
)
|
||||
|
||||
func TestFFmpegx264Command(t *testing.T) {
|
||||
latencyLevel := models.GetLatencyLevel(3)
|
||||
codec := Libx264Codec{}
|
||||
|
||||
transcoder := new(Transcoder)
|
||||
transcoder.ffmpegPath = "/fake/path/ffmpeg"
|
||||
transcoder.SetInput("fakecontent.flv")
|
||||
transcoder.SetOutputPath("fakeOutput")
|
||||
transcoder.SetIdentifier("jdofFGg")
|
||||
transcoder.SetInternalHTTPPort("8123")
|
||||
transcoder.SetCodec(codec.Name())
|
||||
transcoder.currentLatencyLevel = latencyLevel
|
||||
|
||||
variant := HLSVariant{}
|
||||
variant.videoBitrate = 1200
|
||||
variant.isAudioPassthrough = true
|
||||
variant.SetVideoFramerate(30)
|
||||
variant.SetCPUUsageLevel(2)
|
||||
transcoder.AddVariant(variant)
|
||||
|
||||
variant2 := HLSVariant{}
|
||||
variant2.videoBitrate = 3500
|
||||
variant2.isAudioPassthrough = true
|
||||
variant2.SetVideoFramerate(24)
|
||||
variant2.SetCPUUsageLevel(4)
|
||||
transcoder.AddVariant(variant2)
|
||||
|
||||
variant3 := HLSVariant{}
|
||||
variant3.isAudioPassthrough = true
|
||||
variant3.isVideoPassthrough = true
|
||||
transcoder.AddVariant(variant3)
|
||||
|
||||
cmd := transcoder.getString()
|
||||
|
||||
expected := `FFREPORT=file="transcoder.log":level=32 /fake/path/ffmpeg -hide_banner -loglevel warning -fflags +genpts -i fakecontent.flv -map v:0 -c:v:0 libx264 -b:v:0 1200k -maxrate:v:0 1272k -g:v:0 60 -keyint_min:v:0 60 -r:v:0 30 -x264-params:v:0 "scenecut=0:open_gop=0" -bufsize:v:0 1440k -profile:v:0 high -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 libx264 -b:v:1 3500k -maxrate:v:1 3710k -g:v:1 48 -keyint_min:v:1 48 -r:v:1 24 -x264-params:v:1 "scenecut=0:open_gop=0" -bufsize:v:1 4200k -profile:v:1 high -map a:0? -c:a:1 copy -preset fast -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset ultrafast -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2 " -f hls -hls_time 2 -hls_list_size 3 -segment_format_options mpegts_flags=+initial_discontinuity:mpegts_copyts=1 -tune zerolatency -pix_fmt yuv420p -sc_threshold 0 -master_pl_name stream.m3u8 -strftime 1 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdofFGg%s.ts -max_muxing_queue_size 400 -method PUT -http_persistent 0 http://127.0.0.1:8123/%v/stream.m3u8`
|
||||
|
||||
if cmd != expected {
|
||||
t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)
|
||||
}
|
||||
}
|
81
core/transcoder/utils.go
Normal file
81
core/transcoder/utils.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
package transcoder
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var _lastTranscoderLogMessage = ""
|
||||
var l = &sync.RWMutex{}
|
||||
|
||||
var errorMap = map[string]string{
|
||||
"Unrecognized option 'vaapi_device'": "you are likely trying to utilize a vaapi codec, but your version of ffmpeg or your hardware doesn't support it. change your codec to libx264 and restart your stream",
|
||||
"unable to open display": "your copy of ffmpeg is likely installed via snap packages. please uninstall and re-install via a non-snap method. https://owncast.online/docs/troubleshooting/#misc-video-issues",
|
||||
"Failed to open file 'http://127.0.0.1": "error transcoding. make sure your version of ffmpeg is compatible with your selected codec or is recent enough https://owncast.online/docs/troubleshooting/#codecs",
|
||||
"can't configure encoder": "error with codec. if your copy of ffmpeg or your hardware does not support your selected codec you may need to select another",
|
||||
"Unable to parse option value": "you are likely trying to utilize a specific codec, but your version of ffmpeg or your hardware doesn't support it. either fix your ffmpeg install or try changing your codec to libx264 and restart your stream",
|
||||
"OpenEncodeSessionEx failed: out of memory": "your NVIDIA gpu is limiting the number of concurrent stream qualities you can support. remove a stream output variant and try again.",
|
||||
"Cannot use rename on non file protocol, this may lead to races and temporary partial files": "",
|
||||
"No VA display found for device": "vaapi not enabled. either your copy of ffmpeg does not support it, your hardware does not support it, or you need to install additional drivers for your hardware.",
|
||||
"Could not find a valid device": "your codec is either not supported or not configured properly",
|
||||
"H.264 bitstream error": "transcoding content error playback issues may arise. you may want to use the default codec if you are not already.",
|
||||
|
||||
`Unknown encoder 'h264_qsv'`: "your copy of ffmpeg does not have support for Intel QuickSync encoding (h264_qsv). change the selected codec in your video settings",
|
||||
`Unknown encoder 'h264_vaapi'`: "your copy of ffmpeg does not have support for VA-API encoding (h264_vaapi). change the selected codec in your video settings",
|
||||
`Unknown encoder 'h264_nvenc'`: "your copy of ffmpeg does not have support for NVIDIA hardware encoding (h264_nvenc). change the selected codec in your video settings",
|
||||
`Unknown encoder 'h264_x264'`: "your copy of ffmpeg does not have support for the default x264 codec (h264_x264). download a version of ffmpeg that supports this.",
|
||||
`Unrecognized option 'x264-params`: "your copy of ffmpeg does not have support for the default libx264 codec (h264_x264). download a version of ffmpeg that supports this.",
|
||||
|
||||
// Generic error for a codec
|
||||
"Unrecognized option": "error with codec. if your copy of ffmpeg or your hardware does not support your selected codec you may need to select another",
|
||||
}
|
||||
|
||||
var ignoredErrors = []string{
|
||||
"Duplicated segment filename detected",
|
||||
"Error while opening encoder for output stream",
|
||||
"Unable to parse option value",
|
||||
"Last message repeated",
|
||||
"Option not found",
|
||||
"use of closed network connection",
|
||||
"URL read error: End of file",
|
||||
"upload playlist failed, will retry with a new http session",
|
||||
"VBV underflow",
|
||||
"Cannot use rename on non file protocol",
|
||||
}
|
||||
|
||||
func handleTranscoderMessage(message string) {
|
||||
log.Debugln(message)
|
||||
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
|
||||
// Ignore certain messages that we don't care about.
|
||||
for _, error := range ignoredErrors {
|
||||
if strings.Contains(message, error) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Convert specific transcoding messages to human-readable messages.
|
||||
for error, displayMessage := range errorMap {
|
||||
if strings.Contains(message, error) {
|
||||
message = displayMessage
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if message == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// No good comes from a flood of repeated messages.
|
||||
if message == _lastTranscoderLogMessage {
|
||||
return
|
||||
}
|
||||
|
||||
log.Error(message)
|
||||
|
||||
_lastTranscoderLogMessage = message
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -12,7 +12,7 @@ func GetLatencyConfigs() map[int]LatencyLevel {
|
|||
return map[int]LatencyLevel{
|
||||
1: {Level: 1, SecondsPerSegment: 1, SegmentCount: 2},
|
||||
2: {Level: 2, SecondsPerSegment: 2, SegmentCount: 2},
|
||||
3: {Level: 3, SecondsPerSegment: 3, SegmentCount: 3},
|
||||
3: {Level: 3, SecondsPerSegment: 2, SegmentCount: 3},
|
||||
4: {Level: 4, SecondsPerSegment: 3, SegmentCount: 4}, // Default
|
||||
5: {Level: 5, SecondsPerSegment: 4, SegmentCount: 5},
|
||||
6: {Level: 6, SecondsPerSegment: 6, SegmentCount: 10},
|
||||
|
|
|
@ -25,8 +25,7 @@ type StreamOutputVariant struct {
|
|||
ScaledWidth int `yaml:"scaledWidth" json:"scaledWidth,omitempty"`
|
||||
ScaledHeight int `yaml:"scaledHeight" json:"scaledHeight,omitempty"`
|
||||
|
||||
Framerate int `yaml:"framerate" json:"framerate"`
|
||||
EncoderPreset string `yaml:"encoderPreset" json:"encoderPreset"` // Remove after migration is no longer used
|
||||
Framerate int `yaml:"framerate" json:"framerate"`
|
||||
// CPUUsageLevel represents a codec preset to configure CPU usage.
|
||||
CPUUsageLevel int `json:"cpuUsageLevel"`
|
||||
}
|
||||
|
@ -44,32 +43,6 @@ func (q *StreamOutputVariant) GetFramerate() int {
|
|||
return 24
|
||||
}
|
||||
|
||||
// GetEncoderPreset returns the preset or default.
|
||||
func (q *StreamOutputVariant) GetEncoderPreset() string {
|
||||
if q.IsVideoPassthrough {
|
||||
return ""
|
||||
}
|
||||
|
||||
if q.EncoderPreset != "" {
|
||||
return q.EncoderPreset
|
||||
}
|
||||
|
||||
return "veryfast"
|
||||
}
|
||||
|
||||
// GetCPUUsageLevel will return the libx264 codec encoder preset that maps to a level.
|
||||
func (q *StreamOutputVariant) GetCPUUsageLevel() int {
|
||||
presetMapping := map[string]int{
|
||||
"ultrafast": 1,
|
||||
"superfast": 2,
|
||||
"veryfast": 3,
|
||||
"faster": 4,
|
||||
"fast": 5,
|
||||
}
|
||||
|
||||
return presetMapping[q.GetEncoderPreset()]
|
||||
}
|
||||
|
||||
// GetIsAudioPassthrough will return if this variant audio is passthrough.
|
||||
func (q *StreamOutputVariant) GetIsAudioPassthrough() bool {
|
||||
if q.IsAudioPassthrough {
|
||||
|
|
34
openapi.yaml
34
openapi.yaml
|
@ -182,9 +182,9 @@ components:
|
|||
framerate:
|
||||
type: integer
|
||||
description: The target frames per second of the video.
|
||||
encoderPreset:
|
||||
type: string
|
||||
description: "The [H.264 preset value](https://trac.ffmpeg.org/wiki/Encode/H.264) selected for this HLS variant."
|
||||
cpuUsageLevel:
|
||||
type: integer
|
||||
description: "The amount of hardware utilization selected for this HLS variant."
|
||||
|
||||
TimestampedValue:
|
||||
type: object
|
||||
|
@ -984,13 +984,35 @@ paths:
|
|||
- framerate: 30
|
||||
videoPassthrough: false
|
||||
videoBitrate: 1800
|
||||
encoderPreset: veryfast
|
||||
cpuUsageLevel: 2
|
||||
audioPassthrough: true
|
||||
- framerate: 24
|
||||
videoPassthrough: false
|
||||
videoBitrate: 1000
|
||||
encoderPreset: superfast
|
||||
audioPassthrough: true
|
||||
cpuUsageLevel: 3
|
||||
audioPassthrough: true
|
||||
|
||||
/api/admin/config/video/codec:
|
||||
post:
|
||||
summary: Set the video codec.
|
||||
description: Sets the specific video codec that will be used for video encoding. Some codecs will support hardware acceleration. Not all codecs will be supported for all systems.
|
||||
tags: ["Admin"]
|
||||
security:
|
||||
- AdminBasicAuth: []
|
||||
responses:
|
||||
'200':
|
||||
$ref: "#/components/responses/BasicResponse"
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
value:
|
||||
description: The video codec to change to.
|
||||
type: string
|
||||
example:
|
||||
value: libx264
|
||||
|
||||
/api/admin/config/s3:
|
||||
post:
|
||||
|
|
2
pkged.go
2
pkged.go
File diff suppressed because one or more lines are too long
|
@ -115,6 +115,9 @@ func Start() error {
|
|||
// Disable chat
|
||||
http.HandleFunc("/api/admin/config/chat/disable", middleware.RequireAdminAuth(admin.SetChatDisabled))
|
||||
|
||||
// Set video codec
|
||||
http.HandleFunc("/api/admin/config/video/codec", middleware.RequireAdminAuth(admin.SetVideoCodec))
|
||||
|
||||
// Return all webhooks
|
||||
http.HandleFunc("/api/admin/webhooks", middleware.RequireAdminAuth(admin.GetWebhooks))
|
||||
|
||||
|
|
Loading…
Reference in a new issue