Skip to content
This repository has been archived by the owner on Jan 30, 2023. It is now read-only.

Commit

Permalink
Trac #7298: Enable HTML5 video in the Sage Notebook
Browse files Browse the repository at this point in the history
The way to achieve this is via a newly introduced rich output type which
covers all video formats, in combination with a format and / or mimetype
argument to the Animation.show method.
  • Loading branch information
gagern committed Aug 26, 2015
1 parent 779e5a8 commit a8d97b7
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 8 deletions.
1 change: 1 addition & 0 deletions src/doc/en/reference/repl/index.rst
Expand Up @@ -77,6 +77,7 @@ Display Backend Infrastructure
sage/repl/rich_output/output_basic
sage/repl/rich_output/output_graphics
sage/repl/rich_output/output_graphics3d
sage/repl/rich_output/output_video
sage/repl/rich_output/output_catalog

sage/repl/rich_output/backend_base
Expand Down
Binary file added src/ext/doctest/rich_output/example.ogv
Binary file not shown.
81 changes: 74 additions & 7 deletions src/sage/plot/animate.py
Expand Up @@ -638,12 +638,54 @@ def _rich_repr_(self, display_manager, **kwds):
OutputImageGif container
"""
OutputImageGif = display_manager.types.OutputImageGif
if OutputImageGif not in display_manager.supported_output():
return
return display_manager.graphics_from_save(
self.save, kwds, '.gif', OutputImageGif)

def show(self, delay=20, iterations=0):
OutputVideoAny = display_manager.types.OutputVideoAny
supported = display_manager.supported_output()
format = kwds.pop("format", None)
mimetype = kwds.pop("mimetype", None)
if format is None and mimetype is not None:
import mimetypes
format = mimetypes.guess_extension(mimetype, strict=False)
if format is None:
raise ValueError("MIME type without associated extension")
else:
format = format.lstrip(".")
if format is None:
if OutputImageGif in supported:
format = "gif"
else:
return # No supported format could be guessed
suffix = None # we might want to translate from format to suffix
outputType = OutputVideoAny
if format == "gif":
outputType = OutputImageGif
if suffix is None: # may change this in some of the rules above
suffix = "." + format
if not outputType in supported:
return # Sorry, requested format is not supported
if outputType is not OutputVideoAny:
return display_manager.graphics_from_save(
self.save, kwds, suffix, outputType)

# Now we save for OutputVideoAny
filename = tmp_filename(ext=suffix)
attrs = { "autoplay": True, "controls": True, "loop": True }
iterations = kwds.get('iterations', 0)
if iterations:
attrs["loop"] = False
for k in attrs:
if k in kwds:
attrs[k] = kwds.pop(k)
if mimetype is None:
import mimetypes
mimetype = mimetypes.guess_type(filename, strict=False)[0]
if mimetype is None:
mimetype = 'video/' + format
self.save(filename, **kwds)
from sage.repl.rich_output.buffer import OutputBuffer
buf = OutputBuffer.from_file(filename)
return OutputVideoAny(buf, suffix, mimetype, attrs)

def show(self, delay=None, iterations=None, **kwds):
r"""
Show this animation immediately.
Expand All @@ -661,6 +703,11 @@ def show(self, delay=20, iterations=0):
- ``iterations`` -- integer (default: 0); number of
iterations of animation. If 0, loop forever.
- ``format`` - (default: gif) format to use for output.
- ``mimetype`` - (default: 'video/'+format) the mime type to be
used in an HTML5 video tag.
OUTPUT:
This method does not return anything. Use :meth:`save` if you
Expand Down Expand Up @@ -690,6 +737,19 @@ def show(self, delay=20, iterations=0):
sage: a.show(delay=50) # optional -- ImageMagick
You can also make use of the HTML5 video element in the Sage Notebook::
sage: a.show(format="webm") # optional -- ffmpeg
sage: a.show(mimetype="video/ogg") # optional -- ffmpeg
sage: a.show(format="webm", iterations=1, autoplay=False) # optional -- ffmpeg
TESTS:
Use of positional parameters is discouraged, will likely get
deprecated, but should still work for the time being::
sage: a.show(50, 3) # optional -- ImageMagick
.. note::
If you don't have ffmpeg or ImageMagick installed, you will
Expand All @@ -701,9 +761,16 @@ def show(self, delay=20, iterations=0):
See www.imagemagick.org and www.ffmpeg.org for more information.
"""

# Positional parameters for the sake of backwards compatibility
if delay is not None:
kwds.setdefault("delay", delay)
if iterations is not None:
kwds.setdefault("iterations", iterations)

from sage.repl.rich_output import get_display_manager
dm = get_display_manager()
dm.display_immediately(self, delay=delay, iterations=iterations)
dm.display_immediately(self, **kwds)

def _have_ffmpeg(self):
"""
Expand Down
5 changes: 5 additions & 0 deletions src/sage/repl/rich_output/backend_doctest.py
Expand Up @@ -137,6 +137,7 @@ def supported_output(self):
OutputImagePng, OutputImageGif, OutputImageJpg,
OutputImageSvg, OutputImagePdf, OutputImageDvi,
OutputSceneJmol, OutputSceneCanvas3d, OutputSceneWavefront,
OutputVideoAny
])

def displayhook(self, plain_text, rich_output):
Expand Down Expand Up @@ -275,5 +276,9 @@ def validate(self, rich_output):
assert rich_output.mtl.get().startswith('newmtl ')
elif isinstance(rich_output, OutputSceneCanvas3d):
assert rich_output.canvas3d.get().startswith('[{vertices:')
elif isinstance(rich_output, OutputVideoAny):
assert rich_output.ext
assert rich_output.mimetype
assert rich_output.video
else:
raise TypeError('rich_output type not supported')
13 changes: 12 additions & 1 deletion src/sage/repl/rich_output/backend_sagenb.py
Expand Up @@ -52,6 +52,7 @@
import os
import stat
from sage.misc.cachefunc import cached_method
from sage.misc.html import html
from sage.misc.temporary_file import graphics_filename
from sage.doctest import DOCTEST_MODE
from sage.repl.rich_output.backend_base import BackendBase
Expand Down Expand Up @@ -308,6 +309,7 @@ def supported_output(self):
OutputImagePdf, OutputImageSvg,
SageNbOutputSceneJmol,
OutputSceneCanvas3d,
OutputVideoAny,
])

def display_immediately(self, plain_text, rich_output):
Expand Down Expand Up @@ -361,6 +363,8 @@ def display_immediately(self, plain_text, rich_output):
rich_output.embed()
elif isinstance(rich_output, OutputSceneCanvas3d):
self.embed_image(rich_output.canvas3d, '.canvas3d')
elif isinstance(rich_output, OutputVideoAny):
self.embed_video(rich_output)
else:
raise TypeError('rich_output type not supported, got {0}'.format(rich_output))

Expand Down Expand Up @@ -400,4 +404,11 @@ def embed_image(self, output_buffer, file_ext):
output_buffer.save_as(filename)
world_readable(filename)


def embed_video(self, video_output):
filename = graphics_filename(ext=video_output.ext)
video_output.video.save_as(filename)
world_readable(filename)
html(video_output.html_fragment(
url='cell://' + filename,
link_attrs='class="file_link"',
))
4 changes: 4 additions & 0 deletions src/sage/repl/rich_output/output_catalog.py
Expand Up @@ -36,3 +36,7 @@
OutputSceneWavefront,
OutputSceneCanvas3d,
)

from .output_video import (
OutputVideoAny,
)
123 changes: 123 additions & 0 deletions src/sage/repl/rich_output/output_video.py
@@ -0,0 +1,123 @@
# -*- encoding: utf-8 -*-
r"""
Video Output Types
This module defines the rich output types for video formats.
"""

#*****************************************************************************
# Copyright (C) 2015 Martin von Gagern <Martin.vGagern@gmx.net>
#
# Distributed under the terms of the GNU General Public License (GPL)
# as published by the Free Software Foundation; either version 2 of
# the License, or (at your option) any later version.
# http://www.gnu.org/licenses/
#*****************************************************************************


import os

from sage.repl.rich_output.output_basic import OutputBase
from sage.repl.rich_output.buffer import OutputBuffer


class OutputVideoAny(OutputBase):

def __init__(self, video, ext, mimetype, attrs = {}):
"""
Video of any common video format
If a backend claims support for this class, then it should
accept files in common video formats and at least present
video controls to the user, or open a video player.
Due to the large number of video container formats, codecs,
possible bit rate requirements and so on, it might well be
that the video still won't be played, but the user should at
least see a useful message about what is going on.
INPUT:
- ``video`` --
:class:`~sage.repl.rich_output.buffer.OutputBuffer`.
The video data.
- ``ext`` -- string. The file name extension for this video format.
- ``mimetype`` -- string. The MIME type of the video format.
- ``attrs`` -- dict. Attributes for a ``<video>`` tag in HTML.
Keys are strings, and values either strings or boolean values.
EXAMPLES::
sage: from sage.repl.rich_output.output_catalog import OutputVideoAny
sage: OutputVideoAny.example() # indirect doctest
OutputVideoAny container
"""
assert isinstance(video, OutputBuffer)
self.video = video
self.ext = ext
self.mimetype = mimetype
self.attrs = attrs

@classmethod
def example(cls):
r"""
Construct a sample video output container
This static method is meant for doctests, so they can easily
construct an example.
OUTPUT:
An instance of :class:`OutputVideoAny`.
EXAMPLES::
sage: from sage.repl.rich_output.output_catalog import OutputVideoAny
sage: OutputVideoAny.example()
OutputVideoAny container
sage: OutputVideoAny.example().video
buffer containing 5540 bytes
sage: OutputVideoAny.example().ext
'.ogv'
sage: OutputVideoAny.example().mimetype
'video/ogg'
"""
from sage.env import SAGE_EXTCODE
filename = os.path.join(SAGE_EXTCODE, 'doctest', 'rich_output', 'example.ogv')
return cls(OutputBuffer.from_file(filename), '.ogv', 'video/ogg',
{'controls': True})

def html_fragment(self, url, link_attrs=''):
r"""
Construct a HTML fragment for embedding this video
INPUT:
- ``url`` -- string. The URL where the data of this video can be found.
- ``link_attrs`` -- string. Can be used to style the fallback link
which is presented to the user if the video is not supported.
EXAMPLES::
sage: from sage.repl.rich_output.output_catalog import OutputVideoAny
sage: print(OutputVideoAny.example().html_fragment
....: ('foo', 'class="bar"').replace('><','>\n<'))
<video controls="controls">
<source src="foo" type="video/ogg" />
<p>
<a target="_new" href="foo" class="bar">Download video/ogg video</a>
</p>
</video>
"""
attrs = dict((k, (k if v is True else v))
for k, v in self.attrs.iteritems()
if v is not False)
attrs = ''.join(' {}="{}"'.format(k, v) for k, v in attrs.iteritems())
return ('<video{attrs}>'
'<source src="{url}" type="{mimetype}" /><p>'
'<a target="_new" href="{url}" {link_attrs}>'
'Download {mimetype} video</a></p></video>'
).format(url=url,
mimetype=self.mimetype,
attrs=attrs,
link_attrs=link_attrs,
)

0 comments on commit a8d97b7

Please sign in to comment.