ElJoker63 commited on
Commit ·
eddd40a
1
Parent(s): 92d60cf
fix: cancelacion forzosa inmediata de FFmpeg y tracking robusto por PID
Browse files- internal/ffmpeg/ffmpeg.go +36 -2
- main.go +51 -29
internal/ffmpeg/ffmpeg.go
CHANGED
|
@@ -270,8 +270,6 @@ func wmOverlayExpr(pos string) string {
|
|
| 270 |
}
|
| 271 |
|
| 272 |
// Compress ejecuta la compresión usando FFmpeg y reporta el progreso en vivo.
|
| 273 |
-
// wmImagePath es la ruta local a un logo de marca de agua (opcional); si se
|
| 274 |
-
// indica, tiene prioridad sobre la marca de agua de texto.
|
| 275 |
func Compress(
|
| 276 |
ctx context.Context,
|
| 277 |
inputPath, outputPath string,
|
|
@@ -279,6 +277,19 @@ func Compress(
|
|
| 279 |
wmImagePath string,
|
| 280 |
totalDuration float64,
|
| 281 |
onProgress func(p protocol.TaskProgress),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
) error {
|
| 283 |
settings = SanitizeSettings(settings)
|
| 284 |
|
|
@@ -388,6 +399,13 @@ func Compress(
|
|
| 388 |
)
|
| 389 |
|
| 390 |
cmd := exec.CommandContext(ctx, "ffmpeg", args...)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
|
| 392 |
stderr, err := cmd.StderrPipe()
|
| 393 |
if err != nil {
|
|
@@ -398,6 +416,22 @@ func Compress(
|
|
| 398 |
return fmt.Errorf("error iniciando ffmpeg: %w", err)
|
| 399 |
}
|
| 400 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
scanner := bufio.NewScanner(stderr)
|
| 402 |
// FFmpeg usa retorno de carro '\r' para actualizar la línea de estado
|
| 403 |
scanner.Split(scanCRLFOrCR)
|
|
|
|
| 270 |
}
|
| 271 |
|
| 272 |
// Compress ejecuta la compresión usando FFmpeg y reporta el progreso en vivo.
|
|
|
|
|
|
|
| 273 |
func Compress(
|
| 274 |
ctx context.Context,
|
| 275 |
inputPath, outputPath string,
|
|
|
|
| 277 |
wmImagePath string,
|
| 278 |
totalDuration float64,
|
| 279 |
onProgress func(p protocol.TaskProgress),
|
| 280 |
+
) error {
|
| 281 |
+
return CompressWithCmd(ctx, inputPath, outputPath, settings, wmImagePath, totalDuration, nil, onProgress)
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
// CompressWithCmd ejecuta la compresión y permite capturar el puntero a *exec.Cmd para terminación forzosa inmediata
|
| 285 |
+
func CompressWithCmd(
|
| 286 |
+
ctx context.Context,
|
| 287 |
+
inputPath, outputPath string,
|
| 288 |
+
settings protocol.VideoSettings,
|
| 289 |
+
wmImagePath string,
|
| 290 |
+
totalDuration float64,
|
| 291 |
+
onCmd func(cmd *exec.Cmd),
|
| 292 |
+
onProgress func(p protocol.TaskProgress),
|
| 293 |
) error {
|
| 294 |
settings = SanitizeSettings(settings)
|
| 295 |
|
|
|
|
| 399 |
)
|
| 400 |
|
| 401 |
cmd := exec.CommandContext(ctx, "ffmpeg", args...)
|
| 402 |
+
cmd.Cancel = func() error {
|
| 403 |
+
if cmd.Process != nil {
|
| 404 |
+
return cmd.Process.Kill()
|
| 405 |
+
}
|
| 406 |
+
return nil
|
| 407 |
+
}
|
| 408 |
+
cmd.WaitDelay = 1 * time.Second
|
| 409 |
|
| 410 |
stderr, err := cmd.StderrPipe()
|
| 411 |
if err != nil {
|
|
|
|
| 416 |
return fmt.Errorf("error iniciando ffmpeg: %w", err)
|
| 417 |
}
|
| 418 |
|
| 419 |
+
if onCmd != nil {
|
| 420 |
+
onCmd(cmd)
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
stopWatch := make(chan struct{})
|
| 424 |
+
defer close(stopWatch)
|
| 425 |
+
go func() {
|
| 426 |
+
select {
|
| 427 |
+
case <-ctx.Done():
|
| 428 |
+
if cmd.Process != nil {
|
| 429 |
+
_ = cmd.Process.Kill()
|
| 430 |
+
}
|
| 431 |
+
case <-stopWatch:
|
| 432 |
+
}
|
| 433 |
+
}()
|
| 434 |
+
|
| 435 |
scanner := bufio.NewScanner(stderr)
|
| 436 |
// FFmpeg usa retorno de carro '\r' para actualizar la línea de estado
|
| 437 |
scanner.Split(scanCRLFOrCR)
|
main.go
CHANGED
|
@@ -12,6 +12,7 @@ import (
|
|
| 12 |
"net/http"
|
| 13 |
"net/url"
|
| 14 |
"os"
|
|
|
|
| 15 |
"os/signal"
|
| 16 |
"path/filepath"
|
| 17 |
"runtime"
|
|
@@ -42,6 +43,7 @@ type CurrentJob struct {
|
|
| 42 |
FileName string
|
| 43 |
UploadURL string
|
| 44 |
Cancel context.CancelFunc
|
|
|
|
| 45 |
Done bool
|
| 46 |
}
|
| 47 |
|
|
@@ -53,7 +55,7 @@ type Worker struct {
|
|
| 53 |
|
| 54 |
conn *websocket.Conn
|
| 55 |
connMu sync.Mutex
|
| 56 |
-
|
| 57 |
jobMu sync.RWMutex
|
| 58 |
|
| 59 |
connected bool
|
|
@@ -118,6 +120,7 @@ func main() {
|
|
| 118 |
managerURL: managerURL,
|
| 119 |
secret: secret,
|
| 120 |
workDir: workDir,
|
|
|
|
| 121 |
}
|
| 122 |
|
| 123 |
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
@@ -285,15 +288,15 @@ func (w *Worker) run(ctx context.Context) error {
|
|
| 285 |
// 1. Enviar Handshake (incluyendo cualquier tarea activa si reconecta tras reinicio del Manager)
|
| 286 |
w.jobMu.RLock()
|
| 287 |
var activeTasks []protocol.TaskState
|
| 288 |
-
|
| 289 |
status := "converting"
|
| 290 |
-
if
|
| 291 |
status = "completed"
|
| 292 |
}
|
| 293 |
activeTasks = append(activeTasks, protocol.TaskState{
|
| 294 |
-
TaskID:
|
| 295 |
Status: status,
|
| 296 |
-
UploadURL:
|
| 297 |
})
|
| 298 |
}
|
| 299 |
w.jobMu.RUnlock()
|
|
@@ -359,19 +362,37 @@ func (w *Worker) run(ctx context.Context) error {
|
|
| 359 |
case protocol.TypeTaskAssign:
|
| 360 |
var task protocol.TaskAssign
|
| 361 |
if err := json.Unmarshal(msg.Payload, &task); err == nil {
|
| 362 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
}
|
| 364 |
|
| 365 |
case protocol.TypeTaskCancel:
|
| 366 |
var cancel protocol.TaskCancel
|
| 367 |
if err := json.Unmarshal(msg.Payload, &cancel); err == nil {
|
| 368 |
w.jobMu.Lock()
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
|
|
|
| 373 |
}
|
| 374 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
}
|
| 376 |
w.jobMu.Unlock()
|
| 377 |
}
|
|
@@ -379,25 +400,16 @@ func (w *Worker) run(ctx context.Context) error {
|
|
| 379 |
}
|
| 380 |
}
|
| 381 |
|
| 382 |
-
func (w *Worker) executeTask(
|
| 383 |
log.Printf("📥 Nueva tarea recibida: %s (Archivo: %s)", task.TaskID, task.FileName)
|
| 384 |
|
| 385 |
-
taskCtx, cancel := context.WithCancel(parentCtx)
|
| 386 |
-
w.jobMu.Lock()
|
| 387 |
-
w.currentJob = &CurrentJob{
|
| 388 |
-
TaskID: task.TaskID,
|
| 389 |
-
FileName: task.FileName,
|
| 390 |
-
UploadURL: task.UploadURL,
|
| 391 |
-
Cancel: cancel,
|
| 392 |
-
Done: false,
|
| 393 |
-
}
|
| 394 |
-
w.jobMu.Unlock()
|
| 395 |
-
|
| 396 |
defer func() {
|
| 397 |
w.jobMu.Lock()
|
| 398 |
-
w.
|
| 399 |
w.jobMu.Unlock()
|
| 400 |
-
|
|
|
|
|
|
|
| 401 |
}()
|
| 402 |
|
| 403 |
ext := filepath.Ext(task.FileName)
|
|
@@ -482,21 +494,31 @@ func (w *Worker) executeTask(parentCtx context.Context, task protocol.TaskAssign
|
|
| 482 |
log.Printf("[%s] Iniciando compresión con FFmpeg (Duración: %.1fs)...", task.TaskID, duration)
|
| 483 |
startTime := time.Now()
|
| 484 |
|
| 485 |
-
err = ffmpeg.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 486 |
p.TaskID = task.TaskID
|
| 487 |
_ = w.sendMessage(protocol.TypeTaskProgress, p)
|
| 488 |
})
|
| 489 |
|
| 490 |
if err != nil {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 491 |
log.Printf("[%s] Error en compresión: %v", task.TaskID, err)
|
| 492 |
w.reportFailed(task.TaskID, fmt.Sprintf("falló compresión: %v", err))
|
| 493 |
return
|
| 494 |
}
|
| 495 |
|
| 496 |
w.jobMu.Lock()
|
| 497 |
-
|
| 498 |
-
w.currentJob.Done = true
|
| 499 |
-
}
|
| 500 |
w.jobMu.Unlock()
|
| 501 |
|
| 502 |
outStat, err := os.Stat(outputPath)
|
|
|
|
| 12 |
"net/http"
|
| 13 |
"net/url"
|
| 14 |
"os"
|
| 15 |
+
"os/exec"
|
| 16 |
"os/signal"
|
| 17 |
"path/filepath"
|
| 18 |
"runtime"
|
|
|
|
| 43 |
FileName string
|
| 44 |
UploadURL string
|
| 45 |
Cancel context.CancelFunc
|
| 46 |
+
Cmd *exec.Cmd
|
| 47 |
Done bool
|
| 48 |
}
|
| 49 |
|
|
|
|
| 55 |
|
| 56 |
conn *websocket.Conn
|
| 57 |
connMu sync.Mutex
|
| 58 |
+
activeJobs map[string]*CurrentJob
|
| 59 |
jobMu sync.RWMutex
|
| 60 |
|
| 61 |
connected bool
|
|
|
|
| 120 |
managerURL: managerURL,
|
| 121 |
secret: secret,
|
| 122 |
workDir: workDir,
|
| 123 |
+
activeJobs: make(map[string]*CurrentJob),
|
| 124 |
}
|
| 125 |
|
| 126 |
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
|
|
| 288 |
// 1. Enviar Handshake (incluyendo cualquier tarea activa si reconecta tras reinicio del Manager)
|
| 289 |
w.jobMu.RLock()
|
| 290 |
var activeTasks []protocol.TaskState
|
| 291 |
+
for _, j := range w.activeJobs {
|
| 292 |
status := "converting"
|
| 293 |
+
if j.Done {
|
| 294 |
status = "completed"
|
| 295 |
}
|
| 296 |
activeTasks = append(activeTasks, protocol.TaskState{
|
| 297 |
+
TaskID: j.TaskID,
|
| 298 |
Status: status,
|
| 299 |
+
UploadURL: j.UploadURL,
|
| 300 |
})
|
| 301 |
}
|
| 302 |
w.jobMu.RUnlock()
|
|
|
|
| 362 |
case protocol.TypeTaskAssign:
|
| 363 |
var task protocol.TaskAssign
|
| 364 |
if err := json.Unmarshal(msg.Payload, &task); err == nil {
|
| 365 |
+
taskCtx, cancel := context.WithCancel(ctx)
|
| 366 |
+
job := &CurrentJob{
|
| 367 |
+
TaskID: task.TaskID,
|
| 368 |
+
FileName: task.FileName,
|
| 369 |
+
UploadURL: task.UploadURL,
|
| 370 |
+
Cancel: cancel,
|
| 371 |
+
}
|
| 372 |
+
w.jobMu.Lock()
|
| 373 |
+
w.activeJobs[task.TaskID] = job
|
| 374 |
+
w.jobMu.Unlock()
|
| 375 |
+
|
| 376 |
+
go w.executeTask(taskCtx, job, task)
|
| 377 |
}
|
| 378 |
|
| 379 |
case protocol.TypeTaskCancel:
|
| 380 |
var cancel protocol.TaskCancel
|
| 381 |
if err := json.Unmarshal(msg.Payload, &cancel); err == nil {
|
| 382 |
w.jobMu.Lock()
|
| 383 |
+
job, exists := w.activeJobs[cancel.TaskID]
|
| 384 |
+
if exists {
|
| 385 |
+
log.Printf("⛔ [Worker] Cancelando tarea %s...", cancel.TaskID)
|
| 386 |
+
if job.Cancel != nil {
|
| 387 |
+
job.Cancel()
|
| 388 |
}
|
| 389 |
+
if job.Cmd != nil && job.Cmd.Process != nil {
|
| 390 |
+
log.Printf("💀 [Worker] Matando proceso FFmpeg PID %d de tarea %s...", job.Cmd.Process.Pid, cancel.TaskID)
|
| 391 |
+
_ = job.Cmd.Process.Kill()
|
| 392 |
+
}
|
| 393 |
+
delete(w.activeJobs, cancel.TaskID)
|
| 394 |
+
} else {
|
| 395 |
+
log.Printf("⚠️ [Worker] Tarea %s no encontrada en activeJobs para cancelar", cancel.TaskID)
|
| 396 |
}
|
| 397 |
w.jobMu.Unlock()
|
| 398 |
}
|
|
|
|
| 400 |
}
|
| 401 |
}
|
| 402 |
|
| 403 |
+
func (w *Worker) executeTask(taskCtx context.Context, job *CurrentJob, task protocol.TaskAssign) {
|
| 404 |
log.Printf("📥 Nueva tarea recibida: %s (Archivo: %s)", task.TaskID, task.FileName)
|
| 405 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
defer func() {
|
| 407 |
w.jobMu.Lock()
|
| 408 |
+
delete(w.activeJobs, task.TaskID)
|
| 409 |
w.jobMu.Unlock()
|
| 410 |
+
if job.Cancel != nil {
|
| 411 |
+
job.Cancel()
|
| 412 |
+
}
|
| 413 |
}()
|
| 414 |
|
| 415 |
ext := filepath.Ext(task.FileName)
|
|
|
|
| 494 |
log.Printf("[%s] Iniciando compresión con FFmpeg (Duración: %.1fs)...", task.TaskID, duration)
|
| 495 |
startTime := time.Now()
|
| 496 |
|
| 497 |
+
err = ffmpeg.CompressWithCmd(taskCtx, inputPath, outputPath, task.Settings, wmImagePath, duration, func(cmd *exec.Cmd) {
|
| 498 |
+
w.jobMu.Lock()
|
| 499 |
+
job.Cmd = cmd
|
| 500 |
+
w.jobMu.Unlock()
|
| 501 |
+
if cmd.Process != nil {
|
| 502 |
+
log.Printf("[%s] Proceso FFmpeg iniciado con PID %d", task.TaskID, cmd.Process.Pid)
|
| 503 |
+
}
|
| 504 |
+
}, func(p protocol.TaskProgress) {
|
| 505 |
p.TaskID = task.TaskID
|
| 506 |
_ = w.sendMessage(protocol.TypeTaskProgress, p)
|
| 507 |
})
|
| 508 |
|
| 509 |
if err != nil {
|
| 510 |
+
if taskCtx.Err() != nil {
|
| 511 |
+
log.Printf("[%s] Compresión cancelada por solicitud del usuario/manager.", task.TaskID)
|
| 512 |
+
w.reportFailed(task.TaskID, "compresión cancelada")
|
| 513 |
+
return
|
| 514 |
+
}
|
| 515 |
log.Printf("[%s] Error en compresión: %v", task.TaskID, err)
|
| 516 |
w.reportFailed(task.TaskID, fmt.Sprintf("falló compresión: %v", err))
|
| 517 |
return
|
| 518 |
}
|
| 519 |
|
| 520 |
w.jobMu.Lock()
|
| 521 |
+
job.Done = true
|
|
|
|
|
|
|
| 522 |
w.jobMu.Unlock()
|
| 523 |
|
| 524 |
outStat, err := os.Stat(outputPath)
|