2021-02-19 10:05:52 +03:00
package transcoder
2020-06-26 03:44:47 +03:00
import (
2021-04-15 23:55:51 +03:00
"bufio"
2020-06-26 03:44:47 +03:00
"fmt"
2021-07-03 22:28:25 +03:00
"io"
2020-06-26 03:44:47 +03:00
"os/exec"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
2020-10-02 09:34:29 +03:00
"github.com/teris-io/shortid"
2020-06-26 03:44:47 +03:00
2020-10-05 20:07:09 +03:00
"github.com/owncast/owncast/config"
2021-02-19 10:05:52 +03:00
"github.com/owncast/owncast/core/data"
2021-05-23 05:25:33 +03:00
"github.com/owncast/owncast/logging"
2021-02-19 10:05:52 +03:00
"github.com/owncast/owncast/models"
2020-10-05 20:07:09 +03:00
"github.com/owncast/owncast/utils"
2020-06-26 03:44:47 +03:00
)
2020-07-12 02:00:23 +03:00
var _commandExec * exec . Cmd
2020-11-13 02:14:59 +03:00
// Transcoder is a single instance of a video transcoder.
2020-06-26 03:44:47 +03:00
type Transcoder struct {
input string
2021-07-03 22:28:25 +03:00
stdin * io . PipeReader
2020-06-26 03:44:47 +03:00
segmentOutputPath string
playlistOutputPath string
variants [ ] HLSVariant
appendToStream bool
2020-07-15 04:52:48 +03:00
ffmpegPath string
2020-10-02 09:34:29 +03:00
segmentIdentifier string
2021-02-19 10:05:52 +03:00
internalListenerPort string
2021-04-15 23:55:51 +03:00
codec Codec
2021-02-19 10:05:52 +03:00
currentStreamOutputSettings [ ] models . StreamOutputVariant
currentLatencyLevel models . LatencyLevel
2021-11-27 07:53:27 +03:00
isEvent bool
2021-02-19 10:05:52 +03:00
TranscoderCompleted func ( error )
2020-06-26 03:44:47 +03:00
}
2020-11-13 02:14:59 +03:00
// HLSVariant is a combination of settings that results in a single HLS stream.
2020-06-26 03:44:47 +03:00
type HLSVariant struct {
index int
videoSize VideoSize // Resizes the video via scaling
framerate int // The output framerate
2020-08-06 22:19:35 +03:00
videoBitrate int // The output bitrate
2020-06-26 03:44:47 +03:00
isVideoPassthrough bool // Override all settings and just copy the video stream
audioBitrate string // The audio bitrate
isAudioPassthrough bool // Override all settings and just copy the audio stream
2021-04-15 23:55:51 +03:00
cpuUsageLevel int // The amount of hardware to use for encoding a stream
2020-06-26 03:44:47 +03:00
}
2020-11-13 02:14:59 +03:00
// VideoSize is the scaled size of the video output.
2020-06-26 03:44:47 +03:00
type VideoSize struct {
Width int
Height int
}
2021-11-01 09:00:11 +03:00
// For limiting the output bitrate
// https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
// https://developer.apple.com/documentation/http_live_streaming/about_apple_s_http_live_streaming_tools
// Adjust the max & buffer size until the output bitrate doesn't exceed the ~+10% that Apple's media validator
// complains about.
// getAllocatedVideoBitrate returns the video bitrate we allocate after making some room for audio.
// 192 is pretty average.
func ( v * HLSVariant ) getAllocatedVideoBitrate ( ) int {
return int ( float64 ( v . videoBitrate ) - 192 )
}
// getMaxVideoBitrate returns the maximum video bitrate we allow the encoder to support.
func ( v * HLSVariant ) getMaxVideoBitrate ( ) int {
2021-12-01 00:26:51 +03:00
return int ( float64 ( v . getAllocatedVideoBitrate ( ) ) * 1.08 )
2021-11-01 09:00:11 +03:00
}
// getBufferSize returns how often it checks the bitrate of encoded segments to see if it's too high/low.
func ( v * HLSVariant ) getBufferSize ( ) int {
2021-12-01 00:26:51 +03:00
return int ( float64 ( v . getMaxVideoBitrate ( ) ) )
2021-11-01 09:00:11 +03:00
}
2020-11-13 02:14:59 +03:00
// getString returns a WxH formatted getString for scaling video output.
2020-06-26 03:44:47 +03:00
func ( v * VideoSize ) getString ( ) string {
widthString := strconv . Itoa ( v . Width )
heightString := strconv . Itoa ( v . Height )
if widthString != "0" && heightString != "0" {
return widthString + ":" + heightString
} else if widthString != "0" {
return widthString + ":-2"
} else if heightString != "0" {
return "-2:" + heightString
}
return ""
}
2021-09-12 10:18:15 +03:00
// Stop will stop the transcoder and kill all processing.
2020-07-12 02:00:23 +03:00
func ( t * Transcoder ) Stop ( ) {
2020-07-12 03:22:10 +03:00
log . Traceln ( "Transcoder STOP requested." )
2020-10-17 01:04:31 +03:00
err := _commandExec . Process . Kill ( )
if err != nil {
log . Errorln ( err )
2020-07-12 02:00:23 +03:00
}
}
2020-06-26 03:44:47 +03:00
// Start will execute the transcoding process with the settings previously set.
func ( t * Transcoder ) Start ( ) {
2021-04-15 23:55:51 +03:00
_lastTranscoderLogMessage = ""
2020-06-26 03:44:47 +03:00
2021-04-15 23:55:51 +03:00
command := t . getString ( )
log . Infof ( "Video transcoder started using %s with %d stream variants." , t . codec . DisplayName ( ) , len ( t . variants ) )
2021-04-16 07:34:51 +03:00
createVariantDirectories ( )
2020-06-26 03:44:47 +03:00
2021-02-19 10:05:52 +03:00
if config . EnableDebugFeatures {
2020-07-09 04:27:24 +03:00
log . Println ( command )
}
2020-07-12 02:00:23 +03:00
_commandExec = exec . Command ( "sh" , "-c" , command )
2021-07-03 22:28:25 +03:00
if t . stdin != nil {
2021-07-09 21:16:44 +03:00
_commandExec . Stdin = t . stdin
2021-07-03 22:28:25 +03:00
}
2021-04-15 23:55:51 +03:00
stdout , err := _commandExec . StderrPipe ( )
if err != nil {
2021-10-25 10:14:42 +03:00
log . Fatalln ( err )
2021-04-15 23:55:51 +03:00
}
2021-07-09 21:16:44 +03:00
if err := _commandExec . Start ( ) ; err != nil {
2023-01-11 05:50:32 +03:00
log . Errorln ( "Transcoder error. See" , logging . GetTranscoderLogFilePath ( ) , "for full output to debug." )
2020-06-26 03:44:47 +03:00
log . Panicln ( err , command )
}
2021-04-15 23:55:51 +03:00
go func ( ) {
scanner := bufio . NewScanner ( stdout )
for scanner . Scan ( ) {
line := scanner . Text ( )
handleTranscoderMessage ( line )
}
} ( )
2020-10-15 00:07:38 +03:00
err = _commandExec . Wait ( )
if t . TranscoderCompleted != nil {
t . TranscoderCompleted ( err )
}
2021-04-15 23:55:51 +03:00
if err != nil {
2023-01-11 05:50:32 +03:00
log . Errorln ( "transcoding error. look at" , logging . GetTranscoderLogFilePath ( ) , "to help debug. your copy of ffmpeg may not support your selected codec of" , t . codec . Name ( ) , "https://owncast.online/docs/codecs/" )
2021-04-15 23:55:51 +03:00
}
2020-06-26 03:44:47 +03:00
}
2021-11-27 07:53:27 +03:00
// SetLatencyLevel will set the latency level for the instance of the transcoder.
func ( t * Transcoder ) SetLatencyLevel ( level models . LatencyLevel ) {
t . currentLatencyLevel = level
}
// SetIsEvent will allow you to set a stream as an "event".
func ( t * Transcoder ) SetIsEvent ( isEvent bool ) {
t . isEvent = isEvent
}
2020-06-26 03:44:47 +03:00
func ( t * Transcoder ) getString ( ) string {
2021-11-01 09:00:11 +03:00
port := t . internalListenerPort
2021-02-19 10:05:52 +03:00
localListenerAddress := "http://127.0.0.1:" + port
2020-10-15 00:07:38 +03:00
2021-11-01 10:10:14 +03:00
hlsOptionFlags := [ ] string {
"program_date_time" ,
"independent_segments" ,
}
2020-10-15 00:07:38 +03:00
2020-06-26 03:44:47 +03:00
if t . appendToStream {
hlsOptionFlags = append ( hlsOptionFlags , "append_list" )
}
2020-10-02 09:34:29 +03:00
if t . segmentIdentifier == "" {
t . segmentIdentifier = shortid . MustGenerate ( )
}
2021-11-27 07:53:27 +03:00
hlsEventString := ""
if t . isEvent {
hlsEventString = "-hls_playlist_type event"
} else {
// Don't let the transcoder close the playlist. We do it manually.
hlsOptionFlags = append ( hlsOptionFlags , "omit_endlist" )
}
2020-10-15 00:07:38 +03:00
hlsOptionsString := ""
if len ( hlsOptionFlags ) > 0 {
hlsOptionsString = "-hls_flags " + strings . Join ( hlsOptionFlags , "+" )
}
2020-06-26 03:44:47 +03:00
ffmpegFlags := [ ] string {
2021-05-23 05:25:33 +03:00
fmt . Sprintf ( ` FFREPORT=file="%s":level=32 ` , logging . GetTranscoderLogFilePath ( ) ) ,
2020-07-15 04:52:48 +03:00
t . ffmpegPath ,
2020-06-26 03:44:47 +03:00
"-hide_banner" ,
2020-10-15 00:07:38 +03:00
"-loglevel warning" ,
2021-04-15 23:55:51 +03:00
t . codec . GlobalFlags ( ) ,
"-fflags +genpts" , // Generate presentation time stamp if missing
2023-05-12 02:56:07 +03:00
"-flags +cgop" , // Force closed GOPs
2020-10-15 00:07:38 +03:00
"-i " , t . input ,
2020-06-26 03:44:47 +03:00
t . getVariantsString ( ) ,
// HLS Output
"-f" , "hls" ,
2020-10-15 00:07:38 +03:00
2021-02-19 10:05:52 +03:00
"-hls_time" , strconv . Itoa ( t . currentLatencyLevel . SecondsPerSegment ) , // Length of each segment
"-hls_list_size" , strconv . Itoa ( t . currentLatencyLevel . SegmentCount ) , // Max # in variant playlist
2020-10-15 00:07:38 +03:00
hlsOptionsString ,
2021-11-27 07:53:27 +03:00
hlsEventString ,
2021-11-01 10:10:14 +03:00
"-segment_format_options" , "mpegts_flags=mpegts_copyts=1" ,
2020-06-26 03:44:47 +03:00
// Video settings
2021-04-15 23:55:51 +03:00
t . codec . ExtraArguments ( ) ,
"-pix_fmt" , t . codec . PixelFormat ( ) ,
2020-06-26 03:44:47 +03:00
"-sc_threshold" , "0" , // Disable scene change detection for creating segments
// Filenames
"-master_pl_name" , "stream.m3u8" ,
2020-10-15 00:07:38 +03:00
2022-03-11 01:13:15 +03:00
"-hls_segment_filename" , localListenerAddress + "/%v/stream-" + t . segmentIdentifier + "-%d.ts" , // Send HLS segments back to us over HTTP
2020-06-26 03:44:47 +03:00
"-max_muxing_queue_size" , "400" , // Workaround for Too many packets error: https://trac.ffmpeg.org/ticket/6375?cversion=0
2020-10-15 00:07:38 +03:00
2022-01-28 07:16:35 +03:00
"-method PUT" , // HLS results sent back to us will be over PUTs
2020-10-15 00:07:38 +03:00
localListenerAddress + "/%v/stream.m3u8" , // Send HLS playlists back to us over HTTP
2020-06-26 03:44:47 +03:00
}
return strings . Join ( ffmpegFlags , " " )
}
2021-02-19 10:05:52 +03:00
func getVariantFromConfigQuality ( quality models . StreamOutputVariant , index int ) HLSVariant {
2020-06-26 03:44:47 +03:00
variant := HLSVariant { }
variant . index = index
variant . isAudioPassthrough = quality . IsAudioPassthrough
variant . isVideoPassthrough = quality . IsVideoPassthrough
// If no audio bitrate is specified then we pass through original audio
if quality . AudioBitrate == 0 {
variant . isAudioPassthrough = true
}
if quality . VideoBitrate == 0 {
2020-11-13 01:23:52 +03:00
quality . VideoBitrate = 1200
2020-06-26 03:44:47 +03:00
}
// If the video is being passed through then
// don't continue to set options on the variant.
if variant . isVideoPassthrough {
return variant
}
// 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
2021-04-15 23:55:51 +03:00
variant . cpuUsageLevel = quality . CPUUsageLevel
2020-06-26 03:44:47 +03:00
2020-08-06 22:19:35 +03:00
variant . SetVideoBitrate ( quality . VideoBitrate )
2020-06-26 03:44:47 +03:00
variant . SetAudioBitrate ( strconv . Itoa ( quality . AudioBitrate ) + "k" )
variant . SetVideoScalingWidth ( quality . ScaledWidth )
variant . SetVideoScalingHeight ( quality . ScaledHeight )
2020-08-06 22:19:35 +03:00
variant . SetVideoFramerate ( quality . GetFramerate ( ) )
2020-06-26 03:44:47 +03:00
return variant
}
2020-11-13 02:14:59 +03:00
// NewTranscoder will return a new Transcoder, populated by the config.
2020-10-15 00:07:38 +03:00
func NewTranscoder ( ) * Transcoder {
2021-02-19 10:05:52 +03:00
ffmpegPath := utils . ValidatedFfmpegPath ( data . GetFfMpegPath ( ) )
2020-06-26 03:44:47 +03:00
transcoder := new ( Transcoder )
2021-02-19 10:05:52 +03:00
transcoder . ffmpegPath = ffmpegPath
transcoder . internalListenerPort = config . InternalHLSListenerPort
transcoder . currentStreamOutputSettings = data . GetStreamOutputVariants ( )
transcoder . currentLatencyLevel = data . GetStreamLatencyLevel ( )
2021-04-15 23:55:51 +03:00
transcoder . codec = getCodec ( data . GetVideoCodec ( ) )
2021-09-12 21:32:42 +03:00
transcoder . segmentOutputPath = config . HLSStoragePath
transcoder . playlistOutputPath = config . HLSStoragePath
2020-06-26 03:44:47 +03:00
2021-07-03 22:28:25 +03:00
transcoder . input = "pipe:0" // stdin
2020-06-26 03:44:47 +03:00
2021-02-19 10:05:52 +03:00
for index , quality := range transcoder . currentStreamOutputSettings {
2020-06-26 03:44:47 +03:00
variant := getVariantFromConfigQuality ( quality , index )
transcoder . AddVariant ( variant )
}
2020-10-15 00:07:38 +03:00
return transcoder
2020-06-26 03:44:47 +03:00
}
// Uses `map` https://www.ffmpeg.org/ffmpeg-all.html#Stream-specifiers-1 https://www.ffmpeg.org/ffmpeg-all.html#Advanced-options
2020-08-06 22:19:35 +03:00
func ( v * HLSVariant ) getVariantString ( t * Transcoder ) string {
2020-06-26 03:44:47 +03:00
variantEncoderCommands := [ ] string {
2020-08-06 22:19:35 +03:00
v . getVideoQualityString ( t ) ,
2020-06-26 03:44:47 +03:00
v . getAudioQualityString ( ) ,
}
2021-04-15 23:55:51 +03:00
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 )
2020-06-26 03:44:47 +03:00
}
2021-04-15 23:55:51 +03:00
preset := t . codec . GetPresetForLevel ( v . cpuUsageLevel )
if preset != "" {
variantEncoderCommands = append ( variantEncoderCommands , fmt . Sprintf ( "-preset %s" , preset ) )
2020-06-26 03:44:47 +03:00
}
return strings . Join ( variantEncoderCommands , " " )
}
2020-11-13 02:14:59 +03:00
// Get the command flags for the variants.
2020-06-26 03:44:47 +03:00
func ( t * Transcoder ) getVariantsString ( ) string {
2021-11-01 09:00:11 +03:00
variantsCommandFlags := ""
variantsStreamMaps := " -var_stream_map \""
2020-06-26 03:44:47 +03:00
for _ , variant := range t . variants {
2020-08-06 22:19:35 +03:00
variantsCommandFlags = variantsCommandFlags + " " + variant . getVariantString ( t )
2021-10-25 10:31:45 +03:00
singleVariantMap := fmt . Sprintf ( "v:%d,a:%d " , variant . index , variant . index )
variantsStreamMaps += singleVariantMap
2020-06-26 03:44:47 +03:00
}
variantsCommandFlags = variantsCommandFlags + " " + variantsStreamMaps + "\""
return variantsCommandFlags
}
// Video Scaling
// https://trac.ffmpeg.org/wiki/Scaling
// If we'd like to keep the aspect ratio, we need to specify only one component, either width or height.
// Some codecs require the size of width and height to be a multiple of n. You can achieve this by setting the width or height to -n.
2020-11-13 02:14:59 +03:00
// SetVideoScalingWidth will set the scaled video width of this variant.
2020-06-26 03:44:47 +03:00
func ( v * HLSVariant ) SetVideoScalingWidth ( width int ) {
v . videoSize . Width = width
}
2020-11-13 02:14:59 +03:00
// SetVideoScalingHeight will set the scaled video height of this variant.
2020-06-26 03:44:47 +03:00
func ( v * HLSVariant ) SetVideoScalingHeight ( height int ) {
v . videoSize . Height = height
}
func ( v * HLSVariant ) getScalingString ( ) string {
2021-04-15 23:55:51 +03:00
return fmt . Sprintf ( "scale=%s" , v . videoSize . getString ( ) )
2020-06-26 03:44:47 +03:00
}
// Video Quality
2020-11-13 02:14:59 +03:00
// SetVideoBitrate will set the output bitrate of this variant's video.
2020-08-06 22:19:35 +03:00
func ( v * HLSVariant ) SetVideoBitrate ( bitrate int ) {
2020-06-26 03:44:47 +03:00
v . videoBitrate = bitrate
}
2020-08-06 22:19:35 +03:00
func ( v * HLSVariant ) getVideoQualityString ( t * Transcoder ) string {
2020-06-26 03:44:47 +03:00
if v . isVideoPassthrough {
return fmt . Sprintf ( "-map v:0 -c:v:%d copy" , v . index )
}
2021-05-06 04:21:27 +03:00
gop := v . framerate * t . currentLatencyLevel . SecondsPerSegment // force an i-frame every segment
2020-08-06 22:19:35 +03:00
cmd := [ ] string {
"-map v:0" ,
2021-11-01 09:00:11 +03:00
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 . getAllocatedVideoBitrate ( ) ) , // The average bitrate for this variant allowing space for audio
fmt . Sprintf ( "-maxrate:v:%d %dk" , v . index , v . getMaxVideoBitrate ( ) ) , // The max bitrate allowed for this variant
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
2020-11-20 09:07:28 +03:00
fmt . Sprintf ( "-r:v:%d %d" , v . index , v . framerate ) ,
2021-04-15 23:55:51 +03:00
t . codec . VariantFlags ( v ) ,
2020-08-06 22:19:35 +03:00
}
return strings . Join ( cmd , " " )
2020-06-26 03:44:47 +03:00
}
2020-11-13 02:14:59 +03:00
// SetVideoFramerate will set the output framerate of this variant's video.
2020-06-26 03:44:47 +03:00
func ( v * HLSVariant ) SetVideoFramerate ( framerate int ) {
v . framerate = framerate
}
2021-04-15 23:55:51 +03:00
// SetCPUUsageLevel will set the hardware usage of this variant.
func ( v * HLSVariant ) SetCPUUsageLevel ( level int ) {
v . cpuUsageLevel = level
2020-06-26 03:44:47 +03:00
}
// Audio Quality
2020-11-13 02:14:59 +03:00
// SetAudioBitrate will set the output framerate of this variant's audio.
2020-06-26 03:44:47 +03:00
func ( v * HLSVariant ) SetAudioBitrate ( bitrate string ) {
v . audioBitrate = bitrate
}
func ( v * HLSVariant ) getAudioQualityString ( ) string {
if v . isAudioPassthrough {
2020-12-02 11:19:55 +03:00
return fmt . Sprintf ( "-map a:0? -c:a:%d copy" , v . index )
2020-06-26 03:44:47 +03:00
}
2020-07-12 02:34:50 +03:00
// libfdk_aac is not a part of every ffmpeg install, so use "aac" instead
encoderCodec := "aac"
2020-12-02 11:19:55 +03:00
return fmt . Sprintf ( "-map a:0? -c:a:%d %s -b:a:%d %s" , v . index , encoderCodec , v . index , v . audioBitrate )
2020-06-26 03:44:47 +03:00
}
2020-11-13 02:14:59 +03:00
// AddVariant adds a new HLS variant to include in the output.
2020-06-26 03:44:47 +03:00
func ( t * Transcoder ) AddVariant ( variant HLSVariant ) {
2020-10-20 07:34:42 +03:00
variant . index = len ( t . variants )
2020-06-26 03:44:47 +03:00
t . variants = append ( t . variants , variant )
}
2020-11-13 02:14:59 +03:00
// SetInput sets the input stream on the filesystem.
2020-06-26 03:44:47 +03:00
func ( t * Transcoder ) SetInput ( input string ) {
t . input = input
}
2021-07-03 22:28:25 +03:00
// SetStdin sets the Stdin of the ffmpeg command.
2021-10-12 01:04:16 +03:00
func ( t * Transcoder ) SetStdin ( pipe * io . PipeReader ) {
t . stdin = pipe
2021-07-03 22:28:25 +03:00
}
2020-11-13 02:14:59 +03:00
// SetOutputPath sets the root directory that should include playlists and video segments.
2020-06-26 03:44:47 +03:00
func ( t * Transcoder ) SetOutputPath ( output string ) {
t . segmentOutputPath = output
}
2021-09-12 10:18:15 +03:00
// SetIdentifier enables appending a unique identifier to segment file name.
2020-10-02 09:34:29 +03:00
func ( t * Transcoder ) SetIdentifier ( output string ) {
t . segmentIdentifier = output
}
2020-10-15 00:07:38 +03:00
2021-09-12 10:18:15 +03:00
// SetInternalHTTPPort will set the port to be used for internal communication.
2021-02-19 10:05:52 +03:00
func ( t * Transcoder ) SetInternalHTTPPort ( port string ) {
2020-10-15 00:07:38 +03:00
t . internalListenerPort = port
}
2021-04-15 23:55:51 +03:00
2021-09-12 10:18:15 +03:00
// SetCodec will set the codec to be used for the transocder.
2021-04-15 23:55:51 +03:00
func ( t * Transcoder ) SetCodec ( codecName string ) {
t . codec = getCodec ( codecName )
}