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à ...
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.