samvera_hls 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +16 -0
- data/README.md +63 -0
- data/Rakefile +35 -0
- data/app/assets/javascripts/browser_notification.js +39 -0
- data/app/assets/javascripts/embed.js +18 -0
- data/app/assets/javascripts/hls_initialize.js.erb +22 -0
- data/app/assets/javascripts/include_player.js +17 -0
- data/app/assets/stylesheets/embed.css +16 -0
- data/app/assets/stylesheets/hydra-hls-styles.scss +20 -0
- data/app/assets/stylesheets/samvera-hls-styles.scss +20 -0
- data/app/controllers/concerns/samvera_hls/file_sets_controller_behavior.rb +43 -0
- data/app/models/concerns/samvera_hls/file_set_behavior.rb +149 -0
- data/app/services/samvera_hls/hls_output_file_service.rb +10 -0
- data/app/views/_playlist.html.erb +7 -0
- data/app/views/curation_concerns/file_sets/embed.html.erb +15 -0
- data/app/views/curation_concerns/file_sets/media_display/_audio_hls.html.erb +5 -0
- data/app/views/curation_concerns/file_sets/media_display/_video_hls.html.erb +5 -0
- data/app/views/file_sets/embed.html.erb +15 -0
- data/app/views/file_sets/media_display/_audio_hls.html.erb +5 -0
- data/app/views/file_sets/media_display/_video_hls.html.erb +5 -0
- data/app/views/hyrax/file_sets/embed.html.erb +15 -0
- data/app/views/hyrax/file_sets/media_display/_audio_hls.html.erb +5 -0
- data/app/views/hyrax/file_sets/media_display/_video_hls.html.erb +5 -0
- data/config/hls.yml +49 -0
- data/config/routes.rb +7 -0
- data/lib/generators/samvera_hls/install/USAGE +8 -0
- data/lib/generators/samvera_hls/install/install_generator.rb +32 -0
- data/lib/generators/samvera_hls/install/templates/config/hls.yml.example +49 -0
- data/lib/generators/samvera_hls/install/templates/controllers/file_sets_controller.rb +6 -0
- data/lib/samvera_hls/derivatives/audio_derivatives.rb +8 -0
- data/lib/samvera_hls/derivatives/processors/audio.rb +36 -0
- data/lib/samvera_hls/derivatives/processors/media.rb +28 -0
- data/lib/samvera_hls/derivatives/processors/video.rb +37 -0
- data/lib/samvera_hls/derivatives/processors.rb +7 -0
- data/lib/samvera_hls/derivatives/video_derivatives.rb +10 -0
- data/lib/samvera_hls/derivatives.rb +6 -0
- data/lib/samvera_hls/engine.rb +6 -0
- data/lib/samvera_hls/version.rb +3 -0
- data/lib/samvera_hls.rb +12 -0
- data/lib/tasks/samvera_hls_tasks.rake +4 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +29 -0
- data/spec/dummy/config/application.rb +32 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +41 -0
- data/spec/dummy/config/environments/production.rb +79 -0
- data/spec/dummy/config/environments/test.rb +42 -0
- data/spec/dummy/config/initializers/assets.rb +11 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +56 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/test_app_templates/lib/generators/test_app_generator.rb +14 -0
- metadata +170 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 248a3ec8d2a9e81ce645daa12c8d3a8f71056f52
|
4
|
+
data.tar.gz: 25a3e187c1359fbe35c6ac746d8cf38f5b2e9b88
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c927805c5399255af71c80a29336654b63b25c8fec9011b18d1ed5126cd6e3c1cef31621759483560f0f17a09707392854eb9f3377c8e63a4d710874a88bba77
|
7
|
+
data.tar.gz: 81b8213a48f8c9e6bd25360de8dec1dad4909b15b4fd68ddee74dbb67fdefb700982171c505d2f79a88bbaddd92c8b4341549da4bb1ec3413ae2b0416fa33de0
|
data/LICENSE
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
##########################################################################
|
2
|
+
#
|
3
|
+
# Copyright 2016 University of California Santa Cruz Library Digital Initiatives Department
|
4
|
+
# Additional copyright may be held by others, as reflected in the commit log
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
|
2
|
+
## Synopsis
|
3
|
+
|
4
|
+
This gem allows a samvera application to create hls derivative media files automatically after uploading audiovisual media files, and to display those to a user through hls adaptive streaming.
|
5
|
+
|
6
|
+
## Motivation
|
7
|
+
|
8
|
+
Samvera is a promising digital asset management solution with a growing feature set, and we wanted to use as a streaming media service for audiovisual collections. One Samvera based application - [Avalon Media System](https://github.com/avalonmediasystem) - is designed specifically to support multimedia streaming. However, when this module was developed, Avalon was not as feature rich or as fully developed as [Sufia](https://github.com/samvera/sufia) (another Samvera "head") as an asset management system, and its developer community is relatively small. Avalon also relies on a clunky third party transcoding service (Matterhorn) and external streaming server (Red5 or Wowza), creating an unweildly and bug-prone software stack. We wanted the streaming power and flexiblity promised by Avalon, in a more flexible and feature-rich asset management system like Sufia. This gem aims to integrate all of the benefits of adaptive bitrate streaming into Sufia's existing simple, efficient system for creating multimedia derivative files.
|
9
|
+
|
10
|
+
## Security & tokens
|
11
|
+
|
12
|
+
This software is not a DRM solution. We do not claim or intend to offer protection against authorized clients storing streamed media and then using it in undesired ways. We only seek to limit streaming from our servers to authorized clients.
|
13
|
+
|
14
|
+
Streaming media presents a challenge in balancing speed with security. Media streaming requires many requests to a server, each for a small segment of the media file being played. Each of these requests must be individually authenticated to prevent access by unauthorized users. We want to give our Rails app full control over which users are authorized to view which files - but if we load our whole Rails app for each media request, we put a big load on the server and ruin our streaming media performance. On the other hand, if we don't authenticate requests for media segments (Avalon doesn't) we risk people creating their own playlist files linking directly to those segments and bypassing our authorization system entirely.
|
15
|
+
|
16
|
+
Instead, my Rails engine uses signed tokens (using a SHA-256 hash and secret key) which grant direct access to a specific file's media segments for a specified period of time. When an authorized client requests a media segment, the token is authenticated by a lightweight external script which does not need to load the entire Rails app. If a malicious client tries to link to this segment file, the link will break quickly when the token expires.
|
17
|
+
|
18
|
+
## Requirements
|
19
|
+
|
20
|
+
An apache server is required. This may or may not be the same server that serves your Rails app (through passenger).
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
###Install the gem
|
25
|
+
Add the following to your gem file:
|
26
|
+
|
27
|
+
```gem 'samvera_hls'```
|
28
|
+
|
29
|
+
then run the following from your rails app root directory:
|
30
|
+
|
31
|
+
```
|
32
|
+
bundle install
|
33
|
+
rails generate samvera_hls:install
|
34
|
+
```
|
35
|
+
|
36
|
+
### Update apache config
|
37
|
+
These instructions assume you have an apache server running.
|
38
|
+
Edit the appropriate configuration file for your apache server. For my setup on CentOS 7 using passenger, I added this code to the file `/etc/httpd/conf.d/10-sufia.conf` inside the VirtualHost directive .
|
39
|
+
Add the following lines, inserting a random secret key of your own and inserting the absolute path to your rails app.
|
40
|
+
```
|
41
|
+
SetEnv hls_token_secret "PUT_A_SECRET_KEY_HERE"
|
42
|
+
RewriteEngine on
|
43
|
+
RewriteMap hls_auth "prg:/RAILS_PATH/bin/hls_auth"
|
44
|
+
RewriteRule ^/stream/.*\.ts$ "${hls_auth:%{REQUEST_URI}}"
|
45
|
+
```
|
46
|
+
then restart your server
|
47
|
+
```sudo systemctl restart httpd```
|
48
|
+
|
49
|
+
### Create link to derivative folder
|
50
|
+
It is probably possible to create this link automatically, but I have not set that up yet. You need to create this symbolic link manually. The rewrite rule you created in the last step keeps this path from being accessible publicly. Note that the apache directive FollowSymLinks must be set for this to work properly.
|
51
|
+
```
|
52
|
+
sudo su - samverauser
|
53
|
+
ln -s /mnt/MY/DERIVATIVE/PATH /srv/MY/WEB/APP/public/stream
|
54
|
+
logout
|
55
|
+
```
|
56
|
+
|
57
|
+
## Usage
|
58
|
+
|
59
|
+
When you ingest new audio or visual files, hls derivatives ought to be created automatically along with the other derivative files. View partials are included for displaying these. And "Embed" action is created for file sets, allowing an endpoint for embedding these in other websites using iframes.
|
60
|
+
|
61
|
+
## License
|
62
|
+
|
63
|
+
[www.apache.org/licenses/LICENSE-2.0](Apache License, v2.0)
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
#require 'rdoc/task'
|
8
|
+
|
9
|
+
#RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
# rdoc.rdoc_dir = 'rdoc'
|
11
|
+
# rdoc.title = 'HydraHls'
|
12
|
+
# rdoc.options << '--line-numbers'
|
13
|
+
# rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
# rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
#end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
Bundler::GemHelper.install_tasks
|
21
|
+
|
22
|
+
Dir[File.join(File.dirname(__FILE__), 'tasks/**/*.rake')].each {|f| load f }
|
23
|
+
|
24
|
+
require 'rspec/core'
|
25
|
+
require 'rspec/core/rake_task'
|
26
|
+
|
27
|
+
desc "Run all specs in spec directory (excluding plugin specs)"
|
28
|
+
RSpec::Core::RakeTask.new(:spec => 'app:db:test:prepare')
|
29
|
+
|
30
|
+
task :default => :spec
|
31
|
+
|
32
|
+
load 'rails/tasks/statistics.rake'
|
33
|
+
|
34
|
+
Bundler::GemHelper.install_tasks
|
35
|
+
|
@@ -0,0 +1,39 @@
|
|
1
|
+
function isChrome(nAgt) {
|
2
|
+
return nAgt.indexOf('Chrome') != -1
|
3
|
+
}
|
4
|
+
|
5
|
+
function getMacVersion(nAgt) {
|
6
|
+
var macreg = /Mac OS X/;
|
7
|
+
if (macreg.test(nAgt)) {
|
8
|
+
//it is a mac
|
9
|
+
return /Mac OS X (10[\.\_\d]+)/.exec(nAgt)[1].replace(/_/g,".");
|
10
|
+
}
|
11
|
+
return false;
|
12
|
+
}
|
13
|
+
|
14
|
+
function versionCompare(left, right) {
|
15
|
+
if (typeof left + typeof right != 'stringstring')
|
16
|
+
return false;
|
17
|
+
var a = left.split('.')
|
18
|
+
, b = right.split('.')
|
19
|
+
, i = 0, len = Math.max(a.length, b.length);
|
20
|
+
|
21
|
+
for (; i < len; i++) {
|
22
|
+
if ((a[i] && !b[i] && parseInt(a[i]) > 0) || (parseInt(a[i]) > parseInt(b[i]))) {
|
23
|
+
return true;
|
24
|
+
} else if ((b[i] && !a[i] && parseInt(b[i]) > 0) || (parseInt(a[i]) < parseInt(b[i]))) {
|
25
|
+
return false;
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
return 0;
|
30
|
+
}
|
31
|
+
|
32
|
+
$(document).ready(function(){
|
33
|
+
var nAgt = navigator.userAgent;
|
34
|
+
if (isChrome(nAgt)) {
|
35
|
+
if(versionCompare("10.12",getMacVersion(nAgt)))
|
36
|
+
alert('Warning: our streaming media sometimes has problems displaying on your operating system when using the Chrome browser. If you experience problems viewing content, try using another browser such as Safari or Firefox, or update your operating system to the latest version of OS X.');
|
37
|
+
}
|
38
|
+
});
|
39
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into embed.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// compiled file.
|
9
|
+
//
|
10
|
+
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
|
11
|
+
// about supported directives.
|
12
|
+
//
|
13
|
+
//= require browser_notification
|
14
|
+
//= require jquery
|
15
|
+
//= require mediaelement-and-player
|
16
|
+
//= require mep-feature-playlist
|
17
|
+
//= require hls_initialize
|
18
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
$(document).ready(function(){
|
2
|
+
|
3
|
+
if(typeof isPlaylist == "undefined")
|
4
|
+
isPlaylist = false;
|
5
|
+
if(isPlaylist) {
|
6
|
+
$(".hls-media").mediaelementplayer({
|
7
|
+
audioWidth: '100%',
|
8
|
+
loop: true,
|
9
|
+
shuffle: false ,
|
10
|
+
playlist: true,
|
11
|
+
playlistposition: 'bottom',
|
12
|
+
features: ['playlistfeature', 'prevtrack', 'playpause', 'nexttrack', 'loop', 'shuffle', 'playlist', 'current', 'progress', 'duration', 'volume'],
|
13
|
+
flashName: "<%= asset_path("flashmediaelement.swf") %>"
|
14
|
+
});
|
15
|
+
} else {
|
16
|
+
$(".hls-media").mediaelementplayer({
|
17
|
+
audioWidth: '100%',
|
18
|
+
flashName: "<%= asset_path("flashmediaelement.swf") %>"
|
19
|
+
});
|
20
|
+
}
|
21
|
+
});
|
22
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into include_player.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// compiled file.
|
9
|
+
//
|
10
|
+
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
|
11
|
+
// about supported directives.
|
12
|
+
//
|
13
|
+
//= require browser_notification
|
14
|
+
//= require mediaelement-and-player
|
15
|
+
//= require mep-feature-playlist
|
16
|
+
//= require hls_initialize
|
17
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into embed.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any styles
|
10
|
+
* defined in the other CSS/SCSS files in this directory. It is generally better to create a new
|
11
|
+
* file per style scope.
|
12
|
+
*
|
13
|
+
*= require mediaelementplayer.min
|
14
|
+
*= require mep-feature-playlist
|
15
|
+
*= require samvera-hls-styles
|
16
|
+
*/
|
@@ -0,0 +1,20 @@
|
|
1
|
+
.mejs-container.hls-media.mejs-audio {
|
2
|
+
height:auto !important;
|
3
|
+
}
|
4
|
+
|
5
|
+
.mejs-container.hls-media {
|
6
|
+
clear:both;
|
7
|
+
min-height: 30px;
|
8
|
+
|
9
|
+
.mejs-layers { padding-top:2em; }
|
10
|
+
|
11
|
+
.mejs-playlist.mejs-layer {
|
12
|
+
position: relative !important;
|
13
|
+
ul.mejs { margin-top:0px !important;}
|
14
|
+
}
|
15
|
+
|
16
|
+
.mejs-controls {
|
17
|
+
bottom:auto;
|
18
|
+
top:0;
|
19
|
+
}
|
20
|
+
}
|
@@ -0,0 +1,20 @@
|
|
1
|
+
.mejs-container.hls-media.mejs-audio {
|
2
|
+
height:auto !important;
|
3
|
+
}
|
4
|
+
|
5
|
+
.mejs-container.hls-media {
|
6
|
+
clear:both;
|
7
|
+
min-height: 30px;
|
8
|
+
|
9
|
+
.mejs-layers { padding-top:2em; }
|
10
|
+
|
11
|
+
.mejs-playlist.mejs-layer {
|
12
|
+
position: relative !important;
|
13
|
+
ul.mejs { margin-top:0px !important;}
|
14
|
+
}
|
15
|
+
|
16
|
+
.mejs-controls {
|
17
|
+
bottom:auto;
|
18
|
+
top:0;
|
19
|
+
}
|
20
|
+
}
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module SamveraHls
|
2
|
+
module FileSetsControllerBehavior
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
# skip_authorize_resource :only => [:show,:embed,:master,:variant]
|
6
|
+
|
7
|
+
def embed
|
8
|
+
response.headers["X-FRAME-OPTIONS"] = "ALLOWALL"
|
9
|
+
fs = FileSet.find(params[:id])
|
10
|
+
@playlist_url = fs.hls_master_url
|
11
|
+
@media_partial = media_display_partial(fs)
|
12
|
+
render layout: false
|
13
|
+
end
|
14
|
+
|
15
|
+
def master
|
16
|
+
fs = FileSet.find(params[:id])
|
17
|
+
render inline: fs.hls_master_playlist(root_url), content_type: 'application/x-mpegurl'
|
18
|
+
end
|
19
|
+
|
20
|
+
def variant
|
21
|
+
fs = FileSet.find(params[:id])
|
22
|
+
render inline: fs.hls_segment_playlist(root_url,params[:format]), content_type: 'application/x-mpegurl'
|
23
|
+
end
|
24
|
+
|
25
|
+
def media_display_partial(file_set)
|
26
|
+
'curation_concerns/file_sets/media_display/' +
|
27
|
+
if file_set.image?
|
28
|
+
'image'
|
29
|
+
elsif file_set.video?
|
30
|
+
'video_hls'
|
31
|
+
elsif file_set.audio?
|
32
|
+
'audio_hls'
|
33
|
+
elsif file_set.pdf?
|
34
|
+
'pdf'
|
35
|
+
elsif file_set.office_document?
|
36
|
+
'office_document'
|
37
|
+
else
|
38
|
+
'default'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require "samvera_hls/derivatives/video_derivatives"
|
2
|
+
require "samvera_hls/derivatives/audio_derivatives"
|
3
|
+
|
4
|
+
module SamveraHls
|
5
|
+
module FileSetBehavior
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
def create_derivatives(filename)
|
9
|
+
case mime_type
|
10
|
+
when *self.class.audio_mime_types
|
11
|
+
hls_dir = File.join(derivative_dir,"hls")
|
12
|
+
create_audio_derivates filename, hls_dir
|
13
|
+
when *self.class.video_mime_types
|
14
|
+
hls_dir = File.join(derivative_dir,"hls")
|
15
|
+
create_video_derivates filename, hls_dir
|
16
|
+
else
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def hls_master_url
|
22
|
+
File.join("file_set",id,"hls.m3u8")
|
23
|
+
end
|
24
|
+
|
25
|
+
def hls_master_playlist root_url
|
26
|
+
type = mime_type.split('/')[0]
|
27
|
+
defaults = hls_config[type]["default"]
|
28
|
+
playlist = "#EXTM3U\n"
|
29
|
+
playlist << "#EXT-X-VERSION:6\n"
|
30
|
+
variants.each{|variant|
|
31
|
+
next if variant.blank?
|
32
|
+
path, filename = File.split(variant)
|
33
|
+
format = filename.gsub(".m3u8","")
|
34
|
+
options = defaults.merge(hls_config[type][format])
|
35
|
+
playlist << "#EXT-X-STREAM-INF:PROGRAM-ID=1,"
|
36
|
+
playlist << "BANDWIDTH=#{options["maxrate"]},"
|
37
|
+
playlist << "CODECS=\"#{options["codec_code"]}\","
|
38
|
+
playlist << "RESOLUTION=#{options["resolution"]}\n"
|
39
|
+
playlist << File.join(root_url,variant_url(format)) + "\n"
|
40
|
+
}
|
41
|
+
playlist
|
42
|
+
end
|
43
|
+
|
44
|
+
def hls_segment_playlist root_url, format
|
45
|
+
playlist = ""
|
46
|
+
File.open(segment_playlist_path(format),'r') {|file|
|
47
|
+
file.each_line do |line|
|
48
|
+
if line.include? ".ts" then
|
49
|
+
playlist << File.join(root_url,
|
50
|
+
segment_url_base,
|
51
|
+
timestamp.to_s,
|
52
|
+
token(line),
|
53
|
+
line).strip+"\n"
|
54
|
+
else
|
55
|
+
playlist << line.strip+"\n"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
}
|
59
|
+
playlist
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
module ClassMethods
|
64
|
+
|
65
|
+
def audio_mime_types
|
66
|
+
# audio/x-wave is the mime type that fits 0.6.0 returns for a wav file.
|
67
|
+
# audio/mpeg is the mime type that fits 0.6.0 returns for an mp3 file.
|
68
|
+
['audio/mp3', 'audio/mpeg', 'audio/wav', 'audio/x-wave', 'audio/x-wav', 'audio/ogg', 'audio/flac','audio/x-flac', 'audio/x-aiff', 'audio/aiff', ]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def segment_url_base
|
77
|
+
File.join("stream",derivative_url,"hls")
|
78
|
+
end
|
79
|
+
|
80
|
+
def variants
|
81
|
+
variants = Dir.glob(File.join(derivative_dir,"hls","*.m3u8"))
|
82
|
+
variants.map!{ |file|
|
83
|
+
name = File.split(file)[1]
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
def variant_url format
|
88
|
+
File.join("file_set",id,format,"variant.m3u8")
|
89
|
+
end
|
90
|
+
|
91
|
+
def create_audio_derivates filename, hls_dir
|
92
|
+
outputs = [
|
93
|
+
{ label: 'mp3', format: 'mp3', url: derivative_url('mp3') },
|
94
|
+
{ label: 'ogg', format: 'ogg', url: derivative_url('ogg') },
|
95
|
+
{ label: 'hls_hd', format: 'hls_hd',path: hls_dir},
|
96
|
+
{ label: 'hls', format: 'hls', path: hls_dir}]
|
97
|
+
SamveraHls::Derivatives::AudioDerivatives.create(filename,{:outputs => outputs})
|
98
|
+
end
|
99
|
+
|
100
|
+
def create_video_derivates filename, hls_dir
|
101
|
+
outputs = [{ label: :thumbnail, format: 'jpg', url: derivative_url('thumbnail') },
|
102
|
+
{ label: 'webm', format: 'webm', url: derivative_url('webm') },
|
103
|
+
{ label: 'mp4', format: 'mp4', url: derivative_url('mp4') },
|
104
|
+
{ label: 'hls_high', format: "hls_high", path: hls_dir},
|
105
|
+
{ label: 'hls_med', format: "hls_med", path: hls_dir},
|
106
|
+
{ label: 'hls_low', format: "hls_low", path: hls_dir}]
|
107
|
+
SamveraHls::Derivatives::VideoDerivatives.create(filename,{:outputs => outputs})
|
108
|
+
end
|
109
|
+
|
110
|
+
def token line
|
111
|
+
@token ||= Digest::SHA256.hexdigest("/" + File.join(segment_url_base,timestamp.to_s,line).strip[0...-9] + token_secret)
|
112
|
+
end
|
113
|
+
|
114
|
+
def timestamp
|
115
|
+
@timestamp ||= Time.now.to_i + 7200
|
116
|
+
end
|
117
|
+
|
118
|
+
def token_secret
|
119
|
+
@token_secret ||= ENV['hls_token_secret']
|
120
|
+
end
|
121
|
+
|
122
|
+
def hls_config
|
123
|
+
@hls_config ||= YAML.load_file(Rails.root.join('config','hls.yml'))
|
124
|
+
end
|
125
|
+
|
126
|
+
def segment_playlist_path format
|
127
|
+
File.join(derivative_dir,"hls",format+".m3u8")
|
128
|
+
end
|
129
|
+
|
130
|
+
def derivative_url(destination_name = nil)
|
131
|
+
if destination_name.nil?
|
132
|
+
@deriv_url ||= derivative_dir.gsub(CurationConcerns.config.derivatives_path,"")
|
133
|
+
else
|
134
|
+
path = derivative_path(destination_name)
|
135
|
+
URI("file://#{path}").to_s
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def derivative_dir
|
140
|
+
File.split(derivative_path("dummy"))[0]
|
141
|
+
end
|
142
|
+
|
143
|
+
def derivative_path destination_name
|
144
|
+
CurationConcerns::DerivativePath.derivative_path_for_reference(self, destination_name)
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'samvera/derivatives'
|
2
|
+
module SamveraHls
|
3
|
+
class HlsOutputFileService < CurationConcerns::PersistDerivatives
|
4
|
+
def self.call( directives,temp_dir)
|
5
|
+
hls_dir = directives[:path]
|
6
|
+
FileUtils.mkdir_p(hls_dir)
|
7
|
+
FileUtils.move(Dir.glob(temp_dir+"/*") , hls_dir)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
<audio controls="controls" class="hls-media" preload="auto">
|
2
|
+
<% file_sets.each do |file_set| %>
|
3
|
+
<source src="<%= File.join(request.base_url,"file_set",file_set.id,"hls.m3u8") %>" type="application/x-mpegurl"/>
|
4
|
+
<%end %>
|
5
|
+
Your browser does not support the audio tag.
|
6
|
+
</audio>
|
7
|
+
<script type="text/javascript">isPlaylist = true</script>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8" />
|
5
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
7
|
+
<meta title="media">
|
8
|
+
<%= javascript_include_tag "embed" %>
|
9
|
+
<%= stylesheet_link_tag "embed" %>
|
10
|
+
</head>
|
11
|
+
|
12
|
+
<body>
|
13
|
+
<%= render @media_partial %>
|
14
|
+
</body>
|
15
|
+
</html>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8" />
|
5
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
7
|
+
<meta title="media">
|
8
|
+
<%= javascript_include_tag "embed" %>
|
9
|
+
<%= stylesheet_link_tag "embed" %>
|
10
|
+
</head>
|
11
|
+
|
12
|
+
<body style="margin:0px;">
|
13
|
+
<%= render @media_partial %>
|
14
|
+
</body>
|
15
|
+
</html>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8" />
|
5
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
7
|
+
<meta title="media">
|
8
|
+
<%= javascript_include_tag "embed" %>
|
9
|
+
<%= stylesheet_link_tag "embed" %>
|
10
|
+
</head>
|
11
|
+
|
12
|
+
<body>
|
13
|
+
<%= render @media_partial %>
|
14
|
+
</body>
|
15
|
+
</html>
|
data/config/hls.yml
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
---
|
2
|
+
video:
|
3
|
+
hls_high:
|
4
|
+
bitrate: 6500000
|
5
|
+
maxrate: 6800000
|
6
|
+
vf: "scale=1280:-2"
|
7
|
+
resolution: "1280x720"
|
8
|
+
hls_med:
|
9
|
+
bitrate: 3500000
|
10
|
+
maxrate: 3600000
|
11
|
+
vf: "scale=960:-2"
|
12
|
+
resolution: "960x540"
|
13
|
+
hls_low:
|
14
|
+
r: 15
|
15
|
+
profile_v: baseline
|
16
|
+
level: 3.0
|
17
|
+
bitrate: 400000
|
18
|
+
maxrate: 420000
|
19
|
+
g: 45
|
20
|
+
segment_list_type: m3u8
|
21
|
+
vf: "scale=480:-2"
|
22
|
+
codec_code: "mp4a.40.2,avc1.42001e"
|
23
|
+
resolution: "480x360"
|
24
|
+
default:
|
25
|
+
r: 29.97
|
26
|
+
pix_fmt: yuv420p
|
27
|
+
vcodec: libx264
|
28
|
+
acodec: aac
|
29
|
+
profile_v: main
|
30
|
+
level: 3.1
|
31
|
+
codec_code: "mp4a.40.2,avc1.4d001f"
|
32
|
+
segment_time: 9
|
33
|
+
g: 90
|
34
|
+
vf: "scale=960:-2"
|
35
|
+
audio:
|
36
|
+
hls_hd:
|
37
|
+
bitrate: 128000
|
38
|
+
maxrate: 140000
|
39
|
+
hls:
|
40
|
+
bitrate: 64000
|
41
|
+
maxrate: 70000
|
42
|
+
default:
|
43
|
+
acodec: aac
|
44
|
+
flags: "-f segment -global_header"
|
45
|
+
segment_time: 9
|
46
|
+
segment_format: mpeg_ts
|
47
|
+
segment_list_type: m3u8
|
48
|
+
codec_code: "mp4a.40.2"
|
49
|
+
resolution: 300x50
|
data/config/routes.rb
ADDED