cloudinary 1.17.0 → 1.20.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,43 @@
1
+ module Cloudinary
2
+ module Config
3
+ include BaseConfig
4
+
5
+ ENV_URL = "CLOUDINARY_URL"
6
+ SCHEME = "cloudinary"
7
+
8
+ def load_config_from_env
9
+ if ENV["CLOUDINARY_CLOUD_NAME"]
10
+ config_keys = ENV.keys.select! { |key| key.start_with? "CLOUDINARY_" }
11
+ config_keys -= ["CLOUDINARY_URL"] # ignore it when explicit options are passed
12
+ config_keys.each do |full_key|
13
+ conf_key = full_key["CLOUDINARY_".length..-1].downcase # convert "CLOUDINARY_CONFIG_NAME" to "config_name"
14
+ conf_val = ENV[full_key]
15
+ conf_val = conf_val == 'true' if %w[true false].include?(conf_val) # cast relevant boolean values
16
+ update(conf_key => conf_val)
17
+ end
18
+ elsif ENV[ENV_URL]
19
+ load_from_url(ENV[ENV_URL])
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def env_url
26
+ ENV_URL
27
+ end
28
+
29
+ def expected_scheme
30
+ SCHEME
31
+ end
32
+
33
+ def config_from_parsed_url(parsed_url)
34
+ {
35
+ "cloud_name" => parsed_url.host,
36
+ "api_key" => parsed_url.user,
37
+ "api_secret" => parsed_url.password,
38
+ "private_cdn" => !parsed_url.path.blank?,
39
+ "secure_distribution" => parsed_url.path[1..-1]
40
+ }
41
+ end
42
+ end
43
+ end
@@ -438,7 +438,7 @@ begin
438
438
  # @return [::SassC::Script::Value::String]
439
439
  def cloudinary_url(public_id, sass_options = {})
440
440
  options = {}
441
- sass_options.to_h.each { |k, v| options[k.value] = v.value }
441
+ sass_options.to_h.each { |k, v| options[k.value.to_sym] = v.value }
442
442
  url = Cloudinary::Utils.cloudinary_url(public_id.value, {:type => :asset}.merge(options))
443
443
  ::SassC::Script::Value::String.new("url(#{url})")
444
444
  end
@@ -42,6 +42,7 @@ class Cloudinary::Uploader
42
42
  :faces => Cloudinary::Utils.as_safe_bool(options[:faces]),
43
43
  :folder => options[:folder],
44
44
  :format => options[:format],
45
+ :filename_override => options[:filename_override],
45
46
  :headers => build_custom_headers(options[:headers]),
46
47
  :image_metadata => Cloudinary::Utils.as_safe_bool(options[:image_metadata]),
47
48
  :invalidate => Cloudinary::Utils.as_safe_bool(options[:invalidate]),
@@ -65,7 +66,8 @@ class Cloudinary::Uploader
65
66
  :unique_filename => Cloudinary::Utils.as_safe_bool(options[:unique_filename]),
66
67
  :upload_preset => options[:upload_preset],
67
68
  :use_filename => Cloudinary::Utils.as_safe_bool(options[:use_filename]),
68
- :accessibility_analysis => Cloudinary::Utils.as_safe_bool(options[:accessibility_analysis])
69
+ :accessibility_analysis => Cloudinary::Utils.as_safe_bool(options[:accessibility_analysis]),
70
+ :metadata => Cloudinary::Utils.encode_context(options[:metadata])
69
71
  }
70
72
  params
71
73
  end
@@ -272,6 +274,29 @@ class Cloudinary::Uploader
272
274
  return self.call_tags_api(nil, "remove_all", public_ids, options)
273
275
  end
274
276
 
277
+ # Populates metadata fields with the given values. Existing values will be overwritten.
278
+ #
279
+ # Any metadata-value pairs given are merged with any existing metadata-value pairs
280
+ # (an empty value for an existing metadata field clears the value).
281
+ #
282
+ # @param [Hash] metadata A list of custom metadata fields (by external_id) and the values to assign to each of them.
283
+ # @param [Array] public_ids An array of Public IDs of assets uploaded to Cloudinary.
284
+ # @param [Hash] options
285
+ # @option options [String] :resource_type The type of file. Default: image. Valid values: image, raw, video.
286
+ # @option options [String] :type The storage type. Default: upload. Valid values: upload, private, authenticated
287
+ # @return mixed a list of public IDs that were updated
288
+ # @raise [Cloudinary::Api:Error]
289
+ def self.update_metadata(metadata, public_ids, options = {})
290
+ self.call_api("metadata", options) do
291
+ {
292
+ timestamp: (options[:timestamp] || Time.now.to_i),
293
+ type: options[:type],
294
+ public_ids: Cloudinary::Utils.build_array(public_ids),
295
+ metadata: Cloudinary::Utils.encode_context(metadata)
296
+ }
297
+ end
298
+ end
299
+
275
300
  private
276
301
 
277
302
  def self.call_tags_api(tag, command, public_ids = [], options = {})
@@ -317,11 +342,13 @@ class Cloudinary::Uploader
317
342
  non_signable ||= []
318
343
 
319
344
  unless options[:unsigned]
320
- api_key = options[:api_key] || Cloudinary.config.api_key || raise(CloudinaryException, "Must supply api_key")
321
- api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise(CloudinaryException, "Must supply api_secret")
322
- params[:signature] = Cloudinary::Utils.api_sign_request(params.reject { |k, v| non_signable.include?(k) }, api_secret)
323
- params[:api_key] = api_key
345
+ api_key = options[:api_key] || Cloudinary.config.api_key || raise(CloudinaryException, "Must supply api_key")
346
+ api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise(CloudinaryException, "Must supply api_secret")
347
+ signature_algorithm = options[:signature_algorithm]
348
+ params[:signature] = Cloudinary::Utils.api_sign_request(params.reject { |k, v| non_signable.include?(k) }, api_secret, signature_algorithm)
349
+ params[:api_key] = api_key
324
350
  end
351
+ proxy = options[:api_proxy] || Cloudinary.config.api_proxy
325
352
  timeout = options.fetch(:timeout) { Cloudinary.config.to_h.fetch(:timeout, 60) }
326
353
 
327
354
  result = nil
@@ -331,7 +358,7 @@ class Cloudinary::Uploader
331
358
  headers['Content-Range'] = options[:content_range] if options[:content_range]
332
359
  headers['X-Unique-Upload-Id'] = options[:unique_upload_id] if options[:unique_upload_id]
333
360
  headers.merge!(options[:extra_headers]) if options[:extra_headers]
334
- RestClient::Request.execute(:method => :post, :url => api_url, :payload => params.reject { |k, v| v.nil? || v=="" }, :timeout => timeout, :headers => headers) do
361
+ RestClient::Request.execute(:method => :post, :url => api_url, :payload => params.reject { |k, v| v.nil? || v=="" }, :timeout => timeout, :headers => headers, :proxy => proxy) do
335
362
  |response, request, tmpresult|
336
363
  raise CloudinaryException, "Server returned unexpected status code - #{response.code} - #{response.body}" unless [200, 400, 401, 403, 404, 500].include?(response.code)
337
364
  begin
@@ -32,19 +32,34 @@ class Cloudinary::Utils
32
32
 
33
33
  PREDEFINED_VARS = {
34
34
  "aspect_ratio" => "ar",
35
+ "aspectRatio" => "ar",
35
36
  "current_page" => "cp",
37
+ "currentPage" => "cp",
36
38
  "face_count" => "fc",
39
+ "faceCount" => "fc",
37
40
  "height" => "h",
38
41
  "initial_aspect_ratio" => "iar",
42
+ "initialAspectRatio" => "iar",
43
+ "trimmed_aspect_ratio" => "tar",
44
+ "trimmedAspectRatio" => "tar",
39
45
  "initial_height" => "ih",
46
+ "initialHeight" => "ih",
40
47
  "initial_width" => "iw",
48
+ "initialWidth" => "iw",
41
49
  "page_count" => "pc",
50
+ "pageCount" => "pc",
42
51
  "page_x" => "px",
52
+ "pageX" => "px",
43
53
  "page_y" => "py",
54
+ "pageY" => "py",
44
55
  "tags" => "tags",
45
56
  "initial_duration" => "idu",
57
+ "initialDuration" => "idu",
46
58
  "duration" => "du",
47
- "width" => "w"
59
+ "width" => "w",
60
+ "illustration_score" => "ils",
61
+ "illustrationScore" => "ils",
62
+ "context" => "ctx"
48
63
  }
49
64
 
50
65
  SIMPLE_TRANSFORMATION_PARAMS = {
@@ -144,6 +159,16 @@ class Cloudinary::Utils
144
159
  LONG_URL_SIGNATURE_LENGTH = 32
145
160
  SHORT_URL_SIGNATURE_LENGTH = 8
146
161
 
162
+ UPLOAD_PREFIX = 'https://api.cloudinary.com'
163
+
164
+ ALGO_SHA1 = :sha1
165
+ ALGO_SHA256 = :sha256
166
+
167
+ ALGORITHM_SIGNATURE = {
168
+ ALGO_SHA1 => Digest::SHA1,
169
+ ALGO_SHA256 => Digest::SHA256,
170
+ }
171
+
147
172
  def self.extract_config_params(options)
148
173
  options.select{|k,v| URL_KEYS.include?(k)}
149
174
  end
@@ -309,16 +334,16 @@ class Cloudinary::Utils
309
334
  "if_" + normalize_expression(if_value) unless if_value.to_s.empty?
310
335
  end
311
336
 
312
- EXP_REGEXP = Regexp.new('(?<!\$)('+PREDEFINED_VARS.keys.join("|")+')'+'|('+CONDITIONAL_OPERATORS.keys.reverse.map { |k| Regexp.escape(k) }.join('|')+')(?=[ _])')
337
+ EXP_REGEXP = Regexp.new('(\$_*[^_ ]+)|(?<!\$)('+PREDEFINED_VARS.keys.join("|")+')'+'|('+CONDITIONAL_OPERATORS.keys.reverse.map { |k| Regexp.escape(k) }.join('|')+')(?=[ _])')
313
338
  EXP_REPLACEMENT = PREDEFINED_VARS.merge(CONDITIONAL_OPERATORS)
314
339
 
315
340
  def self.normalize_expression(expression)
316
341
  if expression.nil?
317
- ''
342
+ nil
318
343
  elsif expression.is_a?( String) && expression =~ /^!.+!$/ # quoted string
319
344
  expression
320
345
  else
321
- expression.to_s.gsub(EXP_REGEXP,EXP_REPLACEMENT).gsub(/[ _]+/, "_")
346
+ expression.to_s.gsub(EXP_REGEXP) { |match| EXP_REPLACEMENT[match] || match }.gsub(/[ _]+/, "_")
322
347
  end
323
348
  end
324
349
 
@@ -433,9 +458,9 @@ class Cloudinary::Utils
433
458
  params_to_sign.map{|k,v| [k.to_s, v.is_a?(Array) ? v.join(",") : v]}.reject{|k,v| v.nil? || v == ""}.sort_by(&:first).map{|k,v| "#{k}=#{v}"}.join("&")
434
459
  end
435
460
 
436
- def self.api_sign_request(params_to_sign, api_secret)
461
+ def self.api_sign_request(params_to_sign, api_secret, signature_algorithm = nil)
437
462
  to_sign = api_string_to_sign(params_to_sign)
438
- Digest::SHA1.hexdigest("#{to_sign}#{api_secret}")
463
+ hash("#{to_sign}#{api_secret}", signature_algorithm, :hexdigest)
439
464
  end
440
465
 
441
466
  # Returns a JSON array as String.
@@ -501,6 +526,7 @@ class Cloudinary::Utils
501
526
  use_root_path = config_option_consume(options, :use_root_path)
502
527
  auth_token = config_option_consume(options, :auth_token)
503
528
  long_url_signature = config_option_consume(options, :long_url_signature)
529
+ signature_algorithm = config_option_consume(options, :signature_algorithm)
504
530
  unless auth_token == false
505
531
  auth_token = Cloudinary::AuthToken.merge_auth_token(Cloudinary.config.auth_token, auth_token)
506
532
  end
@@ -545,7 +571,10 @@ class Cloudinary::Utils
545
571
  raise(CloudinaryException, "Must supply api_secret") if (secret.nil? || secret.empty?)
546
572
  to_sign = [transformation, sign_version && version, source_to_sign].reject(&:blank?).join("/")
547
573
  to_sign = fully_unescape(to_sign)
548
- signature = compute_signature(to_sign, secret, long_url_signature)
574
+ signature_algorithm = long_url_signature ? ALGO_SHA256 : signature_algorithm
575
+ hash = hash("#{to_sign}#{secret}", signature_algorithm)
576
+ signature = Base64.urlsafe_encode64(hash)
577
+ signature = "s--#{signature[0, long_url_signature ? LONG_URL_SIGNATURE_LENGTH : SHORT_URL_SIGNATURE_LENGTH ]}--"
549
578
  end
550
579
 
551
580
  prefix = unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution)
@@ -661,18 +690,31 @@ class Cloudinary::Utils
661
690
  prefix
662
691
  end
663
692
 
693
+ # Creates a base URL for the cloudinary api
694
+ #
695
+ # @param [Object] path Resource name
696
+ # @param [Hash] options Additional options
697
+ #
698
+ # @return [String]
699
+ def self.base_api_url(path, options = {})
700
+ cloudinary = options[:upload_prefix] || Cloudinary.config.upload_prefix || UPLOAD_PREFIX
701
+ cloud_name = options[:cloud_name] || Cloudinary.config.cloud_name || raise(CloudinaryException, 'Must supply cloud_name')
702
+
703
+ [cloudinary, 'v1_1', cloud_name, path].join('/')
704
+ end
705
+
664
706
  def self.cloudinary_api_url(action = 'upload', options = {})
665
- cloudinary = options[:upload_prefix] || Cloudinary.config.upload_prefix || "https://api.cloudinary.com"
666
- cloud_name = options[:cloud_name] || Cloudinary.config.cloud_name || raise(CloudinaryException, "Must supply cloud_name")
667
- resource_type = options[:resource_type] || "image"
668
- return [cloudinary, "v1_1", cloud_name, resource_type, action].join("/")
707
+ resource_type = options[:resource_type] || 'image'
708
+
709
+ base_api_url([resource_type, action], options)
669
710
  end
670
711
 
671
712
  def self.sign_request(params, options={})
672
713
  api_key = options[:api_key] || Cloudinary.config.api_key || raise(CloudinaryException, "Must supply api_key")
673
714
  api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise(CloudinaryException, "Must supply api_secret")
715
+ signature_algorithm = options[:signature_algorithm]
674
716
  params = params.reject{|k, v| self.safe_blank?(v)}
675
- params[:signature] = Cloudinary::Utils.api_sign_request(params, api_secret)
717
+ params[:signature] = api_sign_request(params, api_secret, signature_algorithm)
676
718
  params[:api_key] = api_key
677
719
  params
678
720
  end
@@ -736,6 +778,18 @@ class Cloudinary::Utils
736
778
  download_archive_url(options.merge(:target_format => "zip"))
737
779
  end
738
780
 
781
+ # Creates and returns a URL that when invoked creates an archive of a folder.
782
+ #
783
+ # @param [Object] folder_path Full path (from the root) of the folder to download.
784
+ # @param [Hash] options Additional options.
785
+ #
786
+ # @return [String]
787
+ def self.download_folder(folder_path, options = {})
788
+ resource_type = options[:resource_type] || "all"
789
+
790
+ download_archive_url(options.merge(:resource_type => resource_type, :prefixes => folder_path))
791
+ end
792
+
739
793
  def self.signed_download_url(public_id, options = {})
740
794
  aws_private_key_path = options[:aws_private_key_path] || Cloudinary.config.aws_private_key_path
741
795
  if aws_private_key_path
@@ -1137,23 +1191,51 @@ class Cloudinary::Utils
1137
1191
  REMOTE_URL_REGEX === url
1138
1192
  end
1139
1193
 
1140
- # Computes a short or long signature based on a message and secret
1141
- # @param [String] message The string to sign
1142
- # @param [String] secret A secret that will be added to the message when signing
1143
- # @param [Boolean] long_signature Whether to create a short or long signature
1144
- # @return [String] Properly formatted signature
1145
- def self.compute_signature(message, secret, long_url_signature)
1146
- combined_message_secret = message + secret
1194
+ # The returned url should allow downloading the backedup asset based on the version and asset id
1195
+ #
1196
+ # asset and version id are returned with resource(<PUBLIC_ID1>, { versions: true })
1197
+ #
1198
+ # @param [String] asset_id Asset identifier
1199
+ # @param [String] version_id Specific version of asset to download
1200
+ # @param [Hash] options Additional options
1201
+ #
1202
+ # @return [String] An url for downloading a file
1203
+ def self.download_backedup_asset(asset_id, version_id, options = {})
1204
+ params = Cloudinary::Utils.sign_request({
1205
+ :timestamp => (options[:timestamp] || Time.now.to_i),
1206
+ :asset_id => asset_id,
1207
+ :version_id => version_id
1208
+ }, options)
1209
+
1210
+ "#{Cloudinary::Utils.base_api_url("download_backup", options)}?#{Cloudinary::Utils.hash_query_params((params))}"
1211
+ end
1147
1212
 
1148
- algo, signature_length =
1149
- if long_url_signature
1150
- [Digest::SHA256, LONG_URL_SIGNATURE_LENGTH]
1151
- else
1152
- [Digest::SHA1, SHORT_URL_SIGNATURE_LENGTH]
1153
- end
1213
+ # Format date in a format accepted by the usage API (e.g., 31-12-2020) if
1214
+ # passed value is of type Date, otherwise return the string representation of
1215
+ # the input.
1216
+ #
1217
+ # @param [Date|Object] date
1218
+ # @return [String]
1219
+ def self.to_usage_api_date_format(date)
1220
+ if date.is_a?(Date)
1221
+ date.strftime('%d-%m-%Y')
1222
+ else
1223
+ date.to_s
1224
+ end
1225
+ end
1154
1226
 
1155
- "s--#{Base64.urlsafe_encode64(algo.digest(combined_message_secret))[0, signature_length]}--"
1227
+ # Computes hash from input string using specified algorithm.
1228
+ #
1229
+ # @param [String] input String which to compute hash from
1230
+ # @param [String|nil] signature_algorithm Algorithm to use for computing hash
1231
+ # @param [Symbol] hash_method Hash method applied to a signature algorithm (:digest or :hexdigest)
1232
+ #
1233
+ # @return [String] Computed hash value
1234
+ def self.hash(input, signature_algorithm = nil, hash_method = :digest)
1235
+ signature_algorithm ||= Cloudinary.config.signature_algorithm || ALGO_SHA1
1236
+ algorithm = ALGORITHM_SIGNATURE[signature_algorithm] || raise("Unsupported algorithm '#{signature_algorithm}'")
1237
+ algorithm.public_send(hash_method, input)
1156
1238
  end
1157
1239
 
1158
- private_class_method :compute_signature
1240
+ private_class_method :hash
1159
1241
  end
@@ -1,4 +1,4 @@
1
1
  # Copyright Cloudinary
2
2
  module Cloudinary
3
- VERSION = "1.17.0"
3
+ VERSION = "1.20.0"
4
4
  end
@@ -3,12 +3,33 @@ module CloudinaryHelper
3
3
  DEFAULT_POSTER_OPTIONS = { :format => 'jpg', :resource_type => 'video' }
4
4
  DEFAULT_SOURCE_TYPES = %w(webm mp4 ogv)
5
5
  DEFAULT_VIDEO_OPTIONS = { :resource_type => 'video' }
6
+ DEFAULT_SOURCES = [
7
+ {
8
+ :type => "mp4",
9
+ :codecs => "hev1",
10
+ :transformations => { :video_codec => "h265" }
11
+ },
12
+ {
13
+ :type => "webm",
14
+ :codecs => "vp9",
15
+ :transformations => { :video_codec => "vp9" }
16
+ },
17
+ {
18
+ :type => "mp4",
19
+ :transformations => { :video_codec => "auto" }
20
+ },
21
+ {
22
+ :type => "webm",
23
+ :transformations => { :video_codec => "auto" }
24
+ }
25
+ ]
6
26
 
7
27
  # Creates an HTML video tag for the provided +source+
8
28
  #
9
29
  # ==== Options
10
30
  # * <tt>:source_types</tt> - Specify which source type the tag should include. defaults to webm, mp4 and ogv.
11
31
  # * <tt>:source_transformation</tt> - specific transformations to use for a specific source type.
32
+ # * <tt>:sources</tt> - list of sources (overrides :source_types when present)
12
33
  # * <tt>:poster</tt> - override default thumbnail:
13
34
  # * url: provide an ad hoc url
14
35
  # * options: with specific poster transformations and/or Cloudinary +:public_id+
@@ -20,6 +41,17 @@ module CloudinaryHelper
20
41
  # cl_video_tag("mymovie.webm", :source_types => [:webm, :mp4], :poster => {:effect => 'sepia'}) do
21
42
  # content_tag( :span, "Cannot present video!")
22
43
  # end
44
+ # cl_video_tag("mymovie", :sources => [
45
+ # {
46
+ # :type => "mp4",
47
+ # :codecs => "hev1",
48
+ # :transformations => { :video_codec => "h265" }
49
+ # },
50
+ # {
51
+ # :type => "webm",
52
+ # :transformations => { :video_codec => "auto" }
53
+ # }
54
+ # ])
23
55
  def cl_video_tag(source, options = {}, &block)
24
56
  source = strip_known_ext(source)
25
57
  video_attributes = [:autoplay,:controls,:loop,:muted,:poster, :preload]
@@ -48,30 +80,12 @@ module CloudinaryHelper
48
80
  video_options[:poster] = cl_video_thumbnail_path(source, options)
49
81
  end
50
82
 
51
- source_transformation = options.delete(:source_transformation) || {}
52
- source_types = Array(options.delete(:source_types))
53
- fallback = (capture(&block) if block_given?) || options.delete(:fallback_content)
83
+ fallback = (capture(&block) if block_given?) || options.delete(:fallback_content)
54
84
 
55
- if source_types.size > 1
56
- cloudinary_tag(source, options) do |_source, tag_options|
57
- content_tag('video', tag_options.merge(video_options)) do
58
- source_tags = source_types.map do |type|
59
- transformation = source_transformation[type.to_sym] || {}
60
- cloudinary_tag("#{source}.#{type}", options.merge(transformation)) do |url, _tag_options|
61
- mime_type = "video/#{(type == 'ogv' ? 'ogg' : type)}"
62
- tag("source", :src => url, :type => mime_type)
63
- end
64
- end
65
- source_tags.push(fallback.html_safe) unless fallback.blank?
66
- safe_join(source_tags)
67
- end
68
- end
85
+ if options[:sources]
86
+ video_tag_from_sources(source, options, video_options, fallback)
69
87
  else
70
- transformation = source_transformation[source_types.first.to_sym] || {}
71
- video_options[:src] = cl_video_path("#{source}.#{source_types.first.to_sym}", transformation.merge(options))
72
- cloudinary_tag(source, options) do |_source, tag_options|
73
- content_tag('video', fallback, tag_options.merge(video_options))
74
- end
88
+ video_tag_from_source_types(source, options, video_options, fallback)
75
89
  end
76
90
  end
77
91
 
@@ -96,6 +110,66 @@ module CloudinaryHelper
96
110
  name.sub(/\.(#{DEFAULT_SOURCE_TYPES.join("|")})$/, '')
97
111
  end
98
112
 
113
+ private
114
+
115
+ def video_tag_from_source_types(source_name, options, video_options, fallback)
116
+ source_transformation = options.delete(:source_transformation) || {}
117
+ source_types = Array(options.delete(:source_types))
118
+
119
+ if source_types.size > 1
120
+ sources = source_types.map do |type|
121
+ {
122
+ :type => type,
123
+ :transformations => source_transformation[type.to_sym] || {}
124
+ }
125
+ end
126
+
127
+ generate_tag_from_sources(:source_name => source_name,
128
+ :sources => sources,
129
+ :options => options,
130
+ :video_options => video_options,
131
+ :fallback => fallback)
132
+ else
133
+ transformation = source_transformation[source_types.first.to_sym] || {}
134
+ video_options[:src] = cl_video_path("#{source_name}.#{source_types.first.to_sym}", transformation.merge(options))
135
+ cloudinary_tag(source_name, options) do |_source, tag_options|
136
+ content_tag('video', fallback, tag_options.merge(video_options))
137
+ end
138
+ end
139
+ end
140
+
141
+ def video_tag_from_sources(source_name, options, video_options, fallback)
142
+ sources = options.delete(:sources)
143
+
144
+ generate_tag_from_sources(:source_name => source_name,
145
+ :sources => sources,
146
+ :options => options,
147
+ :video_options => video_options,
148
+ :fallback => fallback)
149
+ end
150
+
151
+ def generate_tag_from_sources(params)
152
+ source_name, sources, options, video_options, fallback = params.values_at(:source_name, :sources, :options, :video_options, :fallback)
153
+
154
+ cloudinary_tag(source_name, options) do |_source, tag_options|
155
+ content_tag('video', tag_options.merge(video_options)) do
156
+ source_tags = sources.map do |source|
157
+ type = source[:type]
158
+ transformation = source[:transformations] || {}
159
+ cloudinary_tag("#{source_name}.#{type}", options.merge(transformation)) do |url, _tag_options|
160
+ mime_type = "video/#{(type == 'ogv' ? 'ogg' : type)}"
161
+ if source[:codecs]
162
+ codecs = source[:codecs].is_a?(Array) ? source[:codecs].join(", ") : source[:codecs]
163
+ mime_type = "#{mime_type}; codecs=#{codecs}"
164
+ end
165
+ tag("source", :src => url, :type => mime_type)
166
+ end
167
+ end
168
+ source_tags.push(fallback.html_safe) unless fallback.blank?
169
+ safe_join(source_tags)
170
+ end
171
+ end
172
+ end
99
173
  end
100
174
 
101
175