ElJoker63 commited on
Commit
ea02ff4
·
1 Parent(s): 0b0f00e

feat: soporte para marca de agua de imagen/logo y posiciones customizadas

Browse files
Files changed (3) hide show
  1. internal/ffmpeg/ffmpeg.go +68 -20
  2. internal/protocol/messages.go +10 -0
  3. main.go +43 -1
internal/ffmpeg/ffmpeg.go CHANGED
@@ -224,36 +224,88 @@ func SanitizeSettings(s protocol.VideoSettings) protocol.VideoSettings {
224
  return s
225
  }
226
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  func Compress(
228
  ctx context.Context,
229
  inputPath, outputPath string,
230
  settings protocol.VideoSettings,
 
231
  totalDuration float64,
232
  onProgress func(p protocol.TaskProgress),
233
  ) error {
234
  settings = SanitizeSettings(settings)
235
 
236
- var vfParts []string
237
- vfParts = append(vfParts, BuildScaleFilter(settings.Resolution))
238
-
239
- if settings.Watermark != "" {
240
- fontPath := findFontFile()
241
- if fontPath != "" {
242
- vfParts = append(vfParts, fmt.Sprintf("drawtext=fontfile='%s':text='%s':x=w-tw-10:y=10:fontsize=20:fontcolor=white:box=1:boxcolor=black@0.4:boxborderw=4", escapeFontPath(fontPath), settings.Watermark))
243
- } else {
244
- vfParts = append(vfParts, fmt.Sprintf("drawtext=font='Sans':text='%s':x=w-tw-10:y=10:fontsize=20:fontcolor=white", settings.Watermark))
245
- }
246
- }
247
-
248
- vfFilter := strings.Join(vfParts, ",")
249
-
250
  args := []string{
251
  "-y",
252
  "-i", inputPath,
253
  }
254
 
255
- // Mapear video y audio
256
- args = append(args, "-map", "0:v:0", "-map", "0:a?")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
 
258
  // Preservar subtítulos de texto si existen en el contenedor original (mkv/mp4)
259
  textSubStreams := GetTextSubtitleStreams(ctx, inputPath)
@@ -264,10 +316,6 @@ func Compress(
264
  args = append(args, "-c:s", "mov_text")
265
  }
266
 
267
- if vfFilter != "" {
268
- args = append(args, "-vf", vfFilter)
269
- }
270
-
271
  codec := settings.Codec
272
  if codec == "" {
273
  codec = "libx264"
 
224
  return s
225
  }
226
 
227
+ // wmTextPosition devuelve las expresiones x/y de drawtext para la posición elegida
228
+ // ("tr" arriba-derecha, "tl" arriba-izquierda, "br" abajo-derecha, "bl" abajo-izquierda).
229
+ func wmTextPosition(pos string) (x, y string) {
230
+ switch strings.ToLower(strings.TrimSpace(pos)) {
231
+ case "tl":
232
+ return "10", "10"
233
+ case "br":
234
+ return "w-tw-10", "h-th-10"
235
+ case "bl":
236
+ return "10", "h-th-10"
237
+ default: // "tr" y cualquier valor vacío/desconocido
238
+ return "w-tw-10", "10"
239
+ }
240
+ }
241
+
242
+ // wmOverlayExpr devuelve la expresión del filtro overlay para la marca de agua de imagen.
243
+ func wmOverlayExpr(pos string) string {
244
+ switch strings.ToLower(strings.TrimSpace(pos)) {
245
+ case "tl":
246
+ return "10:10"
247
+ case "br":
248
+ return "W-w-10:H-h-10"
249
+ case "bl":
250
+ return "10:H-h-10"
251
+ default:
252
+ return "W-w-10:10"
253
+ }
254
+ }
255
+
256
+ // Compress ejecuta la compresión usando FFmpeg y reporta el progreso en vivo.
257
+ // wmImagePath es la ruta local a un logo de marca de agua (opcional); si se
258
+ // indica, tiene prioridad sobre la marca de agua de texto.
259
  func Compress(
260
  ctx context.Context,
261
  inputPath, outputPath string,
262
  settings protocol.VideoSettings,
263
+ wmImagePath string,
264
  totalDuration float64,
265
  onProgress func(p protocol.TaskProgress),
266
  ) error {
267
  settings = SanitizeSettings(settings)
268
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  args := []string{
270
  "-y",
271
  "-i", inputPath,
272
  }
273
 
274
+ useImageWM := wmImagePath != ""
275
+ if useImageWM {
276
+ args = append(args, "-i", wmImagePath)
277
+ }
278
+
279
+ if useImageWM {
280
+ // Cadena con filter_complex: escala/pad del video principal, logo
281
+ // escalado a máx. 150px de ancho y overlay en la posición elegida.
282
+ chain := "[0:v]" + BuildScaleFilter(settings.Resolution) + "[vbase];" +
283
+ "[1:v]scale='if(gt(iw,150),150,iw)':-1[wm];" +
284
+ "[vbase][wm]overlay=" + wmOverlayExpr(settings.WatermarkPos) + "[v]"
285
+ args = append(args,
286
+ "-filter_complex", chain,
287
+ "-map", "[v]", "-map", "0:a?",
288
+ )
289
+ } else {
290
+ var vfParts []string
291
+ vfParts = append(vfParts, BuildScaleFilter(settings.Resolution))
292
+
293
+ if settings.Watermark != "" {
294
+ x, y := wmTextPosition(settings.WatermarkPos)
295
+ fontPath := findFontFile()
296
+ if fontPath != "" {
297
+ vfParts = append(vfParts, fmt.Sprintf("drawtext=fontfile='%s':text='%s':x=%s:y=%s:fontsize=20:fontcolor=white:box=1:boxcolor=black@0.4:boxborderw=4", escapeFontPath(fontPath), settings.Watermark, x, y))
298
+ } else {
299
+ vfParts = append(vfParts, fmt.Sprintf("drawtext=font='Sans':text='%s':x=%s:y=%s:fontsize=20:fontcolor=white", settings.Watermark, x, y))
300
+ }
301
+ }
302
+
303
+ vfFilter := strings.Join(vfParts, ",")
304
+ args = append(args, "-map", "0:v:0", "-map", "0:a?")
305
+ if vfFilter != "" {
306
+ args = append(args, "-vf", vfFilter)
307
+ }
308
+ }
309
 
310
  // Preservar subtítulos de texto si existen en el contenedor original (mkv/mp4)
311
  textSubStreams := GetTextSubtitleStreams(ctx, inputPath)
 
316
  args = append(args, "-c:s", "mov_text")
317
  }
318
 
 
 
 
 
319
  codec := settings.Codec
320
  if codec == "" {
321
  codec = "libx264"
internal/protocol/messages.go CHANGED
@@ -24,6 +24,13 @@ type VideoSettings struct {
24
  Preset string `json:"preset"`
25
  Codec string `json:"codec"`
26
  Watermark string `json:"watermark"`
 
 
 
 
 
 
 
27
  }
28
 
29
  // WSMessage es el contenedor genérico para los mensajes que viajan por WebSocket
@@ -62,6 +69,9 @@ type TaskAssign struct {
62
  DownloadURL string `json:"download_url"`
63
  UploadURL string `json:"upload_url"`
64
  Settings VideoSettings `json:"settings"`
 
 
 
65
  }
66
 
67
  // TaskProgress es el reporte en vivo que el Worker envía mientras FFmpeg procesa
 
24
  Preset string `json:"preset"`
25
  Codec string `json:"codec"`
26
  Watermark string `json:"watermark"`
27
+ // WatermarkPos: posición de la marca de agua ("tr", "tl", "br", "bl"; vacío = tr)
28
+ WatermarkPos string `json:"watermark_pos,omitempty"`
29
+ // WatermarkImageFileID: referencia al logo en Telegram (modo imagen;
30
+ // si está presente, tiene prioridad sobre el texto)
31
+ WatermarkImageFileID string `json:"watermark_image_file_id,omitempty"`
32
+ // ThumbnailFileID: referencia a la miniatura personalizada en Telegram
33
+ ThumbnailFileID string `json:"thumbnail_file_id,omitempty"`
34
  }
35
 
36
  // WSMessage es el contenedor genérico para los mensajes que viajan por WebSocket
 
69
  DownloadURL string `json:"download_url"`
70
  UploadURL string `json:"upload_url"`
71
  Settings VideoSettings `json:"settings"`
72
+ // WatermarkImageURL: de dónde descargar el logo si la marca de agua es imagen
73
+ // (vacío si la tarea no usa marca de imagen)
74
+ WatermarkImageURL string `json:"watermark_image_url,omitempty"`
75
  }
76
 
77
  // TaskProgress es el reporte en vivo que el Worker envía mientras FFmpeg procesa
main.go CHANGED
@@ -454,6 +454,19 @@ func (w *Worker) executeTask(parentCtx context.Context, task protocol.TaskAssign
454
  originalSize = inStat.Size()
455
  }
456
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
  // 2. Obtener duración del video con ffprobe
458
  duration, err := ffmpeg.GetDuration(taskCtx, inputPath)
459
  if err != nil {
@@ -465,7 +478,7 @@ func (w *Worker) executeTask(parentCtx context.Context, task protocol.TaskAssign
465
  log.Printf("[%s] Iniciando compresión con FFmpeg (Duración: %.1fs)...", task.TaskID, duration)
466
  startTime := time.Now()
467
 
468
- err = ffmpeg.Compress(taskCtx, inputPath, outputPath, task.Settings, duration, func(p protocol.TaskProgress) {
469
  p.TaskID = task.TaskID
470
  _ = w.sendMessage(protocol.TypeTaskProgress, p)
471
  })
@@ -560,6 +573,35 @@ func (w *Worker) executeTask(parentCtx context.Context, task protocol.TaskAssign
560
  log.Printf("[%s] ✅ Tarea finalizada y confirmada.", task.TaskID)
561
  }
562
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
563
  func (w *Worker) reportFailed(taskID, errMsg string) {
564
  _ = w.sendMessage(protocol.TypeTaskFailed, protocol.TaskFailed{
565
  TaskID: taskID,
 
454
  originalSize = inStat.Size()
455
  }
456
 
457
+ // 1.5. Descargar el logo de marca de agua si la tarea incluye uno
458
+ wmImagePath := ""
459
+ if task.WatermarkImageURL != "" {
460
+ candidate := filepath.Join(w.workDir, fmt.Sprintf("%s_wm.png", task.TaskID))
461
+ if wmErr := w.downloadSimpleFile(taskCtx, w.resolveURL(task.WatermarkImageURL), candidate); wmErr != nil {
462
+ log.Printf("[%s] No se pudo descargar el logo (%v); se usará la marca de texto si existe", task.TaskID, wmErr)
463
+ _ = os.Remove(candidate)
464
+ } else {
465
+ wmImagePath = candidate
466
+ defer func() { _ = os.Remove(candidate) }()
467
+ }
468
+ }
469
+
470
  // 2. Obtener duración del video con ffprobe
471
  duration, err := ffmpeg.GetDuration(taskCtx, inputPath)
472
  if err != nil {
 
478
  log.Printf("[%s] Iniciando compresión con FFmpeg (Duración: %.1fs)...", task.TaskID, duration)
479
  startTime := time.Now()
480
 
481
+ err = ffmpeg.Compress(taskCtx, inputPath, outputPath, task.Settings, wmImagePath, duration, func(p protocol.TaskProgress) {
482
  p.TaskID = task.TaskID
483
  _ = w.sendMessage(protocol.TypeTaskProgress, p)
484
  })
 
573
  log.Printf("[%s] ✅ Tarea finalizada y confirmada.", task.TaskID)
574
  }
575
 
576
+ // downloadSimpleFile descarga un archivo pequeño del Manager (logo de marca de agua).
577
+ func (w *Worker) downloadSimpleFile(ctx context.Context, rawURL, destPath string) error {
578
+ req, err := http.NewRequestWithContext(ctx, "GET", rawURL, nil)
579
+ if err != nil {
580
+ return err
581
+ }
582
+ req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36")
583
+ req.Header.Set("X-Worker-Secret", w.secret)
584
+
585
+ resp, err := httpClient.Do(req)
586
+ if err != nil {
587
+ return err
588
+ }
589
+ defer resp.Body.Close()
590
+
591
+ if resp.StatusCode != http.StatusOK {
592
+ return fmt.Errorf("status %d", resp.StatusCode)
593
+ }
594
+
595
+ out, err := os.Create(destPath)
596
+ if err != nil {
597
+ return err
598
+ }
599
+ defer out.Close()
600
+
601
+ _, err = io.Copy(out, resp.Body)
602
+ return err
603
+ }
604
+
605
  func (w *Worker) reportFailed(taskID, errMsg string) {
606
  _ = w.sendMessage(protocol.TypeTaskFailed, protocol.TaskFailed{
607
  TaskID: taskID,