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

Commit

Permalink
Trac #7298: Implement output types for various video container formats
Browse files Browse the repository at this point in the history
Since Volker Braun is unhappy with a catch-all video format specifier, we
won't use that but instead have to provide an explicit list of formats.
Those formats are known to FFmpeg, so we can actually create animations in
these formats.  The magic verified by the doctest backend is based on the
magic file shipped with the file(1) command on most free systems.

The example files were created using the following commands:

a = animate([sin(x), -sin(x)], xmax=2*pi, figsize=[1,0.5], axes=False)
base = os.path.join(sage.env.SAGE_SRC,'ext','doctest','rich_output','example.')
for ext in 'ogv webm mp4 flv mkv avi wmv mov'.split():
    a.save(base + ext, delay=100)

For some file formats FFmpeg won't recreate the same file content, even if
the same commands are used to create the examples.
  • Loading branch information
gagern committed Aug 26, 2015
1 parent a8d97b7 commit f6e6412
Show file tree
Hide file tree
Showing 13 changed files with 246 additions and 78 deletions.
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 modified 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.
96 changes: 58 additions & 38 deletions src/sage/plot/animate.py
Expand Up @@ -637,53 +637,59 @@ def _rich_repr_(self, display_manager, **kwds):
sage: a._rich_repr_(dm) # optional -- ImageMagick
OutputImageGif container
"""
OutputImageGif = display_manager.types.OutputImageGif
OutputVideoAny = display_manager.types.OutputVideoAny

# Pop attributes for HTML5 <video> tag from kwds
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)

t = display_manager.types
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:
if t.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
suffix = None
outputType = None
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:
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 outputType is not OutputVideoAny:
if suffix is not None:
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
# 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 OutputVideoAny(buf, suffix, mimetype, attrs)
return outputType(buf, attrs)

def show(self, delay=None, iterations=None, **kwds):
r"""
Expand All @@ -698,15 +704,20 @@ def show(self, delay=None, iterations=None, **kwds):
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.
- ``autoplay`` - (default: True) whether to automatically
start playback e.g. in a browser interface.
- ``mimetype`` - (default: 'video/'+format) the mime type to be
used in an HTML5 video tag.
- ``controls`` - (default: True) whether to show playback
controls, e.g. in a browser interface.
OUTPUT:
Expand Down Expand Up @@ -739,10 +750,19 @@ def show(self, delay=None, iterations=None, **kwds):
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="ogg") # optional -- ffmpeg
sage: a.show(format="webm") # optional -- ffmpeg
sage: a.show(format="mp4") # optional -- ffmpeg
sage: a.show(format="webm", iterations=1, autoplay=False) # 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
Expand Down
45 changes: 40 additions & 5 deletions src/sage/repl/rich_output/backend_doctest.py
Expand Up @@ -137,7 +137,9 @@ def supported_output(self):
OutputImagePng, OutputImageGif, OutputImageJpg,
OutputImageSvg, OutputImagePdf, OutputImageDvi,
OutputSceneJmol, OutputSceneCanvas3d, OutputSceneWavefront,
OutputVideoAny
OutputVideoOgg, OutputVideoWebM, OutputVideoMp4,
OutputVideoFlash, OutputVideoMatroska, OutputVideoAvi,
OutputVideoWmv, OutputVideoQuicktime,
])

def displayhook(self, plain_text, rich_output):
Expand Down Expand Up @@ -247,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 @@ -276,9 +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, OutputVideoAny):
assert rich_output.ext
assert rich_output.mimetype
assert rich_output.video
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')
5 changes: 3 additions & 2 deletions src/sage/repl/rich_output/backend_sagenb.py
Expand Up @@ -57,6 +57,7 @@
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 @@ -309,7 +310,7 @@ def supported_output(self):
OutputImagePdf, OutputImageSvg,
SageNbOutputSceneJmol,
OutputSceneCanvas3d,
OutputVideoAny,
OutputVideoOgg, OutputVideoWebM, OutputVideoMp4,
])

def display_immediately(self, plain_text, rich_output):
Expand Down Expand Up @@ -363,7 +364,7 @@ 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):
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
9 changes: 8 additions & 1 deletion src/sage/repl/rich_output/output_catalog.py
Expand Up @@ -38,5 +38,12 @@
)

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

0 comments on commit f6e6412

Please sign in to comment.