如何使用 FFMPEG 组合多个视频文件并合并音轨

问题描述 投票:0回答:1

我正在尝试将 Delphi 中的多个 MP4 文件与 FFMPEG 视频库结合起来。我有具有所有功能的标题单元。所有视频都是

MPEG-4
,目标输出文件也是如此。

我在 Stack Overflow 上发现了 this 问题,提出了同样的问题。合并视频文件,同时保留音频和视频轨道。 我把答案翻译成Delphi,虽然代码执行成功,但输出文件无效,无法播放。

这是我的实现:

var
  Files: TArray<PAnsiChar>;
  Output: PAnsiChar;

  I, S: integer;

  i_fmt_ctx: PAVFormatContext;
  i_video_stream: PAVStream;
  o_fmt_ctx: PAVFormatContext;
  o_video_stream: PAVStream;

  P: PPAVStream;
begin
  SetLength(Files, 2);
  Files[0] := PAnsiChar('.\Clips\file9.mp4');
  Files[1] := PAnsiChar('.\Clips\file10.mp4');
  Output := '.\Output\out.mp4';

  avcodec_register_all();   
  av_register_all();

  (* should set to NULL so that avformat_open_input() allocate a new one *)
  i_fmt_ctx := nil;

  if avformat_open_input(@i_fmt_ctx, Files[0], nil, nil) <> 0 then
    raise Exception.Create('Could not open file');

  if avformat_find_stream_info(i_fmt_ctx, nil) < 0 then
    raise Exception.Create('Could not find stream info');
                
  (* Find 1st video stream *)
  i_video_stream := nil;
  P := i_fmt_ctx.streams;
  for i := 0 to i_fmt_ctx.nb_streams-1 do begin
    if P^.codec.codec_type = AVMEDIA_TYPE_VIDEO then
      begin
        i_video_stream := P^;
        Break;
      end;
    Inc(P);
  end;
  if i_video_stream = nil then
    raise Exception.Create('Could not find video stream');

  avformat_alloc_output_context2(@o_fmt_ctx, nil, nil, Output);

  (*
  since all input files are supposed to be identical (framerate, dimension, color format, ...)
  we can safely set output codec values from first input file
  *)
  o_video_stream := avformat_new_stream(o_fmt_ctx, nil);
  
  var c: PAVCodecContext;
  c := o_video_stream.codec;
  c.bit_rate := 400000;
  c.codec_id := i_video_stream.codec.codec_id;
  c.codec_type := i_video_stream.codec.codec_type;
  c.time_base.num := i_video_stream.time_base.num;
  c.time_base.den := i_video_stream.time_base.den;
  //fprintf(stderr, "time_base.num = %d time_base.den = %d\n", c->time_base.num, c->time_base.den);
  c.width := i_video_stream.codec.width;
  c.height := i_video_stream.codec.height;
  c.pix_fmt := i_video_stream.codec.pix_fmt;
  //printf("%d %d %d", c->width, c->height, c->pix_fmt);
  c.flags := i_video_stream.codec.flags;
  c.flags := c.flags or CODEC_FLAG_GLOBAL_HEADER;
  c.me_range := i_video_stream.codec.me_range;
  c.max_qdiff := i_video_stream.codec.max_qdiff;

  c.qmin := i_video_stream.codec.qmin;
  c.qmax := i_video_stream.codec.qmax;

  c.qcompress := i_video_stream.codec.qcompress;

  c.extradata := i_video_stream.codec.extradata;
  c.extradata_size := i_video_stream.codec.extradata_size;

  avio_open(@o_fmt_ctx.pb, Output, AVIO_FLAG_WRITE);

  (* yes! this is redundant *)
  avformat_close_input(@i_fmt_ctx);

  avformat_write_header(o_fmt_ctx, nil);

  var last_pts: integer; last_pts := 0;
  var last_dts: integer; last_dts := 0;
  for i := 1 to High(Files) do begin
    i_fmt_ctx := nil;

    if avformat_open_input(@i_fmt_ctx, Files[i], nil, nil) <> 0 then
      raise Exception.Create('Could not open input file');

    if avformat_find_stream_info(i_fmt_ctx, nil) < 0 then
      raise Exception.Create('Could not find stream info');

    av_dump_format(i_fmt_ctx, 0, Files[i], 0);
    
    (* we only use first video stream of each input file *)
    i_video_stream := nil;

    P := i_fmt_ctx.streams;
    for S := 0 to i_fmt_ctx.nb_streams-1 do
      begin
        if (P^.codec.codec_type = AVMEDIA_TYPE_VIDEO) then
          begin
            i_video_stream := P^;
            break;
          end;
        
        Inc(P);
      end;

    if i_video_stream = nil then
      raise Exception.Create('Could not find video stream');
    
    var pts, dts: int64;
    pts := 0; dts := 0;
    while true do begin
      var i_pkt: TAVPacket;
      av_init_packet( @i_pkt );
      i_pkt.size := 0;
      i_pkt.data := nil;

      if av_read_frame(i_fmt_ctx, @i_pkt) < 0 then
        break;
      (*
        pts and dts should increase monotonically
        pts should be >= dts
      *)
      i_pkt.flags := i_pkt.flags or AV_PKT_FLAG_KEY;
      pts := i_pkt.pts;
      Inc(i_pkt.pts, last_pts);
      dts := i_pkt.dts;
      Inc(i_pkt.dts, last_dts);
      i_pkt.stream_index := 0;

      // Write
      av_interleaved_write_frame(o_fmt_ctx, @i_pkt);
    end;

    Inc(last_dts, dts);
    Inc(last_pts, pts);  
  
    avformat_close_input(@i_fmt_ctx)
  end;

  av_write_trailer(o_fmt_ctx);

  avcodec_close(o_fmt_ctx.streams^.codec);
  av_freep(&o_fmt_ctx.streams^.codec);
  av_freep(&o_fmt_ctx.streams);

  avio_close(o_fmt_ctx.pb);
  av_free(o_fmt_ctx);

这是

Михаил Чеботарев
的答案的翻译。

即使代码有效,我也看不到对

AVMEDIA_TYPE_AUDIO
流的处理,这意味着这个答案是问题的 1/2,因为它只组合了视频流。

我尝试的另一种方法是使用UBitmaps2Video FFMPEG实现,它成功地能够合并视频文件,但只有视频流,没有音频。

我尝试使用低音音频库手动转换音频流。它能够读取音频并将其写入单个 WAV 文件,然后我将其转换为 MP3。最后使用

MuxStreams2
混合合并的视频文件和 MP3 文件。不幸的是,音频和视频无法正确对齐。我无法确定问题所在。

目前,唯一的功能选项是使用预编译的FFMPEG Executables并使用带有相应参数的ShellExecute来组合视频。 更准确地说:

ffmpeg -f concat -safe 0 -i video-list.txt -c copy output.mp4

但我仍然宁愿使用 Delphi 中的 FFMPEG 标头以这种方式组合视频,因为这提供了进度指示器的选项、对播放的更多控制以及随时暂停线程的能力。

那么,为什么我的合并视频文件的实现不起作用。包含音频流的好方法是什么?

delphi ffmpeg libav
1个回答
0
投票

Yo uso este codigo espero te funcione, es bastante sencillo, alone que las instrucciones estan en español:

import subprocess
import os
import tkinter as tk
from tkinter import filedialog


def combine_videos(video_paths, output_folder):
    # Crear el comando para concatenar los videos
    filters = ''.join([f"[{i}:v:0][{i}:a:0]" for i in range(len(video_paths))])
    filter_complex = f"{filters}concat=n={len(video_paths)}:v=1:a=1[outv][outa]"
    output_path = os.path.join(output_folder, 'output.mp4')
    command = ['ffmpeg', '-y']
    for path in video_paths:
        command.extend(['-i', path])
    command.extend(['-filter_complex', filter_complex, '-map', '[outv]', '-map', '[outa]', output_path])
    
    # Ejecutar el comando
    try:
        subprocess.run(command, check=True)
        print(f"Los videos se han combinado exitosamente en '{output_path}'.")
    except subprocess.CalledProcessError as e:
        print(f"Ocurrió un error al combinar los videos: {e}")


def browse_video_paths():
    num_videos = int(num_videos_entry.get())
    video_paths = []
    for i in range(num_videos):
        file_path = filedialog.askopenfilename(filetypes=[("Video Files", "*.mp4")])
        if file_path:
            video_paths.append(file_path)
    video_paths_text.delete(1.0, tk.END)
    video_paths_text.insert(tk.END, "\n".join(video_paths))

def browse_output_folder():
    folder_path = filedialog.askdirectory()
    if folder_path:
        output_folder_entry.delete(0, tk.END)
        output_folder_entry.insert(0, folder_path)

def combine_videos_gui():
    num_videos = int(num_videos_entry.get())
    video_paths = video_paths_text.get("1.0", "end-1c").split("\n")
    output_folder = output_folder_entry.get()
    
    if not os.path.exists(output_folder):
        os.makedirs(output_folder)
    
    combine_videos(video_paths, output_folder)
    status_label.config(text="Los videos se han combinado exitosamente.")

# Crear la ventana principal
root = tk.Tk()
root.title("Combinar Videos")

# Etiqueta y entrada para el número de videos
num_videos_label = tk.Label(root, text="Número de videos a combinar:")
num_videos_label.pack()
num_videos_entry = tk.Entry(root)
num_videos_entry.pack()

# Botón para seleccionar los videos
browse_videos_button = tk.Button(root, text="Seleccionar Videos", command=browse_video_paths)
browse_videos_button.pack()

# Texto para mostrar las rutas de los videos seleccionados
video_paths_text = tk.Text(root, height=5, width=40)
video_paths_text.pack()

# Botón para seleccionar la carpeta de salida
browse_output_button = tk.Button(root, text="Seleccionar Carpeta de Salida", command=browse_output_folder)
browse_output_button.pack()

# Entrada para la carpeta de salida
output_folder_label = tk.Label(root, text="Carpeta de Salida:")
output_folder_label.pack()
output_folder_entry = tk.Entry(root)
output_folder_entry.pack()

# Botón para combinar los videos
combine_button = tk.Button(root, text="Combinar Videos", command=combine_videos_gui)
combine_button.pack()

# Etiqueta para mostrar el estado de la operación
status_label = tk.Label(root, text="")
status_label.pack()

# Iniciar la interfaz gráfica
root.mainloop()
© www.soinside.com 2019 - 2024. All rights reserved.