Add native support for Windows (#4491)

* Add support for Windows

* Fixes for Windows

* Update unit tests

* Fix ffreport setting

* Add test script equivalents

* Fix fontconfig error in test stream

* Fix thumbnail generator

* Fix lint warnings

* Fix warnings in test stream script

* Implement cross-platform ocTestStream

* Migrate to cross-platform script

* Revert ocTestStream.sh

* Add missing EOL

* Alternative test scripts for non-linux environments

---------

Co-authored-by: Gabe Kangas <gabek@real-ity.com>
This commit is contained in:
Nicholas Kwan
2025-10-14 06:55:13 +08:00
committed by GitHub
parent f30b80d473
commit fd89c6e8f2
13 changed files with 590 additions and 169 deletions

36
contrib/local-test.bat Normal file
View File

@ -0,0 +1,36 @@
@echo off
setlocal enabledelayedexpansion
REM This script will make your local Owncast server available at a public URL.
REM It's particularly useful for testing on mobile devices or want to test
REM activitypub integration.
REM Pass a custom domain as an argument if you have previously set it up at
REM localhost.run. Otherwise, a random hostname will be generated.
REM SET DOMAIN=me.example.com && test\test-local.bat
REM Pass a port number as an argument if you are running Owncast on a different port.
REM By default, it will use port 8080.
REM SET PORT=8080 && test\test-local.bat
REM Set default port if not provided
if "%PORT%"=="" set PORT=8080
set HOST=localhost
echo Checking if web server is running on port %PORT%...
REM Using PowerShell to check if the port is open (equivalent to nc -zv)
powershell -Command "try { $tcpConnection = New-Object System.Net.Sockets.TcpClient; $tcpConnection.Connect('%HOST%', %PORT%); $tcpConnection.Close(); exit 0 } catch { exit 1 }"
if %ERRORLEVEL% equ 0 (
echo Your web server is running on port %PORT%. Good.
) else (
echo Please make sure your Owncast server is running on port %PORT%.
exit /b 1
)
if not "%DOMAIN%"=="" (
echo Attempting to use custom domain: %DOMAIN%
ssh -R "%DOMAIN%":80:localhost:%PORT% localhost.run
) else (
echo Using auto-generated hostname for tunnel.
ssh -R 80:localhost:%PORT% localhost.run
)

350
contrib/ocTestStream.js Normal file
View File

@ -0,0 +1,350 @@
#!/usr/bin/env node
/**
* ocTestStream.js - RTMP test stream utility
*
* Requirements:
* - Node.js
* - ffmpeg (a recent version with loop video support)
* - a Sans family font (for overlay text)
*
* Example: node ocTestStream.js ~/Downloads/*.mp4 rtmp://127.0.0.1/live/abc123
*/
const fs = require('fs');
const path = require('path');
const { spawn, exec } = require('child_process');
const os = require('os');
class OcTestStream {
constructor() {
this.ffmpegExec = null;
this.destinationHost = 'rtmp://127.0.0.1/live/abc123';
this.videoFiles = [];
this.listFile = 'list.txt';
}
/**
* Find ffmpeg executable across different platforms
*/
async findFFmpeg() {
const ffmpegExecs = ['ffmpeg', 'ffmpeg.exe'];
const ffmpegPaths = ['./', '../', ''];
// Try local paths first
for (const execName of ffmpegExecs) {
for (const execPath of ffmpegPaths) {
const fullPath = path.join(execPath, execName);
try {
await this.checkExecutable(fullPath);
this.ffmpegExec = fullPath;
return true;
} catch (e) {
// Continue searching
}
}
}
// Try system PATH
for (const execName of ffmpegExecs) {
try {
await this.checkExecutable(execName);
this.ffmpegExec = execName;
return true;
} catch (e) {
// Continue searching
}
}
return false;
}
/**
* Check if an executable exists and is accessible
*/
checkExecutable(execPath) {
return new Promise((resolve, reject) => {
exec(`"${execPath}" -version`, (error, stdout, stderr) => {
if (error) {
reject(error);
} else {
resolve(stdout);
}
});
});
}
/**
* Get ffmpeg version information
*/
async getFFmpegVersion() {
try {
const output = await this.checkExecutable(this.ffmpegExec);
const versionMatch = output.match(/ffmpeg version ([^\s]+)/);
return versionMatch ? versionMatch[1] : 'unknown';
} catch (e) {
return 'unknown';
}
}
/**
* Get ffmpeg executable path
*/
async getFFmpegPath() {
return new Promise((resolve) => {
const command = os.platform() === 'win32' ? `where "${this.ffmpegExec}"` : `which "${this.ffmpegExec}"`;
exec(command, (error, stdout, stderr) => {
if (error) {
resolve('unknown');
} else {
resolve(stdout.trim().split('\n')[0]);
}
});
});
}
/**
* Parse command line arguments
*/
parseArguments(args) {
if (args.includes('--help')) {
this.showHelp();
return false;
}
// Check if last argument is RTMP URL
const lastArg = args[args.length - 1];
if (lastArg && lastArg.includes('rtmp://')) {
console.log('RTMP server is specified');
this.destinationHost = lastArg;
this.videoFiles = args.slice(0, -1);
} else {
console.log('RTMP server is not specified');
this.videoFiles = args;
}
return true;
}
/**
* Show help information
*/
showHelp() {
console.log('ocTestStream is used for sending pre-recorded or internal test content to an RTMP server.');
console.log('Usage: node ocTestStream.js [VIDEO_FILES] [RTMP_DESTINATION]');
console.log('VIDEO_FILES: path to one or multiple videos for sending to the RTMP server (optional)');
console.log('RTMP_DESTINATION: URL of RTMP server with key (optional; default: rtmp://127.0.0.1/live/abc123)');
}
/**
* Get system font path for overlay text
*/
getSystemFont() {
const platform = os.platform();
if (platform === 'win32') {
// Workaround lack of font config on Windows.
const windir = process.env.WINDIR || 'C:\\Windows';
return path.join(windir, 'fonts', 'arial.ttf').replace(/\\/g, '/').replace(/:/g, '\\\\:');
} else {
// Use default font.
return '';
}
}
/**
* Stream internal test video pattern
*/
streamTestPattern() {
console.log(`Streaming internal test video loop to ${this.destinationHost}`);
console.log('...press ctrl+c to exit');
const font = this.getSystemFont();
const fontParam = font ? `:fontfile=${font}` : '';
const args = [
'-hide_banner', '-loglevel', 'panic', '-nostdin', '-re', '-f', 'lavfi',
'-i', 'testsrc=size=1280x720:rate=60[out0];sine=frequency=400:sample_rate=48000[out1]',
'-vf', `[in]drawtext=fontsize=96:box=1:boxcolor=black@0.75:boxborderw=5${fontParam}:fontcolor=white:x=(w-text_w)/2:y=((h-text_h)/2)+((h-text_h)/-2):text='Owncast Test Stream',drawtext=fontsize=96:box=1:boxcolor=black@0.75:boxborderw=5${fontParam}:fontcolor=white:x=(w-text_w)/2:y=((h-text_h)/2)+((h-text_h)/2):text='%{gmtime\\:%H\\\\\\:%M\\\\\\:%S} UTC'[out]`,
'-nal-hrd', 'cbr',
'-metadata:s:v', 'encoder=test',
'-vcodec', 'libx264',
'-acodec', 'aac',
'-preset', 'veryfast',
'-profile:v', 'baseline',
'-tune', 'zerolatency',
'-bf', '0',
'-g', '0',
'-b:v', '6320k',
'-b:a', '160k',
'-ac', '2',
'-ar', '48000',
'-minrate', '6320k',
'-maxrate', '6320k',
'-bufsize', '6320k',
'-muxrate', '6320k',
'-r', '60',
'-pix_fmt', 'yuv420p',
'-color_range', '1', '-colorspace', '1', '-color_primaries', '1', '-color_trc', '1',
'-flags:v', '+global_header',
'-bsf:v', 'dump_extra',
'-x264-params', 'nal-hrd=cbr:min-keyint=2:keyint=2:scenecut=0:bframes=0',
'-f', 'flv', this.destinationHost
];
this.streamWithArgs(args);
}
/**
* Stream with arguments
*/
streamWithArgs(args) {
const ffmpeg = spawn(this.ffmpegExec, args, { detached: false, stdio: 'inherit' });
ffmpeg.on('error', (err) => {
console.error('Error starting ffmpeg:', err);
process.exit(1);
});
process.on('SIGINT', () => {
if (os.platform() === 'win32') {
// Because ffmpeg can spawn children, on windows we need to use taskkill.
// Using ffmpeg.kill on Windows may result in orphaned processes.
exec(`taskkill /T /PID ${ffmpeg.pid}`);
} else {
ffmpeg.kill('SIGINT');
}
});
process.on('exit', () => {
if (os.platform() === 'win32') {
exec(`taskkill /F /T /PID ${ffmpeg.pid}`);
} else {
ffmpeg.kill('SIGINT');
}
});
}
/**
* Create playlist file for video files
*/
createPlaylist() {
try {
const content = this.videoFiles.map(file => `file '${file}'`).join('\n');
fs.writeFileSync(this.listFile, content);
return true;
} catch (err) {
console.error('Error creating playlist file:', err);
return false;
}
}
/**
* Validate video files exist
*/
validateFiles() {
for (const file of this.videoFiles) {
if (!fs.existsSync(file)) {
console.error(`ERROR: File not found: ${file}`);
return false;
}
}
return true;
}
/**
* Stream video files
*/
streamVideoFiles() {
if (!this.validateFiles()) {
process.exit(1);
}
if (!this.createPlaylist()) {
process.exit(1);
}
console.log(`Streaming a loop of ${this.videoFiles.length} video(s) to ${this.destinationHost}`);
if (this.videoFiles.length > 1) {
console.log('Warning: If these files differ greatly in formats, transitioning from one to another may not always work correctly.');
}
console.log(this.videoFiles.join(' '));
console.log('...press ctrl+c to exit');
const font = this.getSystemFont();
const fontParam = font ? `:fontfile=${font}` : '';
const args = [
'-hide_banner', '-loglevel', 'panic', '-nostdin', '-stream_loop', '-1', '-re', '-f', 'concat',
'-safe', '0',
'-i', this.listFile,
'-vcodec', 'libx264',
'-profile:v', 'high',
'-g', '48',
'-r', '24',
'-sc_threshold', '0',
'-b:v', '1300k',
'-preset', 'veryfast',
'-acodec', 'copy',
'-vf', `drawtext=fontsize=96:box=1:boxcolor=black@0.75:boxborderw=5${fontParam}:fontcolor=white:x=(w-text_w)/2:y=((h-text_h)/2)+((h-text_h)/4):text='%{gmtime\\:%H\\\\\\:%M\\\\\\:%S}'`,
'-f', 'flv', this.destinationHost
];
this.streamWithArgs(args);
}
/**
* Cleanup temporary files
*/
cleanup() {
try {
if (fs.existsSync(this.listFile)) {
fs.unlinkSync(this.listFile);
}
} catch (err) {
console.error('Error during cleanup:', err);
}
}
/**
* Main execution function
*/
async run() {
// Parse command line arguments (skip node and script name)
const args = process.argv.slice(2);
if (!this.parseArguments(args)) {
return;
}
// Find ffmpeg executable
if (!(await this.findFFmpeg())) {
console.error('ERROR: ffmpeg was not found in path or in the current directory! Please install ffmpeg before using this script.');
process.exit(1);
}
// Show ffmpeg information
const version = await this.getFFmpegVersion();
const execPath = await this.getFFmpegPath();
console.log(`ffmpeg executable: ${this.ffmpegExec} (${version})`);
console.log(`ffmpeg path: ${execPath}`);
// Stream based on whether files were provided
if (this.videoFiles.length === 0) {
this.streamTestPattern();
} else {
this.streamVideoFiles();
}
}
}
// Run if called directly
if (require.main === module) {
const stream = new OcTestStream();
stream.run().catch(err => {
console.error('Error:', err);
process.exit(1);
});
}
module.exports = OcTestStream;

View File

@ -104,6 +104,5 @@ func saveOfflineClipToDisk(offlineFilename string) (string, error) {
} }
offlineFilePath := offlineTmpFile.Name() offlineFilePath := offlineTmpFile.Name()
return filepath.Abs(offlineFilePath)
return offlineFilePath, nil
} }

View File

@ -14,12 +14,12 @@ import (
type Codec interface { type Codec interface {
Name() string Name() string
DisplayName() string DisplayName() string
GlobalFlags() string GlobalFlags() []string
PixelFormat() string PixelFormat() string
Scaler() string Scaler() string
ExtraArguments() string ExtraArguments() []string
ExtraFilters() string ExtraFilters() string
VariantFlags(v *HLSVariant) string VariantFlags(v *HLSVariant) []string
GetPresetForLevel(l int) string GetPresetForLevel(l int) string
} }
@ -46,8 +46,8 @@ func (c *Libx264Codec) DisplayName() string {
} }
// GlobalFlags are the global flags used with this codec in the transcoder. // GlobalFlags are the global flags used with this codec in the transcoder.
func (c *Libx264Codec) GlobalFlags() string { func (c *Libx264Codec) GlobalFlags() []string {
return "" return nil
} }
// PixelFormat is the pixel format required for this codec. // PixelFormat is the pixel format required for this codec.
@ -61,10 +61,10 @@ func (c *Libx264Codec) Scaler() string {
} }
// ExtraArguments are the extra arguments used with this codec in the transcoder. // ExtraArguments are the extra arguments used with this codec in the transcoder.
func (c *Libx264Codec) ExtraArguments() string { func (c *Libx264Codec) ExtraArguments() []string {
return strings.Join([]string{ return []string{
"-tune", "zerolatency", // Option used for good for fast encoding and low-latency streaming (always includes iframes in each segment) "-tune", "zerolatency", // Option used for good for fast encoding and low-latency streaming (always includes iframes in each segment)
}, " ") }
} }
// ExtraFilters are the extra filters required for this codec in the transcoder. // ExtraFilters are the extra filters required for this codec in the transcoder.
@ -73,12 +73,13 @@ func (c *Libx264Codec) ExtraFilters() string {
} }
// VariantFlags returns a string representing a single variant processed by this codec. // VariantFlags returns a string representing a single variant processed by this codec.
func (c *Libx264Codec) VariantFlags(v *HLSVariant) string { func (c *Libx264Codec) VariantFlags(v *HLSVariant) []string {
return strings.Join([]string{ return []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("-x264-params:v:%d", v.index),
fmt.Sprintf("-bufsize:v:%d %dk", v.index, v.getBufferSize()), "scenecut=0:open_gop=0", // How often the encoder checks the bitrate in order to meet average/max values
fmt.Sprintf("-profile:v:%d %s", v.index, "high"), // Encoding profile fmt.Sprintf("-bufsize:v:%d", v.index), fmt.Sprintf("%dk", v.getBufferSize()),
}, " ") fmt.Sprintf("-profile:v:%d", v.index), "high", // Encoding profile
}
} }
// GetPresetForLevel returns the string preset for this codec given an integer level. // GetPresetForLevel returns the string preset for this codec given an integer level.
@ -115,8 +116,8 @@ func (c *OmxCodec) DisplayName() string {
} }
// GlobalFlags are the global flags used with this codec in the transcoder. // GlobalFlags are the global flags used with this codec in the transcoder.
func (c *OmxCodec) GlobalFlags() string { func (c *OmxCodec) GlobalFlags() []string {
return "" return nil
} }
// PixelFormat is the pixel format required for this codec. // PixelFormat is the pixel format required for this codec.
@ -130,10 +131,10 @@ func (c *OmxCodec) Scaler() string {
} }
// ExtraArguments are the extra arguments used with this codec in the transcoder. // ExtraArguments are the extra arguments used with this codec in the transcoder.
func (c *OmxCodec) ExtraArguments() string { func (c *OmxCodec) ExtraArguments() []string {
return strings.Join([]string{ return []string{
"-tune", "zerolatency", // Option used for good for fast encoding and low-latency streaming (always includes iframes in each segment) "-tune", "zerolatency", // Option used for good for fast encoding and low-latency streaming (always includes iframes in each segment)
}, " ") }
} }
// ExtraFilters are the extra filters required for this codec in the transcoder. // ExtraFilters are the extra filters required for this codec in the transcoder.
@ -142,8 +143,8 @@ func (c *OmxCodec) ExtraFilters() string {
} }
// VariantFlags returns a string representing a single variant processed by this codec. // VariantFlags returns a string representing a single variant processed by this codec.
func (c *OmxCodec) VariantFlags(v *HLSVariant) string { func (c *OmxCodec) VariantFlags(v *HLSVariant) []string {
return "" return nil
} }
// GetPresetForLevel returns the string preset for this codec given an integer level. // GetPresetForLevel returns the string preset for this codec given an integer level.
@ -180,14 +181,14 @@ func (c *VaapiCodec) DisplayName() string {
} }
// GlobalFlags are the global flags used with this codec in the transcoder. // GlobalFlags are the global flags used with this codec in the transcoder.
func (c *VaapiCodec) GlobalFlags() string { func (c *VaapiCodec) GlobalFlags() []string {
flags := []string{ flags := []string{
"-hwaccel", "vaapi", "-hwaccel", "vaapi",
"-hwaccel_output_format", "vaapi", "-hwaccel_output_format", "vaapi",
"-vaapi_device", "/dev/dri/renderD128", "-vaapi_device", "/dev/dri/renderD128",
} }
return strings.Join(flags, " ") return flags
} }
// PixelFormat is the pixel format required for this codec. // PixelFormat is the pixel format required for this codec.
@ -206,13 +207,13 @@ func (c *VaapiCodec) ExtraFilters() string {
} }
// ExtraArguments are the extra arguments used with this codec in the transcoder. // ExtraArguments are the extra arguments used with this codec in the transcoder.
func (c *VaapiCodec) ExtraArguments() string { func (c *VaapiCodec) ExtraArguments() []string {
return "" return nil
} }
// VariantFlags returns a string representing a single variant processed by this codec. // VariantFlags returns a string representing a single variant processed by this codec.
func (c *VaapiCodec) VariantFlags(v *HLSVariant) string { func (c *VaapiCodec) VariantFlags(v *HLSVariant) []string {
return "" return nil
} }
// GetPresetForLevel returns the string preset for this codec given an integer level. // GetPresetForLevel returns the string preset for this codec given an integer level.
@ -249,12 +250,12 @@ func (c *NvencCodec) DisplayName() string {
} }
// GlobalFlags are the global flags used with this codec in the transcoder. // GlobalFlags are the global flags used with this codec in the transcoder.
func (c *NvencCodec) GlobalFlags() string { func (c *NvencCodec) GlobalFlags() []string {
flags := []string{ flags := []string{
"-hwaccel", "cuda", "-hwaccel", "cuda",
} }
return strings.Join(flags, " ") return flags
} }
// PixelFormat is the pixel format required for this codec. // PixelFormat is the pixel format required for this codec.
@ -268,8 +269,8 @@ func (c *NvencCodec) Scaler() string {
} }
// ExtraArguments are the extra arguments used with this codec in the transcoder. // ExtraArguments are the extra arguments used with this codec in the transcoder.
func (c *NvencCodec) ExtraArguments() string { func (c *NvencCodec) ExtraArguments() []string {
return "" return nil
} }
// ExtraFilters are the extra filters required for this codec in the transcoder. // ExtraFilters are the extra filters required for this codec in the transcoder.
@ -278,9 +279,11 @@ func (c *NvencCodec) ExtraFilters() string {
} }
// VariantFlags returns a string representing a single variant processed by this codec. // VariantFlags returns a string representing a single variant processed by this codec.
func (c *NvencCodec) VariantFlags(v *HLSVariant) string { func (c *NvencCodec) VariantFlags(v *HLSVariant) []string {
tuning := "ll" // low latency tuning := "ll" // low latency
return fmt.Sprintf("-tune:v:%d %s", v.index, tuning) return []string{
fmt.Sprintf("-tune:v:%d", v.index), tuning,
}
} }
// GetPresetForLevel returns the string preset for this codec given an integer level. // GetPresetForLevel returns the string preset for this codec given an integer level.
@ -317,13 +320,13 @@ func (c *QuicksyncCodec) DisplayName() string {
} }
// GlobalFlags are the global flags used with this codec in the transcoder. // GlobalFlags are the global flags used with this codec in the transcoder.
func (c *QuicksyncCodec) GlobalFlags() string { func (c *QuicksyncCodec) GlobalFlags() []string {
flags := []string{ flags := []string{
"-init_hw_device", "qsv=hw", "-init_hw_device", "qsv=hw",
"-filter_hw_device", "hw", "-filter_hw_device", "hw",
} }
return strings.Join(flags, " ") return flags
} }
// PixelFormat is the pixel format required for this codec. // PixelFormat is the pixel format required for this codec.
@ -337,8 +340,8 @@ func (c *QuicksyncCodec) Scaler() string {
} }
// ExtraArguments are the extra arguments used with this codec in the transcoder. // ExtraArguments are the extra arguments used with this codec in the transcoder.
func (c *QuicksyncCodec) ExtraArguments() string { func (c *QuicksyncCodec) ExtraArguments() []string {
return "" return nil
} }
// ExtraFilters are the extra filters required for this codec in the transcoder. // ExtraFilters are the extra filters required for this codec in the transcoder.
@ -347,8 +350,8 @@ func (c *QuicksyncCodec) ExtraFilters() string {
} }
// VariantFlags returns a string representing a single variant processed by this codec. // VariantFlags returns a string representing a single variant processed by this codec.
func (c *QuicksyncCodec) VariantFlags(v *HLSVariant) string { func (c *QuicksyncCodec) VariantFlags(v *HLSVariant) []string {
return "" return nil
} }
// GetPresetForLevel returns the string preset for this codec given an integer level. // GetPresetForLevel returns the string preset for this codec given an integer level.
@ -385,8 +388,8 @@ func (c *Video4Linux) DisplayName() string {
} }
// GlobalFlags are the global flags used with this codec in the transcoder. // GlobalFlags are the global flags used with this codec in the transcoder.
func (c *Video4Linux) GlobalFlags() string { func (c *Video4Linux) GlobalFlags() []string {
return "" return nil
} }
// PixelFormat is the pixel format required for this codec. // PixelFormat is the pixel format required for this codec.
@ -400,8 +403,8 @@ func (c *Video4Linux) Scaler() string {
} }
// ExtraArguments are the extra arguments used with this codec in the transcoder. // ExtraArguments are the extra arguments used with this codec in the transcoder.
func (c *Video4Linux) ExtraArguments() string { func (c *Video4Linux) ExtraArguments() []string {
return "" return nil
} }
// ExtraFilters are the extra filters required for this codec in the transcoder. // ExtraFilters are the extra filters required for this codec in the transcoder.
@ -410,8 +413,8 @@ func (c *Video4Linux) ExtraFilters() string {
} }
// VariantFlags returns a string representing a single variant processed by this codec. // VariantFlags returns a string representing a single variant processed by this codec.
func (c *Video4Linux) VariantFlags(v *HLSVariant) string { func (c *Video4Linux) VariantFlags(v *HLSVariant) []string {
return "" return nil
} }
// GetPresetForLevel returns the string preset for this codec given an integer level. // GetPresetForLevel returns the string preset for this codec given an integer level.
@ -447,10 +450,8 @@ func (c *VideoToolboxCodec) DisplayName() string {
} }
// GlobalFlags are the global flags used with this codec in the transcoder. // GlobalFlags are the global flags used with this codec in the transcoder.
func (c *VideoToolboxCodec) GlobalFlags() string { func (c *VideoToolboxCodec) GlobalFlags() []string {
var flags []string return nil
return strings.Join(flags, " ")
} }
// PixelFormat is the pixel format required for this codec. // PixelFormat is the pixel format required for this codec.
@ -469,23 +470,17 @@ func (c *VideoToolboxCodec) ExtraFilters() string {
} }
// ExtraArguments are the extra arguments used with this codec in the transcoder. // ExtraArguments are the extra arguments used with this codec in the transcoder.
func (c *VideoToolboxCodec) ExtraArguments() string { func (c *VideoToolboxCodec) ExtraArguments() []string {
return "" return nil
} }
// VariantFlags returns a string representing a single variant processed by this codec. // VariantFlags returns a string representing a single variant processed by this codec.
func (c *VideoToolboxCodec) VariantFlags(v *HLSVariant) string { func (c *VideoToolboxCodec) VariantFlags(v *HLSVariant) []string {
arguments := []string{ if v.cpuUsageLevel >= 3 {
"-realtime true", return nil
"-realtime true",
"-realtime true",
} }
if v.cpuUsageLevel >= len(arguments) { return []string{"-realtime", "true"}
return ""
}
return arguments[v.cpuUsageLevel]
} }
// GetPresetForLevel returns the string preset for this codec given an integer level. // GetPresetForLevel returns the string preset for this codec given an integer level.

View File

@ -5,7 +5,6 @@ import (
"os/exec" "os/exec"
"path" "path"
"strconv" "strconv"
"strings"
"time" "time"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -94,18 +93,16 @@ func fireThumbnailGenerator(segmentPath string, variantIndex int) error {
outputFileTemp := path.Join(config.TempDir, "tempthumbnail.jpg") outputFileTemp := path.Join(config.TempDir, "tempthumbnail.jpg")
thumbnailCmdFlags := []string{ thumbnailCmdFlags := []string{
ffmpegPath, "-y", // Overwrite file
"-y", // Overwrite file "-threads", "1", // Low priority processing
"-threads 1", // Low priority processing "-t", "1", // Pull from frame 1
"-t 1", // Pull from frame 1
"-i", mostRecentFile, // Input "-i", mostRecentFile, // Input
"-f image2", // format "-f", "image2", // format
"-vframes 1", // Single frame "-vframes", "1", // Single frame
outputFileTemp, outputFileTemp,
} }
ffmpegCmd := strings.Join(thumbnailCmdFlags, " ") if _, err := exec.Command(ffmpegPath, thumbnailCmdFlags...).Output(); err != nil {
if _, err := exec.Command("sh", "-c", ffmpegCmd).Output(); err != nil {
return err return err
} }
@ -126,17 +123,15 @@ func makeAnimatedGifPreview(sourceFile string, outputFile string) {
// Filter is pulled from https://engineering.giphy.com/how-to-make-gifs-with-ffmpeg/ // Filter is pulled from https://engineering.giphy.com/how-to-make-gifs-with-ffmpeg/
animatedGifFlags := []string{ animatedGifFlags := []string{
ffmpegPath, "-y", // Overwrite file
"-y", // Overwrite file "-threads", "1", // Low priority processing
"-threads 1", // Low priority processing
"-i", sourceFile, // Input "-i", sourceFile, // Input
"-t 1", // Output is one second in length "-t", "1", // Output is one second in length
"-filter_complex", "\"[0:v] fps=8,scale=w=480:h=-1:flags=lanczos,split [a][b];[a] palettegen=stats_mode=full [p];[b][p] paletteuse=new=1\"", "-filter_complex", "[0:v] fps=8,scale=w=480:h=-1:flags=lanczos,split [a][b];[a] palettegen=stats_mode=full [p];[b][p] paletteuse=new=1",
outputFileTemp, outputFileTemp,
} }
ffmpegCmd := strings.Join(animatedGifFlags, " ") if _, err := exec.Command(ffmpegPath, animatedGifFlags...).Output(); err != nil {
if _, err := exec.Command("sh", "-c", ffmpegCmd).Output(); err != nil {
log.Errorln(err) log.Errorln(err)
// rename temp file // rename temp file
} else if err := utils.Move(outputFileTemp, outputFile); err != nil { } else if err := utils.Move(outputFileTemp, outputFile); err != nil {

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io" "io"
"os/exec" "os/exec"
"runtime"
"strconv" "strconv"
"strings" "strings"
@ -20,6 +21,16 @@ import (
var _commandExec *exec.Cmd var _commandExec *exec.Cmd
type execInfo struct {
binPath string
command []string
environ []string
}
func (e *execInfo) String() string {
return strings.Join(e.environ, " ") + " " + e.binPath + " " + strings.Join(e.command, " ")
}
// Transcoder is a single instance of a video transcoder. // Transcoder is a single instance of a video transcoder.
type Transcoder struct { type Transcoder struct {
codec Codec codec Codec
@ -114,17 +125,19 @@ func (t *Transcoder) Stop() {
func (t *Transcoder) Start(shouldLog bool) { func (t *Transcoder) Start(shouldLog bool) {
_lastTranscoderLogMessage = "" _lastTranscoderLogMessage = ""
command := t.getString() flags := t.getFlags()
if shouldLog { if shouldLog {
log.Infof("Processing video using codec %s with %d output qualities configured.", t.codec.DisplayName(), len(t.variants)) log.Infof("Processing video using codec %s with %d output qualities configured.", t.codec.DisplayName(), len(t.variants))
} }
createVariantDirectories() createVariantDirectories()
command := flags.String()
if config.EnableDebugFeatures { if config.EnableDebugFeatures {
log.Println(command) log.Println(command)
} }
_commandExec = exec.Command("sh", "-c", command) _commandExec = exec.Command(flags.binPath, flags.command...) // nolint: gosec
_commandExec.Env = flags.environ
if t.stdin != nil { if t.stdin != nil {
_commandExec.Stdin = t.stdin _commandExec.Stdin = t.stdin
@ -168,7 +181,12 @@ func (t *Transcoder) SetIsEvent(isEvent bool) {
t.isEvent = isEvent t.isEvent = isEvent
} }
func (t *Transcoder) getString() string { func (t *Transcoder) GetString() string {
ffmpegFlags := t.getFlags()
return ffmpegFlags.String()
}
func (t *Transcoder) getFlags() *execInfo {
port := t.internalListenerPort port := t.internalListenerPort
localListenerAddress := "http://127.0.0.1:" + port localListenerAddress := "http://127.0.0.1:" + port
@ -185,41 +203,56 @@ func (t *Transcoder) getString() string {
t.segmentIdentifier = shortid.MustGenerate() t.segmentIdentifier = shortid.MustGenerate()
} }
hlsEventString := "" hlsEventString := make([]string, 0, 2)
if t.isEvent { if t.isEvent {
hlsEventString = "-hls_playlist_type event" hlsEventString = append(hlsEventString, "-hls_playlist_type", "event")
} else { } else {
// Don't let the transcoder close the playlist. We do it manually. // Don't let the transcoder close the playlist. We do it manually.
hlsOptionFlags = append(hlsOptionFlags, "omit_endlist") hlsOptionFlags = append(hlsOptionFlags, "omit_endlist")
} }
hlsOptionsString := "" hlsOptionsString := make([]string, 0, len(hlsOptionFlags))
if len(hlsOptionFlags) > 0 { if len(hlsOptionFlags) > 0 {
hlsOptionsString = "-hls_flags " + strings.Join(hlsOptionFlags, "+") hlsOptionsString = append(hlsOptionsString, "-hls_flags", strings.Join(hlsOptionFlags, "+"))
}
logPath := logging.GetTranscoderLogFilePath()
reportEnv := fmt.Sprintf(`FFREPORT=file=%s:level=32`, logPath)
if runtime.GOOS == "windows" {
logPath = strings.ReplaceAll(logPath, "\\", "/")
reportEnv = fmt.Sprintf(`FFREPORT=file=%s:level=32`, logPath)
}
environ := []string{
reportEnv,
} }
ffmpegFlags := []string{ ffmpegFlags := []string{
fmt.Sprintf(`FFREPORT=file="%s":level=32`, logging.GetTranscoderLogFilePath()),
t.ffmpegPath,
"-hide_banner", "-hide_banner",
"-loglevel warning", "-loglevel", "warning",
t.codec.GlobalFlags(), }
"-fflags +genpts", // Generate presentation time stamp if missing ffmpegFlags = append(ffmpegFlags, t.codec.GlobalFlags()...)
"-flags +cgop", // Force closed GOPs ffmpegFlags = append(ffmpegFlags, []string{
"-i ", t.input, "-fflags", "+genpts", // Generate presentation time stamp if missing
"-flags", "+cgop", // Force closed GOPs
t.getVariantsString(), "-i", t.input,
}...)
ffmpegFlags = append(ffmpegFlags, t.getVariantsString()...)
ffmpegFlags = append(ffmpegFlags, []string{
// HLS Output // HLS Output
"-f", "hls", "-f", "hls",
"-hls_time", strconv.Itoa(t.currentLatencyLevel.SecondsPerSegment), // Length of each segment "-hls_time", strconv.Itoa(t.currentLatencyLevel.SecondsPerSegment), // Length of each segment
"-hls_list_size", strconv.Itoa(t.currentLatencyLevel.SegmentCount), // Max # in variant playlist "-hls_list_size", strconv.Itoa(t.currentLatencyLevel.SegmentCount), // Max # in variant playlist
hlsOptionsString, }...)
hlsEventString, ffmpegFlags = append(ffmpegFlags, hlsOptionsString...)
ffmpegFlags = append(ffmpegFlags, hlsEventString...)
ffmpegFlags = append(ffmpegFlags, []string{
"-segment_format_options", "mpegts_flags=mpegts_copyts=1", "-segment_format_options", "mpegts_flags=mpegts_copyts=1",
}...)
ffmpegFlags = append(ffmpegFlags, t.codec.ExtraArguments()...)
ffmpegFlags = append(ffmpegFlags, []string{
// Video settings // Video settings
t.codec.ExtraArguments(),
"-pix_fmt", t.codec.PixelFormat(), "-pix_fmt", t.codec.PixelFormat(),
"-sc_threshold", "0", // Disable scene change detection for creating segments "-sc_threshold", "0", // Disable scene change detection for creating segments
@ -229,12 +262,16 @@ func (t *Transcoder) getString() string {
"-hls_segment_filename", localListenerAddress + "/%v/stream-" + t.segmentIdentifier + "-%d.ts", // Send HLS segments back to us over HTTP "-hls_segment_filename", localListenerAddress + "/%v/stream-" + t.segmentIdentifier + "-%d.ts", // Send HLS segments back to us over HTTP
"-max_muxing_queue_size", "400", // Workaround for Too many packets error: https://trac.ffmpeg.org/ticket/6375?cversion=0 "-max_muxing_queue_size", "400", // Workaround for Too many packets error: https://trac.ffmpeg.org/ticket/6375?cversion=0
"-method PUT", // HLS results sent back to us will be over PUTs "-method", "PUT", // HLS results sent back to us will be over PUTs
localListenerAddress + "/%v/stream.m3u8", // Send HLS playlists back to us over HTTP localListenerAddress + "/%v/stream.m3u8", // Send HLS playlists back to us over HTTP
} }...)
return strings.Join(ffmpegFlags, " ") return &execInfo{
binPath: t.ffmpegPath,
command: ffmpegFlags,
environ: environ,
}
} }
func getVariantFromConfigQuality(quality models.StreamOutputVariant, index int) HLSVariant { func getVariantFromConfigQuality(quality models.StreamOutputVariant, index int) HLSVariant {
@ -298,11 +335,9 @@ func NewTranscoder() *Transcoder {
} }
// Uses `map` https://www.ffmpeg.org/ffmpeg-all.html#Stream-specifiers-1 https://www.ffmpeg.org/ffmpeg-all.html#Advanced-options // 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(t *Transcoder) string { func (v *HLSVariant) getVariantString(t *Transcoder) []string {
variantEncoderCommands := []string{ variantEncoderCommands := v.getVideoQualityString(t)
v.getVideoQualityString(t), variantEncoderCommands = append(variantEncoderCommands, v.getAudioQualityString()...)
v.getAudioQualityString(),
}
if (v.videoSize.Width != 0 || v.videoSize.Height != 0) && !v.isVideoPassthrough { if (v.videoSize.Width != 0 || v.videoSize.Height != 0) && !v.isVideoPassthrough {
// Order here matters, you must scale before changing hardware formats // Order here matters, you must scale before changing hardware formats
@ -313,33 +348,36 @@ func (v *HLSVariant) getVariantString(t *Transcoder) string {
filters = append(filters, t.codec.ExtraFilters()) filters = append(filters, t.codec.ExtraFilters())
} }
scalingAlgorithm := "bilinear" scalingAlgorithm := "bilinear"
filterString := fmt.Sprintf("-sws_flags %s -filter:v:%d \"%s\"", scalingAlgorithm, v.index, strings.Join(filters, ",")) variantEncoderCommands = append(variantEncoderCommands, []string{
variantEncoderCommands = append(variantEncoderCommands, filterString) "-sws_flags", scalingAlgorithm,
"-filter:v:" + strconv.Itoa(v.index), strings.Join(filters, ","),
}...)
} else if t.codec.ExtraFilters() != "" && !v.isVideoPassthrough { } else if t.codec.ExtraFilters() != "" && !v.isVideoPassthrough {
filterString := fmt.Sprintf("-filter:v:%d \"%s\"", v.index, t.codec.ExtraFilters()) variantEncoderCommands = append(variantEncoderCommands, []string{
variantEncoderCommands = append(variantEncoderCommands, filterString) "-filter:v:" + strconv.Itoa(v.index), t.codec.ExtraFilters(),
}...)
} }
preset := t.codec.GetPresetForLevel(v.cpuUsageLevel) preset := t.codec.GetPresetForLevel(v.cpuUsageLevel)
if preset != "" { if preset != "" {
variantEncoderCommands = append(variantEncoderCommands, fmt.Sprintf("-preset %s", preset)) variantEncoderCommands = append(variantEncoderCommands, []string{
"-preset", preset,
}...)
} }
return strings.Join(variantEncoderCommands, " ") return variantEncoderCommands
} }
// Get the command flags for the variants. // Get the command flags for the variants.
func (t *Transcoder) getVariantsString() string { func (t *Transcoder) getVariantsString() []string {
variantsCommandFlags := "" var variantsCommandFlags []string
variantsStreamMaps := " -var_stream_map \"" streamMap := make([]string, 0, len(t.variants))
for _, variant := range t.variants { for _, variant := range t.variants {
variantsCommandFlags = variantsCommandFlags + " " + variant.getVariantString(t) variantsCommandFlags = append(variantsCommandFlags, variant.getVariantString(t)...)
singleVariantMap := fmt.Sprintf("v:%d,a:%d ", variant.index, variant.index) streamMap = append(streamMap, fmt.Sprintf("v:%d,a:%d", variant.index, variant.index))
variantsStreamMaps += singleVariantMap
} }
variantsCommandFlags = variantsCommandFlags + " " + variantsStreamMaps + "\"" variantsCommandFlags = append(variantsCommandFlags, "-var_stream_map", strings.Join(streamMap, " "))
return variantsCommandFlags return variantsCommandFlags
} }
@ -372,24 +410,26 @@ func (v *HLSVariant) SetVideoBitrate(bitrate int) {
v.videoBitrate = bitrate v.videoBitrate = bitrate
} }
func (v *HLSVariant) getVideoQualityString(t *Transcoder) string { func (v *HLSVariant) getVideoQualityString(t *Transcoder) []string {
if v.isVideoPassthrough { if v.isVideoPassthrough {
return fmt.Sprintf("-map v:0 -c:v:%d copy", v.index) return []string{
"-map", "v:0", fmt.Sprintf("-c:v:%d", v.index), "copy",
}
} }
gop := v.framerate * t.currentLatencyLevel.SecondsPerSegment // force an i-frame every segment gop := v.framerate * t.currentLatencyLevel.SecondsPerSegment // force an i-frame every segment
cmd := []string{ cmd := []string{
"-map v:0", "-map", "v:0",
fmt.Sprintf("-c:v:%d %s", v.index, t.codec.Name()), // Video codec used for this variant fmt.Sprintf("-c:v:%d", 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("-b:v:%d", v.index), fmt.Sprintf("%dk", 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("-maxrate:v:%d", v.index), fmt.Sprintf("%dk", 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("-g:v:%d", v.index), fmt.Sprintf("%d", 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("-keyint_min:v:%d", v.index), fmt.Sprintf("%d", gop), // minimum i-keyframe interval
fmt.Sprintf("-r:v:%d %d", v.index, v.framerate), fmt.Sprintf("-r:v:%d", v.index), fmt.Sprintf("%d", v.framerate),
t.codec.VariantFlags(v),
} }
cmd = append(cmd, t.codec.VariantFlags(v)...)
return strings.Join(cmd, " ") return cmd
} }
// SetVideoFramerate will set the output framerate of this variant's video. // SetVideoFramerate will set the output framerate of this variant's video.
@ -409,14 +449,20 @@ func (v *HLSVariant) SetAudioBitrate(bitrate string) {
v.audioBitrate = bitrate v.audioBitrate = bitrate
} }
func (v *HLSVariant) getAudioQualityString() string { func (v *HLSVariant) getAudioQualityString() []string {
if v.isAudioPassthrough { if v.isAudioPassthrough {
return fmt.Sprintf("-map a:0? -c:a:%d copy", v.index) return []string{
"-map", "a:0?", fmt.Sprintf("-c:a:%d", v.index), "copy",
}
} }
// libfdk_aac is not a part of every ffmpeg install, so use "aac" instead // libfdk_aac is not a part of every ffmpeg install, so use "aac" instead
encoderCodec := "aac" encoderCodec := "aac"
return fmt.Sprintf("-map a:0? -c:a:%d %s -b:a:%d %s", v.index, encoderCodec, v.index, v.audioBitrate) return []string{
"-map", "a:0?",
fmt.Sprintf("-c:a:%d", v.index), encoderCodec,
fmt.Sprintf("-b:a:%d", v.index), v.audioBitrate,
}
} }
// AddVariant adds a new HLS variant to include in the output. // AddVariant adds a new HLS variant to include in the output.

View File

@ -39,10 +39,10 @@ func TestFFmpegNvencCommand(t *testing.T) {
variant3.isVideoPassthrough = true variant3.isVideoPassthrough = true
transcoder.AddVariant(variant3) transcoder.AddVariant(variant3)
cmd := transcoder.getString() cmd := transcoder.GetString()
expectedLogPath := filepath.Join("data", "logs", "transcoder.log") expectedLogPath := "data/logs/transcoder.log"
expected := `FFREPORT=file="` + expectedLogPath + `":level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -hwaccel cuda -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_nvenc -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -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 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -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 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -pix_fmt yuv420p -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdoieGg-%d.ts -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8` expected := `FFREPORT=file=` + expectedLogPath + `:level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -hwaccel cuda -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_nvenc -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -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 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -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 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -pix_fmt yuv420p -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdoieGg-%d.ts -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8`
if cmd != expected { if cmd != expected {
t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected) t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)

View File

@ -39,10 +39,10 @@ func TestFFmpegOmxCommand(t *testing.T) {
variant3.isVideoPassthrough = true variant3.isVideoPassthrough = true
transcoder.AddVariant(variant3) transcoder.AddVariant(variant3)
cmd := transcoder.getString() cmd := transcoder.GetString()
expectedLogPath := filepath.Join("data", "logs", "transcoder.log") expectedLogPath := "data/logs/transcoder.log"
expected := `FFREPORT=file="` + expectedLogPath + `":level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_omx -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 h264_omx -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -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 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -tune zerolatency -pix_fmt yuv420p -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdFsdfzGg-%d.ts -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8` expected := `FFREPORT=file=` + expectedLogPath + `:level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_omx -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 h264_omx -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -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 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -tune zerolatency -pix_fmt yuv420p -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdFsdfzGg-%d.ts -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8`
if cmd != expected { if cmd != expected {
t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected) t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)

View File

@ -39,10 +39,10 @@ func TestFFmpegQuicksyncCommand(t *testing.T) {
variant3.isVideoPassthrough = true variant3.isVideoPassthrough = true
transcoder.AddVariant(variant3) transcoder.AddVariant(variant3)
cmd := transcoder.getString() cmd := transcoder.GetString()
expectedLogPath := filepath.Join("data", "logs", "transcoder.log") expectedLogPath := "data/logs/transcoder.log"
expected := `FFREPORT=file="` + expectedLogPath + `":level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -init_hw_device qsv=hw -filter_hw_device hw -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_qsv -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -map a:0? -c:a:0 copy -filter:v:0 "hwupload=extra_hw_frames=64,format=qsv" -preset medium -map v:0 -c:v:1 h264_qsv -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -map a:0? -c:a:1 copy -filter:v:1 "hwupload=extra_hw_frames=64,format=qsv" -preset veryslow -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset veryfast -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2 " -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -pix_fmt qsv -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdofFGg-%d.ts -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8` expected := `FFREPORT=file=` + expectedLogPath + `:level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -init_hw_device qsv=hw -filter_hw_device hw -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_qsv -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -map a:0? -c:a:0 copy -filter:v:0 hwupload=extra_hw_frames=64,format=qsv -preset medium -map v:0 -c:v:1 h264_qsv -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -map a:0? -c:a:1 copy -filter:v:1 hwupload=extra_hw_frames=64,format=qsv -preset veryslow -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset veryfast -var_stream_map v:0,a:0 v:1,a:1 v:2,a:2 -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -pix_fmt qsv -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdofFGg-%d.ts -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8`
if cmd != expected { if cmd != expected {
t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected) t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)

View File

@ -39,10 +39,10 @@ func TestFFmpegVaapiCommand(t *testing.T) {
variant3.isVideoPassthrough = true variant3.isVideoPassthrough = true
transcoder.AddVariant(variant3) transcoder.AddVariant(variant3)
cmd := transcoder.getString() cmd := transcoder.GetString()
expectedLogPath := filepath.Join("data", "logs", "transcoder.log") expectedLogPath := "data/logs/transcoder.log"
expected := `FFREPORT=file="` + expectedLogPath + `":level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -hwaccel vaapi -hwaccel_output_format vaapi -vaapi_device /dev/dri/renderD128 -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_vaapi -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -map a:0? -c:a:0 copy -filter:v:0 "hwupload=extra_hw_frames=64,format=vaapi" -preset veryfast -map v:0 -c:v:1 h264_vaapi -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -map a:0? -c:a:1 copy -filter:v:1 "hwupload=extra_hw_frames=64,format=vaapi" -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 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -pix_fmt vaapi -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdofFGg-%d.ts -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8` expected := `FFREPORT=file=` + expectedLogPath + `:level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -hwaccel vaapi -hwaccel_output_format vaapi -vaapi_device /dev/dri/renderD128 -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_vaapi -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -map a:0? -c:a:0 copy -filter:v:0 hwupload=extra_hw_frames=64,format=vaapi -preset veryfast -map v:0 -c:v:1 h264_vaapi -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -map a:0? -c:a:1 copy -filter:v:1 hwupload=extra_hw_frames=64,format=vaapi -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 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -pix_fmt vaapi -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdofFGg-%d.ts -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8`
if cmd != expected { if cmd != expected {
t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected) t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)

View File

@ -39,10 +39,10 @@ func TestFFmpegVideoToolboxCommand(t *testing.T) {
variant3.isVideoPassthrough = true variant3.isVideoPassthrough = true
transcoder.AddVariant(variant3) transcoder.AddVariant(variant3)
cmd := transcoder.getString() cmd := transcoder.GetString()
expectedLogPath := filepath.Join("data", "logs", "transcoder.log") expectedLogPath := "data/logs/transcoder.log"
expected := `FFREPORT=file="` + expectedLogPath + `":level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_videotoolbox -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -realtime true -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 h264_videotoolbox -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -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 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -pix_fmt nv12 -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdFsdfzGg-%d.ts -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8` expected := `FFREPORT=file=` + expectedLogPath + `:level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_videotoolbox -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -realtime true -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 h264_videotoolbox -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -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 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -pix_fmt nv12 -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdFsdfzGg-%d.ts -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8`
if cmd != expected { if cmd != expected {
t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected) t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)

View File

@ -39,10 +39,10 @@ func TestFFmpegx264Command(t *testing.T) {
variant3.isVideoPassthrough = true variant3.isVideoPassthrough = true
transcoder.AddVariant(variant3) transcoder.AddVariant(variant3)
cmd := transcoder.getString() cmd := transcoder.GetString()
expectedLogPath := filepath.Join("data", "logs", "transcoder.log") expectedLogPath := "data/logs/transcoder.log"
expected := `FFREPORT=file="` + expectedLogPath + `":level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 libx264 -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -x264-params:v:0 "scenecut=0:open_gop=0" -bufsize:v:0 1088k -profile:v:0 high -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 libx264 -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -x264-params:v:1 "scenecut=0:open_gop=0" -bufsize:v:1 3572k -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 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -tune zerolatency -pix_fmt yuv420p -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdofFGg-%d.ts -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8` expected := `FFREPORT=file=` + expectedLogPath + `:level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 libx264 -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -x264-params:v:0 scenecut=0:open_gop=0 -bufsize:v:0 1088k -profile:v:0 high -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 libx264 -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -x264-params:v:1 scenecut=0:open_gop=0 -bufsize:v:1 3572k -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 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -tune zerolatency -pix_fmt yuv420p -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdofFGg-%d.ts -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8`
if cmd != expected { if cmd != expected {
t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected) t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)

View File

@ -13,6 +13,7 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"regexp" "regexp"
"runtime"
"strings" "strings"
"time" "time"
@ -38,7 +39,7 @@ func DoesFileExists(name string) bool {
// GetRelativePathFromAbsolutePath gets the relative path from the provided absolute path. // GetRelativePathFromAbsolutePath gets the relative path from the provided absolute path.
func GetRelativePathFromAbsolutePath(path string) string { func GetRelativePathFromAbsolutePath(path string) string {
pathComponents := strings.Split(path, "/") pathComponents := strings.Split(path, string(os.PathSeparator))
variant := pathComponents[len(pathComponents)-2] variant := pathComponents[len(pathComponents)-2]
file := pathComponents[len(pathComponents)-1] file := pathComponents[len(pathComponents)-1]
@ -47,7 +48,7 @@ func GetRelativePathFromAbsolutePath(path string) string {
// GetIndexFromFilePath is a utility that will return the index/key/variant name in a full path. // GetIndexFromFilePath is a utility that will return the index/key/variant name in a full path.
func GetIndexFromFilePath(path string) string { func GetIndexFromFilePath(path string) string {
pathComponents := strings.Split(path, "/") pathComponents := strings.Split(path, string(os.PathSeparator))
variant := pathComponents[len(pathComponents)-2] variant := pathComponents[len(pathComponents)-2]
return variant return variant
@ -259,22 +260,18 @@ func ValidatedFfmpegPath(ffmpegPath string) string {
log.Warnln(ffmpegPath, "is an invalid path to ffmpeg will try to use a copy in your path, if possible") log.Warnln(ffmpegPath, "is an invalid path to ffmpeg will try to use a copy in your path, if possible")
} }
// First look to see if ffmpeg is in the current working directory // Look for ffmpeg in the system path or in the current working directory.
localCopy := "./ffmpeg" ffmpegPath, err := exec.LookPath("ffmpeg")
hasLocalCopyError := VerifyFFMpegPath(localCopy) if ffmpegPath == "" || (err != nil && !errors.Is(err, exec.ErrDot)) || VerifyFFMpegPath(ffmpegPath) != nil {
if hasLocalCopyError == nil {
// No error, so all is good. Use the local copy.
return localCopy
}
cmd := exec.Command("which", "ffmpeg")
out, err := cmd.CombinedOutput()
if err != nil {
log.Fatalln("Unable to locate ffmpeg. Either install it globally on your system or put the ffmpeg binary in the same directory as Owncast. The binary must be named ffmpeg.") log.Fatalln("Unable to locate ffmpeg. Either install it globally on your system or put the ffmpeg binary in the same directory as Owncast. The binary must be named ffmpeg.")
} }
path := strings.TrimSpace(string(out)) // Resolve to an absolute path.
return path absPath, err := filepath.Abs(ffmpegPath)
if err != nil {
return ffmpegPath
}
return absPath
} }
// VerifyFFMpegPath verifies that the path exists, is a file, and is executable. // VerifyFFMpegPath verifies that the path exists, is a file, and is executable.
@ -293,9 +290,12 @@ func VerifyFFMpegPath(path string) error {
return errors.New("ffmpeg path can not be a folder") return errors.New("ffmpeg path can not be a folder")
} }
mode := stat.Mode() mode := stat.Mode().Perm()
// source: https://stackoverflow.com/a/60128480 // source: https://stackoverflow.com/a/60128480
if mode&0o111 == 0 { // On Windows, Perm() omits the executable bit, only check on Unix-like systems.
// https://github.com/golang/go/issues/41809
if runtime.GOOS != "windows" && mode&0o111 == 0 {
return errors.New("ffmpeg path is not executable") return errors.New("ffmpeg path is not executable")
} }