Initial commit.
This commit is contained in:
185
converter/exported.go
Normal file
185
converter/exported.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
// stdlib
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
// local
|
||||
"github.com/pztrn/ffmpeger/nats"
|
||||
)
|
||||
|
||||
var (
|
||||
// ffmpeg path.
|
||||
ffmpegPath string
|
||||
|
||||
// Tasks queue.
|
||||
tasks []*Task
|
||||
tasksMutex sync.Mutex
|
||||
|
||||
// Currently running tasks.
|
||||
// Reason why this isn't from atomic package is because atomic's
|
||||
// integers (as well as other things) doesn't neccessarily changed
|
||||
// when Add* functions called but we need to make sure that our
|
||||
// running count is precise.
|
||||
// Mutex is here because value will be decremented/incremented from
|
||||
// worker goroutine and read from control goroutine.
|
||||
currentlyRunning int
|
||||
currentlyRunningMutex sync.Mutex
|
||||
|
||||
// Maximum tasks that should be executed concurrently.
|
||||
// No mutex here because it will be accessed from only one place
|
||||
// after initialization.
|
||||
maximumConcurrentTasks int
|
||||
|
||||
// Indicates that we should shutdown working goroutine.
|
||||
shouldShutdown bool
|
||||
shouldShutdownMutex sync.Mutex
|
||||
|
||||
// Indicates that goroutine was successfully shutdown.
|
||||
shuttedDown chan bool
|
||||
)
|
||||
|
||||
// AddTask adds task to processing queue.
|
||||
func AddTask(task *Task) {
|
||||
tasksMutex.Lock()
|
||||
tasks = append(tasks, task)
|
||||
tasksMutex.Unlock()
|
||||
}
|
||||
|
||||
// Initialize initializes package.
|
||||
func Initialize() {
|
||||
log.Println("Initializing converter...")
|
||||
|
||||
tasks = make([]*Task, 0, 64)
|
||||
shuttedDown = make(chan bool, 1)
|
||||
|
||||
flag.IntVar(&maximumConcurrentTasks, "maxconcurrency", 1, "Maximum conversion tasks that should be run concurrently")
|
||||
|
||||
handler := &nats.Handler{
|
||||
Name: "converter",
|
||||
Func: natsMessageHandler,
|
||||
}
|
||||
nats.AddHandler(handler)
|
||||
}
|
||||
|
||||
func natsMessageHandler(data []byte) {
|
||||
t := &Task{}
|
||||
json.Unmarshal(data, t)
|
||||
log.Printf("Received task: %+v\n", t)
|
||||
|
||||
tasksMutex.Lock()
|
||||
tasks = append(tasks, t)
|
||||
tasksMutex.Unlock()
|
||||
}
|
||||
|
||||
// Shutdown sets shutdown flag and waits until shuttedDown channel will
|
||||
// get any message means that shutdown was completed.
|
||||
func Shutdown() {
|
||||
log.Println("Starting converter shutdown...")
|
||||
shouldShutdownMutex.Lock()
|
||||
shouldShutdown = true
|
||||
shouldShutdownMutex.Unlock()
|
||||
|
||||
<-shuttedDown
|
||||
log.Println("Converter shutted down")
|
||||
}
|
||||
|
||||
// Start starts working goroutine.
|
||||
func Start() {
|
||||
log.Println("Starting converter controlling goroutine...")
|
||||
log.Println("Maximum simultaneous tasks to run:", maximumConcurrentTasks)
|
||||
findffmpeg()
|
||||
|
||||
go startReally()
|
||||
}
|
||||
|
||||
// Real start for working goroutine.
|
||||
func startReally() {
|
||||
tick := time.NewTicker(time.Second * 1)
|
||||
for range tick.C {
|
||||
// Check for shutdown.
|
||||
// Boolean values aren't goroutine-safe that's why we create local
|
||||
// copy of package variable.
|
||||
shouldShutdownMutex.Lock()
|
||||
weHaveToShutdown := shouldShutdown
|
||||
shouldShutdownMutex.Unlock()
|
||||
|
||||
if weHaveToShutdown {
|
||||
log.Println("Stopping tasks distribution...")
|
||||
break
|
||||
}
|
||||
|
||||
// Check for tasks available and currently running counts.
|
||||
currentlyRunningMutex.Lock()
|
||||
curRunning := currentlyRunning
|
||||
currentlyRunningMutex.Unlock()
|
||||
|
||||
// Skip iteration if we have maximum tasks launched.
|
||||
if curRunning >= maximumConcurrentTasks {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if we have tasks at all.
|
||||
tasksMutex.Lock()
|
||||
tasksCount := len(tasks)
|
||||
tasksMutex.Unlock()
|
||||
if tasksCount == 0 {
|
||||
log.Println("No tasks to launch")
|
||||
continue
|
||||
}
|
||||
|
||||
// If we're here - we should launch a task! Lets get them.
|
||||
tasksToRunCount := maximumConcurrentTasks - curRunning
|
||||
tasksToRun := make([]*Task, 0, tasksToRunCount)
|
||||
tasksMutex.Lock()
|
||||
// To ensure that our tasks queue will be clean we will copy
|
||||
// queue, clear it and re-add if queue items still be there.
|
||||
tasksQueue := make([]*Task, 0, tasksCount)
|
||||
tasksQueue = append(tasksQueue, tasks...)
|
||||
tasksMutex.Unlock()
|
||||
|
||||
// Get tasks list to launch.
|
||||
for taskID, task := range tasksQueue {
|
||||
if taskID == tasksToRunCount {
|
||||
break
|
||||
}
|
||||
tasksToRun = append(tasksToRun, task)
|
||||
}
|
||||
// Remove tasks that will be launched now.
|
||||
tasksQueue = tasksQueue[tasksToRunCount:]
|
||||
// Re-add remaining tasks to queue.
|
||||
// Note: if another task was added to queue while we compose
|
||||
// our tasks list to launch - it will be executed BEFORE remaining
|
||||
// tasks.
|
||||
tasksMutex.Lock()
|
||||
tasks = append(tasks, tasksQueue...)
|
||||
tasksMutex.Unlock()
|
||||
|
||||
log.Println("Got", len(tasksToRun), "tasks to run")
|
||||
|
||||
// Launch tasks.
|
||||
for _, task := range tasksToRun {
|
||||
go task.Convert()
|
||||
}
|
||||
}
|
||||
|
||||
// Waiting until all child goroutines will also shut down.
|
||||
log.Println("Waiting for all child goroutines to stop...")
|
||||
shutdownTicker := time.NewTicker(time.Millisecond * 500)
|
||||
for range shutdownTicker.C {
|
||||
currentlyRunningMutex.Lock()
|
||||
curRunning := currentlyRunning
|
||||
currentlyRunningMutex.Unlock()
|
||||
|
||||
log.Println("Currently running converter goroutines:", curRunning)
|
||||
if curRunning == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
shuttedDown <- true
|
||||
}
|
36
converter/find_ffmpeg.go
Normal file
36
converter/find_ffmpeg.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
// stdlib
|
||||
"bytes"
|
||||
"log"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func findffmpeg() {
|
||||
// Search for ffmpeg.
|
||||
var err error
|
||||
ffmpegPath, err = exec.LookPath("ffmpeg")
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to find ffmpeg in path:", err.Error())
|
||||
}
|
||||
|
||||
// Get ffmpeg version.
|
||||
stdout := bytes.NewBuffer(nil)
|
||||
ffmpegVersionCmd := exec.Command(ffmpegPath, "-version")
|
||||
ffmpegVersionCmd.Stdout = stdout
|
||||
err1 := ffmpegVersionCmd.Run()
|
||||
if err1 != nil {
|
||||
log.Fatalln("Failed to get ffmpeg version:", err1.Error())
|
||||
}
|
||||
|
||||
stdoutString := stdout.String()
|
||||
if len(stdoutString) == 0 {
|
||||
log.Fatalln("Something weird happened and '" + ffmpegPath + " -version' returns nothing! Check your ffmpeg installation!")
|
||||
}
|
||||
// ffmpeg prints it's version on line 1.
|
||||
ffmpegVersion := strings.Split(stdoutString, " ")[2]
|
||||
|
||||
log.Println("ffmpeg found at", ffmpegPath, "with version", ffmpegVersion)
|
||||
}
|
101
converter/task.go
Normal file
101
converter/task.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
// stdlib
|
||||
"bufio"
|
||||
"log"
|
||||
"os/exec"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Task represents a single task received via NATS.
|
||||
type Task struct {
|
||||
Name string
|
||||
InputFile string
|
||||
OutputFile string
|
||||
|
||||
// Filed in conversion.
|
||||
totalFrames int
|
||||
|
||||
// State information.
|
||||
gotInput bool
|
||||
gotDuration bool
|
||||
|
||||
// File info.
|
||||
duration string
|
||||
}
|
||||
|
||||
// Convert launches conversion procedure. Should be launched in separate
|
||||
// goroutine.
|
||||
func (t *Task) Convert() {
|
||||
log.Printf("Starting conversion task: %+v\n", t)
|
||||
currentlyRunningMutex.Lock()
|
||||
currentlyRunning++
|
||||
currentlyRunningMutex.Unlock()
|
||||
|
||||
defer func() {
|
||||
currentlyRunningMutex.Lock()
|
||||
currentlyRunning--
|
||||
currentlyRunningMutex.Unlock()
|
||||
}()
|
||||
|
||||
ffmpegCmd := exec.Command(ffmpegPath, "-i", t.InputFile, "-c:v", "libx264", "-b:v", "1000k", "-c:a", "aac", "-f", "mp4", t.OutputFile, "-y")
|
||||
stderr, err := ffmpegCmd.StderrPipe()
|
||||
if err != nil {
|
||||
log.Fatalln("Error while preparing to redirect ffmpeg's stderr:", err.Error())
|
||||
}
|
||||
stderrScanner := bufio.NewScanner(stderr)
|
||||
stderrScanner.Split(bufio.ScanWords)
|
||||
|
||||
// We will check state every 500ms.
|
||||
go func() {
|
||||
checkTick := time.NewTicker(time.Millisecond * 500)
|
||||
err1 := ffmpegCmd.Start()
|
||||
if err1 != nil {
|
||||
log.Fatalln("Failed to start ffmpeg:", err1.Error())
|
||||
}
|
||||
|
||||
for range checkTick.C {
|
||||
// Should we shutdown immediately?
|
||||
shouldShutdownMutex.Lock()
|
||||
shouldWeStop := shouldShutdown
|
||||
shouldShutdownMutex.Unlock()
|
||||
|
||||
if shouldWeStop {
|
||||
log.Println("Killing converter goroutine...")
|
||||
err := ffmpegCmd.Process.Kill()
|
||||
if err != nil {
|
||||
log.Println("ERROR: failed to kill ffmpeg process:", err.Error())
|
||||
}
|
||||
ffmpegCmd.Process.Release()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Child ffmpeg process killed")
|
||||
}()
|
||||
|
||||
// Read output.
|
||||
for {
|
||||
// Should we shutdown immediately?
|
||||
shouldShutdownMutex.Lock()
|
||||
shouldWeStop := shouldShutdown
|
||||
shouldShutdownMutex.Unlock()
|
||||
if shouldWeStop {
|
||||
break
|
||||
}
|
||||
|
||||
stderrScanner.Scan()
|
||||
t.workWithOutput(stderrScanner.Text())
|
||||
}
|
||||
|
||||
log.Println("Stopped reading ffmpeg output")
|
||||
}
|
||||
|
||||
// Printing progress for this task.
|
||||
func (t *Task) workWithOutput(output string) {
|
||||
if output == "" {
|
||||
return
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user