Skip to content

Commit

Permalink
Support video tag and video thumbnail generation
Browse files Browse the repository at this point in the history
  • Loading branch information
TalLevAmi committed Mar 26, 2015
1 parent ca67939 commit 7757d21
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 12 deletions.
88 changes: 84 additions & 4 deletions cloudinary/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import os
import sys
import re

CF_SHARED_CDN = "d3jpl91pxevbkh.cloudfront.net"
OLD_AKAMAI_SHARED_CDN = "cloudinary-a.akamaihd.net"
Expand Down Expand Up @@ -78,15 +79,16 @@ def reset_config():
class CloudinaryResource(object):
def __init__(self, public_id = None, format = None, version = None,
signature = None, url_options = {}, metadata = None, type = None, resource_type = None,
default_resource_type="image"):
default_resource_type=None):
self.metadata = metadata
metadata = metadata or {}
self.public_id = public_id or metadata.get('public_id')
self.format = format or metadata.get('format')
self.version = version or metadata.get('version')
self.signature = signature or metadata.get('signature')
self.type = type or metadata.get('type') or "upload"
self.resource_type = resource_type or metadata.get('resource_type') or default_resource_type
self.resource_type = resource_type or metadata.get('resource_type')
self.default_resource_type = default_resource_type
self.url_options = url_options

def __unicode__(self):
Expand All @@ -101,14 +103,22 @@ def url(self):
return self.build_url(**self.url_options)

def __build_url(self, **options):
combined_options = dict(format = self.format, version = self.version, type = self.type, resource_type = self.resource_type)
combined_options = dict(format = self.format, version = self.version, type = self.type, resource_type = self.resource_type or self.default_resource_type)
combined_options.update(options)
return utils.cloudinary_url(self.public_id, **combined_options)
return utils.cloudinary_url(combined_options.get('public_id', self.public_id), **combined_options)

def build_url(self, **options):
return self.__build_url(**options)[0]

def default_poster_options(self, options):
options["format"] = options.get("format", "jpg")

def default_source_types(self):
return ['webm', 'mp4', 'ogv']

def image(self, **options):
if options.get("resource_type", self.resource_type or self.default_resource_type) == "video":
self.default_poster_options(options)
src, attrs = self.__build_url(**options)
responsive = attrs.pop("responsive", False)
hidpi = attrs.pop("hidpi", False)
Expand All @@ -124,6 +134,76 @@ def image(self, **options):

return u"<img {0}/>".format(utils.html_attrs(attrs))

def video_thumbnail(self, **options):
self.default_poster_options(options)
return self.build_url(**options)

# Creates an HTML video tag for the provided +source+
#
# ==== Options
# * <tt>source_types</tt> - Specify which source type the tag should include. defaults to webm, mp4 and ogv.
# * <tt>source_transformation</tt> - specific transformations to use for a specific source type.
# * <tt>poster</tt> - override default thumbnail:
# * url: provide an ad hoc url
# * options: with specific poster transformations and/or Cloudinary +:public_id+
#
# ==== Examples
# CloudinaryResource("mymovie.mp4").video()
# CloudinaryResource("mymovie.mp4").video(source_types = 'webm')
# CloudinaryResource("mymovie.ogv").video(poster = "myspecialplaceholder.jpg")
# CloudinaryResource("mymovie.webm").video(source_types = ['webm', 'mp4'], poster = {'effect': 'sepia'})
def video(self, **options):
public_id = options.get('public_id', self.public_id)
source = re.sub("\.{0}$".format("|".join(self.default_source_types())), '', public_id)

video_attributes = ['autoplay', 'controls', 'loop', 'muted', 'poster', 'preload', 'width', 'height']

source_types = options.pop('source_types', [])
source_transformation = options.pop('source_transformation', {})
fallback = options.pop('fallback_content', '')
options['resource_type'] = options.pop('resource_type', self.resource_type or self.default_resource_type or 'video')

if len(source_types) == 0: source_types = self.default_source_types()
video_options = options.copy()

if 'poster' in video_options:
poster_options = video_options['poster']
if isinstance(poster_options, dict):
if 'public_id' in poster_options:
video_options['poster'] = utils.cloudinary_url(poster_options['public_id'], **poster_options)[0]
else:
video_options['poster'] = self.video_thumbnail(public_id=source, **poster_options)
else:
video_options['poster'] = self.video_thumbnail(public_id=source, **options)

if not video_options['poster']: del video_options['poster']

html = '<video ';

nested_source_types = isinstance(source_types, list) and len(source_types) > 1
if not nested_source_types:
video_attributes.append('src')
source = source + '.' + utils.build_array(source_types)[0];

video_url = utils.cloudinary_url(source, **video_options)
video_options = video_url[1]
video_options['src'] = video_url[0]
if 'html_width' in video_options: video_options['width'] = video_options['html_width']
if 'html_height' in video_options: video_options['height'] = video_options['html_height']
html = html + utils.html_attrs(video_options, video_attributes ) + '>'

if nested_source_types:
for source_type in source_types:
transformation = options.copy()
transformation.update(source_transformation.get(source_type, {}))
src = utils.cloudinary_url(source, format = source_type, **transformation)[0]
video_type = "ogg" if source_type == 'ogv' else source_type
mime_type = "video/" + video_type
html = html + '<source ' + utils.html_attrs({'src': src, 'type': mime_type}) + '>'
html = html + fallback
html = html + '</video>'
return html

class CloudinaryImage(CloudinaryResource):
def __init__(self, public_id=None, **kwargs):
super(CloudinaryImage, self).__init__(public_id=public_id, default_resource_type="image", **kwargs)
Expand Down
10 changes: 9 additions & 1 deletion cloudinary/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,8 +438,16 @@ def build_upload_params(**options):
"auto_tagging": options.get("auto_tagging") and float(options.get("auto_tagging"))}
return params

def __join_pair(key, value):
if value is None or value == "":
return None
elif value is True:
return key
else:
return u"{0}=\"{1}\"".format(key, value)

def html_attrs(attrs, only=None):
return ' '.join(sorted([u"{0}='{1}'".format(key, value) for key, value in attrs.items() if value and (only is None or key in only)]))
return ' '.join(sorted([__join_pair(key, value) for key, value in attrs.items() if only is None or key in only]))

def __safe_value(v):
if isinstance(v, (bool)):
Expand Down
14 changes: 7 additions & 7 deletions tests/image_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ def test_url(self):

def test_image(self):
"""should generate image """
self.assertEqual(self.image.image(), "<img src='http://res.cloudinary.com/test/image/upload/hello.png'/>")
self.assertEqual(self.image.image(), "<img src=\"http://res.cloudinary.com/test/image/upload/hello.png\"/>")

def test_image_unicode(self):
"""should generate image with unicode arguments """
self.assertEqual(self.image.image(alt=u"\ua000abcd\u07b4"), u"<img alt='\ua000abcd\u07b4' src='http://res.cloudinary.com/test/image/upload/hello.png'/>")
self.assertEqual(self.image.image(alt=u"\ua000abcd\u07b4"), u"<img alt=\"\ua000abcd\u07b4\" src=\"http://res.cloudinary.com/test/image/upload/hello.png\"/>")

def test_scale(self):
"""should accept scale crop and pass width/height to image tag """
self.assertEqual(self.image.image(crop='scale', width=100, height=100), "<img height='100' src='http://res.cloudinary.com/test/image/upload/c_scale,h_100,w_100/hello.png' width='100'/>")
self.assertEqual(self.image.image(crop="scale", width=100, height=100), "<img height=\"100\" src=\"http://res.cloudinary.com/test/image/upload/c_scale,h_100,w_100/hello.png\" width=\"100\"/>")

def test_validate(self):
"""should validate signature """
Expand All @@ -35,15 +35,15 @@ def test_validate(self):

def test_responsive_width(self):
"""should add responsive width transformation"""
self.assertEqual(self.image.image(responsive_width = True), "<img class='cld-responsive' data-src='http://res.cloudinary.com/test/image/upload/c_limit,w_auto/hello.png'/>")
self.assertEqual(self.image.image(responsive_width = True), "<img class=\"cld-responsive\" data-src=\"http://res.cloudinary.com/test/image/upload/c_limit,w_auto/hello.png\"/>")

def test_width_auto(self):
"""should support width=auto """
self.assertEqual(self.image.image(width = "auto", crop = "limit"), "<img class='cld-responsive' data-src='http://res.cloudinary.com/test/image/upload/c_limit,w_auto/hello.png'/>")
self.assertEqual(self.image.image(width = "auto", crop = "limit"), "<img class=\"cld-responsive\" data-src=\"http://res.cloudinary.com/test/image/upload/c_limit,w_auto/hello.png\"/>")

def test_dpr_auto(self):
"""should support dpr=auto """
self.assertEqual(self.image.image(dpr = "auto"), "<img class='cld-hidpi' data-src='http://res.cloudinary.com/test/image/upload/dpr_auto/hello.png'/>")
self.assertEqual(self.image.image(dpr = "auto"), "<img class=\"cld-hidpi\" data-src=\"http://res.cloudinary.com/test/image/upload/dpr_auto/hello.png\"/>")

if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()
151 changes: 151 additions & 0 deletions tests/video_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import cloudinary
from cloudinary import CloudinaryVideo
import unittest

VIDEO_UPLOAD_PATH = 'http://res.cloudinary.com/test123/video/upload/'
DEFAULT_UPLOAD_PATH = 'http://res.cloudinary.com/test123/image/upload/'

class VideoTest(unittest.TestCase):
def setUp(self):
cloudinary.config(cloud_name="test123", api_secret="1234")
self.video = CloudinaryVideo("movie")
def test_video_thumbail(self):
self.assertEqual(self.video.video_thumbnail(), VIDEO_UPLOAD_PATH + "movie.jpg")
self.assertEqual(self.video.video_thumbnail(width = 100), VIDEO_UPLOAD_PATH + "w_100/movie.jpg")

def test_video_image_tag(self):
expected_url = VIDEO_UPLOAD_PATH + "movie.jpg"
self.assertEqual(self.video.image(), "<img src=\"" + expected_url + "\"/>")

expected_url = VIDEO_UPLOAD_PATH + "w_100/movie.jpg"
self.assertEqual(self.video.image(width= 100),
"<img src=\"" + expected_url + "\" width=\"100\"/>")

def test_video_tag(self):
""" default """
expected_url = VIDEO_UPLOAD_PATH + "movie"
self.assertEqual(self.video.video(), "<video poster=\"" + expected_url + ".jpg\">" +
"<source src=\"" + expected_url + ".webm\" type=\"video/webm\">" +
"<source src=\"" + expected_url + ".mp4\" type=\"video/mp4\">" +
"<source src=\"" + expected_url + ".ogv\" type=\"video/ogg\">" +
"</video>")

def test_video_tag_with_attributes(self):
""" test video attributes """
expected_url = VIDEO_UPLOAD_PATH + "movie"
self.assertEqual(self.video.video(autoplay = 1, controls = True, loop = True, muted = "true", preload = True),
"<video autoplay=\"1\" controls loop muted=\"true\" poster=\"" + expected_url + ".jpg\" preload>" +
"<source src=\"" + expected_url + ".webm\" type=\"video/webm\">" +
"<source src=\"" + expected_url + ".mp4\" type=\"video/mp4\">" +
"<source src=\"" + expected_url + ".ogv\" type=\"video/ogg\">" +
"</video>")

def test_video_tag_with_transformation(self):
""" test video attributes """
options = {
'source_types': "mp4",
'html_height' : "100",
'html_width' : "200",
'video_codec' : {'codec': 'h264'},
'audio_codec' : 'acc',
'start_offset': 3
}
expected_url = VIDEO_UPLOAD_PATH + "ac_acc,so_3,vc_h264/movie"
self.assertEqual(self.video.video(**options),
"<video height=\"100\" poster=\"" + expected_url + ".jpg\" src=\"" + expected_url + ".mp4\" width=\"200\"></video>")

del options['source_types']
self.assertEqual(self.video.video(**options),
"<video height=\"100\" poster=\"" + expected_url + ".jpg\" width=\"200\">" +
"<source src=\"" + expected_url + ".webm\" type=\"video/webm\">" +
"<source src=\"" + expected_url + ".mp4\" type=\"video/mp4\">" +
"<source src=\"" + expected_url + ".ogv\" type=\"video/ogg\">" +
"</video>")

del options['html_height']
del options['html_width']
options['width'] = 250
expected_url = VIDEO_UPLOAD_PATH + "ac_acc,so_3,vc_h264,w_250/movie"
self.assertEqual(self.video.video(**options),
"<video poster=\"" + expected_url + ".jpg\" width=\"250\">" +
"<source src=\"" + expected_url + ".webm\" type=\"video/webm\">" +
"<source src=\"" + expected_url + ".mp4\" type=\"video/mp4\">" +
"<source src=\"" + expected_url + ".ogv\" type=\"video/ogg\">" +
"</video>")

expected_url = VIDEO_UPLOAD_PATH + "ac_acc,c_fit,so_3,vc_h264,w_250/movie"
options['crop'] = 'fit'
self.assertEqual(self.video.video(**options),
"<video poster=\"" + expected_url + ".jpg\">" +
"<source src=\"" + expected_url + ".webm\" type=\"video/webm\">" +
"<source src=\"" + expected_url + ".mp4\" type=\"video/mp4\">" +
"<source src=\"" + expected_url + ".ogv\" type=\"video/ogg\">" +
"</video>")

def test_video_tag_with_fallback(self):
expected_url = VIDEO_UPLOAD_PATH + "movie"
fallback = "<span id=\"spanid\">Cannot display video</span>"
self.assertEqual(self.video.video(fallback_content = fallback),
"<video poster=\"" + expected_url + ".jpg\">" +
"<source src=\"" + expected_url + ".webm\" type=\"video/webm\">" +
"<source src=\"" + expected_url + ".mp4\" type=\"video/mp4\">" +
"<source src=\"" + expected_url + ".ogv\" type=\"video/ogg\">" +
fallback +
"</video>")
self.assertEqual(self.video.video(fallback_content = fallback, source_types = "mp4"),
"<video poster=\"" + expected_url + ".jpg\" src=\"" + expected_url + ".mp4\">" +
fallback +
"</video>")


def test_video_tag_with_source_types(self):
expected_url = VIDEO_UPLOAD_PATH + "movie"
self.assertEqual(self.video.video(source_types = ['ogv', 'mp4']),
"<video poster=\"" + expected_url + ".jpg\">" +
"<source src=\"" + expected_url + ".ogv\" type=\"video/ogg\">" +
"<source src=\"" + expected_url + ".mp4\" type=\"video/mp4\">" +
"</video>")

def test_video_tag_with_source_transformation(self):
expected_url = VIDEO_UPLOAD_PATH + "q_50/w_100/movie"
expected_ogv_url = VIDEO_UPLOAD_PATH + "q_50/q_70,w_100/movie"
expected_mp4_url = VIDEO_UPLOAD_PATH + "q_50/q_30,w_100/movie"
self.assertEqual(self.video.video(width = 100, transformation = {'quality': 50},
source_transformation = {'ogv': {'quality': 70}, 'mp4': {'quality': 30}}),
"<video poster=\"" + expected_url + ".jpg\" width=\"100\">" +
"<source src=\"" + expected_url + ".webm\" type=\"video/webm\">" +
"<source src=\"" + expected_mp4_url + ".mp4\" type=\"video/mp4\">" +
"<source src=\"" + expected_ogv_url + ".ogv\" type=\"video/ogg\">" +
"</video>")

self.assertEqual(self.video.video(width = 100, transformation = {'quality': 50},
source_transformation = {'ogv': {'quality': 70}, 'mp4': {'quality': 30}},
source_types = ['webm', 'mp4']),
"<video poster=\"" + expected_url + ".jpg\" width=\"100\">" +
"<source src=\"" + expected_url + ".webm\" type=\"video/webm\">" +
"<source src=\"" + expected_mp4_url + ".mp4\" type=\"video/mp4\">" +
"</video>")

def test_video_tag_with_poster(self):
expected_url = VIDEO_UPLOAD_PATH + "movie"

expected_poster_url = 'http://image/somewhere.jpg'
self.assertEqual(self.video.video(poster = expected_poster_url, source_types = "mp4"),
"<video poster=\"" + expected_poster_url + "\" src=\"" + expected_url + ".mp4\"></video>")

expected_poster_url = VIDEO_UPLOAD_PATH + "g_north/movie.jpg"
self.assertEqual(self.video.video(poster = {'gravity': 'north'}, source_types = "mp4"),
"<video poster=\"" + expected_poster_url + "\" src=\"" + expected_url + ".mp4\"></video>")

expected_poster_url = DEFAULT_UPLOAD_PATH + "g_north/my_poster.jpg"
self.assertEqual(self.video.video(poster = {'gravity': 'north', 'public_id': 'my_poster', 'format': 'jpg'}, source_types = "mp4"),
"<video poster=\"" + expected_poster_url + "\" src=\"" + expected_url + ".mp4\"></video>")

self.assertEqual(self.video.video(poster = None, source_types = "mp4"),
"<video src=\"" + expected_url + ".mp4\"></video>")

self.assertEqual(self.video.video(poster = False, source_types = "mp4"),
"<video src=\"" + expected_url + ".mp4\"></video>")

if __name__ == '__main__':
unittest.main()

0 comments on commit 7757d21

Please sign in to comment.