mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-21 20:02:22 +02:00
git-subtree-dir: foundations/stream git-subtree-mainline:328a7dc269git-subtree-split:b7b6930b09
183 lines
5.2 KiB
Go
183 lines
5.2 KiB
Go
//
|
|
// Copyright © 2025 Hardcore Engineering Inc.
|
|
//
|
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License. You may
|
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
//
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
package mediaconvert
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/hcengineering/stream/internal/pkg/log"
|
|
"github.com/hcengineering/stream/internal/pkg/manifest"
|
|
"github.com/hcengineering/stream/internal/pkg/profile"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// LogLevel is ffmpeg log level
|
|
type LogLevel string
|
|
|
|
const (
|
|
// LogLevelQuiet is quiet log level
|
|
LogLevelQuiet LogLevel = "quiet"
|
|
// LogLevelPanic is panic log level
|
|
LogLevelPanic LogLevel = "panic"
|
|
// LogLevelFatal is fatal log level
|
|
LogLevelFatal LogLevel = "fatal"
|
|
// LogLevelError is error log level
|
|
LogLevelError LogLevel = "error"
|
|
// LogLevelWarning is warning log level
|
|
LogLevelWarning LogLevel = "warning"
|
|
// LogLevelInfo is info log level
|
|
LogLevelInfo LogLevel = "info"
|
|
// LogLevelVerbose is verbose log level
|
|
LogLevelVerbose LogLevel = "verbose"
|
|
// LogLevelDebug is debug log level
|
|
LogLevelDebug LogLevel = "debug"
|
|
// LogLevelTrace is trace log level
|
|
LogLevelTrace LogLevel = "trace"
|
|
)
|
|
|
|
// Options represents configuration for the ffmpeg command
|
|
type Options struct {
|
|
Input string
|
|
OutputDir string
|
|
LogLevel LogLevel
|
|
Threads int
|
|
UploadID string
|
|
Profiles []profile.VideoProfile
|
|
}
|
|
|
|
func newFfmpegCommand(ctx context.Context, in io.Reader, args []string) (*exec.Cmd, error) {
|
|
if ctx == nil {
|
|
return nil, errors.New("ctx should not be nil")
|
|
}
|
|
|
|
var cmd = exec.CommandContext(ctx, "ffmpeg", args...)
|
|
cmd.Stdin = in
|
|
|
|
var logger = log.FromContext(ctx).With(zap.String("func", "newFFMpegCommand"))
|
|
logger.Debug("prepared command: ", zap.String("cmd", cmd.String()))
|
|
|
|
return cmd, nil
|
|
}
|
|
|
|
func buildCommonCommand(opts *Options) []string {
|
|
var result = []string{
|
|
"-y", // Overwrite output files without asking.
|
|
"-v", string(opts.LogLevel),
|
|
"-err_detect", "ignore_err",
|
|
"-fflags", "+discardcorrupt",
|
|
"-threads", fmt.Sprint(opts.Threads),
|
|
"-i", opts.Input,
|
|
}
|
|
|
|
// If input is a URL, add HTTP specific parameters
|
|
if strings.HasPrefix(opts.Input, "http://") || strings.HasPrefix(opts.Input, "https://") {
|
|
result = append(result,
|
|
"-reconnect", "1",
|
|
"-reconnect_streamed", "1",
|
|
"-reconnect_delay_max", "5",
|
|
)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func buildHLSCommand(profile profile.VideoProfile, opts *Options) []string {
|
|
return []string{
|
|
"-f", "hls",
|
|
"-hls_time", "5",
|
|
// Use HLS flags
|
|
// - split_by_time
|
|
// Allow segments to start on frames other than key frames.
|
|
// This improves behavior on some players when the time between key frames is inconsistent,
|
|
// but may make things worse on others, and can cause some oddities during seeking.
|
|
// This flag should be used with the hls_time option.
|
|
// - temp_file
|
|
// Write segment data to filename.tmp and rename to filename only once the segment is complete.
|
|
"-hls_flags", "split_by_time+temp_file",
|
|
// Do not limit number of HLS segments
|
|
"-hls_list_size", "0",
|
|
"-hls_segment_filename", filepath.Join(opts.OutputDir, opts.UploadID, fmt.Sprintf("%s_%s_%s.ts", opts.UploadID, "%03d", profile.Name)),
|
|
}
|
|
}
|
|
|
|
func buildVideoCommand(profile profile.VideoProfile) []string {
|
|
crf := profile.CRF
|
|
if crf == 0 {
|
|
crf = 23
|
|
}
|
|
command := []string{
|
|
// Transcode only first video and optionally audio stream
|
|
"-map", "0:v:0",
|
|
"-map", "0:a?",
|
|
// Set up codecs
|
|
"-c:a", profile.AudioCodec,
|
|
"-c:v", profile.VideoCodec,
|
|
"-preset", "veryfast",
|
|
"-crf", strconv.Itoa(crf),
|
|
"-g", "60",
|
|
}
|
|
|
|
if profile.VideoCodec != "copy" && profile.Scale {
|
|
command = append(command, "-vf", "scale=-2:"+strconv.Itoa(profile.Height))
|
|
}
|
|
|
|
return command
|
|
}
|
|
|
|
// BuildAudioCommand returns flags for getting the audio from the input
|
|
func BuildAudioCommand(opts *Options) []string {
|
|
var commonPart = buildCommonCommand(opts)
|
|
|
|
return append(commonPart,
|
|
"-vn", "-acodec",
|
|
"copy", filepath.Join(opts.OutputDir, opts.UploadID),
|
|
)
|
|
}
|
|
|
|
// BuildVideoCommand returns ffmpeg command for converting video.
|
|
func BuildVideoCommand(opts *Options) []string {
|
|
if len(opts.Profiles) == 0 {
|
|
return []string{}
|
|
}
|
|
|
|
var command = buildCommonCommand(opts)
|
|
for _, profile := range opts.Profiles {
|
|
command = append(command, buildVideoCommand(profile)...)
|
|
command = append(command, buildHLSCommand(profile, opts)...)
|
|
command = append(command, filepath.Join(opts.OutputDir, opts.UploadID, manifest.PlaylistFileName(opts.UploadID, profile.Name)))
|
|
}
|
|
return command
|
|
}
|
|
|
|
// BuildThumbnailCommand creates a command that creates a thumbnail for the input video
|
|
func BuildThumbnailCommand(opts *Options) []string {
|
|
return append([]string{},
|
|
"-y", // Overwrite output files without asking.
|
|
"-i", opts.Input,
|
|
"-vframes", "1",
|
|
"-update", "1",
|
|
filepath.Join(opts.OutputDir, opts.UploadID, opts.UploadID+".jpg"),
|
|
)
|
|
}
|