arthropod_hls_video_encoder 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []