12/08/2018, 14:07

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
                                          
0