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
Bài đăng này đã không được cập nhật trong 7 năm
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.
All rights reserved