Tạo nhiều version chất lượng cho video như Youtube mà không làm ảnh hưởng performance của web-app

Trong các loại assets của web-app thì video là một trong những loại asests nặng và chiếm nhiều băng thông nhất. Vì vậy, để đáp ứng được nhiều người dùng hơn thì tạo nhiều version chất lượng cho video như cách Youtube đã làm là một điều cần thiết. Nhưng việc xử lý video thường tốn nhiều thời gian và có thể dẫn đến request time out, và bản thân mình cũng đã gặp phải vấn đề này trong dự án thực tế, vì vậy hôm nay mình sẽ giới thiệu tới các bạn cách để tạo nhiều version cho video mà không ảnh hưởng đến performance của web-app. Ý tưởng chính của phương pháp này là tạo ra 2 hay nhiều version nhưng chưa xử lý gì, sau khi khi video đã được upload lên server(ví dụ S3 Amazone) thì chúng ta sẽ tiến hành xử lý video trong background job và sau đó replace file trên server.

1. Tạo ra nhiều version chưa được xử lý

Đầu tiên chúng ta cần add gem "carrierwave" để upload file, gem "aws-sdk" và "fog" nếu bạn lưu video ở S3-server, gem "streamio-ffmpeg" và cài đặt ffmpeg để có thể dùng thư viện này xử lý video

Gemfile
gem "carrierwave"
gem "streamio-ffmpeg"
gem "fog"
gem "aws-sdk", "~> 3"
gem "fog-aws"

trên terminal

bundle install

sudo apt-get update
sudo apt-get dist-upgrade
sudo apt-get install ffmpeg

khai báo version và chưa xử lý, ở đây mình sẽ tạo 2 version là medium và low

class VideoUploader < CarrierWave::Uploader::Base

  version :medium do
  end

  version :low do
  end

mount uploader vào trường lưu video trong DB

class Video < ApplicationRecord
  mount_uploader :video_file, VideoUploader
end

2. Tiến hành xử lý video ở background job

Ở đây, mình sẽ một worker để xử lý video, worker này sẽ lấy ra tất cả version được khai báo trong uploader và gọi đến một service để xư lý từng version một.

video_process/create_version_worker.rb
class VideoProcess::CreateVersionWorker
  include Sidekiq::Worker

  def perform video_id
    video = Video.find video_id
    VideoUploader.versions.keys.each do |version| 
      VideoProcess::CreateVersionService.new(video: video, version: version,
        resolution: Settings.video_version.to_h[version], preserve_aspect_ratio: :height).perform
    end
  end
end

Trong đoạn code trên mình chọn preserve_aspect_ratio là height, có nghĩa là mình sẽ resize chiều cao về kích cỡ được đặt trong settings.yml còn chiều rộng sẽ được resize theo tỉ lệ của video được upload lên Lưu ý, bạn cần lưu kích cỡ các version ở settings.yml hợp lý để có thể gọi theo tên version. Ví dụ đây là file settings.yml của mình:

video_version:
  medium: "858x480"
  low: "427x240"
  height:
    medium: 480
    low: 240

Tiếp theo mình sẽ tạo service để xử lý video, đây sẽ là nơi chưa những đoạn code quan trọng nhất để xử lý video

class VideoProcess::CreateVersionService
  require "aws-sdk"
  attr_reader :video, :version, :resolution, :preserve_aspect_ratio

  def initialize args
    @video = args[:video].decorate
    @version = args[:version].to_sym
    @resolution = args[:resolution]
    @preserve_aspect_ratio = args[:preserve_aspect_ratio]
  end

  def perform
  end
end

Tiếp theo mình sẽ tạo một thư mục tạm trong thư mục tmp để lưu video mà chúng ta đã xử lý được. Và sau đó mình sẽ tạo một object FFMPEG của video đang cần xử lý, trong trường hợp này mình viết một hàm tên là store_path để lấy ra url của video hoặc path của video tuỳ theo môi trường lưu video là S3 hay local, chi tiết hàm mình viết ở phía dưới. Sau đó mình tiến hành xử lý video và lưu ở thư mục tạm nếu độ phân giải của video lớn hơn độ phân giải của version.

  def perform
    Dir.mkdir(Rails.root.join "tmp/video_version") unless File.exists?(Rails.root.join "tmp/video_version")
    ffmpeg = ::FFMPEG::Movie.new(video.store_path)
    if ffmpeg.height > Settings.video_version.height.to_h[version]
      path = video.video_file.versions[version].path
      tmp_path = "tmp/video_version/#{File.basename video.video_file_url(version)}"
      ffmpeg.transcode(tmp_path, {resolution: resolution}, {preserve_aspect_ratio: :height})
    end
  end
model/video.rb
  def store_path version = nil
    if version.nil?
      (ENV["CDN_UPLOADER"] == "true") ? video_file_url : video_file.path
    else
      (ENV["CDN_UPLOADER"] == "true") ? video_file_url(version) : video_file.versions[version].path
    end
  end

3. Replace file cũ bằng file video đã được xử lý

Phần này mình sẽ tách một hàm private để cho dễ nhìn. Đối với video được lưu ở local thì chúng ra đơn giản chỉ cần dùng hàm rename của class File, hàm này sẽ tự động move file ở thư mục tạm và replace vào địa chỉ mới mà chúng ta truyền vào. Còn đối với video được lưu ở trên S3-Server thì chúng ta phải tạo object của Aws-SDK, upload file với option acl: "public-read"(để video được public) rồi sau đó xoá file video ở thư mục tạm.

  class VideoProcess::CreateVersionService
  ...
  private
  def update_version path, tmp_path
    if ENV["CDN_UPLOADER"] == "true"
      File.rename tmp_path, path
    else
      s3 = Aws::S3::Resource.new region: ENV["AWS_REGION"]
      obj = s3.bucket(ENV["S3_BUCKET_NAME"]).object(path)
      obj.upload_file Rails.root.join(tmp_path), acl: "public-read"
      Rails.root.join(tmp_path).delete
    end
  end

Sau đó mình gọi hàm này trong hàm perform của service và gọi worker ở trong controller. Thế là chúng ta đã hoàn thành việc xử lý video

class VideoProcess::CreateVersionService
...
def perform
    Dir.mkdir(Rails.root.join "tmp/video_version") unless File.exists?(Rails.root.join "tmp/video_version")
    ffmpeg = ::FFMPEG::Movie.new(video.store_path)
    if ffmpeg.height > Settings.video_version.height.to_h[version]
      path = video.video_file.versions[version].path
      tmp_path = "tmp/video_version/#{File.basename video.video_file_url(version)}"
      ffmpeg.transcode(tmp_path, {resolution: resolution}, {preserve_aspect_ratio: :height})
      
      update_version path, tmp_path
    end
  end


videos_controller.rb
VideosController < ApplicationController
  def create
    ...
    VideoProcess::CreateVersionWorker.perform_async video.id
  end
end

Bài viết của mình đến đây là hết. Cảm ơn các bạn đã theo dõi.