Tạo ứng dụng chia sẻ video với Ruby on Rails
Trong hướng dẫn này, bạn sẽ biết cách tạo ứng dụng chia sẻ video cơ bản sử dụng Rails. Các tính năng bao gồm: Sign up, in, out - sử dụng gem devise. Upload video - xử lý mã hoá video. Play video - sử dụng videojs tạo trình chạy video đơn giản. Thông báo - thông báo cho người dùng khi mã ...
Trong hướng dẫn này, bạn sẽ biết cách tạo ứng dụng chia sẻ video cơ bản sử dụng Rails.
Các tính năng bao gồm:
- Sign up, in, out - sử dụng gem devise.
- Upload video - xử lý mã hoá video.
- Play video - sử dụng videojs tạo trình chạy video đơn giản.
- Thông báo - thông báo cho người dùng khi mã hoá xong video, sử dụng pubnub.
Để thực hiện, bạn cần một số kiến thức về Ruby on Rails, CoffeeScript và HAML. Biết cách cài đặt ffmpeg và redis.
Khởi tạo
Tạo mới một ứng dụng Rails
$ rails new VideoShrimp
Chúng ta sẽ sử dụng cơ sở dữ liệu SQLite, tất nhiên bạn có thể sử dụng postgresql, mysql hay bất cứ cơ sở dữ liệu nào bạn muốn.
Sử dụng Rails version 4.1 trở lên:
$ rails -v Rails 4.2.4 $ ruby -v ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin15]
Tiếp theo là thêm các gem cần dùng vào Gemfile.
gem 'haml' gem 'devise' gem 'simple_form' gem 'paperclip' gem 'bootstrap-sass' gem 'sidekiq' gem 'sidetiq', github: 'sfroehler/sidetiq', branch: 'celluloid-0-17-compatibility' gem 'pubnub', github: 'pubnub/ruby', branch: 'celluloid' gem 'sinatra', :require => nil group :development do gem 'pry' gem 'pry-rails' end
Giải thích:
- haml - tương tự template *html.erb.
- devise - xác thực người dùng.
- simple_form - thiết kế form chuẩn trong Rails.
- paperclip - quản lý tệp.
- bootstrap-sass - thiết kế giao diện.
- sidekiq - xử lý video ở chế độ nền.
- sidetiq - là một plugin của sidekiq. Dùng để kiểm tra những video đã được mã hoá mà chưa được công bố.
- pubnub - để thông báo và lấy các thông tin liên lạc giữa backend-frontend.
- sinatra - sidekiq frontend, tuỳ chọn.
- pry - tốt hơn irb.
- pry-rails - dùng pry thay cho irb trong rails c.
Chú ý #1: Như bạn thấy, tôi dùng gem sidekiq từ repository ngoài, vì gem chuẩn khồn tương thích với celluloid hiện tại (thời điểm viết bài).
Chú ý #2: gem pubnub lấy từ branch khác master vì bản celluloid chính chưa ổn định. Hiện dùng pubnub bản beta.
Tiếp theo, chạy bundle install để tải gem.
$ bundle install
Cài đặt devise và simple_form
$ rails generate simple_form:install --bootstrap $ rails generate devise:install
Lệnh tạo devise sẽ tạo ra file config/initializers/devise.rb và config/locales/devise.en.yml nhưng bạn không cần quan tâm đến hai file này, hiện tại không cần cấu hình thêm gì từ devise.
Tạo view để sử dụng simple_form:
$ rails generate devise:views
Tiếp theo là tạo model và migrate.
Tạo model
rails generate devise user invoke active_record create db/migrate/20151117181300_devise_create_users.rb create app/models/user.rb invoke test_unit create test/models/user_test.rb create test/fixtures/users.yml insert app/models/user.rb route devise_for :users
Với ứng dụng cơ bản này, chúng ta không cần thêm gì khác vì sẽ sử dụng user email mặc định để đăng ký.
Tạo video model
rails generate model Video name:string video_file:attachment mp4_file:attachment webm_file:attachment ogg_file:attachment thumbnail:attachment published:boolean likes:integer user:references
File migration sẽ có dạng:
class CreateVideos < ActiveRecord::Migration def change create_table :videos do |t| t.string :name t.attachment :video_file t.attachment :mp4_file t.attachment :webm_file t.attachment :ogg_file t.attachment :thumbnail t.boolean :published t.integer :likes, default: 0 t.references :user, index: true, foreign_key: true t.timestamps null: false end end end
Và giờ sẽ là viết code cho model.
class Video < ActiveRecord::Base # Association declaration belongs_to :user # Paperclip attachments declaration has_attached_file :video_file has_attached_file :mp4_file has_attached_file :webm_file has_attached_file :ogg_file # Styles declaration makes paperclip to use imagemagick to resize image to given size has_attached_file :thumbnail, styles: { medium_nr: "250x150!" } # Paperclip requires to set attachment validators validates_attachment_content_type :video_file, content_type: /Avideo/ validates_attachment_content_type :mp4_file, content_type: /.*/ validates_attachment_content_type :webm_file, content_type: /.*/ validates_attachment_content_type :ogg_file, content_type: /.*/ # We want video model always to have :video_file attachment validates_attachment_presence :video_file # Publish video makes it available def publish! self.published = true save end # Increment likes counter def like! self.likes += 1 save end # Decrease likes counter def dislike! self.likes -= 1 save end # Checks if all formats are already encoded, the simplest way def all_formats_encoded? self.webm_file.path && self.mp4_file.path && self.ogg_file.path ? true : false end end
Thêm relation cho video:
class User < ActiveRecord::Base # Association declaration has_many :videos devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable end
Chạy migration
$ rake db:migrate
Tạo controller và view
Bây giờ, chúng ta sẽ tạo controller và view cho ứng dụng, nên cho phép người dùng tạo tài khoản và đăng nhập. Thêm code cho file app/controllers/application_controller.rb như sau:
class ApplicationController < ActionController::Base protect_from_forgery with: :exception layout :layout_by_resource protected def layout_by_resource if devise_controller? 'devise' else 'application' end end end
Đoạn code trên kiểm tra controller có phải là của devise không, nếu đúng sẽ trả ra devise layout, nếu không sẽ trả ra layout chuẩn.
Tiếp tục với file app/views/layouts/devise.haml.
!!! %html %head %title VideoShrimp = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true = javascript_include_tag 'application', 'data-turbolinks-track' => true = csrf_meta_tags %body{'data-no-turbolink': true} .container .row .col-md-4.col-md-offset-4 %h1.text-center VideoShrimp = yield
Trong devise layout, có các thành phần cơ bản như container, .row, .col-md-4 và .col-md-offset-4 trong bootstrap. Nếu bạn không biết các thành phần này, tôi khuyến khích đọc về Bootstrap Grid System.
Bây giờ tạo users_controller.rb trong app/controllers.
class UsersController < ApplicationController # Checks if user is signed in before running controller, functionality provided by devise before_action :authenticate_user! before_action :set_user, only: [:show] before_action :set_current_user, only: [:edit, :update] def index @@users = User.all end def show end def update respond_to do |format| if @@user.update(user_params) format.html { redirect_to @@user, notice: 'User was successfully updated.' } format.json { render :show, status: :ok, location: @@user } else format.html { render :edit } format.json { render json: @@user.errors, status: :unprocessable_entity } end end end private def set_user @@user = User.find(params[:id]) end def set_current_user @@user = current_user end def user_params params.require(:user).permit(:email) end end
Controller trên dùng để hiển thị hồ sơ người dùng và cho phép thay đổi email. Tiếp tục với video controller
class VideosController < ApplicationController before_action :authenticate_user! before_action :set_video, only: [:show, :edit, :like, :dislike] # All published videos def index @@videos = Video.where(published: true) end def show end def new @@video = Video.new end def edit end def create @@video = Video.new(video_params) respond_to do |format| if @@video.save format.html { redirect_to @@video, notice: 'Video was successfully created.' } format.json { render :show, status: :created, location: @video } else format.html { render :new } format.json { render json: @@video.errors, status: :unprocessable_entity } end end end # Likes video, increment likes count def like @@video.like! end # Dislikes video, increment likes count def dislike @@video.dislike! end private def set_video @@video = Video.find(params[:id]) end def video_params params.require(:video).permit(:video_file, :name) end end
Khá đơn giản, phải không?
Bây giờ xem lại. Hãy loại bỏ file application.html.erb từ app/views/layouts và tạo application.haml.
!!! %html %head %title VideoShrimp = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true = stylesheet_link_tag '//vjs.zencdn.net/5.0.2/video-js.css' = javascript_include_tag 'application', 'data-turbolinks-track' => true = javascript_include_tag '//cdn.pubnub.com/pubnub-dev.js' = csrf_meta_tags %body{'data-no-turbolink': true} - if current_user .navbar.navbar-default.navbar-fixed-top .container .navbar-header = link_to 'VideoShrimp', root_url, class: 'navbar-brand' %ul.nav.navbar-nav.navbar-right %li = link_to 'Browse videos', videos_path %li = link_to 'Upload video', new_video_path %li.dropdown %a.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown", :href => "#", :role => "button"} = current_user.email %span.caret %ul.dropdown-menu %li = link_to 'Profile', current_user %li = link_to 'Edit profile', edit_user_path(current_user) %li.divider{:role => "separator"} %li = link_to 'Log out', destroy_user_session_path, :method => :delete %li#user-notifications.dropdown{"data-pn-auth-key": current_user.pn_auth_key, "data-pn-notification-channel": current_user.notification_channel} %a.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown", :href => "#", :role => "button"} %span.glyphicon.glyphicon-bell %ul.dropdown-menu = yield
Đó là layout trống, với một số điều hướng cơ bản bootstrapped. Như vậy là còn thẻ cho pubnub mà chúng ta sẽ sử dụng để thông báo và videojs để chạy video.
Hãy thêm bootstrap cho stylesheets! Đầu tiên, gỡ bỏ app/assets/stylesheets/application.css và tạo app/assets/stylesheets/application.scss với:
@import "bootstrap-sprockets"; @import "bootstrap"; @import "global"; @import "video";
Tạo global.scss và video.scss cũng tương tự. Bootstrap cần được thêm vào app/assets/javascripts/applications.js, trông sẽ thế này:
//= require jquery //= require jquery_ujs //= require turbolinks //= require bootstrap //= require_tree .
Điều cuối cùng thêm vào assets là sửa global.scss trong app/assets/stylesheets/ và thêm:
body { padding-top: 60px; }
Nhờ đó nội dung trang web sẽ không bị ẩn dưới navigation.
Tất cả các công cụ này sẽ không làm việc? Tất nhiên, bởi vì chúng tôi đã không tạo ra các route. Vì vậy, hãy làm điều đó và chỉnh sửa file config/routes.rb như sau:
require 'sidekiq/web' Rails.application.routes.draw do root 'videos#index' resources :videos get '/videos/:id/like' => 'videos#like' get '/videos/:id/dislike' => 'videos#dislike' devise_for :users resources :users mount Sidekiq::Web => '/sidekiq' end
Vì vậy, trang chủ của ứng dụng sẽ là danh sách các video được tải lên. Resource cho video và user là quá mức cần thiết bởi vì chúng sẽ không được sử dụng một phần của route được tạo ra nhưng nếu bạn muốn mở rộng ứng dụng thì bạn sẽ cần nó. Sidekiq web là interface của sidekiq. Đừng quên require 'sidekiq/web' trên đầu file.
Hãy bắt đầu với user. Dưới app/views/users/ chúng ta sẽ tạo ra hai file: show.haml
.container .row .col-md-12 %h2= @user.email .container .row - @user.videos.each do |video| .col-md-3.thumb.video-thumb{ 'data-video-id': video.id } .likes %span.likes-count= video.likes %span.glyphicon.glyphicon-heart = link_to video, class: 'thumbnail' do = image_tag video.thumbnail.url(:medium_nr)
và edit.haml
.container .row .col-md-6.col-sm-12 %h2 Edit profile = simple_form_for @user do |f| = f.input :email = f.button :submit, class: 'btn-primary', value: 'Update Profile'
Và giờ tạo video views, thêm vào thư mục app/views/videos index.haml
.container .row.video-full{ 'data-video-id': @video.id } .col-md-10 %h1= @video.name %p.small = link_to 'Back to videos' ,videos_path - if @video.all_formats_encoded? %video#my-video.video-js{:controls => "", "data-setup" => "{}", :height => "264", :preload => "auto", :awidth => "640"} %source{:src => @video.mp4_file.url, :type => "video/mp4"} %source{:src => @video.webm_file.url, :type => "video/webm"} %source{:src => @video.ogg_file.url, :type => "video/ogg"} %p.vjs-no-js To view this video please enable JavaScript, and consider upgrading to a web browser that %a{:href => "http://videojs.com/html5-video-support/", :target => "_blank"} supports HTML5 video - else %p Video is still being encoded. .col-md-2 %h1 %span.likes-count= @video.likes %span.glyphicon.glyphicon-heart
Hiển thị video và cho phép thích hay không thích. Nó không cập nhật truy cập bởi vì ta sẽ sử dụng pubnub notification để làm điều đó. new.haml
.container .row .col-md-6.col-sm-12 %h2 Upload new video = simple_form_for @video, html: { multipart: true } do |f| = f.input :name = f.input :video_file, as: :file = f.button :submit, class: 'btn-primary', value: 'Upload Video'
Vậy là đã có một form đơn giản để tải lên video. Sẽ có tên video và nút thêm tệp tin.
Xin chúc mừng! Một số nội dung đã sẵn sàng!
Mã hoá video và hiển thị thông báo
Bây giờ chúng ta cần phải mã hóa video được tải lên, hiển thị chúng cho người dùng và gửi thông báo. Hãy dùng pubnub để làm điều đó.
Đi đến trang web Pubnub và tạo tài khoản mới. Sau khi tạo tài khoản mới, bạn nên thêm ứng dụng mới và tạo ra các khoá cho ứng dụng. Chúng tôi sẽ sử dụng tính năng Storeage & Playback cho lịch sử thông báo và Access manager để làm thông báo tin nhắn.
Bạn sẽ cần phải sao chép các khoá của bạn vào file config/secrets.yml như pubnub_subscribe_key, pubnub_publish_key, pubnub_secret_key và bạn nên tạo cho mình một khoá pubnub_auth_key duy nhất cho máy chủ.
Bây giờ chúng ta cần thêm pubnub.rb trong config/initializers, code đó sẽ chạy khi ứng dụng được khởi động.
pubnub.rb
# Initialize pubnub client with our keys $pubnub = Pubnub.new( subscribe_key: Rails.application.secrets.pubnub_subscribe_key, publish_key: Rails.application.secrets.pubnub_publish_key, secret_key: Rails.application.secrets.pubnub_secret_key, auth_key: Rails.application.secrets.pubnub_auth_key ) # As we have PAM enabled, we have to grant access to channels. # That grants read right to any channel that begins with 'video.' to everyone. $pubnub.grant( read: true, write: false, auth_key: nil, channel: 'video.*', http_sync: true, ttl: 0 ) # That grants read and write right to any channel that begins with 'video.' to this client. $pubnub.grant( read: true, write: true, auth_key: Rails.application.secrets.pubnub_auth_key, channel: 'video.*', http_sync: true, ttl: 0 ) # That grants read and write right to any channel that begins with 'notifications.' to this client. $pubnub.grant( read: true, write: true, auth_key: Rails.application.secrets.pubnub_auth_key, channel: 'notifications.*', http_sync: true, ttl: 0 )
Như bạn thấy, sau khi khởi tạo biến toàn cục $pubnub, chúng tôi đang chạy 3 cấp với ttl: 0 - mà cấp sẽ không bao giờ hết hạn.
Tiếp theo, chúng ta phải cung cấp cho người dùng khả năng để đọc thông báo cá nhân của mình. Chúng tôi sẽ làm điều đó bằng cách tạo ra auth_key duy nhất và cấp quyền đọc kênh thông báo của mình cho mỗi người dùng. Hãy tạo migration:
$ rails generate migration add_pn_auth_key_to_users
Tạo migration như sau:
class AddPnAuthKeyToUsers < ActiveRecord::Migration def change add_column :users, :pn_auth_key, :string end end
Tiếp tục, sửa file user.rb trong devise:
after_create :gen_auth_and_grant_perms def notification_channel "notifications.#{self.id}" end def gen_auth_and_grant_perms generate_pn_auth! $pubnub.grant( channel: notification_channel, auth_key: pn_auth_key, ttl: 0, http_sync: true ) end def generate_pn_auth self.pn_auth_key = SecureRandom.hex end def generate_pn_auth! self.generate_pn_auth save end
Nó sẽ chạy method gen_auth_and_grant_perms sau khi người dùng được tạo ra. Người dùng sẽ nhận được auth_key duy nhất của mình sẽ được sử dụng bởi javascript client và được quyền để đọc trên kênh riêng của mình.
Bây giờ, ta sẽ sửa Video model. Chỉnh sửa video.rb và thực hiện thay đổi các method publish!, like!, dislike!.
# Publish video makes it available def publish! self.published = true save $pubnub.publish(channel: "video.#{id}", message: {event: :published}, http_sync: true) $pubnub.publish(channel: self.user.notification_channel, message: {event: :published, scope: :videos, id: self.id, name: name.truncate(20)}, http_sync: true) end # Increment likes counter def like! self.likes += 1 save $pubnub.publish(channel: "video.#{id}", message: {event: :liked}, http_sync: true) end # Decrease likes counter def dislike! self.likes -= 1 save $pubnub.publish