Skip to content

Commit

Permalink
Trac #7298: use html5 video tag for animations
Browse files Browse the repository at this point in the history
This ticket is about adding support for creating Ogg Theora or WebM
videos from animation objects. The resulting video files are embedded
into the notebook using the HTML 5 video tag.

The show method of animations now works more like the one of Graphics
objects. Animations can now be embeddend in html fragments, for example
using html.table(). By default animations are still created as animated
GIFs. To get a modern video from an animation "a" use one of these:

{{{
a.show(mimetype="video/ogg")
a.show(format="webm")
}}}

The original ticket was designed to use libtheora and libogg from #7297
and only supported Theora. The currently attached branch builds on
FFmpeg and supports a variety of formats, including Theora and WebM.
Support for calling FFmpeg is already in place, so the focus here is on
user interface, mainly integration with the `show` method and using HTML
5 video tags.

URL: http://trac.sagemath.org/7298
Reported by: whuss
Ticket author(s): Martin von Gagern
Reviewer(s): Volker Braun
  • Loading branch information
Release Manager authored and vbraun committed Aug 26, 2015
2 parents 7fd6b64 + ea5dfe2 commit 06e8fe9
Show file tree
Hide file tree
Showing 14 changed files with 378 additions and 9 deletions.
1 change: 1 addition & 0 deletions src/doc/en/reference/repl/index.rst
Original file line number Diff line number Diff line change
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.avi
Binary file not shown.
Binary file added src/ext/doctest/rich_output/example.flv
Binary file not shown.
Binary file added src/ext/doctest/rich_output/example.mkv
Binary file not shown.
Binary file added src/ext/doctest/rich_output/example.mov
Binary file not shown.
Binary file added src/ext/doctest/rich_output/example.mp4
Binary file not shown.
Binary file added src/ext/doctest/rich_output/example.ogv
Binary file not shown.
Binary file added src/ext/doctest/rich_output/example.webm
Binary file not shown.
Binary file added src/ext/doctest/rich_output/example.wmv
Binary file not shown.
91 changes: 83 additions & 8 deletions src/sage/plot/animate.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,13 +637,55 @@ def _rich_repr_(self, display_manager, **kwds):
sage: a._rich_repr_(dm) # optional -- ImageMagick
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):
iterations = kwds.get('iterations', 0)
loop = (iterations == 0)

t = display_manager.types
supported = display_manager.supported_output()
format = kwds.pop("format", None)
if format is None:
if t.OutputImageGif in supported:
format = "gif"
else:
return # No supported format could be guessed
suffix = None
outputType = None
if format == "gif":
outputType = t.OutputImageGif
suffix = ".gif"
if format == "ogg":
outputType = t.OutputVideoOgg
if format == "webm":
outputType = t.OutputVideoWebM
if format == "mp4":
outputType = t.OutputVideoMp4
if format == "flash":
outputType = t.OutputVideoFlash
if format == "matroska":
outputType = t.OutputVideoMatroska
if format == "avi":
outputType = t.OutputVideoAvi
if format == "wmv":
outputType = t.OutputVideoWmv
if format == "quicktime":
outputType = t.OutputVideoQuicktime
if format is None:
raise ValueError("Unknown video format")
if outputType not in supported:
return # Sorry, requested format is not supported
if suffix is not None:
return display_manager.graphics_from_save(
self.save, kwds, suffix, outputType)

# Now we save for OutputVideoBase
filename = tmp_filename(ext=outputType.ext)
self.save(filename, **kwds)
from sage.repl.rich_output.buffer import OutputBuffer
buf = OutputBuffer.from_file(filename)
return outputType(buf, loop=loop)

def show(self, delay=None, iterations=None, **kwds):
r"""
Show this animation immediately.
Expand All @@ -656,11 +698,15 @@ def show(self, delay=20, iterations=0):
INPUT:
- ``delay`` -- (default: 20) delay in hundredths of a
second between frames
second between frames.
- ``iterations`` -- integer (default: 0); number of
iterations of animation. If 0, loop forever.
- ``format`` - (default: gif) format to use for output.
Currently supported formats are: gif,
ogg, webm, mp4, flash, matroska, avi, wmv, quicktime.
OUTPUT:
This method does not return anything. Use :meth:`save` if you
Expand Down Expand Up @@ -690,6 +736,28 @@ 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="ogg") # optional -- ffmpeg
sage: a.show(format="webm") # optional -- ffmpeg
sage: a.show(format="mp4") # optional -- ffmpeg
sage: a.show(format="webm", iterations=1) # optional -- ffmpeg
Other backends may support other file formats as well::
sage: a.show(format="flash") # optional -- ffmpeg
sage: a.show(format="matroska") # optional -- ffmpeg
sage: a.show(format="avi") # optional -- ffmpeg
sage: a.show(format="wmv") # optional -- ffmpeg
sage: a.show(format="quicktime") # 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 +769,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
40 changes: 40 additions & 0 deletions src/sage/repl/rich_output/backend_doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ def supported_output(self):
OutputImagePng, OutputImageGif, OutputImageJpg,
OutputImageSvg, OutputImagePdf, OutputImageDvi,
OutputSceneJmol, OutputSceneCanvas3d, OutputSceneWavefront,
OutputVideoOgg, OutputVideoWebM, OutputVideoMp4,
OutputVideoFlash, OutputVideoMatroska, OutputVideoAvi,
OutputVideoWmv, OutputVideoQuicktime,
])

def displayhook(self, plain_text, rich_output):
Expand Down Expand Up @@ -246,6 +249,14 @@ def validate(self, rich_output):
sage: backend.validate(dm.types.OutputSceneJmol.example())
sage: backend.validate(dm.types.OutputSceneWavefront.example())
sage: backend.validate(dm.types.OutputSceneCanvas3d.example())
sage: backend.validate(dm.types.OutputVideoOgg.example())
sage: backend.validate(dm.types.OutputVideoWebM.example())
sage: backend.validate(dm.types.OutputVideoMp4.example())
sage: backend.validate(dm.types.OutputVideoFlash.example())
sage: backend.validate(dm.types.OutputVideoMatroska.example())
sage: backend.validate(dm.types.OutputVideoAvi.example())
sage: backend.validate(dm.types.OutputVideoWmv.example())
sage: backend.validate(dm.types.OutputVideoQuicktime.example())
"""
if isinstance(rich_output, OutputPlainText):
pass
Expand Down Expand Up @@ -275,5 +286,34 @@ 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, OutputVideoOgg):
assert rich_output.video.get().startswith('OggS')
elif isinstance(rich_output, OutputVideoWebM):
data = rich_output.video.get()
assert data.startswith('\x1a\x45\xdf\xa3')
assert '\x42\x82\x84webm' in data
elif isinstance(rich_output, OutputVideoMp4):
data = rich_output.video.get()
assert data[4:8] == 'ftyp'
assert data.startswith('\0\0\0')
# See http://www.ftyps.com/
ftyps = [data[i:i+4] for i in range(8, ord(data[3]), 4)]
del ftyps[1] # version number, not an ftyp
expected = ['avc1', 'iso2', 'mp41', 'mp42']
assert any(i in ftyps for i in expected)
elif isinstance(rich_output, OutputVideoFlash):
assert rich_output.video.get().startswith('FLV\x01')
elif isinstance(rich_output, OutputVideoMatroska):
data = rich_output.video.get()
assert data.startswith('\x1a\x45\xdf\xa3')
assert '\x42\x82\x88matroska' in data
elif isinstance(rich_output, OutputVideoAvi):
data = rich_output.video.get()
assert data[:4] == 'RIFF' and data[8:12] == 'AVI '
elif isinstance(rich_output, OutputVideoWmv):
assert rich_output.video.get().startswith('\x30\x26\xb2\x75')
elif isinstance(rich_output, OutputVideoQuicktime):
data = rich_output.video.get()
assert data[4:12] == 'ftypqt ' or data[4:8] == 'moov'
else:
raise TypeError('rich_output type not supported')
14 changes: 13 additions & 1 deletion src/sage/repl/rich_output/backend_sagenb.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,12 @@
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
from sage.repl.rich_output.output_catalog import *
from sage.repl.rich_output.output_video import OutputVideoBase


def world_readable(filename):
Expand Down Expand Up @@ -308,6 +310,7 @@ def supported_output(self):
OutputImagePdf, OutputImageSvg,
SageNbOutputSceneJmol,
OutputSceneCanvas3d,
OutputVideoOgg, OutputVideoWebM, OutputVideoMp4,
])

def display_immediately(self, plain_text, rich_output):
Expand Down Expand Up @@ -361,6 +364,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, OutputVideoBase):
self.embed_video(rich_output)
else:
raise TypeError('rich_output type not supported, got {0}'.format(rich_output))

Expand Down Expand Up @@ -400,4 +405,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"',
))
11 changes: 11 additions & 0 deletions src/sage/repl/rich_output/output_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,14 @@
OutputSceneWavefront,
OutputSceneCanvas3d,
)

from .output_video import (
OutputVideoOgg,
OutputVideoWebM,
OutputVideoMp4,
OutputVideoFlash,
OutputVideoMatroska,
OutputVideoAvi,
OutputVideoWmv,
OutputVideoQuicktime,
)
Loading

0 comments on commit 06e8fe9

Please sign in to comment.