mirror of
				https://github.com/owncast/owncast.git
				synced 2025-11-01 02:44:31 +08:00 
			
		
		
		
	New video transcoder (#27)
* New video transcoder * Set a smaller max muxing size + logging tweaks * No need to return an err since it wiill panic anyway * Use a default encoder preset if one is not supplied * Rename to NewTranscoder. Add comments. * Rename Bitrate to VideoBitrate. Config file changes required with this rename. * Allow overrides of segment length. Allow offline stream to live in a single segment * Append offline state video to existin HLS stream instead of overwriting because players do not like that * Make properties private as they do not need to be exported
This commit is contained in:
		| @ -1,18 +1,25 @@ | ||||
| publicHLSPath: webroot/hls | ||||
| privateHLSPath: hls | ||||
| ffmpegPath: /usr/local/bin/ffmpeg | ||||
| ffmpegPath: /usr/bin/ffmpeg | ||||
| webServerPort: 8080 | ||||
| enableOfflineImage: true | ||||
|  | ||||
| videoSettings: | ||||
|   chunkLengthInSeconds: 4 | ||||
|   streamingKey: abc123 | ||||
|   encoderPreset: superfast # https://trac.ffmpeg.org/wiki/Encode/H.264 | ||||
|   passthrough: true # Enabling this will ignore the below stream qualities and pass through the same quality that you're sending it | ||||
|   offlineImage: doc/logo.png # Is displayed when a stream ends | ||||
|   offlineContent: static/offline.m4v # Is displayed when a stream ends | ||||
|  | ||||
|   streamQualities: | ||||
|     - bitrate: 1000 # in k | ||||
|     # Pass through the exact video and audio that you're streaming. | ||||
|     - full: | ||||
|       videoPassthrough: true | ||||
|       audioPassthrough: true | ||||
|  | ||||
|     # Transcode the video to a lower bitrate and resize | ||||
|     # - low: | ||||
|     # videoBitrate: 700 | ||||
|     # scaledWidth: 600 | ||||
|     # audioPassthrough: true | ||||
|     # encoderPreset: superfast | ||||
|  | ||||
| files: | ||||
|   maxNumberInPlaylist: 30 | ||||
| @ -27,4 +34,4 @@ s3: | ||||
|   accessKey: ABC12342069 | ||||
|   secret: lolomgqwtf49583949 | ||||
|   region: us-west-2 | ||||
|   bucket: myvideo | ||||
|   bucket: myvideo | ||||
|  | ||||
| @ -15,28 +15,41 @@ import ( | ||||
| var Config *config | ||||
|  | ||||
| type config struct { | ||||
| 	IPFS               ipfs          `yaml:"ipfs"` | ||||
| 	PublicHLSPath      string        `yaml:"publicHLSPath"` | ||||
| 	PrivateHLSPath     string        `yaml:"privateHLSPath"` | ||||
| 	VideoSettings      videoSettings `yaml:"videoSettings"` | ||||
| 	Files              files         `yaml:"files"` | ||||
| 	FFMpegPath         string        `yaml:"ffmpegPath"` | ||||
| 	WebServerPort      int           `yaml:"webServerPort"` | ||||
| 	S3                 s3            `yaml:"s3"` | ||||
| 	EnableOfflineImage bool          `yaml:"enableOfflineImage"` | ||||
| 	IPFS           ipfs          `yaml:"ipfs"` | ||||
| 	PublicHLSPath  string        `yaml:"publicHLSPath"` | ||||
| 	PrivateHLSPath string        `yaml:"privateHLSPath"` | ||||
| 	VideoSettings  videoSettings `yaml:"videoSettings"` | ||||
| 	Files          files         `yaml:"files"` | ||||
| 	FFMpegPath     string        `yaml:"ffmpegPath"` | ||||
| 	WebServerPort  int           `yaml:"webServerPort"` | ||||
| 	S3             s3            `yaml:"s3"` | ||||
| } | ||||
|  | ||||
| type videoSettings struct { | ||||
| 	ChunkLengthInSeconds int             `yaml:"chunkLengthInSeconds"` | ||||
| 	StreamingKey         string          `yaml:"streamingKey"` | ||||
| 	EncoderPreset        string          `yaml:"encoderPreset"` | ||||
| 	StreamQualities      []streamQuality `yaml:"streamQualities"` | ||||
| 	StreamQualities      []StreamQuality `yaml:"streamQualities"` | ||||
| 	OfflineContent       string          `yaml:"offlineContent"` | ||||
| 	EnablePassthrough    bool            `yaml:"passthrough"` | ||||
| 	OfflineImage         string          `yaml:"offlineImage"` | ||||
| } | ||||
|  | ||||
| type streamQuality struct { | ||||
| 	Bitrate int `yaml:"bitrate"` | ||||
| type StreamQuality struct { | ||||
| 	// Enable passthrough to copy the video and/or audio directly from the | ||||
| 	// incoming stream and disable any transcoding.  It will ignore any of | ||||
| 	// the below settings. | ||||
| 	IsVideoPassthrough bool `yaml:"videoPassthrough"` | ||||
| 	IsAudioPassthrough bool `yaml:"audioPassthrough"` | ||||
|  | ||||
| 	VideoBitrate int `yaml:"videoBitrate"` | ||||
| 	AudioBitrate int `yaml:"audioBitrate"` | ||||
|  | ||||
| 	// Set only one of these in order to keep your current aspect ratio. | ||||
| 	// Or set neither to not scale the video. | ||||
| 	ScaledWidth  int `yaml:"scaledWidth"` | ||||
| 	ScaledHeight int `yaml:"scaledHeight"` | ||||
|  | ||||
| 	Framerate     int    `yaml:"framerate"` | ||||
| 	EncoderPreset string `yaml:"encoderPreset"` | ||||
| } | ||||
|  | ||||
| type files struct { | ||||
| @ -108,10 +121,6 @@ func (c *config) verifySettings() error { | ||||
| 		return fmt.Errorf("ffmpeg does not exist at: %s", c.FFMpegPath) | ||||
| 	} | ||||
|  | ||||
| 	if c.VideoSettings.EncoderPreset == "" { | ||||
| 		return errors.New("a video encoder preset is required to be set in the config file") | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -51,7 +51,9 @@ func createInitialOfflineState() error { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return ffmpeg.ShowStreamOfflineState() | ||||
| 	ffmpeg.ShowStreamOfflineState() | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func resetDirectories() { | ||||
|  | ||||
| @ -1,200 +1,14 @@ | ||||
| package ffmpeg | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"math" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"path" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	log "github.com/sirupsen/logrus" | ||||
|  | ||||
| 	"github.com/gabek/owncast/config" | ||||
| 	"github.com/gabek/owncast/utils" | ||||
| ) | ||||
|  | ||||
| //ShowStreamOfflineState generates and shows the stream's offline state | ||||
| func ShowStreamOfflineState() error { | ||||
| 	log.Println("----- Stream offline!  Showing offline state!") | ||||
|  | ||||
| 	var outputDir = config.Config.PublicHLSPath | ||||
| 	var variantPlaylistPath = config.Config.PublicHLSPath | ||||
|  | ||||
| 	if config.Config.IPFS.Enabled || config.Config.S3.Enabled { | ||||
| 		outputDir = config.Config.PrivateHLSPath | ||||
| 		variantPlaylistPath = config.Config.PrivateHLSPath | ||||
| 	} | ||||
|  | ||||
| 	outputDir = path.Join(outputDir, "%v") | ||||
| 	var variantPlaylistName = path.Join(variantPlaylistPath, "%v", "stream.m3u8") | ||||
|  | ||||
| 	var videoMaps = make([]string, 0) | ||||
| 	var streamMaps = make([]string, 0) | ||||
| 	var videoMapsString = "" | ||||
| 	var streamMappingString = "" | ||||
| 	if config.Config.VideoSettings.EnablePassthrough || len(config.Config.VideoSettings.StreamQualities) == 0 { | ||||
| 		log.Println("Enabling passthrough video for offline state") | ||||
| 		videoMapsString = "-b:v 1200k -b:a 128k" // Since we're compositing multiple sources we can't infer bitrate, so pick something reasonable. | ||||
| 		streamMaps = append(streamMaps, fmt.Sprintf("v:%d", 0)) | ||||
| 	} else { | ||||
| 		for index, quality := range config.Config.VideoSettings.StreamQualities { | ||||
| 			maxRate := math.Floor(float64(quality.Bitrate) * 0.8) | ||||
| 			videoMaps = append(videoMaps, fmt.Sprintf("-map v:0 -c:v:%d libx264 -b:v:%d %dk -maxrate %dk -bufsize %dk", index, index, int(quality.Bitrate), int(maxRate), int(maxRate))) | ||||
| 			streamMaps = append(streamMaps, fmt.Sprintf("v:%d", index)) | ||||
| 			videoMapsString = strings.Join(videoMaps, " ") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	framerate := 25 | ||||
|  | ||||
| 	streamMappingString = "-var_stream_map \"" + strings.Join(streamMaps, " ") + "\"" | ||||
|  | ||||
| 	ffmpegFlags := []string{ | ||||
| 		"-hide_banner", | ||||
| 		// "-stream_loop 100", | ||||
| 		// "-fflags", "+genpts", | ||||
| 		"-i", config.Config.VideoSettings.OfflineImage, | ||||
| 		"-i", "webroot/thumbnail.jpg", | ||||
| 		"-filter_complex", "\"[0:v]scale=2640:2360[bg];[bg][1:v]overlay=200:250:enable='between(t,0,3)'\"", | ||||
| 		videoMapsString, // All the different video variants | ||||
| 		"-f hls", | ||||
| 		// "-hls_list_size " + strconv.Itoa(config.Config.Files.MaxNumberInPlaylist), | ||||
| 		"-hls_time 4", // + strconv.Itoa(config.Config.VideoSettings.ChunkLengthInSeconds), | ||||
| 		"-hls_playlist_type", "event", | ||||
| 		"-master_pl_name", "stream.m3u8", | ||||
| 		"-strftime 1", | ||||
| 		"-use_localtime 1", | ||||
| 		"-hls_flags temp_file", | ||||
| 		"-tune", "zerolatency", | ||||
| 		"-g " + strconv.Itoa(framerate*2), " -keyint_min " + strconv.Itoa(framerate*2), // multiply your output frame rate * 2. For example, if your input is -framerate 30, then use -g 60 | ||||
| 		"-framerate " + strconv.Itoa(framerate), | ||||
| 		"-preset " + config.Config.VideoSettings.EncoderPreset, | ||||
| 		"-sc_threshold 0",    // don't create key frames on scene change - only according to -g | ||||
| 		"-profile:v", "main", // Main – for standard definition (SD) to 640×480, High – for high definition (HD) to 1920×1080 | ||||
| 		// "-movflags +faststart", | ||||
| 		"-pix_fmt yuv420p", | ||||
|  | ||||
| 		streamMappingString, | ||||
| 		"-hls_segment_filename " + path.Join(outputDir, "offline-%s.ts"), | ||||
| 		// "-s", "720x480", // size | ||||
| 		variantPlaylistName, | ||||
| 	} | ||||
|  | ||||
| 	ffmpegFlagsString := strings.Join(ffmpegFlags, " ") | ||||
|  | ||||
| 	ffmpegCmd := config.Config.FFMpegPath + " " + ffmpegFlagsString | ||||
|  | ||||
| 	// log.Println(ffmpegCmd) | ||||
|  | ||||
| 	_, err := exec.Command("sh", "-c", ffmpegCmd).Output() | ||||
|  | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| //Start starts the ffmpeg process | ||||
| func Start() error { | ||||
| 	var outputDir = config.Config.PublicHLSPath | ||||
| 	var variantPlaylistPath = config.Config.PublicHLSPath | ||||
|  | ||||
| 	if config.Config.IPFS.Enabled || config.Config.S3.Enabled { | ||||
| 		outputDir = config.Config.PrivateHLSPath | ||||
| 		variantPlaylistPath = config.Config.PrivateHLSPath | ||||
| 	} | ||||
|  | ||||
| 	outputDir = path.Join(outputDir, "%v") | ||||
| 	var variantPlaylistName = path.Join(variantPlaylistPath, "%v", "stream.m3u8") | ||||
|  | ||||
| 	log.Printf("Starting transcoder saving to /%s.", variantPlaylistName) | ||||
| 	pipePath := utils.GetTemporaryPipePath() | ||||
|  | ||||
| 	var videoMaps = make([]string, 0) | ||||
| 	var streamMaps = make([]string, 0) | ||||
| 	var audioMaps = make([]string, 0) | ||||
| 	var videoMapsString = "" | ||||
| 	var audioMapsString = "" | ||||
| 	var streamMappingString = "" | ||||
| 	var profileString = "" | ||||
|  | ||||
| 	if config.Config.VideoSettings.EnablePassthrough || len(config.Config.VideoSettings.StreamQualities) == 0 { | ||||
| 		log.Println("Enabling passthrough video for stream") | ||||
| 		streamMaps = append(streamMaps, fmt.Sprintf("v:%d,a:%d", 0, 0)) | ||||
| 		videoMaps = append(videoMaps, "-map v:0 -c:v copy") | ||||
| 		videoMapsString = strings.Join(videoMaps, " ") | ||||
| 		audioMaps = append(audioMaps, "-map a:0") | ||||
| 		audioMapsString = strings.Join(audioMaps, " ") + " -c:a copy" // Pass through audio for all the variants, don't reencode | ||||
|  | ||||
| 	} else { | ||||
| 		for index, quality := range config.Config.VideoSettings.StreamQualities { | ||||
| 			maxRate := math.Floor(float64(quality.Bitrate) * 0.8) | ||||
| 			videoMaps = append(videoMaps, fmt.Sprintf("-map v:0 -c:v:%d libx264 -b:v:%d %dk -maxrate %dk -bufsize %dk", index, index, int(quality.Bitrate), int(maxRate), int(maxRate))) | ||||
| 			streamMaps = append(streamMaps, fmt.Sprintf("v:%d,a:%d", index, index)) | ||||
| 			videoMapsString = strings.Join(videoMaps, " ") | ||||
| 			audioMaps = append(audioMaps, "-map a:0") | ||||
| 			audioMapsString = strings.Join(audioMaps, " ") + " -c:a copy" // Pass through audio for all the variants, don't reencode | ||||
| 			profileString = "-profile:v high"                             // Main – for standard definition (SD) to 640×480, High – for high definition (HD) to 1920×1080 | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	framerate := 25 | ||||
|  | ||||
| 	streamMappingString = "-var_stream_map \"" + strings.Join(streamMaps, " ") + "\"" | ||||
| 	ffmpegFlags := []string{ | ||||
| 		"-hide_banner", | ||||
| 		// "-re", | ||||
| 		"-fflags", "+genpts", | ||||
| 		"-i pipe:", | ||||
| 		// "-vf scale=900:-2", // Re-enable in the future with a config to togging resizing? | ||||
| 		// "-sws_flags fast_bilinear", | ||||
| 		videoMapsString, // All the different video variants | ||||
| 		audioMapsString, | ||||
| 		"-master_pl_name stream.m3u8", | ||||
| 		"-framerate " + strconv.Itoa(framerate), | ||||
| 		"-g " + strconv.Itoa(framerate*2), " -keyint_min " + strconv.Itoa(framerate*2), // multiply your output frame rate * 2. For example, if your input is -framerate 30, then use -g 60 | ||||
| 		// "-r 25", | ||||
| 		"-preset " + config.Config.VideoSettings.EncoderPreset, | ||||
| 		"-sc_threshold 0", // don't create key frames on scene change - only according to -g | ||||
| 		profileString, | ||||
| 		"-movflags +faststart", | ||||
| 		"-pix_fmt yuv420p", | ||||
| 		"-f hls", | ||||
| 		"-hls_list_size " + strconv.Itoa(config.Config.Files.MaxNumberInPlaylist), | ||||
| 		"-hls_delete_threshold 10", // Keep 10 unreferenced segments on disk before they're deleted. | ||||
| 		"-hls_time " + strconv.Itoa(config.Config.VideoSettings.ChunkLengthInSeconds), | ||||
| 		"-strftime 1", | ||||
| 		"-use_localtime 1", | ||||
| 		"-hls_segment_filename " + path.Join(outputDir, "stream-%Y%m%d-%s.ts"), | ||||
| 		"-hls_flags delete_segments+program_date_time+temp_file", | ||||
| 		"-tune zerolatency", | ||||
| 		// "-s", "720x480", // size | ||||
|  | ||||
| 		streamMappingString, | ||||
| 		variantPlaylistName, | ||||
| 	} | ||||
|  | ||||
| 	ffmpegFlagsString := strings.Join(ffmpegFlags, " ") | ||||
|  | ||||
| 	ffmpegCmd := "cat " + pipePath + " | " + config.Config.FFMpegPath + " " + ffmpegFlagsString | ||||
|  | ||||
| 	// fmt.Println(ffmpegCmd) | ||||
|  | ||||
| 	_, err := exec.Command("sh", "-c", ffmpegCmd).Output() | ||||
|  | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| //WritePlaylist writes the playlist to disk | ||||
| func WritePlaylist(data string, filePath string) error { | ||||
| 	f, err := os.Create(filePath) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer f.Close() | ||||
|  | ||||
| 	if _, err := f.WriteString(data); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| func ShowStreamOfflineState() { | ||||
| 	transcoder := NewTranscoder() | ||||
| 	transcoder.SetSegmentLength(10) | ||||
| 	transcoder.SetAppendToStream(true) | ||||
| 	transcoder.SetInput(config.Config.VideoSettings.OfflineContent) | ||||
| 	transcoder.Start() | ||||
| } | ||||
|  | ||||
							
								
								
									
										315
									
								
								core/ffmpeg/transcoder.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										315
									
								
								core/ffmpeg/transcoder.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,315 @@ | ||||
| package ffmpeg | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os/exec" | ||||
| 	"path" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	log "github.com/sirupsen/logrus" | ||||
|  | ||||
| 	"github.com/gabek/owncast/config" | ||||
| 	"github.com/gabek/owncast/utils" | ||||
| ) | ||||
|  | ||||
| // Transcoder is a single instance of a video transcoder | ||||
| type Transcoder struct { | ||||
| 	input                string | ||||
| 	segmentOutputPath    string | ||||
| 	playlistOutputPath   string | ||||
| 	variants             []HLSVariant | ||||
| 	hlsPlaylistLength    int | ||||
| 	segmentLengthSeconds int | ||||
| 	appendToStream       bool | ||||
| } | ||||
|  | ||||
| // HLSVariant is a combination of settings that results in a single HLS stream | ||||
| type HLSVariant struct { | ||||
| 	index int | ||||
|  | ||||
| 	videoSize          VideoSize // Resizes the video via scaling | ||||
| 	framerate          int       // The output framerate | ||||
| 	videoBitrate       string    // The output bitrate | ||||
| 	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 | ||||
|  | ||||
| 	encoderPreset string // A collection of automatic settings for the encoder. https://trac.ffmpeg.org/wiki/Encode/H.264#crf | ||||
| } | ||||
|  | ||||
| // VideoSize is the scaled size of the video output | ||||
| type VideoSize struct { | ||||
| 	Width  int | ||||
| 	Height int | ||||
| } | ||||
|  | ||||
| // getString returns a WxH formatted getString for scaling video output | ||||
| 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 "" | ||||
| } | ||||
|  | ||||
| // Start will execute the transcoding process with the settings previously set. | ||||
| func (t *Transcoder) Start() { | ||||
| 	command := t.getString() | ||||
|  | ||||
| 	log.Printf("Video transcoder started with %d stream variants.", len(t.variants)) | ||||
|  | ||||
| 	_, err := exec.Command("sh", "-c", command).Output() | ||||
| 	if err != nil { | ||||
| 		log.Panicln(err, command) | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (t *Transcoder) getString() string { | ||||
| 	hlsOptionFlags := []string{ | ||||
| 		"delete_segments", | ||||
| 		"program_date_time", | ||||
| 		"temp_file", | ||||
| 	} | ||||
|  | ||||
| 	if t.appendToStream { | ||||
| 		hlsOptionFlags = append(hlsOptionFlags, "append_list") | ||||
| 	} | ||||
|  | ||||
| 	ffmpegFlags := []string{ | ||||
| 		"cat", t.input, "|", | ||||
| 		config.Config.FFMpegPath, | ||||
| 		"-hide_banner", | ||||
| 		"-i pipe:", | ||||
| 		t.getVariantsString(), | ||||
|  | ||||
| 		// HLS Output | ||||
| 		"-f", "hls", | ||||
| 		"-hls_time", strconv.Itoa(t.segmentLengthSeconds), // Length of each segment | ||||
| 		"-hls_list_size", strconv.Itoa(config.Config.Files.MaxNumberInPlaylist), // Max # in variant playlist | ||||
| 		"-hls_delete_threshold", "10", // Start deleting files after hls_list_size + 10 | ||||
| 		"-hls_flags", strings.Join(hlsOptionFlags, "+"), // Specific options in HLS generation | ||||
|  | ||||
| 		// Video settings | ||||
| 		"-tune", "zerolatency", // Option used for good for fast encoding and low-latency streaming (always includes iframes in each segment) | ||||
| 		// "-profile:v", "high", // Main – for standard definition (SD) to 640×480, High – for high definition (HD) to 1920×1080 | ||||
| 		"-sc_threshold", "0", // Disable scene change detection for creating segments | ||||
|  | ||||
| 		// Filenames | ||||
| 		"-master_pl_name", "stream.m3u8", | ||||
| 		"-strftime 1",                                                               // Support the use of strftime in filenames | ||||
| 		"-hls_segment_filename", path.Join(t.segmentOutputPath, "/%v/stream-%s.ts"), // Each segment's filename | ||||
| 		"-max_muxing_queue_size", "400", // Workaround for Too many packets error: https://trac.ffmpeg.org/ticket/6375?cversion=0 | ||||
| 		path.Join(t.segmentOutputPath, "/%v/stream.m3u8"), // Each variant's playlist | ||||
| 	} | ||||
|  | ||||
| 	return strings.Join(ffmpegFlags, " ") | ||||
| } | ||||
|  | ||||
| func getVariantFromConfigQuality(quality config.StreamQuality, index int) HLSVariant { | ||||
| 	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 { | ||||
| 		variant.isVideoPassthrough = true | ||||
| 	} | ||||
|  | ||||
| 	// 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 | ||||
| 	if quality.EncoderPreset != "" { | ||||
| 		variant.encoderPreset = quality.EncoderPreset | ||||
| 	} else { | ||||
| 		variant.encoderPreset = "veryfast" | ||||
| 	} | ||||
|  | ||||
| 	variant.SetVideoBitrate(strconv.Itoa(quality.VideoBitrate) + "k") | ||||
| 	variant.SetAudioBitrate(strconv.Itoa(quality.AudioBitrate) + "k") | ||||
| 	variant.SetVideoScalingWidth(quality.ScaledWidth) | ||||
| 	variant.SetVideoScalingHeight(quality.ScaledHeight) | ||||
| 	variant.SetVideoFramerate(quality.Framerate) | ||||
|  | ||||
| 	return variant | ||||
| } | ||||
|  | ||||
| // NewTranscoder will return a new Transcoder, populated by the config | ||||
| func NewTranscoder() Transcoder { | ||||
| 	transcoder := new(Transcoder) | ||||
|  | ||||
| 	var outputPath string | ||||
| 	if config.Config.S3.Enabled || config.Config.IPFS.Enabled { | ||||
| 		// Segments are not available via the local HTTP server | ||||
| 		outputPath = config.Config.PrivateHLSPath | ||||
| 	} else { | ||||
| 		// Segments are available via the local HTTP server | ||||
| 		outputPath = config.Config.PublicHLSPath | ||||
| 	} | ||||
|  | ||||
| 	transcoder.segmentOutputPath = outputPath | ||||
| 	// Playlists are available via the local HTTP server | ||||
| 	transcoder.playlistOutputPath = config.Config.PublicHLSPath | ||||
|  | ||||
| 	transcoder.input = utils.GetTemporaryPipePath() | ||||
| 	transcoder.segmentLengthSeconds = config.Config.VideoSettings.ChunkLengthInSeconds | ||||
|  | ||||
| 	for index, quality := range config.Config.VideoSettings.StreamQualities { | ||||
| 		variant := getVariantFromConfigQuality(quality, index) | ||||
| 		transcoder.AddVariant(variant) | ||||
| 	} | ||||
|  | ||||
| 	return *transcoder | ||||
| } | ||||
|  | ||||
| // Uses `map` https://www.ffmpeg.org/ffmpeg-all.html#Stream-specifiers-1 https://www.ffmpeg.org/ffmpeg-all.html#Advanced-options | ||||
| func (v *HLSVariant) getVariantString() string { | ||||
| 	variantEncoderCommands := []string{ | ||||
| 		v.getVideoQualityString(), | ||||
| 		v.getAudioQualityString(), | ||||
| 	} | ||||
|  | ||||
| 	if v.videoSize.Width != 0 || v.videoSize.Height != 0 { | ||||
| 		variantEncoderCommands = append(variantEncoderCommands, v.getScalingString()) | ||||
| 	} | ||||
|  | ||||
| 	if v.framerate != 0 { | ||||
| 		variantEncoderCommands = append(variantEncoderCommands, fmt.Sprintf("-r %d", v.framerate)) | ||||
| 		// multiply your output frame rate * 2. For example, if your input is -framerate 30, then use -g 60 | ||||
| 		variantEncoderCommands = append(variantEncoderCommands, "-g "+strconv.Itoa(v.framerate*2)) | ||||
| 		variantEncoderCommands = append(variantEncoderCommands, "-keyint_min "+strconv.Itoa(v.framerate*2)) | ||||
| 	} | ||||
|  | ||||
| 	if v.encoderPreset != "" { | ||||
| 		variantEncoderCommands = append(variantEncoderCommands, fmt.Sprintf("-preset %s", v.encoderPreset)) | ||||
| 	} | ||||
|  | ||||
| 	return strings.Join(variantEncoderCommands, " ") | ||||
| } | ||||
|  | ||||
| // Get the command flags for the variants | ||||
| func (t *Transcoder) getVariantsString() string { | ||||
| 	var variantsCommandFlags = "" | ||||
| 	var variantsStreamMaps = " -var_stream_map \"" | ||||
|  | ||||
| 	for _, variant := range t.variants { | ||||
| 		variantsCommandFlags = variantsCommandFlags + " " + variant.getVariantString() | ||||
| 		variantsStreamMaps = variantsStreamMaps + fmt.Sprintf("v:%d,a:%d ", variant.index, variant.index) | ||||
| 	} | ||||
| 	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. | ||||
|  | ||||
| // SetVideoScalingWidth will set the scaled video width of this variant | ||||
| func (v *HLSVariant) SetVideoScalingWidth(width int) { | ||||
| 	v.videoSize.Width = width | ||||
| } | ||||
|  | ||||
| // SetVideoScalingHeight will set the scaled video height of this variant | ||||
| func (v *HLSVariant) SetVideoScalingHeight(height int) { | ||||
| 	v.videoSize.Height = height | ||||
| } | ||||
|  | ||||
| func (v *HLSVariant) getScalingString() string { | ||||
| 	scalingAlgorithm := "bilinear" | ||||
| 	return fmt.Sprintf("-filter:v:%d \"scale=%s\" -sws_flags %s", v.index, v.videoSize.getString(), scalingAlgorithm) | ||||
| } | ||||
|  | ||||
| // Video Quality | ||||
|  | ||||
| // SetVideoBitrate will set the output bitrate of this variant's video | ||||
| func (v *HLSVariant) SetVideoBitrate(bitrate string) { | ||||
| 	v.videoBitrate = bitrate | ||||
| } | ||||
|  | ||||
| func (v *HLSVariant) getVideoQualityString() string { | ||||
| 	if v.isVideoPassthrough { | ||||
| 		return fmt.Sprintf("-map v:0 -c:v:%d copy", v.index) | ||||
| 	} | ||||
|  | ||||
| 	encoderCodec := "libx264" | ||||
| 	return fmt.Sprintf("-map v:0 -c:v:%d %s -b:v:%d %s", v.index, encoderCodec, v.index, v.videoBitrate) | ||||
| } | ||||
|  | ||||
| // SetVideoFramerate will set the output framerate of this variant's video | ||||
| 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 | ||||
| } | ||||
|  | ||||
| // Audio Quality | ||||
|  | ||||
| // SetAudioBitrate will set the output framerate of this variant's audio | ||||
| func (v *HLSVariant) SetAudioBitrate(bitrate string) { | ||||
| 	v.audioBitrate = bitrate | ||||
| } | ||||
|  | ||||
| func (v *HLSVariant) getAudioQualityString() string { | ||||
| 	if v.isAudioPassthrough { | ||||
| 		return fmt.Sprintf("-map a:0 -c:a:%d copy", v.index) | ||||
| 	} | ||||
|  | ||||
| 	encoderCodec := "libfdk_aac" | ||||
| 	return fmt.Sprintf("-map a:0 -c:a:%d %s -profile:a aac_he -b:a:%d %s", v.index, encoderCodec, v.index, v.audioBitrate) | ||||
| } | ||||
|  | ||||
| // AddVariant adds a new HLS variant to include in the output | ||||
| func (t *Transcoder) AddVariant(variant HLSVariant) { | ||||
| 	t.variants = append(t.variants, variant) | ||||
| } | ||||
|  | ||||
| // SetInput sets the input stream on the filesystem | ||||
| func (t *Transcoder) SetInput(input string) { | ||||
| 	t.input = input | ||||
| } | ||||
|  | ||||
| // SetOutputPath sets the root directory that should include playlists and video segments | ||||
| func (t *Transcoder) SetOutputPath(output string) { | ||||
| 	t.segmentOutputPath = output | ||||
| } | ||||
|  | ||||
| // SetHLSPlaylistLength will set the max number of items in a HLS variant's playlist | ||||
| func (t *Transcoder) SetHLSPlaylistLength(length int) { | ||||
| 	t.hlsPlaylistLength = length | ||||
| } | ||||
|  | ||||
| // SetSegmentLength Specifies the number of seconds each segment should be | ||||
| func (t *Transcoder) SetSegmentLength(seconds int) { | ||||
| 	t.segmentLengthSeconds = seconds | ||||
| } | ||||
|  | ||||
| // SetAppendToStream enables appending to the HLS stream instead of overwriting | ||||
| func (t *Transcoder) SetAppendToStream(append bool) { | ||||
| 	t.appendToStream = append | ||||
| } | ||||
| @ -13,7 +13,6 @@ import ( | ||||
| 	"github.com/radovskyb/watcher" | ||||
|  | ||||
| 	"github.com/gabek/owncast/config" | ||||
| 	"github.com/gabek/owncast/core/ffmpeg" | ||||
| 	"github.com/gabek/owncast/models" | ||||
| 	"github.com/gabek/owncast/utils" | ||||
| ) | ||||
| @ -159,5 +158,5 @@ func updateVariantPlaylist(fullPath string) error { | ||||
|  | ||||
| 	playlistString = _storage.GenerateRemotePlaylist(playlistString, variant) | ||||
|  | ||||
| 	return ffmpeg.WritePlaylist(playlistString, path.Join(config.Config.PublicHLSPath, relativePath)) | ||||
| 	return WritePlaylist(playlistString, path.Join(config.Config.PublicHLSPath, relativePath)) | ||||
| } | ||||
|  | ||||
							
								
								
									
										18
									
								
								core/playlist/writer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								core/playlist/writer.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| package playlist | ||||
|  | ||||
| import "os" | ||||
|  | ||||
| //WritePlaylist writes the playlist to disk | ||||
| func WritePlaylist(data string, filePath string) error { | ||||
| 	f, err := os.Create(filePath) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer f.Close() | ||||
|  | ||||
| 	if _, err := f.WriteString(data); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
| @ -76,8 +76,8 @@ func (h *Handler) OnPublish(timestamp uint32, cmd *rtmpmsg.NetStreamPublish) err | ||||
| 	} | ||||
| 	h.flvEnc = enc | ||||
|  | ||||
| 	//TODO: why is this turned into a goroutine? | ||||
| 	go ffmpeg.Start() | ||||
| 	transcoder := ffmpeg.NewTranscoder() | ||||
| 	go transcoder.Start() | ||||
|  | ||||
| 	_isConnected = true | ||||
| 	core.SetStreamAsConnected() | ||||
|  | ||||
| @ -47,7 +47,5 @@ func SetStreamAsDisconnected() { | ||||
| 	_stats.StreamConnected = false | ||||
| 	_stats.LastDisconnectTime = time.Now() | ||||
|  | ||||
| 	if config.Config.EnableOfflineImage { | ||||
| 		ffmpeg.ShowStreamOfflineState() | ||||
| 	} | ||||
| 	ffmpeg.ShowStreamOfflineState() | ||||
| } | ||||
|  | ||||
| @ -39,15 +39,14 @@ build() { | ||||
|  | ||||
|   mkdir -p dist/${NAME} | ||||
|   mkdir -p dist/${NAME}/webroot/static | ||||
|   mkdir -p dist/${NAME}/static | ||||
|  | ||||
|   # Default files | ||||
|   cp config-example.yaml dist/${NAME}/config.yaml | ||||
|   cp webroot/static/content-example.md dist/${NAME}/webroot/static/content.md | ||||
|   cp webroot/img/logo.png dist/${NAME}/static/logo.png | ||||
|  | ||||
|   cp -R webroot/ dist/${NAME}/webroot/ | ||||
|   cp -R doc/ dist/${NAME}/doc/ | ||||
|   cp -R static/ dist/${NAME}/static | ||||
|   cp README.md dist/${NAME} | ||||
|  | ||||
|   env CGO_ENABLED=0 GOOS=$OS GOARCH=$ARCH go build -ldflags "-X main.GitCommit=$GIT_COMMIT -X main.BuildVersion=$VERSION -X main.BuildType=$NAME" -a -o dist/$NAME/owncast | ||||
|  | ||||
										
											Binary file not shown.
										
									
								
							| @ -1,5 +1,5 @@ | ||||
| // const streamURL = '/hls/stream.m3u8'; | ||||
| const streamURL = 'https://goth.land/hls/stream.m3u8'; // Uncomment me to point to remote video | ||||
| const streamURL = '/hls/stream.m3u8'; | ||||
| // const streamURL = 'https://goth.land/hls/stream.m3u8'; // Uncomment me to point to remote video | ||||
|  | ||||
| // style hackings | ||||
| window.VIDEOJS_NO_DYNAMIC_STYLE = true; | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Gabe Kangas
					Gabe Kangas