rails 创建_使用Rails和Icecast创建在线流媒体广播

rails 创建

Hello and welcome to this article! Today I would like to talk about creating an online streaming radio with the Ruby on Rails framework. This task is not that simple but it appears that by selecting the proper tools it can be solved without any big difficulties. By applying the concepts described in this tutorial you will be able to create your own radio station, share it with the world and stream any music you like.

您好,欢迎阅读本文! 今天,我想谈谈使用Ruby on Rails框架创建在线流媒体广播。 这项任务并不是那么简单,但是似乎可以通过选择合适的工具来解决,而不会遇到很大的困难。 通过应用本教程中描述的概念,您将能够创建自己的广播电台,与世界分享,并播放您喜欢的任何音乐。

Our main technologies for today are going to be Rails (what a surprise!), Icecast streaming server, Sidekiq (to run background jobs), ruby-shout (to manage Icecast) and Shrine (to perform file uploading). In this article we will discuss the following topics:

今天,我们的主要技术将是Rails (令人惊讶!), Icecast流服务器Sidekiq (用于运行后台作业), ruby-shout (用于管理Icecast)和Shrine (用于执行文件上传)。 在本文中,我们将讨论以下主题:

  • Creating a simple admin page to manage songs that will be streamed on the radio

    创建一个简单的管理页面来管理将在广播中流播的歌曲
  • Adding file uploading functionality with the help of Shrine

    在Shrine的帮助下添加文件上传功能
  • Storing the uploaded files on Amazon S3

    在Amazon S3上存储上传的文件
  • Installing and configuring Icecast

    安装和配置Icecast
  • Creating a background job powered by Sidekiq

    创建由Sidekiq支持的后台作业
  • Using ruby-shout gem to perform the actual streaming and managing the Icecast server

    使用ruby-shout gem执行实际的流传输并管理Icecast服务器
  • Displaying the HTML5 player and connecting to the stream

    显示HTML5播放器并连接到流
  • Displaying additional information about the currently played track with the help of XSL template and JSONP callback

    在XSL模板和JSONP回调的帮助下显示有关当前播放曲目的其他信息
  • Fetching and displaying even more meta information about the song (like its bitrate)

    获取并显示有关歌曲的更多元信息(例如比特率)

Sounds promising, eh? If you would like to see how the result may look like, visit Kalinka.fm — an online radio created by me that streams Russian national music. Of course, our demo application won't be that stylish, but the underlying concepts will be absolutely the same.

So, let's get started, shall we?

听起来不错,是吗? 如果您想看一下结果如何,请访问Kalinka.fm ,这是由我创建的在线广播,播放俄罗斯民族音乐。 当然,我们的演示应用程序不会那么时尚,但是其基本概念将完全相同。

奠定基础 ( Laying Foundations )

We, as total dev geeks, are listening to a geek music, right (whatever it means)? So, the radio that we are going to create will, of course, be for the geeks only. Therefore create a new Rails application:

作为开发极客,我们正在听极客音乐,对(意味着什么)? 因此,我们将要创建的收音机当然仅适用于极客。 因此,创建一个新的Rails应用程序:

rails new GeekRadio -T

I am going to use Rails 5.1 for this demo. The described concepts can be applied to earlier versions as well, but some commands may differ (for example, you should write rake generate, not rails generate in Rails 4).

我将在这个演示中使用Rails 5.1。 所描述的概念也可以应用于早期版本,但是某些命令可能有所不同(例如,您应该编写rake generate ,而不是Rails 4中的rails generate )。

First of all, we will require a model called Song. The corresponding songs table is going to store all the musical tracks played on our radio. The table will have the following fields:

首先,我们将需要一个名为Song的模型。 相应的songs表将存储我们收音机中播放的所有音乐曲目。 该表将具有以下字段:

  • title (string)

    titlestring
  • singer (string)

    singerstring
  • track_data (text) — will be utilized by the Shrine gem which is going to take care of the file uploading functionality. Note that this field must have a _data postfix as instructed by the docs.

    track_datatext )—将由Shrine gem使用,它将负责文件上传功能。 请注意,该字段必须具有docs指示_data后缀。

Run the following commands to create and apply a migration as well as the corresponding model:

运行以下命令以创建和应用迁移以及相应的模型:

rails g model Song title:string singer:string track_data:text
rails db:migrate

创建控制器 (Creating Controller)

Now let's create a controller to manage our songs:

现在,让我们创建一个控制器来管理我们的歌曲:

# app/controllers/songs_controller.rb

class SongsController < ApplicationController
  layout 'admin'

  def index
    @songs = Song.all
  end

  def new
    @song = Song.new
  end

  def create
    @song = Song.new song_params
    if @song.save
      redirect_to songs_path
    else
      render :new
    end
  end

  private

  def song_params
    params.require(:song).permit(:title, :singer, :track)
  end
end

This is a very trivial controller, but there are two things to note here:

这是一个非常琐碎的控制器,但是这里有两点需要注意:

  • We are using a separate admin layout. This is because the main page of the website (with the actual radio player) will need to contain a different set of scripts and styles.

    我们正在使用单独的admin布局。 这是因为网站的主页(带有实际的广播播放器)将需要包含一组不同的脚本和样式。
  • Even though our column is named track_data, we are permitting the :track attribute inside the song_params private method. This is perfectly okay with Shrine — track_data will only be used internally by the gem.

    添加单独的布局 (Adding a Separate Layout)

    现在,让我们创建一个admin layout which is going to include a separate admin.js script and have no styling (though, of course, you are free to style it anything you like):

    即使我们的列名为track_data ,我们仍允许在song_params私有方法内使用:track属性。 对于Shrine来说,这是完全可以的track_data仅在gem内部使用。 admin布局,其中将包括一个单独的admin.js脚本,并且没有样式(尽管您当然可以随意设置其样式):
<!-- app/views/layouts/admin.html.erb -->

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>GeekRadio Admin</title>
  <%= csrf_meta_tags %>

  <%= javascript_include_tag 'admin', 'data-turbolinks-track': 'reload' %>
</head>

<body>
<%= yield %>
</body>
</html>

The app/assets/javascripts/admin.js file is going to have the following contents:

app/assets/javascripts/admin.js文件将具有以下内容:

// app/assets/javascripts/admin.js
//= require rails-ujs
//= require turbolinks

So, we are adding the built-in Unobtrusive JavaScript adapter for Rails and Turbolinks to make our pages load much faster.

因此,我们为Rails和Turbolinks添加了内置的Unobtrusive JavaScript适配器,以使我们的页面加载更快。

Also note that the admin.js should be manually added to the precompile array. Otherwise, when you deploy to production this file and all the required libraries won't be properly prepared and you will end up with an error:

还要注意,应该将admin.js手动添加到precompile数组中。 否则,当您将该产品部署到生产环境中时,所有必需的库将无法正确准备,并且最终将导致错误:

# config/initializers/assets.rb
# ...
Rails.application.config.assets.precompile += %w( admin.js )

创建视图并添加路线 (Creating Views and Adding Routes)

Now it is time to add some views and partials. Start with the index.html.erb:

现在是时候添加一些视图和局部视图了。 从index.html.erb开始:

<!-- app/views/songs/index.html.erb -->
<h1>Songs</h1>

<p><%= link_to 'Add song', new_song_path %></p>

<%= render @songs %>

In order for the render @songs construct to work properly we need to create a _song.html.erb partial that will be used to display each song from the collection:

为了使render @songs构造正常工作,我们需要创建一个_song.html.erb部分,该部分将用于显示集合中的每首歌曲:

<!-- app/views/songs/_song.html.erb -->
<div>
  <p><strong><%= song.title %></strong> by <%= song.singer %></p>
</div>
<hr>

Now the new view:

现在是new视图:

<!-- app/views/songs/new.html.erb -->
<h1>Add song</h1>

<%= render 'form', song: @song %>

And the _form partial:

_form部分:

<!-- app/views/songs/_form.html.erb -->
<%= form_with model: song do |f| %>
  <div>
    <%= f.label :title %>
    <%= f.text_field :title %>
  </div>

  <div>
    <%= f.label :singer %>
    <%= f.text_field :singer %>
  </div>

  <div>
    <%= f.label :track %>
    <%= f.file_field :track %>
  </div>

  <%= f.submit %>
<% end %>

Note that here I am also using the :track attribute, not :track_data, just like in the SongsController.

请注意,这里也使用了:track属性,而不是:track_data ,就像SongsController

Lastly, add the necessary routes:

最后,添加必要的路线:

# config/routes.rb
# ...
resources :songs, only: %i[new create index]

添加上传功能 (Adding the Upload Functionality)

The next step involves integrating the Shrine gem that will allow to easily provide support for file uploads. After all, the site's administrator should have a way to add some songs to be played on the radio. Surely, there are a bunch of other file uploading solutions, but I have chosen Shrine because of the following reasons:

下一步涉及整合Shrine gem ,以便轻松地为文件上传提供支持。 毕竟,网站的管理员应该可以添加一些要在广播中播放的歌曲。 当然,还有很多其他文件上传解决方案,但由于以下原因,我选择了Shrine:

  • It is actively maintained

    积极维护
  • It is really easy to get started

    上手真的很容易
  • I has a very nice plugin system that allows you to cherry-pick only the features that you actually need

    我有一个非常不错的插件系统,可让您仅挑选您实际需要的功能
  • It is really powerful and allows to easily customize the process of file uploading (for example, you can easily add metadata for each file)

    它非常强大,可以轻松自定义文件上传过程(例如,您可以轻松地为每个文件添加元数据)

But still, if for some reason you prefer other file uploading solution — no problem, the core functionality of the application will be nearly the same.

但是,尽管如此,如果出于某种原因您更喜欢其他文件上传解决方案-没问题,该应用程序的核心功能将几乎相同。

So, drop two new gems into the Gemfile:

因此,将两个新的gem放入Gemfile

# Gemfile
# ...
gem 'shrine'
gem "aws-sdk-s3", '~> 1.2'

Note that we are also adding the aws-sdk-s3 gem because I'd like to store our files on Amazon S3 right away. You may ask why haven't I included aws-sdk gem instead? Well, recently this library has undergone some major changes and various modules are now split into different gems. This is actually a good thing because you can choose only the components that will be really used.

请注意,我们还添加了aws-sdk-s3 gem,因为我想立即将文件存储在Amazon S3上。 您可能会问为什么我没有包含aws-sdk gem ? 好吧,最近,该库进行了一些重大更改,并且各个模块现在分为不同的gem。 这实际上是一件好事,因为您只能选择将真正使用的组件。

Install the gems:

安装宝石:

bundleinstall

Now create an initializer with the following contents:

现在创建具有以下内容的初始化程序:

# config/initializers/shrine.rb

require "shrine"
require "shrine/storage/file_system"
require "shrine/storage/s3"

s3_options = {
    access_key_id:      ENV['S3_KEY'],
    secret_access_key:  ENV['S3_SECRET'],
    region:             ENV['S3_REGION'],
    bucket:             ENV['S3_BUCKET']
}

Shrine.storages = {
    cache: Shrine::Storage::FileSystem.new("tmp/uploads"),
    store: Shrine::Storage::S3.new(upload_options: {acl: "public-read"}, prefix: "store",
                                   **s3_options),
}

Shrine.plugin :activerecord

Here I am using ENV to store my keys and other S3 settings because I don't want them to be publically exposed. I will explain in a moment how to populate ENV with all the necessary values.

在这里,我使用ENV来存储我的密钥和其他S3设置,因为我不想公开它们。 我将在稍后说明如何使用所有必要的值填充ENV

Shrine has two storage locations: one for caching (file system, in our case) and one for permanent storage (S3). Apart from keys, region and bucket, we are specifying two more settings for S3 storage:

Shrine有两个存储位置:一个用于缓存(在我们的示例中为文件系统)和一个用于永久存储(S3)。 除了键,区域和存储桶,我们还为S3存储指定了两个设置:

  • upload_options set ACL (access control list) settings for the uploaded files. In this case we are providing the public-read permission which means that all the files can be read by everyone. After all, that's a public radio, isn't it?

    upload_options设置上载文件的ACL(访问控制列表)设置。 在这种情况下,我们提供了public-read权限,这意味着所有人都可以读取所有文件。 毕竟那是公共广播电台,不是吗?
  • prefix means that all the uploaded files will end up in a store folder. So, for example, if your bucket is named scotch, the path to the file will be something like https://scotch.s3.amazonaws.com/store/your_file.mp3

    prefix意味着所有上传的文件将最终storestore文件夹中。 因此,例如,如果您的存储桶名为scotch ,则文件的路径将类似于https://scotch.s3.amazonaws.com/store/your_file.mp3

Lastly, Shrine.plugin :activerecord enables support for ActiveRecord. Shrine also has a :sequel plugin by the way.

最后, Shrine.plugin :activerecord启用对ActiveRecord的支持 。 顺便说一下,Shrine也有一个:sequel插件。

Now, what about the environment variables that will contain S3 settings? You may load them with the help of dotenv-rails gem really quickly. Add a new gem:

现在,将包含S3设置的环境变量呢? 您可以很快地在dotenv-rails gem的帮助下加载它们。 添加一个新的宝石:

# Gemfile

gem 'dotenv-rails'

Install it:

安装它:

bundleinstall

Then create an .env file in the root of your project:

然后在项目的根目录中创建一个.env文件:

# .env
S3_KEY: YOUR_KEY
S3_SECRET: YOUR_SECRET
S3_BUCKET: YOUR_BUCKET
S3_REGION: YOUR_REGION

This file should be ignored by Git because you do not want to push it accidently to GitHub or Bitbucket. Therefore let's exclude it from version control by adding the following line to .gitignore:

Git应该忽略该文件,因为您不想将其意外推送到GitHub或Bitbucket。 因此,通过在.gitignore添加以下行,让我们将其从版本控制中排除:

# .gitignore
.env

Okay, we've provided some global configuration for Shrine and now it is time to create a special uploader class:

好的,我们已经为Shrine提供了一些全局配置,现在是时候创建一个特殊的上载器类了

# app/uploaders/song_uploader.rb

class SongUploader < Shrine
end

Inside the uploader you may require additional plugins and customize everything as needed.

在上传器内部,您可能需要其他插件并根据需要自定义所有内容。

Lastly, include the uploader in the model and also add some basic validations:

最后,在模型中包含上载器,并添加一些基本验证:

# app/models/song.rb

class Song < ApplicationRecord
  include SongUploader[:track]

  validates :title, presence: true
  validates :singer, presence: true
  validates :track, presence: true
end

Note that Shrine has a bunch of special validation helpers that you may utilize as needed. I won't do it here because, after all, this is not an article about Shrine.

请注意,Shrine有很多特殊的验证助手 ,您可以根据需要使用它们。 我这里不会做这件事,因为毕竟这不是关于神社的文章。

Also, for our own convenience, let's tweak the _song.html.erb partial a bit to display a public link to the file:

另外,为方便起见,让我们对_song.html.erb部分进行一些微调,以显示该文件的公共链接:

<!-- app/views/songs/_song.html.erb -->
<div>
  <p><strong><%= song.title %></strong> by <%= song.singer %></p>
  <%= link_to 'Listen', song.track.url(public: true) %> <!-- add this line -->
</div>
<hr>

Our job in this section is done. You may now boot the server by running

我们在本节中的工作已经完成。 您现在可以通过运行启动服务器

rails s

and try uploading some of your favourite tracks.

并尝试上传一些您喜欢的曲目。

冰铸 ( Icecast )

We are now ready to proceed to the next, a bit more complex part of this tutorial, where I'll show you how to install and configure Icecast.

现在,我们可以继续进行本教程的下一个更复杂的部分,在该教程中,我将向您展示如何安装和配置Icecast。

As mentioned above, Icecast is a streaming media server that can work with both audio and video, supporting Ogg, Opus, WebM and MP3 streams. It works on all major operating systems and provides all the necessary tools for us to quite easily enable streaming radio functionality, so we are going to use it in this article.

如上所述, Icecast是一种流媒体服务器,可以同时处理音频和视频,并支持Ogg,Opus,WebM和MP3流。 它可在所有主要操作系统上运行,并为我们提供了所有必需的工具,以使我们能够轻松地启用流广播功能,因此在本文中我们将使用它。

First navigate to the Downloads section and pick the version that works for you (at the time of writing this article the newest version was 2.4.3). Installation instructions for Linux users can be found on this page, whereas Windows users can simply use the installation wizard. Note, however, that Icecast has a bunch of prerequisites, so make sure you have all the necessary components on your PC.

首先,导航至“ 下载”部分,然后选择适合您的版本(在撰写本文时,最新版本为2.4.3)。 可以在此页面上找到Linux用户的安装说明,而Windows用户只需使用安装向导即可。 但是请注意,Icecast具有许多先决条件 ,因此请确保您的PC上具有所有必需的组件。

Before booting the server, however, we need to tweak some configuration options. All Icecast global configuration is provided in the icecast.xml file in the root of the installation directory. There are lots of settings that you can modify but I will list only the ones that we really require:

但是,在引导服务器之前,我们需要调整一些配置选项。 在安装目录根目录的icecast.xml文件中提供了所有Icecast全局配置。 您可以修改很多设置 ,但仅列出我们真正需要的设置:

<!-- installation_path/icecast/icecast.xml -->

<hostname>localhost</hostname>

<authentication>
    <!-- Sources log in with username 'source' -->
    <source-password>PASSWORD</source-password>

    <!-- Admin logs in with the username given below -->
    <admin-user>admin</admin-user>
    <admin-password>PASSWORD3</admin-password>
</authentication>

<shoutcast-mount>/stream</shoutcast-mount>

<listen-socket>
    <port>35689</port>
</listen-socket>

<http-headers>
    <header name="Access-Control-Allow-Origin" value="*" />
</http-headers>

So, we need to specify the following options:

因此,我们需要指定以下选项:

  • Hostname that Icecast will utilize.

    Icecast将使用的主机名。
  • Two passwords: one for the "source" (that will be used later to manipulate Icecast with some Ruby code) and one for the admin (the guy who can use the web interface to view the status of the server)

    两个密码:一个用于“源”密码(稍后将使用一些Ruby代码来使用Icecast),另一个用于管理员(可以使用Web界面查看服务器状态的人)。
  • Mount path to /stream.

    将路径安装到/stream
  • Port to listen to (35689). It means that in order to access our radio we will need to use the http://localhost:35689/stream URL.

    收听端口( 35689 )。 这意味着要访问我们的广播电台,我们需要使用http://localhost:35689/stream URL。
  • Access-Control-Allow-Origin header to easily embed the stream on other websites.

    Access-Control-Allow-Origin标头可轻松将流嵌入其他网站。

After you done with the settings, Icecast can be started by running the icecast file from the command line interface (for Windows there is an icecast.bat file).

You may also visit the
http://localhost:35689 to see a pretty minimalistic web interface.
Great, now our Icecast sever is up and running but we need to manipulate it somehow and perform the actual streaming. Let's proceed to the next section and take care of that!

设置完成后,可以通过从命令行界面运行icecast文件来启动Icecast(对于Windows,存在icecast.bat文件)。 http://localhost:35689以看到一个非常简约的Web界面。

请注意,要访问“管理”部分,您将需要提供管理员密码。

Sidekiq和Ruby-Shout ( Sidekiq and Ruby-Shout )

Icecast provides bindings for a handful of popular languages, including Python, Java and Ruby. The gem for Ruby is called ruby-shout and we are going to utilize it in this article. Ruby-shout allows us to easily connect to Icecast, change information about the server (like its name or genre of the music) and, of course, perform the actual streaming.

Icecast 提供了几种流行语言的绑定 ,包括Python,Java和Ruby。 Ruby的宝石称为ruby-shout ,我们将在本文中加以利用。 Ruby-shout允许我们轻松连接到Icecast,更改有关服务器的信息(例如其名称或音乐流派),当然还可以执行实际的流式传输。

Ruby-shout relies on the libshout base library that can also be downloaded from the official website. The problem is that this library is not really designed to work with Windows (there might be a way to compile it but I have not found it). So, if you are on Windows, you'll need to stick with Cygwin. Install libshout from there and perform all the commands listed below from the Cygwin CLI.

Ruby-shout依赖libshout基础库,该库也可以从官方网站下载。 问题在于该库并不是真正设计用于Windows( 可能有一种编译它的方法,但我没有找到它)。 因此,如果您使用的是Windows,则需要坚持使用Cygwin 。 从那里安装libshout并从Cygwin CLI执行下面列出的所有命令。

Drop ruby-shout into the Gemfile:

将ruby-shout放到Gemfile

# Gemfile
# ...
gem 'ruby-shout'

We also need decide what tool are we going to use to perform streaming in the background. There are multiple possible ways to solve this task but I propose to stick with a popular gem called Sidekiq that makes working with background jobs a breeze:

我们还需要确定要使用什么工具在后台执行流式传输。 有多种可能的方法可以解决此任务,但我建议坚持使用一种名为Sidekiq的流行宝石,它使轻而易举地处理后台作业变得很容易:

# Gemfile
# ...
gem 'sidekiq'

Now install everything:

现在安装所有内容:

bundleinstall

One thing to note is that Sidekiq relies on Redis, so don't forget to install and run it as well.

需要注意的一件事是,Sidekiq依赖Redis ,因此请不要忘记同时安装和运行它。

So, the idea is quite simple:

所以,这个想法很简单:

  • We are going to have a special worker running in the background.

    我们将在后台运行一个特殊的工人。
  • This worker will be managed by Sidekiq and started from an initializer file (though in production it is advised to create a separate service).

    该工作程序将由Sidekiq管理,并从初始化程序文件启动(尽管建议在生产环境中创建一个单独的服务)。
  • The worker will utilize ruby-shout to connect to the server, stream the uploaded tracks and update information about the currently played song (like its title and performer).

    工作人员将使用ruby-shout连接到服务器,传输上载的曲目并更新有关当前播放的歌曲的信息(例如其标题和表演者)。
  • The currently played song will have a current attribute set to true.

    当前播放的歌曲的current属性设置为true

Before creating our worker, let's generate a new migration to add a current field to the songs table:

在创建工作程序之前,让我们生成一个新的迁移,以将current字段添加到songs表中:

rails g migration add_current_to_songs current:boolean:index

Tweak the migration a bit to make the current attribute default to false:

稍微调整迁移,以使current属性默认为false

# db/migrate/xyz_add_current_to_songs.rb
# ...
add_column :songs, :current, :boolean, default: false

Apply the migration:

应用迁移:

rails db:migrate

Now create a new Sidekiq worker:

现在创建一个新的Sidekiq工作者:

# app/workers/radio_worker.rb

require 'shout'
require 'open-uri'
class RadioWorker
  include Sidekiq::Worker
  def perform(*_args)
  end
end

shout is our ruby-shout gem, whereas open-uri will be used to open the tracks uploaded to Amazon.

shout是我们的ruby-shout瑰宝,而open-uri将用于打开上传到Amazon的曲目。

Inside the perform action we firstly need to connect to Icecast and provide some settings:

perform动作内部,我们首先需要连接到Icecast并提供一些设置:

# app/workers/radio_worker.rb
# ...
def perform(*_args)
    s = Shout.new
    s.mount = "/stream"
    s.charset = "UTF-8"
    s.port = 35689
    s.host = 'localhost'
    s.user = ENV['ICECAST_USER']
    s.pass = ENV['ICECAST_PASSWORD']
    s.format = Shout::MP3
    s.description = 'Geek Radio'
    s.connect
end

The username and the password are taken from the ENV so add two more line to the .env file:

用户名和密码取自ENV因此在.env文件中再添加两行:

# .env
# ...
ICECAST_USER: source
ICECAST_PASSWORD: PASSWORD_FOR_SOURCE

The username is always source whereas the password should be the same as the one you specified in the icecast.xml inside the source-password tag.

用户名始终是source用户名,而密码应与您在source-password标记内的icecast.xml指定的用户名相同。

Now I'd like to keep track of the previously played song and iterate over all the songs in an endless loop:

现在,我想跟踪先前播放的歌曲,并以无限循环的方式遍历所有歌曲:

# app/workers/radio_worker.rb
# ...
def perform(*_args)
    # ...
    s.connect
    prev_song = nil

    loop do
        Song.where(current: true).each do |song|
            song.toggle! :current
        end
        Song.order('created_at DESC').each do |song|
            prev_song.toggle!(:current) if prev_song
            song.toggle! :current
        end
    end

    s.disconnect
end

Before iterating over the songs, we are making sure the current attribute is set to false for all the songs (just to be on a safe side, because the worker might crash). Then we take one song after another and mark it as current.

在遍历歌曲之前,我们确保所有歌曲的current属性都设置为false出于安全考虑,因为工作人员可能会崩溃)。 然后,我们接一首歌,将其标记为current

Now let's open the track file, add information about the currently played song and perform the actual streaming:

现在,让我们打开轨道文件,添加有关当前播放的歌曲的信息并执行实际的流式传输:

# app/workers/radio_worker.rb
# ...
def perform(*_args)
    # ...
    loop do
        # ...
        Song.order('created_at DESC').each do |song|
            prev_song.toggle!(:current) if prev_song
            song.toggle! :current

            open(song.track.url(public: true)) do |file|
                m = ShoutMetadata.new
                m.add 'filename', song.track.original_filename
                m.add 'title', song.title
                m.add 'artist', song.singer
                s.metadata = m

                while data = file.read(16384)
                    s.send data
                    s.sync
                end
            end
            prev_song = song
        end
    end
end

Here we are opening the track file using the open-uri library and set some metadata about the currently played song. original_filename is the method provided by Shrine (it stored the original filename internally), whereas title and singer are just the model's attributes. Then we are reading the file (16384 is the block size) and send portions of it to Icecast.

在这里,我们使用open-uri库打开轨道文件,并设置一些有关当前播放歌曲的元数据。 original_filename是Shrine提供的方法(它在内部存储了原始文件名),而titlesinger只是模型的属性。 然后,我们读取文件(块大小为16384 ),并将其部分发送给Icecast。

Here is the final version of the worker:

这是工作程序的最终版本:

# app/workers/radio_worker.rb

require 'shout'
require 'open-uri'
class RadioWorker
  include Sidekiq::Worker
  def perform(*_args)
    prev_song = nil
    s = Shout.new # ruby-shout instance
    s.mount = "/stream" # our mountpoint
    s.charset = "UTF-8"
    s.port = 35689 # the port we've specified earlier
    s.host = 'localhost' # hostname
    s.user = ENV['ICECAST_USER'] # credentials
    s.pass = ENV['ICECAST_PASSWORD']
    s.format = Shout::MP3 # format is MP3
    s.description = 'Geek Radio' # an arbitrary name
    s.connect 

    loop do # endless loop to peform streaming
      Song.where(current: true).each do |song| # make sure all songs are not `current`
        song.toggle! :current
      end
      Song.order('created_at DESC').each do |song|
        prev_song.toggle!(:current) if prev_song # if there was a previously played song, set `current` to `false`
        song.toggle! :current # a new song is playing so it is `current` now

        open(song.track.url(public: true)) do |file| # open the public URL
          m = ShoutMetadata.new # add metadata
          m.add 'filename', song.track.original_filename
          m.add 'title', song.title
          m.add 'artist', song.singer
          s.metadata = m

          while data = file.read(16384) # read the portions of the file
            s.send data # send portion of the file to Icecast
            s.sync
          end
        end
        prev_song = song # the song has finished playing
      end
    end # end of the endless loop

    s.disconnect # disconnect from the server
  end
end

In order to run this worker upon server boot, create a new initializer:

为了在服务器启动时运行此工作程序,请创建一个新的初始化程序:

# config/initializers/sidekiq.rb

RadioWorker.perform_async

To see everything it in action, make sure Icecast is started, then boot Sidekiq:

要查看运行中的所有内容,请确保已启动Icecast,然后启动Sidekiq:

bundleexec sidekiq

and start the server:

并启动服务器:

rails s

In the Sidekiq's console you should see a similar output:

在Sidekiq的控制台中,您应该看到类似的输出:

The line

线

2017-11-01T17:30:03.727Z 7752 TID-5xikpsg RadioWorker JID-57462d72129bbd0655d2e853 INFO: start

means that our background job is running which means that the radio is now live! Try visiting http://localhost:35689/stream — you should hear your music playing.

意味着我们的后台作业正在运行,这意味着收音机已经开始播放! 尝试访问http:// localhost:35689 / stream-您应该会听到音乐播放。

Nice, the streaming functionality is done! Our next task is to display the actual player and connect to the stream, so proceed to the next part.

不错,流功能已完成! 我们的下一个任务是显示实际的播放器并连接到流,因此继续进行下一部分。

连接到流 ( Connecting to the Stream )

I would like to display radio player on the root page of our website. Let's create a new controller to manage semi-static website pages:

我想在我们网站的根页面上显示广播播放器。 让我们创建一个新的控制器来管理半静态网站页面:

# app/controllers/pages_controller.rb

class PagesController < ApplicationController
end

Add a new root route:

添加新的根路由:

# config/routes.rb
# ...
root 'pages#index'

Now create a new view with an audio element:

现在创建一个带有audio元素的新视图:

<!-- app/views/pages/index.html.erb -->
<h1>Geek Radio</h1>

<div id="js-player-wrapper">
  <audio id="js-player" controls>
    Your browser does not support the <code>audio</code> element. Seems like you are not a geek :(
  </audio>
</div>

Of course, audio will render a very minimalistic player with a very limited set of customization options. There are a handful of third-party libraries allowing to replace the built-in player with something more customizable, but I think for the purposes of this article the generic solution will work just fine.

当然, audio将通过一组非常有限的自定义选项来渲染一个非常简约的播放器。 有少数第三方库允许使用更可定制的功能替换内置播放器,但我认为就本文而言,通用解决方案将可以正常工作。

Now it is time to write some JavaScript code. I would actually like to stick with the jQuery to simplify manipulating the elements and performing AJAX calls later, but the same task can be solved with vanialla JS. Add a new gem to the Gemfile (newer versions of Rails do no have jquery-rails anymore):

现在该写一些JavaScript代码了。 我实际上想坚持使用jQuery简化操作元素并稍后执行AJAX调用,但是可以使用vanialla JS解决相同的任务。 将新的gem添加到Gemfile (新版本的Rails不再具有jquery-rails了):

# Gemfile
gem 'jquery-rails'

Install it:

安装它:

bundleinstall

Tweak the app/assets/javascripts/application.js file to include jQuery and our custom player.coffee which will be created in a moment:

调整app/assets/javascripts/application.js文件,使其包含jQuery和我们即将创建的自定义player.coffee

// app/assets/javascripts/application.js
//= require jquery3
//= require player

Note that we do not need rails-ujs or Turbolinks here because our main section of the site is very simple and consists of only one page.

请注意,我们在这里不需要rails-ujs或Turbolinks,因为我们站点的主要部分非常简单,仅包含一页。

Now create the app/assets/javascripts/player.coffee file (be very careful about indents as CoffeeScript relies heavily on them):

现在创建app/assets/javascripts/player.coffee文件(对于缩进非常小心,因为CoffeeScript严重依赖缩进):

# app/assets/javascripts/player.coffee
$ ->
  wrapper = $('#js-player-wrapper')
  player = wrapper.find('#js-player')
  player.append '<source src="http://localhost:35689/stream">'
  player.get(0).play()

I've decided to be more dynamic here but you may add the source tag right inside your view. Note that in order to manipulate the player, you need to turn jQuery wrapped set to a JavaScript node by saying player.get(0).

我已经决定在此处提高动态性,但是您可以在视图内部添加source代码。 请注意,为了操纵播放器,您需要通过说出player.get(0)来将jQuery包装集设置为JavaScript节点。

Visit the http://localhost:3000 and make sure the player is there and geek radio is actually live!

访问http://localhost:3000并确保播放器在那里并且极客广播实际上已经直播了!

显示有关歌曲的信息 ( Displaying Information About the Song )

The radio is now working, but the problem is the users do not see what track is currently playing. Some people may not even care about this, but when I hear some good composition, I really want to know who sings it. So, let's introduce this functionality now.

收音机现在正在工作,但问题是用户看不到当前正在播放的曲目。 有些人甚至可能并不在意,但是当我听到一些好的作曲时,我真的很想知道是谁唱歌。 因此,让我们现在介绍此功能。

What's interesting, there is no obvious way to see the name of the currently played song even though this meta information is added by us inside the RadioWorker. It appears that the easiest solution would be to stick with good old XSL templates (well, maybe they are not that good actually). Go to the directory where Icecast is installed, open the web folder and create a new .xsl file there, for example info.xsl. Now it's time for some ugly-looking code:

有趣的是,即使此元信息是我们在RadioWorker内部添加的,也没有明显的方法来查看当前播放的歌曲的RadioWorker 。 看来, 最简单的解决方案是坚持使用良好的旧XSL模板 (嗯,也许它们实际上并不是那么好)。 转到安装Icecast的目录,打开web文件夹并在其中创建一个新的.xsl文件,例如info.xsl 。 现在是时候编写一些难看的代码了:

<!-- installation_path/icecast/web/info.xsl -->

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
    <xsl:output omit-xml-declaration="yes" method="text" indent="no" media-type="text/javascript" encoding="UTF-8"/>
    <xsl:strip-space elements="*"/>
    <xsl:template match="/icestats">
        parseMusic({
        <xsl:for-each select="source">
            "<xsl:value-of select="@mount"/>":
            {
                "server_name":"<xsl:value-of select="server_name"/>",
                "listeners":"<xsl:value-of select="listeners"/>",
                "description":"<xsl:value-of select="server_description" />",
                "title":"<xsl:if test="artist"><xsl:value-of select="artist" /> - </xsl:if><xsl:value-of select="title" />",
                "genre":"<xsl:value-of select="genre" />"
            }
            <xsl:if test="position() != last()"><xsl:text>,</xsl:text></xsl:if>
        </xsl:for-each>
        });
    </xsl:template>
</xsl:stylesheet>

You might ask: "What's going on here?". Well, this file generates a JSONP callback parseMusic for us that displays information about all the streams on the Icecast server. Each stream has the following data:

您可能会问:“这是怎么回事?”。 好了,这个文件为我们生成了一个JSONP回调 parseMusic ,它显示有关Icecast服务器上所有流的信息。 每个流具有以下数据:

  • server_name

    server_name
  • listeners — Icecast updates listeners count internally

    listeners — Icecast更新侦听器在内部进行计数
  • description

    description
  • title — displays both title and the singer's name (if it is available)

    title -同时显示标题和歌手的姓名(如果有)
  • genre

    genre

Now reboot your Icecast and navigate to http://localhost:35689/info.xsl. You should see an output similar to this:

现在重新启动Icecast并导航到http://localhost:35689/info.xsl 。 您应该看到类似于以下的输出:

parseMusic({
    "/stream":
    {
        "server_name":"no name",
        "listeners":"0",
        "description":"Geek Radio",
        "title":"some title - some singer",
        "genre":"various"
    }
});

As you see, the mountpoint's URL (/stream in this case) is used as the key. The value is an object with all the necessary info. It means that the JSONP callback is available for us!

如您所见,安装点的URL(在本例中为/stream )用作键。 该值是具有所有必要信息的对象。 这意味着我们可以使用JSONP回调!

Tweak the index view by adding a new section inside the #js-player-wrapper block:

通过在#js-player-wrapper块内添加一个新部分来调整index视图:

<!-- app/views/pages/index.html.erb -->
<div id="js-player-wrapper">
  <p>
    Now playing: <strong class="now-playing"></strong>
    Listeners: <span class="listeners"></span>
  </p>

  <audio id="js-player" controls>
    Your browser does not support the <code>audio</code> element. Seems like you are not a geek :(
  </audio>
</div>

Next, define two new variables to easily change the contents of the corresponding elements later:

接下来,定义两个新变量,以便以后轻松更改相应元素的内容:

# app/assets/javascripts/player.coffee 
$ ->
  wrapper = $('#js-player-wrapper')
  player = wrapper.find('#js-player')
  now_playing = wrapper.find('.now-playing')
  listeners = wrapper.find('.listeners')
  # ...

Create a function to send an AJAX request to our newly added URL:

创建一个函数以将AJAX请求发送到我们新添加的URL:

# app/assets/javascripts/player.coffee
$ ->
  # ... your variables defined here

  updateMetaData = ->
    url = 'http://localhost:35689/info.xsl'
    mount = '/stream'

    $.ajax
      type: 'GET'
      url: url
      async: true
      jsonpCallback: 'parseMusic'
      contentType: "application/json"
      dataType: 'jsonp'
      success: (data) ->
        mount_stat = data[mount]
        now_playing.text mount_stat.title
        listeners.text mount_stat.listeners
      error: (e) -> console.error(e)

We are setting jsonpCallback to parseMusic — this function should be generated for us by the XSL template. Inside the success callback we are then updating the text for the two blocks as needed.

我们将jsonpCallback设置为parseMusic —该功能应该由XSL模板为我们生成。 在success回调中,然后根据需要更新两个块的文本。

Of course, this data should be updated often, so let's set an interval:

当然,这些数据应该经常更新,因此让我们设置一个间隔:

# app/assets/javascripts/player.coffee
$ ->
    # ... variables and function
    player.get(0).play()
    setInterval updateMetaData, 5000

Now each 5 seconds the script will send an asynchronous GET request in order to update the information. Test it out by reloading the root page of the website.

The browser's console should have an output similar to this one:

现在,脚本每5秒钟将发送一个异步GET请求,以更新信息。 通过重新加载网站的根页面对其进行测试。

浏览器的控制台应具有与此类似的输出:

显示其他信息 (Displaying Additional Information)

"That's nice but I want even more info about the played song!", you might say. Well, let me show you a way to extract additional meta information about the track and display it later to the listener. For example, let's fetch the track's bitrate.

您可能会说:“很好,但是我想要有关已播放歌曲的更多信息!” 好吧,让我向您展示一种提取有关轨道的其他元信息并将其稍后显示给听众的方法。 例如,让我们获取轨道的比特率。

In order to do this we'll need two things:

为此,我们需要做两件事:

Firstly, add the gem:

首先,添加宝石:

# Gemfile
gem 'streamio-ffmpeg'

Note that this gem requires ffmpeg tool be present on your PC, so firstly download and install it. Next, install the gem itself:

请注意,此gem需要在计算机上安装ffmpeg工具,因此请首先下载并安装它 。 接下来,安装gem本身:

bundleinstall

Now enable a new Shrine plugin:

现在启用一个新的Shrine插件:

# config/initializers/shrine.rb
# ...
Shrine.plugin :add_metadata

Tweak the uploader to fetch the song's bitrate:

调整上传器以获取歌曲的比特率:

# app/uploaders/song_uploader.rb

class SongUploader < Shrine
  add_metadata do |io, context|
    song = FFMPEG::Movie.new(io.path)

    {
        bitrate: song.bitrate ? (song.bitrate / 1000) : 0
    }
  end
end

The hash returned by add_metadata will be properly added to the track_data column. This operation will be performed before the song is actually saved, so you do not need to do anything else.

add_metadata返回的哈希将正确添加到track_data列。 此操作将在实际保存歌曲之前执行,因此您无需执行其他任何操作。

Next tweak the worker a bit to add provide bitrate information:

接下来调整工作人员以添加提供比特率信息:

# app/workers/song_worker.rb
# ...
def perform(*_args)
    # ...
    open(song.track.url(public: true)) do |file|
        # ...
        m.add 'bitrate', song.track.metadata['bitrate'].to_s
    end
end

Don't forget to modify the XSL template:

不要忘记修改XSL模板:

<!-- installation_path/icecast/web/info.xsl -->

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output omit-xml-declaration="yes" method="text" indent="no" media-type="text/javascript" encoding="UTF-8"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/icestats">
parseMusic({
<xsl:for-each select="source">
"<xsl:value-of select="@mount"/>":
{
"server_name":"<xsl:value-of select="server_name"/>",
"listeners":"<xsl:value-of select="listeners"/>",
"description":"<xsl:value-of select="server_description" />",
"title":"<xsl:if test="artist"><xsl:value-of select="artist" /> - </xsl:if><xsl:value-of select="title" />",
"genre":"<xsl:value-of select="genre" />",
"bitrate":"<xsl:value-of select="bitrate" />"
}
<xsl:if test="position() != last()"><xsl:text>,</xsl:text></xsl:if>
</xsl:for-each>
});
</xsl:template></xsl:stylesheet>

Add a new span tag to the view:

在视图中添加一个新的span标签:

<!-- app/views/pages/index.html.erb -->

<div id="js-player-wrapper">
  <p>
    Now playing: <strong class="now-playing"></strong>
    Listeners: <span class="listeners"></span>
    Bitrate: <span class="bitrate"></span>
  </p>

  <audio id="js-player" controls>
    Your browser does not support the <code>audio</code> element. Seems like you are not a geek :(
  </audio>
</div>

Lastly, define a new variable and update bitrate information inside the success callback:

最后,在success回调中定义一个新变量并更新比特率信息:

# app/assets/javascripts/player.coffee
$ ->
    bitrate = wrapper.find('.bitrate')
    .ajax
        success: (data) ->
            mount_stat = data[mount]
            now_playing.text mount_stat.title
            listeners.text mount_stat.listeners
            bitrate.text mount_stat.bitrate

Upload a new track and its bitrate should now be displayed for you. This is it! You may use the described approach to provide any other information you like, for example, the track's duration or the album's name. Don't be afrid to experiment!

上传新曲目,现在应该为您显示其比特率。 就是这个! 您可以使用所描述的方法来提供您喜欢的任何其他信息,例如,曲目的持续时间或专辑的名称。 不要害怕尝试!

结论 ( Conclusion )

In this article we have covered lots of different topics and created our own online streaming radio powered by Rails and Icecast. You have seen how to:

在本文中,我们涵盖了许多不同的主题,并创建了由Rails和Icecast支持的自己的在线流广播。 您已经了解了如何:

  • Add file uploading feature powered by Shrine

    添加由Shrine提供支持的文件上传功能
  • Make Shrine work with Amazon S3

    使Shrine与Amazon S3一起使用
  • Install and configure Icecast as well as ruby-shout

    安装并配置Icecast以及ruby-shout
  • Create a background job powered by Sidekiq to stream our music

    创建由Sidekiq支持的后台作业以流式播放我们的音乐
  • Use ruby-shout to manage Icecast and perform streaming

    使用ruby-shout管理Icecast并执行流式传输
  • Display additional information about the currently played track using XSL and JSONP

    使用XSL和JSONP显示有关当前播放曲目的其他信息
  • Fetch more information about the uploaded song and display it to the user

    获取有关上传歌曲的更多信息并将其显示给用户

Some of these concepts may seem complex for beginners, so don't be surprised if something does not work for you right away (usually, all the installations cause the biggest pain). Don't hesitate to post your questions if you are stuck — together we will surely find a workaround.

对于初学者来说,其中一些概念可能看起来很复杂,因此,如果某些问题不能立即为您解决(通常,所有安装都会造成最大的痛苦),请不要感到惊讶。 如果您遇到问题,请随时提出您的问题-我们一定会找到解决方法。

I really hope this article was entertaining and useful for you. If you manage to create a public radio station following this guide, do share it with me. I thank you for reading this tutorial and happy coding!

我真的希望这篇文章对您有帮助,对您有帮助。 如果您按照本指南设法创建一个公共广播电台,请与我共享。 感谢您阅读本教程并编写愉快的代码!

翻译自: https://scotch.io/tutorials/creating-online-streaming-radio-with-rails-and-icecast

rails 创建

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值