arthropod_hls_video_encoder 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8311820e563fed91a93536b6ef5defa75b560ab8d68d02bbcfc9ee32d851ea6c
4
+ data.tar.gz: 8fe7ad465fb6f41cba32b3f58d673e47b6703978b2c6f9e979749a4dcd41041d
5
+ SHA512:
6
+ metadata.gz: cd0f75b5521288300a26fbebcf17b2051381abe7629e601ac3ce3b670f559dcd89b3852ee069137d00fbb38975de1f66136c0074ffb08b6574ae23d678447a08
7
+ data.tar.gz: 104d79c41ca986d505a2bb911ea8aa3bcc74302b4200e40cf1b2fa028e3c4e1a2dd456ca69bec8dce7ae766b754bd345a318df474711f678401cb7351def542c
@@ -0,0 +1,2 @@
1
+ ffmpeg*
2
+ .byebug_history
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
@@ -0,0 +1,64 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ arthropod_hls_video_encoder (0.0.1)
5
+ arthropod (= 0.0.2)
6
+ aws-sdk-sqs
7
+ fog-aws
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ arthropod (0.0.2)
13
+ aws-sdk-sqs
14
+ aws-eventstream (1.0.3)
15
+ aws-partitions (1.251.0)
16
+ aws-sdk-core (3.84.0)
17
+ aws-eventstream (~> 1.0, >= 1.0.2)
18
+ aws-partitions (~> 1, >= 1.239.0)
19
+ aws-sigv4 (~> 1.1)
20
+ jmespath (~> 1.0)
21
+ aws-sdk-sqs (1.23.1)
22
+ aws-sdk-core (~> 3, >= 3.71.0)
23
+ aws-sigv4 (~> 1.1)
24
+ aws-sigv4 (1.1.0)
25
+ aws-eventstream (~> 1.0, >= 1.0.2)
26
+ builder (3.2.3)
27
+ byebug (11.0.1)
28
+ excon (0.70.0)
29
+ fog-aws (3.5.2)
30
+ fog-core (~> 2.1)
31
+ fog-json (~> 1.1)
32
+ fog-xml (~> 0.1)
33
+ ipaddress (~> 0.8)
34
+ fog-core (2.1.2)
35
+ builder
36
+ excon (~> 0.58)
37
+ formatador (~> 0.2)
38
+ mime-types
39
+ fog-json (1.2.0)
40
+ fog-core
41
+ multi_json (~> 1.10)
42
+ fog-xml (0.1.3)
43
+ fog-core
44
+ nokogiri (>= 1.5.11, < 2.0.0)
45
+ formatador (0.2.5)
46
+ ipaddress (0.8.3)
47
+ jmespath (1.4.0)
48
+ mime-types (3.3)
49
+ mime-types-data (~> 3.2015)
50
+ mime-types-data (3.2019.1009)
51
+ mini_portile2 (2.4.0)
52
+ multi_json (1.14.1)
53
+ nokogiri (1.10.7)
54
+ mini_portile2 (~> 2.4.0)
55
+
56
+ PLATFORMS
57
+ ruby
58
+
59
+ DEPENDENCIES
60
+ arthropod_hls_video_encoder!
61
+ byebug
62
+
63
+ BUNDLED WITH
64
+ 2.0.2
@@ -0,0 +1,78 @@
1
+ # Arthropod-hls-video-encoder
2
+
3
+ ## Installation
4
+
5
+ ```
6
+ gem install arthropod_hls_video_encoder
7
+ ```
8
+
9
+ ## Usage
10
+
11
+ Just run it with the required arguments.
12
+ ```shell
13
+ $ arthropod_hls_video_encoder -h
14
+
15
+ Usage: arthropod_hls_video_encoder [options]
16
+ -q, --queue [string] SQS queue name
17
+ -i, --access-key-id [string] AWS access key ID, default to the AWS_ACCESS_KEY_ID environment variable
18
+ -k, --secret-access-key [string] AWS secret access key, default to the AWS_SECRET_ACCESS_KEY environment variable
19
+ -r, --region [string] AWS region, default to the AWS_REGION environment variable
20
+ ```
21
+
22
+ Example of client side call:
23
+ ```ruby
24
+ result = Arthropod::Client.push(queue_name: "hls_video_encoder", body: {
25
+ video_url: "https://s3-#{ENV['S3_REGION']}.amazonaws.com/#{ENV['S3_BUCKET']}/#{medium.temporary_key}",
26
+ root_dir: Digest::SHA1.hexdigest("#{ENV["SECURE_UPLOADER_KEY"]}#{medium.uuid}").insert(3, '/'),
27
+ aws_access_key_id: ENV['S3_ACCESS_KEY_ID'],
28
+ aws_secret_access_key: ENV['S3_SECRET_ACCESS_KEY'],
29
+ region: ENV['S3_REGION'],
30
+ endpoint: ENV['S3_ENDPOINT'],
31
+ host: ENV['S3_HOST'],
32
+ bucket: ENV['S3_BUCKET'],
33
+ profiles: [
34
+ {
35
+ codec: "libx264",
36
+ bandwidth: 1500000,
37
+ resolution: 720,
38
+ name: 'high',
39
+ },
40
+ {
41
+ codec: "libx264",
42
+ bandwidth: 800000,
43
+ resolution: 720,
44
+ name: 'low'
45
+ }
46
+ ]
47
+ })
48
+ ```
49
+
50
+ * `video_url`: the URL of the video you want to transcode to HLS
51
+ * `root`: the destination directory in your bucket
52
+ * `aws_access_key_id`: an AWS access key to access your bucket
53
+ * `aws_secret_access_key`: an AWS secret access key to access your bucket
54
+ * `region`: the region of your bucket
55
+ * `endpoint`: the endpoint of your S3 instance if you have one (useful for Minio)
56
+ * `host`: the host of your S3 instance if you have one (useful for Minio)
57
+ * `bucket`: your bucket name
58
+ * `profiles`: the HLS profile you want to generate
59
+
60
+ The result object is a follow.
61
+
62
+ ```ruby
63
+ {
64
+ key: "[string]",
65
+ thumbnail_key: "[string]",
66
+ small_thumbnail_key: "[string]",
67
+ preview_key: "[string]",
68
+ duration: "[string]"
69
+ }
70
+ ```
71
+
72
+ * `key`: the key the root HLS file in your bucket
73
+ * `thumbnail_key`: the key of the auto-generated thumbnail (the thumbnail is taken at half the video time)
74
+ * `small_thumbnail_key`: same thing, but with a smaller thumbnail
75
+ * `preview_key`: a GIF preview of your video
76
+ * `duration`: the duration of the video
77
+
78
+ *Both the input and output are very opiniated and follow my needs*
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ lib = File.expand_path("../lib", __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "arthropod_hls_video_encoder/version"
6
+
7
+ Gem::Specification.new do |gem|
8
+ gem.name = "arthropod_hls_video_encoder"
9
+ gem.version = ArthropodHlsVideoEncoder::VERSION
10
+ gem.authors = ["Victor Goya"]
11
+ gem.email = ["goya.victor@gmail.com"]
12
+ gem.description = "HLS video encoder using Arthropod"
13
+ gem.summary = "HLS video encoder using Arthropod"
14
+
15
+ gem.files = `git ls-files -z`.split("\x0")
16
+ gem.executables = %w(arthropod_hls_video_encoder)
17
+ gem.require_paths = ["lib"]
18
+ gem.bindir = 'bin'
19
+
20
+ gem.licenses = ["MIT"]
21
+
22
+ gem.required_ruby_version = "~> 2.0"
23
+
24
+ gem.add_dependency 'arthropod', '= 0.0.2'
25
+ gem.add_dependency 'aws-sdk-sqs'
26
+ gem.add_dependency 'fog-aws'
27
+
28
+ gem.add_development_dependency "byebug"
29
+ end
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'aws-sdk-sqs'
5
+ require 'arthropod'
6
+ require 'arthropod_hls_video_encoder/encoder'
7
+
8
+ options = {}
9
+ OptionParser.new do |opts|
10
+ opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
11
+
12
+ opts.on("-q", "--queue [string]", "SQS queue name") do |q|
13
+ options[:queue] = q
14
+ end
15
+ opts.on("-i", "--access-key-id [string]", "AWS access key ID, default to the AWS_ACCESS_KEY_ID environment variable") do |i|
16
+ options[:access_key_id] = i
17
+ end
18
+ opts.on("-k", "--secret-access-key [string]", "AWS secret access key, default to the AWS_SECRET_ACCESS_KEY environment variable") do |k|
19
+ options[:secret_access_key] = k
20
+ end
21
+ opts.on("-r", "--region [string]", "AWS region, default to the AWS_REGION environment variable") do |r|
22
+ options[:region] = r
23
+ end
24
+ end.parse!
25
+
26
+ client = Aws::SQS::Client.new({
27
+ access_key_id: options[:access_key_id] || ENV["AWS_ACCESS_KEY_ID"],
28
+ secret_access_key: options[:secret_access_key] || ENV["AWS_SECRET_ACCESS_KEY"],
29
+ region: options[:region] || ENV["AWS_REGION"],
30
+ })
31
+
32
+ Arthropod::Server.pull(client: client, queue_name: options[:queue]) do |request|
33
+ ArthropodHlsVideoEncoder::Encoder.new({
34
+ video_url: request.body["video_url"],
35
+ root_dir: request.body["root_dir"],
36
+ aws_access_key_id: request.body["aws_access_key_id"],
37
+ aws_secret_access_key: request.body["aws_secret_access_key"],
38
+ region: request.body["region"],
39
+ endpoint: request.body["endpoint"],
40
+ host: request.body["host"],
41
+ bucket: request.body["bucket"],
42
+ profiles: request.body["profiles"]
43
+ }).perform!
44
+ end
@@ -0,0 +1,158 @@
1
+ require 'shellwords'
2
+ require 'json'
3
+ require 'securerandom'
4
+ require 'fog/aws'
5
+
6
+ module ArthropodHlsVideoEncoder
7
+ class Encoder
8
+ attr_reader :video_url, :aws_access_key_id, :aws_secret_access_key, :region, :endpoint, :host, :bucket, :profiles, :job_id, :root_dir
9
+
10
+ def initialize(video_url:, root_dir:, aws_access_key_id:, aws_secret_access_key:, region:, endpoint:, host:, bucket:, profiles:)
11
+ @video_url = video_url
12
+ @root_dir = root_dir
13
+ @aws_access_key_id = aws_access_key_id
14
+ @aws_secret_access_key = aws_secret_access_key
15
+ @region = region
16
+ @endpoint = endpoint
17
+ @host = host
18
+ @bucket = bucket
19
+ @profiles = profiles
20
+ @job_id = SecureRandom.uuid
21
+ end
22
+
23
+ def perform!
24
+ Dir.mktmpdir do |wdir|
25
+ @wdir = wdir
26
+
27
+ download_input!
28
+
29
+ {
30
+ key: perform_video_encoding!,
31
+ thumbnail_key: get_thumbnail!,
32
+ small_thumbnail_key: get_small_thumbnail!,
33
+ preview_key: get_preview!,
34
+ duration: get_duration!
35
+ }
36
+ end
37
+ end
38
+
39
+ def download_input!
40
+ unless File.exists? input_path
41
+ call_command("curl #{Shellwords.escape(video_url)} -s -o #{input_path}")
42
+ end
43
+ end
44
+
45
+ def perform_video_encoding!
46
+ # Reencode
47
+ ffmpeg_configurations = profiles.map { |profile| ffmpeg_configuration_for(profile) }.join(" ")
48
+
49
+ call_command("ffmpeg -i #{input_path} -pass 1 #{ffmpeg_configurations}")
50
+ call_command("ffmpeg -i #{input_path} -pass 2 #{ffmpeg_configurations}")
51
+
52
+ # create index file
53
+ indices = profiles.map do |profile|
54
+ [
55
+ "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=#{profile["bandwidth"]}",
56
+ "#{profile["name"]}_#{job_id}.m3u8"
57
+ ]
58
+ end
59
+ File.open("#{@wdir}/index.m3u8", 'w') { |f| f.write("#EXTM3U\n" + indices.flatten.join("\n")) }
60
+
61
+ # Upload to storage
62
+ Dir["#{@wdir}/*.{ts,m3u8}"].each do |path|
63
+ upload(path, "#{root_dir}/#{File.basename(path)}")
64
+ end
65
+
66
+ "#{root_dir}/index.m3u8"
67
+ end
68
+
69
+ def get_thumbnail!
70
+ call_command "ffmpeg -i #{input_path} -vcodec mjpeg -vframes 1 -filter:v scale=\"1080:-1\" -q:v 10 -an -f rawvideo -ss #{video_middle} #{thumbnail_path}"
71
+
72
+ "#{root_dir}/thumbnail.jpg".tap do |key|
73
+ upload(thumbnail_path, key)
74
+ end
75
+ end
76
+
77
+ def get_small_thumbnail!
78
+ call_command "ffmpeg -i #{input_path} -vcodec mjpeg -vframes 1 -filter:v scale=\"640:-1\" -q:v 10 -an -f rawvideo -ss #{video_middle} #{small_thumbnail_path}"
79
+
80
+ "#{root_dir}/small_thumbnail.jpg".tap do |key|
81
+ upload(small_thumbnail_path, key)
82
+ end
83
+ end
84
+
85
+ def get_preview!
86
+ call_command "ffmpeg -y -ss #{video_middle} -t 3 -i #{input_path} -vf fps=10,scale=320:-1:flags=lanczos,palettegen #{palette_path}"
87
+ call_command "ffmpeg -ss #{video_middle} -t 3 -i #{input_path} -i #{palette_path} -filter_complex \"fps=10,scale=320:-1:flags=lanczos[x];[x][1:v]paletteuse\" #{preview_path}"
88
+
89
+ "#{root_dir}/preview.gif".tap do |key|
90
+ upload(preview_path, key)
91
+ end
92
+ end
93
+
94
+ def get_duration!
95
+ output = JSON.parse(`ffprobe -of json -show_format_entry name -show_format #{input_path} -loglevel quiet`)
96
+ output["format"]["duration"].to_i
97
+ end
98
+
99
+ protected
100
+
101
+ def input_path
102
+ Shellwords.escape("#{@wdir}/input")
103
+ end
104
+
105
+ def thumbnail_path
106
+ Shellwords.escape("#{@wdir}/thumbnail.jpeg")
107
+ end
108
+
109
+ def small_thumbnail_path
110
+ Shellwords.escape("#{@wdir}/small_thumbnail.jpeg")
111
+ end
112
+
113
+ def preview_path
114
+ Shellwords.escape("#{@wdir}/preview.gif")
115
+ end
116
+
117
+ def palette_path
118
+ Shellwords.escape("#{@wdir}/palette.png")
119
+ end
120
+
121
+ def video_middle
122
+ Shellwords.escape(`ffmpeg -i #{input_path} 2>&1 | grep Duration | awk '{print $2}' | tr -d , | awk -F ':' '{print ($3+$2*60+$1*3600)/2}'`.chomp)
123
+ end
124
+
125
+ def call_command(command)
126
+ puts command
127
+ system(command)
128
+ raise if $?.to_i != 0
129
+ end
130
+
131
+ def ffmpeg_configuration_for(profile)
132
+ "-vcodec #{profile["codec"]} -acodec aac -strict -2 -q:a 5 -ac 1 -r 25 -profile:v baseline -vf scale='trunc(oh*a/2)*2:#{profile["resolution"]}' -preset slow -b:v #{profile["bandwidth"]} -maxrate #{profile["bandwidth"]} -pix_fmt yuv420p -flags -global_header -hls_time 10 -hls_list_size 0 #{@wdir}/#{profile["name"]}_#{job_id}.m3u8"
133
+ end
134
+
135
+ def storage
136
+ @storage ||= Fog::Storage.new({
137
+ provider: 'AWS',
138
+ aws_access_key_id: aws_access_key_id,
139
+ aws_secret_access_key: aws_secret_access_key,
140
+ region: region,
141
+ endpoint: endpoint,
142
+ host: host,
143
+ path_style: true
144
+ })
145
+ @storage.directories.get(bucket)
146
+ end
147
+
148
+ def upload(path, key)
149
+ open(path) do |file|
150
+ storage.files.create({
151
+ key: key,
152
+ body: file,
153
+ public: true
154
+ })
155
+ end.public_url
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,3 @@
1
+ module ArthropodHlsVideoEncoder
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: arthropod_hls_video_encoder
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Victor Goya
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-12-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: arthropod
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.0.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.0.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: aws-sdk-sqs
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: fog-aws
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: byebug
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: HLS video encoder using Arthropod
70
+ email:
71
+ - goya.victor@gmail.com
72
+ executables:
73
+ - arthropod_hls_video_encoder
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - Gemfile
79
+ - Gemfile.lock
80
+ - README.md
81
+ - arthropod_hls_video_encoder.gemspec
82
+ - bin/arthropod_hls_video_encoder
83
+ - lib/arthropod_hls_video_encoder/encoder.rb
84
+ - lib/arthropod_hls_video_encoder/version.rb
85
+ homepage:
86
+ licenses:
87
+ - MIT
88
+ metadata: {}
89
+ post_install_message:
90
+ rdoc_options: []
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '2.0'
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ requirements: []
104
+ rubygems_version: 3.0.3
105
+ signing_key:
106
+ specification_version: 4
107
+ summary: HLS video encoder using Arthropod
108
+ test_files: []