发布于 2026-01-06 10 阅读
0

使用 Rails 6 和 FFmpeg 构建视频转换器

使用 Rails 6 和 FFmpeg 构建视频转换器

今天的项目将 Rails 内置的多个工具,以及几个非常有用的开源项目整合在一起,构建一个可以将用户上传的视频文件转换为 MP4 格式的 Web 应用程序。

为了展示标准 Rails 技术栈的一些现代功能,我们不仅要构建一个转换器,还要进一步优化上传和转换过程中的用户体验,例如Active Storage支持直接文件上传、Action Cable实时更新视频转换进度,以及Stimulus在无需使用繁重的 JavaScript 框架的情况下动态更新 DOM。

完成后,我们将拥有一个虽然界面简陋但功能齐全的网页应用程序,允许用户上传视频文件。文件上传后,我们会根据需要将视频转换为 MP4 格式,然后将转换后的视频显示给用户。界面大致如下:

屏幕录像显示用户点击提交按钮,屏幕上出现进度条,然后随着视频转换进度,文字会递增计数。转换完成后,文字会被视频片段替换。

本指南假设您熟悉 Ruby on Rails,并了解Active StorageStimulus,但您无需精通这些工具也能从中获益。我们将从一个全新的 Rails 6 项目开始,当然,如果您愿意,也可以使用现有的 Rails 6 项目进行操作。

如果您使用现有项目,则可能需要完成刺激、动作电缆和主动存储的额外设置步骤,这些步骤在本指南中不会涵盖。

项目设置

首先,让我们使用 webpack 和 Stimulus 创建 Rails 应用程序,安装 Active Storage,添加一个 User 脚手架以供工作,并迁移我们的数据库。

这里都是标准的 Rails 内容,希望还没有什么全新的东西!



rails new upload_progress --webpack=stimulus --skip-coffee --database=postgresql -T
cd upload_progress
rails db:create
rails g scaffold User name:string
rails active_storage:install
rails db:migrate


Enter fullscreen mode Exit fullscreen mode

此时,您可以启动 Rails 服务器,rails s然后访问localhost:3000/users查看脚手架是否正常工作。您可以根据需要创建一些用户。这里没有任何规则。

项目设置完毕,让我们开始有趣的部分吧。

使用 Active Storage 上传文件

我们的用户将拥有一段个人资料视频,我们将以此为基础,在本指南的后续部分探讨视频转换。

要添加个人资料视频,请更新User模型以添加profile_video由 Active Storage 支持的版本。



# app/models/user.rb

class User < ApplicationRecord
  has_one_attached :profile_video
end


Enter fullscreen mode Exit fullscreen mode

得益于 Active Storage 的强大功能,我们现在只需将个人资料视频添加到控制器和视图中,即可立即开始上传文件。

前往用户表单,添加一个用于存放视频的新表单字段:



<!-- app/views/users/_form.html.erb -->

<%= form_with(model: user) do |form| %>
  <% if user.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(user.errors.count, "error") %> prohibited this user from being saved:</h2>

      <ul>
        <% user.errors.each do |error| %>
          <li><%= error.full_message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :name %>
    <%= form.text_field :name %>
  </div>

  <div class="field">
    <%= form.label :profile_video %>
    <%= form.file_field :profile_video %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>


Enter fullscreen mode Exit fullscreen mode

然后更新UsersController现有user_params方法,添加 profile_video 参数。



def user_params
  params.require(:user).permit(:name, :profile_video)
end


Enter fullscreen mode Exit fullscreen mode

最后,我们希望能够在上传视频后看到它。请前往并向页面app/views/users/show添加一个元素。<video>



<video controls>
  <source src="<%= url_for(@user.profile_video) %>">
</video>


Enter fullscreen mode Exit fullscreen mode

请尝试创建一个新用户,并将视频文件附加到 profile_video 字段,以确保视频上传功能按预期工作。

屏幕截图显示一段视频显示在纯白页面上。

太惊艳了!你真是个明星!

虽然这样可以实现上传,但我们还可以让上传体验更好一些。

首先,当文件上传对话框打开时,用户可以选择任何类型的文件。我们来添加一个限制,只允许在上传对话框中选择视频文件。



<%= form.file_field :profile_video, accept: "video/*" %>


Enter fullscreen mode Exit fullscreen mode

请注意,在实际应用中,accept虽然这能提升用户体验,但并不能验证用户的输入。您应该始终在服务器端验证文件类型!恶意用户或好奇的用户很容易在浏览器中更改 accept 属性。目前,我们可以在客户端使用字段级验证来解决问题。

添加直接上传功能和上传进度条

当用户直接上传大文件到您的服务器时,他们会发现请求需要很长时间,并且在生产环境中,可能会经常超时,阻塞其他请求,并导致各种各样的问题。

我们可以使用 Active Storage 内置的直接上传功能来避免所有这些问题。直接上传功能可将文件直接从用户客户端发送到您选择的云存储提供商。直接上传可以加快上传速度,并防止请求超时或占用服务器资源。

让我们在文件字段中添加直接上传功能,并且作为额外功能,为用户添加上传进度条。

首先,请确认您已将 ActiveStorage 的 javascript 添加到您的application.js文件中。



// If you started with a new Rails project, these lines should already be in application.js. If not, add them
import * as ActiveStorage from "@rails/activestorage"
ActiveStorage.start()


Enter fullscreen mode Exit fullscreen mode

现在,我们需要告诉文件字段使用直接上传的 JavaScript 代码:



<%= form.file_field :profile_video, accept: "video/*", direct_upload: true %>


Enter fullscreen mode Exit fullscreen mode

刷新页面,确认一切运行正常,与我们添加直接上传功能之前完全一样。Rails 的确让一切变得非常简单。

虽然我们的文件上传功能目前运行正常,我们可以就此作罢,但在实际应用中,上传文件可能需要一些时间,尤其是在处理大型视频文件时。在用户上传文件的过程中提供反馈,可以有效提升用户体验。让我们添加一个进度条来跟踪文件上传进度,只需使用一些Stimulus插件即可。

为了跟踪上传进度,我们将监听该direct-upload:progress事件。每次触发该事件时,我们将使用事件中的数据来更新用户界面中的进度条。

我们将从刺激控制器开始,我们可以用以下代码创建它touch app/javascript/controllers/upload_progress_controller.js,然后用以下代码填充:



// app/javascript/controllers/upload_progress_controller.js

import { Controller } from 'stimulus'

export default class extends Controller {
  static targets = [ "progress", "progressText", "progressWidth" ]

  initialize() {
  }

  connect() {
    this.element.addEventListener("direct-upload:progress", this.updateProgress.bind(this))

    this.element.addEventListener("direct-upload:error", event => {
      event.preventDefault()
      const { id, error } = event.detail
      console.log(error)
    })
  }

  showProgress() {
    this.progressTarget.style.display = "block"
  }

  updateProgress() {
    const { id, progress } = event.detail
    this.progressWidthTarget.style.width = `${Math.round(progress)}%`
    this.progressTextTarget.innerHTML = `${Math.round(progress)}% complete`
  }

  disconnect() {
    this.element.removeEventListener("direct-upload:progress", this.updateProgress)
  }
}


Enter fullscreen mode Exit fullscreen mode

让我们来详细分析一下——我假设你对刺激物已经有一些了解,如果你不了解,《刺激物手册》是一个很好的起点。

connect用于设置我们的事件监听器并订阅我们关心的事件——在本例中,direct-upload:progress以及direct-upload:error

我们的进度事件监听器会调用updateProgress并使用progress该事件中的数据来更新构成进度条的 UI 元素。

接下来,我们将添加进度条 HTML 代码,并将 Stimulus 控制器连接到 DOM。



<!-- app/views/users/_form.html.erb -->

<%= form_with(model: user, html: { data: { controller: "upload-progress" } } ) do |form| %>

  <!-- snipped form fields -->

  <div style="display: none;" data-upload-progress-target="progress">
    <div>Uploading your video to our servers: <span id="progressText" data-upload-progress-target="progressText">Warming up...</span></div>
      <div id="progress-bar" style="background: gray; position: relative; width: 200px; height: 20px;">
      <div id="progressWidth" data-upload-progress-target="progressWidth" style="width: 0%; height: 100%; position: absolute; background: green;">
      </div>
    </div>
  </div>

  <div class="actions">
    <%= form.submit "Save", data: { action: "click->upload-progress#showProgress" } %>
  </div>
<% end %>


Enter fullscreen mode Exit fullscreen mode

这里的内容很多,我们逐一来看更新内容。

data: { controller: “upload-progress” }首先,我们通过向代码中添加元素,将 Stimulus 控制器连接到 DOM。form_with

接下来,我们添加进度条的HTML代码。不用在意所有的内联样式,你也可以使用CSS。



<div style="display: none;" data-upload-progress-target="progress">
  <div>Uploading your video to our servers: <span id="progressText" data-upload-progress-target="progressText">Warming up...</span></div>
  <div id="progress-bar" style="background: gray; position: relative; width: 200px; height: 20px;">
    <div id="progressWidth" data-upload-progress-target="progressWidth" style="width: 0%; height: 100%; position: absolute; background: green;">
    </div>
  </div>
</div>


Enter fullscreen mode Exit fullscreen mode

页面加载时进度条会被隐藏,我们在进度条容器、进度文本和进度宽度元素上使用了 Stimulus 目标。Stimulus 控制器会利用这些目标,在direct-upload:progress事件触发时更新 DOM。

最后,我们监听表单提交按钮的点击事件。当表单提交按钮被点击时,我们会调用函数upload-progress#showProgress移除进度条容器的隐藏样式。

添加视频转换服务

现在我们的个人资料视频上传一切正常,我们会随时向用户通报进度。

进展显著,但我们目前接受并展示任何类型的视频,而我们的目标是构建一个视频转换器,以便个人资料视频的格式能够统一。在本教程中,我们假设所有上传的视频最终都必须是 MP4 格式。

在深入细节之前,让我们先回顾一下我们想要达成的目标。我们想要:

  1. 允许用户通过用户个人资料表单上传视频文件。
  2. 上传视频文件时,请检查视频的内容类型
  3. 如果视频已经是mp4文件,我们就无需转换视频——我们的工作已经完成。
  4. 如果视频不是mp4文件,我们需要将视频从其原始格式转码为.mp4格式。

那么,我们如何完成第四步呢?即时转换视频听起来很复杂,对吧?

FFmpeg一款非常流行的开源视频(以及音频,如果你喜欢的话)处理解决方案。

虽然我们可以直接使用 FFmpeg,但为了简化与 FFmpeg 的交互,我们将使用一个 gem:streamio-ffmpeg。

首先,我们需要将 gem 添加到项目中,bundle add streamio-ffmpeg然后在 Mac 系统上使用以下命令安装 ffmpeg 。其他安装选项可以在这里brew install ffmpeg找到。

FFmpeg 准备就绪后,让我们添加一个用于处理视频转换的服务。mkdir app/services && touch app/services/video_converter.rb

该服务界面如下:



# app/services/video_converter.rb

class VideoConverter
  def initialize(user_id)
    @user = User.find(user_id)
  end

  def convert!
    process_video
  end

  private

  def process_video
    @user.profile_video.open(tmpdir: "/tmp") do |file|
      movie = FFMPEG::Movie.new(file.path)
      path = "tmp/video-#{SecureRandom.alphanumeric(12)}.mp4"
      movie.transcode(path, { video_codec: 'libx264', audio_codec: 'aac' })
      @user.profile_video.attach(io: File.open(path), filename: "video-#{SecureRandom.alphanumeric(12)}.mp4", content_type: 'video/mp4')
    end
  end
end


Enter fullscreen mode Exit fullscreen mode

这项服务的关键在于process_video方法,所以我们来深入探讨一下。

首先,我们打开附加到用户的现有个人资料视频,以便我们可以访问视频路径。

@user.profile_video.open(tmpdir: "/tmp") do |file|

接下来,我们Movie使用 gem 创建一个对象streamio-ffmpeg,使用用户上传的原始文件,我们稍后将对其进行转码。

movie = FFMPEG::Movie.new(file.path)

路径变量指定的是我们将创建新的转码视频的位置。使用这个新路径和Movie对象,我们调用gem中的transcode 方法streamio-ffmpeg

最后,我们将新创建的视频附加到用户帐户,替换之前上传的视频。就这么简单!

@user.profile_video.attach(io: File.open(path), filename: "video-#{SecureRandom.alphanumeric(12)}.mp4", content_type: 'video/mp4')

添加后台工作

在开始转换上传的视频之前,还需要完成一个步骤。处理视频可能非常耗时耗力。我们不希望在页面翻页时转换视频,也不希望昂贵的视频处理占用应用程序服务器资源。

我们来添加一个任务,用于在后台排队等待视频处理任务。在本教程中,我们将使用带有默认:async适配器的 ActiveJob,但在生产环境中,您需要使用真正的后台处理器。

添加该职位rails g job convert_video

我们的任务会接收一个用户 ID,并调用该用户的视频转换服务,如下所示:



class ConvertVideoJob < ApplicationJob
  queue_as :default

  def perform(user_id)
    VideoConverter.new(user_id).convert!
  end
end


Enter fullscreen mode Exit fullscreen mode

转换上传的视频

现在我们准备转换视频了。在我们的代码中UsersController,用户保存后,我们会在创建和更新方法中将后台任务加入队列。



# app/controllers/users_controller.rb

def create
  @user = User.new(user_params)

  respond_to do |format|
    if @user.save
      ConvertVideoJob.perform_later(@user.id)
    # snip response boilerplate
    end
  end
end

def update
  respond_to do |format|
    if @user.update(user_params)
      ConvertVideoJob.perform_later(@user.id)
    # snip response boilerplate
    end
  end
end


Enter fullscreen mode Exit fullscreen mode

现在当我们上传个人资料视频时,我们的VideoConverter服务会将视频转换为mp4格式,并将上传的视频替换为新转换的视频。

您可以从用户表单上传任意非mp4格式的视频进行测试。如果一切正常,您应该会在服务器日志中看到类似这样的输出:

INFO -- : Transcoding of /tmp/ActiveStorage-22-20210428-10611-1wvew4g.mov to tmp/video-9ZrvVFnAZZTJ.mp4 succeeded

等等。转码需要一些时间,而且是在后台进行的,所以当我上传视频时,节目页面会先渲染未转码的视频,直到转码完成。这会导致各种各样的奇怪bug,对吧?

正确的。

让我们用Action Cable来提升一下体验,再加上一些 Stimulus 的功能,就可以跟踪视频转换进度,无需翻页即可渲染转换后的视频。

利用 Action Cable 广播视频更新

首先,让我们从宏观层面再讨论一下这个问题。我们想要实现的目标大致如下:

  1. 视频上传后,检查它是否为mp4格式。
  2. 如果不是,请将该视频标记为需要转换。
  3. 如果视频被标记为需要转换,则不要在用户的节目页面上渲染该视频元素。
  4. 如果正在处理视频,请将此信息告知用户。
  5. 如果视频已处理完毕或无需处理,则在节目页面上渲染视频元素。

让我们从上一节中跳过的逻辑入手,检查视频是否需要转码。由于将mp4格式的视频转换成mp4格式毫无意义,我们应该在进行任何处理之前检查上传视频的内容类型。

如果内容类型为 video/mp4,则无需转码。如果内容类型为其他任何类型,请标记该视频并开始转换。

在编写逻辑代码之前,让我们先向 User 模型添加一个布尔值,用于跟踪个人资料视频是否需要转换:

rails g migration AddConvertVideoToUsers convert_video:boolean

在继续操作之前,请先迁移数据库。rails db:migrate

现在我们可以把逻辑添加到控制器中。在实际应用中,控制器可能不是编写这段代码的合适位置,但我们来这里是为了学习,所以让我们专注于学习本身。



# app/controllers/users_controller.rb

def create
  @user = User.new(user_params)
  respond_to do |format|
    if @user.save
      update_conversion_value
      ConvertVideoJob.perform_later(@user.id)
    # snip render logic
    end
  end
end

# PATCH/PUT /users/1 or /users/1.json
def update
  respond_to do |format|
    if @user.update(user_params)
      update_conversion_value
      ConvertVideoJob.perform_later(@user.id)
    # snip render logic
    end
  end
end

private
# snip
def update_conversion_value
  return unless @user.profile_video

  needs_conversion = @user.profile_video.content_type != "video/mp4"
  @user.update_column(:convert_video, needs_conversion)
end


Enter fullscreen mode Exit fullscreen mode

这种略显笨拙的update_conversion_value方法是检查个人资料视频的内容类型,并将相应的值赋给 convert_video 列。

现在我们已经有了这个逻辑,我们可以进入转换服务并应用这个逻辑了。



# app/services/video_converter.rb
def convert!
  return unless @user.convert_video?

  process_video
end


Enter fullscreen mode Exit fullscreen mode

这项更改确保我们的视频转换器不会转换未标记为需要转换的视频。

convert_video接下来,我们需要在转换非mp4视频后更新该值。我们可以使用update_needs_conversion服务中的一个新方法来实现这一点:



# app/services/video_converter.rb

class VideoConverter
  def convert!
    return unless @user.convert_video?

    process_video
    update_needs_conversion
  end

  private

  # Snip

  def update_needs_conversion
    @user.update_column(:convert_video, false)
  end
end


Enter fullscreen mode Exit fullscreen mode

现在,当我们上传视频时,如果视频是 mp4 格式,转换器将不做任何操作直接返回;否则,它将转换视频,然后将convert_video用户的标志更新为 false。

有了这个标志,我们就可以添加我们的 Action Cable 和 Stimulus 功能,将视频的转换状态传达给用户,这样他们就可以在不刷新页面的情况下看到自己上传的视频。

让我们记住这个项目这一部分的目标。我们想要:

  1. 在渲染用户显示页面时,检查视频的转换状态。
  2. 如果视频已转换,则在视频元素中显示该视频。
  3. 如果视频转换失败,则显示视频占位符,并将视频转换状态告知用户。
  4. 视频转换完成后,自动更新用户节目页面的内容,在视频元素中显示视频。

让我们深入探讨一下。

首先,我们需要生成一个 Action Cable 频道用于广播。Rails 自带一个内置转换器来完成这项任务。使用该rails g channel VideoConversion转换器生成新频道。

将生成的文件更新video_conversion_channel.rb为如下所示:



class VideoConversionChannel < ApplicationCable::Channel
  def subscribed
    stream_from "video_conversion_#{params[:id]}"
  end
end


Enter fullscreen mode Exit fullscreen mode

该频道负责播报视频转换过程的进度,并在用户界面中更新完成百分比元素。

接下来,我们添加 Stimulus 控制器,它将监听来自此通道的事件,并在接收到事件时更新 UI。

第一的touch app/javascript/controllers/conversion_progress_controller.js

进而:



import { Controller } from "stimulus";
import consumer from "channels/consumer";

export default class extends Controller {
  static targets = [ "progressText" ]

  initialize() {
    this.subscription = consumer.subscriptions.create(
      {
        channel: "VideoConversionChannel",
        id: this.element.dataset.id,
      },
      {
        connected: this._connected.bind(this),
        disconnected: this._disconnected.bind(this),
        received: this._received.bind(this),
      }
    );
  }

  _connected() {}

  _disconnected() {}

  _received(data) {
    this.updateProgress(data * 100)
  }

  updateProgress = (progress) => {
    let progressPercent = ''
    if (progress >= 100) {
      progressPercent = "100%"
    } else {
      progressPercent = Math.round(progress) + "%"
    }
    this.progressTextTarget.innerHTML = progressPercent
  }
}


Enter fullscreen mode Exit fullscreen mode

这个控制器包含了很多 Action Cable 的样板代码,不要被它们吓到。

关键部分在于channel我们在方法中订阅的内容initialize以及该方法。当用户订阅的频道(根据方法中的 ID)_received收到新消息时,会调用该方法,该方法会使用 Action Cable 广播的进度值更新 DOM。initialize_receivedupdateProgress

让我们把这个控制器连接到我们的 HTML 代码中,开始把它们整合起来。

我们想要订阅特定用户的更新,这意味着我们需要更新我们的显示视图以连接到ConversionProgressStimulus 控制器。



<!-- views/users/show.html.erb -->

<!-- snip -->
<div data-id="<%= @user.id %>" data-controller="conversion-progress" style="max-width: 500px; max-height: 500px;">
  <% if @user.convert_video? %>
    <div>
      <p>We are converting your video. The video is currently <span data-conversion-progress-target="progressText">0%</span> processed</p>
    </div>
  <% else %>
    <video controls style="max-width: 100%; max-height: 100%;">
      <source src="<%= url_for(@user.profile_video) %>">
    </video>
  <% end %>
</div>
<!-- snip -->


Enter fullscreen mode Exit fullscreen mode

这里我们向视频的父视频添加了一个 ` data-id<video>` 和 `<video>` 。` <video>` 由 Stimulus 控制器使用,用于知道要订阅哪个频道的更新,而 `<video>`用于将 Stimulus 控制器连接到 DOM。data-controllerdata-iddata-controller

另一个变化是添加了逻辑,以便在视频转换完成后按原样显示视频;否则,我们将渲染文本,我们的 Stimulus 控制器将在从 Action Cable 接收更新时更新该文本。

完成这些更改后,如果您上传一个新的非mp4视频给用户并访问节目页面,您会看到“我们正在转换您的视频”的提示,但转换进度百分比不会更新。这是因为我们尚未将转换进度广播到Action Cable频道。

一张显示视频转换进度为 0% 的纯文本截图

幸运的是,FFmpeg gem 让广播进度变得非常简单。让我们更新transcode视频转换服务中的调用,以便广播更改:



movie.transcode(path, { video_codec: 'libx264', audio_codec: 'aac' }) { |progress| ActionCable.server.broadcast("video_conversion_#{@user.id}", progress) }


Enter fullscreen mode Exit fullscreen mode

现在,当您上传需要转换的新视频时,进度条会从 0% 逐渐增加到 100%。根据视频的属性和您电脑的性能,转换过程可能很快,也可能需要几分钟。无论如何,您现在都可以实时查看转换进度!

当转化率达到 100% 时,你会注意到最后一个需要解决的问题。转化率达到 100% 时,视频占位符并没有被替换成实际视频,而是计数器一直停留在那里,用户必须刷新页面才能看到视频。我们可以通过增加一些行动提示和一些激励措施来解决这个问题。

首先,添加另一个 Action Cable 频道,rails g channel ConvertedVideo并在生成的 _channel.rb 文件中更新 subscribed 方法:



class ConvertedVideoChannel < ApplicationCable::Channel
  def subscribed
    stream_from "converted_video_#{params[:id]}"
  end
end


Enter fullscreen mode Exit fullscreen mode

然后添加一个新的 Stimulus 控制器,用于订阅频道和管理更新。touch app/javascript/controllers/converted_video_controller.js

添加代码以订阅和处理ConvertedVideo频道上的广播。



// javascript/controllers/converted_video_controller.js

import { Controller } from "stimulus";
import consumer from "channels/consumer";

export default class extends Controller {
  static targets = ["videoContainer"];

  initialize() {
    this.subscription = consumer.subscriptions.create(
      {
        channel: "ConvertedVideoChannel",
        id: this.element.dataset.id,
      },
      {
        connected: this._connected.bind(this),
        disconnected: this._disconnected.bind(this),
        received: this._received.bind(this),
      }
    );
  }

  _connected() {}

  _disconnected() {}

  _received(data) {
    const videoElement = this.videoContainerTarget
    videoElement.innerHTML = data
  }
}


Enter fullscreen mode Exit fullscreen mode

ConvertedVideoChannel这个 Stimulus 控制器与上一个控制器非常相似。在这个控制器中,我们使用用户 ID订阅。当数据在通道上广播时,Stimulus 控制器会查找一个videoContainerDOM 元素,并将该元素的内容替换为 Action Cable 发送的数据。接下来我们将看到这些数据是什么样的。

我们的目标是在视频处理完成后,将其容器中的占位符元素替换为实际视频。我们可以利用 Action Cable 的一个特性来实现这一点:我们可以将视图局部渲染为一个字符串,然后通过 Action Cable 广播该字符串,从而轻松地用消息中的 HTML 内容替换 DOM 内容。

首先,我们添加一个用于渲染视频元素的局部视图,并将其添加到视图中。在显示视图中,我们还会将新的 Stimulus 控制器连接到 DOM。

首先touch app/views/users/_profile_video.html.erb在终端中运行以下命令,然后:



<!-- views/users/_profile_video.html.erb -->

<video controls style="max-width: 100%; max-height: 100%;">
  <source src="<%= url_for(user.profile_video) %>">
</video>


Enter fullscreen mode Exit fullscreen mode


<!-- views/users/show.html.erb -->

<div data-id="<%= @user.id %>" data-controller="conversion-progress converted-video" data-converted-video-target="videoContainer" style="max-width: 500px; max-height: 500px;">
  <% if @user.convert_video? %>
    <div>
      <p>We are converting your video. The video is currently <span data-conversion-progress-target="progressText">0%</span> processed</p>
    </div>
  <% else %>
    <%= render "profile_video", user: @user %>
  <% end %>
</div>


Enter fullscreen mode Exit fullscreen mode

现在我们将视频元素移到了局部视图中,并更新了显示视图,以便在不需要转换视频时渲染局部视图。

请注意上面的第一行代码。我们已将新的converted-videoStimulus 控制器添加到视频容器的data-controller属性中。这会将控制器连接到 DOM,并确保节目页面的访问者已订阅该ConvertedVideo频道。我们还data-converted-video-target向同一个元素添加了一个属性<div>。Stimulus 控制器使用此目标将进度文本替换为视频元素。

最后一步是更新服务,以便在视频转换完成后,在频道上VideoConverter广播包含部分内容的消息。profile_videoConvertedVideo



# app/services/video_converter.rb

def convert!
  return unless @user.convert_video?

  process_video
  update_needs_conversion
  render_processed_video
end

private

# Snip
def render_processed_video
  partial = ApplicationController.render(partial: "users/profile_video", locals: { user: @user })
  ActionCable.server.broadcast("converted_video_#{@user.id}", partial)
end


Enter fullscreen mode Exit fullscreen mode

这里我们render_processed_videoconvert!方法中调用了一个新方法。这个方法会将一个局部视图渲染成一个字符串,然后将该字符串作为数据广播出去,供我们的 Stimulus 控制器获取和使用。神奇吧!

让我们看看它的实际效果。

屏幕录像显示用户点击提交按钮,屏幕上出现进度条,然后随着视频转换进度,文字会递增计数。转换完成后,文字会被视频片段替换。

总结

感谢您阅读完本指南!您可以在Github上找到本指南的完整源代码。

总结一下,今天我们从一个全新的 Rails 6 应用开始。借助 Stimulus、Active Storage、Action Cable 以及最重要的 FFmpeg,我们构建了一个应用,可以根据需要将用户上传的视频文件转换为 mp4 格式。文件转换过程中,我们会将转换进度告知用户,并在无需用户刷新页面的情况下显示转换后的视频。

为了使本教程中的代码能够用于生产环境,除了清理代码和进行样式调整之外,您还应该花时间验证用户上传的文件,添加真正的后台作业处理器,并为视频转换服务添加错误处理和弹性机制。

您还可以在客户端和服务器端为视频添加文件大小验证,并考虑使用功能更强大的 FFmpeg 来更高效地将视频转换为 mp4 格式。

更多资源和联系方式

如果您想查看此项目的生产版本,请查看Vestimonials的演示,这是我在不撰写像这样的技术文章时正在开发的产品。

如果您对本文有任何疑问或反馈,可以在Twitter上找到我。

感谢阅读!

文章来源:https://dev.to/davidcolbyatx/building-a-video-converter-with-rails-6-and-ffmpeg-5e88